Skip to content

contract(identity_quad): four identifier spaces joined before the bake into one 96-bit facet - #902

Merged
AdaWorldAPI merged 3 commits into
mainfrom
claude/vocab-tenant-bake
Aug 6, 2026
Merged

contract(identity_quad): four identifier spaces joined before the bake into one 96-bit facet#902
AdaWorldAPI merged 3 commits into
mainfrom
claude/vocab-tenant-bake

Conversation

@AdaWorldAPI

@AdaWorldAPI AdaWorldAPI commented Aug 6, 2026

Copy link
Copy Markdown
Owner

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.

  • IdentityQuad4 × u24 over the 12-byte payload, read/written through legacy_outliers::LegacyOutlier::WideTriple (G2), never duplicating its bit math. Rides a real FacetCascade (into_facet / from_facet).
  • IdentityCodebook + check_capacity — the bijective key ⇄ ordinal book.
  • QuadJoin — four codebooks, one per slot; resolve and the pull-back explain.

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-grouped 4×(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^24 covers every space intended here with room to spare.

Encoding — 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. Capacity is therefore MAX_ORDINAL = 2^24 − 2.

⚠ Known tension, surfaced not resolved — ISS-IDENTITY-QUAD-WIDE-CARVING-HOME

The module sits in legacy_outliers, which le-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:

  1. god-object-related — no. Four fixed slots of ONE kind (an identifier ordinal); it does not grow with the class's concerns. A fifth space is a second facet, never a wider field.
  2. lacking proper bucket rollover — no. Each slot's capacity is its codebook's size, and IdentityCodebook refuses 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 preserves key → ordinal → key.

The honest cost: a u24 slot has no byte axis — not shift-addressable, group_of does not apply, and CascadeShape correctly 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 of legacy_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, and WideTriple gains 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:

sources
  → [pre-bake join: resolve IDENTITIES and TYPES —
       node type · edge property hierarchy]
  → typed joined object
  → BAKE (pure layout)
  → register

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_type test asserts at the identity layer.

Two consequences for reviewers of this module:

  • A consumer's catch-all relation bucket is a missing STAGE, not a data defect. Where a consumer buckets unrecognised relations into a generic slot, the cause is that nothing resolved the property hierarchy into edge types before the bake. Widening the type palette moves the catch-all's boundary and leaves the pipeline unchanged; adding the stage is the correct fix. These are not variants of each other.
  • This module implements the node side of that stage only. The edge-side twin — resolving the property hierarchy into edge-slot type assignments — is unbuilt, and under the federation ruling below it is a federation-level dependency rather than a local nicety.

⚠ 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() proves key → ordinal → key within 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 checking canonical_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.
  • Every slot is one byte: a basin-local ref capped at 256 targets, never a global pointer. It cannot address a node in another bake, whose classid differs entirely; global reach is classid + cascade prefix.

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-byte EdgeBlock. 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::Codebook is 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. IdentityCodebook is 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 u24 slot is not dword-aligned: "find every row whose slot k equals X" is a strided 3-byte compare, not a single vpcmpeqd + 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

  • Field-isolation matrix over all four slots (I-LEGACY-API-FEATURE-GATED).
  • Whole-book bijectivity sweep, not a spot check (an off-by-one breaks the middle of a book).
  • The negative case: a non-injective key list is refused at construction, so a many-to-one mapping cannot exist to be found later — paired with a non-duplicate list that builds, so the guard discriminates.
  • Capacity gate asserted at both sides of its boundary.
  • Join anti-vacuity (genuinely multi-slot AND genuinely partial rows) plus can-stay-silent (an unknown key stays absent, never fabricated).

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 extracted check_capacity, per E-VACUOUS-ASSERTION-IS-THE-HOUSE-STYLE-1.

Fixtures are synthetic; the module names no external identifier space.

Board hygiene (same commit)

LATEST_STATE.md contract 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-contract 12/12 new, 1135 existing intact · cargo fmt clean · cargo clippy -p lance-graph-contract --all-targets -- -D warnings clean.

Summary by CodeRabbit

  • New Features

    • Added support for compact four-part identity payloads with optional values and facet attachment.
    • Added key-to-ordinal mapping with reverse lookup, capacity limits, duplicate detection, and deterministic consistency checks.
    • Added bake-time resolution of external keys and explanation of stored identities back into readable keys.
    • Added safeguards for invalid slots, oversized values, missing keys, and ambiguous mappings.
  • Documentation

    • Documented identity encoding, cross-bake consistency requirements, validation rules, and known ordinal stability considerations.

@cursor

cursor Bot commented Aug 6, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot 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)

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds a public identity_quad module with four-slot 24-bit identity encoding, bijective codebooks, digest validation, bake-time joins, reverse explanation, tests, and contract records.

Changes

Identity quad contract

