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

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.