Calibrax · Platform Architecture · v2

Calibrax — Architecture & Data Flow

A multi-tenant pipeline that turns raw portfolio-company financials into a standardised, queryable Budget-vs-Actual — every line mapped to one canonical Chart of Accounts, every figure traceable back to its source cell, every mapping decision either made or confirmed by a human.

Cloudflare Worker — ingestion, deterministic parse, queue orchestration
Claude (Sonnet 4.6) — sheet classification + CoA mapping, 2 call sites only
FastAPI — pipeline logic, persistence orchestration
Neon Postgres + R2 — system of record
🧭

How trust is distributed across the system

Deterministic code does the parsing. Claude only does two things — classify what a sheet is, and propose a canonical Chart-of-Accounts code for each line item. A human analyst confirms or overrides every mapping before it's trusted long-term — and that decision is written back into per-entity memory so the same label is never asked twice.

classify [agent] → parse [code] → normalise [code] → map [agent] → review [human] → bva [code] → recompute [code, on override]
The big picture

Inputs → engine → outputs, at a glance

One workbook upload sets off a five-phase chain. Each phase writes its result to Postgres, then hands the next phase off via a Cloudflare Queue message.

Inputs

  • Workbook uploadGET /upload/presigned-url → PUT direct to R2
  • entity_id — which portfolio company this file belongs to (FK on every table)
  • upload_mode — parsing strategy hint, "A" or "B", passed through every phase
  • r2_key format{entity_id}/{uuid}/{filename}

Engine

  • 5 chained phasesclassify → parse → normalise → map → bva
  • Each phase is its own FastAPI endpoint, triggered by a Cloudflare Queue message and re-queuing the next on success
  • Advisory lock on mappingpg_try_advisory_lock(hash(file_id)) — skips duplicate concurrent map invocations rather than queuing
  • BvA has a readiness gate — needs both ACTUAL and BUDGET rows mapped for the entity, else status → bva_pending

Outputs

  • Statement views/statement/{entity_id}?source=actual|budget
  • Lineage, drilldown, provenance/lineage, /drilldown, /provenance
  • Mapping review queue/mapping-review/{entity_id} — sorted by ascending confidence
  • BvA exportGET /bva/export/{entity_id} → .xlsx via openpyxl
The engine

Five phases, chained by queue messages

Each phase completes, writes its status to pipeline_jobs, and pushes the next phase's payload onto the same Cloudflare Queue. Two of the five involve Claude — highlighted below.

Phase 1
classifyFile()
One Claude call per sheet — doc type (ACTUAL/BUDGET/IGNORE), currency, period range, confidence, and the exact label text marking where the real statement starts/ends.
WorkerAgent
Phase 2
parseFile()
Deterministic SheetJS extraction — date/period headers, currency blocks, unit tags — into tidy row objects. No LLM involved at all.
Worker
Phase 3
pipeline_normalise()
Persists parsed rows as display_rows; idempotent — re-running an already-normalised job short-circuits with no side effects.
FastAPI
Phase 4
pipeline_map()
Every label checked against this entity's confirmed-mapping memory first; only genuinely new labels go to Claude, batched 25 at a time, for a canonical CoA code + confidence + tier.
FastAPIAgent
Phase 5
pipeline_bva()
Readiness-gated aggregation by canonical_code & period. If Budget or Actual is still missing for the entity, parks the job at bva_pending instead of failing.
FastAPI
Where the agent comes in

The only two places Claude touches this system

Every other line of logic — parsing, persistence, aggregation, lineage — is deterministic code. These are the exact prompts and schemas, as written.

Sheet classification
classifyFile() in index.js · runs in the Worker · model: claude-sonnet-4-6 · max_tokens: 512
What it receives
  • One sheet at a time — filename, sheet name, full sheet list, and every row of that sheet as JSON (header:1, 0-indexed)
  • Sheets are pre-filtered by a skip-pattern list before this even runs"thesis","nav","cover","assumption","market","_hr","calculation","exec"…
