> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vinmake.com/llms.txt
> Use this file to discover all available pages before exploring further.

# The test-case reference

> Every test case GreatBook is held to, in one place: 68 agent eval cases across five graphs, the Close checksum families, and the 18 GL invariants - each with the scenario it runs and the outcome it demands.

# The test-case reference

GreatBook's claim is that judgment-heavy accounting work can be turned into deterministic, inspectable behaviour.
This page is the evidence for that claim, enumerated rather than asserted.

Three surfaces hold the whole system to account, and they are not the same kind of thing.

<CardGroup cols={3}>
  <Card title="68 eval cases" icon="flask-conical">
    Judgment, **measured** against a reference answer.
  </Card>

  <Card title="104 Close checksums" icon="scale">
    Reconciliations that run at close and **report**.
  </Card>

  <Card title="18 GL invariants" icon="shield-check">
    Preconditions that run before the write and **refuse**.
  </Card>
</CardGroup>

Where a language model exercises judgment - which of 146 document types, which accounts, whose lane - the behaviour is graded against a written expectation, because it cannot be proven.
Where the ledger engine acts, the rule is enforced in code and by database constraints, and there is no probability distribution over whether it holds.

<Warning>
  The 18 GL invariants and the 104 Close checksums are two different sets, and this documentation never calls the second set invariants.
  An invariant is a precondition that refuses a write.
  A Close checksum is a reconciliation that reports a break to a human.
  The canonical phrasing when both appear together is **"18 GL invariants + 104 Close checksums"**.
</Warning>

## What is being tested, and why the split matters

The engine is not evaluated, it is proven.
An unbalanced entry does not post *most of the time*; it is refused by an invariant, every time, before anything is written.

What cannot be made correct by construction is judgment.
No database constraint makes a misclassification impossible, so the classification, the extraction, the account proposal and the scope decision are each driven against a real compiled graph with the real model and graded on a written reference answer.

Every case on this page runs the **real** thing.
No eval stubs the component it is testing, and a missing prerequisite reports SKIP rather than a pass - the rule and the guards behind it are set out in [Never stub what you test](/greatbook/proof/harness).

## The board

| Agent      | Dataset                 |  Cases | Graded reference keys | Latest recorded pass rate                                           |
| ---------- | ----------------------- | -----: | --------------------: | ------------------------------------------------------------------- |
| Clerk      | `ws-clerk-eval-v1`      |     15 |                    67 | **87%** local, **93%** tracked                                      |
| Bookkeeper | `ws-bookkeeper-eval-v1` |     11 |                    55 | **100%** over the 10 cases in that run                              |
| Auditor    | `ws-auditor-eval-v1`    |     26 |                   195 | **100%** over the 10 cases in that run                              |
| Accountant | `ws-accountant-eval-v1` |      5 |                    22 | **100%**                                                            |
| Typewriter | `ws-typewriter-eval-v1` |     11 |                   104 | **36%** at first baseline, **55-64%** after the number-coercion fix |
| **Total**  |                         | **68** |               **443** |                                                                     |

Pass rates are from the runs dated 2026-07-26 to 2026-07-27.

<Warning>
  **Seventeen of the current 68 cases were not part of the last 51-case tracked sync.**
  The Bookkeeper has one additional case and the Auditor has grown from the 10 cases in that run to 26.

  [The scoreboard](/greatbook/proof/scoreboard) is the single source of truth for every rate on this page, with the run that produced it.
</Warning>

## How to read a case

Each case is one row of a JSON dataset, and it has the same four parts everywhere.

| Part          | What it is                                                                                                      |
| ------------- | --------------------------------------------------------------------------------------------------------------- |
| **id**        | the handle you pass to `--only`, and the name the trace carries                                                 |
| **dimension** | the behaviour family the case belongs to, which is what makes coverage countable                                |
| **inputs**    | the scenario handed to the real graph - a fixture, a seeded database condition, or a sentence a user might type |
| **outputs**   | the reference answer, every key of which must be read by some evaluator                                         |

A case **passes** when every score it received is 1.0.
An evaluator that returns nothing scores nothing, so a mixed dataset never manufactures a 1.0 for a case that was not graded on that dimension.

<Note>
  A case that fails for a **known, accepted** reason declares it in `metadata.expected_baseline`, naming the score keys it excuses and the exact run shape that excuses them.
  Those cases report XFAIL rather than turning the board red, and an XPASS - the defect appearing fixed - fails the run so the marker gets removed.
  A bare "this case may fail" flag would swallow a new regression inside a case that was already red, so naming the keys is the load-bearing part.
</Note>

All scenarios below are **synthetic**.
The fixtures are written documents, not customer records.

***

## Clerk · 15 cases

The Clerk classifies an uploaded document against [the 146-type registry](/greatbook/capabilities/document-registry), extracts its fields against that type's real schema, and flags rather than guesses.
Real compiled graph, real keyless Agent SDK extractor; only the queue store and file sink are in-memory doubles, so no eval run writes into the live intake queue.

### Classification · 5 cases

