Sprint 44 — a column nothing ever wrote
Most sprints start with a plan. This one started with me looking at my own Transactions page and being mildly annoyed four times in a row.
A June invoice showing an August date. A subscription that had quietly filed itself as an order. A spend panel where an Amazon order, a Netflix charge and the hydro bill sat in one column that summed to nothing useful. And a misfiled charge I could see was wrong and could not fix — because fixing it meant an engineer, and the engineer was me, and it was Tuesday night.
Every one of those turned out to be a structural bug wearing a cosmetic disguise. Sixteen PRs (#1564–#1597), three migrations, five production promotes from v1.0.165 to v1.0.168.
The column nothing ever wrote to
The date bug is my favourite of the sprint, because everything about it was already built.
orders.placed_at existed as a column. findOrCreateOrder accepted a placedAt argument. The order detail page had a field for it. All shipped, all correct — and no production callsite ever passed a value. So the column was NULL on every row, and the list quietly fell back to last_event_at: the wall-clock moment the document was ingested. A June 23 invoice uploaded on August 9 read “Aug 9” next to its own “Jun 23” charge.
The fix is a ladder that tries the printed purchase date, then the issued date, then the statement date, then the email’s sent date, then gives up. Strict ISO parsing only — an ambiguous 03/06/2026 is rejected, not guessed, because a date that might be March or June is worse than no date. The enrichment merge had to change too: it used to fill only when NULL, but documents arrive in any order, so a June invoice landing after a July shipping notice has to pull the date backwards. Earliest wins.
Then the backfill: 68 of 92 production orders re-dated. One moved from August 2026 to September 2024.
The lesson has a shape I keep meeting: a parameter nothing supplies is indistinguishable from a missing feature. Reading the code tells you it works. Only the data tells you it never ran.
Check the assumption against real rows, not against reasoning
Twice this sprint the obvious design would have been wrong, and both times a query caught it before the code did.
The Subscriptions tab was going to be backed by the obligations table — clean, already carries cadence and renewal terms. So I checked what my household’s subscriptions actually looked like first. GitHub, Granola, Gumroad, Vercel: almost all of them existed as charges with no obligation attached. An obligations-only tab would have shipped with two rows in it. Hence the untracked-charges section, which deliberately shows no renewal date — Domi hasn’t seen the terms, so it doesn’t get to imply it has.
Same story on the ingestion gate. Subscription invoices were becoming Orders, and the obvious fix was to require evidence of goods — a tracking number, a delivery signal. Measured against the 92 real orders: 65 have no tracking number, and 61 have neither tracking nor a purchase invoice. That gate would have suppressed two-thirds of genuine orders. It got solved from the other direction instead, with a positive subscription signal coming out of document extraction.
Neither of those was caught by being careful. Both were caught by running a query.
The leak that never reached production
Lifting the filters and the export into one shared component turned up something that had shipped that same day: the subscriptions queries accept a set of visible groups, and the page wasn’t passing it. A private group’s subscriptions would have been listed to every member of the household.
Introduced and fixed inside one day, never promoted. I’m writing it down anyway, because a dev log that only records the bugs that escaped is a marketing document.
The same class of problem showed up bigger on orders: the table carried only a member_id — no group, no junctions. Adding group_id turned every existing read into a potential leak, so four things had to move together rather than one at a time: the list, the detail view (a private-group order is now a 404 by URL, not merely absent from a list), the chat and MCP tool — which took no user ID at all — and the knowledge-graph node.
Vendor of record
A small rule with a long tail: capture the service sold, never the payment intermediary. Apple One is Apple. Claude Max billed through Apple is Anthropic.
Writing that down exposed a live bug in shipped code. The duplicate-detection for new obligations compared a candidate’s vendor against an existing obligation’s display name — so a “Claude Max” proposal billed by Apple was swallowed as a duplicate of “Apple One Premier” and never surfaced at all. The registry that fixes it is deliberately a bar-raiser rather than a rewrite table: nothing maps “Apple” onto a real vendor. It only withdraws the assumption that a shared payment vendor proves a shared commitment.
Giving the correction back to the household
The sprint’s actual goal, underneath the four papercuts: when Domi files something in the wrong bucket, the person who lives in the house should be able to move it. Without an engineer, and without a support ticket.
That became a bucket column with a sticky manual lock — deliberately unlike the order-status lock, which only pins a hand-set “delivered”. A status is a position on a timeline that later events can legitimately advance. A classification is a judgement with no timeline, so once a human overrules it, it stays overruled.
Building it exposed that the requirement wasn’t merely missing, it was violated: the Transactions tab was listing every transaction, so an order’s charge appeared under Orders and again in the ledger. And a second pass was needed after that, because moving a row between tabs turned out not to be a column update at all — an Orders row and a Subscriptions row are different entities, so it’s a conversion.
Process debt, paid
Three findings this sprint were about tests that looked green and weren’t:
- Seven of eleven env-gated web suites had never run in CI. The workflow ran a hard-coded three-file list. Three of the seven were themselves drift gates — gates that were not being gated.
- One eval could not have passed since July 27. The role was re-routed to a different provider while the workflow injected only the old provider’s key. Runs passed the gate, then died. It read as green because the workflow is path-filtered and skipped runs look like successes.
- The role that reads every household document had no eval workflow at all — while its prompt changed three times in one day. It has one now, plus a drift gate that derives the required set from the catalog, so adding a role without adding CI fails at pull-request time. It caught its own first bug within the hour.
Five production data repairs ran behind all of this, each with a dry run I reviewed first. Worth recording: for an LLM-driven backfill, the dry run is not a preview. The apply produced six subscriptions where the dry run promised seven, changed a currency, and merged two Claude Max charges into Apple One Premier — which then needed unpicking by hand. The deterministic backfill, order dates, matched its dry run exactly. That difference is now written down where the next backfill will find it.
What actually caught the misses
Two real misses this sprint, and both came from the same mistake: I specified a mechanism instead of an outcome.
One ticket said “archive the wrong orders” — and that is exactly what shipped, removing them from the wrong bucket without putting them in the right one. Another said “a per-row control”, which got built for the one entity that had the column, leaving two others unreachable.
Tests were green through both. What caught them was me using the app.
That’s the argument for dogfooding stated as plainly as I can state it. Not that it finds bugs — that a test can only check the thing you asked for, and the thing you asked for is where the mistake was.
Carried forward
A duplicate transaction sitting in production, double-counting a GitHub bill. Two subscriptions with no renewal date because they predate this work. A shopping-list test that flakes locally under unsharded runs and passes in CI’s four-way shard — test concurrency, not a product bug. And seven charges deliberately left untracked: real commitments nobody has recorded terms for. The sweep is forbidden from inventing them.