Skip to content

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) — holds GENERATE_NOTES. Pre-state: Engagement at STATEMENTS_GENERATED. Every mandatory disclosure rule answered. Mandatory policy texts present in packages/financial-statements/constitution/mandatory_policies/. Post-state: Engagement at NOTES_COMPLETE with one FsNote row per generated note (mandatory + dynamic). On failure: NOTES_ERROR.

0. Prerequisites

  • Engagement at STATEMENTS_GENERATED with all mandatory disclosures answered (see FS-05).
  • The generate-notes worker is registered with an Anthropic API key (or mock adapter for dev).
  • Mandatory policy files exist:
    • mandatory_policies/income_tax.md
    • mandatory_policies/employee_benefits.md
    • mandatory_policies/provisions.md

1. Steps

1.1 Trigger generation

http
POST /ops/financial-statements/engagements/<engagementId>/notes/auto-generate
Authorization: Bearer <fs-preparer-jwt>

(Or click Generate notes on the Notes tab.)

The handler:

  1. Asserts GENERATE_NOTES.
  2. Asserts engagement is at STATEMENTS_GENERATED.
  3. Asserts every triggered mandatory rule has every required prompt answered.
  4. Enqueues generate-notes with {engagementId, requestedBy}.
  5. Returns 202 Accepted with 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.

  1. Loads disclosure responses, the latest FsGeneratedStatement, engagement + client metadata.
  2. Loads mandatory policy texts via getMandatoryPolicies(reportingStandard) from policies-loader.ts — returns {incomeTax, employeeBenefits, provisions}.
  3. 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}}
  4. Calls Claude. Single call by design.
  5. Parses the response. Validates every note_key against the taxonomy (note_trigger keys + the mandatory always-present list).
  6. Replaces every {type: "financial_results_placeholder"} block with a computed financial-results table (deterministic post-processing using FsGeneratedStatement).
  7. Upserts FsNote rows keyed (engagementId, noteKey).
  8. On success: transitions engagement STATEMENTS_GENERATED → NOTES_COMPLETE. Emits AuditEvent fs.notes.generated with {inputTokens, outputTokens, model, requestId, noteKeys}.
  9. On failure (timeout, JSON parse, schema mismatch): transitions engagement STATEMENTS_GENERATED → NOTES_ERROR. Emits AuditEvent fs.notes.failed with 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_GENERATED and re-enqueues generate-notes.
  • Adjust upstream — fix the disclosure / mapping issue first; the next attempt sends an updated prompt.

2. Verification

Database

sql
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 notes
sql
SELECT 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:

sql
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 typeNotes
fs.notes.requestedAPI hit
fs.notes.generatedBackground success; token counts + model + Claude request id
fs.notes.failedBackground 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_key not in the constitution — parser refuses; the engagement transitions to NOTES_ERROR with 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 at STATEMENTS_GENERATED and the first call hasn't transitioned yet. Use the polling endpoint instead.
  • Retry from NOTES_ERROR — allowed; counter does not increment (the same versionNo is re-used). The activity log preserves both attempts.

Next

Proceed to FS-07 · Export DOCX.

Internal use only — BreezyCorp