diff --git a/.claude/board/LATEST_STATE.md b/.claude/board/LATEST_STATE.md index 07ebdf0ff..076b03662 100644 --- a/.claude/board/LATEST_STATE.md +++ b/.claude/board/LATEST_STATE.md @@ -1,3 +1,11 @@ +## 2026-07-18 — branch `claude/review-claude-board-files-nhqgx1` (PR #729) — P4 ancestry pipeline on the SoA/ractor carrier: triangle read seam + owned columns + the 226-atom FROZEN palette256 codebook + +### Current Contract Inventory — new entry +- `lance_graph_contract::soa_view::StyleLane {Frozen, Learned, Explore}` + `MailboxSoaView::{style_lane_at(row, lane) -> Option<[u8;12]>, triangle_at(row, family) -> Option<(u8,u8,u8)>}` — the deferred-binding triangle read seam (default `None`, owner overrides; `family >= 12` guard). The SoA-native replacement for the symbiont-`Vec` path (symbiont deprecated; the triangle is owned by the ractor `KanbanActor`, compile-time sole-mutator E-CE64-MB-4). (P4 Brick 1.) +- `lance_graph_contract::cognitive_palette::{AtomId, AtomCatalogue, VERB/RECIPE/PERSONA/FAMILY_{COUNT,BASE}, RESERVED_BASE, ATOM_COUNT}` — the **226-atom palette256 FROZEN value codebook** (operator ruling 2026-07-18 "226 ARE the frozen"). A zero-dep ADDRESSING table (I-VSA-IDENTITIES: address only, content in registries): `0` null, `1..=144` Verb (dntree), `145..=178` Recipe, `179..=214` Persona (ThinkingStyle), `215..=226` Family (StyleFamily; local == ordinal), `227..=255` reserved (RESERVE-DON'T-RECLAIM). `resolve()` total over 256; const-asserted layout. The `12×u8` FROZEN reading of the content-blind triangle register; the LEARNED/EXPLORE orchestration reading is the `6×(8:8)` le-contract §3 register (replayable). 6 tests. + +lance-graph core (non-contract, same PR — cognitive-shader-driver): three `[[u8;12];N]` triangle columns on `MailboxSoA` + `style_lane_at` override + owned write ops (`set_style_lane`/`set_style_atom`/`promote_family`, `&mut self`) + `reset_row` clears them (codex #729 P2). (P4 Brick 2.) + ## 2026-07-17 — branch `claude/happy-hamilton-0azlw4` (post-#714) — D-GR-1 `contract::doc_graph::{DocGraphQuery, ScoredId}` + D-GR-3b AriGraph capabilities (PPR, Leiden refinement, BM25) + G0 harness ### Current Contract Inventory — new entry diff --git a/crates/cognitive-shader-driver/src/mailbox_soa.rs b/crates/cognitive-shader-driver/src/mailbox_soa.rs index 5b6d85c6b..b31761f37 100644 --- a/crates/cognitive-shader-driver/src/mailbox_soa.rs +++ b/crates/cognitive-shader-driver/src/mailbox_soa.rs @@ -31,7 +31,7 @@ use lance_graph_contract::cognitive_shader::MetaWord; use lance_graph_contract::collapse_gate::MailboxId; use lance_graph_contract::kanban::{ExecTarget, KanbanColumn, KanbanMove}; use lance_graph_contract::qualia::QualiaI4_16D; -use lance_graph_contract::soa_view::{IdentityPlane, MailboxSoaOwner, MailboxSoaView}; +use lance_graph_contract::soa_view::{IdentityPlane, MailboxSoaOwner, MailboxSoaView, StyleLane}; /// Canonical named-fingerprint plane width: 256 × u64 = 16,384 bits /// (mirrors `bindspace::WORDS_PER_FP`; defined locally so the mailbox does NOT @@ -152,6 +152,29 @@ pub struct MailboxSoA { /// `BindSpace.fingerprints.angle`. pub angle: Box<[u64]>, + // ── P4: autopoiesis-triangle policy lanes (12 palette256 atoms/row/lane) ── + // The three per-row style lanes appended after Kanban in the canonical + // `NodeRow` value slab (`ValueTenant::{FrozenStyle, LearnedStyle, + // ExploreStyle}`, offsets 152/164/176). Held here as SoA columns so a + // `KanbanActor`'s owned advance reads/writes them `&mut` (E-CE64-MB-4, ractor + // sole-mutator) — NOT a deprecated symbiont `Vec`. Each lane is a + // 12-byte content-blind register whose reading is ClassView-selected per + // ROW/CLASS (never per lane): a policy row reads all three as 12 palette atoms + // (index `f` = `StyleFamily` ordinal 0..11, value = a + // `cognitive_palette::AtomId`); an orchestration row reads all three as the + // 6×(8:8) rails (`soa_view::style_rails_at`). Atom 0 = null default (a zeroed + // lane reads all-null, never a wrong policy). The write ops below are byte-level + // and representation-agnostic; the reading is the caller's ClassView decision. + /// Per-row FROZEN policy lane — the checkpoint the can't-stop-thinking dispatch + /// runs off (read during `CognitiveWork`). + pub frozen_style: [[u8; 12]; N], + /// Per-row LEARNED policy lane — the L4 NARS-revision write target (written + /// during `Evaluation`; `learned[f]` promotes to `frozen[f]` on winning the arm). + pub learned_style: [[u8; 12]; N], + /// Per-row EXPLORE policy lane — the P64 perturbation variant, deterministic + /// address-derived jitter (the D-QUANTGATE coprime walk, never RNG; replay holds). + pub explore_style: [[u8; 12]; N], + /// Monotonic cycle stamp; advanced by `tick()`. pub current_cycle: u32, @@ -299,6 +322,10 @@ impl MailboxSoA { content: vec![0u64; N * WORDS_PER_FP].into_boxed_slice(), topic: vec![0u64; N * WORDS_PER_FP].into_boxed_slice(), angle: vec![0u64; N * WORDS_PER_FP].into_boxed_slice(), + // ── P4: autopoiesis-triangle lanes — zero (atom 0 = null default) ── + frozen_style: [[0u8; 12]; N], + learned_style: [[0u8; 12]; N], + explore_style: [[0u8; 12]; N], // ── W1c — empty mailbox: zero logical rows until `set_populated(...)` // declares the size (no write path bumps this implicitly) ── populated: 0, @@ -501,6 +528,14 @@ impl MailboxSoA { self.content[lo..hi].fill(0); self.topic[lo..hi].fill(0); self.angle[lo..hi].fill(0); + // ── P4 autopoiesis-triangle lanes reset (atom 0 = null) ── + // A reused row MUST read the all-null default, never a stale checkpoint/ + // learned/explore policy (codex #729 P2). Same field-isolation discipline as + // every column above — a lane reset_row forgets is exactly the stale-policy + // leak `style_lane_at` would then serve on a reused row. + self.frozen_style[row] = [0u8; 12]; + self.learned_style[row] = [0u8; 12]; + self.explore_style[row] = [0u8; 12]; } // ── Read-only inspectors ────────────────────────────────────────────────── @@ -729,6 +764,81 @@ impl MailboxSoA { } } +// ── P4: autopoiesis-triangle owned write ops (the ancestry-pipeline seam) ── +// +// Per R1 ("the SoA columns are mutated by the owner's own cognitive ops, never +// serialized through the contract trait"), these are the OWNER's crate-visible +// mutation surface for the three style lanes. Each is `&mut self`, so — when +// driven from a `KanbanActor::handle` whose `State` IS this owner — the +// single-writer no-aliasing guarantee is compile-time (E-CE64-MB-4, ractor +// sole-mutator), not by-convention. The *value* decisions (the explore +// coprime-walk atom, the NARS-revision learned atom) belong to the caller (the +// KanbanActor's phase handlers); these ops only apply an already-decided atom to +// the owned lane. An un-gated impl (NOT under `with-planner`): the triangle write +// surface has no planner dependency. `family >= 12` is a no-op (the #717 +// `triangle_for` guard — an out-of-range family never aliases slot 12); +// `row >= populated` is a no-op (the logical-row discipline shared with +// `style_lane_at` / `identity_plane_at`). +impl MailboxSoA { + /// Write a full 12-atom style `lane` for `row` (owner `&mut`). No-op if + /// `row >= populated`. + #[inline] + pub fn set_style_lane(&mut self, row: usize, lane: StyleLane, atoms: [u8; 12]) { + if row >= self.populated { + return; + } + match lane { + StyleLane::Frozen => self.frozen_style[row] = atoms, + StyleLane::Learned => self.learned_style[row] = atoms, + StyleLane::Explore => self.explore_style[row] = atoms, + } + } + + /// Write a single palette256 `atom` at `family` in `row`'s style `lane` (owner + /// `&mut`). No-op if `row >= populated` or `family >= 12`. + #[inline] + pub fn set_style_atom(&mut self, row: usize, lane: StyleLane, family: u8, atom: u8) { + if row >= self.populated || family >= 12 { + return; + } + let f = family as usize; + match lane { + StyleLane::Frozen => self.frozen_style[row][f] = atom, + StyleLane::Learned => self.learned_style[row][f] = atom, + StyleLane::Explore => self.explore_style[row][f] = atom, + } + } + + /// The **promotion** act (Evaluation-phase, kanbanstep-interior): `frozen[family] + /// := learned[family]` for `row` — the learned policy that won the held-out arm + /// replaces the checkpoint. Owner compares its own bytes and writes one; no + /// events, no waiting. Returns `true` if a promotion occurred (the byte + /// changed), `false` if it was a no-op (already equal, or out of range). No-op if + /// `row >= populated` or `family >= 12`. + /// + /// **Representation-preserving by construction:** the copy is coherent because + /// the Frozen and Learned lanes ALWAYS share the row's ClassView-selected reading + /// (`12×u8` palette for a policy row, `6×(8:8)` for an orchestration row — the + /// reading is per-row/class, never per-lane; `soa_view::StyleLane`). So a copied + /// byte is the same kind on both sides — an `AtomId` stays an `AtomId`, a rail + /// byte stays a rail byte — never an orchestration byte reinterpreted as a + /// palette atom. `family` indexes the slot uniformly regardless of which reading + /// the row uses. + #[inline] + pub fn promote_family(&mut self, row: usize, family: u8) -> bool { + if row >= self.populated || family >= 12 { + return false; + } + let f = family as usize; + let learned = self.learned_style[row][f]; + if self.frozen_style[row][f] == learned { + return false; + } + self.frozen_style[row][f] = learned; + true + } +} + // ── Contract trait impls: MailboxSoA IS the in-RAM Rubicon owner ────────────── // // `MailboxSoaView` (read) + `MailboxSoaOwner` (write) make `MailboxSoA` the @@ -783,6 +893,22 @@ impl MailboxSoaView for MailboxSoA { IdentityPlane::Angle => self.angle_row(row), }) } + /// Override the deferred-binding default: the in-RAM owner DOES carry the three + /// autopoiesis-triangle lanes (P4), so a `KanbanActor` reading `FrozenStyle` + /// during `CognitiveWork` gets the real checkpoint policy. `populated`-guarded + /// (same logical-row discipline as `identity_plane_at`); a query into + /// `populated..N` returns `None`. + #[inline] + fn style_lane_at(&self, row: usize, lane: StyleLane) -> Option<[u8; 12]> { + if row >= self.populated { + return None; + } + Some(match lane { + StyleLane::Frozen => self.frozen_style[row], + StyleLane::Learned => self.learned_style[row], + StyleLane::Explore => self.explore_style[row], + }) + } #[inline] fn energy(&self) -> &[f32] { &self.energy @@ -894,6 +1020,60 @@ mod tests { } } + // ── P4: autopoiesis-triangle owned lanes ───────────────────────────────── + + /// The three triangle lanes zero-init (atom 0 = null), the owner writes them + /// `&mut`, the view reads them back, promotion copies learned→frozen, and the + /// range/logical-row guards hold (the #717 aliasing + `populated` discipline). + #[test] + fn triangle_lanes_owned_write_read_and_promote() { + let mut mb: MailboxSoA<8> = MailboxSoA::new(3, 1, 1.0); + mb.set_populated(2); + + // Zero-init: every lane is all-null until written. + assert_eq!(mb.style_lane_at(0, StyleLane::Frozen), Some([0u8; 12])); + assert_eq!(mb.triangle_at(0, 0), Some((0, 0, 0))); + + // Owner writes a single learned atom, then explore; the view reflects it. + mb.set_style_atom(0, StyleLane::Learned, 5, 42); + mb.set_style_atom(0, StyleLane::Explore, 5, 99); + assert_eq!(mb.style_lane_at(0, StyleLane::Learned).unwrap()[5], 42); + assert_eq!(mb.triangle_at(0, 5), Some((0, 42, 99))); + + // Promotion: learned[5] (42) replaces frozen[5] (0) → returns true (changed). + assert!(mb.promote_family(0, 5)); + assert_eq!(mb.triangle_at(0, 5), Some((42, 42, 99))); + // Idempotent: a second promotion is a no-op (already equal) → false. + assert!(!mb.promote_family(0, 5)); + + // Field isolation: writing family 5 left the other 11 family slots untouched. + let frozen = mb.style_lane_at(0, StyleLane::Frozen).unwrap(); + for (f, &atom) in frozen.iter().enumerate() { + assert_eq!(atom, if f == 5 { 42 } else { 0 }, "frozen[{f}] isolation"); + } + + // Guards: family >= 12 is a no-op write + None read; row >= populated too. + mb.set_style_atom(0, StyleLane::Frozen, 12, 7); // out-of-range family: no-op + assert_eq!(mb.triangle_at(0, 12), None); + assert!(!mb.promote_family(0, 12)); + mb.set_style_atom(5, StyleLane::Frozen, 0, 7); // row 5 >= populated 2: no-op + assert_eq!(mb.style_lane_at(5, StyleLane::Frozen), None); + // The out-of-range writes changed nothing observable in the valid range. + assert_eq!(mb.triangle_at(0, 0), Some((0, 0, 0))); + + // reset_row clears all three lanes → a reused row reads the all-null default, + // never a stale policy (codex #729 P2; same guarantee the other columns get). + mb.set_style_atom(1, StyleLane::Frozen, 3, 77); + mb.set_style_atom(1, StyleLane::Learned, 3, 88); + mb.set_style_atom(1, StyleLane::Explore, 3, 99); + assert_eq!(mb.triangle_at(1, 3), Some((77, 88, 99))); + mb.reset_row(1); + assert_eq!(mb.style_lane_at(1, StyleLane::Frozen), Some([0u8; 12])); + assert_eq!(mb.style_lane_at(1, StyleLane::Learned), Some([0u8; 12])); + assert_eq!(mb.style_lane_at(1, StyleLane::Explore), Some([0u8; 12])); + assert_eq!(mb.triangle_at(1, 3), Some((0, 0, 0))); + } + // ── test 2: w_slot panic ───────────────────────────────────────────────── /// w_slot >= 64 must panic with a clear message (programming error). diff --git a/crates/lance-graph-contract/src/cognitive_palette.rs b/crates/lance-graph-contract/src/cognitive_palette.rs new file mode 100644 index 000000000..90074da63 --- /dev/null +++ b/crates/lance-graph-contract/src/cognitive_palette.rs @@ -0,0 +1,309 @@ +//! `cognitive_palette` — the **226-atom palette256 FROZEN value codebook**. +//! +//! # What this is (operator ruling 2026-07-18) +//! +//! *"226 ARE the frozen; anything else needs 6×2×8bit (12 slots for an +//! Orchestration for v3 substrate replayability)."* +//! +//! The autopoiesis-triangle lanes ([`StyleLane`](crate::soa_view::StyleLane), on +//! `NodeRow` value tenants / `MailboxSoA` columns) are each a 12-byte +//! **content-blind register** — the same 96 bits the LE contract carves as +//! `6×(8:8)` / `4×(8:8:8)` / `3×(8:8:8:8)` / `12×8` (`.claude/v3/soa_layout/le-contract.md` +//! §3). This module names the **`12×u8` FROZEN reading**: each of the 12 slots +//! (a [`StyleFamily`](crate::style_family::StyleFamily) ordinal) holds ONE +//! **`AtomId`** — a palette256 index into this 226-atom catalogue. +//! +//! **The reading is ClassView-selected PER ROW/CLASS, never per lane** (the +//! le-contract §3 content-blind-register rule). Within a **policy / thinking-class** +//! row, ALL THREE triangle lanes — Frozen, Learned, Explore — are read as `12×u8` +//! palette atoms into THIS catalogue; that uniformity is what makes the autopoiesis +//! promotion `learned[f] → frozen[f]` a coherent `AtomId` copy (both operands are +//! palette atoms, never a byte reinterpreted across representations). The **other** +//! reading of the same 12 bytes — `6×(8:8)` (le-contract §3 L1/L4, replayable per +//! `E-H268-REPLAYABLE-TILE-1`) — is what an **orchestration-class** row selects for +//! ALL its lanes; it does NOT index this catalogue. One register, two ClassView- +//! selected readings (the triangle plan +//! `.claude/plans/triangle-tenants-gestalt-separation-v1.md` §4 "12 families | 12 +//! template steps") — the discriminant is the ROW's class, so Frozen/Learned/Explore +//! never disagree on representation. +//! +//! # This is an ADDRESSING table, not a content store +//! +//! Per `I-VSA-IDENTITIES` (Layer 2 = domain role catalogues, Layer 3 = content +//! stores): this module owns only the **address space** — which palette index +//! resolves to which catalogue and local index. It is deliberately **zero-dep** +//! and does NOT import the catalogues' concrete types. The content each atom +//! points to lives in its own registry, resolved by the caller: +//! +//! | Catalogue | count | content registry (where the atom RESOLVES) | +//! |---|---|---| +//! | Verb | 144 | `holograph::dntree` `DnVerb` (0..=143; 6 categories × 24) | +//! | Recipe | 34 | `crate::recipes` `RECIPES` (the 34 NARS tactic runbooks) | +//! | Persona | 36 | `crate::thinking` `ThinkingStyle::ALL` (36 styles / 6 clusters) | +//! | Family | 12 | `crate::style_family` `StyleFamily::ALL` (the 12 abstract families) | +//! +//! # Layout (operator-locked composition, RESERVE-DON'T-RECLAIM) +//! +//! The composition (`144 verb ∥ 34 recipe ∥ 36 persona ∥ 12 family; 30 reserved`) +//! is fixed by the triangle plan §1. Offsets are **permanent** once shipped — a +//! consumer stores a bare `u8` atom, so reordering a sub-range would silently +//! reinterpret every persisted lane. The catalogue is append-only into the 29 +//! reserved slots; existing ranges never move. +//! +//! ```text +//! 0 NULL (atom 0 = the null default — a zeroed lane reads all-null) +//! 1 ..= 144 Verb (144; local = palette - 1) +//! 145 ..= 178 Recipe ( 34; local = palette - 145) +//! 179 ..= 214 Persona ( 36; local = palette - 179) +//! 215 ..= 226 Family ( 12; local = palette - 215; local == StyleFamily ordinal) +//! 227 ..= 255 Reserved ( 29; the append margin — never free real estate) +//! ``` + +/// A palette256 index — one byte, `0..=255`. Value `0` is the null default +/// (`AtomId::NULL`); `1..=226` address the four catalogues; `227..=255` are the +/// append-only reserve. Stored bare (`u8`) in a `12×u8` FROZEN lane slot. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct AtomId(pub u8); + +/// The null default — an un-populated FROZEN lane slot reads this (never a wrong +/// policy). Matches the `canonical_node` "atom 0 = null default" convention. +impl AtomId { + /// The null / unassigned atom (palette index `0`). + pub const NULL: AtomId = AtomId(0); +} + +// ── Range layout (operator-locked; RESERVE-DON'T-RECLAIM) ── + +/// Count of Verb atoms (`holograph::dntree` `DnVerb`, 6 categories × 24). +pub const VERB_COUNT: u8 = 144; +/// Count of Recipe atoms (the 34 NARS tactic runbooks, `crate::recipes`). +pub const RECIPE_COUNT: u8 = 34; +/// Count of Persona atoms (`crate::thinking` `ThinkingStyle::ALL`, 36 styles). +pub const PERSONA_COUNT: u8 = 36; +/// Count of Family atoms (`crate::style_family` `StyleFamily::ALL`, 12 families). +pub const FAMILY_COUNT: u8 = 12; + +/// First palette index of the Verb range (`1`; index `0` is `NULL`). +pub const VERB_BASE: u8 = 1; +/// First palette index of the Recipe range. +pub const RECIPE_BASE: u8 = VERB_BASE + VERB_COUNT; // 145 +/// First palette index of the Persona range. +pub const PERSONA_BASE: u8 = RECIPE_BASE + RECIPE_COUNT; // 179 +/// First palette index of the Family range. +pub const FAMILY_BASE: u8 = PERSONA_BASE + PERSONA_COUNT; // 215 +/// First palette index of the reserved append-margin range. +pub const RESERVED_BASE: u8 = FAMILY_BASE + FAMILY_COUNT; // 227 + +/// Total addressed atoms (`226` — the "226 ARE the frozen" catalogue). +pub const ATOM_COUNT: u16 = + VERB_COUNT as u16 + RECIPE_COUNT as u16 + PERSONA_COUNT as u16 + FAMILY_COUNT as u16; + +// Compile-time proof the layout is exactly the operator-locked 256-slot carve: +// null(1) + 226 atoms + reserved = 256, and the sub-ranges are contiguous. +const _: () = assert!(ATOM_COUNT == 226); +const _: () = assert!(RESERVED_BASE == 227); +const _: () = assert!( + 256 - RESERVED_BASE as u16 == 29, + "29 reserved slots (227..=255)" +); + +/// Which catalogue (and local index within it) a palette [`AtomId`] resolves to. +/// The `u8` payload is the **local** index inside that catalogue's registry +/// (Verb `0..=143`, Recipe `0..=33`, Persona `0..=35`, Family `0..=11`), NOT the +/// palette index. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum AtomCatalogue { + /// The null default (palette index `0`). + Null, + /// A `holograph::dntree` `DnVerb`, local `0..=143`. + Verb(u8), + /// A `crate::recipes` recipe, local `0..=33`. + Recipe(u8), + /// A `crate::thinking` `ThinkingStyle`, local `0..=35`. + Persona(u8), + /// A `crate::style_family` `StyleFamily`, local `0..=11` (== the ordinal). + Family(u8), + /// An append-margin reserved slot (palette `227..=255`); carries the offset + /// past [`RESERVED_BASE`] so a future promotion can be placed deterministically. + Reserved(u8), +} + +impl AtomId { + /// Resolve this palette index to its catalogue + local index. Total function + /// over all `256` byte values (reserved / null included) — never panics. + #[must_use] + pub const fn resolve(self) -> AtomCatalogue { + let p = self.0; + if p == 0 { + AtomCatalogue::Null + } else if p < RECIPE_BASE { + AtomCatalogue::Verb(p - VERB_BASE) + } else if p < PERSONA_BASE { + AtomCatalogue::Recipe(p - RECIPE_BASE) + } else if p < FAMILY_BASE { + AtomCatalogue::Persona(p - PERSONA_BASE) + } else if p < RESERVED_BASE { + AtomCatalogue::Family(p - FAMILY_BASE) + } else { + AtomCatalogue::Reserved(p - RESERVED_BASE) + } + } + + /// The palette [`AtomId`] for Verb `local` (`0..=143`), or `None` if out of range. + #[inline] + #[must_use] + pub const fn verb(local: u8) -> Option { + if local < VERB_COUNT { + Some(AtomId(VERB_BASE + local)) + } else { + None + } + } + + /// The palette [`AtomId`] for Recipe `local` (`0..=33`), or `None` if out of range. + #[inline] + #[must_use] + pub const fn recipe(local: u8) -> Option { + if local < RECIPE_COUNT { + Some(AtomId(RECIPE_BASE + local)) + } else { + None + } + } + + /// The palette [`AtomId`] for Persona `local` (`0..=35`), or `None` if out of range. + #[inline] + #[must_use] + pub const fn persona(local: u8) -> Option { + if local < PERSONA_COUNT { + Some(AtomId(PERSONA_BASE + local)) + } else { + None + } + } + + /// The palette [`AtomId`] for Family `local` (`0..=11`, == the `StyleFamily` + /// ordinal), or `None` if out of range. + #[inline] + #[must_use] + pub const fn family(local: u8) -> Option { + if local < FAMILY_COUNT { + Some(AtomId(FAMILY_BASE + local)) + } else { + None + } + } + + /// Whether this is the null default. + #[inline] + #[must_use] + pub const fn is_null(self) -> bool { + self.0 == 0 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn ranges_are_contiguous_and_total_226() { + // The four sub-ranges tile [1, 227) with no gap and no overlap. + assert_eq!(VERB_BASE, 1); + assert_eq!(RECIPE_BASE, 145); + assert_eq!(PERSONA_BASE, 179); + assert_eq!(FAMILY_BASE, 215); + assert_eq!(RESERVED_BASE, 227); + assert_eq!(ATOM_COUNT, 226); + // 29 reserved slots fill 227..=255. + assert_eq!(256 - RESERVED_BASE as u16, 29); + } + + #[test] + fn resolve_is_total_and_every_byte_lands_in_exactly_one_catalogue() { + // Walk all 256 byte values; count each catalogue; assert the census. + let (mut null, mut verb, mut recipe, mut persona, mut family, mut reserved) = + (0, 0, 0, 0, 0, 0); + for p in 0u16..=255 { + match AtomId(p as u8).resolve() { + AtomCatalogue::Null => null += 1, + AtomCatalogue::Verb(l) => { + assert!(l < VERB_COUNT); + verb += 1; + } + AtomCatalogue::Recipe(l) => { + assert!(l < RECIPE_COUNT); + recipe += 1; + } + AtomCatalogue::Persona(l) => { + assert!(l < PERSONA_COUNT); + persona += 1; + } + AtomCatalogue::Family(l) => { + assert!(l < FAMILY_COUNT); + family += 1; + } + AtomCatalogue::Reserved(_) => reserved += 1, + } + } + assert_eq!(null, 1); + assert_eq!(verb, 144); + assert_eq!(recipe, 34); + assert_eq!(persona, 36); + assert_eq!(family, 12); + assert_eq!(reserved, 29); + assert_eq!(null + verb + recipe + persona + family + reserved, 256); + } + + #[test] + fn constructors_round_trip_through_resolve() { + for l in 0..VERB_COUNT { + assert_eq!(AtomId::verb(l).unwrap().resolve(), AtomCatalogue::Verb(l)); + } + for l in 0..RECIPE_COUNT { + assert_eq!( + AtomId::recipe(l).unwrap().resolve(), + AtomCatalogue::Recipe(l) + ); + } + for l in 0..PERSONA_COUNT { + assert_eq!( + AtomId::persona(l).unwrap().resolve(), + AtomCatalogue::Persona(l) + ); + } + for l in 0..FAMILY_COUNT { + assert_eq!( + AtomId::family(l).unwrap().resolve(), + AtomCatalogue::Family(l) + ); + } + } + + #[test] + fn constructors_reject_out_of_range() { + assert_eq!(AtomId::verb(VERB_COUNT), None); + assert_eq!(AtomId::recipe(RECIPE_COUNT), None); + assert_eq!(AtomId::persona(PERSONA_COUNT), None); + assert_eq!(AtomId::family(FAMILY_COUNT), None); + } + + #[test] + fn null_default_matches_the_zeroed_lane_convention() { + assert!(AtomId::NULL.is_null()); + assert_eq!(AtomId::NULL.resolve(), AtomCatalogue::Null); + // A zeroed FROZEN lane byte reads as the null atom, never a wrong policy. + assert_eq!(AtomId(0).resolve(), AtomCatalogue::Null); + assert!(!AtomId::family(0).unwrap().is_null()); + } + + #[test] + fn family_local_is_the_style_family_ordinal() { + // Family atom `f` resolves to Family(f) where f is exactly the + // StyleFamily ordinal — so triangle slot `f` (indexed by family ordinal) + // and the family atom's local index agree. + assert_eq!(AtomId::family(0).unwrap(), AtomId(FAMILY_BASE)); + assert_eq!(AtomId::family(11).unwrap(), AtomId(226)); + assert_eq!(AtomId(226).resolve(), AtomCatalogue::Family(11)); + } +} diff --git a/crates/lance-graph-contract/src/lib.rs b/crates/lance-graph-contract/src/lib.rs index be91f3060..9e8c80dd3 100644 --- a/crates/lance-graph-contract/src/lib.rs +++ b/crates/lance-graph-contract/src/lib.rs @@ -63,6 +63,7 @@ pub mod classid_scan; pub mod codebook; pub mod codegen_manifest; pub mod codegen_spine; +pub mod cognitive_palette; pub mod cognitive_shader; pub mod collapse_gate; pub mod container; diff --git a/crates/lance-graph-contract/src/soa_view.rs b/crates/lance-graph-contract/src/soa_view.rs index c2c3782ea..05f6adea4 100644 --- a/crates/lance-graph-contract/src/soa_view.rs +++ b/crates/lance-graph-contract/src/soa_view.rs @@ -33,6 +33,30 @@ pub enum IdentityPlane { Angle, } +/// Which lane of the **autopoiesis triangle** a style read selects — the three +/// per-row policy lanes appended after Kanban in the canonical `NodeRow` value slab +/// (`ValueTenant::{FrozenStyle, LearnedStyle, ExploreStyle}`, offsets 152/164/176). +/// Each lane is 12 palette256 atoms (one per [`StyleFamily`](crate::style_family::StyleFamily) +/// ordinal 0..11, OR per compiled-template step 0..11 — ClassView-selected). Reading +/// one is a value-slab read (the costed tier), mirroring [`IdentityPlane`]. +/// +/// The triangle is the ancestry-pipeline wiring surface: **dispatch** reads +/// [`Frozen`](StyleLane::Frozen) (the checkpoint policy the can't-stop-thinking +/// cycle runs off), the P64 perturbation ladder writes [`Explore`](StyleLane::Explore), +/// and the L4 learning seam writes [`Learned`](StyleLane::Learned) via NARS revision — +/// each write an **owned SoA update** on the mailbox's own lane (`MailboxSoaOwner`, +/// ractor sole-mutator), never a by-convention `&mut`. Triangle plan +/// `.claude/plans/triangle-tenants-gestalt-separation-v1.md` §1. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum StyleLane { + /// The CHECKPOINT policy lane (`ValueTenant::FrozenStyle`) — what the row does now. + Frozen, + /// The NARS-revision-updated policy lane (`ValueTenant::LearnedStyle`) — the L4 write target. + Learned, + /// The deterministic address-derived exploration variant (`ValueTenant::ExploreStyle`). + Explore, +} + /// A transparent, read-only view over one mailbox's SoA columns. /// /// Implementors return **borrows** (`&[T]`) or `Copy` scalars — never clones of the @@ -153,6 +177,78 @@ pub trait MailboxSoaView { None } + /// `row`'s 12-atom **autopoiesis-triangle** lane for the selected + /// [`StyleLane`] (Frozen / Learned / Explore) — the value-slab policy bytes at + /// `ValueTenant::{FrozenStyle, LearnedStyle, ExploreStyle}` (offsets + /// 152/164/176 in the canonical `NodeRow`). Index `f` of the returned `[u8; 12]` + /// is the palette256 atom for [`StyleFamily`](crate::style_family::StyleFamily) + /// ordinal `f` (atom `0` = null default — an un-populated lane reads all-null, + /// never a wrong policy). + /// + /// **Default = `None` (zero-fallback, deferred binding)** — same discipline as + /// [`identity_plane_at`](MailboxSoaView::identity_plane_at): a view that has not + /// materialized the triangle lanes returns `None`, and a consumer falls back to + /// the default style. The in-RAM `MailboxSoA` owner (which carries the triangle + /// columns, P4) overrides this; the canonical `NodeRow` already exposes the bytes + /// via [`NodeRow::style_lane`](crate::canonical_node::NodeRow::style_lane). + #[inline] + fn style_lane_at(&self, _row: usize, _lane: StyleLane) -> Option<[u8; 12]> { + None + } + + /// `row`'s per-family triangle read `(frozen, learned, explore)` — the three + /// palette256 atoms at [`StyleFamily`](crate::style_family::StyleFamily) ordinal + /// `family`, one glance across the three lanes (mirrors + /// [`NodeRow::triangle_for`](crate::canonical_node::NodeRow::triangle_for)). + /// + /// Default composes [`style_lane_at`](MailboxSoaView::style_lane_at) over the + /// three lanes: `None` if the view has not materialized them, or if `family >= 12` + /// (out of range → no triangle, never an aliased slot — the #717 `triangle_for` + /// guard). An owner with a cheaper single-row read may override. + #[inline] + fn triangle_at(&self, row: usize, family: u8) -> Option<(u8, u8, u8)> { + if family >= 12 { + return None; + } + let f = self.style_lane_at(row, StyleLane::Frozen)?; + let l = self.style_lane_at(row, StyleLane::Learned)?; + let e = self.style_lane_at(row, StyleLane::Explore)?; + let i = family as usize; + Some((f[i], l[i], e[i])) + } + + /// `row`'s style `lane` read as the **6×(8:8) orchestration register** — the six + /// `(u8, u8)` rails of the same 12-byte content-blind register + /// (`.claude/v3/soa_layout/le-contract.md` §3: `12 = 6×2`). + /// + /// **This is the ALTERNATE reading of the SAME bytes, ClassView-selected per + /// ROW/CLASS — never per lane.** A **policy / thinking-class** row reads all + /// three lanes as `12×u8` palette atoms + /// ([`style_lane_at`](MailboxSoaView::style_lane_at) → + /// [`cognitive_palette`](crate::cognitive_palette)); an **orchestration-class** + /// row reads all three lanes as these `6×(8:8)` rails (V3-replayable — le-contract + /// §3 L1 `part_of:is_a` / L4 `palette256²`, `E-H268-REPLAYABLE-TILE-1`). Frozen, + /// Learned, and Explore therefore never disagree on representation within a row, + /// which is what keeps the autopoiesis promotion coherent. This accessor exposes + /// the rails reading mechanically (`rail k = (lane[2k], lane[2k+1])`); whether a + /// given row's class actually uses it is the ClassView's call, not this lane's. + /// + /// Default composes [`style_lane_at`](MailboxSoaView::style_lane_at) (so any owner + /// that materializes the lane gets this reading for free); `None` if the lane is + /// not materialized. + #[inline] + fn style_rails_at(&self, row: usize, lane: StyleLane) -> Option<[(u8, u8); 6]> { + let b = self.style_lane_at(row, lane)?; + Some([ + (b[0], b[1]), + (b[2], b[3]), + (b[4], b[5]), + (b[6], b[7]), + (b[8], b[9]), + (b[10], b[11]), + ]) + } + // NOTE (follow-up): the qualia column (`QualiaI4_16D`) accessor is intentionally omitted — // add `fn qualia(&self) -> &[crate::qualia::QualiaI4_16D]` when the first consumer // (planner strategy selection) needs it; keep the read surface minimal until then. @@ -355,4 +451,100 @@ mod tests { assert_eq!(m.to, KanbanColumn::CognitiveWork); assert_eq!(soa.phase(), KanbanColumn::CognitiveWork); } + + #[test] + fn style_lane_defaults_to_none_until_the_owner_materializes_the_triangle() { + // Deferred binding: a view with no triangle columns resolves every style + // lane (and the composed per-family read) to None — the consumer falls back + // to the default style, never a wrong policy. The in-RAM MailboxSoA owner + // (P4) overrides this. + let soa = sample(); + assert_eq!(soa.style_lane_at(0, StyleLane::Frozen), None); + assert_eq!(soa.style_lane_at(0, StyleLane::Learned), None); + assert_eq!(soa.style_lane_at(0, StyleLane::Explore), None); + assert_eq!(soa.triangle_at(0, 0), None); + } + + /// An owner that HAS materialized the triangle lanes — proves the override + + /// `triangle_at` composition + the `family >= 12` guard (the #717 aliasing guard). + struct StyledSoa { + frozen: [u8; 12], + learned: [u8; 12], + explore: [u8; 12], + } + + impl MailboxSoaView for StyledSoa { + fn mailbox_id(&self) -> MailboxId { + 1 + } + fn n_rows(&self) -> usize { + 1 + } + fn w_slot(&self) -> u8 { + 1 + } + fn current_cycle(&self) -> u32 { + 0 + } + fn phase(&self) -> KanbanColumn { + KanbanColumn::CognitiveWork + } + fn energy(&self) -> &[f32] { + &[] + } + fn edges_raw(&self) -> &[u64] { + &[] + } + fn meta_raw(&self) -> &[u32] { + &[] + } + fn entity_type(&self) -> &[u16] { + &[] + } + fn style_lane_at(&self, _row: usize, lane: StyleLane) -> Option<[u8; 12]> { + Some(match lane { + StyleLane::Frozen => self.frozen, + StyleLane::Learned => self.learned, + StyleLane::Explore => self.explore, + }) + } + } + + #[test] + fn triangle_at_composes_the_three_lanes_and_guards_family_range() { + let soa = StyledSoa { + frozen: [10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21], + learned: [30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41], + explore: [50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61], + }; + // Per-family read picks index `family` from each lane. + assert_eq!(soa.triangle_at(0, 0), Some((10, 30, 50))); + assert_eq!(soa.triangle_at(0, 11), Some((21, 41, 61))); + // family == 12 is out of range → None, never the aliased slot 12 (#717 guard). + assert_eq!(soa.triangle_at(0, 12), None); + assert_eq!(soa.triangle_at(0, u8::MAX), None); + // The whole lane reads through. + assert_eq!(soa.style_lane_at(0, StyleLane::Learned).unwrap()[5], 35); + + // The 6×(8:8) orchestration reading carves the SAME 12 bytes as 6 rails + // (le-contract §3; the "anything else" reading). Frozen bytes 10..21 → + // (10,11),(12,13),...,(20,21). + assert_eq!( + soa.style_rails_at(0, StyleLane::Frozen), + Some([(10, 11), (12, 13), (14, 15), (16, 17), (18, 19), (20, 21)]) + ); + // Same register, two readings: rail k = (lane[2k], lane[2k+1]). + let learned = soa.style_lane_at(0, StyleLane::Learned).unwrap(); + let rails = soa.style_rails_at(0, StyleLane::Learned).unwrap(); + for (k, &(lo, hi)) in rails.iter().enumerate() { + assert_eq!((lo, hi), (learned[2 * k], learned[2 * k + 1])); + } + } + + #[test] + fn style_rails_defaults_to_none_when_lane_unmaterialized() { + // The 6×(8:8) reading composes style_lane_at, so a view with no triangle + // returns None (same deferred-binding discipline). + assert_eq!(sample().style_rails_at(0, StyleLane::Explore), None); + } }