Skip to content

Commit a4f77df

Browse files
authored
Merge pull request #102 from AdaWorldAPI/claude/ogar-best-practices-bbb-barrier
docs(best-practices): §2 spine-vs-membrane split — close the lance-graph #592 coordination loop
2 parents c757c47 + aaf1f3f commit a4f77df

1 file changed

Lines changed: 114 additions & 20 deletions

File tree

docs/OGAR-CONSUMER-BEST-PRACTICES.md

Lines changed: 114 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -124,19 +124,62 @@ Memorize these. They appear across every consumer doc; recognizing them on sight
124124

125125
Every 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
134153
use ogar_vocab::ports::{HealthcarePort, PortSpec};
135154

136155
let 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
154197
use 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
158201
const 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
162205
let b = MedcareBridge::new(registry)?; // ← deprecated alias; round-trip via
163206
let ent = b.entity("Patient")?; // bridge + registry just to recover
164207
let 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
175219
use ogar_vocab::ports::{HealthcarePort, PortSpec};
176220

177221
let cid: u16 = HealthcarePort::class_id("Patient").unwrap(); // 0x0901
@@ -199,15 +243,55 @@ OpenProjectPort::APP_PREFIX | 0x0102 → 0x0001_0102 // OpenProject WorkPacka
199243
RedminePort::APP_PREFIX | 0x01020x0007_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
204288
const MEDCARE_APP: u32 = 0x0005_0000; // ← drifts from PortSpec
205289
let render = MEDCARE_APP | (cid as u32); // if APP allocation changes
206290

207-
// ANTI — bit-shift inline
291+
// ANTI — bit-shift inline
208292
let 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)
211295
fn 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:
225309
use lance_graph_rbac::authorize;
226310

227311
let 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
235319
fn 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
245329
let bridge = MedcareBridge::new(registry)?; // ← deprecated; replaces one
246330
let 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

Comments
 (0)