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:
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!(