Skip to content

Commit 320808d

Browse files
AdaWorldAPIclaude
andauthored
Java RowStore facade: W3 shipped (185/185, one bug found+fixed) (#8)
* Core vertical slice: docs/abi.md contract, native/lgj-abi, Java facade Ships the fully verified core of the Panama x ndarray::simd x Valhalla vertical slice (Phases A-E of the mission plan): - docs/abi.md: the normative Rust<->Java ABI contract, written before either side was implemented so both could be checked against one frozen doc instead of each other. - Five new ndarray::simd primitives (eq_u32_to_mask, gt_i32_to_mask, mask_and/mask_or(_assign), masked_sum_i32), added under ndarray's own W1a consumer contract. - native/lgj-abi: the Rust ABI crate. Generation-checked handle registry, generic SoA fixture, bulk kernels routed exclusively through ndarray::simd, 14-symbol extern "C" surface. 72/72 tests green, clippy/fmt clean, and the registry's core safety check was disable-verified (short-circuited, confirmed exactly the two guarding tests go red, restored). - java/: the Panama membrane (internal/ffm, never exposed publicly) and the public semantic facade (NativePattern/View/Predicate/ Pattern/Mask). 132/132 checks green across 8 suites, including a reflection-enforced ApiSurfaceTest that mechanically proves zero FFM types ever reach a public signature, and a LazinessTest that empirically proves the thesis: building a chain costs zero crossings, evaluating it costs exactly one, independent of row count up to 1,000,000. - .claude/: a 6-agent ensemble, 6 knowledge docs, and a full board (LATEST_STATE/STATUS_BOARD/AGENT_LOG/EPIPHANIES/TECH_DEBT/ISSUES/ PR_ARC_INVENTORY/INTEGRATION_PLANS/CODEX_REVIEW_CHECKLIST), all scoped to this repo's actual seams. A mechanical audit (D-LGJ-AUDIT) found and fixed the one real rule violation before this commit: kernels.rs::simd_popcount was calling the internal ndarray::hpc::bitwise path instead of the sanctioned ndarray::simd re-export. Deliberately NOT included: the Valhalla lab (valhalla-lab/) and the Vector API benchmark harness (bench/) — still in flight, tracked as open STATUS_BOARD.md rows, to land in a follow-up PR once reviewed with the same rigor as this slice. Generated by [Claude Code](https://claude.ai/code) * Valhalla lab: three-truths method, causal isolation, 3 real reproducers Completes D-LGJ-F. One experiment source (src/shared/), compiled twice against real JDKs -- stable JDK 26 GA (record) and the official JEP 401 early-access binary (value record) -- via a self-verifying run.sh that mechanically diffs the two Vocab.java files modulo the 'value' keyword before trusting the A/B is honest. Experiments: IdentityExperiment (semantic truth -- is identity actually unobservable), FootprintExperiment (real per-object/array/field bytes via allocation-delta + JOL where available), FfmAddressingExperiment (is the wrapper free where it touches native memory), ThesisExperiment (the mandatory headline: 65,536 rows as one native lane vs hydrated Java objects, on both platforms). Causal isolation via three additional run.sh passes: escape analysis off, and UseArrayFlattening/UseFieldFlattening toggled independently -- isolates which flag actually drives the measured difference rather than inferring it. Three real Valhalla limitations reproduced and filed under reproducers/, none of which changed the production API: - R1: @NullRestricted field on an identity class is a VerifyError (javac's fault -- no source form expresses the required strict-field init order relative to super()) - R2: array flattening has a hard 8-byte payload cliff, confirmed via -XX:+PrintFlatArrayLayout. LaneId/Ordinal/MaskId (<=8B) flatten; RowRange/Row (16B) do not. This turns "Valhalla helps descriptors, not entities" from a hand-wave into a measured VM cutoff -- and RowRange landing on the wrong side is flagged as the one place the expectation was too optimistic. - R3: the densest null-restricted array form is jdk.internal-only and generics erase flattening entirely; Foo! null-restricted type syntax confirmed not to parse, matching the earlier archaeology finding. One real defect found and fixed before landing: IdentityExperiment and the stable Platform called Class::isValue() directly on four vocabulary types with a comment incorrectly claiming it was "final API on JDK 26" -- it does not exist there at all, confirmed by a real javac failure. Fixed by routing every query through Platform.isValueClass(Class<?>), answered honestly per platform. Generated by [Claude Code](https://claude.ai/code) * Vector API bench: real JMH, cross-checked; the crossing does not always win Completes D-LGJ-G, the mission's mandated "where does execution belong" comparison -- measured, not assumed to favor the Rust crossing. Real JMH 1.37 (fork+warmup+compiler-blackholes confirmed in the log, not a hand-rolled loop -- that lives in valhalla-lab and is labelled as such there). Four components, cost kept strictly separate per the mission brief: A_DowncallOverhead (bare crossing, no work), B_SegmentAccess (raw native-memory read throughput), C_ExecutionBoundary (native fused plan vs Java Vector API vs Java scalar, swept 64 to 4,194,304 rows), E_FusionAndPlanning (fused vs unfused vs the scalar reference kernel vs plan-construction-only, swept 1-8 predicates). 50/50 rows, 0 failures. Data.crossCheck() runs in @setup and throws if the three kernels disagree on count or sum, so a faster-but-wrong Vector kernel could not have won the comparison undetected. The headline complicates the thesis honestly: for a single predicate over one native lane, the Java Vector API -- reading the SAME native MemorySegment zero-copy via IntVector.fromMemorySegment, no byte[], no bounce buffer -- beats the native crossing at EVERY row count tested, 56.4x at small sizes down to 1.3-1.4x at 4M rows. A second crossover is also real: native beats a plain Java scalar loop only past roughly 4,096-16,384 rows. Component E shows why this doesn't overturn the project's premise: SIMD-vs-scalar is the largest lever measured anywhere in this suite (10.8x-31.1x, growing with predicate count), and fused/unfused land within this harness's own ~10% noise floor of each other at 65,536 rows -- the fused plan's real value is the structural one-crossing guarantee (already proven by LazinessTest), not a large measured time saving at this scale. Verdict: the crossing is worth paying for composed, multi-predicate work, not for reading one predicate off one lane, where Java on the same memory is simply faster. RESULTS.md was hand-written from the raw CSV, then independently cross-checked against summarise.sh -- a script the same PR ships that mechanically regenerates every table from results/jmh-results.csv, so a re-run's numbers can never silently drift from a hand-transcribed table. Both productions agreed to 3 decimal places on every cell checked. Generated by [Claude Code](https://claude.ai/code) * Phase I synthesis docs + fusion-sweep 256-row re-run + board hygiene Four synthesis docs close D-LGJ-I: architecture.md (the four layers and what each is provably responsible for), panama.md (manifest-over-header, belt-and-braces ownership, restricted-method discipline), valhalla-lab.md (three-truths synthesis, the 8-byte flattening cliff, zero API adoption), execution-boundary.md (the measured crossover picture + three structural hot-path facts, each checked rather than assumed). The fusion sweep was re-run with a 256-row arm after the first pass's 'fusion does nothing' finding proved true only at 65,536 rows: at 256 rows x 8 predicates unfused/fused reaches 2.99x. RESULTS.md is rewritten from jmh-results-merged.csv; TABLES.md is mechanically generated from the same file so the two cannot drift. MultiLaneColumn (ndarray::simd_soa) evaluated for the fixture kernels and declined on two concrete API mismatches (64-byte-multiple constraint, no u32 lane); earmarked for the future 512-byte row-store slice where it fits by construction. Operator layout reference recorded on the board. PR_ARC_INVENTORY backfilled for merged PRs 1-3; the lapse is owned in the file itself. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Pud4qpxFHwqyqDjSabQbs * Board: PR #4 arc entry (post-merge) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Pud4qpxFHwqyqDjSabQbs * SoA row store: 512B rows, 32 facet lanes, ABI minor 2 (W1+W2) The lance-graph-shaped substrate, wired end to end. The flat three-lane fixture was always scaffolding (docs/abi.md 10, architecture.md said so from PR #1); this is the layout the stack actually converges on: 64K x 512-byte rows, 32 facet lanes of 16 bytes = 4-byte LE classid + 12-byte payload, the V3 content-blind facet. Rust (native/lgj-abi): - rowstore.rs: one Arc<[u8]>, two readings (row-major chunks and strided facet columns), zero copies, normative SplitMix64 generator. - LGJ_RESOURCE_ROWSTORE + lgj_rowstore_open; facet lanes described through the UNCHANGED LgjLaneDesc (stride_bytes carried this since minor 1); lgj_op_eq_classid produces ordinary masks that compose with the existing algebra; lgj_row_facet_match writes per-row 32-bit facet sets into a caller-owned buffer via MultiLaneColumn (Arc refcount bump, no copy). - byte_len tightened to the exact covered span (len-1)*stride + elem_bytes: a full-stride final window would let Java bound a segment past the allocation's end on a facet lane. - ABI minor 1 -> 2; docs/abi.md gains 11 and its symbol count is corrected (the 14 was drift; the list already enumerated 15, and the real number is now 18 per nm -D). Gates: cargo test 84/84, clippy -D warnings clean, fmt clean, release build exports 18/18 symbols. Both new kernels are parity-checked against independent scalar references over 10 row counts x 2 seeds x 4 facets x 4 needles, then cross-checked a third way against RowStore::classid_at; a two-sided falsifier proves payload bytes never satisfy a classid match and that a real match does fire. Docs: .claude/plans/lgj-soa-substrate-v1.md (W1-W5 waves) + one plan per consumer example (world-trades / bricks-analytics / graph-traversal), .claude/knowledge/soa-row-store-layout.md, and the board triple ledger. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Pud4qpxFHwqyqDjSabQbs * Board: PR #5 arc entry (post-merge) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Pud4qpxFHwqyqDjSabQbs * Knowledge: assess the archived layout-bridge discussion; name W6 The operator's pre-build ChatGPT discussion is assessed once, in .claude/knowledge/prior-art-and-the-layout-bridge-claim.md, so it is never re-mined or cited naively. Verdict: it converged independently on the architecture this repo then built and measured. Kept: the callability-vs-shared-executable-layout positioning, the schema-key-as- join-point extractable (now the named W6 consideration: an explicit schema/classid field on the descriptors when ClassView lands), and the baseline-dependent claims discipline for W5 comparisons. Pinned: its page-descriptor sketch has no liveness story (the registry's whole job), its native-always-wins assumption is measured false (Component C), and its ndarray paragraph describes upstream crates.io ndarray, not the AdaWorldAPI fork whose ndarray::simd polyfill this stack mandates. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Pud4qpxFHwqyqDjSabQbs * Plans: OGAR Machine (exploratory) + lance-graph-hydrate dependency note Captures the operator's second archived context as .claude/plans/ogar-machine-v1.md — a genuinely new workload for the shipped substrate, not convergent confirmation: one row = one machine STATE, control flow as population masks over 64K execution contexts, Ghidra P-code as the normalized guest ISA (repo attached and cloned), differential migration testing (legacy XOR replacement across 65,536 worlds) as the killer demo, Lance as the time machine. Strong claim vs weak claim separated per the discussion's own discipline; gated on W3 + one W5 example + Ghidra archaeology + a tiny falsifiable probe (P-M1). Also records lance-graph #957 (merged: lance-graph-hydrate, the generic SoA->S3->volume->Lance hydration crate minted for consumers to inherit) and #958 (its open hardening fast-follow) in the substrate plan: when this repo's persistence slice arrives, hydration is inherited from lance-graph-hydrate, never re-derived here. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Pud4qpxFHwqyqDjSabQbs * Board: PR #6 arc entry (post-merge) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Pud4qpxFHwqyqDjSabQbs * Waves calcified: dispatch maps for every plan; Ghidra plan from real archaeology Operator ruling: calcify, don't execute. .claude/waves/ holds one dispatchable map per plan — README with the standing rules and the verbatim worker preamble, substrate W3+W4 (the only READY wave), three consumer waves stamped DO-NOT-DISPATCH, Ghidra G1+G2, and OGAR-Machine P-M1 (BLOCKED behind a 4-condition gate including an explicit operator go). Each map carries disjoint worker scopes, orchestrator-only steps, exact gate commands, disable-runs, and STOP triggers. ghidra-integration-v1.md is written from archaeology against the real clone, not the sketch: 74 P-code opcodes (CPUI_MAX=75), 12.2 DEV / Java 25+, analyzeHeadless entry, and Ghidra's own PcodeEmulator as the reference-implementation parity oracle (the tesseract-rs method). The ogar-machine plan is cross-updated to cite it. Mapping-time catches that would have burned a dispatch: the graph consumer needs a deliberate edge-bearing generator arm (today's payload is PRNG noise) - a substrate change, flagged in the wave; the hop has a real D1a/D1b design fork with ruling guidance recorded. Muscle memory pinned as E-LGJ-CALCIFY-THEN-DISPATCH-1: the eight earned-this-session rules and the plan->wave->shelf->dispatch rhythm. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Pud4qpxFHwqyqDjSabQbs * Board: PR #7 arc entry (post-merge) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Pud4qpxFHwqyqDjSabQbs * Plan: lance-graph #958 merged (was open at last check) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Pud4qpxFHwqyqDjSabQbs * Java RowStore facade: W3 shipped (185/185, one bug found+fixed) First real dispatch of the calcified wave system (wave-substrate-w3-w4.md Dispatch 1) — 3 Sonnet workers on disjoint scopes, Opus orchestrator integration and central gating, per the standing rules in .claude/waves/README.md. New public surface: RowStore (open/rowCount/isOpen/maskOfFacetClass/ facetMatches/close), FacetMatchView (rowCount/matchesOf/cardinality), FacetId (0..31-checked record) -- zero java.lang.foreign types in any public signature, ApiSurfaceTest passed unmodified. Mask.source() retyped NativePattern -> NativeResource (new minimal interface) so a mask parents onto either a pattern or a row store with the existing algebra unchanged; verified zero call-site breakage before the retype. One real bug caught by the test suite itself: FacetMatchView.rowCount() was missing the closed-store guard its sibling accessors both had -- found by RowStoreLifetimeTest on the first real run, fixed, re-verified. Gate: javac -Xlint:all clean (7 pre-existing [restricted] warnings, 0 new); AllTests 132 -> 185 (+53 checks: 29 parity + 24 lifetime). Both mandated disable-runs ran red-then-green with the exact expected blast radius: (1) Abi.requireMinor inflated by 1 -> exactly the two RowStore suites failed, 8 others stayed green; (2) the generator's a/b draw order swapped -> exactly RowStoreParityTest broke (17/29), the generator-independent RowStoreLifetimeTest stayed green. Board: STATUS_BOARD D-LGJ-W3 DONE, LATEST_STATE, and E-LGJ-WAVE-DISPATCH-VALIDATED-1 -- the wave system's first real dispatch, including an orchestrator-side false alarm (wrong env var name guessed instead of read from source) recorded so it isn't repeated. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Pud4qpxFHwqyqDjSabQbs --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 68f7add commit 320808d

18 files changed

Lines changed: 897 additions & 9 deletions

File tree

‎.claude/board/EPIPHANIES.md‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,47 @@
44
> `**Status:**`/`**Confidence:**` line. A correction gets its own new,
55
> dated entry that references the one it corrects — the storno rule.
66
7+
## 2026-08-17 — E-LGJ-WAVE-DISPATCH-VALIDATED-1
8+
9+
**Status:** FINDING (first real dispatch of the wave system). **Confidence:**
10+
High — measured, not asserted.
11+
12+
The wave map (`E-LGJ-CALCIFY-THEN-DISPATCH-1`) was written to be
13+
"dispatchable as-is by a session with zero shared context." First test: W3,
14+
three Sonnet workers on disjoint file scopes, zero coordination between
15+
them beyond the frozen signatures the orchestrator's briefs specified. All
16+
three landed clean, mutually consistent (same `RowStore.open(long,long)`
17+
signature, same `FacetId.index()` accessor — nobody guessed differently),
18+
and the disjoint-scope rule held with zero merge conflicts.
19+
20+
Two things the gate sequence actually caught, worth recording precisely
21+
because they're the mechanism, not the anecdote:
22+
23+
1. **A real defect, caught by the tests the wave mandated.**
24+
`FacetMatchView.rowCount()` was missing the same closed-store guard
25+
`matchesOf`/`cardinality` both carried — one accessor out of three,
26+
asymmetric, exactly the kind of gap a reviewer skims past and a
27+
two-sided lifetime test does not. `RowStoreLifetimeTest` (itself
28+
AI-written, by a different worker than the one who wrote the class
29+
under test) caught it on the first real run. This is the payoff of
30+
"workers never run the gate themselves" — the orchestrator's fresh,
31+
independent test run is what a self-reported "looks right" cannot be.
32+
2. **A false alarm from the ORCHESTRATOR's own environment, not the
33+
code.** The first two `AllTests` invocations failed with every
34+
pre-existing suite red — before touching a single line of worker
35+
output. Root cause: I used an invented env var name (`LGJ_NATIVE_LIB`)
36+
instead of the real one (`LGJ_LIBRARY`, defined in `Abi.java`), so the
37+
runtime silently fell back to a stale `.so` from an unrelated default
38+
search path. The fix was to READ THE CODE (`Abi.java`'s
39+
`ENV_LIBRARY` constant) rather than guess a plausible-sounding name.
40+
Lesson for future orchestrator runs: verify the discovery mechanism
41+
from source before trusting a gate result — a wrong environment can
42+
look exactly like a real regression.
43+
44+
Both disable-runs (version-gate inflation; generator draw-order swap) ran
45+
red-then-green with the EXACT expected suite-level blast radius — no
46+
overreach, no under-reach — closing the loop the wave file promised.
47+
748
## 2026-08-17 — E-LGJ-CALCIFY-THEN-DISPATCH-1
849

950
**Status:** DOCTRINE (operator-ruled: "don't execute the consumer plans yet,

‎.claude/board/LATEST_STATE.md‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,34 @@
1+
## 2026-08-17 (dispatch 1) — W3 shipped: the Java `RowStore` facade, from the calcified wave map
2+
3+
First real dispatch of the wave system: `wave-substrate-w3-w4.md` Dispatch 1
4+
executed exactly as mapped — 3 Sonnet workers on disjoint file scopes (FFM
5+
membrane extension / public facade / tests), Opus orchestrator integrated,
6+
gated, and fixed centrally. Confirms the calcify-then-dispatch rhythm works
7+
end to end, not just as a documentation exercise.
8+
9+
- **New public surface:** `RowStore` (`open`/`rowCount`/`isOpen`/
10+
`maskOfFacetClass`/`facetMatches`/`close`), `FacetMatchView`
11+
(`rowCount`/`matchesOf`/`cardinality`), `FacetId` (a 0..31-checked record).
12+
Zero `java.lang.foreign` types in any public signature — `ApiSurfaceTest`
13+
passed unmodified.
14+
- **`Mask` generalized**: `source()` retypes `NativePattern → NativeResource`
15+
(new minimal interface), so a mask can parent onto EITHER a pattern or a
16+
row store with the existing algebra unchanged. Verified zero call-site
17+
breakage before the retype.
18+
- **One real bug found by the suite itself and fixed**:
19+
`FacetMatchView.rowCount()` was missing the closed-store guard its sibling
20+
accessors both had — a caller could read a stale row count off a dead
21+
view. Caught by `RowStoreLifetimeTest`, fixed, re-verified 185/185.
22+
- **Both disable-runs green-red-green**, confirming the version gate and the
23+
generator-transcription parity are load-bearing, not decorative (full
24+
detail on `STATUS_BOARD.md` D-LGJ-W3).
25+
- Gate: `javac -Xlint:all` clean (7 pre-existing `[restricted]` warnings,
26+
0 new); `AllTests` 132→185 (+53); native `.so` unchanged this dispatch
27+
(Rust side untouched — pure Java consumer work).
28+
29+
**Next:** W4 (bench Component F, Vector API vs the crossing on the real
30+
row-store layout) — the second half of the same wave file.
31+
132
## 2026-08-17 (latest) — waves calcified, Ghidra plan grounded, NOTHING dispatched
233

334
Operator ruling: consumer plans are **calcified, not executed** — insights

‎.claude/board/PR_ARC_INVENTORY.md‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,27 @@
88
> anti-pattern the imported board rules name. Backfilled below in one
99
> pass rather than left stale; PR #4 onward gets its entry at merge time.
1010
11+
## PR #7 — waves calcified: dispatch maps for every plan (merged 2026-08-17, squash `68f7add`)
12+
13+
- **Added:** `.claude/waves/` — README (standing rules + verbatim worker
14+
preamble) + six dispatchable maps (substrate W3+W4 READY; three
15+
consumer waves DO-NOT-DISPATCH; Ghidra G1+G2 shelved; OGAR-Machine
16+
P-M1 BLOCKED behind a 4-condition gate incl. explicit operator go);
17+
`.claude/plans/ghidra-integration-v1.md` (G0 archaeology from the real
18+
clone: 74 P-code opcodes, `PcodeEmulator` as reference oracle, Toy as
19+
minimal lift target).
20+
- **Locked:** the calcify-then-dispatch rhythm + the eight
21+
muscle-memory rules (`E-LGJ-CALCIFY-THEN-DISPATCH-1`); the op-set
22+
halt-loudly discipline; the D1a/D1b hop fork with ruling guidance.
23+
- **Deferred:** ALL execution, by operator ruling — nothing dispatched,
24+
no code changed.
25+
- **Docs:** the PR is docs; mapping-time catches recorded (graph
26+
consumer needs an edge-bearing generator arm — substrate change,
27+
flagged before it could burn a dispatch).
28+
- **Confidence:** High for the maps (grounded in shipped code + real
29+
clone archaeology); execution confidence deliberately unclaimed until
30+
the waves run. Bot reviewers at usage limits, did not run.
31+
1132
## PR #6 — layout-bridge assessment, OGAR Machine plan, hydrate note (merged 2026-08-17, squash `8954e53`)
1233

1334
- **Added:** `.claude/knowledge/prior-art-and-the-layout-bridge-claim.md`

‎.claude/board/STATUS_BOARD.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,6 @@ layout wired end to end. Doctrine: `E-LGJ-THE-MIDDLE-TIER-IS-DELETED-NOT-WRAPPED
3838
|---|---|---|
3939
| D-LGJ-W1 | ndarray: `MultiLaneColumn::iter_u32x16`/`len_u32x16` + `eq_u32_strided_to_mask` (W1a contract) | **DONE 2026-08-17** — ndarray PR #279; `simd_int_ops` 46/46 (5 new strided tests incl. two `should_panic` bounds/overflow arms + stride-4 parity against the contiguous primitive), `simd_soa` 15/15, full `simd` 263/263, doctests, clippy `-D warnings` + fmt clean |
4040
| D-LGJ-W2 | lgj-abi row store: `rowstore.rs`, `LGJ_RESOURCE_ROWSTORE`, `lgj_rowstore_open`, strided facet lanes through the unchanged `LgjLaneDesc`, `lgj_op_eq_classid`, `lgj_row_facet_match`, ABI minor 1→2, `docs/abi.md` §11 | **DONE 2026-08-17** — `cargo test` **84/84**, clippy/fmt clean, release build exports **18/18** symbols (`nm -D`). Parity: both kernels vs independent scalar references over 10 row counts × 2 seeds × 4 facets × 4 needles, cross-checked a THIRD way against `RowStore::classid_at`. Two-sided payload-vs-classid falsifier. End-to-end membrane test covers describe → predicate → mask algebra → count → facet-match → lifecycle |
41-
| D-LGJ-W3 | Java `RowStore` facade: structured `MemoryLayout`, minor-≥2 gate, `FacetMatchView`, parity test transcribing the generator | **NEXT** |
41+
| D-LGJ-W3 | Java `RowStore` facade: structured `MemoryLayout`, minor-≥2 gate, `FacetMatchView`, parity test transcribing the generator | **DONE 2026-08-17** — dispatched per `.claude/waves/wave-substrate-w3-w4.md` (3 Sonnet workers, disjoint scopes: FFM membrane extension / public facade / tests), orchestrator-integrated. `javac -Xlint:all` clean (same 7 pre-existing `[restricted]` warnings, zero new). `AllTests` **185/185** (was 132; +53 new checks: 29 parity + 24 lifetime). **One real bug caught by the suite and fixed**: `FacetMatchView.rowCount()` was missing the closed-store guard `matchesOf`/`cardinality` both had — a stale row count was readable after the owning store closed. Fixed, re-verified. Both mandated disable-runs ran red-then-green: (1) `Abi.requireMinor` inflated by 1 → exactly `RowStoreParityTest`+`RowStoreLifetimeTest` failed, all 8 other suites stayed green; (2) the pure-Java generator's a/b draw order swapped in `RowStoreParityTest` → exactly that suite broke (17/29), `RowStoreLifetimeTest` (generator-independent) stayed green — confirming the parity test is a real falsifier, not decorative. `Mask.source()` retyped `NativePattern → NativeResource` (new interface) so a `Mask` can parent onto either a `NativePattern` or a `RowStore` — zero call-site breakage (verified: no existing caller bound the narrower type) |
4242
| D-LGJ-W4 | Bench Component F: Vector API facet scan vs the crossing, on the REAL layout | Queued |
4343
| D-LGJ-W5 | Three consumer examples (trades / bricks / graph) — one plan file each | Planned, gated on W3 |

‎.claude/plans/lgj-soa-substrate-v1.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ disable-verified, every measured claim lands with its reproduction command.
4343
SoA→S3→volume→Lance hydration pattern (four-state lifecycle,
4444
hydrate-aside/publish-by-rename, warm markers, dirty detection), minted in
4545
lance-graph *specifically so consumers inherit it as a path/git dependency
46-
rather than re-implement*. **#958** (open, another session's PR) is its 5+3
46+
rather than re-implement*. **#958** (merged 2026-08-17, another session's
47+
PR) was its 5+3
4748
council hardening fast-follow. Consequence here: when this substrate's
4849
persistence slice arrives (the "Seal & Persist (Lance)" column of the
4950
formula, and `ogar-machine-v1.md`'s time-machine storage), the hydration
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.adaworldapi.lancegraph;
2+
3+
/**
4+
* Which of a {@link RowStore} row's 32 facet lanes to read.
5+
*
6+
* <p><strong>Not a lane id.</strong> abi.md §11 is explicit that {@code lgj_op_eq_classid} takes a
7+
* facet index, not a lane id — lane {@code 0} is the raw buffer and facet {@code f}'s lane id is
8+
* {@code 1 + f}. Mixing the two up is exactly the bug the end-to-end test pins (facet {@code 32}
9+
* is invalid while lane {@code 32} is valid). A distinct type is what stops that confusion at the
10+
* call site instead of relying on a comment.
11+
*
12+
* <h2>Valhalla A/B candidate</h2>
13+
*
14+
* <p>Same rules as {@link LaneId} and {@link Ordinal}: final, immutable, identity-free, so the
15+
* same source compiles as a {@code value record} under JEP 401. It wraps a single {@code int}
16+
* purely so a facet index cannot be confused with a lane id, a classid, or a row number, and under
17+
* Valhalla that type-safety is expected to cost nothing at all.
18+
*
19+
* @param index zero-based facet index, {@code 0..32} exclusive (a row has exactly 32 facets)
20+
*/
21+
public record FacetId(int index) {
22+
23+
public FacetId {
24+
if (index < 0 || index > 31) {
25+
throw new IllegalArgumentException("facet index must be in 0..31, was " + index);
26+
}
27+
}
28+
29+
public static FacetId of(int index) {
30+
return new FacetId(index);
31+
}
32+
33+
@Override
34+
public String toString() {
35+
return "facet#" + index;
36+
}
37+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package com.adaworldapi.lancegraph;
2+
3+
import java.lang.foreign.MemorySegment;
4+
import java.lang.foreign.ValueLayout;
5+
6+
/**
7+
* For every row of the {@link RowStore} it was taken from, which of its 32 facets carry the
8+
* queried classid — one {@code int} bitset per row, bit {@code f} set exactly when facet
9+
* {@code f}'s classid equals the value {@link RowStore#facetMatches} was called with.
10+
*
11+
* <p>Backed by a Java-owned segment a single native crossing wrote into ({@code
12+
* lgj_row_facet_match}, abi.md §11). Every read below — {@link #matchesOf} and
13+
* {@link #cardinality} — is an in-process access of that already-fetched segment; no further
14+
* crossing occurs.
15+
*
16+
* <h2>Lifetime</h2>
17+
*
18+
* <p>A view is a child of the {@link RowStore} it was taken from: the segment it wraps is
19+
* allocated from that store's own arena. It may outlive its parent as an object, but reading from
20+
* it after the parent closes throws {@link ClosedResourceException} — the segment's backing arena
21+
* dies with the store, so there is no arrangement of closes that lets a view read freed memory.
22+
*/
23+
public final class FacetMatchView {
24+
25+
private final RowStore owner;
26+
private final MemorySegment data;
27+
private final long rowCount;
28+
29+
FacetMatchView(RowStore owner, MemorySegment data, long rowCount) {
30+
this.owner = owner;
31+
this.data = data;
32+
this.rowCount = rowCount;
33+
}
34+
35+
/**
36+
* How many rows this view covers.
37+
*
38+
* <p>Reads no segment bytes, but is guarded exactly like {@link #matchesOf} and
39+
* {@link #cardinality} for the reason the class doc states: once the store closes, this view is
40+
* no longer usable at all, not merely unsafe to read the segment through — a caller that checks
41+
* {@code rowCount()} before deciding whether to call {@link #matchesOf} must not be able to
42+
* observe a stale, disconnected-from-reality number from a dead view.
43+
*/
44+
public long rowCount() {
45+
requireUsable("rowCount()");
46+
return rowCount;
47+
}
48+
49+
/**
50+
* The 32-bit facet-match bitset for {@code row}: bit {@code f} set means facet {@code f}'s
51+
* classid equals the value queried.
52+
*
53+
* <p>An in-process segment read. No membrane crossing occurs.
54+
*
55+
* @throws IndexOutOfBoundsException if {@code row} is not in {@code [0, rowCount())}
56+
*/
57+
public int matchesOf(long row) {
58+
requireUsable("matchesOf()");
59+
if (row < 0 || row >= rowCount) {
60+
throw new IndexOutOfBoundsException(
61+
"row " + row + " is out of range [0, " + rowCount + ")");
62+
}
63+
return data.getAtIndex(ValueLayout.JAVA_INT, row);
64+
}
65+
66+
/**
67+
* The total number of set bits across every row's bitset.
68+
*
69+
* <p>Deliberately Java-side: this is a bulk reduction over a result that already crossed the
70+
* membrane once (the single {@code lgj_row_facet_match} call behind
71+
* {@link RowStore#facetMatches}), so a second crossing just to reduce it would undo the point
72+
* of having fetched the whole thing in bulk.
73+
*/
74+
public long cardinality() {
75+
requireUsable("cardinality()");
76+
long total = 0;
77+
for (long row = 0; row < rowCount; row++) {
78+
total += Integer.bitCount(data.getAtIndex(ValueLayout.JAVA_INT, row));
79+
}
80+
return total;
81+
}
82+
83+
private void requireUsable(String what) {
84+
// Checked before any segment access, on purpose: the segment's backing arena is the
85+
// owning store's, so once the store is closed the segment must not be touched at all.
86+
if (!owner.isOpen()) {
87+
throw new ClosedResourceException(
88+
what + " was called on a facet-match view whose store is closed. The segment's"
89+
+ " backing arena dies with the store, so it can never be read again.");
90+
}
91+
}
92+
93+
@Override
94+
public String toString() {
95+
return "FacetMatchView[" + rowCount + " rows]";
96+
}
97+
}

‎java/src/main/java/com/adaworldapi/lancegraph/Mask.java‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,11 @@
2121
*/
2222
public final class Mask implements AutoCloseable {
2323

24-
private final NativePattern parent;
24+
private final NativeResource parent;
2525
private final long handle;
2626
private boolean closed;
2727

28-
Mask(NativePattern parent, long handle) {
28+
Mask(NativeResource parent, long handle) {
2929
this.parent = parent;
3030
this.handle = handle;
3131
}
@@ -41,8 +41,8 @@ public MaskId id() {
4141
return new MaskId(handle);
4242
}
4343

44-
/** The resource whose rows this selects. */
45-
public NativePattern source() {
44+
/** The resource whose rows this selects — a {@link NativePattern} or a {@link RowStore}. */
45+
public NativeResource source() {
4646
return parent;
4747
}
4848

‎java/src/main/java/com/adaworldapi/lancegraph/NativePattern.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
* <p>Terminal operations reuse one internal scratch selection, so they are serialised on this
3636
* instance. Distinct resources do not contend with each other.
3737
*/
38-
public final class NativePattern implements AutoCloseable {
38+
public final class NativePattern implements NativeResource, AutoCloseable {
3939

4040
/**
4141
* The seed used by {@link #open(long)}.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.adaworldapi.lancegraph;
2+
3+
/**
4+
* The common face of row-shaped native resources a {@link Mask} can select over.
5+
*
6+
* <p>Both {@link NativePattern} and {@link RowStore} hold rows natively and let a {@link Mask}
7+
* parent onto them — abi.md §11 states this directly: "masks may parent onto a pattern OR a row
8+
* store — both are read-only, row-shaped resources." This interface names that shared shape so
9+
* {@link Mask} does not need to know, or care, which kind of resource it selects over.
10+
*
11+
* <p>Deliberately small: just enough for {@link Mask} to report a count and to check liveness
12+
* before every operation. Anything specific to how a resource's rows are laid out or generated
13+
* belongs on the concrete type, not here.
14+
*/
15+
public interface NativeResource {
16+
17+
/** How many rows this resource holds. */
18+
long rowCount();
19+
20+
/** False once the resource has been closed. */
21+
boolean isOpen();
22+
}

0 commit comments

Comments
 (0)