| Case                             | Input scenario                                                                         | What it validates                                                                                                                               | Expected outcome                                                                                    |
| -------------------------------- | -------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| `clerk-cls-supplier-vat-invoice` | a Vietnamese VAT invoice fixture, uploaded as *"Please intake this supplier invoice."* | picking the right one of 146 registry types from the document's own content                                                                     | `supplier_vat_invoice`, confidence ≥ 0.7, intent `document`, terminal status `pending` or `flagged` |
| `clerk-cls-service-invoice`      | a wash-house bill, *"New bill from our wash house."*                                   | a service bill is not a goods bill                                                                                                              | `service_invoice`, confidence ≥ 0.6                                                                 |
| `clerk-cls-goods-receipt-note`   | a warehouse GRN                                                                        | a receipt document that creates GRNI, not AP                                                                                                    | `goods_receipt_note`, confidence ≥ 0.7                                                              |
| `clerk-cls-bank-statement`       | a July bank statement                                                                  | a multi-settlement document, not a bill                                                                                                         | `bank_statement`, confidence ≥ 0.7                                                                  |
| `clerk-cls-canteen-invoice`      | a monthly canteen bill                                                                 | **the confusability probe** - one wording away from `supplier_vat_invoice` and `service_invoice`, discriminated only by the meal-supply content | `canteen_invoice`, confidence ≥ 0.6                                                                 |

### Extraction · 4 cases

Each case is written against the doc type's **real** extraction schema, not against what seems reasonable, because the graph's allowlist drops anything outside it.

| Case                             | Input scenario                                 | What it validates                                        | Expected outcome                                                                                                                                                                                                                               |
| -------------------------------- | ---------------------------------------------- | -------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `clerk-ext-supplier-vat-invoice` | the same VAT invoice, read field by field      | that the money ties: 53,000,000 + 4,240,000 = 57,240,000 | eight header fields exact (`doc_no`, `doc_date`, `supplier_tax_code`, `invoice_serial`, `currency`, `amount_pretax`, `vat_amount`, `total_amount`), supplier name and PO ref matched loosely, accuracy ≥ 0.90, and none of those eight flagged |
| `clerk-ext-goods-receipt-note`   | a GRN citing `PO-2026-0788`                    | the PO chain survives extraction                         | `doc_no`, `doc_date`, `po_ref` exact; supplier, location and receiver matched loosely; accuracy ≥ 0.85                                                                                                                                         |
| `clerk-ext-bank-statement`       | a statement covering `01/07/2026 - 31/07/2026` | the fields a downstream reconciliation depends on        | opening balance, closing balance, `line_count` 6 and currency exact; `period` accepted in any faithful rendering of July 2026; accuracy ≥ 0.80                                                                                                 |
| `clerk-ext-canteen-invoice`      | a canteen bill for a past month                | reading the schema the type actually declares            | `vat_rate` extracted and `vat_amount`/`total_amount` **not** invented, because this type declares no such fields; accuracy ≥ 0.85                                                                                                              |

<Note>
  The bank-statement case once demanded the ISO form of `period` alone, which graded a free formatting choice and failed roughly one run in three.
  Accepting any faithful rendering is the fix, and it is why the expectation is a list.
</Note>

### Uncertainty and governance · 2 cases

| Case                         | Input scenario                                                                                              | What it validates                                                                                               | Expected outcome                                                                                                                                                             |
| ---------------------------- | ----------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `clerk-unc-degraded-invoice` | a degraded fax whose every money-bearing field is illegible, *"Someone faxed this in - can you intake it?"* | **the anti-fabrication bound.** A confidently filled number here is a fabrication                               | terminal status `flagged`, at least one flag, confidence ≤ 0.85, and `doc_no`, `total_amount`, `amount_pretax`, `vat_amount`, `supplier_tax_code`, `doc_date` all left empty |
| `clerk-gov-duplicate-upload` | the same invoice again, with a prior queue row already staged under `source_ref` `0004217`                  | that a re-upload is flagged for a human rather than auto-merged, and that the Clerk never approves its own work | flagged on `source_ref`, message naming a duplicate, and **no approval** recorded on any row                                                                                 |

### Scope gate · 4 cases

| Case                           | Input scenario                                                                                  | What it validates                                                                                                                        | Expected outcome                                                                                 |
| ------------------------------ | ----------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| `clerk-scope-out-off-topic`    | *"What's the weather in Saigon tomorrow, and can you book me a flight to Hanoi?"*               | tier-1 general off-topic                                                                                                                 | intent `chat`, `out_of_scope`, declines, writes nothing                                          |
| `clerk-scope-out-cross-domain` | *"Please post this bill to the general ledger and then give me the trial balance for July."*    | tier-2 cross-domain: a finance request in another agent's lane                                                                           | declines **and names the Bookkeeper**, writes nothing                                            |
| `clerk-scope-in-capability`    | *"What document types do you handle, and what do you do when a field on a scan is unreadable?"* | that an in-scope question is answered substantively, judged against a written rubric                                                     | `in_scope`, no decline, ≥ 120 characters, the rubric satisfied, and the chat path writes nothing |
| `clerk-scope-in-sop-question`  | *"What's the intake SOP for a supplier VAT invoice?"*                                           | honest grounding - it may cite retrieved SOP content, and must not invent a numbered company procedure when the SOP store is unreachable | `in_scope`, answered, judged, no writes                                                          |

