Skip to content

Commit 5dd1645

Browse files
committed
feat(contract): D-GV2-1 — GUID v2 tail (leaf·family·identity 3×u16), feature-gated
Operator greenlit the guid-v2-tail plan. Ships D-GV2-1: the v2 basin tail behind feature `guid-v2-tail` (default OFF), ADDITIVE + NON-breaking (v1 new/family()/identity() untouched). canonical_node (under #[cfg(feature="guid-v2-tail")]): - new_v2(classid,heel,hip,twig,leaf,family,identity) — leaf/family/identity as full u16 at fixed offsets 10..12 / 12..14 / 14..16 (no u24). - leaf() (4th HHTL tier), family_v2() (basin/episodic hub), identity_v2() (instance), local_key_v2() (family++identity, 4B), decode_v2/GuidPartsV2, to_hex_v2 (uniform 4-hex Display), GUID_TAIL_LAYOUT_VERSION_V2 = 2. hhtl: from_guid_prefix_v2 = HEEL·HIP·TWIG·leaf (16 nibbles). leaf IS in the routing path; family/identity are the basin tail (NOT in the path); classid is the separate codebook prefix. Per I-LEGACY-API-FEATURE-GATED: distinct v2 names (no function silently changes semantics under the flag), field-isolation matrix test (vary one tier → only that accessor changes), v1/v2 coexistence test, leaf-in-path test, version-gate const. Cutover (rename v2→canonical, deprecate v1, ENVELOPE_LAYOUT_VERSION bump) = D-GV2-5, after D-GV2-2/3/4 adopt the v2 accessors. Verified BOTH configs: default lib tests 703 (unchanged, non-breaking); --features guid-v2-tail 706 (+3 v2 tests); clippy -D warnings clean both. Plan D-GV2-1 marked SHIPPED; AGENT_LOG updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CcpLeEC3XK8Eye53GKBVvi
1 parent 0c8558e commit 5dd1645

5 files changed

Lines changed: 239 additions & 1 deletion

File tree

.claude/board/AGENT_LOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 2026-06-20 (cont.⁸) — D-GV2-1 shipped: GUID v2 tail (leaf·family·identity 3×u16), feature-gated
2+
3+
**Main thread (Opus), autoattended.** Operator "go" on the guid-v2-tail plan (canon version bump + capacity numbers accepted). Built **D-GV2-1** additive + `#[cfg(feature="guid-v2-tail")]` + NON-breaking (v1 untouched): `canonical_node::{new_v2, leaf() 10..12, family_v2() 12..14, identity_v2() 14..16, local_key_v2, decode_v2/GuidPartsV2, to_hex_v2, GUID_TAIL_LAYOUT_VERSION_V2=2}`; `hhtl::from_guid_prefix_v2` (HEEL·HIP·TWIG·leaf, 16 nibbles — leaf in path, family/identity in basin tail). Per `I-LEGACY-API-FEATURE-GATED`: distinct v2 names (no silent semantic swap), field-isolation matrix test (vary one tier → only that accessor changes), v1/v2 coexistence test, version-gate const. **Verified BOTH configs:** default `cargo test -p lance-graph-contract --lib` = **703** (unchanged, non-breaking); `--features guid-v2-tail` = **706** (+3 v2 tests); clippy `-D warnings` clean on both. Cutover (rename v2→canonical, deprecate v1, ENVELOPE_LAYOUT_VERSION bump) = D-GV2-5, after D-GV2-2 (family→Codebook registry) / D-GV2-3 (soa_graph per-family edges) / D-GV2-4 (aiwar re-key) consume the v2 accessors. Pushed to jirak (extends PR #560). Plan D-GV2-1 marked SHIPPED.
4+
15
## 2026-06-20 (cont.⁷) — codex roll-up + 16-family-adapter edges + Callcenter DataFusion/Gremlin + aiwar POC
26

37
**Main thread (Opus), autoattended.** Follow-up to merged PR #557. Pulled the 2 codex P1 review comments (chatgpt-codex-connector; the CodeRabbit arg-count was fixed pre-merge) and rolled both into this PR per operator. **codex #1** (classid filter): `project_snapshot`/`nearest_anchor` now include only rows where `classid == domain.classid` (a classid IS the class, exact — operator). **codex #2** (ambiguous edge bytes): resolved via the operator's **16×8-bit family-node adapter** model — the `EdgeBlock` is read as 16 family adapters (each byte → a FAMILY by `family & 0xFF`, collision-aware skip), not member-by-identity; the >255-member aliasing dissolves (resolution is family-level only). EPIPHANY `E-FAMILY-ADAPTER-EDGES-ARE-RENDER-STABLE` (mixin-dependency traded for render stability + flexibility).

.claude/plans/guid-v2-tail-per-family-codebook-v1.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ coarse→fine left-to-right: `…·TWIG·leaf` (routing) then `family·identity`
9292

9393
## Deliverables (when greenlit — feature `guid-v2-tail`, default OFF)
9494

95-
- **D-GV2-1** `canonical_node` v2 layout behind `#[cfg(feature="guid-v2-tail")]` — u16 tiers at fixed offsets: classid `0..4`, HEEL `4..6`, HIP `6..8`, TWIG `8..10`, **leaf `10..12`**, **family `12..14`**, **identity `14..16`**. `leaf()`/`family()`/`identity()` return `u16`; `new` arity (+`leaf`); `local_key` = `family·identity` (4 bytes); `decode`/`GuidParts`; `Display` (uniform 4-hex groups). **`from_guid_prefix_v2` = `HEEL·HIP·TWIG·leaf`** (16 nibbles; classid is the separate codebook prefix). v1 stays default. **Field-isolation matrix test** (write each tier, assert all others unchanged) + `ENVELOPE_LAYOUT_VERSION` bump + v1→v2 version gate (`I-LEGACY-API-FEATURE-GATED`).
95+
- **D-GV2-1** **SHIPPED (this PR, feature `guid-v2-tail` default OFF, additive & non-breaking).** `canonical_node`: `new_v2` (+`leaf`), `leaf()` `10..12`, `family_v2()` `12..14`, `identity_v2()` `14..16` (all `u16`), `local_key_v2()` (4 bytes), `decode_v2()`/`GuidPartsV2`, `to_hex_v2()` (uniform 4-hex), `GUID_TAIL_LAYOUT_VERSION_V2 = 2` (version gate). `hhtl::from_guid_prefix_v2` = `HEEL·HIP·TWIG·leaf` (16 nibbles; classid is the separate codebook prefix; leaf in path, family/identity NOT). v1 `new`/`family()`/`identity()` UNTOUCHED (distinct v2 names → no silent semantic swap, `I-LEGACY-API-FEATURE-GATED`). **Field-isolation matrix test** + v1/v2 coexistence + leaf-in-path tests. default 703 / `--features guid-v2-tail` 706, clippy clean both. **Cutover (rename v2→canonical, deprecate v1, bump `ENVELOPE_LAYOUT_VERSION`) = D-GV2-5.**
9696
- **D-GV2-2** `family → Codebook` registry = **episodic basin** (the codebook + the basin's accumulated supporting edges), sibling of `classid → ClassView` in `lance-graph-ontology`: `LazyLock`/Lance-backed, masked-load lookup, head-only. 256-entry cap + split-on-overflow guard. Mixin = O(1) reference to this basin (`E-MIXIN-IS-AN-ADDRESS-REFERENCE-NOT-A-COPY`).
9797
- **D-GV2-3** `soa_graph` per-family edge resolution: 12 in-family = 1-byte own-codebook index, 4 out-of-family = `(family,index)`; retire `family & 0xFF` collision-skip under v2.
9898
- **D-GV2-4** `aiwar` re-keyed on `leaf` (coarse node-type, 5 hubs) + per-family codebook (System/Stakeholder/… vocabularies) → resolves the "60 noisy families" on real data.

crates/lance-graph-contract/Cargo.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,10 @@ harness = false
3535
# cross-PR bridge between PR #278 audit log + PR #279 grammar trajectory.
3636
# No-op alias today; concrete impl lands once the bridge PR ships.
3737
trajectory-audit = []
38+
39+
# guid-v2-tail (D-GV2-1, plan: guid-v2-tail-per-family-codebook-v1.md) — the v2
40+
# basin tail leaf(u16)·family(u16)·identity(u16) replacing v1 family(u24)·
41+
# identity(u24), leaf as the 4th HHTL tier. Default OFF; additive v2 accessors
42+
# (`leaf`/`*_v2`) coexist with v1 until cutover (D-GV2-5). Layout reclaim →
43+
# I-LEGACY-API-FEATURE-GATED (field-isolation matrix + version gate).
44+
guid-v2-tail = []

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

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,131 @@ impl NodeGuid {
204204
}
205205
}
206206

207+
// ── GUID v2 tail (leaf·family·identity, 3×u16) — D-GV2-1, feature-gated ────────
208+
//
209+
// The v2 basin tail repartitions bytes 10..16: leaf(u16) 10..12 (the 4th HHTL
210+
// tier), family(u16) 12..14 (the basin / episodic hub), identity(u16) 14..16
211+
// (the instance). Bytes 0..10 (classid·HEEL·HIP·TWIG) are IDENTICAL to v1.
212+
// Additive and NON-breaking: v1 `new`/`family`/`identity` are untouched; these
213+
// v2 accessors coexist behind `guid-v2-tail` until cutover (D-GV2-5). Per
214+
// I-LEGACY-API-FEATURE-GATED the v2 names are distinct (`leaf`/`*_v2`), so no
215+
// function silently changes semantics, and `GUID_TAIL_LAYOUT_VERSION_V2` is the
216+
// version gate marking a v2-tail packet.
217+
#[cfg(feature = "guid-v2-tail")]
218+
impl NodeGuid {
219+
/// Construct a v2-tail GUID: `classid·HEEL·HIP·TWIG` identical to v1, then the
220+
/// 3×u16 basin tail `leaf·family·identity`. Each tail field is a full `u16` —
221+
/// no 24-bit truncation footgun (the point of v2).
222+
#[allow(clippy::too_many_arguments)]
223+
pub const fn new_v2(
224+
classid: u32,
225+
heel: u16,
226+
hip: u16,
227+
twig: u16,
228+
leaf: u16,
229+
family: u16,
230+
identity: u16,
231+
) -> Self {
232+
let c = classid.to_le_bytes();
233+
let h = heel.to_le_bytes();
234+
let p = hip.to_le_bytes();
235+
let t = twig.to_le_bytes();
236+
let l = leaf.to_le_bytes();
237+
let f = family.to_le_bytes();
238+
let i = identity.to_le_bytes();
239+
Self([
240+
c[0], c[1], c[2], c[3], // 0..4 classid
241+
h[0], h[1], // 4..6 HEEL
242+
p[0], p[1], // 6..8 HIP
243+
t[0], t[1], // 8..10 TWIG
244+
l[0], l[1], // 10..12 leaf (4th HHTL tier)
245+
f[0], f[1], // 12..14 family (basin / episodic hub)
246+
i[0], i[1], // 14..16 identity (instance)
247+
])
248+
}
249+
250+
/// v2 `leaf` — bytes 10..12, the 4th HHTL routing tier (cascade terminal).
251+
#[inline]
252+
pub const fn leaf(&self) -> u16 {
253+
u16::from_le_bytes([self.0[10], self.0[11]])
254+
}
255+
256+
/// v2 `family` — bytes 12..14, the basin / episodic-hub tier (the codebook
257+
/// selector). Distinct from v1 [`family`](NodeGuid::family) (u24 at 10..13):
258+
/// different name, different bytes — no silent semantic swap.
259+
#[inline]
260+
pub const fn family_v2(&self) -> u16 {
261+
u16::from_le_bytes([self.0[12], self.0[13]])
262+
}
263+
264+
/// v2 `identity` — bytes 14..16, the instance tier (full `u16`).
265+
#[inline]
266+
pub const fn identity_v2(&self) -> u16 {
267+
u16::from_le_bytes([self.0[14], self.0[15]])
268+
}
269+
270+
/// v2 basin-local key: trailing 4 bytes (family ++ identity), zero-padded to
271+
/// `u32` — the discriminator once the HHTL prefix (incl. leaf) is bound.
272+
#[inline]
273+
pub const fn local_key_v2(&self) -> u32 {
274+
u32::from_le_bytes([self.0[12], self.0[13], self.0[14], self.0[15]])
275+
}
276+
277+
/// v2 decode — every tier (`classid·HEEL·HIP·TWIG·leaf·family·identity`) as a
278+
/// native integer. The "read the GUID as a GUID" surface for v2.
279+
#[inline]
280+
pub const fn decode_v2(&self) -> GuidPartsV2 {
281+
GuidPartsV2 {
282+
classid: self.classid(),
283+
heel: self.heel(),
284+
hip: self.hip(),
285+
twig: self.twig(),
286+
leaf: self.leaf(),
287+
family: self.family_v2(),
288+
identity: self.identity_v2(),
289+
}
290+
}
291+
292+
/// v2 self-describing hex: `classid-heel-hip-twig-leaf-family-identity`,
293+
/// uniform 4-hex groups (classid as 8) — the v2 Display shape.
294+
pub fn to_hex_v2(&self) -> String {
295+
let p = self.decode_v2();
296+
format!(
297+
"{:08x}-{:04x}-{:04x}-{:04x}-{:04x}-{:04x}-{:04x}",
298+
p.classid, p.heel, p.hip, p.twig, p.leaf, p.family, p.identity
299+
)
300+
}
301+
}
302+
303+
/// The v2-tail GUID decoded — `classid · HEEL · HIP · TWIG · leaf · family ·
304+
/// identity`, every tier a native integer (no `u24`). The v2 counterpart of
305+
/// [`GuidParts`]. (D-GV2-1; feature `guid-v2-tail`.)
306+
#[cfg(feature = "guid-v2-tail")]
307+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
308+
pub struct GuidPartsV2 {
309+
/// 0..4 — prefix-routable class id.
310+
pub classid: u32,
311+
/// 4..6 — HEEL (HHT cascade tier 1).
312+
pub heel: u16,
313+
/// 6..8 — HIP (HHT cascade tier 2).
314+
pub hip: u16,
315+
/// 8..10 — TWIG (HHT cascade tier 3).
316+
pub twig: u16,
317+
/// 10..12 — leaf, the 4th HHTL tier.
318+
pub leaf: u16,
319+
/// 12..14 — family, the basin / episodic hub.
320+
pub family: u16,
321+
/// 14..16 — identity, the instance.
322+
pub identity: u16,
323+
}
324+
325+
/// v2 layout-version marker: a v2-tail packet is layout version 2. A v1 reader
326+
/// MUST refuse a v2 blob (and vice-versa) — the version gate per
327+
/// `I-LEGACY-API-FEATURE-GATED`. Wired into the `SoaEnvelope` version at cutover
328+
/// (D-GV2-5).
329+
#[cfg(feature = "guid-v2-tail")]
330+
pub const GUID_TAIL_LAYOUT_VERSION_V2: u16 = 2;
331+
207332
/// The whole canonical key decoded in one shot — `classid · HEEL · HIP · TWIG ·
208333
/// family · identity`, each as its native LE-decoded integer.
209334
///
@@ -1284,4 +1409,65 @@ mod tests {
12841409
);
12851410
assert!(osint.is_layout_preserving() && fma.is_layout_preserving());
12861411
}
1412+
1413+
// ── GUID v2 tail (D-GV2-1) — field-isolation matrix + coexistence ─────────
1414+
1415+
#[cfg(feature = "guid-v2-tail")]
1416+
#[test]
1417+
fn v2_field_isolation_matrix() {
1418+
// Each tier carries a distinct value; every accessor reads back exactly
1419+
// its own, and varying ONE tier changes ONLY that accessor (the
1420+
// mandatory layout-bit-boundary test for a reclaim, I-LEGACY).
1421+
let base = NodeGuid::new_v2(0x1111_2222, 0x3333, 0x4444, 0x5555, 0x6666, 0x7777, 0x8888);
1422+
assert_eq!(base.classid(), 0x1111_2222);
1423+
assert_eq!(base.heel(), 0x3333);
1424+
assert_eq!(base.hip(), 0x4444);
1425+
assert_eq!(base.twig(), 0x5555);
1426+
assert_eq!(base.leaf(), 0x6666);
1427+
assert_eq!(base.family_v2(), 0x7777);
1428+
assert_eq!(base.identity_v2(), 0x8888);
1429+
1430+
// vary ONLY leaf
1431+
let l = NodeGuid::new_v2(0x1111_2222, 0x3333, 0x4444, 0x5555, 0xAAAA, 0x7777, 0x8888);
1432+
assert_eq!(l.leaf(), 0xAAAA);
1433+
assert_eq!(l.family_v2(), base.family_v2());
1434+
assert_eq!(l.identity_v2(), base.identity_v2());
1435+
assert_eq!(l.twig(), base.twig());
1436+
// vary ONLY family
1437+
let f = NodeGuid::new_v2(0x1111_2222, 0x3333, 0x4444, 0x5555, 0x6666, 0xBBBB, 0x8888);
1438+
assert_eq!(f.family_v2(), 0xBBBB);
1439+
assert_eq!(f.leaf(), base.leaf());
1440+
assert_eq!(f.identity_v2(), base.identity_v2());
1441+
// vary ONLY identity
1442+
let i = NodeGuid::new_v2(0x1111_2222, 0x3333, 0x4444, 0x5555, 0x6666, 0x7777, 0xCCCC);
1443+
assert_eq!(i.identity_v2(), 0xCCCC);
1444+
assert_eq!(i.leaf(), base.leaf());
1445+
assert_eq!(i.family_v2(), base.family_v2());
1446+
1447+
// local_key_v2 = family ++ identity (LE)
1448+
assert_eq!(base.local_key_v2(), 0x8888_7777);
1449+
// decode_v2 round-trips the tail
1450+
let d = base.decode_v2();
1451+
assert_eq!((d.leaf, d.family, d.identity), (0x6666, 0x7777, 0x8888));
1452+
// Display is uniform 4-hex groups (classid 8).
1453+
assert_eq!(base.to_hex_v2(), "11112222-3333-4444-5555-6666-7777-8888");
1454+
}
1455+
1456+
#[cfg(feature = "guid-v2-tail")]
1457+
#[test]
1458+
fn v1_and_v2_share_prefix_differ_in_tail() {
1459+
// v1 and v2 agree on the prefix (classid·HEEL·HIP·TWIG)…
1460+
let v1 = NodeGuid::new(0xDEAD_BEEF, 0x1111, 0x2222, 0x3333, 0x00_00AB, 0x00_00CD);
1461+
let v2 = NodeGuid::new_v2(0xDEAD_BEEF, 0x1111, 0x2222, 0x3333, 0, 0xABCD, 0);
1462+
assert_eq!(v1.classid(), v2.classid());
1463+
assert_eq!(v1.heel(), v2.heel());
1464+
assert_eq!(v1.hip(), v2.hip());
1465+
assert_eq!(v1.twig(), v2.twig());
1466+
// …but the tail bytes are interpreted differently — which is exactly why
1467+
// the version gate is mandatory before reading a tail.
1468+
assert_eq!(GUID_TAIL_LAYOUT_VERSION_V2, 2);
1469+
// v1 accessors remain UNTOUCHED under the feature (additive, non-breaking).
1470+
assert_eq!(v1.family(), 0x00_00AB);
1471+
assert_eq!(v1.identity(), 0x00_00CD);
1472+
}
12871473
}

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

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,27 @@ impl NiblePath {
312312
Self::from_packed(path, MAX_DEPTH)
313313
}
314314

