The exception system
The Auditor is not only an agent you ask questions. It is a scheduled control that runs while nobody is watching, and a register that remembers what it found. That distinction is the whole page. A chat answer is read once and closed. An exception that nobody has dealt with is still there tomorrow, and it is 24 hours older.
The current demo register refuses an unreadable pagination envelope. It says nothing was checked rather than converting an unavailable response into zero exceptions.
Where you see it: the Audit workspace in the rail is the assurance desk, with Exceptions, Sweeps, Actions and Chat; Actions carries the subset of findings a person can actually do something about, split into two lanes.
Working the exception register is the same system from a user’s chair.
Seven families, and what each one asks
All seven have a runner.
close_gated is explicit-only: an invalid, open or unevaluable period is answered inconclusive by name rather than silently dropped.
It stays out of the routine full sweep on purpose, because a permanent “could not verify” line under every sweep teaches a reviewer to skim past the lines that matter.
Why the Close list is two families and not one with a flag
The register resolves findings by family, never by job or by parameter. So a singleclose_list family running nightly with phase: continuous would clear every close-gated finding a period-close job had filed, every night.
Making the phase a family costs one entry in the vocabulary and one database constraint, and the resolve semantics are then correct by construction rather than correct by everyone remembering.
The continuous Close sweep
The Close list splits cleanly along one question: is the expected value true at every instant, or only after a period-end procedure has run?
Of the 78 continuous, 56 are must-be-zero checks that can produce a finding and 22 are report-only explainable balances that are carried in the summary and never filed.
The codebase already reached for this distinction and had no word for it.
The payroll family gates five of its own checks behind “only meaningful once the period’s bills are raised”.
That comment was the phase tag, written once, in one family, unnamed.
Now it is a column on all 104.
AP-CONTROL and AR-CONTROL are the recon family’s.
Two families reporting one fact would be two register rows for one exception, and the fingerprint cannot merge them because it includes the family.
GROUP-TB is not skipped: it was removed from the inventory because it restated structural journal balance without proving consolidation.
The fail-safe is the period question, and it is not optional.
The Close oracle skips the period-scoped payroll, depreciation-coverage and Wave E kết chuyển controls when given no period, which is 26 of the 104.
A nightly run passing no period would therefore cover 78 of 104 and report a clean pass, which is exactly the silent under-coverage the report’s unverified_scope field exists to refuse.
So the open fiscal periods are resolved at run time from the book, the payroll subset runs once per open period, and if no period is open that subset is inconclusive with the reason stated.
The executed count is data-dependent because the payroll subset expands per open period and other definitions expand per rail, item or facility.
The run summary reports that count, its open-period scope, and the explainable balances separately; it also names AP-CONTROL and AR-CONTROL as the recon family’s rather than silently double-running them.
The completeness family
The row-level, early face of the Close list.
GRNI asks “is the goods-received-not-invoiced pool zero?” once, organisation-wide, at close.
This family asks “is this goods receipt still uninvoiced, and how old is it?” tonight, and names the document.
Same money, two grains, with the Close code as the join, which is why every rule that has one restates it.
The six rules
Rules 1 to 3 are “find the missing document”, which is the matching layer.
Rules 4 and 6 are “this money is attached to nothing”, which is the allocation layer.
Rule 5 is the obligation lifecycle itself.
Rules 2 and 3 have no clock at all, deliberately: a posted document with nothing behind it is wrong the moment it exists, not wrong for getting old.
Only rule 5 can reach
critical, because an unpaid obligation is the one that stops being housekeeping and becomes an integrity question.
The flagship case: a bill with no VAT invoice, aging
The flagship case: a bill with no VAT invoice, aging
The pairs are configuration, not code: eight required-counterparty pairs live in the generated reconciliation ontology, each carrying its own matching terms, its clock, its lane and the Close code it restates.
Aging is a second, separate layer of configuration: the bands live on the job, default
[30, 60, 90] days, overridable per organisation and per pair, so an organisation can age goods-receipt-to-invoice at 15/30/60 and leave everything else alone.The severity is a lookup, never a boolean: the highest threshold the age exceeds picks the rung.The verdict is deterministic
The honest limit rides in the finding, not in a design document.
Where a pair matches on party and month rather than on a purchase-order reference, the finding says so in its own text, so a reviewer reading the exception learns what it could and could not distinguish.
The register: one exception is one row that ages
A nightly job that files a fresh row every night is not a register, it is a firehose. So a finding has an identity, hashed from the organisation, the family, the rule, the subject and a small declared set of salient fields. Everything volatile is excluded from it on purpose:
The row therefore carries
first_seen_at, last_seen_at and seen_count, and the tenth sighting of one problem is one row that says “seen 10 times”, not ten rows.
Re-running a sweep over the same unresolved subjects updates seen_count on the existing rows: one exception ages, and no duplicate row is filed.
Acknowledge, waive, resolve
Three verbs, and the difference between them is the most important thing on this page.
Every arrow into
resolved starts at a sweep, and that is the whole picture.
acknowledged and waived both survive a re-sighting; only the waiver’s expiry undoes one.
Why there is no resolve button
The control is the statusresolved, and it means one specific thing: a run that re-read the book did not find this.
A button that wrote it would be the product asserting an outcome the database never established.
That is the same rule the money path lives by, applied to assurance: an agent may report what it recomputed, never assert what it hopes.
Waive is bounded in four ways, and the database holds three of them
The database CHECK rather than a request model is the point.
A rule stated only in application code is bypassed by one direct SQL write; stated as a constraint it is the database’s answer.
The refusals are written to be read:
“a service-token caller cannot waive an audit finding: a waiver is a human silencing a control, and there is no human in a server-to-server request”
“‘auditor’ is an agent identity. A waiver is a judgement a person makes and answers for; an agent cannot make one”When the expiry passes, the next sweep re-opens the finding by itself. Until then a waiver behaves exactly like an acknowledgement: a re-sighting does not undo it. The waiver’s author, reason and date are kept on the row after it re-opens, as the trail.
Actions: two lanes, and closing a task is not resolving a finding
Not every exception is something a person can act on. A coverage hole is an operations matter; an inconclusive finding may never become a task, and the code refuses to build one that is both. So the worklist route is a property of the rule, declared per rule, rather than a severity threshold. That is what lets awarning-severity completeness finding open a task at all, and it is guarded from the other direction too: a test enumerates every rule any family can emit at critical and requires each one to be either declared actionable or listed with a written reason why nothing can be done about it.
Replacing a threshold with a declaration is a control that can silently narrow, so it is pinned.
Each task carries the lane its finding carries.
A break in the matching layer lands in the bookkeeping lane, which is worked in the Data workspace; a break in the analytical layer lands in the accounting lane.
Neither person has to read the other’s list.
An untagged finding appears in BOTH lanes, not neither.
A missing lane is over-routing, which is a mild annoyance.
The opposite default is how an exception disappears.

