Skip to content

Commit fbeef18

Browse files
committed
feat(graph+contract): EdgeBlock typed-edge facet — CoarseOnly 12-family/4-external slot decode (Inc 0)
The third dispatch-table facet: explicit typed edges (a)-[r]->... under the classid-resolved EdgeCodecFlavor (E-ADJACENCY-IS-KEY-AND-EDGECODEC). EdgeBlock is bytes 16..32 (the edge region), NOT the value slab, so still zero value decode. - MailboxSoaView::edge_block_at(row) -> Option<EdgeBlock> (contract, deferred default None; the canon NodeRow carries edges(16), the override exposes it). - graph::mailbox_scan::{EdgeNeighbors, edge_slots_coarse}: under CoarseOnly, decode the 12 in-family + 4 external slots to their populated (non-zero) refs, family vs external. Pq32x4 (turbovec residue) / CoarseResidue are refused - they are NOT adjacency, never coerced to slots (boundary 4b: classid-resolved, not query-guessed). - Slot-byte -> neighbor-row resolution is deliberately deferred (the basin-local- index convention + zero-collision is the next encoding decision, analogous to local_key->row); this facet lands the structure (which slots are edges, family vs external, under which flavor), never fakes the row resolution. - Tests: populated decode ([2,5] family + [1] external), all-zero = no edges, no-block = None, non-Coarse flavors refused. F2 zero-value-decode extended. 9/9 mailbox_scan, clippy clean (sort_by_key + Reverse). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CcpLeEC3XK8Eye53GKBVvi
1 parent 2f1ad2e commit fbeef18

2 files changed

Lines changed: 105 additions & 1 deletion

File tree

crates/lance-graph-contract/src/soa_view.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,25 @@ pub trait MailboxSoaView {
106106
None
107107
}
108108

109+
/// The 16-byte [`EdgeBlock`](crate::canonical_node::EdgeBlock) of `row` — the
110+
/// node's **explicit typed edges** (12 in-family + 4 out-of-family one-byte
111+
/// slots), bytes 16..32 of the canonical `NodeRow`. This is the edge region,
112+
/// **NOT the value slab** (32..512), so reading it is **zero value decode**.
113+
///
114+
/// How the 16 bytes are *interpreted* is the class's
115+
/// [`EdgeCodecFlavor`](crate::canonical_node::EdgeCodecFlavor)
116+
/// (`CoarseOnly` = 12-family/4-external adjacency, `Pq32x4` = 32×4 turbovec
117+
/// residue) — resolved `classid → ClassView`, never guessed by the query
118+
/// (`E-ADJACENCY-IS-KEY-AND-EDGECODEC`).
119+
///
120+
/// **Default = `None` (zero-fallback, deferred binding)** — a view that has not
121+
/// materialized the edge region returns `None`; an owner that carries the
122+
/// canonical `NodeRow` (which holds `edges(16)`) overrides this.
123+
#[inline]
124+
fn edge_block_at(&self, _row: usize) -> Option<crate::canonical_node::EdgeBlock> {
125+
None
126+
}
127+
109128
// NOTE (follow-up): the qualia column (`QualiaI4_16D`) accessor is intentionally omitted —
110129
// add `fn qualia(&self) -> &[crate::qualia::QualiaI4_16D]` when the first consumer
111130
// (planner strategy selection) needs it; keep the read surface minimal until then.

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

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
//! lookup (via [`MailboxSoaView::row_for_local_key`]); edge dispatch lands once
3535
//! the representation boundary is resolved.
3636
37+
use lance_graph_contract::canonical_node::EdgeCodecFlavor;
3738
use lance_graph_contract::hhtl::NiblePath;
3839
use lance_graph_contract::soa_view::MailboxSoaView;
3940

@@ -132,14 +133,60 @@ pub fn cakes_nearest<V: MailboxSoaView>(
132133
})
133134
.collect();
134135
// Descending by shared depth; stable sort preserves ascending row order on ties.
135-
scored.sort_by(|a, b| b.1.cmp(&a.1));
136+
scored.sort_by_key(|&(_, depth)| core::cmp::Reverse(depth));
136137
scored.truncate(k);
137138
scored
138139
}
139140

