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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions .claude/board/EPIPHANIES.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,51 @@
> `**Status:**`/`**Confidence:**` line. A correction gets its own new,
> dated entry that references the one it corrects — the storno rule.

## 2026-08-25 — E-LGJ-A-CONSTANT-COPIED-THREE-TIMES-HAS-NO-FALSIFIER-1

**Status:** SHIPPED — ABI minor 8, docs/abi.md §17.
**Confidence:** High on the mechanism; every guard disable-verified.

The §14 carving wire encoding lived in three places: a Rust `match`, a
Java `enum`, and abi.md's own table. Each was correct. Nothing anywhere
would have failed if one had stopped agreeing with the others.

**That is the finding, and it generalises past this constant.** Three
copies of one fact is not a documentation problem to be tidied — it is a
correctness problem with NO falsifier, and its failure mode is silent:
a variant added or reordered upstream re-maps one copy, a sweep then
reads the same 12 bytes under the wrong grouping, and returns a
plausible number. "Keep them in sync" is not a mechanism.

The fix is not a fourth copy that checks the others. It is ONE source
(`CascadeShape::ROTATIONS`, the contract's) and two DERIVATIONS: the ABI
computes the encoding from it by a RULE (group count, descending) rather
than by declaration position, and the manifest serves the result to Java
so Java restates nothing. A reorder upstream cannot re-map the wire; an
addition upstream propagates.

**The two corollaries worth keeping:**

1. **Derive by a rule, not by position.** Had the order been "declaration
order of the enum", the derivation would have been just as automatic
and just as fragile — the drift would simply have moved upstream.
2. **Meaning is declared; encoding is served.** `RAILS_6X2` keeps its
arity as a literal, because the arity IS the constant's identity and a
name that lies about it is worse than a hardcode. Only the encoding —
which carries no meaning — became data.

**And the change surfaced a latent defect one layer down**, which is the
usual reward for touching a boundary: Java's load gate required the FULL
manifest layout, so the first growth of that struct would have made every
older artifact fail to load, in flat contradiction of §2's additive
promise. It had been written that way since minor 1 and was unreachable
until now. Measured: restoring the full-layout gate makes all four
historical libraries fail outright.

Cf. `E-LGJ-THE-RESOLVER-WAS-UPSTREAM-ALL-ALONG-1` — same shape one rung
up (the answer was already in the contract; the local version was the
copy).

## 2026-08-25 — E-LGJ-THE-RESOLVER-WAS-UPSTREAM-ALL-ALONG-1

**Status:** SHIPPED — ABI minor 6 + the contract accessor + the ndarray
Expand Down
35 changes: 35 additions & 0 deletions .claude/board/LATEST_STATE.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,38 @@
## 2026-08-25 — ABI minor 8: the register groupings are DATA, and the load gate stopped requiring the whole manifest

- **The wire encoding of §14's `carving` is no longer written anywhere by
hand.** The contract owns the set (`CascadeShape::ROTATIONS`),
`kernels::CARVING_ORDER` (a `const`) derives the order by a RULE (group
count, descending — never declaration position), and the manifest serves
it in two new fields: `carving_count: u32` + `carvings: [u16; 8]`
(`(groups << 8) | group_bytes`). No new symbol; the manifest already
exists so Java can discover the ABI's shape rather than declare it.
`LgjAbiManifest` is now 128 bytes.
- **Java keeps its ARITY and loses its ENCODING.** `Carving.groups()` /
`groupBytes()` stay declared — the arity IS the constant's identity —
while `wire()`/`ofWire()` look up the served table. `CarvingTable` holds
the one clearly-named pre-minor-8 compatibility shim, so exactly one
place in the build carries a literal encoding and its name says it is
history.
- **A latent defect fixed on the way:** Java's load gate required the FULL
manifest layout, so the FIRST growth of that struct — this one — would
have made every older artifact fail to load, contradicting §2's additive
promise. The gate now requires only the 104-byte BASE PREFIX
(`Layouts.MANIFEST_BASE_BYTES`); later fields are read only when
`size_of_manifest` covers them AND the minor is high enough.
- **Gates:** Rust 134 lib tests, fmt + clippy `-D warnings` clean; Java
**304** checks (`AllTests`, was 288 — `CarvingTableTest` adds 16);
`OldAbiCompatTest` green against all four historical `.so`s (minors
1-4). Four disable-runs, each red-then-green: swapped packed axes,
reversed sort, a mismatched Java arity (fires BOTH membership
directions), and the restored full-layout gate (minor-4 library fails to
load).
- **Docs:** `docs/abi.md` §17 (new), §2 (load-gate prefix), §14's table
regraded DESCRIPTIVE rather than normative.
- Stacked on PR #30 (minor 7); board entry in `PR_ARC_INVENTORY.md`,
finding in `EPIPHANIES.md`
(`E-LGJ-A-CONSTANT-COPIED-THREE-TIMES-HAS-NO-FALSIFIER-1`).

## 2026-08-18 — PR-W8b (FACADE + GRAPH MIGRATION) — the mask-native correction reaches the Java surface

### Current surface changes (java/ + consumers/graph)
Expand Down
48 changes: 48 additions & 0 deletions .claude/board/PR_ARC_INVENTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,54 @@
> 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 #32 — ABI minor 8: the register groupings served as DATA (draft, opened 2026-08-25)

- **Added:** `LgjAbiManifest.{carving_count, carvings[8]}` (docs/abi.md
§17) — the §14 wire encoding, previously hand-written in THREE places
(a Rust `match`, Java's `Carving` enum, §14's table) with nothing that
would fail if they disagreed. Now one source and two derivations: the
contract owns the SET (`CascadeShape::ROTATIONS`), `kernels::CARVING_ORDER`
(a `const`, group count descending) derives the ENCODING, and the manifest
serves it. Java's `Carving` keeps its declared ARITY (the name IS the
arity) and looks its wire value up in the served table.
New: `CarvingTable` (with the one clearly-named pre-minor-8 shim),
`CarvingTableTest` (16 checks, in `AllTests`), two Rust falsifiers.
- **Locked:** meaning is declared, encoding is served. A variant REORDER
upstream cannot re-map the wire (position is computed from `groups()`,
not declaration order); a variant ADDED upstream appears automatically
and is caught by the both-ways membership test rather than surfacing on
someone's data. `CARVING_ORDER` is `const` and not `LazyLock` because
the manifest that serves it is const-initialised.
- **Also fixed (a real latent defect, not scope creep):** Java's load gate
required the FULL manifest layout, so this — the first growth of the
manifest struct — would have made every older artifact fail to load,
contradicting §2's additive promise. Gate now requires only the 104-byte
BASE PREFIX; later fields are read when `size_of_manifest` covers them
AND the minor is high enough. Measured: all four historical `.so`s
(minors 1-4) still load and gate per-minor correctly.
- **Deferred:** nothing new. `FacetSchema`'s third reading is still
`Pair48` rather than the operator-ruled L6 quads — flagged earlier,
untouched here.
- **Docs:** `docs/abi.md` §17 (new), §2 (load-gate prefix + minor-8
history), §14 table regraded DESCRIPTIVE, manifest struct listing,
header constants.
- **Gates:** Rust 134 lib tests / fmt / clippy `-D warnings` clean; Java
304 checks (`AllTests`, was 288); `OldAbiCompatTest` 4/7/7/7 against
minors 1-4. Stacked on PR #30 (minor 7).
- **Disable-runs, all red-then-green:** swapping the packed axes fails the
Rust serve test; reversing the sort fails the order test and two others;
changing one Java constant's arity fails BOTH membership directions;
restoring the full-layout load gate makes the minor-4 library fail to
load outright.
- **Confidence:** high on the derivation and the falsifiers (each disabled
and observed red). Medium on the 8-slot table width — the bound is
argued from `G·D = 12`'s divisors, not measured against a future
`CascadeShape`.
- **Owned mistake:** mid-session I ran `git checkout` on `kernels.rs` to
undo a disable-run edit and destroyed the uncommitted work in that file.
Reconstructed and re-verified (134 tests, same count). Disable-runs are
now backed up to a file first, never reverted with `git checkout`.

