From 04f8cc966472defe312180b3b1e6a154b2609683 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 18 Jul 2026 19:55:29 +0000 Subject: [PATCH 1/6] feat(contract): triangle-lane read seam on MailboxSoaView (P4 Brick 1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The autopoiesis triangle (FrozenStyle/LearnedStyle/ExploreStyle, shipped #717 as NodeRow value tenants) needs a SoA-native read seam so the ractor KanbanActor — the sole-mutator MailboxSoaOwner — can read FrozenStyle during CognitiveWork and the planner can project the lanes. Everything is SoA: the triangle read is a MailboxSoaView column read, not a symbiont Vec access (symbiont is the deprecated/blocked arm). Adds, following the established deferred-binding pattern (identity_plane_at / edge_block_at — default None, the owner overrides): - StyleLane { Frozen, Learned, Explore } (mirrors IdentityPlane). - MailboxSoaView::style_lane_at(row, lane) -> Option<[u8;12]> (default None). - MailboxSoaView::triangle_at(row, family) -> Option<(u8,u8,u8)> composing the three lanes, with the family>=12 guard (the #717 triangle_for aliasing guard) so an out-of-range family is None, never an aliased slot. Non-breaking (all defaults): existing MailboxSoaView/Owner impls (surreal_container read view, MailboxSoa owner, planner consumer) compile unchanged; the in-RAM MailboxSoa owner overrides these when it materializes the triangle columns (Brick 2). +2 tests: default-None deferred binding, and an owner override proving triangle_at composition + the family-range guard. Corrected carrier per operator ruling: symbiont deprecated; everything is SoA; every thinking is an owned SoA kanban update; ractor is the compile-time ownership guarantee (KanbanActor &mut state, E-CE64-MB-4). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Awg6TXocHcwTtc6eGsHcdD --- crates/lance-graph-contract/src/soa_view.rs | 139 ++++++++++++++++++++ 1 file changed, 139 insertions(+) diff --git a/crates/lance-graph-contract/src/soa_view.rs b/crates/lance-graph-contract/src/soa_view.rs index c2c3782ea..77355bea6 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,46 @@ 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])) + } + // 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 +419,79 @@ 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); + } } From b750881af393db5ae6317a87cfc3df301391987c Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 18 Jul 2026 20:03:26 +0000 Subject: [PATCH 2/6] feat(shader): triangle SoA columns + owned write ops on MailboxSoA (P4 Brick 2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wire the autopoiesis triangle onto the SoA — the corrected carrier. The lanes are three per-row [[u8;12];N] columns on MailboxSoA (the in-RAM MailboxSoaOwner the ractor KanbanActor owns), NOT a symbiont Vec (deprecated). Value = a palette256 atom indexed by StyleFamily ordinal 0..11; atom 0 = null (a zeroed lane reads all-null, never a wrong policy). Adds: - frozen_style / learned_style / explore_style columns (zero-init). - MailboxSoaView::style_lane_at override (real read, populated-guarded — the KanbanActor reads FrozenStyle during CognitiveWork through the real view). - Owner write ops (crate-visible, &mut self, un-gated — no planner dep): set_style_lane, set_style_atom, promote_family (frozen[f] := learned[f], returns whether it changed). Per R1 these are the owner's own cognitive ops; the VALUE decisions (explore coprime-walk atom, NARS-revision learned atom) belong to the caller (KanbanActor phase handlers, Brick 3). Every op is &mut self, so when driven from 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 exact hazard the ancestry map flagged on the old symbiont path. Guards: family>=12 is a no-op (the #717 triangle_for aliasing guard); row>=populated is a no-op (logical-row discipline). +1 test: owned write/read/promote round-trip, idempotent promotion, field isolation across the 11 untouched family slots, and both guards. 104/104 lib tests green, clippy clean (no new warnings). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Awg6TXocHcwTtc6eGsHcdD --- .../src/mailbox_soa.rs | 148 +++++++++++++++++- 1 file changed, 147 insertions(+), 1 deletion(-) diff --git a/crates/cognitive-shader-driver/src/mailbox_soa.rs b/crates/cognitive-shader-driver/src/mailbox_soa.rs index 5b6d85c6b..de7a5989f 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,24 @@ 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`. Index `f` = + // `StyleFamily` ordinal 0..11; value = a palette256 atom (atom 0 = null + // default, so a zeroed lane reads all-null, never a wrong policy). + /// 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 +317,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, @@ -729,6 +751,72 @@ 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`. + #[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 +871,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 +998,48 @@ 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))); + } + // ── test 2: w_slot panic ───────────────────────────────────────────────── /// w_slot >= 64 must panic with a clear message (programming error). From 3d66781d530623c7b510a9c9888bc6653362823d Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 18 Jul 2026 20:07:59 +0000 Subject: [PATCH 3/6] fix(shader): reset_row clears the triangle lanes (codex #729 P2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex caught a real stale-policy leak: reset_row cleared every other per-row column (energy/plasticity/sentinels/thoughtspace/identity planes) but not the new frozen_style/learned_style/explore_style lanes. A reused row would read an old checkpoint/learned/explore policy via style_lane_at instead of the documented all-null default — the exact class of leak the existing reset field-isolation tests guard against for the other columns. Fix: reset_row zeroes the three lanes (atom 0 = null). Regression: the triangle test now writes all three lanes on a row, resets it, and asserts the all-null default returns. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Awg6TXocHcwTtc6eGsHcdD --- .../src/mailbox_soa.rs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/crates/cognitive-shader-driver/src/mailbox_soa.rs b/crates/cognitive-shader-driver/src/mailbox_soa.rs index de7a5989f..03e3b728d 100644 --- a/crates/cognitive-shader-driver/src/mailbox_soa.rs +++ b/crates/cognitive-shader-driver/src/mailbox_soa.rs @@ -523,6 +523,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 ────────────────────────────────────────────────── @@ -1038,6 +1046,18 @@ mod tests { 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 ───────────────────────────────────────────────── From b26d184a90297dda154f0d1734ba77a6f270b6f8 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 18 Jul 2026 20:35:10 +0000 Subject: [PATCH 4/6] feat(contract): 226-atom palette256 FROZEN value codebook (P4 Brick 3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Operator ruling 2026-07-18: "226 ARE the frozen; anything else needs 6x2x8bit (12 slots for an Orchestration for v3 substrate replayability)." This lands the FROZEN half — the 226-atom palette256 value catalogue that the FROZEN triangle lane's 12 bytes index (12xu8 reading). The LEARNED/EXPLORE orchestration reading of the same content-blind register is the 6x(8:8) le-contract §3 register (replayable, E-H268-REPLAYABLE-TILE-1) — a separate follow-up, not this catalogue. cognitive_palette.rs — a zero-dep ADDRESSING table (I-VSA-IDENTITIES: address space only, content resolves in the registries): - AtomId(u8): 0 null | 1..=144 Verb (holograph dntree) | 145..=178 Recipe (34 NARS runbooks) | 179..=214 Persona (ThinkingStyle::ALL 36) | 215..=226 Family (StyleFamily::ALL 12; local == ordinal) | 227..=255 reserved. - AtomCatalogue resolve() — total over all 256 bytes, never panics. - verb/recipe/persona/family constructors (bounds-checked), AtomId::NULL. - const-asserted layout (ATOM_COUNT==226, RESERVED_BASE==227, 29 reserved). - RESERVE-DON'T-RECLAIM: offsets permanent (consumers store bare u8), append only into the 29 reserved slots. Distinct from the LOCKED 33-dim atoms.rs ThinkingStyleVector basis: that is the style-VECTOR lane basis; this is the FROZEN lane's VALUE catalogue. The composition (144 verb ∥ 34 recipe ∥ 36 persona ∥ 12 family; 30 reserved) is the triangle plan §1 spec; all four source counts verified against the real catalogues before freezing offsets. 6 tests; clippy clean. Board: LATEST_STATE Contract Inventory updated (same commit, hygiene rule). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Awg6TXocHcwTtc6eGsHcdD --- .claude/board/LATEST_STATE.md | 8 + .../src/cognitive_palette.rs | 301 ++++++++++++++++++ crates/lance-graph-contract/src/lib.rs | 1 + 3 files changed, 310 insertions(+) create mode 100644 crates/lance-graph-contract/src/cognitive_palette.rs 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/lance-graph-contract/src/cognitive_palette.rs b/crates/lance-graph-contract/src/cognitive_palette.rs new file mode 100644 index 000000000..5683aa732 --- /dev/null +++ b/crates/lance-graph-contract/src/cognitive_palette.rs @@ -0,0 +1,301 @@ +//! `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 **other** reading — the LEARNED / EXPLORE orchestration lane — is the +//! `6×(8:8)` register (le-contract §3 L1/L4, replayable per `E-H268-REPLAYABLE-TILE-1`); +//! that reading 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"). +//! +//! # 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; From d05088afe3670f8ef3c67e41334f0b506a354295 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 18 Jul 2026 20:37:38 +0000 Subject: [PATCH 5/6] =?UTF-8?q?feat(contract):=206=C3=97(8:8)=20orchestrat?= =?UTF-8?q?ion=20reading=20of=20the=20triangle=20register=20(P4=20Brick=20?= =?UTF-8?q?4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completes the operator's "one register, two readings" ruling (2026-07-18): "226 ARE the frozen; anything else needs 6x2x8bit (12 slots for an Orchestration for v3 substrate replayability)." The 12×u8 FROZEN reading (style_lane_at → 226-atom palette256, shipped) is the frozen half; this is the "anything else" half. MailboxSoaView::style_rails_at(row, lane) -> Option<[(u8,u8); 6]> reads the SAME 12-byte content-blind register as the six (u8:u8) rails (le-contract §3, 12 = 6×2): the V3-replayable orchestration reading (L1 part_of:is_a / L4 palette256², E-H268-REPLAYABLE-TILE-1). rail k = (lane[2k], lane[2k+1]). One register, two ClassView-selected readings — identical bytes, different interpretation. Default composes style_lane_at, so any owner materializing the lane (MailboxSoa) gets it for free; None when unmaterialized. +2 tests (rail carving on known bytes + deferred-None). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Awg6TXocHcwTtc6eGsHcdD --- crates/lance-graph-contract/src/soa_view.rs | 48 +++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/crates/lance-graph-contract/src/soa_view.rs b/crates/lance-graph-contract/src/soa_view.rs index 77355bea6..186fa0bbf 100644 --- a/crates/lance-graph-contract/src/soa_view.rs +++ b/crates/lance-graph-contract/src/soa_view.rs @@ -217,6 +217,33 @@ pub trait MailboxSoaView { 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 "anything + /// else" reading (operator ruling 2026-07-18): where + /// [`style_lane_at`](MailboxSoaView::style_lane_at) reads the `12×u8` FROZEN + /// atoms (indices into the 226-atom [`cognitive_palette`](crate::cognitive_palette)), + /// this reads the SAME bytes as 6 two-byte rails for **V3-replayable + /// orchestration** (le-contract §3 L1 `part_of:is_a` / L4 `palette256²`, + /// `E-H268-REPLAYABLE-TILE-1`). One register, two ClassView-selected readings — + /// the byte storage is identical; only the interpretation differs. + /// + /// 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. @@ -493,5 +520,26 @@ mod tests { 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); } } From 3248bd917d5bf1a321f522fa5bc74d2dfa11ce12 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 18 Jul 2026 20:44:47 +0000 Subject: [PATCH 6/6] =?UTF-8?q?docs(P4):=20one=20canonical=20triangle=20re?= =?UTF-8?q?presentation=20=E2=80=94=20reading=20is=20per-row/class,=20not?= =?UTF-8?q?=20per-lane=20(CodeRabbit=20#729=20Major)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeRabbit flagged a real coherence gap: the palette doc implied Learned/ Explore are 6x(8:8) orchestration registers while the view/storage exposed them as 12 palette atoms, so promote_family could reinterpret an orchestration byte as a Frozen palette atom ID. Resolution (forced by the le-contract §3 content-blind-register rule): the reading is ClassView-selected PER ROW/CLASS, never per lane. A policy/thinking row reads all three lanes as 12xu8 palette atoms; an orchestration row reads all three as 6x(8:8) rails. Frozen/Learned/Explore therefore always share one representation within a row, so promote_family's byte copy is representation-preserving by construction (AtomId->AtomId, or rail->rail, never across). The 6x(8:8) is the ALTERNATE reading of the same register, not a per-lane property of Learned/Explore. Doc-only (storage stays uniform [[u8;12];N], promotion stays a byte copy): - cognitive_palette.rs: reading is per-row/class; all three lanes palette in a policy row; promotion coherent. - soa_view.rs style_rails_at: the alternate reading, ClassView-selected per row-class; exposes rails mechanically, whether a row uses it is the ClassView's call. - mailbox_soa.rs: column block + promote_family doc — representation-preserving by construction; write ops are byte-level and representation-agnostic. Tests unchanged and green (contract 6/6 palette, shader triangle 1/1). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Awg6TXocHcwTtc6eGsHcdD --- .../src/mailbox_soa.rs | 20 +++++++++++++++--- .../src/cognitive_palette.rs | 18 +++++++++++----- crates/lance-graph-contract/src/soa_view.rs | 21 ++++++++++++------- 3 files changed, 43 insertions(+), 16 deletions(-) diff --git a/crates/cognitive-shader-driver/src/mailbox_soa.rs b/crates/cognitive-shader-driver/src/mailbox_soa.rs index 03e3b728d..b31761f37 100644 --- a/crates/cognitive-shader-driver/src/mailbox_soa.rs +++ b/crates/cognitive-shader-driver/src/mailbox_soa.rs @@ -157,9 +157,14 @@ pub struct MailboxSoA { // `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`. Index `f` = - // `StyleFamily` ordinal 0..11; value = a palette256 atom (atom 0 = null - // default, so a zeroed lane reads all-null, never a wrong policy). + // 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], @@ -810,6 +815,15 @@ impl MailboxSoA { /// 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 { diff --git a/crates/lance-graph-contract/src/cognitive_palette.rs b/crates/lance-graph-contract/src/cognitive_palette.rs index 5683aa732..90074da63 100644 --- a/crates/lance-graph-contract/src/cognitive_palette.rs +++ b/crates/lance-graph-contract/src/cognitive_palette.rs @@ -13,11 +13,19 @@ //! (a [`StyleFamily`](crate::style_family::StyleFamily) ordinal) holds ONE //! **`AtomId`** — a palette256 index into this 226-atom catalogue. //! -//! The **other** reading — the LEARNED / EXPLORE orchestration lane — is the -//! `6×(8:8)` register (le-contract §3 L1/L4, replayable per `E-H268-REPLAYABLE-TILE-1`); -//! that reading 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 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 //! diff --git a/crates/lance-graph-contract/src/soa_view.rs b/crates/lance-graph-contract/src/soa_view.rs index 186fa0bbf..05f6adea4 100644 --- a/crates/lance-graph-contract/src/soa_view.rs +++ b/crates/lance-graph-contract/src/soa_view.rs @@ -219,14 +219,19 @@ pub trait MailboxSoaView { /// `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 "anything - /// else" reading (operator ruling 2026-07-18): where - /// [`style_lane_at`](MailboxSoaView::style_lane_at) reads the `12×u8` FROZEN - /// atoms (indices into the 226-atom [`cognitive_palette`](crate::cognitive_palette)), - /// this reads the SAME bytes as 6 two-byte rails for **V3-replayable - /// orchestration** (le-contract §3 L1 `part_of:is_a` / L4 `palette256²`, - /// `E-H268-REPLAYABLE-TILE-1`). One register, two ClassView-selected readings — - /// the byte storage is identical; only the interpretation differs. + /// (`.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