Skip to content

Commit 5961623

Browse files
committed
contract+rbac+ogar: mint-forward compat reader — the P0 sweep missed rbac.rs
Follow-up on the CanonHigh flip: three sites derived classid halves outside the one flippable composition and surfaced only when the flip landed — - contract rbac.rs ClassGrant::permits used 'class as u16' (the codex-P2 collapse pattern). Now routes classid_canon_compat. - lance-graph-rbac AuthProvider::classid() hand-widened u32::from(concept). Now routes contract render_classid(0x0000, concept) -> 0x0B01_0000-form. - lance-graph-ogar AUTH_*_CID / test ENCOUNTER literals in the pre-flip 0x0000_DDCC form. Now const-composed via contract render_classid. New: ogar_codebook::classid_canon_compat — the mint-forward CANON reader for surfaces serving BOTH stored forms (RBAC grants, un-re-baked corpora): active canon when plausible (>= 0x0100 && != 0x1000), legacy-order fallback otherwise; the canon slot exactly 0x1000 (domain-0x10 root) stays reserved-unusable until marker retirement (P4). RBAC authorizes pre-flip persisted rows without re-bake; both-forms grant test added. Board: EPIPHANIES E-CLASSID-COMPAT-READER (same commit). Gates: contract 774 (guid-v3-tail), rbac 30, ogar 81; clippy; fmt. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 804422b commit 5961623

6 files changed

Lines changed: 127 additions & 38 deletions

File tree