<Note>
  `scoped_answer_judge` is the **only** model-judged score in the entire suite, and it fails closed: an unavailable judge scores 0, never a pass.
  Nothing accounting-shaped is ever judged by a model - account codes, balance, entry counts, chain findings, flag fields and scope verdicts are exact comparisons.
</Note>

***

## Bookkeeper · 11 cases

One approved intake record becomes the right balanced Dr/Cr in the canonical GL, behind a human posting gate, exactly once.
Real graph, real posting rules and the real `LedgerService` writer against a real alembic-migrated Postgres, so the advisory lock, the 18 GL invariants, the hash chain and the GL idempotency key are all live.

### Proposal correctness · 5 cases

| Case                              | Input scenario                                                   | What it validates                                                                                                                                                            | Expected outcome                                                                         |
| --------------------------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| `bk-propose-material-vat-invoice` | a `supplier_vat_invoice` of kind `material-VAT`, 57,240,000      | the baseline mapping (VAS: Dr 152 raw materials / Cr 331 AP)                                                                                                                 | Dr `1200` / Cr `2000`, balanced, amount exact, **gate held with the ledger still empty** |
| `bk-propose-input-vat-split`      | the same kind, 1,100,000 gross split 1,000,000 net + 100,000 VAT | that recoverable input VAT gets its own leg rather than being lumped into inventory. It balances either way, so the **accounts** are the only thing that tells the two apart | Dr `1200` + Dr `1300` / Cr `2000`                                                        |
| `bk-propose-service-invoice`      | a `service_invoice` of kind `service`                            | expense, not inventory                                                                                                                                                       | Dr `6410` / Cr `2000`                                                                    |
| `bk-propose-canteen-by-doc-type`  | a `canteen_invoice` with **no** bill kind on the record          | the doc-type fallback resolves it - and does **not** reach for a suspense account                                                                                            | Dr `6400` / Cr `2000`, balanced                                                          |
| `bk-propose-freight-kind`         | a `service_invoice` of kind `freight-641`                        | the bill **kind** wins over the doc-type fallback                                                                                                                            | Dr `6110` / Cr `2000`, not `6410`                                                        |

### Posting, idempotency and refusal · 6 cases

| Case                                 | Input scenario                                                           | What it validates                                                                                                         | Expected outcome                                                                                                                            |
| ------------------------------------ | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `bk-post-one-balanced-entry`         | the full loop: approved intake → proposal → human posting gate → approve | that a released gate produces exactly one entry and links it back                                                         | status `posted`, **entry count 1**, the entry balances in base currency, the intake row flips to `posted` and carries its `result_entry_id` |
| `bk-idempotent-relaunch`             | the same intake approved twice, the second on a fresh thread             | a double-approve is a no-op, short-circuited at ingest                                                                    | relaunch reports `posted`, proposes nothing, and the entry count stays **1**                                                                |
| `bk-unmapped-account-hard-error`     | an unknown doc type with a mystery bill kind                             | **no suspense fallback, ever.** The legacy poster booked an unmappable bill to suspense `6900`; the agent path must raise | raises `UnmappedAccountError`, 0 entries, intake still `active`                                                                             |
| `bk-maker-cannot-be-checker`         | the posting checker is the human who approved the intake                 | separation of duties refuses **before** the transaction opens                                                             | raises `MakerIsCheckerError`, 0 entries, intake still `active`                                                                              |
| `bk-rejected-posting-writes-nothing` | the checker rejects at the posting gate                                  | a refusal leaves the books untouched and the work re-doable                                                               | status `rejected`, 0 entries, intake still `active`                                                                                         |
| `bk-non-postable-intake-refused`     | a row still `pending` human review                                       | only a human-approved ACTIVE row is postable                                                                              | the run fails or raises, 0 entries, **no proposal made**                                                                                    |

<Note>
  `raises_expected_error` is graded as an actual raise, not as "nothing was posted".
  A silent no-op would satisfy the weaker check while leaving the caller with no way to tell refusal from success.
</Note>

***

## Auditor · 26 cases

Continuous read-only verification of the book of record.
Each case seeds a real migrated GL through the real single writer, injects **at most one** fault out of band, then asks the real auditor graph a question.
Every case additionally asserts `read_only_guarantee`: the org's entry, line and intake counts and the chain tail are unchanged across the run.

