diff --git a/.claude/board/LATEST_STATE.md b/.claude/board/LATEST_STATE.md index 34bb6f158..91b933840 100644 --- a/.claude/board/LATEST_STATE.md +++ b/.claude/board/LATEST_STATE.md @@ -1,3 +1,19 @@ +## 2026-09-06 — #1198 MERGED (3797237b): the temporal audit, three probes, one falsified hypothesis + +| PR | merge | content | +|---|---|---| +| **#1198** | `3797237b` | `.claude/temporal/` (10 docs) + two probes (D-LNC-5a delta version columns, D-MW-P2 MemWAL crash-atomicity) + `uuid` as a dev-dep. No runtime code. | + +**Contract inventory net delta: none.** Nothing was minted — this arc is an +audit, a staged plan, and three measurements. What it changes is what a later +session is allowed to assume: the migration hypothesis is FALSIFIED (the alpha +/ rung / SPOG wiring was never present to be lost — near every mechanism is +built and connected to nothing, `TemporalPov` excepted), delta version columns +are RED without `enable_stable_row_ids` (P1), MemWAL is crash-atomic across a +5–500 ms SIGKILL sweep (P2), and ternlog chaining pays ~2× only while +L2-resident (P3). Full per-probe numbers and the four self-corrections are in +`PR_ARC_INVENTORY.md`; the durable reference is `.claude/temporal/`. + ## 2026-09-05 — #1194 + #1195 MERGED (3c84ce16, 8c7aa961): Rust 1.98.1, and the sweep it opens | PR | merge | content | diff --git a/.claude/board/PR_ARC_INVENTORY.md b/.claude/board/PR_ARC_INVENTORY.md index d4d7e3888..a7b00e3b8 100644 --- a/.claude/board/PR_ARC_INVENTORY.md +++ b/.claude/board/PR_ARC_INVENTORY.md @@ -1,3 +1,14 @@ +## 2026-09-06 — lance-graph PR #1198 (merged `3797237b`, branch `claude/great-curie-d2ufyl`) — the temporal audit, three probes, and a hypothesis falsified + +- **Added:** `.claude/temporal/` (10 files: README + delta API + addressing regimes + alpha channel state + reusable patterns + deletion/tombstones + callers/dormancy + deepnsm v1-vs-v2 + memwal-vs-batchwriter + the staged plan); two probes — `crates/lance-graph/tests/delta_version_columns_probe.rs` (D-LNC-5a) and `crates/lance-graph/tests/memwal_atomicity_probe.rs` (D-MW-P2); `uuid` as a lance-graph **dev-dependency** only (to make the `Uuid` type nameable in a test, not a new crate in the graph). No runtime code, no contract types, no layout change. +- **Headline — the hypothesis was FALSIFIED, and that is the finding.** The operator asked whether MedCare's cognition architecture already held the activation/alpha/meta-awareness semantics and whether migration into lance-graph kept the pieces while losing the wiring. It did not lose the wiring: **the wiring was never there.** `SpogTenants`, `AlphaTunnel`, `rung_horizon`, `AlphaMask`, `residue_band`, ndarray `ternlog`, surrealdb kv-lance `Timeline` and `batch_writer::cast` are each built, tested, and connected to nothing. `TemporalPov` is the one live exception. `seed.clone()` ruled **#2 INTENTIONAL** at high confidence — `git log --all -S"&mut seed"` is empty across both repos' entire history, so there is no lost mutation to restore. +- **Measured, three probes, each with a PRE-REGISTERED control so a null is attributable to lance rather than to the apparatus:** **P1/D-LNC-5a RED both arms** — `_row_created_at_version` / `_row_last_updated_at_version` are unpopulated without `enable_stable_row_ids` (insert: control 2, physical 0; update: control held, stable 1, physical 0), so D-LNC-5's insert/update half is gated behind the stable-row-id decision exactly as its delete arm already was. **P2/D-MW-P2 GREEN** — SIGKILL swept 5–500 ms across a 5 000-row put gives `0,0,0,5000,5000,5000,5000`, the boundary bracketed on both sides, no partial batch; the FOLD-onto-MemWAL verdict stands. **P3/D-GTM-0n GREEN but bounded** — chained ternlog is ~2× pairwise `and` (`T3/T1` → 0.50) with total cost flat in K, the K=1 control reads 1.03 as it must, and the win is **L2-residency-contingent** (bandwidth `138 → 15 GB/s` past L2) with mask losing to sparse below 0.1% active. +- **Corrected — four of them against this session's OWN earlier claims, which is the half worth keeping:** the deepnsm 63.3% cross-window figure is an unasserted `println!`, i.e. claimed-unverified; deepnsm-v2 does **not** ENFORCE the no-hindsight property (`window_range` bypasses the automatic bound); the "descriptor, never owned delta bytes" rule is doc-only (`SweepSlot::payload` is a `Vec`); and `LanceCycleWriter::open` has **zero callers repo-wide**, so the sparse-delta invariant it ratifies guards nothing yet. +- **Locked — the review round found a real defect the suite could not.** CodeRabbit asked how the update arm's zero was distinguishable from "no update was committed". It was not: `MergeInsertBuilder::when_matched` defaults to `WhenMatched::DoNothing`, so the merge had been a silent no-op on an existing key. The probe compiled, ran, and reported a plausible zero; fmt, clippy and the test runner were all structurally incapable of catching it. Adding the control failed immediately and turned an INCONCLUSIVE label into a conclusive RED. +- **Deferred, deliberately:** Stage 1 (compact alpha resident form), Stage 1b (the release-mode `AlphaMask::zip` guard — a latent defect, not a feature; **now fixed in the successor PR**), Stage 2 (rung × tenant mask cross), Stage 3 (blocked on P1's red), Stage 4 (unblocked by P2). Semantic delete-awareness is also absent — `GraphDiff` has no removed-nodes field and `witness_tombstone.rs` is all `todo!()`. +- **Docs:** tombstone → `get_deleted_row_ids` marked **PARTIAL ONLY** (it returns row ids, never `version`/`tombstone`/`seq`, and needs stable row ids at both endpoints); the "six bytes recoverable" claim narrowed because `AlphaStamp::cycle` is caller-supplied with no cycle↔version mapping and nothing advances it. Method notes banked: lance-encoding needs `protoc` at build time, and `cargo … | tail` reports TAIL's exit status — a first probe run showed exit 0 while cargo had failed. +- **Confidence:** High on every existence/absence claim (each carries `file:line`) and on all three probe results (each pre-registered, each with a control that was verified to hold). Medium on the plan's staging ORDER past Stage 2, which depends on the stable-row-id decision this repo has not yet made. + ## 2026-09-05 — lance-graph PR #1195 (merged `8c7aa961`, branch `claude/rust-1981-bump`) — toolchain 1.97.1 → 1.98.1 - **Added:** `rust-toolchain.toml` channel → `1.98.1` + an append-only bump-log entry; `CLAUDE.md`'s `rust =` line. Nothing else — no workflow edits are needed because all eleven workflows read the toolchain file with no `toolchain:` input. diff --git a/.claude/board/STATUS_BOARD.md b/.claude/board/STATUS_BOARD.md index fa158a84b..0c69a1a06 100644 --- a/.claude/board/STATUS_BOARD.md +++ b/.claude/board/STATUS_BOARD.md @@ -19,6 +19,7 @@ identity) is real and probe-gated. | D-LNC-5a | **Delta version-columns probe** (pre-registered, `crates/lance-graph/tests/delta_version_columns_probe.rs`): are `_row_created_at_version` / `_row_last_updated_at_version` populated WITHOUT stable row ids? Stable-row-id arm is the CONTROL so a null is attributable to lance, not the apparatus | **In PR** #1198. **RED, both arms.** insert: control 2, physical **0**; update: control HELD (version advanced AND value changed), stable **1**, physical **0**. ⊘ the update arm's first run was INCONCLUSIVE — its control did not hold because `MergeInsertBuilder::when_matched` defaults to `DoNothing`, so no update ever committed (caught by a CodeRabbit finding on #1198). **Consequence: D-LNC-5's insert/update half is gated behind the stable-row-id decision, same as the delete arm** | | D-MW-P2 | **MemWAL crash-atomicity probe** (`crates/lance-graph/tests/memwal_atomicity_probe.rs`): the one measurement `.claude/temporal/08-memwal-vs-batchwriter.md` named as overturning its FOLD verdict — SIGKILL mid-flush on a 5 000-row put | **In PR** #1198. **GREEN.** A1 clean-drop control 5000; A2 SIGKILL sweep 5–500 ms → `0,0,0,5000,5000,5000,5000`. Boundary bracketed on both sides, no partial batch. FOLD stands. Scope recorded as NOT settled: one put, local store, no concurrent writers, no fencing path, and NOT the landing-rows-plus-frame-row composite (that is `LanceCycleWriter`'s, which has zero callers) | | D-GTM-0n / P3 | **Ternlog chaining amortization** (`ndarray/examples/ternlog_amortization_probe.rs`, re-run `--release` 2026-09-06): does chaining pay on THIS mask shape? Pre-registered pass criterion = per-constraint cost flat in K, with a **K=1 control** (nothing to chain ⇒ no win) | **GREEN, bounded.** `T3/T1` bottoms at **0.50** by K=8; T3 total flat in K (57.6 ns @ K=4 → 54.1 ns @ K=32, 16× more constraints). K=1 control reads **1.03** as it must. Two bounds ride with the number and may not be dropped when it is cited: the win is **L2-residency-contingent** (bandwidth `138→15 GB/s` past L2, ratio back to 1.03 at a 512 KiB mask), and mask loses to sparse below **0.1%** active. **Does NOT retire the standing caveat** — 2× on one of three passes still cannot account for 5×. Gates Stage-3 SPEED claims only; the shape/correctness case never depended on it | +| D-ALPHA-1b | **`AlphaMask::zip` release-mode length guard** (`lance-graph-contract/src/alpha.rs`): the only guard was `debug_assert_eq!`, compiled out in release, so mismatched-length operands produced a mask claiming `self.len` addresses over the SHORTER operand's word count — `count`/`is_empty` under-report silently, `contains`/`materialize_ordinals` panic at a distance | **Fixed** — promoted to `assert_eq!`. Defect reproduced in release BEFORE the fix (the `should_panic` falsifier reported "test did not panic as expected"); two-sided falsifiers both disable-verified. No API change, no external callers. Stage 1b of `.claude/temporal/09-plan.md`, now DONE | | D-LNC-7 | **One-WAL ruling**: `LanceCycleWriter`'s watermark WAL vs lance MemWAL — `applied_through` ≡ index catch-up position; two under one commit = two truths. Plan recommends keep-ours until D-LNC-3, then re-decide | **Operator decision** | ## bindspace-mailbox-soa-wiring-v1 (D-ids minted 2026-09-05 with the plan) diff --git a/.claude/temporal/09-plan.md b/.claude/temporal/09-plan.md index 834c48af5..45e8fc306 100644 --- a/.claude/temporal/09-plan.md +++ b/.claude/temporal/09-plan.md @@ -50,11 +50,29 @@ ordinals — the module's own no-unnamed-materializer law, applied consistently. **Kill condition:** if the parallel≡sequential falsifier cannot be kept green, stop and report; that guarantee outranks the byte saving. -## Stage 1b — fix the release-mode mask guard (independent, do it anyway) - -`AlphaMask::zip`'s only length guard is a `debug_assert_eq!` (`alpha.rs:273`), -compiled out in release. Make mismatched lengths fail closed. Small, safe, and -a precondition for any mask stacking. +## Stage 1b — fix the release-mode mask guard — **DONE 2026-09-06** + +`AlphaMask::zip`'s only length guard WAS a `debug_assert_eq!`, compiled out in +release. Now `assert_eq!`, so it holds in the build where the damage is silent. + +**The defect was measured before it was fixed, in release**, and it is worse +than a truncation: iterator `zip` truncates to the shorter operand while `len` +is copied from `self`, so `wide.and(&narrow)` returned a mask claiming +`self.len` addresses over `other.words.len()` words. That is an INVALID mask, +not a smaller one, and it fails two ways at a distance — `count`/`is_empty` +under-report **silently**, while `contains`/`materialize_ordinals` index past +the slice and panic far from the call that caused it. + +Two-sided falsifiers, both disable-verified: the mismatch case panicked only +after the fix (`should_panic`, and pre-fix the runner reported *"test did not +panic as expected"* — the defect, reproduced); the equal-length case proves +the guard stays silent on ordinary input, including `len % 64 != 0`, so a +guard that rejected everything could not pass both. + +A length mismatch is a caller mixing two allocations — a programming error, +not a data condition — so it fails closed at the operation that made it. Cost +is one `u32` compare against a loop over every word. No API change; the ops +have no external callers yet, so this had no ripple. ## Stage 2 — the rung × tenant mask cross (the meta-awareness layer) diff --git a/crates/lance-graph-contract/src/alpha.rs b/crates/lance-graph-contract/src/alpha.rs index c15deee60..467c27661 100644 --- a/crates/lance-graph-contract/src/alpha.rs +++ b/crates/lance-graph-contract/src/alpha.rs @@ -269,8 +269,24 @@ impl AlphaMask { self.len } + /// Word-wise combine. **`assert_eq!`, never `debug_assert_eq!`** — the + /// guard has to hold in release, because that is the build where the + /// damage is silent. + /// + /// Iterator `zip` truncates to the shorter operand while `len` is copied + /// from `self`, so a mismatch that gets past this line produces a mask + /// claiming `self.len` addresses over `other.words.len()` words. That is + /// not a smaller mask, it is an INVALID one, and it fails two ways at a + /// distance: `count`/`is_empty` under-report silently, and + /// `contains`/`materialize_ordinals` index past the slice and panic + /// somewhere far from the call that caused it. + /// + /// A length mismatch is a caller mixing two allocations — a programming + /// error, not a data condition — so it fails closed and immediately, + /// at the operation that made it, rather than becoming a wrong answer. + /// The cost is one `u32` comparison against a loop over every word. fn zip(&self, other: &Self, f: impl Fn(u64, u64) -> u64) -> Self { - debug_assert_eq!(self.len, other.len, "masks from different allocations"); + assert_eq!(self.len, other.len, "masks from different allocations"); Self { words: self .words @@ -746,6 +762,50 @@ mod tests { /// The complement stays INSIDE the allocation. A `not()` that forgets to /// clear the tail word raises up to 63 phantom bits past `len` — here 59 /// of them — and every downstream count/diff silently inflates. + /// The release-mode length guard. `zip` once guarded its operands with + /// `debug_assert_eq!`, which is COMPILED OUT of a release build — so a + /// mismatch reached the iterator `zip`, which truncates to the shorter + /// operand while `len` was copied from `self`. The product claimed + /// `self.len` addresses over `other.words.len()` words, which is not a + /// smaller mask but an INVALID one, and it fails in two different ways + /// depending on which reading runs first: `count`/`is_empty` silently + /// under-report, and `contains`/`materialize_ordinals` index past the + /// slice and panic somewhere far from the call that caused it. + /// + /// Two-sided on purpose. The panic half proves the guard fires in + /// release; [`equal_length_masks_combine_without_panicking`] proves it + /// stays silent on the ordinary case, so a guard that simply rejected + /// everything could not pass both. + #[test] + #[should_panic(expected = "masks from different allocations")] + fn combining_masks_of_different_lengths_is_refused_in_every_build() { + // 200 vs 64: different word counts (4 vs 1), so the truncation is + // reachable rather than hidden by both operands rounding to one word. + let wide = AlphaMask::empty(200); + let narrow = AlphaMask::empty(64); + let _ = wide.and(&narrow); + } + + /// The silence half of the guard above — equal lengths must still work, + /// including the `len % 64 != 0` case where the tail word is partial. + #[test] + fn equal_length_masks_combine_without_panicking() { + let mut a = AlphaMask::empty(200); + let mut b = AlphaMask::empty(200); + a.set(7); + a.set(130); + b.set(130); + b.set(199); + + assert_eq!(a.and(&b).materialize_ordinals(), vec![130], "and"); + assert_eq!(a.or(&b).materialize_ordinals(), vec![7, 130, 199], "or"); + assert_eq!(a.xor(&b).materialize_ordinals(), vec![7, 199], "xor"); + assert_eq!(a.and_not(&b).materialize_ordinals(), vec![7], "and_not"); + // Every product must carry a full-width word slice, not a truncated + // one — the invariant the guard exists to keep. + assert_eq!(a.and(&b).count() + a.and(&b).not().count(), 200); + } + #[test] fn a_complement_never_invents_phantom_addresses() { let rows = tiny_base();