Skip to content

Commit c1a9dda

Browse files
committed
feat(symbiont): §2.4 key-only neo4j-grade render — zero value decode, falsifiable
Superpower §2.4 of unified-soa-rubikon-integration-v1: read all 16k SoA boards touching ONLY the 32-byte head (128-bit NodeGuid + 128-bit EdgeBlock), never the 480-byte value slab — a Neo4j-like graph view at memory-scan speed (E-GUID-IS-THE-GRAPH). New crates/symbiont/src/key_render.rs: - render_key_only(&[NodeRow]) -> KeyGraph reads only row.key + row.edges. - hhtl_path_of lowers the 3×4 HHT cascade (HEEL·HIP·TWIG = 12 nibbles, root-first) to a NiblePath; classid (routing prefix) + identity (leaf) are excluded from the path (tested). - run_demo renders the 16384-board SoA: 16384 nodes / 32768 edges from 512 KiB of heads, 7680 KiB of value slabs left COLD. SymbiontBoard now materialises the contract's zero-value-decode key facets edge_block_at / hhtl_path_at (the owner carries the NodeRow head, so the trait None-defaults become Some); out-of-range rows fall back to None, never a wrong row. domino::seed_board seeds a ring in-family slot + an adapter out-family slot so the render is non-trivial — the Domino AMX sweep never reads the edge region, so this is free. Zero-value-decode is a FALSIFIABLE probe, not a comment: render_ignores_ value_slab poisons every row.value with 0xFF and asserts the render is byte-identical to the zeroed-slab render — any value read would leak a 0xFF and fail. Promoted to EPIPHANY E-ZERO-DECODE-IS-FALSIFIABLE-BY-POISON (the dual of E-SCENT-IS-NOT-READING; generalises to the codec stack). cargo test --manifest-path crates/symbiont/Cargo.toml: 12/12 (4 new key_render + 1 new kanban facet test). Zero new contract types — only materialises existing key facets. Plan §2.4 + §5 step 3 marked SHIPPED; AGENT_LOG + EPIPHANIES updated (board hygiene). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CcpLeEC3XK8Eye53GKBVvi
1 parent ed6cfea commit c1a9dda

7 files changed

Lines changed: 278 additions & 5 deletions

File tree

.claude/board/AGENT_LOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
## 2026-06-20 (cont.⁵) — §2.4 key-only neo4j render green (zero value decode, falsifiable)
2+
3+
**Main thread (Opus), autoattended.** Operator picked superpower §2.4 from the post-reconciliation menu. Read the canon surface in full first (`canonical_node.rs` NodeGuid/EdgeBlock/NodeRow + ValueTenant carve; `soa_view.rs` the `hhtl_path_at`/`edge_block_at`/`identity_plane_at` deferred key facets; `hhtl.rs` NiblePath) — not a scent-skim.
4+
5+
NEW `crates/symbiont/src/key_render.rs`: `render_key_only(&[NodeRow]) -> KeyGraph` reads ONLY `row.key` (128-bit GUID) + `row.edges` (128-bit EdgeBlock), never the 480-byte value slab. `hhtl_path_of` lowers the 3×4 HHT cascade (HEEL·HIP·TWIG = 12 nibbles, root-first) to a `NiblePath`; classid = routing prefix + identity = leaf are excluded (tested). `SymbiontBoard` now OVERRIDES the contract's `edge_block_at`/`hhtl_path_at` (the owner carries the NodeRow head, so the `None` defaults become `Some`) — `main.rs` + `domino::seed_board` seed a ring in-family slot + an adapter out-family slot (the Domino AMX sweep never reads the edge region, so this is free). `cargo test --manifest-path crates/symbiont/Cargo.toml` **12/12** (4 new key_render + 1 new kanban facet test). Binary renders **16384 nodes / 32768 edges from 512 KiB of 32-byte heads, 7680 KiB of value slabs left COLD**; sample node[3] = `00000000-0000-0000-0000-000000000003` hhtl_depth=12. Incremental build 3m04s (warm 8.2G target, 14G free).
6+
7+
**Falsifiable zero-value-decode probe** (`render_ignores_value_slab`): poison every `row.value` with `0xFF` → render is byte-identical → the value region was provably untouched. Promoted to EPIPHANY `E-ZERO-DECODE-IS-FALSIFIABLE-BY-POISON` (the dual of `E-SCENT-IS-NOT-READING`; generalises to the whole codec stack). Plan §2.4 + §5 step 3 = ✅ SHIPPED. **Zero new contract types** — only materialises existing key facets. Pushed to main.
8+
19
## 2026-06-20 (cont.⁴) — D2 kanban loop (pure-SoA slice) green
210

