@@ -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}
0 commit comments