@mulmoclaude/accounting-plugin 3.0.1 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"server.cjs","names":[],"sources":["../src/server/bodyFields.ts","../src/server/context.ts","../src/server/journal.ts","../src/server/io.ts","../src/server/openingBalances.ts","../src/server/accountNormalize.ts","../src/server/report.ts","../src/server/timeSeries.ts","../src/server/eventPublisher.ts","../src/server/snapshotCache.ts","../src/server/defaultAccounts.ts","../src/server/service.ts","../src/server/http.ts","../src/server/router.ts"],"sourcesContent":["// Readers for the loosely-typed dispatch body, and for the service\n// payloads the message builders narrate back.\n//\n// Everything below the `action` discriminator arrives as `unknown` off\n// the wire, so a field is only a string once something has checked. A\n// non-string reads as absent, which lets the service layer reject it on\n// its own terms — `bookId: 42` gets \"bookId is required\" instead of\n// being smuggled through as a string the rest of the stack believes in.\n\nimport { isRecord } from \"@mulmoclaude/common\";\n\nimport type { ReportPeriod } from \"../shared/types.js\";\n\nexport const optionalString = (value: unknown): string | undefined => (typeof value === \"string\" ? value : undefined);\n\nexport const optionalRecord = (value: unknown): Record<string, unknown> | undefined => (isRecord(value) ? value : undefined);\n\nconst YEAR_MONTH_RE = /^\\d{4}-(0[1-9]|1[0-2])$/;\nconst YEAR_MONTH_DAY_RE = /^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])$/;\n\n/** `YYYY-MM-DD` that names a day the calendar actually has. The regex alone\n * admits `2026-02-30`, and `Date.UTC` rolls that forward to March instead of\n * refusing it, so the round trip is what says the date is real. */\nconst isCalendarDate = (value: string): boolean => {\n if (!YEAR_MONTH_DAY_RE.test(value)) return false;\n const [year, month, day] = value.split(\"-\").map(Number);\n if (year === undefined || month === undefined || day === undefined) return false;\n return new Date(Date.UTC(year, month - 1, day)).getUTCDate() === day;\n};\n\n/** Rebuilt field by field rather than narrowed with a predicate, so the\n * returned object is one this function actually proved. A half-formed\n * `{ kind: \"month\" }` reads as absent and the caller raises its own\n * \"period is required\" — previously it reached the report builders and\n * produced an `undefined-01` date.\n *\n * The FORMAT is checked here too, not just the type: `typeof === \"string\"`\n * let `{ kind: \"month\", period: \"banana\" }` through, which came back as a\n * balance sheet dated `\"banana-NaN\"` and made the snapshot cache write\n * `banana.json` / `0NaN-NaN.json` into the book (#2765). Malformed reads as\n * absent so it lands on the caller's existing 400, which already spells out\n * both accepted shapes for the LLM to repair its payload from. */\nexport const optionalReportPeriod = (value: unknown): ReportPeriod | undefined => {\n const period = optionalRecord(value);\n if (period?.kind === \"month\" && typeof period.period === \"string\" && YEAR_MONTH_RE.test(period.period)) {\n return { kind: \"month\", period: period.period };\n }\n if (\n period?.kind === \"range\" &&\n typeof period.from === \"string\" &&\n typeof period.to === \"string\" &&\n isCalendarDate(period.from) &&\n isCalendarDate(period.to)\n ) {\n return { kind: \"range\", from: period.from, to: period.to };\n }\n return undefined;\n};\n\n/** The two fields the addEntries narration quotes back, read out of a\n * service payload that is only `unknown` to the router. */\nexport const describeEntry = (entry: unknown): { id?: string | undefined; date?: string | undefined } => {\n const record = optionalRecord(entry);\n return { id: optionalString(record?.id), date: optionalString(record?.date) };\n};\n","// Host-injected runtime context for the accounting server surface.\n//\n// The backend can't reach into the host for the workspace root, the\n// pub/sub instance, or the logger — those are host-specific. The host\n// injects them once via `configureAccountingServer(...)` before\n// mounting the router (server/index.ts), the server-side mirror of the\n// Vue surface's `configureAccountingHost`. MulmoTerminal wires its own.\n//\n// `log` is a thin proxy that forwards to the injected logger so the\n// many `log.warn(\"accounting\", …)` call sites across the service layer\n// stay unchanged. Before configuration (and in unit tests that drive\n// the service with an explicit workspace root) it falls back to a\n// console logger so nothing throws.\n\nimport type { StructuredLogger } from \"@mulmoclaude/common\";\n\n/** Minimal pub/sub shape — structurally compatible with the host's\n * `IPubSub`. The eventPublisher holds its own instance (set via\n * `initAccountingEventPublisher`); this type is the contract. */\nexport interface IPubSub {\n publish: (channel: string, payload: unknown) => void;\n}\n\n/** Alias of the shared `StructuredLogger` — the host `Logger` stays directly assignable when injected. */\nexport type AccountingLogger = StructuredLogger;\n\nexport interface AccountingServerDeps {\n /** Absolute path to the workspace root (where `data/` lives). Used as\n * the default when a service/io call doesn't pass an explicit root.\n *\n * `null` declares STRICT mode: this host always passes an explicit\n * root per call, and `defaultWorkspaceRoot()` throws instead of\n * guessing. A multi-root host (MulmoTerminal, one root per project\n * directory) MUST use it — with N roots a forgotten option is not a\n * crash but a silent read/write against the wrong project. Mirrors\n * `CollectionHost.workspaceRoot: string | null` in\n * `@mulmoclaude/core/collection/server`. */\n workspaceRoot: string | null;\n logger: AccountingLogger;\n /** Optional: map an absolute root to the OPAQUE scope id the host uses\n * for that project, used to namespace pub/sub channel names so two\n * roots owning one bookId do not cross-notify. Return `null` for the\n * host's default root, which keeps channel names byte-identical to a\n * single-root host's (`accounting:<bookId>`).\n *\n * It must be an opaque id, NEVER a path: channel names reach the\n * browser, and an absolute root there publishes the user's home\n * directory to the client. Same rule as a collection view token. */\n channelScopeForRoot?: (workspaceRoot: string) => string | null;\n}\n\nlet deps: AccountingServerDeps | null = null;\n\n/** Called once by the host before the accounting router is mounted. */\nexport function configureAccountingServer(context: AccountingServerDeps): void {\n deps = context;\n}\n\n/** Default workspace root for io calls that don't pass one explicitly.\n * Throws if the host never configured the server — a real wiring bug\n * (unit tests always pass an explicit root, so they never hit this) —\n * and also under STRICT mode (`workspaceRoot: null`), where there is no\n * ambient root to fall back to and a missing option is a bug rather\n * than a default. */\nexport function defaultWorkspaceRoot(): string {\n if (!deps) {\n throw new Error(\"@mulmoclaude/accounting-plugin: configureAccountingServer() must be called before serving accounting requests\");\n }\n if (deps.workspaceRoot === null) {\n throw new Error(\n \"@mulmoclaude/accounting-plugin: this host is configured with workspaceRoot: null (explicit-root mode), so every call must pass a workspaceRoot. A call reached the engine without one.\",\n );\n }\n return deps.workspaceRoot;\n}\n\n/** The opaque channel scope for a root, or `null` when the host declared\n * none (single-root hosts, and a multi-root host's default root). Used\n * by the event publisher; never contains a path.\n *\n * A missing root throws under explicit-root mode, exactly as\n * `defaultWorkspaceRoot()` does. Returning `null` there would be worse\n * than the io-path bug it mirrors: the event would go out on the\n * UNSCOPED channel name, which every project's default-scope\n * subscriber receives. The publisher catches it and logs, so a dropped\n * root costs one event and a warning rather than a silent cross-project\n * notification. */\nexport function channelScopeFor(workspaceRoot?: string): string | null {\n if (!deps?.channelScopeForRoot) return null;\n return deps.channelScopeForRoot(workspaceRoot ?? defaultWorkspaceRoot());\n}\n\n/** Whether the host declared project scoping at all.\n *\n * The difference matters where a card records the project it belongs\n * to: on a scoped host, \"no scope\" is a REAL answer (the default root)\n * and must be recorded as `null`, not left out — an omitted field means\n * \"ask the host what is active now\", which is precisely the drift a\n * pinned card exists to prevent. On an unscoped host the field is left\n * out entirely and every payload stays byte-identical to today's. */\nexport function isProjectScopedHost(): boolean {\n return Boolean(deps?.channelScopeForRoot);\n}\n\nconst consoleLogger: AccountingLogger = {\n error: (namespace, msg, data) => console.error(`[${namespace}] ${msg}`, data ?? \"\"),\n warn: (namespace, msg, data) => console.warn(`[${namespace}] ${msg}`, data ?? \"\"),\n info: () => {},\n debug: () => {},\n};\n\n/** Logger proxy — forwards to the injected logger, console fallback\n * before configuration. Lets call sites keep `log.warn(\"accounting\", …)`. */\nexport const log: AccountingLogger = {\n error: (namespace, msg, data) => (deps?.logger ?? consoleLogger).error(namespace, msg, data),\n warn: (namespace, msg, data) => (deps?.logger ?? consoleLogger).warn(namespace, msg, data),\n info: (namespace, msg, data) => (deps?.logger ?? consoleLogger).info(namespace, msg, data),\n debug: (namespace, msg, data) => (deps?.logger ?? consoleLogger).debug(namespace, msg, data),\n};\n","// Pure validation + creation logic for journal entries. No fs access\n// here — callers wire the validated entry to `appendJournal` in\n// `server/utils/files/accounting-io.ts`.\n//\n// Double-entry rule: every entry's lines must satisfy\n// Σ debit === Σ credit (within a tolerance of 0.005 — see\n// EQUALITY_TOLERANCE below)\n//\n// Append-only: there is no `editEntry`. Corrections are made by\n// `voidEntry` (creates a reversing pair) followed by a fresh\n// `addEntries` call for the corrected booking.\n\nimport { randomUUID } from \"node:crypto\";\nimport { hasStringProp, isRecord, isUnknownArray } from \"@mulmoclaude/common\";\n\nimport { JOURNAL_ENTRY_KINDS } from \"../shared/types.js\";\nimport type { Account, JournalEntry, JournalLine } from \"../shared/types.js\";\n\n/** Floating-point tolerance for the debit = credit check. Currency\n * amounts arrive as JavaScript numbers (the on-wire format is JSON,\n * so amounts are doubles). 0.005 keeps two-decimal currency math\n * honest while accepting the floating-point noise of summing\n * many lines. */\nconst EQUALITY_TOLERANCE = 0.005;\n\n/** Defensive cap on `JournalLine.taxRegistrationId`. Real-world IDs\n * are short (JP T-numbers are 14 chars, EU VAT IDs ≤ 14, GSTIN is\n * 15, ABN is 11). 32 covers every documented format with comfortable\n * margin while still rejecting accidental paste-bombs. Validation\n * applies to the *trimmed* value so a string of pure whitespace\n * doesn't trip the limit (it normalises to absent). */\nexport const MAX_TAX_REGISTRATION_ID_LENGTH = 32;\n\nexport interface ValidationError {\n field: string;\n message: string;\n}\n\n/** The fields `parseEntry` proves about one wire entry. `makeEntry`\n * takes this, so an entry can only be built from input something\n * actually checked. */\nexport interface ParsedEntry {\n date: string;\n lines: JournalLine[];\n memo?: string | undefined;\n replacesEntryId?: string | undefined;\n}\n\nexport type EntryParseResult = { ok: true; entry: ParsedEntry } | { ok: false; errors: ValidationError[] };\n\nfunction lineHasExactlyOneSide(line: JournalLine): boolean {\n const hasDebit = typeof line.debit === \"number\" && line.debit !== 0;\n const hasCredit = typeof line.credit === \"number\" && line.credit !== 0;\n return hasDebit !== hasCredit;\n}\n\nfunction isNonNegativeNumber(value: unknown): value is number {\n return typeof value === \"number\" && Number.isFinite(value) && value >= 0;\n}\n\n/** Build today's `YYYY-MM-DD` from the host's local timezone.\n * Centralised here so server-side defaults (the void-date on\n * `voidEntry`, the today() stamp on opening replacements, etc.)\n * agree with the client-side `localDateString()` from\n * `src/plugins/accounting/dates.ts`. `toISOString().slice(0, 10)`\n * would emit a UTC date instead — which silently flips into\n * tomorrow / yesterday in negative-offset timezones. */\nexport function localDateString(now: Date = new Date()): string {\n const year = now.getFullYear();\n const month = String(now.getMonth() + 1).padStart(2, \"0\");\n const day = String(now.getDate()).padStart(2, \"0\");\n return `${year}-${month}-${day}`;\n}\n\n/** Validate that `date` is both shaped as YYYY-MM-DD AND represents\n * a real calendar day. The bare regex accepts impossible values\n * like 2026-02-31 or 2026-13-01 which would then poison\n * `periodFromDate`, sort orders, and snapshot keys. We reparse\n * through the Date constructor and roundtrip-format to catch\n * silent normalisation (e.g. \"2026-02-30\" → Mar 02). */\nexport function isValidCalendarDate(date: string): boolean {\n if (!/^\\d{4}-\\d{2}-\\d{2}$/.test(date)) return false;\n const [year, month, day] = date.split(\"-\").map((segment) => parseInt(segment, 10));\n if (year === undefined || month === undefined || day === undefined) return false;\n const parsed = new Date(Date.UTC(year, month - 1, day));\n return parsed.getUTCFullYear() === year && parsed.getUTCMonth() === month - 1 && parsed.getUTCDate() === day;\n}\n\n/** Returns Σ debit − Σ credit. Used by callers that need the actual\n * imbalance value (e.g. the OpeningBalancesForm shows live diff). */\nexport function netBalance(lines: readonly JournalLine[]): number {\n let net = 0;\n for (const line of lines) {\n if (typeof line.debit === \"number\") net += line.debit;\n if (typeof line.credit === \"number\") net -= line.credit;\n }\n return net;\n}\n\nfunction checkTaxRegistrationId(value: unknown, idx: number, errors: ValidationError[]): void {\n if (value === undefined) return;\n if (typeof value !== \"string\") {\n errors.push({ field: `lines[${idx}].taxRegistrationId`, message: \"must be a string\" });\n return;\n }\n if (value.trim().length > MAX_TAX_REGISTRATION_ID_LENGTH) {\n errors.push({\n field: `lines[${idx}].taxRegistrationId`,\n message: `must be at most ${MAX_TAX_REGISTRATION_ID_LENGTH} characters (got ${value.trim().length})`,\n });\n }\n}\n\n/** One issue per bad field, so a caller fixing a line learns about all\n * of them at once. Returns whether the line came through clean. */\nfunction checkLineFields(raw: Record<string, unknown>, idx: number, errors: ValidationError[]): boolean {\n const issuesBefore = errors.length;\n const { accountCode, debit, credit, memo, taxRegistrationId } = raw;\n if (typeof accountCode !== \"string\") errors.push({ field: `lines[${idx}].accountCode`, message: \"accountCode must be a string\" });\n if (debit !== undefined && !isNonNegativeNumber(debit)) errors.push({ field: `lines[${idx}].debit`, message: \"debit must be a non-negative finite number\" });\n if (credit !== undefined && !isNonNegativeNumber(credit))\n errors.push({ field: `lines[${idx}].credit`, message: \"credit must be a non-negative finite number\" });\n if (memo !== undefined && typeof memo !== \"string\") errors.push({ field: `lines[${idx}].memo`, message: \"memo must be a string\" });\n checkTaxRegistrationId(taxRegistrationId, idx, errors);\n return errors.length === issuesBefore;\n}\n\n/** Re-tested rather than assigned straight through: `checkLineFields`\n * proved each of these, but only a `typeof` narrows them for the\n * compiler. */\nfunction buildLine(raw: Record<string, unknown>): JournalLine | null {\n const { accountCode, debit, credit, memo, taxRegistrationId } = raw;\n if (typeof accountCode !== \"string\") return null;\n const line: JournalLine = { accountCode };\n if (typeof debit === \"number\") line.debit = debit;\n if (typeof credit === \"number\") line.credit = credit;\n if (typeof memo === \"string\") line.memo = memo;\n if (typeof taxRegistrationId === \"string\") line.taxRegistrationId = taxRegistrationId;\n return line;\n}\n\n/** Narrow one wire value to a `JournalLine`, pushing an issue per bad\n * field. Shape only: account existence and the debit/credit-side rule\n * belong to the caller, because journal entries and opening balances\n * disagree about them. Returns null when nothing usable came out —\n * the caller drops the line rather than reading fields off it. */\nexport function parseJournalLine(raw: unknown, idx: number, errors: ValidationError[]): JournalLine | null {\n if (!isRecord(raw)) {\n errors.push({ field: `lines[${idx}]`, message: \"each line must be an object with an accountCode and a debit or credit amount\" });\n return null;\n }\n if (!checkLineFields(raw, idx, errors)) return null;\n return buildLine(raw);\n}\n\n/** The rules a journal line answers to on top of its shape: the code\n * must name a real account, and exactly one side carries an amount. */\nfunction validateEntryLine(line: JournalLine, idx: number, accountCodes: ReadonlySet<string>, errors: ValidationError[]): void {\n if (!line.accountCode || !accountCodes.has(line.accountCode)) {\n errors.push({ field: `lines[${idx}].accountCode`, message: `unknown account code ${JSON.stringify(line.accountCode)}` });\n }\n if (!lineHasExactlyOneSide(line)) {\n errors.push({ field: `lines[${idx}]`, message: \"each line must set exactly one of debit or credit (and to a non-zero amount)\" });\n }\n}\n\nfunction parseEntryLines(raw: readonly unknown[], accountCodes: ReadonlySet<string>, errors: ValidationError[]): JournalLine[] {\n const lines: JournalLine[] = [];\n raw.forEach((rawLine, idx) => {\n const line = parseJournalLine(rawLine, idx, errors);\n if (line === null) return;\n validateEntryLine(line, idx, accountCodes, errors);\n lines.push(line);\n });\n return lines;\n}\n\n/** Report the debit = credit imbalance — but only when every line was\n * readable. A line that failed to parse contributes nothing to the sum,\n * so an entry that balances perfectly would otherwise be told it\n * doesn't, sending the caller off to \"fix\" amounts that were never\n * wrong. Naming the unreadable line is the actionable message; the\n * balance is worth re-checking once it's a line. */\nexport function checkBalances(lines: readonly JournalLine[], expectedLineCount: number, subject: string, errors: ValidationError[]): void {\n if (lines.length !== expectedLineCount) return;\n const net = netBalance(lines);\n if (Math.abs(net) > EQUALITY_TOLERANCE) {\n errors.push({ field: \"lines\", message: `Σ debit − Σ credit = ${net.toFixed(4)}; ${subject} must balance` });\n }\n}\n\nfunction parseOptionalString(value: unknown, field: string, errors: ValidationError[]): string | undefined {\n if (value === undefined || typeof value === \"string\") return value;\n errors.push({ field, message: `${field} must be a string when supplied` });\n return undefined;\n}\n\nconst isOptionalString = (value: unknown): boolean => value === undefined || typeof value === \"string\";\nconst isOptionalNumber = (value: unknown): boolean => value === undefined || typeof value === \"number\";\n\nfunction isJournalLine(value: unknown): value is JournalLine {\n return (\n hasStringProp(value, \"accountCode\") &&\n isOptionalNumber(value.debit) &&\n isOptionalNumber(value.credit) &&\n isOptionalString(value.memo) &&\n isOptionalString(value.taxRegistrationId)\n );\n}\n\n/** Checks every field `JournalEntry` and `JournalLine` declare, so a value\n * that passes really is one. Used when reading the journal JSONL back:\n * everything this module ever wrote satisfies it (`id` / `date` / `kind` /\n * `lines` / `createdAt` have been required since the plugin's first\n * release), while a line that doesn't is exactly the line that takes the\n * whole book down — `report.ts` iterates `entry.lines` unguarded. */\nexport function isJournalEntry(value: unknown): value is JournalEntry {\n return (\n hasStringProp(value, \"id\") &&\n hasStringProp(value, \"date\") &&\n hasStringProp(value, \"createdAt\") &&\n JOURNAL_ENTRY_KINDS.some((kind) => kind === value.kind) &&\n isUnknownArray(value.lines) &&\n value.lines.every(isJournalLine) &&\n isOptionalString(value.memo) &&\n isOptionalString(value.voidedEntryId) &&\n isOptionalString(value.voidReason) &&\n isOptionalString(value.replacesEntryId)\n );\n}\n\n/** Normalize a journal line before persistence: trim string fields\n * and drop empty-string optionals so the JSONL doesn't accumulate\n * noise like `\"taxRegistrationId\":\"\"`. Pure — does not mutate\n * `line`. */\nfunction normalizeLine(line: JournalLine): JournalLine {\n const out: JournalLine = { ...line };\n if (typeof out.taxRegistrationId === \"string\") {\n const trimmed = out.taxRegistrationId.trim();\n if (trimmed === \"\") delete out.taxRegistrationId;\n else out.taxRegistrationId = trimmed;\n }\n return out;\n}\n\n/** Parse one entry off the wire. Does not throw: every rejection comes\n * back as a list of issues so the REST handler can return a structured\n * 400 instead of an opaque 500. Parse rather than validate — the\n * narrowed entry rides along on success, so `makeEntry` never has to\n * take the caller's word for the shape. */\nexport function parseEntry(raw: unknown, accounts: readonly Account[]): EntryParseResult {\n if (!isRecord(raw)) {\n return { ok: false, errors: [{ field: \"entry\", message: \"each entry must be an object with a date and a lines array\" }] };\n }\n const errors: ValidationError[] = [];\n const date = typeof raw.date === \"string\" && isValidCalendarDate(raw.date) ? raw.date : null;\n if (date === null) errors.push({ field: \"date\", message: `expected YYYY-MM-DD calendar date, got ${JSON.stringify(raw.date)}` });\n if (!isUnknownArray(raw.lines) || raw.lines.length < 2) {\n errors.push({ field: \"lines\", message: \"an entry needs at least two lines (one debit, one credit)\" });\n return { ok: false, errors };\n }\n const lines = parseEntryLines(raw.lines, new Set(accounts.map((account) => account.code)), errors);\n checkBalances(lines, raw.lines.length, \"entry\", errors);\n const memo = parseOptionalString(raw.memo, \"memo\", errors);\n const replacesEntryId = parseOptionalString(raw.replacesEntryId, \"replacesEntryId\", errors);\n if (errors.length > 0 || date === null) return { ok: false, errors };\n return { ok: true, entry: { date, lines, memo, replacesEntryId } };\n}\n\n/** Build a JournalEntry from lines something already parsed\n * (`parseEntry` / `parseOpening`). The id is a fresh\n * UUID; createdAt is the wall clock at the moment of creation.\n * Lines are normalized so optional string fields don't persist as\n * empty strings. */\nexport function makeEntry(input: {\n date: string;\n lines: readonly JournalLine[];\n memo?: string | undefined;\n kind?: JournalEntry[\"kind\"] | undefined;\n replacesEntryId?: string | undefined;\n}): JournalEntry {\n const entry: JournalEntry = {\n id: randomUUID(),\n date: input.date,\n kind: input.kind ?? \"normal\",\n lines: input.lines.map(normalizeLine),\n memo: input.memo,\n createdAt: new Date().toISOString(),\n };\n if (input.replacesEntryId) entry.replacesEntryId = input.replacesEntryId;\n return entry;\n}\n\n/** Pick the most descriptive memo from the original entry to quote\n * in the voiding entry's memo. Precedence: entry-level memo →\n * first non-empty line memo → null (caller falls back to a\n * date-only template). */\nfunction originalMemoToQuote(target: JournalEntry): string | null {\n if (target.memo && target.memo.trim() !== \"\") return target.memo;\n for (const line of target.lines) {\n if (line.memo && line.memo.trim() !== \"\") return line.memo;\n }\n return null;\n}\n\n/** Build the human-readable memo that goes on the voiding entry.\n * Format: `void of '<original memo>' on <original date>` (or the\n * no-memo fallback when the original carried no memo). The reason\n * the user typed is appended after a colon when present. */\nexport function voidMemo(target: JournalEntry, reason: string | undefined): string {\n const quoted = originalMemoToQuote(target);\n const base = quoted !== null ? `void of '${quoted}' on ${target.date}` : `void of entry on ${target.date}`;\n return reason && reason.trim() !== \"\" ? `${base}: ${reason}` : base;\n}\n\n/** Build the reversing pair for a voided entry. The `void` entry\n * swaps debit / credit on every line so the net effect is zero;\n * the `void-marker` is a zero-line entry that exists purely to\n * carry the `voidedEntryId` reference and the user's reason. The\n * marker keeps `listEntries` queries simple — filtering by\n * `kind: \"void-marker\"` surfaces every voided id without scanning\n * for matching pairs. */\nexport function makeVoidEntries(target: JournalEntry, reason: string | undefined, voidDate: string): { reverse: JournalEntry; marker: JournalEntry } {\n const swappedLines: JournalLine[] = target.lines.map((line) => {\n const swapped: JournalLine = {\n accountCode: line.accountCode,\n debit: line.credit,\n credit: line.debit,\n memo: line.memo,\n };\n // Preserve the counterparty tax-registration ID on each reversed\n // line so the audit trail survives the void — without it, the\n // reversing pair would silently drop the input-tax-credit\n // documentation and a later report scan couldn't reconstruct\n // which T-number / VAT ID the original input tax was tied to.\n if (line.taxRegistrationId !== undefined) swapped.taxRegistrationId = line.taxRegistrationId;\n return swapped;\n });\n const reverse: JournalEntry = {\n id: randomUUID(),\n date: voidDate,\n kind: \"void\",\n lines: swappedLines,\n memo: voidMemo(target, reason),\n voidedEntryId: target.id,\n voidReason: reason,\n createdAt: new Date().toISOString(),\n };\n const marker: JournalEntry = {\n id: randomUUID(),\n date: voidDate,\n kind: \"void-marker\",\n lines: [],\n voidedEntryId: target.id,\n voidReason: reason,\n createdAt: new Date().toISOString(),\n };\n return { reverse, marker };\n}\n\n/** Returns the set of entry ids that have been voided — built from\n * every `void-marker` entry's `voidedEntryId`. Reports use this to\n * exclude original-and-reverse pairs from the activity listing\n * (the netting is automatic in B/S aggregates because the reverse\n * entry has equal-and-opposite lines). */\nexport function voidedIdSet(entries: readonly JournalEntry[]): Set<string> {\n const set = new Set<string>();\n for (const entry of entries) {\n if (entry.kind === \"void-marker\" && entry.voidedEntryId) set.add(entry.voidedEntryId);\n }\n return set;\n}\n","// Single fs gateway for the accounting plugin. Every read / write\n// against `data/accounting/...` lives here so callers don't sprinkle\n// raw `fs` / path concatenation across the codebase (CLAUDE.md rule:\n// raw `fs.readFile` / `fs.writeFile` is forbidden in route handlers).\n//\n// Snapshot cache rule: snapshots are derived state. Any write that\n// touches past data must call `invalidateSnapshotsFrom(...)` to drop\n// stale snapshot files; the next read regenerates lazily via\n// `server/accounting/snapshotCache.ts`. The journal JSONL files are\n// the single source of truth.\n\nimport { promises as fsPromises } from \"node:fs\";\nimport path from \"node:path\";\n\nimport { defaultWorkspaceRoot } from \"./context.js\";\nimport { isJournalEntry } from \"./journal.js\";\nimport { ACCOUNTING_DIRS as WORKSPACE_DIRS, resolveFiscalYearEnd } from \"../shared\";\nimport { writeJsonAtomic, isEnoent } from \"@mulmoclaude/core/files\";\nimport type { AccountingConfig, MonthSnapshot } from \"./types.js\";\nimport type { Account, BookSummary, JournalEntry } from \"../shared/types.js\";\n\nconst root = (workspaceRoot?: string): string => workspaceRoot ?? defaultWorkspaceRoot();\n\nfunction accountingRoot(workspaceRoot?: string): string {\n return path.join(root(workspaceRoot), WORKSPACE_DIRS.accounting);\n}\n\nfunction configPath(workspaceRoot?: string): string {\n return path.join(accountingRoot(workspaceRoot), \"config.json\");\n}\n\n/** Allowed shape for a book id used as a directory name. Defense\n * against path traversal: a crafted id like \"../../config\" or\n * \"/tmp/x\" would otherwise let `bookRoot` escape the\n * `data/accounting/books/` tree, since every write path joins\n * `bookId` directly into the filesystem. The first character is\n * alphanumeric to forbid leading dashes / underscores that some\n * shells / docs render confusingly; `_` and `-` are allowed inside.\n * 64 chars is plenty for any reasonable book name. */\nconst SAFE_BOOK_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;\n\nexport function isSafeBookId(bookId: string): boolean {\n return typeof bookId === \"string\" && SAFE_BOOK_ID_RE.test(bookId);\n}\n\nfunction assertSafeBookId(bookId: string): void {\n if (!isSafeBookId(bookId)) {\n throw new Error(`accounting: invalid bookId ${JSON.stringify(bookId)} (allowed: alphanumeric / _ / -; 1-64 chars; cannot start with _ or -)`);\n }\n}\n\nexport function bookRoot(bookId: string, workspaceRoot?: string): string {\n assertSafeBookId(bookId);\n return path.join(root(workspaceRoot), WORKSPACE_DIRS.accountingBooks, bookId);\n}\n\nfunction accountsPath(bookId: string, workspaceRoot?: string): string {\n return path.join(bookRoot(bookId, workspaceRoot), \"accounts.json\");\n}\n\nfunction journalDir(bookId: string, workspaceRoot?: string): string {\n return path.join(bookRoot(bookId, workspaceRoot), \"journal\");\n}\n\nfunction journalFileFor(bookId: string, period: string, workspaceRoot?: string): string {\n return path.join(journalDir(bookId, workspaceRoot), `${period}.jsonl`);\n}\n\nfunction snapshotsDir(bookId: string, workspaceRoot?: string): string {\n return path.join(bookRoot(bookId, workspaceRoot), \"snapshots\");\n}\n\nfunction snapshotFileFor(bookId: string, period: string, workspaceRoot?: string): string {\n return path.join(snapshotsDir(bookId, workspaceRoot), `${period}.json`);\n}\n\nasync function fileExists(filePath: string): Promise<boolean> {\n try {\n await fsPromises.access(filePath);\n return true;\n } catch {\n return false;\n }\n}\n\n/** Strict variant of `readJsonOrNull` from `./json.ts`: returns null\n * on ENOENT but RETHROWS other read errors and parse failures so a\n * corrupted accounting journal surfaces rather than silently\n * collapsing to \"no data\". `./json.ts` keeps the permissive\n * variant for user-config files where a single bad keystroke\n * shouldn't 500 the server. */\nasync function readJsonStrict<T>(filePath: string): Promise<T | null> {\n try {\n const raw = await fsPromises.readFile(filePath, \"utf-8\");\n // Cast kept (#2692): the persisted shapes are deliberately looser than\n // their types — `BookSummary.fiscalYearEnd` is typed as a month number but\n // legacy books hold \"Q1\"..\"Q4\" on disk (`normalizeBookFiscalYearEnd`\n // absorbs it on read). A predicate written against the type would reject\n // those files and lock the user out of their books.\n return JSON.parse(raw) as T;\n } catch (err) {\n if (isEnoent(err)) return null;\n throw err;\n }\n}\n\n// ── config.json ────────────────────────────────────────────────────\n\n/** Migrate a legacy calendar-quarter `fiscalYearEnd` token (\"Q1\"..\"Q4\")\n * to its closing-month number in memory so every downstream consumer\n * (reports, time-series, the UI selects) sees one shape. Absent stays\n * absent — the field is optional and resolves to the default on read;\n * we don't stamp an explicit December onto a book that never chose one.\n * Nothing is written back here (no auto-migrate on disk). */\nfunction normalizeBookFiscalYearEnd(book: BookSummary): BookSummary {\n if (book.fiscalYearEnd === undefined) return book;\n const resolved = resolveFiscalYearEnd(book.fiscalYearEnd);\n return book.fiscalYearEnd === resolved ? book : { ...book, fiscalYearEnd: resolved };\n}\n\nexport async function readConfig(workspaceRoot?: string): Promise<AccountingConfig | null> {\n const config = await readJsonStrict<AccountingConfig>(configPath(workspaceRoot));\n if (!config) return null;\n return { ...config, books: config.books.map(normalizeBookFiscalYearEnd) };\n}\n\nexport async function writeConfig(config: AccountingConfig, workspaceRoot?: string): Promise<void> {\n await writeJsonAtomic(configPath(workspaceRoot), config);\n}\n\n// ── accounts.json ──────────────────────────────────────────────────\n\nexport async function readAccounts(bookId: string, workspaceRoot?: string): Promise<Account[]> {\n const accounts = await readJsonStrict<Account[]>(accountsPath(bookId, workspaceRoot));\n return accounts ?? [];\n}\n\nexport async function writeAccounts(bookId: string, accounts: Account[], workspaceRoot?: string): Promise<void> {\n await writeJsonAtomic(accountsPath(bookId, workspaceRoot), accounts);\n}\n\n// ── journal/YYYY-MM.jsonl (append-only) ────────────────────────────\n\n/** Convert a YYYY-MM-DD date string to its YYYY-MM month bucket. The\n * month bucket dictates which JSONL file the entry lives in. */\nexport function periodFromDate(date: string): string {\n // YYYY-MM-DD → YYYY-MM. Validate the prefix shape so a malformed\n // input fails early instead of silently bucketing into \"1970-01\"\n // or similar.\n if (!/^\\d{4}-\\d{2}-\\d{2}$/.test(date)) {\n throw new Error(`accounting: invalid date format ${JSON.stringify(date)} (expected YYYY-MM-DD)`);\n }\n return date.slice(0, 7);\n}\n\n/** Append one entry to the appropriate month's JSONL.\n *\n * Uses POSIX append-only semantics (`fs.appendFile` → `O_APPEND`).\n * Two concurrent callers landing in the same month file are\n * serialised by the kernel — neither overwrites the other, which\n * is the bug the previous read-modify-write implementation had.\n *\n * Crash mid-write: an entry shorter than `PIPE_BUF` (≥ 512 bytes\n * on every supported platform) writes atomically; a single\n * serialised `JournalEntry` is comfortably under that. If the\n * process is killed during the syscall the worst case is a torn\n * trailing line, which `readJournalMonth` already tolerates by\n * skipping unparseable lines and surfacing a `skipped` count to\n * the caller. */\nexport async function appendJournal(bookId: string, entry: JournalEntry, workspaceRoot?: string): Promise<void> {\n const period = periodFromDate(entry.date);\n const file = journalFileFor(bookId, period, workspaceRoot);\n await fsPromises.mkdir(path.dirname(file), { recursive: true });\n await fsPromises.appendFile(file, `${JSON.stringify(entry)}\\n`, { encoding: \"utf-8\" });\n}\n\nfunction groupEntriesByPeriod(entries: readonly JournalEntry[]): Map<string, JournalEntry[]> {\n const byPeriod = new Map<string, JournalEntry[]>();\n for (const entry of entries) {\n const period = periodFromDate(entry.date);\n const list = byPeriod.get(period) ?? [];\n list.push(entry);\n byPeriod.set(period, list);\n }\n return byPeriod;\n}\n\n/** Append a batch of entries: same-period entries are concatenated\n * into one `appendFile` call so the whole same-period chunk hits\n * the kernel as a single `O_APPEND` write — small chunks (under\n * `PIPE_BUF`, ≥ 512 bytes on every supported platform) are\n * guaranteed atomic by POSIX, and `O_APPEND` serialises with any\n * concurrent appender (a parallel `appendJournal` / `addEntries`\n * call can never overwrite our write or vice versa). Cross-period\n * batches loop one append per period; each is independently\n * concurrency-safe but their union is not transactional across\n * files (out of scope for the append-only JSONL design). */\nexport async function appendJournalBatch(bookId: string, entries: readonly JournalEntry[], workspaceRoot?: string): Promise<void> {\n if (entries.length === 0) return;\n const byPeriod = groupEntriesByPeriod(entries);\n for (const [period, items] of byPeriod) {\n const file = journalFileFor(bookId, period, workspaceRoot);\n await fsPromises.mkdir(path.dirname(file), { recursive: true });\n const chunk = items.map((entry) => `${JSON.stringify(entry)}\\n`).join(\"\");\n await fsPromises.appendFile(file, chunk, { encoding: \"utf-8\" });\n }\n}\n\n/** One JSONL line, or null when it isn't a journal entry — unparseable JSON\n * and JSON of the wrong shape are the same failure to a caller that can only\n * skip the line. */\nfunction parseJournalLine(line: string): JournalEntry | null {\n try {\n const parsed: unknown = JSON.parse(line);\n return isJournalEntry(parsed) ? parsed : null;\n } catch {\n return null;\n }\n}\n\n/** Read a single month's JSONL. Malformed lines are skipped (logged\n * by the caller; this layer just returns the parseable subset) so\n * one bad line doesn't lock the user out of their book. */\nexport async function readJournalMonth(bookId: string, period: string, workspaceRoot?: string): Promise<{ entries: JournalEntry[]; skipped: number }> {\n const file = journalFileFor(bookId, period, workspaceRoot);\n let raw: string;\n try {\n raw = await fsPromises.readFile(file, \"utf-8\");\n } catch (err) {\n if (isEnoent(err)) return { entries: [], skipped: 0 };\n throw err;\n }\n const parsed = raw\n .split(\"\\n\")\n .filter((line) => line.trim() !== \"\")\n .map(parseJournalLine);\n const entries = parsed.filter((entry): entry is JournalEntry => entry !== null);\n return { entries, skipped: parsed.length - entries.length };\n}\n\n/** List the YYYY-MM periods that have a journal file on disk, sorted\n * ascending. Useful for full-history scans (rebuilding snapshots\n * from scratch). */\nexport async function listJournalPeriods(bookId: string, workspaceRoot?: string): Promise<string[]> {\n let names: string[];\n try {\n names = await fsPromises.readdir(journalDir(bookId, workspaceRoot));\n } catch (err) {\n if (isEnoent(err)) return [];\n throw err;\n }\n return names\n .filter((name) => /^\\d{4}-\\d{2}\\.jsonl$/.test(name))\n .map((name) => name.slice(0, 7))\n .sort();\n}\n\n// ── snapshots/YYYY-MM.json (cache, not source of truth) ────────────\n\nexport async function readSnapshot(bookId: string, period: string, workspaceRoot?: string): Promise<MonthSnapshot | null> {\n return readJsonStrict<MonthSnapshot>(snapshotFileFor(bookId, period, workspaceRoot));\n}\n\nexport async function writeSnapshot(bookId: string, snapshot: MonthSnapshot, workspaceRoot?: string): Promise<void> {\n const file = snapshotFileFor(bookId, snapshot.period, workspaceRoot);\n await fsPromises.mkdir(path.dirname(file), { recursive: true });\n // `uniqueTmp` guards against the lazy fallback in `getOrBuildSnapshot`\n // racing with the background rebuild — both can land in\n // `writeSnapshot` for the same period at once. Distinct tmp file\n // names mean each writer renames its own; the destination\n // overwrites idempotently (both derive from the same journal).\n await writeJsonAtomic(file, snapshot, { uniqueTmp: true });\n}\n\n/** Drop snapshot files for all periods >= `fromPeriod`. The next\n * read regenerates them. Idempotent: missing files are silently\n * ignored. */\nexport async function invalidateSnapshotsFrom(bookId: string, fromPeriod: string, workspaceRoot?: string): Promise<{ removed: string[] }> {\n let names: string[];\n try {\n names = await fsPromises.readdir(snapshotsDir(bookId, workspaceRoot));\n } catch (err) {\n if (isEnoent(err)) return { removed: [] };\n throw err;\n }\n const removed: string[] = [];\n for (const name of names) {\n const match = /^(\\d{4}-\\d{2})\\.json$/.exec(name);\n if (!match) continue;\n const [, period] = match;\n if (period === undefined) continue;\n if (period >= fromPeriod) {\n await fsPromises.rm(path.join(snapshotsDir(bookId, workspaceRoot), name), { force: true });\n removed.push(period);\n }\n }\n return { removed: removed.sort() };\n}\n\n/** Drop ALL snapshots for a book — used by `rebuildSnapshots()`\n * with no `from`. Equivalent to `invalidateSnapshotsFrom(\"0000-00\")`\n * but reads more clearly at call sites. */\nexport async function invalidateAllSnapshots(bookId: string, workspaceRoot?: string): Promise<{ removed: string[] }> {\n return invalidateSnapshotsFrom(bookId, \"0000-00\", workspaceRoot);\n}\n\n// ── book directory housekeeping ────────────────────────────────────\n\nexport async function bookExists(bookId: string, workspaceRoot?: string): Promise<boolean> {\n return fileExists(bookRoot(bookId, workspaceRoot));\n}\n\nexport async function ensureBookDir(bookId: string, workspaceRoot?: string): Promise<void> {\n await fsPromises.mkdir(bookRoot(bookId, workspaceRoot), { recursive: true });\n await fsPromises.mkdir(journalDir(bookId, workspaceRoot), { recursive: true });\n await fsPromises.mkdir(snapshotsDir(bookId, workspaceRoot), { recursive: true });\n}\n\n/** Recursively delete a book's directory. Used by `deleteBook` after\n * the config has been updated to drop the entry. */\nexport async function removeBookDir(bookId: string, workspaceRoot?: string): Promise<void> {\n await fsPromises.rm(bookRoot(bookId, workspaceRoot), { recursive: true, force: true });\n}\n","// Opening balance (\"year-start B/S\") logic. Adoption flow: a user\n// migrating from another bookkeeping system enters their existing\n// asset / liability / equity balances as of a chosen `asOfDate`,\n// instead of replaying their entire historical journal.\n//\n// Stored as a single `kind: \"opening\"` entry in the regular journal\n// — keeps the journal as the single source of truth, and makes\n// reports treat the opening as just an early entry without special\n// branches in aggregation.\n//\n// Replacing an existing opening: void the old, append the new. The\n// route handler is responsible for ordering this with snapshot\n// invalidation so the \"before\" snapshots get dropped.\n\nimport { isUnknownArray } from \"@mulmoclaude/common\";\n\nimport type { Account, JournalEntry, JournalLine } from \"../shared/types.js\";\nimport { BALANCE_SHEET_ACCOUNT_TYPES } from \"../shared/types.js\";\nimport { checkBalances, isValidCalendarDate, parseJournalLine, voidedIdSet } from \"./journal.js\";\n\nexport interface OpeningValidationError {\n field: string;\n message: string;\n}\n\nexport type OpeningParseResult = { ok: true; lines: JournalLine[] } | { ok: false; errors: OpeningValidationError[] };\n\n/** Find the existing opening entry for a book, if any. Multiple\n * openings shouldn't coexist (the route enforces void-then-append),\n * but if they do the most recent by `createdAt` wins so callers\n * always see one canonical opening. */\nexport function findActiveOpening(entries: readonly JournalEntry[]): JournalEntry | null {\n const voided = voidedIdSet(entries);\n let active: JournalEntry | null = null;\n for (const entry of entries) {\n if (entry.kind !== \"opening\") continue;\n if (voided.has(entry.id)) continue;\n if (!active || entry.createdAt > active.createdAt) active = entry;\n }\n return active;\n}\n\ninterface OpeningParseInput {\n asOfDate: string;\n lines: unknown;\n accounts: readonly Account[];\n existingEntries: readonly JournalEntry[];\n}\n\nfunction validateOpeningAccount(line: JournalLine, idx: number, accountByCode: ReadonlyMap<string, Account>, errors: OpeningValidationError[]): void {\n const acct = accountByCode.get(line.accountCode);\n if (!acct) {\n errors.push({ field: `lines[${idx}].accountCode`, message: `unknown account code ${JSON.stringify(line.accountCode)}` });\n return;\n }\n if (!BALANCE_SHEET_ACCOUNT_TYPES.includes(acct.type)) {\n errors.push({\n field: `lines[${idx}].accountCode`,\n message: `account ${acct.code} is type ${acct.type}; opening balances may only reference balance-sheet accounts (asset / liability / equity)`,\n });\n }\n}\n\n/** Opening lines are narrowed by the same shape parser the journal\n * uses, but they answer to different rules afterwards: balance-sheet\n * accounts only, and no \"exactly one side\" requirement — the opening\n * form lets a user carry both columns on one account. */\nfunction parseOpeningLines(raw: readonly unknown[], accounts: readonly Account[], errors: OpeningValidationError[]): JournalLine[] {\n const accountByCode = new Map(accounts.map((account) => [account.code, account]));\n const lines: JournalLine[] = [];\n raw.forEach((rawLine, idx) => {\n const line = parseJournalLine(rawLine, idx, errors);\n if (line === null) return;\n validateOpeningAccount(line, idx, accountByCode, errors);\n lines.push(line);\n });\n return lines;\n}\n\nfunction validateAsOfPredatesEverything(input: { asOfDate: string; existingEntries: readonly JournalEntry[] }, errors: OpeningValidationError[]): void {\n // The point of the rule is \"you can't enter an opening dated\n // 2026-01-01 if you've already booked transactions in December\n // 2025\" — that would silently change the meaning of those\n // December transactions. Existing openings (about to be\n // replaced) and already-voided entries are exempt.\n const voided = voidedIdSet(input.existingEntries);\n for (const entry of input.existingEntries) {\n if (entry.kind === \"opening\") continue;\n if (entry.kind === \"void-marker\") continue;\n if (voided.has(entry.id)) continue;\n if (entry.date < input.asOfDate) {\n errors.push({\n field: \"asOfDate\",\n message: `cannot set opening as of ${input.asOfDate}: existing entry ${entry.id} dated ${entry.date} is older. Void it first or pick an earlier asOfDate.`,\n });\n break; // one error is enough — listing every conflicting entry would be noisy\n }\n }\n}\n\n/** Parse the inputs for `setOpeningBalances`, returning the narrowed\n * lines so the caller can persist what was actually checked. Caller\n * passes the full list of journal entries in the book so we can check\n * the \"asOfDate must precede every other entry\" rule. An opening with\n * zero lines is accepted as a no-op marker — it satisfies the\n * \"book has an opening\" gate the UI uses without committing the\n * user to specific balances on day one (they can replace it\n * later). */\nexport function parseOpening(input: OpeningParseInput): OpeningParseResult {\n const errors: OpeningValidationError[] = [];\n if (!isValidCalendarDate(input.asOfDate)) {\n errors.push({ field: \"asOfDate\", message: `expected YYYY-MM-DD calendar date, got ${JSON.stringify(input.asOfDate)}` });\n }\n if (!isUnknownArray(input.lines)) {\n errors.push({ field: \"lines\", message: \"lines must be an array\" });\n return { ok: false, errors };\n }\n const lines = parseOpeningLines(input.lines, input.accounts, errors);\n checkBalances(lines, input.lines.length, \"opening\", errors);\n validateAsOfPredatesEverything(input, errors);\n if (errors.length > 0) return { ok: false, errors };\n return { ok: true, lines };\n}\n","// Pure parsing + normalization for the Account record: `parseAccountInput`\n// narrows a wire payload to an `Account`, `normalizeStoredAccount` decides\n// what of it reaches disk. Both are pure, so unit tests exercise the\n// shape rules + the field-whitelist + the active-flag policy without\n// spinning up a file system, and the service-layer `upsertAccount` is\n// left with only its file-IO and snapshot-invalidation orchestration.\n//\n// Policy summary (mirrored in the `upsertAccount` JSDoc):\n// - whitelist: only `code`, `name`, `type`, optional `note`, and\n// `active` are persisted. Unknown keys from a mistyped caller\n// are dropped — this includes the now-removed\n// `tracksTaxRegistration` flag from older books, which is\n// silently sloughed off the next time an account is upserted.\n// - `note`: stored only when a non-empty trimmed string. An\n// empty string is treated the same as omitted.\n// - `active`:\n// explicit `false` → store `false` (deactivate)\n// explicit `true` → omit (reactivate; default-active)\n// omitted → inherit from `existing` (preserves\n// a soft-deleted account when a caller\n// updates name/type/note without\n// mentioning the active flag — the bug\n// coverage that prompted this helper)\n\nimport { isRecord } from \"@mulmoclaude/common\";\n\nimport type { Account, AccountType } from \"../shared/types.js\";\nimport { ACCOUNT_TYPES } from \"../shared/types.js\";\n\nexport type AccountParseResult = { ok: true; account: Account } | { ok: false; message: string };\n\nfunction isAccountType(value: unknown): value is AccountType {\n return ACCOUNT_TYPES.some((accountType) => accountType === value);\n}\n\n/** Narrow a wire payload to an `Account`. `name` and `type` are as\n * required as `code`: an account persisted without a type is invisible\n * to every report, which groups rows by it. */\nexport function parseAccountInput(raw: unknown): AccountParseResult {\n if (!isRecord(raw)) return { ok: false, message: \"account is required — pass an object with code, name, and type\" };\n const { code, name, type, note, active } = raw;\n if (typeof code !== \"string\" || code.length === 0) return { ok: false, message: \"account code is required\" };\n if (typeof name !== \"string\" || name.trim() === \"\") return { ok: false, message: \"account name is required\" };\n if (!isAccountType(type)) return { ok: false, message: `account type ${JSON.stringify(type)} is invalid — must be one of: ${ACCOUNT_TYPES.join(\", \")}` };\n if (note !== undefined && typeof note !== \"string\") return { ok: false, message: \"account note must be a string when supplied\" };\n if (active !== undefined && typeof active !== \"boolean\") return { ok: false, message: \"account active must be a boolean when supplied\" };\n const account: Account = { code, name, type };\n if (typeof note === \"string\") account.note = note;\n if (typeof active === \"boolean\") account.active = active;\n return { ok: true, account };\n}\n\nexport function normalizeStoredAccount(input: Account, existing?: Account): Account {\n const stored: Account = { code: input.code, name: input.name, type: input.type };\n if (typeof input.note === \"string\" && input.note.length > 0) stored.note = input.note;\n const inheritInactive = input.active === undefined && existing?.active === false;\n if (input.active === false || inheritInactive) stored.active = false;\n return stored;\n}\n","// Aggregation: balance sheet, profit & loss, and ledger from journal\n// entries. Pure — feeds on the entries and accounts caller has\n// already loaded. Snapshot-aware aggregation is layered on top in\n// `snapshotCache.ts`, which calls into here.\n\nimport type { Account, AccountBalance, AccountType, BalanceSheet, BalanceSheetSection, JournalEntry, Ledger, LedgerRow, ProfitLoss } from \"../shared/types.js\";\n\nconst ZERO_TOLERANCE = 0.0049; // hide rows that round to 0 at 2dp\n\n/** Collapse an accountCode → netDebit map into the sorted\n * `AccountBalance[]` wire shape. Shared by `aggregateBalances` and\n * the snapshot cache's `mergeBalances` so the two produce identical,\n * deterministically ordered output. */\nexport function sortedBalancesFromMap(balanceByCode: ReadonlyMap<string, number>): AccountBalance[] {\n return Array.from(balanceByCode.entries())\n .map(([accountCode, netDebit]) => ({ accountCode, netDebit }))\n .sort((lhs, rhs) => lhs.accountCode.localeCompare(rhs.accountCode));\n}\n\n/** Returns net (debit − credit) per account across the supplied\n * entries. Voids work by having an original + reverse pair that\n * cancel mathematically — both are included in aggregation (their\n * contributions sum to zero). The `void-marker` entries carry no\n * lines, so excluding them is just a formality.\n *\n * Why not \"exclude original via voidedIdSet\"? Because then the\n * reverse half would remain unmatched, and the net would be the\n * original's amount with the wrong sign. Letting the math cancel\n * naturally is simpler and impossible to get wrong. */\nexport function aggregateBalances(entries: readonly JournalEntry[]): AccountBalance[] {\n const map = new Map<string, number>();\n for (const entry of entries) {\n if (entry.kind === \"void-marker\") continue;\n for (const line of entry.lines) {\n const cur = map.get(line.accountCode) ?? 0;\n const debit = line.debit ?? 0;\n const credit = line.credit ?? 0;\n map.set(line.accountCode, cur + debit - credit);\n }\n }\n return sortedBalancesFromMap(map);\n}\n\nfunction naturalSign(type: AccountType, netDebit: number): number {\n // Assets / expenses are debit-positive (positive netDebit reads\n // as a positive presentation balance). Liabilities / equity /\n // income are credit-positive — flip the sign for display.\n if (type === \"asset\" || type === \"expense\") return netDebit;\n return -netDebit;\n}\n\n/** Sentinel `accountCode` for the synthetic \"Current period\n * earnings\" row added to the Equity section by `buildBalanceSheet`.\n * The View detects this code and substitutes a localised label\n * for the fixed English fallback. */\nexport const CURRENT_EARNINGS_ACCOUNT_CODE = \"_currentEarnings\";\n\nfunction computeCurrentEarnings(accounts: readonly Account[], balanceByCode: ReadonlyMap<string, number>): number {\n // Σ income − Σ expense, in natural-sign presentation. Without\n // this synthetic Equity row the B/S would be off by exactly net\n // income during the period, because closing entries that fold\n // income/expense into Retained Earnings haven't been booked yet.\n let earnings = 0;\n for (const account of accounts) {\n if (account.type !== \"income\" && account.type !== \"expense\") continue;\n const presented = naturalSign(account.type, balanceByCode.get(account.code) ?? 0);\n earnings += account.type === \"income\" ? presented : -presented;\n }\n return earnings;\n}\n\nfunction buildBalanceSheetSection(\n type: AccountType,\n accounts: readonly Account[],\n balanceByCode: ReadonlyMap<string, number>,\n currentEarnings: number,\n): BalanceSheetSection {\n const rows: BalanceSheetSection[\"rows\"] = accounts\n .filter((account) => account.type === type)\n .map((account) => ({\n accountCode: account.code,\n accountName: account.name,\n balance: naturalSign(type, balanceByCode.get(account.code) ?? 0),\n }))\n .filter((row) => Math.abs(row.balance) > ZERO_TOLERANCE);\n if (type === \"equity\" && Math.abs(currentEarnings) > ZERO_TOLERANCE) {\n rows.push({ accountCode: CURRENT_EARNINGS_ACCOUNT_CODE, accountName: \"Current period earnings\", balance: currentEarnings });\n }\n return { type, rows, total: rows.reduce((sum, row) => sum + row.balance, 0) };\n}\n\nexport function buildBalanceSheet(input: { accounts: readonly Account[]; balances: readonly AccountBalance[]; asOf: string }): BalanceSheet {\n const balanceByCode = new Map(input.balances.map((row) => [row.accountCode, row.netDebit]));\n const currentEarnings = computeCurrentEarnings(input.accounts, balanceByCode);\n // Named rather than indexed out of an array so the accounting-equation\n // arithmetic below can't silently read a section that isn't there.\n const section = (type: AccountType) => buildBalanceSheetSection(type, input.accounts, balanceByCode, currentEarnings);\n const assets = section(\"asset\");\n const liabilities = section(\"liability\");\n const equity = section(\"equity\");\n return {\n asOf: input.asOf,\n sections: [assets, liabilities, equity],\n imbalance: assets.total - (liabilities.total + equity.total),\n };\n}\n\nexport function buildProfitLoss(input: { accounts: readonly Account[]; entries: readonly JournalEntry[]; from: string; to: string }): ProfitLoss {\n const inRange = input.entries.filter((entry) => entry.date >= input.from && entry.date <= input.to);\n const balances = aggregateBalances(inRange);\n const balanceByCode = new Map(balances.map((row) => [row.accountCode, row.netDebit]));\n const incomeRows: ProfitLoss[\"income\"][\"rows\"] = [];\n const expenseRows: ProfitLoss[\"expense\"][\"rows\"] = [];\n let incomeTotal = 0;\n let expenseTotal = 0;\n for (const account of input.accounts) {\n const netDebit = balanceByCode.get(account.code) ?? 0;\n const presented = naturalSign(account.type, netDebit);\n if (Math.abs(presented) <= ZERO_TOLERANCE) continue;\n if (account.type === \"income\") {\n incomeRows.push({ accountCode: account.code, accountName: account.name, amount: presented });\n incomeTotal += presented;\n } else if (account.type === \"expense\") {\n expenseRows.push({ accountCode: account.code, accountName: account.name, amount: presented });\n expenseTotal += presented;\n }\n }\n return {\n from: input.from,\n to: input.to,\n income: { rows: incomeRows, total: incomeTotal },\n expense: { rows: expenseRows, total: expenseTotal },\n netIncome: incomeTotal - expenseTotal,\n };\n}\n\ninterface LedgerLineAccumulator {\n rows: LedgerRow[];\n running: number;\n}\n\n/** Concatenate the entry-level memo (the *what-happened*) with the\n * line-level memo (the *why-this-account*) so a per-account ledger\n * view shows both. Without this combine, a Sales Tax Receivable\n * ledger row would show \"仮払消費税 10%\" but lose the originating\n * \"Starbucks Tokyo — coffee\" — and the user can't tell which\n * transaction the row came from. Identity-collapse handles the\n * case where someone set both fields to the same string. */\nfunction combineMemo(entryMemo: string | undefined, lineMemo: string | undefined): string | undefined {\n if (!entryMemo) return lineMemo;\n if (!lineMemo) return entryMemo;\n if (entryMemo === lineMemo) return entryMemo;\n return `${entryMemo} · ${lineMemo}`;\n}\n\nfunction accumulateLedgerEntry(\n entry: JournalEntry,\n accountCode: string,\n fromDate: string | undefined,\n toDate: string | undefined,\n acc: LedgerLineAccumulator,\n): void {\n if (entry.kind === \"void-marker\") return;\n for (const line of entry.lines) {\n if (line.accountCode !== accountCode) continue;\n const debit = line.debit ?? 0;\n const credit = line.credit ?? 0;\n acc.running += debit - credit;\n if (fromDate && entry.date < fromDate) continue;\n if (toDate && entry.date > toDate) continue;\n const row: LedgerRow = {\n entryId: entry.id,\n date: entry.date,\n kind: entry.kind,\n memo: combineMemo(entry.memo, line.memo),\n debit,\n credit,\n runningBalance: acc.running,\n };\n if (line.taxRegistrationId !== undefined) row.taxRegistrationId = line.taxRegistrationId;\n acc.rows.push(row);\n }\n}\n\nexport function buildLedger(input: { account: Account; entries: readonly JournalEntry[]; from?: string | undefined; to?: string | undefined }): Ledger {\n // Same rule as `aggregateBalances`: include original and reverse\n // for the math to cancel; exclude markers. The reverse entry\n // itself carries `kind: \"void\"` so the row is visually\n // distinguishable in the ledger.\n const sorted = [...input.entries].sort((lhs, rhs) => (lhs.date === rhs.date ? lhs.createdAt.localeCompare(rhs.createdAt) : lhs.date.localeCompare(rhs.date)));\n const acc: LedgerLineAccumulator = { rows: [], running: 0 };\n for (const entry of sorted) {\n accumulateLedgerEntry(entry, input.account.code, input.from, input.to, acc);\n }\n return {\n accountCode: input.account.code,\n accountName: input.account.name,\n rows: acc.rows,\n closingBalance: acc.running,\n };\n}\n","// Time-series aggregation for the accounting plugin: bucketise a\n// date range by month / fiscal quarter / fiscal year and roll up a\n// metric (revenue / expense / net income / closing balance of a\n// specific account) into a chart-ready `(label, value)[]` series.\n//\n// LLM-facing only — the in-canvas Accounting `<View>` keeps using\n// `getReport` for its tab-driven UI. `getTimeSeries` exists so a\n// single tool round-trip can answer \"chart my quarterly revenue\n// over the last two years\" without the LLM fanning out N calls and\n// stitching the buckets itself.\n//\n// Pure module: no I/O, no service-layer awareness. Caller hands in\n// the entries / accounts already loaded; we return the points.\n\nimport type { Account, AccountType, JournalEntry } from \"../shared/types.js\";\nimport { aggregateBalances } from \"./report.js\";\nimport {\n fiscalYearEndMonth,\n type FiscalYearEnd,\n TIME_SERIES_GRANULARITIES,\n TIME_SERIES_METRICS,\n type TimeSeriesGranularity,\n type TimeSeriesMetric,\n} from \"../shared\";\n\nexport { TIME_SERIES_GRANULARITIES, TIME_SERIES_METRICS };\nexport type { TimeSeriesGranularity, TimeSeriesMetric };\n\nexport interface Bucket {\n /** Inclusive YYYY-MM-DD lower bound. */\n from: string;\n /** Inclusive YYYY-MM-DD upper bound. */\n to: string;\n /** Chart x-axis label. Format depends on granularity:\n * \"YYYY-MM\" / \"FY{endYear}-Q{1..4}\" / \"FY{endYear}\". For fiscal\n * years that don't align with the calendar year (Q1/Q2/Q3 books)\n * the FY is named by its END calendar year — matches Japanese\n * \"令和7年度\" convention (Apr 2025 - Mar 2026 = FY2026). */\n label: string;\n}\n\nexport interface TimeSeriesPoint {\n label: string;\n from: string;\n to: string;\n /** Single number, natural-sign per metric. Revenue and net income\n * positive when income exceeds expense; expense reported as a\n * positive cost; account balance follows the account's display\n * sign (assets debit-positive, liabilities/equity credit-positive). */\n value: number;\n}\n\n// ── date arithmetic (pure, no Date for parsing/formatting) ─────────\n\nfunction pad2(num: number): string {\n return String(num).padStart(2, \"0\");\n}\n\nfunction fmtYmd(year: number, month: number, day: number): string {\n return `${year}-${pad2(month)}-${pad2(day)}`;\n}\n\ninterface YmdParts {\n year: number;\n month: number;\n day: number;\n}\n\nfunction parseYmd(value: string): YmdParts {\n // Callers validate `YYYY-MM-DD` first, so the defaults are unreachable;\n // they keep a malformed value degrading to NaN (what a non-numeric\n // segment already produces) rather than to `undefined`.\n const [year = Number.NaN, month = Number.NaN, day = Number.NaN] = value.split(\"-\").map((segment) => parseInt(segment, 10));\n return { year, month, day };\n}\n\nfunction lastDayOf(year: number, month: number): number {\n // UTC day 0 of next month = last day of this month, immune to TZ.\n return new Date(Date.UTC(year, month, 0)).getUTCDate();\n}\n\nfunction addDay(date: YmdParts): YmdParts {\n const stepped = new Date(Date.UTC(date.year, date.month - 1, date.day + 1));\n return {\n year: stepped.getUTCFullYear(),\n month: stepped.getUTCMonth() + 1,\n day: stepped.getUTCDate(),\n };\n}\n\n// ── fiscal-year arithmetic (year-month, no Date) ───────────────────\n\ninterface FyAnchor {\n /** Calendar year the fiscal year STARTED in. */\n fyStartYear: number;\n /** Calendar year the fiscal year ENDS in (used for labelling). */\n fyEndYear: number;\n /** First calendar month of the fiscal year (1-based). */\n startMonth: number;\n}\n\nfunction fyAnchorFor(date: YmdParts, end: FiscalYearEnd): FyAnchor {\n const closingMonth = fiscalYearEndMonth(end);\n const startMonth = (closingMonth % 12) + 1; // month after the close\n // FY containing date: started this calendar year if date.month is\n // at-or-after startMonth, else previous calendar year. Q4 books\n // (startMonth = 1) always land in the same calendar year — covered\n // by the same predicate.\n const fyStartYear = date.month >= startMonth ? date.year : date.year - 1;\n const fyEndYear = closingMonth === 12 ? fyStartYear : fyStartYear + 1;\n return { fyStartYear, fyEndYear, startMonth };\n}\n\n// ── per-granularity bucket lookup ──────────────────────────────────\n\nfunction monthBucketContaining(date: YmdParts): Bucket {\n return {\n from: fmtYmd(date.year, date.month, 1),\n to: fmtYmd(date.year, date.month, lastDayOf(date.year, date.month)),\n label: `${date.year}-${pad2(date.month)}`,\n };\n}\n\nfunction quarterBucketContaining(date: YmdParts, end: FiscalYearEnd): Bucket {\n const anchor = fyAnchorFor(date, end);\n const offset = (date.month - anchor.startMonth + 12) % 12; // 0..11\n const qIdx = Math.floor(offset / 3); // 0..3\n // Flat month-index from the calendar epoch makes year rollover\n // arithmetic trivial.\n const startFlat = anchor.fyStartYear * 12 + (anchor.startMonth - 1) + qIdx * 3;\n const endFlat = startFlat + 2;\n const qStartYear = Math.floor(startFlat / 12);\n const qStartMonth = (startFlat % 12) + 1;\n const qEndYear = Math.floor(endFlat / 12);\n const qEndMonth = (endFlat % 12) + 1;\n return {\n from: fmtYmd(qStartYear, qStartMonth, 1),\n to: fmtYmd(qEndYear, qEndMonth, lastDayOf(qEndYear, qEndMonth)),\n label: `FY${anchor.fyEndYear}-Q${qIdx + 1}`,\n };\n}\n\nfunction yearBucketContaining(date: YmdParts, end: FiscalYearEnd): Bucket {\n const anchor = fyAnchorFor(date, end);\n const startFlat = anchor.fyStartYear * 12 + (anchor.startMonth - 1);\n const endFlat = startFlat + 11;\n const yStartYear = Math.floor(startFlat / 12);\n const yStartMonth = (startFlat % 12) + 1;\n const yEndYear = Math.floor(endFlat / 12);\n const yEndMonth = (endFlat % 12) + 1;\n return {\n from: fmtYmd(yStartYear, yStartMonth, 1),\n to: fmtYmd(yEndYear, yEndMonth, lastDayOf(yEndYear, yEndMonth)),\n label: `FY${anchor.fyEndYear}`,\n };\n}\n\nfunction bucketContaining(date: YmdParts, granularity: TimeSeriesGranularity, end: FiscalYearEnd): Bucket {\n if (granularity === \"month\") return monthBucketContaining(date);\n if (granularity === \"quarter\") return quarterBucketContaining(date, end);\n return yearBucketContaining(date, end);\n}\n\n/** Walk inclusive `[from, to]` and return every bucket that overlaps\n * it, ordered ascending by `from`. The first bucket is the one\n * CONTAINING `from` — it can extend earlier than `from`; the last\n * bucket is the one CONTAINING `to` — it can extend past `to`. The\n * caller's response echoes the input range so the LLM can label the\n * chart truthfully (\"Revenue Apr 2025 – Sep 2026\" even though the\n * outermost buckets cover Apr-Jun 2025 and Jul-Sep 2026). */\nexport function bucketize(input: { from: string; to: string; granularity: TimeSeriesGranularity; fiscalYearEnd: FiscalYearEnd }): Bucket[] {\n if (input.from > input.to) return [];\n const start = parseYmd(input.from);\n const result: Bucket[] = [];\n let bucket = bucketContaining(start, input.granularity, input.fiscalYearEnd);\n // Buckets are contiguous; once a bucket's `from` is past `input.to`\n // every subsequent bucket is too.\n while (bucket.from <= input.to) {\n result.push(bucket);\n const next = addDay(parseYmd(bucket.to));\n bucket = bucketContaining(next, input.granularity, input.fiscalYearEnd);\n }\n return result;\n}\n\n// ── value computation ──────────────────────────────────────────────\n\n/** Convert raw netDebit to natural-sign presentation per account\n * type. Mirrors the helper in `report.ts` (kept private there). */\nfunction naturalSign(type: AccountType, netDebit: number): number {\n if (type === \"asset\" || type === \"expense\") return netDebit;\n return -netDebit;\n}\n\ninterface PresentationTotals {\n income: number;\n expense: number;\n}\n\n/** Sum presented income and expense values across the supplied\n * entries. Used for window-based metrics (revenue / expense /\n * netIncome). Opening entries reference B/S accounts only and so\n * contribute zero to either total — including them is harmless. */\nfunction presentedPlTotals(entries: readonly JournalEntry[], accountTypeByCode: ReadonlyMap<string, AccountType>): PresentationTotals {\n const balances = aggregateBalances(entries);\n let income = 0;\n let expense = 0;\n for (const row of balances) {\n const type = accountTypeByCode.get(row.accountCode);\n if (!type) continue;\n if (type === \"income\") income += naturalSign(type, row.netDebit);\n else if (type === \"expense\") expense += naturalSign(type, row.netDebit);\n }\n return { income, expense };\n}\n\nfunction entriesInWindow(entries: readonly JournalEntry[], from: string, toDate: string): JournalEntry[] {\n return entries.filter((entry) => entry.date >= from && entry.date <= toDate);\n}\n\nfunction entriesUpTo(entries: readonly JournalEntry[], toDate: string): JournalEntry[] {\n return entries.filter((entry) => entry.date <= toDate);\n}\n\nfunction valueForBucket(input: {\n bucket: Bucket;\n entries: readonly JournalEntry[];\n accounts: readonly Account[];\n metric: TimeSeriesMetric;\n accountCode?: string | undefined;\n}): number {\n const accountTypeByCode = new Map(input.accounts.map((acct) => [acct.code, acct.type]));\n if (input.metric === \"accountBalance\") {\n const code = input.accountCode;\n if (!code) return 0; // guarded at the route — defensive zero.\n const type = accountTypeByCode.get(code);\n if (!type) return 0;\n const cumulative = entriesUpTo(input.entries, input.bucket.to);\n const balances = aggregateBalances(cumulative);\n const row = balances.find((balance) => balance.accountCode === code);\n return row ? naturalSign(type, row.netDebit) : 0;\n }\n const window = entriesInWindow(input.entries, input.bucket.from, input.bucket.to);\n const totals = presentedPlTotals(window, accountTypeByCode);\n if (input.metric === \"revenue\") return totals.income;\n if (input.metric === \"expense\") return totals.expense;\n return totals.income - totals.expense; // netIncome\n}\n\nexport function buildTimeSeries(input: {\n buckets: readonly Bucket[];\n entries: readonly JournalEntry[];\n accounts: readonly Account[];\n metric: TimeSeriesMetric;\n accountCode?: string | undefined;\n}): TimeSeriesPoint[] {\n return input.buckets.map((bucket) => ({\n label: bucket.label,\n from: bucket.from,\n to: bucket.to,\n value: valueForBucket({\n bucket,\n entries: input.entries,\n accounts: input.accounts,\n metric: input.metric,\n accountCode: input.accountCode,\n }),\n }));\n}\n","// Pub/sub publisher for the accounting plugin. Mirror of\n// `server/events/file-change.ts`: module singleton + init function +\n// fire-and-forget publish helpers. The init wiring lives in\n// `server/index.ts` next to `initFileChangePublisher`.\n//\n// Channel names + payload shapes are imported from\n// `src/config/pubsubChannels.ts` so the publisher cannot drift from\n// the View-side subscribers.\n\nimport {\n bookChannel as accountingBookChannel,\n booksChannel as accountingBooksChannel,\n type BookChannelPayload as AccountingBookChannelPayload,\n} from \"../shared\";\nimport { channelScopeFor, log, type IPubSub } from \"./context.js\";\nimport { errorMessage } from \"../shared\";\n\nlet pubsub: IPubSub | null = null;\n\nexport function initAccountingEventPublisher(instance: IPubSub): void {\n pubsub = instance;\n}\n\n/** `channelFor` is called INSIDE the guard, not before it: resolving the\n * scope can throw (a write that reached the publisher with no root\n * under explicit-root mode), and a lost event with a warning beats\n * either crashing the write or publishing to the unscoped channel that\n * every project can hear. */\nfunction safePublish(channelFor: () => string, payload: unknown): void {\n if (!pubsub) return;\n let channel = \"(unresolved)\";\n try {\n channel = channelFor();\n pubsub.publish(channel, payload);\n } catch (err) {\n // Same fire-and-forget rationale as the file-change publisher:\n // dropping one event is better than crashing the server.\n log.warn(\"accounting\", \"publish failed; subscribers will miss this event\", {\n channel,\n error: errorMessage(err),\n });\n }\n}\n\n/** Per-book change notification. `period` should be the entry's\n * YYYY-MM bucket (or the earliest invalidated month for snapshot\n * events).\n *\n * `workspaceRoot` is the root the write happened under; the channel\n * name is namespaced by the host's opaque scope for it (see\n * `channelScopeFor`). Omit it — as a single-root host's service calls\n * do — and the name is what it has always been. */\nexport function publishBookChange(bookId: string, payload: AccountingBookChannelPayload, workspaceRoot?: string): void {\n safePublish(() => accountingBookChannel(bookId, channelScopeFor(workspaceRoot)), payload);\n}\n\n/** Fired when the *list* of books changes (createBook, deleteBook).\n * Payload is intentionally empty — subscribers refetch from\n * /api/accounting. Scoped by root like `publishBookChange`. */\nexport function publishBooksChanged(workspaceRoot?: string): void {\n safePublish(() => accountingBooksChannel(channelScopeFor(workspaceRoot)), {});\n}\n\n/** Test-only — drop the module singleton so each test starts clean. */\nexport function _resetAccountingEventPublisherForTesting(): void {\n pubsub = null;\n}\n","// Monthly balance snapshot cache.\n//\n// Source of truth: the journal JSONL files. Snapshots are derived\n// state — `data/accounting/books/<id>/snapshots/YYYY-MM.json` is\n// only ever a perf optimization. The invariant we maintain:\n//\n// for any (book, period) pair,\n// getOrBuildSnapshot(book, period)\n// ===\n// aggregateBalances(<all entries up to period end>)\n//\n// I.e. running with snapshots and running without snapshots must\n// produce byte-identical results. The unit test for this lives in\n// `test/accounting/test_snapshotCache.ts`.\n//\n// Rebuild policy: writes call `scheduleRebuild(bookId, fromPeriod)`\n// after invalidating stale snapshot files. Each book has at most one\n// rebuild in flight; additional writes during a running rebuild merge\n// into a single queued follow-up (so a burst of N writes runs at most\n// two rebuilds). `getOrBuildSnapshot` keeps a lazy fallback so a\n// report requested before the rebuild reaches that month is still\n// correct — it just builds inline.\n//\n// Test API: `awaitRebuildIdle(bookId)` and `inspectRebuildQueue(bookId)`\n// are diagnostics that let tests assert on queue state without\n// sleep-and-poll. Production code never needs them.\n\nimport path from \"node:path\";\n\nimport {\n invalidateSnapshotsFrom as ioInvalidateFrom,\n invalidateAllSnapshots as ioInvalidateAll,\n listJournalPeriods,\n readJournalMonth,\n readSnapshot,\n writeSnapshot,\n} from \"./io.js\";\nimport { aggregateBalances, sortedBalancesFromMap } from \"./report.js\";\nimport { publishBookChange } from \"./eventPublisher.js\";\nimport { defaultWorkspaceRoot, log } from \"./context.js\";\nimport { errorMessage, BOOK_EVENT_KINDS as ACCOUNTING_BOOK_EVENT_KINDS } from \"../shared\";\nimport type { MonthSnapshot } from \"./types.js\";\nimport type { AccountBalance, JournalEntry } from \"../shared/types.js\";\n\nfunction previousPeriod(period: string): string {\n // YYYY-MM → previous YYYY-MM. December rolls back to the previous\n // year's December.\n // A malformed period already yields NaN for a non-numeric segment, so\n // a missing segment degrades the same way instead of throwing here.\n const [year = Number.NaN, month = Number.NaN] = period.split(\"-\").map((segment) => parseInt(segment, 10));\n if (month === 1) return `${(year - 1).toString().padStart(4, \"0\")}-12`;\n return `${year.toString().padStart(4, \"0\")}-${(month - 1).toString().padStart(2, \"0\")}`;\n}\n\nfunction mergeBalances(base: readonly AccountBalance[], delta: readonly AccountBalance[]): AccountBalance[] {\n const map = new Map<string, number>();\n for (const row of base) map.set(row.accountCode, row.netDebit);\n for (const row of delta) {\n map.set(row.accountCode, (map.get(row.accountCode) ?? 0) + row.netDebit);\n }\n return sortedBalancesFromMap(map);\n}\n\nasync function buildEmptySnapshot(bookId: string, period: string, workspaceRoot?: string): Promise<MonthSnapshot> {\n const empty: MonthSnapshot = { period, balances: [], builtAt: new Date().toISOString() };\n await writeSnapshot(bookId, empty, workspaceRoot);\n return empty;\n}\n\n/** Build a snapshot at end-of-`period` for one book, lazily relying\n * on the previous month's snapshot if it exists. Falls all the way\n * back to the earliest journal month if no upstream snapshot is\n * available. Always writes the result to disk before returning. */\nexport async function getOrBuildSnapshot(bookId: string, period: string, workspaceRoot?: string): Promise<MonthSnapshot> {\n const cached = await readSnapshot(bookId, period, workspaceRoot);\n if (cached) return cached;\n\n // Earliest journal month determines where the recursion stops.\n // If the book has no journal at all, return an empty snapshot.\n const [earliestPeriod] = await listJournalPeriods(bookId, workspaceRoot);\n if (earliestPeriod === undefined || period < earliestPeriod) {\n return buildEmptySnapshot(bookId, period, workspaceRoot);\n }\n\n const { entries } = await readJournalMonth(bookId, period, workspaceRoot);\n const monthDelta = aggregateBalances(entries);\n\n // Get the prior month's closing snapshot — recurse, which will\n // either hit cache or build the chain back to the start.\n let priorBalances: readonly AccountBalance[] = [];\n if (period > earliestPeriod) {\n const prior = previousPeriod(period);\n const priorSnap = await getOrBuildSnapshot(bookId, prior, workspaceRoot);\n priorBalances = priorSnap.balances;\n }\n const merged = mergeBalances(priorBalances, monthDelta);\n const snap: MonthSnapshot = {\n period,\n balances: merged,\n builtAt: new Date().toISOString(),\n };\n await writeSnapshot(bookId, snap, workspaceRoot);\n return snap;\n}\n\n/** Compute closing balances at end-of-`period` from journal alone,\n * bypassing the snapshot cache. Used by the byte-equality\n * invariant test, and as a safety net for \"compute without\n * trusting cache\" paths. */\nexport async function balancesAtEndOf(bookId: string, period: string, workspaceRoot?: string): Promise<AccountBalance[]> {\n const periods = await listJournalPeriods(bookId, workspaceRoot);\n const all: JournalEntry[] = [];\n for (const monthKey of periods) {\n if (period < monthKey) break;\n const { entries } = await readJournalMonth(bookId, monthKey, workspaceRoot);\n for (const entry of entries) all.push(entry);\n }\n return aggregateBalances(all);\n}\n\n/** Drop snapshots for `fromPeriod` and later. Re-export from\n * accounting-io for callers that conceptually live in the cache\n * layer (so they don't reach into the IO module). */\nexport async function invalidateSnapshotsFrom(bookId: string, fromPeriod: string, workspaceRoot?: string): Promise<{ removed: string[] }> {\n return ioInvalidateFrom(bookId, fromPeriod, workspaceRoot);\n}\n\n/** Drop all snapshots and rebuild from scratch. Used by the\n * `rebuildSnapshots` admin action. Returns the periods that were\n * rebuilt. */\nexport async function rebuildAllSnapshots(bookId: string, workspaceRoot?: string): Promise<{ rebuilt: string[] }> {\n await ioInvalidateAll(bookId, workspaceRoot);\n const periods = await listJournalPeriods(bookId, workspaceRoot);\n for (const monthKey of periods) {\n await getOrBuildSnapshot(bookId, monthKey, workspaceRoot);\n }\n return { rebuilt: periods };\n}\n\n// ── async rebuild queue ────────────────────────────────────────────\n//\n// Per-book queue. A `running` promise represents the in-flight\n// rebuild. While it's running, additional `scheduleRebuild` calls\n// merge their `fromPeriod` into `pendingFromPeriod` (taking the\n// minimum so the next pass covers everyone's invalidation), and we\n// kick off a follow-up once the current one resolves.\n\ninterface RebuildQueueEntry {\n running: Promise<void>;\n pendingFromPeriod: string | null;\n pendingWorkspaceRoot: string | undefined;\n coalescedWriteCount: number;\n runningFromPeriod: string;\n /** Set by `cancelRebuild` (called from `deleteBook`). The runRebuild\n * loop checks before each write so a rebuild cannot resurrect the\n * book directory after `removeBookDir` has run. */\n cancelled: boolean;\n}\n\n// Keyed by (root, bookId), not by bookId. A bookId is unique WITHIN a\n// root and nowhere else, so a multi-root host (MulmoTerminal, one root\n// per project directory) running two projects that both hold a book\n// called `main` would otherwise share one queue: project B's write\n// would cancel project A's rebuild, and `awaitRebuildIdle` would\n// return while the other project's rebuild is still writing.\nconst rebuildQueues = new Map<string, RebuildQueueEntry>();\n\n/** Canonical key for the root a rebuild belongs to. An absent root\n * resolves to the host's configured one, so a call that passes it\n * explicitly and one that relies on the default share a queue. Under\n * explicit-root mode there is no default and `defaultWorkspaceRoot()`\n * throws — deliberately loud: a `cancelRebuild` / `awaitRebuildIdle`\n * that quietly keyed a queue nobody scheduled would return while the\n * real rebuild is still writing, which is how `deleteBook` races a\n * `writeSnapshot` back into existence.\n *\n * Lexical resolution only — symlinks are deliberately NOT resolved,\n * matching `canonicalRoot` in `@mulmoclaude/core`'s collection engine,\n * so one project has one identity across both packages. A host that\n * hands the engine a real path in one call and a symlink alias in the\n * next would split the queue; the fix belongs in the host's own root\n * resolution, which is where the two packages agree it lives. */\nfunction rootKey(workspaceRoot: string | undefined): string {\n return path.resolve(workspaceRoot ?? defaultWorkspaceRoot());\n}\n\nfunction queueKey(bookId: string, workspaceRoot: string | undefined): string {\n // NUL cannot appear in either half (a book id is `[A-Za-z0-9_-]`,\n // a path cannot contain it), so the join is unambiguous.\n return `${rootKey(workspaceRoot)}\\u0000${bookId}`;\n}\n\nfunction minPeriod(lhs: string | null, rhs: string): string {\n if (lhs === null) return rhs;\n return lhs < rhs ? lhs : rhs;\n}\n\nfunction isInvalidatedDuringRebuild(key: string, period: string): boolean {\n // A pending invalidation that covers `period` means the queued\n // follow-up rebuild will redo this period. Skip the in-flight\n // write so we don't pollute the cache with stale data while a\n // fresher computation is queued. Without this guard, the\n // sequence \"rebuild reads journal → caller writes a new entry →\n // caller invalidates → rebuild writes (stale) snapshot\" leaves\n // the cache lying about the latest state.\n const queue = rebuildQueues.get(key);\n return queue !== undefined && queue.pendingFromPeriod !== null && period >= queue.pendingFromPeriod;\n}\n\nfunction isCancelled(key: string): boolean {\n return rebuildQueues.get(key)?.cancelled === true;\n}\n\nasync function runRebuild(key: string, bookId: string, fromPeriod: string, workspaceRoot: string | undefined): Promise<void> {\n const startedAt = Date.now();\n log.info(\"accounting\", \"snapshot rebuild started\", { bookId, fromPeriod });\n publishBookChange(bookId, { kind: ACCOUNTING_BOOK_EVENT_KINDS.snapshotsRebuilding, period: fromPeriod }, workspaceRoot);\n const periods = await listJournalPeriods(bookId, workspaceRoot);\n const targets = periods.filter((monthKey) => monthKey >= fromPeriod);\n let written = 0;\n for (const monthKey of targets) {\n if (isCancelled(key)) break;\n if (isInvalidatedDuringRebuild(key, monthKey)) break;\n // Compute fresh from journal — bypasses getOrBuildSnapshot's\n // own write side-effect so the staleness check below is the\n // only writer in the rebuild path.\n const balances = await balancesAtEndOf(bookId, monthKey, workspaceRoot);\n if (isCancelled(key)) break;\n if (isInvalidatedDuringRebuild(key, monthKey)) break;\n await writeSnapshot(bookId, { period: monthKey, balances, builtAt: new Date().toISOString() }, workspaceRoot);\n if (isCancelled(key)) {\n // The book was deleted between our last check and the write —\n // `writeSnapshot` will have re-created the book directory tree\n // via mkdir-recursive. Undo it so we don't leave an orphaned\n // directory after `deleteBook` has run.\n await ioInvalidateFrom(bookId, monthKey, workspaceRoot);\n break;\n }\n if (isInvalidatedDuringRebuild(key, monthKey)) {\n // A concurrent invalidate raced ahead between our last check\n // and the disk write. The data we just wrote may be stale\n // relative to the latest journal — undo so the queued\n // follow-up rebuild starts from a clean slate.\n await ioInvalidateFrom(bookId, monthKey, workspaceRoot);\n break;\n }\n written += 1;\n publishBookChange(bookId, { kind: ACCOUNTING_BOOK_EVENT_KINDS.snapshotsReady, period: monthKey }, workspaceRoot);\n }\n log.info(\"accounting\", \"snapshot rebuild done\", { bookId, periods: written, durationMs: Date.now() - startedAt });\n}\n\nfunction startRebuild(key: string, bookId: string, fromPeriod: string, workspaceRoot: string | undefined): RebuildQueueEntry {\n const entry: RebuildQueueEntry = {\n running: Promise.resolve(),\n pendingFromPeriod: null,\n pendingWorkspaceRoot: undefined,\n coalescedWriteCount: 1,\n runningFromPeriod: fromPeriod,\n cancelled: false,\n };\n entry.running = runRebuild(key, bookId, fromPeriod, workspaceRoot)\n .catch((err) => {\n // A rebuild failure is logged but does not poison the queue —\n // the next `scheduleRebuild` call will start a fresh promise.\n log.error(\"accounting\", \"snapshot rebuild failed\", { bookId, fromPeriod, error: errorMessage(err) });\n })\n .then(() => {\n // Drain any work that piled up while we were running.\n const current = rebuildQueues.get(key);\n if (!current) return;\n // If the book was cancelled mid-rebuild (e.g. deleteBook ran),\n // drop the queue entry entirely — we must not start a successor\n // that would re-create the deleted book directory.\n if (current.cancelled) {\n rebuildQueues.delete(key);\n return;\n }\n if (current.pendingFromPeriod !== null) {\n const nextFrom = current.pendingFromPeriod;\n const nextRoot = current.pendingWorkspaceRoot;\n const carriedCount = current.coalescedWriteCount;\n const successor = startRebuild(key, bookId, nextFrom, nextRoot);\n successor.coalescedWriteCount += carriedCount;\n rebuildQueues.set(key, successor);\n } else {\n rebuildQueues.delete(key);\n }\n });\n return entry;\n}\n\n/** Schedule a background rebuild for `bookId` starting at `fromPeriod`.\n * Multiple calls during an in-flight rebuild coalesce into a single\n * follow-up rebuild that covers the minimum `fromPeriod` seen.\n * Returns immediately — the rebuild runs on its own promise chain. */\nexport function scheduleRebuild(bookId: string, fromPeriod: string, workspaceRoot?: string): void {\n const key = queueKey(bookId, workspaceRoot);\n const existing = rebuildQueues.get(key);\n if (!existing) {\n rebuildQueues.set(key, startRebuild(key, bookId, fromPeriod, workspaceRoot));\n return;\n }\n existing.pendingFromPeriod = minPeriod(existing.pendingFromPeriod, fromPeriod);\n existing.pendingWorkspaceRoot = workspaceRoot;\n existing.coalescedWriteCount += 1;\n}\n\n/** Test/diagnostic: resolves when no rebuild is running or queued for\n * `bookId`. Also called by `deleteBook` after `cancelRebuild` to\n * ensure a previously running rebuild has fully stopped before the\n * caller removes the book's directory on disk. */\nexport async function awaitRebuildIdle(bookId: string, workspaceRoot?: string): Promise<void> {\n const key = queueKey(bookId, workspaceRoot);\n while (rebuildQueues.has(key)) {\n const entry = rebuildQueues.get(key);\n if (!entry) return;\n await entry.running;\n }\n}\n\n/** Mark the book's in-flight rebuild as cancelled. The runRebuild\n * loop checks before each write and bails out, so a subsequent\n * `removeBookDir` cannot race with a `writeSnapshot` that would\n * re-create the directory tree. Pair with `awaitRebuildIdle(bookId)`\n * to wait for the in-flight rebuild to finish bailing. */\nexport function cancelRebuild(bookId: string, workspaceRoot?: string): void {\n const entry = rebuildQueues.get(queueKey(bookId, workspaceRoot));\n if (!entry) return;\n entry.cancelled = true;\n // Drop pending too — a cancelled book should not get a successor\n // rebuild after the in-flight one drains.\n entry.pendingFromPeriod = null;\n}\n\n/** Test/diagnostic: snapshot of the per-book queue state. Stable\n * enough to assert against; fields may grow over time. */\nexport function inspectRebuildQueue(\n bookId: string,\n workspaceRoot?: string,\n): {\n running: boolean;\n runningFromPeriod: string | null;\n pendingFromPeriod: string | null;\n coalescedWriteCount: number;\n} {\n const entry = rebuildQueues.get(queueKey(bookId, workspaceRoot));\n if (!entry) {\n return { running: false, runningFromPeriod: null, pendingFromPeriod: null, coalescedWriteCount: 0 };\n }\n return {\n running: true,\n runningFromPeriod: entry.runningFromPeriod,\n pendingFromPeriod: entry.pendingFromPeriod,\n coalescedWriteCount: entry.coalescedWriteCount,\n };\n}\n\n/** Test-only — drain all in-flight rebuilds, then drop queue state.\n * Awaiting first means a leftover rebuild can't continue writing\n * into the next test's tmp dir after we clear the bookkeeping. */\nexport async function _resetRebuildQueueForTesting(): Promise<void> {\n // Mark everything cancelled so loops bail at their next checkpoint\n // instead of continuing through every period.\n for (const entry of rebuildQueues.values()) {\n entry.cancelled = true;\n entry.pendingFromPeriod = null;\n }\n const pending = Array.from(rebuildQueues.values()).map((entry) => entry.running);\n await Promise.allSettled(pending);\n rebuildQueues.clear();\n}\n","// Default chart of accounts seeded into a freshly created book.\n// The active set is intentionally minimal — covers the common\n// categories users need to record their first opening balance and\n// post their first entries, without overwhelming a brand-new user.\n//\n// A second tier of `active: false` entries is included so the user\n// can flip on common-but-not-universal accounts (Inventory, Travel,\n// Depreciation Expense, …) from Manage Accounts with one click\n// rather than typing them in by hand. Inactive accounts stay\n// hidden from journal entry / ledger dropdowns until the user\n// reactivates them. Tax-related accounts (1400 / 2400) are an\n// exception — they ship active by default since almost every\n// jurisdiction levies a consumption / sales / VAT tax.\n\nimport type { Account } from \"../shared/types.js\";\n\nexport const DEFAULT_ACCOUNTS: readonly Account[] = [\n // Assets\n { code: \"1000\", name: \"Cash\", type: \"asset\" },\n { code: \"1001\", name: \"Petty Cash\", type: \"asset\", active: false },\n { code: \"1010\", name: \"Bank — Checking\", type: \"asset\" },\n { code: \"1020\", name: \"Bank — Savings\", type: \"asset\" },\n { code: \"1100\", name: \"Accounts Receivable\", type: \"asset\" },\n { code: \"1200\", name: \"Inventory\", type: \"asset\", active: false },\n { code: \"1300\", name: \"Prepaid Expenses\", type: \"asset\", active: false },\n // 14xx is the reserved \"tax-related current assets\" band — pairs\n // with 24xx on the liability side for the tax-excluded (税抜)\n // booking method: input tax paid on purchases sits here as an\n // asset and is netted against output-tax collected at filing\n // time. The Ledger view's T-number column and the\n // JournalEntryForm's per-line taxRegistrationId input key off\n // the 14xx prefix only (see `isTaxAccountCode`) — the\n // counterparty registration ID is load-bearing for input-tax\n // credit on purchases, not for the seller-side liability — so\n // any custom 14xx account a user adds participates without an\n // opt-in step. Active by default — most jurisdictions levy a\n // consumption / sales / VAT tax; tax-free contexts can\n // deactivate from Manage Accounts.\n // 1400 was briefly named \"Sales Tax Receivable\" — that label\n // conventionally means *output* tax billed to customers but not\n // yet collected. Renamed to \"Input Tax Receivable\" so the\n // purchase-side meaning matches the 14xx / 24xx booking pair and\n // the non-US naming the rest of the world uses (EU \"Input VAT\" /\n // UK VAT input / Japan 仮払消費税). CodeRabbit review on PR #1120.\n { code: \"1400\", name: \"Input Tax Receivable\", type: \"asset\" },\n { code: \"1500\", name: \"Equipment\", type: \"asset\" },\n { code: \"1510\", name: \"Furniture & Fixtures\", type: \"asset\", active: false },\n { code: \"1520\", name: \"Vehicles\", type: \"asset\", active: false },\n { code: \"1590\", name: \"Accumulated Depreciation\", type: \"asset\", active: false },\n // Liabilities\n { code: \"2000\", name: \"Accounts Payable\", type: \"liability\" },\n { code: \"2100\", name: \"Credit Card\", type: \"liability\" },\n { code: \"2200\", name: \"Loans Payable\", type: \"liability\" },\n { code: \"2300\", name: \"Accrued Expenses\", type: \"liability\", active: false },\n // 24xx is the reserved \"tax-related current liabilities\" band;\n // pairs with 14xx on the asset side. See the 1400 comment above.\n { code: \"2400\", name: \"Sales Tax Payable\", type: \"liability\" },\n { code: \"2500\", name: \"Payroll Liabilities\", type: \"liability\", active: false },\n // Equity\n // Required for opening balances: setOpeningBalances dumps the\n // plug into \"Retained Earnings\" by convention.\n { code: \"3000\", name: \"Owner's Equity\", type: \"equity\" },\n { code: \"3100\", name: \"Retained Earnings\", type: \"equity\" },\n { code: \"3200\", name: \"Owner's Draws\", type: \"equity\", active: false },\n // Income\n { code: \"4000\", name: \"Sales\", type: \"income\" },\n { code: \"4010\", name: \"Service Revenue\", type: \"income\", active: false },\n { code: \"4100\", name: \"Other Income\", type: \"income\" },\n { code: \"4200\", name: \"Interest Income\", type: \"income\", active: false },\n { code: \"4300\", name: \"Sales Returns & Discounts\", type: \"income\", active: false },\n // Expenses\n { code: \"5000\", name: \"Cost of Goods Sold\", type: \"expense\" },\n { code: \"5100\", name: \"Rent\", type: \"expense\" },\n { code: \"5200\", name: \"Utilities\", type: \"expense\" },\n { code: \"5300\", name: \"Salaries\", type: \"expense\" },\n { code: \"5400\", name: \"Office Supplies\", type: \"expense\" },\n { code: \"5500\", name: \"Advertising & Marketing\", type: \"expense\", active: false },\n { code: \"5600\", name: \"Travel\", type: \"expense\", active: false },\n { code: \"5610\", name: \"Meals & Entertainment\", type: \"expense\", active: false },\n { code: \"5700\", name: \"Professional Fees\", type: \"expense\", active: false },\n { code: \"5710\", name: \"Insurance\", type: \"expense\", active: false },\n { code: \"5720\", name: \"Software & Subscriptions\", type: \"expense\", active: false },\n { code: \"5730\", name: \"Bank Fees\", type: \"expense\", active: false },\n { code: \"5800\", name: \"Depreciation Expense\", type: \"expense\", active: false },\n { code: \"5810\", name: \"Taxes\", type: \"expense\", active: false },\n { code: \"5900\", name: \"Miscellaneous Expense\", type: \"expense\" },\n];\n","// Service layer for the accounting plugin. Wraps the IO + domain\n// modules into the handful of operations the route + MCP bridge\n// expose. Each function:\n//\n// - performs validation,\n// - mutates the journal / accounts / config files atomically,\n// - invalidates dependent snapshots,\n// - publishes a pub/sub event so subscribers refetch.\n//\n// Snapshot rebuild policy: writes invalidate stale snapshot files\n// synchronously, then call `scheduleRebuild` to rebuild them in the\n// background. `getOrBuildSnapshot` keeps a lazy fallback so a report\n// requested before the rebuild reaches that month still returns the\n// right number — it just builds inline. Both paths are byte-identical\n// (enforced by `test/accounting/test_snapshotCache.ts`).\n\nimport { randomUUID } from \"node:crypto\";\nimport { isUnknownArray } from \"@mulmoclaude/common\";\n\nimport {\n appendJournal,\n appendJournalBatch,\n bookExists,\n ensureBookDir,\n invalidateAllSnapshots,\n invalidateSnapshotsFrom,\n isSafeBookId,\n listJournalPeriods,\n periodFromDate,\n readAccounts,\n readConfig,\n readJournalMonth,\n removeBookDir,\n writeAccounts,\n writeConfig,\n} from \"./io.js\";\nimport { findActiveOpening, parseOpening } from \"./openingBalances.js\";\nimport { normalizeStoredAccount, parseAccountInput } from \"./accountNormalize.js\";\nimport { isValidCalendarDate, localDateString, makeEntry, makeVoidEntries, parseEntry, voidedIdSet, type ParsedEntry } from \"./journal.js\";\nimport { aggregateBalances, buildBalanceSheet, buildLedger, buildProfitLoss } from \"./report.js\";\nimport {\n bucketize,\n buildTimeSeries,\n TIME_SERIES_GRANULARITIES,\n TIME_SERIES_METRICS,\n type TimeSeriesGranularity,\n type TimeSeriesMetric,\n type TimeSeriesPoint,\n} from \"./timeSeries.js\";\nimport { awaitRebuildIdle, balancesAtEndOf, cancelRebuild, getOrBuildSnapshot, rebuildAllSnapshots, scheduleRebuild } from \"./snapshotCache.js\";\nimport { publishBookChange, publishBooksChanged } from \"./eventPublisher.js\";\nimport { DEFAULT_ACCOUNTS } from \"./defaultAccounts.js\";\nimport { log } from \"./context.js\";\nimport { BOOK_EVENT_KINDS as ACCOUNTING_BOOK_EVENT_KINDS } from \"../shared\";\nimport {\n isSupportedCountryCode,\n SUPPORTED_COUNTRY_CODES,\n type SupportedCountryCode,\n DEFAULT_FISCAL_YEAR_END,\n FISCAL_YEAR_END_MONTHS,\n isFiscalYearEnd,\n resolveFiscalYearEnd,\n type FiscalYearEnd,\n} from \"../shared\";\nimport type { AccountingConfig } from \"./types.js\";\nimport type { Account, AccountType, BookSummary, JournalEntry, ReportPeriod } from \"../shared/types.js\";\n\nexport class AccountingError extends Error {\n constructor(\n public status: number,\n message: string,\n public details?: unknown,\n ) {\n super(message);\n this.name = \"AccountingError\";\n }\n}\n\nconst DEFAULT_CURRENCY = \"USD\";\nconst GENERATED_ID_RETRIES = 8;\n\nfunction emptyConfig(): AccountingConfig {\n return { books: [] };\n}\n\nasync function loadOrInitConfig(workspaceRoot?: string): Promise<AccountingConfig> {\n const cfg = await readConfig(workspaceRoot);\n return cfg ?? emptyConfig();\n}\n\nfunction findBook(config: AccountingConfig, bookId: string): BookSummary | null {\n return config.books.find((book) => book.id === bookId) ?? null;\n}\n\nfunction resolveBookId(config: AccountingConfig, requested: string | undefined): string {\n // Every book-touching action now requires an explicit `bookId` —\n // there's no server-side \"active book\" to fall back on. Callers\n // are the LLM (which is told to pass bookId on each call) and the\n // View (which tracks the current selection in localStorage).\n if (!requested) {\n throw new AccountingError(400, \"bookId is required\");\n }\n if (!findBook(config, requested)) {\n throw new AccountingError(404, `book ${JSON.stringify(requested)} not found`);\n }\n return requested;\n}\n\nasync function generateBookId(config: AccountingConfig, workspaceRoot?: string): Promise<string> {\n // 8 hex chars × small N → collision odds are negligible, but a\n // bounded retry keeps the generator total even if one happens.\n for (let attempt = 0; attempt < GENERATED_ID_RETRIES; attempt += 1) {\n const candidate = `book-${randomUUID().slice(0, 8)}`;\n if (!findBook(config, candidate) && !(await bookExists(candidate, workspaceRoot))) return candidate;\n }\n throw new AccountingError(500, \"could not generate a unique book id after several attempts\");\n}\n\n/** Read every journal entry across every month, in period-sorted\n * order. Used by paths that need a full-history view (opening\n * balance lookups, P/L date filtering). */\nasync function readAllEntries(bookId: string, workspaceRoot?: string): Promise<JournalEntry[]> {\n const periods = await listJournalPeriods(bookId, workspaceRoot);\n const all: JournalEntry[] = [];\n for (const monthKey of periods) {\n const { entries, skipped } = await readJournalMonth(bookId, monthKey, workspaceRoot);\n for (const entry of entries) all.push(entry);\n if (skipped > 0) {\n // Aggregations and reports built from a partial parse are\n // misleading — log so an operator can spot a corrupted\n // jsonl file. Reads still proceed with what we could parse;\n // refusing here would lock the user out of the whole book\n // for a single bad line.\n log.warn(\"accounting\", \"journal month had unparseable lines\", { bookId, period: monthKey, skipped });\n }\n }\n return all;\n}\n\n// ── books ──────────────────────────────────────────────────────────\n\nexport async function listBooks(workspaceRoot?: string): Promise<{ books: BookSummary[] }> {\n const config = await loadOrInitConfig(workspaceRoot);\n return { books: config.books };\n}\n\nfunction unsupportedCountryError(received: unknown): AccountingError {\n return new AccountingError(400, `unsupported country code ${JSON.stringify(received)} — must be one of: ${SUPPORTED_COUNTRY_CODES.join(\", \")}`);\n}\n\nfunction unsupportedFiscalYearEndError(received: unknown): AccountingError {\n return new AccountingError(\n 400,\n `unsupported fiscalYearEnd ${JSON.stringify(received)} — must be a closing-month number ${FISCAL_YEAR_END_MONTHS.join(\", \")} (1 = January … 12 = December)`,\n );\n}\n\n/** Coerce + validate a free-form `fiscalYearEnd` from any ingress path\n * (REST body, MCP tool args, direct callers). The service is the\n * validation boundary, so this is deliberately tolerant of input SHAPE\n * but strict about the resulting value:\n * - absent / null / empty string → `undefined` (field omitted; the\n * caller decides default-vs-no-op);\n * - a number, or a numeric string (\"8\") from a hand-rolled client →\n * that month;\n * - a legacy calendar-quarter token (\"Q1\"..\"Q4\") from a stale client\n * → its closing month (same Q1→3 mapping the read side applies);\n * - anything else non-empty (a typo, garbage, an out-of-range or\n * non-integer number) → 400, echoing the ORIGINAL value so the\n * bad payload can't be silently mistaken for the default. */\nfunction coerceFiscalYearEndInput(raw: unknown): FiscalYearEnd | undefined {\n if (raw === undefined || raw === null || raw === \"\") return undefined;\n let month: unknown = raw;\n if (typeof raw === \"string\") {\n const trimmed = raw.trim();\n if (trimmed === \"\") return undefined;\n if (/^Q[1-4]$/.test(trimmed)) return resolveFiscalYearEnd(trimmed);\n month = /^-?\\d+$/.test(trimmed) ? Number(trimmed) : Number.NaN;\n }\n if (!isFiscalYearEnd(month)) throw unsupportedFiscalYearEndError(raw);\n return month;\n}\n\n/** Boundary checks for updateBook (name / country only — fiscalYearEnd is\n * coerced + validated separately via `coerceFiscalYearEndInput`). Throws on\n * the first failure so the surrounding function stays under the\n * cognitive-complexity threshold, and hands back the country to persist:\n * `undefined` = the field was omitted, `\"\"` = explicit clear. */\nfunction parseUpdateBookInput(input: { name?: string | undefined; country?: string | undefined }): SupportedCountryCode | \"\" | undefined {\n if (input.name !== undefined && (typeof input.name !== \"string\" || input.name.trim() === \"\")) {\n throw new AccountingError(400, \"name must be a non-empty string when supplied\");\n }\n const { country } = input;\n // Empty string is the explicit \"clear the field\" sentinel from the\n // settings UI; anything else has to land in the curated list, same\n // contract as createBook.\n if (country === undefined || country === \"\") return country;\n if (!isSupportedCountryCode(country)) {\n throw unsupportedCountryError(country);\n }\n return country;\n}\n\nexport async function createBook(\n input: { id?: string | undefined; name: string; currency?: string | undefined; country?: string | undefined; fiscalYearEnd?: unknown },\n workspaceRoot?: string,\n): Promise<{ book: BookSummary }> {\n if (typeof input.name !== \"string\" || input.name.trim() === \"\") {\n throw new AccountingError(400, \"name is required\");\n }\n // Country, when supplied, must be one of the curated codes — keeps\n // the UI dropdown, the role prompt's per-jurisdiction guidance, and\n // the on-disk JSON in sync. A typo from the LLM or an untrusted\n // client is rejected here rather than silently persisted.\n const { country } = input;\n if (country !== undefined && !isSupportedCountryCode(country)) {\n throw unsupportedCountryError(country);\n }\n const fiscalYearEnd = coerceFiscalYearEndInput(input.fiscalYearEnd) ?? DEFAULT_FISCAL_YEAR_END;\n const config = await loadOrInitConfig(workspaceRoot);\n // Auto-generate when no caller id is supplied — every book,\n // including the very first one, gets a generated id. Explicit\n // caller-supplied ids (from a custom config import or a CLI tool)\n // are kept verbatim so users with their own naming scheme can\n // adopt it.\n const bookId = input.id ?? (await generateBookId(config, workspaceRoot));\n // Guard against caller-supplied path-traversal ids before any\n // fs touch (createBook → ensureBookDir → writeAccounts →\n // writeConfig). Auto-generated ids always pass.\n if (!isSafeBookId(bookId)) {\n throw new AccountingError(400, `invalid book id ${JSON.stringify(bookId)} — allowed characters are A-Z a-z 0-9 _ - (1-64 chars; cannot start with _ or -)`);\n }\n if (findBook(config, bookId)) {\n throw new AccountingError(409, `book ${JSON.stringify(bookId)} already exists`);\n }\n if (await bookExists(bookId, workspaceRoot)) {\n throw new AccountingError(409, `book directory ${JSON.stringify(bookId)} already exists on disk`);\n }\n const book: BookSummary = {\n id: bookId,\n name: input.name,\n currency: input.currency ?? DEFAULT_CURRENCY,\n ...(country ? { country } : {}),\n fiscalYearEnd,\n createdAt: new Date().toISOString(),\n };\n await ensureBookDir(bookId, workspaceRoot);\n await writeAccounts(bookId, [...DEFAULT_ACCOUNTS], workspaceRoot);\n const nextConfig: AccountingConfig = { books: [...config.books, book] };\n await writeConfig(nextConfig, workspaceRoot);\n publishBooksChanged(workspaceRoot);\n return { book };\n}\n\nexport async function updateBook(\n input: { bookId: string; name?: string | undefined; country?: string | undefined; fiscalYearEnd?: unknown },\n workspaceRoot?: string,\n): Promise<{ book: BookSummary }> {\n const config = await loadOrInitConfig(workspaceRoot);\n const target = findBook(config, input.bookId);\n if (!target) {\n throw new AccountingError(404, `book ${JSON.stringify(input.bookId)} not found`);\n }\n const country = parseUpdateBookInput(input);\n // Coerce + validate up front so a malformed value 400s before any\n // write (undefined = the field was omitted → leave it untouched).\n const fiscalYearEnd = coerceFiscalYearEndInput(input.fiscalYearEnd);\n // Currency intentionally absent — once entries reference per-book\n // amounts, switching currency would silently re-interpret every\n // historical figure. Country / name / fiscalYearEnd are pure metadata;\n // safe to swap. Changing fiscalYearEnd does not move any entries —\n // it only changes how the date-range shortcuts resolve from now on.\n const next: BookSummary = {\n ...target,\n ...(input.name !== undefined ? { name: input.name } : {}),\n ...(country ? { country } : {}),\n ...(fiscalYearEnd !== undefined ? { fiscalYearEnd } : {}),\n };\n // Strip an explicitly-cleared country so the JSON file stays clean\n // (matches the createBook policy of omitting the field when unset).\n if (country === \"\") delete next.country;\n const nextConfig: AccountingConfig = {\n books: config.books.map((book) => (book.id === input.bookId ? next : book)),\n };\n await writeConfig(nextConfig, workspaceRoot);\n publishBooksChanged(workspaceRoot);\n return { book: next };\n}\n\nexport async function deleteBook(\n input: { bookId: string; confirm: boolean },\n workspaceRoot?: string,\n): Promise<{ deletedBookId: string; deletedBookName: string }> {\n if (!input.confirm) {\n throw new AccountingError(400, \"deleteBook requires confirm: true\");\n }\n const config = await loadOrInitConfig(workspaceRoot);\n const target = findBook(config, input.bookId);\n if (!target) {\n throw new AccountingError(404, `book ${JSON.stringify(input.bookId)} not found`);\n }\n // Stop any in-flight rebuild before removing the directory; otherwise\n // writeSnapshot could re-create the tree via mkdir-recursive after\n // we delete it, leaving an orphaned book folder on disk.\n cancelRebuild(input.bookId, workspaceRoot);\n await awaitRebuildIdle(input.bookId, workspaceRoot);\n await removeBookDir(input.bookId, workspaceRoot);\n const remaining = config.books.filter((book) => book.id !== input.bookId);\n await writeConfig({ books: remaining }, workspaceRoot);\n publishBooksChanged(workspaceRoot);\n // Capture the name BEFORE the splice so the LLM-facing message\n // can reference the human-readable book the user just deleted.\n return { deletedBookId: input.bookId, deletedBookName: target.name };\n}\n\n// ── accounts ───────────────────────────────────────────────────────\n\nexport async function listAccounts(input: { bookId?: string | undefined }, workspaceRoot?: string): Promise<{ bookId: string; accounts: Account[] }> {\n const config = await loadOrInitConfig(workspaceRoot);\n const bookId = resolveBookId(config, input.bookId);\n return { bookId, accounts: await readAccounts(bookId, workspaceRoot) };\n}\n\n/** Parse the caller's account, then apply the one rule the chart owns\n * rather than the record: codes starting with `_` are reserved for the\n * synthetic rows the report layer injects (e.g. `_currentEarnings` in\n * the Equity section). A user account there would either duplicate a\n * B/S row or hide a real account behind the synthetic label. */\nfunction parseUpsertAccount(raw: unknown): Account {\n const parsed = parseAccountInput(raw);\n if (!parsed.ok) throw new AccountingError(400, parsed.message);\n if (parsed.account.code.startsWith(\"_\")) {\n throw new AccountingError(\n 400,\n `account code ${JSON.stringify(parsed.account.code)} is reserved (codes starting with _ are used for synthetic report rows)`,\n );\n }\n return parsed.account;\n}\n\n/** Insert or replace the account in the chart. The whitelist +\n * active-flag policy lives in normalizeStoredAccount (see\n * ./accountNormalize.ts) so it stays unit-testable in isolation.\n * `previousType` is null for a new code — callers use it to decide\n * whether aggregation across periods just changed meaning. */\nfunction applyAccount(accounts: readonly Account[], account: Account): { next: Account[]; previousType: AccountType | null } {\n const existingIdx = accounts.findIndex((stored) => stored.code === account.code);\n const existing = existingIdx >= 0 ? accounts[existingIdx] : undefined;\n const stored = normalizeStoredAccount(account, existing);\n const next = [...accounts];\n if (existingIdx >= 0) next[existingIdx] = stored;\n else next.push(stored);\n // `existing ? … : null`, not `existing?.type ?? null`: a book written\n // before the account payload was parsed can hold a record whose `type`\n // is missing, and collapsing that into \"no previous type\" would skip\n // the invalidation below on the very upsert that repairs it.\n return { next, previousType: existing ? existing.type : null };\n}\n\nexport async function upsertAccount(\n input: { bookId?: string | undefined; account: unknown },\n workspaceRoot?: string,\n): Promise<{ bookId: string; account: Account; accounts: Account[] }> {\n const config = await loadOrInitConfig(workspaceRoot);\n const bookId = resolveBookId(config, input.bookId);\n const account = parseUpsertAccount(input.account);\n const { next, previousType } = applyAccount(await readAccounts(bookId, workspaceRoot), account);\n await writeAccounts(bookId, next, workspaceRoot);\n // Type changes affect aggregation across periods — drop every\n // snapshot to be safe. Pure name / note changes don't, but\n // distinguishing isn't worth the complexity.\n if (previousType !== null && previousType !== account.type) {\n scheduleRebuild(bookId, \"0000-00\", workspaceRoot);\n await invalidateAllSnapshots(bookId, workspaceRoot);\n }\n publishBookChange(bookId, { kind: ACCOUNTING_BOOK_EVENT_KINDS.accounts }, workspaceRoot);\n return { bookId, account, accounts: next };\n}\n\n// ── journal entries ────────────────────────────────────────────────\n\ninterface BatchValidationFailure {\n index: number;\n errors: unknown;\n}\n\ninterface BatchParseResult {\n failures: BatchValidationFailure[];\n items: ParsedEntry[];\n}\n\n// All-or-nothing parse: collect failures across every entry so the\n// whole batch can be rejected before any write touches disk (a\n// half-applied batch can never end up persisted). The parsed items\n// ride along so the build step gets a shape something checked.\nfunction parseBatchEntries(items: readonly unknown[], accounts: readonly Account[]): BatchParseResult {\n const failures: BatchValidationFailure[] = [];\n const parsed: ParsedEntry[] = [];\n items.forEach((item, index) => {\n const result = parseEntry(item, accounts);\n if (result.ok) parsed.push(result.entry);\n else failures.push({ index, errors: result.errors });\n });\n return { failures, items: parsed };\n}\n\nfunction buildBatchEntries(items: readonly ParsedEntry[]): JournalEntry[] {\n return items.map((item) => makeEntry({ date: item.date, lines: item.lines, memo: item.memo, kind: \"normal\", replacesEntryId: item.replacesEntryId }));\n}\n\n// Snapshot maintenance is driven from the earliest period in the\n// batch — invalidating from that point covers every later month a\n// single-entry call would have invalidated individually, while\n// collapsing the rebuild + publish work into one round.\n// Seedless `reduce` is safe here: the sole caller (addEntries) throws on an\n// empty batch, and parseBatchEntries returns one item per input entry.\nfunction earliestPeriodOf(entries: readonly JournalEntry[]): string {\n return entries.map((entry) => periodFromDate(entry.date)).reduce((min, period) => (period < min ? period : min));\n}\n\nexport async function addEntries(\n input: { bookId?: string | undefined; entries: unknown },\n workspaceRoot?: string,\n): Promise<{ bookId: string; entries: JournalEntry[] }> {\n const config = await loadOrInitConfig(workspaceRoot);\n const bookId = resolveBookId(config, input.bookId);\n if (!isUnknownArray(input.entries) || input.entries.length === 0) {\n throw new AccountingError(400, \"addEntries: entries must be a non-empty array\");\n }\n const accounts = await readAccounts(bookId, workspaceRoot);\n const { failures, items } = parseBatchEntries(input.entries, accounts);\n if (failures.length > 0) throw new AccountingError(400, \"invalid journal entries\", failures);\n const built = buildBatchEntries(items);\n // Two-phase batched write: stage every affected month's full new\n // content, then commit all renames at the end. Same-period\n // batches are fully atomic; multi-period failure window is\n // narrowed to the rename phase only.\n await appendJournalBatch(bookId, built, workspaceRoot);\n const earliestPeriod = earliestPeriodOf(built);\n // scheduleRebuild first (sync, sets pendingFromPeriod) so any\n // in-flight rebuild's `isInvalidatedDuringRebuild` check sees the\n // new pending mark before our invalidate races with its write.\n scheduleRebuild(bookId, earliestPeriod, workspaceRoot);\n await invalidateSnapshotsFrom(bookId, earliestPeriod, workspaceRoot);\n publishBookChange(bookId, { kind: ACCOUNTING_BOOK_EVENT_KINDS.journal, period: earliestPeriod }, workspaceRoot);\n return { bookId, entries: built };\n}\n\nasync function findEntryById(bookId: string, entryId: string, workspaceRoot?: string): Promise<JournalEntry | null> {\n const periods = await listJournalPeriods(bookId, workspaceRoot);\n for (const monthKey of periods) {\n const { entries } = await readJournalMonth(bookId, monthKey, workspaceRoot);\n const hit = entries.find((entry) => entry.id === entryId);\n if (hit) return hit;\n }\n return null;\n}\n\nexport async function voidEntry(\n input: { bookId?: string | undefined; entryId: string; reason?: string | undefined; voidDate?: string | undefined },\n workspaceRoot?: string,\n): Promise<{ bookId: string; reverseEntry: JournalEntry; markerEntry: JournalEntry }> {\n const config = await loadOrInitConfig(workspaceRoot);\n const bookId = resolveBookId(config, input.bookId);\n const target = await findEntryById(bookId, input.entryId, workspaceRoot);\n if (!target) {\n throw new AccountingError(404, `entry ${JSON.stringify(input.entryId)} not found`);\n }\n const voidDate = input.voidDate ?? localDateString();\n const { reverse, marker } = makeVoidEntries(target, input.reason, voidDate);\n await appendJournal(bookId, reverse, workspaceRoot);\n await appendJournal(bookId, marker, workspaceRoot);\n // Period whose snapshot is now stale = the older of the\n // original entry's month and the void's month.\n const fromPeriod = target.date < voidDate ? periodFromDate(target.date) : periodFromDate(voidDate);\n scheduleRebuild(bookId, fromPeriod, workspaceRoot);\n await invalidateSnapshotsFrom(bookId, fromPeriod, workspaceRoot);\n publishBookChange(bookId, { kind: ACCOUNTING_BOOK_EVENT_KINDS.journal, period: fromPeriod }, workspaceRoot);\n return { bookId, reverseEntry: reverse, markerEntry: marker };\n}\n\ninterface ListEntriesInput {\n bookId?: string | undefined;\n from?: string | undefined;\n to?: string | undefined;\n accountCode?: string | undefined;\n}\n\nfunction entryMatchesFilters(entry: JournalEntry, input: ListEntriesInput): boolean {\n if (input.from && entry.date < input.from) return false;\n if (input.to && entry.date > input.to) return false;\n if (input.accountCode && !entry.lines.some((line) => line.accountCode === input.accountCode)) return false;\n return true;\n}\n\nexport async function listEntries(\n input: ListEntriesInput,\n workspaceRoot?: string,\n): Promise<{ bookId: string; entries: JournalEntry[]; voidedEntryIds: string[] }> {\n const config = await loadOrInitConfig(workspaceRoot);\n const bookId = resolveBookId(config, input.bookId);\n const periods = await listJournalPeriods(bookId, workspaceRoot);\n const entries: JournalEntry[] = [];\n // Collect voided ids from the *unfiltered* set across every month —\n // an account-filtered query drops void-marker rows (they have no\n // lines), so deriving voided ids from the filtered list misses\n // them and the View loses the strikeout on the cancelled original.\n const allVoidedIds = new Set<string>();\n for (const monthKey of periods) {\n const { entries: monthEntries } = await readJournalMonth(bookId, monthKey, workspaceRoot);\n for (const voidedId of voidedIdSet(monthEntries)) allVoidedIds.add(voidedId);\n if (input.from && monthKey < input.from.slice(0, 7)) continue;\n if (input.to && monthKey > input.to.slice(0, 7)) continue;\n for (const entry of monthEntries) {\n if (entryMatchesFilters(entry, input)) entries.push(entry);\n }\n }\n return { bookId, entries, voidedEntryIds: Array.from(allVoidedIds).sort() };\n}\n\n// ── opening balances ───────────────────────────────────────────────\n\nexport async function getOpeningBalances(\n input: { bookId?: string | undefined },\n workspaceRoot?: string,\n): Promise<{ bookId: string; opening: JournalEntry | null }> {\n const config = await loadOrInitConfig(workspaceRoot);\n const bookId = resolveBookId(config, input.bookId);\n const all = await readAllEntries(bookId, workspaceRoot);\n return { bookId, opening: findActiveOpening(all) };\n}\n\nexport async function setOpeningBalances(\n input: { bookId?: string | undefined; asOfDate: string; lines: unknown; memo?: string | undefined },\n workspaceRoot?: string,\n): Promise<{ bookId: string; openingEntry: JournalEntry; replacedExisting: boolean }> {\n const config = await loadOrInitConfig(workspaceRoot);\n const bookId = resolveBookId(config, input.bookId);\n const accounts = await readAccounts(bookId, workspaceRoot);\n const all = await readAllEntries(bookId, workspaceRoot);\n const parsed = parseOpening({\n asOfDate: input.asOfDate,\n lines: input.lines,\n accounts,\n existingEntries: all,\n });\n if (!parsed.ok) {\n throw new AccountingError(400, \"invalid opening balances\", parsed.errors);\n }\n // Replace-mode: void any existing active opening so the new one\n // is unambiguous. The marker is dated today (when the void\n // happened), not the original opening date.\n const existing = findActiveOpening(all);\n if (existing) {\n const today = localDateString();\n const { reverse, marker } = makeVoidEntries(existing, \"replaced via setOpeningBalances\", today);\n await appendJournal(bookId, reverse, workspaceRoot);\n await appendJournal(bookId, marker, workspaceRoot);\n }\n const opening = makeEntry({\n date: input.asOfDate,\n lines: parsed.lines,\n memo: input.memo ?? \"Opening balances\",\n kind: \"opening\",\n });\n await appendJournal(bookId, opening, workspaceRoot);\n scheduleRebuild(bookId, \"0000-00\", workspaceRoot);\n await invalidateAllSnapshots(bookId, workspaceRoot);\n publishBookChange(bookId, { kind: ACCOUNTING_BOOK_EVENT_KINDS.opening }, workspaceRoot);\n return { bookId, openingEntry: opening, replacedExisting: existing !== null };\n}\n\n// ── reports ────────────────────────────────────────────────────────\n\nfunction endDateOfPeriod(period: ReportPeriod): string {\n if (period.kind === \"month\") {\n const [year = Number.NaN, month = Number.NaN] = period.period.split(\"-\").map((segment) => parseInt(segment, 10));\n const last = new Date(Date.UTC(year, month, 0)).getUTCDate();\n return `${period.period}-${String(last).padStart(2, \"0\")}`;\n }\n return period.to;\n}\n\nexport async function getBalanceSheetReport(\n input: { bookId?: string | undefined; period: ReportPeriod },\n workspaceRoot?: string,\n): Promise<{ bookId: string; balanceSheet: ReturnType<typeof buildBalanceSheet> }> {\n const config = await loadOrInitConfig(workspaceRoot);\n const bookId = resolveBookId(config, input.bookId);\n const accounts = await readAccounts(bookId, workspaceRoot);\n const balances = await balancesAsOf(bookId, input.period, workspaceRoot);\n return {\n bookId,\n balanceSheet: buildBalanceSheet({\n accounts,\n balances,\n asOf: endDateOfPeriod(input.period),\n }),\n };\n}\n\n/** Resolve closing balances at the end of a `ReportPeriod`. Month\n * periods hit the snapshot cache; range periods with a mid-month\n * `to` date have to filter the journal directly because the\n * end-of-month snapshot would include activity past `to`. */\nasync function balancesAsOf(bookId: string, period: ReportPeriod, workspaceRoot?: string): Promise<ReturnType<typeof aggregateBalances>> {\n if (period.kind === \"month\") {\n const snap = await getOrBuildSnapshot(bookId, period.period, workspaceRoot);\n return [...snap.balances];\n }\n const all = await readAllEntries(bookId, workspaceRoot);\n const filtered = all.filter((entry) => entry.date <= period.to);\n return aggregateBalances(filtered);\n}\n\nexport async function getProfitLossReport(\n input: { bookId?: string | undefined; period: ReportPeriod },\n workspaceRoot?: string,\n): Promise<{ bookId: string; profitLoss: ReturnType<typeof buildProfitLoss> }> {\n const config = await loadOrInitConfig(workspaceRoot);\n const bookId = resolveBookId(config, input.bookId);\n const accounts = await readAccounts(bookId, workspaceRoot);\n const all = await readAllEntries(bookId, workspaceRoot);\n const fromDate = input.period.kind === \"month\" ? `${input.period.period}-01` : input.period.from;\n const toDate = endDateOfPeriod(input.period);\n return { bookId, profitLoss: buildProfitLoss({ accounts, entries: all, from: fromDate, to: toDate }) };\n}\n\nexport async function getLedgerReport(\n input: { bookId?: string | undefined; accountCode: string; period?: ReportPeriod | undefined },\n workspaceRoot?: string,\n): Promise<{ bookId: string; ledger: ReturnType<typeof buildLedger> }> {\n const config = await loadOrInitConfig(workspaceRoot);\n const bookId = resolveBookId(config, input.bookId);\n const accounts = await readAccounts(bookId, workspaceRoot);\n const account = accounts.find((acct) => acct.code === input.accountCode);\n if (!account) {\n throw new AccountingError(404, `account ${JSON.stringify(input.accountCode)} not found`);\n }\n const all = await readAllEntries(bookId, workspaceRoot);\n const fromDate = input.period?.kind === \"month\" ? `${input.period.period}-01` : input.period?.from;\n const toDate = input.period ? endDateOfPeriod(input.period) : undefined;\n return { bookId, ledger: buildLedger({ account, entries: all, from: fromDate, to: toDate }) };\n}\n\n// ── time series ────────────────────────────────────────────────────\n\nfunction ensureValidYmd(label: string, value: unknown): string {\n // Reuse the journal-side helper so impossible days (`2025-02-30`,\n // `2025-13-01`) AND silent year drift (`0099-01-01` → 1999 via\n // `Date.UTC` legacy two-digit handling) are both rejected — rolling\n // a separate regex+lastDay check here missed the latter.\n if (typeof value !== \"string\" || !isValidCalendarDate(value)) {\n throw new AccountingError(400, `getTimeSeries: ${label} must be a valid YYYY-MM-DD calendar date`);\n }\n return value;\n}\n\nfunction ensureMetric(value: unknown): TimeSeriesMetric {\n const metric = TIME_SERIES_METRICS.find((candidate) => candidate === value);\n if (metric === undefined) {\n throw new AccountingError(400, `getTimeSeries: metric must be one of ${TIME_SERIES_METRICS.join(\", \")}`);\n }\n return metric;\n}\n\nfunction ensureGranularity(value: unknown): TimeSeriesGranularity {\n const granularity = TIME_SERIES_GRANULARITIES.find((candidate) => candidate === value);\n if (granularity === undefined) {\n throw new AccountingError(400, `getTimeSeries: granularity must be one of ${TIME_SERIES_GRANULARITIES.join(\", \")}`);\n }\n return granularity;\n}\n\nfunction resolveAccountCode(metric: TimeSeriesMetric, raw: unknown): string | undefined {\n if (metric === \"accountBalance\") {\n if (typeof raw !== \"string\" || raw === \"\") {\n throw new AccountingError(400, \"getTimeSeries: accountCode is required when metric is accountBalance\");\n }\n return raw;\n }\n if (raw !== undefined && raw !== \"\") {\n throw new AccountingError(400, \"getTimeSeries: accountCode is only allowed when metric is accountBalance\");\n }\n return undefined;\n}\n\nexport interface TimeSeriesReportInput {\n bookId?: string | undefined;\n metric: unknown;\n granularity: unknown;\n from: unknown;\n to: unknown;\n accountCode?: unknown;\n}\n\nexport interface TimeSeriesReport {\n bookId: string;\n metric: TimeSeriesMetric;\n granularity: TimeSeriesGranularity;\n from: string;\n to: string;\n accountCode?: string;\n points: TimeSeriesPoint[];\n}\n\ninterface ValidatedTimeSeriesInput {\n metric: TimeSeriesMetric;\n granularity: TimeSeriesGranularity;\n from: string;\n toDate: string;\n accountCode: string | undefined;\n}\n\nfunction validateTimeSeriesInput(input: TimeSeriesReportInput): ValidatedTimeSeriesInput {\n const metric = ensureMetric(input.metric);\n const granularity = ensureGranularity(input.granularity);\n const from = ensureValidYmd(\"from\", input.from);\n const toDate = ensureValidYmd(\"to\", input.to);\n if (from > toDate) throw new AccountingError(400, \"getTimeSeries: from must be on or before to\");\n const accountCode = resolveAccountCode(metric, input.accountCode);\n return { metric, granularity, from, toDate, accountCode };\n}\n\ninterface TimeSeriesBookContext {\n bookId: string;\n fiscalYearEnd: FiscalYearEnd;\n accounts: Account[];\n}\n\nasync function loadTimeSeriesBookContext(requestedBookId: string | undefined, workspaceRoot?: string): Promise<TimeSeriesBookContext> {\n const config = await loadOrInitConfig(workspaceRoot);\n const bookId = resolveBookId(config, requestedBookId);\n const book = findBook(config, bookId);\n // resolveBookId guarantees the book exists; resolveFiscalYearEnd\n // covers both the type-checker fallback and any legacy token that\n // slipped past the read-side normalisation.\n const fiscalYearEnd: FiscalYearEnd = resolveFiscalYearEnd(book?.fiscalYearEnd);\n const accounts = await readAccounts(bookId, workspaceRoot);\n return { bookId, fiscalYearEnd, accounts };\n}\n\nexport async function getTimeSeriesReport(input: TimeSeriesReportInput, workspaceRoot?: string): Promise<TimeSeriesReport> {\n const { metric, granularity, from, toDate, accountCode } = validateTimeSeriesInput(input);\n const { bookId, fiscalYearEnd, accounts } = await loadTimeSeriesBookContext(input.bookId, workspaceRoot);\n if (accountCode && !accounts.some((acct) => acct.code === accountCode)) {\n throw new AccountingError(404, `getTimeSeries: account ${JSON.stringify(accountCode)} not found`);\n }\n const entries = await readAllEntries(bookId, workspaceRoot);\n const buckets = bucketize({ from, to: toDate, granularity, fiscalYearEnd });\n const points = buildTimeSeries({ buckets, entries, accounts, metric, accountCode });\n const report: TimeSeriesReport = { bookId, metric, granularity, from, to: toDate, points };\n if (accountCode) report.accountCode = accountCode;\n return report;\n}\n\n// ── snapshot admin ─────────────────────────────────────────────────\n\nexport async function rebuildSnapshots(input: { bookId?: string | undefined }, workspaceRoot?: string): Promise<{ bookId: string; rebuilt: string[] }> {\n const config = await loadOrInitConfig(workspaceRoot);\n const bookId = resolveBookId(config, input.bookId);\n const result = await rebuildAllSnapshots(bookId, workspaceRoot);\n publishBookChange(bookId, { kind: ACCOUNTING_BOOK_EVENT_KINDS.snapshotsReady }, workspaceRoot);\n return { bookId, rebuilt: result.rebuilt };\n}\n\n// Direct access for tests / lazy paths that want to bypass the\n// snapshot cache.\nexport { aggregateBalances, balancesAtEndOf };\n","// Tiny Express helper owned by the package so the router is\n// self-contained (no host util imports). `asyncHandler` turns an\n// uncaught throw inside an async handler into a logged 500 carrying\n// only the caller-supplied fallback message — never the raw error text\n// (which could leak internals).\n//\n// Deliberately NOT shared with the host's server/utils/asyncHandler.ts:\n// a plugin importing host code is an uphill import, forbidden by the\n// package-dependency-direction rule. This copy is also scoped to the\n// package logger and diverges — it forwards to Express `next(err)` on\n// `headersSent` (the host copy doesn't). Do not \"de-dupe\" it by reaching\n// into the host; lift it into @mulmoclaude/core if it ever must be shared.\n\nimport type { Request, Response, NextFunction } from \"express\";\nimport { log } from \"./context.js\";\nimport { errorMessage } from \"../shared/errors.js\";\n\n/** The body the catch path sends. Declared here rather than imported from the\n * host's `httpError.ts` for the same reason the wrapper is copied: reaching\n * into the host would be an uphill import. */\nexport interface ErrorBody {\n error: string;\n}\n\n/** A route response that can carry either its success body or an error — use\n * this, not a bare `Response<T>`, for any handler wrapped by `asyncHandler`,\n * which can always emit `ErrorBody` on the failure path. */\nexport type ApiResponse<T> = Response<T | ErrorBody>;\n\n/** The bounds below name exactly what the catch path dereferences.\n *\n * They are NOT `extends Request` / `extends Response`: Express uses its type\n * parameters in mixed variance positions, so a nominal bound rejects valid\n * call sites like `Request<object, unknown, MyBody>`. Naming only the members\n * we touch keeps every concrete `Request<…>` / `Response<…>` assignable.\n *\n * Structural bounds rather than unconstrained generics plus `as` casts,\n * because the casts were load-bearing in a way that hid a real contract: this\n * wrapper sends `{ error }` on the failure path, so a route declaring a\n * `ResBody` that cannot carry it was lying. That is now a compile error, and\n * the fix at such a site is `ApiResponse<T>`. */\ninterface RoutePathBearing {\n path: string;\n}\n\ninterface ErrorSendableResponse {\n headersSent: boolean;\n status: (code: number) => { json: (body: ErrorBody) => unknown };\n}\n\nexport function asyncHandler<TReq extends RoutePathBearing = Request, TRes extends ErrorSendableResponse = Response>(\n namespace: string,\n fallbackMessage: string,\n handler: (req: TReq, res: TRes) => Promise<void>,\n): (req: TReq, res: TRes, next: NextFunction) => Promise<void> {\n return async (req, res, next) => {\n try {\n await handler(req, res);\n } catch (err) {\n log.error(namespace, \"handler threw\", { route: req.path, error: errorMessage(err) });\n if (res.headersSent) {\n // Response already (partially) sent — we can't write a clean 500.\n // Forward to Express's error flow so it can destroy the socket\n // rather than leaving the request hanging.\n next(err);\n return;\n }\n res.status(500).json({ error: fallbackMessage });\n }\n };\n}\n","// REST endpoint for the accounting plugin. Single POST dispatch\n// route with an `action` discriminator — matches the todos /\n// scheduler convention so the LLM-facing MCP bridge (which invokes\n// `apiPost` with the tool args verbatim) plugs in without\n// translation.\n//\n// The mounted `<AccountingApp>` View hits this same endpoint\n// directly for tab switches, filter changes, and form submits — no\n// LLM round trip per click. The MCP bridge calls into the same\n// service layer, so manual clicks and Claude tool calls produce\n// identical state changes.\n\nimport { Router, Request, Response } from \"express\";\nimport { isRecord, isUnknownArray } from \"@mulmoclaude/common\";\n\nimport { describeEntry, optionalRecord, optionalReportPeriod, optionalString } from \"./bodyFields.js\";\n\nimport {\n AccountingError,\n addEntries,\n createBook,\n updateBook,\n deleteBook,\n getBalanceSheetReport,\n getLedgerReport,\n getOpeningBalances,\n getProfitLossReport,\n getTimeSeriesReport,\n listAccounts,\n listBooks,\n listEntries,\n rebuildSnapshots,\n setOpeningBalances,\n upsertAccount,\n voidEntry,\n} from \"./service.js\";\nimport type { BookSummary } from \"../shared/types.js\";\nimport { ACCOUNTING_ACTIONS, ACCOUNTING_API } from \"../shared\";\nimport { channelScopeFor, isProjectScopedHost, log } from \"./context.js\";\nimport { asyncHandler, type ErrorBody } from \"./http.js\";\n\nexport interface AccountingActionBody {\n action: string;\n [key: string]: unknown;\n}\n\n/** `ErrorBody` plus the structured detail this router attaches to a failed\n * dispatch (see the `err.details` send below) — a superset, not a rival\n * declaration of the same shape. */\ninterface AccountingErrorResponse extends ErrorBody {\n details?: unknown;\n}\n\n// Tool-result envelope for the MCP-driven `openBook` action. The\n// frontend tool-result renderer keys off `kind: \"accounting-app\"`\n// to mount `<AccountingApp>` (vs the compact `Preview.vue` which\n// renders summaries for every other action). `message` is picked\n// up by the MCP bridge and surfaced as the tool's text response\n// to the LLM (server/agent/mcp-server.ts).\ninterface OpenBookToolResult {\n kind: \"accounting-app\";\n bookId: string;\n initialTab?: string | undefined;\n /** Same shape getBooks returns — included so an LLM that calls\n * openBook doesn't need a follow-up getBooks round-trip to learn\n * what other books exist before its next action. */\n books: BookSummary[];\n /** The host's OPAQUE scope id for the root this book lives under, so\n * the mounted card fetches ITS OWN project rather than whatever the\n * host has selected when it renders.\n *\n * `null` is a real answer on a project-scoped host — the default root\n * — and is spelled out rather than omitted, because an ABSENT field\n * means \"resolve the host's active project at mount\" and a host that\n * switches project between the tool result and the render would then\n * point a default-root card at the new one. The field is absent only\n * on a host that declared no scoping at all, which keeps a\n * single-root envelope byte-identical to what it has always been.\n *\n * Stamped by the SERVER from the resolved root — it is never read\n * off the request body, so the model cannot pick a project. */\n scope?: string | null;\n}\n\ntype ActionRest = Omit<AccountingActionBody, \"action\">;\n/** `root` is the workspace root this request resolved to — `undefined`\n * means \"the host's configured one\", the single-root path. Every\n * handler MUST pass it on to the service layer: under a multi-root\n * host a dropped root is not an error but a silent read or write\n * against the wrong project. */\ntype ActionHandler = (rest: ActionRest, root: string | undefined) => Promise<unknown>;\n\n// Each action is a tiny adapter that pulls the typed slice it needs\n// out of the loosely-typed body. Parsing of the slice shape itself\n// lives inside the service layer (parseEntry, parseOpening,\n// parseAccountInput) so the adapters can stay one-liners.\n\nasync function handleOpenBook(rest: ActionRest, root: string | undefined): Promise<OpenBookToolResult> {\n // openBook requires an explicit `bookId` that resolves to an\n // existing book. On a fresh workspace the LLM is expected to\n // call `createBook` first and then `openBook` with the new id.\n if (typeof rest.bookId !== \"string\" || rest.bookId === \"\") {\n throw new AccountingError(400, \"openBook: bookId is required. Call 'getBooks' to enumerate, or 'createBook' first on a fresh workspace.\");\n }\n const list = await listBooks(root);\n if (!list.books.some((book) => book.id === rest.bookId)) {\n throw new AccountingError(404, `openBook: book ${JSON.stringify(rest.bookId)} not found`);\n }\n const initialTab = typeof rest.initialTab === \"string\" ? rest.initialTab : undefined;\n const scopeField = isProjectScopedHost() ? { scope: channelScopeFor(root) } : {};\n return { kind: \"accounting-app\", bookId: rest.bookId, initialTab, books: list.books, ...scopeField };\n}\n\nasync function handleGetReport(rest: ActionRest, root: string | undefined): Promise<unknown> {\n const kind = typeof rest.kind === \"string\" ? rest.kind : \"\";\n const periodInput = optionalReportPeriod(rest.period);\n const bookId = optionalString(rest.bookId);\n // A period that is present but malformed reads as absent, so it\n // lands on this same message rather than reaching the report\n // builders as `${undefined}-01`. Spell the accepted shapes out —\n // the LLM repairs its own payload from this text.\n const periodRequired = (label: string) =>\n new AccountingError(400, `${label}: period is required — { kind: \"month\", period: \"YYYY-MM\" } or { kind: \"range\", from: \"YYYY-MM-DD\", to: \"YYYY-MM-DD\" }`);\n // `ledger` treats an absent period as \"full history\", which would turn a\n // period the caller DID send but got wrong into a silently wider answer\n // than they asked for. Sent-but-unusable is an error for every kind (#2765).\n // `null` counts as NOT sent: it is how a JSON caller spells \"I am not using\n // this optional field\", and rejecting it would break ledger calls that work\n // today.\n const periodSent = rest.period !== undefined && rest.period !== null;\n if (periodSent && !periodInput) throw periodRequired(`getReport ${kind || \"(no kind)\"}`);\n if (kind === \"balance\") {\n if (!periodInput) throw periodRequired(\"getReport balance\");\n return getBalanceSheetReport({ bookId, period: periodInput }, root);\n }\n if (kind === \"pl\") {\n if (!periodInput) throw periodRequired(\"getReport pl\");\n return getProfitLossReport({ bookId, period: periodInput }, root);\n }\n if (kind === \"ledger\") {\n // period is optional for ledger — full-history view from the\n // UI calls getLedger(accountCode, undefined, bookId). The\n // account code, however, is mandatory; without it the request\n // is meaningless and the service would 404 on a blank code.\n if (typeof rest.accountCode !== \"string\" || rest.accountCode === \"\") {\n throw new AccountingError(400, \"getReport ledger: accountCode is required\");\n }\n return getLedgerReport({ bookId, accountCode: rest.accountCode, period: periodInput }, root);\n }\n throw new AccountingError(400, `getReport: unknown kind ${JSON.stringify(kind)}`);\n}\n\nconst ACTION_HANDLERS: Record<string, ActionHandler> = {\n [ACCOUNTING_ACTIONS.openBook]: handleOpenBook,\n [ACCOUNTING_ACTIONS.getBooks]: (_rest, root) => listBooks(root),\n [ACCOUNTING_ACTIONS.createBook]: async (rest, root) => {\n // Surface bookId at the top level so the dispatch envelope's\n // `data` carries it like every other write action — the View\n // uses it to preselect the new book on mount.\n const result = await createBook(\n {\n name: typeof rest.name === \"string\" ? rest.name : \"\",\n currency: typeof rest.currency === \"string\" ? rest.currency : undefined,\n country: typeof rest.country === \"string\" ? rest.country : undefined,\n // Passed through raw — the service coerces + validates it (number,\n // numeric string, or a legacy \"Q1\"..\"Q4\" token), 400ing garbage.\n fiscalYearEnd: rest.fiscalYearEnd,\n },\n root,\n );\n return { bookId: result.book.id, ...result };\n },\n [ACCOUNTING_ACTIONS.updateBook]: async (rest, root) => {\n const result = await updateBook(\n {\n bookId: typeof rest.bookId === \"string\" ? rest.bookId : \"\",\n name: typeof rest.name === \"string\" ? rest.name : undefined,\n country: typeof rest.country === \"string\" ? rest.country : undefined,\n // Passed through raw — the service coerces + validates it.\n fiscalYearEnd: rest.fiscalYearEnd,\n },\n root,\n );\n return { bookId: result.book.id, ...result };\n },\n [ACCOUNTING_ACTIONS.deleteBook]: (rest, root) =>\n deleteBook({ bookId: typeof rest.bookId === \"string\" ? rest.bookId : \"\", confirm: rest.confirm === true }, root),\n [ACCOUNTING_ACTIONS.getAccounts]: (rest, root) => listAccounts({ bookId: optionalString(rest.bookId) }, root),\n // `account` / `entries` / `lines` travel to the service as `unknown`:\n // the parsers there own the narrowing, so a bad field keeps its own\n // message (\"debit must be a non-negative finite number\") instead of\n // collapsing into a generic shape error the LLM can't repair from.\n [ACCOUNTING_ACTIONS.upsertAccount]: (rest, root) => upsertAccount({ bookId: optionalString(rest.bookId), account: rest.account }, root),\n [ACCOUNTING_ACTIONS.addEntries]: (rest, root) => addEntries({ bookId: optionalString(rest.bookId), entries: rest.entries }, root),\n [ACCOUNTING_ACTIONS.voidEntry]: (rest, root) =>\n voidEntry(\n {\n bookId: optionalString(rest.bookId),\n entryId: typeof rest.entryId === \"string\" ? rest.entryId : \"\",\n reason: optionalString(rest.reason),\n voidDate: optionalString(rest.voidDate),\n },\n root,\n ),\n [ACCOUNTING_ACTIONS.getJournalEntries]: (rest, root) =>\n listEntries(\n {\n bookId: optionalString(rest.bookId),\n from: optionalString(rest.from),\n to: optionalString(rest.to),\n accountCode: optionalString(rest.accountCode),\n },\n root,\n ),\n [ACCOUNTING_ACTIONS.getOpeningBalances]: (rest, root) => getOpeningBalances({ bookId: optionalString(rest.bookId) }, root),\n [ACCOUNTING_ACTIONS.setOpeningBalances]: (rest, root) =>\n setOpeningBalances(\n {\n bookId: optionalString(rest.bookId),\n asOfDate: typeof rest.asOfDate === \"string\" ? rest.asOfDate : \"\",\n // Omitted `lines` means the zero-line opening marker that unlocks\n // the View's gate, so it stays an empty array rather than a 400.\n lines: rest.lines ?? [],\n memo: optionalString(rest.memo),\n },\n root,\n ),\n [ACCOUNTING_ACTIONS.getReport]: handleGetReport,\n [ACCOUNTING_ACTIONS.getTimeSeries]: (rest, root) =>\n getTimeSeriesReport(\n {\n bookId: optionalString(rest.bookId),\n metric: rest.metric,\n granularity: rest.granularity,\n from: rest.from,\n to: rest.to,\n accountCode: rest.accountCode,\n },\n root,\n ),\n [ACCOUNTING_ACTIONS.rebuildSnapshots]: (rest, root) => rebuildSnapshots({ bookId: optionalString(rest.bookId) }, root),\n};\n\n// Actions whose tool-result envelope should carry a `data` field so\n// the sidebar renders a preview card. Everything else returns\n// without `data` and the host gates the preview off (silent action).\n// Reads (lists / reports) and View-driven maintenance ops stay\n// silent — they're invoked from inside the canvas and the LLM will\n// summarise reads in its text reply anyway.\nconst PREVIEW_ACTIONS = new Set<string>([\n ACCOUNTING_ACTIONS.openBook,\n ACCOUNTING_ACTIONS.createBook,\n ACCOUNTING_ACTIONS.updateBook,\n ACCOUNTING_ACTIONS.upsertAccount,\n ACCOUNTING_ACTIONS.addEntries,\n ACCOUNTING_ACTIONS.voidEntry,\n ACCOUNTING_ACTIONS.setOpeningBalances,\n]);\n\n// LLM-facing `message` tacked onto YES actions. The shared trailer\n// (\"The accounting view is shown to the user.\") tells the LLM that a\n// canvas / sidebar surface is already visible, so its text reply\n// shouldn't redundantly enumerate the result the user can see — it\n// should narrate what was *done*, not re-list what's on screen.\nconst VIEW_VISIBLE_TRAILER = \"The accounting view is shown to the user.\";\n\ntype MessageBuilder = (fields: Record<string, unknown>) => string;\n\nconst MESSAGE_BUILDERS: Record<string, MessageBuilder> = {\n [ACCOUNTING_ACTIONS.openBook]: (fields) => {\n // Include the books list inline so the LLM doesn't need a\n // follow-up getBooks round-trip before deciding what to do\n // next.\n const { books, bookId } = fields;\n const booksFragment = Array.isArray(books) ? ` Books available: ${JSON.stringify(books)}.` : \"\";\n const idFragment = typeof bookId === \"string\" ? ` (book id: ${bookId})` : \"\";\n return `Mounted the accounting app in the canvas${idFragment}.${booksFragment}`;\n },\n [ACCOUNTING_ACTIONS.createBook]: (fields) => {\n const book = optionalRecord(fields.book);\n const name = optionalString(book?.name);\n const bookId = optionalString(book?.id);\n const subject = name ? `A new book named ${JSON.stringify(name)}` : \"A new book\";\n // The LLM needs book.id to call any follow-up action on this\n // book (getAccounts, addEntries, etc.), so include it in the\n // status message instead of forcing a round-trip via getBooks.\n const idFragment = bookId ? ` (id: ${bookId})` : \"\";\n // The View's opening-gate hides every tab except `opening` and\n // `settings` until an opening entry is on file (even a zero-line\n // one). If the agent doesn't tell the user to set opening\n // balances first, the user's \"can I add an entry?\" attempt\n // silently fails because the New Entry tab isn't even visible.\n // Include the next-step instruction inline so the agent's reply\n // matches the UI's actual constraints.\n return `${subject} has been created${idFragment}. Next required step: set opening balances via setOpeningBalances — the journal-entry, ledger, and report tabs are locked until an opening (even an empty one) is saved.`;\n },\n [ACCOUNTING_ACTIONS.upsertAccount]: (fields) => {\n const account = optionalRecord(fields.account);\n const code = optionalString(account?.code);\n const name = optionalString(account?.name);\n if (code && name) {\n return `Upserted account ${code} ${JSON.stringify(name)}.`;\n }\n return \"Updated the chart of accounts.\";\n },\n [ACCOUNTING_ACTIONS.addEntries]: (fields) => {\n const entries = (isUnknownArray(fields.entries) ? fields.entries : []).map(describeEntry);\n const [firstEntry, ...furtherEntries] = entries;\n if (!firstEntry) return \"Posted 0 journal entries.\";\n if (furtherEntries.length === 0) {\n const idFragment = firstEntry.id ? ` (id: ${firstEntry.id})` : \"\";\n return `Posted a journal entry on ${firstEntry.date ?? \"the requested date\"}${idFragment}.`;\n }\n // Surface every id so the LLM can later voidEntry any one of\n // them without a follow-up getJournalEntries round-trip.\n const summary = entries.map((entry) => `${entry.date ?? \"?\"} (id: ${entry.id ?? \"?\"})`).join(\", \");\n return `Posted ${entries.length} journal entries: ${summary}.`;\n },\n [ACCOUNTING_ACTIONS.voidEntry]: (fields) => {\n const reverseDate = optionalString(optionalRecord(fields.reverseEntry)?.date);\n return `Voided the entry; a reversing pair was posted on ${reverseDate ?? \"today\"}.`;\n },\n [ACCOUNTING_ACTIONS.setOpeningBalances]: (fields) => {\n const opening = optionalRecord(fields.openingEntry);\n const verb = fields.replacedExisting === true ? \"replaced\" : \"set\";\n const date = optionalString(opening?.date) ?? \"the requested date\";\n // Surface the actual lines so the LLM can answer follow-up\n // questions like \"what's my opening cash?\" without a separate\n // getOpeningBalances round-trip. An empty-marker opening\n // (zero lines, used to unlock the gate) gets no fragment.\n const openingLines = opening?.lines;\n const lines = isUnknownArray(openingLines) ? openingLines : [];\n const linesFragment = lines.length > 0 ? ` Lines: ${JSON.stringify(lines)}.` : \"\";\n return `Opening balances were ${verb} as of ${date}.${linesFragment}`;\n },\n [ACCOUNTING_ACTIONS.deleteBook]: (fields) => {\n const bookId = optionalString(fields.deletedBookId);\n const name = optionalString(fields.deletedBookName);\n const subject = name ? `the book ${JSON.stringify(name)}` : \"the book\";\n const idFragment = bookId ? ` (id: ${bookId})` : \"\";\n return `Deleted ${subject}${idFragment}.`;\n },\n [ACCOUNTING_ACTIONS.updateBook]: (fields) => {\n const book = optionalRecord(fields.book);\n const bookName = optionalString(book?.name);\n const country = optionalString(book?.country);\n const name = bookName ? JSON.stringify(bookName) : \"the book\";\n const countryFragment = country ? ` (country: ${country})` : \"\";\n return `Updated ${name}${countryFragment}.`;\n },\n};\n\n/** Read a record entry under a user/LLM-controlled key. The\n * `Object.hasOwn` gate is load-bearing: a bare `record[key]` resolves\n * inherited prototype members, so a crafted action (\"constructor\",\n * \"toString\") would dispatch to an unexpected target instead of\n * reading as absent. */\nfunction ownEntry<T>(record: Record<string, T>, key: string): T | undefined {\n return Object.hasOwn(record, key) ? record[key] : undefined;\n}\n\nfunction previewMessage(action: string, fields: Record<string, unknown>): string {\n const head = ownEntry(MESSAGE_BUILDERS, action)?.(fields);\n return head ? `${head} ${VIEW_VISIBLE_TRAILER}` : VIEW_VISIBLE_TRAILER;\n}\n\nasync function dispatch(body: AccountingActionBody, root: string | undefined): Promise<unknown> {\n const { action, ...rest } = body;\n const handler = ownEntry(ACTION_HANDLERS, action);\n if (!handler) throw new AccountingError(400, `unknown action ${JSON.stringify(action)}`);\n // Stamp the dispatch verb onto the response so the MCP bridge's\n // spread `{ toolName, uuid, ...result }` surfaces it as\n // `ToolResult.action`. The sidebar reads this to label cards as\n // `manageAccounting(openBook)` etc., and it round-trips a refresh\n // because the result envelope is persisted to the chat log.\n // Direct browser callers (the AccountingApp view) ignore the field.\n // Service responses that already set `action` win via the spread.\n const result = await handler(rest, root);\n // `isRecord` rejects arrays where the previous `typeof === \"object\"`\n // check accepted them — an array result would have spread into\n // `{0: …, 1: …}`. Every service function returns a plain object, so\n // nothing changes today; an array would now arrive as `{ value: [...] }`.\n const handlerFields = isRecord(result) ? result : { value: result };\n // `data` is the host's preview-eligibility signal (see\n // SessionSidebar.vue's v-if gate). Mirror the handler payload\n // into it for the actions that should render a card; leave it\n // off for silent ones so the gate suppresses the preview.\n const dataField = PREVIEW_ACTIONS.has(action) ? { data: { action, ...handlerFields } } : {};\n // The MCP bridge only forwards `message` / `instructions` to the\n // LLM (`data` / `jsonData` reach the view but not the model). So\n // every action MUST set a message — silence resolves to \"Done\"\n // and gives the LLM nothing to reason about. Resolution order:\n // 1. handler-set `message` wins (reserved for special-case\n // narration that the standard MESSAGE_BUILDER can't capture);\n // 2. an action with a registered MESSAGE_BUILDER gets the\n // per-action human-friendly summary; this is decoupled from\n // PREVIEW_ACTIONS so silent ops like deleteBook can still\n // narrate without earning a card;\n // 3. everything else returns the JSON-stringified handler\n // payload so the LLM can read the raw data.\n const handlerMessage = typeof handlerFields.message === \"string\" ? handlerFields.message : undefined;\n const messageField = handlerMessage\n ? {}\n : MESSAGE_BUILDERS[action]\n ? { message: previewMessage(action, handlerFields) }\n : { message: JSON.stringify(handlerFields) };\n return { action, ...handlerFields, ...messageField, ...dataField };\n}\n\n/** The request shape the dispatch route hands a host resolver — the\n * same one the route itself is typed with, so a host can read headers,\n * query and the parsed body without casting. */\nexport type AccountingDispatchRequest = Request<object, unknown, AccountingActionBody>;\n\nexport interface AccountingRouterOptions {\n /** Resolve the workspace root ONE request operates on, for a host\n * that serves more than one (MulmoTerminal: one root per project\n * directory). Return `undefined` to use the host's configured root.\n *\n * The resolver is entirely host-owned and the package never reads a\n * root or a project id off the request body itself. That is what\n * keeps the model out of it: `manageAccounting`'s tool schema has no\n * project parameter, so an LLM cannot name a project even by\n * guessing — the host derives the scope from the session, exactly as\n * `manageCollection` does. A host that DOES accept a project id from\n * a browser must resolve it against its own list of projects and\n * never accept a path (see `AccountingServerDeps.channelScopeForRoot`). */\n resolveWorkspaceRoot?: (req: AccountingDispatchRequest) => string | undefined;\n}\n\n/** Build the accounting Express router. The host injects its workspace\n * root + logger via `configureAccountingServer(...)` and pub/sub via\n * `initAccountingEventPublisher(...)`, then mounts the returned router\n * with `app.use(...)`. Pass `resolveWorkspaceRoot` to serve more than\n * one root; omit it and every request uses the configured one. */\nexport function createAccountingRouter(options: AccountingRouterOptions = {}): Router {\n const router = Router();\n router.post(\n ACCOUNTING_API.dispatch.path,\n asyncHandler<Request<object, unknown, AccountingActionBody>, Response<unknown | AccountingErrorResponse>>(\n \"accounting\",\n \"accounting dispatch failed\",\n async (req, res) => {\n // Validate the body shape up front so a missing / non-object body\n // surfaces as a 400 instead of crashing `dispatch` and bubbling\n // through to the 500 catch-all.\n const { body } = req;\n if (!body || typeof body !== \"object\" || typeof body.action !== \"string\") {\n log.warn(\"accounting\", \"POST dispatch: invalid body\");\n res.status(400).json({ error: \"request body must be an object with a string `action` field\" });\n return;\n }\n const { action } = body;\n log.info(\"accounting\", \"POST dispatch: start\", { action });\n try {\n // Inside the try: a host resolver rejects an unknown project id\n // by throwing, and that is a 4xx about the request, not a\n // server fault — resolving it out here would skip the\n // AccountingError mapping below and answer a bad project with\n // a generic 500.\n const root = options.resolveWorkspaceRoot?.(req);\n const result = await dispatch(body, root);\n log.info(\"accounting\", \"POST dispatch: ok\", { action });\n res.json(result);\n } catch (err) {\n // Domain errors (AccountingError) map to 4xx with `details`.\n // Anything else rethrows — the asyncHandler wrapper catches\n // it, logs `unexpected error`, and returns a generic 500.\n if (err instanceof AccountingError) {\n log.warn(\"accounting\", \"POST dispatch: error\", { action, status: err.status, message: err.message });\n res.status(err.status).json({ error: err.message, details: err.details });\n return;\n }\n throw err;\n }\n },\n ),\n );\n return router;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"server.cjs","names":[],"sources":["../src/server/bodyFields.ts","../src/server/context.ts","../src/server/journal.ts","../src/server/io.ts","../src/server/openingBalances.ts","../src/server/accountNormalize.ts","../src/server/report.ts","../src/server/timeSeries.ts","../src/server/eventPublisher.ts","../src/server/snapshotCache.ts","../src/server/defaultAccounts.ts","../src/server/service.ts","../src/server/http.ts","../src/server/router.ts"],"sourcesContent":["// Readers for the loosely-typed dispatch body, and for the service\n// payloads the message builders narrate back.\n//\n// Everything below the `action` discriminator arrives as `unknown` off\n// the wire, so a field is only a string once something has checked. A\n// non-string reads as absent, which lets the service layer reject it on\n// its own terms — `bookId: 42` gets \"bookId is required\" instead of\n// being smuggled through as a string the rest of the stack believes in.\n\nimport { isRecord } from \"@mulmoclaude/common\";\n\nimport type { ReportPeriod } from \"../shared/types.js\";\n\nexport const optionalString = (value: unknown): string | undefined => (typeof value === \"string\" ? value : undefined);\n\nexport const optionalRecord = (value: unknown): Record<string, unknown> | undefined => (isRecord(value) ? value : undefined);\n\nconst YEAR_MONTH_RE = /^\\d{4}-(0[1-9]|1[0-2])$/;\nconst YEAR_MONTH_DAY_RE = /^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])$/;\n\n/** `YYYY-MM-DD` that names a day the calendar actually has. The regex alone\n * admits `2026-02-30`, and `Date.UTC` rolls that forward to March instead of\n * refusing it, so the round trip is what says the date is real. */\nconst isCalendarDate = (value: string): boolean => {\n if (!YEAR_MONTH_DAY_RE.test(value)) return false;\n const [year, month, day] = value.split(\"-\").map(Number);\n if (year === undefined || month === undefined || day === undefined) return false;\n return new Date(Date.UTC(year, month - 1, day)).getUTCDate() === day;\n};\n\n/** Rebuilt field by field rather than narrowed with a predicate, so the\n * returned object is one this function actually proved. A half-formed\n * `{ kind: \"month\" }` reads as absent and the caller raises its own\n * \"period is required\" — previously it reached the report builders and\n * produced an `undefined-01` date.\n *\n * The FORMAT is checked here too, not just the type: `typeof === \"string\"`\n * let `{ kind: \"month\", period: \"banana\" }` through, which came back as a\n * balance sheet dated `\"banana-NaN\"` and made the snapshot cache write\n * `banana.json` / `0NaN-NaN.json` into the book (#2765). Malformed reads as\n * absent so it lands on the caller's existing 400, which already spells out\n * both accepted shapes for the LLM to repair its payload from. */\nexport const optionalReportPeriod = (value: unknown): ReportPeriod | undefined => {\n const period = optionalRecord(value);\n if (period?.kind === \"month\" && typeof period.period === \"string\" && YEAR_MONTH_RE.test(period.period)) {\n return { kind: \"month\", period: period.period };\n }\n if (\n period?.kind === \"range\" &&\n typeof period.from === \"string\" &&\n typeof period.to === \"string\" &&\n isCalendarDate(period.from) &&\n isCalendarDate(period.to)\n ) {\n return { kind: \"range\", from: period.from, to: period.to };\n }\n return undefined;\n};\n\n/** The two fields the addEntries narration quotes back, read out of a\n * service payload that is only `unknown` to the router. */\nexport const describeEntry = (entry: unknown): { id?: string | undefined; date?: string | undefined } => {\n const record = optionalRecord(entry);\n return { id: optionalString(record?.id), date: optionalString(record?.date) };\n};\n","// Host-injected runtime context for the accounting server surface.\n//\n// The backend can't reach into the host for the workspace root, the\n// pub/sub instance, or the logger — those are host-specific. The host\n// injects them once via `configureAccountingServer(...)` before\n// mounting the router (server/index.ts), the server-side mirror of the\n// Vue surface's `configureAccountingHost`. MulmoTerminal wires its own.\n//\n// `log` is a thin proxy that forwards to the injected logger so the\n// many `log.warn(\"accounting\", …)` call sites across the service layer\n// stay unchanged. Before configuration (and in unit tests that drive\n// the service with an explicit workspace root) it falls back to a\n// console logger so nothing throws.\n\nimport type { StructuredLogger } from \"@mulmoclaude/common\";\n\n/** Minimal pub/sub shape — structurally compatible with the host's\n * `IPubSub`. The eventPublisher holds its own instance (set via\n * `initAccountingEventPublisher`); this type is the contract. */\nexport interface IPubSub {\n publish: (channel: string, payload: unknown) => void;\n}\n\n/** Alias of the shared `StructuredLogger` — the host `Logger` stays directly assignable when injected. */\nexport type AccountingLogger = StructuredLogger;\n\nexport interface AccountingServerDeps {\n /** Absolute path to the workspace root (where `data/` lives). Used as\n * the default when a service/io call doesn't pass an explicit root.\n *\n * `null` declares STRICT mode: this host always passes an explicit\n * root per call, and `defaultWorkspaceRoot()` throws instead of\n * guessing. A multi-root host (MulmoTerminal, one root per project\n * directory) MUST use it — with N roots a forgotten option is not a\n * crash but a silent read/write against the wrong project. Mirrors\n * `CollectionHost.workspaceRoot: string | null` in\n * `@mulmoclaude/core/collection/server`. */\n workspaceRoot: string | null;\n logger: AccountingLogger;\n /** Optional: map an absolute root to the OPAQUE scope id the host uses\n * for that project, used to namespace pub/sub channel names so two\n * roots owning one bookId do not cross-notify. Return `null` for the\n * host's default root, which keeps channel names byte-identical to a\n * single-root host's (`accounting:<bookId>`).\n *\n * It must be an opaque id, NEVER a path: channel names reach the\n * browser, and an absolute root there publishes the user's home\n * directory to the client. Same rule as a collection view token. */\n channelScopeForRoot?: (workspaceRoot: string) => string | null;\n}\n\nlet deps: AccountingServerDeps | null = null;\n\n/** Called once by the host before the accounting router is mounted. */\nexport function configureAccountingServer(context: AccountingServerDeps): void {\n deps = context;\n}\n\n/** Default workspace root for io calls that don't pass one explicitly.\n * Throws if the host never configured the server — a real wiring bug\n * (unit tests always pass an explicit root, so they never hit this) —\n * and also under STRICT mode (`workspaceRoot: null`), where there is no\n * ambient root to fall back to and a missing option is a bug rather\n * than a default. */\nexport function defaultWorkspaceRoot(): string {\n if (!deps) {\n throw new Error(\"@mulmoclaude/accounting-plugin: configureAccountingServer() must be called before serving accounting requests\");\n }\n if (deps.workspaceRoot === null) {\n throw new Error(\n \"@mulmoclaude/accounting-plugin: this host is configured with workspaceRoot: null (explicit-root mode), so every call must pass a workspaceRoot. A call reached the engine without one.\",\n );\n }\n return deps.workspaceRoot;\n}\n\n/** The opaque channel scope for a root, or `null` when the host declared\n * none (single-root hosts, and a multi-root host's default root). Used\n * by the event publisher; never contains a path.\n *\n * A missing root throws under explicit-root mode, exactly as\n * `defaultWorkspaceRoot()` does. Returning `null` there would be worse\n * than the io-path bug it mirrors: the event would go out on the\n * UNSCOPED channel name, which every project's default-scope\n * subscriber receives. The publisher catches it and logs, so a dropped\n * root costs one event and a warning rather than a silent cross-project\n * notification. */\nexport function channelScopeFor(workspaceRoot?: string): string | null {\n if (!deps?.channelScopeForRoot) return null;\n return deps.channelScopeForRoot(workspaceRoot ?? defaultWorkspaceRoot());\n}\n\n/** Whether the host declared project scoping at all.\n *\n * The difference matters where a card records the project it belongs\n * to: on a scoped host, \"no scope\" is a REAL answer (the default root)\n * and must be recorded as `null`, not left out — an omitted field means\n * \"ask the host what is active now\", which is precisely the drift a\n * pinned card exists to prevent. On an unscoped host the field is left\n * out entirely and every payload stays byte-identical to today's. */\nexport function isProjectScopedHost(): boolean {\n return Boolean(deps?.channelScopeForRoot);\n}\n\nconst consoleLogger: AccountingLogger = {\n error: (namespace, msg, data) => console.error(`[${namespace}] ${msg}`, data ?? \"\"),\n warn: (namespace, msg, data) => console.warn(`[${namespace}] ${msg}`, data ?? \"\"),\n info: () => {},\n debug: () => {},\n};\n\n/** Logger proxy — forwards to the injected logger, console fallback\n * before configuration. Lets call sites keep `log.warn(\"accounting\", …)`. */\nexport const log: AccountingLogger = {\n error: (namespace, msg, data) => (deps?.logger ?? consoleLogger).error(namespace, msg, data),\n warn: (namespace, msg, data) => (deps?.logger ?? consoleLogger).warn(namespace, msg, data),\n info: (namespace, msg, data) => (deps?.logger ?? consoleLogger).info(namespace, msg, data),\n debug: (namespace, msg, data) => (deps?.logger ?? consoleLogger).debug(namespace, msg, data),\n};\n","// Pure validation + creation logic for journal entries. No fs access\n// here — callers wire the validated entry to `appendJournal` in\n// `server/utils/files/accounting-io.ts`.\n//\n// Double-entry rule: every entry's lines must satisfy\n// Σ debit === Σ credit (within a tolerance of 0.005 — see\n// EQUALITY_TOLERANCE below)\n//\n// Append-only: there is no `editEntry`. Corrections are made by\n// `voidEntry` (creates a reversing pair) followed by a fresh\n// `addEntries` call for the corrected booking.\n\nimport { randomUUID } from \"node:crypto\";\nimport { hasStringProp, isRecord, isUnknownArray } from \"@mulmoclaude/common\";\n\nimport { JOURNAL_ENTRY_KINDS } from \"../shared/types.js\";\nimport type { Account, JournalEntry, JournalLine } from \"../shared/types.js\";\n\n/** Floating-point tolerance for the debit = credit check. Currency\n * amounts arrive as JavaScript numbers (the on-wire format is JSON,\n * so amounts are doubles). 0.005 keeps two-decimal currency math\n * honest while accepting the floating-point noise of summing\n * many lines. */\nconst EQUALITY_TOLERANCE = 0.005;\n\n/** Defensive cap on `JournalLine.taxRegistrationId`. Real-world IDs\n * are short (JP T-numbers are 14 chars, EU VAT IDs ≤ 14, GSTIN is\n * 15, ABN is 11). 32 covers every documented format with comfortable\n * margin while still rejecting accidental paste-bombs. Validation\n * applies to the *trimmed* value so a string of pure whitespace\n * doesn't trip the limit (it normalises to absent). */\nexport const MAX_TAX_REGISTRATION_ID_LENGTH = 32;\n\nexport interface ValidationError {\n field: string;\n message: string;\n}\n\n/** The fields `parseEntry` proves about one wire entry. `makeEntry`\n * takes this, so an entry can only be built from input something\n * actually checked. */\nexport interface ParsedEntry {\n date: string;\n lines: JournalLine[];\n memo?: string | undefined;\n replacesEntryId?: string | undefined;\n}\n\nexport type EntryParseResult = { ok: true; entry: ParsedEntry } | { ok: false; errors: ValidationError[] };\n\nfunction lineHasExactlyOneSide(line: JournalLine): boolean {\n const hasDebit = typeof line.debit === \"number\" && line.debit !== 0;\n const hasCredit = typeof line.credit === \"number\" && line.credit !== 0;\n return hasDebit !== hasCredit;\n}\n\nfunction isNonNegativeNumber(value: unknown): value is number {\n return typeof value === \"number\" && Number.isFinite(value) && value >= 0;\n}\n\n/** Build today's `YYYY-MM-DD` from the host's local timezone.\n * Centralised here so server-side defaults (the void-date on\n * `voidEntry`, the today() stamp on opening replacements, etc.)\n * agree with the client-side `localDateString()` from\n * `src/plugins/accounting/dates.ts`. `toISOString().slice(0, 10)`\n * would emit a UTC date instead — which silently flips into\n * tomorrow / yesterday in negative-offset timezones. */\nexport function localDateString(now: Date = new Date()): string {\n const year = now.getFullYear();\n const month = String(now.getMonth() + 1).padStart(2, \"0\");\n const day = String(now.getDate()).padStart(2, \"0\");\n return `${year}-${month}-${day}`;\n}\n\n/** Validate that `date` is both shaped as YYYY-MM-DD AND represents\n * a real calendar day. The bare regex accepts impossible values\n * like 2026-02-31 or 2026-13-01 which would then poison\n * `periodFromDate`, sort orders, and snapshot keys. We reparse\n * through the Date constructor and roundtrip-format to catch\n * silent normalisation (e.g. \"2026-02-30\" → Mar 02). */\nexport function isValidCalendarDate(date: string): boolean {\n if (!/^\\d{4}-\\d{2}-\\d{2}$/.test(date)) return false;\n const [year, month, day] = date.split(\"-\").map((segment) => parseInt(segment, 10));\n if (year === undefined || month === undefined || day === undefined) return false;\n const parsed = new Date(Date.UTC(year, month - 1, day));\n return parsed.getUTCFullYear() === year && parsed.getUTCMonth() === month - 1 && parsed.getUTCDate() === day;\n}\n\n/** Returns Σ debit − Σ credit. Used by callers that need the actual\n * imbalance value (e.g. the OpeningBalancesForm shows live diff). */\nexport function netBalance(lines: readonly JournalLine[]): number {\n let net = 0;\n for (const line of lines) {\n if (typeof line.debit === \"number\") net += line.debit;\n if (typeof line.credit === \"number\") net -= line.credit;\n }\n return net;\n}\n\nfunction checkTaxRegistrationId(value: unknown, idx: number, errors: ValidationError[]): void {\n if (value === undefined) return;\n if (typeof value !== \"string\") {\n errors.push({ field: `lines[${idx}].taxRegistrationId`, message: \"must be a string\" });\n return;\n }\n if (value.trim().length > MAX_TAX_REGISTRATION_ID_LENGTH) {\n errors.push({\n field: `lines[${idx}].taxRegistrationId`,\n message: `must be at most ${MAX_TAX_REGISTRATION_ID_LENGTH} characters (got ${value.trim().length})`,\n });\n }\n}\n\n/** One issue per bad field, so a caller fixing a line learns about all\n * of them at once. Returns whether the line came through clean. */\nfunction checkLineFields(raw: Record<string, unknown>, idx: number, errors: ValidationError[]): boolean {\n const issuesBefore = errors.length;\n const { accountCode, debit, credit, memo, taxRegistrationId } = raw;\n if (typeof accountCode !== \"string\") errors.push({ field: `lines[${idx}].accountCode`, message: \"accountCode must be a string\" });\n if (debit !== undefined && !isNonNegativeNumber(debit)) errors.push({ field: `lines[${idx}].debit`, message: \"debit must be a non-negative finite number\" });\n if (credit !== undefined && !isNonNegativeNumber(credit))\n errors.push({ field: `lines[${idx}].credit`, message: \"credit must be a non-negative finite number\" });\n if (memo !== undefined && typeof memo !== \"string\") errors.push({ field: `lines[${idx}].memo`, message: \"memo must be a string\" });\n checkTaxRegistrationId(taxRegistrationId, idx, errors);\n return errors.length === issuesBefore;\n}\n\n/** Re-tested rather than assigned straight through: `checkLineFields`\n * proved each of these, but only a `typeof` narrows them for the\n * compiler. */\nfunction buildLine(raw: Record<string, unknown>): JournalLine | null {\n const { accountCode, debit, credit, memo, taxRegistrationId } = raw;\n if (typeof accountCode !== \"string\") return null;\n const line: JournalLine = { accountCode };\n if (typeof debit === \"number\") line.debit = debit;\n if (typeof credit === \"number\") line.credit = credit;\n if (typeof memo === \"string\") line.memo = memo;\n if (typeof taxRegistrationId === \"string\") line.taxRegistrationId = taxRegistrationId;\n return line;\n}\n\n/** Narrow one wire value to a `JournalLine`, pushing an issue per bad\n * field. Shape only: account existence and the debit/credit-side rule\n * belong to the caller, because journal entries and opening balances\n * disagree about them. Returns null when nothing usable came out —\n * the caller drops the line rather than reading fields off it. */\nexport function parseJournalLine(raw: unknown, idx: number, errors: ValidationError[]): JournalLine | null {\n if (!isRecord(raw)) {\n errors.push({ field: `lines[${idx}]`, message: \"each line must be an object with an accountCode and a debit or credit amount\" });\n return null;\n }\n if (!checkLineFields(raw, idx, errors)) return null;\n return buildLine(raw);\n}\n\n/** The rules a journal line answers to on top of its shape: the code\n * must name a real account, and exactly one side carries an amount. */\nfunction validateEntryLine(line: JournalLine, idx: number, accountCodes: ReadonlySet<string>, errors: ValidationError[]): void {\n if (!line.accountCode || !accountCodes.has(line.accountCode)) {\n errors.push({ field: `lines[${idx}].accountCode`, message: `unknown account code ${JSON.stringify(line.accountCode)}` });\n }\n if (!lineHasExactlyOneSide(line)) {\n errors.push({ field: `lines[${idx}]`, message: \"each line must set exactly one of debit or credit (and to a non-zero amount)\" });\n }\n}\n\nfunction parseEntryLines(raw: readonly unknown[], accountCodes: ReadonlySet<string>, errors: ValidationError[]): JournalLine[] {\n const lines: JournalLine[] = [];\n raw.forEach((rawLine, idx) => {\n const line = parseJournalLine(rawLine, idx, errors);\n if (line === null) return;\n validateEntryLine(line, idx, accountCodes, errors);\n lines.push(line);\n });\n return lines;\n}\n\n/** Report the debit = credit imbalance — but only when every line was\n * readable. A line that failed to parse contributes nothing to the sum,\n * so an entry that balances perfectly would otherwise be told it\n * doesn't, sending the caller off to \"fix\" amounts that were never\n * wrong. Naming the unreadable line is the actionable message; the\n * balance is worth re-checking once it's a line. */\nexport function checkBalances(lines: readonly JournalLine[], expectedLineCount: number, subject: string, errors: ValidationError[]): void {\n if (lines.length !== expectedLineCount) return;\n const net = netBalance(lines);\n if (Math.abs(net) > EQUALITY_TOLERANCE) {\n errors.push({ field: \"lines\", message: `Σ debit − Σ credit = ${net.toFixed(4)}; ${subject} must balance` });\n }\n}\n\nfunction parseOptionalString(value: unknown, field: string, errors: ValidationError[]): string | undefined {\n if (value === undefined || typeof value === \"string\") return value;\n errors.push({ field, message: `${field} must be a string when supplied` });\n return undefined;\n}\n\nconst isOptionalString = (value: unknown): boolean => value === undefined || typeof value === \"string\";\nconst isOptionalNumber = (value: unknown): boolean => value === undefined || typeof value === \"number\";\n\nfunction isJournalLine(value: unknown): value is JournalLine {\n return (\n hasStringProp(value, \"accountCode\") &&\n isOptionalNumber(value.debit) &&\n isOptionalNumber(value.credit) &&\n isOptionalString(value.memo) &&\n isOptionalString(value.taxRegistrationId)\n );\n}\n\n/** Checks every field `JournalEntry` and `JournalLine` declare, so a value\n * that passes really is one. Used when reading the journal JSONL back:\n * everything this module ever wrote satisfies it (`id` / `date` / `kind` /\n * `lines` / `createdAt` have been required since the plugin's first\n * release), while a line that doesn't is exactly the line that takes the\n * whole book down — `report.ts` iterates `entry.lines` unguarded. */\nexport function isJournalEntry(value: unknown): value is JournalEntry {\n return (\n hasStringProp(value, \"id\") &&\n hasStringProp(value, \"date\") &&\n hasStringProp(value, \"createdAt\") &&\n JOURNAL_ENTRY_KINDS.some((kind) => kind === value.kind) &&\n isUnknownArray(value.lines) &&\n value.lines.every(isJournalLine) &&\n isOptionalString(value.memo) &&\n isOptionalString(value.voidedEntryId) &&\n isOptionalString(value.voidReason) &&\n isOptionalString(value.replacesEntryId)\n );\n}\n\n/** Normalize a journal line before persistence: trim string fields\n * and drop empty-string optionals so the JSONL doesn't accumulate\n * noise like `\"taxRegistrationId\":\"\"`. Pure — does not mutate\n * `line`. */\nfunction normalizeLine(line: JournalLine): JournalLine {\n const out: JournalLine = { ...line };\n if (typeof out.taxRegistrationId === \"string\") {\n const trimmed = out.taxRegistrationId.trim();\n if (trimmed === \"\") delete out.taxRegistrationId;\n else out.taxRegistrationId = trimmed;\n }\n return out;\n}\n\n/** Parse one entry off the wire. Does not throw: every rejection comes\n * back as a list of issues so the REST handler can return a structured\n * 400 instead of an opaque 500. Parse rather than validate — the\n * narrowed entry rides along on success, so `makeEntry` never has to\n * take the caller's word for the shape. */\nexport function parseEntry(raw: unknown, accounts: readonly Account[]): EntryParseResult {\n if (!isRecord(raw)) {\n return { ok: false, errors: [{ field: \"entry\", message: \"each entry must be an object with a date and a lines array\" }] };\n }\n const errors: ValidationError[] = [];\n const date = typeof raw.date === \"string\" && isValidCalendarDate(raw.date) ? raw.date : null;\n if (date === null) errors.push({ field: \"date\", message: `expected YYYY-MM-DD calendar date, got ${JSON.stringify(raw.date)}` });\n if (!isUnknownArray(raw.lines) || raw.lines.length < 2) {\n errors.push({ field: \"lines\", message: \"an entry needs at least two lines (one debit, one credit)\" });\n return { ok: false, errors };\n }\n const lines = parseEntryLines(raw.lines, new Set(accounts.map((account) => account.code)), errors);\n checkBalances(lines, raw.lines.length, \"entry\", errors);\n const memo = parseOptionalString(raw.memo, \"memo\", errors);\n const replacesEntryId = parseOptionalString(raw.replacesEntryId, \"replacesEntryId\", errors);\n if (errors.length > 0 || date === null) return { ok: false, errors };\n return { ok: true, entry: { date, lines, memo, replacesEntryId } };\n}\n\n/** Build a JournalEntry from lines something already parsed\n * (`parseEntry` / `parseOpening`). The id is a fresh\n * UUID; createdAt is the wall clock at the moment of creation.\n * Lines are normalized so optional string fields don't persist as\n * empty strings. */\nexport function makeEntry(input: {\n date: string;\n lines: readonly JournalLine[];\n memo?: string | undefined;\n kind?: JournalEntry[\"kind\"] | undefined;\n replacesEntryId?: string | undefined;\n}): JournalEntry {\n const entry: JournalEntry = {\n id: randomUUID(),\n date: input.date,\n kind: input.kind ?? \"normal\",\n lines: input.lines.map(normalizeLine),\n memo: input.memo,\n createdAt: new Date().toISOString(),\n };\n if (input.replacesEntryId) entry.replacesEntryId = input.replacesEntryId;\n return entry;\n}\n\n/** Pick the most descriptive memo from the original entry to quote\n * in the voiding entry's memo. Precedence: entry-level memo →\n * first non-empty line memo → null (caller falls back to a\n * date-only template). */\nfunction originalMemoToQuote(target: JournalEntry): string | null {\n if (target.memo && target.memo.trim() !== \"\") return target.memo;\n for (const line of target.lines) {\n if (line.memo && line.memo.trim() !== \"\") return line.memo;\n }\n return null;\n}\n\n/** Build the human-readable memo that goes on the voiding entry.\n * Format: `void of '<original memo>' on <original date>` (or the\n * no-memo fallback when the original carried no memo). The reason\n * the user typed is appended after a colon when present. */\nexport function voidMemo(target: JournalEntry, reason: string | undefined): string {\n const quoted = originalMemoToQuote(target);\n const base = quoted !== null ? `void of '${quoted}' on ${target.date}` : `void of entry on ${target.date}`;\n return reason && reason.trim() !== \"\" ? `${base}: ${reason}` : base;\n}\n\n/** Build the reversing pair for a voided entry. The `void` entry\n * swaps debit / credit on every line so the net effect is zero;\n * the `void-marker` is a zero-line entry that exists purely to\n * carry the `voidedEntryId` reference and the user's reason. The\n * marker keeps `listEntries` queries simple — filtering by\n * `kind: \"void-marker\"` surfaces every voided id without scanning\n * for matching pairs. */\nexport function makeVoidEntries(target: JournalEntry, reason: string | undefined, voidDate: string): { reverse: JournalEntry; marker: JournalEntry } {\n const swappedLines: JournalLine[] = target.lines.map((line) => {\n const swapped: JournalLine = {\n accountCode: line.accountCode,\n debit: line.credit,\n credit: line.debit,\n memo: line.memo,\n };\n // Preserve the counterparty tax-registration ID on each reversed\n // line so the audit trail survives the void — without it, the\n // reversing pair would silently drop the input-tax-credit\n // documentation and a later report scan couldn't reconstruct\n // which T-number / VAT ID the original input tax was tied to.\n if (line.taxRegistrationId !== undefined) swapped.taxRegistrationId = line.taxRegistrationId;\n return swapped;\n });\n const reverse: JournalEntry = {\n id: randomUUID(),\n date: voidDate,\n kind: \"void\",\n lines: swappedLines,\n memo: voidMemo(target, reason),\n voidedEntryId: target.id,\n voidReason: reason,\n createdAt: new Date().toISOString(),\n };\n const marker: JournalEntry = {\n id: randomUUID(),\n date: voidDate,\n kind: \"void-marker\",\n lines: [],\n voidedEntryId: target.id,\n voidReason: reason,\n createdAt: new Date().toISOString(),\n };\n return { reverse, marker };\n}\n\n/** Returns the set of entry ids that have been voided — built from\n * every `void-marker` entry's `voidedEntryId`. Reports use this to\n * exclude original-and-reverse pairs from the activity listing\n * (the netting is automatic in B/S aggregates because the reverse\n * entry has equal-and-opposite lines). */\nexport function voidedIdSet(entries: readonly JournalEntry[]): Set<string> {\n const set = new Set<string>();\n for (const entry of entries) {\n if (entry.kind === \"void-marker\" && entry.voidedEntryId) set.add(entry.voidedEntryId);\n }\n return set;\n}\n","// Single fs gateway for the accounting plugin. Every read / write\n// against `data/accounting/...` lives here so callers don't sprinkle\n// raw `fs` / path concatenation across the codebase (CLAUDE.md rule:\n// raw `fs.readFile` / `fs.writeFile` is forbidden in route handlers).\n//\n// Snapshot cache rule: snapshots are derived state. Any write that\n// touches past data must call `invalidateSnapshotsFrom(...)` to drop\n// stale snapshot files; the next read regenerates lazily via\n// `server/accounting/snapshotCache.ts`. The journal JSONL files are\n// the single source of truth.\n\nimport { promises as fsPromises } from \"node:fs\";\nimport path from \"node:path\";\n\nimport { defaultWorkspaceRoot } from \"./context.js\";\nimport { isJournalEntry } from \"./journal.js\";\nimport { ACCOUNTING_DIRS as WORKSPACE_DIRS, resolveFiscalYearEnd } from \"../shared\";\nimport { writeJsonAtomic, isEnoent } from \"@mulmoclaude/core/files\";\nimport type { AccountingConfig, MonthSnapshot } from \"./types.js\";\nimport type { Account, BookSummary, JournalEntry } from \"../shared/types.js\";\n\nconst root = (workspaceRoot?: string): string => workspaceRoot ?? defaultWorkspaceRoot();\n\nfunction accountingRoot(workspaceRoot?: string): string {\n return path.join(root(workspaceRoot), WORKSPACE_DIRS.accounting);\n}\n\nfunction configPath(workspaceRoot?: string): string {\n return path.join(accountingRoot(workspaceRoot), \"config.json\");\n}\n\n/** Allowed shape for a book id used as a directory name. Defense\n * against path traversal: a crafted id like \"../../config\" or\n * \"/tmp/x\" would otherwise let `bookRoot` escape the\n * `data/accounting/books/` tree, since every write path joins\n * `bookId` directly into the filesystem. The first character is\n * alphanumeric to forbid leading dashes / underscores that some\n * shells / docs render confusingly; `_` and `-` are allowed inside.\n * 64 chars is plenty for any reasonable book name. */\nconst SAFE_BOOK_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;\n\nexport function isSafeBookId(bookId: string): boolean {\n return typeof bookId === \"string\" && SAFE_BOOK_ID_RE.test(bookId);\n}\n\nfunction assertSafeBookId(bookId: string): void {\n if (!isSafeBookId(bookId)) {\n throw new Error(`accounting: invalid bookId ${JSON.stringify(bookId)} (allowed: alphanumeric / _ / -; 1-64 chars; cannot start with _ or -)`);\n }\n}\n\nexport function bookRoot(bookId: string, workspaceRoot?: string): string {\n assertSafeBookId(bookId);\n return path.join(root(workspaceRoot), WORKSPACE_DIRS.accountingBooks, bookId);\n}\n\nfunction accountsPath(bookId: string, workspaceRoot?: string): string {\n return path.join(bookRoot(bookId, workspaceRoot), \"accounts.json\");\n}\n\nfunction journalDir(bookId: string, workspaceRoot?: string): string {\n return path.join(bookRoot(bookId, workspaceRoot), \"journal\");\n}\n\nfunction journalFileFor(bookId: string, period: string, workspaceRoot?: string): string {\n return path.join(journalDir(bookId, workspaceRoot), `${period}.jsonl`);\n}\n\nfunction snapshotsDir(bookId: string, workspaceRoot?: string): string {\n return path.join(bookRoot(bookId, workspaceRoot), \"snapshots\");\n}\n\nfunction snapshotFileFor(bookId: string, period: string, workspaceRoot?: string): string {\n return path.join(snapshotsDir(bookId, workspaceRoot), `${period}.json`);\n}\n\nasync function fileExists(filePath: string): Promise<boolean> {\n try {\n await fsPromises.access(filePath);\n return true;\n } catch {\n return false;\n }\n}\n\n/** Strict variant of `readJsonOrNull` from `./json.ts`: returns null\n * on ENOENT but RETHROWS other read errors and parse failures so a\n * corrupted accounting journal surfaces rather than silently\n * collapsing to \"no data\". `./json.ts` keeps the permissive\n * variant for user-config files where a single bad keystroke\n * shouldn't 500 the server. */\nasync function readJsonStrict<T>(filePath: string): Promise<T | null> {\n try {\n const raw = await fsPromises.readFile(filePath, \"utf-8\");\n // Cast kept (#2692): the persisted shapes are deliberately looser than\n // their types — `BookSummary.fiscalYearEnd` is typed as a month number but\n // legacy books hold \"Q1\"..\"Q4\" on disk (`normalizeBookFiscalYearEnd`\n // absorbs it on read). A predicate written against the type would reject\n // those files and lock the user out of their books.\n return JSON.parse(raw) as T;\n } catch (err) {\n if (isEnoent(err)) return null;\n throw err;\n }\n}\n\n// ── config.json ────────────────────────────────────────────────────\n\n/** Migrate a legacy calendar-quarter `fiscalYearEnd` token (\"Q1\"..\"Q4\")\n * to its closing-month number in memory so every downstream consumer\n * (reports, time-series, the UI selects) sees one shape. Absent stays\n * absent — the field is optional and resolves to the default on read;\n * we don't stamp an explicit December onto a book that never chose one.\n * Nothing is written back here (no auto-migrate on disk). */\nfunction normalizeBookFiscalYearEnd(book: BookSummary): BookSummary {\n if (book.fiscalYearEnd === undefined) return book;\n const resolved = resolveFiscalYearEnd(book.fiscalYearEnd);\n return book.fiscalYearEnd === resolved ? book : { ...book, fiscalYearEnd: resolved };\n}\n\nexport async function readConfig(workspaceRoot?: string): Promise<AccountingConfig | null> {\n const config = await readJsonStrict<AccountingConfig>(configPath(workspaceRoot));\n if (!config) return null;\n return { ...config, books: config.books.map(normalizeBookFiscalYearEnd) };\n}\n\nexport async function writeConfig(config: AccountingConfig, workspaceRoot?: string): Promise<void> {\n await writeJsonAtomic(configPath(workspaceRoot), config);\n}\n\n// ── accounts.json ──────────────────────────────────────────────────\n\nexport async function readAccounts(bookId: string, workspaceRoot?: string): Promise<Account[]> {\n const accounts = await readJsonStrict<Account[]>(accountsPath(bookId, workspaceRoot));\n return accounts ?? [];\n}\n\nexport async function writeAccounts(bookId: string, accounts: Account[], workspaceRoot?: string): Promise<void> {\n await writeJsonAtomic(accountsPath(bookId, workspaceRoot), accounts);\n}\n\n// ── journal/YYYY-MM.jsonl (append-only) ────────────────────────────\n\n/** Convert a YYYY-MM-DD date string to its YYYY-MM month bucket. The\n * month bucket dictates which JSONL file the entry lives in. */\nexport function periodFromDate(date: string): string {\n // YYYY-MM-DD → YYYY-MM. Validate the prefix shape so a malformed\n // input fails early instead of silently bucketing into \"1970-01\"\n // or similar.\n if (!/^\\d{4}-\\d{2}-\\d{2}$/.test(date)) {\n throw new Error(`accounting: invalid date format ${JSON.stringify(date)} (expected YYYY-MM-DD)`);\n }\n return date.slice(0, 7);\n}\n\n/** Append one entry to the appropriate month's JSONL.\n *\n * Uses POSIX append-only semantics (`fs.appendFile` → `O_APPEND`).\n * Two concurrent callers landing in the same month file are\n * serialised by the kernel — neither overwrites the other, which\n * is the bug the previous read-modify-write implementation had.\n *\n * Crash mid-write: an entry shorter than `PIPE_BUF` (≥ 512 bytes\n * on every supported platform) writes atomically; a single\n * serialised `JournalEntry` is comfortably under that. If the\n * process is killed during the syscall the worst case is a torn\n * trailing line, which `readJournalMonth` already tolerates by\n * skipping unparseable lines and surfacing a `skipped` count to\n * the caller. */\nexport async function appendJournal(bookId: string, entry: JournalEntry, workspaceRoot?: string): Promise<void> {\n const period = periodFromDate(entry.date);\n const file = journalFileFor(bookId, period, workspaceRoot);\n await fsPromises.mkdir(path.dirname(file), { recursive: true });\n await fsPromises.appendFile(file, `${JSON.stringify(entry)}\\n`, { encoding: \"utf-8\" });\n}\n\nfunction groupEntriesByPeriod(entries: readonly JournalEntry[]): Map<string, JournalEntry[]> {\n const byPeriod = new Map<string, JournalEntry[]>();\n for (const entry of entries) {\n const period = periodFromDate(entry.date);\n const list = byPeriod.get(period) ?? [];\n list.push(entry);\n byPeriod.set(period, list);\n }\n return byPeriod;\n}\n\n/** Append a batch of entries: same-period entries are concatenated\n * into one `appendFile` call so the whole same-period chunk hits\n * the kernel as a single `O_APPEND` write — small chunks (under\n * `PIPE_BUF`, ≥ 512 bytes on every supported platform) are\n * guaranteed atomic by POSIX, and `O_APPEND` serialises with any\n * concurrent appender (a parallel `appendJournal` / `addEntries`\n * call can never overwrite our write or vice versa). Cross-period\n * batches loop one append per period; each is independently\n * concurrency-safe but their union is not transactional across\n * files (out of scope for the append-only JSONL design). */\nexport async function appendJournalBatch(bookId: string, entries: readonly JournalEntry[], workspaceRoot?: string): Promise<void> {\n if (entries.length === 0) return;\n const byPeriod = groupEntriesByPeriod(entries);\n for (const [period, items] of byPeriod) {\n const file = journalFileFor(bookId, period, workspaceRoot);\n await fsPromises.mkdir(path.dirname(file), { recursive: true });\n const chunk = items.map((entry) => `${JSON.stringify(entry)}\\n`).join(\"\");\n await fsPromises.appendFile(file, chunk, { encoding: \"utf-8\" });\n }\n}\n\n/** One JSONL line, or null when it isn't a journal entry — unparseable JSON\n * and JSON of the wrong shape are the same failure to a caller that can only\n * skip the line. */\nfunction parseJournalLine(line: string): JournalEntry | null {\n try {\n const parsed: unknown = JSON.parse(line);\n return isJournalEntry(parsed) ? parsed : null;\n } catch {\n return null;\n }\n}\n\n/** Read a single month's JSONL. Malformed lines are skipped (logged\n * by the caller; this layer just returns the parseable subset) so\n * one bad line doesn't lock the user out of their book. */\nexport async function readJournalMonth(bookId: string, period: string, workspaceRoot?: string): Promise<{ entries: JournalEntry[]; skipped: number }> {\n const file = journalFileFor(bookId, period, workspaceRoot);\n let raw: string;\n try {\n raw = await fsPromises.readFile(file, \"utf-8\");\n } catch (err) {\n if (isEnoent(err)) return { entries: [], skipped: 0 };\n throw err;\n }\n const parsed = raw\n .split(\"\\n\")\n .filter((line) => line.trim() !== \"\")\n .map(parseJournalLine);\n const entries = parsed.filter((entry): entry is JournalEntry => entry !== null);\n return { entries, skipped: parsed.length - entries.length };\n}\n\n/** List the YYYY-MM periods that have a journal file on disk, sorted\n * ascending. Useful for full-history scans (rebuilding snapshots\n * from scratch). */\nexport async function listJournalPeriods(bookId: string, workspaceRoot?: string): Promise<string[]> {\n let names: string[];\n try {\n names = await fsPromises.readdir(journalDir(bookId, workspaceRoot));\n } catch (err) {\n if (isEnoent(err)) return [];\n throw err;\n }\n return names\n .filter((name) => /^\\d{4}-\\d{2}\\.jsonl$/.test(name))\n .map((name) => name.slice(0, 7))\n .sort();\n}\n\n// ── snapshots/YYYY-MM.json (cache, not source of truth) ────────────\n\nexport async function readSnapshot(bookId: string, period: string, workspaceRoot?: string): Promise<MonthSnapshot | null> {\n return readJsonStrict<MonthSnapshot>(snapshotFileFor(bookId, period, workspaceRoot));\n}\n\nexport async function writeSnapshot(bookId: string, snapshot: MonthSnapshot, workspaceRoot?: string): Promise<void> {\n const file = snapshotFileFor(bookId, snapshot.period, workspaceRoot);\n await fsPromises.mkdir(path.dirname(file), { recursive: true });\n // `uniqueTmp` guards against the lazy fallback in `getOrBuildSnapshot`\n // racing with the background rebuild — both can land in\n // `writeSnapshot` for the same period at once. Distinct tmp file\n // names mean each writer renames its own; the destination\n // overwrites idempotently (both derive from the same journal).\n await writeJsonAtomic(file, snapshot, { uniqueTmp: true });\n}\n\n/** Drop snapshot files for all periods >= `fromPeriod`. The next\n * read regenerates them. Idempotent: missing files are silently\n * ignored. */\nexport async function invalidateSnapshotsFrom(bookId: string, fromPeriod: string, workspaceRoot?: string): Promise<{ removed: string[] }> {\n let names: string[];\n try {\n names = await fsPromises.readdir(snapshotsDir(bookId, workspaceRoot));\n } catch (err) {\n if (isEnoent(err)) return { removed: [] };\n throw err;\n }\n const removed: string[] = [];\n for (const name of names) {\n const match = /^(\\d{4}-\\d{2})\\.json$/.exec(name);\n if (!match) continue;\n const [, period] = match;\n if (period === undefined) continue;\n if (period >= fromPeriod) {\n await fsPromises.rm(path.join(snapshotsDir(bookId, workspaceRoot), name), { force: true });\n removed.push(period);\n }\n }\n return { removed: removed.sort() };\n}\n\n/** Drop ALL snapshots for a book — used by `rebuildSnapshots()`\n * with no `from`. Equivalent to `invalidateSnapshotsFrom(\"0000-00\")`\n * but reads more clearly at call sites. */\nexport async function invalidateAllSnapshots(bookId: string, workspaceRoot?: string): Promise<{ removed: string[] }> {\n return invalidateSnapshotsFrom(bookId, \"0000-00\", workspaceRoot);\n}\n\n// ── book directory housekeeping ────────────────────────────────────\n\nexport async function bookExists(bookId: string, workspaceRoot?: string): Promise<boolean> {\n return fileExists(bookRoot(bookId, workspaceRoot));\n}\n\nexport async function ensureBookDir(bookId: string, workspaceRoot?: string): Promise<void> {\n await fsPromises.mkdir(bookRoot(bookId, workspaceRoot), { recursive: true });\n await fsPromises.mkdir(journalDir(bookId, workspaceRoot), { recursive: true });\n await fsPromises.mkdir(snapshotsDir(bookId, workspaceRoot), { recursive: true });\n}\n\n/** Recursively delete a book's directory. Used by `deleteBook` after\n * the config has been updated to drop the entry. */\nexport async function removeBookDir(bookId: string, workspaceRoot?: string): Promise<void> {\n await fsPromises.rm(bookRoot(bookId, workspaceRoot), { recursive: true, force: true });\n}\n","// Opening balance (\"year-start B/S\") logic. Adoption flow: a user\n// migrating from another bookkeeping system enters their existing\n// asset / liability / equity balances as of a chosen `asOfDate`,\n// instead of replaying their entire historical journal.\n//\n// Stored as a single `kind: \"opening\"` entry in the regular journal\n// — keeps the journal as the single source of truth, and makes\n// reports treat the opening as just an early entry without special\n// branches in aggregation.\n//\n// Replacing an existing opening: void the old, append the new. The\n// route handler is responsible for ordering this with snapshot\n// invalidation so the \"before\" snapshots get dropped.\n\nimport { isUnknownArray } from \"@mulmoclaude/common\";\n\nimport type { Account, JournalEntry, JournalLine } from \"../shared/types.js\";\nimport { BALANCE_SHEET_ACCOUNT_TYPES } from \"../shared/types.js\";\nimport { checkBalances, isValidCalendarDate, parseJournalLine, voidedIdSet } from \"./journal.js\";\n\nexport interface OpeningValidationError {\n field: string;\n message: string;\n}\n\nexport type OpeningParseResult = { ok: true; lines: JournalLine[] } | { ok: false; errors: OpeningValidationError[] };\n\n/** Find the existing opening entry for a book, if any. Multiple\n * openings shouldn't coexist (the route enforces void-then-append),\n * but if they do the most recent by `createdAt` wins so callers\n * always see one canonical opening. */\nexport function findActiveOpening(entries: readonly JournalEntry[]): JournalEntry | null {\n const voided = voidedIdSet(entries);\n let active: JournalEntry | null = null;\n for (const entry of entries) {\n if (entry.kind !== \"opening\") continue;\n if (voided.has(entry.id)) continue;\n if (!active || entry.createdAt > active.createdAt) active = entry;\n }\n return active;\n}\n\ninterface OpeningParseInput {\n asOfDate: string;\n lines: unknown;\n accounts: readonly Account[];\n existingEntries: readonly JournalEntry[];\n}\n\nfunction validateOpeningAccount(line: JournalLine, idx: number, accountByCode: ReadonlyMap<string, Account>, errors: OpeningValidationError[]): void {\n const acct = accountByCode.get(line.accountCode);\n if (!acct) {\n errors.push({ field: `lines[${idx}].accountCode`, message: `unknown account code ${JSON.stringify(line.accountCode)}` });\n return;\n }\n if (!BALANCE_SHEET_ACCOUNT_TYPES.includes(acct.type)) {\n errors.push({\n field: `lines[${idx}].accountCode`,\n message: `account ${acct.code} is type ${acct.type}; opening balances may only reference balance-sheet accounts (asset / liability / equity)`,\n });\n }\n}\n\n/** Opening lines are narrowed by the same shape parser the journal\n * uses, but they answer to different rules afterwards: balance-sheet\n * accounts only, and no \"exactly one side\" requirement — the opening\n * form lets a user carry both columns on one account. */\nfunction parseOpeningLines(raw: readonly unknown[], accounts: readonly Account[], errors: OpeningValidationError[]): JournalLine[] {\n const accountByCode = new Map(accounts.map((account) => [account.code, account]));\n const lines: JournalLine[] = [];\n raw.forEach((rawLine, idx) => {\n const line = parseJournalLine(rawLine, idx, errors);\n if (line === null) return;\n validateOpeningAccount(line, idx, accountByCode, errors);\n lines.push(line);\n });\n return lines;\n}\n\nfunction validateAsOfPredatesEverything(input: { asOfDate: string; existingEntries: readonly JournalEntry[] }, errors: OpeningValidationError[]): void {\n // The point of the rule is \"you can't enter an opening dated\n // 2026-01-01 if you've already booked transactions in December\n // 2025\" — that would silently change the meaning of those\n // December transactions. Existing openings (about to be\n // replaced) and already-voided entries are exempt.\n const voided = voidedIdSet(input.existingEntries);\n for (const entry of input.existingEntries) {\n if (entry.kind === \"opening\") continue;\n if (entry.kind === \"void-marker\") continue;\n if (voided.has(entry.id)) continue;\n if (entry.date < input.asOfDate) {\n errors.push({\n field: \"asOfDate\",\n message: `cannot set opening as of ${input.asOfDate}: existing entry ${entry.id} dated ${entry.date} is older. Void it first or pick an earlier asOfDate.`,\n });\n break; // one error is enough — listing every conflicting entry would be noisy\n }\n }\n}\n\n/** Parse the inputs for `setOpeningBalances`, returning the narrowed\n * lines so the caller can persist what was actually checked. Caller\n * passes the full list of journal entries in the book so we can check\n * the \"asOfDate must precede every other entry\" rule. An opening with\n * zero lines is accepted as a no-op marker — it satisfies the\n * \"book has an opening\" gate the UI uses without committing the\n * user to specific balances on day one (they can replace it\n * later). */\nexport function parseOpening(input: OpeningParseInput): OpeningParseResult {\n const errors: OpeningValidationError[] = [];\n if (!isValidCalendarDate(input.asOfDate)) {\n errors.push({ field: \"asOfDate\", message: `expected YYYY-MM-DD calendar date, got ${JSON.stringify(input.asOfDate)}` });\n }\n if (!isUnknownArray(input.lines)) {\n errors.push({ field: \"lines\", message: \"lines must be an array\" });\n return { ok: false, errors };\n }\n const lines = parseOpeningLines(input.lines, input.accounts, errors);\n checkBalances(lines, input.lines.length, \"opening\", errors);\n validateAsOfPredatesEverything(input, errors);\n if (errors.length > 0) return { ok: false, errors };\n return { ok: true, lines };\n}\n","// Pure parsing + normalization for the Account record: `parseAccountInput`\n// narrows a wire payload to an `Account`, `normalizeStoredAccount` decides\n// what of it reaches disk. Both are pure, so unit tests exercise the\n// shape rules + the field-whitelist + the active-flag policy without\n// spinning up a file system, and the service-layer `upsertAccount` is\n// left with only its file-IO and snapshot-invalidation orchestration.\n//\n// Policy summary (mirrored in the `upsertAccount` JSDoc):\n// - whitelist: only `code`, `name`, `type`, optional `note`, and\n// `active` are persisted. Unknown keys from a mistyped caller\n// are dropped — this includes the now-removed\n// `tracksTaxRegistration` flag from older books, which is\n// silently sloughed off the next time an account is upserted.\n// - `note`: stored only when a non-empty trimmed string. An\n// empty string is treated the same as omitted.\n// - `active`:\n// explicit `false` → store `false` (deactivate)\n// explicit `true` → omit (reactivate; default-active)\n// omitted → inherit from `existing` (preserves\n// a soft-deleted account when a caller\n// updates name/type/note without\n// mentioning the active flag — the bug\n// coverage that prompted this helper)\n\nimport { isRecord } from \"@mulmoclaude/common\";\n\nimport type { Account, AccountType } from \"../shared/types.js\";\nimport { ACCOUNT_TYPES } from \"../shared/types.js\";\n\nexport type AccountParseResult = { ok: true; account: Account } | { ok: false; message: string };\n\nfunction isAccountType(value: unknown): value is AccountType {\n return ACCOUNT_TYPES.some((accountType) => accountType === value);\n}\n\n/** Narrow a wire payload to an `Account`. `name` and `type` are as\n * required as `code`: an account persisted without a type is invisible\n * to every report, which groups rows by it. */\nexport function parseAccountInput(raw: unknown): AccountParseResult {\n if (!isRecord(raw)) return { ok: false, message: \"account is required — pass an object with code, name, and type\" };\n const { code, name, type, note, active } = raw;\n if (typeof code !== \"string\" || code.length === 0) return { ok: false, message: \"account code is required\" };\n if (typeof name !== \"string\" || name.trim() === \"\") return { ok: false, message: \"account name is required\" };\n if (!isAccountType(type)) return { ok: false, message: `account type ${JSON.stringify(type)} is invalid — must be one of: ${ACCOUNT_TYPES.join(\", \")}` };\n if (note !== undefined && typeof note !== \"string\") return { ok: false, message: \"account note must be a string when supplied\" };\n if (active !== undefined && typeof active !== \"boolean\") return { ok: false, message: \"account active must be a boolean when supplied\" };\n const account: Account = { code, name, type };\n if (typeof note === \"string\") account.note = note;\n if (typeof active === \"boolean\") account.active = active;\n return { ok: true, account };\n}\n\nexport function normalizeStoredAccount(input: Account, existing?: Account): Account {\n const stored: Account = { code: input.code, name: input.name, type: input.type };\n if (typeof input.note === \"string\" && input.note.length > 0) stored.note = input.note;\n const inheritInactive = input.active === undefined && existing?.active === false;\n if (input.active === false || inheritInactive) stored.active = false;\n return stored;\n}\n","// Aggregation: balance sheet, profit & loss, and ledger from journal\n// entries. Pure — feeds on the entries and accounts caller has\n// already loaded. Snapshot-aware aggregation is layered on top in\n// `snapshotCache.ts`, which calls into here.\n\nimport type { Account, AccountBalance, AccountType, BalanceSheet, BalanceSheetSection, JournalEntry, Ledger, LedgerRow, ProfitLoss } from \"../shared/types.js\";\n\nconst ZERO_TOLERANCE = 0.0049; // hide rows that round to 0 at 2dp\n\n/** Collapse an accountCode → netDebit map into the sorted\n * `AccountBalance[]` wire shape. Shared by `aggregateBalances` and\n * the snapshot cache's `mergeBalances` so the two produce identical,\n * deterministically ordered output. */\nexport function sortedBalancesFromMap(balanceByCode: ReadonlyMap<string, number>): AccountBalance[] {\n return Array.from(balanceByCode.entries())\n .map(([accountCode, netDebit]) => ({ accountCode, netDebit }))\n .sort((lhs, rhs) => lhs.accountCode.localeCompare(rhs.accountCode));\n}\n\n/** Returns net (debit − credit) per account across the supplied\n * entries. Voids work by having an original + reverse pair that\n * cancel mathematically — both are included in aggregation (their\n * contributions sum to zero). The `void-marker` entries carry no\n * lines, so excluding them is just a formality.\n *\n * Why not \"exclude original via voidedIdSet\"? Because then the\n * reverse half would remain unmatched, and the net would be the\n * original's amount with the wrong sign. Letting the math cancel\n * naturally is simpler and impossible to get wrong. */\nexport function aggregateBalances(entries: readonly JournalEntry[]): AccountBalance[] {\n const map = new Map<string, number>();\n for (const entry of entries) {\n if (entry.kind === \"void-marker\") continue;\n for (const line of entry.lines) {\n const cur = map.get(line.accountCode) ?? 0;\n const debit = line.debit ?? 0;\n const credit = line.credit ?? 0;\n map.set(line.accountCode, cur + debit - credit);\n }\n }\n return sortedBalancesFromMap(map);\n}\n\nfunction naturalSign(type: AccountType, netDebit: number): number {\n // Assets / expenses are debit-positive (positive netDebit reads\n // as a positive presentation balance). Liabilities / equity /\n // income are credit-positive — flip the sign for display.\n if (type === \"asset\" || type === \"expense\") return netDebit;\n return -netDebit;\n}\n\n/** Sentinel `accountCode` for the synthetic \"Current period\n * earnings\" row added to the Equity section by `buildBalanceSheet`.\n * The View detects this code and substitutes a localised label\n * for the fixed English fallback. */\nexport const CURRENT_EARNINGS_ACCOUNT_CODE = \"_currentEarnings\";\n\nfunction computeCurrentEarnings(accounts: readonly Account[], balanceByCode: ReadonlyMap<string, number>): number {\n // Σ income − Σ expense, in natural-sign presentation. Without\n // this synthetic Equity row the B/S would be off by exactly net\n // income during the period, because closing entries that fold\n // income/expense into Retained Earnings haven't been booked yet.\n let earnings = 0;\n for (const account of accounts) {\n if (account.type !== \"income\" && account.type !== \"expense\") continue;\n const presented = naturalSign(account.type, balanceByCode.get(account.code) ?? 0);\n earnings += account.type === \"income\" ? presented : -presented;\n }\n return earnings;\n}\n\nfunction buildBalanceSheetSection(\n type: AccountType,\n accounts: readonly Account[],\n balanceByCode: ReadonlyMap<string, number>,\n currentEarnings: number,\n): BalanceSheetSection {\n const rows: BalanceSheetSection[\"rows\"] = accounts\n .filter((account) => account.type === type)\n .map((account) => ({\n accountCode: account.code,\n accountName: account.name,\n balance: naturalSign(type, balanceByCode.get(account.code) ?? 0),\n }))\n .filter((row) => Math.abs(row.balance) > ZERO_TOLERANCE);\n if (type === \"equity\" && Math.abs(currentEarnings) > ZERO_TOLERANCE) {\n rows.push({ accountCode: CURRENT_EARNINGS_ACCOUNT_CODE, accountName: \"Current period earnings\", balance: currentEarnings });\n }\n return { type, rows, total: rows.reduce((sum, row) => sum + row.balance, 0) };\n}\n\nexport function buildBalanceSheet(input: { accounts: readonly Account[]; balances: readonly AccountBalance[]; asOf: string }): BalanceSheet {\n const balanceByCode = new Map(input.balances.map((row) => [row.accountCode, row.netDebit]));\n const currentEarnings = computeCurrentEarnings(input.accounts, balanceByCode);\n // Named rather than indexed out of an array so the accounting-equation\n // arithmetic below can't silently read a section that isn't there.\n const section = (type: AccountType) => buildBalanceSheetSection(type, input.accounts, balanceByCode, currentEarnings);\n const assets = section(\"asset\");\n const liabilities = section(\"liability\");\n const equity = section(\"equity\");\n return {\n asOf: input.asOf,\n sections: [assets, liabilities, equity],\n imbalance: assets.total - (liabilities.total + equity.total),\n };\n}\n\nexport function buildProfitLoss(input: { accounts: readonly Account[]; entries: readonly JournalEntry[]; from: string; to: string }): ProfitLoss {\n const inRange = input.entries.filter((entry) => entry.date >= input.from && entry.date <= input.to);\n const balances = aggregateBalances(inRange);\n const balanceByCode = new Map(balances.map((row) => [row.accountCode, row.netDebit]));\n const incomeRows: ProfitLoss[\"income\"][\"rows\"] = [];\n const expenseRows: ProfitLoss[\"expense\"][\"rows\"] = [];\n let incomeTotal = 0;\n let expenseTotal = 0;\n for (const account of input.accounts) {\n const netDebit = balanceByCode.get(account.code) ?? 0;\n const presented = naturalSign(account.type, netDebit);\n if (Math.abs(presented) <= ZERO_TOLERANCE) continue;\n if (account.type === \"income\") {\n incomeRows.push({ accountCode: account.code, accountName: account.name, amount: presented });\n incomeTotal += presented;\n } else if (account.type === \"expense\") {\n expenseRows.push({ accountCode: account.code, accountName: account.name, amount: presented });\n expenseTotal += presented;\n }\n }\n return {\n from: input.from,\n to: input.to,\n income: { rows: incomeRows, total: incomeTotal },\n expense: { rows: expenseRows, total: expenseTotal },\n netIncome: incomeTotal - expenseTotal,\n };\n}\n\ninterface LedgerLineAccumulator {\n rows: LedgerRow[];\n running: number;\n}\n\n/** Concatenate the entry-level memo (the *what-happened*) with the\n * line-level memo (the *why-this-account*) so a per-account ledger\n * view shows both. Without this combine, a Sales Tax Receivable\n * ledger row would show \"仮払消費税 10%\" but lose the originating\n * \"Starbucks Tokyo — coffee\" — and the user can't tell which\n * transaction the row came from. Identity-collapse handles the\n * case where someone set both fields to the same string. */\nfunction combineMemo(entryMemo: string | undefined, lineMemo: string | undefined): string | undefined {\n if (!entryMemo) return lineMemo;\n if (!lineMemo) return entryMemo;\n if (entryMemo === lineMemo) return entryMemo;\n return `${entryMemo} · ${lineMemo}`;\n}\n\nfunction accumulateLedgerEntry(\n entry: JournalEntry,\n accountCode: string,\n fromDate: string | undefined,\n toDate: string | undefined,\n acc: LedgerLineAccumulator,\n): void {\n if (entry.kind === \"void-marker\") return;\n for (const line of entry.lines) {\n if (line.accountCode !== accountCode) continue;\n const debit = line.debit ?? 0;\n const credit = line.credit ?? 0;\n acc.running += debit - credit;\n if (fromDate && entry.date < fromDate) continue;\n if (toDate && entry.date > toDate) continue;\n const row: LedgerRow = {\n entryId: entry.id,\n date: entry.date,\n kind: entry.kind,\n memo: combineMemo(entry.memo, line.memo),\n debit,\n credit,\n runningBalance: acc.running,\n };\n if (line.taxRegistrationId !== undefined) row.taxRegistrationId = line.taxRegistrationId;\n acc.rows.push(row);\n }\n}\n\nexport function buildLedger(input: { account: Account; entries: readonly JournalEntry[]; from?: string | undefined; to?: string | undefined }): Ledger {\n // Same rule as `aggregateBalances`: include original and reverse\n // for the math to cancel; exclude markers. The reverse entry\n // itself carries `kind: \"void\"` so the row is visually\n // distinguishable in the ledger.\n const sorted = [...input.entries].sort((lhs, rhs) => (lhs.date === rhs.date ? lhs.createdAt.localeCompare(rhs.createdAt) : lhs.date.localeCompare(rhs.date)));\n const acc: LedgerLineAccumulator = { rows: [], running: 0 };\n for (const entry of sorted) {\n accumulateLedgerEntry(entry, input.account.code, input.from, input.to, acc);\n }\n return {\n accountCode: input.account.code,\n accountName: input.account.name,\n rows: acc.rows,\n closingBalance: acc.running,\n };\n}\n","// Time-series aggregation for the accounting plugin: bucketise a\n// date range by month / fiscal quarter / fiscal year and roll up a\n// metric (revenue / expense / net income / closing balance of a\n// specific account) into a chart-ready `(label, value)[]` series.\n//\n// LLM-facing only — the in-canvas Accounting `<View>` keeps using\n// `getReport` for its tab-driven UI. `getTimeSeries` exists so a\n// single tool round-trip can answer \"chart my quarterly revenue\n// over the last two years\" without the LLM fanning out N calls and\n// stitching the buckets itself.\n//\n// Pure module: no I/O, no service-layer awareness. Caller hands in\n// the entries / accounts already loaded; we return the points.\n\nimport type { Account, AccountType, JournalEntry } from \"../shared/types.js\";\nimport { aggregateBalances } from \"./report.js\";\nimport {\n fiscalYearEndMonth,\n type FiscalYearEnd,\n TIME_SERIES_GRANULARITIES,\n TIME_SERIES_METRICS,\n type TimeSeriesGranularity,\n type TimeSeriesMetric,\n} from \"../shared\";\n\nexport { TIME_SERIES_GRANULARITIES, TIME_SERIES_METRICS };\nexport type { TimeSeriesGranularity, TimeSeriesMetric };\n\nexport interface Bucket {\n /** Inclusive YYYY-MM-DD lower bound. */\n from: string;\n /** Inclusive YYYY-MM-DD upper bound. */\n to: string;\n /** Chart x-axis label. Format depends on granularity:\n * \"YYYY-MM\" / \"FY{endYear}-Q{1..4}\" / \"FY{endYear}\". For fiscal\n * years that don't align with the calendar year (Q1/Q2/Q3 books)\n * the FY is named by its END calendar year — matches Japanese\n * \"令和7年度\" convention (Apr 2025 - Mar 2026 = FY2026). */\n label: string;\n}\n\nexport interface TimeSeriesPoint {\n label: string;\n from: string;\n to: string;\n /** Single number, natural-sign per metric. Revenue and net income\n * positive when income exceeds expense; expense reported as a\n * positive cost; account balance follows the account's display\n * sign (assets debit-positive, liabilities/equity credit-positive). */\n value: number;\n}\n\n// ── date arithmetic (pure, no Date for parsing/formatting) ─────────\n\nfunction pad2(num: number): string {\n return String(num).padStart(2, \"0\");\n}\n\nfunction fmtYmd(year: number, month: number, day: number): string {\n return `${year}-${pad2(month)}-${pad2(day)}`;\n}\n\ninterface YmdParts {\n year: number;\n month: number;\n day: number;\n}\n\nfunction parseYmd(value: string): YmdParts {\n // Callers validate `YYYY-MM-DD` first, so the defaults are unreachable;\n // they keep a malformed value degrading to NaN (what a non-numeric\n // segment already produces) rather than to `undefined`.\n const [year = Number.NaN, month = Number.NaN, day = Number.NaN] = value.split(\"-\").map((segment) => parseInt(segment, 10));\n return { year, month, day };\n}\n\nfunction lastDayOf(year: number, month: number): number {\n // UTC day 0 of next month = last day of this month, immune to TZ.\n return new Date(Date.UTC(year, month, 0)).getUTCDate();\n}\n\nfunction addDay(date: YmdParts): YmdParts {\n const stepped = new Date(Date.UTC(date.year, date.month - 1, date.day + 1));\n return {\n year: stepped.getUTCFullYear(),\n month: stepped.getUTCMonth() + 1,\n day: stepped.getUTCDate(),\n };\n}\n\n// ── fiscal-year arithmetic (year-month, no Date) ───────────────────\n\ninterface FyAnchor {\n /** Calendar year the fiscal year STARTED in. */\n fyStartYear: number;\n /** Calendar year the fiscal year ENDS in (used for labelling). */\n fyEndYear: number;\n /** First calendar month of the fiscal year (1-based). */\n startMonth: number;\n}\n\nfunction fyAnchorFor(date: YmdParts, end: FiscalYearEnd): FyAnchor {\n const closingMonth = fiscalYearEndMonth(end);\n const startMonth = (closingMonth % 12) + 1; // month after the close\n // FY containing date: started this calendar year if date.month is\n // at-or-after startMonth, else previous calendar year. Q4 books\n // (startMonth = 1) always land in the same calendar year — covered\n // by the same predicate.\n const fyStartYear = date.month >= startMonth ? date.year : date.year - 1;\n const fyEndYear = closingMonth === 12 ? fyStartYear : fyStartYear + 1;\n return { fyStartYear, fyEndYear, startMonth };\n}\n\n// ── per-granularity bucket lookup ──────────────────────────────────\n\nfunction monthBucketContaining(date: YmdParts): Bucket {\n return {\n from: fmtYmd(date.year, date.month, 1),\n to: fmtYmd(date.year, date.month, lastDayOf(date.year, date.month)),\n label: `${date.year}-${pad2(date.month)}`,\n };\n}\n\nfunction quarterBucketContaining(date: YmdParts, end: FiscalYearEnd): Bucket {\n const anchor = fyAnchorFor(date, end);\n const offset = (date.month - anchor.startMonth + 12) % 12; // 0..11\n const qIdx = Math.floor(offset / 3); // 0..3\n // Flat month-index from the calendar epoch makes year rollover\n // arithmetic trivial.\n const startFlat = anchor.fyStartYear * 12 + (anchor.startMonth - 1) + qIdx * 3;\n const endFlat = startFlat + 2;\n const qStartYear = Math.floor(startFlat / 12);\n const qStartMonth = (startFlat % 12) + 1;\n const qEndYear = Math.floor(endFlat / 12);\n const qEndMonth = (endFlat % 12) + 1;\n return {\n from: fmtYmd(qStartYear, qStartMonth, 1),\n to: fmtYmd(qEndYear, qEndMonth, lastDayOf(qEndYear, qEndMonth)),\n label: `FY${anchor.fyEndYear}-Q${qIdx + 1}`,\n };\n}\n\nfunction yearBucketContaining(date: YmdParts, end: FiscalYearEnd): Bucket {\n const anchor = fyAnchorFor(date, end);\n const startFlat = anchor.fyStartYear * 12 + (anchor.startMonth - 1);\n const endFlat = startFlat + 11;\n const yStartYear = Math.floor(startFlat / 12);\n const yStartMonth = (startFlat % 12) + 1;\n const yEndYear = Math.floor(endFlat / 12);\n const yEndMonth = (endFlat % 12) + 1;\n return {\n from: fmtYmd(yStartYear, yStartMonth, 1),\n to: fmtYmd(yEndYear, yEndMonth, lastDayOf(yEndYear, yEndMonth)),\n label: `FY${anchor.fyEndYear}`,\n };\n}\n\nfunction bucketContaining(date: YmdParts, granularity: TimeSeriesGranularity, end: FiscalYearEnd): Bucket {\n if (granularity === \"month\") return monthBucketContaining(date);\n if (granularity === \"quarter\") return quarterBucketContaining(date, end);\n return yearBucketContaining(date, end);\n}\n\n/** Walk inclusive `[from, to]` and return every bucket that overlaps\n * it, ordered ascending by `from`. The first bucket is the one\n * CONTAINING `from` — it can extend earlier than `from`; the last\n * bucket is the one CONTAINING `to` — it can extend past `to`. The\n * caller's response echoes the input range so the LLM can label the\n * chart truthfully (\"Revenue Apr 2025 – Sep 2026\" even though the\n * outermost buckets cover Apr-Jun 2025 and Jul-Sep 2026). */\nexport function bucketize(input: { from: string; to: string; granularity: TimeSeriesGranularity; fiscalYearEnd: FiscalYearEnd }): Bucket[] {\n if (input.from > input.to) return [];\n const start = parseYmd(input.from);\n const result: Bucket[] = [];\n let bucket = bucketContaining(start, input.granularity, input.fiscalYearEnd);\n // Buckets are contiguous; once a bucket's `from` is past `input.to`\n // every subsequent bucket is too.\n while (bucket.from <= input.to) {\n result.push(bucket);\n const next = addDay(parseYmd(bucket.to));\n bucket = bucketContaining(next, input.granularity, input.fiscalYearEnd);\n }\n return result;\n}\n\n// ── value computation ──────────────────────────────────────────────\n\n/** Convert raw netDebit to natural-sign presentation per account\n * type. Mirrors the helper in `report.ts` (kept private there). */\nfunction naturalSign(type: AccountType, netDebit: number): number {\n if (type === \"asset\" || type === \"expense\") return netDebit;\n return -netDebit;\n}\n\ninterface PresentationTotals {\n income: number;\n expense: number;\n}\n\n/** Sum presented income and expense values across the supplied\n * entries. Used for window-based metrics (revenue / expense /\n * netIncome). Opening entries reference B/S accounts only and so\n * contribute zero to either total — including them is harmless. */\nfunction presentedPlTotals(entries: readonly JournalEntry[], accountTypeByCode: ReadonlyMap<string, AccountType>): PresentationTotals {\n const balances = aggregateBalances(entries);\n let income = 0;\n let expense = 0;\n for (const row of balances) {\n const type = accountTypeByCode.get(row.accountCode);\n if (!type) continue;\n if (type === \"income\") income += naturalSign(type, row.netDebit);\n else if (type === \"expense\") expense += naturalSign(type, row.netDebit);\n }\n return { income, expense };\n}\n\nfunction entriesInWindow(entries: readonly JournalEntry[], from: string, toDate: string): JournalEntry[] {\n return entries.filter((entry) => entry.date >= from && entry.date <= toDate);\n}\n\nfunction entriesUpTo(entries: readonly JournalEntry[], toDate: string): JournalEntry[] {\n return entries.filter((entry) => entry.date <= toDate);\n}\n\nfunction valueForBucket(input: {\n bucket: Bucket;\n entries: readonly JournalEntry[];\n accounts: readonly Account[];\n metric: TimeSeriesMetric;\n accountCode?: string | undefined;\n}): number {\n const accountTypeByCode = new Map(input.accounts.map((acct) => [acct.code, acct.type]));\n if (input.metric === \"accountBalance\") {\n const code = input.accountCode;\n if (!code) return 0; // guarded at the route — defensive zero.\n const type = accountTypeByCode.get(code);\n if (!type) return 0;\n const cumulative = entriesUpTo(input.entries, input.bucket.to);\n const balances = aggregateBalances(cumulative);\n const row = balances.find((balance) => balance.accountCode === code);\n return row ? naturalSign(type, row.netDebit) : 0;\n }\n const window = entriesInWindow(input.entries, input.bucket.from, input.bucket.to);\n const totals = presentedPlTotals(window, accountTypeByCode);\n if (input.metric === \"revenue\") return totals.income;\n if (input.metric === \"expense\") return totals.expense;\n return totals.income - totals.expense; // netIncome\n}\n\nexport function buildTimeSeries(input: {\n buckets: readonly Bucket[];\n entries: readonly JournalEntry[];\n accounts: readonly Account[];\n metric: TimeSeriesMetric;\n accountCode?: string | undefined;\n}): TimeSeriesPoint[] {\n return input.buckets.map((bucket) => ({\n label: bucket.label,\n from: bucket.from,\n to: bucket.to,\n value: valueForBucket({\n bucket,\n entries: input.entries,\n accounts: input.accounts,\n metric: input.metric,\n accountCode: input.accountCode,\n }),\n }));\n}\n","// Pub/sub publisher for the accounting plugin. Mirror of\n// `server/events/file-change.ts`: module singleton + init function +\n// fire-and-forget publish helpers. The init wiring lives in\n// `server/index.ts` next to `initFileChangePublisher`.\n//\n// Channel names + payload shapes are imported from\n// `src/config/pubsubChannels.ts` so the publisher cannot drift from\n// the View-side subscribers.\n\nimport {\n bookChannel as accountingBookChannel,\n booksChannel as accountingBooksChannel,\n type BookChannelPayload as AccountingBookChannelPayload,\n} from \"../shared\";\nimport { channelScopeFor, log, type IPubSub } from \"./context.js\";\nimport { errorMessage } from \"../shared\";\n\nlet pubsub: IPubSub | null = null;\n\nexport function initAccountingEventPublisher(instance: IPubSub): void {\n pubsub = instance;\n}\n\n/** `channelFor` is called INSIDE the guard, not before it: resolving the\n * scope can throw (a write that reached the publisher with no root\n * under explicit-root mode), and a lost event with a warning beats\n * either crashing the write or publishing to the unscoped channel that\n * every project can hear. */\nfunction safePublish(channelFor: () => string, payload: unknown): void {\n if (!pubsub) return;\n let channel = \"(unresolved)\";\n try {\n channel = channelFor();\n pubsub.publish(channel, payload);\n } catch (err) {\n // Same fire-and-forget rationale as the file-change publisher:\n // dropping one event is better than crashing the server.\n log.warn(\"accounting\", \"publish failed; subscribers will miss this event\", {\n channel,\n error: errorMessage(err),\n });\n }\n}\n\n/** Per-book change notification. `period` should be the entry's\n * YYYY-MM bucket (or the earliest invalidated month for snapshot\n * events).\n *\n * `workspaceRoot` is the root the write happened under; the channel\n * name is namespaced by the host's opaque scope for it (see\n * `channelScopeFor`). Omit it — as a single-root host's service calls\n * do — and the name is what it has always been. */\nexport function publishBookChange(bookId: string, payload: AccountingBookChannelPayload, workspaceRoot?: string): void {\n safePublish(() => accountingBookChannel(bookId, channelScopeFor(workspaceRoot)), payload);\n}\n\n/** Fired when the *list* of books changes (createBook, deleteBook).\n * Payload is intentionally empty — subscribers refetch from\n * /api/accounting. Scoped by root like `publishBookChange`. */\nexport function publishBooksChanged(workspaceRoot?: string): void {\n safePublish(() => accountingBooksChannel(channelScopeFor(workspaceRoot)), {});\n}\n\n/** Test-only — drop the module singleton so each test starts clean. */\nexport function _resetAccountingEventPublisherForTesting(): void {\n pubsub = null;\n}\n","// Monthly balance snapshot cache.\n//\n// Source of truth: the journal JSONL files. Snapshots are derived\n// state — `data/accounting/books/<id>/snapshots/YYYY-MM.json` is\n// only ever a perf optimization. The invariant we maintain:\n//\n// for any (book, period) pair,\n// getOrBuildSnapshot(book, period)\n// ===\n// aggregateBalances(<all entries up to period end>)\n//\n// I.e. running with snapshots and running without snapshots must\n// produce byte-identical results. The unit test for this lives in\n// `test/accounting/test_snapshotCache.ts`.\n//\n// Rebuild policy: writes call `scheduleRebuild(bookId, fromPeriod)`\n// after invalidating stale snapshot files. Each book has at most one\n// rebuild in flight; additional writes during a running rebuild merge\n// into a single queued follow-up (so a burst of N writes runs at most\n// two rebuilds). `getOrBuildSnapshot` keeps a lazy fallback so a\n// report requested before the rebuild reaches that month is still\n// correct — it just builds inline.\n//\n// Test API: `awaitRebuildIdle(bookId)` and `inspectRebuildQueue(bookId)`\n// are diagnostics that let tests assert on queue state without\n// sleep-and-poll. Production code never needs them.\n\nimport path from \"node:path\";\n\nimport {\n invalidateSnapshotsFrom as ioInvalidateFrom,\n invalidateAllSnapshots as ioInvalidateAll,\n listJournalPeriods,\n readJournalMonth,\n readSnapshot,\n writeSnapshot,\n} from \"./io.js\";\nimport { aggregateBalances, sortedBalancesFromMap } from \"./report.js\";\nimport { publishBookChange } from \"./eventPublisher.js\";\nimport { defaultWorkspaceRoot, log } from \"./context.js\";\nimport { errorMessage, BOOK_EVENT_KINDS as ACCOUNTING_BOOK_EVENT_KINDS } from \"../shared\";\nimport type { MonthSnapshot } from \"./types.js\";\nimport type { AccountBalance, JournalEntry } from \"../shared/types.js\";\n\nfunction previousPeriod(period: string): string {\n // YYYY-MM → previous YYYY-MM. December rolls back to the previous\n // year's December.\n // A malformed period already yields NaN for a non-numeric segment, so\n // a missing segment degrades the same way instead of throwing here.\n const [year = Number.NaN, month = Number.NaN] = period.split(\"-\").map((segment) => parseInt(segment, 10));\n if (month === 1) return `${(year - 1).toString().padStart(4, \"0\")}-12`;\n return `${year.toString().padStart(4, \"0\")}-${(month - 1).toString().padStart(2, \"0\")}`;\n}\n\nfunction mergeBalances(base: readonly AccountBalance[], delta: readonly AccountBalance[]): AccountBalance[] {\n const map = new Map<string, number>();\n for (const row of base) map.set(row.accountCode, row.netDebit);\n for (const row of delta) {\n map.set(row.accountCode, (map.get(row.accountCode) ?? 0) + row.netDebit);\n }\n return sortedBalancesFromMap(map);\n}\n\nasync function buildEmptySnapshot(bookId: string, period: string, workspaceRoot?: string): Promise<MonthSnapshot> {\n const empty: MonthSnapshot = { period, balances: [], builtAt: new Date().toISOString() };\n await writeSnapshot(bookId, empty, workspaceRoot);\n return empty;\n}\n\n/** Build a snapshot at end-of-`period` for one book, lazily relying\n * on the previous month's snapshot if it exists. Falls all the way\n * back to the earliest journal month if no upstream snapshot is\n * available. Always writes the result to disk before returning. */\nexport async function getOrBuildSnapshot(bookId: string, period: string, workspaceRoot?: string): Promise<MonthSnapshot> {\n const cached = await readSnapshot(bookId, period, workspaceRoot);\n if (cached) return cached;\n\n // Earliest journal month determines where the recursion stops.\n // If the book has no journal at all, return an empty snapshot.\n const [earliestPeriod] = await listJournalPeriods(bookId, workspaceRoot);\n if (earliestPeriod === undefined || period < earliestPeriod) {\n return buildEmptySnapshot(bookId, period, workspaceRoot);\n }\n\n const { entries } = await readJournalMonth(bookId, period, workspaceRoot);\n const monthDelta = aggregateBalances(entries);\n\n // Get the prior month's closing snapshot — recurse, which will\n // either hit cache or build the chain back to the start.\n let priorBalances: readonly AccountBalance[] = [];\n if (period > earliestPeriod) {\n const prior = previousPeriod(period);\n const priorSnap = await getOrBuildSnapshot(bookId, prior, workspaceRoot);\n priorBalances = priorSnap.balances;\n }\n const merged = mergeBalances(priorBalances, monthDelta);\n const snap: MonthSnapshot = {\n period,\n balances: merged,\n builtAt: new Date().toISOString(),\n };\n await writeSnapshot(bookId, snap, workspaceRoot);\n return snap;\n}\n\n/** Compute closing balances at end-of-`period` from journal alone,\n * bypassing the snapshot cache. Used by the byte-equality\n * invariant test, and as a safety net for \"compute without\n * trusting cache\" paths. */\nexport async function balancesAtEndOf(bookId: string, period: string, workspaceRoot?: string): Promise<AccountBalance[]> {\n const periods = await listJournalPeriods(bookId, workspaceRoot);\n const all: JournalEntry[] = [];\n for (const monthKey of periods) {\n if (period < monthKey) break;\n const { entries } = await readJournalMonth(bookId, monthKey, workspaceRoot);\n for (const entry of entries) all.push(entry);\n }\n return aggregateBalances(all);\n}\n\n/** Drop snapshots for `fromPeriod` and later. Re-export from\n * accounting-io for callers that conceptually live in the cache\n * layer (so they don't reach into the IO module). */\nexport async function invalidateSnapshotsFrom(bookId: string, fromPeriod: string, workspaceRoot?: string): Promise<{ removed: string[] }> {\n return ioInvalidateFrom(bookId, fromPeriod, workspaceRoot);\n}\n\n/** Drop all snapshots and rebuild from scratch. Used by the\n * `rebuildSnapshots` admin action. Returns the periods that were\n * rebuilt. */\nexport async function rebuildAllSnapshots(bookId: string, workspaceRoot?: string): Promise<{ rebuilt: string[] }> {\n await ioInvalidateAll(bookId, workspaceRoot);\n const periods = await listJournalPeriods(bookId, workspaceRoot);\n for (const monthKey of periods) {\n await getOrBuildSnapshot(bookId, monthKey, workspaceRoot);\n }\n return { rebuilt: periods };\n}\n\n// ── async rebuild queue ────────────────────────────────────────────\n//\n// Per-book queue. A `running` promise represents the in-flight\n// rebuild. While it's running, additional `scheduleRebuild` calls\n// merge their `fromPeriod` into `pendingFromPeriod` (taking the\n// minimum so the next pass covers everyone's invalidation), and we\n// kick off a follow-up once the current one resolves.\n\ninterface RebuildQueueEntry {\n running: Promise<void>;\n pendingFromPeriod: string | null;\n pendingWorkspaceRoot: string | undefined;\n coalescedWriteCount: number;\n runningFromPeriod: string;\n /** Set by `cancelRebuild` (called from `deleteBook`). The runRebuild\n * loop checks before each write so a rebuild cannot resurrect the\n * book directory after `removeBookDir` has run. */\n cancelled: boolean;\n}\n\n// Keyed by (root, bookId), not by bookId. A bookId is unique WITHIN a\n// root and nowhere else, so a multi-root host (MulmoTerminal, one root\n// per project directory) running two projects that both hold a book\n// called `main` would otherwise share one queue: project B's write\n// would cancel project A's rebuild, and `awaitRebuildIdle` would\n// return while the other project's rebuild is still writing.\nconst rebuildQueues = new Map<string, RebuildQueueEntry>();\n\n/** Canonical key for the root a rebuild belongs to. An absent root\n * resolves to the host's configured one, so a call that passes it\n * explicitly and one that relies on the default share a queue. Under\n * explicit-root mode there is no default and `defaultWorkspaceRoot()`\n * throws — deliberately loud: a `cancelRebuild` / `awaitRebuildIdle`\n * that quietly keyed a queue nobody scheduled would return while the\n * real rebuild is still writing, which is how `deleteBook` races a\n * `writeSnapshot` back into existence.\n *\n * Lexical resolution only — symlinks are deliberately NOT resolved,\n * matching `canonicalRoot` in `@mulmoclaude/core`'s collection engine,\n * so one project has one identity across both packages. A host that\n * hands the engine a real path in one call and a symlink alias in the\n * next would split the queue; the fix belongs in the host's own root\n * resolution, which is where the two packages agree it lives. */\nfunction rootKey(workspaceRoot: string | undefined): string {\n return path.resolve(workspaceRoot ?? defaultWorkspaceRoot());\n}\n\nfunction queueKey(bookId: string, workspaceRoot: string | undefined): string {\n // NUL cannot appear in either half (a book id is `[A-Za-z0-9_-]`,\n // a path cannot contain it), so the join is unambiguous.\n return `${rootKey(workspaceRoot)}\\u0000${bookId}`;\n}\n\nfunction minPeriod(lhs: string | null, rhs: string): string {\n if (lhs === null) return rhs;\n return lhs < rhs ? lhs : rhs;\n}\n\nfunction isInvalidatedDuringRebuild(key: string, period: string): boolean {\n // A pending invalidation that covers `period` means the queued\n // follow-up rebuild will redo this period. Skip the in-flight\n // write so we don't pollute the cache with stale data while a\n // fresher computation is queued. Without this guard, the\n // sequence \"rebuild reads journal → caller writes a new entry →\n // caller invalidates → rebuild writes (stale) snapshot\" leaves\n // the cache lying about the latest state.\n const queue = rebuildQueues.get(key);\n return queue !== undefined && queue.pendingFromPeriod !== null && period >= queue.pendingFromPeriod;\n}\n\nfunction isCancelled(key: string): boolean {\n return rebuildQueues.get(key)?.cancelled === true;\n}\n\nasync function runRebuild(key: string, bookId: string, fromPeriod: string, workspaceRoot: string | undefined): Promise<void> {\n const startedAt = Date.now();\n log.info(\"accounting\", \"snapshot rebuild started\", { bookId, fromPeriod });\n publishBookChange(bookId, { kind: ACCOUNTING_BOOK_EVENT_KINDS.snapshotsRebuilding, period: fromPeriod }, workspaceRoot);\n const periods = await listJournalPeriods(bookId, workspaceRoot);\n const targets = periods.filter((monthKey) => monthKey >= fromPeriod);\n let written = 0;\n for (const monthKey of targets) {\n if (isCancelled(key)) break;\n if (isInvalidatedDuringRebuild(key, monthKey)) break;\n // Compute fresh from journal — bypasses getOrBuildSnapshot's\n // own write side-effect so the staleness check below is the\n // only writer in the rebuild path.\n const balances = await balancesAtEndOf(bookId, monthKey, workspaceRoot);\n if (isCancelled(key)) break;\n if (isInvalidatedDuringRebuild(key, monthKey)) break;\n await writeSnapshot(bookId, { period: monthKey, balances, builtAt: new Date().toISOString() }, workspaceRoot);\n if (isCancelled(key)) {\n // The book was deleted between our last check and the write —\n // `writeSnapshot` will have re-created the book directory tree\n // via mkdir-recursive. Undo it so we don't leave an orphaned\n // directory after `deleteBook` has run.\n await ioInvalidateFrom(bookId, monthKey, workspaceRoot);\n break;\n }\n if (isInvalidatedDuringRebuild(key, monthKey)) {\n // A concurrent invalidate raced ahead between our last check\n // and the disk write. The data we just wrote may be stale\n // relative to the latest journal — undo so the queued\n // follow-up rebuild starts from a clean slate.\n await ioInvalidateFrom(bookId, monthKey, workspaceRoot);\n break;\n }\n written += 1;\n publishBookChange(bookId, { kind: ACCOUNTING_BOOK_EVENT_KINDS.snapshotsReady, period: monthKey }, workspaceRoot);\n }\n log.info(\"accounting\", \"snapshot rebuild done\", { bookId, periods: written, durationMs: Date.now() - startedAt });\n}\n\nfunction startRebuild(key: string, bookId: string, fromPeriod: string, workspaceRoot: string | undefined): RebuildQueueEntry {\n const entry: RebuildQueueEntry = {\n running: Promise.resolve(),\n pendingFromPeriod: null,\n pendingWorkspaceRoot: undefined,\n coalescedWriteCount: 1,\n runningFromPeriod: fromPeriod,\n cancelled: false,\n };\n entry.running = runRebuild(key, bookId, fromPeriod, workspaceRoot)\n .catch((err) => {\n // A rebuild failure is logged but does not poison the queue —\n // the next `scheduleRebuild` call will start a fresh promise.\n log.error(\"accounting\", \"snapshot rebuild failed\", { bookId, fromPeriod, error: errorMessage(err) });\n })\n .then(() => {\n // Drain any work that piled up while we were running.\n const current = rebuildQueues.get(key);\n if (!current) return;\n // If the book was cancelled mid-rebuild (e.g. deleteBook ran),\n // drop the queue entry entirely — we must not start a successor\n // that would re-create the deleted book directory.\n if (current.cancelled) {\n rebuildQueues.delete(key);\n return;\n }\n if (current.pendingFromPeriod !== null) {\n const nextFrom = current.pendingFromPeriod;\n const nextRoot = current.pendingWorkspaceRoot;\n const carriedCount = current.coalescedWriteCount;\n const successor = startRebuild(key, bookId, nextFrom, nextRoot);\n successor.coalescedWriteCount += carriedCount;\n rebuildQueues.set(key, successor);\n } else {\n rebuildQueues.delete(key);\n }\n });\n return entry;\n}\n\n/** Schedule a background rebuild for `bookId` starting at `fromPeriod`.\n * Multiple calls during an in-flight rebuild coalesce into a single\n * follow-up rebuild that covers the minimum `fromPeriod` seen.\n * Returns immediately — the rebuild runs on its own promise chain. */\nexport function scheduleRebuild(bookId: string, fromPeriod: string, workspaceRoot?: string): void {\n const key = queueKey(bookId, workspaceRoot);\n const existing = rebuildQueues.get(key);\n if (!existing) {\n rebuildQueues.set(key, startRebuild(key, bookId, fromPeriod, workspaceRoot));\n return;\n }\n existing.pendingFromPeriod = minPeriod(existing.pendingFromPeriod, fromPeriod);\n existing.pendingWorkspaceRoot = workspaceRoot;\n existing.coalescedWriteCount += 1;\n}\n\n/** Test/diagnostic: resolves when no rebuild is running or queued for\n * `bookId`. Also called by `deleteBook` after `cancelRebuild` to\n * ensure a previously running rebuild has fully stopped before the\n * caller removes the book's directory on disk. */\nexport async function awaitRebuildIdle(bookId: string, workspaceRoot?: string): Promise<void> {\n const key = queueKey(bookId, workspaceRoot);\n while (rebuildQueues.has(key)) {\n const entry = rebuildQueues.get(key);\n if (!entry) return;\n await entry.running;\n }\n}\n\n/** Mark the book's in-flight rebuild as cancelled. The runRebuild\n * loop checks before each write and bails out, so a subsequent\n * `removeBookDir` cannot race with a `writeSnapshot` that would\n * re-create the directory tree. Pair with `awaitRebuildIdle(bookId)`\n * to wait for the in-flight rebuild to finish bailing. */\nexport function cancelRebuild(bookId: string, workspaceRoot?: string): void {\n const entry = rebuildQueues.get(queueKey(bookId, workspaceRoot));\n if (!entry) return;\n entry.cancelled = true;\n // Drop pending too — a cancelled book should not get a successor\n // rebuild after the in-flight one drains.\n entry.pendingFromPeriod = null;\n}\n\n/** Test/diagnostic: snapshot of the per-book queue state. Stable\n * enough to assert against; fields may grow over time. */\nexport function inspectRebuildQueue(\n bookId: string,\n workspaceRoot?: string,\n): {\n running: boolean;\n runningFromPeriod: string | null;\n pendingFromPeriod: string | null;\n coalescedWriteCount: number;\n} {\n const entry = rebuildQueues.get(queueKey(bookId, workspaceRoot));\n if (!entry) {\n return { running: false, runningFromPeriod: null, pendingFromPeriod: null, coalescedWriteCount: 0 };\n }\n return {\n running: true,\n runningFromPeriod: entry.runningFromPeriod,\n pendingFromPeriod: entry.pendingFromPeriod,\n coalescedWriteCount: entry.coalescedWriteCount,\n };\n}\n\n/** Test-only — drain all in-flight rebuilds, then drop queue state.\n * Awaiting first means a leftover rebuild can't continue writing\n * into the next test's tmp dir after we clear the bookkeeping. */\nexport async function _resetRebuildQueueForTesting(): Promise<void> {\n // Mark everything cancelled so loops bail at their next checkpoint\n // instead of continuing through every period.\n for (const entry of rebuildQueues.values()) {\n entry.cancelled = true;\n entry.pendingFromPeriod = null;\n }\n const pending = Array.from(rebuildQueues.values()).map((entry) => entry.running);\n await Promise.allSettled(pending);\n rebuildQueues.clear();\n}\n","// Default chart of accounts seeded into a freshly created book.\n// The active set is intentionally minimal — covers the common\n// categories users need to record their first opening balance and\n// post their first entries, without overwhelming a brand-new user.\n//\n// A second tier of `active: false` entries is included so the user\n// can flip on common-but-not-universal accounts (Inventory, Travel,\n// Depreciation Expense, …) from Manage Accounts with one click\n// rather than typing them in by hand. Inactive accounts stay\n// hidden from journal entry / ledger dropdowns until the user\n// reactivates them. Tax-related accounts (1400 / 2400) are an\n// exception — they ship active by default since almost every\n// jurisdiction levies a consumption / sales / VAT tax.\n\nimport type { Account } from \"../shared/types.js\";\n\nexport const DEFAULT_ACCOUNTS: readonly Account[] = [\n // Assets\n { code: \"1000\", name: \"Cash\", type: \"asset\" },\n { code: \"1001\", name: \"Petty Cash\", type: \"asset\", active: false },\n { code: \"1010\", name: \"Bank — Checking\", type: \"asset\" },\n { code: \"1020\", name: \"Bank — Savings\", type: \"asset\" },\n { code: \"1100\", name: \"Accounts Receivable\", type: \"asset\" },\n { code: \"1200\", name: \"Inventory\", type: \"asset\", active: false },\n { code: \"1300\", name: \"Prepaid Expenses\", type: \"asset\", active: false },\n // 14xx is the reserved \"tax-related current assets\" band — pairs\n // with 24xx on the liability side for the tax-excluded (税抜)\n // booking method: input tax paid on purchases sits here as an\n // asset and is netted against output-tax collected at filing\n // time. The Ledger view's T-number column and the\n // JournalEntryForm's per-line taxRegistrationId input key off\n // the 14xx prefix only (see `isTaxAccountCode`) — the\n // counterparty registration ID is load-bearing for input-tax\n // credit on purchases, not for the seller-side liability — so\n // any custom 14xx account a user adds participates without an\n // opt-in step. Active by default — most jurisdictions levy a\n // consumption / sales / VAT tax; tax-free contexts can\n // deactivate from Manage Accounts.\n // 1400 was briefly named \"Sales Tax Receivable\" — that label\n // conventionally means *output* tax billed to customers but not\n // yet collected. Renamed to \"Input Tax Receivable\" so the\n // purchase-side meaning matches the 14xx / 24xx booking pair and\n // the non-US naming the rest of the world uses (EU \"Input VAT\" /\n // UK VAT input / Japan 仮払消費税). CodeRabbit review on PR #1120.\n { code: \"1400\", name: \"Input Tax Receivable\", type: \"asset\" },\n { code: \"1500\", name: \"Equipment\", type: \"asset\" },\n { code: \"1510\", name: \"Furniture & Fixtures\", type: \"asset\", active: false },\n { code: \"1520\", name: \"Vehicles\", type: \"asset\", active: false },\n { code: \"1590\", name: \"Accumulated Depreciation\", type: \"asset\", active: false },\n // Liabilities\n { code: \"2000\", name: \"Accounts Payable\", type: \"liability\" },\n { code: \"2100\", name: \"Credit Card\", type: \"liability\" },\n { code: \"2200\", name: \"Loans Payable\", type: \"liability\" },\n { code: \"2300\", name: \"Accrued Expenses\", type: \"liability\", active: false },\n // 24xx is the reserved \"tax-related current liabilities\" band;\n // pairs with 14xx on the asset side. See the 1400 comment above.\n { code: \"2400\", name: \"Sales Tax Payable\", type: \"liability\" },\n { code: \"2500\", name: \"Payroll Liabilities\", type: \"liability\", active: false },\n // Equity\n // Required for opening balances: setOpeningBalances dumps the\n // plug into \"Retained Earnings\" by convention.\n { code: \"3000\", name: \"Owner's Equity\", type: \"equity\" },\n { code: \"3100\", name: \"Retained Earnings\", type: \"equity\" },\n { code: \"3200\", name: \"Owner's Draws\", type: \"equity\", active: false },\n // Income\n { code: \"4000\", name: \"Sales\", type: \"income\" },\n { code: \"4010\", name: \"Service Revenue\", type: \"income\", active: false },\n { code: \"4100\", name: \"Other Income\", type: \"income\" },\n { code: \"4200\", name: \"Interest Income\", type: \"income\", active: false },\n { code: \"4300\", name: \"Sales Returns & Discounts\", type: \"income\", active: false },\n // Expenses\n { code: \"5000\", name: \"Cost of Goods Sold\", type: \"expense\" },\n { code: \"5100\", name: \"Rent\", type: \"expense\" },\n { code: \"5200\", name: \"Utilities\", type: \"expense\" },\n { code: \"5300\", name: \"Salaries\", type: \"expense\" },\n { code: \"5400\", name: \"Office Supplies\", type: \"expense\" },\n { code: \"5500\", name: \"Advertising & Marketing\", type: \"expense\", active: false },\n { code: \"5600\", name: \"Travel\", type: \"expense\", active: false },\n { code: \"5610\", name: \"Meals & Entertainment\", type: \"expense\", active: false },\n { code: \"5700\", name: \"Professional Fees\", type: \"expense\", active: false },\n { code: \"5710\", name: \"Insurance\", type: \"expense\", active: false },\n { code: \"5720\", name: \"Software & Subscriptions\", type: \"expense\", active: false },\n { code: \"5730\", name: \"Bank Fees\", type: \"expense\", active: false },\n { code: \"5800\", name: \"Depreciation Expense\", type: \"expense\", active: false },\n { code: \"5810\", name: \"Taxes\", type: \"expense\", active: false },\n { code: \"5900\", name: \"Miscellaneous Expense\", type: \"expense\" },\n];\n","// Service layer for the accounting plugin. Wraps the IO + domain\n// modules into the handful of operations the route + MCP bridge\n// expose. Each function:\n//\n// - performs validation,\n// - mutates the journal / accounts / config files atomically,\n// - invalidates dependent snapshots,\n// - publishes a pub/sub event so subscribers refetch.\n//\n// Snapshot rebuild policy: writes invalidate stale snapshot files\n// synchronously, then call `scheduleRebuild` to rebuild them in the\n// background. `getOrBuildSnapshot` keeps a lazy fallback so a report\n// requested before the rebuild reaches that month still returns the\n// right number — it just builds inline. Both paths are byte-identical\n// (enforced by `test/accounting/test_snapshotCache.ts`).\n\nimport { randomUUID } from \"node:crypto\";\nimport { isUnknownArray } from \"@mulmoclaude/common\";\n\nimport {\n appendJournal,\n appendJournalBatch,\n bookExists,\n ensureBookDir,\n invalidateAllSnapshots,\n invalidateSnapshotsFrom,\n isSafeBookId,\n listJournalPeriods,\n periodFromDate,\n readAccounts,\n readConfig,\n readJournalMonth,\n removeBookDir,\n writeAccounts,\n writeConfig,\n} from \"./io.js\";\nimport { findActiveOpening, parseOpening } from \"./openingBalances.js\";\nimport { normalizeStoredAccount, parseAccountInput } from \"./accountNormalize.js\";\nimport { isValidCalendarDate, localDateString, makeEntry, makeVoidEntries, parseEntry, voidedIdSet, type ParsedEntry } from \"./journal.js\";\nimport { aggregateBalances, buildBalanceSheet, buildLedger, buildProfitLoss } from \"./report.js\";\nimport {\n bucketize,\n buildTimeSeries,\n TIME_SERIES_GRANULARITIES,\n TIME_SERIES_METRICS,\n type TimeSeriesGranularity,\n type TimeSeriesMetric,\n type TimeSeriesPoint,\n} from \"./timeSeries.js\";\nimport { awaitRebuildIdle, balancesAtEndOf, cancelRebuild, getOrBuildSnapshot, rebuildAllSnapshots, scheduleRebuild } from \"./snapshotCache.js\";\nimport { publishBookChange, publishBooksChanged } from \"./eventPublisher.js\";\nimport { DEFAULT_ACCOUNTS } from \"./defaultAccounts.js\";\nimport { log } from \"./context.js\";\nimport { BOOK_EVENT_KINDS as ACCOUNTING_BOOK_EVENT_KINDS } from \"../shared\";\nimport {\n isSupportedCountryCode,\n SUPPORTED_COUNTRY_CODES,\n type SupportedCountryCode,\n DEFAULT_FISCAL_YEAR_END,\n FISCAL_YEAR_END_MONTHS,\n isFiscalYearEnd,\n resolveFiscalYearEnd,\n type FiscalYearEnd,\n} from \"../shared\";\nimport type { AccountingConfig } from \"./types.js\";\nimport type { Account, AccountType, BookSummary, JournalEntry, ReportPeriod } from \"../shared/types.js\";\n\nexport class AccountingError extends Error {\n constructor(\n public status: number,\n message: string,\n public details?: unknown,\n ) {\n super(message);\n this.name = \"AccountingError\";\n }\n}\n\nconst DEFAULT_CURRENCY = \"USD\";\nconst GENERATED_ID_RETRIES = 8;\n\nfunction emptyConfig(): AccountingConfig {\n return { books: [] };\n}\n\nasync function loadOrInitConfig(workspaceRoot?: string): Promise<AccountingConfig> {\n const cfg = await readConfig(workspaceRoot);\n return cfg ?? emptyConfig();\n}\n\nfunction findBook(config: AccountingConfig, bookId: string): BookSummary | null {\n return config.books.find((book) => book.id === bookId) ?? null;\n}\n\nfunction resolveBookId(config: AccountingConfig, requested: string | undefined): string {\n // Every book-touching action now requires an explicit `bookId` —\n // there's no server-side \"active book\" to fall back on. Callers\n // are the LLM (which is told to pass bookId on each call) and the\n // View (which tracks the current selection in localStorage).\n if (!requested) {\n throw new AccountingError(400, \"bookId is required\");\n }\n if (!findBook(config, requested)) {\n throw new AccountingError(404, `book ${JSON.stringify(requested)} not found`);\n }\n return requested;\n}\n\nasync function generateBookId(config: AccountingConfig, workspaceRoot?: string): Promise<string> {\n // 8 hex chars × small N → collision odds are negligible, but a\n // bounded retry keeps the generator total even if one happens.\n for (let attempt = 0; attempt < GENERATED_ID_RETRIES; attempt += 1) {\n const candidate = `book-${randomUUID().slice(0, 8)}`;\n if (!findBook(config, candidate) && !(await bookExists(candidate, workspaceRoot))) return candidate;\n }\n throw new AccountingError(500, \"could not generate a unique book id after several attempts\");\n}\n\n/** Read every journal entry across every month, in period-sorted\n * order. Used by paths that need a full-history view (opening\n * balance lookups, P/L date filtering). */\nasync function readAllEntries(bookId: string, workspaceRoot?: string): Promise<JournalEntry[]> {\n const periods = await listJournalPeriods(bookId, workspaceRoot);\n const all: JournalEntry[] = [];\n for (const monthKey of periods) {\n const { entries, skipped } = await readJournalMonth(bookId, monthKey, workspaceRoot);\n for (const entry of entries) all.push(entry);\n if (skipped > 0) {\n // Aggregations and reports built from a partial parse are\n // misleading — log so an operator can spot a corrupted\n // jsonl file. Reads still proceed with what we could parse;\n // refusing here would lock the user out of the whole book\n // for a single bad line.\n log.warn(\"accounting\", \"journal month had unparseable lines\", { bookId, period: monthKey, skipped });\n }\n }\n return all;\n}\n\n// ── books ──────────────────────────────────────────────────────────\n\nexport async function listBooks(workspaceRoot?: string): Promise<{ books: BookSummary[] }> {\n const config = await loadOrInitConfig(workspaceRoot);\n return { books: config.books };\n}\n\nfunction unsupportedCountryError(received: unknown): AccountingError {\n return new AccountingError(400, `unsupported country code ${JSON.stringify(received)} — must be one of: ${SUPPORTED_COUNTRY_CODES.join(\", \")}`);\n}\n\nfunction unsupportedFiscalYearEndError(received: unknown): AccountingError {\n return new AccountingError(\n 400,\n `unsupported fiscalYearEnd ${JSON.stringify(received)} — must be a closing-month number ${FISCAL_YEAR_END_MONTHS.join(\", \")} (1 = January … 12 = December)`,\n );\n}\n\n/** Coerce + validate a free-form `fiscalYearEnd` from any ingress path\n * (REST body, MCP tool args, direct callers). The service is the\n * validation boundary, so this is deliberately tolerant of input SHAPE\n * but strict about the resulting value:\n * - absent / null / empty string → `undefined` (field omitted; the\n * caller decides default-vs-no-op);\n * - a number, or a numeric string (\"8\") from a hand-rolled client →\n * that month;\n * - a legacy calendar-quarter token (\"Q1\"..\"Q4\") from a stale client\n * → its closing month (same Q1→3 mapping the read side applies);\n * - anything else non-empty (a typo, garbage, an out-of-range or\n * non-integer number) → 400, echoing the ORIGINAL value so the\n * bad payload can't be silently mistaken for the default. */\nfunction coerceFiscalYearEndInput(raw: unknown): FiscalYearEnd | undefined {\n if (raw === undefined || raw === null || raw === \"\") return undefined;\n let month: unknown = raw;\n if (typeof raw === \"string\") {\n const trimmed = raw.trim();\n if (trimmed === \"\") return undefined;\n if (/^Q[1-4]$/.test(trimmed)) return resolveFiscalYearEnd(trimmed);\n month = /^-?\\d+$/.test(trimmed) ? Number(trimmed) : Number.NaN;\n }\n if (!isFiscalYearEnd(month)) throw unsupportedFiscalYearEndError(raw);\n return month;\n}\n\n/** Boundary checks for updateBook (name / country only — fiscalYearEnd is\n * coerced + validated separately via `coerceFiscalYearEndInput`). Throws on\n * the first failure so the surrounding function stays under the\n * cognitive-complexity threshold, and hands back the country to persist:\n * `undefined` = the field was omitted, `\"\"` = explicit clear. */\nfunction parseUpdateBookInput(input: { name?: string | undefined; country?: string | undefined }): SupportedCountryCode | \"\" | undefined {\n if (input.name !== undefined && (typeof input.name !== \"string\" || input.name.trim() === \"\")) {\n throw new AccountingError(400, \"name must be a non-empty string when supplied\");\n }\n const { country } = input;\n // Empty string is the explicit \"clear the field\" sentinel from the\n // settings UI; anything else has to land in the curated list, same\n // contract as createBook.\n if (country === undefined || country === \"\") return country;\n if (!isSupportedCountryCode(country)) {\n throw unsupportedCountryError(country);\n }\n return country;\n}\n\nexport async function createBook(\n input: { id?: string | undefined; name: string; currency?: string | undefined; country?: string | undefined; fiscalYearEnd?: unknown },\n workspaceRoot?: string,\n): Promise<{ book: BookSummary }> {\n if (typeof input.name !== \"string\" || input.name.trim() === \"\") {\n throw new AccountingError(400, \"name is required\");\n }\n // Country, when supplied, must be one of the curated codes — keeps\n // the UI dropdown, the role prompt's per-jurisdiction guidance, and\n // the on-disk JSON in sync. A typo from the LLM or an untrusted\n // client is rejected here rather than silently persisted.\n const { country } = input;\n if (country !== undefined && !isSupportedCountryCode(country)) {\n throw unsupportedCountryError(country);\n }\n const fiscalYearEnd = coerceFiscalYearEndInput(input.fiscalYearEnd) ?? DEFAULT_FISCAL_YEAR_END;\n const config = await loadOrInitConfig(workspaceRoot);\n // Auto-generate when no caller id is supplied — every book,\n // including the very first one, gets a generated id. Explicit\n // caller-supplied ids (from a custom config import or a CLI tool)\n // are kept verbatim so users with their own naming scheme can\n // adopt it.\n const bookId = input.id ?? (await generateBookId(config, workspaceRoot));\n // Guard against caller-supplied path-traversal ids before any\n // fs touch (createBook → ensureBookDir → writeAccounts →\n // writeConfig). Auto-generated ids always pass.\n if (!isSafeBookId(bookId)) {\n throw new AccountingError(400, `invalid book id ${JSON.stringify(bookId)} — allowed characters are A-Z a-z 0-9 _ - (1-64 chars; cannot start with _ or -)`);\n }\n if (findBook(config, bookId)) {\n throw new AccountingError(409, `book ${JSON.stringify(bookId)} already exists`);\n }\n if (await bookExists(bookId, workspaceRoot)) {\n throw new AccountingError(409, `book directory ${JSON.stringify(bookId)} already exists on disk`);\n }\n const book: BookSummary = {\n id: bookId,\n name: input.name,\n currency: input.currency ?? DEFAULT_CURRENCY,\n ...(country ? { country } : {}),\n fiscalYearEnd,\n createdAt: new Date().toISOString(),\n };\n await ensureBookDir(bookId, workspaceRoot);\n await writeAccounts(bookId, [...DEFAULT_ACCOUNTS], workspaceRoot);\n const nextConfig: AccountingConfig = { books: [...config.books, book] };\n await writeConfig(nextConfig, workspaceRoot);\n publishBooksChanged(workspaceRoot);\n return { book };\n}\n\nexport async function updateBook(\n input: { bookId: string; name?: string | undefined; country?: string | undefined; fiscalYearEnd?: unknown },\n workspaceRoot?: string,\n): Promise<{ book: BookSummary }> {\n const config = await loadOrInitConfig(workspaceRoot);\n const target = findBook(config, input.bookId);\n if (!target) {\n throw new AccountingError(404, `book ${JSON.stringify(input.bookId)} not found`);\n }\n const country = parseUpdateBookInput(input);\n // Coerce + validate up front so a malformed value 400s before any\n // write (undefined = the field was omitted → leave it untouched).\n const fiscalYearEnd = coerceFiscalYearEndInput(input.fiscalYearEnd);\n // Currency intentionally absent — once entries reference per-book\n // amounts, switching currency would silently re-interpret every\n // historical figure. Country / name / fiscalYearEnd are pure metadata;\n // safe to swap. Changing fiscalYearEnd does not move any entries —\n // it only changes how the date-range shortcuts resolve from now on.\n const next: BookSummary = {\n ...target,\n ...(input.name !== undefined ? { name: input.name } : {}),\n ...(country ? { country } : {}),\n ...(fiscalYearEnd !== undefined ? { fiscalYearEnd } : {}),\n };\n // Strip an explicitly-cleared country so the JSON file stays clean\n // (matches the createBook policy of omitting the field when unset).\n if (country === \"\") delete next.country;\n const nextConfig: AccountingConfig = {\n books: config.books.map((book) => (book.id === input.bookId ? next : book)),\n };\n await writeConfig(nextConfig, workspaceRoot);\n publishBooksChanged(workspaceRoot);\n return { book: next };\n}\n\nexport async function deleteBook(\n input: { bookId: string; confirm: boolean },\n workspaceRoot?: string,\n): Promise<{ deletedBookId: string; deletedBookName: string }> {\n if (!input.confirm) {\n throw new AccountingError(400, \"deleteBook requires confirm: true\");\n }\n const config = await loadOrInitConfig(workspaceRoot);\n const target = findBook(config, input.bookId);\n if (!target) {\n throw new AccountingError(404, `book ${JSON.stringify(input.bookId)} not found`);\n }\n // Stop any in-flight rebuild before removing the directory; otherwise\n // writeSnapshot could re-create the tree via mkdir-recursive after\n // we delete it, leaving an orphaned book folder on disk.\n cancelRebuild(input.bookId, workspaceRoot);\n await awaitRebuildIdle(input.bookId, workspaceRoot);\n await removeBookDir(input.bookId, workspaceRoot);\n const remaining = config.books.filter((book) => book.id !== input.bookId);\n await writeConfig({ books: remaining }, workspaceRoot);\n publishBooksChanged(workspaceRoot);\n // Capture the name BEFORE the splice so the LLM-facing message\n // can reference the human-readable book the user just deleted.\n return { deletedBookId: input.bookId, deletedBookName: target.name };\n}\n\n// ── accounts ───────────────────────────────────────────────────────\n\nexport async function listAccounts(input: { bookId?: string | undefined }, workspaceRoot?: string): Promise<{ bookId: string; accounts: Account[] }> {\n const config = await loadOrInitConfig(workspaceRoot);\n const bookId = resolveBookId(config, input.bookId);\n return { bookId, accounts: await readAccounts(bookId, workspaceRoot) };\n}\n\n/** Parse the caller's account, then apply the one rule the chart owns\n * rather than the record: codes starting with `_` are reserved for the\n * synthetic rows the report layer injects (e.g. `_currentEarnings` in\n * the Equity section). A user account there would either duplicate a\n * B/S row or hide a real account behind the synthetic label. */\nfunction parseUpsertAccount(raw: unknown): Account {\n const parsed = parseAccountInput(raw);\n if (!parsed.ok) throw new AccountingError(400, parsed.message);\n if (parsed.account.code.startsWith(\"_\")) {\n throw new AccountingError(\n 400,\n `account code ${JSON.stringify(parsed.account.code)} is reserved (codes starting with _ are used for synthetic report rows)`,\n );\n }\n return parsed.account;\n}\n\n/** Insert or replace the account in the chart. The whitelist +\n * active-flag policy lives in normalizeStoredAccount (see\n * ./accountNormalize.ts) so it stays unit-testable in isolation.\n * `previousType` is null for a new code — callers use it to decide\n * whether aggregation across periods just changed meaning. */\nfunction applyAccount(accounts: readonly Account[], account: Account): { next: Account[]; previousType: AccountType | null } {\n const existingIdx = accounts.findIndex((stored) => stored.code === account.code);\n const existing = existingIdx >= 0 ? accounts[existingIdx] : undefined;\n const stored = normalizeStoredAccount(account, existing);\n const next = [...accounts];\n if (existingIdx >= 0) next[existingIdx] = stored;\n else next.push(stored);\n // `existing ? … : null`, not `existing?.type ?? null`: a book written\n // before the account payload was parsed can hold a record whose `type`\n // is missing, and collapsing that into \"no previous type\" would skip\n // the invalidation below on the very upsert that repairs it.\n return { next, previousType: existing ? existing.type : null };\n}\n\nexport async function upsertAccount(\n input: { bookId?: string | undefined; account: unknown },\n workspaceRoot?: string,\n): Promise<{ bookId: string; account: Account; accounts: Account[] }> {\n const config = await loadOrInitConfig(workspaceRoot);\n const bookId = resolveBookId(config, input.bookId);\n const account = parseUpsertAccount(input.account);\n const { next, previousType } = applyAccount(await readAccounts(bookId, workspaceRoot), account);\n await writeAccounts(bookId, next, workspaceRoot);\n // Type changes affect aggregation across periods — drop every\n // snapshot to be safe. Pure name / note changes don't, but\n // distinguishing isn't worth the complexity.\n if (previousType !== null && previousType !== account.type) {\n scheduleRebuild(bookId, \"0000-00\", workspaceRoot);\n await invalidateAllSnapshots(bookId, workspaceRoot);\n }\n publishBookChange(bookId, { kind: ACCOUNTING_BOOK_EVENT_KINDS.accounts }, workspaceRoot);\n return { bookId, account, accounts: next };\n}\n\n// ── journal entries ────────────────────────────────────────────────\n\ninterface BatchValidationFailure {\n index: number;\n errors: unknown;\n}\n\ninterface BatchParseResult {\n failures: BatchValidationFailure[];\n items: ParsedEntry[];\n}\n\n// All-or-nothing parse: collect failures across every entry so the\n// whole batch can be rejected before any write touches disk (a\n// half-applied batch can never end up persisted). The parsed items\n// ride along so the build step gets a shape something checked.\nfunction parseBatchEntries(items: readonly unknown[], accounts: readonly Account[]): BatchParseResult {\n const failures: BatchValidationFailure[] = [];\n const parsed: ParsedEntry[] = [];\n items.forEach((item, index) => {\n const result = parseEntry(item, accounts);\n if (result.ok) parsed.push(result.entry);\n else failures.push({ index, errors: result.errors });\n });\n return { failures, items: parsed };\n}\n\nfunction buildBatchEntries(items: readonly ParsedEntry[]): JournalEntry[] {\n return items.map((item) => makeEntry({ date: item.date, lines: item.lines, memo: item.memo, kind: \"normal\", replacesEntryId: item.replacesEntryId }));\n}\n\n// Snapshot maintenance is driven from the earliest period in the\n// batch — invalidating from that point covers every later month a\n// single-entry call would have invalidated individually, while\n// collapsing the rebuild + publish work into one round.\n// Seedless `reduce` is safe here: the sole caller (addEntries) throws on an\n// empty batch, and parseBatchEntries returns one item per input entry.\nfunction earliestPeriodOf(entries: readonly JournalEntry[]): string {\n return entries.map((entry) => periodFromDate(entry.date)).reduce((min, period) => (period < min ? period : min));\n}\n\nexport async function addEntries(\n input: { bookId?: string | undefined; entries: unknown },\n workspaceRoot?: string,\n): Promise<{ bookId: string; entries: JournalEntry[] }> {\n const config = await loadOrInitConfig(workspaceRoot);\n const bookId = resolveBookId(config, input.bookId);\n if (!isUnknownArray(input.entries) || input.entries.length === 0) {\n throw new AccountingError(400, \"addEntries: entries must be a non-empty array\");\n }\n const accounts = await readAccounts(bookId, workspaceRoot);\n const { failures, items } = parseBatchEntries(input.entries, accounts);\n if (failures.length > 0) throw new AccountingError(400, \"invalid journal entries\", failures);\n const built = buildBatchEntries(items);\n // Two-phase batched write: stage every affected month's full new\n // content, then commit all renames at the end. Same-period\n // batches are fully atomic; multi-period failure window is\n // narrowed to the rename phase only.\n await appendJournalBatch(bookId, built, workspaceRoot);\n const earliestPeriod = earliestPeriodOf(built);\n // scheduleRebuild first (sync, sets pendingFromPeriod) so any\n // in-flight rebuild's `isInvalidatedDuringRebuild` check sees the\n // new pending mark before our invalidate races with its write.\n scheduleRebuild(bookId, earliestPeriod, workspaceRoot);\n await invalidateSnapshotsFrom(bookId, earliestPeriod, workspaceRoot);\n publishBookChange(bookId, { kind: ACCOUNTING_BOOK_EVENT_KINDS.journal, period: earliestPeriod }, workspaceRoot);\n return { bookId, entries: built };\n}\n\nasync function findEntryById(bookId: string, entryId: string, workspaceRoot?: string): Promise<JournalEntry | null> {\n const periods = await listJournalPeriods(bookId, workspaceRoot);\n for (const monthKey of periods) {\n const { entries } = await readJournalMonth(bookId, monthKey, workspaceRoot);\n const hit = entries.find((entry) => entry.id === entryId);\n if (hit) return hit;\n }\n return null;\n}\n\nexport async function voidEntry(\n input: { bookId?: string | undefined; entryId: string; reason?: string | undefined; voidDate?: string | undefined },\n workspaceRoot?: string,\n): Promise<{ bookId: string; reverseEntry: JournalEntry; markerEntry: JournalEntry }> {\n const config = await loadOrInitConfig(workspaceRoot);\n const bookId = resolveBookId(config, input.bookId);\n const target = await findEntryById(bookId, input.entryId, workspaceRoot);\n if (!target) {\n throw new AccountingError(404, `entry ${JSON.stringify(input.entryId)} not found`);\n }\n const voidDate = input.voidDate ?? localDateString();\n const { reverse, marker } = makeVoidEntries(target, input.reason, voidDate);\n await appendJournal(bookId, reverse, workspaceRoot);\n await appendJournal(bookId, marker, workspaceRoot);\n // Period whose snapshot is now stale = the older of the\n // original entry's month and the void's month.\n const fromPeriod = target.date < voidDate ? periodFromDate(target.date) : periodFromDate(voidDate);\n scheduleRebuild(bookId, fromPeriod, workspaceRoot);\n await invalidateSnapshotsFrom(bookId, fromPeriod, workspaceRoot);\n publishBookChange(bookId, { kind: ACCOUNTING_BOOK_EVENT_KINDS.journal, period: fromPeriod }, workspaceRoot);\n return { bookId, reverseEntry: reverse, markerEntry: marker };\n}\n\ninterface ListEntriesInput {\n bookId?: string | undefined;\n from?: string | undefined;\n to?: string | undefined;\n accountCode?: string | undefined;\n}\n\nfunction entryMatchesFilters(entry: JournalEntry, input: ListEntriesInput): boolean {\n if (input.from && entry.date < input.from) return false;\n if (input.to && entry.date > input.to) return false;\n if (input.accountCode && !entry.lines.some((line) => line.accountCode === input.accountCode)) return false;\n return true;\n}\n\nexport async function listEntries(\n input: ListEntriesInput,\n workspaceRoot?: string,\n): Promise<{ bookId: string; entries: JournalEntry[]; voidedEntryIds: string[] }> {\n const config = await loadOrInitConfig(workspaceRoot);\n const bookId = resolveBookId(config, input.bookId);\n const periods = await listJournalPeriods(bookId, workspaceRoot);\n const entries: JournalEntry[] = [];\n // Collect voided ids from the *unfiltered* set across every month —\n // an account-filtered query drops void-marker rows (they have no\n // lines), so deriving voided ids from the filtered list misses\n // them and the View loses the strikeout on the cancelled original.\n const allVoidedIds = new Set<string>();\n for (const monthKey of periods) {\n const { entries: monthEntries } = await readJournalMonth(bookId, monthKey, workspaceRoot);\n for (const voidedId of voidedIdSet(monthEntries)) allVoidedIds.add(voidedId);\n if (input.from && monthKey < input.from.slice(0, 7)) continue;\n if (input.to && monthKey > input.to.slice(0, 7)) continue;\n for (const entry of monthEntries) {\n if (entryMatchesFilters(entry, input)) entries.push(entry);\n }\n }\n return { bookId, entries, voidedEntryIds: Array.from(allVoidedIds).sort() };\n}\n\n// ── opening balances ───────────────────────────────────────────────\n\nexport async function getOpeningBalances(\n input: { bookId?: string | undefined },\n workspaceRoot?: string,\n): Promise<{ bookId: string; opening: JournalEntry | null }> {\n const config = await loadOrInitConfig(workspaceRoot);\n const bookId = resolveBookId(config, input.bookId);\n const all = await readAllEntries(bookId, workspaceRoot);\n return { bookId, opening: findActiveOpening(all) };\n}\n\nexport async function setOpeningBalances(\n input: { bookId?: string | undefined; asOfDate: string; lines: unknown; memo?: string | undefined },\n workspaceRoot?: string,\n): Promise<{ bookId: string; openingEntry: JournalEntry; replacedExisting: boolean }> {\n const config = await loadOrInitConfig(workspaceRoot);\n const bookId = resolveBookId(config, input.bookId);\n const accounts = await readAccounts(bookId, workspaceRoot);\n const all = await readAllEntries(bookId, workspaceRoot);\n const parsed = parseOpening({\n asOfDate: input.asOfDate,\n lines: input.lines,\n accounts,\n existingEntries: all,\n });\n if (!parsed.ok) {\n throw new AccountingError(400, \"invalid opening balances\", parsed.errors);\n }\n // Replace-mode: void any existing active opening so the new one\n // is unambiguous. The marker is dated today (when the void\n // happened), not the original opening date.\n const existing = findActiveOpening(all);\n if (existing) {\n const today = localDateString();\n const { reverse, marker } = makeVoidEntries(existing, \"replaced via setOpeningBalances\", today);\n await appendJournal(bookId, reverse, workspaceRoot);\n await appendJournal(bookId, marker, workspaceRoot);\n }\n const opening = makeEntry({\n date: input.asOfDate,\n lines: parsed.lines,\n memo: input.memo ?? \"Opening balances\",\n kind: \"opening\",\n });\n await appendJournal(bookId, opening, workspaceRoot);\n scheduleRebuild(bookId, \"0000-00\", workspaceRoot);\n await invalidateAllSnapshots(bookId, workspaceRoot);\n publishBookChange(bookId, { kind: ACCOUNTING_BOOK_EVENT_KINDS.opening }, workspaceRoot);\n return { bookId, openingEntry: opening, replacedExisting: existing !== null };\n}\n\n// ── reports ────────────────────────────────────────────────────────\n\nfunction endDateOfPeriod(period: ReportPeriod): string {\n if (period.kind === \"month\") {\n const [year = Number.NaN, month = Number.NaN] = period.period.split(\"-\").map((segment) => parseInt(segment, 10));\n const last = new Date(Date.UTC(year, month, 0)).getUTCDate();\n return `${period.period}-${String(last).padStart(2, \"0\")}`;\n }\n return period.to;\n}\n\nexport async function getBalanceSheetReport(\n input: { bookId?: string | undefined; period: ReportPeriod },\n workspaceRoot?: string,\n): Promise<{ bookId: string; balanceSheet: ReturnType<typeof buildBalanceSheet> }> {\n const config = await loadOrInitConfig(workspaceRoot);\n const bookId = resolveBookId(config, input.bookId);\n const accounts = await readAccounts(bookId, workspaceRoot);\n const balances = await balancesAsOf(bookId, input.period, workspaceRoot);\n return {\n bookId,\n balanceSheet: buildBalanceSheet({\n accounts,\n balances,\n asOf: endDateOfPeriod(input.period),\n }),\n };\n}\n\n/** Resolve closing balances at the end of a `ReportPeriod`. Month\n * periods hit the snapshot cache; range periods with a mid-month\n * `to` date have to filter the journal directly because the\n * end-of-month snapshot would include activity past `to`. */\nasync function balancesAsOf(bookId: string, period: ReportPeriod, workspaceRoot?: string): Promise<ReturnType<typeof aggregateBalances>> {\n if (period.kind === \"month\") {\n const snap = await getOrBuildSnapshot(bookId, period.period, workspaceRoot);\n return [...snap.balances];\n }\n const all = await readAllEntries(bookId, workspaceRoot);\n const filtered = all.filter((entry) => entry.date <= period.to);\n return aggregateBalances(filtered);\n}\n\nexport async function getProfitLossReport(\n input: { bookId?: string | undefined; period: ReportPeriod },\n workspaceRoot?: string,\n): Promise<{ bookId: string; profitLoss: ReturnType<typeof buildProfitLoss> }> {\n const config = await loadOrInitConfig(workspaceRoot);\n const bookId = resolveBookId(config, input.bookId);\n const accounts = await readAccounts(bookId, workspaceRoot);\n const all = await readAllEntries(bookId, workspaceRoot);\n const fromDate = input.period.kind === \"month\" ? `${input.period.period}-01` : input.period.from;\n const toDate = endDateOfPeriod(input.period);\n return { bookId, profitLoss: buildProfitLoss({ accounts, entries: all, from: fromDate, to: toDate }) };\n}\n\nexport async function getLedgerReport(\n input: { bookId?: string | undefined; accountCode: string; period?: ReportPeriod | undefined },\n workspaceRoot?: string,\n): Promise<{ bookId: string; ledger: ReturnType<typeof buildLedger> }> {\n const config = await loadOrInitConfig(workspaceRoot);\n const bookId = resolveBookId(config, input.bookId);\n const accounts = await readAccounts(bookId, workspaceRoot);\n const account = accounts.find((acct) => acct.code === input.accountCode);\n if (!account) {\n throw new AccountingError(404, `account ${JSON.stringify(input.accountCode)} not found`);\n }\n const all = await readAllEntries(bookId, workspaceRoot);\n const fromDate = input.period?.kind === \"month\" ? `${input.period.period}-01` : input.period?.from;\n const toDate = input.period ? endDateOfPeriod(input.period) : undefined;\n return { bookId, ledger: buildLedger({ account, entries: all, from: fromDate, to: toDate }) };\n}\n\n// ── time series ────────────────────────────────────────────────────\n\nfunction ensureValidYmd(label: string, value: unknown): string {\n // Reuse the journal-side helper so impossible days (`2025-02-30`,\n // `2025-13-01`) AND silent year drift (`0099-01-01` → 1999 via\n // `Date.UTC` legacy two-digit handling) are both rejected — rolling\n // a separate regex+lastDay check here missed the latter.\n if (typeof value !== \"string\" || !isValidCalendarDate(value)) {\n throw new AccountingError(400, `getTimeSeries: ${label} must be a valid YYYY-MM-DD calendar date`);\n }\n return value;\n}\n\nfunction ensureMetric(value: unknown): TimeSeriesMetric {\n const metric = TIME_SERIES_METRICS.find((candidate) => candidate === value);\n if (metric === undefined) {\n throw new AccountingError(400, `getTimeSeries: metric must be one of ${TIME_SERIES_METRICS.join(\", \")}`);\n }\n return metric;\n}\n\nfunction ensureGranularity(value: unknown): TimeSeriesGranularity {\n const granularity = TIME_SERIES_GRANULARITIES.find((candidate) => candidate === value);\n if (granularity === undefined) {\n throw new AccountingError(400, `getTimeSeries: granularity must be one of ${TIME_SERIES_GRANULARITIES.join(\", \")}`);\n }\n return granularity;\n}\n\nfunction resolveAccountCode(metric: TimeSeriesMetric, raw: unknown): string | undefined {\n if (metric === \"accountBalance\") {\n if (typeof raw !== \"string\" || raw === \"\") {\n throw new AccountingError(400, \"getTimeSeries: accountCode is required when metric is accountBalance\");\n }\n return raw;\n }\n if (raw !== undefined && raw !== \"\") {\n throw new AccountingError(400, \"getTimeSeries: accountCode is only allowed when metric is accountBalance\");\n }\n return undefined;\n}\n\nexport interface TimeSeriesReportInput {\n bookId?: string | undefined;\n metric: unknown;\n granularity: unknown;\n from: unknown;\n to: unknown;\n accountCode?: unknown;\n}\n\nexport interface TimeSeriesReport {\n bookId: string;\n metric: TimeSeriesMetric;\n granularity: TimeSeriesGranularity;\n from: string;\n to: string;\n accountCode?: string;\n points: TimeSeriesPoint[];\n}\n\ninterface ValidatedTimeSeriesInput {\n metric: TimeSeriesMetric;\n granularity: TimeSeriesGranularity;\n from: string;\n toDate: string;\n accountCode: string | undefined;\n}\n\nfunction validateTimeSeriesInput(input: TimeSeriesReportInput): ValidatedTimeSeriesInput {\n const metric = ensureMetric(input.metric);\n const granularity = ensureGranularity(input.granularity);\n const from = ensureValidYmd(\"from\", input.from);\n const toDate = ensureValidYmd(\"to\", input.to);\n if (from > toDate) throw new AccountingError(400, \"getTimeSeries: from must be on or before to\");\n const accountCode = resolveAccountCode(metric, input.accountCode);\n return { metric, granularity, from, toDate, accountCode };\n}\n\ninterface TimeSeriesBookContext {\n bookId: string;\n fiscalYearEnd: FiscalYearEnd;\n accounts: Account[];\n}\n\nasync function loadTimeSeriesBookContext(requestedBookId: string | undefined, workspaceRoot?: string): Promise<TimeSeriesBookContext> {\n const config = await loadOrInitConfig(workspaceRoot);\n const bookId = resolveBookId(config, requestedBookId);\n const book = findBook(config, bookId);\n // resolveBookId guarantees the book exists; resolveFiscalYearEnd\n // covers both the type-checker fallback and any legacy token that\n // slipped past the read-side normalisation.\n const fiscalYearEnd: FiscalYearEnd = resolveFiscalYearEnd(book?.fiscalYearEnd);\n const accounts = await readAccounts(bookId, workspaceRoot);\n return { bookId, fiscalYearEnd, accounts };\n}\n\nexport async function getTimeSeriesReport(input: TimeSeriesReportInput, workspaceRoot?: string): Promise<TimeSeriesReport> {\n const { metric, granularity, from, toDate, accountCode } = validateTimeSeriesInput(input);\n const { bookId, fiscalYearEnd, accounts } = await loadTimeSeriesBookContext(input.bookId, workspaceRoot);\n if (accountCode && !accounts.some((acct) => acct.code === accountCode)) {\n throw new AccountingError(404, `getTimeSeries: account ${JSON.stringify(accountCode)} not found`);\n }\n const entries = await readAllEntries(bookId, workspaceRoot);\n const buckets = bucketize({ from, to: toDate, granularity, fiscalYearEnd });\n const points = buildTimeSeries({ buckets, entries, accounts, metric, accountCode });\n const report: TimeSeriesReport = { bookId, metric, granularity, from, to: toDate, points };\n if (accountCode) report.accountCode = accountCode;\n return report;\n}\n\n// ── snapshot admin ─────────────────────────────────────────────────\n\nexport async function rebuildSnapshots(input: { bookId?: string | undefined }, workspaceRoot?: string): Promise<{ bookId: string; rebuilt: string[] }> {\n const config = await loadOrInitConfig(workspaceRoot);\n const bookId = resolveBookId(config, input.bookId);\n const result = await rebuildAllSnapshots(bookId, workspaceRoot);\n publishBookChange(bookId, { kind: ACCOUNTING_BOOK_EVENT_KINDS.snapshotsReady }, workspaceRoot);\n return { bookId, rebuilt: result.rebuilt };\n}\n\n// Direct access for tests / lazy paths that want to bypass the\n// snapshot cache.\nexport { aggregateBalances, balancesAtEndOf };\n","// Tiny Express helper owned by the package so the router is\n// self-contained (no host util imports). `asyncHandler` turns an\n// uncaught throw inside an async handler into a logged 500 carrying\n// only the caller-supplied fallback message — never the raw error text\n// (which could leak internals).\n//\n// Deliberately NOT shared with the host's server/utils/asyncHandler.ts:\n// a plugin importing host code is an uphill import, forbidden by the\n// package-dependency-direction rule. This copy is also scoped to the\n// package logger and diverges — it forwards to Express `next(err)` on\n// `headersSent` (the host copy doesn't). Do not \"de-dupe\" it by reaching\n// into the host; lift it into @mulmoclaude/core if it ever must be shared.\n\nimport type { Request, Response, NextFunction } from \"express\";\nimport { log } from \"./context.js\";\nimport { errorMessage } from \"../shared/errors.js\";\n\n/** The body the catch path sends. Declared here rather than imported from the\n * host's `httpError.ts` for the same reason the wrapper is copied: reaching\n * into the host would be an uphill import. */\nexport interface ErrorBody {\n error: string;\n}\n\n/** A route response that can carry either its success body or an error — use\n * this, not a bare `Response<T>`, for any handler wrapped by `asyncHandler`,\n * which can always emit `ErrorBody` on the failure path. */\nexport type ApiResponse<T> = Response<T | ErrorBody>;\n\n/** The bounds below name exactly what the catch path dereferences.\n *\n * They are NOT `extends Request` / `extends Response`: Express uses its type\n * parameters in mixed variance positions, so a nominal bound rejects valid\n * call sites like `Request<object, unknown, MyBody>`. Naming only the members\n * we touch keeps every concrete `Request<…>` / `Response<…>` assignable.\n *\n * Structural bounds rather than unconstrained generics plus `as` casts,\n * because the casts were load-bearing in a way that hid a real contract: this\n * wrapper sends `{ error }` on the failure path, so a route declaring a\n * `ResBody` that cannot carry it was lying. That is now a compile error, and\n * the fix at such a site is `ApiResponse<T>`. */\ninterface RoutePathBearing {\n path: string;\n}\n\ninterface ErrorSendableResponse {\n headersSent: boolean;\n status: (code: number) => { json: (body: ErrorBody) => unknown };\n}\n\nexport function asyncHandler<TReq extends RoutePathBearing = Request, TRes extends ErrorSendableResponse = Response>(\n namespace: string,\n fallbackMessage: string,\n handler: (req: TReq, res: TRes) => Promise<void>,\n): (req: TReq, res: TRes, next: NextFunction) => Promise<void> {\n return async (req, res, next) => {\n try {\n await handler(req, res);\n } catch (err) {\n log.error(namespace, \"handler threw\", { route: req.path, error: errorMessage(err) });\n if (res.headersSent) {\n // Response already (partially) sent — we can't write a clean 500.\n // Forward to Express's error flow so it can destroy the socket\n // rather than leaving the request hanging.\n next(err);\n return;\n }\n res.status(500).json({ error: fallbackMessage });\n }\n };\n}\n","// REST endpoint for the accounting plugin. Single POST dispatch\n// route with an `action` discriminator — matches the todos /\n// scheduler convention so the LLM-facing MCP bridge (which invokes\n// `apiPost` with the tool args verbatim) plugs in without\n// translation.\n//\n// The mounted `<AccountingApp>` View hits this same endpoint\n// directly for tab switches, filter changes, and form submits — no\n// LLM round trip per click. The MCP bridge calls into the same\n// service layer, so manual clicks and Claude tool calls produce\n// identical state changes.\n\nimport { Router, Request, Response } from \"express\";\nimport { isRecord, isUnknownArray } from \"@mulmoclaude/common\";\n\nimport { describeEntry, optionalRecord, optionalReportPeriod, optionalString } from \"./bodyFields.js\";\n\nimport {\n AccountingError,\n addEntries,\n createBook,\n updateBook,\n deleteBook,\n getBalanceSheetReport,\n getLedgerReport,\n getOpeningBalances,\n getProfitLossReport,\n getTimeSeriesReport,\n listAccounts,\n listBooks,\n listEntries,\n rebuildSnapshots,\n setOpeningBalances,\n upsertAccount,\n voidEntry,\n} from \"./service.js\";\nimport type { BookSummary } from \"../shared/types.js\";\nimport { ACCOUNTING_ACTIONS, ACCOUNTING_API } from \"../shared\";\nimport { channelScopeFor, isProjectScopedHost, log } from \"./context.js\";\nimport { asyncHandler, type ErrorBody } from \"./http.js\";\n\nexport interface AccountingActionBody {\n action: string;\n [key: string]: unknown;\n}\n\n/** `ErrorBody` plus the structured detail this router attaches to a failed\n * dispatch (see the `err.details` send below) — a superset, not a rival\n * declaration of the same shape. */\ninterface AccountingErrorResponse extends ErrorBody {\n details?: unknown;\n}\n\n// Tool-result envelope for the MCP-driven `openBook` action. The\n// frontend tool-result renderer keys off `kind: \"accounting-app\"`\n// to mount `<AccountingApp>` (vs the compact `Preview.vue` which\n// renders summaries for every other action). `message` is picked\n// up by the MCP bridge and surfaced as the tool's text response\n// to the LLM (server/agent/mcp-server.ts).\ninterface OpenBookToolResult {\n kind: \"accounting-app\";\n bookId: string;\n initialTab?: string | undefined;\n /** Same shape getBooks returns — included so an LLM that calls\n * openBook doesn't need a follow-up getBooks round-trip to learn\n * what other books exist before its next action. */\n books: BookSummary[];\n /** The host's OPAQUE scope id for the root this book lives under, so\n * the mounted card fetches ITS OWN project rather than whatever the\n * host has selected when it renders.\n *\n * `null` is a real answer on a project-scoped host — the default root\n * — and is spelled out rather than omitted, because an ABSENT field\n * means \"resolve the host's active project at mount\" and a host that\n * switches project between the tool result and the render would then\n * point a default-root card at the new one. The field is absent only\n * on a host that declared no scoping at all, which keeps a\n * single-root envelope byte-identical to what it has always been.\n *\n * Stamped by the SERVER from the resolved root — it is never read\n * off the request body, so the model cannot pick a project. */\n scope?: string | null;\n}\n\ntype ActionRest = Omit<AccountingActionBody, \"action\">;\n/** `root` is the workspace root this request resolved to — `undefined`\n * means \"the host's configured one\", the single-root path. Every\n * handler MUST pass it on to the service layer: under a multi-root\n * host a dropped root is not an error but a silent read or write\n * against the wrong project. */\ntype ActionHandler = (rest: ActionRest, root: string | undefined) => Promise<unknown>;\n\n// Each action is a tiny adapter that pulls the typed slice it needs\n// out of the loosely-typed body. Parsing of the slice shape itself\n// lives inside the service layer (parseEntry, parseOpening,\n// parseAccountInput) so the adapters can stay one-liners.\n\nasync function handleOpenBook(rest: ActionRest, root: string | undefined): Promise<OpenBookToolResult> {\n // openBook requires an explicit `bookId` that resolves to an\n // existing book. On a fresh workspace the LLM is expected to\n // call `createBook` first and then `openBook` with the new id.\n if (typeof rest.bookId !== \"string\" || rest.bookId === \"\") {\n throw new AccountingError(400, \"openBook: bookId is required. Call 'getBooks' to enumerate, or 'createBook' first on a fresh workspace.\");\n }\n const list = await listBooks(root);\n if (!list.books.some((book) => book.id === rest.bookId)) {\n throw new AccountingError(404, `openBook: book ${JSON.stringify(rest.bookId)} not found`);\n }\n const initialTab = typeof rest.initialTab === \"string\" ? rest.initialTab : undefined;\n const scopeField = isProjectScopedHost() ? { scope: channelScopeFor(root) } : {};\n return { kind: \"accounting-app\", bookId: rest.bookId, initialTab, books: list.books, ...scopeField };\n}\n\nasync function handleGetReport(rest: ActionRest, root: string | undefined): Promise<unknown> {\n const kind = typeof rest.kind === \"string\" ? rest.kind : \"\";\n const periodInput = optionalReportPeriod(rest.period);\n const bookId = optionalString(rest.bookId);\n // A period that is present but malformed reads as absent, so it\n // lands on this same message rather than reaching the report\n // builders as `${undefined}-01`. Spell the accepted shapes out —\n // the LLM repairs its own payload from this text.\n const periodRequired = (label: string) =>\n new AccountingError(400, `${label}: period is required — { kind: \"month\", period: \"YYYY-MM\" } or { kind: \"range\", from: \"YYYY-MM-DD\", to: \"YYYY-MM-DD\" }`);\n // `ledger` treats an absent period as \"full history\", which would turn a\n // period the caller DID send but got wrong into a silently wider answer\n // than they asked for. Sent-but-unusable is an error for every kind (#2765).\n // `null` counts as NOT sent: it is how a JSON caller spells \"I am not using\n // this optional field\", and rejecting it would break ledger calls that work\n // today.\n const periodSent = rest.period !== undefined && rest.period !== null;\n if (periodSent && !periodInput) throw periodRequired(`getReport ${kind || \"(no kind)\"}`);\n if (kind === \"balance\") {\n if (!periodInput) throw periodRequired(\"getReport balance\");\n return getBalanceSheetReport({ bookId, period: periodInput }, root);\n }\n if (kind === \"pl\") {\n if (!periodInput) throw periodRequired(\"getReport pl\");\n return getProfitLossReport({ bookId, period: periodInput }, root);\n }\n if (kind === \"ledger\") {\n // period is optional for ledger — full-history view from the\n // UI calls getLedger(accountCode, undefined, bookId). The\n // account code, however, is mandatory; without it the request\n // is meaningless and the service would 404 on a blank code.\n if (typeof rest.accountCode !== \"string\" || rest.accountCode === \"\") {\n throw new AccountingError(400, \"getReport ledger: accountCode is required\");\n }\n return getLedgerReport({ bookId, accountCode: rest.accountCode, period: periodInput }, root);\n }\n throw new AccountingError(400, `getReport: unknown kind ${JSON.stringify(kind)}`);\n}\n\nconst ACTION_HANDLERS: Record<string, ActionHandler> = {\n [ACCOUNTING_ACTIONS.openBook]: handleOpenBook,\n [ACCOUNTING_ACTIONS.getBooks]: (_rest, root) => listBooks(root),\n [ACCOUNTING_ACTIONS.createBook]: async (rest, root) => {\n // Surface bookId at the top level so the dispatch envelope's\n // `data` carries it like every other write action — the View\n // uses it to preselect the new book on mount.\n const result = await createBook(\n {\n name: typeof rest.name === \"string\" ? rest.name : \"\",\n currency: typeof rest.currency === \"string\" ? rest.currency : undefined,\n country: typeof rest.country === \"string\" ? rest.country : undefined,\n // Passed through raw — the service coerces + validates it (number,\n // numeric string, or a legacy \"Q1\"..\"Q4\" token), 400ing garbage.\n fiscalYearEnd: rest.fiscalYearEnd,\n },\n root,\n );\n return { bookId: result.book.id, ...result };\n },\n [ACCOUNTING_ACTIONS.updateBook]: async (rest, root) => {\n const result = await updateBook(\n {\n bookId: typeof rest.bookId === \"string\" ? rest.bookId : \"\",\n name: typeof rest.name === \"string\" ? rest.name : undefined,\n country: typeof rest.country === \"string\" ? rest.country : undefined,\n // Passed through raw — the service coerces + validates it.\n fiscalYearEnd: rest.fiscalYearEnd,\n },\n root,\n );\n return { bookId: result.book.id, ...result };\n },\n [ACCOUNTING_ACTIONS.deleteBook]: (rest, root) =>\n deleteBook({ bookId: typeof rest.bookId === \"string\" ? rest.bookId : \"\", confirm: rest.confirm === true }, root),\n [ACCOUNTING_ACTIONS.getAccounts]: (rest, root) => listAccounts({ bookId: optionalString(rest.bookId) }, root),\n // `account` / `entries` / `lines` travel to the service as `unknown`:\n // the parsers there own the narrowing, so a bad field keeps its own\n // message (\"debit must be a non-negative finite number\") instead of\n // collapsing into a generic shape error the LLM can't repair from.\n [ACCOUNTING_ACTIONS.upsertAccount]: (rest, root) => upsertAccount({ bookId: optionalString(rest.bookId), account: rest.account }, root),\n [ACCOUNTING_ACTIONS.addEntries]: (rest, root) => addEntries({ bookId: optionalString(rest.bookId), entries: rest.entries }, root),\n [ACCOUNTING_ACTIONS.voidEntry]: (rest, root) =>\n voidEntry(\n {\n bookId: optionalString(rest.bookId),\n entryId: typeof rest.entryId === \"string\" ? rest.entryId : \"\",\n reason: optionalString(rest.reason),\n voidDate: optionalString(rest.voidDate),\n },\n root,\n ),\n [ACCOUNTING_ACTIONS.getJournalEntries]: (rest, root) =>\n listEntries(\n {\n bookId: optionalString(rest.bookId),\n from: optionalString(rest.from),\n to: optionalString(rest.to),\n accountCode: optionalString(rest.accountCode),\n },\n root,\n ),\n [ACCOUNTING_ACTIONS.getOpeningBalances]: (rest, root) => getOpeningBalances({ bookId: optionalString(rest.bookId) }, root),\n [ACCOUNTING_ACTIONS.setOpeningBalances]: (rest, root) =>\n setOpeningBalances(\n {\n bookId: optionalString(rest.bookId),\n asOfDate: typeof rest.asOfDate === \"string\" ? rest.asOfDate : \"\",\n // Omitted `lines` means the zero-line opening marker that unlocks\n // the View's gate, so it stays an empty array rather than a 400.\n lines: rest.lines ?? [],\n memo: optionalString(rest.memo),\n },\n root,\n ),\n [ACCOUNTING_ACTIONS.getReport]: handleGetReport,\n [ACCOUNTING_ACTIONS.getTimeSeries]: (rest, root) =>\n getTimeSeriesReport(\n {\n bookId: optionalString(rest.bookId),\n metric: rest.metric,\n granularity: rest.granularity,\n from: rest.from,\n to: rest.to,\n accountCode: rest.accountCode,\n },\n root,\n ),\n [ACCOUNTING_ACTIONS.rebuildSnapshots]: (rest, root) => rebuildSnapshots({ bookId: optionalString(rest.bookId) }, root),\n};\n\n// Actions whose tool-result envelope carries a `data` field. On the live path\n// that is what decides whether a card appears AT ALL: the MCP bridge posts a\n// result to the session only when `data` is set\n// (`server/agent/mcp-server.ts`), so an action left out of this set never\n// reaches `toolResults` and never renders. Adding one here makes a NEW card\n// appear; it does not fill an existing one.\n//\n// The sidebar's own `v-if` reads `getPlugin(...)?.previewComponent` and never\n// looks at `data`, which is easy to mistake for \"the host does not gate on\n// data\" — it does, one layer up. A directly-injected envelope with no `data`\n// does render, but nothing on the live path produces one.\n//\n// Reads and View-driven maintenance ops stay out: the ops are invoked from\n// inside the canvas, where a card is noise about something the user is already\n// looking at, and a read the LLM will narrate in its reply does not need one.\n//\n// `getReport` was weighed for inclusion under #2716 and deliberately left out\n// (2026-09-21) — a card per report call is noise the narration already covers.\n// The consequence is stated where it bites, on `summarisePl` / `summariseBs`.\nexport const PREVIEW_ACTIONS = new Set<string>([\n ACCOUNTING_ACTIONS.openBook,\n ACCOUNTING_ACTIONS.createBook,\n ACCOUNTING_ACTIONS.updateBook,\n ACCOUNTING_ACTIONS.upsertAccount,\n ACCOUNTING_ACTIONS.addEntries,\n ACCOUNTING_ACTIONS.voidEntry,\n ACCOUNTING_ACTIONS.setOpeningBalances,\n]);\n\n// LLM-facing `message` tacked onto YES actions. The shared trailer\n// (\"The accounting view is shown to the user.\") tells the LLM that a\n// canvas / sidebar surface is already visible, so its text reply\n// shouldn't redundantly enumerate the result the user can see — it\n// should narrate what was *done*, not re-list what's on screen.\nconst VIEW_VISIBLE_TRAILER = \"The accounting view is shown to the user.\";\n\ntype MessageBuilder = (fields: Record<string, unknown>) => string;\n\nconst MESSAGE_BUILDERS: Record<string, MessageBuilder> = {\n [ACCOUNTING_ACTIONS.openBook]: (fields) => {\n // Include the books list inline so the LLM doesn't need a\n // follow-up getBooks round-trip before deciding what to do\n // next.\n const { books, bookId } = fields;\n const booksFragment = Array.isArray(books) ? ` Books available: ${JSON.stringify(books)}.` : \"\";\n const idFragment = typeof bookId === \"string\" ? ` (book id: ${bookId})` : \"\";\n return `Mounted the accounting app in the canvas${idFragment}.${booksFragment}`;\n },\n [ACCOUNTING_ACTIONS.createBook]: (fields) => {\n const book = optionalRecord(fields.book);\n const name = optionalString(book?.name);\n const bookId = optionalString(book?.id);\n const subject = name ? `A new book named ${JSON.stringify(name)}` : \"A new book\";\n // The LLM needs book.id to call any follow-up action on this\n // book (getAccounts, addEntries, etc.), so include it in the\n // status message instead of forcing a round-trip via getBooks.\n const idFragment = bookId ? ` (id: ${bookId})` : \"\";\n // The View's opening-gate hides every tab except `opening` and\n // `settings` until an opening entry is on file (even a zero-line\n // one). If the agent doesn't tell the user to set opening\n // balances first, the user's \"can I add an entry?\" attempt\n // silently fails because the New Entry tab isn't even visible.\n // Include the next-step instruction inline so the agent's reply\n // matches the UI's actual constraints.\n return `${subject} has been created${idFragment}. Next required step: set opening balances via setOpeningBalances — the journal-entry, ledger, and report tabs are locked until an opening (even an empty one) is saved.`;\n },\n [ACCOUNTING_ACTIONS.upsertAccount]: (fields) => {\n const account = optionalRecord(fields.account);\n const code = optionalString(account?.code);\n const name = optionalString(account?.name);\n if (code && name) {\n return `Upserted account ${code} ${JSON.stringify(name)}.`;\n }\n return \"Updated the chart of accounts.\";\n },\n [ACCOUNTING_ACTIONS.addEntries]: (fields) => {\n const entries = (isUnknownArray(fields.entries) ? fields.entries : []).map(describeEntry);\n const [firstEntry, ...furtherEntries] = entries;\n if (!firstEntry) return \"Posted 0 journal entries.\";\n if (furtherEntries.length === 0) {\n const idFragment = firstEntry.id ? ` (id: ${firstEntry.id})` : \"\";\n return `Posted a journal entry on ${firstEntry.date ?? \"the requested date\"}${idFragment}.`;\n }\n // Surface every id so the LLM can later voidEntry any one of\n // them without a follow-up getJournalEntries round-trip.\n const summary = entries.map((entry) => `${entry.date ?? \"?\"} (id: ${entry.id ?? \"?\"})`).join(\", \");\n return `Posted ${entries.length} journal entries: ${summary}.`;\n },\n [ACCOUNTING_ACTIONS.voidEntry]: (fields) => {\n const reverseDate = optionalString(optionalRecord(fields.reverseEntry)?.date);\n return `Voided the entry; a reversing pair was posted on ${reverseDate ?? \"today\"}.`;\n },\n [ACCOUNTING_ACTIONS.setOpeningBalances]: (fields) => {\n const opening = optionalRecord(fields.openingEntry);\n const verb = fields.replacedExisting === true ? \"replaced\" : \"set\";\n const date = optionalString(opening?.date) ?? \"the requested date\";\n // Surface the actual lines so the LLM can answer follow-up\n // questions like \"what's my opening cash?\" without a separate\n // getOpeningBalances round-trip. An empty-marker opening\n // (zero lines, used to unlock the gate) gets no fragment.\n const openingLines = opening?.lines;\n const lines = isUnknownArray(openingLines) ? openingLines : [];\n const linesFragment = lines.length > 0 ? ` Lines: ${JSON.stringify(lines)}.` : \"\";\n return `Opening balances were ${verb} as of ${date}.${linesFragment}`;\n },\n [ACCOUNTING_ACTIONS.deleteBook]: (fields) => {\n const bookId = optionalString(fields.deletedBookId);\n const name = optionalString(fields.deletedBookName);\n const subject = name ? `the book ${JSON.stringify(name)}` : \"the book\";\n const idFragment = bookId ? ` (id: ${bookId})` : \"\";\n return `Deleted ${subject}${idFragment}.`;\n },\n [ACCOUNTING_ACTIONS.updateBook]: (fields) => {\n const book = optionalRecord(fields.book);\n const bookName = optionalString(book?.name);\n const country = optionalString(book?.country);\n const name = bookName ? JSON.stringify(bookName) : \"the book\";\n const countryFragment = country ? ` (country: ${country})` : \"\";\n return `Updated ${name}${countryFragment}.`;\n },\n};\n\n/** Read a record entry under a user/LLM-controlled key. The\n * `Object.hasOwn` gate is load-bearing: a bare `record[key]` resolves\n * inherited prototype members, so a crafted action (\"constructor\",\n * \"toString\") would dispatch to an unexpected target instead of\n * reading as absent. */\nfunction ownEntry<T>(record: Record<string, T>, key: string): T | undefined {\n return Object.hasOwn(record, key) ? record[key] : undefined;\n}\n\nfunction previewMessage(action: string, fields: Record<string, unknown>): string {\n const head = ownEntry(MESSAGE_BUILDERS, action)?.(fields);\n return head ? `${head} ${VIEW_VISIBLE_TRAILER}` : VIEW_VISIBLE_TRAILER;\n}\n\nasync function dispatch(body: AccountingActionBody, root: string | undefined): Promise<unknown> {\n const { action, ...rest } = body;\n const handler = ownEntry(ACTION_HANDLERS, action);\n if (!handler) throw new AccountingError(400, `unknown action ${JSON.stringify(action)}`);\n // Stamp the dispatch verb onto the response so the MCP bridge's\n // spread `{ toolName, uuid, ...result }` surfaces it as\n // `ToolResult.action`. The sidebar reads this to label cards as\n // `manageAccounting(openBook)` etc., and it round-trips a refresh\n // because the result envelope is persisted to the chat log.\n // Direct browser callers (the AccountingApp view) ignore the field.\n // Service responses that already set `action` win via the spread.\n const result = await handler(rest, root);\n // `isRecord` rejects arrays where the previous `typeof === \"object\"`\n // check accepted them — an array result would have spread into\n // `{0: …, 1: …}`. Every service function returns a plain object, so\n // nothing changes today; an array would now arrive as `{ value: [...] }`.\n const handlerFields = isRecord(result) ? result : { value: result };\n // Mirror the handler payload into `data` for the actions that should render a\n // card. Leaving it off keeps the action silent: the bridge drops the result\n // rather than posting it to the session (see PREVIEW_ACTIONS).\n const dataField = PREVIEW_ACTIONS.has(action) ? { data: { action, ...handlerFields } } : {};\n // The MCP bridge only forwards `message` / `instructions` to the\n // LLM (`data` / `jsonData` reach the view but not the model). So\n // every action MUST set a message — silence resolves to \"Done\"\n // and gives the LLM nothing to reason about. Resolution order:\n // 1. handler-set `message` wins (reserved for special-case\n // narration that the standard MESSAGE_BUILDER can't capture);\n // 2. an action with a registered MESSAGE_BUILDER gets the\n // per-action human-friendly summary; this is decoupled from\n // PREVIEW_ACTIONS so silent ops like deleteBook can still\n // narrate without earning a card;\n // 3. everything else returns the JSON-stringified handler\n // payload so the LLM can read the raw data.\n const handlerMessage = typeof handlerFields.message === \"string\" ? handlerFields.message : undefined;\n const messageField = handlerMessage\n ? {}\n : MESSAGE_BUILDERS[action]\n ? { message: previewMessage(action, handlerFields) }\n : { message: JSON.stringify(handlerFields) };\n return { action, ...handlerFields, ...messageField, ...dataField };\n}\n\n/** The request shape the dispatch route hands a host resolver — the\n * same one the route itself is typed with, so a host can read headers,\n * query and the parsed body without casting. */\nexport type AccountingDispatchRequest = Request<object, unknown, AccountingActionBody>;\n\nexport interface AccountingRouterOptions {\n /** Resolve the workspace root ONE request operates on, for a host\n * that serves more than one (MulmoTerminal: one root per project\n * directory). Return `undefined` to use the host's configured root.\n *\n * The resolver is entirely host-owned and the package never reads a\n * root or a project id off the request body itself. That is what\n * keeps the model out of it: `manageAccounting`'s tool schema has no\n * project parameter, so an LLM cannot name a project even by\n * guessing — the host derives the scope from the session, exactly as\n * `manageCollection` does. A host that DOES accept a project id from\n * a browser must resolve it against its own list of projects and\n * never accept a path (see `AccountingServerDeps.channelScopeForRoot`). */\n resolveWorkspaceRoot?: (req: AccountingDispatchRequest) => string | undefined;\n}\n\n/** Build the accounting Express router. The host injects its workspace\n * root + logger via `configureAccountingServer(...)` and pub/sub via\n * `initAccountingEventPublisher(...)`, then mounts the returned router\n * with `app.use(...)`. Pass `resolveWorkspaceRoot` to serve more than\n * one root; omit it and every request uses the configured one. */\nexport function createAccountingRouter(options: AccountingRouterOptions = {}): Router {\n const router = Router();\n router.post(\n ACCOUNTING_API.dispatch.path,\n asyncHandler<Request<object, unknown, AccountingActionBody>, Response<unknown | AccountingErrorResponse>>(\n \"accounting\",\n \"accounting dispatch failed\",\n async (req, res) => {\n // Validate the body shape up front so a missing / non-object body\n // surfaces as a 400 instead of crashing `dispatch` and bubbling\n // through to the 500 catch-all.\n const { body } = req;\n if (!body || typeof body !== \"object\" || typeof body.action !== \"string\") {\n log.warn(\"accounting\", \"POST dispatch: invalid body\");\n res.status(400).json({ error: \"request body must be an object with a string `action` field\" });\n return;\n }\n const { action } = body;\n log.info(\"accounting\", \"POST dispatch: start\", { action });\n try {\n // Inside the try: a host resolver rejects an unknown project id\n // by throwing, and that is a 4xx about the request, not a\n // server fault — resolving it out here would skip the\n // AccountingError mapping below and answer a bad project with\n // a generic 500.\n const root = options.resolveWorkspaceRoot?.(req);\n const result = await dispatch(body, root);\n log.info(\"accounting\", \"POST dispatch: ok\", { action });\n res.json(result);\n } catch (err) {\n // Domain errors (AccountingError) map to 4xx with `details`.\n // Anything else rethrows — the asyncHandler wrapper catches\n // it, logs `unexpected error`, and returns a generic 500.\n if (err instanceof AccountingError) {\n log.warn(\"accounting\", \"POST dispatch: error\", { action, status: err.status, message: err.message });\n res.status(err.status).json({ error: err.message, details: err.details });\n return;\n }\n throw err;\n }\n },\n ),\n );\n return router;\n}\n"],"mappings":"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"}