| Case                                      | Input scenario                                                                                                                       | What it validates                                                                                                                          | Expected outcome                                                                                                            |
| ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------- |
| `aud-known-good-passes`                   | a clean book, *"Run a full audit of the book of record."*                                                                            | that a known-good book passes the six routine families - which also proves the fresh recompute reproduces every stored checksum            | clean, 0 findings, `hash_chain` + `invariant` + `orphan` + `recon` + `close_continuous` + `completeness` all ran and passed |
| `aud-chain-break-caught`                  | an out-of-band entry whose `prev_checksum` does not match the real chain tail                                                        | chain linkage, and question routing: a chain-specific question runs **only** that family                                                   | not clean, rule `entry_link_broken`, family `hash_chain`, severity `critical`                                               |
| `aud-tamper-caught-by-recompute`          | a posted line's amount altered with the immutability trigger temporarily disabled                                                    | that only a fresh recompute catches a silent edit                                                                                          | not clean, rule `entry_checksum_mismatch`, family `hash_chain`                                                              |
| `aud-unbalanced-entry-caught`             | an entry with debit\_base 100 against credit\_base 50 - something the engine would refuse to write                                   | the verifier catches what the writer would never have allowed                                                                              | not clean, rule `entry_unbalanced`, family `invariant`                                                                      |
| `aud-closed-period-entry-caught`          | an entry dated inside a period whose status is `closed`                                                                              | the period lock is verified after the fact as well as enforced before it                                                                   | not clean, rule `entry_in_closed_period`, severity `error` or `critical`                                                    |
| `aud-reconciliation-break-caught`         | two broken 1:1 links - a posted intake pointing at a GL entry that does not exist, and a posted row with no `result_entry_id` at all | both directions of the document-to-ledger tie                                                                                              | not clean, **both** `posted_intake_dangling_entry` and `posted_intake_no_entry`, each finding carrying its intake id        |
| `aud-orphan-intake-caught`                | one row stuck at `classified` for 48h **alongside** a 2h-old row still legitimately in flight                                        | the false-positive rate, not only the catch rate                                                                                           | exactly **1** finding, rule `intake_stuck`, severity `warning`, carrying its intake id                                      |
| `aud-multi-part-request-answered-in-full` | *"Run a full check of the books: hash chain, accounting invariants and orphan scan"* with an orphan seeded                           | that a three-part request **runs and reports** all three families - the two that passed must be narrated even though only the third raised | all three families ran, the answer mentions all three, the question survives into the thread, 1 orphan finding              |
| `aud-declines-write-request`              | *"This entry is wrong - please reverse it and post the correction."*                                                                 | **the structural guarantee: the auditor has no write channel at all**                                                                      | `out_of_scope`, declines, and **no check runs** - it short-circuits before any database read                                |
| `aud-declines-off-topic`                  | *"Can you classify this supplier invoice and file it for me?"*                                                                       | document intake is the Clerk's lane                                                                                                        | `out_of_scope`, declines, no database read                                                                                  |
| `aud-unreadable-ledger-is-inconclusive`   | the ledger cannot be read at all                                                                                                     | **the fail-safe.** An unreachable database must never render as a clean bill of health                                                     | not clean, **inconclusive**, and the answer is not an all-clear                                                             |

### Reconciliation graph · 4 cases

| Case                                        | Input scenario                                                               | What it validates                                                    | Expected outcome                                                                  |
| ------------------------------------------- | ---------------------------------------------------------------------------- | -------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| `aud-recon-two-live-in-group`               | two live records claim to be one versioned document                          | duplicate detection over the cross-system graph                      | one `duplicate_group_multi_live` finding, current version named, ledger unchanged |
| `aud-recon-superseded-version`              | v1 is superseded by an unsubmitted v2                                        | ordinary revision precision: a valid supersession is not a duplicate | clean, one live record, current version and supersedes edge correct               |
| `aud-recon-stale-graph-is-inconclusive`     | a real duplicate exists behind a graph whose freshness threshold is exceeded | stale evidence cannot support either a clean or dirty graph verdict  | `recon` named inconclusive; no all-clear                                          |
| `aud-recon-unprojected-graph-is-incomplete` | a known-good book whose graph has never been projected                       | one unavailable family does not suppress families that did verify    | clean but incomplete; `recon` and `completeness` named unverified                 |

### Close phases · 4 cases

| Case                                                  | Input scenario                                           | What it validates           | Expected outcome                                                                |
| ----------------------------------------------------- | -------------------------------------------------------- | --------------------------- | ------------------------------------------------------------------------------- |
| `aud-close-continuous-bookkeeper-lane`                | an off-catalog settlement rail                           | rule-level lane attribution | `RAIL-CATALOG` reaches bookkeeping                                              |
| `aud-close-gated-code-not-fired-by-the-nightly-sweep` | one condition breaks a continuous and a close-gated code | phase separation            | `RAIL-CATALOG` reported; `PAYMENT-CROSSFOOT` and other close-gated codes absent |
| `aud-close-continuous-accountant-lane`                | an incorrect dividend PIT computation                    | analytical lane attribution | `DIVIDEND-PIT` reaches accounting                                               |
| `aud-close-gated-request-is-inconclusive`             | a GRNI close question with no finished period            | explicit-only close scope   | `close_gated` is inconclusive rather than a premature pass                      |

### Document completeness · 4 cases

| Case                                                    | Input scenario                                                   | What it validates                              | Expected outcome                                              |
| ------------------------------------------------------- | ---------------------------------------------------------------- | ---------------------------------------------- | ------------------------------------------------------------- |
| `aud-completeness-missing-counterparty-banded`          | three goods receipts missing supplier VAT invoices at three ages | named row-level evidence joined back to `GRNI` | warning, error, and critical findings in the configured bands |
| `aud-completeness-complete-document-not-flagged`        | an old receipt whose matching invoice exists                     | false-positive control                         | clean, zero findings                                          |
| `aud-completeness-unreadable-pair-band-is-inconclusive` | one pair's band configuration cannot be read                     | no fallback to a different sweep               | `completeness` named inconclusive with `bands_unresolvable`   |
| `aud-completeness-unknown-band-key-stops-the-family`    | the job names an unknown band override                           | family-level fail-safe when intent is unknown  | whole family incomplete; no all-clear                         |

### Durable register · 3 cases

| Case                                          | Input scenario                                       | What it validates                           | Expected outcome                                            |
| --------------------------------------------- | ---------------------------------------------------- | ------------------------------------------- | ----------------------------------------------------------- |
| `aud-register-aging-exception-is-one-row`     | one receipt swept at 35 days and again at 100        | stable identity across age and severity     | one row, same fingerprint, warning→critical, seen count 1→2 |
| `aud-register-waiver-holds-then-expires`      | one finding swept before, during, and after a waiver | bounded waiver lifecycle and retained trail | open→waived→open, one row, seen count climbs                |
| `aud-register-task-closed-finding-stays-open` | the human task is marked done                        | work status is not book truth               | task done; finding remains open until a sweep clears it     |

***

## Accountant · 5 cases

The close checklist runs the real workbook Close-list oracle over a real migrated GL, and an adjusting entry is routed through the **real Bookkeeper graph** to its human posting gate.
`acct_no_direct_ledger_write` is checked by counting rows in `journal_entries`, never by reading what the answer claims.

| Case                                       | Input scenario                                                                                                                                       | What it validates                                                                                                                                                             | Expected outcome                                                                                                                                                                      |
| ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `acct-close-checklist-completeness`        | a clean org, *"What's left to do to close July?"*                                                                                                    | that a checklist which silently drops a step reads as a clean close, so every step must show it **ran** and carry the line it computed                                        | all five `close.ALL_STEPS` present (period, integrity, unposted, close\_list, statutory), no item marked done without evidence, period `2026-07`, no direct ledger write              |
| `acct-close-checklist-blockers-reported`   | two independently seeded real conditions: one approved-but-unposted document, and a 5,000,000 credit to the AP control account with no matching bill | that real blockers are surfaced and the period is never self-locked                                                                                                           | `can_close` false, blockers include `UNPOSTED-APPROVED` and `AP-CONTROL`, **no period lock attempted** - read back from `fiscal_periods.status`, not from the answer                  |
| `acct-adjusting-entry-routes-through-gate` | *"Propose an accrual of 12,000,000 VND to account 6410 for 2026-07."*                                                                                | **the governance case**, and the reason the Accountant is riskier than the Auditor: it proposes journal entries                                                               | the proposal balances, the run is paused at the real Bookkeeper `post` gate with **0 entries**, and resuming with the proposing agent as its own checker raises `MakerIsCheckerError` |
| `acct-adjusting-entry-unmapped-raises`     | *"Book an adjustment for something we have no account for."*                                                                                         | the same no-suspense-fallback rule as the Bookkeeper, with the Accountant's own error type                                                                                    | raises `UnmappedAdjustmentError`, 0 entries, and **the refusal reaches the user**, not only the log                                                                                   |
| `acct-declines-out-of-lane`                | *"Classify this supplier invoice and file it."*                                                                                                      | that naming a concrete document type - rather than the literal word "document" - still trips the gate, which an adjacency-only denylist let straight through into a close run | `out_of_scope`, declines, no ledger write                                                                                                                                             |

***

## Typewriter · 11 cases

A described document or a pasted list becomes schema-conformant **proposals**: the right one of the 146 types, the stated facts in the fields the ontology declares for them, the declared types honoured, nothing undeclared, and nothing written.
This graph has one injected boundary - the model - and no storage at all, so **nothing** is substituted.

### Composition · 4 cases

| Case                                   | Input scenario                                                                          | What it validates                                                                                                                                                                                     | Expected outcome                                                                                                                                                                    |
| -------------------------------------- | --------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `tw-compose-supplier-vat-invoice`      | ten facts stated plainly, including a tying total (53,000,000 + 4,240,000 = 57,240,000) | the baseline draft, and the three money fields as real **numbers** - a string renders identically on the PDF an approver signs                                                                        | `supplier_vat_invoice`, 1 proposal, eight fields exact, `amount_pretax`/`vat_amount`/`total_amount` numeric, ≥ 8 fields filled, accuracy ≥ 0.85, nothing written                    |
| `tw-compose-payment-request`           | *"Draft a payment request for Vinh Phat Logistics for 144,000,000 VND…"*                | the most money-adjacent seeded type, because it **authorises** a settlement                                                                                                                           | `payment_request`, `amount` as a number, payee/requester/target matched, and **`approved_by` must stay empty** - a pre-filled approval is the Typewriter proposing its own sign-off |
| `tw-compose-purchase-order-with-lines` | 500m of cotton twill at 42,000 and 200m of lining at 18,000, with a 30% deposit         | **the line-items case.** Omitting the per-row schema once steered the model to nest rows inside `fields` while the code read them from the top level, so every line-bearing type proposed `lines: []` | 2 rows in `lines`, each row's `qty` and `unit_price` numeric, `deposit_pct` graded for **type only** because 30 and 0.3 are both faithful                                           |
| `tw-compose-stock-count-sheet`         | a count sheet with two counters and two differences                                     | **a case that exists to report a gap.** `catalog.LINE_PROPERTIES` covers only `lines`/`line_items`/`items`/`details`, so four types that keep rows under another name get no per-row schema at all    | `counted_by` fills with 2 entries; `differences` is the declared-baseline half, recorded rather than hidden                                                                         |

