From 13be86e1ec4e4373d4c9f87290ad1bb713867282 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 28 Jun 2026 22:12:12 +0000 Subject: [PATCH 1/8] build(toolchain): bump 1.94.0 sub-toolchain stragglers to 1.95 python/, crates/bge-m3/, and crates/reader-lm/ still pinned channel 1.94.0 while the root rust-toolchain.toml (and bge-m3/reader-lm's own Cargo.toml rust-version) are already 1.95. Align all three to the ecosystem-wide 1.95 pin. Co-Authored-By: Claude --- crates/bge-m3/rust-toolchain.toml | 4 +++- crates/reader-lm/rust-toolchain.toml | 4 +++- python/rust-toolchain.toml | 4 +++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/crates/bge-m3/rust-toolchain.toml b/crates/bge-m3/rust-toolchain.toml index 76a06e6b8..2d8dd2bea 100644 --- a/crates/bge-m3/rust-toolchain.toml +++ b/crates/bge-m3/rust-toolchain.toml @@ -1,2 +1,4 @@ [toolchain] -channel = "1.94.0" +# Aligned to the ecosystem-wide 1.95 pin; matches this crate's own +# Cargo.toml rust-version = "1.95" (the channel was a 1.94.0 straggler). +channel = "1.95" diff --git a/crates/reader-lm/rust-toolchain.toml b/crates/reader-lm/rust-toolchain.toml index 76a06e6b8..2d8dd2bea 100644 --- a/crates/reader-lm/rust-toolchain.toml +++ b/crates/reader-lm/rust-toolchain.toml @@ -1,2 +1,4 @@ [toolchain] -channel = "1.94.0" +# Aligned to the ecosystem-wide 1.95 pin; matches this crate's own +# Cargo.toml rust-version = "1.95" (the channel was a 1.94.0 straggler). +channel = "1.95" diff --git a/python/rust-toolchain.toml b/python/rust-toolchain.toml index 76a06e6b8..096d85509 100644 --- a/python/rust-toolchain.toml +++ b/python/rust-toolchain.toml @@ -1,2 +1,4 @@ [toolchain] -channel = "1.94.0" +# Aligned to the AdaWorldAPI ecosystem-wide 1.95 pin (root lance-graph +# rust-toolchain.toml). Was a 1.94.0 straggler. +channel = "1.95" From cd6160d26c9e3ce407660944d1a60780a3be28e6 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 29 Jun 2026 15:30:43 +0000 Subject: [PATCH 2/8] =?UTF-8?q?feat(contract):=20facet::CascadeShape=20?= =?UTF-8?q?=E2=80=94=20one=20cascade=20algebra,=20three=20carvings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the G×D=12 cascade carving algebra to lance-graph-contract::facet, the shared substrate the compiled-ClassView spine and the per-language SDKs read. - CascadeShape {G6D2, G4D3, G3D4}: carve the 12 cascade units as G groups × D levels, G·D = CASCADE_UNITS = 12 always. groups()/levels(), index(g,l) = g·D+l, inverses group_of/level_of (G3D4/G6D2 group_of is a shift — the canon 'tier-of-level is a shift, never a branch'; G4D3 is the straddle that divides). - FacetCascade::tier_bytes() exposes the 12 cascade bytes as one coarse→fine ladder (hi then lo per tier); cascade_byte(shape,g,l) reads one unit; cascade_group_shared(other,shape,g) is the per-group coarse→fine LCP redout. - Unit-agnostic: the same index math addresses the facet bytes AND a 12-field class — the OGAR GUID 3×4-vs-4×3 debate generalised from nibbles to bytes. Zero-dep const fn; +3 tests + 3 compile-time G·D asserts; lib facet 7/7 green, clippy -D warnings clean (probe-workspace verified offline). Board hygiene: LATEST_STATE.md Contract Inventory updated in the same commit. Co-Authored-By: Claude --- .claude/board/LATEST_STATE.md | 8 + crates/lance-graph-contract/src/facet.rs | 246 +++++++++++++++++++++++ 2 files changed, 254 insertions(+) diff --git a/.claude/board/LATEST_STATE.md b/.claude/board/LATEST_STATE.md index 26c37bf78..3c22e1109 100644 --- a/.claude/board/LATEST_STATE.md +++ b/.claude/board/LATEST_STATE.md @@ -657,3 +657,11 @@ PR sequence: #360 → #361 → post-#360 substrate-sweep (this PR). - **`canonical_node::ValueSchema` + `ValueTenant` + `VALUE_TENANTS`** (NEW; re-exported from `lib.rs`). The **value-side analog of `EdgeCodecFlavor`**: per-class presets for which tenants the 480-byte `NodeRow::value` slab materialises. `ValueTenant` = 9 stable append-only positions (discriminant == `FieldMask` bit == `VALUE_TENANTS` index): Meta(`MetaWord`) · Qualia(`QualiaI4_16D`) · MaterializedEdges(4×`CausalEdge64`) · Fingerprint(32 B) · **HelixResidue(48 B)** · **TurbovecResidue(`Pq32x4` 16 B)** · Energy(f32) · Plasticity(u32) · EntityType(u16). `VALUE_TENANTS` = the stable row-relative byte carve `[32,186)` (reserve-don't-reclaim, contiguous, compile-time asserted ≤ 480). Presets: `Bootstrap` (EMPTY, zero-fallback **default**) · `Cognitive` (58 B: hot SoA columns, no codec residues) · `Compressed` (98 B: codec stack + fingerprint, no hot lifecycle) · `Full` (154 B: all 9 tenants). Built on existing `class_view::FieldMask` (presence) + `soa_envelope::ColumnDescriptor` — **no new presence type** (per "refactor into what exists"). `is_layout_preserving()` always `true` (carves WITHIN the reserved slab; `NODE_ROW_STRIDE=512` untouched → no `ENVELOPE_LAYOUT_VERSION` bump). Selection surface: defaulted `ClassView::value_schema(&self, ClassId) -> ValueSchema` (default `Bootstrap`, non-breaking — mirrors `edge_codec_flavor`). **Closes the SoA-extension dilution gap**: the formerly-comment-only helix-48 is now a first-class tenant alongside turbovec-`Pq32x4` + signed-`CoarseResidue`. +6 tests + 3 compile-time canon asserts; contract lib 611 green; clippy `-D warnings` + fmt clean. - **Encode/measure kernels live in `ndarray` (the hardware layer), not the contract:** `ndarray::hpc::edge_codec` (Codebook k-means, `CoarseResidueCodec`, `ProductQuantizer`, `reconstruct_coarse`) + `ndarray::hpc::reliability` (Pearson r, Spearman ρ, Cronbach α, ICC(2,1), `FidelityReport`). Harness `examples/edge_codec_compare` measures all flavors × {blob, continuous} regimes. **Measured:** CoarseResidue dominates agreement (ICC 0.97–0.99, ρ 0.98, α 0.99); Pq32x4 keeps rank (ρ 0.60–0.67) but not absolute distance (ICC 0.11–0.29); CoarseOnly collapses on continuous (ICC 0.003); AMX `matmul_i8_to_i32` assign 100% vs scalar, 24–28 GMAC/s. ndarray commit `d3b608f`. - **Deferred (flagged):** turbovec PQ4 *throughput* path (the FastScan nibble-LUT for the Pq32x4 flavor) blocked on the **#493 P2** build break — `lance-graph-turbovec` requests the `ndarray-simd` turbovec feature that was REMOVED (turbovec commit `7fa217c`); the polyfill fns are gone. turbovec's API is end-to-end (owns encode+scan), so it is a *PQ4 flavor*, not a residue-nibble-scan primitive. Fidelity (what ICC/Pearson/α measure) is independent of the fast kernel, so this is throughput-only follow-up. + +## 2026-06-29 — Append: `facet::CascadeShape` cascade algebra (branch `claude/odoo-rs-transcode-lf8ya5`) + +(Per APPEND-ONLY rule: new top-of-inventory entry. Branch work; records the contract type so a new session does not re-derive it. Part of the OGAR transpile-substrate arc — the compiled-`ClassView`-spine work, `OGAR/docs/OGAR-TRANSPILE-SUBSTRATE.md` §1.5/§1.6.) + +### Current Contract Inventory — new entry + +- **`facet::CascadeShape` + `facet::CASCADE_UNITS` + `FacetCascade::{tier_bytes, cascade_byte, cascade_group_shared}`** (NEW; `lance-graph-contract::facet`, zero-dep `const fn`). The **one cascade algebra, three carvings** the operator confirmed (carving = "Both — one cascade algebra"; home = "lance-graph-contract (FacetCascade/ClassView)"). `CASCADE_UNITS = 12` = the facet's 6×2 tier-bytes = a 12-field class's fields — the algebra is unit-agnostic. `CascadeShape::{G6D2, G4D3, G3D4}` carves the 12-unit ladder as `G groups × D levels` with `G·D = 12` always (`6×(1:2)` native per-tier `(hi:lo)`; `3×(1:2:3:4)` byte-aligned tier-pair super-groups; `4×(1:2:3)` the straddle). Methods: `groups()/levels()`, `index(g,l) = g·D+l`, inverses `group_of`/`level_of` (`G3D4`/`G6D2` `group_of` is a shift — the canon's "tier-of-level is a shift, never a branch"; `G4D3` needs a divide), `ALL`. `FacetCascade::tier_bytes()` exposes the 12 cascade bytes as one coarse→fine ladder (`hi` then `lo` per tier); `cascade_byte(shape,g,l)` reads one unit; `cascade_group_shared(other,shape,g)` is the per-group coarse→fine LCP redout. This is the OGAR GUID `3×4`-vs-`4×3` debate generalised from nibble-units to byte/field-units — **the shared substrate the three language SDKs (§1.6) all read.** +3 tests (`cascade_shape_is_one_algebra_three_carvings`, `tier_bytes_ladder_and_per_carving_grouping`, `cascade_group_shared_is_per_group_lcp`) + 3 compile-time `G·D == CASCADE_UNITS` asserts. Lib facet tests 7/7 green; clippy `-D warnings` clean (probe-workspace verified offline — the workspace ndarray git dep is 403 offline). diff --git a/crates/lance-graph-contract/src/facet.rs b/crates/lance-graph-contract/src/facet.rs index 8d6eb4418..67c7915b7 100644 --- a/crates/lance-graph-contract/src/facet.rs +++ b/crates/lance-graph-contract/src/facet.rs @@ -286,8 +286,162 @@ impl FacetCascade { } m } + + /// The 12 cascade tier-bytes as **one coarse→fine ladder** — `hi` then `lo` + /// per tier, tiers in order: + /// `[t0.hi, t0.lo, t1.hi, t1.lo, … t5.hi, t5.lo]`. Excludes the 4-byte + /// [`facet_classid`](Self::facet_classid). This is the input the + /// [`CascadeShape`] algebra re-carves; it is byte-for-byte the same 12-unit + /// ladder a 12-field class exposes, so a ClassView addresses *either* the + /// facet bytes or its own fields with the identical `(group, level)` math. + /// (Distinct from [`hi_chain`](Self::hi_chain)/[`lo_chain`](Self::lo_chain), + /// which group by *axis* across tiers; this groups by *position* in the + /// ladder — the `(1:2)`/`(1:2:3)`/`(1:2:3:4)` view.) + #[inline] + #[must_use] + pub const fn tier_bytes(self) -> [u8; CASCADE_UNITS] { + let t = &self.tiers; + [ + t[0].hi, t[0].lo, t[1].hi, t[1].lo, t[2].hi, t[2].lo, t[3].hi, t[3].lo, t[4].hi, t[4].lo, + t[5].hi, t[5].lo, + ] + } + + /// The cascade byte at `(group, level)` under `shape` — + /// `tier_bytes()[shape.index(group, level)]`. The same lookup whether the + /// ClassView reads the facet as `6×2`, `4×3`, or `3×4`; the bytes never move. + #[inline] + #[must_use] + pub const fn cascade_byte(self, shape: CascadeShape, group: u8, level: u8) -> u8 { + self.tier_bytes()[shape.index(group, level)] + } + + /// Coarse→fine shared-prefix length (`0..=D`) of one group `g` between two + /// facets under `shape` — the **per-group LCP redout**; `D` ⇒ that group's + /// whole `(1:…:D)` hierarchy agrees. The per-carving refinement of the + /// whole-facet [`shared_prefix_tiles`](Self::shared_prefix_tiles): pick the + /// carving, then read locality one group (one axis of meaning) at a time. + #[inline] + #[must_use] + pub const fn cascade_group_shared(self, other: Self, shape: CascadeShape, group: u8) -> u8 { + let (a, b) = (self.tier_bytes(), other.tier_bytes()); + let d = shape.levels(); + let base = shape.index(group, 0); + let mut n = 0u8; + while n < d && a[base + n as usize] == b[base + n as usize] { + n += 1; + } + n + } } +/// The number of cascade tier-bytes a [`FacetCascade`] carries (excludes the +/// 4-byte [`FacetCascade::facet_classid`]): `6 tiers × 2 bytes`. Equivalently +/// the field-count of a 12-field class — the cascade algebra is unit-agnostic, +/// so the same `G·D = CASCADE_UNITS` invariant binds bytes and fields alike. +pub const CASCADE_UNITS: usize = 12; + +/// **One cascade algebra, three carvings.** The 12 cascade units (the facet's +/// [`tier_bytes`](FacetCascade::tier_bytes), or a 12-field class's fields) are +/// read as `G groups × D levels` with `G·D = CASCADE_UNITS` *always*. The units +/// never move — the ClassView picks the carving and the same index math +/// (`i = g·D + l`) addresses any of them. +/// +/// This is the OGAR GUID's `3×4`-vs-`4×3` debate **generalized**: there the +/// unit is a nibble and the ladder is 12 levels; here the unit is a byte (or a +/// class field) and the ladder is the same 12. The carvings inherit the canon's +/// known trade-offs exactly: +/// +/// | shape | groups × levels | notation | alignment | +/// |---|---|---|---| +/// | [`G6D2`](Self::G6D2) | 6 × 2 | `6×(1:2)` | native per-tier `(hi:lo)`; `group_of` is `i >> 1` | +/// | [`G4D3`](Self::G4D3) | 4 × 3 | `4×(1:2:3)` | **straddles** tier boundaries (the canon's "1.5-byte" cost); `group_of` needs a real divide | +/// | [`G3D4`](Self::G3D4) | 3 × 4 | `3×(1:2:3:4)` | tier-pair super-groups, byte-aligned; `group_of` is `i >> 2` (the canon's "tier-of-level is a shift, never a branch") | +/// +/// All three are legal so the algebra is total; the ClassView chooses by +/// efficiency. With 12 fields it is often cheaper to map a sub-range as a +/// hierarchy (a deep group) and stack **nested** ClassViews into constructors +/// before materializing the `32×GUID` SoA — see +/// `docs/OGAR-TRANSPILE-SUBSTRATE.md` §1.5. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum CascadeShape { + /// 6 groups × 2 levels — native `(hi:lo)` per tier (`6×(1:2)`). + G6D2, + /// 4 groups × 3 levels (`4×(1:2:3)`) — straddles tier boundaries. + G4D3, + /// 3 groups × 4 levels — tier-pair super-groups (`3×(1:2:3:4)`). + G3D4, +} + +impl CascadeShape { + /// All three carvings, group-count ascending (coarsest split first). + pub const ALL: [CascadeShape; 3] = [CascadeShape::G3D4, CascadeShape::G4D3, CascadeShape::G6D2]; + + /// `G` — number of groups (axes of meaning). `groups() · levels() == CASCADE_UNITS`. + #[inline] + #[must_use] + pub const fn groups(self) -> u8 { + match self { + CascadeShape::G6D2 => 6, + CascadeShape::G4D3 => 4, + CascadeShape::G3D4 => 3, + } + } + + /// `D` — levels per group: the depth of the `(1:2:…:D)` coarse→fine ladder. + /// `groups() · levels() == CASCADE_UNITS`. + #[inline] + #[must_use] + pub const fn levels(self) -> u8 { + match self { + CascadeShape::G6D2 => 2, + CascadeShape::G4D3 => 3, + CascadeShape::G3D4 => 4, + } + } + + /// Linear unit index of `(group, level)`: `group · D + level` — groups laid + /// out in order, coarse→fine within each. The single shared addressing rule + /// for facet bytes *and* class fields. + #[inline] + #[must_use] + pub const fn index(self, group: u8, level: u8) -> usize { + (group * self.levels() + level) as usize + } + + /// Inverse of [`index`](Self::index): which group linear unit `i` belongs to + /// (`i / D`). For [`G3D4`](Self::G3D4)/[`G6D2`](Self::G6D2) (`D` a power of + /// two) this is a shift (`i >> 2` / `i >> 1`) — the canon's "tier-of-level is + /// a shift, never a branch"; [`G4D3`](Self::G4D3) needs a real divide (its + /// straddle cost made explicit). + #[inline] + #[must_use] + pub const fn group_of(self, unit: usize) -> u8 { + (unit / self.levels() as usize) as u8 + } + + /// Inverse of [`index`](Self::index): the within-group level of unit `i` + /// (`i % D`). + #[inline] + #[must_use] + pub const fn level_of(self, unit: usize) -> u8 { + (unit % self.levels() as usize) as u8 + } +} + +const _: () = assert!( + CascadeShape::G6D2.groups() as usize * CascadeShape::G6D2.levels() as usize == CASCADE_UNITS, + "6×2 = 12" +); +const _: () = assert!( + CascadeShape::G4D3.groups() as usize * CascadeShape::G4D3.levels() as usize == CASCADE_UNITS, + "4×3 = 12" +); +const _: () = assert!( + CascadeShape::G3D4.groups() as usize * CascadeShape::G3D4.levels() as usize == CASCADE_UNITS, + "3×4 = 12" +); + #[cfg(test)] mod tests { use super::*; @@ -366,6 +520,98 @@ mod tests { assert_eq!(h.row_match_mask(f), 0b1110); } + #[test] + fn cascade_shape_is_one_algebra_three_carvings() { + // G·D == 12 for every carving, and ALL is exhaustive. + for s in CascadeShape::ALL { + assert_eq!( + s.groups() as usize * s.levels() as usize, + CASCADE_UNITS, + "every carving covers all 12 units exactly" + ); + } + assert_eq!(CascadeShape::ALL.len(), 3); + + // index / group_of / level_of are mutual inverses over the whole ladder. + for s in CascadeShape::ALL { + for unit in 0..CASCADE_UNITS { + let (g, l) = (s.group_of(unit), s.level_of(unit)); + assert!(g < s.groups() && l < s.levels()); + assert_eq!(s.index(g, l), unit, "{s:?}: index∘(group,level) = id"); + } + } + + // Power-of-two carvings: group_of IS the canon's shift (no branch); + // 4×3 is the straddle that needs a real divide. + for unit in 0..CASCADE_UNITS { + assert_eq!(CascadeShape::G6D2.group_of(unit) as usize, unit >> 1); + assert_eq!(CascadeShape::G3D4.group_of(unit) as usize, unit >> 2); + assert_eq!(CascadeShape::G4D3.group_of(unit) as usize, unit / 3); + } + } + + #[test] + fn tier_bytes_ladder_and_per_carving_grouping() { + let f = FacetCascade::from_bytes(&sample()); + + // The 12-unit ladder: hi then lo per tier, coarse→fine. (hi = sample odd + // bytes 0xAB..0x56; lo = even bytes 0x01..0x06.) + assert_eq!( + f.tier_bytes(), + [0xAB, 0x01, 0xCD, 0x02, 0xEF, 0x03, 0x12, 0x04, 0x34, 0x05, 0x56, 0x06] + ); + + // 6×2: group g == tier g's (hi, lo) — the native pairing. + for g in 0..6u8 { + assert_eq!(f.cascade_byte(CascadeShape::G6D2, g, 0), f.tiers[g as usize].hi); + assert_eq!(f.cascade_byte(CascadeShape::G6D2, g, 1), f.tiers[g as usize].lo); + } + // 3×4: group 0 spans tiers 0–1 (4 bytes), byte-aligned super-group. + assert_eq!( + [ + f.cascade_byte(CascadeShape::G3D4, 0, 0), + f.cascade_byte(CascadeShape::G3D4, 0, 1), + f.cascade_byte(CascadeShape::G3D4, 0, 2), + f.cascade_byte(CascadeShape::G3D4, 0, 3), + ], + [0xAB, 0x01, 0xCD, 0x02] + ); + // 4×3: group 0 straddles tier 0 fully + tier 1's hi (the 1.5-tier cost). + assert_eq!( + [ + f.cascade_byte(CascadeShape::G4D3, 0, 0), + f.cascade_byte(CascadeShape::G4D3, 0, 1), + f.cascade_byte(CascadeShape::G4D3, 0, 2), + ], + [0xAB, 0x01, 0xCD] + ); + } + + #[test] + fn cascade_group_shared_is_per_group_lcp() { + let f = FacetCascade::from_bytes(&sample()); + + // identical ⇒ every group's whole ladder agrees (== D). + for s in CascadeShape::ALL { + for g in 0..s.groups() { + assert_eq!(f.cascade_group_shared(f, s, g), s.levels()); + } + } + + // Perturb tier1's hi byte (ladder unit 2). Under 6×2 that is group 1, + // level 0 ⇒ group 1 diverges immediately (shared 0); group 0 untouched. + let mut b = sample(); + b[7] = 0x99; // tier1.hi + let g = FacetCascade::from_bytes(&b); + assert_eq!(f.cascade_group_shared(g, CascadeShape::G6D2, 0), 2, "tier0 intact"); + assert_eq!(f.cascade_group_shared(g, CascadeShape::G6D2, 1), 0, "tier1.hi differs first"); + + // Under 3×4 unit 2 is group 0, level 2 ⇒ group 0 shares its first 2 + // levels then breaks; group 1 (tiers 2–3) fully intact. + assert_eq!(f.cascade_group_shared(g, CascadeShape::G3D4, 0), 2); + assert_eq!(f.cascade_group_shared(g, CascadeShape::G3D4, 1), 4); + } + #[test] fn reinterpret_is_a_no_op() { // align(16) ⇒ the facet's own bytes are 16-aligned, so the zero-copy borrow From 18286a0fd7042c5f48b7157aa93290f6289bd535 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 29 Jun 2026 16:02:48 +0000 Subject: [PATCH 3/8] refactor(contract): CascadeShape view-rotations + ClassArm switch (review #621) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address operator review on #621: 1. G4D3 (the divide/straddle) is the WORST CASE, not a co-equal carving. Carvings are VIEW rotations of the 12-unit ladder: - CascadeShape::ALIGNED = [G3D4, G6D2] — byte-aligned defaults; group_of is a pure SHIFT (canon 'tier-of-level is a shift, never a branch'). New shift() -> Option returns Some(2)/Some(1); is_byte_aligned() == true. - CascadeShape::ROTATIONS = [G3D4, G4D3, G6D2] — the full rotation set a ClassView may rotate through. A ClassView can ALWAYS rotate (read the same bytes under a different carving) per class — the relief valve for classid-stacking entropy (rare, e.g. some Odoo classes: rotate instead of minting another classid). - G4D3: shift() == None, is_byte_aligned() == false, group_of DIVIDES — excluded from ALIGNED, kept in ROTATIONS only as the deliberate rare escape hatch. (replaces the old co-equal ALL constant.) 2. Functions are NOT a facet carving. New ClassArm { View, Functions } — the classid is an additional THINK/DO switch (OGAR-AST-CONTRACT). Behaviour is reached by switching the classid to the Functions arm (ActionDef/KausalSpec on the resolved node); carvings address the View arm only. A straddling carve to 'get to' a function is exactly the worst case G4D3 warns against. +4 facet tests (cascade_rotations_are_total_but_only_aligned_are_defaults, classid_switch_separates_view_from_functions, + updated grouping/LCP tests); 8/8 lib facet green, clippy -D warnings + rustfmt clean (probe-verified offline). Board: LATEST_STATE.md Contract Inventory entry updated in the same commit. Co-Authored-By: Claude --- .claude/board/LATEST_STATE.md | 7 +- crates/lance-graph-contract/src/facet.rs | 255 ++++++++++++++++++----- 2 files changed, 209 insertions(+), 53 deletions(-) diff --git a/.claude/board/LATEST_STATE.md b/.claude/board/LATEST_STATE.md index 3c22e1109..18a025072 100644 --- a/.claude/board/LATEST_STATE.md +++ b/.claude/board/LATEST_STATE.md @@ -664,4 +664,9 @@ PR sequence: #360 → #361 → post-#360 substrate-sweep (this PR). ### Current Contract Inventory — new entry -- **`facet::CascadeShape` + `facet::CASCADE_UNITS` + `FacetCascade::{tier_bytes, cascade_byte, cascade_group_shared}`** (NEW; `lance-graph-contract::facet`, zero-dep `const fn`). The **one cascade algebra, three carvings** the operator confirmed (carving = "Both — one cascade algebra"; home = "lance-graph-contract (FacetCascade/ClassView)"). `CASCADE_UNITS = 12` = the facet's 6×2 tier-bytes = a 12-field class's fields — the algebra is unit-agnostic. `CascadeShape::{G6D2, G4D3, G3D4}` carves the 12-unit ladder as `G groups × D levels` with `G·D = 12` always (`6×(1:2)` native per-tier `(hi:lo)`; `3×(1:2:3:4)` byte-aligned tier-pair super-groups; `4×(1:2:3)` the straddle). Methods: `groups()/levels()`, `index(g,l) = g·D+l`, inverses `group_of`/`level_of` (`G3D4`/`G6D2` `group_of` is a shift — the canon's "tier-of-level is a shift, never a branch"; `G4D3` needs a divide), `ALL`. `FacetCascade::tier_bytes()` exposes the 12 cascade bytes as one coarse→fine ladder (`hi` then `lo` per tier); `cascade_byte(shape,g,l)` reads one unit; `cascade_group_shared(other,shape,g)` is the per-group coarse→fine LCP redout. This is the OGAR GUID `3×4`-vs-`4×3` debate generalised from nibble-units to byte/field-units — **the shared substrate the three language SDKs (§1.6) all read.** +3 tests (`cascade_shape_is_one_algebra_three_carvings`, `tier_bytes_ladder_and_per_carving_grouping`, `cascade_group_shared_is_per_group_lcp`) + 3 compile-time `G·D == CASCADE_UNITS` asserts. Lib facet tests 7/7 green; clippy `-D warnings` clean (probe-workspace verified offline — the workspace ndarray git dep is 403 offline). +- **`facet::CascadeShape` + `facet::ClassArm` + `facet::CASCADE_UNITS` + `FacetCascade::{tier_bytes, cascade_byte, cascade_group_shared}`** (NEW; `lance-graph-contract::facet`, zero-dep `const fn`). Carving = "Both — one cascade algebra"; home = "lance-graph-contract (FacetCascade/ClassView)". `CASCADE_UNITS = 12` = the facet's 6×2 tier-bytes = a 12-field class's fields — unit-agnostic. **Carvings are VIEW rotations, not function layouts** (operator correction 2026-06-29): + - `CascadeShape::{G6D2, G4D3, G3D4}` carves the 12-unit ladder as `G groups × D levels`, `G·D = 12`. `CascadeShape::ALIGNED = [G3D4, G6D2]` are the byte-aligned **defaults** (`group_of` is a pure SHIFT — `shift()` returns `Some(2)`/`Some(1)` — the canon's "tier-of-level is a shift, never a branch"). `CascadeShape::ROTATIONS = [G3D4, G4D3, G6D2]` is the full **rotation set** a ClassView may rotate through: a ClassView can **always rotate** (read the same bytes under a different carving) per class — the relief valve for **classid-stacking entropy** (rare, e.g. some Odoo classes — rotate instead of minting another classid). + - **`G4D3` is the WORST CASE, not a co-equal carving** (operator: "4 group_of is a very bad and unwanted example"): it straddles tier boundaries, `shift()` is `None`, `group_of` must DIVIDE; excluded from `ALIGNED`, kept in `ROTATIONS` only as the deliberate rare rotation. `is_byte_aligned()` is the guard (`false` for G4D3) — reject the straddle on the common path. + - **`ClassArm::{View, Functions}`** — the classid is an **additional switch** (operator: "functions should be encoded using the Classid as an additional switch (functions, view)"). Functions are NOT a facet carving — they are the DO arm (`ActionDef`/`KausalSpec` on the resolved node), reached by switching the classid to `Functions`; carvings address the THINK/`View` arm only. (OGAR THINK/DO split, `OGAR-AST-CONTRACT.md`.) `ClassArm::BOTH`, `is_functions()`. + - `FacetCascade::tier_bytes()` = the 12 cascade bytes as one coarse→fine ladder (`hi` then `lo` per tier); `cascade_byte(shape,g,l)`; `cascade_group_shared(other,shape,g)` = per-group coarse→fine LCP redout. + This generalises the OGAR GUID `3×4`-vs-`4×3` debate from nibble-units to byte/field-units and lands on the canon's verdict (aligned 3×4 default; straddling 4×3 worst-case). **The shared substrate the three language SDKs (§1.6) all read.** +4 facet tests (`cascade_rotations_are_total_but_only_aligned_are_defaults`, `classid_switch_separates_view_from_functions`, `tier_bytes_ladder_and_per_carving_grouping`, `cascade_group_shared_is_per_group_lcp`) + 3 compile-time `G·D == CASCADE_UNITS` asserts. Lib facet tests 8/8 green; clippy `-D warnings` + rustfmt clean (probe-workspace verified offline — the workspace ndarray git dep is 403 offline). diff --git a/crates/lance-graph-contract/src/facet.rs b/crates/lance-graph-contract/src/facet.rs index 67c7915b7..e12cab434 100644 --- a/crates/lance-graph-contract/src/facet.rs +++ b/crates/lance-graph-contract/src/facet.rs @@ -302,8 +302,8 @@ impl FacetCascade { pub const fn tier_bytes(self) -> [u8; CASCADE_UNITS] { let t = &self.tiers; [ - t[0].hi, t[0].lo, t[1].hi, t[1].lo, t[2].hi, t[2].lo, t[3].hi, t[3].lo, t[4].hi, t[4].lo, - t[5].hi, t[5].lo, + t[0].hi, t[0].lo, t[1].hi, t[1].lo, t[2].hi, t[2].lo, t[3].hi, t[3].lo, t[4].hi, + t[4].lo, t[5].hi, t[5].lo, ] } @@ -341,41 +341,74 @@ impl FacetCascade { /// so the same `G·D = CASCADE_UNITS` invariant binds bytes and fields alike. pub const CASCADE_UNITS: usize = 12; -/// **One cascade algebra, three carvings.** The 12 cascade units (the facet's -/// [`tier_bytes`](FacetCascade::tier_bytes), or a 12-field class's fields) are -/// read as `G groups × D levels` with `G·D = CASCADE_UNITS` *always*. The units -/// never move — the ClassView picks the carving and the same index math -/// (`i = g·D + l`) addresses any of them. +/// **One cascade algebra; carvings are VIEW rotations, not function layouts.** +/// The 12 cascade units (the facet's [`tier_bytes`](FacetCascade::tier_bytes), +/// or a 12-field class's fields) are read as `G groups × D levels` with +/// `G·D = CASCADE_UNITS` *always*. The units never move — a `ClassView` picks +/// the carving (its **rotation**) and the same index math (`i = g·D + l`) +/// addresses any of them. A ClassView can **always rotate** — read the SAME +/// bytes under a different carving — per class. /// -/// This is the OGAR GUID's `3×4`-vs-`4×3` debate **generalized**: there the -/// unit is a nibble and the ladder is 12 levels; here the unit is a byte (or a -/// class field) and the ladder is the same 12. The carvings inherit the canon's -/// known trade-offs exactly: +/// **This addresses the VIEW only.** Functions (behaviour) are NOT a facet +/// carving — they are reached by switching the classid to the +/// [`ClassArm::Functions`] arm (the OGAR THINK/DO split). Never slice the +/// tier-bytes to reach a function. /// -/// | shape | groups × levels | notation | alignment | -/// |---|---|---|---| -/// | [`G6D2`](Self::G6D2) | 6 × 2 | `6×(1:2)` | native per-tier `(hi:lo)`; `group_of` is `i >> 1` | -/// | [`G4D3`](Self::G4D3) | 4 × 3 | `4×(1:2:3)` | **straddles** tier boundaries (the canon's "1.5-byte" cost); `group_of` needs a real divide | -/// | [`G3D4`](Self::G3D4) | 3 × 4 | `3×(1:2:3:4)` | tier-pair super-groups, byte-aligned; `group_of` is `i >> 2` (the canon's "tier-of-level is a shift, never a branch") | +/// Two carvings are byte-**aligned** ([`ALIGNED`](Self::ALIGNED) — `group_of` +/// is a shift); the third, [`G4D3`](Self::G4D3), **straddles** tier boundaries +/// (`group_of` must DIVIDE) and is the **worst case** — *prevented as a +/// default*, kept only as the **rare rotation / escape hatch** (some Odoo +/// classes may need it to relieve **classid-stacking entropy** — rotate the +/// reading instead of minting another classid): /// -/// All three are legal so the algebra is total; the ClassView chooses by -/// efficiency. With 12 fields it is often cheaper to map a sub-range as a -/// hierarchy (a deep group) and stack **nested** ClassViews into constructors -/// before materializing the `32×GUID` SoA — see -/// `docs/OGAR-TRANSPILE-SUBSTRATE.md` §1.5. +/// | shape | G×D | notation | role | `group_of` | +/// |---|---|---|---|---| +/// | [`G6D2`](Self::G6D2) | 6 × 2 | `6×(1:2)` | aligned default (native `hi:lo`) | `i >> 1` (shift) | +/// | [`G3D4`](Self::G3D4) | 3 × 4 | `3×(1:2:3:4)` | aligned default (tier-pair super-groups) | `i >> 2` (shift) | +/// | [`G4D3`](Self::G4D3) | 4 × 3 | `4×(1:2:3)` | **worst case** — straddle; rare rotation only | `i / 3` (**divide**) | +/// +/// This is the OGAR GUID's `3×4`-vs-`4×3` debate generalized from nibble-units +/// to byte/field-units — and it lands on the canon's verdict: the aligned (3×4) +/// carving is the default; the straddling (4×3) is the worst case, kept legal +/// only so a rotation can reach it deliberately and a guard can reject it +/// elsewhere. With 12 fields it is often cheaper to map a sub-range as a +/// hierarchy and stack **nested** ClassViews into constructors before +/// materializing the `32×GUID` SoA — see `docs/OGAR-TRANSPILE-SUBSTRATE.md` §1.5. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum CascadeShape { - /// 6 groups × 2 levels — native `(hi:lo)` per tier (`6×(1:2)`). + /// 6 groups × 2 levels — native `(hi:lo)` per tier (`6×(1:2)`). Aligned + /// default (`group_of` = `i >> 1`). G6D2, - /// 4 groups × 3 levels (`4×(1:2:3)`) — straddles tier boundaries. + /// 4 groups × 3 levels (`4×(1:2:3)`) — the **straddle**: `group_of` must + /// divide (`i / 3`), crossing tier boundaries. The **worst case**, NOT a + /// default — kept only as the rare rotation / escape hatch (e.g. an Odoo + /// class avoiding classid-stacking entropy). Use [`is_byte_aligned`](Self::is_byte_aligned) + /// to reject it on the common path. G4D3, - /// 3 groups × 4 levels — tier-pair super-groups (`3×(1:2:3:4)`). + /// 3 groups × 4 levels — tier-pair super-groups (`3×(1:2:3:4)`). Aligned + /// default (`group_of` = `i >> 2`). G3D4, } impl CascadeShape { - /// All three carvings, group-count ascending (coarsest split first). - pub const ALL: [CascadeShape; 3] = [CascadeShape::G3D4, CascadeShape::G4D3, CascadeShape::G6D2]; + /// The full **rotation set** — every carving a `ClassView` may rotate the + /// 12-unit ladder through (`G·D = 12` each), group-count ascending. A + /// ClassView can always rotate (read the same facet bytes under a different + /// grouping) per class; this is the relief valve for **classid-stacking + /// entropy** (rare, e.g. some Odoo classes — rotate the reading instead of + /// minting another classid). Includes the straddle [`G4D3`](Self::G4D3) so a + /// rotation can reach it deliberately; prefer [`ALIGNED`](Self::ALIGNED) on + /// the common path. + pub const ROTATIONS: [CascadeShape; 3] = + [CascadeShape::G3D4, CascadeShape::G4D3, CascadeShape::G6D2]; + + /// The byte-**ALIGNED** carvings — the default rotations. `group_of` is a + /// pure shift for both ([`shift`](Self::shift) is `Some`) — the canon's + /// "tier-of-level is a shift, never a branch". The straddle [`G4D3`](Self::G4D3) + /// is deliberately EXCLUDED — it is the worst case + /// ([`is_byte_aligned`](Self::is_byte_aligned) is `false`), available only as + /// the rare rotation, never a default. + pub const ALIGNED: [CascadeShape; 2] = [CascadeShape::G3D4, CascadeShape::G6D2]; /// `G` — number of groups (axes of meaning). `groups() · levels() == CASCADE_UNITS`. #[inline] @@ -410,10 +443,11 @@ impl CascadeShape { } /// Inverse of [`index`](Self::index): which group linear unit `i` belongs to - /// (`i / D`). For [`G3D4`](Self::G3D4)/[`G6D2`](Self::G6D2) (`D` a power of - /// two) this is a shift (`i >> 2` / `i >> 1`) — the canon's "tier-of-level is - /// a shift, never a branch"; [`G4D3`](Self::G4D3) needs a real divide (its - /// straddle cost made explicit). + /// (`i / D`). For the [`ALIGNED`](Self::ALIGNED) carvings this is a pure + /// shift (see [`shift`](Self::shift)); for the straddle [`G4D3`](Self::G4D3) + /// it is a real divide — the worst case. Prefer dispatching on + /// [`shift`](Self::shift) so the divide path is opt-in (the rare rotation), + /// never silently on the hot path. #[inline] #[must_use] pub const fn group_of(self, unit: usize) -> u8 { @@ -427,6 +461,74 @@ impl CascadeShape { pub const fn level_of(self, unit: usize) -> u8 { (unit % self.levels() as usize) as u8 } + + /// The bit-shift that implements [`group_of`](Self::group_of) for a + /// byte-aligned carving — `Some(1)` for [`G6D2`](Self::G6D2) (`i >> 1`), + /// `Some(2)` for [`G3D4`](Self::G3D4) (`i >> 2`) — or `None` for the straddle + /// [`G4D3`](Self::G4D3), which has NO shift (`group_of` must divide). `None` + /// IS the "this rotation is the rare escape hatch, not a default" signal: the + /// canon's "tier-of-level is a shift, never a branch" holds exactly for the + /// `Some` carvings. + #[inline] + #[must_use] + pub const fn shift(self) -> Option { + match self { + CascadeShape::G6D2 => Some(1), + CascadeShape::G3D4 => Some(2), + CascadeShape::G4D3 => None, + } + } + + /// A carving is byte-aligned iff its group boundaries fall on tier + /// boundaries — [`group_of`](Self::group_of) is a shift, not a divide. True + /// for the [`ALIGNED`](Self::ALIGNED) defaults; **false** for the straddle + /// [`G4D3`](Self::G4D3). A default layout MUST assert this; the straddle is a + /// worst case to PREVENT on the common path, reached only as a deliberate + /// rare rotation (classid-stacking-entropy relief). Functions are never + /// reached by any carving — see [`ClassArm`]. + #[inline] + #[must_use] + pub const fn is_byte_aligned(self) -> bool { + self.shift().is_some() + } +} + +/// The classid is an **additional switch**, not only a data address: resolving a +/// classid selects one of two ARMS — the same THINK/DO split the OGAR AST draws +/// (`docs/OGAR-AST-CONTRACT.md`). +/// +/// - [`View`](Self::View) — the THINK arm: the class's **data layout**, read by +/// a `ClassView` over the facet bytes carved/rotated per [`CascadeShape`] +/// (byte-aligned on the common path). +/// - [`Functions`](Self::Functions) — the DO arm: the class's **behaviour** +/// (`ActionDef` / `KausalSpec`) on the Core node the classid resolves to. +/// +/// **Functions are NOT a facet carving.** Behaviour is reached by switching the +/// classid to the `Functions` arm — never by slicing the facet's tier-bytes (a +/// straddling carve like the worst-case [`CascadeShape::G4D3`] is exactly what +/// that mistake looks like). The carving addresses the VIEW; this switch reaches +/// the functions. (Canon: neither u16 half of the classid carries behaviour — +/// behaviour is a property of the resolved node, *selected* by this arm; see +/// `OGAR/docs/OGAR-CONSUMER-BEST-PRACTICES.md`.) +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ClassArm { + /// THINK arm — data layout (a `ClassView` over the carved/rotated facet bytes). + View, + /// DO arm — behaviour (`ActionDef` / `KausalSpec` on the resolved Core node). + Functions, +} + +impl ClassArm { + /// The two arms the classid switch selects, `View` first (the + /// prerender-from-key default; canon "THE GUID IS THE KEY OF KEY-VALUE"). + pub const BOTH: [ClassArm; 2] = [ClassArm::View, ClassArm::Functions]; + + /// Whether this arm reaches behaviour (the DO arm). `false` for `View`. + #[inline] + #[must_use] + pub const fn is_functions(self) -> bool { + matches!(self, ClassArm::Functions) + } } const _: () = assert!( @@ -521,19 +623,16 @@ mod tests { } #[test] - fn cascade_shape_is_one_algebra_three_carvings() { - // G·D == 12 for every carving, and ALL is exhaustive. - for s in CascadeShape::ALL { + fn cascade_rotations_are_total_but_only_aligned_are_defaults() { + // The rotation set is exhaustive; every rotation covers all 12 units. + assert_eq!(CascadeShape::ROTATIONS.len(), 3); + for s in CascadeShape::ROTATIONS { assert_eq!( s.groups() as usize * s.levels() as usize, CASCADE_UNITS, - "every carving covers all 12 units exactly" + "every rotation covers all 12 units exactly" ); - } - assert_eq!(CascadeShape::ALL.len(), 3); - - // index / group_of / level_of are mutual inverses over the whole ladder. - for s in CascadeShape::ALL { + // index / group_of / level_of are mutual inverses over the whole ladder. for unit in 0..CASCADE_UNITS { let (g, l) = (s.group_of(unit), s.level_of(unit)); assert!(g < s.groups() && l < s.levels()); @@ -541,13 +640,48 @@ mod tests { } } - // Power-of-two carvings: group_of IS the canon's shift (no branch); - // 4×3 is the straddle that needs a real divide. - for unit in 0..CASCADE_UNITS { - assert_eq!(CascadeShape::G6D2.group_of(unit) as usize, unit >> 1); - assert_eq!(CascadeShape::G3D4.group_of(unit) as usize, unit >> 2); - assert_eq!(CascadeShape::G4D3.group_of(unit) as usize, unit / 3); + // ALIGNED is the default subset: group_of IS a shift (canon "shift, not + // a branch"); shift() is Some and matches. + assert_eq!( + CascadeShape::ALIGNED, + [CascadeShape::G3D4, CascadeShape::G6D2] + ); + for s in CascadeShape::ALIGNED { + assert!(s.is_byte_aligned(), "{s:?} is an aligned default"); + let sh = s.shift().expect("aligned carving has a shift"); + for unit in 0..CASCADE_UNITS { + assert_eq!( + s.group_of(unit) as usize, + unit >> sh, + "{s:?} group_of is a shift" + ); + } } + assert_eq!(CascadeShape::G6D2.shift(), Some(1)); + assert_eq!(CascadeShape::G3D4.shift(), Some(2)); + + // G4D3 is the WORST CASE, not a default: it straddles, has NO shift + // (group_of DIVIDES), is excluded from ALIGNED, but stays in ROTATIONS + // as the rare escape hatch (classid-stacking-entropy relief, msg 2). + assert!(!CascadeShape::G4D3.is_byte_aligned()); + assert_eq!(CascadeShape::G4D3.shift(), None); + assert!(!CascadeShape::ALIGNED.contains(&CascadeShape::G4D3)); + assert!(CascadeShape::ROTATIONS.contains(&CascadeShape::G4D3)); + assert_eq!(CascadeShape::G4D3.group_of(2) as usize, 2 / 3); // a divide — the bad example + } + + #[test] + fn classid_switch_separates_view_from_functions() { + // The classid is an additional (functions, view) switch; functions are + // NOT a facet carving — they are the DO arm, reached by this switch. + assert_eq!(CascadeShape::ROTATIONS.len(), 3); // carvings address the VIEW only + assert_eq!(ClassArm::BOTH, [ClassArm::View, ClassArm::Functions]); + assert_ne!(ClassArm::View, ClassArm::Functions); + assert!(!ClassArm::View.is_functions(), "View is the THINK/data arm"); + assert!( + ClassArm::Functions.is_functions(), + "Functions is the DO/behaviour arm" + ); } #[test] @@ -563,8 +697,14 @@ mod tests { // 6×2: group g == tier g's (hi, lo) — the native pairing. for g in 0..6u8 { - assert_eq!(f.cascade_byte(CascadeShape::G6D2, g, 0), f.tiers[g as usize].hi); - assert_eq!(f.cascade_byte(CascadeShape::G6D2, g, 1), f.tiers[g as usize].lo); + assert_eq!( + f.cascade_byte(CascadeShape::G6D2, g, 0), + f.tiers[g as usize].hi + ); + assert_eq!( + f.cascade_byte(CascadeShape::G6D2, g, 1), + f.tiers[g as usize].lo + ); } // 3×4: group 0 spans tiers 0–1 (4 bytes), byte-aligned super-group. assert_eq!( @@ -576,7 +716,10 @@ mod tests { ], [0xAB, 0x01, 0xCD, 0x02] ); - // 4×3: group 0 straddles tier 0 fully + tier 1's hi (the 1.5-tier cost). + // 4×3 (the worst-case rare rotation): group 0 straddles tier 0 fully + + // tier 1's hi (the 1.5-tier cost) — shown only to demonstrate why it is + // NOT a default, not to endorse it. + assert!(!CascadeShape::G4D3.is_byte_aligned()); assert_eq!( [ f.cascade_byte(CascadeShape::G4D3, 0, 0), @@ -591,8 +734,8 @@ mod tests { fn cascade_group_shared_is_per_group_lcp() { let f = FacetCascade::from_bytes(&sample()); - // identical ⇒ every group's whole ladder agrees (== D). - for s in CascadeShape::ALL { + // identical ⇒ every group's whole ladder agrees (== D), for every rotation. + for s in CascadeShape::ROTATIONS { for g in 0..s.groups() { assert_eq!(f.cascade_group_shared(f, s, g), s.levels()); } @@ -603,8 +746,16 @@ mod tests { let mut b = sample(); b[7] = 0x99; // tier1.hi let g = FacetCascade::from_bytes(&b); - assert_eq!(f.cascade_group_shared(g, CascadeShape::G6D2, 0), 2, "tier0 intact"); - assert_eq!(f.cascade_group_shared(g, CascadeShape::G6D2, 1), 0, "tier1.hi differs first"); + assert_eq!( + f.cascade_group_shared(g, CascadeShape::G6D2, 0), + 2, + "tier0 intact" + ); + assert_eq!( + f.cascade_group_shared(g, CascadeShape::G6D2, 1), + 0, + "tier1.hi differs first" + ); // Under 3×4 unit 2 is group 0, level 2 ⇒ group 0 shares its first 2 // levels then breaks; group 1 (tiers 2–3) fully intact. From 0b6004d8ffcb1a07ccb1b620467f5ec46b25a7e2 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 29 Jun 2026 20:37:18 +0000 Subject: [PATCH 4/8] feat(contract): GUIDS_PER_NODE = 32 + clean/SoC-over-packed doctrine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Operator principle (2026-06-29): the 4096-bit / 512-byte SoA node is 32 × 16-byte GUID-sized slots, so in the worst case you Tetris what you need across slots — preference clean / SoC over packed. - canonical_node::GUIDS_PER_NODE = NODE_ROW_STRIDE / 16 = 32 (compile-time asserted == 32 && ·size_of::() == NODE_ROW_STRIDE). key + edges occupy 2 slots; the 480-byte value slab is the remaining 30 to lay a class's concerns into. Doc states the doctrine: give each concern its own 16-byte slot rather than bit-pack two into one; the 32-slot capacity is why the CascadeShape::G4D3 straddle (packing) is almost never needed, and the headroom behind 'rotate / spread to a fresh slot instead of minting another classid'. - facet::CascadeShape G4D3 doc cross-refs GUIDS_PER_NODE (SoC over packed). - test guids_per_node_is_32_slots_clean_soc_over_packed (asserts the 2+30 split). Lib facet 8/8 + canonical_node 43/43 green; clippy -D warnings + rustfmt clean (probe-verified offline). Board: LATEST_STATE.md entry updated same commit. Co-Authored-By: Claude --- .claude/board/LATEST_STATE.md | 3 +- .../src/canonical_node.rs | 43 +++++++++++++++++++ crates/lance-graph-contract/src/facet.rs | 7 +++ 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/.claude/board/LATEST_STATE.md b/.claude/board/LATEST_STATE.md index 18a025072..64a34a46b 100644 --- a/.claude/board/LATEST_STATE.md +++ b/.claude/board/LATEST_STATE.md @@ -669,4 +669,5 @@ PR sequence: #360 → #361 → post-#360 substrate-sweep (this PR). - **`G4D3` is the WORST CASE, not a co-equal carving** (operator: "4 group_of is a very bad and unwanted example"): it straddles tier boundaries, `shift()` is `None`, `group_of` must DIVIDE; excluded from `ALIGNED`, kept in `ROTATIONS` only as the deliberate rare rotation. `is_byte_aligned()` is the guard (`false` for G4D3) — reject the straddle on the common path. - **`ClassArm::{View, Functions}`** — the classid is an **additional switch** (operator: "functions should be encoded using the Classid as an additional switch (functions, view)"). Functions are NOT a facet carving — they are the DO arm (`ActionDef`/`KausalSpec` on the resolved node), reached by switching the classid to `Functions`; carvings address the THINK/`View` arm only. (OGAR THINK/DO split, `OGAR-AST-CONTRACT.md`.) `ClassArm::BOTH`, `is_functions()`. - `FacetCascade::tier_bytes()` = the 12 cascade bytes as one coarse→fine ladder (`hi` then `lo` per tier); `cascade_byte(shape,g,l)`; `cascade_group_shared(other,shape,g)` = per-group coarse→fine LCP redout. - This generalises the OGAR GUID `3×4`-vs-`4×3` debate from nibble-units to byte/field-units and lands on the canon's verdict (aligned 3×4 default; straddling 4×3 worst-case). **The shared substrate the three language SDKs (§1.6) all read.** +4 facet tests (`cascade_rotations_are_total_but_only_aligned_are_defaults`, `classid_switch_separates_view_from_functions`, `tier_bytes_ladder_and_per_carving_grouping`, `cascade_group_shared_is_per_group_lcp`) + 3 compile-time `G·D == CASCADE_UNITS` asserts. Lib facet tests 8/8 green; clippy `-D warnings` + rustfmt clean (probe-workspace verified offline — the workspace ndarray git dep is 403 offline). + - **`canonical_node::GUIDS_PER_NODE = 32` + the clean/SoC-over-packed doctrine** (operator 2026-06-29): the 512-byte node = `NODE_ROW_STRIDE / 16` = **32 × 16-byte GUID slots** (`key` + `edges` = 2; value slab = 30). Doctrine: when a class needs more than fits cleanly in one slot, **Tetris each concern into its own slot (SoC)** rather than bit-pack — the 32-slot capacity is *why* the `G4D3` straddle / packing is almost never needed (it's the headroom that also lets a ClassView rotate and lets the rare classid-stacking-entropy case spread to a fresh slot instead of minting another classid). Compile-time assert (`GUIDS_PER_NODE == 32 && ·16 == NODE_ROW_STRIDE`) + test `guids_per_node_is_32_slots_clean_soc_over_packed` (asserts the 2+30 split). `facet::CascadeShape` cross-refs it from the `G4D3` worst-case doc. + This generalises the OGAR GUID `3×4`-vs-`4×3` debate from nibble-units to byte/field-units and lands on the canon's verdict (aligned 3×4 default; straddling 4×3 worst-case). **The shared substrate the three language SDKs (§1.6) all read.** +4 facet tests (`cascade_rotations_are_total_but_only_aligned_are_defaults`, `classid_switch_separates_view_from_functions`, `tier_bytes_ladder_and_per_carving_grouping`, `cascade_group_shared_is_per_group_lcp`) + canonical_node `guids_per_node_*` + 4 compile-time asserts. Lib facet 8/8 + canonical_node 43/43 green; clippy `-D warnings` + rustfmt clean (probe-workspace verified offline — the workspace ndarray git dep is 403 offline). diff --git a/crates/lance-graph-contract/src/canonical_node.rs b/crates/lance-graph-contract/src/canonical_node.rs index 4fb4a9440..3b9abb252 100644 --- a/crates/lance-graph-contract/src/canonical_node.rs +++ b/crates/lance-graph-contract/src/canonical_node.rs @@ -643,6 +643,29 @@ pub const NODE_ROW_COLUMNS: &[ColumnDescriptor] = &[ /// Row stride for [`NodeRow`] in bytes — equal to `size_of::()`. pub const NODE_ROW_STRIDE: usize = 512; +/// The node viewed as fixed-size **GUID slots**: `NODE_ROW_STRIDE / +/// size_of::()` = `512 / 16` = **32**. A 512-byte SoA row can carry up +/// to 32 GUID-sized (16-byte) entries; `key` + `edges` occupy 2, leaving the +/// 480-byte value slab = 30 slots for the class-resolved layout. +/// +/// **Doctrine — clean / SoC over packed (operator, 2026-06-29).** When a class +/// needs more structure than fits cleanly in one slot, *Tetris it across the +/// slots* — give each concern its own 16-byte slot — rather than bit-pack two +/// concerns into one. Packing into a single facet via a straddling carve is the +/// worst case [`crate::facet::CascadeShape::G4D3`] / +/// [`is_byte_aligned`](crate::facet::CascadeShape::is_byte_aligned) flags; the +/// 32-slot capacity is *why* that straddle is almost never needed — +/// separation-of-concerns layout is the default, packing the rare last resort. +/// (This is also the headroom that lets a ClassView *rotate* and lets the rare +/// classid-stacking-entropy case spread to a fresh slot instead of minting +/// another classid.) +pub const GUIDS_PER_NODE: usize = NODE_ROW_STRIDE / 16; + +const _: () = assert!( + GUIDS_PER_NODE == 32 && GUIDS_PER_NODE * core::mem::size_of::() == NODE_ROW_STRIDE, + "512-byte node = 32 × 16-byte GUID slots" +); + // ── Value-slab schema presets: which tenants a class materialises ───────────── /// Full-row byte offset of the value slab (key 16 + edges 16). @@ -1596,6 +1619,26 @@ mod tests { assert_eq!(NODE_ROW_STRIDE, core::mem::size_of::()); } + #[test] + fn guids_per_node_is_32_slots_clean_soc_over_packed() { + // The 512-byte node is 32 × 16-byte GUID-sized slots. + assert_eq!(GUIDS_PER_NODE, 32); + assert_eq!( + GUIDS_PER_NODE * core::mem::size_of::(), + NODE_ROW_STRIDE + ); + // key + edges occupy 2 slots; the value slab is the remaining 30 to + // Tetris a class's concerns into (SoC over packed — no straddle needed). + let key_edges_slots = + (core::mem::size_of::() + core::mem::size_of::()) / 16; + assert_eq!(key_edges_slots, 2); + assert_eq!( + GUIDS_PER_NODE - key_edges_slots, + 30, + "value slab = 30 slots" + ); + } + #[test] fn uniqueness_guard_is_noop_outside_bootstrap() { // family != 0 ⇒ no longer the bootstrap address: the guard is a no-op diff --git a/crates/lance-graph-contract/src/facet.rs b/crates/lance-graph-contract/src/facet.rs index e12cab434..5d1a33a61 100644 --- a/crates/lance-graph-contract/src/facet.rs +++ b/crates/lance-graph-contract/src/facet.rs @@ -374,6 +374,13 @@ pub const CASCADE_UNITS: usize = 12; /// elsewhere. With 12 fields it is often cheaper to map a sub-range as a /// hierarchy and stack **nested** ClassViews into constructors before /// materializing the `32×GUID` SoA — see `docs/OGAR-TRANSPILE-SUBSTRATE.md` §1.5. +/// +/// **Clean / SoC over packed.** Packing two concerns into one facet via a +/// straddle (`G4D3`) is rarely necessary: a node has +/// [`GUIDS_PER_NODE`](crate::canonical_node::GUIDS_PER_NODE) = 32 sixteen-byte +/// slots, so the cheap move is to *Tetris* each concern into its own slot +/// (separation-of-concerns) rather than bit-pack — capacity is the reason the +/// straddle stays a last resort. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum CascadeShape { /// 6 groups × 2 levels — native `(hi:lo)` per tier (`6×(1:2)`). Aligned From 3c7cd3e8fe9d7644d653d47278ab765a1062466f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 29 Jun 2026 21:13:32 +0000 Subject: [PATCH 5/8] refactor(contract): CascadeShape class-conditioned (operator veto) + from_levels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Operator small veto (2026-06-29): the shape is mapped from the class's INHERITED format and selected by classid — it shouldn't be restated or LOCKED. 'Quadruplet' was a misread; the shapes are the per-class CascadeShape: Rails 6×2, other frameworks 4×3, the GUID 3×4 (all G·D=12, 8-bit tiers). So 4×3 is legitimate per-class, NOT a 'worst case to prevent'. - Recast all CascadeShape docs from 'G4D3 worst case / prevent / rare escape hatch' → class-conditioned: Rails→G6D2, other frameworks→G4D3, GUID→G3D4. The divide group_of of G4D3 is a per-class shape COST a class opts into, not a prohibition; is_byte_aligned()/shift()/ALIGNED now read as 'shift fast-path vs divide shape', not a reject gate. - NEW CascadeShape::from_levels(d) — the class-conditioned D∈{2,3,4} selector (2→G6D2, 3→G4D3, 4→G3D4), inverse of levels(); the classid resolves D, never a global lock. Handles 'if one of the 2-4 becomes a constant dependent on class condition'. - canonical_node GUIDS_PER_NODE doctrine reworded: 'don't mix distinct concerns in one facet' (independent of shape); the per-class 6×2/4×3/3×4 shape is a separate, class-conditioned choice (a 4×3 class is clean). - Test renamed cascade_shapes_are_total_and_class_conditioned (+ from_levels round-trip). Facet 7/7 + canonical_node 43/43 green, clippy + rustfmt clean. Board: LATEST_STATE.md correction line appended (append-only). Co-Authored-By: Claude --- .claude/board/LATEST_STATE.md | 1 + .../src/canonical_node.rs | 18 +- crates/lance-graph-contract/src/facet.rs | 177 ++++++++++-------- 3 files changed, 108 insertions(+), 88 deletions(-) diff --git a/.claude/board/LATEST_STATE.md b/.claude/board/LATEST_STATE.md index 64a34a46b..03585eaf4 100644 --- a/.claude/board/LATEST_STATE.md +++ b/.claude/board/LATEST_STATE.md @@ -671,3 +671,4 @@ PR sequence: #360 → #361 → post-#360 substrate-sweep (this PR). - `FacetCascade::tier_bytes()` = the 12 cascade bytes as one coarse→fine ladder (`hi` then `lo` per tier); `cascade_byte(shape,g,l)`; `cascade_group_shared(other,shape,g)` = per-group coarse→fine LCP redout. - **`canonical_node::GUIDS_PER_NODE = 32` + the clean/SoC-over-packed doctrine** (operator 2026-06-29): the 512-byte node = `NODE_ROW_STRIDE / 16` = **32 × 16-byte GUID slots** (`key` + `edges` = 2; value slab = 30). Doctrine: when a class needs more than fits cleanly in one slot, **Tetris each concern into its own slot (SoC)** rather than bit-pack — the 32-slot capacity is *why* the `G4D3` straddle / packing is almost never needed (it's the headroom that also lets a ClassView rotate and lets the rare classid-stacking-entropy case spread to a fresh slot instead of minting another classid). Compile-time assert (`GUIDS_PER_NODE == 32 && ·16 == NODE_ROW_STRIDE`) + test `guids_per_node_is_32_slots_clean_soc_over_packed` (asserts the 2+30 split). `facet::CascadeShape` cross-refs it from the `G4D3` worst-case doc. This generalises the OGAR GUID `3×4`-vs-`4×3` debate from nibble-units to byte/field-units and lands on the canon's verdict (aligned 3×4 default; straddling 4×3 worst-case). **The shared substrate the three language SDKs (§1.6) all read.** +4 facet tests (`cascade_rotations_are_total_but_only_aligned_are_defaults`, `classid_switch_separates_view_from_functions`, `tier_bytes_ladder_and_per_carving_grouping`, `cascade_group_shared_is_per_group_lcp`) + canonical_node `guids_per_node_*` + 4 compile-time asserts. Lib facet 8/8 + canonical_node 43/43 green; clippy `-D warnings` + rustfmt clean (probe-workspace verified offline — the workspace ndarray git dep is 403 offline). + - **2026-06-29 correction (operator veto):** the "G4D3 = worst case to prevent" framing above is SOFTENED — **the shape is class-conditioned, not locked**. A ClassView is mapped from the class's *inherited* format and selected by `classid` (the filter); the shape follows: **Rails → `6×2`, other frameworks → `4×3`, the GUID → `3×4`** (operator: "Rails might need 6x2x8bit, others 4x3x8bit"). So `4×3` (`G4D3`) is **legitimate per-class**, not a thing to "reject" — its `group_of` divides (a per-class *cost* a class opts into), and `is_byte_aligned()`/`shift()`/`ALIGNED` now read as "distinguishes the shift fast-path from the divide shape," not "prevent." NEW `CascadeShape::from_levels(d)` — the class-conditioned `D ∈ {2,3,4}` selector (`2→G6D2`/`3→G4D3`/`4→G3D4`), inverse of `levels()`; the classid resolves `D`, never a global lock. Test renamed → `cascade_shapes_are_total_and_class_conditioned` (adds the `from_levels` round-trip). The earlier "quadruplet/4-bucket FieldMask" framing in ruff `soc` was likewise unlocked → byte-cardinality cap, class-conditioned shape (ruff #36). Facet 7/7 + canonical_node 43/43 green post-correction. diff --git a/crates/lance-graph-contract/src/canonical_node.rs b/crates/lance-graph-contract/src/canonical_node.rs index 3b9abb252..b92f9a532 100644 --- a/crates/lance-graph-contract/src/canonical_node.rs +++ b/crates/lance-graph-contract/src/canonical_node.rs @@ -650,15 +650,15 @@ pub const NODE_ROW_STRIDE: usize = 512; /// /// **Doctrine — clean / SoC over packed (operator, 2026-06-29).** When a class /// needs more structure than fits cleanly in one slot, *Tetris it across the -/// slots* — give each concern its own 16-byte slot — rather than bit-pack two -/// concerns into one. Packing into a single facet via a straddling carve is the -/// worst case [`crate::facet::CascadeShape::G4D3`] / -/// [`is_byte_aligned`](crate::facet::CascadeShape::is_byte_aligned) flags; the -/// 32-slot capacity is *why* that straddle is almost never needed — -/// separation-of-concerns layout is the default, packing the rare last resort. -/// (This is also the headroom that lets a ClassView *rotate* and lets the rare -/// classid-stacking-entropy case spread to a fresh slot instead of minting -/// another classid.) +/// slots* — give each concern its own 16-byte slot — rather than cram two +/// *distinct concerns* into one. The 32-slot capacity is *why* that cramming is +/// almost never needed — separation-of-concerns layout is the default, packing +/// the rare last resort. (This is also the headroom that lets a ClassView +/// *rotate* and lets the rare classid-stacking-entropy case spread to a fresh +/// slot instead of minting another classid.) The per-class *shape* of one +/// facet — [`CascadeShape`](crate::facet::CascadeShape) `6×2`/`4×3`/`3×4`, +/// selected by `classid` — is a separate, class-conditioned choice (a `4×3` +/// class is clean); this doctrine is about not mixing concerns, not about shape. pub const GUIDS_PER_NODE: usize = NODE_ROW_STRIDE / 16; const _: () = assert!( diff --git a/crates/lance-graph-contract/src/facet.rs b/crates/lance-graph-contract/src/facet.rs index 5d1a33a61..3c206eb03 100644 --- a/crates/lance-graph-contract/src/facet.rs +++ b/crates/lance-graph-contract/src/facet.rs @@ -354,69 +354,87 @@ pub const CASCADE_UNITS: usize = 12; /// [`ClassArm::Functions`] arm (the OGAR THINK/DO split). Never slice the /// tier-bytes to reach a function. /// -/// Two carvings are byte-**aligned** ([`ALIGNED`](Self::ALIGNED) — `group_of` -/// is a shift); the third, [`G4D3`](Self::G4D3), **straddles** tier boundaries -/// (`group_of` must DIVIDE) and is the **worst case** — *prevented as a -/// default*, kept only as the **rare rotation / escape hatch** (some Odoo -/// classes may need it to relieve **classid-stacking entropy** — rotate the -/// reading instead of minting another classid): +/// **The shape is class-conditioned, not locked.** A `ClassView` is *mapped +/// from the class's inherited format* and selected by `classid` (the filter), so +/// a framework picks the carving its schema implies — **Rails → `6×2`, other +/// frameworks → `4×3`, the canonical GUID → `3×4`** (all `G·D = 12`, 8-bit +/// tiers; the per-group depth `D ∈ {2,3,4}` is the per-class knob, see +/// [`from_levels`](Self::from_levels)). Each is legitimate for the class that +/// needs it; none is restated or locked here. /// -/// | shape | G×D | notation | role | `group_of` | +/// | shape | G×D | notation | framework | `group_of` | /// |---|---|---|---|---| -/// | [`G6D2`](Self::G6D2) | 6 × 2 | `6×(1:2)` | aligned default (native `hi:lo`) | `i >> 1` (shift) | -/// | [`G3D4`](Self::G3D4) | 3 × 4 | `3×(1:2:3:4)` | aligned default (tier-pair super-groups) | `i >> 2` (shift) | -/// | [`G4D3`](Self::G4D3) | 4 × 3 | `4×(1:2:3)` | **worst case** — straddle; rare rotation only | `i / 3` (**divide**) | +/// | [`G6D2`](Self::G6D2) | 6 × 2 | `6×(1:2)` | Rails (native `hi:lo`) | `i >> 1` (shift) | +/// | [`G4D3`](Self::G4D3) | 4 × 3 | `4×(1:2:3)` | other frameworks | `i / 3` (divide) | +/// | [`G3D4`](Self::G3D4) | 3 × 4 | `3×(1:2:3:4)` | canonical GUID (tier-pair super-groups) | `i >> 2` (shift) | /// -/// This is the OGAR GUID's `3×4`-vs-`4×3` debate generalized from nibble-units -/// to byte/field-units — and it lands on the canon's verdict: the aligned (3×4) -/// carving is the default; the straddling (4×3) is the worst case, kept legal -/// only so a rotation can reach it deliberately and a guard can reject it -/// elsewhere. With 12 fields it is often cheaper to map a sub-range as a -/// hierarchy and stack **nested** ClassViews into constructors before -/// materializing the `32×GUID` SoA — see `docs/OGAR-TRANSPILE-SUBSTRATE.md` §1.5. +/// `G6D2`/`G3D4` carve on tier boundaries so `group_of` is a pure shift +/// ([`ALIGNED`](Self::ALIGNED) — the canon's "tier-of-level is a shift, never a +/// branch"); `G4D3` straddles, so its `group_of` divides — a **per-class cost a +/// class opts into when its schema needs `4×3`**, not a prohibition. This is the +/// OGAR GUID `3×4`-vs-`4×3` debate generalized from nibble-units to byte/field- +/// units: `3×4` is the GUID default, the others are class-conditioned. With 12 +/// fields a class may also map a sub-range as a hierarchy and stack **nested** +/// ClassViews into constructors before materializing the `32×GUID` SoA — see +/// `docs/OGAR-TRANSPILE-SUBSTRATE.md` §1.5. /// -/// **Clean / SoC over packed.** Packing two concerns into one facet via a -/// straddle (`G4D3`) is rarely necessary: a node has +/// **Clean / SoC over packed.** What stays the last resort is cramming two +/// *distinct concerns* into one facet (independent of shape): a node has /// [`GUIDS_PER_NODE`](crate::canonical_node::GUIDS_PER_NODE) = 32 sixteen-byte /// slots, so the cheap move is to *Tetris* each concern into its own slot -/// (separation-of-concerns) rather than bit-pack — capacity is the reason the -/// straddle stays a last resort. +/// (separation-of-concerns) rather than bit-pack. (`G4D3`'s divide is a per-class +/// *shape* cost, separate from this — a class whose schema needs `4×3` is clean.) #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum CascadeShape { - /// 6 groups × 2 levels — native `(hi:lo)` per tier (`6×(1:2)`). Aligned - /// default (`group_of` = `i >> 1`). + /// 6 groups × 2 levels — native `(hi:lo)` per tier (`6×(1:2)`). The Rails + /// shape; `group_of` is the shift `i >> 1`. G6D2, - /// 4 groups × 3 levels (`4×(1:2:3)`) — the **straddle**: `group_of` must - /// divide (`i / 3`), crossing tier boundaries. The **worst case**, NOT a - /// default — kept only as the rare rotation / escape hatch (e.g. an Odoo - /// class avoiding classid-stacking entropy). Use [`is_byte_aligned`](Self::is_byte_aligned) - /// to reject it on the common path. + /// 4 groups × 3 levels (`4×(1:2:3)`) — the **class-conditioned** shape for + /// frameworks whose inherited schema implies `4×3` ("other frameworks"; not + /// the GUID default `3×4`, not Rails' `6×2`). It straddles tier boundaries so + /// `group_of` divides (`i / 3`) — the per-class *cost* the class opts into, + /// not a prohibition. [`is_byte_aligned`](Self::is_byte_aligned) is `false` + /// (it distinguishes the divide shape from the shift shapes — not a "reject"). G4D3, - /// 3 groups × 4 levels — tier-pair super-groups (`3×(1:2:3:4)`). Aligned - /// default (`group_of` = `i >> 2`). + /// 3 groups × 4 levels — tier-pair super-groups (`3×(1:2:3:4)`). The + /// canonical GUID shape; `group_of` is the shift `i >> 2`. G3D4, } impl CascadeShape { - /// The full **rotation set** — every carving a `ClassView` may rotate the - /// 12-unit ladder through (`G·D = 12` each), group-count ascending. A - /// ClassView can always rotate (read the same facet bytes under a different - /// grouping) per class; this is the relief valve for **classid-stacking - /// entropy** (rare, e.g. some Odoo classes — rotate the reading instead of - /// minting another classid). Includes the straddle [`G4D3`](Self::G4D3) so a - /// rotation can reach it deliberately; prefer [`ALIGNED`](Self::ALIGNED) on - /// the common path. + /// Every shape, group-count ascending — the full set a class may be + /// carved/rotated through (`G·D = 12` each). Which one a class uses is + /// **class-conditioned**: `classid` selects it from the inherited schema + /// (Rails `6×2`, other frameworks `4×3`, the GUID `3×4`) — see + /// [`from_levels`](Self::from_levels). A ClassView can also always rotate + /// (read the same bytes under a different grouping) per class. pub const ROTATIONS: [CascadeShape; 3] = [CascadeShape::G3D4, CascadeShape::G4D3, CascadeShape::G6D2]; - /// The byte-**ALIGNED** carvings — the default rotations. `group_of` is a - /// pure shift for both ([`shift`](Self::shift) is `Some`) — the canon's - /// "tier-of-level is a shift, never a branch". The straddle [`G4D3`](Self::G4D3) - /// is deliberately EXCLUDED — it is the worst case - /// ([`is_byte_aligned`](Self::is_byte_aligned) is `false`), available only as - /// the rare rotation, never a default. + /// The byte-**aligned** shapes — `group_of` is a pure shift + /// ([`shift`](Self::shift) is `Some`), the canon's "tier-of-level is a shift, + /// never a branch". `G6D2` (Rails) and `G3D4` (GUID). [`G4D3`](Self::G4D3) + /// (other frameworks) is excluded because its `group_of` *divides*, not + /// because it is forbidden — it is a legitimate class-conditioned shape. pub const ALIGNED: [CascadeShape; 2] = [CascadeShape::G3D4, CascadeShape::G6D2]; + /// Select the shape by its per-group depth `D` — the **class-conditioned + /// knob** (operator 2026-06-29): a framework/class picks `D ∈ {2,3,4}` from + /// its inherited format and the shape follows — `2 → G6D2` (Rails), + /// `3 → G4D3` (other frameworks), `4 → G3D4` (the GUID default). `None` for + /// any other `D` (only 2/3/4 divide the 12-unit ladder). This is the + /// inverse of [`levels`](Self::levels); the classid resolves `D`, not a lock. + #[inline] + #[must_use] + pub const fn from_levels(d: u8) -> Option { + match d { + 2 => Some(CascadeShape::G6D2), + 3 => Some(CascadeShape::G4D3), + 4 => Some(CascadeShape::G3D4), + _ => None, + } + } + /// `G` — number of groups (axes of meaning). `groups() · levels() == CASCADE_UNITS`. #[inline] #[must_use] @@ -450,11 +468,10 @@ impl CascadeShape { } /// Inverse of [`index`](Self::index): which group linear unit `i` belongs to - /// (`i / D`). For the [`ALIGNED`](Self::ALIGNED) carvings this is a pure - /// shift (see [`shift`](Self::shift)); for the straddle [`G4D3`](Self::G4D3) - /// it is a real divide — the worst case. Prefer dispatching on - /// [`shift`](Self::shift) so the divide path is opt-in (the rare rotation), - /// never silently on the hot path. + /// (`i / D`). For the [`ALIGNED`](Self::ALIGNED) shapes this is a pure shift + /// (see [`shift`](Self::shift)); for [`G4D3`](Self::G4D3) it is a real divide + /// — the per-class cost of the `4×3` shape. Dispatch on [`shift`](Self::shift) + /// when you want the shift fast-path for the aligned shapes. #[inline] #[must_use] pub const fn group_of(self, unit: usize) -> u8 { @@ -470,12 +487,11 @@ impl CascadeShape { } /// The bit-shift that implements [`group_of`](Self::group_of) for a - /// byte-aligned carving — `Some(1)` for [`G6D2`](Self::G6D2) (`i >> 1`), - /// `Some(2)` for [`G3D4`](Self::G3D4) (`i >> 2`) — or `None` for the straddle - /// [`G4D3`](Self::G4D3), which has NO shift (`group_of` must divide). `None` - /// IS the "this rotation is the rare escape hatch, not a default" signal: the - /// canon's "tier-of-level is a shift, never a branch" holds exactly for the - /// `Some` carvings. + /// byte-aligned shape — `Some(1)` for [`G6D2`](Self::G6D2) (`i >> 1`), + /// `Some(2)` for [`G3D4`](Self::G3D4) (`i >> 2`) — or `None` for + /// [`G4D3`](Self::G4D3), whose `group_of` divides. `Some` carvings satisfy the + /// canon's "tier-of-level is a shift, never a branch"; `None` marks the + /// `4×3` shape's per-class divide cost (a property, not a verdict). #[inline] #[must_use] pub const fn shift(self) -> Option { @@ -486,13 +502,13 @@ impl CascadeShape { } } - /// A carving is byte-aligned iff its group boundaries fall on tier - /// boundaries — [`group_of`](Self::group_of) is a shift, not a divide. True - /// for the [`ALIGNED`](Self::ALIGNED) defaults; **false** for the straddle - /// [`G4D3`](Self::G4D3). A default layout MUST assert this; the straddle is a - /// worst case to PREVENT on the common path, reached only as a deliberate - /// rare rotation (classid-stacking-entropy relief). Functions are never - /// reached by any carving — see [`ClassArm`]. + /// A shape is byte-aligned iff its group boundaries fall on tier boundaries — + /// [`group_of`](Self::group_of) is a shift, not a divide. True for the + /// [`ALIGNED`](Self::ALIGNED) shapes (`6×2`/`3×4`); **false** for + /// [`G4D3`](Self::G4D3) (`4×3`), whose `group_of` divides. This distinguishes + /// the shift fast-path from the divide shape — it is not a "prevent" gate; + /// `4×3` is a legitimate class-conditioned shape. Functions are never reached + /// by any carving — see [`ClassArm`]. #[inline] #[must_use] pub const fn is_byte_aligned(self) -> bool { @@ -630,16 +646,11 @@ mod tests { } #[test] - fn cascade_rotations_are_total_but_only_aligned_are_defaults() { - // The rotation set is exhaustive; every rotation covers all 12 units. + fn cascade_shapes_are_total_and_class_conditioned() { + // Every shape covers all 12 units; index/group_of/level_of are inverses. assert_eq!(CascadeShape::ROTATIONS.len(), 3); for s in CascadeShape::ROTATIONS { - assert_eq!( - s.groups() as usize * s.levels() as usize, - CASCADE_UNITS, - "every rotation covers all 12 units exactly" - ); - // index / group_of / level_of are mutual inverses over the whole ladder. + assert_eq!(s.groups() as usize * s.levels() as usize, CASCADE_UNITS); for unit in 0..CASCADE_UNITS { let (g, l) = (s.group_of(unit), s.level_of(unit)); assert!(g < s.groups() && l < s.levels()); @@ -647,15 +658,26 @@ mod tests { } } - // ALIGNED is the default subset: group_of IS a shift (canon "shift, not - // a branch"); shift() is Some and matches. + // The shape is CLASS-CONDITIONED, selected by depth D (the classid knob): + // 2 → G6D2 (Rails), 3 → G4D3 (other frameworks), 4 → G3D4 (GUID). Round- + // trips with levels(); only 2/3/4 divide 12. + assert_eq!(CascadeShape::from_levels(2), Some(CascadeShape::G6D2)); + assert_eq!(CascadeShape::from_levels(3), Some(CascadeShape::G4D3)); + assert_eq!(CascadeShape::from_levels(4), Some(CascadeShape::G3D4)); + assert_eq!(CascadeShape::from_levels(1), None); + assert_eq!(CascadeShape::from_levels(5), None); + for s in CascadeShape::ROTATIONS { + assert_eq!(CascadeShape::from_levels(s.levels()), Some(s)); + } + + // The aligned shapes (Rails 6×2, GUID 3×4) have a shift group_of; G4D3 + // (other frameworks, 4×3) divides — a per-class cost, NOT a prohibition. assert_eq!( CascadeShape::ALIGNED, [CascadeShape::G3D4, CascadeShape::G6D2] ); for s in CascadeShape::ALIGNED { - assert!(s.is_byte_aligned(), "{s:?} is an aligned default"); - let sh = s.shift().expect("aligned carving has a shift"); + let sh = s.shift().expect("aligned shape has a shift"); for unit in 0..CASCADE_UNITS { assert_eq!( s.group_of(unit) as usize, @@ -666,15 +688,12 @@ mod tests { } assert_eq!(CascadeShape::G6D2.shift(), Some(1)); assert_eq!(CascadeShape::G3D4.shift(), Some(2)); - - // G4D3 is the WORST CASE, not a default: it straddles, has NO shift - // (group_of DIVIDES), is excluded from ALIGNED, but stays in ROTATIONS - // as the rare escape hatch (classid-stacking-entropy relief, msg 2). + // G4D3 is legitimate but not a shift shape: divide group_of, not in ALIGNED. assert!(!CascadeShape::G4D3.is_byte_aligned()); assert_eq!(CascadeShape::G4D3.shift(), None); assert!(!CascadeShape::ALIGNED.contains(&CascadeShape::G4D3)); assert!(CascadeShape::ROTATIONS.contains(&CascadeShape::G4D3)); - assert_eq!(CascadeShape::G4D3.group_of(2) as usize, 2 / 3); // a divide — the bad example + assert_eq!(CascadeShape::G4D3.group_of(2) as usize, 2 / 3); // a divide, its per-class cost } #[test] From b42356ca95310e08ac6b295fab864dc677aa336a Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 29 Jun 2026 21:20:42 +0000 Subject: [PATCH 6/8] =?UTF-8?q?docs(facet):=20scope=20CascadeShape=20(tran?= =?UTF-8?q?spile)=20vs=20the=20G2=C3=9748bit=20helix/CAM-PQ=20lane?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Operator scoping (2026-06-29): helix + CAM-PQ are a separate G2×48bit encoding lane (the two 48-bit chains / CAM-PQ 6×256 path code) for location (helix) and centroid (CAM-PQ, DeepNSM) — NOT required by transpile. Note on CascadeShape that the byte-shapes are the transpile/ClassView field-grouping lane; the G2×48bit two-chain view (hi_chain/lo_chain) is the helix/CAM-PQ location/centroid lane, never dragged into shape selection. Doc-only. Co-Authored-By: Claude --- crates/lance-graph-contract/src/facet.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/crates/lance-graph-contract/src/facet.rs b/crates/lance-graph-contract/src/facet.rs index 3c206eb03..98927ab8c 100644 --- a/crates/lance-graph-contract/src/facet.rs +++ b/crates/lance-graph-contract/src/facet.rs @@ -384,6 +384,13 @@ pub const CASCADE_UNITS: usize = 12; /// slots, so the cheap move is to *Tetris* each concern into its own slot /// (separation-of-concerns) rather than bit-pack. (`G4D3`'s divide is a per-class /// *shape* cost, separate from this — a class whose schema needs `4×3` is clean.) +/// +/// **Encoding-lane scope.** These byte-shapes (8-bit tiers) are the **transpile / +/// ClassView field-grouping** lane. A *separate* `G2×48bit` lane reads the same 12 +/// tier-bytes as the two 48-bit chains ([`hi_chain`](FacetCascade::hi_chain) / +/// [`lo_chain`](FacetCascade::lo_chain), cf. the CAM-PQ `6×256` path code) — for +/// **helix** (location) and **CAM-PQ** (centroid) encoding. That lane is **not +/// required by transpile** and is never dragged into ClassView shape selection. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum CascadeShape { /// 6 groups × 2 levels — native `(hi:lo)` per tier (`6×(1:2)`). The Rails From 10d2f1be37a71bd07ca55ae78e8c2387a92524f8 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 29 Jun 2026 21:24:38 +0000 Subject: [PATCH 7/8] docs(canonical_node): out-of-line bulk raw data + classid-inherited expansion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Operator doctrines (2026-06-29), neither a blocker: 1. Clean => expansion is classid-inherited (no global layout change; cf. RESERVE-DON'T-RECLAIM + class-conditioned CascadeShape). 2. Bulk raw data lives out-of-line in a SEPARATE Lance table, not the 480-byte value slab — a payload that won't fit even compressed (~3.2 Gbp genome; the FMA/BodyParts3D anatomy mesh at 4M vertices/6M triangles) is referenced by key/classid, addressed not inlined. Still not a blocker — the anatomy mesh baked cleanly as a SoA release. Node stays 512 B. Doc-only. Co-Authored-By: Claude --- crates/lance-graph-contract/src/canonical_node.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/crates/lance-graph-contract/src/canonical_node.rs b/crates/lance-graph-contract/src/canonical_node.rs index b92f9a532..129da4e1a 100644 --- a/crates/lance-graph-contract/src/canonical_node.rs +++ b/crates/lance-graph-contract/src/canonical_node.rs @@ -578,6 +578,19 @@ impl EdgeCodecFlavor { /// The 480-byte value is deferred — energy/meta/qualia/entity_type, materialized /// CausalEdge64, helix residue, fingerprint, class extensions all land here later, /// Lance-compressible. This is the row the MailboxSoA owns and the MailboxSoaView reads. +/// +/// **Two doctrines (operator 2026-06-29), neither a blocker:** +/// 1. **Clean ⇒ expansion is `classid`-inherited.** When a clean class's field +/// set / capacity grows, the `classid` selects the (expanded) shape — no +/// global layout change (cf. RESERVE-DON'T-RECLAIM + the class-conditioned +/// [`CascadeShape`](crate::facet::CascadeShape)). Expansion is never a blocker. +/// 2. **Bulk raw data lives out-of-line — a *separate* Lance table, not this +/// 480-byte value.** The value slab is for structured/compressible columns; a +/// raw payload that can't fit even compressed (a ~3.2 Gbp genome; the +/// FMA / BodyParts3D anatomy mesh at 4M vertices / 6M triangles) is its own +/// table, referenced by `key`/`classid` — and still not a blocker (the +/// anatomy mesh baked cleanly as a SoA release). The node stays 512 B; bulk +/// is addressed, not inlined. #[derive(Clone, Copy)] #[repr(C, align(64))] pub struct NodeRow { From 64a0d2cd22c8c43b6583813259096b3cc0a812f4 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 29 Jun 2026 21:31:41 +0000 Subject: [PATCH 8/8] fix(#621 review): guard CascadeShape coordinate math + exact-pin sub-toolchains MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address the two actionable PR-review findings on #621: - CodeRabbit (Major), facet.rs: CascadeShape::index/group_of/level_of did u8 arithmetic before widening, so out-of-range group/level/unit could wrap and the inverse only held for unit < CASCADE_UNITS. index now widens to usize BEFORE multiply/add (no u8 wrap possible), and all three carry a debug_assert precondition + a documented precondition (group --- crates/bge-m3/rust-toolchain.toml | 8 +++++--- crates/lance-graph-contract/src/facet.rs | 26 ++++++++++++++++++++++-- crates/reader-lm/rust-toolchain.toml | 8 +++++--- python/rust-toolchain.toml | 8 +++++--- 4 files changed, 39 insertions(+), 11 deletions(-) diff --git a/crates/bge-m3/rust-toolchain.toml b/crates/bge-m3/rust-toolchain.toml index 2d8dd2bea..c2127f491 100644 --- a/crates/bge-m3/rust-toolchain.toml +++ b/crates/bge-m3/rust-toolchain.toml @@ -1,4 +1,6 @@ [toolchain] -# Aligned to the ecosystem-wide 1.95 pin; matches this crate's own -# Cargo.toml rust-version = "1.95" (the channel was a 1.94.0 straggler). -channel = "1.95" +# Aligned to the ecosystem-wide pin; matches this crate's own Cargo.toml +# rust-version = "1.95" (the channel was a 1.94.0 straggler). EXACT pin +# "1.95.0" (not floating "1.95") so this subdir reuses the installed root +# toolchain instead of syncing a separate channel — offline-safe (Codex #621). +channel = "1.95.0" diff --git a/crates/lance-graph-contract/src/facet.rs b/crates/lance-graph-contract/src/facet.rs index 98927ab8c..306208f2d 100644 --- a/crates/lance-graph-contract/src/facet.rs +++ b/crates/lance-graph-contract/src/facet.rs @@ -468,10 +468,19 @@ impl CascadeShape { /// Linear unit index of `(group, level)`: `group · D + level` — groups laid /// out in order, coarse→fine within each. The single shared addressing rule /// for facet bytes *and* class fields. + /// + /// **Precondition:** `group < groups()` and `level < levels()` (the result is + /// then in `0..CASCADE_UNITS`). The multiply/add is done in `usize` (widen + /// first), so an out-of-range argument cannot wrap a `u8` — and a + /// `debug_assert` catches the misuse in debug builds. #[inline] #[must_use] pub const fn index(self, group: u8, level: u8) -> usize { - (group * self.levels() + level) as usize + debug_assert!( + group < self.groups() && level < self.levels(), + "CascadeShape::index: (group, level) out of range for this shape" + ); + group as usize * self.levels() as usize + level as usize } /// Inverse of [`index`](Self::index): which group linear unit `i` belongs to @@ -479,17 +488,30 @@ impl CascadeShape { /// (see [`shift`](Self::shift)); for [`G4D3`](Self::G4D3) it is a real divide /// — the per-class cost of the `4×3` shape. Dispatch on [`shift`](Self::shift) /// when you want the shift fast-path for the aligned shapes. + /// + /// **Precondition:** `unit < CASCADE_UNITS` — the inverse identity + /// `index(group_of(u), level_of(u)) == u` holds only on the 12-unit ladder + /// (`debug_assert`-checked). #[inline] #[must_use] pub const fn group_of(self, unit: usize) -> u8 { + debug_assert!( + unit < CASCADE_UNITS, + "CascadeShape::group_of: unit out of range" + ); (unit / self.levels() as usize) as u8 } /// Inverse of [`index`](Self::index): the within-group level of unit `i` - /// (`i % D`). + /// (`i % D`). **Precondition:** `unit < CASCADE_UNITS` (`debug_assert`-checked; + /// the inverse identity holds only on the 12-unit ladder). #[inline] #[must_use] pub const fn level_of(self, unit: usize) -> u8 { + debug_assert!( + unit < CASCADE_UNITS, + "CascadeShape::level_of: unit out of range" + ); (unit % self.levels() as usize) as u8 } diff --git a/crates/reader-lm/rust-toolchain.toml b/crates/reader-lm/rust-toolchain.toml index 2d8dd2bea..c2127f491 100644 --- a/crates/reader-lm/rust-toolchain.toml +++ b/crates/reader-lm/rust-toolchain.toml @@ -1,4 +1,6 @@ [toolchain] -# Aligned to the ecosystem-wide 1.95 pin; matches this crate's own -# Cargo.toml rust-version = "1.95" (the channel was a 1.94.0 straggler). -channel = "1.95" +# Aligned to the ecosystem-wide pin; matches this crate's own Cargo.toml +# rust-version = "1.95" (the channel was a 1.94.0 straggler). EXACT pin +# "1.95.0" (not floating "1.95") so this subdir reuses the installed root +# toolchain instead of syncing a separate channel — offline-safe (Codex #621). +channel = "1.95.0" diff --git a/python/rust-toolchain.toml b/python/rust-toolchain.toml index 096d85509..5470ef311 100644 --- a/python/rust-toolchain.toml +++ b/python/rust-toolchain.toml @@ -1,4 +1,6 @@ [toolchain] -# Aligned to the AdaWorldAPI ecosystem-wide 1.95 pin (root lance-graph -# rust-toolchain.toml). Was a 1.94.0 straggler. -channel = "1.95" +# Aligned to the AdaWorldAPI ecosystem-wide pin (root lance-graph +# rust-toolchain.toml). Was a 1.94.0 straggler. EXACT pin "1.95.0" (not +# floating "1.95") so this subdir reuses the installed root toolchain +# instead of syncing a separate channel — offline-safe (Codex #621). +channel = "1.95.0"