Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .claude/board/LATEST_STATE.md
Original file line number Diff line number Diff line change
@@ -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 |
Expand Down
11 changes: 11 additions & 0 deletions .claude/board/PR_ARC_INVENTORY.md
Original file line number Diff line number Diff line change
@@ -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<u8>`); 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.
Expand Down
1 change: 1 addition & 0 deletions .claude/board/STATUS_BOARD.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
28 changes: 23 additions & 5 deletions .claude/temporal/09-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
62 changes: 61 additions & 1 deletion crates/lance-graph-contract/src/alpha.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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();
Expand Down
Loading