* chore: repo-wide bloat sweep, remove dead code and fold duplicate helpers Remove 33 dead files, ~270 unreferenced exports/types, 13 dead i18n namespaces and 4 unused dependencies; fold byte-identical helper copies into one canonical home each (lib/utils chunk/sleep/utcDateStamp, lib/dates/iso, lib/invariants/uuid, lib/xml/escape, lib/reports/sru/format, lib/pdf/number-text, lib/browser/panel-request, lib/api/v1/body + v1ValidationError rolled out to ~55 v1 routes, booking-template schemas). No behaviour change: v1 bodies and status codes, MCP tool schemas, DB writes and money math are untouched. Naive ore rounding was deliberately not swapped for roundOre; see DECISIONS.md 2026-09-02 for the full list of things left alone on purpose. tsc, lint, 19588 unit tests and check:guards green; antipattern baseline ratcheted (naive-ore-round 622 -> 620, hand-rolled-invariant 115 -> 113). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test(transactions): import RawTransaction from @/types after the ingest re-export removal CI's type ratchet (check:types, full tsconfig) caught the one test file that still imported the type through lib/transactions/ingest. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
lib/invariants
Shared product contracts: the formats and bounds that more than one consumer must agree on, with the reason for each rule recorded next to it.
What belongs here
A rule belongs here only when consumers outside the owning module need the identical invariant to validate input or to generate compatible output.
Our consumers are the web app, the public /api/v1 surface, the MCP server, the
SIE importer and exporter, and the Skatteverket-bound generators (AGI, KU10,
SRU, iXBRL). When two of those disagree about what a valid value looks like, the
disagreement is invisible until a customer's filing fails.
What does not belong here
- Rules specific to one module. They stay in that module.
- General helpers.
lib/utils.ts,lib/money.ts. - Database questions. "Is this account in the company's chart?" is
lib/bookkeeping/account-validation.ts, not a format rule. - Business rules. "Is this fiscal year open?" is a period question.
Naming
The name must make the owning concept explicit. isAccountNumber, not
isValidNumber. Two rules in here share a byte-identical regex (account-number
and fiscal-year are both /^\d{4}$/) and mean entirely different things: the
names are the only thing keeping a call site honest, so they carry the weight.
Layout
| File | Owns |
|---|---|
org-number.ts |
Swedish organisationsnummer / personnummer: canonical 10-digit form, Luhn, Skatteverket 12-digit conversion |
account-number.ts |
BAS account number format (4 digits, always a string) |
iso-date.ts |
YYYY-MM-DD shape, plus a real-calendar-date check |
fiscal-year.ts |
Four-digit räkenskapsår key |
zod.ts |
Zod primitives built from the rules above, for API schemas |
zod.ts is separate so that consumers which do not use Zod (the MCP server,
report generators) can import a rule without pulling the dependency in.
Adding a rule
- Write the rule and the reason in the docblock. A rule without a recorded reason gets re-litigated or worked around within a quarter.
- Export a pure validator (primitives in, boolean or
nullout). - Add the Zod primitive to
zod.tsif an API surface needs it. - Replace the hand-rolled copies.
npm run check:guardstracks how many remain and fails if the count goes up.