* fix(vat): downgrade per-voucher RC basis gaps only under per-rate evidence
Per-voucher RC basis gap findings (findRcBasisGaps) blocked "Skicka till
Skatteverket" as ERROR even when the flagged vouchers were legitimate
moms-only rattelseverifikat whose basbelopp lives in another (often
reversed) verifikat. In that state no arrangement of vouchers satisfies
both the per-voucher scan and the aggregate basis/moms identity, so the
block was unfixable: every correction voucher joined the blocklist it
was meant to clear (Orto Engineering 3DJake support case, 2026-08).
The gap finding now downgrades to a non-blocking WARNING only when ALL
of the following hold, otherwise the blocking ERROR stays exactly as
before:
- the 44xx/45xx RC basis accounts, grouped per momssats
(RC_BASIS_ACCOUNTS_BY_RATE), match ruta 30/31/32 two-sided within a
0.5 kr ore epsilon per rate;
- no moms box (ruta 30/31/32) is negative;
- the aggregate RC_OUTPUT_MISSING check has not fired;
- the caller supplied the evidence at all (older wire payloads and
totals-less contexts keep the blocking behavior).
A first cross-rate-sum predicate was refuted by adversarial review: a
wrong-rate fiktiv moms voucher (12% moms "covered" by a 25% basis)
reached parity and unblocked a 7 800 kr under-declaration, and a
net-negative rate box made the summed comparison vacuous (textbook
FK004 state filing). Rutor 20-24 are partitioned by purchase type, not
rate, so the certificate must come from account totals; both
counterexamples plus the tolerance-hole case (shortfall inside the
aggregate 0.5% tolerance still blocks) are locked in as regression
tests.
The evidence travels as rcBasisByRate on the declaration payload
(rcBasisTotalsByRate projection), consumed by the web view and the MCP
completeness checks; rc-basis-gaps.ts derives its flat account set from
the same rate-grouped single source so scan and evidence cannot drift.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(vat): refuse gap downgrade on non-finite evidence; pin the ore epsilon
Review findings, one pass:
- CodeRabbit (major): rcBasisByRate arrives as unvalidated JSON in the
web view; a missing or non-numeric field made every per-rate
comparison evaluate against NaN, which compares false and PASSED the
predicate, relaxing the filing gate in the unsafe direction. The
predicate now refuses the downgrade outright on any non-finite basis
or moms figure, covering both the web and MCP callers.
- CodeRabbit (nit): added a 0.51 kr drift case so a future widening of
the 0.5 kr epsilon fails a test instead of slipping through green.
Declined with reasons (recorded in the PR summary): requiring textual
voucher-to-voucher references before downgrading (belongs to the
rattelse documentation flow, and would reintroduce the unfixable block
this PR removes); epsilon stacking across rates (max 1.5 kr, immaterial
at whole-krona filing and below the aggregate tolerance); explicit
negative-basis guard (all negative-basis paths already block via the
two-sided mismatch or the negative-moms guard, now plus the finite
guard).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>