Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

Info

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::Decimal in 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

SurfaceAudiencePurpose
Excel workbookCPA/operatorReview, correction, schedule summaries, audit signoff
MCP toolsagentsControlled capability execution through ledgerr_* tool families
Sidecar statehost/serviceRestart recovery, replay, idempotency cache, lifecycle state
Desktop hostoperatorapprovals, notifications, credentials, process supervision
mdBook docsdevelopers/operatorsexecutable diagrams and technical behavior reference

Local-first by design

All processing runs on your machine — no private financial data leaves the host.

Warning

PDF ingestion rewrites the workbook in-place. Back up tax-ledger.xlsx before running a full re-ingest.

CPA handoff

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

ComponentModuleStatusNotes
Filename routingfilename.rsImplementedVENDOR--ACCT--YYYY-MM--DOCTYPE parser
Blake3 hash IDsingest.rsImplementeddeterministic_tx_id, idempotent dedup
IngestedLedgeringest.rsImplementedJournal + workbook ingest pipeline
DocType enumdocument.rsImplementedDocument type classification
DocumentGraph typesdocument.rsImplementedGraph node/edge types defined
Pipeline HSMpipeline.rsImplementedType-state + statig state machine
Verb traitpipeline.rsImplementedDetectVerb, ValidateVerb
ClassificationEngineclassify.rsImplementedRhai rule execution
ClassificationOutcomeclassify.rsImplementedcategory, confidence, reason
ReviewFlagclassify.rsImplementedFlag upsert, query by year/status
Rhai rule filesrules/Implementedforeign_income, self_employment, fallback
Jurisdiction enumlegal.rsImplementedUS, AU, UK
LegalRule + Z3 formulaslegal.rsImplementedHard predicate checks for AU GST and US Schedule C
LegalSolverlegal.rsImplementedUses pinned z3 = 0.8 for violation satisfiability checks behind legal-z3; default builds use the same deterministic result semantics without native Z3
Proposer/Reviewer LLMverify.rsPartialPattern defined, no real LLM calls
WorkflowToml DSLworkflow.rsImplementedTOML → Rhai FSM + Mermaid
IssueSource::RhaiRulevalidation.rsImplementedValidation layer with rule source
Workbook writeworkbook.rsImplementedrust_xlsxwriter tx projection
Workbook read-backworkbook.rsImplementedcalamine round-trip
Journaljournal.rsImplementedNDJSON append and replay
Audit trail (MetaCtx)pipeline.rsImplementedMutation log per pipeline state
MCP contractledgerr-mcp/src/contract.rsImplemented8 advertised ledgerr_* capability families
MCP adapterledgerr-mcp/src/mcp_adapter.rsImplementedDispatches contract actions to TurboLedgerService
Ontology storeledgerr-mcp/src/ontology.rsImplementedEntity/edge upsert and path query surface
Xero serviceledgerr-mcp/src/xero_service.rsPartialSupervised catalog/link actions; credentials remain host-owned
Mermaid auto-generationworkflow.rsImplementedrhai DSL → diagram blocks
Slint desktop UIslint_viz.rsPartialStub, not wired to window system
RuleRegistryrule_registry.rsImplementedLoads transaction .rhai rules and optional ReqIF sidecars
Keyword rule selectionrule_registry.rsImplementedDeterministic keyword fallback; semantic selector remains planned
Waterfall orchestrationrule_registry.rsImplementedFirst non-Unclassified result wins; fallback outcome is preserved
ReqIfCandidate (Rust)rule_registry.rsStubType defined, sidecar bridge missing
DocumentChunkrule_registry.rsStubType defined, bridge missing
SemanticRuleSelectorrule_registry.rsStubTrait defined, embeddings not wired
Docling extraction bridgeMissingPython subprocess call not written
reqif-opa-mcp MCP wiringMissingNo Rust MCP client for sidecar
Vector embedding indexMissingNo embedding model or HNSW index
File watcher (notify)Missingnotify 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:

  1. Docling extraction bridge — Write the Rust std::process::Command call to invoke the Python sidecar, parse its NDJSON stdout, and deserialize into DocumentChunk / ReqIfCandidate. This is the critical path for Phase 2 document intelligence. Estimated scope: new sidecar.rs module, ~100 lines.

  2. Wire ClassifyTransactionsOp to RuleRegistry — Replace the current operation stub with registry loading, transaction iteration, waterfall classification, and review-flag emission.

  3. Expand LegalSolver coverage — 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.

  4. File watcher via notify — Add a debounced notify watcher on the workbook path and the rules/ directory. This enables live rule reloading and human Excel-edit detection without polling. Estimated scope: new watcher.rs module, ~60 lines.

  5. SemanticRuleSelector embedding index — Wire a local fastembed-rs or ONNX embedding model to encode transaction descriptions and ReqIfCandidate texts. 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.

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

