Ledger Operations
Overview
The LedgerOperation trait is the primitive interface for every discrete action the pipeline can take: ingesting a statement, classifying transactions, checking a tax deadline, exporting the CPA workbook. Composing these operations through an OperationDispatcher rather than calling them directly provides:
- Idempotency guarantees: Each operation implementation declares whether it is idempotent. The dispatcher enforces this contract and deduplicates re-triggered operations where safe.
- Uniform result surface: Every operation returns an
OperationResultthat flows into the audit trail, regardless of what the operation does internally. - Calendar integration:
ScheduledEventrecords carry anOperationKindthat the dispatcher resolves to a concrete operation at runtime. The calendar and the operation layer are decoupled. - Agent-editable dispatch rules: Rhai rules can inspect
OperationContextfields and short-circuit or redirect operations without changing Rust code.
Operation Dispatch Flow
fn receive_trigger() -> resolve_operation
fn resolve_operation() -> validate_context
fn validate_context() -> execute_operation
fn execute_operation() -> record_result
if result_success -> mark_complete
if result_failure -> emit_issue
flowchart TD
receive_trigger["receive_trigger"]
resolve_operation["resolve_operation"]
validate_context["validate_context"]
execute_operation["execute_operation"]
record_result["record_result"]
result_success{"result_success"}
mark_complete["mark_complete"]
result_failure{"result_failure"}
emit_issue["emit_issue"]
receive_trigger --> resolve_operation
resolve_operation --> validate_context
validate_context --> execute_operation
execute_operation --> record_result
result_success --> mark_complete
result_failure --> emit_issue
receive_trigger— accepts anOperationKindfrom a scheduled event, MCP call, or manual invocation.resolve_operation— looks up the registeredLedgerOperationimplementation for the givenOperationKind.validate_context— checks thatOperationContextcontains required fields (e.g.journal_path,workbook_path); returnsErrif preconditions are not met.execute_operation— calls the operation’sexecute()method; the operation is responsible for its own internal error handling.record_result— writes theOperationResult(success or failure) to the audit trail with timestamps and the triggering event ID.mark_complete— on success, updates the scheduler’s completion record so the event is not re-fired.emit_issue— on failure, emits a structured issue record for operator review; does not re-trigger automatically.
Dispatcher Architecture
fn operation_dispatcher() -> operation_context
fn operation_dispatcher() -> ingest_statement_op
fn operation_dispatcher() -> classify_transactions_op
fn operation_dispatcher() -> check_tax_deadline_op
fn operation_dispatcher() -> export_workbook_op
fn operation_dispatcher() -> generate_audit_trail_op
fn ingest_statement_op() -> operation_result
fn classify_transactions_op() -> operation_result
fn check_tax_deadline_op() -> operation_result
fn export_workbook_op() -> operation_result
fn generate_audit_trail_op() -> operation_result
fn operation_result() -> audit_trail
flowchart TD
operation_dispatcher["operation_dispatcher"]
operation_context["operation_context"]
ingest_statement_op["ingest_statement_op"]
classify_transactions_op["classify_transactions_op"]
check_tax_deadline_op["check_tax_deadline_op"]
export_workbook_op["export_workbook_op"]
generate_audit_trail_op["generate_audit_trail_op"]
operation_result["operation_result"]
audit_trail["audit_trail"]
operation_dispatcher --> operation_context
operation_dispatcher --> ingest_statement_op
operation_dispatcher --> classify_transactions_op
operation_dispatcher --> check_tax_deadline_op
operation_dispatcher --> export_workbook_op
operation_dispatcher --> generate_audit_trail_op
ingest_statement_op --> operation_result
classify_transactions_op --> operation_result
check_tax_deadline_op --> operation_result
export_workbook_op --> operation_result
generate_audit_trail_op --> operation_result
operation_result --> audit_trail
IngestStatementOp is annotated as idempotent: re-ingesting the same source file produces the same Blake3 content-hash transaction IDs and the dispatcher skips duplicate writes.
Operations Reference
| Operation | ID | Idempotent | Status | Description |
|---|---|---|---|---|
IngestStatementOp | IngestStatement | Yes (Blake3 dedup) | Implemented | Parse a source PDF or CSV, extract transactions, write to journal and workbook |
ClassifyTransactionsOp | ClassifyTransactions | Yes (rule determinism) | Implemented | Run the Rhai classification waterfall over unclassified transactions |
CheckTaxDeadlineOp | CheckTaxDeadline | Yes | Implemented | Emit a deadline notification record; no data mutation |
ExportWorkbookOp | ExportWorkbook | Yes (overwrite) | Implemented | Write the current journal state to the CPA Excel workbook |
GenerateAuditTrailOp | GenerateAuditTrail | Yes (overwrite) | Implemented | Produce the year-end audit trail report for a given tax year |
Document Shape Classification
Before IngestStatementOp can extract transactions, it must know which extraction profile to use. classify_document_shape() in document_shape.rs (and mirrored in rules/classify_document_shape.rhai) maps a raw document to a DocumentShape:
fn ingest_file() -> detect_shape
fn detect_shape() -> route_extractor
fn route_extractor() -> extract_transactions
fn extract_transactions() -> classify_transactions
flowchart TD
ingest_file["ingest_file"]
detect_shape["detect_shape"]
route_extractor["route_extractor"]
extract_transactions["extract_transactions"]
classify_transactions["classify_transactions"]
ingest_file --> detect_shape
detect_shape --> route_extractor
route_extractor --> extract_transactions
extract_transactions --> classify_transactions
ingest_file— receives a source path matching theVENDOR--ACCOUNT--YYYY-MM--DOCTYPEnaming convention.detect_shape— callsclassify_document_shape()with filename, doc_type, and a content sample. ReturnsDocumentShapewith vendor, account_type, statement_format, currency, confidence, and signals.route_extractor— selects the extraction backend based onstatement_format:csv_generic,csv_ofx,pdf_tabular, orxlsx_native.extract_transactions— runs the selected extractor; outputs rawTransactionrows withDecimalamounts.classify_transactions— passes extracted transactions through the Rhai classification waterfall inrules/.
Shape detection uses a confidence score. If confidence is below 0.5, IngestStatementOp emits a review flag and halts rather than ingesting with an unknown vendor profile.
DocumentShape Fields
| Field | Type | Description |
|---|---|---|
vendor | StatementVendor | Institution slug: WellsFargo, Chase, Anz, Commbank, etc. |
account_type | String | checking, savings, brokerage, crypto |
statement_format | String | csv_generic, csv_ofx, pdf_tabular, xlsx_native |
currency | String | USD, AUD, EUR, GBP |
confidence | f64 | 0.0–1.0 heuristic score based on filename slug and content signals |
signals | Vec<String> | Matched signal names for audit; e.g. filename_vendor_slug, csv_header_match |
reason | String | Human-readable explanation for the audit trail |
Integration with Calendar
ScheduledEvent in the TOML calendar manifests carries an operation field that is an OperationKind variant. When BusinessCalendar::upcoming() returns a due event, the caller passes event.operation to OperationDispatcher::dispatch():
#![allow(unused)]
fn main() {
let due = calendar.upcoming(today, 30);
for event in due {
let result = dispatcher.dispatch(&event.operation, &context).await?;
audit_trail.record(event.id, result);
}
}
The TOML operation inline table maps directly to OperationKind enum variants:
# Maps to OperationKind::IngestStatement { source_glob: "samples/**/*.pdf" }
operation = { type = "IngestStatement", source_glob = "samples/**/*.pdf" }
# Maps to OperationKind::ClassifyTransactions { rule_dir: "rules" }
operation = { type = "ClassifyTransactions", rule_dir = "rules" }
# Maps to OperationKind::CheckTaxDeadline { deadline_id: "fbar_deadline" }
operation = { type = "CheckTaxDeadline", deadline_id = "fbar_deadline" }
This bidirectional mapping means calendar manifests are the single source of truth for what runs, when it runs, and what it does — the Rust dispatcher merely executes.