diff --git a/Cargo.lock b/Cargo.lock index 82b6e04..faed152 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,22 +2,69 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "itoa" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[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 = "memchr" version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" +[[package]] +name = "ogar-class-view" +version = "0.1.0" +source = "git+https://github.com/AdaWorldAPI/OGAR?branch=main#410779ba367c067187d64b8faf381149b396a431" +dependencies = [ + "lance-graph-contract", + "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 = "proc-macro2" @@ -41,11 +88,19 @@ dependencies = [ name = "redmine-canon" version = "0.1.0" dependencies = [ + "lance-graph-contract", + "ogar-class-view", "ogar-vocab", "serde", "serde_json", ] +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + [[package]] name = "serde" version = "1.0.228" @@ -89,6 +144,19 @@ dependencies = [ "zmij", ] +[[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 = "syn" version = "2.0.118" @@ -106,6 +174,12 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "zmij" version = "1.0.21" diff --git a/crates/redmine-canon/Cargo.toml b/crates/redmine-canon/Cargo.toml index 71335b9..6f802ce 100644 --- a/crates/redmine-canon/Cargo.toml +++ b/crates/redmine-canon/Cargo.toml @@ -13,3 +13,11 @@ serde_json = { workspace = true } # `class_ids` so this port shares the single source of truth with # `op-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 +# `redmine_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. +ogar-class-view = { git = "https://github.com/AdaWorldAPI/OGAR", branch = "main" } +# Run-time ClassView trait + FieldMask + RenderRow. Re-exported so +# downstream `rm-*` crates have one import for the projection contract. +lance-graph-contract = { git = "https://github.com/AdaWorldAPI/lance-graph", branch = "main" } diff --git a/crates/redmine-canon/src/class_view.rs b/crates/redmine-canon/src/class_view.rs new file mode 100644 index 0000000..f04d29b --- /dev/null +++ b/crates/redmine-canon/src/class_view.rs @@ -0,0 +1,98 @@ +//! **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 `op-canon`, and downstream `rm-*` crates have one import +//! for the run-time projection layer. +//! +//! ``` +//! use redmine_canon::class_view::{OgarClassView, ClassView, FieldMask}; +//! +//! let view = OgarClassView::new(); +//! let class_id = redmine_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 a Redmine consumer holding `redmine_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(); + // The view knows about the same project-mgmt concepts the + // vendored snapshot pins. Walk a few: + 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 redmine-canon's re-export. SoA row knows its class_id; + // registry knows its ClassView; render_rows projects the + // populated subset. + let view = OgarClassView::new(); + let id = crate::class_ids::PROJECT_WORK_ITEM; + + // Empty mask → no rows. + let empty = view.render_rows(id, FieldMask::EMPTY); + assert!(empty.is_empty()); + + // Bit 0 set → exactly one row. + 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 redmine snapshot promotes + // resolves to a non-empty field set via the re-exported + // ClassView. This is the projection-layer companion of the + // `snapshot_concepts_match_re_exported_constants` test in + // class_ids. + 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/redmine-canon/src/lib.rs b/crates/redmine-canon/src/lib.rs index 8af496e..ef6cfba 100644 --- a/crates/redmine-canon/src/lib.rs +++ b/crates/redmine-canon/src/lib.rs @@ -46,6 +46,7 @@ #![warn(missing_docs)] pub mod class_ids; +pub mod class_view; pub mod convergence; use serde::Deserialize;