From f82a938075a02cacd17e374eaa8441ea20be57ef Mon Sep 17 00:00:00 2001 From: Brian H Date: Sun, 3 May 2026 13:41:58 +0000 Subject: [PATCH 1/2] feat: checkpoint workspace core updates Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/arc-kit-au/src/badge.rs | 50 +-- crates/arc-kit-au/src/builder.rs | 12 +- crates/arc-kit-au/src/edge.rs | 12 +- crates/arc-kit-au/src/graph.rs | 14 +- crates/arc-kit-au/src/lib.rs | 18 +- crates/arc-kit-au/src/missing.rs | 28 +- crates/arc-kit-au/src/store.rs | 2 +- crates/arc-kit-au/src/trace.rs | 2 +- crates/b00t-iface/src/core/governance.rs | 6 +- crates/b00t-iface/src/core/machine.rs | 33 +- crates/b00t-iface/src/core/mod.rs | 8 +- crates/b00t-iface/src/core/promise.rs | 15 +- crates/b00t-iface/src/core/surface.rs | 17 +- crates/b00t-iface/src/exec/harness.rs | 52 ++- crates/b00t-iface/src/gated/autoresearch.rs | 33 +- crates/b00t-iface/src/gated/mod.rs | 4 +- crates/b00t-iface/src/handshake/mod.rs | 44 +- crates/b00t-iface/src/lib.rs | 2 +- crates/b00t-iface/src/llm/mod.rs | 31 +- crates/b00t-iface/src/metric/mod.rs | 34 +- crates/b00t-iface/src/ralph.rs | 36 +- crates/b00t-iface/src/sarif/mod.rs | 227 ++++++++--- crates/b00t-iface/src/viz/mod.rs | 28 +- crates/datum/src/ast.rs | 90 ++-- crates/datum/src/lib.rs | 21 +- crates/datum/src/logic.rs | 203 ++++++++-- crates/datum/src/protocol.rs | 19 +- crates/datum/src/tomllmd.rs | 15 +- crates/ledger-core/src/calendar.rs | 46 ++- crates/ledger-core/src/constraints.rs | 69 +++- crates/ledger-core/src/document_shape.rs | 143 ++++--- crates/ledger-core/src/graph.rs | 9 +- crates/ledger-core/src/integration_tests.rs | 39 +- crates/ledger-core/src/iso.rs | 73 +++- crates/ledger-core/src/iso_objects.rs | 58 ++- crates/ledger-core/src/layout.rs | 9 +- crates/ledger-core/src/ledger_ops.rs | 106 +++-- crates/ledger-core/src/legal.rs | 23 +- crates/ledger-core/src/lib.rs | 2 +- crates/ledger-core/src/observability.rs | 68 +++- crates/ledger-core/src/ontology.rs | 4 +- crates/ledger-core/src/pipeline.rs | 81 +++- crates/ledger-core/src/render.rs | 8 +- crates/ledger-core/src/slint_viz.rs | 2 +- crates/ledger-core/src/validation.rs | 21 +- crates/ledger-core/src/verify.rs | 78 ++-- crates/ledger-core/src/visualize.rs | 87 ++-- crates/ledger-core/src/workflow.rs | 31 +- crates/ledger-core/tests/iso_lint.rs | 12 +- .../ledger-core/tests/legal_z3_integration.rs | 26 +- crates/ledger-core/tests/rhai_rules.rs | 19 +- .../ledger-core/tests/rhai_rules_extended.rs | 84 +++- crates/ledger-core/tests/type_mesh.rs | 25 +- crates/ledgerr-host/src/bin/host-window.rs | 20 +- crates/ledgerr-host/src/evidence.rs | 19 +- crates/ledgerr-host/src/internal_openai.rs | 48 ++- crates/ledgerr-host/src/lib.rs | 6 +- crates/ledgerr-host/src/local_llm.rs | 17 +- crates/ledgerr-host/src/local_llm_mistral.rs | 32 +- crates/ledgerr-host/src/settings/schema.rs | 7 +- crates/ledgerr-host/tests/phi4_smoke.rs | 4 +- .../ledgerr-host/tests/visualization_e2e.rs | 80 ++-- crates/ledgerr-mcp-core/src/provider.rs | 48 ++- crates/ledgerr-mcp/src/actor.rs | 383 +++++++++--------- .../ledgerr-mcp/src/bin/ledgerr-mcp-server.rs | 49 ++- crates/ledgerr-mcp/src/contract.rs | 9 +- crates/ledgerr-mcp/src/gate.rs | 29 +- crates/ledgerr-mcp/src/lib.rs | 26 +- crates/ledgerr-mcp/src/mcp_adapter.rs | 26 +- .../ledgerr-mcp/src/providers/definitions.rs | 12 +- crates/ledgerr-mcp/src/xero_service.rs | 6 +- crates/ledgerr-xero/src/auth.rs | 3 +- crates/mdbook-rhai-mermaid/src/emitter.rs | 12 +- crates/mdbook-rhai-mermaid/src/main.rs | 29 +- crates/mdbook-rhai-mermaid/src/parser.rs | 148 +++++-- xtask/src/viz_manifest.rs | 69 +--- 76 files changed, 2080 insertions(+), 1181 deletions(-) diff --git a/crates/arc-kit-au/src/badge.rs b/crates/arc-kit-au/src/badge.rs index e344e2f..792d4e5 100644 --- a/crates/arc-kit-au/src/badge.rs +++ b/crates/arc-kit-au/src/badge.rs @@ -65,9 +65,7 @@ impl From<&EvidenceChain> for ProvenanceBadge { let critical_missing: Vec<_> = missing .iter() .filter(|m| { - **m == "source_document" - || **m == "classification" - || **m == "extracted_rows" + **m == "source_document" || **m == "classification" || **m == "extracted_rows" }) .map(|s| s.to_string()) .collect(); @@ -86,8 +84,8 @@ impl From<&EvidenceChain> for ProvenanceBadge { #[cfg(test)] mod tests { - use crate::node::Confidence; use super::*; + use crate::node::Confidence; use crate::node::{EvidenceNode, NodeId, NodeType, SourceDoc}; use chrono::TimeZone; use chrono::Utc; @@ -146,18 +144,16 @@ mod tests { source_document: NodeId::new(NodeType::SourceDoc, "abc"), extraction_confidence: Confidence::from(0.95), })], - classifications: vec![EvidenceNode::Classification( - crate::node::Classification { - tx_id: "tx_1".to_string(), - category: "Meals".to_string(), - sub_category: None, - confidence: Confidence::from(0.92), - rule_used: None, - actor: "operator".to_string(), - classified_at: Utc.with_ymd_and_hms(2024, 2, 1, 11, 0, 0).unwrap(), - note: None, - }, - )], + classifications: vec![EvidenceNode::Classification(crate::node::Classification { + tx_id: "tx_1".to_string(), + category: "Meals".to_string(), + sub_category: None, + confidence: Confidence::from(0.92), + rule_used: None, + actor: "operator".to_string(), + classified_at: Utc.with_ymd_and_hms(2024, 2, 1, 11, 0, 0).unwrap(), + note: None, + })], proposals: vec![], approvals: vec![EvidenceNode::OperatorApproval( crate::node::OperatorApproval { @@ -204,18 +200,16 @@ mod tests { tx_id: "tx_3".to_string(), source_documents: vec![], extracted_rows: vec![], - classifications: vec![EvidenceNode::Classification( - crate::node::Classification { - tx_id: "tx_3".to_string(), - category: "Meals".to_string(), - sub_category: None, - confidence: Confidence::from(0.92), - rule_used: None, - actor: "operator".to_string(), - classified_at: Utc.with_ymd_and_hms(2024, 2, 1, 11, 0, 0).unwrap(), - note: None, - }, - )], + classifications: vec![EvidenceNode::Classification(crate::node::Classification { + tx_id: "tx_3".to_string(), + category: "Meals".to_string(), + sub_category: None, + confidence: Confidence::from(0.92), + rule_used: None, + actor: "operator".to_string(), + classified_at: Utc.with_ymd_and_hms(2024, 2, 1, 11, 0, 0).unwrap(), + note: None, + })], proposals: vec![], approvals: vec![EvidenceNode::OperatorApproval( crate::node::OperatorApproval { diff --git a/crates/arc-kit-au/src/builder.rs b/crates/arc-kit-au/src/builder.rs index 19bad34..bd4a5b6 100644 --- a/crates/arc-kit-au/src/builder.rs +++ b/crates/arc-kit-au/src/builder.rs @@ -3,7 +3,6 @@ //! Provides a fluent API for building evidence chains from ingest, //! classify, approve, and export operations. - use crate::edge::EdgeType; use crate::graph::EvidenceGraph; use crate::node::{ @@ -76,7 +75,8 @@ impl<'a> EvidenceBuilder<'a> { pub fn ensure_proposal(&mut self, proposal: ModelProposal) -> NodeId { let prop_id = proposal.node_id(); let tx_id = NodeId::new(NodeType::Transaction, &proposal.tx_id); - self.graph.ensure_node(EvidenceNode::ModelProposal(proposal)); + self.graph + .ensure_node(EvidenceNode::ModelProposal(proposal)); self.graph .ensure_edge(tx_id, prop_id.clone(), EdgeType::ProposedBy); prop_id @@ -98,7 +98,8 @@ impl<'a> EvidenceBuilder<'a> { let wb_id = wb_row.node_id(); let tx_id = NodeId::new(NodeType::Transaction, &wb_row.tx_id); self.graph.ensure_node(EvidenceNode::WorkbookRow(wb_row)); - self.graph.ensure_edge(tx_id, wb_id.clone(), EdgeType::ExportedTo); + self.graph + .ensure_edge(tx_id, wb_id.clone(), EdgeType::ExportedTo); wb_id } @@ -106,8 +107,7 @@ impl<'a> EvidenceBuilder<'a> { pub fn ensure_validation_issue(&mut self, issue: ValidationIssue) -> NodeId { let vi_id = issue.node_id(); let tx_id = NodeId::new(NodeType::Transaction, &issue.tx_id); - self.graph - .ensure_node(EvidenceNode::ValidationIssue(issue)); + self.graph.ensure_node(EvidenceNode::ValidationIssue(issue)); self.graph .ensure_edge(tx_id, vi_id.clone(), EdgeType::ValidatedAs); vi_id @@ -133,8 +133,8 @@ impl<'a> EvidenceBuilder<'a> { #[cfg(test)] mod tests { - use crate::node::Confidence; use super::*; + use crate::node::Confidence; use chrono::{TimeZone, Utc}; use rust_decimal::Decimal; diff --git a/crates/arc-kit-au/src/edge.rs b/crates/arc-kit-au/src/edge.rs index 923b319..d13e4ee 100644 --- a/crates/arc-kit-au/src/edge.rs +++ b/crates/arc-kit-au/src/edge.rs @@ -56,18 +56,18 @@ pub struct EvidenceEdge { impl EvidenceEdge { pub fn new(from: NodeId, to: NodeId, edge_type: EdgeType) -> Self { - Self { from, to, edge_type } + Self { + from, + to, + edge_type, + } } } /// Edge traversal utilities. pub trait EdgeTraversal { /// Find all edges of a specific type from a node. - fn edges_of_type<'a>( - &'a self, - from: &'a NodeId, - edge_type: EdgeType, - ) -> Vec<&'a EvidenceEdge>; + fn edges_of_type<'a>(&'a self, from: &'a NodeId, edge_type: EdgeType) -> Vec<&'a EvidenceEdge>; /// Find all incoming edges to a node. fn incoming_edges<'a>(&'a self, to: &'a NodeId) -> Vec<&'a EvidenceEdge>; diff --git a/crates/arc-kit-au/src/graph.rs b/crates/arc-kit-au/src/graph.rs index e73c32d..012b083 100644 --- a/crates/arc-kit-au/src/graph.rs +++ b/crates/arc-kit-au/src/graph.rs @@ -104,12 +104,18 @@ impl EvidenceGraph { /// Get all nodes of a specific type. pub fn nodes_of_type(&self, node_type: NodeType) -> Vec<&EvidenceNode> { - self.nodes.iter().filter(|n| n.node_type() == node_type).collect() + self.nodes + .iter() + .filter(|n| n.node_type() == node_type) + .collect() } /// Get all edges of a specific type. pub fn edges_of_type(&self, edge_type: EdgeType) -> Vec<&EvidenceEdge> { - self.edges.iter().filter(|e| e.edge_type == edge_type).collect() + self.edges + .iter() + .filter(|e| e.edge_type == edge_type) + .collect() } /// Find all outgoing edges from a node. @@ -358,9 +364,7 @@ mod tests { let tx = test_tx(); let doc_id = graph.add_node(EvidenceNode::SourceDoc(doc)).unwrap(); let tx_id = graph.add_node(EvidenceNode::Transaction(tx)).unwrap(); - graph - .add_edge(doc_id, tx_id, EdgeType::Produces) - .unwrap(); + graph.add_edge(doc_id, tx_id, EdgeType::Produces).unwrap(); let json = graph.to_json().unwrap(); let restored = EvidenceGraph::from_json(&json).unwrap(); diff --git a/crates/arc-kit-au/src/lib.rs b/crates/arc-kit-au/src/lib.rs index ae4296f..f3bef76 100644 --- a/crates/arc-kit-au/src/lib.rs +++ b/crates/arc-kit-au/src/lib.rs @@ -25,20 +25,20 @@ //! assert!(restored.is_empty()); //! ``` -pub mod node; +pub mod badge; +pub mod builder; pub mod edge; pub mod graph; -pub mod builder; -pub mod trace; pub mod missing; -pub mod badge; +pub mod node; pub mod store; +pub mod trace; -pub use node::{Confidence, EvidenceNode, NodeId, NodeType}; -pub use edge::{EvidenceEdge, EdgeType}; -pub use graph::{EvidenceGraph, WorkQueueSummary}; +pub use badge::ProvenanceBadge; pub use builder::EvidenceBuilder; -pub use trace::{EvidenceChain, EvidenceTracer}; +pub use edge::{EdgeType, EvidenceEdge}; +pub use graph::{EvidenceGraph, WorkQueueSummary}; pub use missing::{MissingElement, ProvenanceGap, ProvenanceScanner}; -pub use badge::ProvenanceBadge; +pub use node::{Confidence, EvidenceNode, NodeId, NodeType}; pub use store::EvidenceStore; +pub use trace::{EvidenceChain, EvidenceTracer}; diff --git a/crates/arc-kit-au/src/missing.rs b/crates/arc-kit-au/src/missing.rs index a63e8fa..81e9d0f 100644 --- a/crates/arc-kit-au/src/missing.rs +++ b/crates/arc-kit-au/src/missing.rs @@ -76,9 +76,7 @@ impl ProvenanceScanner for EvidenceGraph { // Check incoming edges for source rows let incoming = self.incoming_edges(&tx_node_id); - let has_rows = incoming - .iter() - .any(|e| e.edge_type == EdgeType::Produces); + let has_rows = incoming.iter().any(|e| e.edge_type == EdgeType::Produces); if has_rows { // Check if rows have source documents @@ -156,9 +154,9 @@ impl ProvenanceScanner for EvidenceGraph { #[cfg(test)] mod tests { - use crate::node::Confidence; use super::*; use crate::builder::EvidenceBuilder; + use crate::node::Confidence; use crate::node::{Classification, ExtractedRow, NodeId, SourceDoc, Transaction}; use chrono::TimeZone; use chrono::Utc; @@ -228,9 +226,7 @@ mod tests { // build_full_chain doesn't create approvals or exports, so those will be gaps let gaps = graph.find_missing_provenance(); assert_eq!(gaps.len(), 1); - assert!(gaps[0] - .missing - .contains(&MissingElement::OperatorApproval)); + assert!(gaps[0].missing.contains(&MissingElement::OperatorApproval)); assert!(gaps[0].missing.contains(&MissingElement::WorkbookExport)); // But source and classification should be present assert!(!gaps[0].missing.contains(&MissingElement::SourceDocument)); @@ -254,12 +250,8 @@ mod tests { let gaps = graph.find_missing_provenance(); assert_eq!(gaps.len(), 1); - assert!(gaps[0] - .missing - .contains(&MissingElement::Classification)); - assert!(gaps[0] - .missing - .contains(&MissingElement::OperatorApproval)); + assert!(gaps[0].missing.contains(&MissingElement::Classification)); + assert!(gaps[0].missing.contains(&MissingElement::OperatorApproval)); assert!(gaps[0].missing.contains(&MissingElement::WorkbookExport)); } @@ -293,11 +285,17 @@ mod tests { #[test] fn missing_element_display_format() { - assert_eq!(MissingElement::SourceDocument.to_string(), "source_document"); + assert_eq!( + MissingElement::SourceDocument.to_string(), + "source_document" + ); assert_eq!( MissingElement::OperatorApproval.to_string(), "operator_approval" ); - assert_eq!(MissingElement::WorkbookExport.to_string(), "workbook_export"); + assert_eq!( + MissingElement::WorkbookExport.to_string(), + "workbook_export" + ); } } diff --git a/crates/arc-kit-au/src/store.rs b/crates/arc-kit-au/src/store.rs index a4d8b58..0afbcbd 100644 --- a/crates/arc-kit-au/src/store.rs +++ b/crates/arc-kit-au/src/store.rs @@ -113,9 +113,9 @@ impl EvidenceStore { #[cfg(test)] mod tests { - use crate::node::Confidence; use super::*; use crate::builder::EvidenceBuilder; + use crate::node::Confidence; use crate::node::{Classification, ExtractedRow, SourceDoc, Transaction}; use chrono::TimeZone; use chrono::Utc; diff --git a/crates/arc-kit-au/src/trace.rs b/crates/arc-kit-au/src/trace.rs index 79f4285..ca40443 100644 --- a/crates/arc-kit-au/src/trace.rs +++ b/crates/arc-kit-au/src/trace.rs @@ -158,9 +158,9 @@ impl EvidenceTracer for EvidenceGraph { #[cfg(test)] mod tests { - use crate::node::Confidence; use super::*; use crate::builder::EvidenceBuilder; + use crate::node::Confidence; use crate::node::{Classification, ExtractedRow, OperatorApproval, SourceDoc, Transaction}; use chrono::TimeZone; use chrono::Utc; diff --git a/crates/b00t-iface/src/core/governance.rs b/crates/b00t-iface/src/core/governance.rs index 0d87d0a..df51941 100644 --- a/crates/b00t-iface/src/core/governance.rs +++ b/crates/b00t-iface/src/core/governance.rs @@ -71,7 +71,11 @@ impl GovernancePolicy { /// Solver-friendly encoding as a serializable constraint set. pub fn to_constraints(&self) -> GovernanceConstraints { GovernanceConstraints { - allowed_starters: self.allowed_starters.iter().map(|r| r.to_string()).collect(), + allowed_starters: self + .allowed_starters + .iter() + .map(|r| r.to_string()) + .collect(), max_ttl_secs: self.max_ttl.as_secs(), auto_restart: self.auto_restart, crash_budget: self.crash_budget, diff --git a/crates/b00t-iface/src/core/machine.rs b/crates/b00t-iface/src/core/machine.rs index ac1613e..252e444 100644 --- a/crates/b00t-iface/src/core/machine.rs +++ b/crates/b00t-iface/src/core/machine.rs @@ -36,8 +36,8 @@ //! ``` use super::surface::{AuditRecord, MaintenanceAction}; -use std::time::Duration; use std::fmt; +use std::time::Duration; /// The finite set of states a surface machine can be in. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -114,7 +114,11 @@ impl SurfaceMachine { } /// Attempt a transition. Returns the trigger label if valid. - pub fn transition(&mut self, to: MachineState, elapsed: Duration) -> Result<&'static str, String> { + pub fn transition( + &mut self, + to: MachineState, + elapsed: Duration, + ) -> Result<&'static str, String> { let trigger = valid_transition(self.state, to) .ok_or_else(|| format!("invalid transition: {} → {}", self.state, to))?; @@ -130,7 +134,11 @@ impl SurfaceMachine { } /// Process a maintenance action and transition accordingly. - pub fn apply_maintenance(&mut self, action: &MaintenanceAction, elapsed: Duration) -> Result<(), String> { + pub fn apply_maintenance( + &mut self, + action: &MaintenanceAction, + elapsed: Duration, + ) -> Result<(), String> { match action { MaintenanceAction::NoOp => { self.transition(MachineState::Healthy, elapsed)?; @@ -163,10 +171,14 @@ mod tests { #[test] fn valid_lifecycle() { let mut m = SurfaceMachine::new("test"); - m.transition(MachineState::Ready, Duration::from_millis(10)).unwrap(); - m.transition(MachineState::Running, Duration::from_millis(50)).unwrap(); - m.apply_maintenance(&MaintenanceAction::NoOp, Duration::from_millis(5)).unwrap(); - m.transition(MachineState::Terminated, Duration::from_millis(20)).unwrap(); + m.transition(MachineState::Ready, Duration::from_millis(10)) + .unwrap(); + m.transition(MachineState::Running, Duration::from_millis(50)) + .unwrap(); + m.apply_maintenance(&MaintenanceAction::NoOp, Duration::from_millis(5)) + .unwrap(); + m.transition(MachineState::Terminated, Duration::from_millis(20)) + .unwrap(); assert!(m.is_terminal()); assert_eq!(m.transitions.len(), 4); } @@ -174,7 +186,9 @@ mod tests { #[test] fn invalid_transition_fails() { let mut m = SurfaceMachine::new("test"); - let err = m.transition(MachineState::Running, Duration::ZERO).unwrap_err(); + let err = m + .transition(MachineState::Running, Duration::ZERO) + .unwrap_err(); assert!(err.contains("invalid transition")); } @@ -203,7 +217,8 @@ mod tests { let mut m = SurfaceMachine::new("test"); m.transition(MachineState::Ready, Duration::ZERO).unwrap(); m.transition(MachineState::Running, Duration::ZERO).unwrap(); - m.apply_maintenance(&MaintenanceAction::Restart, Duration::ZERO).unwrap(); + m.apply_maintenance(&MaintenanceAction::Restart, Duration::ZERO) + .unwrap(); assert_eq!(m.crash_count, 1); assert_eq!(m.state, MachineState::Ready); } diff --git a/crates/b00t-iface/src/core/mod.rs b/crates/b00t-iface/src/core/mod.rs index 393a93c..995dcd8 100644 --- a/crates/b00t-iface/src/core/mod.rs +++ b/crates/b00t-iface/src/core/mod.rs @@ -10,12 +10,12 @@ //! - `promise` — typed event: a value that will be produced at a future lifecycle point //! - `machine` — abstract state machine over surface states -pub mod surface; pub mod governance; -pub mod promise; pub mod machine; +pub mod promise; +pub mod surface; -pub use surface::*; pub use governance::*; -pub use promise::*; pub use machine::*; +pub use promise::*; +pub use surface::*; diff --git a/crates/b00t-iface/src/core/promise.rs b/crates/b00t-iface/src/core/promise.rs index c49ec60..cc0305a 100644 --- a/crates/b00t-iface/src/core/promise.rs +++ b/crates/b00t-iface/src/core/promise.rs @@ -53,7 +53,11 @@ impl std::fmt::Display for PromiseOp { impl LifecyclePromise { pub fn new(op: PromiseOp, elapsed: Duration, value: PromiseValue) -> Self { - Self { operation: op, elapsed, value } + Self { + operation: op, + elapsed, + value, + } } pub fn fulfilled(op: PromiseOp, elapsed: Duration, value: T) -> Self { @@ -89,7 +93,8 @@ mod tests { #[test] fn promise_fulfilled() { - let p = LifecyclePromise::::fulfilled(PromiseOp::Init, Duration::from_secs(1), 42); + let p = + LifecyclePromise::::fulfilled(PromiseOp::Init, Duration::from_secs(1), 42); assert!(p.is_fulfilled()); assert!(!p.is_rejected()); assert_eq!(p.operation.to_string(), "init"); @@ -97,7 +102,11 @@ mod tests { #[test] fn promise_rejected() { - let p = LifecyclePromise::::rejected(PromiseOp::Operate, Duration::from_secs(2), "kaboom".into()); + let p = LifecyclePromise::::rejected( + PromiseOp::Operate, + Duration::from_secs(2), + "kaboom".into(), + ); assert!(p.is_rejected()); assert!(!p.is_fulfilled()); } diff --git a/crates/b00t-iface/src/core/surface.rs b/crates/b00t-iface/src/core/surface.rs index a67552e..4cb1805 100644 --- a/crates/b00t-iface/src/core/surface.rs +++ b/crates/b00t-iface/src/core/surface.rs @@ -125,8 +125,7 @@ pub enum DatumWatcherError { impl DatumWatcher { pub fn new() -> Self { let base = if cfg!(test) { - Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../../_b00t_") + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../_b00t_") } else { Path::new("_b00t_").to_path_buf() }; @@ -142,14 +141,12 @@ impl DatumWatcher { if !dir.exists() { return Err(DatumWatcherError::DirNotFound(dir.display().to_string())); } - let entries = std::fs::read_dir(dir).map_err(|e| { - DatumWatcherError::DirNotFound(format!("{}: {e}", dir.display())) - })?; + let entries = std::fs::read_dir(dir) + .map_err(|e| DatumWatcherError::DirNotFound(format!("{}: {e}", dir.display())))?; let mut files = Vec::new(); for entry in entries { - let entry = entry.map_err(|e| { - DatumWatcherError::DirNotFound(format!("{}: {e}", dir.display())) - })?; + let entry = entry + .map_err(|e| DatumWatcherError::DirNotFound(format!("{}: {e}", dir.display())))?; let name = entry.file_name().to_string_lossy().to_string(); if name.ends_with(".datum") { files.push(name.trim_end_matches(".datum").to_owned()); @@ -168,7 +165,9 @@ impl ProcessSurface for DatumWatcher { fn capability(&self) -> SurfaceCapability { SurfaceCapability { name: "datum-watcher", - requirements: vec![Requirement::PathExists(self.datum_dir.display().to_string())], + requirements: vec![Requirement::PathExists( + self.datum_dir.display().to_string(), + )], governance: GovernancePolicy { allowed_starters: vec![AgentRole::Executive, AgentRole::Operator], max_ttl: Duration::from_secs(86400), diff --git a/crates/b00t-iface/src/exec/harness.rs b/crates/b00t-iface/src/exec/harness.rs index 50fb856..be9e960 100644 --- a/crates/b00t-iface/src/exec/harness.rs +++ b/crates/b00t-iface/src/exec/harness.rs @@ -4,8 +4,8 @@ //! governance validation, and promise event log. use crate::core::{ - GovernancePolicy, MachineState, ProcessSurface, PromiseChain, PromiseOp, - SurfaceCapability, SurfaceMachine, AuditRecord, LifecyclePromise, MaintenanceAction, + AuditRecord, GovernancePolicy, LifecyclePromise, MachineState, MaintenanceAction, + ProcessSurface, PromiseChain, PromiseOp, SurfaceCapability, SurfaceMachine, }; use std::time::{Duration, Instant}; @@ -56,9 +56,7 @@ impl SurfaceHarness { let _ = self.machine.transition(MachineState::Ready, now.elapsed()); LifecyclePromise::fulfilled(PromiseOp::Init, now.elapsed(), ()) } - Err(e) => { - LifecyclePromise::rejected(PromiseOp::Init, now.elapsed(), e.to_string()) - } + Err(e) => LifecyclePromise::rejected(PromiseOp::Init, now.elapsed(), e.to_string()), }; if init_promise.is_rejected() { @@ -68,18 +66,23 @@ impl SurfaceHarness { // Operate let operate_promise = match self.surface.operate() { Ok(h) => { - let _ = self.machine.transition(MachineState::Running, now.elapsed()); + let _ = self + .machine + .transition(MachineState::Running, now.elapsed()); LifecyclePromise::fulfilled(PromiseOp::Operate, now.elapsed(), h) } - Err(e) => { - LifecyclePromise::rejected(PromiseOp::Operate, now.elapsed(), e.to_string()) - } + Err(e) => LifecyclePromise::rejected(PromiseOp::Operate, now.elapsed(), e.to_string()), }; // Maintain (single pass) let maintain_action = self.surface.maintain(); - let maintain_promise = match self.machine.apply_maintenance(&maintain_action, now.elapsed()) { - Ok(()) => LifecyclePromise::fulfilled(PromiseOp::Maintain, now.elapsed(), maintain_action), + let maintain_promise = match self + .machine + .apply_maintenance(&maintain_action, now.elapsed()) + { + Ok(()) => { + LifecyclePromise::fulfilled(PromiseOp::Maintain, now.elapsed(), maintain_action) + } Err(e) => LifecyclePromise::rejected(PromiseOp::Maintain, now.elapsed(), e), }; @@ -93,7 +96,11 @@ impl SurfaceHarness { init: init_promise, operate: operate_promise, maintains: vec![maintain_promise], - terminate: LifecyclePromise::rejected(PromiseOp::Terminate, Duration::ZERO, "operate never succeeded".into()), + terminate: LifecyclePromise::rejected( + PromiseOp::Terminate, + Duration::ZERO, + "operate never succeeded".into(), + ), }, governance_violations: self.governance_violations.clone(), }; @@ -103,7 +110,9 @@ impl SurfaceHarness { // Terminate let terminate_promise = match S::terminate(handle) { Ok(record) => { - let _ = self.machine.transition(MachineState::Terminated, now.elapsed()); + let _ = self + .machine + .transition(MachineState::Terminated, now.elapsed()); LifecyclePromise::fulfilled(PromiseOp::Terminate, now.elapsed(), record) } Err(e) => { @@ -136,9 +145,17 @@ impl SurfaceHarness { surface: self.capability.name.to_owned(), chain: PromiseChain { init, - operate: LifecyclePromise::rejected(PromiseOp::Operate, Duration::ZERO, "init failed".into()), + operate: LifecyclePromise::rejected( + PromiseOp::Operate, + Duration::ZERO, + "init failed".into(), + ), maintains: vec![], - terminate: LifecyclePromise::rejected(PromiseOp::Terminate, Duration::ZERO, "init failed".into()), + terminate: LifecyclePromise::rejected( + PromiseOp::Terminate, + Duration::ZERO, + "init failed".into(), + ), }, governance_violations: self.governance_violations.clone(), } @@ -162,7 +179,10 @@ mod tests { let outcome = harness.run_lifecycle(config); assert!(outcome.chain.init.is_fulfilled(), "init should pass"); assert!(outcome.chain.operate.is_fulfilled(), "operate should pass"); - assert!(outcome.governance_violations.is_empty(), "no governance violations"); + assert!( + outcome.governance_violations.is_empty(), + "no governance violations" + ); } #[test] diff --git a/crates/b00t-iface/src/gated/autoresearch.rs b/crates/b00t-iface/src/gated/autoresearch.rs index caa07cf..c407f27 100644 --- a/crates/b00t-iface/src/gated/autoresearch.rs +++ b/crates/b00t-iface/src/gated/autoresearch.rs @@ -7,7 +7,8 @@ //! remote eval dispatch. The base trait is available under `b00t` feature alone. use crate::core::{ - AuditRecord, GovernancePolicy, MaintenanceAction, ProcessSurface, Requirement, SurfaceCapability, + AuditRecord, GovernancePolicy, MaintenanceAction, ProcessSurface, Requirement, + SurfaceCapability, }; use crate::AgentRole; use std::fmt::Debug; @@ -172,7 +173,10 @@ impl AutoresearchSurface { } } - pub fn run_experiment(&mut self, experiment: R::Experiment) -> Result { + pub fn run_experiment( + &mut self, + experiment: R::Experiment, + ) -> Result { if self.experiment_count >= self.max_experiments { return Err(AutoresearchError::MaxExperiments(self.max_experiments)); } @@ -235,7 +239,10 @@ where surface_name: "autoresearch".into(), uptime: Duration::from_secs(0), exit_reason: format!("{} experiments completed", handle.len()), - crash_count: handle.iter().filter(|l| matches!(l.verdict, ExperimentVerdict::Fail { .. })).count() as u32, + crash_count: handle + .iter() + .filter(|l| matches!(l.verdict, ExperimentVerdict::Fail { .. })) + .count() as u32, bytes_logged: 0, }) } @@ -270,21 +277,31 @@ mod tests { fn cargo_test_researcher_judge_regression() { let r = CargoTestResearcher::new("test-crate"); let verdict = r.judge(Some(&TestMetric(5, 5)), &TestMetric(3, 5)); - assert!(matches!(verdict, ExperimentVerdict::Fail { reason } if reason.contains("regression"))); + assert!( + matches!(verdict, ExperimentVerdict::Fail { reason } if reason.contains("regression")) + ); } #[test] fn autoresearch_surface_experiment_limits() { let researcher = CargoTestResearcher::new("test"); let mut surface = AutoresearchSurface::new(researcher, 2); - assert!(surface.run_experiment(TestExperiment::new("", "exp 1")).is_ok()); - assert!(surface.run_experiment(TestExperiment::new("", "exp 2")).is_ok()); - assert!(surface.run_experiment(TestExperiment::new("", "exp 3")).is_err()); + assert!(surface + .run_experiment(TestExperiment::new("", "exp 1")) + .is_ok()); + assert!(surface + .run_experiment(TestExperiment::new("", "exp 2")) + .is_ok()); + assert!(surface + .run_experiment(TestExperiment::new("", "exp 3")) + .is_err()); } #[test] fn experiment_verdict_display() { - let e = ExperimentVerdict::Fail { reason: "timeout".into() }; + let e = ExperimentVerdict::Fail { + reason: "timeout".into(), + }; assert!(matches!(e, ExperimentVerdict::Fail { .. })); } } diff --git a/crates/b00t-iface/src/gated/mod.rs b/crates/b00t-iface/src/gated/mod.rs index 89f4fcd..b38e908 100644 --- a/crates/b00t-iface/src/gated/mod.rs +++ b/crates/b00t-iface/src/gated/mod.rs @@ -5,8 +5,8 @@ //! Within l3dg3rr, they are maintained here with the intent to contribute. // Re-export so the parent crate can use them when feature is enabled -pub mod opencode_provider; pub mod autoresearch; +pub mod opencode_provider; -pub use opencode_provider::*; pub use autoresearch::*; +pub use opencode_provider::*; diff --git a/crates/b00t-iface/src/handshake/mod.rs b/crates/b00t-iface/src/handshake/mod.rs index 4575e95..e4c1f90 100644 --- a/crates/b00t-iface/src/handshake/mod.rs +++ b/crates/b00t-iface/src/handshake/mod.rs @@ -17,7 +17,10 @@ //! The handshake IS the integration — it's a b00t surface that, on operate(), //! performs the full exchange. -use crate::core::{AuditRecord, GovernancePolicy, MaintenanceAction, ProcessSurface, Requirement, SurfaceCapability}; +use crate::core::{ + AuditRecord, GovernancePolicy, MaintenanceAction, ProcessSurface, Requirement, + SurfaceCapability, +}; use crate::AgentRole; use serde::{Deserialize, Serialize}; use std::path::Path; @@ -155,10 +158,9 @@ impl HandshakeSurface { version: "1.0.0".into(), }; - let json = serde_json::to_string_pretty(&doc) - .map_err(|e| HandshakeError::Write(e.to_string()))?; - std::fs::write(self.doc_path(), json) - .map_err(|e| HandshakeError::Write(e.to_string()))?; + let json = + serde_json::to_string_pretty(&doc).map_err(|e| HandshakeError::Write(e.to_string()))?; + std::fs::write(self.doc_path(), json).map_err(|e| HandshakeError::Write(e.to_string()))?; Ok(()) } @@ -166,18 +168,18 @@ impl HandshakeSurface { fn read_peer(&self) -> Result, HandshakeError> { // b00t writes to ~/.b00t/mesh/l3dg3rr.handshake; // we check if it exists and parse it. - let b00t_path = dirs::home_dir() - .map(|h| h.join(".b00t").join("mesh").join("l3dg3rr.handshake")); + let b00t_path = + dirs::home_dir().map(|h| h.join(".b00t").join("mesh").join("l3dg3rr.handshake")); let path = match b00t_path { Some(p) if p.exists() => p, _ => return Ok(None), }; - let content = std::fs::read_to_string(&path) - .map_err(|e| HandshakeError::Read(e.to_string()))?; - let doc: HandshakeDocument = serde_json::from_str(&content) - .map_err(|e| HandshakeError::Parse(e.to_string()))?; + let content = + std::fs::read_to_string(&path).map_err(|e| HandshakeError::Read(e.to_string()))?; + let doc: HandshakeDocument = + serde_json::from_str(&content).map_err(|e| HandshakeError::Parse(e.to_string()))?; Ok(Some(doc)) } @@ -214,7 +216,9 @@ impl ProcessSurface for HandshakeSurface { fn capability(&self) -> SurfaceCapability { SurfaceCapability { name: "handshake", - requirements: vec![Requirement::PathExists(self.handshake_dir.display().to_string())], + requirements: vec![Requirement::PathExists( + self.handshake_dir.display().to_string(), + )], governance: GovernancePolicy { allowed_starters: vec![AgentRole::Executive], max_ttl: Duration::from_secs(3600), @@ -232,7 +236,11 @@ impl ProcessSurface for HandshakeSurface { self.heartbeat_interval = Duration::from_secs(config.heartbeat_secs); std::fs::create_dir_all(&self.handshake_dir) .map_err(|e| HandshakeError::Dir(e.to_string()))?; - tracing::info!("HandshakeSurface initialized: {}@{}", self.identity, self.host); + tracing::info!( + "HandshakeSurface initialized: {}@{}", + self.identity, + self.host + ); Ok(()) } @@ -262,8 +270,14 @@ impl ProcessSurface for HandshakeSurface { let peer_doc = surface_clone.peer_doc; Ok(HandshakeHandle { result, - peer_surfaces: peer_doc.as_ref().map(|d| d.surfaces.clone()).unwrap_or_default(), - peer_models: peer_doc.as_ref().map(|d| d.models.clone()).unwrap_or_default(), + peer_surfaces: peer_doc + .as_ref() + .map(|d| d.surfaces.clone()) + .unwrap_or_default(), + peer_models: peer_doc + .as_ref() + .map(|d| d.models.clone()) + .unwrap_or_default(), }) } diff --git a/crates/b00t-iface/src/lib.rs b/crates/b00t-iface/src/lib.rs index 6085bac..01c8889 100644 --- a/crates/b00t-iface/src/lib.rs +++ b/crates/b00t-iface/src/lib.rs @@ -21,9 +21,9 @@ pub mod core; pub mod exec; +pub mod metric; pub mod sarif; pub mod viz; -pub mod metric; #[cfg(feature = "b00t")] pub mod ralph; diff --git a/crates/b00t-iface/src/llm/mod.rs b/crates/b00t-iface/src/llm/mod.rs index 72203b7..ec01293 100644 --- a/crates/b00t-iface/src/llm/mod.rs +++ b/crates/b00t-iface/src/llm/mod.rs @@ -12,7 +12,10 @@ //! (when `autoresearch` feature is active), enabling self-play: the machine //! calls itself to evaluate prompt variants and keeps the best. -use crate::core::{AuditRecord, GovernancePolicy, MaintenanceAction, ProcessSurface, Requirement, SurfaceCapability}; +use crate::core::{ + AuditRecord, GovernancePolicy, MaintenanceAction, ProcessSurface, Requirement, + SurfaceCapability, +}; use crate::AgentRole; use serde::{Deserialize, Serialize}; use std::time::Duration; @@ -201,11 +204,13 @@ impl ProcessSurface for LlmMachine { fn capability(&self) -> SurfaceCapability { SurfaceCapability { name: "llm-machine", - requirements: vec![ - Requirement::PortAvailable(15115), - ], + requirements: vec![Requirement::PortAvailable(15115)], governance: GovernancePolicy { - allowed_starters: vec![AgentRole::Executive, AgentRole::Operator, AgentRole::Specialist], + allowed_starters: vec![ + AgentRole::Executive, + AgentRole::Operator, + AgentRole::Specialist, + ], max_ttl: Duration::from_secs(86400), auto_restart: true, crash_budget: 10, @@ -217,7 +222,11 @@ impl ProcessSurface for LlmMachine { self.config = config; self.total_completions = 0; self.total_tokens = 0; - tracing::info!("LlmMachine initialized: {} @ {}", self.config.model, self.config.endpoint); + tracing::info!( + "LlmMachine initialized: {} @ {}", + self.config.model, + self.config.endpoint + ); Ok(()) } @@ -234,7 +243,10 @@ impl ProcessSurface for LlmMachine { Ok(AuditRecord { surface_name: "llm-machine".into(), uptime: Duration::from_secs(0), - exit_reason: format!("{} completions, {} tokens", handle.completions, handle.tokens), + exit_reason: format!( + "{} completions, {} tokens", + handle.completions, handle.tokens + ), crash_count: 0, bytes_logged: handle.tokens * 4, }) @@ -260,7 +272,10 @@ mod tests { let cap = m.capability(); assert_eq!(cap.name, "llm-machine"); assert_eq!(cap.governance.crash_budget, 10); - assert!(cap.requirements.iter().any(|r| matches!(r, Requirement::PortAvailable(15115)))); + assert!(cap + .requirements + .iter() + .any(|r| matches!(r, Requirement::PortAvailable(15115)))); } #[test] diff --git a/crates/b00t-iface/src/metric/mod.rs b/crates/b00t-iface/src/metric/mod.rs index 9ccf520..fb87caf 100644 --- a/crates/b00t-iface/src/metric/mod.rs +++ b/crates/b00t-iface/src/metric/mod.rs @@ -133,7 +133,11 @@ impl MetricRegistry { // Find or create stream let target_key = key.to_owned(); let target_surface = surface.to_owned(); - if let Some(stream) = self.streams.iter_mut().find(|s| s.surface == target_surface && s.key == target_key) { + if let Some(stream) = self + .streams + .iter_mut() + .find(|s| s.surface == target_surface && s.key == target_key) + { let prev = stream.latest().map(|o| o.value.clone()); stream.observations.push(obs); if let Some(from) = prev { @@ -154,10 +158,20 @@ impl MetricRegistry { } /// Record a set of standard lifecycle metrics from a state machine. - pub fn record_lifecycle(&mut self, surface: &str, state: &str, crash_count: u32, uptime: Duration) { + pub fn record_lifecycle( + &mut self, + surface: &str, + state: &str, + crash_count: u32, + uptime: Duration, + ) { self.record(surface, "state", MetricValue::State(state.to_owned())); self.record(surface, "crashes", MetricValue::Counter(crash_count as u64)); - self.record(surface, "uptime_ms", MetricValue::DurationMs(uptime.as_millis() as u64)); + self.record( + surface, + "uptime_ms", + MetricValue::DurationMs(uptime.as_millis() as u64), + ); } /// Update all animations by the given delta. @@ -200,8 +214,14 @@ mod tests { reg.record_lifecycle("autoresearch", "idle", 0, Duration::from_secs(60)); let flat = reg.flat_display(); - assert_eq!(flat.get("watcher").unwrap().get("state").unwrap(), "running"); - assert_eq!(flat.get("autoresearch").unwrap().get("uptime_ms").unwrap(), "60000ms"); + assert_eq!( + flat.get("watcher").unwrap().get("state").unwrap(), + "running" + ); + assert_eq!( + flat.get("autoresearch").unwrap().get("uptime_ms").unwrap(), + "60000ms" + ); // 2 from watcher (state, crashes) + 3 from autoresearch (state, crashes, uptime_ms) = 5 streams assert_eq!(reg.streams.len(), 5); } @@ -274,6 +294,8 @@ mod tests { assert_eq!(MetricValue::Counter(42).to_string(), "42"); assert_eq!(MetricValue::State("running".into()).to_string(), "running"); assert_eq!(MetricValue::DurationMs(5000).to_string(), "5000ms"); - assert!(!MetricValue::Gauge(std::f64::consts::PI).to_string().is_empty()); + assert!(!MetricValue::Gauge(std::f64::consts::PI) + .to_string() + .is_empty()); } } diff --git a/crates/b00t-iface/src/ralph.rs b/crates/b00t-iface/src/ralph.rs index e566c8a..b11386f 100644 --- a/crates/b00t-iface/src/ralph.rs +++ b/crates/b00t-iface/src/ralph.rs @@ -23,10 +23,11 @@ //! - `cadence`: min delay between iterations use crate::core::{ - AuditRecord, GovernancePolicy, MaintenanceAction, ProcessSurface, Requirement, SurfaceCapability, + AuditRecord, GovernancePolicy, MaintenanceAction, ProcessSurface, Requirement, + SurfaceCapability, }; -use crate::AgentRole; use crate::gated::autoresearch::{Experiment, ExperimentLog, ExperimentVerdict, Researcher}; +use crate::AgentRole; use std::time::{Duration, Instant}; /// Properties that define a ralph loop's behavior. @@ -101,13 +102,19 @@ impl RalphLoopSurface { } let iter_start = Instant::now(); - let experiment = self.researcher.propose(&self.history.iter().map(|h| ExperimentLog { - number: h.number, - hypothesis: h.hypothesis.clone(), - verdict: h.verdict.clone(), - eval_time: h.elapsed, - target_file: String::new(), - }).collect::>()); + let experiment = self.researcher.propose( + &self + .history + .iter() + .map(|h| ExperimentLog { + number: h.number, + hypothesis: h.hypothesis.clone(), + verdict: h.verdict.clone(), + eval_time: h.elapsed, + target_file: String::new(), + }) + .collect::>(), + ); let hypothesis = experiment.hypothesis().to_owned(); let number = self.history.len() as u32 + 1; @@ -276,9 +283,12 @@ mod tests { }, ); let verdict = loop_surface.iterate().unwrap(); - assert_eq!(verdict, ExperimentVerdict::Fail { - reason: "no tests ran".into() - }); + assert_eq!( + verdict, + ExperimentVerdict::Fail { + reason: "no tests ran".into() + } + ); assert_eq!(loop_surface.history.len(), 1); } @@ -349,7 +359,7 @@ mod tests { RalphLoopProperties::default(), ); let handle = Vec::new(); - let audit = >::terminate(handle).unwrap(); + let audit = >::terminate(handle).unwrap(); assert_eq!(audit.surface_name, "ralph-loop"); } diff --git a/crates/b00t-iface/src/sarif/mod.rs b/crates/b00t-iface/src/sarif/mod.rs index da35687..d44e810 100644 --- a/crates/b00t-iface/src/sarif/mod.rs +++ b/crates/b00t-iface/src/sarif/mod.rs @@ -187,7 +187,9 @@ impl SarifLog { pub fn has_errors(&self) -> bool { self.runs.iter().any(|r| { - r.results.iter().any(|res| matches!(res.level, SarifLevel::Error)) + r.results + .iter() + .any(|res| matches!(res.level, SarifLevel::Error)) }) } @@ -215,8 +217,12 @@ impl LintRule { pub fn to_sarif_rule(&self) -> SarifRule { SarifRule { id: self.id.clone(), - short_description: Some(SarifMessage { text: self.short_desc.clone() }), - full_description: Some(SarifMessage { text: self.long_desc.clone() }), + short_description: Some(SarifMessage { + text: self.short_desc.clone(), + }), + full_description: Some(SarifMessage { + text: self.long_desc.clone(), + }), help_uri: None, properties: HashMap::new(), } @@ -226,14 +232,20 @@ impl LintRule { SarifResult { rule_id: self.id.clone(), level: self.level.clone(), - message: SarifMessage { text: self.long_desc.clone() }, + message: SarifMessage { + text: self.long_desc.clone(), + }, locations: Some(vec![SarifLocation { physical_location: SarifPhysicalLocation { - artifact_location: SarifArtifactLocation { uri: file.to_owned() }, + artifact_location: SarifArtifactLocation { + uri: file.to_owned(), + }, region: SarifRegion { start_line: line, end_line: None, - snippet: Some(SarifMessage { text: snippet.to_owned() }), + snippet: Some(SarifMessage { + text: snippet.to_owned(), + }), }, }, }]), @@ -319,9 +331,7 @@ pub fn l3dg3rr_doc_rules() -> Vec { } /// Generate a SARIF report for a set of doc/code rule violations. -pub fn check_l3dg3rr_standards( - doc_files: &[(&str, &str)], -) -> SarifLog { +pub fn check_l3dg3rr_standards(doc_files: &[(&str, &str)]) -> SarifLog { let rules = l3dg3rr_doc_rules(); let mut log = SarifLog::new(); let mut run = SarifRun::new("l3dg3rr-lint", "1.0.0"); @@ -332,31 +342,54 @@ pub fn check_l3dg3rr_standards( for (file, content) in doc_files { for rule in &rules { - if rule.id.contains("mermaid-parse") && content.contains("```rhai") && !content.contains("flowchart TD") { - run.add_result(rule.to_result(file, first_line_containing(content, "```rhai"), "Rhai fence found but no Mermaid output detected")); + if rule.id.contains("mermaid-parse") + && content.contains("```rhai") + && !content.contains("flowchart TD") + { + run.add_result(rule.to_result( + file, + first_line_containing(content, "```rhai"), + "Rhai fence found but no Mermaid output detected", + )); } if rule.id.contains("iso-projection") && content.contains("isoProject") { let line = first_line_containing(content, "isoProject"); if !content.contains("0.866") || !content.contains("0.5") { - run.add_result(rule.to_result(file, line, "isoProject may not use standard 2:1 dimetric constants")); + run.add_result(rule.to_result( + file, + line, + "isoProject may not use standard 2:1 dimetric constants", + )); } } if rule.id.contains("cross-ref") { // Simple heuristic: check for broken markdown links for (i, line_content) in content.lines().enumerate() { - if line_content.contains("](./") && !line_content.ends_with(".md)") && !line_content.ends_with(".md#") { + if line_content.contains("](./") + && !line_content.ends_with(".md)") + && !line_content.ends_with(".md#") + { // Potential broken reference — report as note run.add_result(SarifResult { rule_id: rule.id.clone(), level: SarifLevel::Note, - message: SarifMessage { text: format!("Potential broken cross-ref: {}", line_content.trim()) }, + message: SarifMessage { + text: format!( + "Potential broken cross-ref: {}", + line_content.trim() + ), + }, locations: Some(vec![SarifLocation { physical_location: SarifPhysicalLocation { - artifact_location: SarifArtifactLocation { uri: file.to_string() }, + artifact_location: SarifArtifactLocation { + uri: file.to_string(), + }, region: SarifRegion { start_line: (i + 1) as u64, end_line: None, - snippet: Some(SarifMessage { text: line_content.to_owned() }), + snippet: Some(SarifMessage { + text: line_content.to_owned(), + }), }, }, }]), @@ -399,7 +432,9 @@ pub fn check_l3dg3rr_standards( && content.contains("tool_name") && content.contains("match") { - if !content.contains("McpProviderRegistry") && !content.contains("handle_external_tool") { + if !content.contains("McpProviderRegistry") + && !content.contains("handle_external_tool") + { run.add_result(rule.to_result( file, first_line_containing(content, "fn handle_request"), @@ -412,7 +447,8 @@ pub fn check_l3dg3rr_standards( if rule.id.contains("z3-kasuari-coherence") { // Check that both Z3 and Kasuari concepts are used together if content.contains("verify_legal") || content.contains("LegalSolver::verify") { - if !content.contains("constraints") && !content.contains("VendorConstraintSet") { + if !content.contains("constraints") && !content.contains("VendorConstraintSet") + { run.add_result(rule.to_result( file, first_line_containing(content, "verify_legal"), @@ -439,7 +475,11 @@ fn first_line_containing(content: &str, needle: &str) -> u64 { /// Evaluate an observable OTel/Rotel build-gate expression using existing /// symbolic logic helpers. Supported forms are `log_shape && metric` and /// `log_shape || metric`; AND/OR are derived from NAND/NOR. -pub fn evaluate_otel_logic_expression(expression: &str, log_shape_observed: bool, metric_observed: bool) -> bool { +pub fn evaluate_otel_logic_expression( + expression: &str, + log_shape_observed: bool, + metric_observed: bool, +) -> bool { let tokens = tokenize_shorthand(expression); if tokens.iter().any(|t| matches!(t, ShorthandToken::Or)) { !nor(log_shape_observed, metric_observed) @@ -463,14 +503,34 @@ pub fn check_otel_logic_slo_as_sarif( let surface = format!("rotel-otel:{gate_name}"); let sli_met = evaluate_otel_logic_expression(expression, log_shape_observed, metric_observed); - registry.record(&surface, "log_shape_observed", MetricValue::Counter(log_shape_observed as u64)); - registry.record(&surface, "metric_observed", MetricValue::Counter(metric_observed as u64)); - registry.record(&surface, "sli_met", MetricValue::Gauge(if sli_met { 1.0 } else { 0.0 })); - registry.record(&surface, "slo_expected", MetricValue::Gauge(if slo_expected { 1.0 } else { 0.0 })); + registry.record( + &surface, + "log_shape_observed", + MetricValue::Counter(log_shape_observed as u64), + ); + registry.record( + &surface, + "metric_observed", + MetricValue::Counter(metric_observed as u64), + ); + registry.record( + &surface, + "sli_met", + MetricValue::Gauge(if sli_met { 1.0 } else { 0.0 }), + ); + registry.record( + &surface, + "slo_expected", + MetricValue::Gauge(if slo_expected { 1.0 } else { 0.0 }), + ); registry.record( &surface, "build_gate", - MetricValue::State(if sli_met == slo_expected { "pass".into() } else { "fail".into() }), + MetricValue::State(if sli_met == slo_expected { + "pass".into() + } else { + "fail".into() + }), ); let rule = LintRule { @@ -492,14 +552,22 @@ pub fn check_otel_logic_slo_as_sarif( properties.insert("sli.expression".to_string(), expression.to_string()); properties.insert("sli.value".to_string(), sli_met.to_string()); properties.insert("slo.expected".to_string(), slo_expected.to_string()); - properties.insert("otel.log_shape_observed".to_string(), log_shape_observed.to_string()); - properties.insert("otel.metric_observed".to_string(), metric_observed.to_string()); + properties.insert( + "otel.log_shape_observed".to_string(), + log_shape_observed.to_string(), + ); + properties.insert( + "otel.metric_observed".to_string(), + metric_observed.to_string(), + ); properties.insert("rotel.surface".to_string(), surface.clone()); run.add_result(SarifResult { rule_id: rule.id, level: SarifLevel::Error, - message: SarifMessage { text: rule.long_desc }, + message: SarifMessage { + text: rule.long_desc, + }, locations: None, fixes: None, properties: Some(properties), @@ -532,7 +600,9 @@ mod tests { run.add_result(SarifResult { rule_id: "test/error".into(), level: SarifLevel::Error, - message: SarifMessage { text: "something broke".into() }, + message: SarifMessage { + text: "something broke".into(), + }, locations: None, fixes: None, properties: None, @@ -554,7 +624,9 @@ mod tests { fn lint_detects_missing_mermaid() { let content = "# Test\n\n```rhai\nfn foo() -> bar\n```\n\nNo mermaid output"; let report = check_l3dg3rr_standards(&[("test.md", content)]); - let mermaid_results: Vec<_> = report.runs[0].results.iter() + let mermaid_results: Vec<_> = report.runs[0] + .results + .iter() .filter(|r| r.rule_id.contains("mermaid-parse")) .collect(); assert!(!mermaid_results.is_empty(), "should flag missing mermaid"); @@ -564,7 +636,9 @@ mod tests { fn valid_mermaid_passes_lint() { let content = "```rhai\nfn foo() -> bar\n```\n\n```mermaid\nflowchart TD\nfoo[bar]\n```"; let report = check_l3dg3rr_standards(&[("good.md", content)]); - let mermaid_results: Vec<_> = report.runs[0].results.iter() + let mermaid_results: Vec<_> = report.runs[0] + .results + .iter() .filter(|r| r.rule_id.contains("mermaid-parse")) .collect(); // With valid mermaid content following a rhai fence, the heuristic may @@ -577,10 +651,15 @@ mod tests { fn iso_projection_constants_checked() { let content = "function isoProject(pt, scale, origin) { return {x: 0, y: 0}; }"; let report = check_l3dg3rr_standards(&[("viz.js", content)]); - let iso_results: Vec<_> = report.runs[0].results.iter() + let iso_results: Vec<_> = report.runs[0] + .results + .iter() .filter(|r| r.rule_id.contains("iso-projection")) .collect(); - assert!(!iso_results.is_empty(), "should flag missing 0.866/0.5 constants"); + assert!( + !iso_results.is_empty(), + "should flag missing 0.866/0.5 constants" + ); } #[test] @@ -590,7 +669,9 @@ mod tests { run.add_result(SarifResult { rule_id: "test/note".into(), level: SarifLevel::Note, - message: SarifMessage { text: "info".into() }, + message: SarifMessage { + text: "info".into(), + }, locations: None, fixes: None, properties: None, @@ -607,7 +688,9 @@ mod tests { fn dead_builder_field_detected() { let content = "#[allow(dead_code)]\nmax_retries: usize,\nenable_legal_verification: bool,\n}\n\npub fn build(self) -> LedgerPipeline {"; let report = check_l3dg3rr_standards(&[("pipeline.rs", content)]); - let dead_results: Vec<_> = report.runs[0].results.iter() + let dead_results: Vec<_> = report.runs[0] + .results + .iter() .filter(|r| r.rule_id.contains("dead-builder-field")) .collect(); assert!(!dead_results.is_empty(), "should flag dead builder fields"); @@ -618,27 +701,39 @@ mod tests { // Simulate content where build() references PipelineBuilder but not LegalSolver let content = "impl PipelineBuilder {\n pub fn build(self) -> LedgerPipeline {\n LedgerPipeline::new(self.jurisdiction)\n }\n}"; let report = check_l3dg3rr_standards(&[("pipeline.rs", content)]); - let legal_results: Vec<_> = report.runs[0].results.iter() + let legal_results: Vec<_> = report.runs[0] + .results + .iter() .filter(|r| r.rule_id.contains("unwired-legal-verification")) .collect(); - assert!(!legal_results.is_empty(), "should flag unwired legal verification"); + assert!( + !legal_results.is_empty(), + "should flag unwired legal verification" + ); } #[test] fn wired_legal_verification_passes() { let content = "impl PipelineBuilder {\n pub fn build(self) -> LedgerPipeline {\n let solver = LegalSolver::new();\n LedgerPipeline::new(self.jurisdiction).with_legal_solver(solver)\n }\n}"; let report = check_l3dg3rr_standards(&[("pipeline.rs", content)]); - let legal_results: Vec<_> = report.runs[0].results.iter() + let legal_results: Vec<_> = report.runs[0] + .results + .iter() .filter(|r| r.rule_id.contains("unwired-legal-verification")) .collect(); - assert!(legal_results.is_empty(), "should pass when LegalSolver is wired"); + assert!( + legal_results.is_empty(), + "should pass when LegalSolver is wired" + ); } #[test] fn mcp_provider_unwired_detected() { let content = "fn handle_request(request: Value) -> Option {\n let tool_name = params.get(\"name\").and_then(Value::as_str).unwrap_or(\"\");\n match tool_name {\n mcp_adapter::DOCUMENTS_TOOL => { }\n _ => mcp_adapter::unknown_tool_result(tool_name),\n }\n}"; let report = check_l3dg3rr_standards(&[("ledgerr-mcp-server.rs", content)]); - let mcp_results: Vec<_> = report.runs[0].results.iter() + let mcp_results: Vec<_> = report.runs[0] + .results + .iter() .filter(|r| r.rule_id.contains("mcp-provider-unwired")) .collect(); assert!(!mcp_results.is_empty(), "should flag unwired MCP provider"); @@ -649,20 +744,30 @@ mod tests { // Pipeline has legal verification but no constraint checking let content = "fn verify_legal(&self, solver, rules) {\n let result = solver.verify(rule, facts);\n // no constraint evaluation after legal check\n}"; let report = check_l3dg3rr_standards(&[("pipeline.rs", content)]); - let coherence_results: Vec<_> = report.runs[0].results.iter() + let coherence_results: Vec<_> = report.runs[0] + .results + .iter() .filter(|r| r.rule_id.contains("z3-kasuari-coherence")) .collect(); - assert!(!coherence_results.is_empty(), "should flag missing kasuari constraints after z3"); + assert!( + !coherence_results.is_empty(), + "should flag missing kasuari constraints after z3" + ); } #[test] fn z3_kasuari_coherence_passes_when_composed() { let content = "fn process_tx(&self, solver, rules, constraints) {\n let result = solver.verify_all(rules, &facts);\n let eval = constraints.evaluate(amount, day, tax_code, account);\n}"; let report = check_l3dg3rr_standards(&[("pipeline.rs", content)]); - let coherence_results: Vec<_> = report.runs[0].results.iter() + let coherence_results: Vec<_> = report.runs[0] + .results + .iter() .filter(|r| r.rule_id.contains("z3-kasuari-coherence")) .collect(); - assert!(coherence_results.is_empty(), "should pass when both z3 and constraints are used"); + assert!( + coherence_results.is_empty(), + "should pass when both z3 and constraints are used" + ); } #[test] @@ -679,9 +784,17 @@ mod tests { assert!(!report.has_errors()); let flat = registry.flat_display(); - let surface = flat.get("rotel-otel:gpu-driver-fault").expect("surface metrics"); - assert_eq!(surface.get("log_shape_observed").map(String::as_str), Some("1")); - assert_eq!(surface.get("metric_observed").map(String::as_str), Some("1")); + let surface = flat + .get("rotel-otel:gpu-driver-fault") + .expect("surface metrics"); + assert_eq!( + surface.get("log_shape_observed").map(String::as_str), + Some("1") + ); + assert_eq!( + surface.get("metric_observed").map(String::as_str), + Some("1") + ); assert_eq!(surface.get("build_gate").map(String::as_str), Some("pass")); } @@ -701,9 +814,15 @@ mod tests { let result = &report.runs[0].results[0]; assert_eq!(result.rule_id, "l3dg3rr/otel/build-gate-slo"); let props = result.properties.as_ref().expect("sarif properties"); - assert_eq!(props.get("sli.expression").map(String::as_str), Some("log_shape && metric")); + assert_eq!( + props.get("sli.expression").map(String::as_str), + Some("log_shape && metric") + ); assert_eq!(props.get("sli.value").map(String::as_str), Some("false")); - assert_eq!(props.get("otel.metric_observed").map(String::as_str), Some("false")); + assert_eq!( + props.get("otel.metric_observed").map(String::as_str), + Some("false") + ); } #[test] @@ -719,13 +838,21 @@ mod tests { ); assert!(!report.has_errors()); - assert!(evaluate_otel_logic_expression("log_shape || metric", true, false)); + assert!(evaluate_otel_logic_expression( + "log_shape || metric", + true, + false + )); } #[test] fn runtime_rules_have_all_ids() { let rules = l3dg3rr_doc_rules(); - assert!(rules.len() >= 11, "expected at least 11 rules (7 doc + 4 runtime), got {}", rules.len()); + assert!( + rules.len() >= 11, + "expected at least 11 rules (7 doc + 4 runtime), got {}", + rules.len() + ); let runtime_ids = [ "l3dg3rr/code/dead-builder-field", "l3dg3rr/code/unwired-legal-verification", diff --git a/crates/b00t-iface/src/viz/mod.rs b/crates/b00t-iface/src/viz/mod.rs index 521b065..850820b 100644 --- a/crates/b00t-iface/src/viz/mod.rs +++ b/crates/b00t-iface/src/viz/mod.rs @@ -16,7 +16,7 @@ use serde::Serialize; /// 2:1 dimetric isometric projection constants. pub const ISO_SCALE_X: f32 = 0.8660254; // cos(30°) -pub const ISO_SCALE_Y: f32 = 0.5; // sin(30°) +pub const ISO_SCALE_Y: f32 = 0.5; // sin(30°) /// A 2D point in screen space. #[derive(Debug, Clone, Copy, PartialEq, Serialize)] @@ -156,7 +156,10 @@ impl SceneGraph { max_y = max_y.max(p.y); } - (Point2D { x: min_x, y: min_y }, Point2D { x: max_x, y: max_y }) + ( + Point2D { x: min_x, y: min_y }, + Point2D { x: max_x, y: max_y }, + ) } } @@ -219,7 +222,10 @@ pub fn scene_to_svg(scene: &SceneGraph, theme: &SceneTheme) -> String { let d = if edge.is_bezier { let cpx = (fp.x + tp.x) / 2.0; let cpy = (fp.y + tp.y) / 2.0 - 30.0; - format!("M {:.1} {:.1} Q {:.1} {:.1} {:.1} {:.1}", fp.x, fp.y, cpx, cpy, tp.x, tp.y) + format!( + "M {:.1} {:.1} Q {:.1} {:.1} {:.1} {:.1}", + fp.x, fp.y, cpx, cpy, tp.x, tp.y + ) } else { format!("M {:.1} {:.1} L {:.1} {:.1}", fp.x, fp.y, tp.x, tp.y) }; @@ -251,7 +257,9 @@ pub fn scene_to_svg(scene: &SceneGraph, theme: &SceneTheme) -> String { let role_label = node.role.to_string(); let hw = w / 2.0; svg.push_str(&format!( - r#""#, tx = p.x - hw, ty = p.y - h / 2.0 + r#""#, + tx = p.x - hw, + ty = p.y - h / 2.0 )); // Right depth face svg.push_str(&format!( @@ -304,14 +312,22 @@ mod tests { // JS: isoProject({x:1,y:0,z:0}, 1, {x:400,y:300}) // x = 400 + (1-0) * 1 * 0.8660254 = 400.866 // y = 300 + (1+0) * 1 * 0.5 - 0 * 1 = 300.5 - let p = iso_project(Point3D::new(1.0, 0.0, 0.0), 1.0, Point2D { x: 400.0, y: 300.0 }); + let p = iso_project( + Point3D::new(1.0, 0.0, 0.0), + 1.0, + Point2D { x: 400.0, y: 300.0 }, + ); assert!((p.x - 400.866).abs() < 0.001); assert!((p.y - 300.5).abs() < 0.001); } #[test] fn iso_project_origin_at_origin() { - let p = iso_project(Point3D::new(0.0, 0.0, 0.0), 32.0, Point2D { x: 0.0, y: 0.0 }); + let p = iso_project( + Point3D::new(0.0, 0.0, 0.0), + 32.0, + Point2D { x: 0.0, y: 0.0 }, + ); assert!((p.x - 0.0).abs() < 0.001); assert!((p.y - 0.0).abs() < 0.001); } diff --git a/crates/datum/src/ast.rs b/crates/datum/src/ast.rs index 356340b..620a51e 100644 --- a/crates/datum/src/ast.rs +++ b/crates/datum/src/ast.rs @@ -55,12 +55,22 @@ pub fn parse_datum(content: &str) -> Result { for line in content.lines() { if line.trim_start().starts_with("```") { in_code_block = !in_code_block; - append_body(&mut current_subsection, &mut current_section, &mut preamble, line); + append_body( + &mut current_subsection, + &mut current_section, + &mut preamble, + line, + ); continue; } if in_code_block { - append_body(&mut current_subsection, &mut current_section, &mut preamble, line); + append_body( + &mut current_subsection, + &mut current_section, + &mut preamble, + line, + ); continue; } @@ -121,7 +131,12 @@ pub fn parse_datum(content: &str) -> Result { continue; } - append_body(&mut current_subsection, &mut current_section, &mut preamble, line); + append_body( + &mut current_subsection, + &mut current_section, + &mut preamble, + line, + ); } if let Some(sub) = current_subsection.take() { @@ -248,9 +263,7 @@ pub fn lint_ast(ast: &DatumAst, datum_name: &str) -> Vec { } // Check for toml sections with key=value content - if !sec.heading.contains(' ') && !sec.heading.contains('_') - && sec.body.contains('=') - { + if !sec.heading.contains(' ') && !sec.heading.contains('_') && sec.body.contains('=') { lint_toml_section(sec, datum_name, &mut findings); } @@ -395,7 +408,14 @@ pub fn validate_datum_structure(ast: &DatumAst, datum_name: &str) -> Result<(), let errors: Vec = findings .into_iter() .filter(|f| f.severity == LintSeverity::Error) - .map(|f| format!("[{}] {}: {}", datum_name, f.message, f.section.unwrap_or_default())) + .map(|f| { + format!( + "[{}] {}: {}", + datum_name, + f.message, + f.section.unwrap_or_default() + ) + }) .collect(); if errors.is_empty() { @@ -456,7 +476,9 @@ mod tests { let content = "# Bare\n\nJust preamble, no sections\n"; let ast = parse_datum(content).unwrap(); let findings = lint_ast(&ast, "bare"); - assert!(findings.iter().any(|f| f.message.contains("no H2 or TOML sections"))); + assert!(findings + .iter() + .any(|f| f.message.contains("no H2 or TOML sections"))); } #[test] @@ -481,7 +503,9 @@ mod tests { let content = "# Test\n\n## Table\n\n| H1 | H2 |\n| A | B |\n| C | D |\n"; let ast = parse_datum(content).unwrap(); let findings = lint_ast(&ast, "test"); - assert!(findings.iter().any(|f| f.message.contains("header separator"))); + assert!(findings + .iter() + .any(|f| f.message.contains("header separator"))); } #[test] @@ -489,7 +513,9 @@ mod tests { let content = "# Test\n\n## Table\n\n| H1 | H2 |\n|----|----|\n| A | B |\n"; let ast = parse_datum(content).unwrap(); let findings = lint_ast(&ast, "test"); - assert!(!findings.iter().any(|f| f.message.contains("header separator"))); + assert!(!findings + .iter() + .any(|f| f.message.contains("header separator"))); } #[test] @@ -497,7 +523,9 @@ mod tests { let content = "# Test\n\n## Code\n\n```\nlet x = 1;\n```\n"; let ast = parse_datum(content).unwrap(); let findings = lint_ast(&ast, "test"); - assert!(findings.iter().any(|f| f.message.contains("language specifier"))); + assert!(findings + .iter() + .any(|f| f.message.contains("language specifier"))); } #[test] @@ -505,7 +533,9 @@ mod tests { let content = "# Test\n\n## Code\n\n```rust\nlet x = 1;\n```\n"; let ast = parse_datum(content).unwrap(); let findings = lint_ast(&ast, "test"); - assert!(!findings.iter().any(|f| f.message.contains("language specifier"))); + assert!(!findings + .iter() + .any(|f| f.message.contains("language specifier"))); } #[test] @@ -544,15 +574,15 @@ mod tests { // We try to include and catch compile errors — if it fails, the test is skipped. match name { "opencode" => Some(include_str!("../../../../_b00t_/datums/opencode.datum")), - "opencode-codebase-memory-integration" => { - Some(include_str!("../../../../_b00t_/datums/opencode-codebase-memory-integration.datum")) - } - "b00t-opencode-gaps" => { - Some(include_str!("../../../../_b00t_/datums/b00t-opencode-gaps.datum")) - } - "openagents-control" => { - Some(include_str!("../../../../_b00t_/datums/openagents-control.datum")) - } + "opencode-codebase-memory-integration" => Some(include_str!( + "../../../../_b00t_/datums/opencode-codebase-memory-integration.datum" + )), + "b00t-opencode-gaps" => Some(include_str!( + "../../../../_b00t_/datums/b00t-opencode-gaps.datum" + )), + "openagents-control" => Some(include_str!( + "../../../../_b00t_/datums/openagents-control.datum" + )), _ => None, } } @@ -576,14 +606,24 @@ mod tests { #[test] fn lint_real_datums() { - for name in &["opencode", "opencode-codebase-memory-integration", "b00t-opencode-gaps", "openagents-control"] { + for name in &[ + "opencode", + "opencode-codebase-memory-integration", + "b00t-opencode-gaps", + "openagents-control", + ] { let content = datum_content(name).unwrap(); let ast = parse_datum(content).unwrap(); let findings = lint_ast(&ast, name); - let errors: Vec<_> = findings.iter().filter(|f| f.severity == LintSeverity::Error).collect(); - assert!(errors.is_empty(), "datum {name} has lint errors: {errors:?}"); + let errors: Vec<_> = findings + .iter() + .filter(|f| f.severity == LintSeverity::Error) + .collect(); + assert!( + errors.is_empty(), + "datum {name} has lint errors: {errors:?}" + ); } } } - } diff --git a/crates/datum/src/lib.rs b/crates/datum/src/lib.rs index cd8f892..cb81bac 100644 --- a/crates/datum/src/lib.rs +++ b/crates/datum/src/lib.rs @@ -10,7 +10,10 @@ use std::path::Path; #[derive(Debug, thiserror::Error)] pub enum DatumError { #[error("I/O error reading datum file '{path}': {source}")] - Io { path: String, source: std::io::Error }, + Io { + path: String, + source: std::io::Error, + }, #[error("datum file '{path}' is empty")] Empty { path: String }, #[error("datum file '{path}' has no H1 header (line must start with '# ')")] @@ -107,11 +110,19 @@ mod tests { #[cfg(feature = "real_datums")] fn load_all_expected_datums() { let base = datum_base(); - for name in &["opencode-codebase-memory-integration", "opencode", "b00t-opencode-gaps", "openagents-control"] { - let datum = load_datum(&base, name) - .unwrap_or_else(|e| panic!("datum {name}: {e}")); + for name in &[ + "opencode-codebase-memory-integration", + "opencode", + "b00t-opencode-gaps", + "openagents-control", + ] { + let datum = load_datum(&base, name).unwrap_or_else(|e| panic!("datum {name}: {e}")); assert!(!datum.h1.is_empty(), "datum {name} has empty H1"); - assert!(datum.line_count > 3, "datum {name} too short ({})", datum.line_count); + assert!( + datum.line_count > 3, + "datum {name} too short ({})", + datum.line_count + ); } } diff --git a/crates/datum/src/logic.rs b/crates/datum/src/logic.rs index f9529ea..016c229 100644 --- a/crates/datum/src/logic.rs +++ b/crates/datum/src/logic.rs @@ -23,13 +23,16 @@ //! (wired to a source) before any transition can fire. An unfilled port //! produces `Disposition::Unrecoverable`. - - /// Gate integer codes used by the macro's static array generation. /// 0=NAND, 1=NOR, 2=ADD, 3=WAIT, 4=TX, 5=RX, 6=CAP pub const GATE_CODES: &[(u8, &str)] = &[ - (0, "NAND"), (1, "NOR"), (2, "ADD"), (3, "WAIT"), - (4, "TX"), (5, "RX"), (6, "CAP"), + (0, "NAND"), + (1, "NOR"), + (2, "ADD"), + (3, "WAIT"), + (4, "TX"), + (5, "RX"), + (6, "CAP"), ]; /// Port identifier within a meta-state machine. @@ -138,7 +141,13 @@ impl FluxCapacitor { id } - pub fn wire(&mut self, from_gate: usize, from_port: usize, to_gate: usize, to_port: usize) -> Result<(), String> { + pub fn wire( + &mut self, + from_gate: usize, + from_port: usize, + to_gate: usize, + to_port: usize, + ) -> Result<(), String> { if from_gate >= self.ports.len() { return Err(format!("from_gate {from_gate} does not exist")); } @@ -147,13 +156,22 @@ impl FluxCapacitor { } let from_kind = self.ports[from_gate].kind; if from_port >= from_kind.arity_out() { - return Err(format!("from_gate {from_gate} ({from_kind:?}) has no output port {from_port}")); + return Err(format!( + "from_gate {from_gate} ({from_kind:?}) has no output port {from_port}" + )); } let to_kind = self.ports[to_gate].kind; if to_port >= to_kind.arity_in() { - return Err(format!("to_gate {to_gate} ({to_kind:?}) has no input port {to_port}")); + return Err(format!( + "to_gate {to_gate} ({to_kind:?}) has no input port {to_port}" + )); } - let wire = Wire { from_gate, from_port, to_gate, to_port }; + let wire = Wire { + from_gate, + from_port, + to_gate, + to_port, + }; self.ports[to_gate].input_ports[to_port] = Some(wire.clone()); self.ports[from_gate].output_wire = Some(wire.clone()); self.wires.push(wire); @@ -167,7 +185,13 @@ impl FluxCapacitor { let required = kind.arity_in(); let filled = gate.input_ports.iter().filter(|p| p.is_some()).count(); if filled < required && kind != GateKind::Rx && kind != GateKind::Cap { - unfilled.push((gate.id, format!("{kind:?} gate {} has {filled}/{required} ports filled", gate.id))); + unfilled.push(( + gate.id, + format!( + "{kind:?} gate {} has {filled}/{required} ports filled", + gate.id + ), + )); } } unfilled @@ -224,11 +248,23 @@ pub fn tokenize_shorthand(input: &str) -> Vec { let mut chars = input.chars().peekable(); while let Some(c) = chars.next() { match c { - '&' if chars.peek() == Some(&'&') => { chars.next(); tokens.push(ShorthandToken::And); } - '|' if chars.peek() == Some(&'|') => { chars.next(); tokens.push(ShorthandToken::Or); } + '&' if chars.peek() == Some(&'&') => { + chars.next(); + tokens.push(ShorthandToken::And); + } + '|' if chars.peek() == Some(&'|') => { + chars.next(); + tokens.push(ShorthandToken::Or); + } '!' => tokens.push(ShorthandToken::Not), - '-' if chars.peek() == Some(&'>') => { chars.next(); tokens.push(ShorthandToken::Arrow); } - '<' if chars.peek() == Some(&'-') => { chars.next(); tokens.push(ShorthandToken::BackArrow); } + '-' if chars.peek() == Some(&'>') => { + chars.next(); + tokens.push(ShorthandToken::Arrow); + } + '<' if chars.peek() == Some(&'-') => { + chars.next(); + tokens.push(ShorthandToken::BackArrow); + } '=' => tokens.push(ShorthandToken::Eq), ':' => tokens.push(ShorthandToken::Colon), '{' => tokens.push(ShorthandToken::LBrace), @@ -245,7 +281,12 @@ pub fn tokenize_shorthand(input: &str) -> Vec { c if c.is_ascii_alphabetic() || c == '_' || c == '.' => { let mut s = String::from(c); while let Some(&c) = chars.peek() { - if c.is_ascii_alphanumeric() || c == '_' || c == '.' { s.push(c); chars.next(); } else { break; } + if c.is_ascii_alphanumeric() || c == '_' || c == '.' { + s.push(c); + chars.next(); + } else { + break; + } } tokens.push(ShorthandToken::Ident(s)); } @@ -257,11 +298,21 @@ pub fn tokenize_shorthand(input: &str) -> Vec { // ── Gate evaluation functions ──────────────────────────────────────────────── -pub fn nand(a: bool, b: bool) -> bool { !(a && b) } -pub fn nor(a: bool, b: bool) -> bool { !(a || b) } -pub fn add_u8(a: u8, b: u8) -> u8 { a.wrapping_add(b) } +pub fn nand(a: bool, b: bool) -> bool { + !(a && b) +} +pub fn nor(a: bool, b: bool) -> bool { + !(a || b) +} +pub fn add_u8(a: u8, b: u8) -> u8 { + a.wrapping_add(b) +} pub fn wait_stable(current: bool, previous: bool, debounce_ticks: u64, tick: u64) -> bool { - if current == previous && tick >= debounce_ticks { current } else { previous } + if current == previous && tick >= debounce_ticks { + current + } else { + previous + } } // ── MultiBridge: two gate networks composed under a typed invariant ───────── @@ -307,10 +358,16 @@ impl MultiBridge { for (s, t) in &self.wires { if seen_source.contains(s) { - issues.push(format!("bridge '{}': source port {s} wired multiple times", self.name)); + issues.push(format!( + "bridge '{}': source port {s} wired multiple times", + self.name + )); } if seen_target.contains(t) { - issues.push(format!("bridge '{}': target port {t} wired multiple times", self.name)); + issues.push(format!( + "bridge '{}': target port {t} wired multiple times", + self.name + )); } seen_source.push(*s); seen_target.push(*t); @@ -320,13 +377,23 @@ impl MultiBridge { } /// Apply the bridge wiring to a pair of capacitors. - pub fn apply(&self, source: &mut FluxCapacitor, target: &mut FluxCapacitor) -> Result<(), String> { + pub fn apply( + &self, + source: &mut FluxCapacitor, + target: &mut FluxCapacitor, + ) -> Result<(), String> { for (s_port, t_port) in &self.wires { if *s_port >= source.ports.len() { - return Err(format!("bridge '{}': source has no port {s_port}", self.name)); + return Err(format!( + "bridge '{}': source has no port {s_port}", + self.name + )); } if *t_port >= target.ports.len() { - return Err(format!("bridge '{}': target has no port {t_port}", self.name)); + return Err(format!( + "bridge '{}': target has no port {t_port}", + self.name + )); } // Wire source gate's output to target gate's input (port 0 → port 0) let _ = source.wire(*s_port, 0, *t_port, 0); @@ -336,13 +403,23 @@ impl MultiBridge { } /// Invariant validator: checks that a specific invariant holds across a bridge. -pub fn check_bridge_invariant(bridge: &MultiBridge, source: &FluxCapacitor, target: &FluxCapacitor) -> Vec { +pub fn check_bridge_invariant( + bridge: &MultiBridge, + source: &FluxCapacitor, + target: &FluxCapacitor, +) -> Vec { let mut violations = bridge.validate_invariants(); if !source.is_stable() { - violations.push(format!("bridge '{}': source capacitor not stable", bridge.name)); + violations.push(format!( + "bridge '{}': source capacitor not stable", + bridge.name + )); } if !target.is_stable() { - violations.push(format!("bridge '{}': target capacitor not stable", bridge.name)); + violations.push(format!( + "bridge '{}': target capacitor not stable", + bridge.name + )); } violations } @@ -407,7 +484,11 @@ impl ModelTransformer { } /// Apply the transformation by wiring input capacitor to output capacitor. - pub fn transform(&self, input: &mut FluxCapacitor, _output: &mut FluxCapacitor) -> Result<(), String> { + pub fn transform( + &self, + input: &mut FluxCapacitor, + _output: &mut FluxCapacitor, + ) -> Result<(), String> { self.check_port_compatibility()?; let n = self.input_shape.len().min(self.output_shape.len()); for i in 0..n { @@ -765,7 +846,11 @@ fn run_stable_test(kinds: &[GateKind]) { } } let stable = cap.evaluate(); - assert!(stable, "flux capacitor not meta-stable: {:?}", cap.all_ports_filled()); + assert!( + stable, + "flux capacitor not meta-stable: {:?}", + cap.all_ports_filled() + ); } #[cfg(test)] @@ -927,35 +1012,61 @@ mod tests { // ── symbolic_gate_test! invocations ──────────────────────────── - mod gate_nand_tx_cap { symbolic_gate_test!(NAND, TX, CAP); } - mod gate_rx_nand { symbolic_gate_test!(RX, NAND); } - mod gate_tx_only { symbolic_gate_test!(TX); } - mod gate_rx_wait_cap_stable { symbolic_gate_test!(stable => RX, WAIT, CAP); } + mod gate_nand_tx_cap { + symbolic_gate_test!(NAND, TX, CAP); + } + mod gate_rx_nand { + symbolic_gate_test!(RX, NAND); + } + mod gate_tx_only { + symbolic_gate_test!(TX); + } + mod gate_rx_wait_cap_stable { + symbolic_gate_test!(stable => RX, WAIT, CAP); + } // ── bridge_invariant_test! invocations ───────────────────────── // Rx → Nand in same capacitor: Rx output port 0 → Nand input port 0 - mod bridge_rx_to_nand { bridge_invariant_test!("rx2nand" => 0:Rx >> 0:Nand); } + mod bridge_rx_to_nand { + bridge_invariant_test!("rx2nand" => 0:Rx >> 0:Nand); + } // Wait → Nand: Wait output feeds Nand input - mod bridge_wait_nand { bridge_invariant_test!("wait2nand" => 0:Wait >> 0:Nand); } + mod bridge_wait_nand { + bridge_invariant_test!("wait2nand" => 0:Wait >> 0:Nand); + } // Rx, Rx >> Wait: two Rx gates wired in src capacitor, Wait in dst capacitor // Both capacitors are independently stable (Rx has no input ports, Wait is unfilled but skipped) // Actually both need internal wires — skip for now, use single-capacitor test instead - // + // // ── transformer_invariant_test! invocations ──────────────────── - mod tx_rx_nand_to_nor { transformer_invariant_test!("rx_nand_nor" => Rx, Nand ~> Nand, Nor); } - mod tx_single_rx_to_wait { transformer_invariant_test!("rx_wait" => Rx ~> Wait); } + mod tx_rx_nand_to_nor { + transformer_invariant_test!("rx_nand_nor" => Rx, Nand ~> Nand, Nor); + } + mod tx_single_rx_to_wait { + transformer_invariant_test!("rx_wait" => Rx ~> Wait); + } // Output gates must have input ports to receive transformed values - mod tx_three_chain { transformer_invariant_test!("chain" => Rx, Wait, Nand ~> Wait, Nor, Add); } + mod tx_three_chain { + transformer_invariant_test!("chain" => Rx, Wait, Nand ~> Wait, Nor, Add); + } // ── abstract_cap_test! invocations ───────────────────────────── - mod abs_single_pair { abstract_cap_test!("single" => [Rx, Wait, Cap]); } + mod abs_single_pair { + abstract_cap_test!("single" => [Rx, Wait, Cap]); + } // Two Rx feed Nand (both ports) → Nand stable → Wait stable - mod abs_two_networks { abstract_cap_test!("two_net" => [Rx, Wait] ~ [Rx, Wait]); } - mod abs_with_bridge_tx { abstract_cap_test!("w_bridge_tx" => [Rx, Wait] ~~ [Rx, Nand ~> Nand, Nor]); } - mod abs_three_net_bridge_tx { abstract_cap_test!("complex" => [Rx, Wait] ~ [Rx, Nand] ~~ [Rx ~> Wait]); } + mod abs_two_networks { + abstract_cap_test!("two_net" => [Rx, Wait] ~ [Rx, Wait]); + } + mod abs_with_bridge_tx { + abstract_cap_test!("w_bridge_tx" => [Rx, Wait] ~~ [Rx, Nand ~> Nand, Nor]); + } + mod abs_three_net_bridge_tx { + abstract_cap_test!("complex" => [Rx, Wait] ~ [Rx, Nand] ~~ [Rx ~> Wait]); + } // ── MultiBridge runtime tests ────────────────────────────────── @@ -971,7 +1082,9 @@ mod tests { let mut bridge = MultiBridge::new("dup", GateKind::Nand, GateKind::Nor); bridge.wire(0, 0).wire(0, 1); let violations = bridge.validate_invariants(); - assert!(violations.iter().any(|v| v.contains("wired multiple times"))); + assert!(violations + .iter() + .any(|v| v.contains("wired multiple times"))); } #[test] @@ -1127,6 +1240,8 @@ mod tests { abs.add_bridge(bridge); let violations = abs.check_all_invariants(); - assert!(violations.iter().any(|v| v.contains("wired multiple times"))); + assert!(violations + .iter() + .any(|v| v.contains("wired multiple times"))); } } diff --git a/crates/datum/src/protocol.rs b/crates/datum/src/protocol.rs index 1939836..91fc5e1 100644 --- a/crates/datum/src/protocol.rs +++ b/crates/datum/src/protocol.rs @@ -217,9 +217,7 @@ pub fn has_unrecoverable_violations(constraints: &[ProtocolConstraint]) -> bool } /// Return constraint violations grouped by severity. -pub fn classify_violations( - constraints: &[ProtocolConstraint], -) -> Vec<(LintSeverity, String)> { +pub fn classify_violations(constraints: &[ProtocolConstraint]) -> Vec<(LintSeverity, String)> { constraints .iter() .filter(|c| !c.passed) @@ -275,7 +273,10 @@ mod tests { let (_, constraints) = evaluate_protocol(&features); let violations = classify_violations(&constraints); let has_error = violations.iter().any(|(s, _)| *s == LintSeverity::Error); - assert!(has_error, "no-protocol should produce Error-level violation"); + assert!( + has_error, + "no-protocol should produce Error-level violation" + ); } #[test] @@ -362,7 +363,13 @@ mod tests { }; let (optimal, constraints) = evaluate_protocol(&features); assert!(optimal, "binary-only satisfies XOR: P=0,B=1 → O=1"); - let warnings: Vec<_> = constraints.iter().filter(|c| !c.passed && c.strength == ConstraintStrength::Medium).collect(); - assert!(!warnings.is_empty(), "should warn about missing content hash"); + let warnings: Vec<_> = constraints + .iter() + .filter(|c| !c.passed && c.strength == ConstraintStrength::Medium) + .collect(); + assert!( + !warnings.is_empty(), + "should warn about missing content hash" + ); } } diff --git a/crates/datum/src/tomllmd.rs b/crates/datum/src/tomllmd.rs index ec198e5..3d219cf 100644 --- a/crates/datum/src/tomllmd.rs +++ b/crates/datum/src/tomllmd.rs @@ -35,8 +35,8 @@ use std::collections::{HashMap, HashSet}; /// Known datum types for entanglement validation. pub const KNOWN_DATUM_TYPES: &[&str] = &[ - "mcp", "cli", "install", "config", "skill", "workflow", - "ontology", "agent", "datum", "bridge", "provider", "surface", + "mcp", "cli", "install", "config", "skill", "workflow", "ontology", "agent", "datum", "bridge", + "provider", "surface", ]; /// The top-level .tomllmd structure. @@ -216,13 +216,11 @@ pub fn render_tomllmd(tomllmd: &Tomllmd, tier: &str) -> String { /// Compile a .tomllmd string: parse, validate entanglement, render at tier. pub fn compile_tomllmd(content: &str, tier: &str) -> Result> { - let tomllmd = - parse_tomllmd(content).map_err(|e| vec![format!("parse: {e}")])?; + let tomllmd = parse_tomllmd(content).map_err(|e| vec![format!("parse: {e}")])?; // Validate entanglement refs if present if let Some(ent) = &tomllmd.entanglement { - let all_refs: Vec = - ent.values().flat_map(|v| v.iter().cloned()).collect(); + let all_refs: Vec = ent.values().flat_map(|v| v.iter().cloned()).collect(); if !all_refs.is_empty() { validate_entanglement_refs(&all_refs, KNOWN_DATUM_TYPES)?; } @@ -263,7 +261,10 @@ epigram = "One-liner." "#; let parsed = parse_tomllmd(content).unwrap(); let sec = parsed.sections.get("overview").unwrap(); - assert_eq!(sec.verbatim, "Full technical detail with code examples and edge cases."); + assert_eq!( + sec.verbatim, + "Full technical detail with code examples and edge cases." + ); assert_eq!(sec.executive, "Compressed summary of the feature."); assert_eq!(sec.epigram, "One-liner."); } diff --git a/crates/ledger-core/src/calendar.rs b/crates/ledger-core/src/calendar.rs index 48ab7b4..b03ddd7 100644 --- a/crates/ledger-core/src/calendar.rs +++ b/crates/ledger-core/src/calendar.rs @@ -7,8 +7,8 @@ use chrono::{Datelike, Days, Months, NaiveDate}; use serde::{Deserialize, Serialize}; use thiserror::Error; -use crate::legal::Jurisdiction; use crate::ledger_ops::OperationKind; +use crate::legal::Jurisdiction; // --------------------------------------------------------------------------- // Recurrence rule @@ -134,8 +134,8 @@ impl BusinessCalendar { /// Load from a TOML file. The TOML format mirrors [`ScheduledEvent`] fields. pub fn from_toml_file(path: &std::path::Path) -> Result { let raw = std::fs::read_to_string(path)?; - let parsed: TomlCalendar = toml::from_str(&raw) - .map_err(|e| CalendarError::Toml(e.to_string()))?; + let parsed: TomlCalendar = + toml::from_str(&raw).map_err(|e| CalendarError::Toml(e.to_string()))?; let events = parsed .events @@ -164,18 +164,10 @@ impl BusinessCalendar { /// constructed (e.g. day_of_month 31 in February). pub fn next_due(rule: &RecurrenceRule, after: NaiveDate) -> Option { match rule { - RecurrenceRule::Monthly { day_of_month } => { - next_monthly(after, *day_of_month) - } - RecurrenceRule::Annual { month, day } => { - next_annual(after, *month, *day) - } - RecurrenceRule::QuarterlyEstimated { day } => { - next_quarterly_estimated(after, *day) - } - RecurrenceRule::EveryNDays { n } => { - after.checked_add_days(Days::new(*n as u64)) - } + RecurrenceRule::Monthly { day_of_month } => next_monthly(after, *day_of_month), + RecurrenceRule::Annual { month, day } => next_annual(after, *month, *day), + RecurrenceRule::QuarterlyEstimated { day } => next_quarterly_estimated(after, *day), + RecurrenceRule::EveryNDays { n } => after.checked_add_days(Days::new(*n as u64)), RecurrenceRule::CronExpr(_) => None, } } @@ -290,7 +282,8 @@ impl BusinessCalendar { }, ScheduledEvent { id: "us-fatca-8938".to_string(), - description: "FATCA Form 8938 (foreign financial assets) due with return".to_string(), + description: "FATCA Form 8938 (foreign financial assets) due with return" + .to_string(), recurrence: RecurrenceRule::Annual { month: 4, day: 15 }, operation: OperationKind::CheckTaxDeadline { deadline_id: "us-fatca-8938".to_string(), @@ -346,7 +339,8 @@ impl BusinessCalendar { }, ScheduledEvent { id: "au-annual-return-tax-agent".to_string(), - description: "AU income tax return with registered tax agent due May 15".to_string(), + description: "AU income tax return with registered tax agent due May 15" + .to_string(), recurrence: RecurrenceRule::Annual { month: 5, day: 15 }, operation: OperationKind::CheckTaxDeadline { deadline_id: "au-annual-return-tax-agent".to_string(), @@ -354,7 +348,11 @@ impl BusinessCalendar { jurisdiction: Some(Jurisdiction::AU), enabled: true, last_run: None, - tags: vec!["au".to_string(), "annual-return".to_string(), "tax-agent".to_string()], + tags: vec![ + "au".to_string(), + "annual-return".to_string(), + "tax-agent".to_string(), + ], }, ScheduledEvent { id: "au-monthly-ingest".to_string(), @@ -627,7 +625,12 @@ mod tests { let items = cal.upcoming(from, 60); // Verify sort order for w in items.windows(2) { - assert!(w[0].0 <= w[1].0, "dates out of order: {:?} > {:?}", w[0].0, w[1].0); + assert!( + w[0].0 <= w[1].0, + "dates out of order: {:?} > {:?}", + w[0].0, + w[1].0 + ); } } @@ -690,7 +693,10 @@ mod tests { fn events_by_tag_returns_matching() { let cal = BusinessCalendar::us_tax_defaults(); let fbar_events = cal.events_by_tag("fbar"); - assert!(!fbar_events.is_empty(), "expected at least one fbar-tagged event"); + assert!( + !fbar_events.is_empty(), + "expected at least one fbar-tagged event" + ); for ev in &fbar_events { assert!(ev.tags.contains(&"fbar".to_string())); } diff --git a/crates/ledger-core/src/constraints.rs b/crates/ledger-core/src/constraints.rs index 7951f58..45d2446 100644 --- a/crates/ledger-core/src/constraints.rs +++ b/crates/ledger-core/src/constraints.rs @@ -37,7 +37,11 @@ impl ConstraintEvaluation { return (0.0, Disposition::Unrecoverable); } let score = self.strong_ratio * 0.60 + self.medium_ratio * 0.30 + self.weak_ratio * 0.10; - let disposition = if score >= 0.85 { Disposition::Advisory } else { Disposition::Recoverable }; + let disposition = if score >= 0.85 { + Disposition::Advisory + } else { + Disposition::Recoverable + }; (score, disposition) } @@ -55,15 +59,25 @@ impl ConstraintEvaluation { if self.strong_ratio < 1.0 { issues.push(Issue::recoverable( "constraint_strong_fail", - format!("vendor {vendor_id}: strong constraint ratio {:.0}%", self.strong_ratio * 100.0), - IssueSource::Constraint { strength: self.strong_ratio }, + format!( + "vendor {vendor_id}: strong constraint ratio {:.0}%", + self.strong_ratio * 100.0 + ), + IssueSource::Constraint { + strength: self.strong_ratio, + }, )); } if self.medium_ratio < 0.5 { issues.push(Issue::recoverable( "constraint_medium_fail", - format!("vendor {vendor_id}: medium constraint ratio {:.0}%", self.medium_ratio * 100.0), - IssueSource::Constraint { strength: self.medium_ratio }, + format!( + "vendor {vendor_id}: medium constraint ratio {:.0}%", + self.medium_ratio * 100.0 + ), + IssueSource::Constraint { + strength: self.medium_ratio, + }, )); } issues @@ -74,7 +88,9 @@ impl ConstraintEvaluation { use super::validation::MetaFlag; let (score, _) = self.to_confidence(); if score < 0.85 { - Some(MetaFlag::ConstraintWeak { constraint: constraint_name.to_string() }) + Some(MetaFlag::ConstraintWeak { + constraint: constraint_name.to_string(), + }) } else { None } @@ -102,7 +118,13 @@ pub struct VendorConstraintSet { } impl VendorConstraintSet { - pub fn evaluate(&self, amount: f64, day: u32, tax_code: &str, account: &str) -> ConstraintEvaluation { + pub fn evaluate( + &self, + amount: f64, + day: u32, + tax_code: &str, + account: &str, + ) -> ConstraintEvaluation { let in_range = amount >= self.amount_p05 && amount <= self.amount_p95; let tax_matches = tax_code == self.usual_tax_code; let account_matches = account == self.usual_account; @@ -112,10 +134,18 @@ impl VendorConstraintSet { let strong_ratio = strong_pass / strong_count; let day_matches = self.usual_day_of_month.map(|d| day == d).unwrap_or(true); let medium_count = 2.0; - let medium_pass = [day_matches, account_matches].iter().filter(|&&b| b).count() as f32; + let medium_pass = [day_matches, account_matches] + .iter() + .filter(|&&b| b) + .count() as f32; let medium_ratio = medium_pass / medium_count; let weak_ratio = 1.0; - ConstraintEvaluation { required_pass, strong_ratio, medium_ratio, weak_ratio } + ConstraintEvaluation { + required_pass, + strong_ratio, + medium_ratio, + weak_ratio, + } } } @@ -128,7 +158,9 @@ pub struct InvoiceConstraintSolver { impl InvoiceConstraintSolver { pub fn new() -> Self { - Self { constraint_count: 0 } + Self { + constraint_count: 0, + } } /// Verify invoice arithmetic and return structured audit result. @@ -141,9 +173,17 @@ impl InvoiceConstraintSolver { } else if !arithmetic_ok { format!("invoice arithmetic error: {total:.2} != {subtotal:.2} + {gst:.2}") } else { - format!("GST rate anomaly: expected {:.2}, got {gst:.2}", subtotal * 0.1) + format!( + "GST rate anomaly: expected {:.2}, got {gst:.2}", + subtotal * 0.1 + ) }; - InvoiceVerification { evaluation, arithmetic_ok, gst_rate_ok, audit_note } + InvoiceVerification { + evaluation, + arithmetic_ok, + gst_rate_ok, + audit_note, + } } pub fn validate(&self, total: f64, subtotal: f64, gst: f64) -> ConstraintEvaluation { @@ -223,7 +263,10 @@ mod tests { }; let issues = eval.to_issues("TESTVENDOR"); assert_eq!(issues.len(), 1); - assert_eq!(issues[0].disposition, super::super::validation::Disposition::Unrecoverable); + assert_eq!( + issues[0].disposition, + super::super::validation::Disposition::Unrecoverable + ); } #[test] diff --git a/crates/ledger-core/src/document_shape.rs b/crates/ledger-core/src/document_shape.rs index 0c23733..7e4d826 100644 --- a/crates/ledger-core/src/document_shape.rs +++ b/crates/ledger-core/src/document_shape.rs @@ -64,13 +64,13 @@ impl StatementVendor { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DocumentShape { pub vendor: StatementVendor, - pub account_type: String, // "checking", "savings", "brokerage", "crypto" - pub statement_format: String, // "csv_ofx", "pdf_tabular", "xlsx_native", "csv_generic" + pub account_type: String, // "checking", "savings", "brokerage", "crypto" + pub statement_format: String, // "csv_ofx", "pdf_tabular", "xlsx_native", "csv_generic" pub column_map: HashMap, // canonical → source_header - pub date_format: Option, // e.g. "%m/%d/%Y" - pub currency: String, // "USD", "AUD", "EUR" + pub date_format: Option, // e.g. "%m/%d/%Y" + pub currency: String, // "USD", "AUD", "EUR" pub confidence: f64, - pub signals: Vec, // which signals fired + pub signals: Vec, // which signals fired } impl DocumentShape { @@ -143,10 +143,7 @@ impl<'a> Classifier<'a> { let vendor_slug = parts[0].to_ascii_lowercase(); if let Some(v) = vendor_from_slug(&vendor_slug) { self.vendor = v; - self.add_signal( - format!("filename-convention vendor={vendor_slug}"), - 0.5, - ); + self.add_signal(format!("filename-convention vendor={vendor_slug}"), 0.5); // account type from second segment if present if parts.len() >= 2 { self.account_type = parts[1].to_ascii_lowercase(); @@ -163,20 +160,48 @@ impl<'a> Classifier<'a> { let fl = self.filename_lower.clone(); let checks: &[(&str, StatementVendor, &str)] = &[ - ("wellsfargo", StatementVendor::WellsFargo, "filename-keyword:wellsfargo"), + ( + "wellsfargo", + StatementVendor::WellsFargo, + "filename-keyword:wellsfargo", + ), ("wf_", StatementVendor::WellsFargo, "filename-keyword:wf_"), ("chase", StatementVendor::Chase, "filename-keyword:chase"), ("jpmc", StatementVendor::Chase, "filename-keyword:jpmc"), ("hsbc", StatementVendor::Hsbc, "filename-keyword:hsbc"), ("anz", StatementVendor::Anz, "filename-keyword:anz"), - ("commbank", StatementVendor::Commbank, "filename-keyword:commbank"), + ( + "commbank", + StatementVendor::Commbank, + "filename-keyword:commbank", + ), ("cba", StatementVendor::Commbank, "filename-keyword:cba"), - ("westpac", StatementVendor::WestpacAu, "filename-keyword:westpac"), - ("interactive", StatementVendor::Interactive, "filename-keyword:interactive"), - ("coinbase", StatementVendor::Coinbase, "filename-keyword:coinbase"), + ( + "westpac", + StatementVendor::WestpacAu, + "filename-keyword:westpac", + ), + ( + "interactive", + StatementVendor::Interactive, + "filename-keyword:interactive", + ), + ( + "coinbase", + StatementVendor::Coinbase, + "filename-keyword:coinbase", + ), ("kraken", StatementVendor::Kraken, "filename-keyword:kraken"), - ("bankofamerica", StatementVendor::BankOfAmerica, "filename-keyword:bankofamerica"), - ("boa", StatementVendor::BankOfAmerica, "filename-keyword:boa"), + ( + "bankofamerica", + StatementVendor::BankOfAmerica, + "filename-keyword:bankofamerica", + ), + ( + "boa", + StatementVendor::BankOfAmerica, + "filename-keyword:boa", + ), ]; for (needle, vendor, signal) in checks { @@ -201,8 +226,12 @@ impl<'a> Classifier<'a> { let content_lower = self.sample_content.to_ascii_lowercase(); // Chase CSV header - if self.sample_content.contains("Transaction Date,Post Date,Description,Amount") - || self.sample_content.contains("Transaction Date,Post Date,Description,Category,Type,Amount") + if self + .sample_content + .contains("Transaction Date,Post Date,Description,Amount") + || self + .sample_content + .contains("Transaction Date,Post Date,Description,Category,Type,Amount") { if self.vendor == StatementVendor::Unknown { self.vendor = StatementVendor::Chase; @@ -290,7 +319,10 @@ impl<'a> Classifier<'a> { // Step 5: CSV column map inference // ----------------------------------------------------------------------- fn infer_column_map(&mut self) { - if !matches!(self.doc_type, DocType::SpreadsheetCsv | DocType::SpreadsheetXlsx) { + if !matches!( + self.doc_type, + DocType::SpreadsheetCsv | DocType::SpreadsheetXlsx + ) { return; } @@ -310,8 +342,12 @@ impl<'a> Classifier<'a> { | "transaction_date" => Some("date"), "amount" | "debit" | "credit" | "debit amount" | "credit amount" | "transaction amount" => Some("amount"), - "description" | "memo" | "narrative" | "transaction description" - | "trans description" | "details" => Some("description"), + "description" + | "memo" + | "narrative" + | "transaction description" + | "trans description" + | "details" => Some("description"), "balance" | "running balance" | "available balance" => Some("balance"), "category" | "type" | "transaction type" => Some("category"), _ => None, @@ -324,10 +360,7 @@ impl<'a> Classifier<'a> { } if !self.column_map.is_empty() { - self.add_signal( - format!("csv-column-map:{}", self.column_map.len()), - 0.1, - ); + self.add_signal(format!("csv-column-map:{}", self.column_map.len()), 0.1); } } @@ -351,9 +384,7 @@ impl<'a> Classifier<'a> { // ----------------------------------------------------------------------- fn reconcile_au_currency(&mut self) { match &self.vendor { - StatementVendor::Anz - | StatementVendor::Commbank - | StatementVendor::WestpacAu => { + StatementVendor::Anz | StatementVendor::Commbank | StatementVendor::WestpacAu => { self.currency = "AUD".to_string(); } _ => {} @@ -392,9 +423,9 @@ impl<'a> Classifier<'a> { // --------------------------------------------------------------------------- enum DatePattern { - Iso, // 2024-01-15 - UsSlash, // 01/15/2024 - AuSlash, // 15/01/2024 (day > 12 in first position gives this away) + Iso, // 2024-01-15 + UsSlash, // 01/15/2024 + AuSlash, // 15/01/2024 (day > 12 in first position gives this away) } fn has_date_pattern(content: &str, pattern: DatePattern) -> bool { @@ -511,46 +542,40 @@ mod tests { #[test] fn wellsfargo_filename_keyword() { - let shape = classify_document_shape( - &DocType::SpreadsheetCsv, - "wellsfargo_checking_2024.csv", - "", - ); + let shape = + classify_document_shape(&DocType::SpreadsheetCsv, "wellsfargo_checking_2024.csv", ""); assert_eq!(shape.vendor, StatementVendor::WellsFargo); assert!(shape.confidence > 0.0); } #[test] fn wellsfargo_wf_prefix() { - let shape = classify_document_shape( - &DocType::SpreadsheetCsv, - "wf_savings_jan2024.csv", - "", - ); + let shape = classify_document_shape(&DocType::SpreadsheetCsv, "wf_savings_jan2024.csv", ""); assert_eq!(shape.vendor, StatementVendor::WellsFargo); } #[test] fn chase_csv_header_detection() { let header = "Transaction Date,Post Date,Description,Amount\n01/15/2024,01/16/2024,AMAZON.COM,-42.99\n"; - let shape = classify_document_shape( - &DocType::SpreadsheetCsv, - "statement.csv", - header, - ); + let shape = classify_document_shape(&DocType::SpreadsheetCsv, "statement.csv", header); assert_eq!(shape.vendor, StatementVendor::Chase); // Column map should have date and amount - assert!(shape.column_map.contains_key("date"), "expected 'date' key in column_map, got {:?}", shape.column_map); - assert!(shape.column_map.contains_key("amount"), "expected 'amount' key, got {:?}", shape.column_map); + assert!( + shape.column_map.contains_key("date"), + "expected 'date' key in column_map, got {:?}", + shape.column_map + ); + assert!( + shape.column_map.contains_key("amount"), + "expected 'amount' key, got {:?}", + shape.column_map + ); } #[test] fn au_filename_vendor_and_currency() { - let shape = classify_document_shape( - &DocType::Pdf, - "anz--checking--2024-03--statement.pdf", - "", - ); + let shape = + classify_document_shape(&DocType::Pdf, "anz--checking--2024-03--statement.pdf", ""); assert_eq!(shape.vendor, StatementVendor::Anz); assert_eq!(shape.currency, "AUD"); } @@ -579,11 +604,8 @@ mod tests { #[test] fn vendor_account_filename_convention_parse() { - let shape = classify_document_shape( - &DocType::Pdf, - "chase--checking--2024-01--statement.pdf", - "", - ); + let shape = + classify_document_shape(&DocType::Pdf, "chase--checking--2024-01--statement.pdf", ""); assert_eq!(shape.vendor, StatementVendor::Chase); assert_eq!(shape.account_type, "checking"); } @@ -631,10 +653,7 @@ mod tests { #[test] fn vendor_slug_roundtrip() { - assert_eq!( - StatementVendor::WellsFargo.slug(), - "wellsfargo" - ); + assert_eq!(StatementVendor::WellsFargo.slug(), "wellsfargo"); assert_eq!(StatementVendor::Anz.slug(), "anz"); assert_eq!(StatementVendor::Unknown.slug(), "unknown"); } diff --git a/crates/ledger-core/src/graph.rs b/crates/ledger-core/src/graph.rs index 0f4e6a5..7f81a48 100644 --- a/crates/ledger-core/src/graph.rs +++ b/crates/ledger-core/src/graph.rs @@ -49,10 +49,5 @@ pub fn create_pipeline_nodes() -> Vec { } pub fn create_pipeline_edges() -> Vec<(usize, usize)> { - vec![ - (0, 1), - (1, 2), - (2, 3), - (3, 4), - ] -} \ No newline at end of file + vec![(0, 1), (1, 2), (2, 3), (3, 4)] +} diff --git a/crates/ledger-core/src/integration_tests.rs b/crates/ledger-core/src/integration_tests.rs index 9c1496c..47b62ea 100644 --- a/crates/ledger-core/src/integration_tests.rs +++ b/crates/ledger-core/src/integration_tests.rs @@ -35,10 +35,7 @@ mod integration { let cal = BusinessCalendar::us_tax_defaults(); let dispatcher = OperationDispatcher::from_scheduled_events(&cal.events); - let ctx = OperationContext::new( - PathBuf::from("/tmp/working"), - PathBuf::from("/tmp/rules"), - ); + let ctx = OperationContext::new(PathBuf::from("/tmp/working"), PathBuf::from("/tmp/rules")); let result = dispatcher.run_by_id("us-quarterly-estimated", &ctx); assert!( @@ -85,7 +82,9 @@ mod integration { // // The fixture at tests/fixtures/sample_hsbc_statement.pdf should contain // exactly one transaction line for deterministic test assertions. - use crate::ledger_ops::{IngestStatementOp, LedgerOperation, LedgerOpError, OperationContext}; + use crate::ledger_ops::{ + IngestStatementOp, LedgerOpError, LedgerOperation, OperationContext, + }; let op = IngestStatementOp { source_glob: "tests/fixtures/*.pdf".to_string(), @@ -94,8 +93,10 @@ mod integration { // Point working_dir at the repo root so the glob resolves correctly. let repo_root = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .parent().unwrap() // crates/ledger-core → crates - .parent().unwrap() // crates → repo root + .parent() + .unwrap() // crates/ledger-core → crates + .parent() + .unwrap() // crates → repo root .to_path_buf(); let ctx = OperationContext::new(repo_root, PathBuf::from("/tmp/rules")); @@ -158,7 +159,9 @@ mod integration { // This test uses ClassifyTransactionsOp output as input to OpaGateOp, // demonstrating the pipeline composition: // IngestStatementOp → ClassifyTransactionsOp → OpaGateOp → ExportWorkbookOp - use crate::ledger_ops::{ClassifyTransactionsOp, LedgerOperation, LedgerOpError, OperationContext}; + use crate::ledger_ops::{ + ClassifyTransactionsOp, LedgerOpError, LedgerOperation, OperationContext, + }; // ClassifyTransactionsOp is the nearest existing op; OpaGateOp doesn't exist yet. // This stub exercises the existing op to prove pipeline composition compiles. @@ -168,10 +171,7 @@ mod integration { account_filter: None, }; - let ctx = OperationContext::new( - PathBuf::from("/tmp/working"), - PathBuf::from("/tmp/rules"), - ); + let ctx = OperationContext::new(PathBuf::from("/tmp/working"), PathBuf::from("/tmp/rules")); let classify_result = classify_op.execute(&ctx); @@ -205,24 +205,23 @@ mod integration { // Replace proposer/reviewer with AnthropicModelClient for live LLM coverage. #[test] fn test_llm_verification_proposes_category() { - use crate::verify::{MockModelClient, MultiModelConfig, MultiModelVerifier, VerificationOutcome}; + use crate::verify::{ + MockModelClient, MultiModelConfig, MultiModelVerifier, VerificationOutcome, + }; let proposer_json = r#"{ "rule_id": "ForeignIncome", "proposed_fix": "ForeignIncome", "reasoning": "Wire transfer from foreign employer matches ForeignIncome pattern", "confidence": 0.92 }"#; - let reviewer_json = - r#"{"approved":true,"concerns":[],"suggestions":[],"confidence":0.90}"#; + let reviewer_json = r#"{"approved":true,"concerns":[],"suggestions":[],"confidence":0.90}"#; let proposer = MockModelClient::default().with_response(proposer_json); let reviewer = MockModelClient::default().with_response(reviewer_json); - let config = MultiModelConfig::new( - "claude-haiku-4-5-20251001", - "claude-haiku-4-5-20251001", - ) - .with_threshold(0.80); + let config = + MultiModelConfig::new("claude-haiku-4-5-20251001", "claude-haiku-4-5-20251001") + .with_threshold(0.80); let verifier = MultiModelVerifier::new(proposer, reviewer, config); diff --git a/crates/ledger-core/src/iso.rs b/crates/ledger-core/src/iso.rs index 3451288..77cc3cb 100644 --- a/crates/ledger-core/src/iso.rs +++ b/crates/ledger-core/src/iso.rs @@ -270,9 +270,8 @@ impl From for RhaiDslOwned { } const RHAI_KEYWORDS: &[&str] = &[ - "let", "const", "if", "else", "match", "fn", "for", "while", "loop", - "return", "break", "continue", "true", "false", "import", "export", - "throw", "try", "catch", "in", "is", + "let", "const", "if", "else", "match", "fn", "for", "while", "loop", "return", "break", + "continue", "true", "false", "import", "export", "throw", "try", "catch", "in", "is", ]; /// Extract identifiers from Rhai source with line/col spans and kind classification. @@ -296,18 +295,27 @@ fn extract_dsl_symbols(source: &str) -> Vec { } // Skip line comments if c == '/' && i + 1 < chars.len() && chars[i + 1] == '/' { - while i < chars.len() && chars[i] != '\n' { i += 1; } + while i < chars.len() && chars[i] != '\n' { + i += 1; + } continue; } // Skip string literals (avoid picking up identifiers inside strings) if c == '"' || c == '\'' { let quote = c; - i += 1; col += 1; + i += 1; + col += 1; while i < chars.len() && chars[i] != quote { - if chars[i] == '\n' { line += 1; col = 1; } else { col += 1; } + if chars[i] == '\n' { + line += 1; + col = 1; + } else { + col += 1; + } i += 1; } - i += 1; col += 1; + i += 1; + col += 1; continue; } if c.is_alphabetic() || c == '_' { @@ -320,7 +328,9 @@ fn extract_dsl_symbols(source: &str) -> Vec { let name: String = chars[start..i].iter().collect(); // Peek past whitespace to detect function call let mut j = i; - while j < chars.len() && chars[j] == ' ' { j += 1; } + while j < chars.len() && chars[j] == ' ' { + j += 1; + } let kind = if RHAI_KEYWORDS.contains(&name.as_str()) { DslSymbolKind::Keyword } else if j < chars.len() && chars[j] == '(' { @@ -328,7 +338,11 @@ fn extract_dsl_symbols(source: &str) -> Vec { } else { DslSymbolKind::Variable }; - symbols.push(DslSymbol { kind, name, span: Some(span) }); + symbols.push(DslSymbol { + kind, + name, + span: Some(span), + }); continue; } col += 1; @@ -466,10 +480,7 @@ impl IsoAnimationPath { /// cumulative `run_time` derived from `duration_ms`. pub fn to_manim_script(&self, label: &str) -> String { let mut out = String::new(); - out.push_str(&format!( - "# Manim animation stub for '{}'\n", - label - )); + out.push_str(&format!("# Manim animation stub for '{}'\n", label)); out.push_str("from manim import *\n\n"); out.push_str(&format!( "class {}Scene(Scene):\n def construct(self):\n", @@ -721,7 +732,10 @@ mod tests { #[test] fn smil_svg_empty_transforms_returns_empty_string() { - let path = IsoAnimationPath { label: "empty".into(), transforms: vec![] }; + let path = IsoAnimationPath { + label: "empty".into(), + transforms: vec![], + }; assert!(path.to_smil_svg(1.0, 0.0, 0.0).is_empty()); } @@ -742,14 +756,21 @@ mod tests { #[test] fn xml_attr_escape_handles_special_chars() { - assert_eq!(xml_attr_escape(r#"a & "b" "#), "a & "b" <c>"); + assert_eq!( + xml_attr_escape(r#"a & "b" "#), + "a & "b" <c>" + ); } #[test] fn zlayer_display_nonempty() { let all = [ - ZLayer::Document, ZLayer::Pipeline, ZLayer::Constraint, - ZLayer::Legal, ZLayer::FormalProof, ZLayer::Attestation, + ZLayer::Document, + ZLayer::Pipeline, + ZLayer::Constraint, + ZLayer::Legal, + ZLayer::FormalProof, + ZLayer::Attestation, ]; for layer in all { assert!(!layer.to_string().is_empty()); @@ -759,13 +780,21 @@ mod tests { #[test] fn semantic_type_display_nonempty() { let all = [ - SemanticType::Document, SemanticType::Pipeline, SemanticType::Constraint, - SemanticType::Gate, SemanticType::Legal, SemanticType::Solver, - SemanticType::Result, SemanticType::Flag, SemanticType::Issue, - SemanticType::Proof, SemanticType::Attestation, SemanticType::Unknown, + SemanticType::Document, + SemanticType::Pipeline, + SemanticType::Constraint, + SemanticType::Gate, + SemanticType::Legal, + SemanticType::Solver, + SemanticType::Result, + SemanticType::Flag, + SemanticType::Issue, + SemanticType::Proof, + SemanticType::Attestation, + SemanticType::Unknown, ]; for st in all { assert!(!st.to_string().is_empty()); } } -} \ No newline at end of file +} diff --git a/crates/ledger-core/src/iso_objects.rs b/crates/ledger-core/src/iso_objects.rs index 3f5b76e..9453223 100644 --- a/crates/ledger-core/src/iso_objects.rs +++ b/crates/ledger-core/src/iso_objects.rs @@ -3,7 +3,9 @@ use crate::iso::{HasVisualization, RhaiDsl, SemanticType, VisualizationSpec, ZLayer}; -use crate::constraints::{ConstraintEvaluation, InvoiceConstraintSolver, InvoiceVerification, VendorConstraintSet}; +use crate::constraints::{ + ConstraintEvaluation, InvoiceConstraintSolver, InvoiceVerification, VendorConstraintSet, +}; use crate::legal::{Jurisdiction, LegalRule, LegalSolver, TransactionFacts, Z3Result}; use crate::pipeline::{ Classified, Committed, Ingested, KasuariSolver, NeedsReview, PipelineState, Reconciled, @@ -20,8 +22,10 @@ impl HasVisualization for PipelineState { VisualizationSpec { semantic_type: SemanticType::Pipeline, z_layer: ZLayer::Pipeline, - rhai_dsl: RhaiDsl::new(r#"let tx = ingest(pdf_path); -check_constraints(tx, constraint_set);"#), + rhai_dsl: RhaiDsl::new( + r#"let tx = ingest(pdf_path); +check_constraints(tx, constraint_set);"#, + ), description: "Raw ingested transaction — structure validated, awaiting constraint pass", } } @@ -58,10 +62,13 @@ impl HasVisualization for PipelineState { VisualizationSpec { semantic_type: SemanticType::Pipeline, z_layer: ZLayer::Pipeline, - rhai_dsl: RhaiDsl::new(r#"let reconciled = match_workbook(classified_tx, workbook); + rhai_dsl: RhaiDsl::new( + r#"let reconciled = match_workbook(classified_tx, workbook); if reconciled.matched { open_commit_gate(reconciled) } -else { flag("unmatched_entry") }"#), - description: "Transaction matched against workbook entries — commit gate evaluation pending", +else { flag("unmatched_entry") }"#, + ), + description: + "Transaction matched against workbook entries — commit gate evaluation pending", } } } @@ -71,9 +78,11 @@ impl HasVisualization for PipelineState { VisualizationSpec { semantic_type: SemanticType::Pipeline, z_layer: ZLayer::Pipeline, - rhai_dsl: RhaiDsl::new(r#"let committed = commit_gate.approve(reconciled_tx); + rhai_dsl: RhaiDsl::new( + r#"let committed = commit_gate.approve(reconciled_tx); write_xlsx(committed); -emit_audit_trail(committed.id);"#), +emit_audit_trail(committed.id);"#, + ), description: "Committed to workbook — final immutable state, audit trail emitted", } } @@ -84,10 +93,13 @@ impl HasVisualization for PipelineState { VisualizationSpec { semantic_type: SemanticType::Pipeline, z_layer: ZLayer::Pipeline, - rhai_dsl: RhaiDsl::new(r#"let review = legal_fail(tx, z3_result); + rhai_dsl: RhaiDsl::new( + r#"let review = legal_fail(tx, z3_result); flag_operator("legal_violation", review.rule_id); -route_to_review_queue(review);"#), - description: "Legal verification failed — operator flag set, transaction held for manual review", +route_to_review_queue(review);"#, + ), + description: + "Legal verification failed — operator flag set, transaction held for manual review", } } } @@ -101,8 +113,10 @@ impl HasVisualization for ConstraintEvaluation { VisualizationSpec { semantic_type: SemanticType::Result, z_layer: ZLayer::Constraint, - rhai_dsl: RhaiDsl::new(r#"let eval = constraint_set.evaluate(amount, day, code, acct); -if eval.required_pass { classify_ok() } else { flag("constraint_fail") }"#), + rhai_dsl: RhaiDsl::new( + r#"let eval = constraint_set.evaluate(amount, day, code, acct); +if eval.required_pass { classify_ok() } else { flag("constraint_fail") }"#, + ), description: "Numerical constraint evaluation result with pass/fail per-field scores", } } @@ -139,10 +153,13 @@ impl HasVisualization for InvoiceVerification { VisualizationSpec { semantic_type: SemanticType::Result, z_layer: ZLayer::Constraint, - rhai_dsl: RhaiDsl::new(r#"let v = invoice_solver.verify(gross, gst); + rhai_dsl: RhaiDsl::new( + r#"let v = invoice_solver.verify(gross, gst); if !v.arithmetic_ok { flag("arithmetic_mismatch", v.audit_note) } -if !v.gst_rate_ok { flag("gst_rate_mismatch", v.audit_note) }"#), - description: "Invoice verification result — arithmetic_ok and gst_rate_ok flags with audit note", +if !v.gst_rate_ok { flag("gst_rate_mismatch", v.audit_note) }"#, + ), + description: + "Invoice verification result — arithmetic_ok and gst_rate_ok flags with audit note", } } } @@ -172,11 +189,14 @@ impl HasVisualization for LegalRule { VisualizationSpec { semantic_type: SemanticType::Legal, z_layer: ZLayer::Legal, - rhai_dsl: RhaiDsl::new(r#"let rule = LegalRule::new(jurisdiction, "au-gst-38-190") + rhai_dsl: RhaiDsl::new( + r#"let rule = LegalRule::new(jurisdiction, "au-gst-38-190") .with_formula("supply_type == 'GST_FREE' && vendor_jurisdiction == 'AU'") .with_category("GST"); -legal_solver.verify(rule, facts);"#), - description: "Single jurisdiction-bound legal rule: threshold, exclusion, or benefit predicate", +legal_solver.verify(rule, facts);"#, + ), + description: + "Single jurisdiction-bound legal rule: threshold, exclusion, or benefit predicate", } } } diff --git a/crates/ledger-core/src/layout.rs b/crates/ledger-core/src/layout.rs index 2f1cb22..e8b74ac 100644 --- a/crates/ledger-core/src/layout.rs +++ b/crates/ledger-core/src/layout.rs @@ -1,5 +1,5 @@ use crate::graph::{create_pipeline_edges, create_pipeline_nodes}; -use glam::{Vec3, Vec2}; +use glam::{Vec2, Vec3}; use std::collections::HashMap; pub fn iso_project(p: Vec3, scale: f32, origin: Vec2) -> Vec2 { @@ -32,10 +32,7 @@ impl ForceLayout { let velocities = HashMap::new(); for (i, _node) in nodes.iter().enumerate() { let angle = (i as f32) * std::f32::consts::TAU / nodes.len() as f32; - positions.insert( - i, - Vec3::new(angle.cos() * 100.0, 0.0, angle.sin() * 100.0), - ); + positions.insert(i, Vec3::new(angle.cos() * 100.0, 0.0, angle.sin() * 100.0)); } Self { positions, @@ -71,4 +68,4 @@ impl Default for ForceLayout { fn default() -> Self { Self::new() } -} \ No newline at end of file +} diff --git a/crates/ledger-core/src/ledger_ops.rs b/crates/ledger-core/src/ledger_ops.rs index 43ef421..ce1e210 100644 --- a/crates/ledger-core/src/ledger_ops.rs +++ b/crates/ledger-core/src/ledger_ops.rs @@ -186,15 +186,14 @@ impl LedgerOperation for IngestStatementOp { } fn execute(&self, ctx: &OperationContext) -> Result { - use calamine::{open_workbook_auto, Reader}; use crate::document::DocType; use crate::document_shape::classify_document_shape; use crate::ingest::{IngestedLedger, TransactionInput}; + use calamine::{open_workbook_auto, Reader}; - let input_path = ctx - .input_path - .as_ref() - .ok_or_else(|| LedgerOpError::InvalidInput("input_path not set in context".to_string()))?; + let input_path = ctx.input_path.as_ref().ok_or_else(|| { + LedgerOpError::InvalidInput("input_path not set in context".to_string()) + })?; let filename = input_path .file_name() @@ -249,7 +248,11 @@ impl LedgerOperation for IngestStatementOp { .enumerate() .filter_map(|(i, cell)| { let s = cell.to_string().trim().to_ascii_lowercase(); - if s.is_empty() { None } else { Some((s, i)) } + if s.is_empty() { + None + } else { + Some((s, i)) + } }) .collect(); @@ -444,17 +447,25 @@ impl LedgerOperation for ExportWorkbookOp { } // Write each sheet group - let write_sheet = |wb: &mut Workbook, sheet_name: &str, rows: &[TxProjectionRow]| -> Result<(), LedgerOpError> { + let write_sheet = |wb: &mut Workbook, + sheet_name: &str, + rows: &[TxProjectionRow]| + -> Result<(), LedgerOpError> { let ws = wb .worksheet_from_name(sheet_name) .map_err(|e| LedgerOpError::Workbook(e.to_string()))?; - ws.write_string(0, 0, "tx_id").map_err(|e| LedgerOpError::Workbook(e.to_string()))?; - ws.write_string(0, 1, "category").map_err(|e| LedgerOpError::Workbook(e.to_string()))?; - ws.write_string(0, 2, "reason").map_err(|e| LedgerOpError::Workbook(e.to_string()))?; + ws.write_string(0, 0, "tx_id") + .map_err(|e| LedgerOpError::Workbook(e.to_string()))?; + ws.write_string(0, 1, "category") + .map_err(|e| LedgerOpError::Workbook(e.to_string()))?; + ws.write_string(0, 2, "reason") + .map_err(|e| LedgerOpError::Workbook(e.to_string()))?; for (idx, row) in rows.iter().enumerate() { let r = (idx + 1) as u32; - ws.write_string(r, 0, &row.tx_id).map_err(|e| LedgerOpError::Workbook(e.to_string()))?; - ws.write_string(r, 2, &row.source_ref).map_err(|e| LedgerOpError::Workbook(e.to_string()))?; + ws.write_string(r, 0, &row.tx_id) + .map_err(|e| LedgerOpError::Workbook(e.to_string()))?; + ws.write_string(r, 2, &row.source_ref) + .map_err(|e| LedgerOpError::Workbook(e.to_string()))?; } Ok(()) }; @@ -570,7 +581,8 @@ impl LedgerOperation for PdfIngestOp { // 6. This op should be idempotent: the Blake3 content hash prevents duplicate rows // even if the PDF is re-ingested Err(LedgerOpError::NotImplemented( - "PdfIngestOp: PDF ingestion via reqif-opa-mcp not yet implemented (phase-2)".to_string(), + "PdfIngestOp: PDF ingestion via reqif-opa-mcp not yet implemented (phase-2)" + .to_string(), )) } } @@ -640,18 +652,14 @@ impl OperationDispatcher { for event in events { let op: Box = match &event.operation { - OperationKind::CheckTaxDeadline { deadline_id } => { - Box::new(CheckTaxDeadlineOp { - deadline_id: deadline_id.clone(), - warn_days_before: 30, - }) - } - OperationKind::IngestStatement { source_glob } => { - Box::new(IngestStatementOp { - source_glob: source_glob.clone(), - vendor_hint: None, - }) - } + OperationKind::CheckTaxDeadline { deadline_id } => Box::new(CheckTaxDeadlineOp { + deadline_id: deadline_id.clone(), + warn_days_before: 30, + }), + OperationKind::IngestStatement { source_glob } => Box::new(IngestStatementOp { + source_glob: source_glob.clone(), + vendor_hint: None, + }), OperationKind::ClassifyTransactions { rule_dir } => { Box::new(ClassifyTransactionsOp { rule_dir: PathBuf::from(rule_dir), @@ -659,24 +667,18 @@ impl OperationDispatcher { account_filter: None, }) } - OperationKind::ReconcileAccount { account_id } => { - Box::new(ReconcileAccountOp { - account_id: account_id.clone(), - dry_run: false, - }) - } - OperationKind::ExportWorkbook { output_path } => { - Box::new(ExportWorkbookOp { - output_path: PathBuf::from(output_path), - include_flags: true, - }) - } - OperationKind::GenerateAuditTrail { year } => { - Box::new(GenerateAuditTrailOp { - output_path: PathBuf::from(format!("audit-trail-{}.xlsx", year)), - year: *year, - }) - } + OperationKind::ReconcileAccount { account_id } => Box::new(ReconcileAccountOp { + account_id: account_id.clone(), + dry_run: false, + }), + OperationKind::ExportWorkbook { output_path } => Box::new(ExportWorkbookOp { + output_path: PathBuf::from(output_path), + include_flags: true, + }), + OperationKind::GenerateAuditTrail { year } => Box::new(GenerateAuditTrailOp { + output_path: PathBuf::from(format!("audit-trail-{}.xlsx", year)), + year: *year, + }), }; dispatcher.ops.push(op); @@ -686,10 +688,7 @@ impl OperationDispatcher { } /// Run every registered operation and collect results. - pub fn run_all( - &self, - ctx: &OperationContext, - ) -> Vec> { + pub fn run_all(&self, ctx: &OperationContext) -> Vec> { self.ops.iter().map(|op| op.execute(ctx)).collect() } @@ -699,7 +698,10 @@ impl OperationDispatcher { id: &str, ctx: &OperationContext, ) -> Option> { - self.ops.iter().find(|op| op.id() == id).map(|op| op.execute(ctx)) + self.ops + .iter() + .find(|op| op.id() == id) + .map(|op| op.execute(ctx)) } } @@ -735,10 +737,7 @@ mod tests { #[test] fn operation_context_new() { - let ctx = OperationContext::new( - PathBuf::from("/work"), - PathBuf::from("/rules"), - ); + let ctx = OperationContext::new(PathBuf::from("/work"), PathBuf::from("/rules")); assert_eq!(ctx.working_dir, PathBuf::from("/work")); assert_eq!(ctx.rules_dir, PathBuf::from("/rules")); assert!(!ctx.dry_run); @@ -747,8 +746,7 @@ mod tests { #[test] fn operation_context_builder_dry_run() { - let ctx = OperationContext::new(PathBuf::from("/w"), PathBuf::from("/r")) - .dry_run(); + let ctx = OperationContext::new(PathBuf::from("/w"), PathBuf::from("/r")).dry_run(); assert!(ctx.dry_run); } diff --git a/crates/ledger-core/src/legal.rs b/crates/ledger-core/src/legal.rs index b4d0fef..3c12639 100644 --- a/crates/ledger-core/src/legal.rs +++ b/crates/ledger-core/src/legal.rs @@ -264,7 +264,9 @@ impl LegalSolver { let violation_bool = Bool::from_bool(&ctx, violation); solver.assert(&violation_bool); match solver.check() { - SatResult::Sat => Z3Result::Violated { witness: witness.to_string() }, + SatResult::Sat => Z3Result::Violated { + witness: witness.to_string(), + }, SatResult::Unsat => Z3Result::Satisfied, SatResult::Unknown => Z3Result::Unknown, } @@ -273,7 +275,9 @@ impl LegalSolver { #[cfg(not(feature = "legal-z3"))] fn violation_result(&self, violation: bool, witness: &str) -> Z3Result { if violation { - Z3Result::Violated { witness: witness.to_string() } + Z3Result::Violated { + witness: witness.to_string(), + } } else { Z3Result::Satisfied } @@ -293,7 +297,9 @@ pub mod au_gst { pub fn rule_40_5() -> LegalRule { LegalRule::new("au-gst-40-5", Jurisdiction::AU) - .with_description("Financial supplies are input-taxed; no GST credits on related expenses") + .with_description( + "Financial supplies are input-taxed; no GST credits on related expenses", + ) .with_category("GST") .with_formula("supply_type == financial -> input_taxed AND no_gst_credit") } @@ -395,10 +401,15 @@ mod tests { #[test] fn test_from_z3result_violated() { - let issues: Vec = - Z3Result::Violated { witness: "test violation".to_string() }.into(); + let issues: Vec = Z3Result::Violated { + witness: "test violation".to_string(), + } + .into(); assert_eq!(issues.len(), 1); - assert_eq!(issues[0].disposition, crate::validation::Disposition::Unrecoverable); + assert_eq!( + issues[0].disposition, + crate::validation::Disposition::Unrecoverable + ); } #[test] diff --git a/crates/ledger-core/src/lib.rs b/crates/ledger-core/src/lib.rs index 6b682a4..b6d42eb 100644 --- a/crates/ledger-core/src/lib.rs +++ b/crates/ledger-core/src/lib.rs @@ -7,9 +7,9 @@ pub mod filename; pub mod fs_meta; pub mod graph; pub mod ingest; -pub mod journal; pub mod iso; pub mod iso_objects; +pub mod journal; pub mod layout; pub mod ledger_ops; pub mod legal; diff --git a/crates/ledger-core/src/observability.rs b/crates/ledger-core/src/observability.rs index f8db0b2..ee87e91 100644 --- a/crates/ledger-core/src/observability.rs +++ b/crates/ledger-core/src/observability.rs @@ -189,7 +189,9 @@ impl TryFrom<&otlp_json::LogRecord> for OTelLogRecord { type Error = ObservabilityError; fn try_from(record: &otlp_json::LogRecord) -> Result { - let time_unix_nano = record.time_unix_nano.parse() + let time_unix_nano = record + .time_unix_nano + .parse() .map_err(|e| ObservabilityError::InvalidRule(format!("parse time_unix_nano: {e}")))?; let severity = OTelSeverityNumber::try_from(record.severity_number)?; let body = record.body.string_value.clone().unwrap_or_default(); @@ -336,19 +338,17 @@ impl LogShapeClassifier { /// Built-in rules that ship with l3dg3rr for common operational signals. pub fn with_builtin_rules() -> Result { - Self::new(vec![ - LogShapeRule { - rule_id: "gpu-driver-device-disappeared".to_string(), - abstract_regex_type: "hardware.gpu.driver.device_handle_unknown".to_string(), - pattern: "Unable to determine the device handle for GPU[0-9]+.*Unknown Error" - .to_string(), - metric_name: "l3dg3rr.hardware.gpu.driver_faults".to_string(), - metric_delta: 1, - min_severity: OTelSeverityNumber::Error, - rationale: "GPU was expected but nvidia-smi returned an unknown device-handle error" - .to_string(), - }, - ]) + Self::new(vec![LogShapeRule { + rule_id: "gpu-driver-device-disappeared".to_string(), + abstract_regex_type: "hardware.gpu.driver.device_handle_unknown".to_string(), + pattern: "Unable to determine the device handle for GPU[0-9]+.*Unknown Error" + .to_string(), + metric_name: "l3dg3rr.hardware.gpu.driver_faults".to_string(), + metric_delta: 1, + min_severity: OTelSeverityNumber::Error, + rationale: "GPU was expected but nvidia-smi returned an unknown device-handle error" + .to_string(), + }]) } pub fn classify_log(&self, log: &OTelLogRecord) -> Vec { @@ -657,14 +657,38 @@ mod tests { #[test] fn otlp_severity_try_from_u8_maps_ranges_correctly() { - assert_eq!(OTelSeverityNumber::try_from(1).unwrap(), OTelSeverityNumber::Trace); - assert_eq!(OTelSeverityNumber::try_from(4).unwrap(), OTelSeverityNumber::Trace); - assert_eq!(OTelSeverityNumber::try_from(5).unwrap(), OTelSeverityNumber::Debug); - assert_eq!(OTelSeverityNumber::try_from(9).unwrap(), OTelSeverityNumber::Info); - assert_eq!(OTelSeverityNumber::try_from(13).unwrap(), OTelSeverityNumber::Warn); - assert_eq!(OTelSeverityNumber::try_from(17).unwrap(), OTelSeverityNumber::Error); - assert_eq!(OTelSeverityNumber::try_from(21).unwrap(), OTelSeverityNumber::Fatal); - assert_eq!(OTelSeverityNumber::try_from(24).unwrap(), OTelSeverityNumber::Fatal); + assert_eq!( + OTelSeverityNumber::try_from(1).unwrap(), + OTelSeverityNumber::Trace + ); + assert_eq!( + OTelSeverityNumber::try_from(4).unwrap(), + OTelSeverityNumber::Trace + ); + assert_eq!( + OTelSeverityNumber::try_from(5).unwrap(), + OTelSeverityNumber::Debug + ); + assert_eq!( + OTelSeverityNumber::try_from(9).unwrap(), + OTelSeverityNumber::Info + ); + assert_eq!( + OTelSeverityNumber::try_from(13).unwrap(), + OTelSeverityNumber::Warn + ); + assert_eq!( + OTelSeverityNumber::try_from(17).unwrap(), + OTelSeverityNumber::Error + ); + assert_eq!( + OTelSeverityNumber::try_from(21).unwrap(), + OTelSeverityNumber::Fatal + ); + assert_eq!( + OTelSeverityNumber::try_from(24).unwrap(), + OTelSeverityNumber::Fatal + ); } #[test] diff --git a/crates/ledger-core/src/ontology.rs b/crates/ledger-core/src/ontology.rs index f521296..b974612 100644 --- a/crates/ledger-core/src/ontology.rs +++ b/crates/ledger-core/src/ontology.rs @@ -450,7 +450,9 @@ mod arc_kit_bridge { } #[cfg(feature = "arc-kit-au")] -pub use arc_kit_au::{EdgeType, EvidenceGraph, EvidenceNode, EvidenceStore, NodeId, ProvenanceBadge}; +pub use arc_kit_au::{ + EdgeType, EvidenceGraph, EvidenceNode, EvidenceStore, NodeId, ProvenanceBadge, +}; #[cfg(feature = "arc-kit-au")] pub use arc_kit_au::{ EvidenceBuilder, EvidenceChain, EvidenceTracer, ProvenanceGap, ProvenanceScanner, diff --git a/crates/ledger-core/src/pipeline.rs b/crates/ledger-core/src/pipeline.rs index 99f75a1..3efe8a6 100644 --- a/crates/ledger-core/src/pipeline.rs +++ b/crates/ledger-core/src/pipeline.rs @@ -194,11 +194,15 @@ pub fn evaluate_commit_gate( .collect(); if !unrecoverable.is_empty() { - return CommitGate::Blocked { issues: unrecoverable }; + return CommitGate::Blocked { + issues: unrecoverable, + }; } if state.confidence >= threshold { - CommitGate::Approved { confidence: state.confidence } + CommitGate::Approved { + confidence: state.confidence, + } } else { CommitGate::PendingOperator { confidence: state.confidence, @@ -216,15 +220,30 @@ pub fn evaluate_commit_gate( #[derive(Debug, Clone)] pub enum PipelineEvent { - DocumentIngested { document_id: String, source_ref: String }, + DocumentIngested { + document_id: String, + source_ref: String, + }, ValidationPassed, - ValidationFailed { reason: String }, - Classified { category: String }, - LowConfidence { score: f32 }, - Reconciled { xero_id: Option }, - XeroPushFailed { error: String }, + ValidationFailed { + reason: String, + }, + Classified { + category: String, + }, + LowConfidence { + score: f32, + }, + Reconciled { + xero_id: Option, + }, + XeroPushFailed { + error: String, + }, CommitApproved, - CommitRejected { reason: String }, + CommitRejected { + reason: String, + }, } #[derive(Default)] @@ -252,7 +271,11 @@ impl Default for LedgerPipeline { impl LedgerPipeline { pub fn new(jurisdiction: crate::legal::Jurisdiction) -> Self { - Self { jurisdiction, repair_attempts: 0, xero_retries: 0 } + Self { + jurisdiction, + repair_attempts: 0, + xero_retries: 0, + } } } @@ -332,7 +355,9 @@ pub mod verbs { type Input = Vec; type Output = String; - fn name(&self) -> &'static str { "detect" } + fn name(&self) -> &'static str { + "detect" + } fn reversibility(&self) -> crate::validation::Reversibility { crate::validation::Reversibility::Free } @@ -360,7 +385,9 @@ pub mod verbs { type Input = (String, f64); type Output = bool; - fn name(&self) -> &'static str { "validate" } + fn name(&self) -> &'static str { + "validate" + } fn reversibility(&self) -> crate::validation::Reversibility { crate::validation::Reversibility::Free } @@ -540,17 +567,25 @@ mod tests { &mut ctx, ); assert_eq!(next, Some(State::Validating)); - let next = handle_event(State::Validating, &PipelineEvent::ValidationPassed, &mut ctx); + let next = handle_event( + State::Validating, + &PipelineEvent::ValidationPassed, + &mut ctx, + ); assert_eq!(next, Some(State::Classifying)); let next = handle_event( State::Classifying, - &PipelineEvent::Classified { category: "6-8800".to_string() }, + &PipelineEvent::Classified { + category: "6-8800".to_string(), + }, &mut ctx, ); assert_eq!(next, Some(State::Reconciling)); let next = handle_event( State::Reconciling, - &PipelineEvent::Reconciled { xero_id: Some("XERO-123".to_string()) }, + &PipelineEvent::Reconciled { + xero_id: Some("XERO-123".to_string()), + }, &mut ctx, ); assert_eq!(next, Some(State::Committed)); @@ -562,14 +597,18 @@ mod tests { ctx.repair_attempts = 0; let next = handle_event( State::Validating, - &PipelineEvent::ValidationFailed { reason: "test".to_string() }, + &PipelineEvent::ValidationFailed { + reason: "test".to_string(), + }, &mut ctx, ); assert_eq!(next, Some(State::Validating)); assert_eq!(ctx.repair_attempts, 1); let next = handle_event( State::Validating, - &PipelineEvent::ValidationFailed { reason: "test".to_string() }, + &PipelineEvent::ValidationFailed { + reason: "test".to_string(), + }, &mut ctx, ); assert_eq!(next, Some(State::NeedsReview)); @@ -596,8 +635,7 @@ mod tests { use crate::legal::{au_gst, LegalSolver}; let solver = LegalSolver::new(); let rules = vec![au_gst::rule_38_190()]; - let state = - PipelineState::::new("doc1", "WF--BH--2026-01").validate(Vec::new()); + let state = PipelineState::::new("doc1", "WF--BH--2026-01").validate(Vec::new()); let result = state.verify_legal(&solver, &rules); assert!(result.is_ok() || result.is_err()); } @@ -609,7 +647,10 @@ mod tests { .classify("6-8800".to_string()) .reconcile(None); let gate = evaluate_commit_gate(&state, 0.85); - assert!(matches!(gate, crate::validation::CommitGate::Approved { .. })); + assert!(matches!( + gate, + crate::validation::CommitGate::Approved { .. } + )); } #[test] diff --git a/crates/ledger-core/src/render.rs b/crates/ledger-core/src/render.rs index cff83a1..b2ec2ef 100644 --- a/crates/ledger-core/src/render.rs +++ b/crates/ledger-core/src/render.rs @@ -18,10 +18,6 @@ impl GraphRenderer { } pub fn screen_position(&self, x: f32, y: f32, z: f32) -> Vec2 { - crate::layout::iso_project( - glam::Vec3::new(x, y, z), - self.scale, - self.origin, - ) + crate::layout::iso_project(glam::Vec3::new(x, y, z), self.scale, self.origin) } -} \ No newline at end of file +} diff --git a/crates/ledger-core/src/slint_viz.rs b/crates/ledger-core/src/slint_viz.rs index d7af401..ff0849d 100644 --- a/crates/ledger-core/src/slint_viz.rs +++ b/crates/ledger-core/src/slint_viz.rs @@ -32,4 +32,4 @@ impl SlintGraphView { let screen = self.renderer.screen_position(pos.x, pos.y, pos.z); Some((screen.x, screen.y)) } -} \ No newline at end of file +} diff --git a/crates/ledger-core/src/validation.rs b/crates/ledger-core/src/validation.rs index 80e3858..61565b3 100644 --- a/crates/ledger-core/src/validation.rs +++ b/crates/ledger-core/src/validation.rs @@ -129,7 +129,10 @@ impl MetaCtx { confidence: stage_confidence, issue_count: issues.len(), }); - for _issue in issues.iter().filter(|i| matches!(i.disposition, Disposition::Recoverable)) { + for _issue in issues + .iter() + .filter(|i| matches!(i.disposition, Disposition::Recoverable)) + { next.flags.push(MetaFlag::LowUpstreamConf { score: stage_confidence, stage: stage.to_string(), @@ -276,19 +279,27 @@ pub mod verbs { use super::*; pub fn detect() -> VerbDef { - VerbDef::new("detect").with_input("bytes").with_output("ShapeResult") + VerbDef::new("detect") + .with_input("bytes") + .with_output("ShapeResult") } pub fn validate() -> VerbDef { - VerbDef::new("validate").with_input("ShapeResult").with_output("Validated") + VerbDef::new("validate") + .with_input("ShapeResult") + .with_output("Validated") } pub fn classify() -> VerbDef { - VerbDef::new("classify").with_input("Validated").with_output("Classified") + VerbDef::new("classify") + .with_input("Validated") + .with_output("Classified") } pub fn reconcile() -> VerbDef { - VerbDef::new("reconcile").with_input("Classified").with_output("Posting") + VerbDef::new("reconcile") + .with_input("Classified") + .with_output("Posting") } pub fn commit() -> VerbDef { diff --git a/crates/ledger-core/src/verify.rs b/crates/ledger-core/src/verify.rs index ae300d8..7142a99 100644 --- a/crates/ledger-core/src/verify.rs +++ b/crates/ledger-core/src/verify.rs @@ -65,7 +65,7 @@ impl MultiModelConfig { pub trait ModelClient: Send + Sync { /// Generate a completion from the model. fn complete(&self, prompt: &str, max_tokens: usize) -> anyhow::Result; - + /// Extract structured output (JSON) from the model response. fn extract(&self, prompt: &str) -> anyhow::Result; } @@ -110,16 +110,25 @@ pub struct MultiModelVerifier { impl MultiModelVerifier { pub fn new(proposer: C, reviewer: C, config: MultiModelConfig) -> Self { - Self { proposer, reviewer, config } + Self { + proposer, + reviewer, + config, + } } /// Propose a fix for validation issues. - pub fn propose_fix(&self, rule_id: &str, issues_json: &str, context: &str) -> anyhow::Result { + pub fn propose_fix( + &self, + rule_id: &str, + issues_json: &str, + context: &str, + ) -> anyhow::Result { let prompt = format!( "Given these validation issues:\n{}\n\nContext: {}\n\nPropose a fix for rule {}. Return JSON: {{\"rule_id\": \"{}\", \"proposed_fix\": \"...\", \"reasoning\": \"...\", \"confidence\": 0.0-1.0}}", issues_json, context, rule_id, rule_id ); - + self.proposer.extract::(&prompt) } @@ -129,9 +138,9 @@ impl MultiModelVerifier { "Review this proposed fix:\nRule: {}\nFix: {}\nReasoning: {}\nConfidence: {}\n\nReturn JSON: {{\"approved\": bool, \"concerns\": [], \"suggestions\": [], \"confidence\": 0.0-1.0}}", proposal.rule_id, proposal.proposed_fix, proposal.reasoning, proposal.confidence ); - + let result = self.reviewer.extract::(&prompt)?; - + // Check confidence threshold if result.confidence < self.config.min_reviewer_confidence { return Ok(ReviewResult { @@ -143,25 +152,31 @@ impl MultiModelVerifier { ..result }); } - + Ok(result) } /// Full verification loop: propose -> review -> decide. - pub fn verify(&self, rule_id: &str, issues_json: &str, context: &str) -> anyhow::Result { + pub fn verify( + &self, + rule_id: &str, + issues_json: &str, + context: &str, + ) -> anyhow::Result { // Step 1: propose let proposal = self.propose_fix(rule_id, issues_json, context)?; - + // Step 2: review let review = self.review(&proposal)?; - + // Step 3: decision - let outcome = if review.approved && review.confidence >= self.config.min_reviewer_confidence { + let outcome = if review.approved && review.confidence >= self.config.min_reviewer_confidence + { VerificationOutcome::Approved { proposal, review } } else { VerificationOutcome::Rejected { proposal, review } }; - + Ok(outcome) } } @@ -193,9 +208,9 @@ mod tests { fn test_mock_proposer() { let json = r#"{"rule_id":"test","proposed_fix":"fix content","reasoning":"because","confidence":0.85}"#; let mock = MockModelClient::default().with_response(json); - + let result: RepairProposal = mock.extract("prompt").unwrap(); - + assert_eq!(result.rule_id, "test"); assert_eq!(result.confidence, 0.85); } @@ -204,9 +219,9 @@ mod tests { fn test_mock_reviewer_approved() { let json = r#"{"approved":true,"concerns":[],"suggestions":[],"confidence":0.9}"#; let mock = MockModelClient::default().with_response(json); - + let result: ReviewResult = mock.extract("prompt").unwrap(); - + assert!(result.approved); assert_eq!(result.confidence, 0.9); } @@ -214,27 +229,26 @@ mod tests { #[test] fn test_verification_approved() { // Setup mock models that return valid JSON - let proposer_json = r#"{"rule_id":"test-rule","proposed_fix":"x = 1","reasoning":"fix","confidence":0.85}"#; + let proposer_json = + r#"{"rule_id":"test-rule","proposed_fix":"x = 1","reasoning":"fix","confidence":0.85}"#; let reviewer_json = r#"{"approved":true,"concerns":[],"suggestions":[],"confidence":0.9}"#; - + let proposer = MockModelClient::default().with_response(proposer_json); let reviewer = MockModelClient::default().with_response(reviewer_json); - - let verifier = MultiModelVerifier::new( - proposer, - reviewer, - MultiModelConfig::default(), - ); - + + let verifier = MultiModelVerifier::new(proposer, reviewer, MultiModelConfig::default()); + let outcome = verifier.verify("test-rule", "[]", "context").unwrap(); - + assert!(outcome.is_approved()); } #[test] fn test_verification_rejected_low_confidence() { - let proposer_json = r#"{"rule_id":"test","proposed_fix":"x","reasoning":"y","confidence":0.5}"#; - let reviewer_json = r#"{"approved":false,"concerns":["too risky"],"suggestions":[],"confidence":0.6}"#; + let proposer_json = + r#"{"rule_id":"test","proposed_fix":"x","reasoning":"y","confidence":0.5}"#; + let reviewer_json = + r#"{"approved":false,"concerns":["too risky"],"suggestions":[],"confidence":0.6}"#; let proposer = MockModelClient::default().with_response(proposer_json); let reviewer = MockModelClient::default().with_response(reviewer_json); @@ -252,7 +266,7 @@ mod tests { #[test] fn test_config_defaults() { let config = MultiModelConfig::default(); - + assert_eq!(config.proposer_model, "claude-sonnet-4-5"); assert_eq!(config.reviewer_model, "claude-haiku-4-5"); assert_eq!(config.min_reviewer_confidence, 0.80); @@ -274,7 +288,7 @@ mod tests { confidence: 0.9, }, }; - + let rejected = VerificationOutcome::Rejected { proposal: RepairProposal { rule_id: "r1".to_string(), @@ -289,8 +303,8 @@ mod tests { confidence: 0.5, }, }; - + assert!(approved.is_approved()); assert!(!rejected.is_approved()); } -} \ No newline at end of file +} diff --git a/crates/ledger-core/src/visualize.rs b/crates/ledger-core/src/visualize.rs index 0a00563..5ed6f06 100644 --- a/crates/ledger-core/src/visualize.rs +++ b/crates/ledger-core/src/visualize.rs @@ -31,17 +31,24 @@ pub enum NodeVisualState { } impl NodeVisualState { - pub fn from_pipeline(state: PipelineStateEnum, confidence: f32, issues: &[crate::validation::Issue]) -> Self { - if issues.iter().any(|i| i.disposition == Disposition::Unrecoverable) { + pub fn from_pipeline( + state: PipelineStateEnum, + confidence: f32, + issues: &[crate::validation::Issue], + ) -> Self { + if issues + .iter() + .any(|i| i.disposition == Disposition::Unrecoverable) + { return NodeVisualState::Error; } match state { PipelineStateEnum::Committed => NodeVisualState::Success, PipelineStateEnum::NeedsReview => NodeVisualState::Review, - PipelineStateEnum::Ingested | - PipelineStateEnum::Validating | - PipelineStateEnum::Classifying | - PipelineStateEnum::Reconciling => { + PipelineStateEnum::Ingested + | PipelineStateEnum::Validating + | PipelineStateEnum::Classifying + | PipelineStateEnum::Reconciling => { if confidence < 0.5 { NodeVisualState::Warning } else { @@ -149,11 +156,8 @@ impl PipelineGraph { // Update all nodes' visual state based on current position for (node_name, visual_state) in self.nodes.iter_mut() { - *visual_state = NodeVisualState::from_pipeline( - state_from_name(node_name), - confidence, - issues, - ); + *visual_state = + NodeVisualState::from_pipeline(state_from_name(node_name), confidence, issues); } // Mark current node as active (overrides above) @@ -177,7 +181,10 @@ impl PipelineGraph { name, name, fill, anim_class )); } else { - diagram.push_str(&format!(" state {} {{\n {}: {}\n }}\n", name, name, fill)); + diagram.push_str(&format!( + " state {} {{\n {}: {}\n }}\n", + name, name, fill + )); } } @@ -226,7 +233,8 @@ impl PipelineGraph { .shake { animation: shake 0.3s infinite; } .blink { animation: blink 0.5s infinite; } .bounce { animation: bounce 0.5s infinite; } -"#.to_string() +"# + .to_string() } } @@ -267,10 +275,21 @@ pub mod layout { let mut result = HashMap::new(); let mut x = 100.0; - let layer_order = ["Ingested", "Validating", "Classifying", "Reconciling", "Committed", "NeedsReview"]; + let layer_order = [ + "Ingested", + "Validating", + "Classifying", + "Reconciling", + "Committed", + "NeedsReview", + ]; for state in layer_order { if graph.nodes.contains_key(state) { - let width = if state == &graph.current_state { 120.0 } else { 100.0 }; + let width = if state == &graph.current_state { + 120.0 + } else { + 100.0 + }; result.insert(state.to_string(), (x, width)); x += 150.0; } @@ -317,7 +336,13 @@ pub fn to_html(graph: &PipelineGraph) -> String { "#, current, - if confidence > 0.7 { "#4caf50" } else if confidence > 0.4 { "#ff9800" } else { "#f44336" }, + if confidence > 0.7 { + "#4caf50" + } else if confidence > 0.4 { + "#ff9800" + } else { + "#f44336" + }, styles, current, confidence, @@ -332,28 +357,26 @@ mod tests { #[test] fn test_node_visual_state() { // Test Active state - let state = NodeVisualState::from_pipeline( - PipelineStateEnum::Validating, - 0.9, - &[] - ); + let state = NodeVisualState::from_pipeline(PipelineStateEnum::Validating, 0.9, &[]); assert_eq!(state, NodeVisualState::Active); // Test Warning (low confidence) - let state = NodeVisualState::from_pipeline( - PipelineStateEnum::Classifying, - 0.3, - &[] - ); + let state = NodeVisualState::from_pipeline(PipelineStateEnum::Classifying, 0.3, &[]); assert_eq!(state, NodeVisualState::Warning); } #[test] fn test_mermaid_generation() { let mut graph = PipelineGraph::new(); - graph.nodes.insert("Ingested".to_string(), NodeVisualState::Success); - graph.nodes.insert("Validating".to_string(), NodeVisualState::Active); - graph.nodes.insert("Classifying".to_string(), NodeVisualState::Idle); + graph + .nodes + .insert("Ingested".to_string(), NodeVisualState::Success); + graph + .nodes + .insert("Validating".to_string(), NodeVisualState::Active); + graph + .nodes + .insert("Classifying".to_string(), NodeVisualState::Idle); graph.current_state = "Validating".to_string(); graph.accumulated_confidence = 0.85; @@ -383,11 +406,13 @@ mod tests { fn test_layout_constraints() { let solver = layout::LayoutSolver::new(); let mut graph = PipelineGraph::new(); - graph.nodes.insert("Validating".to_string(), NodeVisualState::Active); + graph + .nodes + .insert("Validating".to_string(), NodeVisualState::Active); graph.current_state = "Validating".to_string(); graph.accumulated_confidence = 0.8; let layout = solver.generate_layout(&graph); assert!(!layout.is_empty()); } -} \ No newline at end of file +} diff --git a/crates/ledger-core/src/workflow.rs b/crates/ledger-core/src/workflow.rs index a1705d0..d8d4bfb 100644 --- a/crates/ledger-core/src/workflow.rs +++ b/crates/ledger-core/src/workflow.rs @@ -56,20 +56,33 @@ impl WorkflowToml { } // Guard without else if t.guard.is_some() && t.else_to.is_none() { - errors.push(format!("transition {}→{} has guard but no else", t.from, t.to)); + errors.push(format!( + "transition {}→{} has guard but no else", + t.from, t.to + )); } } // Check exactly one initial - let initials: Vec<_> = self.state.iter().filter(|s| s.initial == Some(true)).collect(); + let initials: Vec<_> = self + .state + .iter() + .filter(|s| s.initial == Some(true)) + .collect(); if initials.len() != 1 { - errors.push(format!("exactly one initial state required, found {}", initials.len())); + errors.push(format!( + "exactly one initial state required, found {}", + initials.len() + )); } // Check non-terminal states have outgoing transitions for s in self.state.iter().filter(|s| s.terminal != Some(true)) { if !self.transitions.iter().any(|t| t.from == s.id) { - errors.push(format!("non-terminal state {} has no outgoing transitions", s.id)); + errors.push(format!( + "non-terminal state {} has no outgoing transitions", + s.id + )); } } @@ -120,13 +133,15 @@ impl WorkflowToml { let from = &t.from; let to = &t.to; let event = &t.event; - + let arm = match &t.guard { None => format!(" [\"{}\", \"{}\"] => \"{}\"", from, event, to), Some(g) => { let else_to = t.else_to.as_deref().unwrap_or(to); - format!(" [\"{}\", \"{}\"] => if {} then \"{}\" else \"{}\"", - from, event, g, to, else_to) + format!( + " [\"{}\", \"{}\"] => if {} then \"{}\" else \"{}\"", + from, event, g, to, else_to + ) } }; arms.push(arm); @@ -291,4 +306,4 @@ mod tests { let rust = wf.to_rust_enum(); assert!(rust.contains("enum PipelineState")); } -} \ No newline at end of file +} diff --git a/crates/ledger-core/tests/iso_lint.rs b/crates/ledger-core/tests/iso_lint.rs index e2361ce..40385c7 100644 --- a/crates/ledger-core/tests/iso_lint.rs +++ b/crates/ledger-core/tests/iso_lint.rs @@ -15,8 +15,16 @@ use ledger_core::validation::{CommitGate, Issue, MetaFlag, StageResult}; macro_rules! lint_spec { ($ty:ty) => {{ let spec = <$ty>::viz_spec(); - assert!(!spec.description.is_empty(), "description is empty for {}", stringify!($ty)); - assert!(!spec.rhai_dsl.is_empty(), "rhai_dsl is empty for {}", stringify!($ty)); + assert!( + !spec.description.is_empty(), + "description is empty for {}", + stringify!($ty) + ); + assert!( + !spec.rhai_dsl.is_empty(), + "rhai_dsl is empty for {}", + stringify!($ty) + ); assert!( spec.z_layer.index() <= 5, "z_layer.index() > 5 for {}", diff --git a/crates/ledger-core/tests/legal_z3_integration.rs b/crates/ledger-core/tests/legal_z3_integration.rs index 6b41587..57d2c11 100644 --- a/crates/ledger-core/tests/legal_z3_integration.rs +++ b/crates/ledger-core/tests/legal_z3_integration.rs @@ -9,9 +9,7 @@ fn z3_native_violation_satisfied() { use ledger_core::legal::{LegalSolver, TransactionFacts, Z3Result}; let solver = LegalSolver::new(); - let rules = [ - ledger_core::legal::au_gst::rule_38_190(), - ]; + let rules = [ledger_core::legal::au_gst::rule_38_190()]; let facts = TransactionFacts::new() .with_vendor("US") .with_supply_type("SaaS") @@ -28,9 +26,7 @@ fn z3_native_violation_violated() { use ledger_core::legal::{LegalSolver, TransactionFacts}; let solver = LegalSolver::new(); - let rules = [ - ledger_core::legal::au_gst::rule_38_190(), - ]; + let rules = [ledger_core::legal::au_gst::rule_38_190()]; let facts = TransactionFacts::new() .with_vendor("US") .with_supply_type("SaaS") @@ -48,9 +44,7 @@ fn z3_native_disposition_via_to_issues() { use ledger_core::legal::{LegalSolver, TransactionFacts, Z3Result}; let solver = LegalSolver::new(); - let rules = [ - ledger_core::legal::au_gst::rule_38_190(), - ]; + let rules = [ledger_core::legal::au_gst::rule_38_190()]; // Violated → Unrecoverable let violated_facts = TransactionFacts::new() @@ -59,7 +53,10 @@ fn z3_native_disposition_via_to_issues() { .with_tax_code("INPUT"); let (_, issues) = solver.verify_all(&rules, &violated_facts); assert_eq!(issues.len(), 1); - assert_eq!(issues[0].disposition, ledger_core::validation::Disposition::Unrecoverable); + assert_eq!( + issues[0].disposition, + ledger_core::validation::Disposition::Unrecoverable + ); // Satisfied → no issues let satisfied_facts = TransactionFacts::new() @@ -75,7 +72,12 @@ fn z3_native_disposition_via_to_issues() { #[test] fn z3_feature_disabled_fallback() { use ledger_core::legal::Z3Result; - let violated = Z3Result::Violated { witness: "test".into() }; - assert_eq!(violated.to_disposition(), ledger_core::validation::Disposition::Unrecoverable); + let violated = Z3Result::Violated { + witness: "test".into(), + }; + assert_eq!( + violated.to_disposition(), + ledger_core::validation::Disposition::Unrecoverable + ); assert_eq!(violated.to_confidence(), 0.0); } diff --git a/crates/ledger-core/tests/rhai_rules.rs b/crates/ledger-core/tests/rhai_rules.rs index d3a8b9e..ca610c3 100644 --- a/crates/ledger-core/tests/rhai_rules.rs +++ b/crates/ledger-core/tests/rhai_rules.rs @@ -19,8 +19,8 @@ use ledger_core::classify::{ClassificationEngine, SampleTransaction}; fn rule_path(filename: &str) -> PathBuf { // CARGO_MANIFEST_DIR = .../crates/ledger-core // two parents up = workspace root - let manifest = std::env::var("CARGO_MANIFEST_DIR") - .expect("CARGO_MANIFEST_DIR must be set by cargo test"); + let manifest = + std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR must be set by cargo test"); PathBuf::from(manifest) .parent() // crates/ .expect("crates parent") @@ -88,7 +88,10 @@ fn rhai_02_foreign_income_zero_amount_no_review() { .unwrap_or_else(|e| panic!("rule execution failed: {e}")); assert_eq!(outcome.category, "ForeignIncome"); - assert!(!outcome.needs_review, "zero amount must not trigger review flag"); + assert!( + !outcome.needs_review, + "zero amount must not trigger review flag" + ); } #[test] @@ -189,7 +192,10 @@ fn rhai_06_self_employment_strong_keyword() { (outcome.confidence - 0.85).abs() < f64::EPSILON, "strong keyword should yield confidence 0.85" ); - assert!(!outcome.needs_review, "strong match should not require review"); + assert!( + !outcome.needs_review, + "strong match should not require review" + ); } #[test] @@ -285,7 +291,10 @@ fn rhai_10_fallback_zero_amount_still_review() { .unwrap_or_else(|e| panic!("rule execution failed: {e}")); assert_eq!(outcome.category, "Unclassified"); - assert!(outcome.needs_review, "zero-amount fallback must require review"); + assert!( + outcome.needs_review, + "zero-amount fallback must require review" + ); } #[test] diff --git a/crates/ledger-core/tests/rhai_rules_extended.rs b/crates/ledger-core/tests/rhai_rules_extended.rs index 812816b..3517583 100644 --- a/crates/ledger-core/tests/rhai_rules_extended.rs +++ b/crates/ledger-core/tests/rhai_rules_extended.rs @@ -15,8 +15,8 @@ use std::path::PathBuf; use ledger_core::classify::{ClassificationEngine, SampleTransaction}; fn rule_path(filename: &str) -> PathBuf { - let manifest = std::env::var("CARGO_MANIFEST_DIR") - .expect("CARGO_MANIFEST_DIR must be set by cargo test"); + let manifest = + std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR must be set by cargo test"); PathBuf::from(manifest) .parent() // crates/ .expect("crates parent") @@ -55,7 +55,10 @@ fn sc_01_income_keyword_classifies_self_employment() { "expected 0.88, got {}", outcome.confidence ); - assert!(!outcome.needs_review, "amount 3000 is below $5000 review threshold"); + assert!( + !outcome.needs_review, + "amount 3000 is below $5000 review threshold" + ); } #[test] @@ -96,7 +99,10 @@ fn sc_03_negative_expense_keyword_classifies_office_supplies() { "expected 0.75, got {}", outcome.confidence ); - assert!(!outcome.needs_review, "expense $150 is below $2500 review trigger"); + assert!( + !outcome.needs_review, + "expense $150 is below $2500 review trigger" + ); } #[test] @@ -156,7 +162,10 @@ fn sd_01_stock_sale_positive_classifies_capital_gain() { "expected 0.85, got {}", outcome.confidence ); - assert!(!outcome.needs_review, "no short-term signal — no review required"); + assert!( + !outcome.needs_review, + "no short-term signal — no review required" + ); } #[test] @@ -195,7 +204,10 @@ fn sd_03_short_term_signal_triggers_review() { .unwrap_or_else(|e| panic!("rule failed: {e}")); assert_eq!(outcome.category, "CapitalGain"); - assert!(outcome.needs_review, "short-term indicator must trigger review"); + assert!( + outcome.needs_review, + "short-term indicator must trigger review" + ); } #[test] @@ -237,7 +249,10 @@ fn se_01_rental_income_classifies_correctly() { "expected 0.87, got {}", outcome.confidence ); - assert!(!outcome.needs_review, "amount 2200 below $10000 review threshold"); + assert!( + !outcome.needs_review, + "amount 2200 below $10000 review threshold" + ); } #[test] @@ -277,7 +292,10 @@ fn se_03_negative_amount_with_rent_signal_lower_confidence() { "expected 0.80, got {}", outcome.confidence ); - assert!(outcome.needs_review, "negative amount with rent signal must trigger review"); + assert!( + outcome.needs_review, + "negative amount with rent signal must trigger review" + ); } // --------------------------------------------------------------------------- @@ -304,7 +322,10 @@ fn fbar_01_hsbc_account_classifies_foreign_income() { "expected 0.82, got {}", outcome.confidence ); - assert!(!outcome.needs_review, "amount 5000 below $9000 FBAR near-threshold trigger"); + assert!( + !outcome.needs_review, + "amount 5000 below $9000 FBAR near-threshold trigger" + ); } #[test] @@ -322,7 +343,10 @@ fn fbar_02_near_threshold_triggers_review() { .unwrap_or_else(|e| panic!("rule failed: {e}")); assert_eq!(outcome.category, "ForeignIncome"); - assert!(outcome.needs_review, "amount > $9000 must trigger FBAR review"); + assert!( + outcome.needs_review, + "amount > $9000 must trigger FBAR review" + ); assert!( outcome.reason.contains("31 USC §5314"), "reason must cite 31 USC §5314; got: {}", @@ -383,7 +407,10 @@ fn fatca_01_high_value_foreign_financial_triggers_review() { .unwrap_or_else(|e| panic!("rule failed: {e}")); assert_eq!(outcome.category, "ForeignIncome"); - assert!(outcome.needs_review, "amount > 25000 must trigger FATCA review"); + assert!( + outcome.needs_review, + "amount > 25000 must trigger FATCA review" + ); assert!( outcome.reason.contains("IRC §6038D"), "reason must cite IRC §6038D; got: {}", @@ -411,7 +438,10 @@ fn fatca_02_low_value_no_review() { "expected 0.65 for low-value FATCA signal, got {}", outcome.confidence ); - assert!(!outcome.needs_review, "amount 10000 below 25000 → no review required"); + assert!( + !outcome.needs_review, + "amount 10000 below 25000 → no review required" + ); } #[test] @@ -454,7 +484,10 @@ fn ct_01_crypto_buy_classifies_transfer() { "expected 0.90, got {}", outcome.confidence ); - assert!(!outcome.needs_review, "buy is not a taxable event — no review needed"); + assert!( + !outcome.needs_review, + "buy is not a taxable event — no review needed" + ); } #[test] @@ -477,7 +510,10 @@ fn ct_02_crypto_sell_positive_classifies_crypto_gain() { "expected 0.85, got {}", outcome.confidence ); - assert!(outcome.needs_review, "crypto sale is a capital event — must review"); + assert!( + outcome.needs_review, + "crypto sale is a capital event — must review" + ); } #[test] @@ -495,7 +531,10 @@ fn ct_03_crypto_sell_negative_classifies_capital_loss() { .unwrap_or_else(|e| panic!("rule failed: {e}")); assert_eq!(outcome.category, "CapitalLoss"); - assert!(outcome.needs_review, "crypto loss is a capital event — must review"); + assert!( + outcome.needs_review, + "crypto loss is a capital event — must review" + ); } #[test] @@ -554,7 +593,10 @@ fn cs_01_staking_reward_classifies_crypto_income() { "expected 0.88, got {}", outcome.confidence ); - assert!(outcome.needs_review, "staking reward always requires FMV review"); + assert!( + outcome.needs_review, + "staking reward always requires FMV review" + ); assert!( outcome.reason.contains("Rev. Rul. 2023-14"), "reason must cite Rev. Rul. 2023-14; got: {}", @@ -665,7 +707,10 @@ fn ag_02_positive_au_income_classifies_foreign_income_with_review() { "expected 0.78, got {}", outcome.confidence ); - assert!(outcome.needs_review, "AU income for US expat always requires review"); + assert!( + outcome.needs_review, + "AU income for US expat always requires review" + ); } #[test] @@ -708,7 +753,10 @@ fn ac_01_au_property_sale_classifies_au_cgt() { "expected 0.87, got {}", outcome.confidence ); - assert!(outcome.needs_review, "AU CGT always requires review for discount eligibility"); + assert!( + outcome.needs_review, + "AU CGT always requires review for discount eligibility" + ); assert!( outcome.reason.contains("ITAA 1997 s.115-A"), "reason must cite ITAA 1997 s.115-A; got: {}", diff --git a/crates/ledger-core/tests/type_mesh.rs b/crates/ledger-core/tests/type_mesh.rs index 18f8b00..adeab80 100644 --- a/crates/ledger-core/tests/type_mesh.rs +++ b/crates/ledger-core/tests/type_mesh.rs @@ -4,9 +4,7 @@ //! structurally compatible at compile time. If a type changes and breaks //! the mesh, these tests fail to compile — catching drift before runtime. -use ledger_core::classify::{ - ClassificationOutcome, ClassifiedTransaction, SampleTransaction, -}; +use ledger_core::classify::{ClassificationOutcome, ClassifiedTransaction, SampleTransaction}; use ledger_core::ingest::{deterministic_tx_id, IngestedTransaction, TransactionInput}; use ledger_core::journal::JournalTransaction; use ledger_core::validation::{and_then, Disposition, Issue, IssueSource, MetaCtx, StageResult}; @@ -79,10 +77,7 @@ fn test_transaction_input_to_journal_shape() { #[test] fn test_classification_outcome_to_classified_shape() { - fn check_shape( - tx_id: String, - outcome: ClassificationOutcome, - ) -> ClassifiedTransaction { + fn check_shape(tx_id: String, outcome: ClassificationOutcome) -> ClassifiedTransaction { ClassifiedTransaction { tx_id, category: outcome.category, @@ -140,9 +135,13 @@ fn test_classified_to_projection_row_requires_context() { #[test] fn test_validation_pipeline_mesh() { - let issue = Issue::recoverable("V-001", "low confidence classification", IssueSource::RhaiRule { - rule_id: "classify_schedule_c".into(), - }); + let issue = Issue::recoverable( + "V-001", + "low confidence classification", + IssueSource::RhaiRule { + rule_id: "classify_schedule_c".into(), + }, + ); let issues = vec![issue]; let initial = MetaCtx::initial(); @@ -173,7 +172,11 @@ fn test_validation_pipeline_mesh() { #[test] fn test_disposition_enum_coverage() { let unrecoverable = Issue::unrecoverable("E-001", "data corruption detected"); - let recoverable = Issue::recoverable("W-001", "low confidence classification", IssueSource::Constraint { strength: 0.5 }); + let recoverable = Issue::recoverable( + "W-001", + "low confidence classification", + IssueSource::Constraint { strength: 0.5 }, + ); let advisory = Issue::advisory("A-001", "consider reviewing category"); assert_eq!(unrecoverable.disposition, Disposition::Unrecoverable); diff --git a/crates/ledgerr-host/src/bin/host-window.rs b/crates/ledgerr-host/src/bin/host-window.rs index 4acb3e4..0120135 100644 --- a/crates/ledgerr-host/src/bin/host-window.rs +++ b/crates/ledgerr-host/src/bin/host-window.rs @@ -1075,18 +1075,16 @@ fn apply_initial_window_geometry(window: &slint::Window) { const SPI_GETWORKAREA: u32 = 0x0030; let mut work = Rect::default(); // SAFETY: Rect is repr(C) and sized exactly as RECT; pointer is valid for the call duration. - let ok = unsafe { - SystemParametersInfoW( - SPI_GETWORKAREA, - 0, - &mut work as *mut _ as *mut _, - 0, - ) - }; + let ok = unsafe { SystemParametersInfoW(SPI_GETWORKAREA, 0, &mut work as *mut _ as *mut _, 0) }; // Fall back to a safe default if the API call fails. if ok == 0 { - work = Rect { left: 0, top: 0, right: 1920, bottom: 1040 }; + work = Rect { + left: 0, + top: 0, + right: 1920, + bottom: 1040, + }; } let scale = window.scale_factor(); @@ -1100,7 +1098,9 @@ fn apply_initial_window_geometry(window: &slint::Window) { let win_x = work_x + (work_w - win_w) / 2.0; let win_y = work_y + (work_h - win_h) / 2.0; - window.set_size(slint::WindowSize::Logical(slint::LogicalSize::new(win_w, win_h))); + window.set_size(slint::WindowSize::Logical(slint::LogicalSize::new( + win_w, win_h, + ))); window.set_position(slint::WindowPosition::Logical(slint::LogicalPosition::new( win_x, win_y, ))); diff --git a/crates/ledgerr-host/src/evidence.rs b/crates/ledgerr-host/src/evidence.rs index a2b7332..1d10d52 100644 --- a/crates/ledgerr-host/src/evidence.rs +++ b/crates/ledgerr-host/src/evidence.rs @@ -1,9 +1,8 @@ +use crate::internal_openai::{provider_status, ProviderReadiness}; +use crate::settings::AppSettings; use arc_kit_au::{ - EvidenceGraph, EvidenceTracer, ProvenanceBadge, ProvenanceScanner, - node::NodeType, + node::NodeType, EvidenceGraph, EvidenceTracer, ProvenanceBadge, ProvenanceScanner, }; -use crate::internal_openai::{ProviderReadiness, provider_status}; -use crate::settings::AppSettings; #[derive(Debug, Default)] pub struct EvidenceState { @@ -83,10 +82,16 @@ impl TodayQueue { let mut next_actions = vec![]; if blocked > 0 { - next_actions.push(format!("Review {} transactions with critical gaps.", blocked)); + next_actions.push(format!( + "Review {} transactions with critical gaps.", + blocked + )); } if ready > 0 { - next_actions.push(format!("Review {} transactions with partial evidence.", ready)); + next_actions.push(format!( + "Review {} transactions with partial evidence.", + ready + )); } if next_actions.is_empty() && evidence.checked { next_actions.push("No review items — ready to export workbook.".to_string()); @@ -120,8 +125,8 @@ impl TodayQueue { #[cfg(test)] mod tests { - use crate::internal_openai::ModelProviderLabel; use super::*; + use crate::internal_openai::ModelProviderLabel; fn test_settings() -> AppSettings { AppSettings::default() diff --git a/crates/ledgerr-host/src/internal_openai.rs b/crates/ledgerr-host/src/internal_openai.rs index 7b583af..d9ed307 100644 --- a/crates/ledgerr-host/src/internal_openai.rs +++ b/crates/ledgerr-host/src/internal_openai.rs @@ -197,7 +197,6 @@ pub fn provider_status(settings: &crate::settings::AppSettings) -> Vec) -> ChatSetting /// /// Requires Foundry Local to be running. Returns an error if the foundry /// binary is not found or the service status cannot be determined. -pub fn windows_ai_chat_settings( - system_prompt: impl Into, -) -> Result { +pub fn windows_ai_chat_settings(system_prompt: impl Into) -> Result { foundry_local_chat_settings(system_prompt) } @@ -395,7 +392,12 @@ fn discover_foundry_rest_endpoint(endpoint: &str) -> Option { .connect_timeout(Duration::from_secs(1)) .build() .ok()?; - let status = client.get(&status_url).send().ok()?.json::().ok()?; + let status = client + .get(&status_url) + .send() + .ok()? + .json::() + .ok()?; status .endpoints .into_iter() @@ -414,7 +416,10 @@ fn normalize_foundry_endpoint(endpoint: &str) -> String { } fn foundry_chat_url(endpoint: &str) -> String { - format!("{}/v1/chat/completions", normalize_foundry_endpoint(endpoint)) + format!( + "{}/v1/chat/completions", + normalize_foundry_endpoint(endpoint) + ) } pub fn internal_phi_backend_status() -> String { @@ -1568,36 +1573,36 @@ mod tests { assert!(response.starts_with("HTTP/1.1 400 Bad Request")); } - + #[test] fn provider_label_display_names_match_prd5() { assert_eq!(ModelProviderLabel::LocalDemo.display_name(), "Local Demo"); assert_eq!(ModelProviderLabel::WindowsAi.display_name(), "Windows AI"); assert_eq!(ModelProviderLabel::Cloud.display_name(), "Cloud"); } - + #[test] fn provider_label_descriptions_explain_privacy_and_setup() { let local = ModelProviderLabel::LocalDemo.description(); assert!(local.contains("Private")); assert!(local.contains("fallback")); - + let windows = ModelProviderLabel::WindowsAi.description(); assert!(windows.contains("Private")); assert!(windows.contains("setup")); - + let cloud = ModelProviderLabel::Cloud.description(); assert!(cloud.contains("external")); assert!(cloud.contains("endpoint")); } - + #[test] fn local_demo_readiness_is_ready() { let settings = crate::settings::AppSettings::default(); let readiness = ModelProviderLabel::LocalDemo.readiness(&settings); assert!(matches!(readiness, ProviderReadiness::Ready)); } - + #[test] fn cloud_readiness_needs_configured_endpoint_and_key() { let settings = crate::settings::AppSettings::default(); @@ -1627,10 +1632,13 @@ mod tests { assert!(labels.contains(&ModelProviderLabel::LocalDemo)); assert!(labels.contains(&ModelProviderLabel::WindowsAi)); assert!(labels.contains(&ModelProviderLabel::Cloud)); - let default = providers.iter().find(|p| p.is_default).expect("default provider"); + let default = providers + .iter() + .find(|p| p.is_default) + .expect("default provider"); assert_eq!(default.label, ModelProviderLabel::LocalDemo); } - + #[test] fn local_demo_chat_settings_uses_internal_endpoint() { let settings = local_demo_chat_settings("test prompt"); @@ -1639,7 +1647,7 @@ mod tests { assert_eq!(settings.api_key, INTERNAL_LOCAL_API_KEY); assert_eq!(settings.system_prompt, "test prompt"); } - + #[test] fn cloud_chat_settings_uses_default_cloud_url_with_empty_auth() { let settings = cloud_chat_settings("test prompt"); @@ -1648,7 +1656,7 @@ mod tests { assert!(settings.api_key.is_empty()); assert_eq!(settings.system_prompt, "test prompt"); } - + /// Resolve active ChatSettings from the AppSettings model_provider field. /// /// Returns the resolved settings and an optional warning if a fallback occurred. @@ -1667,7 +1675,7 @@ mod tests { assert!(cs.api_key.is_empty()); assert!(warning.is_none()); } - + #[test] fn resolve_chat_settings_falls_back_to_local_demo_for_windows_ai_when_not_installed() { use crate::settings::AppSettings; @@ -1679,7 +1687,6 @@ mod tests { assert!(!cs.endpoint_url.is_empty()); assert!(warning.is_some()); } - } /// Resolve active ChatSettings from the AppSettings model_provider field. @@ -1689,7 +1696,10 @@ mod tests { pub fn resolve_chat_settings( settings: &crate::settings::AppSettings, ) -> (ChatSettings, Option) { - match settings.model_provider.chat_settings(settings.chat.system_prompt.clone()) { + match settings + .model_provider + .chat_settings(settings.chat.system_prompt.clone()) + { Ok(cs) => (cs, None), Err(_) => { let fallback = local_demo_chat_settings(settings.chat.system_prompt.clone()); diff --git a/crates/ledgerr-host/src/lib.rs b/crates/ledgerr-host/src/lib.rs index 0a13780..d9f7248 100644 --- a/crates/ledgerr-host/src/lib.rs +++ b/crates/ledgerr-host/src/lib.rs @@ -9,8 +9,8 @@ pub mod local_llm_mistral; pub mod notify; pub mod settings; pub mod tray; +pub use evidence::{EvidenceState, TodayQueue}; pub use internal_openai::{ - cloud_chat_settings, local_demo_chat_settings, windows_ai_chat_settings, provider_status, - resolve_chat_settings, ModelProviderLabel, ProviderInfo, ProviderReadiness, + cloud_chat_settings, local_demo_chat_settings, provider_status, resolve_chat_settings, + windows_ai_chat_settings, ModelProviderLabel, ProviderInfo, ProviderReadiness, }; -pub use evidence::{EvidenceState, TodayQueue}; diff --git a/crates/ledgerr-host/src/local_llm.rs b/crates/ledgerr-host/src/local_llm.rs index f672ef7..fefd0cc 100644 --- a/crates/ledgerr-host/src/local_llm.rs +++ b/crates/ledgerr-host/src/local_llm.rs @@ -75,15 +75,17 @@ use std::path::{Path, PathBuf}; -use candle_core::{quantized::gguf_file, Device, Tensor}; use candle_core::quantized::gguf_file::TensorInfo; +use candle_core::{quantized::gguf_file, Device, Tensor}; use candle_transformers::{ generation::{LogitsProcessor, Sampling}, models::quantized_phi3::ModelWeights, }; use tokenizers::Tokenizer; -use crate::agent_runtime::{AgentRuntime, AgentRuntimeError, ModelRequest, ModelResponse, ModelRole}; +use crate::agent_runtime::{ + AgentRuntime, AgentRuntimeError, ModelRequest, ModelResponse, ModelRole, +}; /// Phi-4 Mini chat template tokens. const CHAT_START_SYS: &str = "<|system|>\n"; @@ -277,9 +279,10 @@ fn patch_rope_dim_to_head_dim(content: &mut gguf_file::Content) { emb / heads }; - content - .metadata - .insert("phi3.rope.dimension_count".to_string(), Value::U32(head_dim)); + content.metadata.insert( + "phi3.rope.dimension_count".to_string(), + Value::U32(head_dim), + ); } /// Phi-4 mini GGUF omits `output.weight` because the model uses tied embeddings @@ -295,7 +298,9 @@ fn patch_tied_output_weight(content: &mut gguf_file::Content) { shape: embd.shape.clone(), offset: embd.offset, }; - content.tensor_infos.insert("output.weight".to_string(), aliased); + content + .tensor_infos + .insert("output.weight".to_string(), aliased); } } diff --git a/crates/ledgerr-host/src/local_llm_mistral.rs b/crates/ledgerr-host/src/local_llm_mistral.rs index dc76962..46df6eb 100644 --- a/crates/ledgerr-host/src/local_llm_mistral.rs +++ b/crates/ledgerr-host/src/local_llm_mistral.rs @@ -178,7 +178,9 @@ use std::path::{Path, PathBuf}; use mistralrs::{GgufModelBuilder, TextMessageRole, TextMessages}; -use crate::agent_runtime::{AgentRuntime, AgentRuntimeError, ModelRequest, ModelResponse, ModelRole}; +use crate::agent_runtime::{ + AgentRuntime, AgentRuntimeError, ModelRequest, ModelResponse, ModelRole, +}; /// HuggingFace repo used to fetch and cache the Phi-4 mini tokenizer vocabulary. /// @@ -238,10 +240,7 @@ impl LocalMistralRuntime { .file_name() .and_then(|n| n.to_str()) .ok_or_else(|| { - AgentRuntimeError::LocalLlm(format!( - "cannot read filename from {}", - path.display() - )) + AgentRuntimeError::LocalLlm(format!("cannot read filename from {}", path.display())) })? .to_string(); Ok(Self { @@ -304,7 +303,10 @@ impl LocalMistralRuntime { /// 1. Ensure the patched GGUF sidecar exists (see `ensure_patched_gguf`). /// 2. Build a mistralrs model pipeline from the sidecar file. /// 3. Send the chat request and extract the first completion choice. - async fn complete_async(&self, request: ModelRequest) -> Result { + async fn complete_async( + &self, + request: ModelRequest, + ) -> Result { let original = self.model_dir.join(&self.model_file); // Produce (or reuse) the patched sidecar GGUF that works around Phi-4 Mini's @@ -315,12 +317,16 @@ impl LocalMistralRuntime { let model_dir_str = patched .parent() .and_then(|p| p.to_str()) - .ok_or_else(|| AgentRuntimeError::LocalLlm("patched model dir path is not valid UTF-8".into()))? + .ok_or_else(|| { + AgentRuntimeError::LocalLlm("patched model dir path is not valid UTF-8".into()) + })? .to_string(); let model_file = patched .file_name() .and_then(|n| n.to_str()) - .ok_or_else(|| AgentRuntimeError::LocalLlm("patched model filename is not valid UTF-8".into()))? + .ok_or_else(|| { + AgentRuntimeError::LocalLlm("patched model filename is not valid UTF-8".into()) + })? .to_string(); let model = GgufModelBuilder::new(model_dir_str, vec![model_file]) @@ -510,8 +516,14 @@ fn ensure_patched_gguf(original: &Path) -> Result { .collect(); // Ensure the key is present even if the original GGUF omitted it. - if !metadata_owned.iter().any(|(k, _)| k == "phi3.rope.dimension_count") { - metadata_owned.push(("phi3.rope.dimension_count".to_string(), gguf_file::Value::U32(head_dim))); + if !metadata_owned + .iter() + .any(|(k, _)| k == "phi3.rope.dimension_count") + { + metadata_owned.push(( + "phi3.rope.dimension_count".to_string(), + gguf_file::Value::U32(head_dim), + )); } let meta_refs: Vec<(&str, &gguf_file::Value)> = metadata_owned diff --git a/crates/ledgerr-host/src/settings/schema.rs b/crates/ledgerr-host/src/settings/schema.rs index 180c3ad..df28edd 100644 --- a/crates/ledgerr-host/src/settings/schema.rs +++ b/crates/ledgerr-host/src/settings/schema.rs @@ -113,7 +113,12 @@ impl AppSettings { /// Returns (resolved_settings, Option) where the second /// element is Some when a fallback occurred (e.g., WindowsAi selected but /// Foundry not installed). The caller decides whether to surface the warning. - pub fn resolve_chat(&self) -> (ChatSettings, Option) { + pub fn resolve_chat( + &self, + ) -> ( + ChatSettings, + Option, + ) { crate::internal_openai::resolve_chat_settings(self) } } diff --git a/crates/ledgerr-host/tests/phi4_smoke.rs b/crates/ledgerr-host/tests/phi4_smoke.rs index 576b2bb..ec5269b 100644 --- a/crates/ledgerr-host/tests/phi4_smoke.rs +++ b/crates/ledgerr-host/tests/phi4_smoke.rs @@ -66,8 +66,8 @@ mod phi4 { let request = ModelRequest::text("Reply with only the single word: HELLO") .with_system_prompt("You are a terse assistant. Follow instructions exactly."); - let response = AgentRuntime::complete(&runtime, request) - .expect("phi4 completion should not fail"); + let response = + AgentRuntime::complete(&runtime, request).expect("phi4 completion should not fail"); assert!( !response.assistant_text.is_empty(), diff --git a/crates/ledgerr-host/tests/visualization_e2e.rs b/crates/ledgerr-host/tests/visualization_e2e.rs index d8d48ea..a273b79 100644 --- a/crates/ledgerr-host/tests/visualization_e2e.rs +++ b/crates/ledgerr-host/tests/visualization_e2e.rs @@ -1,12 +1,12 @@ //! E2E Visualization Tests for Slint UX //! Tests the pipeline flow visualization through the Slint UI. +use ledger_core::pipeline::State as PipelineState; +use ledger_core::validation::{Issue, IssueSource}; use ledger_core::visualize::{ - PipelineGraph, NodeVisualState, EdgeVisual, to_html, layout::LayoutSolver, + layout::LayoutSolver, to_html, EdgeVisual, NodeVisualState, PipelineGraph, }; -use ledger_core::pipeline::State as PipelineState; use ledger_core::workflow::examples::ledger_ingest; -use ledger_core::validation::{Issue, IssueSource}; /// Test: Full pipeline visualization cycle from ingest to commit. #[test] @@ -22,7 +22,9 @@ fn test_e2e_pipeline_visualization() { graph.nodes.insert(state.id.clone(), NodeVisualState::Idle); } for trans in &wf.transitions { - graph.edges.push(EdgeVisual::new(&trans.from, &trans.to, &trans.event)); + graph + .edges + .push(EdgeVisual::new(&trans.from, &trans.to, &trans.event)); } // 3. Stage 1: Ingested -> Validating @@ -35,9 +37,15 @@ fn test_e2e_pipeline_visualization() { #[test] fn test_high_confidence_state() { let mut graph = PipelineGraph::new(); - graph.nodes.insert("Ingested".to_string(), NodeVisualState::Success); - graph.nodes.insert("Validating".to_string(), NodeVisualState::Active); - graph.nodes.insert("Classifying".to_string(), NodeVisualState::Idle); + graph + .nodes + .insert("Ingested".to_string(), NodeVisualState::Success); + graph + .nodes + .insert("Validating".to_string(), NodeVisualState::Active); + graph + .nodes + .insert("Classifying".to_string(), NodeVisualState::Idle); graph.current_state = "Validating".to_string(); graph.accumulated_confidence = 0.92; @@ -51,7 +59,9 @@ fn test_high_confidence_state() { #[test] fn test_low_confidence_warning() { let mut graph = PipelineGraph::new(); - graph.nodes.insert("Validating".to_string(), NodeVisualState::Warning); + graph + .nodes + .insert("Validating".to_string(), NodeVisualState::Warning); graph.current_state = "Validating".to_string(); graph.accumulated_confidence = 0.35; @@ -63,7 +73,9 @@ fn test_low_confidence_warning() { #[test] fn test_error_state() { let mut graph = PipelineGraph::new(); - graph.nodes.insert("Validating".to_string(), NodeVisualState::Error); + graph + .nodes + .insert("Validating".to_string(), NodeVisualState::Error); graph.current_state = "Validating".to_string(); graph.accumulated_confidence = 0.0; @@ -75,7 +87,9 @@ fn test_error_state() { #[test] fn test_review_state() { let mut graph = PipelineGraph::new(); - graph.nodes.insert("NeedsReview".to_string(), NodeVisualState::Review); + graph + .nodes + .insert("NeedsReview".to_string(), NodeVisualState::Review); graph.current_state = "NeedsReview".to_string(); let html = to_html(&graph); @@ -87,7 +101,7 @@ fn test_review_state() { fn test_animation_styles() { let graph = PipelineGraph::new(); let styles = graph.animation_styles(); - + assert!(styles.contains("@keyframes pulse")); assert!(styles.contains("@keyframes check")); assert!(styles.contains("@keyframes shake")); @@ -98,16 +112,20 @@ fn test_animation_styles() { fn test_layout_solver() { let solver = LayoutSolver::new(); let mut graph = PipelineGraph::new(); - graph.nodes.insert("Ingested".to_string(), NodeVisualState::Success); - graph.nodes.insert("Validating".to_string(), NodeVisualState::Active); + graph + .nodes + .insert("Ingested".to_string(), NodeVisualState::Success); + graph + .nodes + .insert("Validating".to_string(), NodeVisualState::Active); graph.current_state = "Validating".to_string(); graph.accumulated_confidence = 0.8; let layout = solver.generate_layout(&graph); - + assert!(layout.contains_key("Ingested")); assert!(layout.contains_key("Validating")); - + let (_, active_width) = layout.get("Validating").unwrap(); let (_, ingest_width) = layout.get("Ingested").unwrap(); assert!(*active_width > *ingest_width); @@ -116,10 +134,12 @@ fn test_layout_solver() { /// Test: NodeVisualState from pipeline with issues. #[test] fn test_node_from_pipeline_with_issues() { - let issues = vec![ - Issue::recoverable("test", "warning", IssueSource::TypeCheck), - ]; - + let issues = vec![Issue::recoverable( + "test", + "warning", + IssueSource::TypeCheck, + )]; + let state = NodeVisualState::from_pipeline(PipelineState::Validating, 0.3, &issues); // Low confidence + recoverable issues = Warning assert_eq!(state, NodeVisualState::Warning); @@ -129,7 +149,7 @@ fn test_node_from_pipeline_with_issues() { #[test] fn test_node_unrecoverable() { let issues = vec![Issue::unrecoverable("fatal", "cannot continue")]; - + let state = NodeVisualState::from_pipeline(PipelineState::Validating, 0.9, &issues); assert_eq!(state, NodeVisualState::Error); } @@ -139,11 +159,11 @@ fn test_node_unrecoverable() { fn test_complete_html() { let wf = ledger_ingest(); let mut graph = PipelineGraph::new(); - + for state in &wf.state { graph.nodes.insert(state.id.clone(), NodeVisualState::Idle); } - + graph.current_state = "Classifying".to_string(); graph.accumulated_confidence = 0.78; if let Some(s) = graph.nodes.get_mut("Classifying") { @@ -151,7 +171,7 @@ fn test_complete_html() { } let html = to_html(&graph); - + assert!(html.contains("")); assert!(html.contains("mermaid")); assert!(html.contains("@keyframes")); @@ -164,23 +184,25 @@ fn test_complete_html() { fn test_workflow_to_visualization_roundtrip() { let wf = ledger_ingest(); assert!(wf.validate().is_ok()); - + let mut graph = PipelineGraph::new(); for state in &wf.state { graph.nodes.insert(state.id.clone(), NodeVisualState::Idle); } for trans in &wf.transitions { - graph.edges.push(EdgeVisual::new(&trans.from, &trans.to, &trans.event)); + graph + .edges + .push(EdgeVisual::new(&trans.from, &trans.to, &trans.event)); } - + let mermaid = graph.to_mermaid(); assert!(mermaid.contains("stateDiagram-v2")); - + for state in &wf.state { assert!(mermaid.contains(&state.id), "Missing state: {}", state.id); } - + let solver = LayoutSolver::new(); let layout = solver.generate_layout(&graph); assert!(!layout.is_empty()); -} \ No newline at end of file +} diff --git a/crates/ledgerr-mcp-core/src/provider.rs b/crates/ledgerr-mcp-core/src/provider.rs index 1a940ca..f2e53c9 100644 --- a/crates/ledgerr-mcp-core/src/provider.rs +++ b/crates/ledgerr-mcp-core/src/provider.rs @@ -130,17 +130,20 @@ impl StdioMcpProvider { pub fn new(command: &str, args: &[String]) -> ProviderResult { let transport = StdinTransport::spawn(command, args)?; Ok(Self { - name: format!("mcp-{}", PathBuf::from(command).file_stem().unwrap_or_default().to_string_lossy()), + name: format!( + "mcp-{}", + PathBuf::from(command) + .file_stem() + .unwrap_or_default() + .to_string_lossy() + ), transport, next_id: Arc::new(Mutex::new(1)), }) } fn next_id(&self) -> u64 { - let mut id = self - .next_id - .lock() - .unwrap_or_else(|e| e.into_inner()); + let mut id = self.next_id.lock().unwrap_or_else(|e| e.into_inner()); let curr = *id; *id += 1; curr @@ -263,7 +266,12 @@ impl McpProviderRegistry { descriptors } - pub fn call_tool(&self, provider_name: &str, tool_name: &str, arguments: Value) -> ProviderResult { + pub fn call_tool( + &self, + provider_name: &str, + tool_name: &str, + arguments: Value, + ) -> ProviderResult { for provider in &self.providers { if provider.name() == provider_name { return provider.call_tool(tool_name, arguments); @@ -362,7 +370,10 @@ pub(crate) mod mock { fn call_tool(&self, _name: &str, _arguments: Value) -> ProviderResult { self.call_count.fetch_add(1, Ordering::SeqCst); - self.call_result.as_ref().map_err(|e| e.clone()).and_then(|v| Ok(v.clone())) + self.call_result + .as_ref() + .map_err(|e| e.clone()) + .and_then(|v| Ok(v.clone())) } fn shutdown(&self) {} @@ -371,8 +382,8 @@ pub(crate) mod mock { #[cfg(test)] mod tests { - use super::*; use super::mock::MockProvider; + use super::*; #[test] fn test_mock_provider_initialize_ok() { @@ -395,7 +406,12 @@ mod tests { let provider = MockProvider::new("calc", "add"); let result = provider.call_tool("add", json!({"a": 1, "b": 2})); assert!(result.is_ok()); - assert_eq!(provider.call_count.load(std::sync::atomic::Ordering::SeqCst), 1); + assert_eq!( + provider + .call_count + .load(std::sync::atomic::Ordering::SeqCst), + 1 + ); } #[test] @@ -415,7 +431,8 @@ mod tests { let results = registry.initialize_all(); assert_eq!(results.len(), 2); - let ok_names: Vec<_> = results.iter() + let ok_names: Vec<_> = results + .iter() .filter(|(_, r)| r.is_ok()) .map(|(n, _)| n.as_str()) .collect(); @@ -452,7 +469,10 @@ mod tests { let result = registry.call_tool("", "nonexistent", json!({})); assert!(result.is_err()); - assert!(result.unwrap_err().to_string().contains("no provider found")); + assert!(result + .unwrap_err() + .to_string() + .contains("no provider found")); } #[test] @@ -487,7 +507,11 @@ mod tests { registry.register(Arc::new(MockProvider::new("my-provider", "my-tool"))); let result = registry.call_tool("", "my-tool", json!({})); - assert!(result.is_ok(), "should find tool by name across providers: {:?}", result); + assert!( + result.is_ok(), + "should find tool by name across providers: {:?}", + result + ); } #[test] diff --git a/crates/ledgerr-mcp/src/actor.rs b/crates/ledgerr-mcp/src/actor.rs index 6bede8c..454656f 100644 --- a/crates/ledgerr-mcp/src/actor.rs +++ b/crates/ledgerr-mcp/src/actor.rs @@ -57,7 +57,10 @@ impl ServiceHandle { &self, file_name: String, ) -> Result { - self.send(|reply_tx| GateMessage::ValidateFilename { file_name, reply_tx }) + self.send(|reply_tx| GateMessage::ValidateFilename { + file_name, + reply_tx, + }) } pub fn ingest_statement_rows( @@ -387,9 +390,8 @@ impl ServiceActor { break; } // Catch panics so a single faulty request doesn't kill the entire actor. - let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - self.dispatch(msg) - })); + let result = + std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| self.dispatch(msg))); if let Err(panic) = result { let info = if let Some(s) = panic.downcast_ref::<&str>() { s.to_string() @@ -405,178 +407,176 @@ impl ServiceActor { fn dispatch(&mut self, msg: GateMessage) { match msg { - GateMessage::Shutdown => { /* handled in run() */ } - GateMessage::ListAccounts { reply_tx } => { - let _ = reply_tx.send(self.service.list_accounts()); - } - GateMessage::ListAccountsTool { request, reply_tx } => { - let _ = reply_tx.send(self.service.list_accounts_tool(request)); - } - GateMessage::DocumentInventory { request, reply_tx } => { - let _ = reply_tx.send(self.service.document_inventory(request)); - } - GateMessage::ValidateFilename { - file_name, - reply_tx, - } => { - let _ = reply_tx.send(self.service.validate_source_filename(&file_name)); - } - GateMessage::IngestStatementRows { request, reply_tx } => { - let _ = reply_tx.send(self.service.ingest_statement_rows(request)); - } - GateMessage::IngestPdf { request, reply_tx } => { - let _ = reply_tx.send(self.service.ingest_pdf(request)); - } - GateMessage::GetRawContext { request, reply_tx } => { - let _ = reply_tx.send(self.service.get_raw_context(request)); - } - GateMessage::RunRhaiRule { request, reply_tx } => { - let _ = reply_tx.send(self.service.run_rhai_rule(request)); - } - GateMessage::ClassifyIngested { request, reply_tx } => { - let _ = reply_tx.send(self.service.classify_ingested(request)); - } - GateMessage::QueryFlags { request, reply_tx } => { - let _ = reply_tx.send(self.service.query_flags(request)); - } - GateMessage::ClassifyTransaction { request, reply_tx } => { - let _ = reply_tx.send(self.service.classify_transaction(request)); - } - GateMessage::ReconcileExcelClassification { request, reply_tx } => { - let _ = reply_tx.send(self.service.reconcile_excel_classification(request)); - } - GateMessage::QueryAuditLog { request, reply_tx } => { - let _ = reply_tx.send(self.service.query_audit_log(request)); - } - GateMessage::ExportCpaWorkbook { request, reply_tx } => { - let _ = reply_tx.send(self.service.export_cpa_workbook(request)); - } - GateMessage::GetScheduleSummary { request, reply_tx } => { - let _ = reply_tx.send(self.service.get_schedule_summary(request)); - } - GateMessage::HsmTransition { request, reply_tx } => { - let _ = reply_tx.send(self.service.hsm_transition_tool(request)); - } - GateMessage::HsmStatus { request, reply_tx } => { - let _ = reply_tx.send(self.service.hsm_status_tool(request)); - } - GateMessage::HsmResume { request, reply_tx } => { - let _ = reply_tx.send(self.service.hsm_resume_tool(request)); - } - GateMessage::EventHistory { filter, reply_tx } => { - let _ = reply_tx.send(self.service.event_history(filter)); - } - GateMessage::ReplayLifecycle { request, reply_tx } => { - let _ = reply_tx.send(self.service.replay_lifecycle(request)); - } - GateMessage::TaxAssist { request, reply_tx } => { - let _ = reply_tx.send(self.service.tax_assist_tool(request)); - } - GateMessage::TaxEvidenceChain { request, reply_tx } => { - let _ = reply_tx.send(self.service.tax_evidence_chain_tool(request)); - } - GateMessage::TaxAmbiguityReview { request, reply_tx } => { - let _ = reply_tx.send(self.service.tax_ambiguity_review_tool(request)); - } - GateMessage::ValidateReconciliationStage { request, reply_tx } => { - let _ = reply_tx.send(self.service.validate_reconciliation_stage_tool(request)); - } - GateMessage::ReconcileReconciliationStage { request, reply_tx } => { - let _ = reply_tx.send(self.service.reconcile_reconciliation_stage_tool(request)); - } - GateMessage::CommitReconciliationStage { request, reply_tx } => { - let _ = reply_tx.send(self.service.commit_reconciliation_stage_tool(request)); - } - GateMessage::AdjustTransaction { request, reply_tx } => { - let _ = reply_tx.send(self.service.adjust_transaction(request)); - } - GateMessage::OntologyUpsertEntities { request, reply_tx } => { - let _ = reply_tx.send(self.service.ontology_upsert_entities(request)); - } - GateMessage::OntologyUpsertEdges { request, reply_tx } => { - let _ = reply_tx.send(self.service.ontology_upsert_edges(request)); - } - GateMessage::OntologyQueryPath { request, reply_tx } => { - let _ = reply_tx.send(self.service.ontology_query_path(request)); - } - GateMessage::OntologyExportSnapshot { request, reply_tx } => { - let _ = reply_tx.send(self.service.ontology_export_snapshot(request)); - } - GateMessage::IngestImage { request, reply_tx } => { - let _ = reply_tx.send(self.service.ingest_image_tool(request)); - } - GateMessage::ApplyTags { request, reply_tx } => { - let _ = reply_tx.send(self.service.apply_tags_tool(request)); - } - GateMessage::RemoveTags { request, reply_tx } => { - let _ = reply_tx.send(self.service.remove_tags_tool(request)); - } - GateMessage::ListTagged { request, reply_tx } => { - let _ = reply_tx.send(self.service.list_tagged_tool(request)); - } - GateMessage::SyncFsMetadata { request, reply_tx } => { - let _ = reply_tx.send(self.service.sync_fs_metadata_tool(request)); - } - GateMessage::NormalizeFilename { request, reply_tx } => { - let _ = reply_tx.send(self.service.normalize_filename_tool(request)); - } - #[cfg(feature = "xero")] - GateMessage::XeroGetAuthUrl { reply_tx } => { - let _ = reply_tx.send(self.service.xero_get_auth_url()); - } - #[cfg(feature = "xero")] - GateMessage::XeroExchangeCode { - code, - state, - reply_tx, - } => { - let _ = reply_tx.send(self.service.xero_exchange_code(code, state)); - } - #[cfg(feature = "xero")] - GateMessage::XeroFetchContacts { search, reply_tx } => { - let _ = - reply_tx.send(self.service.xero_fetch_contacts(search.as_deref())); - } - #[cfg(feature = "xero")] - GateMessage::XeroFetchAccounts { reply_tx } => { - let _ = reply_tx.send(self.service.xero_fetch_accounts()); - } - #[cfg(feature = "xero")] - GateMessage::XeroFetchBankAccounts { reply_tx } => { - let _ = reply_tx.send(self.service.xero_fetch_bank_accounts()); - } - #[cfg(feature = "xero")] - GateMessage::XeroFetchInvoices { status, reply_tx } => { - let _ = - reply_tx.send(self.service.xero_fetch_invoices(status.as_deref())); - } - #[cfg(feature = "xero")] - GateMessage::XeroLinkEntity { + GateMessage::Shutdown => { /* handled in run() */ } + GateMessage::ListAccounts { reply_tx } => { + let _ = reply_tx.send(self.service.list_accounts()); + } + GateMessage::ListAccountsTool { request, reply_tx } => { + let _ = reply_tx.send(self.service.list_accounts_tool(request)); + } + GateMessage::DocumentInventory { request, reply_tx } => { + let _ = reply_tx.send(self.service.document_inventory(request)); + } + GateMessage::ValidateFilename { + file_name, + reply_tx, + } => { + let _ = reply_tx.send(self.service.validate_source_filename(&file_name)); + } + GateMessage::IngestStatementRows { request, reply_tx } => { + let _ = reply_tx.send(self.service.ingest_statement_rows(request)); + } + GateMessage::IngestPdf { request, reply_tx } => { + let _ = reply_tx.send(self.service.ingest_pdf(request)); + } + GateMessage::GetRawContext { request, reply_tx } => { + let _ = reply_tx.send(self.service.get_raw_context(request)); + } + GateMessage::RunRhaiRule { request, reply_tx } => { + let _ = reply_tx.send(self.service.run_rhai_rule(request)); + } + GateMessage::ClassifyIngested { request, reply_tx } => { + let _ = reply_tx.send(self.service.classify_ingested(request)); + } + GateMessage::QueryFlags { request, reply_tx } => { + let _ = reply_tx.send(self.service.query_flags(request)); + } + GateMessage::ClassifyTransaction { request, reply_tx } => { + let _ = reply_tx.send(self.service.classify_transaction(request)); + } + GateMessage::ReconcileExcelClassification { request, reply_tx } => { + let _ = reply_tx.send(self.service.reconcile_excel_classification(request)); + } + GateMessage::QueryAuditLog { request, reply_tx } => { + let _ = reply_tx.send(self.service.query_audit_log(request)); + } + GateMessage::ExportCpaWorkbook { request, reply_tx } => { + let _ = reply_tx.send(self.service.export_cpa_workbook(request)); + } + GateMessage::GetScheduleSummary { request, reply_tx } => { + let _ = reply_tx.send(self.service.get_schedule_summary(request)); + } + GateMessage::HsmTransition { request, reply_tx } => { + let _ = reply_tx.send(self.service.hsm_transition_tool(request)); + } + GateMessage::HsmStatus { request, reply_tx } => { + let _ = reply_tx.send(self.service.hsm_status_tool(request)); + } + GateMessage::HsmResume { request, reply_tx } => { + let _ = reply_tx.send(self.service.hsm_resume_tool(request)); + } + GateMessage::EventHistory { filter, reply_tx } => { + let _ = reply_tx.send(self.service.event_history(filter)); + } + GateMessage::ReplayLifecycle { request, reply_tx } => { + let _ = reply_tx.send(self.service.replay_lifecycle(request)); + } + GateMessage::TaxAssist { request, reply_tx } => { + let _ = reply_tx.send(self.service.tax_assist_tool(request)); + } + GateMessage::TaxEvidenceChain { request, reply_tx } => { + let _ = reply_tx.send(self.service.tax_evidence_chain_tool(request)); + } + GateMessage::TaxAmbiguityReview { request, reply_tx } => { + let _ = reply_tx.send(self.service.tax_ambiguity_review_tool(request)); + } + GateMessage::ValidateReconciliationStage { request, reply_tx } => { + let _ = reply_tx.send(self.service.validate_reconciliation_stage_tool(request)); + } + GateMessage::ReconcileReconciliationStage { request, reply_tx } => { + let _ = reply_tx.send(self.service.reconcile_reconciliation_stage_tool(request)); + } + GateMessage::CommitReconciliationStage { request, reply_tx } => { + let _ = reply_tx.send(self.service.commit_reconciliation_stage_tool(request)); + } + GateMessage::AdjustTransaction { request, reply_tx } => { + let _ = reply_tx.send(self.service.adjust_transaction(request)); + } + GateMessage::OntologyUpsertEntities { request, reply_tx } => { + let _ = reply_tx.send(self.service.ontology_upsert_entities(request)); + } + GateMessage::OntologyUpsertEdges { request, reply_tx } => { + let _ = reply_tx.send(self.service.ontology_upsert_edges(request)); + } + GateMessage::OntologyQueryPath { request, reply_tx } => { + let _ = reply_tx.send(self.service.ontology_query_path(request)); + } + GateMessage::OntologyExportSnapshot { request, reply_tx } => { + let _ = reply_tx.send(self.service.ontology_export_snapshot(request)); + } + GateMessage::IngestImage { request, reply_tx } => { + let _ = reply_tx.send(self.service.ingest_image_tool(request)); + } + GateMessage::ApplyTags { request, reply_tx } => { + let _ = reply_tx.send(self.service.apply_tags_tool(request)); + } + GateMessage::RemoveTags { request, reply_tx } => { + let _ = reply_tx.send(self.service.remove_tags_tool(request)); + } + GateMessage::ListTagged { request, reply_tx } => { + let _ = reply_tx.send(self.service.list_tagged_tool(request)); + } + GateMessage::SyncFsMetadata { request, reply_tx } => { + let _ = reply_tx.send(self.service.sync_fs_metadata_tool(request)); + } + GateMessage::NormalizeFilename { request, reply_tx } => { + let _ = reply_tx.send(self.service.normalize_filename_tool(request)); + } + #[cfg(feature = "xero")] + GateMessage::XeroGetAuthUrl { reply_tx } => { + let _ = reply_tx.send(self.service.xero_get_auth_url()); + } + #[cfg(feature = "xero")] + GateMessage::XeroExchangeCode { + code, + state, + reply_tx, + } => { + let _ = reply_tx.send(self.service.xero_exchange_code(code, state)); + } + #[cfg(feature = "xero")] + GateMessage::XeroFetchContacts { search, reply_tx } => { + let _ = reply_tx.send(self.service.xero_fetch_contacts(search.as_deref())); + } + #[cfg(feature = "xero")] + GateMessage::XeroFetchAccounts { reply_tx } => { + let _ = reply_tx.send(self.service.xero_fetch_accounts()); + } + #[cfg(feature = "xero")] + GateMessage::XeroFetchBankAccounts { reply_tx } => { + let _ = reply_tx.send(self.service.xero_fetch_bank_accounts()); + } + #[cfg(feature = "xero")] + GateMessage::XeroFetchInvoices { status, reply_tx } => { + let _ = reply_tx.send(self.service.xero_fetch_invoices(status.as_deref())); + } + #[cfg(feature = "xero")] + GateMessage::XeroLinkEntity { + local_id, + xero_entity_type, + xero_id, + display_name, + ontology_path, + reply_tx, + } => { + let _ = reply_tx.send(self.service.xero_link_entity( local_id, xero_entity_type, xero_id, display_name, ontology_path, - reply_tx, - } => { - let _ = reply_tx.send(self.service.xero_link_entity( - local_id, - xero_entity_type, - xero_id, - display_name, - ontology_path, - )); - } - #[cfg(feature = "xero")] - GateMessage::XeroSyncCatalog { - ontology_path, - reply_tx, - } => { - let _ = reply_tx.send(self.service.xero_sync_catalog(ontology_path)); - } + )); + } + #[cfg(feature = "xero")] + GateMessage::XeroSyncCatalog { + ontology_path, + reply_tx, + } => { + let _ = reply_tx.send(self.service.xero_sync_catalog(ontology_path)); } } } +} pub fn spawn_actor(service: TurboLedgerService) -> ServiceHandle { let (tx, rx) = crossbeam::channel::unbounded::(); @@ -603,29 +603,36 @@ mod tests { #[test] fn service_handle_list_accounts() { - let service = TurboLedgerService::from_manifest_str(&test_manifest()) - .expect("manifest must parse"); + let service = + TurboLedgerService::from_manifest_str(&test_manifest()).expect("manifest must parse"); let handle = spawn_actor(service); let accounts = handle.list_accounts().expect("list_accounts must succeed"); - assert!(accounts.iter().any(|a: &AccountSummary| a.account_id == "WF-BH-CHK")); + assert!(accounts + .iter() + .any(|a: &AccountSummary| a.account_id == "WF-BH-CHK")); } #[test] fn service_handle_list_accounts_tool() { - let service = TurboLedgerService::from_manifest_str(&test_manifest()) - .expect("manifest must parse"); + let service = + TurboLedgerService::from_manifest_str(&test_manifest()).expect("manifest must parse"); let handle = spawn_actor(service); - let response = handle.list_accounts_tool(ListAccountsRequest) + let response = handle + .list_accounts_tool(ListAccountsRequest) .expect("list_accounts_tool must succeed"); - assert!(response.accounts.iter().any(|a| a.account_id == "WF-BH-CHK")); + assert!(response + .accounts + .iter() + .any(|a| a.account_id == "WF-BH-CHK")); } #[test] fn service_handle_validate_filename() { - let service = TurboLedgerService::from_manifest_str(&test_manifest()) - .expect("manifest must parse"); + let service = + TurboLedgerService::from_manifest_str(&test_manifest()).expect("manifest must parse"); let handle = spawn_actor(service); - let result = handle.validate_source_filename("WF--BH-CHK--2023-01--statement.pdf".to_string()); + let result = + handle.validate_source_filename("WF--BH-CHK--2023-01--statement.pdf".to_string()); assert!(result.is_ok()); let parsed = result.unwrap(); assert_eq!(parsed.vendor, "WF"); @@ -635,28 +642,26 @@ mod tests { #[test] fn actor_survives_bad_statement_filename() { - let service = TurboLedgerService::from_manifest_str(&test_manifest()) - .expect("manifest must parse"); + let service = + TurboLedgerService::from_manifest_str(&test_manifest()).expect("manifest must parse"); let handle = spawn_actor(service); let result = handle.validate_source_filename("bad-filename.pdf".to_string()); assert!(result.is_err()); // Actor thread should still be alive for subsequent calls. - let accounts = handle.list_accounts().expect("actor should still be responsive"); + let accounts = handle + .list_accounts() + .expect("actor should still be responsive"); assert!(!accounts.is_empty()); } #[test] fn actor_handles_concurrent_calls() { - let service = TurboLedgerService::from_manifest_str(&test_manifest()) - .expect("manifest must parse"); + let service = + TurboLedgerService::from_manifest_str(&test_manifest()).expect("manifest must parse"); let handle = spawn_actor(service); let handle2 = handle.clone(); - let jh1 = std::thread::spawn(move || { - handle.list_accounts().expect("thread 1") - }); - let jh2 = std::thread::spawn(move || { - handle2.list_accounts().expect("thread 2") - }); + let jh1 = std::thread::spawn(move || handle.list_accounts().expect("thread 1")); + let jh2 = std::thread::spawn(move || handle2.list_accounts().expect("thread 2")); let r1 = jh1.join().expect("thread 1 join"); let r2 = jh2.join().expect("thread 2 join"); assert!(r1.iter().any(|a| a.account_id == "WF-BH-CHK")); diff --git a/crates/ledgerr-mcp/src/bin/ledgerr-mcp-server.rs b/crates/ledgerr-mcp/src/bin/ledgerr-mcp-server.rs index 87fee19..ca09242 100644 --- a/crates/ledgerr-mcp/src/bin/ledgerr-mcp-server.rs +++ b/crates/ledgerr-mcp/src/bin/ledgerr-mcp-server.rs @@ -4,10 +4,10 @@ use std::sync::OnceLock; use ledgerr_mcp::mcp_adapter; use serde_json::{json, Value}; -#[cfg(feature = "b00t")] -use ledgerr_mcp_core::McpProviderRegistry; #[cfg(feature = "b00t")] use ledgerr_mcp::providers::definitions::register_default_providers; +#[cfg(feature = "b00t")] +use ledgerr_mcp_core::McpProviderRegistry; fn main() { // Pre-warm: construct and spawn the service actor on startup so all @@ -29,7 +29,9 @@ fn initialize_providers() { let results = registry.initialize_all(); for (name, result) in &results { match result { - Ok(info) => tracing::info!(provider = %name, tools = info.tools.len(), "external provider registered"), + Ok(info) => { + tracing::info!(provider = %name, tools = info.tools.len(), "external provider registered") + } Err(e) => tracing::warn!(provider = %name, error = %e, "external provider init failed"), } } @@ -38,8 +40,6 @@ fn initialize_providers() { ledgerr_mcp::mcp_adapter::set_global_provider_registry(registry); } - - fn serve(reader: R, mut writer: W) { for line in reader.lines() { let Ok(raw) = line else { continue }; @@ -157,11 +157,19 @@ fn handle_request(request: Value) -> Option { } "l3dg3rr_validate_reconciliation" => { let arguments = params.get("arguments").cloned().unwrap_or(Value::Null); - mcp_adapter::dispatch_reconciliation(global_raw_service(), "validate", &arguments) + mcp_adapter::dispatch_reconciliation( + global_raw_service(), + "validate", + &arguments, + ) } "l3dg3rr_reconcile_postings" => { let arguments = params.get("arguments").cloned().unwrap_or(Value::Null); - mcp_adapter::dispatch_reconciliation(global_raw_service(), "reconcile", &arguments) + mcp_adapter::dispatch_reconciliation( + global_raw_service(), + "reconcile", + &arguments, + ) } "l3dg3rr_commit_guarded" => { let arguments = params.get("arguments").cloned().unwrap_or(Value::Null); @@ -217,7 +225,10 @@ fn handle_request(request: Value) -> Option { } "l3dg3rr_reconcile_excel_classification" => { let arguments = params.get("arguments").cloned().unwrap_or(Value::Null); - mcp_adapter::handle_reconcile_excel_classification(global_raw_service(), &arguments) + mcp_adapter::handle_reconcile_excel_classification( + global_raw_service(), + &arguments, + ) } "l3dg3rr_get_schedule_summary" => { let arguments = params.get("arguments").cloned().unwrap_or(Value::Null); @@ -243,14 +254,16 @@ fn handle_request(request: Value) -> Option { ) } _ => { - #[cfg(feature = "b00t")] { + #[cfg(feature = "b00t")] + { // Registry is accessed internally by handle_external_tool // via mcp_adapter's GLOBAL_PROVIDER_REGISTRY. let ext_args = params.get("arguments").cloned().unwrap_or(Value::Null); let dummy = ledgerr_mcp_core::McpProviderRegistry::new(); mcp_adapter::handle_external_tool(&dummy, tool_name, &ext_args) } - #[cfg(not(feature = "b00t"))] { + #[cfg(not(feature = "b00t"))] + { mcp_adapter::unknown_tool_result(tool_name) } } @@ -266,20 +279,28 @@ fn handle_request(request: Value) -> Option { } /// Build the service, spawn an actor, and leak a raw reference for the adapter path. -fn build_service() -> (&'static ledgerr_mcp::TurboLedgerService, ledgerr_mcp::actor::ServiceHandle) { +fn build_service() -> ( + &'static ledgerr_mcp::TurboLedgerService, + ledgerr_mcp::actor::ServiceHandle, +) { let manifest = std::env::var("LEDGERR_MCP_MANIFEST").unwrap_or_else(|_| { "[session]\nworkbook_path=\"tax-ledger.xlsx\"\nactive_year=2023\n\n[accounts]\nWF-BH-CHK = { institution = \"Wells Fargo\", type = \"checking\", currency = \"USD\" }\n".to_string() }); let service = ledgerr_mcp::TurboLedgerService::from_manifest_str(&manifest) .expect("default manifest must parse"); let handle = service.spawn_actor(); - let raw = Box::new(ledgerr_mcp::TurboLedgerService::from_manifest_str(&manifest) - .expect("default manifest must parse")); + let raw = Box::new( + ledgerr_mcp::TurboLedgerService::from_manifest_str(&manifest) + .expect("default manifest must parse"), + ); let leaked = Box::leak(raw); (leaked, handle) } fn global_raw_service() -> &'static ledgerr_mcp::TurboLedgerService { - static PAIR: OnceLock<(&'static ledgerr_mcp::TurboLedgerService, ledgerr_mcp::actor::ServiceHandle)> = OnceLock::new(); + static PAIR: OnceLock<( + &'static ledgerr_mcp::TurboLedgerService, + ledgerr_mcp::actor::ServiceHandle, + )> = OnceLock::new(); PAIR.get_or_init(|| build_service()).0 } diff --git a/crates/ledgerr-mcp/src/contract.rs b/crates/ledgerr-mcp/src/contract.rs index 79f89c6..972ad54 100644 --- a/crates/ledgerr-mcp/src/contract.rs +++ b/crates/ledgerr-mcp/src/contract.rs @@ -140,10 +140,7 @@ pub const PUBLISHED_TOOLS: [ToolContractSpec; 9] = [ ToolContractSpec { name: EVIDENCE_TOOL, purpose: "evidence traceability: provenance gaps, transaction lineage, review badges", - actions: &[ - "provenance_gaps", - "trace_tx", - ], + actions: &["provenance_gaps", "trace_tx"], }, ]; @@ -596,9 +593,7 @@ pub enum EvidenceArgs { #[serde(rename = "provenance_gaps")] ProvenanceGaps, #[serde(rename = "trace_tx")] - TraceTx { - tx_id: String, - }, + TraceTx { tx_id: String }, } pub fn parse_evidence(arguments: &Value) -> Result { diff --git a/crates/ledgerr-mcp/src/gate.rs b/crates/ledgerr-mcp/src/gate.rs index d24e68f..112f4d1 100644 --- a/crates/ledgerr-mcp/src/gate.rs +++ b/crates/ledgerr-mcp/src/gate.rs @@ -5,21 +5,20 @@ use crossbeam::channel::Sender; use crate::{ ClassifyIngestedRequest, ClassifyIngestedResponse, ClassifyTransactionRequest, ClassifyTransactionResponse, DocumentInventoryRequest, DocumentInventoryResponse, - EventHistoryFilter, EventHistoryResponse, ExportCpaWorkbookRequest, - ExportCpaWorkbookResponse, GetRawContextRequest, GetRawContextResponse, - GetScheduleSummaryRequest, GetScheduleSummaryResponse, HsmResumeRequest, HsmResumeResponse, - HsmStatusRequest, HsmStatusResponse, HsmTransitionRequest, HsmTransitionResponse, - IngestImageRequest, IngestImageResponse, IngestPdfRequest, IngestPdfResponse, - IngestStatementRowsRequest, IngestStatementRowsResponse, NormalizeFilenameRequest, - NormalizeFilenameResponse, OntologyExportSnapshotRequest, OntologyExportSnapshotResponse, - OntologyQueryPathRequest, OntologyQueryPathResponse, OntologyUpsertEdgesRequest, - OntologyUpsertEdgesResponse, OntologyUpsertEntitiesRequest, OntologyUpsertEntitiesResponse, - QueryAuditLogRequest, QueryAuditLogResponse, QueryFlagsRequest, QueryFlagsResponse, - ReconciliationStageRequest, ReconciliationStageResponse, ReplayLifecycleRequest, - ReplayLifecycleResponse, RunRhaiRuleRequest, RunRhaiRuleResponse, SyncFsMetadataRequest, - SyncFsMetadataResponse, TaxAmbiguityReviewRequest, TaxAmbiguityReviewResponse, - TaxAssistRequest, TaxAssistResponse, TaxEvidenceChainRequest, TaxEvidenceChainResponse, - ToolError, + EventHistoryFilter, EventHistoryResponse, ExportCpaWorkbookRequest, ExportCpaWorkbookResponse, + GetRawContextRequest, GetRawContextResponse, GetScheduleSummaryRequest, + GetScheduleSummaryResponse, HsmResumeRequest, HsmResumeResponse, HsmStatusRequest, + HsmStatusResponse, HsmTransitionRequest, HsmTransitionResponse, IngestImageRequest, + IngestImageResponse, IngestPdfRequest, IngestPdfResponse, IngestStatementRowsRequest, + IngestStatementRowsResponse, NormalizeFilenameRequest, NormalizeFilenameResponse, + OntologyExportSnapshotRequest, OntologyExportSnapshotResponse, OntologyQueryPathRequest, + OntologyQueryPathResponse, OntologyUpsertEdgesRequest, OntologyUpsertEdgesResponse, + OntologyUpsertEntitiesRequest, OntologyUpsertEntitiesResponse, QueryAuditLogRequest, + QueryAuditLogResponse, QueryFlagsRequest, QueryFlagsResponse, ReconciliationStageRequest, + ReconciliationStageResponse, ReplayLifecycleRequest, ReplayLifecycleResponse, + RunRhaiRuleRequest, RunRhaiRuleResponse, SyncFsMetadataRequest, SyncFsMetadataResponse, + TaxAmbiguityReviewRequest, TaxAmbiguityReviewResponse, TaxAssistRequest, TaxAssistResponse, + TaxEvidenceChainRequest, TaxEvidenceChainResponse, ToolError, }; use ledger_core::filename::StatementFilename; diff --git a/crates/ledgerr-mcp/src/lib.rs b/crates/ledgerr-mcp/src/lib.rs index 5860cec..ee5afbc 100644 --- a/crates/ledgerr-mcp/src/lib.rs +++ b/crates/ledgerr-mcp/src/lib.rs @@ -23,8 +23,8 @@ use xero_service::XeroService; pub mod actor; pub mod calendar_tool; pub mod contract; -pub mod gate; pub mod events; +pub mod gate; pub mod hsm; pub mod mcp_adapter; pub mod ontology; @@ -1119,13 +1119,9 @@ impl TurboLedgerService { Ok(response) } - fn emit_ingest_evidence( - &self, - row: &TransactionInput, - tx_id: &str, - ) -> Result<(), ToolError> { - use arc_kit_au::EdgeType; + fn emit_ingest_evidence(&self, row: &TransactionInput, tx_id: &str) -> Result<(), ToolError> { use arc_kit_au::node::{ExtractedRow, SourceDoc, Transaction}; + use arc_kit_au::EdgeType; use chrono::Utc; let mut evidence = self @@ -2780,14 +2776,17 @@ impl TurboLedgerService { }) .filter(|r| { request.doc_type.as_deref().is_none_or(|dt| { - format!("{:?}", r.doc_type).to_ascii_lowercase().contains(dt) + format!("{:?}", r.doc_type) + .to_ascii_lowercase() + .contains(dt) }) }) .filter(|r| { // If a directory filter was provided, only include records under that directory. - request.directory.as_deref().is_none_or(|dir| { - std::path::Path::new(&r.file_path).starts_with(dir) - }) + request + .directory + .as_deref() + .is_none_or(|dir| std::path::Path::new(&r.file_path).starts_with(dir)) }) .map(|r| DocumentSummary { doc_id: r.doc_id.clone(), @@ -2943,10 +2942,7 @@ impl TurboLedgerService { }; let path = resolved_path.as_path(); - let ext = path - .extension() - .and_then(|e| e.to_str()) - .unwrap_or("pdf"); + let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("pdf"); let original_name = path .file_name() .and_then(|n| n.to_str()) diff --git a/crates/ledgerr-mcp/src/mcp_adapter.rs b/crates/ledgerr-mcp/src/mcp_adapter.rs index e74b0a6..9d7ebfe 100644 --- a/crates/ledgerr-mcp/src/mcp_adapter.rs +++ b/crates/ledgerr-mcp/src/mcp_adapter.rs @@ -23,8 +23,8 @@ use serde_json::{json, Value}; use crate::{ contract::{ - self, AuditArgs, DocumentsArgs, EvidenceArgs, OntologyArgs, ReconciliationArgs, - ReviewArgs, TaxArgs, WorkflowArgs, + self, AuditArgs, DocumentsArgs, EvidenceArgs, OntologyArgs, ReconciliationArgs, ReviewArgs, + TaxArgs, WorkflowArgs, }, ClassifyIngestedRequest, ClassifyTransactionRequest, DocumentInventoryRequest, DocumentQueueStatusRequest, EventHistoryFilter, ExportCpaWorkbookRequest, FlagStatusRequest, @@ -58,9 +58,7 @@ fn external_tool_descriptors() -> Vec { registry .all_tool_descriptors() .into_iter() - .map(|td: ToolDescriptor| { - json!({ "name": td.name, "inputSchema": td.input_schema }) - }) + .map(|td: ToolDescriptor| json!({ "name": td.name, "inputSchema": td.input_schema })) .collect() } @@ -144,11 +142,7 @@ pub fn handle_external_tool( } #[cfg(not(feature = "b00t"))] -pub fn handle_external_tool( - _registry: (), - tool_name: &str, - _arguments: &Value, -) -> Value { +pub fn handle_external_tool(_registry: (), tool_name: &str, _arguments: &Value) -> Value { unknown_tool_result(tool_name) } @@ -2362,7 +2356,11 @@ pub fn handle_evidence_tool(service: &TurboLedgerService, arguments: &Value) -> use arc_kit_au::ProvenanceScanner; let evidence = match service.evidence.lock() { Ok(e) => e, - Err(_) => return error_envelope(&ToolError::Internal("evidence mutex poisoned".to_string())), + Err(_) => { + return error_envelope(&ToolError::Internal( + "evidence mutex poisoned".to_string(), + )) + } }; let gaps = evidence.find_missing_provenance(); let gap_jsons: Vec<_> = gaps @@ -2392,7 +2390,11 @@ pub fn handle_evidence_tool(service: &TurboLedgerService, arguments: &Value) -> use arc_kit_au::EvidenceTracer; let evidence = match service.evidence.lock() { Ok(e) => e, - Err(_) => return error_envelope(&ToolError::Internal("evidence mutex poisoned".to_string())), + Err(_) => { + return error_envelope(&ToolError::Internal( + "evidence mutex poisoned".to_string(), + )) + } }; match evidence.trace_transaction(&tx_id) { Some(chain) => { diff --git a/crates/ledgerr-mcp/src/providers/definitions.rs b/crates/ledgerr-mcp/src/providers/definitions.rs index d42fb17..ffad674 100644 --- a/crates/ledgerr-mcp/src/providers/definitions.rs +++ b/crates/ledgerr-mcp/src/providers/definitions.rs @@ -15,9 +15,7 @@ impl B00tProvider { let home = b00t_home.unwrap_or_else(|| { let p = PathBuf::from( std::env::var("B00T_HOME") - .or_else(|_| { - std::env::var("HOME").map(|h| format!("{h}/.b00t")) - }) + .or_else(|_| std::env::var("HOME").map(|h| format!("{h}/.b00t"))) .unwrap_or_else(|_| "~/.b00t".to_string()), ); if p.starts_with("~") { @@ -196,8 +194,7 @@ mod tests { Ok(p) => assert_eq!(p.name(), "just"), Err(e) => { assert!( - e.to_string().contains("spawn failed") - || e.to_string().contains("not found"), + e.to_string().contains("spawn failed") || e.to_string().contains("not found"), "unexpected error: {e}" ); } @@ -248,10 +245,7 @@ mod tests { if let Err(e) = result { // Expected: any error indicating graceful degradation let msg = e.to_string(); - assert!( - !msg.is_empty(), - "unexpected empty error for {name}: {e}" - ); + assert!(!msg.is_empty(), "unexpected empty error for {name}: {e}"); } } } diff --git a/crates/ledgerr-mcp/src/xero_service.rs b/crates/ledgerr-mcp/src/xero_service.rs index 6cb6a22..4f44210 100644 --- a/crates/ledgerr-mcp/src/xero_service.rs +++ b/crates/ledgerr-mcp/src/xero_service.rs @@ -71,9 +71,9 @@ impl XeroService { .client .lock() .map_err(|_| ToolError::Internal("lock poisoned".into()))?; - let client = guard - .as_mut() - .ok_or_else(|| ToolError::Internal("No pending auth flow; call get_auth_url first".into()))?; + let client = guard.as_mut().ok_or_else(|| { + ToolError::Internal("No pending auth flow; call get_auth_url first".into()) + })?; let tenant = client .exchange_code(code, state) .map_err(|e| ToolError::Internal(e.to_string()))?; diff --git a/crates/ledgerr-xero/src/auth.rs b/crates/ledgerr-xero/src/auth.rs index 418b9ad..a18779f 100644 --- a/crates/ledgerr-xero/src/auth.rs +++ b/crates/ledgerr-xero/src/auth.rs @@ -148,8 +148,7 @@ impl XeroAuth { let expires_in = resp.expires_in.unwrap_or(1800); let expires_at = Utc::now() + Duration::seconds(expires_in as i64); - let (tenant_id, tenant_name) = - fetch_tenant_blocking(&resp.access_token, &http)?; + let (tenant_id, tenant_name) = fetch_tenant_blocking(&resp.access_token, &http)?; let tokens = XeroTokens { access_token: resp.access_token, diff --git a/crates/mdbook-rhai-mermaid/src/emitter.rs b/crates/mdbook-rhai-mermaid/src/emitter.rs index c3bd570..4729751 100644 --- a/crates/mdbook-rhai-mermaid/src/emitter.rs +++ b/crates/mdbook-rhai-mermaid/src/emitter.rs @@ -33,7 +33,12 @@ pub fn emit_mermaid(graph: &Graph) -> String { None => None, }; let line = match lbl { - Some(l) => format!(" {} -->|\"{}\"|{}\n", edge.from, escape_label(&l), edge.to), + Some(l) => format!( + " {} -->|\"{}\"|{}\n", + edge.from, + escape_label(&l), + edge.to + ), None => format!(" {} --> {}\n", edge.from, edge.to), }; out.push_str(&line); @@ -78,7 +83,10 @@ mod tests { let src = "if confidence > 0.5 -> reconcile\nif confidence > 0.8 -> commit\n"; let graph = parse(src); let out = emit_mermaid(&graph); - assert!(out.contains("|\"false\"|"), "expected false chain edge in output"); + assert!( + out.contains("|\"false\"|"), + "expected false chain edge in output" + ); assert!(out.contains("|\"true\"|"), "expected true edge in output"); } diff --git a/crates/mdbook-rhai-mermaid/src/main.rs b/crates/mdbook-rhai-mermaid/src/main.rs index 6b59173..28ff21b 100644 --- a/crates/mdbook-rhai-mermaid/src/main.rs +++ b/crates/mdbook-rhai-mermaid/src/main.rs @@ -62,7 +62,11 @@ fn main() { /// Walk the entire book value, mutating chapter content in-place. fn process_book(mut book: Value) -> Value { // mdbook 0.5.x serializes Book with key "items"; older versions used "sections" - let key = if book.get("items").is_some() { "items" } else { "sections" }; + let key = if book.get("items").is_some() { + "items" + } else { + "sections" + }; process_sections(book.get_mut(key)); book } @@ -253,12 +257,21 @@ mod tests { let content = "# Title\n\n```rhai\nfn ingest() -> classify\n```\n\nSome text.\n"; let result = inject_mermaid_blocks(content); assert!(result.contains("```rhai\n"), "should preserve rhai block"); - assert!(result.contains("```mermaid\n"), "should inject mermaid block"); - assert!(result.contains("flowchart TD\n"), "mermaid should be a flowchart"); + assert!( + result.contains("```mermaid\n"), + "should inject mermaid block" + ); + assert!( + result.contains("flowchart TD\n"), + "mermaid should be a flowchart" + ); // Mermaid block must come after the rhai block. let rhai_pos = result.find("```rhai").unwrap(); let mermaid_pos = result.find("```mermaid").unwrap(); - assert!(mermaid_pos > rhai_pos, "mermaid block should be after rhai block"); + assert!( + mermaid_pos > rhai_pos, + "mermaid block should be after rhai block" + ); } #[test] @@ -282,8 +295,7 @@ mod tests { #[test] fn test_inject_multiple_blocks() { - let content = - "```rhai\nfn a() -> b\n```\n\nMiddle.\n\n```rhai\nfn c() -> d\n```\n"; + let content = "```rhai\nfn a() -> b\n```\n\nMiddle.\n\n```rhai\nfn c() -> d\n```\n"; let result = inject_mermaid_blocks(content); let count = result.matches("```mermaid").count(); assert_eq!(count, 2, "should inject one mermaid block per rhai block"); @@ -301,7 +313,10 @@ mod tests { let content = "```rhai\n// only a comment\n```\n"; let result = inject_mermaid_blocks(content); // Graph is empty — no mermaid block should be injected. - assert!(!result.contains("```mermaid"), "empty graph should not inject mermaid"); + assert!( + !result.contains("```mermaid"), + "empty graph should not inject mermaid" + ); } #[test] diff --git a/crates/mdbook-rhai-mermaid/src/parser.rs b/crates/mdbook-rhai-mermaid/src/parser.rs index d51c697..3d0120b 100644 --- a/crates/mdbook-rhai-mermaid/src/parser.rs +++ b/crates/mdbook-rhai-mermaid/src/parser.rs @@ -1,3 +1,4 @@ +use indexmap::IndexMap; /// Graph-based AST for the rhai pseudo-DSL. /// /// Two statement forms are supported: @@ -8,7 +9,6 @@ /// The syntax is stable — richer identity and placement semantics are encoded /// in the parser output, not in a second incompatible syntax. use std::collections::HashMap; -use indexmap::IndexMap; // --------------------------------------------------------------------------- // Public types @@ -42,33 +42,53 @@ impl SemanticRole { return SemanticRole::Decision; } let lower = label.to_lowercase(); - if lower.contains("ingest") || lower.contains("load") || lower.contains("parse") - || lower.contains("extract") || lower.contains("source") || lower.contains("input") + if lower.contains("ingest") + || lower.contains("load") + || lower.contains("parse") + || lower.contains("extract") + || lower.contains("source") + || lower.contains("input") { return SemanticRole::Ingest; } - if lower.contains("validate") || lower.contains("verify") || lower.contains("check") - || lower.contains("guard") || lower.contains("audit") || lower.contains("rule") + if lower.contains("validate") + || lower.contains("verify") + || lower.contains("check") + || lower.contains("guard") + || lower.contains("audit") + || lower.contains("rule") { return SemanticRole::Validate; } - if lower.contains("classify") || lower.contains("label") || lower.contains("tag") - || lower.contains("map") || lower.contains("route") + if lower.contains("classify") + || lower.contains("label") + || lower.contains("tag") + || lower.contains("map") + || lower.contains("route") { return SemanticRole::Classify; } - if lower.contains("review") || lower.contains("approve") || lower.contains("manual") - || lower.contains("operator") || lower.contains("human") + if lower.contains("review") + || lower.contains("approve") + || lower.contains("manual") + || lower.contains("operator") + || lower.contains("human") { return SemanticRole::Review; } - if lower.contains("reconcile") || lower.contains("match") || lower.contains("balance") + if lower.contains("reconcile") + || lower.contains("match") + || lower.contains("balance") || lower.contains("ledger") { return SemanticRole::Reconcile; } - if lower.contains("commit") || lower.contains("publish") || lower.contains("export") - || lower.contains("write") || lower.contains("persist") || lower.contains("done") + if lower.contains("commit") + || lower.contains("publish") + || lower.contains("export") + || lower.contains("write") + || lower.contains("persist") + || lower.contains("done") || lower.contains("finish") { return SemanticRole::Commit; @@ -130,15 +150,18 @@ impl Graph { if !self.nodes.contains_key(&id) { self.order.push(id.clone()); let role = SemanticRole::infer(&label, &kind); - self.nodes.insert(id.clone(), Node { - id: id.clone(), - identity_key: id, - label, - kind, - role, - arm_index: None, - is_default: false, - }); + self.nodes.insert( + id.clone(), + Node { + id: id.clone(), + identity_key: id, + label, + kind, + role, + arm_index: None, + is_default: false, + }, + ); } } @@ -154,20 +177,29 @@ impl Graph { if !self.nodes.contains_key(&id) { self.order.push(id.clone()); let role = SemanticRole::infer(&label, &kind); - self.nodes.insert(id.clone(), Node { - id, - identity_key, - label, - kind, - role, - arm_index, - is_default, - }); + self.nodes.insert( + id.clone(), + Node { + id, + identity_key, + label, + kind, + role, + arm_index, + is_default, + }, + ); } } pub fn add_edge(&mut self, from: String, to: String, label: Option) { - self.edges.push(Edge { from, to, label, arm_index: None, is_default: false }); + self.edges.push(Edge { + from, + to, + label, + arm_index: None, + is_default: false, + }); } pub fn add_edge_rich( @@ -178,7 +210,13 @@ impl Graph { arm_index: Option, is_default: bool, ) { - self.edges.push(Edge { from, to, label, arm_index, is_default }); + self.edges.push(Edge { + from, + to, + label, + arm_index, + is_default, + }); } } @@ -188,7 +226,13 @@ impl Graph { pub fn sanitize_id(raw: &str) -> String { raw.chars() - .map(|c| if c.is_alphanumeric() || c == '_' { c } else { '_' }) + .map(|c| { + if c.is_alphanumeric() || c == '_' { + c + } else { + '_' + } + }) .collect() } @@ -503,16 +547,25 @@ mod tests { if confidence > 0.8 -> commit "#; let g = parse(src); - let decision_nodes: Vec<&Node> = - g.nodes.values().filter(|n| n.kind == NodeKind::Decision).collect(); + let decision_nodes: Vec<&Node> = g + .nodes + .values() + .filter(|n| n.kind == NodeKind::Decision) + .collect(); assert_eq!(decision_nodes.len(), 2); let false_edge = g.edges.iter().find(|e| e.label.as_deref() == Some("false")); assert!(false_edge.is_some(), "expected a false-chain edge"); let fe = false_edge.unwrap(); - assert!(fe.from.contains("0_8"), "false edge from should reference 0.8 threshold"); - assert!(fe.to.contains("0_5"), "false edge to should reference 0.5 threshold"); + assert!( + fe.from.contains("0_8"), + "false edge from should reference 0.8 threshold" + ); + assert!( + fe.to.contains("0_5"), + "false edge to should reference 0.5 threshold" + ); } #[test] @@ -576,12 +629,22 @@ mod tests { "#; let g = parse(src); - let match_nodes: Vec<&Node> = g.nodes.values().filter(|n| n.kind == NodeKind::Match).collect(); + let match_nodes: Vec<&Node> = g + .nodes + .values() + .filter(|n| n.kind == NodeKind::Match) + .collect(); assert_eq!(match_nodes.len(), 1); assert_eq!(match_nodes[0].label, "match result.disposition"); assert_eq!(g.edges.len(), 3); - assert_eq!(g.edges[0].label.as_deref(), Some("Disposition::Unrecoverable")); - assert_eq!(g.edges[1].label.as_deref(), Some("Disposition::Recoverable")); + assert_eq!( + g.edges[0].label.as_deref(), + Some("Disposition::Unrecoverable") + ); + assert_eq!( + g.edges[1].label.as_deref(), + Some("Disposition::Recoverable") + ); assert_eq!(g.edges[2].label.as_deref(), Some("Disposition::Advisory")); } @@ -613,7 +676,10 @@ mod tests { assert_eq!(g.nodes["ingest_pdf"].role, SemanticRole::Ingest); assert_eq!(g.nodes["validate_rows"].role, SemanticRole::Validate); - assert_eq!(g.nodes["classify_transactions"].role, SemanticRole::Classify); + assert_eq!( + g.nodes["classify_transactions"].role, + SemanticRole::Classify + ); assert_eq!(g.nodes["reconcile_xero"].role, SemanticRole::Reconcile); assert_eq!(g.nodes["review_flags"].role, SemanticRole::Review); assert_eq!(g.nodes["commit_workbook"].role, SemanticRole::Commit); diff --git a/xtask/src/viz_manifest.rs b/xtask/src/viz_manifest.rs index 584a178..65c7cee 100644 --- a/xtask/src/viz_manifest.rs +++ b/xtask/src/viz_manifest.rs @@ -6,7 +6,9 @@ use std::path::Path; use ledger_core::{ - constraints::{ConstraintEvaluation, InvoiceConstraintSolver, InvoiceVerification, VendorConstraintSet}, + constraints::{ + ConstraintEvaluation, InvoiceConstraintSolver, InvoiceVerification, VendorConstraintSet, + }, iso::{HasVisualization, VizManifest, VizManifestEntry}, legal::{Jurisdiction, LegalRule, LegalSolver, TransactionFacts, Z3Result}, pipeline::{ @@ -42,62 +44,23 @@ pub fn export_viz_manifest(output: &Path) -> Result<(), Box", PipelineState::::viz_spec(), ), - VizManifestEntry::new( - "ConstraintEvaluation", - ConstraintEvaluation::viz_spec(), - ), - VizManifestEntry::new( - "VendorConstraintSet", - VendorConstraintSet::viz_spec(), - ), + VizManifestEntry::new("ConstraintEvaluation", ConstraintEvaluation::viz_spec()), + VizManifestEntry::new("VendorConstraintSet", VendorConstraintSet::viz_spec()), VizManifestEntry::new( "InvoiceConstraintSolver", InvoiceConstraintSolver::viz_spec(), ), - VizManifestEntry::new( - "InvoiceVerification", - InvoiceVerification::viz_spec(), - ), - VizManifestEntry::new( - "Z3Result", - Z3Result::viz_spec(), - ), - VizManifestEntry::new( - "LegalRule", - LegalRule::viz_spec(), - ), - VizManifestEntry::new( - "LegalSolver", - LegalSolver::viz_spec(), - ), - VizManifestEntry::new( - "Jurisdiction", - Jurisdiction::viz_spec(), - ), - VizManifestEntry::new( - "TransactionFacts", - TransactionFacts::viz_spec(), - ), - VizManifestEntry::new( - "CommitGate", - CommitGate::viz_spec(), - ), - VizManifestEntry::new( - "Issue", - Issue::viz_spec(), - ), - VizManifestEntry::new( - "MetaFlag", - MetaFlag::viz_spec(), - ), - VizManifestEntry::new( - "StageResult<()>", - StageResult::<()>::viz_spec(), - ), - VizManifestEntry::new( - "KasuariSolver", - KasuariSolver::viz_spec(), - ), + VizManifestEntry::new("InvoiceVerification", InvoiceVerification::viz_spec()), + VizManifestEntry::new("Z3Result", Z3Result::viz_spec()), + VizManifestEntry::new("LegalRule", LegalRule::viz_spec()), + VizManifestEntry::new("LegalSolver", LegalSolver::viz_spec()), + VizManifestEntry::new("Jurisdiction", Jurisdiction::viz_spec()), + VizManifestEntry::new("TransactionFacts", TransactionFacts::viz_spec()), + VizManifestEntry::new("CommitGate", CommitGate::viz_spec()), + VizManifestEntry::new("Issue", Issue::viz_spec()), + VizManifestEntry::new("MetaFlag", MetaFlag::viz_spec()), + VizManifestEntry::new("StageResult<()>", StageResult::<()>::viz_spec()), + VizManifestEntry::new("KasuariSolver", KasuariSolver::viz_spec()), ]; let count = objects.len(); From 5a443398176f59383a4bcaebdfec6a5bb7f1807c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 3 May 2026 13:52:51 +0000 Subject: [PATCH 2/2] fix: apply review feedback - doc comment, workbook headers, verify test - parser.rs: convert outer doc comment to inner module doc (//!) and move before use statements - ledger_ops.rs: fix write_sheet to use correct headers matching TxProjectionRow fields (tx_id, account_id, date, amount, description, source_ref) and write all field values - verify.rs: make test_verification_rejected_low_confidence actually invoke verifier.verify() and assert !is_approved() Agent-Logs-Url: https://github.com/PromptExecution/l3dg3rr/sessions/b1a08908-ae43-4870-9678-d6b9ef09c136 Co-authored-by: elasticdotventures <35611074+elasticdotventures@users.noreply.github.com> --- crates/ledger-core/src/ledger_ops.rs | 20 +++++++++++++++++--- crates/ledger-core/src/verify.rs | 10 ++++------ crates/mdbook-rhai-mermaid/src/parser.rs | 19 ++++++++++--------- 3 files changed, 31 insertions(+), 18 deletions(-) diff --git a/crates/ledger-core/src/ledger_ops.rs b/crates/ledger-core/src/ledger_ops.rs index ce1e210..c0d061b 100644 --- a/crates/ledger-core/src/ledger_ops.rs +++ b/crates/ledger-core/src/ledger_ops.rs @@ -456,15 +456,29 @@ impl LedgerOperation for ExportWorkbookOp { .map_err(|e| LedgerOpError::Workbook(e.to_string()))?; ws.write_string(0, 0, "tx_id") .map_err(|e| LedgerOpError::Workbook(e.to_string()))?; - ws.write_string(0, 1, "category") + ws.write_string(0, 1, "account_id") .map_err(|e| LedgerOpError::Workbook(e.to_string()))?; - ws.write_string(0, 2, "reason") + ws.write_string(0, 2, "date") + .map_err(|e| LedgerOpError::Workbook(e.to_string()))?; + ws.write_string(0, 3, "amount") + .map_err(|e| LedgerOpError::Workbook(e.to_string()))?; + ws.write_string(0, 4, "description") + .map_err(|e| LedgerOpError::Workbook(e.to_string()))?; + ws.write_string(0, 5, "source_ref") .map_err(|e| LedgerOpError::Workbook(e.to_string()))?; for (idx, row) in rows.iter().enumerate() { let r = (idx + 1) as u32; ws.write_string(r, 0, &row.tx_id) .map_err(|e| LedgerOpError::Workbook(e.to_string()))?; - ws.write_string(r, 2, &row.source_ref) + ws.write_string(r, 1, &row.account_id) + .map_err(|e| LedgerOpError::Workbook(e.to_string()))?; + ws.write_string(r, 2, &row.date) + .map_err(|e| LedgerOpError::Workbook(e.to_string()))?; + ws.write_string(r, 3, &row.amount) + .map_err(|e| LedgerOpError::Workbook(e.to_string()))?; + ws.write_string(r, 4, &row.description) + .map_err(|e| LedgerOpError::Workbook(e.to_string()))?; + ws.write_string(r, 5, &row.source_ref) .map_err(|e| LedgerOpError::Workbook(e.to_string()))?; } Ok(()) diff --git a/crates/ledger-core/src/verify.rs b/crates/ledger-core/src/verify.rs index 7142a99..55fdcd4 100644 --- a/crates/ledger-core/src/verify.rs +++ b/crates/ledger-core/src/verify.rs @@ -247,20 +247,18 @@ mod tests { fn test_verification_rejected_low_confidence() { let proposer_json = r#"{"rule_id":"test","proposed_fix":"x","reasoning":"y","confidence":0.5}"#; + // Reviewer confidence 0.6 is below the 0.80 threshold → must force rejection let reviewer_json = r#"{"approved":false,"concerns":["too risky"],"suggestions":[],"confidence":0.6}"#; let proposer = MockModelClient::default().with_response(proposer_json); let reviewer = MockModelClient::default().with_response(reviewer_json); - // Use higher threshold to force rejection let config = MultiModelConfig::default().with_threshold(0.80); - let _verifier = MultiModelVerifier::new(proposer, reviewer, config); + let verifier = MultiModelVerifier::new(proposer, reviewer, config); - // Override with mocked threshold test - manually check - // In real code, reviewer_json confidence 0.6 < 0.80 threshold - // This test shows the logic path - assert!(true); // Placeholder - confidence check happens in review() + let outcome = verifier.verify("test", "[]", "context").unwrap(); + assert!(!outcome.is_approved()); } #[test] diff --git a/crates/mdbook-rhai-mermaid/src/parser.rs b/crates/mdbook-rhai-mermaid/src/parser.rs index 3d0120b..c3abbd7 100644 --- a/crates/mdbook-rhai-mermaid/src/parser.rs +++ b/crates/mdbook-rhai-mermaid/src/parser.rs @@ -1,13 +1,14 @@ +//! Graph-based AST for the rhai pseudo-DSL. +//! +//! Two statement forms are supported: +//! - Pipeline step: `fn name() -> target` +//! - Conditional: `if expr -> target` where expr is e.g. `confidence > 0.8` +//! - Match arm: `match expr => Arm -> target` +//! +//! The syntax is stable — richer identity and placement semantics are encoded +//! in the parser output, not in a second incompatible syntax. + use indexmap::IndexMap; -/// Graph-based AST for the rhai pseudo-DSL. -/// -/// Two statement forms are supported: -/// - Pipeline step: `fn name() -> target` -/// - Conditional: `if expr -> target` where expr is e.g. `confidence > 0.8` -/// - Match arm: `match expr => Arm -> target` -/// -/// The syntax is stable — richer identity and placement semantics are encoded -/// in the parser output, not in a second incompatible syntax. use std::collections::HashMap; // ---------------------------------------------------------------------------