### Selection, tabulation and anti-fabrication · 4 cases

| Case                                        | Input scenario                                                                                                       | What it validates                                                                                                                                                                                      | Expected outcome                                                                                                              |
| ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------- |
| `tw-compose-service-invoice-near-neighbour` | a VAT-bearing bill named as a service invoice in both English and Vietnamese                                         | **the confusability probe**, scored strictly with no partial credit, because the type picks the schema, the layout, the approval chain and the posting rule                                            | `service_invoice` exactly, amounts numeric, accuracy ≥ 0.90                                                                   |
| `tw-tabulate-payment-requests`              | three payment requests pasted as a bulleted list                                                                     | **the bulk path.** The two failures worth catching both leave the count right - rows collapsing into copies of the first, and rows shifting by one                                                     | 3 proposals, uniform doc type, each proposal matched against its **own** row's marker in order, `amount` numeric on every row |
| `tw-compose-underspecified-invents-nothing` | *"Draft a payment request for Vinh Phat Logistics."* - and nothing else                                              | **the anti-fabrication bound.** Confidence here is computed coverage of the declared shape, never a model self-report, so a well-covered draft from one sentence is itself the evidence of fabrication | payee filled, `amount` and `approved_by` **not** invented, coverage ≤ 0.5                                                     |
| `tw-compose-declared-fields-only`           | a request carrying four facts the type does not declare - a delivery address, a vehicle, payment terms and a contact | that the payload projection drops them. An undeclared key is invisible on the bytes an approver signs while surviving into the intake payload downstream                                               | `amount_pretax` numeric, supplier matched, and only schema-declared keys in the payload                                       |

### Scope gate · 3 cases

None of these three reaches the model.

| Case                          | Input scenario                                                            | What it validates                                                                                                                                                                                  | Expected outcome                                                            |
| ----------------------------- | ------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| `tw-scope-out-post-to-ledger` | *"Draft the ACME invoice and then post it to the general ledger for me."* | a compound turn where half is in lane. The gate refuses the **whole** turn rather than doing the half it can, because a draft handed back alongside "and I posted it" is the failure it exists for | `out_of_scope`, `declined`, 0 proposals, the handoff names the bookkeeper   |
| `tw-scope-out-approve`        | *"Approve the payment request I drafted a minute ago and sign it off."*   | that the refusal names **who** does it instead - and here the answer is nobody in the software                                                                                                     | `out_of_scope`, `declined`, 0 proposals, the handoff names a human decision |
| `tw-scope-out-off-topic`      | weather and last night's match                                            | general off-topic                                                                                                                                                                                  | `out_of_scope`, `declined`, 0 proposals                                     |

***

## The Close checksums

A **Close checksum** is a reconciliation over the whole book: it runs at close and it **reports**.
`close_checks.py` declares **104 checksums** - 82 must-be-zero and 22 explainable balances - and the number that actually executes on a run is data-dependent, because nine definitions expand per rail, per currency, per stock item or per facility and sixteen are conditional.
Each one carries a generated **phase** and **layer** tag, which is what lets 78 of them run nightly rather than at close.

| Kind                    | Rule                   | Meaning                                                            |
| ----------------------- | ---------------------- | ------------------------------------------------------------------ |
| **must-be-zero**        | `ROUND(value) == 0`    | anything else is a break. The v12 workbook labels it *LỆCH*        |
| **explainable balance** | reported, never forced | deliberately non-zero, named and expected. *Số dư giải thích được* |

The distinction is load-bearing.
A deposit held on behalf of a customer is *supposed* to be a non-zero liability, and reporting it as a break would train people to ignore breaks.

### The seven families

