From 3083553c26759c88c57728e07820ef7c732242a4 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 22 Jun 2026 17:49:56 +0000 Subject: [PATCH 1/2] feat(ogar-vocab): typed PortSpec::APP_PREFIX for the high-u16 render prefix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements the §2 allocation table in APP-CLASS-CODEBOOK-LAYOUT.md as typed associated constants on PortSpec, so consumers re-export the reserved prefix instead of hardcoding hex literals like 0x0001. The trait gains a provided (defaulted) associated const: const APP_PREFIX: u16 = 0x0000; Default 0x0000 is the shared canonical core — the cross-app ontology every consumer reuses. Each of the six app ports overrides with its reserved prefix from the §2 table: OpenProjectPort 0x0001 OdooPort 0x0002 WoaPort 0x0003 SmbPort 0x0004 HealthcarePort 0x0005 RedminePort 0x0007 These values are straight from the §2 allocation table; the doc explicitly states "reserving costs nothing" — no codebook is materialised until an app mints its first private class. This PR is reserving (implementing the table as typed data), not minting a new class_id. Change is non-breaking: the provided default (0x0000) means every PortSpec impl that does not override APP_PREFIX continues to compile and behaves as shared-core, which is the correct default. A new test `app_prefixes_match_the_allocation_table` asserts all six prefix values against the §2 table. Existing 26 tests are unchanged and continue to pass. --- crates/ogar-vocab/src/ports.rs | 56 ++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/crates/ogar-vocab/src/ports.rs b/crates/ogar-vocab/src/ports.rs index 2a4d526..92aaaf4 100644 --- a/crates/ogar-vocab/src/ports.rs +++ b/crates/ogar-vocab/src/ports.rs @@ -56,6 +56,26 @@ 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). + /// + /// Composing the full render classid: + /// ```text + /// render_classid = (APP_PREFIX as u32) << 16 | concept_low_u16 + /// ``` + /// + /// `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. + /// + /// Reserving this prefix costs nothing (§2: "Reserving a prefix costs + /// nothing — no codebook is materialised until the app mints its first + /// private class"). This constant is the allocation table as typed + /// data — consumers re-export it instead of hardcoding `0x0001` etc. + const APP_PREFIX: u16 = 0x0000; + /// All `(port-public-name, canonical-class-id)` aliases for this /// port. Order is not significant for resolution but kept stable /// for human readability. @@ -84,6 +104,8 @@ pub struct OpenProjectPort; impl PortSpec for OpenProjectPort { const NAMESPACE: &'static str = "OpenProject"; const BRIDGE_ID: &'static str = "openproject"; + /// `0x0001` — OpenProject render prefix (§2 allocation table). + const APP_PREFIX: u16 = 0x0001; fn aliases() -> &'static [(&'static str, u16)] { OPENPROJECT_ALIASES } @@ -152,6 +174,8 @@ pub struct RedminePort; impl PortSpec for RedminePort { const NAMESPACE: &'static str = "Redmine"; const BRIDGE_ID: &'static str = "redmine"; + /// `0x0007` — Redmine render prefix (§2 allocation table). + const APP_PREFIX: u16 = 0x0007; fn aliases() -> &'static [(&'static str, u16)] { REDMINE_ALIASES } @@ -221,6 +245,8 @@ pub struct HealthcarePort; impl PortSpec for HealthcarePort { const NAMESPACE: &'static str = "Healthcare"; const BRIDGE_ID: &'static str = "medcare"; + /// `0x0005` — Medcare / Healthcare render prefix (§2 allocation table). + const APP_PREFIX: u16 = 0x0005; fn aliases() -> &'static [(&'static str, u16)] { HEALTHCARE_ALIASES } @@ -270,6 +296,8 @@ pub struct WoaPort; impl PortSpec for WoaPort { const NAMESPACE: &'static str = "WorkOrder"; const BRIDGE_ID: &'static str = "woa"; + /// `0x0003` — WoA render prefix (§2 allocation table). + const APP_PREFIX: u16 = 0x0003; fn aliases() -> &'static [(&'static str, u16)] { WOA_ALIASES } @@ -353,6 +381,8 @@ pub struct SmbPort; impl PortSpec for SmbPort { const NAMESPACE: &'static str = "SMB"; const BRIDGE_ID: &'static str = "smb"; + /// `0x0004` — SMB-Office render prefix (§2 allocation table). + const APP_PREFIX: u16 = 0x0004; fn aliases() -> &'static [(&'static str, u16)] { SMB_ALIASES } @@ -420,6 +450,8 @@ pub struct OdooPort; impl PortSpec for OdooPort { const NAMESPACE: &'static str = "Odoo"; const BRIDGE_ID: &'static str = "odoo"; + /// `0x0002` — Odoo render prefix (§2 allocation table). + const APP_PREFIX: u16 = 0x0002; fn aliases() -> &'static [(&'static str, u16)] { ODOO_ALIASES } @@ -925,6 +957,30 @@ mod tests { assert_eq!(OdooPort::class_id(""), None); } + /// Pins all six APP_PREFIX overrides against the §2 allocation table + /// in `APP-CLASS-CODEBOOK-LAYOUT.md`. The default in the trait is + /// `0x0000` (shared canonical core); each app port overrides with its + /// reserved high-u16 so consumers can re-export the typed constant + /// instead of hardcoding hex literals. + /// + /// Reserving a prefix costs nothing (§2): no codebook is materialised + /// until the app mints its first private class. This test is asserting + /// the allocation table as typed data, not minting class_ids. + #[test] + fn app_prefixes_match_the_allocation_table() { + // § 2 table rows that have a PortSpec impl: + assert_eq!(OpenProjectPort::APP_PREFIX, 0x0001, "OpenProject prefix must be 0x0001"); + assert_eq!(OdooPort::APP_PREFIX, 0x0002, "Odoo prefix must be 0x0002"); + assert_eq!(WoaPort::APP_PREFIX, 0x0003, "WoA prefix must be 0x0003"); + assert_eq!(SmbPort::APP_PREFIX, 0x0004, "SMB-Office prefix must be 0x0004"); + assert_eq!(HealthcarePort::APP_PREFIX, 0x0005, "Healthcare/Medcare prefix must be 0x0005"); + assert_eq!(RedminePort::APP_PREFIX, 0x0007, "Redmine prefix must be 0x0007"); + // The trait default (0x0000 = shared core) is expressed directly + // in the trait definition; ports that do not override it resolve + // to the core codebook namespace, which is the bootstrap/core + // prefix per §2 ("hi = 0x0000 is the bootstrap/core prefix"). + } + /// **The five-way `billable_work_entry` convergence pin.** Ratifies /// the `APP-CODEBOOK-MIGRATION-PLAN.md` W0 + W1 + W2 + W3 worked /// tables: five port-bearing apps all resolve their *own* timesheet From e72463a312a73c362724323186b3a11c64ecf364 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 22 Jun 2026 18:09:52 +0000 Subject: [PATCH 2/2] =?UTF-8?q?feat(ogar-vocab):=20app=20module=20?= =?UTF-8?q?=E2=80=94=20central=20APP=E2=80=96class=20render-classid=20comp?= =?UTF-8?q?osition?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Companion to the typed PortSpec::APP_PREFIX in this branch: adds `ogar_vocab::app`, the central high-u16 composition machinery from APP-CLASS-CODEBOOK-LAYOUT.md §0/§4, so consumers re-export ONE source of the bit math instead of each re-implementing `(prefix << 16) | concept` locally. - render_classid(prefix, concept) -> u32 compose the full classid - render_classid_for::(concept) compose from P::APP_PREFIX - app_of(classid) -> u16 high half (render lens, §4 key) - concept_of(classid) -> u16 low half (shared RBAC+ontology) This resolves the duplication where each consumer (op-canon #56, the redmine-canon twin) defines its own `render_classid` over a local 0x000N literal: they now compose via `render_classid_for::(concept)` reading the typed PortSpec::APP_PREFIX — one source of truth, same discipline as class_ids. Composing the high half is reserving/stamping, not minting a class_id (§2); concept (low-u16) mints stay gated on the 5+3 pass. Core (hi=0x0000) is bit-identical to the bare concept (I-APP1/I-APP5), pinned by test. cargo +1.95 test -p ogar-vocab --lib app:: -> 6 passed; ports:: -> 27 passed; check --workspace --all-targets clean. --- crates/ogar-vocab/src/app.rs | 141 +++++++++++++++++++++++++++++++++++ crates/ogar-vocab/src/lib.rs | 4 + 2 files changed, 145 insertions(+) create mode 100644 crates/ogar-vocab/src/app.rs diff --git a/crates/ogar-vocab/src/app.rs b/crates/ogar-vocab/src/app.rs new file mode 100644 index 0000000..f78fff8 --- /dev/null +++ b/crates/ogar-vocab/src/app.rs @@ -0,0 +1,141 @@ +//! **APP‖class composition** — the high-u16 render-prefix machinery +//! (`docs/APP-CLASS-CODEBOOK-LAYOUT.md` §0, §4). +//! +//! A full 32-bit `classid` is two orthogonal halves: +//! +//! ```text +//! classid : u32 = [ hi u16 : APP / render prefix ] [ lo u16 : concept ] +//! 0xAAAA (per-app ClassView lens) 0xDDCC (shared RBAC+ontology) +//! ``` +//! +//! 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). +//! +//! 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. + +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`. +/// +/// `render_classid(0x0001, 0x0102)` → `0x0001_0102` (OpenProject's +/// `project_work_item`); the Redmine twin `render_classid(0x0007, 0x0102)` → +/// `0x0007_0102` — same concept, different render lens. +#[must_use] +pub const fn render_classid(prefix: u16, concept: u16) -> u32 { + ((prefix as u32) << 16) | (concept 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 +/// 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 shared core]: PortSpec::APP_PREFIX +#[must_use] +pub const fn app_of(classid: u32) -> u16 { + (classid >> 16) 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. +#[must_use] +pub const fn concept_of(classid: u32) -> u16 { + classid as u16 +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::class_ids; + use crate::ports::{OdooPort, OpenProjectPort, RedminePort}; + + #[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); + } + + #[test] + fn render_classid_for_reads_the_port_prefix() { + // OpenProject (0x0001) and Redmine (0x0007) render the SAME concept + // under different prefixes — "two renders, one concept" (§1). + assert_eq!( + render_classid_for::(class_ids::PROJECT_WORK_ITEM), + 0x0001_0102, + ); + assert_eq!( + render_classid_for::(class_ids::PROJECT_WORK_ITEM), + 0x0007_0102, + ); + assert_eq!( + render_classid_for::(class_ids::COMMERCIAL_DOCUMENT), + 0x0002_0202, + ); + } + + #[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!(app_of(cid), 0x0005); + assert_eq!(concept_of(cid), class_ids::PATIENT); + } + + #[test] + fn roundtrip_over_prefixes_and_concepts() { + for prefix in [0x0000u16, 0x0001, 0x0002, 0x0005, 0x0007] { + for concept in [ + class_ids::PROJECT_WORK_ITEM, + class_ids::BILLABLE_WORK_ENTRY, + class_ids::COMMERCIAL_DOCUMENT, + class_ids::PATIENT, + ] { + let cid = render_classid(prefix, concept); + assert_eq!(app_of(cid), prefix); + assert_eq!(concept_of(cid), concept); + } + } + } + + #[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. + 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!(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 + // 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); + assert_ne!(app_of(op), app_of(rm), "render lenses differ"); + assert_eq!(concept_of(op), concept_of(rm), "concept is shared"); + assert_eq!(concept_of(op), class_ids::BILLABLE_WORK_ENTRY); + } +} diff --git a/crates/ogar-vocab/src/lib.rs b/crates/ogar-vocab/src/lib.rs index 5862d45..06e61e6 100644 --- a/crates/ogar-vocab/src/lib.rs +++ b/crates/ogar-vocab/src/lib.rs @@ -1515,6 +1515,10 @@ pub mod class_ids { // so the bridge harness in lance-graph stays generic. pub mod ports; +// APP‖class composition — the high-u16 render-prefix machinery +// (APP-CLASS-CODEBOOK-LAYOUT.md §0/§4). Builds on `PortSpec::APP_PREFIX`. +pub mod app; + /// **Cross-domain bridge concepts** — promoted concepts whose convergence /// is intentionally *across* domains, so they must be exempt from the /// domain gate in [`canonical_concept_in_domain`].