What it returns
{ "doc_type": "ACTUAL" | "BUDGET" | "IGNORE", "currency": "VND" | "USD" | … | null, "period_start": "YYYY-MM" | null, "period_end": "YYYY-MM" | null, "confidence": 0.0–1.0, "financial_statement_start_label": "…" | null, "financial_statement_end_label": "…" | null }
Key prompt rules (verbatim logic)
  • Classify on content & structure only — never on sheet name or filename, with one named exception
  • Exception: a sheet named like "BvA", or one pairing scenario columns (Best/Base/Stretch) against Actual side-by-side, is forced to IGNORE — it's a derived output, not source data
  • Start/end labels must trim to the genuine statement span — explicitly told to exclude operational counts, cash/bank balances, loan metrics, user-acquisition metrics even if they sit inside the same sheet
  • If duplicated/stacked line-item blocks appear, include the full contiguous span, not just the first instance
After the call
  • File-level doc_type derived deterministically — single type if all sheets agree, else MIXED
  • File-level currency = most frequent currency across non-IGNORE sheets
  • Result is persisted after every sheet, not just at the end — a crash mid-file doesn't lose earlier sheets' classifications
Chart-of-Accounts mapping
pipeline_map() in main.py · runs in FastAPI · model: claude-sonnet-4-6 · batched, MAP_BATCH_SIZE = 25
What it receives, per batch
  • Company context — name, sector, reporting currency, doc_type, period range, known product/line aliases
  • Previously confirmed mappings for this entity, injected as reference examples
  • Existing canonical code assignments (any status, not just confirmed) — explicit instruction to reuse a code rather than invent a near-duplicate
  • Each label arrives with hierarchy context: parent_label, indent_level, sheet, statement_type
What it returns, per label
{ "label": "exact label text", "canonical_code": "REV.Net", "confidence": 0.97, "rationale": "one sentence", "suggestion": "confirm"|"review"|"override", "suggestion_basis": "one sentence to the analyst" }
The canonical taxonomy it's anchored to
  • Top-level: REV, COGS, GP, OPEX, EBITDA, DA, EBIT, TAX, NI, BS, CF — dot notation for sub-codes, e.g. COGS.Teacher.LS
  • BS sub-structure spelled out explicitly: BS.Asset.Current.Cash, BS.Asset.Fixed.PPE, BS.Liab.Current.AP, BS.Equity.Retained
  • Sub-codes are told to reflect the actual business — e.g. COGS.CSAT for a SaaS company vs. COGS.Teacher for a tutoring company
  • Confidence below 0.85 is the explicit threshold for "this label is ambiguous"
Before the call even happens
  • Every distinct unmapped label for the file is pulled first
  • Labels already in entity_profiles.confirmed_mappings are auto-mapped from memory — zero Claude calls
  • Only the remainder is sent to Claude, in batches of 25
End-to-end flow

How one upload moves through the system

Three lanes: the client, the Cloudflare Worker (ingestion + queue consumer + both deterministic parsing and the classify agent call), and FastAPI backed by Neon Postgres + R2.

Client
browser / app
Get presigned URLGET /upload/presigned-url
PUT direct to R2entity_id/uuid/filename
Confirm uploadPOST /upload/confirm
file_metadata + pipeline_jobs rows createdstatus: queued
Cloudflare Worker
calibrax-classify · index.js · queue consumer
Queue: classify
classifyFile()1 Claude call per sheet
self-enqueues: parse
parseFile()deterministic SheetJS extraction
POST rows[] to FastAPI/pipeline/parse
normaliseFile() / mapFile() / bvaFile()thin proxies — fetch() to FastAPI, ack/retry on result
FastAPI + Neon/R2
main.py · pipeline logic
run_parse()writes display_rows, queues normalise
pipeline_normalise()idempotent, queues map
pipeline_map()memory check → batch 25 → Claude → mapping_records
pipeline_bva()readiness gate → aggregate by canonical_code
mapping_action()analyst confirm/override → writes entity memory
recompute_bva()re-aggregates without re-parsing
Queue is calibrax-pipelinemax_batch_size: 1 (messages processed one at a time, not true batching despite the per-message loop in the consumer), max_retries: 3, no dead_letter_queue configured. R2 bucket is tnba-files, with CORS open to all origins for GET/PUT/HEAD/POST/DELETE — required for the client's direct presigned-URL upload in step 1 of the user journey.
User journey

From file upload to a reviewed BvA

The same pipeline described above, from the perspective of the portfolio analyst operating it rather than the system executing it.

01
Analyst
Selects entity, uploads workbook

Picks the portfolio company from /entities and uploads a budget, actuals, or mixed workbook. No sheet selection or formatting is required at this step.