315+
/// v2 GUID→path lowering (D-GV2-1, feature `guid-v2-tail`): the HHTL path is
316+
/// `HEEL·HIP·TWIG·leaf` — 4 tiers × 4 nibbles = 16 nibbles = a full `u64`
317+
/// NiblePath. `leaf` (the v2 4th tier) IS part of the routing path; `classid`
318+
/// is the separate codebook prefix (not folded in), and `family`/`identity`
319+
/// are the basin tail (NOT in the path). Two GUIDs differing only in
320+
/// family/identity therefore share a path; differing in any HHT tier (incl.
321+
/// `leaf`) do not — the property v2 hop-distance relies on.
322+
#[cfg(feature = "guid-v2-tail")]
323+
#[must_use]
324+
pub const fn from_guid_prefix_v2(guid: &crate::canonical_node::NodeGuid) -> Self {
325+
let path = ((guid.heel() as u64) << 48)
326+
| ((guid.hip() as u64) << 32)
327+
| ((guid.twig() as u64) << 16)
328+
| (guid.leaf() as u64);
329+
// 16 nibbles = full depth; from_packed is always Some at MAX_DEPTH.
330+
match Self::from_packed(path, MAX_DEPTH) {
331+
Some(p) => p,
332+
None => Self::EMPTY,
333+
}
334+
}
335+
315336
/// **Family hop count** — the CLAM tree distance to `other`: the number of
316337
/// edges between the two nodes through their lowest common ancestor in the
317338
/// 16ⁿ tree. `(self.depth − common) + (other.depth − common)` where `common =
@@ -673,6 +694,26 @@ mod tests {
673694
assert_eq!(NiblePath::EMPTY.common_ancestor(a), None);
674695
}
675696

