From 096669867c18fd0b7c515422661cf36ea2b2af35 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 27 Aug 2026 15:12:37 +0000 Subject: [PATCH 1/2] board: PR #42 arc entry, and log the arc-inventory gap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR_ARC_INVENTORY gets #42's entry per the board README's every-PR rule. Writing it surfaced a real lapse, logged as ISS-LGJ-ARC-INVENTORY-STOPPED-AT-32 rather than papered over: entries run #1..#20, then #32, then nothing — #21-#31 and #33-#41 have none, and #32's own entry is still marked "(draft, opened …)" with no merge sha, so the newest entry is also stale. Not backfilled here. Reconstructing nineteen entries from a later session's memory is the same retroactive-hygiene anti-pattern the file's own header already owns, with worse provenance — I have first-hand knowledge of #38-#41 only. The issue records the exact extent and the method for filling it from each PR's own diff. This commit is board hygiene only, so per the termination clause it generates no arc entry of its own. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01DCfrD5y19cvFc4AoyydXYv --- .claude/board/ISSUES.md | 32 +++++++++++++++++++ .claude/board/PR_ARC_INVENTORY.md | 53 +++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) diff --git a/.claude/board/ISSUES.md b/.claude/board/ISSUES.md index 2f92a1f..8878216 100644 --- a/.claude/board/ISSUES.md +++ b/.claude/board/ISSUES.md @@ -1,5 +1,37 @@ # Issues Log — Open + Resolved (double-entry, append-only) +## ISS-LGJ-ARC-INVENTORY-STOPPED-AT-32 (2026-08-27) — OPEN + +**Found.** While landing PR #42's own arc entry, per the board README's +rule (`PR_ARC_INVENTORY.md` — "every PR, at open"). + +**Measured.** The file's entries run `#1..#20`, then `#32`, then nothing. +Missing: **#21-#31 and #33-#41** — nineteen PRs with no arc entry. And +#32's own entry still reads `(draft, opened 2026-08-25)`: it was written +at open, correctly, and never updated with its merge sha, so the newest +entry in the file is also stale. + +**Why it matters, precisely.** `LATEST_STATE.md` answers *what exists*; +the arc answers *why it exists*. A future session reading the current file +would conclude the repo went from minor 8 (#32) straight to a ClassView +provider (#42), with the whole hop arc — the 32-sweep root-cause, the +gather rewrite, the measured absence of a crossover, the memoisation and +its cold regression — absent from the record that exists to carry it. + +**Not backfilled here, deliberately.** Reconstructing nineteen entries +from a later session's memory is the retroactive-hygiene anti-pattern the +file's own 2026-08-17 header lapse already names, with worse provenance: +I have first-hand knowledge of #38-#41 only, and inventing Added/Locked/ +Deferred lines for #21-#31 and #33-#37 would produce a record that reads +authoritative and is partly guessed. The honest move is to log the gap +with its exact extent and let it be filled from each PR's own diff. + +**Next step when picked up.** Walk `git log --merges origin/main` for the +missing numbers, write each entry from its own diff and PR body (not from +recall), and update #32's header with its merge sha. One pass, not a +trickle — a half-filled arc is harder to trust than an empty stretch with +a dated note explaining it. + ## ISS-LGJ-HOP-SWEEPS-FULL-POPULATION (2026-08-27) — RESOLVED **Found.** By running bench Component G — the F-PARITY harness — for the diff --git a/.claude/board/PR_ARC_INVENTORY.md b/.claude/board/PR_ARC_INVENTORY.md index 8e28c7c..517f533 100644 --- a/.claude/board/PR_ARC_INVENTORY.md +++ b/.claude/board/PR_ARC_INVENTORY.md @@ -8,6 +8,59 @@ > anti-pattern the imported board rules name. Backfilled below in one > pass rather than left stale; PR #4 onward gets its entry at merge time. +## PR #42 — lgj-abi: the REAL OGAR ClassView provider, bound behind a feature (merged 2026-08-27, `507cc93`) + +- **Added:** `ogar-classview` feature on `native/lgj-abi` binding + `ogar_class_view::OgarClassView` — the ontology-backed provider over + `ogar_vocab` — so `edge_participation` derives from each class's real + field basis instead of the fixture's `FieldMask::FULL` constant. New + `examples/classview_census.rs`. A `[patch."…/lance-graph"]` collapsing + the git-vs-path SourceId split (ogar-class-view pulls the contract by + git branch, this crate by path; cargo does NOT unify the two, so + without it the build carries two `lance-graph-contract` crates and two + incompatible `ClassView` traits — the trap recorded in tesseract-rs for + `ogar-doc-ir`). Verified: `cargo tree` shows one. +- **Locked:** participation is a per-class fact, not a constant. Measured + over the vocabulary: **98 registered classes, 12 distinct participation + masks** (field counts 0-13) against the fixture's single answer for all + 98. An unregistered classid participates in NOTHING — an unknown class + is not a licence to traverse every facet. +- **Deferred:** the row CONTENT. Binding the provider EXPOSED that the + generated store draws classids from `0..16` + (`ROWSTORE_CLASS_CARDINALITY`) while every vocabulary classid is + `>= 0x0100` — **disjoint**, so a generated store under the real + provider hops nothing. Lance-loaded SoA rows are what make the bound + provider observable end-to-end. Pinned by + `hop_under_the_real_provider_narrows_by_class`, not left in prose. + Also deferred: narrowing participation to associations-only (the + `fields()` answer flattens attributes + associations, so it is a + SUPERSET — safe direction; the split needs an OGAR-side ask). +- **Docs:** `class_view_provider`'s module docs rewritten (fixture is now + the DEFAULT provider, not "the ONLY one this crate needs"); the census + numbers and the disjoint-domain gap stated there rather than only in a + commit message. +- **Gates:** default (feature OFF) Rust **134/134** and Java **447/447** + (304 core + 143 consumer) — unchanged, the point of defaulting off; + feature ON **136/136**; `clippy --all-targets -- -D warnings` + `fmt + --check` clean in BOTH configurations; G11 contract-import fence green + (`class_view`, `canonical_node`, `ontology`, `facet` only). +- **Disable-run, red-then-green:** `edge_participation`'s ogar arm + returns `FieldMask::FULL` → **5 tests red** (4 provider + the hop), + green on restore. +- **Two gated tests, each with a contrasting twin** — neither deleted nor + suppressed. `edge_participation_covers_exactly_the_low_32_bits` (all + 32) pairs with `the_real_provider_narrows_rather_than_widens` + (unregistered → 0; richest class → exactly its low-13 prefix); + `hop_matches_the_pinned_rowstore_regression_10_19_29` (19/29) pairs + with the hop twin above (empty — two-sided, since the provider still + answers 13 for `0x0103`, so emptiness is the store's domain and not a + dead provider). +- **Confidence:** high. Both halves are measured numbers with a + red-then-green disable behind them. The finding — that a fixture + answering `FULL` for every input cannot be falsified in place, and that + binding a real provider is what measures the fixture's reach — is + recorded as `E-BINDING-A-REAL-PROVIDER-MEASURES-THE-FIXTURE-1`. + ## PR #32 — ABI minor 8: the register groupings served as DATA (draft, opened 2026-08-25) - **Added:** `LgjAbiManifest.{carving_count, carvings[8]}` (docs/abi.md From c93995b7ffd630393e6b1ac51dea42fde2b6115a Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 27 Aug 2026 15:23:51 +0000 Subject: [PATCH 2/2] board: backfill the twenty missing arc entries, from each PR's own diff MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR_ARC_INVENTORY had entries for #1-#12, #14, #16, #18, #20 and #32, and nothing else. Missing: #13, #22-#31, #33-#41 — twenty PRs. (#15/#17/#19/#21 are also absent and correctly so: each is itself an arc-entry-only PR, exempt under the termination clause.) #32's own entry still read "(draft, opened …)" with no merge sha. Corrects this branch's first count, which said nineteen and read the gap as starting at #21. Both were wrong — it is twenty, and it starts at #13. The first count was a range subtraction over a file with holes; the enumeration is what found the difference. Method, which is the point rather than an aside: each entry drafted from that PR's own body and diff, five parallel agents over four PRs each, none permitted to work from a later session's recall. Every backfilled entry's Confidence bullet ends "Backfilled 2026-08-27 from the PR body and diff, not written at merge time", so reconstructed entries are distinguishable from ones written at merge time; several state which claims are the PR body's own and were not re-verified. Every cited sha, date and merge-vs- squash label machine-checked against git. Four things the backfill turned up, each recorded in the entry it belongs to: #25's body asserts "no code, no reproducer changes" and its own diff contradicts it; #39 left its lgj_hop doc comment describing the pre-change design; #34's banked evidence file did not identify its own JDK; and #41 is on main while its own title reads [DO NOT MERGE AS-IS], recorded as unresolved disposition rather than an endorsement. ISS-LGJ-ARC-INVENTORY-STOPPED-AT-32 moves to RESOLVED with the corrected count and the standing rule restated: the entry goes in at open, in the PR's own commit. The backfill is the repair, not the process. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01DCfrD5y19cvFc4AoyydXYv --- .claude/board/ISSUES.md | 74 ++++++---- .claude/board/PR_ARC_INVENTORY.md | 230 +++++++++++++++++++++++++++++- 2 files changed, 276 insertions(+), 28 deletions(-) diff --git a/.claude/board/ISSUES.md b/.claude/board/ISSUES.md index 8878216..3cb91c1 100644 --- a/.claude/board/ISSUES.md +++ b/.claude/board/ISSUES.md @@ -1,36 +1,56 @@ # Issues Log — Open + Resolved (double-entry, append-only) -## ISS-LGJ-ARC-INVENTORY-STOPPED-AT-32 (2026-08-27) — OPEN +## ISS-LGJ-ARC-INVENTORY-STOPPED-AT-32 (2026-08-27) — RESOLVED **Found.** While landing PR #42's own arc entry, per the board README's rule (`PR_ARC_INVENTORY.md` — "every PR, at open"). -**Measured.** The file's entries run `#1..#20`, then `#32`, then nothing. -Missing: **#21-#31 and #33-#41** — nineteen PRs with no arc entry. And -#32's own entry still reads `(draft, opened 2026-08-25)`: it was written -at open, correctly, and never updated with its merge sha, so the newest -entry in the file is also stale. - -**Why it matters, precisely.** `LATEST_STATE.md` answers *what exists*; -the arc answers *why it exists*. A future session reading the current file -would conclude the repo went from minor 8 (#32) straight to a ClassView -provider (#42), with the whole hop arc — the 32-sweep root-cause, the -gather rewrite, the measured absence of a crossover, the memoisation and -its cold regression — absent from the record that exists to carry it. - -**Not backfilled here, deliberately.** Reconstructing nineteen entries -from a later session's memory is the retroactive-hygiene anti-pattern the -file's own 2026-08-17 header lapse already names, with worse provenance: -I have first-hand knowledge of #38-#41 only, and inventing Added/Locked/ -Deferred lines for #21-#31 and #33-#37 would produce a record that reads -authoritative and is partly guessed. The honest move is to log the gap -with its exact extent and let it be filled from each PR's own diff. - -**Next step when picked up.** Walk `git log --merges origin/main` for the -missing numbers, write each entry from its own diff and PR body (not from -recall), and update #32's header with its merge sha. One pass, not a -trickle — a half-filled arc is harder to trust than an empty stretch with -a dated note explaining it. +**Measured, and the first count was wrong.** The entry as first filed said +"entries run `#1..#20`, then `#32`" and "nineteen PRs". Enumerating rather +than eyeballing the range gives a different and less tidy answer: + +- entries present: `#1`–`#12`, `#14`, `#16`, `#18`, `#20`, `#32`; +- **missing: `#13`, `#22`–`#31`, `#33`–`#41` — twenty PRs**, not nineteen, + and the gap starts at `#13`, well inside the range the first count read + as complete; +- `#15`, `#17`, `#19`, `#21` are also absent and are **correctly** absent — + each is itself an arc-entry-only PR, exempt under the termination clause; +- `#32`'s entry read `(draft, opened 2026-08-25)` with no merge sha, so the + newest entry in the file was also stale. + +The first count was a range subtraction over a file that has holes. Stated +here rather than silently corrected, because "I checked" and "I enumerated" +are different claims and only the second one was ever load-bearing. + +**Why it mattered.** `LATEST_STATE.md` answers *what exists*; the arc +answers *why*. As it stood, a future session read this repo as going from +ABI minor 8 straight to a ClassView provider — the whole hop arc (the +32-sweep root cause, the gather rewrite, the measured absence of a +crossover, the memoisation and its cold regression) absent from the record +that exists to carry exactly that. + +**Resolved.** All twenty entries written, plus `#32`'s header corrected to +name its merge and the fact that it reached `main` only via `#33`. Method, +which was the whole point: each entry drafted from **that PR's own body and +diff** — five parallel agents, four PRs each, none permitted to work from a +later session's recall. Every backfilled entry ends its **Confidence:** +bullet with `Backfilled 2026-08-27 from the PR body and diff, not written at +merge time`, so a reader can tell at a glance which entries were written at +merge time and which were reconstructed; several say plainly which of their +claims are the PR body's own and were not re-verified. + +**What the backfill itself turned up** (each recorded in the entry it +belongs to, not only here): #25's PR body asserts "no code, no reproducer +changes" and its own diff contradicts it — a second commit landed on the +branch after the body was written; #39 left its `lgj_hop` doc comment +describing the pre-change design, caught one PR later by #40; #34's banked +evidence file did not identify its own JDK, corrected same-day by #35; and +#41 is merged on `main` while its own title reads `[DO NOT MERGE AS-IS]`, +which the entry records as unresolved disposition rather than an +endorsement. + +**Standing rule, unchanged and now the live one:** the entry goes in at +open, in the PR's own commit. The backfill is the repair, not the process. ## ISS-LGJ-HOP-SWEEPS-FULL-POPULATION (2026-08-27) — RESOLVED diff --git a/.claude/board/PR_ARC_INVENTORY.md b/.claude/board/PR_ARC_INVENTORY.md index 517f533..dda75c0 100644 --- a/.claude/board/PR_ARC_INVENTORY.md +++ b/.claude/board/PR_ARC_INVENTORY.md @@ -61,7 +61,114 @@ binding a real provider is what measures the fixture's reach — is recorded as `E-BINDING-A-REAL-PROVIDER-MEASURES-THE-FIXTURE-1`. -## PR #32 — ABI minor 8: the register groupings served as DATA (draft, opened 2026-08-25) +## PR #41 — [DO NOT MERGE AS-IS] hop as AND over a memoised per-facet mask — measured, conditional (merged 2026-08-27, merge `9cd8a65`) + +- **Merged despite its own title, and the conditionality is the record.** The PR opens *"Not recommended for merge as-is… it is a **2 500× regression cold**"* and exists so the work and its numbers are not stranded while the choice is made on data. It landed on `main` anyway; a future session reading this must not read the merge as an endorsement. +- **Added:** `RowStore::facet_bits(classid) -> Arc<[u32]>` (`native/lgj-abi/src/rowstore.rs`) — one 32-bit per-row mask of which facets carry that class, built once through `simd_rowstore_facet_match` and shared by refcount, behind an `RwLock)>>` bounded at `FACET_CACHE_SLOTS = 4` (`n_rows × 4` bytes each, 1 MiB at 262 144 rows), oldest-first eviction. `lgj_hop` (`exports.rs`) becomes `facet_bits[row] & effective_facets` plus a `trailing_zeros` walk — an AND — replacing #40's inline per-facet `u32::from_le_bytes` classid compare. `examples/hop_gather_vs_sweep.rs` extended to four shapes with equivalence asserted at every point; raw output banked at `.claude/board/hop-memoisation-cold-vs-warm.txt` (five populations × twelve densities). +- **What it says #40 got wrong:** not the numbers — the STRUCTURE. #40's gather "traded the algebra away: the classid predicate stopped being a mask at all." The PR decomposes the hop four ways (classid compare / `payload_hi32 == 0` / ∩ `src` / scatter) and finds only the scatter genuinely outside mask algebra — the destination index is decoded from the row's payload, so it is data-dependent. The contract had held under #40 (no `java/`, no `abi.md`, no minor bump, allowlist + G2 + G3 green at the identical 384-byte floor); the mask-native currency had not. +- **The regression it introduces, as the PR measured it (65 536 rows, Component G):** warm the AND wins everywhere, up to **13.5×** (0.01 % 2.7 → 2.8 µs; 1 % 46.8 → **10.1 µs**; 25 % 3 673.3 → **271.5 µs**; 100 % 6 070.2 → **1 952.3 µs**). Cold it is flat at ~6 900 µs at every density because the O(n) build dominates — at 0.01 % that is **2 500× worse than #40's gather**. Break-even ~**190 hops** at a 1 % frontier, ~**2 hops** at 25 %+. +- **No invalidation, by construction:** `RowStore` exposes no `&mut self` method, so the buffer is immutable for the store's life. The read lock is dropped before any build, so two threads racing a cold classid may both build — a wasted pass, never a wrong answer. +- **Deferred — the three options, undecided:** (1) this, unconditional (worst case 2 500×); (2) keep #40's gather (no regression anywhere, leaves 13.5× on the table for repeated dense hops); (3) a **lazily-filled mask** — fill each row's `u32` during the gather that first touches it, with a per-row filled bit: cold ≈ gather, warm = the AND, no O(n) build, no tuning knob — **the PR's own recommendation, pending a measurement of whether rows are re-hopped, which is not measured.** +- **A separate finding, one variable at a time:** the first run was on JDK 27 and looked like a further win. The SAME binary re-run on JDK 26 lands within ~5 % and marginally *faster* (1 %/65 536: 10.856 vs 10.713; 25 %/65 536: 250.0 vs 262.5). **The JDK contributes nothing** — confirmed against the source: `grep` finds **zero** `value record` / `value class` in `java/src/main/java`, so JDK 27 is a different JIT and no Valhalla flattening. Consequence recorded: every Component G number in this repo, past and present, includes ordinary heap allocation for the per-hop `Mask` / `WideFieldMask` wrappers — Panama is real and measured, Valhalla has never run outside `valhalla-lab`. +- **Docs:** `lgj_hop`'s kernel-composition comment rewritten to the three-try arc (32 sweeps 24.8 ms → gather 34 µs → memoised AND) with each superseded shape named; a doc comment displaced onto `bytes_arc` by inserting `facet_bits` above it, fixed — caught by clippy's missing-docs gate, not by reading. +- **Gates (as the PR claimed them):** lgj-abi **134/134**; AllTests 304, GraphHopTest 66 (G3 unchanged), TradesParity 12, TradesAllocation 3, BricksAuth 62 = **447 Java checks**; `clippy -D warnings` + `fmt` clean. No ABI change, no minor bump. +- **Confidence:** medium-high on the measurements (two instruments, equivalence asserted at every configuration), but the DISPOSITION is unresolved — the branch is merged while its own body recommends against it, and option 3's upside is explicitly unmeasured. Backfilled 2026-08-27 from the PR body and diff, not written at merge time. + +## PR #40 — lgj-abi: hop gathers instead of sweeping — the predicted crossover does not exist (merged 2026-08-27, merge `2758730`) + +- **Added:** `lgj_hop` (`native/lgj-abi/src/exports.rs`) rewritten to GATHER — touching only the rows `src` names and reading each row's participating facets in place out of that row's own 512 bytes, no per-row intermediate at all. New `native/lgj-abi/examples/hop_gather_vs_sweep.rs` (202 lines): both shapes over five populations × twelve densities, **byte-identical output asserted at every configuration** plus an anti-vacuity guard against an empty hop (which would let both shapes "agree" on nothing and time two empty loops). Raw output banked at `.claude/board/hop-gather-vs-sweep-crossover.txt`. +- **What it corrects in #39:** #39 left the compare half sweeping the whole population and predicted a **density crossover** — sparse frontiers favouring a gather, dense ones favouring the sweep's sequential vectorised access — and said the merit of the next rung depended on it. **Measured: no crossover. Gather wins all 60 configurations**, 2 754× at 0.01 % density down to **1.73× at 100 %** (1 024 rows: 361×/113×/7.3×/1.73×; 262 144 rows: 2 754×/90×/2.6×/1.73×). +- **The mechanism is the correction, not just the number:** a sweep MATERIALISES an `n`-element per-row intermediate that each row reads exactly once, so it is never amortised — at full density it does everything the gather does PLUS allocate, zero, write and re-read `n` u32s. Strictly more work at every density. The prediction reasoned about access PATTERN and missed that one shape simply does MORE. +- **Locked:** the absence of a crossover makes the change **unconditional** — no threshold, no dispatch, no heuristic gate, and none of the two-sided evidence such a gate would have demanded. Also locked: a scalar gather beats a vectorised `ndarray::simd` sweep — the win is in not doing the work, not in the vector width. No SIMD primitive is orphaned by this (checked, not assumed): `simd_rowstore_facet_match` remains behind `lgj_row_facet_match`, `simd_rowstore_classid_mask` behind `lgj_op_eq_classid`. +- **End to end through Component G, the independent instrument:** `native_hop` 1 %/65 536 **24 798 → 7 120 (#39) → 34.4 µs** (**720×** vs original); 1 %/4096 479.0 → 1.9 µs (246×); 25 %/4096 8.5×; 25 %/65 536 6.7×. **The ordering the component was built to test has inverted** — native is now fastest at every configuration, 2.0×–13× ahead of the best scalar arm, having begun the arc slowest at every configuration by 2.6×–165×. The two instruments agree: the Rust probe measures the sweep at 6 754–8 118 µs at 1 %/65 536 against JMH's 7 120 µs. +- **Honesty on the JMH re-run, as the PR states it:** noisier than the previous one — `classidScan` at 1 %/4096 reports 25.6 ± 61.9 µs, an error bar larger than the score — so the SCALAR absolutes in that table are weak. Native's own errors are tight (1.9 ± 0.3, 34.4 ± 8.1) and a 720× change is far outside container noise. +- **Deferred:** REUSE — memoising a per-row mask across many hops on the same `(store, classid)`. Named as "a caching design with its own invalidation questions, and deliberately not this function's". PR #41 attempts exactly this the same day. +- **Docs:** `lgj_hop`'s "Kernel composition" block still described the **pre-#39** design (one full-width classid sweep per facet, a scratch buffer "REUSED across every participating facet") — #39 changed the code and left the prose. Rewritten to the gather with both superseded shapes named. The overflow guard kept as an explicit check rather than dropped: the gather no longer needs `n`, but row indices are still cast to `usize`. `ISSUES.md` `ISS-LGJ-HOP-SWEEPS-FULL-POPULATION` closed; `bench/RESULTS.md` § G. +- **Gates (as the PR claimed them):** lgj-abi **134/134**; AllTests 304, GraphHopTest 66 (incl. G3 at its unchanged allocation floor), TradesParity 12, TradesAllocation 3, BricksAuth 62 = **447 Java checks**; `clippy -D warnings` + `fmt` clean. No ABI change, no signature change, no minor bump. +- **Confidence:** high — the 60-configuration probe asserts byte-identical output at every point and is independently corroborated by Component G, and the falsified prediction is recorded with its mechanism rather than quietly dropped. Backfilled 2026-08-27 from the PR body and diff, not written at merge time. + +## PR #39 — lgj-abi: hop sweeps the store ONCE, not 32 times — 3.5× at scale (merged 2026-08-27, merge `7be27be`) + +- **Added:** `lgj_hop` (`native/lgj-abi/src/exports.rs`, +78/−46) no longer loops `for facet in 0..32 { sweep(all n rows) }`. It calls `simd_rowstore_facet_match` **once** — all 32 facets per row, one `U32x16::eq_bitmask` per 64-byte chunk via `MultiLaneColumn` — then walks **src's set rows** rather than every row × facet, taking only facets that both matched and participate (`facet_bits[row] & effective`). +- **What it acts on:** `ISS-LGJ-HOP-SWEEPS-FULL-POPULATION`, opened hours earlier by #38's Component G. The PR is explicit that the measurement authorises it and nothing here is taste. +- **The arithmetic is the argument, and it is not "too many rows":** 32 × 65 536 strided `u32` compares is ~2 M operations, nowhere near the 24 ms observed — but **32 passes over a 33 MB store**, each re-reading every 512-byte row to look at 4 bytes of it, is ~1 GB of memory traffic. **The loop ORDER was the cost, not the row count** — which is what makes this a re-ordering rather than a new kernel. +- **Locked:** no new kernel. `simd_rowstore_facet_match` already existed and is the same sanctioned `ndarray::simd` surface (`abi.md` §8) — it was being consumed the wrong way round. No ABI change, no signature change, **no minor bump**: an internal re-ordering behind an unchanged contract. +- **Measured, same instrument, same command:** `native_hop` 479.0 → **374.7 µs** (1 %/4096, 1.28×), 24 798.3 → **7 120.3 µs** (1 %/65 536, **3.48×**), 521.2 → 375.8 µs (25 %/4096, 1.39×), 23 633.9 → **8 076.9 µs** (25 %/65 536, **2.93×**). **The control matters:** the two scalar arms are untouched code and moved <9 %, slightly *slower* (`classidScan` 150.6 → 164.2 µs; `facetMatches` 7 142.3 → 7 698.8 µs at 1 %/65 536) — so the native gain is not a faster host, and if anything is understated. JMH's banner reports a different CPU string between runs, which is why the control arms are quoted rather than the box assumed identical. +- **Deferred — and this is what #40 falsifies:** at 1 %/65 536 native is still **43×** the best scalar arm (7 120 vs 164 µs). `simd_rowstore_facet_match` still sweeps the whole population: the DECODE half is now frontier-bounded, the COMPARE half is not. The issue was regraded **RESOLVED IN PART**, not closed. The next rung — gather per src row, O(frontier) — was called "**not obviously better**, because a dense frontier should favour the sweep's sequential access", a real density crossover to be measured rather than judged. **#40 measured it and found no crossover at any of 60 configurations.** +- **Why not both at once (the PR's own reasoning, and it holds):** the one-pass change is bounded, needs no new kernel, has no crossover, and is strictly less work at every point in the measured space; the gather rewrite is none of those. Landing them together would make a regression in either impossible to attribute. +- **Docs:** `ISSUES.md` regraded with what remains open and why the split; `bench/RESULTS.md` § G carries the before/after table, the control-arm reasoning, and the named remaining gap — same commit. +- **Gates (as the PR claimed them):** lgj-abi **134/134** (the hop's own aliasing and semantics tests among them); AllTests 304, GraphHopTest **66** including **G3 at the identical 384-byte allocation floor**, TradesParity 12, TradesAllocation 3, BricksAuth 62 = **447 Java checks**; `clippy -D warnings` + `fmt` clean. +- **Confidence:** high on the direction and the control-arm reasoning; the absolutes inherit #38's container noise caveat. The PR left its own `lgj_hop` doc comment describing the pre-change design — caught and fixed one PR later by #40, not by this one. Backfilled 2026-08-27 from the PR body and diff, not written at merge time. + +## PR #38 — bench Component G — the F-PARITY harness, measured; W8 FALSIFIERS + POLICY closed (merged 2026-08-27, merge `b572457`) + +- **Added:** `bench/.../G_HopExecutionBoundary.java` (254 lines) — native `lgj_hop` (mask in, mask out, no row id ever produced) against the two scalar oracles **preserved verbatim** from `GraphHopTest`, reused rather than rewritten precisely because they are already cross-checked against each other AND against native on every change, so a divergence here is real rather than a fresh transcription bug. Swept on **both** axes §3.8 requires: rows {4096, 65536} × frontier {1 %, 25 %}; all three arms must agree on the destination set in `@Setup` before anything is timed, plus an anti-vacuity guard refusing a fixture whose hop reaches nothing. Own `bench/results/jmh-results-G.csv` — different fixture, different axes, deliberately not merged into the A–F tables. No production code touched. +- **Scoping first, because it changed the work:** §12 marks exactly **two** of eight `F-*` as [W8]; the other six (`F-PAR`/`F-ORD`/`F-ONE`/`F-SPARSE`/`F-ACK`/`F-LAND`) are pre-registered for the COMPUTE wave, where the board's "spec §12 F-* pre-registrations" wording had implied all eight. **F-HYDR was already shipped** (GraphHopTest's G3 gate, allocation flat at a 384-byte floor across 10-vs-500 rows). **POLICY was discharged at the A3 freeze (PR #20), not last** — every §3.9 artifact exists dated 2026-08-18 (root `CLAUDE.md` incl. §13, `EPIPHANIES.md:683` storno, the STATUS_BOARD row, LATEST_STATE, PR_ARC_INVENTORY); the ladder's `… → FALSIFIERS → POLICY` ordering is misleading and the board carried POLICY as remaining for nine days. +- **The finding this arc exists for.** Native is slowest at **every** configuration, **2.6×–165×**: `java_scalar_classidScan` 8.5 / 150.6 / 204.0 / 7 173.6 µs, `java_scalar_facetMatches` 394.9 / 7 142.3 / 433.8 / 10 071.5 µs, `native_hop` **479.0 / 24 798.3 / 521.2 / 23 633.9 µs**. **The ranking is the less interesting half — the SHAPE is the finding:** flat in frontier density (479 → 521 at 4096; 24 798 → 23 634 at 65 536 — a 25× bigger frontier costs *nothing*) and linear in population (16× the rows → ~52× the time). A hop whose cost tracks the population it ignores rather than the frontier it starts from is doing full-population work. +- **Root cause localized, in the PR, not deferred to a later reader:** `exports.rs:1589-1599` builds the classid mask across the whole population once per participating facet — 32 full-width sweeps per hop, ~2.1 M strided classid reads at 65 536 rows before one edge is decoded — and only then intersects with `src`. Filed as `ISS-LGJ-HOP-SWEEPS-FULL-POPULATION`. +- **Deferred deliberately, with the reason stated:** the remedy is a KERNEL change, and §12 scopes W8's F-PARITY to "seeds the HARNESS only" on a component §3.8 declares non-gating — a kernel rewrite inside a bench commit widens the PR past what the measurement authorises. Acted on the same day by #39. Also absent deliberately: the Vector-API arm §3.8 calls "optional" — it would be a second implementation of the kernel, and building one before the scalar arms had been measured is the taste-before-measurement §3.8 forbids. +- **What it does NOT say:** not a verdict on mask-native execution. Allocation independence is pinned separately (G3) and unaffected; the no-row-id guarantees are structural. This is the throughput-placement axis §3.8 says a measurement decides. +- **Caveats carried, not buried** (in `RESULTS.md` and `ISSUES.md` both): the per-call `Engine.createMask` + close has no scalar analogue and was **not** isolated — implausible as the story at 470 µs on 4096 rows and it would not scale with `rows`, named anyway. Native absolutes are noisy (±12 844 on 24 798) on a shared 4-vCPU container: the ordering is robust, the absolutes are not. One machine, one run. The first run overwrote `jmh-results.csv`; caught by `git status` and restored. +- **Also corrects a stale cross-repo claim, in the file that exists to prevent them:** `WAVE_STATUS_CHECKLIST.md` said `ruff_r2il` PR1/PR2 were unmerged and PR2 was a "drill-down proposer". Measured against the repo: PR1 **DONE** (`CORPUS-PROFILE-RESULT.md`, 100.00 % inline fit on all four binaries), PR2 **DONE** (`ORACLE-RESULT.md`, **zero mismatches over 35,946 matched op sites**), and PR2 was never a drill-down proposer — its gate deliverable is the round-trip oracle. The real open item is **O6** (the `MemorySpace` schema-widening decision), which scopes O5/PR3's `ogar_codebook` mint. Wrong text quoted rather than deleted, per append-only. +- **Docs / board hygiene, same commit:** `STATUS_BOARD` flipped to **D-LGJ-W8 COMPLETE**, the `ISSUES` entry, the checklist correction, `bench/RESULTS.md` § G, `bench/README.md`. +- **Gates (as the PR claimed them):** bench compiles clean under `run.sh`'s exact command line (`javac -proc:full`, JDK 26.0.2); Component G's `@Setup` cross-check passed at all four configurations — the harness gating itself; no production code touched. +- **Confidence:** high on the shape (flat-in-density, linear-in-population is a structural signature, not a noise artifact) and on the localized root cause, which the next three PRs each act on; low on the absolutes by the PR's own caveat — one machine, one run, ±12 844 on 24 798. Backfilled 2026-08-27 from the PR body and diff, not written at merge time. + +## PR #37 — board: pin the wave-status checklist (the LGJ wave is done; ghidra-g1-g2 is superseded) (merged 2026-08-27, merge `8da1dae`) + +- **Added:** `.claude/board/WAVE_STATUS_CHECKLIST.md` (new, 142 lines, the only file touched) as the durable answer to a recurring cross-session conflation, operator-corrected 2026-08-27: sessions kept restating that "one lance-graph-java wave is ~90 % done" and attributing the remainder to `wave-ghidra-g1-g2`. **Wrong on both halves.** The substrate+facade arc (`D-LGJ-A…AUDIT` 12 rows, `W1`–`W7`, the mask-native sweep `SWEEP-5`…`SWEEP-8` = ABI minors 5–8) is COMPLETE; `wave-ghidra-g1-g2` is SUPERSEDED (2026-08-18), its own file saying "do not dispatch under any circumstance", the capability having shipped upstream as `AdaWorldAPI/ruff` PR #94 (`crates/ruff_r2il`). The genuine ~90 % item is **D-LGJ-W8**: AUDIT→SPEC→COUNCIL→FREEZE→SUBSTRATE→FACADE→GRAPH MIGRATION all done, **FALSIFIERS** and **POLICY** remaining — 7 of 9 rungs, entirely in-repo, nothing upstream blocking it. +- **Locked:** the two `G1`/`G2` name collisions that fed the confusion are named and separated in the doc — the D-LGJ-W5 graph-consumer workers G1/G2 (both DONE, PR #18) and the D-LGJ-W8 spec gates G1/G2/G6/G9/G11 — neither of which is `wave-ghidra-g1-g2`. The real Ghidra/R2IL critical path is placed upstream (`ruff_r2il` PR2 → PR3, also gate #3 of the blocked `wave-ogar-machine-pm1`, repointed 2026-08-18 from the old unsatisfiable "Ghidra G1+G2 merged" wording). +- **The measured negative, recorded because the opposite reads as in-flight:** `native/lgj-abi/Cargo.toml` `[dependencies]` is exactly two local path deps — `ndarray`, `lance-graph-contract`; no `r2sleigh`, no `ogar-r2il`, no `r2conc`. Every apparent Ghidra hit across Rust, Java and `docs/` is the substring `opcode`, naming this crate's own `LgjOpCode` ABI vocabulary. The only real P-code content is `valhalla-lab/reproducers/R12_GhidraPcodeVocabularyVsCliff.java`, wired into nothing. The seam is verified and the vocabulary measured (PR #34), but **no binding exists.** +- **Method, as the PR states it:** verified against three independent reads not allowed to see each other's sources — the seven `.claude/waves/*.md` files, the four board files, and a git + source census that trusts no doc; all three agree. Statuses are copied from `STATUS_BOARD.md` / `LATEST_STATE.md`, not re-derived. The census the file banks: 8 Rust files / 7,444 lines / 134 `#[test]`; 33 Java files / 4,317 lines / 14 suites; three consumer modules each with its own suite. +- **Deferred:** nothing recorded — the file is a status pin, not a plan. +- **Docs:** the new checklist is itself the doc; no other board file changed in the PR. +- **Gates:** docs only, no code gates. The PR states explicitly: no code, no ABI, no layout, no test touched. +- **Why this one gets an entry at all** (the termination clause does not apply): the PR's content is a CORRECTION of a standing cross-session claim plus a supersession ruling, not board hygiene recording prior PRs. A future session needs the "why" — which is exactly the test the clause turns on. +- **Confidence:** high on the correction itself (three-way agreement, and the dependency/substring negatives are mechanically checkable); the per-rung W8 statuses are copied from the board rather than independently re-verified here, which the PR says in its own Method section. Backfilled 2026-08-27 from the PR body and diff, not written at merge time. + +## PR #36 — board: one substrate, five gloves — Ghidra is the glove, not the model (merged 2026-08-26, merge `79885e7`) + +- **Added:** one `EPIPHANIES.md` entry (100 lines, the only file changed), `E-ONE-SUBSTRATE-FIVE-GLOVES-GHIDRA-IS-THE-GLOVE-NOT-THE-MODEL-1`, status DOCTRINE [OPERATOR-FRAMED] — naming the vision the r2il/r2conc/ogar-loco arc has been building toward so it stops being re-derived each session. The claim: every product framing — bring-your-own-code Foundry substrate, RE/security-analyst platform, zero-trust sandbox (whitelist-only, malware-scan-before-run, autonomous alerting), the C64-game-into-ogar-loco dream, stone-age-Java bare-metal — is the SAME three steps (lower arbitrary code → R2IL, intake once; address it in ogar-loco by ordinal, not object; execute zero-copy through the mask via r2conc). One substrate, a policy/render **glove** on top; only the glove differs. +- **Locked:** "Ghidra as Java" resolves against R12 (PR #34) — the p-code vocabulary cannot flatten (payloads 0/2, ordinals 3/3), so the answer is the inversion the stack keeps re-deriving: **Java STEERS, Rust DECODES+EXECUTES, the seam carries ordinals not objects.** What remains of Ghidra at runtime is one arm — `libsla` decode — with §7.8's hexagon as its replacement path. Also fixed as an invariant for the zero-trust glove: **scan-then-execute, never execute-then-scan** — the malware mask runs on the lifted R2IL *before* `step`; the sandbox executes only `lifted ∩ whitelist`. +- **A correction applied inside the PR (codex P2), recorded rather than amended away:** the first draft banked "2.1 ns/op through Panama into ndarray" — both halves unsupported by the committed reproducer. R7 explicitly refuses to bank a throughput number (1.76–3.48 s, a 2× spread) and its `sweep` is pure in-JVM `MemorySegment.get`, no FFI and no ndarray call, so it cannot support an "into ndarray" claim at all. What R7 does bank, and what the entry now rests on, is **zero per-op allocation — 960 B fixed for 10⁹ ops**, byte-identical across three runs. The doctrine's load-bearing evidence is R12's flatten measurement, not a ns/op. The PR carries two commits for this reason. +- **Deferred:** a real ndarray-through-Panama throughput measurement — the entry states it would need its own committed reproducer and does not exist yet. +- **Docs:** the epiphany is the doc; cross-refs `E-LGJ-GHIDRAS-SEAM-IS-AN-INTERFACE-…` (R12, PR #34) and lance-graph `r2il-machine-semantic-contract-v1.md` §7.8. +- **Gates:** docs only, no code gates — the diff is one board file, no Rust, Java, ABI or test touched. +- **Confidence:** high on the framing and on R12's measured seam, per the entry's own Confidence line; the throughput half is explicitly NOT pinned after the codex P2 correction. Backfilled 2026-08-27 from the PR body and diff, not written at merge time. + +## PR #35 — R12-observed's JDK line said the tool options, not the JDK (merged 2026-08-25, merge `358809c`) + +- **Added (a CORRECTION, not a capability):** the one-line provenance fix to `valhalla-lab/reproducers/R12-observed.txt`, banked one commit earlier in PR #34. **What was wrong:** the file's `JDK:` line had captured `JAVA_TOOL_OPTIONS`' startup notice — `Picked up JAVA_TOOL_OPTIONS: -Djavax.net.ssl.trustStore=… -Dhttps.proxyHost=… -Dhttp.nonProxyHosts=…` — instead of the first line of `java -version`. So the one field identifying WHICH BUILD produced the measurement identified nothing, and dragged a wall of proxy and truststore settings into a committed evidence file. **What replaced it:** `JDK: openjdk version "27-jep401ea3" 2026-09-15` — which is also the build the R12 epiphany cites by name, so the two now agree instead of one being noise. Diff is exactly 1 insertion / 1 deletion in 1 file. +- **Locked:** the reasoning for making it its own PR rather than a quiet amend — an observed file's entire job is to be the durable record of a run, so a provenance line naming the wrong thing is a **defect in evidence, not a typo**. The PR also names the mechanism by which it got through: a claim nothing checks, the repo's own falsifiability rule applied to itself. +- **Deferred:** nothing recorded. +- **Docs:** none beyond the evidence file itself; no board file changed. +- **Gates:** docs/evidence only, no code gates — the measurement rows in `R12-observed.txt` are untouched, only the provenance line changed. +- **Non-finding stated by the PR:** no secret leaked — the captured text is a localhost proxy port and truststore paths, not a credential. +- **Confidence:** high; the change is a single line and both the wrong and the right value are visible in the diff, so nothing here rests on narration. Backfilled 2026-08-27 from the PR body and diff, not written at merge time. + +## PR #34 — R12: the Ghidra seam is an interface, and its P-code vocabulary cannot flatten (merged 2026-08-25, merge `23369f8`) + +- **Added:** the Ghidra end of the `r2il-machine-semantic-contract-v1` arc (lance-graph #1027, merged), driven while a sibling session works W0–W4. Five files, 304 insertions, no deletions: `valhalla-lab/reproducers/R12_GhidraPcodeVocabularyVsCliff.java` (120 lines), the banked run `R12-observed.txt` (49 lines, including the VM's own `PrintFlatArrayLayout` output verbatim), a `reproducers/README.md` section, plus `EPIPHANIES.md` `E-LGJ-GHIDRAS-SEAM-IS-AN-INTERFACE-AND-ITS-VOCABULARY-CANNOT-FLATTEN-1` and a dated `LATEST_STATE.md` entry — both board artifacts in the PR's own commit. +- **Locked, finding 1 — the seam is an interface, no core fork required.** An open unknown flagged twice in-session and never checked, gating the entire Java half. Traced: `Language` (interface, `model/lang/Language.java:29`) `.parse(MemBuffer, ProcessorContext, boolean)` → `InstructionPrototype` (interface, `:35`) `.getPcode(context, override)` → `PcodeOp[]`. `InstructionDB.getPcode()` (`:608-628`) does nothing but delegate; `InstructionPrototype` has exactly TWO implementations (`SleighInstructionPrototype`, `InvalidPrototype`); `SleighLanguage` constructs the real one at exactly ONE site (`SleighLanguage.java:392`); `Instruction` is itself an interface with an `InstructionStub` already in tree. A third implementation is the insertion point, and the answer is favourable. +- **Locked, finding 2 — the vocabulary cannot be carried as value classes; its identity can.** Field shapes TRANSCRIBED from `Varnode.java:51-54` and `PcodeOp.java:102-105` (not invented), run through R2/R4's harness on the JEP 401 EA build with `-XX:+PrintFlatArrayLayout` so the **VM** reports element sizes rather than the program asserting them: `VarnodePayload(int,int,long)` 16 B and `PcodeOpPayload(int,long)` 12 B do NOT flatten; `VarnodeNarrow`, `VarnodeRef(long)`, `PcodeOpRef(long)`, `InstructionRef(long)` all flatten at VM element size 8. Banked as **refs flat 3/3, payloads flat 0/2**. The two Payload rows are the OPTIMISTIC lower bound — every reference deleted; the real `Varnode` also holds an `Address` and the real `PcodeOp` a `SequenceNumber`, a `Varnode[]` and a `Varnode`, so **a 2-input `PcodeOp` is five heap objects** — *ONE ROW IS NOT ONE JAVA OBJECT* at its worst. Verdict: the W5 facade ADDRESSES the vocabulary rather than carrying it, which needs nothing new — the same result `LaneId`/`Ordinal`/`MaskId` already rely on, and the same reason `RowRange` (16 B) does not flatten. W5's central question answered before W5 starts. +- **The unanticipated part, named as an option and deliberately NOT proposed as the design:** `VarnodeNarrow` (`spaceId:u8`, `size:u8`, 48-bit offset) also flattens, so 8 bytes carry a varnode's real CONTENT rather than a pointer to it — a descriptor reading space and size with no lane round-trip, bounded by exactly one condition (a 48-bit offset), and whether that suffices is a W0/W1 address-space question, not a Valhalla one. Pre-empting W1's tenant carving from this side is what the plan's own R1 rule ("no private object graph then serialize") forbids one layer up. Also noted: the single-`long` refs come back `NON_ATOMIC_FLAT` while multi-field `VarnodeNarrow` is `ATOMIC_FLAT` — both flatten at 8, only the tearing guarantee differs. +- **Deferred:** everything downstream — no `InstructionPrototype` implementation, no facade types, no descriptor mint; those wait on W1's tenant spec. Nothing swapped, nothing minted, no layout touched; measurement and source trace only. +- **Docs:** `reproducers/README.md` R12 section (question / method / measured table / verdict); EPIPHANIES + LATEST_STATE in the same commit, cross-referencing lance-graph `r2il-machine-semantic-contract-v1.md` §2 and §6 W5. +- **Gates:** `lgj-abi` 134 lib tests green — unchanged, and nothing in the PR touches it (no Rust or Java source file is in the diff). R12 compiles and runs on `/opt/jdks/jdk-27` (JEP 401 EA); the run is banked verbatim in `R12-observed.txt`. +- **Confidence:** high on both halves — each is a cited source location or a VM-reported number, and the evidence file is committed rather than narrated. One caveat, corrected the same day by PR #35: the banked file's `JDK:` provenance line initially named the tool options rather than the JDK, so as merged this PR's evidence did not identify its own build. Backfilled 2026-08-27 from the PR body and diff, not written at merge time. + +## PR #33 — Land the stranded ABI minor 8 on main + board hygiene (merged 2026-08-25, merge `23096ce`) + +- **Landed (the stranded content, not hygiene):** the two commits carrying ABI minor 8 that had never reached `main`. PR #32 (minor 8) was stacked on PR #30's branch — two open PRs claiming the same ABI minor would have collided — and #30 merged to `main` FIRST, so #32 merged into a branch already absorbed. `main` therefore sat at `LGJ_ABI_MINOR = 7` while `Carving.java`'s rework, `CarvingTable` + `CarvingTableTest` (136 lines), the load-gate prefix fix in `internal/ffm/{Abi,Layouts}.java`, `abi.md` §17 and the minor-8 `abi.rs`/`kernels.rs` derivation lived only on `claude/layout-probe`, two commits ahead. Nothing was lost or divergent (`main` was a strict ancestor) — but the reviewed work was absent from the branch anyone would build, and neither PR gave any signal: both report "merged", both green. It surfaced only because a wake event prompted a `git log origin/main` check instead of trusting the merge notification. +- **Locked:** the failure is generic to stacked PRs, so it is written down twice rather than quietly fixed — `ISSUES.md` `ISS-LGJ-STACK-TAIL-STRANDED-MINOR-8` (found + resolved, double-entry) and a new `CLAUDE.md` iron rule naming the check that must come back empty: `git log origin/main..origin/`. +- **Added (board hygiene half):** `CROSS_REPO_PRS.md` (NEW, 112 lines, modeled on lance-graph's file of the same name) — this repo is gated on three upstream PRs by its own missing-capability STOP rule and had no ledger of them at all, the gating relationship living only in commit messages: ndarray **#283** (`masked_strided_group_sum`, gated minor 5), lance-graph **#1025** (`ClassView::cascade_shape`, gated minor 6 and minor 8's derivation), ndarray **#280** (the `mask_andnot` family, gated minor 4). Each entry names the missing capability AND what the hand-rolled local version would have been — the part that makes the STOP rule legible rather than a slogan; all three numbers verified against the upstream logs. `README.md` (NEW, 65 lines) — what each board file answers, the one-writer rule and its base case, the non-recursion clause, measure-then-pin, and an explicit section on what is deliberately NOT carried over from lance-graph's board (`sprint-log-*/`, `agent-tags/`, the entropy ledgers): that repo runs large worker fleets, this one has not, and an empty ledger read as authoritative is worse than an absent file. `STATUS_BOARD.md` gains the minors 5–8 sweep arc (it stopped at 2026-08-18 and carried nothing for the sweep, including the two operator corrections that shaped it). `ISSUES.md` also gains `ISS-LGJ-FACETSCHEMA-PAIR48` (open, upstream-owned, verified at `facet_schema.rs:34` and `class_view.rs:1168`), noted honestly as possibly not a defect — `Pair48` has real consumers (`helix` `Signed360`, `cam_pq` `[u8; 6]`, both genuinely 48-bit), so the open question is whether it and `CascadeShape` are one question asked twice; this repo consumes the contract, it does not arbitrate it. `AGENT_LOG.md` gains the session including both owned mistakes. +- **Deferred:** `ISS-LGJ-FACETSCHEMA-PAIR48` stays open and upstream-owned — nothing resolved here. +- **Docs:** `docs/abi.md` (the minor-8 §17 and §2 text arriving with the stranded tail); root `CLAUDE.md` iron-rule addition; `.claude/board/README.md` as the board's own rule set. +- **Gates:** as the PR claimed, re-run on this branch rather than inherited — Rust **134** lib tests, `fmt` + `clippy -D warnings` clean; Java **304** checks; `OldAbiCompatTest` green against minors 1 and 4. +- **Owned mistake:** two, both logged in `AGENT_LOG.md` — the `git checkout` that destroyed uncommitted work in `kernels.rs` (carried over from the #32 session), and the stacked-PR tail this PR exists to repair. +- **Confidence:** high on the stranding narrative and the file inventory — both are visible directly in the 18-file diffstat (1086 insertions) and the parent chain (`53ce031` → `23096ce`). Gate numbers are as the PR body claimed, not re-verified. Backfilled 2026-08-27 from the PR body and diff, not written at merge time. + +## PR #32 — ABI minor 8: the register groupings served as DATA (merged 2026-08-25, merge `feba97e` — reached `main` only via PR #33; see that entry for the stranding) - **Added:** `LgjAbiManifest.{carving_count, carvings[8]}` (docs/abi.md §17) — the §14 wire encoding, previously hand-written in THREE places @@ -109,6 +216,116 @@ Reconstructed and re-verified (134 tests, same count). Disable-runs are now backed up to a file first, never reverted with `git checkout`. +## PR #31 — valhalla-lab: R11 — the physical layout is a schema; SoA lanes measured 9.2× (merged 2026-08-25, merge `d741fd9`) + +- **Added:** `valhalla-lab/reproducers/R11_LayoutIsASchema.java` (122 lines) + `R11-observed.txt` (38 lines) + a `README.md` section — 190 insertions, **zero deletions, lab-only, no ABI change**. Answers the question *"would it be difficult to nudge Valhalla/Panama through 32×12-bucket SoA?"* with a measurement and a structural finding. +- **Measured:** one-facet sweep, 65,536 rows, the same logical content under two layouts, checksum-parity-pinned across all 32 facets — AoS 512-stride (today) **12.0–13.0 ns/row** against SoA facet-lane (32 × 12-B-register buckets) **~1.30 ns/row**, i.e. **~9.2×**. Line arithmetic predicts only 4× (16 of 64 B used per line vs all 64); the rest is sequential prefetch plus 32× denser TLB coverage (256 rows per 4K page at stride 16 vs 8 at stride 512). An earlier note in this repo called the 4× *"arithmetic, not a result"* — it is now a result, and an underestimate. +- **Locked (the structural finding, which the PR body ranks above the ratio):** the layout was already DATA at every boundary except the store's constructor. ONE projector runs both layouts, selected by a `LayoutSchema` **record** — the "schema apply" is a descriptor swap, not code; no Java type changes, so **Valhalla is untouched by construction** (what crosses is still a ≤4-B group, only offsets moved); and the native kernels are already stride-parameterized (`masked_strided_group_sum`, `eq_u32_strided_to_mask`) — AoS is stride 512, an SoA lane is stride 16, one code path either way, with `LgjLaneDesc` carrying `stride_bytes` since minor 1. The ABI-side cost of a real SoA store is therefore an **additive constructor plus lane descriptors**, not a kernel rewrite and not a Java rewrite. +- **Deferred:** a real SoA store (this PR builds none). Explicitly not measured: writes/generation, and Lance's own on-disk columnar behaviour. Scope stated honestly in the body — a whole-ROW consumer inverts the preference (AoS is contiguous for "all 32 facets of one row"; SoA is the scattered one there), which is exactly why the layout belongs in data as a per-workload schema. +- **Docs:** `valhalla-lab/reproducers/README.md` (+30) is the whole doc surface; `docs/abi.md` untouched. +- **Gates:** none recorded in the PR body — no ABI or main-tree code changed, so no Rust/Java suite number is claimed for this PR. The evidence is `R11-observed.txt` (the committed run output) and the checksum-parity pin inside the reproducer. +- **Confidence:** medium-high. The diff is unambiguous (three lab files, additive only) and the structural claims about `LgjLaneDesc`/the stride-parameterized kernels are checkable in-tree; the 12.0–13.0 vs ~1.30 ns/row numbers are as the body and `R11-observed.txt` claimed them, on one machine, not re-run here. No disable-runs recorded for this PR. Backfilled 2026-08-27 from the PR body and diff, not written at merge time. + +## PR #30 — abi: minor 7 — whole-row layout probe, and the classid table made global (merged 2026-08-25, merge `53ce031`) + +- **Added:** `lgj_row_layout_probe` — for EVERY facet, the set of groupings its selected rows carry, in ONE crossing. Per facet, OR-accumulate a 3-bit set plus one bit for an unanswerable classid, then `aligned ⟺ popcount(byte) == 1 && no unanswerable bit`. One `or` per (row, facet): no comparison, no early exit, cost independent of the data. **An OR-accumulated set is exact where cheaper accumulators are not** — a sum of wire values cannot tell `{0,2}` from `{1,1}`, an XOR cannot tell `{1,1}` from `{}`; the set forgets multiplicity, which is exactly what the question does not need, and `0` stays the empty set, distinguishable from disagreement. Java side: `RowLayout` (NEW, 98 lines), `RowStore` + `FacetId` accessors, `internal/ffm/{Downcalls,Engine}.java` wiring, `FacetSumParityTest` (58 lines). Rust side: `native/lgj-abi/src/class_view_provider.rs` (NEW, 115 lines), `exports.rs` + `kernels.rs` growth, `abi.rs` minor bump. Plus `valhalla-lab/reproducers/R10_SchemaAlignsWithStorage.java` + `R10-observed.txt`. +- **Corrected (shape, not output):** the `classid → grouping` table was on `RowStore` — one 64 KiB copy per dataset. The same classid means the same class in every SoA, so the resolution is dataset-independent, provably so here since `FixtureClassView` is a unit struct with no per-store state. Hoisted to a process-global `LazyLock` in `class_view_provider` (the 44 lines deleted from `rowstore.rs` are that move). The answers were already right; the placement implied two datasets could disagree about what a classid carves into, which the address space does not permit. +- **Locked:** the table captures **layout only** — meaning, RBAC, ontology category and render template are separate resolutions off the same address, none belong in the table and none can be inferred from it, stated in `abi.md` so it does not accrete. R10's result: raw storage bytes, a Panama `MemoryLayout`, and a Valhalla value class decode every register identically, each layout describes exactly 12 bytes, and the schemas genuinely read differently so the agreement is not trivial — with the flattening split pinned, the whole 12-B schema `false` (and cannot be), one group (2 / 3 / 4 B) **`true`, all three**. **The schema bolts on at the GROUP, not the register:** `12 = 6×2 = 4×3 = 3×4` means the largest group in any carving is 4 bytes, half the flattening budget, while the register is 12 and the facet 16 — neither of which Java can flatten or needs to. +- **The probe paid immediately:** a test asserting a `maskOfFacetClass(facet 3, …)` selection is fully aligned **failed**, and the expectation was wrong rather than the code — that mask constrains facet 3 only, so the other 31 facets carry whatever classids the generator gave them. Measured **1 of 32 facets aligned**. Precisely the confusion a whole-row probe exists to remove. +- **Deferred:** nothing recorded. +- **Docs:** `docs/abi.md` +67 (the probe, the global table, the layout-only statement); `valhalla-lab/reproducers/README.md` +23. +- **Gates:** as the PR claimed — **132 Rust · 288 Java** · `clippy -D warnings` clean · `fmt` clean · `abi 0.7`. +- **Confidence:** high on the structural claims (the `rowstore.rs` → `class_view_provider.rs` move and the new probe are directly legible in the 15-file diffstat, 734 insertions / 58 deletions). The 1-of-32 measurement and the gate counts are as the body claimed, not re-verified; no disable-runs recorded for this PR. Note also that this PR merged to `main` BEFORE the minor-8 PR stacked on its branch — the stranding PR #33 then had to repair. Backfilled 2026-08-27 from the PR body and diff, not written at merge time. + +## PR #29 — abi: memoise the carving resolution — per dataset, and per population (merged 2026-08-25, merge `b0e84e2`) + +- **Added:** both memos the resolved sweep was re-deriving, at their two natural lifetimes. **Per dataset** — `RowStore` gains a `OnceLock` `classid → grouping` table built on the first resolved sweep; `class_id_for` narrows a `u32` classid to `u16`, so it is 65,536 one-byte entries (`0` = no ClassView answer, else wire+1): **64 KiB, 65,536 ClassView calls once, never again** — bounded and one-off against a per-row consult that is unbounded in sweeps. `OnceLock` rather than `LazyLock` because the resolver is supplied by the caller: first caller wins, every later one reads. **Per population** — the grouping itself, memoised INSIDE `MaskWords` (`registry.rs`), so it is read under the same lock that guards the words and a resolution can never be observed against a population it was not computed from. +- **Locked (three properties that make it safe, not merely fast):** **keyed by facet, not merely cached** — different facets of the same rows carry different classids and can resolve differently, so a memo holding only "the grouping" would answer a question about facet 3 with facet 7's answer, *wrong, not stale*; **invalidation cannot be forgotten** — it lives in `write_mask()` and `lock_masks_ordered()`, the only two ways to obtain the right to mutate a mask, and any writer invalidates whether or not it changes a bit (conservative, and impossible for a new mask-mutating op to skip); **filled under the READ guard** — the obvious alternative (compute under read, upgrade to write, store) is wrong twice, since it would clear the very memo it is storing and, between dropping the read and taking the write, another thread could change the population so the stored value would describe rows that no longer match. The read guard IS the interval in which the population is stable — hence `AtomicU64` (`resolved_carving`) rather than a plain `Option`, encoded `0` = empty else `PRESENT | (facet << 8) | wire` with `CARVING_PRESENT = 1 << 63`, so **facet 0 / grouping 0 is a real answer rather than reading as an empty memo**. +- **Disable-runs, five, all red-then-green as the body claimed:** ignore the facet key; stop invalidating in `write_mask`; stop invalidating in `lock_masks_ordered`; and drop the present bit — **both halves**. That last one is the recorded lesson: removing only the *check* leaves the bit still *set*, so the first attempt at that disable passed, and the wrong thing was the disable rather than the test. A test-only `RESOLUTIONS` counter makes the memo observable rather than asserted — the first sweep resolves, five repeats over the same population do not, a different facet does, and a rewritten population does. **A memo that never hits is overhead; one that hits after a write is a wrong answer** — both halves falsifiable. Pinned in-tree by `the_memo_is_keyed_by_facet_not_merely_cached`, `facet_zero_wire_zero_is_a_real_answer_not_an_empty_memo`, `every_legal_facet_round_trips_through_the_memo_encoding`, `handing_out_mutation_rights_invalidates_the_memo`, `the_ordered_multi_mask_lock_invalidates_too`. +- **Deferred:** nothing recorded. (The per-dataset `RowStore` table shipped here is superseded one PR later — PR #30 hoists it to a process-global `LazyLock`, on the ground that a classid is a global address; the memo's *placement* was the correction, not its answers.) +- **Docs:** `docs/abi.md` +43. +- **Gates:** as the PR claimed — **129 Rust · 277 Java** · `clippy -D warnings` clean · `fmt` clean. +- **Confidence:** high. The atomic encoding, the present bit, and all five named falsifiers are visible directly in the `registry.rs` diff (183 lines added), so the safety argument is checkable rather than asserted; the gate counts and the red-then-green disable observations are as the body claimed, not re-run here. Backfilled 2026-08-27 from the PR body and diff, not written at merge time. + +## PR #28 — abi: minor 6 — ResolvedCarving, and all three followups closed (merged 2026-08-25, merge `671ea98`) + +- **Added:** ABI minor 5→6 — `lgj_reduce_facet_sum_resolved` (`docs/abi.md`, +110 lines) resolving every selected row's `classid → ClassId → ClassView::cascade_shape`, requiring agreement across the population, sweeping monomorphically, and **reporting the resolved grouping back** so a caller learns what it got. `RowStore.facetSum` (the verified sibling to #26's unchecked `facetSumAs`), `FacetSum` result carrier, `Carving.ofWire` (throws on an unknown value, so an unrecognised grouping can never be read as a known one), `Downcalls`/`Engine`/`Status` wiring. 14 files, +832/−114. +- **Locked:** `Carving` is `pub type Carving = CascadeShape` — the local enum minted in #26 was **a re-mint of an existing contract type**, the parallel-object-model anti-pattern, and the PR names it as such. Only the u32 wire encoding stays local, pinned **by group count** so an upstream variant reorder cannot silently re-map it (`the_wire_mapping_is_pinned_by_group_count_not_declaration_order`). The question is asked once at the population's edge, never inside the sweep. G11 contract-import fence widened by one module (`facet`), deliberately. Upstream dependencies (`ClassView::cascade_shape` in lance-graph, `masked_strided_group_sum` in ndarray) landed FIRST per the missing-capability STOP rule — the PR body says "merge those first". +- **The fixture had to be made able to fail:** `FixtureClassView::cascade_shape` now varies by `class % 3` (cycle `G6D2, G4D3, G3D4`) rather than answering the trait's constant `G3D4` zero-fallback. A constant answer makes every population trivially homogeneous, so the resolve-to-one guard could never fire and its test would pass for an implementation that never checked. The paired half is stated in the fixture's own doc comment: classids 3 and 6 **share** a grouping and must still resolve, or the refusal degenerates to "reject every multi-class population". Empty resolves to `None`, never to a default — pinned by `an_empty_population_resolves_to_nothing_rather_than_a_default`. +- **Followup 3 closed at the right layer:** the scalar kernel #26 shipped as a named `ndarray::simd` gap is now one delegating call into `ndarray::simd::masked_strided_group_sum`; the "SCALAR, and deliberately so" doc block is deleted, not merely amended. +- **Deferred:** generalising to an op-code on ONE reduce symbol (mirroring `lgj_plan_eval`'s `LgjOpDesc`, `sum` as op-code 0) — stated with its firing condition rather than as an aspiration: **a second reduction must NOT become a second symbol.** #26's open product question ("is `sum` R8's checksum escaping the laboratory?") is answered rather than dropped: it is the only mask-**consuming** op over the register, and the cheapest that cannot be faked from outside, which is why it doubles as the parity oracle for both. +- **Docs:** `docs/abi.md` (+110) carries the new symbol and the minor-6 ledger; `.claude/board/EPIPHANIES.md` (+87) in the PR's own commit. +- **Gates (as the PR claimed):** 123 Rust · 277 Java · **7 old-ABI compat checks per historical library**, now gating minor 6 too · clippy `-D warnings` clean · fmt clean · `.so` rebuilt first · runtime reports `abi 0.6`. +- **Disable-runs:** six red-then-green on the resolver and wire mapping — ignore mixing, skip an unanswerable classid, invent a default for empty, reorder the wire mapping — plus the paired can-still-resolve halves. +- **Owned mistake:** the local `Carving` enum was committed in #26 "while writing a note about not claiming authority" — the contract's `CascadeShape` had carried the three groupings, the algebra, and the class-conditioned sentence all along. What genuinely did not exist was the `ClassView` accessor. The PR states this in its own body rather than quietly aliasing the type. +- **Confidence:** high on the diff-verified facts (the `Carving` alias, the varying fixture, the `masked_strided_group_sum` delegation, the six named resolver tests all appear in `git show`); the gate counts and the six disable-runs are cited as the body claimed them, not re-run here. Backfilled 2026-08-27 from the PR body and diff, not written at merge time. + +## PR #27 — abi: close the eager-init defect at minors 2-4, not just 5 (merged 2026-08-25, merge `7479c40`) + +- **Added:** one lazy `Downcalls` holder per minor — `Minor2`/`Minor3`/`Minor4`, joining #26's `Minor5` — initialised on first *access* rather than in the class initialiser. `OldAbiCompatTest` rewritten from minor-5-only and one-directional to per-minor and both-directional. 4 files, +261/−55; the Rust side untouched. +- **Reproduced against real libraries, not reasoned about:** four libraries built from this repo's own history — `bd92c58` (minor 1), `beac5de` (2), `92a0e55` (3), `e8f0ce6` (4) — with the **current** Java run against each. `SmokeTest`, which uses nothing newer than minor 1, died every time in the `Downcalls` class initialiser on the first symbol from a *later* minor: against the minor-1 library on `lgj_rowstore_open` (a minor-2 symbol), against minor 2 on `lgj_rowstore_open_with_edges` (minor 3), against minor 3 on `lgj_mask_andnot` (minor 4). **Against the minor-1 library, minor-1 operations could not run** — the additive-minor promise was not merely unenforced in that direction, it was inverted. +- **Locked:** the **14 minor-1 base handles stay eager on purpose.** A library missing any of those is not an older library, it is a wrong one, and that failure should be immediate and total. Laziness answers "this library predates the feature"; it is not a general policy. The falsifier gates each minor in whichever direction the loaded library calls for — available ⇒ the feature must actually work; absent ⇒ `AbiMismatchException` naming *that* minor, never a bare missing-symbol failure and never a failure of some other minor's feature. Both halves are required, because a gate that rejected everything would satisfy a rejection-only test. +- **Deferred:** the compat suite stays OUT of `AllTests` and skips loudly without `-Dlgj.oldlibrary` — it needs artifacts this repo does not ship; the four libraries are reproducible from the commits named above. +- **Docs:** `docs/abi.md` §2 (+35) gains the both-directions compatibility statement and the measured table — versioning is where that promise belongs; `.claude/board/EPIPHANIES.md` (+58) in the PR's own commit. +- **Gates (as the PR claimed):** 263 Java checks · **22 old-ABI compat checks across four historical libraries** — minor 1 → 4 checks, minors 2/3/4 → 6 each, all green, each reporting the right verdict for its library. +- **Disable-run, per minor rather than in aggregate:** reverting *only* minor 2 to eager while leaving 3/4/5 lazy reproduces the class-initialiser crash against the minor-1 library. A single minor regressing is caught. +- **Confidence:** high. This PR closes a gap #26 recorded and explicitly left open ("minors 2-4 share the defect and are NOT fixed here"), and the measured table is per-library evidence rather than reasoning; the diff (`Downcalls.java`, `OldAbiCompatTest.java` +110) matches the claimed shape. The gate counts and the per-minor disable-run are cited as the body claimed them, not re-run here. Backfilled 2026-08-27 from the PR body and diff, not written at merge time. + +## PR #26 — abi: minor 5 — `lgj_reduce_facet_sum`, the mask path's missing execution half (merged 2026-08-25, merge `beac2fe`) + +- **Added:** ABI minor 4→5, one new symbol `lgj_reduce_facet_sum` (`docs/abi.md` §14) — the first op that can *consume* a mask against the 12-byte facet register. `Carving` (Java enum, 3 readings), `RowStore.facetSumAs`, `FacetSumParityTest`, `Downcalls.Minor5` lazy holder, `Status` codes, plus the Rust kernel and export. 13 files, +1215/−9. +- **The finding came before the code:** reading the membrane first showed "wire the mask path into lgj-abi" was **half a task already done** — `lgj_op_eq_classid` has turned a classid column into a mask since minor 2, already through `ndarray::simd::eq_u32_strided_to_mask`, the same primitive R8 arm E′ measured. The gap was the execution half. +- **Six operator-review findings, all confirmed and fixed** — each sitting where no test was pointed, none falsified by the 117 Rust + 263 Java green gates: (1) **the feature gate was defeated by the class it guards** — `Abi.requireMinor(N)`'s javadoc promises to fail before any downcall is attempted, but every `Downcalls` handle is a `static final` resolved in the class initialiser; reproduced against a real ABI 0.4 `.so` built from merged `main`, where `SmokeTest` died in ``. (2) the normative ledger contradicted itself (minor 4 / "21 symbols" / history ending at 4 while §14 already documented `-15`) — now consistent at minor 5, 22 symbols. (3) **claimed ClassView authority it cannot verify** — a mask is an opaque population and the fixture ClassView had no carving resolver, so the method was renamed `facetSumAs` and documented as a raw reinterpretation primitive whose caller owns correctness. (4) **`i64` is not closed under the reduction** — `wrapping_add` at up to `3 × (2³² − 1)` per row under quads overflows after ~715,827,882 rows ≈ 341 GiB of 512-byte rows, inside this substrate's contemplated scale; now `i128` accumulation, range-checked once, `LGJ_ERR_SUM_OVERFLOW` (`-16`), `out_sum` untouched. (5–8) four small untruths (wrong status constant in the export doc, a header still counting 20 handles, a stranded javadoc, a complexity claim ignoring the unconditional mask scan — now `O(mask_words + popcount × groups)`). (9) an overclaimed reachability reclassified rather than deleted, since `registry` allocates masks at exactly `mask_words_for(n_rows)`. +- **Deferred, and each with its successor named:** the verified shape `classid → ClassView → ResolvedCarving → (population + its carving) → sum` recorded as the next rung rather than faked — deliberately NOT a per-row consult, "that would put the entropy straight back in the loop" (closed by #28). **Minors 2–4 share the eager-init defect and are NOT fixed here** — pre-existing, own change, own falsifier (closed by #27). The kernel is **scalar, deliberately**: `ndarray::simd` had no primitive for "gather a sub-word group out of a 512-byte-strided register under a runtime grouping and widen-accumulate", and raw intrinsics would create the second SIMD surface §8 forbids — a named gap under the W1a contract, not an unexamined choice (closed by #28). +- **Left open as a product question, flagged rather than decided:** is `sum` genuinely the first product operation, or R8's checksum escaping the laboratory? R8 proved the execution *shape*; it did not prove that "sum packed rails/triplets/quads" earns permanent ABI vocabulary. If no consumer needs facet sum, the honest move is to keep the shape and drop the operation. +- **Docs:** `docs/abi.md` (+131) §14 and the corrected minor/symbol ledger; `.claude/board/EPIPHANIES.md` (+170) in the PR's own commit. +- **Gates (as the PR claimed):** 118 Rust tests · 263 Java checks · **4 old-ABI compat checks against a real 0.4 library** · clippy `-D warnings` clean · fmt clean · `.so` rebuilt first · runtime reports `abi 0.5, simd ndarray::simd avx512`. +- **Disable-runs:** nine red-then-green, including two through the membrane and one reverting the lazy holder. `FacetSumParityTest` recomputes every expected value in Java from the public per-row accessors — the two paths share no code. +- **Confidence:** high on substance, with one provenance caveat: this entry is reconstructed largely from a body that was itself rewritten after operator review ("_Updated after operator review — six findings_"), so the six findings are recorded as the body states them and the gate counts and nine disable-runs are not re-verified here; the file list, +1215/−9, and the new `Carving`/`FacetSumParityTest`/`OldAbiCompatTest` additions are diff-checked. Backfilled 2026-08-27 from the PR body and diff, not written at merge time. + +## PR #25 — board: record R6–R8 and the twice-repeated measurement-ledger defect (merged 2026-08-25, merge `405a2a1`) + +- **Added:** one append-only `.claude/board/EPIPHANIES.md` entry (+78) for the R4–R8 arc merged in #24, which carried an entry for R4/R5 only — plus, in a second commit on the same branch (`e8f0ce6`), a calibration of the R8 FFI claim in `valhalla-lab/reproducers/{r8_report.py, README.md, R8-observed.txt}`. 4 files, +148/−48. +- **Locked — the defect that arc produced twice.** `R7-observed.txt` shipped with prose quoting a throughput range its own pinned runs contradicted; that was caught and repaired, and **R8 committed the identical defect one commit later**. Root cause is mechanical, not attentional: prose hand-copied from run N while the raw block was regenerated at run N+1, which leaves every such artifact one regeneration away from lying about itself. Repaired structurally in #24 by generating the report from the runs it just captured. +- **The repair turned out to be the right shape, not merely a safer one:** the first regeneration moved B′ ~25 % while every structural conclusion held identically. **The stability of conclusions under unstable absolutes is itself the result**, and only a regenerable artifact can expose it — a hand-pinned one hides it. +- **Two further review findings, both real:** the toolchain was not unified across the control and the arm (1.94.1 vs 1.97.1 — an escape hatch on the "bulk FFI costs nothing" claim), and the sweep-only comparison understated the lawful mask shape, which wins **end-to-end** once population construction is counted. +- **The calibration, in the diff:** "B ≈ the standalone Rust process. One bulk FFI crossing costs nothing measurable" is replaced by "**one bulk Panama crossing shows no measurable penalty at this scale**", stated that way deliberately — one generated run had the in-JVM arm *faster* than the standalone process, which means process/JIT/turbo/cache context is larger than any crossing cost, so the two arms must not be called performance-identical even though no penalty is exposed. `r8_report.py` now **detects which of the three cases a run lands in and says so** (faster than max / slower than min / ranges overlap), so the calibration is enforced by the generator rather than restated in prose — the same structural fix the entry itself is about. +- **Docs:** the R6–R8 measured architecture recorded so a future session does not reconstruct it from commit messages — JEP 401's atomicity cause for the 8-byte cliff (and that `UseArrayFlattening`/`UseFieldFlattening` are `false` by default, so the flags-on run had to be *done* rather than assumed); 10⁹ projections at 960 B total; the five-arm entropy-boundary result with its control leg. +- **Gates:** nothing recorded — no test counts claimed, and none apply to a board-and-generator change. +- **Owned mistake, this entry's own:** the PR body says "Docs-only: one append-only entry in `.claude/board/EPIPHANIES.md`. **No code, no reproducer changes.**" The diff contradicts it — the second commit changes `r8_report.py`, `README.md`, and `R8-observed.txt`. The body was accurate when written and was not updated when the calibration commit was pushed onto the same branch; recorded here rather than smoothed over, because a PR body going stale against its own branch is the same one-regeneration-from-lying shape the entry documents. +- **Confidence:** medium-high. The recorded findings are the PR body's own account of the R6–R8 arc and are not re-derived here; what is diff-verified is the file set, the calibration text, and the three-case detection in `r8_report.py`, plus the body-vs-diff discrepancy noted above. Backfilled 2026-08-27 from the PR body and diff, not written at merge time. + +## PR #24 — Valhalla/Panama layout authority + mask-native Rust execution, measured (merged 2026-08-25, merge `7165615`) + +- **Added:** `valhalla-lab/reproducers/` — five reproducers (R4–R8), each with a pinned `R*-observed.txt`: `R4_CarvingVsCliff.java`, `R5_ClassidHasNoStaticSpelling.java`, `R6_WhyEightBytes.java`, `R7_BillionOpsZeroAlloc.java`, `R8_EntropyBoundary.java`, plus the R8 native side (`r8_native.rs`, `r8_standalone.rs`, the `r8-ndarray/` crate) and `r8_report.py`. A `README.md` for the reproducer set; `.gitignore` entries for the build artifacts (`.so`, standalone binary, `r8-ndarray/target/`). +- **Locked (as the PR body reports its measurements):** the 8-byte array-flattening cliff is on TOTAL payload and the V3 carvings do not dodge it — `Pair`/`Triplet`/`Quad` flat, every `Reg12*`/`Facet16*` non-flat, so **the carving is sound as SoA and only as SoA** (N parallel rail arrays, never one `Facet[]`). `isFlatArray()` alone is not a sufficient test: `Four8AsTwo8`, 32 bytes, reports flat at VM element size 8 because its components are stored as *references* — element sizes are now pinned beside every boolean. A runtime-`classid` layout requires descriptor/accessor dispatch (R5): over 65,536 rows, projecting allocates 800 B total while hydrating a 16-byte `Facet` costs 32–104 B/row **varying by run** — the spread is the finding. The cliff is JEP 401 by design, not a version gap or a flag (R6: already the Java 27 numbers, `27-jep401ea3`; forcing all five flattening flags changes nothing). 10⁹ projected operations allocate **960 B total** (R7) — 15,000× the operations for +160 B over R5. R8: five arms, same bytes, same op multiset, checksum-identical *including a standalone Rust process with no JVM*; one bulk FFI crossing costs nothing measurable (B ≈ standalone), `D > B` is falsified, and one crossing per projection is ~30× slower (~12 ns/op) — the anti-JNI rule with a number on it. Under random classid both D′ (index lists) and E′ (`ndarray::simd` masks) recover ~4.8×, and counting the population BUILD moves break-even from ~120 passes to ~10, leaving a reusable mask where D′ leaves a materialized population the mask-native law forbids as currency. The win is specialization **placement**, not "Java beats Rust". +- **Deferred:** nothing recorded — the PR body states it touches neither `native/lgj-abi`, the Java API, nor the membrane; it is a measurement lab plus board/doc corrections. +- **Docs:** `.claude/board/EPIPHANIES.md` (+73), `AGENT_LOG.md` (+85), `TECH_DEBT.md` (+95); `java/README.md` `[restricted]`-warning count corrected six → **seven** (the board had said seven since 2026-08-17; this file was the only stale copy). +- **Gates:** as claimed in the body — all Rust arms built with `rustc 1.97.1`, `-O -Ctarget-cpu=x86-64-v4 -Cdebuginfo=0` (toolchain unified; native kernels and the standalone baseline had been 1.94.1 while the ndarray crate required 1.97.1, an escape hatch on the "bulk FFI == standalone" claim). Arm E consumes SIMD ONLY through `ndarray::simd` (`eq_u32_strided_to_mask`, `mask_or`) per `abi.md` §8. Arm E asserts its mask popcounts equal the partition scan's counts, so the two population representations are the same set. +- **Owned mistake:** R7's prose quoted a throughput range its own pinned runs contradicted (throughput is now explicitly **not banked**, only the exact 960 B), and **R8's report drifted the same way one commit after the R7 fix** — prose hand-copied from run N, raw block regenerated at run N+1. Repaired structurally rather than by hand: `r8_report.py` now derives the prose from the captured runs, so they cannot disagree. One regeneration saw B′ move ~25% while every structural conclusion held identically. Also recorded: R4/R5 code landed without its board entry — a same-commit-rule break, recorded rather than back-dated, in `E-LGJ-LAYOUT-AUTHORITY-IS-TRANSFERABLE-BUT-ONLY-ABOVE-8-BYTES-1`. +- **Confidence:** medium-high. The reproducers and their pinned `R*-observed.txt` are in-tree and the generated-report discipline is structural, but this entry is reconstructed from the PR body and diffstat — the numbers are as the body claimed, not re-verified here, and this is a real merge commit (two parents), not a squash. Backfilled 2026-08-27 from the PR body and diff, not written at merge time. + +## PR #23 — W8b: mask-native Java facade + Graph migration (merged 2026-08-18, squash `3f927c7`) + +- **Added:** the D-LGJ-W8 FACADE + GRAPH MIGRATION rungs (spec `.claude/plans/mask-native-navigation-correction-v1.md` §3.5–§3.7) on top of PR-W8a's substrate. Facade: `WideFieldMask` (record; validated `ofFacets`, zero-extending `ofMatchBits`), `RowStore.hop(int, WideFieldMask, Mask)` + `hop(int, Mask)`, `RowStore.importRows(long...)` — the ONE named import exception — `Mask.minus(Mask)`, `Mask.materializeRows()` — the ONE named materialiser — `Status −14`, `Downcalls` 20 handles / 21 symbols + `requireMinor(4)`, `Engine.LaneWindow.setU64` (the first write accessor, used only by `importRows`). Graph migration (`consumers/graph/`): a native `Mask` frontier with zero `long[]`/`Collection` fields, `from(Mask)` new, `minus(long...)` REMOVED, `rows()` → `materializeRows()`, a real `close()`. New test `MaskNativeOpsTest`; `GraphHopTest` rewritten with a reflective 3-way allowlist (fields / parameters / returns) over `Graph` AND `Edge`, the vacuous literal-true assert deleted, a G9 flagship `from(maskOfFacetClass(...)).hop(...).count()` (seed=133 → hop → 240, zero row-id values outside the independent oracle), and a G3 allocation gate (384-byte floor, flat across 10-vs-500-row frontiers). +- **Locked:** zero FFM types in any public signature; the two named exceptions are the only doors in and out of row-id space. +- **Measured, then pinned (two predictions were wrong, in the direction the worker brief's own reasoning flagged):** crossing constants pinned from measurement (ABI 0.4, release `.so`, JDK 26.0.2) — `hop` predicted 1, **measured 2** (`createMask` + `lgj_hop`); `importRows` predicted 1, **measured 2** (`createMask` + `describeMask`, per-row word writes in-process — 3-vs-29-row cost identical); `count` / `minus` / materialize-first unchanged at 1 / 2 / 1. Three stale "one native crossing" javadoc claims corrected at the source (`maskOfFacetClass`, `hop`, `importRows`). +- **Deferred:** nothing recorded. +- **Docs:** `.claude/board/{ISSUES,LATEST_STATE,STATUS_BOARD}.md` and the wave/plan supersession notes (`.claude/plans/consumer-graph-traversal-v1.md`, `.claude/waves/wave-consumer-graph.md`) appended per spec §3.7 — append-only, originals preserved, all in the PR's own commit. +- **Gates:** the `.so` was rebuilt FIRST — and the root-level copy was found STALE (pre-minor-4), the exact eager-clinit trap the repo `CLAUDE.md` names; refreshed before any suite ran. `javac -Xlint:all`: 7 pre-existing `[restricted]` warnings, 0 new. **AllTests 245 ✓ (incl. the new MaskNativeOpsTest 41 + ApiSurfaceTest) + GraphHopTest 66 ✓ + TradesParity 12 ✓ + TradesAllocation 3 ✓ + BricksAuth 62 ✓ = 388 checks.** +- **Disable-run, red-then-green:** an injected `public long[] rows()` fired exactly the G1/G8 allowlist check; restore → 66/66. +- **Confidence:** high — the crossing table is measurement-corrected prediction with attribution read from source, and the allowlist has a red-then-green disable behind it; the gate counts are as the PR body claimed, not re-run here. Backfilled 2026-08-27 from the PR body and diff, not written at merge time. + +## PR #22 — D-LGJ-W8 PR-W8a: contract dep + FixtureClassView + `lgj_mask_andnot` + `lgj_hop` (ABI minor 4) (merged 2026-08-18, squash `9019417`) + +- **Added:** the substrate half of the council-ratified mask-native correction (spec v3 §3.2–3.4, frozen in #20; PR-N = ndarray #280, merged first per the wave order). A `lance-graph-contract` path dep (`default-features = false`) — the RULING's contract inheritance, the law without the engine, with the G11 import fence honored (`class_view` / `canonical_node` / `ontology` only). New `native/lgj-abi/src/class_view_provider.rs`: `FixtureClassView`, the late-bound law provider (32 `FieldRef`s via `OnceLock`), the named seam fns `edge_participation` / `decode_mode`, and `class_id_for` as the pinned u32→u16 boundary. `lgj_mask_andnot` in `exports.rs`: dedup-before-lock aliasing discipline as a **5-branch tree** — ANDNOT is non-commutative, so `dst==b` needs a scratch copy of `a`, a real structural divergence from `mask_binop`, documented — with the kernel via `ndarray::simd::{mask_andnot, mask_andnot_assign}` plus an explicit repairing tail-clear. `lgj_hop`: mode fence first (≠0 → new status **−14**, dst provably untouched); src snapshot under a read lock released before the dst write lock (aliasing deadlock-free by construction, council S3-4); composition kernel reusing `eq_u32_strided_to_mask` for classid-match, scalar decode+scatter only, u64 bounds check BEFORE the cast per S3-6. +- **Locked:** ABI minor 3→4; `abi.md` §13 added, symbol counts 19→21, and §12's Java-layer hop composition regraded ⊘ SUPERSEDED in place. +- **Deferred:** nothing recorded — W8b (Java facade + Graph migration) named as the follower, and it landed as #23. +- **Docs:** `docs/abi.md` (+163) and the board artifacts in the same commit per §3.10's gate column — STATUS_BOARD SUBSTRATE flip, LATEST_STATE entry, ISSUES `ISS-LGJ-CLASSID-WIDTH-PIN` width-pin entry. +- **Gates:** `cargo test` **110/110** · clippy `-D warnings` clean · fmt clean · release `.so` exports **21/21** `lgj_` symbols (`nm -D`). +- **Disable-runs, G6(a)–(e), all red-then-green:** (a) decode offset +4 → exactly the 10/19/29 fixture-parity test red; (b) participation forced EMPTY → parity + provider test red; (c) tail-clear removed → the corrupted-operand repair test red; (d) mode fence bypassed → reserved-mode test red; (e) **`ptr_eq` dedup bypassed → the aliasing test DEADLOCKS** (60 s timeout kill) — the S3-4 deadlock is real and the discipline load-bearing. +- **Confidence:** high — five disable-runs, one of which reproduces a genuine deadlock rather than a failed assertion, and the symbol count is checkable against the built `.so`; the numbers are as the PR body claimed, not re-run here. Backfilled 2026-08-27 from the PR body and diff, not written at merge time. + ## PR #20 — D-LGJ-W8 A3 freeze (PR-0): ratified correction spec v3 + root CLAUDE.md + board storno (merged 2026-08-18, squash `c479f76`) - **Added:** `.claude/plans/mask-native-navigation-correction-v1.md` @@ -261,6 +478,17 @@ verified. Both bot reviewers (Cursor Bugbot, Codex) hit usage limits, no review obtained. +## PR #13 — rowstore: `generate_with_edges` — the graph-consumer wave's measured blocker, cleared (merged 2026-08-18, squash `beac5de`) + +- **Added:** `RowStore::generate_with_edges(n_rows, seed, edge_classid, edge_gate_mask, edge_radius)` in `native/lgj-abi/src/rowstore.rs` (+265) — additive, `generate()` untouched. Classid assignment is byte-identical to `generate()` (same SplitMix64 draws, same `(a >> 33) & 0xF` formula); bits 37..64 of the same `a` draw become an independent sparsity gate, so `edge_classid = 16` (outside the `0..16` range) makes the gate structurally unreachable and reproduces `generate()` exactly — **pinned by test, not just argued**. A sparse, gated subset of `edge_classid`-matching facets get a bounded local-neighbourhood target row instead of raw noise, which is what keeps a 1–2 hop BFS non-vacuous. New `native/lgj-abi/examples/graph_density_probe.rs` (+85). +- **Locked:** the blocker was a DATA-SHAPE problem, not a mechanism problem, and it was caught before any workers spawned. The wave was nearly dispatched on the strength of Decision D1a's mechanism (writable masks, existing facet-match) being sufficient — claimed twice in earlier turns — before `wave-consumer-graph.md`'s own STOP condition was re-read **in full**: plain `generate()`'s payload is uniform random noise, so a decoded 1–2 hop BFS over it saturates to nearly every row regardless of decode convention, vacuous under the wave's own falsifier #4 ("seed / 1-hop / 2-hop must be three different, non-empty, non-total sizes"). +- **Measured, not guessed:** the probe swept `(gate_mask, radius)` before any parameter was chosen. The first pass at `n_rows=1000` was too small (avg degree < 1, everything collapsed to zero); widened to `n_rows=20_000` for real signal (`gate_mask=0x0, radius=25`: seed=20 → 1-hop=30 → 2-hop=40), then re-measured at a test-suite-sized `n_rows=2000` for the pinned regression: **`seed=10 → 1-hop=19 → 2-hop=29`**, pinned as `measured_hop_counts_are_three_distinct_non_empty_non_total_sizes`. +- **Deferred:** the consumer itself. Scoped to the substrate-tier generator only, per the wave file's own rule that a generator extension is not a consumer hack — "the graph consumer is dispatchable", not dispatched in this pass. +- **Docs:** `.claude/board/{EPIPHANIES,LATEST_STATE,PR_ARC_INVENTORY}.md` in the same commit; `wave-consumer-graph.md` updated in place (STOP condition marked RESOLVED with the measured numbers, and its stale "calcify, do not dispatch" header corrected — that gate had already been lifted session-wide when W5a/W5b shipped under identical wording); `.claude/plans/lgj-soa-substrate-v1.md` and the ghidra / ogar-machine wave files touched. +- **Gates:** `lgj-abi` **90/90** (was 84, +6 new tests), fmt clean, clippy `--all-targets --all-features` clean. +- **Disable-runs, red-then-green:** dropping `rem_euclid` from the target-formula wrap → exactly the 3 target-formula tests red (transcription, in-bounds/radius, pinned regression), the other 7 green; firing the sparsity gate on classid match alone → only the transcription test red. The second result is stated precisely rather than treated as a miss: the in-bounds/radius invariant correctly stayed green, because geometry validity is orthogonal to *which* facets get the treatment — verified as the right outcome rather than a vacuous test before moving on. +- **Confidence:** high on the mechanism and the disable table, which reads as genuinely two-sided; note the squash's own commit message body is the mission-plan text from the branch's first commit rather than this PR's scope, so the entry is written from the PR body and the diffstat, and the gate counts are as the body claimed, not re-run here. Backfilled 2026-08-27 from the PR body and diff, not written at merge time. + ## PR #12 — consumer example: Bricks, mask-first authorization (merged 2026-08-17, squash `8f16e8d`) - **Added:** `consumers/bricks/` — the second consumer proof over the