Reports and the close lock
What every monthly close writes into your workspace, how each locked month records whether it was fully reconciled, and how the lock keeps a reported figure from changing without a written reason.
What a close writes
A close runs for one company and one month. Here is what it leaves in the review workspace, and what it reads. Haruno calls each company a client; that is the word in folder names, commands and the dashboard. In the paths, <client> is the company's folder name under workbooks/, for example cobalt-systems.
| Output | What it is | When it changes |
|---|---|---|
| Statement package | The statements, in the company's own layout. | Every close of that company. |
| Reconciliation binder | Each schedule recomputed against the ledger. | Every close of that company. |
| Investor report | One company's figures, for its investors. | Every close of that company. |
| reports/<client>/adjustments.csv | The adjustments log. | Every close of that company, after the lock. |
| Portfolio dashboard | Every company that has been closed in this workspace and still has a spec. | At the end of every close run in which at least one company closed. |
| ledger/ledger.sqlite | The canonical ledger every report is built from: one file for the workspace, with each company's books held separately. | Every close. A locked month changes only after a reopen. |
| ledger/staging/ | Working copies the close makes of each source file before your mapping is applied. You never need to open them. | Rebuilt by every close. |
| ledger/evidence/ | A read-only copy of every source file. | When a new or corrected file is imported. |
| .haruno/skills/QuickBooksCFO/specs/<client>.yaml | The spec: the mapping and the judgments you recorded. | Never by the close. Only when you answer or approve a change. |
The company's own files stay where they arrived, in workbooks/<client>/. The close reads them and never edits them.
You read the reports and the dashboard in the macOS app, and you can export any of them as a PDF to send on.
The close itself runs on your machine and transmits no client data, with or without the assistant running. A conversation sends what you send: when you ask Haruno to do something, what you type and what the tools print back (account names, captions, figures, findings) goes through Inferara's hosted service to the model provider that answers. See Where your data goes.
The statement package
The statement package is the monthly reporting package in the company's own layout: the same captions, the same subtotals and the same scale as the financial package they sent, because it is built from that workbook's own structure. Every figure comes from the ledger. Nothing in it is typed by hand.
From top to bottom:
- Cover. The company's name, "Financial Reporting Package", "Unaudited Monthly Results Through" the close month, and the reporting currency, stated once.
- Restated periods. Only when a locked month was reopened and its figures then moved.
- Issued with unresolved findings. Only when high or medium findings were open. It lists each by severity, check and subject, and says the months are reported with exception rather than fully reconciled.
- Balance sheet. One column per month the ledger holds. The company's own CHECK row, normally total assets less total liabilities and equity, is flagged in any month where it does not tie within a cent.
- Statement of operations. The same columns, plus a calendar year-to-date column for the latest month's calendar year, never life to date.
- Solvency and going concern. Always included: working capital, the current ratio, cash, total equity, and runway with the basis it rests on. Where the chart of accounts carries them, related-party balances are shown apart from third-party debt. It lists which ASC 205-40 going-concern indicators fired, or says none did. It reports indicators and never concludes on going concern.
- Collections against revenue. What the statements imply arrived, against what was recognized, labelled a derived estimate, not a bank total.
- Gross margin. Revenue less cost of revenue per month, and per revenue stream where the spec pairs each stream's revenue line with its cost line.
The statements follow the company's scale, in thousands when the company reports in thousands; the last three sections and the restated rows are in whole currency units and say so. One currency per company, never converted. Without an imported statement of cash flows, the runway rests on the change in cash, financing included, and is labelled that way. See Solvency indicators.
The reconciliation binder
The reconciliation binder holds the company's schedules against the general ledger for the close month: the latest month the ledger holds reconciliations for. Its header names that month and the currency.
Status summary. One row per balance-sheet account the company's binder reconciles: Account, Schedule, Per GL, Per schedule, Difference and Status. The schedule column names what the tab was read as, such as rollforward (opening, activity and closing), prepaid_amort (prepaid amortization), fixed_assets (a depreciation register) or accrual_reverse_new (accruals reversed and rebooked).
| Status | What it means |
|---|---|
| reconciled | The schedule and the general ledger agree within a cent. |
| exception | They differ by more than a cent. Materiality does not soften it: a $200 difference is an exception. |
| open | No difference could be computed. |
Coverage alerts. A material balance with no reconciliation at all is listed by name with its balance, measured against the company's materiality threshold in its spec. Accounts you have exempted are left out.
One section per account. Each schedule's rows, recomputed rather than copied. Roll-forward and accrual rows carry a Recomputed column, flagged where a row does not reach its stated ending balance. Prepaid schedules show what was recognized to the close and what remains; fixed-asset registers show monthly depreciation, accumulated depreciation and net book value.
Recomputing is the point. The binder adds up a schedule's own rows independently of any difference typed on the tab, so a stale or hand-typed difference cannot assert a tie the schedule does not support. Where a tab states no general-ledger figure, the tie is made against the balance imported from the statements.
Exceptions and missing schedules also reach the findings, as recon_exceptions and recon_coverage. See Reconciliation statuses.
The investor report
The investor report is one company's page of the dashboard, cut for the people who fund the company rather than the people who keep its books. Every close rewrites it, so it is as fresh as the latest export. It is the report to send when a founder asks for an investor update.
What it keeps
- Cash, burn and runway. The runway tile rests on the trailing change in cash, financing included, and is labelled that way.
- The statements as the statement package presents them, without the CHECK row that measures the tie-out gap.
- Period over period.
- Collections against revenue.
- Gross margin.
- Operating expenses by category, and by department where the company has a department split.
- Headcount and the per-employee figures, where the company supplies a headcount.
- Recurring revenue (MRR, ARR and churn) and customer acquisition cost and payback, where the company has them.
- The solvency panel with its indicators. Without them the report would be accurate and misleading at once.
- Notes on what the books can and cannot support, such as a figure that needs two months when only one is held.
What is absent
- The integrity findings.
- The mapping table.
- The reconciliation status.
- The variance watchlist.
- The close state and the lock record.
- The tiles that report control status: statement tie-out, recon exceptions and the worst budget line.
- Budget against actual, unless the company's spec lets it in.
- Customer-level detail: the by-customer revenue table and the invoice and schedule rows behind a figure. The figure itself stays.
- Notes addressed to whoever maintains the spec.
Absent means left out, not hidden: these sections are never put into the report, so they are not in its PDF either. The dashboard's buttons that stage a question in chat are not drawn.
Its header says what it is: prepared for the company's investors, through the latest month the ledger holds a balance for, with the date stamp of the export behind the figures where there is one, in the reporting currency. It covers exactly one company.
Whether investors see the founder's plan against the books is the founder's call, so budget against actual stays out unless the company's spec includes it with the setting below. The value must be true or false. Anything else, including "no" written in quotes, is refused rather than guessed at.
investor:
budget: trueClose states
Every locked month carries a record of what was known when it was locked. The dashboard shows it as a chip at the top of each company's page, beside the number of months locked.
| Close state | Chip | What it means |
|---|---|---|
| fully reconciled | Green | Every locked month was closed with nothing unresolved. |
| reported with exception | Red | At least one locked month was closed while high or medium findings were still open. |
| open | Amber | No month has been locked yet. |
| not recorded | Grey | A locked month's close did not record its findings, as closes made by earlier versions did not. |
The weakest wins. One chip speaks for every locked month, so it makes the only claim that stays true of all of them: an exception in any month outranks a month that recorded nothing, which outranks the reconciled ones. The note beside the chip counts the months locked, and any still open. It does not say how many of them carry the exception. To see which months do, run ingest.py periods, which prints the state on each month's line (see Tracing a figure).
Only high and medium findings count as unresolved. Low findings, solvency indicators and questions do not: a company short of cash does not make its bookkeeping unreconciled. See Severities.


