Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ members = [
"crates/beankeeper-bridge",
"crates/ledgerr-desktop-agent",
"crates/sysml-derive",
"crates/reqif-mcp-spike",
]
resolver = "2"

Expand Down
16 changes: 8 additions & 8 deletions crates/arc-kit-au/src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down
46 changes: 43 additions & 3 deletions crates/ledger-core/src/iso.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,16 @@ pub enum ZLayer {
Legal,
FormalProof,
Attestation,
/// SysML-v2 systems-modeling primitives (Requirement/Decision/Cost) —
/// a dedicated layer rather than folded into the (still-unimplemented,
/// see `docs/ontological-implementation-spec.md` §6.1) proposed `Domain`
/// layer, per explicit decision: independent toggle/color in the
/// isometric renderer over reusing an ontological-concepts layer.
SystemsModel,
}

impl ZLayer {
/// 0-based layer index (max 5).
/// 0-based layer index (max 6).
pub fn index(self) -> u8 {
match self {
ZLayer::Document => 0,
Expand All @@ -63,6 +69,7 @@ impl ZLayer {
ZLayer::Legal => 3,
ZLayer::FormalProof => 4,
ZLayer::Attestation => 5,
ZLayer::SystemsModel => 6,
}
}

Expand All @@ -75,6 +82,7 @@ impl ZLayer {
ZLayer::Legal => "#b91c1c",
ZLayer::FormalProof => "#0f766e",
ZLayer::Attestation => "#b45309",
ZLayer::SystemsModel => "#be185d",
}
}

Expand All @@ -87,6 +95,7 @@ impl ZLayer {
ZLayer::Legal => 408.0,
ZLayer::FormalProof => 544.0,
ZLayer::Attestation => 680.0,
ZLayer::SystemsModel => 816.0,
}
}

Expand All @@ -99,6 +108,7 @@ impl ZLayer {
ZLayer::Legal => "Legal",
ZLayer::FormalProof => "FormalProof",
ZLayer::Attestation => "Attestation",
ZLayer::SystemsModel => "SystemsModel",
}
}
}
Expand Down Expand Up @@ -127,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,
}

Expand All @@ -145,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",
}
}
Expand Down Expand Up @@ -641,17 +660,31 @@ mod tests {
ZLayer::Legal,
ZLayer::FormalProof,
ZLayer::Attestation,
ZLayer::SystemsModel,
];
for layer in all {
assert!(
layer.index() <= 5,
"ZLayer::{:?} has index {} > 5",
layer.index() <= 6,
"ZLayer::{:?} has index {} > 6",
layer,
layer.index()
);
}
}

#[test]
fn z_layer_systems_model_is_dedicated_not_pipeline() {
// Decision 2: Requirement/Decision/Cost content gets its own layer,
// not folded into an existing one (e.g. Pipeline or a future Domain).
assert_eq!(ZLayer::SystemsModel.index(), 6);
assert_eq!(ZLayer::SystemsModel.label(), "SystemsModel");
assert_ne!(ZLayer::SystemsModel.color(), ZLayer::Pipeline.color());
assert_eq!(
ZLayer::SystemsModel.base_z(),
ZLayer::Attestation.base_z() + 136.0
);
}

#[test]
fn semantic_type_known_name_nonempty() {
let all = [
Expand All @@ -666,6 +699,9 @@ mod tests {
SemanticType::Issue,
SemanticType::Proof,
SemanticType::Attestation,
SemanticType::Requirement,
SemanticType::Decision,
SemanticType::Cost,
SemanticType::Unknown,
];
for st in all {
Expand Down Expand Up @@ -771,6 +807,7 @@ mod tests {
ZLayer::Legal,
ZLayer::FormalProof,
ZLayer::Attestation,
ZLayer::SystemsModel,
];
for layer in all {
assert!(!layer.to_string().is_empty());
Expand All @@ -791,6 +828,9 @@ mod tests {
SemanticType::Issue,
SemanticType::Proof,
SemanticType::Attestation,
SemanticType::Requirement,
SemanticType::Decision,
SemanticType::Cost,
SemanticType::Unknown,
];
for st in all {
Expand Down
64 changes: 63 additions & 1 deletion crates/ledger-core/src/iso_objects.rs
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -469,9 +469,64 @@
}
}

// ============================================================================
// 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 {

Check warning

Code scanning / clippy

items after a test module Warning

items after a test module

Check warning

Code scanning / clippy

items after a test module Warning

items after a test module
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() {
Expand Down Expand Up @@ -517,6 +572,13 @@
check!(UsRdcCredit);
check!(CryptoTx);
check!(CryptoWallet);
// Systems-modeling domain
#[cfg(feature = "arc-kit-au")]
{
check!(Requirement);
check!(Decision);
check!(Cost);
}
}
}

Expand Down
45 changes: 44 additions & 1 deletion crates/ledgerr-mcp/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,13 +166,16 @@ pub const PUBLISHED_TOOLS: [ToolContractSpec; 12] = [
},
ToolContractSpec {
name: EVIDENCE_TOOL,
purpose: "evidence traceability: provenance gaps, transaction lineage, review badges, graph summary and node queries",
purpose: "evidence traceability: provenance gaps, transaction lineage, review badges, graph summary and node queries; requirement/decision/cost recording",
actions: &[
"provenance_gaps",
"trace_tx",
"summary",
"list_nodes",
"node_detail",
"import_requirement",
"record_decision",
"record_cost",
],
},
ToolContractSpec {
Expand Down Expand Up @@ -915,6 +918,46 @@ pub enum EvidenceArgs {
},
#[serde(rename = "node_detail")]
NodeDetail { node_id: String },
/// Import a requirement record (e.g. from `reqif-mcp-spike`'s
/// `RequirementRecord`) as a `NodeType::Requirement` evidence node.
#[serde(rename = "import_requirement")]
ImportRequirement {
requirement_id: String,
title: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
rationale: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
source: Option<String>,
#[serde(default = "default_requirement_status")]
status: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
related_decisions: Vec<String>,
},
/// Record a decision as a `NodeType::Decision` evidence node.
#[serde(rename = "record_decision")]
RecordDecision {
decision_id: String,
subject: String,
rationale: String,
decided_by: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
related_requirements: Vec<String>,
},
/// Record a cost as a `NodeType::Cost` evidence node.
#[serde(rename = "record_cost")]
RecordCost {
cost_id: String,
subject: String,
amount: String,
currency: String,
recorded_by: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
related_decision: Option<String>,
},
}

fn default_requirement_status() -> String {
"active".to_string()
}

pub fn parse_evidence(arguments: &Value) -> Result<EvidenceArgs, ToolError> {
Expand Down
Loading
Loading