6a68ecb4d4b6faafee46a64366ceffbaacda2d02
6 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
82859d01db |
feat(parties): name the company inside a voucher text, and stop asking SCB about foreign ones (#2265)
* feat(parties): name the company inside a voucher text, and stop asking SCB about foreign ones
The registry picker searched SCB on the whole display name, which for an
assistant-written voucher is a sentence, so "1511768101 · Visma Spcs AB,
faktura ..." never matched and foreign suppliers produced an empty list
with no explanation.
- lib/parties/name-extract.ts: name candidates read out of the text,
anchored on legal-form words (AB, AB (publ), Inc., Ltd, B.V., GmbH, Oy,
...) and on country words, plus EU VAT numbers. Every candidate is a
substring of the text; foreign forms and countries mark the candidate
as one SCB cannot hold.
- Suggestions: the display name prefers the legal person named in the
text ("TIC identity" becomes "The Intelligence Company AB (publ)"),
the voucher texts are stored as a ledger fact for the picker, the
country is stored when the text says, and a single foreign VAT number
in the text becomes the party's VAT number.
- GET .../enrich/candidates plans the search: Swedish legal person first,
cleaned head last, at most three queries, stopping at the first hit;
no SCB call when the best reading is foreign, the response says which
company it read and where.
- Picker: "X ser ut att vara ett utländskt bolag (Irland). SCB:s register
täcker bara svenska företag." with a hint to save by name and VAT
number; alternate readings offered as one-click searches when the
first found nothing.
- nameQuery strips stacked legal-form suffixes ("AB (publ)").
- The queue builds itself whenever the books hold counterparts it has
not seen, not only on a first visit; the toast only appears when
something was created.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(parties): take a text-derived VAT number only on the expense side
A customer's VAT number steers reverse charge on outgoing invoices, so it
must come from a document or a person, never from a text heuristic. A
supplier's is informational and may still be read from the voucher text.
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>
|
||
|
|
91e2c66afc |
fix(parties): readable suggestions from assistant vouchers, auto-build queue, SCB fetch after promotion (#2259)
* fix(parties): readable suggestions from assistant-written vouchers, auto-build queue, SCB fetch after promotion Live feedback on a real company (2026-09-03): the queue showed 35 one-off suggestions with sentence-long names, wide empty rows, a "Hämta förslag" step nobody could predict, no SCB fetch after promotion, and an empty supplier created from a Finansinspektionen fee line. - ledger_key v2 (migration 20260904002000): keep the counterpart head of "<counterpart> · <note>" descriptions, drop bank method tokens and long references before normalising; JS mirror in lib/parties/ledger-key.ts with shared LEDGER_KEY_CASES. Suggested parties nobody has touched are rebuilt under the new keys (repair in the same migration). - apply_party_suggestions attaches by VAT number too, so ledger keys with a VAT number but no org number reach existing roles. - Queue: fixed name/reason column widths, inline "Hitta i företagsregistret" for rows without an org number. - Page: builds the queue automatically on first visit when nothing has been suggested yet; after promotion, fetches SCB facts for every promoted legal person (spaced under the 10 calls/10 s limit) and fills the role's VAT number; confirm dialog says how many rows lack an org number. - Classifier: more authorities (Finansinspektionen, Arbetsförmedlingen, Pensionsmyndigheten, ...) and fee words (registreringsavgift, tillsynsavgift, ...) so fee lines stop becoming suppliers. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(parties): scope the suggestion repair to keys the new ledger_key no longer produces Superagent flagged the repair DELETE as global. It now only removes untouched pipeline suggestions that no posted voucher of the company maps to under the new function; suggestions whose key is unchanged stay. 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> |
||
|
|
22b98e0a3b |
feat(parties): fetch registry facts from SCB into the dossier, with a picker for parties without an org number (#2258)
* feat(parties): Kontakter register, suggestion queue, dossier and merge Phase 1's two surfaces on top of the parties substrate: - /parties page: one list with the five-way switch (Alla, Kunder, Leverantörer, Förslag, Bara i bokföringen), search, a 12-month/all period picker, and at most one attention line. Confirmed rows show roles as muted text, rhythm, underlag, dominant account and money. Observed rows are computed and never stored; a generic band keeps unattributed spend visible. - Suggestion queue: a reason per row, hard-key rows pre-ticked, bulk confirm behind one dialog, dismiss on hover, undo on the toast. - Dossier slide-over: Pengar, Bokföring, Vad Accounted vet (facts and identities with source and count), Underlag och verifikat, Historik. - Merge dialog with a visible, swappable survivor and undo. - API: GET /api/parties, GET /api/parties/[id], POST suggest, decide, decide/undo, merge, merge/undo (withRouteContext, Zod, 15 tests). - Migration 20260903090000: decide_parties snapshots the reason it clears; undo_party_decisions reverses confirm/dismiss within 30 days; decision kind 'undo'. - The pipeline runs after SIE import and provider migration (non-blocking) so a migrant's register is full on arrival. - Nav entry under Register; sv/en strings. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(parties): pass explicit interpolation values to next-intl next build's type check rejects a typed interface where the translator wants an index-signature record. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(parties): retry label on the load-failed state Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(parties): hard keys for companies without org number, readable names, look-alikes at read time - get_ledger_key_evidence dropped every document for a company whose own org number is NULL (the self check compared against NULL). Replaced in 20260903100000 with a coalesced comparison; pg test covers it. - Display names come from the printed name on documents, otherwise from the voucher text with the AP/AR prefix and supplier number removed. - Look-alike parties (same core, or one core extending the other by whole words: Fortnox / Fortnox Finans) are detected when the register is read, never stored, and feed the Dubblett? chip and the merge dialog. - Queue shows Intäkt beside Kostnad; dossier hides zero money rows and formats bankgiro/plusgiro; merge dialog cancels with Avbryt; no synchronous setState inside effects. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(parties): link every new supplier and customer to a party on write The backfill covered the rows that existed on 2026-09-02; 108 rows created since had no party and never reached the register. A BEFORE INSERT/UPDATE trigger on customers and suppliers now calls ensure_party on every write path at once: find-or-create by org number inside the company, never by name; a private customer gets a kind=person party without any number; a nameless row stays unlinked; a foreign party id is refused with the same error as the composite foreign key; a link to a merged party follows the chain to the survivor; the clear that ON DELETE SET NULL performs is kept. ensure_party lets the trigger act for the row's owner (pg_trigger_depth() > 0); the RPC path is unchanged. The migration also links the rows created since the backfill. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(parties): dossier hides dismissed parties and follows merges to the survivor The register hid archived parties while the dossier still served them by id, and a merged party's dossier pointed at a dead row. Superagent P2 on #2206; three unit tests. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * chore(parties): move the role-link migration past main's 20260903110000 Two files with one version would collide in schema_migrations. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(parties): confirm suggestions into Leverantörer and Kunder, no third noun Founder decision after the walkthrough: users know two words. The page becomes the queue 'Förslag från bokföringen' with 'Bara i bokföringen' beside it; the Kontakter nav entry and the Alla/Kunder/Leverantörer views go. Each suggestion shows what it becomes (Blir), read from the ledger side and changeable per row; confirming calls promote_parties, which creates the supplier and/or customer row from the party's facts, never a duplicate, and is undoable for 30 days through undo_party_promotions (the created rows are archived, the party returns to the queue). Leverantörer and Kunder carry the one attention line that leads here. The dossier offers Lägg upp som leverantör / som kund. Migration 20260903130000, 5 pg tests, route and unit tests updated. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(parties): write bankgiro and plusgiro the way the supplier form does Identities are stored as digits; suppliers carry 5317-0900. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(parties): fetch registry facts from SCB into the dossier SCB granted API access today (certificate + password, layouts Je and Ae). This adds the first registry enricher of phase 3: - lib/parties/scb: config from env (SCB_API_CERT_PFX_BASE64, SCB_API_CERT_PASSWORD), an mTLS transport on node:https, the mapping of every documented Je variable to a labelled fact, and a client whose wire format sits in one file because SCB replaces the API this month. Legal persons only: a sole trader's org number is a personnummer. - Migration 20260903150000: record_party_facts(company, user, party, source, facts, fetched_at) refreshes unchanged values, supersedes changed ones, never touches other sources. pg test. - POST /api/parties/[id]/enrich: 503 when not configured, 400 for a sole trader, 502 when SCB fails, fills an empty legal name. 7 tests. - Dossier: 'Hämta uppgifter' button (gated on configuration) and the registry rows with 'SCB · datum' as their source line. - scripts/scb/discover.ts prints the live variable list, code tables and one lookup so the request shape is checked against the real API. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(parties): SCB client on the live wire format, mapper on the real Je row Verified against the API on 2026-09-03: an identity lookup is one filter (Variabel 'OrgNr (10 siffror)', Operator ArLikaMed) without status keys, and the row carries '<name>, kod' beside SCB's own text. The mapper now reads those columns, prefers SCB's text, and adds turnover band, seat names and Skatteverket registration. The AB Volvo row is the fixture. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(parties): registry legal name outranks the document one, never a person's Survivorship from the plan: user > registry > document. The dossier's legal-name row now carries 'SCB · datum' when the registry is the source. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(parties): VAT number from the moms flag, one primary action, one source line Founder review of the SCB dossier: - A Swedish company registered for moms has VAT number SE + org number + 01 by construction, so the registry's moms flag yields the number; it fills an empty vat_number on the party and shows in the Momsnr row instead of 'Saknas'. - The 'Registrerad hos Skatteverket' row said nothing (true for every legal person) and is gone. - Five buttons became one primary (the role the ledger suggests) and a menu with the rest; the per-row 'SCB · datum' notes became one group line 'Från SCB · hämtat datum'. - A postal-code-only address (large companies) is labelled as such. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(parties): do not repeat the county when it equals the municipality Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(parties): SCB picker for parties without an org number 'Hitta i företagsregistret' in the dossier menu opens a picker: SCB is searched on the party's name (prefix first, contains as fallback, counts before rows, capped at 25, natural persons and estates excluded, active companies first). The user chooses; the org number is recorded as a fact with source 'user' and set on the party, then the normal fetch runs, so every later fetch is by number. A number another live party holds is refused with a pointer to it. One match is still shown, never auto-picked. The transport retries once on a dropped connection (seen live). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(parties): a picked org number shows in the queue's reason and counts as a hard key Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(parties): SCB search tightened after a batch of real supplier names Twenty-five prod supplier names and twenty org numbers across every legal form went through the search and the lookup: - total is what the picker can offer, not SCB's raw count (Eismann counted one row and offered none, a natural person); - foreign legal forms stay in the query: they are part of the registered name and dropping them floods (Schmidt GmbH became 167 Schmidts); - a fusion or delning in progress is no longer a warning (Fortnox AB and Avanza Bank trade normally under 'Fusion pågår'); distress and disappearance codes still are. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * chore(parties): move the four queue migrations past main's 20260903170000 Main merged 20260903120000_skattekonto_transactions_realtime_publication with the same version as the role-link trigger; the preview database refused the duplicate key. All four now sit after main's newest so the set applies in one ordered run on prod. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * chore(parties): move record_party_facts after the queue migrations Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * chore(parties): move record_party_facts to a version after tonight's collisions 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> |
||
|
|
b996da60ee |
feat(parties): Förslag från bokföringen, confirmed straight into Leverantörer and Kunder (#2206)
* feat(parties): Kontakter register, suggestion queue, dossier and merge Phase 1's two surfaces on top of the parties substrate: - /parties page: one list with the five-way switch (Alla, Kunder, Leverantörer, Förslag, Bara i bokföringen), search, a 12-month/all period picker, and at most one attention line. Confirmed rows show roles as muted text, rhythm, underlag, dominant account and money. Observed rows are computed and never stored; a generic band keeps unattributed spend visible. - Suggestion queue: a reason per row, hard-key rows pre-ticked, bulk confirm behind one dialog, dismiss on hover, undo on the toast. - Dossier slide-over: Pengar, Bokföring, Vad Accounted vet (facts and identities with source and count), Underlag och verifikat, Historik. - Merge dialog with a visible, swappable survivor and undo. - API: GET /api/parties, GET /api/parties/[id], POST suggest, decide, decide/undo, merge, merge/undo (withRouteContext, Zod, 15 tests). - Migration 20260903090000: decide_parties snapshots the reason it clears; undo_party_decisions reverses confirm/dismiss within 30 days; decision kind 'undo'. - The pipeline runs after SIE import and provider migration (non-blocking) so a migrant's register is full on arrival. - Nav entry under Register; sv/en strings. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(parties): pass explicit interpolation values to next-intl next build's type check rejects a typed interface where the translator wants an index-signature record. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(parties): retry label on the load-failed state Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(parties): hard keys for companies without org number, readable names, look-alikes at read time - get_ledger_key_evidence dropped every document for a company whose own org number is NULL (the self check compared against NULL). Replaced in 20260903100000 with a coalesced comparison; pg test covers it. - Display names come from the printed name on documents, otherwise from the voucher text with the AP/AR prefix and supplier number removed. - Look-alike parties (same core, or one core extending the other by whole words: Fortnox / Fortnox Finans) are detected when the register is read, never stored, and feed the Dubblett? chip and the merge dialog. - Queue shows Intäkt beside Kostnad; dossier hides zero money rows and formats bankgiro/plusgiro; merge dialog cancels with Avbryt; no synchronous setState inside effects. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(parties): link every new supplier and customer to a party on write The backfill covered the rows that existed on 2026-09-02; 108 rows created since had no party and never reached the register. A BEFORE INSERT/UPDATE trigger on customers and suppliers now calls ensure_party on every write path at once: find-or-create by org number inside the company, never by name; a private customer gets a kind=person party without any number; a nameless row stays unlinked; a foreign party id is refused with the same error as the composite foreign key; a link to a merged party follows the chain to the survivor; the clear that ON DELETE SET NULL performs is kept. ensure_party lets the trigger act for the row's owner (pg_trigger_depth() > 0); the RPC path is unchanged. The migration also links the rows created since the backfill. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(parties): dossier hides dismissed parties and follows merges to the survivor The register hid archived parties while the dossier still served them by id, and a merged party's dossier pointed at a dead row. Superagent P2 on #2206; three unit tests. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * chore(parties): move the role-link migration past main's 20260903110000 Two files with one version would collide in schema_migrations. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(parties): confirm suggestions into Leverantörer and Kunder, no third noun Founder decision after the walkthrough: users know two words. The page becomes the queue 'Förslag från bokföringen' with 'Bara i bokföringen' beside it; the Kontakter nav entry and the Alla/Kunder/Leverantörer views go. Each suggestion shows what it becomes (Blir), read from the ledger side and changeable per row; confirming calls promote_parties, which creates the supplier and/or customer row from the party's facts, never a duplicate, and is undoable for 30 days through undo_party_promotions (the created rows are archived, the party returns to the queue). Leverantörer and Kunder carry the one attention line that leads here. The dossier offers Lägg upp som leverantör / som kund. Migration 20260903130000, 5 pg tests, route and unit tests updated. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(parties): write bankgiro and plusgiro the way the supplier form does Identities are stored as digits; suppliers carry 5317-0900. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * chore(parties): move the four queue migrations past main's 20260903170000 Main merged 20260903120000_skattekonto_transactions_realtime_publication with the same version as the role-link trigger; the preview database refused the duplicate key. All four now sit after main's newest so the set applies in one ordered run on prod. 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> |
||
|
|
fc04578818 |
feat(parties): suggestion pipeline from ledger keys and linked documents (#2172)
* feat(parties): phase 1 substrate, one party per counterpart Adds the identity layer above customers and suppliers, which keep their tables and every foreign key and gain a nullable party_id. - parties: company-scoped identity with status (suggested | confirmed), kind, alias keys, origin and merged_into. One live party per org number and company, enforced by a partial unique index; merged losers leave the index so a merge can be undone. This is the unique key the duplicate-invoice guard has lacked, since suppliers never had one. - party_facts: statements with a source, a rank (preferred | normal | deprecated) and two time axes, never overwritten. - party_identities: bankgiro, plusgiro, IBAN and friends per party, with seen and paid counts and a known | unverified status. - party_decisions: every human action on a party as a labelled example. - normalize_org_number(text): SQL mirror of lib/invariants/org-number.ts (strip separators, drop the century on 12 digits, Luhn check, 10 digits). - ensure_party(): find by org number inside the company, else create. Name-only rows never merge at insert time; a name merge is a recorded human decision. - Backfill: one party per existing supplier and customer, merged on org number, suppliers first so both roles land on one party. - Archive contract: the four tables are master data in the full archive. Observed parties (keys derived from voucher and bank text) are not stored; they stay computed by the ledger-context RPC. No posted entry is touched. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(parties): observed parties from voucher text, ledger_key and its mirror Migrants arrive with vouchers, not bank transactions, so the bank-keyed ledger context is empty for them. This adds the description-keyed twin. - public.ledger_key(text): legibility key on top of the frozen normalize_counterparty_key mirror: strips AP-register prefixes (levfakt, leverantörsfaktura från N, levbet, faktura, kvitto, utgift), the supplier number that follows them, and trailing 1-3 digit runs, never "inköp". Mirrored by lib/parties/ledger-key.ts; the pair is pinned by a shared fixture list in the pg test. - public.get_observed_parties(company, from_date, limit): posted vouchers grouped by ledger_key(description) with occurrences, variants, expense and revenue SEK from the lines, first/last seen, median cadence and the Laplace-smoothed dominant result account. Excludes storno, opening balance, year-end and VAT settlement, and vouchers that carry a bank merchant name (those stay with get_ledger_deep_context). SECURITY INVOKER, so RLS scopes it. Never stored. - lib/parties/classify.ts: the deterministic pre-classifier moved out of the evaluation script so product and evaluation share one implementation (0.965 agreement with the founder labels, party recall 0.99). - lib/parties/observed.ts: RPC wrapper that classifies each row and derives a display rhythm from the cadence. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(parties): tenant-safe composite foreign keys on every party link Facts, identities, decisions, customers.party_id, suppliers.party_id and parties.merged_into now reference parties(id, company_id), so a row can only point at a party in its own company. ON DELETE SET NULL names party_id so role rows keep their company_id. Adds a pg-real test that rejects every cross-company link and checks company_id survives a party delete. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(parties): suggestion pipeline from ledger keys and linked documents Phase 1c of the parties plan. Fills a migrant's register with suggested parties from what the ledger already knows, never as facts: - get_ledger_key_evidence(company): hard keys per ledger_key from the documents linked to posted vouchers (org number via normalize_org_number, VAT, bankgiro, plusgiro, printed name). Documents whose supplier org is the company's own are the company's sales invoices and only count in self_docs. - apply_party_suggestions(company, user, items): upserts suggestions. Attaches by explicit party_id, org number or an exact alias key; never by name. Identities become known at two sightings. Idempotent. - decide_parties(company, user, ids, kind, note): bulk confirm or dismiss with one party_decisions row each. - parties.suggested_reason: the evidence summary the queue shows per row. - lib/parties/suggest.ts: buildSuggestions (pure) and suggestPartiesForCompany. Keys that mix two org numbers keep neither the hard key nor identities; same-core live parties are reported as similar_to for a person to decide. coreKey() moves into ledger-key.ts. 55 unit tests and 14 pg-real tests pass locally. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(parties): decide_parties dismisses suggested parties only Dismiss is the queue's answer to a suggestion; a confirmed party is never archived through it. Superagent P2 on #2172. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test(parties): type the rpc mock with its args so the ratchet stays clean 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> |
||
|
|
5291806c37 |
feat(parties): observed parties from voucher text, ledger_key and its mirror (#2168)
Migrants arrive with vouchers, not bank transactions, so the bank-keyed ledger context is empty for them. This adds the description-keyed twin. - public.ledger_key(text): legibility key on top of the frozen normalize_counterparty_key mirror: strips AP-register prefixes (levfakt, leverantörsfaktura från N, levbet, faktura, kvitto, utgift), the supplier number that follows them, and trailing 1-3 digit runs, never "inköp". Mirrored by lib/parties/ledger-key.ts; the pair is pinned by a shared fixture list in the pg test. - public.get_observed_parties(company, from_date, limit): posted vouchers grouped by ledger_key(description) with occurrences, variants, expense and revenue SEK from the lines, first/last seen, median cadence and the Laplace-smoothed dominant result account. Excludes storno, opening balance, year-end and VAT settlement, and vouchers that carry a bank merchant name (those stay with get_ledger_deep_context). SECURITY INVOKER, so RLS scopes it. Never stored. - lib/parties/classify.ts: the deterministic pre-classifier moved out of the evaluation script so product and evaluation share one implementation (0.965 agreement with the founder labels, party recall 0.99). - lib/parties/observed.ts: RPC wrapper that classifies each row and derives a display rhythm from the cadence. Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |