docs: record the currency invariants and the CSS-token guards

roundMoney takes digits now, Currency is open with the dataset as authority,
codes are identity, and an amount field parses its own currency. Also folds in
the two CSS guards earned during the redesign.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
Lucas Winther
2026-09-02 15:52:42 +02:00
co-authored by Claude Opus 5
parent cdece63c68
commit 5b9754f1d2
2 changed files with 35 additions and 2 deletions
+9
View File
@@ -88,6 +88,15 @@ A float, deliberately, with two rules that make it safe:
**`FxRate.baseUnitsPerOne` is never rounded.** Rates are stored at six decimals
and are not money; quantizing one to cents is wrong in kind.
**Decimal places are data, not a constant.** `Dataset.currencies` records each
currency's code, name, symbol and digits, because the owner can define their
own. `roundMoney` therefore takes a digit count and every call site resolves it
with `digitsFor` — required rather than defaulted, so a missed site is a
compile error instead of something that works for every 2-decimal currency and
breaks on the first zero-decimal one. `src/domain/currencyDigits.test.ts` drives
a zero-decimal currency through the whole stack, which is the only test that can
tell a real implementation from one that assumes 2.
## Splits and finance plans compose
A purchase can be split across posts *and* spread over months at once. The trick
+26 -2
View File
@@ -111,13 +111,29 @@ For more information, read the Bun API docs in `node_modules/bun-types/docs/**.m
without an explicit argument. It must be testable with no DOM.
- Money is a float. Always round with `roundMoney` from `src/domain/money.ts`
after any division, after any FX conversion, and before persisting.
- **`roundMoney(amount, digits)` takes a DIGIT COUNT, not a currency code.**
Decimal places live in `Dataset.currencies`, because the owner defines
currencies; resolve them with `digitsFor(dataset.currencies, code)`. The
argument is required on purpose — a default of 2 is right for every currency
the app shipped with, so a missed call site would pass every test and be
silently wrong for the first zero-decimal currency added.
- When splitting an amount, never round the last part independently. Compute it
as `total - sum(others)` so the parts sum exactly to the whole.
- `MonthId` is the string `"YYYY-MM"`. Stored data never contains `Date` objects.
- **`Currency` is an open string code; `Dataset.currencies` is the authority.**
There is no global list of valid currencies any more. A code is IDENTITY — it
keys the FX table and every stored `Money` — so codes are normalised
uppercase, are never editable, and a currency still referenced cannot be
removed (`currencyUsage` names what refers to it). `parseDatasetJson`
validates the table and every `Money` against it; that boundary check is
what replaced the compile-time safety of the old closed union.
- The envelope rollover fold has exactly one implementation, in
`src/domain/fold.ts`. Year and summary views aggregate over it — they never
reimplement the math.
- Over-budget is always allowed. Never add validation that blocks it.
- An amount field parses its own currency: "30$" is thirty dollars. Use
`parseMoneyInput` (`src/ui/moneyInput.ts`) and keep the input `type="text"` —
a number input discards the "$" before the parser ever sees it.
- Base path comes from `BUN_PUBLIC_BASE_PATH`; never hardcode a subpath, and
read it through a `try/catch` accessor — see the gotcha below.
- A post's allocation is a dated series, `Post.rules: RuleVersion[]`, resolved by
@@ -148,8 +164,16 @@ choice.
unset, the bare reference reaches the browser and throws before the app boots.
A `typeof process` guard does not work — it survives inlining and discards the
inlined value, silently breaking subpath deploys.
- **Never define a bare `--accent` CSS variable.** shadcn owns it; overriding it
drops hover contrast below WCAG AA app-wide. Project tokens are `--budget-*`.
- **Never define a bare `--accent` CSS variable.** shadcn owns it as a hover
BACKGROUND paired with `--accent-foreground`; setting one without the other
drops hover contrast below WCAG AA app-wide. Redefine shadcn tokens only in
pairs — `src/cssPairs.test.ts` enforces it — and put brand colour on
`--primary`, never `--accent`. Project tokens are `--budget-*`.
- **A `var(--token)` naming a token `src/index.css` does not define paints
NOTHING, silently.** An undefined custom property invalidates the whole
declaration: no warning, no throw, no fallback. Renaming `--rule` this way
killed the carry meter and no test noticed. `src/ui/cssTokens.test.ts` guards
it.
- **A React error boundary does not catch event-handler throws.** Domain
functions that throw must be guarded at the call site when called from one.
- **Do not "fix" a domain throw by making it return a fallback.** Silently wrong