Skip to content

Commit e6e2b3e

Browse files
committed
feat(graph): Backend::MailboxSoa + the classid-route node-match half (Inc 0 first slice)
cypher-kanban-ast-unification-v1 Inc 0, the verified-safe half: the substrate IS the graph (E-GUID-IS-THE-GRAPH), so MATCH (n:Label) is a classid prefix-route over the zero-dep MailboxSoaView contract, resolved off the class column with zero value decode. - graph_router::Backend gains the MailboxSoa variant (the named router gap). - graph/mailbox_scan.rs: match_nodes_by_class (classid route; reads only the class column) + match_node_by_local_key (local_key->row via row_for_local_key, None-fallback to positional address). - Gates: parity (matched set == reference classid filter); F2 zero-value-decode proven structurally by a GuardedSoa whose energy()/meta_raw() panic on access; key-index point lookup. 4/4 green, no new clippy warnings. Edge-traversal ((a)-[r]->(b)) deliberately deferred, grounded not faked: CausalEdge64 (the edges_raw column) is an SPO triple, NOT a row->row adjacency pointer, and the View exposes no EdgeBlock adjacency accessor. That is the edge-representation boundary the 5+3 council said to pin first (verdict 4b); it lands as the next slice once the classid-resolved edge rep + adjacency accessor are added. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CcpLeEC3XK8Eye53GKBVvi
1 parent faca377 commit e6e2b3e

3 files changed

Lines changed: 211 additions & 0 deletions

File tree

crates/lance-graph/src/graph/graph_router.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,15 @@ pub enum Backend {
5050
Blasgraph,
5151
/// Palette-accelerated traversal (Entry 2: bgz17 hot path).
5252
Palette,
53+
/// MailboxSoA traversal (Entry 3, `cypher-kanban-ast-unification-v1` Inc 0):
54+
/// a Cypher `MATCH` routed over the canonical GUID-keyed substrate via the
55+
/// zero-dep `MailboxSoaView` contract — classid prefix-route for node match,
56+
/// `local_key`→row for point lookup. Edge-slot traversal is deferred until the
57+
/// edge-representation boundary is pinned (classid-resolved `EdgeBlock`
58+
/// adjacency vs `CausalEdge64` SPO — they are NOT interchangeable; see
59+
/// `graph::mailbox_scan` and the plan's verdict §4b). See
60+
/// [`crate::graph::mailbox_scan`].
61+
MailboxSoa,
5362
}
5463