CapabilityStatusImplementationNotes
ingest_artifactImplementedIngestedLedger::ingestBlake3 hash dedup, journal write
extract_documentPlannedPython sidecar: extract_docling_documentDocling 2.78 PDF parse
build_graphPlannedPython sidecar: DocumentGraph assemblyDocumentNode tree construction
derive_candidatesPlannedPython sidecar: RequirementCandidateHeuristic requirement extraction
opa_gatePlannedPython sidecar: OPA policy evaluationPolicy: admit or reject candidates
emit_reqifPlannedPython sidecar: emit_reqif_xmlReqIF XML baseline output
load_rulesImplementedRuleRegistry::load_from_dirLoads 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
ArtifactRecordSource document metadata; consumed by sidecar only
DocumentNodeDocumentChunkCanonical graph node with text, parent, anchors
DocumentGraphIntermediate; not materialized in Rust
RequirementCandidateReqIfCandidateDeserialized 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:

  1. A local embedding model (ONNX via ort, or fastembed-rs)
  2. A vector index (usearch, hnsw, or qdrant sidecar)
  3. ReqIfCandidate objects 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 stepRust typeStatus
load_rulesRuleRegistry::load_from_dirImplemented for transaction rules
select_applicableRuleRegistry::select_rules_deterministicImplemented keyword fallback
run_waterfallRuleRegistry::classify_waterfallImplemented first-match waterfall
evaluate_confidenceClassificationOutcome.confidenceImplemented
emit_resultClassifiedTransaction / ReviewFlagImplemented

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 transactions
  • rules/classify_self_employment.rhai — matches self-employment / Schedule C income
  • rules/classify_fallback.rhai — catch-all returning Unclassified with review: 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:

  1. Embedding model — a local ONNX or fastembed-rs model to encode transaction text
  2. ReqIfCandidate indexReqIfCandidate objects loaded from the Python sidecar
  3. 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.

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:

  1. Convert known TransactionFacts into boolean predicates.
  2. Build a violation formula.
  3. When legal-z3 is enabled, ask Z3 whether the violation formula is satisfiable.
  4. Interpret unsat as Z3Result::Satisfied, sat as Z3Result::Violated, and solver unknown as Z3Result::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.

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 OperationResult that flows into the audit trail, regardless of what the operation does internally.
  • Calendar integration: ScheduledEvent records carry an OperationKind that 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 OperationContext fields 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 an OperationKind from a scheduled event, MCP call, or manual invocation.
  • resolve_operation — looks up the registered LedgerOperation implementation for the given OperationKind.
  • validate_context — checks that OperationContext contains required fields (e.g. journal_path, workbook_path); returns Err if preconditions are not met.
  • execute_operation — calls the operation’s execute() method; the operation is responsible for its own internal error handling.
  • record_result — writes the OperationResult (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

OperationIDIdempotentStatusDescription
IngestStatementOpIngestStatementYes (Blake3 dedup)ImplementedParse a source PDF or CSV, extract transactions, write to journal and workbook
ClassifyTransactionsOpClassifyTransactionsYes (rule determinism)ImplementedRun the Rhai classification waterfall over unclassified transactions
CheckTaxDeadlineOpCheckTaxDeadlineYesImplementedEmit a deadline notification record; no data mutation
ExportWorkbookOpExportWorkbookYes (overwrite)ImplementedWrite the current journal state to the CPA Excel workbook
GenerateAuditTrailOpGenerateAuditTrailYes (overwrite)ImplementedProduce 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 the VENDOR--ACCOUNT--YYYY-MM--DOCTYPE naming convention.
  • detect_shape — calls classify_document_shape() with filename, doc_type, and a content sample. Returns DocumentShape with vendor, account_type, statement_format, currency, confidence, and signals.
  • route_extractor — selects the extraction backend based on statement_format: csv_generic, csv_ofx, pdf_tabular, or xlsx_native.
  • extract_transactions — runs the selected extractor; outputs raw Transaction rows with Decimal amounts.
  • classify_transactions — passes extracted transactions through the Rhai classification waterfall in rules/.

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

FieldTypeDescription
vendorStatementVendorInstitution slug: WellsFargo, Chase, Anz, Commbank, etc.
account_typeStringchecking, savings, brokerage, crypto
statement_formatStringcsv_generic, csv_ofx, pdf_tabular, xlsx_native
currencyStringUSD, AUD, EUR, GBP
confidencef640.0–1.0 heuristic score based on filename slug and content signals
signalsVec<String>Matched signal names for audit; e.g. filename_vendor_slug, csv_header_match
reasonStringHuman-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.

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 active BusinessCalendar and computes today’s date relative to each event’s RecurrenceRule.
  • find_due_events — returns events whose next fire date falls within the configured horizon window.
  • dispatch_operations — resolves each event’s OperationKind and forwards it to the OperationDispatcher.
  • 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:

EventDateTax Code CitationTags
Q1 Estimated Tax PaymentApril 15IRC §6654estimated_tax, quarterly, schedule_c
Q2 Estimated Tax PaymentJune 15IRC §6654estimated_tax, quarterly
Q3 Estimated Tax PaymentSeptember 15IRC §6654estimated_tax, quarterly
Q4 Estimated Tax PaymentJanuary 15 (next year)IRC §6654estimated_tax, quarterly
Form 1040 Filing DeadlineApril 15 (June 15 expat auto-extension)26 USC §6072annual_return, form_1040
Form 1040 Extended DeadlineOctober 15Form 4868annual_return, extension, form_4868
FBAR (FinCEN Form 114)April 15 (auto-extension to Oct 15)31 USC §5314fbar, foreign_accounts, fincen_114
FATCA Form 8938April 15IRC §6038Dfatca, form_8938, foreign_assets
Monthly Statement Ingest1st of each monthautomation, ingest, recurring
Monthly Classification Run2nd of each monthautomation, classification, recurring

AU Tax Calendar

Key Australian tax deadlines encoded in calendar/tax_calendar_au.toml:

EventDateTax Code CitationTags
BAS GST Q1 LodgementOctober 28ATO BAS programbas, gst, quarterly, q1
BAS GST Q2 LodgementFebruary 28ATO BAS programbas, gst, quarterly, q2
BAS GST Q3 LodgementApril 28ATO BAS programbas, gst, quarterly, q3
BAS GST Q4 LodgementJuly 28ATO BAS programbas, gst, quarterly, q4
Individual Tax ReturnOctober 31ITAA 1997annual_return, individual, itaa_1997
Tax Agent ExtensionMay 15 (next year)ATO lodgement programannual_return, extension, tax_agent
Concessional Super ContributionJune 30ITAA 1997 s.290-180superannuation, concessional, s290-180
CGT Discount Asset ReviewJune 30ITAA 1997 s.115-Acgt, capital_gains, s115-a, discount_method
Monthly Statement Ingest1st of each monthautomation, ingest, recurring
Monthly Classification Run2nd of each monthautomation, 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:

FieldTypeDescription
idStringUnique event identifier; referenced in audit trail records
descriptionStringHuman-readable label; include tax code citation for compliance events
recurrenceInline tableRecurrenceRule variant with type-specific fields
operationInline tableOperationKind variant dispatched on fire
jurisdictionStringRegulatory jurisdiction; used to filter calendars per entity
enabledBoolSet false to suspend without deleting
tagsString arrayFree-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; returns Result<BusinessCalendar>
  • upcoming(today, horizon_days) — returns Vec<ScheduledEvent> due within horizon_days
  • overdue(today) — returns events whose last expected fire date has passed without a completion record
  • merge(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

Theory of Operation

This chapter documents the novel architecture patterns that power l3dg3rr’s AI agent governance system.

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
  1. Composable instruction streams - Tools accept not just data, but instructions that modify their behavior
  2. Idempotent re-execution - Tools can be safely re-run with the same inputs, producing deterministic outputs
  3. Content-addressed identity - All outputs are identified by cryptographic hashes of their inputs
  4. 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 UseNovel Theory of Tool
fn(input) -> outputfn(instruction, state) -> (output, evidence)
StatelessStateful with checkpointing
Random UUIDsContent-hash IDs
Best-effortDeterministic/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 StateVisual NodeColorAnimation
IdleEmpty circle#f0f0f0None
ActiveFilled circle#4a90d9Pulse
SuccessCheckmark#4caf50Check
WarningTriangle#ff9800Shake
ErrorX mark#f44336Blink
ReviewStar#9c27b0Bounce

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:

LayerPrefixRole
Bookkeeping/domain MCPledgerr_*Existing financial document, workflow, audit, ontology, Xero, and workbook capabilities.
Desktop/controller MCPledgrrr_*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

ToolCapability familyTypical actions
ledgerr_documentsintake, filename validation, raw context, tags, filesystem metadataingest_pdf, ingest_rows, document_inventory, normalize_filename
ledgerr_reviewrule execution, classification, review flagsrun_rule, classify_ingested, query_flags, classify_transaction
ledgerr_reconciliationtotals and posting guardrailsvalidate, reconcile, commit
ledgerr_workflowlifecycle and plugin operationsstatus, transition, resume, plugin_info
ledgerr_auditevent history and audit replayevent_history, event_replay, query_audit_log
ledgerr_taxevidence, ambiguity review, workbook exportassist, evidence_chain, ambiguity_review, export_workbook
ledgerr_ontologygraph/ontology query and write operationsquery_path, export_snapshot, upsert_entities, upsert_edges
ledgerr_xerosupervised Xero catalog and entity linkageget_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.

ToolResponsibility
ledgrrr_statusReport desktop, service, tray, model runtime, Office add-in, SharePoint, and b00t state.
ledgrrr_install_planReturn dry-run install/repair actions and required privilege level.
ledgrrr_install_desktopLaunch the native Windows installer.
ledgrrr_start_serviceStart the local ledgrrr service.
ledgrrr_stop_serviceStop the local ledgrrr service.
ledgrrr_open_trayLaunch or focus tray/taskbar UI.
ledgrrr_render_diagramRender typed playbook models into Mermaid/SVG/PNG/HTML.
ledgrrr_simulate_pipelineRun deterministic or local-CPU-model simulation and return evidence summary.
ledgrrr_export_office_artifactProduce OneNote/Office/SharePoint-safe playbook artifacts.
ledgrrr_repairRepair service, tray, model runtime, Office manifests, and b00t linkage.
ledgrrr_uninstallLaunch 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.

  1. ledgerr-mcp-server: stdio transport.
  2. contract: published tool families, actions, generated JSON Schema.
  3. mcp_adapter: request parsing, envelope shaping, compatibility aliases.
  4. TurboLedgerService: domain behavior, state, audit, lifecycle.
  5. 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.

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

AreaCurrent state
MCP serverledgerr-mcp-server exposes ledgrrr capability families over stdio.
Claude pluginThe repo has a Claude plugin marketplace entry for Cowork/plugin workflows.
ledgrrr-mcp controllerImplemented (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 bundleImplemented. 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-serviceDurable 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 hostTauri 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 packageImplemented: 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 renderingMermaid, 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 simulationImplemented: 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/SharePointNot 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 linkageSource-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.exe
  • ledgrrr-tray.exe
  • ledgrrr-mcp.exe
  • support-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.VCTools workload) and its recommended Windows SDK components, which provide link.exe, makeappx.exe, and signtool.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.

ToolPurpose
ledgrrr_statusReport installed desktop, service, tray, model, Office, and b00t state.
ledgrrr_install_planReturn a dry-run install/repair plan and privilege requirements.
ledgrrr_install_desktopLaunch the signed native installer.
ledgrrr_start_serviceStart the service if installed.
ledgrrr_stop_serviceStop the service.
ledgrrr_open_trayLaunch or focus tray/taskbar UI.
ledgrrr_render_diagramRender Mermaid/SVG/PNG/HTML from a typed playbook.
ledgrrr_simulate_pipelineRun local deterministic or model-assisted simulation.
ledgrrr_export_office_artifactProduce OneNote/SharePoint-safe diagram artifacts.
ledgrrr_repairRepair service/tray/model/Office integration.
ledgrrr_uninstallLaunch the native uninstaller or return exact removal steps.

Office and SharePoint Surface

The diagram generator is the control surface for AI-generated process models.

SurfaceRole
OneNote/Office Add-inTask pane for generating, previewing, inserting, and refreshing diagrams/playbooks.
SPFx web partSharePoint rendering surface for published playbook artifacts.
Local service bridgeConverts 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 — the ledgrrr-mcp controller 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 by ledgrrr_status.desktop_package.
  • ledgrrr.office-addin / ledgrrr.sharepoint-webpart — Office/SharePoint overlays, type = "overlay", marked status = "missing" pending a Microsoft 365 tenant.
  • ledgrrr.model-runtime — local CPU inference profile, type = "ai", marked missing; ledgrrr_status.model_runtime.configured stays false until LEDGRRR_MODEL_RUNTIME_PROFILE is 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.mcpb and ledgrrr_status works.
  • 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.

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.

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:

  1. Proposer: Primary model generates the classification/decision via ModelClient::complete()
  2. Decision Store: Intermediate representation of the proposal
  3. Reviewer: Second model reviews and validates the proposal
  4. Outcome: Either VerificationOutcome::Approved or VerificationOutcome::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

  1. Input transaction with classification issue
  2. Proposal: Proposer model generates RepairProposal with suggested fix and confidence
  3. Review: Reviewer model evaluates proposal, returns ReviewResult
  4. Decision:
    • If review.approved && review.confidence >= thresholdVerificationOutcome::Approved
    • Otherwise → VerificationOutcome::Rejected (flags for human review)
  5. 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 entities
  • export_snapshot: produce a serializable graph snapshot
  • upsert_entities: add or update typed entities
  • upsert_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:

  • TransactionInput becomes IngestedTransaction, JournalTransaction, and SampleTransaction.
  • ClassificationOutcome becomes ClassifiedTransaction and review flags.
  • LegalRule + TransactionFacts becomes Z3Result.
  • VendorConstraintSet becomes ConstraintEvaluation.

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

StageInput TypeOutput TypeConfidenceJurisdictionNotes
IngestTransactionInputIngestedTransaction, JournalTransactionDeterministic (1.0)AllBlake3 content-hash IDs
ValidateTransactionInputMetaCtx0.0–1.0AllType checks, constraint evaluation
ClassifySampleTransactionClassificationOutcome, ClassifiedTransaction0.0–1.0US/AU/UKRhai rule waterfall
ReconcileClassifiedTransactionOperationResult0.0–1.0AllXero match/diff
ExportOperationContextrust_xlsxwriter::WorkbookDeterministic (1.0)AllCPA-auditable Excel
VerifyRepairProposalVerificationOutcome0.0–1.0AllMulti-model proposer/reviewer

Cross-Stage Compatibility

From TypeTo TypeCompatible?BridgeTest
TransactionInputSampleTransactionYesdeterministic_tx_id()test_transaction_input_to_sample_transaction_shape
TransactionInputJournalTransactionYesJournalTransaction::from_input()test_transaction_input_to_journal_shape
ClassificationOutcomeClassifiedTransactionYesField mapping + tx_idtest_classification_outcome_to_classified_shape
ClassifiedTransactionTxProjectionRowPartialRequires upstream contexttest_classified_to_projection_row_requires_context
IssueMetaCtxYesMetaCtx::advance()test_validation_pipeline_mesh
MetaCtxStageResult<T>Yesand_then() combinatortest_validation_pipeline_mesh
LegalRule + TransactionFactsZ3ResultYesLegalSolver::verify()
VendorConstraintSetConstraintEvaluationYesVendorConstraintSet::evaluate()

Known Type Gaps

ClassifiedTransactionTxProjectionRow

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:

  1. Add missing fields to ClassifiedTransaction, or
  2. Create an explicit ExportContext type 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

ConceptPrimary TypeModuleRelated TypesDerives
TransactionTransactionInputingestIngestedTransaction, JournalTransaction, SampleTransactionDebug, Clone, PartialEq, Eq, Serialize, Deserialize
ClassificationClassificationOutcomeclassifyClassifiedTransaction, ClassificationBatch, ReviewFlagDebug, Clone, PartialEq
ValidationIssuevalidationDisposition, IssueSource, MetaCtx, StageResult<T>Debug, Clone, PartialEq, Serialize, Deserialize
Pipeline StatePipelineState<S>pipelineType-state markers: Ingested, Validated, Classified, Reconciled, Committed, NeedsReviewDebug, Clone, Serialize, Deserialize
Legal RuleLegalRulelegalTransactionFacts, Z3Result, LegalSolver, JurisdictionDebug, Clone, Serialize, Deserialize
ConstraintVendorConstraintSetconstraintsConstraintEvaluation, ConstraintStrength, InvoiceConstraintSolverDebug, Clone, Serialize, Deserialize
VerificationMultiModelVerifier<C>verifyRepairProposal, ReviewResult, VerificationOutcome, ModelClientDebug, Clone, Serialize, Deserialize
WorkflowWorkflowTomlworkflowStateDecl, TransitionDeclDebug, Clone, Deserialize, Serialize
CalendarBusinessCalendarcalendarScheduledEvent, RecurrenceRule, CalendarError
OperationLedgerOperation (trait)ledger_opsOperationContext, OperationResult, OperationKind, OperationDispatcher
DocumentDocumentRecorddocumentDocType, DocumentStatus, XeroLink, XeroEntityTypeDebug, Clone, PartialEq, Eq, Serialize, Deserialize
ShapeDocumentShapedocument_shapeStatementVendor, ColumnMapDebug, Clone, Serialize, Deserialize
Rule RegistryRuleRegistryrule_registryReqIfCandidate, DocumentChunk, SemanticRuleSelector
WorkbookTxProjectionRowworkbookREQUIRED_SHEETS constantDebug, 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.

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() -> target
  • if expression -> target
  • match expr => Arm -> target

The preview surface exposes a two-position view slider:

  • isometric-3d renders an animated SVG scene with deterministic layered placement, so inserting a node causes nearby steps to visibly reflow instead of snapping in place
  • mermaid-2d keeps 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

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-2d should read as a simple left-to-right pipeline
  • isometric-3d should 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_flag and escalate_operator on 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:

FormatUse
MermaidSource-level editable diagram and review diff.
SVGPreferred high-fidelity Office/SharePoint visual artifact where supported.
PNGCompatibility fallback for Office clients that do not preserve SVG behavior.
HTMLInteractive playbook renderer for local desktop and SharePoint-hosted views.
JSONCanonical playbook model with typed nodes, edges, gates, b00t capability refs, ledgrrr evidence refs, and simulation state.

Refresh semantics:

  1. A refresh creates a new artifact version.
  2. The previous published artifact remains addressable.
  3. The evidence graph records source input, model/runtime profile, render hash, and approval state.
  4. 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:

ZLayerColorBase-ZTypes
0Document#3341550.0Raw ingestion, file surface
1Pipeline#1d4ed8136.0PipelineState<S>, StageResult, CommitGate, MetaFlag
2Constraint#7c3aed272.0ConstraintEvaluation, VendorConstraintSet, InvoiceConstraintSolver, InvoiceVerification, Issue
3Legal#b91c1c408.0Z3Result, LegalRule, LegalSolver, Jurisdiction, TransactionFacts
4FormalProof#0f766e544.0KasuariSolver
5Attestation#b45309680.0PRD-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);

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

PhaseBackendOutput
0SVG SMIL (IsoAnimationPath::to_smil_svg)Inline <animateTransform>
1rerun.ioInteractive 3D timeline
2manim 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.

Problem Statement

The documentation renderer currently understands a narrow diagram DSL:

  • fn source() -> target
  • if 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 match node, not three unrelated if nodes.
  • Preserve arm order from source.
  • Label every outgoing edge with the arm key.
  • Render a default edge when the source includes _ or else.
  • 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 match node 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 match node sits on the main workflow spine
  • each arm receives a stable lane on the z axis by declaration order
  • the branch targets inherit the parent stage depth on x
  • terminal or review-heavy arms may get a small y lift to improve readability

Kasuari Constraint Sketch

  • x(match) == x(previous) + stage_gap
  • x(target_arm_n) >= x(match) + branch_gap
  • z(target_arm_n) == z(match) + arm_index * lane_gap
  • z(target_arm_default) should stay at the outermost lane so explicit arms keep their positions
  • x(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 match node per expression, not scattered if nodes.
  • Arm order preservation: IndexMap in Rust and ordered Map in JS preserve declaration order.
  • Labeled edges: Every outgoing edge carries the arm key as its label.
  • Default arm detection: _, else, otherwise, default arms are flagged with is_default: true.
  • Stable identity keys: Node.identity_key enables identity-stable reflow across label changes.
  • Arm index tracking: Node.arm_index and Edge.arm_index carry 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: _/else arms 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_validation already 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.

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 match sample 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 as match_ + 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 match example chapter so regressions surface in just docgen-check