The same state, in the same words, appears in the dashboard header, on each month's line in the period list (ingest.py periods, see Tracing a figure), in the statement package's Issued with unresolved findings note, and in the summary the close prints when it finishes. For Cobalt Systems on the synthetic demo, that summary reads:
locked 2023-02-28 … 2026-06-30 (41 periods) — reported with exception: 3 unresolved finding(s) (presentation_tieout, recon_exceptions, unmapped_active_account); re-opening one needs a reason (`ingest.py reopen`)The lock
Every month a close reports figures for is locked when the close finishes, whether or not it tied; the findings have no vote. Months that exist only because a document named them, such as a forward-looking budget's, are not locked.
What the lock refuses
Once a month is locked, anything that would change what it presents stops that company's close: nothing is written to its ledger, no report is refreshed for it and nothing is locked. Other companies closed in the same run carry on. That covers a corrected workbook that changes, drops or adds a figure, and equally a change in the spec to where an account is presented or how a statement line is calculated, which moves a statement line without touching a stored number. The refusal names every locked month it would touch and spells out the figures that would move: the first twelve in full, then a count of the rest. From the synthetic demo, after approving a mapping for Cobalt's account 1450:
2023-02-28 presentation BS line 'CHECK': -6,600.00 -> 0.00
2023-02-28 presentation BS line 'Other assets': 19,500.00 -> 26,100.00
2023-02-28 presentation BS line 'TOTAL ASSETS': 5,394,868.29 -> 5,401,468.29What does not trip it: re-closing a month from the same figures; the company renaming an account that keeps its account number; a zero row the export stopped carrying, since an absent figure and 0.00 are the same reported figure; a year-to-date cash-flow statement for a new span; and detail that sits on no statement line, such as transaction-detail reports and billing or CRM exports.
Reopening a month
A locked month can still be corrected, but not silently. Reopening it needs a reason: one sentence, in the words of whoever decided to change a reported month. It is kept with the period for good, and printed in the statement package when the figures move. A blank reason, a placeholder such as tbd, or anything wrapped in angle brackets is refused.
When Haruno meets the refusal in a conversation, it relays it and stops. It never reopens a month on its own and never writes the reason for you. Tell it why in your own words, and it reopens exactly the months the refusal named, keeps your sentence against each, applies the change and closes again. From Terminal, the same is one command:
python3 .haruno/skills/QuickBooksCFO/scripts/close.py --workspace . --client cobalt-systems --reopen-reason "1450 was unmapped; approved as Other assets"To reopen single months by hand, use ingest.py reopen. Here --client is the company's name as its spec records it and its reports show it (for example "Cobalt Systems, Inc."), not the folder name, and --period is a month end, repeatable. Replace the placeholders with your own words.
python3 .haruno/skills/QuickBooksCFO/scripts/ingest.py reopen --ledger ledger/ledger.sqlite --client "<client name>" --period YYYY-MM-DD --reason "<their words>"To see where a company stands, run ingest.py periods (the full command is under Tracing a figure). It lists every month as closed or open with its close state, and for a reopened month, each transition with who ran it and the reason.
The restatement
Haruno has no cell to edit and no adjustment to post. A correction is booked in the company's own accounting system, so it appears in the next export, and re-importing that corrected workbook is the restatement: reopen the months it touches with a reason, and the close re-closes them.
If the figures actually moved, every statement package from then on carries a Restated periods table: the month, when it was reopened, by whom, the reason, and the figures that moved. A month reopened and left alone presents what was already sent, so it appears in ingest.py periods and not in the package. Every close and every reopen is kept, in order, with the figures each month presented at that moment; that history is never rewritten.
For how a refused close plays out in the workflow, see When the close refuses.
The adjustments log
reports/<client>/adjustments.csv is the schedule of adjustments a practice keeps by hand, derived from the ledger instead. Every close rewrites it after the lock with the company's whole history, one row per figure per restatement.
| Column | What it holds |
|---|---|
| period | The month end, or the span for a cash-flow line. |
| kind | "balance" or "activity" for a ledger figure; "presentation" for a statement line that moved with no figure behind it, such as after a mapping change; "cash flow" for a line of an imported cash-flow statement; "not itemized" for a reopen recorded by an earlier version that kept no figures. |
| subject | The account or statement line. |
| was, now, delta | The figure previously reported, the figure now, and the change. |
| reopened_at, reason, actor | When the month was reopened, the reason, and the user who ran it. The actor is a record, not a sign-off. |
| closed_at | When the month was closed again. |
The headers are always written, so an empty log says nothing was restated. A "not itemized" row says the month moved and nothing can say which figure; that is the whole answer, not a gap to fill by guessing.
On the synthetic demo, approving the mapping for Cobalt's account 1450 itemizes as "presentation" rows for CHECK, Other assets and TOTAL ASSETS in each of 41 months, 123 rows in all, because a mapping moves what the reader sees and no stored figure. A corrected bank balance would itemize as one "balance" row naming the account. The statement package's Restated periods table carries the same rows, in whole currency units rather than thousands.
Retained source workbooks
Every source file is copied into ledger/evidence/ as its figures are imported, so the file behind a reported figure can be produced later, even after the company has saved over its own copy a dozen times.
- Filed per company, read-only, and named for the file's content, so re-closing an unchanged month copies nothing. The store holds the distinct versions a company has sent.
- A superseded copy stays after a correction: it is the evidence for what was reported before.
- A file that changed between being read and being filed is not kept, and the close says so on the day, the only day it might still be recoverable. Keeping a copy never stops a close.
To find the file a figure came from:
python3 .haruno/skills/QuickBooksCFO/scripts/ingest.py evidence --ledger ledger/ledger.sqlite --client "<client name>"It names each source file, what was read out of it, the dates it carries figures for, and whether the kept copy is still that file: ok, missing, altered or unreadable, checked afresh every time you ask.
Every close also re-checks the copies and raises an evidence_integrity finding (medium) for one that has gone missing, been written to, or cannot be read. The statements are unaffected; what is lost is the ability to produce the file behind them. A missing copy is put back when the same file is imported again. A re-import never overwrites an altered copy, so the finding stands until a person replaces the copy by hand from a known-good original.
Tracing a figure
Every figure on the dashboard can be followed to a report the close wrote, a source file, or the ledger itself.
The reports, the adjustments log and the spec are listed under What a close writes; the company's files as received are in workbooks/<client>/. To go below the reports, read the ledger itself.
To read the ledger itself, use query.py from the workspace folder. It opens the ledger read-only and has no option to write; figures change only through a close, from the spec and the workbooks. These commands need a standard Python 3 and nothing else. The first returns a company's cash at a month end, summed the way the dashboard sums it: on the synthetic demo, Meridian Robotics' $1,442,000, the figure on its Cash tile. In the query, '10%' means every account whose number begins with 10, which is the default cash prefix. If the company's cash accounts are numbered differently, use its own prefixes instead.
# cash at a month-end, summed the way the dashboard sums it
python3 .haruno/skills/QuickBooksCFO/scripts/query.py ledger/ledger.sqlite "SELECT p.end_date, SUM(b.balance) AS cash FROM account_balance b JOIN gl_account a ON a.account_id = b.account_id JOIN period p ON p.period_id = b.period_id JOIN client c ON c.client_id = a.client_id WHERE c.name = 'Meridian Robotics, Inc.' AND a.acct_num LIKE '10%' AND p.end_date = '2026-06-30' GROUP BY 1"
# every line of a period-over-period comparison, not just the largest
python3 .haruno/skills/QuickBooksCFO/scripts/ingest.py compare --ledger ledger/ledger.sqlite --client "Meridian Robotics, Inc." --grain quarter
# which months are locked
python3 .haruno/skills/QuickBooksCFO/scripts/ingest.py periods --ledger ledger/ledger.sqlite --client "Meridian Robotics, Inc."
# the ledger's tables and columns
python3 .haruno/skills/QuickBooksCFO/scripts/query.py ledger/ledger.sqlite --schemaCommands you run yourself read the workspace and send nothing. When you ask Haruno to trace a figure for you, what you type and what the tools print back is sent through Inferara's hosted service to the model provider that answers. See Where your data goes and Running it yourself.