02
System
Classifies, parses, normalises

Runs unattended — each sheet is typed, financial sheets are extracted, and rows are persisted. No analyst action is required at this stage.

03
Agent
Maps line items to the CoA

Previously confirmed labels are reused from entity memory; unseen labels are mapped to a canonical code with a confidence score and a tier.

04
System
BvA computes automatically

High-confidence mappings (tier 1) are auto-confirmed and aggregation runs immediately, provided both Budget and Actual rows exist for the entity.

05
Analyst
Reviews the mapping queue

Opens /mapping-review/{entity_id}, sorted by ascending confidence. Confirms or overrides tier-2 mappings; an override recomputes the BvA automatically.

06
Analyst
Consumes the output

Reads the statement view, traces any figure to its source cell via lineage and drilldown, and exports the BvA as a formatted workbook.

Job lifecycle

States a pipeline_jobs row moves through

Tracked via status, current_phase, an append-only phase_logs array, and a phases_complete JSONB map.

queued
running
classified
parsed
normalised
mapped
bva_pending
complete
failed
queue_failed
bva_pending is a deliberate, non-error parked state — it fires when an entity has mapped rows from only one of ACTUAL or BUDGET. The job sits there correctly until the matching file arrives and re-triggers /pipeline/bva readiness, rather than treating an out-of-order upload as a failure.
The review loop

Where a human closes the trust gap

Mapping runs automatically and BvA computes right after — review happens in parallel, sorted by ascending confidence so the most uncertain mappings surface first.

confirm

High confidence, unambiguous label. Analyst clicks confirm — no value changes, it's locked in as reviewed.

writes: mapping_records.confirmed = true

review

Plausible but ambiguous, or appears in an unexpected context. Already flowing into BvA — flagged for the analyst to verify against source before it's trusted.

amber_dot flag · no auto-block on bva

override

Low confidence, wrong language, a reconciliation artifact, or unmapped (UNKNOWN). Analyst replaces the code directly.

triggers: POST /recompute-bva/{entity_id}
Every confirm or override writes the label → canonical-code pair back into entity_profiles.confirmed_mappings. The next file from the same company checks this memory before calling Claude at all — so mapping accuracy compounds per entity over time. /pipeline/backfill-suggestions retroactively applies a new confirmation to previously unmapped rows from earlier files.
Traceability & provenance layer

From a source cell to wherever it surfaces

Four read endpoints, each answering a different audit question — none re-run any computation; all read straight off persisted lineage. Lineage and drilldown are the two that carry figures back to an exact source cell, and are marked accordingly.

/lineage/{entity}/{code}Lineage

Question: every source row that ever fed this one canonical code, across every file ever uploaded for this entity.
  • Identity stats — label_count, min/max confidence, pending/confirmed/overridden counts for this code
  • Per source: filename, sheet, label, doc_type, uploaded_at, FX rate, periods in both raw and USD
  • Per mapping: confidence, confirmed/overridden status, confirmed_by, confirmed_at, rationale, suggestion_basis
  • Sorted by upload recency then confidence — the newest, most-trusted source surfaces first

/drilldown/{entity}/{code}/{period}Drilldown

Question: for this exact code and this exact period, which source cells, and which formulas reference them?
  • parse_formula_refs() — regex-extracts every Sheet!Cell reference out of any Excel formula string, including quoted sheet names and multi-cell SUM() expressions
  • period_variants() — matches a period against both "2025-01" and "Jan-25" label formats, since source sheets are never consistent about which they use
  • The narrowest traceability view in the system — one code, one period, exact cells

/lineage/canonical-codes/{entity}

Question: which codes exist for this company, and how confident is the mapping overall?
  • label_count, avg_confidence, pending_count — grouped per canonical_code
  • The entry point before drilling into one code's full lineage

/provenance

Question: what happened, to which entity, and when — independent of any one canonical code.
  • File upload events — entity, filename, doc_type, uploaded_at — the first event in every chain
  • Filterable by entity_id and event_type, paginated via limit/offset
  • The system-wide audit log; lineage and drilldown answer "where did this number come from," provenance answers "what actions has this entity been through"
System of record

What's persisted, and where

entity_profilesPostgres

One row per portfolio company.

entity_id · name · sector · currency · display_currency · fx_to_usd · known_aliases · confirmed_mappings (JSONB)
file_metadataPostgres

