From bffc14c06e05b507b3208fbd61d773831454e153 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 15:23:46 +0000 Subject: [PATCH 1/3] codegraph: G10 x G11 publication lab, falsifiers L1-L6 (D-TGL-PUB step 3) A branch revises one Codegraph source and carries DT64 route intents. It lands in the G10 order: prepare (prepare_refresh + check_route), durable commit through the lab WalSink, then an infallible publication. Recovery reads land records from the durable image above a watermark, in canonical cycle order. L1/L5 prepare and discard change nothing; L2 Fenced publishes nothing; L3 recovery after a crash before publication restores exactly the confirmed state; L4 a second recovery changes nothing; L6 a rebuild replays every record once in canonical order. Lab only; EXO-148 stays locked. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01X6y3drwKSE2zSgoexheLFX --- crates/lance-graph-codegraph/src/graph.rs | 17 +- .../src/graph/publication.rs | 383 ++++++++++++++++++ crates/lance-graph-codegraph/src/lab_wal.rs | 6 + 3 files changed, 399 insertions(+), 7 deletions(-) create mode 100644 crates/lance-graph-codegraph/src/graph/publication.rs diff --git a/crates/lance-graph-codegraph/src/graph.rs b/crates/lance-graph-codegraph/src/graph.rs index 65738cb45..3329152f5 100644 --- a/crates/lance-graph-codegraph/src/graph.rs +++ b/crates/lance-graph-codegraph/src/graph.rs @@ -1271,6 +1271,9 @@ mod atomic { /// two branches over one W0 base, each a revision of one source; landing /// recomputes against the current W0; discarding leaves W0 untouched. Lab /// only: no Thought-Git wiring, no persistence, no routing. +#[cfg(test)] +mod publication; + #[cfg(test)] mod lifecycle { use super::*; @@ -1281,7 +1284,7 @@ mod lifecycle { const HEAD: &str = "#version 2\n#schema predicate\tsubject\tobject\tgrade\tfile\tstart\tend\n"; - fn dir(tag: &str) -> PathBuf { + pub(super) fn dir(tag: &str) -> PathBuf { static NEXT: AtomicU32 = AtomicU32::new(0); let n = NEXT.fetch_add(1, Ordering::Relaxed); let d = std::env::temp_dir().join(format!("cg_lifecycle_{}_{tag}_{n}", std::process::id())); @@ -1310,18 +1313,18 @@ mod lifecycle { } // W0: m::a calls m::b at two sites; n::c calls m::b once. - fn m_v1() -> String { + pub(super) fn m_v1() -> String { rows( "m.rs", &["m::a", "m::b"], &[("m::a", "m::b"), ("m::a", "m::b")], ) } - fn n_v1() -> String { + pub(super) fn n_v1() -> String { rows("n.rs", &["n::c"], &[("n::c", "m::b")]) } // Branch A revises source m: a new symbol m::x calling m::b. - fn m_v2() -> String { + pub(super) fn m_v2() -> String { rows( "m.rs", &["m::a", "m::b", "m::x"], @@ -1329,7 +1332,7 @@ mod lifecycle { ) } // Branch B revises source n: a new symbol n::y; n::c now calls it too. - fn n_v2() -> String { + pub(super) fn n_v2() -> String { rows( "n.rs", &["n::c", "n::y"], @@ -1337,7 +1340,7 @@ mod lifecycle { ) } - fn store(tag: &str, m: &str, n: &str) -> PathBuf { + pub(super) fn store(tag: &str, m: &str, n: &str) -> PathBuf { let d = dir(tag); fs::write(d.join("m.rows.tsv"), m).expect("write"); fs::write(d.join("n.rows.tsv"), n).expect("write"); @@ -1355,7 +1358,7 @@ mod lifecycle { /// Every symbol's callers (names + call-site count), callees and /// impact, by name, so two graphs with different positions compare. - fn answers(g: &mut Codegraph) -> BTreeMap { + pub(super) fn answers(g: &mut Codegraph) -> BTreeMap { let all: Vec = (0..g.words() * 64) .filter_map(|p| g.symbol(p as Pos).map(str::to_string)) .collect(); diff --git a/crates/lance-graph-codegraph/src/graph/publication.rs b/crates/lance-graph-codegraph/src/graph/publication.rs new file mode 100644 index 000000000..ca463711d --- /dev/null +++ b/crates/lance-graph-codegraph/src/graph/publication.rs @@ -0,0 +1,383 @@ +//! D-TGL-PUB step 3: the G10 × G11 publication lab, falsifiers L1–L6 +//! (`.claude/plans/g10-g11-atomic-publication-lab-v1.md` §3). Lab only: no +//! production land, no `ThoughtCommitId` / `DurableRef` (EXO-148 stays +//! locked), no Lance. A branch revises one Codegraph source and carries DT64 +//! route intents. It lands in the G10 order: prepare (Codegraph +//! `prepare_refresh` + `BindStore::check_route`), durable commit through the +//! lab `WalSink`, then an infallible in-memory publication. Recovery reads land +//! records from the durable image above a watermark, in canonical cycle order. + +use std::fs; +use std::path::Path; + +use lance_graph_contract::scheduler::DatasetVersion; +use lance_graph_dt64::{BindStore, ClassSchema, Extent, RouteVersion, SchemaRev}; +use lance_graph_planner::persist_sink::{ + CommitError, CommitOutcome, CycleFrame, CycleId, DetachedCycleBatch, SweepSlot, WalSink, +}; + +use super::atomic::snapshot; +use super::lifecycle::{answers, dir, m_v1, m_v2, n_v1, n_v2, store}; +use super::{Codegraph, PreparedRefresh}; +use crate::lab_wal::LabWal; + +const BASE: u32 = 0x0A1E_0001; +const C1: u32 = 0x0A1E_0002; +const C2: u32 = 0x0A1E_0003; +const P: u16 = 9; + +fn run(f: F) -> F::Output { + tokio::runtime::Builder::new_current_thread() + .build() + .expect("runtime") + .block_on(f) +} + +/// The DT64 side of W0: a base class and two derived ones, no routes yet. +fn binds() -> BindStore { + let schema = || ClassSchema { + schema_rev: SchemaRev(1), + field_count: 4, + extent: Extent::DR256_SQ_X64, + }; + let mut b = BindStore::new(); + b.register(BASE, schema(), vec![]).expect("base"); + b.register_derived(BASE, C1, schema(), vec![]).expect("C1"); + b.register_derived(BASE, C2, schema(), vec![]).expect("C2"); + b +} + +/// One land record: a source revision and route intents. No positions and +/// no `RouteVersion`: W0 assigns both when it publishes. +#[derive(Clone, Debug, PartialEq, Eq)] +struct Record { + krate: String, + rows: String, + routes: Vec<(u16, u32)>, +} + +fn put(out: &mut Vec, b: &[u8]) { + out.extend((b.len() as u32).to_le_bytes()); + out.extend(b); +} + +fn take<'a>(b: &mut &'a [u8], n: usize) -> Option<&'a [u8]> { + let (head, tail) = (b.get(..n)?, b.get(n..)?); + *b = tail; + Some(head) +} + +fn take_u32(b: &mut &[u8]) -> Option { + Some(u32::from_le_bytes(take(b, 4)?.try_into().ok()?)) +} + +impl Record { + fn encode(&self) -> Vec { + let mut out = Vec::new(); + put(&mut out, self.krate.as_bytes()); + put(&mut out, self.rows.as_bytes()); + out.extend((self.routes.len() as u32).to_le_bytes()); + for &(p, c) in &self.routes { + out.extend(p.to_le_bytes()); + out.extend(c.to_le_bytes()); + } + out + } + + fn decode(mut b: &[u8]) -> Option { + let n = take_u32(&mut b)? as usize; + let krate = String::from_utf8(take(&mut b, n)?.to_vec()).ok()?; + let n = take_u32(&mut b)? as usize; + let rows = String::from_utf8(take(&mut b, n)?.to_vec()).ok()?; + let k = take_u32(&mut b)? as usize; + let mut routes = Vec::with_capacity(k); + for _ in 0..k { + let p = u16::from_le_bytes(take(&mut b, 2)?.try_into().ok()?); + routes.push((p, take_u32(&mut b)?)); + } + b.is_empty().then_some(Record { + krate, + rows, + routes, + }) + } +} + +/// A prepared land: the checked Codegraph change and the checked route +/// intents, plus the record to make durable. +struct Prepared { + refresh: Option, + routes: Vec<(u16, u32)>, + record: Record, +} + +/// The land record of `cycle`, read from the durable image. `scan_sealed` +/// carries no payload, so it is never the source. +fn record_of(wal: &LabWal, cycle: CycleId) -> Option { + wal.image(cycle) + .and_then(|img| img.get(&0)) + .and_then(|b| Record::decode(b)) +} + +/// W0: the Codegraph, the DT64 binding store, the durable head they reflect +/// and the watermark of the last land record applied. +struct W0 { + graph: Codegraph, + binds: BindStore, + head: DatasetVersion, + applied_through: Option, +} + +impl W0 { + fn open(base: &Path) -> W0 { + W0 { + graph: Codegraph::open(base).expect("open W0"), + binds: binds(), + head: DatasetVersion(0), + applied_through: None, + } + } + + /// Prepare and check, changing nothing. (`&mut` only so the L1 disable + /// can call `route`; the checks themselves read.) + fn prepare(&mut self, record: Record) -> Prepared { + let d = dir("land"); + fs::write(d.join(format!("{}.rows.tsv", record.krate)), &record.rows).expect("write"); + let refresh = self + .graph + .prepare_refresh(&d, &record.krate) + .expect("codegraph prepare"); + for &(p, c) in &record.routes { + self.binds.check_route(p, c).expect("route intent checks"); + } + Prepared { + refresh, + routes: record.routes.clone(), + record, + } + } + + /// The infallible publication. The writer is W0's sole mutator, so the + /// Codegraph fence cannot be stale and checked routes cannot refuse. + fn publish(&mut self, p: Prepared, cycle: CycleId, head: DatasetVersion) { + if let Some(r) = p.refresh { + self.graph + .commit_refresh(r) + .expect("sole mutator: not stale"); + } + for (pos, class) in p.routes { + self.binds.route(pos, class).expect("checked intent"); + } + self.applied_through = Some(cycle); + self.head = head; + } + + /// The G10 order: prepare, durable commit, then publish. `crash` stops + /// after the durable commit, before publication (boundary B4). + fn land( + &mut self, + wal: &mut LabWal, + cycle: u64, + record: Record, + crash: bool, + ) -> Result<(), CommitError> { + let p = self.prepare(record); + let payload = p.record.encode(); + let out = durable(wal, cycle, self.head, payload)?; + let head = match out { + CommitOutcome::Committed { version, .. } => version, + CommitOutcome::Reconciled { current_head, .. } => current_head, + CommitOutcome::NoChange { head } => head, + }; + if crash { + return Ok(()); + } + self.publish(p, CycleId(cycle), head); + Ok(()) + } + + /// Recovery: every durable land record above the watermark, in canonical + /// cycle order, re-prepared from the image and published. + fn recover(&mut self, wal: &LabWal) { + let mut frames = run(wal.timeline()).expect("timeline"); + frames.sort_by_key(|f| f.cycle); + for f in frames { + if self.applied_through.is_some_and(|w| f.cycle <= w) { + continue; + } + let record = record_of(wal, f.cycle).expect("land record in the durable image"); + let p = self.prepare(record); + self.publish(p, f.cycle, wal.head()); + } + } + + /// The DT64 side, comparable: the version and `P`'s route at every version. + fn routes(&self) -> (RouteVersion, Vec>) { + let v = self.binds.current_version(); + let history = (0..=v.0) + .map(|i| self.binds.routed_at(RouteVersion(i), P).expect("routed_at")) + .collect(); + (v, history) + } +} + +fn durable( + wal: &mut LabWal, + cycle: u64, + base: DatasetVersion, + payload: Vec, +) -> Result { + let batch = DetachedCycleBatch::freeze( + CycleFrame::new(CycleId(cycle), base), + vec![SweepSlot { + cycle: CycleId(cycle), + stream_position: cycle, + owner: 1, + row: 0, + paired_move: None, + payload, + }], + ); + run(wal.commit_cycle(batch)) +} + +fn branch_m() -> Record { + Record { + krate: "m".into(), + rows: m_v2(), + routes: vec![(P, C1)], + } +} + +fn branch_n() -> Record { + Record { + krate: "n".into(), + rows: n_v2(), + routes: vec![(P, C2)], + } +} + +/// L1 + L5. Preparing a branch changes nothing in W0: Codegraph snapshot, +/// `RouteVersion`, `P`'s route. A discarded preparation leaves no trace. +/// Red if preparation applies routes eagerly (disable: `route` instead of +/// `check_route` in `prepare`). +#[test] +fn l1_l5_prepare_and_discard_leave_w0_and_routing_unchanged() { + let base = store("w0", &m_v1(), &n_v1()); + let mut w = W0::open(&base); + let (g, r) = (snapshot(&w.graph), w.routes()); + let claim = w + .binds + .claimed(C1, w.binds.current_version()) + .expect("claim"); + let p = w.prepare(branch_m()); + // Anti-vacuity: the discarded change is real. + assert!(p.refresh.is_some() && !p.routes.is_empty()); + drop(p); + assert_eq!(snapshot(&w.graph), g); + assert_eq!(w.routes(), r); + assert_eq!( + w.binds + .claimed(C1, w.binds.current_version()) + .expect("claim"), + claim + ); +} + +/// L2 + L5. A durable commit against a stale base is `Fenced`: nothing is +/// published, and the branch lands after re-preparing against the new head. +/// Red if publication runs before the durable commit (disable: publish +/// first, the order G10 forbids). +#[test] +fn l2_a_fenced_commit_publishes_nothing_and_the_branch_lands_after_reprepare() { + let base = store("w0", &m_v1(), &n_v1()); + let mut w = W0::open(&base); + let mut wal = LabWal::new(DatasetVersion(1)); // W0 believes head 0 + let (g, r) = (snapshot(&w.graph), w.routes()); + match w.land(&mut wal, 1, branch_m(), false) { + Err(CommitError::Fenced { current_head }) => w.head = current_head, + other => panic!("expected Fenced, got {other:?}"), + } + assert_eq!(snapshot(&w.graph), g, "nothing published"); + assert_eq!(w.routes(), r, "no route, no version"); + w.land(&mut wal, 1, branch_m(), false) + .expect("lands after re-prepare"); + assert_eq!( + w.binds.routed_at(w.binds.current_version(), P), + Ok(Some(C1)) + ); +} + +/// L3 + L4. The commit is durable, then a crash before publication. Recovery +/// from the durable image restores exactly the confirmed state; a second +/// recovery changes nothing. Red if recovery reads `scan_sealed` (no +/// payload) instead of the image; red if the watermark is ignored (DT64 +/// `route` is not idempotent: `RouteVersion` would tick again while the +/// Codegraph stays unchanged by its digest skip). +#[test] +fn l3_l4_recovery_restores_the_confirmed_state_once() { + let base = store("w0", &m_v1(), &n_v1()); + let mut w = W0::open(&base); + let mut wal = LabWal::new(DatasetVersion(0)); + let (g0, (v0, _)) = (snapshot(&w.graph), w.routes()); + w.land(&mut wal, 1, branch_m(), true).expect("durable"); + // B4: durable, not published. + assert_eq!(snapshot(&w.graph), g0); + assert_eq!(w.binds.current_version(), v0); + + w.recover(&wal); + assert_ne!(snapshot(&w.graph), g0, "anti-vacuity: the graph moved"); + let r1 = w.routes(); + assert_eq!(r1.0, RouteVersion(v0.0 + 1), "exactly one tick"); + assert_eq!(w.binds.routed_at(r1.0, P), Ok(Some(C1))); + let mut clean = Codegraph::open(&store("clean", &m_v2(), &n_v1())).expect("clean"); + assert_eq!(answers(&mut w.graph), answers(&mut clean)); + // Taken after the oracle comparison, whose queries fill the closure + // caches; the second recovery must then change nothing at all. + let g1 = snapshot(&w.graph); + + // L4: the watermark makes a second recovery a no-op. + w.recover(&wal); + assert_eq!(snapshot(&w.graph), g1); + assert_eq!(w.routes(), r1); +} + +/// L6. B5: W0 is lost and rebuilt from the durable base, then every land +/// record is replayed once in canonical cycle order, whatever the physical +/// order. The result equals the W0 that published both lands live. Two +/// records route `P` (C1 then C2), so order is observable. Red if recovery +/// trusts physical order (disable: drop the canonical sort). +#[test] +fn l6_a_rebuild_replays_every_record_once_in_canonical_order() { + let base = store("w0", &m_v1(), &n_v1()); + let mut live = W0::open(&base); + let mut wal = LabWal::new(DatasetVersion(0)); + live.land(&mut wal, 1, branch_m(), false).expect("land m"); + live.land(&mut wal, 2, branch_n(), false).expect("land n"); + assert_eq!( + live.binds.routed_at(live.binds.current_version(), P), + Ok(Some(C2)) + ); + + wal.reverse_physical_order(); + let mut rebuilt = W0::open(&base); + rebuilt.recover(&wal); + assert_eq!(rebuilt.routes(), live.routes()); + assert_eq!(answers(&mut rebuilt.graph), answers(&mut live.graph)); + assert_eq!(rebuilt.head, live.head); + assert_eq!(rebuilt.applied_through, Some(CycleId(2))); +} + +/// The record codec round-trips and refuses trailing bytes, so a recovery +/// cannot silently read a truncated or padded record. +#[test] +fn a_land_record_round_trips_and_refuses_garbage() { + let r = branch_m(); + let b = r.encode(); + assert_eq!(Record::decode(&b), Some(r)); + let mut padded = b.clone(); + padded.push(0); + assert_eq!(Record::decode(&padded), None); + assert_eq!(Record::decode(&b[..b.len() - 1]), None); + assert_eq!(Record::decode(&[]), None); +} diff --git a/crates/lance-graph-codegraph/src/lab_wal.rs b/crates/lance-graph-codegraph/src/lab_wal.rs index 7a945a38d..ec442cd3e 100644 --- a/crates/lance-graph-codegraph/src/lab_wal.rs +++ b/crates/lance-graph-codegraph/src/lab_wal.rs @@ -59,6 +59,12 @@ impl LabWal { self.fault = Some(fault); } + /// Lab seam: reverse the stored (physical) order of the sealed cycles, + /// so a recovery that trusts physical order replays them wrongly. + pub(crate) fn reverse_physical_order(&mut self) { + self.sealed.reverse(); + } + /// The durable image of `cycle` (row → payload): where recovery reads /// land records, because `scan_sealed` carries no payload. pub(crate) fn image(&self, cycle: CycleId) -> Option<&BTreeMap>> { From 9bdee1d4c55af5c34d7946e0484d73d6d3e28dca Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 15:25:52 +0000 Subject: [PATCH 2/3] board: D-TGL-PUB step 3 (L1-L6) shipped in the lab; L6 finding: physical-order replay with a watermark loses records Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01X6y3drwKSE2zSgoexheLFX --- .claude/board/STATUS_BOARD.md | 2 +- ...gl-release-fence-and-g10-g11-boundaries.md | 26 +++++++++++++++++++ .../g10-g11-atomic-publication-lab-v1.md | 2 +- 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/.claude/board/STATUS_BOARD.md b/.claude/board/STATUS_BOARD.md index 5b5717066..f9e55128b 100644 --- a/.claude/board/STATUS_BOARD.md +++ b/.claude/board/STATUS_BOARD.md @@ -249,7 +249,7 @@ Plan: [d-codegraph-0-resident-codegraph-v1.md](../plans/d-codegraph-0-resident-c | **D-TGL-G10** | atomic publication: prepare all, durable commit first, then infallible in-memory apply from the land record, replay after crash | OPEN (production); named gap: no prepare/validate route API on `BindStore` | a `Fenced` leaves no published partial state | | **D-TGL-G11** | branch-local routing; no branch-minted DR position as durable routing key | OPEN (production) | a branch route leaves `BindStore::current_version()` unchanged until land | | **D-TGL-G12** | identities apart: W-slot, generation, ThoughtCommitId, DurableRef, RouteVersion, Lance version, source SHA (provenance only) | OPEN (production) | joins only through receipt / CommitWitness / MetaKey / land record | -| **D-TGL-PUB** | G10 × G11 lab: prepared land (Codegraph + DT64 route intent), fenced durable commit through a test `WalSink`, crash recovery from the durable record, branch-local routing | SPEC: plan `g10-g11-atomic-publication-lab-v1.md`; boundaries B1–B5 located. Step 1 SHIPPED (lab-ready): `BindStore::check_route` (read-only; `route` runs the same `RouteLog::check`), 3 tests, 3 disables red. Step 2 SHIPPED (lab): `LabWal` in `lance-graph-codegraph/src/lab_wal.rs` (test-only `WalSink`: fence, one version per commit, reconcile, `HashConflict`, payload-free scan, image read, injected I/O / Ambiguous faults), 4 tests, 4 disables red; planner is a dev-dependency, one ndarray via `[patch]`. Step 3 (L1–L6) open | L1–L6, each with a disable; L4 shows route re-application ticks `RouteVersion` without a watermark | +| **D-TGL-PUB** | G10 × G11 lab: prepared land (Codegraph + DT64 route intent), fenced durable commit through a test `WalSink`, crash recovery from the durable record, branch-local routing | SPEC: plan `g10-g11-atomic-publication-lab-v1.md`; boundaries B1–B5 located. Step 1 SHIPPED (lab-ready): `BindStore::check_route` (read-only; `route` runs the same `RouteLog::check`), 3 tests, 3 disables red. Step 2 SHIPPED (lab): `LabWal` in `lance-graph-codegraph/src/lab_wal.rs` (test-only `WalSink`: fence, one version per commit, reconcile, `HashConflict`, payload-free scan, image read, injected I/O / Ambiguous faults), 4 tests, 4 disables red; planner is a dev-dependency, one ndarray via `[patch]`. Step 3 SHIPPED (lab): `graph/publication.rs`, L1–L6 green, 5 disables red at the intended assertion (L4: Codegraph unchanged, `RouteVersion` 1 → 2; L6 without the canonical sort silently loses cycle 1). EXO-148 locked; no production land | L1–L6, each with a disable; L4 shows route re-application ticks `RouteVersion` without a watermark | | **D-TGL-EXO148** | `DurableRef` + never-reused `ThoughtCommitId` before any durable land, replay or PR record | OPEN (production, EXO-148) | reuse test (D-RST-1) | | **D-DT64-AUTH** | DT64 / F### as the one method authority: census of production words with a two-way exception ratchet; parallel identities (`MethodSignature`, tract `Registry`, XLOG `Word`, ogar-loco `FnIndex`) stay as listed `LAB` adapters | QUEUED: plan `d-dt64-method-authority-census-v1.md` (operator decision 2026-11-02); egglog pinned to one commit in `egglog-r2il-probe`, `dr-math-binding-probe`, `xlog-relcomp-probe` | undeclared production word → red; stale exception → red; current tree green | | **D-CODEGRAPH-0-D** | local stdio MCP over C | QUEUED | only after B and C gates | diff --git a/.claude/board/entries/2026-11-02-d-tgl-release-fence-and-g10-g11-boundaries.md b/.claude/board/entries/2026-11-02-d-tgl-release-fence-and-g10-g11-boundaries.md index 3c3d22aa8..e61811c03 100644 --- a/.claude/board/entries/2026-11-02-d-tgl-release-fence-and-g10-g11-boundaries.md +++ b/.claude/board/entries/2026-11-02-d-tgl-release-fence-and-g10-g11-boundaries.md @@ -62,3 +62,29 @@ OPEN: `check_route`, the lab `WalSink`, and the L1–L6 test are three separate - a fault before publish and one after publish leave different durable states. - **Disables (each red on its own test):** drop the fence; leak the payload through `scan`; skip the reconcile lookup; make `Ambiguous` publish nothing. - **Gates:** codegraph suites green (12 lib tests); clippy and fmt clean. + +## Step 3 done: L1–L6 (2026-11-02, MEASURED in the lab) +- **Code:** `crates/lance-graph-codegraph/src/graph/publication.rs` (`#[cfg(test)]`). + - A branch revises one Codegraph source and carries DT64 route intents. It lands in the G10 order: + 1. prepare (`prepare_refresh` + `check_route`); + 2. durable commit through `LabWal`, with the land record (source + route intents, no positions, no `RouteVersion`) as payload; + 3. an infallible publication (`commit_refresh`, `route`, watermark). + - Recovery reads records from the durable image above the watermark, in canonical cycle order. The record codec refuses truncated or padded bytes. +- **Results (all green):** + - **L1/L5:** prepare and discard leave the Codegraph snapshot, `RouteVersion`, `routed_at(P)` and `claimed` unchanged. + - **L2:** `Fenced` publishes nothing; the branch lands after re-preparing against the new head. + - **L3:** after a crash between the durable commit and publication, recovery restores exactly the confirmed state: clean-open equality, `RouteVersion` +1, `P → C1`. + - **L4:** a second recovery changes nothing. + - **L6:** a rebuild from the base with reversed physical order equals the live state. +- **Disables (each red at the intended assertion):** + - **L1/L5 eager `route`:** L1, L2 and L3 red. + - **L2 publish before the durable commit:** "nothing published" red. + - **L3 read records from `scan_sealed`:** "land record in the durable image" red. + - **L4 drop the watermark:** the Codegraph snapshot stays equal (digest skip) while `RouteVersion` goes 1 → 2. This is the predicted non-idempotence of `route`, now measured. + - **L6 drop the canonical sort:** the rebuild replays cycle 2 first, then the watermark skips cycle 1. The record is lost, not merely reordered (`RouteVersion` 1, `P → C2` only). **Physical-order replay plus a watermark loses data silently.** +- **Gates:** codegraph suites green (17 lib tests); clippy and fmt clean. +- **Limits:** + - The lab `prepare` takes `&mut` only so the L1 disable can call `route`. A production prepare should take `&self`. + - The `WalSink` is in memory; no 512-byte ABI and no `LanceCycleWriter`. + - EXO-148 stays locked. + - Lab results satisfy no production gate. diff --git a/.claude/plans/g10-g11-atomic-publication-lab-v1.md b/.claude/plans/g10-g11-atomic-publication-lab-v1.md index 39e2c31fe..f125f6ea0 100644 --- a/.claude/plans/g10-g11-atomic-publication-lab-v1.md +++ b/.claude/plans/g10-g11-atomic-publication-lab-v1.md @@ -82,6 +82,6 @@ The in-memory publication is one infallible step (Codegraph `commit_refresh` + t - it agrees with `route` on accept and refuse for every case; - it leaves `current_version()` unchanged. 2. **DONE (2026-11-02):** a lab-only test `WalSink` (`LabWal`, `crates/lance-graph-codegraph/src/lab_wal.rs`) beside the Codegraph test (dev-dependency on `lance-graph-planner`; test `WalSink` impls exist in ten places, for example `lance-graph-planner/examples/blw_fusion.rs` `MemWal`). No production edge from Codegraph to the planner. -3. The lab test itself, in-crate, because `prepare_refresh` / `commit_refresh` are crate-private. +3. **DONE (2026-11-02):** the lab test itself (`crates/lance-graph-codegraph/src/graph/publication.rs`), in-crate, because `prepare_refresh` / `commit_refresh` are crate-private. Out of scope: `LanceCycleWriter` (needs the 512-byte ABI and real Lance), `ThoughtCommitId`/`DurableRef` (EXO-148), MergeQueue, review gates. From 5e262e934545a5679ef169e0fc6ceb4babc4b893 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 15:26:17 +0000 Subject: [PATCH 3/3] board: post-merge record for #1925; carry L1-L6 made after its merge point Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01X6y3drwKSE2zSgoexheLFX --- .claude/board/LATEST_STATE.md | 5 +++++ .claude/board/PR_ARC_INVENTORY.md | 11 +++++++++++ 2 files changed, 16 insertions(+) diff --git a/.claude/board/LATEST_STATE.md b/.claude/board/LATEST_STATE.md index cf074d64a..88dbaf00f 100644 --- a/.claude/board/LATEST_STATE.md +++ b/.claude/board/LATEST_STATE.md @@ -1,3 +1,8 @@ +## 2026-11-02 — main after #1925 (merged `e89e61790`) + +- dt64: `BindStore::check_route` (read-only route check). +- Codegraph: the lab `WalSink` for the G10 × G11 publication lab; closure caches filled between prepare and commit are dropped at commit. + ## 2026-11-02 — main after #1924 (merged `78f9996af`) - Codegraph refresh splits into a fenced prepare / commit (crate-private); the F-LAND / F-DISCARD lab tests pin it, in debug and release. diff --git a/.claude/board/PR_ARC_INVENTORY.md b/.claude/board/PR_ARC_INVENTORY.md index f1d8b79a7..e4d3d6f55 100644 --- a/.claude/board/PR_ARC_INVENTORY.md +++ b/.claude/board/PR_ARC_INVENTORY.md @@ -1,3 +1,14 @@ +## 2026-11-02 — lance-graph PR #1925 (merged `e89e61790`, branch `claude/clone-repositories-71a5sw`) — D-TGL-PUB steps 1–2: `check_route`, lab `WalSink`; stale-closure fix + +- **Added:** + - dt64 `BindStore::check_route`, the read-only form of `route`; `route` runs the same `RouteLog::check`. + - Codegraph lab `WalSink` (`LabWal`, test-only): fence, reconcile, `HashConflict`, payload-free scan, image read, injected faults. + - Fix: commit drops a closure cache filled between prepare and commit (Codex P2 on #1924). + - Post-merge record for #1924. +- **Locked:** a checked route intent stays valid; `scan_sealed` carries no payload, so land records are read from the image. +- **Deferred:** the L1–L6 lab test (two commits after the merge point, carried by the next PR). +- **Confidence:** dt64 25 + 29, codegraph 12 lib tests; 3 + 4 + 1 disables red; planner is a dev-dependency only, with one ndarray via `[patch]`. + ## 2026-11-02 — lance-graph PR #1924 (merged `78f9996af`, branch `claude/clone-repositories-71a5sw`) — F-LAND / F-DISCARD lab, D-TGL lifecycle contract, G10 × G11 lab spec - **Added:**