docs(adr): ADR 009 leaves the operator's shape to its open question and points recommended clauses at theirs (improvise-felo.4)

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-08 23:20:39 -07:00
committed by Edward Langley
parent b7add90830
commit e4f915423c
+33 -22
View File
@@ -252,8 +252,8 @@ read-time parameters here. It keeps its one job, bumping `layout_version`. The c
neither reads nor depends on that tier, which is a view fact. Only `document_generation`
and the record tier matter to this ADR.
**Item grain.** Occupancy and per-column aggregates are keyed by the **items** of the
axis categories, never by a position. So collapsing, expanding, hiding, showing,
**Item grain** (recommended; see open question 6). Occupancy and per-column aggregates
are keyed by the **items** of the axis categories, never by a position. So collapsing, expanding, hiding, showing,
reordering and adding an axis item change no operator: the visible axis is a remap.
ADR 008 measured the remap at 2.3 ms for 100k occupied tuples, against 6 to 45 ms for a
rebuild. A collapsed group's value is combined from its members when read. That is exact
@@ -349,7 +349,8 @@ money.
### 9. What ADR 008 gets
- **Occupancy**, per shape key at item grain: a count per row tuple and per column tuple.
- **Occupancy**, per shape key at item grain: a count per row tuple and per column tuple
(kept as a pre-filter and as the unpruned extent; see open question 11).
Formula cells count by clause 5's stem rule. A tuple is occupied when its count is above zero.
A fold row with a formula column but no stored cell under it, and the `Exact` case with
nothing on None, are **not** occupied, on purpose.
@@ -403,7 +404,8 @@ part of the output and is never state. So a request costs O(visible).
**Positions are absolute cross-product positions**, in each category's own item order,
and a bigint where they do not fit 2⁵³ (sent as a JSON string). They do not move when
occupancy changes. They do move when an item is inserted, removed or reordered, so:
occupancy changes. They do move when an item is inserted, removed or reordered, so the
recommended design (open question 10) is:
- output elements are keyed by tuple identity (item ids under ADR 007), with the absolute
position attached when sent;
- an item edit is sent to every client as an **axis change** ("inserted at index j of
@@ -426,11 +428,13 @@ O(|S| log n + k|S|). The index holds O(occupied cells) beside the store, fed fro
cell delta and the stem rule. The query only looks forward, so a sorted `Vec` or a
B-tree with a lower-bound seek suffices, with no rank over occupied tuples. A fill's
whole state is its request and its last absolute position, so a resume is a seek.
**Scrolling up or left is a new request with a new top-left.** The new top-left is found
by galloping backwards in doubling jumps until a probe hits, then narrowing until one new
row is gained. Each probe is a restricted successor query bounded by the old top-left.
That is about 14 to 28 probes at density 10⁻⁴, and no special answer is needed when a
top-left lands on an empty row. Which side of the wire runs the prune and the fill is
**Scrolling up or left is a new request with a new top-left.** How the client finds that
top-left when rows are pruned is the recommendation of open question 9: it gallops
backwards in doubling jumps until a probe hits, then narrows until one new row is gained.
Each probe is a restricted successor query bounded by the old top-left. That is about 14
to 28 probes at density 10⁻⁴, and no special answer is needed when a top-left lands on an
empty row. The alternatives there are a model-side predecessor query, and failing the
fetch and retrying. Which side of the wire runs the prune and the fill is
open question 9.
**The wire properties**, which hold on each hop:
@@ -483,10 +487,10 @@ refetch by every client in a busy batch. Today's per-mirror push (`Sessions`,
extent is the admitted record count, which the client fetches. Whether notices should be
filtered per client is open question 3.
**A connection operator, at the wire boundary.** One component owns both ends of a
connection. Its host half is a sink for the circuit's results and notices and a source of
requests and model commands. Its client half is the mirror image: a source of results to
the client, and a sink for the client's requests. It owns:
**A connection operator, at the wire boundary.** A connection operator sits at the wire
boundary as a sink/source adapter for the circuit. Toward the circuit it is a sink for
results and notices and a source of requests and model commands. Toward the client it is
the mirror image. It owns:
- serialization (T1, T2);
- stamps and the refetch rule (T3);
- coalescing (T5);
@@ -495,12 +499,11 @@ the client, and a sink for the client's requests. It owns:
batches";
- session resumption.
In process, both halves are function calls. That covers the TUI, and the browser's
`Worker`, which already owns one `Host` and one port (`worker.rs:14-15`). The host half
belongs in or above `improvise-incremental`, and the wire types in `improvise-protocol`.
`zset` stays free of serde. The client half is shared by every front end, beside
`improvise-protocol`'s `ClientState`, so that a new front end does not write it again.
Its remaining shape is open question 17.
In process it is function calls, as in the TUI and in the browser's `Worker`, which
already owns one `Host` and one port (`worker.rs:14-15`). The wire types belong in
`improvise-protocol`, and `zset` stays free of serde. Where its halves live (one
component per connection, or a host half shared per host process with a client half in
every front end), and who owns resumption state, are open question 17.
### 13. Column widths for text
@@ -610,7 +613,10 @@ the displayed path until the differential test holds:
re-keys every entry stored under the moved items. *Recommendation:* the order label,
with only the drawn members ordered. Not measured.
3. **Should notices be filtered per client?** Broadcast (clause 12) keeps no per-client state,
3. **Should notices be filtered per client?** Clause 12's notice replaces the pushed
stream. The rejected alternative is to send no notice at all and let each client
refetch on its own schedule: it keeps the host silent, but another client's change
stays invisible until the next refetch. Broadcast (clause 12) keeps no per-client state,
but every client receives every live shape's moved keys. The options:
- **topics:** a client receives only its own shape key's key lists. Headers still go to
everyone, so the refetch rule keeps working. That costs one subscription per
@@ -663,8 +669,13 @@ the displayed path until the differential test holds:
state. On the model, prune is a request parameter, and the model keeps a pruned axis,
with rank and select for "row i of n", and runs the fill as a sliced job resumable
from (request, fill front). At a density of 10⁻⁴, a client without the successor query
would ship mostly empty windows. ADR 008 asks the same question. *Recommendation:* the
client.
would ship mostly empty windows. A second part: when scrolling up or left, how does the
client find a new top-left when rows are pruned? The options are galloping backwards
with successor probes (about 14 to 28 probes at density 10⁻⁴, with no host state), a
model-side predecessor query (one round trip, no new state, but a second query
direction), or failing the fetch and retrying with the nearest occupied row named. ADR
008 asks the same question. *Recommendation:* the client prunes and fills, and it
gallops.
10. **Are deltas keyed by identity, with item edits sent as axis changes?** Keying deltas
by position instead goes stale on every item insert. Identity costs rank and select