sdk

Radroots SDK and bindings
git clone https://radroots.dev/git/sdk.git
Log | Files | Refs | README

commit 2452accade2358dc65a38fb22f654d2eab60fd2e
parent a307d2d00aed4d37b846e6668cf482e44b3a60e4
Author: triesap <tyson@radroots.org>
Date:   Mon, 27 Jul 2026 08:02:11 +0000

architecture: add deviation and traceability controls

- Record both approved sequence deviations in a strict machine-readable ledger.
- Validate identifiers, statuses, step ranges, evidence, and local spec anchors.
- Add executable architecture checks with complete and incomplete fixtures.
- Install traceability and step-report templates for every remaining checkpoint.

Diffstat:
MAGENTS.md | 5++++-
MCONTRIBUTING.md | 8++++++--
Mdocs/implementation/DEVIATIONS.md | 51++++++++++++++++++++++++++++++++++++---------------
Adocs/implementation/STEP_REPORT_TEMPLATE.md | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adocs/implementation/TRACEABILITY.md | 20++++++++++++++++++++
Adocs/implementation/deviations.toml | 47+++++++++++++++++++++++++++++++++++++++++++++++
Atools/xtask/src/architecture.rs | 269+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtools/xtask/src/check.rs | 1+
Mtools/xtask/src/main.rs | 15++++++++++++++-
9 files changed, 455 insertions(+), 19 deletions(-)