.claude/board/EPIPHANIES.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,29 @@
1+
## 2026-07-02 — E-CLASSID-COMPAT-READER — the P0 sweep missed rbac.rs: `class as u16` in ClassGrant::permits; fixed via a mint-forward compat reader
2+
3+
**Status:** SHIPPED (follow-up on the P1 flip commit, PR #628 arc).
4+
5+
**Correction:** the D-CCF-0 route-through sweep covered ogar_codebook /
6+
hhtl / canonical_node but NOT `rbac.rs` — `ClassGrant::permits` derived its
7+
class discriminator via `class as u16` (the exact codex-P2 collapse
8+
pattern), and `lance-graph-rbac AuthProvider::classid()` hand-widened
9+
`u32::from(class_id)`, and `lance-graph-ogar` carried `0x0000_0B01`-form
10+
literals. All three surfaced only when the flip landed.
11+
12+
**Mechanism added:** `ogar_codebook::classid_canon_compat` — the
13+
mint-forward CANON reader for surfaces that must serve BOTH stored forms
14+
(RBAC grant matching, reads over un-re-baked corpora): active canon when
15+
plausible (`>= 0x0100 && != 0x1000`), legacy-order fallback otherwise.
16+
Sound because real render prefixes are §2-allocated (`0x0000..0x0007`) and
17+
the marker is `0x1000`; documented limitation — the future canon slot
18+
exactly `0x1000` (domain-0x10 root) stays reserved-unusable until marker
19+
retirement (P4). RBAC now authorizes pre-flip persisted rows without
20+
re-bake (fails OPEN-to-correct-concept, never collapses classes).
21+
22+
**Lesson (Rule-7 adjacent):** a route-through sweep's own coverage claim
23+
needs the same exhaustive-grep declaration as a negative-existence claim —
24+
"all sites routed" was asserted from the plan's §2 inventory, not from a
25+
whole-crate `as u16`/`u32::from` sweep.
26+
127
## 2026-07-02 — E-CLASSID-FLIP-P1-LANDED — CanonHigh is live: canon HIGH / custom LOW, legacy aliases resolve persisted rows, OGAR#95 reconciled as the custom-half render catalogue
228

329
**Status:** SHIPPED (PR #628 arc; P0 route-through fd9bf6b → P1 flip this commit).

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

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,34 @@ pub const fn classid_custom(classid: u32) -> u16 {
358358
split_classid(classid).1
359359
}
360360

361+
/// **Mint-forward CANON reader** for surfaces that must serve BOTH stored
362+
/// forms — RBAC grant matching, read paths over corpora not yet re-baked to
363+
/// the post-flip order. Strict new-form-only surfaces use [`classid_canon`].
364+
///
365+
/// Returns the canon half under the active order when it is a *plausible*
366+
/// canon — a `0xDDCC` codebook id has domain byte `>= 0x01`, and the canon
367+
/// half never carries the `0x1000` V3 marker — otherwise re-reads the id
368+
/// under the legacy [`CanonLow`](ClassidOrder::CanonLow) order (where every
369+
/// pre-flip form keeps its canon in the LOW half: core `0x0000_0901`, render
370+
/// `0x0005_0901`, V3 `0x1000_0700` all resolve their true canon).
371+
///
372+
/// Documented limitation: a future canon exactly equal to `0x1000` (the
373+
/// domain-root slot of the currently-Unassigned domain `0x10`) would be
374+
/// indistinguishable from the V3 marker under this heuristic — that slot is
375+
/// reserved-unusable until the marker retires (plan §4 P4).
376+
#[inline]
377+
#[must_use]
378+
pub const fn classid_canon_compat(classid: u32) -> u16 {
379+
let (canon, custom) = split_classid(classid);
380+
if canon >= 0x0100 && canon != 0x1000 {
381+
canon
382+
} else if custom != 0 {
383+
split_classid_with(ClassidOrder::CanonLow, classid).0
384+
} else {
385+
canon
386+
}
387+
}
388+
361389
/// Recompose a classid under the OTHER order — the flip itself. Involutive:
362390
/// `flip_classid(flip_classid(x)) == x` (probed below).
363391
#[inline]
@@ -832,6 +860,20 @@ mod tests {
832860
}
833861
}
834862

863+
#[test]
864+
fn classid_canon_compat_reads_both_stored_forms() {
865+
// New-form ids: compat == strict canon.
866+
for id in [0x0901_0005u32, 0x0701_1000, 0x0102_0001, 0x0700_0000] {
867+
assert_eq!(classid_canon_compat(id), classid_canon(id));
868+
}
869+
// Persisted pre-flip forms resolve their true canon via the legacy
870+
// fallback: core, render, and V3-marked shapes.
871+
assert_eq!(classid_canon_compat(0x0000_0901), 0x0901); // legacy core
872+
assert_eq!(classid_canon_compat(0x0005_0901), 0x0901); // legacy render
873+
assert_eq!(classid_canon_compat(0x1000_0700), 0x0700); // legacy V3
874+
assert_eq!(classid_canon_compat(0x0000_0000), 0x0000); // default class
875+
}
876+
835877
#[test]
836878
fn no_class_collapse_under_canon_high() {
837879
// codex P2 (#627): post-flip, a naive `as u16` reads the CUSTOM half —

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

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,8 @@ impl ScopeSpec {
9696
}
9797

9898
/// The codebook class identity an authorization targets — the
99-
/// [`NodeGuid`](crate::NodeGuid) `classid` (or its low-`u16` codebook id widened).
99+
/// [`NodeGuid`](crate::NodeGuid) `classid` (its canon half is the codebook id;
100+
/// compose via [`render_classid`](crate::ogar_codebook::render_classid)).
100101
/// Opaque to the kernel: it is compared and looked up, never decoded (the kernel
101102
/// "never touches a token" — only resolved keys go inward).
102103
pub type ClassId = u32;
@@ -244,12 +245,13 @@ impl OpMask {
244245
/// text` blob (keystone §6 / I-K0 registry axiom: "decisions key on `classid`,
245246
/// not on text"). A role's `granted` value-tenant is a `&[ClassGrant]`.
246247
///
247-
/// `target_classid` is the **low `u16` codebook id** (the shared-concept half of
248-
/// a [`NodeGuid`](crate::NodeGuid)'s `classid`) — the RBAC + ontology identity,
249-
/// app-render-skin-independent (the hi `u16` chooses render, never grants).
248+
/// `target_classid` is the **CANON `u16` codebook id** (the shared-concept half
249+
/// of a [`NodeGuid`](crate::NodeGuid)'s `classid` — the HIGH u16 since the
250+
/// 2026-07-02 half-order flip) — the RBAC + ontology identity,
251+
/// app-render-skin-independent (the custom half chooses render, never grants).
250252
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, PartialOrd, Ord, Hash)]
251253
pub struct ClassGrant {
252-
/// The class this grant targets (low-`u16` codebook id).
254+
/// The class this grant targets (canon-`u16` codebook id).
253255
pub target_classid: u16,
254256
/// The verbs this grant permits on that class.
255257
pub op_mask: OpMask,
@@ -266,14 +268,19 @@ impl ClassGrant {
266268
}
267269
}
268270

269-
/// Whether this grant permits `op` on `class`. Matches on the **low `u16`**
270-
/// of `class` (the codebook id), so a grant authored against the shared
271-
/// concept applies regardless of which app's render-skin (hi `u16`) the
272-
/// `ClassId` carries.
271+
/// Whether this grant permits `op` on `class`. Matches on the **CANON
272+
/// half** of `class` (the codebook id) via the mint-forward compat reader
273+
/// [`classid_canon_compat`](crate::ogar_codebook::classid_canon_compat) —
274+
/// a grant authored against the shared concept applies regardless of
275+
/// which app's render-skin (custom half) the `ClassId` carries, AND
276+
/// regardless of whether the id is a post-flip (canon HIGH) or persisted
277+
/// pre-flip (canon LOW) stored form. Never `class as u16` — post-flip
278+
/// that reads the custom half and collapses every class (codex P2 #627).
273279
#[inline]
274280
#[must_use]
275281
pub fn permits(&self, class: ClassId, op: &Operation<'_>) -> bool {
276-
self.target_classid == (class as u16) && self.op_mask.permits(op)
282+
self.target_classid == crate::ogar_codebook::classid_canon_compat(class)
283+
&& self.op_mask.permits(op)
277284
}
278285
}
279286