One row per upload.

id · r2_key · doc_type · sheet_classifications · financial_row_labels · period_start/end · currency · classify_confidence · processing_status
pipeline_jobsPostgres

One row per upload's journey through the 5 phases.

id · file_id · entity_id · status · current_phase · phases_complete (JSONB) · phase_logs (append-only) · error_message
display_rowsPostgres

One row per parsed line item.

label · parent_label · indent_level · sheet · statement_type · periods (JSONB) · periods_usd · canonical_code · sheet_doc_type · block_excluded
mapping_recordsPostgres

One row per label → canonical-code decision.

original_label · canonical_code · confidence · rationale · tier · suggestion · suggestion_basis · amber_dot · confirmed/overridden · confirmed_by · confirmed_at
R2 object storageCloudflare R2

Raw uploaded workbooks — never re-read after parse extracts rows from them.

key: entity_id/uuid/filename
Stage by stage

Every endpoint · what it does · output

Rows shaded purple are the two agent call sites — everything else is deterministic.

FunctionRuns onWhat it doesWrites / returns
classifyFile()Agent Worker + Claude Per-sheet doc-type, currency, period-range classification, statement boundary trimming. Persists incrementally after each sheet. file_metadata.doc_typequeues: parse
parseFile() Worker, deterministic SheetJS heuristics — date/period headers, currency blocks, unit tags — across all financial sheets. rows[]POST /pipeline/parse
run_parse() FastAPI Persists parsed rows as display_rows; on failure writes parse_debug for diagnosis. display_rowsqueues: normalise
pipeline_normalise() FastAPI Idempotent — re-running a normalised job short-circuits. Prepares rows for canonical mapping. pipeline_jobs.statusqueues: map
pipeline_map()Agent FastAPI + Claude Advisory-locks per file_id; checks confirmed_mappings memory first, batches only new labels (25 at a time) to Claude for canonical_code + confidence + tier. mapping_recordsqueues: bva
pipeline_bva() FastAPI Readiness-gated: requires both ACTUAL and BUDGET mapped rows for the entity, else parks at bva_pending. Aggregates by canonical_code & period. BvA aggregates
mapping_action() FastAPI Analyst confirms or overrides a mapping; override updates display_rows and writes the pair back into entity memory. entity_profiles.confirmed_mappingsPOST /mapping-review/{id}/action
recompute_bva() FastAPI Re-runs aggregation for an entity after mapping corrections, without re-parsing source files. BvA aggregatesPOST /recompute-bva/{entity_id}
backfill_suggestions() FastAPI Retroactively applies a newly-confirmed label→code mapping to previously unmapped rows from earlier files. mapping_recordsPOST /pipeline/backfill-suggestions/{entity_id}
get_statement() FastAPI Serves Actual or Budget statement views by canonical code & period, with FX-to-USD rollups, independent budget-source path. statement rowsGET /statement/{entity_id}?source=actual|budget
get_mapping_review() FastAPI Pulls mapping_records for the entity's latest files, sorted ascending by confidence — most uncertain first. review queueGET /mapping-review/{entity_id}
get_canonical_codes() FastAPI Aggregate view per canonical code — label_count, avg_confidence, pending_count. code summaryGET /lineage/canonical-codes/{entity_id}
get_lineage() FastAPI Every source row across every file that ever fed a given canonical code, with confidence and confirmation status. source traceGET /lineage/{entity}/{code}
get_drilldown() FastAPI Expands a canonical code + period to exact source cells, parsing formula references via regex. source rows + formula refsGET /drilldown/{entity}/{code}/{period}
get_provenance() FastAPI Cross-entity event log — file uploads and other tracked actions, filterable by entity/event type, paginated. event logGET /provenance
export_bva() FastAPI Builds a formatted .xlsx of the entity's BvA via openpyxl (Font, PatternFill, Alignment), ready for download. .xlsx fileGET /bva/export/{entity_id}
Agent guardrails

Controls wrapping the two agent calls

Neither agent call output is trusted unconditionally. The following are the code-level controls applied to each call — response validation, blast-radius limits, and the conditions under which a call is skipped entirely.

Around pipeline_map() — the mapping call
main.py · everything below runs regardless of what Claude returns
Before the call
Memory short-circuit

