@@ -124,19 +124,62 @@ Memorize these. They appear across every consumer doc; recognizing them on sight
124124
125125Every consumer code path is one of these four. Pattern-match before you write.
126126
127+ > ** Two canonical paths — spine vs membrane.** Every pattern below has
128+ > two forms based on the BBB-barrier: ** spine-internal** crates
129+ > (` lance-graph-* ` , ` ogar-vocab ` , ` ogar-ontology ` , ` ogar-class-view ` )
130+ > freely depend on ` ogar-vocab ` and use the typed ` *Port::* ` surface;
131+ > ** membrane / customer binaries** (` woa-rs ` , ` smb-office-rs ` ,
132+ > ` medcare-realtime ` ) are restricted to ` lance-graph-contract ` only and
133+ > use the wire-compat mirror ` lance_graph_contract::ogar_codebook::* ` .
134+ > Both return identical classids — the choice is about dep-tree posture,
135+ > not concept identity. The barrier is enforced by each consumer's
136+ > CLAUDE.md allow-list (e.g. woa-rs Iron Rule 1, smb-office-rs Iron
137+ > Rule 3). Wire-compat is pinned by parity tests on the contract side.
138+ >
139+ > | Crate type | Allowed OGAR deps | Canonical lookup path |
140+ > | ---| ---| ---|
141+ > | spine-internal | ` ogar-vocab ` · ` ogar-ontology ` · ` lance-graph-* ` | ` ogar_vocab::ports::*Port ` |
142+ > | membrane / customer binary (BBB) | ` lance-graph-contract ` only | ` lance_graph_contract::ogar_codebook ` |
143+
127144### Pattern 1 — pull a classid (the codebook lookup)
128145
129- The canonical concept ID, from the consumer's PortSpec, via static
130- function call. ** No registry, no hydration, no bridge.**
146+ The canonical concept ID, via pure static function call. ** No registry,
147+ no hydration, no bridge.** Two paths — pick by your crate's BBB posture.
148+
149+ #### Pattern 1a — spine-internal (lance-graph-* + OGAR-internal)
131150
132151``` rust
133- // ✅ CANONICAL — direct PortSpec lookup
152+ // CANONICAL — direct PortSpec lookup, full typed-port surface
134153use ogar_vocab :: ports :: {HealthcarePort , PortSpec };
135154
136155let cid : Option <u16 > = HealthcarePort :: class_id (" Patient" );
137156// → Some(0x0901)
138157```
139158
159+ When: your crate is INSIDE the spine — ` lance-graph-* ` , ` ogar-vocab ` ,
160+ ` ogar-ontology ` , ` ogar-class-view ` , or another OGAR-internal crate.
161+
162+ #### Pattern 1b — membrane / customer binary (BBB-barrier)
163+
164+ ``` rust
165+ // CANONICAL — wire-compat mirror, BBB-safe (zero ogar-vocab dep)
166+ use lance_graph_contract :: ogar_codebook :: canonical_concept_id;
167+
168+ let cid : Option <u16 > = canonical_concept_id (" patient" );
169+ // → Some(0x0901)
170+ ```
171+
172+ When: your crate is BEHIND the BBB-barrier (` woa-rs ` , ` smb-office-rs ` ,
173+ ` medcare-realtime ` , any customer binary). Per the consumer's allow-list,
174+ ` lance-graph-ogar ` / ` ogar-vocab ` are ** forbidden** deps; you depend on
175+ ` lance-graph-contract ` only. The contract's ` ogar_codebook ` mirrors the
176+ canonical codebook wire-compat (zero-dep, parity-tested against OGAR per
177+ the ` canonical_concept_name ` precedent — OGAR #98 — and the APP-prefix
178+ mirror — lance-graph #592 ).
179+
180+ Both 1a and 1b return ` 0x0901 ` . The choice is which crate's ** dep tree**
181+ you're inside, not which classid you pull.
182+
140183| Consumer | Port | Example call | Returns |
141184| ---| ---| ---| ---|
142185| medcare-rs | ` HealthcarePort ` | ` ::class_id("Patient") ` | ` Some(0x0901) ` |
@@ -150,28 +193,29 @@ let cid: Option<u16> = HealthcarePort::class_id("Patient");
150193| (any Redmine consumer) | ` RedminePort ` | ` ::class_id("Issue") ` | ` Some(0x0102) ` |
151194
152195``` rust
153- // ❌ ANTI — go through the deprecated bridge layer
196+ // ANTI — go through the deprecated bridge layer
154197use lance_graph_ogar :: bridges :: HealthcarePort ; // ← works, but extra hop;
155198 // migrate to ogar_vocab::ports
156199
157- // ❌ ANTI — re-mint the canonical id locally
200+ // ANTI — re-mint the canonical id locally
158201const PATIENT_CLASSID : u16 = 0x0901 ; // ← bypasses PortSpec;
159202 // loses the alias-table mapping
160203
161- // ❌ ANTI — construct a UnifiedBridge to ask the same question
204+ // ANTI — construct a UnifiedBridge to ask the same question
162205let b = MedcareBridge :: new (registry )? ; // ← deprecated alias; round-trip via
163206let ent = b . entity (" Patient" )? ; // bridge + registry just to recover
164207let cid = ent . schema_ptr. entity_type_id (); // what PortSpec gives in one call
165208```
166209
167210### Pattern 2 — compose a render classid (concept ‖ APP prefix)
168211
169- Once you have the concept's low u16 and want the ** per-app render
170- address** (for ClassView dispatch, template selection, SoA row layout),
171- stamp the app's ` APP_PREFIX ` via OGAR #97 's typed helper.
212+ Stamp the per-app render prefix on the concept. Same spine-vs-membrane
213+ split as Pattern 1.
214+
215+ #### Pattern 2a — spine-internal (OGAR #97 typed helper)
172216
173217``` rust
174- // ✅ CANONICAL — typed APP_PREFIX from the PortSpec
218+ // CANONICAL — typed APP_PREFIX from the PortSpec
175219use ogar_vocab :: ports :: {HealthcarePort , PortSpec };
176220
177221let cid : u16 = HealthcarePort :: class_id (" Patient" ). unwrap (); // 0x0901
@@ -199,15 +243,55 @@ OpenProjectPort::APP_PREFIX | 0x0102 → 0x0001_0102 // OpenProject WorkPacka
199243RedminePort :: APP_PREFIX | 0x0102 → 0x0007_0102 // Redmine Issue
200244```
201245
246+ #### Pattern 2b — membrane (BBB-safe, per lance-graph #592 )
247+
248+ ``` rust
249+ // CANONICAL — one-call lookup + stamp via the contract mirror
250+ use lance_graph_contract :: {AppPrefix , render_classid_for_concept};
251+
252+ let render : Option <u32 > = render_classid_for_concept (
253+ AppPrefix :: Healthcare ,
254+ " patient" ,
255+ );
256+ // → Some(0x0005_0901)
257+ ```
258+
259+ Or split (pull then stamp), for symmetry with Pattern 1b:
260+
261+ ``` rust
262+ use lance_graph_contract :: ogar_codebook :: {canonical_concept_id, AppPrefix };
263+
264+ let cid : u16 = canonical_concept_id (" patient" ). unwrap (); // 0x0901
265+ let render : u32 = AppPrefix :: Healthcare . render (cid ); // 0x0005_0901
266+ ```
267+
268+ ` AppPrefix ` is the OGAR #95 §2 allocation table mirrored into the
269+ contract as typed data (lance-graph #592 closed ` ISS-CONTRACT-APP-PREFIX-MIRROR ` ,
270+ following the OGAR #98 ` canonical_concept_name ` mirror precedent).
271+ Parity is pinned: the contract's ` app_prefixes_match_ogar_allocation_table `
272+ test fires the moment OGAR re-allocates a prefix. ** The membrane never
273+ hand-stamps ` 0x000N ` ** — both halves come from one source.
274+
275+ Worked examples (mirror of 2a, via the contract):
276+
277+ ``` rust
278+ AppPrefix :: Healthcare . render (0x0901 ) → 0x0005_0901 // Medcare patient
279+ AppPrefix :: Woa . render (0x0103 ) → 0x0003_0103 // WoA Stundenzettel
280+ AppPrefix :: Smb . render (0x0204 ) → 0x0004_0204 // SMB Kunde
281+ AppPrefix :: Odoo . render (0x0103 ) → 0x0002_0103 // Odoo HrAttendance
282+ AppPrefix :: OpenProject . render (0x0102 ) → 0x0001_0102 // OpenProject WorkPackage
283+ AppPrefix :: Redmine . render (0x0102 ) → 0x0007_0102 // Redmine Issue
284+ ```
285+
202286``` rust
203- // ❌ ANTI — hardcode the APP prefix as a magic constant
287+ // ANTI — hardcode the APP prefix as a magic constant
204288const MEDCARE_APP : u32 = 0x0005_0000 ; // ← drifts from PortSpec
205289let render = MEDCARE_APP | (cid as u32 ); // if APP allocation changes
206290
207- // ❌ ANTI — bit-shift inline
291+ // ANTI — bit-shift inline
208292let render = ((0x0005u32 ) << 16 ) | (cid as u32 ); // ← un-typed; lose source-of-truth
209293
210- // ❌ ANTI — store full u32 render classid where lo u16 would do (RBAC, ontology)
294+ // ANTI — store full u32 render classid where lo u16 would do (RBAC, ontology)
211295fn authorize (actor : & Actor , render_cid : u32 , op : Op ) { … }
212296// ^^^^^^^^^^^^ shared grant lattice keys on LO u16;
213297// passing the full u32 leaks render lens
@@ -221,7 +305,7 @@ The keystone `authorize(actor, classid, op)` is **[H]** and gated on
221305— do NOT re-introduce a bridge as a stopgap.
222306
223307``` rust
224- // ⏳ FUTURE — once lance-graph-rbac keystone ships:
308+ // FUTURE — once lance-graph-rbac keystone ships:
225309use lance_graph_rbac :: authorize;
226310
227311let concept : u16 = HealthcarePort :: class_id (" Patient" ). unwrap (); // 0x0901
@@ -231,7 +315,7 @@ let decision = authorize(&actor, concept, Op::Read);
231315```
232316
233317``` rust
234- // ✅ INTERIM — keep existing static_role / Policy / membrane gate
318+ // INTERIM — keep existing static_role / Policy / membrane gate
235319fn authorize_patient_read (actor_role : & str ) -> AccessDecision {
236320 let role_static = static_role (actor_role );
237321 // medcare-rbac::Policy check, OR MedCareMembraneGate, OR static role map
@@ -241,7 +325,7 @@ fn authorize_patient_read(actor_role: &str) -> AccessDecision {
241325```
242326
243327``` rust
244- // ❌ ANTI — re-introduce a UnifiedBridge to "auth gate" while waiting for keystone
328+ // ANTI — re-introduce a UnifiedBridge to "auth gate" while waiting for keystone
245329let bridge = MedcareBridge :: new (registry )? ; // ← deprecated; replaces one
246330let decision = bridge . authorize_read (" Patient" , & role ); // trap with another. Wait
247331 // for the real keystone.
@@ -364,7 +448,10 @@ is mandatory pre-read:
364448` ClassView ` · ` MedcareBridge ` · ` WoaBridge ` · ` SmbBridge ` · ` OdooBridge ` ·
365449` OpenProjectBridge ` · ` RedmineBridge ` · ` UnifiedBridge ` · ` ogar_vocab::ports ` ·
366450` lance_graph_ogar::bridges ` · ` render classid ` · ` concept classid ` ·
367- ` per-consumer bridge ` · ` consumer migration `
451+ ` per-consumer bridge ` · ` consumer migration ` ·
452+ ** BBB-barrier** · ` contract::ogar_codebook ` · ` canonical_concept_id ` ·
453+ ` AppPrefix ` · ` render_classid_for_concept ` · ` lance_graph_contract::ogar_codebook ` ·
454+ ` membrane consumer ` · ` spine vs membrane `
368455
369456** Agents that load it Tier-1** :
370457- ` core-first-architect ` · ` adapter-shaper ` · ` core-gap-auditor `
@@ -381,6 +468,13 @@ is mandatory pre-read:
381468- ` docs/OGAR-AST-CONTRACT.md ` — the canonical Class / ActionDef IR (what the address resolves to)
382469- ` docs/APP-CODEBOOK-MIGRATION-PLAN.md ` — the wave-ordered consumer migration plan (W0–W4)
383470- ** Parallel-session sibling** : ` lance-graph/.claude/knowledge/ogar-consumer-preflight.md `
384- (lance-graph #591 ) — the spine-side spellbook with the
385- ` ISS-CONTRACT-APP-PREFIX-MIRROR ` Core-gap entry; pin the same
386- address-vs-magic precision
471+ (lance-graph #591 ) — the spine-side spellbook that surfaced the
472+ ` ISS-CONTRACT-APP-PREFIX-MIRROR ` Core-gap and pinned the
473+ spine-vs-membrane BBB-barrier framing now reflected in §2.
474+ - ** Membrane mirror landed** : lance-graph #592 — ` lance_graph_contract::ogar_codebook `
475+ now carries ` AppPrefix ` , ` render_classid_for_concept ` ,
476+ ` classid_app_prefix ` , ` classid_concept ` (mirror of OGAR #97
477+ ` ogar_vocab::app ` , no ` ogar-vocab ` dep). This is what Pattern 1b/2b
478+ call into; the parity test
479+ ` app_prefixes_match_ogar_allocation_table ` fuses drift against OGAR
480+ ` PortSpec::APP_PREFIX ` .
0 commit comments