Introduction
l3dg3rr is designed for US expats who need to reconcile complex financial histories across multiple jurisdictions (US, AU, UK) without compromising privacy.
l3dg3rr is a local-first financial document intelligence system
for retroactive U.S. expat tax preparation. It ingests raw statements, classifies transactions with editable rules, verifies hard constraints, and exports an accountant-usable Excel workbook with audit history.
The system is built around an operator/agent workflow: agents do ingestion, classification, reconciliation, flagging, and evidence gathering; the human operator and CPA keep approval authority through Excel, notifications, and auditable review surfaces.
Core Functional Shape
fn source_documents() -> document_ingestion
fn document_ingestion() -> validation
fn validation() -> classification
fn classification() -> legal_verification
fn legal_verification() -> reconciliation
fn reconciliation() -> workbook_export
fn workbook_export() -> cpa_review
fn cpa_review() -> audit_history
flowchart TD
source_documents["source_documents"]
document_ingestion["document_ingestion"]
validation["validation"]
classification["classification"]
legal_verification["legal_verification"]
reconciliation["reconciliation"]
workbook_export["workbook_export"]
cpa_review["cpa_review"]
audit_history["audit_history"]
source_documents --> document_ingestion
document_ingestion --> validation
validation --> classification
classification --> legal_verification
legal_verification --> reconciliation
reconciliation --> workbook_export
workbook_export --> cpa_review
cpa_review --> audit_history
Product Guarantees
- Local-first operation: private financial data does not require third-party SaaS processing.
- Excel-first audit layer: the workbook is the CPA-facing review and signoff artifact.
- Deterministic identity: transaction IDs are content hashes, not random UUIDs.
- Decimal money semantics: currency values stay in
rust_decimal::Decimalin financial paths. - Agent-visible but operator-governed tools: MCP exposes capability families while l3dg3rr owns policy, audit, approvals, and credentials.
How To Read This Book
Use Capability Map for the current implementation state. Then read the operator capability chapters for what the application does, followed by the application structure chapters for how it behaves internally.
The visualization chapters document the live mdBook diagram system. They are important, but they are no longer the top-level architecture of the whole application.
Primary Surfaces
| Surface | Audience | Purpose |
|---|---|---|
| Excel workbook | CPA/operator | Review, correction, schedule summaries, audit signoff |
| MCP tools | agents | Controlled capability execution through ledgerr_* tool families |
| Sidecar state | host/service | Restart recovery, replay, idempotency cache, lifecycle state |
| Desktop host | operator | approvals, notifications, credentials, process supervision |
| mdBook docs | developers/operators | executable diagrams and technical behavior reference |
Related Chapters
All processing runs on your machine — no private financial data leaves the host.
PDF ingestion rewrites the workbook in-place. Back up tax-ledger.xlsx before running a full re-ingest.
Export the workbook after every classification pass so your accountant always has the latest reconciled state.
Capability Map
This chapter provides a complete status map of the l3dg3rr system — every major capability, its current implementation state, and what is needed to complete it.
System Architecture Diagram
fn filename_routing() -> blake3_ids
fn filename_routing() -> docling_bridge
fn docling_bridge() -> document_graph
fn document_graph() -> reqif_candidates
fn reqif_candidates() -> rule_registry
fn rule_files() -> rule_registry
fn rule_registry() -> keyword_selector
fn rule_registry() -> semantic_selector
fn keyword_selector() -> classify_waterfall
fn semantic_selector() -> classify_waterfall
fn classify_waterfall() -> classification_engine
fn classification_engine() -> review_flags
fn classification_engine() -> legal_solver
fn legal_solver() -> workbook_output
fn review_flags() -> workbook_output
fn workbook_output() -> audit_trail
fn audit_trail() -> sidecar_state
fn workflow_toml() -> mermaid_generation
fn pipeline_hsm() -> agent_runtime
fn issue_source() -> agent_runtime
fn reqif_opa_bridge() -> agent_runtime
fn xero_catalog() -> reconciliation_candidates
fn ledgerr_mcp_contract() -> agent_runtime
flowchart TD
filename_routing["filename_routing"]
blake3_ids["blake3_ids"]
docling_bridge["docling_bridge"]
document_graph["document_graph"]
reqif_candidates["reqif_candidates"]
rule_registry["rule_registry"]
rule_files["rule_files"]
keyword_selector["keyword_selector"]
semantic_selector["semantic_selector"]
classify_waterfall["classify_waterfall"]
classification_engine["classification_engine"]
review_flags["review_flags"]
legal_solver["legal_solver"]
workbook_output["workbook_output"]
audit_trail["audit_trail"]
sidecar_state["sidecar_state"]
workflow_toml["workflow_toml"]
mermaid_generation["mermaid_generation"]
pipeline_hsm["pipeline_hsm"]
agent_runtime["agent_runtime"]
issue_source["issue_source"]
reqif_opa_bridge["reqif_opa_bridge"]
xero_catalog["xero_catalog"]
reconciliation_candidates["reconciliation_candidates"]
ledgerr_mcp_contract["ledgerr_mcp_contract"]
filename_routing --> blake3_ids
filename_routing --> docling_bridge
docling_bridge --> document_graph
document_graph --> reqif_candidates
reqif_candidates --> rule_registry
rule_files --> rule_registry
rule_registry --> keyword_selector
rule_registry --> semantic_selector
keyword_selector --> classify_waterfall
semantic_selector --> classify_waterfall
classify_waterfall --> classification_engine
classification_engine --> review_flags
classification_engine --> legal_solver
legal_solver --> workbook_output
review_flags --> workbook_output
workbook_output --> audit_trail
audit_trail --> sidecar_state
workflow_toml --> mermaid_generation
pipeline_hsm --> agent_runtime
issue_source --> agent_runtime
reqif_opa_bridge --> agent_runtime
xero_catalog --> reconciliation_candidates
ledgerr_mcp_contract --> agent_runtime
PRD-4 Phase 1: Canonical Ontology Core
The first PRD-4 implementation slice makes ledger-core the canonical owner of
ontology artifact and relation primitives while preserving the legacy MCP storage
shape for compatibility.
fn ledger_core_types() -> artifact_kind
fn ledger_core_types() -> relation_kind
fn artifact_kind() -> ontology_snapshot
fn relation_kind() -> ontology_snapshot
fn ontology_snapshot() -> mcp_transport_adapter
fn ontology_snapshot() -> visual_audit_graph
flowchart TD
ledger_core_types["ledger_core_types"]
artifact_kind["artifact_kind"]
relation_kind["relation_kind"]
ontology_snapshot["ontology_snapshot"]
mcp_transport_adapter["mcp_transport_adapter"]
visual_audit_graph["visual_audit_graph"]
ledger_core_types --> artifact_kind
ledger_core_types --> relation_kind
artifact_kind --> ontology_snapshot
relation_kind --> ontology_snapshot
ontology_snapshot --> mcp_transport_adapter
ontology_snapshot --> visual_audit_graph
PRD-4 Phase 2: Automatic Artifact Relationship Emission
The second PRD-4 implementation slice makes ontology facts emerge from normal pipeline work: ingest, classification, validation, workbook projection, audit events, and integration links emit typed artifact relationships instead of requiring manual ontology upserts.
fn ingest_pdf() -> document_artifact
fn ingest_pdf() -> raw_context_artifact
fn document_artifact() -> extracted_row_artifact
fn extracted_row_artifact() -> transaction_artifact
fn transaction_artifact() -> classification_artifact
fn classification_artifact() -> validation_artifact
fn validation_artifact() -> workbook_row_artifact
fn workbook_row_artifact() -> audit_event_artifact
flowchart TD
ingest_pdf["ingest_pdf"]
document_artifact["document_artifact"]
raw_context_artifact["raw_context_artifact"]
extracted_row_artifact["extracted_row_artifact"]
transaction_artifact["transaction_artifact"]
classification_artifact["classification_artifact"]
validation_artifact["validation_artifact"]
workbook_row_artifact["workbook_row_artifact"]
audit_event_artifact["audit_event_artifact"]
ingest_pdf --> document_artifact
ingest_pdf --> raw_context_artifact
document_artifact --> extracted_row_artifact
extracted_row_artifact --> transaction_artifact
transaction_artifact --> classification_artifact
classification_artifact --> validation_artifact
validation_artifact --> workbook_row_artifact
workbook_row_artifact --> audit_event_artifact
PRD-4 Phase 3: Visual Audit Graph
The third PRD-4 implementation slice turns canonical ontology snapshots into the supported Rhai diagram DSL so the same graph can be rendered by Mermaid and the isometric live editor without hand-written diagram source.
fn ontology_snapshot() -> filter_graph
match filter.kind => Transaction -> transaction_evidence_view
match filter.kind => Document -> document_lineage_view
match filter.kind => XeroEntity -> xero_reconciliation_view
match filter.kind => ModelJob -> model_proposal_view
match filter.kind => _ -> full_snapshot_view
fn transaction_evidence_view() -> mermaid_2d
fn transaction_evidence_view() -> isometric_3d
flowchart TD
ontology_snapshot["ontology_snapshot"]
filter_graph["filter_graph"]
transaction_evidence_view["transaction_evidence_view"]
mermaid_2d["mermaid_2d"]
isometric_3d["isometric_3d"]
match_filter_kind{"match filter.kind"}
document_lineage_view["document_lineage_view"]
xero_reconciliation_view["xero_reconciliation_view"]
model_proposal_view["model_proposal_view"]
full_snapshot_view["full_snapshot_view"]
ontology_snapshot --> filter_graph
transaction_evidence_view --> mermaid_2d
transaction_evidence_view --> isometric_3d
match_filter_kind -->|"Transaction"|transaction_evidence_view
match_filter_kind -->|"Document"|document_lineage_view
match_filter_kind -->|"XeroEntity"|xero_reconciliation_view
match_filter_kind -->|"ModelJob"|model_proposal_view
match_filter_kind -->|"_ (default)"|full_snapshot_view
PRD-4 Phase 4: Typed Phi-4 Job Runtime
The fourth PRD-4 implementation slice keeps model integration host-owned and schema-bound: Phi-4 receives a typed job request, returns only JSON, and the host validates the response before any ontology proposal can become an auditable fact.
fn typed_model_job() -> host_agent_runtime
fn host_agent_runtime() -> phi4_local_endpoint
fn phi4_local_endpoint() -> structured_json_response
fn structured_json_response() -> schema_validation
fn schema_validation() -> invariant_validation
fn invariant_validation() -> ontology_proposal
fn ontology_proposal() -> operator_or_policy_gate
fn operator_or_policy_gate() -> committed_ontology_edge
flowchart TD
typed_model_job["typed_model_job"]
host_agent_runtime["host_agent_runtime"]
phi4_local_endpoint["phi4_local_endpoint"]
structured_json_response["structured_json_response"]
schema_validation["schema_validation"]
invariant_validation["invariant_validation"]
ontology_proposal["ontology_proposal"]
operator_or_policy_gate["operator_or_policy_gate"]
committed_ontology_edge["committed_ontology_edge"]
typed_model_job --> host_agent_runtime
host_agent_runtime --> phi4_local_endpoint
phi4_local_endpoint --> structured_json_response
structured_json_response --> schema_validation
schema_validation --> invariant_validation
invariant_validation --> ontology_proposal
ontology_proposal --> operator_or_policy_gate
operator_or_policy_gate --> committed_ontology_edge
PRD-4 Phase 5: Proposal Review and Commit
The fifth PRD-4 implementation slice gives model-suggested ontology relations a deterministic lifecycle. Phi-4 can propose an edge, but Rust validation, policy thresholds, and operator approval decide whether that edge becomes committed ontology state.
fn phi4_proposal() -> parse_typed_output
fn parse_typed_output() -> rust_invariant_check
if confidence >= 0.90 -> policy_gate
if confidence < 0.90 -> operator_review
fn policy_gate() -> committed_edge
fn operator_review() -> approved_edge
fn operator_review() -> rejected_proposal
fn approved_edge() -> committed_edge
fn rejected_proposal() -> audit_event
flowchart TD
phi4_proposal["phi4_proposal"]
parse_typed_output["parse_typed_output"]
rust_invariant_check["rust_invariant_check"]
policy_gate["policy_gate"]
committed_edge["committed_edge"]
operator_review["operator_review"]
approved_edge["approved_edge"]
rejected_proposal["rejected_proposal"]
audit_event["audit_event"]
confidence_lt_0_9{"confidence < 0.9"}
confidence____0_90{"confidence >= 0.90"}
phi4_proposal --> parse_typed_output
parse_typed_output --> rust_invariant_check
policy_gate --> committed_edge
operator_review --> approved_edge
operator_review --> rejected_proposal
approved_edge --> committed_edge
rejected_proposal --> audit_event
confidence_lt_0_9 -->|"true"|operator_review
confidence____0_90 -->|"true"|policy_gate
PRD-4 Phase 6: Local Semantic Retrieval
The sixth PRD-4 implementation slice introduces a local retrieval index for rule and evidence candidates. The first implementation uses deterministic lexical records as the fallback index so later model embeddings can improve ranking without changing candidate IDs, provenance, or Rhai authority.
fn document_chunk() -> embedding_record
fn transaction_description() -> embedding_record
fn rule_registry() -> embedding_record
fn embedding_record() -> local_vector_index
fn local_vector_index() -> candidate_context
fn candidate_context() -> phi4_typed_job
fn phi4_typed_job() -> validated_classification
fn validated_classification() -> ontology_edge
flowchart TD
document_chunk["document_chunk"]
embedding_record["embedding_record"]
transaction_description["transaction_description"]
rule_registry["rule_registry"]
local_vector_index["local_vector_index"]
candidate_context["candidate_context"]
phi4_typed_job["phi4_typed_job"]
validated_classification["validated_classification"]
ontology_edge["ontology_edge"]
document_chunk --> embedding_record
transaction_description --> embedding_record
rule_registry --> embedding_record
embedding_record --> local_vector_index
local_vector_index --> candidate_context
candidate_context --> phi4_typed_job
phi4_typed_job --> validated_classification
validated_classification --> ontology_edge
PRD-4 Phase 7: End-to-End Audit Playbook
The seventh PRD-4 implementation slice ties a sample statement flow to the operator-facing proof surface: ingest creates the transaction identity, workbook export and audit events preserve it, ontology explains it, and the visual graph shows the path for CPA review.
fn sample_statement() -> ingest_rows
fn ingest_rows() -> classify_transactions
fn classify_transactions() -> phi4_edge_proposals
fn phi4_edge_proposals() -> operator_review
fn operator_review() -> workbook_export
fn workbook_export() -> evidence_chain
fn evidence_chain() -> visual_audit_graph
fn visual_audit_graph() -> cpa_review
flowchart TD
sample_statement["sample_statement"]
ingest_rows["ingest_rows"]
classify_transactions["classify_transactions"]
phi4_edge_proposals["phi4_edge_proposals"]
operator_review["operator_review"]
workbook_export["workbook_export"]
evidence_chain["evidence_chain"]
visual_audit_graph["visual_audit_graph"]
cpa_review["cpa_review"]
sample_statement --> ingest_rows
ingest_rows --> classify_transactions
classify_transactions --> phi4_edge_proposals
phi4_edge_proposals --> operator_review
operator_review --> workbook_export
workbook_export --> evidence_chain
evidence_chain --> visual_audit_graph
visual_audit_graph --> cpa_review
Component Status Table
| Component | Module | Status | Notes |
|---|---|---|---|
| Filename routing | filename.rs | Implemented | VENDOR--ACCT--YYYY-MM--DOCTYPE parser |
| Blake3 hash IDs | ingest.rs | Implemented | deterministic_tx_id, idempotent dedup |
| IngestedLedger | ingest.rs | Implemented | Journal + workbook ingest pipeline |
| DocType enum | document.rs | Implemented | Document type classification |
| DocumentGraph types | document.rs | Implemented | Graph node/edge types defined |
| Pipeline HSM | pipeline.rs | Implemented | Type-state + statig state machine |
| Verb trait | pipeline.rs | Implemented | DetectVerb, ValidateVerb |
| ClassificationEngine | classify.rs | Implemented | Rhai rule execution |
| ClassificationOutcome | classify.rs | Implemented | category, confidence, reason |
| ReviewFlag | classify.rs | Implemented | Flag upsert, query by year/status |
| Rhai rule files | rules/ | Implemented | foreign_income, self_employment, fallback |
| Jurisdiction enum | legal.rs | Implemented | US, AU, UK |
| LegalRule + Z3 formulas | legal.rs | Implemented | Hard predicate checks for AU GST and US Schedule C |
| LegalSolver | legal.rs | Implemented | Uses pinned z3 = 0.8 for violation satisfiability checks behind legal-z3; default builds use the same deterministic result semantics without native Z3 |
| Proposer/Reviewer LLM | verify.rs | Partial | Pattern defined, no real LLM calls |
| WorkflowToml DSL | workflow.rs | Implemented | TOML → Rhai FSM + Mermaid |
| IssueSource::RhaiRule | validation.rs | Implemented | Validation layer with rule source |
| Workbook write | workbook.rs | Implemented | rust_xlsxwriter tx projection |
| Workbook read-back | workbook.rs | Implemented | calamine round-trip |
| Journal | journal.rs | Implemented | NDJSON append and replay |
| Audit trail (MetaCtx) | pipeline.rs | Implemented | Mutation log per pipeline state |
| MCP contract | ledgerr-mcp/src/contract.rs | Implemented | 8 advertised ledgerr_* capability families |
| MCP adapter | ledgerr-mcp/src/mcp_adapter.rs | Implemented | Dispatches contract actions to TurboLedgerService |
| Ontology store | ledgerr-mcp/src/ontology.rs | Implemented | Entity/edge upsert and path query surface |
| Xero service | ledgerr-mcp/src/xero_service.rs | Partial | Supervised catalog/link actions; credentials remain host-owned |
| Mermaid auto-generation | workflow.rs | Implemented | rhai DSL → diagram blocks |
| Slint desktop UI | slint_viz.rs | Partial | Stub, not wired to window system |
| RuleRegistry | rule_registry.rs | Implemented | Loads transaction .rhai rules and optional ReqIF sidecars |
| Keyword rule selection | rule_registry.rs | Implemented | Deterministic keyword fallback; semantic selector remains planned |
| Waterfall orchestration | rule_registry.rs | Implemented | First non-Unclassified result wins; fallback outcome is preserved |
| ReqIfCandidate (Rust) | rule_registry.rs | Stub | Type defined, sidecar bridge missing |
| DocumentChunk | rule_registry.rs | Stub | Type defined, bridge missing |
| SemanticRuleSelector | rule_registry.rs | Stub | Trait defined, embeddings not wired |
| Docling extraction bridge | — | Missing | Python subprocess call not written |
| reqif-opa-mcp MCP wiring | — | Missing | No Rust MCP client for sidecar |
| Vector embedding index | — | Missing | No embedding model or HNSW index |
| File watcher (notify) | — | Missing | notify crate not yet wired |
North Star Pipeline (Rhai DSL)
The following DSL block describes the full intended end-to-end system flow — the “north star” that all stub work is building toward.
fn document_ingest() -> reqif_extract
fn reqif_extract() -> opa_gate
fn opa_gate() -> rule_registry
fn rule_registry() -> classify_waterfall
fn classify_waterfall() -> legal_verify
fn legal_verify() -> workbook_commit
fn workbook_commit() -> audit_trail
flowchart TD
document_ingest["document_ingest"]
reqif_extract["reqif_extract"]
opa_gate["opa_gate"]
rule_registry["rule_registry"]
classify_waterfall["classify_waterfall"]
legal_verify["legal_verify"]
workbook_commit["workbook_commit"]
audit_trail["audit_trail"]
document_ingest --> reqif_extract
reqif_extract --> opa_gate
opa_gate --> rule_registry
rule_registry --> classify_waterfall
classify_waterfall --> legal_verify
legal_verify --> workbook_commit
workbook_commit --> audit_trail
Each step in this pipeline has a corresponding Rust type or trait. The deterministic rule-registry waterfall is now implemented; the remaining ingestion-side gap is the reqif-opa-mcp bridge and semantic rule-selection infrastructure. Z3 is wired for the first hard legal predicates; broader solver coverage is still a roadmap item.
Next Steps
The five highest-value missing capabilities to implement, in priority order:
-
Docling extraction bridge — Write the Rust
std::process::Commandcall to invoke the Python sidecar, parse its NDJSON stdout, and deserialize intoDocumentChunk/ReqIfCandidate. This is the critical path for Phase 2 document intelligence. Estimated scope: newsidecar.rsmodule, ~100 lines. -
Wire
ClassifyTransactionsOptoRuleRegistry— Replace the current operation stub with registry loading, transaction iteration, waterfall classification, and review-flag emission. -
Expand
LegalSolvercoverage — Add hard Z3 checks for FBAR/FATCA thresholds, mutually exclusive categories, and reconciliation/workbook invariants. The initial Z3 integration covers AU GST and US Schedule C predicates. -
File watcher via
notify— Add a debouncednotifywatcher on the workbook path and therules/directory. This enables live rule reloading and human Excel-edit detection without polling. Estimated scope: newwatcher.rsmodule, ~60 lines. -
SemanticRuleSelectorembedding index — Wire a localfastembed-rsor ONNX embedding model to encode transaction descriptions andReqIfCandidatetexts. Build an HNSW index over candidate embeddings for cosine-similarity rule selection. Estimated scope: ~200 lines, depends on item 2.
Audit Playbook
This playbook is the PRD-4 end-to-end operator path. It proves that a sample statement row, workbook projection, ontology snapshot, audit events, and visual graph all carry the same transaction identity.
fn sample_statement() -> ingest_rows
fn ingest_rows() -> classify_transactions
fn classify_transactions() -> phi4_edge_proposals
fn phi4_edge_proposals() -> operator_review
fn operator_review() -> workbook_export
fn workbook_export() -> evidence_chain
fn evidence_chain() -> visual_audit_graph
fn visual_audit_graph() -> cpa_review
flowchart TD
sample_statement["sample_statement"]
ingest_rows["ingest_rows"]
classify_transactions["classify_transactions"]
phi4_edge_proposals["phi4_edge_proposals"]
operator_review["operator_review"]
workbook_export["workbook_export"]
evidence_chain["evidence_chain"]
visual_audit_graph["visual_audit_graph"]
cpa_review["cpa_review"]
sample_statement --> ingest_rows
ingest_rows --> classify_transactions
classify_transactions --> phi4_edge_proposals
phi4_edge_proposals --> operator_review
operator_review --> workbook_export
workbook_export --> evidence_chain
evidence_chain --> visual_audit_graph
visual_audit_graph --> cpa_review
Runnable Paths
Basic deterministic path:
just mcp-cli-basic
Blocked diagnostic path:
just mcp-cli-spinning-wheels
Host playbook path with deterministic Phi-4 fallback:
just host-playbook-window
Host playbook path with local Phi-4 model assets:
just host-playbook-window-phi4
Host playbook path with Windows AI / Foundry Local:
just windows-ai-install
just windows-ai-setup
just windows-ai-smoke
just host-playbook-window-windows-ai
The Windows AI provider is selectable in the host window. It is not auto-selected.
Use the Windows AI / Foundry Local control after the smoke test passes.
Related Chapters
Document Ingestion
Document ingestion converts raw PDF statements into structured, auditable transaction records. The process is split into two phases: deterministic ingestion (fully implemented) and rule derivation from document content (partially implemented via stubs).
Two-Phase Ingestion Model
Phase 1 — Deterministic Ingestion takes a PDF, extracts transactions using Blake3 content-hash IDs for deduplication, and writes raw records to the workbook and journal. This phase is fully implemented and produces stable, reproducible output regardless of processing order.
Phase 2 — Rule Derivation uses the extracted document structure to derive applicable classification rules. This requires the reqif-opa-mcp Python sidecar to build a DocumentGraph from the PDF, run it through an OPA policy gate, and produce RequirementCandidate objects. The Rust side then deserializes these into ReqIfCandidate structs and uses them to seed the RuleRegistry. This phase is currently stubbed.
Ingestion Pipeline Diagram
fn pdf_statement() -> filename_routing
fn filename_routing() -> blake3_ingest
fn filename_routing() -> docling_extract
fn docling_extract() -> document_graph
fn document_graph() -> requirement_candidates
fn requirement_candidates() -> opa_gate
fn opa_gate() -> reqif_baseline
fn reqif_baseline() -> rule_registry
fn rule_registry() -> classify_waterfall
fn classify_waterfall() -> workbook_commit
flowchart TD
pdf_statement["pdf_statement"]
filename_routing["filename_routing"]
blake3_ingest["blake3_ingest"]
docling_extract["docling_extract"]
document_graph["document_graph"]
requirement_candidates["requirement_candidates"]
opa_gate["opa_gate"]
reqif_baseline["reqif_baseline"]
rule_registry["rule_registry"]
classify_waterfall["classify_waterfall"]
workbook_commit["workbook_commit"]
pdf_statement --> filename_routing
filename_routing --> blake3_ingest
filename_routing --> docling_extract
docling_extract --> document_graph
document_graph --> requirement_candidates
requirement_candidates --> opa_gate
opa_gate --> reqif_baseline
reqif_baseline --> rule_registry
rule_registry --> classify_waterfall
classify_waterfall --> workbook_commit
Ingestion Flow (Rhai DSL)
The following DSL block describes the intended end-to-end ingestion flow. Each step corresponds to a node in the pipeline diagram above.
fn ingest_artifact() -> extract_document
fn extract_document() -> build_graph
fn build_graph() -> derive_candidates
fn derive_candidates() -> opa_gate
fn opa_gate() -> emit_reqif
fn emit_reqif() -> load_rules
flowchart TD
ingest_artifact["ingest_artifact"]
extract_document["extract_document"]
build_graph["build_graph"]
derive_candidates["derive_candidates"]
opa_gate["opa_gate"]
emit_reqif["emit_reqif"]
load_rules["load_rules"]
ingest_artifact --> extract_document
extract_document --> build_graph
build_graph --> derive_candidates
derive_candidates --> opa_gate
opa_gate --> emit_reqif
emit_reqif --> load_rules
Capability Status Table
| Capability | Status | Implementation | Notes |
|---|---|---|---|
ingest_artifact | Implemented | IngestedLedger::ingest | Blake3 hash dedup, journal write |
extract_document | Planned | Python sidecar: extract_docling_document | Docling 2.78 PDF parse |
build_graph | Planned | Python sidecar: DocumentGraph assembly | DocumentNode tree construction |
derive_candidates | Planned | Python sidecar: RequirementCandidate | Heuristic requirement extraction |
opa_gate | Planned | Python sidecar: OPA policy evaluation | Policy: admit or reject candidates |
emit_reqif | Planned | Python sidecar: emit_reqif_xml | ReqIF XML baseline output |
load_rules | Implemented | RuleRegistry::load_from_dir | Loads transaction .rhai rules and optional ReqIfCandidate sidecars |
ReqIF-OPA-MCP Integration
The Python sidecar at https://github.com/PromptExecution/reqif-opa-mcp implements the full ArtifactRecord → DocumentGraph → RequirementCandidate → OPA → ReqIF pipeline. It is invoked as a subprocess by the Rust core and communicates via NDJSON over stdout.
Key Python types and their Rust mirrors:
| Python (sidecar) | Rust (ledger-core) | Notes |
|---|---|---|
ArtifactRecord | — | Source document metadata; consumed by sidecar only |
DocumentNode | DocumentChunk | Canonical graph node with text, parent, anchors |
DocumentGraph | — | Intermediate; not materialized in Rust |
RequirementCandidate | ReqIfCandidate | Deserialized from sidecar NDJSON output |
The sidecar also exposes an MCP server interface for querying existing ReqIF baselines and evaluating new candidates against established policy. This allows the agent to ask “does this transaction match any known requirement?” before falling back to keyword-match rule selection.
The Rust bridge will call the sidecar via std::process::Command, read its NDJSON output, and deserialize each line into a ReqIfCandidate. These candidates are then stored in the RuleRegistry alongside their associated rule files.
Vector Search Stub
SemanticRuleSelector (defined in crates/ledger-core/src/rule_registry.rs) will eventually use vector embeddings to rank rule files by semantic similarity to each transaction’s description. The embedding space is anchored to ReqIfCandidate text so that rules are selected based on document-derived semantics rather than string keywords.
The interface is already defined:
#![allow(unused)]
fn main() {
pub trait SemanticRuleSelector {
fn select_rules_semantic(&self, tx: &SampleTransaction, top_k: usize) -> Vec<PathBuf>;
fn build_embedding_index(&mut self) -> Result<(), RuleRegistryError>;
}
}
Implementation is blocked on:
- A local embedding model (ONNX via
ort, orfastembed-rs) - A vector index (
usearch,hnsw, orqdrantsidecar) ReqIfCandidateobjects being populated from the sidecar bridge
Until these are available, select_rules_deterministic provides a stable keyword-match fallback. See Rule Engine for the full classification pipeline.
Rule Engine
The rule engine is the classification core of l3dg3rr. It applies a set of Rhai rule files to each ingested transaction to assign a tax category, confidence score, and optional review flag. Rules are authored as standalone .rhai files and executed at runtime without recompilation.
Classification Pipeline
Rules execute in a waterfall model: each rule is tried in order, and the first rule that returns a category other than "Unclassified" wins. This keeps individual rules simple and composable while still handling complex multi-category edge cases.
// Multi-rule waterfall: first non-Unclassified result wins
fn load_rules() -> select_applicable
fn select_applicable() -> run_waterfall
fn run_waterfall() -> evaluate_confidence
fn evaluate_confidence() -> emit_result
flowchart TD
load_rules["load_rules"]
select_applicable["select_applicable"]
run_waterfall["run_waterfall"]
evaluate_confidence["evaluate_confidence"]
emit_result["emit_result"]
load_rules --> select_applicable
select_applicable --> run_waterfall
run_waterfall --> evaluate_confidence
evaluate_confidence --> emit_result
The pipeline nodes map to Rust types:
| Pipeline step | Rust type | Status |
|---|---|---|
load_rules | RuleRegistry::load_from_dir | Implemented for transaction rules |
select_applicable | RuleRegistry::select_rules_deterministic | Implemented keyword fallback |
run_waterfall | RuleRegistry::classify_waterfall | Implemented first-match waterfall |
evaluate_confidence | ClassificationOutcome.confidence | Implemented |
emit_result | ClassifiedTransaction / ReviewFlag | Implemented |
Rule Selection
Before running the waterfall, applicable rules are selected from the registry. A confidence gate then routes the result to either a commit or a manual review queue.
if confidence > 0.85 -> commit
if confidence > 0.60 -> review
if confidence <= 0.60 -> escalate
flowchart TD
confidence_gt_0_85{"confidence > 0.85"}
commit["commit"]
confidence_gt_0_6{"confidence > 0.6"}
review["review"]
confidence____0_60{"confidence <= 0.60"}
escalate["escalate"]
confidence_gt_0_85 -->|"true"|commit
confidence_gt_0_85 -->|"false"|confidence_gt_0_6
confidence_gt_0_6 -->|"true"|review
confidence____0_60 -->|"true"|escalate
The thresholds are configurable per deployment. The review path creates a ReviewFlag in the workbook’s Flags sheet. The escalate path promotes the flag to a high-priority manual item.
Architecture Overview
The diagram below shows the full classification pipeline from document ingestion through workbook commit. Each node is tagged with its current implementation status.
fn pdf_source() -> doctype_detection
fn doctype_detection() -> reqif_bridge
fn doctype_detection() -> rule_registry
fn reqif_bridge() -> rule_registry
fn rule_registry() -> rule_selection
fn rule_selection() -> classify_waterfall
fn classify_waterfall() -> high_confidence
fn classify_waterfall() -> medium_confidence
fn classify_waterfall() -> low_confidence
fn high_confidence() -> workbook_commit
fn medium_confidence() -> review_flag
fn low_confidence() -> review_flag
fn review_flag() -> workbook_commit
flowchart TD
pdf_source["pdf_source"]
doctype_detection["doctype_detection"]
reqif_bridge["reqif_bridge"]
rule_registry["rule_registry"]
rule_selection["rule_selection"]
classify_waterfall["classify_waterfall"]
high_confidence["high_confidence"]
medium_confidence["medium_confidence"]
low_confidence["low_confidence"]
workbook_commit["workbook_commit"]
review_flag["review_flag"]
pdf_source --> doctype_detection
doctype_detection --> reqif_bridge
doctype_detection --> rule_registry
reqif_bridge --> rule_registry
rule_registry --> rule_selection
rule_selection --> classify_waterfall
classify_waterfall --> high_confidence
classify_waterfall --> medium_confidence
classify_waterfall --> low_confidence
high_confidence --> workbook_commit
medium_confidence --> review_flag
low_confidence --> review_flag
review_flag --> workbook_commit
Rule Authoring
Rules are standalone .rhai files that implement a single fn classify(tx) function. The function receives a transaction map and returns a map with fields: category, confidence, review, reason.
See docs/rhai-rules.md for the complete rule authoring guide, including field contracts, available built-ins, and worked examples for foreign_income, self_employment, and fallback rules.
The baseline rules currently covered by the registry waterfall are:
rules/classify_foreign_income.rhai— matches foreign-sourced income transactionsrules/classify_self_employment.rhai— matches self-employment / Schedule C incomerules/classify_fallback.rhai— catch-all returningUnclassifiedwithreview: true
Additional Schedule, FBAR/FATCA, crypto, AU GST, and AU CGT transaction rules are also loaded. classify_document_shape.rhai is excluded because it exposes classify_document(doc), not the transaction classify(tx) entry point.
Stub: Semantic Rule Selection
SemanticRuleSelector is the planned upgrade to the deterministic keyword-match selector. It will use vector embeddings to rank rules by semantic similarity to a transaction’s description, drawing on ReqIfCandidate objects produced by the reqif-opa-mcp Python sidecar.
#![allow(unused)]
fn main() {
pub trait SemanticRuleSelector {
fn select_rules_semantic(&self, tx: &SampleTransaction, top_k: usize) -> Vec<PathBuf>;
fn build_embedding_index(&mut self) -> Result<(), RuleRegistryError>;
}
}
This is unimplemented!() today because it requires:
- Embedding model — a local ONNX or
fastembed-rsmodel to encode transaction text - ReqIfCandidate index —
ReqIfCandidateobjects loaded from the Python sidecar - Vector similarity — cosine distance or HNSW index over candidate embeddings
Until those are wired, select_rules_deterministic provides a stable keyword-match fallback. See Document Ingestion for the full sidecar bridge plan.
Validation
The validation module provides the core type system for pipeline stage results.
Related Chapters
- Pipeline - State machine integration
- Constraints - Kasuari constraint types
- Verify - Multi-model confidence calculation
- Legal Verification - Tax rule disposition
Disposition
#![allow(unused)]
fn main() {
pub enum Disposition {
Unrecoverable, // Fatal error, cannot proceed
Recoverable, // Error that can be fixed
Advisory, // Warning or suggestion
}
}
Issue
#![allow(unused)]
fn main() {
pub struct Issue {
pub disposition: Disposition,
pub message: String,
pub source: IssueSource,
}
}
MetaCtx
Metadata context that accumulates through pipeline stages:
#![allow(unused)]
fn main() {
pub struct MetaCtx {
pub accumulated_confidence: f32, // Compounded from all stages
pub stage_history: Vec<StageResult>,
// ...
}
}
Confidence compounds multiplicatively: next.confidence = current.confidence * stage.confidence
StageResult
Captures the outcome of a pipeline stage with issues and confidence score.
Legal Verification
The legal module verifies hard tax predicates with Z3. Rhai rules can propose a category and confidence score, but legal verification answers a stricter question: “are the known facts compatible with this rule’s required conditions?”
Solver Role
fn classified_transaction() -> transaction_facts
fn transaction_facts() -> legal_rule
fn legal_rule() -> z3_solver
fn z3_solver() -> z3_result
match z3_result => Satisfied -> continue_pipeline
match z3_result => Violated -> create_review_flag
match z3_result => Unknown -> request_more_facts
flowchart TD
classified_transaction["classified_transaction"]
transaction_facts["transaction_facts"]
legal_rule["legal_rule"]
z3_solver["z3_solver"]
z3_result["z3_result"]
match_z3_result{"match z3_result"}
continue_pipeline["continue_pipeline"]
create_review_flag["create_review_flag"]
request_more_facts["request_more_facts"]
classified_transaction --> transaction_facts
transaction_facts --> legal_rule
legal_rule --> z3_solver
z3_solver --> z3_result
match_z3_result -->|"Satisfied"|continue_pipeline
match_z3_result -->|"Violated"|create_review_flag
match_z3_result -->|"Unknown"|request_more_facts
LegalSolver::verify() currently covers:
- AU GST Act s38-190 style SaaS tax-code checks.
- US Schedule C ordinary-and-necessary deduction checks.
Z3 Integration
The Rust crate z3 provides an idiomatic wrapper over Microsoft’s Z3 theorem prover. The repo currently pins z3 = "0.8" behind the ledger-core/legal-z3 feature because local developer machines may not have native libz3 installed. When that feature is enabled, ledger-core checks for common native Z3 library locations and emits a Cargo warning if the library is missing. On Ubuntu/WSL, install it with sudo apt install -y libz3-dev.
docs.rs currently shows newer z3 0.20.0 APIs, so examples in this book describe the application pattern rather than relying on newest-version syntax.
The core pattern is:
- Convert known
TransactionFactsinto boolean predicates. - Build a violation formula.
- When
legal-z3is enabled, ask Z3 whether the violation formula is satisfiable. - Interpret
unsatasZ3Result::Satisfied,satasZ3Result::Violated, and solver unknown asZ3Result::Unknown.
This makes the result explainable: a violation is not just a failed if branch; it is a satisfiable counterexample to the rule obligation.
When legal-z3 is not enabled, LegalSolver preserves the same public result semantics with a deterministic boolean mirror so default builds do not require system Z3.
Hard vs Soft Constraints
Z3 should handle hard proof obligations:
- tax rule implication checks
- mutually exclusive classifications
- reconciliation arithmetic that must balance
- workflow commit guards
- workbook export invariants
Kasuari remains the right tool for soft plausibility constraints and layout constraints:
- vendor amount ranges
- weak/medium/strong historical expectations
- graph and isometric placement constraints
Rule Examples
AU GST
For a foreign SaaS vendor, the hard predicate is:
foreign_vendor AND saas_supply AND NOT tax_code_BASEXCLUDED
If Z3 says that violation is satisfiable, the transaction is flagged. If it is unsatisfiable, the tax code satisfies the rule.
US Schedule C
For a business expense, the hard predicate is:
business_activity AND NOT (ordinary AND necessary)
If satisfiable, the deduction lacks a required fact and should be reviewed or repaired.
Transaction Facts
#![allow(unused)]
fn main() {
pub struct TransactionFacts {
pub vendor_jurisdiction: Option<String>,
pub supply_type: Option<String>,
pub tax_code: Option<String>,
pub amount: Option<String>,
pub is_business_activity: Option<bool>,
pub is_ordinary: Option<bool>,
pub is_necessary: Option<bool>,
}
}
Unknown facts should produce Z3Result::Unknown rather than pretending the transaction passed. This keeps the legal layer conservative and audit-friendly.
Related Chapters
Ledger Operations
Overview
The LedgerOperation trait is the primitive interface for every discrete action the pipeline can take: ingesting a statement, classifying transactions, checking a tax deadline, exporting the CPA workbook. Composing these operations through an OperationDispatcher rather than calling them directly provides:
- Idempotency guarantees: Each operation implementation declares whether it is idempotent. The dispatcher enforces this contract and deduplicates re-triggered operations where safe.
- Uniform result surface: Every operation returns an
OperationResultthat flows into the audit trail, regardless of what the operation does internally. - Calendar integration:
ScheduledEventrecords carry anOperationKindthat the dispatcher resolves to a concrete operation at runtime. The calendar and the operation layer are decoupled. - Agent-editable dispatch rules: Rhai rules can inspect
OperationContextfields and short-circuit or redirect operations without changing Rust code.
Operation Dispatch Flow
fn receive_trigger() -> resolve_operation
fn resolve_operation() -> validate_context
fn validate_context() -> execute_operation
fn execute_operation() -> record_result
if result_success -> mark_complete
if result_failure -> emit_issue
flowchart TD
receive_trigger["receive_trigger"]
resolve_operation["resolve_operation"]
validate_context["validate_context"]
execute_operation["execute_operation"]
record_result["record_result"]
result_success{"result_success"}
mark_complete["mark_complete"]
result_failure{"result_failure"}
emit_issue["emit_issue"]
receive_trigger --> resolve_operation
resolve_operation --> validate_context
validate_context --> execute_operation
execute_operation --> record_result
result_success --> mark_complete
result_failure --> emit_issue
receive_trigger— accepts anOperationKindfrom a scheduled event, MCP call, or manual invocation.resolve_operation— looks up the registeredLedgerOperationimplementation for the givenOperationKind.validate_context— checks thatOperationContextcontains required fields (e.g.journal_path,workbook_path); returnsErrif preconditions are not met.execute_operation— calls the operation’sexecute()method; the operation is responsible for its own internal error handling.record_result— writes theOperationResult(success or failure) to the audit trail with timestamps and the triggering event ID.mark_complete— on success, updates the scheduler’s completion record so the event is not re-fired.emit_issue— on failure, emits a structured issue record for operator review; does not re-trigger automatically.
Dispatcher Architecture
fn operation_dispatcher() -> operation_context
fn operation_dispatcher() -> ingest_statement_op
fn operation_dispatcher() -> classify_transactions_op
fn operation_dispatcher() -> check_tax_deadline_op
fn operation_dispatcher() -> export_workbook_op
fn operation_dispatcher() -> generate_audit_trail_op
fn ingest_statement_op() -> operation_result
fn classify_transactions_op() -> operation_result
fn check_tax_deadline_op() -> operation_result
fn export_workbook_op() -> operation_result
fn generate_audit_trail_op() -> operation_result
fn operation_result() -> audit_trail
flowchart TD
operation_dispatcher["operation_dispatcher"]
operation_context["operation_context"]
ingest_statement_op["ingest_statement_op"]
classify_transactions_op["classify_transactions_op"]
check_tax_deadline_op["check_tax_deadline_op"]
export_workbook_op["export_workbook_op"]
generate_audit_trail_op["generate_audit_trail_op"]
operation_result["operation_result"]
audit_trail["audit_trail"]
operation_dispatcher --> operation_context
operation_dispatcher --> ingest_statement_op
operation_dispatcher --> classify_transactions_op
operation_dispatcher --> check_tax_deadline_op
operation_dispatcher --> export_workbook_op
operation_dispatcher --> generate_audit_trail_op
ingest_statement_op --> operation_result
classify_transactions_op --> operation_result
check_tax_deadline_op --> operation_result
export_workbook_op --> operation_result
generate_audit_trail_op --> operation_result
operation_result --> audit_trail
IngestStatementOp is annotated as idempotent: re-ingesting the same source file produces the same Blake3 content-hash transaction IDs and the dispatcher skips duplicate writes.
Operations Reference
| Operation | ID | Idempotent | Status | Description |
|---|---|---|---|---|
IngestStatementOp | IngestStatement | Yes (Blake3 dedup) | Implemented | Parse a source PDF or CSV, extract transactions, write to journal and workbook |
ClassifyTransactionsOp | ClassifyTransactions | Yes (rule determinism) | Implemented | Run the Rhai classification waterfall over unclassified transactions |
CheckTaxDeadlineOp | CheckTaxDeadline | Yes | Implemented | Emit a deadline notification record; no data mutation |
ExportWorkbookOp | ExportWorkbook | Yes (overwrite) | Implemented | Write the current journal state to the CPA Excel workbook |
GenerateAuditTrailOp | GenerateAuditTrail | Yes (overwrite) | Implemented | Produce the year-end audit trail report for a given tax year |
Document Shape Classification
Before IngestStatementOp can extract transactions, it must know which extraction profile to use. classify_document_shape() in document_shape.rs (and mirrored in rules/classify_document_shape.rhai) maps a raw document to a DocumentShape:
fn ingest_file() -> detect_shape
fn detect_shape() -> route_extractor
fn route_extractor() -> extract_transactions
fn extract_transactions() -> classify_transactions
flowchart TD
ingest_file["ingest_file"]
detect_shape["detect_shape"]
route_extractor["route_extractor"]
extract_transactions["extract_transactions"]
classify_transactions["classify_transactions"]
ingest_file --> detect_shape
detect_shape --> route_extractor
route_extractor --> extract_transactions
extract_transactions --> classify_transactions
ingest_file— receives a source path matching theVENDOR--ACCOUNT--YYYY-MM--DOCTYPEnaming convention.detect_shape— callsclassify_document_shape()with filename, doc_type, and a content sample. ReturnsDocumentShapewith vendor, account_type, statement_format, currency, confidence, and signals.route_extractor— selects the extraction backend based onstatement_format:csv_generic,csv_ofx,pdf_tabular, orxlsx_native.extract_transactions— runs the selected extractor; outputs rawTransactionrows withDecimalamounts.classify_transactions— passes extracted transactions through the Rhai classification waterfall inrules/.
Shape detection uses a confidence score. If confidence is below 0.5, IngestStatementOp emits a review flag and halts rather than ingesting with an unknown vendor profile.
DocumentShape Fields
| Field | Type | Description |
|---|---|---|
vendor | StatementVendor | Institution slug: WellsFargo, Chase, Anz, Commbank, etc. |
account_type | String | checking, savings, brokerage, crypto |
statement_format | String | csv_generic, csv_ofx, pdf_tabular, xlsx_native |
currency | String | USD, AUD, EUR, GBP |
confidence | f64 | 0.0–1.0 heuristic score based on filename slug and content signals |
signals | Vec<String> | Matched signal names for audit; e.g. filename_vendor_slug, csv_header_match |
reason | String | Human-readable explanation for the audit trail |
Integration with Calendar
ScheduledEvent in the TOML calendar manifests carries an operation field that is an OperationKind variant. When BusinessCalendar::upcoming() returns a due event, the caller passes event.operation to OperationDispatcher::dispatch():
#![allow(unused)]
fn main() {
let due = calendar.upcoming(today, 30);
for event in due {
let result = dispatcher.dispatch(&event.operation, &context).await?;
audit_trail.record(event.id, result);
}
}
The TOML operation inline table maps directly to OperationKind enum variants:
# Maps to OperationKind::IngestStatement { source_glob: "samples/**/*.pdf" }
operation = { type = "IngestStatement", source_glob = "samples/**/*.pdf" }
# Maps to OperationKind::ClassifyTransactions { rule_dir: "rules" }
operation = { type = "ClassifyTransactions", rule_dir = "rules" }
# Maps to OperationKind::CheckTaxDeadline { deadline_id: "fbar_deadline" }
operation = { type = "CheckTaxDeadline", deadline_id = "fbar_deadline" }
This bidirectional mapping means calendar manifests are the single source of truth for what runs, when it runs, and what it does — the Rust dispatcher merely executes.
Workbook & Audit
The workbook is the accountant-facing artifact. The journal, audit log, and sidecar state are machine-facing recovery surfaces. The application keeps these roles separate so a CPA can inspect and sign off in Excel while agents still get deterministic replay and audit evidence.
Workbook Contract
ledger_core::workbook::REQUIRED_SHEETS is the base workbook contract. Export paths should rebuild the workbook from canonical service state rather than mutating partial output in place.
Required workbook concerns:
- transaction projection rows with stable transaction IDs
- account registry and metadata sheets
- Schedule-oriented summaries
- review and ambiguity flags
- audit log projection
- configuration and manifest metadata
fn canonical_state() -> workbook_projection
fn workbook_projection() -> transaction_sheets
fn workbook_projection() -> schedule_summaries
fn workbook_projection() -> flag_sheets
fn workbook_projection() -> audit_sheet
fn audit_sheet() -> cpa_review
flowchart TD
canonical_state["canonical_state"]
workbook_projection["workbook_projection"]
transaction_sheets["transaction_sheets"]
schedule_summaries["schedule_summaries"]
flag_sheets["flag_sheets"]
audit_sheet["audit_sheet"]
cpa_review["cpa_review"]
canonical_state --> workbook_projection
workbook_projection --> transaction_sheets
workbook_projection --> schedule_summaries
workbook_projection --> flag_sheets
workbook_projection --> audit_sheet
audit_sheet --> cpa_review
Audit Flow
Every meaningful mutation should have an audit event before it becomes externally visible. Classification edits, reconciliation commits, lifecycle transitions, and workbook exports should all be explainable from event history.
Machine recovery state lives in deterministic sidecars next to the manifest workbook path. If a sidecar exists but cannot be parsed or has an unsupported version, the service should fail closed rather than silently resetting state.
Journal Flow
The NDJSON journal provides append/replay behavior for ingested transactions. Transaction identity is content-addressed with Blake3 over account, date, amount, and description, making repeated ingest idempotent.
Projection Rule
The workbook is a projection, not the only system of record for agent queues or replay state. It remains the canonical human/audit layer because the CPA workflow depends on Excel, but operational restart state belongs in the sidecar snapshot.
Related Chapters
Business Calendar & Scheduler
Overview
The business calendar drives deterministic, auditable scheduling for the tax preparation pipeline. Rather than relying on ad-hoc manual triggers, every recurring obligation — quarterly estimated tax payments, BAS lodgements, monthly statement ingest runs — is encoded as a ScheduledEvent in a TOML manifest. The BusinessCalendar evaluates upcoming events against a horizon window and feeds them to the OperationDispatcher.
This approach provides several guarantees critical for expat tax work:
- No missed deadlines: US and AU obligations are codified with tax code citations; the scheduler surfaces them before they are due.
- Reproducible automation: Monthly ingest and classification runs fire on fixed day-of-month rules, not human memory.
- Audit trail: Every dispatched operation records its triggering event ID, so the audit log can be traced back to the calendar manifest.
- Agent-editable without recompile: TOML manifests and Rhai dispatch rules can be updated by an agent or operator without rebuilding the Rust binary.
Calendar-Driven Operation Pipeline
The scheduling loop is expressed as a Rhai function chain. Each step either proceeds or short-circuits based on the result of the previous step.
fn check_calendar() -> find_due_events
fn find_due_events() -> dispatch_operations
fn dispatch_operations() -> execute_waterfall
fn execute_waterfall() -> record_completion
if horizon_days > 30 -> warn_upcoming
if horizon_days <= 7 -> escalate_urgent
flowchart TD
check_calendar["check_calendar"]
find_due_events["find_due_events"]
dispatch_operations["dispatch_operations"]
execute_waterfall["execute_waterfall"]
record_completion["record_completion"]
horizon_days_gt_30{"horizon_days > 30"}
warn_upcoming["warn_upcoming"]
horizon_days____7{"horizon_days <= 7"}
escalate_urgent["escalate_urgent"]
check_calendar --> find_due_events
find_due_events --> dispatch_operations
dispatch_operations --> execute_waterfall
execute_waterfall --> record_completion
horizon_days_gt_30 -->|"true"|warn_upcoming
horizon_days____7 -->|"true"|escalate_urgent
check_calendar— loads the activeBusinessCalendarand computes today’s date relative to each event’sRecurrenceRule.find_due_events— returns events whose next fire date falls within the configured horizon window.dispatch_operations— resolves each event’sOperationKindand forwards it to theOperationDispatcher.execute_waterfall— runs each operation in dependency order (ingest before classify, classify before export).record_completion— writes the completion record to the audit trail with the triggering event ID.warn_upcoming— logs upcoming events beyond 7 days but within the horizon (informational).escalate_urgent— emits a high-priority notification for events due within 7 days.
Scheduling Loop Diagram
fn business_calendar() -> events_due
fn events_due() -> operation_dispatcher
fn operation_dispatcher() -> ingest_statement
fn operation_dispatcher() -> classify_transactions
fn operation_dispatcher() -> check_tax_deadline
fn operation_dispatcher() -> export_workbook
fn ingest_statement() -> operation_result
fn classify_transactions() -> operation_result
fn check_tax_deadline() -> operation_result
fn export_workbook() -> operation_result
fn operation_result() -> audit_trail
if recurrence == monthly -> events_due
if recurrence == quarterly_estimated -> events_due
if recurrence == annual -> events_due
flowchart TD
business_calendar["business_calendar"]
events_due["events_due"]
operation_dispatcher["operation_dispatcher"]
ingest_statement["ingest_statement"]
classify_transactions["classify_transactions"]
check_tax_deadline["check_tax_deadline"]
export_workbook["export_workbook"]
operation_result["operation_result"]
audit_trail["audit_trail"]
recurrence____monthly{"recurrence == monthly"}
recurrence____quarterly_estimated{"recurrence == quarterly_estimated"}
recurrence____annual{"recurrence == annual"}
business_calendar --> events_due
events_due --> operation_dispatcher
operation_dispatcher --> ingest_statement
operation_dispatcher --> classify_transactions
operation_dispatcher --> check_tax_deadline
operation_dispatcher --> export_workbook
ingest_statement --> operation_result
classify_transactions --> operation_result
check_tax_deadline --> operation_result
export_workbook --> operation_result
operation_result --> audit_trail
recurrence____monthly -->|"true"|events_due
recurrence____quarterly_estimated -->|"true"|events_due
recurrence____annual -->|"true"|events_due
US Tax Calendar
Key US federal deadlines encoded in calendar/tax_calendar_us.toml:
| Event | Date | Tax Code Citation | Tags |
|---|---|---|---|
| Q1 Estimated Tax Payment | April 15 | IRC §6654 | estimated_tax, quarterly, schedule_c |
| Q2 Estimated Tax Payment | June 15 | IRC §6654 | estimated_tax, quarterly |
| Q3 Estimated Tax Payment | September 15 | IRC §6654 | estimated_tax, quarterly |
| Q4 Estimated Tax Payment | January 15 (next year) | IRC §6654 | estimated_tax, quarterly |
| Form 1040 Filing Deadline | April 15 (June 15 expat auto-extension) | 26 USC §6072 | annual_return, form_1040 |
| Form 1040 Extended Deadline | October 15 | Form 4868 | annual_return, extension, form_4868 |
| FBAR (FinCEN Form 114) | April 15 (auto-extension to Oct 15) | 31 USC §5314 | fbar, foreign_accounts, fincen_114 |
| FATCA Form 8938 | April 15 | IRC §6038D | fatca, form_8938, foreign_assets |
| Monthly Statement Ingest | 1st of each month | — | automation, ingest, recurring |
| Monthly Classification Run | 2nd of each month | — | automation, classification, recurring |
AU Tax Calendar
Key Australian tax deadlines encoded in calendar/tax_calendar_au.toml:
| Event | Date | Tax Code Citation | Tags |
|---|---|---|---|
| BAS GST Q1 Lodgement | October 28 | ATO BAS program | bas, gst, quarterly, q1 |
| BAS GST Q2 Lodgement | February 28 | ATO BAS program | bas, gst, quarterly, q2 |
| BAS GST Q3 Lodgement | April 28 | ATO BAS program | bas, gst, quarterly, q3 |
| BAS GST Q4 Lodgement | July 28 | ATO BAS program | bas, gst, quarterly, q4 |
| Individual Tax Return | October 31 | ITAA 1997 | annual_return, individual, itaa_1997 |
| Tax Agent Extension | May 15 (next year) | ATO lodgement program | annual_return, extension, tax_agent |
| Concessional Super Contribution | June 30 | ITAA 1997 s.290-180 | superannuation, concessional, s290-180 |
| CGT Discount Asset Review | June 30 | ITAA 1997 s.115-A | cgt, capital_gains, s115-a, discount_method |
| Monthly Statement Ingest | 1st of each month | — | automation, ingest, recurring |
| Monthly Classification Run | 2nd of each month | — | automation, classification, recurring |
TOML Format Reference
Each entry in a calendar manifest is a [[event]] block:
[[event]]
id = "fbar_deadline"
# Human-readable description including tax code citation
description = "FinCEN Form 114 (FBAR) deadline — April 15 with auto-extension to October 15 (31 USC §5314)"
# RecurrenceRule — determines when the event fires
# Supported types: Annual, Monthly, QuarterlyEstimated, EveryNDays, CronExpr (stub)
recurrence = { type = "Annual", month = 4, day = 15 }
# OperationKind — the operation dispatched when this event fires
# Supported: CheckTaxDeadline, IngestStatement, ClassifyTransactions,
# GenerateAuditTrail, ExportWorkbook
operation = { type = "CheckTaxDeadline", deadline_id = "fbar_deadline" }
jurisdiction = "US" # "US" | "AU" | "UK" | ...
enabled = true # false to suspend without removing the event
tags = ["fbar", "foreign_accounts", "fincen_114"]
Field descriptions:
| Field | Type | Description |
|---|---|---|
id | String | Unique event identifier; referenced in audit trail records |
description | String | Human-readable label; include tax code citation for compliance events |
recurrence | Inline table | RecurrenceRule variant with type-specific fields |
operation | Inline table | OperationKind variant dispatched on fire |
jurisdiction | String | Regulatory jurisdiction; used to filter calendars per entity |
enabled | Bool | Set false to suspend without deleting |
tags | String array | Free-form labels for filtering and grouping |
Using the Calendar
#![allow(unused)]
fn main() {
use ledger::calendar::{BusinessCalendar, ScheduledEvent};
use chrono::Local;
// Load a TOML calendar manifest
let cal = BusinessCalendar::from_toml_file("calendar/tax_calendar_us.toml")?;
// Find events due within the next 30 days
let today = Local::now().date_naive();
let due = cal.upcoming(today, 30);
for event in &due {
println!("Due: {} — {}", event.next_date, event.description);
}
// Dispatch all due operations
let dispatcher = OperationDispatcher::new();
for event in due {
let result = dispatcher.dispatch(&event.operation, &context).await?;
audit_trail.record(event.id, result);
}
}
Key methods on BusinessCalendar:
from_toml_file(path)— parse a TOML manifest from disk; returnsResult<BusinessCalendar>upcoming(today, horizon_days)— returnsVec<ScheduledEvent>due withinhorizon_daysoverdue(today)— returns events whose last expected fire date has passed without a completion recordmerge(other)— combine US and AU calendars for multi-jurisdiction entities
Stub: CronExpr
The CronExpr recurrence type is defined in the RecurrenceRule enum for forward compatibility but its evaluation is not yet implemented. Calling next_date() on a CronExpr event returns None. This reserves the variant in TOML manifests without blocking the current annual/monthly scheduling paths.
# CronExpr — planned, not yet evaluated
recurrence = { type = "CronExpr", expr = "0 9 1 * *" }
When CronExpr evaluation is implemented it will use the cron crate and integrate with the existing upcoming() method without changes to the TOML format.
Xero Integration
Xero is a supervised accounting integration, not a raw credential surface for models. Agents should interact through ledgerr_xero actions and supervised worker processes while l3dg3rr owns credentials, approvals, audit, and process supervision.
Capability Boundary
The current MCP family is ledgerr_xero. It exposes catalog and linkage operations:
- authorization URL generation and code exchange
- contacts, accounts, bank accounts, and invoice fetches
- contact search
- local entity linking
- catalog synchronization
fn xero_auth() -> supervised_token_store
fn supervised_token_store() -> fetch_catalog
fn fetch_catalog() -> link_entities
fn link_entities() -> reconciliation_candidates
fn reconciliation_candidates() -> operator_review
flowchart TD
xero_auth["xero_auth"]
supervised_token_store["supervised_token_store"]
fetch_catalog["fetch_catalog"]
link_entities["link_entities"]
reconciliation_candidates["reconciliation_candidates"]
operator_review["operator_review"]
xero_auth --> supervised_token_store
supervised_token_store --> fetch_catalog
fetch_catalog --> link_entities
link_entities --> reconciliation_candidates
reconciliation_candidates --> operator_review
Credential Model
Long-lived secrets should be mediated by the host credential abstraction, with Windows Credential Manager as the first practical backend. .env is acceptable for local bootstrap and tests, but it is not the target long-term secret model.
Reconciliation Use
Xero data should enrich local evidence rather than replace local-first records:
- local statements and workbook remain primary for tax preparation
- Xero contacts/accounts provide counterparties and entity hints
- reconciliation gates compare extracted totals, local postings, and remote accounting facts
- link decisions should be audit-visible and reversible where practical
Related Chapters
Theory of Operation
This chapter documents the novel architecture patterns that power l3dg3rr’s AI agent governance system.
Related Chapters
- Graph Data Model - Node and edge definitions
- Force Layout - Force-directed positioning
- Isometric Projection - 3D to 2D mapping
- Pipeline - Type-state workflow
- Validation - Confidence accumulation
- Visualization - Mermaid/HTML export
- Match Visualization Plan - multi-arm branch rendering contract
- Legal Verification - Tax rule verification
- Constraints - Kasuari constraints
The Novel Theory of Tool Pattern
Concept
Traditional tool use in LLM agents treats tools as stateless functions that transform inputs to outputs. The Novel Theory of Tool (NTTP) pattern instead treats tools as stateful instruction executors with:
// Rhai patterns auto-parse to Mermaid
fn ingest() -> validate
fn validate() -> classify
fn classify() -> reconcile
fn reconcile() -> commit
flowchart TD
ingest["ingest"]
validate["validate"]
classify["classify"]
reconcile["reconcile"]
commit["commit"]
ingest --> validate
validate --> classify
classify --> reconcile
reconcile --> commit
Conditional flow:
if confidence > 0.8 -> commit
if confidence > 0.5 -> reconcile
if confidence <= 0.5 -> review
if review == approved -> classify
flowchart TD
confidence_gt_0_8{"confidence > 0.8"}
commit["commit"]
confidence_gt_0_5{"confidence > 0.5"}
reconcile["reconcile"]
confidence____0_5{"confidence <= 0.5"}
review["review"]
review____approved{"review == approved"}
classify["classify"]
confidence_gt_0_8 -->|"true"|commit
confidence_gt_0_8 -->|"false"|confidence_gt_0_5
confidence_gt_0_5 -->|"true"|reconcile
confidence____0_5 -->|"true"|review
review____approved -->|"true"|classify
- Composable instruction streams - Tools accept not just data, but instructions that modify their behavior
- Idempotent re-execution - Tools can be safely re-run with the same inputs, producing deterministic outputs
- Content-addressed identity - All outputs are identified by cryptographic hashes of their inputs
- Audit-native design - Every tool execution produces traceable evidence
Executable Pattern Example
#![allow(unused)]
fn main() {
use ledger_core::{graph::*, layout::*, render::*};
// Create a pipeline graph
let nodes = create_pipeline_nodes();
let edges = create_pipeline_edges();
// Initialize force layout
let mut layout = ForceLayout::for_pipeline();
// Run simulation
for _ in 0..50 {
layout.tick();
}
// Render to screen coordinates
let renderer = GraphRenderer::new(800, 600);
for (idx, _) in nodes.iter().enumerate() {
if let Some(pos) = layout.position(idx) {
let screen = renderer.screen_position(pos.x, pos.y, pos.z);
println!("Node {} -> ({:.1}, {:.1})", idx, screen.x, screen.y);
}
}
}
Comparison
| Traditional Tool Use | Novel Theory of Tool |
|---|---|
fn(input) -> output | fn(instruction, state) -> (output, evidence) |
| Stateless | Stateful with checkpointing |
| Random UUIDs | Content-hash IDs |
| Best-effort | Deterministic/auditable |
System Architecture Diagram
fn human_accountant() -> tray_icon
fn human_accountant() -> window_ui
fn tray_icon() -> slint_graph_view
fn window_ui() -> slint_graph_view
fn toast_notifier() -> slint_graph_view
fn credential_manager() -> slint_graph_view
fn slint_graph_view() -> pipeline_hsm
fn pipeline_hsm() -> validation
fn pipeline_hsm() -> legal_solver
fn pipeline_hsm() -> constraints
fn pipeline_hsm() -> ledgerr_documents
fn pipeline_hsm() -> ledgerr_review
fn pipeline_hsm() -> ledgerr_reconciliation
flowchart TD
human_accountant["human_accountant"]
tray_icon["tray_icon"]
window_ui["window_ui"]
slint_graph_view["slint_graph_view"]
toast_notifier["toast_notifier"]
credential_manager["credential_manager"]
pipeline_hsm["pipeline_hsm"]
validation["validation"]
legal_solver["legal_solver"]
constraints["constraints"]
ledgerr_documents["ledgerr_documents"]
ledgerr_review["ledgerr_review"]
ledgerr_reconciliation["ledgerr_reconciliation"]
human_accountant --> tray_icon
human_accountant --> window_ui
tray_icon --> slint_graph_view
window_ui --> slint_graph_view
toast_notifier --> slint_graph_view
credential_manager --> slint_graph_view
slint_graph_view --> pipeline_hsm
pipeline_hsm --> validation
pipeline_hsm --> legal_solver
pipeline_hsm --> constraints
pipeline_hsm --> ledgerr_documents
pipeline_hsm --> ledgerr_review
pipeline_hsm --> ledgerr_reconciliation
Pipeline Flow Diagram
fn ingested() -> validating
fn validating() -> classifying
fn classifying() -> reconciling
fn reconciling() -> committed
if low_confidence == true -> needs_review
if approved == true -> classifying
flowchart TD
ingested["ingested"]
validating["validating"]
classifying["classifying"]
reconciling["reconciling"]
committed["committed"]
low_confidence____true{"low_confidence == true"}
needs_review["needs_review"]
approved____true{"approved == true"}
ingested --> validating
validating --> classifying
classifying --> reconciling
reconciling --> committed
low_confidence____true -->|"true"|needs_review
approved____true -->|"true"|classifying
LLM Verification Pattern
fn proposer_llm() -> decision_store
fn decision_store() -> reviewer_llm
if reviewer_agreed == true -> accepted_result
if reviewer_agreed == false -> human_review
fn human_review() -> accepted_result
flowchart TD
proposer_llm["proposer_llm"]
decision_store["decision_store"]
reviewer_llm["reviewer_llm"]
human_review["human_review"]
accepted_result["accepted_result"]
reviewer_agreed____true{"reviewer_agreed == true"}
reviewer_agreed____false{"reviewer_agreed == false"}
proposer_llm --> decision_store
decision_store --> reviewer_llm
human_review --> accepted_result
reviewer_agreed____true -->|"true"|accepted_result
reviewer_agreed____false -->|"true"|human_review
Executable LLM Pipeline Integration
Proposer/Reviewer Pattern
The verification system uses a two-model approach for classification quality:
#![allow(unused)]
fn main() {
use ledger_core::verify::Verifier;
use ledger_core::validation::{Disposition, MetaCtx};
// Initialize verifier with two models
let verifier = Verifier::new(proposer_model.clone(), reviewer_model.clone());
// Propose classification
let proposal = verifier.propose(&transaction, "OfficeSupplies");
// Reviewer evaluates
let review = verifier.review(&proposal);
// Combine into result with confidence
let confidence = if review.agreed {
proposal.confidence * 0.95 // High agreement boost
} else {
proposal.confidence * 0.5 // Disagreement penalty
};
}
Multi-Stage Classification Flow (Executable)
#![allow(unused)]
fn main() {
use ledger_core::{graph::*, layout::*, pipeline::*, validation::*};
// Complete pipeline execution
fn run_pipeline(document_path: &str) -> Result<PipelineState<Committed>, Issue> {
// Stage 1: Ingest
let data = std::fs::read(document_path)?;
let tx_id = blake3::hash(&data).to_hex();
let state = PipelineState::new(Ingested { tx_id, data });
// Stage 2: Validate (Kasuari constraints + Z3 legal)
let ctx = MetaCtx::default();
let validated = state.validate(&ctx)?;
// Stage 3: Classify (LLM → Reviewer → Human if needed)
let classified = validated.classify("OfficeSupplies".to_string())?;
// Stage 4: Reconcile (Xero)
let reconciled = classified.reconcile(Some(xero_id))?;
// Stage 5: Commit (Audit log + schedule)
Ok(reconciled.commit()?)
}
}
Isometric Visualization (Executable)
3D Force Layout to 2D Screen
fn node_a_3d() -> isometric_projection
fn node_b_3d() -> isometric_projection
fn node_c_3d() -> isometric_projection
fn isometric_projection() -> screen_coordinates
flowchart TD
node_a_3d["node_a_3d"]
isometric_projection["isometric_projection"]
node_b_3d["node_b_3d"]
node_c_3d["node_c_3d"]
screen_coordinates["screen_coordinates"]
node_a_3d --> isometric_projection
node_b_3d --> isometric_projection
node_c_3d --> isometric_projection
isometric_projection --> screen_coordinates
State Visualization Mapping
#![allow(unused)]
fn main() {
use ledger_core::{graph::*, layout::*, render::*};
// Full visualization pipeline
fn visualize_pipeline() -> String {
// 1. Create graph data
let nodes = create_pipeline_nodes();
// 2. Run force-directed layout
let mut layout = ForceLayout::for_pipeline();
for _ in 0..100 { layout.tick(); }
// 3. Render to screen coordinates
let renderer = GraphRenderer::new(800, 600);
let mut positions = Vec::new();
for (idx, node) in nodes.iter().enumerate() {
if let Some(pos) = layout.position(idx) {
let screen = renderer.screen_position(pos.x, pos.y, pos.z);
positions.push((node.label.clone(), screen));
}
}
// 4. Generate Mermaid diagram
let mut mermaid = String::from("stateDiagram-v2\n");
for (label, _) in &positions {
mermaid.push_str(&format!(" {}: {}\n", label, label));
}
mermaid
}
}
State Visualization Mapping
| Pipeline State | Visual Node | Color | Animation |
|---|---|---|---|
| Idle | Empty circle | #f0f0f0 | None |
| Active | Filled circle | #4a90d9 | Pulse |
| Success | Checkmark | #4caf50 | Check |
| Warning | Triangle | #ff9800 | Shake |
| Error | X mark | #f44336 | Blink |
| Review | Star | #9c27b0 | Bounce |
Integration Test Recipes
CI/CD Test Matrix
# .github/workflows/ci.yml
test-recipes:
- name: e2e-mvp
command: ./scripts/e2e_mvp.sh
validates: full ingest → classify → audit → schedule
- name: visualization-render
command: cargo test --package ledgerr-host visualization_e2e
validates: isometric graph rendering
- name: mdbook-build
command: just docgen-check
validates: documentation generation plus live Rhai editor assets
- name: mcp-surface-contract
command: cargo run -p xtask-mcpb -- generate-mcp-artifacts
validates: MCP tool contract matches code
Executable Documentation Tests
#![allow(unused)]
fn main() {
// Tests that verify documentation examples work
#[cfg(test)]
mod doc_tests {
use ledger_core::{graph::*, layout::*, render::*, visualize::*};
#[test]
fn test_force_layout_tick() {
let mut layout = ForceLayout::for_pipeline();
let initial = layout.position(0);
layout.tick();
// Position should change after tick
assert_ne!(initial, layout.position(0));
}
#[test]
fn test_isometric_projection() {
let renderer = GraphRenderer::new(800, 600);
// Center position should map near origin
let center = renderer.screen_position(0.0, 0.0, 0.0);
assert!((center.x - 400.0).abs() < 1.0);
assert!((center.y - 300.0).abs() < 1.0);
}
#[test]
fn test_pipeline_state_transitions() {
let state = PipelineState::new(Ingested {
tx_id: "test123".to_string(),
data: vec![1, 2, 3]
});
let ctx = MetaCtx::default();
let validated = state.validate(&ctx).unwrap();
assert!(matches!(validated, PipelineState::Validating(_)));
}
}
}
Multi-Jurisdiction Tax Rules
Jurisdiction Activation
#![allow(unused)]
fn main() {
use ledger_core::legal::{us_schedule_c, LegalSolver, TransactionFacts, Z3Result};
let solver = LegalSolver::new();
let rule = us_schedule_c::rule_ordinary_necessary();
let mut facts = TransactionFacts::new();
facts.is_business_activity = Some(true);
facts.is_ordinary = Some(true);
facts.is_necessary = Some(false);
let result = solver.verify(&rule, &facts);
match result {
Z3Result::Satisfied => println!("rule satisfied"),
Z3Result::Violated { witness } => println!("blocked: {witness}"),
Z3Result::Unknown => println!("more facts required"),
}
}
The visualization plan for this kind of multi-arm branch is documented in Match Visualization Plan. Branch-heavy examples should include a fan-out sample that keeps arm order stable across Mermaid and isometric views.
Content-Hash Identity Model
Idempotent Ingest (Executable)
#![allow(unused)]
fn main() {
use blake3::hash;
// Generate deterministic transaction ID
fn compute_tx_id(account: &str, date: &str, amount: f64, desc: &str) -> String {
let input = format!("{}|{}|{}|{}", account, date, amount, desc);
hash(input.as_bytes()).to_hex().to_string()
}
// Example: Same inputs produce same ID (idempotent)
let id1 = compute_tx_id("WF-BH-CHK", "2024-01-15", 150.00, "Office Depot");
let id2 = compute_tx_id("WF-BH-CHK", "2024-01-15", 150.00, "Office Depot");
assert_eq!(id1, id2); // Idempotent!
}
Content-Hash Flow
fn account_date_amount_desc() -> blake3_hasher
fn blake3_hasher() -> hex_64_chars
fn hex_64_chars() -> tx_id_stored
flowchart TD
account_date_amount_desc["account_date_amount_desc"]
blake3_hasher["blake3_hasher"]
hex_64_chars["hex_64_chars"]
tx_id_stored["tx_id_stored"]
account_date_amount_desc --> blake3_hasher
blake3_hasher --> hex_64_chars
hex_64_chars --> tx_id_stored
Workflow DSL Compilation (Executable)
TOML → Triple Compilation
#![allow(unused)]
fn main() {
use ledger_core::workflow::{WorkflowToml, compile_mermaid, compile_rhai, compile_rust_enum};
// Parse TOML workflow definition
let toml_str = r#"
[[state]]
id = "Ingested"
[[state]]
id = "Validating"
[[transition]]
from = "Ingested"
to = "Validating"
"#;
let workflow: WorkflowToml = toml::from_str(toml_str).unwrap();
// Compile to three outputs
let mermaid = compile_mermaid(&workflow);
let rhai = compile_rhai(&workflow);
let rust_enum = compile_rust_enum(&workflow);
println!("Mermaid:\n{}", mermaid);
println!("\nRhai:\n{}", rhai);
println!("\nRust:\n{}", rust_enum);
}
Verb Pattern (Executable)
Reversible Operations
#![allow(unused)]
fn main() {
use ledger_core::pipeline::*;
// Ingest with idempotency
let result1 = service.ingest_statement_rows(rows.clone())?;
assert_eq!(result1.inserted_count, 1);
let result2 = service.ingest_statement_rows(rows)?; // Same rows
assert_eq!(result2.inserted_count, 0); // Idempotent - no dup!
// Classification with confidence
let updated = service.classify_transaction(ClassifyTransactionRequest {
tx_id,
category: "OfficeSupplies".to_string(),
confidence: "0.93".to_string(),
actor: "agent".to_string(),
})?;
assert_eq!(updated.category, "OfficeSupplies");
}
MCP Surface
The MCP surface is the agent-facing contract for l3dg3rr. It is intentionally smaller than the internal Rust API: agents see a compact set of capability families, each selected with a required action argument.
The source of truth is crates/ledgerr-mcp/src/contract.rs. Generated operator docs live in docs/mcp-capability-contract.md and docs/agent-mcp-runbook.md; regenerate them with cargo run -p xtask-mcpb -- generate-mcp-artifacts after changing the published surface.
There are two MCP layers in the roadmap:
| Layer | Prefix | Role |
|---|---|---|
| Bookkeeping/domain MCP | ledgerr_* | Existing financial document, workflow, audit, ontology, Xero, and workbook capabilities. |
| Desktop/controller MCP | ledgrrr_* | Claude Desktop MCPB controller for install/status/service/tray/diagram/simulation/Office actions. |
The ledgrrr_* controller surface must stay thin. It should inspect and orchestrate installed components, then delegate domain work to the existing ledgerr_* service layer.
Published Tool Families
| Tool | Capability family | Typical actions |
|---|---|---|
ledgerr_documents | intake, filename validation, raw context, tags, filesystem metadata | ingest_pdf, ingest_rows, document_inventory, normalize_filename |
ledgerr_review | rule execution, classification, review flags | run_rule, classify_ingested, query_flags, classify_transaction |
ledgerr_reconciliation | totals and posting guardrails | validate, reconcile, commit |
ledgerr_workflow | lifecycle and plugin operations | status, transition, resume, plugin_info |
ledgerr_audit | event history and audit replay | event_history, event_replay, query_audit_log |
ledgerr_tax | evidence, ambiguity review, workbook export | assist, evidence_chain, ambiguity_review, export_workbook |
ledgerr_ontology | graph/ontology query and write operations | query_path, export_snapshot, upsert_entities, upsert_edges |
ledgerr_xero | supervised Xero catalog and entity linkage | get_auth_url, fetch_contacts, link_entity, sync_catalog |
Desktop Controller Tools
PRD-11 defines a Claude Desktop MCPB bundle that runs ledgrrr-mcp as the stdio controller. The controller is not the privileged installer. It exposes explicit tools that return plans, status, and local artifact outputs.
| Tool | Responsibility |
|---|---|
ledgrrr_status | Report desktop, service, tray, model runtime, Office add-in, SharePoint, and b00t state. |
ledgrrr_install_plan | Return dry-run install/repair actions and required privilege level. |
ledgrrr_install_desktop | Launch the native Windows installer. |
ledgrrr_start_service | Start the local ledgrrr service. |
ledgrrr_stop_service | Stop the local ledgrrr service. |
ledgrrr_open_tray | Launch or focus tray/taskbar UI. |
ledgrrr_render_diagram | Render typed playbook models into Mermaid/SVG/PNG/HTML. |
ledgrrr_simulate_pipeline | Run deterministic or local-CPU-model simulation and return evidence summary. |
ledgrrr_export_office_artifact | Produce OneNote/Office/SharePoint-safe playbook artifacts. |
ledgrrr_repair | Repair service, tray, model runtime, Office manifests, and b00t linkage. |
ledgrrr_uninstall | Launch native uninstall or return exact removal steps. |
Every mutating controller action must support a plan-first flow and emit audit evidence. Privileged Windows operations cross the native installer/UAC boundary.
Runtime Flow
fn initialize() -> tools_list
fn tools_list() -> choose_capability
fn choose_capability() -> call_action
fn call_action() -> service_dispatch
fn service_dispatch() -> audit_event
if action == commit -> approval_gate
if action == export_workbook -> workbook_projection
flowchart TD
initialize["initialize"]
tools_list["tools_list"]
choose_capability["choose_capability"]
call_action["call_action"]
service_dispatch["service_dispatch"]
audit_event["audit_event"]
action____commit{"action == commit"}
approval_gate["approval_gate"]
action____export_workbook{"action == export_workbook"}
workbook_projection["workbook_projection"]
initialize --> tools_list
tools_list --> choose_capability
choose_capability --> call_action
call_action --> service_dispatch
service_dispatch --> audit_event
action____commit -->|"true"|approval_gate
action____export_workbook -->|"true"|workbook_projection
Layering
The transport adapter should not redefine business behavior. It parses the published shape, normalizes boundary variance, and dispatches to TurboLedgerService.
ledgerr-mcp-server: stdio transport.contract: published tool families, actions, generated JSON Schema.mcp_adapter: request parsing, envelope shaping, compatibility aliases.TurboLedgerService: domain behavior, state, audit, lifecycle.ledger-core: deterministic financial primitives.
Compatibility Rule
Hidden legacy l3dg3rr_* and proxy names may continue to parse, but documentation and examples should use ledgerr_* only. Drift between contract.rs and generated docs is a test failure, not a manual documentation chore.
Related Chapters
- Capability Map
- Document Ingestion
- Workbook & Audit
- Xero Integration
- Desktop Agent and Office Playbook Surface
Desktop Agent and Office Playbook Surface
Ledgrrr’s desktop direction is a local-first Windows control plane for process modelling, visualization, simulation, and approval. The desktop stack is not only a bookkeeping UI. It is the durable host for a local service, tray/taskbar controls, Claude Desktop MCPB integration, Microsoft 365 diagram surfaces, and b00t-controlled orchestration playbooks.
See PRD-11 for the full requirements and definition of done.
Present State
| Area | Current state |
|---|---|
| MCP server | ledgerr-mcp-server exposes ledgrrr capability families over stdio. |
| Claude plugin | The repo has a Claude plugin marketplace entry for Cowork/plugin workflows. |
ledgrrr-mcp controller | Implemented (crates/ledgerr-desktop-agent). Real stdio JSON-RPC server exposing exactly 11 ledgrrr_* desktop-control tools; it is distinct from the 12 published ledgerr_* ledger capability families. |
| Claude Desktop MCPB bundle | Implemented. scripts/package-desktop-agent.sh / just package-desktop-mcpb builds dist/ledgrrr-claude.mcpb/. It includes only the unprivileged controller and visible package helper—not a signing key, service, or silent installer. |
ledgrrr-service | Durable local runtime with an authenticated loopback health/shutdown endpoint, shared per-user config/audit schema, and an explicit per-user fallback. The package may later register it with SCM for machine scope. |
| Windows desktop host | Tauri host is the tray/taskbar executable in the external payload; ledgrrr_open_tray discovers the installed ledgrrr-tray.exe before compatibility names. |
| Native Windows dogfood package | Implemented: a test-signed sparse MSIX/external-location identity package, external Win32 payload, install/repair/uninstall helper, checksum, provenance, and Windows release-CI smoke. Public certificate procurement is deferred. |
| Diagram rendering | Mermaid, JSON, and a minimal deterministic SVG layout are implemented and golden-tested in ledgerr-desktop-agent for the ledgrrr_render_diagram tool. PNG remains unsupported pending a rasterizer dependency. Mermaid/isometric documentation rendering for the Rhai diagram DSL is separately implemented in the mdBook tooling. |
| Deterministic simulation | Implemented: ledgrrr_simulate_pipeline walks a playbook’s nodes/edges/gates with no LLM and no wall-clock dependency, producing a reproducible run id, step trace, and gate decisions (golden-tested). |
| Office/SharePoint | Not packaged as OneNote/Office add-in or SPFx web part — requires a Microsoft 365/SharePoint tenant. ledgrrr_export_office_artifact writes a local, version-numbered bundle (Mermaid + SVG + playbook JSON + provenance) that a future Office/SPFx bridge can consume. |
| b00t linkage | Source-controlled package and Tauri datums live in .b00t/datums/; installed desktop/runtime state is reported by ledgrrr_status. Office, SharePoint, and model integrations remain status-visible but unconfigured. |
Target Architecture
Claude Desktop
-> ledgrrr-claude.mcpb
-> ledgrrr-mcp.exe --stdio
-> ledgrrr-service.exe
-> ledgrrr-tray.exe
-> local model runtime
-> b00t capability index
-> ledger-core / ledgerr-mcp / visualization crates
OneNote / Office Add-in
-> local service or exported artifact bridge
-> diagram renderer
-> evidence graph
-> workbook/playbook store
SharePoint SPFx Web Part
-> stored diagram artifact
-> optional signed refresh link / local handoff
Installation Boundary
MCPB installs the Claude-facing controller only. It must not silently install a service, write machine-wide registry keys, or mutate host state during bundle install.
The authoritative Windows delivery is a sparse MSIX/external-location identity package. Its package identity enables Windows integration while its external payload owns the Win32 stack:
ledgrrr-service.exeledgrrr-tray.exeledgrrr-mcp.exesupport-manifest.json(binary names, per-user state contract, prerequisites)- local model/runtime assets
- WebView2/Tauri prerequisites
- Start Menu entries
- repair/uninstall registration
- update metadata
Per-user install is the dogfood default and requires no UAC: it installs the test certificate into Current User Trusted People, copies the external payload under %LOCALAPPDATA%\Programs\ledgrrr, and registers the MSIX identity with Add-AppxPackage -ExternalLocation. It writes %LOCALAPPDATA%\ledgrrr\package-install.json so an MCPB controller installed elsewhere can locate that payload, and caches the public MSIX/certificate under %LOCALAPPDATA%\ledgrrr\package-cache so repair can re-register the identity without a new download. Machine scope surfaces UAC and stages/provisions the package. Uninstall removes the external payload, install record, and package cache; runtime audit/config data remain under %LOCALAPPDATA%\ledgrrr unless the operator chooses to remove their personal data separately.
The repeatable Windows commands are:
just windows-package <Build|TestInstall> <windows-repo-root> <version> [output-dir] [certificate-store-path]
just wsl2-pwsh-msix-build <windows-repo-root> <version>
just wsl2-pwsh-msix-smoke <windows-repo-root> <version>
Use the first recipe when composing an automation flow. TestInstall is the
single mutating dogfood command: it keeps package changes behind one visible
Windows/UAC boundary instead of asking an operator to approve each lifecycle
step individually. Controller MCP tools remain plan-first and require explicit
approval for a user-initiated install, repair, or uninstall.
The smoke path builds, test-signs, installs, discovers, and uninstalls the identity package. Per-user dogfood registration uses the explicit Add-AppxPackage -AllowUnsigned test mode so a self-signed root CA is never installed; this is not a public-signing substitute and is not used for machine-wide installation. Release CI uploads the .msix, public .cer, external-payload.zip, INSTALL.json, checksum, and provenance next to (not instead of) the domain MCPB artifacts. Extract external-payload.zip to a sibling payload directory before running the command in INSTALL.json.
Windows Toolchain Prerequisites
The package command uses PowerShell 7 (pwsh.exe; 7.2+), including when invoked
from WSL, not only a manually opened Developer PowerShell. Install it with
winget install --id Microsoft.PowerShell --source winget if it is absent.
When P: is available, the test signing PFX and matching public certificate
persist under P:\ledgrrr\test-signing; release outputs contain only the public
.cer. Use -CertificateStorePath to select another private signer location.
Prefer a WSLC Windows container for repeatable Windows toolchain work when the
installed b00t exposes that capability; otherwise stage a local Windows copy.
It activates the Visual Studio environment through VsDevCmd.bat when needed.
Before a local build or smoke test, install:
- Windows 10 version 2004 / build 19041 or newer;
- Visual Studio 2022 Build Tools with Desktop development with C++ (the
Microsoft.VisualStudio.Workload.VCToolsworkload) and its recommended Windows SDK components, which providelink.exe,makeappx.exe, andsigntool.exe; - the Microsoft Edge WebView2 Evergreen Runtime (required when the tray is installed); and
- the built-in Windows Appx PowerShell module.
The package build detects a missing or stale mdBook toolchain and repairs the
mdbook-admonish asset/binary mismatch automatically. It then embeds the
generated playbook in the external payload, where the tray’s /docs/ route
discovers it beside ledgrrr-tray.exe. Offline builds should preinstall
mdbook 0.5.x, mdbook-admonish 1.20.x, and the repo-local
mdbook-rhai-mermaid binary. The legacy mdbook-mermaid 0.16 preprocessor is
not compatible with mdBook 0.5 and is intentionally not used.
On a clean Windows machine, install the compiler workload with:
winget install --id Microsoft.VisualStudio.2022.BuildTools --exact --source winget --accept-package-agreements --accept-source-agreements --silent --override "--wait --passive --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended"
Then verify the C++ workload and run the acceptance path (substitute the checkout’s Windows path when launching from WSL):
$vswhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe'
& $vswhere -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath
.\scripts\windows-package.ps1 -Action TestInstall -Version 1.9.0
If a repaired/offline Build Tools instance has VsDevCmd.bat but is not yet
reported by vswhere, set LEDGRRR_VSDEVCMD to that file before invoking the
package script. The script also probes the normal Build Tools location and the
documented C:\BuildTools recovery location.
Native Windows Cargo does not support a \\wsl.localhost\... checkout as its
working directory. Keep a normal Windows clone under a drive such as C:\src
or D:\src for desktop packaging. If the source only exists in the WSL Linux
filesystem, stage a temporary local copy (excluding .git, target, dist,
.claude, and optional models) before running the command; copy artifacts
back only after the smoke succeeds. This is a Windows/Cargo constraint, not an
installer permission requirement.
TestInstall builds the package, generates a test certificate, creates the
MSIX/checksum/provenance/payload archive, installs per-user, calls the
controller’s status/start/render/stop actions, repairs, and uninstalls. It is
safe for dogfood only: the certificate is public/test-only and must be replaced
by public signing before distribution outside the test channel.
Required MCPB Tools
Governed Process State Machines
PlaybookModel is also the deterministic process-state-machine contract. A
task state may declare its executing role, the b00t capability it invokes, and
the stable outcome it emits. Validation fails closed when a state invokes a
capability absent from capability_refs, or when its role lacks that capability
in role_authorizations. This is the executable form of the invariant that a
process cannot exceed its declared tools.
The checked-in b00t learn ooda fixture represents Observe → Orient → Decide
→ Act as states with outcome-labelled transitions. Render it through
ledgrrr_render_diagram with format: "state-machine"; simulate it through
ledgrrr_simulate_pipeline with the deterministic profile. The resulting trace
records each state, role, capability, evidence id, gate decision, and outcome
without a model call or wall-clock dependency.
All 11 tools below are implemented in ledgrrr-mcp (crates/ledgerr-desktop-agent) and covered by contract tests. Package mutations are plan-first: ledgrrr_install_plan exposes affected paths, scope, unattended command, and UAC boundary; install_desktop/repair/uninstall require approved: true and launch the actual visible PowerShell package workflow.
| Tool | Purpose |
|---|---|
ledgrrr_status | Report installed desktop, service, tray, model, Office, and b00t state. |
ledgrrr_install_plan | Return a dry-run install/repair plan and privilege requirements. |
ledgrrr_install_desktop | Launch the signed native installer. |
ledgrrr_start_service | Start the service if installed. |
ledgrrr_stop_service | Stop the service. |
ledgrrr_open_tray | Launch or focus tray/taskbar UI. |
ledgrrr_render_diagram | Render Mermaid/SVG/PNG/HTML from a typed playbook. |
ledgrrr_simulate_pipeline | Run local deterministic or model-assisted simulation. |
ledgrrr_export_office_artifact | Produce OneNote/SharePoint-safe diagram artifacts. |
ledgrrr_repair | Repair service/tray/model/Office integration. |
ledgrrr_uninstall | Launch the native uninstaller or return exact removal steps. |
Office and SharePoint Surface
The diagram generator is the control surface for AI-generated process models.
| Surface | Role |
|---|---|
| OneNote/Office Add-in | Task pane for generating, previewing, inserting, and refreshing diagrams/playbooks. |
| SPFx web part | SharePoint rendering surface for published playbook artifacts. |
| Local service bridge | Converts playbook JSON into Mermaid, SVG, PNG, HTML, and provenance metadata. |
Office artifacts must be versioned. Refreshing a generated diagram creates a new artifact version and evidence node; it must not silently replace a previously published diagram.
b00t Contract
The source-controlled b00t package/Tauri datums are in .b00t/datums/; the
other integration names below are status vocabulary, not falsely advertised
installed packages:
ledgrrr.cli— pre-existing FOCUS/transport datum for the vendored checkout.ledgrrr.mcp— theledgrrr-mcpcontroller surface (this document),tool_prefix = "ledgrrr_".ledgrrr.service— the authenticated per-user runtime boundary,type = "runtime"; a machine service registration remains an elevated package option.ledgrrr.desktop— the test-signed sparse-MSIX package contract (.b00t/datums/ledgrrr.toml), with installed state reported byledgrrr_status.desktop_package.ledgrrr.office-addin/ledgrrr.sharepoint-webpart— Office/SharePoint overlays,type = "overlay", markedstatus = "missing"pending a Microsoft 365 tenant.ledgrrr.model-runtime— local CPU inference profile,type = "ai", marked missing;ledgrrr_status.model_runtime.configuredstaysfalseuntilLEDGRRR_MODEL_RUNTIME_PROFILEis set to something real.
There is no dedicated b00t ledgrrr <verb> subcommand in b00t-cli — that would require changes to the separate b00t-cli crate, out of scope here. What works today is the generic guard-enforced execution path plus the datum registry:
b00t learn ledgrrr-desktop-agent
b00t capabilities # lists the published ledgrrr.* datums
b00t exec ledgrrr_status # invoke the controller tool through the guard/audit path
Every ledgrrr_* tool call returns structured JSON (see tests/contract.rs); ledgrrr_simulate_pipeline additionally returns deterministic evidence ids per step.
Future Definition of Done
The desktop/Office future state is done when:
- Claude Desktop installs
ledgrrr-claude.mcpbandledgrrr_statusworks. - A signed/test-signed native Windows package installs service, tray, controller, model config, repair, and uninstall.
- The tray UI shows service, model, MCPB, Office, and b00t status.
- OneNote/Office can insert a generated diagram artifact.
- SharePoint can render the same playbook artifact via SPFx.
- Local CPU inference can generate or mutate playbooks without cloud access.
- Deterministic non-LLM simulation exists for CI and audit replay.
- CI validates MCPB, Windows build, Office/SPFx manifests, diagram golden outputs, and b00t JSON contracts.
- Release CI uploads MCPB, Windows installer, checksums, and provenance metadata.
Pipeline
The pipeline module implements a type-state pattern for the document processing workflow.
Related Chapters
- Validation - Disposition and confidence types
- Visualization - PipelineGraph rendering
- Workflow - TOML DSL compilation
- Verify - Multi-model verification
State Types
fn ingested() -> validating
fn validating() -> classifying
fn classifying() -> reconciling
fn reconciling() -> committed
if confidence < 0.5 -> needs_review
if human_approved == true -> classifying
if fatal_error == true -> error
flowchart TD
ingested["ingested"]
validating["validating"]
classifying["classifying"]
reconciling["reconciling"]
committed["committed"]
confidence_lt_0_5{"confidence < 0.5"}
needs_review["needs_review"]
human_approved____true{"human_approved == true"}
fatal_error____true{"fatal_error == true"}
error["error"]
ingested --> validating
validating --> classifying
classifying --> reconciling
reconciling --> committed
confidence_lt_0_5 -->|"true"|needs_review
human_approved____true -->|"true"|classifying
fatal_error____true -->|"true"|error
- Ingested: Document has been parsed
- Validating: Checking data integrity
- Classifying: Categorizing transactions
- Reconciling: Matching against external data
- Committed: Finalized andauditable
- NeedsReview: Awaiting human approval
Type-State Pattern
The pipeline uses Rust’s type system to enforce state transitions at compile time:
#![allow(unused)]
fn main() {
pub struct PipelineState<S> { /* ... */ }
impl PipelineState<Ingested> {
pub fn validate(self) -> PipelineState<Validating> { ... }
}
impl PipelineState<Validating> {
pub fn classify(self, category: String) -> PipelineState<Classified> { ... }
}
}
This ensures invalid state transitions are caught at compile time.
Statig Integration
The pipeline uses statig for hierarchical state machine (HSM) implementation with:
- Superstates for grouping related states
- State-local storage for context
- Async-first design
Workflow
The workflow module provides a TOML-based DSL for defining pipeline stages and transitions.
WorkflowToml
#![allow(unused)]
fn main() {
pub struct WorkflowToml {
pub version: String,
pub state: Vec<StateDef>,
pub transitions: Vec<TransitionDef>,
}
}
StateDef
#![allow(unused)]
fn main() {
pub struct StateDef {
pub id: String,
pub description: String,
pub verbs: Vec<VerbDef>,
}
}
TransitionDef
#![allow(unused)]
fn main() {
pub struct TransitionDef {
pub from: String,
pub to: String,
pub event: String,
pub guard: Option<String>,
}
}
Compilation
The workflow DSL compiles to:
- Mermaid: Visual diagram generation
- Rhai: Runtime execution FSM
- Rust enum: Compile-time type safety
Example
[workflow]
version = "1.0"
[[state]]
id = "Ingested"
description = "Document parsed"
[[state]]
id = "Validating"
description = "Checking integrity"
[[transition]]
from = "Ingested"
to = "Validating"
event = "validate"
Constraints
The constraints module handles plausibility checks and visual placement rules. It deliberately sits next to, but separate from, Legal Verification.
Constraint Families
fn transaction_input() -> vendor_constraints
fn transaction_input() -> invoice_arithmetic
fn pipeline_graph() -> layout_constraints
fn vendor_constraints() -> constraint_evaluation
fn invoice_arithmetic() -> constraint_evaluation
fn layout_constraints() -> visualization_model
match constraint_evaluation => Required -> block_pipeline
match constraint_evaluation => Strong -> recoverable_issue
match constraint_evaluation => Medium -> warning_issue
match constraint_evaluation => Weak -> advisory_issue
flowchart TD
transaction_input["transaction_input"]
vendor_constraints["vendor_constraints"]
invoice_arithmetic["invoice_arithmetic"]
pipeline_graph["pipeline_graph"]
layout_constraints["layout_constraints"]
constraint_evaluation["constraint_evaluation"]
visualization_model["visualization_model"]
match_constraint_evaluation{"match constraint_evaluation"}
block_pipeline["block_pipeline"]
recoverable_issue["recoverable_issue"]
warning_issue["warning_issue"]
advisory_issue["advisory_issue"]
transaction_input --> vendor_constraints
transaction_input --> invoice_arithmetic
pipeline_graph --> layout_constraints
vendor_constraints --> constraint_evaluation
invoice_arithmetic --> constraint_evaluation
layout_constraints --> visualization_model
match_constraint_evaluation -->|"Required"|block_pipeline
match_constraint_evaluation -->|"Strong"|recoverable_issue
match_constraint_evaluation -->|"Medium"|warning_issue
match_constraint_evaluation -->|"Weak"|advisory_issue
Kasuari Use
Kasuari-style strengths are used for constraints where failure is graded:
- Required: must pass before the pipeline proceeds.
- Strong: recoverable issue; normally needs repair or review.
- Medium: warning; may proceed with an audit note.
- Weak: advisory signal.
This is appropriate for vendor plausibility and document-shape expectations because historical data is rarely a hard legal proof.
Z3 Boundary
Use Z3 when the application needs proof-like yes/no behavior:
- tax rule satisfaction
- reconciliation balance equations
- workbook export invariants
- mutually exclusive classifications
- workflow transition guards
Use this module’s constraint evaluation when the question is “how plausible is this value?” rather than “is this formula satisfiable?”
VendorConstraintSet
#![allow(unused)]
fn main() {
pub struct VendorConstraintSet {
pub vendor: String,
pub constraints: Vec<Constraint>,
}
}
Typical checks:
- amount range
- date window
- description pattern
- account format
InvoiceConstraintSolver
InvoiceConstraintSolver checks invoice arithmetic such as subtotal, tax, and total consistency. Today it is a lightweight plausibility solver; future work can route strict arithmetic proof obligations through Z3 where audit explanations need counterexamples.
LayoutSolver
The visualization system also uses constraints to keep graph nodes readable. Match arms, default lanes, and rejoin points are layout constraints, not financial constraints.
Related Chapters
Verification
The verify module implements multi-model verification for transaction classification and rule repair.
Multi-Model Verification Flow
fn transaction_input() -> proposer_model
fn proposer_model() -> decision_store
fn decision_store() -> reviewer_model
if reviewer_agreed == true -> accepted_result
if reviewer_agreed == false -> human_review
fn human_review() -> accepted_result
flowchart TD
transaction_input["transaction_input"]
proposer_model["proposer_model"]
decision_store["decision_store"]
reviewer_model["reviewer_model"]
human_review["human_review"]
accepted_result["accepted_result"]
reviewer_agreed____true{"reviewer_agreed == true"}
reviewer_agreed____false{"reviewer_agreed == false"}
transaction_input --> proposer_model
proposer_model --> decision_store
decision_store --> reviewer_model
human_review --> accepted_result
reviewer_agreed____true -->|"true"|accepted_result
reviewer_agreed____false -->|"true"|human_review
The system uses a two-model approach:
- Proposer: Primary model generates the classification/decision via
ModelClient::complete() - Decision Store: Intermediate representation of the proposal
- Reviewer: Second model reviews and validates the proposal
- Outcome: Either
VerificationOutcome::ApprovedorVerificationOutcome::Rejected
Core Types
MultiModelVerifier
#![allow(unused)]
fn main() {
pub struct MultiModelVerifier<C: ModelClient> {
proposer: C,
reviewer: C,
config: MultiModelConfig,
}
}
ModelClient Trait
#![allow(unused)]
fn main() {
pub trait ModelClient: Send + Sync {
fn complete(&self, prompt: &str, max_tokens: usize) -> anyhow::Result<String>;
fn extract<T: serde::de::DeserializeOwned>(&self, prompt: &str) -> anyhow::Result<T>;
}
}
RepairProposal
#![allow(unused)]
fn main() {
pub struct RepairProposal {
pub rule_id: String,
pub proposed_fix: String,
pub reasoning: String,
pub confidence: f32,
}
}
ReviewResult
#![allow(unused)]
fn main() {
pub struct ReviewResult {
pub approved: bool,
pub concerns: Vec<String>,
pub suggestions: Vec<String>,
pub confidence: f32,
}
}
Verification Process
- Input transaction with classification issue
- Proposal: Proposer model generates
RepairProposalwith suggested fix and confidence - Review: Reviewer model evaluates proposal, returns
ReviewResult - Decision:
- If
review.approved && review.confidence >= threshold→VerificationOutcome::Approved - Otherwise →
VerificationOutcome::Rejected(flags for human review)
- If
- Confidence score reflects agreement between both models
Usage Example
#![allow(unused)]
fn main() {
use ledger_core::verify::{MockModelClient, MultiModelConfig, MultiModelVerifier};
// Create mock models for testing
let proposer = MockModelClient::default().with_response(
r#"{"rule_id":"ForeignIncome","proposed_fix":"ForeignIncome",
"reasoning":"Foreign wire transfer","confidence":0.92}"#
);
let reviewer = MockModelClient::default().with_response(
r#"{"approved":true,"concerns":[],"suggestions":[],"confidence":0.90}"#
);
let config = MultiModelConfig::new("claude-haiku-4-5", "claude-haiku-4-5")
.with_threshold(0.80);
let verifier = MultiModelVerifier::new(proposer, reviewer, config);
// Verify a classification issue
let outcome = verifier.verify(
"ForeignIncome",
r#"[{"field":"category","value":"Unclassified","confidence":0.3}]"#,
"Wire transfer from DE employer, $5000"
)?;
match outcome {
VerificationOutcome::Approved { proposal, review } => {
println!("Approved: {} (confidence: {})", proposal.rule_id, review.confidence);
}
VerificationOutcome::Rejected { .. } => {
println!("Rejected: needs human review");
}
}
}
Testing
The verify module provides MockModelClient for testing without real API calls. The integration test test_llm_verification_proposes_category demonstrates full proposer-reviewer flow with mock clients.
Graph Data Model
The graph module provides the data structures for the pipeline visualization.
NodeData
#![allow(unused)]
fn main() {
pub struct NodeData {
pub label: String,
pub color: [f32; 3],
pub mass: f32,
}
}
- label: Human-readable name for the node (e.g., “Ingested”, “Validating”)
- color: RGB values as floats [0-1] for visualization
- mass: Relative size/weight for force-directed layout
EdgeData
#![allow(unused)]
fn main() {
pub struct EdgeData {
pub weight: f32,
}
}
-
weight: Edge strength for layout calculations
-
Layout: ForceLayout for positioning nodes
-
Visualization: PipelineGraph for rendering
Functions
create_pipeline_nodes
Returns the standard pipeline node definitions:
- Ingested
- Validating
- Classifying
- Reconciling
- Committed
create_pipeline_edges
Returns the standard pipeline connections:
- Ingested → Validating
- Validating → Classifying
- Classifying → Reconciling
- Reconciling → Committed
Ontology & Type Mesh
The ontology layer describes relationships between documents, accounts, transactions, tax categories, evidence references, workflow tags, and Xero entities. The type mesh describes how Rust values move through pipeline stages without losing identity or auditability.
Ontology Role
Ontology operations are exposed through ledgerr_ontology:
query_path: follow relationships between entitiesexport_snapshot: produce a serializable graph snapshotupsert_entities: add or update typed entitiesupsert_edges: add or update relationships
fn document_record() -> ontology_entity
fn account_record() -> ontology_entity
fn transaction() -> ontology_entity
fn xero_contact() -> ontology_entity
fn ontology_entity() -> evidence_path
fn evidence_path() -> tax_assist
flowchart TD
document_record["document_record"]
ontology_entity["ontology_entity"]
account_record["account_record"]
transaction["transaction"]
xero_contact["xero_contact"]
evidence_path["evidence_path"]
tax_assist["tax_assist"]
document_record --> ontology_entity
account_record --> ontology_entity
transaction --> ontology_entity
xero_contact --> ontology_entity
ontology_entity --> evidence_path
evidence_path --> tax_assist
Type Mesh Role
The type mesh answers a different question: which Rust values are compatible between stages, and what bridge is responsible for transforming them?
Examples:
TransactionInputbecomesIngestedTransaction,JournalTransaction, andSampleTransaction.ClassificationOutcomebecomesClassifiedTransactionand review flags.LegalRule + TransactionFactsbecomesZ3Result.VendorConstraintSetbecomesConstraintEvaluation.
Related Tables
The detailed tables are generated:
Update generated tables through the repo tooling rather than hand-editing those chapters.
Pipeline Stage Type Compatibility
Auto-generated by
cargo run -p xtask-mcpb -- generate-type-tables. Do not edit manually. Source of truth:crates/ledger-core/tests/type_mesh.rs
Stage I/O Matrix
| Stage | Input Type | Output Type | Confidence | Jurisdiction | Notes |
|---|---|---|---|---|---|
| Ingest | TransactionInput | IngestedTransaction, JournalTransaction | Deterministic (1.0) | All | Blake3 content-hash IDs |
| Validate | TransactionInput | MetaCtx | 0.0–1.0 | All | Type checks, constraint evaluation |
| Classify | SampleTransaction | ClassificationOutcome, ClassifiedTransaction | 0.0–1.0 | US/AU/UK | Rhai rule waterfall |
| Reconcile | ClassifiedTransaction | OperationResult | 0.0–1.0 | All | Xero match/diff |
| Export | OperationContext | rust_xlsxwriter::Workbook | Deterministic (1.0) | All | CPA-auditable Excel |
| Verify | RepairProposal | VerificationOutcome | 0.0–1.0 | All | Multi-model proposer/reviewer |
Cross-Stage Compatibility
| From Type | To Type | Compatible? | Bridge | Test |
|---|---|---|---|---|
TransactionInput | SampleTransaction | Yes | deterministic_tx_id() | test_transaction_input_to_sample_transaction_shape |
TransactionInput | JournalTransaction | Yes | JournalTransaction::from_input() | test_transaction_input_to_journal_shape |
ClassificationOutcome | ClassifiedTransaction | Yes | Field mapping + tx_id | test_classification_outcome_to_classified_shape |
ClassifiedTransaction | TxProjectionRow | Partial | Requires upstream context | test_classified_to_projection_row_requires_context |
Issue | MetaCtx | Yes | MetaCtx::advance() | test_validation_pipeline_mesh |
MetaCtx | StageResult<T> | Yes | and_then() combinator | test_validation_pipeline_mesh |
LegalRule + TransactionFacts | Z3Result | Yes | LegalSolver::verify() | — |
VendorConstraintSet | ConstraintEvaluation | Yes | VendorConstraintSet::evaluate() | — |
Known Type Gaps
ClassifiedTransaction → TxProjectionRow
ClassifiedTransaction lacks account_id, date, amount, description, and source_ref fields that TxProjectionRow requires. The ExportWorkbookOp must reconstruct these from the OperationContext’s upstream ingest data.
Status: Documented in test_classified_to_projection_row_requires_context. Future work should either:
- Add missing fields to
ClassifiedTransaction, or - Create an explicit
ExportContexttype that carries full row data.
StageResult<T> constructors use MetaCtx::default()
StageResult::ok() and StageResult::with_issues() initialize meta with MetaCtx::default() (confidence 0.0). The correct way to chain stages is via the and_then() combinator, which properly advances the MetaCtx with multiplicative confidence.
Status: Documented in test_validation_pipeline_mesh.
Type Invariants
All pipeline I/O types must implement Send + Sync for async dispatch:
TransactionInput✓IngestedTransaction✓SampleTransaction✓ClassificationOutcome✓ClassifiedTransaction✓JournalTransaction✓TxProjectionRow✓Issue✓MetaCtx✓
Concept Affinity Table
Auto-generated by
cargo run -p xtask-mcpb -- generate-type-tables. Maps domain concepts to their implementing types and modules.
Domain Concepts
| Concept | Primary Type | Module | Related Types | Derives |
|---|---|---|---|---|
| Transaction | TransactionInput | ingest | IngestedTransaction, JournalTransaction, SampleTransaction | Debug, Clone, PartialEq, Eq, Serialize, Deserialize |
| Classification | ClassificationOutcome | classify | ClassifiedTransaction, ClassificationBatch, ReviewFlag | Debug, Clone, PartialEq |
| Validation | Issue | validation | Disposition, IssueSource, MetaCtx, StageResult<T> | Debug, Clone, PartialEq, Serialize, Deserialize |
| Pipeline State | PipelineState<S> | pipeline | Type-state markers: Ingested, Validated, Classified, Reconciled, Committed, NeedsReview | Debug, Clone, Serialize, Deserialize |
| Legal Rule | LegalRule | legal | TransactionFacts, Z3Result, LegalSolver, Jurisdiction | Debug, Clone, Serialize, Deserialize |
| Constraint | VendorConstraintSet | constraints | ConstraintEvaluation, ConstraintStrength, InvoiceConstraintSolver | Debug, Clone, Serialize, Deserialize |
| Verification | MultiModelVerifier<C> | verify | RepairProposal, ReviewResult, VerificationOutcome, ModelClient | Debug, Clone, Serialize, Deserialize |
| Workflow | WorkflowToml | workflow | StateDecl, TransitionDecl | Debug, Clone, Deserialize, Serialize |
| Calendar | BusinessCalendar | calendar | ScheduledEvent, RecurrenceRule, CalendarError | — |
| Operation | LedgerOperation (trait) | ledger_ops | OperationContext, OperationResult, OperationKind, OperationDispatcher | — |
| Document | DocumentRecord | document | DocType, DocumentStatus, XeroLink, XeroEntityType | Debug, Clone, PartialEq, Eq, Serialize, Deserialize |
| Shape | DocumentShape | document_shape | StatementVendor, ColumnMap | Debug, Clone, Serialize, Deserialize |
| Rule Registry | RuleRegistry | rule_registry | ReqIfCandidate, DocumentChunk, SemanticRuleSelector | — |
| Workbook | TxProjectionRow | workbook | REQUIRED_SHEETS constant | Debug, Clone, PartialEq, Eq, Serialize, Deserialize |
Module Dependency Graph
ingest ──┬──> journal
├──> classify (via SampleTransaction)
└──> workbook (via TxProjectionRow)
classify ──> ledger_ops (via ClassifiedTransaction → OperationContext)
──> rule_registry (via ClassificationEngine)
validation ──> pipeline (via Issue, MetaCtx → PipelineState<S>)
legal ──┬──> pipeline (via Jurisdiction)
└──> calendar (via Jurisdiction)
constraints ──> validation (via ConstraintEvaluation → Disposition)
──> pipeline (via ConstraintStrength)
verify ──> classify (via ModelClient trait)
workflow ──> pipeline (via state machine definition)
document ──> ingest (via DocType detection)
──> document_shape (via vendor classification)
Visualization
The visualize module generates Mermaid diagrams and HTML exports for pipeline state.
Related Chapters
- Pipeline - State definitions
- Layout - Position calculations
- Render - Screen coordinate mapping
- Slint Viz - Slint integration
- Match Visualization Plan - future multi-arm branch semantics
- Desktop Agent and Office Playbook Surface - MCPB, OneNote, SharePoint, and local simulation surface
PipelineGraph
fn workflow_toml() -> pipeline_graph
fn pipeline_state() -> pipeline_graph
fn pipeline_graph() -> to_mermaid
fn pipeline_graph() -> to_html
fn to_mermaid() -> mermaid_svg
fn to_html() -> html_export
flowchart TD
workflow_toml["workflow_toml"]
pipeline_graph["pipeline_graph"]
pipeline_state["pipeline_state"]
to_mermaid["to_mermaid"]
to_html["to_html"]
mermaid_svg["mermaid_svg"]
html_export["html_export"]
workflow_toml --> pipeline_graph
pipeline_state --> pipeline_graph
pipeline_graph --> to_mermaid
pipeline_graph --> to_html
to_mermaid --> mermaid_svg
to_html --> html_export
#![allow(unused)]
fn main() {
pub struct PipelineGraph {
pub nodes: HashMap<String, NodeVisualState>,
pub edges: Vec<EdgeVisual>,
pub current_state: String,
pub accumulated_confidence: f32,
}
}
NodeVisualState
Visual states for pipeline nodes:
- Idle: Waiting, not entered
- Active: Currently executing
- Success: Completed successfully
- Warning: Recoverable issue
- Error: Unrecoverable issue
- Review: Awaiting human review
Mermaid Generation
#![allow(unused)]
fn main() {
let mermaid = graph.to_mermaid();
// Generates stateDiagram-v2
}
Live Rhai Editor
The rendered book now upgrades generated Rhai diagram blocks into a local editor + preview surface. Use just docserve, open the book in a browser, edit a supported ```rhai block, and click Regenerate to redraw the chart without changing the source markdown.
The live editor uses the same narrow diagram DSL as the mdBook preprocessor:
fn source() -> targetif expression -> targetmatch expr => Arm -> target
The preview surface exposes a two-position view slider:
isometric-3drenders an animated SVG scene with deterministic layered placement, so inserting a node causes nearby steps to visibly reflow instead of snapping in placemermaid-2dkeeps the canonical Mermaid flowchart for the same parsed graph
Diagnostics now surface line-level parse feedback:
- malformed DSL lines are marked as errors
- ignored non-DSL lines are marked as informational notes
- Mermaid failures include a concrete fallback hint to switch to the isometric view
Node visuals use an internal SVG icon library keyed by inferred workflow role (ingest, validate, classify, review, reconcile, commit, decision), and each node carries an autogenerated glTF data URI when no explicit model is supplied. The docs renderer still draws the isometric scene in SVG so it works inside mdBook without a separate WebGL runtime.
Model-Assisted Rule Mutation
Each live editor also includes a Rhai mutation prompt panel. Type the desired workflow change, click Prepare Model Prompt, and paste that prompt into the configured local or OpenAI-compatible model. The prompt is constrained to the supported documentation DSL and asks the model to return a replacement block plus a short explanation.
The checked-in playground does not make browser-side model calls. Apply Example Draft demonstrates the expected interaction by applying a deterministic Phi-family example mutation to the editor, then re-rendering the same Mermaid and isometric views. The default example target is phi-4-mini-reasoning; if a Phi-5 endpoint is configured later, the same prompt contract can be sent through the Slint host chat window or another supervised model bridge without changing the DSL.
Example prompt request:
Add a medium-confidence review path and keep workbook commit behind review or high confidence.
Example model-shaped mutation:
fn classify_rows() -> score_confidence
if confidence > 0.85 -> commit_workbook
if confidence > 0.60 -> review_flag
if confidence <= 0.60 -> escalate_operator
fn review_flag() -> commit_workbook
flowchart TD
classify_rows["classify_rows"]
score_confidence["score_confidence"]
review_flag["review_flag"]
commit_workbook["commit_workbook"]
confidence_gt_0_85{"confidence > 0.85"}
confidence_gt_0_6{"confidence > 0.6"}
confidence____0_60{"confidence <= 0.60"}
escalate_operator["escalate_operator"]
classify_rows --> score_confidence
review_flag --> commit_workbook
confidence_gt_0_85 -->|"true"|commit_workbook
confidence_gt_0_85 -->|"false"|confidence_gt_0_6
confidence_gt_0_6 -->|"true"|review_flag
confidence____0_60 -->|"true"|escalate_operator
Sample Gallery
Use the examples below as copy-paste seeds in the live editor. Each sample is deliberately small enough to make view changes obvious.
Sample: Ledger Ingest Happy Path
fn ingest_pdf() -> detect_shape
fn detect_shape() -> validate_rows
fn validate_rows() -> classify_rows
fn classify_rows() -> reconcile_rows
fn reconcile_rows() -> commit_workbook
flowchart TD
ingest_pdf["ingest_pdf"]
detect_shape["detect_shape"]
validate_rows["validate_rows"]
classify_rows["classify_rows"]
reconcile_rows["reconcile_rows"]
commit_workbook["commit_workbook"]
ingest_pdf --> detect_shape
detect_shape --> validate_rows
validate_rows --> classify_rows
classify_rows --> reconcile_rows
reconcile_rows --> commit_workbook
Expected view behavior:
mermaid-2dshould read as a simple left-to-right pipelineisometric-3dshould keep every stage on one main spine with no lane fan-out
Sample: Confidence Gate with Review Path
fn classify_rows() -> score_confidence
if confidence > 0.85 -> commit_workbook
if confidence > 0.60 -> review_flag
if confidence <= 0.60 -> escalate_operator
fn review_flag() -> commit_workbook
flowchart TD
classify_rows["classify_rows"]
score_confidence["score_confidence"]
review_flag["review_flag"]
commit_workbook["commit_workbook"]
confidence_gt_0_85{"confidence > 0.85"}
confidence_gt_0_6{"confidence > 0.6"}
confidence____0_60{"confidence <= 0.60"}
escalate_operator["escalate_operator"]
classify_rows --> score_confidence
review_flag --> commit_workbook
confidence_gt_0_85 -->|"true"|commit_workbook
confidence_gt_0_85 -->|"false"|confidence_gt_0_6
confidence_gt_0_6 -->|"true"|review_flag
confidence____0_60 -->|"true"|escalate_operator
Expected view behavior:
- Mermaid should show three labeled outgoing edges from the confidence checks
- the isometric view should place
review_flagandescalate_operatoron separate side lanes - inserting a new medium-confidence branch should animate the lane spread rather than replacing the whole scene
Sample: Match-Style Disposition Routing
This now uses the dedicated match-arm DSL.
fn verify_result() -> match_result_disposition
match result.disposition => Disposition::Unrecoverable -> halt_pipeline
match result.disposition => Disposition::Recoverable -> repair_and_retry
match result.disposition => Disposition::Advisory -> record_note
fn repair_and_retry() -> requeue_validation
flowchart TD
verify_result["verify_result"]
match_result_disposition["match_result_disposition"]
repair_and_retry["repair_and_retry"]
requeue_validation["requeue_validation"]
halt_pipeline["halt_pipeline"]
record_note["record_note"]
verify_result --> match_result_disposition
repair_and_retry --> requeue_validation
match_result_disposition -->|"Disposition::Unrecoverable"|halt_pipeline
match_result_disposition -->|"Disposition::Recoverable"|repair_and_retry
match_result_disposition -->|"Disposition::Advisory"|record_note
Use this sample when comparing the current live rendering with the intended placement and reflow rules in Match Visualization Plan.
Executable Rust Example
#![allow(unused)]
fn main() {
use ledger_core::workflow::examples::ledger_ingest;
let workflow = ledger_ingest();
workflow.validate().expect("example workflow should be valid");
let mermaid = workflow.to_mermaid();
let rhai = workflow.to_rhai();
assert!(mermaid.contains("Ingested --> Validating"));
assert!(rhai.contains("next_state"));
}
HTML Export
#![allow(unused)]
fn main() {
let html = to_html(&graph);
// Returns complete HTML with Mermaid.js
}
Office Playbook Export
The desktop agent treats diagrams as versioned playbook artifacts that can be inserted into OneNote/Office or rendered in SharePoint. A diagram export is therefore more than a screenshot: it must carry a typed playbook model, render metadata, simulation profile, and evidence refs.
Minimum formats:
| Format | Use |
|---|---|
| Mermaid | Source-level editable diagram and review diff. |
| SVG | Preferred high-fidelity Office/SharePoint visual artifact where supported. |
| PNG | Compatibility fallback for Office clients that do not preserve SVG behavior. |
| HTML | Interactive playbook renderer for local desktop and SharePoint-hosted views. |
| JSON | Canonical playbook model with typed nodes, edges, gates, b00t capability refs, ledgrrr evidence refs, and simulation state. |
Refresh semantics:
- A refresh creates a new artifact version.
- The previous published artifact remains addressable.
- The evidence graph records source input, model/runtime profile, render hash, and approval state.
- OneNote/Office and SharePoint views render the artifact; ledgrrr remains the owner of source truth and provenance.
LayoutSolver
Kasuari-backed constraint solver for node positioning:
#![allow(unused)]
fn main() {
pub struct LayoutSolver;
pub fn generate_layout(&self, graph: &PipelineGraph) -> HashMap<String, (f32, f32)>;
}
Force Layout
The layout module implements force-directed graph layout using a custom Fruchterman-Reingold algorithm.
ForceLayout
#![allow(unused)]
fn main() {
pub struct ForceLayout {
positions: HashMap<usize, Vec3>,
velocities: HashMap<usize, Vec3>,
}
}
Methods
new()
Creates an empty force layout.
for_pipeline()
Creates a force layout initialized with the standard pipeline nodes arranged in a circle.
tick()
Runs one iteration of the force-directed layout algorithm, updating node positions based on:
- Repulsion between all nodes
- Attraction along edges
- Center gravity to keep the graph bounded
position(node_idx: usize) -> Option<Vec3>
Returns the 3D position of a node by index.
all_positions() -> &HashMap<usize, Vec3>
Returns all node positions.
Usage
See Graph for node creation and Iso for projection details.
#![allow(unused)]
fn main() {
let mut layout = ForceLayout::for_pipeline();
for _ in 0..100 {
layout.tick();
}
for (idx, pos) in layout.all_positions() {
println!("Node {}: {:?}", idx, pos);
}
}
Isometric Projection
The isometric projection converts 3D force-directed positions to 2D screen coordinates.
iso_project
#![allow(unused)]
fn main() {
pub fn iso_project(p: Vec3, scale: f32, origin: Vec2) -> Vec2
}
Parameters
- p: 3D position from force layout (Vec3)
- scale: Pixel scale factor (default: 32.0)
- origin: Screen center point (Vec2)
Returns
2D screen coordinates (Vec2)
Formula
#![allow(unused)]
fn main() {
x = origin.x + (p.x - p.z) * scale * 0.866 // cos(30°)
y = origin.y + (p.x + p.z) * scale * 0.5 - p.y * scale
}
This produces a classic dimetric isometric view with 2:1 pixel ratio.
Usage
#![allow(unused)]
fn main() {
let screen_pos = iso_project(node_position, 32.0, Vec2::new(400.0, 300.0));
}
Isometric Pipeline Objects
Every domain type in the PRD-6 / PRD-7 pipeline implements the HasVisualization trait,
which pins it to a ZLayer, a SemanticType, and a Rhai DSL snippet.
This contract is enforced by the 20 lint tests in crates/ledger-core/tests/iso_lint.rs.
ZLayer Stack
The 3-D space maps pipeline semantics to the Z axis:
| Z | Layer | Color | Base-Z | Types |
|---|---|---|---|---|
| 0 | Document | #334155 | 0.0 | Raw ingestion, file surface |
| 1 | Pipeline | #1d4ed8 | 136.0 | PipelineState<S>, StageResult, CommitGate, MetaFlag |
| 2 | Constraint | #7c3aed | 272.0 | ConstraintEvaluation, VendorConstraintSet, InvoiceConstraintSolver, InvoiceVerification, Issue |
| 3 | Legal | #b91c1c | 408.0 | Z3Result, LegalRule, LegalSolver, Jurisdiction, TransactionFacts |
| 4 | FormalProof | #0f766e | 544.0 | KasuariSolver |
| 5 | Attestation | #b45309 | 680.0 | PRD-6-FUTURE: InvariantEntry, AttestationSpec |
The X axis encodes pipeline progress (0 → committed); the Y axis encodes confidence lift.
Projection Formula
#![allow(unused)]
fn main() {
// iso_project — matches JS rhai-live-core.js isoProject()
screen_x = origin_x + (pt.x - pt.z) * scale * 0.866
screen_y = origin_y + (pt.x + pt.z) * scale * 0.5 - pt.y * scale
}
Contract test: iso_project(Vec3{x:192, y:0, z:0}, 1.0, 0.0, 0.0) → IsoProjected{screen_x:≈166.27, screen_y:96.0}.
Rendered Mini-DSL Samples
These samples use the supported Rhai diagram mini-DSL so the mdBook preprocessor can emit Mermaid blocks for the GitHub Pages build.
fn raw_statement() -> extracted_rows
fn extracted_rows() -> deterministic_tx_ids
fn deterministic_tx_ids() -> validated_facts
fn validated_facts() -> classified_tx
fn classified_tx() -> workbook_projection
flowchart TD
raw_statement["raw_statement"]
extracted_rows["extracted_rows"]
deterministic_tx_ids["deterministic_tx_ids"]
validated_facts["validated_facts"]
classified_tx["classified_tx"]
workbook_projection["workbook_projection"]
raw_statement --> extracted_rows
extracted_rows --> deterministic_tx_ids
deterministic_tx_ids --> validated_facts
validated_facts --> classified_tx
classified_tx --> workbook_projection
if constraint_passed == true -> legal_verification
if constraint_passed == false -> operator_review
if recovered == true -> legal_verification
if recovered == false -> blocked_queue
flowchart TD
constraint_passed____true{"constraint_passed == true"}
legal_verification["legal_verification"]
constraint_passed____false{"constraint_passed == false"}
operator_review["operator_review"]
recovered____true{"recovered == true"}
recovered____false{"recovered == false"}
blocked_queue["blocked_queue"]
constraint_passed____true -->|"true"|legal_verification
constraint_passed____false -->|"true"|operator_review
recovered____true -->|"true"|legal_verification
recovered____false -->|"true"|blocked_queue
match issue.disposition => Unrecoverable -> blocked_queue
match issue.disposition => Recoverable -> repair_pipeline
match issue.disposition => Advisory -> workbook_projection
flowchart TD
match_issue_disposition{"match issue.disposition"}
blocked_queue["blocked_queue"]
repair_pipeline["repair_pipeline"]
workbook_projection["workbook_projection"]
match_issue_disposition -->|"Unrecoverable"|blocked_queue
match_issue_disposition -->|"Recoverable"|repair_pipeline
match_issue_disposition -->|"Advisory"|workbook_projection
fn z3_rule_check() -> proof_result
if proof_result == satisfied -> commit_gate
if proof_result == violated -> legal_review
if proof_result == unknown -> operator_review
flowchart TD
z3_rule_check["z3_rule_check"]
proof_result["proof_result"]
proof_result____satisfied{"proof_result == satisfied"}
commit_gate["commit_gate"]
proof_result____violated{"proof_result == violated"}
legal_review["legal_review"]
proof_result____unknown{"proof_result == unknown"}
operator_review["operator_review"]
z3_rule_check --> proof_result
proof_result____satisfied -->|"true"|commit_gate
proof_result____violated -->|"true"|legal_review
proof_result____unknown -->|"true"|operator_review
fn commit_gate() -> audit_log
fn audit_log() -> evidence_graph
fn evidence_graph() -> cpa_workbook
fn cpa_workbook() -> exported_artifact
flowchart TD
commit_gate["commit_gate"]
audit_log["audit_log"]
evidence_graph["evidence_graph"]
cpa_workbook["cpa_workbook"]
exported_artifact["exported_artifact"]
commit_gate --> audit_log
audit_log --> evidence_graph
evidence_graph --> cpa_workbook
cpa_workbook --> exported_artifact
fn operator_review() -> approval_decision
fn approval_decision() -> replayable_audit_event
fn replayable_audit_event() -> evidence_graph
flowchart TD
operator_review["operator_review"]
approval_decision["approval_decision"]
replayable_audit_event["replayable_audit_event"]
evidence_graph["evidence_graph"]
operator_review --> approval_decision
approval_decision --> replayable_audit_event
replayable_audit_event --> evidence_graph
Pipeline Layer (z=1)
PipelineState<Ingested>
Semantic: Pipeline | Z: 1 | Color: #1d4ed8
Raw ingested transaction — structure validated, awaiting constraint pass.
let tx = ingest(pdf_path);
check_constraints(tx, constraint_set);
PipelineState<Validated>
Semantic: Pipeline | Z: 1 | Color: #1d4ed8
Post-constraint validated transaction — all numerical bounds passed, awaiting legal verification.
let validated = tx.validate(constraint_set);
if validated.confidence >= MIN_CONF { route_to_legal(validated) }
else { flag("low_confidence") }
PipelineState<Classified>
Semantic: Pipeline | Z: 1 | Color: #1d4ed8
Legal-verified transaction with tax category assigned — ready for workbook reconciliation.
let classified = legal_verified_tx.classify(rules);
set_category(classified, tax_category);
emit_to_workbook(classified);
PipelineState<Reconciled>
Semantic: Pipeline | Z: 1 | Color: #1d4ed8
Transaction matched against workbook entries — commit gate evaluation pending.
let reconciled = match_workbook(classified_tx, workbook);
if reconciled.matched { open_commit_gate(reconciled) }
else { flag("unmatched_entry") }
PipelineState<Committed>
Semantic: Pipeline | Z: 1 | Color: #1d4ed8
Committed to workbook — final immutable state, audit trail emitted.
let committed = commit_gate.approve(reconciled_tx);
write_xlsx(committed);
emit_audit_trail(committed.id);
PipelineState<NeedsReview>
Semantic: Pipeline | Z: 1 | Color: #1d4ed8
Legal verification failed — operator flag set, transaction held for manual review.
let review = legal_fail(tx, z3_result);
flag_operator("legal_violation", review.rule_id);
route_to_review_queue(review);
CommitGate
Semantic: Gate | Z: 1 | Color: #1d4ed8
Approval gate before workbook commit: Approved / PendingOperator / Blocked based on confidence and issues.
let gate = evaluate_commit_gate(stage_result);
match gate {
Approved => commit_to_workbook(tx),
PendingOperator => route_to_operator(tx, gate.reason),
Blocked => abort_commit(gate.issues),
}
MetaFlag
Semantic: Flag | Z: 1 | Color: #1d4ed8
Classification meta-annotation: NewVendor, AnomalyDetected, RepairApplied, LowUpstreamConf, or ConstraintWeak.
if vendor_is_new(tx.vendor) {
attach_flag(MetaFlag::NewVendor { vendor: tx.vendor });
}
if anomaly_score > THRESHOLD {
attach_flag(MetaFlag::AnomalyDetected { code: "AMT_SPIKE", impact: 0.9 });
}
StageResult<T>
Semantic: Result | Z: 1 | Color: #1d4ed8
Pipeline stage output wrapper: typed data payload, confidence score, issues, and meta context.
let result = StageResult::ok(data, confidence)
.with_issues(issues);
if result.confidence >= MIN_CONF { next_stage(result.data) }
else { flag_low_confidence(result) }
Constraint Layer (z=2)
ConstraintEvaluation
Semantic: Result | Z: 2 | Color: #7c3aed
Numerical constraint evaluation result with pass/fail per-field scores.
let eval = constraint_set.evaluate(amount, day, code, acct);
if eval.required_pass { classify_ok() } else { flag("constraint_fail") }
VendorConstraintSet
Semantic: Constraint | Z: 2 | Color: #7c3aed
Vendor-specific statistical bounds: amount percentiles, usual day-of-month, tax code, and account.
let bounds = load_vendor_constraints(vendor_id);
let eval = bounds.evaluate(amount, day_of_month, tax_code, account);
emit_constraint_result(eval);
InvoiceConstraintSolver
Semantic: Solver | Z: 2 | Color: #7c3aed
Invoice GST arithmetic solver — checks gross/net/GST consistency and rate conformance.
let solver = InvoiceConstraintSolver::new(gst_rate, expected_net);
let verification = solver.verify(gross, gst_amount);
if verification.arithmetic_ok && verification.gst_rate_ok { pass() }
InvoiceVerification
Semantic: Result | Z: 2 | Color: #7c3aed
Invoice verification result — arithmetic_ok and gst_rate_ok flags with audit note.
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) }
Issue
Semantic: Issue | Z: 2 | Color: #7c3aed
Single typed validation issue with severity (Unrecoverable / Recoverable / Advisory), code, message, and field.
let issue = Issue::unrecoverable("AMT_NEG", "amount is negative")
.with_field("amount");
stage_result.add_issue(issue);
Legal Layer (z=3)
Z3Result
Semantic: Result | Z: 3 | Color: #b91c1c
Symbolic satisfiability outcome from Z3-style legal predicate check: Satisfied / Violated / Unknown.
let result = legal_solver.verify(rule, facts);
match result {
Satisfied => ok(),
Violated => flag("legal_violation"),
Unknown => flag("legal_unknown"),
}
LegalRule
Semantic: Legal | Z: 3 | Color: #b91c1c
Single jurisdiction-bound legal rule: threshold, exclusion, or benefit predicate.
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);
LegalSolver
Semantic: Solver | Z: 3 | Color: #b91c1c
Runs all jurisdiction rules against TransactionFacts, returns aggregate confidence and issue list.
let solver = LegalSolver::new();
let (confidence, issues) = solver.verify_all(jurisdiction.legal_ruleset(), facts);
if issues.is_empty() { advance_pipeline() } else { route_review(issues) }
Jurisdiction
Semantic: Legal | Z: 3 | Color: #b91c1c
Jurisdiction enum controlling which legal ruleset applies: US (FBAR/FEIE), AU (GST/FBT), UK.
let j = Jurisdiction::AU;
let rules = j.legal_ruleset();
// US -> FBAR/FEIE rules; AU -> GST/FBT rules
let code = j.code(); // "US" | "AU" | "UK"
TransactionFacts
Semantic: Legal | Z: 3 | Color: #b91c1c
Raw fact bundle fed to LegalSolver: vendor jurisdiction, supply type, tax code, amount, and activity flags.
let facts = TransactionFacts::new()
.with_vendor("AU")
.with_supply_type("TAXABLE")
.with_tax_code("G1")
.with_amount("1100.00");
legal_solver.verify_all(rules, facts);
Formal Proof Layer (z=4)
KasuariSolver
Semantic: Proof | Z: 4 | Color: #0f766e
Kasuari constraint layout solver — evaluates field values against (min, max) ranges, bridges constraint → formal verification layer.
let solver = KasuariSolver;
let score = solver.evaluate("amount", value, [(min, max)]);
let strength = solver.strength("required"); // Required | Strong | Medium | Weak
// Bridges constraint satisfaction into formal layout verification
Attestation Layer (z=5)
Populated in PRD-6-FUTURE once the ledger-attest proc-macro crate is implemented.
Planned types: InvariantEntry, AttestationSpec, InvariantRegistry.
Running the Lint Suite
cargo test -p ledger-core --test iso_lint
All 20 lint tests assert per-object: non-empty description, non-empty rhai_dsl,
z_layer.index() <= 5, and non-empty semantic_type.known_name().
Animation Backends
| Phase | Backend | Output |
|---|---|---|
| 0 | SVG SMIL (IsoAnimationPath::to_smil_svg) | Inline <animateTransform> |
| 1 | rerun.io | Interactive 3D timeline |
| 2 | manim Python stub (IsoAnimationPath::to_manim_script) | Rendered video |
See crates/ledger-core/src/iso.rs for IsoTransform, IsoEasing, and IsoAnimationPath.
Renderer
The render module provides screen coordinate mapping for the visualization.
GraphRenderer
#![allow(unused)]
fn main() {
pub struct GraphRenderer {
pub width: u32,
pub height: u32,
pub scale: f32,
pub origin: Vec2,
}
}
Methods
new(width: u32, height: u32)
Creates a renderer with the specified canvas dimensions.
screen_position(x: f32, y: f32, z: f32) -> Vec2
Converts 3D coordinates to 2D screen position using isometric projection.
Usage
#![allow(unused)]
fn main() {
let renderer = GraphRenderer::new(800, 600);
let screen = renderer.screen_position(100.0, 0.0, 50.0);
}
Match Visualization Plan
This chapter documents the planned visualization contract for a future match operator so the Mermaid and isometric views stay semantically aligned.
Related Chapters
- Theory of Operation - branching examples and disposition handling
- Workflow - TOML workflow compilation surface
- Visualization - current live editor and view modes
- Constraints - Kasuari placement rules
- Verification - reviewer and human escalation flows
Problem Statement
The documentation renderer currently understands a narrow diagram DSL:
fn source() -> targetif expression -> target
That is enough for linear pipelines and threshold gates, but it does not yet express a true multi-arm match branch as one semantic node with ordered arms, fallthrough, and stable reflow behavior.
Canonical Sample
The operator contract should be driven by small, realistic examples instead of abstract syntax fragments. The baseline sample is a disposition router:
#![allow(unused)]
fn main() {
match result.disposition {
Disposition::Unrecoverable => halt_pipeline,
Disposition::Recoverable => repair_and_retry,
Disposition::Advisory => record_note,
}
}
Equivalent workflow intent:
- a single branching point reads
result.disposition - each arm is declaration-ordered and labeled
- terminal or review-heavy arms remain visually distinct
Mermaid Plan
Projection Rules
- Compile one explicit
matchnode, not three unrelatedifnodes. - Preserve arm order from source.
- Label every outgoing edge with the arm key.
- Render a default edge when the source includes
_orelse. - Keep the branch target node ids stable so diffs are readable across regenerations.
Sample 2D Layout
fn verify_result() -> match_result_disposition
match result.disposition => Disposition::Unrecoverable -> halt_pipeline
match result.disposition => Disposition::Recoverable -> repair_and_retry
match result.disposition => Disposition::Advisory -> record_note
fn repair_and_retry() -> requeue_validation
flowchart TD
verify_result["verify_result"]
match_result_disposition["match_result_disposition"]
repair_and_retry["repair_and_retry"]
requeue_validation["requeue_validation"]
halt_pipeline["halt_pipeline"]
record_note["record_note"]
verify_result --> match_result_disposition
repair_and_retry --> requeue_validation
match_result_disposition -->|"Disposition::Unrecoverable"|halt_pipeline
match_result_disposition -->|"Disposition::Recoverable"|repair_and_retry
match_result_disposition -->|"Disposition::Advisory"|record_note
Mermaid Acceptance Criteria
- arm labels must be the declared pattern names
- the
matchnode must render once even when there are many arms - adding a new arm should only add one new edge and one target node in the diff
- Mermaid output should stay readable when an arm routes back into the main workflow
Isometric Plan
Placement Model
The isometric renderer should treat match as a structured fan-out node:
- the
matchnode sits on the main workflow spine - each arm receives a stable lane on the
zaxis by declaration order - the branch targets inherit the parent stage depth on
x - terminal or review-heavy arms may get a small
ylift to improve readability
Kasuari Constraint Sketch
x(match) == x(previous) + stage_gapx(target_arm_n) >= x(match) + branch_gapz(target_arm_n) == z(match) + arm_index * lane_gapz(target_arm_default)should stay at the outermost lane so explicit arms keep their positionsx(rejoin)should be constrained after the widest branch target, not after the first branch target
Animated Reflow Goals
When the source changes:
- inserting a new arm should push later lanes sideways instead of replacing the full scene
- renaming an arm should preserve its lane if its identity key is unchanged
- moving an arm in source order should animate the lane swap so the operator can see what changed
- deleting an arm should collapse only the affected lane group
Sample Isometric Intent
fn verify_result() -> match_result_disposition
match result.disposition => Disposition::Unrecoverable -> halt_pipeline
match result.disposition => Disposition::Recoverable -> repair_and_retry
match result.disposition => Disposition::Advisory -> record_note
fn repair_and_retry() -> requeue_validation
flowchart TD
verify_result["verify_result"]
match_result_disposition["match_result_disposition"]
repair_and_retry["repair_and_retry"]
requeue_validation["requeue_validation"]
halt_pipeline["halt_pipeline"]
record_note["record_note"]
verify_result --> match_result_disposition
repair_and_retry --> requeue_validation
match_result_disposition -->|"Disposition::Unrecoverable"|halt_pipeline
match_result_disposition -->|"Disposition::Recoverable"|repair_and_retry
match_result_disposition -->|"Disposition::Advisory"|record_note
Implementation Status
Completed
- Single match node: Parser compiles one
matchnode per expression, not scatteredifnodes. - Arm order preservation:
IndexMapin Rust and orderedMapin JS preserve declaration order. - Labeled edges: Every outgoing edge carries the arm key as its label.
- Default arm detection:
_,else,otherwise,defaultarms are flagged withis_default: true. - Stable identity keys:
Node.identity_keyenables identity-stable reflow across label changes. - Arm index tracking:
Node.arm_indexandEdge.arm_indexcarry declaration order. - Semantic role inference: Both Rust and JS infer roles from label keywords (ingest/validate/classify/review/reconcile/commit/decision/step).
- Mermaid default annotation: Default arms rendered with
(default)suffix. - Match-specific lane assignment in JS layout solver: Arms receive stable Z lanes from
arm_index. - Default arm outer-lane placement:
_/elsearms render at the outermost lane. - Basic rejoin constraint: Simple rejoin points are placed after the widest branch and centered on the match node lane.
In Progress
- Richer rejoin semantics: The live solver handles simple joins, but branch-local work and explicit rejoin syntax still need a stronger graph model.
Planned
- Animated reflow: Inserting/deleting/moving arms should animate lane changes instead of snapping.
- Explicit rejoin syntax: e.g.
fn repair_and_retry() -> requeue_validationalready implies rejoin; the parser should expose that intent directly.
The current live editor and mdBook preprocessor both accept the repeated-arm syntax above. The next implementation target is richer identity and placement semantics, not a second incompatible syntax.
Example Gallery
Disposition Routing
Use this as the primary worked example in docs and tests:
#![allow(unused)]
fn main() {
match result.disposition {
Disposition::Unrecoverable => halt_pipeline,
Disposition::Recoverable => repair_and_retry,
Disposition::Advisory => record_note,
}
}
Confidence Band Routing
This is the threshold analogue that should remain visually consistent with match:
#![allow(unused)]
fn main() {
match confidence_band {
ConfidenceBand::High => commit_result,
ConfidenceBand::Medium => queue_manual_review,
ConfidenceBand::Low => escalate_to_operator,
}
}
Keyword Match Selector
This sample ties the visualization plan back to the rule-engine roadmap:
#![allow(unused)]
fn main() {
match selector.pick(tx.description) {
RulePick::Exact(rule) => run_rule(rule),
RulePick::Semantic(rule) => run_rule(rule),
RulePick::Fallback => queue_unclassified_review,
}
}
Documentation Plan
- add at least one
matchsample to every branch-heavy chapter instead of relying on generic prose - keep Mermaid and isometric screenshots or generated views side-by-side for the same sample
- use stable example names derived from the full expression:
match_result_disposition,match_confidence_band,match_selector_pick— the parser generates the node ID asmatch_+sanitize_id(expr), so pipeline steps must target the full sanitized form - keep one example focused on routing to a terminal state and one focused on rejoining the main workflow
Test Plan
- parser tests should confirm declaration order is preserved
- Mermaid snapshot tests should assert a single switch node with labeled arms
- isometric scene tests should assert stable lane ordering and animated reflow metadata
- docs validation should include at least one
matchexample chapter so regressions surface injust docgen-check