diff --git a/Cargo.lock b/Cargo.lock index 01fbb40..9e1f3de 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11577,6 +11577,7 @@ dependencies = [ name = "xtask-mcpb" version = "1.10.0" dependencies = [ + "arc-kit-au", "clap", "hex", "ledger-core", diff --git a/crates/arc-kit-au/src/node.rs b/crates/arc-kit-au/src/node.rs index dc93026..81059d2 100644 --- a/crates/arc-kit-au/src/node.rs +++ b/crates/arc-kit-au/src/node.rs @@ -124,7 +124,7 @@ pub fn content_hash(parts: &[&str]) -> String { } /// Source document evidence. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, SysmlBlock)] pub struct SourceDoc { /// Original filename (must follow VENDOR--ACCOUNT--YYYY-MM--DOCTYPE.ext) pub filename: String, @@ -151,7 +151,7 @@ impl SourceDoc { } /// Extracted row from document parsing. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, SysmlBlock)] pub struct ExtractedRow { pub account_id: String, pub date: String, @@ -176,7 +176,7 @@ impl ExtractedRow { } /// Deterministic transaction record. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, SysmlBlock)] pub struct Transaction { /// Blake3 hash of account/date/amount/description pub tx_id: String, @@ -194,7 +194,7 @@ impl Transaction { } /// Classification applied to transaction. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, SysmlBlock)] pub struct Classification { pub tx_id: String, pub category: String, @@ -221,7 +221,7 @@ impl Classification { } /// Model-generated classification proposal. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, SysmlBlock)] pub struct ModelProposal { pub tx_id: String, pub model_name: String, @@ -247,7 +247,7 @@ impl ModelProposal { } /// Operator approval/rejection of model proposal. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, SysmlBlock)] pub struct OperatorApproval { pub tx_id: String, pub operator_id: String, @@ -275,7 +275,7 @@ impl OperatorApproval { /// Distinct from Classification — validation artifacts represent rule/constraint /// failures, not categorization decisions. PRD-4 Phase 2 requires /// classification_artifact → validation_artifact as a separate chain step. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, SysmlBlock)] pub struct ValidationIssue { pub tx_id: String, pub rule: String, @@ -301,7 +301,7 @@ impl ValidationIssue { } /// Final workbook row in CPA export. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, SysmlBlock)] pub struct WorkbookRow { pub tx_id: String, pub sheet_name: String, diff --git a/crates/ledger-core/src/iso.rs b/crates/ledger-core/src/iso.rs index 0b16966..cb64638 100644 --- a/crates/ledger-core/src/iso.rs +++ b/crates/ledger-core/src/iso.rs @@ -137,6 +137,12 @@ pub enum SemanticType { Issue, Proof, Attestation, + /// SysML-v2 systems-modeling requirement record — see `ZLayer::SystemsModel`. + Requirement, + /// SysML-v2 systems-modeling decision record — see `ZLayer::SystemsModel`. + Decision, + /// SysML-v2 systems-modeling cost record — see `ZLayer::SystemsModel`. + Cost, Unknown, } @@ -155,6 +161,9 @@ impl SemanticType { SemanticType::Issue => "issue", SemanticType::Proof => "proof", SemanticType::Attestation => "attestation", + SemanticType::Requirement => "requirement", + SemanticType::Decision => "decision", + SemanticType::Cost => "cost", SemanticType::Unknown => "unknown", } } @@ -690,6 +699,9 @@ mod tests { SemanticType::Issue, SemanticType::Proof, SemanticType::Attestation, + SemanticType::Requirement, + SemanticType::Decision, + SemanticType::Cost, SemanticType::Unknown, ]; for st in all { @@ -816,6 +828,9 @@ mod tests { SemanticType::Issue, SemanticType::Proof, SemanticType::Attestation, + SemanticType::Requirement, + SemanticType::Decision, + SemanticType::Cost, SemanticType::Unknown, ]; for st in all { diff --git a/crates/ledger-core/src/iso_objects.rs b/crates/ledger-core/src/iso_objects.rs index 0b0fbcf..9c724e3 100644 --- a/crates/ledger-core/src/iso_objects.rs +++ b/crates/ledger-core/src/iso_objects.rs @@ -1,4 +1,4 @@ -//! `HasVisualization` implementations for the 28 domain types that participate +//! `HasVisualization` implementations for the 31 domain types that participate //! in the isometric pipeline view. //! //! Every `impl HasVisualization` added here must also be registered in @@ -469,9 +469,64 @@ let lots = wallet.cost_basis_lots(method); // FIFO | HIFO | ACB"#, } } +// ============================================================================ +// SYSTEMS MODEL — Requirement/Decision/Cost (z=6, SystemsModel layer) +// ============================================================================ + +#[cfg(feature = "arc-kit-au")] +mod systems_model { + use arc_kit_au::node::{Cost, Decision, Requirement}; + + use crate::iso::{HasVisualization, RhaiDsl, SemanticType, VisualizationSpec, ZLayer}; + + impl HasVisualization for Requirement { + fn viz_spec() -> VisualizationSpec { + VisualizationSpec { + semantic_type: SemanticType::Requirement, + z_layer: ZLayer::SystemsModel, + rhai_dsl: RhaiDsl::new( + r#"let req = load_requirement(source); +if req.status == "active" { link_decisions(req.related_decisions) }"#, + ), + description: "SysML v2 systems-modeling requirement — traceable spec record, linked to the decisions it constrains", + } + } + } + + impl HasVisualization for Decision { + fn viz_spec() -> VisualizationSpec { + VisualizationSpec { + semantic_type: SemanticType::Decision, + z_layer: ZLayer::SystemsModel, + rhai_dsl: RhaiDsl::new( + r#"let decision = record_decision(subject, rationale, decided_by); +link_requirements(decision.related_requirements);"#, + ), + description: "Version-controlled decision record — rationale + decider, linked back to the requirements it satisfies", + } + } + } + + impl HasVisualization for Cost { + fn viz_spec() -> VisualizationSpec { + VisualizationSpec { + semantic_type: SemanticType::Cost, + z_layer: ZLayer::SystemsModel, + rhai_dsl: RhaiDsl::new( + r#"let cost = record_cost(subject, amount, currency); +if cost.related_decision.is_some() { attribute_to_decision(cost) }"#, + ), + description: "Version-controlled cost record — monetary amount attributed to a decision for systems-modeling cost traceability", + } + } + } +} + #[cfg(test)] mod tests { use super::*; + #[cfg(feature = "arc-kit-au")] + use arc_kit_au::node::{Cost, Decision, Requirement}; #[test] fn all_viz_spec_rhai_dsl_has_valid_syntax() { @@ -517,6 +572,13 @@ mod tests { check!(UsRdcCredit); check!(CryptoTx); check!(CryptoWallet); + // Systems-modeling domain + #[cfg(feature = "arc-kit-au")] + { + check!(Requirement); + check!(Decision); + check!(Cost); + } } } diff --git a/crates/ledgerr-mcp/tests/pipeline_e2e.rs b/crates/ledgerr-mcp/tests/pipeline_e2e.rs index a7aa017..333f92d 100644 --- a/crates/ledgerr-mcp/tests/pipeline_e2e.rs +++ b/crates/ledgerr-mcp/tests/pipeline_e2e.rs @@ -180,7 +180,7 @@ fn pipe_viz_manifest_entry_count_matches_registered_types() { // `impl HasVisualization` blocks in `ledger_core::iso_objects`. It must be // regenerated (and this count updated) whenever an impl is added, removed, // or registered/deregistered in xtask/src/viz_manifest.rs. - const EXPECTED_ENTRY_COUNT: usize = 28; + const EXPECTED_ENTRY_COUNT: usize = 31; let manifest_path = Path::new(env!("CARGO_MANIFEST_DIR")) .parent() @@ -218,6 +218,9 @@ fn pipe_viz_manifest_entry_count_matches_registered_types() { "AuRdActivity", "UsRdcCredit", "CryptoWallet", + "Requirement", + "Decision", + "Cost", ] { assert!(names.contains(&expected), "missing {expected} entry"); } diff --git a/ui/docs/public/viz-manifest.json b/ui/docs/public/viz-manifest.json index 5bdf8ad..212ac67 100644 --- a/ui/docs/public/viz-manifest.json +++ b/ui/docs/public/viz-manifest.json @@ -3188,6 +3188,293 @@ }, "description": "Crypto wallet — tracks asset lots, cost basis methods, and balance history for tax event attribution" } + }, + { + "type_name": "Requirement", + "spec": { + "semantic_type": "requirement", + "z_layer": "SystemsModel", + "rhai_dsl": { + "source": "let req = load_requirement(source);\nif req.status == \"active\" { link_decisions(req.related_decisions) }", + "symbols": [ + { + "kind": "Keyword", + "name": "let", + "span": { + "line": 1, + "col": 1 + } + }, + { + "kind": "Variable", + "name": "req", + "span": { + "line": 1, + "col": 5 + } + }, + { + "kind": "FunctionCall", + "name": "load_requirement", + "span": { + "line": 1, + "col": 11 + } + }, + { + "kind": "Variable", + "name": "source", + "span": { + "line": 1, + "col": 28 + } + }, + { + "kind": "Keyword", + "name": "if", + "span": { + "line": 2, + "col": 1 + } + }, + { + "kind": "Variable", + "name": "req", + "span": { + "line": 2, + "col": 4 + } + }, + { + "kind": "Variable", + "name": "status", + "span": { + "line": 2, + "col": 8 + } + }, + { + "kind": "FunctionCall", + "name": "link_decisions", + "span": { + "line": 2, + "col": 29 + } + }, + { + "kind": "Variable", + "name": "req", + "span": { + "line": 2, + "col": 44 + } + }, + { + "kind": "Variable", + "name": "related_decisions", + "span": { + "line": 2, + "col": 48 + } + } + ] + }, + "description": "SysML v2 systems-modeling requirement — traceable spec record, linked to the decisions it constrains" + } + }, + { + "type_name": "Decision", + "spec": { + "semantic_type": "decision", + "z_layer": "SystemsModel", + "rhai_dsl": { + "source": "let decision = record_decision(subject, rationale, decided_by);\nlink_requirements(decision.related_requirements);", + "symbols": [ + { + "kind": "Keyword", + "name": "let", + "span": { + "line": 1, + "col": 1 + } + }, + { + "kind": "Variable", + "name": "decision", + "span": { + "line": 1, + "col": 5 + } + }, + { + "kind": "FunctionCall", + "name": "record_decision", + "span": { + "line": 1, + "col": 16 + } + }, + { + "kind": "Variable", + "name": "subject", + "span": { + "line": 1, + "col": 32 + } + }, + { + "kind": "Variable", + "name": "rationale", + "span": { + "line": 1, + "col": 41 + } + }, + { + "kind": "Variable", + "name": "decided_by", + "span": { + "line": 1, + "col": 52 + } + }, + { + "kind": "FunctionCall", + "name": "link_requirements", + "span": { + "line": 2, + "col": 1 + } + }, + { + "kind": "Variable", + "name": "decision", + "span": { + "line": 2, + "col": 19 + } + }, + { + "kind": "Variable", + "name": "related_requirements", + "span": { + "line": 2, + "col": 28 + } + } + ] + }, + "description": "Version-controlled decision record — rationale + decider, linked back to the requirements it satisfies" + } + }, + { + "type_name": "Cost", + "spec": { + "semantic_type": "cost", + "z_layer": "SystemsModel", + "rhai_dsl": { + "source": "let cost = record_cost(subject, amount, currency);\nif cost.related_decision.is_some() { attribute_to_decision(cost) }", + "symbols": [ + { + "kind": "Keyword", + "name": "let", + "span": { + "line": 1, + "col": 1 + } + }, + { + "kind": "Variable", + "name": "cost", + "span": { + "line": 1, + "col": 5 + } + }, + { + "kind": "FunctionCall", + "name": "record_cost", + "span": { + "line": 1, + "col": 12 + } + }, + { + "kind": "Variable", + "name": "subject", + "span": { + "line": 1, + "col": 24 + } + }, + { + "kind": "Variable", + "name": "amount", + "span": { + "line": 1, + "col": 33 + } + }, + { + "kind": "Variable", + "name": "currency", + "span": { + "line": 1, + "col": 41 + } + }, + { + "kind": "Keyword", + "name": "if", + "span": { + "line": 2, + "col": 1 + } + }, + { + "kind": "Variable", + "name": "cost", + "span": { + "line": 2, + "col": 4 + } + }, + { + "kind": "Variable", + "name": "related_decision", + "span": { + "line": 2, + "col": 9 + } + }, + { + "kind": "FunctionCall", + "name": "is_some", + "span": { + "line": 2, + "col": 26 + } + }, + { + "kind": "FunctionCall", + "name": "attribute_to_decision", + "span": { + "line": 2, + "col": 38 + } + }, + { + "kind": "Variable", + "name": "cost", + "span": { + "line": 2, + "col": 60 + } + } + ] + }, + "description": "Version-controlled cost record — monetary amount attributed to a decision for systems-modeling cost traceability" + } } ] } \ No newline at end of file diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index 500f158..5b4960a 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -15,6 +15,7 @@ name = "xtask_mcpb" path = "src/lib.rs" [dependencies] +arc-kit-au = { path = "../crates/arc-kit-au" } clap = { version = "4", features = ["derive"] } ledger-core = { path = "../crates/ledger-core" } hex = "0.4" diff --git a/xtask/src/viz_manifest.rs b/xtask/src/viz_manifest.rs index 43e2751..aa0750c 100644 --- a/xtask/src/viz_manifest.rs +++ b/xtask/src/viz_manifest.rs @@ -1,12 +1,13 @@ //! Export the VisualizationSpec JSON manifest for the docs UI. //! -//! Collects `HasVisualization::viz_spec()` from all 28 domain types (every +//! Collects `HasVisualization::viz_spec()` from all 31 domain types (every //! `impl HasVisualization` in `ledger_core::iso_objects`, with the generic //! `StageResult` represented once via `StageResult<()>`) and writes a //! `VizManifest` JSON file to the specified output path. use std::path::Path; +use arc_kit_au::node::{Cost, Decision, Requirement}; use ledger_core::{ au_rd::{AuRdActivity, AuRdOffset}, constraints::{ @@ -74,6 +75,9 @@ pub fn export_viz_manifest(output: &Path) -> Result<(), Box