lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

commit b092782e95485fccb5a9d4c0b07dfa832e019ded
parent fc2f1429f372153731a594808a4abb001a07c4d6
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:
Atools/sdk_xtask_import/src/architecture.rs | 269+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtools/sdk_xtask_import/src/check.rs | 1+
Mtools/sdk_xtask_import/src/main.rs | 15++++++++++++++-
3 files changed, 284 insertions(+), 1 deletion(-)

diff --git a/tools/sdk_xtask_import/src/architecture.rs b/tools/sdk_xtask_import/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/sdk_xtask_import/src/check.rs b/tools/sdk_xtask_import/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/sdk_xtask_import/src/main.rs b/tools/sdk_xtask_import/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!(