Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 141 additions & 0 deletions crates/ogar-vocab/src/app.rs
Original file line number Diff line number Diff line change
@@ -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::<OpenProjectPort>(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<P: PortSpec>(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::<OpenProjectPort>(class_ids::PROJECT_WORK_ITEM),
0x0001_0102,
);
assert_eq!(
render_classid_for::<RedminePort>(class_ids::PROJECT_WORK_ITEM),
0x0007_0102,
);
assert_eq!(
render_classid_for::<OdooPort>(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::<OpenProjectPort>(class_ids::BILLABLE_WORK_ENTRY);
let rm = render_classid_for::<RedminePort>(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);
}
}
4 changes: 4 additions & 0 deletions crates/ogar-vocab/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`].
Expand Down
56 changes: 56 additions & 0 deletions crates/ogar-vocab/src/ports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
Expand Down
Loading