From 844c84a05952b6c7f4f9020f5c407ca5c5230b11 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 30 Jun 2026 20:53:44 +0000 Subject: [PATCH] feat(op-canon): app_of/concept_of decomposition re-exports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The app module had the forward surface (class_id_of, render_classid, render_classid_of) but the inverse decomposition was only reachable via the full `ogar_vocab::app::{app_of, concept_of}` path (used directly in the low-half test). Re-export both on `op_canon::app` so a render classid round-trips entirely on this crate's surface: concept --render_classid--> 0x0001_DDCC --app_of/concept_of--> (prefix, concept) - app_of(classid) -> u16 re-export of ogar_vocab::app::app_of (high half) - concept_of(classid) -> u16 re-export of ogar_vocab::app::concept_of (low half) Same one-source-of-truth discipline as render_classid / APP_PREFIX (OGAR #97): the canonical layer owns the bit math, this crate re-exports — no local shifts to drift. Two doctests + two unit tests (round-trip inversion of render_classid; equality with OGAR's central helpers + bit-exact reconstruction). cargo +1.95 fmt / clippy -D warnings / test -p op-canon: 21 unit + 7 doctests green. --- crates/op-canon/src/app.rs | 67 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/crates/op-canon/src/app.rs b/crates/op-canon/src/app.rs index 7de3cd6..eed407c 100644 --- a/crates/op-canon/src/app.rs +++ b/crates/op-canon/src/app.rs @@ -45,6 +45,10 @@ //! - [`render_classid`] re-exports `ogar_vocab::app::render_classid_for::` //! — the central `(prefix << 16) | concept` composition; one place owns the //! bit math. +//! - [`app_of`] / [`concept_of`] re-export `ogar_vocab::app::{app_of, concept_of}` +//! — the inverse decomposition (`0x0001_DDCC` → prefix + concept), so a render +//! classid round-trips entirely on this crate's surface without reaching into +//! `ogar_vocab::app`. //! //! Same discipline as [`crate::class_ids`] (which re-exports //! `ogar_vocab::class_ids::*`): the canonical layer mints; this crate @@ -111,6 +115,41 @@ pub fn render_classid_of(surface_name: &str) -> Option { class_id_of(surface_name).map(render_classid) } +/// Decompose a 32-bit render classid into its **OpenProject render prefix** +/// (the high u16) — the inverse of [`render_classid`]'s high half. +/// +/// Re-export of `ogar_vocab::app::app_of` (OGAR PR #97) — the central +/// decomposition, not local bit math; paired with [`concept_of`]. For any +/// concept, `app_of(render_classid(concept)) == APP_PREFIX`. +/// +/// ``` +/// use op_canon::{app, class_ids}; +/// let cid = app::render_classid(class_ids::PROJECT_WORK_ITEM); // 0x0001_0102 +/// assert_eq!(app::app_of(cid), app::APP_PREFIX); // 0x0001 +/// ``` +#[must_use] +pub fn app_of(classid: u32) -> u16 { + ogar_vocab::app::app_of(classid) +} + +/// Decompose a 32-bit render classid into its **shared concept** (the low +/// u16) — the inverse of [`render_classid`]'s low half, recovering the +/// cross-app currency a [`class_id_of`] pull returns. +/// +/// Re-export of `ogar_vocab::app::concept_of` (OGAR PR #97), paired with +/// [`app_of`]. For any concept, `concept_of(render_classid(concept)) == concept`; +/// this is the id Redmine's twin carries under prefix `0x0007`. +/// +/// ``` +/// use op_canon::{app, class_ids}; +/// let cid = app::render_classid(class_ids::PROJECT_WORK_ITEM); // 0x0001_0102 +/// assert_eq!(app::concept_of(cid), class_ids::PROJECT_WORK_ITEM); // 0x0102 +/// ``` +#[must_use] +pub fn concept_of(classid: u32) -> u16 { + ogar_vocab::app::concept_of(classid) +} + #[cfg(test)] mod tests { use super::*; @@ -202,6 +241,34 @@ mod tests { } } + #[test] + fn app_of_and_concept_of_invert_render_classid() { + // The local decomposition surface round-trips render_classid: app_of + // recovers the OpenProject prefix, concept_of the shared concept. + for &concept in &[ + class_ids::PROJECT_WORK_ITEM, + class_ids::PROJECT, + class_ids::BILLABLE_WORK_ENTRY, + class_ids::PROJECT_ROLE, + class_ids::PROJECT_ACTOR, + ] { + let cid = render_classid(concept); + assert_eq!(app_of(cid), APP_PREFIX, "app_of recovers the prefix"); + assert_eq!(concept_of(cid), concept, "concept_of recovers the concept"); + } + } + + #[test] + fn app_of_concept_of_are_the_central_ogar_decomposition() { + // The re-exports ARE OGAR's app_of/concept_of — same one-source-of-truth + // discipline as render_classid (no local bit math to drift). + let cid = render_classid(class_ids::PROJECT_WORK_ITEM); + assert_eq!(app_of(cid), ogar_vocab::app::app_of(cid)); + assert_eq!(concept_of(cid), ogar_vocab::app::concept_of(cid)); + // And the full decomposition reconstructs the classid bit-for-bit. + assert_eq!(((app_of(cid) as u32) << 16) | (concept_of(cid) as u32), cid,); + } + #[test] fn port_pull_agrees_with_the_snapshot() { // The drift guard that lets the PORT be the resolver and the snapshot