5564
/// Classification of a query for routing purposes.
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright The Lance Authors
3+
4+
//! MailboxSoA scan — `Backend::MailboxSoa` (`cypher-kanban-ast-unification-v1` Inc 0).
5+
//!
6+
//! A Cypher `MATCH` routed over the canonical GUID-keyed substrate via the
7+
//! zero-dep [`MailboxSoaView`] contract, instead of the index-built
8+
//! [`TypedGraph`](crate::graph::blasgraph::typed_graph::TypedGraph) the other
9+
//! backends use. The thesis (`E-GUID-IS-THE-GRAPH`): the substrate **is** the
10+
//! graph — a node is its GUID key, and `MATCH (n:Label)` is a **classid
11+
//! prefix-route**, resolved off the key/class column with **zero value decode**
12+
//! (it never touches the 480 B value slab: `energy` / `meta` / fingerprints).
13+
//!
14+
//! ## Scope of this increment (the verified-safe half)
15+
//!
16+
//! This lands the **node-match** half — `MATCH (n:Label)` → the set of rows whose
17+
//! class discriminator equals the queried class. That is the half that is correct
18+
//! *without* the boundary the 5+3 council said to pin first.
19+
//!
20+
//! The **edge-traversal** half (`(a)-[r]->(b)`) is deliberately **deferred**, for
21+
//! two grounded reasons (verdict §4b):
22+
//!
23+
//! 1. **Edge-representation is not yet pinned.** `EdgeBlock` (12+4 one-byte
24+
//! *adjacency* slots → neighbor `local_key`) and `CausalEdge64` (an **SPO
25+
//! triple** of s/p/o palette indices, the `edges_raw` column) are NOT
26+
//! interchangeable. A relationship-type must bind to one via the class's
27+
//! `EdgeCodecFlavor` — the router must not guess by availability.
28+
//! 2. **The View exposes only `edges_raw` (`CausalEdge64`/SPO), not `EdgeBlock`
29+
//! adjacency.** `CausalEdge64` carries s/p/o palette indices, not a row→row
30+
//! pointer, so it cannot be dereferenced to a neighbor row without the
31+
//! adjacency accessor (a follow-on contract addition).
32+
//!
33+
//! So this module does the classid prefix-route and the `local_key`→row point
34+
//! lookup (via [`MailboxSoaView::row_for_local_key`]); edge dispatch lands once
35+
//! the representation boundary is resolved.
36+
37+
use lance_graph_contract::soa_view::MailboxSoaView;
38+
39+
use crate::graph::graph_router::Backend;
40+
41+
/// A node matched by a MailboxSoA scan: the row index plus the backend tag.
42+
///
43+
/// Distinct from `GraphHit` (an *edge* with source/target) — a node match has no
44+
/// target until the edge-traversal half lands. Kept minimal and honest.
45+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46+
pub struct NodeMatch {
47+
/// The matched row index within the mailbox.
48+
pub row: usize,
49+
/// Always [`Backend::MailboxSoa`] — names the route that produced this match.
50+
pub backend: Backend,
51+
}
52+
53+
/// `MATCH (n:Label)` → the rows whose class discriminator equals `class_id`.
54+
///
55+
/// The classid prefix-route over the `MailboxSoaView`. **Zero value decode by
56+
/// construction:** the only column read is `class_id()` (which aliases the
57+
/// `entity_type` u16 slot — the Cognitive-RISC N1 class hook); the 480 B value
58+
/// slab (`energy` / `meta` / fingerprints) is never touched. This is the
59+
/// substrate-is-the-graph node-selection half of `Backend::MailboxSoa`.
60+
pub fn match_nodes_by_class<V: MailboxSoaView>(view: &V, class_id: u16) -> Vec<NodeMatch> {
61+
let classes = view.class_id();
62+
classes
63+
.iter()
64+
.enumerate()
65+
.filter_map(|(row, &c)| {
66+
(c == class_id).then_some(NodeMatch {
67+
row,
68+
backend: Backend::MailboxSoa,
69+
})
70+
})
71+
.collect()
72+
}
73+
74+
/// Point lookup: resolve a canonical [`NodeGuid::local_key`] to a single row,
75+
/// the GUID-keyed address half of `Backend::MailboxSoa`.
76+
///
77+
/// [`NodeGuid::local_key`]: lance_graph_contract::canonical_node::NodeGuid::local_key
78+
///
79+
/// Returns `None` when the view has not materialized a key index (the
80+
/// deferred-binding default of [`MailboxSoaView::row_for_local_key`]) — the
81+
/// caller then falls back to the positional `(mailbox_id, row)` address, never a
82+
/// wrong row.
83+
pub fn match_node_by_local_key<V: MailboxSoaView>(view: &V, local_key: u64) -> Option<NodeMatch> {
84+
view.row_for_local_key(local_key).map(|row| NodeMatch {
85+
row,
86+
backend: Backend::MailboxSoa,
87+
})
88+
}
89+
90+
#[cfg(test)]
91+
mod tests {
92+
use super::*;
93+
use lance_graph_contract::kanban::KanbanColumn;
94+
95+
/// A minimal view over fixed columns. The value-side columns
96+
/// (`energy`/`meta`/fingerprints) PANIC on access so the zero-value-decode
97+
/// gate (F2) is proven structurally: if the scan ever touches them, the test
98+
/// fails loudly.
99+
struct GuardedSoa {
100+
class_ids: Vec<u16>,
101+
edges: Vec<u64>,
102+
keyed_rows: Vec<(u64, usize)>,
103+
}
104+
105+
impl MailboxSoaView for GuardedSoa {
106+
fn mailbox_id(&self) -> u32 {
107+
0
108+
}
109+
fn n_rows(&self) -> usize {
110+
self.class_ids.len()
111+
}
112+
fn w_slot(&self) -> u8 {
113+
0
114+
}
115+
fn current_cycle(&self) -> u32 {
116+
0
117+
}
118+
fn phase(&self) -> KanbanColumn {
119+
KanbanColumn::Planning
120+
}
121+
// ── value slab — must NEVER be touched by a classid route (F2 guard) ──
122+
fn energy(&self) -> &[f32] {
123+
panic!("F2 violated: classid node-match touched the energy value column");
124+
}
125+
fn edges_raw(&self) -> &[u64] {
126+
// edges are key/causal side, not value slab — allowed, but the
127+
// node-match half does not use them; returned for trait completeness.
128+
&self.edges
129+
}
130+
fn meta_raw(&self) -> &[u32] {
131+
// meta is value-slab adjacent; the node-match must not read it.
132+
panic!("F2 violated: classid node-match touched the meta value column");
133+
}
134+
fn entity_type(&self) -> &[u16] {
135+
// class_id() aliases entity_type — this IS the class hook, allowed.
136+
&self.class_ids
137+
}
138+
fn row_for_local_key(&self, local_key: u64) -> Option<usize> {
139+
self.keyed_rows
140+
.iter()
141+
.find(|(k, _)| *k == local_key)
142+
.map(|(_, r)| *r)
143+
}
144+
}
145+
146+
fn sample() -> GuardedSoa {
147+
GuardedSoa {
148+
// rows: 0=A(7) 1=B(9) 2=C(7) 3=D(9) 4=E(7)
149+
class_ids: vec![7, 9, 7, 9, 7],
150+
edges: vec![0; 5],
151+
keyed_rows: vec![(0xABCD, 3), (0x1234, 0)],
152+
}
153+
}
154+
155+
#[test]
156+
fn match_nodes_by_class_routes_on_classid_only() {
157+
let soa = sample();
158+
let hits = match_nodes_by_class(&soa, 7);
159+
let rows: Vec<usize> = hits.iter().map(|h| h.row).collect();
160+
assert_eq!(rows, vec![0, 2, 4], "all class-7 rows, in order");
161+
assert!(hits.iter().all(|h| h.backend == Backend::MailboxSoa));
162+
// parity: the matched set equals the reference classid filter.
163+
let reference: Vec<usize> = soa
164+
.class_ids
165+
.iter()
166+
.enumerate()
167+
.filter(|(_, &c)| c == 7)
168+
.map(|(i, _)| i)
169+
.collect();
170+
assert_eq!(rows, reference);
171+
}
172+
173+
#[test]
174+
fn match_nodes_by_class_empty_when_no_match() {
175+
let soa = sample();
176+
assert!(match_nodes_by_class(&soa, 42).is_empty());
177+
}
178+
179+
#[test]
180+
fn match_node_by_local_key_resolves_via_key_index() {
181+
let soa = sample();
182+
assert_eq!(
183+
match_node_by_local_key(&soa, 0xABCD),
184+
Some(NodeMatch {
185+
row: 3,
186+
backend: Backend::MailboxSoa
187+
})
188+
);
189+
// unknown key → None (caller falls back to positional address).
190+
assert_eq!(match_node_by_local_key(&soa, 0xDEAD), None);
191+
}
192+
193+
#[test]
194+
fn f2_zero_value_decode_the_scan_never_panics_on_value_columns() {
195+
// The GuardedSoa panics if energy()/meta_raw() are read. If this test
196+
// completes, the classid node-match touched ONLY the class column.
197+
let soa = sample();
198+
let _ = match_nodes_by_class(&soa, 7);
199+
let _ = match_node_by_local_key(&soa, 0x1234);
200+
}
201+
}

crates/lance-graph/src/graph/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ pub mod blasgraph;
1313
pub mod fingerprint;
1414
pub mod graph_router;
1515
pub mod hydrate;
16+
pub mod mailbox_scan;
1617
pub mod metadata;
1718
pub mod neighborhood;
1819
pub mod neuron;

0 commit comments

Comments
 (0)