carlos / platform Public

Design: per-line tax codes, revision 3 (the daily-data requirement confined to two-code whole months under the split answer; the line identity checked on the POST answer and the draft; the transfer arithmetic in the month's own order; the roll rule's rollback note; the next pass's conditions)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 1db5bcf023b1766d38e2af9aee1c627117393218 parent 6637026
1 file changed, +15 −14
  • docs/superpowers/specs/2026-10-03-per-line-tax-codes-design.md +15 −14

CI cancelled — superseded by a newer push — run details

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 c9efb9671..0fafec11c 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 2, for review (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 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`.
## What happens today
@@ -17,10 +17,10 @@ So the only thing missing is which tax code each overage line carries. That is d
Decision 16 says "split by span, with the usage of each UTC day attributed to the plan in force on that day" or "the month-end plan's code". Neither sentence is precise enough to build from, and the refinements are choices, not mechanics. They are listed in `DECISIONS-FOR-PAUL.md` under 16 as 16a to 16d and this design chooses none of them; it builds so that any answer is one body of one function:
-- **16a, the change day.** A UTC day is the finest grain the box records. Proposed approximation: a day's usage follows the plan in force at the day's start (00:00 UTC). It can attribute nearly a whole day to the earlier plan.
+- **16a, the change day.** A UTC day is the finest grain the frozen billing statement provides (the collection underneath has finer readings; the statement does not). Proposed approximation: a day's usage follows the plan in force at the day's start (00:00 UTC). It can attribute nearly a whole day to the earlier plan.
- **16b, how a meter's unchanged amount is divided between codes.** Proposed: in proportion to the usage attributed to each code (gross usage, after the eligibility rule below). The alternatives differ: by each span's own excess over its own share of the allowance (a span whose usage stayed under its allowance gets nothing), or by consuming the pooled allowance chronologically and attributing only the excess after it. With span allowances of 100 and 100 and usage of 50 and 250 the pooled overage is 100: gross weighting gives the first span a sixth of it, the other two give it nothing. Pricing spans independently would charge 150, not 100, and is not an option here: amounts do not change.
- **16c, no paid plan at the month's end.** "The month-end plan" is undefined when the last paid span ended before the month did (a cancellation; the account became Free). The last paid span's code, or a hold, is the owner's.
-- **16d, months already held.** A month held today with `billing_invoice_tax_code_mixed` is not invoiced by the switch: its review stays open until an operator resolves it (`carlos billing resolve`), after which the next pass prices it under the chosen rule. Whether every such month is resolved at once on activation, or case by case, is the owner's.
+- **16d, months already held.** A month held today with `billing_invoice_tax_code_mixed` is not invoiced by the switch: its review stays open until an operator resolves it (`carlos billing resolve`). After that the pass prices it only when nothing else holds it: no other review of the account is open (`freezeBillingMonth` refuses while any is, so release is account-wide where other reviews remain), its retry backoff has passed, and the month is within the pass's twelve-month lookback; a held month older than that is not scheduled by resolving its review and needs its own handling, which this design does not provide. Whether every such month is resolved at once on activation, or case by case, is the owner's.
What the design asserts without a decision: pre-tax amounts (each resource axis's minor units and the subtotal) do not change under any answer; the floor stays the monthly floor; the tax-inclusive total can differ, because attribution is what tax is computed from.
@@ -28,13 +28,13 @@ What the design asserts without a decision: pre-tax amounts (each resource axis'
It builds the mechanics every answer needs, keeps the hold exactly where it is until the answer, and keeps every calculation that has one tax code frozen byte for byte as today (its digest, request hashes and operation names unchanged).
-1. **A line carries its own tax code.** `plans.OverageLine` gains `TaxCode string \`json:"tax_code,omitempty"\``; its existing fields keep their untagged names (`Meter`, `AmountCents`), so no old byte moves. Two shapes: a **one-code calculation** has `tax_code` set and no line carrying one (today's shape, every existing record, every month whose counted spans map to one code, including Personal to Community changes, quantity changes and partial coverage); a **two-code calculation** has `tax_code` empty and every line carrying a code. `wellFormed` gains the tax shape beside the money: exactly one of the two, a code of the form `txcd_` plus at least one character wherever one appears, and a non-waived calculation with no lines refused; a waived calculation has no lines and carries the one-code shape when its spans map to one code, and the two-code shape (empty `tax_code`, no lines) otherwise. Two distinct codes among the emitted lines are not required: a two-code history can have chargeable usage under one code only, and the month-end answer puts one code on every line. Version 1 calculations stay version 1 and are read as today. The entry guard in `billingInvoiceRun` (today `wellFormed` plus a top-level `txcd_` check) becomes `wellFormed` alone, since the shape rule now covers both levels; `lineParams` takes the line's code or, for the one-code shape, the calculation's, and refuses a line with a code at both levels or at neither.
+1. **A line carries its own tax code.** `plans.OverageLine` gains `TaxCode string \`json:"tax_code,omitempty"\``; its existing fields keep their untagged names (`Meter`, `AmountCents`), so no old byte moves. Two shapes: a **one-code calculation** has `tax_code` set and no line carrying one (today's shape, every existing record, every month whose counted spans map to one code, including Personal to Community changes, quantity changes and partial coverage); a **two-code calculation** has `tax_code` empty and every line carrying a code. `wellFormed` gains the tax shape beside the money: exactly one of the two; a code of the form `txcd_` plus at least one character wherever one appears; a non-waived calculation with no lines refused; and an explicit waived branch, no lines, carrying the one-code shape when the spans map to one code and the two-code shape (empty `tax_code`, no lines) otherwise. Two distinct codes among the emitted lines are not required: a two-code history can have chargeable usage under one code only, and the month-end answer puts one code on every line. Version 1 calculations stay version 1 and are read as today. The entry guard in `billingInvoiceRun` keeps its account and month checks and `wellFormed`, and drops only the separate top-level `txcd_` predicate, which the shape rule now covers; `lineParams` takes the line's code or, for the one-code shape, the calculation's, and refuses a line with a code at both levels or at neither.
-2. **A line has a stable identity.** The invoice engine journals each provider operation under a kind (`line-<meter>`), replays by request hash, recovers a lost answer by matching the provider's lines on `metadata[meter]` and `metadata[calculation_digest]`, and validates the draft by the same kinds. One-code calculations keep all of that unchanged. A two-code line's kind is `line-<meter>-<tax code>`, its request carries `metadata[tax_code]` (added only for lines that carry a code, so an old request's hash is unchanged), recovery matches meter, digest and tax code, and validation expects the operation of each line's own kind. The old kinds and the new cannot collide (a tax code is never empty).
+2. **A line has a stable identity, checked on every acceptance path.** The invoice engine journals each provider operation under a kind (`line-<meter>`), replays by request hash (a hash of the `InvoiceItemParams` JSON), checks the provider's answer to a POST against the frozen line, recovers a lost answer by matching the provider's lines on `metadata[meter]` and `metadata[calculation_digest]`, and validates the draft by the same kinds and keys. One-code calculations keep all of that unchanged: the new `InvoiceItemParams` field for the line's metadata tax code is `omitempty`, so an old request's JSON, and its hash, are byte-identical. A two-code line's kind is `line-<meter>-<tax code>`; its request carries `tax_code` and `metadata[tax_code]`, both from the frozen line; the POST answer is accepted only if the item's `metadata[tax_code]` is the frozen line's; recovery matches meter, digest and tax code; the draft validation expects the operation of each line's own kind and requires each listed line's `metadata[tax_code]` to be the frozen line's, equal-amount lines included. The old kinds and the new cannot collide (a tax code is never empty); a calculation is frozen before any operation and its shape decides every kind, and an old worker refuses the new shape, so no path acknowledges an old-kind and a new-kind operation for the same axis of one calculation.
-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. 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.
+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.** Attribution is per statement meter (the five billed meters by day), then combined into the four resource axes exactly as the month's usage is (`egress_gb` plus `store_egress_gb` into transfer), per code: a month with ordinary egress under Personal and store egress under Business has both in the transfer axis's split. Each per-code usage is turned into base units by the same `billingBaseUnits` floor as the month's own. Spans carrying the same code are aggregated. A statement without `Days` is usable only when every billed meter is zero (today's rule, kept: such a month is waived and needs no split); otherwise it holds as `billing_statement_days_unusable`. `Days` are validated as today (dates inside the month, finite non-negative values, sums agreeing with the monthly totals) whether the month is partial or whole.
+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).
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,18 +42,19 @@ 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`). 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 before `taxCodesForLines` stops returning the error, and no older build is rolled back to afterwards; the rule is written in `ENABLEMENT.md` beside the retirement protocol's. 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 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.
- 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. Nothing resolves a review but the operator door; `freezeBillingMonth` refuses a new month while any review of the account is open, as today, and this design adds no automatic resolution.
+- 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.
## Tests
-- Byte and digest fixtures, pinned: 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. Each byte-identical before and after, with its request hashes and operation kinds.
+- 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; 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.
-- 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.
-- The invoice lifecycle for a two-code month, behind a test switch: two lines for one axis with equal and with unequal amounts; a request interrupted after the provider wrote and before the journal acknowledged, replayed; recovery by the three metadata keys; the draft validated against both kinds; a one-code month's journal unchanged.
-- 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.
+- 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 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.
- `wellFormed` and `lineParams`: a code at both levels; at neither; a two-code calculation with one line lacking a code; a malformed code; a non-waived calculation with no lines.
- Mutants in a tree copy once the assertions above exist, each with its killing test named.