* fix(bookkeeping): comprehensive chart_of_accounts charset repair
The 20260625120000 backfill (PR #734) only covered the 26 short-name seed
accounts. Investigation found the corruption was far broader — ~4,500 rows
across 858 companies — in four signatures, and verified the root cause is
already closed (prod's seed_chart_of_accounts() carries correct diacritics;
the corruption was prod-migration-drift, the seed fix reached prod ~2026-06-12,
no companies corrupted since).
Adds a tested, reusable repair core + a guarded script:
- lib/bookkeeping/charset-repair.ts — pure, unit-tested resolvers:
* stripped diacritics ("Utgaende moms forsaljning...") → restore from a
de-accent-equal clean sibling. DIRECTIONAL guard (only acts on a fully
de-accented input) so a correct name is never stripped down; unique-match
only, so user-renamed accounts are never clobbered.
* double-encoded UTF-8-as-CP1252 ("Företagskonto") → lossless CP1252-aware
byte reversal (recovers custom names too).
* CP437-as-CP1252 ("F”rmedlad", "™vriga", "V„rdef”r„ndring") → lossless
CP437 letter reversal.
* lost-byte U+FFFD ("p� bilar") → fill via single-char-wildcard match to a
unique clean sibling (the byte is gone, so only a confident sibling wins).
isClean() rejects mojibake AND mid-word CP1252 artifacts, but treats a
space-padded en-dash ("Kundfordringar – delad faktura") as legitimate.
- scripts/repair-chart-of-accounts-charset.ts — dry-run by default, --execute to
apply; idempotent; refuses any non-prod project. Sources canonical names from
the table's own clean sibling rows + BAS_REFERENCE.
Applied to production (UPDATE-only, account_name is display-only): 4,499 rows
across 858 companies repaired, 0 double-encoded remaining, 0 errors. 247 rows
left untouched and reported — custom account names with lost bytes and no
canonical (unrecoverable from the data; need the source SIE file or manual fix).
21 unit tests cover every transform with real prod fixtures, plus the two
dry-run bugs caught before any write (correct→stripped direction; matching a
CP437-mojibake sibling).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(scripts): avoid supabase-js generic mismatch in charset repair fetch
next build's tsc rejected fetchAll(supabase: ReturnType<typeof createClient>)
— the default-generic SupabaseClient type doesn't unify with the inferred
createClient() return. Make fetchAll a closure over the inferred client.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(scripts): add TOCTOU guard to charset repair updates
Per PR review: only write when the row still holds the exact corrupted value
read (.eq account_name), so a concurrent rename is skipped, not clobbered, and
the script is strictly idempotent. Track skipped count.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(charset-repair): build combining-marks regex from ASCII string
Per PR review: the deaccent regex literal embedded raw U+0300–U+036F combining
marks (invisible, encoding-fragile). Build it via RegExp('[\\u0300-\\u036f]')
so the source is plain ASCII. Behavior-identical; 21 tests still green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>