Skip to content
Merged
15 changes: 15 additions & 0 deletions .claude/board/LATEST_STATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -657,3 +657,18 @@ 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::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.
- **`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.
6 changes: 5 additions & 1 deletion crates/bge-m3/rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
[toolchain]
channel = "1.94.0"
# 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"
56 changes: 56 additions & 0 deletions crates/lance-graph-contract/src/canonical_node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -643,6 +656,29 @@ pub const NODE_ROW_COLUMNS: &[ColumnDescriptor] = &[
/// Row stride for [`NodeRow`] in bytes — equal to `size_of::<NodeRow>()`.
pub const NODE_ROW_STRIDE: usize = 512;

/// The node viewed as fixed-size **GUID slots**: `NODE_ROW_STRIDE /
/// size_of::<NodeGuid>()` = `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 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!(
GUIDS_PER_NODE == 32 && GUIDS_PER_NODE * core::mem::size_of::<NodeGuid>() == 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).
Expand Down Expand Up @@ -1596,6 +1632,26 @@ mod tests {
assert_eq!(NODE_ROW_STRIDE, core::mem::size_of::<NodeRow>());
}

#[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::<NodeGuid>(),
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::<NodeGuid>() + core::mem::size_of::<EdgeBlock>()) / 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
Expand Down
Loading
Loading