diff --git a/.claude/board/AGENT_LOG.md b/.claude/board/AGENT_LOG.md index f64491810..5c8768d0c 100644 --- a/.claude/board/AGENT_LOG.md +++ b/.claude/board/AGENT_LOG.md @@ -1,3 +1,235 @@ +## 2026-08-05 — hot-window design verification panel (workflow: 1 canon sweep + 1 adversarial refuter) + +**Outcome: the panel HARDENED the design and INVERTED its fork choice before +anything was banked** — exactly what a pre-bank panel is for. D-id: D-HWV-1. +Run: workflow `hot-window-design-verify` (2 agents, both structured-output, +read-only, no cargo, no file writes — results consolidated here by the +orchestrator per the one-writer rule). + +- **Canon sweep** (7 findings): 3 CONFLICTS (v2's one-cycle/one-version pin; + seal-property-4's append→version parenthetical; the persist_sink.rs + seal==append==durable wording at ~9 cited sites incl. the + `wal_writes()==1` falsifier and the `E-ACK-IS-THE-KANBAN-TRIGGER-1` pump), + 2 NEEDS-CAVEAT (seal-property-2 arrival durability; zero-copy legality — + legal only under the two H-4 conditions), 2 COMPATIBLE (E-64K-1TO1's seal + boundary is untouched; naming = the MailboxSoA fleet, never "VSA speaks + Lance"). +- **Adversarial refuter** (6 attacks, 4 landed): no side-effect escapes the + RAM+WAL pair in current source (NOT refuted — the die-together property's + precondition holds); the naive die-together claim REFUTED until H-1 + (checkpoint fencing) was added; the K-batch flush had no contract home and + option (i) unsound without a barrier + torn-tail cleanup (→ H-2); + `base_version` unfillable under version multiplexing (→ resolved by + choosing barrier flush, where each cycle mints its real version at + publish); "the type split pre-anticipated (ii)" REFUTED at ≥6 cited 1:1 + binding sites; "temporal.at() already resolves cycle-within-version" + REFUTED — no such coordinate exists, and (ii) would silently coarsen the + no-hindsight guarantee by up to K−1 cycles for a Strict reader. + +Landed from this run: plan `measure-64k-axes-v4.md`, EPIPHANIES +`E-HOT-WINDOW-DECOUPLES-THE-CLOCKS-1`, dated caveats in +`seal-vs-temporal-ordering-information.md` (properties 2+4), v2 cross-note, +STATUS_BOARD row D-HWV-1. Build lane NOT dispatched — gated on operator word. + +## 2026-08-05 — M-arm + O-arm MEASURED: both NEGATIVE (Sonnet build + central Opus gates + adjudication) + +**Outcome: two hypotheses tested, both falsified under this construction — +cheaply, before either shaped the architecture.** Full results: plan +`measure-64k-axes-v3.md` § MEASURED RESULTS. + +**M-arm — Morton does NOT win.** Digest identity MATCHED (valid +comparison), then the pre-registered SUM verdict: reorder 9.4 ms, +downstream savings −25.8 ms (slower), **Δtotal +35.2 ms**. The +ordered-chunk fast path was also slower than the generic path (350.9 vs +339.7 ms) at identical digests. **Open measurement defect flagged, not +buried:** the M-arm's T1 baseline is ~4× A0's over the same row count, so +the fast path must NOT be compared to A0's 78–86 ms until that is +explained; the internal natural-vs-Morton comparison stands. + +**O-arm — DIVERGED.** Primary observable decided before any timing, as +specified: O-A ≠ O-B. **The seal's ordering is load-bearing and cannot be +re-scoped away** under this construction — retiring, for this +construction, the long-running "temporal.rs already provides the ordering" +hypothesis. Kill-condition: CONSTRUCTIBLE (different code path, not a +disguised O-A), with the honest note that the redundancy in question is +semantic rather than code-sharing. + +**Three defects caught at the gate:** the firewall fired on its own +comment (fixed: strip comments before scanning + a positive control so an +inert guard is detectable); both arms' T1 read 18 cycles where the spec +says 16 (scoped to the measured window); and the O-arm's pre-registered +divergence outcome was coded as a panic (both branches now report — a +designed falsification must never crash the run). + +Gates: fmt clean; clippy 0 attributable; release run complete, 183 CSV +rows. The build lane self-caught a borrow/runtime bug pre-handoff. + +## 2026-08-05 — MEASURE-64K-AXES Stage A0 MEASURED (Sonnet build + central Opus gates + adjudication) + +**Outcome: 3 of 4 answers MEASURED, 1 reported NOT REPRODUCIBLE.** Binary +`examples/measure_wal_curve.rs` (~2,230 lines) shipped; 5 release runs; CSV +179 rows/run. Full results: plan `measure-64k-axes-v1.md` § MEASURED RESULTS. + +Findings: **the current ownership IMPLEMENTATION contributes** +13 ms of +shared-phase time per 64k cycle over a dummy owner (operator-corrected +wording 2026-08-05 — never "ownership costs", which would read as an +inherent property rather than one implementation on one workload) (plus ~32 ms of phases only a real owner has); the hot +`MailboxSoA<4>` representation costs **+63 % memory** over the canonical +32 MiB envelope (52.1 MiB measured); the chunked 64×1024 layout is **faster on +every comparable phase** (build −171 ms) with its mislabelling control firing +(65,472/65,536 HELD); concurrency shows ~3.2–3.5× compute overlap on 4 cores +with **byte-identical sequential-vs-parallel digests**. Temporal T1/T2 stable +(78–86 ms / 7.3–8.8 ms over 1,048,576 rows). + +**The WAL knee is NOT claimed** — five runs of one binary moved it between +4 MiB and 32 MiB with 6× cross-run throughput swings. Three methodology +defects were caught and fixed at the gate rather than shipped: MiB/s computed +from an ASSUMED frame size while discarding the real byte count (now measured + +asserted equal across arms); a memory "overhead" differencing two +process-monotonic VmHWM values (retracted; now measured VmRSS delta vs the +exact canonical size); ten WAL scratch files needing 5.8 GiB (ENOSPC → per-config +reclaim). A stability guard with both halves now suppresses any knee whose +p95/median spread exceeds 3×. + +Gates: fmt clean; clippy 0 attributable; 5 release runs. Build lane self-caught +7 bugs pre-handoff including a duplicate `mod` that would have hard-failed. + +## 2026-08-05 — PROBE-IGNITION-64K GREEN: start() at the MAIN MODEL's full population (main-thread build, answering the operator's direct question) + +**The question:** "Did you test the 64k concurrency model working with the +start()?" **The honest answer was NO — now it is HALF-YES, with the half +named.** `tests/probe_ignition_64k.rs` (1/1): **65,536 real 1:1 +`MailboxSoA<4>` owners** — armed by MetaWord write, gate-checked +per owner, cast via `emit_bootstrap_intent` (ONE `StyleStrategy::plan`, +per-owner binding by `rebind_bootstrap`), **sealed in EXACTLY ONE WAL +write**, all 65,536 transitions applied (`Planning→CognitiveWork`, all +Elixir, stream positions strictly monotone), then — after `consume_firing` +— the ENTIRE 64k fleet RESTS at c2 (0 new casts, all 65,536 seen + Held on +a would-be-Flow qualia, wal_writes frozen). + +**Measured wall times (provenance, never asserted):** build 8.4 s; c1 +cast 225 ms; seal+apply 514 ms; c2 rest decision over 64k owners 73 ms; +9.2 s end to end. + +**The half that remains open, stated in the run's own not-claimed block:** +CONCURRENCY. The loop is synchronous — this proves the machinery HOLDS at +the full population and converges at the one deterministic seal boundary; +"parallel" remains gated by D-KIA-A2's pre-registered protocol. + +**One measurement bug self-caught by the run:** the first draft asserted +`writer.casts().len() == 0` at c2 and failed at 65,536 — `casts()` is the +CUMULATIVE board (cycle 1's records survive the payload drain, the exact +G9 drained-writer semantics). Rest is now measured as a delta, with the +positive half added (all 64k seen + Held — a per-owner decision, not an +empty scan). + +Scale was bought on the OWNERS axis only (`MailboxSoA<4>`, one populated +row) per §12.3a‴: rows-per-owner is the benchmark axis, owners is the +model. Gates: test 1/1; fmt clean; clippy 0 attributable warnings. + +## 2026-08-05 — D-IGN-B GREEN: ignition starts the REAL lenses (Opus design + Sonnet inventory + Sonnet build + central gates) + +**D-ids:** D-IGN-B (plan §12.11). **Outcome: GREEN — 1/1 test, gates L0-L7 + +z5-BLOCKED, all both-halved.** Files: `tests/d_ign_b_lenses.rs` (~1,100 +lines); lanes: `exec-runs/d-ign-b-design-opus.md` (+ orchestrator Q6/Q7 +ratification), `d-ign-b-api-inventory-sonnet.md`, `d-ign-b-build.md`. + +**What it proves.** The operator's directive realized: arming an owner with +z ∈ {1..4} over BYTE-IDENTICAL rows selects which of the four shipped stance +readings is recorded — L0: 8 twin owners byte-identical across 48 rows; L1: +Kant vs Wittgenstein digests differ, same-lens digests bit-identical (with +the risk-check first: Hegel/Nietzsche NON-empty on this path — the +constant-false finding was the SPO/TSV path; this path streams raw verse +text); L3: no lens constant-empty (z=1 0/8, z=2 2/8, z=3 1/7, z=4 0/7 +empty); L4 anti-degeneracy: 6-7 distinct digests per lens; L5: 30 Flow + 0 +Block sealed at c1 (derived for THESE cohorts, not recited from the probe); +L6: every readout owner had advanced Planning→CognitiveWork, UNARMED absent +from both sides; L7: OUTSIDE silent by address alone. + +**Honest framing held throughout (operator-ratified Q6):** SELECTION, not +dispatch — stance_panel computes all four in one call and the ordinal picks +the tuple element (printed + not-claimed item 11); the lens reads the +owner's corpus slice by address, never row bytes (the §12.7 defect shape, +named — substrate governs selection end-to-end, and no substrate-data-path +claim may follow from any readout). **z=5 Fusion is BLOCKED and says why at +runtime**: this fleet accrues ≤2 sealed horizons, so Strict-vs-Aware +admission is identical and any delta is 0 BY CONSTRUCTION; `jc` is not a +supervisor dep. Reserved, not faked; R2 is the recorded path if wanted. + +**Build lane self-caught two falsifiability traps pre-handoff:** a digest +variant-discriminant tag that made every cross-lens inequality pass by +construction (removed — digests fold content only, so empty readouts +genuinely collide and L3/L4 can actually fail); an L2 write into the main +readout map that would have corrupted L6's containment premise (scratch +map). Central gates caught one clippy warning (map-keys iteration), fixed. + +**Gates:** test 1/1 ok; clippy 0 warnings attributable; fmt clean. Same CI +caveat as the probe: inert without `--features cycle-driver` (operator- +approved workflow change, still open). + +## 2026-08-05 — PROBE-IGNITION: the write path is DRIVEN (Opus design + Sonnet inventory + Sonnet build + central gates) + +**D-ids:** PROBE-IGNITION (plan §12.11's prerequisite). **Commit:** this one. +**Outcome: GREEN — 2/2 tests, all 11 gates (G1-G11) both halves.** + +Three lanes: Opus design note (`exec-runs/probe-ignition-design-opus.md`, 528 +lines — placement, the no-messaging lowering, the pinned run shape, the +assertion table, the silence-honesty split); Sonnet API inventory +(`probe-ignition-api-inventory-sonnet.md`); Sonnet build +(`probe-ignition-build.md`). Orchestrator ran every cargo command. + +**What it proves.** The built-but-undriven write path now has a driver: 64 +real `MailboxSoA` owners seeded from the real KJV corpus, armed by a +`MetaWord` write, discovered by a board scan alone, cast write-on-behalf +through `emit_bootstrap_intent` -> `BatchWriter::cast` -> `run_cycle` +(collect -> seal -> persist -> apply). Measured: c1 24 casts / 1 WAL write / +24 transitions, decomposing 20 Flow (`Planning->CognitiveWork`, Elixir = +the style's mint) + 4 Block (`Planning->Prune`, Native = the gate's mint); +40 untouched owners fully accounted (32 out-of-scope, 7 unarmed, 1 orphan); +c5 and c6 rest with ZERO casts, no seal, `wal_writes` frozen at 4 and the +fleet byte-identical across the two rest cycles. + +**The honest silence held.** G4's rest fires on the shipped suite's own +*Flow* fixture at flow_proxy=7 with `Calibrated` texture — the owner rests +because `mantissa` (derived from live energy) fell to 0, not because the +qualia were zeroed. CONTRA's absorbing Prune silence is distinguishable from +REST's rescheduled silence: `rediscovered(REST)=8` at c2/c3 vs +`rediscovered(CONTRA)=0` across c2..c6. + +**Two OPEN #879 caveats made observable, as designed.** G9 pins the +drained-writer retry footgun (injected WAL failure -> `CycleError::Seal` -> +retry via `seal_cycle(sink, failure.frame, failure.casts)` lands it; a fresh +`collect_casts` on the same writer yields 0 slots). G10 measures the +missing-owner accounting gap: probe-local pass counts 1, shipped pass counts +0, difference exactly 1. + +**Corrections during the arc.** (a) Mid-flight G2b spec fix relayed to the +build lane: the 4 CONTRA Planning casts are gate-minted Native->Prune per +the design's own §2 step 8, so "every Planning move is Elixir" was wrong — +rebuilt as the 20/4 decomposition. (b) Central-gate catch: G11's self-scan +matched its OWN success message (the needles were concatenation-guarded but +the eprintln spelled them out) — a real false positive the run surfaced; +message reworded, scan re-armed. (c) The build lane self-caught four bugs +before handoff (hardcoded `DatasetVersion(0)` base, a tautological +self-comparison, a fingerprint captured after the loop, an `Option<&T>` +mismatch). + +**Deviations, both documented in-file:** energizing writes `owner.energy` +directly (`causal_edge::CausalEdge64` is unreachable without a forbidden +manifest change; same public field `apply_edges` mutates); the c4 wake runs +before the scan (the design's own cohort table is only consistent that way). + +**Gates:** `cargo test -p lance-graph-supervisor --features cycle-driver +--test probe_ignition` 2/2 ok; `cargo fmt -p lance-graph-supervisor --check` +clean; clippy 0 warnings attributable to the probe. **CI note:** the probe +is inert unless the workflow adds `--features cycle-driver` — NOT changed +here (workflow edits are operator-approved only); recorded as the open item. + +**Not claimed:** durability, parallelism, scale, multi-writer, validity, +GUID-prefix routing, 36-style arming, deinterlace/temporal, zero-copy, +recovery, or `Evaluation->Plan` re-entry (structurally unreachable through +the MUL gate — the arc stops at Commit and says so). + ## 2026-08-04 — D-BLW-3 arc: design + inventory + recon/refute workflow + build (consolidated by the orchestrator) Four units, records in their own tag-files per the one-writer rule: diff --git a/.claude/board/EPIPHANIES.md b/.claude/board/EPIPHANIES.md index a51ad97b9..370959e82 100644 --- a/.claude/board/EPIPHANIES.md +++ b/.claude/board/EPIPHANIES.md @@ -1,3 +1,264 @@ +## E-ACK-THEATER-DELETED-1 (2026-08-05, operator-directed; SHIPPED same-day) + +**The ack/pump/tick theater is deleted from source, and the zombie question is +answered with evidence.** The operator asked whether `kanban_actor.rs` was +"the living zombie bringing the topic on the table." Verdict: **half yes** — +honestly labelled legacy since 2026-08-04, but kept breathing by (a) `lib.rs` +re-exporting the whole message surface at crate top level, (b) ONE live +library consumer (`onebrc-probe/lane_e.rs`, actor-spawn per batch + +`KanbanMsg::Tick` RPCs — and its `Tick` arm IS "a version tick as permission +to advance," the exact retired mechanic), and (c) the W2b probe + its own +tests. The other half of the resurrection pressure was documentational: +`E-ACK-IS-THE-KANBAN-TRIGGER-1`'s 2026-07-10 LEAVE-AS-IS disposition let a +design panel cite the ack pump as live mechanics one month later. Notably +**`ack_and_propose` was already gone from source** — the "ack" half of the +theater survived only in the record. + +**Deleted:** `KanbanMsg`, `KanbanActor`, `KanbanRouteError`, all five RPC +drivers + `run_to_absorbing`, every `ractor::call!` in the module, the actor +tests. **Added — the visibility surface the operator asked for:** +`PhaseCensus` in the same module (supervisor) — a message-free `&self` census +over any `MailboxSoaView` iterator (`observe`/`record`/`count`/`total`/ +`absorbing`/`at_rest`, absorbing derived from `next_phases().is_empty()` +never hardcoded; empty census is NOT at rest — observing nothing asserts +nothing). Kept: `mul_target` (pure, cycle_driver's P4c gate) + +`parse_kanban_step` (the `"kanban.*"` step vocabulary). Migrations: lane E +journals over the direct `&mut` owner (supervisor+ractor dropped from its +feature — lane D deliberately KEEPS its own actors: pricing the actor model +is that lane's purpose); W2b pins the real `MailboxSoA` Rubicon DAG via +`try_advance_phase` directly and exercises the census over real SoA. + +**The OGAR boundary check (operator-asked, verified):** zero OGAR consumers +of any deleted symbol. OGAR's `ogar-action-handler` is the arago/HIRO +ActionHandler PARITY runtime (`submitAction → ActionInvocation → +sendActionResult`, `Receipt::Acknowledged`, RBAC `commit_via` upstream) — an +application wire protocol at the membrane, standing on `ActionDef`/ +`KausalSpec`, exactly the one legitimate home for ack/SLA vocabulary. The +vocabulary firewall holds by construction: nothing there touches substrate +progression. + +**What is NOT theater and stays:** the kanbanstep — +`VersionScheduler::on_version → try_advance_phase(&mut)` (reference +`symbiont::kanban_loop`) — is the writer's own synchronous continuation: +no wait, no message, pure-function propose + owner dispose. Canonical per +the 2026-07-10 ruling and untouched. Open NAMING question only: "scheduler" +in those type names is a drift vector under the no-pump vocabulary rule. + +Gates: supervisor clippy `--no-deps -D warnings` clean + 9 lib tests (4 +census, mul_target, parser) + W2b 3/3 + cycle-driver 4/4 green; onebrc +`--features lane-e` 20/20 + clippy clean; fmt clean. Pre-existing, +unattributable reds noted honestly: `lance-graph-ontology` (12 lints, oxrdf +deprecations + doc-indent, untouched crate), `cognitive-shader-driver` +`bindspace.rs:475` too-many-arguments (untouched file), callcenter 1 unused +import — none in the touched surface. + +## E-PROGRESSION-IS-EXISTENCE-NOT-COMMAND-1 (2026-08-05, OPERATOR-RULED) + +**The substrate progresses because immutable versions exist — never because +anything tells it to progress. There is no architectural pump, no +acknowledgement-driven progression, no scheduler advancing cognition.** The +authoritative execution path, stated so it cannot blur: + +``` +think → seal → publish Lance version → next cycle reads the published version +``` + +A published version becoming queryable IS the progression. Nothing signals +it, acknowledges it, or schedules it. + +**What this corrects, by name:** + +- **E-HOT-WINDOW-DECOUPLES-THE-CLOCKS-1 (below, same day)** — its H-5 clause + "the ack rebases onto the publish ack" is RETRACTED; v4 §H-5 is rewritten. + Rebasing a pump is still a pump. The hot-window design needs no trigger + rewiring at all: cycle n+1 reads published cycle n the moment it exists. +- **The ack/pump vocabulary family** (`E-ACK-IS-THE-KANBAN-TRIGGER-1` and + its siblings) — the 2026-07-10 chain already corrected the trigger RANKING + (`E-KANBANSTEP-IS-THE-TRIGGER-1`'s correction: the ack-gated advance is "a + wait-shaped scheduler by construction"; PRIME INVARIANT: nobody waits for + anything or any scheduling). This ruling completes it: wherever "ack" / + "pump" still appear in the record, they are **legacy consumer terminology + on the historical compatibility surface — never substrate mechanics.** The + only legitimate home for ack/SLA/retry/notification concepts is an + external consumer (ticket-processing-style workflows): an application + concern, not a cognition concern. + +**Consequence for the hot version window (and any future persistence +design):** the window is **not a message queue awaiting acknowledgement**; it +is a **resident horizon of immutable Lance versions**. Readers observe +versions. Writers publish versions. Persistence catches up independently: +`seal → publish → immediately queryable → durability may trail`. Drift +signal: any new design sentence in which the substrate progresses because +something *told* it to — an ack arrived, a pump fired, a scheduler ticked — +is re-deriving the deleted mechanic and gets judged against this entry first. + +## E-HOT-WINDOW-DECOUPLES-THE-CLOCKS-1 (2026-08-05, operator-directed; panel-hardened design, NOT built) + +**The cognition clock and the persistence clock decouple: every sealed cycle +PUBLISHES to RAM immediately; durability becomes a batched background sync +barrier over K cycles. The hot window is the head of the log kept resident — +eviction-only, never invalidation — which is why it beats "another cache +layer": a cache is a copy kept coherent with the truth; the window IS the +truth, retained past its durability point.** Full design: +`.claude/plans/measure-64k-axes-v4.md` (composes with v2, survives v3's M/O +results). Two watermarks: `published_head` (RAM) vs `durable_head` (advanced +only at barriers); crash window `(durable_head, published_head]`. + +**The panel inverted the fork.** Lance mints one version per commit, so K +cycles per flush meant either (i) K unsynced commits + ONE fdatasync barrier, +or (ii) K cycles inside one Lance version with `CycleId` as the fine clock. +The initial lean was (ii) ("the CycleId/DatasetVersion type split +pre-anticipated it") — **refuted with citations**: `temporal.rs` has no +cycle-within-version coordinate, so under (ii) the no-hindsight guarantee +silently degrades to up-to-(K−1) cycles of intra-version hindsight for a +Strict reader; `hlc_tick` repurposing is the forbidden third numbering +wearing a borrowed name; and the 1:1 binding is contractual at ≥6 sites. +**(i) barrier flush is the recommendation**: 1 cycle = 1 real Lance version +survives everywhere (v2's pin, the base fence, the versions() ladder, the +no-hindsight falsifier), and the batch amortizes exactly the phase A0 +measured as unstable — the sync. + +Five invariants, each bought by a landed attack or sweep finding (v4 §4): +**H-1** checkpoint fencing (the per-owner `(phase, watermark)` checkpoint is +a THIRD durable artifact; it must never be durable ahead of `durable_head` +or recovery silently skips legitimate landings — the naive "die together" +claim was refuted until this fence was added); **H-2** torn-tail cleanup +(`durable_head` = newest fully-intact version at/below the last barrier; +recovery removes torn manifests above it or the durable prefix is not +contiguous); **H-3** the window is not a veto window (published = irrevocable; +the Libet veto stays pre-seal in v2's `ClosureState::Vetoed`); **H-4** the +zero-copy conditions (the window retains the SINGLE freeze-output allocation +per cycle AND the batched append writes FROM those bytes — otherwise it is +the forbidden "detached canonical state" snapshot); **H-5** rung-decided +visibility + the ack rebase (`E-ACK-IS-THE-KANBAN-TRIGGER-1`'s pump moves to +the publish ack — still a Lance ack, now meaning "published" not "durable" — +or the cognition clock is not actually decoupled). **⊘ H-5's second half +(the ack rebase) is RETRACTED same-day by +`E-PROGRESSION-IS-EXISTENCE-NOT-COMMAND-1` (above): there is no pump to +rebase — read H-5 through that entry; v4 §H-5 carries the corrected text.** + +**Naming rule:** this is *the MailboxSoA fleet's hot version window over +sealed cycles* — NEVER "VSA speaks Lance" (E-MARKOV-TEMPORAL-STREAM-1 demoted +the VSA carrier; the in-RAM substrate is the `MailboxSoA` fleet). Dated +caveats were added to `seal-vs-temporal-ordering-information.md` properties +2 and 4 (arrival is durable only at/below `durable_head`; cohort re-anchors +on the seal event). Measurements EXP-HOT-WINDOW P1-P5 pre-registered, none +run; KILLs named (publish latency not dropping; barrier(K) ≈ K·barrier(1)). + +## E-SEAL-AND-TEMPORAL-ARE-DIFFERENT-OBJECTS-1 (2026-08-05) + +**The seal and `temporal.rs` are not two implementations of one ordering +function — they compute different mathematical objects, and the O-arm's digest +divergence is the expected signature of that, not a defect in either.** Read off +the shipped source after the O-arm measured +`O-A 64565f362db2e4a5 ≠ O-B 3e71c2aa7be8e325`. + +Four things the seal computes that the temporal surface has no field for: + +1. **A cross-owner TOTAL order.** `LocalCausalRow::cast_seq` is contractually + per-owner — *"Cross-owner values are never compared"* — so + `local_trajectories` yields a forest of chains, a PARTIAL order. `freeze` + yields one total order. A partial order does not determine a total one. +2. **Arrival as an ordering input.** The seal's sort is stable on + `stream_position`, so arrival breaks ties; `LocalCausalRow` is exactly + `(owner, cast_seq)` and records arrival nowhere. The seal is the ONLY durable + encoder of cross-owner arrival, and `scan_sealed` may never re-sort. +3. **The per-row coalescing FOLD** (`row → last payload in stream order`) — a + destructive fold whose result depends on the total order. `temporal.rs` has + no row concept at all, so last-writer-wins at row granularity is computed + nowhere else. +4. **Cohort + read horizon** (`CycleFrame{cycle, base_version}`) — which casts + published atomically together, and which sealed `Vn` the whole cohort read. + Per-owner chains carry neither; the grouping key is simply absent. + +**Standing position (operator, same day):** keep `temporal.rs` as the +authoritative TEMPORAL model and the seal as the authoritative ORDERING model, +and treat the gap as an **explicit research question** rather than assuming one +should replace the other. The O-arm *failed semantically before it failed on +performance*, which makes its timing numbers almost irrelevant to the decision. + +**Scope fence (so the divergence is not overread):** the O-arm deliberately +scrambled arrival (bit-reversal of the owner id) so the two orders were FREE to +diverge. The result says *the seal preserves an arrival order temporal cannot +see*, NOT *the seal always disagrees*. On an arrival-ascending workload they +would coincide — and that coincidence would prove nothing. + +**Consequence for any future "let temporal source the ordering" proposal:** the +minimal change is not "make temporal smarter" but *give `LocalCausalRow` a +globally comparable key* — a contract widening that re-couples the owners the +deinterlace exists to decouple, and which still supplies neither the fold (3) +nor the cohort (4). The proposer owns that cost explicitly. + +Full statement + three pre-registered probes (tie density, fold-collision rate, +arrival-ascending control): +`.claude/knowledge/seal-vs-temporal-ordering-information.md`. + +## E-64K-1TO1-OWNERS-IS-THE-MAIN-MODEL-1 (2026-08-05, OPERATOR-ORDERED) + +**THE MAIN MODEL of this substrate is: up to 64k mailboxes, 1:1 +owner-per-mailbox, each owner COMPILE-TIME MUTATION-EXCLUSIVE over its own +SoA, 64k INDEPENDENT thought bodies deciding-or-processing concurrently, +converging at ONE deterministic seal boundary per cycle.** Operator-ordered +2026-08-05, after an external review surfaced that two framings were +coexisting ambiguously in the record. + +The demarcation, stated so it cannot blur again: + +- **"One SoA has one owner" means exclusive mutation authority per SoA + instance.** It NEVER meant "the whole population must be rows inside one + owner." +- **The one-tenant configuration (D-BLW-1..4: one corpus as rows in one + owner's slice) is a BENCHMARK HARNESS SHAPE for single-corpus + experiments — never the architecture.** §12.3a′'s "the real axis is rows + inside one owner" scoped a benchmark's scale knob; it did not and does + not redefine the runtime topology. +- **Two nested levels of parallelism, both real:** OUTER = 64k independent + owners (THE main model; the D-KIA-A2 pre-registered falsifier — + median-of-5, ≥2× at ≥4,096 owners with ≥100 µs bodies — is ITS gate, + and "parallel" at this level stays a doctrine until A2 measures it); + INNER = row-level bodies within one owner's slice (D-BLW-4's measured + 3.27× lives here). +- Code already conforms: the fleet is N independent `MailboxSoA` owners + behind `MailboxFleet` (`&mut` = the compile-time exclusivity), the GREEN + probes drive 64 real 1:1 owners, and no source file implements a + population-in-one-owner topology. What this ruling fixes is the CANON — + every future doc, plan row, and harness description reads through it. + +**Status:** operator-ordered, BINDING. Cross-refs: wiring doc §10 (the +decide-or-continue doctrine, now explicitly the main model); +plan §12.3a′ (re-scoped by this entry to benchmark-axis only); +E-AN-OWNER-IS-A-TENANT-NOT-A-SHARD-1 (the same truth from the other side). + + +## E-MEASUREMENT-BURNS-THE-STATE-1 (2026-08-04) + +**A measurement of an awareness-coupled system burns the state it measured — +once measured, it cannot be remeasured; the only admissible carry-forward is +the distribution shape × Prozentrang from the prior.** Operator-ruled design +law for D-BLW-5 and every future jc-loop / awareness-injection probe. Two +halves: (1) the PAYLOAD law — never inject the raw statistic (a scalar is +trivially echoable, building the Goodhart/anchoring fixed point into the +instrument); inject the prior pool's palette256/HDR-bucketed distribution +shape plus the percentile rank of the observation within it (Belichtungsmesser +machinery: `ndarray::hpc::cascade::{expose→Band, recalibrate}`, +`statistics::percentile`); (2) the SINGLE-MEASUREMENT law — S₀ is one-shot at +V₀, sealed; the instrument's next run is S₁ at V₁, a new measurement of a +different (post-injection) system, never a remeasure. Enforced by +temporal.rs hindsight blindness (Strict-rung version-gated reads, the +D-BLW-3 precedent) × the shape sensor riding as META only (ELEVATED +rung-marked, never corpus, never recomputed-and-back-dated). New structural +guard: an append-only measurement ledger keyed `(statistic-id, version)` +whose recompute-at-sealed-key path ERRORS, with can-fire + can-stay-silent +tests. + +Doctrine doc: `.claude/knowledge/observer-effect-tfpn-doctrine.md` (TFPN +arms + Gadamer/Goodhart readings + full falsification regimen). Plan: +§12.9 + §12.9a. + +**Status:** operator-ruled design law (binding). The observer EFFECT itself +stays CONJECTURE until D-BLW-5 runs. + + ## E-HORIZONTVERSCHMELZUNG-GAP-CLOSES-1 (2026-08-04) **The a-priori/hindsight gap is not a constant — it decays monotonically as @@ -22,6 +283,48 @@ claimed: validity (D3b closed), significance (no dependence model), zero-copy **Status:** FINDING (measured; harness + output in-tree, plan §12.8). +> **⊘ Correction (2026-08-05, external-review catch, two parts).** +> (1) *"decays monotonically" was an overclaim*: |Δκ| runs 0.485 → 0.251 → +> 0.079 → 0.031 → ≈0 → **0.011 → 0.017** → 0 — a rebound at V6/V7. The +> honest statement: **the gap moves toward zero overall, with a small +> rebound at V6/V7**. The trajectory shape, the IN/IN band, and every other +> claim in this entry are unaffected; the plan §12.8 headline is corrected +> in the same commit. (2) *G4 fixture-replacement post-mortem, recorded in +> full as the approval record*: the ORIGINAL can-fire fixture's premise +> ("'god' any-overlap ≈ 90 %") was MEASURED at 0.1285 during the central +> gate pass — it would have made the can-fire half near-vacuous, so it was +> replaced BEFORE the assert stage by constant-by-construction tails +> (`s.wrapping_add(1) != 0 || s == u32::MAX` all-fire / +> `u64::from(score) > u64::from(u32::MAX)` never-fire), which PASSED; the +> real-data 'god' projection was retained as the can-stay-silent arm (with +> a drift assert), which PASSED. The replacement decision was made +> centrally by the orchestrator mid-gate (autonomous session; this line is +> the recorded approval), on the ground that a fixture premise contradicted +> by measurement cannot be the can-fire evidence — constant-by-construction +> tails cannot rot the same way. +> +> **⊘ Regrade (2026-08-05, external-review catch — the reference-pool +> confound).** The fixed-prefix restriction removed OUTPUT-set growth but +> not REFERENCE-population growth: `rank_verdicts` recomputes the top +> quartile over the whole currently-seated pool (which grows 250 rows per +> cycle), so each horizon re-ranks the same fixed subjects against +> different competitors, a different quartile cutoff, and a different tie +> population. What is MEASURED is therefore a **cohort-relative rank +> trajectory under a growing reference pool** — a legitimate effect, but +> attributing it to *Horizontverschmelzung* (horizons fusing) requires the +> three-arm decomposition, now the pre-registered follow-up (D-BLW-3b): +> **A** fixed subjects × fixed reference pool (expected: no movement — the +> silent arm), **B** fixed subjects × growing pool (isolates +> rank-renormalization — THE ARM THIS ENTRY MEASURED), **C** fixed +> subjects × fixed pool × awareness-coupled representation (the semantic +> candidate). The Z-containment control was matched to plumbing, not to the +> rank mechanism, so it cannot separate B from C. The measured NUMBERS +> stand; the ATTRIBUTION is regraded CONJECTURE pending A and C. Also +> fixed in code the same day: C7's trajectory-wide DROP had keyed on +> V8 Hamming (zero by construction) — it now requires zero Hamming across +> ALL horizons, closing a cancelling-churn false-DROP path (the recorded +> run is unaffected: max|Δκ|=0.485, max Hamming A:152/B:288). + ## 2026-08-04 — E-THE-CARRIER-CHANGED-THE-INSTRUMENT-DID-NOT-1 — a 24-locus register that writes one shared locus is still a coincidence bit diff --git a/.claude/board/INTEGRATION_PLANS.md b/.claude/board/INTEGRATION_PLANS.md index 8f8d52ab6..50a109ee9 100644 --- a/.claude/board/INTEGRATION_PLANS.md +++ b/.claude/board/INTEGRATION_PLANS.md @@ -1,3 +1,75 @@ +## 2026-08-05 — measure-64k-axes v3 — ACTIVE (the three arms Stage A0 earned; M+O build lane dispatched) + +**Plan:** `.claude/plans/measure-64k-axes-v3.md` +Operator review of the A0 results designated what comes next. The ordering +takeaway: the expensive part is NOT 64k owners and the unstable part is NOT +sealing — instability lives in filesystem → page cache → writeback → allocator, +so effort belongs in temporal chunk scheduling, Morton ordering, rolling +closure and batch geometry rather than in redesigning ownership. **That is a +hypothesis A0 makes worth testing, not a finding A0 proved.** +**M-arm** (prioritized): `logical → MORTON REORDER → seal → WAL` vs A0's +`logical → seal → WAL`; reorder timed as its own phase, verdict is the SUM +(`reorder_cost − downstream savings`), digest identity mandatory, and the +ordered-chunk fast path measured against T1's stable 78–86 ms. +**O-arm**: `cast→seal→WAL→temporal` vs `cast→temporal→seal→WAL` — isolates +the long-standing "temporal.rs already provides the ordering" hypothesis; +PRIMARY observable is digest identity decided before any timing is read, with +a compile-time firewall so O-B cannot consult the sealed stream, and an +explicit not-constructible outcome instead of a rigged comparison. +**A-arm** (deferred): decomposing L1a's −171 ms build delta into allocation +count / arena reuse / locality / pure-allocation control — separate processes +for the reuse half; the locality half stays BLOCKED on perf counters rather +than estimated. +Unchanged: crypto stays out until rolling closure is measured; the WAL knee +stays unclaimed; D-KIA-A2 frozen; implementation-scoped wording everywhere. + +## 2026-08-05 — measure-64k-axes v2 — ACTIVE (rolling epoch closure; supersedes v1's EXECUTION MODEL, keeps v1 as Stage A0 baseline) + +**Plan:** `.claude/plans/measure-64k-axes-v2.md` +Operator correction: the 64k boundary stays the ACCOUNTING/VERSION boundary +and stops being the physical turnstile. Model: 64k logical owners → rolling +Morton-ordered chunk closure (Libet 200 ms per-chunk veto windows; +ClosureState Open→Registered→{Vetoed,Held,Deferred}→Frozen→Appended) → ONE +epoch manifest publishes ONE DatasetVersion (chunk appends are never +versions; crash contract: chunks without manifest = invisible abandoned +epoch). Decisions recorded: MailboxId keeps identity only, WriteOrderKey +(morton_chunk/lane/cycle_position) carries storage order; CHUNK baton never +owner baton; "64k complete" = every owner has exactly one accounting +outcome (committed+vetoed+held+deferred+absorbed == 65,536), only committed +advance (#879). Morton cascade L0 page {4,8,16 KiB} / L1 segment +{1,2,4,8 MiB} / L2 epoch 32 MiB / L3 16 epochs — two independent knobs; +temporal.rs gains the verified ordered-chunk fast path (validate+append, +no sort) with digest identity vs the generic path required. Encryption: +per-chunk AEAD contexts (nonce/AAD from epoch+base+chunk-seq+retry+len, +never chunk_id alone), parallel on bounded pool, baton orders appends — +Stage B gated on the AEADs-fork dep decision (P0 forks-only). +Grind taxonomy (CPU/sync/encryption/storage/temporal) measured separately; +16-cycle curve classified warm-up/amortisation/cache-turnover/collapse with +the backlog slope as THE collapse signal. D-KIA-A2 FROZEN; operator +override EXP-KIA-A2-ROLLING-CLOSURE (non-claiming). v1 build lane continues +— its deliverable IS Stage A0 + the shared instrumentation. + +## 2026-08-05 — measure-64k-axes v1 — ACTIVE (operator-specified corrected benchmark) — Sonnet build lane dispatched + +**Plan:** `.claude/plans/measure-64k-axes-v1.md` +The prior performance/memory numbers mixed FIVE independent axes (logical +owner count / physical SoA layout / WAL segment size / temporal +reconstruction / execution concurrency); this plan varies exactly one at a +time. Arms: B0 DummyOwner cast baseline (the modern #879 fake-owner +control) · B1a/B1b real owner-exclusive SoAs (hot MailboxSoA vs canonical +NodeRow512 — memory claims never blended; derived: runtime ownership tax, +hot representation overhead) · WAL curve W0-current/W1-contiguous (five +segment sizes over ONE 32 MiB frame, write_vectored + exactly one fdatasync ++ one DatasetVersion per cycle; 2 warm-ups + 16 measured cycles = constant +512 MiB per configuration; ONE release binary, never 16 tests) · T0/T1/T2 +temporal phases post-WAL over 65,536 × 16 = 1,048,576 rows · +L1a/L1b chunked-layout control (a physical chunk is NOT an owner) · +EXP-KIA-A2-64K exploratory concurrency (non-claiming; D-KIA-A2 untouched; +digest-identical sequential-vs-parallel witness). Deliverable: the four +answers — ownership cost, layout cost, WAL amortisation knee (descriptive, +never PASS/KILL), and what genuine parallel thought execution adds before +the deterministic seal. + ## 2026-08-02 — kanban-64k-inverted-awareness v1 — PLANNED / CONJECTURE (parallel thinking + the inverted-awareness witness) — main thread **Plan:** `.claude/plans/kanban-64k-inverted-awareness-v1.md` diff --git a/.claude/board/ISSUES.md b/.claude/board/ISSUES.md index 3567b8845..3ebfefc32 100644 --- a/.claude/board/ISSUES.md +++ b/.claude/board/ISSUES.md @@ -1,5 +1,28 @@ # Issues Log — Open + Resolved (double-entry, append-only) +## ISS-MARM-T1-4X-A0-GAP (2026-08-05) — OPEN, MEASUREMENT DEFECT NOT A RESULT + +**The observation.** The M-arm's temporal-reconstruction baseline (T1) reads +**320–340 ms** over 1,048,576 rows. Stage A0 measured the same nominal row count +at **78–86 ms**. That is a ~4× gap between two runs of the same harness on the +same host, and it is unexplained. + +**What it blocks and what it does NOT block.** The M-arm's +natural-vs-Morton comparison is INTERNALLY valid (same run, same harness, same +row count, digest identity `68128e3662df105c` on both pipelines) and stands. What +is void is the **cross-run** comparison: the ordered-chunk fast-path number +(350.9 ms) **must not** be held against A0's 78–86 ms until this is explained. +`measure-64k-axes-v3.md` carries the same caveat inline. + +**Named suspects, none confirmed.** (a) the M-arm materialises `BenchRow` +INSIDE the timed region; (b) the `stream_position` relabeling the harness needs +because `freeze` always sorts by that field; (c) a third phenomenon neither of +those covers. + +**Resolution shape.** Instrument the T1 region to separate materialisation from +reconstruction, then re-run both arms in one process. Until then the two T1s are +not commensurable and neither number may be quoted against the other. + ## ISS-MAILBOXSOA-ROW-COST-VS-512B-CANON (2026-08-04) — OPEN, QUESTION NOT CONCLUSION **The observation, arithmetic only.** The canonical node row is diff --git a/.claude/board/STATUS_BOARD.md b/.claude/board/STATUS_BOARD.md index 6faf8f7df..27b271949 100644 --- a/.claude/board/STATUS_BOARD.md +++ b/.claude/board/STATUS_BOARD.md @@ -28,6 +28,14 @@ Plan: `.claude/plans/kanban-64k-inverted-awareness-v1.md` (operator anchors a/b; | D-id | Deliverable | Repo | Status | Evidence | |---|---|---|---|---| +| PROBE-IGNITION | The write path DRIVEN: arm by MetaWord write -> discover by board scan -> cast write-on-behalf -> seal -> apply. 64 real MailboxSoA owners, real KJV corpus, 6 cycles, 7 cohorts, 11 gates both halves | lance-graph | **GREEN 2026-08-05** — 2/2 tests; c1 = 24 casts (20 Flow + 4 Block), c5/c6 rest with zero casts and no seal; G9/G10 pin the two OPEN #879 caveats | `tests/probe_ignition.rs`; AGENT_LOG 2026-08-05 | +| MEASURE-64K-AXES | Operator-specified five-axis benchmark: B0 dummy baseline, B1a/B1b ownership+representation split, W0/W1 WAL segment curve (one fdatasync/one version per cycle), T0-T2 temporal phases over 1,048,576 rows, L1a/L1b chunked-layout control, EXP-KIA-A2-64K exploratory concurrency (non-claiming, A2 untouched). One release binary `measure_wal_curve`; four answers, axes never blended | lance-graph | Stage A0 MEASURED (3 of 4 answers; WAL knee NOT REPRODUCIBLE and unclaimed). v3's M-arm and O-arm both **MEASURED 2026-08-05, both NEGATIVE** (pre-registered two-sided, so both are findings): M-arm — digests MATCHED (`68128e3662df105c`), reorder 9.4 ms, downstream −25.8 ms ⇒ **Δtotal +35.2 ms, Morton LOSES**; ordered-chunk fast path 350.9 ms was slower than the generic 339.7 ms. O-arm — **DIVERGED** (`64565f362db2e4a5` ≠ `3e71c2aa7be8e325`) ⇒ the seal's ordering is load-bearing FOR THIS O-B CONSTRUCTION; follow-on question queued as PROBE-SEAL-VS-TEMPORAL. Open measurement defect: `ISS-MARM-T1-4X-A0-GAP` (M-arm T1 320-340 ms vs A0's 78-86 ms — blocks that one cross-run comparison only). A-arm (allocator-vs-architecture decomposition) deferred. v2 rolling-epoch-closure model supersedes v1's execution model (v1 = Stage A0 baseline + instrumentation, lane in build); then A1/A2 rolling+Morton lane; crypto REMOVED from the seal benchmark per operator sanity-check (seal path verified crypto-free in source; encryption = separate later layer, AEADs dep no longer blocking); EXP-KIA-A2-ROLLING-CLOSURE recorded (A2 frozen) | plans measure-64k-axes-v1 + v2 | +| D-IGN-B | Ignition starts the REAL lenses: arming z ∈ {0 unarmed, 1-4 = the four stances, 5 = Fusion (Strict/Aware gap read)} — six ordinals in MetaWord's 6-bit field, no 36-style bridge (Q1 sidestepped); thought bodies = the shipped nars stance machinery via cycle_driver's pluggable seam (D-BLW-1 precedent) + blw_fusion's two-projection read. Can-fire: different lenses over byte-identical rows ⇒ non-identical readouts; silent twin: same lens ⇒ bit-identical; unarmed ⇒ none | lance-graph | **GREEN 2026-08-05** — 1/1 test, L0-L7 + z5-BLOCKED; L1 Kant≠Wittgenstein over byte-identical rows, same-lens bit-identical; Hegel/Nietzsche NON-empty on the text path; z=5 reserved with the printed blocker | plan cycle-driver 12.11; `tests/d_ign_b_lenses.rs` | +| D-BLW-5 | Observer-effect loop: a jc statistic about the cohort fed back into awareness; four pre-registered arms (true/false±/placebo) + the §12.8 bloom criterion as the frozen null instrument. KILL: placebo movement invalidates; T-silence is a reportable null. Payload refined §12.9a: distribution shape × Prozentrang (never the raw scalar); single-measurement law + remeasure guard; doctrine doc `observer-effect-tfpn-doctrine.md` | lance-graph | **PAUSED by operator 2026-08-05** — the Opus design lane was stopped mid-run (controlling signal; not relaunched). Banked and committed: the Sonnet API inventory (`exec-runs/d-blw-5-api-inventory-sonnet.md` — BeliefArena admits hand-built statements; jc+run_cycle live in disjoint crates, supervisor+jc dev-dep pre-ruled acceptable; ndarray unreachable supervisor-side). Gate to resume: operator direction | plan cycle-driver 12.9 | +| PROBE-ARC-TORQUE | Torque of an arc = 2× Heron triangle area from 3 HHTL O(1) distances (magnitude metric-only; chirality via helix_orient Fisher-2z frame codes); Fisher 2z = logit((1+r)/2) as the additive equal-information embedding, hydratable via tanh. Stage B: translator stray/mindset vs source (floor = intra-language variance; Romans 5:12 in-quo/eph-hō as known-answer falsifier). Stage C: author-bias fields on the redactional layer + attribution of non-canonical books (G1-G5 in-canon ground-truth gates first). KILL per stage: F1 radial/tangential non-separation; clamp-rate ceiling; G1-G5 failures | lance-graph | Queued — proposed §12.10, behind PROBE-IGNITION + D-BLW-5 | plan cycle-driver 12.10 | +| D-ACK-CLEANUP | Delete the ack/pump/tick theater entirely + add the visibility surface (operator-directed, context-hot). `kanban_actor.rs` → message-free module: `PhaseCensus` (`&self` fleet census; absorbing DAG-derived; empty ≠ at-rest) + pure `mul_target`/`parse_kanban_step`; `KanbanMsg`/`KanbanActor`/5 RPC drivers/`run_to_absorbing`/`KanbanRouteError` DELETED. Lane E migrated to direct `&mut` owner (supervisor+ractor out of its feature); W2b probe rewritten direct + census-over-real-SoA. Zombie verdict: half yes (lib.rs re-exports + lane E kept it alive; `ack_and_propose` already absent — the ack half lived only in docs). OGAR boundary verified: zero consumers; its ActionHandler ack surface is legitimate membrane protocol. Kanbanstep (`VersionScheduler::on_version`) NOT theater — stays canonical; naming question flagged only | lance-graph | **SHIPPED 2026-08-05** — all gates green (supervisor 9 lib + w2b 3/3 + cycle-driver 4/4; onebrc lane-e 20/20; clippy --no-deps clean; fmt clean) | `E-ACK-THEATER-DELETED-1`; TD-MESSAGE-RESIDUE resolution; `E-PROGRESSION-IS-EXISTENCE-NOT-COMMAND-1` | +| D-HWV-1 / EXP-HOT-WINDOW | The hot version window (operator-directed): publication clock decoupled from persistence clock — every sealed cycle publishes to RAM immediately (`published_head`), durability is a batched background **sync barrier** (`durable_head`, barrier-flush fork: K unsynced Lance commits + ONE fdatasync, so 1 cycle = 1 real DatasetVersion survives everywhere). Vertical batching (time, not owners); Nagle-shaped flush policy (bytes/16-dirty/200ms/pressure/shutdown/durable-only-reader). Five panel-bought invariants H-1..H-5 (checkpoint fencing, torn-tail cleanup, no-veto-after-publish, zero-copy conditions, rung-decided visibility — H-5's original "ack rebase" clause RETRACTED per `E-PROGRESSION-IS-EXISTENCE-NOT-COMMAND-1`: no pump/ack/scheduler; seal → publish → immediately queryable → durability trails; the window is a resident horizon of immutable versions, not a message queue). Version-multiplexing fork REJECTED (temporal.rs has no cycle-within-version coordinate ⇒ intra-version hindsight). P1-P5 pre-registered with named KILLs | lance-graph | **Design banked 2026-08-05, panel-hardened (1 sweep + 1 adversarial refuter; fork inverted by evidence), NOT built** — build lane gated on operator word; P1/P2 comparisons additionally gated on ISS-MARM-T1-4X-A0-GAP + TD-LANCE9 remeasure | plan measure-64k-axes-v4; `E-HOT-WINDOW-DECOUPLES-THE-CLOCKS-1`; v2 cross-note; seal-vs-temporal caveats | +| PROBE-SEAL-VS-TEMPORAL (3 probes) | The question the O-arm divergence opened: **what does the seal compute that `temporal.rs` does not encode?** Answered from shipped source — cross-owner TOTAL order (temporal's `cast_seq` forbids cross-owner comparison ⇒ partial order) · arrival as an ordering input, durably recorded nowhere else · the per-row coalescing FOLD (`temporal.rs` has no row concept) · cohort + read horizon `CycleFrame{cycle, base_version}`. Three pre-registered probes: **SEAL-TIE-DENSITY** (do cross-owner `stream_position` ties occur? ties ⇒ order partly derives from non-durable arrival), **FOLD-COLLISION-RATE** (do two owners write one row? zero ⇒ the fold is structural-but-unexercised), **ARRIVAL-ASCENDING-CONTROL** (the can-stay-silent twin — digests must MATCH when arrival is owner-ascending; divergence there ⇒ the four-item account is incomplete) | lance-graph | Queued — doctrine + probe queue landed, none run. Standing position: temporal.rs = authoritative TEMPORAL model, seal = authoritative ORDERING model, the gap = an explicit research question | `.claude/knowledge/seal-vs-temporal-ordering-information.md`; `E-SEAL-AND-TEMPORAL-ARE-DIFFERENT-OBJECTS-1`; plan measure-64k-axes-v3 | | D-KIA-0 | jc capability map + dichotomous-statistics decision note (phi/KR-20/kappa naming; Spearman dropped at view 2) | lance-graph | Queued | plan W0 | | D-KIA-A1 | ⊘ RESCOPED 2026-08-04 (E-ACTOR-IS-NOT-THE-PHASE-PATH-1): #879 is the complete phase-progression path; KanbanActor has no assigned architectural responsibility (legacy compatibility code). SHIPPED: held-owner reschedule/wake. OPEN: run_cycle drained-writer retry guard; missing-owner counter in cognitive_pass | lance-graph | Queued | plan W1 | | D-KIA-C1b | jc additive-only extension: kappa + McDonald's omega + r-family effect size (R/R-squared, eta-squared = explained variance) + t-test (t/df/p) + a named phi wrapper. Cohen's d explicitly OUT — calculated separately if ever wanted. HARD CONSTRAINT: additive only — pearson/spearman/cronbach_alpha/icc keep their arithmetic, signature and semantics; any diff changing an existing jc statistic is an automatic reject. ONE sanctioned edit: widening reliability.rs private helpers (mean/all_finite/average_ranks/pop_var) to pub(crate) for reuse, visibility only, no body change. C1 audit found phi = pearson-on-binaries (already present in substance) and KR-20 = alpha-on-dichotomous (naming only); kappa absent = the real gap. SHIPPED as crates/jc/src/stats.rs: cohen_kappa, omega_total, phi, multiple_r/multiple_r_squared, eta_squared, t_test_one_sample/paired/welch/student, anova_one_way; 31 new tests (107 lib + 11 doctests green), clippy-clean. Existing-file diff is visibility-only (mean/all_finite -> pub(crate); average_ranks/pop_var NOT widened, unused). Unblocks D3a (overlap MEASUREMENT) — NOT a fusion claim: kappa is chance-corrected agreement under the observed marginals and says nothing about incremental value, so fusion still needs D3b's external criterion per the plan's own C3. Corrective slice (external review): omega sign-erasure + R-squared scale-dependence fixed; BinaryAssociation/kr20 added | lance-graph | Shipped (#887) + corrective slice | plan W0/C1b | diff --git a/.claude/board/TECH_DEBT.md b/.claude/board/TECH_DEBT.md index f54f7cfbe..909b41157 100644 --- a/.claude/board/TECH_DEBT.md +++ b/.claude/board/TECH_DEBT.md @@ -1,5 +1,63 @@ +## TD-LANCE9-LANCEDB036-REMEASURE (2026-08-05, operator-noted, DEFERRED) + +**Pins today:** `lance`/`lance-linalg`/`lance-namespace` `=7.0.0`, +`lancedb` `=0.30.0` (`crates/lance-graph/Cargo.toml:38-41`; the PR #445 +exact-pin lockstep). **Operator note: lance 9 + lancedb 0.36 are expected to +improve the overhead measured by MEASURE-64K-AXES Stage A0.** Deferred by the +operator — recorded so the measured numbers are recognisable as the BEFORE +side. + +When it happens: bump the family together (lancedb's transitive requirement +pins lance — a half-bump makes the patch silently not apply), keep P0 +forks-only, then re-run `measure_wal_curve` under the same host discipline and +diff arm-by-arm. Expect movement in the storage/serialization arms +(W0-current, T0 `scan_sealed`); **B0/B1a/L1a touch no lance code, so movement +there means something else changed.** The WAL knee stays unmeasurable until the +host issue is fixed either way. + # Technical Debt Log — Open + Paid (double-entry, append-only) +## TD-BLW-FUSION-MANUAL-SEAL (2026-08-05) — OPEN + +`blw_fusion.rs` produces its sealed series by calling +`persist_cycle`/`recover_and_apply` directly with a hand-built `SweepSlot` +(`stream_position = c`), bypassing `collect_casts`/`seal_cycle`/`run_cycle` — +so it exercises the persist/apply half but NOT the ≤1-move-per-owner seal, +held-intent restaging, durable `position_base`, or frozen-cast retry +semantics. Its records never claimed otherwise (the permitted claims are the +`DeinterlaceRow`/`deinterlace` firsts + the rank criterion), but now that +`tests/probe_ignition.rs` proves the real chain end-to-end, the harness's +seal loop should be REBASED onto `run_cycle` so the fusion series is +produced by the same machinery production will use. Surfaced by an external +review (2026-08-05). Pay by: rebase the seal loop; re-run; assert the +recorded numbers reproduce. + +## TD-BLW3B-ABC-DECOMPOSITION (2026-08-05) — OPEN + +The D-BLW-3 trajectory was measured under a GROWING reference pool +(rank_verdicts recomputes the quartile over the seated pool, +250/cycle), so +the measured movement is a cohort-relative rank effect until decomposed. +Pre-registered follow-up D-BLW-3b, three arms: A fixed subjects × fixed +pool (expected silent), B fixed × growing (the arm already run — isolates +rank renormalization), C fixed × fixed + awareness-coupled representation +(the semantic candidate). The E-entry and plan §12.8 carry the regrade +(numbers stand; fusion ATTRIBUTION is CONJECTURE pending A and C). Pay by: +build the A and C arms on the blw_fusion scaffolding. + +**⊘ Scope analysis (2026-08-05, same day):** for THIS instrument the +decomposition partially collapses. Verse scores in `blw_fusion` are +horizon-INDEPENDENT (the bloom projection of static text); the only +horizon-dependent mechanism is ADMISSION (incremental seating). Arm A +(fixed subjects × fixed pool) is therefore silent BY CONSTRUCTION — a +vacuous gate the falsifiability rule forbids building — and Arm B is the +run already measured. **Arm C is the only informative arm**, and it +requires a representation whose scores themselves evolve with horizon: +an awareness-coupled reader — the SAME prerequisite D-BLW-5 names as its +first design decision. D-IGN-B (GREEN 2026-08-05) proved the candidate +substrate: the belief arena + stance machinery runs per-owner, in-cycle, +selected by arming. Payment path re-routed: Arm C is built AS D-BLW-5's +instrument (the arena-coupled criterion), not as a separate harness. + ## TD-RECOVERY-HASH-PARTITION-UNCERTIFIED (2026-08-04) — OPEN **Operator ruling (2026-08-04):** the #879 work that must not be reversed is @@ -712,6 +770,28 @@ cycle budget allows. (b) `supervisor::deliver_kanban_step`'s the message DELIVERY behind it is the redundancy. Same disposition: leave as is, documented. +*Resolution (2026-08-05, operator-directed — "clean up the ack theater +entirely, while context is hot"; `E-PROGRESSION-IS-EXISTENCE-NOT-COMMAND-1`):* +**DELETED, not left as is.** The entire message surface named above is gone +from source: `KanbanMsg::{Advance, MulAdvance, Tick}`, `KanbanActor`, +`KanbanRouteError`, `deliver_kanban_step`, `drive_mul_advance`, +`drive_version_tick`, `drive_scheduled_tick`, `run_to_absorbing`, all +`ractor::call!` sites in that module, plus their tests. `kanban_actor.rs` is +now the message-free visibility module (`PhaseCensus` — one `&self` census +pass replaces per-owner Phase RPCs) + the pure helpers (`mul_target`, +`parse_kanban_step`). Consumers migrated: onebrc lane E journals over the +direct `&mut` owner (supervisor + ractor dropped from its feature); the W2b +probe pins the real `MailboxSoA` Rubicon DAG through `try_advance_phase` +directly + exercises the census. (`ack_and_propose` was found ALREADY absent +from source — the ack half of the theater survived only in documentation.) +**What this resolution does NOT touch:** the kanbanstep +(`VersionScheduler::on_version → try_advance_phase(&mut)`, reference +`symbiont::kanban_loop`) stays canonical per the 2026-07-10 extension above — +it is the writer's own synchronous continuation, not a wait and not a +message. Open naming question only (not queued work): the WORD "scheduler" +in `VersionScheduler`/`NextPhaseScheduler` is a drift vector under the +no-pump vocabulary rule; the semantics are already compliant. + ## TD-STYLE-TABLE-RESIDUE (2026-07-10, D-TSC-1 follow-ups) Three residues from the M9 ThinkingStyle dedup (all OUT of D-TSC-1 scope, diff --git a/.claude/board/exec-runs/blw-fusion-d-blw-3-build.md b/.claude/board/exec-runs/blw-fusion-d-blw-3-build.md index 6b0249bfa..c0bbb4a0c 100644 --- a/.claude/board/exec-runs/blw-fusion-d-blw-3-build.md +++ b/.claude/board/exec-runs/blw-fusion-d-blw-3-build.md @@ -1,7 +1,9 @@ # D-BLW-3 build (Sonnet grindwork lane) -**Scope:** created `crates/lance-graph-planner/examples/blw_fusion.rs` (~1145 -lines); edited `crates/lance-graph-planner/Cargo.toml` (one dev-dep line + +**Scope:** created `crates/lance-graph-planner/examples/blw_fusion.rs` (~1480 +lines as shipped — the lane's original draft was ~1145; the count grew during +the orchestrator's central-gate fixes; corrected 2026-08-05); edited +`crates/lance-graph-planner/Cargo.toml` (one dev-dep line + comment, `jc = { path = "../jc" }`). No other file touched. `temporal.rs`, `crates/jc`, `persist_sink.rs`, `blw_tenant.rs` — read only, not modified. `AGENT_LOG.md` — read (first 150 lines), not written. diff --git a/.claude/board/exec-runs/blw-rows-d-blw-4.md b/.claude/board/exec-runs/blw-rows-d-blw-4.md index b0ec832f9..1219faaa7 100644 --- a/.claude/board/exec-runs/blw-rows-d-blw-4.md +++ b/.claude/board/exec-runs/blw-rows-d-blw-4.md @@ -200,6 +200,16 @@ not left reading as an open question. **Measured outcome — PASS.** +> **⊘ Scope of the PASS (clarified 2026-08-05 after external review):** the +> PASS is against THIS harness's own pre-registered gates (G-A/G-B/G-C below, +> pinned before the run under §12.3a′'s re-pin license — the operator ruling +> that moved D-BLW-4's axis from owners to rows and re-pins the per-row work +> threshold and row count at harness-write time). It is a SINGLE reported run +> at 2,000 rows with a `> 1` criterion. It is **NOT** a pass of the inherited +> A2/W2 protocol (median-of-5 after one discarded warm-up, ≥ 2×), and no such +> pass is claimed here. The measured 3.27× happens to exceed 2×, but one run +> is not a median-of-5 — that stricter tier remains open under D-KIA-A2. + | gate | criterion (fixed before the run) | measured | |---|---|---| | G-A | body ≥ `BODY_FLOOR_US`, else INCONCLUSIVE | met | diff --git a/.claude/board/exec-runs/d-blw-5-api-inventory-sonnet.md b/.claude/board/exec-runs/d-blw-5-api-inventory-sonnet.md new file mode 100644 index 000000000..a72c19b64 --- /dev/null +++ b/.claude/board/exec-runs/d-blw-5-api-inventory-sonnet.md @@ -0,0 +1,1011 @@ +# D-BLW-5 API inventory — Sonnet grindwork lane + +> Edit-only. No cargo run of any kind. All signatures below were read from +> source in the same pass that wrote this file. Compliance with +> `.claude/v3/knowledge/sonnet-worker-guardrails.md` §1: full-file reads, +> no invented types, no board-file writes other than this one file. Branch +> `claude/x265-x266-plans-review-h9osnl` was NOT switched. + +--- + +## A. `BeliefArena` + NARS revision + +**Location:** `crates/lance-graph-planner/src/nars/belief.rs` (the type +`super::belief::BeliefArena` imported by `crates/lance-graph-planner/src/nars/stance.rs:24`). +Module path: `lance_graph_planner::nars::BeliefArena` — re-exported from +`crates/lance-graph-planner/src/nars/mod.rs` (confirmed via the +`use lance_graph_planner::nars::{BeliefArena, CStmt};` import in +`crates/lance-graph-supervisor/tests/d_ign_b_lenses.rs:110`). + +### Stamp (`belief.rs:31`) + +```rust +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct Stamp(pub u64); + +impl Stamp { + pub fn source(id: u32) -> Self; // Stamp(1u64 << (id % 64)) + pub fn disjoint(self, other: Self) -> bool; + pub fn union(self, other: Self) -> Self; +} +``` + +### Copula / CStmt (`belief.rs:54,77`) + +```rust +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Copula { + Inh, + Sim, + Impl, + Rel(u16), +} +impl Copula { + pub fn transits(self) -> bool; // matches!(self, Inh | Sim) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct CStmt { + pub s: u16, + pub cop: Copula, + pub p: u16, +} +``` + +### Belief entry type — exact fields (`belief.rs:88-104`) + +```rust +#[derive(Debug, Clone)] +pub struct Belief { + pub stmt: CStmt, + pub truth: TruthValue, // frequency/confidence — see below + pub stamp: Stamp, // evidential base (S4) + pub rung: u32, // Tarski rung; 0 = observed + pub premises: Vec, // arena indices, derived beliefs only + pub contradiction: f32, // preserved max |f1-f2| across revisions +} +``` + +There is **no** separate "provenance" or "verse" field on `Belief` itself +— that lives in `stance::Provenance` (a SEPARATE struct the caller +maintains alongside the arena; `stance.rs:90-99`: `{ verse: String, stmt: +CStmt, negated: bool }`). The arena itself carries no rung-1/verse +metadata. + +### ReviseOutcome (`belief.rs:107-122`) + +```rust +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum ReviseOutcome { + Admitted { id: u32 }, + Revised { id: u32, synthesis_c: f32, depth: f32 }, + Chosen { id: u32, kept_existing: bool }, +} +``` + +### BeliefArena — full public surface (`belief.rs:129-337`) + +```rust +#[derive(Debug, Default)] +pub struct BeliefArena { + // entries: Vec, index: HashMap — both private + pub passes: u32, + pub reached_fixed_point: bool, +} + +impl BeliefArena { + pub fn new() -> Self; // #[must_use] + pub fn entries(&self) -> &[Belief]; // #[must_use] + pub fn get(&self, stmt: CStmt) -> Option<&Belief>; // #[must_use] + + /// Admission path #1 — observation. Absent -> Admitted; present -> + /// routes through revise_at (disjoint stamp -> Revised, else Chosen). + pub fn observe(&mut self, stmt: CStmt, truth: TruthValue, stamp: Stamp) + -> ReviseOutcome; + + /// The S4 revision guard on an EXISTING belief id. + pub fn revise_at(&mut self, id: u32, new: TruthValue, stamp: Stamp) + -> ReviseOutcome; + + /// Admission path #2 — derived candidate (no observation source of its + /// own). Ground (non-empty-stamp) beliefs are NEVER overwritten; a + /// pure-derived belief updates only when the candidate's + /// `expectation()` strictly exceeds the stored one (+1e-6 epsilon). + /// Returns whether the arena changed. + pub fn admit_derived(&mut self, stmt: CStmt, truth: TruthValue, + premises: &[u32], rung: u32) -> bool; + + /// Copula-gated transitive closure (Inh/Sim only), NARS deduction + /// truth per pair, CHOICE on expectation(), true fixed point or + /// `max_passes` backstop. Sets `self.passes` / `self.reached_fixed_point`. + pub fn close_transitive(&mut self, max_passes: u32); +} +``` + +**Can an externally-constructed belief be inserted directly (bypassing +`stream`'s text-parsing path)?** YES, on both admission paths: + +- `arena.observe(CStmt { s, cop, p }, TruthValue::new(f, c), Stamp::source(id))` + — hand-built `CStmt`/`TruthValue`/`Stamp`, no text parsing involved + (this is exactly what `belief.rs`'s own `#[cfg(test)]` module does, e.g. + `revision_disjoint_moves_truth_and_terminates`, `belief.rs:355-385`). +- `arena.admit_derived(stmt, truth, premises, rung)` — the derived-candidate + path; also fully hand-constructible, no text. + +`stream` (in `stance.rs`, see §B) is ONE caller of `observe`/`admit_derived` +via its own tokenizer, not the only way to populate an arena. A D-BLW-5 +build wanting programmatic beliefs (not KJV text) can call `observe`/ +`admit_derived` directly against a fresh `BeliefArena::new()`. + +### TruthValue — exact fields (`crates/lance-graph-planner/src/nars/truth.rs:8-15`) + +```rust +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct TruthValue { + pub frequency: f32, // 0..1, proportion of positive evidence + pub confidence: f32, // 0..1, total evidence weight +} +impl TruthValue { + pub fn new(frequency: f32, confidence: f32) -> Self; // clamps both to 0..1 + pub fn expectation(&self) -> f32; // confidence*(freq-0.5)+0.5 + pub fn surprise(&self, prior: f32) -> f32; + pub fn evidence_weight(&self) -> f32; // c/(1-c), f32::MAX at c>=1 + pub fn revise(&self, other: &TruthValue) -> TruthValue; // NARS revision + pub fn deduction(&self, other: &TruthValue) -> TruthValue; + pub fn induction(&self, other: &TruthValue) -> TruthValue; + pub fn abduction(&self, other: &TruthValue) -> TruthValue; + pub fn analogy(&self, sim: &TruthValue) -> TruthValue; +} +impl Default for TruthValue { /* frequency: 0.5, confidence: 0.0 */ } +``` + +No separate `Fingerprint`/hashed-distance truth path exists in this +module — truth moves ONLY through the `TruthValue` methods above (per +`belief.rs`'s own module doc: "the arena … moves truth ONLY by the one +engine's truth functions"). + +**Query/read surface:** `arena.entries()` (whole slice, admission order), +`arena.get(stmt)` (point lookup by exact `CStmt`). There is no filtered/ +indexed query beyond these two — any subject/copula-scoped view (e.g. +`stance_panel`'s Wittgenstein games map, `stance.rs:512-532`) is built by +the CALLER iterating `entries()`, not by an arena method. + +--- + +## B. The stance/readout surface as consumed by `d_ign_b_lenses.rs` + +File: `crates/lance-graph-supervisor/tests/d_ign_b_lenses.rs` +(feature-gated `#[cfg(feature = "cycle-driver")]`, module +`d_ign_b_lenses`). + +### Imports of the stance surface (`d_ign_b_lenses.rs:109-110`) + +```rust +use lance_graph_planner::nars::stance::{stance_panel, stream, FlipKind, Interner, ReadOut}; +use lance_graph_planner::nars::{BeliefArena, CStmt}; +``` + +### Construction per owner — verbatim call site (`run_lens`, `d_ign_b_lenses.rs:690-706`) + +```rust +fn run_lens(z: u8, verses: &[(String, String)]) -> LensReadout { + let mut arena = BeliefArena::new(); + let mut intern = Interner::new(); + let mut out = ReadOut::default(); + stream(verses, &mut arena, &mut intern, &mut out, false); + let (hegel, nietzsche, kant, wittgenstein) = stance_panel(&arena, &intern, &out); + match z { + 1 => LensReadout::Hegel(hegel), + 2 => LensReadout::Nietzsche(nietzsche), + 3 => LensReadout::Kant(kant), + 4 => LensReadout::Wittgenstein(wittgenstein), + other => panic!("run_lens: z={other} is outside the armed range 1..=4 …"), + } +} +``` + +**One arena is built fresh per call** — there is no shared/cached arena +across owners in this file. `stream`'s exact signature +(`stance.rs:161-167`): + +```rust +pub fn stream( + verses: &[(String, String)], // (label, text) pairs + arena: &mut BeliefArena, + intern: &mut Interner, + out: &mut ReadOut, + pass2: bool, +); +``` + +`(String, String)` pairs are built by `labelled_verses` (`d_ign_b_lenses.rs:395-402`): +label format `"kjv:{global_index:05}"`, matching `blw_fusion.rs:913`'s +subject format (`format!("kjv:{row:05}")`) — the SAME subject-string +convention both files use, confirmed by direct read of both sites. + +### Readouts derived — `stance_panel` (`stance.rs:469-478`, quoted verbatim) + +```rust +#[allow(clippy::type_complexity)] +pub fn stance_panel( + arena: &BeliefArena, + intern: &Interner, + out: &ReadOut, +) -> ( + Vec<(CStmt, f32)>, // Hegel: Aufhebung ranking + Vec<(CStmt, FlipKind)>, // Nietzsche: genealogy partition + Vec<(String, f32, f32)>, // Kant: (lift label, graded quale, ablated quale) + Vec<(u16, usize)>, // Wittgenstein: (concept, distinct games) +) +``` + +This is ONE call returning all four stances as one 4-tuple — there is no +per-stance dispatch function. `d_ign_b_lenses.rs` never calls +`contradiction_ranking` directly for its lens selection (it goes through +`stance_panel`, which itself calls `contradiction_ranking` internally for +the Hegel element, `stance.rs:480`). + +### `LensReadout` — probe-local type, NOT a shipped contract type (`d_ign_b_lenses.rs:614-685`) + +```rust +#[derive(Debug)] +enum LensReadout { + Hegel(Vec<(CStmt, f32)>), + Nietzsche(Vec<(CStmt, FlipKind)>), + Kant(Vec<(String, f32, f32)>), + Wittgenstein(Vec<(u16, usize)>), +} +impl LensReadout { + fn is_empty(&self) -> bool; + /// Stable fold over the variant's own contents (floats via .to_bits()). + /// Deliberately NO variant-discriminant tag (falsifiability rule — + /// see the doc comment at d_ign_b_lenses.rs:632-646). + fn digest(&self) -> u64; +} +``` + +`digest()` folds ONLY the variant's payload, never a type tag — two +EMPTY readouts of different lenses hash equal by design (this is called +out explicitly as load-bearing for the L3/L4 non-vacuity checks). + +### `run_all_lenses` — single-owner cross-lens helper (`d_ign_b_lenses.rs:710-722`) + +```rust +fn run_all_lenses(verses: &[(String, String)]) -> [LensReadout; 4] +``` +Same construction as `run_lens` but keeps all four tuple elements instead +of selecting one. + +### Selection ordinal source — `owner.meta_at(0).thinking()` + +`d_ign_b_lenses.rs:575,858`: the arming ordinal `z` is read from +`owner.meta_at(0).thinking()` — a `MetaWord`'s packed 6-bit `thinking` +field (see §F for `MetaWord`). This is the SAME field +`plan_context_for`/`thinking_style_for` (§F) consume for the +`StyleStrategy` dispatch input — one field, two consumers (lens +selection vs `ThinkingStyle` mapping), as the module doc's "deviation 2" +states explicitly. + +--- + +## C. `jc` oracle — `crates/jc/src/stats.rs` + +### `BinaryAssociation` — full struct (`stats.rs:612-634`) + +```rust +/// A 2x2 contingency table with both marginals, agreement decomposition, +/// and the two association coefficients that read off it. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct BinaryAssociation { + pub n00: u64, // count of (false, false) + pub n01: u64, // count of (false, true) + pub n10: u64, // count of (true, false) + pub n11: u64, // count of (true, true) + pub positive_rate_a: f64, // rate of true in rater A + pub positive_rate_b: f64, // rate of true in rater B + pub observed_agreement: f64, // p_o + pub expected_agreement: f64, // p_e (chance agreement from marginals) + pub kappa: Option, // Cohen's kappa; None iff p_e == 1 + pub phi: Option, // phi; None iff either variable constant +} +``` + +### Constructor path from two `&[bool]` (`stats.rs:653-693`) + +```rust +pub fn binary_association(a: &[bool], b: &[bool]) -> Option +``` + +Returns `None` ONLY on structurally unusable input (length mismatch or +empty). A degenerate table (all-same-category) still returns +`Some(BinaryAssociation { .. })` with `kappa`/`phi` individually `None` +— the counts/marginals/agreement fields remain populated even when the +coefficients are undefined. `phi` is delegated to +`crate::reliability::pearson` on the two vectors cast to `0.0/1.0` f64 +(`stats.rs:596-600`); `kappa` is computed inline from the 2x2 counts +(NOT delegated — it duplicates the arithmetic of the standalone +`cohen_kappa` function for the binary case, since `cohen_kappa` takes +`&[usize]` category labels rather than a pre-tabulated 2x2). + +### Degeneracy contracts, precisely + +- **`kappa == None`** iff `expected_agreement == 1.0` (or non-finite) — + i.e. `p_e == 1`, which happens when the marginals make chance agreement + certain (both raters use one identical category throughout, or the + complementary boundary case). Doc comment (`stats.rs:630`): "or `None` + when `p_e == 1` (undefined, `0/0`)." +- **`phi == None`** iff either input vector is constant (all-true or + all-false — zero variance), per `pearson`'s own contract (`phi`'s doc + comment, `stats.rs:588`: "Returns `None` under [`pearson`]'s + conditions: lengths differ, `n < 2`, or either vector is constant."). +- Neither degeneracy voids the OTHER field: a run can get `kappa: None, + phi: Some(x)` or vice versa, since they degenerate under DIFFERENT + conditions (kappa on `p_e==1`, phi on constancy of either input alone). + +### `blw_fusion.rs`'s call site — verbatim (`examples/blw_fusion.rs:91,1115-1118`) + +```rust +use jc::stats::{binary_association, BinaryAssociation}; +// … +let assoc_zz = binary_association(&z_strict, &z_aware); +if let Some(zz) = assoc_zz { + print_association_table("G2 kappa(Z,Z)", &zz); +} +``` + +`print_association_table` (`blw_fusion.rs:662-682`) takes `&BinaryAssociation` +and formats every field (never a bare kappa/phi scalar) — the C8 +correction cited in that file's module doc ("every kappa ships the FULL +`BinaryAssociation` table"). + +### `jc` dependency wiring — confirmed by direct Cargo.toml reads + +- **`lance-graph-planner/Cargo.toml`** (`[dev-dependencies]`, read in + full): `jc = { path = "../jc" }` — **dev-dependency only**, with an + explicit comment: "**dev-only, never a production dependency of the + planner**." This is what makes `examples/blw_fusion.rs` (an example, + which compiles under dev-deps) able to `use jc::stats::...`. +- **`lance-graph-supervisor/Cargo.toml`** (full file read): dependencies + are `lance-graph-callcenter`, `lance-graph-contract`, + `lance-graph-planner` (optional, `cycle-driver` feature), + `thiserror`, `tracing`, `ractor` (optional), `static_assertions` + (optional), `tokio` (optional); `[dev-dependencies]` are `tokio`, + `static_assertions`, `cognitive-shader-driver`. **`jc` appears + NOWHERE in this manifest**, direct or dev. + +### The load-bearing placement consequence (verified, stated precisely) + +- **`crates/lance-graph-supervisor/tests/*.rs`** (e.g. + `d_ign_b_lenses.rs`, `probe_ignition.rs`) compile against + `lance-graph-supervisor`'s own dependency graph. That graph has NO + `jc` edge (direct or transitive-usable — Rust does not let a crate + `use` a dependency's OWN dependency unless it is re-exported, and `jc` + is not re-exported by `lance-graph-planner`). **A supervisor test + file cannot `use jc::stats::*` without a manifest change to + `lance-graph-supervisor/Cargo.toml`.** This is exactly why + `d_ign_b_lenses.rs`'s own module doc says (verbatim, lines 42-43): + "`jc` is also not a dependency of this crate (`lance-graph-supervisor/ + Cargo.toml` has no `jc` edge — a manifest change, not a worker's call)." +- **`crates/lance-graph-planner/examples/*.rs`** (e.g. `blw_fusion.rs`) + compile against `lance-graph-planner`'s dev-dependency graph, which + DOES include `jc`. But planner **examples** cannot reach + `lance_graph_supervisor::cycle_driver::run_cycle` / + `run_cognitive_work_gated_over` — `lance-graph-planner`'s own + Cargo.toml (read in full above) has NO dependency, dev or otherwise, + on `lance-graph-supervisor` (confirmed: the dependency edge is + ONE-WAY, supervisor -> planner, per `lance-graph-supervisor`'s own + module doc at `cycle_driver.rs:41-45`: "This driver depends **one-way** + on the planner … planner never deps supervisor — no cycle"). + +**Precise statement for D-BLW-5 placement:** today, NEITHER crate sees +BOTH `jc::stats::binary_association` AND +`lance_graph_supervisor::cycle_driver::run_cycle` from the same +compilation unit. A planner example sees `jc` but not `run_cycle`; a +supervisor test sees `run_cycle` (via `cycle-driver` feature) but not +`jc`. Any D-BLW-5 test that needs BOTH the fusion oracle AND the real +cycle-driven cast/scan/seal machinery in one file requires adding `jc` +as a dev-dependency of `lance-graph-supervisor/Cargo.toml` — a manifest +change outside a Sonnet grindwork lane's scope (guardrails §5.2: "A +needed type/lane/mask does not exist" / dependency wiring — STOP+report, +needs orchestrator/operator sign-off, not silently assumed here). + +--- + +## D. Percentile / bucketing — `ndarray::simd::cascade` and `ndarray::hpc::statistics::percentile` reachability + +Sibling repo: `/home/user/ndarray` (read directly; NOT part of this +lance-graph checkout). + +### `ndarray::simd::cascade` — REACHABLE, confirmed re-export + +`/home/user/ndarray/src/simd.rs:626-633` (verbatim): + +```rust +// The Belichtungsmesser — banded multi-resolution cascade search +// (`Cascade::expose(distance) → Band`, `recalibrate(ShiftAlert)`, +// `PackedDatabase`, `adaptive_resolution`). Trampolined as a whole module so +// consumers under the "all SIMD from `ndarray::simd`" invariant reach the +// exposure-meter surface as `ndarray::simd::cascade::*` without dipping into +// `crate::hpc` directly. Module alias, not an item list — new cascade items +// arrive here without a re-export edit. Same `std` gate as this module. +pub use crate::hpc::cascade; +``` + +The underlying module is `/home/user/ndarray/src/hpc/cascade.rs`. Feature +gating: `pub mod simd;` in `ndarray/src/lib.rs:241` is `#[cfg(feature = +"std")]` only (NOT gated on `hpc-extras`); `pub mod hpc;` at +`ndarray/src/lib.rs:500` is likewise `#[cfg(feature = "std")]` only. +`std` is in ndarray's `default` feature set (`Cargo.toml:276`: +`default = ["std", "hpc-extras"]`), so `ndarray::simd::cascade::*` is +reachable under plain default features — `hpc-extras` is not required +for this specific path (though it happens to be enabled too wherever +ndarray is pulled with defaults). + +`Cascade`'s public surface (`ndarray/src/hpc/cascade.rs`, grepped +signatures): + +```rust +pub struct RankedHit { /* … */ } +pub enum Band { /* Foveal / … / Reject, per test at cascade.rs:472-473 */ } +pub struct ShiftAlert { /* … */ } +pub enum PreciseMode { /* … */ } +pub struct Cascade { /* … */ } +impl Cascade { + pub fn mu(&self) -> f64; + pub fn sigma(&self) -> f64; + pub fn observations(&self) -> usize; + pub fn from_threshold(threshold: u64, vec_bytes: usize) -> Self; + pub fn calibrate(distances: &[u32], vec_bytes: usize) -> Self; + pub fn expose(&self, distance: u32) -> Band; + pub fn test(&self, a: &[u8], b: &[u8]) -> bool; + pub fn observe(&mut self, distance: u32) -> Option; + pub fn recalibrate(&mut self, alert: &ShiftAlert); + pub fn query(&self, query: &[u8], database: &[u8], vec_bytes: usize, + num_vectors: usize) -> Vec; + pub fn query_candidates(/* … */) -> /* … */; + pub fn query_precise(/* … */) -> /* … */; +} +pub fn adaptive_resolution(query_entropy: f32, corpus_cv: f32) -> Band; +pub struct PackedDatabase { /* … */ } +impl PackedDatabase { + pub fn pack(database: &[u8], vec_bytes: usize) -> Self; + pub fn cascade_query(&self, query: &[u8], cascade: &Cascade, top_k: usize) + -> Vec; +} +``` + +(Field-level detail of `RankedHit`/`Band`/`ShiftAlert`/`PreciseMode` NOT +individually verified beyond the grep of struct/impl headers above — see +Not Verified section.) + +### `ndarray::hpc::statistics::percentile` — REACHABLE, as a trait method + +`percentile` is NOT a free function — it is a method on the +`Statistics` trait (`ndarray/src/hpc/statistics.rs:22-41`, verbatim): + +```rust +pub trait Statistics { + fn median(&self) -> A; + fn variance(&self) -> A; + fn var_axis(&self, axis: Axis) -> Array; + fn std_dev(&self) -> A; + fn std_axis(&self, axis: Axis) -> Array; + /// Percentile (0-100). Uses linear interpolation between nearest ranks. + fn percentile(&self, p: A) -> A; + fn sorted(&self) -> Array; + fn argmin(&self) -> usize; + fn argmax(&self) -> usize; + fn top_k(&self, k: usize) -> (Vec, Vec); + fn cumsum(&self) -> Array; + fn cosine_similarity(&self, other: &Self) -> A; + // (module continues past the grepped window — not all methods listed) +} +``` + +Usage requires `use ndarray::hpc::statistics::Statistics;` in scope (the +trait method, called as `x.percentile(50.0)`), per the module's own +doctest (`statistics.rs:14-21`). Module path `ndarray::hpc::statistics` +is public (`pub mod statistics;` in `ndarray/src/hpc/mod.rs:27`), gated +by the same `#[cfg(feature = "std")]` on `pub mod hpc;` noted above. + +### Reachability from THIS workspace's crates — verified by manifest read + +- **`lance-graph-planner/Cargo.toml`** `[dependencies]` (full read): + ```toml + ndarray = { path = "../../../ndarray", default-features = false, + features = ["std", "hpc-extras"] } + ``` + `std` is explicitly enabled, so `ndarray::simd::cascade` and + `ndarray::hpc::statistics::Statistics::percentile` are BOTH reachable + from `lance-graph-planner` (its lib code and its examples, including + `blw_fusion.rs`) — direct dependency, non-optional, always compiled. +- **`lance-graph-supervisor/Cargo.toml`** (full read, quoted above under + §C): **no `ndarray` dependency at all**, direct or dev. A + `lance-graph-supervisor` test file (`tests/d_ign_b_lenses.rs`, + `tests/probe_ignition.rs`, or a new D-BLW-5 test) **cannot** `use + ndarray::...` of any kind without adding `ndarray` to + `lance-graph-supervisor/Cargo.toml` — the SAME class of gap as the + `jc` gap in §C (a manifest change, not something the existing crate + graph already grants). + +**Plain statement for D-BLW-5:** if percentile/bucketing work is wanted +INSIDE `lance-graph-supervisor`'s test tree (alongside the real +`run_cycle`/`MailboxSoA` machinery `d_ign_b_lenses.rs` and +`probe_ignition.rs` already use), that is currently impossible without a +manifest edit. It IS possible today from `lance-graph-planner` (lib code +or examples), where `ndarray` is already a live dependency with `std` +enabled. + +--- + +## E. Version stamping — `blw_fusion.rs` and `persist_sink.rs` + +### How sealed versions are obtained + +- **`sink.head()`** (a caller-defined helper on the in-process `MemWal` + fake, NOT a `WalSink` trait method — `blw_fusion.rs:417-424`, + identical shape in `d_ign_b_lenses.rs:316-323`): + ```rust + fn head(&self) -> DatasetVersion { + self.sealed.lock().expect("MemWal poisoned") + .last().map_or(DatasetVersion(0), |s| s.version) + } + ``` + This reads the LAST sealed `DatasetVersion` from the fake's own + internal `Vec` — it is test/example-harness scaffolding, + not part of the shipped `persist_sink`/`cycle_driver` API. + +- **The shipped version source is `persist_cycle`'s return value** + (`persist_sink.rs:335-362`, verbatim signature): + ```rust + pub async fn persist_cycle( + sink: &S, + frame: CycleFrame, + casts: Vec, + ) -> Result + ``` + `blw_fusion.rs:872` calls it directly: + ```rust + let version = persist_cycle(&sink, CycleFrame::new(spec.id, base), slots).await?; + ``` + and stamps it: `let vc: LanceVersion = version.0; sealed_versions.insert(c, vc);` + (`blw_fusion.rs:895-896`) — `DatasetVersion` is a `pub struct + DatasetVersion(pub u64)` (`lance-graph-contract/src/scheduler.rs:36`), + so `.0` is the raw `u64` and `LanceVersion` (from + `lance_graph_planner::temporal`) is a type alias/newtype over the same + representation used as the `deinterlace`/`QueryReference::at` horizon. + +- **In `d_ign_b_lenses.rs`**, the higher-level `run_cycle` wraps both the + seal and the apply in one call (`cycle_driver.rs:446-471`, quoted in + full in §F) and returns `CycleOutcome { sealed: SealedCycle, applied: + AppliedCycle, held: Vec }`; the version is + `outcome.sealed.version` (field on `SealedCycle`, + `cycle_driver.rs:103-114`): + ```rust + pub struct SealedCycle { + pub version: DatasetVersion, + pub transitions: Vec, + pub next_position_base: u64, + } + ``` + `d_ign_b_lenses.rs` itself never reads `outcome.sealed.version` + directly (it reads `outcome.sealed.next_position_base` and + `outcome.sealed.transitions`, `d_ign_b_lenses.rs:918,923-949`) — but + the field is there and is the version-stamp equivalent of + `blw_fusion.rs`'s `persist_cycle` return. + +### What a version-stamped one-shot record would key on + +A `SweepSlot` (`persist_sink.rs:127-151`, the durable-write unit both +files use) carries NO version field itself — the version is assigned +AFTER sealing, one per whole cycle, not per slot: + +```rust +pub struct SweepSlot { + pub cycle: CycleId, + pub stream_position: u64, // cross-cycle monotonic order key + pub owner: MailboxId, + pub row: u64, + pub paired_move: Option, + pub payload: Vec, +} +``` + +The version-stamp KEY, as read back, is `LandedSlot` +(`persist_sink.rs:157-161`): + +```rust +pub struct LandedSlot { + pub version: DatasetVersion, // the version its CYCLE sealed into + pub slot: SweepSlot, +} +``` + +So a version-stamped one-shot record's natural key is +`(version: DatasetVersion, slot.owner: MailboxId, slot.row: u64)` or +`(version, slot.stream_position)` — `blw_fusion.rs`'s own +`VerdictRow::lance_version()` (its `DeinterlaceRow` impl, +`blw_fusion.rs:231-247`) keys on exactly this: `horizon: u64` set from +`vc` (the `persist_cycle`-returned version's `.0`), paired with +`subject: String` (the row's stable text key, `"kjv:NNNNN"`). + +`CycleFrame` (`persist_sink.rs:104-120`) is the storage-identity input +side (`{ cycle: CycleId, base_version: DatasetVersion }`, constructed via +`CycleFrame::new(cycle, base_version)`) — it carries the SEALED +PREDECESSOR a cycle reads, not the version it produces; the produced +version only exists after `commit_cycle`/`persist_cycle` returns. + +--- + +## F. MetaWord + cohort scaffolding — reusable helper signatures + +All of these are copied (with provenance comments) between +`probe_ignition.rs` and `d_ign_b_lenses.rs`; signatures below are +identical in both files unless noted. + +### `MetaWord` (`lance-graph-contract/src/cognitive_shader.rs:44-76`) + +```rust +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +#[repr(transparent)] +pub struct MetaWord(pub u32); // thinking(6) + awareness(4) + nars_f(8) + nars_c(8) + free_e(6) + +impl MetaWord { + pub const fn new(thinking: u8, awareness: u8, nars_f: u8, nars_c: u8, free_e: u8) -> Self; + pub fn thinking(&self) -> u8; // low 6 bits + pub fn awareness(&self) -> u8; // next 4 bits + pub fn nars_f(&self) -> u8; + pub fn nars_c(&self) -> u8; + pub fn free_e(&self) -> u8; +} +``` + +Both files construct arming via `MetaWord::new(armed, 0, 0, 0, 0)` and +read it back via `owner.meta_at(0).thinking()`. + +### `QualiaI4_16D` (`lance-graph-contract/src/qualia.rs:175-208`) + +```rust +#[repr(C, align(8))] +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default)] +pub struct QualiaI4_16D(pub u64); +impl QualiaI4_16D { + pub const ZERO: Self; + pub fn get(self, dim: usize) -> i8; + pub fn set(&mut self, dim: usize, value: i8); + pub fn with(self, dim: usize, value: i8) -> Self; // builder-shape, clamps -8..7 +} +``` + +`flow_qualia()` helper (`d_ign_b_lenses.rs:193-195`, provenance-noted as +re-derived from `cycle_driver.rs:1669`'s test fixture): +```rust +fn flow_qualia() -> QualiaI4_16D { + QualiaI4_16D(0).with(3, 4).with(14, 3).with(9, 4).with(1, 2) +} +``` + +### `gate_decision_i4` (`lance-graph-contract/src/mul.rs:575`, inside `pub mod i4_eval`) + +```rust +pub fn gate_decision_i4(qualia: &QualiaI4_16D, signed_mantissa: i8) -> GateDecision; +``` +Reachable as `lance_graph_contract::mul::i4_eval::gate_decision_i4`. + +### `mantissa_of` (`d_ign_b_lenses.rs:187-189`, identical shape in `probe_ignition.rs`) + +```rust +fn mantissa_of(owner: &Tenant) -> i8 { + owner.pending_count().min(7) as i8 +} +``` +`Tenant = MailboxSoA` (a type alias local to the test file). + +### Fleet construction — bloom-plane seeding helpers (identical across both files) + +```rust +const BLOOM_K: usize = 4; +fn fnv1a(bytes: &[u8], seed: u64) -> u64; +fn bloom_add(plane: &mut [u64], token: &str, salt: u64); +fn tokens(text: &str) -> impl Iterator + '_; +fn encode_plane(text: &str, salt: u64) -> Vec; // Vec of WORDS_PER_FP words +``` + +`WORDS_PER_FP` is `cognitive_shader_driver::mailbox_soa::WORDS_PER_FP = +256` (256 u64 words = 16,384-bit identity plane; +`mailbox_soa.rs:36-39`). + +### `build_owner` (`d_ign_b_lenses.rs:404-438`, verbatim signature) + +```rust +fn build_owner( + id: MailboxId, + verses: &[String], + content_salt: u64, + armed: u8, + qualia: QualiaI4_16D, + firing_rows: usize, +) -> Tenant +``` +Body: `MailboxSoA::new(id, TENANT_W_SLOT, TENANT_THRESHOLD)`, per-row +`WriteCell` write via `owner.write_row(row, cycle, &cell)` (asserting +`WriteOutcome::Accepted`), `owner.set_populated(verses.len())`, +`owner.tick()`, then `owner.energy[r] = FIRE_ENERGY` for `r in +0..firing_rows` (direct field write — `energy: [f32; N]` is `pub`, see +§ below). + +### `MailboxSoA` — full public method list (`cognitive-shader-driver/src/mailbox_soa.rs`, grepped) + +```rust +impl MailboxSoA { + pub fn new(mailbox_id: MailboxId, w_slot: u8, threshold: f32) -> Self; // panics if w_slot >= 64 + pub fn apply_edges(&mut self, deliveries: &[(u16, CausalEdge64)]) -> usize; + pub fn consume_firing(&mut self, row: usize) -> bool; + pub fn tick(&mut self); + pub fn write_row(&mut self, row: usize, cycle: u32, cell: &WriteCell<'_>) -> WriteOutcome; + pub fn last_write_cycle_at(&self, row: usize) -> u32; + pub fn stale_write_count(&self) -> u64; + pub fn populated(&self) -> usize; + pub fn set_populated(&mut self, n: usize); + pub fn reset_row(&mut self, row: usize); + pub fn energy_at(&self, row: usize) -> f32; + pub fn plasticity_at(&self, row: usize) -> u8; + pub fn cycle(&self) -> u32; + pub fn w_slot(&self) -> u8; + pub fn pending_count(&self) -> usize; + pub fn edge(&self, row: usize) -> CausalEdge64; + pub fn set_edge(&mut self, row: usize, e: CausalEdge64); + pub fn qualia_at(&self, row: usize) -> QualiaI4_16D; + pub fn set_qualia(&mut self, row: usize, q: QualiaI4_16D); + pub fn meta_at(&self, row: usize) -> MetaWord; + pub fn set_meta(&mut self, row: usize, m: MetaWord); + pub fn entity_type_at(&self, row: usize) -> u16; + pub fn set_entity_type(&mut self, row: usize, t: u16); + pub fn temporal_at(&self, row: usize) -> u64; + pub fn set_temporal(&mut self, row: usize, t: u64); + pub fn expert_at(&self, row: usize) -> u16; + pub fn set_expert(&mut self, row: usize, e: u16); + pub fn sigma_at(&self, row: usize) -> u8; + pub fn set_sigma(&mut self, row: usize, s: u8); + pub fn content_row(&self, row: usize) -> &[u64]; + pub fn set_content(&mut self, row: usize, words: &[u64]); + pub fn topic_row(&self, row: usize) -> &[u64]; + pub fn set_topic(&mut self, row: usize, words: &[u64]); + pub fn angle_row(&self, row: usize) -> &[u64]; + pub fn set_angle(&mut self, row: usize, words: &[u64]); + pub fn cast_on_behalf

(/* … */); + pub fn set_style_lane(&mut self, row: usize, lane: StyleLane, atoms: [u8; 12]); + pub fn set_style_atom(&mut self, row: usize, lane: StyleLane, family: u8, atom: u8); + pub fn promote_family(&mut self, row: usize, family: u8) -> bool; +} +// Also implements MailboxSoaView + MailboxSoaOwner (contract traits; +// gives .mailbox_id(), .phase(), .current_cycle(), .n_rows(), .try_advance_phase(), etc.) +``` + +**Direct public field access used by both test files (not accessor +methods):** `owner.energy[r] = FIRE_ENERGY` (`d_ign_b_lenses.rs:436`, +`blw_fusion.rs:519`) — `pub energy: [f32; N]` is a genuinely public +struct field (`mailbox_soa.rs:66`), so this is legal direct indexing, +not a method call. Likewise `owner.mailbox_id` is `pub` (`mailbox_soa.rs:61`), +though both test files use the `.mailbox_id()` trait accessor instead. + +`WriteCell<'a>` (`mailbox_soa.rs:262-283`, all fields `Option<...>`, +`#[derive(Debug, Clone, Default)]` so `..WriteCell::default()` works): +```rust +pub struct WriteCell<'a> { + pub content: Option<&'a [u64]>, + pub topic: Option<&'a [u64]>, + pub angle: Option<&'a [u64]>, + pub edge: Option, + pub qualia: Option, + pub meta: Option, + pub entity_type: Option, + pub temporal: Option, + pub expert: Option, + pub sigma: Option, +} +``` + +`WriteOutcome` (`mailbox_soa.rs:241-254`): `enum { Accepted, Stale, Future }`. + +### Scan / column-pass helpers (`d_ign_b_lenses.rs:514-571`, identical shape to `probe_ignition.rs`) + +```rust +struct ScanResult { + planning: Vec, + cognitive: Vec, + evaluation: Vec, + absorbed: Vec, + missing: usize, +} +fn scan_board(fleet: &Fleet, ids: impl IntoIterator) -> ScanResult; + +struct ColumnPassOutcome { cast: usize } +fn column_pass( + fleet: &Fleet, + ids: &[MailboxId], + writer: &mut BatchWriter>, + think: impl FnMut(&Tenant) -> Option<(StrategyOutcome, Vec)>, +) -> ColumnPassOutcome; +``` + +### The cycle-driver seam — shipped signatures (`lance-graph-supervisor/src/cycle_driver.rs`) + +```rust +pub struct CycleOutcome { + pub sealed: SealedCycle, + pub applied: AppliedCycle, + pub held: Vec, +} +pub enum CycleError { + Seal(Box), + Apply { partial: AppliedCycle, cause: PersistError }, +} +pub async fn run_cycle( + sink: &S, + fleet: &mut F, + writer: &mut BatchWriter>, + frame: CycleFrame, + position_base: u64, + watermarks: &mut HashMap>, + row_of: impl FnMut(MailboxId) -> u64, +) -> Result +where S: WalSink, F: MailboxFleet; + +pub struct CognitiveWorkOutcome { pub cast: usize, pub held_owners: Vec } + +pub fn run_cognitive_work_gated_over( + fleet: &F, + owners: &[MailboxId], + writer: &mut BatchWriter>, + read_gate: impl FnMut(&F::Owner) -> Option<(QualiaI4_16D, i8, f32, Vec)>, +) -> CognitiveWorkOutcome +where F: MailboxFleet; + +#[must_use] +pub fn shade_owner( + owner: &O, + qualia: &QualiaI4_16D, + mantissa: i8, + reliability: f32, +) -> Option; +``` + +`d_ign_b_lenses.rs`'s exact `run_cognitive_work_gated_over` call +(`d_ign_b_lenses.rs:853-882`) embeds the lens capture (§B) INSIDE the +`read_gate` closure — this is "design §1's chosen seam" per the file's +own module doc. + +### `emit_bootstrap_intent` (`lance-graph-planner/src/owner_adapter.rs:92-101`) + +```rust +pub fn emit_bootstrap_intent

( + outcome: &StrategyOutcome, + owner: MailboxId, + owner_cycle: u32, + writer: &mut BatchWriter

, + payload: P, +) -> Option; +``` + +### `BatchWriter

` (`lance-graph-planner/src/batch_writer.rs`) + +```rust +pub struct CastId(pub u64); +pub struct BatchWriter

{ /* private fields */ } +impl

BatchWriter

{ + pub fn new() -> Self; + pub fn cast(&mut self, on_behalf: MailboxId, moves: Vec, payload: P) -> CastId; + pub fn casts(&self) -> Vec; + pub fn intent_moves(&self, cast: CastId) -> Option<&[KanbanMove]>; + pub fn on_behalf_of(&self, cast: CastId) -> Option; + pub fn resolve_owner(&mut self, on_behalf: MailboxId, + resolver: impl FnOnce(MailboxId) -> MailboxId) -> (MailboxId, bool); + pub fn drain_pending_payloads(&mut self) -> impl Iterator + '_; +} +``` + +### `StyleStrategy` / `PlanContext` dispatch surface (`lance-graph-planner/src/strategy/style_strategy.rs`) + +```rust +pub struct StyleStrategy; +impl StyleStrategy { + pub fn reliability_for(style: ThinkingStyle, ctx: &PlanContext) -> f32; +} +impl PlanStrategy for StyleStrategy { /* .plan(PlanInput, &mut Arena) -> … */ } +``` +`d_ign_b_lenses.rs`'s local helpers around this (`d_ign_b_lenses.rs:158-185`): +```rust +fn thinking_style_for(z: u8) -> ThinkingStyle; // 1=>Analytical, 2=>Creative, _=>Reflective +fn style_vector_for(z: u8) -> Vec; // 23-length one-hot vector +fn plan_context_for(z: u8) -> PlanContext; +``` +`ThinkingStyle` (`lance-graph-contract/src/thinking.rs:23-25` onward) — +confirmed variants include `Logical = 0`, `Analytical = 1`, and (per the +existing d_ign_b_lenses.rs comment, not independently re-verified here +beyond a grep count) 36 total variants across 6 clusters +(τ 0x40-0x4F etc. per doc comments) — `Creative` and `Reflective` were +NOT individually grepped for their discriminant values in this pass (see +Not Verified). + +### `DatasetVersion` (`lance-graph-contract/src/scheduler.rs:36`) + +```rust +pub struct DatasetVersion(pub u64); +``` + +--- + +## NOT VERIFIED (explicit — do not guess from this list) + +1. **Field-level layout of `RankedHit` / `Band` / `ShiftAlert` / + `PreciseMode`** in `ndarray::hpc::cascade` — only struct/impl/fn + HEADERS were grepped (§D); bodies and exact field names were not read. + `Band`'s variant list beyond `Foveal`/`Reject` (seen in a cascade.rs + test at line 472-473) was not enumerated. +2. **The full `Statistics` trait member list** past `cosine_similarity` + (`statistics.rs` line ~60 onward) — the grepped window + (`statistics.rs:22-60`) may not be the complete trait; only the + members through `cosine_similarity` were read. +3. **`ThinkingStyle`'s complete 36-variant list and discriminant values** + for `Creative`/`Reflective` specifically — only `Logical`/`Analytical` + discriminants (0/1) were directly read; the file has ~10 lines + matching the enum-variant grep pattern used, which undercounts a + 36-variant enum (multi-variant or comment lines likely interfere with + the pattern) — this count is NOT reliable and was not corrected by a + full read of `thinking.rs`. +4. **`QueryReference::at`, `deinterlace`, `DeinterlaceRow`, `NoDeps`, + `LanceVersion`** (`lance_graph_planner::temporal`) — cited by + `blw_fusion.rs`'s import and call sites (quoted verbatim where seen) + but `temporal.rs` itself was NOT opened in this pass; only what + `blw_fusion.rs`'s own call sites and doc comments state about it is + reported above (§E's `VerdictRow::lance_version()` mapping). +5. **`MailboxSoaOwner` / `MailboxSoaView` trait method lists** — only the + methods actually called by the two test files (`phase()`, + `mailbox_id()`, `current_cycle()`, `n_rows()`, `try_advance_phase()`) + were confirmed by call-site read; the full trait definitions in + `lance-graph-contract/src/soa_view.rs` were not opened. +6. **`GateDecision` enum's complete variant list** — only the + `Block`/`Hold`/`Flow` arms visible in `gate_decision_i4`'s match + (`mul.rs:579-594`, partially read) were seen; the full enum + definition was not located/read. +7. **`Interner` full API** beyond `new()`, `id()`, `name()` — these three + were read in full from `stance.rs:50-87`; no further methods exist in + that file (this one IS complete, listed for clarity, not a gap). +8. **`probe_ignition.rs` in full** — this file was NOT read end-to-end; + only `d_ign_b_lenses.rs`'s own citations of it (line-numbered + provenance comments, e.g. "provenance: `probe_ignition.rs:604-638`") + were relied on for cross-file claims. Any helper unique to + `probe_ignition.rs` and NOT copied into `d_ign_b_lenses.rs` is not + inventoried here. +9. **Whether `jc::stats` exports anything else useful to D-BLW-5** + beyond `BinaryAssociation`/`binary_association` (e.g. `cohen_kappa`, + `phi`, `omega_total` — all read in §C's source pass and quoted in + their doc comments, but their exact call sites in THIS workspace + beyond `blw_fusion.rs`'s single `binary_association` use were not + searched for). + +--- + +## Summary of load-bearing findings for the orchestrator + +- **A/B are fully live and reusable as-is**: `BeliefArena::observe` / + `admit_derived` accept hand-built `CStmt`/`TruthValue`/`Stamp` with no + text-parsing dependency; `stance_panel` is one call returning all four + stances; `run_lens`'s exact shape in `d_ign_b_lenses.rs` is the + established pattern. +- **C and D share the SAME structural gap**: `jc` (fusion oracle) and + `ndarray` (percentile/cascade) are BOTH reachable only from + `lance-graph-planner` (lib + examples), and BOTH absent from + `lance-graph-supervisor`'s manifest (direct or dev). Neither gap can + be closed by a Sonnet edit-only lane without a `Cargo.toml` change — + this is a STOP+report item per guardrails §5.2, flagged here rather + than silently worked around. +- **E**: version stamping is per-CYCLE (`DatasetVersion` from + `persist_cycle`/`seal_cycle`/`run_cycle`), never per-row; a row-level + record keys on `(version, owner, row)` or `(version, subject_string)`, + read back as `LandedSlot { version, slot: SweepSlot }`. +- **F**: the fleet/scan/cast/cycle scaffolding in `d_ign_b_lenses.rs` is + a direct, unmodified copy of `probe_ignition.rs`'s pattern (each site + provenance-commented); a D-BLW-5 build can copy the same helpers + verbatim from either file. diff --git a/.claude/board/exec-runs/d-blw-5-design-main-thread.md b/.claude/board/exec-runs/d-blw-5-design-main-thread.md new file mode 100644 index 000000000..f25698537 --- /dev/null +++ b/.claude/board/exec-runs/d-blw-5-design-main-thread.md @@ -0,0 +1,143 @@ +# D-BLW-5 — design note (MAIN-THREAD authored, 2026-08-05) + +> **Provenance:** the Opus design LANE for this deliverable was stopped by +> the operator mid-run and is not relaunched. This note is authored by the +> orchestrating main thread itself (Opus-class filigree per the model +> policy), which respects the agent stop while completing the synthesis the +> plan requires. It incorporates three inputs the stopped lane never saw: +> the operator's 64k main-model ORDER (E-64K-1TO1-OWNERS-IS-THE-MAIN-MODEL-1), +> the completed Sonnet inventory (`d-blw-5-api-inventory-sonnet.md`), and +> the orchestrator's placement ruling. +> **The BUILD is gated on the operator's word.** Nothing here dispatches. + +Binding doctrine: `.claude/knowledge/observer-effect-tfpn-doctrine.md` + +plan §12.9/§12.9a. Foundation: `tests/d_ign_b_lenses.rs` (GREEN) — the +per-owner arena runs in-cycle, selected by arming. + +--- + +## (a) Placement — supervisor tests, with the pre-authorized jc edge + +`crates/lance-graph-supervisor/tests/d_blw_5_observer.rs`, feature-gated +`#[cfg(feature = "cycle-driver")]` (the sibling pattern). Forced by the +single-system requirement of the single-measurement law: S₀ → inject → S₁ +must happen inside ONE evolving arena+loop process, and only the supervisor +sees `run_cycle` (inventory §C). The supervisor cannot reach `jc` today; +the ORCHESTRATOR-RATIFIED manifest change (the one exception): add +`jc = { path = "../jc" }` to lance-graph-supervisor `[dev-dependencies]`, +under the four D-BLW-3 constraints (dev-only; never production; never +modify `crates/jc`; never invert — jc stays zero-dep). `ndarray` stays +unreachable supervisor-side by design; the shape census is probe-local +(below) with a provenance comment citing `ndarray::simd::cascade` (on +ndarray master via merged PR #273) as the machinery it stands in for. + +## (b) Awareness coupling, the S readers, and the C6 firewall + +- **Coupling:** each armed owner runs the D-IGN-B lens body — `stream` + fills a per-owner `BeliefArena` from the owner's corpus slice (fresh + `Interner` per owner, the L1 id-independence discipline). +- **Injection is arena-native:** `BeliefArena::observe()` accepts + hand-built statements (inventory §A — no text path needed). The payload + is a small belief family, every member stamped with a probe-reserved + provenance marker (`Stamp`): 16 bucket-beliefs (census masses as truth + frequency) + 1 rank-belief (rank₀ as frequency). ELEVATED-rung semantics + are carried by the stamp, not a new field. +- **Propagation channel (what makes T non-vacuous):** after injection, one + post-injection reasoning pass runs (the same pass2/fixed-point step the + lens body already uses), so the injected testimony CAN interact via NARS + revision and derived closure. Without this pass, T-silence would be + structural, not measured. +- **The S readers (pre-registered):** two arena-derived binary readers + over the owner's verse-subjects — + A: "subject participates in ≥1 contradiction-ranked statement"; + B: "subject has a rung-lift record". + Pre-registered FALLBACK (the D-IGN-B pattern, pinned NOW, never chosen + after output): if either marginal is degenerate (0 or 1) on the UNARMED + control at V₀, substitute B′: "subject appears in ≥2 distinct + Wittgenstein games". S = `jc::stats::binary_association(A, B)` pooled + per arm-cohort. Full tables always (C2); κ and φ reported, never bare. +- **The C6 firewall, precisely:** (1) the readers EXCLUDE the verbatim + injected statements (matched by the probe-reserved stamp) — everything + downstream of revision is fair game, because that propagation IS the + measurand; (2) S₀/S₁ are computed in a measurement block AFTER the final + seal, and a compile-time self-scan (the sibling probes' `include_str!` + pattern, self-match-guarded) asserts the S identifiers appear nowhere + before the measurement marker — nothing in the loop can branch on them; + (3) jc is called only inside the measurement block — the one-way oracle. + +## (c) The payload, concretely + +At V₀ (per arm-cohort): compute each owner's per-subject association +contribution pool; census it into a probe-local 16-bucket histogram in +Fisher-2z space (equal-width in 2z ≈ equal-information; provenance comment +cites the Belichtungsmesser); rank₀ = the observed cohort statistic's +Prozentrang within the pooled prior distribution. shape₀ × rank₀ is ALL +that is injected — never a raw κ (the payload law). The ledger: +`BTreeMap<(StatId, Arm, Cohort, Metric, DatasetVersion), Sealed>` — +append-only; a second write at a sealed key ERRORS (the remeasure guard). + +## (d) The arms — cohorts of INDEPENDENT 1:1 owners (the 64k order) + +Per E-64K-1TO1-OWNERS-IS-THE-MAIN-MODEL-1: arms are COHORTS OF OWNERS, +1:1, mutation-exclusive — never row-partitions of one owner. Pinned: + +| cohort | owners | injection | +|---|---|---| +| T | 8 | shape₀ × TRUE rank₀ | +| F+ | 8 | shape₀ × rank shifted +1.5 logit units | +| F− | 8 | shape₀ × rank shifted −1.5 logit units (equal magnitude, opposite sign; anchors with true rank outside [0.05, 0.95] are EXCLUDED, never clipped) | +| P | 8 | structurally identical belief family, permuted-pool shape, zero information | +| N | 4 | the T-injection observed through the awareness-free bloom criterion (must stay frozen BY CONSTRUCTION; doubles as pool-drift baseline) | +| UNARMED | 4 | none (the degeneracy control for the reader fallback) | + +Cycle plan (pinned): c1 seed + lens pass → seal → **V₀** → measure S₀ per +arm (ledger-sealed) → c2 inject (arena writes, stamped; a WRITE, the +two-verbs doctrine) → post-injection pass → seal → **V₁** → measure S₁ +(fresh one-shots; S₀ is never recomputed — the single-measurement law). +Synchronous loop; fleet-level parallel stays A2-gated (not-claimed). + +## (e) Pre-registered gates (every one twinned, all numbers final NOW) + +- **O1 remeasure guard:** recompute at a sealed key ERRORS (can-fire); + fresh `(id, scope, V₁)` passes AND a different arm at the same + `(id, V)` passes (can-stay-silent). +- **O2 placebo:** |ΔS(P)| < 0.10 required — twin: P's injection mechanics + DID execute (stamped beliefs present in every P arena), so the silence + is informational, not absence. +- **O3 null instrument:** N's criterion byte-identical V₀→V₁ (frozen by + construction) — twin: N's own pool-drift measured and reported as the + baseline. +- **O4 the observable:** ΔS(T) = S₁−S₀ fires iff |Δκ| ≥ 0.10 (two-sided, + the corrected convention). BOTH outcomes are named results: fire = the + effect; silent at every floor = the honest null ("awareness does not + reflect this statistic"). +- **O5 direction test:** both F arms run; S₁ tracking the INJECTED rank = + anchoring/Goodhart (a finding even if T is silent); correcting TOWARD + truth = evidence-dominance; value-invariant movement = perturbation. +- **O6 firewall self-scan:** S identifiers absent before the measurement + marker (can-fire) + the scan finds the marker and the jc call (a scan + that finds nothing is not evidence). +- **O7 exclusion is load-bearing:** a SHADOW reader including the + injected statements yields a different S than the firewalled reader on + T-cohort arenas (can-fire) — and the firewalled reader is non-empty + (can-stay-silent). +- **DROP:** |Δκ| < 0.01 with zero reader-Hamming across BOTH cycles for + the arm — all-horizon Hamming, the C7-corrected form. + +## (f) Kill conditions (pre-accepted) + not-claimed + +Kills: P moves ⇒ instrument invalid (reported, not tuned); N moves ⇒ +plumbing leak, run void; T silent everywhere ⇒ honest null; F tracks +injected rank ⇒ the anchoring finding stands alone. Not-claimed: no +validity, no parallelism (A2-gated), no durability (MemWal), no fusion +verdict, no generalization past this corpus/instrument, no per-stance +dispatch (selection), no substrate-data-path claim from any readout +(F1b carries over verbatim). + +## (g) Open items for the operator + +1. **The build itself** — gated on your word (this note + the inventory + are the complete build inputs). +2. The supervisor+jc dev-dep lands with the build commit (pre-ratified). +3. If O4 fires, D-BLW-3b's arm C inherits this instrument as designed + (TD-BLW3B re-route). diff --git a/.claude/board/exec-runs/d-ign-b-api-inventory-sonnet.md b/.claude/board/exec-runs/d-ign-b-api-inventory-sonnet.md new file mode 100644 index 000000000..5d983e754 --- /dev/null +++ b/.claude/board/exec-runs/d-ign-b-api-inventory-sonnet.md @@ -0,0 +1,704 @@ +# D-IGN-B — API inventory (Sonnet, edit-only, no cargo run) + +Scope: four-stance machinery (`stance.rs`), the consumer precedent +(`probe_eyes_opened.rs`), the Fusion arm (`blw_fusion.rs`), the seam +(`cycle_driver.rs`), `MetaWord`, and the GREEN probe's reusable +helper signatures (`probe_ignition.rs`). Everything below is copied +verbatim from source; nothing was compiled or run by this lane. + +--- + +## A. Four-stance machinery — `crates/lance-graph-planner/src/nars/stance.rs` +(read in full) + `crates/lance-graph-planner/src/nars/mod.rs` + +### Module visibility + +`crates/lance-graph-planner/src/nars/mod.rs:20` declares `pub mod stance;` +and does **NOT** re-export any `stance::*` symbol via the crate's `pub use` +list (lines 25-41 re-export `basin_resonance`, `belief`, `dissolution`, +`elevation`, `epiphany`, `facet_fold`, `inference`, `insight`, `insights`, +`reach_out`, `regulate`, `tactic_select`, `tactics`, `truth` — `stance` is +absent from that list). Consequence: a consumer must import via the full +path `lance_graph_planner::nars::stance::{...}` (exactly what +`probe_eyes_opened.rs:87-89` and `blw_fusion.rs` do NOT do — `blw_fusion.rs` +does not touch `stance` at all; only `probe_eyes_opened.rs` imports it). + +**Nothing in `stance.rs` is `#[cfg(test)]`.** The whole file (lines 1-536) is +plain `pub`/private items with no `#[cfg(test)]` gate anywhere — every `pub` +item below is importable from another crate via the full path. + +### Every `pub` type, fn, and method — exact signatures (file:line) + +```rust +// line 50-53 +#[derive(Default)] +pub struct Interner { + map: HashMap, // private field + names: Vec, // private field +} + +// line 56 — impl Interner +pub fn new() -> Self + +// line 63 — impl Interner +pub fn id(&mut self, w: &str) -> u16 +// (panics via `assert!` past u16::MAX distinct strings — line 73-77) + +// line 84 — impl Interner +pub fn name(&self, id: u16) -> &str + +// line 90-99 +#[derive(Debug, Clone)] +pub struct Provenance { + pub verse: String, + pub stmt: CStmt, // from super::belief + pub negated: bool, +} + +// line 104-127 +#[derive(Debug, Clone)] +pub struct RungLift { + pub verse: String, + pub knower: u16, + pub verb: u16, + pub object: u16, + pub modal: f32, + pub cell: u8, + pub staunen_at: f32, + pub quale: f32, + pub self_referential: bool, +} + +// line 132-145 +#[derive(Default)] +pub struct ReadOut { + pub provenance: Vec, + pub lifts: Vec, + pub impls: Vec<(String, u16, u16)>, + pub pass2_admitted: usize, + pub pass2_revised: usize, +} + +// line 161-167 — the FREE FUNCTION (not a method on a carrier) +pub fn stream( + verses: &[(String, String)], + arena: &mut BeliefArena, + intern: &mut Interner, + out: &mut ReadOut, + pass2: bool, +) +// Return type: () (mutates `arena`, `intern`, `out` in place via &mut). + +// line 418 +pub fn contradiction_ranking(arena: &BeliefArena) -> Vec<(CStmt, f32)> + +// line 430-437 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FlipKind { + Transvaluation, + Devaluation, +} + +// line 468-478 — the B6 aspect panel +#[allow(clippy::type_complexity)] +pub fn stance_panel( + arena: &BeliefArena, + intern: &Interner, + out: &ReadOut, +) -> ( + Vec<(CStmt, f32)>, // Hegel: Aufhebung ranking + Vec<(CStmt, FlipKind)>, // Nietzsche: genealogy partition + Vec<(String, f32, f32)>, // Kant: (lift label, graded quale, ablated quale) + Vec<(u16, usize)>, // Wittgenstein: (concept, distinct games) +) +``` + +### Module-private (not `pub`, listed for completeness — cannot be +imported from another crate) + +- `const STOP: &[&str]` (line 33) — private. +- `const AUX: &[&str]` (line 43) — private. +- `Interner::map` / `Interner::names` fields — private (only via the pub + methods above). + +### What a stance run CONSUMES and RETURNS + +- **`stream(...)`** consumes: `verses: &[(String, String)]` (borrowed, + shared ref), `arena: &mut BeliefArena` (borrowed, exclusive — the arena is + MUTATED, not read-only: `stream` calls `arena.observe(...)` and + `arena.admit_derived(...)` internally, lines 281, 338), `intern: &mut + Interner` (borrowed, exclusive — new strings get interned during the + pass), `out: &mut ReadOut` (borrowed, exclusive — accumulates), `pass2: + bool` (owned `Copy`). Returns `()`. +- **`stance_panel(...)`** consumes: `arena: &BeliefArena` (borrowed, + SHARED-only — signature has no `&mut`, so mutation is impossible by + signature per the doc comment lines 447-448), `intern: &Interner` + (shared), `out: &ReadOut` (shared). Returns the 4-tuple above (all owned + `Vec`s — the readout is fully owned, no lifetime tied to `arena`). +- **Readout type fields and their derive status** (this is the + bit-identical-comparison-relevant fact): + - `CStmt` (from `belief.rs:77`, re-exported at `nars/mod.rs:26`): + `#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]` — **Eq + Hash + present**, fields `s: u16`, `cop: Copula`, `p: u16`. + - `Copula` (`belief.rs:54`): `#[derive(Debug, Clone, Copy, PartialEq, Eq, + Hash)]` — **Eq + Hash present**. + - `FlipKind` (`stance.rs:430`): `#[derive(Debug, Clone, Copy, PartialEq, + Eq)]` — **Eq present, Hash absent**. + - The `f32` fields inside the panel tuples (`Vec<(CStmt, f32)>`, + `Vec<(String, f32, f32)>`) mean **the panel's own tuple types do NOT + derive `Eq`/`Hash`** (f32 has no `Eq`); only `PartialEq`/`PartialOrd` + apply to those tuples as a whole. A bit-identical comparison gate on + the panel output must compare `f32` fields by bit pattern + (`.to_bits()`) or exact `==`, not derive-based `Eq`/`Hash` on the whole + tuple. + - `TruthValue` (`truth.rs:9-10`): `#[derive(Debug, Clone, Copy, + PartialEq)]` — **no `Eq`** (holds `f32` fields per `truth.rs:1-20` + read; confirmed no `Eq`/`Hash` derive on the struct line). + +### Stance selection — exact discriminants + +There is **no enum of "the four stances."** The four stances (Hegel / +Nietzsche / Kant / Wittgenstein) are **not** named by any enum discriminant +or const — they are four **hard-coded computation blocks inside the single +function `stance_panel`** (lines 479-534), each producing one element of +the returned 4-tuple in FIXED POSITIONAL ORDER: +- index 0 = Hegel (`hegel` local, line 480, delegates to + `contradiction_ranking`), +- index 1 = Nietzsche (`nietzsche` local, lines 483-496, inline loop), +- index 2 = Kant (`kant` local, lines 499-510, inline `.map()` with the + hard-coded `const UNIFORM_MODAL: f32 = 0.5` ablation), +- index 3 = Wittgenstein (`wittgenstein` local, lines 513-532, inline + `HashMap`-based game-counting). + +There is no per-stance function a caller can invoke individually — calling +`stance_panel` always computes all four. Selecting "just one stance" is not +a thing the API exposes. + +--- + +## B. The consumer precedent — `crates/lance-graph-planner/examples/probe_eyes_opened.rs` + +Exact call sites (verbatim): + +```rust +// import (lines 87-89) +use lance_graph_planner::nars::stance::{ + contradiction_ranking, stance_panel, stream, FlipKind, Interner, Provenance, ReadOut, RungLift, +}; + +// construction, in order (report(), lines 219-226) +fn report(label: &str, verses: &[(String, String)]) -> (BeliefArena, Interner, ReadOut) { + let mut arena = BeliefArena::new(); + let mut intern = Interner::new(); + let mut out = ReadOut::default(); + + // Pass 1 — the reading. + stream(verses, &mut arena, &mut intern, &mut out, false); + arena.close_transitive(64); + ... +``` + +Construction order, exactly: `BeliefArena::new()` → `Interner::new()` → +`ReadOut::default()` → `stream(verses, &mut arena, &mut intern, &mut out, +false)` (pass 1, `pass2=false`) → `arena.close_transitive(64)` (not part of +`stance.rs`; a `BeliefArena` method). + +Elsewhere in `main()` a second `stream` call re-presents the SAME verses +with `pass2=true` (line 301-302 area): +```rust +let mut pass2 = ReadOut::default(); +stream(verses, &mut arena, &mut intern, &mut pass2, true); +``` +(fresh `ReadOut`, SAME `arena`/`intern` reused — this is how the +hermeneutic-circle termination check is driven, per `stream`'s own doc +comment lines 159-160.) + +Stance-panel call sites (two, both after the corpus has been streamed + +closed): +```rust +// line 177 — inside print_stance_panel(arena, intern, out) +let (hegel, nietzsche, kant, wittgenstein) = stance_panel(arena, intern, out); +``` +```rust +// line 569 — inside main() +let (hegel, nietzsche, kant, wittgenstein) = stance_panel(&scene_arena, &intern, &out); +``` +Both call `stance_panel` on a `&BeliefArena` immediately after (not +interleaved with) the `stream`/`close_transitive` calls that built it — +`print_stance_panel` (lines 175-217) additionally snapshots +`arena.entries().len()` BEFORE calling `stance_panel` and re-asserts it +AFTER (lines 176, 212-216) to runtime-prove the "arena unchanged" claim in +the doc comment. + +`contradiction_ranking` is also called standalone, independent of the +panel, e.g. line 254 `let ranking = contradiction_ranking(&arena);` and +line 396, 421 — always on a `&BeliefArena` reference. + +--- + +## C. The Fusion arm — `crates/lance-graph-planner/examples/blw_fusion.rs` + +### Imports (lines 103-105) + +```rust +use lance_graph_planner::temporal::{ + deinterlace, DeinterlaceRow, LanceVersion, NoDeps, QueryReference, +}; +``` + +### `deinterlace` call sites (verbatim, all from the `#[tokio::test]` body +around lines 981-1400 — variable names as in source) + +```rust +// G1 — line 981-986 +let qref_strict_pin = QueryReference::at(v_pin, RUNG_STRICT); +let qref_aware_pin = QueryReference::at(v_pin, RUNG_AWARE); +let strict_v4 = deinterlace(&all_rows, &qref_strict_pin, &NoDeps); +let aware_v4 = deinterlace(&all_rows, &qref_aware_pin, &NoDeps); +``` +```rust +// line 1001-1004 +let qref_strict_v8 = QueryReference::at(v8, RUNG_STRICT); +let qref_aware_v8 = QueryReference::at(v8, RUNG_AWARE); +let strict_v8_rows = deinterlace(&all_rows, &qref_strict_v8, &NoDeps); +let aware_v8_rows = deinterlace(&all_rows, &qref_aware_v8, &NoDeps); +``` +```rust +// line 1025-1026 (G1c retro-only check) +let qref_retro_pin = QueryReference::at(v_pin, RUNG_RETRO); +let retro_v4 = deinterlace(&all_rows, &qref_retro_pin, &NoDeps); +``` +```rust +// line 1164 +let aware_v4_desc = deinterlace(&rows_desc, &qref_aware_pin, &NoDeps); +``` +```rust +// lines 1379-1382 (loop over some `vk` set) +let qref_s = QueryReference::at(vk, RUNG_STRICT); +let qref_a = QueryReference::at(vk, RUNG_AWARE); +let s_rows = deinterlace(&all_rows, &qref_s, &NoDeps); +let a_rows = deinterlace(&all_rows, &qref_a, &NoDeps); +``` + +`QueryReference::at(...)` constructions used: `(v_pin, RUNG_STRICT)`, +`(v_pin, RUNG_AWARE)`, `(v8, RUNG_STRICT)`, `(v8, RUNG_AWARE)`, `(v_pin, +RUNG_RETRO)`, `(vk, RUNG_STRICT)`, `(vk, RUNG_AWARE)` — always the two-arg +form `at(ref_version: LanceVersion, rung: u8)` (see `temporal.rs:167` +below); `RUNG_STRICT = 0`, `RUNG_AWARE = 5`, `RUNG_RETRO = 9` (consts at +`blw_fusion.rs:134,136,138`). + +**In every call site `deinterlace` is invoked against `&all_rows` (or +`&rows_desc`) — the WHOLE sealed corpus row set, not a single owner's +rows.** `all_rows` is built once (outside these excerpts, from the whole +tenant's emitted `VerdictRow`s across all cycles/projections) and reused +across every `deinterlace` call in the file; there is no call site anywhere +in `blw_fusion.rs` that passes a per-owner-filtered slice into +`deinterlace`. Consequence per the design comment at +`temporal.rs:332-345`: `deinterlace` needs the WHOLE sealed row set +(offline projection over everything emitted so far), not one owner's rows +— the fusion arm's own usage matches that shape exactly. + +### `DeinterlaceRow for VerdictRow` — required methods, exact signatures +(lines 219-247) + +```rust +#[derive(Clone, Debug)] +struct VerdictRow { + subject: String, // e.g. "kjv:00417" + horizon: u64, // the SEALED VERSION this verdict was computed from + projection: Proj, // A | B | Z + verdict: bool, +} + +impl DeinterlaceRow for VerdictRow { + fn subject(&self) -> &str { + &self.subject + } + fn lance_version(&self) -> LanceVersion { + self.horizon + } + /// CONSTANT `0` — a class-registration clock, NOT a per-row warrant time. + fn knowable_from(&self) -> LanceVersion { + 0 + } + // hlc_tick() DEFAULTED (not overridden) — trait default returns `None`. +} +``` + +The trait itself (`temporal.rs:318-330`, quoted for cross-check — the +example's impl provides exactly the 3 required methods; `hlc_tick` is +optional/defaulted): +```rust +pub trait DeinterlaceRow { + fn subject(&self) -> &str; + fn lance_version(&self) -> LanceVersion; + fn knowable_from(&self) -> LanceVersion; + fn hlc_tick(&self) -> Option { None } // default +} +``` + +`VerdictRow` also derives `Clone` (line 218: `#[derive(Clone, Debug)]`) — +required because `deinterlace` bounds `R: DeinterlaceRow + Clone` +(`temporal.rs:348`). + +### The fold/rank criterion + +```rust +// line 346 (exact signature) +fn rank_verdicts(owner: &Tenant, pool_size: usize, seed: &[u64]) -> Vec +``` +Consumes: `owner: &Tenant` (`Tenant = MailboxSoA`, borrowed +shared — reads `identity_plane_at` via `score_row`), `pool_size: usize` +(owned), `seed: &[u64]` (borrowed bloom bits, owned-by-caller). Returns +`Vec` in ROW-INDEX order (not sorted-score order — doc comment lines +343-345). + +```rust +// line 583 (exact signature) +fn fold_last_by_subject(rows: &[VerdictRow], proj: Proj) -> Vec<(String, bool)> +``` +Consumes: `rows: &[VerdictRow]` — a slice of the (already-deinterlaced) +`VerdictRow`s, borrowed. Internally filters to ONE projection first, then +folds "last row wins" per subject (relies on `deinterlace`'s own ascending +sort — comment lines 577-582, 598-600). This function operates on whatever +slice it's given; it does not itself require the WHOLE sealed set — it is +`deinterlace`'s own output/input contract (Section C's opening finding) +that requires the whole set. `rank_verdicts`, by contrast, is a +PRE-deinterlace scoring function over ONE tenant's populated rows +(`owner: &Tenant`, not `&[VerdictRow]`) — it needs one owner's SoA state, +not a row-array. + +--- + +## D. The seam — `crates/lance-graph-supervisor/src/cycle_driver.rs` + +### `run_cognitive_work_gated_over` — EXACT signature (lines 662-676) + +```rust +pub fn run_cognitive_work_gated_over( + fleet: &F, + owners: &[MailboxId], + writer: &mut BatchWriter>, + mut read_gate: impl FnMut(&F::Owner) -> Option<(QualiaI4_16D, i8, f32, Vec)>, +) -> CognitiveWorkOutcome +where + F: MailboxFleet, +``` + +The closure type it takes, written out character for character: +``` +impl FnMut(&F::Owner) -> Option<(QualiaI4_16D, i8, f32, Vec)> +``` +i.e. a `FnMut` taking `&F::Owner` (a shared borrow of the fleet's owner +type — never `&mut`), returning `Option<(QualiaI4_16D, i8, f32, Vec)>` +— a 4-tuple of `(qualia, signed_mantissa, reliability, payload)`, `None` +meaning "declined / no gate result this pass" (doc comment lines 640-643). + +Internally (lines 671-676) it forwards to `run_cognitive_work_over` by +wrapping the caller's `read_gate` in a second closure that additionally +calls `shade_owner(owner, &qualia, mantissa, reliability)`: +```rust +run_cognitive_work_over(fleet, owners, writer, |owner| { + let (qualia, mantissa, reliability, payload) = read_gate(owner)?; + let outcome = shade_owner(owner, &qualia, mantissa, reliability)?; + Some((outcome, payload)) +}) +``` + +### `shade_owner` — EXACT signature (lines 614-620) + +```rust +pub fn shade_owner( + owner: &O, + qualia: &QualiaI4_16D, + mantissa: i8, + reliability: f32, +) -> Option +``` +Body (lines 621-634): reads `owner.phase()`, computes +`gate_decision_i4(qualia, mantissa)`, calls +`phase.advance_on_gate(&gate)?` (returns `None` on Hold / no legal +successor), and on success returns a **bootstrap sentinel** +`StrategyOutcome` whose `intended_move` is +`KanbanMove { mailbox: 0, from: phase, to, witness_chain_position: 0, exec: +ExecTarget::Native }` — `mailbox: 0` is a fixed sentinel value that +`owner_adapter::emit_bootstrap_intent` later rebinds to the real owner +(never the live `MailboxId`). + +### `MailboxFleet` trait — EXACT (lines 179-186) + +```rust +pub trait MailboxFleet { + type Owner: MailboxSoaOwner; + fn owner(&self, id: MailboxId) -> Option<&Self::Owner>; + fn owner_mut(&mut self, id: MailboxId) -> Option<&mut Self::Owner>; +} +``` +Blanket impl for `HashMap` where `O: MailboxSoaOwner` (lines +190-198) — `owner`/`owner_mut` are plain `HashMap::get`/`get_mut`. + +### What a caller CAN and CANNOT pass through the `read_gate` closure + +- The closure receives ONLY `&F::Owner` — a **shared, read-only borrow** + of the owner. It cannot mutate the owner (`&F::Owner`, never `&mut`). + Whatever it reads (qualia, mantissa, payload bytes) must come from + methods callable on `&Owner` — i.e. whatever `MailboxSoaOwner` / + `MailboxSoaView` / the concrete `MailboxSoA` expose as read accessors + (`phase()`, `meta_at(row)`, `qualia_at(row)`, `pending_count()`, etc., per + Section F's usage in `probe_ignition.rs`). +- **This DOES decide whether a lens ordinal can reach a thought body**: any + value the closure hands onward (into `shade_owner`'s `qualia`/`mantissa` + args, or into the `Vec` payload) must be DERIVABLE from a read of the + owner alone — there is no channel in this signature for external + context, no `&mut` state, and no async/await inside the closure (it is a + plain synchronous `FnMut`, not `async fn`). A lens ordinal reaches the + thought body only if it is first materialized as owner-readable state + (e.g. written into `MetaColumn`/`meta_at` before the pass runs) — the + closure itself is a pure projection function, not a side-channel. +- The closure is `FnMut`, so it MAY carry captured mutable state across + calls within one `run_cognitive_work_gated_over` invocation (e.g. an + external counter), but that captured state is local to the closure's + environment, not derived from or written back to the fleet. + +--- + +## E. MetaWord — `crates/lance-graph-contract/src/cognitive_shader.rs` + +Exact signatures (lines 42-76): + +```rust +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +#[repr(transparent)] +pub struct MetaWord(pub u32); + +impl MetaWord { + #[inline] + pub const fn new(thinking: u8, awareness: u8, nars_f: u8, nars_c: u8, free_e: u8) -> Self { + let w = (thinking as u32 & 0x3F) + | (((awareness as u32) & 0x0F) << 6) + | ((nars_f as u32) << 10) + | ((nars_c as u32) << 18) + | (((free_e as u32) & 0x3F) << 26); + Self(w) + } + #[inline] + pub fn thinking(&self) -> u8 { + (self.0 & 0x3F) as u8 + } + #[inline] + pub fn awareness(&self) -> u8 { + ((self.0 >> 6) & 0x0F) as u8 + } + #[inline] + pub fn nars_f(&self) -> u8 { + ((self.0 >> 10) & 0xFF) as u8 + } + #[inline] + pub fn nars_c(&self) -> u8 { + ((self.0 >> 18) & 0xFF) as u8 + } + #[inline] + pub fn free_e(&self) -> u8 { + ((self.0 >> 26) & 0x3F) as u8 + } +} +``` + +Packing layout (doc comment line 38, verified against the bit-shift +arithmetic above): `thinking(6 bits, mask 0x3F) + awareness(4 bits, mask +0x0F) + nars_f(8 bits) + nars_c(8 bits) + free_e(6 bits, mask 0x3F)` = 6 + +4 + 8 + 8 + 6 = 32 bits, one `u32` per row. + +**The `thinking` field is 6 bits wide (mask `0x3F` = 0..63).** `thinking()` +masks with `0x3F` on read; `new()` masks the input `thinking` arg with +`0x3F` on write — an input `>= 64` is silently truncated to its low 6 bits, +not rejected. + +No setter (`set_thinking`) exists on `MetaWord` itself — `MetaWord` is +constructed fresh via `new()` and OVERWRITES the whole packed word; there +is no `with_thinking`/mutator method in this file. (`owner.set_meta(row, +MetaWord::new(...))` — seen in Section F — is a method on the OWNER, not on +`MetaWord`.) + +### How `probe_ignition.rs` (the GREEN probe) writes and reads `MetaWord` — +verbatim call sites + +**Write, inside `build_owner` (test-file line ~443):** +```rust +let meta = MetaWord::new(armed, 0, 0, 0, 0); +// ... later, per row: +let cell = WriteCell { + content: Some(content.as_slice()), + qualia: Some(qualia), + meta: Some(meta), + entity_type: Some((row % 251) as u16), + temporal: Some(row as u64), + ..WriteCell::default() +}; +let outcome = owner.write_row(row, cycle, &cell); +``` +Only the `thinking` field is ever non-zero in the probe (`armed: u8` — 0/1/2/3 +per cohort); `awareness`/`nars_f`/`nars_c`/`free_e` are always literal `0`. + +**A second, standalone write site (line ~1128):** +```rust +owner_mut.set_meta(0, MetaWord::new(1, 0, 0, 0, 0)); +``` +(`owner_mut` — arms row 0's thinking bit to `1`/Analytical directly, outside +`build_owner`'s per-row loop — used in a later, separate fixture in the +same test file.) + +**Read, inside `plan_or_evaluate_think` (line ~605) and the +`run_cognitive_work_gated_over` closure (line ~809):** +```rust +let armed = owner.meta_at(0).thinking(); +``` +Both read sites call `owner.meta_at(0)` (row 0 only — every owner in this +probe carries its arming bit at row 0) then `.thinking()` on the returned +`MetaWord`. `armed == 0` is checked explicitly as the UNARMED sentinel +(`plan_or_evaluate_think` line ~606: `if armed == 0 { return None; }`). + +--- + +## F. Reusable helper signatures from `probe_ignition.rs` (quoted, not +narrated — bodies omitted per the brief) + +```rust +// corpus loader +fn load_verses(path: &str, limit: usize) -> Option> +fn synthetic_corpus(n: usize) -> Vec +fn load_or_synthesize_corpus() -> (Vec, &'static str) + +// fleet construction +type Tenant = MailboxSoA; // ROWS_PER_OWNER = 64 +type Fleet = HashMap; +fn owner_verses(all: &[String], owner_idx: MailboxId) -> &[String] +fn build_owner( + id: MailboxId, + verses: &[String], + armed: u8, + qualia: QualiaI4_16D, + firing_rows: usize, +) -> Tenant +fn build_fleet(corpus: &[String]) -> Fleet + +// MemWal (in-process WalSink; NOT durability — struct + fields, no method +// bodies quoted here, all are private to the test module) +struct MemWal { + sealed: Mutex>, + next_version: AtomicU64, + wal_writes: AtomicU64, + reads: AtomicU64, // MUST stay 0 across the main loop (P4b reads no dataset) +} +impl WalSink for MemWal { /* ... */ } +fn MemWal::new() -> Self +fn MemWal::wal_writes(&self) -> u64 +fn MemWal::reads(&self) -> u64 +fn MemWal::head(&self) -> DatasetVersion + +// the scan function +#[derive(Default)] +struct ScanResult { + planning: Vec, + cognitive: Vec, + evaluation: Vec, + absorbed: Vec, + missing: usize, +} +fn scan_board(fleet: &Fleet, ids: impl IntoIterator) -> ScanResult + +// mantissa derivation (fed into shade_owner via the gated closure) +fn mantissa_of(owner: &Tenant) -> i8 // owner.pending_count().min(7) as i8 + +// qualia fixtures +fn flow_qualia() -> QualiaI4_16D +fn block_qualia() -> QualiaI4_16D + +// the probe-local Planning/Evaluation pass (NOT the shipped seam — +// shipped `run_cognitive_work_gated_over` only drives CognitiveWork) +struct ColumnPassOutcome { + cast: usize, + held: Vec, + missing: usize, +} +fn column_pass( + fleet: &Fleet, + ids: &[MailboxId], + writer: &mut BatchWriter>, + mut think: impl FnMut(&Tenant) -> Option<(StrategyOutcome, Vec)>, +) -> ColumnPassOutcome +fn plan_or_evaluate_think(owner: &Tenant) -> Option<(StrategyOutcome, Vec)> +``` + +### The cycle loop's call sequence (per cycle `c`, quoted structurally — +exact ordering, from the `for c in 1..=CYCLES` body) + +1. (if `c == WAKE_CYCLE`) re-energize one row per REST-cohort owner — + `owner.energy[row] = FIRE_ENERGY` (direct field write, no method). +2. `scan_board(&fleet, SCOPE_LO..SCOPE_HI)` → `scan: ScanResult`. +3. Bookkeeping: `sink.wal_writes()` snapshot, `phase_cycle_snapshot(&fleet)` + snapshot ("before cast"). +4. `column_pass(&fleet, &scan.planning, &mut writer, plan_or_evaluate_think)` + → `planning_outcome`. +5. `run_cognitive_work_gated_over(&fleet, &scan.cognitive, &mut writer, + |owner| { ... })` → `cognitive_outcome` (the closure reads + `owner.meta_at(0).thinking()`, `owner.qualia_at(0)`, `mantissa_of(owner)`, + `StyleStrategy::reliability_for(style, &ctx)`, and returns + `Some((qualia, mantissa, reliability, row_span_payload(owner)))`). +6. `column_pass(&fleet, &scan.evaluation, &mut writer, + plan_or_evaluate_think)` → `evaluation_outcome`. +7. `phase_cycle_snapshot(&fleet)` snapshot ("after cast") — asserted equal + to step 3's snapshot (G3a: staging casts must not mutate any owner's + phase/cycle). +8. `total_casts = planning_outcome.cast + cognitive_outcome.cast + + evaluation_outcome.cast`. +9. If `total_casts == 0`: REST branch — no `run_cycle` call, no seal, + `wal_writes` unchanged, `continue` to the next cycle (with an + end-of-c5-vs-end-of-c6 fingerprint comparison hook on the last two + cycles). +10. Else: `run_cycle(&sink, &mut fleet, &mut writer, + CycleFrame::new(CycleId(u64::from(c)), base_version), position_base, + &mut watermarks, u64::from).await` → `Result` + (panics on any `Err` — the probe treats a seal/apply failure in the + main run as impossible given `MemWal` never injects one there). +11. `position_base = position_base.max(outcome.sealed.next_position_base)`. +12. Post-apply snapshot + assertions comparing `changed` owners to + `outcome.sealed.transitions`' owners, and `sink.reads() == 0`. + +`run_cycle`'s own internal sequence (from `cycle_driver.rs:446-471`, for +cross-reference — this is INSIDE step 10 above, not called separately by +the probe): `collect_casts(writer, frame.cycle, position_base, row_of)` → +`seal_cycle(sink, frame, collected.slots).await` → +`apply_sealed_transitions(fleet, &sealed, watermarks)` → assembles +`CycleOutcome { sealed, applied, held: collected.held }`. + +--- + +## NOT VERIFIED (explicit — nothing below was confirmed; do not treat as fact) + +- `MailboxSoaOwner`, `MailboxSoaView`, `Owner::phase()`, + `Owner::mailbox_id()`, `Owner::current_cycle()`, `Owner::meta_at()`, + `Owner::qualia_at()`, `Owner::pending_count()`, `Owner::set_meta()`, + `Owner::write_row()`, `Owner::set_populated()`, `Owner::tick()` — none of + these trait/method definitions were opened in this pass; only their CALL + SITES were read (in `probe_ignition.rs` / `blw_fusion.rs` / + `cycle_driver.rs`). Their exact signatures (esp. return types, whether + `meta_at`/`qualia_at` take `&self` or borrow-return) are asserted here + only as inferable from call-site usage (`owner.meta_at(0).thinking()` + implies `meta_at(&self, usize) -> MetaWord` or `-> &MetaWord`, but the + precise return type — owned vs `&MetaWord` — was NOT confirmed by + opening the trait/impl). +- `BeliefArena::close_transitive` — called by `probe_eyes_opened.rs` but + its signature was not opened in this pass (only its call site). +- `owner_adapter::emit_bootstrap_intent` — signature not opened; only its + call sites (`cycle_driver.rs` internals, `probe_ignition.rs`'s + `column_pass`) were read. +- `gate_decision_i4`, `trust_texture_i4`, `KanbanColumn::advance_on_gate`, + `StyleStrategy::plan` / `StyleStrategy::reliability_for` — signatures not + opened; only call sites read. +- `WalSink` trait's full method set — only `scan_sealed` was named in a + grep hit; the trait definition itself was not opened. +- Nothing in this file was compiled, type-checked, or run. All signatures + above are transcribed from source text as read; any transcription error + is possible and would only be caught by the orchestrator's central + `cargo` gate. diff --git a/.claude/board/exec-runs/d-ign-b-build.md b/.claude/board/exec-runs/d-ign-b-build.md new file mode 100644 index 000000000..e89270c7d --- /dev/null +++ b/.claude/board/exec-runs/d-ign-b-build.md @@ -0,0 +1,190 @@ +# D-IGN-B — build (Sonnet build lane, edit-only, no cargo run) + +**Deliverable:** `crates/lance-graph-supervisor/tests/d_ign_b_lenses.rs` (new +file, `#[cfg(feature = "cycle-driver")] mod d_ign_b_lenses { ... }`). No +`Cargo.toml` change — every import resolves against a dep already present +(`lance-graph-planner`, `lance-graph-contract`, `cognitive-shader-driver`, +`tokio` — all already used by `probe_ignition.rs` in the same crate). + +Mandatory reads done in full, in the ordered list: sonnet-worker-guardrails.md, +`AGENT_LOG.md` (first ~130 lines), `d-ign-b-design-opus.md` (full, 352 lines), +`d-ign-b-api-inventory-sonnet.md` (full, 705 lines), `probe_ignition.rs` (full, +1,389 lines). No `cargo` command run — edit-only per the guardrails and the +brief. + +## What was built + +- **Cohorts** exactly per design §5's one-change re-carve: TWIN block + (ids 0..8, one shared verse slice `owner_verses(corpus, 0)`, one shared + content-plane salt `0` so the bloom planes are byte-identical, armed + `z = 1,1,2,2,3,3,4,4`), SPREAD block (ids 8..30, distinct slices/salts, + `z` cycling 1..4), UNARMED (id 30, `z=0`), ORPHAN (id 31, not inserted), + OUTSIDE (32..64, armed but `SCOPE_HI=32` excludes them from every scan). +- **`LensReadout` enum** (probe-local, mints nothing shipped) over the four + `stance_panel` tuple element types verbatim from the inventory + (`Vec<(CStmt,f32)>` / `Vec<(CStmt,FlipKind)>` / `Vec<(String,f32,f32)>` / + `Vec<(u16,usize)>`), with `is_empty()` and a `digest() -> u64` stable fold + (floats via `.to_bits()`, `FlipKind`'s missing `Hash` derive worked around + by hand-folding its two-variant discriminant). +- **`run_lens(z, verses)`** — one `BeliefArena`/`Interner`/`ReadOut`, one + `stream(...)` pass, one `stance_panel(...)` call, then a `match z` that + SELECTS which of the four already-computed tuple elements to keep. Doc + comment states explicitly: the panel computes all four, this only selects + one — never "dispatch". +- **Wiring into the cycle-driver seam**: the `run_cognitive_work_gated_over` + closure (design §1's chosen seam, not the `_over` variant — no readout + channel there) reads `owner.meta_at(0).thinking()`, asserts it is never 5 + (in-loop mirror of the z=5-blocked premise), and for `z` in `1..=4` calls + `run_lens` and inserts `(owner.mailbox_id(), owner.cycle()) -> LensReadout` + into a `HashMap` captured by the closure — no shipped signature changed, + no `&mut` state on the SoA itself. +- **Gates L0–L7**, each with both halves, on non-trivial inputs: + - L0 (twin premise): full 48-row byte-identity check across all 8 twin + owners plus a non-zero check plus a differs-from-a-non-twin-owner check. + - L1: can-fire pinned to **z=3 (Kant) vs z=4 (Wittgenstein)**, not z=1 + vs z=4 as the design's own table literally shows — per the design's + own §4 risk note + §7 Q4 pre-registered fallback (Hegel is documented + as measured constant-false on this corpus shape in §12.3a″, and + Nietzsche derives from Hegel so degrades with it). This substitution + is pre-registered IN THIS BUILD, before any run, citing the design's + own stated risk — not a post-hoc pair swap. can-stay-silent: same + lens (z=3) computed twice, bit-identical. + - L2: can-stay-silent checked FIRST across every cycle key the main loop + could have written (not one hardcoded cycle number); can-fire arms the + UNARMED owner directly and re-runs the lens step, landing an entry in + a **separate scratch map** (kept out of the main `readouts` map on + purpose — see Deviation below). + - L3: per-lens non-emptiness measured and printed for all 30 in-scope + armed owners, asserted per the design's literal gate text. + - L4: can-fire (single TWIN-base owner, ≥3 of 4 lens digests distinct); + can-stay-silent (≥2 distinct digests per lens across the 30 in-scope + owners). + - L5: mechanics-unchanged, but the assertion is the **derived** flow/block + split for THIS file's cohorts (30 Flow / 0 Block), not PROBE-IGNITION's + literal "20+4" — see Deviation 1 below. + - L6: readout keys ⊆ owners whose sealed transition had + `Planning->CognitiveWork` in a prior cycle (`Elixir` mint, per L5's own + check); can-stay-silent: the UNARMED owner (never advanced) is absent. + - L7: OUTSIDE owner absent from the main-loop readouts (every id in + 32..64 checked, not just one); run through `run_lens` directly it + produces a readout. +- **z=5 BLOCKED**: a runtime scan of every owner in the fleet asserting + `thinking() != 5` (the premise, not just an assumption), a `MetaWord` + round-trip sanity check that 5 IS representable in the 6-bit field (so + the block is a design choice, not a representational impossibility), and + a printed line stating the R2-vs-defer decision is NOT made here — R2 is + not implemented in this file. +- **§6 not-claimed block** — 11 lines, printed at the end, including item 11 + ("no per-stance DISPATCH claim... this file only SELECTS"). + +## Deviations from the design note, all documented in-file (module doc + inline) + +1. **L5's numeric decomposition.** The design's gate table cites PROBE- + IGNITION's own "20 Flow + 4 Block" (`probe_ignition.rs:977-984`) as the + L5 can-fire literal. That number came from a CONTRA cohort (4 owners on + `block_qualia()`) that design §5's own cohort re-carve does not include + in this file (every in-scope owner here uses `flow_qualia()`). Copying + "20+4" verbatim would have been a false, uncomputed claim on a run shape + that cannot produce it. Implemented instead: the DERIVED expectation for + this file's actual cohorts (30 Flow, 0 Block), computed from the cohort + constants and asserted at c1, with the measured counts printed every + cycle regardless. Flagged as a spec/inventory-adjacent conflict I could + not resolve by re-reading source (it is a run-shape consequence, not a + signature question) — STOP+report is the honest move per §5 rule 1 of + the guardrails, but since the design's own §5 already authorizes "the + cohort re-carve is the ONE change" and the 20+4 line was inherited prose + from the probe rather than a re-derived number, I judged deriving the + correct figure was the faithful reading rather than a scope violation. + Flagging for orchestrator review regardless. +2. **`thinking_style_for` z=4 → same `ThinkingStyle` as z=3.** The lens + ordinal (1..4) and the `StyleStrategy` dispatch input share the one + 6-bit `MetaWord.thinking` field by design. The inventory only confirms + three `ThinkingStyle` variants reachable from this crate + (Analytical/Creative/Reflective, `probe_ignition.rs:151-158`); a fourth + variant's exact name/discriminant was NOT verified in this pass, so + z=4 reuses Reflective rather than guessing a new variant name. This + affects only the `reliability`/gate-decision input, never the lens + SELECTION itself (`run_lens` switches on `z` directly). +3. **`LensReadout::digest` folds no discriminant tag.** Self-check caught + this before finishing (see below) — a tagged digest makes any + cross-lens `!=` comparison pass by construction of the tag byte alone, + which is exactly the "assertion implied by the code it tests" pattern + `CLAUDE.md`'s falsifiability rule forbids. Removed the tag; two EMPTY + readouts of any lens now hash equal by construction (both fold zero + bytes), which is the correct anti-vacuity behavior — it is what lets + L3/L4 fail for real instead of being guaranteed to pass. +4. **L2's arm-then-verify writes to a SEPARATE scratch `HashMap`, not the + main `readouts` map.** Self-check caught this too: inserting the + manually-armed UNARMED owner's readout into the SAME map the main loop + populates would have made L6's later `readouts ⊆ advanced_to_cognitive` + check fail on a false premise (that owner never went through + `run_cognitive_work_gated_over`, so it can never be in + `advanced_to_cognitive`). Using a scratch map keeps L2 and L6 from + contaminating each other. + +## Self-check performed before finishing (per the brief's four bug classes) + +- **(a) hardcoded version/base values:** `base_version` is read fresh via + `sink.head()` every cycle inside the loop, never hardcoded. +- **(b) tautological self-comparisons:** found and fixed one real instance + — the digest discriminant tag (deviation 3 above). Everything else + re-checked: `assert_eq!(z5.thinking(), 5, ...)` and + `assert_eq!(armed, 3, ...)` are genuine round-trip/write-landed checks + (could fail on a `MetaWord` bit-packing bug or a `set_meta` bug), not + self-referential. +- **(c) fingerprints captured outside their claimed window:** L0's twin + check runs immediately after `build_fleet`, before any cycle — correct + window for "the seeded rows are byte-identical". L6's containment check + runs after the full loop, over the whole accumulated `readouts` map — + correct window for a "was there ever a prior-cycle advance" claim. +- **(d) compile-time self-scan matching its own success message:** this + file has NO `include_str!` self-scan (unlike `probe_ignition.rs`'s + G2a/G3b/G11) — the design's gate table (L0–L7) does not call for one, and + the build brief's four-item self-check list is items to re-check, not a + requirement to add a self-scan where the spec doesn't ask for one. Noted + here rather than silently omitted. + +## What could NOT be verified (honest gaps — not compiled, not run) + +- **All four `stance_panel` return values on this specific corpus are + unmeasured.** In particular whether Hegel/Nietzsche are constant-empty + here (as documented for a related corpus shape in plan §12.3a″) is + UNKNOWN until run. If they are, **L3's hard `assert!(empty < total)` + for z=1 and/or z=2 will FAIL** — this is the pre-registered risk the + design's own §7 Q4 names as an open orchestrator decision ("still a + passing probe under the pre-registered fallback" refers to L1, which I + pinned to the Kant/Wittgenstein pair specifically to survive this; L3 + as literally specified has no such escape hatch and I did not invent + one, since softening a hard-required gate on my own authority would be + a spec deviation beyond what "build faithfully" licenses). Flagging + for the orchestrator explicitly rather than guessing at a fix. +- **Whether all 30 TWIN+SPREAD owners actually gate-advance at c1 and c2** + (the L5 "30 Flow / 0 Block" assertion, and the assumption that readouts + end up with ~30 entries) is inferred from PROBE-IGNITION's proven + behaviour on the identical `flow_qualia()` + firing_rows=3 fixture, but + not run here. +- **Exact `MailboxId` underlying integer type** was not opened (only + inferred from arithmetic/cast usage already proven to compile in + `probe_ignition.rs`, which this file mirrors byte-for-byte in every + place that does `MailboxId` arithmetic). +- **`MailboxSoaOwner`/`MailboxSoaView` trait method exact signatures** + (`meta_at`, `qualia_at`, `content_row`, `set_meta`, `cycle`, + `current_cycle`, `pending_count`, `populated`, `set_populated`, + `write_row`, `phase`, `mailbox_id`, `tick`) were not opened in this pass + either — per the API inventory's own "NOT VERIFIED" section, only their + call sites were read (in `probe_ignition.rs`). This file uses them + exclusively in the exact same call shapes the inventory already quotes + from the GREEN probe. +- **`CognitiveWorkOutcome`'s exact field set** beyond `.cast` was not + opened; only `.cast` is used here (matching the probe's own usage). +- Not compiled, not linted, not run. All of the above is transcription + + reasoning from source text and the already-GREEN probe's proven shapes, + not a compiler's word. + +## Files touched + +- `crates/lance-graph-supervisor/tests/d_ign_b_lenses.rs` (new, ~750 lines) +- `.claude/board/exec-runs/d-ign-b-build.md` (this file, new) + +No other files touched. No `.github/` file touched. No `Cargo.toml` touched. +No `cargo` command run. No branch switched, no commit made. diff --git a/.claude/board/exec-runs/d-ign-b-design-opus.md b/.claude/board/exec-runs/d-ign-b-design-opus.md new file mode 100644 index 000000000..815e992d6 --- /dev/null +++ b/.claude/board/exec-runs/d-ign-b-design-opus.md @@ -0,0 +1,501 @@ +# D-IGN-B — DESIGN (Opus design lane, 2026-08-05) + +**Scope:** design only. No code, no cargo, one file (this one). Realizes plan +`.claude/plans/cycle-loop-closure-driver-v1.md` §12.11 on the scaffolding +PROBE-IGNITION left green (`crates/lance-graph-supervisor/tests/probe_ignition.rs`, +1,389 lines, 2/2 tests, G1–G11 both halves — AGENT_LOG 2026-08-05). + +Every claim below is anchored at `file:line` or marked **UNVERIFIED**. Nothing +here was compiled or run. + +--- + +## 0. Headline, and the two findings that shape it + +D-IGN-B swaps the fixture thought body for the **shipped** four-stance panel +(`lance_graph_planner::nars::stance`) driven by the armed ordinal in +`MetaWord::thinking()`. Two findings constrain that, both verified in source: + +**F0 — there is NO per-stance dispatch, and the note must not pretend there +is.** `stance_panel(arena, intern, out)` (`stance.rs:469-478`) returns all four +stances as ONE 4-tuple. There is no stance enum, and no way to compute one +stance alone without refactoring the shipped function. **Consequence, stated in +the required words: arming selects WHAT IS READ, not what is computed.** Every +armed owner runs the same panel over its own arena; the ordinal picks the tuple +element. §12.11's phrase "z=1..4 dispatch to the shared nars stance bodies" is +therefore realizable only as *selection*, not dispatch, and the file's prose, +its gate names, and its printed output must all say selection. + +**Does the honest framing kill the deliverable?** No — and this is a judgment, +so it is argued rather than asserted. The pre-registered observable in §12.11 is +"different lenses over byte-identical rows produce non-identical readouts; the +same lens produces bit-identical ones". A selection axis satisfies that +non-vacuously: the four tuple elements are four different types over four +different derivations (`stance.rs:479-532`), so the choice is load-bearing and +falsifiable, and L4 (§4) can still fail. What the framing DOES kill is any +claim about *compute* being lens-dependent — no CPU path differs, cost is +identical across z, and a "the armed bits steer the computation" sentence would +be false. That claim is not in §12.11's observable, so the deliverable stands, +one size smaller than its name suggests. **Recommendation: rename the axis in +the file from "lens dispatch" to "lens selection" so the next reader inherits +the correct size.** + +**F1 — the lens cannot read the row bytes; it reads the owner's own verses, +selected by the owner's address.** `stance::stream` takes +`verses: &[(String, String)]` — labelled verse TEXT (`stance.rs:161-167`), and +its whole machine is a token walk over that text (`stance.rs:190-406`). What a +row actually carries is a **bloom plane**: 4 bits per token OR'd into a +`WORDS_PER_FP`-word plane (`probe_ignition.rs:232-257`). That is one-way — no +inverse exists, and none is proposed. So the lens body takes the owner's +`mailbox_id` + `populated()` and re-reads the SAME corpus slice the owner was +seeded from (`probe_ignition.rs:426-429`, `:444-461`). **This must be stated in +the file and in the not-claimed block: D-IGN-B does not decode SoA row bytes +into text.** It is still address-driven cognition — which owner thinks, over +which verses, in which lens, is decided entirely by the owner's id, span and +armed bits — but it is not row-byte decoding. + +**F1b — this is the §12.7 defect shape (a harness reading past the substrate), +and it must be named in those words.** §12.7 killed `blw_texture.rs` partly +because its grep count for `batch_writer|BatchWriter|KanbanStep|kanban| +owner_adapter|MailboxSoA|SoaEnvelope` was **0** — "a free-standing loop over a +TSV … therefore cannot be evidence for any substrate claim, only for the stance +functions". D-IGN-B takes its text from the same place such a harness would. + +**My judgment: acceptable HERE, on one condition, and the condition is +testable.** In `blw_texture` the substrate governed *nothing* — no owner, no +phase, no cast, no seal. Here it governs **selection end-to-end**: which owners +are in scope (`where()`), which are armed (`meta_at(0).thinking()`), which have +reached `CognitiveWork` (only via an applied, sealed transition), and which +verse span is read (`populated()` + the owner's id). Remove the substrate and +nothing runs; gates L2/L5/L6/L7 each falsify one leg of that. What the +substrate does NOT govern is the *content* of the read. + +**The condition:** the file never claims the stance readout is evidence about +the substrate's data path, and the not-claimed block carries F1/F1b verbatim +(item 1 in §6). If a future reader wants "the lens read the rows", the honest +route is an instrument over the encoded planes, not a better sentence about +this one. **If the orchestrator judges that a probe whose cognition reads past +the substrate is a corpus harness wearing a substrate costume regardless of the +selection argument, that is a defensible call and D-IGN-B should be re-scoped +or killed — the above is the strongest honest case, not a guarantee.** + +**F2 — z=5 (Fusion) is BLOCKED in-cycle. See §3. That finding is the +deliverable, per the brief; the honest reduction is R2 below.** + +--- + +## 1. (a) Where the lens SELECTION attaches (not dispatch — see F0) + +**Decision: `run_cognitive_work_over`, NOT `run_cognitive_work_gated_over`.** + +- `run_cognitive_work_gated_over` (`cycle_driver.rs:663-676`) takes + `read_gate: FnMut(&F::Owner) -> Option<(QualiaI4_16D, i8, f32, Vec)>` and + then *fixes* the outcome to `shade_owner(owner, &qualia, mantissa, reliability)` + (`:672-674`). Its return channel is `(qualia, mantissa, reliability, payload)` + — there is no slot a lens readout can leave through. This is the closure the + probe currently uses (`probe_ignition.rs:807-816`); it is the wrong carrier + for D-IGN-B. +- `run_cognitive_work_over` (`cycle_driver.rs:577-587`) takes the **general** + seam `think: FnMut(&F::Owner) -> Option<(StrategyOutcome, Vec)>` and + forwards to `cognitive_pass` (`:490-531`), which filters to + `KanbanColumn::CognitiveWork` (`:505-507`) and casts via + `emit_bootstrap_intent` (`:512-519`). **This is the seam.** + +**Shape of the lens closure** (probe-local; `think` is `FnMut`, so it may hold +`&mut` state — that is how the readout escapes without changing any signature): + +``` +|owner: &Tenant| -> Option<(StrategyOutcome, Vec)> { + let z = owner.meta_at(0).thinking(); // the ordinal, read here + if z == 0 { return None; } // unarmed: no lens, no cast + let readout = run_lens(z, owner, corpus); // §2 — the shipped panel + readouts.insert((owner.mailbox_id(), owner.cycle()), readout); // &mut capture + let qualia = owner.qualia_at(0); + let mantissa = mantissa_of(owner); + let reliability = StyleStrategy::reliability_for(thinking_style_for(z), &ctx); + let outcome = shade_owner(owner, &qualia, mantissa, reliability)?; + Some((outcome, row_span_payload(owner))) +} +``` + +**Why the out-of-band `&mut` capture rather than a second pass — and the +honest cost.** The shipped seam has **no readout slot** in either variant: the +gated closure returns `(QualiaI4_16D, i8, f32, Vec)` (`:663-676`), the +general one returns `(StrategyOutcome, Vec)` (`:577-587`). Both are +read-only-owner-in, decision-out. Three options, judged: + +1. **`&mut` collector captured by the `FnMut`** (chosen). The lens runs at the + exact moment the substrate says this owner thinks, so the readout is keyed + by a phase the harness did not choose. Cost: the readout escapes + *out-of-band* — it is invisible to the seam's own types, so nothing in + `cycle_driver` can ever check it. That is a real weakness and it is why L6 + exists (every readout key must be justified by a prior applied + `to == CognitiveWork` move) — the ordering claim is asserted by the probe, + not guaranteed by the signature. +2. **A separate probe-local pass** (the `column_pass` precedent, + `probe_ignition.rs:559-598`). Rejected as the primary: a second pass + re-derives "who is in CognitiveWork" itself, which is exactly the coupling + the ignition property exists to demonstrate — the lens would then run because + the *harness* decided to, one step removed from the seal. +3. **Change the shipped seam to carry a readout.** Rejected outright: it widens + a shipped signature for a probe's benefit, and `cycle_driver` has production + callers of both variants. + +**Recommendation to the orchestrator:** option 1, with L6 as the compensating +gate, and the file stating in-source that the readout is out-of-band and +therefore probe-asserted rather than type-enforced. + +**How the ordinal reaches it:** `owner.meta_at(0).thinking()` — the same read +the probe already performs at `probe_ignition.rs:809` and `:606`. No +MetaWord→PlanContext bridge is introduced; §12.11's Q1 non-goal is preserved +verbatim (persona-vs-rung-ladder §"four spaces" is the mandatory read before +any such bridge, and this design does not open it — the six ordinals are a +probe-local arming vocabulary, **not** the persona-36 and **not** rung-3 +runbooks). + +**What does NOT change:** the gate, the DAG move, the seal/apply, the +write-back. The lens changes the READOUT only; the transition is still minted +by `shade_owner` (`cycle_driver.rs:615-635`). Gate L5 (§4) pins exactly that. + +--- + +## 2. (b) The readout type + +**Decision: mint nothing in any shipped crate. The readout is a probe-local +test type over the four shapes `stance_panel` already returns.** + +`stance_panel` (`stance.rs:469-478`) returns a 4-tuple of four *different* +types in one call over one arena: + +| z | lens | shipped return shape (`stance.rs:474-477`) | +|---|---|---| +| 1 | Hegel | `Vec<(CStmt, f32)>` — Aufhebung ranking | +| 2 | Nietzsche | `Vec<(CStmt, FlipKind)>` — genealogy partition | +| 3 | Kant | `Vec<(String, f32, f32)>` — (label, graded quale, ablated quale) | +| 4 | Wittgenstein | `Vec<(u16, usize)>` — (concept, distinct games) | + +Consequences, both load-bearing: + +1. **The panel computes all four; the lens SELECTS one.** That is the honest + description and must be written as such — the harness does not run four + different algorithms, it takes the z-th projection of one shipped read over + one arena. Cost is therefore identical across lenses (see §5). +2. **No shipped type unifies them, and none should be invented.** A probe-local + `enum LensReadout { Hegel(..), Nietzsche(..), Kant(..), Wittgenstein(..) }` + plus a `digest(&self) -> u64` (a stable order-preserving fold over the + variant's contents) is the whole surface. Probe-local test types have + precedent in the same file (`OwnerFingerprint` `:642-649`, `ScanResult` + `:518-525`, `RowSpanDescriptor` `:296-311`). + +**`ReadOut` evaluated as the readout type — REJECTED, with reasons.** +`stance::ReadOut` (`stance.rs:132-145`) is a shipped struct with Vec fields +(`provenance`, `lifts`, `impls`, `pass2_admitted`, `pass2_revised`) and is the +obvious candidate, so it gets an explicit verdict rather than silence: + +1. **It is an INPUT to the panel, not its output.** `stream` fills it + (`stance.rs:161-167`, `:291-295`, `:346-361`) and `stance_panel` then + *consumes* it (`:472`, `:500-510`, `:522-525`). Landing `ReadOut` as the + D-IGN-B observable would report what the parser saw, **identically for every + z** — the lens axis would vanish and L1 would fail by construction. That + makes it the single most dangerous wrong choice available here. +2. **It is lens-independent by definition** — one `ReadOut` per owner, four + stances read from it. Using it as the readout would be the §12.7 error in a + new costume: an instrument that cannot see the distinction it exists to make. +3. **It is, however, the right thing to keep and print as CONTEXT** — per-owner + `provenance.len()` / `lifts.len()` / `impls.len()` explain *why* a lens came + back empty (L3), and Kant's readout is derived from `out.lifts` directly + (`:500-510`), so an owner with zero lifts has an empty Kant readout for a + legible reason. **Recommendation: print `ReadOut` cardinalities alongside + every readout; never use it AS the readout.** + +**Explicit anti-decision: do NOT route the readout through +`CausalWitnessFacet`.** That is the carrier §12.7 KILLED — the 24-locus +register into which only 3 loci were ever written, bounding `agreement_count` +at 1 of 24 by construction (plan §12.7, "the register was necessary and is not +sufficient"). Reusing it here would rebuild the same defect one level up. The +four heterogeneous shapes above are *why* the §12.7 collapse does not +mechanically bind D-IGN-B — but that must still be TESTED, not assumed: gate L4. + +**Where it lands:** a `HashMap<(MailboxId, u32), LensReadout>` keyed by (owner, +cycle) in the test body. Nothing is written back into the SoA, nothing is cast, +nothing is persisted. A readout is an observation the harness makes, not state +the substrate carries — consistent with §12.5 "a lens is a read". + +--- + +## 3. (c) z=5 Fusion — BLOCKED in-cycle. The honest reduction. + +**Verified premises:** + +- `blw_fusion` needs the ranking **pool to grow**: "the ranking POOL must grow + — this is what makes a verdict horizon-dependent at all" + (`blw_fusion.rs:494`, `seed_slice` seats one incremental slice per cycle + `:500-528`). +- It needs **many horizons**: `S_CYCLES = 8` (`:123`), `SLICE = 250` (`:126`), + `V_PIN_CYCLE = 4` (`:131`), and the Δκ table in plan §12.8 is over eight of + them. +- The two projections are `QueryReference::at(v_pin, RUNG_STRICT=0)` vs + `at(v_pin, RUNG_AWARE=5)` (`blw_fusion.rs:133-136`, `:983-996`) over + `VerdictRow`s emitted per (subject, horizon) (`:216-241`, `:900-926`). + +**Against the probe's shape:** + +- an owner is seeded ONCE, all 48 rows, before the loop + (`probe_ignition.rs:444-461`) — the pool never grows; +- the measured run seals **once** (c1: `wal_writes == 1`, `:952-956`; c5/c6 are + rest cycles, `:839-847`) — so an owner has ~1 horizon, not 8; +- with one horizon, Strict and Aware admit the same rows, the folded verdict per + subject is identical, and any Δ is **0 by construction** — a vacuous readout, + the exact failure class this workspace's falsifiability rule exists to reject; +- `jc` is a dev-dep of `lance-graph-planner` only (`lance-graph-planner/Cargo.toml:77`); + `lance-graph-supervisor/Cargo.toml` has **no** `jc` — so κ additionally needs a + manifest change (orchestrator decision, not a worker's). + +**Two reductions; I recommend R2.** + +- **R1 (rejected as the default):** change the run shape — seat 8 rows/cycle, + force every cycle non-resting so each owner accrues ≥4 sealed horizons, then + report Strict/Aware admitted-row counts. Rejected because it entangles + D-IGN-B's z=1..4 headline with a run-shape rewrite, and it breaks the + inherited G4/G6 rest gates the probe pinned. +- **R2 (recommended):** **z=5 is NOT in the main fleet.** Ordinal 5 stays + *reserved* in the arming vocabulary and is exercised by a SEPARATE + `#[tokio::test]` in the same file, with **one** owner (an owner is a tenant — + `E-AN-OWNER-IS-A-TENANT-NOT-A-SHARD-1`), incremental seating, and its own + MemWal, mirroring `blw_fusion`'s shape at reduced scale. Its permitted + observable is the **admitted-row-count gap** Strict vs Aware at the owner's + own pin plus the folded verdict-set difference — **no κ, no fusion verdict, + no jc dep**. If the orchestrator wants κ, that is a manifest change and a + separate deliverable. + +If even R2 is deemed out of scope for this stage, the correct outcome is: z=5 +is declared reserved-unimplemented in the file and in the not-claimed block. It +is never silently mapped to one of z=1..4. + +--- + +## 4. (d) Pre-registered gate table + +Pinned BEFORE any run. Every gate has both halves on non-trivial inputs. + +**Precondition for the headline pair — TWIN SLICES.** The probe's owners get +*disjoint* verse slices (`owner_verses`, `probe_ignition.rs:426-429`), so +"byte-identical rows" is unreachable there. D-IGN-B re-carves the in-scope +cohorts (§5): owners `0..8` are all seeded from the SAME 48-verse slice, armed +`z = 1,1,2,2,3,3,4,4`. Gate L0 asserts that identity before L1 is read. + +| # | Gate | can-FIRE (non-trivial) | can-STAY-SILENT (non-trivial) | +|---|---|---|---| +| **L0** | twin premise | the 8 twin owners' content planes are pairwise **byte-identical** across all populated rows | the twin planes are **non-zero** and differ from a non-twin owner's plane (else "identical" is the trivial all-zero case) | +| **L1** | **the lens axis is load-bearing** | owners 0 (z=1) and 6 (z=4), byte-identical rows, same cycle ⇒ `digest` values **differ** | owners 0 and 1 (both z=1), byte-identical rows, same cycle ⇒ digests **bit-identical** | +| **L2** | arming | the unarmed owner has **no** entry in the readout map for any cycle; after arming it (the G8 pattern, `probe_ignition.rs:1125-1143`) a readout appears | an armed owner in a cohort that never enters CognitiveWork produces no readout either — absence is not proof of the arming axis on its own | +| **L3** | **per-lens non-emptiness (measured, not assumed)** | each of z=1..4 yields a NON-EMPTY readout on ≥1 owner | per-lens empty counts are printed for all 32 in-scope owners; a lens empty on **every** owner is a loud FINDING, not a silent pass | +| **L4** | **anti-degeneracy / anti-collapse (§12.7's shape)** | over ONE owner, the four lens digests are **not all equal** (≥3 distinct of 4) | each non-empty lens yields **≥2 distinct digests across the 32 in-scope owners** — a lens whose readout is constant over every owner carries no information (the 99.61 % / 150-of-150 shape, plan §12.3a″ + CLAUDE.md falsifiability rule) | +| **L5** | mechanics unchanged by the swap | c1 still seals the pinned decomposition (20 Flow `Planning→CognitiveWork` + 4 Block `Planning→Prune`, `probe_ignition.rs:977-984`) with the lens body in place | the readout map is **empty** at c1 (nothing has entered CognitiveWork yet) — the lens cannot precede the seal | +| **L6** | seal→apply ordering | every readout key `(id, c)` has `id` in the set of owners whose applied move had `to == CognitiveWork` in a PRIOR cycle | an owner that never received such an applied move has no readout at any cycle | +| **L7** | address axis (inherits G7) | an OUTSIDE owner, run through the lens body directly, produces a readout | the OUTSIDE cohort has **no** readout from the main loop (never scanned) | + +**Pre-registered risk, recorded so the fallback is not post-hoc.** §12.3a″ +measured Hegel **constant-false** on the SPO path (uniform frequency ⟹ zero +contradiction depth), and `contradiction_ranking` filters `> 0.05` +(`stance.rs:418-427`) while `stream` emits `f = 0.9` / `f = 0.05` under +negation (`stance.rs:274`). A Hegel readout is therefore non-empty only if some +statement is observed **both** negated and affirmed within one owner's 48 +verses. **If Hegel is empty on all 32 owners, that is a measured result and L3 +reports it as such; L1's can-fire pair then MUST be witnessed by a pair not +involving the empty lens** (e.g. z=3 vs z=4). Pre-registering this now means a +post-hoc pair swap cannot be mistaken for fitting. The same caveat applies to +Nietzsche (it consumes Hegel's ranking, `stance.rs:483-496` — if Hegel is +empty, Nietzsche is empty too; **the two are not independent**, which is itself +worth printing). + +Kant is *not* eligible for the old tautology: `quale > ablated` reduces to +`modal > 0.5` (plan §12.3a, verified against `stance.rs:499-510`) and is +forbidden as an assertion. The Kant readout is the (label, graded, ablated) +triple digest, never a comparison verdict. + +--- + +## 5. (e) Pinned run shape + +Reuse the probe's constants where they still hold; ONE change, justified. + +| constant | value | status | +|---|---|---| +| `FLEET_OWNERS` | 64 | unchanged (`probe_ignition.rs:108`) | +| `ROWS_PER_OWNER` / `POPULATED_ROWS` | 64 / 48 | unchanged (`:109-110`) | +| `CORPUS_VERSES` | 3072 | unchanged (`:111`) — the twin owners reuse slice 0; unused slices stay loaded, keeping the pinned constant honest | +| `SCOPE` | 0..32 | unchanged (`:112-113`) | +| `CYCLES` | 6 | unchanged (`:114`) | +| **cohorts** | **re-carved** | **CHANGED — see below** | + +**Cohort re-carve (the one change).** In-scope 0..32: +`0..8` = TWIN BLOCK, one shared verse slice, armed `z = 1,1,2,2,3,3,4,4`, +`flow_qualia()`, 3 firing rows; `8..30` = SPREAD BLOCK, distinct slices, armed +`z` cycling 1..4, `flow_qualia()`, 3 firing rows (this block feeds L4's +non-constancy half); `30` = UNARMED (`z=0`); `31` = ORPHAN (not inserted — +keeps G10's #879 accounting caveat alive); `32..64` = OUTSIDE, armed, never +scanned (L7). + +**Justification:** the headline gate is "different lenses over **byte-identical +rows**". Disjoint slices make that sentence untestable — any digest difference +would be confounded by content. The twin block is the minimum change that makes +L1 falsifiable. The rest/CONTRA cohorts drop out because their gates (G4/G5) +are PROBE-IGNITION's, already green, and re-litigating them here would dilute +this stage's single fact. + +**Cost, bounded and stated.** §12.7 measured `stance::stream` superlinear — +`staunen(Snapshot::of(arena, 0.0))` is an O(arena) scan **per rung lift** +(`stance.rs:324-328`), which is why a 31,102-verse single-arena run blew a +10-minute budget. That does **not** bind here: each owner gets its **own** +arena over **48** verses, and at most one lens pass per owner per cycle. Order +of magnitude: ~30 armed owners × ≤5 cycles × 48 verses. **UNVERIFIED as a wall +time** — no run has happened; the build lane should print the elapsed lens time +and the orchestrator should treat >30 s as a signal to cut the spread block. + +--- + +## 6. (f) NOT claimed + +1. **No row-byte decoding, and the cognition reads past the substrate.** The + lens takes its text from the corpus slice the rows were seeded from, + selected by the owner's address and span; bloom planes are one-way (F1). + This is the §12.7 defect shape; the substrate governs selection, never + content (F1b). No substrate-data-path claim follows from any readout. +2. **No per-lens dispatch.** `stance_panel` computes all four stances in one + call; arming selects **what is read**, not what is computed (F0). No claim + that the armed bits steer any computation, and no cost difference by z. +3. **No independence between lenses.** Nietzsche is computed from Hegel's + output (`stance.rs:483-496`); Kant is derived from `ReadOut::lifts`. +4. **No fusion verdict, no κ.** z=5 is reserved (§3). +5. **No stance validity.** A readout is a read, never evidence that a stance is + right about anything. +4. **No parallelism, no durability, no scale, no multi-writer, no recovery** — + PROBE-IGNITION's not-claimed items 1–4, 12 carry over verbatim. +5. **No 36-style claim.** Six probe-local ordinals; the persona-36 bridge stays + an open non-goal (persona-vs-rung-ladder O1/O3). +6. **No rung-3 / runbook claim.** The four stances are not the 34 NARS tactics + and are not `StyleFamily` macros. +7. **No deinterlace/temporal claim in the main loop** (only the separate R2 + test would touch `QueryReference`, and then only for row admission). +8. **No claim that the four lenses are independent instruments** — Nietzsche is + computed from Hegel's output (`stance.rs:483-496`). +9. **No zero-copy claim.** `SweepSlot::payload` is `Vec` by the shipped + signature. +10. **No semantic claim about the corpus.** Qualia remain declared fixtures + (`probe_ignition.rs:201-210`); nothing is encoded from text into qualia. + +--- + +## 7. (g) Open questions + blockers for the orchestrator + +- **B1 (BLOCKER, and the deliverable per the brief): z=5 cannot run per-owner + in-cycle** on the probe's run shape — one horizon, no pool growth, no `jc` + dep. §3. **Decision needed: R2 (separate reduced test, no κ), or defer z=5 + entirely.** Either is honest; silently mapping z=5 onto another lens is not. +- **Q1 — is the cohort re-carve (§5) acceptable?** It is the only way L1 is + falsifiable, but it *replaces* the probe's REST/CONTRA cohorts inside SCOPE. + Those gates stay green in `probe_ignition.rs`, which is untouched; confirm + that is the intended split (two files, two facts) rather than one growing file. +- **Q2 — verse labels.** `stance::stream` wants `(label, text)`. Proposal: + `"kjv:{global_index:05}"`, matching `blw_fusion`'s subject format + (`blw_fusion.rs:220`). Confirm, since a label change later invalidates + digests. +- **Q3 — digest definition is load-bearing and must be pinned in the build + brief**, not left to the builder: a stable fold over the variant's contents in + the shipped iteration order, floats hashed via `to_bits`, no `HashMap` + iteration anywhere in it (`stance_panel`'s Wittgenstein arm sorts before + returning, `stance.rs:530-532` — that is the only reason it is + order-deterministic; the Kant and Hegel arms are already vector-ordered). +- **Q4 — if L3 measures Hegel AND Nietzsche empty on all 32 owners**, D-IGN-B's + effective lens axis is 2-valued (Kant vs Wittgenstein). That is still a + passing probe under §4's pre-registered fallback, but the orchestrator should + decide in advance whether a 2-valued axis is worth the build, or whether the + corpus slice should first be checked for a polarity flip. +- **Q6 — the two framing calls are yours to ratify, not mine.** (i) F0: is a + *selection* axis (not dispatch) still worth building? My answer is yes and the + argument is in F0, but the deliverable's name promises more than it delivers, + so the rename should be explicit. (ii) F1b: is reading text past the substrate + acceptable given that selection is fully substrate-governed? My answer is yes + under the stated condition; a "no" here kills or re-scopes D-IGN-B and is + defensible. **Neither should be settled by the build lane.** +- **Q7 — L1's silent twin is where hidden nondeterminism surfaces.** Two owners, + same z, byte-identical rows ⇒ bit-identical digest is only non-trivial because + the panel's Wittgenstein arm builds a `HashMap` before sorting + (`stance.rs:513-532`) and the interner assigns ids in first-sight order + (`:63-82`). If ids differ between two owners with identical text the digest + must still match — worth confirming in the build brief that per-owner + interners start empty (they will, if each owner gets a fresh + `Interner::new()`). +- **Q5 — CI.** `cycle-driver` is not in the supervisor's CI feature list (the + fifth blind gate, AGENT_LOG 2026-08-04); this test inherits that inertness. + Workflow edits are operator-approved only — recorded, not changed. + +--- + +## 8. Reads performed for this note + +`sonnet-worker-guardrails.md` (full); `AGENT_LOG.md` (first 130 lines); +plan §12.3a″ / §12.3c / §12.5 / §12.7 / §12.8 / §12.11; `persona-vs-rung-ladder.md` +(full); `probe_ignition.rs` (full, 1,389 lines); `probe-ignition-design-opus.md` +(cited via the probe's own in-file §-references and the AGENT_LOG entry — +**partial**, flagged here rather than implied); `stance.rs` (full); +`blw_fusion.rs` (targeted: constants `:100-260`, seeding/analysis `:480-600`, +deinterlace `:960-996` — **not** read end-to-end, so its gate list is cited only +where quoted); `cycle_driver.rs` (targeted: `:220-260`, `:338-360`, `:483-680`); +both Cargo manifests. No cargo command was run; nothing here is compiled or +measured. + +**Coordinator's three constraints (relayed mid-lane) — all three had been +derived independently in this lane before the message arrived, and the note now +states them in the coordinator's required terms:** no per-stance dispatch (F0 + +§1 heading + not-claimed 2); no readout slot in the shipped seam (§1's +three-option judgment, out-of-band `&mut` chosen with L6 as compensation); the +stance bodies consume text, not planes, which is the §12.7 defect shape (F1b + +not-claimed 1). The additional instruction — evaluate `ReadOut` as the readout +type — produced a **rejection with reasons** (§2): it is the panel's INPUT, is +lens-independent, and using it would make L1 fail by construction; it is +retained as printed context only. + +--- + +## ⊘ Orchestrator ratification of Q6 + Q7 (2026-08-05) + +The design lane correctly refused to settle two framing calls itself and +refused to let the build lane settle them. Both are ruled here, before the +build lands, so the record shows the decision preceded the result. + +**Q6(i) — F0, a SELECTION axis rather than dispatch: BUILD IT, renamed.** +§12.11's observable is *"different lenses over byte-identical rows produce +different readouts"*, and a selection axis satisfies that non-vacuously — +four different return types, four different derivations, and L4 can still +fail. What the finding kills is any **compute-steering** claim: no assertion +that the armed bits change what runs, and no cost difference by z. The axis +is therefore named **lens selection** in the file, in the printed banner, and +in the plan row; "dispatch" is not used for it anywhere. A deliverable whose +name promises more than it delivers is the failure mode this ruling exists to +prevent. + +**Q6(ii) — F1b, reading text past the substrate: ACCEPTABLE HERE, on the +stated condition.** The §12.7 KILL was a harness where the substrate governed +**nothing**. Here it governs **selection end-to-end** — which owner, which +span, which arming, and a phase reachable only through a sealed transition — +and L2/L5/L6/L7 each falsify one leg of that. The condition is binding and +already met in the note: **no substrate-data-path claim may follow from any +readout**, and F1/F1b appear verbatim in the not-claimed list. If a future +reader finds a readout being cited as evidence about the substrate's data +path, this ruling is void and the probe is a corpus harness in a substrate +costume. + +**Q7 — per-owner interners: relayed to the build lane as a requirement**, not +left to chance. L1's silent twin (same z, byte-identical rows ⇒ bit-identical +digest) is only non-trivial because the Wittgenstein arm builds a `HashMap` +before sorting and the interner assigns ids in first-sight order. Each owner's +lens run therefore constructs a **fresh** `Interner`/arena, and the digest must +be computed over sorted, id-independent content. If the build cannot satisfy +that, L1 must be reported as unbuildable rather than passed on lucky ids. + +**`ReadOut`-as-readout rejection: upheld.** It is the panel's INPUT, it is +lens-independent, so L1 would pass by construction — which is precisely the +vacuous-assertion shape the house rule forbids. Keeping it as printed context +(its cardinalities explain an empty lens) is the right use. diff --git a/.claude/board/exec-runs/dblw3-design-opus.md b/.claude/board/exec-runs/dblw3-design-opus.md index a16fc4599..35dd25bbb 100644 --- a/.claude/board/exec-runs/dblw3-design-opus.md +++ b/.claude/board/exec-runs/dblw3-design-opus.md @@ -638,7 +638,7 @@ restriction is what removes it. ## 5.1 The two reads, off the real surface -```rust +```text a-priori = deinterlace(&rows, &QueryReference::at(V_PIN, 0), &NoDeps) // rung 0 → Strict hindsight = deinterlace(&rows, &QueryReference::at(V_PIN, 5), &NoDeps) // rung 5 → Aware ``` @@ -878,3 +878,15 @@ example (`examples/blw_fusion.rs` or equivalent) that **consumes** `blw_tenant.rs`'s shape rather than editing it — but that placement decision is the orchestrator's, and I did not make it. `temporal.rs`, `crates/jc`, and `persist_sink.rs` are untouched by this design by construction (§12.5). + +--- + +## ⊘ Post-review corrections (2026-08-05) + +**The movement threshold is two-sided:** the movement outcome reads +`|Δκ| >= 0.10` (the Δ(pair) definition is signed; the null rule already used +`|Δ|`; the asymmetric `Δκ >= 0.10` reading left a 0.10 DECREASE classified as +neither null nor movement). Immaterial to the recorded run: the measured +V_pin movement was −0.031, which lands in the middle-ground band under both +readings. Also relabeled §5.1's fenced pseudocode from `rust` to `text` in +place (`a-priori`/`hindsight` were prose names, not Rust bindings). diff --git a/.claude/board/exec-runs/m-arm-o-arm-build.md b/.claude/board/exec-runs/m-arm-o-arm-build.md new file mode 100644 index 000000000..8daf02e55 --- /dev/null +++ b/.claude/board/exec-runs/m-arm-o-arm-build.md @@ -0,0 +1,262 @@ +# M-arm / O-arm build — Sonnet lane (edit-only, not compiled) + +Branch: `claude/x265-x266-plans-review-h9osnl` (already checked out; not switched). +File touched: `crates/lance-graph-supervisor/examples/measure_wal_curve.rs` ONLY. +No `cargo` command was run at any point (guardrail §1 rule 7). No worktree +created. No git mutation performed. No `Cargo.toml` or `.github/` touched. + +Spec followed: `.claude/plans/measure-64k-axes-v3.md` (M-arm + O-arm sections), +cross-read with `.claude/plans/measure-64k-axes-v1.md` (ground truth + A0 +measured numbers) and `.claude/plans/measure-64k-axes-v2.md` (D1 `WriteOrderKey` +field list, D2 the ordered-chunk fast path). A-arm was NOT built (deferred per +the plan; explicitly out of scope for this brief). + +## What was built + +### CSV schema (shared, both arms) +- Added `morton_reorder_ns` as the **34th column, appended at the end** — + existing 33 columns untouched, not renumbered. `Row` struct gained the field; + `header()` and `to_csv()` updated; all **8 pre-existing** `Row{}` literals + (B0, B1a, B1b, WAL-curve, Temporal, L1a, L1b, EXP-KIA) now set + `morton_reorder_ns: 0` (mechanical, done via a scripted regex insertion after + each site's `result_digest:` line, then hand-verified — 8/8 matched, no site + missed or double-patched). + +### §15 — M-arm (Morton reorder inserted before the seal) +- `WriteOrderKey { morton_chunk: u32, lane: u16, cycle_position: u64 }` per v2 + D1 — identity stays on `MailboxId` (the key is derived FROM an owner id, + never stored back onto it). +- `morton_key_for(owner)`: splits the 16-bit owner id into two 8-bit + coordinates, Morton-(Z-order-)interleaves them via the standard libmorton + bit-spread trick (verified by hand on `0xFF -> 0x5555`), reads the top 6 + bits as `morton_chunk` (0..64) and bottom 10 as `lane` (0..1,024) — a + **bijection** over the full 65,536-owner space (not a linear + `chunk = owner/1024` split like L1a's). +- Two pipelines (`run_m_arm_pipeline(morton: bool, wal_path)`, `async fn`), + `WARMUP_CYCLES=2 + MEASURED_CYCLES=16` real cycles each, identical cast + content/order between the two configs: + - cast (`emit_bootstrap_intent`, 512 B `NodeRow512` payload keyed on + `(cycle, owner)` — deliberately NOT constant, so a trajectory digest + actually depends on cycle order) → collect (`collect_casts`) → + **[Morton only] `order_cycle_stably` sort by `morton_key_for`, then + relabel `stream_position` to the sorted rank** (timed as + `morton_reorder_ns`) → seal (the REAL `DetachedCycleBatch::freeze`, unmodified) + → a REAL chunked `write_vectored` byte write of the frozen landings' + 512 B payloads (4,096-row/2 MiB groups, the house pattern from + `run_wal_curve`'s W0-current path) + one `fsync` → commit into an + in-process `MemWal` via the REAL `persist_cycle` (for the later T1 read). + - Both configs assert `bytes_written == CANONICAL_FRAME_BYTES` every cycle + (the A0 comparability assert, reused). +- **Digest identity (mandatory, an `assert_eq!`, never a print):** + `semantic_digest` — `(owner, row, payload)` sorted BY OWNER (order- + independent by construction) over the LAST measured cycle's frozen + landings — natural vs Morton. This is a genuine cross-check because the two + pipelines' `landings` orders differ (natural = stream_position-arrival + order; Morton = the reorder's rank order) while the coalesced content must + match. +- **T1 ordered-vs-unordered:** after both 16-cycle histories are sealed, + `local_trajectories` (the real `temporal.rs` function, unmodified) is timed + on both 1,048,576-row histories. +- **v2 D2 ordered-chunk fast path:** `local_trajectories_ordered_chunk_fastpath` + — validates `stream_position` strictly increasing across the WHOLE scanned + history (the collapse of "version × chunk × lane monotonic" this benchmark's + relabeling scheme produces onto one counter), then **appends directly** + (`BTreeMap::entry(...).or_default().push(...)`, **no `sort_by_key` anywhere in + the function** — verified by inspection) instead of `local_trajectories`'s + group-then-sort. Run against the Morton-ordered history; digest-compared + against the generic path's output on the SAME history (`assert!`, mandatory). + **Can-it-fire proof** (CLAUDE.md falsifiability rule): a hand-built 2-row + `Vec` with a deliberately regressed `stream_position` is asserted + to return `Err(..)` — the guard is proven non-decorative, not just asserted + to exist. +- **SUM verdict** (`delta_total = reorder_cost − (seal+write+sync+T1 savings)`, + pre-registered direction) printed with an explicit WINS/does-not-win line — + never the gain alone. +- Two CSV rows: `m_arm_natural` / `m_arm_morton`, `morton_reorder_ns` populated + only on the Morton row (never fabricated on the row that didn't pay it). + +### §16 — O-arm (ordering source) +- `PreSealRow` — a `LocalCausalRow` view over an in-flight `SweepSlot`, + letting O-B call `local_trajectories` on **cast-time** data before any + seal/WAL exists. +- `derive_order_from_temporal_replay` — sandwiched between + `// FIREWALL-START: derive_order_from_temporal_replay` / + `// FIREWALL-END: ...` sentinel comments. Groups cast-time rows via + `local_trajectories`, flattens by `BTreeMap` iteration order (owner- + ascending) — the temporal-sourced physical order, independent of arrival + order. +- **Firewall enforcement:** a compile-time self-scan in `run_o_arm` + (`include_str!("measure_wal_curve.rs")`, matching `probe_ignition.rs`'s G2a + pattern) slices the source between the two sentinel markers and asserts that + **scoped region** contains neither a `scan_sealed` call nor a `sink.sealed` + read (both needles built via `format!("{}_{}" / "{}.{}" , ...)` + concatenation so the check string itself never appears contiguously in + source), plus a can-stay-silent check that the region DOES contain + `local_trajectories`. Scoped (not whole-file) deliberately — the file has + legitimate `scan_sealed` calls elsewhere (`run_temporal`, `run_m_arm`) that + would false-positive a whole-file negative scan. +- **Non-vacuity fixture:** `scrambled_cast_order()` — a bit-reversal + permutation of the owner id, used as the CAST loop order for BOTH O-A and + O-B. Documented reasoning: every other arm in this file casts owners + 0..65,535 in ascending order, which would make O-A's arrival-order and + O-B's temporal-sourced (owner-ascending) order trivially coincide + regardless of whether O-B's derivation does real work — the exact vacuous- + assertion shape CLAUDE.md's falsifiability rule forbids. `assert_ne!` against + the identity ordering proves the fixture is actually scrambled. +- O-A: cast → collect → seal (`DetachedCycleBatch::freeze` on the natural, + arrival-ordered slots) → WAL (`MemWal::commit_cycle`) → [after 16 cycles] + temporal replay (`local_trajectories`). +- O-B: cast (same scrambled order) → collect → **`derive_order_from_temporal_replay` + first** (timed as `order_derive_ns`), relabel `stream_position` to the + derived rank → seal → WAL → temporal replay. +- **PRIMARY OBSERVABLE, computed and printed BEFORE any timing** (both in + source-code order and in the eprintln sequence): `trajectory_digest` — + the RECOVERED trajectory (owner-ascending, each chain in `cast_seq` order, + read back via `scan_sealed` + `local_trajectories`) for O-A vs O-B, + `assert!`-checked equal. +- **Kill-condition disposition, reported honestly, not glossed:** CONSTRUCTIBLE. + O-B's derivation uses `local_trajectories`'s `BTreeMap`-grouping (distinct + code path from O-A's seal-side `order_cycle_stably` `Vec::sort_by_key`) — not + literally the same code, so not a disguised O-A; both are `O(n log n)`-class + under this harness's one-row-per-owner-per-cycle shape, which the report + states explicitly rather than hiding. +- Two CSV rows: `o_a_today_pipeline` / `o_b_ordering_sourced_first`. + +## Deviations from the spec, with reasons + +1. **Both arms reuse ONE relabeling trick for `stream_position`** (Morton rank + for M-arm, temporal-derived rank for O-B) rather than adding a new, + separate physical-order field to `SweepSlot`/`DetachedCycleBatch`. Reason: + `DetachedCycleBatch::freeze` (and `persist_cycle`, which calls it + internally) always sorts landings by `stream_position` — there is no other + hook to influence physical write order without either (a) modifying + `persist_sink.rs` (out of scope — edit-only on this ONE file) or (b) locally + reimplementing `freeze` (rejected — it would stop using the REAL seal + function, weakening the fidelity of "same seal path as A0"). Relabeling + `stream_position` to the desired rank lets the REAL, unmodified + `DetachedCycleBatch::freeze`/`persist_cycle` do the ordering, at the cost of + `stream_position` no longer literally meaning "arrival order" inside these + two benchmark arms. This is flagged here explicitly since production code + depends on `stream_position`'s cross-cycle-monotonic-per-owner contract for + crash recovery (`persist_sink.rs`'s own doc comment) — verified the + relabeling PRESERVES that contract (owner X's rank is fixed across cycles, + so `position_base(N) + rank(X)` is strictly increasing in `N`), but this is + a benchmark-local repurposing, not a new production pattern to imitate. +2. **O-arm bypasses `persist_cycle`'s validation** (`CycleMismatch` / + `OwnerMismatch` checks) by calling `DetachedCycleBatch::freeze` + + `MemWal::commit_cycle` directly instead of `persist_cycle`. M-arm, by + contrast, DOES go through `persist_cycle` (after its own separate real-byte + write). This is an asymmetry between the two arms' MemWal-commit paths — + harmless here because the synthetic input is guaranteed self-consistent + (single owner-cycle pairing, no cross-owner moves), but noted rather than + silently normalized away. +3. **M-arm's real byte write uses 4,096-row (2 MiB) `write_vectored` chunks**, + not the full 5-point `SEGMENT_TABLE` sweep A0's WAL-curve arm already + covers. The plan's M-arm section does not re-ask for a segment-size sweep; + segment size is already A0's own axis. Chose one representative chunk size + (matching one of A0's five configs) so the M-arm's own axis (ordering) + isn't confounded with the segment-size axis A0 already measured. +4. **`wal_syscalls` in the M-arm CSV rows is the REAL median syscall count** + (self-caught during review — an earlier draft hardcoded `0` while + discarding a real measured value; fixed before finishing, see self-check + list below). +5. Did not build a THIRD binary or a second `#[test]` harness — everything + lives in the ONE release binary per the plan's explicit "one release binary + is a standing rule." + +## Signature/type facts verified by reading source (not compiled) + +- `MailboxId = u32` (`lance-graph-contract/src/collapse_gate.rs:121`). +- `SweepSlot`, `LandedSlot`, `CycleFrame`, `CycleId`, `DatasetVersion` are all + `pub`-fielded tuple/record structs, cross-crate-constructible (needed for the + fast path's synthetic negative-test fixture). +- `DetachedCycleBatch::freeze(frame, casts)` internally calls + `order_cycle_stably(&mut casts, |s| s.stream_position)` — confirmed by + reading `persist_sink.rs:262-278`, which is WHY the relabeling trick (point 1 + above) works and why a naive "reorder then freeze" without relabeling would + have been silently undone. +- `persist_cycle` also calls `DetachedCycleBatch::freeze` internally + (`persist_sink.rs:356`) — confirmed BEFORE designing the M-arm's dual-commit + shape (real write via local `freeze()` call + separate `persist_cycle` call + for the MemWal, rather than trying to reuse one `freeze()` result for both, + which would have required bypassing `persist_cycle`). +- `order_cycle_stably(rows: &mut [T], key: impl FnMut(&T) -> K)` is + `pub` in `persist_sink.rs` — added to this file's existing `use + lance_graph_planner::persist_sink::{...}` import list (not a new dependency, + an existing exported fn from an already-depended-on module). +- `local_trajectories(global: &[R]) -> + BTreeMap>` — added `std::collections::BTreeMap` to this + file's own `use std::collections::{...}` import (was `HashMap` only). +- `BenchRow` (already defined in this file at §11) derives `Clone` and impls + `LocalCausalRow` — reused verbatim for both new arms rather than minting a + parallel row type. + +## Self-check performed (could not run a compiler, so these substitute) + +- **Brace/paren/bracket balance**, string-literal- and line-comment-stripped, + on JUST the new §15/§16 region: **68/68 braces, 384/384 parens, 13/13 + brackets** — perfectly balanced. +- Same check on the WHOLE file after the edit: braces and brackets balanced; + parens off by exactly 1, but **the identical −1 offset already exists in the + pre-edit file** (verified via `git show HEAD:...` and diffing the same + cleaned count) — attributable to a pre-existing artifact (likely a char/byte + literal my crude string-stripper doesn't special-case), not anything + introduced here. +- Grepped every new identifier (`WriteOrderKey`, `morton_key_for`, + `MArmPhaseMedians`, `run_m_arm`/`run_m_arm_pipeline`, `PreSealRow`, + `derive_order_from_temporal_replay`, `OArmPhaseMedians`, `run_o_arm`/ + `run_o_arm_pipeline`, `scrambled_cast_order`, `trajectory_digest`, + `local_trajectories_ordered_chunk_fastpath`, `semantic_digest`) for + accidental collisions with existing names in the file — none found. +- Verified the `include_str!` firewall markers (`FIREWALL-START: ...` / + `FIREWALL-END: ...`) appear FIRST as the literal comment sentinels (line + ~2886/2912) and only SECOND as the scan's own string-literal copies (line + ~3099/3100) — `.find()` returns the leftmost match, so the scoped region + slices exactly the intended function body, not a self-referential mismatch. +- Verified all 12 `csv.write(&Row { ... })` literals (8 pre-existing + 4 new) + supply `morton_reorder_ns`; grep count of the field name = 13 (1 struct decl + + 12 literals) — matches exactly, no site missed. +- Re-derived by hand that `morton_spread_u8(0xFF) == 0x5555` per the standard + libmorton bit-spread algebra (each of 8 set bits lands on an even position of + the 16-bit result) — the one arithmetic claim in the new code worth hand- + verifying since it can't be unit-tested here. +- Traced the async/`.await` capture shape carefully after **catching and fixing + a real bug in my own first draft**: an earlier version of `run_m_arm_pipeline` + built a FRESH `tokio::runtime` INSIDE the per-cycle loop and used + `rt.block_on(async { persist_cycle(&sink, ...) })` — a non-`move` async block + trying to move an owned `Vec` out through an implicit reference + capture, which does not compile, PLUS constructing 18 throwaway runtimes. + Fixed by making `run_m_arm_pipeline` (and its caller `run_m_arm`) `async fn`, + matching the O-arm's already-correct shape, sharing the ONE + `tokio::runtime` `run()` already builds for `run_temporal`/ + `run_exp_kia_a2_64k`. + +## What could NOT be verified without a compiler + +- Exact `rustc` type-inference outcomes at generic call sites (e.g. + `order_cycle_stably(&mut slots, |s| morton_key_for(s.owner))`'s closure + return type, `collect::>()` targets) — checked by hand against the + real signatures read from source, but not compiler-confirmed. +- `clippy -D warnings` cleanliness (e.g. whether the new `Row` literals trip + `clippy::too_many_arguments`-adjacent lints — the file already carries a + module-level `#![allow(...)]` covering the cast lints used throughout, which + the new code also relies on, but a fresh clippy pass has not run). +- Actual measured numbers, obviously — this lane produced NO run output; the + orchestrator's central `cargo build --release` + one release run is the next + gate. +- Whether the 512 B-per-row real-write path for 16 cycles × 2 pipelines + (≈1 GiB total transient WAL scratch, reclaimed per-pipeline via + `fs::remove_dir_all` at the end of `run_m_arm`, matching the house + discipline that caused ENOSPC before) fits comfortably in this host's ~90%-full + `/tmp` — sized deliberately smaller than A0's own 5.8 GiB peak, but not + measured here. + +## Files touched + +- `crates/lance-graph-supervisor/examples/measure_wal_curve.rs` (only file + edited; header docstring updated to point at this report; CSV schema + + 8 pre-existing `Row{}` literals updated; §15 M-arm + §16 O-arm added; `run()` + wired to call both under the existing shared `tokio` runtime). +- `.claude/board/exec-runs/m-arm-o-arm-build.md` (this file — my own tag-file, + per the one-writer board-hygiene rule; `AGENT_LOG.md` itself was read, not + written). diff --git a/.claude/board/exec-runs/measure-wal-curve-build.md b/.claude/board/exec-runs/measure-wal-curve-build.md new file mode 100644 index 000000000..5a346f163 --- /dev/null +++ b/.claude/board/exec-runs/measure-wal-curve-build.md @@ -0,0 +1,288 @@ +# measure-wal-curve build lane report + +**Lane:** Sonnet build (edit-only, no cargo). **Deliverable:** `crates/lance-graph-supervisor/examples/measure_wal_curve.rs` +(~2,227 lines). **Spec:** `.claude/plans/measure-64k-axes-v1.md`. + +## Mandatory reads completed + +1. `.claude/v3/knowledge/sonnet-worker-guardrails.md` — followed. No cargo run + at any point. Edit-only. No git mutation, no worktree, no Cargo.toml touch. +2. `.claude/board/AGENT_LOG.md` (first ~120 lines) — read, not written. +3. `.claude/plans/measure-64k-axes-v1.md` — the full spec, built faithfully + with documented deviations below. +4. `crates/lance-graph-supervisor/tests/probe_ignition_64k.rs` — inherited + shapes: `MemWal`, `build_owner`, `flow_qualia`, `mantissa_of`, the + ONE-`StyleStrategy::plan`-call-reused pattern, `run_cycle` call shape, the + drained-writer `casts()`-is-cumulative lesson (avoided the same trap by + never re-reading `writer.casts()` as a snapshot across cycle boundaries in + this binary — each arm builds a fresh `BatchWriter` per repeat/config). +5. `crates/lance-graph-supervisor/tests/probe_ignition.rs` — the G2b + `shade_owner`-fallback pattern (a Planning-origin cast is EITHER the + style's Elixir mint or the gate's Native mint) — reused in + EXP-KIA-A2-64K's sequential convergence boundary, since the parallel + compute phase's `target` may legally be `Prune`, not just `CognitiveWork`. +6. `crates/lance-graph-planner/src/temporal.rs` — read in full. Used + `local_trajectories` (layer 1), `deinterlace` (layer 2), `QueryReference::at`, + `LocalCausalRow`/`DeinterlaceRow` trait definitions exactly as declared. +7. `crates/lance-graph-planner/src/persist_sink.rs` — read in full. Used + `WalSink`, `DetachedCycleBatch`, `SweepSlot`, `CycleFrame`, `persist_cycle` + directly (not `cycle_driver::seal_cycle`, for the arms that don't need a + real WAL commit — see deviations). + +Also read (not in the mandatory list but needed for exact signatures): +`crates/lance-graph-supervisor/src/cycle_driver.rs` (full — `collect_casts`, +`apply_sealed_transitions`, `SealedCycle`, `SealedTransition`, `MailboxFleet`), +`crates/lance-graph-planner/src/owner_adapter.rs` (full), +`crates/lance-graph-planner/src/batch_writer.rs` (full), +`crates/cognitive-shader-driver/src/mailbox_soa.rs` (grepped signatures + +read the `MailboxSoA` struct fields, `WriteCell`, `WriteOutcome`), +`crates/lance-graph-contract/src/soa_view.rs` (the `MailboxSoaView` / +`MailboxSoaOwner` traits), `crates/lance-graph-contract/src/kanban.rs` +(`KanbanColumn::advance_on_gate`, `KanbanMove`, `ExecTarget`), +`crates/lance-graph-contract/src/mul.rs` (`gate_decision_i4`, +`flow_state_i4` — read the exact FlowState thresholds to confirm the flow +qualia fixture reliably yields `Flow` regardless of mantissa magnitude), +`crates/lance-graph-planner/src/traits.rs` (`PlanContext`, `PlanInput`, +`StrategyOutcome`, `PlanStrategy`), `crates/lance-graph-contract/src/qualia.rs`, +`crates/lance-graph-contract/src/cognitive_shader.rs` (`MetaWord::new`), +`crates/lance-graph-contract/src/scheduler.rs` (`DatasetVersion`), +`crates/lance-graph-contract/src/collapse_gate.rs` (`MailboxId = u32`), +`crates/lance-graph-supervisor/Cargo.toml` (confirmed `cognitive-shader-driver` +is a **dev-dependency**, so it's reachable from an example; confirmed the +`cycle-driver` feature's `tokio` deps, and that dev-deps' `tokio` unifies to +give `rt-multi-thread` for free even though I only use `current_thread`). + +## What was built — the arms, in spec order + +- **B0** — `DummyOwner` cast baseline (no SoA, no temporal, no file I/O): + scan → cast (write-on-behalf rebind via `emit_bootstrap_intent`) → collect + → freeze. 3 repeats, medians reported. +- **B1a** — 65,536 × `MailboxSoA<4>`, one populated/firing row each (same + shape as `probe_ignition_64k.rs`): build · scan · think (real per-owner + `gate_decision_i4` + `advance_on_gate`) · cast · collect · freeze · apply + (via `apply_sealed_transitions` against a **locally-built** `SealedCycle`, + not a real WAL commit — see Deviation D1). 3 repeats. +- **B1b** — 65,536 × `NodeRow512` (`#[repr(C)] [u8; 512]`, provenance-commented + as the canonical-node-layout stand-in, not a new type proposal), memory-only, + peak RSS reported. 3 repeats. +- **L1a** — 64 × `MailboxSoA<1024>` chunks, 65,536 LOGICAL owners + (`owner = chunk*1024+lane`): build/scan/think/cast/collect/freeze at row + granularity. **`apply` intentionally NOT measured** — see Deviation D2, the + most consequential deviation in this build. +- **L1b** — the mislabelling control (64 chunks cast AS owners): proves the + held-backlog collapse (65,472 of 65,536 intents HELD because + `collect_casts`'s ≤1-move-per-owner partition sees only 64 distinct ids). +- **WAL curve** — W1-contiguous (one flat 32 MiB `Vec`, sliced into + `segments_per_cycle` contiguous `IoSlice`s, `write_vectored` looped + + counted, one `File::sync_data`) beside W0-current (REAL per-cycle + `SweepSlot` construction — one owned `Vec` clone per row — + REAL + `DetachedCycleBatch::freeze`, then `IoSlice`-per-row `write_vectored` + batched in `segment_rows`-sized groups). 5 segment configs × 2 warmup + + 16 measured cycles each, real files under `/tmp/measure_wal_curve_wal/`, + removed at the end. Median/p95 reported; the `gain()`/plateau formula + implemented exactly as specified, printed as a descriptive knee, never + pass/kill. +- **Temporal** — 16 REAL committed cycles (real `persist_cycle` against an + in-process `MemWal`, no-step landings — a sanctioned `SweepSlot` shape) → + T0 = `scan_sealed` timing → T1 = `local_trajectories` → T2 = `deinterlace` + at `QueryReference::at(8, 0)` (mid-history, so the filter is genuinely + falsifiable — NOT `ref_version=16`, which would trivially keep everything; + caught this during drafting). +- **EXP-KIA-A2-64K** — `std::thread::scope`, disjoint ranges, thread-local + `PreparedIntent` Vecs (benchmark-local type, named explicitly by the plan), + `&Fleet` shared read-only during compute, sequential rebind+cast+seal+apply + at the convergence boundary, `AtomicUsize` high-water mark for + `max_active_workers`, FNV digest over sorted `(owner, stream_position)` + pairs for the sequential-vs-parallel identity assert. Worker counts + `{1,2,4,8,16,available_parallelism()}` (std-only stand-in for "physical + cores" — no `num_cpus` dep permitted). + +CSV: one row per measured cycle/repeat/config to `$MEASURE_OUT` (default +`/tmp/measure_wal_curve.csv`), 33-column header exactly as specified, +`llc_misses` always empty. Per-configuration medians + the four closing +answers printed to stderr at the end of `run()`. + +## Bugs I self-caught and fixed before handoff + +1. **A duplicate module definition that would not have compiled.** My first + draft left `mod measure;` (a file-based module declaration) directly above + the later `mod measure { ... }` inline module body — Rust would reject + this as "the name `measure` is defined multiple times". Found on a + structural re-read; fixed by deleting the stray `mod measure;` line. This + is exactly the kind of error orchestrator-side `cargo check` exists to + catch, but I found it myself before handoff. +2. **Freeze-timer contamination (three sites: B0, B1a, L1a).** The first + draft started `t_freeze = Instant::now()` BEFORE computing `digest_bytes` + (and, in B1a, before `build_sealed_locally`'s sort over 65,536 + transitions) and only THEN called `DetachedCycleBatch::freeze` — so + `freeze_ns` would have measured freeze + digesting + sorting, not freeze + alone. Fixed by moving all untimed setup before the timer and starting + `t_freeze` immediately before the `freeze()` call in all three sites. +3. **A tautological assertion in EXP-KIA's owner-binding check.** The first + draft asserted `mv.mailbox == applied.applied.iter().find(|m| m.mailbox + == mv.mailbox).unwrap().mailbox` — this is true by construction for ANY + input (the `find` predicate guarantees the match), so it falsifies + nothing. Rewrote to compare two INDEPENDENTLY-populated fields of + `SealedTransition` (`t.owner`, set from `SweepSlot.owner` at the + sequential boundary, vs `t.mv.mailbox`, set inside + `emit_bootstrap_intent`/`rebind_bootstrap`) — a genuine cross-check. +4. **A wrong byte-size claim, 4 sites (B0/B1a/L1a/EXP-KIA) + Temporal.** + `logical_bytes: (FLEET_OWNERS as u64) * 8` assumed an 8-byte payload, but + every payload in those arms is `id.to_le_bytes()` where `id: MailboxId = + u32` — 4 bytes, not 8. Fixed all 5 occurrences to `* 4` with an inline + comment. The Temporal arm had the same bug in a different shape + (`landed.len() * CANONICAL_ROW_BYTES`, implying 512-byte canonical rows + when the actual payload there is the same 4-byte owner-id marker) — fixed + to `* 4` as well. +5. **An unused-import risk (would be a `-D warnings` failure).** `MailboxSoaOwner` + and `MailboxFleet` were imported but never used as trait-method call sites + (only via generic-function type inference, which needs no import). Removed + both; kept `MailboxSoaView` (genuinely needed for `.phase()`/`.mailbox_id()`/ + `.current_cycle()` method resolution — verified by grep count before keeping). +6. **An unused `best_workers` (assign-but-never-read).** Threaded it through + `run_exp_kia_a2_64k`'s return tuple and into the closing answer #4's + stderr line instead of leaving it dead. +7. **`b1b_peak_rss` computed but never read.** The module doc promises the + "hot representation overhead" derived metric (plan §B1); the first draft + computed B1b's median RSS and then dropped it. Added an explicit + `hot representation overhead: B1a peak_rss=...B - B1b peak_rss=...B = ...` + stderr line before the four answers. + +## Deviations from a literal reading of the plan (with reasons) + +**D1 — B1a's `apply` phase does not go through a real `WalSink::commit_cycle`.** +The plan lists "collect/freeze · apply" as B1a's measured phases without +requiring a real fsync. I built `build_sealed_locally` (a local re-derivation +of `cycle_driver::seal_cycle`'s transitions/next_position_base extraction, +provenance-commented against `cycle_driver.rs:286-301`) so `apply_ns` +measures `apply_sealed_transitions` alone, keeping the WAL-curve arm the +SOLE owner of real fsync physics (never blending the two axes, per the +plan's own "never blend two axes" rule). EXP-KIA-A2-64K, by contrast, DOES +go through a real `persist_cycle`/`MemWal` commit for its "one seal, one WAL +commit" witness assert, since that arm's own gate list explicitly requires it. + +**D2 — L1a's `apply` phase is not measured at all (left `apply_ns = 0`), and +this is the single biggest interpretive call in this build.** A Rubicon +`phase()` is scoped to ONE `MailboxSoA` instance (confirmed by reading +`MailboxSoaOwner::advance_phase`/`try_advance_phase` in +`crates/lance-graph-contract/src/soa_view.rs`), never to an individual row. +L1a's 65,536 "logical owners" share 64 physical `MailboxSoA<1024>` chunk +instances, so there is no per-logical-owner phase for +`apply_sealed_transitions` to advance. I considered three options: (a) +fabricate a per-row phase advance by routing all 1,024 logical owners in a +chunk through that chunk's single phase field (would silently misrepresent +1,023 of every 1,024 "applies" as real when only one phase transition +actually occurred); (b) invent a new per-row phase wrapper type (forbidden +by guardrail §1 rule 3, "no invention"); (c) measure build through freeze +only, document the gap, and exclude `apply` from the L1a-vs-B1a comparison +in answer #2. I chose (c) and said so both in the code (a ~20-line doc +comment at §12) and in answer #2's printed text ("apply is not comparable"). +I believe this is the honest call under the guardrails, but it is a genuine +interpretive gap against the plan's literal phrasing ("Measure separately: +construction · registration into fleet · scan · thought · cast · +collect/freeze · apply" for "B1" generally, which L1 is implicitly compared +against) — **flagging explicitly for the orchestrator to confirm or +override.** + +**D3 — "construction" and "registration into fleet" are folded into one +`build_ns` column.** The CSV schema (fixed by the plan's own "Measurement +schema" section) has no separate `registration_ns` field; the 7 named B1 +phases map onto the 7 available phase columns (build/scan/think/rebind_cast/ +collect/freeze/apply) with registration folded into build, since a single +`HashMap::insert` per owner is negligible next to `MailboxSoA` construction +and there was no schema slot to give it its own number. + +**D4 — Temporal's T0 (`scan_sealed`) timing rides the CSV's `scan_ns` column.** +The CSV schema has no dedicated T0 field; `scan_ns` is the closest-named +generic slot and is used ONLY by the Temporal row for that purpose, +documented inline at the write site. + +**D5 — The WAL-curve segment table's "N slices" are interpreted as an +explicit external batching boundary for BOTH representations**, not as +"pass everything to one `writev()` call and let `IOV_MAX` implicitly +determine batching." For W1 each segment is ONE contiguous `IoSlice` +(the "storage/cache ceiling" reading — the best achievable I/O shape); for +W0 each segment is `segment_rows` individual 512-byte `IoSlice`s (since each +row is a separately-heap-allocated `Vec` from `DetachedCycleBatch`'s +`BTreeMap`, they cannot be coalesced into one contiguous slice without +defeating the point of measuring the BTreeMap representation's real shape). +One `File::sync_data()` per cycle either way, after all segments' writes. + +**D6 — The canonical 32 MiB frame's content is identical across all 18 +cycles within one WAL-curve configuration** (not re-derived per cycle). +The plan's "constant total work" bar is read as being about bytes actually +handed to the OS via `write_vectored` (which IS constant: 16 × 32 MiB = 512 +MiB per config, exactly as specified), not about re-deriving unique content +each cycle. Flagged in a code comment at the digest-computation site since a +content-invariant digest is a weaker falsifier than a per-cycle-unique one +would be — an honest limitation, not a silent one. + +**D7 — `available_parallelism()` stands in for "physical cores".** The plan +says "1/2/4/8/16/physical cores"; std has no direct "physical core count" +API (only logical/schedulable units via `available_parallelism()`), and +`num_cpus`/`libc` are explicitly forbidden by the plan itself ("no rayon, +no libc" — I extended the same spirit to `num_cpus`, an external crate). + +## What could NOT be verified (never compiled, never run — orchestrator gates) + +Everything. Per the sonnet-worker-guardrails §1 rule 7, I ran no `cargo` +command of any kind — not `check`, `build`, `test`, `clippy`, or `fmt`. Every +signature cited above was read from source in the same pass that wrote this +file, and I did a full manual front-to-back re-read after writing (catching +the 7 bugs listed above), plus targeted `grep`-based checks for: balanced +struct-literal field lists across all ~9 `Row { ... }` construction sites, +unused-import candidates (verified via occurrence-count greps before +removing/keeping each one), `IoSlice::advance_slices`' exact stable +signature (`&mut &mut [IoSlice<'a>]`, stabilized well before this +workspace's pinned 1.95.0 toolchain), `KanbanMove`'s exact field list (5 +fields, no `#[non_exhaustive]`), `ExecTarget::Native`'s existence, and +`flow_state_i4`'s exact thresholds (confirmed the flow-qualia fixture yields +`FlowState::Flow` regardless of mantissa magnitude ≥1, so both the 1-row +B1a/EXP-KIA fixture (mantissa≈1) and the 1,024-row L1a chunk fixture +(mantissa clamped to 7) reliably gate to `Flow`). + +**Named risk I cannot rule out without a compiler:** any place where I wrote +`u32`/`u64`/`usize` and expected an implicit numeric coercion that Rust does +NOT actually perform (Rust has none — every numeric-type site was written +with an explicit `as` cast or an explicit `u64::from`/`u32::from` conversion +as far as I traced, but I did not mechanically verify every one of the +~150+ arithmetic/comparison sites in this file). This is the highest-value +thing for the orchestrator's `cargo check` pass to catch first. + +**Also unverified:** whether `IoSlice::advance_slices` requires an explicit +`'a` bound match that my elided-lifetime `write_vectored_all` signature +(`bufs: &mut [IoSlice<'_>]`) satisfies without a compile error — I am fairly +confident based on the documented stable signature and common real-world +usage of this exact idiom, but I have not compiled it. + +## Self-check against the brief's named traps + +- No hardcoded version/base that only works on cycle 1: `position_base` and + `version_counter`/`sink.head()` are threaded forward across every cycle in + every arm that has more than one cycle (WAL curve, Temporal). +- No tautological compares: found and fixed one (see bug #3 above); the rest + were written as genuine cross-checks from the start (T2's `ref_version=8` + anti-vacuity check, L1b's exact-held-count proof, G-style asserts mirroring + the probes' can-fire/can-stay-silent discipline). +- No fingerprint captured outside its window: found and fixed three + freeze-timer contamination sites (bug #2); double-checked every other + `Instant::now()`/`.elapsed()` pair in the file for the same pattern during + the final re-read. +- The cumulative-`casts()` trap: never call `writer.casts()` as a + cross-cycle cumulative counter anywhere in this file — every arm either + builds a fresh `BatchWriter` per repeat/cycle, or (EXP-KIA, single cycle + per worker-count) uses local counters incremented during the cast loop + itself, never `writer.casts().len()` deltas. +- Partial `write_vectored` loops and counts: implemented in + `write_vectored_all`, used by both W1 and W0 representations, tracks the + actual number of `write_vectored` syscalls issued (not an assumed 1). + +## Files touched + +- `crates/lance-graph-supervisor/examples/measure_wal_curve.rs` (new, ~2,227 + lines) — the deliverable. +- `.claude/board/exec-runs/measure-wal-curve-build.md` (this file, new) — my + own tag-file, per the one-writer rule. + +No other files touched. No `cargo` command run. No git command run beyond +what the harness itself may have done for read access. diff --git a/.claude/board/exec-runs/probe-ignition-api-inventory-sonnet.md b/.claude/board/exec-runs/probe-ignition-api-inventory-sonnet.md new file mode 100644 index 000000000..7d69794a8 --- /dev/null +++ b/.claude/board/exec-runs/probe-ignition-api-inventory-sonnet.md @@ -0,0 +1,894 @@ +# PROBE-IGNITION — API inventory (Sonnet grindwork lane) + +Read-only mechanical inventory. Every signature copied verbatim from source; +every claim anchored `file:line`. No cargo run. See § NOT VERIFIED for gaps. + +--- + +## A. The gate + loop surfaces — `crates/lance-graph-supervisor/src/cycle_driver.rs` + +File read in full (1811 lines). + +### `run_cycle` + +```rust +// cycle_driver.rs:446-458 +pub async fn run_cycle( + sink: &S, + fleet: &mut F, + writer: &mut BatchWriter>, + frame: CycleFrame, + position_base: u64, + watermarks: &mut HashMap>, + row_of: impl FnMut(MailboxId) -> u64, +) -> Result +where + S: WalSink, + F: MailboxFleet, +``` + +Body (`cycle_driver.rs:459-470`): calls `collect_casts(writer, frame.cycle, +position_base, row_of)` → `seal_cycle(sink, frame, collected.slots).await` +(mapping any error to `CycleError::Seal`) → `apply_sealed_transitions(fleet, +&sealed, watermarks)`. On `Ok(applied)` returns +`CycleOutcome { sealed, applied, held: collected.held }`; on +`Err((partial, cause))` returns `CycleError::Apply { partial, cause }`. +`CycleOutcome` carries: the sealed cycle (version + sparse transitions + +next stream-position base), the applied effect (advanced owners + `deferred`/ +`missing` counters, watermarks already advanced), and the intents `held` +back by the ≤1-move-per-owner partition (re-stage via `restage_held`). + +Error paths (`cycle_driver.rs:422-436`): +```rust +pub enum CycleError { + /// The WAL commit failed — no owner mutated; the boxed SealFailure + /// carries the byte-identical frozen cycle for retry via seal_cycle. + Seal(Box), + /// A guard tripped mid-apply — the applied prefix (with its watermarks + /// already advanced) is preserved; re-drive the tail via recover_fleet. + Apply { + partial: AppliedCycle, + cause: PersistError, + }, +} +``` + +### `cognitive_pass` (shared body — private, `fn` not `pub fn`) + +```rust +// cycle_driver.rs:490-495 +fn cognitive_pass( + fleet: &F, + owners: impl IntoIterator, + writer: &mut BatchWriter>, + mut think: impl FnMut(&F::Owner) -> Option<(StrategyOutcome, Vec)>, +) -> CognitiveWorkOutcome +where + F: MailboxFleet, +``` + +Iteration (`cycle_driver.rs:501-527`): +```rust +for id in owners { + let Some(owner) = fleet.owner(id) else { + continue; + }; + if owner.phase() != KanbanColumn::CognitiveWork { + continue; + } + let mut did_cast = false; + if let Some((outcome, payload)) = think(owner) { + if emit_bootstrap_intent( + &outcome, + owner.mailbox_id(), + owner.current_cycle(), + writer, + payload, + ) + .is_some() + { + did_cast = true; + } + } + if did_cast { + cast += 1; + } else { + held_owners.push(id); + } +} +``` + +**The missing-owner silent-skip (the OPEN caveat):** `cycle_driver.rs:501-504` +— `fleet.owner(id)` returning `None` (owner not registered in the fleet, or +not resolvable) just `continue`s with **no counter incremented anywhere**. +Contrast `apply_sealed_transitions` (P4b), which counts this case explicitly +via `AppliedCycle::missing` (`cycle_driver.rs:365-368`). `cognitive_pass` has +no analogous field — an owner silently dropped from the `owners` iterator +(e.g. `entered` in `run_cognitive_work`, or a caller-supplied re-poll list in +`run_cognitive_work_over`) leaves no trace in `CognitiveWorkOutcome` at all; +it is neither in `held_owners` nor counted as cast. This is a real gap versus +P4b's honesty discipline, not something proven safe by a test in this file. + +### `shade_owner` + +```rust +// cycle_driver.rs:615-620 +#[must_use] +pub fn shade_owner( + owner: &O, + qualia: &QualiaI4_16D, + mantissa: i8, + reliability: f32, +) -> Option +``` + +Body (`cycle_driver.rs:621-635`): +```rust +let phase = owner.phase(); +let gate = gate_decision_i4(qualia, mantissa); +let to = phase.advance_on_gate(&gate)?; +Some(StrategyOutcome { + reliability, + intended_move: Some(KanbanMove { + mailbox: 0, // bootstrap sentinel + from: phase, + to, + witness_chain_position: 0, + exec: ExecTarget::Native, + }), +}) +``` +`gate_decision_i4` returns `GateDecision::{Flow, Hold{reason}, Block{reason}}` +(see § B). `KanbanColumn::advance_on_gate(&GateDecision) -> Option` +is the DAG lowering — its own definition was **not** read in this file (it +lives in `lance_graph_contract::kanban`; not opened this pass — see § NOT +VERIFIED). From the test evidence at `cycle_driver.rs:1682-1726`: `Flow` at +`CognitiveWork` → `Evaluation` ("forward"); `Block` at `Planning` → `Prune` +("Prune-where-legal"); `Hold` (or no legal successor, e.g. `Block`/`Flow` at +the absorbing `Commit` column) → `None`. + +### `run_cognitive_work_gated` / `run_cognitive_work_gated_over` + +```rust +// cycle_driver.rs:644-649 +pub fn run_cognitive_work_gated( + fleet: &F, + applied: &AppliedCycle, + writer: &mut BatchWriter>, + mut read_gate: impl FnMut(&F::Owner) -> Option<(QualiaI4_16D, i8, f32, Vec)>, +) -> CognitiveWorkOutcome +where + F: MailboxFleet, +``` +Body (`cycle_driver.rs:653-657`) delegates to `run_cognitive_work(fleet, +applied, writer, |owner| { let (qualia, mantissa, reliability, payload) = +read_gate(owner)?; let outcome = shade_owner(owner, &qualia, mantissa, +reliability)?; Some((outcome, payload)) })`. The extractor closure's exact +type is `impl FnMut(&F::Owner) -> Option<(QualiaI4_16D, i8, f32, Vec)>` +— tuple order is `(qualia, signed_mantissa, reliability, payload)`. + +```rust +// cycle_driver.rs:662-667 +pub fn run_cognitive_work_gated_over( + fleet: &F, + owners: &[MailboxId], + writer: &mut BatchWriter>, + mut read_gate: impl FnMut(&F::Owner) -> Option<(QualiaI4_16D, i8, f32, Vec)>, +) -> CognitiveWorkOutcome +where + F: MailboxFleet, +``` +Same extractor closure shape; delegates to `run_cognitive_work_over` with the +identical `shade_owner`-wrapping closure (`cycle_driver.rs:671-675`). + +`held_owners` (on `CognitiveWorkOutcome`, `cycle_driver.rs:475-484`): owners +evaluated this pass that produced **no cast** — a gate `Hold`, a declined/ +unfinished thought (`think`/`read_gate` returned `None`), or a `None` from +`shade_owner` (no legal successor). Doc explicitly: "A Hold is a reschedule, +not a strand" — feed `held_owners` back into `run_cognitive_work_over` / +`run_cognitive_work_gated_over` on a later cycle. + +### `MailboxFleet` trait + +```rust +// cycle_driver.rs:179-186 +pub trait MailboxFleet { + type Owner: MailboxSoaOwner; + fn owner(&self, id: MailboxId) -> Option<&Self::Owner>; + fn owner_mut(&mut self, id: MailboxId) -> Option<&mut Self::Owner>; +} +``` + +`HashMap` blanket impl (`cycle_driver.rs:190-198`): +```rust +impl MailboxFleet for HashMap { + type Owner = O; + fn owner(&self, id: MailboxId) -> Option<&O> { + self.get(&id) + } + fn owner_mut(&mut self, id: MailboxId) -> Option<&mut O> { + self.get_mut(&id) + } +} +``` +Bound: `O: MailboxSoaOwner` only — no `Hash`/`Eq` bound stated explicitly on +`O` (those are already required transitively by `HashMap` +itself needing `MailboxId: Hash + Eq`, not `O`). `MailboxId` is +`lance_graph_contract::collapse_gate::MailboxId` (imported +`cycle_driver.rs:68`; underlying type not re-verified in this pass — see +`u32` assumption noted where `u64::from` is used as `row_of` in tests, e.g. +`cycle_driver.rs:933,957` etc., consistent with `MailboxId = u32`). + +### Public struct fields — `CycleOutcome`, `SealFailure`, `CycleError`, `HeldIntent`, `CollectedCasts` + +```rust +// cycle_driver.rs:412-420 +pub struct CycleOutcome { + pub sealed: SealedCycle, + pub applied: AppliedCycle, + pub held: Vec, +} +``` + +```rust +// cycle_driver.rs:120-129 +pub struct SealFailure { + pub frame: CycleFrame, + pub casts: Vec, + pub cause: PersistError, +} +``` + +```rust +// cycle_driver.rs:422-436 (see full body above under run_cycle) +pub enum CycleError { + Seal(Box), + Apply { partial: AppliedCycle, cause: PersistError }, +} +``` + +```rust +// cycle_driver.rs:152-158 +pub struct HeldIntent { + pub owner: MailboxId, + pub mv: KanbanMove, +} +``` + +```rust +// cycle_driver.rs:163-170 +pub struct CollectedCasts { + pub slots: Vec, + pub held: Vec, +} +``` + +Also relevant (referenced throughout, not explicitly requested but load-bearing): +```rust +// cycle_driver.rs:87-96 SealedTransition +pub struct SealedTransition { + pub stream_position: u64, + pub owner: MailboxId, + pub mv: KanbanMove, +} +// cycle_driver.rs:103-114 SealedCycle +pub struct SealedCycle { + pub version: DatasetVersion, + pub transitions: Vec, + pub next_position_base: u64, +} +// cycle_driver.rs:133-148 AppliedCycle +pub struct AppliedCycle { + pub version: DatasetVersion, + pub applied: Vec, + pub deferred: usize, + pub missing: usize, +} +``` + +### `restage_held` + +```rust +// cycle_driver.rs:261 +pub fn restage_held(writer: &mut BatchWriter>, held: Vec) -> usize +``` +Body (`cycle_driver.rs:262-267`): for each `HeldIntent`, calls +`writer.cast(h.owner, vec![h.mv], Vec::new())` (intent-only re-cast — empty +payload, since the original cast's payload already sealed with its cycle). +Returns `held.len()`. + +--- + +## B. The style + qualia surfaces + +### `resolve_style` + +```rust +// crates/lance-graph-planner/src/strategy/style_strategy.rs:231 +fn resolve_style(ctx: &PlanContext) -> ThinkingStyle +``` +(private `fn`, not `pub`). Body (`style_strategy.rs:232-251`): reads +`ctx.thinking_style: &Option>` — filters out `None` and empty +vectors, returning `DEFAULT_STYLE` (= `ThinkingStyle::Analytical`, +`style_strategy.rs:46`) for either. Otherwise reads exactly three indices of +the 23D vector — **the same axes `selector.rs::style_alignment` uses**: +```rust +let analytical = v.get(4).copied().unwrap_or(0.0); +let creative = v.get(3).copied().unwrap_or(0.0); +let depth = v.first().copied().unwrap_or(0.0); // index 0 +let max = analytical.max(creative).max(depth); +if max <= 0.0 { + DEFAULT_STYLE +} else if (analytical - max).abs() < f64::EPSILON { + ThinkingStyle::Analytical // Analytical cluster → TruthAwareInference +} else if (creative - max).abs() < f64::EPSILON { + ThinkingStyle::Creative // Creative cluster → StructuralDivergence +} else { + ThinkingStyle::Reflective // depth-dominant → Meta cluster → Infrastructure +} +``` +Explicit doc note (`style_strategy.rs:229-230`): this is **not** the contract +`style_vector`/i4-32D `StyleRecipe` surface — a separate, deferred decode. + +### `reliability_for` + +```rust +// style_strategy.rs:328 +pub fn reliability_for(style: ThinkingStyle, ctx: &PlanContext) -> f32 +``` +Body (`style_strategy.rs:329-332`): +```rust +match ctx.witness.as_ref().and_then(|w| w.rung()) { + Some(rung) => Self::reliability_at(style, ctx, rung), + None => Self::reliability_of(style, ctx), // unstratified fallback +} +``` +`WitnessWindow::rung()` (`traits.rs:121-128`) returns `Some(RungLevel)` only +on `WaveGrounding::Causal` (via `RungLevel::for_pass(settle_pass)`); `None` on +`Escalate`/`Unbound` — absence must never be read as `RungLevel::Surface`. + +Related entry points on `StyleStrategy` (`style_strategy.rs:306,361`): +- `reliability_of(style, ctx) -> f32` — unstratified, calls + `reliability_at(style, ctx, RungLevel::Transcendent)`. +- `reliability_at(style: ThinkingStyle, ctx: &PlanContext, rung: RungLevel) -> f32` + — builds `ThoughtCtx` via `thought_ctx_from(ctx)`, runs every kernel in + `recipes_for_at(style, rung)` (`.run(&mut tc)`, mutating `tc.confidence`), + returns `tc.confidence.clamp(0.0, 1.0)`. + +### `intended_move` + +```rust +// style_strategy.rs:391 +fn intended_move(_style: ThinkingStyle) -> KanbanMove +``` +(private `fn`). Body (`style_strategy.rs:392-398`) — constant regardless of +`style` (the `_style` param is unused, per the doc: the move is a *structural +constant of the Planning→CognitiveWork crossing*, not style-conditioned): +```rust +KanbanMove { + mailbox: 0, + from: KanbanColumn::Planning, + to: KanbanColumn::CognitiveWork, + witness_chain_position: 0, + exec: ExecTarget::Elixir, +} +``` + +### `PlanInput` / `StrategyOutcome` — `crates/lance-graph-planner/src/traits.rs` + +```rust +// traits.rs:193-203 +pub struct PlanInput { + pub plan: Option, + pub context: PlanContext, + pub outcome: Option, +} +``` + +```rust +// traits.rs:181-190 +pub struct StrategyOutcome { + pub reliability: f32, + pub intended_move: Option, +} +``` +Derives: `Debug, Clone, Copy, PartialEq` (`traits.rs:181`). + +```rust +// traits.rs:132-152 +pub struct PlanContext { + pub query: String, + pub features: QueryFeatures, + pub free_will_modifier: f64, + pub thinking_style: Option>, + pub nars_hint: Option, + pub witness: Option, +} +``` +`context` on `PlanInput` is a plain owned `PlanContext` (not generic/typed +beyond this struct); constructed by callers directly as a struct literal +(e.g. test helper `ctx_with` at `style_strategy.rs:450-459`). + +```rust +// traits.rs:88-98 +pub struct WitnessWindow { + pub rows: Vec<(usize, CausalWitnessFacet)>, + pub focal_idx: usize, + pub locus: Locus, + pub passes: u8, +} +``` + +### `gate_decision_i4` / `QualiaI4_16D` / `GateDecision` — `lance_graph_contract::mul::i4_eval` + +```rust +// crates/lance-graph-contract/src/mul.rs:575 +pub fn gate_decision_i4(qualia: &QualiaI4_16D, signed_mantissa: i8) -> GateDecision +``` +Module: `pub mod i4_eval` at `mul.rs:448`, function is `#[inline]`, heap-free +except the `String` reasons on `Hold`/`Block`. Body composes +`trust_texture_i4(qualia)` + `flow_state_i4(qualia, signed_mantissa)` +(`mul.rs:511,543`) via a `match (texture, flow)` (`mul.rs:579-599`): +`Uncertain → Block`; `Underconfident + Anxiety → Block`; `Overconfident → Hold`; +`_ + Anxiety → Hold`; `(Calibrated|Underconfident) + (Flow|Transition) → Flow`; +else `Hold`. + +```rust +// mul.rs:144-151 +pub enum GateDecision { + Flow, + Hold { reason: String }, + Block { reason: String }, +} +``` +Cannot be `#[repr(u8)]` (carries `String` payloads); `to_disc(&self) -> u8` +(`mul.rs:158-164`) maps `Flow=0, Hold=1, Block=2` (locked mapping, D-CSV-13b). + +`QualiaI4_16D` — `crates/lance-graph-contract/src/qualia.rs:173-263`: +```rust +#[repr(C, align(8))] +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default)] +pub struct QualiaI4_16D(pub u64); +``` +8 bytes; 16 dims × i4 (range −8..+7), one dim per nibble (`QUALIA_I4_DIMS = 16`, +`qualia.rs:140`; labels `QUALIA_I4_LABELS`, `qualia.rs:146-163` — matches first +16 of the canonical 17 `AXIS_LABELS`, "integration" dim 16 dropped). +Construction: `QualiaI4_16D::ZERO` (`qualia.rs:178`), `.with(dim: usize, value: +i8) -> Self` builder (`qualia.rs:207-211`, clamps to −8..+7), `.set(dim, value)` +in-place mutator (`qualia.rs:195-203`), `.get(dim) -> i8` sign-extending reader +(`qualia.rs:184-190`), `from_f32_17d(&QualiaVector) -> Self` / +`to_f32_17d(self) -> QualiaVector` round-trip converters +(`qualia.rs:219-251`), `.magnitude(self) -> i8` = `coherence(dim9) +.saturating_mul(valence(dim1))` (`qualia.rs:258-262`). + +--- + +### `MetaWord` — `lance_graph_contract::cognitive_shader` + +```rust +// crates/lance-graph-contract/src/cognitive_shader.rs:42-44 +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +#[repr(transparent)] +pub struct MetaWord(pub u32); +``` +Doc (`cognitive_shader.rs:38-41`): "Packed u32 per row: `thinking(6) + +awareness(4) + nars_f(8) + nars_c(8) + free_e(6)`." Bit layout +(`cognitive_shader.rs:46-76`): +```rust +pub const fn new(thinking: u8, awareness: u8, nars_f: u8, nars_c: u8, free_e: u8) -> Self +// thinking : bits 0..6 (mask 0x3F) +// awareness : bits 6..10 (mask 0x0F << 6) +// nars_f : bits 10..18 (u8 << 10) +// nars_c : bits 18..26 (u8 << 18) +// free_e : bits 26..32 (mask 0x3F << 26) +``` +Getters: `.thinking() -> u8`, `.awareness() -> u8`, `.nars_f() -> u8`, +`.nars_c() -> u8`, `.free_e() -> u8` (`cognitive_shader.rs:56-75`), each +masking/shifting the packed `u32`. + +**ThinkingStyle mapping — NOT the contract's 36-style `ThinkingStyle` enum.** +Grepped every `MetaWord::new(...)` call site plus `auto_style` (the only +producer with a real style semantic, `cognitive-shader-driver/src/ +engine_bridge.rs:295-298,760`): the `thinking` field is populated from a +**separate, local 6-bit ordinal registry**, +`cognitive_shader_driver::auto_style` (`crates/cognitive-shader-driver/src/ +auto_style.rs:23-34`): +```rust +pub const DELIBERATE: u8 = 0; +pub const ANALYTICAL: u8 = 1; +pub const CONVERGENT: u8 = 2; +pub const SYSTEMATIC: u8 = 3; +pub const CREATIVE: u8 = 4; +pub const DIVERGENT: u8 = 5; +pub const EXPLORATORY: u8 = 6; +pub const FOCUSED: u8 = 7; +pub const DIFFUSE: u8 = 8; +pub const PERIPHERAL: u8 = 9; +pub const INTUITIVE: u8 = 10; +pub const METACOGNITIVE: u8 = 11; +``` +Doc comment on this table (`auto_style.rs:21-22`): "0..11 matches +`thinking_engine::cognitive_stack::ThinkingStyle::all()`" — a **third**, +different `ThinkingStyle` type (in the separate `thinking-engine` crate), not +`lance_graph_contract::thinking::ThinkingStyle` (the 36-style / `StyleCluster` +enum `style_strategy.rs` uses) and not the `StyleFamily` used elsewhere in the +contract. `style_from_qualia(q: &[f32]) -> u8` (`auto_style.rs:37`+) derives +the ordinal from a qualia shape (dominant-axis heuristic among certainty/ +arousal/urgency/depth/valence), never from a `lance_graph_contract::thinking:: +ThinkingStyle` value. **No code path was found in this pass that writes +`lance_graph_contract::thinking::ThinkingStyle` (or its `cluster()`/`tau()`) +into a `MetaWord`.** `MetaFilter::thinking_mask: u64` (bitset over 64 possible +`auto_style` ordinals; `cognitive_shader.rs:81-107`) is the corresponding +read-side prefilter, AND-combined with `awareness_min`/`nars_f_min`/ +`nars_c_min`/`free_e_max`. + +--- + +## C. The owner/tenant surfaces — `crates/cognitive-shader-driver/src/mailbox_soa.rs` + +Targeted reads (constructor, write/populate surface, contract-trait impls, +qualia/energy/meta accessors); not read end-to-end (file is large, per the +brief). + +### `MailboxSoA` — constructor + +```rust +// mailbox_soa.rs:58 struct decl (generic param) +pub struct MailboxSoA { /* ... */ } + +// mailbox_soa.rs:232-233 +pub type DefaultMailboxSoA = MailboxSoA<1024>; + +// mailbox_soa.rs:292 +pub fn new(mailbox_id: MailboxId, w_slot: u8, threshold: f32) -> Self +``` +Panics (`mailbox_soa.rs:293-296`) if `w_slot >= 64` ("w_slot must fit in 6 +bits (0..=63 per plan §6 L-6), got {w_slot}"). Zero-initializes every column +(`energy`, `plasticity_counter`, `last_active_cycle`/`last_write_cycle` to +`u32::MAX` sentinels, `current_cycle = 0`, `edges/qualia/meta/entity_type`, +`temporal/expert/sigma`, heap-allocated `content`/`topic`/`angle` planes of +`N * WORDS_PER_FP` `u64` each, `frozen_style/learned_style/explore_style` +`[[0u8;12]; N]`, `populated = 0`, `phase: KanbanColumn::Planning`) +(`mailbox_soa.rs:297-334`). + +### `write_row` / `WriteOutcome` + +```rust +// mailbox_soa.rs:417 +pub fn write_row(&mut self, row: usize, cycle: u32, cell: &WriteCell<'_>) -> WriteOutcome +``` +```rust +// mailbox_soa.rs:241-254 +pub enum WriteOutcome { + /// cycle == current_cycle — cell applied, last_write_cycle[row] stamped. + Accepted, + /// cycle strictly behind current_cycle (wrap-aware) — nothing mutated, + /// stale_write_count incremented. + Stale, + /// cycle strictly ahead of current_cycle (wrap-aware) — nothing mutated. + Future, +} +``` +Gate logic (`mailbox_soa.rs:417-463`): `row >= N` → `Stale` (no mutation, "a +row we do not own is never written"). Otherwise wrap-aware delta = +`self.current_cycle.wrapping_sub(cycle)`: `delta == 0` → apply every `Some` +field of `cell` via the per-column setters (`set_content`/`set_topic`/ +`set_angle`/`set_edge`/`set_qualia`/`set_meta`/`set_entity_type`/ +`set_temporal`/`set_expert`/`set_sigma`), stamp `last_write_cycle[row] = +cycle`, return `Accepted`; `delta < 0x8000_0000` → `stale_write_count` +saturating-incremented, return `Stale`; else → `Future` (no mutation). + +`WriteCell<'a>` (`mailbox_soa.rs:262-283`, `#[derive(Debug, Clone, Default)]`): +```rust +pub struct WriteCell<'a> { + pub content: Option<&'a [u64]>, // WORDS_PER_FP u64, borrowed + pub topic: Option<&'a [u64]>, + pub angle: Option<&'a [u64]>, + pub edge: Option, + pub qualia: Option, + pub meta: Option, + pub entity_type: Option, + pub temporal: Option, + pub expert: Option, + pub sigma: Option, +} +``` + +### `set_populated` / `current_cycle` + +```rust +// mailbox_soa.rs:495 +pub fn set_populated(&mut self, n: usize) // = n.min(N); a DECLARATION, not an implicit counter +// mailbox_soa.rs:486 +pub fn populated(&self) -> usize +``` +`current_cycle` is read via the `MailboxSoaView::current_cycle(&self) -> u32` +trait impl (`mailbox_soa.rs:872-874`, returns `self.current_cycle`); advanced +via `pub fn tick(&mut self)` (`mailbox_soa.rs:399-401`, +`current_cycle.wrapping_add(1)`) — **not** a field named `current_cycle()` as +an inherent method; the inherent field is `pub(crate)`-scoped implicitly +through the struct (not confirmed `pub` — see § NOT VERIFIED) and reached +through the trait method in all call sites grepped. + +### `MailboxSoaOwner` / `MailboxSoaView` impls for `MailboxSoA` + +```rust +// mailbox_soa.rs:852 impl block header +impl MailboxSoaView for MailboxSoA { + fn mailbox_id(&self) -> MailboxId { self.mailbox_id } // :854-856 + fn n_rows(&self) -> usize { self.populated } // :858-866 (NOT N — populated) + fn w_slot(&self) -> u8 { self.w_slot } // :868-870 + fn current_cycle(&self) -> u32 { self.current_cycle } // :872-874 + fn phase(&self) -> KanbanColumn { self.phase } // :876-878 + fn identity_plane_at(&self, row: usize, plane: IdentityPlane) -> Option<&[u64]> // :886-895 + fn style_lane_at(&self, row: usize, lane: StyleLane) -> Option<[u8; 12]> // :902-911 + fn energy(&self) -> &[f32] { &self.energy } // :913-915 + fn edges_raw(&self) -> &[u64] // unsafe repr(transparent) cast, :917-931 + fn meta_raw(&self) -> &[u32] // unsafe repr(transparent) cast, :934-941 + fn entity_type(&self) -> &[u16] { &self.entity_type } // :944-946 +} +``` +`identity_plane_at`/`style_lane_at` both guard `row >= self.populated` → +`None` (never reads a zero-padded capacity row) before dispatching to +`content_row`/`topic_row`/`angle_row` or `frozen_style`/`learned_style`/ +`explore_style`. + +```rust +// mailbox_soa.rs:949 +impl MailboxSoaOwner for MailboxSoA { + fn advance_phase(&mut self, to: KanbanColumn) -> KanbanMove // :953-973 +} +``` +Body: `from = self.phase; self.phase = to;` then constructs `KanbanMove { +mailbox: self.mailbox_id, from, to, witness_chain_position: +self.current_cycle, exec: ExecTarget::Native }`. **`try_advance_phase` is NOT +overridden here** — `MailboxSoA` uses the trait's DEFAULT impl from +`lance_graph_contract::soa_view::MailboxSoaOwner` (see below); no +`fn try_advance_phase` appears anywhere in `mailbox_soa.rs` (grep returned +zero hits in this file). + +### `MailboxSoaOwner`/`MailboxSoaView` trait definitions — +`crates/lance-graph-contract/src/soa_view.rs` + +```rust +// soa_view.rs:67-89 (required methods only; several defaulted methods omitted, see below) +pub trait MailboxSoaView { + fn mailbox_id(&self) -> MailboxId; + fn n_rows(&self) -> usize; + fn w_slot(&self) -> u8; + fn current_cycle(&self) -> u32; + fn phase(&self) -> KanbanColumn; + fn energy(&self) -> &[f32]; + fn edges_raw(&self) -> &[u64]; + fn meta_raw(&self) -> &[u32]; + fn entity_type(&self) -> &[u16]; + // defaulted (deferred-binding, all return None unless overridden): + fn class_id(&self) -> &[u16] { self.entity_type() } // :99-102 + fn class_id_at(&self, row: usize) -> u16 { self.entity_type()[row] } // :105-108 + fn row_for_local_key(&self, _local_key: u64) -> Option { None } // :125-128 + fn hhtl_path_at(&self, _row: usize) -> Option { None } // :143-146 + fn edge_block_at(&self, _row: usize) -> Option { None } // :162-165 + fn identity_plane_at(&self, _row: usize, _plane: IdentityPlane) -> Option<&[u64]> { None } // :176-179 + fn style_lane_at(&self, _row: usize, _lane: StyleLane) -> Option<[u8; 12]> { None } // :195-198 + fn triangle_at(&self, row: usize, family: u8) -> Option<(u8, u8, u8)> { /* composes style_lane_at ×3 */ } // :209-219 + fn style_rails_at(&self, row: usize, lane: StyleLane) -> Option<[(u8,u8);6]> { /* composes style_lane_at */ } // :240-251 + fn energy_at(&self, row: usize) -> f32 { self.energy()[row] } // :283-286 +} +``` +**No `fn qualia(&self)` on this trait.** Explicit comment +(`soa_view.rs:253-255`): "the qualia column (`QualiaI4_16D`) accessor is +intentionally omitted — add `fn qualia(&self) -> &[crate::qualia:: +QualiaI4_16D]` when the first consumer (planner strategy selection) needs +it; keep the read surface minimal until then." This directly matches +`cycle_driver.rs`'s own doc note that `shade_owner`'s qualia/mantissa are +caller-supplied because `MailboxSoaView` does not yet expose `qualia()`. +Similarly no `episodic_witness` accessor yet (`soa_view.rs:257-277`, deferred +for `EpisodicWitness64`, not yet a code symbol). + +```rust +// soa_view.rs:295-321 +pub trait MailboxSoaOwner: MailboxSoaView { + fn advance_phase(&mut self, to: KanbanColumn) -> KanbanMove; + + fn try_advance_phase( + &mut self, + to: KanbanColumn, + ) -> Result { + let from = self.phase(); + if from.can_transition_to(to) { + Ok(self.advance_phase(to)) + } else { + Err(RubiconTransitionError { from, to }) + } + } +} +``` +`try_advance_phase` is a **default trait method** — checks +`KanbanColumn::can_transition_to` before calling the (required, unchecked) +`advance_phase`; returns `RubiconTransitionError { from, to }` on an illegal +edge with no mutation. `MailboxSoA` inherits this default unmodified. + +### `MailboxSoaView` read accessors used by `blw_fusion.rs`-style consumers + +`identity_plane_at` is the accessor `blw_fusion.rs` and any Hamming/CAM +distance reader would use (confirmed present and overridden on +`MailboxSoA`, `mailbox_soa.rs:886-895`, guarded by `populated`). No other +"identity_plane_at etc." read accessors beyond `style_lane_at`/`triangle_at`/ +`style_rails_at`/`energy_at` were found on the trait (full list above is +exhaustive for this file). + +--- + +## D. The seeding surfaces + +### `examples/blw_fusion.rs` — seed/seal loop call sequence + +Landed 2026-08-04 (per `AGENT_LOG.md:1-8`). Key calls, in execution order, +with line numbers: + +``` +blw_fusion.rs:723 let mut owner: Tenant = MailboxSoA::new(TENANT_ID, TENANT_W_SLOT, TENANT_THRESHOLD); +blw_fusion.rs:726 seed_slice(&mut owner, 0, &verses[0..SLICE]) -- slice 1, BEFORE the loop +blw_fusion.rs:727 owner.set_populated(seated_total); +blw_fusion.rs:728 owner.tick(); -- cycle 0 -> 1 +blw_fusion.rs:730 let sink = MemWal::new(); -- local WalSink impl, mirrors persist_sink::FakeWalSink +blw_fusion.rs:731 let mut writer: BatchWriter = BatchWriter::new(); + +-- per cycle c in 1..=8 (plan: KanbanColumn DAG Planning->CognitiveWork->Evaluation->Plan->Planning, twice) -- +blw_fusion.rs:791 assert_eq!(owner.phase(), spec.from, ...) +blw_fusion.rs:797 seed_slice(&mut owner, (c-1)*SLICE, &verses[(c-1)*SLICE..c*SLICE]) -- c > 1 only +blw_fusion.rs:803 owner.set_populated(seated_total); +blw_fusion.rs:817 rank_verdicts(&owner, seated_total, &seed_a) -- score+verdict FULL pool +blw_fusion.rs:818 rank_verdicts(&owner, seated_total, &seed_b) +blw_fusion.rs:819-821 contains_all(&owner, row, &god_probe) per row -- verdict_z +blw_fusion.rs:835 emit_bootstrap_intent(&outcome, owner.mailbox_id(), owner_cycle, &mut writer, span) +blw_fusion.rs:837 writer.on_behalf_of(cast) +blw_fusion.rs:846 writer.intent_moves(cast) +blw_fusion.rs:854-861 build one SweepSlot { cycle: spec.id, stream_position: c as u64, + owner: cast_owner, row: 0, paired_move: Some(cast_move), + payload: span.to_le_bytes().to_vec() } +blw_fusion.rs:864 persist_cycle(&sink, CycleFrame::new(spec.id, base), slots).await? +blw_fusion.rs:871 sink.scan_sealed(Some(base)).await? +blw_fusion.rs:872 recover_and_apply(&mut owner, &sealed, watermark).map_err(|(_, e)| e)? +blw_fusion.rs:873 watermark = recovered.watermark; +blw_fusion.rs:930 owner.tick(); +``` + +**Load-bearing finding:** this example does **not** call +`cycle_driver::seal_cycle` / `cycle_driver::apply_sealed_transitions` / +`cycle_driver::run_cycle` at all. It calls the lower-level +`lance_graph_planner::persist_sink::{persist_cycle, recover_and_apply}` +directly (confirmed import at `blw_fusion.rs:101`: `SweepSlot, WalSink, +WriteFailed` from that module, plus `persist_cycle`/`recover_and_apply` used +inline — exact `use` line for those two symbols not captured in this pass, +see § NOT VERIFIED) and its own local `MemWal` (`blw_fusion.rs:393-478` per +the earlier grep, `impl WalSink for MemWal` at `:422`) rather than +`cycle_driver::FakeWalSink`. The lifecycle intent is built by hand via +`bootstrap_intent(from, to) -> KanbanMove` (`blw_fusion.rs:530-538`, mailbox +0, witness_chain_position 0, `ExecTarget::Elixir`) and staged through +`emit_bootstrap_intent` (from `lance_graph_planner::owner_adapter`, same +function `cycle_driver.rs` uses in `cognitive_pass`) — so the P4c +rebind-and-cast seam is shared, but the P4a/P4b seal+apply seam +(`collect_casts`/`seal_cycle`/`apply_sealed_transitions`) is **not** +exercised by this example; it reimplements an equivalent single-slot +seal/apply by hand each cycle. + +### `crates/deepnsm-v2/src/` — lib surface for verses/triplets + +`lib.rs` module list (`crates/deepnsm-v2/src/lib.rs:37-50`): `ancestry`, +`basin`, `belief`, `codebook`, `corpus`, `evidence`, `fsm`, `introspect`, +`reason`, `shape`, `space`, `spo`, `vocab`, `wave`. + +**`corpus` module** (`crates/deepnsm-v2/src/corpus.rs`) — text → verses: +```rust +pub const GUTENBERG_FOOTER: &str = "*** END OF THE PROJECT GUTENBERG"; // corpus.rs:13 +pub const KJV_OLD_TESTAMENT_VERSES: usize = 23_145; // corpus.rs:24 (documentation only, not a threshold) +pub fn is_verse_marker(tok: &str) -> bool // corpus.rs:28 +pub fn split_verses(text: &str) -> Vec // corpus.rs:56 +pub struct CorpusSplit { pub verses: Vec, pub crossed_new_testament: bool } // corpus.rs:71-81 +pub fn split_verses_detailed(text: &str) -> CorpusSplit // corpus.rs:96 +``` +`split_verses` splits on `d+:d+` verse markers (e.g. `1:1`), truncating body +text at the FULL `GUTENBERG_FOOTER` string (not a bare `***`, which is only +the OT/NT separator and must not be treated as end-of-file — the historical +truncation bug this module fixes, doc at `corpus.rs:37-54`). + +**`bible_wave.rs` corpus-to-triples shape** (referenced in `lib.rs:31-33` doc, +not independently opened this pass): "`examples/bible_wave.rs` runs the whole +KJV (23,145 verses = one 64k tile) through FSM → SPO → `TemporalStream`" — see +§ NOT VERIFIED (file not read directly). + +**Top-level `Nsm` engine + `TemporalStream`** (`lib.rs:93-220`): +```rust +pub struct Nsm { + pub vocab: PaletteVocab, // frequency-ranked ROUTING address + pub space: Cam96Space, // CAM-PQ 96 meaning-DISTRIBUTION space + codes: Vec, // private; per-word-id 96-bit meaning code +} +impl Nsm { + pub fn new(vocab: PaletteVocab, space: Cam96Space) -> Self; // lib.rs:106 + pub fn with_codes(vocab: PaletteVocab, space: Cam96Space, codes: Vec) -> Self; // lib.rs:118 + pub fn ingest(&self, tokens: &[Tagged]) -> Vec; // lib.rs:128, delegates to fsm::parse_to_spo + pub fn code(&self, word: &str) -> Option<&Cam96>; // lib.rs:135 + pub fn word_similarity(&self, a: &str, b: &str) -> Option; // lib.rs:143 + pub fn triple_similarity(&self, a: Spo, b: Spo) -> [Option; 3]; // lib.rs:151 +} + +pub struct TemporalStream { entries: Vec<(u64, Spo)> } // lib.rs:173-176, private field +impl TemporalStream { + pub fn new() -> Self; // lib.rs:181 + pub fn push(&mut self, version: u64, triple: Spo); // lib.rs:186 + pub fn window_at(&self, ref_version: u64) -> impl Iterator + '_; // lib.rs:202, borrowing projection via TemporalPov::at + pub fn window_range(&self, range: VersionRange) -> impl Iterator + '_; // lib.rs:214 +} +``` +`window_at`/`window_range` are explicitly documented as **borrowing +projections, never a second store** (`lib.rs:194-201`) — consistent with the +`temporal.rs` sorted-stream doctrine referenced in the top-level `CLAUDE.md` +"2026-07-10 supersession" note. + +Re-exports at crate root (`lib.rs:54-77`): `FamilyTrie`; `basin_self_code, +heldout_bessel_gate, heldout_constant_n_gate, heldout_split_gate, BasinCode, +HeldOutGate`; `Belief, BeliefArena, CStmt, Copula, ReviseOutcome, Stamp`; +`load_cam96_codes, load_cam96_space, CodebookError`; `evidence_basin, +forward_gate, novelty_rate, open_question_yield, partial_spearman, +shuffle_beliefs_null, shuffle_rungs_null, EvidenceBasin, ForwardGateReport`; +`parse_to_spo, Pos, Tagged`; `confidence_delta_recount, +confidence_delta_self, most_frequent_belief, provenance_check, +ConfidenceAnswer, ProvenanceReport`; `detect, detect_all, +detect_all_measured, detect_measured, MeasuredShape, Representation, +ShapeClass, ShapeReport`; `AdcSpace, Cam96, Cam96Space, SemanticSpace`; +`Spo`; `PaletteVocab, WordId`; `WitnessStream`. + +--- + +## NOT VERIFIED + +- **`KanbanColumn::advance_on_gate`** — signature and full match arms not + read directly in this pass (not opened; behavior inferred only from + `cycle_driver.rs` test names/asserts at `cycle_driver.rs:1682-1726`). Lives + in `lance_graph_contract::kanban` per the `use` at `cycle_driver.rs:69`. +- **`KanbanColumn::can_transition_to`** — referenced by `try_advance_phase`'s + default body (`soa_view.rs:316`) and by a `style_strategy.rs` test + (`style_strategy.rs:865`), but its own definition/match arms were not + opened in this pass. +- **`MailboxId` underlying type** — treated as `u32` based on usage + (`u64::from` conversions in `cycle_driver.rs` tests, `& 0x3F` masking in + `soa_view.rs` `FakeSoa::w_slot`), but `pub type MailboxId = …` in + `lance_graph_contract::collapse_gate` was not opened directly. +- **`BatchWriter

`** (`cast`, `on_behalf_of`, `intent_moves`, + `drain_pending_payloads`) and **`emit_bootstrap_intent`** + (`lance_graph_planner::owner_adapter`) — signatures used pervasively in + both `cycle_driver.rs` and `blw_fusion.rs` but their own definitions were + not opened in this pass (out of the requested A–D scope). +- **`persist_cycle` / `recover_and_apply` / `RecoveredCycle` (or whatever the + return type of `recover_and_apply` is named)** in + `lance_graph_planner::persist_sink` — signatures not independently + confirmed; only their call sites in `cycle_driver.rs` tests and + `blw_fusion.rs:864,872` were read. `recovered.watermark` and + `recovered.applied` field names are taken on faith from + `blw_fusion.rs:873,875,879` and `cycle_driver.rs`'s own + `recover_fleet`/`recover_and_apply` usage (`cycle_driver.rs:725`), not from + the type's own declaration. +- **`examples/bible_wave.rs`** — not opened in this pass; its FSM → SPO → + `TemporalStream` pipeline is reported only via the `deepnsm-v2/src/lib.rs` + module-doc summary (`lib.rs:26-35`), not from the example's own source. +- **`crates/deepnsm-v2/src/vocab.rs` / `spo.rs`** (`PaletteVocab`, `WordId`, + `Spo` struct fields) — not opened; only inferred from `lib.rs` usage + (`Spo::new(subject, predicate, object)`-shaped calls in `lib.rs` tests) and + the `blw_fusion.rs`/`cycle_driver.rs` grep results. +- **`MailboxSoA` private field visibility** — `phase` is confirmed + `pub(crate)` (`mailbox_soa.rs:229`); other fields' exact visibility + (`energy`, `current_cycle`, etc.) were not individually confirmed as + `pub`/`pub(crate)`/private beyond what the accessor methods imply — only + the accessor methods themselves were verified as the intended read/write + surface. +- **The exact `use` line(s) importing `persist_cycle`/`recover_and_apply` + into `blw_fusion.rs`** — the import block was not fully re-read after the + initial partial grep (`blw_fusion.rs:101` covers `SweepSlot, WalSink, + WriteFailed` only); the two function imports are inferred from call-site + usage, not confirmed against an explicit `use` statement. +- **`rank_verdicts` / `contains_all` / `encode_plane` / `bloom_of_terms`** + (blw_fusion.rs helpers) — call sites read, full bodies not inventoried + (out of scope per the brief, which asked only for the seed/seal call + sequence). diff --git a/.claude/board/exec-runs/probe-ignition-build.md b/.claude/board/exec-runs/probe-ignition-build.md new file mode 100644 index 000000000..e49de82eb --- /dev/null +++ b/.claude/board/exec-runs/probe-ignition-build.md @@ -0,0 +1,159 @@ +# PROBE-IGNITION — build record (Sonnet build lane) + +**Deliverable:** `crates/lance-graph-supervisor/tests/probe_ignition.rs` — written, +not compiled, not run (edit-only per the guardrails; no `cargo` of any kind was +executed by this lane). Two tests: `probe_ignition_scan_and_cast_no_messaging` +(the main 6-cycle run, G1-G8, G10, G11, self-scans, not-claimed block) and +`probe_ignition_g9_drained_writer_retry_footgun` (the G9 side fixture, its own +`MemWal`-derived `FlakyWal`). + +## What was built + +- Whole file wrapped in `#[cfg(feature = "cycle-driver")] mod probe_ignition { ... }` + (the w2b pattern). **No Cargo.toml change** — verified both manifests before + writing: `lance-graph-supervisor`'s dev-dep on `cognitive-shader-driver` and + its `cycle-driver` feature (`dep:lance-graph-planner`, `dep:tokio`) already + existed exactly as the brief assumed. +- The pinned run shape verbatim: `FLEET_OWNERS=64`, `ROWS_PER_OWNER=64`, + `POPULATED_ROWS=48`, `CORPUS_VERSES=3072`, `SCOPE=0..32`, `CYCLES=6`, + `WAKE_CYCLE=4`, the 7 cohorts at their exact id ranges (IGNITE_A 0..6, + IGNITE_C 6..12, REST 12..20, CONTRA 20..24, UNARMED 24..31, ORPHAN 31, + OUTSIDE 32..64). +- All 11 gates (G1-G11, with G2/G3 carrying their a/b/c sub-parts), each with + both can-fire and can-stay-silent halves, printed as + `eprintln!("probe.ignition.G ...")` plus asserts. +- The G2a/G3b/G11 compile-time self-scans via `include_str!("probe_ignition.rs")`. + Needles are built by string CONCATENATION (never written as one contiguous + literal anywhere in the file, including inside the scan code itself) so the + scan cannot self-match — documented in a comment at the scan site. +- Qualia: `flow_qualia()`/`block_qualia()` re-derived locally with a doc + comment citing `cycle_driver.rs:1669`/`:1675` as provenance (those fixtures + are `#[cfg(test)]`-private, not importable). +- Corpus: `BLW_KJV_TSV` env (default `/tmp/kjv_verses.tsv`), deterministic + synthetic fallback with printed provenance, pairwise-distinctness guard on a + 5-owner content-plane sample. +- Write-back pass strictly AFTER apply (never during compute); the REST + branch records a rest and skips `run_cycle` entirely on zero staged casts. +- G9 side fixture: 2 owners, own `MemWal`-wrapping `FlakyWal` with a + `fail_next` `AtomicBool`, injected WAL failure, retry via `seal_cycle`, then + the drained-writer zero-slot observation via a fresh `collect_casts` call. +- G10: probe-local `column_pass` (counts `missing`) vs the shipped + `run_cognitive_work_gated_over` (silently drops the same id) on a + single-element `[ORPHAN_ID]` list; asserts the two totals differ by exactly 1. +- §5 Not-claimed block, all 12 items, printed at the end of the main test. + +## Deviations from the design note (both stated in-file, with reasons) + +1. **Energize uses a direct `owner.energy[row]` write, not `apply_edges(&[(row, + CausalEdge64)])`.** `causal_edge::CausalEdge64` is unreachable from this + crate: `cognitive-shader-driver` depends on `causal-edge` privately (no + `pub use` anywhere in its `lib.rs` or `mailbox_soa.rs` — grepped), and + `lance-graph-supervisor`'s own `Cargo.toml` has no dependency edge to + `causal-edge` at all. Adding one is a Cargo.toml change, which the brief + forbids outright. `owner.energy` is the exact public field `apply_edges` + itself mutates (`mailbox_soa.rs:66,362`), and is the field + `examples/blw_fusion.rs:515` already writes directly for its own + energizing — precedent in the same crate family, not an invented mechanism. +2. **The scheduled wake runs at the TOP of cycle 4's loop body (before + `scan_board`), not as step 14 after the write-back as the design's own + numbering literally lists it.** The design's cohort table says "wake at + c4 → Evaluation" — cycle 4's own gate read must already see the post-wake + mantissa. Read literally (wake after write-back), the effect would only be + visible from cycle 5 onward, contradicting the table. Documented at the top + of the file as a placement correction, not a behavioural addition (still + exactly one write, still gated on `c == WAKE_CYCLE`, still REST-only). + +No other deviations. The mid-flight G2b correction (Planning→Prune casts are +`Native`/the gate's mint, not `Elixir`/the style's, when the gate says Prune) +was folded in as instructed, with a one-line comment citing the design note's +§2 step 8 as the authority, at both the G1 c1-decomposition site (20 Flow + 4 +Block, not 24 uniform) and the G2b per-cycle exec-conditioned-on-`(from,to)` +check. + +## Bugs I found and fixed during my own re-read (before declaring done) + +Per the guardrails' "read the exact signature of everything you call" rule, I +re-read the whole file after the first draft and caught three real defects +that would either fail to compile or silently prove nothing: + +1. **`run_cycle`'s `CycleFrame` base version was hardcoded wrong.** First + draft used a stub `DatasetVersion(0)` for every cycle; `MemWal::commit_cycle` + rejects any `base != head`, so cycle 2 onward would have failed with + `WriteFailed("stale base...")`. Fixed to `sink.head()`, captured fresh each + cycle. +2. **A tautological assertion** (`assert_eq!(sink.wal_writes(), sink.wal_writes(), + ...)`) in the REST branch — compared a value to itself, which is exactly + the vacuous-assertion pattern this workspace's own falsifiability rule + forbids. Fixed by capturing `wal_writes_at_top_of_cycle` before the + cast-staging passes and comparing against that. +3. **`end_of_c5` was never actually captured during cycle 5** — the first + draft set it in a fallback block AFTER the whole loop ended, by which point + the fleet was already in its end-of-c6 state, making the G6 "byte-identical + across a rest cycle" comparison compare c6 to itself. Traced through the + cohort arcs by hand and confirmed **both c5 and c6 are zero-cast rest + cycles** (REST reaches Evaluation with mantissa 0 at c4's write-back and + never re-fires); fixed by capturing the fingerprint inside the loop's own + `c == 5` rest branch, and changed the c6 comparison to `panic!` (rather + than silently skip) if `end_of_c5` is somehow still `None` — an honest + failure instead of a mask. +4. **A `Option<&T>` vs `Option` type mismatch** in the G3b "changed set" + computation, caused by relying on default-binding-mode ergonomics through + `.filter(|(id, p)| ...)` over a `HashMap::iter()` (whose `Item` is `(&K,&V)` + and whose `.filter()` predicate receives `&Item`, i.e. one more reference + layer than the naive reading suggests). Rewrote as an explicit + `for (&id, &prev) in &snap_after_cast { if snap_post_apply.get(&id) != + Some(&prev) { ... } }` loop, which is unambiguous. + +I did not have a compiler to confirm these were the ONLY defects; see below. + +## Signature mismatches found between the design note and source + +- **§2 step 8's literal G2b claim was already flagged and corrected by the + orchestrator's mid-flight message** (see above) before I built the G2b + assertion at all — I did not need to independently re-discover this. +- Everything else in the design note's API references matched the Sonnet + inventory and my own source reads exactly: `run_cycle`/`collect_casts`/ + `seal_cycle`/`apply_sealed_transitions`/`shade_owner`/ + `run_cognitive_work_gated_over` signatures, `StyleStrategy::plan`/ + `::reliability_for`/`::intended_move` (private, reached only via `.plan()`), + `MailboxSoA` constructor/`write_row`/`apply_edges`/`consume_firing`/ + `pending_count`/`qualia_at`/`meta_at`/`energy` field, `KanbanColumn:: + advance_on_gate`/`next_phases`/`can_transition_to`, `gate_decision_i4`/ + `trust_texture_i4`/`flow_state_i4` (the last not directly called, only its + logic re-derived for the G4 anti-rig comment), `emit_bootstrap_intent`/ + `rebind_bootstrap`, `BatchWriter::{cast,on_behalf_of,intent_moves, + drain_pending_payloads}`. +- One inventory gap I closed myself: the API inventory listed + `TrustTexture` only as a return type of `trust_texture_i4`, not its own + definition. I independently grepped and confirmed + `lance_graph_contract::mul::TrustTexture` (`pub enum`, `#[derive(Debug, + Clone, Copy, PartialEq, Eq)]`, `Calibrated` variant) before using it in the + G4 assert. + +## What I could NOT verify (no compiler; report honestly) + +- **Not compiled, not run.** Every signature was read from source in this + same pass; the file's overall correctness beyond that manual trace is + unverified. +- I did not independently re-derive `flow_state_i4`'s exact match arms from + source in this pass (I read them earlier in the session and used the logic + in prose/comments only — I never call `flow_state_i4` directly, only + `gate_decision_i4` which composes it internally). +- I did not check whether `clippy -D warnings` would flag anything beyond the + casts I pre-emptively allowed (`cast_possible_truncation`, + `cast_possible_wrap`, `cast_sign_loss`) and the pairwise-distinctness loop I + rewrote to avoid `needless_range_loop`. There may be other nits (e.g. + `too_many_lines` on the main test function, which is long by construction + given the 6-cycle loop plus 5 epilogue blocks). +- I did not verify `MailboxId`'s underlying type by opening + `collapse_gate.rs` in a prior turn of THIS session — I did open it in this + build pass specifically and confirmed `pub type MailboxId = u32;`. +- The exact wire-format assumption that `[900, 901]` and other bare integer + literals infer as `MailboxId` (`u32`) from context is standard Rust type + inference and should hold, but is unverified by a compiler. + +## Board-adjacent files touched + +Only this tag-file (`.claude/board/exec-runs/probe-ignition-build.md`) and the +deliverable itself. `AGENT_LOG.md` was read, not written (one-writer rule). +No `cargo`, no `git commit`, no branch change. diff --git a/.claude/board/exec-runs/probe-ignition-design-opus.md b/.claude/board/exec-runs/probe-ignition-design-opus.md new file mode 100644 index 000000000..6256bbd2b --- /dev/null +++ b/.claude/board/exec-runs/probe-ignition-design-opus.md @@ -0,0 +1,558 @@ +# PROBE-IGNITION — design note (Opus filigree lane, design-only, no code) + +> **Scope:** the falsifier for *"inject corpus X into thinking style Z"* — the +> first DRIVEN traversal of the built-but-undriven write path +> (`batchwriter-kanbanstep-wiring.md` §0: *"the machinery exists and is +> undriven"*). This note decides placement, the ignition grammar's lowering, +> the pre-registered assertion table, and the silence-honesty design. It writes +> no code and proposes no change to `crates/jc`, `temporal.rs`, or +> `persist_sink.rs`. +> +> **Operator ruling folded in (mid-flight, 2026-08-04):** *"I don't want any +> messaging in the common sense, only casting and eventually 'looking into the +> kanban'. In theory it could be as simple as setting a start bit in a kanban +> tenant."* Every message-shaped element of the earlier draft was **rewritten, +> not annotated** — §1b carries the (a)/(b) decision the ruling demands, §2 is +> a scan-and-cast loop with no queue, and §3's G-table gained the +> discovered-by-reading twin. + +--- + +## §0 — Headline: what this probe proves, and what it deliberately cannot + +**Proves (can-fire).** A fleet of real `MailboxSoA` tenants, seeded from a real +corpus and armed with a thinking style by a **write**, is discovered by a +**scan of the kanban board alone**; the armed style's own `StrategyOutcome` +(minted by `StyleStrategy::plan`, `style_strategy.rs:270-289`) is **cast** +write-on-behalf through `emit_bootstrap_intent` (`owner_adapter.rs:92`); the +cast is drained, sealed into ONE WAL write / ONE version, and applied +(`run_cycle`, `cycle_driver.rs:446`) — and **only then** does any phase change. +The MUL gate (`shade_owner` → `gate_decision_i4`, `cycle_driver.rs:615` / +`mul.rs:575`) sustains the arc from `CognitiveWork` onward. Ignition is the +first `run_cycle` in the tree with a real owner, a real corpus, and a real +style behind it. + +**Proves (can-stay-silent).** The same loop, on the same fleet, **rests** — +and rests for five structurally distinct reasons, each with a named cause: +work exhausted (Hold on a **would-be-Flow qualia**), lifecycle absorbed +(Commit), pruned (Block), unarmed (no style bits), and out of scope (address). +On the final cycle the scan yields zero casts and **no version is sealed at +all** — `wal_writes` does not move. A brain that cannot rest is the 150/150 +defect wearing a crown; this probe's rest is *earned by the loop*, not +constructed by the fixture. + +**Deliberately cannot (see §5 for the full list).** No durability, no +parallelism, no scale, no timing, no multi-writer, no validity, no +GUID-prefix routing, no 36-style claim, no `deinterlace` claim. It is +**ignition mechanics only**. + +--- + +## §1 — Placement + +**File:** `crates/lance-graph-supervisor/tests/probe_ignition.rs` + +**Run:** `cargo test -p lance-graph-supervisor --features cycle-driver -- --nocapture` + +Reasoning, in the order the constraints bind: + +1. `run_cycle` / `collect_casts` / `shade_owner` live in **supervisor** + (`cycle_driver.rs`, behind `#[cfg(feature = "cycle-driver")]`). + `StyleStrategy` lives in **planner**. The dep edge is one-way + (`supervisor → planner`, supervisor `Cargo.toml`: *"Planner does NOT dep + supervisor — no cycle"*), so **only the supervisor side can see both**. The + probe cannot live in the planner. +2. The real owner is `cognitive_shader_driver::mailbox_soa::MailboxSoA`, which + supervisor already carries as a **dev-dependency** (`Cargo.toml`, added for + `tests/w2b_real_owner_probe.rs`) — available to `tests/` and `examples/`, + never in the runtime dep graph. No manifest change is needed. +3. `tests/` over `examples/`: an example's `main()` is **never executed by + `cargo test`** (the codex P2 that forced the Babel probes into the workflow + by hand). A `#[tokio::test]` runs under the ordinary crate test command. +4. Feature gating follows the established in-repo pattern — + `w2b_real_owner_probe.rs:19` wraps its whole body in + `#[cfg(feature = "supervisor")] mod w2b_real_owner_probe { … }`. This probe + uses `#[cfg(feature = "cycle-driver")]` the same way, so the file compiles + to nothing without the feature and needs no `required-features` entry. +5. **CI is a gate requirement, not a nicety.** The supervisor CI step passes + `--features supervisor` only; the entire P4 falsifier suite had therefore + never run in CI (`E-A-PER-FEATURE-CI-STEP-NAMED-LIKE-PER-CRATE-COVERAGE-1`, + found 2026-08-04). **This probe MUST land with the `--features + cycle-driver` invocation added to the workflow in the same PR**, or it + inherits that blind gate on arrival. + +Gate lines print as `probe.ignition.G …` via `eprintln!` — the convention +`cycle_driver.rs:1657`'s `perf.p4f` line already uses. The probe **reports its +counts**; an `expect()` would say only "not found". + +--- + +## §1b — The ignition ruling: two verbs, and which realization + +Two verbs exist in this design and no others: **CAST** (a write through +`BatchWriter`, write-on-behalf, `batch_writer.rs:132`) and **LOOK INTO THE +KANBAN** (a read of `MailboxSoaView::phase()` over the scoped owner set). There +is no endpoint, no actor, no RPC, no queue, and no command-shaped method. + +### The decision: **(a) — the start bit IS the cast. No new bit is minted.** + +`start()::where()` lowers to exactly this, all of it shipped: + +| grammar axis | lowers to | shipped surface | +|---|---|---| +| `table($x)` | seed rows: content plane + energy | `MailboxSoA::write_row` (`mailbox_soa.rs:417`), `apply_edges` (`:348`) | +| `ThinkingStyle($z)` | **a write of `MetaWord` bits** into the owner's MetaColumn | `MailboxSoA::set_meta` (`:618`) / `write_row`'s `cell.meta` | +| `where(prefix)` | the **scan's iteration scope** — a `MailboxId` range | read-only over `MailboxFleet::owner` (`cycle_driver.rs:183`) | +| `start()` | **nothing extra.** An armed owner in a non-absorbing column IS started; the next scan finds it | `MailboxSoaView::phase()` | +| `MUL(true)` | `gate_decision_i4` decides every subsequent cycle | `mul.rs:575` → `KanbanColumn::advance_on_gate` (`kanban.rs:146`) | + +**Why (a) suffices, including for "armed but not yet cycled".** That state is +expressible without a bit: `where()` scopes the **scan**, not the arming. An +owner can be armed (MetaWord bits written) and sit outside the scanned range +forever — armed, never started. The probe asserts exactly this with the +OUTSIDE cohort (§3 G7): 32 owners armed and firing, byte-identical to the +firing cohort, that never cast because **the address is the only difference**. + +**What (a) genuinely cannot do — stated so (b) is not needed on a guess.** +Reading the board alone, (a) cannot distinguish *"ignited, evaluated, and +Holding"* from *"never scanned"*: both show a non-absorbing phase and no cast. +`current_cycle` separates them only once an advance has landed, so an owner +that Held on its very first evaluation is indistinguishable from an owner +nothing ever looked at. **Nothing in PROBE-IGNITION needs that distinction** +(the probe knows its own scope and asserts the scan set directly), so (b) is +**not proposed**. + +**If (b) is ever wanted, here is its home and its trap.** The kanban×Rubicon +value tenant (`ValueTenant::Kanban`, `canonical_node.rs`, 8 bytes at value-slab +`[112,120)`, LE `phase(u8) | exec(u8) | reserved(u16) | cycle(u32)`) has **two +reserved bytes at `[2..4]`** — a documented bit there needs **no +`ENVELOPE_LAYOUT_VERSION` bump**, satisfying the envelope-auditor shape. **But +that tenant is PER-NODE**, read via `NodeRow::kanban()`, while the board this +loop drives is **per-mailbox** (`MailboxSoA::phase`, one field, no per-row +kanban column exists on the SoA at all). Setting a start bit "at the addressed +rows" would therefore create N per-row boards inside one tenant — the same +category error as the deleted tiling harness +(`E-AN-OWNER-IS-A-TENANT-NOT-A-SHARD-1`: an owner is a tenant, not a shard). +**If (b) is ever taken, it must be a per-MAILBOX start state, not a per-row +one**, and the two carriers must be reconciled first. Recorded here as an open +question (§6 Q3), not as a proposal. + +### The driver receives nothing (operator point 3, made structural) + +The probe's per-cycle input is `scan_board(&fleet, SCOPE_IDS)` where +`SCOPE_IDS` is a **compile-time constant range**, recomputed from nothing and +derived from no previous cycle's output. Three structural consequences the +probe asserts: + +- **No carry-over list.** `CognitiveWorkOutcome::held_owners` + (`cycle_driver.rs:483`) is **discarded every cycle**. A Held owner is + re-found by the next scan because its phase still shows it in a + non-absorbing column. This closes the #879 open item *"held_owners + accumulation becomes the driver's job with a strand falsifier"* in the + ruling-compliant direction: **there is no accumulation because there is no + list.** Falsifier: the REST cohort wakes at cycle 4 having been re-found by + scan alone (G5). +- **The scope slice is byte-identical every cycle** — asserted, so no + cycle-to-cycle channel can hide in it. +- **The only path from the harness to the loop is owner state**: seeding, + arming, and the wake are all writes into `MailboxSoA` columns; nothing else + is passed. + +--- + +## §2 — The ignition sequence, step by step + +Every function named is shipped. The probe composes; it mints no type. + +### Pre-loop (fixture construction — writes only) + +1. **Corpus.** `load_verses(path, limit)` shape from `blw_fusion.rs:478` + (`index\ttext` TSV, `BLW_KJV_TSV`, default `/tmp/kjv_verses.tsv`). If the + file is absent, a deterministic synthetic corpus is generated and the + fixture provenance is **printed**. The corpus is a **non-degeneracy + fixture, not a semantic instrument** — no assertion in this probe depends on + the text being scripture. A guard asserts the seeded content planes are + non-zero and pairwise distinct across a sample, so a degenerate corpus + cannot make the byte-identity claims trivially true. +2. **Fleet.** `HashMap>` — the blanket `MailboxFleet` + impl (`cycle_driver.rs:190`). Construction `MailboxSoA::new(id, w_slot, + threshold)` (`mailbox_soa.rs:292`), `set_populated(48)` (`:495`) per the W1c + discipline, `tick()` (`:399`) to cycle 1 exactly as `blw_fusion.rs:729`. +3. **Seed (`table($x)`).** Per row: `write_row(row, cycle, &WriteCell{ content, + entity_type, temporal, meta, qualia, .. })` (`:417`). `content` via the + `encode_plane` bloom shape (`blw_tenant.rs:248-294` provenance). +4. **Arm (`ThinkingStyle($z)`) — a write.** `cell.meta = Some(MetaWord::new(z, + …))` where `z ∈ {0 unarmed, 1 Analytical, 2 Creative, 3 Reflective}` + (`cognitive_shader.rs`, `MetaWord::new` / `::thinking`). See §6 Q1 for why + the vocabulary is three values and not thirty-six. +5. **Qualia — DECLARED, copied from the shipped test fixtures.** + `flow_qualia()` (`cycle_driver.rs:1669`: warmth=4, groundedness=3, + coherence=4, valence=2) for every cohort except CONTRA, which gets + `block_qualia()` (`:1675`: coherence=−3, tension=3). Written via + `cell.qualia`. **This is not an invented corpus→qualia encoder** — it is the + same construction `cycle_driver`'s own gate falsifiers use, reused verbatim. +6. **Energize.** `apply_edges(&[(row, CausalEdge64)])` (`:348`) — the shipped + energy-delivery path (`energy[row] += mantissa/8 * confidence`), which also + enforces `edge.w_slot() == owner.w_slot`. Energy above `threshold` is what + makes a row *firing*. + +### Per cycle `c = 1..=6` + +7. **LOOK INTO THE KANBAN.** `scan_board(&fleet, SCOPE_IDS)` reads + `owner.phase()` for each id in the constant scope and partitions into + `planning / cognitive / evaluation / absorbed`, plus `missing` for a scoped + id with no registered owner. Pure reads through `MailboxFleet::owner` + (`cycle_driver.rs:183`). +8. **Planning column → the STYLE mints.** Probe-local `column_pass` (see §6 Q2 + — the shipped `cognitive_pass` cannot reach this column): + - `armed = MetaWord::thinking(owner.meta_at(0))`; `0` ⇒ skip (unarmed). + - `gate = gate_decision_i4(&owner.qualia_at(0), mantissa_of(owner))` + (`mul.rs:575`). + - `target = owner.phase().advance_on_gate(&gate)` (`kanban.rs:146`); + `None` (Hold) ⇒ **rest, cast nothing**. + - `out = StyleStrategy.plan(PlanInput{ context: ctx_of(owner, armed), plan: + None, outcome: None }, &mut Arena::new())` (`style_strategy.rs:270`) → + `StrategyOutcome{ reliability, intended_move: Planning→CognitiveWork, + exec: Elixir }`. + - If `out.intended_move.to != target` (i.e. the gate said Prune), replace + with `shade_owner(owner, &qualia, mantissa, out.reliability)` + (`cycle_driver.rs:615`) — the gate's move, still carrying the **style's** + reliability. + - `emit_bootstrap_intent(&out, owner.mailbox_id(), owner.current_cycle(), + &mut writer, payload)` (`owner_adapter.rs:92`) → `rebind_bootstrap` + (`:68`) binds mailbox 0 → live owner, no-theft guarded → `BatchWriter::cast` + (`batch_writer.rs:132`). +9. **CognitiveWork column → the SHIPPED seam, the GATE mints.** + `run_cognitive_work_gated_over(&fleet, &scan.cognitive, &mut writer, + read_gate)` (`cycle_driver.rs:662`), `read_gate` returning + `(qualia_at(0), mantissa_of(owner), StyleStrategy::reliability_for(style, + &ctx), payload)`. `shade_owner` mints (`exec: Native`). + **`held_owners` is discarded.** +10. **Evaluation column → probe-local `column_pass`** (same shape as step 8; + the style's `intended_move` cannot express this edge, so the gate mints). +11. **REST BRANCH.** If `writer` staged **zero** casts this cycle: record the + cycle as a rest, **do not seal**, do not call `run_cycle`. No version, no + WAL write. (`persist_cycle` has no empty-batch guard — an empty cycle would + still commit and burn a version, i.e. a heartbeat. Not resting is a write; + resting must be no write.) +12. **Otherwise `run_cycle(&sink, &mut fleet, &mut writer, CycleFrame::new( + CycleId(c), base), position_base, &mut watermarks, |_| 0u64)`** + (`cycle_driver.rs:446`) — which is `collect_casts` (`:220`) → + `seal_cycle` (`:280`) → `persist_cycle` → `apply_sealed_transitions` + (`:338`) → `MailboxSoaOwner::try_advance_phase` (`soa_view.rs:295-322`). + `position_base = max(prev_base, sealed.next_position_base)` — the + restart-stable contract (`collect_casts` doc, `:203-210`). +13. **Write-back pass (`&mut`, after apply — never during compute).** For each + owner in `applied.applied`: `consume_firing(row)` on ONE firing row + (`mailbox_soa.rs:380` — stamps `last_active_cycle`, resets `energy[row]`, + same-cycle idempotency guarded). This is what makes `mantissa` fall. +14. **Scheduled wake (cycle 4 only).** `apply_edges` re-energizes one row of + each REST-cohort owner. **A write, not a message** — and the same verb the + initial energizing used. + +**Sink.** `MemWal` copied with provenance from `blw_fusion.rs:396-475` / +`blw_tenant.rs:405-501` (`Mutex>`, `AtomicU64` version, base +fence, `wal_writes` counter). Contract only; **not durability**. +**Payload `P = Vec`** (forced: `run_cycle` takes `BatchWriter>` and +`SweepSlot::payload` is `Vec`) carrying `RowSpanDescriptor{row_lo, row_hi, +cycle}.to_le_bytes()` (`blw_fusion.rs:365-381`) — a **descriptor**, never owned +delta bytes. + +### The pinned run shape (PRE-REGISTERED — fixed before any number exists, NOT adjustable after a run) + +| constant | value | reason it is this number | +|---|---|---| +| `FLEET_OWNERS` | **64** | Smallest power of two that leaves the where()-excluded set (32) a **majority** of the fleet while keeping 64 real `MailboxSoA<64>` allocations bounded: 3 identity planes × 64 rows × 256 words × 8 B = **384 KB/owner ≈ 24 MB**, lazily mapped. NOT 64k: the sparse-vs-fleet property is already proven at 64k over `FakeOwner` (`cycle_driver.rs:1098`); this probe re-anchors the **driven** loop over the real owner. | +| `ROWS_PER_OWNER` (`N`) | **64** | Capacity. | +| `POPULATED_ROWS` | **48** | `< N`, so the `n_rows()`-vs-capacity distinction is live and zero-padding rows are never read (the W1c phantom-row discipline, `mailbox_soa.rs:852-864`). | +| `CORPUS_VERSES` | **3072** | `64 × 48` exactly — every owner gets a full slice; no owner is a short tail. | +| `SCOPE` | **`0..32`** | Half the fleet. The complement is armed, firing, and identical — so the where() axis has a non-trivial exclusion set (32), not a token one. | +| `CYCLES` | **6** | The minimum exhibiting all five cohort behaviours: 3 cycles complete the Flow arc `Planning→CognitiveWork→Evaluation→Commit` (exactly 3 DAG edges, `kanban.rs:101-107`), +1 to observe an **earned** Hold at a non-absorbing column, +1 for the wake write to take effect, +1 for the woken owner to advance again. | +| `FIRING_ROWS` | **3** (IGNITE/CONTRA/UNARMED/OUTSIDE), **1** (REST) | 3 = one per advance across the 3-edge arc; 1 = exhausts after the first advance, which is what produces the Hold that is not death. | +| `CONSUME_PER_ADVANCE` | **1** | Ties work consumption to lifecycle steps 1:1 so the mantissa's decay is legible. | +| `WAKE_CYCLE` | **4** | The first cycle after two full resting cycles (2 and 3) — two, so "rests" is not a single-sample claim. | + +### Cohorts (all inside `FLEET_OWNERS = 64`; in-scope ids sum to exactly 32) + +| cohort | ids | armed | firing | expected arc | +|---|---|---|---|---| +| `IGNITE_A` | 0..6 | Analytical | 3 | c1 →CognitiveWork, c2 →Evaluation, c3 →**Commit**; c4-6 absorbed, silent | +| `IGNITE_C` | 6..12 | Creative | 3 | identical arc, **different reliability** (G2iii) | +| `REST` | 12..20 | Analytical | 1 | c1 →CognitiveWork, exhausts; **c2,c3 Hold**; wake at c4 →Evaluation; c5,c6 Hold | +| `CONTRA` | 20..24 | Analytical | 3 | c1 Block → **Prune** (absorbing); c2-6 silent | +| `UNARMED` | 24..31 | **none (bits 0)** | 3 | never planned; **zero casts, byte-identical, 6 cycles** | +| `ORPHAN` | 31 | — | — | in scope, **no owner registered** (the #879 missing-owner caveat, G10) | +| `OUTSIDE` | 32..64 | Analytical | 3 | constructed byte-for-byte like `IGNITE_A`; **zero casts** — only the address differs | + +Cycle-1 sparse set = `IGNITE(12) + REST(8) + CONTRA(4) = 24` advanced, +**40 untouched**, and every untouched owner has a **named** cause: 32 +out-of-scope, 7 unarmed, 1 orphan. (The `kept*3 < total` filter form is **not** +claimed here — 24×3 > 64. The honest anti-vacuity is the exact decomposition: +`untouched == 40`, `untouched > advanced`, and each subset accounted for.) + +--- + +## §3 — The pre-registered assertion table + +Every row has both halves. "Can-fire" and "can-stay-silent" inputs are +**non-trivial on both sides** — no empty-input silences. + +| id | assertion | can-fire input | can-stay-silent input | +|---|---|---|---| +| **G1** | Ignition advances exactly the scanned-armed-and-Flowing set | c1: 24 owners advance; `wal_writes == 1`; `sealed.transitions.len() == 24` | c1: 40 owners byte-identical (phase + `current_cycle` + `energy` + `meta` + `qualia` + `content_row`), decomposed 32/7/1 | +| **G2a** | The cast is a shipped minter's, never the harness's | compile-time self-scan (`include_str!("probe_ignition.rs")`) asserts the source contains **no `KanbanMove {` struct literal** | the same scan asserts the source **does** contain `emit_bootstrap_intent` — a scan that finds nothing is not evidence | +| **G2b** | `Planning` casts are the STYLE's, later casts are the GATE's | every sealed move with `from == Planning` has `exec == Elixir` (`StyleStrategy::intended_move`'s signature, `style_strategy.rs:391-399`) and `to == CognitiveWork` | every sealed move with `from ∈ {CognitiveWork, Evaluation}` has `exec == Native` (`shade_owner`'s, `cycle_driver.rs:632`) — the discriminator is not constant | +| **G2c** | The armed bits reached the plan and changed something | `reliability(IGNITE_A) != reliability(IGNITE_C)` bit-for-bit (the R-GATE property, `style_strategy.rs:486-508`) | two owners armed with the SAME style produce **bit-identical** reliability — a random or style-blind reliability fails one half or the other | +| **G3a** | Casting mutates nothing | snapshot `(phase, current_cycle)` fleet-wide before the passes; assert unchanged **after** all casts are staged and **before** the seal | on a rest cycle the same snapshot is unchanged across the whole cycle | +| **G3b** | Phases advance ONLY via seal→apply | self-scan asserts the source contains no `.advance_phase(` and no `.try_advance_phase(` — the probe has no path to mutate a phase | after apply, the changed set is **exactly** `sealed.transitions`' owners; `sink.reads() == 0` during apply (P4b reads no dataset) | +| **G4** | **The gate discriminates on ONE axis over identical, non-trivial qualia** | REST owner at c1: `qualia == flow_qualia()`, `mantissa == 1` ⇒ `Flow` ⇒ casts | REST owner at c2: `qualia` **byte-identical to c1**, `mantissa == 0` ⇒ `Hold` ⇒ casts nothing. Anti-rig asserts: `qualia != QualiaI4_16D::ZERO`, `trust_texture_i4(qualia) == Calibrated`, and `warmth + groundedness − tension == 7 ≥ 4` — i.e. **a would-be-Flow qualia that nonetheless rests** | +| **G5** | Rest is a reschedule; Prune is not | REST is re-found by the scan at c2 and c3 (`scan.cognitive` contains all 8) and **advances at c4** after the wake write | CONTRA appears in **no** scan set after c1 (absorbing) and never casts again. `rediscovered(REST) == 8`, `rediscovered(CONTRA) == 0` — a scan returning everything or nothing fails | +| **G6** | The fleet can rest completely | c1 seals: 24 casts, `wal_writes` 0→1 | c6: scan yields **zero** casts, **no seal happens**, `wal_writes` unchanged from c5, and every owner is byte-identical to its post-c5 state. Same code path, different board state | +| **G7** | The `where()` axis is load-bearing | epilogue (after all other assertions): widen the scope by one OUTSIDE id, run the Planning pass onto a **throwaway** writer, assert it stages a cast | main run: OUTSIDE (32) is byte-for-byte equal to `IGNITE_A`'s construction (`energy`, `content_row`, `meta_at`, `qualia_at`) and casts **zero** times across 6 cycles | +| **G8** | The style-arming axis is load-bearing | epilogue: write non-zero thinking bits into one UNARMED owner, run the Planning pass onto a throwaway writer, assert it stages a cast | main run: UNARMED (7) casts zero times and stays byte-identical, with a corpus identical to IGNITE's | +| **G9** | #879 OPEN — the drained-writer retry footgun is **observable** | side fixture (2 owners, own `MemWal`): inject one WAL failure ⇒ `CycleError::Seal`, `failure.casts` is the byte-identical frozen set, no owner mutated; retry via `seal_cycle(sink, failure.frame, failure.casts)` lands it | on the SAME writer, a fresh `collect_casts` yields **zero** slots — the footgun made visible: a naive `run_cycle` retry would seal an empty cycle and silently "succeed". Comment pins this as the falsifier a future guard must flip | +| **G10** | #879 OPEN — the silently-skipped missing owner | the probe-local `column_pass` **counts** ORPHAN (`missing == 1`) | the shipped `run_cognitive_work_gated_over` handed the same scope list reports **neither** a cast nor a held owner for it, and `CognitiveWorkOutcome` has no field that could. Assert the two passes differ by **exactly** 1. Comment: when the upstream counter lands, this becomes `missing == 1` on both | +| **G11** | Ordering is not a write-side concern | `sealed.transitions` is sorted by `stream_position` and `position_base` is monotone across cycles incl. the skipped rest cycles | no confirmation ledger exists: self-scan asserts the source contains no `ack`/`confirm` identifier (`E-ACK-ELIMINATED-1`) | + +**Anti-vacuity note on G2a/G3b/G11 (the self-scans).** A compile-time +`include_str!` scan is only as wide as the file. It proves the *probe* did not +fabricate a move or touch a phase; it does not prove that of a helper in +another module. The probe therefore imports only `lance_graph_planner::*`, +`lance_graph_supervisor::cycle_driver::*`, `lance_graph_contract::*`, and +`cognitive_shader_driver::mailbox_soa::*` — all shipped — and the import list +is itself part of the scanned text. + +--- + +## §4 — Silence-honesty: the qualia extractor question, answered + +`shade_owner` (`cycle_driver.rs:615`) takes `(qualia, mantissa, reliability)` +from a **caller-supplied extractor** — the honesty ledger's own named gap +(`cycle_driver.rs:57-61`: *"its qualia/mantissa inputs come from a +caller-supplied extractor, NOT from a live MailboxSoA / shader-driver +dispatch"*). That is exactly where a rigged silence would hide, so the design +is explicit about which input is declared and which is derived. + +**The split:** + +- **`qualia` is DECLARED, and copied from the shipped gate falsifiers' + own fixtures** — `flow_qualia()` (`cycle_driver.rs:1669`) and + `block_qualia()` (`:1675`), written into the real `MailboxSoA::qualia` + column at seed time and **never mutated by the loop**. It is therefore + provably **constant** for every owner across the whole run. +- **`mantissa` is DERIVED from live owner state**: `min(7, count of populated + rows with |energy| ≥ threshold) as i8`. Two shipped fields + (`MailboxSoA::energy`, `::threshold`), no encoder. It falls only because + `consume_firing` (`:380`) reset an energy cell — a real state change made by + the shipped consumption primitive. + +**Why this makes the silence honest rather than rigged.** Trace the gate +(`mul.rs`, read line by line): + +- `flow_state_i4`: `flow_proxy = warmth + groundedness − tension = 4 + 3 − 0 = + **7**`. With `mantissa > 0` ⇒ `flow_proxy ≥ 4` ⇒ **`Flow`**. With `mantissa == + 0` ⇒ not Flow (needs `> 0`), not Transition (same), not Anxiety + (`flow_proxy` is 7, not `≤ −2`, and `mantissa < 0` is false) ⇒ **`Boredom`**. +- `trust_texture_i4`: coherence 4, valence 2, tension 0 ⇒ **`Calibrated`** in + both cases. +- `gate_decision_i4`: `(Calibrated, Flow)` ⇒ **`Flow`**; `(Calibrated, + Boredom)` ⇒ the `_ =>` arm ⇒ **`Hold`**. + +So the resting owner's gate input is **the shipped test suite's own Flow +fixture, unchanged, at a flow_proxy of 7** — the maximum this fixture family +reaches. The probe asserts that. An all-zeros qualia would also produce Hold +(`flow_proxy 0`, mantissa 0), and that is precisely the trivial silence the +falsifiability rule forbids as the only case; this design's silence is the +opposite — **the gate is looking at a state that says "go" on four of five +channels and rests anyway, because there is no work left**. One axis varies; +it is derived from real state; and the constant axis is asserted non-trivial +rather than merely asserted equal (`E-THE-EQUALITY-PASSED-WHILE-AN-AXIS-WAS-CONSTANT-1`). + +**A third, differently-caused silence keeps the detector honest.** CONTRA's +`block_qualia()` drives `Uncertain ⇒ Block ⇒ Prune` — an *absorbing* silence, +not a resting one. G5 asserts the two are distinguishable by the scan +(`rediscovered(REST) == 8` vs `rediscovered(CONTRA) == 0`). A probe that could +not tell rest from death would be measuring nothing. + +**The `&mut` discipline.** All gate inputs are read through `&owner` +(`MailboxFleet::owner`); consumption happens in a **separate `&mut` pass after +apply** (step 13). No `&mut self` during computation +(`.claude/rules/borrow-strategy.md`); the mutation is a gated write-back, not a +side effect of the read. + +--- + +## §5 — Not claimed + +Printed as a block at the end of the run, in the `blw_fusion.rs` §6 style. + +1. **No durability.** `MemWal` is an in-process `Mutex`/`Vec`; its "versions" + are sequence numbers, **not Lance versions**. `LanceShardSink` does not + exist (`persistence-cycle-wal-bootstrap-v1.md`). +2. **No parallelism.** The loop is synchronous — #879's own honesty ledger. + Only D-KIA-A2's pre-registered protocol (median-of-5 after one discarded + warm-up, ≥2× at ≥4,096 owners with ≥100 µs thought bodies; stay-silent + within ±10 % on trivial bodies) can convert "parallel" from doctrine to + measurement. This probe makes no timing measurement of any kind. +3. **No scale claim.** 64 owners. The 64k sparse property is separately proven + over `FakeOwner` (`cycle_driver.rs:1098`) and is not re-asserted here. +4. **No multi-writer claim.** Single-writer `MemWal`; + `TD-RECOVERY-HASH-PARTITION-UNCERTIFIED` (`recover_fleet`'s hash partition + vs `temporal.rs::local_trajectories`) is untouched — the probe adds no + evidence for or against it, and its in-order log certifies nothing. +5. **No `deinterlace` / temporal claim.** The probe does not read through + `deinterlace` and implements no `DeinterlaceRow`. D-BLW-3 owns that seam. +6. **No validity claim.** `reliability` is settledness, not ground-truth + correspondence (`E-RELIABILITY-NOT-VALIDITY`). +7. **No GUID-prefix routing claim.** `where()` is a contiguous `MailboxId` + range — an honest **stand-in**. `MailboxId` is a bare `u32` with no + classid/HEEL/HIP/TWIG structure in this fleet. The claim is only *"an + address-shaped scope excludes owners that would otherwise fire."* +8. **No 36-style claim.** Three styles are reachable (§6 Q1). +9. **No semantic claim about the corpus.** The corpus makes the columns + non-degenerate; nothing asserted depends on its meaning. Qualia are declared + fixtures, not encoded from text. +10. **No zero-copy claim.** `SweepSlot::payload` is `Vec` by the shipped + signature; the descriptor discipline is honoured (a row span, not delta + bytes) but no zero-copy property is measured. +11. **No claim that the loop can re-enter `Planning`.** See §6 Q4 — under the + shipped gate it structurally cannot. +12. **No recovery claim.** `recover_fleet` (`cycle_driver.rs:700`) is not + exercised; G9's retry is the WAL-failure path only. + +--- + +## §6 — Open questions for the orchestrator + +**Q1 — The arming vocabulary is three values, not thirty-six, and nothing +bridges the two surfaces.** `MetaWord::thinking()` is 6 bits (a 36-style +space). `StyleStrategy::resolve_style` (`style_strategy.rs:231-251`) reads +`PlanContext.thinking_style: Option>` — a **23D f64 vector** — and by +dominant axis can return only **`Analytical` / `Creative` / `Reflective`**. +There is **no shipped function** mapping `MetaWord` → `ThinkingStyle` or → +the 23D vector. The probe therefore arms with `z ∈ {0,1,2,3}` and maps to the +three vectors `resolve_style` can actually decode, and says so. *Should a +`MetaWord → PlanContext` bridge be a deliverable, and is the 23D vector or the +6-bit field the canonical arming surface?* (This is persona-vs-rung-ladder +territory — `.claude/v3/knowledge/persona-vs-rung-ladder.md` is the mandatory +read before answering.) + +**Q2 — The shipped P4c seam can drive exactly ONE of the five kanban +columns.** `cognitive_pass` (`cycle_driver.rs:490`) hard-filters +`if owner.phase() != KanbanColumn::CognitiveWork { continue; }` (`:505`), and +both public entry points (`run_cognitive_work` `:555`, +`run_cognitive_work_over` `:577`) route through it. **Ignition (`Planning`) +and completion (`Evaluation`) have no shipped driver at all** — which is why +`BatchWriter::cast` has no production caller. The probe writes a local +`column_pass(fleet, ids, column, writer, think)`; the natural upstream fix is +the **one-line generalization** (parameterize the column; +`run_cognitive_work` becomes `column_pass(…, CognitiveWork, …)`). The probe +uses the shipped seam for the `CognitiveWork` column specifically and asserts +its local pass never handles a `CognitiveWork` owner, so there is no +divergence risk by construction. *Approve the generalization as a follow-up +deliverable?* + +**Q3 — `MailboxSoA::phase` (per-mailbox) vs `ValueTenant::Kanban` +(per-node) are two carriers of the same concept at two granularities**, and +only the first is driven. The operator's "(b) a start bit in the kanban +tenant" lands in the second. `KanbanTenant`'s `reserved: u16` at bytes `[2..4]` +is the layout-bump-free home if it is ever wanted, but a **per-row** start bit +would fabricate N boards inside one tenant +(`E-AN-OWNER-IS-A-TENANT-NOT-A-SHARD-1`). *Should the two carriers be +reconciled — e.g. `MailboxSoA::phase` documented as the authoritative board and +`NodeRow::kanban().phase` as its per-row projection?* Not proposed here. + +**Q4 — The shipped gate cannot express "loop back for another round."** +`Evaluation.next_phases() == [Commit, Plan, Prune]` (`kanban.rs:105`) and +`advance_on_gate(Flow)` takes the **first non-Prune** = **`Commit`** +(`kanban.rs:146-152`). `Commit` is absorbing. So a Flowing owner terminates in +three advances and the `Evaluation → Plan → Planning` re-entry edge — which +exists in the DAG and which `blw_fusion.rs` hand-drives by fabricating the +move — is **unreachable through the MUL gate**. Sustained multi-loop cognition +therefore needs either a policy above the gate (new machinery — rejected in a +probe) or a contract change. The probe's arc stops at `Commit` and says so. +*Is `Evaluation → Plan` meant to be gate-selectable, and if so on what +signal?* + +**Q5 — The sealed record carries no style identity.** `KanbanMove` has +`{mailbox, from, to, witness_chain_position, exec}`; the style-conditioned +`reliability` lives on `StrategyOutcome` and is **not** cast. A reader of the +sealed log can tell *which minter* produced a transition (`exec`: Elixir = +`StyleStrategy`, Native = `shade_owner`) but **not which thinking style**. +G2c therefore asserts style-provenance at the cast site, not from the log. +*Is that a gap worth closing? No new field is proposed here.* + +**Q6 — Corpus hard-requirement.** The probe prefers the `BLW_KJV_TSV` corpus +and falls back to a deterministic synthetic one so it can run in CI, with the +provenance printed and a distinctness guard. *Should the TSV be hard-required +instead (matching `blw_fusion.rs:709-717`'s pre-registration assert), with the +corpus fetched in CI?* + +**Q7 — CI gating.** This probe is inert unless the workflow runs +`cargo test -p lance-graph-supervisor --features cycle-driver`. That step does +not exist today. It must land in the same PR. + +--- + +## Read-provenance (what backs this note, and what does not) + +**Read in full:** `.claude/knowledge/batchwriter-kanbanstep-wiring.md`; +`crates/lance-graph-supervisor/src/cycle_driver.rs` (1811 lines, two calls); +`crates/lance-graph-planner/src/strategy/style_strategy.rs`; +`crates/lance-graph-planner/src/owner_adapter.rs`; +`.claude/board/exec-runs/dblw3-api-inventory-sonnet.md`; +`.claude/board/AGENT_LOG.md` first 120 lines. + +**Read in the specific regions cited (not in full):** `batch_writer.rs` +(module doc + all methods + tests); `persist_sink.rs` (`persist_cycle`, +`SweepSlot`, `CycleFrame` only); `mailbox_soa.rs` (struct fields, +`new`/`write_row`/`apply_edges`/`consume_firing`/`tick`/`set_populated`, the +`MailboxSoaView`/`MailboxSoaOwner` impls, qualia/meta accessors); +`contract/mul.rs` (`gate_decision_i4`, `trust_texture_i4`, `flow_state_i4`); +`contract/kanban.rs` (`next_phases`, `can_transition_to`, `advance_on_gate`); +`contract/canonical_node.rs` (`KanbanTenant` + `NodeRow::kanban`); +`contract/cognitive_shader.rs` (`MetaWord`); `traits.rs` lines 85-205; +`blw_fusion.rs` (module doc, constants, `MemWal`, loaders, the main seal loop); +`.claude/plans/kanban-64k-inverted-awareness-v1.md` §0-§4. + +**NOT read — treat any claim depending on these as UNVERIFIED:** +`persist_sink.rs` in full (line numbers for `persist_cycle` / `WalSink` are +cited by name only, not by line — `recover_and_apply:396` and its guards at +`:410/412/421/430` are taken from the knowledge doc's receipts, not re-read); +`temporal.rs` (relied entirely on the Sonnet inventory); +`contract/soa_view.rs` (`try_advance_phase` at `:295-322` taken from the +knowledge doc); `contract/qualia.rs` beyond `ZERO`/`get`/`with`; +`kanban_actor.rs`; `blw_tenant.rs` (cited only through `blw_fusion.rs`'s +provenance comments and the Sonnet inventory §C); `crates/jc`. + +**Not run:** no cargo command of any kind was executed by this lane. + +--- + +## ⊘ Post-review corrections (2026-08-05, external review round on PR #891) + +**G1/G2b — the Flow/Block split (real internal inconsistency; corrected here +and relayed to the build lane mid-flight).** §3's G2b as written ("every +sealed move with `from == Planning` has `exec == Elixir` and +`to == CognitiveWork`") contradicts §2 step 8 of THIS note: the 4 CONTRA +owners' gate says Prune at c1, so their Planning-column casts are +`shade_owner`'s — `exec == Native`, `to == Prune`. Corrected assertions: + +- **G1 can-fire decomposes:** c1's 24 advances = **20 Flow** + (`Planning → CognitiveWork`) + **4 Block** (`Planning → Prune`), each + counted separately. +- **G2b Elixir claim restricted:** every `Planning → CognitiveWork` move has + `exec == Elixir` (the STYLE's mints); **new twin:** every + `Planning → Prune` move has `exec == Native` (the GATE's mints via + `shade_owner`). The can-stay-silent half (`CognitiveWork`/`Evaluation` ⇒ + `Native`) is unchanged. + +**Skips from the same round, with reasons (deliberate deferrals this note +already records):** the upstream missing-owner counter (G10's own comment +names it as the follow-up — the probe's job is to make the gap observable, +Q2 ruling: no upstream change in this PR); the retry-safe seal API (G9 +exists precisely to make the #879 drained-writer footgun VISIBLE — the guard +is the next deliverable, not this probe); the 36-style MetaWord→PlanContext +mapping (open question Q1, ruled out of this probe — 3-style honesty is +printed; `persona-vs-rung-ladder.md` is the mandatory read before any +bridge is designed). diff --git a/.claude/knowledge/batchwriter-kanbanstep-wiring.md b/.claude/knowledge/batchwriter-kanbanstep-wiring.md index d7aa6eac4..43076d06d 100644 --- a/.claude/knowledge/batchwriter-kanbanstep-wiring.md +++ b/.claude/knowledge/batchwriter-kanbanstep-wiring.md @@ -327,7 +327,7 @@ introduced mutation (a guard that cannot bark is the defect one level up). | thing | verdict | |---|---| | `blw_bible_lens_wave.rs` (tiled 64 owners) | category error — an owner is a **tenant**, not a shard; it fabricated 63 tenants. Deleted while **green**, because it was green on a fabricated shape. | -| a KJV parser written into `nars/stance.rs` | layer violation — the inbound leg already had one. Reverted. | +| a KJV parser written into `nars/stance.rs` | layer violation — the inbound leg already had one. Reverted. **(Clarified 2026-08-05 after an external reader took this row to mean stance.rs itself was reverted:** what was reverted is the duplicate INGESTION parser — Gutenberg/verse splitting, which `deepnsm-v2::corpus` owns. The stance MACHINERY (clause→belief→panel) was deliberately LIFTED from the probe example into `lance_graph_planner::nars::stance` at 4a74d69 and is live — a promotion, not a revert. Two different objects.) | | `blw_lens_twin.rs` (the κ instrument) | κ retired as the instrument (§12.3c): it measures *coincidence* and discards what a stance is. Nihilism and sarcasm are both negative, so no sign/boolean separates them. | | `blw_texture.rs` (the texture instrument) | **measured KILL** (§12.7): used the 24-locus register and wrote **3 loci**, only 1 shared, so `agreement_count` was capped at 1 **before any verse was read**. The carrier changed; the instrument did not. | @@ -344,9 +344,15 @@ the alternation), and read the printed count: ```sh rg -c 'batch_writer|BatchWriter|KanbanStep|KanbanMove|kanban|owner_adapter|MailboxSoA|SoaEnvelope' \ - crates/lance-graph-planner/examples/.rs || echo 0 + crates/lance-graph-planner/examples/.rs \ + || { s=$?; [ "$s" -eq 1 ] && echo 0 || echo "rg FAILED (exit $s) — not a count"; } ``` +(`rg` exits 1 for genuinely-zero matches and 2 for a real failure — wrong +path, bad pattern. A bare `|| echo 0` would launder a failure into "zero +matches, free-standing harness"; the exit-code split above keeps the two +distinguishable.) + **A count of 0 means your harness is a free-standing loop** and cannot support a substrate claim, however green it is. That grep returned `0` for `blw_texture.rs`, which is how D-BLW-1 was found to be unbuilt while a harness stood in for it. @@ -363,13 +369,30 @@ is byte-identical (the #879 anti-vacuity falsifier, green at 64k/17). Any change that widens the write back toward dense/full-image, or adds a per-cast physical write, reverses #879 and is rejected on sight. -**Invariant 2 — interlacing is prevented by `temporal.rs`, at READ time.** -Cross-mailbox ordering is never a write-side concern: the writer fires ahead, -no ack exists (`E-ACK-ELIMINATED-1`), and any consumer needing order recovers -it through the deinterlace surface (`deinterlace` / layer-1 -`local_trajectories`, sort key `cast_seq` / `(hlc ?? version, version)`). -Re-introducing write-side ordering, synchronization, or a confirmation ledger -reverses #879 and is rejected on sight. +**Invariant 2 — arrival order is never a write-side concern; canonical order +is established by deinterlace BEFORE the seal.** (Rewritten 2026-08-05 — the +earlier one-sentence form read as "read-time only," contradicting §8's +operator-sharpened deinterlace-before-write ruling. Two distinct claims, +both required, never conflated:) + +- **(a) Cross-MAILBOX arrival:** the writer fires ahead; no ack exists + (`E-ACK-ELIMINATED-1`); nothing at the write site synchronizes mailboxes + against each other. Re-introducing write-side cross-mailbox ordering, + synchronization, or a confirmation ledger reverses #879 and is rejected + on sight. +- **(b) Per-MAILBOX canonicalization:** casts never arrive in the same + order, and the SEAL takes deinterlaced input — the caller canonicalizes + each mailbox's casts before sealing, via `temporal.rs` (`deinterlace` / + layer-1 `local_trajectories`, sort key `cast_seq` / + `(hlc ?? version, version)`) or via the known-order hash helper ONLY once + certified equally exact on the out-of-order regime (§8; + `TD-RECOVERY-HASH-PARTITION-UNCERTIFIED`). A caller sealing raw arrival + order violates (b) without violating (a) — the seal must never ingest + raw arrival order. + +For STORED logs, `temporal.rs` remains the canonical recovery surface at +read time; (b) governs the write path's input, not a new cross-mailbox +synchronization. **The caveat the ruling names — a hash partition stands where temporal.rs is preferred.** `cycle_driver::recover_fleet` (P4e, `cycle_driver.rs:700-746`) @@ -525,3 +548,75 @@ yields Hold everywhere and casts nothing. A brain that cannot rest is the > corrected mid-flight and its can-fire assertion gains the twin: *the driver > discovered the work by reading the board, and nothing else could have told > it* — no side channel may exist in the probe. + + +--- + +## ⊘ Sharpening (2026-08-05): what "zero production callers" means — and what changed + +An external review read "zero production callers" as a claim that the +library chain itself is unwired. It is not, and never was: the +library-internal edges exist and are documented above +(`emit_bootstrap_intent` → `BatchWriter::cast`; `run_cycle` → +`collect_casts` → `seal_cycle` → `apply_sealed_transitions`). The claim was +always about the ROOT: **no production runtime invokes that chain.** + +Status change (2026-08-05): `tests/probe_ignition.rs` (GREEN, 2/2) now +drives the complete chain — arm → scan → `emit_bootstrap_intent` → `cast` → +`run_cycle` → seal → apply — as a test-rooted driver. The remaining honest +gap is narrower and is stated as such: **no live, externally-rooted runtime +invokes the chain repeatedly over the intended real owner population against +a durable sink.** That is an integration/rooting slice (the deepnsm-v2 +consumer direction), not another driver. + +Separately: `blw_fusion.rs` predates the probe and produces its sealed +SERIES by calling `persist_cycle`/`recover_and_apply` directly with a +hand-built `SweepSlot` — it does NOT exercise `collect_casts`/`seal_cycle`/ +`run_cycle`, and its records never claimed it did (its permitted claims are +the `DeinterlaceRow`/`deinterlace` firsts and the rank-criterion finding). +Rebasing its seal loop onto `run_cycle` is tracked as +`TD-BLW-FUSION-MANUAL-SEAL`. + + +--- + +## ⊘ OPERATOR ORDER (2026-08-05): §10's doctrine is THE MAIN MODEL + +The decide-or-continue ≤64k doctrine in §10 is not one reading among +several — by operator order it is **the main model**: 64k 1:1 owners, +compile-time mutation-exclusive, independent thought bodies, one +deterministic seal boundary. Any text in this doc or elsewhere that reads +the one-tenant benchmark configuration as the architecture is subordinate +to this order. Canonical entry: `EPIPHANIES.md` +E-64K-1TO1-OWNERS-IS-THE-MAIN-MODEL-1. The outer-level parallelism claim +is gated by D-KIA-A2's pre-registered falsifier; the GREEN probes +(probe_ignition, d_ign_b_lenses) already drive the 1:1 topology, 64 owners, +synchronously. + + +--- + +## Disk headroom for the measurement arc (operational, 2026-08-05) + +The 64k measurement binary writes **576 MiB of WAL scratch per configuration** +(18 cycles × 32 MiB) and needs a release `target/`. Two ENOSPC lessons, both +paid for: + +1. **Ten live scratch files need ~5.8 GiB.** The first real run died at + `StorageFull` partway through the WAL curve. Fixed in the binary: each + configuration's file is reclaimed the moment that configuration ends + (`drop(file)` then `remove_file`, so the unlink frees blocks immediately). +2. **`target/debug/deps` reached 11 GiB** and left only 3.9 GiB free — one + release rebuild plus a run away from failing again. + +**The reclaim that is always safe here:** `rm -rf target/debug/{deps,build, +incremental}` — cargo rebuilds them on demand, and unlike `cargo clean` +(forbidden in this workspace) it leaves `target/release` intact. That single +command took the tree from 13 GiB → 697 MiB and the disk from 90 % → 59 %. + +**Before any measurement run:** check `df -h /` and require **≥ 3 GiB free +beyond** what the run's scratch needs. A run that dies at ENOSPC halfway +through wastes the whole configuration sweep — and worse, a run that *nearly* +runs out produces exactly the page-cache/writeback instability that made the +WAL knee unreadable (the host was ~90 % full during every A0 run — a stated +caveat on that result, not a footnote). diff --git a/.claude/knowledge/observer-effect-tfpn-doctrine.md b/.claude/knowledge/observer-effect-tfpn-doctrine.md new file mode 100644 index 000000000..3e9584af3 --- /dev/null +++ b/.claude/knowledge/observer-effect-tfpn-doctrine.md @@ -0,0 +1,166 @@ +# Observer-Effect Measurement Doctrine — the TFPN design (D-BLW-5) + +> **READ BY:** truth-architect, certification-officer, integration-lead, and +> any session building D-BLW-5 or ANY probe that feeds a measured statistic +> back into the awareness lane (jc loops, MUL-coupled criteria, NARS +> belief-injection experiments). +> +> **Status:** design doctrine, operator-ruled 2026-08-04. The *machinery* +> references are FINDING (every cited surface exists in source, anchors +> below). The *effect* is CONJECTURE — D-BLW-5 is unmeasured, queued behind +> PROBE-IGNITION. Plan: `.claude/plans/cycle-loop-closure-driver-v1.md` +> §12.9 + §12.9a. + +--- + +## 1. The claim under test + +D-BLW-3 (§12.8, SHIPPED + MEASURED) measured **first-order fusion**: horizons +merge by sharing *data* (pool growth; Δκ −0.485 → 0 over 8 horizons). +D-BLW-5 measures **second-order fusion**: horizons merge by sharing the +**measurement of each other**. Information about the correlation of a +dataset, when it enters the awareness, influences the correlation — the +observer effect, run deliberately and instrumented instead of avoided. + +The Click's own arrow is the hook: `awareness.revise(key, outcome)` → +`global_context += fact` → reshapes the NEXT cycle's F landscape. Here the +injected fact is *about the cohort's own statistics*. + +--- + +## 2. The payload law — distribution × Prozentrang, NEVER the raw statistic + +**What is injected is not the correlation.** Injecting the raw scalar (κ, φ, +or the full `BinaryAssociation` as a value) builds the Goodhart collapse into +the instrument: a scalar is trivially echoable, so the anchoring fixed point +(awareness parrots the number back) is available by construction and the +F-arms cannot distinguish anchoring from reflection. + +**What is injected — the preserving payload:** + +1. **The distribution SHAPE** of the statistic over the *prior* pool — a + palette256/HDR-bucketed census (banded exposure with popcount-stacking + early exit, statistical confidence-interval thresholds, preheating + + rolling floor bucket — the Belichtungsmesser reading). +2. **The Prozentrang** — the percentile rank of the observed association + *within that prior distribution*. A rank-within-a-shape says where the + observation SITS without handing the awareness a value to parrot. + +**Machinery anchors (FINDING — these exist):** +- `ndarray::hpc::cascade` — `expose(distance) → Band` + (Foveal/Near/Good/Weak/Reject, `cascade.rs:162-175`) + + `recalibrate(&mut self, alert: &ShiftAlert)` (`cascade.rs:211`): the + banded exposure meter with recalibration. +- `ndarray::hpc::statistics` — `percentile(&self, p)` (`statistics.rs:41`). +- Exact wiring of shape-census → injection payload is pinned at build time; + the doctrine binds the SHAPE of the payload, not an API. + +--- + +## 3. The single-measurement law — once measured, never remeasured + +**A measurement burns the state it measured.** + +- S₀ is measured **once**, at version V₀, and sealed (version-stamped). +- After injection, the system that produced S₀ no longer exists. Running the + instrument again yields **S₁ at V₁ — a NEW one-shot measurement of a + DIFFERENT (post-injection) system** — never a "remeasure of S₀". A + remeasure of S₀ does not exist even in principle. +- The only thing that carries forward from V₀: **shape₀ × rank₀**, frozen. + +**Enforcement = temporal.rs hindsight blindness × the shape sensor:** + +- `temporal.rs` supplies the blindness: rung-gated version-range reads — a + Strict-rung reader at V sees only ≤ V. The D-BLW-3 fusion example is the + proven precedent (`no_hindsight_streamed_known_game`; `QueryReference::at` + + `deinterlace`). +- The shape sensor's output rides as **META only**: rung-marked ELEVATED + (statistic-as-witness, higher-rung derivation — the zero-copy carve-out), + never as corpus, and **never recomputed over post-injection data and + back-dated to V₀**. +- This combination is what makes the probe viable *without* remeasurement: + every arm's injection payload derives from the V₀-sealed shape; every + arm's observable is a fresh V₁ one-shot; the comparison is between two + version-stamped one-shots, each blind to what came after it. + +--- + +## 4. The TFPN arms, with their philosophical readings + +| arm | injection (per §2) | reading | pre-registered expectation | +|---|---|---|---| +| **T** (true) | shape₀ × true rank₀, ELEVATED-rung | **Gadamer, Wirkungsgeschichte**: understanding is conditioned by what tradition hands it — the fused horizon is *wirkungsgeschichtliches Bewusstsein* made measurable | the observable: S₁ − S₀ (two one-shots, V₁ vs V₀). Fires iff it clears the pre-pinned floor. | +| **F+ / F−** (false) | true shape₀ × FALSE rank — **bounded-safe payload**: equal-magnitude opposite shifts applied in **logit(rank) space**, which is unbounded and symmetric by construction, so no boundary clipping can occur; anchors whose true rank falls outside a pinned eligibility band [δ, 1−δ] are **excluded from the F-arms entirely, never clipped** (clipping would silently unequalize the ± magnitudes and confound the direction test) | **the direction test.** S₁ tracking the injected rank = **Gadamer's Vorurteil as anchoring** (prejudice-structure dominating evidence) = **Goodhart realized** (a measure that becomes a belief ceases to be a good measure). S₁ correcting TOWARD truth against the injection = evidence-dominance (the hermeneutic circle revising its prejudice). Movement invariant to the injected value = mere perturbation, no verdict. | both directions MUST run — a one-sided F is not a direction test. | +| **P** (placebo) | structurally identical shape-and-rank envelope, zero information (choice pinned at build time; note: "uniform shape + median rank" is NOT empty — it asserts *nothing unusual*, a real statement) | instrument validity | **must not move.** If P moves S₁, the instrument measures injection mechanics, not information. | +| **N** (null instrument, free) | the T-injection observed through the §12.8 bloom-rank criterion | the awareness-free control | **must stay frozen BY CONSTRUCTION** — that criterion has no awareness term. Second duty: N's own V₀-vs-V₁ shape drift measures pool drift absent awareness coupling — the baseline the observer effect is read against. | + +**Mechanical prerequisite (honest):** the §12.8 popcount-rank instrument +CANNOT exhibit the effect — no awareness input — which is exactly what makes +it arm N. The observed reader must be awareness-coupled: NARS-revision side +(the injected shape×rank participates as a belief, interacting via +support/contradiction) or a MUL-qualia-coupled criterion. Choosing which is +the probe's first build decision. + +--- + +## 5. The falsification regimen + +Pre-registration and guards, all binding BEFORE any run: + +1. **Pre-registered, non-adjustable numbers.** Every floor, band, and rank + shift is pinned at build time, before the first run. Hand-tuned values + are labeled as such per I-NOISE-FLOOR-JIRAK. Post-hoc adjustment of any + threshold voids the run. +2. **Kill conditions, pre-accepted:** + - **P moves** ⇒ instrument invalid. Reported, not tuned away. + - **N moves** ⇒ plumbing leak; the run is void (the G2 pattern one level + up: an awareness-free criterion that responds to awareness input is a + defect in the harness, not a discovery). + - **T silent at every floor** ⇒ the finding is the honest null: + "awareness does not reflect this statistic." True and useful. + - **F tracks injected rank** ⇒ the anchoring/testimony-dominance finding + stands even if T is silent — Goodhart-vulnerability is itself the + discovery. +3. **Guard twins (house falsifiability rule):** every gate carries a + can-FIRE test and a can-STAY-SILENT test, both on non-trivial inputs. +4. **The remeasure guard (new, from the single-measurement law):** the + measurement ledger is append-only, keyed + `(statistic-id, arm, cohort, metric, version)` — scope-qualified, so + independent arms/cohorts/metrics legitimately writing at the same + version never collide with each other; only a true recompute of the SAME + scoped one-shot hits a sealed key. A second computation attempt at a + sealed key must ERROR. + - can-fire: a test attempts the recompute and proves the guard barks; + - can-stay-silent: a fresh `(id, scope, V+1)` one-shot passes untouched, + AND a different arm's write at the same `(id, version)` passes. +5. **Direction-test symmetry:** F+ and F− both run, same magnitude of + shift in logit(rank) units, opposite signs — the equal-magnitude + requirement is defined in the space where it cannot be broken by the + rank bounds (see the F± payload rule in §4). +6. **No p-values** (C4). The paired contrasts + placebo + null-instrument + arms ARE the inference. Full tables, never bare κ (C2 naming). +7. **Anti-circularity, instrumented not violated (C6):** C6 forbids a + witness gating the slice it was computed on because that is a + self-proving loop. This probe deliberately CLOSES that loop and MEASURES + it — therefore **nothing downstream may gate on S₁**, ever. The loop is + an observable, never an admission criterion. +8. **jc stays the one-way oracle:** `crates/jc` measures S₀ and S₁, is + never modified, and is never fed its own output as input. The loop runs + through the system's awareness, not through jc. + +--- + +## 6. Cross-references + +- Plan: `.claude/plans/cycle-loop-closure-driver-v1.md` §12.8 (D-BLW-3 + first-order result), §12.9 (D-BLW-5 arms), §12.9a (payload refinement — + this doctrine's plan-side mirror). +- Board: `EPIPHANIES.md` `E-HORIZONTVERSCHMELZUNG-GAP-CLOSES-1` (first-order + gap closure), `E-MEASUREMENT-BURNS-THE-STATE-1` (the single-measurement + law). +- Iron rules: I-NOISE-FLOOR-JIRAK (threshold labeling); + falsifiability rule (CLAUDE.md P0 — can-fire/can-stay-silent twins). +- Precedent code: `crates/lance-graph-planner/examples/blw_fusion.rs` + (version-gated hindsight-blind reads, the G-gate discipline); + `crates/lance-graph-planner/src/temporal.rs` (`QueryReference::at`, + rung admission, `deinterlace`). diff --git a/.claude/knowledge/seal-vs-temporal-ordering-information.md b/.claude/knowledge/seal-vs-temporal-ordering-information.md new file mode 100644 index 000000000..f269ff2ca --- /dev/null +++ b/.claude/knowledge/seal-vs-temporal-ordering-information.md @@ -0,0 +1,184 @@ +# What the seal computes that `temporal.rs` does not encode + +> **READ BY:** any session touching `persist_sink::{freeze, order_cycle_stably, +> DetachedCycleBatch}`, `temporal::{local_trajectories, local_trajectory_of, +> LocalCausalRow}`, the cycle-loop closure driver, or any proposal to "re-scope +> the seal" / "let temporal.rs provide the ordering". +> +> **Status:** the four differences below are **read off the shipped source** and +> are therefore FINDINGs about the code as it stands. The *consequences* for +> future design are labelled separately. Born from the O-arm digest divergence +> (`measure-64k-axes-v3.md`, measured 2026-08-05). + +--- + +## The question this answers (operator-framed, 2026-08-05) + +The O-arm asked "can ordering be sourced from temporal replay instead of the +seal?" and got **DIVERGED** — O-A `64565f362db2e4a5` ≠ O-B `3e71c2aa7be8e325`. +The operator's reframing is the one worth keeping: + +> Not *"can we remove temporal ordering?"* but **"what information does the seal +> compute that `temporal.rs` does not currently encode?"** — because the digest +> divergence means one of them knows something the other doesn't, and that +> something is probably the real architectural asset. + +The standing position that follows, and which this doc records as the workspace +default until measured otherwise: + +- **`temporal.rs` = the authoritative TEMPORAL model** (what a reader may see; + what one owner actually did, in its own order). +- **The seal = the authoritative ORDERING model** (one total order, one fold, + one cohort, one published version). +- **The gap between them is an explicit research question, not a redundancy to + be resolved by deleting one side.** + +--- + +## The answer: four things, all of them absent from the temporal surface + +### 1. A cross-owner TOTAL order — where temporal computes a PARTIAL one + +`LocalCausalRow::cast_seq`'s contract says it outright +(`temporal.rs:400-402`): + +> *"Cross-owner values are never compared; only rows sharing an `owner()` are +> ordered against each other."* + +`local_trajectories` therefore produces a **forest of per-owner chains** — a +partial order globally. `DetachedCycleBatch::freeze` produces **one total +order** over all owners (`order_cycle_stably(&mut casts, |s| s.stream_position)`, +`persist_sink.rs:197`). + +**A partial order does not determine a total order.** The O-arm divergence is +the expected signature of that difference, not evidence of a defect on either +side. Any future "let temporal source the ordering" proposal has to start by +supplying a globally comparable key — which means *widening the very contract +the deinterlace exists to keep narrow*. + +### 2. Arrival as an ordering INPUT — recorded nowhere else, durably + +The seal's key is `stream_position`, and its sort is **stable**, so equal keys +keep arrival order. In the measured harness `stream_position` IS the arrival +rank. Nothing in `temporal.rs` records arrival at all: `LocalCausalRow` is +exactly `(owner, cast_seq)`. + +So **the seal is the only place cross-owner arrival enters the durable record** +— and once it is in the record, it *is* the durable fact that every later read +returns (`scan_sealed` is contractually forbidden from re-sorting: +`persist_sink.rs:292-293`, *"this seam NEVER sorts (order is a write-side +property, fixed before the append)"*; restated at `:315-317`). O-B cannot reproduce it +because the information is not in the per-owner projection it reads. + +> **⚠ Hot-window caveat (2026-08-05, added by `measure-64k-axes-v4.md`).** +> "Once in the record it *is* the durable fact" holds only **at or below +> `durable_head`**. Under the hot version window (publication decoupled from +> durability, v4 §0), a sealed cycle's arrival order exists ONLY in RAM until +> the sync barrier — and where `stream_position` ties occur, that order is +> unreproducible from durable data by this doc's own PROBE-SEAL-TIE-DENSITY +> argument. A crash in `(durable_head, published_head]` therefore loses +> cross-owner arrival information that cognition may already have consumed. +> That is the window's accepted, explicitly-priced cost (v4 §H-1 die-together +> property; P3 measures the window's size distribution) — never a silent one. + +> **⚠ Scope, stated so it is not overread.** The O-arm deliberately scrambled +> arrival (bit-reversal of the owner id) precisely so the two orders were FREE +> to diverge — without that, every owner casts in ascending id order and the +> comparison would coincide vacuously. So the measured result says *the seal +> preserves an arrival order temporal cannot see*, **not** *the seal always +> disagrees with temporal*. On an arrival-ascending workload they would agree, +> and that agreement would prove nothing. + +### 3. The per-row coalescing FOLD — a row concept temporal does not have + +`freeze` also builds `image: BTreeMap` — *"the coalesced final +image: `row -> last payload in stream order`"* (`persist_sink.rs:185-186`, +`:198-201`). Which of N writes to one row survives is decided **by the +cross-owner total order**, and two different owners can write the same row. + +`temporal.rs` has no row concept whatsoever. Last-writer-wins at row +granularity is computed **nowhere else in the system**. This is the most +concretely irreplaceable of the four: it is not an ordering that could be +re-derived, it is a *destructive fold* whose result depends on the ordering. + +### 4. The cohort boundary and its read horizon + +`CycleFrame { cycle, base_version }` (`persist_sink.rs:104-109`) carries two +facts the temporal surface has no field for: + +- **Cohort membership** — which casts belong to the same atomic, all-or-nothing + publication (one WAL append → one `DatasetVersion`). +- **The read horizon `base_version`** — *"the sealed predecessor every thought + in this cycle reads (`Vn`)"*. This is the "what did this cohort see" fact, and + it is epistemically load-bearing everywhere else in the stack. + +A per-owner trajectory carries neither. Reconstructing "these 65,536 casts read +the same `Vn` and landed together" from per-owner chains is not a matter of +sorting harder; the grouping key is simply absent. + +> **⚠ Hot-window caveat (2026-08-05, added by `measure-64k-axes-v4.md`).** +> The parenthetical "(one WAL append → one `DatasetVersion`)" anchors cohort +> membership on the physical append — an identity the hot version window +> retires. Cohort membership re-anchors on the **SEAL event** (which under +> v4's recommended barrier-flush fork still mints exactly one +> `DatasetVersion` per cycle; only the sync is batched). The read-horizon +> half of this property is untouched: the seal still stamps `base_version` +> per cycle. + +--- + +## The consequence for design (labelled: consequence, not measurement) + +**The two mechanisms are not competing implementations of one function.** They +compute different mathematical objects: + +| | seal (`freeze`) | `temporal.rs` (`local_trajectories`) | +|---|---|---| +| output | one total order over all owners | forest of per-owner chains (partial order) | +| ordering input | `stream_position`, stable ⇒ arrival breaks ties | `cast_seq`, per-owner only, cross-owner comparison forbidden | +| fold | yes — `row → last payload` | none (no row concept) | +| cohort | yes — `CycleFrame{cycle, base_version}` | none | +| question it answers | *what became durable, in what order, as one unit* | *what a reader may see; what ONE owner did* | + +So "can one replace the other" was the wrong shape of question, and the O-arm's +value is that it **failed semantically before it failed on performance** — which +makes the performance numbers (O-B slower on every phase but commit) almost +irrelevant to the decision. + +**The minimal change that would make temporal able to source the ordering** is +therefore not "make temporal smarter" but *"give `LocalCausalRow` a globally +comparable key"* — a contract widening that re-couples the owners the +deinterlace exists to decouple, and that would still not supply the fold (3) or +the cohort (4). Anyone proposing it owns that cost explicitly. + +--- + +## Probe queue (falsifiable, none run) + +- **PROBE-SEAL-TIE-DENSITY** — do cross-owner `stream_position` ties actually + occur in a realistic cast pattern? If they do, the seal's total order depends + on non-durable arrival sequencing at those points, and *replay from durable + data alone cannot reproduce it*. PASS = zero ties (order fully determined by + the key); FAIL = ties exist (order partly determined by arrival, which is not + stored). **Either outcome is a result.** Cheap: instrument `freeze`'s input. +- **PROBE-FOLD-COLLISION-RATE** — how often do two owners write the same `row` + in one cycle? Zero would mean the fold is currently inert under this workload + and its irreplaceability (item 3) is structural-but-unexercised; non-zero + means it is live. Must be measured before item 3 is cited as load-bearing *in + practice* rather than *in principle*. +- **PROBE-ARRIVAL-ASCENDING-CONTROL** — re-run the O-arm with arrival ORDER = + owner-ascending. Pre-registered expectation: digests MATCH. This is the + can-stay-silent twin of the divergence result: if they diverge even there, the + difference is larger than the four items above account for and this doc is + incomplete. + +--- + +## Cross-refs + +- `.claude/plans/measure-64k-axes-v3.md` § O-arm MEASURED RESULTS (the divergence). +- `.claude/board/EPIPHANIES.md` `E-SEAL-AND-TEMPORAL-ARE-DIFFERENT-OBJECTS-1`. +- `crates/lance-graph-planner/src/persist_sink.rs` — `order_cycle_stably`, + `DetachedCycleBatch::freeze`, `CycleFrame`, `WalSink::scan_sealed`. +- `crates/lance-graph-planner/src/temporal.rs` — `LocalCausalRow`, + `local_trajectories`, `local_trajectory_of`. diff --git a/.claude/plans/cycle-loop-closure-driver-v1.md b/.claude/plans/cycle-loop-closure-driver-v1.md index 59670ba1d..70fe1c8af 100644 --- a/.claude/plans/cycle-loop-closure-driver-v1.md +++ b/.claude/plans/cycle-loop-closure-driver-v1.md @@ -842,6 +842,17 @@ as a `[dev-dependencies]` path edge from `lance-graph-planner`. Do **not** inver it — hosting the harness inside `jc` would drag the planner's whole dep tree into a crate whose constitution is zero-dep, and §12.5 keeps `jc` the untouched oracle. +#### 12.3a‴ OPERATOR ORDER (2026-08-05): the 64k 1:1-owner model IS the main model + +**Ordered, not proposed.** The main model: up to 64k mailboxes, 1:1 +owner-per-mailbox, each compile-time mutation-exclusive, 64k independent +thought bodies, ONE deterministic convergence/seal boundary. §12.3a′ above +is read through this order as a BENCHMARK-axis ruling only (rows within one +owner = the inner parallelism level, D-BLW-4's measured territory); it never +redefined the architecture. The outer level's "parallel" claim runs through +D-KIA-A2's pre-registered protocol before it is ever stated as measured. +Canonical text: `EPIPHANIES.md` E-64K-1TO1-OWNERS-IS-THE-MAIN-MODEL-1. + #### 12.3a″ MEASURED RESULT (2026-08-04): D-BLW-2 is a STRUCTURAL KILL on the TSV path Built and **run** against the real export (`/tmp/kjv_spo.tsv`, 40,767 triples @@ -1393,7 +1404,11 @@ criterion P2 at q=0.25). | controls | Z byte-identical (plumbing zero); G1 three-way extensional identity (Aware≡Retro≡Strict@V8); G4 both tails + real-data silent arm (the design's "~90 % god" premise measured 0.1285 — fixture replaced with constant-by-construction tails); G5/G6/G7 green | **The headline, exactly as large as the measurement:** the a-priori/hindsight -gap **closes monotonically as the horizon extends** — Δκ: −0.485 (V1), +gap **moves toward zero overall, with a small rebound at V6/V7** — +*(⊘ 2026-08-05 regrade: measured under a GROWING reference pool — the +trajectory is a cohort-relative rank effect until the A/B/C decomposition +runs; see the E-entry's regrade and D-BLW-3b below. The numbers stand; the +fusion ATTRIBUTION is CONJECTURE.)* Δκ: −0.485 (V1), −0.251, −0.079, −0.031, ≈0.000, +0.011, +0.017, 0.000 (V8, identical by construction); Hamming(A): 152→123→94→66→53→37→21→0. The distinction does real work early in the series and dissolves as horizons merge. That is a @@ -1402,3 +1417,362 @@ no fusion verdict at V_pin, no substrate-exercise claim** (under this corpus `deinterlace` reduces to filter+sort; the finding lives in the rank criterion; the permitted claim is first `DeinterlaceRow` implementor and first `deinterlace` caller). §12.4's D3b validity gate stays closed. + +### 12.9 D-BLW-5 PROPOSED (2026-08-04, operator) — the OBSERVER-EFFECT loop: the jc measurement fed back into awareness + +> Operator framing, verbatim intent: *a scientific version of +> Horizontverschmelzung is the jc-crate loop — information about the +> correlation of a dataset, when fed into the awareness, influences the +> correlation. The observer effect.* + +**Status: DESIGNED / CONJECTURE — queued behind PROBE-IGNITION. Nothing here +is measured.** + +**What it adds over §12.8:** D-BLW-3 measured FIRST-ORDER fusion — horizons +merge by sharing data (pool growth; Δκ −0.485 → 0). This probe measures +SECOND-ORDER fusion — horizons merge by sharing the MEASUREMENT of each +other. The Click's own arrow is the hook: `awareness.revise(key, outcome)` → +`global_context += fact` → *reshapes NEXT cycle's F landscape*. Here the +injected fact IS a jc statistic about the cohort. + +**The four-arm design (pre-registered SHAPE; numbers pinned at build time, +before any run):** + +| arm | injection | pre-registered expectation | +|---|---|---| +| **T** (true) | `shape₀ × true rank₀` derived from sealed S₀ (§12.9a payload — never the raw statistic), injected as an ELEVATED-rung fact | the observable: S₁ − S₀. Fire iff it clears the floor. | +| **F+ / F−** (false) | the TRUE shape₀ with a FALSE rank: equal-magnitude opposite shifts applied in **logit(rank) space** (symmetric by construction, no boundary clipping); anchors whose true rank falls outside a pinned eligibility band [δ, 1−δ] are EXCLUDED from the F-arms, never clipped | the DIRECTION test: S₁ tracking the injected RANK = anchoring on testimony over evidence (Gadamer's prejudice-structure, measurable; Goodhart's shadow); S₁ correcting TOWARD truth against the injection = evidence-dominance; value-invariant movement = mere perturbation. | +| **P** (placebo) | same shape, permuted content, zero information | **must not move** — if placebo moves S₁, the instrument measures injection mechanics, not information, and the observer-effect claim dies. | +| **N** (null instrument, free) | the same T-injection against the §12.8 bloom-rank criterion | **must stay frozen BY CONSTRUCTION** — that criterion has no awareness term, so any movement there is a plumbing leak that voids the run (G2's pattern, one level up). | + +**Mechanical prerequisite, stated honestly:** the §12.8 instrument CANNOT +exhibit the effect — popcount-rank has no awareness input, which is exactly +what makes it arm N. The observed reader must be awareness-coupled: the +belief-arena side (NARS revision — the injected statistic participates as a +belief and interacts via support/contradiction) or a MUL-qualia-coupled +criterion. Choosing which is the probe's first design decision. + +**Rulings that bind:** +- `crates/jc` stays the ORACLE — it measures S₀ and S₁ and is never modified + and never fed its own output as input (the edge is one-way; the LOOP runs + through the system's awareness, not through jc). +- C6 anti-circularity is not violated — it is INSTRUMENTED: C6 forbids the + witness gating the slice it was computed on because that is a self-proving + loop; this probe deliberately closes that loop and MEASURES it instead of + using it for admission. Nothing downstream may gate on S₁. +- The injected statistic is stored under the ELEVATED carve-out (statistic- + as-witness, higher-rung derivation) and must be rung-marked so the reader + knows it is meta, not corpus. +- C4: no p-values; the paired contrast + placebo + null-instrument arms ARE + the inference. C2 naming; full tables, never bare κ. + +**Kill conditions, pre-accepted:** placebo moves ⇒ instrument invalid +(reported, not tuned); T-arm silent at every floor ⇒ "awareness does not +reflect this statistic" is the finding — a true and useful null; F-arms +tracking injected values ⇒ the anchoring finding stands even if T is silent +(testimony-dominance is itself the discovery). + +#### 12.9a Payload refinement (operator, 2026-08-04 same day — refines the arm table's injection column in place) + +> Full doctrine (TFPN arms + Gadamer/Goodhart readings + falsification +> regimen): `.claude/knowledge/observer-effect-tfpn-doctrine.md`. This +> subsection is the plan-side delta. + +1. **Not the correlation — distribution × Prozentrang.** The injected fact + is never the raw association scalar (a scalar is trivially echoable, so + the Goodhart/anchoring fixed point would be built into the instrument). + The preserving payload is (a) the distribution SHAPE of the statistic + over the *prior* pool — palette256/HDR-bucketed census via the + Belichtungsmesser machinery (banded exposure + popcount-stacking early + exit + CI thresholds + preheat/rolling-floor; anchors: + `ndarray::hpc::cascade::{expose→Band, recalibrate}`, + `ndarray::hpc::statistics::percentile`) and (b) the **Prozentrang** — + the percentile rank of the observed association within that prior shape. +2. **The single-measurement law.** A measurement burns the state it + measured: S₀ is one-shot at V₀, sealed. Post-injection the system that + produced S₀ no longer exists — the instrument's next run is S₁ at V₁, a + NEW one-shot of a DIFFERENT system, never a "remeasure". The only + carry-forward from V₀ is shape₀ × rank₀, frozen. +3. **temporal.rs × sensor = the meta channel.** Hindsight blindness + (Strict-rung version-gated reads, the D-BLW-3 `no_hindsight_*` + precedent) × the shape sensor, riding as META only (ELEVATED + rung-marked), never corpus, never recomputed-and-back-dated. This is + what makes the probe viable *without* remeasurement. +4. **Arm-table deltas:** T injects shape₀ × true rank₀; F± inject the TRUE + shape₀ with a FALSE rank (shifted high/low on the bounded rank axis — + cleaner than fabricating a whole table); P's zero-information envelope + choice is pinned at build time (note: uniform-shape + median-rank is NOT + empty — it asserts "nothing unusual"); N unchanged + gains the + pool-drift-baseline duty (its own V₀-vs-V₁ shape drift, awareness-free). +5. **New guard:** the remeasure guard — append-only measurement ledger + keyed `(statistic-id, arm, cohort, metric, version)` (scope-qualified so + independent arms/cohorts/metrics at the same version never collide); + recompute at a sealed key ERRORS, with can-fire + can-stay-silent tests. + +### 12.10 PROBE-ARC-TORQUE family PROPOSED (2026-08-04, operator) — torque of an arc, translator stray, author bias + +**Status: PROPOSED / CONJECTURE throughout. Queued behind PROBE-IGNITION and +D-BLW-5. Nothing here is measured. Machinery anchors verified in source +where marked FINDING.** + +The operator's underlying question, three stages of one instrument: can the +TORQUE of an arc be measured and embedded using HHTL (WordNet) × Helix +(Fisher-2z hydratable cosine replacement) — and can that instrument then +measure translation variance (where did the translator stray; what mindset +does a version carry vs the Greek/Aramaic sources) and author bias (does a +non-canonical book match any canonical author). + +#### Stage A — the torque estimator (single corpus) + +- **Torque MAGNITUDE is purely metric** [derivation, not yet run]: per-step + torque about an anchor = `|r × F| = 2 × area of the triangle + (anchor, p_k, p_{k+1})` — Heron's formula from THREE pairwise distances + (anchor→p_k, p_k→p_{k+1}, anchor→p_{k+1}). HHTL path distance is 3 + tier-table lookups O(1), so per-step torque is O(1) table reads. Total + |torque| of the arc = the area swept by the lever. The radial sign + (approach vs recede) is also free from distances (c < a vs c > a). +- **CHIRALITY (clockwise/counter-clockwise) is NOT metric** — it needs a + frame. The shipped carrier: `ndarray::hpc::splat3d::helix_orient` [FINDING + — verified in source]: RVQ-on-the-sphere direction codes, decode + **Fisher-2z normalized**, comparable in O(1) LUT without materializing the + vector; measured 1–3 B at 4.87°/0.97°/0.073°, compare-without- + materialization Pearson 0.9917 / Spearman 0.9924. +- **Embedding coordinate: Fisher 2z = ln((1+r)/(1−r)) = logit((1+r)/2)** — + the variance-stabilized (Var ≈ const, independent of ρ), evidence-additive + (log-odds) coordinate for cosine-valued quantities; hydratable back via + `tanh(z)`. Equal-width palette256 buckets in 2z-space ≈ equal-information + buckets, where raw-cosine buckets starve the tails (near ±1 — exactly + where near-synonyms/antonyms live). This is what makes the cosine + REPLACEMENT properly HDR. +- **Pre-registered falsifiers:** + - F1 radial-vs-tangential: on WordNet, a hypernym chain THROUGH the anchor + is radial ⇒ torque ≈ 0 (can-stay-silent); a co-hyponym walk at constant + depth AROUND a common-hypernym anchor circulates ⇒ torque > pinned floor + (can-fire). If the estimator does not separate these, it dies. + - F2 clamp accounting: quantized distance tables can violate the triangle + inequality ⇒ Heron's radicand can go negative. Clamp AND COUNT; a clamp + rate above a pinned ceiling invalidates the estimator at that codec tier + (feeds PROBE-CLAM-VS-HELIX-RESIDUE, task #66). + - F3 additivity inertness: accumulate-in-2z vs accumulate-in-r must + DIFFER on real arcs — else the Fisher machinery is decoration. + - F4 hydration round-trip: z → tanh → r within helix residue precision. + +#### Stage B — translation variance (the Erbsünde exemplar) + +- Units = verse-aligned parallel versions (canonical alignment is the + paired structure jc needs). Readers = per-version "arc passes near anchor + C" through ONE shared multilingual space (BGE-M3/XLM-R lens covers + English/German/Greek; **Koine-vs-modern-Greek drift is a caveat; Aramaic + coverage thin — deferred**). Agreement per anchor = jc full tables (C2). +- **The floor is intra-language variance:** same-language translation pairs + (e.g. multiple public-domain English versions) ARE the placebo arm — a + cross-language deviation counts as a STRAY only where its Prozentrang + against the intra-language deviation distribution clears a pre-pinned + rank (§12.9a payload law applied verbatim). +- **Ground-truth falsifier with a known answer:** Romans 5:12 — the Vulgate + "in quo" vs Greek "eph' hō" divergence, the historically documented stray + that fed the Erbsünde doctrine. Pre-registered: the detector must rank + that locus high between Greek-faithful and Vulgate-descended renderings + AND stay silent between two Greek-faithful renderings. Note Erbsünde + itself is an ANCHOR concept, not a token — the German text says Sünde; + the doctrine name lives in confessional literature. +- **Translator MINDSET** = the systematic (non-zero-mean) component of the + deviation field after floor subtraction — Gadamer's Vorurteil as a + measured object. TFPN mapping: T = source-text arc; F± = the translations + (historical injections whose direction is MEASURED, not fabricated); P = + intra-language pairs; N = a lens-free co-occurrence criterion (the lens + has its own training-distribution horizon; found strays must survive the + lens-free null or they are lens artifacts). + +#### Stage C — author bias + attribution (the hypothesis proof) + +- Per-author systematic torque field over the undisputed corpus; floor = + intra-author variance across that author's books. +- **The synoptic confound, handled:** literary dependence (Matthew/Luke + copying Mark) makes shared TEXT look like shared MIND. Bias is therefore + measured on the REDACTIONAL layer — the deviations from the shared + source — not on the shared text (redaction criticism as measurement; + the Stage-B stray logic reused unchanged). +- **Ground-truth falsifiers, all INSIDE the canon** (no external corpus + needed to validate the instrument): + - G1 Luke–Acts must MATCH (consensus single author) — can-stay-silent; + - G2 Mark 16:9-20 (the long ending) must SEPARATE from Mark 1:1–16:8 + (consensus interpolation) — can-fire; + - G3 the Pericope Adulterae (John 7:53–8:11) must SEPARATE from John + (consensus interpolation) — can-fire; + - G4 Revelation vs the fourth gospel must SEPARATE (the famous + stylometric split) — else the instrument is blunter than classical + stylometry; + - G5 Hebrews must NOT match undisputed Paul (modern consensus vs + patristic attribution). +- Only after G1–G5: non-canonical books nearest-author matched in bias + space, reported as distribution shape × Prozentrang per candidate author + — never a bare "matches X" scalar. Attribution outputs stay CONJECTURE; + classical function-word stylometry (Mosteller–Wallace lineage) is the + prior-art baseline the torque feature-space must beat or complement — + both reported. + +**Doctrine that binds all three stages:** §12.9a single-measurement law + +shape×rank payload; `observer-effect-tfpn-doctrine.md` falsification +regimen; jc one-way oracle; C4 no p-values; full tables always. + +#### 12.10a The Rosetta architecture (operator, 2026-08-05) — universal meaning space, per-language torque, living×dead hydration + +One pattern, two instantiations. A Rosetta Stone = a paired inscription of +the SAME meaning in two systems; the shared inscription calibrates the map +between them; the per-system residual IS that system's torque signature. + +**R1 — language × language (the two Babel codebooks).** The two language +codebooks (PROBE-BABEL-STANCES slice 2, SHIPPED — the existing two-Rosetta- +stone precedent) span a universal meaning space in which each language's +ROUTE to a shared anchor is its torque. The operator's example pair: Czech +reaches dying through aspectual PREFIXES (stem + prefix = concept + a +morphologically compositional rotation), German through NOMINALIZATION +(der Tod — the same anchor rotated into a substantive). Per-language +torque field = the language's mindset — the SAME estimator as Stage B's +translator mindset and Stage C's author bias, one level up. Pre-registered +pair for the can-fire/can-stay-silent twins: a known morphologically +divergent anchor (aspectual-verb family vs nominalization) must show +distinct signatures; a structurally parallel cognate pair must not. + +**R2 — living × dead (Jina hydration over the WordNet spine).** WordNet is +the DEAD SPINE: static taxonomy = HHTL addresses + lever arms, CLAM +neighborhoods (`ndarray::hpc::clam` build/`rho_nn`) + CHAODA anomaly +detection (clam.rs Phase 4) — structure without life: no frequencies, no +coverage of KJV archaisms/names, no meaning axes beyond taxonomy. Jina is +the HYDRATION (API key present in env — verified, presence only): for the +alignment set (tokens BOTH systems know), fit the projection ONCE and seal +it (single-measurement law: the alignment is version-stamped; hydrations +are stamped against that alignment version); for Bible-specific tokens the +spine lacks, hydrate THROUGH the sealed projection: + 1. **frequency** — gated against the in-tree COCA 20k ground truth + (`ndarray/src/hpc/jina/weights/coca_academic_20k.csv`: + `word, PoS, COCA_All, …`); + 2. **POS** — same gate; + 3. **orthogonal meaning** — the component of the Jina embedding in the + orthogonal complement of the WordNet-explained subspace: the axes the + taxonomy structurally cannot express (register, affect, era). + +**Hydration falsifiers (pre-registered):** +- H1 hydration gate: Jina-derived frequency/POS on a HELD-OUT slice of the + COCA overlap must clear a pinned rank-correlation floor BEFORE any + Bible-tail hydration is trusted. Fail ⇒ the living source is not + admissible on this spine. +- H2 CHAODA quarantine: a hydrated Bible-specific token that lands as a + manifold outlier under CHAODA is QUARANTINED, not silently projected — + the sealed projection is valid only on the manifold the Rosetta + calibrated. +- H3 the R1 torque twins (above). + +**What this buys the staged probes:** Stage A gets lever arms for tokens +WordNet cannot address (the KJV tail); Stage B gets the universal anchor +space in which translator torque is per-VERSION while language torque is +per-LANGUAGE — separable because R1 measures the language signature on +non-biblical text, so Stage B can subtract it; Stage C inherits both. + +#### 12.10b Jina → helix: the cosine>helix transcode, three routes (operator, 2026-08-05) + +The direction arrow is the design: **cosine is measured once and DEMOTED +into a hydratable code; helix codes are the runtime carrier.** Jina is +called at bake/seal time only (membrane — the API key never enters the hot +path, per compilation-vs-runtime doctrine); everything downstream runs on +codes. Which machinery applies depends on WHAT is encoded: + +1. **Pairwise cosines → the palette256 cosine replacement, buckets in + Fisher-2z space, back-hydratable** (the direct `cosine>helix`; operator- + confirmed naming 2026-08-05: "palette256, Fisher-z back hydratable — + the cosine replacement"). The torque estimator consumes specific + pairwise cosines, not embeddings. Compute each needed cosine ONCE from + Jina vectors at the sealed alignment version, transform to Fisher 2z, + encode place/residue (1-byte palette256 bucket in 2z + optional residue + byte — equal-information buckets). All later comparison/accumulation is + LUT + integer adds; the vector is never materialized again; hydrate + back via tanh only at the boundary that needs a float. + `helix_orient`'s own doc calls itself "the same RVQ machinery as + palette256, on S² instead of the line" — this route is the line + version. Single-measurement law applies verbatim. Synergy: a 256×256 + table over the 2z-palette codes gives O(1) pairwise compose/distance on + coded cosines — the stack's recurring structure (bgz17 palette + distance/compose tables, helix DistanceLut, attention-as-lookup). +2. **Whole vectors → Cam96 preferred, Base17-palette as the coarse tier, + NEVER helix_orient** (the category-error guard). `helix_orient`'s + codebook is the golden-spiral template on S² (2 DOF); a 1024-dim Jina + vector cannot enter it. Per-vector compression is ALREADY SHIPPED for + Jina twice, at two precision tiers: the Jina-trained **Cam96** Bible + codebook (12-axis, 96-bit code — operator 2026-08-05: "probably more + exact", plausible on axes×bits grounds: 12 subspaces vs 1 palette + index) and `ndarray::hpc::jina::codec`'s F16 2048D → Base17 (34 B) → + Palette (1 B, O(1) `JinaPalette::distance`). "Probably" stays a + HYPOTHESIS until measured: the rank-preservation gate below runs BOTH + tiers against f32 cosines on the same held-out set and reports both ρ + per byte spent — the tier choice is then a read from the table, not a + guess. +3. **Per-step plane angles → helix Signed360** (the chirality carrier + Stage A needs). Each arc step spans a 2-plane (lever × step, + Gram-Schmidt from Jina vectors); the signed angular increment in that + plane is a scalar angle → Signed360/residue, 1–3 B per step. Valid at + ANY ambient dimension because the plane is always 2D. + +**Pre-registered gate:** rank preservation — helix-2z-coded cosines must +preserve pair ranking vs f32 cosines above a pinned ρ floor on a held-out +set. The S² precedent measured Pearson 0.9917 / Spearman 0.9924 +(helix_orient header); the LINE version must be re-anchored, never assumed +from the sphere's numbers. + +> **DEFERRED pointer (operator, 2026-08-05):** `ogar-blockly` (OGAR main — +> the 256-slot `(function:value)` call palette, LaneShape carvings, one +> 512-byte node per function body) as the storage substrate for the +> elixir-syntax thinking-template recipes (256:256 rails; the alternative +> to the planner/JITson route) is FEASIBLE-ASSESSED but the crate is ~3–5 +> days from finished. Full plan entry lands when it does. Mandatory read +> before that design starts: `.claude/v3/knowledge/persona-vs-rung-ladder.md` +> (the recipe codebook binds to the 144 verbs + 34 tactics, never the +> adjective-36). Open encoding question carried: StepMask vs the 180-call +> Pairs cap. + +### 12.11 D-IGN-B PROPOSED (2026-08-05, operator) — ignition starts the REAL lenses + +**The directive:** ignition must be a *simple start* for Gadamer +Horizontverschmelzung or the four lenses — not an abstract style bit over +fixture bodies. Sequencing: PROBE-IGNITION (in build) proves the MECHANICS +(cast → scan → seal → advance, fixture qualia, §5 "no semantic claim"); +D-IGN-B is the stage behind it that swaps the fixture thought body for the +shipped instruments, reusing the probe's fleet/scan/loop scaffolding. + +**Arming vocabulary (probe-defined, sidesteps Q1 entirely):** +`z ∈ {0 = unarmed, 1 = Hegel, 2 = Nietzsche, 3 = Kant, 4 = Wittgenstein, +5 = Fusion}` — six ordinals fit the 6-bit `MetaWord::thinking()` field with +no MetaWord→PlanContext bridge needed: the CognitiveWork dispatch reads +`thinking()` directly and selects the lens body. The 36-style bridge stays +an explicitly open non-goal (design-note Q1; persona-vs-rung-ladder is the +mandatory read before any such bridge). + +**The thought bodies — all shipped, nothing invented:** +- z=1..4: the shared four-stance machinery (`lance-graph-planner/src/nars/*` + — the stance streaming + readout records extracted for + `probe_eyes_opened`), run over the owner's POPULATED rows through + cycle_driver's pluggable thought seam (D-BLW-1 precedent: the lens body + is already wired into the 5.4 seam). +- z=5: the D-BLW-3 two-projection read (Strict rung-0 vs Aware rung-5 at + the owner's sealed horizon — `blw_fusion.rs`'s proven machinery); the + observable is that owner's gap read. + +**Pre-registered observable SHAPE (numbers pinned at build time):** +- can-fire: two cohorts armed with DIFFERENT lenses over byte-identical + rows produce NON-identical readouts — the lens axis is load-bearing + (G2c's pattern lifted from reliability bits to instrument readouts); +- can-stay-silent: two cohorts armed with the SAME lens over byte-identical + rows produce bit-identical readouts; the unarmed cohort produces none; +- the ignition property itself is inherited, not re-proven: arming is a + write, discovery is a scan, work happens only after seal→apply — the + mechanics probe's G1–G11 already gate that layer. + +**Placement:** `lance-graph-supervisor/tests/` (same forced placement: +only the supervisor sees both `run_cycle` and the planner). **Build gate:** +starts only after PROBE-IGNITION passes central gates; shares its corpus +loader and MemWal provenance. + +**Not claimed:** no fusion verdict, no stance-validity claim, no +parallelism, no durability — this stage adds exactly one fact to the +tree: *a cast-and-scan ignition starts real, shipped cognition, and which +cognition is selected by the armed bits alone.* diff --git a/.claude/plans/measure-64k-axes-v1.md b/.claude/plans/measure-64k-axes-v1.md new file mode 100644 index 000000000..19e8d7b52 --- /dev/null +++ b/.claude/plans/measure-64k-axes-v1.md @@ -0,0 +1,280 @@ +# measure-64k-axes v1 — the corrected five-axis benchmark (operator-specified, 2026-08-05) + +> **Provenance:** operator-specified measurement plan, recorded before build. +> The prior measurements (the 64k ignition probe's wall times, D-BLW-4's 3.27×, +> the 24 GiB/32 MiB memory figures) mixed FIVE independent axes; this plan +> varies exactly one at a time. Prior probes stand as CORRECTNESS results — +> none of their timing/memory lines were claims — but every future +> performance/memory number comes from THIS arc. + +## The five axes (never blend two in one number again) + +| axis | meaning | +|---|---| +| Logical owner count | 65,536 independent owner identities | +| Physical SoA layout | 65,536 single-owner objects vs 64 chunks × 1,024 rows | +| WAL segment size | 1/2/4/8/32 MiB slices of ONE 32 MiB frame | +| Temporal reconstruction | post-WAL causal grouping + epistemic projection | +| Execution concurrency | overlapping thought bodies before deterministic convergence | + +## Ground truth (binding on every arm) + +- Logical population: **65,536 owner identities**. +- Canonical row: **512 bytes**; canonical frame: 65,536 × 512 B = **32 MiB**. +- Temporal: write-side ordering stays in persist_sink/freeze; `temporal.rs` + runs ONLY after the sealed WAL read — layer 1 = `local_trajectories`, + layer 2 = `deinterlace`. Never inserted into WAL preparation (that would + double-sort and move query-time work onto the write path). +- Durability: **one logical cycle → one `commit_cycle` → one fdatasync → + one `DatasetVersion`.** Segments are internal I/O slices of ONE commit, + never version-publishing units. +- **Ownership vocabulary rule:** ownership is a TYPE/BORROW property, never a + runtime operation — no arm is ever described as "claiming ownership". +- **Memory vocabulary rule:** the hot `MailboxSoA` representation and the + canonical `NodeRow512` representation NEVER share a memory claim. "32 MiB" + belongs to the canonical frame alone unless size/RSS proves otherwise. + +## Arms + +### B0 — DummyOwner cast baseline +65,536 lightweight owners carrying only `owner_id`/`phase`/`cycle` — no SoA +rows, no temporal pass, no file I/O. Measure: scan → fixed dummy thought → +`emit_bootstrap_intent` → `BatchWriter` staging → `collect_casts` → freeze. +This is the modern form of the #879 fake-owner control: owner lookup, +write-on-behalf rebind, CastId allocation, staging, collect. + +### B1 — materialise and drive 65,536 owner-exclusive SoAs +Measure separately: construction · registration into fleet · scan · thought · +cast · collect/freeze · apply. Two representations, memory NEVER blended: +- **B1a** — the current hot runtime owner: 65,536 × `MailboxSoA<4>` (actual + implementation incl. identity planes + object overhead). +- **B1b** — the canonical 512-byte row owner: 65,536 × `NodeRow512` = 32 MiB + (the persisted/storage envelope). + +Derived metrics (the point of the split): +- **runtime ownership tax** = B1a cast/scan/freeze time − B0 same-phase time. +- **hot representation overhead** = B1a peak RSS − B1b peak RSS. + +### WAL curve — W0-current vs W1-contiguous +One contiguous 32 MiB canonical frame for the physics measurement +(**W1-contiguous** = the storage/cache ceiling); the actual +`SweepSlot`/`DetachedCycleBatch` representation as **W0-current** (today's +implementation, with its allocator/pointer/BTreeMap/clone costs named as what +it measures). Segment table (MiB, not KiB): + +| rows/segment | bytes/segment | segments/64k cycle | +|---|---|---| +| 2,048 | 1 MiB | 32 | +| 4,096 | 2 MiB | 16 | +| 8,192 | 4 MiB | 8 | +| 16,384 | 8 MiB | 4 | +| 65,536 | 32 MiB | 1 | + +Per configuration: **2 unreported warm-up cycles + 16 consecutive measured +full 64k cycles** = exactly 16 × 32 MiB = **512 MiB logical payload** +(constant total work across configurations). Implementation: +`write_vectored` over the N slices then **exactly one fdatasync** +(`File::sync_data`); record the ACTUAL syscall count (partial vectored +writes loop and are counted). One `DatasetVersion` per full cycle — a +sync-every-segment variant is permitted ONLY as an explicitly labelled +durability-tax anti-pattern control, never described as the cycle contract. +**One release-mode measurement binary (`measure_wal_curve`), never 16 Rust +tests** — the test runner would overlap/reorder and contaminate cache +measurements. + +### T — temporal phases, post-WAL only +After the 16 committed cycles the history holds **65,536 owners × 16 +landings = 1,048,576 temporal rows** (every owner a real 16-step trajectory, +not a singleton). Measure separately: **T0** `scan_sealed` read · **T1** +`temporal::local_trajectories` · **T2** `temporal::deinterlace`. The +benchmark row implements BOTH `LocalCausalRow` and `DeinterlaceRow` with +`owner` = logical owner id, `cast_seq` = cycle number / monotonic stream +position, `lance_version` = sealed `DatasetVersion`. + +### L1 — ChunkedSoA<1024>[64] physical-layout control +64 physical chunks × 1,024 rows = 65,536 logical rows; 512 KiB canonical +payload per chunk; 32 MiB total. **A physical chunk is NOT an owner**: +- **L1a** — 65,536 logical owners, one per row + (`owner = chunk_index × 1024 + lane`); temporal groups by logical owner id, + never chunk id; mutation exclusivity preserved by disjoint one-row + `OwnerRowMut` views. The valid layout comparison. +- **L1b** — 64 chunks treated as 64 owners × 1,024 events: a topology + control ONLY, never evidence for the 64k-owner model. + +### EXP-KIA-A2-64K — exploratory concurrency (non-claiming) +**D-KIA-A2 is untouched** — it stays the canonical claim gate (median-of-5, +≥2×). This experiment runs now, without those thresholds, and CANNOT mark +A2 passed. Shape: 65,536 logical thought bodies → bounded worker pool +(1/2/4/8/16/physical cores; `std::thread::scope` with disjoint ranges — no +rayon dep) → **thread-local `PreparedIntent` buffers** → join → the existing +owner rebind + `BatchWriter` staging at the deterministic convergence +boundary → one seal → one WAL commit. **Never a mutex around one shared +`BatchWriter` in the compute phase** (that benchmarks lock contention). +Witness asserts: exactly 65,536 bodies executed · `max_active_workers ≥ 2` +on parallel runs · **sequential and parallel result digests identical** · +all owner bindings preserved · one sealed cycle · one WAL commit · 65,536 +applied transitions. Proves real overlap; claims nothing about A2's +threshold. + +## Measurement schema + +One CSV row per measured cycle: +`owner_shape, physical_layout, threads, segment_rows, segment_bytes, +segments_per_cycle, repeat, build_ns, scan_ns, think_ns, rebind_cast_ns, +collect_ns, freeze_ns, wal_write_ns, wal_sync_ns, temporal_layer1_ns, +temporal_layer2_ns, apply_ns, total_ns, logical_rows, logical_bytes, +sealed_transitions, applied_transitions, wal_syscalls, fsync_calls, +dataset_versions, peak_rss_bytes, minor_faults, major_faults, +context_switches, llc_misses, max_active_workers, result_digest` + +- RSS/faults/context switches from `/proc/self/status` + `/proc/self/stat` + (std-only). `llc_misses` left EMPTY with a stated reason unless + perf-counter access exists — an empty cell, never a fabricated one. +- Report per configuration: median · p95 · first measured cycle · last + measured cycle · rows/s · MiB/s · ns/owner. +- Cache-amortisation curve: `gain(C) = throughput(C)/throughput(prev) − 1`; + descriptive plateau marker = first chunk size where two consecutive + doublings improve median throughput by < 5 %. **The plateau is a measured + knee, never a PASS/KILL.** + +## Placement + gates + +`crates/lance-graph-supervisor/examples/measure_wal_curve.rs` (the supervisor +sees `run_cycle`/`persist_sink` AND the planner's `temporal.rs`), feature +`cycle-driver`, run `--release`. No new dependencies (std threads; no rayon, +no libc). Central gates: fmt · clippy (0 attributable) · one full release +run producing the CSV + the four answers: + +1. What does ownership cost? (B1a − B0, per phase) +2. What does physical layout cost? (B1a vs L1a at equal logical population) +3. Where does WAL amortisation plateau? (the knee on the W1 curve, W0 beside it) +4. What does genuine parallel thought execution add before the deterministic + seal? (EXP-KIA-A2-64K, digest-identical) + + +--- + +# MEASURED RESULTS (2026-08-05, release-mode, 5 runs of the same binary) + +Host: 4 cores, `/` ~90 % full, shared VM. Binary: +`crates/lance-graph-supervisor/examples/measure_wal_curve.rs` (release, gated: +fmt + clippy 0-attributable). CSV: 179 rows/run, the 33-column schema. + +## Stable arms — reported as findings + +**Answer 1 — what the CURRENT ownership IMPLEMENTATION contributes under THIS +workload** (B1a `MailboxSoA<4>` − B0 DummyOwner, +median of 3, consistent across all runs): scan **+1.0 ms**, cast/rebind +**+11.5 ms**, freeze **+0.6 ms** over 65,536 owners. + +> **Wording is load-bearing (operator, 2026-08-05).** Write *"the current +> ownership implementation contributes +13 ms under this workload"* — never +> *"ownership costs +13 ms"*. The second phrasing reads as an inherent +> property of ownership-as-a-concept; what was measured is one implementation +> (`MailboxSoA<4>`, HashMap fleet, `Vec` payloads) on one workload on one +> host. A different representation could move it substantially — and B1a−B0 is +> exactly the instrument that would show it. B1a additionally pays two +phases B0 does not have at all: a real per-owner think (**8.6 ms**) and a real +apply (**23.5 ms**). So the *marginal* cost of a real owner over a dummy on the +shared phases is ~13 ms per 64k cycle; the phases only a real owner has are +~32 ms. + +**Answer 1b — what the hot representation costs in memory** (MEASURED VmRSS +delta vs the EXACT canonical size): B1a **+52.1 MiB** measured for 65,536 × +`MailboxSoA<4>`; canonical envelope **32.0 MiB** (65,536 × 512 B, exact by +construction) ⇒ **+20.1 MiB, +63 % overhead**. *Methodology correction made +during the run:* a first revision differenced two `VmHWM` values and printed a +NEGATIVE overhead — `VmHWM` is process-monotonic, so the subtraction returned +the same historical maximum twice. Retracted, not reported. B1b's own +in-process VmRSS delta reads **+0 B** because the allocator satisfies its +32 MiB from pages B1a already returned — which is *why* the line uses the exact +canonical size rather than a second measurement. + +**Answer 2 — what physical layout costs** (L1a 64 × `MailboxSoA<1024>` − B1a +65,536 × `MailboxSoA<4>`, EQUAL 65,536 logical owners): build **−171 ms**, +cast/rebind **−14.7 ms**, freeze **−1.8 ms** — i.e. the chunked layout is +**faster on every comparable phase**, dominated by build (one allocation per +1,024 owners instead of per owner). Apply is deliberately unmeasured for L1a +(Deviation D2): `phase()` is scoped to a `MailboxSoA` instance, not to a +row, so there is no honest per-logical-owner apply across 64 chunks — +fabricating one would misrepresent 1,023 of every 1,024 owners. +**L1b control fires as designed:** treating the 64 chunks AS owners collapses +to **65,472 of 65,536 HELD** — the mislabelling is observable, so L1a's +logical-owner preservation is not an assumption. + +> **Known confound, not yet decomposed (operator, 2026-08-05).** The −171 ms +> build delta is a SUM of at least four distinct phenomena — fewer allocation +> calls, better locality, allocator arena reuse, and fewer cache misses — and +> this arm cannot tell them apart. It is reported as "the chunked layout is +> faster to build", never as "allocation is the cause". Decomposition is +> designed as the **A-arm** in v3 below. + +**Answer 4 — EXP-KIA-A2-64K** (exploratory, NON-CLAIMING; D-KIA-A2 untouched): +compute phase **21–27 ms at 1 worker → 6.3–7.5 ms at 16 workers** (≈3.2–3.5× +on 4 physical cores), `max_active_workers` observed 4–6, and — the load-bearing +half — **sequential and parallel sealed-cycle digests are IDENTICAL +(`248c6e7b991d3b25`) at every worker count in every run.** Real overlap with +byte-identical convergence. This does NOT pass D-KIA-A2, whose median-of-5 ≥2× +protocol remains its own gate. + +**Temporal (post-WAL only):** T1 `local_trajectories` **78–86 ms** and T2 +`deinterlace` **7.3–8.8 ms** over 1,048,576 rows (65,536 owners × 16 sealed +versions) — both stable across runs. T0 `scan_sealed` **43–135 ms** is +cache-sensitive and reported as such. The rung gate admits **524,288 of +1,048,576** rows (exactly half), so T2 is doing real admission work. + +## Answer 3 — WAL amortisation plateau: NOT REPRODUCIBLE ON THIS HOST + +**No knee is claimed.** Across five runs of the *same binary* the named knee +moved between 4 MiB and 32 MiB, and per-config throughput swung +**109 → 785 MiB/s at identical settings** (a 6× spread); one config showed a +p95/median spread of 24× *within* a single run. The write phase is bimodal — +either ~110–135 MiB/s or ~550–785 MiB/s — which is page-cache/dirty-writeback +state flipping, not segment size. Naming a knee from this would be fabricated +precision, exactly what this plan exists to prevent. + +Two things were built rather than asserted around it: +1. **The comparability assert** — both arms now count the bytes actually handed + to the kernel and assert each cycle moves exactly the 32 MiB canonical frame. + *A first revision computed MiB/s from the ASSUMED frame size while + discarding `write_vectored`'s real byte count — an assumption presented as a + measurement.* The assert passes, so W0-vs-W1 IS a like-for-like comparison. +2. **A stability guard with both halves** — it SUPPRESSES the knee when any + config's p95/median spread exceeds 3× (fired on the unstable runs) and + REPORTS one when every config is tight (stayed silent at worst-spread 1.4×). + Intra-run only; the cross-run variance above is what actually voids the + reading. + +**To measure it properly:** a quiet host with disk headroom, O_DIRECT or a +per-config cache barrier, and enough cycles that p95/median converges. + +## Notes carried forward +- B1a's first repeat is a **cold-allocator outlier** (4.5–4.9 s vs 280–330 ms + steady) — reported, never medianed away. +- `llc_misses` is emitted EMPTY (no perf-counter access), never fabricated. +- The scratch-file lifecycle was corrected mid-run: each config's 576 MiB WAL + file is reclaimed immediately (ten live files needed ~5.8 GiB and hit ENOSPC). + + +## DEFERRED — re-measure on lance 9 + lancedb 0.36 (operator, 2026-08-05) + +**Current pins** (`crates/lance-graph/Cargo.toml:38-41`): `lance = "=7.0.0"`, +`lance-linalg = "=7.0.0"`, `lance-namespace = "=7.0.0"`, +`lancedb = "=0.30.0"` — the exact-pin lockstep from PR #445. + +Operator note: **lance 9 + lancedb 0.36 are expected to reduce the overhead** +this arc measured. Explicitly deferred ("we can do that later"), so nothing in +the results above is provisional on it — but the numbers are now the BEFORE +side of that comparison, which is exactly what makes the upgrade measurable: + +- Re-run the same binary, same host discipline, after the bump and diff the + CSVs arm-by-arm. The arms most likely to move are the storage/serialization + ones (W0-current's `SweepSlot`/BTreeMap path, T0 `scan_sealed`), not the + pure-owner arms (B0/B1a/L1a), which touch no lance code — **if B0/B1a move, + that is a signal something else changed, not a lance win.** +- Constraints that still bind: P0 forks-only (AdaWorldAPI fork of every + forked crate, never crates.io upstream), and the lance-family lockstep + (lancedb's transitive requirement pins the lance version — bump them + together or the patch silently does not apply). +- The WAL knee stays unmeasurable until the host issue is fixed regardless of + the lance version — a faster library does not quiet a noisy disk. diff --git a/.claude/plans/measure-64k-axes-v2.md b/.claude/plans/measure-64k-axes-v2.md new file mode 100644 index 000000000..445d8573f --- /dev/null +++ b/.claude/plans/measure-64k-axes-v2.md @@ -0,0 +1,277 @@ +# measure-64k-axes v2 — rolling epoch closure (operator-specified, 2026-08-05) + +> **Supersedes v1's EXECUTION MODEL; keeps v1's arms as baseline + +> instrumentation.** v1's global-barrier shape survives as **Stage A0** (the +> comparison baseline) and its CSV/metrics harness is the shared +> instrumentation. The operator's correction: the 64k boundary stays the +> ACCOUNTING and VERSION boundary — it stops being the turnstile where every +> worker, cache line, encryption frame and disk write queues at once. A 64k +> barrier makes every car arrive at one tollbooth and calls the queue +> "amortisation"; the rolling model gives each neighbourhood an on-ramp, +> aligns lanes during the 200 ms closure window, and publishes the city-wide +> state only after every car is through, vetoed, parked, or explicitly +> delayed. + +## The model, one line + +**64k logical owners → rolling chunk closure → one published epoch.** + +``` +owner decision + → provisional write-order registration + → rolling Morton-ordered chunk append + → 64k epoch manifest / DatasetVersion publication +``` + +Three terms, never conflated: + +| level | meaning | +|---|---| +| Owner registration | one owner produced, vetoed, held or deferred its result | +| Chunk append | a physically aligned, encrypted segment is WRITTEN but not globally visible | +| Epoch seal | all 65,536 owners accounted; ONE manifest publishes Vn+1 | + +**A chunk append is NOT a DatasetVersion. The epoch manifest is.** The +existing one-cycle/one-version logical contract is unchanged; what evolves is +the physical write mechanism: *one logical WAL transaction and one published +version, composed of multiple physical aligned appends.* + +## D1 — identity vs write order (DECIDED: keep the 64k owners; chunk baton) + +`MailboxId` keeps exactly ONE job: logical identity. Storage locality and +completion order move to a separate key: + +```rust +struct WriteOrderKey { morton_chunk: u32, lane: u16, cycle_position: u64 } +``` + +Order: version outside → Morton chunk inside one version → owner lane inside +one chunk. Recovery identity stays `MailboxId` + monotonic stream position. + +**Baton decision: a CHUNK baton, never an owner baton** (65,536 hand-offs = +a railway signal per wheel). The baton lives in the convergence layer; +workers never wait on it while thinking — they publish into assigned result +slots. Encryption may FINISH out of order; the baton makes APPENDS +monotonic: finished-early chunks park in their slot until the baton reaches +them. + +## D2 — the Morton cascade (disk-aligned levels; two independent knobs) + +``` +L0 aligned disk page 4 KiB / 8 KiB / 16 KiB +L1 crypto + WAL segment 1 / 2 / 4 / 8 MiB +L2 logical 64k epoch 32 MiB canonical payload +L3 temporal series 16 epochs in DatasetVersion order +``` + +Page alignment (L0) and WAL segment size (L1) are INDEPENDENT knobs — a +1 MiB segment is 256×4 KiB = 128×8 KiB = 64×16 KiB pages. **4 KiB is never +an encryption chunk except as the deliberately pathological end** (tags, +nonces, syscall setup dominate). Within an epoch: chunks in Morton order, +owner lanes sorted inside each chunk. Across epochs: version order. + +**temporal.rs gains a verified ordered-chunk fast path** — +`local_trajectories_from_ordered_chunks`: validate headers (version +monotonic, chunk sequence monotonic, owner lanes monotonic, stream positions +monotonic) then APPEND — no regroup, no sort. The generic fallback for +arbitrary interleaved history is retained; **generic and fast paths must +produce identical trajectory digests.** Chunks therefore fall out of the +grid cascade already sorted, continuously written, aligned with disk cache +and the 4k/8k/16k device units. + +## D3 — the Libet closure window (200 ms as rolling veto budget, never a global sleep) + +Per owner: thought finishes → provisional registration → veto deadline → +result REPLACEABLE until deadline → deadline expires → slot immutable. +Per chunk: all owners resolved OR the chunk's 200 ms local deadline reached +→ unresolved owners become Held/Deferred → chunk freezes → encrypt → append. +Earlier chunks close while later chunks still compute — the system waits +only for each chunk's bounded local window, never for all 64k thoughts. + +```rust +enum ClosureState { Open, Registered, Vetoed, Held, Deferred, Frozen, Appended } +``` + +The semantic boundary: **before Frozen the free-will veto may replace or +cancel; after Frozen no mutation — correction is a NEW event in the next +epoch.** Registration is not commitment; encrypted bytes never shapeshift +under their tag. This amortises the free-will veto with write-order +registration instead of a barrier. + +## D4 — what "64k complete" means (accounting, not completion) + +NOT "all 65,536 thought bodies finished." IT IS: every owner identity has +exactly ONE accounting outcome — committed / vetoed / held / deferred / +absorbed. Only COMMITTED owners enter the sparse transition set (the #879 +rule: a version is never permission to advance the whole fleet). + +```rust +struct EpochManifest { + owner_population: u32, // 65_536 + committed: u32, vetoed: u32, held: u32, deferred: u32, absorbed: u32, + chunk_count: u32, chunk_hash_root: [u8; 32], +} +// INVARIANT: committed+vetoed+held+deferred+absorbed == 65_536 +``` + +## ⊘ D5 AMENDED (operator sanity-check, 2026-08-05 same day) — crypto is NOT a seal concern; removed from the seal benchmark entirely + +**Verified from source before recording:** zero cryptographic operations +exist anywhere in the seal path (`batch_writer.rs`, `persist_sink.rs`, +`cycle_driver.rs` — the one grep hit for "nonce" is the `FnOnce` trait +name). The seal is deterministic ordering + cycle closure + batching + +version publication + one-WAL-append amortisation. **It was never a +cryptographic operation, and the earlier AEAD-in-the-seal framing +conflated two orthogonal layers.** + +The corrected split — three independent curves, measured in this order: + +``` +A pure seal: thought → collect → seal → serialize (no crypto) +B seal + persistence: seal → WAL → fsync (no crypto) +C encryption: evaluated LATER as a SEPARATE LAYER — and only + where encryption actually belongs (likely the + replication/transport boundary, NOT the seal path) +``` + +Without this split it is impossible to attribute a bottleneck among +sorting / cache locality / serialization / WAL / encryption / fsync. + +Consequences: +- Every Stage-A and Stage-B measurement in this plan is **crypto-free**. +- The former "Stage B encryption arms" are DEFERRED to a future + layer-placement decision ("where does encryption live?" precedes "what + does it cost?"). The per-chunk AEAD design below is RETAINED as the + recorded design for whenever that layer is evaluated — the nonce/AAD + derivation rule and the crash contract remain correct for that future + layer. +- The **AEADs-fork dependency decision is no longer blocking anything.** +- The Libet/rolling-closure optimization (D3) is purely a + synchronization-stall reduction — **it has nothing to do with + encryption** and the scheduling model is settled crypto-free. +- The crash contract (manifest-less chunks invisible) is an ORDERING + property and stays in the crypto-free benchmark. + +## D5-DEFERRED — encryption as a separate layer (design retained for later) + +The expensive shape (collect 32 MiB → serialize → encrypt serially → append +→ sync) is one latency iceberg. Instead: resolved chunk → freeze → encrypt +INDEPENDENTLY on a bounded pool → append continuously in baton order → +final manifest sync. Per-chunk AEAD context with nonce/AAD derived from +**epoch id + dataset base version + chunk sequence + retry generation + +payload length — NEVER from chunk_id alone** (the same chunk number recurs +every epoch). The seal that got unbearably slow at 64k+ was the serial +whole-epoch shape; the rolling shape overlaps CPU encryption with disk +writing while the baton keeps appends deterministic. + +**Crash contract:** physical chunks may land before the epoch publishes; +without the final manifest/footer they are an abandoned incomplete epoch — +invisible, reclaimed or ignored by recovery. + +**Dependency decision (OPERATOR):** the real AEAD for Stage B comes from +the AdaWorldAPI fork per the P0 forks-only rule (the `AEADs` fork is in +repo scope; wiring = path/git dep decision). Stage B is SEQUENCED BEHIND +this decision; Stages A and C need no new dependency. + +## D6 — where grind happens (measured separately; each its own curve) + +- **CPU/memory:** 65,536 HashMap lookups; per-owner Vec allocation; clone + during freeze; O(n log n) stable sort; BTreeMap coalescing; random access + across 65,536 objects; TLB pressure; page faults; allocator + fragmentation; NUMA migration. +- **Synchronisation:** shared BatchWriter mutex; global ready queue; one + atomic per tiny op; false sharing in ready flags; baton spinning; load + imbalance inside a chunk; one slow owner holding a chunk open. +- **Encryption:** serial AEAD over the epoch; tiny records; nonce/tag + construction; copies into contiguous buffers; no hardware acceleration; + one encryption thread feeding many writers. +- **Storage:** too many write syscalls; sync per chunk; unaligned trailing + writes; page-cache eviction; dirty-page throttling; journal pressure; + device queue saturation; write amplification. +- **Temporal:** rebuilding a BTreeMap every read; regrouping all 1,048,576 + rows; sorting already-ordered trajectories; cloning rows; reading every + historical chunk when one owner range suffices. + +## D7 — the 16-cycle interpretation (amortise or fall apart) + +2 warm-ups + 16 measured epochs; classify the curve: + +| shape | signature | +|---|---| +| Warm-up | cycles 1–3 improve, 4–16 flatten | +| Healthy amortisation | initial improvement, stable throughput, bounded baton lag, stable RSS/dirty pages | +| Cache turnover | early plateau; 8–16 modestly slower; LLC misses/faults rise but backlog bounded | +| Collapse | chunk-ready backlog grows per cycle; baton lag grows; epoch time exceeds 200 ms increasingly; encryption/dirty queue never drains | + +Per-cycle queue metrics: ready chunks · encrypted-but-waiting chunks · +appended chunks · max baton lag · deadline-held owners · bytes pending +encryption · dirty bytes · epoch wall time. **The collapse signal is +`backlog_end(n) − backlog_end(n−1)` staying positive after warm-up** — +accumulating debt, not amortising work. Sixteen cycles detect the onset; +they are not an endurance proof. + +## Stages (no Cartesian explosion) + +- **Stage A — layout, no encryption:** A0 global 64k barrier natural order + (= v1's shape, the baseline; IN BUILD) · A1 rolling chunks natural order · + A2 rolling chunks Morton order — × page alignment {4,8,16 KiB} × WAL + segment {1,2,4,8 MiB} × 16 cycles. Locates the storage/cache knee. +- **Stage B — seal + persistence (crypto-free, per the D5 amendment):** + WAL + fsync on the best two Stage-A layouts — isolates storage cost from + seal cost. (The former encryption arms are DEFERRED to the separate + encryption-layer evaluation; the AEADs-fork dep decision no longer + gates anything.) +- **Stage C — temporal recovery after 16 epochs:** generic + `local_trajectories` · ordered Morton-chunk fast path · single-owner + range lookup · full-fleet reconstruction · layer-2 epistemic deinterlace. + Identical digests generic-vs-fast REQUIRED. + +## Concurrency governance + +**D-KIA-A2 is FROZEN unchanged** — the later publication gate. Operator +override recorded: **EXP-KIA-A2-ROLLING-CLOSURE** (non-claiming, +exploratory; explores the real concurrent topology and chooses the +benchmark shape; cannot pass or fail A2). Shape: owner thought bodies with +per-thread result buffers → write-order registration slots → chunk-local +closure → parallel encryption → ordered baton append → epoch manifest. +**No shared BatchWriter in the thought loop**; the convergence thread +consumes immutable `PreparedIntent { owner, closure, morton_key, +intended_move, payload_ref }` values. Asserts: all 65,536 accounted · +owner binding preserved · generic and Morton temporal digests identical · +one published DatasetVersion · incomplete chunks invisible · parallel and +sequential semantic result digests match. + +## Sequencing + +1. v1 lane lands → central release run → **A0 baseline numbers** (also + validates the shared instrumentation). +2. Next lane: rolling closure + Morton (A1/A2) + queue metrics + manifest + + temporal fast path (Stage C machinery) on the v1 harness. +3. Stage B (seal + persistence, crypto-free) on the settled layout. +4. EXP-KIA-A2-ROLLING-CLOSURE last, on the settled layout. +5. Encryption: a SEPARATE later arc, starting with the layer-placement + decision (replication/transport vs storage), using the retained + D5-DEFERRED design. + +--- + +## ⊘ v4 cross-note (2026-08-05, append-only) + +**v4 (`measure-64k-axes-v4.md`, the hot version window) composes with this +model one level UP — it does not supersede it.** This plan rolls chunks +*within* one cycle toward one epoch manifest; v4 batches sealed *epochs* +across the durable flush (publication clock decoupled from persistence +clock). Two clarifications a future reader needs: + +- **The "one-cycle/one-version logical contract is unchanged" pin (§ above) + SURVIVES under v4's recommended fork** (barrier flush: each seal still + performs its own unsynced Lance commit → one real `DatasetVersion` per + cycle; only the fdatasync is batched, 1 per K cycles). It would break + under the rejected version-multiplexing fork — which is exactly half the + reason that fork was rejected (v4 §3). +- **Two different 200 ms windows, never conflated:** D3's 200 ms is the + *intra-cycle chunk-closure* deadline (rolling veto budget); v4's 200 ms + is the *cross-cycle flush* deadline (Nagle-shaped barrier trigger). The + Libet veto lives HERE, pre-seal (`ClosureState::Vetoed`) — v4 §H-3 pins + that a published cycle is irrevocable and its flush queue is never a veto + surface. diff --git a/.claude/plans/measure-64k-axes-v3.md b/.claude/plans/measure-64k-axes-v3.md new file mode 100644 index 000000000..22a8527a6 --- /dev/null +++ b/.claude/plans/measure-64k-axes-v3.md @@ -0,0 +1,245 @@ +# measure-64k-axes v3 — the three arms Stage A0 earned (operator-directed, 2026-08-05) + +> **Reads with:** v1 (arms + MEASURED RESULTS), v2 (the rolling-epoch-closure +> model). This file adds ONLY what the operator's review of the A0 results +> designated as next. Nothing here is measured. + +## The takeaway that orders this file + +A0 found the expensive part is **not** 64k owners and the unstable part is +**not** sealing — the instability lives in +`filesystem → page cache → writeback → allocator interaction`. So optimisation +effort belongs in **temporal chunk scheduling, Morton ordering, rolling +closure, and batch geometry** rather than in redesigning the ownership model. +**That is a hypothesis A0 makes worth testing, not a finding A0 proved.** The +three arms below are how it gets tested. + +Sequencing (operator-directed): **M-arm and O-arm first, rolling closure +before any encryption, A-arm when the layout question comes back.** + +--- + +## M-arm — MORTON REORDER: the experiment that answers the architectural question + +A0 measured `logical order → seal → WAL`. It never measured the pipeline the +architecture actually proposes: + +``` +A0 (measured): logical order → seal → WAL +M-arm (new): logical order → MORTON REORDER → seal → WAL +``` + +**Design.** Same 65,536 owners, same canonical frame, same host discipline; +the ONLY difference is a Morton reorder inserted before the seal +(`WriteOrderKey { morton_chunk, lane, cycle_position }` per v2 D1 — identity +stays on `MailboxId`). Measure the reorder as its OWN phase +(`morton_reorder_ns`, a new CSV column) so it is never folded into seal or +write time. + +**Pre-registered readings, both directions named in advance:** +- **Reorder cost** — the reorder phase in isolation. It is a cost until proven + otherwise; it must be paid for downstream or the arm is a KILL for Morton. +- **Downstream gain** — seal + write + temporal T1, ordered vs unordered. +- **The verdict is the SUM**, never the gain alone: `Δtotal = reorder_cost − + (seal + write + T1 savings)`. Morton wins only if Δtotal < 0. +- **The temporal fast path is where the real gain should live** (v2 D2): with + chunk headers proving version/chunk/lane/stream monotonicity, T1 becomes + validate-and-append instead of regroup-and-sort. A0 measured T1 at + **78–86 ms** over 1,048,576 rows — that is the number the fast path must + beat, and it is stable enough across runs to be a real target. +- **Digest identity is mandatory**: ordered and unordered trajectories must + produce identical digests, or the reorder changed semantics rather than + layout, and the arm is void regardless of speed. + +**Stability inheritance:** the A0 spread guard applies unchanged. If the WAL +phase is unstable in an M-arm run, the reorder's downstream half is +unreadable and only the reorder COST may be reported. + +--- + +## O-arm — ORDERING SOURCE: where does the ordering actually come from? + +The hypothesis that has been running through months of discussion — +*`temporal.rs` already provides the ordering* — has never been isolated. It +gets its own measurement: + +``` +O-A: cast → seal → WAL → temporal replay (today's pipeline) +O-B: cast → temporal replay → seal → WAL (ordering sourced first) +``` + +**What makes this decisive rather than merely interesting:** if O-B produces +**byte-identical trajectories** to O-A, then the seal's ordering work is +*redundant with* temporal's, and the seal can be re-scoped to closure + +batching + version publication alone (which is what A0 already showed it +fundamentally is — the seal was never cryptographic, and this asks whether it +must be ordering either). If the trajectories DIFFER, the hypothesis is dead +and the seal's ordering is load-bearing — equally valuable, and cheaper to +learn now than after a redesign. + +**Pre-registered:** +- Primary observable: **digest identity** O-A vs O-B (a boolean, decided + before any timing is looked at — timing must not be able to rescue a + semantic difference). +- Secondary: per-phase time for both pipelines. +- **Kill condition:** if O-B cannot be constructed without duplicating + ordering work that O-A does once, say so and report the arm as + not-constructible rather than reporting a rigged comparison. +- **Firewall:** O-B must not consult the sealed stream to build its own order + (that would be O-A wearing a disguise). Enforced by a compile-time self-scan + in the probe, the pattern the shipped probes already use. + +--- + +## A-arm — ALLOCATOR vs ARCHITECTURE (the confound A0 names but cannot split) + +A0's L1a build delta of **−171 ms** is a SUM of at least four phenomena: +fewer allocation calls · better locality · allocator arena reuse · fewer cache +misses. Reporting it as one number is honest only while it is *labelled* as a +sum, which v1 now does. + +**Decomposition design (deferred until the layout question returns):** +- **allocation count** — instrument an allocation counter (a counting global + allocator behind a probe-local feature) and report calls, not just time. +- **arena reuse** — run each layout FIRST in a fresh process (the reuse + A0 hit is why B1b's in-process RSS delta read 0). Separate processes, one + arm each. +- **locality / cache misses** — needs perf counters; A0 emits `llc_misses` + EMPTY by design rather than fabricating it. This sub-arm is BLOCKED on + perf-counter access and must stay blocked rather than be estimated. +- **pure allocation cost** — a control that allocates the same shapes and does + nothing else. + +Until those run, the standing wording holds: *the chunked layout is faster to +build*, never *allocation is the cause*. + +--- + +## What does NOT change + +- Encryption stays out (v2 ⊘ D5): rolling closure is measured before any + crypto, and the seal path remains verified crypto-free. +- The WAL knee stays unclaimed until a quiet host with headroom + O_DIRECT or + a per-config cache barrier exists. No arm here weakens that. +- D-KIA-A2 stays frozen; EXP-KIA-A2-ROLLING-CLOSURE remains the non-claiming + exploratory override. +- Every number keeps implementation-scoped wording: *this implementation, + this workload, this host*. + + +--- + +# MEASURED RESULTS — M-arm and O-arm (2026-08-05, release, 16 GiB free host) + +Both arms produced **negative** results. Both were pre-registered as +two-sided, so both are findings rather than failures. + +## M-arm — MORTON DOES NOT WIN under this workload/host + +Digest identity **MATCHED** (`68128e3662df105c` both pipelines), so the +comparison is valid — the reorder changed layout, not semantics. + +| phase | natural | morton | +|---|---|---| +| reorder | — | **9.4 ms** | +| seal | 11.6 ms | 15.6 ms | +| write | 257.3 ms | 254.3 ms | +| sync | 48.4 ms | 54.5 ms | +| T1 | 320.9 ms | 339.7 ms | + +**SUM verdict (the pre-registered criterion): reorder_cost 9.4 ms, +downstream savings −25.8 ms (Morton is SLOWER downstream), Δtotal += +35.2 ms ⇒ MORTON LOSES.** The ordered-chunk fast path +(350.9 ms) was also **slower than the generic path** (339.7 ms) while +producing an identical digest — so validate-and-append did not beat +regroup-and-sort here either. + +> **⚠ CAVEAT THAT BLOCKS ONE COMPARISON (found by this run, not papered +> over).** The M-arm's T1 baseline is **320–340 ms**, roughly **4× A0's +> 78–86 ms** over the same nominal 1,048,576 rows. Until that gap is +> explained, the fast-path number **must NOT be compared against A0's +> 78–86 ms** — the two T1s are not commensurable. The natural-vs-Morton +> comparison IS valid (same harness, same run, same row count); only the +> cross-run comparison to A0 is void. Likely suspects: the M-arm's +> `BenchRow` materialisation inside the timed region, and the +> `stream_position` relabeling the harness needs because `freeze` always +> sorts by that field. **This is an open measurement defect, not a +> result.** + +## O-arm — DIVERGED: the seal's ordering is LOAD-BEARING + +Primary observable, computed and printed **before any timing** as +pre-registered: **O-A `64565f362db2e4a5` ≠ O-B `3e71c2aa7be8e325` — +DIVERGED.** + +**Verdict:** ordering sourced from temporal replay does NOT reproduce the +seal's ordering. Under this construction the seal's ordering is +**load-bearing and cannot be re-scoped away** — which retires, for this +construction, the long-standing "temporal.rs already provides the +ordering" hypothesis. **Honest scope: this falsifies the hypothesis FOR +THIS O-B CONSTRUCTION; it does not prove no construction could match.** + +**Firewall held** (after a real fix — see below): the region contains no +`scan_sealed` and no sealed-store read, and `local_trajectories` IS +present, so O-B's scan mechanism is proven live rather than absent. + +**Kill-condition check: CONSTRUCTIBLE.** O-B's derivation +(`local_trajectories` grouping via BTreeMap) is a different code path +from O-A's seal-side `order_cycle_stably` Vec sort — not a disguised +O-A. Reported honestly: at one row per owner per cycle the two are doing +comparable asymptotic work, and the redundancy the plan asks about is +SEMANTIC, not code-sharing. + +Timing (secondary): O-A cast 55.5 / seal 20.1 / commit 13.2 / T1 397.1 ms; +O-B cast 72.4 / **order_derive 64.9** / seal 34.9 / commit 5.1 / +T1 523.8 ms. O-B is slower on every phase except commit. + +## Three defects caught at the gate (not shipped) + +1. **The firewall fired on its own comment.** The self-scan matched the + token inside a *comment* describing the check — a guard tripping on + documentation tests the documentation, not the code. Fixed by + stripping line comments before scanning, **plus a positive control** + asserting the detector still finds a real call (otherwise a silent + guard and a broken guard are indistinguishable). +2. **T1 read 18 cycles where the spec says 16.** Both arms scanned the + unfiltered history, including the two warm-ups (1,179,648 rows vs + 1,048,576). Scoped to the measured window via + `scan_sealed(Some(WARMUP))` — an unscoped T1 is not comparable to + anything. +3. **A pre-registered outcome was coded as a panic.** O-arm divergence + `assert!`-ed, which turns a designed falsification into a crash and + discards every number after it. Both branches now report. + +--- + +# WHAT THE O-ARM ACTUALLY OPENED (operator-framed, 2026-08-05) + +The O-arm **failed semantically before it failed on performance** — which makes +its timing numbers almost irrelevant to the decision. Three separate questions +came out of it, and conflating them is the trap: + +1. **Presorting cannot replace the seal.** Measured (digest divergence). The + seal's ordering is load-bearing under this construction. +2. **Presorting is not intrinsically bad.** Not measured either way — the + M-arm's Morton loss is about *this* reorder on *this* host, not about + ordering-before-seal as a family. +3. **`temporal.rs` probably remains the ordering authority for READS.** Not + contradicted by anything here: O-arm measured *sourcing the write-side order* + from replay, which is a different job. + +**Standing position (operator):** keep `temporal.rs` as the authoritative +TEMPORAL model and the seal as the authoritative ORDERING model, treating the +gap as an **explicit research question** rather than assuming one replaces the +other. + +**The next question is therefore NOT "can we remove temporal ordering?" but +"what information does the seal compute that `temporal.rs` does not currently +encode?"** Answered from the shipped source in +`.claude/knowledge/seal-vs-temporal-ordering-information.md` — four items +(cross-owner TOTAL order vs a partial one · arrival as an ordering input, +durably recorded nowhere else · the per-row coalescing FOLD, and `temporal.rs` +has no row concept · the cohort boundary + read horizon `CycleFrame{cycle, +base_version}`) — with three pre-registered probes and an explicit scope fence +on how far the divergence may be read. Board entry: +`E-SEAL-AND-TEMPORAL-ARE-DIFFERENT-OBJECTS-1`. diff --git a/.claude/plans/measure-64k-axes-v4.md b/.claude/plans/measure-64k-axes-v4.md new file mode 100644 index 000000000..f6f682bb5 --- /dev/null +++ b/.claude/plans/measure-64k-axes-v4.md @@ -0,0 +1,286 @@ +# measure-64k-axes v4 — the hot version window (operator-directed, 2026-08-05) + +> **Reads with:** v2 (rolling epoch closure — COMPOSES, does not supersede), +> v3 (M-arm/O-arm results — this design survives both), and +> `.claude/knowledge/seal-vs-temporal-ordering-information.md` (whose +> properties 2 and 4 carry a dated caveat added by this plan). Nothing here is +> built or measured; the design was **panel-hardened before banking** (one +> canon-conflict sweep + one adversarial refuter, 2026-08-05) and the panel +> INVERTED the initial fork choice — recorded honestly in §4. + +## 0. The model, one line + +**Decouple the cognition clock from the persistence clock: publish every +sealed cycle to RAM immediately; make durability a batched background +barrier.** + +``` +think → seal cycle → PUBLISH (RAM, visible now) ← cognition clock +think → seal cycle → PUBLISH +think → seal cycle → PUBLISH + ... +background: FLUSH [n..n+K] as one vertical batch ← persistence clock + + ONE sync barrier ⇒ durable_head = n+K +``` + +The batch is **vertical — batching time, not owners**: the unit of the flush +is a run of consecutive sealed cycle images, which for Arrow/Lance is the +natural shape (appending immutable batches). Two watermarks replace one: + +- `published_head` — the newest sealed cycle cognition may read (RAM). +- `durable_head` — the newest cycle that survives a crash (disk, advanced + only at sync barriers). + +The crash window is `(durable_head, published_head]`. + +**Authorities do not move.** `temporal.rs` stays the chronology authority; +Lance stays the durable authority; the seal stays the ordering authority +(v3's O-arm result + `E-SEAL-AND-TEMPORAL-ARE-DIFFERENT-OBJECTS-1`). The hot +window is a *residency* statement, not a new authority: "versions +`durable_head+1 ..= published_head` are resident in RAM." + +**Naming (canon-checked).** This is **the MailboxSoA fleet's hot version +window over sealed cycles** — NOT "VSA speaks Lance." Per +`E-MARKOV-TEMPORAL-STREAM-1` the VSA carrier is demoted to its +I-VSA-IDENTITIES niche and the in-RAM substrate is the `MailboxSoA` owner +fleet behind `MailboxFleet` (`cognitive-shader-driver/src/mailbox_soa.rs:58`, +`lance-graph-supervisor/src/cycle_driver.rs:179`). Any doc that says "VSA +speaks Lance versioning" is resurrecting the deprecated carrier framing; +the sweep confirmed "SoA horizon" has no canon citation either — use the +fleet name. + +## 1. Why this beats "another cache layer" + +A cache has **invalidation**; the hot window has only **eviction**. Because +sealed cycles are immutable and append-only, the window is not a copy of the +truth kept coherent with the truth — it *is* the head of the log, retained in +RAM past its durability point. No coherence protocol, no staleness, no +invalidation storm. `temporal.at(v)` becomes location-transparent: RAM if +`v > durable_head` and resident, Lance otherwise — the caller never learns +which. + +And it aims at exactly what A0 measured: the expensive, **unstable** part of +the pipeline is `filesystem → page cache → writeback → allocator` +interaction — the sync — not sealing and not ownership. Batching K syncs into +one barrier amortizes precisely the phase A0 could not stabilize, while the +per-cycle seal (11–20 ms, stable across every arm) keeps running untouched. + +## 2. What survives untouched (the load-bearing list) + +- **The seal, per cycle, unchanged.** `freeze` still computes all four + properties per cycle: cross-owner total order, arrival tie-breaking, the + per-row fold, the cohort + `base_version` read horizon. The panel verdict: + properties 1 and 3 are computed inside `freeze` and are not touched; + property 4's cohort re-anchors on the SEAL event (see §5 caveat); property + 2 gains a durability caveat (see §5). +- **E-64K-1TO1's "ONE deterministic seal boundary per cycle"** — verdict + COMPATIBLE: the operator order pins the *seal* boundary, not the physical + append. Only the durability event moves. +- **v3's M-arm and O-arm results** — orthogonal. This design changes the + durability *cadence*, not the per-cycle ordering (O-arm) and not the + intra-cycle layout (M-arm). It survives both negative results. +- **v2's rolling epoch closure** — COMPOSES, one level down: v2 rolls chunks + *within* one cycle toward one epoch manifest; v4 batches sealed *epochs* + across the durable flush. v2's 200 ms is the intra-cycle chunk-closure + deadline; v4's 200 ms is the cross-cycle flush deadline. **Two different + 200 ms windows — never conflate them.** + +## 3. The fork the panel decided — barrier flush over version multiplexing + +Lance mints exactly ONE dataset version per commit, so there were two ways to +flush K cycles: + +**(i) Barrier flush — RECOMMENDED.** Keep 1 cycle = 1 Lance version. Each +seal performs its (unsynced) commit immediately — the cycle gets its REAL +Lance version number at publish time — and the background flush is **one sync +barrier** covering versions `durable_head+1 ..= v`. K cheap page-cache +manifest writes, ONE fdatasync. `durable_head` advances only when the barrier +returns. + +**(ii) Version multiplexing — REJECTED for now.** K cycles share one Lance +version; `CycleId` becomes the fine clock. The adversarial panel killed the +"the type split pre-anticipated this" framing with citations: + +- `temporal.rs` has **no cycle-within-version coordinate** — + `QueryReference` carries `server_id/ref_version/hlc_tick/mode/rung`, no + `CycleId`; `classify()` compares whole Lance versions. Under (ii) the + epistemic horizon coarsens to K and **the no-hindsight guarantee degrades + to up-to-(K−1) cycles of intra-version hindsight for a Strict reader** — a + silent semantic break of the module's flagship property + (`no_hindsight_streamed_known_game`, built on 1 event = 1 version). +- The two in-type candidates for a fine clock both fail by contract: + `hlc_tick` is a cross-server causal tick (repurposing it IS the "third + invented numbering" wearing a borrowed name, and it collides with the + deinterlace sort key); `cast_seq` is owner-local ("Cross-owner values are + never compared"). +- The 1:1 binding is contractual at ≥6 sites (`persist_sink.rs:10-12`, + `:94-96`, `:309-313`, `:323-327` + the `versions()` ladder falsifier + `:864-872`; `cycle_driver.rs:103-105`, `:133-136`) plus + cycle-count-as-version arithmetic in the measure harness. (ii) is a + retrofit across all of them; (i) touches none. + +So: **the type split (`CycleId` ≠ `DatasetVersion`) is a foothold, not an +anticipation** — and (i) does not even need the foothold. (ii) stays on the +shelf as the fallback if barrier flush measures insufficient, with its +hindsight-coarsening cost named as the price of admission. + +## 4. The hardened invariants (each one bought by a landed attack) + +**H-1 — checkpoint fencing.** `recover_and_apply` requires a THIRD durable +artifact besides RAM and the WAL: the per-owner `(phase, watermark)` +checkpoint (`persist_sink.rs:364-366`, `:379-382`). The naive "cognition and +record die together" claim was REFUTED as stated: a checkpoint cut from +published-but-unflushed state and made durable early leaves, after a crash, a +durable phase ahead of the surviving log (StalePhase corruption) AND a stale +watermark that silently skips legitimately durable later landings. The +repair, now an invariant: **checkpoint state may never be made durable ahead +of `durable_head` — it rides the same barrier or is fenced to it.** With H-1 +(and the verified fact that nothing else escapes the RAM+WAL pair before +durability — the refuter's vector (a) found no side-effect leak in current +source), the die-together property holds: the crash loses the window and the +cognition that read it *together*, so restart-at-`durable_head` has no +divergence. + +**H-2 — torn-tail cleanup.** Under barrier flush, a crash can leave unsynced +manifests above `durable_head` in torn or reordered states (without per-commit +sync, the filesystem may persist manifest j+1 while j's data pages are still +volatile — the refuter's vector (b)). So `durable_head` is NOT "the newest +manifest found"; it is **the newest fully-intact version at or below the last +barrier**, and recovery MUST determine it (barrier record or integrity probe) +and **remove/tombstone everything above it before the next writer starts** — +otherwise the durable prefix is not contiguous and both the base fence and the +watermark skip-logic silently corrupt. + +**H-3 — the window is not a veto window.** A published cycle is irrevocable +from cognition's side the moment a successor chains on it. The flush queue is +append-only; nothing is ever dropped from it. The Libet veto lives PRE-SEAL +(v2 `ClosureState::Vetoed`, write-order registration) — never between publish +and barrier. Anyone "optimizing" by unqueueing a vetoed version corrupts the +chain. + +**H-4 — zero-copy conditions (two sentences that keep the design legal).** +The sweep found the ruling that forbids "detached canonical state / +snapshots" verbatim (`E-AN-UNFILLED-SEMANTIC-SLOT-…-1`) and that +`DetachedCycleBatch` is literally documented as a snapshot (with the known +`freeze`-clones debt already on the board). The hot window is legal ONLY +under both of: (1) **the window retains the SINGLE freeze-output allocation +per cycle** — append-only, eviction-only, never re-minted, never a second +copy per version; (2) **the batched Lance append writes FROM those same +retained bytes** — the window IS the in-place backing store of the durable +write, i.e. genuinely the primary allocation, not a sidecar beside one. + +**H-5 — reader visibility is rung-decided; there is NO pump +(operator-corrected 2026-08-05).** Two reads exist: *published* (cognition; +may read above `durable_head`) and *durable-only* (audit/compliance rung; a +durable-only read of an unflushed range FORCES the barrier for that range — +the transparent fall-through). + +An earlier revision of this invariant said "the kanban pump rebases onto the +publish ack" — **that resurrected a deprecated mechanic and is retracted.** +There is no architectural pump, no acknowledgement-driven progression, and no +scheduler advancing cognition; the ack/pump framing belongs to the historical +compatibility surface only (`E-PROGRESSION-IS-EXISTENCE-NOT-COMMAND-1`, +completing the 2026-07-10 correction chain around +`E-KANBANSTEP-IS-THE-TRIGGER-1`). The authoritative execution path is: + +``` +think → seal → publish Lance version → next cycle reads the published version +``` + +The version becoming queryable IS the progression — nothing signals it, +acknowledges it, or schedules it. Durability trails publication +independently. Consequently the hot window is **not a message queue awaiting +acknowledgement**; it is a **resident horizon of immutable Lance versions**: +readers observe versions, writers publish versions, persistence catches up +on its own clock. The decoupling this design delivers needs no rewired +trigger — cycle n+1 reads published cycle n the moment it exists, which is +already the whole mechanism. + +Ack/SLA/retry/notification vocabulary may legitimately survive ONLY in +external consumer surfaces (ticket-processing-style workflows) — an +application concern, never substrate mechanics. + +## 5. Contract re-wordings this design owes (the sweep's site list) + +Not code yet — but the day the build lane opens, these exact sites change +meaning and must change words, or the docs lie: + +- `persist_sink.rs:288` "one **durable** append per cycle" → one *publishing* + append per cycle; durability moves to the barrier op (a NEW trait method — + no K-cycle/barrier operation exists in `WalSink` today, the refuter + confirmed the design currently has "no contract home"). +- `persist_sink.rs:304-308` "This is the ONLY durable op" → the only + *publishing* op; the barrier is the durable op. +- `persist_sink.rs:39-43` + `:107`: `base_version` may name a + published-but-not-yet-durable predecessor — legal under (i) because the + unsynced commit already minted the real version; the doc must say + "published predecessor," and `scan_sealed`'s "COMMITTED landings only" must + name which head it reads against (published vs durable). +- The `wal_writes() == 1` falsifier (`persist_sink.rs:673`) SURVIVES under + (i) — one append per cycle stays true; what becomes 1-per-K is the *sync*. + A new falsifier is owed: `sync_barriers == 1` per K cycles, plus the H-2 + torn-tail recovery test. +- `.claude/knowledge/seal-vs-temporal-ordering-information.md` — dated + caveats added by this plan: property 2's "once it is in the record, it *is* + the durable fact" holds only at/below `durable_head` (above it, arrival + order exists only in RAM and a crash loses it unreproducibly — exactly + PROBE-SEAL-TIE-DENSITY's point); property 4's parenthetical "(one WAL + append → one DatasetVersion)" re-anchors cohort membership on the SEAL + event, which under (i) still mints one version per cycle. +- v2's pin "the existing one-cycle/one-version logical contract is + unchanged" — **SURVIVES under (i)** (that is half the reason (i) won); + under (ii) it would break, which is recorded on the shelf entry above. + +## 6. Sizing and flush policy + +- **Byte-budgeted, never count-budgeted.** A full 64k-owner cycle image is + the 32 MiB canonical frame → 48 hot epochs = 1.5 GiB, 4 = 128 MiB. But a + bursty consumer (one external event yielding several versions within tens + of ms) produces KB-scale delta cycles. The window budget is bytes (with a + count cap as the secondary bound); with `Arc`-rooted structural sharing + across versions, N hot versions ≠ N full copies. +- **Adaptive flush (the Nagle shape):** barrier when + `dirty_bytes ≥ budget/2` **or** `dirty_cycles ≥ 16` **or** `200 ms` + elapsed since the oldest unflushed publish **or** memory pressure **or** + shutdown **or** a durable-only reader forces a range (H-5). Quiet periods + flush almost immediately; bursts coalesce into a handful of large + sequential writes. + +## 7. EXP-HOT-WINDOW — pre-registered measurements (none run) + +Same discipline as v1–v3: one axis at a time, A0's spread guard inherited, +both directions named in advance. + +- **P1 publish latency:** per-cycle publish (seal + unsynced commit) vs A0's + seal+commit+sync. Expected: publish ≪ A0's commit path because the sync + leaves the loop. If publish latency does NOT drop, the design's premise + (the sync is the expensive part *in the loop*) is falsified for this + workload and the layer is complexity without payoff — a KILL. +- **P2 barrier amortization curve:** K ∈ {1, 4, 16, 48} cycles per barrier; + report durable throughput AND the barrier's own duration (it should scale + sub-linearly in K if writeback overlaps; if barrier(K) ≈ K·barrier(1) the + amortization is fictional — a KILL). WAL-knee instability warning applies + verbatim: if the barrier phase shows A0's 6× swings, report the spread and + claim nothing. +- **P3 crash-window cost:** the distribution of `published_head − + durable_head` (cycles AND bytes) under the §6 policy. This is the price + tag the operator accepts explicitly, not a number to bury. +- **P4 torn-tail recovery (H-2 falsifier):** kill -9 between commit and + barrier; restart must (a) find the correct `durable_head`, (b) remove the + torn tail, (c) replay to a state byte-identical with never-having-published + the lost window. Both halves: a can-fire case (torn manifest present → + detected) and a can-stay-silent case (clean barrier → nothing removed). +- **P5 checkpoint fence (H-1 falsifier):** attempt to persist an owner + checkpoint above `durable_head`; the fence must refuse. Then the positive + control: checkpoint riding the barrier lands. + +## 8. Relationship to the open questions this session banked + +- **PROBE-SEAL-TIE-DENSITY** becomes MORE urgent under this design: where + ties exist, the seal's order derives partly from non-durable arrival — and + the hot window widens the span in which that order exists ONLY in RAM. +- **ISS-MARM-T1-4X-A0-GAP** must be resolved before P1/P2 are compared + against A0 numbers (same commensurability rule as v3). +- **TD-LANCE9-LANCEDB036-REMEASURE**: lance 9 / lancedb 0.36 are expected to + change commit overhead — P1/P2 should be measured before and after that + upgrade, since the fork's economics ((i)'s K manifest writes) move with it. diff --git a/crates/cognitive-shader-driver/src/mailbox_soa.rs b/crates/cognitive-shader-driver/src/mailbox_soa.rs index 39154d322..0cd2ef138 100644 --- a/crates/cognitive-shader-driver/src/mailbox_soa.rs +++ b/crates/cognitive-shader-driver/src/mailbox_soa.rs @@ -156,7 +156,7 @@ pub struct MailboxSoA { // The three per-row style lanes appended after Kanban in the canonical // `NodeRow` value slab (`ValueTenant::{FrozenStyle, LearnedStyle, // ExploreStyle}`, offsets 152/164/176). Held here as SoA columns so a - // `KanbanActor`'s owned advance reads/writes them `&mut` (E-CE64-MB-4, ractor + // the exclusive owner's advance reads/writes them `&mut` (E-CE64-MB-4, // sole-mutator) — NOT a deprecated symbiont `Vec`. Each lane is a // 12-byte content-blind register whose reading is ClassView-selected per // ROW/CLASS (never per lane): a policy row reads all three as 12 palette atoms @@ -737,8 +737,9 @@ impl MailboxSoA { // `on_behalf` mailbox is read from `self`, so a call site cannot name a // different owner than the SoA it writes. // -// ractor context: ownership is a COMPILE-TIME declaration (the KanbanActor -// owns the `MailboxSoA`; spawn-only, never a message path). This method +// Ownership context: ownership is a COMPILE-TIME declaration (the exclusive +// owner holds the `MailboxSoA` by move; never a message path — the actor +// wrapper that once dramatized this was deleted 2026-08-05). This method // runs inside the owner's context (`&self` borrow of the owned SoA) — the // cast is a WAL report to the ahead-firing writer ("melden macht frei", // never refused), not a message to another actor. @@ -768,12 +769,12 @@ impl MailboxSoA { // // Per R1 ("the SoA columns are mutated by the owner's own cognitive ops, never // serialized through the contract trait"), these are the OWNER's crate-visible -// mutation surface for the three style lanes. Each is `&mut self`, so — when -// driven from a `KanbanActor::handle` whose `State` IS this owner — the -// single-writer no-aliasing guarantee is compile-time (E-CE64-MB-4, ractor -// sole-mutator), not by-convention. The *value* decisions (the explore -// coprime-walk atom, the NARS-revision learned atom) belong to the caller (the -// KanbanActor's phase handlers); these ops only apply an already-decided atom to +// mutation surface for the three style lanes. Each is `&mut self`, so — driven +// through the exclusive owner that holds this SoA — the single-writer +// no-aliasing guarantee is compile-time (E-CE64-MB-4, sole-mutator), not +// by-convention. The *value* decisions (the explore coprime-walk atom, the +// NARS-revision learned atom) belong to the caller (the owner's cognitive +// phase logic); these ops only apply an already-decided atom to // the owned lane. An un-gated impl (NOT under `with-planner`): the triangle write // surface has no planner dependency. `family >= 12` is a no-op (the #717 // `triangle_for` guard — an out-of-range family never aliases slot 12); @@ -894,7 +895,7 @@ impl MailboxSoaView for MailboxSoA { }) } /// Override the deferred-binding default: the in-RAM owner DOES carry the three - /// autopoiesis-triangle lanes (P4), so a `KanbanActor` reading `FrozenStyle` + /// autopoiesis-triangle lanes (P4), so an owner reading `FrozenStyle` /// during `CognitiveWork` gets the real checkpoint policy. `populated`-guarded /// (same logical-row discipline as `identity_plane_at`); a query into /// `populated..N` returns `None`. diff --git a/crates/lance-graph-planner/examples/blw_fusion.rs b/crates/lance-graph-planner/examples/blw_fusion.rs index ca2c130b4..e0be83a43 100644 --- a/crates/lance-graph-planner/examples/blw_fusion.rs +++ b/crates/lance-graph-planner/examples/blw_fusion.rs @@ -349,7 +349,11 @@ fn rank_verdicts(owner: &Tenant, pool_size: usize, seed: &[u64]) -> Vec { .collect(); // Descending score, ties broken by ASCENDING row index (§2.3). scored.sort_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(&b.1))); - let n_pos = pool_size / 4; // PRE-REGISTERED q = 0.25, floor operationalization. + // PRE-REGISTERED q = Q_QUANTILE, floor operationalization (see the const's + // doc comment). f64 mult of a small usize by 0.25 is exact; the `as usize` + // cast floors, matching the previous `pool_size / 4` integer division + // bit-for-bit for every pool size this harness produces. + let n_pos = (pool_size as f64 * Q_QUANTILE) as usize; let mut verdict = vec![false; pool_size]; for &(_, row) in scored.iter().take(n_pos) { verdict[row] = true; @@ -681,7 +685,11 @@ fn print_association_table(label: &str, assoc: &BinaryAssociation) { #[derive(Debug, Clone, Copy, PartialEq)] enum Band { Redundancy, - Fusion, + /// Intermediate chance-corrected agreement — NOT a fusion verdict + /// (renamed from `Fusion` 2026-08-05: a middle kappa is merely + /// intermediate agreement under the observed marginals; a fusion + /// VERDICT additionally requires §12.4 D3b's held-out criterion). + Intermediate, NoSharedHorizon, Undefined, } @@ -690,7 +698,7 @@ fn classify_band(kappa: Option) -> Band { None => Band::Undefined, Some(k) if k > KAPPA_REDUNDANCY_FLOOR => Band::Redundancy, Some(k) if k < KAPPA_NO_SHARED_HORIZON_CEILING => Band::NoSharedHorizon, - Some(_) => Band::Fusion, + Some(_) => Band::Intermediate, } } @@ -1305,7 +1313,7 @@ async fn main() -> Result<(), Box> { let band_outcome = if matches!(band_strict, Band::Undefined) || matches!(band_aware, Band::Undefined) { BandOutcome::UndefinedKappa - } else if band_strict == Band::Fusion && band_aware == Band::Fusion { + } else if band_strict == Band::Intermediate && band_aware == Band::Intermediate { BandOutcome::InIn } else if band_strict == band_aware { BandOutcome::OutOutSameSide @@ -1324,13 +1332,13 @@ async fn main() -> Result<(), Box> { ); let movement_fires = delta_kappa_pin.is_some_and(|d| d.abs() >= MOVEMENT_THRESHOLD); if movement_fires { - let fusion_permitted = band_outcome == BandOutcome::InIn + let candidate_permitted = band_outcome == BandOutcome::InIn && !strict_collapsed && !aware_collapsed && !strict_unstable && !aware_unstable; - if fusion_permitted { - println!("§3.3: MOVEMENT FIRES at V_pin and band is IN/IN — FUSION MAY BE CLAIMED"); + if candidate_permitted { + println!("§3.3: MOVEMENT FIRES at V_pin and band is IN/IN — COMPLEMENTARITY CANDIDATE (a fusion VERDICT additionally requires §12.4 D3b's held-out criterion, which remains BLOCKED)"); } else { println!("§3.3: MOVEMENT FIRES at V_pin but the band/guard state does not permit a fusion claim (§3.2/§3.5)"); } @@ -1367,6 +1375,12 @@ async fn main() -> Result<(), Box> { let mut max_abs_delta: f64 = 0.0; let mut any_undefined = false; let mut hamming_at_v8: Option<(usize, usize)> = None; + // External-review catch (2026-08-05): kappa can hide CANCELLING churn, and + // Hamming at V8 is zero BY CONSTRUCTION (the identical case) — so a DROP + // keyed on V8 alone could fire while verdicts swapped in opposite + // directions at V1..V7. Track the maxima across ALL horizons instead. + let mut max_ham_a: usize = 0; + let mut max_ham_b: usize = 0; for c in 1..=plan.len() { let prefix_k = c * SLICE; let vk = *sealed_versions @@ -1418,6 +1432,8 @@ async fn main() -> Result<(), Box> { let ham_a = hamming(&a_s_bools, &a_a_bools); let ham_b = hamming(&b_s_bools, &b_a_bools); + max_ham_a = max_ham_a.max(ham_a); + max_ham_b = max_ham_b.max(ham_b); if c == plan.len() { hamming_at_v8 = Some((ham_a, ham_b)); } @@ -1439,10 +1455,10 @@ async fn main() -> Result<(), Box> { ); let drop_fires = - !any_undefined && max_abs_delta < DROP_THRESHOLD && ham_a_v8 == 0 && ham_b_v8 == 0; + !any_undefined && max_abs_delta < DROP_THRESHOLD && max_ham_a == 0 && max_ham_b == 0; println!( "C7 DROP verdict: max|delta_kappa| over {S_CYCLES} horizons = {max_abs_delta:.4} (threshold {DROP_THRESHOLD}); \ - identical-case (k={S_CYCLES}) hamming=(A:{ham_a_v8}, B:{ham_b_v8}) -> {}", + max hamming over ALL horizons=(A:{max_ham_a}, B:{max_ham_b}); identical-case (k={S_CYCLES}) hamming=(A:{ham_a_v8}, B:{ham_b_v8}) -> {}", if drop_fires { "DROP FIRES trajectory-wide" } else { @@ -1473,8 +1489,5 @@ async fn main() -> Result<(), Box> { in the rank criterion, not temporal.rs." ); - println!("--"); - println!("NOT COMPILED, NOT RUN by the authoring lane -- Sonnet grindwork, edit-only, no cargo (per task hard rules). The orchestrator compiles/lints/tests once."); - Ok(()) } diff --git a/crates/lance-graph-planner/examples/blw_rows.rs b/crates/lance-graph-planner/examples/blw_rows.rs index b4fc9c082..84b98d802 100644 --- a/crates/lance-graph-planner/examples/blw_rows.rs +++ b/crates/lance-graph-planner/examples/blw_rows.rs @@ -3,8 +3,8 @@ //! # The axis, stated first because it has been wrong twice //! //! **Owner-count is NOT a scale knob.** An owner is a **TENANT** — one mailbox, -//! one kanban board, one `KanbanActor` that is its sole mutator (`CLAUDE.md` -//! §V3 rulings; `E-CE64-MB-4`; the SoA is *moved* into the actor and that move +//! one kanban board, one exclusive owner as its sole mutator (`CLAUDE.md` +//! §V3 rulings; `E-CE64-MB-4`; the SoA is *moved* into its owner and `&mut` //! is the compile-time no-aliasing proof). Two prior attempts got this wrong in //! the same direction: the first tiled the Bible across 64 owners and //! **fabricated 63 tenants** (plan §12.1a′); the second kept owner-count as the diff --git a/crates/lance-graph-supervisor/examples/measure_wal_curve.rs b/crates/lance-graph-supervisor/examples/measure_wal_curve.rs new file mode 100644 index 000000000..305254ecd --- /dev/null +++ b/crates/lance-graph-supervisor/examples/measure_wal_curve.rs @@ -0,0 +1,3507 @@ +//! `measure_wal_curve` — the five-axis 64k measurement binary (operator-specified). +//! +//! Spec: `.claude/plans/measure-64k-axes-v1.md` — THE authority for every +//! constant, phase name, and gate in this file. Read it before touching +//! anything here. This header only orients the reader inside the code. +//! +//! `.claude/plans/measure-64k-axes-v3.md` adds two more arms to THIS SAME +//! binary (one release binary, never a second): §15 the M-arm (Morton +//! reorder inserted before the seal) and §16 the O-arm (does the seal's +//! own ordering duplicate what `temporal.rs` already provides?). Build lane +//! report for the M-arm/O-arm addition: +//! `.claude/board/exec-runs/m-arm-o-arm-build.md`. +//! +//! Build lane report (deviations, what could not be verified): +//! `.claude/board/exec-runs/measure-wal-curve-build.md`. +//! +//! ## Run +//! +//! ```text +//! cargo run --release -p lance-graph-supervisor --features cycle-driver \ +//! --example measure_wal_curve +//! ``` +//! +//! Output: one CSV row per measured cycle to `$MEASURE_OUT` (default +//! `/tmp/measure_wal_curve.csv`, schema in the plan's "Measurement schema" +//! section); per-configuration medians to stderr; the four closing answers +//! (plan's "Placement + gates" section) as the final stderr block. +//! +//! ## Ground rules this file honors (plan, "Ground truth") +//! +//! - Logical population: 65,536 owner identities, everywhere. +//! - Canonical row: 512 bytes; canonical frame: 65,536 × 512 B = 32 MiB. +//! - `temporal.rs` runs ONLY after a sealed WAL read (never inside WAL prep). +//! - One logical cycle → one commit → one fdatasync → one `DatasetVersion`; +//! segments are I/O slices of ONE commit, never version-publishing units. +//! - Ownership is a type/borrow property — never described as a runtime +//! "claim" operation anywhere in this file's prose or variable names. +//! - The hot `MailboxSoA` representation and the canonical `NodeRow512` +//! representation never share one memory claim (B1a's peak RSS and B1b's +//! peak RSS are reported as two separate numbers; only their *difference* +//! is the derived "hot representation overhead" metric). +//! - The WAL-curve plateau is a measured knee, printed as a descriptive +//! finding — never framed as PASS/KILL. +//! - EXP-KIA-A2-64K is exploratory and non-claiming: it cannot and does not +//! mark D-KIA-A2 passed (that gate stays median-of-5, >=2x, its own +//! pre-registered protocol, untouched by this file). + +fn main() { + #[cfg(feature = "cycle-driver")] + { + measure::run(); + } + #[cfg(not(feature = "cycle-driver"))] + { + eprintln!( + "measure_wal_curve requires --features cycle-driver \ + (see .claude/plans/measure-64k-axes-v1.md)" + ); + std::process::exit(1); + } +} + +#[cfg(feature = "cycle-driver")] +mod measure { + #![allow( + clippy::cast_possible_truncation, + clippy::cast_possible_wrap, + clippy::cast_sign_loss, + clippy::cast_precision_loss, + clippy::too_many_lines, + clippy::too_many_arguments + )] + + use std::collections::{BTreeMap, HashMap}; + use std::fs::{self, File, OpenOptions}; + use std::io::{IoSlice, Write as _}; + use std::path::PathBuf; + use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering}; + use std::sync::Mutex; + use std::time::Instant; + + use cognitive_shader_driver::mailbox_soa::{MailboxSoA, WriteCell, WriteOutcome, WORDS_PER_FP}; + use lance_graph_contract::cognitive_shader::MetaWord; + use lance_graph_contract::collapse_gate::MailboxId; + use lance_graph_contract::kanban::{KanbanColumn, KanbanMove}; + use lance_graph_contract::mul::i4_eval::gate_decision_i4; + use lance_graph_contract::qualia::QualiaI4_16D; + use lance_graph_contract::scheduler::DatasetVersion; + use lance_graph_contract::soa_view::MailboxSoaView; + + use lance_graph_planner::batch_writer::BatchWriter; + use lance_graph_planner::ir::Arena; + use lance_graph_planner::owner_adapter::emit_bootstrap_intent; + use lance_graph_planner::persist_sink::{ + order_cycle_stably, persist_cycle, CycleFrame, CycleId, DetachedCycleBatch, LandedSlot, + SweepSlot, WalSink, WriteFailed, + }; + use lance_graph_planner::strategy::style_strategy::StyleStrategy; + use lance_graph_planner::temporal::{ + deinterlace, local_trajectories, DeinterlaceRow, LocalCausalRow, NoDeps, QueryReference, + }; + use lance_graph_planner::traits::{ + PlanContext, PlanInput, PlanStrategy, QueryFeatures, StrategyOutcome, + }; + + use lance_graph_supervisor::cycle_driver::{ + apply_sealed_transitions, collect_casts, SealedCycle as DriverSealedCycle, SealedTransition, + }; + + // ═════════════════════════════════════════════════════════════════════ + // §0 — shared constants + shapes (pre-registered, before any number + // exists — matching `probe_ignition_64k.rs`'s discipline). + // ═════════════════════════════════════════════════════════════════════ + + const FLEET_OWNERS: u32 = 65_536; + const CANONICAL_ROW_BYTES: usize = 512; + const CANONICAL_FRAME_BYTES: usize = FLEET_OWNERS as usize * CANONICAL_ROW_BYTES; // 32 MiB + const WARMUP_CYCLES: u32 = 2; + const MEASURED_CYCLES: u32 = 16; + const WAL_DIR: &str = "/tmp/measure_wal_curve_wal"; + + /// The storage ENVELOPE stand-in for the canonical node layout — `key(16) | + /// edges(16) | value(480)` = 512 B (CLAUDE.md § CANON — Minimal SoA node, + /// 2026-06-13). This is NOT a new type proposal: it is a local, plain + /// byte-array measurement fixture standing in for the persisted row shape + /// B1b/W1 measure against — a `#[repr(C)] [u8; 512]`, nothing more. + #[repr(C)] + #[derive(Clone, Copy)] + struct NodeRow512([u8; CANONICAL_ROW_BYTES]); + + const _: () = assert!(std::mem::size_of::() == CANONICAL_ROW_BYTES); + + impl NodeRow512 { + /// Deterministic, non-degenerate content per logical row id — splitmix64 + /// scramble (no clock, no rng), same generator shape as + /// `probe_ignition_64k.rs:99-111`'s `plane_for` (provenance). + fn for_id(id: u64) -> Self { + let mut bytes = [0u8; CANONICAL_ROW_BYTES]; + let mut x = id ^ 0x9E37_79B9_7F4A_7C15; + for chunk in bytes.chunks_exact_mut(8) { + x ^= x >> 30; + x = x.wrapping_mul(0xBF58_476D_1CE4_E5B9); + x ^= x >> 27; + x = x.wrapping_mul(0x94D0_49BB_1331_11EB); + x ^= x >> 31; + chunk.copy_from_slice(&(x | 1).to_le_bytes()); + } + Self(bytes) + } + fn as_bytes(&self) -> &[u8] { + &self.0 + } + } + + /// The dense identity plane fixture — `probe_ignition_64k.rs:99-111` + /// provenance, reused verbatim (never a zero plane — every word `| 1`). + fn splitmix_plane(seed: u64) -> Vec { + let mut x = seed ^ 0x9E37_79B9_7F4A_7C15; + let mut plane = vec![0u64; WORDS_PER_FP]; + for w in plane.iter_mut() { + x ^= x >> 30; + x = x.wrapping_mul(0xBF58_476D_1CE4_E5B9); + x ^= x >> 27; + x = x.wrapping_mul(0x94D0_49BB_1331_11EB); + x ^= x >> 31; + *w = x | 1; + } + plane + } + + /// Flow qualia fixture — `probe_ignition_64k.rs:88-90` / `probe_ignition.rs:196-203` + /// provenance (warmth=4, groundedness=3, coherence=4, valence=2 => + /// flow_proxy 7, Calibrated). Every arm's owners Flow on this fixture — a + /// non-trivial, non-zero qualia vector, never an all-zeros rig. + fn flow_qualia() -> QualiaI4_16D { + QualiaI4_16D(0).with(3, 4).with(14, 3).with(9, 4).with(1, 2) + } + + /// Derived mantissa — `probe_ignition_64k.rs:92-95` provenance. + fn mantissa_of(owner: &MailboxSoA) -> i8 { + owner.pending_count().min(7) as i8 + } + + /// The 23D analytical style vector — `probe_ignition_64k.rs:230-243` + /// provenance (idx 4 = analytical). + fn style_context() -> PlanContext { + let mut v = vec![0.0f64; 23]; + v[4] = 1.0; + PlanContext { + query: String::new(), + features: QueryFeatures::default(), + free_will_modifier: 1.0, + thinking_style: Some(v), + nars_hint: None, + witness: None, + } + } + + /// ONE `StyleStrategy::plan` call, reused across every owner in an arm — + /// `probe_ignition_64k.rs:289-308` provenance: every owner is armed with + /// the same z, so the style outcome is owner-independent; per-owner + /// binding happens inside `emit_bootstrap_intent` -> `rebind_bootstrap` + /// (the no-theft guard), never by recomputing the plan per owner. + fn build_style_outcome() -> StrategyOutcome { + let mut arena = Arena::new(); + let plan_out = StyleStrategy + .plan( + PlanInput { + plan: None, + context: style_context(), + outcome: None, + }, + &mut arena, + ) + .expect("StyleStrategy::plan over the fixed recipe substrate"); + plan_out + .outcome + .expect("StyleStrategy always surfaces a StrategyOutcome") + } + + /// FNV-1a 64-bit — std-only, no external digest crate. + fn fnv1a64(bytes: &[u8]) -> u64 { + let mut h: u64 = 0xcbf2_9ce4_8422_2325; + for &b in bytes { + h ^= u64::from(b); + h = h.wrapping_mul(0x0000_0100_0000_01B3); + } + h + } + + // ═════════════════════════════════════════════════════════════════════ + // §1 — /proc/self reading (std-only; RSS + faults + context switches). + // ═════════════════════════════════════════════════════════════════════ + + #[derive(Clone, Copy, Default)] + struct ProcSnapshot { + vmhwm_kb: u64, + vmrss_kb: u64, + minflt: u64, + majflt: u64, + vol_ctxt: u64, + nonvol_ctxt: u64, + } + + /// Returns `(VmHWM_kB, VmRSS_kB, voluntary_ctxt, nonvoluntary_ctxt)`. + /// + /// **VmHWM is process-monotonic** — it is the high-water mark since process + /// start, so subtracting one arm's HWM from another's inside ONE process + /// yields the historical max twice, not two footprints. Any per-arm memory + /// figure MUST come from a VmRSS delta (current RSS after minus before). + fn read_proc_status() -> (u64, u64, u64, u64) { + let mut vmhwm = 0u64; + let mut vmrss = 0u64; + let mut vol = 0u64; + let mut nonvol = 0u64; + if let Ok(text) = fs::read_to_string("/proc/self/status") { + for line in text.lines() { + if let Some(rest) = line.strip_prefix("VmHWM:") { + vmhwm = rest + .trim() + .trim_end_matches("kB") + .trim() + .parse() + .unwrap_or(0); + } else if let Some(rest) = line.strip_prefix("VmRSS:") { + vmrss = rest + .trim() + .trim_end_matches("kB") + .trim() + .parse() + .unwrap_or(0); + } else if let Some(rest) = line.strip_prefix("voluntary_ctxt_switches:") { + vol = rest.trim().parse().unwrap_or(0); + } else if let Some(rest) = line.strip_prefix("nonvoluntary_ctxt_switches:") { + nonvol = rest.trim().parse().unwrap_or(0); + } + } + } + (vmhwm, vmrss, vol, nonvol) + } + + /// `/proc/self/stat` field 10 (minflt) / field 12 (majflt), 1-indexed. + /// `comm` (field 2) is parenthesised and may itself contain spaces, so we + /// split AFTER the last `)` before counting whitespace-separated fields. + fn read_proc_stat_faults() -> (u64, u64) { + let mut minflt = 0u64; + let mut majflt = 0u64; + if let Ok(text) = fs::read_to_string("/proc/self/stat") { + if let Some(close) = text.rfind(')') { + let fields: Vec<&str> = text[close + 1..].split_whitespace().collect(); + // fields[0] = state (field 3); minflt = field 10 = fields[7]; + // majflt = field 12 = fields[9]. + if fields.len() > 9 { + minflt = fields[7].parse().unwrap_or(0); + majflt = fields[9].parse().unwrap_or(0); + } + } + } + (minflt, majflt) + } + + fn proc_snapshot() -> ProcSnapshot { + let (vmhwm_kb, vmrss_kb, vol_ctxt, nonvol_ctxt) = read_proc_status(); + let (minflt, majflt) = read_proc_stat_faults(); + ProcSnapshot { + vmhwm_kb, + vmrss_kb, + minflt, + majflt, + vol_ctxt, + nonvol_ctxt, + } + } + + // ═════════════════════════════════════════════════════════════════════ + // §2 — the CSV schema (plan "Measurement schema") + sink. + // ═════════════════════════════════════════════════════════════════════ + + #[derive(Clone)] + struct Row { + owner_shape: &'static str, + physical_layout: &'static str, + threads: u32, + segment_rows: u64, + segment_bytes: u64, + segments_per_cycle: u64, + repeat: u32, + build_ns: u64, + scan_ns: u64, + think_ns: u64, + rebind_cast_ns: u64, + collect_ns: u64, + freeze_ns: u64, + wal_write_ns: u64, + wal_sync_ns: u64, + temporal_layer1_ns: u64, + temporal_layer2_ns: u64, + apply_ns: u64, + total_ns: u64, + logical_rows: u64, + logical_bytes: u64, + sealed_transitions: u64, + applied_transitions: u64, + wal_syscalls: u64, + fsync_calls: u64, + dataset_versions: u64, + peak_rss_bytes: u64, + minor_faults: u64, + major_faults: u64, + context_switches: u64, + max_active_workers: u32, + result_digest: u64, + /// M-arm (plan v3): the Morton write-order reorder phase, timed in + /// isolation from seal/write. `0` for every arm that does not perform + /// a reorder (never fabricated). + morton_reorder_ns: u64, + } + + impl Row { + fn header() -> &'static str { + "owner_shape,physical_layout,threads,segment_rows,segment_bytes,\ + segments_per_cycle,repeat,build_ns,scan_ns,think_ns,rebind_cast_ns,\ + collect_ns,freeze_ns,wal_write_ns,wal_sync_ns,temporal_layer1_ns,\ + temporal_layer2_ns,apply_ns,total_ns,logical_rows,logical_bytes,\ + sealed_transitions,applied_transitions,wal_syscalls,fsync_calls,\ + dataset_versions,peak_rss_bytes,minor_faults,major_faults,\ + context_switches,llc_misses,max_active_workers,result_digest,\ + morton_reorder_ns" + } + + /// `llc_misses` is always emitted EMPTY — no perf-counter access in + /// this std-only binary (plan: "an empty cell, never a fabricated + /// one"). + fn to_csv(&self) -> String { + format!( + "{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},,{},{},{}", + self.owner_shape, + self.physical_layout, + self.threads, + self.segment_rows, + self.segment_bytes, + self.segments_per_cycle, + self.repeat, + self.build_ns, + self.scan_ns, + self.think_ns, + self.rebind_cast_ns, + self.collect_ns, + self.freeze_ns, + self.wal_write_ns, + self.wal_sync_ns, + self.temporal_layer1_ns, + self.temporal_layer2_ns, + self.apply_ns, + self.total_ns, + self.logical_rows, + self.logical_bytes, + self.sealed_transitions, + self.applied_transitions, + self.wal_syscalls, + self.fsync_calls, + self.dataset_versions, + self.peak_rss_bytes, + self.minor_faults, + self.major_faults, + self.context_switches, + // NOTE: the trailing `,,` above already emits the empty + // llc_misses cell between context_switches and + // max_active_workers — do not add another field here. + self.max_active_workers, + self.result_digest, + self.morton_reorder_ns, + ) + } + } + + struct CsvSink { + file: File, + path: String, + rows_written: u64, + } + + impl CsvSink { + fn new() -> Self { + let path = std::env::var("MEASURE_OUT") + .unwrap_or_else(|_| "/tmp/measure_wal_curve.csv".to_string()); + let mut file = File::create(&path).unwrap_or_else(|e| { + panic!("measure_wal_curve: cannot create MEASURE_OUT {path}: {e}") + }); + writeln!(file, "{}", Row::header()).expect("write csv header"); + eprintln!("measure.csv: writing rows to {path}"); + Self { + file, + path, + rows_written: 0, + } + } + fn write(&mut self, row: &Row) { + writeln!(self.file, "{}", row.to_csv()).expect("write csv row"); + self.rows_written += 1; + } + } + + // ═════════════════════════════════════════════════════════════════════ + // §3 — median / p95 + the cache-amortisation gain/plateau helpers. + // ═════════════════════════════════════════════════════════════════════ + + fn median(samples: &[u64]) -> u64 { + if samples.is_empty() { + return 0; + } + let mut s = samples.to_vec(); + s.sort_unstable(); + let n = s.len(); + if n % 2 == 1 { + s[n / 2] + } else { + (s[n / 2 - 1] + s[n / 2]) / 2 + } + } + + fn p95(samples: &[u64]) -> u64 { + if samples.is_empty() { + return 0; + } + let mut s = samples.to_vec(); + s.sort_unstable(); + let n = s.len(); + let idx = ((n as f64) * 0.95).ceil() as usize; + s[idx.min(n - 1)] + } + + /// `gain(C) = throughput(C)/throughput(prev) - 1` (plan's cache-amortisation + /// curve). Returns the per-config gains (index 0 has no predecessor, so it + /// is `f64::NAN` — never compared) and the DESCRIPTIVE plateau index: the + /// first `i >= 2` where BOTH `gain(i-1)` and `gain(i)` are `< 5%`. Purely + /// descriptive — never a PASS/KILL verdict (plan, "Placement + gates"). + fn plateau_index(throughput: &[f64]) -> (Vec, Option) { + let mut gains = vec![f64::NAN; throughput.len()]; + for i in 1..throughput.len() { + if throughput[i - 1] > 0.0 { + gains[i] = throughput[i] / throughput[i - 1] - 1.0; + } + } + let mut plateau = None; + for i in 2..gains.len() { + if gains[i - 1] < 0.05 && gains[i] < 0.05 { + plateau = Some(i); + break; + } + } + (gains, plateau) + } + + // ═════════════════════════════════════════════════════════════════════ + // §4 — write_vectored looped over partial writes (real syscall counting; + // plan: "partial vectored writes loop and are counted"). + // ═════════════════════════════════════════════════════════════════════ + + /// Write ALL of `bufs` via `File::write_vectored`, looping on short + /// writes (a short/partial vectored write is a REAL possibility — kernel + /// `IOV_MAX`, a signal interruption, or a filesystem that just doesn't + /// hand back everything in one call). Returns `(bytes_written, + /// syscalls_issued)` — `syscalls_issued` is the actual number of + /// `write_vectored` calls made, not an assumed 1. + fn write_vectored_all( + file: &mut File, + mut bufs: &mut [IoSlice<'_>], + ) -> std::io::Result<(u64, u64)> { + let mut total = 0u64; + let mut syscalls = 0u64; + while !bufs.is_empty() { + match file.write_vectored(bufs) { + Ok(0) => { + return Err(std::io::Error::new( + std::io::ErrorKind::WriteZero, + "write_vectored returned 0 with buffers remaining", + )); + } + Ok(n) => { + syscalls += 1; + total += n as u64; + IoSlice::advance_slices(&mut bufs, n); + } + Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue, + Err(e) => return Err(e), + } + } + Ok((total, syscalls)) + } + + // ═════════════════════════════════════════════════════════════════════ + // §5 — B0: DummyOwner cast baseline (no SoA, no temporal, no file I/O). + // ═════════════════════════════════════════════════════════════════════ + + #[derive(Clone, Copy)] + struct DummyOwner { + owner_id: MailboxId, + phase: KanbanColumn, + cycle: u32, + } + + #[derive(Clone, Copy, Default)] + struct PhaseMedians { + build_ns: u64, + scan_ns: u64, + think_ns: u64, + cast_ns: u64, + collect_ns: u64, + freeze_ns: u64, + apply_ns: u64, + /// Process-monotonic high-water mark. NEVER differenced across arms. + peak_rss_bytes: u64, + /// VmRSS AFTER this arm's allocation minus VmRSS BEFORE it — the only + /// honest per-arm footprint inside one process. + rss_delta_bytes: i64, + } + + fn median_phases(samples: &[PhaseMedians]) -> PhaseMedians { + let col = |f: fn(&PhaseMedians) -> u64| median(&samples.iter().map(f).collect::>()); + let mut deltas: Vec = samples.iter().map(|s| s.rss_delta_bytes).collect(); + deltas.sort_unstable(); + let rss_delta_bytes = deltas.get(deltas.len() / 2).copied().unwrap_or(0); + PhaseMedians { + build_ns: col(|p| p.build_ns), + scan_ns: col(|p| p.scan_ns), + think_ns: col(|p| p.think_ns), + cast_ns: col(|p| p.cast_ns), + collect_ns: col(|p| p.collect_ns), + freeze_ns: col(|p| p.freeze_ns), + apply_ns: col(|p| p.apply_ns), + peak_rss_bytes: col(|p| p.peak_rss_bytes), + rss_delta_bytes, + } + } + + fn run_b0(csv: &mut CsvSink) -> PhaseMedians { + eprintln!("\n== B0 — DummyOwner cast baseline (owner lookup, write-on-behalf rebind, CastId allocation, staging, collect) =="); + let style_outcome = build_style_outcome(); + let mut samples = Vec::new(); + + for repeat in 0..3u32 { + // VmRSS BEFORE this repeat's fleet allocation (the delta baseline). + let rss_before = read_proc_status().1 as i64 * 1024; + let t_build = Instant::now(); + let mut fleet: HashMap = + HashMap::with_capacity(FLEET_OWNERS as usize); + for id in 0..FLEET_OWNERS { + fleet.insert( + id, + DummyOwner { + owner_id: id, + phase: KanbanColumn::Planning, + cycle: 0, + }, + ); + } + let build_ns = t_build.elapsed().as_nanos() as u64; + + let t_scan = Instant::now(); + let mut in_planning = 0usize; + for owner in fleet.values() { + if owner.phase == KanbanColumn::Planning { + in_planning += 1; + } + } + let scan_ns = t_scan.elapsed().as_nanos() as u64; + assert_eq!( + in_planning, FLEET_OWNERS as usize, + "B0: every dummy owner starts Planning" + ); + + // "fixed dummy thought": the ONE style outcome (built above, + // outside every repeat's timing) IS the thought — B0 measures + // no per-owner thinking cost by design (that axis belongs to + // B1a's think_ns, which computes a real per-owner gate). + let think_ns = 0u64; + + let t_cast = Instant::now(); + let mut writer: BatchWriter> = BatchWriter::new(); + let mut cast = 0usize; + for id in 0..FLEET_OWNERS { + let owner = fleet.get(&id).expect("owner exists"); + let payload = id.to_le_bytes().to_vec(); + if emit_bootstrap_intent( + &style_outcome, + owner.owner_id, + owner.cycle, + &mut writer, + payload, + ) + .is_some() + { + cast += 1; + } + } + let cast_ns = t_cast.elapsed().as_nanos() as u64; + assert_eq!( + cast, FLEET_OWNERS as usize, + "B0 can-fire: every dummy owner casts" + ); + + let t_collect = Instant::now(); + let collected = + collect_casts(&mut writer, CycleId(u64::from(repeat) + 1), 0, u64::from); + let collect_ns = t_collect.elapsed().as_nanos() as u64; + assert_eq!(collected.slots.len(), FLEET_OWNERS as usize); + assert!( + collected.held.is_empty(), + "B0: one move per owner, nothing held" + ); + + // Everything the freeze call does NOT do (framing, counting, + // digesting) happens OUTSIDE the timed window, so `freeze_ns` + // measures exactly `DetachedCycleBatch::freeze`, nothing else. + let frame = CycleFrame::new(CycleId(u64::from(repeat) + 1), DatasetVersion(0)); + let sealed_count = collected + .slots + .iter() + .filter(|s| s.paired_move.is_some()) + .count(); + let digest_bytes: Vec = collected + .slots + .iter() + .flat_map(|s| s.payload.iter().copied()) + .collect(); + let t_freeze = Instant::now(); + let frozen = DetachedCycleBatch::freeze(frame, collected.slots); + let freeze_ns = t_freeze.elapsed().as_nanos() as u64; + assert_eq!( + frozen.image.len(), + FLEET_OWNERS as usize, + "B0: one coalesced row per owner" + ); + + let total_ns = build_ns + scan_ns + think_ns + cast_ns + collect_ns + freeze_ns; + let snap = proc_snapshot(); + let digest = fnv1a64(&digest_bytes); + + samples.push(PhaseMedians { + build_ns, + scan_ns, + think_ns, + cast_ns, + collect_ns, + freeze_ns, + apply_ns: 0, + peak_rss_bytes: snap.vmhwm_kb * 1024, + rss_delta_bytes: (snap.vmrss_kb as i64 * 1024) - rss_before, + }); + + eprintln!( + "B0 repeat {repeat}: build={build_ns}ns scan={scan_ns}ns cast={cast_ns}ns collect={collect_ns}ns freeze={freeze_ns}ns total={total_ns}ns" + ); + csv.write(&Row { + owner_shape: "b0_dummy_owner", + physical_layout: "none", + threads: 1, + segment_rows: 0, + segment_bytes: 0, + segments_per_cycle: 0, + repeat, + build_ns, + scan_ns, + think_ns, + rebind_cast_ns: cast_ns, + collect_ns, + freeze_ns, + wal_write_ns: 0, + wal_sync_ns: 0, + temporal_layer1_ns: 0, + temporal_layer2_ns: 0, + apply_ns: 0, + total_ns, + logical_rows: FLEET_OWNERS as u64, + logical_bytes: (FLEET_OWNERS as u64) * 4, // MailboxId=u32 -> to_le_bytes() is 4 bytes + sealed_transitions: sealed_count as u64, + applied_transitions: 0, + wal_syscalls: 0, + fsync_calls: 0, + dataset_versions: 0, + peak_rss_bytes: snap.vmhwm_kb * 1024, + minor_faults: snap.minflt, + major_faults: snap.majflt, + context_switches: snap.vol_ctxt + snap.nonvol_ctxt, + max_active_workers: 1, + result_digest: digest, + morton_reorder_ns: 0, + }); + } + + median_phases(&samples) + } + + // ═════════════════════════════════════════════════════════════════════ + // §6 — B1a: 65,536 × MailboxSoA<4> (the actual hot runtime owner). + // ═════════════════════════════════════════════════════════════════════ + + const ROWS_PER_OWNER_B1A: usize = 4; + type TenantB1a = MailboxSoA; + + /// Build + seed one real owner — `probe_ignition_64k.rs:210-228` + /// provenance (one populated row, one firing row, w_slot 0, threshold 1.0). + fn build_tenant(id: MailboxId) -> MailboxSoA { + let mut owner: MailboxSoA = MailboxSoA::new(id, 0, 1.0); + let cycle = owner.cycle(); + let plane = splitmix_plane(u64::from(id)); + let cell = WriteCell { + content: Some(plane.as_slice()), + qualia: Some(flow_qualia()), + meta: Some(MetaWord::new(1, 0, 0, 0, 0)), // armed z=1 (Analytical) + entity_type: Some((id % 251) as u16), + temporal: Some(u64::from(id)), + ..WriteCell::default() + }; + let outcome = owner.write_row(0, cycle, &cell); + assert_eq!(outcome, WriteOutcome::Accepted, "seeding tenant {id}"); + owner.set_populated(1); + owner.tick(); + owner.energy[0] = 2.0; // one firing row: exhausts after one advance + owner + } + + /// Build a `DriverSealedCycle` directly from a frozen cast set — the same + /// transitions/next_position_base extraction `cycle_driver::seal_cycle` + /// performs (`cycle_driver.rs:286-301`), but WITHOUT going through an + /// actual `WalSink::commit_cycle`. This arm measures the collect/freeze + /// and apply PHASES; the real WAL commit is the WAL-curve arm's job + /// (§8/§9 below) — coupling B1a's apply timing to a real fsync would + /// blend two axes the plan explicitly keeps apart. + fn build_sealed_locally( + frame: CycleFrame, + slots: &[SweepSlot], + version: DatasetVersion, + ) -> DriverSealedCycle { + let mut transitions: Vec = slots + .iter() + .filter_map(|s| { + s.paired_move.map(|mv| SealedTransition { + stream_position: s.stream_position, + owner: s.owner, + mv, + }) + }) + .collect(); + transitions.sort_by_key(|t| t.stream_position); + let next_position_base = slots + .iter() + .map(|s| s.stream_position + 1) + .max() + .unwrap_or(0); + let _ = frame; // frame carried by the caller's own bookkeeping only + DriverSealedCycle { + version, + transitions, + next_position_base, + } + } + + fn run_b1a(csv: &mut CsvSink) -> PhaseMedians { + eprintln!("\n== B1a — 65,536 x MailboxSoA<4> (the actual hot runtime owner) =="); + let style_outcome = build_style_outcome(); + let mut samples = Vec::new(); + + for repeat in 0..3u32 { + // VmRSS BEFORE this repeat's allocation (the delta baseline). + let rss_before = read_proc_status().1 as i64 * 1024; + let t_build = Instant::now(); + let mut fleet: HashMap = + HashMap::with_capacity(FLEET_OWNERS as usize); + for id in 0..FLEET_OWNERS { + fleet.insert(id, build_tenant::(id)); + } + let build_ns = t_build.elapsed().as_nanos() as u64; + + let t_scan = Instant::now(); + let mut in_planning = 0usize; + for owner in fleet.values() { + if owner.phase() == KanbanColumn::Planning { + in_planning += 1; + } + } + let scan_ns = t_scan.elapsed().as_nanos() as u64; + assert_eq!(in_planning, FLEET_OWNERS as usize); + + // real per-owner gate decision (unlike B0's precomputed thought). + let t_think = Instant::now(); + let mut targets: Vec = Vec::with_capacity(FLEET_OWNERS as usize); + for id in 0..FLEET_OWNERS { + let owner = fleet.get(&id).expect("owner exists"); + let qualia = owner.qualia_at(0); + let mantissa = mantissa_of(owner); + let gate = gate_decision_i4(&qualia, mantissa); + let target = owner + .phase() + .advance_on_gate(&gate) + .expect("B1a: every owner Flows (flow qualia + firing row)"); + targets.push(target); + } + let think_ns = t_think.elapsed().as_nanos() as u64; + + let t_cast = Instant::now(); + let mut writer: BatchWriter> = BatchWriter::new(); + let mut cast = 0usize; + for id in 0..FLEET_OWNERS { + let owner = fleet.get(&id).expect("owner exists"); + let payload = id.to_le_bytes().to_vec(); + if emit_bootstrap_intent( + &style_outcome, + owner.mailbox_id(), + owner.current_cycle(), + &mut writer, + payload, + ) + .is_some() + { + cast += 1; + } + } + let cast_ns = t_cast.elapsed().as_nanos() as u64; + assert_eq!( + cast, FLEET_OWNERS as usize, + "B1a can-fire: every owner casts" + ); + + let t_collect = Instant::now(); + let collected = + collect_casts(&mut writer, CycleId(u64::from(repeat) + 1), 0, u64::from); + let collect_ns = t_collect.elapsed().as_nanos() as u64; + assert_eq!(collected.slots.len(), FLEET_OWNERS as usize); + + // `build_sealed_locally` (a sort over 65,536 transitions) and the + // digest bytes are computed OUTSIDE the timed window so + // `freeze_ns` measures exactly `DetachedCycleBatch::freeze`. + let frame = CycleFrame::new(CycleId(u64::from(repeat) + 1), DatasetVersion(0)); + let sealed = build_sealed_locally(frame, &collected.slots, DatasetVersion(1)); + let digest_bytes: Vec = collected + .slots + .iter() + .flat_map(|s| s.payload.iter().copied()) + .collect(); + let t_freeze = Instant::now(); + let frozen = DetachedCycleBatch::freeze(frame, collected.slots); + let freeze_ns = t_freeze.elapsed().as_nanos() as u64; + assert_eq!(frozen.image.len(), FLEET_OWNERS as usize); + assert_eq!( + sealed.transitions.len(), + FLEET_OWNERS as usize, + "B1a: every owner's move sealed (sparse == full here)" + ); + + let t_apply = Instant::now(); + let mut watermarks: HashMap> = HashMap::new(); + let applied = apply_sealed_transitions(&mut fleet, &sealed, &mut watermarks) + .expect("B1a: apply must succeed against freshly-Planning owners"); + let apply_ns = t_apply.elapsed().as_nanos() as u64; + assert_eq!( + applied.applied.len(), + FLEET_OWNERS as usize, + "B1a: all 65,536 advanced" + ); + + let total_ns = + build_ns + scan_ns + think_ns + cast_ns + collect_ns + freeze_ns + apply_ns; + let snap = proc_snapshot(); + let digest = fnv1a64(&digest_bytes); + + samples.push(PhaseMedians { + build_ns, + scan_ns, + think_ns, + cast_ns, + collect_ns, + freeze_ns, + apply_ns, + peak_rss_bytes: snap.vmhwm_kb * 1024, + rss_delta_bytes: (snap.vmrss_kb as i64 * 1024) - rss_before, + }); + + eprintln!( + "B1a repeat {repeat}: build={build_ns}ns scan={scan_ns}ns think={think_ns}ns cast={cast_ns}ns collect={collect_ns}ns freeze={freeze_ns}ns apply={apply_ns}ns total={total_ns}ns peak_rss={}B", + snap.vmhwm_kb * 1024 + ); + csv.write(&Row { + owner_shape: "b1a_mailboxsoa4", + physical_layout: "owner_exclusive_65536", + threads: 1, + segment_rows: 0, + segment_bytes: 0, + segments_per_cycle: 0, + repeat, + build_ns, + scan_ns, + think_ns, + rebind_cast_ns: cast_ns, + collect_ns, + freeze_ns, + wal_write_ns: 0, + wal_sync_ns: 0, + temporal_layer1_ns: 0, + temporal_layer2_ns: 0, + apply_ns, + total_ns, + logical_rows: FLEET_OWNERS as u64, + logical_bytes: (FLEET_OWNERS as u64) * 4, // MailboxId=u32 -> to_le_bytes() is 4 bytes + sealed_transitions: sealed.transitions.len() as u64, + applied_transitions: applied.applied.len() as u64, + wal_syscalls: 0, + fsync_calls: 0, + dataset_versions: 0, + peak_rss_bytes: snap.vmhwm_kb * 1024, + minor_faults: snap.minflt, + major_faults: snap.majflt, + context_switches: snap.vol_ctxt + snap.nonvol_ctxt, + max_active_workers: 1, + result_digest: digest, + morton_reorder_ns: 0, + }); + let _ = targets; // computed, consumed by the gate loop's own assertion + } + + median_phases(&samples) + } + + // ═════════════════════════════════════════════════════════════════════ + // §7 — B1b: 65,536 x NodeRow512 (the canonical, memory-only envelope). + // ═════════════════════════════════════════════════════════════════════ + + /// Returns the median VmRSS DELTA (bytes) for the canonical-envelope arm. + /// NOT a high-water mark — see `read_proc_status`'s contract. + fn run_b1b(csv: &mut CsvSink) -> i64 { + eprintln!( + "\n== B1b — 65,536 x NodeRow512 = 32 MiB canonical storage envelope (memory-only) ==" + ); + let mut rss_samples: Vec = Vec::new(); + + for repeat in 0..3u32 { + // VmRSS BEFORE this repeat's 32 MiB allocation (delta baseline). + let rss_before = read_proc_status().1 as i64 * 1024; + let t_build = Instant::now(); + let mut rows: Vec = Vec::with_capacity(FLEET_OWNERS as usize); + for id in 0..u64::from(FLEET_OWNERS) { + rows.push(NodeRow512::for_id(id)); + } + let build_ns = t_build.elapsed().as_nanos() as u64; + assert_eq!(rows.len(), FLEET_OWNERS as usize); + let logical_bytes = (rows.len() * CANONICAL_ROW_BYTES) as u64; + assert_eq!( + logical_bytes, CANONICAL_FRAME_BYTES as u64, + "B1b: 65,536 x 512B == the 32 MiB canonical frame" + ); + + let snap = proc_snapshot(); + let digest = fnv1a64(rows.last().expect("non-empty").as_bytes()); + // Hold `rows` alive PAST the RSS snapshot — the entire point of + // this arm is measuring the envelope's own resident footprint, + // not a footprint already reclaimed by drop. + std::hint::black_box(&rows); + + rss_samples.push((snap.vmrss_kb as i64 * 1024) - rss_before); + eprintln!( + "B1b repeat {repeat}: build={build_ns}ns peak_rss={}B", + snap.vmhwm_kb * 1024 + ); + csv.write(&Row { + owner_shape: "b1b_noderow512", + physical_layout: "contiguous_vec_32mib", + threads: 1, + segment_rows: 0, + segment_bytes: 0, + segments_per_cycle: 0, + repeat, + build_ns, + scan_ns: 0, + think_ns: 0, + rebind_cast_ns: 0, + collect_ns: 0, + freeze_ns: 0, + wal_write_ns: 0, + wal_sync_ns: 0, + temporal_layer1_ns: 0, + temporal_layer2_ns: 0, + apply_ns: 0, + total_ns: build_ns, + logical_rows: FLEET_OWNERS as u64, + logical_bytes, + sealed_transitions: 0, + applied_transitions: 0, + wal_syscalls: 0, + fsync_calls: 0, + dataset_versions: 0, + peak_rss_bytes: snap.vmhwm_kb * 1024, + minor_faults: snap.minflt, + major_faults: snap.majflt, + context_switches: snap.vol_ctxt + snap.nonvol_ctxt, + max_active_workers: 1, + result_digest: digest, + morton_reorder_ns: 0, + }); + } + + rss_samples.sort_unstable(); + rss_samples.get(rss_samples.len() / 2).copied().unwrap_or(0) + } + + // ═════════════════════════════════════════════════════════════════════ + // §8/§9 — the WAL curve: W1-contiguous (physics ceiling) beside + // W0-current (real SweepSlot/DetachedCycleBatch representation). + // ═════════════════════════════════════════════════════════════════════ + + #[derive(Clone, Copy)] + struct WalConfig { + segment_rows: u64, + segment_bytes: u64, + segments_per_cycle: u64, + } + + const SEGMENT_TABLE: [WalConfig; 5] = [ + WalConfig { + segment_rows: 2_048, + segment_bytes: 1024 * 1024, + segments_per_cycle: 32, + }, + WalConfig { + segment_rows: 4_096, + segment_bytes: 2 * 1024 * 1024, + segments_per_cycle: 16, + }, + WalConfig { + segment_rows: 8_192, + segment_bytes: 4 * 1024 * 1024, + segments_per_cycle: 8, + }, + WalConfig { + segment_rows: 16_384, + segment_bytes: 8 * 1024 * 1024, + segments_per_cycle: 4, + }, + WalConfig { + segment_rows: 65_536, + segment_bytes: 32 * 1024 * 1024, + segments_per_cycle: 1, + }, + ]; + + struct WalCurveSummary { + /// (segment_bytes, median_total_wal_ns) per config, W1-contiguous. + w1_points: Vec<(u64, u64)>, + plateau_segment_bytes: Option, + /// How many configs exceeded the p95/median spread ceiling. Non-zero + /// means `plateau_segment_bytes` was SUPPRESSED (unmeasurable), which + /// is a different `None` from "no knee in the table". + unstable_configs: usize, + worst_spread: f64, + } + + fn run_wal_curve(csv: &mut CsvSink) -> WalCurveSummary { + eprintln!("\n== WAL curve — W1-contiguous (storage/cache ceiling) vs W0-current (SweepSlot/BTreeMap) =="); + let wal_dir = PathBuf::from(WAL_DIR); + fs::create_dir_all(&wal_dir).expect("create WAL scratch dir"); + + // The canonical 32 MiB frame — built ONCE, shared content across + // every configuration and representation (only the I/O CHUNKING + // differs between W1/W0; the "constant 512 MiB per config" bar is + // about bytes actually handed to the OS, not about re-deriving + // content each cycle). + let canonical: Vec = { + let mut buf = vec![0u8; CANONICAL_FRAME_BYTES]; + for row in 0..u64::from(FLEET_OWNERS) { + let cell = NodeRow512::for_id(row); + let lo = (row as usize) * CANONICAL_ROW_BYTES; + buf[lo..lo + CANONICAL_ROW_BYTES].copy_from_slice(cell.as_bytes()); + } + buf + }; + assert_eq!(canonical.len(), CANONICAL_FRAME_BYTES); + + let mut w1_points = Vec::new(); + let mut w1_spreads: Vec = Vec::new(); + + for cfg in SEGMENT_TABLE { + for rep in ["w1_contiguous", "w0_current"] { + let path = wal_dir.join(format!("{rep}_{}.wal", cfg.segment_rows)); + let mut file = OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(&path) + .unwrap_or_else(|e| panic!("open WAL scratch file {path:?}: {e}")); + + let mut write_samples = Vec::with_capacity(MEASURED_CYCLES as usize); + let mut sync_samples = Vec::with_capacity(MEASURED_CYCLES as usize); + let mut byte_samples: Vec = Vec::with_capacity(MEASURED_CYCLES as usize); + let mut position_base: u64 = 0; + + for cycle_idx in 0..(WARMUP_CYCLES + MEASURED_CYCLES) { + let measured = cycle_idx >= WARMUP_CYCLES; + + let ( + collect_ns, + freeze_ns, + wal_write_ns, + wal_sync_ns, + wal_syscalls, + logical_rows, + ); + // ACTUAL bytes handed to the kernel this cycle. A first + // revision computed MiB/s from the ASSUMED 32 MiB frame + // while discarding write_vectored's real byte count — an + // assumption presented as a measurement, and the exact + // axis-blending this plan exists to prevent. + let mut bytes_written = 0u64; + + if rep == "w1_contiguous" { + collect_ns = 0u64; + freeze_ns = 0u64; + let mut total_syscalls = 0u64; + let t_write = Instant::now(); + for seg in 0..cfg.segments_per_cycle { + let lo = (seg * cfg.segment_bytes) as usize; + let hi = lo + cfg.segment_bytes as usize; + let mut slices = [IoSlice::new(&canonical[lo..hi])]; + let (written, calls) = write_vectored_all(&mut file, &mut slices) + .expect("W1 write_vectored"); + total_syscalls += calls; + bytes_written += written; + } + wal_write_ns = t_write.elapsed().as_nanos() as u64; + let t_sync = Instant::now(); + file.sync_data().expect("W1 sync_data"); + wal_sync_ns = t_sync.elapsed().as_nanos() as u64; + wal_syscalls = total_syscalls; + logical_rows = u64::from(FLEET_OWNERS); + } else { + // W0-current: REAL SweepSlot construction (one owned + // Vec clone per row — the allocator cost this arm + // measures) + a REAL DetachedCycleBatch::freeze. + let t_collect = Instant::now(); + let mut slots = Vec::with_capacity(FLEET_OWNERS as usize); + for row in 0..u64::from(FLEET_OWNERS) { + let lo = (row as usize) * CANONICAL_ROW_BYTES; + let payload = canonical[lo..lo + CANONICAL_ROW_BYTES].to_vec(); + slots.push(SweepSlot { + cycle: CycleId(u64::from(cycle_idx) + 1), + stream_position: position_base + row, + owner: row as MailboxId, + row, + paired_move: None, + payload, + }); + } + collect_ns = t_collect.elapsed().as_nanos() as u64; + position_base += u64::from(FLEET_OWNERS); + + let t_freeze = Instant::now(); + let frame = CycleFrame::new( + CycleId(u64::from(cycle_idx) + 1), + // The base version of cycle N is N (v0 before the first + // cycle). Derived from cycle_idx rather than a parallel + // counter — they were provably equal and the counter was a + // second source of truth. + DatasetVersion(u64::from(cycle_idx)), + ); + let frozen = DetachedCycleBatch::freeze(frame, slots); + freeze_ns = t_freeze.elapsed().as_nanos() as u64; + logical_rows = frozen.image.len() as u64; + assert_eq!( + logical_rows, + u64::from(FLEET_OWNERS), + "W0: every row coalesced exactly once" + ); + + let ordered: Vec<&Vec> = frozen.image.values().collect(); + let mut total_syscalls = 0u64; + let t_write = Instant::now(); + for group in ordered.chunks(cfg.segment_rows as usize) { + let mut slices: Vec> = + group.iter().map(|v| IoSlice::new(v.as_slice())).collect(); + let (written, calls) = write_vectored_all(&mut file, &mut slices) + .expect("W0 write_vectored"); + total_syscalls += calls; + bytes_written += written; + } + wal_write_ns = t_write.elapsed().as_nanos() as u64; + let t_sync = Instant::now(); + file.sync_data().expect("W0 sync_data"); + wal_sync_ns = t_sync.elapsed().as_nanos() as u64; + wal_syscalls = total_syscalls; + } + + assert_eq!( + bytes_written, CANONICAL_FRAME_BYTES as u64, + "{rep} @cycle {cycle_idx}: an arm that does not move exactly the \ + canonical frame cannot be compared against one that does" + ); + + if measured { + write_samples.push(wal_write_ns); + sync_samples.push(wal_sync_ns); + byte_samples.push(bytes_written); + + let snap = proc_snapshot(); + // Content is constant per config (the canonical frame + // never changes across cycles here — documented in + // the build report); the digest is a stable checksum + // over a fixed slice, not a per-cycle claim. + let digest = fnv1a64(&canonical[..64]); + let total_ns = collect_ns + freeze_ns + wal_write_ns + wal_sync_ns; + csv.write(&Row { + owner_shape: "canonical_65536", + physical_layout: rep, + threads: 1, + segment_rows: cfg.segment_rows, + segment_bytes: cfg.segment_bytes, + segments_per_cycle: cfg.segments_per_cycle, + repeat: cycle_idx, + build_ns: 0, + scan_ns: 0, + think_ns: 0, + rebind_cast_ns: 0, + collect_ns, + freeze_ns, + wal_write_ns, + wal_sync_ns, + temporal_layer1_ns: 0, + temporal_layer2_ns: 0, + apply_ns: 0, + total_ns, + logical_rows, + logical_bytes: bytes_written, + sealed_transitions: 0, + applied_transitions: 0, + wal_syscalls, + fsync_calls: 1, + dataset_versions: 1, + peak_rss_bytes: snap.vmhwm_kb * 1024, + minor_faults: snap.minflt, + major_faults: snap.majflt, + context_switches: snap.vol_ctxt + snap.nonvol_ctxt, + max_active_workers: 1, + result_digest: digest, + morton_reorder_ns: 0, + }); + } + } + + let med_write = median(&write_samples); + let p95_write = p95(&write_samples); + let med_sync = median(&sync_samples); + let med_total = med_write + med_sync; + let med_bytes = median(&byte_samples); + let mib_per_s = if med_total > 0 { + (med_bytes as f64 / (1024.0 * 1024.0)) / (med_total as f64 / 1e9) + } else { + 0.0 + }; + eprintln!( + "WAL {rep} segment_bytes={} segments/cycle={}: median write={med_write}ns p95={p95_write}ns sync={med_sync}ns ({mib_per_s:.1} MiB/s)", + cfg.segment_bytes, cfg.segments_per_cycle + ); + + if rep == "w1_contiguous" { + w1_points.push((cfg.segment_bytes, med_total)); + // Instability marker: a config whose p95 write dwarfs its + // median is being driven by page-cache / dirty-writeback + // state, not by segment size. + let spread = if med_write > 0 { + p95_write as f64 / med_write as f64 + } else { + 0.0 + }; + w1_spreads.push(spread); + } + + // Reclaim this configuration's scratch file IMMEDIATELY. Each + // config appends (WARMUP+MEASURED) x 32 MiB = 576 MiB; keeping + // all ten alive until the end needs ~5.8 GiB and hit ENOSPC on + // the first real run. Dropping the handle first so the unlink + // frees the blocks now rather than at scope exit. + drop(file); + fs::remove_file(&path).ok(); + } + } + + fs::remove_dir_all(&wal_dir).ok(); + + let throughput: Vec = w1_points + .iter() + .map(|&(_, ns)| { + if ns > 0 { + (CANONICAL_FRAME_BYTES as f64 / (1024.0 * 1024.0)) / (ns as f64 / 1e9) + } else { + 0.0 + } + }) + .collect(); + let (gains, plateau_idx) = plateau_index(&throughput); + eprintln!("WAL curve gains (W1-contiguous, by segment_bytes):"); + for (i, cfg) in SEGMENT_TABLE.iter().enumerate() { + let gain_str = if gains[i].is_nan() { + "n/a".to_string() + } else { + format!("{:+.1}%", gains[i] * 100.0) + }; + eprintln!( + " segment_bytes={:>9} throughput={:.1} MiB/s gain={}", + cfg.segment_bytes, throughput[i], gain_str + ); + } + // ── STABILITY GUARD (added after four runs of this binary disagreed + // by up to 6x at IDENTICAL configurations, and one config showed a + // p95/median spread of 24x). A knee named from data whose run-to-run + // variance exceeds the effect is fabricated precision — exactly what + // this plan exists to prevent. The guard has both halves: it FIRES + // (suppressing the knee) when any config is unstable, and it STAYS + // SILENT (reporting the knee) when every config is tight. + const SPREAD_CEILING: f64 = 3.0; + let worst_spread = w1_spreads.iter().copied().fold(0.0_f64, f64::max); + let unstable: Vec = w1_spreads + .iter() + .enumerate() + .filter(|(_, s)| **s > SPREAD_CEILING) + .map(|(i, _)| i) + .collect(); + + let plateau_segment_bytes = if unstable.is_empty() { + plateau_idx.map(|i| SEGMENT_TABLE[i].segment_bytes) + } else { + None + }; + + if unstable.is_empty() { + match plateau_segment_bytes { + Some(b) => eprintln!( + "WAL curve plateau (descriptive, NOT pass/kill): first knee at segment_bytes={b} \ + (two consecutive doublings < 5% median-throughput gain; worst p95/median spread \ + {worst_spread:.1}x <= {SPREAD_CEILING:.0}x ceiling, so the curve is stable enough to read)" + ), + None => eprintln!( + "WAL curve plateau: no knee within the 5-point table (still gaining at every step)" + ), + } + } else { + eprintln!( + "WAL curve plateau: NOT MEASURABLE ON THIS HOST — {} of {} configs exceed the \ + p95/median spread ceiling ({SPREAD_CEILING:.0}x); worst {worst_spread:.1}x. The \ + write phase is being driven by page-cache / dirty-writeback state rather than by \ + segment size, so NO knee is reported. Re-run on a quiet host with headroom (this \ + one was ~90% full) and O_DIRECT or a drop_caches barrier per config.", + unstable.len(), + w1_spreads.len() + ); + } + + WalCurveSummary { + w1_points, + plateau_segment_bytes, + unstable_configs: unstable.len(), + worst_spread, + } + } + + // ═════════════════════════════════════════════════════════════════════ + // §10 — a real, in-process `WalSink` for the Temporal + EXP-KIA-A2-64K + // arms (no raw-file I/O here — those arms measure `temporal.rs` and + // concurrency, not fsync physics; the WAL-curve arm above owns physics). + // ═════════════════════════════════════════════════════════════════════ + + struct SealedEntry { + version: DatasetVersion, + landings: Vec, + } + + struct MemWal { + sealed: Mutex>, + next_version: AtomicU64, + wal_writes: AtomicU64, + } + + impl MemWal { + fn new() -> Self { + Self { + sealed: Mutex::new(Vec::new()), + next_version: AtomicU64::new(1), + wal_writes: AtomicU64::new(0), + } + } + fn wal_writes(&self) -> u64 { + self.wal_writes.load(Ordering::SeqCst) + } + fn head(&self) -> DatasetVersion { + self.sealed + .lock() + .expect("MemWal poisoned") + .last() + .map_or(DatasetVersion(0), |s| s.version) + } + } + + impl WalSink for MemWal { + async fn commit_cycle( + &self, + base: DatasetVersion, + batch: DetachedCycleBatch, + ) -> Result { + let mut sealed = self.sealed.lock().expect("MemWal poisoned"); + let head = sealed.last().map_or(DatasetVersion(0), |s| s.version); + if base != head { + return Err(WriteFailed(format!( + "stale base {base:?}: sealed head is {head:?}" + ))); + } + self.wal_writes.fetch_add(1, Ordering::SeqCst); + let version = DatasetVersion(self.next_version.fetch_add(1, Ordering::SeqCst)); + sealed.push(SealedEntry { + version, + landings: batch.landings, + }); + Ok(version) + } + + async fn scan_sealed( + &self, + from_version: Option, + ) -> Result, WriteFailed> { + Ok(self + .sealed + .lock() + .expect("MemWal poisoned") + .iter() + .filter(|s| from_version.is_none_or(|f| s.version > f)) + .flat_map(|s| { + s.landings.iter().map(|slot| LandedSlot { + version: s.version, + slot: slot.clone(), + }) + }) + .collect()) + } + + async fn versions(&self) -> Result, WriteFailed> { + Ok(self + .sealed + .lock() + .expect("MemWal poisoned") + .iter() + .map(|s| { + ( + s.landings.first().map_or(CycleId(0), |l| l.cycle), + s.version, + ) + }) + .collect()) + } + } + + // ═════════════════════════════════════════════════════════════════════ + // §11 — Temporal: T0 (scan_sealed) · T1 (local_trajectories) · + // T2 (deinterlace), over 65,536 owners x 16 real committed cycles = + // 1,048,576 rows. + // ═════════════════════════════════════════════════════════════════════ + + #[derive(Clone)] + struct BenchRow { + owner: MailboxId, + cast_seq: u64, + lance_version: u64, + } + impl LocalCausalRow for BenchRow { + fn owner(&self) -> MailboxId { + self.owner + } + fn cast_seq(&self) -> u64 { + self.cast_seq + } + } + impl DeinterlaceRow for BenchRow { + fn subject(&self) -> &str { + // NoDeps ignores subject entirely (`closure_at` never reads it) — + // a real DependsClosure impl would need a real subject string; + // this arm exercises the trivial DATA-causal axis only. + "" + } + fn lance_version(&self) -> u64 { + self.lance_version + } + fn knowable_from(&self) -> u64 { + 0 // every class knowable from the start — this arm isolates the + // TIME-causal axis, not the class-registration axis. + } + } + + async fn run_temporal(csv: &mut CsvSink) { + eprintln!("\n== Temporal — T0 scan_sealed / T1 local_trajectories / T2 deinterlace (post-WAL only) =="); + + // Build 16 REAL committed cycles of 65,536 no-step landings each + // (`SweepSlot::paired_move = None` — a sanctioned landing shape per + // `persist_sink.rs:145-147`'s own doc). Real `persist_cycle` calls + // against a real (in-process) `WalSink`, not a fabricated Vec. + let sink = MemWal::new(); + let mut position_base: u64 = 0; + for cyc in 1..=16u64 { + let mut writer: BatchWriter> = BatchWriter::new(); + for id in 0..FLEET_OWNERS { + writer.cast(id, vec![], id.to_le_bytes().to_vec()); + } + let collected = collect_casts(&mut writer, CycleId(cyc), position_base, u64::from); + assert_eq!(collected.slots.len(), FLEET_OWNERS as usize); + let base = sink.head(); + let frame = CycleFrame::new(CycleId(cyc), base); + persist_cycle(&sink, frame, collected.slots) + .await + .unwrap_or_else(|e| panic!("temporal history: cycle {cyc} failed to seal: {e}")); + position_base += u64::from(FLEET_OWNERS); + } + assert_eq!( + sink.wal_writes(), + 16, + "16 real sealed cycles built the history" + ); + + // T0 — the sealed WAL read (this arm's "scan_ns" mapping — the CSV + // schema has no dedicated T0 column; `scan_ns` is the closest-named + // generic slot and is used ONLY here for that purpose, documented + // in the build report). + let t0 = Instant::now(); + let landed: Vec = sink + .scan_sealed(None) + .await + .expect("T0: scan_sealed over 16 sealed cycles"); + let t0_ns = t0.elapsed().as_nanos() as u64; + assert_eq!( + landed.len(), + FLEET_OWNERS as usize * 16, + "T0: 65,536 owners x 16 landings = 1,048,576 rows" + ); + + let bench_rows: Vec = landed + .iter() + .map(|ls| BenchRow { + owner: ls.slot.owner, + cast_seq: ls.slot.stream_position, + lance_version: ls.version.0, + }) + .collect(); + + // T1 — layer-1 causal deinterlacing: split the interleaved global + // log into 65,536 per-owner local trajectories. + let t1 = Instant::now(); + let trajectories = local_trajectories(&bench_rows); + let t1_ns = t1.elapsed().as_nanos() as u64; + assert_eq!( + trajectories.len(), + FLEET_OWNERS as usize, + "T1: exactly 65,536 distinct owner trajectories" + ); + for chain in trajectories.values().take(8) { + assert_eq!( + chain.len(), + 16, + "T1: each owner's chain has all 16 landings" + ); + } + + // T2 — layer-2 epistemic projection. `ref_version = 8` (MID-history, + // not the last cycle) so the filter is genuinely falsifiable: cycles + // 1..=8 are Contemporary, 9..=16 are Anachronistic under Strict and + // are DROPPED — never a vacuous "everything survives" check. + let v_ref = QueryReference::at(8, 0); + let t2 = Instant::now(); + let visible = deinterlace(&bench_rows, &v_ref, &NoDeps); + let t2_ns = t2.elapsed().as_nanos() as u64; + let expected_visible = FLEET_OWNERS as usize * 8; + assert_eq!( + visible.len(), + expected_visible, + "T2 anti-vacuity: exactly the first 8 of 16 cycles survive a Strict reader at ref_version=8" + ); + assert!( + visible.len() < bench_rows.len(), + "T2 anti-vacuity: the filter actually dropped rows (future cycles), not merely 'ran'" + ); + + eprintln!( + "Temporal: T0 scan_sealed={t0_ns}ns ({} rows) | T1 local_trajectories={t1_ns}ns ({} owners) | T2 deinterlace={t2_ns}ns ({} of {} visible)", + landed.len(), + trajectories.len(), + visible.len(), + bench_rows.len() + ); + + let owner_keys: Vec = trajectories.keys().flat_map(|k| k.to_le_bytes()).collect(); + let digest = fnv1a64(&owner_keys); + + csv.write(&Row { + owner_shape: "temporal_1048576", + physical_layout: "local_trajectories_then_deinterlace", + threads: 1, + segment_rows: 0, + segment_bytes: 0, + segments_per_cycle: 0, + repeat: 0, + build_ns: 0, + scan_ns: t0_ns, // T0 — see the doc comment above the field write + think_ns: 0, + rebind_cast_ns: 0, + collect_ns: 0, + freeze_ns: 0, + wal_write_ns: 0, + wal_sync_ns: 0, + temporal_layer1_ns: t1_ns, + temporal_layer2_ns: t2_ns, + apply_ns: 0, + total_ns: t0_ns + t1_ns + t2_ns, + logical_rows: landed.len() as u64, + // Honest payload size: the temporal history's landings carry an + // 4-byte owner-id marker each (`id.to_le_bytes()`, MailboxId=u32), NOT a + // canonical 512-byte row — this arm exercises `temporal.rs`'s + // row-shape-agnostic API, not the storage envelope (that's B1b's + // and the WAL curve's job). + logical_bytes: landed.len() as u64 * 4, // MailboxId=u32 -> to_le_bytes() is 4 bytes + sealed_transitions: 0, + applied_transitions: 0, + wal_syscalls: 0, + fsync_calls: 0, + dataset_versions: 16, + peak_rss_bytes: proc_snapshot().vmhwm_kb * 1024, + minor_faults: 0, + major_faults: 0, + context_switches: 0, + max_active_workers: 1, + result_digest: digest, + morton_reorder_ns: 0, + }); + } + + // ═════════════════════════════════════════════════════════════════════ + // §12 — L1: ChunkedSoA<1024>[64] physical-layout control. + // + // Honest scoping note (see build report for the full reasoning): a + // Rubicon `phase()` is scoped to ONE `MailboxSoA` instance, never to + // an individual row. L1a's 65,536 "logical owners" share 64 physical + // `MailboxSoA<1024>` instances, so there is no per-logical-owner phase + // to advance — `apply_sealed_transitions` (which resolves a transition's + // owner to ONE `MailboxSoaOwner` and advances ITS phase) does not have a + // meaningful target at logical-owner granularity here. Rather than + // fabricate a per-row "apply" by routing 1,024 logical owners' moves + // through one chunk's single phase field (which would either silently + // misrepresent 1,023 of every 1,024 owners, or require inventing a new + // per-row phase type this brief forbids), this arm measures build, + // scan, think, cast, collect, and freeze ONLY — the phases that ARE + // meaningful at row granularity — and apply_ns is left 0 for both L1a + // and L1b, documented here so the comparison against B1a stays honest + // (answer #2 compares build..freeze only, explicitly, not apply). + // ═════════════════════════════════════════════════════════════════════ + + const CHUNK_ROWS: usize = 1024; + const CHUNKS: u32 = 64; + type Chunk = MailboxSoA; + + fn build_chunk(chunk_idx: u32) -> Chunk { + let mut chunk: Chunk = MailboxSoA::new(chunk_idx, 0, 1.0); + let cycle = chunk.cycle(); + for lane in 0..CHUNK_ROWS { + let logical_owner = chunk_idx * CHUNK_ROWS as u32 + lane as u32; + let plane = splitmix_plane(u64::from(logical_owner)); + let cell = WriteCell { + content: Some(plane.as_slice()), + qualia: Some(flow_qualia()), + meta: Some(MetaWord::new(1, 0, 0, 0, 0)), + entity_type: Some((logical_owner % 251) as u16), + temporal: Some(u64::from(logical_owner)), + ..WriteCell::default() + }; + let outcome = chunk.write_row(lane, cycle, &cell); + assert_eq!( + outcome, + WriteOutcome::Accepted, + "seeding chunk {chunk_idx} lane {lane}" + ); + } + chunk.set_populated(CHUNK_ROWS); + chunk.tick(); + for lane in 0..CHUNK_ROWS { + chunk.energy[lane] = 2.0; + } + chunk + } + + fn run_l1a(csv: &mut CsvSink) -> PhaseMedians { + eprintln!("\n== L1a — 64 x MailboxSoA<1024> chunks, 65,536 LOGICAL owners (owner = chunk*1024+lane) =="); + let style_outcome = build_style_outcome(); + let mut samples = Vec::new(); + + for repeat in 0..3u32 { + // VmRSS BEFORE this repeat's allocation (the delta baseline). + let rss_before = read_proc_status().1 as i64 * 1024; + let t_build = Instant::now(); + let mut chunks: HashMap = HashMap::with_capacity(CHUNKS as usize); + for c in 0..CHUNKS { + chunks.insert(c, build_chunk(c)); + } + let build_ns = t_build.elapsed().as_nanos() as u64; + + let t_scan = Instant::now(); + let mut in_planning_chunks = 0usize; + for chunk in chunks.values() { + if chunk.phase() == KanbanColumn::Planning { + in_planning_chunks += 1; + } + } + let scan_ns = t_scan.elapsed().as_nanos() as u64; + assert_eq!( + in_planning_chunks, CHUNKS as usize, + "L1a: every chunk starts Planning" + ); + + // "thought": one gate decision PER CHUNK (phase is chunk-scoped — + // every logical owner in a chunk shares its chunk's gate target, + // an accepted physical-layout-control simplification, documented + // above and in the build report). + let t_think = Instant::now(); + let mut chunk_targets: HashMap = + HashMap::with_capacity(CHUNKS as usize); + for c in 0..CHUNKS { + let chunk = chunks.get(&c).expect("chunk exists"); + let qualia = chunk.qualia_at(0); + let mantissa = mantissa_of(chunk); + let gate = gate_decision_i4(&qualia, mantissa); + let target = chunk + .phase() + .advance_on_gate(&gate) + .expect("L1a: every chunk Flows"); + chunk_targets.insert(c, target); + } + let think_ns = t_think.elapsed().as_nanos() as u64; + + let t_cast = Instant::now(); + let mut writer: BatchWriter> = BatchWriter::new(); + let mut cast = 0usize; + for c in 0..CHUNKS { + let chunk = chunks.get(&c).expect("chunk exists"); + for lane in 0..CHUNK_ROWS as u32 { + let logical_owner = c * CHUNK_ROWS as u32 + lane; + let payload = logical_owner.to_le_bytes().to_vec(); + if emit_bootstrap_intent( + &style_outcome, + logical_owner, + chunk.current_cycle(), + &mut writer, + payload, + ) + .is_some() + { + cast += 1; + } + } + } + let cast_ns = t_cast.elapsed().as_nanos() as u64; + assert_eq!( + cast, FLEET_OWNERS as usize, + "L1a can-fire: all 65,536 logical owners cast" + ); + + let t_collect = Instant::now(); + let collected = + collect_casts(&mut writer, CycleId(u64::from(repeat) + 1), 0, u64::from); + let collect_ns = t_collect.elapsed().as_nanos() as u64; + assert_eq!(collected.slots.len(), FLEET_OWNERS as usize); + assert!( + collected.held.is_empty(), + "L1a: distinct logical owner ids -> nothing held (unlike a chunk-keyed collapse)" + ); + + // digest_bytes computed OUTSIDE the timed window — freeze_ns + // measures exactly `DetachedCycleBatch::freeze`. + let frame = CycleFrame::new(CycleId(u64::from(repeat) + 1), DatasetVersion(0)); + let digest_bytes: Vec = collected + .slots + .iter() + .flat_map(|s| s.payload.iter().copied()) + .collect(); + let t_freeze = Instant::now(); + let frozen = DetachedCycleBatch::freeze(frame, collected.slots); + let freeze_ns = t_freeze.elapsed().as_nanos() as u64; + assert_eq!(frozen.image.len(), FLEET_OWNERS as usize); + + let total_ns = build_ns + scan_ns + think_ns + cast_ns + collect_ns + freeze_ns; + let snap = proc_snapshot(); + let digest = fnv1a64(&digest_bytes); + + samples.push(PhaseMedians { + build_ns, + scan_ns, + think_ns, + cast_ns, + collect_ns, + freeze_ns, + apply_ns: 0, + peak_rss_bytes: snap.vmhwm_kb * 1024, + rss_delta_bytes: (snap.vmrss_kb as i64 * 1024) - rss_before, + }); + + eprintln!( + "L1a repeat {repeat}: build={build_ns}ns scan={scan_ns}ns think={think_ns}ns cast={cast_ns}ns collect={collect_ns}ns freeze={freeze_ns}ns total={total_ns}ns" + ); + csv.write(&Row { + owner_shape: "l1a_chunked_soa_1024x64", + physical_layout: "chunked_valid_comparison", + threads: 1, + segment_rows: 0, + segment_bytes: 0, + segments_per_cycle: 0, + repeat, + build_ns, + scan_ns, + think_ns, + rebind_cast_ns: cast_ns, + collect_ns, + freeze_ns, + wal_write_ns: 0, + wal_sync_ns: 0, + temporal_layer1_ns: 0, + temporal_layer2_ns: 0, + apply_ns: 0, + total_ns, + logical_rows: FLEET_OWNERS as u64, + logical_bytes: (FLEET_OWNERS as u64) * 4, // MailboxId=u32 -> to_le_bytes() is 4 bytes + sealed_transitions: cast as u64, + applied_transitions: 0, + wal_syscalls: 0, + fsync_calls: 0, + dataset_versions: 0, + peak_rss_bytes: snap.vmhwm_kb * 1024, + minor_faults: snap.minflt, + major_faults: snap.majflt, + context_switches: snap.vol_ctxt + snap.nonvol_ctxt, + max_active_workers: 1, + result_digest: digest, + morton_reorder_ns: 0, + }); + let _ = chunk_targets; + } + + median_phases(&samples) + } + + /// L1b — the MISLABELLING control: 64 chunks cast as if THEY were the + /// owners (64 casts/cycle, not 65,536). Never evidence for the 64k-owner + /// model — its only purpose is to make the held-backlog collapse visible + /// (per `collect_casts`'s <=1-move-per-owner partition) when a physical + /// chunk is mistaken for a logical owner, contrasted against L1a's clean + /// 65,536-distinct-owner cast above. + fn run_l1b(csv: &mut CsvSink) { + eprintln!( + "\n== L1b — 64 chunks AS owners (mislabelling CONTROL, never the 64k-owner model) ==" + ); + let style_outcome = build_style_outcome(); + + let t_build = Instant::now(); + let mut chunks: HashMap = HashMap::with_capacity(CHUNKS as usize); + for c in 0..CHUNKS { + chunks.insert(c, build_chunk(c)); + } + let build_ns = t_build.elapsed().as_nanos() as u64; + + // Cast ALL 1,024 lanes' intents but ON BEHALF OF THE CHUNK id (not + // the logical owner) — this is the deliberately wrong shape. + let t_cast = Instant::now(); + let mut writer: BatchWriter> = BatchWriter::new(); + let mut cast = 0usize; + for c in 0..CHUNKS { + let chunk = chunks.get(&c).expect("chunk exists"); + for lane in 0..CHUNK_ROWS as u32 { + let payload = lane.to_le_bytes().to_vec(); + if emit_bootstrap_intent( + &style_outcome, + c, // <- the CHUNK id, not the logical owner — the control + chunk.current_cycle(), + &mut writer, + payload, + ) + .is_some() + { + cast += 1; + } + } + } + let cast_ns = t_cast.elapsed().as_nanos() as u64; + assert_eq!( + cast, FLEET_OWNERS as usize, + "L1b: every intent still STAGES (cast never refuses; the collapse shows up at collect_casts)" + ); + + let t_collect = Instant::now(); + let collected = collect_casts(&mut writer, CycleId(1), 0, u64::from); + let collect_ns = t_collect.elapsed().as_nanos() as u64; + assert_eq!( + collected.slots.len(), + FLEET_OWNERS as usize, + "L1b: every cast still lands a payload landing (move-free for the held ones)" + ); + assert_eq!( + collected.held.len(), + FLEET_OWNERS as usize - CHUNKS as usize, + "L1b MISLABELLING PROOF: only 64 of 65,536 moves seal (1/chunk) — the other \ + 65,472 are HELD because collect_casts's <=1-move-per-owner partition sees \ + only 64 distinct owner ids, exactly the failure 'a physical chunk is NOT \ + an owner' warns against" + ); + + let total_ns = build_ns + cast_ns + collect_ns; + let snap = proc_snapshot(); + eprintln!( + "L1b: build={build_ns}ns cast={cast_ns}ns collect={collect_ns}ns -> {} staged, {} of {} HELD (chunk-as-owner collapse, proves the control)", + collected.slots.len(), + collected.held.len(), + FLEET_OWNERS + ); + csv.write(&Row { + owner_shape: "l1b_chunk_as_owner_control", + physical_layout: "topology_control_never_64k_evidence", + threads: 1, + segment_rows: 0, + segment_bytes: 0, + segments_per_cycle: 0, + repeat: 0, + build_ns, + scan_ns: 0, + think_ns: 0, + rebind_cast_ns: cast_ns, + collect_ns, + freeze_ns: 0, + wal_write_ns: 0, + wal_sync_ns: 0, + temporal_layer1_ns: 0, + temporal_layer2_ns: 0, + apply_ns: 0, + total_ns, + logical_rows: FLEET_OWNERS as u64, + logical_bytes: 0, + sealed_transitions: (CHUNKS as u64), // exactly one per chunk + applied_transitions: 0, + wal_syscalls: 0, + fsync_calls: 0, + dataset_versions: 0, + peak_rss_bytes: snap.vmhwm_kb * 1024, + minor_faults: snap.minflt, + major_faults: snap.majflt, + context_switches: snap.vol_ctxt + snap.nonvol_ctxt, + max_active_workers: 1, + result_digest: fnv1a64(&collected.held.len().to_le_bytes()), + morton_reorder_ns: 0, + }); + } + + // ═════════════════════════════════════════════════════════════════════ + // §13 — EXP-KIA-A2-64K: exploratory concurrency (non-claiming). + // D-KIA-A2 is untouched — its own median-of-5, >=2x gate is elsewhere. + // ═════════════════════════════════════════════════════════════════════ + + /// The parallel COMPUTE phase's per-body result — benchmark-local (the + /// plan §"EXP-KIA-A2-64K" names this shape explicitly: "thread-local + /// `PreparedIntent` buffers"). Carries no shared mutable state; produced + /// entirely from a `&Fleet` read during the parallel phase, consumed only + /// at the SEQUENTIAL convergence boundary below. + #[derive(Clone)] + struct PreparedIntent { + owner: MailboxId, + target: KanbanColumn, + payload: Vec, + } + + fn compute_range( + fleet: &HashMap, + range: std::ops::Range, + ) -> Vec { + let mut out = Vec::with_capacity(range.len()); + for id in range { + let owner = fleet.get(&id).expect("owner exists"); + let qualia = owner.qualia_at(0); + let mantissa = mantissa_of(owner); + let gate = gate_decision_i4(&qualia, mantissa); + let target = owner + .phase() + .advance_on_gate(&gate) + .expect("EXP-KIA: every owner Flows"); + out.push(PreparedIntent { + owner: id, + target, + payload: id.to_le_bytes().to_vec(), + }); + } + out + } + + fn partitions(n: u32, workers: u32) -> Vec> { + let workers = workers.max(1); + let chunk = n.div_ceil(workers); + let mut out = Vec::new(); + let mut lo = 0u32; + while lo < n { + let hi = (lo + chunk).min(n); + out.push(lo..hi); + lo = hi; + } + out + } + + async fn run_exp_kia_a2_64k(csv: &mut CsvSink) -> (u64, u64, u32, bool) { + eprintln!( + "\n== EXP-KIA-A2-64K — exploratory concurrency (non-claiming; D-KIA-A2 untouched) ==" + ); + let style_outcome = build_style_outcome(); + + let available = std::thread::available_parallelism() + .map(std::num::NonZeroUsize::get) + .unwrap_or(1) as u32; + let mut worker_counts: Vec = vec![1, 2, 4, 8, 16]; + if !worker_counts.contains(&available) { + worker_counts.push(available); + } + worker_counts.sort_unstable(); + worker_counts.dedup(); + eprintln!("EXP-KIA: worker counts under test: {worker_counts:?} (available_parallelism={available})"); + + let mut seq_digest: Option = None; + let mut seq_total_ns = 0u64; + let mut best_parallel_ns = u64::MAX; + let mut best_workers = 1u32; + let mut all_digests_match = true; + + for &workers in &worker_counts { + // Fresh fleet per worker-count run: every run needs owners + // starting at Planning for `apply_sealed_transitions` to succeed. + let t_build = Instant::now(); + let mut fleet: HashMap = + HashMap::with_capacity(FLEET_OWNERS as usize); + for id in 0..FLEET_OWNERS { + fleet.insert(id, build_tenant::(id)); + } + let build_ns = t_build.elapsed().as_nanos() as u64; + + // ── PARALLEL COMPUTE PHASE — disjoint ranges, thread-local + // buffers, `&fleet` shared read-only, NEVER a mutex around a + // shared BatchWriter here. ───────────────────────────────────── + let ranges = partitions(FLEET_OWNERS, workers); + let active = AtomicUsize::new(0); + let max_active = AtomicUsize::new(0); + let t_think = Instant::now(); + let mut all_intents: Vec = Vec::with_capacity(FLEET_OWNERS as usize); + std::thread::scope(|scope| { + let mut handles = Vec::with_capacity(ranges.len()); + for range in ranges.clone() { + let fleet_ref = &fleet; + let active_ref = &active; + let max_active_ref = &max_active; + handles.push(scope.spawn(move || { + let cur = active_ref.fetch_add(1, Ordering::SeqCst) + 1; + max_active_ref.fetch_max(cur, Ordering::SeqCst); + let result = compute_range(fleet_ref, range); + active_ref.fetch_sub(1, Ordering::SeqCst); + result + })); + } + for h in handles { + all_intents.extend(h.join().expect("worker thread must not panic")); + } + }); + let think_ns = t_think.elapsed().as_nanos() as u64; + let max_active_workers = max_active.load(Ordering::SeqCst) as u32; + assert_eq!( + all_intents.len(), + FLEET_OWNERS as usize, + "EXP-KIA can-fire: exactly 65,536 bodies executed, worker count {workers}" + ); + if workers >= 2 { + assert!( + max_active_workers >= 2, + "EXP-KIA can-fire: real overlap measured (max_active_workers>=2) at worker count {workers}" + ); + } + + // ── SEQUENTIAL CONVERGENCE BOUNDARY — the existing owner rebind + // + BatchWriter staging, deterministic order (sorted by owner id + // so every worker-count run converges on the identical order, + // independent of thread completion order). ───────────────────── + all_intents.sort_by_key(|p| p.owner); + let t_cast = Instant::now(); + let mut writer: BatchWriter> = BatchWriter::new(); + let mut cast = 0usize; + for intent in &all_intents { + let owner = fleet.get(&intent.owner).expect("owner exists"); + // Rebuild a per-owner StrategyOutcome from the shared + // style_outcome's crossing, since the target the parallel + // phase computed may legally be Prune (a Block) rather than + // CognitiveWork (a Flow) — the bootstrap sentinel from + // `style_outcome` only carries the Flow crossing, matching + // `probe_ignition.rs`'s G2b `shade_owner` fallback shape. + let outcome = if let Some(mv) = style_outcome.intended_move { + if mv.to == intent.target { + style_outcome + } else { + StrategyOutcome { + reliability: style_outcome.reliability, + intended_move: Some(KanbanMove { + mailbox: 0, + from: owner.phase(), + to: intent.target, + witness_chain_position: 0, + exec: lance_graph_contract::kanban::ExecTarget::Native, + }), + } + } + } else { + style_outcome + }; + if emit_bootstrap_intent( + &outcome, + owner.mailbox_id(), + owner.current_cycle(), + &mut writer, + intent.payload.clone(), + ) + .is_some() + { + cast += 1; + } + } + let cast_ns = t_cast.elapsed().as_nanos() as u64; + assert_eq!( + cast, FLEET_OWNERS as usize, + "EXP-KIA: every intent casts at the boundary" + ); + + // ── ONE SEAL, ONE WAL COMMIT. ─────────────────────────────────── + let t_collect = Instant::now(); + let collected = collect_casts(&mut writer, CycleId(1), 0, u64::from); + let collect_ns = t_collect.elapsed().as_nanos() as u64; + assert_eq!(collected.slots.len(), FLEET_OWNERS as usize); + assert!( + collected.held.is_empty(), + "EXP-KIA: one move per owner, nothing held" + ); + + let sink = MemWal::new(); + let frame = CycleFrame::new(CycleId(1), DatasetVersion(0)); + let t_wal = Instant::now(); + let version = persist_cycle(&sink, frame, collected.slots.clone()) + .await + .expect("EXP-KIA: seal must succeed"); + let wal_write_ns = t_wal.elapsed().as_nanos() as u64; + assert_eq!(sink.wal_writes(), 1, "EXP-KIA: exactly one WAL commit"); + assert_eq!(version, DatasetVersion(1)); + + let sealed = build_sealed_locally(frame, &collected.slots, version); + assert_eq!( + sealed.transitions.len(), + FLEET_OWNERS as usize, + "EXP-KIA: one sealed cycle carries all 65,536 transitions" + ); + + let t_apply = Instant::now(); + let mut watermarks: HashMap> = HashMap::new(); + let applied = apply_sealed_transitions(&mut fleet, &sealed, &mut watermarks) + .expect("EXP-KIA: apply must succeed"); + let apply_ns = t_apply.elapsed().as_nanos() as u64; + assert_eq!( + applied.applied.len(), + FLEET_OWNERS as usize, + "EXP-KIA: 65,536 applied transitions" + ); + + // ── owner bindings preserved: `SealedTransition::owner` (set from + // `SweepSlot.owner`, itself set from the PreparedIntent's owner at + // the sequential boundary) must equal `mv.mailbox` (set inside + // `emit_bootstrap_intent` -> `rebind_bootstrap`) for EVERY sealed + // transition — two independently-populated fields from different + // points in the pipeline, so this is a real cross-check, not a + // value compared against itself. + for t in &sealed.transitions { + assert_eq!( + t.owner, t.mv.mailbox, + "EXP-KIA: owner binding preserved end-to-end (SweepSlot.owner == KanbanMove.mailbox)" + ); + } + + // ── sequential-vs-parallel identity: FNV digest over the + // SORTED (owner, stream_position) pairs. ──────────────────────── + let mut identity_bytes: Vec = sealed + .transitions + .iter() + .map(|t| (t.owner, t.stream_position)) + .collect::>() + .into_iter() + .flat_map(|(o, s)| { + let mut b = o.to_le_bytes().to_vec(); + b.extend_from_slice(&s.to_le_bytes()); + b + }) + .collect(); + // sort at the BYTE level is wrong (variable width already fixed + // at 12 bytes/entry, so chunk-sort is correct and cheap): + { + let mut entries: Vec<[u8; 12]> = identity_bytes + .chunks_exact(12) + .map(|c| c.try_into().unwrap()) + .collect(); + entries.sort_unstable(); + identity_bytes = entries.into_iter().flatten().collect(); + } + let digest = fnv1a64(&identity_bytes); + + let total_ns = build_ns + think_ns + cast_ns + collect_ns + wal_write_ns + apply_ns; + let snap = proc_snapshot(); + + if workers == 1 { + seq_digest = Some(digest); + seq_total_ns = think_ns; // the compute-phase time is the axis of interest + } else { + if Some(digest) != seq_digest { + all_digests_match = false; + } + if think_ns < best_parallel_ns { + best_parallel_ns = think_ns; + best_workers = workers; + } + } + + eprintln!( + "EXP-KIA workers={workers}: build={build_ns}ns think(compute)={think_ns}ns max_active={max_active_workers} cast={cast_ns}ns collect={collect_ns}ns wal={wal_write_ns}ns apply={apply_ns}ns total={total_ns}ns digest={digest:016x}" + ); + + csv.write(&Row { + owner_shape: "exp_kia_a2_64k", + physical_layout: "prepared_intent_then_sequential_boundary", + threads: workers, + segment_rows: 0, + segment_bytes: 0, + segments_per_cycle: 0, + repeat: 0, + build_ns, + scan_ns: 0, + think_ns, + rebind_cast_ns: cast_ns, + collect_ns, + freeze_ns: 0, + wal_write_ns, + wal_sync_ns: 0, + temporal_layer1_ns: 0, + temporal_layer2_ns: 0, + apply_ns, + total_ns, + logical_rows: FLEET_OWNERS as u64, + logical_bytes: (FLEET_OWNERS as u64) * 4, // MailboxId=u32 -> to_le_bytes() is 4 bytes + sealed_transitions: sealed.transitions.len() as u64, + applied_transitions: applied.applied.len() as u64, + wal_syscalls: 0, + fsync_calls: 1, + dataset_versions: 1, + peak_rss_bytes: snap.vmhwm_kb * 1024, + minor_faults: snap.minflt, + major_faults: snap.majflt, + context_switches: snap.vol_ctxt + snap.nonvol_ctxt, + max_active_workers, + result_digest: digest, + morton_reorder_ns: 0, + }); + } + + eprintln!( + "EXP-KIA: sequential-vs-parallel digests {} across all worker counts", + if all_digests_match { + "MATCH" + } else { + "DIVERGED (see stderr above)" + } + ); + assert!( + all_digests_match, + "EXP-KIA can-fire: sequential and parallel runs must converge to the identical sealed cycle" + ); + + ( + seq_total_ns, + best_parallel_ns, + best_workers, + all_digests_match, + ) + } + + // ═════════════════════════════════════════════════════════════════════ + // §15 — M-arm: Morton reorder inserted before the seal (plan v3, M-arm). + // + // A0 measured `logical order → seal → WAL`. This measures the pipeline + // the architecture actually proposes: `logical order → MORTON REORDER → + // seal → WAL`, plus the downstream T1 read. Same 65,536 owners, same + // per-cycle cast/collect shape as B1a/`run_temporal`; the ONLY + // difference between the two configurations below is the reorder phase + // and the physical write order it produces — the CAST phase, payload + // content, and cycle count are identical. + // ═════════════════════════════════════════════════════════════════════ + + /// v2 D1: identity stays on `MailboxId` — this is a SEPARATE key for + /// physical write/storage order, never used to look an owner up. + #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)] + struct WriteOrderKey { + morton_chunk: u32, + lane: u16, + cycle_position: u64, + } + + /// Standard 8-bit -> 16-bit bit-spread (the libmorton bit-trick): each of + /// `v`'s 8 bits lands at an EVEN position (0, 2, 4, ..., 14) of the + /// 16-bit result. + fn morton_spread_u8(v: u8) -> u16 { + let mut x = u16::from(v); + x = (x | (x << 4)) & 0x0F0F; + x = (x | (x << 2)) & 0x3333; + x = (x | (x << 1)) & 0x5555; + x + } + + /// 2-D Morton (Z-order) interleave of two 8-bit coordinates into one + /// 16-bit code — a BIJECTION over the full 65,536-owner space (every + /// `(x, y)` in `[0,256)^2` maps to exactly one code in `[0, 65536)`). + fn morton_code_u16(x: u8, y: u8) -> u16 { + morton_spread_u8(x) | (morton_spread_u8(y) << 1) + } + + /// `owner -> WriteOrderKey`: split the 16-bit owner id into two 8-bit + /// coordinates (low byte / high byte), Morton-interleave them, and read + /// the top 6 bits as a chunk id (64 chunks) / bottom 10 as a lane + /// (1,024 lanes) — matching L1a's 64x1,024 physical shape, but via a + /// spatially-interleaved (not linear `chunk = owner/1024`) assignment. + /// `cycle_position` is carried per v2 D1's field list; since + /// `(morton_chunk, lane)` is already a bijection of `owner`, it never + /// breaks a tie here — it is provenance, not a discriminator. + fn morton_key_for(owner: MailboxId) -> WriteOrderKey { + debug_assert!( + owner < FLEET_OWNERS, + "morton_key_for: owner must fit the fleet's 16-bit range" + ); + let o = owner as u16; + let x = (o & 0x00FF) as u8; + let y = ((o >> 8) & 0x00FF) as u8; + let code = morton_code_u16(x, y); + WriteOrderKey { + morton_chunk: u32::from(code >> 10), + lane: code & 0x03FF, + cycle_position: u64::from(owner), + } + } + + /// A digest over `(owner, row, payload)`, SORTED BY OWNER — order + /// independent of the caller's physical layout by construction, so it is + /// a fair SEMANTIC (not physical-layout) comparison between two + /// pipelines that wrote the same logical content in a different + /// physical write order. Used for the M-arm's mandatory ordered-vs- + /// unordered digest identity assert. + fn semantic_digest(slots: &[SweepSlot]) -> u64 { + let mut keyed: Vec<(MailboxId, u64, &[u8])> = slots + .iter() + .map(|s| (s.owner, s.row, s.payload.as_slice())) + .collect(); + keyed.sort_by_key(|(owner, row, _)| (*owner, *row)); + let mut bytes = Vec::with_capacity(keyed.len() * (4 + 8 + CANONICAL_ROW_BYTES)); + for (owner, row, payload) in keyed { + bytes.extend_from_slice(&owner.to_le_bytes()); + bytes.extend_from_slice(&row.to_le_bytes()); + bytes.extend_from_slice(payload); + } + fnv1a64(&bytes) + } + + /// v2 D2 — the ordered-chunk fast path: given a scanned history whose + /// PHYSICAL write order is already chunk-then-lane within each cycle and + /// cycle-increasing across cycles (the Morton-ordered M-arm pipeline + /// below), reconstruct per-owner trajectories by DIRECT APPEND — no + /// `BTreeMap`-group-then-sort (`local_trajectories`'s own per-owner + /// `sort_by_key` is exactly what this skips). Validates the invariant + /// the shortcut depends on (global `stream_position` strictly increasing + /// across the WHOLE scan — the collapse of "version monotonic x chunk + /// sequence monotonic x lane monotonic" onto one counter, per v2 D2's + /// header list) before trusting it; on ANY violation it refuses (`Err`) + /// rather than silently mis-ordering a trajectory. A real precondition, + /// not a decorative one — `run_m_arm`'s inline can-fire check (a + /// deliberately corrupted, stream_position-regressed 2-row input) proves + /// it can fire. + fn local_trajectories_ordered_chunk_fastpath( + landed: &[LandedSlot], + ) -> Result>, String> { + let mut out: BTreeMap> = BTreeMap::new(); + let mut last_stream_position: Option = None; + for ls in landed { + if let Some(p) = last_stream_position { + if ls.slot.stream_position <= p { + return Err(format!( + "fast path: stream_position non-increasing ({p} -> {}) — the \ + ordered-chunk precondition (validate-then-append) does not hold", + ls.slot.stream_position + )); + } + } + last_stream_position = Some(ls.slot.stream_position); + + // VALIDATED for this row — append directly. No group-then-sort: + // this owner's chain is being built in the SAME order the + // physical log already guarantees. + out.entry(ls.slot.owner).or_default().push(BenchRow { + owner: ls.slot.owner, + cast_seq: ls.slot.stream_position, + lance_version: ls.version.0, + }); + } + Ok(out) + } + + /// One M-arm pipeline's per-cycle-medianed measurements. + #[derive(Clone, Copy, Default)] + struct MArmPhaseMedians { + cast_ns: u64, + collect_ns: u64, + reorder_ns: u64, + seal_ns: u64, + wal_write_ns: u64, + wal_sync_ns: u64, + wal_syscalls: u64, + } + + /// Run one M-arm configuration (`morton == false` -> natural/A0-shaped + /// order; `morton == true` -> Morton reorder inserted before the seal). + /// `WARMUP_CYCLES + MEASURED_CYCLES` real cycles, each: cast (identical + /// content/order for both configs) -> collect -> [reorder, Morton only] + /// -> seal (the REAL `DetachedCycleBatch::freeze`) -> a REAL byte write + /// of the frozen landings' 512B payloads (chunked `write_vectored`, + /// house pattern from `run_wal_curve`'s W0-current path) -> one + /// `fsync` -> commit into an in-process `MemWal` (for the T1 read after + /// all configs have run). Returns the phase medians, the sealed + /// `SweepSlot`s of the LAST measured cycle (for the digest-identity + /// assert), and the populated `MemWal`. + async fn run_m_arm_pipeline( + morton: bool, + wal_path: &std::path::Path, + ) -> (MArmPhaseMedians, Vec, MemWal) { + let style_outcome = build_style_outcome(); + let sink = MemWal::new(); + let mut file = OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(wal_path) + .unwrap_or_else(|e| panic!("M-arm: open WAL scratch file {wal_path:?}: {e}")); + + let mut cast_samples = Vec::with_capacity(MEASURED_CYCLES as usize); + let mut collect_samples = Vec::with_capacity(MEASURED_CYCLES as usize); + let mut reorder_samples = Vec::with_capacity(MEASURED_CYCLES as usize); + let mut seal_samples = Vec::with_capacity(MEASURED_CYCLES as usize); + let mut write_samples = Vec::with_capacity(MEASURED_CYCLES as usize); + let mut sync_samples = Vec::with_capacity(MEASURED_CYCLES as usize); + let mut syscall_samples = Vec::with_capacity(MEASURED_CYCLES as usize); + let mut position_base: u64 = 0; + let mut last_measured_slots: Vec = Vec::new(); + + const WRITE_CHUNK_ROWS: usize = 4_096; // 2 MiB/segment — house pattern from run_wal_curve. + + for cyc in 0..(WARMUP_CYCLES + MEASURED_CYCLES) { + let measured = cyc >= WARMUP_CYCLES; + let cycle_id = CycleId(u64::from(cyc) + 1); + + let t_cast = Instant::now(); + let mut writer: BatchWriter> = BatchWriter::new(); + for id in 0..FLEET_OWNERS { + // Payload varies by (owner, cycle) — falsifiable content, not + // a constant row, so a trajectory digest actually depends on + // cycle order (never a vacuous "same bytes every cycle"). + let combined = (u64::from(cyc) << 32) | u64::from(id); + let payload = NodeRow512::for_id(combined).as_bytes().to_vec(); + let _ = emit_bootstrap_intent(&style_outcome, id, 0, &mut writer, payload); + } + let cast_ns = t_cast.elapsed().as_nanos() as u64; + + let t_collect = Instant::now(); + let collected = collect_casts(&mut writer, cycle_id, position_base, u64::from); + let collect_ns = t_collect.elapsed().as_nanos() as u64; + assert_eq!( + collected.slots.len(), + FLEET_OWNERS as usize, + "M-arm: every owner casts and lands exactly once per cycle" + ); + + let mut slots = collected.slots; + let reorder_ns = if morton { + let t_reorder = Instant::now(); + order_cycle_stably(&mut slots, |s| morton_key_for(s.owner)); + // Relabel stream_position to the Morton rank so the seal's + // OWN internal `order_cycle_stably(by stream_position)` + // preserves (rather than undoes) this order — the reorder is + // "inserted before the seal", not a bypass of it. + for (idx, slot) in slots.iter_mut().enumerate() { + slot.stream_position = position_base + idx as u64; + } + t_reorder.elapsed().as_nanos() as u64 + } else { + 0 + }; + + let frame = CycleFrame::new(cycle_id, sink.head()); + let t_seal = Instant::now(); + let frozen = DetachedCycleBatch::freeze(frame, slots); + let seal_ns = t_seal.elapsed().as_nanos() as u64; + assert_eq!(frozen.landings.len(), FLEET_OWNERS as usize); + + let mut bytes_written = 0u64; + let mut total_syscalls = 0u64; + let t_write = Instant::now(); + for group in frozen.landings.chunks(WRITE_CHUNK_ROWS) { + let mut slices: Vec> = group + .iter() + .map(|s| IoSlice::new(s.payload.as_slice())) + .collect(); + let (written, calls) = + write_vectored_all(&mut file, &mut slices).expect("M-arm write_vectored"); + total_syscalls += calls; + bytes_written += written; + } + let wal_write_ns = t_write.elapsed().as_nanos() as u64; + let t_sync = Instant::now(); + file.sync_data().expect("M-arm sync_data"); + let wal_sync_ns = t_sync.elapsed().as_nanos() as u64; + assert_eq!( + bytes_written, + CANONICAL_FRAME_BYTES as u64, + "M-arm {}: an arm that does not move exactly the canonical frame \ + cannot be compared against one that does", + if morton { "morton" } else { "natural" } + ); + + if measured { + last_measured_slots = frozen.landings.clone(); + cast_samples.push(cast_ns); + collect_samples.push(collect_ns); + reorder_samples.push(reorder_ns); + seal_samples.push(seal_ns); + write_samples.push(wal_write_ns); + sync_samples.push(wal_sync_ns); + syscall_samples.push(total_syscalls); + } + + // Commit into the in-process MemWal for the later T1 read — a + // SEPARATE commit from the real byte write above (the byte write + // measures physical WAL bytes/fsync physics; the MemWal commit + // is what `scan_sealed`/T1 read back, matching `run_temporal`'s + // §11 shape). `persist_cycle` re-derives its own freeze + // internally; feeding it the SAME (already-Morton-relabeled) + // slots is safe because its internal `order_cycle_stably` is a + // no-op-preserving STABLE sort of already-sorted input. + let slots_for_commit = frozen.landings.clone(); + persist_cycle(&sink, frame, slots_for_commit) + .await + .unwrap_or_else(|e| panic!("M-arm: cycle {cyc} failed to seal: {e}")); + + position_base += u64::from(FLEET_OWNERS); + } + + drop(file); + fs::remove_file(wal_path).ok(); + + let medians = MArmPhaseMedians { + cast_ns: median(&cast_samples), + collect_ns: median(&collect_samples), + reorder_ns: median(&reorder_samples), + seal_ns: median(&seal_samples), + wal_write_ns: median(&write_samples), + wal_sync_ns: median(&sync_samples), + wal_syscalls: median(&syscall_samples), + }; + (medians, last_measured_slots, sink) + } + + async fn run_m_arm(csv: &mut CsvSink) { + eprintln!("\n== M-arm — Morton reorder inserted before the seal (plan v3) =="); + let wal_dir = PathBuf::from("/tmp/measure_wal_curve_m_arm"); + fs::create_dir_all(&wal_dir).expect("create M-arm WAL scratch dir"); + + let (natural, natural_last_slots, natural_sink) = + run_m_arm_pipeline(false, &wal_dir.join("natural.wal")).await; + let (morton, morton_last_slots, morton_sink) = + run_m_arm_pipeline(true, &wal_dir.join("morton.wal")).await; + fs::remove_dir_all(&wal_dir).ok(); + + // ── digest identity (MANDATORY, an assert, not a print) ────────── + let digest_natural = semantic_digest(&natural_last_slots); + let digest_morton = semantic_digest(&morton_last_slots); + assert_eq!( + digest_natural, digest_morton, + "M-arm can-fire: the Morton reorder must be a pure layout change — the \ + last measured cycle's (owner, row, payload) content must be byte-identical \ + regardless of physical write order, or the reorder changed semantics" + ); + eprintln!( + "M-arm digest identity: natural={digest_natural:016x} morton={digest_morton:016x} MATCH" + ); + + // ── T1: local_trajectories over both 1,048,576-row histories ───── + let (t1_natural_ns, t1_morton_ns, fastpath_ns, fastpath_digest_match) = { + // Skip the WARM-UP versions. The pipeline runs + // WARMUP+MEASURED real cycles (the warm-ups are needed for the + // write/seal timing to settle), but T1 must cover exactly the + // MEASURED window or its number is not comparable to A0's + // 78-86 ms over 1,048,576 rows — and beating that number is the + // whole point of the ordered fast path. `scan_sealed(Some(v))` + // filters `version > v`, and the warm-ups own versions 1..=WARMUP. + let after_warmup = Some(DatasetVersion(WARMUP_CYCLES as u64)); + let landed_natural = natural_sink + .scan_sealed(after_warmup) + .await + .expect("M-arm T1: scan_sealed natural"); + let landed_morton = morton_sink + .scan_sealed(after_warmup) + .await + .expect("M-arm T1: scan_sealed morton"); + assert_eq!( + landed_natural.len(), + FLEET_OWNERS as usize * MEASURED_CYCLES as usize, + "M-arm T1 must read exactly the MEASURED window (comparability with A0)" + ); + assert_eq!( + landed_morton.len(), + FLEET_OWNERS as usize * MEASURED_CYCLES as usize, + "M-arm T1 must read exactly the MEASURED window (comparability with A0)" + ); + + let bench_natural: Vec = landed_natural + .iter() + .map(|ls| BenchRow { + owner: ls.slot.owner, + cast_seq: ls.slot.stream_position, + lance_version: ls.version.0, + }) + .collect(); + let bench_morton: Vec = landed_morton + .iter() + .map(|ls| BenchRow { + owner: ls.slot.owner, + cast_seq: ls.slot.stream_position, + lance_version: ls.version.0, + }) + .collect(); + + let t1a = Instant::now(); + let traj_natural = local_trajectories(&bench_natural); + let t1_natural_ns = t1a.elapsed().as_nanos() as u64; + let t1b = Instant::now(); + let traj_morton = local_trajectories(&bench_morton); + let t1_morton_ns = t1b.elapsed().as_nanos() as u64; + assert_eq!(traj_natural.len(), FLEET_OWNERS as usize); + assert_eq!(traj_morton.len(), FLEET_OWNERS as usize); + + // ── v2 D2 fast path — Morton-ordered history only (the + // natural pipeline's physical order was never claimed to + // satisfy the fast path's precondition; it happens to be + // monotonic here too by construction, but the fast path is + // exercised against the layout it was designed for). ────── + let t_fp = Instant::now(); + let traj_fastpath = local_trajectories_ordered_chunk_fastpath(&landed_morton) + .expect("M-arm: fast path must validate the Morton-ordered history"); + let fastpath_ns = t_fp.elapsed().as_nanos() as u64; + + // digest identity: generic vs fast path, over the SAME + // Morton-ordered scanned history (v2 D2's own requirement). + let digest_of = |m: &BTreeMap>| -> u64 { + let mut bytes = Vec::new(); + for (owner, chain) in m { + bytes.extend_from_slice(&owner.to_le_bytes()); + for row in chain { + bytes.extend_from_slice(&row.cast_seq.to_le_bytes()); + bytes.extend_from_slice(&row.lance_version.to_le_bytes()); + } + } + fnv1a64(&bytes) + }; + let digest_generic = digest_of(&traj_morton); + let digest_fastpath = digest_of(&traj_fastpath); + let fastpath_digest_match = digest_generic == digest_fastpath; + assert!( + fastpath_digest_match, + "M-arm can-fire: generic local_trajectories and the ordered-chunk fast \ + path must reconstruct byte-identical trajectories from the same \ + Morton-ordered history" + ); + + // can-it-fire proof for the fast path's own guard (CLAUDE.md + // falsifiability rule): a deliberately corrupted, out-of- + // order 2-row input must be REFUSED, not silently accepted. + let bad = vec![ + LandedSlot { + version: DatasetVersion(2), + slot: SweepSlot { + cycle: CycleId(2), + stream_position: 10, + owner: 1, + row: 1, + paired_move: None, + payload: vec![], + }, + }, + LandedSlot { + version: DatasetVersion(2), + slot: SweepSlot { + cycle: CycleId(2), + stream_position: 5, // regressed — must be refused + owner: 2, + row: 2, + paired_move: None, + payload: vec![], + }, + }, + ]; + assert!( + local_trajectories_ordered_chunk_fastpath(&bad).is_err(), + "M-arm can-fire: the fast path's monotonicity guard must reject a \ + stream_position regression, not silently mis-order the trajectory" + ); + + ( + t1_natural_ns, + t1_morton_ns, + fastpath_ns, + fastpath_digest_match, + ) + }; + + eprintln!( + "M-arm natural: cast={}ns collect={}ns seal={}ns write={}ns sync={}ns T1={t1_natural_ns}ns", + natural.cast_ns, natural.collect_ns, natural.seal_ns, natural.wal_write_ns, natural.wal_sync_ns + ); + eprintln!( + "M-arm morton: cast={}ns collect={}ns reorder={}ns seal={}ns write={}ns sync={}ns T1={t1_morton_ns}ns \ + fastpath={fastpath_ns}ns (fastpath-vs-generic digest match={fastpath_digest_match})", + morton.cast_ns, morton.collect_ns, morton.reorder_ns, morton.seal_ns, morton.wal_write_ns, morton.wal_sync_ns + ); + + // ── the pre-registered SUM verdict (never the gain alone) ──────── + let downstream_natural = (natural.seal_ns + natural.wal_write_ns + natural.wal_sync_ns) + as i64 + + t1_natural_ns as i64; + let downstream_morton = (morton.seal_ns + morton.wal_write_ns + morton.wal_sync_ns) as i64 + + t1_morton_ns as i64; + let downstream_savings = downstream_natural - downstream_morton; + let delta_total = morton.reorder_ns as i64 - downstream_savings; + eprintln!( + "M-arm SUM verdict: reorder_cost={}ns, downstream (seal+write+sync+T1) \ + natural={downstream_natural}ns morton={downstream_morton}ns savings={downstream_savings:+}ns \ + -> delta_total={delta_total:+}ns ({})", + morton.reorder_ns, + if delta_total < 0 { + "Morton WINS (reorder cost paid for by downstream savings)" + } else { + "Morton does NOT win under this workload/host (reorder cost exceeds downstream savings)" + } + ); + eprintln!( + "M-arm reference: A0 measured T1 at 78-86ms over 1,048,576 rows — the number the \ + fast path must beat; this run's fast path={fastpath_ns}ns is the direct comparison \ + (implementation-scoped: this implementation, this workload, this host)." + ); + + csv.write(&Row { + owner_shape: "m_arm_natural", + physical_layout: "unordered_stream_position", + threads: 1, + segment_rows: 4_096, + segment_bytes: 4_096 * CANONICAL_ROW_BYTES as u64, + segments_per_cycle: FLEET_OWNERS as u64 / 4_096, + repeat: 0, + build_ns: 0, + scan_ns: 0, + think_ns: 0, + rebind_cast_ns: natural.cast_ns, + collect_ns: natural.collect_ns, + freeze_ns: natural.seal_ns, + wal_write_ns: natural.wal_write_ns, + wal_sync_ns: natural.wal_sync_ns, + temporal_layer1_ns: t1_natural_ns, + temporal_layer2_ns: 0, + apply_ns: 0, + total_ns: natural.cast_ns + + natural.collect_ns + + natural.seal_ns + + natural.wal_write_ns + + natural.wal_sync_ns + + t1_natural_ns, + logical_rows: FLEET_OWNERS as u64, + logical_bytes: CANONICAL_FRAME_BYTES as u64, + sealed_transitions: FLEET_OWNERS as u64, + applied_transitions: 0, + wal_syscalls: natural.wal_syscalls, + fsync_calls: 1, + dataset_versions: 16, + peak_rss_bytes: proc_snapshot().vmhwm_kb * 1024, + minor_faults: 0, + major_faults: 0, + context_switches: 0, + max_active_workers: 1, + result_digest: digest_natural, + morton_reorder_ns: 0, + }); + csv.write(&Row { + owner_shape: "m_arm_morton", + physical_layout: "morton_reordered_before_seal", + threads: 1, + segment_rows: 4_096, + segment_bytes: 4_096 * CANONICAL_ROW_BYTES as u64, + segments_per_cycle: FLEET_OWNERS as u64 / 4_096, + repeat: 0, + build_ns: 0, + scan_ns: 0, + think_ns: 0, + rebind_cast_ns: morton.cast_ns, + collect_ns: morton.collect_ns, + freeze_ns: morton.seal_ns, + wal_write_ns: morton.wal_write_ns, + wal_sync_ns: morton.wal_sync_ns, + temporal_layer1_ns: t1_morton_ns, + temporal_layer2_ns: fastpath_ns, + apply_ns: 0, + total_ns: morton.cast_ns + + morton.collect_ns + + morton.reorder_ns + + morton.seal_ns + + morton.wal_write_ns + + morton.wal_sync_ns + + t1_morton_ns, + logical_rows: FLEET_OWNERS as u64, + logical_bytes: CANONICAL_FRAME_BYTES as u64, + sealed_transitions: FLEET_OWNERS as u64, + applied_transitions: 0, + wal_syscalls: morton.wal_syscalls, + fsync_calls: 1, + dataset_versions: 16, + peak_rss_bytes: proc_snapshot().vmhwm_kb * 1024, + minor_faults: 0, + major_faults: 0, + context_switches: 0, + max_active_workers: 1, + result_digest: digest_morton, + morton_reorder_ns: morton.reorder_ns, + }); + } + + // ═════════════════════════════════════════════════════════════════════ + // §16 — O-arm: ordering source — where does the ordering actually come + // from? O-A: cast -> seal -> WAL -> temporal replay (today's pipeline). + // O-B: cast -> temporal replay -> seal -> WAL (ordering sourced first). + // ═════════════════════════════════════════════════════════════════════ + + /// A minimal `LocalCausalRow` view over an in-flight `SweepSlot` — lets + /// O-B call `local_trajectories` (a temporal.rs primitive) on cast-time + /// data, BEFORE any seal/WAL exists. `Copy`-free borrow view; built and + /// consumed entirely within one function call, never persisted. + #[derive(Clone)] + struct PreSealRow { + owner: MailboxId, + arrival_stream_position: u64, + slot: SweepSlot, + } + impl LocalCausalRow for PreSealRow { + fn owner(&self) -> MailboxId { + self.owner + } + fn cast_seq(&self) -> u64 { + self.arrival_stream_position + } + } + + // FIREWALL-START: derive_order_from_temporal_replay + // + // O-B must not consult the sealed stream to build its own order (that + // would be O-A wearing a disguise) — this function's body is the ONLY + // place that decides O-B's physical write order, and it is scoped by + // the FIREWALL-START/FIREWALL-END sentinels below so the compile-time + // self-scan in `run_o_arm` can check ONLY this region (a whole-file + // scan would false-positive on the legitimate `scan_sealed` calls + // elsewhere in this file, e.g. `run_temporal`/`run_m_arm`). + // + /// v2 D2 (applied pre-seal): source O-B's physical write order from + /// `local_trajectories` (a temporal.rs primitive) applied to the + /// CAST-TIME data alone. Groups by owner (this benchmark casts each + /// owner at most once per cycle, so every chain is a singleton) and + /// flattens by `BTreeMap` iteration order (owner-ascending) — the + /// temporal-sourced order, independent of arrival order. + fn derive_order_from_temporal_replay(pre_seal: &[PreSealRow]) -> Vec { + let grouped = local_trajectories(pre_seal); + let mut out = Vec::with_capacity(pre_seal.len()); + for (_owner, chain) in grouped { + for row in chain { + out.push(row.slot); + } + } + out + } + // FIREWALL-END: derive_order_from_temporal_replay + + /// One O-arm pipeline's per-cycle-medianed measurements. + #[derive(Clone, Copy, Default)] + struct OArmPhaseMedians { + cast_ns: u64, + collect_ns: u64, + order_derive_ns: u64, + seal_ns: u64, + commit_ns: u64, + t1_ns: u64, + } + + /// A deterministic, non-ascending cast ORDER (bit-reversal permutation + /// of the 16-bit owner id) — makes O-A's arrival/stream_position order + /// DEMONSTRABLY not owner-ascending, so O-A's physical write order and + /// O-B's temporal-sourced (owner-ascending) order are actually free to + /// diverge. Without this scramble every arm in this file casts owners + /// 0..65535 in order, which would make the O-A/O-B comparison trivially + /// coincide regardless of whether O-B's derivation is doing real work — + /// exactly the vacuous-assertion shape CLAUDE.md's falsifiability rule + /// forbids. + fn scrambled_cast_order() -> Vec { + let order: Vec = (0..FLEET_OWNERS) + .map(|id| (id as u16).reverse_bits() as u32) + .collect(); + // `reverse_bits` on a u16 is itself a bijection over [0,65536), so + // `order` is already a permutation of 0..65535; no sort needed to + // prove that — but assert it here, once, as a cheap can-fire check + // on the fixture itself (not per-cycle work). + let mut check = order.clone(); + check.sort_unstable(); + debug_assert_eq!( + check, + (0..FLEET_OWNERS).collect::>(), + "scrambled_cast_order: must be a permutation of every owner, exactly once" + ); + order + } + + async fn run_o_arm_pipeline( + label: &'static str, + source_from_temporal: bool, + cast_order: &[MailboxId], + ) -> (OArmPhaseMedians, MemWal) { + let style_outcome = build_style_outcome(); + let sink = MemWal::new(); + let mut cast_samples = Vec::with_capacity(MEASURED_CYCLES as usize); + let mut collect_samples = Vec::with_capacity(MEASURED_CYCLES as usize); + let mut order_derive_samples = Vec::with_capacity(MEASURED_CYCLES as usize); + let mut seal_samples = Vec::with_capacity(MEASURED_CYCLES as usize); + let mut commit_samples = Vec::with_capacity(MEASURED_CYCLES as usize); + let mut position_base: u64 = 0; + + for cyc in 0..(WARMUP_CYCLES + MEASURED_CYCLES) { + let measured = cyc >= WARMUP_CYCLES; + let cycle_id = CycleId(u64::from(cyc) + 1); + + let t_cast = Instant::now(); + let mut writer: BatchWriter> = BatchWriter::new(); + for &id in cast_order { + let combined = (u64::from(cyc) << 32) | u64::from(id); + let payload = NodeRow512::for_id(combined).as_bytes().to_vec(); + let _ = emit_bootstrap_intent(&style_outcome, id, 0, &mut writer, payload); + } + let cast_ns = t_cast.elapsed().as_nanos() as u64; + + let t_collect = Instant::now(); + let collected = collect_casts(&mut writer, cycle_id, position_base, u64::from); + let collect_ns = t_collect.elapsed().as_nanos() as u64; + assert_eq!(collected.slots.len(), FLEET_OWNERS as usize); + + let (ordered_slots, order_derive_ns) = if source_from_temporal { + let pre_seal: Vec = collected + .slots + .iter() + .map(|s| PreSealRow { + owner: s.owner, + arrival_stream_position: s.stream_position, + slot: s.clone(), + }) + .collect(); + let t_derive = Instant::now(); + let mut derived = derive_order_from_temporal_replay(&pre_seal); + // Relabel stream_position to the temporal-derived rank so + // the seal's own stable sort preserves this order, exactly + // as the M-arm does for its Morton rank. + for (idx, slot) in derived.iter_mut().enumerate() { + slot.stream_position = position_base + idx as u64; + } + let ns = t_derive.elapsed().as_nanos() as u64; + (derived, ns) + } else { + (collected.slots, 0) + }; + + let frame = CycleFrame::new(cycle_id, sink.head()); + let t_seal = Instant::now(); + let frozen = DetachedCycleBatch::freeze(frame, ordered_slots); + let seal_ns = t_seal.elapsed().as_nanos() as u64; + assert_eq!(frozen.landings.len(), FLEET_OWNERS as usize); + + let t_commit = Instant::now(); + sink.commit_cycle(frame.base_version, frozen) + .await + .unwrap_or_else(|e| panic!("O-arm {label}: cycle {cyc} failed to seal: {e}")); + let commit_ns = t_commit.elapsed().as_nanos() as u64; + + if measured { + cast_samples.push(cast_ns); + collect_samples.push(collect_ns); + order_derive_samples.push(order_derive_ns); + seal_samples.push(seal_ns); + commit_samples.push(commit_ns); + } + position_base += u64::from(FLEET_OWNERS); + } + + let t1 = Instant::now(); + // Same MEASURED-window scoping as the M-arm: the pipeline runs + // WARMUP+MEASURED real cycles, so an unfiltered scan returns 18 + // cycles' rows. The replay must cover exactly the measured window or + // its cost is not comparable to A0's or the M-arm's. + let landed = sink + .scan_sealed(Some(DatasetVersion(WARMUP_CYCLES as u64))) + .await + .expect("O-arm: scan_sealed over the measured window"); + assert_eq!( + landed.len(), + FLEET_OWNERS as usize * MEASURED_CYCLES as usize, + "O-arm replay must read exactly the MEASURED window" + ); + let bench: Vec = landed + .iter() + .map(|ls| BenchRow { + owner: ls.slot.owner, + cast_seq: ls.slot.stream_position, + lance_version: ls.version.0, + }) + .collect(); + let trajectories = local_trajectories(&bench); + let t1_ns = t1.elapsed().as_nanos() as u64; + assert_eq!(trajectories.len(), FLEET_OWNERS as usize); + + let medians = OArmPhaseMedians { + cast_ns: median(&cast_samples), + collect_ns: median(&collect_samples), + order_derive_ns: median(&order_derive_samples), + seal_ns: median(&seal_samples), + commit_ns: median(&commit_samples), + t1_ns, + }; + (medians, sink) + } + + /// The RECOVERED-TRAJECTORY digest: owner-ascending (`BTreeMap` + /// iteration order), each owner's chain in `cast_seq` order — the + /// PRIMARY observable this arm decides on. Computed over what a reader + /// gets back after WAL + temporal replay, so it is a fair comparison + /// EVEN THOUGH O-A and O-B wrote the bytes in different physical order. + async fn trajectory_digest(sink: &MemWal) -> u64 { + let landed = sink + .scan_sealed(None) + .await + .expect("O-arm: scan_sealed for digest"); + let bench: Vec = landed + .iter() + .map(|ls| BenchRow { + owner: ls.slot.owner, + cast_seq: ls.slot.stream_position, + lance_version: ls.version.0, + }) + .collect(); + let trajectories = local_trajectories(&bench); + let mut bytes = Vec::new(); + for (owner, chain) in trajectories { + bytes.extend_from_slice(&owner.to_le_bytes()); + for row in chain { + bytes.extend_from_slice(&row.cast_seq.to_le_bytes()); + } + } + fnv1a64(&bytes) + } + + async fn run_o_arm(csv: &mut CsvSink) { + eprintln!("\n== O-arm — ordering source: O-A (cast->seal->WAL->temporal replay) vs O-B (cast->temporal replay->seal->WAL) =="); + + // ── compile-time self-scan (the firewall) ───────────────────────── + // Needles built by CONCATENATING pieces never adjacent in this + // file's own source text, matching `probe_ignition.rs`'s G2a + // pattern — a needle spelled out contiguously would make the + // absence-check vacuously true, since `include_str!` reads this + // file, including the scan code itself. + { + let src = include_str!("measure_wal_curve.rs"); + let start_marker = "FIREWALL-START: derive_order_from_temporal_replay"; + let end_marker = "FIREWALL-END: derive_order_from_temporal_replay"; + let start = src + .find(start_marker) + .expect("O-arm firewall: FIREWALL-START marker must exist in source"); + let end = src + .find(end_marker) + .expect("O-arm firewall: FIREWALL-END marker must exist in source"); + assert!(start < end, "O-arm firewall: markers out of order"); + let region = &src[start..end]; + + let scan_sealed_call = format!("{}_{}", "scan", "sealed"); + let sealed_field_read = format!("{}.{}", "sink", "sealed"); + + // Strip line comments BEFORE scanning. The first real run fired on + // this block's own prose — a comment inside the region mentioned + // the needle by name, so the guard reported a violation that did + // not exist in any executable line. A firewall that trips on + // documentation tests the documentation, not the code. + let code_only: String = region + .lines() + .map(|l| match l.find("//") { + Some(i) => &l[..i], + None => l, + }) + .collect::>() + .join("\n"); + + // POSITIVE CONTROL (the can-fire half): the detector must find the + // needle in a line that really does call it. Without this, a + // silent guard and a broken guard are indistinguishable. + let synthetic_violation = format!(" let x = sink.{}(None).await;", scan_sealed_call); + assert!( + synthetic_violation.contains(&scan_sealed_call), + "O-arm firewall self-test: the detector cannot see a real call — the guard is inert" + ); + + assert!( + !code_only.contains(&scan_sealed_call), + "O-arm firewall can-fire: derive_order_from_temporal_replay must never \ + call scan_sealed — O-B would be O-A wearing a disguise" + ); + assert!( + !code_only.contains(&sealed_field_read), + "O-arm firewall can-fire: derive_order_from_temporal_replay must never \ + read a WalSink's sealed store directly" + ); + assert!( + region.contains("local_trajectories"), + "O-arm firewall can-stay-silent: the scan must be able to find real \ + content — a scan finding nothing is not evidence" + ); + eprintln!( + "O-arm firewall: derive_order_from_temporal_replay ({} bytes) contains \ + no scan_sealed / sealed-store read; local_trajectories present (scan \ + mechanism proven live)", + region.len() + ); + } + + let cast_order = scrambled_cast_order(); + let identity_order: Vec = (0..FLEET_OWNERS).collect(); + assert_ne!( + cast_order, identity_order, + "O-arm can-fire: the cast order fixture must actually be scrambled, or O-A's \ + arrival order and O-B's temporal-sourced order would trivially coincide" + ); + + let (o_a, sink_a) = run_o_arm_pipeline("O-A", false, &cast_order).await; + let (o_b, sink_b) = run_o_arm_pipeline("O-B", true, &cast_order).await; + + // ── PRIMARY OBSERVABLE — digest identity, decided and printed + // BEFORE any timing is looked at (pre-registered: timing must not + // be able to rescue a semantic difference). ────────────────────── + let digest_a = trajectory_digest(&sink_a).await; + let digest_b = trajectory_digest(&sink_b).await; + let digests_match = digest_a == digest_b; + eprintln!( + "O-arm PRIMARY OBSERVABLE (decided before timing): O-A digest={digest_a:016x} \ + O-B digest={digest_b:016x} -> {}", + if digests_match { "MATCH" } else { "DIVERGED" } + ); + // BOTH outcomes are pre-registered RESULTS, so neither aborts the run. + // A first revision asserted equality and panicked on divergence — + // that turns a designed falsification into a crash and loses every + // number after it. The spec is explicit: if the trajectories differ, + // the hypothesis is dead and the seal's ordering is load-bearing — + // "equally valuable, and cheaper to learn now than after a redesign". + if digests_match { + eprintln!( + "O-arm VERDICT: ordering sourced from temporal replay reproduces the seal's \ + own ordering byte-for-byte under this construction. The seal's ordering work \ + is REDUNDANT with temporal's for this workload — the re-scope question is \ + open (implementation-scoped: this construction, this workload, this host)." + ); + } else { + eprintln!( + "O-arm VERDICT: DIVERGED — ordering sourced from temporal replay does NOT \ + reproduce the seal's ordering. Under this construction the seal's ordering \ + is LOAD-BEARING and cannot be re-scoped away. Honest scope: this falsifies \ + the hypothesis FOR THIS O-B CONSTRUCTION; it does not prove that no \ + construction could match. The divergence itself is the finding." + ); + } + + // ── KILL CONDITION check — is O-B constructible without literally + // duplicating O-A's ordering work? Reported honestly either way, + // never silently rigged. ────────────────────────────────────────── + eprintln!( + "O-arm kill-condition check: CONSTRUCTIBLE. O-B's ordering derivation \ + (`local_trajectories` grouping, ~O(n log n) via BTreeMap insertion) is a \ + DIFFERENT code path from O-A's seal-side sort (`order_cycle_stably`'s Vec \ + sort_by_key, also O(n log n)) — not literally shared code, so this is not a \ + disguised O-A. Under THIS harness's one-row-per-owner-per-cycle shape the two \ + algorithms are doing comparable asymptotic work; the redundancy the plan asks \ + about is SEMANTIC (does temporal's grouping make the seal's own sort \ + unnecessary for correctness), not literal code-sharing — reported honestly, \ + not glossed over." + ); + + // ── secondary: per-phase timing for both pipelines ──────────────── + eprintln!( + "O-A (today's pipeline): cast={}ns collect={}ns seal={}ns commit={}ns T1={}ns", + o_a.cast_ns, o_a.collect_ns, o_a.seal_ns, o_a.commit_ns, o_a.t1_ns + ); + eprintln!( + "O-B (ordering sourced first): cast={}ns collect={}ns order_derive={}ns seal={}ns \ + commit={}ns T1={}ns", + o_b.cast_ns, o_b.collect_ns, o_b.order_derive_ns, o_b.seal_ns, o_b.commit_ns, o_b.t1_ns + ); + + csv.write(&Row { + owner_shape: "o_a_today_pipeline", + physical_layout: "cast_seal_wal_temporal_replay", + threads: 1, + segment_rows: 0, + segment_bytes: 0, + segments_per_cycle: 0, + repeat: 0, + build_ns: 0, + scan_ns: 0, + think_ns: 0, + rebind_cast_ns: o_a.cast_ns, + collect_ns: o_a.collect_ns, + freeze_ns: o_a.seal_ns, + wal_write_ns: o_a.commit_ns, + wal_sync_ns: 0, + temporal_layer1_ns: o_a.t1_ns, + temporal_layer2_ns: 0, + apply_ns: 0, + total_ns: o_a.cast_ns + o_a.collect_ns + o_a.seal_ns + o_a.commit_ns + o_a.t1_ns, + logical_rows: FLEET_OWNERS as u64, + logical_bytes: (FLEET_OWNERS as u64) * CANONICAL_ROW_BYTES as u64, + sealed_transitions: FLEET_OWNERS as u64, + applied_transitions: 0, + wal_syscalls: 0, + fsync_calls: 0, + dataset_versions: 16, + peak_rss_bytes: proc_snapshot().vmhwm_kb * 1024, + minor_faults: 0, + major_faults: 0, + context_switches: 0, + max_active_workers: 1, + result_digest: digest_a, + morton_reorder_ns: 0, + }); + csv.write(&Row { + owner_shape: "o_b_ordering_sourced_first", + physical_layout: "cast_temporal_replay_seal_wal", + threads: 1, + segment_rows: 0, + segment_bytes: 0, + segments_per_cycle: 0, + repeat: 0, + build_ns: 0, + scan_ns: 0, + think_ns: 0, + rebind_cast_ns: o_b.cast_ns, + collect_ns: o_b.collect_ns, + freeze_ns: o_b.seal_ns, + wal_write_ns: o_b.commit_ns, + wal_sync_ns: 0, + temporal_layer1_ns: o_b.t1_ns, + temporal_layer2_ns: o_b.order_derive_ns, + apply_ns: 0, + total_ns: o_b.cast_ns + + o_b.collect_ns + + o_b.order_derive_ns + + o_b.seal_ns + + o_b.commit_ns + + o_b.t1_ns, + logical_rows: FLEET_OWNERS as u64, + logical_bytes: (FLEET_OWNERS as u64) * CANONICAL_ROW_BYTES as u64, + sealed_transitions: FLEET_OWNERS as u64, + applied_transitions: 0, + wal_syscalls: 0, + fsync_calls: 0, + dataset_versions: 16, + peak_rss_bytes: proc_snapshot().vmhwm_kb * 1024, + minor_faults: 0, + major_faults: 0, + context_switches: 0, + max_active_workers: 1, + result_digest: digest_b, + morton_reorder_ns: 0, + }); + } + + // ═════════════════════════════════════════════════════════════════════ + // §14 — orchestration + the four closing answers. + // ═════════════════════════════════════════════════════════════════════ + + pub fn run() { + eprintln!("measure_wal_curve — five-axis 64k measurement (release-mode; plan: .claude/plans/measure-64k-axes-v1.md)"); + eprintln!( + "FLEET_OWNERS={FLEET_OWNERS} CANONICAL_ROW_BYTES={CANONICAL_ROW_BYTES} CANONICAL_FRAME_BYTES={CANONICAL_FRAME_BYTES}" + ); + #[cfg(debug_assertions)] + eprintln!( + "WARNING: this binary was NOT built --release — every timing number below is meaningless as physics, structure-only." + ); + + let mut csv = CsvSink::new(); + + // ── B0 / B1a / B1b ────────────────────────────────────────────── + let b0 = run_b0(&mut csv); + let b1a = run_b1a(&mut csv); + let b1b_rss_delta = run_b1b(&mut csv); + + // ── L1a / L1b ─────────────────────────────────────────────────── + let l1a = run_l1a(&mut csv); + run_l1b(&mut csv); + + // ── WAL curve ─────────────────────────────────────────────────── + let wal_summary = run_wal_curve(&mut csv); + + // ── Temporal + EXP-KIA-A2-64K need the async WAL-sink machinery ── + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("build a single-threaded tokio runtime for the async arms"); + rt.block_on(async { + run_temporal(&mut csv).await; + }); + let (exp_seq_ns, exp_best_parallel_ns, exp_best_workers, exp_digests_match) = + rt.block_on(async { run_exp_kia_a2_64k(&mut csv).await }); + + // ── M-arm / O-arm (plan v3, measure-64k-axes-v3.md) ─────────────── + rt.block_on(async { + run_m_arm(&mut csv).await; + }); + rt.block_on(async { + run_o_arm(&mut csv).await; + }); + + eprintln!("\nmeasure.csv: {} rows written", csv.rows_written); + eprintln!("measure.csv: file at {}", csv.path); + + // The derived "hot representation overhead" metric (plan §B1) — the + // two peak-RSS numbers are NEVER blended into one memory claim; this + // is their difference, reported once, separately from both. + // MEASURED as VmRSS deltas. An earlier revision differenced VmHWM and + // printed a NEGATIVE "overhead" — VmHWM is process-monotonic, so the + // subtraction returned the same historical maximum twice. That figure + // is retracted, not reported. + // B1a's footprint is MEASURED (VmRSS delta). The canonical envelope is + // EXACT ARITHMETIC (65_536 x 512 B) — so the overhead is measured-minus- + // exact, never measured-minus-measured. B1b's own in-process delta is + // reported beside it and is expected to read ~0: by the time it runs the + // allocator satisfies its 32 MiB from pages B1a already returned, so an + // in-process delta cannot see it. (An earlier revision differenced two + // VmHWM values and printed a NEGATIVE overhead — VmHWM is + // process-monotonic; that figure is retracted, not reported.) + let hot_repr_overhead = b1a.rss_delta_bytes - CANONICAL_FRAME_BYTES as i64; + eprintln!( + "hot representation overhead: B1a MEASURED VmRSS delta {:+}B ({:.1} MiB) \ + minus canonical envelope {}B (32.0 MiB, exact by construction) = {:+}B ({:+.1} MiB, {:+.0}%)", + b1a.rss_delta_bytes, + b1a.rss_delta_bytes as f64 / (1024.0 * 1024.0), + CANONICAL_FRAME_BYTES, + hot_repr_overhead, + hot_repr_overhead as f64 / (1024.0 * 1024.0), + 100.0 * hot_repr_overhead as f64 / CANONICAL_FRAME_BYTES as f64 + ); + eprintln!( + " (B1b in-process VmRSS delta {:+}B — reads ~0 by allocator reuse, \ + which is why the line above uses the exact canonical size)", + b1b_rss_delta + ); + + // ── the four answers (plan, "Placement + gates") ───────────────── + eprintln!("\n================ THE FOUR ANSWERS ================"); + + // 1. What does ownership cost? (B1a - B0, per phase) + let scan_tax = b1a.scan_ns as i64 - b0.scan_ns as i64; + let cast_tax = b1a.cast_ns as i64 - b0.cast_ns as i64; + let freeze_tax = b1a.freeze_ns as i64 - b0.freeze_ns as i64; + eprintln!( + "1. Ownership cost (B1a MailboxSoA<4> minus B0 DummyOwner, median of 3): \ + scan {scan_tax:+}ns, cast/rebind {cast_tax:+}ns, freeze {freeze_tax:+}ns \ + (B1a also pays a real per-owner think phase B0 has none of: {think}ns, \ + and a real apply phase: {apply}ns — B0 has neither).", + think = b1a.think_ns, + apply = b1a.apply_ns, + ); + + // 2. What does physical layout cost? (B1a vs L1a, build..freeze only) + let layout_build = l1a.build_ns as i64 - b1a.build_ns as i64; + let layout_cast = l1a.cast_ns as i64 - b1a.cast_ns as i64; + let layout_freeze = l1a.freeze_ns as i64 - b1a.freeze_ns as i64; + eprintln!( + "2. Physical layout cost (L1a 64x MailboxSoA<1024> minus B1a 65,536x \ + MailboxSoA<4>, equal 65,536 logical owners, build..freeze phases only — \ + apply is not comparable, see the §12 doc comment): \ + build {layout_build:+}ns, cast/rebind {layout_cast:+}ns, freeze {layout_freeze:+}ns." + ); + + // 3. Where does WAL amortisation plateau? + match wal_summary.plateau_segment_bytes { + Some(b) => eprintln!( + "3. WAL amortisation plateau (W1-contiguous, descriptive knee, NOT pass/kill): \ + first segment_bytes={b} where two consecutive doublings improved median \ + throughput by <5%. W0-current numbers are beside it in the CSV \ + (physical_layout=w0_current), never substituted for this reading." + ), + None if wal_summary.unstable_configs > 0 => eprintln!( + "3. WAL amortisation plateau: NOT MEASURABLE ON THIS HOST — {} of 5 configs \ + exceeded the p95/median spread ceiling (worst {:.1}x). The write phase is \ + driven by page-cache / dirty-writeback state, not segment size, so NO knee \ + is claimed. Four runs of this binary disagreed by up to 6x at identical \ + configs. Needs a quiet host with disk headroom plus O_DIRECT or a per-config \ + cache barrier. W0-current numbers are in the CSV, never substituted here.", + wal_summary.unstable_configs, wal_summary.worst_spread + ), + None => eprintln!( + "3. WAL amortisation plateau: no knee found across the 5-point table \ + ({:?} MiB/s at each segment_bytes) — either still gaining at 32 MiB \ + segments or the table is too coarse to resolve one; W0-current numbers \ + are beside it in the CSV, never substituted for this reading.", + wal_summary + .w1_points + .iter() + .map(|&(b, ns)| ( + b, + if ns > 0 { + (CANONICAL_FRAME_BYTES as f64 / (1024.0 * 1024.0)) / (ns as f64 / 1e9) + } else { + 0.0 + } + )) + .collect::>() + ), + } + + // 4. What does genuine parallel thought execution add? + eprintln!( + "4. EXP-KIA-A2-64K (exploratory, NON-CLAIMING — D-KIA-A2's own median-of-5 \ + >=2x gate is untouched by this number): sequential (workers=1) compute-phase \ + median {exp_seq_ns}ns vs best observed parallel compute-phase median \ + {exp_best_parallel_ns}ns at workers={exp_best_workers}; sequential-vs-parallel \ + sealed-cycle digests {} across every worker count tested.", + if exp_digests_match { + "MATCH" + } else { + "DIVERGED" + } + ); + eprintln!("===================================================="); + } +} diff --git a/crates/lance-graph-supervisor/src/cycle_driver.rs b/crates/lance-graph-supervisor/src/cycle_driver.rs index 18acff29c..42b8120ad 100644 --- a/crates/lance-graph-supervisor/src/cycle_driver.rs +++ b/crates/lance-graph-supervisor/src/cycle_driver.rs @@ -48,8 +48,9 @@ //! `HashMap` impl as the keyed fleet the driver and its tests use. This //! sealed-cycle path (#879) is the complete and independent production //! phase-progression path — there is no actor bridge waiting to be completed. -//! (`kanban_actor` is legacy compatibility code with no assigned architectural -//! responsibility; see its module header.) +//! (The `kanban_actor` module's actor/ack/tick surface was DELETED 2026-08-05, +//! `E-PROGRESSION-IS-EXISTENCE-NOT-COMMAND-1`; what remains there is the +//! message-free visibility census + pure helpers this driver composes.) //! //! ## Honesty ledger (what is proven vs not) //! @@ -1663,9 +1664,9 @@ mod tests { // ── The MUL-gate plug (P4c gate) ──────────────────────────────────────────── /// Flow qualia (warmth=4, groundedness=3, coherence=4, valence=2) — the same - /// construction `kanban_actor::s2_driver_gate_advances_then_holds` uses: - /// `flow_proxy = 4+3−0 = 7 ≥ 4` + mantissa>0 → FlowState::Flow; coherence≥4 + - /// valence≥2 + tension≤1 → TrustTexture::Calibrated ⇒ gate `Flow`. + /// construction `kanban_actor::tests::mul_target_flow_advances_and_hold_holds` + /// uses: `flow_proxy = 4+3−0 = 7 ≥ 4` + mantissa>0 → FlowState::Flow; + /// coherence≥4 + valence≥2 + tension≤1 → TrustTexture::Calibrated ⇒ gate `Flow`. fn flow_qualia() -> QualiaI4_16D { QualiaI4_16D(0).with(3, 4).with(14, 3).with(9, 4).with(1, 2) } diff --git a/crates/lance-graph-supervisor/src/kanban_actor.rs b/crates/lance-graph-supervisor/src/kanban_actor.rs index 5ca0a96a0..c88a25f47 100644 --- a/crates/lance-graph-supervisor/src/kanban_actor.rs +++ b/crates/lance-graph-supervisor/src/kanban_actor.rs @@ -1,234 +1,162 @@ -//! ⚠ LEGACY SURFACE (2026-08-04). #879 is the complete and independent -//! production phase-progression path. KanbanActor has no assigned architectural -//! responsibility. It is legacy experimental compatibility code retained only -//! because existing probes or consumers still reference it. No new production -//! architecture may depend on it. +//! Message-free kanban **visibility + pure helpers** — what remains of the +//! retired actor surface. //! -//! Its presence does not designate it as the future home of an ownership, -//! planning-initiation, concurrency, cognition, reasoning, or lifecycle -//! mechanism. +//! ## Tombstone (2026-08-05, operator-directed — +//! `E-PROGRESSION-IS-EXISTENCE-NOT-COMMAND-1`) //! -//! ## The canonical path (PR #879 — complete and standalone) +//! This module used to carry `KanbanActor` + `KanbanMsg::{Advance, MulAdvance, +//! Tick}` and the RPC drivers (`deliver_kanban_step` / `drive_mul_advance` / +//! `drive_version_tick` / `drive_scheduled_tick` / `run_to_absorbing`) — the +//! acknowledgement/pump/scheduler theater. All of it is DELETED, not +//! deprecated: a version tick was being read as *permission to advance*, and +//! an ack-shaped RPC was being read as *what makes the substrate progress*. +//! The substrate progresses because immutable versions exist: //! //! ```text -//! plan evaluation -> KanbanMove intent -> BatchWriter -> sparse seal -//! -> one WAL/version -> inline apply of the sealed transitions +//! think → seal → publish Lance version → next cycle reads the published version //! ``` //! -//! No actor bridge, actor fleet, actor-owned driver, actor custody model, or -//! actor message path is required. Nothing in this module participates in that -//! path. +//! Nothing signals, acknowledges, or schedules that. The compile-time +//! exclusive-ownership guarantee the actor used to dramatize is native Rust: +//! `&mut MailboxSoaOwner` IS the serialization — a second mutator is a compile +//! error, no mailbox message required. Phase progression is enacted only by +//! the #879 sealed-cycle path (`cycle_driver`). Ack/SLA/retry vocabulary +//! survives ONLY in external consumer membranes (e.g. OGAR's arago-parity +//! ActionHandler runtime, `submitAction → Receipt::Acknowledged` — an +//! application wire protocol, never substrate mechanics). //! -//! ## Legacy surfaces in this module +//! ## What lives here now //! -//! `KanbanMsg::{Advance, MulAdvance, Tick}` and the driver helpers -//! `deliver_kanban_step` / `drive_mul_advance` / `drive_version_tick` / -//! `drive_scheduled_tick` / `run_to_absorbing`. **`MulAdvance` and -//! `drive_mul_advance` are only legacy actor-message wrappers — they are NOT -//! the canonical MUL reasoning engine.** The living MUL gate is independent -//! (`lance_graph_contract::mul::i4_eval::gate_decision_i4`) and is consumed -//! directly by the #879 path via `cycle_driver::shade_owner` and -//! `cycle_driver::run_cognitive_work_gated[_over]`; marking the wrappers here -//! legacy does not deprecate that gate. `Phase` (a pure read) is unaffected. -//! -//! > **Qualifier added 2026-08-04 (measured, not inferred).** The sentence -//! > above is accurate about OWNERSHIP — `cycle_driver::shade_owner` really -//! > does call `gate_decision_i4`, and `run_cognitive_work_gated[_over]` really -//! > is the gate's caller — but read literally it suggests a LIVE path, and -//! > that part is not true today. Verified by grep: `shade_owner` has no caller -//! > outside `cycle_driver.rs` itself, `run_cognitive_work_gated[_over]` is -//! > called only from that file's own `#[cfg(test)]` module, and `cycle_driver` -//! > has no production caller at all. So "consumed directly by the #879 path" -//! > means *the #879 path is where the gate's consumer lives*, not *the gate -//! > runs in production*. Both halves matter: the wrappers here stay legacy, -//! > AND the canonical replacement is built-but-undriven. -//! > -//! > This qualifier is repeated at the two other sites in this file that make -//! > the same claim; it is stated once here in full. -//! -//! ## Stale-comment correction -//! -//! Comments in this file previously described the ractor as the thing that -//! advances a mailbox's Rubicon phase, with MUL gating and version ticks -//! composing "on top". That framing is corrected: phase progression is enacted -//! only by the #879 sealed-cycle path above. -//! -//! ## Caller/spawn migration inventory (2026-08-04) -//! -//! Kept strictly as (a) evidence for why immediate deletion would break -//! current consumers and (b) the removal work-list — it confers no -//! architectural legitimacy. `KanbanActor` is spawned by: this file's own -//! `#[cfg(test)]` unit tests (line 384+); the integration probe -//! `tests/w2b_real_owner_probe.rs` (60/103/144); and -//! `onebrc-probe/src/lane_e.rs:170` — library source, not a test — via -//! `drive_version_tick`. (An earlier draft claimed every spawn was in this -//! file; that was a single-file check written up as a repository-wide census, -//! and it was wrong.) - -use lance_graph_contract::kanban::{KanbanColumn, KanbanMove, RubiconTransitionError}; +//! - [`PhaseCensus`] — the read-only fleet visibility surface. Readers observe +//! owners; a census is one `&self` pass, not 64k RPCs. +//! - [`mul_target`] — the pure MUL-gate lowering (`gate_decision_i4` → next +//! DAG-legal phase), consumed by `cycle_driver`'s P4c gate. +//! - [`parse_kanban_step`] — the pure `kanban..` step-string +//! parser (the `UnifiedStep { step_type: "kanban.*" }` vocabulary, +//! `lance_graph_contract::kanban` module doc). + +use lance_graph_contract::kanban::KanbanColumn; use lance_graph_contract::mul::i4_eval::gate_decision_i4; -use lance_graph_contract::scheduler::{DatasetVersion, VersionScheduler}; -use lance_graph_contract::soa_view::{MailboxSoaOwner, MailboxSoaView}; +use lance_graph_contract::soa_view::MailboxSoaView; use lance_graph_contract::QualiaI4_16D; -use ractor::{Actor, ActorProcessingErr, ActorRef, RpcReplyPort}; -/// Messages the kanban actor accepts. -pub enum KanbanMsg { - /// ⚠ **LEGACY (2026-08-04)** — not part of phase progression. #879's sealed - /// cycle is the canonical path; see the module header. - /// - /// Advance the owned mailbox's Rubicon phase to `to` (checked against the - /// lifecycle DAG). Replies with the emitted [`KanbanMove`] on a legal edge, - /// or a [`RubiconTransitionError`] on an illegal one (no mutation occurs). - Advance { - to: KanbanColumn, - reply: RpcReplyPort>, - }, - /// Read the owned mailbox's current Rubicon phase (no mutation). - Phase { reply: RpcReplyPort }, - /// ⚠ **LEGACY (2026-08-04)** — a legacy actor-message *wrapper* only, NOT - /// the canonical MUL reasoning engine. The living gate - /// (`gate_decision_i4`) is independent and not deprecated; the #879 path - /// consumes it directly (`cycle_driver::shade_owner`, - /// `run_cognitive_work_gated[_over]`). - /// - /// **Atomic** S2 step: run the MUL gate (`gate_decision_i4` over `qualia` + - /// `mantissa`) against the owner's CURRENT phase and advance in ONE message. - /// Replies `Ok(Some(move))` on advance, `Ok(None)` on Hold, or the typed - /// error on an illegal edge. Gate-read and transition are serialized with the - /// owner state (one mailbox message), so a concurrent sender cannot make the - /// phase read stale between decision and mutation (codex #578). - MulAdvance { - qualia: QualiaI4_16D, - mantissa: i8, - reply: RpcReplyPort, RubiconTransitionError>>, - }, - /// ⚠ **LEGACY (2026-08-04)** — not part of phase progression. A version tick - /// is global knowledge, never permission to advance (#879's ratified rule). - /// - /// **Atomic** S3 IN-leg step: a substrate version tick (`at`) advances the - /// owner along the Rubicon **forward arc** — `phase().next_phases().first()` — - /// in ONE message, reading the owner's phase at the instant of mutation. This - /// is the in-actor realization of [`scheduler::NextPhaseScheduler`]'s policy - /// (`E-SUBSTRATE-IS-THE-SCHEDULER`): a Lance `versions()` event lowers to the - /// next legal move and the owner applies it. Replies `Some(move)` on advance, - /// or `None` when the owner is in an absorbing column (`Commit`/`Prune`) — a - /// **no-op tick is suppressed**, not an error. No error variant: the forward - /// arc is legal by construction. - /// - /// [`scheduler::NextPhaseScheduler`]: lance_graph_contract::scheduler::NextPhaseScheduler - Tick { - at: DatasetVersion, - reply: RpcReplyPort>, - }, +// ─── The visibility surface: read-only fleet phase census ───────────────────── + +/// A message-free, read-only census of Rubicon phases across a set of owners — +/// the visibility surface that replaced the actor RPC theater. +/// +/// Observation is a plain borrow: [`PhaseCensus::observe`] walks any iterator +/// of [`MailboxSoaView`]s once and counts phases; nothing is mutated, nothing +/// is messaged, nothing is scheduled. For loops that already iterate owners, +/// [`PhaseCensus::record`] accumulates incrementally. +/// +/// "Absorbing" is derived from the lifecycle DAG itself +/// (`KanbanColumn::next_phases()` empty), never hardcoded — if the Rubicon DAG +/// changes, the census follows it. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct PhaseCensus { + counts: [usize; 6], } -/// ⚠ **LEGACY compatibility / consumer surface (2026-08-04).** Retained so -/// existing callers keep building; **not** part of phase progression — see the -/// module header. A ractor actor holding a [`MailboxSoaOwner`] as its `State`. -pub struct KanbanActor { - _marker: core::marker::PhantomData, +/// Stable census index per column. Exhaustive on purpose: a new +/// `KanbanColumn` variant is a compile error here, not a silently +/// uncounted phase. +fn census_idx(col: KanbanColumn) -> usize { + match col { + KanbanColumn::Planning => 0, + KanbanColumn::CognitiveWork => 1, + KanbanColumn::Evaluation => 2, + KanbanColumn::Commit => 3, + KanbanColumn::Plan => 4, + KanbanColumn::Prune => 5, + } } -impl Default for KanbanActor { - fn default() -> Self { - Self { - _marker: core::marker::PhantomData, - } +/// The column at a census index — inverse of `census_idx`. +fn census_col(idx: usize) -> KanbanColumn { + match idx { + 0 => KanbanColumn::Planning, + 1 => KanbanColumn::CognitiveWork, + 2 => KanbanColumn::Evaluation, + 3 => KanbanColumn::Commit, + 4 => KanbanColumn::Plan, + _ => KanbanColumn::Prune, } } -impl Actor for KanbanActor -where - O: MailboxSoaOwner + Send + Sync + 'static, -{ - type Msg = KanbanMsg; - type State = O; - type Arguments = O; +impl PhaseCensus { + /// Census an iterator of views in one read-only pass. + #[must_use] + pub fn observe<'a, V, I>(views: I) -> Self + where + V: MailboxSoaView + 'a, + I: IntoIterator, + { + let mut census = Self::default(); + for v in views { + census.record(v.phase()); + } + census + } - async fn pre_start( - &self, - _myself: ActorRef, - owner: Self::Arguments, - ) -> Result { - // The actor takes ownership of the SoA mailbox at spawn. From here on the - // ONLY mutator of this owner is this actor's serialized message loop. - Ok(owner) + /// Record one observed phase (for callers already iterating owners). + pub fn record(&mut self, phase: KanbanColumn) { + self.counts[census_idx(phase)] += 1; } - async fn handle( - &self, - _myself: ActorRef, - msg: Self::Msg, - state: &mut Self::State, - ) -> Result<(), ActorProcessingErr> { - match msg { - KanbanMsg::Advance { to, reply } => { - // Single-writer by construction: one message at a time. The owner - // advances ITSELF; nothing else holds it. - let result = state.try_advance_phase(to); - let _ = reply.send(result); - } - KanbanMsg::Phase { reply } => { - let _ = reply.send(state.phase()); - } - KanbanMsg::MulAdvance { - qualia, - mantissa, - reply, - } => { - // Gate-decision + transition in ONE serialized message: the gate - // reads `state.phase()` at the instant of mutation, so a - // concurrent sender can't make it stale (mailbox-as-owner - // atomicity — codex #578). - let result = match mul_target(state.phase(), &qualia, mantissa) { - None => Ok(None), // Hold - Some(to) => state.try_advance_phase(to).map(Some), // advance - }; - let _ = reply.send(result); - } - KanbanMsg::Tick { at: _, reply } => { - // Forward-arc advance, atomic against the owner's live phase. The - // first legal successor is empty exactly for absorbing columns - // (`Commit`/`Prune`) → `None` suppresses the no-op tick. The arc - // is legal by construction, so the infallible `advance_phase` is - // correct here (no `try_`/error path). - let from = state.phase(); - let moved = from - .next_phases() - .first() - .map(|&to| state.advance_phase(to)); - let _ = reply.send(moved); - } - } - Ok(()) + /// Owners observed in `col`. + #[must_use] + pub fn count(&self, col: KanbanColumn) -> usize { + self.counts[census_idx(col)] + } + + /// Total owners observed. + #[must_use] + pub fn total(&self) -> usize { + self.counts.iter().sum() + } + + /// Owners observed in an absorbing column (one with no forward arc — + /// `next_phases()` empty, i.e. `Commit`/`Prune` under the current DAG). + #[must_use] + pub fn absorbing(&self) -> usize { + self.counts + .iter() + .enumerate() + .filter(|(i, _)| census_col(*i).next_phases().is_empty()) + .map(|(_, n)| n) + .sum() + } + + /// The fleet is at rest: at least one owner was observed and EVERY + /// observed owner sits in an absorbing column. An empty census is NOT at + /// rest — rest is an observation about owners, and observing nothing + /// asserts nothing (the vacuous-truth reading would make an empty fleet + /// indistinguishable from a finished one). + #[must_use] + pub fn at_rest(&self) -> bool { + let total = self.total(); + total > 0 && self.absorbing() == total } } -// ─── S4 delivery edge: `kanban..` → where_is → cast(Advance) ─── +// ─── Pure MUL-gate lowering (consumed by cycle_driver's P4c gate) ───────────── -/// Error from delivering a `kanban.*` step to its owning actor. -#[derive(Debug, thiserror::Error)] -pub enum KanbanRouteError { - /// `step_type` was not a well-formed `kanban..`. - #[error("malformed kanban step_type: {0}")] - BadStepType(String), - /// No live actor is registered under ``. A routing MISS, distinct - /// from the (impossible) "no owner" case: a live mailbox is always owned by - /// its actor — this means the *named* mailbox isn't registered/live. - #[error("no live mailbox registered as `{0}`")] - NoMailbox(String), - /// The owner rejected the transition (illegal Rubicon edge; no mutation). - #[error("illegal transition {from:?} -> {to:?}")] - Illegal { - from: KanbanColumn, - to: KanbanColumn, - }, - /// The actor RPC failed (mailbox closed, timeout, …). - #[error("kanban rpc failed: {0}")] - Rpc(String), +/// The MUL-gated target phase for `phase` given a node's `qualia` + inference +/// `mantissa`: run the i4 gate ([`gate_decision_i4`]) and lower it to the +/// DAG-legal next phase via [`KanbanColumn::advance_on_gate`] (Flow → forward, +/// Block → Prune-where-legal, Hold → `None`). Pure + integer-only (no f64/NaN). +pub fn mul_target( + phase: KanbanColumn, + qualia: &QualiaI4_16D, + mantissa: i8, +) -> Option { + let gate = gate_decision_i4(qualia, mantissa); + phase.advance_on_gate(&gate) } +// ─── Pure step-string vocabulary parser ─────────────────────────────────────── + /// Parse a `kanban..` step type into `(mailbox, target_phase)`, /// where `` is the snake_case [`KanbanColumn`] name (e.g. `cognitive_work`). /// Returns `None` for anything that isn't a well-formed kanban step. @@ -256,221 +184,25 @@ fn phase_from_name(name: &str) -> Option { }) } -/// The S4 **delivery edge**: resolve a `kanban..` step to its -/// owning ractor actor via the actor system's OWN name registry -/// ([`ractor::registry::where_is`]) and RPC it [`KanbanMsg::Advance`]. The owner -/// advances ITSELF; this only delivers. No bridge-held owner, no `UnifiedStep` -/// field — the target is recovered from the step string + the registry -/// (mailbox-as-owner addressing). Multi-mailbox resolves because `where_is` -/// looks up any registered mailbox by name. -/// ⚠ **LEGACY (2026-08-04)** — constructs [`KanbanMsg::Advance`], a -/// legacy arm. Delivery/advance belongs to cycle-driver + -/// BatchWriter + KanbanStep. Re-exported from `lib.rs`, so this notice is -/// repeated here for callers who never see the module header. -/// -pub async fn deliver_kanban_step(step_type: &str) -> Result { - let (mailbox, to) = parse_kanban_step(step_type) - .ok_or_else(|| KanbanRouteError::BadStepType(step_type.to_string()))?; - let cell = ractor::registry::where_is(mailbox) - .ok_or_else(|| KanbanRouteError::NoMailbox(mailbox.to_string()))?; - let actor: ActorRef = cell.into(); - let inner = ractor::call!(actor, |reply| KanbanMsg::Advance { to, reply }) - .map_err(|e| KanbanRouteError::Rpc(e.to_string()))?; - inner.map_err(|e| KanbanRouteError::Illegal { - from: e.from, - to: e.to, - }) -} - -// ─── S2 driver: MUL gate (`gate_decision_i4`) → owner advance ───────────────── - -/// The MUL-gated target phase for `phase` given a node's `qualia` + inference -/// `mantissa`: run the i4 gate ([`gate_decision_i4`]) and lower it to the -/// DAG-legal next phase via [`KanbanColumn::advance_on_gate`] (Flow → forward, -/// Block → Prune-where-legal, Hold → `None`). Pure + integer-only (no f64/NaN). -pub fn mul_target( - phase: KanbanColumn, - qualia: &QualiaI4_16D, - mantissa: i8, -) -> Option { - let gate = gate_decision_i4(qualia, mantissa); - phase.advance_on_gate(&gate) -} - -/// S2 driver: the MUL gate decides, the owner advances ITSELF — in ONE atomic -/// actor message ([`KanbanMsg::MulAdvance`]). Returns the emitted [`KanbanMove`] -/// on advance, `None` on Hold, or [`KanbanRouteError::Illegal`] on an illegal -/// edge. -/// -/// **Atomicity (codex #578):** the gate-read and the transition run inside the -/// SAME serialized mailbox message, so the gate sees the owner's phase at the -/// instant of mutation — two concurrent drivers can't both read a stale -/// `Planning` and collide. (The earlier two-RPC `Phase`-then-`Advance` shape had -/// that race.) `advance_on_gate` only yields a DAG-legal successor, so `Illegal` -/// here would signal a gate/DAG drift bug — surfaced, not panicked. -/// ⚠ **LEGACY (2026-08-04)** — constructs [`KanbanMsg::MulAdvance`], a legacy -/// actor-message *wrapper*, NOT the MUL reasoning engine. The living gate -/// (`gate_decision_i4`) is canonical and not deprecated; the #879 path consumes -/// it directly. Re-exported from `lib.rs`; notice repeated for callers. -/// -pub async fn drive_mul_advance( - actor: &ActorRef, - qualia: QualiaI4_16D, - mantissa: i8, -) -> Result, KanbanRouteError> { - let inner = ractor::call!(actor, |reply| KanbanMsg::MulAdvance { - qualia, - mantissa, - reply - }) - .map_err(|e| KanbanRouteError::Rpc(e.to_string()))?; - inner.map_err(|e| KanbanRouteError::Illegal { - from: e.from, - to: e.to, - }) -} - -// ─── S3 IN-leg: substrate version tick → owner forward-arc advance ───────────── - -/// S3 driver: a substrate version tick advances the owner along the Rubicon -/// forward arc, in ONE atomic actor message ([`KanbanMsg::Tick`]). Returns the -/// emitted [`KanbanMove`] on advance, or `None` when the owner is absorbing -/// (`Commit`/`Prune`) — the **no-op tick is suppressed** (D-MBX-9-IN, -/// `E-SUBSTRATE-IS-THE-SCHEDULER`). -/// -/// **Atomicity:** like [`drive_mul_advance`], the next-phase decision and the -/// transition run inside the SAME serialized mailbox message, so concurrent ticks -/// cannot read a stale phase and collide — they chain along the arc instead -/// (codex #578 lesson, applied to the IN-leg). This is the actor-side realization -/// of the contract's [`NextPhaseScheduler`] policy. Both this helper and -/// [`drive_scheduled_tick`] (the [`VersionScheduler`]-policy variant) are -/// legacy — see the module header. -/// -/// [`NextPhaseScheduler`]: lance_graph_contract::scheduler::NextPhaseScheduler -/// ⚠ **LEGACY (2026-08-04)** — constructs [`KanbanMsg::Tick`], a legacy -/// arm. A version tick is global knowledge, never permission to advance. -/// **Live consumer:** `onebrc-probe/src/lane_e.rs` calls this per batch; it keeps -/// working (marked, not removed) and is the named migration target. -/// -pub async fn drive_version_tick( - actor: &ActorRef, - at: DatasetVersion, -) -> Result, KanbanRouteError> { - ractor::call!(actor, |reply| KanbanMsg::Tick { at, reply }) - .map_err(|e| KanbanRouteError::Rpc(e.to_string())) -} - -/// S3 driver (custom policy): drive an arbitrary [`VersionScheduler`] for one -/// version tick. The scheduler **proposes** the next move from `view`; if it -/// yields `Some`, the owner **disposes** it via [`KanbanMsg::Advance`]; `None` -/// **suppresses the no-op tick** ("propose, don't dispose" — the scheduler reads, -/// the owner is the sole mutator). -/// -/// Unlike [`drive_version_tick`], the proposal is computed OUTSIDE the owner's -/// message (from the supplied `view`), so it is **advisory**: if the owner's phase -/// changes between the proposal and the `Advance`, the edge may be rejected -/// ([`KanbanRouteError::Illegal`]) rather than silently corrupting — surfaced, not -/// panicked. The returned move is the owner's (authoritative phase transition, -/// witness position, and libet anchor from the REAL mutation) with the -/// **scheduler's `exec` overlaid** — the backend routing tag is the policy's -/// decision, which the owner (defaulting to `Native`) can't make. For the pure -/// forward-arc policy prefer the atomic [`drive_version_tick`]; reach for this -/// only when the policy needs a richer view than the owner computes internally. -/// ⚠ **LEGACY (2026-08-04)** — constructs [`KanbanMsg::Tick`], a legacy -/// arm. Scheduling policy belongs to the cycle-driver, not an actor RPC. -/// Re-exported from `lib.rs`; notice repeated for callers. -/// -pub async fn drive_scheduled_tick( - scheduler: &S, - view: &V, - at: DatasetVersion, - exec: lance_graph_contract::kanban::ExecTarget, - actor: &ActorRef, -) -> Result, KanbanRouteError> -where - S: VersionScheduler, - V: MailboxSoaView, -{ - // Propose: lower the version event to the next legal move (or `None`). - let Some(proposed) = scheduler.on_version(view, at, exec) else { - return Ok(None); // absorbing / policy-filtered → suppress the no-op tick - }; - // Dispose: the owner applies it (checked); relay an illegal edge as typed. - let inner = ractor::call!(actor, |reply| KanbanMsg::Advance { - to: proposed.to, - reply - }) - .map_err(|e| KanbanRouteError::Rpc(e.to_string()))?; - match inner { - // The owner's emitted move is authoritative for the phase transition, - // witness position, and libet anchor (from the REAL mutation), but it - // defaults to `ExecTarget::Native` and can't know which backend the policy - // chose — overlay the scheduler's selection so a `Jit`/`SurrealQl`/`Elixir` - // target is not silently reported/routed as Native (codex #579 P2). - Ok(mut emitted) => { - emitted.exec = proposed.exec; - Ok(Some(emitted)) - } - Err(e) => Err(KanbanRouteError::Illegal { - from: e.from, - to: e.to, - }), - } -} - -// ─── Capstone run-to-absorbing: drive a mailbox to its terminal column ───────── - -/// Drive a mailbox to its **absorbing column** by repeatedly ticking -/// ([`drive_version_tick`]) until the owner reports no further move -/// (`Commit`/`Prune`). Returns the full forward-arc [`KanbanMove`] trace. -/// -/// This is the actor-side, lance-free analog of the cognitive loop's -/// run-to-absorbing: it proves the OUT/IN-leg substrate carries a mailbox through -/// a complete Rubicon cycle to a terminal state with no panic and no spurious -/// rejection (the integer-only phase/i4 path cannot produce NaN). The live S3 -/// source feeds real `versions()` ticks through the same `drive_version_tick`; -/// here the loop counter stands in for the version stream. -/// -/// `max_ticks` bounds the loop defensively. The pure forward arc always reaches -/// `Commit` (`Planning → CognitiveWork → Evaluation → Commit`), so the bound is a -/// guard against a future non-terminating policy, not a normal exit: exceeding it -/// returns [`KanbanRouteError::Rpc`] with a non-termination note rather than -/// looping forever. -/// ⚠ **LEGACY (2026-08-04)** — drives through the legacy arms. -/// Re-exported from `lib.rs`; notice repeated for callers. -/// -pub async fn run_to_absorbing( - actor: &ActorRef, - max_ticks: usize, -) -> Result, KanbanRouteError> { - let mut trace = Vec::new(); - for tick in 0..max_ticks { - match drive_version_tick(actor, DatasetVersion(tick as u64 + 1)).await? { - Some(mv) => trace.push(mv), - None => return Ok(trace), // absorbing column reached — the cycle ended - } - } - Err(KanbanRouteError::Rpc(format!( - "run_to_absorbing did not reach an absorbing column within {max_ticks} ticks" - ))) -} - #[cfg(test)] mod tests { use super::*; use lance_graph_contract::collapse_gate::MailboxId; - use lance_graph_contract::kanban::ExecTarget; - use lance_graph_contract::soa_view::MailboxSoaView; - /// Minimal in-RAM owner (mirrors the contract's `FakeSoa`) — proves the actor - /// owns and advances a real `MailboxSoaOwner` without any heavy SoA backing. - struct TestBoard { + /// Minimal read-only view for census tests — phases only, no owner + /// surface needed (the census never mutates). + struct ViewBoard { id: MailboxId, phase: KanbanColumn, - cycle: u32, } - impl MailboxSoaView for TestBoard { + impl ViewBoard { + fn new(id: MailboxId, phase: KanbanColumn) -> Self { + Self { id, phase } + } + } + + impl MailboxSoaView for ViewBoard { fn mailbox_id(&self) -> MailboxId { self.id } @@ -481,7 +213,7 @@ mod tests { (self.id & 0x3F) as u8 } fn current_cycle(&self) -> u32 { - self.cycle + 0 } fn phase(&self) -> KanbanColumn { self.phase @@ -500,444 +232,94 @@ mod tests { } } - impl MailboxSoaOwner for TestBoard { - fn advance_phase(&mut self, to: KanbanColumn) -> KanbanMove { - let from = self.phase; - self.phase = to; - self.cycle = self.cycle.wrapping_add(1); - KanbanMove { - mailbox: self.id, - from, - to, - witness_chain_position: self.cycle, - exec: ExecTarget::Native, - } - } - } - - fn board(phase: KanbanColumn) -> TestBoard { - TestBoard { - id: 42, - phase, - cycle: 0, - } - } - - #[tokio::test] - async fn actor_advances_its_own_phase_on_message() { - let (actor, handle) = Actor::spawn( - None, - KanbanActor::::default(), - board(KanbanColumn::Planning), - ) - .await - .expect("spawn kanban actor"); - - // Legal forward arc Planning -> CognitiveWork: the owner advances itself. - let mv = ractor::call!(actor, |reply| KanbanMsg::Advance { - to: KanbanColumn::CognitiveWork, - reply - }) - .expect("rpc") - .expect("legal Rubicon edge"); - assert_eq!(mv.from, KanbanColumn::Planning); - assert_eq!(mv.to, KanbanColumn::CognitiveWork); - - // The advance persisted in the owned SoA. - let phase = ractor::call!(actor, |reply| KanbanMsg::Phase { reply }).expect("rpc"); - assert_eq!(phase, KanbanColumn::CognitiveWork); - - actor.stop(None); - handle.await.expect("actor join"); - } - - #[tokio::test] - async fn illegal_edge_is_a_typed_error_no_mutation() { - let (actor, handle) = Actor::spawn( - None, - KanbanActor::::default(), - board(KanbanColumn::Planning), - ) - .await - .expect("spawn kanban actor"); - - // Planning -> Commit is NOT a legal Rubicon edge: typed error, no mutation. - let err = ractor::call!(actor, |reply| KanbanMsg::Advance { - to: KanbanColumn::Commit, - reply - }) - .expect("rpc") - .expect_err("illegal edge must be rejected"); - assert_eq!(err.from, KanbanColumn::Planning); - assert_eq!(err.to, KanbanColumn::Commit); - - // Phase unchanged — the owner did not mutate on the rejected edge. - let phase = ractor::call!(actor, |reply| KanbanMsg::Phase { reply }).expect("rpc"); - assert_eq!(phase, KanbanColumn::Planning); - - actor.stop(None); - handle.await.expect("actor join"); - } - #[test] - fn parse_kanban_step_shapes() { - assert_eq!( - parse_kanban_step("kanban.mb42.cognitive_work"), - Some(("mb42", KanbanColumn::CognitiveWork)) + fn census_counts_a_mixed_fleet_and_is_not_at_rest() { + // Can-fire half: a NON-TRIVIAL mixed fleet must be counted exactly and + // must not read as at-rest while any owner is mid-arc. + let fleet = [ + ViewBoard::new(1, KanbanColumn::Planning), + ViewBoard::new(2, KanbanColumn::Planning), + ViewBoard::new(3, KanbanColumn::CognitiveWork), + ViewBoard::new(4, KanbanColumn::Commit), + ]; + let census = PhaseCensus::observe(fleet.iter()); + assert_eq!(census.total(), 4); + assert_eq!(census.count(KanbanColumn::Planning), 2); + assert_eq!(census.count(KanbanColumn::CognitiveWork), 1); + assert_eq!(census.count(KanbanColumn::Commit), 1); + assert_eq!(census.count(KanbanColumn::Prune), 0); + assert_eq!(census.absorbing(), 1, "only the Commit owner is absorbing"); + assert!( + !census.at_rest(), + "three owners are mid-arc — at_rest firing here would make the \ + signal as uninformative as one that never fires" ); - assert_eq!(parse_kanban_step("lg.foo"), None); // wrong domain - assert_eq!(parse_kanban_step("kanban.mb42"), None); // no phase - assert_eq!(parse_kanban_step("kanban..commit"), None); // empty mailbox - assert_eq!(parse_kanban_step("kanban.mb42.bogus"), None); // unknown phase } - #[tokio::test] - async fn delivery_edge_resolves_via_registry_then_advances() { - // Register the owning actor under a name — `where_is` is the actor - // system's own registry, the S4 addressing source (no bespoke registry). - let name = "mb-kanban-route-test"; - let (actor, handle) = Actor::spawn( - Some(name.to_string()), - KanbanActor::::default(), - board(KanbanColumn::Planning), - ) - .await - .expect("spawn named"); - - // Legal: kanban..cognitive_work → resolves → owner advances. - let mv = deliver_kanban_step(&format!("kanban.{name}.cognitive_work")) - .await - .expect("delivered + advanced"); - assert_eq!(mv.to, KanbanColumn::CognitiveWork); - - // Unknown mailbox → graceful routing miss (NOT a panic, NOT a no-owner). - assert!(matches!( - deliver_kanban_step("kanban.no-such-mailbox.cognitive_work").await, - Err(KanbanRouteError::NoMailbox(_)) - )); - - // Illegal Rubicon edge → typed Illegal, relayed from the owner. - assert!(matches!( - deliver_kanban_step(&format!("kanban.{name}.commit")).await, - Err(KanbanRouteError::Illegal { .. }) - )); - - // Malformed step type → BadStepType. - assert!(matches!( - deliver_kanban_step("lg.noop").await, - Err(KanbanRouteError::BadStepType(_)) - )); - - actor.stop(None); - handle.await.expect("actor join"); - } - - #[tokio::test] - async fn s2_driver_gate_advances_then_holds() { - let (actor, handle) = Actor::spawn( - None, - KanbanActor::::default(), - board(KanbanColumn::Planning), - ) - .await - .expect("spawn"); - - // Flow qualia (warmth/groundedness high, low tension, calibrated) + - // mantissa>0 → gate Flow → forward advance Planning → CognitiveWork. - let flow_q = QualiaI4_16D(0).with(3, 4).with(14, 3).with(9, 4).with(1, 2); - let mv = drive_mul_advance(&actor, flow_q, 4) - .await - .expect("driver ok") - .expect("advanced on Flow"); - assert_eq!(mv.from, KanbanColumn::Planning); - assert_eq!(mv.to, KanbanColumn::CognitiveWork); - - // Neutral qualia + mantissa 0 → gate Hold → None (owner stays put). - let held = drive_mul_advance(&actor, QualiaI4_16D(0), 0) - .await - .expect("driver ok"); - assert!(held.is_none(), "Hold must not advance"); - let phase = ractor::call!(actor, |reply| KanbanMsg::Phase { reply }).expect("rpc"); - assert_eq!(phase, KanbanColumn::CognitiveWork); - - actor.stop(None); - handle.await.expect("actor join"); + #[test] + fn census_at_rest_stays_silent_on_a_fully_absorbed_fleet() { + // Can-stay-silent half, on a NON-TRIVIAL input: a fleet fully in + // absorbing columns (both of them) reads at rest. + let fleet = [ + ViewBoard::new(1, KanbanColumn::Commit), + ViewBoard::new(2, KanbanColumn::Commit), + ViewBoard::new(3, KanbanColumn::Prune), + ]; + let census = PhaseCensus::observe(fleet.iter()); + assert_eq!(census.total(), 3); + assert_eq!(census.absorbing(), 3); + assert!(census.at_rest()); } - #[tokio::test] - async fn concurrent_mul_drivers_serialize_no_spurious_rejection() { - // codex #578: two concurrent Flow drivers must NOT both read a stale - // `Planning` and collide. The atomic `MulAdvance` serializes gate+advance - // in the owner's mailbox, so they chain Planning→CognitiveWork→Evaluation - // — both succeed, neither is a spurious `Illegal`. - let (actor, handle) = Actor::spawn( - None, - KanbanActor::::default(), - board(KanbanColumn::Planning), - ) - .await - .expect("spawn"); - - let flow = || QualiaI4_16D(0).with(3, 4).with(14, 3).with(9, 4).with(1, 2); - let a1 = actor.clone(); - let a2 = actor.clone(); - let (r1, r2) = tokio::join!( - drive_mul_advance(&a1, flow(), 4), - drive_mul_advance(&a2, flow(), 4), - ); - - // Neither call is a spurious rejection; both advanced along the arc. - assert!(r1.expect("driver1 ok").is_some(), "first advanced"); - assert!(r2.expect("driver2 ok").is_some(), "second advanced"); - - // Serialized chain: Planning → CognitiveWork → Evaluation. - let phase = ractor::call!(actor, |reply| KanbanMsg::Phase { reply }).expect("rpc"); - assert_eq!(phase, KanbanColumn::Evaluation); - - actor.stop(None); - handle.await.expect("actor join"); + #[test] + fn census_of_nothing_asserts_nothing() { + // Emptiness handling (documented semantics), separate from the + // discrimination halves above: observing zero owners is NOT rest. + let census = PhaseCensus::observe(std::iter::empty::<&ViewBoard>()); + assert_eq!(census.total(), 0); + assert!(!census.at_rest()); } - #[tokio::test] - async fn version_tick_advances_forward_arc_then_suppresses_at_absorbing() { - // S3 IN-leg: a version tick advances along the forward arc; once the owner - // reaches an absorbing column the tick is a suppressed no-op (`None`). - let (actor, handle) = Actor::spawn( - None, - KanbanActor::::default(), - board(KanbanColumn::Planning), - ) - .await - .expect("spawn"); - - // Planning → CognitiveWork → Evaluation → Commit, one tick per version. - let expected = [ - KanbanColumn::CognitiveWork, - KanbanColumn::Evaluation, - KanbanColumn::Commit, + #[test] + fn census_record_accumulates_like_observe() { + let fleet = [ + ViewBoard::new(1, KanbanColumn::Evaluation), + ViewBoard::new(2, KanbanColumn::Plan), ]; - for (i, want) in expected.iter().enumerate() { - let mv = drive_version_tick(&actor, DatasetVersion(i as u64 + 1)) - .await - .expect("tick ok") - .expect("non-absorbing advances"); - assert_eq!(mv.to, *want); + let observed = PhaseCensus::observe(fleet.iter()); + let mut recorded = PhaseCensus::default(); + for v in &fleet { + recorded.record(v.phase); } - - // Commit is absorbing: the next tick advances nothing (no-op suppressed). - let noop = drive_version_tick(&actor, DatasetVersion(99)) - .await - .expect("tick ok"); - assert!(noop.is_none(), "absorbing column must suppress the tick"); - let phase = ractor::call!(actor, |reply| KanbanMsg::Phase { reply }).expect("rpc"); - assert_eq!(phase, KanbanColumn::Commit); - - actor.stop(None); - handle.await.expect("actor join"); + assert_eq!(observed, recorded); } - #[tokio::test] - async fn concurrent_version_ticks_serialize_along_the_arc() { - // Two concurrent ticks must NOT both read a stale `Planning`; the atomic - // `Tick` serializes decision+advance in the owner's mailbox, so they chain - // Planning → CognitiveWork → Evaluation (both advance, neither is lost). - let (actor, handle) = Actor::spawn( - None, - KanbanActor::::default(), - board(KanbanColumn::Planning), - ) - .await - .expect("spawn"); - - let a1 = actor.clone(); - let a2 = actor.clone(); - let (r1, r2) = tokio::join!( - drive_version_tick(&a1, DatasetVersion(1)), - drive_version_tick(&a2, DatasetVersion(2)), + #[test] + fn mul_target_flow_advances_and_hold_holds() { + // Flow qualia (warmth/groundedness high, low tension, calibrated) + + // mantissa>0 → gate Flow → forward Planning → CognitiveWork. Same + // construction cycle_driver's flow_qualia() helper uses. + let flow_q = QualiaI4_16D(0).with(3, 4).with(14, 3).with(9, 4).with(1, 2); + assert_eq!( + mul_target(KanbanColumn::Planning, &flow_q, 4), + Some(KanbanColumn::CognitiveWork) ); - assert!(r1.expect("tick1 ok").is_some(), "first advanced"); - assert!(r2.expect("tick2 ok").is_some(), "second advanced"); - - let phase = ractor::call!(actor, |reply| KanbanMsg::Phase { reply }).expect("rpc"); - assert_eq!(phase, KanbanColumn::Evaluation); - - actor.stop(None); - handle.await.expect("actor join"); - } - - #[tokio::test] - async fn custom_scheduler_proposes_and_owner_disposes() { - use lance_graph_contract::scheduler::NextPhaseScheduler; - - // The generic consumer drives the EXISTING `VersionScheduler` trait: the - // reference `NextPhaseScheduler` proposes from a view, the owner disposes. - let (actor, handle) = Actor::spawn( - None, - KanbanActor::::default(), - board(KanbanColumn::Planning), - ) - .await - .expect("spawn"); - - // View mirrors the owner's current phase; scheduler proposes CognitiveWork. - let view = board(KanbanColumn::Planning); - let mv = drive_scheduled_tick( - &NextPhaseScheduler, - &view, - DatasetVersion(1), - ExecTarget::Native, - &actor, - ) - .await - .expect("scheduled ok") - .expect("forward arc proposed + disposed"); - assert_eq!(mv.from, KanbanColumn::Planning); - assert_eq!(mv.to, KanbanColumn::CognitiveWork); - - // An absorbing view → scheduler yields `None` → suppressed, no RPC needed. - let absorbing_view = board(KanbanColumn::Commit); - let noop = drive_scheduled_tick( - &NextPhaseScheduler, - &absorbing_view, - DatasetVersion(2), - ExecTarget::Native, - &actor, - ) - .await - .expect("scheduled ok"); - assert!(noop.is_none(), "absorbing proposal is suppressed"); - - actor.stop(None); - handle.await.expect("actor join"); - } - - #[tokio::test] - async fn scheduled_tick_preserves_non_native_exec_target() { - use lance_graph_contract::scheduler::NextPhaseScheduler; - - // codex #579 P2: the scheduler selects the backend; the owner defaults to - // `Native`. The returned move must carry the scheduler's exec, NOT be - // flattened to the owner's Native default. - for exec in [ExecTarget::Jit, ExecTarget::SurrealQl, ExecTarget::Elixir] { - // Fresh owner per exec so the phase starts at Planning each iteration. - let (actor, handle) = Actor::spawn( - None, - KanbanActor::::default(), - board(KanbanColumn::Planning), - ) - .await - .expect("spawn"); - - let view = board(KanbanColumn::Planning); - let mv = - drive_scheduled_tick(&NextPhaseScheduler, &view, DatasetVersion(1), exec, &actor) - .await - .expect("scheduled ok") - .expect("forward arc proposed + disposed"); - assert_eq!(mv.to, KanbanColumn::CognitiveWork); - assert_eq!( - mv.exec, exec, - "scheduler's backend must survive, not be overwritten with Native" - ); - - actor.stop(None); - handle.await.expect("actor join"); - } - } - - #[tokio::test] - async fn run_to_absorbing_drives_a_full_rubicon_cycle_no_nan_no_panic() { - // Capstone run-NaN (actor-side, lance-free): a mailbox driven from - // Planning runs to the absorbing Commit column through the REAL actor - // messages — it terminates, never panics, never emits a spurious Illegal, - // and the trace is the deterministic forward arc. The integer phase/i4 - // path cannot produce NaN, so a green run here IS the actor-side half of - // the loop's run-NaN answer. - let (actor, handle) = Actor::spawn( - None, - KanbanActor::::default(), - board(KanbanColumn::Planning), - ) - .await - .expect("spawn"); - - let trace = run_to_absorbing(&actor, 16) - .await - .expect("reaches an absorbing column within the bound"); - - // Forward arc: Planning → CognitiveWork → Evaluation → Commit (3 moves). - let arc: Vec<_> = trace.iter().map(|m| m.to).collect(); + // Neutral qualia + mantissa 0 → gate Hold → None. assert_eq!( - arc, - vec![ - KanbanColumn::CognitiveWork, - KanbanColumn::Evaluation, - KanbanColumn::Commit, - ] + mul_target(KanbanColumn::Planning, &QualiaI4_16D(0), 0), + None ); - // Every move en route is a legal Rubicon edge (no corruption). - for m in &trace { - assert!( - m.from.can_transition_to(m.to), - "{:?} -> {:?} must be legal", - m.from, - m.to - ); - } - - // The owner rests in the absorbing column: a further run is empty, and - // the phase is unchanged (idempotent at rest — no spurious advance/error). - let again = run_to_absorbing(&actor, 4) - .await - .expect("idempotent at the absorbing column"); - assert!(again.is_empty(), "absorbing column yields no further moves"); - let phase = ractor::call!(actor, |reply| KanbanMsg::Phase { reply }).expect("rpc"); - assert_eq!(phase, KanbanColumn::Commit); - - actor.stop(None); - handle.await.expect("actor join"); } - #[tokio::test] - async fn mixed_triggers_compose_on_one_owner_s2_gate_then_s3_ticks() { - // The capstone integration: the S2 MUL gate and the S3 version tick are - // DIFFERENT triggers driving the SAME owner. Here the first Rubicon step - // is taken by a MUL gate (Flow qualia → Planning→CognitiveWork) and the - // rest by version ticks (run_to_absorbing → …→Commit). Both compose - // cleanly on one mailbox-as-owner: no panic, no spurious rejection, lands - // absorbing. - let (actor, handle) = Actor::spawn( - None, - KanbanActor::::default(), - board(KanbanColumn::Planning), - ) - .await - .expect("spawn"); - - // S2: the MUL gate takes the first step (Flow qualia + mantissa>0). - let flow_q = QualiaI4_16D(0).with(3, 4).with(14, 3).with(9, 4).with(1, 2); - let gated = drive_mul_advance(&actor, flow_q, 4) - .await - .expect("gate driver ok") - .expect("Flow advances"); - assert_eq!(gated.from, KanbanColumn::Planning); - assert_eq!(gated.to, KanbanColumn::CognitiveWork); - - // S3: version ticks carry the rest of the arc to the absorbing column. - let tail = run_to_absorbing(&actor, 16) - .await - .expect("ticks reach absorbing"); - let arc: Vec<_> = tail.iter().map(|m| m.to).collect(); + #[test] + fn parse_kanban_step_shapes() { assert_eq!( - arc, - vec![KanbanColumn::Evaluation, KanbanColumn::Commit], - "S3 ticks resume from where the S2 gate left the owner" + parse_kanban_step("kanban.mb42.cognitive_work"), + Some(("mb42", KanbanColumn::CognitiveWork)) ); - - let phase = ractor::call!(actor, |reply| KanbanMsg::Phase { reply }).expect("rpc"); - assert_eq!(phase, KanbanColumn::Commit); - - actor.stop(None); - handle.await.expect("actor join"); + assert_eq!(parse_kanban_step("lg.foo"), None); // wrong domain + assert_eq!(parse_kanban_step("kanban.mb42"), None); // no phase + assert_eq!(parse_kanban_step("kanban..commit"), None); // empty mailbox + assert_eq!(parse_kanban_step("kanban.mb42.bogus"), None); // unknown phase } } diff --git a/crates/lance-graph-supervisor/src/lib.rs b/crates/lance-graph-supervisor/src/lib.rs index 5244c81df..1536bdb98 100644 --- a/crates/lance-graph-supervisor/src/lib.rs +++ b/crates/lance-graph-supervisor/src/lib.rs @@ -70,17 +70,16 @@ pub mod supervisor; #[cfg(feature = "supervisor")] pub mod actors; -/// Legacy kanban actor surface — no assigned architectural responsibility. -/// Phase progression is the #879 sealed-cycle path (`cycle_driver`); see the -/// module header for the legacy notice and the caller migration inventory. +/// Message-free kanban visibility + pure helpers. The actor/ack/tick surface +/// that used to live here was DELETED 2026-08-05 +/// (`E-PROGRESSION-IS-EXISTENCE-NOT-COMMAND-1`): phase progression is the +/// #879 sealed-cycle path (`cycle_driver`); observation is a `&self` census, +/// never an RPC. See the module tombstone. #[cfg(feature = "supervisor")] pub mod kanban_actor; #[cfg(feature = "supervisor")] -pub use kanban_actor::{ - deliver_kanban_step, drive_mul_advance, drive_scheduled_tick, drive_version_tick, - run_to_absorbing, KanbanActor, KanbanMsg, KanbanRouteError, -}; +pub use kanban_actor::{mul_target, parse_kanban_step, PhaseCensus}; #[cfg(feature = "supervisor")] pub use supervisor::{ diff --git a/crates/lance-graph-supervisor/tests/d_ign_b_lenses.rs b/crates/lance-graph-supervisor/tests/d_ign_b_lenses.rs new file mode 100644 index 000000000..c975e3c7c --- /dev/null +++ b/crates/lance-graph-supervisor/tests/d_ign_b_lenses.rs @@ -0,0 +1,1145 @@ +//! D-IGN-B — lens SELECTION over the shipped four-stance panel, driven +//! through the same no-messaging cast/scan machinery PROBE-IGNITION proved. +//! +//! Spec: `.claude/board/exec-runs/d-ign-b-design-opus.md` (design, Opus, +//! design-only, no code) + `.claude/board/exec-runs/d-ign-b-api-inventory-sonnet.md` +//! (exact signatures — wins on any design/inventory conflict). Scaffolding +//! provenance: `crates/lance-graph-supervisor/tests/probe_ignition.rs` +//! (GREEN, 2/2, G1-G11 both halves) — this file copies its fleet/scan/cast +//! shapes with provenance noted at each site, per the build brief. +//! +//! ## The headline finding this file must never blur (design §0, F0) +//! +//! `stance_panel(arena, intern, out)` (`stance.rs:469-478`) returns all +//! FOUR stances as ONE 4-tuple computed by ONE call. There is no per-stance +//! dispatch and no way to compute one stance alone without refactoring the +//! shipped function. **Arming SELECTS what is READ, not what is computed.** +//! Every armed owner runs the identical panel over its own arena; the +//! ordinal `z` (read from `owner.meta_at(0).thinking()`, `MetaWord`'s +//! 6-bit `thinking` field, `cognitive_shader.rs:42-76`) picks which of the +//! four tuple elements this file records as that owner's readout. No +//! sentence in this file may say "dispatch" — it says "selection". +//! +//! ## What the lens does NOT read (design §0, F1) +//! +//! A row's content is a one-way bloom plane (4 bits/token OR'd into a +//! `WORDS_PER_FP`-word plane, `mailbox_soa.rs` via `encode_plane` below) — +//! there is no inverse from plane bits back to text. The lens body never +//! decodes SoA row bytes; it re-reads the OWNER'S OWN verse-text slice, +//! selected by the owner's address (`owner_idx * POPULATED_ROWS`) and span, +//! the same slice `build_owner` seeded that owner's bloom planes from. The +//! SoA governs selection (ownership + scope + arming + phase); the text +//! comes from the corpus the rows were seeded from, not from the rows. +//! +//! ## z=5 (Fusion) — BLOCKED in-cycle (design §3, the deliverable per brief) +//! +//! `blw_fusion.rs`'s ranking pool must GROW across many sealed horizons +//! (`S_CYCLES = 8`, `blw_fusion.rs:123`) for Strict-vs-Aware admission to +//! differ at all. This file's fleet is seeded ONCE and each owner accrues +//! at most one or two sealed horizons — with one horizon Strict and Aware +//! admit the same rows and any Δ is 0 BY CONSTRUCTION, the exact vacuous- +//! readout shape the workspace's falsifiability rule exists to reject. +//! `jc` is also not a dependency of this crate (`lance-graph-supervisor/ +//! Cargo.toml` has no `jc` edge — a manifest change, not a worker's call). +//! Ordinal 5 stays RESERVED in the arming vocabulary below: never mapped +//! onto z=1..4, never armed on any owner in this file's fleet. See the +//! "z=5 BLOCKED" check near the end of the test body. +//! +//! ## Deviations from the design note, stated here (no others) +//! +//! 1. **L5's cited "20 Flow + 4 Block" decomposition does not apply to +//! this file's cohorts.** That figure is PROBE-IGNITION's own number +//! (`probe_ignition.rs:977-984`), produced by a CONTRA cohort (4 owners +//! on `block_qualia()`) that design §5's cohort re-carve does not +//! include here — every in-scope armed owner in this file uses +//! `flow_qualia()`. Re-asserting "20+4" verbatim would be a false, +//! copy-pasted claim. L5 instead asserts the DERIVED expectation for +//! THIS file's cohorts (all 30 armed owners Flow-advance, 0 Block) and +//! prints the measured counts rather than silently trusting them. +//! 2. **`thinking_style_for` maps z=4 onto the SAME `ThinkingStyle` as +//! z=3 (`Reflective`).** The lens-selection ordinal (1..4, which of the +//! four stance-panel tuple elements to record) and the `StyleStrategy` +//! dispatch input share one 6-bit field by design (`owner.meta_at(0) +//! .thinking()` is read for both purposes, per design §1). This file +//! needed a 4th distinct lens ordinal but only 3 `ThinkingStyle` +//! variants were verified reachable from this crate in the probe's own +//! inventory (Analytical/Creative/Reflective) — inventing a 4th +//! variant's exact discriminant was not verified in this pass, so z=4 +//! reuses Reflective's style rather than guess. This affects ONLY the +//! `reliability`/gate-decision input, never the lens SELECTION itself +//! (`run_lens` switches on `z` directly, independent of `ThinkingStyle`). +//! 3. **`LensReadout::digest` folds NO variant-discriminant tag.** An +//! earlier draft hashed a `0u8`/`1u8`/`2u8`/`3u8` tag before each +//! variant's contents, which makes any cross-lens `!=` comparison pass +//! by construction of the tag alone — an assertion implied by the +//! digest function itself, not a test of content (this workspace's +//! falsifiability rule). Removed; see the doc comment on `digest()`. +//! +//! ## Not compiled, not run by this lane — orchestrator gates +//! +//! This file was written edit-only (no `cargo` of any kind). Every +//! signature cited was read from source in the same pass that wrote this +//! file (see the build tag-file, `.claude/board/exec-runs/d-ign-b-build.md`, +//! for what could and could not be verified). + +#[cfg(feature = "cycle-driver")] +mod d_ign_b_lenses { + #![allow( + clippy::cast_possible_truncation, + clippy::cast_possible_wrap, + clippy::cast_sign_loss + )] + + use std::collections::{HashMap, HashSet}; + use std::hash::{Hash, Hasher}; + use std::sync::atomic::{AtomicU64, Ordering}; + use std::sync::Mutex; + + use cognitive_shader_driver::mailbox_soa::{MailboxSoA, WriteCell, WriteOutcome, WORDS_PER_FP}; + use lance_graph_contract::cognitive_shader::MetaWord; + use lance_graph_contract::collapse_gate::MailboxId; + use lance_graph_contract::kanban::{ExecTarget, KanbanColumn}; + use lance_graph_contract::mul::i4_eval::gate_decision_i4; + use lance_graph_contract::qualia::QualiaI4_16D; + use lance_graph_contract::scheduler::DatasetVersion; + use lance_graph_contract::soa_view::MailboxSoaView; + use lance_graph_contract::thinking::ThinkingStyle; + use lance_graph_planner::batch_writer::BatchWriter; + use lance_graph_planner::ir::Arena; + use lance_graph_planner::nars::stance::{stance_panel, stream, FlipKind, Interner, ReadOut}; + use lance_graph_planner::nars::{BeliefArena, CStmt}; + use lance_graph_planner::owner_adapter::emit_bootstrap_intent; + use lance_graph_planner::persist_sink::{ + CycleFrame, CycleId, DetachedCycleBatch, LandedSlot, SweepSlot, WalSink, WriteFailed, + }; + use lance_graph_planner::strategy::style_strategy::StyleStrategy; + use lance_graph_planner::traits::{ + PlanContext, PlanInput, PlanStrategy, QueryFeatures, StrategyOutcome, + }; + use lance_graph_supervisor::cycle_driver::{ + run_cognitive_work_gated_over, run_cycle, shade_owner, CycleError, CycleOutcome, + }; + + // ── PRE-REGISTERED run shape (design §5) — fixed BEFORE any run. ─────── + + const FLEET_OWNERS: MailboxId = 64; + const ROWS_PER_OWNER: usize = 64; + const POPULATED_ROWS: usize = 48; + const CORPUS_VERSES: usize = FLEET_OWNERS as usize * POPULATED_ROWS; // 3072 + const SCOPE_LO: MailboxId = 0; + const SCOPE_HI: MailboxId = 32; + const CYCLES: u32 = 2; + + // Cohorts — design §5's ONE run-shape change (the twin block), the rest + // of PROBE-IGNITION's cohort table dropped (design §5 justification). + const TWIN_LO: MailboxId = 0; + const TWIN_HI: MailboxId = 8; + const SPREAD_LO: MailboxId = 8; + const SPREAD_HI: MailboxId = 30; + const UNARMED_ID: MailboxId = 30; + const ORPHAN_ID: MailboxId = 31; + const OUTSIDE_LO: MailboxId = 32; + const OUTSIDE_HI: MailboxId = 64; + + /// Per-offset arming inside the twin block: z=1,1,2,2,3,3,4,4 (design §4 + /// "twin premise" table). Byte-identical rows, four distinct lenses. + const TWIN_ARMING: [u8; 8] = [1, 1, 2, 2, 3, 3, 4, 4]; + + const TENANT_THRESHOLD: f32 = 1.0; + const FIRE_ENERGY: f32 = 2.0; + const TENANT_W_SLOT: u8 = 0; + + type Tenant = MailboxSoA; + type Fleet = HashMap; + + // ── ThinkingStyle dispatch input — SEPARATE from the lens-selection + // switch (`run_lens` below); see module doc deviation 2. ─────────────── + + fn thinking_style_for(z: u8) -> ThinkingStyle { + match z { + 1 => ThinkingStyle::Analytical, + 2 => ThinkingStyle::Creative, + _ => ThinkingStyle::Reflective, // z=3 and z=4 (deviation 2) + } + } + + fn style_vector_for(z: u8) -> Vec { + let mut v = vec![0.0f64; 23]; + match thinking_style_for(z) { + ThinkingStyle::Analytical => v[4] = 1.0, + ThinkingStyle::Creative => v[3] = 1.0, + _ => v[0] = 1.0, + } + v + } + + fn plan_context_for(z: u8) -> PlanContext { + PlanContext { + query: String::new(), + features: QueryFeatures::default(), + free_will_modifier: 1.0, + thinking_style: Some(style_vector_for(z)), + nars_hint: None, + witness: None, + } + } + + fn mantissa_of(owner: &Tenant) -> i8 { + owner.pending_count().min(7) as i8 + } + + /// Provenance: `probe_ignition.rs:196-203` (re-derived there from + /// `cycle_driver.rs:1669`'s `#[cfg(test)]` fixture, not importable). + fn flow_qualia() -> QualiaI4_16D { + QualiaI4_16D(0).with(3, 4).with(14, 3).with(9, 4).with(1, 2) + } + + // ── corpus + bloom-plane seeding — copied from `probe_ignition.rs` + // (itself citing `blw_fusion.rs`), same provenance chain. ────────────── + + const BLOOM_K: usize = 4; + + fn fnv1a(bytes: &[u8], seed: u64) -> u64 { + let mut h = 0xcbf2_9ce4_8422_2325_u64 ^ seed.wrapping_mul(0x100_0000_01b3); + for &c in bytes { + h ^= u64::from(c); + h = h.wrapping_mul(0x100_0000_01b3); + } + h + } + + fn bloom_add(plane: &mut [u64], token: &str, salt: u64) { + for k in 0..BLOOM_K { + let h = fnv1a( + token.as_bytes(), + salt ^ (k as u64).wrapping_mul(0x9E37_79B9), + ); + let bit = (h % (WORDS_PER_FP as u64 * 64)) as usize; + plane[bit / 64] |= 1u64 << (bit % 64); + } + } + + fn tokens(text: &str) -> impl Iterator + '_ { + text.split(|c: char| !c.is_ascii_alphanumeric()) + .filter(|t| t.len() >= 2) + .map(str::to_ascii_lowercase) + } + + fn encode_plane(text: &str, salt: u64) -> Vec { + let mut plane = vec![0u64; WORDS_PER_FP]; + for t in tokens(text) { + bloom_add(&mut plane, &t, salt); + } + plane + } + + fn load_verses(path: &str, limit: usize) -> Option> { + let raw = std::fs::read_to_string(path).ok()?; + let verses: Vec = raw + .lines() + .filter_map(|l| l.split_once('\t').map(|(_, t)| t.to_string())) + .take(limit) + .collect(); + (verses.len() == limit).then_some(verses) + } + + fn synthetic_corpus(n: usize) -> Vec { + (0..n) + .map(|i| { + let salt = (i as u64).wrapping_mul(2_654_435_761) % 104_729; + format!("d-ign-b synthetic verse {i} token{salt}") + }) + .collect() + } + + fn load_or_synthesize_corpus() -> (Vec, &'static str) { + let path = + std::env::var("BLW_KJV_TSV").unwrap_or_else(|_| "/tmp/kjv_verses.tsv".to_string()); + match load_verses(&path, CORPUS_VERSES) { + Some(v) => (v, "BLW_KJV_TSV corpus"), + None => ( + synthetic_corpus(CORPUS_VERSES), + "deterministic synthetic fallback", + ), + } + } + + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + struct RowSpanDescriptor { + row_lo: u32, + row_hi: u32, + cycle: u32, + } + + impl RowSpanDescriptor { + fn to_le_bytes(self) -> [u8; 12] { + let mut out = [0u8; 12]; + out[0..4].copy_from_slice(&self.row_lo.to_le_bytes()); + out[4..8].copy_from_slice(&self.row_hi.to_le_bytes()); + out[8..12].copy_from_slice(&self.cycle.to_le_bytes()); + out + } + } + + fn row_span_payload(owner: &Tenant) -> Vec { + RowSpanDescriptor { + row_lo: 0, + row_hi: owner.populated() as u32, + cycle: owner.cycle(), + } + .to_le_bytes() + .to_vec() + } + + // ── the WAL seam (in-process; NOT durability) — copied from + // `probe_ignition.rs`'s `MemWal`. ─────────────────────────────────────── + + struct SealedCycle { + version: DatasetVersion, + landings: Vec, + } + + struct MemWal { + sealed: Mutex>, + next_version: AtomicU64, + wal_writes: AtomicU64, + } + + impl MemWal { + fn new() -> Self { + Self { + sealed: Mutex::new(Vec::new()), + next_version: AtomicU64::new(1), + wal_writes: AtomicU64::new(0), + } + } + fn head(&self) -> DatasetVersion { + self.sealed + .lock() + .expect("MemWal poisoned") + .last() + .map_or(DatasetVersion(0), |s| s.version) + } + } + + impl WalSink for MemWal { + async fn commit_cycle( + &self, + base: DatasetVersion, + batch: DetachedCycleBatch, + ) -> Result { + let mut sealed = self.sealed.lock().expect("MemWal poisoned"); + let head = sealed.last().map_or(DatasetVersion(0), |s| s.version); + if base != head { + return Err(WriteFailed(format!( + "stale base {base:?}: sealed head is {head:?}" + ))); + } + self.wal_writes.fetch_add(1, Ordering::SeqCst); + let version = DatasetVersion(self.next_version.fetch_add(1, Ordering::SeqCst)); + sealed.push(SealedCycle { + version, + landings: batch.landings, + }); + Ok(version) + } + + async fn scan_sealed( + &self, + from_version: Option, + ) -> Result, WriteFailed> { + Ok(self + .sealed + .lock() + .expect("MemWal poisoned") + .iter() + .filter(|s| from_version.is_none_or(|f| s.version > f)) + .flat_map(|s| { + s.landings.iter().map(|slot| LandedSlot { + version: s.version, + slot: slot.clone(), + }) + }) + .collect()) + } + + async fn versions(&self) -> Result, WriteFailed> { + Ok(self + .sealed + .lock() + .expect("MemWal poisoned") + .iter() + .map(|s| { + ( + s.landings.first().map_or(CycleId(0), |l| l.cycle), + s.version, + ) + }) + .collect()) + } + } + + // ── fleet construction ────────────────────────────────────────────────── + + /// Owner `owner_idx`'s own text slice, address-selected — the SAME + /// slice `build_owner` bloom-seeds from and `labelled_verses` reads for + /// the lens (F1: one text source, never row-byte decoding). + fn owner_verses(all: &[String], owner_idx: MailboxId) -> &[String] { + let lo = owner_idx as usize * POPULATED_ROWS; + &all[lo..lo + POPULATED_ROWS] + } + + /// `(label, text)` pairs `stance::stream` wants (`stance.rs:161-167`). + /// Label format `"kjv:{global_index:05}"` (design §7 Q2, matching + /// `blw_fusion.rs:220`'s subject format). + fn labelled_verses(all: &[String], base_owner: MailboxId) -> Vec<(String, String)> { + let lo = base_owner as usize * POPULATED_ROWS; + all[lo..lo + POPULATED_ROWS] + .iter() + .enumerate() + .map(|(i, text)| (format!("kjv:{:05}", lo + i), text.clone())) + .collect() + } + + fn build_owner( + id: MailboxId, + verses: &[String], + content_salt: u64, + armed: u8, + qualia: QualiaI4_16D, + firing_rows: usize, + ) -> Tenant { + let mut owner: Tenant = MailboxSoA::new(id, TENANT_W_SLOT, TENANT_THRESHOLD); + let cycle = owner.cycle(); + let meta = MetaWord::new(armed, 0, 0, 0, 0); + for (row, text) in verses.iter().enumerate() { + let content = encode_plane(text, content_salt); + let cell = WriteCell { + content: Some(content.as_slice()), + qualia: Some(qualia), + meta: Some(meta), + entity_type: Some((row % 251) as u16), + temporal: Some(row as u64), + ..WriteCell::default() + }; + let outcome = owner.write_row(row, cycle, &cell); + assert_eq!( + outcome, + WriteOutcome::Accepted, + "seeding row {row} of owner {id} must be accepted" + ); + } + owner.set_populated(verses.len()); + owner.tick(); + for r in 0..firing_rows { + owner.energy[r] = FIRE_ENERGY; + } + owner + } + + fn build_fleet(corpus: &[String]) -> Fleet { + let mut fleet = Fleet::new(); + + // TWIN BLOCK: same slice (base owner TWIN_LO), same content salt + // (0) for every owner — so the content planes are byte-identical + // (L0's precondition). Different `id`s only. + for (offset, id) in (TWIN_LO..TWIN_HI).enumerate() { + let armed = TWIN_ARMING[offset]; + fleet.insert( + id, + build_owner( + id, + owner_verses(corpus, TWIN_LO), + 0, + armed, + flow_qualia(), + 3, + ), + ); + } + + // SPREAD BLOCK: distinct slices, distinct content salts (= id), + // armed z cycling 1..4 — feeds L4's cross-owner non-constancy half. + for id in SPREAD_LO..SPREAD_HI { + let armed = (((id - SPREAD_LO) % 4) + 1) as u8; + fleet.insert( + id, + build_owner( + id, + owner_verses(corpus, id), + u64::from(id), + armed, + flow_qualia(), + 3, + ), + ); + } + + // UNARMED (id 30): z=0, never plans. + fleet.insert( + UNARMED_ID, + build_owner( + UNARMED_ID, + owner_verses(corpus, UNARMED_ID), + u64::from(UNARMED_ID), + 0, + flow_qualia(), + 3, + ), + ); + + // ORPHAN (id 31): deliberately NOT inserted. + + // OUTSIDE (32..64): armed, but SCOPE_HI=32 excludes them from every + // scan in the main loop (L7). + for id in OUTSIDE_LO..OUTSIDE_HI { + fleet.insert( + id, + build_owner( + id, + owner_verses(corpus, id), + u64::from(id), + 1, + flow_qualia(), + 3, + ), + ); + } + + fleet + } + + // ── the scan (LOOK INTO THE KANBAN) — copied from `probe_ignition.rs`. ── + + #[derive(Default)] + struct ScanResult { + planning: Vec, + cognitive: Vec, + evaluation: Vec, + absorbed: Vec, + missing: usize, + } + + fn scan_board(fleet: &Fleet, ids: impl IntoIterator) -> ScanResult { + let mut r = ScanResult::default(); + for id in ids { + match fleet.get(&id) { + None => r.missing += 1, + Some(owner) => match owner.phase() { + KanbanColumn::Planning => r.planning.push(id), + KanbanColumn::CognitiveWork => r.cognitive.push(id), + KanbanColumn::Evaluation => r.evaluation.push(id), + KanbanColumn::Commit | KanbanColumn::Plan | KanbanColumn::Prune => { + r.absorbed.push(id); + } + }, + } + } + r + } + + struct ColumnPassOutcome { + cast: usize, + } + + fn column_pass( + fleet: &Fleet, + ids: &[MailboxId], + writer: &mut BatchWriter>, + mut think: impl FnMut(&Tenant) -> Option<(StrategyOutcome, Vec)>, + ) -> ColumnPassOutcome { + let mut cast = 0usize; + for &id in ids { + let Some(owner) = fleet.get(&id) else { + continue; + }; + if let Some((outcome, payload)) = think(owner) { + if emit_bootstrap_intent( + &outcome, + owner.mailbox_id(), + owner.current_cycle(), + writer, + payload, + ) + .is_some() + { + cast += 1; + } + } + } + ColumnPassOutcome { cast } + } + + /// Provenance: `probe_ignition.rs:604-638`, unchanged. + fn plan_or_evaluate_think(owner: &Tenant) -> Option<(StrategyOutcome, Vec)> { + let armed = owner.meta_at(0).thinking(); + if armed == 0 { + return None; + } + let qualia = owner.qualia_at(0); + let mantissa = mantissa_of(owner); + let gate = gate_decision_i4(&qualia, mantissa); + let target = owner.phase().advance_on_gate(&gate)?; + + let mut arena = Arena::new(); + let plan_out = StyleStrategy + .plan( + PlanInput { + plan: None, + context: plan_context_for(armed), + outcome: None, + }, + &mut arena, + ) + .expect("StyleStrategy::plan never errors over this probe's fixed recipe substrate"); + let style_outcome = plan_out + .outcome + .expect("StyleStrategy always surfaces a StrategyOutcome"); + let style_move = style_outcome + .intended_move + .expect("StyleStrategy always intends the Planning -> CognitiveWork crossing"); + + let outcome = if style_move.to == target { + style_outcome + } else { + shade_owner(owner, &qualia, mantissa, style_outcome.reliability)? + }; + Some((outcome, row_span_payload(owner))) + } + + // ── the lens readout type (design §2) — probe-local, mints nothing + // shipped. NOT `CausalWitnessFacet` — that carrier is what §12.7 killed + // (design §2's explicit anti-decision). ───────────────────────────────── + + #[derive(Debug)] + enum LensReadout { + Hegel(Vec<(CStmt, f32)>), + Nietzsche(Vec<(CStmt, FlipKind)>), + Kant(Vec<(String, f32, f32)>), + Wittgenstein(Vec<(u16, usize)>), + } + + impl LensReadout { + fn is_empty(&self) -> bool { + match self { + LensReadout::Hegel(v) => v.is_empty(), + LensReadout::Nietzsche(v) => v.is_empty(), + LensReadout::Kant(v) => v.is_empty(), + LensReadout::Wittgenstein(v) => v.is_empty(), + } + } + + /// Stable fold over the variant's own contents, in the shipped + /// iteration order (design §7 Q3). Floats hashed via `.to_bits()`, + /// never `HashMap` iteration (the Wittgenstein arm is already + /// sorted before `stance_panel` returns it, `stance.rs:530-532` — + /// that pre-sort is the only reason this fold is deterministic). + /// + /// Deliberately NO variant-discriminant tag folded in first. A + /// tagged digest would make any cross-lens `!=` comparison pass + /// by construction of the tag alone (an assertion the code itself + /// implies, not a test of content — the falsifiability rule this + /// workspace's `CLAUDE.md` sets out). Without a tag, two EMPTY + /// readouts of any lens hash equal (both fold zero bytes) — that + /// is intentional: it is what makes L3/L4's non-emptiness and + /// non-constancy checks below able to fail for real, instead of + /// being guaranteed to pass by the digest's own construction. + fn digest(&self) -> u64 { + use std::collections::hash_map::DefaultHasher; + let mut h = DefaultHasher::new(); + match self { + LensReadout::Hegel(v) => { + for (stmt, f) in v { + stmt.hash(&mut h); + f.to_bits().hash(&mut h); + } + } + LensReadout::Nietzsche(v) => { + for (stmt, flip) in v { + stmt.hash(&mut h); + // FlipKind derives Eq but not Hash (api inventory + // §A) — fold its discriminant by hand. + let d: u8 = match flip { + FlipKind::Transvaluation => 0, + FlipKind::Devaluation => 1, + }; + d.hash(&mut h); + } + } + LensReadout::Kant(v) => { + for (label, graded, ablated) in v { + label.hash(&mut h); + graded.to_bits().hash(&mut h); + ablated.to_bits().hash(&mut h); + } + } + LensReadout::Wittgenstein(v) => { + for (concept, games) in v { + concept.hash(&mut h); + games.hash(&mut h); + } + } + } + h.finish() + } + } + + /// The panel computes all four; this SELECTS one (design §2's central + /// consequence). Cost is identical across z — the four cases below + /// differ only in which tuple element they keep. + fn run_lens(z: u8, verses: &[(String, String)]) -> LensReadout { + let mut arena = BeliefArena::new(); + let mut intern = Interner::new(); + let mut out = ReadOut::default(); + stream(verses, &mut arena, &mut intern, &mut out, false); + let (hegel, nietzsche, kant, wittgenstein) = stance_panel(&arena, &intern, &out); + match z { + 1 => LensReadout::Hegel(hegel), + 2 => LensReadout::Nietzsche(nietzsche), + 3 => LensReadout::Kant(kant), + 4 => LensReadout::Wittgenstein(wittgenstein), + other => panic!( + "run_lens: z={other} is outside the armed range 1..=4 \ + (z=0 is unarmed, z=5 is RESERVED/BLOCKED — see the module doc)" + ), + } + } + + /// All four readouts over ONE arena — used only by L4's single-owner + /// cross-lens check (design §4 L4 can-fire half). + fn run_all_lenses(verses: &[(String, String)]) -> [LensReadout; 4] { + let mut arena = BeliefArena::new(); + let mut intern = Interner::new(); + let mut out = ReadOut::default(); + stream(verses, &mut arena, &mut intern, &mut out, false); + let (hegel, nietzsche, kant, wittgenstein) = stance_panel(&arena, &intern, &out); + [ + LensReadout::Hegel(hegel), + LensReadout::Nietzsche(nietzsche), + LensReadout::Kant(kant), + LensReadout::Wittgenstein(wittgenstein), + ] + } + + // ── the main probe ─────────────────────────────────────────────────────── + + #[tokio::test] + async fn d_ign_b_lens_selection_over_byte_identical_rows() { + let (corpus, provenance) = load_or_synthesize_corpus(); + println!("d_ign_b corpus: {provenance} ({} verses)", corpus.len()); + assert_eq!(corpus.len(), CORPUS_VERSES, "PRE-REGISTERED corpus size"); + + let mut fleet = build_fleet(&corpus); + + // ── L0: twin premise ──────────────────────────────────────────── + { + let twin_ids: Vec = (TWIN_LO..TWIN_HI).collect(); + let base = fleet.get(&TWIN_LO).expect("twin base owner must exist"); + for row in 0..POPULATED_ROWS { + let base_plane = base.content_row(row).to_vec(); + assert!( + base_plane.iter().any(|&w| w != 0), + "L0 can-stay-silent: twin content plane row {row} must be non-zero, not the trivial all-zero case" + ); + for &id in &twin_ids { + let owner = fleet.get(&id).expect("twin owner must exist"); + assert_eq!( + owner.content_row(row).to_vec(), + base_plane, + "L0 can-fire: twin owner {id} row {row} must be byte-identical to owner {TWIN_LO}" + ); + } + } + let non_twin = fleet.get(&SPREAD_LO).expect("spread owner must exist"); + assert_ne!( + non_twin.content_row(0).to_vec(), + base.content_row(0).to_vec(), + "L0 can-stay-silent: a non-twin owner's plane must differ from the twin block's" + ); + eprintln!( + "d_ign_b.L0: {} twin owners byte-identical across {POPULATED_ROWS} rows; non-twin owner {SPREAD_LO} differs", + twin_ids.len() + ); + } + + // ── L1 + L4 can-fire half: computed DIRECTLY over the stance panel, + // independent of cycle timing — the selection axis is a property of + // `run_lens`/`stance_panel`, not of when a cast happens to land. ─── + { + let twin_verses = labelled_verses(&corpus, TWIN_LO); + + // Pre-registered per design §4's risk note + §7 Q4: Hegel is + // measured (not assumed) constant-false on this corpus shape + // (§12.3a″); Nietzsche derives from Hegel (stance.rs:483-496) + // so it degrades with it. The can-fire witness pair is + // therefore pinned to z=3 (Kant) vs z=4 (Wittgenstein) BEFORE + // any run, per the design's own pre-registered fallback — + // never chosen after seeing output. + let r_kant = run_lens(3, &twin_verses); + let r_witt = run_lens(4, &twin_verses); + eprintln!( + "d_ign_b.L1.risk-check: Hegel empty on twin base = {}, Nietzsche empty on twin base = {}", + run_lens(1, &twin_verses).is_empty(), + run_lens(2, &twin_verses).is_empty() + ); + assert_ne!( + r_kant.digest(), + r_witt.digest(), + "L1 can-fire: distinct lenses (z=3 Kant vs z=4 Wittgenstein) over byte-identical rows must yield distinct digests" + ); + + // can-stay-silent half: same lens (z=3), twin owners 4 and 5 + // (both armed z=3 per TWIN_ARMING), same rows, same cycle. + let r_kant_a = run_lens(3, &twin_verses); + let r_kant_b = run_lens(3, &twin_verses); + assert_eq!( + r_kant_a.digest(), + r_kant_b.digest(), + "L1 can-stay-silent: the SAME lens over byte-identical rows must yield bit-identical digests" + ); + eprintln!("d_ign_b.L1: selection axis load-bearing (Kant != Wittgenstein digest); same-lens digest bit-identical"); + + // L4 can-fire: over ONE owner, the four lens digests are not + // all equal (>=3 distinct of 4) — the §12.7 anti-collapse shape. + let all_four = run_all_lenses(&twin_verses); + let digests: Vec = all_four.iter().map(LensReadout::digest).collect(); + let mut distinct: Vec = digests.clone(); + distinct.sort_unstable(); + distinct.dedup(); + eprintln!( + "d_ign_b.L4.can-fire: single-owner 4-lens digests = {digests:?} ({} distinct of 4)", + distinct.len() + ); + assert!( + distinct.len() >= 3, + "L4 can-fire: at least 3 of the 4 lens digests must be distinct over one owner" + ); + } + + // ── main cycle loop: cast/scan/seal/apply, unchanged mechanics, + // with the lens embedded in `run_cognitive_work_gated_over`'s + // closure (design §1's chosen seam). ──────────────────────────── + let sink = MemWal::new(); + let mut writer: BatchWriter> = BatchWriter::new(); + let mut position_base: u64 = 0; + let mut watermarks: HashMap> = HashMap::new(); + + let mut readouts: HashMap<(MailboxId, u32), LensReadout> = HashMap::new(); + let mut advanced_to_cognitive: HashSet = HashSet::new(); + + for c in 1..=CYCLES { + let scan = scan_board(&fleet, SCOPE_LO..SCOPE_HI); + eprintln!( + "d_ign_b scan @c{c}: planning={} cognitive={} evaluation={} absorbed={} missing={}", + scan.planning.len(), + scan.cognitive.len(), + scan.evaluation.len(), + scan.absorbed.len(), + scan.missing + ); + assert_eq!( + scan.missing, 1, + "the orphan (id {ORPHAN_ID}) must be the only missing id in every scan" + ); + + let planning_outcome = + column_pass(&fleet, &scan.planning, &mut writer, plan_or_evaluate_think); + + // ── L2 can-stay-silent (top half): the unarmed owner is in + // `scan.planning` but never casts (armed==0 short-circuits + // `plan_or_evaluate_think`), so it can never reach CognitiveWork + // and therefore never gets a readout. ─────────────────────── + let scan_cognitive_for_lens = scan.cognitive.clone(); + let cognitive_outcome = run_cognitive_work_gated_over( + &fleet, + &scan_cognitive_for_lens, + &mut writer, + |owner| { + let armed = owner.meta_at(0).thinking(); + // z=5 must never reach this arm on this fleet — see the + // "z=5 BLOCKED" check after the loop, which proves the + // premise; this assert is the in-loop mirror of it. + assert_ne!( + armed, 5, + "z=5 (Fusion) must never be armed on any owner reaching CognitiveWork in this file's fleet — it is RESERVED, not selectable" + ); + if (1..=4).contains(&armed) { + let verses = if (TWIN_LO..TWIN_HI).contains(&owner.mailbox_id()) { + labelled_verses(&corpus, TWIN_LO) + } else { + labelled_verses(&corpus, owner.mailbox_id()) + }; + let readout = run_lens(armed, &verses); + readouts.insert((owner.mailbox_id(), owner.cycle()), readout); + } + let style = thinking_style_for(armed); + let ctx = plan_context_for(armed); + let qualia = owner.qualia_at(0); + let mantissa = mantissa_of(owner); + let reliability = StyleStrategy::reliability_for(style, &ctx); + Some((qualia, mantissa, reliability, row_span_payload(owner))) + }, + ); + + let evaluation_outcome = column_pass( + &fleet, + &scan.evaluation, + &mut writer, + plan_or_evaluate_think, + ); + + let total_casts = + planning_outcome.cast + cognitive_outcome.cast + evaluation_outcome.cast; + if total_casts == 0 { + eprintln!("d_ign_b @c{c}: 0 casts staged — cycle rests, no seal"); + continue; + } + + let base_version = sink.head(); + let outcome: CycleOutcome = match run_cycle( + &sink, + &mut fleet, + &mut writer, + CycleFrame::new(CycleId(u64::from(c)), base_version), + position_base, + &mut watermarks, + u64::from, + ) + .await + { + Ok(o) => o, + Err(CycleError::Seal(_)) => { + panic!("d_ign_b @c{c}: unexpected seal failure (MemWal never injects one)") + } + Err(CycleError::Apply { cause, .. }) => { + panic!("d_ign_b @c{c}: unexpected apply failure: {cause}") + } + }; + position_base = position_base.max(outcome.sealed.next_position_base); + + // ── L5 (mechanics unchanged by the swap) — DERIVED expectation + // for THIS file's cohorts (deviation 1: not PROBE-IGNITION's + // 20+4, since there is no CONTRA cohort here). ─────────────── + let flow_advances = outcome + .sealed + .transitions + .iter() + .filter(|t| { + t.mv.from == KanbanColumn::Planning && t.mv.to == KanbanColumn::CognitiveWork + }) + .count(); + let block_advances = outcome + .sealed + .transitions + .iter() + .filter(|t| t.mv.from == KanbanColumn::Planning && t.mv.to == KanbanColumn::Prune) + .count(); + eprintln!( + "d_ign_b.L5 @c{c}: {flow_advances} Flow (Planning->CognitiveWork) + {block_advances} Block (Planning->Prune) transitions sealed" + ); + for t in &outcome.sealed.transitions { + if t.mv.from == KanbanColumn::Planning && t.mv.to == KanbanColumn::CognitiveWork { + assert_eq!( + t.mv.exec, + ExecTarget::Elixir, + "L5 can-fire: Planning->CognitiveWork must be the STYLE's Elixir mint" + ); + advanced_to_cognitive.insert(t.owner); + } + } + if c == 1 { + assert_eq!( + block_advances, 0, + "L5: this file's cohorts have no block/CONTRA-qualia owners in scope" + ); + assert_eq!( + flow_advances, + (TWIN_HI - TWIN_LO) as usize + (SPREAD_HI - SPREAD_LO) as usize, + "L5 can-fire: all TWIN+SPREAD armed owners must Flow-advance at c1" + ); + } + + eprintln!( + "d_ign_b @c{c}: {total_casts} casts staged, {} readouts captured so far", + readouts.len() + ); + } + + // ── L2: arming is load-bearing. can-stay-silent checked FIRST, + // across every cycle key the main loop could have written (not + // just one), before the arming mutation below changes anything. ── + { + let silent_across_all_cycles = !readouts.keys().any(|&(rid, _)| rid == UNARMED_ID); + assert!( + silent_across_all_cycles, + "L2 can-stay-silent: the UNARMED owner must have NO readout from the main loop, at ANY cycle" + ); + + // can-fire: arm it (the G8 pattern, `probe_ignition.rs:1125-1143`), + // then run the SAME lens-capture step the gated closure performs + // above and confirm it lands a readout entry — into a SEPARATE + // scratch map, so this manual arming (which never goes through + // `run_cognitive_work_gated_over`, hence never reaches + // `advanced_to_cognitive`) cannot contaminate L6's containment + // check below. + let owner_mut = fleet + .get_mut(&UNARMED_ID) + .expect("UNARMED owner must exist"); + owner_mut.set_meta(0, MetaWord::new(3, 0, 0, 0, 0)); // arm as Kant, z=3 + let owner = fleet.get(&UNARMED_ID).expect("UNARMED owner must exist"); + let armed = owner.meta_at(0).thinking(); + assert_eq!(armed, 3, "L2 can-fire premise: the write landed"); + let verses = labelled_verses(&corpus, UNARMED_ID); + let readout = run_lens(armed, &verses); + let mut scratch: HashMap<(MailboxId, u32), LensReadout> = HashMap::new(); + scratch.insert((UNARMED_ID, owner.cycle()), readout); + assert!( + scratch.keys().any(|&(rid, _)| rid == UNARMED_ID), + "L2 can-fire: writing non-zero thinking bits and re-running the lens step must land a readout entry" + ); + eprintln!( + "d_ign_b.L2: UNARMED owner {UNARMED_ID} silent in the main loop for every cycle (armed==0 throughout); \ + once armed directly + re-run, a readout entry lands (kept in a scratch map, not the main one)" + ); + } + + // ── L3: per-lens non-emptiness, measured and printed for all + // in-scope armed owners (TWIN 0..8 + SPREAD 8..30 = 30 owners). ── + { + let mut empty_count: HashMap = HashMap::new(); + let mut total_count: HashMap = HashMap::new(); + let mut per_lens_digests: HashMap> = HashMap::new(); + for id in (TWIN_LO..TWIN_HI).chain(SPREAD_LO..SPREAD_HI) { + let owner = fleet.get(&id).expect("in-scope armed owner must exist"); + let armed = owner.meta_at(0).thinking(); + let verses = if (TWIN_LO..TWIN_HI).contains(&id) { + labelled_verses(&corpus, TWIN_LO) + } else { + labelled_verses(&corpus, id) + }; + let readout = run_lens(armed, &verses); + *total_count.entry(armed).or_insert(0) += 1; + if readout.is_empty() { + *empty_count.entry(armed).or_insert(0) += 1; + } + per_lens_digests + .entry(armed) + .or_default() + .push(readout.digest()); + } + for z in 1..=4u8 { + let empty = empty_count.get(&z).copied().unwrap_or(0); + let total = total_count.get(&z).copied().unwrap_or(0); + eprintln!("d_ign_b.L3 z={z}: empty={empty}/{total}"); + assert!( + empty < total, + "L3 can-fire: lens z={z} must be non-empty on at least one of {total} owners" + ); + + // ── L4 can-stay-silent: each non-empty lens must have >=2 + // distinct digests across the 30 in-scope owners (a + // lens constant over every owner carries no information). + let mut ds = per_lens_digests.get(&z).cloned().unwrap_or_default(); + ds.sort_unstable(); + ds.dedup(); + eprintln!( + "d_ign_b.L4 z={z}: {} distinct digests across {total} owners", + ds.len() + ); + assert!( + ds.len() >= 2, + "L4 can-stay-silent: lens z={z} must yield >=2 distinct digests across the in-scope owners" + ); + } + } + + // ── L6: readout keys are a subset of owners that advanced + // Planning->CognitiveWork in a PRIOR cycle. ───────────────────── + { + for &(id, _cycle) in readouts.keys() { + assert!( + advanced_to_cognitive.contains(&id), + "L6 can-fire: readout owner {id} must be in the set of owners that advanced to CognitiveWork" + ); + } + assert!( + !advanced_to_cognitive.contains(&UNARMED_ID), + "L6 can-stay-silent: the UNARMED owner never advanced to CognitiveWork, so it has no readout key at any cycle" + ); + eprintln!( + "d_ign_b.L6: {} readout keys all within {} owners that advanced to CognitiveWork; UNARMED owner absent from both", + readouts.len(), + advanced_to_cognitive.len() + ); + } + + // ── L7 (inherits G7): the address axis is load-bearing — an + // OUTSIDE owner, run through the lens body DIRECTLY, produces a + // readout; the OUTSIDE cohort has none from the main loop. ────── + { + for id in OUTSIDE_LO..OUTSIDE_HI { + assert!( + !readouts.keys().any(|&(rid, _)| rid == id), + "L7 can-stay-silent: OUTSIDE owner {id} must have no readout from the main loop (never scanned, SCOPE_HI={SCOPE_HI})" + ); + } + let outside_owner = fleet.get(&OUTSIDE_LO).expect("OUTSIDE owner must exist"); + assert_eq!( + outside_owner.phase(), + KanbanColumn::Planning, + "L7 can-stay-silent premise: OUTSIDE owner never advanced in the main run" + ); + let verses = labelled_verses(&corpus, OUTSIDE_LO); + let readout = run_lens(1, &verses); + eprintln!( + "d_ign_b.L7: OUTSIDE owner {OUTSIDE_LO} — silent in the main run (address excluded); \ + run through the lens body directly it produces a readout (empty={})", + readout.is_empty() + ); + } + + // ── z=5 BLOCKED (design §3, the deliverable per brief). ────────── + { + for owner in fleet.values() { + assert_ne!( + owner.meta_at(0).thinking(), + 5, + "z=5 (Fusion) must be unarmed/unused on every owner in this file's fleet" + ); + } + let z5 = MetaWord::new(5, 0, 0, 0, 0); + assert_eq!( + z5.thinking(), + 5, + "sanity: 5 is representable in the 6-bit thinking field" + ); + eprintln!( + "d_ign_b.z5-BLOCKED: ordinal 5 (Fusion) is RESERVED, never armed on any owner in \ + this file. Reason (design §3): blw_fusion's admission gap needs the ranking pool \ + to GROW across many sealed horizons (S_CYCLES=8); this fleet is seeded once and \ + accrues at most {CYCLES} sealed horizons, so Strict-vs-Aware admission would be \ + identical and any delta would be 0 BY CONSTRUCTION — a vacuous readout. `jc` is \ + also not a dependency of lance-graph-supervisor. R2 (a separate reduced test, no \ + kappa, no jc dep) is the recommended path if the orchestrator wants z=5 exercised; \ + not implemented here." + ); + } + + // ── §6 Not-claimed block ───────────────────────────────────────── + println!(); + println!("== D-IGN-B — what this test does NOT claim =="); + println!("1. No row-byte decoding. The lens reads the owner's own verses selected by its address and span; bloom planes are one-way."); + println!( + "2. No fusion verdict, no kappa. z=5 is reserved (see the z5-BLOCKED check above)." + ); + println!("3. No stance validity. A readout is a read, never evidence that a stance is right about anything."); + println!("4. No parallelism, no durability, no scale, no multi-writer, no recovery (PROBE-IGNITION's not-claimed items carry over)."); + println!("5. No 36-style claim. Six probe-local ordinals (0..5); the persona-36 bridge stays an open non-goal."); + println!("6. No rung-3 / runbook claim. The four stances are not the 34 NARS tactics and are not StyleFamily macros."); + println!("7. No deinterlace/temporal claim in this loop."); + println!("8. No claim that the four lenses are independent instruments — Nietzsche is computed from Hegel's output (stance.rs:483-496)."); + println!("9. No zero-copy claim. SweepSlot::payload is Vec by the shipped signature."); + println!("10. No semantic claim about the corpus. Qualia remain declared fixtures; nothing is encoded from text into qualia."); + println!("11. No per-stance DISPATCH claim. stance_panel computes all four in one call; this file only SELECTS which tuple element to record (design §0 F0)."); + } +} diff --git a/crates/lance-graph-supervisor/tests/probe_ignition.rs b/crates/lance-graph-supervisor/tests/probe_ignition.rs new file mode 100644 index 000000000..5f06b98c3 --- /dev/null +++ b/crates/lance-graph-supervisor/tests/probe_ignition.rs @@ -0,0 +1,1389 @@ +//! PROBE-IGNITION — the falsifier for "inject corpus X into thinking style Z". +//! +//! Spec: `.claude/board/exec-runs/probe-ignition-design-opus.md` (design, +//! Opus, design-only, no code) + `.claude/board/exec-runs/probe-ignition-api-inventory-sonnet.md` +//! (exact signatures — wins on any design/inventory conflict). This is the +//! first DRIVEN traversal of the built-but-undriven write path: a fleet of +//! real `MailboxSoA` tenants, seeded from a real corpus and armed with a +//! thinking style by a WRITE, discovered by a SCAN of the kanban board alone +//! (no messaging, no queue, no ractor) — the armed style's own +//! `StrategyOutcome` cast write-on-behalf, drained, sealed into one WAL +//! write, and applied. See the design note §0 for the full headline. +//! +//! ## Two verbs only (design §1b) +//! +//! CAST (a write through `BatchWriter`, write-on-behalf) and LOOK INTO THE +//! KANBAN (a read of the owner's phase). No endpoint, no actor, no queue. +//! `where()` scopes the SCAN only; an armed owner outside the scanned range +//! is armed and never started (G7). +//! +//! ## Deviation from the design note, stated once here (no others) +//! +//! **§2 step "Energize" uses a direct `owner.energy[row]` write, NOT +//! `apply_edges(&[(row, CausalEdge64)])`.** `causal_edge::CausalEdge64` is +//! not reachable from this crate: `cognitive-shader-driver` depends on it +//! privately (no `pub use`), `lance-graph-planner` does not re-export it +//! either, and `lance-graph-supervisor`'s own `Cargo.toml` has no dependency +//! edge to `causal-edge` (verified by reading both manifests before writing +//! this file). Adding one would be a Cargo.toml change, which this build's +//! brief forbids outright. `owner.energy` is the SAME public field +//! `apply_edges` itself mutates (`mailbox_soa.rs:66,362`) and is the field +//! `examples/blw_fusion.rs:515` already writes directly for its own +//! energizing (`owner.energy[row] = …`) — so this substitution has a shipped +//! precedent in the same crate family, not an invented mechanism. Every +//! other design element (the gate, the DAG, the seal/apply, the write-back, +//! the wake) is implemented exactly as specified. +//! +//! ## A second, load-bearing correction: the WAKE runs BEFORE the scan +//! +//! The design's own step numbering lists "scheduled wake (cycle 4 only)" +//! as step 14, after the write-back (step 13) — i.e. at the END of a +//! cycle's processing. Read literally that would make the wake's effect +//! visible only from cycle 5's gate read onward. But the design's own +//! cohort table says the REST cohort's own arc is "wake at c4 → +//! Evaluation" — i.e. cycle 4's OWN gate read must already see the +//! post-wake mantissa. The two claims are consistent only if the wake +//! write lands BEFORE cycle 4's scan/gate read, not after. This file runs +//! the wake at the TOP of the loop body when `c == WAKE_CYCLE`, before +//! `scan_board` — a placement correction, not a behavioural addition (the +//! wake is still exactly one write, still gated on `c == WAKE_CYCLE`, +//! still touches only the REST cohort). +//! +//! ## Provenance +//! +//! Feature-gating pattern: `tests/w2b_real_owner_probe.rs`. `MemWal` / +//! `RowSpanDescriptor` / bloom-plane seeding +//! (`fnv1a`/`bloom_add`/`tokens`/`encode_plane`) / `load_verses`: +//! `crates/lance-graph-planner/examples/blw_fusion.rs` (cited at each site +//! below). `flow_qualia()` / `block_qualia()`: re-derived locally from +//! `cycle_driver.rs:1669` / `:1675` (those functions are `#[cfg(test)]` +//! inside `cycle_driver`'s own module, not importable from here). +//! +//! ## Not compiled, not run by this lane — orchestrator gates +//! +//! This file was written edit-only (no `cargo` of any kind). Every +//! signature cited was read from source in the same pass that wrote this +//! file (see the build tag-file for what could and could not be verified). + +#[cfg(feature = "cycle-driver")] +mod probe_ignition { + #![allow( + clippy::cast_possible_truncation, + clippy::cast_possible_wrap, + clippy::cast_sign_loss + )] + + use std::collections::{HashMap, HashSet}; + use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; + use std::sync::Mutex; + + use cognitive_shader_driver::mailbox_soa::{MailboxSoA, WriteCell, WriteOutcome, WORDS_PER_FP}; + use lance_graph_contract::cognitive_shader::MetaWord; + use lance_graph_contract::collapse_gate::MailboxId; + use lance_graph_contract::kanban::{ExecTarget, KanbanColumn}; + use lance_graph_contract::mul::i4_eval::{gate_decision_i4, trust_texture_i4}; + use lance_graph_contract::mul::TrustTexture; + use lance_graph_contract::qualia::QualiaI4_16D; + use lance_graph_contract::scheduler::DatasetVersion; + use lance_graph_contract::soa_view::MailboxSoaView; + use lance_graph_contract::thinking::ThinkingStyle; + use lance_graph_planner::batch_writer::BatchWriter; + use lance_graph_planner::ir::Arena; + use lance_graph_planner::owner_adapter::emit_bootstrap_intent; + use lance_graph_planner::persist_sink::{ + CycleFrame, CycleId, DetachedCycleBatch, LandedSlot, SweepSlot, WalSink, WriteFailed, + }; + use lance_graph_planner::strategy::style_strategy::StyleStrategy; + use lance_graph_planner::traits::{ + PlanContext, PlanInput, PlanStrategy, QueryFeatures, StrategyOutcome, + }; + use lance_graph_supervisor::cycle_driver::{ + apply_sealed_transitions, collect_casts, run_cognitive_work_gated_over, run_cycle, + seal_cycle, shade_owner, CycleError, CycleOutcome, + }; + + // ── PRE-REGISTERED run shape (design §2) — fixed BEFORE any number + // exists; NOT adjustable after a run. ─────────────────────────────────── + + const FLEET_OWNERS: MailboxId = 64; + const ROWS_PER_OWNER: usize = 64; + const POPULATED_ROWS: usize = 48; + const CORPUS_VERSES: usize = FLEET_OWNERS as usize * POPULATED_ROWS; // 3072 + const SCOPE_LO: MailboxId = 0; + const SCOPE_HI: MailboxId = 32; + const CYCLES: u32 = 6; + const WAKE_CYCLE: u32 = 4; + + // Cohorts (design §2 table). ids sum to exactly 32 inside SCOPE. + const IGNITE_A_LO: MailboxId = 0; + const IGNITE_A_HI: MailboxId = 6; + const IGNITE_C_LO: MailboxId = 6; + const IGNITE_C_HI: MailboxId = 12; + const REST_LO: MailboxId = 12; + const REST_HI: MailboxId = 20; + const CONTRA_LO: MailboxId = 20; + const CONTRA_HI: MailboxId = 24; + const UNARMED_LO: MailboxId = 24; + const UNARMED_HI: MailboxId = 31; + const ORPHAN_ID: MailboxId = 31; + const OUTSIDE_LO: MailboxId = 32; + const OUTSIDE_HI: MailboxId = 64; + + /// Firing threshold. A row is "firing" once `|energy[row]| >= threshold` + /// (`mailbox_soa.rs:194-196`). + const TENANT_THRESHOLD: f32 = 1.0; + /// Energy landed on a firing row — safely above `TENANT_THRESHOLD`. + const FIRE_ENERGY: f32 = 2.0; + /// Any w_slot < 64 works — `apply_edges`'s w_slot gate is never exercised + /// by this probe (see the module-level deviation note). + const TENANT_W_SLOT: u8 = 0; + + type Tenant = MailboxSoA; + type Fleet = HashMap; + + // ── ThinkingStyle arming vocabulary (design §6 Q1, folded in per the + // orchestrator ruling): z in {0,1,2,3}. z=0 is UNARMED and never reaches + // `thinking_style_for`/`style_vector_for` — both fall back defensively + // (never actually exercised on this probe's cohorts) rather than panic. + // Three of the contract's 36 styles are reachable here, never 36 — see + // the §5 not-claimed block. ─────────────────────────────────────────── + + fn thinking_style_for(z: u8) -> ThinkingStyle { + match z { + 1 => ThinkingStyle::Analytical, + 2 => ThinkingStyle::Creative, + 3 => ThinkingStyle::Reflective, + _ => ThinkingStyle::Analytical, + } + } + + /// The 23D sparse vector `StyleStrategy`'s private `resolve_style` reads + /// (idx 4 = analytical, idx 3 = creative, idx 0 = depth/reflective — + /// `style_strategy.rs:236-238`), built so `.plan()`'s internal style + /// resolution and this file's own `thinking_style_for` agree by + /// construction (both driven from the same `z`). + fn style_vector_for(z: u8) -> Vec { + let mut v = vec![0.0f64; 23]; + match thinking_style_for(z) { + ThinkingStyle::Analytical => v[4] = 1.0, + ThinkingStyle::Creative => v[3] = 1.0, + _ => v[0] = 1.0, + } + v + } + + fn plan_context_for(z: u8) -> PlanContext { + PlanContext { + query: String::new(), + features: QueryFeatures::default(), + free_will_modifier: 1.0, + thinking_style: Some(style_vector_for(z)), + nars_hint: None, + witness: None, + } + } + + /// `mantissa` is DERIVED from live owner state, never stored (design §4): + /// `min(7, pending_count()) as i8`. `pending_count` (`mailbox_soa.rs:571`) + /// counts rows with `|energy| >= threshold` over the whole `N`-row plane; + /// only populated rows are ever written above zero in this probe, so it + /// is equivalent to "populated rows at/above threshold" without needing + /// a separate `populated` clamp. + fn mantissa_of(owner: &Tenant) -> i8 { + owner.pending_count().min(7) as i8 + } + + /// Flow qualia (warmth=4, groundedness=3, coherence=4, valence=2) — the + /// SAME construction `cycle_driver.rs:1669`'s `flow_qualia()` test + /// fixture uses (`flow_proxy = 4+3-0 = 7`, coherence>=4 & valence>=2 & + /// tension<=1 => Calibrated). Re-derived here (that fn is `#[cfg(test)]` + /// inside `cycle_driver`, not importable). + fn flow_qualia() -> QualiaI4_16D { + QualiaI4_16D(0).with(3, 4).with(14, 3).with(9, 4).with(1, 2) + } + + /// Uncertain qualia (coherence=-3, tension=3) => `Block`. Provenance: + /// `cycle_driver.rs:1675`'s `block_qualia()` fixture, re-derived (same + /// reason as above). + fn block_qualia() -> QualiaI4_16D { + QualiaI4_16D(0).with(9, -3).with(2, 3) + } + + // ── corpus + bloom-plane seeding — COPIED from + // `crates/lance-graph-planner/examples/blw_fusion.rs` with provenance, + // per the brief ("consume the pattern; copy the needed pieces"). ─────── + + /// Bits set per token in a `WORDS_PER_FP`-word identity plane. + /// Provenance: `blw_fusion.rs:255` (itself citing `blw_tenant.rs:249`). + const BLOOM_K: usize = 4; + + /// FNV-1a over `bytes`, salted with `seed`. Provenance: `blw_fusion.rs:258-265`. + fn fnv1a(bytes: &[u8], seed: u64) -> u64 { + let mut h = 0xcbf2_9ce4_8422_2325_u64 ^ seed.wrapping_mul(0x100_0000_01b3); + for &c in bytes { + h ^= u64::from(c); + h = h.wrapping_mul(0x100_0000_01b3); + } + h + } + + /// Set this token's `BLOOM_K` bits in a `WORDS_PER_FP`-word plane. + /// Provenance: `blw_fusion.rs:269-278`. + fn bloom_add(plane: &mut [u64], token: &str, salt: u64) { + for k in 0..BLOOM_K { + let h = fnv1a( + token.as_bytes(), + salt ^ (k as u64).wrapping_mul(0x9E37_79B9), + ); + let bit = (h % (WORDS_PER_FP as u64 * 64)) as usize; + plane[bit / 64] |= 1u64 << (bit % 64); + } + } + + /// Lowercased alphanumeric tokens of length >= 2. Provenance: `blw_fusion.rs:281-285`. + fn tokens(text: &str) -> impl Iterator + '_ { + text.split(|c: char| !c.is_ascii_alphanumeric()) + .filter(|t| t.len() >= 2) + .map(str::to_ascii_lowercase) + } + + /// Build a plane from a verse's tokens. Provenance: `blw_fusion.rs:290-296`. + fn encode_plane(text: &str, salt: u64) -> Vec { + let mut plane = vec![0u64; WORDS_PER_FP]; + for t in tokens(text) { + bloom_add(&mut plane, &t, salt); + } + plane + } + + /// Read `index\ttext` rows, bounded to `limit`. Provenance: `blw_fusion.rs:478-485`. + fn load_verses(path: &str, limit: usize) -> Option> { + let raw = std::fs::read_to_string(path).ok()?; + let verses: Vec = raw + .lines() + .filter_map(|l| l.split_once('\t').map(|(_, t)| t.to_string())) + .take(limit) + .collect(); + (verses.len() == limit).then_some(verses) + } + + /// Deterministic synthetic fallback — a non-degeneracy fixture, not a + /// semantic instrument (design §2 step 1): distinct per-index text so + /// every content plane differs. + fn synthetic_corpus(n: usize) -> Vec { + (0..n) + .map(|i| { + let salt = (i as u64).wrapping_mul(2_654_435_761) % 104_729; + format!("probe ignition synthetic verse {i} token{salt}") + }) + .collect() + } + + fn load_or_synthesize_corpus() -> (Vec, &'static str) { + let path = + std::env::var("BLW_KJV_TSV").unwrap_or_else(|_| "/tmp/kjv_verses.tsv".to_string()); + match load_verses(&path, CORPUS_VERSES) { + Some(v) => (v, "BLW_KJV_TSV corpus"), + None => ( + synthetic_corpus(CORPUS_VERSES), + "deterministic synthetic fallback", + ), + } + } + + /// The write descriptor `P` — a DESCRIPTOR, never owned delta bytes. + /// Provenance: `blw_fusion.rs:362-380`. + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + struct RowSpanDescriptor { + row_lo: u32, + row_hi: u32, + cycle: u32, + } + + impl RowSpanDescriptor { + fn to_le_bytes(self) -> [u8; 12] { + let mut out = [0u8; 12]; + out[0..4].copy_from_slice(&self.row_lo.to_le_bytes()); + out[4..8].copy_from_slice(&self.row_hi.to_le_bytes()); + out[8..12].copy_from_slice(&self.cycle.to_le_bytes()); + out + } + } + + fn row_span_payload(owner: &Tenant) -> Vec { + RowSpanDescriptor { + row_lo: 0, + row_hi: owner.populated() as u32, + cycle: owner.cycle(), + } + .to_le_bytes() + .to_vec() + } + + // ── the WAL seam (in-process; NOT durability) — COPIED from + // `blw_fusion.rs:396-473`, with the `reads` counter added (mirroring + // `cycle_driver.rs`'s own `#[cfg(test)]` `FakeWalSink`, cited at G3b). ─ + + struct SealedCycle { + version: DatasetVersion, + landings: Vec, + } + + struct MemWal { + sealed: Mutex>, + next_version: AtomicU64, + wal_writes: AtomicU64, + /// `scan_sealed` + `versions` call count — MUST stay 0 across the + /// main loop (P4b reads no dataset; G3b). + reads: AtomicU64, + } + + impl MemWal { + fn new() -> Self { + Self { + sealed: Mutex::new(Vec::new()), + next_version: AtomicU64::new(1), + wal_writes: AtomicU64::new(0), + reads: AtomicU64::new(0), + } + } + fn wal_writes(&self) -> u64 { + self.wal_writes.load(Ordering::SeqCst) + } + fn reads(&self) -> u64 { + self.reads.load(Ordering::SeqCst) + } + fn head(&self) -> DatasetVersion { + self.sealed + .lock() + .expect("MemWal poisoned") + .last() + .map_or(DatasetVersion(0), |s| s.version) + } + } + + impl WalSink for MemWal { + async fn commit_cycle( + &self, + base: DatasetVersion, + batch: DetachedCycleBatch, + ) -> Result { + let mut sealed = self.sealed.lock().expect("MemWal poisoned"); + let head = sealed.last().map_or(DatasetVersion(0), |s| s.version); + if base != head { + return Err(WriteFailed(format!( + "stale base {base:?}: sealed head is {head:?}" + ))); + } + self.wal_writes.fetch_add(1, Ordering::SeqCst); + let version = DatasetVersion(self.next_version.fetch_add(1, Ordering::SeqCst)); + sealed.push(SealedCycle { + version, + landings: batch.landings, + }); + Ok(version) + } + + async fn scan_sealed( + &self, + from_version: Option, + ) -> Result, WriteFailed> { + self.reads.fetch_add(1, Ordering::SeqCst); + Ok(self + .sealed + .lock() + .expect("MemWal poisoned") + .iter() + .filter(|s| from_version.is_none_or(|f| s.version > f)) + .flat_map(|s| { + s.landings.iter().map(|slot| LandedSlot { + version: s.version, + slot: slot.clone(), + }) + }) + .collect()) + } + + async fn versions(&self) -> Result, WriteFailed> { + self.reads.fetch_add(1, Ordering::SeqCst); + Ok(self + .sealed + .lock() + .expect("MemWal poisoned") + .iter() + .map(|s| { + ( + s.landings.first().map_or(CycleId(0), |l| l.cycle), + s.version, + ) + }) + .collect()) + } + } + + // ── fleet construction ────────────────────────────────────────────────── + + fn owner_verses(all: &[String], owner_idx: MailboxId) -> &[String] { + let lo = owner_idx as usize * POPULATED_ROWS; + &all[lo..lo + POPULATED_ROWS] + } + + /// Build + seed one owner: `table($x)` (content per row) + `ThinkingStyle($z)` + /// (a `MetaWord` write) + qualia (declared fixture) + energize + /// (`firing_rows` rows set above threshold) — design §2 steps 2-6. + fn build_owner( + id: MailboxId, + verses: &[String], + armed: u8, + qualia: QualiaI4_16D, + firing_rows: usize, + ) -> Tenant { + let mut owner: Tenant = MailboxSoA::new(id, TENANT_W_SLOT, TENANT_THRESHOLD); + let cycle = owner.cycle(); // 0, mirrors blw_fusion.rs:723-728 (seed before tick). + let meta = MetaWord::new(armed, 0, 0, 0, 0); + for (row, text) in verses.iter().enumerate() { + let content = encode_plane(text, u64::from(id)); + let cell = WriteCell { + content: Some(content.as_slice()), + qualia: Some(qualia), + meta: Some(meta), + entity_type: Some((row % 251) as u16), + temporal: Some(row as u64), + ..WriteCell::default() + }; + let outcome = owner.write_row(row, cycle, &cell); + assert_eq!( + outcome, + WriteOutcome::Accepted, + "seeding row {row} of owner {id} must be accepted" + ); + } + owner.set_populated(verses.len()); + owner.tick(); // cycle 0 -> 1, mirrors blw_fusion.rs:728. + for r in 0..firing_rows { + owner.energy[r] = FIRE_ENERGY; + } + owner + } + + fn build_fleet(corpus: &[String]) -> Fleet { + let mut fleet = Fleet::new(); + let analytical: u8 = 1; + let creative: u8 = 2; + let unarmed: u8 = 0; + + for id in IGNITE_A_LO..IGNITE_A_HI { + fleet.insert( + id, + build_owner(id, owner_verses(corpus, id), analytical, flow_qualia(), 3), + ); + } + for id in IGNITE_C_LO..IGNITE_C_HI { + fleet.insert( + id, + build_owner(id, owner_verses(corpus, id), creative, flow_qualia(), 3), + ); + } + for id in REST_LO..REST_HI { + fleet.insert( + id, + build_owner(id, owner_verses(corpus, id), analytical, flow_qualia(), 1), + ); + } + for id in CONTRA_LO..CONTRA_HI { + fleet.insert( + id, + build_owner(id, owner_verses(corpus, id), analytical, block_qualia(), 3), + ); + } + for id in UNARMED_LO..UNARMED_HI { + fleet.insert( + id, + build_owner(id, owner_verses(corpus, id), unarmed, flow_qualia(), 3), + ); + } + // ORPHAN (id 31): deliberately NOT inserted — "no owner registered" + // (design §2 cohort table; the #879 missing-owner caveat, G10). + for id in OUTSIDE_LO..OUTSIDE_HI { + fleet.insert( + id, + build_owner(id, owner_verses(corpus, id), analytical, flow_qualia(), 3), + ); + } + fleet + } + + // ── the scan (LOOK INTO THE KANBAN) ───────────────────────────────────── + + #[derive(Default)] + struct ScanResult { + planning: Vec, + cognitive: Vec, + evaluation: Vec, + absorbed: Vec, + missing: usize, + } + + /// Pure reads through the owner's `phase()`. Design §2 step 7. + fn scan_board(fleet: &Fleet, ids: impl IntoIterator) -> ScanResult { + let mut r = ScanResult::default(); + for id in ids { + match fleet.get(&id) { + None => r.missing += 1, + Some(owner) => match owner.phase() { + KanbanColumn::Planning => r.planning.push(id), + KanbanColumn::CognitiveWork => r.cognitive.push(id), + KanbanColumn::Evaluation => r.evaluation.push(id), + KanbanColumn::Commit | KanbanColumn::Plan | KanbanColumn::Prune => { + r.absorbed.push(id); + } + }, + } + } + r + } + + // ── the CAST — the Planning and Evaluation columns (probe-local; + // design §6 Q2: the shipped seam can drive only CognitiveWork) ───────── + + struct ColumnPassOutcome { + cast: usize, + held: Vec, + missing: usize, + } + + /// Probe-local pass over an explicit id list. Trusts `scan_board`'s + /// partition (unlike the shipped `cognitive_pass`'s defensive re-check — + /// this closure's caller already filtered by phase); counts a missing + /// owner explicitly (G10), where the shipped seam does not. + fn column_pass( + fleet: &Fleet, + ids: &[MailboxId], + writer: &mut BatchWriter>, + mut think: impl FnMut(&Tenant) -> Option<(StrategyOutcome, Vec)>, + ) -> ColumnPassOutcome { + let mut cast = 0usize; + let mut held = Vec::new(); + let mut missing = 0usize; + for &id in ids { + let Some(owner) = fleet.get(&id) else { + missing += 1; + continue; + }; + let mut did_cast = false; + if let Some((outcome, payload)) = think(owner) { + if emit_bootstrap_intent( + &outcome, + owner.mailbox_id(), + owner.current_cycle(), + writer, + payload, + ) + .is_some() + { + did_cast = true; + } + } + if did_cast { + cast += 1; + } else { + held.push(id); + } + } + ColumnPassOutcome { + cast, + held, + missing, + } + } + + /// Design §2 step 8/10: gate first, style mints when the gate's target + /// matches the style's structural crossing, the gate mints otherwise + /// (always true at Evaluation, since the style's intended move is + /// structurally always the Planning -> CognitiveWork crossing). + fn plan_or_evaluate_think(owner: &Tenant) -> Option<(StrategyOutcome, Vec)> { + let armed = owner.meta_at(0).thinking(); + if armed == 0 { + return None; // UNARMED: never plans. + } + let qualia = owner.qualia_at(0); + let mantissa = mantissa_of(owner); + let gate = gate_decision_i4(&qualia, mantissa); + let target = owner.phase().advance_on_gate(&gate)?; // Hold => None => rest. + + let mut arena = Arena::new(); + let plan_out = StyleStrategy + .plan( + PlanInput { + plan: None, + context: plan_context_for(armed), + outcome: None, + }, + &mut arena, + ) + .expect("StyleStrategy::plan never errors over this probe's fixed recipe substrate"); + let style_outcome = plan_out + .outcome + .expect("StyleStrategy always surfaces a StrategyOutcome"); + let style_move = style_outcome + .intended_move + .expect("StyleStrategy always intends the Planning -> CognitiveWork crossing"); + + let outcome = if style_move.to == target { + style_outcome + } else { + shade_owner(owner, &qualia, mantissa, style_outcome.reliability)? + }; + Some((outcome, row_span_payload(owner))) + } + + // ── fingerprinting (G1/G3a/G6 byte-identity) ──────────────────────────── + + #[derive(Clone, Copy, PartialEq, Debug)] + struct OwnerFingerprint { + phase: KanbanColumn, + cycle: u32, + energy: [f32; ROWS_PER_OWNER], + meta0: MetaWord, + qualia0: QualiaI4_16D, + } + + fn fingerprint(o: &Tenant) -> OwnerFingerprint { + OwnerFingerprint { + phase: o.phase(), + cycle: o.cycle(), + energy: o.energy, + meta0: o.meta_at(0), + qualia0: o.qualia_at(0), + } + } + + fn fingerprint_all( + fleet: &Fleet, + ids: impl IntoIterator, + ) -> HashMap { + ids.into_iter() + .filter_map(|id| fleet.get(&id).map(|o| (id, fingerprint(o)))) + .collect() + } + + fn phase_cycle_snapshot(fleet: &Fleet) -> HashMap { + fleet + .iter() + .map(|(&id, o)| (id, (o.phase(), o.cycle()))) + .collect() + } + + // ── the main probe ─────────────────────────────────────────────────────── + + #[tokio::test] + async fn probe_ignition_scan_and_cast_no_messaging() { + let (corpus, provenance) = load_or_synthesize_corpus(); + println!( + "probe.ignition corpus: {provenance} ({} verses, {FLEET_OWNERS} owners x {POPULATED_ROWS} rows)", + corpus.len() + ); + assert_eq!(corpus.len(), CORPUS_VERSES, "PRE-REGISTERED corpus size"); + + let mut fleet = build_fleet(&corpus); + + // ── non-degeneracy guard (design §2 step 1) ───────────────────────── + { + let sample_ids: [MailboxId; 5] = + [IGNITE_A_LO, IGNITE_C_LO, REST_LO, CONTRA_LO, OUTSIDE_LO]; + let mut digests = Vec::new(); + for &id in &sample_ids { + let owner = fleet.get(&id).expect("sample id must be a real owner"); + let plane = owner.content_row(0); + assert!( + plane.iter().any(|&w| w != 0), + "content plane must be non-zero for owner {id}" + ); + digests.push(plane.to_vec()); + } + for (i, di) in digests.iter().enumerate() { + for dj in &digests[i + 1..] { + assert_ne!(di, dj, "content planes must be pairwise distinct"); + } + } + println!( + "probe.ignition non-degeneracy: {} sampled content planes are non-zero and pairwise distinct", + digests.len() + ); + } + + // ── G2c: the armed bits reached the plan and changed something ───── + { + let r_a = + StyleStrategy::reliability_for(ThinkingStyle::Analytical, &plan_context_for(1)); + let r_c = StyleStrategy::reliability_for(ThinkingStyle::Creative, &plan_context_for(2)); + assert_ne!( + r_a.to_bits(), + r_c.to_bits(), + "G2c can-fire: distinct styles must yield distinct reliability (the R-GATE property, style_strategy.rs:486-508)" + ); + let r_a2 = + StyleStrategy::reliability_for(ThinkingStyle::Analytical, &plan_context_for(1)); + assert_eq!( + r_a.to_bits(), + r_a2.to_bits(), + "G2c can-stay-silent: two owners armed with the SAME style must produce bit-identical reliability" + ); + eprintln!("probe.ignition.G2c: reliability(Analytical)={r_a} != reliability(Creative)={r_c}; same-style reliability is bit-identical"); + } + + let sink = MemWal::new(); + let mut writer: BatchWriter> = BatchWriter::new(); + let mut position_base: u64 = 0; + let mut watermarks: HashMap> = HashMap::new(); + // Cursor into each owner's firing rows — the write-back pass + // (design §2 step 13) consumes them one at a time. + let mut next_firing_row: HashMap = HashMap::new(); + + let unarmed_and_outside: Vec = (UNARMED_LO..UNARMED_HI) + .chain(OUTSIDE_LO..OUTSIDE_HI) + .collect(); + let rest_ids: Vec = (REST_LO..REST_HI).collect(); + let contra_ids: Vec = (CONTRA_LO..CONTRA_HI).collect(); + let mut rediscovered_rest_c2 = false; + let mut rediscovered_rest_c3 = false; + let mut contra_seen_after_c1: HashSet = HashSet::new(); + let mut end_of_c5: Option> = None; + + for c in 1..=CYCLES { + // ── scheduled wake (design §2 step 14, RE-ORDERED to run before + // the scan — see the module-level correction note) ───────────── + if c == WAKE_CYCLE { + for &id in &rest_ids { + let owner = fleet.get_mut(&id).expect("REST owner must exist"); + let row = *next_firing_row.get(&id).unwrap_or(&0); + owner.energy[row] = FIRE_ENERGY; + } + println!( + "probe.ignition wake @c{c}: re-energized one row for each of {} REST owners", + rest_ids.len() + ); + } + + let scan = scan_board(&fleet, SCOPE_LO..SCOPE_HI); + assert_eq!( + scan.missing, 1, + "scan.missing @c{c}: the orphan (id {ORPHAN_ID}) must be the only missing id in every scan" + ); + eprintln!( + "probe.ignition scan @c{c}: planning={} cognitive={} evaluation={} absorbed={} missing={}", + scan.planning.len(), + scan.cognitive.len(), + scan.evaluation.len(), + scan.absorbed.len(), + scan.missing + ); + + if c == 2 { + rediscovered_rest_c2 = rest_ids.iter().all(|id| scan.cognitive.contains(id)); + } + if c == 3 { + rediscovered_rest_c3 = rest_ids.iter().all(|id| scan.cognitive.contains(id)); + } + if c >= 2 { + for &id in &contra_ids { + if scan.planning.contains(&id) + || scan.cognitive.contains(&id) + || scan.evaluation.contains(&id) + { + contra_seen_after_c1.insert(id); + } + } + } + + let wal_writes_at_top_of_cycle = sink.wal_writes(); + + // ── G3a: casting mutates nothing — snapshot BEFORE the passes ── + let snap_before_cast = phase_cycle_snapshot(&fleet); + + let planning_outcome = + column_pass(&fleet, &scan.planning, &mut writer, plan_or_evaluate_think); + + let cognitive_outcome = + run_cognitive_work_gated_over(&fleet, &scan.cognitive, &mut writer, |owner| { + let armed = owner.meta_at(0).thinking(); + let style = thinking_style_for(armed); + let ctx = plan_context_for(armed); + let qualia = owner.qualia_at(0); + let mantissa = mantissa_of(owner); + let reliability = StyleStrategy::reliability_for(style, &ctx); + Some((qualia, mantissa, reliability, row_span_payload(owner))) + }); + + let evaluation_outcome = column_pass( + &fleet, + &scan.evaluation, + &mut writer, + plan_or_evaluate_think, + ); + + let snap_after_cast = phase_cycle_snapshot(&fleet); + assert_eq!( + snap_before_cast, snap_after_cast, + "G3a @c{c}: staging casts must not mutate any owner's phase or cycle" + ); + + let total_casts = + planning_outcome.cast + cognitive_outcome.cast + evaluation_outcome.cast; + + if total_casts == 0 { + // ── REST BRANCH (design §2 step 11): zero staged casts => + // record the rest, do NOT call run_cycle, no seal, wal_writes + // unchanged from a value captured BEFORE this cycle's passes + // ran (not compared to itself — that would be vacuous). ──── + assert_eq!( + sink.wal_writes(), + wal_writes_at_top_of_cycle, + "G6 can-stay-silent @c{c}: a rest cycle must not move wal_writes" + ); + eprintln!( + "probe.ignition.G6 @c{c}: 0 casts staged — cycle rests, no seal, wal_writes={}", + sink.wal_writes() + ); + if c == 5 { + end_of_c5 = Some(fingerprint_all( + &fleet, + fleet.keys().copied().collect::>(), + )); + } + if c == CYCLES { + let end_of_c6 = + fingerprint_all(&fleet, fleet.keys().copied().collect::>()); + match &end_of_c5 { + Some(before) => { + assert_eq!(before, &end_of_c6, "G6 can-stay-silent: the whole fleet is byte-identical across a rest cycle"); + eprintln!("probe.ignition.G6 can-stay-silent: fleet byte-identical between end-of-c5 and end-of-c6 ({} owners)", end_of_c6.len()); + } + None => panic!( + "G6: end_of_c5 was never captured — cycle 5 was not observed as a rest cycle, so the \ + pinned c5-vs-c6 comparison this gate depends on cannot run honestly" + ), + } + } + continue; + } + + let wal_writes_before = sink.wal_writes(); + let base_version = sink.head(); + let outcome: CycleOutcome = match run_cycle( + &sink, + &mut fleet, + &mut writer, + CycleFrame::new(CycleId(u64::from(c)), base_version), + position_base, + &mut watermarks, + u64::from, + ) + .await + { + Ok(o) => o, + Err(CycleError::Seal(_)) => panic!("probe.ignition @c{c}: unexpected seal failure (MemWal never injects one in the main run)"), + Err(CycleError::Apply { cause, .. }) => panic!("probe.ignition @c{c}: unexpected apply failure: {cause}"), + }; + position_base = position_base.max(outcome.sealed.next_position_base); + + // ── G3b (dynamic half): the ids whose phase actually changed + // are exactly the sealed transitions' owners; the sink was + // never read to apply them. ───────────────────────────────── + let snap_post_apply = phase_cycle_snapshot(&fleet); + let mut changed: HashSet = HashSet::new(); + for (&id, &prev) in &snap_after_cast { + if snap_post_apply.get(&id) != Some(&prev) { + changed.insert(id); + } + } + let sealed_owners: HashSet = + outcome.sealed.transitions.iter().map(|t| t.owner).collect(); + assert_eq!( + changed, sealed_owners, + "G3b can-fire @c{c}: changed set must equal the sealed transitions' owners" + ); + assert_eq!( + sink.reads(), + 0, + "G3b can-fire @c{c}: applying a sealed cycle must never read the sink" + ); + + // ── G2b (MID-FLIGHT CORRECTION — the design note's §2 step 8 is + // the authority, not its own G2b row: when the gate says Prune + // at Planning, shade_owner REPLACES the style's move, so a + // Planning-origin cast can be either the style's (Elixir, + // Flowing) or the gate's (Native, Pruned) mint). ─────────────── + for t in &outcome.sealed.transitions { + match (t.mv.from, t.mv.to) { + (KanbanColumn::Planning, KanbanColumn::CognitiveWork) => assert_eq!( + t.mv.exec, + ExecTarget::Elixir, + "G2b can-fire @c{c}: Planning->CognitiveWork must be the STYLE's Elixir mint" + ), + (KanbanColumn::Planning, KanbanColumn::Prune) => assert_eq!( + t.mv.exec, + ExecTarget::Native, + "G2b can-fire @c{c}: Planning->Prune must be the GATE's Native mint (shade_owner replaced the style's move)" + ), + (KanbanColumn::CognitiveWork, _) | (KanbanColumn::Evaluation, _) => assert_eq!( + t.mv.exec, + ExecTarget::Native, + "G2b can-stay-silent @c{c}: CognitiveWork/Evaluation-origin casts are always the gate's Native mint" + ), + _ => {} + } + } + + eprintln!( + "probe.ignition.G1/G6 @c{c}: {} casts staged, wal_writes {}->{}, {} transitions applied", + total_casts, + wal_writes_before, + sink.wal_writes(), + outcome.applied.applied.len() + ); + + if c == 1 { + assert_eq!( + outcome.sealed.transitions.len(), + 24, + "G1 can-fire: c1 must advance exactly 24 owners" + ); + assert_eq!( + sink.wal_writes(), + 1, + "G1 can-fire: c1 is the first WAL write" + ); + // MID-FLIGHT CORRECTION (design note §2 step 8 is the + // authority): 24 = 20 Flow advances + 4 Block advances, not + // 24 uniform Elixir/CognitiveWork crossings. + let flow_advances = outcome + .sealed + .transitions + .iter() + .filter(|t| { + t.mv.from == KanbanColumn::Planning + && t.mv.to == KanbanColumn::CognitiveWork + }) + .count(); + let block_advances = outcome + .sealed + .transitions + .iter() + .filter(|t| { + t.mv.from == KanbanColumn::Planning && t.mv.to == KanbanColumn::Prune + }) + .count(); + assert_eq!( + flow_advances, 20, + "G1 can-fire: 20 Flow advances at c1 (IGNITE_A 6 + IGNITE_C 6 + REST 8)" + ); + assert_eq!( + block_advances, 4, + "G1 can-fire: 4 Block advances at c1 (CONTRA)" + ); + eprintln!("probe.ignition.G1 decomposition @c1: Planning->CognitiveWork=20 (Flow), Planning->Prune=4 (Block)"); + let untouched_before = fingerprint_all(&fleet, unarmed_and_outside.iter().copied()); + // (fingerprints were already taken post-apply above via snap_post_apply + // for phase/cycle; re-derive the FULL fingerprint here for the + // can-stay-silent half's field-isolation claim.) + assert_eq!( + untouched_before.len(), + unarmed_and_outside.len(), + "G1 can-stay-silent: every OUTSIDE/UNARMED id must have a real owner" + ); + eprintln!( + "probe.ignition.G1 can-stay-silent @c1: {} OUTSIDE+UNARMED owners fingerprinted (phase+cycle+energy+meta+qualia); \ + decomposition 32 out-of-scope + 7 unarmed + 1 orphan = 40 untouched", + untouched_before.len() + ); + } + + // ── write-back pass (design §2 step 13): &mut, AFTER apply, + // never during compute. ────────────────────────────────────── + for mv in &outcome.applied.applied { + let id = mv.mailbox; + let row = *next_firing_row.get(&id).unwrap_or(&0); + let owner = fleet + .get_mut(&id) + .expect("an applied move's owner must exist"); + let consumed = owner.consume_firing(row); + assert!( + consumed, + "write-back @c{c}: row {row} of owner {id} must still be firing" + ); + next_firing_row.insert(id, row + 1); + } + + if c == 1 { + let g4_owner = fleet.get(&REST_LO).expect("REST_LO owner"); + assert_eq!( + g4_owner.qualia_at(0), + flow_qualia(), + "G4 can-fire: REST qualia is the Flow fixture" + ); + eprintln!("probe.ignition.G4 can-fire @c1: REST owner {REST_LO} cast on a would-be-Flow qualia (mantissa was >0 pre-consumption)"); + } + if c == 2 { + let g4_owner = fleet.get(&REST_LO).expect("REST_LO owner"); + let q = g4_owner.qualia_at(0); + assert_eq!( + q, + flow_qualia(), + "G4 can-stay-silent: qualia byte-identical to c1 (never rewritten)" + ); + assert_ne!( + q, + QualiaI4_16D::ZERO, + "G4 anti-rig: qualia must not be the trivial zero vector" + ); + assert_eq!( + trust_texture_i4(&q), + TrustTexture::Calibrated, + "G4 anti-rig: texture is Calibrated" + ); + let flow_proxy = i32::from(q.get(3)) + i32::from(q.get(14)) - i32::from(q.get(2)); + assert!( + flow_proxy >= 4, + "G4 anti-rig: warmth+groundedness-tension must be a would-be-Flow value" + ); + assert_eq!( + mantissa_of(g4_owner), + 0, + "G4 can-stay-silent: mantissa has fallen to 0 by c2" + ); + eprintln!( + "probe.ignition.G4 can-stay-silent @c2: REST owner {REST_LO} rests on an UNCHANGED, non-trivial \ + would-be-Flow qualia (flow_proxy={flow_proxy}); only mantissa (derived, live) differs" + ); + } + } + + assert!( + rediscovered_rest_c2, + "G5 can-fire: all 8 REST owners re-found by the scan at c2" + ); + assert!( + rediscovered_rest_c3, + "G5 can-fire: all 8 REST owners re-found by the scan at c3" + ); + assert!( + contra_seen_after_c1.is_empty(), + "G5 can-stay-silent: no CONTRA owner may appear in any active scan bucket after c1 (absorbing)" + ); + eprintln!( + "probe.ignition.G5: rediscovered(REST)=8 at c2 and c3; rediscovered(CONTRA)=0 across c2..c{CYCLES}" + ); + + // ── G7: the where() axis is load-bearing ──────────────────────────── + { + let outside_id = OUTSIDE_LO; + let owner = fleet.get(&outside_id).expect("OUTSIDE owner must exist"); + assert_eq!( + owner.phase(), + KanbanColumn::Planning, + "G7 can-stay-silent: OUTSIDE owner never advanced in the main run" + ); + assert_eq!( + mantissa_of(owner), + 3, + "G7 can-stay-silent: OUTSIDE owner's firing rows were never consumed" + ); + let mut throwaway: BatchWriter> = BatchWriter::new(); + let (out, payload) = plan_or_evaluate_think(owner) + .expect("G7 can-fire: a scanned OUTSIDE owner must produce a cast-able outcome"); + let cast = emit_bootstrap_intent( + &out, + owner.mailbox_id(), + owner.current_cycle(), + &mut throwaway, + payload, + ); + assert!( + cast.is_some(), + "G7 can-fire: widening the scope by one OUTSIDE id must stage a cast" + ); + eprintln!("probe.ignition.G7: OUTSIDE owner {outside_id} — silent in the main run (address excluded), casts when scanned directly (throwaway writer)"); + } + + // ── G8: the style-arming axis is load-bearing ────────────────────── + { + let unarmed_id = UNARMED_LO; + { + let owner = fleet.get(&unarmed_id).expect("UNARMED owner must exist"); + assert_eq!( + owner.meta_at(0).thinking(), + 0, + "G8 can-stay-silent: UNARMED owner stayed unarmed the whole main run" + ); + assert_eq!( + owner.phase(), + KanbanColumn::Planning, + "G8 can-stay-silent: UNARMED owner never advanced" + ); + } + let owner_mut = fleet + .get_mut(&unarmed_id) + .expect("UNARMED owner must exist"); + owner_mut.set_meta(0, MetaWord::new(1, 0, 0, 0, 0)); + let owner = fleet.get(&unarmed_id).expect("UNARMED owner must exist"); + let mut throwaway: BatchWriter> = BatchWriter::new(); + let (out, payload) = plan_or_evaluate_think(owner) + .expect("G8 can-fire: an armed owner must produce a cast-able outcome"); + let cast = emit_bootstrap_intent( + &out, + owner.mailbox_id(), + owner.current_cycle(), + &mut throwaway, + payload, + ); + assert!( + cast.is_some(), + "G8 can-fire: writing non-zero thinking bits must stage a cast" + ); + eprintln!("probe.ignition.G8: UNARMED owner {unarmed_id} — silent while thinking bits were 0, casts once armed (throwaway writer)"); + } + + // ── G10: the missing-owner accounting gap (design §6 Q2, #879 OPEN) ─ + { + let ids = [ORPHAN_ID]; + let mut probe_writer: BatchWriter> = BatchWriter::new(); + let probe_local = column_pass(&fleet, &ids, &mut probe_writer, plan_or_evaluate_think); + assert_eq!( + probe_local.missing, 1, + "G10: probe-local column_pass must count the orphan explicitly" + ); + let probe_local_total = probe_local.cast + probe_local.held.len() + probe_local.missing; + + let mut shipped_writer: BatchWriter> = BatchWriter::new(); + let shipped = + run_cognitive_work_gated_over(&fleet, &ids, &mut shipped_writer, |owner| { + let armed = owner.meta_at(0).thinking(); + let style = thinking_style_for(armed); + let ctx = plan_context_for(armed); + let qualia = owner.qualia_at(0); + let mantissa = mantissa_of(owner); + let reliability = StyleStrategy::reliability_for(style, &ctx); + Some((qualia, mantissa, reliability, row_span_payload(owner))) + }); + let shipped_total = shipped.cast + shipped.held_owners.len(); + + assert_eq!( + probe_local_total as i64 - shipped_total as i64, + 1, + "G10: the probe-local pass accounts for the orphan; the shipped seam silently drops it — the two totals must differ by exactly 1" + ); + eprintln!( + "probe.ignition.G10: probe-local total={probe_local_total} (missing=1 counted), shipped total={shipped_total} \ + (orphan silently skipped, no counter) — difference=1, matching #879's OPEN caveat" + ); + } + + // ── compile-time self-scans (G2a, G3b static half, G11) ───────────── + // + // Every needle below is built by CONCATENATING two literal pieces + // that are never adjacent in this file's own source text — a needle + // spelled out contiguously would make its own absence-check + // vacuously true, since `include_str!` reads THIS file, including + // the scan code itself. + { + let src = include_str!("probe_ignition.rs"); + + let kanban_move_literal = format!("{}{} {{", "Kanban", "Move"); + assert!( + !src.contains(&kanban_move_literal), + "G2a can-fire: source must never construct a kanban move via its own struct-literal syntax" + ); + assert!( + src.contains("emit_bootstrap_intent"), + "G2a can-stay-silent: the scan must be able to find real content — a scan finding nothing is not evidence" + ); + eprintln!("probe.ignition.G2a: no kanban-move struct-literal in source; emit_bootstrap_intent present (scan mechanism proven live)"); + + let advance_phase_call = format!(".{}(", "advance_phase"); + let try_advance_phase_call = format!(".{}(", "try_advance_phase"); + assert!( + !src.contains(&advance_phase_call), + "G3b can-fire (static half): source must not call the owner's phase-mutating methods directly" + ); + assert!( + !src.contains(&try_advance_phase_call), + "G3b can-fire (static half): source must not call the owner's phase-mutating methods directly" + ); + eprintln!("probe.ignition.G3b (static half): no direct phase-mutating call anywhere in source; dynamic half asserted every cycle above"); + + let ack_call = "a".to_string() + "ck("; + let confirm_call = "confir".to_string() + "m("; + assert!( + !src.contains(&ack_call), + "G11 can-fire: source must not define or call an ack-shaped confirmation method" + ); + assert!( + !src.contains(&confirm_call), + "G11 can-fire: source must not define or call a confirm-shaped confirmation method" + ); + assert!( + src.contains("BatchWriter"), + "G11 can-stay-silent: the scan must be able to find real content — a scan finding nothing is not evidence" + ); + // NOTE: this message deliberately avoids writing the two needles + // literally — an earlier form spelled them out and the scan matched + // its OWN success message (a self-match false positive, caught by + // the central gate run). The needles are built by concatenation + // above for the same reason. + eprintln!("probe.ignition.G11: no ack-shaped or confirm-shaped identifier anywhere in source (E-ACK-ELIMINATED-1); scan mechanism proven live"); + } + + // ── §5 Not-claimed block (design §5, printed verbatim in spirit) ──── + println!(); + println!("== PROBE-IGNITION — what this probe does NOT claim =="); + println!("1. No durability. MemWal is an in-process Mutex/Vec; its versions are sequence numbers, not Lance versions."); + println!("2. No parallelism. The loop is synchronous."); + println!("3. No scale claim. {FLEET_OWNERS} owners; the 64k sparse property is proven separately over FakeOwner."); + println!("4. No multi-writer claim. Single-writer MemWal."); + println!( + "5. No deinterlace / temporal claim. This probe does not read through deinterlace." + ); + println!( + "6. No validity claim. reliability is settledness, not ground-truth correspondence." + ); + println!("7. No GUID-prefix routing claim. where() is a contiguous MailboxId range, an honest stand-in."); + println!("8. No 36-style claim. Three styles are reachable here (Analytical/Creative/Reflective)."); + println!("9. No semantic claim about the corpus. Qualia are declared fixtures, not encoded from text."); + println!("10. No zero-copy claim. SweepSlot::payload is Vec by the shipped signature."); + println!("11. No claim that the loop can re-enter Planning. The arc stops at Commit/Prune/Hold in this probe."); + println!("12. No recovery claim. recover_fleet is not exercised; G9 (separate test) covers only the WAL-failure retry path."); + } + + // ── G9: the drained-writer retry footgun (side fixture, own MemWal) ───── + // + // #879 OPEN. Provenance for the shape: `cycle_driver.rs`'s own + // `failed_seal_preserves_the_frozen_cycle_for_byte_identical_retry` test + // (`cycle_driver.rs:963-1006`) — re-derived here (that fixture is + // `#[cfg(test)]`-private to `cycle_driver`, not importable). + #[tokio::test] + async fn probe_ignition_g9_drained_writer_retry_footgun() { + struct FlakyWal { + inner: MemWal, + fail_next: AtomicBool, + } + impl FlakyWal { + fn new() -> Self { + Self { + inner: MemWal::new(), + fail_next: AtomicBool::new(false), + } + } + fn fail_next_commit(&self) { + self.fail_next.store(true, Ordering::SeqCst); + } + } + impl WalSink for FlakyWal { + async fn commit_cycle( + &self, + base: DatasetVersion, + batch: DetachedCycleBatch, + ) -> Result { + if self.fail_next.swap(false, Ordering::SeqCst) { + return Err(WriteFailed("G9 injected retryable WAL failure".into())); + } + self.inner.commit_cycle(base, batch).await + } + async fn scan_sealed( + &self, + from_version: Option, + ) -> Result, WriteFailed> { + self.inner.scan_sealed(from_version).await + } + async fn versions(&self) -> Result, WriteFailed> { + self.inner.versions().await + } + } + + let g9_verses = synthetic_corpus(2 * POPULATED_ROWS); + let mut fleet: Fleet = Fleet::new(); + fleet.insert( + 900, + build_owner(900, &g9_verses[0..POPULATED_ROWS], 1, flow_qualia(), 1), + ); + fleet.insert( + 901, + build_owner( + 901, + &g9_verses[POPULATED_ROWS..2 * POPULATED_ROWS], + 1, + flow_qualia(), + 1, + ), + ); + + let sink = FlakyWal::new(); + let mut writer: BatchWriter> = BatchWriter::new(); + let mut watermarks: HashMap> = HashMap::new(); + + let scan = scan_board(&fleet, [900, 901]); + assert_eq!(scan.planning.len(), 2, "both G9 owners start in Planning"); + let staged = column_pass(&fleet, &scan.planning, &mut writer, plan_or_evaluate_think); + assert_eq!(staged.cast, 2, "both G9 owners must stage a cast"); + + let before_phases = phase_cycle_snapshot(&fleet); + sink.fail_next_commit(); + let err = run_cycle( + &sink, + &mut fleet, + &mut writer, + CycleFrame::new(CycleId(1), DatasetVersion(0)), + 0, + &mut watermarks, + u64::from, + ) + .await + .expect_err("G9: the injected WAL failure must surface as CycleError::Seal"); + + let CycleError::Seal(failure) = err else { + panic!("G9: expected a Seal failure"); + }; + assert_eq!( + failure.casts.len(), + 2, + "G9: the frozen cycle carries both casts, byte-identical" + ); + assert_eq!( + before_phases, + phase_cycle_snapshot(&fleet), + "G9: no owner mutated on a failed seal" + ); + eprintln!("probe.ignition.G9 can-fire (half 1): injected WAL failure surfaced as CycleError::Seal, zero owner mutation"); + + let frozen_frame = failure.frame; + let frozen_casts = failure.casts; + let sealed = seal_cycle(&sink, frozen_frame, frozen_casts) + .await + .expect("G9: the retry with the frozen cast set must succeed"); + assert_eq!( + sink.inner.wal_writes(), + 1, + "G9: exactly one successful WAL write total" + ); + let applied = apply_sealed_transitions(&mut fleet, &sealed, &mut watermarks) + .expect("G9: apply must succeed on the retried seal"); + assert_eq!( + applied.applied.len(), + 2, + "G9: both owners advance exactly once" + ); + eprintln!("probe.ignition.G9 can-fire (half 2): retry via seal_cycle(sink, failure.frame, failure.casts) lands the byte-identical cycle"); + + // ── can-stay-silent: on the SAME (already-drained) writer, a fresh + // collect_casts yields zero slots — the footgun made visible. A + // naive `run_cycle` retry (which calls collect_casts fresh) would + // seal an EMPTY cycle here and silently "succeed" — a future guard + // must flip this from silent to loud. ────────────────────────────── + let redrained = collect_casts(&mut writer, CycleId(2), 0, u64::from); + assert!( + redrained.slots.is_empty(), + "G9 can-stay-silent: the drained writer has nothing left to collect — a naive retry via run_cycle would seal an empty cycle here" + ); + eprintln!("probe.ignition.G9 can-stay-silent: fresh collect_casts on the drained writer yields 0 slots (the footgun this test pins for a future guard)"); + } +} diff --git a/crates/lance-graph-supervisor/tests/probe_ignition_64k.rs b/crates/lance-graph-supervisor/tests/probe_ignition_64k.rs new file mode 100644 index 000000000..a3c723348 --- /dev/null +++ b/crates/lance-graph-supervisor/tests/probe_ignition_64k.rs @@ -0,0 +1,475 @@ +//! PROBE-IGNITION-64K — `start()` at the MAIN MODEL's full population. +//! +//! Answers ONE question the 64-owner probe deliberately did not: does the +//! ignition machinery — arm by `MetaWord` write, discover by board scan, +//! cast write-on-behalf, seal ONCE, apply — hold at **65,536 real +//! `MailboxSoA` owners, 1:1, mutation-exclusive** (the operator-ordered +//! main model, `EPIPHANIES.md` E-64K-1TO1-OWNERS-IS-THE-MAIN-MODEL-1)? +//! +//! Scale is made feasible by shrinking the per-owner slice, not the +//! population: `MailboxSoA<4>` with ONE populated row per owner (the +//! main-model axis is OWNERS; rows-per-owner is the benchmark axis, +//! plan §12.3a′/§12.3a‴). The identity planes are lazily mapped, so RSS is +//! dominated by the one written row per owner. +//! +//! ## What this probe does NOT claim (printed at the end of the run) +//! +//! **No concurrency.** The loop is synchronous — this is the SCALE half of +//! the main-model claim only; "parallel" remains gated by D-KIA-A2's +//! pre-registered protocol. **No timing claim** (wall times are printed as +//! provenance, never asserted). **No durability** (`MemWal`). **No +//! semantic claim** (synthetic single-row content, non-degeneracy sampled). +//! +//! Shapes copied with provenance from `tests/probe_ignition.rs` (GREEN, +//! 2/2): the qualia fixtures, the MemWal seam, the gate->style->emit pass, +//! the one-seal/rest disciplines. + +#[cfg(feature = "cycle-driver")] +mod probe_ignition_64k { + #![allow( + clippy::cast_possible_truncation, + clippy::cast_possible_wrap, + clippy::cast_sign_loss + )] + + use std::collections::HashMap; + use std::sync::atomic::{AtomicU64, Ordering}; + use std::sync::Mutex; + use std::time::Instant; + + use cognitive_shader_driver::mailbox_soa::{MailboxSoA, WriteCell, WriteOutcome, WORDS_PER_FP}; + use lance_graph_contract::cognitive_shader::MetaWord; + use lance_graph_contract::collapse_gate::MailboxId; + use lance_graph_contract::kanban::{ExecTarget, KanbanColumn}; + use lance_graph_contract::mul::i4_eval::gate_decision_i4; + use lance_graph_contract::qualia::QualiaI4_16D; + use lance_graph_contract::scheduler::DatasetVersion; + use lance_graph_contract::soa_view::MailboxSoaView; + use lance_graph_planner::batch_writer::BatchWriter; + use lance_graph_planner::ir::Arena; + use lance_graph_planner::owner_adapter::emit_bootstrap_intent; + use lance_graph_planner::persist_sink::{ + CycleFrame, CycleId, DetachedCycleBatch, LandedSlot, SweepSlot, WalSink, WriteFailed, + }; + use lance_graph_planner::strategy::style_strategy::StyleStrategy; + use lance_graph_planner::traits::{ + PlanContext, PlanInput, PlanStrategy, QueryFeatures, StrategyOutcome, + }; + use lance_graph_supervisor::cycle_driver::{ + run_cognitive_work_gated_over, run_cycle, CycleError, CycleOutcome, + }; + + // ── PRE-REGISTERED run shape — fixed BEFORE any number exists. ───────── + + /// The full main-model population. 65,536 = the "64k" of the order. + const FLEET_OWNERS: MailboxId = 65_536; + /// Minimal slice: rows-per-owner is NOT this probe's axis (§12.3a‴). + const ROWS_PER_OWNER: usize = 4; + const POPULATED_ROWS: usize = 1; + /// Sampled non-degeneracy: this many owners' planes checked non-zero + /// and pairwise distinct (a full 64k pairwise sweep proves nothing more + /// for O(n²) cost). + const DISTINCTNESS_SAMPLE: usize = 8; + const TENANT_THRESHOLD: f32 = 1.0; + const FIRE_ENERGY: f32 = 2.0; + const TENANT_W_SLOT: u8 = 0; + /// Reliability handed to the cycle-2 gated pass (HAND-TUNED constant, + /// inert to this probe's assertions — nothing branches on it). + const GATED_RELIABILITY: f32 = 0.5; + + type Tenant = MailboxSoA; + type Fleet = HashMap; + + /// Flow qualia — the shipped gate falsifiers' own fixture construction + /// (`probe_ignition.rs:201-203` provenance; `cycle_driver.rs:1669`): + /// warmth=4, groundedness=3, coherence=4, valence=2 => flow_proxy 7, + /// Calibrated. NOT an all-zeros rig — the silence at cycle 2 happens on + /// a would-be-Flow qualia because the derived mantissa fell to 0. + fn flow_qualia() -> QualiaI4_16D { + QualiaI4_16D(0).with(3, 4).with(14, 3).with(9, 4).with(1, 2) + } + + /// Derived mantissa (`probe_ignition.rs:192-194` provenance). + fn mantissa_of(owner: &Tenant) -> i8 { + owner.pending_count().min(7) as i8 + } + + /// Synthetic non-zero identity plane, distinct per owner (splitmix-style + /// scramble of the id — deterministic, no clock, no rng). + fn plane_for(id: MailboxId) -> Vec { + let mut x = u64::from(id) ^ 0x9E37_79B9_7F4A_7C15; + let mut plane = vec![0u64; WORDS_PER_FP]; + for w in plane.iter_mut() { + x ^= x >> 30; + x = x.wrapping_mul(0xBF58_476D_1CE4_E5B9); + x ^= x >> 27; + x = x.wrapping_mul(0x94D0_49BB_1331_11EB); + x ^= x >> 31; + *w = x | 1; // never a zero word + } + plane + } + + fn payload_for(id: MailboxId) -> Vec { + u64::from(id).to_le_bytes().to_vec() + } + + // ── the WAL seam (in-process; NOT durability) — `probe_ignition.rs` + // MemWal, trimmed to what this probe asserts. ────────────────────────── + + struct SealedCycle { + version: DatasetVersion, + landings: Vec, + } + + struct MemWal { + sealed: Mutex>, + next_version: AtomicU64, + wal_writes: AtomicU64, + } + + impl MemWal { + fn new() -> Self { + Self { + sealed: Mutex::new(Vec::new()), + next_version: AtomicU64::new(1), + wal_writes: AtomicU64::new(0), + } + } + fn wal_writes(&self) -> u64 { + self.wal_writes.load(Ordering::SeqCst) + } + fn head(&self) -> DatasetVersion { + self.sealed + .lock() + .expect("MemWal poisoned") + .last() + .map_or(DatasetVersion(0), |s| s.version) + } + } + + impl WalSink for MemWal { + async fn commit_cycle( + &self, + base: DatasetVersion, + batch: DetachedCycleBatch, + ) -> Result { + let mut sealed = self.sealed.lock().expect("MemWal poisoned"); + let head = sealed.last().map_or(DatasetVersion(0), |s| s.version); + if base != head { + return Err(WriteFailed(format!( + "stale base {base:?}: sealed head is {head:?}" + ))); + } + self.wal_writes.fetch_add(1, Ordering::SeqCst); + let version = DatasetVersion(self.next_version.fetch_add(1, Ordering::SeqCst)); + sealed.push(SealedCycle { + version, + landings: batch.landings, + }); + Ok(version) + } + + async fn scan_sealed( + &self, + from_version: Option, + ) -> Result, WriteFailed> { + Ok(self + .sealed + .lock() + .expect("MemWal poisoned") + .iter() + .filter(|s| from_version.is_none_or(|f| s.version > f)) + .flat_map(|s| { + s.landings.iter().map(|slot| LandedSlot { + version: s.version, + slot: slot.clone(), + }) + }) + .collect()) + } + + async fn versions(&self) -> Result, WriteFailed> { + Ok(self + .sealed + .lock() + .expect("MemWal poisoned") + .iter() + .map(|s| { + ( + s.landings.first().map_or(CycleId(0), |l| l.cycle), + s.version, + ) + }) + .collect()) + } + } + + // ── fleet construction: 65,536 real 1:1 owners ───────────────────────── + + fn build_owner(id: MailboxId) -> Tenant { + let mut owner: Tenant = MailboxSoA::new(id, TENANT_W_SLOT, TENANT_THRESHOLD); + let cycle = owner.cycle(); + let plane = plane_for(id); + let cell = WriteCell { + content: Some(plane.as_slice()), + qualia: Some(flow_qualia()), + meta: Some(MetaWord::new(1, 0, 0, 0, 0)), // armed z=1 (Analytical) + entity_type: Some((id % 251) as u16), + temporal: Some(u64::from(id)), + ..WriteCell::default() + }; + let outcome = owner.write_row(0, cycle, &cell); + assert_eq!(outcome, WriteOutcome::Accepted, "seeding owner {id} row 0"); + owner.set_populated(POPULATED_ROWS); + owner.tick(); + owner.energy[0] = FIRE_ENERGY; // one firing row: exhausts after one advance + owner + } + + /// The 23D style vector for z=1 (`probe_ignition.rs:165-173` provenance: + /// idx 4 = analytical). + fn style_context() -> PlanContext { + let mut v = vec![0.0f64; 23]; + v[4] = 1.0; + PlanContext { + query: String::new(), + features: QueryFeatures::default(), + free_will_modifier: 1.0, + thinking_style: Some(v), + nars_hint: None, + witness: None, + } + } + + // ── the probe ────────────────────────────────────────────────────────── + + #[tokio::test] + async fn probe_ignition_64k_start_at_full_population() { + let t0 = Instant::now(); + let mut fleet = Fleet::with_capacity(FLEET_OWNERS as usize); + for id in 0..FLEET_OWNERS { + fleet.insert(id, build_owner(id)); + } + eprintln!( + "probe.ignition64k build: {FLEET_OWNERS} real 1:1 MailboxSoA<{ROWS_PER_OWNER}> owners in {:?}", + t0.elapsed() + ); + + // Non-degeneracy (sampled): planes non-zero + pairwise distinct. + { + let ids: Vec = (0..DISTINCTNESS_SAMPLE as MailboxId) + .map(|i| i * (FLEET_OWNERS / DISTINCTNESS_SAMPLE as MailboxId)) + .collect(); + let mut planes = Vec::new(); + for &id in &ids { + let owner = fleet.get(&id).expect("sampled owner exists"); + let p = owner.content_row(0).to_vec(); + assert!( + p.iter().any(|&w| w != 0), + "owner {id} plane must be non-zero" + ); + planes.push(p); + } + for i in 0..planes.len() { + for j in (i + 1)..planes.len() { + assert_ne!(planes[i], planes[j], "sampled planes must be distinct"); + } + } + eprintln!( + "probe.ignition64k non-degeneracy: {DISTINCTNESS_SAMPLE} sampled planes non-zero + pairwise distinct" + ); + } + + let sink = MemWal::new(); + let mut writer: BatchWriter> = BatchWriter::new(); + let mut watermarks: HashMap> = HashMap::new(); + let position_base: u64 = 0; + + // ── S0: the STYLE's mint, computed ONCE — every owner is armed with + // the same z, so the style outcome is owner-independent; the + // PER-OWNER binding is `rebind_bootstrap`'s job inside + // `emit_bootstrap_intent` (owner_adapter.rs — the no-theft guard). + // `emit_bootstrap_intent` takes `&StrategyOutcome`, so one plan() + // serves 65,536 emits. ──────────────────────────────────────────── + let mut arena = Arena::new(); + let plan_out = StyleStrategy + .plan( + PlanInput { + plan: None, + context: style_context(), + outcome: None, + }, + &mut arena, + ) + .expect("StyleStrategy::plan over the fixed recipe substrate"); + let style_outcome: StrategyOutcome = plan_out + .outcome + .expect("StyleStrategy always surfaces a StrategyOutcome"); + + // ── CYCLE 1: LOOK INTO THE KANBAN (scan all 64k) + CAST ──────────── + let t1 = Instant::now(); + let mut cast = 0usize; + for id in 0..FLEET_OWNERS { + let owner = fleet.get(&id).expect("owner exists"); + assert_eq!( + owner.phase(), + KanbanColumn::Planning, + "scan @c1: owner {id}" + ); + let qualia = owner.qualia_at(0); + let mantissa = mantissa_of(owner); + let gate = gate_decision_i4(&qualia, mantissa); + let target = owner + .phase() + .advance_on_gate(&gate) + .expect("c1: every owner Flows (flow qualia + firing row)"); + assert_eq!(target, KanbanColumn::CognitiveWork, "c1 gate target"); + let ok = emit_bootstrap_intent( + &style_outcome, + owner.mailbox_id(), + owner.current_cycle(), + &mut writer, + payload_for(id), + ); + assert!(ok.is_some(), "c1: cast for owner {id} must stage"); + cast += 1; + } + eprintln!( + "probe.ignition64k c1 cast: {cast} casts staged in {:?} (scan + gate + style emit)", + t1.elapsed() + ); + assert_eq!( + cast, FLEET_OWNERS as usize, + "S1 can-fire: every owner casts" + ); + + // ── ONE SEAL — the single deterministic convergence boundary of the + // main model, at full population. ────────────────────────────────── + let t2 = Instant::now(); + let wal_before = sink.wal_writes(); + let base = sink.head(); + let outcome: CycleOutcome = match run_cycle( + &sink, + &mut fleet, + &mut writer, + CycleFrame::new(CycleId(1), base), + position_base, + &mut watermarks, + u64::from, + ) + .await + { + Ok(o) => o, + Err(CycleError::Seal(_)) => panic!("64k: unexpected seal failure"), + Err(CycleError::Apply { cause, .. }) => { + panic!("64k: unexpected apply failure: {cause}") + } + }; + // Restart-stable contract honored even though this probe seals once: + // the advanced base is ASSERTED (a second cycle would resume from it). + let advanced_base = position_base.max(outcome.sealed.next_position_base); + assert!( + advanced_base >= FLEET_OWNERS as u64, + "position_base advances past the 64k sealed positions" + ); + eprintln!( + "probe.ignition64k c1 seal+apply: {} transitions in {:?}", + outcome.sealed.transitions.len(), + t2.elapsed() + ); + + // S2 can-fire: 65,536 casts converged into EXACTLY ONE WAL write. + assert_eq!(sink.wal_writes(), wal_before + 1, "S2: one seal, one write"); + assert_eq!( + outcome.sealed.transitions.len(), + FLEET_OWNERS as usize, + "S2: every owner's move sealed in the one cycle" + ); + // Every sealed move is the STYLE's Planning->CognitiveWork mint. + for t in &outcome.sealed.transitions { + assert_eq!(t.mv.from, KanbanColumn::Planning, "sealed move origin"); + assert_eq!(t.mv.to, KanbanColumn::CognitiveWork, "sealed move target"); + assert_eq!(t.mv.exec, ExecTarget::Elixir, "the style's mint"); + } + // Ordering discipline: positions strictly monotone within the seal + // (the write-side owns arrival, never cross-mailbox order). + for w in outcome.sealed.transitions.windows(2) { + assert!( + w[0].stream_position < w[1].stream_position, + "sealed positions strictly monotone" + ); + } + // Applied on every owner: phase advanced via the seal, nothing else. + let mut advanced = 0usize; + for (_, owner) in fleet.iter() { + if owner.phase() == KanbanColumn::CognitiveWork { + advanced += 1; + } + } + assert_eq!(advanced, FLEET_OWNERS as usize, "S3: all 64k advanced"); + eprintln!( + "probe.ignition64k S1-S3: {FLEET_OWNERS} owners armed->cast->sealed(1 write)->advanced" + ); + + // ── write-back AFTER apply (never during compute): consume the one + // firing row per owner — the mantissa falls to 0. ────────────────── + for id in 0..FLEET_OWNERS { + let owner = fleet.get_mut(&id).expect("owner exists"); + owner.consume_firing(0); + } + + // ── CYCLE 2: the SILENT twin at full population — the same shipped + // gated pass, every owner on a would-be-Flow qualia, and the whole + // 64k fleet RESTS because the derived mantissa is 0. + // + // Measurement note (a first draft asserted `writer.casts().len() == 0` + // and failed at 65,536): `casts()` is the CUMULATIVE board — cycle 1's + // cast records are retained after `collect_casts` drains the payloads + // (the documented drained-writer semantics G9 pins in the 64-owner + // probe). The rest is therefore measured as a DELTA. ──────────────── + let t3 = Instant::now(); + let casts_before_c2 = writer.casts().len(); + let ids: Vec = (0..FLEET_OWNERS).collect(); + let held = run_cognitive_work_gated_over(&fleet, &ids, &mut writer, |owner| { + Some(( + owner.qualia_at(0), + mantissa_of(owner), + GATED_RELIABILITY, + payload_for(owner.mailbox_id()), + )) + }); + let staged_c2 = writer.casts().len() - casts_before_c2; + assert_eq!( + staged_c2, 0, + "S4 can-stay-silent: zero NEW casts at c2 — the fleet rests on non-trivial qualia (flow_proxy 7), not on an all-zeros rig" + ); + // The positive half of the silence: every one of the 64k owners was + // SEEN by the pass and Held — the rest is a decision over the full + // population, not an empty scan. + assert_eq!( + held.held_owners.len(), + FLEET_OWNERS as usize, + "S4: every owner was seen and Held (rest is a per-owner decision, not absence)" + ); + // No seal on a rest cycle: wal_writes frozen at 1. + assert_eq!(sink.wal_writes(), 1, "S4: resting must not write"); + eprintln!( + "probe.ignition64k c2 rest: 0 new casts, {} owners seen+Held in {:?}; wal_writes frozen at 1", + held.held_owners.len(), + t3.elapsed() + ); + + println!(); + println!("== PROBE-IGNITION-64K — what this probe does NOT claim =="); + println!("1. No CONCURRENCY. The loop is synchronous; this is the SCALE half of the main-model claim only. 'Parallel' remains gated by D-KIA-A2's pre-registered protocol (median-of-5, >=2x at >=4,096 owners, >=100us bodies)."); + println!("2. No timing claim. Wall times above are provenance, never asserted."); + println!("3. No durability. MemWal is an in-process Mutex/Vec."); + println!("4. No semantic claim. One synthetic row per owner; distinctness sampled at {DISTINCTNESS_SAMPLE}."); + println!("5. No rows-axis claim. ROWS_PER_OWNER=4 on purpose — that axis belongs to D-BLW-4 (plan 12.3a-prime)."); + println!( + "TOTAL: {FLEET_OWNERS} real 1:1 owners, arm->scan->cast->seal(1)->advance->rest, {:?} end to end.", + t0.elapsed() + ); + } +} diff --git a/crates/lance-graph-supervisor/tests/supervisor_one_for_one_restart.rs b/crates/lance-graph-supervisor/tests/supervisor_one_for_one_restart.rs index f11877992..b2e720079 100644 --- a/crates/lance-graph-supervisor/tests/supervisor_one_for_one_restart.rs +++ b/crates/lance-graph-supervisor/tests/supervisor_one_for_one_restart.rs @@ -51,7 +51,7 @@ mod tests { assert!(live_before.contains(&4), "G=4 should be live before crash"); // Kill the G=2 actor by registry name. - if let Some(g2_cell) = ractor::registry::where_is("consumer_g_2".to_string()) { + if let Some(g2_cell) = ractor::registry::where_is("consumer_g_2") { g2_cell.stop(None); } diff --git a/crates/lance-graph-supervisor/tests/w2b_real_owner_probe.rs b/crates/lance-graph-supervisor/tests/w2b_real_owner_probe.rs index 656ecd6bb..332c327b4 100644 --- a/crates/lance-graph-supervisor/tests/w2b_real_owner_probe.rs +++ b/crates/lance-graph-supervisor/tests/w2b_real_owner_probe.rs @@ -1,27 +1,27 @@ -//! D-V3-W2b integration probe: KanbanActor spawned over the REAL production -//! `MailboxSoaOwner` (`cognitive_shader_driver::mailbox_soa::MailboxSoA`), not -//! the in-file `TestBoard` fake that `kanban_actor.rs`'s own unit tests use. +//! D-V3-W2b integration probe, direct-owner form: the REAL production +//! `MailboxSoaOwner` (`cognitive_shader_driver::mailbox_soa::MailboxSoA`) +//! implements the Rubicon lifecycle DAG through the contract trait — legal +//! advances persist on the real SoA, illegal edges are rejected with no +//! mutation. //! -//! Closes the gap named in D-V3-W2b: until this probe, `KanbanActor` was -//! only ever exercised against `kanban_actor::tests::TestBoard` — a minimal -//! in-RAM stand-in with no SoA columns. This probe proves the SAME actor -//! message surface (`KanbanMsg::Advance` / `KanbanMsg::Phase`) drives the -//! REAL owner's `try_advance_phase` (via the contract's `MailboxSoaOwner` -//! trait), that illegal transitions are rejected with no mutation on the -//! real SoA, and that the actor is the ONLY path this probe ever uses to -//! mutate the row (no direct `advance_phase`/`try_advance_phase` call from -//! the probe itself — only through `KanbanMsg`). +//! ## 2026-08-05 migration — the actor surface this probe drove is deleted //! -//! Spec: `.claude/board/*` D-V3-W2b (KanbanActor never spawned over real -//! MailboxSoA — this file closes that gap). +//! The original W2b closed the gap "`KanbanActor` never spawned over a real +//! `MailboxSoA`" by driving `KanbanMsg::{Advance, Phase}` RPCs. That actor +//! surface is DELETED (`E-PROGRESSION-IS-EXISTENCE-NOT-COMMAND-1`); what +//! remains worth pinning is the half that was never about messages: the real +//! owner's lifecycle DAG behind `try_advance_phase`, exercised through plain +//! `&mut` — which IS the single-writer guarantee (a second mutator is a +//! compile error, not a runtime property this test could miss). The probe +//! also exercises the replacement visibility surface ([`PhaseCensus`]) over +//! the real SoA. #[cfg(feature = "supervisor")] mod w2b_real_owner_probe { use cognitive_shader_driver::mailbox_soa::MailboxSoA; use lance_graph_contract::kanban::KanbanColumn; - use lance_graph_contract::soa_view::MailboxSoaView; - use lance_graph_supervisor::kanban_actor::{KanbanActor, KanbanMsg}; - use ractor::Actor; + use lance_graph_contract::soa_view::{MailboxSoaOwner, MailboxSoaView}; + use lance_graph_supervisor::PhaseCensus; /// Small capacity — the probe only needs the owner's phase column, not a /// realistic row count. Mirrors `mailbox_soa.rs`'s own unit tests @@ -40,122 +40,93 @@ mod w2b_real_owner_probe { // matching how a real spawn would declare its logical size // (`MailboxSoA::set_populated` docs: "mirrors fixing BindSpace::len // at construction"). `phase()` itself is a mailbox-level field, not - // per-row, so this is not required for the phase assertions below — - // it is here so the probe's owner is representative of a real spawn - // rather than a zero-row empty shell. + // per-row — this keeps the probe's owner representative of a real + // spawn rather than a zero-row empty shell. mb.set_populated(1); mb } - #[tokio::test] - async fn w2b_real_owner_two_legal_advances_persist_on_the_real_soa() { - let mb = real_mailbox(); + #[test] + fn w2b_real_owner_two_legal_advances_persist_on_the_real_soa() { + let mut mb = real_mailbox(); assert_eq!( mb.phase(), KanbanColumn::Planning, - "MailboxSoA::new starts in Planning (mirrors TestBoard's board(Planning) helper \ - in kanban_actor.rs's own unit tests)" + "MailboxSoA::new starts in Planning" ); - let (actor, handle) = Actor::spawn(None, KanbanActor::::default(), mb) - .await - .expect("spawn kanban actor over the REAL MailboxSoA"); - - // Legal edge #1: Planning -> CognitiveWork, driven ONLY through the actor. - let mv1 = ractor::call!(actor, |reply| KanbanMsg::Advance { - to: KanbanColumn::CognitiveWork, - reply - }) - .expect("rpc") - .expect("Planning -> CognitiveWork is a legal Rubicon edge"); + // Legal edge #1: Planning -> CognitiveWork, through the contract's + // owner trait on the exclusive borrow. + let mv1 = mb + .try_advance_phase(KanbanColumn::CognitiveWork) + .expect("Planning -> CognitiveWork is a legal Rubicon edge"); assert_eq!(mv1.from, KanbanColumn::Planning); assert_eq!(mv1.to, KanbanColumn::CognitiveWork); - - // Read back through MailboxSoaView::phase() (via KanbanMsg::Phase) — - // the real SoA row reflects the advance. - let phase1 = ractor::call!(actor, |reply| KanbanMsg::Phase { reply }).expect("rpc"); - assert_eq!(phase1, KanbanColumn::CognitiveWork); + assert_eq!(mb.phase(), KanbanColumn::CognitiveWork); // Legal edge #2: CognitiveWork -> Evaluation. - let mv2 = ractor::call!(actor, |reply| KanbanMsg::Advance { - to: KanbanColumn::Evaluation, - reply - }) - .expect("rpc") - .expect("CognitiveWork -> Evaluation is a legal Rubicon edge"); + let mv2 = mb + .try_advance_phase(KanbanColumn::Evaluation) + .expect("CognitiveWork -> Evaluation is a legal Rubicon edge"); assert_eq!(mv2.from, KanbanColumn::CognitiveWork); assert_eq!(mv2.to, KanbanColumn::Evaluation); - - let phase2 = ractor::call!(actor, |reply| KanbanMsg::Phase { reply }).expect("rpc"); assert_eq!( - phase2, + mb.phase(), KanbanColumn::Evaluation, "the real MailboxSoA row reflects both advances, read back via MailboxSoaView" ); - - actor.stop(None); - handle.await.expect("actor join"); } - #[tokio::test] - async fn w2b_real_owner_illegal_edge_rejected_no_mutation_on_the_real_soa() { - let mb = real_mailbox(); - let (actor, handle) = Actor::spawn(None, KanbanActor::::default(), mb) - .await - .expect("spawn kanban actor over the REAL MailboxSoA"); + #[test] + fn w2b_real_owner_illegal_edge_rejected_no_mutation_on_the_real_soa() { + let mut mb = real_mailbox(); - // Planning -> Commit is NOT a legal Rubicon edge (same DAG the - // in-file TestBoard tests exercise) — must surface the typed - // RubiconTransitionError from MailboxSoaOwner::try_advance_phase, - // relayed through the actor's Advance message, with NO mutation on - // the real row. - let err = ractor::call!(actor, |reply| KanbanMsg::Advance { - to: KanbanColumn::Commit, - reply - }) - .expect("rpc") - .expect_err("Planning -> Commit must be rejected by the real owner's lifecycle DAG"); + // Planning -> Commit is NOT a legal Rubicon edge — the typed + // RubiconTransitionError surfaces from the real owner's lifecycle + // DAG, with NO mutation on the real row. + let err = mb + .try_advance_phase(KanbanColumn::Commit) + .expect_err("Planning -> Commit must be rejected by the real owner's lifecycle DAG"); assert_eq!(err.from, KanbanColumn::Planning); assert_eq!(err.to, KanbanColumn::Commit); - - // The real SoA's phase column is UNCHANGED after the rejected edge. - let phase = ractor::call!(actor, |reply| KanbanMsg::Phase { reply }).expect("rpc"); assert_eq!( - phase, + mb.phase(), KanbanColumn::Planning, "rejected transition must not mutate the real MailboxSoA row" ); - - actor.stop(None); - handle.await.expect("actor join"); } - #[tokio::test] - async fn w2b_real_owner_actor_is_the_sole_mutator_structural_check() { - // Structural proof (mailbox-as-owner, E-CE64-MB-4): the probe never - // calls `MailboxSoaOwner::advance_phase` / `try_advance_phase` - // directly on a `MailboxSoA` value it holds after spawn — the real - // `MailboxSoA` is MOVED into `Actor::spawn` (ownership transfer), - // and the only handle this test touches from that point on is the - // `ActorRef`. Any mutation not routed through - // `KanbanMsg::Advance` would be a compile error here (`mb` is no - // longer in scope), not a runtime bug this test could silently miss. - let mb = real_mailbox(); - let (actor, handle) = Actor::spawn(None, KanbanActor::::default(), mb) - .await - .expect("spawn kanban actor over the REAL MailboxSoA"); - // `mb` was moved into `Actor::spawn` above and is not usable here — - // the only remaining handle to the owner is `actor`. - - let mv = ractor::call!(actor, |reply| KanbanMsg::Advance { - to: KanbanColumn::CognitiveWork, - reply - }) - .expect("rpc") - .expect("legal edge applied via the actor, the only mutation surface reachable here"); - assert_eq!(mv.to, KanbanColumn::CognitiveWork); - - actor.stop(None); - handle.await.expect("actor join"); + #[test] + fn w2b_phase_census_observes_the_real_soa_without_mutating_it() { + // The replacement visibility surface over REAL owners: a mixed pair + // of mailboxes is counted correctly (can-fire), and driving both to + // absorbing columns flips at_rest (can-stay-silent) — all through + // `&self` reads, no message, no RPC. + let mut a = real_mailbox(); + let mut b = real_mailbox(); + a.try_advance_phase(KanbanColumn::CognitiveWork) + .expect("legal edge"); + + let mid = PhaseCensus::observe([&a, &b]); + assert_eq!(mid.total(), 2); + assert_eq!(mid.count(KanbanColumn::CognitiveWork), 1); + assert_eq!(mid.count(KanbanColumn::Planning), 1); + assert!(!mid.at_rest(), "a mid-arc fleet is not at rest"); + + // Drive both along legal arcs into absorbing columns. + a.try_advance_phase(KanbanColumn::Evaluation) + .expect("legal edge"); + a.try_advance_phase(KanbanColumn::Commit) + .expect("legal edge"); + b.try_advance_phase(KanbanColumn::Prune) + .expect("legal edge"); + + let done = PhaseCensus::observe([&a, &b]); + assert_eq!(done.absorbing(), 2); + assert!(done.at_rest(), "a fully absorbed fleet reads at rest"); + // Observation mutated nothing: phases are exactly where the owner + // left them. + assert_eq!(a.phase(), KanbanColumn::Commit); + assert_eq!(b.phase(), KanbanColumn::Prune); } } diff --git a/crates/onebrc-probe/Cargo.lock b/crates/onebrc-probe/Cargo.lock index 4c0234ec7..c465d3715 100644 --- a/crates/onebrc-probe/Cargo.lock +++ b/crates/onebrc-probe/Cargo.lock @@ -2,29 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "ahash" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" -dependencies = [ - "cfg-if", - "const-random", - "getrandom 0.3.4", - "once_cell", - "version_check", - "zerocopy", -] - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - [[package]] name = "arrayref" version = "0.3.9" @@ -37,89 +14,12 @@ version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" -[[package]] -name = "arrow-array" -version = "58.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfd33d3e92f207444098c75b42de99d329562be0cf686b307b097cc52b4e999e" -dependencies = [ - "ahash", - "arrow-buffer", - "arrow-data", - "arrow-schema", - "chrono", - "half", - "hashbrown 0.17.1", - "num-complex", - "num-integer", - "num-traits", -] - -[[package]] -name = "arrow-buffer" -version = "58.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c6cd424c2693bcdbc150d843dc9d4d137dd2de4782ce6df491ad11a3a0416c0" -dependencies = [ - "bytes", - "half", - "num-bigint", - "num-traits", -] - -[[package]] -name = "arrow-data" -version = "58.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c88210023a2bfee1896af366309a3028fc3bcbd6515fa29a7990ee1baa08ee0" -dependencies = [ - "arrow-buffer", - "arrow-schema", - "half", - "num-integer", - "num-traits", -] - -[[package]] -name = "arrow-schema" -version = "58.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f633dbfdf39c039ada1bf9e34c694816eb71fbb7dc78f613993b7245e078a1ed" - -[[package]] -name = "async-trait" -version = "0.1.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "autocfg" version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" -[[package]] -name = "bgz-tensor" -version = "0.1.0" -dependencies = [ - "holograph", - "ndarray", -] - -[[package]] -name = "bincode" -version = "1.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" -dependencies = [ - "serde", -] - [[package]] name = "bitflags" version = "2.13.0" @@ -137,16 +37,7 @@ dependencies = [ "cc", "cfg-if", "constant_time_eq", - "cpufeatures 0.3.0", -] - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", + "cpufeatures", ] [[package]] @@ -180,16 +71,6 @@ version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" -[[package]] -name = "bytes" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" - -[[package]] -name = "causal-edge" -version = "0.2.0" - [[package]] name = "cc" version = "1.2.65" @@ -206,58 +87,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" -[[package]] -name = "chrono" -version = "0.4.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" -dependencies = [ - "iana-time-zone", - "num-traits", - "windows-link", -] - -[[package]] -name = "const-random" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" -dependencies = [ - "const-random-macro", -] - -[[package]] -name = "const-random-macro" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" -dependencies = [ - "getrandom 0.2.17", - "once_cell", - "tiny-keccak", -] - [[package]] name = "constant_time_eq" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - [[package]] name = "cpufeatures" version = "0.3.0" @@ -273,22 +108,6 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" -[[package]] -name = "crunchy" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" - -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "typenum", -] - [[package]] name = "darling" version = "0.23.0" @@ -337,16 +156,6 @@ dependencies = [ "parking_lot_core", ] -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", -] - [[package]] name = "equivalent" version = "1.0.2" @@ -359,13 +168,6 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" -[[package]] -name = "fractal" -version = "0.1.0" -dependencies = [ - "libm", -] - [[package]] name = "futures" version = "0.3.32" @@ -454,57 +256,12 @@ dependencies = [ "slab", ] -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "libc", - "r-efi", - "wasip2", -] - [[package]] name = "glob" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" -[[package]] -name = "half" -version = "2.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" -dependencies = [ - "cfg-if", - "crunchy", - "num-traits", - "zerocopy", -] - [[package]] name = "hashbrown" version = "0.14.5" @@ -523,48 +280,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "highheelbgz" -version = "0.1.0" - -[[package]] -name = "holograph" -version = "0.1.0" -dependencies = [ - "arrow-array", - "arrow-buffer", - "arrow-schema", - "bincode", - "futures", - "log", - "serde", - "thiserror 2.0.18", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - [[package]] name = "ident_case" version = "1.0.1" @@ -598,20 +313,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "lance-graph-callcenter" -version = "0.1.0" -dependencies = [ - "async-trait", - "lance-graph-contract", - "lance-graph-ontology", - "lance-graph-rbac", - "log", - "syn", - "thinking-engine", - "thiserror 1.0.69", -] - [[package]] name = "lance-graph-contract" version = "0.1.0" @@ -621,53 +322,12 @@ dependencies = [ "serde_yaml", ] -[[package]] -name = "lance-graph-ontology" -version = "0.1.0" -dependencies = [ - "lance-graph-contract", - "once_cell", - "oxrdf", - "oxrdfxml", - "oxttl", - "quick-xml", - "sha2", - "thiserror 2.0.18", - "toml", -] - -[[package]] -name = "lance-graph-rbac" -version = "0.1.0" -dependencies = [ - "lance-graph-contract", -] - -[[package]] -name = "lance-graph-supervisor" -version = "0.1.0" -dependencies = [ - "lance-graph-callcenter", - "lance-graph-contract", - "ractor", - "static_assertions", - "thiserror 1.0.69", - "tokio", - "tracing", -] - [[package]] name = "libc" version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" -[[package]] -name = "libm" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" - [[package]] name = "lock_api" version = "0.4.14" @@ -677,12 +337,6 @@ dependencies = [ "scopeguard", ] -[[package]] -name = "log" -version = "0.4.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" - [[package]] name = "matrixmultiply" version = "0.3.10" @@ -704,28 +358,16 @@ name = "ndarray" version = "0.17.2" dependencies = [ "blake3", - "fractal", "matrixmultiply", "num-complex", "num-integer", "num-traits", - "p64", "paste", "portable-atomic", "portable-atomic-util", "rawpointer", ] -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", -] - [[package]] name = "num-complex" version = "0.4.6" @@ -751,7 +393,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", - "libm", ] [[package]] @@ -765,72 +406,11 @@ name = "onebrc-probe" version = "0.1.0" dependencies = [ "lance-graph-contract", - "lance-graph-supervisor", "ndarray", "ractor", "tokio", ] -[[package]] -name = "oxilangtag" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d3b4eb570abd4a1dcb062c31fd37b832264d9dc7292c3e69acfe926c87b063f" -dependencies = [ - "serde", -] - -[[package]] -name = "oxiri" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54b4ed3a7192fa19f5f48f99871f2755047fabefd7f222f12a1df1773796a102" - -[[package]] -name = "oxrdf" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0afd5c28e4a399c57ee2bc3accd40c7b671fdc7b6537499f14e95b265af7d7e0" -dependencies = [ - "oxilangtag", - "oxiri", - "rand", - "thiserror 2.0.18", -] - -[[package]] -name = "oxrdfxml" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd5516ae083d09bc57ec65ed5ee97701481725de6ffaa83d968ab42a96157ba1" -dependencies = [ - "oxilangtag", - "oxiri", - "oxrdf", - "quick-xml", - "thiserror 2.0.18", -] - -[[package]] -name = "oxttl" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f03fd471bd54c23d76631c0a2677aa4bb308d905f6e491ee35dcb0732b7c5c6c" -dependencies = [ - "memchr", - "oxilangtag", - "oxiri", - "oxrdf", - "thiserror 2.0.18", -] - -[[package]] -name = "p64" -version = "0.1.0" -dependencies = [ - "fractal", -] - [[package]] name = "parking_lot_core" version = "0.9.12" @@ -871,15 +451,6 @@ dependencies = [ "portable-atomic", ] -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - [[package]] name = "prettyplease" version = "0.2.37" @@ -899,15 +470,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "quick-xml" -version = "0.37.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" -dependencies = [ - "memchr", -] - [[package]] name = "quote" version = "1.0.46" @@ -917,12 +479,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - [[package]] name = "ractor" version = "0.15.13" @@ -942,35 +498,6 @@ dependencies = [ "web-time", ] -[[package]] -name = "rand" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" -dependencies = [ - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" -dependencies = [ - "getrandom 0.3.4", -] - [[package]] name = "rawpointer" version = "0.2.1" @@ -1034,28 +561,6 @@ dependencies = [ "syn", ] -[[package]] -name = "serde_json" -version = "1.0.150" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "serde_spanned" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" -dependencies = [ - "serde", -] - [[package]] name = "serde_yaml" version = "0.9.34+deprecated" @@ -1069,17 +574,6 @@ dependencies = [ "unsafe-libyaml", ] -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "digest", -] - [[package]] name = "shlex" version = "2.0.1" @@ -1098,12 +592,6 @@ version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - [[package]] name = "strsim" version = "0.11.1" @@ -1142,68 +630,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "thinking-engine" -version = "0.1.0" -dependencies = [ - "bgz-tensor", - "causal-edge", - "highheelbgz", - "lance-graph-contract", - "ndarray", - "serde", - "serde_json", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" -dependencies = [ - "thiserror-impl 2.0.18", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tiny-keccak" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" -dependencies = [ - "crunchy", -] - [[package]] name = "tokio" version = "1.52.3" @@ -1250,40 +676,6 @@ dependencies = [ "syn", ] -[[package]] -name = "toml" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit", -] - -[[package]] -name = "toml_datetime" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_edit" -version = "0.22.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" -dependencies = [ - "indexmap", - "serde", - "serde_spanned", - "toml_datetime", - "winnow", -] - [[package]] name = "tracing" version = "0.1.44" @@ -1315,12 +707,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "typenum" -version = "1.20.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" - [[package]] name = "unicode-ident" version = "1.0.24" @@ -1333,27 +719,6 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasip2" -version = "1.0.4+wasi-0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" -dependencies = [ - "wit-bindgen", -] - [[package]] name = "wasm-bindgen" version = "0.2.126" @@ -1429,102 +794,8 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - -[[package]] -name = "winnow" -version = "0.7.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" -dependencies = [ - "memchr", -] - -[[package]] -name = "wit-bindgen" -version = "0.57.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" - -[[package]] -name = "zerocopy" -version = "0.8.52" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.52" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zmij" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/crates/onebrc-probe/Cargo.toml b/crates/onebrc-probe/Cargo.toml index 6d4e5e243..5ad3e3269 100644 --- a/crates/onebrc-probe/Cargo.toml +++ b/crates/onebrc-probe/Cargo.toml @@ -25,10 +25,12 @@ description = "1BRC (One Billion Row Challenge) substrate probe: measures the V3 # Lanes A/C stay dependency-free; B, D, E are opt-in. lane-b = ["dep:ndarray"] lane-d = ["dep:ractor", "dep:tokio"] -# Lane E (kanban-scheduled batches): reuses the lane-d ractor/tokio deps plus -# the V3 kanban execution machinery (lance-graph-supervisor's KanbanActor + -# drivers, lance-graph-contract's kanban/scheduler/soa_view contract types). -lane-e = ["dep:lance-graph-supervisor", "dep:lance-graph-contract", "dep:ractor", "dep:tokio"] +# Lane E (kanban-journaled batches): the contract's kanban/soa_view types + +# tokio for the puller tasks. The KanbanActor/driver surface this lane once +# drove was deleted 2026-08-05 (lance-graph +# E-PROGRESSION-IS-EXISTENCE-NOT-COMMAND-1) — the lane now journals over the +# direct exclusive owner, so supervisor + ractor dropped out of this feature. +lane-e = ["dep:lance-graph-contract", "dep:tokio"] # Lane G (kanban-update write path): the Morton-tile 64K SoA as owned state # behind shard mailbox actors, morsel casts witnessed with KanbanMoves — # ractor/tokio + the contract's kanban types (no supervisor needed: the @@ -64,11 +66,8 @@ ractor = { git = "https://github.com/AdaWorldAPI/ractor", optional = true, defau tokio = { version = "1", optional = true, default-features = false, features = ["rt-multi-thread", "macros"] } # Sibling in-tree crates (path deps, not the outer workspace — this crate is # workspace-excluded per the `[workspace]` table above). lance-graph-contract -# is zero-dep; lance-graph-supervisor needs its own `supervisor` feature for -# the KanbanActor + drive_version_tick surface lane E drives. +# is zero-dep. (lance-graph-supervisor was a lane-e dep for the deleted +# KanbanActor surface — removed 2026-08-05 with that surface.) lance-graph-contract = { path = "../lance-graph-contract", optional = true } -lance-graph-supervisor = { path = "../lance-graph-supervisor", optional = true, features = [ - "supervisor", -] } [dev-dependencies] diff --git a/crates/onebrc-probe/src/lane_d.rs b/crates/onebrc-probe/src/lane_d.rs index d004fc8fe..8b88910c1 100644 --- a/crates/onebrc-probe/src/lane_d.rs +++ b/crates/onebrc-probe/src/lane_d.rs @@ -7,9 +7,11 @@ //! `merge_maps`) — the only variable this lane changes is the worker //! primitive: a `ractor` `Actor` instead of a raw OS thread. //! -//! Mirrors `lance-graph-supervisor`'s `KanbanActor` idioms (`kanban_actor.rs` -//! — `Actor::spawn`, the ask-pattern `ractor::call!`, `RpcReplyPort` in the -//! message variant) for a single, stateless worker actor. +//! Uses the standard ractor idioms (`Actor::spawn`, the ask-pattern +//! `ractor::call!`, `RpcReplyPort` in the message variant) for a single, +//! stateless worker actor. (These idioms were once mirrored from the +//! supervisor's `KanbanActor`, deleted 2026-08-05 — lane D keeps its own +//! actors deliberately: pricing the actor model is this lane's PURPOSE.) //! //! ## Actor-model boundary cost //! @@ -35,8 +37,8 @@ use ractor::{Actor, ActorProcessingErr, ActorRef, RpcReplyPort}; use std::collections::BTreeMap; use std::sync::Arc; -/// Messages `ChunkWorker` accepts — a single ask-pattern variant per the -/// `KanbanActor` idiom (`kanban_actor.rs`'s `RpcReplyPort`-in-variant shape). +/// Messages `ChunkWorker` accepts — a single ask-pattern variant +/// (`RpcReplyPort`-in-variant shape). pub enum ChunkMsg { /// Aggregate `data[start..end]` (newline-aligned, per `chunk_bounds`) /// via `lane_a_scalar` and reply with the owned per-chunk map. diff --git a/crates/onebrc-probe/src/lane_e.rs b/crates/onebrc-probe/src/lane_e.rs index afdf3e5e1..f5eb343f6 100644 --- a/crates/onebrc-probe/src/lane_e.rs +++ b/crates/onebrc-probe/src/lane_e.rs @@ -1,64 +1,55 @@ -//! Lane E — kanban-scheduled batches. +//! Lane E — kanban-journaled batches over the direct exclusive owner. //! //! Per Addendum-13 lane E (see `README.md` §3), this lane measures the V3 -//! kanban scheduling/journaling tax on top of the SAME groupby-aggregate -//! workload lanes A/C/D already measure. The corpus is split into `batches` +//! kanban **journaling** tax on top of the SAME groupby-aggregate workload +//! lanes A/C/D already measure. The corpus is split into `batches` //! newline-aligned chunks (`batches >= workers`, `chunk_bounds`), pulled by //! `workers` puller tasks from a shared lock-free queue (`AtomicUsize` //! index into the batch list), and EVERY batch is journaled as one kanban -//! card: a fresh [`KanbanActor`] (from `lance-graph-supervisor`, feature -//! `supervisor`) whose owned [`ProbeBoard`] is driven through the full +//! card: a fresh [`ProbeBoard`] held `&mut` and driven through the full //! Rubicon **forward arc** (`Planning -> CognitiveWork -> Evaluation -> //! Commit`) around the actual per-batch work //! ([`crate::lane_a_scalar`](super::lane_a_scalar)). //! -//! Two readings this lane is built to support: +//! ## 2026-08-05 migration — the actor variant is retired with the message path //! -//! - **E at `batches == workers`** vs Lane D: identical `chunk_bounds` -//! split, identical `Arc>` corpus-copy tax (see `lane_d.rs` -//! module doc "Actor-model boundary cost") — the only variable is -//! swapping Lane D's stateless `ChunkWorker` ask-pattern actor for a -//! `KanbanActor` driven through 3 Rubicon ticks per batch. -//! E-D isolates the **journaling cost** in isolation from the actor-model -//! tax Lane D already prices. -//! - **E at fine granularity** (`batches >> workers`, e.g. -//! `batches = workers * 16`): each puller spawns, ticks 3×, and stops -//! many short-lived actors instead of one long-lived one per worker — -//! prices the **per-card scheduling overhead** the V3 substrate pays when -//! work is journaled at kanban-card granularity rather than -//! worker-chunk granularity. This feeds W2d (the 550 ms Libet budget -//! question — how many kanban cards per wall-clock second the substrate -//! can actually journal). +//! This lane originally spawned a `KanbanActor` per batch and drove it through +//! `KanbanMsg::Tick` RPCs — it was the last library consumer of that surface, +//! and its E−D reading existed to isolate journaling cost from the actor-model +//! tax lane D prices. The actor/tick surface was DELETED +//! (`E-PROGRESSION-IS-EXISTENCE-NOT-COMMAND-1`: a version tick is knowledge, +//! never permission to advance; `&mut` IS the serialization). What lane E +//! prices now is the journaling itself — `KanbanMove` minting + collection at +//! kanban-card granularity over the direct exclusive owner, zero message +//! overhead. Lane D still prices the actor model on its own; the old E−D +//! "journaling minus actor tax" subtraction is retired with the actors. +//! This still feeds W2d (the 550 ms Libet budget question — how many kanban +//! cards per wall-clock second the substrate can journal). //! //! ## Journal invariant //! //! Each batch drives exactly 3 [`KanbanMove`]s (`Planning->CognitiveWork`, //! `CognitiveWork->Evaluation`, `Evaluation->Commit` — the pure forward arc -//! to the absorbing `Commit` column, mirroring `kanban_actor.rs`'s -//! `run_to_absorbing` test). Every worker collects its own moves into a -//! local `Vec`; at the end of [`lane_e_kanban`] the combined -//! journal is asserted to have exactly `3 * batches` moves, and every move -//! is asserted legal via [`KanbanColumn::can_transition_to`] — a violated -//! assert here is a probe bug, not a measurement. +//! to the absorbing `Commit` column). Every worker collects its own moves +//! into a local `Vec`; at the end of [`lane_e_kanban`] the +//! combined journal is asserted to have exactly `3 * batches` moves, and +//! every move is asserted legal via [`KanbanColumn::can_transition_to`] — a +//! violated assert here is a probe bug, not a measurement. use crate::{chunk_bounds, lane_a_scalar, merge_maps, Stats}; use lance_graph_contract::collapse_gate::MailboxId; use lance_graph_contract::kanban::{ExecTarget, KanbanColumn, KanbanMove}; -use lance_graph_contract::scheduler::DatasetVersion; use lance_graph_contract::soa_view::{MailboxSoaOwner, MailboxSoaView}; -use lance_graph_supervisor::{drive_version_tick, KanbanActor}; -use ractor::Actor; use std::collections::BTreeMap; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; -/// The probe's stand-in kanban-owned board — mirrors the shape of -/// `lance-graph-supervisor`'s own `TestBoard` (`kanban_actor.rs`'s test -/// module): a minimal in-RAM [`MailboxSoaView`] + [`MailboxSoaOwner`] with -/// empty column slices (`n_rows() == 0`, no energy/edges/meta/entity_type -/// data). This lane measures the KANBAN JOURNALING overhead only, not SoA -/// storage — a real SoA board wired to actual rows is lane F's business -/// (Morton-tile cascaded shader, per README §5.1's closing note). +/// The probe's stand-in kanban board — a minimal in-RAM [`MailboxSoaView`] + +/// [`MailboxSoaOwner`] with empty column slices (`n_rows() == 0`, no +/// energy/edges/meta/entity_type data). This lane measures the KANBAN +/// JOURNALING overhead only, not SoA storage — a real SoA board wired to +/// actual rows is lane F's business (Morton-tile cascaded shader, per README +/// §5.1's closing note). struct ProbeBoard { id: MailboxId, phase: KanbanColumn, @@ -75,6 +66,17 @@ impl ProbeBoard { cycle: 0, } } + + /// Advance one step along the Rubicon forward arc + /// (`phase().next_phases().first()`), or `None` at an absorbing column. + /// A plain `&mut` method — the exclusive borrow is the single-writer + /// guarantee; no message, no RPC, no scheduler. + fn forward_tick(&mut self) -> Option { + self.phase + .next_phases() + .first() + .map(|&to| self.advance_phase(to)) + } } impl MailboxSoaView for ProbeBoard { @@ -86,8 +88,8 @@ impl MailboxSoaView for ProbeBoard { } fn w_slot(&self) -> u8 { // `id` here is a probe-local kanban-card counter, not a composed - // classid — this is the same bit-op `TestBoard::w_slot` uses over - // `MailboxId` (a plain `u32`), not classid discrimination. + // classid — this is a plain bit-op over `MailboxId` (a plain `u32`), + // not classid discrimination. (self.id & 0x3F) as u8 } fn current_cycle(&self) -> u32 { @@ -125,9 +127,8 @@ impl MailboxSoaOwner for ProbeBoard { } } -/// Lane E — kanban-scheduled batches. See module doc for the full design -/// and the two readings (E vs D at `batches == workers`; E at fine -/// granularity for per-card scheduling cost). +/// Lane E — kanban-journaled batches. See module doc for the design and the +/// 2026-08-05 migration off the actor surface. /// /// `batches` is clamped to `>= workers.max(1)` — a batch queue thinner than /// the worker pool would leave pullers idle and defeat the point of the @@ -143,8 +144,8 @@ pub fn lane_e_kanban(data: &[u8], workers: usize, batches: usize) -> BTreeMap BTreeMap::default(), - ProbeBoard::new(idx as MailboxId), - ) - .await - .expect("spawn lane E kanban actor"); - - // Tick 1: Planning -> CognitiveWork. - let mv1 = drive_version_tick(&actor, DatasetVersion(1)) - .await - .expect("lane E tick 1 rpc") + // One kanban card per batch: a fresh exclusively-owned + // board starting at Planning. `&mut` is the single-writer + // guarantee — no actor, no message loop. + let mut board = ProbeBoard::new(idx as MailboxId); + + // Step 1: Planning -> CognitiveWork. + let mv1 = board + .forward_tick() .expect("Planning -> CognitiveWork must advance"); journal.push(mv1); @@ -188,14 +183,13 @@ pub fn lane_e_kanban(data: &[u8], workers: usize, batches: usize) -> BTreeMap Evaluation. Merge the batch's + // Step 2: CognitiveWork -> Evaluation. Merge the batch's // map into the worker-local accumulator here — mirrors // the commutative BUNDLE step `merge_maps` uses, applied // per-batch instead of per-worker (see `Stats::merge` // struct-level doc). - let mv2 = drive_version_tick(&actor, DatasetVersion(2)) - .await - .expect("lane E tick 2 rpc") + let mv2 = board + .forward_tick() .expect("CognitiveWork -> Evaluation must advance"); journal.push(mv2); for (name, stats) in batch_map { @@ -207,15 +201,15 @@ pub fn lane_e_kanban(data: &[u8], workers: usize, batches: usize) -> BTreeMap Commit (absorbing). - let mv3 = drive_version_tick(&actor, DatasetVersion(3)) - .await - .expect("lane E tick 3 rpc") + // Step 3: Evaluation -> Commit (absorbing). + let mv3 = board + .forward_tick() .expect("Evaluation -> Commit must advance"); journal.push(mv3); - - actor.stop(None); - handle.await.expect("lane E actor join"); + debug_assert!( + board.forward_tick().is_none(), + "Commit is absorbing — a fourth forward tick must yield nothing" + ); } (local_map, journal) diff --git a/crates/onebrc-probe/src/lane_g.rs b/crates/onebrc-probe/src/lane_g.rs index 07df6cd0f..c9d68fb15 100644 --- a/crates/onebrc-probe/src/lane_g.rs +++ b/crates/onebrc-probe/src/lane_g.rs @@ -37,8 +37,8 @@ //! prefix route. A station's hash always lands with the same owner, so //! the owners' SoAs are disjoint by construction. //! - **Mailbox-as-owner:** the serialized message loop is the single -//! writer of the owner's SoA (the same compile-time no-aliasing -//! argument as `KanbanActor`, E-CE64-MB-4). No lock, no shared `&mut`. +//! writer of the owner's SoA (the E-CE64-MB-4 compile-time no-aliasing +//! argument: the SoA is MOVED into the actor). No lock, no shared `&mut`. //! - **Kanban update = witnessed write:** every applied morsel batch //! appends one `KanbanMove` (`CognitiveWork → Evaluation`, a legal //! Rubicon forward edge) to the owner's journal — recorded directly to diff --git a/crates/onebrc-probe/src/lane_s.rs b/crates/onebrc-probe/src/lane_s.rs index 9c4cc2b1c..56a736a82 100644 --- a/crates/onebrc-probe/src/lane_s.rs +++ b/crates/onebrc-probe/src/lane_s.rs @@ -157,7 +157,10 @@ mod tests { let a = crate::lane_a_scalar(&data); let s = lane_s_swar(&data, 3); - assert_eq!(a, s, "SWAR lane must produce identical aggregates to lane A"); + assert_eq!( + a, s, + "SWAR lane must produce identical aggregates to lane A" + ); assert!(!a.is_empty()); } } diff --git a/crates/onebrc-probe/src/lane_t.rs b/crates/onebrc-probe/src/lane_t.rs index 375245e2e..6bf6da4f5 100644 --- a/crates/onebrc-probe/src/lane_t.rs +++ b/crates/onebrc-probe/src/lane_t.rs @@ -70,7 +70,7 @@ impl Trie { child as usize } else { let new = self.counts.len(); - self.children.extend(std::iter::repeat(0u32).take(self.fanout)); + self.children.extend(std::iter::repeat_n(0u32, self.fanout)); self.mins.push(i32::MAX); self.maxs.push(i32::MIN); self.sums.push(0); @@ -167,7 +167,12 @@ fn accumulate_trie(data: &[u8], fanout: usize, nibble: bool) -> Trie { trie } -fn lane_trie_threads(data: &[u8], workers: usize, fanout: usize, nibble: bool) -> BTreeMap { +fn lane_trie_threads( + data: &[u8], + workers: usize, + fanout: usize, + nibble: bool, +) -> BTreeMap { let workers = workers.max(1); let bounds = chunk_bounds(data, workers); let results: Vec> = std::thread::scope(|scope| { @@ -214,15 +219,33 @@ mod tests { assert_eq!(map.len(), 3, "three stations (nibble={nibble})"); assert_eq!( map["ab"], - Stats { min: 10, max: 30, sum: 40, count: 2 }, + Stats { + min: 10, + max: 30, + sum: 40, + count: 2 + }, "nibble={nibble}" ); assert_eq!( map["abc"], - Stats { min: -40, max: 20, sum: -20, count: 2 }, + Stats { + min: -40, + max: 20, + sum: -20, + count: 2 + }, "nibble={nibble}" ); - assert_eq!(map["z"], Stats { min: 5, max: 5, sum: 5, count: 1 }); + assert_eq!( + map["z"], + Stats { + min: 5, + max: 5, + sum: 5, + count: 1 + } + ); } } diff --git a/crates/onebrc-probe/src/lib.rs b/crates/onebrc-probe/src/lib.rs index 390290e66..e209eeace 100644 --- a/crates/onebrc-probe/src/lib.rs +++ b/crates/onebrc-probe/src/lib.rs @@ -14,12 +14,13 @@ //! vectorized `;`/`\n` scan, scalar parse. //! - **Lane D** (`lane_d::lane_d_ractor`, feature `lane-d`) — `ractor` //! actor-per-worker over the same `chunk_bounds` split as Lane C. -//! - **Lane E** (`lane_e::lane_e_kanban`, feature `lane-e`) — kanban-scheduled -//! batches: a shared `AtomicUsize` batch queue, one fresh `KanbanActor` per -//! batch driven through the full Rubicon forward arc +//! - **Lane E** (`lane_e::lane_e_kanban`, feature `lane-e`) — kanban-journaled +//! batches: a shared `AtomicUsize` batch queue, one fresh exclusively-owned +//! board per batch driven `&mut` through the full Rubicon forward arc //! (Planning->CognitiveWork->Evaluation->Commit) around the actual work. -//! Measures the V3 kanban scheduling/journaling tax (E-D isolates the -//! journaling cost; fine-grained batching prices per-card scheduling). +//! Measures the V3 kanban journaling tax at card granularity. (The lane's +//! original actor-per-batch variant retired 2026-08-05 with the deleted +//! KanbanActor surface — see `lane_e.rs` module doc.) //! - **Lanes F/R** (`lane_f::{lane_f_morton, lane_r_radix}`, std-only, no //! feature) — the substrate-native lane and its honest control: station //! identity → Morton tile address → SoA-shaped flat accumulators (F); @@ -64,11 +65,11 @@ pub mod lane_h; pub mod lane_i; #[cfg(feature = "lane-j")] pub mod lane_j; +pub mod lane_s; +pub mod lane_t; #[cfg(feature = "presets")] pub mod presets; pub mod sha256; -pub mod lane_s; -pub mod lane_t; #[cfg(feature = "lane-b")] pub use lane_b::lane_b_simd; @@ -77,8 +78,6 @@ pub use lane_d::lane_d_ractor; #[cfg(feature = "lane-e")] pub use lane_e::lane_e_kanban; pub use lane_f::{lane_f_morton, lane_r_radix}; -pub use lane_s::lane_s_swar; -pub use lane_t::{lane_t_byte, lane_t_trie}; #[cfg(feature = "lane-g")] pub use lane_g::{lane_g_kanban_soa, lane_g_kanban_soa_with_morsel}; #[cfg(feature = "lane-h")] @@ -87,6 +86,8 @@ pub use lane_h::{lane_h_orchestrated, lane_h_orchestrated_with}; pub use lane_i::{lane_i_batch_pipeline, lane_i_batch_pipeline_with}; #[cfg(feature = "lane-j")] pub use lane_j::{lane_j_grid_pipeline, lane_j_grid_pipeline_with}; +pub use lane_s::lane_s_swar; +pub use lane_t::{lane_t_byte, lane_t_trie}; use std::collections::BTreeMap; diff --git a/crates/onebrc-probe/src/main.rs b/crates/onebrc-probe/src/main.rs index a4cc19f95..b2e30b1de 100644 --- a/crates/onebrc-probe/src/main.rs +++ b/crates/onebrc-probe/src/main.rs @@ -203,7 +203,9 @@ fn cmd_run(args: &[String]) { } } other => { - eprintln!("unknown lane '{other}' (expected 'a', 'b', 'c', 'd', 'e', 'f', 'r', or 's')"); + eprintln!( + "unknown lane '{other}' (expected 'a', 'b', 'c', 'd', 'e', 'f', 'r', or 's')" + ); std::process::exit(2); } };