Skip to content

Commit 82bd03b

Browse files
committed
feat(dn_path): replace scent_stub XOR-fold with FNV-1a hash
Replace the Phase-A XOR-fold scent stub with a proper FNV-1a hash of the canonical hex path string, folded to u8. This avoids pulling bgz-tensor into the callcenter dep tree while providing better avalanche properties than raw XOR of segment hashes. - Add `DnPath::scent()` using FNV-1a of hex-rendered segment hashes - Keep `scent_stub()` as `#[deprecated]` alias delegating to `scent()` - Update `lance_membrane.rs` caller to use `scent()` directly - Add tests: deterministic, different-paths, empty-path, alias parity https://claude.ai/code/session_01NYGrxVopyszZYgLBxe4hgj
1 parent aae260d commit 82bd03b

2 files changed

Lines changed: 62 additions & 23 deletions

File tree

crates/lance-graph-callcenter/src/dn_path.rs

Lines changed: 59 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
//!
55
//! Each segment is a u64 FNV-1a hash of the path component. The 6-tuple
66
//! compresses via ZeckBF17→Base17→CAM-PQ→scent (1B, ρ=0.937).
7-
//! Phase C wires the full compression chain. Phase A carries the
8-
//! parsed address and a stub scent derived by XOR-folding the 6 hashes.
7+
//! Phase C wires the full compression chain. The scent is computed by
8+
//! FNV-1a hashing the canonical hex representation of the 6 segment
9+
//! hashes and XOR-folding the 64-bit digest to 1 byte.
910
//!
1011
//! Plan: `.claude/plans/callcenter-membrane-v1.md` § 10.12 – § 10.13
1112
@@ -45,21 +46,42 @@ impl DnPath {
4546
})
4647
}
4748

48-
/// Phase-A scent stub: XOR-fold of the 6 segment hashes into 1 byte.
49+
/// Compute the scent of this DN path: FNV-1a hash of the canonical
50+
/// path string, folded to a single `u8`.
4951
///
50-
/// Phase C replaces this with the full ZeckBF17→Base17→CAM-PQ chain
51-
/// (16Kbit → 48B → 34B → 6B → 1B, ρ=0.937). Until then, this gives
52-
/// a deterministic, stable placeholder that exercises the scent field.
52+
/// The canonical form is the 6 segment hashes rendered as hex and
53+
/// concatenated with `/` separators (deterministic, stable, zero-dep).
54+
/// The full 64-bit FNV-1a digest is XOR-folded into 1 byte, preserving
55+
/// avalanche properties much better than the old XOR-fold of individual
56+
/// segment hashes.
57+
///
58+
/// Future phases may replace this with ZeckBF17→Base17→CAM-PQ
59+
/// (16Kbit → 48B → 34B → 6B → 1B, ρ=0.937) once bgz-tensor
60+
/// enters the callcenter dep tree.
61+
pub fn scent(&self) -> u8 {
62+
use core::fmt::Write;
63+
let mut buf = String::with_capacity(6 * 17);
64+
let segments = [self.ns, self.heel, self.hip, self.branch, self.twig, self.leaf];
65+
for (i, seg) in segments.iter().enumerate() {
66+
if i > 0 { buf.push('/'); }
67+
let _ = write!(buf, "{:016x}", seg);
68+
}
69+
let h = fnv1a(&buf);
70+
let folded = h
71+
^ (h >> 8)
72+
^ (h >> 16)
73+
^ (h >> 24)
74+
^ (h >> 32)
75+
^ (h >> 40)
76+
^ (h >> 48)
77+
^ (h >> 56);
78+
folded as u8
79+
}
80+
81+
/// Deprecated alias — use [`scent()`](Self::scent) instead.
82+
#[deprecated(since = "0.1.1", note = "renamed to `scent()`; the XOR-fold stub has been replaced with FNV-1a")]
5383
pub fn scent_stub(&self) -> u8 {
54-
let fold = self.ns ^ self.heel ^ self.hip ^ self.branch ^ self.twig ^ self.leaf;
55-
(fold
56-
^ (fold >> 8)
57-
^ (fold >> 16)
58-
^ (fold >> 24)
59-
^ (fold >> 32)
60-
^ (fold >> 40)
61-
^ (fold >> 48)
62-
^ (fold >> 56)) as u8
84+
self.scent()
6385
}
6486
}
6587

