Skip to main content

Fiscal periods and closing

What it guarantees

An entry whose date falls outside an open fiscal period never reaches the database. Not as a draft, not as a flagged row, not at all. And a period is closed on the strength of a checklist that reports what it could not verify, rather than one that assumes silence means success.

The period rule

Every posting resolves its date to a period before anything is written:
Two refusals, both PeriodClosedError: An undefined period is as blocking as a closed one. Silently opening a period that nobody defined would mean a mistyped year posts an entry into 2062 and nothing objects. This is GL invariant 9, and it applies to reversals too: the reviewed reversal date must land in an open period even when the original belongs to a closed one.

The close checklist

Closing a period is the Accountant’s job, and it runs five steps in the order a human closes a period in.

The checklist orchestrates, it does not reinvent

Steps 2 and 4 do not implement anything. Step 2 calls the Auditor’s own sweep. Step 4 calls the backend’s close_checks.run_close_list, the same oracle the finance layer’s own tests use. That is deliberate. A close checklist with its own second copy of the integrity rules is a second thing to keep current, and the day the two disagree, neither is trustworthy. The two checks that genuinely belong here are the ones that live between the agents: is the period open, and are there approved documents still unposted. Neither the Auditor nor the finance layer owns that question, so the checklist adds it.
Step 3 is the one people underestimate. Closing a period while approved documents sit unposted books them into the wrong period when they eventually post, and the correction is a reversal in a later month. A close that ignores the approval queue produces a clean set of books that are wrong by exactly the documents nobody posted.

Inconclusive is not a pass

Every step is wrapped so that a read failure degrades to INCONCLUSIVE, and an inconclusive report is never ready_to_close.
The accountant certifies nothing it did not verify. This is the same posture the Auditor takes: a read failure produces inconclusive findings, never a false pass. An assurance process that reports success when it could not check is worse than one that does not run, because it manufactures confidence.

Items are capped, and the cap is stated

Each step carries at most 25 concrete items before summarising the tail. The count is always reported in full; only the enumeration is capped, and the summary says so:
A silently truncated list reads as “there were 25 problems” when there were 312.

Reading the verdict back from the database

The close verdict is read from fiscal_periods.status, not from what the answer says. That distinction matters more than it looks. An agent that reports “the period is closed” is making a claim; a row that says closed is a fact. The eval cases for the Accountant grade the row, for exactly this reason.

The guarded period transition

Reading or running the checklist is not the same operation as closing a period. The canonical close_period service stores the checklist result, refuses blocking or inconclusive readiness, then locks and rechecks the full authoritative state before appending the close event. Reopen is a separate named-human decision with a required written reason and its own append-only event.

Close and accounting writes are exclusive

Close, journal posting, typed reversal, intake/workpaper transitions, Matching decisions and supported mutations of facts consumed by the Close controls share one transaction-scoped organization accounting lock. The global order is organization lock, subsystem rows, fiscal-period row, journal/native writes, then commit. No supported writer may take the organization lock after a row lock. A stored ready run carries the exact journal tail, period transition version, late-intake identities, relevant workpaper hashes and states, Books/finance control fingerprint and a versioned SHA-256 authority fingerprint. Immediately before open → closed, GreatBook takes the shared lock, reruns the complete checklist, rereads those facts and compares the authority fingerprint with the reviewed run. A posting committed before finalization is either included in that certified snapshot or changes the tail and forces rerun_required. A relevant workpaper or control fact changing underneath the review produces review_required. A writer arriving while the final reread holds the lock waits; after Close commits, invariant 9 refuses a journal dated in the now-closed period. The period never closes over a journal tail different from the one stored in its close run and close event. The merged staged-workspace adapters expose exactly those close/reopen transitions. They bind the YYYY-MM period and its transition-version hash, re-derive organization and actor, enforce the period role and exact confirmation, and call the canonical services. They do not let a model edit the checklist result or flip a status directly. Public OAuth/MCP ingress carries these registry 1.14.1 adapters, but their behavior through ChatGPT or Claude has not been exercised there.

Where it is enforced

The whole checklist runs on read-only psycopg sessions, so the database itself rejects a write. Nothing in a checklist read can change a book; only the separate guarded transition changes the fiscal-period state.

The test that would fail if it broke

backend/tests/test_ledger_engine.py covers the closed-period and undefined-period refusals. langgraph_chat/agents/accountant/tests/ covers the checklist against a real migrated GL, including two independently seeded blockers: an approved document that is not in the books, and a credit to AP control with no matching bill in the sub-ledger. The Close race suite uses independent PostgreSQL connections to prove a writer either lands inside the certified snapshot or waits and is refused after the period closes.

What goes wrong without it

Without the period rule, last quarter’s numbers change after they were filed, and nothing tells you which entries did it. Without the inconclusive-is-not-a-pass rule, a close that could not reach the database reports a clean book. That is the worst possible failure of an assurance process: not a wrong answer, but a confident one produced by not looking.