141+
/// The explicit typed edges of a node under the `CoarseOnly` flavor: the
142+
/// **populated** (non-zero) slot references, split family-internal vs external.
143+
///
144+
/// Canon: an `EdgeBlock` slot is "zeroed when unused", so a zero byte is an empty
145+
/// slot and a non-zero byte is a basin-local edge reference. The 12 `in_family`
146+
/// slots are intra-basin edges; the 4 `external` slots are cross-family interface
147+
/// references (`E-ADJACENCY-IS-KEY-AND-EDGECODEC`). The refs are returned raw —
148+
/// resolving a ref → neighbor row needs the basin-local-index→row convention
149+
/// (the next encoding decision, analogous to `row_for_local_key`); this facet
150+
/// lands the *structure* (which slots are edges, family vs external), not the
151+
/// row-resolution, and never fakes it.
152+
#[derive(Debug, Clone, PartialEq, Eq, Default)]
153+
pub struct EdgeNeighbors {
154+
/// Populated in-family (intra-basin) edge slot references (non-zero bytes).
155+
pub in_family: Vec<u8>,
156+
/// Populated out-of-family (cross-basin interface) edge slot references.
157+
pub external: Vec<u8>,
158+
}
159+
160+
/// Decode a node's explicit typed edges under the **`CoarseOnly`** flavor —
161+
/// `(a)-[r]->…` as the populated 12-family/4-external slot references.
162+
///
163+
/// Returns `None` when the view has no edge block for `row`
164+
/// ([`MailboxSoaView::edge_block_at`] default), or when the class's `flavor` is
165+
/// not `CoarseOnly` (the adjacency reading) — `Pq32x4` is **turbovec residue**,
166+
/// not adjacency, and `CoarseResidue` is coarse+residue; both are a different
167+
/// read handled elsewhere, never coerced into slot adjacency
168+
/// (`E-ADJACENCY-IS-KEY-AND-EDGECODEC` boundary §4b: classid-resolved, not
169+
/// query-guessed). **Zero value decode** — the `EdgeBlock` is bytes 16..32, the
170+
/// edge region, never the 480 B value slab.
171+
pub fn edge_slots_coarse<V: MailboxSoaView>(
172+
view: &V,
173+
row: usize,
174+
flavor: EdgeCodecFlavor,
175+
) -> Option<EdgeNeighbors> {
176+
if flavor != EdgeCodecFlavor::CoarseOnly {
177+
return None;
178+
}
179+
let block = view.edge_block_at(row)?;
180+
Some(EdgeNeighbors {
181+
in_family: block.in_family.iter().copied().filter(|&b| b != 0).collect(),
182+
external: block.out_family.iter().copied().filter(|&b| b != 0).collect(),
183+
})
184+
}
185+
140186
#[cfg(test)]
141187
mod tests {
142188
use super::*;
189+
use lance_graph_contract::canonical_node::EdgeBlock;
143190
use lance_graph_contract::kanban::KanbanColumn;
144191

145192
/// A minimal view over fixed columns. The value-side columns
@@ -151,6 +198,7 @@ mod tests {
151198
edges: Vec<u64>,
152199
keyed_rows: Vec<(u64, usize)>,
153200
paths: Vec<Option<NiblePath>>,
201+
blocks: Vec<Option<EdgeBlock>>,
154202
}
155203

156204
impl MailboxSoaView for GuardedSoa {
@@ -195,6 +243,9 @@ mod tests {
195243
fn hhtl_path_at(&self, row: usize) -> Option<NiblePath> {
196244
self.paths.get(row).copied().flatten()
197245
}
246+
fn edge_block_at(&self, row: usize) -> Option<EdgeBlock> {
247+
self.blocks.get(row).copied().flatten()
248+
}
198249
}
199250

200251
fn sample() -> GuardedSoa {
@@ -212,6 +263,17 @@ mod tests {
212263
Some(NiblePath::root(1).child(5)),
213264
Some(NiblePath::root(9)),
214265
],
266+
// row0 has in-family edges to refs 2,5 and one external ref 1; rest empty.
267+
blocks: vec![
268+
Some(EdgeBlock {
269+
in_family: [2, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
270+
out_family: [1, 0, 0, 0],
271+
}),
272+
Some(EdgeBlock::default()),
273+
None,
274+
None,
275+
None,
276+
],
215277
}
216278
}
217279

@@ -263,6 +325,29 @@ mod tests {
263325
let _ = match_node_by_local_key(&soa, 0x1234);
264326
let _ = clam_contained(&soa, NiblePath::root(1).child(2));
265327
let _ = cakes_nearest(&soa, NiblePath::root(1).child(2).child(3), 3);
328+
let _ = edge_slots_coarse(&soa, 0, EdgeCodecFlavor::CoarseOnly);
329+
}
330+
331+
#[test]
332+
fn edge_slots_coarse_decodes_populated_family_and_external() {
333+
let soa = sample();
334+
let n = edge_slots_coarse(&soa, 0, EdgeCodecFlavor::CoarseOnly).unwrap();
335+
assert_eq!(n.in_family, vec![2, 5], "non-zero in-family slots only");
336+
assert_eq!(n.external, vec![1], "non-zero external slot");
337+
// an all-zero block ⇒ no edges (zeroed = unused).
338+
let empty = edge_slots_coarse(&soa, 1, EdgeCodecFlavor::CoarseOnly).unwrap();
339+
assert!(empty.in_family.is_empty() && empty.external.is_empty());
340+
// no edge block materialized ⇒ None (deferred-binding fallback).
341+
assert!(edge_slots_coarse(&soa, 2, EdgeCodecFlavor::CoarseOnly).is_none());
342+
}
343+
344+
#[test]
345+
fn edge_slots_coarse_refuses_non_coarse_flavors() {
346+
// Pq32x4 = turbovec residue, NOT adjacency; CoarseResidue likewise.
347+
// The classid-resolved flavor gates the read — never coerced to slots.
348+
let soa = sample();
349+
assert!(edge_slots_coarse(&soa, 0, EdgeCodecFlavor::Pq32x4).is_none());
350+
assert!(edge_slots_coarse(&soa, 0, EdgeCodecFlavor::CoarseResidue).is_none());
266351
}
267352

268353
#[test]

0 commit comments

Comments
 (0)