@@ -344,21 +351,27 @@ mod tests {
344351
}
345352

346353
#[test]
347-
fn class_grant_matches_on_low_u16_codebook_id() {
354+
fn class_grant_matches_on_canon_codebook_id() {
348355
let grant = ClassGrant::new(0x0901, OpMask::READ.union(OpMask::ACT));
349-
// Same concept, different app render-skin (hi u16) → still permitted:
350-
// the grant keys on the shared-concept low u16, never the render half.
351-
let app_a: ClassId = 0x0000_0901;
352-
let app_b: ClassId = 0xAB12_0901;
353356
let read = Operation::Read {
354357
depth: PrefetchDepth::Identity,
355358
};
356-
assert!(grant.permits(app_a, &read));
357-
assert!(grant.permits(app_b, &read));
358-
// Wrong concept → denied even with the verb.
359+
// Same concept, different app render-skin (custom half) → still
360+
// permitted: the grant keys on the shared-concept CANON half, never
361+
// the render half. Post-flip forms: concept HIGH, any prefix LOW.
362+
assert!(grant.permits(0x0901_0000, &read)); // core lens
363+
assert!(grant.permits(0x0901_0005, &read)); // Healthcare lens
364+
assert!(grant.permits(0x0901_AB12, &read)); // arbitrary custom half
365+
// Mint-forward: persisted PRE-flip forms (canon LOW, §2-allocated
366+
// prefixes < 0x0100 or the 0x1000 V3 marker) still match through the
367+
// compat reader — a re-bake is never required for authorization.
368+
assert!(grant.permits(0x0000_0901, &read)); // legacy core
369+
assert!(grant.permits(0x0005_0901, &read)); // legacy Healthcare render
370+
// Wrong concept → denied even with the verb (both forms).
371+
assert!(!grant.permits(0x0902_0000, &read));
359372
assert!(!grant.permits(0x0000_0902, &read));
360373
// Right concept, ungranted verb → denied.
361-
assert!(!grant.permits(app_a, &Operation::Write { predicate: "due" }));
374+
assert!(!grant.permits(0x0901_0000, &Operation::Write { predicate: "due" }));
362375
}
363376

364377
/// A typed [`ClassRbac`] impl whose `grant_permits` body IS [`grants_permit`]

crates/lance-graph-ogar/src/actions.rs

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -107,16 +107,19 @@ const AUTH_ZITADEL_ACTIONS: &[ActionDef] = &[
107107
},
108108
];
109109

