FS-06 · Generate notes & Directors Report
SOP:
SOP_Financial_Statements.md§6 / Step 6.0 (STATEMENTS_GENERATED → NOTES_COMPLETE)Actors: FS Preparer (fs-preparer@spade.local) — holdsGENERATE_NOTES. Pre-state: Engagement atSTATEMENTS_GENERATED. Every mandatory disclosure rule answered. Mandatory policy texts present inpackages/financial-statements/constitution/mandatory_policies/. Post-state: Engagement atNOTES_COMPLETEwith oneFsNoterow per generated note (mandatory + dynamic). On failure:NOTES_ERROR.
0. Prerequisites
- Engagement at
STATEMENTS_GENERATEDwith all mandatory disclosures answered (see FS-05). - The
generate-notesworker is registered with an Anthropic API key (or mock adapter for dev). - Mandatory policy files exist:
mandatory_policies/income_tax.mdmandatory_policies/employee_benefits.mdmandatory_policies/provisions.md
1. Steps
1.1 Trigger generation
POST /ops/financial-statements/engagements/<engagementId>/notes/auto-generate
Authorization: Bearer <fs-preparer-jwt>(Or click Generate notes on the Notes tab.)
The handler:
- Asserts
GENERATE_NOTES. - Asserts engagement is at
STATEMENTS_GENERATED. - Asserts every triggered mandatory rule has every required prompt answered.
- Enqueues
generate-noteswith{engagementId, requestedBy}. - Returns
202 Acceptedwith a polling URL.
1.2 Worker — generate-notes
This is the largest single LLM call in the system. Background — the UI shows a polling indicator.
- Loads disclosure responses, the latest
FsGeneratedStatement, engagement + client metadata. - Loads mandatory policy texts via
getMandatoryPolicies(reportingStandard)frompolicies-loader.ts— returns{incomeTax, employeeBenefits, provisions}. - Builds one consolidated 16K-token Claude Sonnet prompt (
packages/financial-statements/src/ai/notes-prompt.ts):- System prompt (cached): writing style, block-type schema, mandatory-policy verbatim instruction
- User prompt: statement JSON; disclosure responses; verbatim policy texts; engagement + client metadata
- Output constraint:
{notes: [{note_key, title, blocks}], directors_report: {blocks}}
- Calls Claude. Single call by design.
- Parses the response. Validates every
note_keyagainst the taxonomy (note_triggerkeys + the mandatory always-present list). - Replaces every
{type: "financial_results_placeholder"}block with a computed financial-results table (deterministic post-processing usingFsGeneratedStatement). - Upserts
FsNoterows keyed(engagementId, noteKey). - On success: transitions engagement
STATEMENTS_GENERATED → NOTES_COMPLETE. EmitsAuditEventfs.notes.generatedwith{inputTokens, outputTokens, model, requestId, noteKeys}. - On failure (timeout, JSON parse, schema mismatch): transitions engagement
STATEMENTS_GENERATED → NOTES_ERROR. EmitsAuditEventfs.notes.failedwith the failure record (model id, request id, error class, prompt fingerprint).
1.3 Recover from NOTES_ERROR
Open the Notes tab. The failure record is shown with model id, request id, and prompt fingerprint. Two paths:
- Retry — click Retry. The handler transitions
NOTES_ERROR → STATEMENTS_GENERATEDand re-enqueuesgenerate-notes. - Adjust upstream — fix the disclosure / mapping issue first; the next attempt sends an updated prompt.
2. Verification
Database
SELECT note_key, title, jsonb_array_length(content) AS block_count, generated_at
FROM fs_notes
WHERE engagement_id = '<engagementId>'
ORDER BY note_key;
-- expect rows for: directors_report, note_general, note_basis, note_policies, plus dynamic notesSELECT status FROM fs_engagements WHERE id = '<engagementId>';
-- expect 'NOTES_COMPLETE'Verbatim policy assertion
Spot-check that the income-tax policy text from constitution/mandatory_policies/income_tax.md appears verbatim in note_policies:
SELECT content FROM fs_notes
WHERE engagement_id = '<engagementId>' AND note_key = 'note_policies';The output should contain the exact Section 29 wording from the markdown file. A diff against the constitution file is the canonical check.
Audit log
| Event type | Notes |
|---|---|
fs.notes.requested | API hit |
fs.notes.generated | Background success; token counts + model + Claude request id |
fs.notes.failed | Background failure; error class + request id |
3. Negative & edge cases
- Mandatory rule unanswered — handler refuses (
409 Conflict) and returns the list of missing mandatory prompt ids. - Claude returns a
note_keynot in the constitution — parser refuses; the engagement transitions toNOTES_ERRORwith the offending key in the failure record. - Claude paraphrases the mandatory policy text — the renderer's verbatim assertion at DOCX export time catches this. The note is persisted but the export step refuses with
mandatory_policy_mismatch— see FS-07. Solution: regenerate notes. - Multiple parallel generate calls — the second is rejected (
409 Conflict) because the engagement is atSTATEMENTS_GENERATEDand the first call hasn't transitioned yet. Use the polling endpoint instead. - Retry from
NOTES_ERROR— allowed; counter does not increment (the sameversionNois re-used). The activity log preserves both attempts.
Next
Proceed to FS-07 · Export DOCX.