Ground truth on page 3 of the Halifax test statement: 34 transactions. The 4-bit quantised Llama 3.2 1B did not produce 34 transactions.
That failure was decisive. Phase H’s on-device LLM spike had been taken through to first inference in Sprint 5 via mlx-swift 0.21.2 and mlx-swift-examples. A model that hallucinates transaction counts on financial statements is not a model you can use for financial statements. PF-002 disabled the path the same sprint it shipped.
But the model failure was the more visible problem, not the more interesting one.
Before PR #13 landed Phase I, the import layer had three coexisting hash schemes with no shared contract between them. PDFs used positionHash(accountId, fileHash, page, row), a SHA hex digest encoding position on page relative to a specific document. CSV transactions used a different scheme entirely, derived from row fields rather than coordinates. A third handled the general import case. None could deduplicate against the others. None carried source-document attribution either: downstream code had no reliable way to answer which file a given transaction came from.
That was the structural problem Phase I had to solve before universal parsers were possible at all.
How three schemes end up in one codebase
They don’t appear at the same time. The PDF scheme appeared first because the PDF parser was the first import path built. The CSV scheme appeared later, with different identity requirements at the time. A third accumulated as general import infrastructure arrived. Each was locally reasonable. The sum was not.
By the time Phase I was in scope, the dedup layer had a coherence problem. Re-importing a file that mixed PDF and CSV sources would produce duplicates, because the two schemes had no common identity. Cross-format deduplication was structurally impossible. Attribution (which file a transaction came from) was unanswerable from the data model.
CDImportSource
CDImportSource is a source-attribution model. Every import operation stamps each transaction with a reference to the source document. From that reference, a single consistent hash derives the dedup identity. The three prior schemes collapsed into one.
The immediate structural consequence was that the parser layer became bank-agnostic by default. Attribution is uniform; the hash derives from the source reference in the same way regardless of whether the source is a Monzo CSV or a Halifax PDF. There is no technical argument for branching at the dedup layer on file type, because the dedup layer no longer cares about file type. It cares about position within a source document, and CDImportSource carries that consistently.
The CSV route
The universal CSV parser uses HeuristicMapper. Two paths.
The fast path is preset signature matching. Four banks are registered today: Monzo, Halifax, Revolut, HSBC. Each has a distinct column structure in its exports, and the preset registry encodes those signatures. If a file’s column headers match a registered preset, the mapper selects it and proceeds. Deterministic.
The slow path is heuristic: inspect column names and value distributions to infer which column carries the date, which carries the amount, and which carries the narrative. This covers formats not yet registered as presets.
Zero per-bank conditionals in the parser. Per-bank variation lives in the preset registry as data, not as logic. Adding a fifth bank means adding a preset, not modifying the parser.
PI-003 validated across all four banks. Closed.
The PDF route
Sprint 5 wired in the on-device LLM as the primary PDF extraction path. mlx-swift 0.21.2, mlx-swift-examples, Llama 3.2 1B at 4-bit quantisation. Phase H had run a spike; Sprint 5 made it the production route on AnF.
The replacement came the same sprint.
A model that, given a formatted bank statement, produces an incorrect transaction count is not a model you use for financial records. There is no acceptable tolerance here. A hallucinated amount is wrong, not approximately right. A missed transaction is a gap, not an approximation. The failure mode on Halifax page 3 was not at the margin.
The band-based parser replaced it. Pages are divided into horizontal bands; transactions are detected by vertical position relative to known statement structure. No inference. Coordinate geometry. The Halifax validation: 54 out of 54 transactions extracted correctly.
PI-004 closed. PDF parser parked at v1 per CJ. The band-based approach is correct for Halifax’s layout; generalising to other bank PDF formats is a separate sprint with its own validation surface. V1 boundary is the right place to stop.
The dependency order
This is worth naming explicitly, because the PR description led with the parser work.
The parsers are the visible outcome. CDImportSource is the enabling condition. Without a unified attribution model, a “universal” parser is incomplete: it accepts input from multiple sources but cannot deduplicate across them. Re-importing a file produces duplicates. Cross-format imports have no common identity. The parser’s correctness depends entirely on the dedup layer’s completeness.
It would be easy to misread this as two parallel workstreams. It was not. The sequence was strict: attribution model first, universal parser second. PR #13 shipped both, and the devlog records the order.
What it looks like from the import screen
A Monzo CSV comes in. HeuristicMapper matches the Monzo preset. Transactions are extracted, attributed via CDImportSource, hashed with the unified scheme, deduplicated against the database. Re-import of the same file: same hashes, duplicates identified and rejected.
A Halifax PDF comes in. The band-based parser reads the page geometry. 54 transactions extracted. Same CDImportSource attribution. Same dedup layer as the CSV path. Same hash scheme.
No conditionals in the import controller asking whether this is Monzo or Halifax. File type determines which parser runs. Below that level, everything is uniform.
What’s out of scope
Band-based parser generalisation beyond Halifax. Other banks’ PDF layouts will need their own band configurations. Follow-on sprint, clean seam.
Slow-path heuristic validation. The heuristic CSV path exists; it hasn’t been validated against bank exports outside the four presets. Follow-on.
On-device LLM as a PDF path. Not dead in principle. Dead at 4-bit quantisation on financial statements. If future model quality changes that calculus, the revisit needs a controlled spike with a higher evidence bar than Phase H set.
The category layer
The 2026-06-16 sprint (B-051, smart category propagation) landed directly on top of this work. Three identical DVLA transactions with one categorised raised the question: why doesn’t the categorisation propagate? The answer requires knowing with confidence that the transactions are identical. That confidence comes from the dedup layer working correctly.
Category propagation is a separate piece. The reason it arrived next is that import correctness was finally reliable enough to build on.



