> ## 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 Auditor

> Continuous, strictly read-only assurance over the book of record. Seven check families, an inconclusive verdict that is never a pass, and a read-only guarantee the database enforces.

# The Auditor

🟡 **Deployed source; agent flow unexercised end to end for this documentation cut.**
Strictly read-only, and the reason it was safe to build before any further write agent.

## The shape

```
START -> scope_gate -> { decline               (out of the auditor's lane)
                         audit -> answer }      (in scope: recompute + sweep, then narrate)
```

No posting gate.
No interrupt.
**No write channel at all.**

The contrast with its siblings is the design: the Bookkeeper compiles with an interrupt and a single boxed write channel; the Auditor compiles with neither.
Every node here reads or recomputes.

## Read-only, enforced by the database

Postgres refuses the write.
That is the claim, and it took four mechanisms to make it true through the deployed transport, because the obvious two were both measured to fail.

<Warning>
  **What does *not* work, and both were tried.**

  `psycopg.connect(dsn, read_only=True)` on an **autocommit** connection is inert.
  psycopg applies that attribute when *it* begins a transaction, and an autocommit connection never begins one.
  Measured on psycopg 3.3.4: the session reports `transaction_read_only = off` and executes an INSERT happily.

  Forcing `default_transaction_read_only=on` as a **startup parameter** is correct, and on the deployed DSN it is honoured by nothing.
  Supabase's pooler consumes the `options` startup field for its own tenant routing and does not forward it to Postgres.
  There is no error: the session simply comes up writable, and a live probe through this channel once **committed a `CREATE TABLE`**.
</Warning>

So the channel does four things, in this order, and refuses rather than degrades at every one of them.

<Steps>
  <Step title="Refuse the transaction pooler, from the DSN">
    Port `6543` is Supabase's transaction-mode pooler.
    This channel proves itself read-only with a session-scoped `SET`, which on a transaction pooler would persist onto a **shared backend** the pooler later hands to a writer, and whose read-back could be answered by a different backend than the one it was set on.
    Reading the book through that pooler would be fine; *proving* the channel read-only through it is impossible, so the DSN is refused before anything connects.
  </Step>

  <Step title="Force the startup parameter anyway">
    `-c default_transaction_read_only=on`, appended to any `options` the DSN already carries rather than replacing it.
    It is the only form with **no writable window at all**, and it is the real enforcement on a direct DSN and in CI.
    It is kept precisely because it is stronger where it survives.
  </Step>

  <Step title="Ask libpq which endpoint it actually reached">
    The DSN can leave the port unstated and let `PGPORT` or a `service=` entry fill it in, which passes the string check and then lands the `SET` on a shared backend.
    `conn.info.port` is libpq's own answer after all that resolution.
    A port that reads as `6543`, **or that cannot be read at all**, is refused and the connection is discarded unused.
  </Step>

  <Step title="SET it a second way, then make the database confirm it">
    `SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY` is measured to survive the pooler that discards the startup parameter.
    Then `show transaction_read_only` is **read back**, and a session that does not answer `on` is refused.
    That last clause is the whole design: an assertion defends the property against *any* transport, including the next pooler that swallows something else.
  </Step>
</Steps>

A caller may not suppress the refusal.
An Auditor that cannot open a provably read-only channel has nothing to report from, so a check reports **inconclusive** rather than reading the book through a session that could also write to it.

The four probes below were run inside the deployed container, against the real database, through the real pooler:

```
transaction_read_only = on          <- read back from the server, not assumed
read works, journal_entries = 27
INSERT (0 rows):  REFUSED -> cannot execute INSERT in a read-only transaction
UPDATE (0 rows):  REFUSED -> cannot execute UPDATE in a read-only transaction
DELETE (0 rows):  REFUSED -> cannot execute DELETE in a read-only transaction
DDL create table: REFUSED -> cannot execute CREATE TABLE in a read-only transaction
```

The three DML probes are deliberately zero-row, so a writable channel would have changed nothing.
The DDL probe is the exact vector that committed before the fix.

<Note>
  This is why the Auditor has its own read layer rather than using the shared pool.
  The shared pool **commits** on a clean exit, which is correct for the writers it serves and wrong here.
  And the Auditor never imports a writer: it reuses the ledger writer's **pure hash helpers** so its recompute cannot drift, but never its post path.

  The Accountant reads through **this same channel**, deliberately.
  The read-only guarantee is enforced in exactly one place, so there is one thing to prove rather than two things to keep in step.
</Note>

The read-only claim is also **graded**, not asserted.
Every ledger eval case fingerprints the organisation's entry, line and intake counts and the chain tail before and after the run, and requires them identical.