The current demo worklist cannot be read. Both lane counts and list membership remain unavailable.
“the task was updated; the audit finding behind it is unchanged - only a sweep that re-reads the book may resolve an exception”It is entirely legitimate for a person to mark the work done while the finding stays open until the next sweep confirms. A route that quietly resolved the finding would be the same lie as a resolve button, one screen over. The other direction is automatic: when a sweep resolves a finding, its task is closed in the same transaction, stamped with why it vanished, so no orphan task sits behind a resolved exception.
The API
Coverage and cleanliness are reported as two separate facts, never folded into one.
A run states what it verified and what it could not, so “nothing complained” can never be read as “everything was checked”.
What is scheduled today
Nine jobs, seven on a clock and two on an event.monthly_close_gated runs with period: last_closed, resolving a bounded batch of finished periods that still need verification.
An invalid, open or otherwise unevaluable period is reported inconclusive by name, never passed or treated as an ordinary exception.
The two event jobs are narrower than they look, deliberately.
on_intake_duplicate_check fires when an intake row is acted on - approved, rejected, resubmitted, posted - and not on arrival, because a duplicate merely sitting in the queue is covered by the nightly pass and approval is the moment one could reach the ledger.
on_post_invariant_sweep re-checks the 18 GL invariants after any ledger write, coalesced so a burst of writes is one sweep, and the run records the event range it answered for.
The two new nightly jobs sit after the three-hourly projector pass they depend on.
Two guards run at boot and warn rather than fail: one that every enabled job’s cron expression actually parses, and one that two jobs touching a single family do not disagree about its scope.
A cron expression the evaluator refuses is a review that quietly never happens, and a nightly verification absent for a fortnight with nobody looking is the failure both guards exist to make loud.
Where it is enforced
Related
- The Auditor - the agent, its graph, and the read-only channel
- Two layers on the same documents - what the lane on a finding means
- The Close checksums - all 104, by phase and by layer
- The reconciliation ontology - the graph three of the rules read
- Working the exception register - the same system, from a user’s chair