contract(identity_quad): four identifier spaces joined before the bake into one 96-bit facet - #902
Conversation
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_770d7b02-5652-4b57-bc9c-947083535daf) |
📝 WalkthroughWalkthroughThe PR adds a public ChangesIdentity quad contract
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related PRs
Sequence Diagram(s)sequenceDiagram
participant ExternalKeys
participant QuadJoin
participant IdentityCodebook
participant IdentityQuad
ExternalKeys->>QuadJoin: provide optional keys
QuadJoin->>IdentityCodebook: resolve keys to ordinals
IdentityCodebook-->>QuadJoin: return ordinals or unknown
QuadJoin->>IdentityQuad: encode four slots
IdentityQuad-->>QuadJoin: return identity payload
QuadJoin-->>ExternalKeys: explain payload as keys
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 95e6a43822
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| /// injectivity gate) and any list larger than a 24-bit slot can address. | ||
| pub fn try_new(keys: impl IntoIterator<Item = String>) -> Result<Self, CodebookError> { | ||
| let mut keys: Vec<String> = keys.into_iter().collect(); | ||
| keys.sort(); |
There was a problem hiding this comment.
Preserve ordinals when the codebook grows
When an identifier space gains a key that sorts before an existing key, sorting the complete list renumbers persisted identities: for example, try_new(["b"]) assigns b ordinal 0, while rebuilding with ["a", "b"] assigns it ordinal 1, causing a previously baked facet containing ordinal 0 to be explained as a. Because the facet carries no codebook version and this API provides no append operation that preserves assignments, expanding a codebook silently relabels existing rows unless every old facet is atomically rebaked; preserve existing assignments or bind facets to a versioned codebook instead.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Accepted — this is correct, and it was the real defect in the module.
Verified: try_new sorts and derives each ordinal from the sorted position, so the ordinal is a property of the whole key set. Your ["b"] → ["a","b"] example reproduces exactly as you describe, and it is now a test — growing_a_codebook_with_an_early_key_renumbers_the_existing_ones asserts both that after.ordinal("b") == 1 and that a payload baked against the old book explains as "a" through the new one. Making the hazard observable was worth more than asserting it away.
The sharpest part of your comment is the one about the facet carrying no codebook version. That is what shipped:
IdentityCodebook::digest()— deterministic FNV-1a over the ordered key list, each key fed length-first so["ab","c"]and["a","bc"]cannot collide through concatenation. A bake records it beside its rows; a later read compares and refuses a shifted book instead of resolving a wrong key. Fire/silence tested: different digest for a renumbering book, same digest for the same key set in a different input order. The length prefix was mutation-tested — deleting it fails the concatenation assertion.- The constraint stated in the module doc, including why there is deliberately no append-preserving constructor: a book that preserved assignments would no longer be sorted, and
ordinal()'sbinary_searchwould be unsound. Growing a book is a rebake.
Where I stopped short, deliberately. The digest is a witness, not an enforcement — nothing obliges a caller to record or compare it. Making QuadJoin carry an expected digest and refuse a mismatched book would make the check structural, but it changes a shipped public signature, and this PR is additive-only. Recorded as ISS-IDENTITY-CODEBOOK-ORDINAL-STABILITY with that option named, rather than taken unilaterally.
Commit 1cf86cb.
Generated by Claude Code
| impl IdentityQuad { | ||
| /// The carving this type reads and writes — stated as a value so an audit | ||
| /// can assert it rather than infer it from the byte math. | ||
| pub const CARVING: LegacyOutlier = LegacyOutlier::WideTriple; |
There was a problem hiding this comment.
Obtain a sanctioned home before exposing this tenant
For this new permanent identity tenant, selecting LegacyOutlier::WideTriple directly contradicts the provider contract in crates/lance-graph-contract/src/legacy_outliers.rs:34-39, which defines these carvings as a temporary V1-migration waiting room and explicitly states that new classes must not use them. Publishing the module before the recorded operator ruling makes a permanent public encoding depend on a representation that downstream class views are expressly not allowed to sanction; move the primitives into an approved layout or obtain and codify the exception first.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Not accepted as stated — but the underlying question is real, already recorded, and genuinely open.
You are right that legacy_outliers.rs says new classes must not be born into these carvings, and right that this tenant is new and permanent. That contradiction is not an oversight: it is .claude/board/ISSUES.md → ISS-IDENTITY-QUAD-WIDE-CARVING-HOME, added in this PR, marked OPEN, NEEDS AN OPERATOR RULING, listing the three options — ratify the exception, mint a sanctioned layout and move the primitives out, or reject the carving.
What the record already argues, so it is not re-derived:
- §3a's discouragement is conditional, and it names its two conditions. god-object-related — no: four fixed slots of one kind, and a fifth identifier space is a second facet, never a wider field. lacking proper bucket rollover — no: capacity is the codebook's size and
IdentityCodebookrefuses past it rather than saturating. - §3a's named exit is unavailable. It says migrate to L4
6×(8:8)palette256² — a cosine/similarity replacement. This tenant's acceptance criterion is exact invertibility. There is no version of "migrate to palette256" that preserveskey → ordinal → key. - The cost is stated, not hidden. A
u24has no byte axis: not shift-addressable,group_ofdoes not apply, and it will not sweep with a cleanvpcmpeqd. Everything §3a says a wide carving gives up, this one gives up.
So the choice is between an exception and a new sanctioned layout, and both amend le-contract.md — an operator-locked document. Picking one in a review round is precisely the unilateral move the issue exists to avoid, and your instinct that it should be codified before publication is the same instinct that produced the issue.
Flagged for the operator; not resolved here. The module claims no sanction it does not have — the tension is in its own doc comment — and LegacyOutlier::WideTriple gains no new semantics from this use.
Generated by Claude Code
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.claude/board/EPIPHANIES.md:
- Around line 9-13: Clarify the capacity wording in the first paragraph to
distinguish the maximum ordinal from the number of codebook entries. Refer to
MAX_ORDINAL for the `2^24 - 2` limit, and state that MAX_ENTRIES/check_capacity
permits one additional entry.
In `@crates/lance-graph-contract/src/identity_quad.rs`:
- Around line 294-305: Document in the module documentation that sorted-position
ordinals are stable only when the complete key set is unchanged, and extend
IdentityCodebook with a deterministic version or content digest derived from its
ordered keys. Expose the witness for bake-time comparison and ensure codebook
construction computes it consistently, while preserving duplicate and capacity
validation in try_new.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: e3942190-ef1b-4aa9-8396-c02643540e91
📒 Files selected for processing (5)
.claude/board/EPIPHANIES.md.claude/board/ISSUES.md.claude/board/LATEST_STATE.mdcrates/lance-graph-contract/src/identity_quad.rscrates/lance-graph-contract/src/lib.rs
Both reviewers on PR #902 converged, from opposite directions, on the same real defect: `IdentityCodebook` derives ordinals from SORTED POSITION, so an ordinal is a property of the whole key set, not of the key. Grow a book with a key that sorts early and every ordinal at or after it shifts by one — and because `IdentityQuad` is a PERSISTED payload, a facet baked against the old book then explains as a neighbouring key. `verify_bijective()` cannot catch it: it is a within-book property and BOTH revisions pass. An invariant proved inside one artifact says nothing about two artifacts that must agree with each other. Added (all additive; no existing signature changed): - `IdentityCodebook::digest()` — deterministic FNV-1a over the ORDERED key list, each key fed length-first so ["ab","c"] and ["a","bc"] cannot collide through concatenation. A bake records it beside its rows; a later read compares and refuses a shifted book instead of resolving a wrong key. A WITNESS, not an enforcement — stated as such rather than papered over. - Module-doc section stating the constraint, and why there is deliberately no append-preserving constructor (a book that preserved assignments would not be sorted and `ordinal()`'s binary_search would be unsound — growing a book is a REBAKE). - `growing_a_codebook_with_an_early_key_renumbers_the_existing_ones` — makes the hazard observable: asserts the mis-explanation happens AND that both books pass `verify_bijective`. Falsifier stated inline. - `the_digest_fires_on_a_shift_and_stays_silent_on_an_equivalent_book` — fire/silence pair on one value: different digest for a renumbering book, SAME digest for the same key set in a different input order, different digests for the concatenation-colliding pair. Mutation-tested: deleting the length prefix fails the third assertion, so the delimiter is load-bearing. Also corrected a real wording imprecision review flagged: `2^24 - 2` is `MAX_ORDINAL`, a SLOT BOUND; `MAX_ENTRIES = 2^24 - 1` is the ENTRY COUNT `check_capacity` gates (ordinals start at zero). Both now named separately in the module doc and on the board. Board hygiene: EPIPHANIES PREPEND (`E-A-SORTED-CODEBOOK-ORDINAL-IS-A-PROPERTY-OF-THE-KEY-SET-NOT-THE-KEY-1`), ISSUES PREPEND (`ISS-IDENTITY-CODEBOOK-ORDINAL-STABILITY` — the digest is a witness not an enforcement; cross-bake agreement still unbuilt), LATEST_STATE contract-inventory line for the new method. NOT done, and why: the request to obtain a sanctioned home for the carving before publishing is not an oversight — it is the already-recorded `ISS-IDENTITY-QUAD-WIDE-CARVING-HOME`, open and awaiting an operator ruling. Moving the primitives out of `legacy_outliers` is one of the three options that issue lists; choosing among them is not a review-round decision. 14 lib tests green; `cargo fmt --check` and `cargo clippy -p lance-graph-contract --all-targets -- -D warnings` clean.
Review round complete — 4 comments, 3 fixed, 1 deferred to an operator ruling, 0 ignoredCommit The real defect, found independently by both reviewers from opposite directions. The part worth keeping is why the existing guard could not catch it: Shipped:
Stated rather than absorbed. The digest is a witness, not an enforcement — nothing obliges a caller to compare it. And there is deliberately no append-preserving constructor: a book that preserved assignments would no longer be sorted and The one not accepted — and it needs an operator, not a reviewer. The request to obtain a sanctioned home for the carving before publishing is not an oversight: it is Board hygiene: EPIPHANIES PREPEND ( Generated by Claude Code |
…nto one 96-bit facet A row whose identity is asserted independently by four external identifier spaces (each in the 10^5-10^7 range) had two bad options: four side tables joined per read, or a crosswalk chain walked per read. Both put work on the read path that the addresses could have carried themselves. This is the third option -- resolve the crosswalk ONCE at bake time and materialize all four identities in a single V3 facet payload. Afterwards a read is a fixed-offset register read: no join, no crosswalk consulted, no walk. The saving is not space, it is the disappearance of the read-time join. Carving: contiguous G2 `4 x u24`, read and written THROUGH `legacy_outliers::LegacyOutlier::WideTriple` rather than duplicating its bit math. Deliberately NOT the axis-grouped `4x(8:8:8)` -- that shape is three independently-meaningful bytes per slot, and an exact identity split across three independently-read bytes is no longer one invertible value. Invertibility is this tenant's whole acceptance criterion. Slots store `ordinal + 1` so raw 0 means absent (the CANON zero-fallback ladder). Without the offset, every codebook's zeroth entry would be indistinguishable from a never-filled slot and a partially-joined row would silently read as fully joined. `IdentityCodebook` rejects a non-injective key list at CONSTRUCTION, so a many-to-one mapping cannot exist to be found later; `verify_bijective()` is the explicit whole-book witness a bake runs anyway, because "the constructor guarantees it" is an argument and a bake wants a measurement. Overflow refuses rather than truncating -- the same refuse-don't-widen discipline `codebook::Codebook` uses at its own 256-entry scale, of which this is a sibling and not a widening. 12 tests, each carrying its falsifier: field-isolation matrix over all four slots (I-LEGACY-API-FEATURE-GATED), whole-book bijectivity sweep, the non-injective negative case, the capacity gate asserted at both sides of its boundary, join anti-vacuity plus can-stay-silent. Two guards were mutation-tested in-session to confirm they bite. One first-draft test was caught vacuous by that discipline and rewritten. Board hygiene in the same commit: LATEST_STATE contract inventory, EPIPHANIES (the identity-vs-rail-reference finding), and ISSUES (ISS-IDENTITY-QUAD-WIDE-CARVING-HOME) -- the module sits in `legacy_outliers` territory, which le-contract.md 3a discourages; the tension is surfaced for an operator ruling, not resolved in code. Fixtures are synthetic. Additive only: no existing behaviour, signature, or file changed apart from the module registration.
…and the join-key slot is not pinned Operator ruling: there is not one bake. Several domain bakes coexist, each with its own classid space, ClassView, and quadruple of identifier spaces. The quadruple varies BY DESIGN; what must not vary is the join mechanism — one shared identity slot present in every quadruple, plus the relation edges that cross bakes. Records the requirement this creates and which the shipped module does NOT meet: if one slot is the cross-bake join key, its position must be fixed and identical in every bake. `IdentityQuad` treats all four slots as symmetric, so the contract PERMITS a fixed join-key position but does not GUARANTEE one. A join key whose position varies by class must be looked up before it can be used, which defeats "the position is the type" exactly where it matters most. Candidate fix (not implemented pending the carving-home ruling it interacts with): a contract-level `JOIN_KEY_SLOT` invariant. Also records that bijectivity becomes a CROSS-bake property — the same key must resolve to the same ordinal in every bake carrying that space, or two bakes disagree while each stays internally consistent. No test covers this. And corrects one reading in the ruling that does NOT survive the canon: the proposed mapping of EdgeBlock's 12/4 split onto the federation (in-family = within a bake, out-of-family = across bakes). canonical_node.rs:646-649 documents in_family as "basin-local" and out_family as "inherited adapter slots (out-of-family interfaces)" — family is a tier BELOW classid, so one bake contains many families and out-of-family means out-of-BASIN, including other basins in the same bake. Every slot is one byte, a basin-local ref capped at 256 targets, so it cannot address a node in another bake whose classid differs; global reach is classid + cascade prefix. Therefore "4 out-of-family slots is a hard budget on reachable domains" does not follow, and cross-bake relations must be carried as edge ROWS with full (classid, identity) on both endpoints, never in the 16-byte EdgeBlock. Consequence recorded: the relation lane is the federation fabric, so an untyped relation there is a broken join between bakes rather than a lost label — which raises the edge-side pre-bake type-resolution stage to a federation-level dependency, and makes the still-unmeasured coverage question about published relation typings matter more, not less. Board-only; no code touched.
Both reviewers on PR #902 converged, from opposite directions, on the same real defect: `IdentityCodebook` derives ordinals from SORTED POSITION, so an ordinal is a property of the whole key set, not of the key. Grow a book with a key that sorts early and every ordinal at or after it shifts by one — and because `IdentityQuad` is a PERSISTED payload, a facet baked against the old book then explains as a neighbouring key. `verify_bijective()` cannot catch it: it is a within-book property and BOTH revisions pass. An invariant proved inside one artifact says nothing about two artifacts that must agree with each other. Added (all additive; no existing signature changed): - `IdentityCodebook::digest()` — deterministic FNV-1a over the ORDERED key list, each key fed length-first so ["ab","c"] and ["a","bc"] cannot collide through concatenation. A bake records it beside its rows; a later read compares and refuses a shifted book instead of resolving a wrong key. A WITNESS, not an enforcement — stated as such rather than papered over. - Module-doc section stating the constraint, and why there is deliberately no append-preserving constructor (a book that preserved assignments would not be sorted and `ordinal()`'s binary_search would be unsound — growing a book is a REBAKE). - `growing_a_codebook_with_an_early_key_renumbers_the_existing_ones` — makes the hazard observable: asserts the mis-explanation happens AND that both books pass `verify_bijective`. Falsifier stated inline. - `the_digest_fires_on_a_shift_and_stays_silent_on_an_equivalent_book` — fire/silence pair on one value: different digest for a renumbering book, SAME digest for the same key set in a different input order, different digests for the concatenation-colliding pair. Mutation-tested: deleting the length prefix fails the third assertion, so the delimiter is load-bearing. Also corrected a real wording imprecision review flagged: `2^24 - 2` is `MAX_ORDINAL`, a SLOT BOUND; `MAX_ENTRIES = 2^24 - 1` is the ENTRY COUNT `check_capacity` gates (ordinals start at zero). Both now named separately in the module doc and on the board. Board hygiene: EPIPHANIES PREPEND (`E-A-SORTED-CODEBOOK-ORDINAL-IS-A-PROPERTY-OF-THE-KEY-SET-NOT-THE-KEY-1`), ISSUES PREPEND (`ISS-IDENTITY-CODEBOOK-ORDINAL-STABILITY` — the digest is a witness not an enforcement; cross-bake agreement still unbuilt), LATEST_STATE contract-inventory line for the new method. NOT done, and why: the request to obtain a sanctioned home for the carving before publishing is not an oversight — it is the already-recorded `ISS-IDENTITY-QUAD-WIDE-CARVING-HOME`, open and awaiting an operator ruling. Moving the primitives out of `legacy_outliers` is one of the three options that issue lists; choosing among them is not a review-round decision. 14 lib tests green; `cargo fmt --check` and `cargo clippy -p lance-graph-contract --all-targets -- -D warnings` clean.
65be7d2 to
9ef9723
Compare
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_d47cbe2d-23ba-448e-90f0-dfb258ed3414) |
|
Note GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer. |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.claude/board/ISSUES.md:
- Around line 14-17: Update the IdentityCodebook::digest() mitigation
description to make recording and comparing the digest optional: state that a
caller may record it alongside written rows and may compare it before decoding
to reject shifted books, rather than implying every caller must perform those
actions.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: e652df5c-65ef-42f9-87d7-3c83dd39c467
📒 Files selected for processing (5)
.claude/board/EPIPHANIES.md.claude/board/ISSUES.md.claude/board/LATEST_STATE.mdcrates/lance-graph-contract/src/identity_quad.rscrates/lance-graph-contract/src/lib.rs
🚧 Files skipped from review as they are similar to previous changes (3)
- crates/lance-graph-contract/src/lib.rs
- crates/lance-graph-contract/src/identity_quad.rs
- .claude/board/LATEST_STATE.md
| **Mitigated, not closed.** `IdentityCodebook::digest()` shipped as the stability | ||
| witness — deterministic FNV-1a over the ordered key list, length-delimited per key. | ||
| A bake records it beside the rows it wrote; a later read compares and refuses a | ||
| shifted book. Fire/silence tested, and the length prefix was mutation-tested. |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== board entry around cited lines =="
sed -n '1,50p' .claude/board/ISSUES.md | cat -n
echo
echo "== IdentityCodebook digest references =="
rg -n "IdentityCodebook|IdentityCodebook::digest|digest\\(" --glob '!target/**' --glob '!dist/**' --glob '!build/**' . | head -200
echo
echo "== candidate files =="
git ls-files | rg 'IdentityCodebook|identity|codebook|bake|bake' | head -200Repository: AdaWorldAPI/lance-graph
Length of output: 15518
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== identity_quad digest method and surrounding docs =="
sed -n '115,145p;380,410p' crates/lance-graph-contract/src/identity_quad.rs | cat -n
echo
echo "== caller searches in Rust sources =="
rg -n "digest\\(|IdentityCodebook|IdentityQuad|soa_bake|bake\\(" crates -g '*.rs' | head -300
echo
echo "== focused soa_bake files =="
git ls-files crates/lance-graph-ontology/src/soa_bake | while read -r f; do
echo "--- $f ($(wc -l < "$f") lines) ---"
sed -n '1,220p' "$f" | cat -n
doneRepository: AdaWorldAPI/lance-graph
Length of output: 31339
Make the digest action conditional.
The mitigation says a bake records IdentityCodebook::digest() and reads refuse shifted books, but the open item says callers are not required to record or compare it. Use conditional wording such as “a caller may record it … and compare it before decoding”.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.claude/board/ISSUES.md around lines 14 - 17, Update the
IdentityCodebook::digest() mitigation description to make recording and
comparing the digest optional: state that a caller may record it alongside
written rows and may compare it before decoding to reject shifted books, rather
than implying every caller must perform those actions.
CI red is pre-existing on
|
| tree | result | digest (both sides of the assertion) |
|---|---|---|
f9206fc — main, the #901 merge |
FAILED | 15130871412783076140 |
9ef9723 — this branch, rebased on that |
FAILED | 15130871412783076140 |
Same test, same assertion at d_ign_b_lenses.rs:785, same value. This PR's footprint is crates/lance-graph-contract (one new module + a mod line) and three board files; the failing test lives in lance-graph-supervisor and does not reach identity_quad.
Without --features cycle-driver the test binary runs 0 tests, which is why this has not been visible: LATEST_STATE.md records the --features cycle-driver CI step as a standing gate that was unarmed. Arming it exposed a defect that was already there — the gate did its job.
The defect itself
The assertion is a can-fire check:
L1 can-fire: distinct lenses (z=3 Kant vs z=4 Wittgenstein) over byte-identical rows must yield distinct digests
Two distinct lenses over byte-identical rows produce the same digest, so lens selection is not discriminating — the selection is inert with respect to the digest. Note the test's own preceding output shows its L0 half passing (8 twin owners byte-identical across 48 rows; non-twin owner 8 differs), so the fixture is sound and the failure is specific to the L1 lens-selection path.
That is a real finding and worth keeping red rather than quieting: a lens that cannot change the digest carries exactly as much information as no lens at all — the same class as a guard that fires on everything, which this repo's own P0 falsifiability rule names.
Not fixed here — it is outside this PR's scope and lives in a different crate. Raising it so the red is not misattributed, and so it gets an owner rather than being absorbed as background noise. Happy to open a separate issue or PR against it if wanted.
Generated by Claude Code
What this adds
lance_graph_contract::identity_quad— the 4 × 24-bit identity tenant.A row whose identity is asserted independently by four external identifier spaces (each in the 10^5–10^7 range) had two bad options: four side tables joined per read, or a crosswalk chain walked per read. Both put work on the read path that the addresses could have carried themselves.
This is the third option: the identities are resolved before the bake, and the bake places all four into one V3 facet payload. Afterwards a read is a fixed-offset register read — no join, no crosswalk consulted, no walk. The saving is not space, it is the disappearance of the read-time join.
IdentityQuad—4 × u24over the 12-byte payload, read/written throughlegacy_outliers::LegacyOutlier::WideTriple(G2), never duplicating its bit math. Rides a realFacetCascade(into_facet/from_facet).IdentityCodebook+check_capacity— the bijectivekey ⇄ ordinalbook.QuadJoin— four codebooks, one per slot;resolveand the pull-backexplain.Additive only: one new module plus its registration. No existing behaviour, signature, or file changed.
Two decisions worth reviewing
Carving — contiguous G2
4 × u24, deliberately NOT the axis-grouped4×(8:8:8). That shape is three independently-meaningful bytes per slot — a rail reading. An exact identity split across three independently-read bytes is no longer a single invertible value, and invertibility is this tenant's whole acceptance criterion.2^24covers every space intended here with room to spare.Encoding — slots store
ordinal + 1, so raw0means absent (the CANON zero-fallback ladder). Without the offset, every codebook's zeroth entry would be indistinguishable from a never-filled slot, and a partially-joined row would silently read as fully joined. Capacity is thereforeMAX_ORDINAL = 2^24 − 2.⚠ Known tension, surfaced not resolved —
ISS-IDENTITY-QUAD-WIDE-CARVING-HOMEThe module sits in
legacy_outliers, whichle-contract.md§3a calls the V1-migration waiting room and states "new classes MUST NOT be born into G1–G3; the waiting room is not a destination." This tenant is new and intended to be permanent. I did not resolve that in code.My judgement, with the evidence, is that it is a legitimate exception — §3a's discouragement is conditional and names its two conditions, and neither holds:
IdentityCodebookrefuses to exceed it rather than saturating silently.And §3a's named exit — migrate to L4
palette256²— is not available here, which is the point: L4 is a cosine/similarity replacement and is lossy by design; there is no version of it that preserveskey → ordinal → key.The honest cost: a
u24slot has no byte axis — not shift-addressable,group_ofdoes not apply, andCascadeShapecorrectly refuses to bless it. Everything §3a says a wide carving gives up, this one gives up. The claim is only that the trade is right for identities, not that it is free.Three options are written up in
ISSUES.md(ratify the exception / mint a new sanctioned layout and move the primitives out oflegacy_outliers/ reject the carving — the last requires naming what an exact 24-bit identity should be stored as instead). This needs a ruling; it is not mine to make. Until then the module claims no sanction it does not have, andWideTriplegains no new semantics from this use.The pre-bake stage resolves TYPES, not only identities
A second operator ruling (2026-08-06) names the stage this tenant sits behind. Node type and relation/property type are the same kind of thing — one types the nodes, the other types the edges — and both resolve pre-bake, never at bake time and never at read time:
The bake never asks "what type is this" or "what relation is this." That is exactly what lets a ClassView be a pure position lookup and an edge slot be a pure position — the property this module's
the_position_is_the_typetest asserts at the identity layer.Two consequences for reviewers of this module:
⚠ This is ONE INSTANCE OF A FEDERATION — and the join-key slot is not pinned
Operator ruling, 2026-08-06: there is not one bake. Several domain bakes coexist — each with its own classid space, its own ClassView, and its own quadruple of identifier spaces appropriate to that domain. The quadruple varies by class by design, not as permitted flexibility. What must not vary is the join mechanism: one shared identity slot present in every quadruple, plus the relation edges that cross bakes.
Verified against what I actually built, and it does not meet the requirement. If one slot is the cross-bake join key, its position must be fixed and identical in every bake. In this PR the shared-identity space sits at slot 2, and nothing pins it:
IdentityQuad::slot(i)is positional and symmetric,Slot::index()reads the position from a per-repo config, and no mechanism enforces that another bake places the same space at the same index. The design permits the invariant; it does not guarantee it. That is a real defect under this model — a join key whose position varies by class must be looked up before it can be used, which defeats "the position is the type" exactly where it is most load-bearing: the one read that crosses a bake boundary.Candidate fix, deliberately not implemented (it interacts with the carving-home ruling): pin the join key to slot 0 as a contract-level invariant, so a bake cannot silently place it elsewhere. Recorded in
ISS-IDENTITY-QUAD-WIDE-CARVING-HOME.Bijectivity becomes a cross-bake property too. The shipped
verify_bijective()proveskey → ordinal → keywithin one codebook. Federation needs strictly more: the same key must resolve to the same ordinal in every bake carrying that space, or two bakes disagree about which concept they mean while each stays internally consistent. No test covers this — it is unbuilt, and it is the natural companion to a pinned slot.One reading in the ruling does NOT hold against the canon
The proposal that
EdgeBlock's 12/4 split maps onto the federation — in-family = within a bake, out-of-family = across bakes — does not survive checkingcanonical_node.rs:646-649:in_family: [u8; 12]is documented "12 local adjacency slots (basin-local)";out_family: [u8; 4]is "4 inherited adapter slots (out-of-family interfaces)". Family is a tier below classid, so one bake contains many families — "out-of-family" means out-of-basin, including other basins inside the same bake.So "4 out-of-family slots is a hard budget on how many domains a node can reach directly" does not follow. The budget is four out-of-basin adapter refs, and cross-bake reach is not expressible in them at any count.
The architectural consequence: cross-bake relations must be carried as edge rows with full
(classid, identity)on both endpoints, not in the 16-byteEdgeBlock. The existing cross-namespace edge lane already has exactly that shape, which is corroboration rather than new design.What this raises in priority
The relation lane stops being an accessory to one bake and becomes the federation fabric. An untyped relation there is not a lost label — it is a broken join between bakes. That reframes the catch-all bucket a third time and makes the edge-side pre-bake type stage a federation-level dependency. It also makes the unmeasured coverage question matter more: the cross-namespace relations a published closure claims to supply are precisely the inter-bake edges.
Scope — unchanged, deliberately
This PR remains one bake's worth of machinery and should stay that way. Unbuilt and named: the other domain bakes; the pinned join-key slot; cross-bake bijectivity agreement; and cross-bake edge resolution.
Related, and deliberately not reused
codebook::Codebookis the 1-byte, ≤255-entry per-family vocabulary where a family that outgrows it splits, never widens the byte. Right rule at rail scale, wrong instrument at this one — a 10^6-entry space cannot be reached by splitting into 256-entry families without the split becoming the address.IdentityCodebookis its 24-bit sibling with the same refuse-don't-widen discipline, not a widening of it.Sweep honesty
The 16-byte facet is still the lane unit, so 32 of them are one 512-byte block. But a
u24slot is not dword-aligned: "find every row whose slot k equals X" is a strided 3-byte compare, not a singlevpcmpeqd+vmovmskps. That is the real cost of the contiguous carving, and it is acceptable only because this is not a sweep-by-value workload — an identity slot is read at a fixed offset once a row is in hand; the sweep-by-value lanes are the byte-axis rail and palette readings, which keep their clean compare. A consumer that genuinely needs to sweep by identity value wants an index beside this, not this carving.Tests — 12, each carrying its falsifier
I-LEGACY-API-FEATURE-GATED).Two guards were mutation-tested in-session rather than trusted: replacing the duplicate check with
dedup()fails the injectivity test; dropping the absent sentinel fails four tests. One first-draft test (over_capacity_codebook_is_refused) was caught vacuous by that discipline — it asserted a value equalled itself — and was rewritten as a boundary assertion against an extractedcheck_capacity, perE-VACUOUS-ASSERTION-IS-THE-HOUSE-STYLE-1.Fixtures are synthetic; the module names no external identifier space.
Board hygiene (same commit)
LATEST_STATE.mdcontract inventory ·EPIPHANIES.md(E-AN-IDENTITY-SLOT-IS-NOT-A-RAIL-REF-WHICH-IS-WHY-A-WIDE-CARVING-CAN-BE-CORRECT-1— the byte-axis rule is a rule about references, and an exact identity is not one) ·ISSUES.md(the carving-home question above).Gates
cargo test -p lance-graph-contract12/12 new, 1135 existing intact ·cargo fmtclean ·cargo clippy -p lance-graph-contract --all-targets -- -D warningsclean.Summary by CodeRabbit
New Features
Documentation