Skip to content

Commit f686d3a

Browse files
authored
Merge pull request #614 from AdaWorldAPI/claude/facet-module
refactor(contract): reusable `facet` module — content-blind 8:8 substrate + zero-cost reinterpret
2 parents 6136eb5 + f8becf2 commit f686d3a

4 files changed

Lines changed: 397 additions & 233 deletions

File tree

.claude/board/LATEST_STATE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,8 @@ Membrane consumers can now pull BOTH halves of a render `classid` BBB-safely fro
140140

141141
## Current Contract Inventory (lance-graph-contract)
142142

143+
> **2026-06-25 — MODULARIZED (follow-up to #613) — `lance_graph_contract::facet`**: extracted `FacetTier` / `FacetCascade` from `canonical_node` into a dedicated, reusable `facet` module (a *reading*, NOT part of the locked node layout — the cleaner factoring; `canonical_node` re-exports both for the historical path). **Reusable lane API rounded out:** `as_u128`/`from_u128` (single-register view), `rows()` (the 4 dword rows `{domain}{schema}` / `HEEL:HIP` / `TWIG:LEAF` / `family:identity`), `prefix_distance`/`shared_prefix_tiles` (the **granularity-free LCP redout** — `vpxor`+`tzcnt`; 8:8 vs nibble is a free `>>` on the count, measured), `row_match_mask` (`vpcmpeqd`-lane), plus `as_bytes`/`ref_from_bytes` — a **zero-cost reinterpret** (`#[repr(C, align(16))]`; `as_bytes` measured to lower to `mov rax,rdi`, a literal no-op; fields read straight through as single loads). One register → row(`u32`)/tile(`u16`)/prefix(bit)/nibble(Morton) lenses, each one SIMD op (module docs). Lab-test write-up deferred. Additive, zero-dep; 741 lib green (default + `guid-v3-tail`), clippy `-D warnings` + fmt clean. EPIPHANIES `E-FACET-8-8-ALWAYS`. Branch `claude/facet-module`.
144+
>
143145
> **2026-06-25 — ADDED (#613, the 6-tier 8:8 homogeneous facet + V3 routing fold)**: `lance_graph_contract::canonical_node::{FacetTier, FacetCascade}` — the **ALWAYS-8:8** content-blind facet substrate. `FacetTier{lo, hi}` (2 B, `const`; `as_u16` concatenated + `morton` 2bit×2bit Morton-tile projections); `FacetCascade{facet_classid: u32, tiers: [FacetTier; 6]}` (16 B = `facet_classid(4) | 6×(8:8)=12`, harvest §5.1) — a *reading* over a borrowed `[u8;16]` with `from_bytes`/`to_bytes`/`hi_chain`/`lo_chain`/`hi_distance`/`lo_distance`. **Carries NO value-slab offset** → does NOT touch the operator-LOCKED 480 B layout (the `classid→ClassView` byte-pick is the separate, panel-gated step); content-blind — only the consumer projects meaning (`part_of:is_a` / 256:256 palette centroid / `group:member` / `column:row` / concatenated u16 …), every reading amortizing to one 2bit×2bit Morton tile cascade. **Key-side V3 routing:** `hhtl::NiblePath::from_guid_prefix_v3` (feature `guid-v3-tail`) folds the 4 HHTL tiers `HEEL·HIP·TWIG·LEAF` in FULL (both bytes, depth 16) — the facet's routing prefix; `family`/`identity` stay the basin tail. `classid` NOT folded, so `soa_graph::hhtl_path` (schema-driven by `tail_variant`) routes OSINT-V3 `0x1000_0700` non-empty — fixes the Codex-P2 latent EMPTY-fold. `from_guid_prefix`'s "reserved-zero" doc/guard scoped to **v1-fold** (NOT a global classid law). Additive, zero-dep; 739 lib green (default + `guid-v3-tail`), clippy `-D warnings` + fmt clean. EPIPHANIES `E-FACET-8-8-ALWAYS`. Branch `claude/p-a-readmode-tail-variant`.
144146
145147
> **2026-06-21 — ADDED (content-store for the AriGraph/OSINT episodic arc)**: `lance_graph_contract::content_store::{ContentId, SourceSpan, ContentError, ContentStore, ContentSink}` — the content-addressed **cold text/blob store** contract. `ContentId(u64)` = `hash::fnv1a` of the bytes (stable across versions — the correct content address; `DefaultHasher` must never key one; `0` = sentinel). `SourceSpan{ContentId,u32,u32}` = the fixed-size, `Copy` typed form of `template-equivalence`'s `(source_id,start,end)` provenance; `is_cited()` = "no source span → no claim" (non-sentinel content + non-empty span). `ContentStore` (cold read: `resolve(id) -> Option<&[u8]>` zero-copy slice into the mmap/backing store; `resolve_span`/`contains` defaulted) + `ContentSink` (idempotent `put -> ContentId`, dedup by content-address: many episodes → one source row). **Hot/cold firewall (ADR-022)**: the hot path (SIMD sweep, AriGraph edge traversal) touches only the fixed-size `ContentId`/`SourceSpan`; bytes hydrate cold at the membrane (the fingerprint is the hot-path stand-in for text). Nothing variable-length enters the 512 B node. Additive, zero-dep; +6 tests (stable/dedup, idempotent put, resolve_span slice, OOB/missing errors, uncited-rejected); clippy clean. Consumers: `rs-graph-llm/episodic-arc-task` (replaces its local fnv1a), `template-equivalence` (typed provenance). Plan: `.claude/plans/arigraph-osint-episodic-v1.md` (D-CC-ARI-3). Branch `claude/content-store-contract-draft`.

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

Lines changed: 4 additions & 233 deletions
Original file line numberDiff line numberDiff line change
@@ -1213,183 +1213,10 @@ impl KanbanTenant {
12131213
}
12141214
}
12151215

1216-
// ── FacetCascade value tenant (the 6-tier 8:8 homogeneous facet) ─────────────────
1217-
1218-
/// One **8:8 tile** of a [`FacetCascade`] — ALWAYS exactly two bytes, `hi` and `lo`.
1219-
/// The substrate is **content-blind**: only the CONSUMER (the
1220-
/// [`FacetCascade::facet_classid`]'s ClassView) decides what the 8:8 *means*. The
1221-
/// same two bytes project as any of:
1222-
///
1223-
/// - `(part_of : is_a)` — mereology : taxonomy (the anatomy / `converge.rs` default)
1224-
/// - a **256:256 palette centroid** pair (CAM-PQ — `hi`/`lo` index a 256-codebook)
1225-
/// - a concatenated `u16` ([`as_u16`](Self::as_u16))
1226-
/// - `(group : member)`, `(mixin : identity)`, `(column : row)`, `(memberof : name)`,
1227-
/// a `(Y : Z)` coordinate, …
1228-
///
1229-
/// The producer never bakes a meaning in; the reader projects one (AGI-as-glove: the
1230-
/// SoA is content-blind). `hi` is the coarse-side byte, `lo` the fine-side byte.
1231-
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
1232-
#[repr(C)]
1233-
pub struct FacetTier {
1234-
/// Low byte of the LE 8:8 tile (is_a / member / row / centroid-lo / …).
1235-
pub lo: u8,
1236-
/// High byte of the LE 8:8 tile (part_of / group / column / centroid-hi / …).
1237-
pub hi: u8,
1238-
}
1239-
1240-
impl FacetTier {
1241-
/// The two bytes as the LE `u16 = (hi << 8) | lo` — the "consumer reads the 8:8
1242-
/// as one concatenated 16-bit value" projection.
1243-
#[inline]
1244-
#[must_use]
1245-
pub const fn as_u16(self) -> u16 {
1246-
((self.hi as u16) << 8) | self.lo as u16
1247-
}
1248-
1249-
/// The `hi:lo` pair **Morton-interleaved** into a `u16` Z-order code (`lo` on
1250-
/// even bits, `hi` on odd) — the amortization benefit of the always-8:8
1251-
/// substrate: every nibble of the result is a **2 bit × 2 bit Morton tile** (2
1252-
/// bits of `hi` interleaved with 2 bits of `lo`), so a nibble prefix is a
1253-
/// quad-tree quadrant in BOTH bytes at once (`256 = 4⁴` hierarchical ancestry).
1254-
/// Whatever the consumer decides the 8:8 *means* (part_of:is_a, centroid:centroid,
1255-
/// group:member …), it ALWAYS amortizes to this one Morton tile cascade — so
1256-
/// hierarchical-prefix routing is uniform across every interpretation.
1257-
#[inline]
1258-
#[must_use]
1259-
pub const fn morton(self) -> u16 {
1260-
Self::spread8(self.lo) | (Self::spread8(self.hi) << 1)
1261-
}
1262-
1263-
/// Spread a byte's 8 bits to the even positions `0,2,…,14` of a `u16` (the
1264-
/// Morton building block).
1265-
const fn spread8(x: u8) -> u16 {
1266-
let mut v = x as u16; // ........ abcdefgh
1267-
v = (v | (v << 4)) & 0x0F0F; // ....abcd ....efgh
1268-
v = (v | (v << 2)) & 0x3333; // ..ab..cd ..ef..gh
1269-
v = (v | (v << 1)) & 0x5555; // .a.b.c.d .e.f.g.h
1270-
v
1271-
}
1272-
}
1273-
1274-
/// The **FacetCascade** — the 6-tier **8:8** homogeneous facet, read at an
1275-
/// **alternative location to the key**: a 16-byte ClassView reading over the value
1276-
/// slab (`soa-value-tenant-migration-v1-harvest.md` §5.1,
1277-
/// `facet_classid(4) | 6×(8:8)=12 = 16B`).
1278-
///
1279-
/// **The substrate is ALWAYS 8:8.** Six tiers, each two opaque bytes (`hi:lo`); the
1280-
/// `facet_classid`'s ClassView decides the interpretation — `(part_of:is_a)`,
1281-
/// 256:256 palette centroid, `(group:member)`, `(column:row)`, concatenated `u16`,
1282-
/// … (see [`FacetTier`]). Both bytes of every tier are carried (lossless): the `hi`
1283-
/// chain prefix-routes one hierarchy, the `lo` chain the orthogonal one.
1284-
///
1285-
/// The full 6-tier facet does NOT fit the 64-bit key `NiblePath` — that carries only
1286-
/// the 4-tier HHTL routing **prefix** ([`crate::hhtl::NiblePath::from_guid_prefix_v3`],
1287-
/// `HEEL·HIP·TWIG·LEAF`); the complete 6-tier address (HEEL·HIP·TWIG·LEAF·family·
1288-
/// identity) lives here, at the alternative value-slab location.
1289-
///
1290-
/// This type is a *reading* over a borrowed `[u8; 16]` — it carries NO value-slab
1291-
/// offset, so it does not touch the operator-LOCKED 480-byte layout. The
1292-
/// `classid → ClassView` wiring that picks which 16 value bytes it reads is a
1293-
/// separate, panel-gated step (harvest §5 + §6).
1294-
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
1295-
#[repr(C)]
1296-
pub struct FacetCascade {
1297-
/// The facet's own class id — which ClassView interprets the 6 tiers' 8:8.
1298-
pub facet_classid: u32,
1299-
/// 6 tiers coarse→fine: HEEL·HIP·TWIG·LEAF·family·identity, each an 8:8 tile.
1300-
pub tiers: [FacetTier; 6],
1301-
}
1302-
1303-
const _: () = assert!(core::mem::size_of::<FacetTier>() == 2, "one 8:8 tile");
1304-
const _: () = assert!(
1305-
core::mem::size_of::<FacetCascade>() == 16,
1306-
"facet_classid(4) | 6×(8:8)=12 = 16B (harvest §5.1)"
1307-
);
1308-
1309-
impl FacetCascade {
1310-
/// Decode from the 16 facet bytes (LE): `facet_classid` in `[0..4)`, then 6
1311-
/// tiers, each an LE `u16 = (hi << 8) | lo` — on the wire `[lo, hi]` (matching
1312-
/// the key tiers' `converge.rs` byte order).
1313-
#[inline]
1314-
#[must_use]
1315-
pub const fn from_bytes(b: &[u8; 16]) -> Self {
1316-
FacetCascade {
1317-
facet_classid: u32::from_le_bytes([b[0], b[1], b[2], b[3]]),
1318-
tiers: [
1319-
FacetTier { lo: b[4], hi: b[5] },
1320-
FacetTier { lo: b[6], hi: b[7] },
1321-
FacetTier { lo: b[8], hi: b[9] },
1322-
FacetTier {
1323-
lo: b[10],
1324-
hi: b[11],
1325-
},
1326-
FacetTier {
1327-
lo: b[12],
1328-
hi: b[13],
1329-
},
1330-
FacetTier {
1331-
lo: b[14],
1332-
hi: b[15],
1333-
},
1334-
],
1335-
}
1336-
}
1337-
1338-
/// Encode to the 16 facet bytes (LE), the inverse of [`from_bytes`](Self::from_bytes).
1339-
#[inline]
1340-
#[must_use]
1341-
pub const fn to_bytes(self) -> [u8; 16] {
1342-
let c = self.facet_classid.to_le_bytes();
1343-
let t = &self.tiers;
1344-
[
1345-
c[0], c[1], c[2], c[3], t[0].lo, t[0].hi, t[1].lo, t[1].hi, t[2].lo, t[2].hi, t[3].lo,
1346-
t[3].hi, t[4].lo, t[4].hi, t[5].lo, t[5].hi,
1347-
]
1348-
}
1349-
1350-
/// The `hi`-byte chain, coarse→fine — one hierarchy (part_of / group / column /
1351-
/// centroid-hi, per the consumer).
1352-
#[inline]
1353-
#[must_use]
1354-
pub const fn hi_chain(self) -> [u8; 6] {
1355-
let t = &self.tiers;
1356-
[t[0].hi, t[1].hi, t[2].hi, t[3].hi, t[4].hi, t[5].hi]
1357-
}
1358-
1359-
/// The `lo`-byte chain, coarse→fine — the orthogonal hierarchy (is_a / member /
1360-
/// row / centroid-lo, per the consumer).
1361-
#[inline]
1362-
#[must_use]
1363-
pub const fn lo_chain(self) -> [u8; 6] {
1364-
let t = &self.tiers;
1365-
[t[0].lo, t[1].lo, t[2].lo, t[3].lo, t[4].lo, t[5].lo]
1366-
}
1367-
1368-
/// Shared coarse→fine prefix length (0..=6) of two 6-byte chains.
1369-
const fn shared(a: [u8; 6], b: [u8; 6]) -> u8 {
1370-
let mut n = 0u8;
1371-
while (n as usize) < 6 && a[n as usize] == b[n as usize] {
1372-
n += 1;
1373-
}
1374-
n
1375-
}
1376-
1377-
/// `hi`-chain distance: `6 − shared hi-prefix` — locality along the `hi`
1378-
/// hierarchy (e.g. `part_of` place), orthogonal to [`lo_distance`](Self::lo_distance).
1379-
#[inline]
1380-
#[must_use]
1381-
pub const fn hi_distance(self, other: Self) -> u8 {
1382-
6 - Self::shared(self.hi_chain(), other.hi_chain())
1383-
}
1384-
1385-
/// `lo`-chain distance: `6 − shared lo-prefix` — locality along the orthogonal
1386-
/// `lo` hierarchy (e.g. `is_a` type), on the SAME facet.
1387-
#[inline]
1388-
#[must_use]
1389-
pub const fn lo_distance(self, other: Self) -> u8 {
1390-
6 - Self::shared(self.lo_chain(), other.lo_chain())
1391-
}
1392-
}
1216+
// `FacetTier` / `FacetCascade` live in the dedicated [`crate::facet`] module — a
1217+
// reusable, content-blind 8:8 substrate (a *reading* over borrowed bytes, NOT part of
1218+
// the locked node layout). Re-exported here for the historical `canonical_node` path.
1219+
pub use crate::facet::{FacetCascade, FacetTier};
13931220

13941221
impl NodeRow {
13951222
/// Read the [`KanbanTenant`] phase cursor from the [`ValueTenant::Kanban`]
@@ -1451,62 +1278,6 @@ impl NodeRow {
14511278
mod tests {
14521279
use super::*;
14531280

1454-
#[test]
1455-
fn facet_cascade_is_16_bytes_always_8_8_consumer_neutral() {
1456-
// 16-byte facet: facet_classid(4) | 6×(8:8)=12 (harvest §5.1). The bytes are
1457-
// content-blind — this test reads them as raw hi:lo, no part_of/is_a baked in.
1458-
assert_eq!(core::mem::size_of::<FacetCascade>(), 16);
1459-
assert_eq!(core::mem::size_of::<FacetTier>(), 2);
1460-
1461-
let bytes: [u8; 16] = [
1462-
0xEF, 0xBE, 0xAD, 0xDE, // facet_classid = 0xDEAD_BEEF (LE)
1463-
0x01, 0xAB, // tier0: lo=0x01, hi=0xAB
1464-
0x02, 0xCD, // tier1
1465-
0x03, 0xEF, // tier2
1466-
0x04, 0x12, // tier3
1467-
0x05, 0x34, // tier4
1468-
0x06, 0x56, // tier5
1469-
];
1470-
let f = FacetCascade::from_bytes(&bytes);
1471-
assert_eq!(f.facet_classid, 0xDEAD_BEEF);
1472-
// round-trip is exact (the substrate stores the 8:8 verbatim).
1473-
assert_eq!(f.to_bytes(), bytes);
1474-
1475-
// The two orthogonal chains: hi (one hierarchy) and lo (the other).
1476-
assert_eq!(f.hi_chain(), [0xAB, 0xCD, 0xEF, 0x12, 0x34, 0x56]);
1477-
assert_eq!(f.lo_chain(), [0x01, 0x02, 0x03, 0x04, 0x05, 0x06]);
1478-
1479-
// Consumer projections of ONE tier's 8:8 — concatenated u16 and Morton tile.
1480-
let t0 = f.tiers[0]; // hi=0xAB, lo=0x01
1481-
assert_eq!(t0.as_u16(), 0xAB01, "concatenated 16-bit reading");
1482-
// Morton: lo on even bits, hi on odd. Every nibble = 2 bits hi × 2 bits lo.
1483-
assert_eq!(t0.morton(), {
1484-
let spread = |x: u8| {
1485-
let mut v = x as u16;
1486-
v = (v | (v << 4)) & 0x0F0F;
1487-
v = (v | (v << 2)) & 0x3333;
1488-
v = (v | (v << 1)) & 0x5555;
1489-
v
1490-
};
1491-
spread(0x01) | (spread(0xAB) << 1)
1492-
});
1493-
// Morton de-interleaves back to the two bytes (lossless amortization).
1494-
assert_eq!(t0.morton() & 0x5555, FacetTier { lo: 0x01, hi: 0 }.morton());
1495-
1496-
// Distances are prefix metrics, orthogonal: same hi-chain, different lo-chain
1497-
// ⇒ hi_distance 0, lo_distance > 0 (and vice-versa).
1498-
let same_hi_diff_lo = FacetCascade::from_bytes(&{
1499-
let mut b = bytes;
1500-
b[4] = 0x99; // tier0 lo (the fine end of the lo chain... actually coarsest)
1501-
b
1502-
});
1503-
assert_eq!(f.hi_distance(same_hi_diff_lo), 0, "hi chain unchanged");
1504-
assert!(
1505-
f.lo_distance(same_hi_diff_lo) > 0,
1506-
"lo chain diverges at tier0"
1507-
);
1508-
}
1509-
15101281
#[test]
15111282
fn kanban_tenant_round_trip_and_field_isolation() {
15121283
let mut row = NodeRow {

0 commit comments

Comments
 (0)