feat(packs): konteringspaket as validated data files (phase 2a) (#1386)

* feat(packs): konteringspaket as validated data files, ported losslessly

The 26 system booking templates lived inside migration 20260413160000. Under
the never-modify-a-shipped-migration rule that froze them: correcting a wrong
BAS account or a Swedish typo needed a whole new migration, and nothing checked
that a seeded account existed in the chart or that a template balanced. #1321
was exactly that failure with seeded chart names.

They are now one YAML file per pattern under packs/, with a Zod contract and a
CI gate. A correction becomes a one-line edit plus a green run.

The port is proven lossless, not asserted. The test fixture was read out of a
Postgres with all 548 migrations applied, so it is the exact JSONB production
holds; lib/packs/__tests__/port-is-lossless.test.ts asserts the YAML reproduces
it by value. Phase 2b can swap the seeded rows for the loader as a no-op.

The gate checks what makes a pack CORRECT, not just well-formed, because #1321
was structurally valid and still wrong: every account must exist in BAS 2026,
and every pack must balance at five probe amounts through the real
applyTemplate() rather than a reimplementation. Account numbers validate through
lib/invariants, so a pack cannot disagree with the API or the SIE importer about
what an account number is.

Doing that immediately found four pre-existing breakages in the shipped
templates:

  loneutbetalning                    debits total 1.42x the amount against a
                                     1.0 credit: it can never post
  periodiseringsfond-avsattning-ab   account 2113 is not in BAS 2026 and is not
  periodiseringsfond-aterforing-ab   seeded into any company chart
  preliminar-f-skatt-ef              account 2012, same problem

These are quarantined in KNOWN_BROKEN, not fixed and not hidden: a quarantined
pack's findings are warnings, any NEW finding fails the build, and the validator
fails if a quarantined pack turns out to be clean, so the list may only shrink.
Each is a Swedish accounting content change to a user-facing template, which
deserves its own review rather than riding along inside a file-format change.

Five shipped descriptions contain em dashes, preserved verbatim and pinned by a
test: a lossless port must not silently rewrite user-visible strings.

js-yaml is promoted from a transitive dependency to a declared one (MIT, already
in node_modules), so the catalogue does not depend on it by accident.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(deps): regenerate package-lock.json with npm 10 to match CI

`npm ci` failed on every job with "Missing: @swc/helpers@0.5.23 from lock
file". The lockfile was written by local npm 11.6.0; CI runs npm 10.8.2 on
node 20, and npm 11 emits a tree npm 10 reads as out of sync.

Regenerated with `npx npm@10 install --package-lock-only`, which cuts the diff
from a sprawling rewrite down to the three entries this branch actually adds
(js-yaml, @types/js-yaml, and the @swc/helpers entry npm 11 had dropped).
Verified with `npx npm@10 ci --dry-run`.

This is the documented gotcha for this repo: regenerate lockfiles with
npx npm@10, never with a local npm 11.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-08-03 18:05:54 +02:00
committed by GitHub
co-authored by Claude Opus 5 Jakob Wennberg
parent c1888fcd5e
commit df34cae9bf
36 changed files with 2047 additions and 2 deletions
+9 -2
View File
@@ -37,6 +37,7 @@
"framer-motion": "^12.42.2",
"fuse.js": "^7.4.2",
"ics": "^3.12.0",
"js-yaml": "4.1.1",
"jszip": "^3.10.1",
"lucide-react": "^1.24.0",
"mailparser": "^3.9.14",
@@ -66,6 +67,7 @@
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/js-yaml": "4.0.9",
"@types/mailparser": "^3.4.6",
"@types/node": "^20",
"@types/pg": "^8.20.0",
@@ -7634,6 +7636,13 @@
"@types/unist": "*"
}
},
"node_modules/@types/js-yaml": {
"version": "4.0.9",
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz",
"integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@@ -8616,7 +8625,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true,
"license": "Python-2.0"
},
"node_modules/aria-hidden": {
@@ -12008,7 +12016,6 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"dev": true,
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"