148c4f5c29
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
92 lines
6.5 KiB
Markdown
92 lines
6.5 KiB
Markdown
# `improvise-incremental`: the reasoning
|
|
|
|
`crates/improvise-incremental` is ADR 009 step 2 (improvise-t8ow, epic improvise-bw4p): the
|
|
plug. It holds the `Derived` trait, `Eager` (today's evaluator behind it) and the
|
|
differential harness every later backend is checked with. There is no incremental backend
|
|
here, the evaluator is untouched and no existing crate changed. The decisions are the ADR's
|
|
(`context/adr/009-incremental-evaluation.md`, clause 2). The evidence for each test is in
|
|
`context/test-evidence/improvise-incremental.md`.
|
|
|
|
## The shape
|
|
|
|
| item | what it is |
|
|
|---|---|
|
|
| `Derived` | the two things the evaluator reads: `value(key, agg)` and `raw_fold(partial)` |
|
|
| `Eager` | an owned `Model` plus its None categories; every answer is a call into `Model` |
|
|
| `Maintained` | a `Derived` that is told each `Edit`; `Eager` is one |
|
|
| `Edit` | one model mutation as data (set, clear, add or remove a formula, set a measure's aggregation) |
|
|
| `probes`, `Probe` | every question worth asking of a model, and a way to put it to any backend |
|
|
| `differential` | runs one edit sequence on a candidate and on `Eager`, comparing every probe at every step |
|
|
| `Divergence`, `Report` | what a failure names (step, probe, both answers) and what a pass compared |
|
|
|
|
## Interpretations
|
|
|
|
Clause 2 names the trait and says what it carries ("the formula result at a key, and the raw
|
|
fold of the cells that match a partial key") and nothing else. These are the readings taken.
|
|
|
|
1. **`Derived` lives in this crate, not in improvise-core.** The ADR says improvise-core
|
|
"gains a trait". Putting it there changes an existing crate's source, which the brief for
|
|
this step ruled out, and the brief was ambiguous about it. This departs from the accepted
|
|
ADR's text, so it is flagged to the user and not decided here: improvise-bw4p.2 moves it
|
|
(one file and a `pub use`, since the trait names only core types). While it sits
|
|
downstream, core cannot read through it, which is the clause's point.
|
|
2. **Two methods, the minimum.** `value(key, agg)` is the formula result at a key, or the cell
|
|
as stored or folded; `raw_fold(partial)` is the fold of matching stored cells. `agg` is
|
|
`Aggregation`, the evaluator's own type. Hidden-item `covers`, totals and occupancy are not
|
|
in the trait, so the differential cannot probe them, and a hidden item (view state) cannot
|
|
be exercised at all. The ADR's differential compares "every drawn cell, total and occupancy
|
|
count"; improvise-bw4p.3 adds them. A group is exercised: a stored key with one coordinate
|
|
replaced by its item's group is a probe.
|
|
3. **`raw_fold` is never a formula, and `Eager` enforces it.** For a partial naming a formula
|
|
target `Eager::raw_fold` is `None`; otherwise it is `evaluate_aggregated(partial, OverHidden)`,
|
|
because the evaluator's own raw fold (`aggregate_raw`, `aggregate_cell_values`) is private to
|
|
core. Calling it through the public path is exact for any partial that names no formula
|
|
target. improvise-bw4p.2 exposes the raw fold from core so `Eager` stops going round.
|
|
4. **`Eager` recomputes formulas after every edit.** Production does it in `rebuild_layout`
|
|
(`Model::recompute_formulas(none_cats)`), and an over-hidden read of a formula cell reads
|
|
the cache it fills. So `Eager` carries the None categories of the view it stands for and
|
|
recomputes under them after each edit, as a layout rebuild does.
|
|
5. **`Edit` is the vocabulary of the harness, not of the wire.** Five variants, each one
|
|
public `Model` call. A later step adds variants for what it maintains (an item added, a
|
|
category removed); the harness does not care which. An `Edit` fault reaches both sides of a
|
|
differential alike, so it is pinned only where a direct `Eager` test reads the result:
|
|
`ClearCell`, `AddFormula` and `RemoveFormula`, and `SetMeasureAgg` through the fold tests.
|
|
`SetCell` is read by nearly every test.
|
|
6. **Values agree by float rules with every NaN one value** (`same_value`), the rule clause 3
|
|
states for cell deltas. Signed zero compares equal, so a later `ExactSum` cannot be told
|
|
from eager's sum by the sign of a zero here; clause 8 has its own property test for that.
|
|
7. **No `zset` dependency yet.** Nothing in this step uses a Z-set. It arrives with the first
|
|
backend, as the cell delta of step 3.
|
|
|
|
## What the harness checks
|
|
|
|
`differential(model, none_cats, candidate, edits)` builds `Eager` over a clone of the model,
|
|
applies each edit to both, and after step 0 and after every edit asks both every probe. The
|
|
probe set **accumulates**: probes from the model at every earlier step stay asked, so a backend
|
|
that goes on answering for a removed formula diverges. A pass returns a `Report` of how many
|
|
states and answers were compared.
|
|
|
|
`probes(model)` takes the base keys (each stored key, and each with one coordinate replaced by
|
|
its item's group), every sub-partial of a base key dropping at most `MAX_DROPPED = 3`
|
|
coordinates plus the empty key, and each formula's target over each of those. Each key is asked
|
|
as an exact value, an over-hidden value and a raw fold. A fold dropping more than three
|
|
coordinates from a stored key is not asked about. `edit_probes`
|
|
adds the key an edit names, so a cleared cell is asked about after it is gone. A divergence
|
|
reads `divergence at step N: <probe>: eager <a> != candidate <b>`.
|
|
|
|
**A limit, stated plainly (improvise-bw4p.4).** The probe set is every stored key's
|
|
sub-partials, not the keys a view draws. A view with four or more categories on None draws a cell
|
|
that drops four or more coordinates of a stored key, and the harness checks none of those cells:
|
|
a rival wrong only at `{C0=a, _Measure=Revenue}` over a five-category model passes. Neither
|
|
shipped demo reaches the limit, so the corpus does not show it. Raising `MAX_DROPPED` is not the
|
|
answer, because `sub_partials` walks all 2^n subsets of a key whatever the limit is (93ms at 20
|
|
categories and 225ms at 22 in a debug build, per call). The fix is to probe the keys each view
|
|
draws, with steps 3 and 4; until then a candidate on such a view is under-checked.
|
|
|
|
The corpus the tests use is `examples/demo.improv` and `web/demo.improv`, loaded through
|
|
`improvise_io::persistence::load`, and hand-built models (formulas, text, mixed values, a group).
|
|
`eager_matches_the_evaluator_on_the_shipped_documents` takes every view's None categories; the
|
|
differential runs take the first per document, and the largest document only its first four
|
|
edits, to keep the binary under two seconds in a debug build. A proptest (256 cases) runs random
|
|
edit sequences against a candidate that rebuilds an `Eager` from scratch on each edit.
|