From 69d6bbf23e77f7d660e282492a6c7669ddc1fc62 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 19 Jun 2026 11:09:48 +0000 Subject: [PATCH] =?UTF-8?q?fix(ogar-vocab):=20three=20codex=20P2s=20?= =?UTF-8?q?=E2=80=94=20neutral=20language,=20typed=20boolean=20flag,=20can?= =?UTF-8?q?onical=20target?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All three P2s from codex on the merged OGAR#57 + OGAR#58. 1. (#57 P2) Synthetic canonical classes default to Language::Ruby via Default, but they're not Ruby-harvested. TripleEmitter writes ogar:sourceLanguage = Ruby and routes them through Ruby-specific handling. Set c.language = Language::Unknown in both billable_work_entry() and project_work_item() (proactive same-defect fix; project_work_item wasn't flagged separately but has the identical bug from the same one-line cause). 2. (#57 P2) Attribute::new('billable') leaves type_name=None; DDL adapters default untyped fields to string-like. The defining flag is boolean — type it as such so generated schemas don't store billable-as-text. 3. (#58 P2) BillableWorkEntry's family edge still pointed at the OP curator surface name, but PR #58 promoted Redmine Issue and OP WorkPackage to canonical ProjectWorkItem. The edge now targets ProjectWorkItem so consumers traversing BillableWorkEntry.about see both curators converge. Updates the test fixture accordingly. Test additions pin all three contracts: - assert_eq!(c.language, Language::Unknown) on both canonical classes - assert_eq!(billable.type_name.as_deref(), Some('boolean')) - expected family-edge target list switches 'WorkPackage' -> 'ProjectWorkItem' 21 ogar-vocab + 22 ogar-from-ruff + 6 real-corpus green; clippy clean. --- crates/ogar-vocab/src/lib.rs | 42 +++++++++++++++++++++++++++++++----- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/crates/ogar-vocab/src/lib.rs b/crates/ogar-vocab/src/lib.rs index 965a2a9..495f9e8 100644 --- a/crates/ogar-vocab/src/lib.rs +++ b/crates/ogar-vocab/src/lib.rs @@ -1233,12 +1233,21 @@ pub fn canonical_concept(name: &str) -> String { #[must_use] pub fn billable_work_entry() -> Class { let mut c = Class::new("BillableWorkEntry"); + // Synthetic canonical class — NOT a Ruby-harvested model. Mark the + // language neutral so the triple-emitter writes `ogar:sourceLanguage` + // = `Unknown` and consumers do not route this through Ruby-specific + // handling (codex P2 on OGAR#57). + c.language = Language::Unknown; c.canonical_concept = Some("billable_work_entry".to_string()); // The 12 family edges — internal ontology meaning. Every target is a // canonical concept (PascalCase), never a curator/adapter surface. c.associations = vec![ family_edge("project", "Project"), - family_edge("about", "WorkPackage"), + // `about` targets the canonical project-work-item concept — NOT + // `WorkPackage` (OP curator surface), so Redmine `Issue` and OP + // `WorkPackage` converge here through their shared + // `ProjectWorkItem` projection (codex P2 on OGAR#58). + family_edge("about", "ProjectWorkItem"), family_edge("performed_by", "Worker"), family_edge("duration", "Duration"), family_edge("priced_by", "RatePolicy"), @@ -1250,8 +1259,12 @@ pub fn billable_work_entry() -> Class { family_edge("audit_trail", "AuditTrail"), family_edge("posted_by", "PostingAction"), // ERP boundary ]; - // The defining flag (a scalar, not an edge). - c.attributes = vec![Attribute::new("billable")]; + // The defining flag — typed as boolean so DDL adapters that default + // untyped fields to string-like columns generate the right schema + // shape (codex P2 on OGAR#57). + let mut billable = Attribute::new("billable"); + billable.type_name = Some("boolean".to_string()); + c.attributes = vec![billable]; c } @@ -1290,6 +1303,10 @@ fn family_has_many(role: &str, target_concept: &str) -> Association { #[must_use] pub fn project_work_item() -> Class { let mut c = Class::new("ProjectWorkItem"); + // Synthetic canonical class — neutral language so the triple-emitter + // does not route this through Ruby-specific handling. Same fix as + // [`billable_work_entry`] (codex P2 on OGAR#57). + c.language = Language::Unknown; c.canonical_concept = Some("project_work_item".to_string()); c.associations = vec![ family_edge("project", "Project"), @@ -1384,11 +1401,24 @@ mod tests { let c = billable_work_entry(); assert_eq!(c.name, "BillableWorkEntry"); assert_eq!(c.canonical_concept.as_deref(), Some("billable_work_entry")); - // Exactly the 12 internal family edges, to canonical concepts. + // Synthetic canonical class — neutral language (codex P2 on #57). + assert_eq!(c.language, Language::Unknown); + // Defining `billable` flag is typed as a boolean so DDL adapters + // do not default it to string (codex P2 on #57). + let billable = c + .attributes + .iter() + .find(|a| a.name == "billable") + .expect("billable attribute"); + assert_eq!(billable.type_name.as_deref(), Some("boolean")); + // Exactly the 12 internal family edges, to canonical concepts — + // `about` points at `ProjectWorkItem` (not the OP curator surface + // `WorkPackage`) so Redmine `Issue` and OP `WorkPackage` converge + // here through their shared canonical concept (codex P2 on #58). assert_eq!(c.associations.len(), 12); for target in [ "Project", - "WorkPackage", + "ProjectWorkItem", "Worker", "Duration", "RatePolicy", @@ -1478,6 +1508,8 @@ mod tests { let c = project_work_item(); assert_eq!(c.name, "ProjectWorkItem"); assert_eq!(c.canonical_concept.as_deref(), Some("project_work_item")); + // Synthetic canonical class — neutral language (codex P2 on #57). + assert_eq!(c.language, Language::Unknown); // The 9 family edges named in the smoke spec. for (role, target) in [ ("project", "Project"),