Skip to main content

Worked cases: the Bookkeeper

The Bookkeeper turns an approved document into a proposed journal entry and then stops. Nothing here is doubled. The compiled graph, the real posting rules and the real ledger writer run against a real alembic upgrade head Postgres, so the advisory lock, the 18 GL invariants, the hash chain and the GL idempotency key are all live in every case. The line that makes this page worth reading is on the first case, and it is a number: expect_entry_count: 0.

bk-propose-material-vat-invoice · right answer, empty ledger

Given an approved intake row: doc type supplier_vat_invoice, bill kind material-VAT. The agent produced a proposal: Graded on
The proposal is correct and the ledger is still empty.Three separate facts are graded here, and they are graded separately on purpose: the accounts are right, the proposal balances, and nothing was written.An agent that proposed the right entry and posted it would pass the first two and fail the third. That is the whole design of this lane, expressed as an assertion.

bk-propose-input-vat-split · why balance is not enough

Given a supplier VAT invoice with the amounts stated separately: net 1,000,000, VAT 100,000, gross 1,100,000. The agent produced a three-leg proposal: Graded on debit accounts 1200 and 1300, credit 2000, amount 1,100,000, balanced, gate held, ledger empty. Why it matters, and it is the sharpest illustration on this page. The old behaviour lumped the recoverable input VAT into the inventory leg: Dr 1200 1,100,000 / Cr 2000 1,100,000. That balances. It passes every GL invariant. It hashes into the chain identically well. It is also wrong, in a way that costs real money: the recoverable VAT is an asset the business can reclaim, and buried inside inventory it is neither reclaimed nor visible.
Balance cannot tell those two entries apart. Only the accounts can.Which is exactly why the judgment layer needs its own board: the deterministic engine is doing its job perfectly on a wrong answer, and no invariant in the system will ever object.

bk-propose-canteen-by-doc-type · fallback, never suspense

Given an intake row with no bill kind, doc type canteen_invoice, amount 350,438,400. Graded on debit 6400 (office and admin expense) resolved by the doc-type fallback, credit 2000, balanced, gate held. Why it matters. There is a wrong answer available that looks tidy: book it to a suspense account and let someone sort it later. The registry resolves the account instead. A suspense fallback is a decision deferred into a place nobody revisits, which is the next case.

bk-unmapped-account-hard-error · the refusal must actually raise

Given an unmappable document: doc type unknown_doc, bill kind mystery-kind. Graded on
The grading here is deliberately stronger than “nothing posted”.A silent no-op also posts nothing. So does a crash, a swallowed exception and a graph that quietly ends.This case grades that the error is actually raised, because the legacy poster this replaced booked an unmappable bill to suspense 6900 and reported success. An unmapped account is a question for a human, and the only honest way to ask it is to stop.

bk-post-one-balanced-entry · the full loop, exactly once

Given an approved intake, bill kind service, amount 5,000,000, with a distinct third person approving the posting. The agent produced exactly one entry: Dr 6410 Services 5,000,000 / Cr 2000 Accounts payable 5,000,000. Graded on status posted, the two accounts, balanced, expect_entry_count: 1, the posted entry balances in base currency in the ledger, the intake row flipped to posted, and the row stamped with its resulting entry id. Why it matters. This is the one case where something does reach the book of record, and it is graded end to end rather than at the graph’s own report: the entry count and the balance are read out of Postgres, and the link back to the source document is checked in the row.
This case was permanently red on main and nobody noticed.The eval fixture set an approver and a checker but never a submitter, and strict separation of duties needs three distinct named people - so every posting scenario died at the gate before account mapping was even reached.The Bookkeeper’s two most important cases, “it posts a balanced entry” and “it does so exactly once”, had never passed.A permanently red case hides a regression exactly as well as a permanently skipped one. That lesson produced the expected-baseline marker, so a known failure now has to declare itself by name.

bk-idempotent-relaunch · a double-approve is a no-op

Given the same intake, approved and posted, then relaunched on a fresh thread. Graded on the relaunch short-circuits at ingest, proposes nothing, reports posted, and the ledger entry count is still 1. Why it matters. A double-click on Approve, a retried request, a resumed run after a restart: all three are the same event arriving twice. The GL idempotency key makes the second one a no-op at the writer, and this case checks the agent does not paper over it by proposing a second, different entry.

Separation of duties, graded twice

The refusal happens before the transaction opens, so nothing reaches the ledger and nothing needs rolling back. See two gates, three people for the rule, and the posting mandate for what makes it hold when someone attacks it directly.

bk-non-postable-intake-refused · eligibility

Given a row still pending human review. Graded on it never reaches propose. The gate is not the only thing standing between a document and the ledger. A document that no human has approved is not a candidate for posting at all, and the graph declines it a step earlier.

What the whole page adds up to

Seven of the eleven cases assert expect_entry_count: 0. That is not padding. The most valuable property of this agent is what it does not do, and a property nobody asserts is a property nobody will notice losing.