Design: per-line tax codes, revision 2 (line identity in the invoice engine, eligibility before attribution, five meters to four axes, exact apportionment, the older worker's review and the roll rule, held months by the operator door, 16a-16d as the owner's)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 file changed,
+33
−22
docs/superpowers/specs/2026-10-03-per-line-tax-codes-design.md+33 −22
CI passed — 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 4ce5f94e3..c9efb9671 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 1, for review. 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 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`. |
| ## What happens today |
| @@ -13,43 +13,54 @@ Two facts make this a tax-code question and nothing else: |
| So the only thing missing is which tax code each overage line carries. That is decision 16, with two answers on the table: split each meter's line by span (one line per tax code, the usage of each UTC day attributed to the plan in force at that day's start) or tax the whole month by the plan in force at the month's end. Both keep every amount as it is today. |
| +## What is the owner's, stated as such |
| + |
| +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. |
| +- **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. |
| + |
| +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. |
| + |
| ## What this design builds, and what it leaves to the decision |
| -It builds the mechanics either answer needs, with the hold left exactly where it is until the answer comes, and every month that has one tax code frozen byte for byte as it is today. |
| +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"\``. For a month with one code the field is empty on every line and the calculation's `tax_code` carries it, as now: the frozen JSON is byte-identical, so every digest already recorded stays valid. For a month with two codes the calculation's `tax_code` is empty and every line carries one. `wellFormed` requires exactly one of the two shapes: a non-empty `tax_code` with no line carrying one, or an empty `tax_code` with every line carrying one. `lineParams` reads the line's code first and the calculation's second, and refuses a line with 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, 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. |
| -2. **The allowance names its spans.** `plans.MonthAllowance` gains `Spans []PlanSpan \`json:"spans,omitempty"\`` with `From`, `To` and `Plan`, one per paid assignment interval the allowance counted (the intervals `AllowanceForMonth` already computes as `lo` and `paidHi`, before adjacent ones are joined into `Covered`). It is written only when the spans carry more than one tax code (the console decides that; the plans package does not know tax codes, so the console asks for the spans and drops them when they all map to one code), so a single-code month's JSON is unchanged. |
| +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). |
| -3. **A day's usage is attributed to a span.** A console function takes the statement's `Days` (the five billed meters by UTC day; present on every statement written since the field existed, absent on older ones), the spans, and a rule, and returns each meter's usage per span. The rule this design proposes, for decision 16's split answer, is the one named there: a UTC day's usage follows the plan in force at the day's start (00:00 UTC); a day no span covers is not attributed (its usage is not billed, as today for a partly covered month). A statement without `Days` cannot be split: such a month holds as today. |
| +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. |
| -4. **A meter's line is apportioned, not re-priced.** Under the split answer each meter's overage amount, computed for the whole month exactly as today, is divided among the meter's tax codes in proportion to the meter's attributed usage per code, in micros, with the whole minor units handed out by the same largest-remainder rule `overageLines` uses, so the lines of one meter sum to the meter's amount and the invoice total is unchanged. A meter whose usage all fell under one code has one line. Under the month-end answer every line carries the last span's code and nothing is divided. |
| +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. |
| -5. **The hold stays until the decision.** `freeze` keeps returning `errBillingTaxCodeMixed` for a two-code month. The decision is applied by one function, `taxCodesForLines`, which today returns the error for two codes; the split and the month-end answers are two bodies for it, both written and tested behind the error so the chosen one is a one-line switch and a review, not a build. |
| +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. |
| -Nothing in this touches the price book, the plans' amounts, the retirement protocol, the ledger or the provider calls, other than the tax code on an invoice item which already exists per item. |
| +6. **The hold stays until the decision.** `freeze` keeps returning `errBillingTaxCodeMixed` for a two-code month; its answer is one function, `taxCodesForLines`, which today returns the error, with the bodies for the answers written and tested behind it. Activation is a reviewed one-line change plus the roll rule and 16d below. |
| -## Compatibility |
| +## Compatibility and the roll rule |
| -- An older binary reading a two-code calculation written by this one sees an empty `tax_code` and refuses it in `wellFormed` and `lineParams`: it does not invoice it and it does not corrupt it. Roll order is therefore free. |
| -- A calculation frozen before this change is read exactly as before: its `tax_code` is set, its lines carry none. |
| -- The barrier trigger on `billing_months` reads `$.v` and is not touched; the calculation's version stays 2. |
| -- Nothing is backfilled: a month already held for two codes is frozen when the decision's function first prices it, from the same statement and history, and its review is resolved by that invoice run as any hold is. |
| +- **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. |
| +- 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. |
| ## Tests |
| -- A single-plan month's frozen JSON and digest are byte-identical before and after (a recorded calculation from the test fixtures compared as bytes). |
| -- A two-code month still holds, with the same review, and its calculation is not frozen. |
| -- The attribution: a change at 00:00 UTC, at 15:00 UTC (the day follows the plan at its start), a day no span covers, a statement without `Days`. |
| -- The apportionment: the lines of each meter sum to the meter's amount, the invoice total is unchanged, a meter with usage under one code only has one line, the largest-remainder hand-out, zero-usage meters. |
| -- `wellFormed` and `lineParams` refuse the mixed shapes (a code at both levels; a code at neither; a two-code calculation with a line missing its code). |
| -- An older binary's view, by a test that strips the line codes from a two-code calculation and shows it is refused rather than invoiced. |
| -- Mutants in a tree copy, each with its killing test named. |
| +- 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. |
| +- 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. |
| +- `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. |
| ## Not in this design |
| -Which answer decision 16 takes; any change to amounts; a door that invoices a held month by hand; the box side (statements already carry `Days`); copy on the invoice line (the description stays input-bound until the copy review). |
| +16a to 16d; any change to pre-tax amounts or to the monthly floor; a door that invoices a held month by hand (the operator door resolves the review and the pass invoices); the box side (`Days` are written for every account with usage); copy on the invoice line (the description stays input-bound until the copy review). |
| ## Questions for the reviewer |
| -A. Is there a third answer to decision 16 that the mechanics above would not serve? B. Is the apportionment in proportion to attributed usage the right split of an unchanged amount, or would a per-span overage (each span priced against its own pro-rated allowance) be the honest reading of "the plan in force", and if so what does it do to the floor and to a span with no overage of its own? C. The change day: is "the plan in force at the day's start" defensible, given the box aggregates usage by UTC day? D. Is leaving the frozen JSON byte-identical for single-code months worth the two shapes, or should every calculation carry per-line codes from now on and the digest rule be stated for the two generations? E. Anything an older binary could do with a two-code calculation other than refuse it. |
| +A. Does the line identity (`line-<meter>-<code>`, `metadata[tax_code]`) cover every path the journal, replay, recovery and validation take, and is there a path where an old-kind operation and a new-kind one for the same axis could both be acknowledged? B. Is "eligible days as today, then attribution against unmerged spans" the right order, and is there an eligible day that no span contains? C. Is the apportionment now reproducible from the text alone (five to four, base units per code, floor and largest remainder within the axis, byte-order ties, sorted iteration)? D. Is the roll rule sufficient for the older worker, given that the hold it files is resolvable by the operator door? E. Anything the owner is being asked to decide that is in fact mechanics, or the reverse. |