Every label is checked against entity_profiles.confirmed_mappings first. Only labels with no prior confirmed match are ever sent to Claude — the agent never re-decides something a human already settled.

Blast radius
Batches of 25, isolated

Labels chunk into batches of MAP_BATCH_SIZE = 25. Each batch is its own try/except — one malformed batch response doesn't take down the others, and successful batches commit immediately rather than waiting on the whole set.

Fault isolation
Partial failure ≠ total loss

If a batch throws (bad JSON, timeout, anything), it's flagged and logged but the loop continues. Only after all batches finish does the phase raise — and by then, every successful batch is already committed to mapping_records.

Output sanitation
Defaults for missing fields

canonical_code defaults to 'UNKNOWN' if absent, confidence defaults to 0.5. The response is never trusted to be complete — every field read with .get() and a fallback.

Tier is recomputed
Code overrides the agent's own framing

Claude returns a suggestion string, but the actual tier (1 or 2) and confirmed flag are derived by code from the numeric confidence — tier 1 if confidence ≥ 0.85. The agent proposes; a fixed threshold decides.

Auto-confirm
Tier 1 skips human review entirely

Mappings at confidence ≥ 0.85 are inserted with confirmed = true immediately — no analyst action required. Only tier 2 (<0.85) sits in the review queue. This is a real throughput/risk trade-off, not an oversight.

Concurrency
Per-file advisory lock

pg_try_advisory_lock(hash(file_id)) — if a duplicate map invocation arrives (e.g. a redelivered queue message) while one is already running, it's skipped outright rather than queued or double-run.

Write safety
Guarded, non-destructive updates

Every UPDATE display_rows is scoped to rows that are still unmapped and not block_excluded — an agent response can never silently overwrite an already-mapped or deliberately-excluded row.

Idempotency
ON CONFLICT DO NOTHING

Auto-mapped-from-memory inserts use ON CONFLICT DO NOTHING — safe to re-run the same phase twice without duplicate rows, which matters because queue messages can be redelivered.

The classify call (Worker-side) carries a lighter set of controls by comparison: markdown-fence stripping before JSON.parse, a hard-coded skip-pattern prefilter applied before any sheet reaches Claude, and per-sheet incremental persistence so a failure mid-file does not discard earlier results. No batching, confidence-based auto-confirm, or advisory lock is applied, as each sheet's classification is independent and idempotent by design.
Failure & edge-case handling

How the pipeline handles non-standard input

Each item below corresponds to a conditional already present in the codebase.