110-
// The auth-family codebook ids (keystone §7 `0x0B` core domain). Written as
111-
// literals — NOT `ogar_vocab::class_ids::AUTH_STORE` — so this DO-arm provider is
112-
// strictly `lance_graph_contract`-dependent and does not couple to whichever
113-
// `ogar-vocab` git ref this crate pins (the action manifest is a contract-shaped
114-
// artifact, exactly as `contract::action::ClassActions` documents — "generated
115-
// downstream; the Core provides the type"). They MUST equal
116-
// `ogar_vocab::class_ids::{AUTH_STORE, AUTH_ZITADEL}`; the lib's `parity` guard is
117-
// what binds the codebook itself.
118-
const AUTH_STORE_CID: u32 = 0x0000_0B01;
119-
const AUTH_ZITADEL_CID: u32 = 0x0000_0B02;
110+
// The auth-family codebook ids (keystone §7 `0x0B` core domain). Concept
111+
// slots are literals — NOT `ogar_vocab::class_ids::AUTH_STORE` — so this
112+
// DO-arm provider is strictly `lance_graph_contract`-dependent and does not
113+
// couple to whichever `ogar-vocab` git ref this crate pins (the action
114+
// manifest is a contract-shaped artifact, exactly as
115+
// `contract::action::ClassActions` documents — "generated downstream; the
116+
// Core provides the type"). They MUST equal
117+
// `ogar_vocab::class_ids::{AUTH_STORE, AUTH_ZITADEL}`; the lib's `parity`
118+
// guard is what binds the codebook itself. The FULL-classid widening routes
119+
// through the contract's one flippable composition (core render lens,
120+
// prefix 0x0000) — canon HIGH since the 2026-07-02 half-order flip.
121+
const AUTH_STORE_CID: u32 = contract::render_classid(0x0000, 0x0B01);
122+
const AUTH_ZITADEL_CID: u32 = contract::render_classid(0x0000, 0x0B02);
120123

121124
/// The registry: one [`ClassActions`] row per class with a DO surface. Seeded
122125
/// with the auth family (the worked hardcoded-RBAC example); other domains append

crates/lance-graph-ogar/src/rbac_impl.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,9 @@ mod tests {
8686
}
8787
}
8888

89-
const ENCOUNTER: ClassId = 0x0000_0901;
89+
// Full classid via the contract's flippable core-render composition
90+
// (canon 0x0901 HIGH since the 2026-07-02 flip).
91+
const ENCOUNTER: ClassId = lance_graph_contract::render_classid(0x0000, 0x0901);
9092

9193
fn fixture() -> OgarRbac<FixtureGrants> {
9294
OgarRbac::new(FixtureGrants {

crates/lance-graph-rbac/src/auth.rs

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -84,13 +84,15 @@ impl AuthProvider {
8484
.find(|p| p.class_id() == id)
8585
}
8686

87-
/// As a full 32-bit `ClassId` (hi-`u16` core prefix `0x0000`, lo-`u16`
88-
/// concept) — the form [`authorize`](crate::authorize::authorize) and the
89-
/// `NodeGuid` classid take. Auth concepts are core (cross-app), so the
90-
/// render prefix is `0x0000`.
87+
/// As a full 32-bit `ClassId` under the core render lens (concept in the
88+
/// CANON high `u16` since the 2026-07-02 half-order flip, prefix `0x0000`
89+
/// in the custom low half) — the form
90+
/// [`authorize`](crate::authorize::authorize) and the `NodeGuid` classid
91+
/// take. Auth concepts are core (cross-app). Routed through the
92+
/// contract's one flippable composition — never a local widening.
9193
#[must_use]
9294
pub fn classid(self) -> ClassId {
93-
u32::from(self.class_id())
95+
lance_graph_contract::render_classid(0x0000, self.class_id())
9496
}
9597

9698
/// The claim-key grammar for this provider — which claim names carry the
@@ -204,8 +206,9 @@ mod tests {
204206
assert_eq!(AuthProvider::Zitadel.class_id(), 0x0B02);
205207
assert_eq!(AuthProvider::Zanzibar.class_id(), 0x0B03);
206208
assert_eq!(AuthProvider::OryKeto.class_id(), 0x0B04);
207-
// Full classid is core-prefixed (hi u16 = 0x0000 — auth is cross-app).
208-
assert_eq!(AuthProvider::Store.classid(), 0x0000_0B01);
209+
// Full classid under the core lens: concept in the CANON high u16
210+
// (post-flip form), custom prefix 0x0000 — auth is cross-app.
211+
assert_eq!(AuthProvider::Store.classid(), 0x0B01_0000);
209212
// Round-trips.
210213
for p in [
211214
AuthProvider::Store,
@@ -244,7 +247,7 @@ mod tests {
244247
assert!(id.has_role("physician"));
245248
assert!(!id.has_role("admin"));
246249
assert_eq!(id.tenant.as_deref(), Some("clinic-7"));
247-
assert_eq!(id.auth_classid(), 0x0000_0B02);
250+
assert_eq!(id.auth_classid(), 0x0B02_0000);
248251
}
249252

250253
#[test]

0 commit comments

Comments
 (0)