697+
#[cfg(feature = "guid-v2-tail")]
698+
#[test]
699+
fn from_guid_prefix_v2_includes_leaf_not_basin_tail() {
700+
use crate::canonical_node::NodeGuid;
701+
let g = NodeGuid::new_v2(0xDEAD_BEEF, 0x1234, 0x5678, 0x9ABC, 0xDEF0, 0, 0);
702+
assert_eq!(NiblePath::from_guid_prefix_v2(&g).depth(), 16);
703+
// family/identity (basin tail) do NOT affect the path
704+
let same = NodeGuid::new_v2(0xDEAD_BEEF, 0x1234, 0x5678, 0x9ABC, 0xDEF0, 0xFFFF, 0xFFFF);
705+
assert_eq!(
706+
NiblePath::from_guid_prefix_v2(&g),
707+
NiblePath::from_guid_prefix_v2(&same)
708+
);
709+
// leaf IS in the path → changing it changes the path
710+
let diff_leaf = NodeGuid::new_v2(0xDEAD_BEEF, 0x1234, 0x5678, 0x9ABC, 0x0EF0, 0, 0);
711+
assert_ne!(
712+
NiblePath::from_guid_prefix_v2(&g),
713+
NiblePath::from_guid_prefix_v2(&diff_leaf)
714+
);
715+
}
716+
676717
#[test]
677718
fn family_hop_count_is_clam_tree_distance() {
678719
let a = NiblePath::root(0x1).child(0x2).child(0x3).child(0x4);

0 commit comments

Comments
 (0)