diff --git a/crates/cognitive-shader-driver/src/mailbox_soa.rs b/crates/cognitive-shader-driver/src/mailbox_soa.rs index a5e4eccc..16aff100 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::{MailboxSoaOwner, MailboxSoaView}; +use lance_graph_contract::soa_view::{IdentityPlane, MailboxSoaOwner, MailboxSoaView}; /// Canonical named-fingerprint plane width: 256 × u64 = 16,384 bits /// (mirrors `bindspace::WORDS_PER_FP`; defined locally so the mailbox does NOT @@ -729,6 +729,23 @@ impl MailboxSoaView for MailboxSoA { fn phase(&self) -> KanbanColumn { self.phase } + /// Override the deferred-binding default: the in-RAM owner DOES carry the + /// content/topic/angle identity planes (W1b), so the value-side Hamming + /// distance (`graph::mailbox_scan::DistanceMeans::Hamming`) can compute over + /// the real view, not just the unit-test fake. Guarded by `populated` — never + /// reads a zero-padded capacity row (same logical-row discipline as + /// `n_rows()`); a query into `populated..N` returns `None`. + #[inline] + fn identity_plane_at(&self, row: usize, plane: IdentityPlane) -> Option<&[u64]> { + if row >= self.populated { + return None; + } + Some(match plane { + IdentityPlane::Content => self.content_row(row), + IdentityPlane::Topic => self.topic_row(row), + IdentityPlane::Angle => self.angle_row(row), + }) + } #[inline] fn energy(&self) -> &[f32] { &self.energy @@ -1208,6 +1225,32 @@ mod tests { assert_eq!(mb.sigma_at(2), 0, "sigma[2] must reset to 0"); } + /// `identity_plane_at` (the `MailboxSoaView` override, #545 codex P2): the real + /// owner DOES carry the W1b planes, so the value-side Hamming distance can read + /// them on the live view (not just the unit-test fake). Guarded by `populated`: + /// a row beyond the logical size returns `None`, never a zero-padded capacity row. + #[test] + fn identity_plane_at_returns_planes_and_guards_padding() { + let mut mb: MailboxSoA<4> = MailboxSoA::new(1, 0, 1.0); + mb.set_populated(2); + let mut c0 = vec![0u64; WORDS_PER_FP]; + c0[0] = 0b1011; + mb.set_content(0, &c0); + // populated row → Some, byte-identical to content_row. + assert_eq!( + mb.identity_plane_at(0, IdentityPlane::Content), + Some(mb.content_row(0)) + ); + // topic/angle planes default zero but are still materialized (Some). + assert_eq!( + mb.identity_plane_at(0, IdentityPlane::Topic), + Some(mb.topic_row(0)) + ); + // row beyond `populated` (2) → None: never a zero-padded capacity row. + assert_eq!(mb.identity_plane_at(2, IdentityPlane::Content), None); + assert_eq!(mb.identity_plane_at(3, IdentityPlane::Angle), None); + } + // ── test 15: W1b dense identity planes — parity with BindSpace ─────────── /// **The W1b "test the new" proof.** The content/topic/angle Hamming identity diff --git a/crates/lance-graph-contract/src/soa_view.rs b/crates/lance-graph-contract/src/soa_view.rs index c6d6fff3..c2c3782e 100644 --- a/crates/lance-graph-contract/src/soa_view.rs +++ b/crates/lance-graph-contract/src/soa_view.rs @@ -19,6 +19,20 @@ use crate::collapse_gate::MailboxId; use crate::kanban::{KanbanColumn, KanbanMove, RubiconTransitionError}; +/// Which dense identity plane a value-side read selects — the orthogonal +/// perspective axes of a node's content (`E-TENANT-ANGLE-RANK-IS-CAM-PQ-ADC`). +/// Each is a `WORDS_PER_FP`-u64 fingerprint in the value slab; reading one is a +/// **value decode** (the costed tier), never the zero-decode key path. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum IdentityPlane { + /// The content identity fingerprint plane. + Content, + /// The topic identity fingerprint plane. + Topic, + /// The angle (perspective) identity fingerprint plane. + Angle, +} + /// A transparent, read-only view over one mailbox's SoA columns. /// /// Implementors return **borrows** (`&[T]`) or `Copy` scalars — never clones of the @@ -125,6 +139,20 @@ pub trait MailboxSoaView { None } + /// `row`'s dense identity-plane fingerprint (`WORDS_PER_FP` u64) for the + /// selected [`IdentityPlane`] — content / topic / angle. This is the + /// **value-side** read behind the costed distance/sweep tier + /// (`E-TENANT-ANGLE-RANK-IS-CAM-PQ-ADC`): a Hamming/CAM rank "from an angle" + /// reads this plane, so unlike the key facets it is **NOT zero value decode**. + /// + /// **Default = `None` (zero-fallback, deferred binding)** — a view that has not + /// materialized the planes returns `None`; the in-RAM `MailboxSoA` owner + /// (which carries content/topic/angle planes, W1b) overrides this. + #[inline] + fn identity_plane_at(&self, _row: usize, _plane: IdentityPlane) -> Option<&[u64]> { + None + } + // 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. diff --git a/crates/lance-graph/src/graph/mailbox_scan.rs b/crates/lance-graph/src/graph/mailbox_scan.rs index 31b90859..0ae0fcb8 100644 --- a/crates/lance-graph/src/graph/mailbox_scan.rs +++ b/crates/lance-graph/src/graph/mailbox_scan.rs @@ -45,7 +45,7 @@ use lance_graph_contract::canonical_node::EdgeCodecFlavor; use lance_graph_contract::hhtl::NiblePath; -use lance_graph_contract::soa_view::MailboxSoaView; +use lance_graph_contract::soa_view::{IdentityPlane, MailboxSoaView}; use crate::graph::graph_router::Backend; @@ -226,13 +226,18 @@ pub enum DistanceMeans { /// triangle inequality). Key-only, zero value decode /// (`E-PANCAKES-IS-RADIX-IS-HHTL`). PrefixDepth, - // ── value-decode means (the costed tier — named here as the dispatch - // surface; wired on their own branch with their own cost gate, never - // mixed into the zero-decode facets; `E-TENANT-ANGLE-RANK-IS-CAM-PQ-ADC`, - // `E-HELIX-IS-EXACT-LOCATION`): ── - // Hamming(plane) — fingerprint popcount over a content/topic/angle plane; - // HelixAngular — Signed360 exact-orthogonal-location distance; - // PqAdc — CAM-PQ asymmetric distance (IVF probe + tables). + /// **Hamming over an identity plane** (`IdentityPlane`: content / topic / + /// angle) — the popcount of the XOR of the two nodes' fingerprint planes. + /// This is the **costed tier**: it reads the value-side plane + /// (`MailboxSoaView::identity_plane_at`), so — unlike `PrefixDepth` — it is + /// **NOT zero value decode** (`E-TENANT-ANGLE-RANK-IS-CAM-PQ-ADC`). The right + /// use of popcount: homogeneous 16K-bit fingerprint bits, not the + /// heterogeneous GUID key. + Hamming(IdentityPlane), + // ── further value means (named; wired as they land, same costed tier): + // HelixAngular — Signed360 exact-orthogonal-location distance + // (`E-HELIX-IS-EXACT-LOCATION`); + // PqAdc — CAM-PQ asymmetric distance (IVF probe + tables). } /// Distance between two nodes (rows, each a GUID) under the chosen `means`. @@ -258,6 +263,12 @@ pub fn node_distance( let cpd = pa.common_prefix_depth(pb); Some(u32::from((pa.depth() - cpd) + (pb.depth() - cpd))) } + // COSTED tier: reads the value-side plane (NOT zero value decode). + DistanceMeans::Hamming(plane) => { + let fa = view.identity_plane_at(a, plane)?; + let fb = view.identity_plane_at(b, plane)?; + Some(fa.iter().zip(fb).map(|(x, y)| (x ^ y).count_ones()).sum()) + } } } @@ -277,6 +288,7 @@ mod tests { keyed_rows: Vec<(u64, usize)>, paths: Vec>, blocks: Vec>, + content_planes: Vec>>, /// Logical populated rows; `None` ⇒ the full `class_ids` length. Set /// smaller to model the real `MailboxSoA` (zero-padded capacity with /// `n_rows() == populated < entity_type().len()`). @@ -328,6 +340,13 @@ mod tests { fn edge_block_at(&self, row: usize) -> Option { self.blocks.get(row).copied().flatten() } + fn identity_plane_at(&self, row: usize, plane: IdentityPlane) -> Option<&[u64]> { + // Only the Content plane is materialized in this fake. + match plane { + IdentityPlane::Content => self.content_planes.get(row).and_then(|p| p.as_deref()), + IdentityPlane::Topic | IdentityPlane::Angle => None, + } + } } fn sample() -> GuardedSoa { @@ -356,6 +375,14 @@ mod tests { None, None, ], + // content planes (2 u64 each) for the Hamming means; row2 unmaterialized. + content_planes: vec![ + Some(vec![0b1011, 0]), + Some(vec![0b1101, 0]), + None, + Some(vec![0, 0]), + Some(vec![u64::MAX, u64::MAX]), + ], logical_n: None, } } @@ -511,6 +538,36 @@ mod tests { assert!(d(0, 1).unwrap() < d(0, 4).unwrap()); } + #[test] + fn node_distance_hamming_plane_is_popcount_xor_over_the_value_plane() { + let soa = sample(); + // row0 0b1011, row1 0b1101 → XOR 0b0110 → popcount 2. + assert_eq!( + node_distance(&soa, 0, 1, DistanceMeans::Hamming(IdentityPlane::Content)), + Some(2) + ); + // self-distance = 0 (metric). + assert_eq!( + node_distance(&soa, 0, 0, DistanceMeans::Hamming(IdentityPlane::Content)), + Some(0) + ); + // all-zero vs all-ones over 2×u64 = 128 bits. + assert_eq!( + node_distance(&soa, 3, 4, DistanceMeans::Hamming(IdentityPlane::Content)), + Some(128) + ); + // unmaterialized plane (row2) ⇒ None (costed-tier fallback). + assert_eq!( + node_distance(&soa, 0, 2, DistanceMeans::Hamming(IdentityPlane::Content)), + None + ); + // a plane this fake doesn't carry ⇒ None (Topic/Angle not materialized). + assert_eq!( + node_distance(&soa, 0, 1, DistanceMeans::Hamming(IdentityPlane::Angle)), + None + ); + } + #[test] fn node_distance_none_without_materialized_path() { let mut soa = sample();