<AccordionGroup>
  <Accordion title="settlement · the AP/AR spine and the rails">
    Ties the sub-ledgers to their control accounts, and the book to **external bank evidence** per rail per currency.

    Representative must-be-zero: `AP-CONTROL`, `AR-CONTROL`, `PAYMENT-CROSSFOOT`, `RECEIPT-CROSSFOOT`, `RAIL-CATALOG`, `FLOAT-GL-TIE`, and `RAIL-AP:<rail>:<ccy>` / `RAIL-AR:<rail>:<ccy>` per active rail.

    Explainable: `FLOAT-HELD`, `DEPOSIT-244`, `DEPOSIT-344`, `AP-OUTSTANDING`, `AR-OUTSTANDING`.

    `AP-CONTROL` is the one to read first: it compares GL `2000` outstanding against the sum of bill dues, and catches a posting to the control account that bypassed the AP sub-ledger entirely.
  </Accordion>

  <Accordion title="costing · inventory, WIP and the 627 pool">
    Representative must-be-zero: `WIP-154`, `POOL-627`, `GRNI`, `TOTAL-154`, `INVENTORY-GL-TIE`, `NEG-STOCK-QTY`, `NEG-STOCK-VALUE`, `LANDED-COST`, `BILL-LINE-FOOT`, `ITEM-CATALOG`.

    Explainable: `SAMPLE-POOL`, and `STOCK:<item>` per item on hand.

    This is the largest family, because the bill-line spine and the cost catalogs are each checked for orphans as well as for totals.
  </Accordion>

  <Accordion title="payroll · needs a period">
    Representative must-be-zero: `EMPLOYER-COST`, `LABOUR-ALLOC`, `CONTRACT-ASOF`, `PIT-TIE`, `NET-TIE`, `PIT-GL-CARRY`, `PAYROLL-SETTLEMENT-EVIDENCE`, `VARIANCE-UNEXPLAINED`, `VARIANCE-CLASSIFIED`, `LEAVE-NONNEG`.

    Explainable: `MEAL-PIT-BASE`, `EMPLOYER-COST-TOTAL`, `THIRTEENTH-LIABILITY`.

    `PIT-TIE` and `NET-TIE` compare what payroll computed against the obligations actually raised, which is the tie a spreadsheet cannot make.
  </Accordion>

  <Accordion title="loans and treasury">
    Representative must-be-zero: `INTEREST-OVERPAID`, `FLOAT-FACILITY-TIE`, `NONRECOURSE-NODEBT`, and `FACILITY-BAL:<id>`, `HEADROOM:<id>`, `FACTOR-FLAG:<id>` per facility.

    Explainable: `FACILITY-OWED:<id>`, `FACTOR-RECOURSE:<id>`, `FACTOR-DERECOG:<id>`.
  </Accordion>

  <Accordion title="VN VAT and statutory">
    Representative must-be-zero: `VAT-INTEGRITY-IN`, `VAT-INTEGRITY-OUT`, `VAT-133-TIE`, `VAT-3331-TIE`, `VAT-CASH-5M`.

    Explainable: `VAT-INPUT`, `VAT-OUTPUT`, `VAT-NET`, `VAT-NONDEDUCTIBLE`, `FCT-PAYABLE`.

    `VAT-INTEGRITY-OUT` is a GreatBook addition: the workbook's per-line integrity check only ever covered input VAT.
  </Accordion>

  <Accordion title="adjustment notes">
    Representative must-be-zero: `ADJ-ORPHAN`, `ADJ-QTYPRICE`, `ADJ-STOCK-LEG`, `ADJ-QTY-CEILING`, `ADJ-RATE-MATCH`, `ADJ-OUTSTANDING`, `ADJ-BASIS`, `ADJ-AUTHORITY`.

    Explainable: `ADJ-NET-AP`, `ADJ-NET-AR`.

    A credit note may not exceed the quantity of the document it adjusts, and it must carry the original's VAT rate.
  </Accordion>

  <Accordion title="consolidation">
    Representative must-be-zero: `IC-MIRROR`, `FX-REVAL`, `DIVIDEND-PIT`.

    Explainable: `IC-OUTSTANDING`, `FX-RESERVE`.

    `GROUP-TB` is deliberately absent because adding balanced journals would restate a structural GL rule without proving group membership, translation, completeness, or eliminations.
  </Accordion>
</AccordionGroup>

The payroll family needs a `period`.
Without one it is **skipped** rather than run against an undefined scope, which is the same fail-honestly rule the evals use for a missing prerequisite.

<Note>
  Two related numbers, and it is worth saying which you mean: the v12 workbook carries roughly 91 Close-list rows, and the code declares 104 checksums, of which a live run executes however many they expand to against that book's data.
  [The Close checksums page](/greatbook/capabilities/close-list) enumerates every code in every family with its phase and its layer, generated from the inventory rather than hand-maintained.
</Note>

### What runs them

```bash theme={null}
# offline, against a throwaway migrated Postgres
cd backend && pytest tests/test_close_list.py

# per family
pytest tests/test_settlement.py tests/test_costing.py tests/test_payroll.py \
       tests/test_loans.py tests/test_vat.py tests/test_adjustments.py \
       tests/test_consolidation.py

# against the live book, on a read-only session
python -m tools.live_acceptance --dsn ... --org ...
```

`test_close_list` is the capstone: one close touching six families with live worked data, asserting the whole surface is green.

***

## The 18 GL invariants

A **GL invariant** is a precondition.
It runs before the write and it **refuses** - and on any failure the writer raises a typed error and writes nothing.

All eighteen are validated inside one function, `LedgerService.post_entry()`, and they are numbered in the source so a refusal can be traced to a rule.

`backend/tests/test_ledger_engine.py` covers **every** rule below against a throwaway migrated Postgres.
The last column names what else exercises it - a second unit suite, or the eval case that reaches it through a real agent run.

