Design: per-line tax codes, revision 4 (the month-end answer adds no daily-data requirement and partly covered months keep theirs under either; the waiver and the nonterminal-record qualifications)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 file changed,
+4
−4
docs/superpowers/specs/2026-10-03-per-line-tax-codes-design.md+4 −4
diff --git a/docs/superpowers/specs/2026-10-03-per-line-tax-codes-design.md b/docs/superpowers/specs/2026-10-03-per-line-tax-codes-design.md| index 0fafec11c..b86681a27 100644 |
| --- a/docs/superpowers/specs/2026-10-03-per-line-tax-codes-design.md |
| +++ b/docs/superpowers/specs/2026-10-03-per-line-tax-codes-design.md |
| @@ -1,6 +1,6 @@ |
| # Per-line tax codes for a month in which the plan changed |
| -Date: 2026-10-03. Status: design, revision 3, for a confirming review (revision 2 accepted with fixes by Astra: no P1; three P2, a daily-data requirement that would regress one-code months, the line identity not checked on every acceptance path, two readings of the transfer arithmetic; two P3, references not yet written, "the next pass" promised without its conditions; each answered below. Revision 1 rejected by Astra on two P1s, four P2s and a P3: the invoice engine's per-meter line identity, held months that cannot resume, an older worker's durable review, the day-eligibility rule, policy stated as mechanics, imprecise apportionment, the zero-usage statement; each answered below). Answers `OUTSTANDING.md` item 17 and prepares decision 16 (`DECISIONS-FOR-PAUL.md`), which is Paul's; nothing here chooses it. Branch `design-per-line-tax-codes`, off the launch branch at `29ea6676`. |
| +Date: 2026-10-03. Status: design, revision 4, built from (revision 3 accepted with fixes by Astra's confirming review: no P1; one P2, the month-end answer's daily-data exemption contradicted partial-month pricing, and two qualifications, the waiver sentence and the older worker's nonterminal records; corrected here. Revision 2 accepted with fixes by Astra: no P1; three P2, a daily-data requirement that would regress one-code months, the line identity not checked on every acceptance path, two readings of the transfer arithmetic; two P3, references not yet written, "the next pass" promised without its conditions; each answered below. Revision 1 rejected by Astra on two P1s, four P2s and a P3: the invoice engine's per-meter line identity, held months that cannot resume, an older worker's durable review, the day-eligibility rule, policy stated as mechanics, imprecise apportionment, the zero-usage statement; each answered below). Answers `OUTSTANDING.md` item 17 and prepares decision 16 (`DECISIONS-FOR-PAUL.md`), which is Paul's; nothing here chooses it. Branch `design-per-line-tax-codes`, off the launch branch at `29ea6676`. |
| ## What happens today |
| @@ -34,7 +34,7 @@ It builds the mechanics every answer needs, keeps the hold exactly where it is u |
| 3. **Eligible days first, as today; then attribution.** Which days of a partly covered month are billed does not change: a day counts only when the whole of it lies inside the merged paid coverage (`billingUsageOnPaidDays`, `Allowance.Covered`, `CountedDays`). A continuous change from Personal to Business at 15:00 UTC keeps that day eligible (the merged coverage is continuous); a lapse at 15:00 excludes it, as today. For a month covered from its first instant to its last `Allowance.Covered` is nil by design; attribution then takes the whole month as the one eligible interval, and no day is lost to a nil list. Attribution runs on eligible days only, against the unmerged spans: `plans.MonthAllowance` gains `Spans []PlanSpan \`json:"spans,omitempty"\`` with `From`, `To` (half-open, `[From, To)`) and `Plan`, one per paid assignment interval the allowance counted, written by the console only for a two-code month (the plans package knows no tax codes; the console asks for the spans and drops them when they map to one code), so no one-code month's JSON changes. Under 16a a day belongs to the span that contains 00:00 UTC of that day. |
| -4. **Usage per code, five meters to four axes, in today's order.** For each (code, statement meter) the eligible days' values are accumulated in date order as the statement's floating values, one sum per pair (spans carrying the same code aggregate into the same sum); each accumulated sum is converted once by `billingBaseUnits` (the floor the month's own usage gets); then, per code, the two transfer meters' integers are added with the overflow check, into the four axes. That is the month's own order (floor each of the five meters, then add the two transfer integers), so a month with ordinary egress under Personal and store egress under Business has both in the transfer axis's split and a fractional boundary in one meter cannot move a unit by being floored after the addition. These per-code integers are allocation weights only; the month's billed usage and amounts come from the whole month's meters exactly as today. **The daily data requirement does not widen for one-code months:** `Days` are read and validated only where they are today (a partly covered month) and, new, for a two-code whole month under the split answer, which cannot be attributed without them; a two-code whole month without `Days` and with non-zero billed usage holds as `billing_statement_days_unusable` (that is the one new behaviour, confined to months that hold today anyway); a statement without `Days` and every billed meter zero is waived, as today; the month-end answer needs no `Days` at all. Validation of `Days` when read is today's (dates inside the month, finite non-negative values, sums agreeing with the monthly totals). |
| +4. **Usage per code, five meters to four axes, in today's order.** For each (code, statement meter) the eligible days' values are accumulated in date order as the statement's floating values, one sum per pair (spans carrying the same code aggregate into the same sum); each accumulated sum is converted once by `billingBaseUnits` (the floor the month's own usage gets); then, per code, the two transfer meters' integers are added with the overflow check, into the four axes. That is the month's own order (floor each of the five meters, then add the two transfer integers), so a month with ordinary egress under Personal and store egress under Business has both in the transfer axis's split and a fractional boundary in one meter cannot move a unit by being floored after the addition. These per-code integers are allocation weights only; the month's billed usage and amounts come from the whole month's meters exactly as today. **The daily data requirement does not widen for one-code months:** `Days` are read and validated only where they are today (a partly covered month) and, new, for a two-code whole month under the split answer, which cannot be attributed without them; a two-code whole month without `Days` and with non-zero billed usage holds as `billing_statement_days_unusable` under the split answer (that is the one new behaviour, confined to months that hold today anyway); the month-end answer adds no daily-data requirement of its own, and partly covered months keep today's requirement under either answer (a mixed-code month that was also partly covered, say Personal then Business then a cancellation on the 20th, cannot be priced without `Days` whichever code its lines carry, because the eligible days cannot be told from the rest); a statement without `Days` and every billed meter zero prices as waived today, though a mixed-code history still holds before that today, and will until the switch. Validation of `Days` when read is today's (dates inside the month, finite non-negative values, sums agreeing with the monthly totals). |
| 5. **A meter's amount is apportioned, never re-priced.** The four axes' minor-unit amounts and the subtotal are computed exactly as today (`overageLines`: floor, floor test, invoice-wide largest-remainder hand-out between axes). Then, under 16b's gross-usage proposal, each axis's amount is divided among that axis's codes: weights are the per-code base units; `share_c = floor(amount * weight_c / sum_weights)` in minor units, with the minor units still owed to reach the axis's amount handed out one at a time to the codes with the largest remainder, ties broken by the code string's byte order; a code's share of zero is omitted; an axis with a positive amount and a zero weight sum holds (`billing_invoice_tax_code_mismatch`: the usage that produced the charge is not attributed). Lines are ordered by axis order then code byte order. The invoice-wide rounding is never rerun over the expanded lines. Under 16c's last-span answer, every line carries one code and nothing is divided. Any map is iterated in sorted order; two runs over the same inputs produce identical bytes. |
| @@ -42,7 +42,7 @@ It builds the mechanics every answer needs, keeps the hold exactly where it is u |
| ## Compatibility and the roll rule |
| -- **Roll order is not free.** An older invoice worker that reads a two-code calculation passes today's money-only `wellFormed`, fails the top-level code check, and files `billing_invoice_record_mismatch` for the month: a durable, account-wide hold that then blocks the new worker too (`checkBillingHoldTx`), and that it can file again after an operator resolves it while it still runs. It does not invoice and does not rewrite the calculation, but it is not harmless. So: every console that can run the invoice pass is on a build with this change, and stopped if older, before `taxCodesForLines` stops returning the error; no older build is rolled back to afterwards; and the generic rollback note in `ENABLEMENT.md` ("an older binary runs against the migrated schema") does not apply past activation. Those sentences are a required edit to `ENABLEMENT.md` in the plan's last task, beside the retirement protocol's roll rule, before activation; until activation no two-code calculation exists, so the change itself rolls in any order. |
| +- **Roll order is not free.** An older invoice worker that reads a nonterminal two-code calculation (terminal records are skipped before the guard) passes today's money-only `wellFormed`, fails the top-level code check, and files `billing_invoice_record_mismatch` for the month: a durable, account-wide hold that then blocks the new worker too (`checkBillingHoldTx`), and that it can file again after an operator resolves it while it still runs. It does not invoice and does not rewrite the calculation, but it is not harmless. So: every console that can run the invoice pass is on a build with this change, and stopped if older, before `taxCodesForLines` stops returning the error; no older build is rolled back to afterwards; and the generic rollback note in `ENABLEMENT.md` ("an older binary runs against the migrated schema") does not apply past activation. Those sentences are a required edit to `ENABLEMENT.md` in the plan's last task, beside the retirement protocol's roll rule, before activation; until activation no two-code calculation exists, so the change itself rolls in any order. |
| - A calculation frozen before this change is read exactly as before. The barrier trigger on `billing_months` reads `$.v` and is untouched; the version of a price-book calculation stays 2, a legacy one stays 1. |
| - Months already held: 16d, with its conditions (no other open review, the backoff, the twelve-month lookback). Nothing resolves a review but the operator door, and this design adds no automatic resolution and no handling for a held month outside the lookback. |
| - `DECISIONS-FOR-PAUL.md` carries 16a to 16d (launch branch `27419dcf`); the design's own text calls each a proposal and nothing in it reads as decided. |
| @@ -51,7 +51,7 @@ It builds the mechanics every answer needs, keeps the hold exactly where it is u |
| - Byte and digest fixtures, pinned as literal bytes recorded before the change (not regenerated by it): a legacy version 1 month; a price-book version 2 month with one plan; a one-code month with two plans (Personal to Community); a quantity change; a partly covered month; a waived month; a fully covered one-code month with non-zero usage and no `Days` (priced today, priced after). Each byte-identical, with its request hashes and operation kinds. |
| - A two-code month still holds, with the same review, and nothing is frozen; its review resolved by the operator door and the month priced on the next pass once a body is enabled in the test. |
| -- The attribution: a change at 00:00 UTC; a continuous change at 15:00 (the day eligible, attributed under 16a to the earlier plan); a lapse at 15:00 (the day excluded as today); an expiry mid-month; a change exactly at the month's first instant; a fully covered month (nil `Covered`) attributed over the whole month; repeated spans with one code aggregated; the no-paid-plan month end; both transfer inputs under different codes; malformed `Days` (a date outside the month, a negative value, sums disagreeing) holding; `Days` absent with zero usage waived, with non-zero usage holding under the split answer only. |
| +- The attribution: a change at 00:00 UTC; a continuous change at 15:00 (the day eligible, attributed under 16a to the earlier plan); a lapse at 15:00 (the day excluded as today); an expiry mid-month; a change exactly at the month's first instant; a fully covered month (nil `Covered`) attributed over the whole month; repeated spans with one code aggregated; the no-paid-plan month end; both transfer inputs under different codes; malformed `Days` (a date outside the month, a negative value, sums disagreeing) holding; `Days` absent with zero usage waived; absent with non-zero usage on a fully covered two-code month holding under the split answer and priced under the month-end one; absent on a partly covered two-code month holding under both. |
| - The apportionment: each axis's shares sum to its amount and the subtotal is unchanged; a code with all of an axis's usage gives one line; equal remainders broken by byte order; a tiny share rounding to zero omitted; a zero weight sum with a positive amount holding; overflow refused; shuffled map insertion giving identical bytes; a fractional boundary in one transfer meter (N + 0.75 and 0.75 bytes-equivalent under two codes) with the exact expected shares, which conservation alone would not catch. |
| - The invoice lifecycle for a two-code month, behind a test switch: two lines for one axis with equal and with unequal amounts; for each split line an in-window replay and an expired-window recovery, with a paginated draft listing; recovery by the three metadata keys; the draft validated against both kinds; the POST answer and the draft listing each refused when `metadata[tax_code]` is missing or wrong on one line (the fake provider stores and returns the tax code and the metadata, which today it does not); the transmitted `tax_code` and `metadata[tax_code]` asserted to be the frozen line's; a one-code month's journal, request bytes and hashes unchanged; a mixed history that is waived, and one that emits one code only, through both reader generations. |
| - The older reader, with the real old control flow (the old `wellFormed` and entry guard kept in a test file): a two-code record makes it file `billing_invoice_record_mismatch`, write nothing to the provider, leave the calculation bytes unchanged, and block a subsequent new-reader run until the review is resolved; and with a second, unrelated review open on the account, the resolution of the tax-code review alone does not free the month. |