**The read-only guarantee is proven, not trusted.**

## Browser questions and job mode are separate

The browser may submit only conversational `messages` to the Auditor.
It cannot select `mode="job"`, a privileged check family, job-only scope or an arbitrary job thread; those fields are absent from the browser allowlist and an attempted override is rejected.

Scheduled and event-triggered jobs are created server-side.
The job runner sets job mode, selects one recognized family and its bounded scope, owns retries, and persists the durable run and findings.
This keeps a browser question read-only without letting it impersonate a scheduled sweep.
If the job's required scope cannot be established, the result is inconclusive rather than clean.

The `monthly_close_gated` job is the important period-scoped example.
It runs only the `close_gated` family with a bounded `last_closed` scope, revisits closed periods that have never been verified or still carry unresolved findings, and advances coverage only after a verified run.
Finding identity includes the period, so a later clean month cannot resolve an earlier month's exception.

## The seven check families

The Auditor started with four families and now declares **seven**.
Two lists matter here and conflating them would make every "audit everything" answer permanently incomplete:

| List               | What it is                                                                   | Members                                             |
| ------------------ | ---------------------------------------------------------------------------- | --------------------------------------------------- |
| the **vocabulary** | what a scheduled job may name, and what the database's family CHECK admits   | all seven                                           |
| the **full sweep** | the routine families safe to run without an explicit finished-period subject | six routine families; `close_gated` runs explicitly |

`close_gated` is declared, runnable and schedulable.
A job that names it resolves an explicit finished period; an invalid, open or unevaluable period is answered **inconclusive by name** rather than dropped or crashed.
It is kept out of the routine full sweep on purpose: a permanent "could not verify" line under every sweep is not honesty, it is noise that teaches a reviewer to skim past the lines that matter.

<Note>
  Build-time tests pin all seven declared families to runnable implementations and pin the routine full sweep separately, so the explicit-period boundary cannot drift silently.
</Note>

<AccordionGroup>
  <Accordion title="hash_chain · recompute, do not trust a flag">
    Relinks **and freshly recomputes** the per-organisation entry checksum chain, and the event-stream hash chain, flagging any linkage break, gap, or a stored hash that does not match a fresh recompute.

    It reuses the writer's own `_entry_checksum` and `_canonical_lines`, so if the writer's hashing changes, the Auditor recomputes with the new rule because it is literally the same code.
    A verifier with its own copy drifts, and then reports either false tampering or false safety.

    **One historical limit.**
    New checksum-version-2 entries, including foreign-currency ones, preserve complete canonical facts for recomputation.
    A legacy version-1 non-base entry may lack the old raw rendering, so it falls back to **legacy linkage-only** verification - still chain-guarded, and explicitly counted rather than reported as fully recomputed.
  </Accordion>

  <Accordion title="invariant · the GL invariant sweep">
    Re-checks over **already-posted** entries:

    * the trial balance nets to zero, organisation-wide and per period;
    * every posted entry balances in base currency;
    * no posted entry sits in a closed or undefined period;
    * every `posted` intake row reconciles **one to one** to a real GL entry.

    This is the same set of properties the [18 GL invariants](/greatbook/capabilities/gl-invariants) enforce at write time, re-asked of the book as it stands.
    The code keys this family `invariant`; in prose it is the **GL invariant sweep**.

    It is **not** the [Close checksums](/greatbook/capabilities/close-list) - those are the Accountant's, and they report tie-outs across the seven sub-ledgers.
  </Accordion>

  <Accordion title="orphan · rows stuck mid-pipeline">
    Intake rows resting in a non-terminal pipeline status past an age threshold.

    `received`, `classified` and `extracted` are statuses a row passes **through** within one Clerk run.
    Resting there means the run died mid-pipeline.

    `flagged` and `pending` (awaiting review), `active` (awaiting posting), and `rejected` and `posted` (terminal) are **not** orphans.
    The eval case for this seeds a stale orphan **alongside a legitimately in-flight row**, so it grades false positives too.
  </Accordion>

  <Accordion title="recon · one transaction, across every system it lives in">
    Cross-system reconciliation over the [reconciliation knowledge graph](/greatbook/capabilities/reconciliation-ontology): a document that moves a balance and claims to be posted with no link to any entry, a version group with more than one record that can still enter the book, an obligation whose key names two rows, and the AP/AR control account against its own sub-ledger.

    **Freshness is a gate, not a footnote.**
    The graph is projected by the DevCenter every three hours, and a reconciliation over a graph older than six hours is reported **inconclusive** with no other rule having run.
    Reporting four clean rules over a three-day-old graph is worse than reporting nothing, because it reads as an all-clear.

    The family **proposes and reports; it never merges**, and like everything else here it writes nothing: the graph is written by a projector the DevCenter owns, and a build-time test fails if any Auditor module so much as imports it.
  </Accordion>

  <Accordion title="close_continuous · the Close checksums that hold at every instant">
    The subset of the [Close checksums](/greatbook/capabilities/close-list) whose expected value is true *now*, not only after a period-end procedure, run **nightly** rather than once a month.

    A tie between two representations of one fact written in the same transaction, a referential-integrity or catalog check, an arithmetic identity inside a row, or a bound.
    It reuses the Accountant's Close oracle rather than carrying a second copy of it, and it deliberately **skips** the two checks another family already owns, naming the owner as it does: `AP-CONTROL` and `AR-CONTROL` belong to `recon`.
    Two families reporting one fact would be two register rows for one exception.

    **Its fail-safe is the period question.** The Close oracle skips the period-scoped payroll, depreciation-coverage and Wave E kết chuyển controls when it is given no period, which is 26 of the 104 checksums, so a nightly run with no period would cover most of the list and report a clean pass.
    Instead the open fiscal periods are resolved at run time, the payroll subset runs once per open period, and with no period open that subset is **inconclusive** with the reason stated rather than silently absent.

    The family is labelled **"Close checksums (continuous)"** and never "invariants".
  </Accordion>

  <Accordion title="close_gated · the Close checksums that need the period to have ended">
    Runnable only with an explicit finished-period subject: scheduled monthly or requested by a question naming one of its controls.

    A drain-to-zero pool whose non-zero state is a normal in-flight condition, a tie against evidence that arrives with a lag, or a completeness assertion about a period.
    Twenty-six of the 104, and asking them mid-period would file findings nobody can clear.

    It is a separate family rather than a parameter on `close_continuous` for a structural reason: the register resolves findings by *family*, never by job or parameter, so one family running under two phases would clear every close-gated finding the monthly job filed, every night.
  </Accordion>

  <Accordion title="completeness · the missing document, named and aged">
    The row-level, **early** face of the Close list, and the newest family.

    `GRNI` asks "is the goods-received-not-invoiced pool zero?" once, organisation-wide, at close.
    This family asks "is *this* goods receipt, 47 days old, still uninvoiced?" tonight, and names the document.
    Same money, two grains, with the Close code as the join, so the Accountant's close and the Auditor's nightly sweep agree by construction.

    Six rules, four of them aged on a configurable severity ladder.
    [The exception system](/greatbook/agents/assurance) is the page for all of it.

    Like `recon` it inherits the graph freshness gate: a stale or unprojected graph makes the **whole** family inconclusive and no rule runs.
    A completeness sweep over a graph that stopped projecting last week would report "no missing documents" about documents it never saw.
  </Accordion>
