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

# Fiscal periods and closing

> The open-period rule the writer enforces, the five-step close checklist, and why an inconclusive step is never ready to close.

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

```sql theme={null}
select id, status from fiscal_periods
where org_id = %s and start_date <= %s and end_date >= %s
order by start_date desc limit 1
```

Two refusals, both `PeriodClosedError`:

| Case                              | Message                                              |
| --------------------------------- | ---------------------------------------------------- |
| no period covers the date         | `no fiscal period covers 2026-04-12`                 |
| the period exists but is not open | `fiscal period for 2026-04-12 is 'closed', not open` |

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](/greatbook/capabilities/gl-invariants), 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](/greatbook/agents/accountant) job, and it runs five steps in the order a human closes a period in.

```mermaid theme={null}
flowchart TD
  P["1 · period<br/>the fiscal period is defined and still open"] --> I
  I["2 · integrity<br/>the Auditor's hash-chain recompute + GL invariant sweep"] --> U
  U["3 · unposted<br/>approved documents with no GL entry yet"] --> C
  C["4 · close_list<br/>104 Close checksums"] --> S
  S["5 · statutory<br/>accounts with activity and no TT200 mapping"] --> V["verdict"]
```

| Step         | Question                                                                     | Failure severity                      |
| ------------ | ---------------------------------------------------------------------------- | ------------------------------------- |
| `period`     | is the period defined and still open?                                        | **blocking**                          |
| `integrity`  | is the chain intact and do the GL invariants still hold over posted entries? | **blocking**                          |
| `unposted`   | are there approved documents that never reached the ledger?                  | **blocking**                          |
| `close_list` | does every must-be-zero Close checksum tie?                                  | **blocking**, per failing checksum    |
| `statutory`  | can every active account be projected to TT200?                              | **warning**, not a block on the books |

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

<Warning>
  **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.
</Warning>

## 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`.

```
DONE          the step ran and passed
BLOCKING      the step ran and found something in the way
WARNING       the step ran and found something worth knowing
INCONCLUSIVE  the step could not run
SKIPPED       the step was not applicable
```

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:

```
showing 25 of 312; re-run scoped to see the rest
```

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

| Concern                                          | Code                                                                                                      |
| ------------------------------------------------ | --------------------------------------------------------------------------------------------------------- |
| the open-period rule at post time                | `backend/app/services/ledger_service.py`, `_resolve_open_period`                                          |
| the checklist, its order and its severities      | `langgraph_chat/agents/accountant/close.py` - `ALL_STEPS`                                                 |
| the integrity oracle it calls                    | `langgraph_chat/agents/auditor/runner.run_sweep`                                                          |
| the Close checksum oracle it calls               | `backend/app/logics/finance/close_checks.run_close_list`                                                  |
| the read-only connections it uses                | `langgraph_chat/agents/accountant/gl_read.py`, which delegates to the Auditor's `gl_read.read_connection` |
| the guarded close and reopen transitions         | `langgraph_chat/devcenter/close_service.py`, `close_period` and `reopen_period`                           |
| the shared lock order and certification contract | `backend/app/services/organization_lock.py`, `docs/contracts/greatbook-close-exclusivity.md`              |

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.

## Related

* [The 18 GL invariants](/greatbook/capabilities/gl-invariants) - invariant 9
* [The Close checksums](/greatbook/capabilities/close-list) - step 4 in full
* [The Accountant](/greatbook/agents/accountant) - who runs the checklist
* [The Auditor](/greatbook/agents/auditor) - the integrity oracle step 2 calls
