# lazily-rs

Lazy reactive primitives library for Rust.

## Commit & Push

Commit and push completed work at the end of every turn that changed code,
tests, docs, or fixtures — do not leave finished work uncommitted. Run `make
check` first and ensure it is green; stage only the files that belong to the
change (never secrets or private customer names — see the workspace
`runbooks/private-name-hygiene.md`); write a concise commit message in the
repo's existing style; push to the current branch on `origin`. This standing
rule overrides the harness default of "commit only when explicitly asked" for
this repo.

## Architecture

- `src/reactive_graph.rs` — capability traits over the three execution models (`#lzspecedgeindex`): bound-free `ReactiveGraph` (disposal, scopes, batch, degree introspection, associated `Computed`/`Source`/`Effect`/`Scope` types), `Teardown` (scope contract), `SyncReactiveGraph`/`AsyncReactiveGraph` (construction + reads, split by read discipline), and the blanket-implemented `ThreadSafeReactiveGraph` marker. Read discipline and thread-safety are orthogonal axes, hence four traits
- `src/context.rs` — `Context` struct, dependency graph, value-threaded tracking via the fortified `Compute` view (no thread-local tracking stack; `#lzcellkernel`)
- `src/cell.rs` — **the Cell kernel** (`#lzcellkernel`): `Cell` is a *conceptual* word (a value-bearing reactive node), not a type — there is **no `Cell<T, K>` genus struct**. The two kinds of cell are two concrete `Copy` handle structs (each a lightweight `SlotId` into Context): `Source<T, M = KeepLatest>` (source cell — written from outside, folds under policy `M`) and `Computed<T>` (computed cell — computed from upstream). The former `Source<M>` / `Formula` kind markers dissolve — `M` is now `Source`'s own policy param. Reads (`get`, `subscribe`, `dispose`) exist on both handles; writes (`set`, `merge`) exist **only** on `impl<T, M: MergePolicy<T>> Source<T, M>`, so `computed.set(…)` is a *"no method"* compile error (write protection without a trait, design §3). Computed lifecycle (`eager`/`lazy`/`is_eager`/`clear`/`get_rc`) is on `impl<T> Computed<T>`. Replaces the former `SlotHandle`/`CellHandle`/`SignalHandle`/`MergeCellHandle` structs (all deleted) and the vestigial `Reactive<T>`/`Source<T>` traits (deleted). Note `src/slot.rs` and `src/signal.rs` are gone — `Slot` survives only as the *storage* sense (`SlotId`, and the arena node struct `ComputedNode`/variant `Node::Computed` in `context.rs`; the source node is `SourceNode`/`Node::Source`)
- **Unified shared reads (`#lzrsgetarc`).** `Context::get_rc` and tracked `Compute::get_rc` accept either `Source` or `Computed` through `ReadRc`, return the same current value as `get` without requiring `T: Clone`, refresh computed values identically, and register the same dependency edge when tracked. This is the Rust spelling of the `Reactive.readShared_eq_readCell`, `Reactive.trackedSharedRead_eq_trackedRead`, and `Reactive.trackedSharedRead_registers_edge` formal pins.
- `src/keyed_order.rs` — `KeyedOrder<K, H>`, the graph-agnostic bookkeeping core shared by all three map flavors: present set + authoritative key order, `position`, and the atomic-move algebra (`move_to`/`move_before`/`move_after`, LIS-preserving). Closure-free, context-free, and with no interior mutability — each flavor wraps it in its own `RefCell`/`Mutex` and owns its own reactivity. Mutators report `Mutation::{Changed,Unchanged}` / `Move::{Absent,Unchanged,Reordered}` so the shell decides which signal cells to bump; reactivity deliberately stays out of the core, because a core that faked it with polled counters is exactly lazily-zig's non-reactive map
- `src/cell_family.rs` — `ReactiveMap<K, V, H>` unified keyed reactive collection (`#reactivemap`) over the `MapHandle` trait (`Source` input cells / `Computed` derived cells) + `EntryKind`. `SourceMap<K,V> = ReactiveMap<K,V,Source<V>>` (adds cell-only `set` + eager value-minting `entry`/`entry_with`); `ComputedMap<K,V> = ReactiveMap<K,V,Computed<V>>` (lazy `get_or_insert_with` mint-on-access + eager `materialize_all` pre-mint; no `set`). `MapHandle` is **kept** (not collapsed): its two impls are for the distinct handle structs `Source<V, KeepLatest>` and `Computed<V>`, carrying `const KIND: EntryKind` per kind. No eager/lazy mode flag — eager = pre-mint loop, lazy = mint-on-access
- `src/thread_safe_reactive_family.rs` — `ThreadSafeReactiveMap<K, V, H>` (`#reactivemap`, feature `thread-safe`): the `Send + Sync` flavor over `ThreadSafeContext` (`Arc<Mutex<KeyedOrder>>` present-set state; reactive `keys`/`len`/`contains_key` plus `position`/`move_*`/`remove`) so a keyed map can live in a cross-thread owner; `ThreadSafeMapHandle` trait, `ThreadSafeSourceMap`/`ThreadSafeComputedMap` aliases. Materialization confluence proved in lazily-formal
- `src/async_reactive_family.rs` — `AsyncReactiveMap<K, V, H>` (`#reactivemap`, feature `async`): the `AsyncContext` flavor, with the same `KeyedOrder` core, membership/order plane, and `position`/`move_*`/`remove` surface — ordering is not async-coloured; derived slots resolve asynchronously so `observe` returns `Option<V>` (eventual transparency); `AsyncMapHandle` trait, `AsyncSourceMap`/`AsyncComputedMap` aliases
- **Eager construction** (`#lzcellkernel`, formerly `src/signal.rs`) — an eager `Computed`: `ctx.computed(f).eager()` (or the `ctx.signal(f)` convenience, which builds a guarded computed cell via `computed` and makes it eager) attaches a puller `Effect` that keeps it materialized. `.eager()` is idempotent and returns the *same* handle; `.lazy()` reverses it. Eagerness is graph state — an `eager` bit on `ComputedNode` plus the `eager_by: HashMap<SlotId, SlotId>` side table in `context.rs` (cleared on dispose/lazy) — not a distinct type, so the former `Signal`/`SignalHandle` are retired and the `#lzsignaleager` per-write-puller bug is structurally unwritable. `ThreadSafeContext`/`AsyncContext` keep their own signal handles for now
- `src/async_context.rs` — `AsyncContext` async reactive graph (feature-gated behind `async`)
- `src/thread_safe.rs` — `ThreadSafeContext` mutex-backed shared graph (feature-gated behind `thread-safe` since v0.18.0)
- `src/merge.rs` — RelayCell Phase 1 (`#relaycell`): the `MergePolicy` merge algebra (associative `⊕`, `const COMMUTATIVE`/`IDEMPOTENT` flags) and concrete policies (`KeepLatest`/`Sum`/`Max`/`SetUnion`/`RawFifo`, plus `CrdtJoin<C>` wiring existing `CellCrdt` units behind `distributed`). Under the Cell kernel (`#lzcellkernel`) a "merge cell" is just a `Source<T, M>` with `M ≠ KeepLatest`; the identity `Source ≡ Source<T, KeepLatest>` is a default type parameter, not a spec assertion. The former `MergeCellHandle<T,M>` struct and the `Reactive<T>`/`Source<T>: Reactive<T>` read/write traits are **deleted**. Constructors `Context::source::<M>(v)` (was `merge_cell`) / `Context::apply_merge` (the merge write by id, routes through `set_source`).
- `src/relay.rs` — RelayCell Phase 2 (`#relaycell`): the in-proc `RelayCell<T,M>` conflating relay — hot head (`Option<T>` coalesced under `MergePolicy`), reactive `BackpressurePolicy` (cells: `dimension`/`high_water`/`low_water`/`overflow`), `Overflow` (Block/DropNewest/DropOldest/Conflate/Spill), `IngressOutcome`, demand-driven `depth`/`is_full`/`is_empty` Slots, `ingress`/`drain`. Construction validates overflow vs `M::CONFLATES` (Conflate rejected for RawFifo). Converged egress independent of drain schedule (the `relay_converges` invariant).
- `src/spill.rs` — RelayCell Phase 3 (`#relaycell`): `SpillStore<T,M>` paged durable tail (generalizes `DurableOutbox`) — immutable cold `SpillPage`s (coalesced window summaries), bounded `manifest`, egress cursor, ack-before-reclaim; `SpillMode` (CompactOnWrite/AppendCompact); `reconstruct` (spill_lossless) + `replay_unacked` (idempotent crash-replay).
- `src/relay_roles.rs` — RelayCell Phase 5 (`#relaycell`): `Outbox<T,M>`/`Inbox<T,M>` role facades over `RelayCell` — Outbox backpressures the local producer via `is_full` (default Conflate); Inbox meters the remote via a credit budget (`ready`/`credits`/`receive`/`consume`). A link is Outbox→Transport→Inbox.
- `src/service.rs` — embedded-service plane (`#lzservice`): `HealthCell`/`Core` (composed liveness probe → Healthy/Degraded/Unhealthy, worst component dominates), `ReadinessCell`/`Core` (ready iff all conditions), `DiscoveryCell`/`Core` (service→endpoint keyed by owner peer, membership `evict` removes), `ServiceRegistry`/`Core` (durable log + replayable projection). Pure cores split from reactive cells; composes membership/lease/resilience.
- `src/resilience.rs` — fault-tolerance primitives (`#lzresilience`): `CircuitBreakerCell`/`Core` (Closed/Open/HalfOpen over a sliding failure window, gates `CommandTransport`), `RetryPolicyCell`/`Core` (exponential backoff `min(cap, base·2^attempt)`), `BulkheadCell`/`Core` (bounded isolation pool, `permits_in_use`), `TimeoutCell`/`Core` (deadline fast-fail). Pure cores split from reactive cells projecting state/delay/in_use/is_timed_out.
- `src/windowing.rs` — stream windowing (`#lzwindow`): `TumblingCountCore`/`Window` + `TumblingTimeCore`/`Window` (fixed non-overlapping), `SlidingCore`/`Window` (overlapping fold-recompute), `SessionCore`/`Window` (gap-based sessionization). Window aggregation reuses `MergePolicy` (`Sum`/`Max`/`SetUnion`) — the aggregate is the associative fold of window elements. Pure cores split from reactive cells projecting the last emitted aggregate.
- `src/presence.rs` — presence + ephemeral plane (`#lzpresence`): `Ephemeral`/`Durable` plane markers (durable sink statically rejects ephemeral — compile-fail doctest), `EphemeralCell`/`Core` (single value + auto-expiry), `PresenceCell` (per-peer heartbeat, membership `evict` + TTL) and `AwarenessCell` (last-writer-per-peer cursors/selections) over the shared `EphemeralMapCore`. Reactive cells project the live view/map; invalidate only on a live-view change.
- `src/coordination.rs` — distributed coordination (`#lzcoord`): `LeaseCore`/`LeaseCell` (single-writer authority + monotone fencing token), `LeaderCell`/`LeaderRole` (Leader/Follower/Candidate over a lease), `LockCell` (mutex + fencing `validate`), `SemaphoreCore`/`SemaphoreCell` (bounded permits), `BarrierCore`/`BarrierCell` (wait-for-N, `quorum()` = strict majority). Pure cores split from reactive cells projecting holder/role/is_locked/permits/is_open.
- `src/membership.rs` — membership + failure detection (`#lzmemb`): `PhiAccrual` (Akka-style bit-portable phi over a heartbeat inter-arrival window) + `MembershipCore<P>` SWIM state machine (`PeerState` Alive/Suspect/Dead/Left, `join`/`heartbeat`/`leave`/`tick`, `PeerChangeEvent` diff stream) split from the reactive `MembershipCell<P>` projecting the alive `PeerSet` onto a `Cell<BTreeSet<P>>` (invalidates only on set change). Generic peer id; distributed plane plugs in `PeerId`. Below the CRDT plane.
- `src/rateshape.rs` — rate-shaping source operators (`#lzrateshape`): the lifted `RatePolicy`/`WindowPolicy`/`ExpiryPolicy` (moved out of `relay_policy`; re-exported at crate top-level so relay semantics/API are unchanged) plus source-level operators over any cell handle — `DebounceCell`/`Core` (emit latest after quiet period), `ThrottleCell`/`Core` + `ThrottleEdge` (Leading/Trailing, one emit per window), `SampleCell`/`Core` + `SampleMode` (Count every-nth / Time every-boundary), `ProbabilisticSampleCell`/`Core` + `SampleRng`/`Lcg` (tail sampling `draw < rate`, the plan's only new algorithm). Pure cores split from cells projecting emit onto a `Computed<Option<T>>`; emit-only invalidation.
- `src/relay_policy.rs` — RelayCell Phase 6 (`#relaycell`): `PriorityStorage<T>` (max-priority, FIFO within priority) + `KeyedRelay<K,T,M>` (sharded relays by key). `RatePolicy`/`WindowPolicy`/`ExpiryPolicy` moved to `rateshape` (`#lzrateshape`). Logical-clock time for determinism.
- `src/relay_transport.rs` — RelayCell Phase 4 (`#relaycell`): the `Transport<T>` delivery seam (`deliver`/`poll`/`has_pending`) + `InProcTransport` (direct) and `FramedTransport` (MTU-style framing = CrossThread/Ipc/Ws). The merge algebra, not the transport, guarantees convergence (transport_independent).
- `src/queue.rs` — `QueueCell` (SPSC reactive FIFO + MPSC-via-`batch()` usage rule) + `QueueStorage` adapter trait + `VecDequeStorage` default backend (`#lzqueue`). Reader-kind invalidation (head/len/is_empty/is_full/closed); bounded reactive backpressure via `is_full`; closure lifecycle (drain / Closed-distinct-from-Empty / idempotent+terminal).
- `src/work_queue.rs` — `WorkQueueCell` competing-consumer local authority
  (`#lzworkqueue`): FIFO exclusive claims with stable item/fresh delivery ids,
  worker-owned ack/nack, strict visibility-timeout redelivery, bounded attempts,
  DLQ, and independent pending/in-flight/dead-letter reader kinds.
- `src/topic_core.rs` / `src/work_queue_core.rs` — the graph-agnostic cores every
  queue-family flavor shares (`#lazilythreadsafetopiccel`,
  `#lazilythreadsafeworkqueu`), the same split `keyed_order.rs` makes for the map
  family and for the same reason: the cursor/retention/GC algebra and the
  lease/ack/nack/redelivery/DLQ state machine touch no handle and await nothing.
  **Reactivity is deliberately excluded** — invalidation is a graph write, so
  every mutator returns *which readers changed* (a `Vec` of subscriber ids;
  `Transition::changed() -> ReaderChange`) and each flavor clears exactly that set
  on its own graph. `QueueStorage` already played this role for `QueueCell`
- `src/thread_safe_queue.rs` / `src/thread_safe_topic.rs` /
  `src/thread_safe_work_queue.rs` — the `Send + Sync` flavors (feature
  `thread-safe`). `Arc<Mutex<core>>` storage with invalidation run **outside** the
  lock (a reader's compute takes the context lock then the core lock, so an op
  that invalidated while holding the core inverts the order and deadlocks), and
  multi-root invalidation via `batch()` — `ThreadSafeContext` needs no
  `clear_slots` because `finish_batch` hands every collected root to one
  `clear_frontier_locked`
- `src/async_queue.rs` / `src/async_topic.rs` / `src/async_work_queue.rs` — the
  `AsyncContext` flavors (feature `async`). Reader kinds use
  `AsyncContext::computed` (synchronous compute, async graph), so `len()`,
  `read_stream()` and `pending_len()` return plain values rather than an `Option`
  needing a settle step: **nothing in the queue family is async-coloured**, and
  the work queue's clock stays a caller argument so lease expiry is deterministic
  and fixture-replayable. Multi-root invalidation via `AsyncContext::clear_slots`
- `src/ingress_core.rs` — the graph-agnostic admission algebra behind every
  ingress flavor (`#designimplementtransport`), the same split `topic_core.rs`
  makes for the broadcast family. Keyed lifecycle scopes (Opening/Live/Suspended/
  Closed), a normative admission order (lifecycle → generation fence → freshness →
  handoff → dedupe → ordering → backpressure → merge), a bounded reorder buffer, a
  coalescing hot window under `MergePolicy`, and a bounded three-channel receipt
  log with eviction-stable offsets. **Reactivity is deliberately excluded** — every
  mutator returns `IngressChange`, the set of dirtied reader kinds, and each shell
  clears exactly that set on its own graph. Also carries the `IngressTransport`
  seam and `InProcIngress`: the core never touches a transport, so a WebSocket
  frame, an RPC response, and a polled page are the same input once decoded.
  Freshness enters through an explicit `tick(now)`, so staleness transitions are
  deterministic and fixture-replayable
- `src/ingress.rs` / `src/thread_safe_ingress.rs` / `src/async_ingress.rs` — the
  three flavor shells (`#designimplementtransport`). Four reader kinds per scope
  (`value` / `readiness` / `authority` / `retry`) plus three receipt readers and a
  derived `IngressSchedule`; readiness, authority, and retry are **derives, not
  refresh calls**. The thread-safe shell runs invalidation outside the core lock
  and fans out through `batch()`; the async shell uses `AsyncContext::clear_slots`
  and `AsyncContext::computed` (sync compute, async graph) because **admission is
  not async-coloured** — an admission decision is a function of the fence, the
  watermark, the reorder buffer, and the observed clock, so there is nothing to
  await. Spec: `lazily-spec/docs/transport-ingress.md`; formal:
  `lazily-formal/LazilyFormal/Ingress.lean`
- `src/time.rs` — temporal source primitives (`#lztime`): logical-clock-driven `TimelineSource` cores (`TimerCore`/`IntervalCore`/`CronCore`/`DeadlineCore`) split from thin reactive cells (`TimerCell` single-shot / `IntervalCell` periodic / `CronCell` pattern-periodic / `DeadlineCell<T>` value+deadline → `Deadlined`), plus `ManualClock`. Edge-only reactive invalidation; `BytesPayload` cores (`DeadlineCell` is `PyObjectPayload`). Foundation for leases/expiry/windows/presence.
- `src/state_table.rs` — typed **state tables** (`#lazilystatetable`): a total pure
  `StateTable::decide(&Input) -> Decision` over a *finite product state*, wired as a
  `Computed`. The complement of `state_machine.rs`, not a variant of it —
  `StateMachine` **owns** an accepted sequence of state changes, a `StateTable`
  **derives** a decision from the current product of independently observed facts
  and holds nothing, so replaying the same ordered facts into a fresh `Context`
  reproduces the decision. `projected_state_table` builds **two** guarded cells
  (project, then decide) rather than one: unbounded payloads — hashes, document
  text, revisions — are classified into finite enum axes at the projection
  boundary, and payload churn that leaves the product state unchanged stops there
  and never reaches `decide`. Exhaustive `match` proves the function is *total*;
  it does not prove the table is *reviewed*, so `FiniteState` + `table_coverage`
  produce the Cartesian row set and `TableCoverage` asserts over it — every
  declared decision reached, no row deciding a fall-through, and every assertion
  failing on an empty row set instead of sweeping nothing (`#lzvacuousrun`;
  `assert_at_least(0)` is rejected outright). Impossible combinations are meant to
  be unrepresentable — nest the enum so the axes do not exist, or hand-write
  `FiniteState` to filter through a smart constructor. `thread_safe_*`
  counterparts under `thread-safe`. Plan:
  `tasks/software/plan-lazily-state-tables.md`
- `src/stdlib.rs` — Lazily standard-library conveniences layered over portable primitives rather than added to the graph kernel. `stdlib::Timer` binds logical `TimerCore` to Rust's monotone `Instant`; `Timeout<T>` adds caller-driven operation/cancellation polling, strict monotone deadlines, typed latched outcomes, and deterministic clock/wait seams without owning a future, executor, or thread; `RevisionBarrier` combines monotone revisions with derived predicates, barrier-owned cancellation, `Timer` deadlines, disposal, and application-owned keyed effect receipts while closing check-to-sleep lost wakeups.
- `src/transport.rs` — cross-process zero-copy transport (`#lzzcpy`): `BlobBackend` adapter trait + `InProcessBackend` (wraps `ShmBlobArena`) + `ArrowBackend` (Arrow IPC stream bytes) + `ShmBackend` (POSIX `shm_open`+`mmap`, `shm` feature, Linux) + `spill_message`/`resolve_value` policy + `BlobRouter` multi-backend resolver
- `src/crdt_tree.rs` — `CrdtTree` lossless document contract (`#lzcrdttree`): merge, frontier, delta, empty-frontier snapshot, and materialized value; implemented by `TextCrdt`
- `src/outbox.rs` — storage-independent durable outbox (`#lzdurableoutbox`): `OutboxStore` ordered-byte boundary, shared `Outbox<S>` append/ack/prune/replay protocol, in-memory backend, and `durable-sqlite` adapter
- `tests/ingress_family_conformance.rs` — the ingress contract
  (`#designimplementtransport`) replayed against **all three flavors** through one
  `IngressModel` trait, over `lazily-spec/conformance/ingress/*.json`: ordered
  delivery, reorder + both duplicate classes, reorder-window overflow,
  disconnect/replay, `Block` backpressure, build-skew generation handoff (including
  the handoff that *buffers*), freshness horizon and retry backoff. `invalidates`
  is asserted per reader kind in **both** directions via a cache-validity probe, so
  over-invalidation is as visible as under-. Carries a three-row ledger enforced by
  grepping `src/` in both directions, plus a mutation-check record of seven probes
- `tests/temporal_conformance.rs` — temporal sources (`#lztime`) compute fixtures (lazily-spec/conformance/temporal/`*.json`); timer single-shot idempotent fire, interval boundary counting under clock jumps, cron pattern matching, deadline expiry preserving value, edge-only reader invalidation
- `tests/common/mod.rs` — the runtime conformance manifest recorder
  (`#lazilyupgradeconformance`). Rust integration tests are separate crates, so
  the seam is a shared `mod common;` rather than a Go-style package-wide helper;
  `common::spec_read_to_string` wraps every fixture read in `tests/` and appends
  the corpus-relative id to `$LAZILY_CONFORMANCE_MANIFEST` (absolute, truncated
  once by `make check`, appended by every test binary). Unset ⇒ no-op, so a bare
  `cargo test` is unaffected. `scripts/check-conformance-coverage.sh` then fails
  on any canonical fixture the suite did not OPEN — and on a missing manifest,
  which is missing evidence rather than evidence of absence. The static grep it
  replaced could not tell a replayed fixture from a hand-transcribed one.
  The same file carries the **per-scenario replay ledger**
  (`#lzscenariocoverage`), the rung above the manifest: a fixture with four
  scenarios of which a runner replays three is green under the manifest alone —
  `reliable-sync/liveness_orset_lww.json` was exactly that, and the key guards
  cannot see it either, because an unreplayed scenario contributes no unconsumed
  and no unasserted key. `common::scenarios` / `scenario_by_name` /
  `scenario_at` hand back a `ScenarioView` that books on the first read of the
  scenario's PAYLOAD (`#lzscenariobodyskip`) — never at the yield, which cannot
  tell a loop body that ran from one that `continue`d, and never at a by-name
  lookup, which walks past every scenario ahead of its match. `id`, `name`,
  `description` and the rest of `SCENARIO_LABEL_KEYS` stay silent, so a dispatch
  chain that reads the label and matches no arm books nothing; `.value()` books
  and hands the whole scenario to a replay helper, `.peek()` is the escape hatch
  that does not. Id resolution is `id`, else `name` — the order every binding
  shares, recorded into `$LAZILY_CONFORMANCE_SCENARIOS`, and the guard script
  compares that ledger with the scenarios each OPENED fixture carries on disk, in
  both directions. The positional `#<n>` fallback is gone (`#lzspecscenarioids`),
  and as of `#recommendedconformanceco` the canonical spelling is settled at
  `id`: every scenario in the corpus carries a unique snake_case one, lazily-spec's
  `scenario-identity-check` requires it, and `name` is demoted to an optional
  human label. So the `name` leg no longer fires for the canonical corpus, and
  `common::scenario_by_id` matches on the resolved id rather than the prose —
  keying a lookup on `name` is the same defect as keying it on position, because
  35 scenarios named themselves with a sentence a copy-edit could reword. The
  excuse list `KNOWN_UNREPLAYED_SCENARIOS` ("fixture|id|reason") sits beside
  `KNOWN_UNCOVERED` so there is one place to read what this binding does not
  prove; it is currently EMPTY — every scenario of every opened fixture is
  replayed.
  The same file carries **RUNG 0**, the assertion-block BIND ledger
  (`#lznullformblind`), which sits BELOW every guard above it. Each of those is
  scoped to blocks a runner already BOUND to the tracker: the unconsumed-key
  guard fires on a key nothing read, the unasserted-key guard on a key read and
  discarded, the prose ledger on a discharge naming nothing. None of them can
  fire for a block **no runner ever bound**, because there is no tracker — its
  keys are not unread, nothing reads them, and the fixture reports exactly
  nothing. lazily-dart found two such blocks carrying eight silent keys, one of
  them the anti-spoof invariant its fixture exists for. So `spec_read_to_string`
  inventories every `assertions` block at READ time (top-level plus per-frame
  and per-scenario) and `Expect::new` books one as BOUND, the two sides matched
  by the block's **CONTENT digest, never by its `where` label** — runners spell
  those labels inconsistently and a label-keyed ledger would silently miss the
  mismatch rather than report it. `$LAZILY_CONFORMANCE_BLOCKS` carries the
  ledger on the manifest's terms, and `check-conformance-coverage.sh` fails on
  any inventoried block with no bind, carries a `KNOWN_UNBOUND_BLOCKS` excuse
  list (currently EMPTY, reason REQUIRED) so an unbindable block is visible
  every run rather than invisible, and enforces a `MIN_BLOCKS` floor because
  zero declared blocks means zero unbound blocks reported OK over nothing.
  30/30 blocks bound. Validated in four directions: a declared block with no
  bind FAILS naming it, the floor FAILS when raised above the real count, an
  absent ledger FAILS as missing evidence, and the real ledger passes
- `tests/common/expect.rs` — the assertion-key guard
  (`#lzassertunknownkeys`, `#lzconsumednotasserted`), the two rungs below the
  manifest. Rung 2: having OPENED a fixture, did the runner CONSUME the keys it
  asserts? Rung 3: having read a key, did it reach a comparison against the
  fixture's own value? `Expect` wraps one `expected` / `expect` / `assertions`
  block and panics on drop on any of three conditions — a key never read, a key
  read and then discarded, or an excuse the same run made redundant.
  Observational, not declarative: a key becomes **asserted** only by passing
  through `Expect::assert_key` (equality, compared inside the guard),
  `Expect::assert_key_with` (fixture value handed to the caller's own check, for
  tolerances / containment / decode-then-compare), or
  `Expect::assert_key_if_present` (same, for a key that is optional per fixture —
  an absent key carries no obligation, and binding the comparison to presence is
  what stops `if let Some(x) = exp["k"].as_bool()` from consuming a key it never
  compares). `Expect::get` / `Index` still mark a key READ, for a value that
  drives the replay rather than one that is compared, and a read alone is now a
  failure. `Expect::sub` descends into nested assertion blocks (`invalidates`,
  `final_state`) and satisfies the parent key structurally.
  `Expect::excuse_key` (formerly `declared_exception`) marks a key that genuinely
  cannot be compared at this call site, with a required reason; it runs in BOTH
  directions, so excusing a key the same run also asserts fails as a stale
  excuse. Self-tested in `tests/expect_guard.rs`, including one case per
  read-then-discard shape (named skip in a consuming loop, value bound but never
  compared, comparison against a literal).
  Rung 4 is the **prose-key convention** (`#lzprosekeyconvention`), the family's
  answer to a key whose value is an English paragraph. The corpus declares which
  sibling keys those are in `assertions.prose`; a binding MUST NOT decide for
  itself, which is exactly what the nine of us did — four different treatments of
  the same four keys in `blob_backend_discriminator.json` v2, and lazily-rs's was
  `Expect::prose`, a third exempt state that required a reason and then discarded
  it. That method is **deleted**, not given a sibling. A paragraph is now
  DISCHARGED: `exp.prose_key("epoch_disambiguation", &["frame_epoch",
  "blob_epoch"])` names the executable keys that carry its obligation, and
  `expect::verify_prose(fixture)` — armed by a `ProseLedger` guard whose own
  `Drop` fails a run that never verified — checks the naming against what the run
  really asserted. That is the point: "discharged by `frame_epoch`" is a claim
  about the run and can be falsified; "is prose" cannot. Seven failure modes, one
  self-test and one mutation probe each: a declared paragraph asserted, a
  declared paragraph excused, an undeclared key discharged, a discharged set
  differing from `prose`, a discharge naming nothing, a discharge naming a key
  the run never asserted, a discharge naming another paragraph **or `prose`
  itself** — the prose-name set is SEEDED with `prose`, because `prose` never
  self-lists and rule 4's own comparison is what marks it asserted, so without
  the seed a paragraph could be discharged by the declaration that it is a
  paragraph. The DECLARATION and rules 3-4 are block-local; only rule 6/7 name
  matching is **fixture-wide**, because `epoch_disambiguation` is stated in
  `assertions` and discharged by `expect.frame_epoch` / `expect.blob_epoch`,
  asserted long after that block drops. A "run" is ONE TEST: the ledger is
  cleared at each verification, and a claim recorded after one un-verifies it, so
  a discharge in one replay can never be satisfied by an assertion in another.
  `ProseLedger::open` goes FIRST in the test body — Rust drops in reverse
  declaration order, so the teardown net fires after every block check and after
  the explicit `verify_prose`; opening it later inverts that and reports a false
  failure. Two discharges in this corpus are PROXIES and say so at the call site:
  `wire_encoding` is a claim about how the corpus carries its bytes, which no
  assertion a run makes can observe, and `theorem` names a Lean theorem in
  lazily-formal. `note`, `description` and `reason` stay exempt BY NAME wherever
  the block does not declare them prose — the reactive-graph corpus carries ~97
  per-step ones — and the declaration is evaluated on the RAW block FIRST: a
  tracker that subtracts its reserved names before consulting `assertions.prose`
  makes both `frame_roundtrip_*.json` declarations invisible and skips the whole
  convention while still reporting conforming.
  Rung 5 is the **object-valued key-set guard** (`#lzsubblockkeyset`), the same
  defect one level DOWN: an assertion key whose VALUE is a JSON object, compared
  field by field and never by its key set, so a field added upstream is compared
  by nothing while every rung above reports clean — the parent key is read,
  asserted and consumed. Found by the `#lznullformblind` perturbation pass in
  lazily-zig on `arena_blob.json`'s `descriptor`. The fix is in the TRACKER, not
  a per-call-site field count that holds only while every site remembers: three
  entry points satisfy the obligation — `Expect::sub` / `Expect::sub_if_present`
  (descent, the child owns every sub-key, so an unrecognised one is an
  unconsumed key), `Expect::assert_key_set` (the fixture's key set compared in
  BOTH directions against the set the run produced, for a VOCABULARY whose values
  are glosses nothing can compare), and `Expect::assert_key`/`assert_key_at`
  (whole-`Value` equality subsumes key-set equality). `Expect::check` then FAILS
  at drop for any object-valued key consumed through `assert_key_with`,
  `assert_key_if_present` or a bare `get` without one of the three; `excuse_key`
  and `prose_key` stay available and already require a recorded reason. The guard
  is deliberately NOT scoped to top-level `assertions` blocks — it covers every
  block the tracker guards, which is where the defect actually lives: 25 distinct
  block/key shapes across 27 test targets in this suite, `invalidates` most of
  all. Array-valued vocabularies are out of scope (already guarded by set
  difference under `#lzblobbackendstrict`). Both of this binding's top-level
  object-valued keys were covered only BY ACCIDENT before — `descriptor` by a
  `#[serde(deny_unknown_fields)]` attribute on a test-local struct, `outcomes` by
  a hand-rolled set comparison inside a closure the tracker could not see — while
  a key planted inside `queuecell_spsc_push_pop.json`'s `expected.invalidates`
  left the whole suite GREEN. Self-tested in `tests/expect_guard.rs` (opaque
  consumption fails and names the key; descent, key-set and whole-value equality
  each satisfy it; a planted sub-key reddens under each; both set directions;
  array values untouched), and mutation-checked by neutering the descend /
  key-set bookkeeping, which names 25 sites instead of reporting clean
- `tests/state_table_pilot.rs` — the `#lazilystatetable` pilot: Agent Doc's
  retained-document-transition decision as a typed table (Phase 1 — written
  *before* any Agent Doc runtime change, so nothing here reaches into that repo).
  Covers the plan's whole coverage contract: the row set and every decision
  reached, impossible rows unrepresentable (11 constructible of a naive 32), both
  arrival orders for independently propagated facts, replay into a fresh context,
  the effect sink seeing one wake per *distinct* decision, and a successful
  publication settling the table as a fed-back observation rather than an ACK
  gate. Mutation-checked in four directions — collapse the two cells into one,
  drop the decision guard, disable the dominating frontier row, and delete an
  axis variant — each reddens a different test
- `tests/integration.rs` — 13 integration tests
- `tests/spec_compliance.rs` — 68 spec compliance tests
- `tests/conformance.rs` — cross-language IPC fixture round-trip tests (lazily-spec/conformance)
- `tests/codec_roundtrip_conformance.rs` — the frame-codec obligation
  (`#lzmsgpackparity`), over `lazily-spec/conformance/codec/*.json`. protocol.md
  § Frame codecs makes `json` (reference) and `msgpack` (cross-language binary
  default) MUST-level and requires every frame to round-trip through both for
  all three `IpcMessage` variants — a requirement that lived only in prose,
  because all four conformance rungs reason about fixture CONTENT replay and
  content replay never exercises a codec. Each scenario decodes `wire`,
  **re-encodes the decoded message**, decodes again, and asserts against the
  SECOND decode; asserting against the fixture literal would prove nothing. The
  msgpack half also decodes the produced bytes schema-lessly (`rmp_serde` into
  `serde_json::Value`) and pins the external tag plus the SORTED body field
  names, which is the only way to see the named-field rule: a positional encoder
  passes every value assertion and is still non-conforming. Landing this found a
  real defect — `encode_msgpack` used rmp-serde's default `is_human_readable()`
  of `false`, which this crate reads as "positional", so msgpack wrote
  `key: null` where § NodeKey requires it omitted. `IpcMessage::encode_msgpack`/
  `decode_msgpack` now opt in with `.with_human_readable()`. The fixture-level
  block is evaluated AFTER the replay, because `scenario_count` compared against
  `fixture["scenarios"].len()` is the fixture against itself — green over a
  runner that decodes nothing, the exact vacuity `anti_vacuity` exists to name
  (`#lznullformblind`) — and `opaque_node_state_tag` is read off the
  round-tripped node's state rather than written as the literal `"Opaque"`.
  `codec`, `self_describing`, `byte_canonical`, `required_of_binding` and `role`
  stay fixture-versus-literal DELIBERATELY and a self-comparison sweep should
  leave them: they are corpus declarations a binding pins by agreement, not
  facts a run produces — `byte_canonical` states what two conforming bindings
  may do to each other's bytes, which no single run has a comparable value for.
  Three bindings built run-derived versions of these and reverted them.

### Auditing this suite for vacuous assertions (`#lznullformblind`)

Two passes, and the STATIC one is the weaker of the two.

**Corpus perturbation** is the pass that cannot be fooled by how the source
looks: flip every key of every top-level `assertions` block, ONE AT A TIME, in a
SCRATCH COPY of the corpus, and check whether the suite reddens. A key that
stays green when its value changes is not load-bearing however carefully it is
read, typed and referenced — go found three keys gated by their own declared
value, so flipping one to `false` silently retired the rule. Never edit
`lazily-spec` in place: a probe there reddens every other binding concurrently.
Point precompiled test binaries at a scratch sibling instead (`X/lazily-spec`
beside an empty `X/lazily-rs` used only as the process CWD), because every
runner resolves `../lazily-spec/conformance/...` from the crate root. Expected
result: **everything reddens except the keys the corpus declares
`assertions.prose`**, plus `description`/`note`/`reason`/`generator`; anything
else staying green is a finding. Current state: 107 keys, 86 red, 21 green and
all 21 are prose or by-design-uncompared.

**A static detector is guilty until validated.** Three bindings in a row shipped
an audit tool that reported CLEAN over defects it already knew were present.
Run any such pass against `git show HEAD:` / an archive of pre-fix sources FIRST
and confirm it flags the instances you already know about — a detector that
reports zero is indistinguishable from a detector that is broken. The known
traps, all of which have bitten: per-line matching that misses a wrapped
declaration; taint that does not survive rebinding through a loader
(`let (path, fixture) = load(name)`) or through mutation (`set.insert(x)`);
**comment text parsed as identifiers**, so the better-commented a vacuous
assertion is the more certainly it escapes; over-tainting through library calls;
and helper functions laundering taint. State the blind spots rather than
claiming a clean sweep.

Finally, mutation probes need **negative controls that deliberately survive**.
A probe that reddens shows the assertion fires; only reinstating the pre-fix
shape and watching it stay green shows the probe was aimed at the real defect.
  Needs `ipc,ipc-msgpack` (`make test-codec-roundtrip-conformance`); the feature
  is not optional, because an unopened canonical fixture fails
  `conformance-coverage` and a feature flag is not a carve-out
- `tests/blob_backend_discriminator_conformance.rs` — the blob-backend
  discriminator's wire forms (`#lzblobbackendstrict`), over
  `lazily-spec/conformance/codec/blob_backend_discriminator.json` (fixture v2:
  seven forms × two codecs = **14 scenarios**). An OMITTED or NULL `backend`
  MUST decode as `shm` — absence is the forward-compat channel, and the only
  one. A PRESENT value outside `{shm, arrow, in_process}` MUST be rejected
  NAMING the token: every string that does reach `BlobBackendKind::from_str`
  names a backend this build lacks, so normalizing it to `Shm` routes a foreign
  descriptor into the shm arena — the misroute `resolve_wrong_backend` forbids —
  trading a guarantee discharged structurally by routing for one discharged
  probabilistically by a 64-bit checksum. Five of nine bindings normalized, each
  with a written forward-compat rationale. Both halves are checked:
  `error_names_token` separates "refused" from "refused for the stated reason",
  and each accept scenario RE-ENCODES under its own codec and inspects field
  PRESENCE, because rejecting `rdma` and then emitting `backend: "shm"` is a
  conforming decoder with a broken encoder. lazily-rs satisfied the encoder half
  from the field's introduction in v0.25.0 — `skip_serializing_if =
  "BlobBackendKind::is_default"` has been on the field since the day it landed.
  Of v2's four new shapes, three already held (`in_process` since the enum's
  third variant landed; the non-string refusal, which is an ordinary
  `serde_json`/`rmp_serde` type error; and the epoch split, once asserted
  separately). The **null form did not**, and that was a real library defect —
  `#[serde(default)]` supplies the default when the KEY is missing, so a present
  null still reached the enum's `Deserialize` and failed as a type error in both
  codecs. Fixed with a field-level `deserialize_backend_null_as_absent`, which
  branches on `is_human_readable` because a non-self-describing codec writes no
  option tag for this field. Two assertions here are unreachable from a scenario
  count: `backends_decoded` is a SET DIFFERENCE against `assertions.backends`
  (the guard that catches a binding implementing a smaller enum than the clause
  declares), and `frame_epoch`/`blob_epoch` are compared against the Delta and
  the descriptor respectively (v1 carried 9 in both, so one `expect.epoch` could
  not tell the two readings apart). The refusal is carried as the codec's own
  error TYPE, not a string, and the decode runs inside `catch_unwind`, because
  `rejection_is_decode_error` is about the family a caller already guards a
  decode with — a panic refuses the frame past every handler. Nine mutation
  probes, each red on its own assertion. Needs `ipc,ipc-msgpack`
  (`make test-blob-backend-discriminator-conformance`)
- `tests/nodekey_null_leniency_conformance.rs` — `NodeKey` null-leniency on
  decode (`#lzkeynullstrict`), over
  `lazily-spec/conformance/codec/nodekey_null_leniency.json` (two optional-key
  sites × three key forms × two codecs = 12 scenarios). Omit-when-absent binds
  the ENCODER; a decoder MUST read both an omitted `key` and an explicit
  `key: null` as absent, so every scenario also RE-ENCODES the decoded message
  under its own codec and inspects the produced frame for the field, which is
  the half no decode assertion reaches. The runner carries a **raw-wire
  control** (`#lznullformblind`): every key in this fixture's `expect` blocks is
  byte-identical across the `omitted` and `null` families — `decoded_key` is
  null for both by design, because reading an explicit null as absent IS the
  leniency — so post-decode the four `null` scenarios are the four `omitted`
  ones wearing a different id, `#[serde(default)]` collapses them on contact,
  and four scenarios prove nothing while staying invisible to the manifest rung,
  the scenario-replay rung and both assertion-key rungs at once (an unreplayed
  distinction contributes no unconsumed and no unasserted key). Five of nine
  bindings were blind. `wire_key_form` classifies the `key` slot out of the
  `wire_json` TEXT and the `wire_msgpack_hex` BYTES before any decode; because
  that classification runs on the very crates the decode under test runs on and
  so cannot see a defect it shares with them, `raw_key_form` witnesses the same
  slot a second time with NO decoder at all — a text scan for the member name in
  json, and the msgpack `fixstr` field-name header plus the one type-tag byte
  after it (nil = `0xc0`) in msgpack — and the two witnesses are held to each
  other and to the scenario's declared `key_form`, failing closed on an
  unrecognised form rather than defaulting into the lenient branch. The
  fixture-level vocabularies (`codecs`, `fields`, `key_forms`) are asserted as
  SETS against what the replay really dispatched on, and `scenario_count`
  against the scenarios really reached. Proof of the blindness: a corpus whose
  four `null` scenarios carry the `omitted` wires is fully green under the
  pre-control runner and red under this one. Needs `ipc,ipc-msgpack`
- `tests/nodeid_exact_range_conformance.rs` — the `NodeId` exact-representation
  bound (`#lzspecdecoderbound`), over
  `lazily-spec/conformance/codec/nodeid_exact_range.json`. A decoder that cannot
  represent a received identifier exactly MUST reject the frame rather than
  round it; lazily-rs is `u64`-wide, so it asserts the `exact` branch for every
  scenario including `u64::MAX`, and the expectation is compared as a decimal
  STRING because the fixture is itself JSON. `codecs`, `outcomes` and
  `scenario_count` are asserted against what the replay dispatched on
  (`#lznullformblind`). Needs `ipc,ipc-msgpack`
- `tests/collections_family_conformance.rs` — the ordering/independence contract replayed against **all three execution models** (`SourceMap`, `ThreadSafeSourceMap`, `AsyncSourceMap`) via a `MapModel` trait whose only async-coloured method is `settle`. `collections_conformance.rs` covers the single-threaded flavor only, which is how the thread-safe/async ordering gap stayed invisible while `coverage.json` read green
- `tests/collections_conformance.rs` — keyed cell collections compute fixtures (lazily-spec/conformance/collections); value/membership/order independence, atomic move, LIS reconciliation, memoized semantic tree, manufactured text identity, character CRDT convergence
- `tests/materialization_conformance.rs` — `ComputedMap` materialization (`#reactivemap`) compute fixtures (lazily-spec/conformance/materialization/`*.json`); observational transparency eager (pre-mint) vs lazy (`get_or_insert_with`), deferral-not-deallocation present-set monotonicity, entry-kind orthogonal to strategy (input cells always materialized / derived slots deferred under lazy)
- `tests/materialization_threadsafe_conformance.rs` — same materialization fixtures replayed through `ThreadSafeComputedMap` (feature-gated `thread-safe`); proves the `Send + Sync` flavor obeys the shared laws plus materialization confluence (order-independent present set + observed values)
- `tests/materialization_async_conformance.rs` — same materialization fixtures replayed through `AsyncComputedMap` (feature-gated `async`, tokio); present-set laws + eventual transparency (a driven async slot resolves to the canonical value, eager ≡ lazy)
- `tests/relay_examples.rs` — RelayCell Phase 7 (`#relaycell`) example systems as integration tests: §7.2 telemetry pipeline (Sum relay → SpillStore(AppendCompact) → rate-paced batch egress, lossless); §7.4 doc-sync (per-cell KeepLatest KeyedRelay plane converges per cell); §7.1 broadcast (per-subscriber Outbox<KeepLatest> conflation)
- `tests/relay_roles.rs` — RelayCell Phase 5 (`#relaycell`) spike: Outbox state-conflation + Block producer-backpressure; Inbox credit metering; Outbox→Inbox link convergence
- `tests/relay_policy.rs` — RelayCell Phase 6 (`#relaycell`) spike: RatePolicy token bucket; WindowPolicy flush-on-fill/tick + converged-sum preserved; ExpiryPolicy TTL drop; PriorityStorage ordering; KeyedRelay sharding (regression guard for the `#lzrateshape` policy lift — passes unmodified)
- `tests/service_conformance.rs` — embedded-service (`#lzservice`) fixtures (lazily-spec/conformance/service/`*.json`): health aggregation, readiness gating, discovery register/evict, durable registry replay; reader invalidation
- `tests/resilience_conformance.rs` — fault-tolerance (`#lzresilience`) fixtures (lazily-spec/conformance/resilience/`*.json`): circuit-breaker trip/probe/close, retry exponential saturation, bulkhead bounds, timeout deadline edge; reader invalidation
- `tests/windowing_conformance.rs` — stream windowing (`#lzwindow`) fixtures (lazily-spec/conformance/windowing/`*.json`): tumbling count/time, sliding, session windows with Sum aggregate; emit-only invalidation
- `tests/presence_conformance.rs` — presence/ephemeral (`#lzpresence`) fixtures (lazily-spec/conformance/presence/`*.json`): presence heartbeat/evict/TTL, awareness last-writer, ephemeral value expiry; live-view invalidation
- `tests/coordination_conformance.rs` — coordination (`#lzcoord`) fixtures (lazily-spec/conformance/coordination/`*.json`): lease grant/renew/expire + fence monotonicity, leader handover, lock fencing validate, semaphore bounds, quorum majority gate; reader invalidation
- `tests/reactive_graph_conformance.rs` — reactive-graph disposal/teardown (`#lzspecedgeindex`) fixtures (lazily-spec/conformance/reactive-graph/`*.json`), replayed against **all three execution models** (`Context`, `ThreadSafeContext`, `AsyncContext`) via the `GraphModel` trait in `tests/reactive_graph/`: edge detach in both directions, read-after-dispose, churn returns to baseline, recycled-id cleanliness, scope teardown vs the fold of individual disposals, `disarm()`, cross-scope teardown hazard. First executor of that corpus in the family; carries a per-model `KNOWN_DIVERGENCES` ledger asserted in both directions
- `tests/membership_conformance.rs` — membership (`#lzmemb`) lifecycle fixture (lazily-spec/conformance/membership/`*.json`): SWIM join→Alive/heartbeat/leave, phi gap→Suspect→Dead timeout, PeerSet invalidation only on set change
- `tests/rateshape_conformance.rs` — rate-shaping operators (`#lzrateshape`) compute fixtures (lazily-spec/conformance/rateshape/`*.json`): debounce quiet-period emit, throttle leading/trailing, count/time sampling, probabilistic draw<rate; emit-only reader invalidation
- `tests/relay_transport.rs` — RelayCell Phase 4 (`#relaycell`) spike: converged egress independent of transport framing (operational `transport_independent`) across InProc vs Framed at several MTUs, for Sum/Max/KeepLatest; framed transport preserves the op stream
- `tests/relay_spill.rs` — RelayCell Phase 3 (`#relaycell`) spike: `spill_lossless` (reconstruct cold pages + hot = flat fold, both modes); `spill_replay_idempotent` (Max/SetUnion crash-replay converges); CompactOnWrite page bounding; ack-before-reclaim; RelayCell Spill-overflow → SpillStore end-to-end
- `tests/relay_core.rs` — RelayCell Phase 2 (`#relaycell`) spike: converged-egress independent of drain schedule (operational `relay_converges`) across Sum/Max/KeepLatest; Block/DropNewest/DropOldest/Conflate overflow behaviour; reactive `depth`/`is_full`/`is_empty`; construction rejects Conflate for RawFifo
- `tests/merge_conformance.rs` — RelayCell Phase 1 (`#relaycell`) cross-language fixture replay (lazily-spec/conformance/collections/`mergecell_algebra.json`); KeepLatest/Sum/Max per-op converged value + invalidation (idempotent/identity no-op), fixture flags vs policy `const`s
- `tests/merge_laws.rs` — RelayCell Phase 1 (`#relaycell`) property-based law-tests: every `MergePolicy` is associative; commutativity/idempotency asserted per `const` flag (and flag-honesty counterexamples); `Cell ≡ MergeCell<KeepLatest>`, converged-state determinism regardless of op order, idempotent-`⊕` free dedup via the `PartialEq` store-guard, `Reactive`/`Source` supertype uniformity
- `tests/queue_family_conformance.rs` — the queue family's per-flavor gate: all
  eleven canonical fixtures (`queuecell_*` / `topiccell_*` / `workqueue_*`)
  replayed against **all three flavors** through one `TopicModel` /
  `WorkQueueModel` trait, plus a nine-row ledger (3 primitives × 3 flavors)
  enforced by grepping `src/` in both directions so a shipped flavor cannot sit
  unreplayed. Every gate was mutation-checked. Note the topic fixtures were
  previously opened by nothing — `topic_conformance.rs` hand-transcribes the same
  scenarios as Rust asserts, which is coverage that cannot detect drift
- `tests/queue_conformance.rs` — reactive queue (`QueueCell`) compute fixtures (lazily-spec/conformance/collections/`queuecell_*.json`); SPSC total FIFO, popped-head reader-kind independence, MPSC multi-writer inside `batch()`, bounded reactive backpressure (`is_full`), closure lifecycle
- `tests/work_queue_conformance.rs` — canonical `workqueue_*.json` replay:
  exclusive competing delivery, ownership rejection, at-least-once lease
  redelivery, and poison routing to the DLQ
- `tests/seqcrdt_conformance.rs` — move-aware sequence CRDT compute fixture (lazily-spec/conformance/collections/seqcrdt_convergence.json); concurrent-insert/move/value-edit convergence, tombstone commutativity (feature-gated, needs `distributed`)
- `tests/schema_compliance.rs` — lazily-rs serde output validates against lazily-spec JSON Schemas (#lzspecschema)
- `tests/command_conformance.rs` — command/RPC message plane (`command-plane-v1`) fixture replay (lazily-spec/conformance/message-passing); projection reducer + RPC facade terminal-only rule (feature-gated `ipc`)
- `tests/signal.rs` — 19 eager-Signal integration tests (single-threaded + thread-safe)
- `tests/tokio_sync.rs` — 2 Tokio feature-gated sync integration tests (requires `tokio` + `thread-safe`)
- `tests/async_integration.rs` — AsyncContext feature-gated integration tests (incl. eager `signal_async`)

## Key Design Decisions

- **Lazy by default, eager when asked:** Slots mark dirty on invalidation and recompute on access; `ctx.signal()` opts into eager recomputation (guarded computed cell + puller-effect) with no intermediate unset value (`v1 -> v2`)
- **PartialEq guard:** `Cell.set()` only invalidates when value actually changes
- **Guarded computed (`#lzcellkernel`):** every `ctx.computed()` cell is guarded — `T: PartialEq`, and a recompute that yields an equal value keeps downstream caches (no version bump). There is **no unguarded mode**; the former `memo` constructor is retired because `computed` now *is* the guarded form (`slot()` remains the bound-free storage-sense primitive)
- **Dynamic dependencies:** Edges re-discovered on each recomputation (no stale subscriptions)
- **RefCell interior mutability:** Single-threaded by design
- **Explicit thread safety:** `ThreadSafeContext` preserves `Context`'s fast path while adding `Send + Sync` shared graph support

## Commands

```bash
make check           # Run fmt, clippy, build, every Rust feature test, both Lean formal models (lazily-spec + lazily-formal), and benchmark result checks
make test-thread-safe  # ThreadSafeContext + ThreadSafeStateMachine (feature-gated since v0.18.0)
make test-tokio      # Tokio sync integration tests (requires tokio + thread-safe)
make test-async      # AsyncContext integration tests
make test-loom       # Run thread-safe Loom model tests
make test-lean-formal    # Build ../lazily-spec/formal/lean with lake
make test-lazily-formal  # Build ../lazily-formal with lake (full Harel chart + reactive graph + collections/tree/reconciliation/async proofs)
make test-seqcrdt-conformance  # Replay ../lazily-spec/conformance/collections/seqcrdt_convergence.json (needs --features distributed)
make test-queue-conformance   # Replay ../lazily-spec/conformance/collections/queuecell_*.json (needs --features serde)
make benchmark-evidence # Quick gating measurement (~1 min): reduced-sample Criterion over the budgeted groups + instrumentation profile + source fingerprint
make benchmark-evidence-full # Full-fidelity measurement (tens of minutes); backs BENCHMARKS.md wall-clock numbers
make benchmark-check # Enforce instrumentation budgets against that evidence
make benchmark-spread # Re-measure the per-counter spreads every budget ceiling is derived from
make benchmark-update # Run python3 scripts/update-benchmark-results.py to regenerate BENCHMARKS.md
make instrumentation-profile # Run examples/instrumentation_profile.rs with --features instrumentation
```

### Benchmark budgets are never "skipped"

`make check` runs `benchmark-evidence` then `benchmark-check`, so the budgets are
measured against the tree in front of you every time. `benchmark-check` on its
own has no skip path and three outcomes:

| exit | meaning |
| --- | --- |
| 0 | budgets measured against THIS source tree and green |
| 2 | no evidence — nothing was measured, so nothing can be green |
| 3 | evidence is stale — it measured a different source tree |

Staleness is a content hash (`target/lazily-benchmark-evidence.json`) over
`src/`, `benches/`, `macros/src/`, `examples/instrumentation_profile.rs`, and the
manifests — not an mtime, because `git checkout`, `touch`, and restoring a backup
all move mtimes without any relationship to whether the code changed. Regenerate
with `make benchmark-evidence`; deleting the evidence is not a way to turn a red
budget green.

### Budget ceilings are derived from a measured spread, never typed

Not every instrumentation counter is deterministic, and the gate used to assume
they all were (`#lzbenchbudgetheadroom`). It had the tradeoff exactly backwards:
loose where the counter never varies (`dependency_edge <= 1600` for a counter
that is always 64) and tight where the counter is pure scheduling noise
(`set_cell_invalidation <= 16` for a counter measured from 1 to 256). The second
half is why it flaked on a busy machine, and a gate that reddens on noise trains
everyone to waive the next real regression as a flake.

Each counter now carries an `ObservedSpread` from a real sweep, and its ceiling
is derived from that spread:

| Spread | Class | Ceiling |
| --- | --- | --- |
| zero across idle/loaded/2-core runs | `deterministic` | the observed value, EXACTLY |
| at most half the observed maximum | `scheduling_sensitive` | observed max + one full observed range |
| more than half the observed maximum | `scheduling_dominated` | none — recorded, reported, NOT enforced |

22 of the 51 gated counters are deterministic: they count work items, not
interleavings, and every one held the identical constant on 2 cores and on 32.
Those are where the regression signal lives, and they are enforced with no slack.
19 are scheduling-dominated and carry no signal at all; `benchmark-check` prints
how many it did not enforce, so a green run is never mistaken for full coverage.

Refresh the recording with `make benchmark-spread` (`BUDGET_SPREAD_SAMPLES`
controls the sample count). It prints the measured table, the derived ceilings, a
paste-ready `REGRESSION_BUDGETS` block, and any counter that fell outside its
recorded spread. Take the sweep under the conditions the gate runs in — idle,
loaded, and `taskset -c 0,1` — because a sweep on a quiet machine is how a
0.4 percent headroom budget got written in the first place.

## Benchmark Skill

Use `/lazily-benchmark` to check, update, or run A/B regression workflows for `BENCHMARKS.md`. See [runbooks/lazily-benchmark.md](../../runbooks/lazily-benchmark.md) for the full workflow.

## Related Projects

- `lazily-zig` — Zig counterpart with FFI, thread-safe mutex
- `lazily-py` — Python counterpart with context-as-dict model


## Library Context Policy

This library follows the agent-loop library-context policy. Contributors
authoring `AGENTS.md`, `SKILL.md`, or runbooks in this repo must read:

[Library Context Policy](../instruction-files/LIBRARY_CONTEXT_POLICY.md)

before making changes.

<!-- tsift:code-navigation v=0.1.80 -->
## Code Navigation

Run `tsift status` at session start from the owning repo root. If the task or file lives under a git submodule (for example `src/tsift/...`), switch to that submodule root first so the harness loads the narrower local instructions and repo state instead of the superproject root. If status prints a `run:` recommendation for stale or missing tsift state, run `tsift status --fix` before relying on tsift results; when the harness cannot perform write commands, ask the user to run the printed command instead.

Prefer tsift envelopes over raw reads:
- `tsift --envelope search <query>` instead of `grep`/`rg`
- `tsift --envelope source-read <file>` / `tsift --envelope symbol-read <symbol>` instead of `cat`/`head`
- `tsift --envelope explain <symbol>` and `tsift graph <symbol> --callers` / `--callees` for call graphs
- `tsift diff-digest [path]` instead of `git diff`, `git show`, or patch-style `git log`
- `tsift --envelope session-review <path>` / `tsift --envelope context-pack <path>` instead of replaying long session docs, transcripts, or runtime logs
- `tsift --envelope digest-runner --kind test|log --path . --shell-command '<command>'` instead of raw test/build output

Command detail lives in [`runbooks/code-navigation.md`](runbooks/code-navigation.md) — budgets, `tsift workflow search`, `report.scale_guard` handling, the harness rewrite path for `PreToolUse`-less harnesses, and Codex/OpenCode integration. `tsift init` writes and versions that runbook alongside this block, so it is present in every initialized checkout; read it before broad exploration instead of expanding this block. A repository that also ships a current `.claude/skills/tsift/SKILL.md` should use that skill as the deeper source.

For local verification, run `make check` before committing. After local changes, check the latest GitHub Actions CI run with `gh run list --workflow CI --limit 1` and fix any failing tests before calling the work complete.

Only read full source files when tsift results are insufficient.
<!-- /tsift:code-navigation -->
