docs(adr): ADR 009 revised to fit ADR 008's view/host split (improvise-felo.4)

Adds the user's ADR 008 statements that bear on this ADR, clauses 12-14
(network transparency, incremental max width, resident state), keys
operator state by projection shape at item grain, retires the per-mirror
delivery assumptions, and appends questions 12-21. Questions 1-11 stay
open. Reads ADR 008 at 69f0a7c6.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
This commit is contained in:
Claude (adr9-steward)
2026-10-05 14:54:30 -07:00
committed by Edward Langley
parent 502373c025
commit a9bcb816e9
+714 -72
View File
@@ -1,12 +1,19 @@
# ADR 009 — Incremental evaluation: an in-house DBSP core, plugged in behind a trait
**Status:** proposed. Only the user accepts an ADR. The numbered questions at the end
are theirs to answer on the PR. ·
are theirs to answer on the PR. **Revised on 2026-10-05 to fit ADR 008 revision 19**
(PR #308, read at `69f0a7c6`, which the user is still reviewing). The revision brings in
the view/host split, the user's answers on ADR 008 that bear on this ADR, and the details
ADR 008 routes here. They are in the section "What ADR 008's review established for this
ADR", in clauses 12 to 14, and in questions 12 to 18. **The user has answered none of
this ADR's questions 1 to 11.** That was checked on #309 on 2026-10-05: its only inline
comments are Copilot's two and our replies. ·
**Ticket:** improvise-felo.4 ·
**Decision from:** the user, 2026-09-25 (below) ·
**Builds on:** ADR 001 (the record tiers), ADR 003 (live-backed models) ·
**Relates to:** ADR 005 (exact decimals, improvise-l2b8, proposed on PR #320), ADR 008 (the
virtualized grid, PR #308, which is revised to assume this ADR) ·
virtualized grid and the view/host split, PR #308, which assumes this ADR; this revision
reads it at `69f0a7c6`, its revision 19) ·
**Governed by:** `context/design-principles.md` §3 and §5
**Numbering.** ADR 005 and 006 are claimed by improvise-l2b8 and improvise-ilvl. ADR 005
@@ -14,8 +21,13 @@ is proposed on PR #320, and 006 has no file yet. 007 is merged, and 008 is PR #3
no open PR claimed it when this was written.
**Citations.** Every `file:line` below is at `gh/main` `57d385e1`. Line numbers are
dated, so re-derive them before citing this document elsewhere. "ADR 008 clause N" means
revision 3 of `context/adr/008-virtualized-grid.md` on PR #308.
dated, so re-derive them before citing this document elsewhere. **The 2026-10-05
revision's new citations are at `gh/main` `50e1314d`**, and each says so. The older
citations were not re-derived, and some have moved. For example, `Host::mutating_frame`
takes its snapshot at `host.rs:224-225` at `50e1314d`, not `:162`, and
`App::apply_effects` starts at `app.rs:1069`, not `:989`. "ADR 008 clause N" means
revision 19 of `context/adr/008-virtualized-grid.md` on PR #308, at `69f0a7c6`. Clauses 3,
5 and 8, which this ADR cites most, have the same numbers there as in revision 3.
## The decision this ADR writes down
@@ -61,7 +73,123 @@ from scratch. The machinery is:
**The grid is the second consumer.** ADR 008 needs the same maintained state for other
purposes: occupancy for `prune_empty`, the occupancy delta for its frame observation,
and column totals over rows never scrolled to (clause 9). It gets them from the same
circuit, not from a second mechanism.
circuit, not from a second mechanism. Since ADR 008's view/host split, the grid's consumer
may be in another process, and clause 12 says how the same circuit serves both.
## What ADR 008's review established for this ADR
At its revisions 18 and 19, ADR 008 (PR #308) became the ADR of the **view/host split**, and the
user routed part of it here. This section quotes what the user said that bears on this ADR,
and says where each statement came from.
- **PR comments** are cited by their GitHub comment id and UTC time. The question numbers
are ADR 008's, as ADR 008 assigns them.
- **The two chat statements** were given to the orchestrator and relayed to this ADR. They
are on no PR, and ADR 008 records them the same way. The user's "Approved" on this ADR
would confirm them.
None of this answers this ADR's own questions 1 to 11.
**Routed here, in chat:**
- "The DBSP machinery needs to be implemented in a network transparent way, but the
details of that go to ADR 009" (chat, relayed on 2026-10-05). Clause 12.
- "Thinking about it a bit more, we might need to keep track of recent projections +
clients to stream aggregation results back to the UI correctly. But this should be as
stateless as possible" (chat, relayed on 2026-10-05). Clause 12.
- On ADR 008's question 53: "Q53 is cross product: I'm still ambivalent about which side
of the wire pruning should happen on" (chat, relayed on 2026-10-05). So a request's
top-left is a cross-product position, and which side prunes is open. ADR 008 records it
at revision 19 as its question 53 answered and its question 57 open. Clause 12,
question 19.
- "But the top left should be as absolute as possible" (chat, relayed on 2026-10-05).
Clause 12, question 20 (ADR 008's question 58).
- "After pruning we might have to stream rows and columns to attempt to fill out the full
grid" (chat, relayed on 2026-10-05). Clause 12, question 21 (ADR 008's question 59).
**Asked on ADR 008, and not yet answered:**
- "Maybe dbsp can give us incremental max width?" (#308, comment 4171487459,
2026-10-03T03:15:38Z, on ADR 008's question 2). Clause 13 is this ADR's reading of it.
Question 12.
**The read side and the write side** (#308, inline comments of 2026-10-05):
- Q43 (4189071865, 21:34:45Z): "yeah, the read side of the model is a projection of the
model's full space into a particular set of axes using a `project` verb, with
offset+dimensions. The write side is an apply function that can only take model
commands and applies them to the model using Redux-style Flux architecture."
- Q38 (4189007864, 21:27:55Z): "No, axes are a model concept because they define the
grid. Aggregation should always be done model-side for consistency."
- Q38 (4189027924, 21:30:08Z): "A grid request would look something like:"
```
Axes: Row(A,B,C), Col(D,E), Page(F), None(G,H)
Top-left (4,5) # relative to the cross-product of the axes in the category-native order (which might be arbitrary)
Dimensions (10,10) # so get ten rows and ten columns worth of data from the grid defined by the axes.
```
- Q33 (4188936570, 21:20:11Z): "The host shouldn't have to know anything about the
viewport: it's mostly a question of the parameters of the grid the user is actually
looking at, rather than a "viewport" strictly speaking."
- Q37 (4189000059, 21:27:04Z): "I don't think the host needs to no [sic] anything about
the UI: the initial load of the UI should just be the default view stored in the model
(or, in some cases, a particularly requested view). Sending UI state over the wire is
an architecture smell."
- Q42 (4189051110, 21:32:31Z): "The space scrolling happens in is defined by the axes, top
left corner offset and with/height. But how much of this space is actually materialized
and when fetches, etc. happens (and model values are cached) is client-policy driven."
- Q32 (4188917796, 21:18:17Z): "I can't think of any cursor command with model-side
effects."
- Q27 (4188816383, 21:07:43Z): "go with the recommendation: the ordered list of records is
a model-side concept."
- Q31 (4188911659, 21:17:39Z): "You have to distinguish view in the sense of UI and view
in the sense written into `.improv` files. P has to be a model-side effect because it
prunes data for a particular view. It is not a UI-only command." And (4188926877,
21:19:10Z): "The only real issue here is that pruning shouldn't infect every client
here. It might need to be UI-only until a view is saved, and then the view definition
goes to the model."
- Q29 (4188887721, 21:15:14Z): "Prune should only be written if the user explicitly
unpruned the view."
- The cursor: "The cursor should never jump: it should stay where the user left it as much
as possible." (Q12, 4171501445, 2026-10-03T03:21:05Z). And "As far as possible, updates
should shift the grid being viewed to keep the cursor in the same visual place on the
screen." (Q34, 4188946818, 2026-10-05T21:21:18Z).
**Answers ADR 008 records that this ADR's text had not caught up with:**
- Q1 (4171484686, 2026-10-03T03:14:34Z): "go with occupied". Clause 9.
- Q2 (4171486541, 03:15:19Z): "do b, but maybe with some easing into narrower in cases
where the column size has been stably smaller." And Q22 (4188309022,
2026-10-05T20:15:12Z): "hysteresis as recommended." Clauses 9 and 13.
- Q6 (4171493762, 2026-10-03T03:18:12Z): "maintained incrementally". Clause 9.
- Q13, Q14 and Q15: "the recommendation is fine.", three comments (4171504439,
4171505979 and 4171509012, 2026-10-03T03:22:16Z to 03:23:36Z), which ADR 008 records as
its answers to those three questions. Q13 sets the frame budget, and Q15 sends the
formulas the per-record map cannot express to the occupied contexts. Clauses 5a and 14.
- The store's memory (4171481179, 2026-10-03T03:13:14Z): "We have to be careful here to
minimize memory requirements of the store: offloading inactive regions to disk (maybe
mmap or similar), double-delta/gorilla/lz4 compression, etc." And Q17 (4188271710,
2026-10-05T20:11:02Z): "Let's defer this as recommended." Clause 14.
- Jobs (4171458281, 2026-10-03T03:04:52Z): "We also have to have a way to
invalidate/cancel in-flight jobs efficiently and probably a way to throttle them (a
priority queue or similar) to avoid pinning the CPU." Then (4171694278, 04:30:31Z):
"priority queue for work stealing is also important here." Clause 7.
**What this ADR reads from them.** These are the author's readings, and each one names
the clause it changes:
1. **Reads and writes go through `project` and `apply`.** A consumer reads the circuit's
outputs through `project`, and the circuit's inputs change only through `apply` of
model commands. So nothing here may depend on a host-held mirror of a client
(`Sessions`, `ClientSession`, `reduce_view` on the host), on the per-mirror diff
(`grid_updates`), or on resetting a client's cursor. Clauses 9 and 12.
2. **Aggregation stays on the model side.** No client runs an aggregate operator, so
network transparency concerns the circuit's outputs, and a live source's inputs. It
does not mean splitting operators across a wire. Clause 12.
3. **State is keyed by the request's shape, not by a view's name.** Axes are a model
concept, so operator state keyed by axes is model state, not UI state. But a `project`
request names its own axes, and an unsaved request may carry parameters no saved view
holds (ADR 008's flagged reading 1, its question 51). Clause 7, question 14.
4. **The record order belongs to the store.** The ordered list of records is the store's
(Q27). The circuit may consume it, and does not own it. Clause 9.
5. **The memory requirement covers this ADR too.** The store-memory requirement covers the
circuit's resident state as well, so the non-goal "spilling to disk" is withdrawn.
Clause 14, question 17.
6. **Five of ADR 008's questions are answered.** Those are questions 1, 2, 6, 13 and 15,
and clauses 5a, 9, 13 and 14 now follow the answers.
## Context
@@ -133,9 +261,9 @@ whose inputs moved, over aggregates that are maintained rather than re-folded. T
machinery is a new, problem-agnostic workspace crate implementing the DBSP core.
improvise-core defines a trait for the derived state its evaluator reads. Today's
evaluator is one implementation of that trait, and a circuit built on the new crate is
the other. Both stay in the tree, and a differential test holds them equal.** Eleven
the other. Both stay in the tree, and a differential test holds them equal.** Fourteen
clauses, plus clause 5a. It is numbered 5a so that no clause number or question number
cited elsewhere moves.
cited elsewhere moves. The 2026-10-05 revision added clauses 12 to 14.
### 1. The crate: `zset` (name is question 1)
@@ -179,8 +307,12 @@ Like `gp-tree`, it knows nothing about improvise. It has **no dependencies** bey
improvise's circular semantics are not a least fixed point (clause 6).
- **Timestamps beyond one clock, lattices, and out-of-order input.** improvise applies
batches in one total order (ADR 001, "There is already a total order").
- **Persistence, spilling to disk, and threads.** State lives in memory, and a browser
worker has one thread.
- **Spilling to disk, compression, and threads, in the first version.** State lives in
memory, and a browser worker has one thread. **This was a non-goal and is now only a
deferral.** The user's store-memory requirement (ADR 008 clause 12) covers this state
too, and clause 14 says what the crate must not rule out. A host with threads may run
independent circuits on a pool (ADR 008 question 23's answer). One circuit's `step`
stays single-threaded.
- **A query language or planner.** Circuits are wired by hand in the seam crate
(clause 2).
@@ -244,7 +376,9 @@ same-value remove-then-set would cancel, defeating 5a.
An overwrite with an equal value, `−(k, v, s) + (k, v, s)`, consolidates to the empty
Z-set. A remove-then-set of an equal value does not: `s ≠ s′`, which is the point. The circuit steps once
per model effect, as ADR 008 clause 3 requires: the frame observation reads occupancy
inside the effect loop (`app.rs:1073-1084`).
inside the effect loop (`app.rs:1073-1084`). ADR 008 clause 8 revises that observation
under the view/host split. The per-effect step stays, because each step is what clause
12 stamps.
**A delta key's identity is bit identity, the same identity the record tier uses
today.** Two elements have the same `(key, value)` part exactly when their keys are
@@ -301,7 +435,7 @@ new is observed. Its cost is O(formulas), where `document_content` is O(cells) p
**The group** is the cell key with the view's `none_cats` removed. That is the partial
key `evaluate_unfolded` folds over. There is one aggregation circuit per distinct
`none_cats` list, shared by every view that has that list. That replaces
`none_cats` list, shared by every projection shape that has that list (clause 7). That replaces
`rederived_for`'s model clone (`project.rs:195-210`). **The aggregator** for a group
comes from its `_Measure` coordinate through `measure_agg`, as `agg_for` does
(`types.rs:717-722`).
@@ -541,9 +675,16 @@ is one evaluator.
**The boundary.** The map is sound only when `expr` reads the record's own cells, so
that its value at a record changes only when that record changes. An inner expression
that reads another record, holds a nested aggregate, or refers to a formula whose value
depends on other records, is not a function of one record. That formula stays on the
eager path, and the differential test pins the boundary. ADR 008 clause 5 adopts the
same boundary. Two regressions pin the record stage against eager:
depends on other records, is not a function of one record. **Revised on 2026-10-05:**
such a formula does **not** stay on today's eager path, because that path is
`Expansion::members`, the cross product (ADR 008 clause 5, a finding of Copilot's on
#308). It takes ADR 008's **store-driven fallback**, which evaluates the expression only
over the **occupied contexts**, the contexts some record projects to. The user accepted
that recommendation, as ADR 008's question 15 ("the recommendation is fine."). It differs
from today's eager answer wherever the expression is non-empty at a context that holds no
record, and ADR 008 carves those cases out of its differential test by name. Whether this
ADR's `Eager` adopts the same rule, so that its own differential test needs no carve-out,
is question 18. Two regressions pin the record stage against eager:
- one changes **one operand** of a two-operand expression (`Price` alone, under
`SUM(Revenue * Price)`);
- one **deletes a record's leading cell**, including a cell the expression never reads
@@ -555,7 +696,10 @@ another category (`[CustomCat]`), the context keeps a `_Measure` coordinate. A r
whose key `record_of` takes without `_Measure`, then belongs to the context only through
its cell under that measure, and eager orders it by *that* cell's position, not by the
record's first live cell. Until 5a defines that projection and its order key, such
formulas stay on the eager path, and the differential test pins that they do.
formulas stay on the eager path, and the differential test pins that they do. **That
eager path is `Expansion::members`, so while it lasts it blocks ADR 008's retirement of
`Expansion::members` (its migration step 6) for these formulas.** Sending them to the
occupied-contexts fallback above as well is question 18.
**What stays eager until step 5, and what is bit-equal before step 6.**
- **Before step 5** (formulas), every aggregate over an expression is evaluated as
@@ -610,13 +754,58 @@ post-loop `view_shape` diff that bumps `layout_version` (`app.rs:1102`, `:1116-1
That diff comes too late, and it cannot see most of these changes. ADR 008 clause 3
states the same rule.
- **Operator shape, per view.** The operator shape is `view_shape` (`app.rs:40-72`),
plus the group membership of every drawn item, collapsed or not, plus
`measure_agg`. `view_shape` itself is not widened, so what bumps `layout_version` is
unchanged. A view whose operator shape moved has its operators rebuilt from the
integrated base Z-set **before the frame observation that follows this effect**.
The `LayoutGeometry` frame arm (`app.rs:1062-1071`) reads occupancy right after the
effect that moved the shape, so the next effect's cell delta is too late.
- **Operator shape, per projection shape (revised on 2026-10-05).** Operator state is
keyed by the **operator shape of a `project` request**, not by the name of a view
(question 14). The shape is made of:
- the categories of each axis, by level;
- the None categories;
- the filter and page keys;
- the hidden items and collapsed groups the request carries;
- `measure_agg`.
A saved `.improv` view supplies these to a request that names it. An unsaved request
carries its own, for example an unsaved prune or hide (ADR 008's flagged reading 1, its
question 51). Requests with the same shape share one set of operators, whichever
clients sent them. The live shapes are a **cache**. A shape enters when a request names
it, and it is evicted once no recent request has named it (clause 12). For a saved
view the shape still includes what `view_shape` (`app.rs:40-72` at `57d385e1`) hashes,
plus the group membership of every drawn item and `measure_agg`. `view_shape` itself is
not widened, so what bumps `layout_version` is unchanged.
*As first written* (superseded): a view whose operator shape moved had its operators
rebuilt **before the frame observation that follows this effect**. That rule served the
per-view frame observation, which ADR 008 clause 8 revises under the split. The three
rules below replace it.
- **Item grain (ADR 008's first request; question 16).** Occupancy and the per-column
aggregates are keyed by the **items** of the axis categories, together with the filter
and page keys. They are never keyed by a position in a level sequence. So a collapse,
expand, hide or show of an **axis** category's items changes no operator. The visible
axis is a remap of the item-grain state. ADR 008 clause 3 measured that remap at 2.3 ms
for 100k occupied tuples, against 6 to 45 ms for a rebuild. Adding an item changes no
operator either, because a new item tuple is just a new key. Three things still
rebuild a shape:
- a change of which categories are its levels;
- a change of its filter or page keys, which includes hiding an item of a category that
is not on an axis;
- a change of `measure_agg`.
A collapsed group's value is a pooled fold over its members. With item grain it is
combined from the members' state when it is read. For `Sum` and `Count` that combine is
linear. For the integrated aggregates it costs O(members) per read, and whether that
fits the frame budget at a 1,000-member group is **not measured**.
- **A rebuild is a job (ADR 008's third request).** A rebuild costs O(cells), so it
cannot run on the hot path. ADR 008 clause 11 sets 8 ms for a normal UI operation and a
2 ms slice for a job, and the user accepted that recommendation (ADR 008 question 13).
So a rebuild is a resumable `step(deadline)` over an owned snapshot of its input. The
deltas that arrive meanwhile are buffered and added at install, which Z-set addition
makes exact. A circuit `step` on a cell delta is O(delta), and it is never sliced.
Several things are ADR 008 clause 11's, not this ADR's: what a pending shape shows, the
install as a `Model(LayoutGeometry)` effect, and the cancellation, throttling and
priority queue the user required. This ADR only makes its rebuilds steppable and
cancellable between slices.
- **A crossing count (ADR 008's second request).** Each step's occupancy output carries
the number of tuples whose count crossed zero. ADR 008's ordered index uses it to
choose between an insertion and a rebuild by sorting.
- **The formula set is an input, not a rebuild.** After each model effect the host
compares a fingerprint of the formula set, over (`raw`, `target`,
`target_category`), with its previous read. If it moved, the formula delta (−the old
@@ -628,18 +817,18 @@ states the same rule.
- **A document swap rebuilds every circuit** from the new store. `install_workbook`
bumps `document_generation`, and the same after-each-effect read sees it.
Re-reading the operator shape costs O(items) per view per model effect, and the formula
Re-reading the operator shape costs O(items) per live shape per model effect, and the formula
fingerprint O(formulas). Neither grows with the cells or the grid. A rebuild costs
O(cells), once per actual reshape. The same `view_shape` function is therefore read at
two moments: by the circuit after each effect, and by `layout_version` after the loop.
They answer different questions, and neither replaces the other.
**The worked case is gotcha 43's frame, `[AddItem(East), SetCell(East)]`.** `AddItem`
moves the operator shape, so the view is rebuilt with East in its sequences. Only then
does `SetCell`'s delta arrive, at operators that already know East. So the in-loop
`CellData` frame observation reads occupancy for the new shape. The `LayoutGeometry`
arm (`app.rs:1062-1071`) reads the frame immediately after each geometry effect, and it
reads a rebuilt occupancy for the same reason.
**The worked case is gotcha 43's frame, `[AddItem(East), SetCell(East)]`.** *As first
written* (superseded by item grain): `AddItem` moved the operator shape, so the view was
rebuilt with East in its sequences before `SetCell`'s delta arrived. **Under item grain,
`AddItem` moves no operator.** `SetCell`'s delta inserts the item tuple `East` as a new
key, and only the visible axis, which is derived, gains a row. The case needs no
ordering rule at all.
**Recommended: a page selection is not a rebuild (question 7).** Under the recommended
answer, the group key includes the page categories, so `[` and `]` read another slice
@@ -886,37 +1075,61 @@ option, and its recommendation.
### 9. What ADR 008 gets from it
ADR 008 (revision 3, PR #308) assumes these, under the shared names below.
ADR 008 (revision 19 at `69f0a7c6`, PR #308) assumes these, under the shared names below.
**Revised on 2026-10-05 for the view/host split.** Every item below is a model-side fact.
A consumer reads it through `project` (clause 12), and none of it is addressed to a
host-held mirror of a client.
- **Occupancy** (ADR 008 clause 3). For each view, there is a count per row tuple and
per column tuple of cells that hold a value. Stored cells pass the view's filter, with
hidden items dropped, and a collapsed group's members map to the group's tuple.
Formula cells count by clause 5's stem rule, which is when today's `formula_cache`
holds a value: a top-level `WHERE` must match, or raw data must exist at the target.
The operators are rebuilt per clause 7, so a batch that reshapes and then writes feeds
its write to the new shape. Page coordinates key a slice, subject to question 7. A tuple
is on the axis iff its count is above zero. **This ADR does not settle ADR 008
question 1 (both cases: fold rows with a formula column, and `Exact`).** Today those
cells are values with no stored cell under them. The stem definition drops them, and
if the user rules otherwise, those views fall back to a non-occupancy path. Occupancy
is integer and order-free, so it does not wait for ADR 005.
- **The occupancy delta** (ADR 008 clause 8). `distinct` over occupancy emits a
non-empty delta exactly when a tuple enters or leaves an axis. A reframe is a
non-empty occupancy delta. The two-arm asymmetry of the frame observation (gotcha 24,
improvise-mcq4) is unchanged, but each arm costs O(delta) instead of a rebuilt grid
per view.
- **Column totals** (ADR 008 clause 5). A column total is the formula evaluated over
the pooled fold (`GridEval::column_total`, `crates/improvise-core/src/view/layout.rs:976-980`),
- **Occupancy** (ADR 008 clause 3). For each projection shape (clause 7), there is a count
per row tuple and per column tuple of cells that hold a value. The counts are kept at
**item grain**, and the visible axis is a remap of them.
- Stored cells pass the shape's filter.
- A hidden item of an axis category is masked only when the visible axis is derived.
- A collapsed group's members map to the group's tuple at read time.
- Formula cells count by clause 5's stem rule, which is when today's `formula_cache`
holds a value: a top-level `WHERE` must match, or raw data must exist at the target.
- Page coordinates key a slice, subject to question 7.
A tuple is on the axis exactly when its count is above zero. **ADR 008's question 1 is
answered: "go with occupied"** (#308, comment 4171484686). So the two cases where
today's pruning keeps a row with no stored cell under it are dropped on purpose: a fold
row with a formula column, and `Aggregation::Exact`. Clause 5's stem rule already drops
them, there is no "non-occupancy path" for them, and the differential test carves them
out by name. *As first written*, this item said this ADR "does not settle" that
question; that is superseded. Occupancy is integer and order-free, so it does not wait
for ADR 005.
- **The ordered record list** (ADR 008 clause 1, `Records`). The user: "the ordered list
of records is a model-side concept" (Q27). It is the store's order (gotcha 18,
`DataStore::live_records`), and this ADR does **not** maintain it as a per-shape
operator. Clause 5a's record stage keys records by `record_of` and orders them by the
store sequence, so it *consumes* the store's order and never defines a second one. How
a page and filter select from that list is ADR 008's question 55. It is not this ADR's
to answer, because this ADR keeps no per-view copy of the list. Whether records mode is
`project` over the virtual axes (ADR 008 question 54) changes nothing here: occupancy
over `_Index` and `_Dim` is the same operator as over any other axis.
- **The occupancy delta** (ADR 008 clause 8, which the split revises). `distinct` over
occupancy emits a non-empty delta exactly when a tuple enters or leaves the **visible**
axis (ADR 008 clause 3, "A write or removal on a collapsed or hidden axis"). *As first
written*, its consumer was the per-view frame observation and its reset of each client's
cursor (gotcha 24). Under the split the host holds no cursor. So the delta is a model
fact, published on clause 12's delivery stream to the consumers of that shape, and a
client uses it to shift its grid so that the cursor keeps its place (Q34). Whether the
host pushes it is question 13, which is ADR 008's question 52 seen from this side.
- **Column totals** (ADR 008 clause 5). A column total is the formula evaluated over the
pooled fold (`GridEval::column_total`, `crates/improvise-core/src/view/layout.rs:976-980`),
not a sum of row values. So the circuit keeps a per-(column tuple, raw measure)
aggregate, and the formula layer re-evaluates only the columns whose inputs moved,
through clause 2's trait. `Expansion::members` is replaced by clause 5. Totals that
hold a `Sum` or `Avg` wait for step 6, whose prerequisite is question 3's answer: the
exact accumulator (recommended) or ADR 005 (clause 8).
- **What ADR 008 therefore stops asking.** Its question 6 (totals: query and memoize,
or maintain) is struck there as decided by the user through this ADR. Its questions 1
and 2 stay open there.
- **What it does not get.** Column widths depend on formatted text, which is
display-only (gotcha 8), so they are not maintained here. They stay ADR 008 question 2.
aggregate at item grain, and the formula layer re-evaluates only the columns whose
inputs moved, through clause 2's trait. Clause 5 replaces `Expansion::members`. Totals
that hold a `Sum` or `Avg` wait for step 6, whose prerequisite is question 3's answer:
the exact accumulator (recommended) or ADR 005 (clause 8).
- **What ADR 008 no longer asks.** Its question 6 is answered ("maintained
incrementally"). Its questions 1 and 2 are answered there too: 1 is "go with occupied",
and 2 is (b), sticky per session, with hysteresis as the easing (its question 22). *As
first written*, this item said they "stay open there", which had gone stale.
- **Widths.** A width depends on formatted text, which is display-only (gotcha 8). Under
the user's answer to ADR 008's question 2, widths are taken over the rows realized and
held by the view layer. The user's question whether DBSP can maintain a maximum width is
answered in clause 13.
### 10. ADR 003's live sources are delta streams, when they can be
@@ -963,10 +1176,336 @@ read-only live source also sidesteps ADR 001's record tier, as ADR 003 already n
(`host.rs:162`, `app.rs:627-629`), and the integrated state is of the same order. So
on the network path a write is not O(delta): it is O(cells) for the snapshot today,
and roughly twice that with the circuit in it. The keyboard path (`apply_effects`
without a `Host`) takes no snapshot and keeps the O(delta) cost.
without a `Host`) takes no snapshot and keeps the O(delta) cost. Under the
store-memory requirement (clause 14), that second clone is also peak memory, which a
rewind would avoid. Question 9 stays open, and is now weighed against clause 14 as
well.
- **ADR 006, replay (improvise-ilvl).** A circuit is a deterministic function of the
delta sequence, so replaying commands reproduces its state.
### 12. Network transparency: one circuit, consumed in process or across a wire
**The requirement is the user's:** "The DBSP machinery needs to be implemented in a
network transparent way, but the details of that go to ADR 009" (chat). **Everything
else in this clause is this ADR's design, and the author's choices are marked as such.
None of it is measured.**
**What crosses a wire, and what does not** (the author's reading). Two boundaries can be
a wire.
- **The output boundary** carries the results of `project` and their changes: a window's
cells, its axis occupancy, its totals, and ADR 008 clause 4's width facts. In the TUI
the consumer is in the same process. In the browser it is the page, across `postMessage`
from the worker or a socket from `improvise-ws-server` (gotcha 45).
- **The input boundary of a live source** (clause 10). A source that reports its changes
delivers a cell delta, possibly from another machine. The circuit cannot tell that
delta from one `DataStore` emits.
Three things do not cross:
- **The write side carries no deltas.** A client changes the model only by `apply` of
model commands (the user, Q43), so traffic from a client to the host stays
`ModelCommand`s with their basis (ADR 001), as today.
- **The circuit's internal streams stay on the host.** The cell delta, the per-record
stage and the group aggregates never cross: aggregation is model-side (the user, Q38),
so no operator runs in a client.
- **No UI state crosses.** The host is told the request and nothing about a cursor, a
scroll or a key (Q32, Q33, Q37).
**What a projection request is to the circuit** (the author's answer to "is the result
keyed by request, by axes, or shared?"). A request has two parts.
- **Its operator shape** (clause 7) selects the circuit's state. The shape is shared by
every request, from any client, that has it.
- **Its window**, the top-left and the dimensions, selects a part of that state's output.
The window is never state.
So maintained results are keyed by **shape**, and the only per-request work is
restricting a shape's output to a window. That is O(visible) per request and needs
nothing kept between requests.
**Positions are cross-product positions.** The user answered ADR 008's question 53 in
chat: "Q53 is cross product: I'm still ambivalent about which side of the wire pruning
should happen on" (relayed on 2026-10-05; on no PR). So a request's top-left is a position
in the **cross product** of its axes, in each category's own item order. That position is
a bigint where it does not fit a machine word (ADR 008 questions 28 and 36: "in typed
contexts, we can represent bigints as strings in JSON"). A cross-product position names
the same tuple whatever is occupied, so **an occupancy change never moves a request's
window**. It changes only which positions inside the window hold data. **Which side of
the wire prunes is open** (the user is "still ambivalent"), and is question 19. The
circuit must serve both sides, and what it must expose in each case is the author's
analysis:
- **Pruning on the model side.** `project` returns occupied rows and columns. The circuit
must then answer two order-statistic queries over a shape's visible occupancy, from a
cross-product position:
- the next k occupied tuples at or after a position, so a pruned window can start where
the request points;
- the rank of a tuple among the occupied ones, and the occupied tuple of a given rank,
so a client can scroll a pruned axis by rows and show "row i of n".
That is ADR 008's ordered index over occupied tuples (its clause 3), the sorted `Vec`
first and the order-statistic B-tree when measured (its question 16), fed by this ADR's
occupancy delta and crossing count. Under this case a client sees only occupied rows,
and its scroll space is the pruned one, built on top of the cross-product offset.
- **Pruning on the client side.** `project` returns the cross-product window, empty
positions included, together with a **skip structure** the client can query: for each
axis, the occupied runs that intersect the window and the next occupied position past
each edge. The circuit then has to expose the same order-statistic queries, but as a
model fact a client can request, not as a re-indexing the model performs on its behalf.
The cost is that a sparse grid at 3 × 1,000 (10^9 positions, about 10^5 occupied in ADR
008's fixture) sends mostly empty windows unless the client asks for skips first.
Both cases need the same maintained structure. They differ only in who calls it, and in
whether a `project` result is already pruned. Prune would be a request parameter in the
first case (ADR 008's flagged reading 1) and a client policy in the second.
**"As absolute as possible", and what still moves a position.** The user, in chat: "But
the top left should be as absolute as possible" (relayed on 2026-10-05; on no PR). *The
rest of this paragraph is the author's analysis.* A cross-product position does not move
when occupancy changes, pruning included. **It does move when an item is inserted,
removed or reordered** in a category's own order. Positions are mixed-radix numbers over
the items of each level, so inserting item j at level L shifts every later item of L, and
changes the radix that every outer position is multiplied by. No position over a
changing item list is fully absolute. Three things follow:
- **Deltas are keyed by identity, and positions are attached when sent.** An output
element names its row and column by tuple identity: item ids under ADR 007, and names
until then. The host attaches the absolute position from the current item order when it
sends the element. A delta keyed by position alone would go stale with the next item
insert.
- **An item edit is sent as an axis change, not as moved rows.** "Item inserted at index
j of level L", or removed, or moved. A client re-derives every position it holds
arithmetically, at O(levels) per position, and its top-left can be re-anchored to the
same tuple. That is the cursor's identity rule from ADR 008 clause 11 applied to the
window. Resending every row whose position moved would cost O(rows after j).
- **The cost is rank and select over each category's items.** "The position of item x"
and "the item at position i" are ADR 008 clause 10's order-statistic structure (a `Vec`
first, a B-tree when measured, its question 16). A tuple's position is O(levels × log
items). The ordered occupancy index must compare tuples by **current** item position,
not by a position stored at insert time. With item grain (clause 7) its keys are item
identities, so an item insert re-orders nothing in it, and comparison goes through the
order-statistic lookup, at O(levels × log items) per comparison. That is **not
measured**. Question 20.
**A pruned answer may arrive as a stream.** The user, in chat: "After pruning we might
have to stream rows and columns to attempt to fill out the full grid" (relayed on
2026-10-05; on no PR). *The author's analysis:* under model-side pruning, `project` scans
forward from the absolute top-left over each axis's occupancy until it has the requested
number of occupied rows and columns, or reaches the end of the axis. It sends the rows
and columns it finds as it finds them. What the circuit must support:
- **Skipping empty space in O(log n + k).** "The next k occupied tuples at or after a
position" over the ordered occupancy index. It must never walk empty cross-product
space, which at 3 × 1,000 is mostly empty.
- **The absolute position of every row and column sent**, so that a client can place it,
and a later delta can name it, without counting.
- **Bounded work per slice.** The fill is a job under ADR 008 clause 11's runner when k
is large, and it can be resumed from the last absolute position it sent. So the only
state a fill keeps is `(request, fill front)`, which is "as stateless as possible" in
the user's words. A lost fill restarts from the client's last known position.
- **Changes during a fill are identity-keyed deltas, stamped by step** (T3). Suppose a
row becomes occupied between the top-left and the fill front after it was passed. The
subscriber receives it as an ordinary occupancy delta carrying its absolute position,
so its window now holds one row more than asked, and it drops or keeps the extra row by
client policy (Q42). A row that becomes occupied ahead of the front is simply found by
the fill. Each chunk of the fill is a delta to the window's Z-set, stamped like any
other. So a fill and a live update are one stream, which keeps the in-process and the
remote consumer the same (T6).
- **Rows and columns fill independently** (the author's, on ADR 008's question 59(c)).
This ADR's occupancy is kept **per axis**: a count per row tuple and a count per
column tuple (clause 9). That matches today's `prune_empty`, which drops a row only
when it is empty across the whole grid, and prunes the columns against the same
unpruned grid (`layout.rs:752-757` at `50e1314d`). So a row occupied only in a column that falls
outside the window is still an occupied row, and the row fill never waits on the
column fill. A joint rule (a row occupied *within the requested columns*) would need a
two-dimensional count per (row tuple, column tuple). That is O(occupied cells) more
state, and it would change today's pruning, so it is not proposed.
Whether a pruned response is streamed or returned whole, and what a client sees while a
fill is incomplete, is question 21.
**What network transparency requires** (T1 to T6, the author's):
- **T1. Results are plain data.** Every element on an output stream is a value the
protocol crate can serialize, with no `Rc`, no borrowed store key and no closure. Facts
at `50e1314d`: `CellValue` is `Serialize`, with numbers going through `wire_f64`
(`crates/improvise-core/src/model/cell.rs:86-118`). `CellKey` is **not**: it derives only
`Debug, Clone, PartialEq, Eq, Hash` (`cell.rs:10`). So an output element names a cell by
the request's row and column tuples, or by cross-product positions. Where a write may
follow, it also carries ADR 002's surrogate (`CellId`). It never names a cell by
`CellKey`.
- **T2. One identity on both sides of the wire.** A consumer across a wire must
consolidate exactly as an in-process one does. **On NaN they would not.** Clause 3's
delta identity is bit identity, under which two NaNs with different payloads differ. But
the wire spells every NaN `"nan"` (`wire_f64`, `cell.rs:86-114` at `50e1314d`), and the
projection compares values with `same_on_the_wire` (`crates/improvise-protocol/src/render.rs:173-180`
at `50e1314d`; `context/code/protocol.md` gives the reason). So an output delta
`−(c, NaN₁) + (c, NaN₂)` is non-empty in process and consolidates to nothing after a
round trip. **Proposed:** output streams are consolidated under the wire's identity
(every NaN is one value, and every other number is its bits) before they leave the host.
The record tier keeps bit identity, as it does today. Question 15.
- **T3. Every output change is stamped.** A delta carries the step that computed it, as
`(document_generation, step)`. The circuit steps once per model effect (clause 3), and
ADR 001 already puts batches in one total order. A consumer that has applied step n
drops anything at or below n, so a duplicate does no harm, and it treats a gap as a
loss, which a fresh snapshot answers. What is published is the **batch's** consolidated
delta, and only once the batch is accepted. A refused frame is restored (clause 11) and
publishes nothing.
- **T4. A snapshot, then deltas.** A subscription starts with the integrated result at
step n: the window as a Z-set of current values, each with weight +1. Deltas from step
n+1 follow. Because I∘D = id (clause 1), the snapshot plus the deltas equals the current
result. `GridCache::folded(&cache.seed()) == *cache` states the same property for
today's bootstrap compaction (gotcha 32).
- **T5. Order holds across streams, and coalescing happens within a stream.** A slow
consumer's pending deltas on one stream can be **summed** into one delta. Z-set
addition is commutative and associative, so the sum is exact, and the queue is bounded
by the size of the result, not by the number of steps. When the summed delta outgrows
the snapshot, a snapshot replaces it. That is the backpressure, and nothing is dropped
silently. **Across streams, order stays load-bearing**, as gotchas 33 and 43 require:
the occupancy change comes before the cells it places, and the cells come before the
basis that describes them. This does not break gotcha 42's "never coalesce". That rule
is about positional `GridUpdate`s, whose fold depends on order. A delta keyed by tuple
identity, or by a stable cross-product position, is not positional in that sense.
- **T6. The in-process consumer uses the same API.** The TUI calls `project` and receives
the same result and delta types as a remote client. Its transport is a function call.
That is what makes the requirement testable: run one edit sequence through two
consumers, one in process and one behind a serialize and deserialize round trip, and
require their integrated results to be equal under T2's identity. A TUI shortcut that
read operator state directly would be a second read path, which is the derivation-twice
defect (design-principles §5).
Weights on the output are ±1, because a result is a set of current values. A consolidated
weight that does reach past 2^53 follows the user's bigint-as-string rule (Q36).
**Where the wire form lives** (the author's). `zset` stays dependent on `std` alone (clause 1).
The wire types live in `improvise-protocol`, with the one envelope pair (gotcha 42). A
consolidated `ZSet` is converted to a sorted `Vec<(element, weight)>` at that boundary.
The alternative is a `serde` feature on `zset`. The author leans to the conversion,
because what crosses is the projected result, not the crate's own element type.
**How stateless the host can be.** The user: "as stateless as possible". These are the
options of question 13, and ADR 008's question 52 asks the push half of the same thing.
- **(a) Pure fetch.** The host keeps nothing per client. A client calls
`project(request)`. It learns its result may be stale from a change notice addressed to
no one: the accepted batch's step. It then refetches when its own policy says to (Q42).
Each refetch costs O(visible).
- **(b) Soft-state subscriptions** (the author's leaning). A request may ask to be kept
current. For each subscription the host keeps the request and the last step it sent,
and nothing else: no copy of the window, no cursor and no viewport. After each accepted
batch it reads the delta of each subscribed shape, restricts it to the request's window,
and sends it under T3 to T5. **The table is a cache.** If it is lost (a restart, an
eviction, a reconnect), a client resumes by resending its request and the last step it
applied, and the host answers with a snapshot. The host could instead keep a short ring
of past deltas per shape; the author's lean is no ring at first. So the user's "recent
projections + clients" are this table plus clause 7's shape cache, and **both can be
rebuilt from the requests**. A subscription costs O(request) in memory, not O(window).
- **(c) Today's push to a mirror per client.** That is `Sessions`, `ClientSession`, and
`grid_updates` with `Projection` and `GridUpdate` (`crates/improvise-command/src/ui/project.rs:771`,
`:822` and `:981` at `50e1314d`). It keeps each client's `ViewState` and grid on the host
and diffs against them. ADR 008 lists it as obsolete under the split, and this ADR does
not propose it.
A shape is kept while some subscription or recent request names it. Eviction drops its
operators, and the next request rebuilds them as a job (clause 7). How long "recent" lasts
is a cost the user has not set (question 14).
**What a client must hold to resume:** its request, the last step it applied, its
integrated window, and its basis (ADR 001). The host holds nothing that a client needs
back.
**The basis.** ADR 001's basis is published with the result it describes, and after it
(gotcha 43). The host builds it from the request's window (ADR 008, lens row 18), not from
a mirror. `published_basis` (`project.rs:585` at `50e1314d`) already takes the `GridCache`
it describes, not a cursor.
### 13. Incremental max width (the user's question on ADR 008)
The user asked, on ADR 008's question 2: "Maybe dbsp can give us incremental max width?"
**This clause is the author's analysis, and none of it is measured.** The user's decision
on widths stands as ADR 008 records it: (b), sticky per session, eased by hysteresis,
taken over the rows realized and held by the view layer (ADR 008 clause 4).
**DBSP can maintain a maximum, but not as a linear aggregate.** A maximum has no inverse.
When the widest value is retracted, the new maximum cannot be computed from the old
maximum and the retraction. So it belongs to clause 4's **integrated** class, beside `Min`
and `Max`: each group keeps its integrated multiset and reads off the last key. A *top-k*
(keeping only the k widest) holds less state, but it is wrong once the top k have all been
retracted, and then it needs a rescan. So it is not proposed.
**For widths the multiset is small.** The group is one column of a projection shape,
together with its number format, since a width is measured on formatted text. The values
are display widths, which are small integers. So the state is a histogram from width to
count, per column. It is O(distinct widths) per column, whatever the number of rows, and
an update costs O(log d), where d is the number of distinct widths.
**The cost is upstream, in what feeds the histogram.** Its input is the display width of
**every evaluated cell in the column**, under the request's number format. That needs
three things:
- **A delta of evaluated cells over the whole column**, not a window. Clause 4's group
aggregates and clause 5's formula re-evaluation already produce an output delta for
every unfolded cell that moves, with its old value as the retraction. So unfolded rows
need no new state. **Fold rows are not covered.** Under item grain (clause 7) a
collapsed group's value is combined when it is read, not maintained, so its width would
need the fold maintained per column at the visible grain.
- **Formatting every changed cell on the host**, on the write path: `format_f64` and a
display width for each changed cell. Each one is cheap, but it would be the first
display-side quantity the circuit maintains. Gotcha 8 forbids feeding a rounded value
back into evaluation, and a width sink feeds nothing back, so the gotcha holds. But
today the host is asked for widths only over the rows requested (ADR 008 clause 4), and
this would widen that to whole columns.
- **A rebuild whenever the format changes.** `number_format` is a parameter of the view
definition, and a rebuild costs O(occupied rows) per column, as a job.
**What it would buy, set against what the user decided.** A maintained whole-column width
is ADR 008's question 2 option (c), "size each column from its whole contents", which the
user did **not** choose. Under (b) a window's widths are an O(visible) computation over
cells the window already evaluates, so maintaining them incrementally adds nothing. So
the real question is whether the user wants (c) after all, now that its state is cheap. It
would remove the jitter at its source and make hysteresis unnecessary, at the upstream
cost above. That is question 12.
### 14. Resident state, and the store-memory requirement
The user, on ADR 008: "We have to be careful here to minimize memory requirements of the
store: offloading inactive regions to disk (maybe mmap or similar),
double-delta/gorilla/lz4 compression, etc." (#308, comment 4171481179). And then, on ADR
008's question 17: "Let's defer this as recommended." (4188271710). So the store's memory
gets its own ADR, once this ADR's operator state has been measured at ADR 008's fixture.
**This clause withdraws the earlier non-goal** ("Persistence, spilling to disk, and
threads", clause 1), and it says what this ADR must not rule out.
**What this ADR keeps resident, beside the store.** This is the author's inventory. The
orders come from the clauses and are not measured.
| state | clause | order |
|---|---|---|
| the integrated base that a rebuild reads | 7 | O(cells), if it is kept as a copy |
| clause 5a's per-record stage | 5a | O(cells of the records an aggregate over an expression reads), as written |
| multisets for `Min`, `Max` and text | 4 | O(cells) of the measures that need them |
| `ExactSum`, per summed group | 8 | 568 bytes per group |
| occupancy, per live shape, at item grain | 7, 9 | O(occupied tuples) per shape |
| the circuit's rollback snapshot | 11 | a second O(state) clone per mutating frame (question 9) |
| the subscription table | 12 | O(request) per subscription |
**Proposed by the author:**
- **The store is the integral, so it is not copied.** The integrated base a rebuild reads
is `DataStore` itself, which is I(cell delta) by definition. Clause 5a's per-record
stage keeps each record's cells only to compute the retraction `I(x)₋₁`. That equals
`I(x) − Δx`: the record's cells in the store after the step, minus the step's delta. So
the stage can read the store and keep no copy of the values. It still needs each cell's
store sequence (clause 5a), one integer per cell, which `DataStore` would keep beside
the value rather than in a second map. This is **not measured**, and the retraction of
the order key has to be checked against it (question 17).
- **Operator state sits behind one interface.** DBSP calls it a trace: look up, read a
range, and apply a delta. The first implementation keeps it in memory, and the
store-memory ADR can page or compress it without changing any operator.
- **Shapes are evicted** (clause 12). So per-shape state is bounded by the shapes in
recent use, not by every saved view.
- **What does not shrink.** The multisets and `ExactSum` are the price of exact `Min`,
`Max` and unanimity, and of an exact maintained `Sum` (clause 8). They are O(cells) and
O(groups), and the store-memory ADR has to count them.
**The frame budget.** ADR 008 clause 11 sets it, and the user accepted the recommendation
(ADR 008 question 13): a normal UI operation fits in 8 ms or becomes a job, run in 2 ms
slices. A circuit step is O(delta) and runs on the hot path. A rebuild is O(cells) and is
a job (clause 7). This ADR has measured neither at ADR 008's fixture yet, and migration
step 4 must.
## Alternatives considered
**Feldera's `dbsp` crate.** What was verified on 2026-09-24:
@@ -1038,6 +1577,12 @@ displayed path until the differential test holds.
accumulator in both backends (recommended), or after ADR 005. A column total is a formula over the pooled fold, so it needs step 5's
re-evaluation. **ADR 008 clause 5 (column totals) depends on this step.**
7. **Live sources as delta streams**, when ADR 003's source trait exists.
8. **The delivery stream** (clause 12), on the answers to questions 13, 15, 19, 20 and
21. It brings the output types in `improvise-protocol`, the step stamp,
snapshot-then-delta, the order-statistic queries over occupancy, and the test with two
consumers (T6). It depends on step 4 for occupancy. It replaces the per-mirror diff
only when ADR 008's view/host split lands. Step 4 also measures, at ADR 008's fixture,
the cost of a step and of a rebuild (clause 14).
**Tickets.** improvise-felo.4 is this ADR. The steps above are not yet filed, which is
question 10. improvise-swre stays independent and is useful under either backend.
@@ -1055,6 +1600,9 @@ longer a prerequisite of step 6.
- ADR 008 gets occupancy, the occupancy delta and column totals without a second
evaluator.
- Live sources that can report changes plug in without touching the evaluator.
- One delivery path serves the TUI in process and every remote client (clause 12), and
the host keeps no per-client UI state: only a subscription table that can be rebuilt
from the requests.
**Bad.**
@@ -1072,25 +1620,33 @@ longer a prerequisite of step 6.
- A mutating frame from a client already clones the workbook for rollback
(`host.rs:162`). Snapshotting the circuit too (question 9) adds a second O(cells)
clone per frame. Incremental evaluation does not make the network path O(delta).
- The operator shape is re-read after every model effect: O(items) per view, a cost the
- The operator shape is re-read after every model effect: O(items) per live shape, a cost the
post-loop `view_shape` diff paid once per batch.
- The browser bundle grows by two small crates.
- Per-shape state lives only as long as requests name it. A shape that comes back after
eviction is rebuilt as a job, and the client waits through its pending state.
- The output identity (every NaN is one value) differs from the record tier's (bit
identity), as T2 explains. Two identities need stating everywhere a delta crosses.
- A position over a changing item list is never fully absolute. An item insert moves
every later position, and is sent as an axis change (clause 12).
## Open questions
Each question has a recommendation. The user's answers will be folded in under "What
the user decided", as ADRs 004 and 007 did.
the user decided", as ADRs 004 and 007 did. **Status on 2026-10-05: the user has answered
none of questions 1 to 11 on #309.** Questions 12 to 21 were added by the 2026-10-05
revision, and none is renumbered.
1. **The crate's name.** `zset`, `dbsp-core`, or a name with the `improvise-` prefix?
1. *[Open.]* **The crate's name.** `zset`, `dbsp-core`, or a name with the `improvise-` prefix?
*Recommendation:* `zset`. It is problem-agnostic, like `gp-tree`, so it should not
carry improvise's name. `dbsp-core` invites confusion with Feldera's `dbsp`.
2. **Where `Incremental` lives.** In a seam crate (`improvise-incremental`, depending on
2. *[Open.]* **Where `Incremental` lives.** In a seam crate (`improvise-incremental`, depending on
`zset` and `improvise-core`), or inside improvise-core?
*Recommendation:* a seam crate. improvise-core then never depends on `zset`, and
`Eager` stays buildable and testable alone. It is the `gp-tree`/`improvise-gp` shape.
3. **Floating point.** What makes a maintained `Sum` and `Avg` safe to display?
3. *[Open.]* **Floating point.** What makes a maintained `Sum` and `Avg` safe to display?
- (a) ADR 005 (exact decimals) as a prerequisite, with the eager fold kept for sums
until then.
- (b) A compensated f64 sum, re-folded on detected cancellation, with a tolerance in
@@ -1117,35 +1673,35 @@ the user decided", as ADRs 004 and 007 did.
that `eval_agg` uses (`types.rs:79`) gives `−0.0`.
- `Avg`'s one rounding must be stated in step 6.
4. **The sixteen-pass bound.** An acyclic chain needing all sixteen passes is reported
4. *[Open.]* **The sixteen-pass bound.** An acyclic chain needing all sixteen passes is reported
`circular` today (core.md §3). Should `Incremental` reproduce that?
*Recommendation:* reproduce it, so the two backends are comparable. If the bound is to
change, change it in both backends at once, as its own decision.
5. **Formula cycles.** Should formulas in a cycle of the formula graph be evaluated by
5. *[Open.]* **Formula cycles.** Should formulas in a cycle of the formula graph be evaluated by
today's bounded fixed point, restricted to their component, rather than by any
operator in the crate?
*Recommendation:* yes. It keeps `Error("circular")` exactly as it is, including the
cells a guarded cycle leaves convergent. The crate needs no recursion operator.
6. **Keep `Eager` permanently?** Once `Incremental` is the default, should `Eager` stay?
6. *[Open.]* **Keep `Eager` permanently?** Once `Incremental` is the default, should `Eager` stay?
*Recommendation:* yes, permanently. It is the differential test's oracle and the
backend for ADR 003's query-only sources. Deleting it would delete the test.
7. **Page selection as a slice.** Should the group key include the page categories, so
7. *[Open. ADR 008's flagged reading 1 makes the page selection a parameter of the request, which is this question's slice seen from the request side.]* **Page selection as a slice.** Should the group key include the page categories, so
switching page reads existing state, rather than rebuilding the view's operators per
page change?
*Recommendation:* the slice. The state stays bounded by the stored cells, and the
held-key path (`[`/`]`) stays O(visible). Clause 7 is written to this
recommendation, and ADR 008 clause 3 says it is "subject to ADR 009 question 7".
8. **Live sources.** Should `Incremental` serve only sources that can emit change deltas,
8. *[Open.]* **Live sources.** Should `Incremental` serve only sources that can emit change deltas,
with query-only sources kept on `Eager` and ADR 003's pushdown?
*Recommendation:* yes, chosen per document. Integrated state is proportional to the
source, which contradicts ADR 003's larger-than-memory goal for exactly the sources
that cannot report their changes anyway.
9. **Rollback.** For ADR 001 clause 4, should the backend's state join the `App`
9. *[Open. Clause 14 now weighs the second clone against the store-memory requirement.]* **Rollback.** For ADR 001 clause 4, should the backend's state join the `App`
snapshot that `Host::mutating_frame` already takes?
*Recommendation:* yes, snapshot, with no later switch to the inverse delta. Applying
`−d` restores integrated values but not the history `delay` and `differentiate`
@@ -1154,13 +1710,13 @@ the user decided", as ADRs 004 and 007 did.
today. A true rewind would need its own design and an observational-equivalence test
over every stateful operator.
10. **Ticket structure.** Should improvise-felo.4 become the epic for clause-order steps
10. *[Open.]* **Ticket structure.** Should improvise-felo.4 become the epic for clause-order steps
1–7, with one child bead per step, and ADR 008's dependent steps linked as blocked by
steps 4, 5 and 6?
*Recommendation:* yes. The user's decision is recorded on felo.4, and the steps are
how it is met.
11. **Ordered aggregates' key** (clause 4, for improvise-felo.7 and improvise-felo.8).
11. *[Open, and affected by question 16. Under item grain a reorder of an axis category's items rebuilds nothing, so the recommendation's premise (key by position and rely on `view_shape`'s rebuild) no longer holds. Keying by item identity and reading the order at read time, which clause 12 already needs (question 20), becomes the natural form. The recommendation is left as written until the user answers both.]* **Ordered aggregates' key** (clause 4, for improvise-felo.7 and improvise-felo.8).
Should `LAST`/`FIRST` key their per-group state by the ordering category's item
position, with that category's item order added to the operator shape so that a
reorder rebuilds them? Or should they key by stable item identity (ADR 007), and
@@ -1174,3 +1730,89 @@ the user decided", as ADRs 004 and 007 did.
no widening at all, and ADR 008's text and cross-references hold unchanged. If the
user wants hidden items ordered too, the `measure_agg` part must also cover their
positions.
12. *[New.]* **Incremental max width** (clause 13; the user's question on ADR 008).
Should the host maintain each column's whole-column maximum display width? That is
ADR 008's question 2 option (c), and its state would now be a small histogram per
column. *Recommendation (the author's):* no, not now. The user decided on (b) with
hysteresis, and under (b) a window's widths cost O(visible) and need no maintenance.
The maintained form also needs fold values maintained per column, and formatting on
the write path. Revisit it if a measured workload shows jitter that hysteresis does
not cure.
13. *[New.]* **Delivery: fetch, soft-state subscriptions, or push to a mirror per
client** (clause 12; ADR 008's question 52 asks the push half). *Recommendation (the
author's):* (b), soft-state subscriptions that can be rebuilt from the request and the
last step, with no copy of a client's window on the host. When its table is lost, (b)
falls back to (a). (c) is obsolete under ADR 008's split. One of the two ADRs should
hold this question, and the other should point to it.
14. *[New.]* **What keys operator state, and how long a shape lives** (clause 7). Is it
keyed per projection shape, cached and evicted (the author's)? Or per saved `.improv`
view only, so that a request whose shape is unsaved (an unsaved prune or hide, ADR
008's question 51) is served without incremental state? *Recommendation:* per shape,
cached, and evicted once no request has named it for a while. The time is an
implementation number. An unsaved shape is then as incremental as a saved one, which
keeps "pruning shouldn't infect every client" from costing an eager path.
15. *[New.]* **The output identity** (clause 12, T2). Should output streams consolidate
under the wire's identity, where every NaN is one value, while the record tier keeps
bit identity? *Recommendation:* yes. Otherwise a remote consumer and an in-process
one disagree when only a NaN's payload changes.
16. *[New.]* **ADR 008's three requests** (ADR 008, "What revision 4 asks of ADR 009"):
occupancy and the per-column aggregates kept at item grain, a crossing count per
step, and rebuilds as steppable jobs. *Recommendation:* adopt all three, as clause 7
is now written. This changes clause 7's list of what rebuilds, removes the rebuild
from gotcha 43's worked case, and undercuts question 11's premise.
17. *[New.]* **Resident state** (clause 14). Should this ADR withdraw "spilling to disk"
as a non-goal, keep no second copy of the store (the store is the integral), and put
operator state behind one interface that the store-memory ADR can page?
*Recommendation:* yes. The measurement at ADR 008's fixture comes first, which is the
order the user's answer to ADR 008's question 17 already sets.
18. *[New.]* **Formulas the per-record map cannot express** (clause 5a). ADR 008's
question 15 sends them to a fallback over the occupied contexts, never to
`Expansion::members`; the user's answer was "the recommendation is fine.". Two parts:
- Does this ADR's `Eager` adopt the same rule, so that `Eager` and `Incremental`
agree and this ADR's differential test needs no carve-out?
- Do the formulas that target a category other than `_Measure` take the same
fallback, so that they do not block ADR 008's retirement of `Expansion::members`?
*Recommendation:* yes to both. `Eager`'s answers then change in exactly ADR 008's
question 1 and question 15 cases, as a stated change of behaviour.
19. *[New. The user is "still ambivalent".]* **Which side of the wire prunes** (clause 12;
the user's chat answer to ADR 008's question 53; ADR 008's question 57 asks the same,
and one ADR should hold it). On the model side, `project`
returns occupied rows and columns, and the circuit answers next-occupied and
rank/select queries for it. On the client side, `project` returns the cross-product
window together with a skip structure, and the client asks for the queries itself.
*Recommendation (the author's):* the model side, with prune as a request parameter.
A sparse grid at 3 × 1,000 then never ships empty space, and a client stays thin.
Either way, the circuit maintains the same structure (clause 12).
20. *[New.]* **Absolute positions under item edits** (clause 12; the user: "the top left
should be as absolute as possible"; ADR 008's question 58 asks the same from the
request side). A cross-product position stays fixed when
occupancy changes, but it moves when an item is inserted, removed or reordered. Should
deltas be keyed by tuple identity, with absolute positions attached when they are
sent, and should an item edit be sent as an axis change, so that clients re-derive
their positions arithmetically? *Recommendation:* yes. It costs rank and select over
each category's items (ADR 008 clause 10), and comparisons in the occupancy index at
O(levels × log items) each, which is not measured. It is compatible with ADR 008's
question 58 recommendation, which makes a tuple of item ids the primary form of the
top-left. Whether hidden items belong to the absolute space is ADR 008's question 58.
Under item grain (clause 7) this ADR serves either answer, because hiding is a remap.
21. *[New.]* **A pruned answer as a stream** (clause 12; the user: "After pruning we
might have to stream rows and columns to attempt to fill out the full grid"). Should
`project` under pruning fill forward from the absolute top-left, as a sliced job that
can be resumed from its fill front, and send each found row and column as a stamped
delta? Should a row that becomes occupied behind the front arrive as an ordinary
occupancy delta, and leave the window one row larger than asked, for the client's
policy to trim? *Recommendation:* yes. It keeps the fill and live updates as one
stream, and the host's state per fill is `(request, fill front)`. ADR 008's question 59
asks the same, and asks in its part (c) whether rows or columns fill first. Clause 12's
answer is that the two fill independently (the author's).