docs(adr): ADR 005 folds in the user's answer to question 16, K = 32 (improvise-l2b8)

Revision 7. Q16 decided (a), K = 32, over the K = 8 recommendation; figures
re-derived from the appendix, none new. Adds Relations to other ADRs (007, 008,
009) with flagged conflicts.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
This commit is contained in:
Claude (adr5-steward)
2026-10-08 20:41:13 -07:00
committed by Edward Langley
parent b232c5a9fa
commit 5f0bbb837b
+117 -16
View File
@@ -1,7 +1,7 @@
# ADR 005 — Exact decimal arithmetic: a three-level numeric tower for formulas
**Status:** proposed. Only the user accepts an ADR. The numbered questions at the end
are theirs to answer on the PR. ·
**Status:** proposed. Only the user accepts an ADR. All sixteen numbered questions are
answered (question 16 in revision 7); the ADR waits for the user's "Approved". ·
**Ticket:** improvise-l2b8 (parent improvise-felo) ·
**Direction from:** the user, 2026-09-22 (below) ·
**Relates to:** ADR 009 (incremental evaluation, improvise-felo.4, PR #309), which now
@@ -72,6 +72,16 @@ for question 15. It answers the review of `33833f28` (issuecomment-5836990563):
It also corrects when a last-bit miss is visible to `=`. The bound is on the **terms**,
not the total, as ADR 009 now says at `4973845f` (Codex on #309).
**Revision 7** folds in **the user's answer to question 16**, left inline on `b232c5a9`
at 03:37 UTC on 2026-10-09: "let's do k = 32". K is **32**, which is option (a) and not
the recommendation (b), K = 8. Every question 1–16 is now answered, and the ADR stays
**proposed** until the user says "Approved". The change is confined to the value of K
and the text that waited on it; the figures that depend on K were re-derived from the
appendix's own measurements and none is new (question 16's "Re-derived under K = 32"
note lists them). The revision also adds "Relations to other ADRs", a note on what ADRs
007, 008 and 009 ask of this one since it was written. It changes none of the user's
decisions, and it lists the places where the other ADRs' text and this one's disagree.
## What the user decided
The user answered inline on `e4acf643`, one comment per question. Each answer is quoted
@@ -167,7 +177,8 @@ and a read that combines at most K groups in a bigint rational and then applies
stored bound. One figure changed after the answer, and it is flagged here: the exact
read was an estimate of 16–34 µs at K = 32, and it measures **0.67–1.9 ms** in the worst
case (option (d), "What that costs"). K is a constant inside `numtower`, not a format
or wire fact, so its value is split out as question 16 rather than reopening this one.
or wire fact, so its value was split out as question 16 rather than reopening this one.
The user then decided K = 32 there.
**11. The `MIN`/`MAX` tie-break.**
> "Yeah, exact."
@@ -202,6 +213,19 @@ Decided: **(a)**, in `numtower`, beside the exact decimal accumulator. ADR 009's
crate adapts it to `zset`'s group trait through a newtype, and `numtower` can land
holding `ExactSum` alone, ahead of this ADR (clause 14).
**16. The value of K.**
> "let's do k = 32"
(Left on `b232c5a9`, on question 16's options, 2026-10-09T03:37:34Z; comment
4226381911.)
Decided: **(a), K = 32.** This is the value revision 5 proposed and question 10 was
answered with, and it is **not** the recommendation of question 16, which was K = 8.
The worst-case exact read stays at **0.67–1.9 ms**, the measured figure the question put
to the user. The ceiling stays at 8,192 bits, which already covers K = 32 (4,320 bits
needed). K remains a constant inside `numtower`, so it can still be tuned later against
the evaluator benchmark without a new ADR.
## The decision this ADR writes down
1. **Three levels** (decided, question 1). A number is an **exact decimal**, an **exact
@@ -1349,7 +1373,7 @@ integer and reads in O(1). The review proposed a mitigation, and it is adopted:
accumulator alone, and is converted to f64 **once, at an f64 read** (only when there
are more than K non-decimal groups). So a pure-decimal `SUM` pays nothing for (d).
- **Read exactly only when the current number of non-decimal groups is at most
K = 32.** Otherwise read the f64 accumulator, and the result is inexact. The count is
K = 32** (decided, question 16). Otherwise read the f64 accumulator, and the result is inexact. The count is
a function of the current state, not of history, so the choice is independent of
order.
@@ -1381,7 +1405,7 @@ integer and reads in O(1). The review proposed a mitigation, and it is adopted:
should do that.
- Denominators a person produces (`/ 3`, `/ 12`, a percentage) are small, and the read
is then far cheaper. The table is the bound, not the common case. The value of K is
question 16.
32, decided in question 16.
- **f64 read:** ADR 009's 0.6–1.1 µs.
- **Memory is still O(distinct denominators)**, because the map must survive
retraction to know when the count falls back under K. For a 90k-record margin sum
@@ -1393,8 +1417,12 @@ This keeps (d)'s wins, because `SUM(v / 3)`, averages and percentages have one o
handful of denominators, and it bounds the rest. The sum of margins reads the f64
accumulator and is inexact, as under (a). Its exact combination would overflow by the
6th term anyway (below). The 32 was a judgement: large enough for a `/ N` over a few
different Ns, and small enough to bound the read. It was not tuned, and the read's
measured cost above is why question 16 asks for a smaller one.
different Ns, and small enough to bound the read. It was not tuned. The read's measured
cost above is why question 16 recommended a smaller one (K = 8), and the user chose 32
anyway, so 0.67–1.9 ms is the accepted worst case. It is paid per read, and under ADR 009
per edit, by a maintained `SUM` that holds between 2 and 32 distinct non-decimal
denominators near `i128` size; a sum with more groups reads the f64 accumulator, and one
with few or small denominators is far cheaper (the table is the bound).
### The file and the wire
@@ -1604,9 +1632,9 @@ In order. Each step lands on its own, with the suite green.
## Open questions
The user answered questions 1–9, 11 and 12 inline on `e4acf643`, and 10, 13, 14 and
15 inline on `33833f28`. Each is marked **decided** below, and quoted in full under
"What the user decided". Question 16 is new in revision 6, and it is the only open one.
Nothing is renumbered.
15 inline on `33833f28`, and question 16 inline on `b232c5a9`. Each is marked
**decided** below, and quoted in full under "What the user decided". No question is
open. Nothing is renumbered.
1. **The tower's shape.** **Decided (the user): three levels**: exact decimal, exact
bounded fraction, inexact f64 (clause 1). The original recommendation, two levels,
@@ -1667,13 +1695,14 @@ Nothing is renumbered.
denominators, with no bigint on the read;
- **(c)** `Decimal`'s own `+`, accepting demotion that depends on order.
*Recommendation:* (a), with K = 32.
*Recommendation:* (a), with K = 32. The user confirmed the value in question 16.
- A fixed register cannot be generic for fractions at any width.
- 2,048 bits pays 256 bytes per summed group for a combination that rarely fits the
stored bound anyway.
- The bigint read is paid only when a sum holds 2 to K distinct fraction
denominators: about 16–34 µs at K = 32, an estimate. (Revision 6 measured it:
0.67–1.9 ms at K = 32 in the worst case. See question 16.)
0.67–1.9 ms at K = 32 in the worst case, and the user kept K = 32. See
question 16.)
- (c) breaks order-independence (revision 2).
11. **The `MIN`/`MAX` tie-break.** **Decided (the user): prefer the exact value**
@@ -1750,7 +1779,8 @@ Nothing is renumbered.
`ExactSum` alone, ahead of this ADR, so ADR 009 does not wait for it (clause 14).
This question is shared with ADR 009 (PR #309).
16. **The value of K** (open; added in revision 6). K is the most non-decimal groups a
16. **The value of K.** **Decided (the user): (a), K = 32** ("let's do k = 32"). Added
in revision 6, and put as follows. K is the most non-decimal groups a
`SUM` reads exactly (question 10). Above it the read is the inexact f64
accumulator. Revision 5 proposed K = 32 on an estimate of 16–34 µs per read, and
the measured worst case is **0.67–1.9 ms** (option (d), "What that costs"). Under
@@ -1761,8 +1791,8 @@ Nothing is renumbered.
denominators is inexact.
- **(c)** K = 16: worst case 174–448 µs per read.
*Recommendation:* **(b), K = 8**, with the read combining over the lcm and reducing
once (the lower figure in each range).
*Recommendation (not taken):* **(b), K = 8**, with the read combining over the lcm
and reducing once (the lower figure in each range). The user chose (a).
- The ceiling (question 13) stays at 8,192 bits under any of the three.
- The common sums keep exactness under K = 8: `SUM(v / 3)` has one denominator, and
an average or a percentage has a handful.
@@ -1771,6 +1801,77 @@ Nothing is renumbered.
- K is a constant inside `numtower`, not a format or wire fact, so it can be tuned
later against the evaluator benchmark (migration step 1) without a new ADR.
**Re-derived under K = 32** (revision 7), from this ADR's own measurements, with
nothing new measured:
- **Read cost:** the K = 32 row of the table, **0.67 ms** (lcm, then one reduce) to
**1.9 ms** (reduce after every add). `numtower` reads by the first method, so the
lower figure is the working number. The 16–34 µs of revision 5 stays withdrawn.
- **Combined width:** lcm up to about 4,100 bits (measured 3,924), numerator up to
K·127 + 256 = **4,320 bits** (measured 4,053).
- **Ceiling:** 8,192 bits, which clears 4,320 by 3,872. Unchanged, so question 13
is untouched.
- **Memory:** O(distinct denominators), which K does not change (clause 8).
- **Not re-derived:** an end-to-end per-edit latency for a maintained `SUM`. The
benchmark times the read alone. Whether 0.67–1.9 ms per edit is acceptable on a
page of such cells is a question for the evaluator benchmark, not a figure here.
## Relations to other ADRs
Added in revision 7. This lists what the three ADRs written since ask of this one, as
read at their heads, and **changes none of the user's decisions** here. Each ADR is
cited at the head named, and none was built or run.
**ADR 007 (stable identities, accepted; merged, #326).**
- **The version literal.** ADR 007 clause 10 adds a second `.improv` literal and says
its exact spelling is left to the implementation. This ADR shares it (clause 10,
question 5), so `#i` and `n/d` ride on the same bump. ADR 007 says nothing about
numbers, so nothing in its text conflicts.
- **Flag, ordering.** If ADR 007's migration ships the new literal **before** this ADR's
grammar arms, a reader of that literal would not know `#i` or `n/d`, and "shared" would
mean one literal with two grammars. The two should either land the literal together or
this ADR's arms should be added under the literal ADR 007 has already shipped, with the
old readers' behaviour on `#i` stated. That is a merge-order matter for the
implementer, not a decision the user has made.
- **Ids and display.** This ADR's name-quoting and the formula-target handling are about
names, and ADR 007 moves keys from names to ids. Numbers are not names, so clause 10's
number spellings are unaffected; any clause here that quotes a category or item by
name should be read as "by display name" once ADR 007 lands. Not re-read clause by
clause.
**ADR 008 (virtualized grid, PR #308, proposed).** Its Q28 answer, "bigint is fine, we
don't need to reinvent integers > safe size", makes a grid position a bigint above 2^53
(its Q36 spells it as a decimal string in JSON).
- **Dependency.** It adds `num-bigint` to `improvise-protocol`, a wasm-bundle crate. This
ADR already accepts `num-bigint` in `numtower` (+20.8 KB gzipped for `num-rational`
plus `num-bigint`, probed; question 8). If `improvise-core` depends on `numtower` (clause
2), `improvise-protocol` already reaches `num-bigint` through it, so the cost is paid
once, not twice. ADR 008 says its `cargo tree --edges normal` check has not been run;
neither has one here for the bundle.
- **Flag, a new consumer of `numtower`.** Positions are integers, not numbers in the
tower's sense. Whether they use `numtower`'s type or `num-bigint` directly is open in
ADR 008. The tower's `n/d` and `#i` spellings must not leak into a position's wire
spelling.
**ADR 009 (incremental evaluation, PR #309, proposed).**
- **`ExactSum` lives in `numtower`** (question 15, (a)), which ADR 009 cites. `numtower`
can land holding it alone, so ADR 009 does not wait for this ADR.
- **`Sum` is exact only under ADR 009's question 3 (c) or under this ADR's wide
accumulator.** This ADR's accumulator is exact for decimals (320 bits) and for up to
K = 32 fraction denominators (clause 8). For **f64** terms the exact accumulator is
ADR 009's `ExactSum`. A mixed sum is read from both (clause 8), so "exact under this
ADR" holds for decimal and bounded-fraction data, not for f64 data.
- **The per-edit cost.** ADR 009 reads a maintained `SUM` after every edit that touches
it. With K = 32 (question 16) that is up to 0.67–1.9 ms per edit for a cell holding
many large distinct denominators (see "Re-derived under K = 32"). ADR 009's own
`ExactSum` figures (2.0–3.6 ns per update, 0.6–1.1 µs per read) are for the f64
accumulator and are not comparable.
- **Flag, stale text in ADR 009.** At its head it describes ADR 005's cost as
`rust_decimal` alone, "not a bignum, +15.4 KB", with 40 bytes per summed group. Under
option (d) the crate also carries `num-rational` and `num-bigint` (up to about +36 KB
together, unprobed together) and a per-denominator group map. It also gives `ExactSum`
568 bytes per group where this ADR gives the decimal partition 40 bytes; those are two
different accumulators and are not a conflict.
## Consequences
- Typed values and their `+ - *` are exact. `0.1 + 0.2 = 0.3`, `1234.56 - 1234.55 =
@@ -1821,7 +1922,7 @@ Nothing is renumbered.
- A value typed past `Decimal`'s range is an exact fraction when it fits `i128`
(`1e-30`, a 30-digit integer), as the same value computed already is (clause 3).
- A `SUM` reads exactly over at most K distinct fraction denominators, at a worst-case
cost that grows about as K² (question 16), and past K it is inexact.
cost that grows about as K² (question 16: K = 32, decided), and past K it is inexact.
- Formulas gain `EQUAL(a, b, epsilon)`, and `EQUAL` becomes a keyword the writer quotes.
- **Costs:** `Num` is 24 bytes where an `f64` is 8, but **`CellValue` stays 32 bytes**
under both (a) and (d), because its `String` arm already sets the size (the review