diff --git a/AGENTS.md b/AGENTS.md @@ -15,7 +15,9 @@ overrides this file for its subtree. - Current source and tests are implementation evidence. They do not silently override `radroots.crates.release.v1`. - Record any evidence-based plan deviation in - `docs/implementation/DEVIATIONS.md` before proceeding. + `docs/implementation/deviations.toml`, following + `docs/implementation/DEVIATIONS.md`, before proceeding. Validate it with + `cargo xtask architecture`. ## Repository operating model @@ -77,6 +79,7 @@ overrides this file for its subtree. body and `- ` bullets for notable changes and validation. - If repository evidence proves a planned step obsolete or unsafe, record the evidence, affected specification anchor, disposition, and validation in + `docs/implementation/deviations.toml`, following `docs/implementation/DEVIATIONS.md`. A normative change also requires an approved decision record. - Do not publish crates or packages, create release tags, change registry diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md @@ -40,5 +40,9 @@ private checkout. Do not publish, tag, merge, or change registry ownership without explicit authorization. When current evidence proves a planned step obsolete or unsafe, follow -`docs/implementation/DEVIATIONS.md`. Record the evidence and affected spec -anchor before changing the plan; do not silently redefine the architecture. +`docs/implementation/DEVIATIONS.md` and validate the machine-readable ledger +with `cargo xtask architecture`. Complete +`docs/implementation/STEP_REPORT_TEMPLATE.md`, and keep +`docs/implementation/TRACEABILITY.md` aligned with durable requirements. +Record the evidence and affected spec anchor before changing the plan; do not +silently redefine the architecture. diff --git a/docs/implementation/DEVIATIONS.md b/docs/implementation/DEVIATIONS.md @@ -1,22 +1,43 @@ # Implementation deviations -This ledger records evidence-based deviations from -`radroots.crates.release.v1` implementation planning. It does not authorize a -change to the normative package architecture. +The machine-readable authority is [`deviations.toml`](deviations.toml). +Repository checks validate it on every architecture and full check lane. This +ledger records evidence-based changes to implementation planning; it does not +silently change `radroots.crates.release.v1`. -No deviations are currently recorded. +## Active records -## Required record +| ID | Affected steps | Approved disposition | +| --- | --- | --- | +| `RCRV1-DEV-001` | 015-023 | Preserve the existing standalone `lib` and `sdk` repositories; replace repository import/unification with independent qualification. | +| `RCRV1-DEV-002` | 249 | Pull only the facade scaffold forward to immediately after Step 014 in `sdk`; do not repeat it later. | -Every deviation entry must include: +## Record template -- a stable identifier and date; -- the affected plan step and normative specification anchor; -- current repository evidence proving the planned action obsolete or unsafe; -- the smallest safe disposition and any temporary compatibility boundary; -- validation performed and unresolved risk; -- the approving decision record when normative architecture changes. +Add one `[[deviation]]` table to `deviations.toml`: -Do not silently skip, merge, reorder, or broaden implementation steps. Keep -the repository at a known-good checkpoint and obtain approval for any -normative change before proceeding. +```toml +[[deviation]] +id = "RCRV1-DEV-NNN" +date = "YYYY-MM-DD" +status = "active" # active | closed | superseded +approval = "Explicit approving decision." +affected_steps = ["NNN"] +spec_anchors = ["docs/specs/<durable-spec>#<anchor>"] +source_evidence = ["Committed source evidence."] +replacement_action = "Smallest safe disposition." +verification = ["Command or review evidence."] +unresolved_risk = "none, or a concrete bounded risk" +normative_architecture_change = false +adr_required = false +closure_evidence = [] # omit while active; required when closed or superseded +``` + +Every field is mandatory except `closure_evidence` on active records. Spec +anchors must resolve inside `docs/specs/`; affected steps must be three-digit +IDs in 001-315. A normative architecture change needs explicit approval and +the appropriate ADR decision before the record can be accepted. + +Do not silently skip, merge, reorder, or broaden implementation steps. Keep a +red checkpoint uncommitted and mark the next step blocked until its evidence or +approval is complete. diff --git a/docs/implementation/STEP_REPORT_TEMPLATE.md b/docs/implementation/STEP_REPORT_TEMPLATE.md @@ -0,0 +1,58 @@ +# Commit-step report template + +Complete this record in the owning rolling-commit document after verification +and before the next handoff step begins. + +```text +Step: +Title: +Repository: +Branch: +Commit SHA: + +Spec anchors: +- ... + +Files changed: +- ... + +Behavior implemented: +- ... + +Tests and verification: +- command: + result: +- command: + result: + +Self-review: +- public API review: +- architecture-boundary review: +- error/secret review: +- feature/target review: +- documentation review: +- generated/lockfile diff review: + +Deviations: +- none +or +- RCRV1-DEV-NNN and evidence + +Unresolved issues: +- none +or +- ... + +Known pre-existing failures: +- none +or +- command, exact failure, evidence, and why it is outside this step + +Next-step safety: +- SAFE / BLOCKED +- reason: +``` + +A step is not complete without its commit SHA, exact command outcomes, +self-review, deviation disposition, and next-step safety decision. A blocked +step does not authorize later work. diff --git a/docs/implementation/TRACEABILITY.md b/docs/implementation/TRACEABILITY.md @@ -0,0 +1,20 @@ +# Release-v1 requirement traceability + +This matrix maps durable architecture requirements to implementation ownership +and verification. It adds no product requirements; the synchronized +`docs/specs/` bundle remains normative. + +| Durable requirement | Owning package or control | Handoff steps | Required evidence | +| --- | --- | --- | --- | +| Exactly 19 public packages with a 17/2 repository split | release policy and architecture catalog | 013, 015-026, 304-305 | Cargo-resolved graph report and exact allowlist validation | +| Public-only identity and separated signing/secrets | `radroots-identity`, `radroots-signing`, `radroots-secrets` | 052-054, 099-111, 147-155 | public API, feature, dependency, and redaction tests | +| One canonical `TradeId` | `radroots-event`, `radroots-trade` | 073-098 | compile/API inventory and trade conformance | +| Version-neutral protocol ownership | `radroots-protocol` and private generators | 055-064, 261-268 | contract vectors and generated freshness | +| Independent transport source/sink with extensible identity | `radroots-transport` and adapters | 112-134, 190-207 | transport conformance and forward-compatibility fixtures | +| Storage SPI with SQLite backend | `radroots-storage`, `radroots-storage-sqlite` | 156-189 | backend conformance, migration, recovery, and leakage gates | +| Shared sync engine and explicit lifecycle | `radroots-sync` | 208-225 | pull/push, idempotency, cancellation, and close tests | +| Safe SDK defaults and curated facade | `radroots-sdk`, `radroots` | 226-260 | clean-project package smoke tests and compile-time surface guards | +| Preview and implementation packages remain private | release policy and graph validator | 013, 023-026, 304-305 | private-closure and forbidden-edge fixtures | +| Package-realistic reproducible release | release tooling in both repositories | 295-315 | locked zero-diff package, extracted, local-registry, target, and coverage gates | +| Every first-party consumer migrates | downstream cutover matrix | 269-294 | discovered consumer inventory and canary results | +| Deviations remain explicit and reviewable | `docs/implementation/deviations.toml` | 014 and every affected step | `cargo xtask architecture` plus step report evidence | diff --git a/docs/implementation/deviations.toml b/docs/implementation/deviations.toml @@ -0,0 +1,47 @@ +schema_version = 1 +architecture_id = "radroots.crates.release.v1" + +[[deviation]] +id = "RCRV1-DEV-001" +date = "2026-07-27" +status = "active" +approval = "Explicit user correction dated 2026-07-27." +affected_steps = ["015", "016", "017", "018", "019", "020", "021", "022", "023"] +spec_anchors = [ + "docs/specs/radroots_crates_release_v1.md#repository-ownership", + "docs/specs/radroots_crates_release_v1.toml#repository_policy", +] +source_evidence = [ + "The final v1 specification allocates 17 public packages to radrootslabs/lib and 2 to radrootslabs/sdk.", + "Both existing repositories have independent histories, workspaces, lockfiles, remotes, and standalone release boundaries.", +] +replacement_action = "Retain the two existing standalone repositories; replace import and monorepo-unification work with independent workspace, lockfile, metadata, dependency, and release qualification." +verification = [ + "Both repository-local architecture validators resolve every spec anchor.", + "The synchronized architecture catalog enforces the exact 17/2 ownership partition.", +] +unresolved_risk = "Parent gitlinks cannot advance until the new standalone commits are public-remote reachable under separate authorization." +normative_architecture_change = false +adr_required = false +[[deviation]] +id = "RCRV1-DEV-002" +date = "2026-07-27" +status = "active" +approval = "Explicit user correction dated 2026-07-27." +affected_steps = ["249"] +spec_anchors = [ + "docs/specs/radroots_crates_release_v1.md#radroots", + "docs/specs/radroots_crates_release_v1.toml#repositories.sdk", +] +source_evidence = [ + "The final v1 specification assigns the radroots facade to the existing sdk repository.", + "The approved sequence requires radroots to be the first crate-surface mutation after architecture controls are green.", +] +replacement_action = "Scaffold radroots in the sdk repository immediately after Step 014, then execute Steps 250-260 in their original order without repeating the scaffold portion of Step 249." +verification = [ + "The sdk release policy reserves radroots as an approved local package while publication remains frozen.", + "The facade scaffold checkpoint must add radroots only to the sdk workspace and architecture policy.", +] +unresolved_risk = "The facade remains non-publishable until the package-realistic Step 305 enablement gate." +normative_architecture_change = false +adr_required = false diff --git a/tools/xtask/src/architecture.rs b/tools/xtask/src/architecture.rs @@ -0,0 +1,269 @@ +use std::{ + collections::BTreeSet, + fs, + path::{Component, Path}, +}; + +use serde::Deserialize; + +const DEVIATIONS_RELATIVE: &str = "docs/implementation/deviations.toml"; +const ARCHITECTURE_RELATIVE: &str = "docs/specs/radroots_crates_release_v1.toml"; +const ARCHITECTURE_ID: &str = "radroots.crates.release.v1"; + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct DeviationLedger { + schema_version: u16, + architecture_id: String, + deviation: Vec<DeviationRecord>, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct DeviationRecord { + id: String, + date: String, + status: String, + approval: String, + affected_steps: Vec<String>, + spec_anchors: Vec<String>, + source_evidence: Vec<String>, + replacement_action: String, + verification: Vec<String>, + unresolved_risk: String, + normative_architecture_change: bool, + adr_required: bool, + #[serde(default)] + closure_evidence: Vec<String>, +} + +#[derive(Debug, Deserialize)] +struct ArchitectureIdentity { + spec_id: String, +} + +pub fn validate(workspace_root: &Path) -> Result<(), String> { + let architecture_path = workspace_root.join(ARCHITECTURE_RELATIVE); + let architecture_raw = fs::read_to_string(&architecture_path) + .map_err(|error| format!("read {}: {error}", architecture_path.display()))?; + let architecture = toml::from_str::<ArchitectureIdentity>(&architecture_raw) + .map_err(|error| format!("parse {}: {error}", architecture_path.display()))?; + + let ledger_path = workspace_root.join(DEVIATIONS_RELATIVE); + let ledger_raw = fs::read_to_string(&ledger_path) + .map_err(|error| format!("read {}: {error}", ledger_path.display()))?; + validate_ledger(workspace_root, &architecture.spec_id, &ledger_raw) +} + +fn validate_ledger( + workspace_root: &Path, + expected_architecture_id: &str, + raw: &str, +) -> Result<(), String> { + let ledger = toml::from_str::<DeviationLedger>(raw) + .map_err(|error| format!("parse {DEVIATIONS_RELATIVE}: {error}"))?; + if ledger.schema_version != 1 { + return Err("deviation ledger schema_version must be 1".to_owned()); + } + if ledger.architecture_id != expected_architecture_id + || ledger.architecture_id != ARCHITECTURE_ID + { + return Err(format!( + "deviation ledger architecture_id {} must match {}", + ledger.architecture_id, expected_architecture_id + )); + } + + let mut ids = BTreeSet::new(); + for record in &ledger.deviation { + validate_record(workspace_root, record)?; + if !ids.insert(record.id.as_str()) { + return Err(format!("duplicate deviation id {}", record.id)); + } + } + Ok(()) +} + +fn validate_record(workspace_root: &Path, record: &DeviationRecord) -> Result<(), String> { + if !is_deviation_id(&record.id) { + return Err(format!("deviation id {} must use RCRV1-DEV-NNN", record.id)); + } + if !is_iso_date(&record.date) { + return Err(format!("deviation {} date must use YYYY-MM-DD", record.id)); + } + if !matches!(record.status.as_str(), "active" | "closed" | "superseded") { + return Err(format!( + "deviation {} status must be active, closed, or superseded", + record.id + )); + } + require_text(&record.id, "approval", &record.approval)?; + require_text(&record.id, "replacement_action", &record.replacement_action)?; + require_text(&record.id, "unresolved_risk", &record.unresolved_risk)?; + require_nonempty_list(&record.id, "affected_steps", &record.affected_steps)?; + require_nonempty_list(&record.id, "spec_anchors", &record.spec_anchors)?; + require_nonempty_list(&record.id, "source_evidence", &record.source_evidence)?; + require_nonempty_list(&record.id, "verification", &record.verification)?; + + for step in &record.affected_steps { + let valid = step.len() == 3 + && step.bytes().all(|byte| byte.is_ascii_digit()) + && step + .parse::<u16>() + .is_ok_and(|value| (1..=315).contains(&value)); + if !valid { + return Err(format!( + "deviation {} affected step {} must be in 001..315", + record.id, step + )); + } + } + for anchor in &record.spec_anchors { + validate_spec_anchor(workspace_root, &record.id, anchor)?; + } + if record.status == "active" && !record.closure_evidence.is_empty() { + return Err(format!( + "active deviation {} must not carry closure_evidence", + record.id + )); + } + if record.status != "active" { + require_nonempty_list(&record.id, "closure_evidence", &record.closure_evidence)?; + } + + let _ = (record.normative_architecture_change, record.adr_required); + Ok(()) +} + +fn validate_spec_anchor( + workspace_root: &Path, + deviation_id: &str, + anchor: &str, +) -> Result<(), String> { + let (relative, fragment) = anchor + .split_once('#') + .map_or((anchor, None), |(path, fragment)| (path, Some(fragment))); + if relative.trim().is_empty() || fragment.is_some_and(|value| value.trim().is_empty()) { + return Err(format!( + "deviation {deviation_id} has invalid spec anchor {anchor}" + )); + } + let path = Path::new(relative); + if path.components().any(|component| { + matches!( + component, + Component::ParentDir | Component::RootDir | Component::Prefix(_) + ) + }) { + return Err(format!( + "deviation {deviation_id} spec anchor must be repository-relative: {anchor}" + )); + } + if !relative.starts_with("docs/specs/") || !workspace_root.join(path).is_file() { + return Err(format!( + "deviation {deviation_id} spec anchor does not resolve to a local spec: {anchor}" + )); + } + Ok(()) +} + +fn require_text(deviation_id: &str, field: &str, value: &str) -> Result<(), String> { + if value.trim().is_empty() { + return Err(format!( + "deviation {deviation_id} field {field} must not be empty" + )); + } + Ok(()) +} + +fn require_nonempty_list(deviation_id: &str, field: &str, values: &[String]) -> Result<(), String> { + if values.is_empty() || values.iter().any(|value| value.trim().is_empty()) { + return Err(format!( + "deviation {deviation_id} field {field} must contain non-empty values" + )); + } + Ok(()) +} + +fn is_deviation_id(value: &str) -> bool { + value + .strip_prefix("RCRV1-DEV-") + .is_some_and(|suffix| suffix.len() == 3 && suffix.bytes().all(|byte| byte.is_ascii_digit())) +} + +fn is_iso_date(value: &str) -> bool { + value.len() == 10 + && value.as_bytes()[4] == b'-' + && value.as_bytes()[7] == b'-' + && value + .bytes() + .enumerate() + .all(|(index, byte)| matches!(index, 4 | 7) || byte.is_ascii_digit()) +} + +#[cfg(test)] +mod tests { + use std::{ + fs, + path::PathBuf, + time::{SystemTime, UNIX_EPOCH}, + }; + + use super::validate_ledger; + + fn test_root(label: &str) -> PathBuf { + let nonce = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("clock") + .as_nanos(); + let root = std::env::temp_dir().join(format!("radroots_architecture_{label}_{nonce}")); + fs::create_dir_all(root.join("docs/specs")).expect("create spec root"); + fs::write( + root.join("docs/specs/radroots_crates_release_v1.md"), + "# Architecture\n", + ) + .expect("write spec"); + root + } + + fn complete_ledger() -> &'static str { + r#"schema_version = 1 +architecture_id = "radroots.crates.release.v1" + +[[deviation]] +id = "RCRV1-DEV-001" +date = "2026-07-27" +status = "active" +approval = "Explicit user correction dated 2026-07-27." +affected_steps = ["015", "016"] +spec_anchors = ["docs/specs/radroots_crates_release_v1.md#repository-topology"] +source_evidence = ["The approved architecture assigns packages to the existing lib and sdk repositories."] +replacement_action = "Keep both standalone repositories and verify them independently." +verification = ["Repository-local architecture validation passes."] +unresolved_risk = "Remote publication remains separately authorized." +normative_architecture_change = false +adr_required = false +"# + } + + #[test] + fn accepts_complete_active_deviation() { + let root = test_root("complete"); + validate_ledger(&root, "radroots.crates.release.v1", complete_ledger()) + .expect("complete deviation"); + let _ = fs::remove_dir_all(root); + } + + #[test] + fn rejects_incomplete_active_deviation() { + let root = test_root("incomplete"); + let incomplete = complete_ledger().replace( + "spec_anchors = [\"docs/specs/radroots_crates_release_v1.md#repository-topology\"]", + "spec_anchors = []", + ); + let error = validate_ledger(&root, "radroots.crates.release.v1", &incomplete) + .expect_err("missing anchor must fail"); + assert!(error.contains("field spec_anchors must contain non-empty values")); + let _ = fs::remove_dir_all(root); + } +} diff --git a/tools/xtask/src/check.rs b/tools/xtask/src/check.rs @@ -117,6 +117,7 @@ struct CratesReleasePackage { pub fn check() -> Result<(), String> { validate_package_matrix()?; let root = workspace_root()?; + crate::architecture::validate(&root)?; check_publication_policy(&root)?; validate_sdk_contracts(&root)?; check_sdk_feature_matrix(&root)?; diff --git a/tools/xtask/src/main.rs b/tools/xtask/src/main.rs @@ -1,3 +1,4 @@ +mod architecture; mod check; mod cli_host; mod contracts; @@ -17,6 +18,7 @@ mod wasm; mod wasm_declarations; enum CommandAction<'a> { + Architecture, GenerateAll, GenerateTs, GenerateWasm(&'a [String]), @@ -36,6 +38,7 @@ fn main() { fn run(args: impl IntoIterator<Item = String>) -> Result<(), String> { let args = args.into_iter().collect::<Vec<_>>(); match command_action(&args)? { + CommandAction::Architecture => architecture::validate(&fs::workspace_root()?), CommandAction::GenerateAll => generate::generate_all(), CommandAction::GenerateTs => generate::generate_ts(), CommandAction::GenerateWasm(rest) => wasm::generate(rest), @@ -48,6 +51,7 @@ fn run(args: impl IntoIterator<Item = String>) -> Result<(), String> { fn command_action(args: &[String]) -> Result<CommandAction<'_>, String> { match args { + [command] if command == "architecture" => Ok(CommandAction::Architecture), [command] if command == "generate" => Ok(CommandAction::GenerateAll), [command, target] if command == "generate" && target == "ts" => { Ok(CommandAction::GenerateTs) @@ -67,7 +71,7 @@ fn command_action(args: &[String]) -> Result<CommandAction<'_>, String> { } fn usage() -> String { - "usage: cargo xtask generate | cargo xtask generate ts | cargo xtask generate wasm [--package <key>] | cargo xtask generate package-metadata | cargo xtask check | cargo xtask smoke knowledge-rust-local | cargo xtask coverage run" + "usage: cargo xtask architecture | cargo xtask generate | cargo xtask generate ts | cargo xtask generate wasm [--package <key>] | cargo xtask generate package-metadata | cargo xtask check | cargo xtask smoke knowledge-rust-local | cargo xtask coverage run" .to_owned() } @@ -76,6 +80,15 @@ mod tests { use super::{CommandAction, command_action}; #[test] + fn accepts_architecture() { + let args = ["architecture".to_owned()]; + assert!(matches!( + command_action(&args).expect("action"), + CommandAction::Architecture + )); + } + + #[test] fn accepts_generate_ts() { let args = ["generate".to_owned(), "ts".to_owned()]; assert!(matches!(