## 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`
Expand Down
111 changes: 109 additions & 2 deletions docs/abi.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ cannot disagree with itself.
The ABI is a **machine membrane**. It is not the product. The product is the Java
semantic API (see `architecture.md`). Therefore:

- It is **small** — currently 24 symbols (minor 7; the "14" this line carried
- It is **small** — currently 24 symbols (unchanged at minor 8, which adds
manifest FIELDS and no symbol; the "14" this line carried
at minor 1 was arithmetic drift — the §7 list it referred to already
enumerated 15). Growth is a design smell to be argued for, not a default;
minor 2's three additions are argued in §11, minor 3's one addition in
Expand All @@ -79,7 +80,7 @@ semantic API (see `architecture.md`). Therefore:

```
LGJ_ABI_MAJOR = 0 // incompatible change ⇒ bump; Java refuses to load
LGJ_ABI_MINOR = 7 // additive change ⇒ bump; older Java may still load
LGJ_ABI_MINOR = 8 // additive change ⇒ bump; older Java may still load
LGJ_MAGIC = 0x4C_47_4A_5F_41_42_49_00 // "LGJ_ABI\0" big-endian-read
```

Expand All @@ -98,6 +99,18 @@ against an older `.so`**. That case is governed by `Abi.requireMinor(N)`, whose
contract is to fail before the feature's downcall is attempted, naming the
minor.

The **load gate itself** requires only the manifest's BASE PREFIX — everything
through `build_profile`, 104 bytes, the field set minor 1 defined
(`Layouts.MANIFEST_BASE_BYTES`). Requiring the full layout this Java build knows
about would make every future manifest field a hard incompatibility with every
older artifact, in flat contradiction of the promise above. It was written that
way until minor 8 grew the struct and the contradiction became reachable;
measured, restoring the full-layout gate makes all four historical libraries
fail to load outright. Fields past the base prefix are read only when the
library's own `size_of_manifest` covers them AND its minor is high enough — both
conditions, because a manifest that claims a minor it is too short to carry is a
broken artifact and reading it would produce plausible garbage.

**That guard was defeated by eager class initialization until 2026-08-25.**
Every downcall handle was resolved in `Downcalls.<clinit>`, so a single absent
symbol broke the whole class and the guard never ran. Measured with the Java of
Expand Down Expand Up @@ -132,6 +145,11 @@ required — a gate that rejected everything would satisfy a rejection-only test
- **Minor 4** (2026-08-18, D-LGJ-W8) — `lgj_mask_andnot` (mask complement)
and `lgj_hop` (one-hop graph traversal, gated by the
`lance-graph-contract` `ClassView`/`FieldMask` LAW — §13).
- **Minor 8** (2026-08-25) — the manifest carries the register groupings as
DATA (§17): `carving_count` + `carvings`. **No new symbol** and no new
status; it is the first growth of the manifest STRUCT, which is why it is
also the change that made Java's load gate require only the base 104-byte
prefix rather than the full layout.
- **Minor 7** (2026-08-25) — `lgj_row_layout_probe` (§16): the whole-row
alignment answer, all 32 facets in one crossing. No new status.
- **Minor 6** (2026-08-25) — `lgj_reduce_facet_sum_resolved` (§15): the same
Expand Down Expand Up @@ -299,6 +317,10 @@ pub struct LgjAbiManifest {
pub simd_backend: u32, // LgjSimdBackend
pub simd_backend_name: [u8; 32], // NUL-terminated, human-readable
pub build_profile: [u8; 16], // "release" | "debug"
// ── minor 8; everything above is the 104-byte BASE PREFIX the load gate
// requires, and all a pre-minor-8 artifact carries (§17) ──
pub carving_count: u32,
pub carvings: [u16; 8], // (groups << 8) | group_bytes, wire order
}
```

Expand Down Expand Up @@ -796,6 +818,13 @@ bytes:
| `1` | SPO triplets | `4 × (u8:u8:u8)`, LE `u24` zero-extended |
| `2` | odoo quads | `3 × (u8:u8:u8:u8)`, LE `u32` zero-extended |

**Since minor 8 this table is DESCRIPTIVE, not normative** — see §17. The
encoding is derived from the contract's `CascadeShape::ROTATIONS` (group count,
descending) and SERVED in the manifest; a reader that needs the authoritative
answer reads `carvings`, and this row set is what that derivation currently
produces. Before minor 8 it was one of three hand-written copies, which is the
problem §17 exists to remove.

Anything else is `LGJ_ERR_UNSUPPORTED_CARVING` (`-15`), checked **first**,
before the store or mask are resolved, so `out_sum` is provably untouched on a
rejected call. An unknown reading must never alias a known one.
Expand Down Expand Up @@ -1067,3 +1096,81 @@ which the address space does not permit.
And the table captures **layout only**. Meaning, RBAC, ontology category and
render template are separate resolutions off the same address; none belong in it
and none can be inferred from it.

---

## 17. The register groupings, served as data (ABI minor ≥ 8)

The manifest grew two fields. No symbol, no status, no call:

```
u32 carving_count // populated entries in `carvings`
u16 carvings[8] // entry w = wire value w, packed (groups << 8) | group_bytes
```

Entries past `carving_count` are zero, so a reader that trusts the count and one
that scans for a terminator agree. The struct is 128 bytes (108 + 16 = 124,
rounded to its 8-byte alignment).

### Why this exists

The wire encoding of §14's `carving` parameter was hand-written in **three**
places — a Rust `match`, a Java `enum`, and §14's own table — with nothing that
would fail if they disagreed. Three copies of one fact is not a documentation
problem; it is a correctness problem with no falsifier, and the specific failure
it invites is silent: a grouping added or reordered upstream re-maps one copy and
not the others, and a sweep then reads the same 12 bytes under the wrong reading
and returns a plausible number.

So the fact now has one source and two derivations:

1. **The contract owns the SET.** `lance_graph_contract::facet::CascadeShape::ROTATIONS`.
2. **This ABI derives the ENCODING from it** — group count, descending
(`kernels::CARVING_ORDER`, a `const`). A variant REORDER upstream cannot
re-map the wire, because position is computed from `groups()` rather than
from declaration order. A variant ADDED upstream appears automatically, in
its group-count place, with no edit.
3. **The manifest SERVES the result**, and Java reads it rather than restating
it.

`CARVING_ORDER` is deliberately a `const` and not a `LazyLock`: the manifest is
const-initialised, and a runtime-initialised order could not be reached from it.

### Why the manifest rather than a new symbol

The manifest already exists so Java can discover the ABI's SHAPE instead of
declaring it — sizes, alignments, pointer width, byte order. A wire encoding is
exactly such a shape. Serving it here costs no symbol, no crossing at call time,
and no lifetime question (a fixed `u16[8]` rather than a pointer), and it arrives
on the same read Java already performs at load.

### What is still declared on the Java side, and why that is correct

`Carving`'s ARITY stays declared: `RAILS_6X2` named anything other than `6 × 2`
would be a lie in its own name. What is no longer declared is its wire value —
that is looked up in the served table by arity. Meaning is declared; encoding is
served.

### The falsifiers

- Rust, `the_manifest_serves_exactly_the_derived_carving_order` — the served
bytes against the derived order, including that each entry decodes back to its
own shape, so a table that is internally consistent but wrongly ORDERED fails.
- Rust, `the_derived_order_is_strictly_descending_by_group_count` — a future
variant that TIED on group count would make the sort order-dependent again,
which is the property the derivation exists to remove.
- Java, `CarvingTableTest` — membership **both** ways. A grouping served that
Java cannot name (an addition upstream) and a grouping Java names that is not
served (a removal, or a locally invented constant). Neither direction is
redundant: without them, either mismatch would surface only when a particular
row happened to resolve to it — on someone's data, not in the build.

Verified red-then-green: swapping the packed axes fails the Rust serve test;
reversing the sort direction fails the order test and two others; changing one
Java constant's arity fails both membership directions.

A library predating minor 8 serves no table. Java falls back to the encoding
those artifacts actually used, in one clearly-named compatibility shim
(`CarvingTable.PRE_MINOR_8`) rather than back in the enum — so exactly one place
in the build carries a literal encoding, and its name says it is history rather
than the current answer.
Loading