From 2fc2370af89cf0557f0876707d931099ed30a381 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 2 Jul 2026 05:43:41 +0000 Subject: [PATCH 1/2] =?UTF-8?q?vocab+mint:=20classid=20canon:custom=20half?= =?UTF-8?q?-order=20flip=20=E2=80=94=20canon=20HIGH=20/=20APP-render=20pre?= =?UTF-8?q?fix=20LOW?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lockstep flip with lance-graph-contract CLASSID_ORDER=CanonHigh (lance-graph #628; operator trigger 2026-07-02, the 0x07:01::1000 ruling): - ogar_vocab::app: render_classid now composes ((concept as u32) << 16) | prefix; app_of reads the low half, concept_of the high half. APP_PREFIX values unchanged (position moved to the LOW/custom half). - ogar-from-ruff mint + codegen emit: literal-pinned tests flipped (account.move -> 0x0202_0002, WorkPackage -> 0x0102_0001); decompose assertions route through app_of/concept_of instead of local masks. - ruff_spo_address::Facet needs NO companion change: facet_classid is an opaque LE u32 pass-through; the composition lives entirely here. - Doc sweep: APP-CLASS-CODEBOOK-LAYOUT (full layout spec), consumer best practices, migration plan, READMEs (en/de), PHILOSOPHY (en/de), transpile substrate, IR framing, RBAC keystone auth literals (0x0B01_0000-form), mailbox-integration mint guidance, CLAUDE.md non-negotiables. - Ledgers (append-only honored): DISCOVERY-MAP D-CLASSID-CANON-HIGH-FLIP + board EPIPHANIES E-CLASSID-CANON-HIGH-FLIP appended; D-APPCLASS and D-OSINT-APPID-NOT-CONCEPT entries preserved, superseded by citation. Legacy stored forms (0x0000_DDCC / 0xAAAA_DDCC / 0x1000_DDCC) persist in baked data and resolve via read-only legacy registry aliases upstream (mint-forward; retirement gated on corpus proof). Gates: workspace 35 test suites green (vart-gated crate tested via a local path override, reverted before commit — the git dep is canonical); fmt clean on touched crates. Co-Authored-By: Claude --- .claude/board/EPIPHANIES.md | 16 ++ CLAUDE.md | 19 +- README.de.md | 36 +-- README.md | 35 +-- crates/ogar-from-ruff/src/emit.rs | 154 +++++++++-- crates/ogar-from-ruff/src/mint.rs | 87 ++++--- crates/ogar-vocab/src/app.rs | 95 ++++--- crates/ogar-vocab/src/lib.rs | 6 +- crates/ogar-vocab/src/ports.rs | 10 +- docs/APP-CLASS-CODEBOOK-LAYOUT.md | 241 +++++++++--------- docs/APP-CODEBOOK-MIGRATION-PLAN.md | 65 ++--- docs/CLASSID-RBAC-KEYSTONE-SPEC.md | 18 +- docs/DISCOVERY-MAP.md | 42 +++ docs/FOUNDRY-ODOO-MARS-LENS.md | 6 +- docs/NODEGUID-CANON-AUDIT.md | 7 +- docs/ODOO-REDMINE-OPENPROJECT-LANDING.md | 8 +- docs/OGAR-AS-IR.md | 2 +- docs/OGAR-CONSUMER-BEST-PRACTICES.md | 139 +++++----- docs/OGAR-TRANSPILE-SUBSTRATE.md | 25 +- docs/PHILOSOPHIE.md | 22 +- docs/PHILOSOPHY.md | 21 +- docs/SURREAL-AST-TRAP-PREFLIGHT.md | 8 +- .../AR-OGAR-MAILBOX-INTEGRATION-PLAN.md | 19 +- 23 files changed, 678 insertions(+), 403 deletions(-) diff --git a/.claude/board/EPIPHANIES.md b/.claude/board/EPIPHANIES.md index 57d4c9f..7074bbb 100644 --- a/.claude/board/EPIPHANIES.md +++ b/.claude/board/EPIPHANIES.md @@ -7,6 +7,22 @@ --- +## 2026-07-02 — E-CLASSID-CANON-HIGH-FLIP — the composed classid half-order flips: canon concept HIGH, APP/render prefix LOW + +**Status:** FINDING (`[G]`, operator-triggered). Doc-sweep companion to `docs/DISCOVERY-MAP.md` D-CLASSID-CANON-HIGH-FLIP — read that entry for the full ledger; this entry records the same correction in the session-findings log. + +**The trigger:** the operator's `0x07:01::1000` mnemonic — read as `domain:appid::marker` — exposed that the working composed classid stored the APP/render prefix in the **high** u16 and the canonical concept in the **low** u16, backwards from the mnemonic's own read order (domain/concept first, appid second). + +**The ruling:** flip the composed order to `classid : u32 = [hi u16: canon concept][lo u16: APP/render prefix]`. `ogar_vocab::app::{render_classid, app_of, concept_of}` flip in lockstep with lance-graph-contract's `CLASSID_ORDER = CanonHigh` (PR #628 there) — `app_of` now reads `classid as u16`, `concept_of` now reads `classid >> 16`. **APP_PREFIX values are unchanged** (`0x0000` Core … `0x0007` Redmine) — only their bit position moves. V3 marker forms move in lockstep (`0x1000_0700` → `0x0701_1000`; FMA `0x1000_0A01` → `0x0A01_1000`; CPIC `0x1000_0E00` → `0x0E01_1000`, appid normalized `:00`→`:01`). Auth RBAC literals: `0x0000_0B0N` → `0x0B0N_0000` for N∈{1,2,3,4}. + +**Legacy is aliased, never rewritten.** Already-baked/persisted classids in the old order resolve via a read-only legacy registry alias (mint-forward doctrine; RESERVE-DON'T-RECLAIM held throughout). Retirement of the alias path is gated on a corpus proof, never assumed. + +**Supersedes, without editing:** D-APPCLASS (`docs/DISCOVERY-MAP.md`, 2026-06-22 — `classid = APP(hi u16) ‖ class(lo u16)`) and the `0x1000_0701` literal in D-OSINT-APPID-NOT-CONCEPT (same-day predecessor, 2026-07-02). Both entries stand as written per the append-only rule; this entry and its DISCOVERY-MAP twin are the correction of record for the half-order going forward. + +**Cross-ref:** `docs/DISCOVERY-MAP.md` D-CLASSID-CANON-HIGH-FLIP (canonical ledger entry); the doc-wide sweep landed the same session across `APP-CLASS-CODEBOOK-LAYOUT.md`, `APP-CODEBOOK-MIGRATION-PLAN.md`, `OGAR-CONSUMER-BEST-PRACTICES.md`, `OGAR-TRANSPILE-SUBSTRATE.md`, `OGAR-AS-IR.md`, `SURREAL-AST-TRAP-PREFLIGHT.md`, `NODEGUID-CANON-AUDIT.md`, `FOUNDRY-ODOO-MARS-LENS.md`, `CLASSID-RBAC-KEYSTONE-SPEC.md`, `ODOO-REDMINE-OPENPROJECT-LANDING.md`, `PHILOSOPHY.md`/`PHILOSOPHIE.md`, `README.md`/`README.de.md`, `integration/AR-OGAR-MAILBOX-INTEGRATION-PLAN.md` §7, and this repo's `CLAUDE.md`. + +--- + ## 2026-07-01 — E-OSINT-SUBSTRATE-CONVERGES-PER-SOA — the massive cognitive stack converges into the V3 2+14 tenant SoA; the dedup IS the convergence **Status:** FRAMING (`[G]` for the shipped crates + the tenant carve + the sole-writer canon E-CE64-MB-4; `[H]` for the convergence *program* — the deltas + baby-step probes P0–P8 are unrun). Operator-framed 2026-07-01 (*"massive codebase massive entropy … the V3 2+14 tenants converge the awareness massively per SoA"*). diff --git a/CLAUDE.md b/CLAUDE.md index a31ea72..89707f5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -211,14 +211,17 @@ alignment costs. Until measured: 3×4 stands. 10. `docs/OGAR-TRANSPILE-SUBSTRATE.md` — **the power, in one doc.** OGAR as the bidirectional per-class transpiler: pull-in (`source → ogar-from- → ModelGraph → lift + mint → CompiledClass`), - rail-facet addressing (`classid = (APP_PREFIX<<16)|concept`, the 16-byte - `FacetCascade`, cross-app convergence), pull-back (runtime wrapper - contract like `lance-graph-contract`, or codegen emit like + rail-facet addressing (`classid = (concept<<16)|APP_PREFIX`, the + 16-byte `FacetCascade`, cross-app convergence), pull-back (runtime + wrapper contract like `lance-graph-contract`, or codegen emit like `ogar-adapter-surrealql`), and the **85/15 split** (mechanical logic minted into OGAR; the "impossible" 15% = a per-language adapter + ClassView + ontological grounding). READ to understand why a consumer collapses to "a compiler-store caller + adapters, at the cost of an - import." Worked example: `account.move → 0x0002_0202`. + import." Worked example: `account.move → 0x0202_0002`. (Order + flipped 2026-07-02 — canon HIGH / custom LOW; pre-flip docs and + baked data use the legacy order — see `docs/DISCOVERY-MAP.md` + D-CLASSID-CANON-HIGH-FLIP.) 8. `docs/ARCHITECTURAL-DECISIONS-2026-06-04.md` — ADR-001..025 (ADR-026 pending). 9. `.claude/agents/` — the 5+3 hardening pattern (5 research savants + @@ -241,11 +244,13 @@ alignment costs. Until measured: 3×4 stands. resolution target:** before authoring consumer code that pulls a classid, composes a render address, authorizes, or migrates off a bridge, read `docs/OGAR-CONSUMER-BEST-PRACTICES.md` (the muscle- - memory guide). The hi u16 chooses **render** skin (per-app - ClassView/template), the lo u16 names the **shared concept** (RBAC + + memory guide). The lo u16 chooses **render** skin (per-app + ClassView/template), the hi u16 names the **shared concept** (RBAC + ontology); **neither half carries behavior** — class-magic (`ActionDef`+`KausalSpec`) is a property of the Core node the - address resolves to, never of the address. + address resolves to, never of the address. (Order flipped 2026-07-02 + — canon HIGH / custom LOW; pre-flip docs and baked data use the + legacy order — see `docs/DISCOVERY-MAP.md` D-CLASSID-CANON-HIGH-FLIP.) - **PII:** never emit German PII labels (medcare-rs leaf-rename at the adapter is the guarantee). Word-boundary abort-guard before commit. - **No model identifier** in any committed artifact (chat only). diff --git a/README.de.md b/README.de.md index da69c7f..791b745 100644 --- a/README.de.md +++ b/README.de.md @@ -8,18 +8,24 @@ Das [Active-Record-Muster](https://martinfowler.com/eaaCatalog/activeRecord.html (Martin Fowler, 2003) als kanonische 32-Bit-`classid`. Ein Codebook; jeder Consumer zieht, prägt niemals neu. +> Reihenfolge geflippt 2026-07-02 — Kanon HOCH / App-eigen NIEDRIG: das +> kanonische Konzept sitzt im hohen u16, der App-Render-Präfix im +> niedrigen u16. Material von vor dem Flip und bereits gebackene Daten +> nutzen die alte Reihenfolge; siehe `docs/DISCOVERY-MAP.md` +> D-CLASSID-CANON-HIGH-FLIP. + ```rust let cid: u16 = HealthcarePort::class_id("Patient"); // 0x0901 -let render = HealthcarePort::APP_PREFIX | (cid as u32); // 0x0005_0901 -authorize(actor, cid, Op::Read); // geteilter Grant auf lo u16 +let render = ((cid as u32) << 16) | HealthcarePort::APP_PREFIX; // 0x0901_0005 +authorize(actor, cid, Op::Read); // geteilter Grant auf hi u16 // // lokal anreichern — deins ``` ``` -classid : u32 = 0xAAAA ‖ 0xDDCC beide Hälften sind ADRESSE +classid : u32 = 0xDDCC ‖ 0xAAAA beide Hälften sind ADRESSE │ │ - │ └─ lo u16 — WELCHES KONZEPT (geteilte Identität) - └─────────── hi u16 — WESSEN RENDER (App-eigene Haut) + │ └─ lo u16 — WESSEN RENDER (App-eigene Haut) + └─────────── hi u16 — WELCHES KONZEPT (geteilte Identität) ──────► löst auf zu ──────► ├─ ClassView die HAUT (pro App) @@ -49,16 +55,16 @@ Prisma — gleiches Vokabular, ein IR. ## Render-Linse — ein Konzept, viele Apps -| classid | Konzept (lo u16) | Render (hi u16) | App | +| classid | Konzept (hi u16) | Render (lo u16) | App | |---------------|-----------------------------|--------------------------|----------------------| -| `0x0001_0102` | `0x0102` project_work_item | `0x0001` OpenProject | openproject-nexgen-rs | -| `0x0007_0102` | `0x0102` project_work_item | `0x0007` Redmine | (Consumer TBD) | -| `0x0005_0901` | `0x0901` patient | `0x0005` Medcare | medcare-rs | -| `0x0003_0103` | `0x0103` billable_work_entry | `0x0003` WoA | woa-rs | -| `0x0004_0103` | `0x0103` billable_work_entry | `0x0004` SMB | smb-office-rs | -| `0x0001_0103` | `0x0103` billable_work_entry | `0x0001` OpenProject | openproject-nexgen-rs | -| `0x0007_0103` | `0x0103` billable_work_entry | `0x0007` Redmine | (Consumer TBD) | -| `0x0002_0103` | `0x0103` billable_work_entry | `0x0002` Odoo | odoo-rs | +| `0x0102_0001` | `0x0102` project_work_item | `0x0001` OpenProject | openproject-nexgen-rs | +| `0x0102_0007` | `0x0102` project_work_item | `0x0007` Redmine | (Consumer TBD) | +| `0x0901_0005` | `0x0901` patient | `0x0005` Medcare | medcare-rs | +| `0x0103_0003` | `0x0103` billable_work_entry | `0x0003` WoA | woa-rs | +| `0x0103_0004` | `0x0103` billable_work_entry | `0x0004` SMB | smb-office-rs | +| `0x0103_0001` | `0x0103` billable_work_entry | `0x0001` OpenProject | openproject-nexgen-rs | +| `0x0103_0007` | `0x0103` billable_work_entry | `0x0007` Redmine | (Consumer TBD) | +| `0x0103_0002` | `0x0103` billable_work_entry | `0x0002` Odoo | odoo-rs | Gleiche `0x0103` → gleicher RBAC-Grant, gleiche Ontologie, gleiche Cross-Fork-Identität. Fünf Apps rendern *abrechenbare Zeit* auf fünf @@ -70,7 +76,7 @@ Arten; Planer-Stunden stimmen mit der Abrechnung per Codebook-Lookup | Zug | Aufruf | Deiner? | |-----------|--------------------------------------------|-----------| | pull | `Port::class_id(name) -> Option` | nein — reine Funktion | -| render | `Port::APP_PREFIX \| (cid as u32)` | nein — typisierter Helfer | +| render | `((cid as u32) << 16) \| Port::APP_PREFIX` | nein — typisierter Helfer | | authorize | `authorize(actor, cid, op)` ² | nein — geteiltes Grant-Gitter | | enrich | deine Domänen-Logik, an `cid` verschlüsselt | **ja** | diff --git a/README.md b/README.md index 5faadbd..f253276 100644 --- a/README.md +++ b/README.md @@ -8,18 +8,23 @@ The [Active Record pattern](https://martinfowler.com/eaaCatalog/activeRecord.htm (Martin Fowler, 2003) as a canonical 32-bit `classid`. One codebook; every consumer pulls, never re-mints. +> Order flipped 2026-07-02 — canon HIGH / custom LOW: the canonical +> concept sits in the high u16, the per-app render prefix sits in the +> low u16. Pre-flip material and already-baked data use the legacy +> order; see `docs/DISCOVERY-MAP.md` D-CLASSID-CANON-HIGH-FLIP. + ```rust let cid: u16 = HealthcarePort::class_id("Patient"); // 0x0901 -let render = HealthcarePort::APP_PREFIX | (cid as u32); // 0x0005_0901 -authorize(actor, cid, Op::Read); // shared grant on lo u16 +let render = ((cid as u32) << 16) | HealthcarePort::APP_PREFIX; // 0x0901_0005 +authorize(actor, cid, Op::Read); // shared grant on hi u16 // // enrich locally — yours ``` ``` -classid : u32 = 0xAAAA ‖ 0xDDCC both halves are ADDRESS +classid : u32 = 0xDDCC ‖ 0xAAAA both halves are ADDRESS │ │ - │ └─ lo u16 — WHICH CONCEPT (shared identity) - └─────────── hi u16 — WHOSE RENDER (per-app skin) + │ └─ lo u16 — WHOSE RENDER (per-app skin) + └─────────── hi u16 — WHICH CONCEPT (shared identity) ──────► resolves to ──────► ├─ ClassView the SKIN (per-app) @@ -49,16 +54,16 @@ Django, Prisma — same vocabulary, one IR. ## Render lens — one concept, many apps -| classid | concept (lo u16) | render (hi u16) | app | +| classid | concept (hi u16) | render (lo u16) | app | |---------------|-----------------------------|--------------------------|----------------------| -| `0x0001_0102` | `0x0102` project_work_item | `0x0001` OpenProject | openproject-nexgen-rs | -| `0x0007_0102` | `0x0102` project_work_item | `0x0007` Redmine | (consumer TBD) | -| `0x0005_0901` | `0x0901` patient | `0x0005` Medcare | medcare-rs | -| `0x0003_0103` | `0x0103` billable_work_entry | `0x0003` WoA | woa-rs | -| `0x0004_0103` | `0x0103` billable_work_entry | `0x0004` SMB | smb-office-rs | -| `0x0001_0103` | `0x0103` billable_work_entry | `0x0001` OpenProject | openproject-nexgen-rs | -| `0x0007_0103` | `0x0103` billable_work_entry | `0x0007` Redmine | (consumer TBD) | -| `0x0002_0103` | `0x0103` billable_work_entry | `0x0002` Odoo | odoo-rs | +| `0x0102_0001` | `0x0102` project_work_item | `0x0001` OpenProject | openproject-nexgen-rs | +| `0x0102_0007` | `0x0102` project_work_item | `0x0007` Redmine | (consumer TBD) | +| `0x0901_0005` | `0x0901` patient | `0x0005` Medcare | medcare-rs | +| `0x0103_0003` | `0x0103` billable_work_entry | `0x0003` WoA | woa-rs | +| `0x0103_0004` | `0x0103` billable_work_entry | `0x0004` SMB | smb-office-rs | +| `0x0103_0001` | `0x0103` billable_work_entry | `0x0001` OpenProject | openproject-nexgen-rs | +| `0x0103_0007` | `0x0103` billable_work_entry | `0x0007` Redmine | (consumer TBD) | +| `0x0103_0002` | `0x0103` billable_work_entry | `0x0002` Odoo | odoo-rs | Same `0x0103` → same RBAC grant, same ontology, same cross-fork identity. Five apps render *billable time* five ways; planner hours @@ -69,7 +74,7 @@ align with billing by codebook lookup, not translation. | Move | Call | Yours? | |-----------|--------------------------------------------|-----------| | pull | `Port::class_id(name) -> Option` | no — pure function | -| render | `Port::APP_PREFIX \| (cid as u32)` | no — typed helper | +| render | `((cid as u32) << 16) \| Port::APP_PREFIX` | no — typed helper | | authorize | `authorize(actor, cid, op)` ² | no — shared grant lattice | | enrich | your domain logic keyed on `cid` | **yes** | diff --git a/crates/ogar-from-ruff/src/emit.rs b/crates/ogar-from-ruff/src/emit.rs index d573066..5823aaf 100644 --- a/crates/ogar-from-ruff/src/emit.rs +++ b/crates/ogar-from-ruff/src/emit.rs @@ -108,12 +108,57 @@ fn escape_ident(name: &str, lang: Lang) -> String { fn is_rust_keyword(s: &str) -> bool { matches!( s, - "as" | "break" | "const" | "continue" | "crate" | "dyn" | "else" | "enum" | "extern" - | "false" | "fn" | "for" | "if" | "impl" | "in" | "let" | "loop" | "match" | "mod" - | "move" | "mut" | "pub" | "ref" | "return" | "self" | "Self" | "static" | "struct" - | "super" | "trait" | "true" | "type" | "unsafe" | "use" | "where" | "while" - | "async" | "await" | "abstract" | "become" | "box" | "do" | "final" | "macro" - | "override" | "priv" | "typeof" | "unsized" | "virtual" | "yield" | "try" | "gen" + "as" | "break" + | "const" + | "continue" + | "crate" + | "dyn" + | "else" + | "enum" + | "extern" + | "false" + | "fn" + | "for" + | "if" + | "impl" + | "in" + | "let" + | "loop" + | "match" + | "mod" + | "move" + | "mut" + | "pub" + | "ref" + | "return" + | "self" + | "Self" + | "static" + | "struct" + | "super" + | "trait" + | "true" + | "type" + | "unsafe" + | "use" + | "where" + | "while" + | "async" + | "await" + | "abstract" + | "become" + | "box" + | "do" + | "final" + | "macro" + | "override" + | "priv" + | "typeof" + | "unsized" + | "virtual" + | "yield" + | "try" + | "gen" ) } @@ -122,16 +167,83 @@ fn is_rust_keyword(s: &str) -> bool { fn is_csharp_keyword(s: &str) -> bool { matches!( s, - "abstract" | "as" | "base" | "bool" | "break" | "byte" | "case" | "catch" | "char" - | "checked" | "class" | "const" | "continue" | "decimal" | "default" | "delegate" - | "do" | "double" | "else" | "enum" | "event" | "explicit" | "extern" | "false" - | "finally" | "fixed" | "float" | "for" | "foreach" | "goto" | "if" | "implicit" - | "in" | "int" | "interface" | "internal" | "is" | "lock" | "long" | "namespace" - | "new" | "null" | "object" | "operator" | "out" | "override" | "params" | "private" - | "protected" | "public" | "readonly" | "ref" | "return" | "sbyte" | "sealed" - | "short" | "sizeof" | "stackalloc" | "static" | "string" | "struct" | "switch" - | "this" | "throw" | "true" | "try" | "typeof" | "uint" | "ulong" | "unchecked" - | "unsafe" | "ushort" | "using" | "virtual" | "void" | "volatile" | "while" + "abstract" + | "as" + | "base" + | "bool" + | "break" + | "byte" + | "case" + | "catch" + | "char" + | "checked" + | "class" + | "const" + | "continue" + | "decimal" + | "default" + | "delegate" + | "do" + | "double" + | "else" + | "enum" + | "event" + | "explicit" + | "extern" + | "false" + | "finally" + | "fixed" + | "float" + | "for" + | "foreach" + | "goto" + | "if" + | "implicit" + | "in" + | "int" + | "interface" + | "internal" + | "is" + | "lock" + | "long" + | "namespace" + | "new" + | "null" + | "object" + | "operator" + | "out" + | "override" + | "params" + | "private" + | "protected" + | "public" + | "readonly" + | "ref" + | "return" + | "sbyte" + | "sealed" + | "short" + | "sizeof" + | "stackalloc" + | "static" + | "string" + | "struct" + | "switch" + | "this" + | "throw" + | "true" + | "try" + | "typeof" + | "uint" + | "ulong" + | "unchecked" + | "unsafe" + | "ushort" + | "using" + | "virtual" + | "void" + | "volatile" + | "while" ) } @@ -418,7 +530,7 @@ mod tests { let rust = emit_rust(cc); // The rail address travels as a const. - assert!(rust.contains("pub const ACCOUNT_MOVE_CLASSID: u32 = 0x00020202;")); + assert!(rust.contains("pub const ACCOUNT_MOVE_CLASSID: u32 = 0x02020002;")); // The struct is PascalCase. assert!(rust.contains("pub struct AccountMove {")); // Typed scalar: char -> OgStr. @@ -490,7 +602,7 @@ mod tests { let rust = emit_rust(cc); assert!( - rust.contains("pub const WORK_PACKAGE_CLASSID: u32 = 0x00010102;"), + rust.contains("pub const WORK_PACKAGE_CLASSID: u32 = 0x01020001;"), "got:\n{rust}" ); // exercises the screaming_snake PascalCase fix below assert!(rust.contains("pub struct WorkPackage {")); @@ -522,7 +634,7 @@ mod tests { // The rail address travels as a const inside the record. assert!( - cs.contains("public const uint ClassId = 0x00020202;"), + cs.contains("public const uint ClassId = 0x02020002;"), "got:\n{cs}" ); // The type is a PascalCase sealed record. @@ -572,7 +684,7 @@ mod tests { // The rail address travels as a ClassVar. assert!( - py.contains("CLASSID: ClassVar[int] = 0x00020202"), + py.contains("CLASSID: ClassVar[int] = 0x02020002"), "got:\n{py}" ); // A PascalCase @dataclass. @@ -617,7 +729,7 @@ mod tests { assert!(src.contains("ToMany"), "ToMany in every emitter"); // The same rail classid concept in every emitter. assert!( - src.contains("0x00020202"), + src.contains("0x02020002"), "classid travels in every emitter" ); } diff --git a/crates/ogar-from-ruff/src/mint.rs b/crates/ogar-from-ruff/src/mint.rs index 8008255..bef15af 100644 --- a/crates/ogar-from-ruff/src/mint.rs +++ b/crates/ogar-from-ruff/src/mint.rs @@ -20,19 +20,20 @@ //! (intrusive / stateful logic) stays a per-language adapter + ClassView. //! ``` //! -//! The classid is the cross-app join key: the **low u16** is the shared -//! concept ([`PortSpec::class_id`] → the OGAR codebook), the **high u16** is -//! the app render prefix ([`PortSpec::APP_PREFIX`]). So `account.move` lands -//! as `0x0002_0202` under [`OdooPort`](ogar_vocab::ports::OdooPort) and an -//! OpenProject `WorkPackage` lands as `0x0001_0102` under +//! The classid is the cross-app join key: the **high u16** (CANON, since the +//! 2026-07-02 half-order flip) is the shared concept ([`PortSpec::class_id`] +//! → the OGAR codebook), the **low u16** is the app render prefix +//! ([`PortSpec::APP_PREFIX`]). So `account.move` lands as `0x0202_0002` +//! under [`OdooPort`](ogar_vocab::ports::OdooPort) and an OpenProject +//! `WorkPackage` lands as `0x0102_0001` under //! [`OpenProjectPort`](ogar_vocab::ports::OpenProjectPort) — different render //! skins, the same conceptual identity where they converge. +use ogar_vocab::Class; use ogar_vocab::app::render_classid_for; use ogar_vocab::ports::PortSpec; -use ogar_vocab::Class; -use ruff_spo_address::{mint_with_classid, Facet, Mint}; -use ruff_spo_triplet::{expand, ModelGraph}; +use ruff_spo_address::{Facet, Mint, mint_with_classid}; +use ruff_spo_triplet::{ModelGraph, expand}; use crate::{lift_model_graph, lift_model_graph_python}; @@ -91,8 +92,8 @@ pub fn compile_graph_python(graph: &ModelGraph) -> Vec(graph: &ModelGraph) -> Vec { @@ -113,7 +114,8 @@ pub fn compile_graph_ruby(graph: &ModelGraph) -> Vec /// /// The IRI is `:` or `:.`; members inherit /// their class's id, so we resolve the *model*'s concept and compose the -/// render classid `(APP_PREFIX << 16) | concept`. Odoo models arrive +/// render classid `((concept as u32) << 16) | APP_PREFIX` (canon HIGH since +/// the 2026-07-02 flip). Odoo models arrive /// underscore-normalised in the IRI (`account_move`) while ports key on the /// dotted name (`account.move`), so we try the dotted form first, then the /// raw form (mirrors `od-ontology`'s `concept_classid`). An unmapped model @@ -199,12 +201,16 @@ mod tests { "amount_total projects into computed_fields" ); - // The rail facet carries the canonical render classid — Odoo prefix - // 0x0002 | commercial_document concept 0x0202. - assert_eq!(cc.facet.facet_classid(), 0x0002_0202); - assert_eq!(cc.facet.facet_classid() & 0xFFFF, 0x0202, "shared concept"); + // The rail facet carries the canonical render classid — + // commercial_document concept 0x0202 (canon HIGH) | Odoo prefix 0x0002. + assert_eq!(cc.facet.facet_classid(), 0x0202_0002); assert_eq!( - (cc.facet.facet_classid() >> 16) as u16, + ogar_vocab::app::concept_of(cc.facet.facet_classid()), + 0x0202, + "shared concept" + ); + assert_eq!( + ogar_vocab::app::app_of(cc.facet.facet_classid()), OdooPort::APP_PREFIX, "Odoo render prefix", ); @@ -217,7 +223,7 @@ mod tests { // Members are addressed within their class: same render classid. for node in ["odoo:account_move", "odoo:account_move.partner_id"] { let f = mint.facet(node).unwrap_or_else(|| panic!("{node} mints")); - assert_eq!(f.facet_classid(), 0x0002_0202, "{node} classid"); + assert_eq!(f.facet_classid(), 0x0202_0002, "{node} classid"); } } @@ -225,18 +231,22 @@ mod tests { fn mint_is_port_agnostic_same_concept_different_render_prefix() { // The mint doesn't care about the source language — only the port // lens. An OpenProject WorkPackage graph minted through OpenProjectPort - // lands as 0x0001_0102 (OpenProject prefix | project_work_item), the - // SAME low-u16 concept Odoo/Redmine converge on, a different prefix. + // lands as 0x0102_0001 (project_work_item canon | OpenProject prefix), + // the SAME canon concept Odoo/Redmine converge on, a different prefix. let mut graph = ModelGraph::new("openproject"); graph.models.push(Model::new("WorkPackage")); let mint = mint_graph::(&graph); let facet = mint .facet("openproject:WorkPackage") .expect("WorkPackage mints"); - assert_eq!(facet.facet_classid(), 0x0001_0102); - assert_eq!(facet.facet_classid() & 0xFFFF, 0x0102, "project_work_item"); + assert_eq!(facet.facet_classid(), 0x0102_0001); + assert_eq!( + ogar_vocab::app::concept_of(facet.facet_classid()), + 0x0102, + "project_work_item" + ); assert_eq!( - (facet.facet_classid() >> 16) as u16, + ogar_vocab::app::app_of(facet.facet_classid()), OpenProjectPort::APP_PREFIX, ); } @@ -299,11 +309,15 @@ mod tests { "belongs_to :project projects into an association" ); - // OpenProject prefix 0x0001 | project_work_item concept 0x0102. - assert_eq!(cc.facet.facet_classid(), 0x0001_0102); - assert_eq!(cc.facet.facet_classid() & 0xFFFF, 0x0102, "shared concept"); + // project_work_item concept 0x0102 (canon HIGH) | OpenProject prefix 0x0001. + assert_eq!(cc.facet.facet_classid(), 0x0102_0001); + assert_eq!( + ogar_vocab::app::concept_of(cc.facet.facet_classid()), + 0x0102, + "shared concept" + ); assert_eq!( - (cc.facet.facet_classid() >> 16) as u16, + ogar_vocab::app::app_of(cc.facet.facet_classid()), OpenProjectPort::APP_PREFIX, "OpenProject render prefix", ); @@ -313,8 +327,8 @@ mod tests { fn openproject_and_redmine_compile_to_the_same_concept_different_render_skin() { // The literal "one canonical concept, two render skins" proof: the // SAME Rails-shaped ModelGraph (WorkPackage / Issue) minted through - // each fork's port converges on the identical low-u16 concept and - // diverges only on the high-u16 app-render prefix. + // each fork's port converges on the identical CANON concept and + // diverges only on the app-render prefix (the CUSTOM/low half). let op_graph = work_package_graph("openproject", "WorkPackage"); let rm_graph = work_package_graph("redmine", "Issue"); @@ -324,14 +338,19 @@ mod tests { let op_id = op_compiled[0].facet.facet_classid(); let rm_id = rm_compiled[0].facet.facet_classid(); - assert_eq!(op_id & 0xFFFF, rm_id & 0xFFFF, "shared concept converges"); - assert_eq!(op_id & 0xFFFF, 0x0102, "project_work_item"); + use ogar_vocab::app::{app_of, concept_of}; + assert_eq!( + concept_of(op_id), + concept_of(rm_id), + "shared concept converges" + ); + assert_eq!(concept_of(op_id), 0x0102, "project_work_item"); assert_ne!( - (op_id >> 16) as u16, - (rm_id >> 16) as u16, + app_of(op_id), + app_of(rm_id), "render prefixes diverge (OpenProject vs Redmine skin)" ); - assert_eq!((op_id >> 16) as u16, OpenProjectPort::APP_PREFIX); - assert_eq!((rm_id >> 16) as u16, RedminePort::APP_PREFIX); + assert_eq!(app_of(op_id), OpenProjectPort::APP_PREFIX); + assert_eq!(app_of(rm_id), RedminePort::APP_PREFIX); } } diff --git a/crates/ogar-vocab/src/app.rs b/crates/ogar-vocab/src/app.rs index f78fff8..57929e5 100644 --- a/crates/ogar-vocab/src/app.rs +++ b/crates/ogar-vocab/src/app.rs @@ -1,65 +1,76 @@ -//! **APP‖class composition** — the high-u16 render-prefix machinery +//! **class‖APP composition** — the render-prefix machinery //! (`docs/APP-CLASS-CODEBOOK-LAYOUT.md` §0, §4). //! -//! A full 32-bit `classid` is two orthogonal halves: +//! A full 32-bit `classid` is two orthogonal halves. Since the 2026-07-02 +//! canon:custom half-order flip (lance-graph +//! `classid-canon-custom-flip-v1.md` P1/P2 — the operator's `0x07:01::1000` +//! ruling), the CANON concept sits HIGH and the APP/render prefix LOW: //! //! ```text -//! classid : u32 = [ hi u16 : APP / render prefix ] [ lo u16 : concept ] -//! 0xAAAA (per-app ClassView lens) 0xDDCC (shared RBAC+ontology) +//! classid : u32 = [ hi u16 : CANON concept ] [ lo u16 : APP / render prefix ] +//! 0xDDCC (shared RBAC+ontology) 0xAAAA (per-app ClassView lens) //! ``` //! //! The per-port reserved prefix is [`PortSpec::APP_PREFIX`] (the §2 allocation -//! table as typed data); this module composes and decomposes the full id so -//! consumers re-export ONE source of the bit math instead of each -//! re-implementing `(prefix << 16) | concept`. `0x0000` is the shared -//! canonical core (every existing id is `0x0000_DDCC` — additive, invariant -//! I-APP1). +//! table as typed data; the prefix VALUES are order-invariant); this module +//! composes and decomposes the full id so consumers re-export ONE source of +//! the bit math instead of each re-implementing +//! `((concept as u32) << 16) | prefix`. `0x0000` is the shared canonical core +//! (every existing id renders under the core lens as `0xDDCC_0000` — +//! additive, invariant I-APP1). Pre-flip stored ids (`0x0000_DDCC` / +//! `0xAAAA_DDCC`) are the LEGACY order — readers of persisted data resolve +//! them through concrete legacy-alias registry keys, never by +//! reinterpreting the halves (mint-forward). //! -//! Composing the high half is **reserving/stamping**, not minting a class_id -//! (§2: "reserving costs nothing") — concept (low-u16) mints stay gated on the -//! 5+3 codebook pass. The low half is the cross-app currency: concept/domain -//! routing reads it alone (`lance_graph_contract::classid_concept_domain` -//! does `… as u16`), so it is identical under every render prefix. +//! Composing the prefix half is **reserving/stamping**, not minting a class_id +//! (§2: "reserving costs nothing") — concept mints stay gated on the +//! 5+3 codebook pass. The concept half is the cross-app currency: +//! concept/domain routing reads it alone (`lance_graph_contract:: +//! classid_concept_domain` routes the canon half), so it is identical under +//! every render prefix. use crate::ports::PortSpec; -/// Compose a full render `classid` from an app `prefix` (high u16) and a -/// canonical `concept` id (low u16): `(prefix << 16) | concept`. +/// Compose a full render `classid` from an app `prefix` (CUSTOM half, low +/// u16) and a canonical `concept` id (CANON half, high u16): +/// `((concept as u32) << 16) | prefix`. /// -/// `render_classid(0x0001, 0x0102)` → `0x0001_0102` (OpenProject's +/// `render_classid(0x0001, 0x0102)` → `0x0102_0001` (OpenProject's /// `project_work_item`); the Redmine twin `render_classid(0x0007, 0x0102)` → -/// `0x0007_0102` — same concept, different render lens. +/// `0x0102_0007` — same concept, different render lens. #[must_use] pub const fn render_classid(prefix: u16, concept: u16) -> u32 { - ((prefix as u32) << 16) | (concept as u32) + ((concept as u32) << 16) | (prefix as u32) } /// Compose a render `classid` for a specific port, reading its reserved /// [`PortSpec::APP_PREFIX`]: `render_classid_for::(concept)` -/// → `0x0001_DDCC`. This is the helper consumers call so the prefix and the +/// → `0xDDCC_0001`. This is the helper consumers call so the prefix and the /// bit math both come from OGAR (one source of truth), not a local literal. #[must_use] pub fn render_classid_for(concept: u16) -> u32 { render_classid(P::APP_PREFIX, concept) } -/// The APP / render prefix — the **high u16** of a full `classid`. `0x0000` -/// ([the shared core]) is the abstract/default-`ClassView` anchor; a non-zero -/// value selects an app's render lens. This is the §4 `resolve_codebook` -/// routing key (`classid >> 16`). +/// The APP / render prefix — the CUSTOM half (**low u16** since the flip) of +/// a full `classid`. `0x0000` ([the shared core]) is the +/// abstract/default-`ClassView` anchor; a non-zero value selects an app's +/// render lens. This is the §4 `resolve_codebook` routing key +/// (`classid as u16`). /// /// [the shared core]: PortSpec::APP_PREFIX #[must_use] pub const fn app_of(classid: u32) -> u16 { - (classid >> 16) as u16 + classid as u16 } -/// The canonical concept id — the **low u16** of a full `classid`. The shared -/// RBAC + ontology + cross-app identity key; concept/domain routing reads only -/// this half, so it is identical for every render prefix. +/// The canonical concept id — the CANON half (**high u16** since the flip) of +/// a full `classid`. The shared RBAC + ontology + cross-app identity key; +/// concept/domain routing reads only this half, so it is identical for every +/// render prefix. #[must_use] pub const fn concept_of(classid: u32) -> u16 { - classid as u16 + (classid >> 16) as u16 } #[cfg(test)] @@ -70,9 +81,10 @@ mod tests { #[test] fn render_classid_composes_the_two_halves() { - assert_eq!(render_classid(0x0001, 0x0102), 0x0001_0102); - assert_eq!(render_classid(0x0007, 0x0102), 0x0007_0102); - assert_eq!(render_classid(0x0002, 0x0202), 0x0002_0202); + // Canon-high order (2026-07-02 flip): concept HIGH, prefix LOW. + assert_eq!(render_classid(0x0001, 0x0102), 0x0102_0001); + assert_eq!(render_classid(0x0007, 0x0102), 0x0102_0007); + assert_eq!(render_classid(0x0002, 0x0202), 0x0202_0002); } #[test] @@ -81,22 +93,22 @@ mod tests { // under different prefixes — "two renders, one concept" (§1). assert_eq!( render_classid_for::(class_ids::PROJECT_WORK_ITEM), - 0x0001_0102, + 0x0102_0001, ); assert_eq!( render_classid_for::(class_ids::PROJECT_WORK_ITEM), - 0x0007_0102, + 0x0102_0007, ); assert_eq!( render_classid_for::(class_ids::COMMERCIAL_DOCUMENT), - 0x0002_0202, + 0x0202_0002, ); } #[test] fn app_of_and_concept_of_decompose() { let cid = render_classid(0x0005, class_ids::PATIENT); // Medcare patient - assert_eq!(cid, 0x0005_0901); + assert_eq!(cid, 0x0901_0005); assert_eq!(app_of(cid), 0x0005); assert_eq!(concept_of(cid), class_ids::PATIENT); } @@ -119,18 +131,19 @@ mod tests { #[test] fn core_prefix_is_additive_and_bit_identical() { - // I-APP1/I-APP5: a core (hi=0x0000) classid equals the bare concept - // widened to u32 — no renumber, no version cost. + // I-APP1/I-APP5 (post-flip form): a core (prefix=0x0000) classid is + // the bare concept in the CANON (high) half — no renumber, no version + // cost; `concept_of` recovers it exactly. let core = render_classid(0x0000, class_ids::PROJECT_WORK_ITEM); - assert_eq!(core, 0x0000_0102); - assert_eq!(core, u32::from(class_ids::PROJECT_WORK_ITEM)); + assert_eq!(core, 0x0102_0000); + assert_eq!(core, u32::from(class_ids::PROJECT_WORK_ITEM) << 16); assert_eq!(app_of(core), 0x0000); assert_eq!(concept_of(core), class_ids::PROJECT_WORK_ITEM); } #[test] fn render_prefix_never_changes_the_concept_half() { - // The high half is the render lens; it must not perturb the low-half + // The prefix half is the render lens; it must not perturb the CANON // concept that RBAC + ontology key on. let op = render_classid_for::(class_ids::BILLABLE_WORK_ENTRY); let rm = render_classid_for::(class_ids::BILLABLE_WORK_ENTRY); diff --git a/crates/ogar-vocab/src/lib.rs b/crates/ogar-vocab/src/lib.rs index 720f39e..ae526b7 100644 --- a/crates/ogar-vocab/src/lib.rs +++ b/crates/ogar-vocab/src/lib.rs @@ -1127,8 +1127,10 @@ const CODEBOOK: &[(&str, u16)] = &[ // `osint_system@0x0700` / `osint_person@0x0701`). Within the OSINT domain // the low byte is NOT a concept slot — it is allocated domain-wise as an // APPID: `0x0700` = the OSINT domain itself (low byte 00 = domain-wide), - // `0x0701` = OSINT-for-q2 (q2 is appid 0x01, the consumer); V3 form - // `0x1000_0701`. Class content (AIRO/VAIR system card, McClelland/Rubicon + // `0x0701` = OSINT-for-q2 (q2 is appid 0x01, the consumer); V3 stored form + // `0x0701_1000` (canon HIGH since the same-day half-order flip — + // human-readable `0x07:01::1000`). Class content (AIRO/VAIR system card, + // McClelland/Rubicon // person lens) lives consumer-side in q2's `osint_classview.rs` — OGAR // vocabulary carries no OSINT concept names. Do NOT re-mint rows here. // ── 0x09XX — Health domain (clinical / patient / care) ── diff --git a/crates/ogar-vocab/src/ports.rs b/crates/ogar-vocab/src/ports.rs index ccc3888..42ce820 100644 --- a/crates/ogar-vocab/src/ports.rs +++ b/crates/ogar-vocab/src/ports.rs @@ -56,19 +56,21 @@ pub trait PortSpec: 'static + Send + Sync { /// Lowercase bridge_id for `NamespaceBridge::bridge_id()`. const BRIDGE_ID: &'static str; - /// Reserved APP / render prefix — the high u16 of a full 32-bit - /// classid (`APP-CLASS-CODEBOOK-LAYOUT.md` §2). + /// Reserved APP / render prefix — the CUSTOM half (low u16 since the + /// 2026-07-02 canon:custom flip) of a full 32-bit classid + /// (`APP-CLASS-CODEBOOK-LAYOUT.md` §2; the prefix VALUES are + /// order-invariant). /// /// Composing the full render classid: /// ```text - /// render_classid = (APP_PREFIX as u32) << 16 | concept_low_u16 + /// render_classid = (concept as u32) << 16 | APP_PREFIX /// ``` /// /// `0x0000` is the **shared canonical core** — the cross-app ontology /// every consumer reuses. Each app port overrides with its reserved /// prefix from the §2 allocation table, waking its own per-app /// ClassView / Askama template set for rendering while keeping the - /// low-u16 concept (RBAC + ontology) shared. + /// CANON (high-u16) concept (RBAC + ontology) shared. /// /// Reserving this prefix costs nothing (§2: "Reserving a prefix costs /// nothing — no codebook is materialised until the app mints its first diff --git a/docs/APP-CLASS-CODEBOOK-LAYOUT.md b/docs/APP-CLASS-CODEBOOK-LAYOUT.md index 117f6bd..3b21ba4 100644 --- a/docs/APP-CLASS-CODEBOOK-LAYOUT.md +++ b/docs/APP-CLASS-CODEBOOK-LAYOUT.md @@ -5,22 +5,29 @@ > **low 4 hex (u16)**. The high u16 was *reserved zero*, not used for > SoA versioning (that is `ENVELOPE_LAYOUT_VERSION: u8`, a separate > byte — `lance-graph-contract/src/soa_envelope.rs:54`). This doc -> claims the high u16 as the **APP / codebook-namespace + render +> claims the low u16 as the **APP / codebook-namespace + render > prefix** and pins the rule that keeps "classid is shared currency" > intact. > +> **Order flipped 2026-07-02 — canon HIGH / custom LOW.** The layout +> below now reads `classid : u32 = [ hi u16 : concept ] [ lo u16 : APP +> prefix ]` — the canonical concept sits in the **high** u16, the +> per-app render prefix sits in the **low** u16. Pre-flip docs and any +> already-baked data use the legacy order (`hi = APP`, `lo = concept`); +> see `docs/DISCOVERY-MAP.md` D-CLASSID-CANON-HIGH-FLIP. +> > **Read together with** `docs/OGAR-CONSUMER-BEST-PRACTICES.md` — the > muscle-memory guide with worked examples across every consumer. **The > classid is pure address (both halves)**; behavior lives at the > resolution target (ClassView for the skin / `Class`+`ActionDef` for -> the canonical shape and magic). Hi u16 selects **render** magic, NOT +> the canonical shape and magic). Lo u16 selects **render** magic, NOT > class magic; class magic is the Core's, never the address's. > > **The goal it serves (§3.5–3.7):** every renderable thing — strings, > text, media, online sources — is rendered by **key-value resolution** > against typed content stores, so **no serialization exists in the hot -> path** (the Firewall, ADR-022/023). The high u16 selects *which app's* -> Askama template / `ClassView` renders an object; the low u16 is the +> path** (the Firewall, ADR-022/023). The low u16 selects *which app's* +> Askama template / `ClassView` renders an object; the high u16 is the > shared concept (RBAC + ontology); each field within is itself a key > into a content store. Render = address resolution, never parse. The > **same discipline extends to RAG** (§3.7): retrieval over the graph @@ -39,28 +46,28 @@ ## 0. The layout (counted in hex, per the canon) ``` -classid : u32 = [ hi u16 : APP / codebook namespace ] [ lo u16 : in-codebook class ] - 0xAAAA 0xDDCC - ^ which codebook (0x0000 = shared core) ^ domain DD | concept CC +classid : u32 = [ hi u16 : in-codebook class ] [ lo u16 : APP / codebook namespace ] + 0xDDCC 0xAAAA + ^ domain DD | concept CC ^ which codebook (0x0000 = shared core) ``` -- **`hi u16` (0xAAAA) — APP prefix = codebook namespace selector.** +- **`hi u16` (0xDDCC) — in-codebook class id.** Domain byte `DD` + + concept byte `CC`, exactly as the codebook encodes today. Within the + core codebook (`lo = 0x0000`) the domain bytes are the canonical map + (`0x01` project, `0x02` commerce, `0x07` osint, `0x08` ocr, `0x09` + health, `0x0A` anatomy, `0x0B` auth, `0x0C` automation). Within an + app-private codebook the app owns its own `DD|CC` layout. +- **`lo u16` (0xAAAA) — APP prefix = codebook namespace selector.** Which 256⁶ semantic space / which centroid-codebook set the key resolves against (the longest-prefix codebook scoping already pinned in OGAR `CLAUDE.md`). `0x0000` is the **shared canonical core** — the cross-app ontology every consumer reuses. A non-zero value is an **app-private codebook**. -- **`lo u16` (0xDDCC) — in-codebook class id.** Domain byte `DD` + - concept byte `CC`, exactly as the codebook encodes today. Within the - core codebook (`hi = 0x0000`) the domain bytes are the canonical map - (`0x01` project, `0x02` commerce, `0x07` osint, `0x08` ocr, `0x09` - health, `0x0A` anatomy, `0x0B` auth, `0x0C` automation). Within an - app-private codebook the app owns its own `DD|CC` layout. **This is additive, not a reclaim.** Every classid shipped to date is -`0x0000_DDCC` — i.e. it was *always* an APP‖class id with `APP = core`. +`0xDDCC_0000` — i.e. it was *always* an APP‖class id with `APP = core`. Nothing re-numbers. The canon's "RESERVE, DON'T RECLAIM" holds exactly: -`hi = 0x0000` is the bootstrap/core prefix; minting a non-zero `hi` +`lo = 0x0000` is the bootstrap/core prefix; minting a non-zero `lo` wakes app-private codebook routing with **zero `ENVELOPE_LAYOUT_VERSION` change**, because the classid keeps its fixed 4-byte offset at key bytes `0..4`. @@ -76,45 +83,45 @@ layout). The two halves of the u32 carry exactly those, orthogonally: | half | answers | keyed by | shared? | |---|---|---|---| -| **lo u16 `0xDDCC`** | **WHAT it is** — canonical concept + domain | RBAC grant lattice, ontology enrichment, cross-app identity | **shared** across all apps | -| **hi u16 `0xAAAA`** | **WHOSE rendering** — app `ClassView` / Askama template / SoA layout | object render + skeleton-layout | **per-app** | - -So Medcare's patient is **`0x0005_0901`**: the low half `0x0901` shares -the `patient` grant lattice and OGIT ontology with *every* health app; -the high half `0x0005` binds it to **Medcare's** clinical template. -`0x0000_0901` is the canonical/abstract anchor (the master concept + -default `ClassView`). This is the canon's "the key prerenders nodes with -zero value decode" made literal — **both halves come straight from the -key**: high picks the template, low picks the concept/domain, no value -decode (see §3.5). - -**Consequence: the high u16 is the NORM for every rendered object, not +| **hi u16 `0xDDCC`** | **WHAT it is** — canonical concept + domain | RBAC grant lattice, ontology enrichment, cross-app identity | **shared** across all apps | +| **lo u16 `0xAAAA`** | **WHOSE rendering** — app `ClassView` / Askama template / SoA layout | object render + skeleton-layout | **per-app** | + +So Medcare's patient is **`0x0901_0005`**: the high half `0x0901` +shares the `patient` grant lattice and OGIT ontology with *every* +health app; the low half `0x0005` binds it to **Medcare's** clinical +template. `0x0901_0000` is the canonical/abstract anchor (the master +concept + default `ClassView`). This is the canon's "the key prerenders +nodes with zero value decode" made literal — **both halves come +straight from the key**: high picks the concept/domain, low picks the +template, no value decode (see §3.5). + +**Consequence: the low u16 is the NORM for every rendered object, not an escape hatch.** Every app stamps its own prefix so its surface -objects bind to its own templates, *while still pulling a shared low-u16 +objects bind to its own templates, *while still pulling a shared high-u16 concept* so RBAC and ontology stay cross-app. The unit of currency — the -**shared meaning** — is the low half; the high half is the render lens. +**shared meaning** — is the high half; the low half is the render lens. -> **Rule: low half = pull a CORE concept (shared identity). High half = +> **Rule: high half = pull a CORE concept (shared identity). Low half = > stamp YOUR app prefix (your render binding).** An object that has a -> canonical analogue uses `your_app ‖ core_concept`. An object with **no** -> canonical analogue uses `your_app ‖ app_local_concept` — the genuine -> escape hatch, where the low half is also app-minted. +> canonical analogue uses `core_concept ‖ your_app`. An object with **no** +> canonical analogue uses `app_local_concept ‖ your_app` — the genuine +> escape hatch, where the high half is also app-minted. -| | low half = CORE concept (`0xDDCC` shared) | low half = APP-LOCAL concept (`0xDDCC` app-minted) | +| | high half = CORE concept (`0xDDCC` shared) | high half = APP-LOCAL concept (`0xDDCC` app-minted) | |---|---|---| | **When** | the object means something canonical (patient, invoice, project) | the object has **no** canonical analogue | -| **classid** | `your_app ‖ 0x0901` e.g. `0x0005_0901` | `your_app ‖ 0xFF01` e.g. `0x0005_FF01` | -| **RBAC/ontology** | shared lattice via low half | app-private lattice | -| **Rendering** | app template via high half | app template via high half | +| **classid** | `0x0901 ‖ your_app` e.g. `0x0901_0005` | `0xFF01 ‖ your_app` e.g. `0xFF01_0005` | +| **RBAC/ontology** | shared lattice via high half | app-private lattice | +| **Rendering** | app template via low half | app template via low half | | **Frequency** | the **norm** | the **exception** | -The "codebook per project" win the operator named is the high half: +The "codebook per project" win the operator named is the low half: each app prefix roots **its own** centroid-codebook hierarchy and its own ClassView/template set, so per-app rendering scales **without radix-trie codebook overflow** — the shared core never has to hold one template-variant per app. -**Promotion path (app-local → core):** if an app-local *concept* (low +**Promotion path (app-local → core):** if an app-local *concept* (high half) turns out reusable, promote it into a core domain block via the 5+3 codebook gate and leave the app-local id as a deprecated alias. ClassViews/templates never promote (they are app-private by nature). @@ -124,11 +131,11 @@ Demotion never happens (RESERVE, DON'T RECLAIM). ## 2. APP-prefix allocation table (reserved; non-zero wakes a private codebook) -`hi = 0x0000` is core. Non-zero prefixes are **reserved by app** so two +`lo = 0x0000` is core. Non-zero prefixes are **reserved by app** so two apps never collide. Reserving a prefix costs nothing (no codebook is materialised until the app mints its first private class). -| `hi u16` | App / namespace | Core domain(s) it consumes | Private codebook today? | +| `lo u16` | App / namespace | Core domain(s) it consumes | Private codebook today? | |---|---|---|---| | `0x0000` | **Shared canonical core** | all (`0x01/02/07/08/09` + `0x0A` anatomy + `0x0B` auth + `0x0C` automation) | n/a (this *is* core) | | `0x0001` | OpenProject (openproject-nexgen-rs) | `0x01` project-mgmt | **no** — maps onto core | @@ -141,15 +148,15 @@ materialised until the app mints its first private class). | `0x00A0` | (reserved) future app block | — | — | > **OpenProject (`0x0001`) and Redmine (`0x0007`) are the showcase:** -> same low-u16 concepts (`WorkPackage`/`Issue` both → `0x0102` +> same high-u16 concepts (`WorkPackage`/`Issue` both → `0x0102` > project_work_item; same RBAC `project_role 0x0117` lattice), different -> high-u16 render prefix (different ClassView/Askama template). Two +> low-u16 render prefix (different ClassView/Askama template). Two > renders, one concept — the cleanest demonstration of §1. See > `APP-CODEBOOK-MIGRATION-PLAN.md` W0. Auth is **not** its own APP — auth providers are canonical, cross-app profiles, so they live in **core** under a new `0x0B` auth domain -(`auth_store = 0x0000_0B01`, `auth_zitadel`/`auth_zanzibar`/ +(`auth_store = 0x0B01_0000`, `auth_zitadel`/`auth_zanzibar`/ `auth_ory_keto` as provider profiles). See `CLASSID-RBAC-KEYSTONE-SPEC.md` §7 — those classes mint into core, not under any app prefix. (This corrects the earlier "flat 0x011B" mint @@ -163,95 +170,95 @@ attempt: auth classes are core-domain `0x0B`, not project-domain Medcare is one **consumer of the canonical Health domain**, not the owner of clinical ontology. So: -### 3a. Concept low half — PULLED from core (domain `0x09`, shared) +### 3a. Concept high half — PULLED from core (domain `0x09`, shared) The 7 canonical OGIT Healthcare concepts — already shipped, shared with -any future health consumer. These are the **low-u16 concept anchors**; -their `hi = 0x0000` form is the abstract master + default ClassView: +any future health consumer. These are the **high-u16 concept anchors**; +their `lo = 0x0000` form is the abstract master + default ClassView: -| Concept | core anchor (u32) | low half (shared) | Source | +| Concept | core anchor (u32) | high half (shared) | Source | |---|---|---|---| -| patient | `0x0000_0901` | `0x0901` | OGIT `Healthcare:Patient` | -| diagnosis | `0x0000_0902` | `0x0902` | OGIT `Healthcare:Diagnosis` | -| lab_value | `0x0000_0903` | `0x0903` | OGIT `Healthcare:LabValue` | -| medication | `0x0000_0904` | `0x0904` | OGIT `Healthcare:Medication` | -| treatment | `0x0000_0905` | `0x0905` | OGIT `Healthcare:Treatment` | -| visit | `0x0000_0906` | `0x0906` | OGIT `Healthcare:Visit` | -| vital_sign | `0x0000_0907` | `0x0907` | OGIT `Healthcare:VitalSign` | +| patient | `0x0901_0000` | `0x0901` | OGIT `Healthcare:Patient` | +| diagnosis | `0x0902_0000` | `0x0902` | OGIT `Healthcare:Diagnosis` | +| lab_value | `0x0903_0000` | `0x0903` | OGIT `Healthcare:LabValue` | +| medication | `0x0904_0000` | `0x0904` | OGIT `Healthcare:Medication` | +| treatment | `0x0905_0000` | `0x0905` | OGIT `Healthcare:Treatment` | +| visit | `0x0906_0000` | `0x0906` | OGIT `Healthcare:Visit` | +| vital_sign | `0x0907_0000` | `0x0907` | OGIT `Healthcare:VitalSign` | `HealthcarePort` resolves Medcare's surface names (`Patient`, -`Befund`→`diagnosis`, `Laborwert`→`lab_value`, …) onto these **low -halves**. RBAC + ontology key on the low half, so the grant lattice is +`Befund`→`diagnosis`, `Laborwert`→`lab_value`, …) onto these **high +halves**. RBAC + ontology key on the high half, so the grant lattice is shared with every health consumer. **No medcare bridge.** -### 3b. Render high half — STAMPED as `0x0005` (Medcare's ClassView) +### 3b. Render low half — STAMPED as `0x0005` (Medcare's ClassView) -Medcare's *rendered* objects carry its own prefix in the high half. Same +Medcare's *rendered* objects carry its own prefix in the low half. Same shared concept, Medcare's template + SoA layout: -| Medcare object | classid (u32) | low (shared concept) | high (Medcare render) | +| Medcare object | classid (u32) | high (shared concept) | low (Medcare render) | |---|---|---|---| -| Medcare patient view | `0x0005_0901` | `0x0901` patient | Medcare `patient.html` Askama template | -| Medcare diagnosis (Befund) | `0x0005_0902` | `0x0902` diagnosis | Medcare `befund.html`, PII leaf-rename at adapter | -| Medcare lab value (Laborwert) | `0x0005_0903` | `0x0903` lab_value | Medcare `laborwert.html` | +| Medcare patient view | `0x0901_0005` | `0x0901` patient | Medcare `patient.html` Askama template | +| Medcare diagnosis (Befund) | `0x0902_0005` | `0x0902` diagnosis | Medcare `befund.html`, PII leaf-rename at adapter | +| Medcare lab value (Laborwert) | `0x0903_0005` | `0x0903` lab_value | Medcare `laborwert.html` | -- **Authorize** keys on `classid as u16` → `0x0901` → shared patient - grant: `authorize(actor, 0x0005_0901 as u16, Read)`. -- **Render** keys on the full u32 → `0x0005_0901` → Medcare ClassView → +- **Authorize** keys on `classid >> 16` → `0x0901` → shared patient + grant: `authorize(actor, (0x0901_0005 >> 16) as u16, Read)`. +- **Render** keys on the full u32 → `0x0901_0005` → Medcare ClassView → Askama template + field order (§3.5). - The PII leaf-rename (German clinical labels never leave the membrane) - is the Medcare ClassView's job — bound by the high half, exactly where + is the Medcare ClassView's job — bound by the low half, exactly where it should be. -### 3c. Genuinely-bespoke Medcare objects (low half ALSO app-minted) +### 3c. Genuinely-bespoke Medcare objects (high half ALSO app-minted) -Only entities with **no** canonical analogue — low half is Medcare's to +Only entities with **no** canonical analogue — high half is Medcare's to mint too: ``` -0x0005_F0CC medcare bespoke object classes (no canonical analogue) -0x0005_FFCC medcare local special-cases (the long tail CLAUDE.md §3 names) +0xF0CC_0005 medcare bespoke object classes (no canonical analogue) +0xFFCC_0005 medcare local special-cases (the long tail CLAUDE.md §3 names) ``` | Candidate | Provisional classid | Why fully app-private | |---|---|---| -| medcare insurance-case (German GKV/PKV billing quirk) | `0x0005_F001` | clinic-billing specific; no canonical analogue yet | -| medcare KIM/TI-message envelope | `0x0005_F002` | German telematics-infra specific | -| medcare migration import row (`/api/admin/migration/sql/import`) | `0x0005_FF01` | the one MySQL-only-by-design route (medcare-rs CLAUDE.md) | +| medcare insurance-case (German GKV/PKV billing quirk) | `0xF001_0005` | clinic-billing specific; no canonical analogue yet | +| medcare KIM/TI-message envelope | `0xF002_0005` | German telematics-infra specific | +| medcare migration import row (`/api/admin/migration/sql/import`) | `0xFF01_0005` | the one MySQL-only-by-design route (medcare-rs CLAUDE.md) | -**If a concept has a canonical analogue, use it in the low half** (§3b) -— do not fork a new low-u16 id just to render it. The fully-private form +**If a concept has a canonical analogue, use it in the high half** (§3b) +— do not fork a new high-u16 id just to render it. The fully-private form (§3c) is for concepts that genuinely don't generalize. ### 3d. Medcare's diff, concretely 1. **OGAR** (one PR, gated): reserve `0x0005` for Medcare; confirm - `HealthcarePort` maps the 7 core concept **low halves** (it does — + `HealthcarePort` maps the 7 core concept **high halves** (it does — `ports::HealthcarePort`); register Medcare's ClassViews/templates under `0x0005`. Mint fully-private (§3c) classes only when a real bespoke entity needs one (none required to ship the patient-read gate). -2. **medcare-rs** (its own crate): pull the concept low half statically +2. **medcare-rs** (its own crate): pull the concept high half statically (`HealthcarePort::class_id("Patient") == Some(0x0901)`); form its - render classid `0x0005_0000 | 0x0901 = 0x0005_0901`; enrich (RLS / - masks) and render by that classid; authorize by the **low half**. The + render classid `(0x0901 << 16) | 0x0005 = 0x0901_0005`; enrich (RLS / + masks) and render by that classid; authorize by the **high half**. The spine (`lance-graph-ogar`, `lance-graph-rbac`) is byte-for-byte unchanged. The in-flight medcare patient-read gate (PR #169) is the first consumer: today it keys on `static_role`; once the keystone + this -layout land, it keys on `authorize(actor, 0x0005_0901 as u16, Read)` +layout land, it keys on `authorize(actor, (0x0901_0005 >> 16) as u16, Read)` (= the shared `0x0901` patient grant) and renders via the `0x0005` Medcare ClassView. --- -## 3.5. Rendering — the high u16 IS the Askama / ClassView binding +## 3.5. Rendering — the low u16 IS the Askama / ClassView binding -This is *why* the high u16 is the norm, not an escape hatch. **Object +This is *why* the low u16 is the norm, not an escape hatch. **Object rendering is per-app**, and the render binding must come straight from the key (canon: "the key prerenders nodes with zero value decode"). The -high u16 is that binding: +low u16 is that binding: ``` object key ──► classid (u32) ──► ClassView lookup keyed on FULL u32 @@ -260,25 +267,25 @@ object key ──► classid (u32) ──► ClassView lookup keyed on FULL u32 │ ├─ SoA field order / column projection │ └─ label set (PII leaf-rename at adapter) │ - └─ low u16 ─► concept/domain ─► RBAC grant + ontology (shared) + └─ high u16 ─► concept/domain ─► RBAC grant + ontology (shared) ``` -- **One concept, many renders.** `0x0000_0901` (canonical patient), - `0x0005_0901` (Medcare's patient form), and a hypothetical - `0x0007_0901` (another health app's patient card) are the **same - concept** (low half `0x0901` — same grant, same ontology) rendered - three ways (three ClassViews / three Askama templates). The high half +- **One concept, many renders.** `0x0901_0000` (canonical patient), + `0x0901_0005` (Medcare's patient form), and a hypothetical + `0x0901_0007` (another health app's patient card) are the **same + concept** (high half `0x0901` — same grant, same ontology) rendered + three ways (three ClassViews / three Askama templates). The low half selects the template **without** decoding the row. - **`ClassView` is already the render manifest.** The canon binds `classid → ClassView`; the ClassView carries the structural signature - (field set, order) the template iterates. Stamping the high u16 means + (field set, order) the template iterates. Stamping the low u16 means "use *this app's* ClassView for this concept" — the Askama template is a property of that ClassView, resolved by the same `resolve` read that resolves schema and codebook. -- **Why not render off the low half alone?** Because then every app +- **Why not render off the high half alone?** Because then every app would have to share one template per concept, or fork the concept id to get a distinct template — the radix-trie overflow the operator - flagged. Splitting render (high) from meaning (low) lets an unbounded + flagged. Splitting render (low) from meaning (high) lets an unbounded number of apps each render `patient` their own way while the `patient` grant lattice and ontology stay singular and shared. - **Skeleton render with zero value decode.** A list/grid/planner view @@ -290,10 +297,10 @@ object key ──► classid (u32) ──► ClassView lookup keyed on FULL u32 ```rust // at the boundary: object's full classid is in its key -let concept = (classid as u16); // 0x0901 — shared: RBAC + ontology +let concept = (classid >> 16) as u16; // 0x0901 — shared: RBAC + ontology let render = ClassView::resolve(classid); // full u32 — this app's template let html = render.template().render(&row)?; // Askama, compile-time checked -authorize(actor, concept, Op::Read)?; // grant lattice on the shared low half +authorize(actor, concept, Op::Read)?; // grant lattice on the shared high half ``` Stack note: woa-rs and smb-office-rs already use **Askama** (compile-time @@ -325,8 +332,8 @@ label = KEY, meaning = VALUE) applied to *content*: | **Media** (image/audio/blob) | base64 in JSON | a `media` content **key** → bytes in Lance / object-store URI resolved from the key; render emits a reference, bytes stream zero-copy | | **Online source** (URL/remote) | the fetched body serialized into the row | a `source` **key** → URI registry entry; render resolves key → canonical URI (+ cache), the remote body is never serialized into the object | -So a rendered object is a **tree of keys**: the object's classid (high = -app template, low = concept) selects the Askama template; each field the +So a rendered object is a **tree of keys**: the object's classid (low = +app template, high = concept) selects the Askama template; each field the template iterates is itself a key that resolves — by the same key-value lookup — into a typed content store. The whole render path, top to leaf, is **address resolution**, not parsing. @@ -398,7 +405,7 @@ membrane (the ONE egress point — content materializes here, once): call — the same "boundary parsed once" the Firewall mandates, and the same MarkovBarrier the cognition stack already uses (crewai-rust blood-brain-barrier: inner cognition on keys/fingerprints, content - materialized only at the external API edge). The high u16 still picks + materialized only at the external API edge). The low u16 still picks *which app's* view/template a key materializes through, so RAG citations render in the asking app's voice. - **Two membranes, one rule.** UI render and LLM prompt are both egress @@ -417,11 +424,13 @@ happens once, at the edge, not threaded through retrieval.) ## 3.8. Intuition — this is C64 / 6502 addressing The model is, deliberately, **8-bit-machine assembler**. It is worth -holding the analogy because every piece maps and the mapping is exact: +holding the analogy because every piece maps and the mapping is exact +(re-mapped 2026-07-02: concept is now the page, the APP/render prefix is +now the offset): | 6502 / C64 / VIC-II | this layout | |---|---| -| 16-bit address as **page : offset** (zero-page indirect) | `classid` as **hi u16 : lo u16** (codebook/app page : concept offset) | +| 16-bit address as **page : offset** (zero-page indirect) | `classid` as **hi u16 : lo u16** (concept page : codebook/app offset) | | **Character ROM** ($D000): char code → 8-byte glyph | **string/glyph codebook**: a key → interned bytes (§3.6 string row) | | **Screen RAM** byte → VIC-II reads glyph, zero decode, every frame | object field key → ClassView resolves content, zero decode, every render | | **Sprite pointers** ($07F8–$07FF): 1 byte → 64-byte sprite block | **media key** → media bytes / URI (§3.6 media row) | @@ -441,23 +450,23 @@ deserialized a sprite, and neither should the hot path. ## 4. Routing consequence (one function, longest-prefix-wins) -`classid_concept_domain` today reads only the low u16 -(`canonical_concept_domain(classid as u16)` — +`classid_concept_domain` today reads only the high u16 +(`canonical_concept_domain((classid >> 16) as u16)` — `lance-graph-contract/src/ogar_codebook.rs:81`). Under APP‖class that becomes a two-step longest-prefix bind, and it stays O(1): -``` +```rust fn resolve_codebook(classid: u32) -> Codebook { - match (classid >> 16) as u16 { + match classid as u16 { 0x0000 => Codebook::Core, // shared canon (today's behaviour) app => Codebook::App(app), // app-private namespace } } -// domain within a codebook is still the low-u16 high byte: -fn domain_in(classid: u32) -> u8 { (classid >> 8) as u8 } +// app = classid as u16; concept = (classid >> 16) as u16; domain = (classid >> 24) as u8 +fn domain_in(classid: u32) -> u8 { (classid >> 24) as u8 } ``` -For `hi = 0x0000` this is **bit-identical to today** — no regression, +For `lo = 0x0000` this is **bit-identical to today** — no regression, no version bump. App-private codebooks add their own `(app → domain map)` record next to their `ClassView` in the registry (the codebook-mints- with-the-class shelf the canon already describes). This is the same @@ -469,20 +478,20 @@ granularity instead of only at the byte granularity. ## 5. Invariants (proposed; pin on 5+3 pass) -- **I-APP1 — additive:** every existing classid is `0x0000_DDCC`; the - high u16 was always present and zero. No id re-numbers. -- **I-APP2 — core is shared:** canonical concepts live in `hi = 0x0000` +- **I-APP1 — additive:** every existing classid is `0xDDCC_0000`; the + low u16 was always present and zero. No id re-numbers. +- **I-APP2 — core is shared:** canonical concepts live in `lo = 0x0000` and are pulled by every consumer. An app never re-numbers a canonical concept into its own prefix. -- **I-APP3 — private is the exception:** an app mints `hi = 0xAAAA` +- **I-APP3 — private is the exception:** an app mints `lo = 0xAAAA` classes only for objects that fail the "would a second consumer reuse this?" test. Default is map-onto-core. - **I-APP4 — reserve, don't reclaim:** app prefixes are reserved once and never re-assigned; promotion (private→core) leaves the private id as a deprecated alias; demotion never happens. - **I-APP5 — zero version cost:** because classid keeps its fixed 4-byte - offset, waking a non-zero high u16 changes no `ENVELOPE_LAYOUT_VERSION` - and breaks no v1 reader of a `0x0000_*` key. + offset, waking a non-zero low u16 changes no `ENVELOPE_LAYOUT_VERSION` + and breaks no v1 reader of a `0xDDCC_0000` key. - **I-APP6 — domain map is codebook-local:** `0x09 = health` is true in the core codebook; an app-private codebook defines its own domain bytes and must ship its `(app → domain)` record with its ClassView. @@ -492,7 +501,7 @@ granularity instead of only at the byte granularity. ## 6. What this is NOT - **Not SoA versioning.** `ENVELOPE_LAYOUT_VERSION: u8 = 2` is the SoA - version, a separate byte. The high u16 of classid has nothing to do + version, a separate byte. The low u16 of classid has nothing to do with it (the question that prompted this doc). - **Not a per-app bridge.** The app-private codebook is *data* (a class block + a PortSpec), authored in OGAR, named by classid. It is not a diff --git a/docs/APP-CODEBOOK-MIGRATION-PLAN.md b/docs/APP-CODEBOOK-MIGRATION-PLAN.md index 53f36f0..3530e26 100644 --- a/docs/APP-CODEBOOK-MIGRATION-PLAN.md +++ b/docs/APP-CODEBOOK-MIGRATION-PLAN.md @@ -2,9 +2,14 @@ > Companion to `APP-CLASS-CODEBOOK-LAYOUT.md` (the layout) and > `CONSUMER-MIGRATION-HOWTO.md` (the generic steps). This doc applies -> the **APP‖class** model (`classid = APP(hi u16) ‖ class(lo u16)`) to +> the **APP‖class** model (`classid = class(hi u16) ‖ APP(lo u16)`) to > each remaining app and orders the work. > +> **Order flipped 2026-07-02 — canon HIGH / custom LOW**: the canonical +> concept sits in the high u16, the APP prefix sits in the low u16. All +> composed literals below use the new order; see +> `docs/DISCOVERY-MAP.md` D-CLASSID-CANON-HIGH-FLIP. +> > Status: **PLAN**. Append-only. The minting steps are gated on the > 5+3 codebook pass; nothing here mints a classid yet. @@ -16,10 +21,10 @@ For each surface entity, the app answers **one** question: > *Would a second, unrelated consumer reuse this concept verbatim?* -- **Yes →** map onto a **CORE** classid (`hi = 0x0000`) via the app's +- **Yes →** map onto a **CORE** classid (`lo = 0x0000`) via the app's OGAR `PortSpec`. No private mint. This is the default and the overwhelming majority. -- **No →** mint an **APP-private** classid (`hi = 0xAAAA`) in the app's +- **No →** mint an **APP-private** classid (`lo = 0xAAAA`) in the app's reserved codebook namespace. Escape hatch only. The migration is then identical to `CONSUMER-MIGRATION-HOWTO.md`: pull @@ -65,44 +70,44 @@ one id. **Private codebook: no** — every project concept is canonical. renderers of the same canonical concepts**. The hi/lo split (`APP-CLASS-CODEBOOK-LAYOUT.md` §1) is at its cleanest here: -| Surface name | shared concept (lo u16) | OpenProject id | Redmine id | +| Surface name | shared concept (hi u16) | OpenProject id | Redmine id | |---|---|---|---| -| WorkPackage / Issue | `0x0102` project_work_item | `0x0001_0102` | `0x0007_0102` | -| Project | `0x0101` project | `0x0001_0101` | `0x0007_0101` | -| Role | `0x0117` project_role | `0x0001_0117` | `0x0007_0117` | -| TimeEntry / billable | `0x0103` billable_work_entry | `0x0001_0103` | `0x0007_0103` | +| WorkPackage / Issue | `0x0102` project_work_item | `0x0102_0001` | `0x0102_0007` | +| Project | `0x0101` project | `0x0101_0001` | `0x0101_0007` | +| Role | `0x0117` project_role | `0x0117_0001` | `0x0117_0007` | +| TimeEntry / billable | `0x0103` billable_work_entry | `0x0103_0001` | `0x0103_0007` | -- **Same low half ⇒ same RBAC + ontology.** Both authorize on `0x0102`; +- **Same high half ⇒ same RBAC + ontology.** Both authorize on `0x0102`; both inherit the `project_role 0x0117` grant lattice — which is already the keystone's worked example (`CLASSID-RBAC-KEYSTONE-SPEC.md` §4, harvested from the Rails/Redmine model). One grant lattice, two apps. -- **Different high half ⇒ different render.** `WorkPackage` renders via +- **Different low half ⇒ different render.** `WorkPackage` renders via OpenProject's ClassView/template (`0x0001`); `Issue` renders via Redmine's (`0x0007`) — distinct field layouts, distinct Askama templates, **zero** concept duplication. This is precisely why the - high u16 exists (§3.5). + low u16 exists (§3.5 of the layout doc). Steps (OpenProject-nexgen-rs is the live migrating consumer; Redmine is the alias-twin that proves the model — any Redmine Rust consumer follows the identical pattern): -1. **Pull the concept low half statically.** Repoint off any +1. **Pull the concept high half statically.** Repoint off any `OpenProjectBridge` / `RedmineBridge` usage to `OpenProjectPort::class_id(name)` / `RedminePort::class_id(name)`; - form the render classid `app_prefix << 16 | concept` (e.g. - `0x0001_0000 | 0x0102`). + form the render classid `(concept << 16) | app_prefix` (e.g. + `0x0102_0001`). 2. **Delete the bridges.** No hand-rolled registry/hydration; the consumer holds no ontology. 3. **Enrich by classid.** Redmine's per-project visibility + role-based access, OpenProject's work-package permissions, become the row-scope axis — compiled to a bitmap, **not** runtime domain-eval (Firewall). -4. **Authorize by the low half.** `authorize(actor, 0x0102, op)` — the +4. **Authorize by the high half.** `authorize(actor, 0x0102, op)` — the shared project grant lattice. Both apps, one upstream resolution. 5. **Render by the full classid.** Each app's ClassView selects its own template; fields resolve key-value against content stores (no serde — `APP-CLASS-CODEBOOK-LAYOUT.md` §3.6). 6. **Private mint only if** an app ships a genuinely non-canonical - project object (none known today) → `0x0001_FFCC` / `0x0007_FFCC`. + project object (none known today) → `0xFFCC_0001` / `0xFFCC_0007`. Cross-ref: `OPENPROJECT-TRANSCODING.md`, `CLASSID-RBAC-KEYSTONE-SPEC.md` §4 (the project_role lattice is the @@ -114,13 +119,13 @@ canonical RBAC worked example). **Port:** `WoaPort` exists (OGAR #93), maps `WorkOrder` and friends onto the commerce block. **Private codebook: no** — work orders, line items, -invoices, dunning stages are canonical commerce concepts (`0x0000_02CC`). +invoices, dunning stages are canonical commerce concepts (`0x02CC_0000`). Steps: 1. Repoint `src/registry.rs`, `src/unified_bridge.rs`, `src/lib.rs`, `tests/…` off `lance_graph_ontology::bridges::WoaBridge` to the static pull: `WoaPort::class_id(name) -> Option`, widened to - `0x0000_0000 | id` at the u32 boundary. + `(id << 16) | 0x0000` at the u32 boundary. 2. Delete `WoaBridge` (= `UnifiedBridge`) + any hand-rolled registry/hydration. 3. Enrich by classid (tenant scoping, Mahnwesen stage rules) — this is @@ -134,7 +139,7 @@ Steps: crate. 6. **Private mint only if** WoA has a genuinely non-canonical object (e.g. a Stefan-specific KeePass-vault row with no commerce analogue) - → `0x0003_FFCC`. Default: none. + → `0xFFCC_0003`. Default: none. Spec cross-ref: `woa-rs/.claude/board/` OGAR-migration note; behaviour-parity stays the witness (Python writer ↔ Rust writer). @@ -145,7 +150,7 @@ behaviour-parity stays the witness (Python writer ↔ Rust writer). **Port:** `SmbPort` exists (OGAR #93). **Private codebook: no** — SKR04 accounts, customers, suppliers, work orders, FiBu reconciliation map -onto canonical commerce (`0x0000_02CC`). +onto canonical commerce (`0x02CC_0000`). Steps: 1. `crates/smb-bridge/src/unified_bridge_wiring.rs` drops `OgitBridge`; @@ -158,7 +163,7 @@ Steps: additive-only — this migration edits **only** smb-office-rs + (if a port gap) OGAR. The spine is untouched. 6. **Private mint only if** an SMB object has no commerce analogue → - `0x0004_FFCC`. Default: none. + `0xFFCC_0004`. Default: none. Tracked: `smb-office-rs/.claude/board/TECH_DEBT.md` `TD-OGAR-CONSUMER-MIGRATION-1`. @@ -187,7 +192,7 @@ Steps (this is convergence, not just a repoint): row-scope compiled to a bitmap (NOT runtime domain-eval — that would violate the Firewall). 5. **Private mint only if** an Odoo module ships an object with no - canonical commerce analogue → `0x0002_FFCC`. Most map onto core. + canonical commerce analogue → `0xFFCC_0002`. Most map onto core. Cross-ref: `ODOO-TRANSCODING.md`, `SURREAL-AST-AS-ADAPTER.md`. @@ -210,14 +215,14 @@ Sub-steps, in order: 2. **Author `Q2Port: PortSpec`** in `ogar-vocab::ports` (its own OGAR PR). Map q2's public entity names → classids. For entities that ARE canonical (a generic `document`, `person`, `organization`, - `location`), map onto core (`0x0000_07CC` osint or the relevant + `location`), map onto core (`0x07CC_0000` osint or the relevant domain). For entities that are q2-specific (a Gotham investigation case, an aiwar scenario branch, a neo4j-native node label with no canonical analogue), mint **app-private** under `0x0006`: ``` - 0x0006_01CC q2/Gotham object classes - 0x0006_02CC q2/aiwar scenario + branch classes - 0x0006_03CC q2/neo4j legacy node/rel adapter classes (if still live) + 0x01CC_0006 q2/Gotham object classes + 0x02CC_0006 q2/aiwar scenario + branch classes + 0x03CC_0006 q2/neo4j legacy node/rel adapter classes (if still live) ``` This is the operator's "codebook per project" win in its purest form: q2 gets a full 65 536-class private space, its own centroid-codebook @@ -241,8 +246,8 @@ Every app's render path migrates to the same shape - **Template dispatch becomes classid-driven.** Replace ad-hoc `match entity_kind { … }` template selection with the object's full - classid → `ClassView::resolve(classid)` → Askama template. The **high - u16** is the app's render prefix; the **low u16** is the shared concept + classid → `ClassView::resolve(classid)` → Askama template. The **low + u16** is the app's render prefix; the **high u16** is the shared concept (RBAC + ontology). woa-rs and smb-office-rs already use Askama (compile-time-checked) — only the *selection* changes, not the engine. - **Fields become keys, not blobs.** Strings, text, media, online @@ -257,7 +262,7 @@ Every app's render path migrates to the same shape This is the operator's stated goal: strings / text / media / online sources rendered via key-value, so no serialization exists in the hot -path. The per-app classid (high u16) is what makes per-app rendering +path. The per-app classid (low u16) is what makes per-app rendering scale without forking concept ids or overflowing the shared codebook. ## Convergence with the RBAC keystone (all waves) @@ -273,7 +278,7 @@ authorize(actor, classid, op) -> Allow | Deny ``` The auth providers (Zitadel / Zanzibar / Ory-Keto) are **core** -preminted class profiles (`0x0000_0BCC`), so token→actor resolution is +preminted class profiles (`0x0BCC_0000`), so token→actor resolution is shared by every app — no per-app auth wiring. An app hands a classid; the grant lattice is upstream. Until the keystone lands, each app keeps its existing auth (do NOT reintroduce a bridge as a stopgap). diff --git a/docs/CLASSID-RBAC-KEYSTONE-SPEC.md b/docs/CLASSID-RBAC-KEYSTONE-SPEC.md index b701b39..ba71a88 100644 --- a/docs/CLASSID-RBAC-KEYSTONE-SPEC.md +++ b/docs/CLASSID-RBAC-KEYSTONE-SPEC.md @@ -126,14 +126,16 @@ hardened to restrictive default-deny. > **Correction (2026-06-22, supersedes the `0x011B`–`0x011E` ids below):** > auth classes do NOT belong in the project block (`0x01XX`). Per > `APP-CLASS-CODEBOOK-LAYOUT.md` §2, auth is a **core domain of its own, -> `0x0B`** (cross-app, provider-agnostic profiles → core, `hi = 0x0000`). -> Mint: `auth_store = 0x0000_0B01`, `auth_zitadel = 0x0000_0B02`, -> `auth_zanzibar = 0x0000_0B03`, `auth_ory_keto = 0x0000_0B04`. The -> `0x011B`–`0x011E` ids in this section are the earlier (project-block) -> draft, retained for provenance only — use the `0x0B` domain. -> Everything else in §7 (the mapping behaviour, I-K7, the Zitadel 1:1) -> stands unchanged. Auth classes still target `actor 0x0104` / `role -> 0x0117` whose low halves are shared core concepts. +> `0x0B`** (cross-app, provider-agnostic profiles → core, `lo = 0x0000`). +> Mint: `auth_store = 0x0B01_0000`, `auth_zitadel = 0x0B02_0000`, +> `auth_zanzibar = 0x0B03_0000`, `auth_ory_keto = 0x0B04_0000`. (Order +> flipped 2026-07-02 — canon HIGH / custom LOW; pre-flip docs and baked +> data use the legacy order.) The `0x011B`–`0x011E` ids in this section +> are the earlier (project-block) draft, retained for provenance only — +> use the `0x0B` domain. Everything else in §7 (the mapping behaviour, +> I-K7, the Zitadel 1:1) stands unchanged. Auth classes still target +> `actor 0x0104` / `role 0x0117` whose high halves are shared core +> concepts. > > **MINTED + CONFIRMED (2026-06-23):** the `0x0B` family is now in code > — `ogar_vocab::class_ids::{AUTH_STORE 0x0B01, AUTH_ZITADEL 0x0B02, diff --git a/docs/DISCOVERY-MAP.md b/docs/DISCOVERY-MAP.md index 1a5a650..3c14587 100644 --- a/docs/DISCOVERY-MAP.md +++ b/docs/DISCOVERY-MAP.md @@ -610,3 +610,45 @@ isolation. The map's job is to keep them visible. consumed by q2; CPIC likewise its own Genetics domain (`0x0E`) under q2. Do NOT re-mint OSINT concept rows; the codebook section carries the guard note. + +--- + +- **D-CLASSID-CANON-HIGH-FLIP (classid half-order flip — canon concept + now HIGH, APP/render prefix now LOW; 2026-07-02; [G], operator + triggered):** the operator's `0x07:01::1000` mnemonic ("domain 0x07, + appid 0x01=q2, custom marker 0x1000" — human-readable as + `domain:appid::marker`) exposed that the working composed-classid + order had the APP/render prefix in the **high** u16 and the canonical + concept in the **low** u16 — backwards from how the mnemonic reads + (domain/concept first, appid second). **Ruling:** flip the composed + order to `classid : u32 = [hi u16: canon concept][lo u16: APP/render + prefix]` — the mnemonic's read order becomes the storage order. + `ogar_vocab::app::{render_classid, app_of, concept_of}` flipped in + lockstep with lance-graph-contract's `CLASSID_ORDER = CanonHigh` (PR + #628 there): `app_of` now reads `classid as u16`; `concept_of` now + reads `classid >> 16`. **APP_PREFIX *values* are unchanged** (`0x0000` + Core, `0x0001` OpenProject, `0x0002` Odoo, `0x0003` WoA, `0x0004` SMB, + `0x0005` Healthcare, `0x0007` Redmine) — only their **position** + moves (hi → lo). V3 marker forms move in lockstep: `0x1000_0700` → + `0x0701_1000`; FMA `0x1000_0A01` → `0x0A01_1000`; CPIC `0x1000_0E00` → + `0x0E01_1000` (appid normalized `:00`→`:01` in the same pass). Auth + RBAC literals: `0x0000_0B01`→`0x0B01_0000`, + `0x0000_0B02`→`0x0B02_0000`, `0x0000_0B03`→`0x0B03_0000`, + `0x0000_0B04`→`0x0B04_0000`. **Legacy stored forms resolve via a + read-only registry alias** (mint-forward doctrine, RESERVE-DON'T- + RECLAIM held) — no data is rewritten, and pre-flip docs are annotated + in place rather than deleted; retirement of the legacy-alias path is + gated on a corpus proof, not assumed. **This supersedes the order + stated in D-APPCLASS** (`classid = APP(hi u16) ‖ class(lo u16)`, + 2026-06-22) **and the `0x1000_0701` literal in + D-OSINT-APPID-NOT-CONCEPT** (2026-07-02, same-day predecessor) — both + entries stand as written (append-only; do not edit), this entry is + the correction of record for the half-order going forward. Doc sweep: + `APP-CLASS-CODEBOOK-LAYOUT.md`, `APP-CODEBOOK-MIGRATION-PLAN.md`, + `OGAR-CONSUMER-BEST-PRACTICES.md`, `OGAR-TRANSPILE-SUBSTRATE.md`, + `OGAR-AS-IR.md`, `SURREAL-AST-TRAP-PREFLIGHT.md`, + `NODEGUID-CANON-AUDIT.md`, `FOUNDRY-ODOO-MARS-LENS.md`, + `CLASSID-RBAC-KEYSTONE-SPEC.md`, `ODOO-REDMINE-OPENPROJECT-LANDING.md`, + `PHILOSOPHY.md`/`PHILOSOPHIE.md`, `README.md`/`README.de.md`, + `integration/AR-OGAR-MAILBOX-INTEGRATION-PLAN.md` §7, and this + repo's `CLAUDE.md`. diff --git a/docs/FOUNDRY-ODOO-MARS-LENS.md b/docs/FOUNDRY-ODOO-MARS-LENS.md index 11e38f4..edd0d84 100644 --- a/docs/FOUNDRY-ODOO-MARS-LENS.md +++ b/docs/FOUNDRY-ODOO-MARS-LENS.md @@ -127,9 +127,9 @@ collapses to one IR with three lowering targets: Foundry's "object explorer" is one `ClassView` (render lens — `docs/APP-CLASS-CODEBOOK-LAYOUT.md`); Odoo's web UI is another; -bardioc's CLI is a third. **Same lo u16 concept, different hi u16 -render prefix** — already the architecture per -`docs/OGAR-CONSUMER-BEST-PRACTICES.md`. +bardioc's CLI is a third. **Same hi u16 concept, different lo u16 +render prefix** (order flipped 2026-07-02 — canon HIGH / custom LOW) — +already the architecture per `docs/OGAR-CONSUMER-BEST-PRACTICES.md`. --- diff --git a/docs/NODEGUID-CANON-AUDIT.md b/docs/NODEGUID-CANON-AUDIT.md index 91838e5..3884932 100644 --- a/docs/NODEGUID-CANON-AUDIT.md +++ b/docs/NODEGUID-CANON-AUDIT.md @@ -47,8 +47,11 @@ bone anchor specifically. The OGAR codebook is canon; the wrapper realigns. ### F-2 — `[G]` **classid width: FMA `Guid` carries 2 bytes, canon carries 4.** -The canon classid is `u32 = [app:u16][concept:u16]` (`app_of` / `concept_of`, -`ogar-vocab/src/app.rs`). The FMA `Guid` tier 0 holds only the **concept** half +The canon classid is `u32 = [concept:u16][app:u16]` (`app_of` reads +`classid as u16`, `concept_of` reads `classid >> 16` — order flipped +2026-07-02, canon HIGH / custom LOW; `ogar-vocab/src/app.rs`). Pre-flip +docs and already-baked data use the legacy `[app:u16][concept:u16]` +order. The FMA `Guid` tier 0 holds only the **concept** half (`0x0A03`), i.e. the `app = 0x0000` (core/default-render) projection. For a non-zero app render prefix it must widen to 4 bytes (tiers 0–1), shifting HEEL to tier 2. **Reconciliation:** treat the FMA `Guid` as the *core-render* (app=0) diff --git a/docs/ODOO-REDMINE-OPENPROJECT-LANDING.md b/docs/ODOO-REDMINE-OPENPROJECT-LANDING.md index fec4f02..31eab2d 100644 --- a/docs/ODOO-REDMINE-OPENPROJECT-LANDING.md +++ b/docs/ODOO-REDMINE-OPENPROJECT-LANDING.md @@ -21,9 +21,11 @@ - **Connecting tissue** (BBB-allowed in any consumer binary): `lance-graph-contract` / `-ontology` / `-ogar` / `-callcenter`. The BBB bars only the **brain/engine** crates (planner, cognitive engine). -- **"Odoo" / "Redmine" are render labels** (the hi-u16 `AppPrefix`): Odoo=`0x0002`, - OpenProject=`0x0001`, Redmine=`0x0007`. The **lo-u16 canonical concept** is the - shared, agnostic identity. Same concept across apps ⇒ same lo-u16. +- **"Odoo" / "Redmine" are render labels** (the lo-u16 `AppPrefix`): Odoo=`0x0002`, + OpenProject=`0x0001`, Redmine=`0x0007`. The **hi-u16 canonical concept** is the + shared, agnostic identity. Same concept across apps ⇒ same hi-u16. + (Order flipped 2026-07-02 — canon HIGH / custom LOW; pre-flip docs and + baked data use the legacy order.) ## 1. Mission: native + agnostic + truthful diff --git a/docs/OGAR-AS-IR.md b/docs/OGAR-AS-IR.md index d1ad7b3..0ff2e9e 100644 --- a/docs/OGAR-AS-IR.md +++ b/docs/OGAR-AS-IR.md @@ -74,7 +74,7 @@ The docs below were already compiler-shaped in design. This framing makes the la | [THE-FIREWALL.md](THE-FIREWALL.md) (ADR-022/023) | "No serialization in the hot path" = **IR is the runtime wire-truth; there is no parse step.** Compiled output runs, not source. | | [OGAR-AST-CONTRACT.md](OGAR-AST-CONTRACT.md) | The typed IR header — the OGAR equivalent of an LLVM IR include. `Class`/`ActionDef`/`Identity`/`KausalSpec` are the IR node kinds. | | [SURREAL-AST-AS-ADAPTER.md](SURREAL-AST-AS-ADAPTER.md) | The carved decision that DDL is a **codegen back-end, not the IR**. The structural arm lowers; the behavioural arm cannot survive lowering and stays in the IR. | -| [APP-CLASS-CODEBOOK-LAYOUT.md](APP-CLASS-CODEBOOK-LAYOUT.md) | The symbol-table layout. `hi u16 ‖ lo u16` = **object-file identifier ‖ symbol id.** The APP prefix selects a lowering target's view; the concept is the linker-canonical name. | +| [APP-CLASS-CODEBOOK-LAYOUT.md](APP-CLASS-CODEBOOK-LAYOUT.md) | The symbol-table layout. `hi u16 ‖ lo u16` = **symbol id ‖ object-file identifier** (order flipped 2026-07-02 — canon HIGH / custom LOW; pre-flip docs and baked data use the legacy order). The concept is the linker-canonical name; the APP prefix selects a lowering target's view. | | [CLASSID-RBAC-KEYSTONE-SPEC.md](CLASSID-RBAC-KEYSTONE-SPEC.md) | The pending **semantic-analysis pass.** `authorize(actor, classid, op) → AccessDecision` is the verification predicate over the IR. | | [OGAR-CONSUMER-BEST-PRACTICES.md](OGAR-CONSUMER-BEST-PRACTICES.md) | The consumer rule "pull the classid via `PortSpec`, never re-mint" = **link against the public symbol table; do not fork it.** §2's spine-vs-membrane split is **ABI tiering**. | | [CONSUMER-MIGRATION-HOWTO.md](CONSUMER-MIGRATION-HOWTO.md) | The bridge-removal recipe = **switch from a private linker (a bridge wrapping the registry) to the public linker (`PortSpec` / `contract::ogar_codebook`).** | diff --git a/docs/OGAR-CONSUMER-BEST-PRACTICES.md b/docs/OGAR-CONSUMER-BEST-PRACTICES.md index 7fcc542..9db2f07 100644 --- a/docs/OGAR-CONSUMER-BEST-PRACTICES.md +++ b/docs/OGAR-CONSUMER-BEST-PRACTICES.md @@ -14,6 +14,12 @@ > Read once before authoring consumer code. Pattern-match against the > examples; if your code doesn't look like one of them, stop and check. > +> **Order flipped 2026-07-02 — canon HIGH / custom LOW.** Every worked +> example below now reads `classid = concept(hi u16) ‖ APP(lo u16)` — +> the canonical concept sits in the high u16, the per-app render prefix +> sits in the low u16. Pre-flip material and already-baked data use the +> legacy order; see `docs/DISCOVERY-MAP.md` D-CLASSID-CANON-HIGH-FLIP. +> > Status: **BEST PRACTICE v1** (2026-06-22). Append-only. > > Companions: `APP-CLASS-CODEBOOK-LAYOUT.md` (the layout spec), @@ -32,21 +38,22 @@ Both halves of the classid are address dimensions, never behavior: ``` -classid : u32 = 0xAAAA ‖ 0xDDCC (8 nibbles) +classid : u32 = 0xDDCC ‖ 0xAAAA (8 nibbles) │ │ - │ └─ lo u16: WHICH CONCEPT ─┐ - │ (DD = domain, │ - │ CC = concept index) │ BOTH halves - │ shared across all apps ├─ are pure + │ └─ lo u16: WHOSE RENDER ─┐ + │ (APP / ClassView / │ + │ Askama template) │ BOTH halves + │ per-app; never shared ├─ are pure │ │ ADDRESS. - └─ hi u16: WHOSE RENDER ───────────────┤ Neither - (APP / ClassView / Askama template) │ carries - per-app; never shared ─┘ behavior. + └─ hi u16: WHICH CONCEPT ───────────────┤ Neither + (DD = domain, │ carries + CC = concept index) │ behavior. + shared across all apps ─┘ ──────► resolves to ──────► │ - ├─ ClassView the SKIN (render shape, per-app — picked by hi) - ├─ Class the SHAPE (structural — canonical, picked by lo) + ├─ ClassView the SKIN (render shape, per-app — picked by lo) + ├─ Class the SHAPE (structural — canonical, picked by hi) └─ ActionDef + the MAGIC (behavioral — lifecycle, callbacks, KausalSpec validations; ALWAYS in OGAR Core, NEVER in DDL or in the address) @@ -55,7 +62,7 @@ classid : u32 = 0xAAAA ‖ 0xDDCC (8 nibbles) **Three drilled corollaries:** 1. **The address is dumb.** Knowing the classid tells you the *shape* + *skin* + which Core node to fetch — it tells you **nothing** about behavior. Behavior lives at the resolution target. -2. **Class-magic ≠ render-magic.** The hi u16 chooses **render** magic (which app's template), not **class** magic (which callbacks/lifecycle). Class magic is the Core's; render magic is the app's. +2. **Class-magic ≠ render-magic.** The lo u16 chooses **render** magic (which app's template), not **class** magic (which callbacks/lifecycle). Class magic is the Core's; render magic is the app's. 3. **You can't smuggle magic into the address.** Encoding behavior in DDL constructs (`DEFINE EVENT … WHEN … THEN …`) puts magic where only the address lives — the trap `SURREAL-AST-TRAP-PREFLIGHT.md` exists to prevent. --- @@ -65,41 +72,41 @@ classid : u32 = 0xAAAA ‖ 0xDDCC (8 nibbles) Memorize these. They appear across every consumer doc; recognizing them on sight is the muscle. ``` -0x0000_0901 ─── canonical patient (core anchor, no app skin) +0x0901_0000 ─── canonical patient (core anchor, no app skin) │ │ - │ └── 0x09 = Health domain · 0x01 = patient concept - └───────── 0x0000 = core (shared, no app prefix) + │ └── 0x0000 = core (shared, no app prefix) + └───────── 0x09 = Health domain · 0x01 = patient concept -0x0005_0901 ─── Medcare's patient — same concept, Medcare's render lens +0x0901_0005 ─── Medcare's patient — same concept, Medcare's render lens ↑ ↑ - │ └── same 0x0901 = same RBAC grant, same ontology, same OGIT identity - └───────── 0x0005 = Medcare's APP prefix → Medcare's ClassView + Askama template + │ └── 0x0005 = Medcare's APP prefix → Medcare's ClassView + Askama template + └───────── same 0x0901 = same RBAC grant, same ontology, same OGIT identity -0x0001_0102 ─── OpenProject's WorkPackage -0x0007_0102 ─── Redmine's Issue +0x0102_0001 ─── OpenProject's WorkPackage +0x0102_0007 ─── Redmine's Issue ↑ ↑ - │ └── BOTH lo = 0x0102 = project_work_item + │ └── BOTH hi = 0x0102 = project_work_item │ → ONE RBAC grant lattice (project_role 0x0117) │ → ONE ontology shape - └───────────────── DIFFERENT hi u16 → DIFFERENT templates, zero concept dup + └───────────────── DIFFERENT lo u16 → DIFFERENT templates, zero concept dup -0x0003_0103 ─── WoA's Stundenzettel (billable_work_entry) -0x0004_0103 ─── SMB's Stundenzettel -0x0001_0103 ─── OpenProject's TimeEntry (planner side) -0x0007_0103 ─── Redmine's TimeEntry (planner side) -0x0002_0103 ─── Odoo's HrAttendance / account.move.line(qty=hours) +0x0103_0003 ─── WoA's Stundenzettel (billable_work_entry) +0x0103_0004 ─── SMB's Stundenzettel +0x0103_0001 ─── OpenProject's TimeEntry (planner side) +0x0103_0007 ─── Redmine's TimeEntry (planner side) +0x0103_0002 ─── Odoo's HrAttendance / account.move.line(qty=hours) ↑ ↑ - │ └── ALL share lo = 0x0103 = BILLABLE_WORK_ENTRY - │ → ONE canonical billable-time concept - │ → the cross-fork convergence pin (OGAR #93/#94/#96) - └───────────────── FIVE app-private renders of the same concept - "Planner times align with billable hours" = - a codebook lookup, not a translation layer. + │ └── FIVE app-private renders of the same concept + │ "Planner times align with billable hours" = + │ a codebook lookup, not a translation layer. + └───────────────── ALL share hi = 0x0103 = BILLABLE_WORK_ENTRY + → ONE canonical billable-time concept + → the cross-fork convergence pin (OGAR #93/#94/#96) ``` **APP prefix allocation** (committed; OGAR #95 §2): -| APP `0xAAAA` | App | Consumer crate | +| APP (lo u16) `0xAAAA` | App | Consumer crate | |---|---|---| | `0x0000` | shared core / no app skin | — | | `0x0001` | OpenProject | openproject-nexgen-rs | @@ -109,7 +116,7 @@ Memorize these. They appear across every consumer doc; recognizing them on sight | `0x0005` | Medcare / Healthcare | medcare-rs | | `0x0007` | Redmine | (consumer TBD) | -**Domain bytes** (committed; the high byte of the lo u16): +**Domain bytes** (committed; the high byte of the hi u16): | `0xDD` | Domain | |---|---| @@ -219,9 +226,9 @@ split as Pattern 1. use ogar_vocab::ports::{HealthcarePort, PortSpec}; let cid: u16 = HealthcarePort::class_id("Patient").unwrap(); // 0x0901 -let render_classid: u32 = HealthcarePort::APP_PREFIX | (cid as u32); -// 0x0005_0000 | 0x0901 -// = 0x0005_0901 ← Medcare's patient render address +let render_classid: u32 = ((cid as u32) << 16) | HealthcarePort::APP_PREFIX; +// 0x0901_0000 | 0x0005 +// = 0x0901_0005 ← Medcare's patient render address // → resolves to: // ClassView = Medcare's clinical patient view (Askama template: @@ -235,12 +242,12 @@ let render_classid: u32 = HealthcarePort::APP_PREFIX | (cid as u32); Worked examples by app: ```rust -HealthcarePort::APP_PREFIX | 0x0901 → 0x0005_0901 // Medcare patient -WoaPort::APP_PREFIX | 0x0103 → 0x0003_0103 // WoA Stundenzettel -SmbPort::APP_PREFIX | 0x0204 → 0x0004_0204 // SMB Kunde -OdooPort::APP_PREFIX | 0x0103 → 0x0002_0103 // Odoo HrAttendance -OpenProjectPort::APP_PREFIX | 0x0102 → 0x0001_0102 // OpenProject WorkPackage -RedminePort::APP_PREFIX | 0x0102 → 0x0007_0102 // Redmine Issue +(0x0901u32 << 16) | HealthcarePort::APP_PREFIX → 0x0901_0005 // Medcare patient +(0x0103u32 << 16) | WoaPort::APP_PREFIX → 0x0103_0003 // WoA Stundenzettel +(0x0204u32 << 16) | SmbPort::APP_PREFIX → 0x0204_0004 // SMB Kunde +(0x0103u32 << 16) | OdooPort::APP_PREFIX → 0x0103_0002 // Odoo HrAttendance +(0x0102u32 << 16) | OpenProjectPort::APP_PREFIX → 0x0102_0001 // OpenProject WorkPackage +(0x0102u32 << 16) | RedminePort::APP_PREFIX → 0x0102_0007 // Redmine Issue ``` #### Pattern 2b — membrane (BBB-safe, per lance-graph #592) @@ -253,7 +260,7 @@ let render: Option = render_classid_for_concept( AppPrefix::Healthcare, "patient", ); -// → Some(0x0005_0901) +// → Some(0x0901_0005) ``` Or split (pull then stamp), for symmetry with Pattern 1b: @@ -262,7 +269,7 @@ Or split (pull then stamp), for symmetry with Pattern 1b: use lance_graph_contract::ogar_codebook::{canonical_concept_id, AppPrefix}; let cid: u16 = canonical_concept_id("patient").unwrap(); // 0x0901 -let render: u32 = AppPrefix::Healthcare.render(cid); // 0x0005_0901 +let render: u32 = AppPrefix::Healthcare.render(cid); // 0x0901_0005 ``` `AppPrefix` is the OGAR #95 §2 allocation table mirrored into the @@ -275,25 +282,25 @@ hand-stamps `0x000N`** — both halves come from one source. Worked examples (mirror of 2a, via the contract): ```rust -AppPrefix::Healthcare.render(0x0901) → 0x0005_0901 // Medcare patient -AppPrefix::Woa.render(0x0103) → 0x0003_0103 // WoA Stundenzettel -AppPrefix::Smb.render(0x0204) → 0x0004_0204 // SMB Kunde -AppPrefix::Odoo.render(0x0103) → 0x0002_0103 // Odoo HrAttendance -AppPrefix::OpenProject.render(0x0102) → 0x0001_0102 // OpenProject WorkPackage -AppPrefix::Redmine.render(0x0102) → 0x0007_0102 // Redmine Issue +AppPrefix::Healthcare.render(0x0901) → 0x0901_0005 // Medcare patient +AppPrefix::Woa.render(0x0103) → 0x0103_0003 // WoA Stundenzettel +AppPrefix::Smb.render(0x0204) → 0x0204_0004 // SMB Kunde +AppPrefix::Odoo.render(0x0103) → 0x0103_0002 // Odoo HrAttendance +AppPrefix::OpenProject.render(0x0102) → 0x0102_0001 // OpenProject WorkPackage +AppPrefix::Redmine.render(0x0102) → 0x0102_0007 // Redmine Issue ``` ```rust // ANTI — hardcode the APP prefix as a magic constant -const MEDCARE_APP: u32 = 0x0005_0000; // ← drifts from PortSpec -let render = MEDCARE_APP | (cid as u32); // if APP allocation changes +const MEDCARE_APP: u32 = 0x0005; // ← drifts from PortSpec +let render = ((cid as u32) << 16) | MEDCARE_APP; // if APP allocation changes // ANTI — bit-shift inline -let render = ((0x0005u32) << 16) | (cid as u32); // ← un-typed; lose source-of-truth +let render = ((cid as u32) << 16) | 0x0005u32; // ← un-typed; lose source-of-truth -// ANTI — store full u32 render classid where lo u16 would do (RBAC, ontology) +// ANTI — store full u32 render classid where hi u16 would do (RBAC, ontology) fn authorize(actor: &Actor, render_cid: u32, op: Op) { … } -// ^^^^^^^^^^^^ shared grant lattice keys on LO u16; +// ^^^^^^^^^^^^ shared grant lattice keys on HI u16; // passing the full u32 leaks render lens // into auth (concept is shared, render is not) ``` @@ -310,7 +317,7 @@ use lance_graph_rbac::authorize; let concept: u16 = HealthcarePort::class_id("Patient").unwrap(); // 0x0901 let decision = authorize(&actor, concept, Op::Read); -// ^^^^^^^ KEY ON LO u16: shared grant lattice +// ^^^^^^^ KEY ON HI u16: shared grant lattice // across all health apps ``` @@ -380,12 +387,12 @@ shapes negatively is half the muscle memory. | Anti-pattern | Right shape (§reference) | Why it bites | |---|---|---| | Re-mint a canonical classid locally (`const PATIENT = 0x0901`) | §2 Pattern 1 | Bypasses PortSpec — loses alias table, alias-to-id stability is a per-Port guarantee | -| Hardcode `APP_PREFIX` as a literal (`0x0005_0000`) | §2 Pattern 2 | Drifts from the typed source; APP allocation changes break silently | -| Pass full u32 render classid to authorize() | §2 Pattern 3 (note) | Auth keys on LO u16; passing full u32 leaks render lens into grant lookup | +| Hardcode `APP_PREFIX` as a literal (`0x0005`) | §2 Pattern 2 | Drifts from the typed source; APP allocation changes break silently | +| Pass full u32 render classid to authorize() | §2 Pattern 3 (note) | Auth keys on HI u16; passing full u32 leaks render lens into grant lookup | | Use the deprecated `lance_graph_ogar::bridges::*` re-export | §2 Pattern 4a | The beacon will warn (lance-graph #589/#590); canonical path is `ogar_vocab::ports` | | Re-introduce `UnifiedBridge` as a stopgap while keystone is pending | §2 Pattern 3 (anti) | Replaces one deprecated wrapper with the same wrapper. Wait for the real authorize | | Smuggle behavior into DDL via `DEFINE EVENT … WHEN … THEN …` | `SURREAL-AST-TRAP-PREFLIGHT.md` | Behavior belongs in OGAR Core (`ActionDef` + `KausalSpec`), not in the address-side artifact | -| Mint a per-tenant low-u16 to dodge sharing the canonical class | §0 corollary 1 | The point of the lo u16 is sharing; per-tenant variation lives in the HI u16 (render lens) | +| Mint a per-tenant high-u16 to dodge sharing the canonical class | §0 corollary 1 | The point of the hi u16 is sharing; per-tenant variation lives in the LO u16 (render lens) | --- @@ -396,14 +403,14 @@ To cement: the same patient concept seen through every pattern. ```rust // The address — pure routing identity let concept_cid: u16 = HealthcarePort::class_id("Patient").unwrap(); // 0x0901 -let render_cid: u32 = HealthcarePort::APP_PREFIX | (concept_cid as u32); -// = 0x0005_0901 +let render_cid: u32 = ((concept_cid as u32) << 16) | HealthcarePort::APP_PREFIX; +// = 0x0901_0005 // The address resolves to (no magic on the address itself): // -// render_cid (0x0005_0901) +// render_cid (0x0901_0005) // │ -// ├──► ClassView::resolve(0x0005_0901) → Medcare's patient view +// ├──► ClassView::resolve(0x0901_0005) → Medcare's patient view // │ (template: patient.html, // │ PII leaf-rename at adapter, // │ German labels stripped at membrane) @@ -416,13 +423,13 @@ let render_cid: u32 = HealthcarePort::APP_PREFIX | (concept_cid as u32); // before_save_audit, // after_destroy_archive, …] // (canonical Healthcare lifecycle — -// ALL apps share these, hi u16 doesn't matter) +// ALL apps share these, lo u16 doesn't matter) // Pattern 1: pull the classid let cid = HealthcarePort::class_id("Patient").unwrap(); // Pattern 2: compose the render -let render = HealthcarePort::APP_PREFIX | (cid as u32); +let render = ((cid as u32) << 16) | HealthcarePort::APP_PREFIX; // Pattern 3: authorize (interim — keystone pending) let decision = static_role_check(actor, "physician"); // existing medcare-rbac path diff --git a/docs/OGAR-TRANSPILE-SUBSTRATE.md b/docs/OGAR-TRANSPILE-SUBSTRATE.md index e58e2fb..b5bad78 100644 --- a/docs/OGAR-TRANSPILE-SUBSTRATE.md +++ b/docs/OGAR-TRANSPILE-SUBSTRATE.md @@ -272,21 +272,26 @@ classes from the 16-byte key alone. ### 2.1 The classid (the cross-app join key) +> Order flipped 2026-07-02 — canon HIGH / custom LOW: the canonical +> concept sits in the high u16, the app render prefix sits in the low +> u16. Pre-flip docs and already-baked data use the legacy order; see +> `docs/DISCOVERY-MAP.md` D-CLASSID-CANON-HIGH-FLIP. + ``` -render classid (u32) = (APP_PREFIX as u32) << 16 | concept (u16) - └── high u16 ──┘ └── low u16 ──┘ - the app RENDER skin the SHARED concept +render classid (u32) = (concept as u32) << 16 | APP_PREFIX (u16) + └── high u16 ──┘ └── low u16 ──┘ + the SHARED concept the app RENDER skin ``` -- **low u16 = the shared concept** — resolved by `PortSpec::class_id` against +- **high u16 = the shared concept** — resolved by `PortSpec::class_id` against the OGAR codebook (`ogar_vocab::class_ids` / `ogar_codebook`). This is the de-facto `owl:equivalentClass` expressed as a u16. -- **high u16 = the app render skin** — `PortSpec::APP_PREFIX`. Picks the +- **low u16 = the app render skin** — `PortSpec::APP_PREFIX`. Picks the per-app `ClassView` / template; **carries no behaviour**. - composed by the canonical `ogar_vocab::app::render_classid_for::

(concept)`. **Cross-app convergence is the payoff.** The same concept across apps gets the -same low u16, so a consumer joins across apps with a `==`: +same high u16, so a consumer joins across apps with a `==`: | concept | id | Odoo (`0x0002`) | OpenProject (`0x0001`) | Redmine (`0x0007`) | WoA/SMB (`0x0003`/`0x0004`) | |---|---|---|---|---|---| @@ -415,11 +420,11 @@ account.move (Odoo Python) │ line_ids → HasMany account.move.line (inverse move_id)] │ computed_fields: [amount_total ← _compute_amount, depends [line_ids.balance]] └─ mint_graph:: ─► Facet - facet_classid = 0x0002_0202 (Odoo 0x0002 | commercial_document 0x0202) + facet_classid = 0x0202_0002 (commercial_document 0x0202 | Odoo 0x0002) ⇒ CompiledClass { class, facet } Cross-checks (all probe-verified): - • facet.to_bytes() ≡ lance_graph_contract::FacetCascade(0x0002_0202) (byte-exact) + • facet.to_bytes() ≡ lance_graph_contract::FacetCascade(0x0202_0002) (byte-exact) • canonical_concept_domain(0x0202) == Commerce (routes to the right ClassView) • grounding resolvable: 0x0202 → fibo:Transaction / DOLCE Perdurant (late, via OGIT) • the GoBD posting (account.move._post) stays od-posting's Rust adapter (the 15%) @@ -446,10 +451,10 @@ correct because of the `relation_kind` predicate (ruff#35): `target` + OgScalar, partner_id: ToOne, line_ids: ToMany }` + `ACCOUNT_MOVE_CLASSID`. - `emit_csharp` → `public sealed record AccountMove { public const uint - ClassId = 0x00020202; public OgScalar name {get;init;} public + ClassId = 0x02020002; public OgScalar name {get;init;} public ToOne partner_id {…} public ToMany line_ids {…} }`. - `emit_python` → `@dataclass class AccountMove: CLASSID: ClassVar[int] = - 0x00020202; name: OgScalar; partner_id: ToOne["ResPartner"]; line_ids: + 0x02020002; name: OgScalar; partner_id: ToOne["ResPartner"]; line_ids: ToMany["AccountMoveLine"]`. All three use the **same wrapper-contract type names** (`OgScalar` / `ToOne` / diff --git a/docs/PHILOSOPHIE.md b/docs/PHILOSOPHIE.md index f5b5e9b..75dcc27 100644 --- a/docs/PHILOSOPHIE.md +++ b/docs/PHILOSOPHIE.md @@ -28,6 +28,12 @@ Du **schöpfst**. Der Rest dieses Dokuments sind die dreißig Jahre. +> Reihenfolge geflippt 2026-07-02 — Kanon HOCH / App-eigen NIEDRIG: das +> kanonische Konzept sitzt im hohen u16, der App-Render-Präfix im +> niedrigen u16. Material von vor dem Flip und bereits gebackene Daten +> nutzen die alte Reihenfolge; siehe `docs/DISCOVERY-MAP.md` +> D-CLASSID-CANON-HIGH-FLIP. + --- ## Die drei Erkenntnisse @@ -43,10 +49,10 @@ Verhalten. Sie zu kennen sagt dir *welcher* Knoten, *wessen* Haut, *welche* Form — nie *wie er sich verhält*. ``` -classid : u32 = 0xAAAA ‖ 0xDDCC beide Hälften sind ADRESSE +classid : u32 = 0xDDCC ‖ 0xAAAA beide Hälften sind ADRESSE │ │ - │ └─ lo u16 — WELCHES KONZEPT (geteilte Identität) - └─────────── hi u16 — WESSEN RENDER (App-eigene Haut) + │ └─ lo u16 — WESSEN RENDER (App-eigene Haut) + └─────────── hi u16 — WELCHES KONZEPT (geteilte Identität) ──────► löst auf zu ──────► ├─ ClassView die HAUT (Render — pro App) @@ -64,14 +70,14 @@ sich. ### 2 · Ein Konzept, viele Render. -Die **unteren** 16 Bit benennen das Konzept — geteilt von jeder App, mit -einem RBAC-Grant und einer Ontologie. Die **oberen** 16 Bit benennen die +Die **oberen** 16 Bit benennen das Konzept — geteilt von jeder App, mit +einem RBAC-Grant und einer Ontologie. Die **unteren** 16 Bit benennen die Render-Linse — das eigene Template jeder App, ihre eigene Haut. Gleiches Konzept, andere Kleidung: ``` -0x0001_0102 ─ OpenProjects WorkPackage ┐ gleiche lo 0x0102 = project_work_item -0x0007_0102 ─ Redmines Issue ┘ → EIN Grant, EINE Ontologie, ZWEI Templates +0x0102_0001 ─ OpenProjects WorkPackage ┐ gleiche hi 0x0102 = project_work_item +0x0102_0007 ─ Redmines Issue ┘ → EIN Grant, EINE Ontologie, ZWEI Templates ``` Fünf Apps können *abrechenbare Zeit* auf fünf Arten rendern und lösen @@ -101,7 +107,7 @@ davon ist je deiner: | Zug | Die Geste | Deiner? | |---|---|---| | **Pull** | `Port::class_id("Patient")` → `0x0901` | nein — reine Funktion | -| **Render** | `APP_PREFIX \| concept` → `0x0005_0901` | nein — typisierter Helfer | +| **Render** | `(concept << 16) \| APP_PREFIX` → `0x0901_0005` | nein — typisierter Helfer | | **Authorize** | `authorize(actor, concept, op)` | nein — das geteilte Grant-Gitter | | **Enrich** | deine Domänen-Logik, an der classid verschlüsselt | **ja** — das ist der Teil, der legitim deiner ist | diff --git a/docs/PHILOSOPHY.md b/docs/PHILOSOPHY.md index fab680c..2e967e3 100644 --- a/docs/PHILOSOPHY.md +++ b/docs/PHILOSOPHY.md @@ -27,6 +27,11 @@ patient. You don't build it. You don't mint it. You **scoop**. The rest of this document is the thirty years. +> Order flipped 2026-07-02 — canon HIGH / custom LOW: the canonical +> concept sits in the high u16, the per-app render prefix sits in the +> low u16. Pre-flip material and already-baked data use the legacy +> order; see `docs/DISCOVERY-MAP.md` D-CLASSID-CANON-HIGH-FLIP. + --- ## The three epiphanies @@ -42,10 +47,10 @@ behaviour. Knowing it tells you *which* node, *whose* skin, *what* shape — never *how it acts*. ``` -classid : u32 = 0xAAAA ‖ 0xDDCC both halves are ADDRESS +classid : u32 = 0xDDCC ‖ 0xAAAA both halves are ADDRESS │ │ - │ └─ lo u16 — WHICH CONCEPT (shared identity) - └─────────── hi u16 — WHOSE RENDER (per-app skin) + │ └─ lo u16 — WHOSE RENDER (per-app skin) + └─────────── hi u16 — WHICH CONCEPT (shared identity) ──────► resolves to ──────► ├─ ClassView the SKIN (render — per-app) @@ -62,13 +67,13 @@ id keys into. The id addresses; the Core behaves. ### 2 · One concept, many renders. -The **low** 16 bits name the concept — shared by every app, with one -RBAC grant and one ontology. The **high** 16 bits name the render lens — +The **high** 16 bits name the concept — shared by every app, with one +RBAC grant and one ontology. The **low** 16 bits name the render lens — each app's own template, its own skin. Same concept, different dress: ``` -0x0001_0102 ─ OpenProject's WorkPackage ┐ same lo 0x0102 = project_work_item -0x0007_0102 ─ Redmine's Issue ┘ → ONE grant, ONE ontology, TWO templates +0x0102_0001 ─ OpenProject's WorkPackage ┐ same hi 0x0102 = project_work_item +0x0102_0007 ─ Redmine's Issue ┘ → ONE grant, ONE ontology, TWO templates ``` Five apps can render *billable time* five ways and still resolve to the @@ -97,7 +102,7 @@ them is ever yours to write: | Move | The gesture | Yours? | |---|---|---| | **Pull** | `Port::class_id("Patient")` → `0x0901` | no — a pure function | -| **Render** | `APP_PREFIX \| concept` → `0x0005_0901` | no — a typed helper | +| **Render** | `(concept << 16) \| APP_PREFIX` → `0x0901_0005` | no — a typed helper | | **Authorize** | `authorize(actor, concept, op)` | no — the shared grant lattice | | **Enrich** | your domain logic keyed on the classid | **yes** — this is the part that's legitimately yours | diff --git a/docs/SURREAL-AST-TRAP-PREFLIGHT.md b/docs/SURREAL-AST-TRAP-PREFLIGHT.md index 037700d..eb4c699 100644 --- a/docs/SURREAL-AST-TRAP-PREFLIGHT.md +++ b/docs/SURREAL-AST-TRAP-PREFLIGHT.md @@ -29,9 +29,11 @@ > resolves to.** (Drilled with worked examples in > `OGAR-CONSUMER-BEST-PRACTICES.md` §0.) -The classid (`0xAAAA_DDCC`) is **pure address**. Both halves — -hi u16 (APP / render lens) and lo u16 (canonical concept) — are -*address dimensions*. Neither carries behavior. +The classid (`0xDDCC_AAAA`) is **pure address**. Both halves — +hi u16 (canonical concept) and lo u16 (APP / render lens) — are +*address dimensions*. Neither carries behavior. (Order flipped +2026-07-02 — canon HIGH / custom LOW; pre-flip docs and baked data use +the legacy order — see `docs/DISCOVERY-MAP.md` D-CLASSID-CANON-HIGH-FLIP.) Where does the behavior live? At the **resolution target**: diff --git a/docs/integration/AR-OGAR-MAILBOX-INTEGRATION-PLAN.md b/docs/integration/AR-OGAR-MAILBOX-INTEGRATION-PLAN.md index 168addb..40f30d4 100644 --- a/docs/integration/AR-OGAR-MAILBOX-INTEGRATION-PLAN.md +++ b/docs/integration/AR-OGAR-MAILBOX-INTEGRATION-PLAN.md @@ -162,7 +162,7 @@ pub struct PredicateName(pub Cow<'static, str>); // contract-side newtype pub struct ActionDef { pub predicate: PredicateName, // const &'static OR owned-harvested - pub object_class: u32, // §7 — low-u16 classid widened here + pub object_class: u32, // §7 — concept widened into the CANON high u16 here // exec / guard / required_role / overrides … } ``` @@ -260,15 +260,22 @@ schema** — never a stored duplicate. ## 7. classid width — resolved (council contract-surface finding) +> **Order flipped 2026-07-02 — canon HIGH / custom LOW.** The +> canonical concept now sits in the CANON high u16, the custom/render +> half in the low u16. This section originally described the pre-flip +> layout (`hi = APP`, `lo = concept`, `classid >> 16 != 0` refusing a +> lossy fold on the app half); the guidance below reflects the new +> order. Pre-flip docs and already-baked data use the legacy order — +> see OGAR `docs/DISCOVERY-MAP.md` D-CLASSID-CANON-HIGH-FLIP. + Verified against `lance-graph-contract` source: `class_view.rs:53` declares `pub type ClassId = u16` (OD-CLASSID-WIDTH ratified); the `NodeGuid` layout -(`canonical_node.rs:23-35`) stores classid in bytes `0..4` as **u32 with the high -u16 canon-reserved zero**; `NiblePath::from_guid_prefix` returns `None` when -`classid >> 16 != 0` (lossy fold refused — no silent truncation); +(`canonical_node.rs:23-35`) stores classid in bytes `0..4` as a **u32**; `register_class_path(entity_type_id: u16, …)` is the narrowing seam; `NodeGuid::new` already `assert!`s family/identity ≤ 24 bits. Not a blocker, not a casting nuance — -a **design asymmetry the producer must honor**: mint into the low u16 only (§5.4), -widen to u32 at the `ActionDef::object_class` boundary. **No contract violation.** +a **design asymmetry the producer must honor**: mint the concept into the CANON +high u16 (`(concept as u32) << 16`) (§5.4); the low u16 is the custom/render +half. **No contract violation.** ## 8. Mailbox-SoA blast radius — verified zero From f7201a5d52f7319187de8b08e44d22269314c624 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 2 Jul 2026 06:16:47 +0000 Subject: [PATCH 2/2] board: add the required Scope line to E-CLASSID-CANON-HIGH-FLIP (codex P2 on #147) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The .claude/AGENTS.md log schema requires a **Scope:** line after **Status:** in every EPIPHANIES entry; the new flip entry (added in this PR, not yet canon) jumped straight to the body. Scope added — the entry's own content is unchanged. Co-Authored-By: Claude --- .claude/board/EPIPHANIES.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.claude/board/EPIPHANIES.md b/.claude/board/EPIPHANIES.md index 7074bbb..7d8cf87 100644 --- a/.claude/board/EPIPHANIES.md +++ b/.claude/board/EPIPHANIES.md @@ -11,6 +11,8 @@ **Status:** FINDING (`[G]`, operator-triggered). Doc-sweep companion to `docs/DISCOVERY-MAP.md` D-CLASSID-CANON-HIGH-FLIP — read that entry for the full ledger; this entry records the same correction in the session-findings log. +**Scope:** the composed 32-bit classid ONLY (`ogar_vocab::app` compose/decompose + every doc/worked example carrying a composed literal). Bare u16 concept ids, `APP_PREFIX` values, the 16-byte GUID key layout, and the HEEL/HIP/TWIG tiers are untouched; already-baked old-order ids stay valid via the legacy registry aliases; consumer repos flip in their own lockstep PRs. + **The trigger:** the operator's `0x07:01::1000` mnemonic — read as `domain:appid::marker` — exposed that the working composed classid stored the APP/render prefix in the **high** u16 and the canonical concept in the **low** u16, backwards from the mnemonic's own read order (domain/concept first, appid second). **The ruling:** flip the composed order to `classid : u32 = [hi u16: canon concept][lo u16: APP/render prefix]`. `ogar_vocab::app::{render_classid, app_of, concept_of}` flip in lockstep with lance-graph-contract's `CLASSID_ORDER = CanonHigh` (PR #628 there) — `app_of` now reads `classid as u16`, `concept_of` now reads `classid >> 16`. **APP_PREFIX values are unchanged** (`0x0000` Core … `0x0007` Redmine) — only their bit position moves. V3 marker forms move in lockstep (`0x1000_0700` → `0x0701_1000`; FMA `0x1000_0A01` → `0x0A01_1000`; CPIC `0x1000_0E00` → `0x0E01_1000`, appid normalized `:00`→`:01`). Auth RBAC literals: `0x0000_0B0N` → `0x0B0N_0000` for N∈{1,2,3,4}.