# `zset`: the reasoning `crates/zset` is ADR 009 step 1 (improvise-tk51, epic improvise-bw4p): the Z-set group, first and alone. Nothing depends on it yet, so it changes no behaviour. The decisions are the ADR's (`context/adr/009-incremental-evaluation.md`, clause 1). This file records how the crate reads them, where the ADR left a choice open, and the evidence discipline. The evidence for each test is in `context/test-evidence/zset.md`. ## The shape | item | what it is | |---|---| | `ZSet` | a map from `K` to a non-zero `i64` weight; an abelian group within `i64` (interpretation 5) | | `+`, `-`, unary `-`, `* i64`, `scale` | the group operations and scalar multiplication | | `new`, `singleton`, `insert`, `FromIterator`, `Extend` | construction; weights of equal keys sum | | `weight`, `is_zero`, `len`, `iter`, `IntoIterator` | reads; iteration is ascending by key | | `map`, `filter`, `distinct` | the three per-element operators | | `integrate`, `differentiate` | the stream operators, over a slice | There are no dependencies: not serde (clause 9 keeps the wire form in `improvise-protocol`), not even the workspace's own crates. The crate is wasm32-clean by construction and is in `nix/ci.nix`'s `wasmCrates`. ## Canonical form A key with weight zero is never stored. `insert` removes a key whose sum reaches zero, and `singleton(k, 0)` is the zero Z-set. Two Z-sets are therefore `==` exactly when every key has the same weight, so the derived `PartialEq`, `Hash` and `Ord` are the group's equality. The clause 9 correction `target - held` relies on it: a correct state gives `==` the zero Z-set, and `is_zero` is `len() == 0`, not a scan. The representation is a `BTreeMap`. Iteration order is the key order, the same in every process and for every insertion order, which clause 3 needs (eager folds in `CellKey` order) and which a `HashMap` would give up (`context/code/core.md`, gotcha 18's argument). ## Interpretations The ADR is one paragraph on the crate, and says more about what it is for than what it is. These are the readings this step takes. 1. **Clause 1 lists a `Circuit` and more operators; step 1 built the pure part, and "The circuit" below built the rest (improvise-bw4p.1).** The ADR's list is `delay`, `integrate`, `differentiate`, `map`, `filter`, `flat_map`, `index_by`, `join_indexed`, `distinct` and `aggregate`, run by a synchronous `Circuit`, plus resumable rebuilds and a trace interface. Step 1 as ticketed is the group and the operators that are functions of a Z-set or of a stream of them: `map`, `filter`, `distinct`, `integrate`, `differentiate`. The rest needs the circuit (a clock, state held between steps), or a second key type (`index_by`, `join_indexed`, `aggregate`), and is left to the steps that use it rather than designed without a caller. `flat_map` is `map` into several images and has no user yet either. 2. **`integrate` and `differentiate` are slice functions, not circuit nodes.** `integrate(&[d_0, d_1, ..])` is the running sums `d_0 + .. + d_i`; `differentiate` is its inverse, `s_0` then `s_i - s_(i-1)`. The state before the first element is zero, as in DBSP. `delay` is not provided: over a slice it is a shift, and a circuit gives it meaning. 3. **`distinct` is not linear, and is not a group homomorphism.** It keeps weight 1 on the keys whose weight is positive and drops every other key, negative included. That is the set-semantics operator of DBSP. The ADR says nothing about negative weights; dropping them is the standard reading, and the one that makes `distinct` idempotent. 4. **Operators are methods on `&self`, group operations are `std::ops` traits.** `K: Clone` is required only where a result copies keys (`filter`, `distinct`, `scale`, the borrowed operators); `map` and the by-value operators need only `Ord`. 5. **Overflow.** Neither clause 1 nor clause 8 states a policy for weights (clause 8 is about `f64` drift, which wrapping integers do not have). The crate currently uses `checked_*` on every weight operation, for the reason given below, and **panics** with `zset: weight overflow in `, in debug and in release (`checked_*` does not depend on `overflow-checks`). The operations are `add` (including `insert`, `Extend`, `FromIterator`), `sub`, `neg` (so `i64::MIN` panics when it has to be negated) and `scale`. Scaling by zero cannot overflow and returns the zero Z-set. A panic, not a `Result`: `Result` does not belong in the majority of the code (`AGENTS.md`), and a weight near `i64::MAX` is a defect in the caller, not an input to recover from. The weights clause 3 produces are small (one per cell). **The group is partial, and that is the stated reading.** The ADR calls a Z-set an abelian group. With `i64` weights and no wrapping, the laws hold wherever no weight, intermediate ones included, leaves `i64`; outside that, an operation panics rather than return a different value. So associativity is order-dependent at the boundary: `(MAX + 1) + (-1)` panics where `MAX + (1 + (-1))` is `MAX`. The result is never wrong, only sometimes refused. Wrapping arithmetic would make the type a true group (the integers mod 2^64), and `(w + x) - x == w` would still hold exactly, so a retraction does cancel its insertion under wrapping. (An earlier version of this paragraph said otherwise; that was wrong.) What wrapping breaks is the intermediate reads. `MAX + 1` stores `i64::MIN`, so `distinct` drops a key whose true weight is positive and `weight()` has the wrong sign. A true weight of 2^64 stores as 0 (by the add path; a `scale` product of 2^64 would store a zero too, which breaks canonical form), so the key vanishes and `is_zero`, `len` and `==` report a set that does not exist. Those are wrong answers returned silently, which is why the crate panics instead. A wider weight (`i128`) moves the boundary and does not remove it. The ADR's "abelian group" wording needs amending whichever policy is chosen: checked-and-panic gives a group only within range, wrapping gives a group whose reads can be wrong, and `i128` is partial too. Which one is a ruling for the user, not for this step; the crate currently panics, and `overflow_depends_on_the_order_of_evaluation_and_never_gives_a_wrong_value` states it. `sub` is checked directly against the held weight, so `a - a` is zero for every `a`, `i64::MIN` included, and only an absent key's `0 - w` can overflow (`w` is `i64::MIN`). 6. **Stable error text.** The panic message names the operation and both operands, so a failure in a long-running host is diagnosable from the log. ## What the tests are `crates/zset/tests/properties.rs` holds one checker per property, parameterised by an `Alg` trait. The property's test runs it on `ZSet`. A companion runs it on `Model(Fault::None)`, a second implementation over a sorted vector, so the checker is shown not to reject a correct implementation that is not `ZSet`. Each rival is `Model(Fault::X)`, run through the same checker with a deterministic seed, and the failure message must carry the property's tag (`[inverse]`, `[map]`, ..). A rival that fails for another reason does not count. The group laws, canonical form, scalar distributivity, the `map` homomorphism, `filter` linearity, `distinct`'s definition, iteration order, the clause 9 correction identity and the `integrate`/`differentiate` inverses are all properties; each runs at the default 256 cases over keys from a range of six, so collisions and cancelling pairs are the common case. Weights come from `-3..=3`, an explicit zero, and `±2^40`, so sums of three cannot wrap. The overflow policy is not a property over the generators (they cannot reach it by design) and is tested by example in `src/lib.rs`, with `should_panic(expected = ..)`. ## The circuit (improvise-bw4p.1) Clause 1's deferred scope, built without ADR 008. The ADR is not edited; this is how the crate reads it. | item | what it is | |---|---| | `Circuit` | creation-order list of boxed nodes; `step()` runs each once | | `Input`, `Stream>`, `Output` | push a delta in; the value on an edge; a sink that sums every step's delta until `take()` | | `delay`, `integrate`, `differentiate`, `plus` | the stream operators as nodes | | `map`, `filter`, `flat_map`, `index_by` | linear, per-element | | `distinct` | incremental, state is the integrated input (`DistinctState`) | | `join_indexed`, `join_indexed_in` | bilinear delta join of two `ZSet<(K, _)>` (`JoinState`) | | `aggregate`, `aggregate_in` | group-by, re-folds only touched keys (`AggregateState`) | | `Trace`, `MemTrace` | operator state behind lookup, range, apply | | `Rebuild`, `Progress` | a snapshot loaded as a resumable job | | `begin`, `commit`, `rollback` | an undo-log transaction over the circuit's state (below) | Decisions, where the ADR left a choice: 1. **Wiring is apart from logic.** `circuit.rs` is the only file with the arena and `Box`. `ops.rs` holds each stateful operator as a plain struct with a pure `step(delta) -> delta`, testable and usable without a circuit. 2. **Scheduling is creation order.** A node is built from streams that already exist, so creation order is a topological order, and the circuit has no cycles: the ADR has no recursion operator, and `integrate` is its own stateful node, not a delayed feedback loop. `delay` is a node with one held Z-set. `Send`, not `Sync`: one thread steps a circuit at a time (clause 1; "Threads" below). 3. **Incremental means the integral of the output is the operator of the integral of the input.** That is the property every operator is tested against, over random delta streams. `distinct` and `aggregate` are not linear, so they keep the integrated input and emit the difference of the operator at the touched keys only. 4. **`index_by` is `map` to `(key, element)`.** A keyed stream is a `ZSet<(K, V)>`; there is no separate indexed collection type, so the join and the aggregate read the same shape. 5. **The join** is `da x b_prev + a_prev x db + da x db`, computed as `da` against the right trace, `da` applied to the left, then `db` against the updated left (which holds the cross term), then `db` applied. Output weights are products and panic on `i64` overflow like every weight (interpretation 5). `f(&K, &A, &B) -> O` builds the output key. 6. **`aggregate`'s fold gets the key and the group's `(value, weight)` pairs** (owned, because a paged trace cannot lend references) and returns `Option`; `None` means the group has no row (an empty group, or a `Min` of nothing). Sum, count, min and max are closures; text unanimity and the ADR's `ExactSum` belong to the caller (clause 4, 8), not this crate. 7. **The trace interface** is `apply(k, v, w)`, `lookup(k)` and `range(lo, hi)`, returning owned vectors. `MemTrace` is a `BTreeMap>`; the `_in` constructors take any `Trace`, which is the seam for paging or compression. An empty or inverted range reads nothing (`BTreeMap::range` would panic). 8. **The resumable step takes a predicate, not an `Instant`, and a load is one logical tick.** `Rebuild::step(circuit, expired)` loads `chunk` keys at a time and stops when `expired()` is true, always running at least one chunk first, so every call progresses. It reports `Done` as soon as the snapshot is exhausted, whatever `expired()` would say. `Instant::now()` panics on wasm32 (`web-time` is a dependency this crate refuses), so the host closes over its own clock. The first chunk is `Circuit::step`; the rest are `step_continue`, which does not advance the clock. A circuit tick that arrives in pieces is exact for the incremental operators because their outputs are deltas. For the temporal ones the pieces are shaped so the outputs still sum to the one-step output: a continued `delay` emits nothing and adds to what it holds, a continued `integrate` emits only its piece. An empty snapshot still takes its one tick; a finished job takes none. Deltas pushed to the same input between calls are absorbed into the next piece. Buffering live deltas during a rebuild (clause 7) is the host's. **`current()` is the whole tick.** A stream keeps the latest piece, which downstream operators read, and the whole value of the tick, which `Circuit::current(&s)` reads: pieces within one tick are absorbed into it, so `current` of an integrate stream after a chunked load is the full integral and a continued `delay` still reads the previous tick's input. `Output` sums pieces as before. **A pending rebuild owns the clock.** `Circuit::step` counts ticks, and a `Rebuild` records the count after its first chunk. If a `Circuit::step` runs before the job is Done (the host stepped between calls, or two jobs interleaved), the next `Rebuild::step` panics with "the rebuild was interrupted"; `Rebuild::interrupted` asks without panicking, so a host can refuse first. `step_continue` calls made by another job are not counted and are not detected. The state a load leaves for the next tick is tested by pushing another delta after Done and stepping twice. 9. **Not built:** `Circuit` does not own a job runner (ADR 008's), and nothing here is serialised (clause 9, `improvise-protocol`). The tests are `crates/zset/tests/circuit.rs`; the rivals and where they failed are in `context/test-evidence/zset.md`. ## Transactions (improvise-bw4p.5) A panic inside `step` (a weight overflow in an operator, say) leaves nodes ahead of the panic updated and the rest not. ADR 009 clause 11 says each command snapshots the circuit for rollback; `Circuit::begin`, `commit` and `rollback` are that, and the ADR is not amended (the clause's wording stands; this is how the crate reads it). 1. **An undo log, not a snapshot.** The ADR budgets memory, and a clone of the circuit's state is the size of the circuit. `begin` instead makes every stateful node start logging, and the log holds what the steps touch: the cost of a transaction is the keys a step touches, not the circuit's size (`a_transaction_costs_the_keys_a_step_touches_and_not_the_size_of_the_circuit` measures it in key clones: 16 extra for `begin; step; commit`, 22 for `rollback`, the same at 10 and at 300 keys of state, with an `integrate` in the pipeline). Outside a transaction nothing is logged and nothing extra is cloned. Two costs are the circuit's size, and are limits, not features: - **A split tick.** A `step_continue` that is the first publish of an edge in a transaction clones that edge's whole value (2009 key clones against 1006 outside one, at N=1000; `a_split_tick_in_a_transaction_costs_the_size_of_the_edge`). - **Memory held until `commit`.** Every edge's previous value is moved into the slot's undo record and kept, so an `integrate -> delay` pipeline holds 4022 live keys during a transaction against 2014 without one (N=1000). An integrate edge is back to three copies of its integral for the length of the transaction. 2. **What is logged, per node.** Each integrated Z-set (`integrate`, `distinct`, a sink's sum) keeps `(key, weight)` for every addition it applied; each trace (the join's two, the aggregate's groups) keeps `(key, value, weight)` for every `apply`, through `Undoable`, a wrapper over the caller's `Trace`; a `Delay` keeps one clone of what it holds on its first eval, which is the previous tick's input; a `Source` keeps a clone of what was pending at `begin`; an edge (`Slot`) keeps the `(piece, whole)` its first publish replaced, moved and not cloned, except for a continued piece, which clones the edge once. `ticks` is restored. 3. **Rollback replays newest first.** Each undo is the exact inverse of an addition that succeeded, so it never leaves `i64`, but only in this order: a key at `MAX` lowered and raised again would reach `MAX + 1` undone oldest first (`rollback_undoes_newest_first_so_no_undo_leaves_i64`). `i64::MIN` has no negation; a trace undoes it as `MAX` then `1` (the weight before was non-negative, so neither leaves `i64`), and a Z-set undoes it by `adjust(.., subtract)`. 4. **An entry is built before its change and logged after it.** An overflow panics before a key changes (`insert` is one checked addition), and the entry's clones are made first, so a panicking `Clone` changes nothing either (`c1_a_clone_panic_anywhere_in_a_step_is_rolled_back` sweeps a panic over every key clone in a step). A step that dies half way has therefore logged exactly the keys it applied. A custom `Trace` must do the same: a panicking `apply` leaves the trace as it was. `MemTrace` does, since it makes every clone before it touches its map (`a_panicking_value_clone_leaves_a_memtrace_unchanged`). 5. **The edge's two copies.** An integrate stream's piece is also its output, so a second copy (the whole-tick value) only exists while a tick is split. `Slot::whole` is `None` otherwise and `Circuit::current` reads the piece. An eval reads its input's piece in the arena by reference rather than cloning it (improvise-bw4p.11). An integrate stream now holds its integral twice (the node's `acc` and the stream's piece), not three times; sharing those two needs shared ownership in the slot and was not done. 6. **What a transaction does not undo.** - A closure's captured state (a `map` function that counts, an FFI callback's side effects). Operators that keep state keep it in the crate's own types for this reason. - A `Rebuild`: it owns an iterator over its snapshot, so a rolled-back chunk leaves the job ahead of the circuit. The clock cannot tell: `rollback` rewinds `ticks`, which can make a dead job's recorded tick current again. So the circuit also counts the rollbacks that undid a step or piece (`undone`, never rewound), and a job records the count when it first runs, before its first chunk can panic. The chunk's `push` is marked as a step too, since it can overflow the pending delta before any step runs (`p1_a_chunk_whose_push_panics_in_a_txn_is_counted`). A later count makes `interrupted` true for good, finished or not (`rollback_rebuild.rs`: begun before the transaction and continued inside, finished inside, a first chunk that panicked, and a dead job revived by a later step). - A `Circuit::take` of an output made inside the transaction: a rollback subtracts the step's output from the sink, which a `take` has emptied. Take after `commit`; `zset-ffi` refuses to take while a transaction is open. 7. **Pushes.** `Circuit::push` is not logged. `rollback` puts the pending delta back to what it was at `begin`, so a push made after `begin` is dropped, and one made before it is pending again. Operators built after `begin` start logging at once and go back to empty; they are not removed. 8. **One transaction at a time, and misuse panics** like an overflow does: `begin` while open, `commit` or `rollback` with none open. `zset-ffi` answers with a code instead. The tests are `tests/rollback.rs` (the property: a step that panics half way, then a rollback, leaves every stream and sink as before and the later ticks equal to a circuit that never ran the step) and `tests/cost.rs`. A step that panics outside a transaction still leaves the circuit unusable: drop it and build another. ## Rebuilds and circuits (improvise-bw4p.12) A `Rebuild` takes its circuit's identity from its `Input`: every handle a circuit makes holds the circuit's token (a process-wide counter since the arena, `Rc::ptr_eq` before), so `Rebuild::belongs_to(&circuit)` is a comparison. `Rebuild::step` on another circuit panics, where it used to report `Done` and land its chunk in that circuit's next tick. An interrupted job (decision 8 above) can never finish: `interrupted` stays true however many steps follow, because a split tick cannot be mended and a rollback that took a chunk is counted forever (decision 6 above). Drop it and start another job from the same snapshot; the host keeps the snapshot. ## Threads (user ruling 2026-10-11; ADR 009 clause 1 and section 9) The graph is network-agnostic and multi-threaded from the start, and **multi-threading is scheduling**: a circuit stays synchronous and single-threaded, and a host moves whole circuits between threads. Shared-nothing: one circuit per runnable unit, joined only by edges (the edge adapter, a later PR, is the one thing that crosses threads). Splitting one circuit's keys across threads (data parallelism) is not done and not precluded: its exchange step would be the same edge. This is a ruling recorded here, not an ADR amendment. Native threads are assumed; wasm threads are out of scope, and nothing in `zset` spawns one. `Send` and `mpsc` compile on wasm32. 1. **`Circuit` is `Send` and not `Sync`, with no lock anywhere in it.** It moves between threads, one at a time (`tests/threads.rs`: a circuit built on one thread, stepped on a new thread each tick, equals a circuit that never moved; a transaction begun on one thread is rolled back on another). Not `Sync` is pinned by `a_circuit_is_not_sync`, an ordinary test using the ambiguous-impl trick: it fails to build (E0283) if `Sync` is ever implemented. 2. **The `Rc` graph is an arena.** What made the circuit `!Send` was the sharing between the circuit and the handles it hands out. A circuit now owns every edge's slot, every input's pending delta and every sink's sum in one `Vec>`; a node holds typed ids, and `eval`, `begin`, `commit` and `rollback` take the arena. A downcast on every access is the price. Measured on an M-series Mac under load, a release build of a 1001-node chain (map, filter, distinct, delay) over 3000 ticks, best of 5, alternating with main: main 127k-131k ticks/s, this 118k-122k, so 7-9% slower per tick (about 1 ns per node per tick). The reviewer saw 10-25% on a more loaded run. Not reduced. 3. **Handles are plain ids, used through the circuit.** `Stream`, `Input` and `Output` are `Copy`, `Send` and `Sync`, and carry their circuit's token. The calls moved onto the circuit: `Circuit::current(&stream)`, `Circuit::push(&input, delta)`, `Circuit::take(&output)`. A handle from another circuit panics ("handle from another circuit"), where before it was quietly wrong. A consequence: a closure can no longer read a stream during a step, since it has no circuit; the old `b1_...` case does not compile, so it is gone. The handle types are invariant in their element (`PhantomData T>`), as main's `Rc>` was, so a stream of `for<'a> fn(&'a u8)` cannot be coerced into a stream of `fn(&'static u8)`, where `current` would panic on a failed downcast. Nine call sites check the token, and `every_call_that_takes_a_handle_refuses_a_foreign_one` has one case for each. 4. **User closures are `Send + 'static`;** keys and values are `Send`. A custom `Trace` handed to `join_indexed_in` or `aggregate_in` must be `Send`. `Piecewise` requires `Send + 'static`. 5. **The sink does not clone.** Its node reads the input edge and writes the sum through `Arena::read_write`, two disjoint entries (the read one is always allocated first). `a_sink_adds_its_input_with_one_clone_per_key` pins it over `Counted` keys. 6. **Handing a circuit to a thread is the host's business** (`std::thread` lives in host crates). Whoever owns the circuit pushes, steps and takes; a handle is a number and needs no thread. ## Edges (user ruling 2026-10-11; ADR 009 section 9) `crates/zset/src/edge.rs`, std only. An edge joins two circuits that share nothing: the producer's `Output` feeds an `EdgeSink`, the consumer's `Input` is fed by an `EdgeSource`, and a `Pipe` carries `Frame`s between them. A channel and a network are the same kind of edge: the channel edge is `Direct` over `ChannelPipe`, a network edge is a codec over a socket pipe. Nothing here spawns a thread; whoever owns each circuit calls `pump` and `poll`. 1. **The clock is a counter the sender increments per committed command.** `Stamp { epoch, seq }`: `epoch` names the sender's incarnation and is passed in, never read from a clock (the host uses a random id, as `Host::with_instance` does). `delivery`'s `(host instance, document_generation, publication)` maps to `(epoch, seq)` with the first two folded into `epoch`; a delta is a notice with its `(from, to]` range. Delivery is not changed. 2. **Only committed deltas cross.** `EdgeSink::committed_from(circuit, &output)` takes the output after `Circuit::commit` and panics inside a transaction (`Circuit::in_transaction`). A rolled back command is never taken, so it consumes no stamp (`a_rolled_back_command_does_not_cross`). An empty command is still a stamp, as in delivery. 3. **Backpressure coalesces exactly.** The sink keeps one waiting delta `(sent, seq]`. A command adds to it (Z-set sum) and moves its `to`; `pump` sends it and, if the pipe says `Full`, leaves it to grow. Nothing is dropped and the range has no gap (`a_full_pipe_coalesces_exactly_and_keeps_the_range`). 4. **Order and gaps.** The receiver applies a delta only if it is in the epoch it joined and `from == watermark`. `to <= watermark` is a duplicate and changes nothing. Anything else (a gap, an overlap, a frame that does not decode) is a *mark*: `stale` is set and a counter of troubles moves. `stale` makes the next `poll` send a `Request`. 5. **A receiver joins an incarnation only by the answer to its latest request.** A `Request` carries a token (the receiver's counter, which `EdgeSource::restart` keeps), and the sender echoes the token of the last request it read in its `Correction { epoch, through: seq, token, target: integral }`. The receiver pushes `target - held` (delivery's rule). This carries over delivery's third fix, "the host that answers a request is live" (`delivery.md` section 4): a frame from an incarnation the receiver has not joined only marks it stale and never moves the watermark, unless it is the answer to the request just sent. A reply to an older request, a duplicate, or a late reply from a dead sender is therefore never a join. A correction from the joined incarnation applies when it is ahead of the watermark, whoever asked. There is no shared log as in delivery, so every answer is a full correction. Two earlier designs were wrong, each found by a property: joining by a first delta with `from == 0` (a restarted sender with a non-empty integral at `seq 0` looks fresh), and a `left` list of incarnations (a late reply from an incarnation never joined sent the receiver back and shut the live sender out: `a_late_reply_from_a_dead_incarnation_changes_nothing`). There is no `left` list now. 6. **What clears `stale`.** Only an answer, and only if it is not behind the watermark (`an_answer_older_than_the_watermark_does_not_clear_stale`) and no mark has been made since its request went out (`trouble_seen_after_a_request_keeps_its_answer_from_clearing_stale`); an answer at the watermark says nothing newer exists (`an_answer_at_the_watermark_says_nothing_newer_and_clears_stale`). 7. **A restart is a new epoch, and an epoch is never reused.** `EdgeSink::restart(epoch, seq, integral)` keeps the pipe and codec. The first frame of an unjoined epoch makes the receiver re-ask once for that epoch, because the request may have died with the sender that read it (`a_new_epoch_frame_re_asks_once_after_a_lost_request`). A sender that reuses an epoch id at a lower seq cannot be told from a reordered old frame, so the receiver ignores it as a duplicate and is stranded until the sender restarts again: the caller must pass a fresh epoch. A receiver that restarts (`EdgeSource::restart`) is stale again. 8. **A lost final message is invisible to the receiver.** `connection_lost()` is the host's timeout: it marks the receiver stale and lets it ask again. The host calls it on its own local monotonic timeout, never by comparing times across machines, and it must be longer than the round trip, or every answer is one request behind. Requests carry the receiver's stamp and this sender does not read it. 9. **The wire form is a hook.** `EdgeCodec` turns a `Frame` into `Wire` and back, with `CodecError` for what does not decode; `Direct` is the identity. `improvise-protocol` is to implement it for its own key types in the lossless text of ADR 005, and `zset` stays free of serde. The tests run every history through a hand-written text codec (`the_same_histories_through_a_text_codec`). 10. **Threads.** `EdgeSink` and `EdgeSource` are `Send` when the pipe and codec are. The real two-thread test steps a producer on one thread and a consumer on another with a capacity-2 channel (`a_circuit_on_each_of_two_threads_equals_one_circuit`). Two properties simulate arbitrary interleavings: `tests/edge.rs` over FIFO pipes that lose frames, and `tests/edge_net.rs` over a network that drops, duplicates, garbles and reorders in-flight frames both ways, restarts either end at any seq and times out (capacity 1 to 4). It checks after every action that the receiver's applied state is the sender's integral at the stamp it claims, and that it never moves to an older incarnation. `poll` and `committed_from` panic inside a transaction: a rollback drops what a poll pushed, and the stamps would not know (`a_poll_inside_a_transaction_is_refused`). `committed` checks overflow before it changes anything, so a caught panic leaves the sink as it was. `pump` clones the waiting delta each attempt against a full pipe; that is left. Native threads are assumed; `mpsc` and `Send` compile on wasm32 and nothing runs there. 11. **Not built.** A sink with a log to answer a watermark with only the moved keys (delivery's `Moved` correction). A pipe that blocks or notifies, because `try_send` and `try_recv` are polling. Key-sharded data parallelism inside a circuit: its exchange step would be this same edge, and nothing here forbids it.