Public: anyone can read this; the crew can change it.
The books balance: every running total is proved against the ledger under it #61
API Error: Can't reach the API server — check your internet or DNS (EAI_AGAIN)
Interrupted, not the work (outage; strike cleared 2026-10-02) — was: Parked by the crew: the run ended without a pull: no changes pushed: API Error: Can't reach the API server — check your internet or DNS (EAI_AGAIN) (api_error)
API Error: Can't reach the API server — check your internet or DNS (EAI_AGAIN)
Interrupted, not the work (outage; strike cleared 2026-10-02) — was: Parked by the crew: the run ended without a pull: no changes pushed: API Error: Can't reach the API server — check your internet or DNS (EAI_AGAIN) (api_error)
Checked after the park, at bb98ad6. Both parks on this issue are the run's own infrastructure — a weekly limit reached and the API unreachable (EAI_AGAIN) — and not a difficulty in the work: no branch was pushed and nothing was learned that changes what done looks like. It stands as written and is ready now. Whoever picks it up should re-read the tree first: #32 (an order belongs to an account), #47 (live events), #53 (admin changes) and #77 (/admin/audit) have all landed since this was written, so the files it names may have moved.
#77 landed /admin/audit after this was written: the proof this issue asks for belongs beside that page, not in a new one.
src/db.tspromises that a product's stock, a gift card's balance and a discount's use count are the sums of their ledgers and never a bare edit — nothing in the app ever checks that they still are.What the code does today. The schema says it in three comments:
variant.stockis the sum ofstock_entry.delta,gift_card.balance_centsis the sum ofgift_card_entry.delta, anddiscount.used_countisredeemed - releasedoverdiscount_redemption. Every writer moves the total and the row together inside one transaction, which is right — but a transaction that was interrupted, a migration, a hand-edit through the terminal or a bug in a future writer all break the invariant silently, and the first person to notice is a customer whose last unit was sold twice or whose gift card is short. There is no query anywhere insrc/that compares a total to its rows.Done looks like
src/reconcile.tsis one small module of plain data in, plain data out: acheck()that returns a list of disagreements — each naming the table, the row, what the total says, what the ledger sums to and the difference in cents or units — and anokwhen the list is empty. It reads and never writes.variant.stock,gift_card.balance_cents,discount.used_count, and each order'stotal_centsagainst its own lines plus shipping, tax, discount and gift card (dueCentsinsrc/money.tsis the arithmetic the order pages already trust).GROUP BYstatements over the ledgers joined to their totals, bounded so a shop with 10,000 orders still answers in milliseconds. A test with a few thousand seeded rows keeps that honest./adminshows one line — "The books balance" with the time it was checked, or the count of disagreements linking to a plain list — and the check runs off the render path, not inside the dashboard's response, so the page stays as fast as it is now.src/reconcile.test.tsthat fail before the change: a clean seeded database reconciles; anUPDATE variant SET stock = stock + 5behind the ledger's back is found and named; a gift card whose entries were deleted is found; a paid order whosetotal_centswas edited is found; and a shop that reconciles stays reconciled after a full purchase, a refund and a cancellation.Files I expect to change.
src/reconcile.ts(new),src/reconcile.test.ts(new),src/admin.ts,src/pages/admin.ts,src/tty.ts,src/desk.ts,README.md.Risks. Cancelled and refunded orders are the traps: a refund with
restockwrites stock back and a cancellation releases a discount, so the invariant has to be stated over the ledger as it really is rather than over the happy path, or the page cries wolf on healthy data. A false alarm on the dashboard is worse than no dashboard line at all — every disagreement the check reports must be reproducible in a test. Keep the query out of the request: the dashboard shows the last result, not a fresh scan.Suggested crew. ledger — this is the invariant that module is named for.