Budget and Actual arrive in separate uploads, out of order
BvA readiness check requires both doc types mapped for the entity before aggregating. If only one exists, the job parks at bva_pending instead of failing, and resumes automatically once the second file completes mapping.
pipeline_bva() · bool_or(...) readiness gate
One workbook contains both Budget and Actual sheets
File-level doc_type can be MIXED; every downstream query uses COALESCE(sheet_doc_type, file doc_type) so each sheet is treated by its own classification, not the file's overall label.
display_rows.sheet_doc_type
A sheet is itself a derived BvA output, not source data
Sheets named like "BvA", or structurally pairing scenario columns against Actual side-by-side, are force-classified IGNORE by an explicit prompt rule — prevents the system from ingesting its own output as a new source.
classifyFile() prompt rule
A sheet mixes statement lines with unrelated content
Claude returns explicit start/end label boundaries; the prompt names what to exclude even mid-sheet — operational counts, cash/bank balances, loan metrics, user-acquisition metrics — so a single sheet can be trimmed to just the genuine statement span.
financial_statement_start/end_label
A mapping batch returns malformed or partial JSON
Caught per-batch; that batch is marked failed and logged, but every other batch in the same run still commits. The phase only raises after all batches attempt, and only after successful ones are already saved.
pipeline_map() try/except per batch
Same label appears with no prior mapping anywhere for this company
Falls through memory, gets sent to Claude with full company context plus every existing canonical code already assigned for this entity, with an explicit instruction to reuse an existing code rather than mint a near-duplicate.
existing_assignments_block in system_prompt
An analyst corrects a mapping after BvA already computed
Override writes back to entity memory immediately; /recompute-bva/{entity_id} deletes and re-inserts that entity's variance_records from scratch — no partial/incremental drift.
recompute_bva() · DELETE then INSERT
A queue message gets redelivered (Worker retry, network blip)
Every phase checks its own job status first and short-circuits if already complete for that phase (e.g. already_mapped, already_normalised) — redelivery doesn't reprocess or duplicate.
status guard at top of every /pipeline/* endpoint
A Worker-side phase throws repeatedly (cold start, transient network error)
The queue redelivers up to max_retries: 3 before giving up. No dead-letter queue is configured, so a message that exhausts its retries is dropped with no automatic recovery path — the job is left at whatever pipeline_jobs.status its last successful phase reached.
wrangler.toml · calibrax-pipeline queue config
Enhancement pipeline

Proposed agent-assisted enhancements

Each item below addresses a specific gap in the current implementation. None of the following is built; scope and priority require confirmation before development.

Proposed — not implemented Scope, sequencing, and priority to be confirmed before any item is scheduled
Self-check on the map phase
gap: no validation after the call
A malformed batch is caught and isolated, but a confidently wrong mapping — valid JSON, high confidence, semantically off — is auto-confirmed at tier 1 without further review. A lightweight second pass that spot-checks a sample of auto-confirmed mappings against the company's own historical confirmed set would surface mapping drift prior to analyst review.
Fuzzy match before the memory lookup
gap: exact-string match only
Confirmed-mapping memory is keyed on exact label text. A label that's the same line item with a typo, trailing space, or minor rewording ("Net Revenue" vs "Net revenues") misses memory entirely and is re-sent to Claude, consuming a call for a label already settled. A normalization or embedding-similarity pass ahead of the exact-match check would close this.
Retry-with-correction on batch failure
gap: failed batch is dropped, not retried
Today a failed batch is logged and the phase eventually raises — those labels stay unmapped until the whole map phase is manually re-triggered. Feeding the parse error back to Claude for one or two corrective retries (same idea already used productively elsewhere for structured-output recovery) would resolve most failures without requiring analyst intervention.
Typed, versioned CoA grounding schema
gap: taxonomy lives inside the prompt string, agent output is unvalidated
REV/COGS/GP/OPEX/EBITDA/DA/EBIT/TAX/NI/BS/CF and the BS sub-structure are hard-coded into system_prompt as free text, and a returned canonical_code is never checked against anything — it is trusted as written, with only 'UNKNOWN' as a fallback. Replacing the free-text paragraph with a typed, versioned schema — a stable backbone of standard codes plus an extensible leaf layer for sector-specific sub-codes, structurally informed by IFRS/GAAP concepts without claiming statutory compliance — would let the harness validate a returned code against a closed vocabulary, compute rollups (e.g. Total OPEX) deterministically from a rollup_parent field instead of trusting the agent's aggregation, and stamp every mapping_record with the schema version active at mapping time so a future taxonomy revision cannot silently reinterpret historical BvA output.
{ "schema_version": "1.0.0", // stored on every mapping_record going forward "framework_basis": "IFRS-informed", "backbone": [ { "code": "REV.Net", "statement": "PNL", "rollup_parent": "REV", "normal_balance": "credit", "definition": "Revenue recognized net of discounts, returns, allowances.", "aliases": ["Net Sales", "Total Revenue, net"], "leaf_extendable": false }, { "code": "COGS", "statement": "PNL", "normal_balance": "debit", "leaf_extendable": true, // agent may propose COGS.<BusinessConcept>, validated against this rule "leaf_naming_rule": "COGS.{BusinessConcept}" } ] }
Classification note: this is a typed, versioned taxonomy — a controlled vocabulary with parent/child structure (rollup_parent) and per-term metadata. It is not a formal ontology: there are no logical axioms, no class/individual distinction, and no reasoner — validation is procedural (code-side checks), not inferential. A true ontology would require formalizing rollup_parent as an RDF/OWL relation, adding axioms (e.g. every REV.* code has normal_balance = credit as an enforced constraint), and introducing a reasoner or SHACL/SKOS validator — deliberately out of scope here, as the mapping problem is classification into a known structure, not inference over asserted facts.
Batch the classify call across sheets
gap: one Claude call per sheet, sequential
A workbook with 15 sheets makes 15 sequential calls before parse can even start. Sending multiple sheets in one call (with per-sheet boundaries in the prompt) would reduce both latency and cost on wide workbooks; context-length limits on very large sheets would need evaluation before adoption.
Configure a dead-letter queue
gap: failed messages are dropped after max_retries, unrecoverable
calibrax-pipeline is configured with max_retries: 3 and no dead_letter_queue binding. A message that fails three times — a transient Worker cold-start, a momentary Claude API timeout — is dropped silently rather than landing somewhere recoverable, leaving the job stalled at its last completed phase with no automated path back. Adding a dead-letter queue and a periodic sweep over pipeline_jobs rows stuck mid-phase would close this without changing any pipeline logic.
Enhancement pipeline · grounded flow

How the proposed enhancements change the end-to-end flow

Same three lanes as the current-state diagram earlier in this document. Solid boxes are unchanged. Dashed orange boxes are new. Dashed blue boxes are existing steps whose behavior changes.

Unchanged — current implementation
New — proposed addition
Changed — existing step, new behavior
Client
browser / app
Upload workbookunchanged
PUT direct to R2
Confirm uploadunchanged
calibrax-classify
Worker · queue consumer
Queue: classify
classifyFile()batched across sheets, 1 call/file
parseFile()unchanged · deterministic
retries on Worker exceptionexisting max_retries: 3
dead-letter queueexhausted retries land here, not dropped
FastAPI + Neon/R2
main.py · pipeline logic
run_parse() → normalise()unchanged
load CoA schemaversioned, typed — replaces inline prompt text
pipeline_map()memory check → batch → Claude, schema injected
schema validationreject codes outside closed vocabulary
retry-with-correctionfailed batch → error fed back → 1–2 retries
confidence audit samplespot-checks tier-1 auto-confirms
mapping_records+ schema_version stamped
pipeline_bva()rollups computed via rollup_parent, not grouped flat
What this buys

Grounding and fidelity impact, point by point

Each card maps to one or more of the new/changed nodes above.

Closed-vocabulary validation
Before — any string the agent returns as canonical_code is trusted as written, with 'UNKNOWN' as the only fallback.
After — a returned code is checked against the schema's backbone and leaf-naming rules; an invalid code is rejected deterministically, not silently accepted.

This is the single largest fidelity gain — it converts an unbounded, free-text output space into a closed, checkable one.

Deterministic rollups
Before — totals like "Total OPEX" depend on every individual mapping being correct and consistently grouped; no formal parent/child structure exists.
After — rollups are computed by code, traversing rollup_parent, independent of how any single leaf mapping was decided.

Removes an entire class of error from the agent's responsibility — aggregation becomes arithmetic, not inference.

Confidence audit sampling
Before — tier-1 mappings (confidence ≥ 0.85) auto-confirm with zero review; a confidently wrong mapping is indistinguishable from a correct one.
After — a sample of auto-confirmed mappings is checked against the entity's historical confirmed set, surfacing drift before it compounds across files.

Closes the gap directly raised in the earlier pipeline comparison — this is the validation step that was missing.

Retry-with-correction + dead-letter queue
Before — a failed mapping batch is logged and dropped; a Worker phase that exhausts its 3 retries is dropped with no recovery path.
After — a failed batch gets the parse error fed back for 1–2 corrective retries; a Worker-side failure lands in a dead-letter queue instead of vanishing.

Reliability, not accuracy — but unrecovered failures are themselves a fidelity problem: a label that silently never gets mapped is indistinguishable from one mapped wrong.

Tech stack & deployment

What this actually runs on

Pinned versions and resource names, read directly from the repository — not approximated.

FastAPI service
main.py
  • fastapi 0.111.0
  • uvicorn 0.29.0
  • boto3 1.34.0
  • psycopg2-binary 2.9.9
  • httpx 0.27.0
  • openpyxl 3.1.2
  • python-multipart 0.0.9
  • Python 3.11.0
Procfile: uvicorn main:app --host 0.0.0.0 --port $PORT
Cloudflare Worker
calibrax-classify
  • @anthropic-ai/sdk ^0.104.1
  • @neondatabase/serverless ^1.1.0
  • xlsx ^0.18.5
  • compatibility_date 2024-06-13
  • compatibility_flags nodejs_compat
SDK imported, unused — actual calls go via raw fetch()
Cloudflare resources
calibrax-pipeline · tnba-files
  • Queue calibrax-pipeline
  • max_batch_size 1
  • max_batch_timeout 30s
  • max_retries 3
  • R2 bucket tnba-files
  • CORS all origins
No dead_letter_queue configured