docs(specs): recurring costs take 7, not 6
The decimals change landed first (8d2b2ca) and took schema 6, so this spec's
migration becomes 6 -> 7 and its undo path 7 -> 8. Both specs were drafted
claiming 6; only the number had to move, because the steps were always
independent — that one changed where digits live, this one adds an array.
Three places the same commit made stale rather than merely renumbered:
- The shared split helper is smaller than designed. chargesForPurchaseInMonth
already takes digits: number rather than a currency table, so it needs no
currency plumbing.
- Display is now a rounding boundary. The Expected band gets its formatters
from useMoneyFormat(), not format.ts, and its amount field parses through
parseMoneyInput as a text input.
- currencyDigits.test.ts no longer drives one zero-decimal CURRENCY; it drives
the whole dataset at zero decimals. The expected track joins that sweep.
This commit is contained in:
@@ -214,10 +214,14 @@ already flow through it.
|
||||
|
||||
One extraction pays for itself: the split-distribution half of
|
||||
`chargesForPurchaseInMonth` becomes a shared helper taking
|
||||
`(amount, splits, splitMode, ...)`, called by both real charges and expected
|
||||
ones. Without it the two paths could round differently or absorb the remainder
|
||||
differently, and the difference would show up as a penny of phantom drift
|
||||
between the tracks.
|
||||
`(amount, splits, splitMode, index, baseCurrency, rates, digits)`, called by
|
||||
both real charges and expected ones. Without it the two paths could round
|
||||
differently or absorb the remainder differently, and the difference would show
|
||||
up as a penny of phantom drift between the tracks.
|
||||
|
||||
The decimals change (`8d2b2ca`) made this smaller than it would have been:
|
||||
`chargesForPurchaseInMonth` already takes `digits: number` rather than a
|
||||
currency table, so the helper needs no currency plumbing at all.
|
||||
|
||||
`PostMonthFigures` gains three fields, and `foldBalances` runs two accumulators
|
||||
over its existing loop:
|
||||
@@ -227,8 +231,8 @@ remaining = carriedIn + allocation - charges
|
||||
projected = projectedCarriedIn + allocation - charges - expected
|
||||
```
|
||||
|
||||
Both are rounded with the same base digits, in the same place, as `remaining`
|
||||
is today.
|
||||
Both are rounded to `dataset.settings.digits`, in the same place, as
|
||||
`remaining` is today.
|
||||
|
||||
Two invariants keep this honest, and both are tests:
|
||||
|
||||
@@ -237,7 +241,7 @@ Two invariants keep this honest, and both are tests:
|
||||
- **Once every occurrence in a month is confirmed, the two tracks reconverge
|
||||
there.** The past settles; only the future is a forecast.
|
||||
|
||||
### 7. Migration 5 -> 6
|
||||
### 7. Migration 6 -> 7
|
||||
|
||||
One appended step: `recurring: []` on the dataset. Nothing else —
|
||||
`Purchase.source` is optional, so no purchase is rewritten.
|
||||
@@ -249,12 +253,12 @@ so every existing dataset folds to the same `remaining` it did before, and
|
||||
The step writes a literal `[]` and reads no live default, so the
|
||||
no-live-default guard is satisfied with nothing to freeze.
|
||||
|
||||
> **Ordering, settled.** `docs/specs/2026-09-02-global-decimals-design.md`
|
||||
> originally claimed 6 as well. Recurring costs take 6; that change becomes
|
||||
> 6 -> 7 and its spec has been updated to say so. The two steps are independent
|
||||
> — this one writes `recurring: []`, that one rewrites where digits live — so
|
||||
> they compose in either implementation order as long as the version numbers
|
||||
> stay as assigned here.
|
||||
> **Ordering, resolved by what shipped.** Both this spec and
|
||||
> `2026-09-02-global-decimals-design.md` were drafted claiming schema 6. The
|
||||
> decimals change landed first (`8d2b2ca`) and took 6, so this one takes 7.
|
||||
> The two steps were always independent — that one moved where digits live,
|
||||
> this one writes `recurring: []` — and nothing about their content had to
|
||||
> change, only the number.
|
||||
|
||||
### 8. Validation — `export/json.ts`
|
||||
|
||||
@@ -314,9 +318,13 @@ dialog per bill would defeat it.
|
||||
**The overdue count** — unconfirmed occurrences from earlier months — appears in
|
||||
that band. §5 is otherwise silent drift.
|
||||
|
||||
Two existing rules apply directly to the inline amount field: capture
|
||||
Three existing rules apply directly to the inline amount field: capture
|
||||
`event.target.value` into a `const` before `mutate` (AGENTS.md §2, the
|
||||
deferred-write rule), and route every `store.*` call through `useMutate`.
|
||||
deferred-write rule); route every `store.*` call through `useMutate`; and get
|
||||
formatters from `useMoneyFormat()` rather than calling `format.ts` directly,
|
||||
since display is a rounding boundary and a pinned 2 there is the bug `8d2b2ca`
|
||||
fixed. The field itself stays `type="text"` and parses through
|
||||
`parseMoneyInput`, so "30$" works the way it does everywhere else.
|
||||
|
||||
### 11. Tests
|
||||
|
||||
@@ -327,13 +335,14 @@ deferred-write rule), and route every `store.*` call through `useMutate`.
|
||||
series rebases to day 42, cap hit again); `endedFrom`; suppression;
|
||||
un-confirming by deleting the purchase; and the strictly-increasing throw.
|
||||
- **`domain/fold.test.ts`** — the two convergence invariants from §6.
|
||||
- **`store/migrations.test.ts`** — 5 -> 6 adds the empty array and moves no
|
||||
- **`store/migrations.test.ts`** — 6 -> 7 adds the empty array and moves no
|
||||
figure.
|
||||
- **`export/json.test.ts`** — acceptance and rejection, including `n: 0`,
|
||||
`n: 1.5`, a `weekday` of 7, and an unknown `kind`.
|
||||
- **`domain/currencyDigits.test.ts`** — a recurring cost in the zero-decimal
|
||||
currency, so the expected track rounds through the same guard as everything
|
||||
else.
|
||||
- **`domain/currencyDigits.test.ts`** — the file now drives the whole dataset
|
||||
at zero decimals and asserts that raising the setting to 2 changes what the
|
||||
fold produces. The `expected` track joins that sweep, so it cannot be the one
|
||||
path that quietly assumes 2.
|
||||
|
||||
`scripts/demo-data.ts` seeds a monthly rent and a 28-day subscription, so the
|
||||
band is screenshot-verifiable.
|
||||
@@ -370,7 +379,7 @@ band is screenshot-verifiable.
|
||||
|
||||
## If this needs undoing
|
||||
|
||||
Deleting `Dataset.recurring` and the `expected` / `projected` fields is a 6 -> 7
|
||||
Deleting `Dataset.recurring` and the `expected` / `projected` fields is a 7 -> 8
|
||||
migration that loses no ledger data — every confirmed occurrence is already an
|
||||
ordinary `Purchase` and stays one, with a dangling `source` that validation
|
||||
would then ignore. The reverse of §7, plus dropping two modules.
|
||||
@@ -402,7 +411,7 @@ dropping the second balance means deleting three fields from
|
||||
- **AGENTS.md §2** — the termination guard, and why `n >= 1` is rejected rather
|
||||
than clamped.
|
||||
- **docs/DECISIONS.md** — a new "Recurring costs" heading pointing here, and
|
||||
"Now at version 5" becomes 6.
|
||||
"Now at version 6" becomes 7.
|
||||
- **docs/PRODUCT.md** — the capability list and the terminology section gain
|
||||
the concept.
|
||||
- **docs/ARCHITECTURE.md** — the layer diagram and the derived-values list gain
|
||||
|
||||
Reference in New Issue
Block a user