Skip to content

Commit 3b5aea0

Browse files
committed
contract: D-CCF-0 — the one flippable classid composition, routed through, zero behavior
ClassidOrder {CanonLow, CanonHigh} + CLASSID_ORDER (P0-pinned CanonLow) + compose_classid[_with] / split_classid[_with] / classid_canon / classid_custom / flip_classid in ogar_codebook (the module that already owned the unnamed split — wire, don't invent). Routed through the ONE definition (behavior-identical under CanonLow, probed): classid_concept_domain, classid_concept, classid_app_prefix, render_classid, hhtl::NiblePath::from_guid_prefix's custom-half-zero guard, and the carve-matrix EntityType discriminator stamps (now classid_canon(...) — the codex-P2 class-collapse guard; 'as u16' on a classid is the forbidden pattern). Probes: split-compose round-trip both orders; flip involution over all wired + post-flip ids; the legacy-boundary matrix (every routed reader == its direct mask for every codebook id under every prefix); the no-class-collapse probe (post-flip canon halves 0x0701/0x0A01/0x0E01 distinct while naive 'as u16' collapses all three to 0x1000). Gates: contract 773 w/ v2+v3 features (14 new), 759 default, doctests green, clippy -D warnings clean, fmt clean. Plan: classid-canon-custom-flip-v1.md P0. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 69bd68e commit 3b5aea0

3 files changed

Lines changed: 237 additions & 21 deletions

File tree

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2374,7 +2374,9 @@ mod tests {
23742374
// read it back — on the V3 class it is still the Osint concept 0x0700
23752375
// (the high-u16 gen-marker never leaks into the entity discriminator).
23762376
let o = ValueTenant::EntityType.value_offset();
2377-
row.value[o..o + 2].copy_from_slice(&(NodeGuid::CLASSID_OSINT_V3 as u16).to_le_bytes());
2377+
row.value[o..o + 2].copy_from_slice(
2378+
&crate::ogar_codebook::classid_canon(NodeGuid::CLASSID_OSINT_V3).to_le_bytes(),
2379+
);
23782380
let et = u16::from_le_bytes([row.value[o], row.value[o + 1]]);
23792381
assert_eq!(
23802382
et, 0x0700,
@@ -2484,13 +2486,17 @@ mod tests {
24842486
// cold classes too — Anatomy 0x0A01 / Genetics root 0x0E00, never the
24852487
// 0x1000 gen-marker.
24862488
let o = ValueTenant::EntityType.value_offset();
2487-
row.value[o..o + 2].copy_from_slice(&(NodeGuid::CLASSID_FMA_V3 as u16).to_le_bytes());
2489+
row.value[o..o + 2].copy_from_slice(
2490+
&crate::ogar_codebook::classid_canon(NodeGuid::CLASSID_FMA_V3).to_le_bytes(),
2491+
);
24882492
assert_eq!(
24892493
u16::from_le_bytes([row.value[o], row.value[o + 1]]),
24902494
0x0A01,
24912495
"EntityType tenant carries the canon Anatomy concept"
24922496
);
2493-
row.value[o..o + 2].copy_from_slice(&(NodeGuid::CLASSID_CPIC_V3 as u16).to_le_bytes());
2497+
row.value[o..o + 2].copy_from_slice(
2498+
&crate::ogar_codebook::classid_canon(NodeGuid::CLASSID_CPIC_V3).to_le_bytes(),
2499+
);
24942500
assert_eq!(
24952501
u16::from_le_bytes([row.value[o], row.value[o + 1]]),
24962502
0x0E00,

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

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -293,20 +293,23 @@ impl NiblePath {
293293
#[must_use]
294294
pub const fn from_guid_prefix(guid: &crate::canonical_node::NodeGuid) -> Option<Self> {
295295
let parts = guid.decode();
296-
// In THIS v1 fold the high 4 classid nibbles must be zero — it folds
297-
// classid_lo as the coarse tier, so a nonzero high u16 would make the
296+
// In THIS v1 fold the CUSTOM half must be zero — it folds the CANON
297+
// half as the coarse tier, so a nonzero custom half would make the
298298
// 20→16 nibble fold lossy. It is reported, not silently re-routed. (The
299299
// v3 fold does NOT fold classid — see from_guid_prefix_v3 — so this is a
300-
// v1-fold constraint, not a global reserved-zero law.)
301-
if (parts.classid >> 16) != 0 {
300+
// v1-fold constraint, not a global reserved-zero law.) Halves come from
301+
// the one flippable split (D-CCF-0) — identical to the historical
302+
// `classid >> 16` / `& 0xFFFF` masks while CLASSID_ORDER is CanonLow.
303+
let (canon, custom) = crate::ogar_codebook::split_classid(parts.classid);
304+
if custom != 0 {
302305
return None;
303306
}
304307
// Pack root-first into 16 nibbles = 64 bits = the full u64 path:
305-
// nibbles 0..4 (high) = classid_lo (basin = top nibble of classid_lo)
308+
// nibbles 0..4 (high) = canon half (basin = top nibble of canon)
306309
// nibbles 4..8 = HEEL
307310
// nibbles 8..12 = HIP
308311
// nibbles 12..16 (low) = TWIG (leaf = low nibble of TWIG)
309-
let classid_lo = (parts.classid & 0xFFFF) as u64;
312+
let classid_lo = canon as u64;
310313
let path = (classid_lo << 48)
311314
| ((parts.heel as u64) << 32)
312315
| ((parts.hip as u64) << 16)

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

Lines changed: 219 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -107,15 +107,18 @@ pub fn canonical_concept_domain(id: u16) -> ConceptDomain {
107107
}
108108

109109
/// Resolve a [`NodeGuid`](crate::NodeGuid) `classid` to its [`ConceptDomain`] (D-OVC-4). The
110-
/// codebook id is the low 16 bits of the classid (`0xDDCC` lives in the low u16);
111-
/// the high u16 is the canon-reserved zero-fallback prefix. So a domain route is
112-
/// `canonical_concept_domain(classid as u16)`. This is the coarse sibling of the
110+
/// codebook id is the CANON half of the classid (under the active
111+
/// [`CLASSID_ORDER`] — the low u16 while the order is `CanonLow`); the other
112+
/// half is the custom/render prefix. So a domain route is
113+
/// `canonical_concept_domain(classid_canon(classid))`. This is the coarse sibling of the
113114
/// per-family scope in [`codebook`](crate::codebook): classid (domain) selects the
114115
/// coarse codebook; `family` selects the sub-codebook (longest-prefix-wins).
115116
#[inline]
116117
#[must_use]
117118
pub fn classid_concept_domain(classid: u32) -> ConceptDomain {
118-
canonical_concept_domain(classid as u16)
119+
// Routes the CANON half via the one flippable split (D-CCF-0) — identical
120+
// to the historical `classid as u16` while CLASSID_ORDER is CanonLow.
121+
canonical_concept_domain(classid_canon(classid))
119122
}
120123

121124
/// Map a coarse curator `source_domain` tag (`"project"`, `"erp"`, `"german-erp"`)
@@ -233,7 +236,12 @@ impl AppPrefix {
233236
#[inline]
234237
#[must_use]
235238
pub const fn render_classid(prefix: u16, concept: u16) -> u32 {
236-
((prefix as u32) << 16) | (concept as u32)
239+
// The prefix is the CUSTOM half, the concept the CANON half — composed
240+
// through the one flippable definition (D-CCF-0): identical to the
241+
// historical `(prefix << 16) | concept` while CLASSID_ORDER is CanonLow.
242+
// The OGAR#95 hi-u16 scheme ↔ CanonHigh reconciliation is the plan's P2
243+
// operator checkpoint; this route-through is what makes it one-place.
244+
compose_classid(concept, prefix)
237245
}
238246

239247
/// Compose a render `classid` from an [`AppPrefix`] and a **canonical-concept
@@ -254,23 +262,121 @@ pub fn render_classid_for_concept(app: AppPrefix, concept: &str) -> Option<u32>
254262
canonical_concept_id(concept).map(|id| app.render(id))
255263
}
256264

257-
/// The APP / render-prefix half of a full `classid` (`classid >> 16`). Mirror
258-
/// of OGAR `ogar_vocab::app::app_of`. Pair with [`AppPrefix::from_prefix`] to
259-
/// recover the typed app.
265+
// ═══════════════════════════════════════════════════════════════════════════
266+
// The ONE flippable classid composition (D-CCF-0, `classid-canon-custom-flip-v1`)
267+
// ═══════════════════════════════════════════════════════════════════════════
268+
269+
/// Which u16 half of a stored `classid: u32` carries the CANON (`domain:appid`
270+
/// / concept) and which carries the CUSTOM (render prefix / the temporary
271+
/// `0x1000` V3 marker). This is the operator's "split order that later you can
272+
/// flip" made a type (`E-CLASSID-SPLIT-ORDER-IS-A-FLIP`): the Canon:Custom
273+
/// half-order migration (`.claude/plans/classid-canon-custom-flip-v1.md`,
274+
/// TRIGGERED 2026-07-02) is a one-place change of [`CLASSID_ORDER`], never
275+
/// per-site byte surgery.
276+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
277+
pub enum ClassidOrder {
278+
/// Legacy order: canon in the LOW u16, custom in the HIGH (the `0xDDCC`
279+
/// low-half convention every wired classid uses today).
280+
CanonLow,
281+
/// Target order: canon in the HIGH u16, custom in the LOW — stored
282+
/// `0x0701_1000`, human-readable `0x07:01::1000` (plan §0).
283+
CanonHigh,
284+
}
285+
286+
/// The active half-order. **P0 pins the legacy order** — every route-through
287+
/// below is behavior-identical to the direct masks it replaces (probed).
288+
/// Flipping this const to [`CanonHigh`](ClassidOrder::CanonHigh) IS Phase 1 of
289+
/// the migration; it is mint-forward with a version boundary — flipping alone
290+
/// must never reinterpret persisted ids (the registry keeps concrete-keyed
291+
/// legacy aliases; plan §4 P3, codex P2 on #627).
292+
pub const CLASSID_ORDER: ClassidOrder = ClassidOrder::CanonLow;
293+
294+
/// Compose a classid under an explicit half-order.
295+
#[inline]
296+
#[must_use]
297+
pub const fn compose_classid_with(order: ClassidOrder, canon: u16, custom: u16) -> u32 {
298+
match order {
299+
ClassidOrder::CanonLow => ((custom as u32) << 16) | (canon as u32),
300+
ClassidOrder::CanonHigh => ((canon as u32) << 16) | (custom as u32),
301+
}
302+
}
303+
304+
/// Split a classid under an explicit half-order → `(canon, custom)`.
305+
#[inline]
306+
#[must_use]
307+
pub const fn split_classid_with(order: ClassidOrder, classid: u32) -> (u16, u16) {
308+
match order {
309+
ClassidOrder::CanonLow => (classid as u16, (classid >> 16) as u16),
310+
ClassidOrder::CanonHigh => ((classid >> 16) as u16, classid as u16),
311+
}
312+
}
313+
314+
/// Compose under the active [`CLASSID_ORDER`].
315+
#[inline]
316+
#[must_use]
317+
pub const fn compose_classid(canon: u16, custom: u16) -> u32 {
318+
compose_classid_with(CLASSID_ORDER, canon, custom)
319+
}
320+
321+
/// Split under the active [`CLASSID_ORDER`] → `(canon, custom)`.
322+
#[inline]
323+
#[must_use]
324+
pub const fn split_classid(classid: u32) -> (u16, u16) {
325+
split_classid_with(CLASSID_ORDER, classid)
326+
}
327+
328+
/// The CANON half under the active order — **the** source of the SoA
329+
/// `class_id`/`EntityType` discriminator. Post-flip, a naive `classid as u16`
330+
/// yields the CUSTOM half (`0x1000`) for every V3 class — total class
331+
/// collapse (codex P2 on #627) — so deriving a class discriminator any other
332+
/// way is a forbidden pattern.
333+
#[inline]
334+
#[must_use]
335+
pub const fn classid_canon(classid: u32) -> u16 {
336+
split_classid(classid).0
337+
}
338+
339+
/// The CUSTOM half under the active order (render prefix / marker).
340+
#[inline]
341+
#[must_use]
342+
pub const fn classid_custom(classid: u32) -> u16 {
343+
split_classid(classid).1
344+
}
345+
346+
/// Recompose a classid under the OTHER order — the flip itself. Involutive:
347+
/// `flip_classid(flip_classid(x)) == x` (probed below).
348+
#[inline]
349+
#[must_use]
350+
pub const fn flip_classid(classid: u32) -> u32 {
351+
let (canon, custom) = split_classid(classid);
352+
let other = match CLASSID_ORDER {
353+
ClassidOrder::CanonLow => ClassidOrder::CanonHigh,
354+
ClassidOrder::CanonHigh => ClassidOrder::CanonLow,
355+
};
356+
compose_classid_with(other, canon, custom)
357+
}
358+
359+
/// The APP / render-prefix half of a full `classid` — since #627, the CUSTOM
360+
/// half under the active [`CLASSID_ORDER`] (identical to the historical
361+
/// `classid >> 16` while the order is [`CanonLow`](ClassidOrder::CanonLow)).
362+
/// Mirror of OGAR `ogar_vocab::app::app_of`. Pair with
363+
/// [`AppPrefix::from_prefix`] to recover the typed app.
260364
#[inline]
261365
#[must_use]
262366
pub const fn classid_app_prefix(classid: u32) -> u16 {
263-
(classid >> 16) as u16
367+
classid_custom(classid)
264368
}
265369

266-
/// The canonical concept-id half of a full `classid` (`classid as u16`) — the
267-
/// shared RBAC + ontology + cross-app identity key, identical under every
370+
/// The canonical concept-id half of a full `classid` — since #627, the CANON
371+
/// half under the active [`CLASSID_ORDER`] (identical to the historical
372+
/// `classid as u16` while the order is [`CanonLow`](ClassidOrder::CanonLow)) —
373+
/// the shared RBAC + ontology + cross-app identity key, identical under every
268374
/// render prefix. Mirror of OGAR `ogar_vocab::app::concept_of`; the sibling of
269375
/// [`classid_concept_domain`], which routes this half to its [`ConceptDomain`].
270376
#[inline]
271377
#[must_use]
272378
pub const fn classid_concept(classid: u32) -> u16 {
273-
classid as u16
379+
classid_canon(classid)
274380
}
275381

276382
/// The curated `(canonical_concept, u16)` codebook — wire-compatible mirror of
@@ -620,4 +726,105 @@ mod tests {
620726
None
621727
);
622728
}
729+
730+
// ── D-CCF-0 probes — the one flippable classid composition ────────────
731+
732+
#[test]
733+
fn classid_split_compose_round_trips_under_both_orders() {
734+
let samples: &[(u16, u16)] = &[
735+
(0x0700, 0x0000), // legacy OSINT domain classid halves
736+
(0x0701, 0x1000), // post-flip OSINT:q2 halves
737+
(0x0A01, 0x1000),
738+
(0x0E01, 0x1000),
739+
(0x0901, 0x0005), // Healthcare render pair
740+
(0x0000, 0x0000),
741+
(0xFFFF, 0xFFFF),
742+
];
743+
for &(canon, custom) in samples {
744+
for order in [ClassidOrder::CanonLow, ClassidOrder::CanonHigh] {
745+
let id = compose_classid_with(order, canon, custom);
746+
assert_eq!(
747+
split_classid_with(order, id),
748+
(canon, custom),
749+
"split∘compose must be identity under {order:?}"
750+
);
751+
}
752+
}
753+
}
754+
755+
#[test]
756+
fn classid_flip_is_involutive_and_p0_pins_legacy_order() {
757+
// P0 pin: the active order is the legacy CanonLow — flipping this
758+
// const IS the migration's Phase 1, never a drive-by.
759+
assert_eq!(CLASSID_ORDER, ClassidOrder::CanonLow);
760+
// flip(flip(x)) == x over every wired classid + the post-flip trio.
761+
for id in [
762+
0x0000_0700u32, // legacy OSINT domain class
763+
0x1000_0700, // pre-flip OSINT-V3
764+
0x1000_0A01,
765+
0x1000_0E00,
766+
0x0701_1000, // post-flip forms (already valid u32s to flip back)
767+
0x0A01_1000,
768+
0x0E01_1000,
769+
0x0005_0901, // Healthcare render classid
770+
0x0000_0000,
771+
0xFFFF_FFFF,
772+
] {
773+
assert_eq!(
774+
flip_classid(flip_classid(id)),
775+
id,
776+
"flip must be involutive"
777+
);
778+
}
779+
}
780+
781+
#[test]
782+
fn classid_route_through_is_behavior_identical_under_legacy_order() {
783+
// The legacy-boundary matrix (plan §3): under CanonLow every routed
784+
// reader equals the direct mask it replaced, for every codebook id
785+
// under every app prefix — the P0 zero-behavior gate.
786+
for &(_, concept) in CODEBOOK {
787+
for prefix in [0x0000u16, 0x0001, 0x0005, 0x1000] {
788+
let id = render_classid(prefix, concept);
789+
assert_eq!(id, ((prefix as u32) << 16) | (concept as u32));
790+
assert_eq!(classid_concept(id), concept);
791+
assert_eq!(classid_app_prefix(id), prefix);
792+
assert_eq!(classid_canon(id), id as u16);
793+
assert_eq!(classid_custom(id), (id >> 16) as u16);
794+
assert_eq!(
795+
classid_concept_domain(id),
796+
canonical_concept_domain(concept),
797+
"domain routing invariant under the route-through"
798+
);
799+
}
800+
}
801+
}
802+
803+
#[test]
804+
fn no_class_collapse_under_canon_high() {
805+
// codex P2 (#627): post-flip, a naive `as u16` reads the CUSTOM half —
806+
// 0x1000 for ALL three V3 classes — collapsing the SoA class_id
807+
// discriminator. The canon half stays distinct; `as u16` does not.
808+
let osint = compose_classid_with(ClassidOrder::CanonHigh, 0x0701, 0x1000);
809+
let fma = compose_classid_with(ClassidOrder::CanonHigh, 0x0A01, 0x1000);
810+
let cpic = compose_classid_with(ClassidOrder::CanonHigh, 0x0E01, 0x1000);
811+
assert_eq!((osint, fma, cpic), (0x0701_1000, 0x0A01_1000, 0x0E01_1000));
812+
813+
let canons = [
814+
split_classid_with(ClassidOrder::CanonHigh, osint).0,
815+
split_classid_with(ClassidOrder::CanonHigh, fma).0,
816+
split_classid_with(ClassidOrder::CanonHigh, cpic).0,
817+
];
818+
assert_eq!(
819+
canons,
820+
[0x0701, 0x0A01, 0x0E01],
821+
"canon halves stay distinct"
822+
);
823+
// The forbidden pattern, demonstrated: `as u16` collapses all three.
824+
assert_eq!(
825+
[osint as u16, fma as u16, cpic as u16],
826+
[0x1000, 0x1000, 0x1000],
827+
"naive `as u16` post-flip = total class collapse (why it is forbidden)"
828+
);
829+
}
623830
}

0 commit comments

Comments
 (0)