From 64a8bd084eadd82ef8f1e63b973eebfbd4c67281 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 20 Jun 2026 01:34:28 +0000 Subject: [PATCH] feat(op-canon): re-export ogar_class_view::OgarClassView (Northstar C2 sibling) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Symmetric move with redmine-rs#6. The ClassView projection bridge from ogar_vocab::Class onto lance_graph_contract::ClassView lives in ogar-class-view (OGAR #77); this port re-exports it so the projection contract is shared with redmine-canon — same one-source-of-truth pattern as class_ids (PRs #49/#50). Downstream op-* crates now reach the run-time projection layer through one import path (op_canon::class_view) without each crate depending on lance-graph-contract directly. Surface: - crates/op-canon/Cargo.toml: adds ogar-class-view + lance-graph-contract git deps (both AdaWorldAPI/* forks). - crates/op-canon/src/class_view.rs: re-exports ClassId, ClassView, ClassProjection, FieldMask, RenderRow + DisplayTemplate, FieldRef, ObjectView + OgarClassView. Module doc explains the pattern, references the Northstar plan, and pairs with redmine-canon's identically-shaped module. Tests (+3 unit, +1 doctest; 12/12 + 2 doctests total): - re_export_loads_the_canonical_class_view - render_rows_skips_off_bits_through_the_re_export - snapshot_ids_resolve_through_the_re_exported_view - doctest pinning the public API Workspace check + test green; clippy + fmt clean. --- Cargo.lock | 56 +++++++++++++++++-- crates/op-canon/Cargo.toml | 8 +++ crates/op-canon/src/class_view.rs | 92 +++++++++++++++++++++++++++++++ crates/op-canon/src/lib.rs | 1 + 4 files changed, 152 insertions(+), 5 deletions(-) create mode 100644 crates/op-canon/src/class_view.rs diff --git a/Cargo.lock b/Cargo.lock index c7a90eb..d57e01e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -707,6 +707,12 @@ dependencies = [ "wasip2", ] +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + [[package]] name = "h2" version = "0.3.27" @@ -1139,6 +1145,16 @@ dependencies = [ name = "lance-graph-contract" version = "0.1.0" +[[package]] +name = "lance-graph-contract" +version = "0.1.0" +source = "git+https://github.com/AdaWorldAPI/lance-graph?branch=main#3336d3be9176c4a6f624e621d9639e9e9faeaa69" +dependencies = [ + "glob", + "serde", + "serde_yaml", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -1423,10 +1439,19 @@ dependencies = [ "url", ] +[[package]] +name = "ogar-class-view" +version = "0.1.0" +source = "git+https://github.com/AdaWorldAPI/OGAR?branch=main#410779ba367c067187d64b8faf381149b396a431" +dependencies = [ + "lance-graph-contract 0.1.0 (git+https://github.com/AdaWorldAPI/lance-graph?branch=main)", + "ogar-vocab", +] + [[package]] name = "ogar-vocab" version = "0.1.0" -source = "git+https://github.com/AdaWorldAPI/OGAR?branch=main#6d2aa579aec02bb29860112172100b560c85bc75" +source = "git+https://github.com/AdaWorldAPI/OGAR?branch=main#410779ba367c067187d64b8faf381149b396a431" [[package]] name = "once_cell" @@ -1503,6 +1528,8 @@ dependencies = [ name = "op-canon" version = "0.1.0" dependencies = [ + "lance-graph-contract 0.1.0 (git+https://github.com/AdaWorldAPI/lance-graph?branch=main)", + "ogar-class-view", "ogar-vocab", "serde", "serde_json", @@ -1520,14 +1547,14 @@ dependencies = [ name = "op-codegen-bucket" version = "0.1.0" dependencies = [ - "lance-graph-contract", + "lance-graph-contract 0.1.0", ] [[package]] name = "op-codegen-pipeline" version = "0.1.0" dependencies = [ - "lance-graph-contract", + "lance-graph-contract 0.1.0", "op-codegen-projection", "ruff_openproject", "ruff_spo_triplet", @@ -1537,7 +1564,7 @@ dependencies = [ name = "op-codegen-projection" version = "0.1.0" dependencies = [ - "lance-graph-contract", + "lance-graph-contract 0.1.0", "op-surreal-ast", ] @@ -1689,7 +1716,7 @@ dependencies = [ name = "op-surreal-ast" version = "0.1.0" dependencies = [ - "lance-graph-contract", + "lance-graph-contract 0.1.0", "serde", "serde_json", ] @@ -2337,6 +2364,19 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "sha1" version = "0.10.6" @@ -3196,6 +3236,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "untrusted" version = "0.9.0" diff --git a/crates/op-canon/Cargo.toml b/crates/op-canon/Cargo.toml index f2a1d9e..d602b19 100644 --- a/crates/op-canon/Cargo.toml +++ b/crates/op-canon/Cargo.toml @@ -11,3 +11,11 @@ serde_json.workspace = true # `class_ids` so this port shares the single source of truth with # `redmine-canon` — the constants cannot drift across ports. ogar-vocab = { git = "https://github.com/AdaWorldAPI/OGAR", branch = "main" } +# ClassView impl over the canonical concepts — re-exported as +# `op_canon::class_view::OgarClassView`. Same one-source-of-truth +# pattern as `class_ids`: the projection bridge lives in OGAR; this port +# re-exports it. Northstar plan §3 C2 (sibling of redmine-rs#6). +ogar-class-view = { git = "https://github.com/AdaWorldAPI/OGAR", branch = "main" } +# Run-time ClassView trait + FieldMask + RenderRow. Re-exported so +# downstream `op-*` crates have one import for the projection contract. +lance-graph-contract = { git = "https://github.com/AdaWorldAPI/lance-graph", branch = "main" } diff --git a/crates/op-canon/src/class_view.rs b/crates/op-canon/src/class_view.rs new file mode 100644 index 0000000..93b942b --- /dev/null +++ b/crates/op-canon/src/class_view.rs @@ -0,0 +1,92 @@ +//! **Canonical ClassView projection** — re-exported from +//! [`ogar_class_view::OgarClassView`], the single source of truth. +//! +//! Pattern mirror of [`crate::class_ids`]: the bridge from +//! [`ogar_vocab::Class`] onto [`lance_graph_contract::ClassView`] lives +//! in OGAR; this port re-exports it so the projection contract is +//! shared with `redmine-canon`, and downstream `op-*` crates have one +//! import for the run-time projection layer. +//! +//! ``` +//! use op_canon::class_view::{OgarClassView, ClassView, FieldMask}; +//! +//! let view = OgarClassView::new(); +//! let class_id = op_canon::class_ids::PROJECT_WORK_ITEM; +//! let mask = FieldMask::EMPTY.with(0).with(1); +//! let rows = view.render_rows(class_id, mask); +//! # let _ = rows; +//! ``` +//! +//! Northstar plan §3, C2. The codebook is minted once in +//! [`AdaWorldAPI/OGAR`](https://github.com/AdaWorldAPI/OGAR); the +//! [`OgarClassView`] adapter that lifts every promoted concept onto the +//! `lance_graph_contract::ClassView` trait lives there too. This port +//! re-exports both, so an OpenProject consumer holding `op_canon` +//! reaches the projection trait + the impl + the constants through one +//! import path. + +pub use lance_graph_contract::class_view::{ + ClassId, ClassProjection, ClassView, FieldMask, RenderRow, +}; +pub use lance_graph_contract::ontology::{DisplayTemplate, FieldRef, ObjectView}; +pub use ogar_class_view::OgarClassView; + +#[cfg(test)] +mod tests { + use super::*; + use crate::Snapshot; + + #[test] + fn re_export_loads_the_canonical_class_view() { + // Sanity: the re-export pulled OgarClassView into scope and it + // initialises cleanly (no panic). + let view = OgarClassView::new(); + for id in [ + crate::class_ids::PROJECT_WORK_ITEM, + crate::class_ids::BILLABLE_WORK_ENTRY, + crate::class_ids::PROJECT_ROLE, + ] { + let n = view.field_count(id); + assert!( + n > 0, + "expected non-empty field set for class id 0x{id:04X}" + ); + } + } + + #[test] + fn render_rows_skips_off_bits_through_the_re_export() { + // The five-line glue from the Northstar plan §2.3 — exercised + // through op-canon's re-export. Symmetric with redmine-canon's + // copy of this test (redmine-rs PR #6). + let view = OgarClassView::new(); + let id = crate::class_ids::PROJECT_WORK_ITEM; + + let empty = view.render_rows(id, FieldMask::EMPTY); + assert!(empty.is_empty()); + + let only_first = FieldMask::EMPTY.with(0); + let rows = view.render_rows(id, only_first); + assert_eq!(rows.len(), 1); + } + + #[test] + fn snapshot_ids_resolve_through_the_re_exported_view() { + // End-to-end pin: every concept the OpenProject snapshot + // promotes resolves to a non-empty field set via the re-exported + // ClassView. Companion of the `snapshot_concepts_match_re_exported_constants` + // class_ids test. + let view = OgarClassView::new(); + let s = Snapshot::load(); + for c in &s.concepts { + let id = c.class_id_u16(); + let n = view.field_count(id); + assert!( + n > 0, + "snapshot concept {} (id 0x{:04X}) has no ClassView fields", + c.canonical_concept, + id, + ); + } + } +} diff --git a/crates/op-canon/src/lib.rs b/crates/op-canon/src/lib.rs index d427bf0..9a6bc38 100644 --- a/crates/op-canon/src/lib.rs +++ b/crates/op-canon/src/lib.rs @@ -57,6 +57,7 @@ #![warn(missing_docs)] pub mod class_ids; +pub mod class_view; use serde::Deserialize;