From bbd421c01932a0f9a1c844291fda8037e87de4d1 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 22 Jun 2026 10:35:23 +0000 Subject: [PATCH] feat(ogar): add WoaBridge / SmbBridge / OdooBridge aliases (OGAR #93/#94) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OGAR #93 (WoaPort + SmbPort) and #94 (OdooPort) landed new PortSpec impls in ogar-vocab::ports. Add the matching bridge type aliases to lance_graph_ogar::bridges so the WoA / SMB / Odoo consumers can migrate onto OGAR the same way medcare did (lance-graph #585 + medcare #168) — sibling of MedcareBridge / OpenProjectBridge / RedmineBridge. - New: woa_bridge.rs (WoaBridge = UnifiedBridge), smb_bridge.rs (SmbBridge = UnifiedBridge), odoo_bridge.rs (OdooBridge = UnifiedBridge). Each re-exports its port + a *_CODEBOOK shim over the port's *_ALIASES const (matching the openproject/redmine precedent — not invented; all three ports expose *_ALIASES) + co-located unit tests. - mod.rs + lib.rs: declare + re-export the three aliases/ports. - ogar-vocab bumped 19a6886 -> 08a9c979 (OGAR main, post #93/#94) so WoaPort/SmbPort/OdooPort resolve. - Purely additive: no existing bridge, the UnifiedBridge harness, or lance-graph-ontology touched functionally. (Distinct from the legacy OGIT-side lance_graph_ontology::bridges::WoaBridge — different crate, no collision.) - Incidental: cargo fmt reflowed 4 pre-existing files in this crate (medcare_bridge/openproject_bridge/unified.rs + bridge_codebook_ convergence.rs) — import-order + chain-wrap only, content-identical. The crate is excluded from the main workspace so main CI never fmt-gated it; this makes it fmt-clean. Verified: cargo build/test --manifest-path crates/lance-graph-ogar green (45 lib + bridge integration suites, 0 failed); fmt --check clean. Verified port facts against the 08a9c97 checkout: WoaPort WorkOrder/woa: class_id("Stundenzettel") == 0x0103 SmbPort SMB/smb: class_id("Kunde") == 0x0204 OdooPort Odoo/odoo: class_id("account.analytic.line")== 0x0103 Generated by [Claude Code](https://claude.com/claude-code) --- .../src/bridges/medcare_bridge.rs | 2 +- crates/lance-graph-ogar/src/bridges/mod.rs | 29 +++++++ .../src/bridges/odoo_bridge.rs | 78 +++++++++++++++++++ .../src/bridges/openproject_bridge.rs | 2 +- .../src/bridges/smb_bridge.rs | 76 ++++++++++++++++++ .../lance-graph-ogar/src/bridges/unified.rs | 18 ++--- .../src/bridges/woa_bridge.rs | 71 +++++++++++++++++ crates/lance-graph-ogar/src/lib.rs | 4 +- .../tests/bridge_codebook_convergence.rs | 7 +- 9 files changed, 272 insertions(+), 15 deletions(-) create mode 100644 crates/lance-graph-ogar/src/bridges/odoo_bridge.rs create mode 100644 crates/lance-graph-ogar/src/bridges/smb_bridge.rs create mode 100644 crates/lance-graph-ogar/src/bridges/woa_bridge.rs diff --git a/crates/lance-graph-ogar/src/bridges/medcare_bridge.rs b/crates/lance-graph-ogar/src/bridges/medcare_bridge.rs index 9b3470fe..7c8c7412 100644 --- a/crates/lance-graph-ogar/src/bridges/medcare_bridge.rs +++ b/crates/lance-graph-ogar/src/bridges/medcare_bridge.rs @@ -28,8 +28,8 @@ use crate::bridges::unified::UnifiedBridge; // `HealthcarePort::NAMESPACE` / `::aliases()` are `PortSpec` associated // items — the trait must be in scope for the resolution to work (codex // P1 on PR #570). Same import in the test module below. -use ogar_vocab::ports::PortSpec; pub use ogar_vocab::ports::HealthcarePort; +use ogar_vocab::ports::PortSpec; /// MedCare `NamespaceBridge` — alias over the generic harness, locked to /// the `Healthcare` namespace via [`HealthcarePort`]. diff --git a/crates/lance-graph-ogar/src/bridges/mod.rs b/crates/lance-graph-ogar/src/bridges/mod.rs index aec092c6..00701f66 100644 --- a/crates/lance-graph-ogar/src/bridges/mod.rs +++ b/crates/lance-graph-ogar/src/bridges/mod.rs @@ -34,17 +34,38 @@ //! — locks to the `Healthcare` namespace. `Patient` / `Diagnosis` / //! `LabValue` / `Medication` / `Treatment` / `Visit` / `VitalSign` //! resolve to the `0x09XX` Health codebook (Northstar T9). +//! - [`WoaBridge`]: `UnifiedBridge` — locks +//! to the `WorkOrder` namespace. WoA's German/English commerce names +//! (`Vorgang` / `Kunde` / `Rechnung` / …) resolve to the `0x02XX` +//! commerce block, and `Stundenzettel` / `TimeEntry` resolve to the +//! SAME `BILLABLE_WORK_ENTRY` (`0x0103`) as the planner ports — the +//! cross-fork convergence pin (OGAR #93). +//! - [`SmbBridge`]: `UnifiedBridge` — locks +//! to the `SMB` namespace. Sister of [`WoaBridge`]: SMB's `Kunde` / +//! `Auftrag` / `Stundenzettel` resolve to the SAME canonical class_ids +//! as the WoA equivalents (OGAR #93). +//! - [`OdooBridge`]: `UnifiedBridge` — locks +//! to the `Odoo` namespace. Odoo model names (`account.move` / +//! `res.partner` / …) resolve to the `0x02XX` commerce block, and the +//! cross-arm `account.analytic.line` resolves to `BILLABLE_WORK_ENTRY` +//! (`0x0103`) — the commerce-arm convergence pin (OGAR #94). pub mod unified; mod medcare_bridge; +mod odoo_bridge; mod openproject_bridge; mod redmine_bridge; +mod smb_bridge; +mod woa_bridge; pub use medcare_bridge::{HealthcarePort, MedcareBridge}; +pub use odoo_bridge::{OdooBridge, OdooPort}; pub use openproject_bridge::{OpenProjectBridge, OpenProjectPort}; pub use redmine_bridge::{RedmineBridge, RedminePort}; +pub use smb_bridge::{SmbBridge, SmbPort}; pub use unified::UnifiedBridge; +pub use woa_bridge::{WoaBridge, WoaPort}; // Compatibility shims for the pre-migration constants. `bridges` // previously re-exported `OPENPROJECT_CODEBOOK` / `REDMINE_CODEBOOK` @@ -56,3 +77,11 @@ pub use unified::UnifiedBridge; pub use openproject_bridge::OPENPROJECT_CODEBOOK; #[allow(deprecated)] pub use redmine_bridge::REDMINE_CODEBOOK; +// The OGAR-port bridges added for #93/#94 expose the same `*_ALIASES` +// constants, so they get the matching `*_CODEBOOK` shim for symmetry. +#[allow(deprecated)] +pub use odoo_bridge::ODOO_CODEBOOK; +#[allow(deprecated)] +pub use smb_bridge::SMB_CODEBOOK; +#[allow(deprecated)] +pub use woa_bridge::WOA_CODEBOOK; diff --git a/crates/lance-graph-ogar/src/bridges/odoo_bridge.rs b/crates/lance-graph-ogar/src/bridges/odoo_bridge.rs new file mode 100644 index 00000000..2c0832d4 --- /dev/null +++ b/crates/lance-graph-ogar/src/bridges/odoo_bridge.rs @@ -0,0 +1,78 @@ +//! Odoo (odoo-rs) tenant bridge — a thin type alias over +//! [`crate::bridges::unified::UnifiedBridge`] parameterised by +//! [`ogar_vocab::ports::OdooPort`]. +//! +//! The differences between bridges (namespace, bridge_id, public-name +//! → class_id alias table) all come from the OGAR class schema. The +//! `OdooPort` carries the `Odoo` namespace + `odoo` bridge_id + the +//! Odoo-model alias table (`account.move` → COMMERCIAL_DOCUMENT, +//! `res.partner` → BILLING_PARTY, …) plus the cross-arm bridge +//! `account.analytic.line` → `BILLABLE_WORK_ENTRY` (`0x0103`), the SAME +//! canonical id the OpenProject / Redmine `TimeEntry` ports resolve to — +//! the commerce-arm convergence pin (OGAR #94, Northstar §7 T10). + +use crate::bridges::unified::UnifiedBridge; +// `OdooPort::NAMESPACE` / `::aliases()` are `PortSpec` associated items — +// the trait must be in scope for the resolution to work (codex P1 on +// PR #570). Same import in the test module below. +pub use ogar_vocab::ports::OdooPort; +use ogar_vocab::ports::PortSpec; + +/// Odoo `NamespaceBridge` — alias over the generic harness, locked to the +/// `Odoo` namespace via [`OdooPort`]. +pub type OdooBridge = UnifiedBridge; + +/// Canonical namespace name for Odoo. Mirrors `OdooPort::NAMESPACE` so +/// consumers that import the constant from this module keep building. +pub const NAMESPACE: &str = OdooPort::NAMESPACE; + +/// Compatibility shim — re-exports `ogar_vocab::ports::ODOO_ALIASES` +/// under a `*_CODEBOOK` name for symmetry with the other per-port +/// bridges. New code should reach for `ogar_vocab::ports::ODOO_ALIASES` +/// (or `OdooPort::aliases()`) directly — going through the canonical +/// layer keeps lance-graph free of port-specific data. +#[deprecated( + note = "use `ogar_vocab::ports::ODOO_ALIASES` (or `OdooPort::aliases()`) — the constant lives in OGAR" +)] +pub const ODOO_CODEBOOK: &[(&str, u16)] = ogar_vocab::ports::ODOO_ALIASES; + +#[cfg(test)] +mod tests { + use super::*; + use ogar_vocab::class_ids; + // PortSpec needed in scope for `OdooPort::aliases()` / `::class_id()` + // (the methods are trait items — codex P1 on PR #570). + use ogar_vocab::ports::PortSpec; + + #[test] + fn namespace_and_bridge_id_mirror_the_port() { + assert_eq!(NAMESPACE, "Odoo"); + assert_eq!(OdooPort::NAMESPACE, "Odoo"); + assert_eq!(OdooPort::BRIDGE_ID, "odoo"); + } + + #[test] + fn port_resolves_account_move_to_commercial_document() { + assert_eq!( + OdooPort::class_id("account.move"), + Some(class_ids::COMMERCIAL_DOCUMENT) + ); + assert_eq!(OdooPort::class_id("account.move"), Some(0x0202)); + } + + #[test] + fn port_analytic_line_converges_on_billable_work_entry() { + // The cross-arm bridge (OGAR #94): Odoo's timesheet/cost line + // resolves to the SAME canonical id as the planner ports. + assert_eq!( + OdooPort::class_id("account.analytic.line"), + Some(class_ids::BILLABLE_WORK_ENTRY) + ); + assert_eq!(OdooPort::class_id("account.analytic.line"), Some(0x0103)); + } + + #[test] + fn port_returns_none_for_non_codebook_name() { + assert_eq!(OdooPort::class_id("not.a.model"), None); + } +} diff --git a/crates/lance-graph-ogar/src/bridges/openproject_bridge.rs b/crates/lance-graph-ogar/src/bridges/openproject_bridge.rs index 5cfdb166..cf40d6cc 100644 --- a/crates/lance-graph-ogar/src/bridges/openproject_bridge.rs +++ b/crates/lance-graph-ogar/src/bridges/openproject_bridge.rs @@ -18,8 +18,8 @@ use crate::bridges::unified::UnifiedBridge; // `OpenProjectPort::NAMESPACE` / `::aliases()` are `PortSpec` // associated items — the trait must be in scope for the resolution to // work (codex P1 on PR #570). Same import in the test module below. -use ogar_vocab::ports::PortSpec; pub use ogar_vocab::ports::OpenProjectPort; +use ogar_vocab::ports::PortSpec; /// OpenProject `NamespaceBridge` — alias over the generic harness. pub type OpenProjectBridge = UnifiedBridge; diff --git a/crates/lance-graph-ogar/src/bridges/smb_bridge.rs b/crates/lance-graph-ogar/src/bridges/smb_bridge.rs new file mode 100644 index 00000000..0a3143b3 --- /dev/null +++ b/crates/lance-graph-ogar/src/bridges/smb_bridge.rs @@ -0,0 +1,76 @@ +//! SMB (small-and-medium-business German office ERP) tenant bridge — a +//! thin type alias over [`crate::bridges::unified::UnifiedBridge`] +//! parameterised by [`ogar_vocab::ports::SmbPort`]. +//! +//! The differences between bridges (namespace, bridge_id, public-name +//! → class_id alias table) all come from the OGAR class schema. The +//! `SmbPort` carries the `SMB` namespace + `smb` bridge_id + the +//! German/English alias table (Kunde ≡ Customer, Auftrag ≡ Order, +//! Rechnung ≡ Invoice, Stundenzettel ≡ TimeEntry, …). Sister of +//! [`super::woa_bridge::WoaBridge`]: SMB's `Stundenzettel` resolves to +//! the SAME canonical `BILLABLE_WORK_ENTRY` (`0x0103`) as the WoA and +//! planner ports — cross-fork convergence (OGAR #93). + +use crate::bridges::unified::UnifiedBridge; +// `SmbPort::NAMESPACE` / `::aliases()` are `PortSpec` associated items — +// the trait must be in scope for the resolution to work (codex P1 on +// PR #570). Same import in the test module below. +use ogar_vocab::ports::PortSpec; +pub use ogar_vocab::ports::SmbPort; + +/// SMB `NamespaceBridge` — alias over the generic harness, locked to the +/// `SMB` namespace via [`SmbPort`]. +pub type SmbBridge = UnifiedBridge; + +/// Canonical namespace name for SMB. Mirrors `SmbPort::NAMESPACE` so +/// consumers that import the constant from this module keep building. +pub const NAMESPACE: &str = SmbPort::NAMESPACE; + +/// Compatibility shim — re-exports `ogar_vocab::ports::SMB_ALIASES` under +/// a `*_CODEBOOK` name for symmetry with the other per-port bridges. New +/// code should reach for `ogar_vocab::ports::SMB_ALIASES` (or +/// `SmbPort::aliases()`) directly — going through the canonical layer +/// keeps lance-graph free of port-specific data. +#[deprecated( + note = "use `ogar_vocab::ports::SMB_ALIASES` (or `SmbPort::aliases()`) — the constant lives in OGAR" +)] +pub const SMB_CODEBOOK: &[(&str, u16)] = ogar_vocab::ports::SMB_ALIASES; + +#[cfg(test)] +mod tests { + use super::*; + use ogar_vocab::class_ids; + // PortSpec needed in scope for `SmbPort::aliases()` / `::class_id()` + // (the methods are trait items — codex P1 on PR #570). + use ogar_vocab::ports::PortSpec; + + #[test] + fn namespace_and_bridge_id_mirror_the_port() { + assert_eq!(NAMESPACE, "SMB"); + assert_eq!(SmbPort::NAMESPACE, "SMB"); + assert_eq!(SmbPort::BRIDGE_ID, "smb"); + } + + #[test] + fn port_resolves_kunde_to_billing_party() { + assert_eq!(SmbPort::class_id("Kunde"), Some(class_ids::BILLING_PARTY)); + assert_eq!(SmbPort::class_id("Kunde"), Some(0x0204)); + // English synonym collapses to the same id. + assert_eq!(SmbPort::class_id("Customer"), Some(0x0204)); + } + + #[test] + fn port_resolves_stundenzettel_to_billable_work_entry() { + // Same convergence pin as WoA (OGAR #93). + assert_eq!( + SmbPort::class_id("Stundenzettel"), + Some(class_ids::BILLABLE_WORK_ENTRY) + ); + assert_eq!(SmbPort::class_id("Stundenzettel"), Some(0x0103)); + } + + #[test] + fn port_returns_none_for_non_codebook_name() { + assert_eq!(SmbPort::class_id("NotAConcept"), None); + } +} diff --git a/crates/lance-graph-ogar/src/bridges/unified.rs b/crates/lance-graph-ogar/src/bridges/unified.rs index ba485a28..5f3288e0 100644 --- a/crates/lance-graph-ogar/src/bridges/unified.rs +++ b/crates/lance-graph-ogar/src/bridges/unified.rs @@ -89,8 +89,8 @@ impl UnifiedBridge

{ /// on the convergence contract. fn synthesize_codebook_entity(&self, class_id: u16) -> EntityRef { let ctx_id = NamespaceRegistry::seed_context_id(P::NAMESPACE).unwrap_or(0); - let schema_ptr = SchemaPtr::new(self.g_lock, class_id, SchemaKind::Entity) - .with_context_id(ctx_id); + let schema_ptr = + SchemaPtr::new(self.g_lock, class_id, SchemaKind::Entity).with_context_id(ctx_id); EntityRef { schema_ptr } } } @@ -144,13 +144,13 @@ impl NamespaceBridge for UnifiedBridge

{ } } } - let ptr = self - .registry() - .resolve_uri(uri.as_str()) - .ok_or_else(|| BridgeError::NotInScope { - bridge_id: self.bridge_id_static(), - public_name: uri.as_str().to_string(), - })?; + let ptr = + self.registry() + .resolve_uri(uri.as_str()) + .ok_or_else(|| BridgeError::NotInScope { + bridge_id: self.bridge_id_static(), + public_name: uri.as_str().to_string(), + })?; if ptr.namespace_id() != self.g_lock() { return Err(BridgeError::CrossNamespaceLeak { bridge_id: self.bridge_id_static(), diff --git a/crates/lance-graph-ogar/src/bridges/woa_bridge.rs b/crates/lance-graph-ogar/src/bridges/woa_bridge.rs new file mode 100644 index 00000000..366e93c2 --- /dev/null +++ b/crates/lance-graph-ogar/src/bridges/woa_bridge.rs @@ -0,0 +1,71 @@ +//! WoA (Work Order Application) tenant bridge — a thin type alias over +//! [`crate::bridges::unified::UnifiedBridge`] parameterised by +//! [`ogar_vocab::ports::WoaPort`]. +//! +//! The differences between bridges (namespace, bridge_id, public-name +//! → class_id alias table) all come from the OGAR class schema. The +//! `WoaPort` carries the `WorkOrder` namespace + `woa` bridge_id + the +//! German/English alias table (Vorgang ≡ WorkOrder, Stundenzettel ≡ +//! TimesheetActivity ≡ TimeEntry, Kunde ≡ Customer, …), so this bridge +//! is one line. `Stundenzettel` / `TimeEntry` resolve to the SAME +//! canonical `BILLABLE_WORK_ENTRY` (`0x0103`) as the OpenProject / +//! Redmine planner ports — the cross-fork convergence pin (OGAR #93). + +use crate::bridges::unified::UnifiedBridge; +// `WoaPort::NAMESPACE` / `::aliases()` are `PortSpec` associated items — +// the trait must be in scope for the resolution to work (codex P1 on +// PR #570). Same import in the test module below. +use ogar_vocab::ports::PortSpec; +pub use ogar_vocab::ports::WoaPort; + +/// WoA `NamespaceBridge` — alias over the generic harness, locked to the +/// `WorkOrder` namespace via [`WoaPort`]. +pub type WoaBridge = UnifiedBridge; + +/// Canonical namespace name for WoA. Mirrors `WoaPort::NAMESPACE` so +/// consumers that import the constant from this module keep building. +pub const NAMESPACE: &str = WoaPort::NAMESPACE; + +/// Compatibility shim — re-exports `ogar_vocab::ports::WOA_ALIASES` under +/// a `*_CODEBOOK` name for symmetry with the other per-port bridges. New +/// code should reach for `ogar_vocab::ports::WOA_ALIASES` (or +/// `WoaPort::aliases()`) directly — going through the canonical layer +/// keeps lance-graph free of port-specific data. +#[deprecated( + note = "use `ogar_vocab::ports::WOA_ALIASES` (or `WoaPort::aliases()`) — the constant lives in OGAR" +)] +pub const WOA_CODEBOOK: &[(&str, u16)] = ogar_vocab::ports::WOA_ALIASES; + +#[cfg(test)] +mod tests { + use super::*; + use ogar_vocab::class_ids; + // PortSpec needed in scope for `WoaPort::aliases()` / `::class_id()` + // (the methods are trait items — codex P1 on PR #570). + use ogar_vocab::ports::PortSpec; + + #[test] + fn namespace_and_bridge_id_mirror_the_port() { + assert_eq!(NAMESPACE, "WorkOrder"); + assert_eq!(WoaPort::NAMESPACE, "WorkOrder"); + assert_eq!(WoaPort::BRIDGE_ID, "woa"); + } + + #[test] + fn port_resolves_stundenzettel_to_billable_work_entry() { + // The cross-fork convergence pin (OGAR #93): WoA's billable-hours + // concept resolves to the SAME canonical id as the planner ports. + assert_eq!( + WoaPort::class_id("Stundenzettel"), + Some(class_ids::BILLABLE_WORK_ENTRY) + ); + assert_eq!(WoaPort::class_id("Stundenzettel"), Some(0x0103)); + // English synonym collapses to the same id. + assert_eq!(WoaPort::class_id("TimeEntry"), Some(0x0103)); + } + + #[test] + fn port_returns_none_for_non_codebook_name() { + assert_eq!(WoaPort::class_id("NotAConcept"), None); + } +} diff --git a/crates/lance-graph-ogar/src/lib.rs b/crates/lance-graph-ogar/src/lib.rs index 280ba178..ab472d59 100644 --- a/crates/lance-graph-ogar/src/lib.rs +++ b/crates/lance-graph-ogar/src/lib.rs @@ -83,8 +83,8 @@ pub use ogar_vocab::Class; pub mod bridges; pub use bridges::{ - HealthcarePort, MedcareBridge, OpenProjectBridge, OpenProjectPort, RedmineBridge, RedminePort, - UnifiedBridge, + HealthcarePort, MedcareBridge, OdooBridge, OdooPort, OpenProjectBridge, OpenProjectPort, + RedmineBridge, RedminePort, SmbBridge, SmbPort, UnifiedBridge, WoaBridge, WoaPort, }; /// Codebook parity-guard — the drift fuse between OGAR's authoritative codebook diff --git a/crates/lance-graph-ogar/tests/bridge_codebook_convergence.rs b/crates/lance-graph-ogar/tests/bridge_codebook_convergence.rs index aaa78c46..7fd0deed 100644 --- a/crates/lance-graph-ogar/tests/bridge_codebook_convergence.rs +++ b/crates/lance-graph-ogar/tests/bridge_codebook_convergence.rs @@ -19,8 +19,8 @@ //! `lance_graph_ontology::bridges::codebook::*` constants. use lance_graph_ogar::bridges::{OpenProjectBridge, RedmineBridge}; -use ogar_vocab::class_ids as codebook; use lance_graph_ontology::{NamespaceBridge, OntologyRegistry}; +use ogar_vocab::class_ids as codebook; use std::fs; use std::sync::Arc; @@ -129,7 +129,10 @@ fn status_and_type_alias_pairs_converge_through_each_ports_naming() { assert_eq!( op.entity("Status").unwrap().schema_ptr.entity_type_id(), - rm.entity("IssueStatus").unwrap().schema_ptr.entity_type_id(), + rm.entity("IssueStatus") + .unwrap() + .schema_ptr + .entity_type_id(), ); assert_eq!( op.entity("Status").unwrap().schema_ptr.entity_type_id(),