Layer / File(s) Summary
Identity payload encoding
crates/lance-graph-contract/src/identity_quad.rs, crates/lance-graph-contract/src/lib.rs
Adds bounded four-slot encoding, ordinal-plus-one absence handling, facet conversion, and payload tests.
Codebook validation and digesting
crates/lance-graph-contract/src/identity_quad.rs, .claude/board/EPIPHANIES.md
Adds capacity checks, bijective mappings, deterministic digests, ordinal-renumbering tests, and related design records.
Quad joins and contract records
crates/lance-graph-contract/src/identity_quad.rs, .claude/board/*
Adds four-way key resolution, reverse explanation, unknown-key handling, integration tests, and contract issue records.

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
Loading

Suggested reviewers: claude

Poem

A rabbit maps each key in line,
Four slots fit in fields of nine.
Digests guard the ordinal way,
Joins resolve what keys convey,
And absent values stay defined.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the primary change: joining four identifier spaces into one 96-bit facet before baking.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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()'s binary_search would 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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.mdISS-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 IdentityCodebook refuses 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 preserves key → ordinal → key.
  • The cost is stated, not hidden. A u24 has no byte axis: not shift-addressable, group_of does not apply, and it will not sweep with a clean vpcmpeqd. 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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 311f765 and 95e6a43.

📒 Files selected for processing (5)
  • .claude/board/EPIPHANIES.md
  • .claude/board/ISSUES.md
  • .claude/board/LATEST_STATE.md
  • crates/lance-graph-contract/src/identity_quad.rs
  • crates/lance-graph-contract/src/lib.rs

Comment thread .claude/board/EPIPHANIES.md Outdated
Comment thread crates/lance-graph-contract/src/identity_quad.rs
AdaWorldAPI pushed a commit that referenced this pull request Aug 6, 2026
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.
@AdaWorldAPI

Copy link
Copy Markdown
Owner Author

Review round complete — 4 comments, 3 fixed, 1 deferred to an operator ruling, 0 ignored

Commit 1cf86cb, plus 65be7d2 merging main to clear the conflict CodeRabbit flagged. Every change additive: no existing signature altered, no behaviour changed. cargo fmt --check and cargo clippy -p lance-graph-contract --all-targets -- -D warnings clean; 14 lib tests green (12 + 2 from this round).

The real defect, found independently by both reviewers from opposite directions. IdentityCodebook::try_new sorts and derives each ordinal from the 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 explains as a neighbouring key through the new one.

The part worth keeping is why the existing guard could not catch it: verify_bijective() sweeps the whole book and passes on both revisions, because each is internally consistent. An invariant proved inside one artifact says nothing about two artifacts that must agree with each other — and a persisted encoding always has the second problem whether or not anyone wrote it down.

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.
  • Two falsifiable tests. growing_a_codebook_with_an_early_key_renumbers_the_existing_ones makes the hazard observable — it asserts the mis-explanation happens and that both books pass verify_bijective — so the constraint is demonstrated rather than claimed. the_digest_fires_on_a_shift_and_stays_silent_on_an_equivalent_book is the 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-checked: deleting the length prefix fails the third assertion, so the delimiter is load-bearing rather than decoration.
  • Capacity wording corrected (the third comment, and it was right): 2^24 - 2 is MAX_ORDINAL, a slot bound; MAX_ENTRIES = 2^24 - 1 is the entry count check_capacity gates, since ordinals start at zero. Both now named separately in the module doc and on the board. The arithmetic was always sound; only the prose conflated them.

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 ordinal()'s binary_search would be unsound, so growing a book is a rebake. Both limits, plus the still-unbuilt cross-bake agreement check, are recorded as ISS-IDENTITY-CODEBOOK-ORDINAL-STABILITY rather than left implied.

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 ISS-IDENTITY-QUAD-WIDE-CARVING-HOME, added in this PR, marked OPEN, NEEDS AN OPERATOR RULING, with three options listed. The short form — §3a's discouragement is conditional and neither condition holds (four fixed slots of one kind, not a god object; capacity refuses rather than saturates), and §3a's named exit (migrate to L4 palette256²) is unavailable because L4 is lossy by design while this carving's acceptance criterion is exact invertibility. The honest cost is real and stated in the module doc: a u24 has no byte axis, is not shift-addressable, and group_of does not apply. Choosing between ratify the exception and mint a sanctioned layout amends an operator-locked document, which is not a review-round decision.

Board hygiene: EPIPHANIES PREPEND (E-A-SORTED-CODEBOOK-ORDINAL-IS-A-PROPERTY-OF-THE-KEY-SET-NOT-THE-KEY-1), ISSUES PREPEND ×1, LATEST_STATE contract-inventory line for the new method.


Generated by Claude Code

claude added 3 commits August 6, 2026 13:51
…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.
@AdaWorldAPI
AdaWorldAPI force-pushed the claude/vocab-tenant-bake branch from 65be7d2 to 9ef9723 Compare August 6, 2026 13:54
@cursor

cursor Bot commented Aug 6, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot 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)

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between f9206fc and 9ef9723.

📒 Files selected for processing (5)
  • .claude/board/EPIPHANIES.md
  • .claude/board/ISSUES.md
  • .claude/board/LATEST_STATE.md
  • crates/lance-graph-contract/src/identity_quad.rs
  • crates/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

Comment thread .claude/board/ISSUES.md
Comment on lines +14 to +17
**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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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 -200

Repository: 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
done

Repository: 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.

@AdaWorldAPI

Copy link
Copy Markdown
Owner Author

CI red is pre-existing on main, not caused by this PR

d_ign_b_lenses::d_ign_b_lens_selection_over_byte_identical_rows fails identically on main, with none of this branch's code in the tree.

Reproduced both sides, same command:

cargo test -p lance-graph-supervisor --features cycle-driver --test d_ign_b_lenses
tree result digest (both sides of the assertion)
f9206fcmain, 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

@AdaWorldAPI
AdaWorldAPI merged commit feddd52 into main Aug 6, 2026
7 of 9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants