From 9ec0a6552fe4ad0d187ec8df1da240adfbc8e87f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 23 Jun 2026 00:09:55 +0000 Subject: [PATCH] refactor(redmine-canon): re-export APP_PREFIX + render_classid from OGAR (one source of truth) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Redmine-side twin of openproject-nexgen-rs#57: adopts the OGAR PR #97 surface in `redmine_canon::app`, replacing the two local mirrors with re-exports from `ogar-vocab`: - APP_PREFIX local `pub const = 0x0007` → `pub const = RedminePort::APP_PREFIX` (#97 typed const) - render_classid local `(APP_PREFIX as u32) << 16 | concept` → `ogar_vocab::app::render_classid_for::` (#97 central) - low/high decomp local `(cid >> 16) as u16` / `cid as u16` in the test → `ogar_vocab::app::app_of(cid)` / `concept_of(cid)` (#97 central) Keeps the two `-canon` ports byte-symmetric on the new OGAR surface: op-canon re-exports OpenProjectPort::APP_PREFIX (0x0001), redmine-canon re-exports RedminePort::APP_PREFIX (0x0007). The local prefix literal and local bit math are gone; the consumer composes its 0x0007 render via the typed upstream surface, matching the one-source-of-truth discipline class_ids already follows. Two new tests: - app_prefix_re_exports_the_typed_ogar_constant - render_classid_agrees_with_the_central_ogar_composition #99 trap-spellbook pre-flight: cleared all 5 questions (no producer reading, no lifecycle vocabulary, target IR is canonical OGAR, arrow producer→OGAR→ consumer, no behavior to recover). Zero diagnostic signatures present. Lockfile: cargo update -p ogar-vocab → OGAR main (post-#97). cargo +1.95 test -p redmine-canon: 22 unit + 5 doctests pass; fmt + clippy (-D warnings) clean. --- Cargo.lock | 10 ++--- crates/redmine-canon/src/app.rs | 74 +++++++++++++++++++++++++++++---- 2 files changed, 71 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 66217c5..3e0d429 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2121,7 +2121,7 @@ dependencies = [ [[package]] name = "lance-graph-contract" version = "0.1.0" -source = "git+https://github.com/AdaWorldAPI/lance-graph?branch=main#46ce40b985903aab48cb5de15c2cb7b17a6d7da3" +source = "git+https://github.com/AdaWorldAPI/lance-graph?branch=main#ebeacde2e626683361639a3d888bb84f53f8cbae" dependencies = [ "glob", "serde", @@ -2518,7 +2518,7 @@ dependencies = [ [[package]] name = "ogar-adapter-surrealql" version = "0.1.0" -source = "git+https://github.com/AdaWorldAPI/OGAR?branch=main#eb0a59a5759a62b608ffcc4ad7fb1769795bb5c0" +source = "git+https://github.com/AdaWorldAPI/OGAR?branch=main#1c95bf235c8e6e932f1f28095171a9c42e75ee9c" dependencies = [ "ogar-vocab", ] @@ -2526,7 +2526,7 @@ dependencies = [ [[package]] name = "ogar-class-view" version = "0.1.0" -source = "git+https://github.com/AdaWorldAPI/OGAR?branch=main#eb0a59a5759a62b608ffcc4ad7fb1769795bb5c0" +source = "git+https://github.com/AdaWorldAPI/OGAR?branch=main#1c95bf235c8e6e932f1f28095171a9c42e75ee9c" dependencies = [ "lance-graph-contract", "ogar-vocab", @@ -2535,7 +2535,7 @@ dependencies = [ [[package]] name = "ogar-render-askama" version = "0.1.0" -source = "git+https://github.com/AdaWorldAPI/OGAR?branch=main#eb0a59a5759a62b608ffcc4ad7fb1769795bb5c0" +source = "git+https://github.com/AdaWorldAPI/OGAR?branch=main#1c95bf235c8e6e932f1f28095171a9c42e75ee9c" dependencies = [ "askama", "ogar-vocab", @@ -2544,7 +2544,7 @@ dependencies = [ [[package]] name = "ogar-vocab" version = "0.1.0" -source = "git+https://github.com/AdaWorldAPI/OGAR?branch=main#eb0a59a5759a62b608ffcc4ad7fb1769795bb5c0" +source = "git+https://github.com/AdaWorldAPI/OGAR?branch=main#1c95bf235c8e6e932f1f28095171a9c42e75ee9c" [[package]] name = "once_cell" diff --git a/crates/redmine-canon/src/app.rs b/crates/redmine-canon/src/app.rs index 6a551ff..0bfcef3 100644 --- a/crates/redmine-canon/src/app.rs +++ b/crates/redmine-canon/src/app.rs @@ -34,13 +34,33 @@ //! prefix** (`APP-CLASS-CODEBOOK-LAYOUT.md` §1, §3d), not minting an OGAR //! codebook class — that mint is gated on OGAR's 5+3 pass. The pull (low //! half) is the part that is canonical and available today. +//! +//! # One source of truth — the OGAR surface +//! +//! Both halves of the composition come from `ogar-vocab` (OGAR PR #97): +//! +//! - [`APP_PREFIX`] is a `pub const` re-export of +//! [`ogar_vocab::ports::RedminePort::APP_PREFIX`] — the typed §2 +//! allocation-table value, not a local literal. +//! - [`render_classid`] re-exports `ogar_vocab::app::render_classid_for::` +//! — the central `(prefix << 16) | concept` composition; one place owns the +//! bit math. +//! +//! Same discipline as [`crate::class_ids`] (which re-exports +//! `ogar_vocab::class_ids::*`): the canonical layer mints; this crate +//! re-exports. Drift between local and OGAR is now structurally impossible. +//! (Symmetric to `op-canon`'s `app` module — openproject-nexgen-rs#57.) use ogar_vocab::ports::{PortSpec, RedminePort}; /// Redmine's reserved **APP / render prefix** — the high u16 of a full /// `classid` (`APP-CLASS-CODEBOOK-LAYOUT.md` §2 allocation table). Pairs with /// OpenProject's `0x0001`: same low-half concept, different render lens. -pub const APP_PREFIX: u16 = 0x0007; +/// +/// `pub const` re-export of [`ogar_vocab::ports::RedminePort::APP_PREFIX`] +/// (OGAR PR #97). Promoted from a local mirror to the typed upstream constant — +/// one source of truth. +pub const APP_PREFIX: u16 = RedminePort::APP_PREFIX; /// Pull the canonical class-id for a Redmine surface name **via the OGAR /// port** — the #589 "pull OGAR via class" path (no bridge, no registry). @@ -58,15 +78,18 @@ pub fn class_id_of(surface_name: &str) -> Option { } /// Compose the full 32-bit **render** classid for a shared `concept` under -/// Redmine's prefix: `(APP_PREFIX << 16) | concept` → `0x0007_DDCC`. +/// Redmine's prefix: `0x0007_DDCC`. +/// +/// Re-export of `ogar_vocab::app::render_classid_for::(concept)` +/// (OGAR PR #97) — the central composition, not local bit math. /// /// ``` /// use redmine_canon::{app, class_ids}; /// assert_eq!(app::render_classid(class_ids::PROJECT_WORK_ITEM), 0x0007_0102); /// ``` #[must_use] -pub const fn render_classid(concept: u16) -> u32 { - ((APP_PREFIX as u32) << 16) | (concept as u32) +pub fn render_classid(concept: u16) -> u32 { + ogar_vocab::app::render_classid_for::(concept) } /// Pull + compose in one step: a Redmine surface name → its full render @@ -109,8 +132,15 @@ mod tests { } #[test] - fn render_classid_composes_redmine_prefix() { + fn app_prefix_re_exports_the_typed_ogar_constant() { + // One source of truth: the local constant IS the upstream typed + // PortSpec::APP_PREFIX, not a parallel literal. (#97) + assert_eq!(APP_PREFIX, RedminePort::APP_PREFIX); assert_eq!(APP_PREFIX, 0x0007); + } + + #[test] + fn render_classid_composes_redmine_prefix() { // Full render classid = 0x0007_DDCC (W0 worked table). assert_eq!(render_classid(class_ids::PROJECT_WORK_ITEM), 0x0007_0102); assert_eq!(render_classid(class_ids::BILLABLE_WORK_ENTRY), 0x0007_0103); @@ -120,11 +150,29 @@ mod tests { assert_eq!(render_classid_of("TimeEntry"), Some(0x0007_0103)); } + #[test] + fn render_classid_agrees_with_the_central_ogar_composition() { + // The local function is exactly OGAR's `render_classid_for::

`; no + // local bit math. If this assertion ever fails, the local impl drifted + // from the canonical upstream composition. + for &concept in &[ + class_ids::PROJECT_WORK_ITEM, + class_ids::PROJECT, + class_ids::BILLABLE_WORK_ENTRY, + class_ids::PROJECT_ROLE, + ] { + assert_eq!( + render_classid(concept), + ogar_vocab::app::render_classid_for::(concept), + ); + } + } + #[test] fn render_classid_keeps_concept_in_the_low_half() { // The low half is the shared concept (== the port pull); the high // half is Redmine's render lens. OpenProject's twin carries the - // SAME low half under prefix 0x0001 — "two renders, one concept". + // SAME low half under prefix 0x0001 (pinned in OGAR's port tests). for &concept in &[ class_ids::PROJECT_WORK_ITEM, class_ids::PROJECT, @@ -132,8 +180,18 @@ mod tests { class_ids::PROJECT_ROLE, ] { let cid = render_classid(concept); - assert_eq!((cid >> 16) as u16, APP_PREFIX, "high half = Redmine lens"); - assert_eq!(cid as u16, concept, "low half = shared concept"); + // Decompose via OGAR's central helpers (one source of truth on the + // bit math, not local shifts). + assert_eq!( + ogar_vocab::app::app_of(cid), + APP_PREFIX, + "high half = Redmine lens", + ); + assert_eq!( + ogar_vocab::app::concept_of(cid), + concept, + "low half = shared concept", + ); } }