311
**Main thread (Opus), autoattended.** Scoped via a read-only explorer (the contract `kanban`/`soa_view`/`scheduler` surface is COMPLETE), then **read the actual files** (kanban.rs/soa_view.rs/scheduler.rs — after a self-caught scent-skim the operator flagged: I'd `grep`/`sed`'d instead of reading, the exact E-SCENT-IS-NOT-READING anti-pattern; corrected by reading in full).

.claude/board/EPIPHANIES.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,18 @@
1+
## 2026-06-20 — E-ZERO-DECODE-IS-FALSIFIABLE-BY-POISON — a "we never read region X" claim (zero-value-decode key render, cold-column skip, head-only scan) is not a comment, it is a TEST: poison region X with a sentinel, run the op, assert the output is byte-identical to the un-poisoned run; if it touched X, the bytes diverge
2+
3+
**Status:** FINDING (perennial; shipped `symbiont/key_render.rs::tests::render_ignores_value_slab`, 2026-06-20).
4+
5+
The canon's whole key-value bet is that the 16-byte GUID + 16-byte EdgeBlock **prerender a node with zero value decode** (OGAR P0: "the key prerenders nodes in any way with zero value decode"). The neo4j-grade render (superpower §2.4) reads `row.key` + `row.edges` and never the 480-byte value slab — but "never" written in a doc-comment is unverified prose, exactly the kind of claim `E-SCENT-IS-NOT-READING` warns drifts. The cheap, decisive proof: **poison the region you claim not to read.** Fill every `row.value` with `0xFF`, render, assert the `KeyGraph` is byte-identical (`PartialEq`) to the render over zeroed slabs. Identical ⇒ the value region was provably untouched; any read would have leaked a `0xFF` into the output and failed the assert.
6+
7+
Why this is perennial, not a one-off test trick:
8+
1. **It generalises to the whole codec stack.** Any "this tier doesn't decompress the next" / "this scan stays in the cold column's metadata" / "this facet is key-only" claim has the same falsifier: sentinel-fill the region asserted unread, assert invariance. Lance columnar skips, CAM-PQ search-without-decompress, Scent-tier triage — all checkable this way.
9+
2. **It is the dual of `E-SCENT-IS-NOT-READING`.** That epiphany says a positional preview is not comprehension; this one says a claimed *non-*read is not proven until you make reading it observably break the result. Both convert a confidence claim into a falsifiable one.
10+
3. **It costs ~nothing.** One `clone()`, one slab memset, one `assert_eq!` — no instrumentation, no coverage tooling. The poison sentinel turns an invisible invariant into a loud failure.
11+
12+
The anti-pattern it kills: shipping `// zero value decode` as a load-bearing claim with no test, then a later refactor quietly adds a value read and nothing catches it until a 16k-board scan is silently 16× slower (or wrong). Cross-ref: `unified-soa-rubikon-integration-v1.md` §2.4; OGAR `CLAUDE.md` P0 "the key prerenders nodes with zero value decode"; `E-SCENT-IS-NOT-READING` (the dual); `E-GUID-IS-THE-GRAPH` (the claim this probe protects).
13+
14+
---
15+
116
## 2026-06-20 — E-SURREALQL-IS-A-LENS-NOT-A-SINK — because SurrealDB's kv-lance engine stores into the SAME Lance bytes the SoA writes, SurrealQL is a second NATIVE runtime over the one substrate, not an egress dialect we export to; "ship it to SurrealDB" is a category error the way "ship it to a different view of the same table" is
217

318
**Status:** FINDING (perennial; grounded in `surrealdb/core/src/kvs/lance/` — the fork, verified 2026-06-20).