</AccordionGroup>

The `recon` and `completeness` families' routing triggers are **narrower** than the others, deliberately.
A full sweep and a generic assurance question still run them, but a narrow question reaches `recon` only on a genuinely cross-system term (duplicates, versions, the document of record, the graph itself) and `completeness` only on a genuinely completeness term (missing, unallocated, unsettled, overdue, aging, unsigned, three-way).
Bare "evidence" and bare "document" are excluded from the second list on purpose: both are ordinary single-family audit vocabulary.
The reason is the inconclusive verdict: a family routed in unnecessarily puts a "did not run" line in front of a reviewer who asked whether the trial balance matched.

## Answering the whole question

A reviewer's question routes to the families it names.
"Any chain breaks?" runs `hash_chain` only.
A generic assurance question, or an empty message, runs the full sweep.

<Info>
  **A multi-part request answers every family it names.**
  Found by a live adversarial review of the usage guide, and fixed: "Run a full check of the books: hash chain, accounting invariants and orphan scan" previously ran **two of three** families and named none of them in the reply.

  Three independent defects sat behind that one symptom, and all three are fixed:

  1. **Routing.** A family trigger matched a singular stem, which a plural cannot match, so the enumerated
     family silently vanished. Family stems now match plurals, the families are a union that never
     short-circuits, and an explicit "full / complete / whole check" wins outright. That last choice is
     deliberate: for a read-only agent, running one family too many costs a database read, while running
     one too few **answers half the question**. It keys on a generic assurance noun, never a family noun,
     so a narrow question still routes narrowly.
  2. **Reporting.** A per-family readout now narrates one line per family the reviewer **requested** -
     PASS, an exception count, INCONCLUSIVE, or **NOT RUN** - on both the clean and the exception branch.
     Driving it from what was *requested* rather than from what came back is what makes a family that never
     ran read as `NOT RUN` instead of vanishing from a confident answer.
  3. **The thread.** The message channel had no append reducer, so the terminal node's write **replaced**
     the channel and wiped the reviewer's own question, leaving an untitled thread with only an answer and
     no history for follow-ups.

  The reviewer-facing family labels are centralised in one place, so the report headline, the chat answer and the generated SOP cannot drift apart.
  That is also where the binding terminology lands: the family is labelled **"GL invariants"**, never "accounting invariants".
</Info>

## Inconclusive is never a pass

A read failure produces **inconclusive** findings, never a false all-clear.

That posture runs through the whole agent.
An assurance process that reports success when it could not check is worse than one that does not run, because it manufactures confidence.

Findings carry two severities at once:

| Audit severity | Meaning                                                               | Review-inbox kind |
| -------------- | --------------------------------------------------------------------- | ----------------- |
| `critical`     | ledger integrity is violated: a chain break, an unbalanced entry      | `invalid`         |
| `error`        | a hard reconciliation failure: a dangling link, a closed-period entry | `invalid`         |
| `warning`      | suspicious but not proven wrong: a stuck or aging row                 | `verify`          |
| `inconclusive` | the check could not run                                               | `verify`          |

The dual shape means the **same** finding can be scored in the audit report and surfaced in the existing review inbox **without the Auditor writing anything**.
It **returns** the flags for a caller to render; it never inserts them.
That is what keeps "no new write path" true.

## Worked cases

Six seeded faults, each injected into a real migrated GL, each asserted to be flagged.

| Case                              | Fault                                                                                                   |
| --------------------------------- | ------------------------------------------------------------------------------------------------------- |
| `aud-chain-break-caught`          | a wrong `prev_checksum`. Also grades that a chain-specific question runs **only** the hash-chain family |
| `aud-tamper-caught-by-recompute`  | a posted line tampered with the immutability trigger temporarily disabled                               |
| `aud-unbalanced-entry-caught`     | an entry that does not balance                                                                          |
| `aud-closed-period-entry-caught`  | an entry in a closed period                                                                             |
| `aud-reconciliation-break-caught` | a broken intake-to-GL link                                                                              |
| `aud-orphan-intake-caught`        | a stale orphan, alongside a legitimately in-flight row                                                  |

Plus three that grade behaviour rather than detection:

* `aud-known-good-passes` - a clean book passes, which also proves the fresh recompute reproduces **every** stored checksum;
* `aud-declines-write-request` - a request to fix the books short-circuits at the gate;
* `aud-unreadable-ledger-is-inconclusive` - the fail-safe. An unreachable database must never render as a clean book.

The [Proof tab](/greatbook/proof/cases-auditor) carries all ten with their graded assertions.

## Where it is enforced

| Concern                                                 | Code                                                 |
| ------------------------------------------------------- | ---------------------------------------------------- |
| the graph, and the deterministic gate                   | `langgraph_chat/agents/auditor/graph.py`, `scope.py` |
| the vocabulary and the full sweep, pinned to each other | `scope.ALL_CHECKS`, `scope.FULL_SWEEP`               |
| the family sweeps                                       | `langgraph_chat/agents/auditor/checks.py`            |
| the `completeness` rules, their ladders and their lanes | `langgraph_chat/agents/auditor/completeness.py`      |
| the `recon` rules, and the graph they read              | `langgraph_chat/recon/rules.py`, `queries.py`        |
| the phase and layer tag on every Close checksum         | `backend/app/logics/finance/close_inventory.py`      |
| the findings and report contract                        | `langgraph_chat/agents/auditor/report.py`            |
| the read-only channel                                   | `langgraph_chat/agents/auditor/gl_read.py`           |
| the sweep entry point, reachable from CI                | `runner.run_sweep`                                   |

## The tests that would fail if it broke

The offline scope and report suites, plus live acceptance against a real migrated GL where each of the six faults is injected and asserted flagged.

## Related

* [The exception system](/greatbook/agents/assurance) - the sweeps, the register, and what a person does with a finding
* [Two layers on the same documents](/greatbook/two-layers) - the lane a finding is routed to, and why
* [Immutability and the hash chain](/greatbook/capabilities/immutability-and-hash-chain) - what the recompute verifies
* [The 18 GL invariants](/greatbook/capabilities/gl-invariants) - what the sweep re-checks
* [The Close checksums](/greatbook/capabilities/close-list) - the 104, by phase and by layer
* [The reconciliation ontology](/greatbook/capabilities/reconciliation-ontology) - the graph the `recon` family reads
* [Tenancy, RLS and derived identity](/greatbook/capabilities/tenancy-and-rls) - the read-only session
* [Asking the Auditor](/greatbook/guides/audit-your-books) - the same agent, from a user's chair