@@ -102,7 +124,7 @@ mod tests {
102124
}
103125

104126
#[test]
105-
fn scent_stub_is_deterministic() {
127+
fn scent_is_deterministic() {
106128
let p1 = DnPath::parse(
107129
"/tree/ada/heel/callcenter/hip/v1/branch/agents/twig/card/leaf/abc",
108130
)
@@ -111,11 +133,11 @@ mod tests {
111133
"/tree/ada/heel/callcenter/hip/v1/branch/agents/twig/card/leaf/abc",
112134
)
113135
.unwrap();
114-
assert_eq!(p1.scent_stub(), p2.scent_stub());
136+
assert_eq!(p1.scent(), p2.scent());
115137
}
116138

117139
#[test]
118-
fn different_paths_typically_differ() {
140+
fn different_paths_different_scents() {
119141
let p1 = DnPath::parse(
120142
"/tree/ada/heel/callcenter/hip/v1/branch/agents/twig/card/leaf/abc",
121143
)
@@ -124,7 +146,24 @@ mod tests {
124146
"/tree/ada/heel/callcenter/hip/v1/branch/agents/twig/card/leaf/xyz",
125147
)
126148
.unwrap();
127-
// leaf differs → scent should differ (not guaranteed but very likely for FNV-1a)
128-
assert_ne!(p1.leaf, p2.leaf);
149+
assert_ne!(p1.scent(), p2.scent());
150+
}
151+
152+
#[test]
153+
fn empty_path_scent() {
154+
let p = DnPath::default();
155+
let s1 = p.scent();
156+
let s2 = p.scent();
157+
assert_eq!(s1, s2);
158+
}
159+
160+
#[allow(deprecated)]
161+
#[test]
162+
fn scent_stub_alias_matches_scent() {
163+
let p = DnPath::parse(
164+
"/tree/ada/heel/callcenter/hip/v1/branch/agents/twig/card/leaf/abc",
165+
)
166+
.unwrap();
167+
assert_eq!(p.scent_stub(), p.scent());
129168
}
130169
}

crates/lance-graph-callcenter/src/lance_membrane.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -397,7 +397,7 @@ impl ExternalMembrane for LanceMembrane {
397397
/// Four-step BBB crossing:
398398
/// 1. Pass membrane — `ExternalIntent` is the safe crossing type.
399399
/// 2. Get a role — `intent.role` is stamped into `current_actor.role`.
400-
/// 3. Get a place — `intent.dn.scent_stub()` becomes `current_scent`.
400+
/// 3. Get a place — `intent.dn.scent()` becomes `current_scent`.
401401
/// 4. Translate — returns `UnifiedStep` for `OrchestrationBridge::route()`.
402402
fn ingest(&self, intent: ExternalIntent) -> UnifiedStep {
403403
// 2. Role — atomic write to the shared actor state (F-01 fix).
@@ -418,8 +418,8 @@ impl ExternalMembrane for LanceMembrane {
418418
actor.role = role;
419419
}
420420

421-
// 3. Place (Phase A: XOR-fold stub; Phase C: full cascade)
422-
let scent = intent.dn.scent_stub();
421+
// 3. Place (FNV-1a scent; Phase C may upgrade to full cascade)
422+
let scent = intent.dn.scent();
423423
self.current_scent.store(scent as u64, Ordering::Release);
424424

425425
// 4. Translate to step type for OrchestrationBridge

0 commit comments

Comments
 (0)