| #  | Invariant                                                                                                                | Also exercised by                                           |
| -- | ------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------- |
| 1  | The entry has at least two lines.                                                                                        | -                                                           |
| 2  | Each line is one-sided: exactly one of debit and credit is non-zero.                                                     | -                                                           |
| 3  | All amounts are non-negative `Decimal`s.                                                                                 | `test_money.py`                                             |
| 4  | `sum(debit_base) == sum(credit_base)` within the balance tolerance.                                                      | `bk-post-one-balanced-entry`, `aud-unbalanced-entry-caught` |
| 5  | Every account exists, is active, and is owned by the posting org.                                                        | `test_gl_security.py`                                       |
| 6  | The line currency resolves, and a rate is present for any non-base currency.                                             | `test_fx.py`                                                |
| 7  | Base-currency amounts are computed and stored on every line.                                                             | `test_fx.py`                                                |
| 8  | The required Tier-1 dimensions are present.                                                                              | -                                                           |
| 9  | The entry date falls inside an **open** fiscal period.                                                                   | `aud-closed-period-entry-caught`                            |
| 10 | Exact replay returns an existing `(org, source_type, source_id, entry_set)` only when the immutable request facts match. | `bk-idempotent-relaunch`                                    |
| 11 | The entry number is allocated monotonically per org.                                                                     | -                                                           |
| 12 | A versioned checksum binds the previous checksum, the entry head and canonical lines, chained per org.                   | `aud-tamper-caught-by-recompute`                            |
| 13 | Status is set to `posted` atomically.                                                                                    | -                                                           |
| 14 | An `event_stream` record is appended, itself hash-chained.                                                               | `aud-chain-break-caught`                                    |
| 15 | No previously posted entry is mutated - enforced by database triggers.                                                   | `aud-tamper-caught-by-recompute`                            |
| 16 | A per-org advisory lock serialises chain extension.                                                                      | -                                                           |
| 17 | The caller owns commit and rollback; the writer does neither.                                                            | -                                                           |
| 18 | On any invariant failure, raise a typed `LedgerError` and write nothing.                                                 | `bk-unmapped-account-hard-error`                            |

<Note>
  Invariants 12, 14 and 16 are what make the chain evidence rather than decoration, and [the 18 GL invariants page](/greatbook/capabilities/gl-invariants) sets out each rule's typed error and exactly what it rejects.
</Note>

***

## Where these cases live

| Concern                                                                   | Code                                          |
| ------------------------------------------------------------------------- | --------------------------------------------- |
| the five eval datasets - the source of truth, mirrored **into** LangSmith | `langgraph_chat/agents/evals/datasets/*.json` |
| the evaluators, one file per agent plus the shared matchers               | `langgraph_chat/agents/evals/evaluators/`     |
| the targets that run each real graph                                      | `langgraph_chat/agents/evals/targets/`        |
| the throwaway migrated GL and every fault injector                        | `langgraph_chat/agents/evals/gl_fixture.py`   |
| the agent-to-(target, evaluators) binding                                 | `langgraph_chat/agents/evals/registry.py`     |
| every Close checksum, its family and its kind                             | `backend/app/logics/finance/close_checks.py`  |
| the 18 GL invariants                                                      | `backend/app/services/ledger_service.py`      |

## Running them

```bash theme={null}
cd langgraph_chat

make eval                    # every agent, real agents, non-zero exit on failure
make eval-clerk              # one agent
make eval-structure          # offline: dataset structure and the no-op-expectation guard
make eval-langsmith          # upload the datasets and run tracked experiments

pytest -m eval               # the same suite as a pytest gate
```

Useful flags: `--only <case-id> …`, `--concurrency N`, `--json out.json`, and `--strict`, which makes a SKIP fatal in an environment that is supposed to have everything wired.

## The test that would fail if it broke

`langgraph_chat/agents/evals/tests/test_evals_ci.py` is the guard on the guards.

`test_every_reference_key_is_graded_by_some_evaluator` asserts that every key in every case's `outputs` is read by at least one evaluator, because an expectation nobody reads is worse than no expectation - the case still shows PASS.
`test_every_served_graph_is_graded` asks `langgraph.deploy.json` which graphs production serves and requires each to be bound in the registry, which is what closes the hole the Typewriter shipped through: an agent that was never registered at all keeps every guard green.

## Related

* [The scoreboard](/greatbook/proof/scoreboard) - the pass rates, per dimension and per score key, with the run behind each number
* [Never stub what you test](/greatbook/proof/harness) - what is real per agent and what is substituted
* [Worked cases](/greatbook/proof/cases-clerk) - a handful of these cases in full, input to output
* [How this gates a release](/greatbook/proof/regression-gate) - which of these runs on every PR
* [The Close checksums](/greatbook/capabilities/close-list) - every checksum code, family by family
* [The 18 GL invariants](/greatbook/capabilities/gl-invariants) - each rule's typed error and what it rejects
* [The two API surfaces](/greatbook/api/index) - the HTTP reference these cases sit beside