.claude/plans/unified-soa-rubikon-integration-v1.md

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ No copies, no per-subsystem mirror (R1 "one SoA never transformed").
4949
-**ractor = ownership guarantee, not a message bus** (E-CE64-MB-4 / #477):
5050
`SymbiontBoard`'s single `&mut self` owner IS the mailbox-as-owner compile-time
5151
proof. No tokio, no messages — a structural/dummy wrapper.
52+
-**Key-only neo4j render (superpower §2.4)**`symbiont/key_render.rs` reads
53+
16384 boards touching ONLY the 32-byte head (128-bit GUID + 128-bit EdgeBlock):
54+
16384 nodes / 32768 edges from 512 KiB of heads, 7680 KiB of value slabs COLD;
55+
zero-value-decode proven by the `0xFF`-poison falsifiable probe. `SymbiontBoard`
56+
now materialises the contract's `edge_block_at`/`hhtl_path_at` key facets.
5257

5358
---
5459

@@ -72,12 +77,20 @@ The SoA is column-major (`MailboxSoaView`: `energy() -> &[f32]`,
7277
`CausalEdge64` (`edges_raw()` raw u64 → `CausalEdge64(raw)`), qualia, plasticity
7378
— superposed/reduced over the SoA the same way as (1). The tenant catalogue
7479
(`VALUE_TENANTS`) is the column set; the projection is generic.
75-
4. **Key-only neo4j-grade render — ZERO value decode.** Read all 16k boards
80+
4. **Key-only neo4j-grade render — ZERO value decode.** Read all 16k boards
7681
touching ONLY the 32-byte head: the 128-bit `NodeGuid` (node) + the 128-bit
7782
`EdgeBlock` (12 in-family + 4 inherited out-of-family edges). `key(16)+edges(16)`,
7883
never the 480-byte value slab — a Neo4j-like graph view at memory-scan speed
7984
(`hhtl_path_at`/`edge_block_at` accessors are declared on `MailboxSoaView` for
8085
exactly this, defaulting to `None` until the owner materialises the head).
86+
**SHIPPED** (`symbiont/key_render.rs` + `SymbiontBoard` overrides of
87+
`edge_block_at`/`hhtl_path_at`): `render_key_only(&[NodeRow])` reads only
88+
`row.key` + `row.edges`; the binary renders **16384 nodes / 32768 edges from
89+
512 KiB of heads, 7680 KiB of value slabs left COLD**. Zero-value-decode is a
90+
FALSIFIABLE probe (`render_ignores_value_slab`): poison every value slab with
91+
`0xFF` → render is byte-identical. `hhtl_path_of` lowers the 3×4 HHT cascade
92+
(HEEL·HIP·TWIG = 12 nibbles) to a `NiblePath`; classid/identity excluded
93+
(tested). 12 symbiont tests green.
8194

8295
---
8396

@@ -136,8 +149,8 @@ to drive the SoA.
136149
1. ☐ Planner reads the symbiont SoA (a `MailboxSoaView`) and runs a real query.
137150
2. ☐ Superpower §2.1 — tenant→fingerprint meta-query (one tenant, 16k rows → one
138151
fingerprint; cosine/CAM over the standing wave).
139-
3. Superpower §2.4 — key-only 32-byte render (materialise `hhtl_path_at` /
140-
`edge_block_at`; assert zero value-slab reads).
152+
3. Superpower §2.4 — key-only 32-byte render (materialise `hhtl_path_at` /
153+
`edge_block_at`; assert zero value-slab reads). **SHIPPED** — see §2.4.
141154
4. ☐ Superpower §2.2/§2.3 — `temporal` Markov chaining + project
142155
witness/CausalEdge64.
143156
5. ☐ Rubikon §3 — the −200 ms Libet-veto anchor on `Planning → Prune`.

crates/symbiont/src/domino.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,10 +84,20 @@ pub fn energy_of(row: &NodeRow) -> f32 {
8484
}
8585

8686
/// Seed a board: a deterministic Morton-addressed 4×4 BF16 tile in `Fingerprint`.
87+
///
88+
/// The 32-byte head is seeded too so the key-only graph render (§2.4,
89+
/// `key_render`) is non-trivial: `key` = bootstrap address (identity = idx),
90+
/// `edges` = one in-family ring slot + one out-of-family adapter slot, both
91+
/// one-byte basin-local indices per the canonical `EdgeBlock`. The Domino sweep
92+
/// NEVER reads the edge region (it touches only the `Fingerprint`/`Energy` value
93+
/// tenants), so seeding edges here is free for the AMX path.
8794
fn seed_board(idx: usize) -> NodeRow {
95+
let mut edges = EdgeBlock::default();
96+
edges.in_family[0] = ((idx % 255) + 1) as u8; // ring neighbour (always 1..=255)
97+
edges.out_family[0] = (1 + (idx % 4)) as u8; // inherited-adapter slot (1..=4)
8898
let mut row = NodeRow {
8999
key: NodeGuid::local(idx as u32),
90-
edges: EdgeBlock::default(),
100+
edges,
91101
value: [0u8; 480],
92102
};
93103
let mut f = [0.0f32; LANES];

crates/symbiont/src/kanban_loop.rs

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,14 @@
4444
//! shipped + 5 `#[tokio::test]`s) — for consumers that aren't the writer. The one
4545
//! remaining stub is the SurrealQL re-read (`surreal_container::view::read_via_kv_lance`).
4646
47-
use lance_graph_contract::canonical_node::NodeRow;
47+
use lance_graph_contract::canonical_node::{EdgeBlock, NodeRow};
48+
use lance_graph_contract::hhtl::NiblePath;
4849
use lance_graph_contract::kanban::{ExecTarget, KanbanColumn, KanbanMove};
4950
use lance_graph_contract::scheduler::{DatasetVersion, NextPhaseScheduler, VersionScheduler};
5051
use lance_graph_contract::soa_view::{MailboxSoaOwner, MailboxSoaView};
5152

5253
use crate::domino;
54+
use crate::key_render;
5355

5456
/// A mailbox-as-owner over symbiont's flat `Vec<NodeRow>` board-set. The SoA
5557
/// columns are kept parallel to the rows so the trait's zero-copy `&[T]` borrows
@@ -160,6 +162,18 @@ impl MailboxSoaView for SymbiontBoard {
160162
fn entity_type(&self) -> &[u16] {
161163
&self.entity
162164
}
165+
166+
// ── §2.4 key facets: the owner carries the canonical `NodeRow`, so it
167+
// materialises the zero-value-decode head accessors the trait defaults to
168+
// `None`. Both read ONLY the 32-byte head (key + edges), never the value slab.
169+
170+
fn edge_block_at(&self, row: usize) -> Option<EdgeBlock> {
171+
self.rows.get(row).map(|r| r.edges)
172+
}
173+
174+
fn hhtl_path_at(&self, row: usize) -> Option<NiblePath> {
175+
self.rows.get(row).map(|r| key_render::hhtl_path_of(&r.key))
176+
}
163177
}
164178

165179
impl MailboxSoaOwner for SymbiontBoard {
@@ -242,4 +256,24 @@ mod tests {
242256
assert!(board.try_advance_phase(KanbanColumn::Evaluation).is_err());
243257
assert_eq!(board.phase(), KanbanColumn::Planning);
244258
}
259+
260+
#[test]
261+
fn key_facets_are_materialized_not_the_none_default() {
262+
// §2.4: the owner carries the NodeRow head, so edge_block_at /
263+
// hhtl_path_at now return Some (the contract default is None until an
264+
// owner materialises them). Both are zero-value-decode key facets.
265+
let board = SymbiontBoard::spawn(16, 9);
266+
let eb = board
267+
.edge_block_at(3)
268+
.expect("owner materialises the edge block");
269+
assert_eq!(eb.in_family[0], 4); // ring edge (3 % 255) + 1
270+
assert_eq!(eb.out_family[0], 4); // adapter slot 1 + (3 % 4)
271+
assert!(
272+
board.hhtl_path_at(3).is_some(),
273+
"owner materialises the HHTL path"
274+
);
275+
// Out-of-range row falls back to the None default, never a wrong row.
276+
assert_eq!(board.edge_block_at(999), None);
277+
assert_eq!(board.hhtl_path_at(999), None);
278+
}
245279
}

crates/symbiont/src/key_render.rs

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
//! §2.4 — the key-only, neo4j-grade graph render.
2+
//!
3+
//! Operator superpower (`unified-soa-rubikon-integration-v1.md` §2.4): read all
4+
//! boards touching ONLY the **32-byte head** of each `NodeRow` — the 128-bit
5+
//! [`NodeGuid`] (`key`, bytes 0..16) + the 128-bit [`EdgeBlock`] (`edges`, bytes
6+
//! 16..32) — and NEVER the 480-byte value slab (bytes 32..512). That is a Neo4j-
7+
//! like node+edge view at memory-scan speed: `E-GUID-IS-THE-GRAPH` — GUID-key =
8+
//! node, EdgeBlock-slot = edge, traversal = prefix-route + slot-deref, all
9+
//! **zero value decode**.
10+
//!
11+
//! "Zero value decode" is not a slogan here — it is a falsifiable probe
12+
//! ([`tests::render_ignores_value_slab`]): poison every row's value slab with
13+
//! `0xFF`, render, and assert the output is byte-identical to the render over
14+
//! zeroed slabs. If the render ever touched the value region the two would
15+
//! differ. The render reads exactly two fields, `row.key` and `row.edges`.
16+
//!
17+
//! This is the read side of the same head the SoA owner materialises through the
18+
//! contract's zero-decode key facets `MailboxSoaView::{hhtl_path_at,
19+
//! edge_block_at}` (overridden on `SymbiontBoard`, kanban_loop.rs) — the trait
20+
//! declares them "zero value decode"; this module is the consumer that proves it.
21+
22+
use lance_graph_contract::canonical_node::NodeRow;
23+
use lance_graph_contract::hhtl::NiblePath;
24+
use lance_graph_contract::NodeGuid;
25+
26+
/// Lower a GUID's 3×4 HHT cascade — `HEEL·HIP·TWIG`, 3 tiers × 4 nibbles = 12
27+
/// nibbles, root-first — to a [`NiblePath`] (the radix-trie / CLAM cluster
28+
/// address). `classid` is the routing PREFIX (codebook selector, resolved
29+
/// separately by longest-prefix); the HHTL *path* proper is the 12 HHT nibbles
30+
/// (OGAR canon "3×4 PATH — uniform"). Pure key arithmetic, zero value decode.
31+
#[inline]
32+
pub fn hhtl_path_of(guid: &NodeGuid) -> NiblePath {
33+
let tiers = [guid.heel(), guid.hip(), guid.twig()];
34+
let mut p = NiblePath::EMPTY;
35+
let mut first = true;
36+
for tier in tiers {
37+
// 4 nibbles per u16 tier, most-significant first (root-first).
38+
for shift in [12u32, 8, 4, 0] {
39+
let nib = ((tier >> shift) & 0xF) as u8;
40+
p = if first {
41+
first = false;
42+
NiblePath::root(nib)
43+
} else {
44+
p.child(nib)
45+
};
46+
}
47+
}
48+
p
49+
}
50+
51+
/// One rendered node, derived from the 32-byte head ONLY.
52+
#[derive(Debug, Clone, PartialEq, Eq)]
53+
pub struct RenderedNode {
54+
/// The node identity = the 128-bit canonical GUID (`key`, bytes 0..16).
55+
pub guid: NodeGuid,
56+
/// The HHTL routing address of the GUID (the 3×4 HHT nibble path).
57+
pub hhtl: NiblePath,
58+
/// Live in-family edge slots: `(slot_index 0..12, neighbour_byte)` for each
59+
/// non-zero of the 12 basin-local slots.
60+
pub in_family: Vec<(u8, u8)>,
61+
/// Live out-of-family edge slots: `(slot_index 0..4, neighbour_byte)` for
62+
/// each non-zero of the 4 inherited-adapter slots.
63+
pub out_family: Vec<(u8, u8)>,
64+
}
65+
66+
/// The whole key-only graph view: one [`RenderedNode`] per board + the total
67+
/// live-edge count. Built touching only the head of every row.
68+
#[derive(Debug, Clone, PartialEq, Eq)]
69+
pub struct KeyGraph {
70+
pub nodes: Vec<RenderedNode>,
71+
pub edge_count: usize,
72+
}
73+
74+
/// Render the key-only graph over a board-set, touching ONLY `row.key` (the
75+
/// GUID) and `row.edges` (the EdgeBlock) — never `row.value`. This is the
76+
/// memory-scan-speed Neo4j view: at 16384 boards it walks 16384 × 32 B = 512 KiB
77+
/// of heads, leaving the 8 MiB of value slabs cold.
78+
pub fn render_key_only(rows: &[NodeRow]) -> KeyGraph {
79+
let mut nodes = Vec::with_capacity(rows.len());
80+
let mut edge_count = 0usize;
81+
for row in rows {
82+
// ── the ONLY two field reads: the 32-byte head ──
83+
let guid = row.key;
84+
let eb = row.edges;
85+
let in_family: Vec<(u8, u8)> = eb
86+
.in_family
87+
.iter()
88+
.enumerate()
89+
.filter(|(_, &b)| b != 0)
90+
.map(|(i, &b)| (i as u8, b))
91+
.collect();
92+
let out_family: Vec<(u8, u8)> = eb
93+
.out_family
94+
.iter()
95+
.enumerate()
96+
.filter(|(_, &b)| b != 0)
97+
.map(|(i, &b)| (i as u8, b))
98+
.collect();
99+
edge_count += in_family.len() + out_family.len();
100+
nodes.push(RenderedNode {
101+
guid,
102+
hhtl: hhtl_path_of(&guid),
103+
in_family,
104+
out_family,
105+
});
106+
}
107+
KeyGraph { nodes, edge_count }
108+
}
109+
110+
/// The §2.4 demo: render the 16k-board SoA key-only and report node/edge counts
111+
/// + a self-describing sample, proving the Neo4j-grade view costs only the
112+
/// 32-byte heads (8 MiB of value slabs untouched).
113+
pub fn run_demo() {
114+
let rows = crate::domino::seed_boards(crate::bridge::MAX_BOARDS);
115+
let g = render_key_only(&rows);
116+
let head_bytes = g.nodes.len() * 32;
117+
let slab_bytes = g.nodes.len() * 480;
118+
let sample = &g.nodes[3];
119+
println!(
120+
"§2.4 key-only render: {} nodes / {} edges from {} KiB of 32-byte heads \
121+
({} KiB of value slabs left COLD, zero decode); sample node[3] = {} \
122+
hhtl_depth={} in_family={:?} out_family={:?}",
123+
g.nodes.len(),
124+
g.edge_count,
125+
head_bytes / 1024,
126+
slab_bytes / 1024,
127+
sample.guid,
128+
sample.hhtl.depth(),
129+
sample.in_family,
130+
sample.out_family,
131+
);
132+
}
133+
134+
#[cfg(test)]
135+
mod tests {
136+
use super::*;
137+
use crate::domino::seed_boards;
138+
139+
#[test]
140+
fn render_reads_head_only_and_finds_edges() {
141+
let rows = seed_boards(16);
142+
let g = render_key_only(&rows);
143+
assert_eq!(g.nodes.len(), 16);
144+
// each seeded board has in_family[0] (ring) + out_family[0] (adapter).
145+
assert_eq!(g.edge_count, 32);
146+
let n = &g.nodes[3];
147+
// identity = idx (bootstrap address: classid + family default).
148+
assert_eq!(n.guid.identity(), 3);
149+
assert!(n.guid.is_bootstrap_address());
150+
assert_eq!(n.in_family, vec![(0u8, 4u8)]); // (3 % 255) + 1
151+
assert_eq!(n.out_family, vec![(0u8, 4u8)]); // 1 + (3 % 4)
152+
}
153+
154+
#[test]
155+
fn render_ignores_value_slab() {
156+
// The falsifiable zero-value-decode probe: poisoning the value slab with
157+
// 0xFF must not change the key-only render by a single byte. If the
158+
// render touched bytes 32..512, the two KeyGraphs would differ.
159+
let clean = seed_boards(64);
160+
let mut poisoned = clean.clone();
161+
for row in &mut poisoned {
162+
row.value = [0xFFu8; 480];
163+
}
164+
assert_eq!(render_key_only(&clean), render_key_only(&poisoned));
165+
}
166+
167+
#[test]
168+
fn hhtl_path_of_bootstrap_is_depth_12_all_zero() {
169+
// A bootstrap GUID (HEEL=HIP=TWIG=0) lowers to a 12-nibble all-zero path
170+
// (root basin 0, descending 0 each level) — every HHT tier consulted,
171+
// none discriminating yet (the zero-fallback ladder, in the path axis).
172+
let g = NodeGuid::local(42);
173+
let p = hhtl_path_of(&g);
174+
assert_eq!(p.depth(), 12);
175+
}
176+
177+
#[test]
178+
fn hhtl_path_of_uses_hht_tiers_not_classid_or_identity() {
179+
// classid is the routing prefix (codebook selector), identity is the
180+
// leaf — neither is part of the HHT path. Two GUIDs differing ONLY in
181+
// classid + identity share the same 12-nibble HHTL path; differing in a
182+
// HHT tier changes it.
183+
let a = NodeGuid::new(0xAAAA_AAAA, 0x1234, 0x5678, 0x9ABC, 0, 0x00_0001);
184+
let b = NodeGuid::new(0xBBBB_BBBB, 0x1234, 0x5678, 0x9ABC, 0, 0x00_0002);
185+
let c = NodeGuid::new(0xAAAA_AAAA, 0x0234, 0x5678, 0x9ABC, 0, 0x00_0001);
186+
assert_eq!(hhtl_path_of(&a), hhtl_path_of(&b));
187+
assert_ne!(hhtl_path_of(&a), hhtl_path_of(&c));
188+
}
189+
}

0 commit comments

Comments
 (0)