@mulmoclaude/accounting-plugin 0.3.0 → 0.3.1

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/context.ts","../src/server/atomic.ts","../src/server/io.ts","../src/server/types.ts","../src/server/journal.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":["// 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\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/** Logger shape — mirrors the host server logger\n * `log.{level}(namespace, message, data?)`. `data` uses\n * `Record<string, unknown>` (not `object`) so the host's `Logger`\n * is structurally assignable when injected. */\nexport interface AccountingLogger {\n error: (namespace: string, message: string, data?: Record<string, unknown>) => void;\n warn: (namespace: string, message: string, data?: Record<string, unknown>) => void;\n info: (namespace: string, message: string, data?: Record<string, unknown>) => void;\n debug: (namespace: string, message: string, data?: Record<string, unknown>) => void;\n}\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 workspaceRoot: string;\n logger: AccountingLogger;\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). */\nexport function defaultWorkspaceRoot(): string {\n if (!deps) {\n throw new Error(\"@mulmoclaude/accounting-plugin: configureAccountingServer() must be called before serving accounting requests\");\n }\n return deps.workspaceRoot;\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","// Self-contained file-I/O primitives for the accounting backend.\n//\n// Reimplemented inside the package (rather than injected) because they\n// are small and generic — owning them keeps the host-injection surface\n// down to the truly host-specific bits (workspace root, pub/sub,\n// logger). Mirrors the host's server/utils/files/{atomic,json,safe}.ts:\n// atomic write = tmp file alongside destination + rename (readers never\n// see a half-written file).\n\nimport { promises as fsPromises } from \"node:fs\";\nimport path from \"node:path\";\nimport { randomUUID } from \"node:crypto\";\n\nexport interface WriteAtomicOptions {\n /** Use a per-write unique tmp filename. Defaults to `true` so two\n * concurrent writers targeting the same destination never race on a\n * shared `${filePath}.tmp` (one renaming/unlinking the other's tmp).\n * Pass `false` only when a stable tmp name is required. */\n uniqueTmp?: boolean;\n}\n\n/** True for a `not found` filesystem error. */\nexport function isEnoent(err: unknown): boolean {\n return typeof err === \"object\" && err !== null && \"code\" in err && (err as { code?: unknown }).code === \"ENOENT\";\n}\n\n// On Windows, AV / Search Indexer / Defender briefly hold handles and\n// rename trips EPERM/EBUSY/EACCES. The retry loop is gated to Windows\n// because on POSIX those codes mean a real permission problem and\n// retrying just adds latency before the inevitable throw. Mirrors the\n// host's server/utils/files/atomic.ts.\nconst IS_WINDOWS = process.platform === \"win32\";\nconst RENAME_RETRY_DELAYS_MS = [30, 100, 300] as const;\n\nfunction hasErrnoCode(err: unknown): err is { code: string } {\n return typeof err === \"object\" && err !== null && \"code\" in err && typeof (err as { code: unknown }).code === \"string\";\n}\n\nfunction isTransientRenameError(err: unknown): boolean {\n if (!IS_WINDOWS || !hasErrnoCode(err)) return false;\n return err.code === \"EPERM\" || err.code === \"EBUSY\" || err.code === \"EACCES\";\n}\n\nasync function renameWithWindowsRetry(fromPath: string, toPath: string): Promise<void> {\n for (const delayMs of RENAME_RETRY_DELAYS_MS) {\n try {\n await fsPromises.rename(fromPath, toPath);\n return;\n } catch (err) {\n if (!isTransientRenameError(err)) throw err;\n await new Promise((resolve) => setTimeout(resolve, delayMs));\n }\n }\n // Final attempt — let any error propagate.\n await fsPromises.rename(fromPath, toPath);\n}\n\n/** Atomic write: unique tmp alongside destination, then rename. */\nexport async function writeFileAtomic(filePath: string, content: string, opts: WriteAtomicOptions = {}): Promise<void> {\n const uniqueTmp = opts.uniqueTmp ?? true;\n const tmp = uniqueTmp ? `${filePath}.${randomUUID()}.tmp` : `${filePath}.tmp`;\n await fsPromises.mkdir(path.dirname(filePath), { recursive: true });\n try {\n await fsPromises.writeFile(tmp, content, { encoding: \"utf-8\" });\n await renameWithWindowsRetry(tmp, filePath);\n } catch (err) {\n await fsPromises.unlink(tmp).catch(() => {});\n throw err;\n }\n}\n\n/** Atomic JSON write (2-space indent), the only serialization shape the\n * accounting io layer needs. */\nexport async function writeJsonAtomic(filePath: string, data: unknown, opts: WriteAtomicOptions = {}): Promise<void> {\n await writeFileAtomic(filePath, JSON.stringify(data, null, 2), opts);\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 { ACCOUNTING_DIRS as WORKSPACE_DIRS, resolveFiscalYearEnd } from \"../shared\";\nimport { writeJsonAtomic, isEnoent } from \"./atomic.js\";\nimport type { AccountingConfig, Account, BookSummary, JournalEntry, MonthSnapshot } from \"./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 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/** 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 entries: JournalEntry[] = [];\n let skipped = 0;\n for (const line of raw.split(\"\\n\")) {\n if (line.trim() === \"\") continue;\n try {\n entries.push(JSON.parse(line) as JournalEntry);\n } catch {\n skipped += 1;\n }\n }\n return { entries, skipped };\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 >= 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","// Domain types for the accounting plugin (opt-in, custom-Role only).\n//\n// Source-of-truth files on disk:\n// data/accounting/config.json ← AccountingConfig\n// data/accounting/books/<id>/accounts.json ← Account[]\n// data/accounting/books/<id>/journal/YYYY-MM.jsonl ← JournalEntry per line\n// data/accounting/books/<id>/snapshots/YYYY-MM.json ← MonthSnapshot (cache)\n//\n// Snapshots are cache only — journal is the single source of truth.\n\nimport type { SupportedCountryCode, FiscalYearEnd } from \"../shared\";\n\nexport const ACCOUNT_TYPES = [\"asset\", \"liability\", \"equity\", \"income\", \"expense\"] as const;\nexport type AccountType = (typeof ACCOUNT_TYPES)[number];\n\n/** B/S accounts (assets / liabilities / equity). Used by opening\n * balance validation: opening entries reference balance-sheet\n * accounts only. */\nexport const BALANCE_SHEET_ACCOUNT_TYPES: readonly AccountType[] = [\"asset\", \"liability\", \"equity\"];\n\nexport interface Account {\n /** Stable identifier the journal lines reference. Typically a\n * numeric string (\"1000\" / \"2000\" …) but free-form is allowed\n * so the user can adopt their existing numbering. */\n code: string;\n name: string;\n type: AccountType;\n /** Optional free-form note (tax bucket, parent group, …). Not\n * interpreted by the engine — passes through verbatim. */\n note?: string;\n /** Soft-delete flag. When `false`, the account is hidden from\n * entry/ledger dropdowns but stays visible in Manage Accounts\n * and historical entries — accounting integrity requires that\n * a code referenced by a journal line never disappears. Omitted\n * (treated as active) by default to keep the JSON files clean\n * for books created before this field existed. */\n active?: boolean;\n}\n\nexport interface BookSummary {\n id: string;\n name: string;\n /** ISO 4217 (e.g. \"USD\" / \"JPY\"). Single-currency per book — no\n * cross-book aggregation. */\n currency: string;\n /** ISO 3166-1 alpha-2 country code (e.g. \"US\" / \"JP\" / \"GB\").\n * Identifies the tax jurisdiction the book is kept under so the\n * Accounting role can give country-aware advice (Japanese T-number\n * under インボイス制度, EU VAT ID, GSTIN, ABN, etc.). Constrained\n * to `SupportedCountryCode` (the curated list shared with the UI\n * dropdown and the LLM tool's JSON-schema enum) so a typo from any\n * ingress path is rejected at the service layer rather than silently\n * persisted. Optional for backward compatibility with books created\n * before the field was introduced; the UI prompts existing books\n * to set it. */\n country?: SupportedCountryCode;\n /** Calendar month (1-12) on whose LAST DAY the book's fiscal year\n * closes — e.g. 8 = August 31, 12 = December 31 (calendar year).\n * Drives the UI's \"current quarter / current year\" date-range\n * shortcuts. Optional in the persisted shape for backward\n * compatibility with books written before this field existed (and\n * with the earlier \"Q1\"..\"Q4\" token form) — read-side code\n * normalises both via `resolveFiscalYearEnd`, treating an absent\n * value as December. New books require it at the create boundary;\n * the default is 12 (December). */\n fiscalYearEnd?: FiscalYearEnd;\n createdAt: string;\n}\n\nexport interface AccountingConfig {\n books: BookSummary[];\n}\n\nexport type JournalEntryKind = \"normal\" | \"opening\" | \"void\" | \"void-marker\";\n\nexport interface JournalLine {\n accountCode: string;\n /** Use exactly one of debit / credit per line, both as positive\n * numbers. The engine treats them as separate fields rather than\n * a single signed amount so the input matches a standard\n * bookkeeping form. */\n debit?: number;\n credit?: number;\n /** Per-line memo (the entry-level memo lives on JournalEntry). */\n memo?: string;\n /** Counterparty's tax-authority-issued registration ID for this\n * line — Japanese 適格請求書発行事業者登録番号 (T-number), EU\n * VAT identification number, UK VAT registration number, India\n * GSTIN, Australia ABN, etc. Required for input-tax-credit\n * eligibility under the Japanese インボイス制度 (effective\n * 2023-10-01) and equivalent regimes elsewhere. Free-form string;\n * format validation belongs upstream (per-jurisdiction). */\n taxRegistrationId?: string;\n}\n\nexport interface JournalEntry {\n /** Globally unique within a book — ULID-style; ordering by id\n * reproduces creation order. */\n id: string;\n /** Calendar date the entry is booked for (YYYY-MM-DD). The month\n * part decides which `journal/YYYY-MM.jsonl` file the entry lives\n * in; entries can be for any past / future date. */\n date: string;\n kind: JournalEntryKind;\n lines: JournalLine[];\n /** Entry-level memo. */\n memo?: string;\n /** When `kind === \"void-marker\"`: id of the entry being voided.\n * When `kind === \"void\"`: the system-generated reverse entry\n * references the original via this field. */\n voidedEntryId?: string;\n /** Reason supplied by the user when voiding. */\n voidReason?: string;\n /** When this entry was posted via the \"edit\" flow (void-then-add),\n * this is the id of the entry it replaces. The void + new-entry\n * pair is *not* atomic on the server — the client issues two\n * sequential calls — but recording the link here makes the\n * edit chain queryable later (e.g. \"what corrected entry X?\"). */\n replacesEntryId?: string;\n /** ISO timestamp the entry was appended to the journal — the\n * authoritative \"when did this hit the books\" clock. Distinct\n * from `date`, which is the user-visible booking date. */\n createdAt: string;\n}\n\n/** Aggregated balance per account at a point in time. The signed\n * number is debit − credit; downstream display logic converts to\n * natural sign per account type (assets debit-positive, liabilities\n * credit-positive). */\nexport interface AccountBalance {\n accountCode: string;\n /** Σ debit − Σ credit across all entries up to and including the\n * snapshot's period end. */\n netDebit: number;\n}\n\nexport interface MonthSnapshot {\n /** \"YYYY-MM\" — the closing month covered. */\n period: string;\n /** Closing balances at end of `period`. */\n balances: AccountBalance[];\n /** ISO timestamp the snapshot file was written. */\n builtAt: string;\n}\n\n/** Period selector for reports. Either a single closing month or a\n * date range. Always inclusive on both ends. */\nexport type ReportPeriod = { kind: \"month\"; period: string } | { kind: \"range\"; from: string; to: string };\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\";\n\nimport type { Account, JournalEntry, JournalLine } from \"./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\nexport interface ValidationResult {\n ok: boolean;\n errors: ValidationError[];\n}\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 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\n/** Pure validation. Does not throw; returns a list of issues so the\n * REST handler can return a structured 400 instead of an opaque\n * 500. */\nfunction validateLine(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 (line.debit !== undefined && !isNonNegativeNumber(line.debit)) {\n errors.push({ field: `lines[${idx}].debit`, message: \"debit must be a non-negative finite number\" });\n }\n if (line.credit !== undefined && !isNonNegativeNumber(line.credit)) {\n errors.push({ field: `lines[${idx}].credit`, message: \"credit must be a non-negative finite number\" });\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 if (line.taxRegistrationId !== undefined) {\n if (typeof line.taxRegistrationId !== \"string\") {\n errors.push({ field: `lines[${idx}].taxRegistrationId`, message: \"must be a string\" });\n } else if (line.taxRegistrationId.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 ${line.taxRegistrationId.trim().length})`,\n });\n }\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\nexport function validateEntry(input: { date: string; lines: readonly JournalLine[]; accounts: readonly Account[] }): ValidationResult {\n const errors: ValidationError[] = [];\n if (!isValidCalendarDate(input.date)) {\n errors.push({ field: \"date\", message: `expected YYYY-MM-DD calendar date, got ${JSON.stringify(input.date)}` });\n }\n if (!Array.isArray(input.lines) || input.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 accountCodes = new Set(input.accounts.map((account) => account.code));\n input.lines.forEach((line, idx) => validateLine(line, idx, accountCodes, errors));\n const net = netBalance(input.lines);\n if (Math.abs(net) > EQUALITY_TOLERANCE) {\n errors.push({ field: \"lines\", message: `Σ debit − Σ credit = ${net.toFixed(4)}; entry must balance` });\n }\n return { ok: errors.length === 0, errors };\n}\n\n/** Build a JournalEntry — validation is the caller's responsibility\n * (it should have called `validateEntry` first). 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;\n kind?: JournalEntry[\"kind\"];\n replacesEntryId?: string;\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","// 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 type { Account, JournalEntry, JournalLine } from \"./types.js\";\nimport { BALANCE_SHEET_ACCOUNT_TYPES } from \"./types.js\";\nimport { isValidCalendarDate, netBalance, voidedIdSet } from \"./journal.js\";\n\nconst EQUALITY_TOLERANCE = 0.005;\n\nexport interface OpeningValidationError {\n field: string;\n message: string;\n}\n\nexport interface OpeningValidationResult {\n ok: boolean;\n errors: OpeningValidationError[];\n}\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 OpeningValidationInput {\n asOfDate: string;\n lines: readonly JournalLine[];\n accounts: readonly Account[];\n existingEntries: readonly JournalEntry[];\n}\n\nfunction validateLineAccountTypes(input: OpeningValidationInput, errors: OpeningValidationError[]): void {\n const accountByCode = new Map(input.accounts.map((account) => [account.code, account]));\n input.lines.forEach((line, idx) => {\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\nfunction validateAsOfPredatesEverything(input: OpeningValidationInput, 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/** Validate inputs for `setOpeningBalances`. Caller passes the full\n * list of journal entries in the book so we can check the\n * \"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 validateOpening(input: OpeningValidationInput): OpeningValidationResult {\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 (!Array.isArray(input.lines)) {\n errors.push({ field: \"lines\", message: \"lines must be an array\" });\n return { ok: false, errors };\n }\n validateLineAccountTypes(input, errors);\n const net = netBalance(input.lines);\n if (Math.abs(net) > EQUALITY_TOLERANCE) {\n errors.push({ field: \"lines\", message: `Σ debit − Σ credit = ${net.toFixed(4)}; opening must balance` });\n }\n validateAsOfPredatesEverything(input, errors);\n return { ok: errors.length === 0, errors };\n}\n","// Pure normalization for the persisted Account record. Lives in its\n// own module so unit tests can exercise the field-whitelist + active-\n// flag policy without spinning up the file system, and so the\n// service-layer `upsertAccount` stays under the repo's 20-line\n// guideline.\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 type { Account } from \"./types.js\";\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, JournalEntry } from \"./types.js\";\n\nconst ZERO_TOLERANCE = 0.0049; // hide rows that round to 0 at 2dp\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 Array.from(map.entries())\n .map(([accountCode, netDebit]) => ({ accountCode, netDebit }))\n .sort((lhs, rhs) => lhs.accountCode.localeCompare(rhs.accountCode));\n}\n\nexport interface BalanceSheetSection {\n type: AccountType;\n rows: { accountCode: string; accountName: string; balance: number }[];\n total: number;\n}\n\nexport interface BalanceSheet {\n asOf: string; // ISO date; period end\n sections: BalanceSheetSection[];\n /** Σ assets − Σ (liabilities + equity). Should be 0 (the\n * accounting equation); a non-zero here indicates either a\n * rounding artefact or a data problem. */\n imbalance: number;\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\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 const sections: BalanceSheetSection[] = [];\n for (const type of [\"asset\", \"liability\", \"equity\"] as const) {\n const rows: BalanceSheetSection[\"rows\"] = [];\n let total = 0;\n for (const account of input.accounts) {\n if (account.type !== type) continue;\n const netDebit = balanceByCode.get(account.code) ?? 0;\n const presented = naturalSign(type, netDebit);\n if (Math.abs(presented) <= ZERO_TOLERANCE) continue;\n rows.push({ accountCode: account.code, accountName: account.name, balance: presented });\n total += presented;\n }\n if (type === \"equity\" && Math.abs(currentEarnings) > ZERO_TOLERANCE) {\n rows.push({ accountCode: CURRENT_EARNINGS_ACCOUNT_CODE, accountName: \"Current period earnings\", balance: currentEarnings });\n total += currentEarnings;\n }\n sections.push({ type, rows, total });\n }\n const assetTotal = sections[0].total;\n const liabEquityTotal = sections[1].total + sections[2].total;\n return {\n asOf: input.asOf,\n sections,\n imbalance: assetTotal - liabEquityTotal,\n };\n}\n\nexport interface ProfitLoss {\n from: string; // inclusive ISO date\n to: string; // inclusive ISO date\n income: { rows: { accountCode: string; accountName: string; amount: number }[]; total: number };\n expense: { rows: { accountCode: string; accountName: string; amount: number }[]; total: number };\n netIncome: number; // income − expense\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\nexport interface LedgerRow {\n entryId: string;\n date: string;\n kind: JournalEntry[\"kind\"];\n memo?: string;\n debit: number;\n credit: number;\n /** Running netDebit balance for this account, in entry order. */\n runningBalance: number;\n /** Counterparty tax-registration ID copied from the source\n * journal line (T-number / VAT ID / GSTIN / ABN). Surfaced as a\n * Ledger column when the active account is in the input-tax\n * band (14xx — see `isTaxAccountCode` in\n * src/plugins/accounting/components/accountNumbering.ts).\n * Carried per row even on non-tax accounts so a future view\n * that wants to show it elsewhere doesn't need a server change. */\n taxRegistrationId?: string;\n}\n\nexport interface Ledger {\n accountCode: string;\n accountName: string;\n rows: LedgerRow[];\n /** Closing netDebit balance — the sum at the bottom of `rows`. */\n closingBalance: number;\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; to?: string }): 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 \"./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 const [year, month, day] = 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;\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;\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 { bookChannel as accountingBookChannel, ACCOUNTING_BOOKS_CHANNEL, type BookChannelPayload as AccountingBookChannelPayload } from \"../shared\";\nimport { 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\nfunction safePublish(channel: string, payload: unknown): void {\n if (!pubsub) return;\n try {\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). */\nexport function publishBookChange(bookId: string, payload: AccountingBookChannelPayload): void {\n safePublish(accountingBookChannel(bookId), payload);\n}\n\n/** Fired when the *list* of books changes (createBook, deleteBook).\n * Payload is intentionally empty — subscribers refetch from\n * /api/accounting. */\nexport function publishBooksChanged(): void {\n safePublish(ACCOUNTING_BOOKS_CHANNEL, {});\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 {\n invalidateSnapshotsFrom as ioInvalidateFrom,\n invalidateAllSnapshots as ioInvalidateAll,\n listJournalPeriods,\n readJournalMonth,\n readSnapshot,\n writeSnapshot,\n} from \"./io.js\";\nimport { aggregateBalances } from \"./report.js\";\nimport { publishBookChange } from \"./eventPublisher.js\";\nimport { log } from \"./context.js\";\nimport { errorMessage, BOOK_EVENT_KINDS as ACCOUNTING_BOOK_EVENT_KINDS } from \"../shared\";\nimport type { AccountBalance, JournalEntry, MonthSnapshot } from \"./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 const [year, month] = 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 Array.from(map.entries())\n .map(([accountCode, netDebit]) => ({ accountCode, netDebit }))\n .sort((lhs, rhs) => lhs.accountCode.localeCompare(rhs.accountCode));\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 periods = await listJournalPeriods(bookId, workspaceRoot);\n if (periods.length === 0 || period < periods[0]) {\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 > periods[0]) {\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\nconst rebuildQueues = new Map<string, RebuildQueueEntry>();\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(bookId: 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(bookId);\n return queue !== undefined && queue.pendingFromPeriod !== null && period >= queue.pendingFromPeriod;\n}\n\nfunction isCancelled(bookId: string): boolean {\n return rebuildQueues.get(bookId)?.cancelled === true;\n}\n\nasync function runRebuild(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 });\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(bookId)) break;\n if (isInvalidatedDuringRebuild(bookId, 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(bookId)) break;\n if (isInvalidatedDuringRebuild(bookId, monthKey)) break;\n await writeSnapshot(bookId, { period: monthKey, balances, builtAt: new Date().toISOString() }, workspaceRoot);\n if (isCancelled(bookId)) {\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(bookId, 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 });\n }\n log.info(\"accounting\", \"snapshot rebuild done\", { bookId, periods: written, durationMs: Date.now() - startedAt });\n}\n\nfunction startRebuild(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(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(bookId);\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(bookId);\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(bookId, nextFrom, nextRoot);\n successor.coalescedWriteCount += carriedCount;\n rebuildQueues.set(bookId, successor);\n } else {\n rebuildQueues.delete(bookId);\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 existing = rebuildQueues.get(bookId);\n if (!existing) {\n rebuildQueues.set(bookId, startRebuild(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): Promise<void> {\n while (rebuildQueues.has(bookId)) {\n const entry = rebuildQueues.get(bookId);\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): void {\n const entry = rebuildQueues.get(bookId);\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(bookId: string): {\n running: boolean;\n runningFromPeriod: string | null;\n pendingFromPeriod: string | null;\n coalescedWriteCount: number;\n} {\n const entry = rebuildQueues.get(bookId);\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 \"./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\";\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, validateOpening } from \"./openingBalances.js\";\nimport { normalizeStoredAccount } from \"./accountNormalize.js\";\nimport { isValidCalendarDate, localDateString, makeEntry, makeVoidEntries, validateEntry, voidedIdSet } 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 { Account, AccountingConfig, BookSummary, JournalEntry, JournalLine, ReportPeriod } from \"./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/** Narrow a free-form `fiscalYearEnd` input. Absent → default\n * (back-compat with old callers and pre-field on-disk books); any\n * other value must be a month number 1-12 or 400. */\nfunction narrowFiscalYearEnd(raw: number | undefined): FiscalYearEnd {\n if (raw === undefined) return DEFAULT_FISCAL_YEAR_END;\n if (!isFiscalYearEnd(raw)) throw unsupportedFiscalYearEndError(raw);\n return raw;\n}\n\n/** Boundary checks shared by updateBook. Throws on the first failure\n * so the surrounding function stays under the cognitive-complexity\n * threshold; each rule is also unit-testable independently via the\n * service entry point. */\nfunction validateUpdateBookInput(input: { name?: string; country?: string; fiscalYearEnd?: number }): void {\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 // 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 (input.country !== undefined && input.country !== \"\" && !isSupportedCountryCode(input.country)) {\n throw unsupportedCountryError(input.country);\n }\n // Fiscal-year end has no \"clear\" path — it always resolves to a\n // closing month (back-compat reads default absent → December).\n // Reject any supplied value that isn't a month number 1-12.\n if (input.fiscalYearEnd !== undefined && !isFiscalYearEnd(input.fiscalYearEnd)) {\n throw unsupportedFiscalYearEndError(input.fiscalYearEnd);\n }\n}\n\nexport async function createBook(\n input: { id?: string; name: string; currency?: string; country?: string; fiscalYearEnd?: number },\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 if (input.country !== undefined && !isSupportedCountryCode(input.country)) {\n throw unsupportedCountryError(input.country);\n }\n const fiscalYearEnd = narrowFiscalYearEnd(input.fiscalYearEnd);\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 // Narrowed by the isSupportedCountryCode check above.\n ...(input.country ? { country: input.country as SupportedCountryCode } : {}),\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();\n return { book };\n}\n\nexport async function updateBook(\n input: { bookId: string; name?: string; country?: string; fiscalYearEnd?: number },\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 validateUpdateBookInput(input);\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 ...(input.country !== undefined && input.country !== \"\" ? { country: input.country as SupportedCountryCode } : {}),\n ...(input.fiscalYearEnd !== undefined ? { fiscalYearEnd: input.fiscalYearEnd as 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 (input.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();\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);\n await awaitRebuildIdle(input.bookId);\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();\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 }, 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\nexport async function upsertAccount(\n input: { bookId?: string; account: Account },\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 // Account codes starting with `_` are reserved for synthetic\n // rows that the report layer injects (e.g. the\n // `_currentEarnings` row added to the Equity section by\n // buildBalanceSheet). Forbid user accounts in that namespace so\n // a B/S can't display two rows with the same code or\n // accidentally lose a real account behind the synthetic label.\n if (typeof input.account?.code !== \"string\" || input.account.code.length === 0) {\n throw new AccountingError(400, \"account code is required\");\n }\n if (input.account.code.startsWith(\"_\")) {\n throw new AccountingError(400, `account code ${JSON.stringify(input.account.code)} is reserved (codes starting with _ are used for synthetic report rows)`);\n }\n const accounts = await readAccounts(bookId, workspaceRoot);\n const existingIdx = accounts.findIndex((account) => account.code === input.account.code);\n const next = [...accounts];\n const oldType = existingIdx >= 0 ? accounts[existingIdx].type : null;\n // Whitelist + active-flag policy lives in normalizeStoredAccount\n // (see ./accountNormalize.ts) so the rules are unit-testable in\n // isolation and this service function stays focused on the\n // file-IO + snapshot-invalidation orchestration.\n const stored = normalizeStoredAccount(input.account, existingIdx >= 0 ? accounts[existingIdx] : undefined);\n if (existingIdx >= 0) {\n next[existingIdx] = stored;\n } else {\n next.push(stored);\n }\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 (oldType !== null && oldType !== input.account.type) {\n scheduleRebuild(bookId, \"0000-00\", workspaceRoot);\n await invalidateAllSnapshots(bookId, workspaceRoot);\n }\n publishBookChange(bookId, { kind: ACCOUNTING_BOOK_EVENT_KINDS.accounts });\n return { bookId, account: { ...input.account }, accounts: next };\n}\n\n// ── journal entries ────────────────────────────────────────────────\n\nexport interface AddEntriesItem {\n date: string;\n lines: JournalLine[];\n memo?: string;\n replacesEntryId?: string;\n}\n\ninterface BatchValidationFailure {\n index: number;\n errors: unknown;\n}\n\n// All-or-nothing validation: collect failures across every entry\n// so the whole batch can be rejected before any write touches disk\n// (a half-applied batch can never end up persisted).\nfunction collectBatchValidationFailures(items: readonly AddEntriesItem[], accounts: readonly Account[]): BatchValidationFailure[] {\n const failures: BatchValidationFailure[] = [];\n for (let idx = 0; idx < items.length; idx++) {\n const item = items[idx];\n const validation = validateEntry({ date: item.date, lines: item.lines, accounts });\n if (!validation.ok) failures.push({ index: idx, errors: validation.errors });\n }\n return failures;\n}\n\nfunction buildBatchEntries(items: readonly AddEntriesItem[]): 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.\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; entries: AddEntriesItem[] },\n workspaceRoot?: string,\n): Promise<{ bookId: string; entries: JournalEntry[] }> {\n const config = await loadOrInitConfig(workspaceRoot);\n const bookId = resolveBookId(config, input.bookId);\n if (!Array.isArray(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 = collectBatchValidationFailures(input.entries, accounts);\n if (failures.length > 0) throw new AccountingError(400, \"invalid journal entries\", failures);\n const built = buildBatchEntries(input.entries);\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 });\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; entryId: string; reason?: string; voidDate?: string },\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 });\n return { bookId, reverseEntry: reverse, markerEntry: marker };\n}\n\ninterface ListEntriesInput {\n bookId?: string;\n from?: string;\n to?: string;\n accountCode?: string;\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(input: { bookId?: string }, workspaceRoot?: string): 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; asOfDate: string; lines: JournalLine[]; memo?: string },\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 validation = validateOpening({\n asOfDate: input.asOfDate,\n lines: input.lines,\n accounts,\n existingEntries: all,\n });\n if (!validation.ok) {\n throw new AccountingError(400, \"invalid opening balances\", validation.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: input.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 });\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, month] = 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; 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; 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; accountCode: string; period?: ReportPeriod },\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 if (typeof value !== \"string\" || !(TIME_SERIES_METRICS as readonly string[]).includes(value)) {\n throw new AccountingError(400, `getTimeSeries: metric must be one of ${TIME_SERIES_METRICS.join(\", \")}`);\n }\n return value as TimeSeriesMetric;\n}\n\nfunction ensureGranularity(value: unknown): TimeSeriesGranularity {\n if (typeof value !== \"string\" || !(TIME_SERIES_GRANULARITIES as readonly string[]).includes(value)) {\n throw new AccountingError(400, `getTimeSeries: granularity must be one of ${TIME_SERIES_GRANULARITIES.join(\", \")}`);\n }\n return value as TimeSeriesGranularity;\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;\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 }, 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 });\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). Mirrors the host's\n// server/utils/asyncHandler.ts, scoped to this package's logger.\n\nimport type { Request, Response, NextFunction } from \"express\";\nimport { log } from \"./context.js\";\nimport { errorMessage } from \"../shared/errors.js\";\n\nexport function asyncHandler<TReq = Request, TRes = 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 const expressReq = req as Request;\n const expressRes = res as Response;\n log.error(namespace, \"handler threw\", { route: expressReq.path, error: errorMessage(err) });\n if (expressRes.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 expressRes.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\";\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 \"./types.js\";\nimport { ACCOUNTING_ACTIONS, ACCOUNTING_API } from \"../shared\";\nimport { log } from \"./context.js\";\nimport { asyncHandler } from \"./http.js\";\n\ninterface AccountingActionBody {\n action: string;\n [key: string]: unknown;\n}\n\ninterface AccountingErrorResponse {\n error: string;\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;\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}\n\ntype ActionRest = Omit<AccountingActionBody, \"action\">;\ntype ActionHandler = (rest: ActionRest) => Promise<unknown>;\n\n/** Coerce an inbound `fiscalYearEnd` to a number. The UI select and\n * the LLM tool (JSON-schema `number` enum) both send a number, but a\n * numeric string (\"8\") from a hand-rolled client is accepted too so\n * it isn't silently dropped to `undefined` (→ the December default).\n * Range validation stays authoritative in the service layer. */\nfunction coerceFiscalYearEnd(raw: unknown): number | undefined {\n if (typeof raw === \"number\") return raw;\n if (typeof raw === \"string\" && raw.trim() !== \"\" && Number.isInteger(Number(raw))) return Number(raw);\n return undefined;\n}\n\n// Each action is a tiny adapter that pulls the typed slice it needs\n// out of the loosely-typed body. Validation of the slice shape\n// itself lives inside the service layer (validateEntry,\n// validateOpening) so the adapters can stay one-liners.\n\nasync function handleOpenBook(rest: ActionRest): 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();\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 return { kind: \"accounting-app\", bookId: rest.bookId, initialTab, books: list.books };\n}\n\nasync function handleGetReport(rest: ActionRest): Promise<unknown> {\n const kind = String(rest.kind ?? \"\");\n const periodInput = rest.period as { kind: \"month\"; period: string } | { kind: \"range\"; from: string; to: string } | undefined;\n const bookId = rest.bookId as string | undefined;\n if (kind === \"balance\") {\n if (!periodInput) throw new AccountingError(400, \"getReport balance: period is required\");\n return getBalanceSheetReport({ bookId, period: periodInput });\n }\n if (kind === \"pl\") {\n if (!periodInput) throw new AccountingError(400, \"getReport pl: period is required\");\n return getProfitLossReport({ bookId, period: periodInput });\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 });\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]: () => listBooks(),\n [ACCOUNTING_ACTIONS.createBook]: async (rest) => {\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 name: String(rest.name ?? \"\"),\n currency: typeof rest.currency === \"string\" ? rest.currency : undefined,\n country: typeof rest.country === \"string\" ? rest.country : undefined,\n fiscalYearEnd: coerceFiscalYearEnd(rest.fiscalYearEnd),\n });\n return { bookId: result.book.id, ...result };\n },\n [ACCOUNTING_ACTIONS.updateBook]: async (rest) => {\n const result = await updateBook({\n bookId: String(rest.bookId ?? \"\"),\n name: typeof rest.name === \"string\" ? rest.name : undefined,\n country: typeof rest.country === \"string\" ? rest.country : undefined,\n fiscalYearEnd: coerceFiscalYearEnd(rest.fiscalYearEnd),\n });\n return { bookId: result.book.id, ...result };\n },\n [ACCOUNTING_ACTIONS.deleteBook]: (rest) => deleteBook({ bookId: String(rest.bookId ?? \"\"), confirm: rest.confirm === true }),\n [ACCOUNTING_ACTIONS.getAccounts]: (rest) => listAccounts({ bookId: rest.bookId as string | undefined }),\n [ACCOUNTING_ACTIONS.upsertAccount]: (rest) =>\n upsertAccount({\n bookId: rest.bookId as string | undefined,\n // Service validates the shape — route doesn't reach into it.\n account: rest.account as never,\n }),\n [ACCOUNTING_ACTIONS.addEntries]: (rest) =>\n addEntries({\n bookId: rest.bookId as string | undefined,\n // Service validates each entry's shape — route doesn't reach into it.\n entries: (rest.entries ?? []) as never,\n }),\n [ACCOUNTING_ACTIONS.voidEntry]: (rest) =>\n voidEntry({\n bookId: rest.bookId as string | undefined,\n entryId: String(rest.entryId ?? \"\"),\n reason: rest.reason as string | undefined,\n voidDate: rest.voidDate as string | undefined,\n }),\n [ACCOUNTING_ACTIONS.getJournalEntries]: (rest) =>\n listEntries({\n bookId: rest.bookId as string | undefined,\n from: rest.from as string | undefined,\n to: rest.to as string | undefined,\n accountCode: rest.accountCode as string | undefined,\n }),\n [ACCOUNTING_ACTIONS.getOpeningBalances]: (rest) => getOpeningBalances({ bookId: rest.bookId as string | undefined }),\n [ACCOUNTING_ACTIONS.setOpeningBalances]: (rest) =>\n setOpeningBalances({\n bookId: rest.bookId as string | undefined,\n asOfDate: String(rest.asOfDate ?? \"\"),\n lines: (rest.lines ?? []) as never,\n memo: rest.memo as string | undefined,\n }),\n [ACCOUNTING_ACTIONS.getReport]: handleGetReport,\n [ACCOUNTING_ACTIONS.getTimeSeries]: (rest) =>\n getTimeSeriesReport({\n bookId: rest.bookId as string | undefined,\n metric: rest.metric,\n granularity: rest.granularity,\n from: rest.from,\n to: rest.to,\n accountCode: rest.accountCode,\n }),\n [ACCOUNTING_ACTIONS.rebuildSnapshots]: (rest) => rebuildSnapshots({ bookId: rest.bookId as string | undefined }),\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 = fields.book as { id?: string; name?: string } | undefined;\n const subject = book?.name ? `A new book named ${JSON.stringify(book.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 = book?.id ? ` (id: ${book.id})` : \"\";\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 = fields.account as { code?: string; name?: string } | undefined;\n if (account?.code && account?.name) {\n return `Upserted account ${account.code} ${JSON.stringify(account.name)}.`;\n }\n return \"Updated the chart of accounts.\";\n },\n [ACCOUNTING_ACTIONS.addEntries]: (fields) => {\n const entries = Array.isArray(fields.entries) ? (fields.entries as { id?: string; date?: string }[]) : [];\n if (entries.length === 0) return \"Posted 0 journal entries.\";\n if (entries.length === 1) {\n const [entry] = entries;\n const idFragment = entry?.id ? ` (id: ${entry.id})` : \"\";\n return `Posted a journal entry on ${entry?.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 reverse = fields.reverseEntry as { date?: string } | undefined;\n return `Voided the entry; a reversing pair was posted on ${reverse?.date ?? \"today\"}.`;\n },\n [ACCOUNTING_ACTIONS.setOpeningBalances]: (fields) => {\n const opening = fields.openingEntry as { date?: string; lines?: unknown } | undefined;\n const verb = fields.replacedExisting === true ? \"replaced\" : \"set\";\n const date = 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 lines = Array.isArray(opening?.lines) ? (opening.lines as unknown[]) : [];\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 = fields.deletedBookId as string | undefined;\n const name = fields.deletedBookName as string | undefined;\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 = fields.book as { id?: string; name?: string; country?: string; currency?: string } | undefined;\n const name = book?.name ? JSON.stringify(book.name) : \"the book\";\n const countryFragment = book?.country ? ` (country: ${book.country})` : \"\";\n return `Updated ${name}${countryFragment}.`;\n },\n};\n\nfunction previewMessage(action: string, fields: Record<string, unknown>): string {\n // `Object.hasOwn` guard so a user-controlled `action` (e.g.\n // \"constructor\" / \"toString\") can't dispatch to an inherited\n // prototype method — own-property check before the dynamic call.\n const head = Object.hasOwn(MESSAGE_BUILDERS, action) ? MESSAGE_BUILDERS[action](fields) : undefined;\n return head ? `${head} ${VIEW_VISIBLE_TRAILER}` : VIEW_VISIBLE_TRAILER;\n}\n\nasync function dispatch(body: AccountingActionBody): Promise<unknown> {\n const { action, ...rest } = body;\n // Own-property check (not just truthiness) before the dynamic call:\n // `ACTION_HANDLERS[action]` would otherwise resolve inherited\n // prototype methods (`toString`, `constructor`, …) for a crafted\n // `action`, dispatching to an unexpected target.\n if (!Object.hasOwn(ACTION_HANDLERS, action)) throw new AccountingError(400, `unknown action ${JSON.stringify(action)}`);\n const handler = ACTION_HANDLERS[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);\n const handlerFields = result && typeof result === \"object\" ? (result as Record<string, unknown>) : { 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/** 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(...)`. */\nexport function createAccountingRouter(): 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 const result = await dispatch(body);\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/context.ts","../src/server/atomic.ts","../src/server/io.ts","../src/server/types.ts","../src/server/journal.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":["// 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\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/** Logger shape — mirrors the host server logger\n * `log.{level}(namespace, message, data?)`. `data` uses\n * `Record<string, unknown>` (not `object`) so the host's `Logger`\n * is structurally assignable when injected. */\nexport interface AccountingLogger {\n error: (namespace: string, message: string, data?: Record<string, unknown>) => void;\n warn: (namespace: string, message: string, data?: Record<string, unknown>) => void;\n info: (namespace: string, message: string, data?: Record<string, unknown>) => void;\n debug: (namespace: string, message: string, data?: Record<string, unknown>) => void;\n}\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 workspaceRoot: string;\n logger: AccountingLogger;\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). */\nexport function defaultWorkspaceRoot(): string {\n if (!deps) {\n throw new Error(\"@mulmoclaude/accounting-plugin: configureAccountingServer() must be called before serving accounting requests\");\n }\n return deps.workspaceRoot;\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","// Self-contained file-I/O primitives for the accounting backend.\n//\n// Reimplemented inside the package (rather than injected) because they\n// are small and generic — owning them keeps the host-injection surface\n// down to the truly host-specific bits (workspace root, pub/sub,\n// logger). Mirrors the host's server/utils/files/{atomic,json,safe}.ts:\n// atomic write = tmp file alongside destination + rename (readers never\n// see a half-written file).\n\nimport { promises as fsPromises } from \"node:fs\";\nimport path from \"node:path\";\nimport { randomUUID } from \"node:crypto\";\n\nexport interface WriteAtomicOptions {\n /** Use a per-write unique tmp filename. Defaults to `true` so two\n * concurrent writers targeting the same destination never race on a\n * shared `${filePath}.tmp` (one renaming/unlinking the other's tmp).\n * Pass `false` only when a stable tmp name is required. */\n uniqueTmp?: boolean;\n}\n\n/** True for a `not found` filesystem error. */\nexport function isEnoent(err: unknown): boolean {\n return typeof err === \"object\" && err !== null && \"code\" in err && (err as { code?: unknown }).code === \"ENOENT\";\n}\n\n// On Windows, AV / Search Indexer / Defender briefly hold handles and\n// rename trips EPERM/EBUSY/EACCES. The retry loop is gated to Windows\n// because on POSIX those codes mean a real permission problem and\n// retrying just adds latency before the inevitable throw. Mirrors the\n// host's server/utils/files/atomic.ts.\nconst IS_WINDOWS = process.platform === \"win32\";\nconst RENAME_RETRY_DELAYS_MS = [30, 100, 300] as const;\n\nfunction hasErrnoCode(err: unknown): err is { code: string } {\n return typeof err === \"object\" && err !== null && \"code\" in err && typeof (err as { code: unknown }).code === \"string\";\n}\n\nfunction isTransientRenameError(err: unknown): boolean {\n if (!IS_WINDOWS || !hasErrnoCode(err)) return false;\n return err.code === \"EPERM\" || err.code === \"EBUSY\" || err.code === \"EACCES\";\n}\n\nasync function renameWithWindowsRetry(fromPath: string, toPath: string): Promise<void> {\n for (const delayMs of RENAME_RETRY_DELAYS_MS) {\n try {\n await fsPromises.rename(fromPath, toPath);\n return;\n } catch (err) {\n if (!isTransientRenameError(err)) throw err;\n await new Promise((resolve) => setTimeout(resolve, delayMs));\n }\n }\n // Final attempt — let any error propagate.\n await fsPromises.rename(fromPath, toPath);\n}\n\n/** Atomic write: unique tmp alongside destination, then rename. */\nexport async function writeFileAtomic(filePath: string, content: string, opts: WriteAtomicOptions = {}): Promise<void> {\n const uniqueTmp = opts.uniqueTmp ?? true;\n const tmp = uniqueTmp ? `${filePath}.${randomUUID()}.tmp` : `${filePath}.tmp`;\n await fsPromises.mkdir(path.dirname(filePath), { recursive: true });\n try {\n await fsPromises.writeFile(tmp, content, { encoding: \"utf-8\" });\n await renameWithWindowsRetry(tmp, filePath);\n } catch (err) {\n await fsPromises.unlink(tmp).catch(() => {});\n throw err;\n }\n}\n\n/** Atomic JSON write (2-space indent), the only serialization shape the\n * accounting io layer needs. */\nexport async function writeJsonAtomic(filePath: string, data: unknown, opts: WriteAtomicOptions = {}): Promise<void> {\n await writeFileAtomic(filePath, JSON.stringify(data, null, 2), opts);\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 { ACCOUNTING_DIRS as WORKSPACE_DIRS, resolveFiscalYearEnd } from \"../shared\";\nimport { writeJsonAtomic, isEnoent } from \"./atomic.js\";\nimport type { AccountingConfig, Account, BookSummary, JournalEntry, MonthSnapshot } from \"./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 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/** 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 entries: JournalEntry[] = [];\n let skipped = 0;\n for (const line of raw.split(\"\\n\")) {\n if (line.trim() === \"\") continue;\n try {\n entries.push(JSON.parse(line) as JournalEntry);\n } catch {\n skipped += 1;\n }\n }\n return { entries, skipped };\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 >= 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","// Domain types for the accounting plugin (opt-in, custom-Role only).\n//\n// Source-of-truth files on disk:\n// data/accounting/config.json ← AccountingConfig\n// data/accounting/books/<id>/accounts.json ← Account[]\n// data/accounting/books/<id>/journal/YYYY-MM.jsonl ← JournalEntry per line\n// data/accounting/books/<id>/snapshots/YYYY-MM.json ← MonthSnapshot (cache)\n//\n// Snapshots are cache only — journal is the single source of truth.\n\nimport type { SupportedCountryCode, FiscalYearEnd } from \"../shared\";\n\nexport const ACCOUNT_TYPES = [\"asset\", \"liability\", \"equity\", \"income\", \"expense\"] as const;\nexport type AccountType = (typeof ACCOUNT_TYPES)[number];\n\n/** B/S accounts (assets / liabilities / equity). Used by opening\n * balance validation: opening entries reference balance-sheet\n * accounts only. */\nexport const BALANCE_SHEET_ACCOUNT_TYPES: readonly AccountType[] = [\"asset\", \"liability\", \"equity\"];\n\nexport interface Account {\n /** Stable identifier the journal lines reference. Typically a\n * numeric string (\"1000\" / \"2000\" …) but free-form is allowed\n * so the user can adopt their existing numbering. */\n code: string;\n name: string;\n type: AccountType;\n /** Optional free-form note (tax bucket, parent group, …). Not\n * interpreted by the engine — passes through verbatim. */\n note?: string;\n /** Soft-delete flag. When `false`, the account is hidden from\n * entry/ledger dropdowns but stays visible in Manage Accounts\n * and historical entries — accounting integrity requires that\n * a code referenced by a journal line never disappears. Omitted\n * (treated as active) by default to keep the JSON files clean\n * for books created before this field existed. */\n active?: boolean;\n}\n\nexport interface BookSummary {\n id: string;\n name: string;\n /** ISO 4217 (e.g. \"USD\" / \"JPY\"). Single-currency per book — no\n * cross-book aggregation. */\n currency: string;\n /** ISO 3166-1 alpha-2 country code (e.g. \"US\" / \"JP\" / \"GB\").\n * Identifies the tax jurisdiction the book is kept under so the\n * Accounting role can give country-aware advice (Japanese T-number\n * under インボイス制度, EU VAT ID, GSTIN, ABN, etc.). Constrained\n * to `SupportedCountryCode` (the curated list shared with the UI\n * dropdown and the LLM tool's JSON-schema enum) so a typo from any\n * ingress path is rejected at the service layer rather than silently\n * persisted. Optional for backward compatibility with books created\n * before the field was introduced; the UI prompts existing books\n * to set it. */\n country?: SupportedCountryCode;\n /** Calendar month (1-12) on whose LAST DAY the book's fiscal year\n * closes — e.g. 8 = August 31, 12 = December 31 (calendar year).\n * Drives the UI's \"current quarter / current year\" date-range\n * shortcuts. Optional in the persisted shape for backward\n * compatibility with books written before this field existed (and\n * with the earlier \"Q1\"..\"Q4\" token form) — read-side code\n * normalises both via `resolveFiscalYearEnd`, treating an absent\n * value as December. New books require it at the create boundary;\n * the default is 12 (December). */\n fiscalYearEnd?: FiscalYearEnd;\n createdAt: string;\n}\n\nexport interface AccountingConfig {\n books: BookSummary[];\n}\n\nexport type JournalEntryKind = \"normal\" | \"opening\" | \"void\" | \"void-marker\";\n\nexport interface JournalLine {\n accountCode: string;\n /** Use exactly one of debit / credit per line, both as positive\n * numbers. The engine treats them as separate fields rather than\n * a single signed amount so the input matches a standard\n * bookkeeping form. */\n debit?: number;\n credit?: number;\n /** Per-line memo (the entry-level memo lives on JournalEntry). */\n memo?: string;\n /** Counterparty's tax-authority-issued registration ID for this\n * line — Japanese 適格請求書発行事業者登録番号 (T-number), EU\n * VAT identification number, UK VAT registration number, India\n * GSTIN, Australia ABN, etc. Required for input-tax-credit\n * eligibility under the Japanese インボイス制度 (effective\n * 2023-10-01) and equivalent regimes elsewhere. Free-form string;\n * format validation belongs upstream (per-jurisdiction). */\n taxRegistrationId?: string;\n}\n\nexport interface JournalEntry {\n /** Globally unique within a book — ULID-style; ordering by id\n * reproduces creation order. */\n id: string;\n /** Calendar date the entry is booked for (YYYY-MM-DD). The month\n * part decides which `journal/YYYY-MM.jsonl` file the entry lives\n * in; entries can be for any past / future date. */\n date: string;\n kind: JournalEntryKind;\n lines: JournalLine[];\n /** Entry-level memo. */\n memo?: string;\n /** When `kind === \"void-marker\"`: id of the entry being voided.\n * When `kind === \"void\"`: the system-generated reverse entry\n * references the original via this field. */\n voidedEntryId?: string;\n /** Reason supplied by the user when voiding. */\n voidReason?: string;\n /** When this entry was posted via the \"edit\" flow (void-then-add),\n * this is the id of the entry it replaces. The void + new-entry\n * pair is *not* atomic on the server — the client issues two\n * sequential calls — but recording the link here makes the\n * edit chain queryable later (e.g. \"what corrected entry X?\"). */\n replacesEntryId?: string;\n /** ISO timestamp the entry was appended to the journal — the\n * authoritative \"when did this hit the books\" clock. Distinct\n * from `date`, which is the user-visible booking date. */\n createdAt: string;\n}\n\n/** Aggregated balance per account at a point in time. The signed\n * number is debit − credit; downstream display logic converts to\n * natural sign per account type (assets debit-positive, liabilities\n * credit-positive). */\nexport interface AccountBalance {\n accountCode: string;\n /** Σ debit − Σ credit across all entries up to and including the\n * snapshot's period end. */\n netDebit: number;\n}\n\nexport interface MonthSnapshot {\n /** \"YYYY-MM\" — the closing month covered. */\n period: string;\n /** Closing balances at end of `period`. */\n balances: AccountBalance[];\n /** ISO timestamp the snapshot file was written. */\n builtAt: string;\n}\n\n/** Period selector for reports. Either a single closing month or a\n * date range. Always inclusive on both ends. */\nexport type ReportPeriod = { kind: \"month\"; period: string } | { kind: \"range\"; from: string; to: string };\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\";\n\nimport type { Account, JournalEntry, JournalLine } from \"./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\nexport interface ValidationResult {\n ok: boolean;\n errors: ValidationError[];\n}\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 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\n/** Pure validation. Does not throw; returns a list of issues so the\n * REST handler can return a structured 400 instead of an opaque\n * 500. */\nfunction validateLine(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 (line.debit !== undefined && !isNonNegativeNumber(line.debit)) {\n errors.push({ field: `lines[${idx}].debit`, message: \"debit must be a non-negative finite number\" });\n }\n if (line.credit !== undefined && !isNonNegativeNumber(line.credit)) {\n errors.push({ field: `lines[${idx}].credit`, message: \"credit must be a non-negative finite number\" });\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 if (line.taxRegistrationId !== undefined) {\n if (typeof line.taxRegistrationId !== \"string\") {\n errors.push({ field: `lines[${idx}].taxRegistrationId`, message: \"must be a string\" });\n } else if (line.taxRegistrationId.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 ${line.taxRegistrationId.trim().length})`,\n });\n }\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\nexport function validateEntry(input: { date: string; lines: readonly JournalLine[]; accounts: readonly Account[] }): ValidationResult {\n const errors: ValidationError[] = [];\n if (!isValidCalendarDate(input.date)) {\n errors.push({ field: \"date\", message: `expected YYYY-MM-DD calendar date, got ${JSON.stringify(input.date)}` });\n }\n if (!Array.isArray(input.lines) || input.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 accountCodes = new Set(input.accounts.map((account) => account.code));\n input.lines.forEach((line, idx) => validateLine(line, idx, accountCodes, errors));\n const net = netBalance(input.lines);\n if (Math.abs(net) > EQUALITY_TOLERANCE) {\n errors.push({ field: \"lines\", message: `Σ debit − Σ credit = ${net.toFixed(4)}; entry must balance` });\n }\n return { ok: errors.length === 0, errors };\n}\n\n/** Build a JournalEntry — validation is the caller's responsibility\n * (it should have called `validateEntry` first). 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;\n kind?: JournalEntry[\"kind\"];\n replacesEntryId?: string;\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","// 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 type { Account, JournalEntry, JournalLine } from \"./types.js\";\nimport { BALANCE_SHEET_ACCOUNT_TYPES } from \"./types.js\";\nimport { isValidCalendarDate, netBalance, voidedIdSet } from \"./journal.js\";\n\nconst EQUALITY_TOLERANCE = 0.005;\n\nexport interface OpeningValidationError {\n field: string;\n message: string;\n}\n\nexport interface OpeningValidationResult {\n ok: boolean;\n errors: OpeningValidationError[];\n}\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 OpeningValidationInput {\n asOfDate: string;\n lines: readonly JournalLine[];\n accounts: readonly Account[];\n existingEntries: readonly JournalEntry[];\n}\n\nfunction validateLineAccountTypes(input: OpeningValidationInput, errors: OpeningValidationError[]): void {\n const accountByCode = new Map(input.accounts.map((account) => [account.code, account]));\n input.lines.forEach((line, idx) => {\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\nfunction validateAsOfPredatesEverything(input: OpeningValidationInput, 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/** Validate inputs for `setOpeningBalances`. Caller passes the full\n * list of journal entries in the book so we can check the\n * \"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 validateOpening(input: OpeningValidationInput): OpeningValidationResult {\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 (!Array.isArray(input.lines)) {\n errors.push({ field: \"lines\", message: \"lines must be an array\" });\n return { ok: false, errors };\n }\n validateLineAccountTypes(input, errors);\n const net = netBalance(input.lines);\n if (Math.abs(net) > EQUALITY_TOLERANCE) {\n errors.push({ field: \"lines\", message: `Σ debit − Σ credit = ${net.toFixed(4)}; opening must balance` });\n }\n validateAsOfPredatesEverything(input, errors);\n return { ok: errors.length === 0, errors };\n}\n","// Pure normalization for the persisted Account record. Lives in its\n// own module so unit tests can exercise the field-whitelist + active-\n// flag policy without spinning up the file system, and so the\n// service-layer `upsertAccount` stays under the repo's 20-line\n// guideline.\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 type { Account } from \"./types.js\";\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, JournalEntry } from \"./types.js\";\n\nconst ZERO_TOLERANCE = 0.0049; // hide rows that round to 0 at 2dp\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 Array.from(map.entries())\n .map(([accountCode, netDebit]) => ({ accountCode, netDebit }))\n .sort((lhs, rhs) => lhs.accountCode.localeCompare(rhs.accountCode));\n}\n\nexport interface BalanceSheetSection {\n type: AccountType;\n rows: { accountCode: string; accountName: string; balance: number }[];\n total: number;\n}\n\nexport interface BalanceSheet {\n asOf: string; // ISO date; period end\n sections: BalanceSheetSection[];\n /** Σ assets − Σ (liabilities + equity). Should be 0 (the\n * accounting equation); a non-zero here indicates either a\n * rounding artefact or a data problem. */\n imbalance: number;\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\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 const sections: BalanceSheetSection[] = [];\n for (const type of [\"asset\", \"liability\", \"equity\"] as const) {\n const rows: BalanceSheetSection[\"rows\"] = [];\n let total = 0;\n for (const account of input.accounts) {\n if (account.type !== type) continue;\n const netDebit = balanceByCode.get(account.code) ?? 0;\n const presented = naturalSign(type, netDebit);\n if (Math.abs(presented) <= ZERO_TOLERANCE) continue;\n rows.push({ accountCode: account.code, accountName: account.name, balance: presented });\n total += presented;\n }\n if (type === \"equity\" && Math.abs(currentEarnings) > ZERO_TOLERANCE) {\n rows.push({ accountCode: CURRENT_EARNINGS_ACCOUNT_CODE, accountName: \"Current period earnings\", balance: currentEarnings });\n total += currentEarnings;\n }\n sections.push({ type, rows, total });\n }\n const assetTotal = sections[0].total;\n const liabEquityTotal = sections[1].total + sections[2].total;\n return {\n asOf: input.asOf,\n sections,\n imbalance: assetTotal - liabEquityTotal,\n };\n}\n\nexport interface ProfitLoss {\n from: string; // inclusive ISO date\n to: string; // inclusive ISO date\n income: { rows: { accountCode: string; accountName: string; amount: number }[]; total: number };\n expense: { rows: { accountCode: string; accountName: string; amount: number }[]; total: number };\n netIncome: number; // income − expense\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\nexport interface LedgerRow {\n entryId: string;\n date: string;\n kind: JournalEntry[\"kind\"];\n memo?: string;\n debit: number;\n credit: number;\n /** Running netDebit balance for this account, in entry order. */\n runningBalance: number;\n /** Counterparty tax-registration ID copied from the source\n * journal line (T-number / VAT ID / GSTIN / ABN). Surfaced as a\n * Ledger column when the active account is in the input-tax\n * band (14xx — see `isTaxAccountCode` in\n * src/plugins/accounting/components/accountNumbering.ts).\n * Carried per row even on non-tax accounts so a future view\n * that wants to show it elsewhere doesn't need a server change. */\n taxRegistrationId?: string;\n}\n\nexport interface Ledger {\n accountCode: string;\n accountName: string;\n rows: LedgerRow[];\n /** Closing netDebit balance — the sum at the bottom of `rows`. */\n closingBalance: number;\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; to?: string }): 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 \"./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 const [year, month, day] = 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;\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;\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 { bookChannel as accountingBookChannel, ACCOUNTING_BOOKS_CHANNEL, type BookChannelPayload as AccountingBookChannelPayload } from \"../shared\";\nimport { 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\nfunction safePublish(channel: string, payload: unknown): void {\n if (!pubsub) return;\n try {\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). */\nexport function publishBookChange(bookId: string, payload: AccountingBookChannelPayload): void {\n safePublish(accountingBookChannel(bookId), payload);\n}\n\n/** Fired when the *list* of books changes (createBook, deleteBook).\n * Payload is intentionally empty — subscribers refetch from\n * /api/accounting. */\nexport function publishBooksChanged(): void {\n safePublish(ACCOUNTING_BOOKS_CHANNEL, {});\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 {\n invalidateSnapshotsFrom as ioInvalidateFrom,\n invalidateAllSnapshots as ioInvalidateAll,\n listJournalPeriods,\n readJournalMonth,\n readSnapshot,\n writeSnapshot,\n} from \"./io.js\";\nimport { aggregateBalances } from \"./report.js\";\nimport { publishBookChange } from \"./eventPublisher.js\";\nimport { log } from \"./context.js\";\nimport { errorMessage, BOOK_EVENT_KINDS as ACCOUNTING_BOOK_EVENT_KINDS } from \"../shared\";\nimport type { AccountBalance, JournalEntry, MonthSnapshot } from \"./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 const [year, month] = 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 Array.from(map.entries())\n .map(([accountCode, netDebit]) => ({ accountCode, netDebit }))\n .sort((lhs, rhs) => lhs.accountCode.localeCompare(rhs.accountCode));\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 periods = await listJournalPeriods(bookId, workspaceRoot);\n if (periods.length === 0 || period < periods[0]) {\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 > periods[0]) {\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\nconst rebuildQueues = new Map<string, RebuildQueueEntry>();\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(bookId: 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(bookId);\n return queue !== undefined && queue.pendingFromPeriod !== null && period >= queue.pendingFromPeriod;\n}\n\nfunction isCancelled(bookId: string): boolean {\n return rebuildQueues.get(bookId)?.cancelled === true;\n}\n\nasync function runRebuild(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 });\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(bookId)) break;\n if (isInvalidatedDuringRebuild(bookId, 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(bookId)) break;\n if (isInvalidatedDuringRebuild(bookId, monthKey)) break;\n await writeSnapshot(bookId, { period: monthKey, balances, builtAt: new Date().toISOString() }, workspaceRoot);\n if (isCancelled(bookId)) {\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(bookId, 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 });\n }\n log.info(\"accounting\", \"snapshot rebuild done\", { bookId, periods: written, durationMs: Date.now() - startedAt });\n}\n\nfunction startRebuild(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(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(bookId);\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(bookId);\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(bookId, nextFrom, nextRoot);\n successor.coalescedWriteCount += carriedCount;\n rebuildQueues.set(bookId, successor);\n } else {\n rebuildQueues.delete(bookId);\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 existing = rebuildQueues.get(bookId);\n if (!existing) {\n rebuildQueues.set(bookId, startRebuild(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): Promise<void> {\n while (rebuildQueues.has(bookId)) {\n const entry = rebuildQueues.get(bookId);\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): void {\n const entry = rebuildQueues.get(bookId);\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(bookId: string): {\n running: boolean;\n runningFromPeriod: string | null;\n pendingFromPeriod: string | null;\n coalescedWriteCount: number;\n} {\n const entry = rebuildQueues.get(bookId);\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 \"./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\";\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, validateOpening } from \"./openingBalances.js\";\nimport { normalizeStoredAccount } from \"./accountNormalize.js\";\nimport { isValidCalendarDate, localDateString, makeEntry, makeVoidEntries, validateEntry, voidedIdSet } 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 { Account, AccountingConfig, BookSummary, JournalEntry, JournalLine, ReportPeriod } from \"./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 shared by updateBook (name / country only —\n * fiscalYearEnd is coerced + validated separately via\n * `coerceFiscalYearEndInput`). Throws on the first failure so the\n * surrounding function stays under the cognitive-complexity threshold;\n * each rule is also unit-testable independently via the service entry\n * point. */\nfunction validateUpdateBookInput(input: { name?: string; country?: string }): void {\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 // 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 (input.country !== undefined && input.country !== \"\" && !isSupportedCountryCode(input.country)) {\n throw unsupportedCountryError(input.country);\n }\n}\n\nexport async function createBook(\n input: { id?: string; name: string; currency?: string; country?: string; 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 if (input.country !== undefined && !isSupportedCountryCode(input.country)) {\n throw unsupportedCountryError(input.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 // Narrowed by the isSupportedCountryCode check above.\n ...(input.country ? { country: input.country as SupportedCountryCode } : {}),\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();\n return { book };\n}\n\nexport async function updateBook(\n input: { bookId: string; name?: string; country?: string; 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 validateUpdateBookInput(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 ...(input.country !== undefined && input.country !== \"\" ? { country: input.country as SupportedCountryCode } : {}),\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 (input.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();\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);\n await awaitRebuildIdle(input.bookId);\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();\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 }, 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\nexport async function upsertAccount(\n input: { bookId?: string; account: Account },\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 // Account codes starting with `_` are reserved for synthetic\n // rows that the report layer injects (e.g. the\n // `_currentEarnings` row added to the Equity section by\n // buildBalanceSheet). Forbid user accounts in that namespace so\n // a B/S can't display two rows with the same code or\n // accidentally lose a real account behind the synthetic label.\n if (typeof input.account?.code !== \"string\" || input.account.code.length === 0) {\n throw new AccountingError(400, \"account code is required\");\n }\n if (input.account.code.startsWith(\"_\")) {\n throw new AccountingError(400, `account code ${JSON.stringify(input.account.code)} is reserved (codes starting with _ are used for synthetic report rows)`);\n }\n const accounts = await readAccounts(bookId, workspaceRoot);\n const existingIdx = accounts.findIndex((account) => account.code === input.account.code);\n const next = [...accounts];\n const oldType = existingIdx >= 0 ? accounts[existingIdx].type : null;\n // Whitelist + active-flag policy lives in normalizeStoredAccount\n // (see ./accountNormalize.ts) so the rules are unit-testable in\n // isolation and this service function stays focused on the\n // file-IO + snapshot-invalidation orchestration.\n const stored = normalizeStoredAccount(input.account, existingIdx >= 0 ? accounts[existingIdx] : undefined);\n if (existingIdx >= 0) {\n next[existingIdx] = stored;\n } else {\n next.push(stored);\n }\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 (oldType !== null && oldType !== input.account.type) {\n scheduleRebuild(bookId, \"0000-00\", workspaceRoot);\n await invalidateAllSnapshots(bookId, workspaceRoot);\n }\n publishBookChange(bookId, { kind: ACCOUNTING_BOOK_EVENT_KINDS.accounts });\n return { bookId, account: { ...input.account }, accounts: next };\n}\n\n// ── journal entries ────────────────────────────────────────────────\n\nexport interface AddEntriesItem {\n date: string;\n lines: JournalLine[];\n memo?: string;\n replacesEntryId?: string;\n}\n\ninterface BatchValidationFailure {\n index: number;\n errors: unknown;\n}\n\n// All-or-nothing validation: collect failures across every entry\n// so the whole batch can be rejected before any write touches disk\n// (a half-applied batch can never end up persisted).\nfunction collectBatchValidationFailures(items: readonly AddEntriesItem[], accounts: readonly Account[]): BatchValidationFailure[] {\n const failures: BatchValidationFailure[] = [];\n for (let idx = 0; idx < items.length; idx++) {\n const item = items[idx];\n const validation = validateEntry({ date: item.date, lines: item.lines, accounts });\n if (!validation.ok) failures.push({ index: idx, errors: validation.errors });\n }\n return failures;\n}\n\nfunction buildBatchEntries(items: readonly AddEntriesItem[]): 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.\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; entries: AddEntriesItem[] },\n workspaceRoot?: string,\n): Promise<{ bookId: string; entries: JournalEntry[] }> {\n const config = await loadOrInitConfig(workspaceRoot);\n const bookId = resolveBookId(config, input.bookId);\n if (!Array.isArray(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 = collectBatchValidationFailures(input.entries, accounts);\n if (failures.length > 0) throw new AccountingError(400, \"invalid journal entries\", failures);\n const built = buildBatchEntries(input.entries);\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 });\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; entryId: string; reason?: string; voidDate?: string },\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 });\n return { bookId, reverseEntry: reverse, markerEntry: marker };\n}\n\ninterface ListEntriesInput {\n bookId?: string;\n from?: string;\n to?: string;\n accountCode?: string;\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(input: { bookId?: string }, workspaceRoot?: string): 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; asOfDate: string; lines: JournalLine[]; memo?: string },\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 validation = validateOpening({\n asOfDate: input.asOfDate,\n lines: input.lines,\n accounts,\n existingEntries: all,\n });\n if (!validation.ok) {\n throw new AccountingError(400, \"invalid opening balances\", validation.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: input.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 });\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, month] = 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; 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; 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; accountCode: string; period?: ReportPeriod },\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 if (typeof value !== \"string\" || !(TIME_SERIES_METRICS as readonly string[]).includes(value)) {\n throw new AccountingError(400, `getTimeSeries: metric must be one of ${TIME_SERIES_METRICS.join(\", \")}`);\n }\n return value as TimeSeriesMetric;\n}\n\nfunction ensureGranularity(value: unknown): TimeSeriesGranularity {\n if (typeof value !== \"string\" || !(TIME_SERIES_GRANULARITIES as readonly string[]).includes(value)) {\n throw new AccountingError(400, `getTimeSeries: granularity must be one of ${TIME_SERIES_GRANULARITIES.join(\", \")}`);\n }\n return value as TimeSeriesGranularity;\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;\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 }, 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 });\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). Mirrors the host's\n// server/utils/asyncHandler.ts, scoped to this package's logger.\n\nimport type { Request, Response, NextFunction } from \"express\";\nimport { log } from \"./context.js\";\nimport { errorMessage } from \"../shared/errors.js\";\n\nexport function asyncHandler<TReq = Request, TRes = 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 const expressReq = req as Request;\n const expressRes = res as Response;\n log.error(namespace, \"handler threw\", { route: expressReq.path, error: errorMessage(err) });\n if (expressRes.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 expressRes.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\";\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 \"./types.js\";\nimport { ACCOUNTING_ACTIONS, ACCOUNTING_API } from \"../shared\";\nimport { log } from \"./context.js\";\nimport { asyncHandler } from \"./http.js\";\n\ninterface AccountingActionBody {\n action: string;\n [key: string]: unknown;\n}\n\ninterface AccountingErrorResponse {\n error: string;\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;\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}\n\ntype ActionRest = Omit<AccountingActionBody, \"action\">;\ntype ActionHandler = (rest: ActionRest) => Promise<unknown>;\n\n// Each action is a tiny adapter that pulls the typed slice it needs\n// out of the loosely-typed body. Validation of the slice shape\n// itself lives inside the service layer (validateEntry,\n// validateOpening) so the adapters can stay one-liners.\n\nasync function handleOpenBook(rest: ActionRest): 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();\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 return { kind: \"accounting-app\", bookId: rest.bookId, initialTab, books: list.books };\n}\n\nasync function handleGetReport(rest: ActionRest): Promise<unknown> {\n const kind = String(rest.kind ?? \"\");\n const periodInput = rest.period as { kind: \"month\"; period: string } | { kind: \"range\"; from: string; to: string } | undefined;\n const bookId = rest.bookId as string | undefined;\n if (kind === \"balance\") {\n if (!periodInput) throw new AccountingError(400, \"getReport balance: period is required\");\n return getBalanceSheetReport({ bookId, period: periodInput });\n }\n if (kind === \"pl\") {\n if (!periodInput) throw new AccountingError(400, \"getReport pl: period is required\");\n return getProfitLossReport({ bookId, period: periodInput });\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 });\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]: () => listBooks(),\n [ACCOUNTING_ACTIONS.createBook]: async (rest) => {\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 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 return { bookId: result.book.id, ...result };\n },\n [ACCOUNTING_ACTIONS.updateBook]: async (rest) => {\n const result = await updateBook({\n 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 return { bookId: result.book.id, ...result };\n },\n [ACCOUNTING_ACTIONS.deleteBook]: (rest) => deleteBook({ bookId: String(rest.bookId ?? \"\"), confirm: rest.confirm === true }),\n [ACCOUNTING_ACTIONS.getAccounts]: (rest) => listAccounts({ bookId: rest.bookId as string | undefined }),\n [ACCOUNTING_ACTIONS.upsertAccount]: (rest) =>\n upsertAccount({\n bookId: rest.bookId as string | undefined,\n // Service validates the shape — route doesn't reach into it.\n account: rest.account as never,\n }),\n [ACCOUNTING_ACTIONS.addEntries]: (rest) =>\n addEntries({\n bookId: rest.bookId as string | undefined,\n // Service validates each entry's shape — route doesn't reach into it.\n entries: (rest.entries ?? []) as never,\n }),\n [ACCOUNTING_ACTIONS.voidEntry]: (rest) =>\n voidEntry({\n bookId: rest.bookId as string | undefined,\n entryId: String(rest.entryId ?? \"\"),\n reason: rest.reason as string | undefined,\n voidDate: rest.voidDate as string | undefined,\n }),\n [ACCOUNTING_ACTIONS.getJournalEntries]: (rest) =>\n listEntries({\n bookId: rest.bookId as string | undefined,\n from: rest.from as string | undefined,\n to: rest.to as string | undefined,\n accountCode: rest.accountCode as string | undefined,\n }),\n [ACCOUNTING_ACTIONS.getOpeningBalances]: (rest) => getOpeningBalances({ bookId: rest.bookId as string | undefined }),\n [ACCOUNTING_ACTIONS.setOpeningBalances]: (rest) =>\n setOpeningBalances({\n bookId: rest.bookId as string | undefined,\n asOfDate: String(rest.asOfDate ?? \"\"),\n lines: (rest.lines ?? []) as never,\n memo: rest.memo as string | undefined,\n }),\n [ACCOUNTING_ACTIONS.getReport]: handleGetReport,\n [ACCOUNTING_ACTIONS.getTimeSeries]: (rest) =>\n getTimeSeriesReport({\n bookId: rest.bookId as string | undefined,\n metric: rest.metric,\n granularity: rest.granularity,\n from: rest.from,\n to: rest.to,\n accountCode: rest.accountCode,\n }),\n [ACCOUNTING_ACTIONS.rebuildSnapshots]: (rest) => rebuildSnapshots({ bookId: rest.bookId as string | undefined }),\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 = fields.book as { id?: string; name?: string } | undefined;\n const subject = book?.name ? `A new book named ${JSON.stringify(book.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 = book?.id ? ` (id: ${book.id})` : \"\";\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 = fields.account as { code?: string; name?: string } | undefined;\n if (account?.code && account?.name) {\n return `Upserted account ${account.code} ${JSON.stringify(account.name)}.`;\n }\n return \"Updated the chart of accounts.\";\n },\n [ACCOUNTING_ACTIONS.addEntries]: (fields) => {\n const entries = Array.isArray(fields.entries) ? (fields.entries as { id?: string; date?: string }[]) : [];\n if (entries.length === 0) return \"Posted 0 journal entries.\";\n if (entries.length === 1) {\n const [entry] = entries;\n const idFragment = entry?.id ? ` (id: ${entry.id})` : \"\";\n return `Posted a journal entry on ${entry?.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 reverse = fields.reverseEntry as { date?: string } | undefined;\n return `Voided the entry; a reversing pair was posted on ${reverse?.date ?? \"today\"}.`;\n },\n [ACCOUNTING_ACTIONS.setOpeningBalances]: (fields) => {\n const opening = fields.openingEntry as { date?: string; lines?: unknown } | undefined;\n const verb = fields.replacedExisting === true ? \"replaced\" : \"set\";\n const date = 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 lines = Array.isArray(opening?.lines) ? (opening.lines as unknown[]) : [];\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 = fields.deletedBookId as string | undefined;\n const name = fields.deletedBookName as string | undefined;\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 = fields.book as { id?: string; name?: string; country?: string; currency?: string } | undefined;\n const name = book?.name ? JSON.stringify(book.name) : \"the book\";\n const countryFragment = book?.country ? ` (country: ${book.country})` : \"\";\n return `Updated ${name}${countryFragment}.`;\n },\n};\n\nfunction previewMessage(action: string, fields: Record<string, unknown>): string {\n // `Object.hasOwn` guard so a user-controlled `action` (e.g.\n // \"constructor\" / \"toString\") can't dispatch to an inherited\n // prototype method — own-property check before the dynamic call.\n const head = Object.hasOwn(MESSAGE_BUILDERS, action) ? MESSAGE_BUILDERS[action](fields) : undefined;\n return head ? `${head} ${VIEW_VISIBLE_TRAILER}` : VIEW_VISIBLE_TRAILER;\n}\n\nasync function dispatch(body: AccountingActionBody): Promise<unknown> {\n const { action, ...rest } = body;\n // Own-property check (not just truthiness) before the dynamic call:\n // `ACTION_HANDLERS[action]` would otherwise resolve inherited\n // prototype methods (`toString`, `constructor`, …) for a crafted\n // `action`, dispatching to an unexpected target.\n if (!Object.hasOwn(ACTION_HANDLERS, action)) throw new AccountingError(400, `unknown action ${JSON.stringify(action)}`);\n const handler = ACTION_HANDLERS[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);\n const handlerFields = result && typeof result === \"object\" ? (result as Record<string, unknown>) : { 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/** 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(...)`. */\nexport function createAccountingRouter(): 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 const result = await dispatch(body);\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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"}