From e096f77a648e9a464805bbae59edf1f3c9efcca4 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 7 Jul 2026 16:25:49 +0000 Subject: [PATCH] capability_registry: generic hot-plug resolution (classid -> vocab + actions) The migration target for EVERY consumer (operator ruling): the consumer declares which classids it hot-plugs + which capabilities its executor covers; resolve_hotplug verifies and returns BOTH the vocab rows and the action surface for exactly those ids. Domain tables register in domain_tables() -- append-only; the thinking-styles table adds one entry and is instantly hot-pluggable. Drift arms: UnknownClassid / NoCapabilitiesFor ("the table was forgotten") / UnexpectedConsumer / Uncovered / Undeclared -- each one named bang, classid is the join key on both sides. The per-registration verify_registration path stays for now; consumers migrate to resolve_hotplug (tesseract-rs first). Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_016b33swuXE23hKtqxsHu9p1 --- crates/ogar-adapter-clickhouse-ddl/src/lib.rs | 46 +- crates/ogar-adapter-postgres-ddl/src/lib.rs | 15 +- .../ogar-adapter-postgres-ddl/src/parity.rs | 17 +- crates/ogar-adapter-surrealql/src/lib.rs | 80 ++- crates/ogar-adapter-ttl/src/lib.rs | 25 +- crates/ogar-adapter/src/lib.rs | 25 +- .../tests/cognition_step_mapping.rs | 5 +- crates/ogar-emitter/src/lib.rs | 578 ++++++++++++++---- crates/ogar-from-elixir/src/lib.rs | 20 +- crates/ogar-from-rails/src/lib.rs | 3 +- crates/ogar-from-ruff/src/lib.rs | 137 +++-- crates/ogar-from-ruff/src/mint.rs | 25 +- crates/ogar-from-ruff/src/sqlalchemy.rs | 30 +- .../ogar-from-ruff/tests/woa_parity_probe.rs | 30 +- crates/ogar-knowable-from/src/lib.rs | 80 +-- crates/ogar-ontology/src/lib.rs | 18 +- crates/ogar-proposal/src/lib.rs | 126 ++-- .../ogar-render-askama/examples/render_osm.rs | 16 +- .../examples/render_python.rs | 28 +- .../src/artifact_kinds/cells.rs | 5 +- .../src/artifact_kinds/html_detail_view.rs | 2 +- .../src/artifact_kinds/html_form.rs | 6 +- .../src/artifact_kinds/html_list_view.rs | 4 +- .../src/artifact_kinds/mod.rs | 8 +- .../src/artifact_kinds/rust_struct.rs | 15 +- .../src/artifact_kinds/surrealql_table.rs | 37 +- crates/ogar-render-askama/src/form_view.rs | 80 ++- crates/ogar-render-askama/src/lib.rs | 191 ++++-- crates/ogar-render-askama/src/list_view.rs | 40 +- crates/ogar-render-askama/src/rust_class.rs | 28 +- crates/ogar-vocab/src/capability_registry.rs | 177 ++++++ 31 files changed, 1442 insertions(+), 455 deletions(-) diff --git a/crates/ogar-adapter-clickhouse-ddl/src/lib.rs b/crates/ogar-adapter-clickhouse-ddl/src/lib.rs index 1e62a7f..f67b4b3 100644 --- a/crates/ogar-adapter-clickhouse-ddl/src/lib.rs +++ b/crates/ogar-adapter-clickhouse-ddl/src/lib.rs @@ -150,7 +150,8 @@ pub fn parse_clickhouse_ddl(_input: &str) -> Result, ParseError> { { Err(ParseError::Unimplemented( "clickhouse-parser feature not enabled; rebuild with \ - --features clickhouse-parser to enable parsing".into(), + --features clickhouse-parser to enable parsing" + .into(), )) } } @@ -206,9 +207,7 @@ fn ogar_type_to_clickhouse(t: Option<&str>) -> String { "Int64".to_string() } Some("float") | Some("double") | Some("real") => "Float64".to_string(), - Some("decimal") | Some("monetary") | Some("numeric") => { - "Decimal(18, 4)".to_string() - } + Some("decimal") | Some("monetary") | Some("numeric") => "Decimal(18, 4)".to_string(), Some("bool") | Some("boolean") => "Bool".to_string(), Some("datetime") | Some("timestamp") | Some("date") => "DateTime".to_string(), Some("uuid") => "UUID".to_string(), @@ -225,7 +224,10 @@ fn ogar_type_to_clickhouse(t: Option<&str>) -> String { /// names like `sale.order` need quoting). fn quote_ch_ident(name: &str) -> String { let bare = !name.is_empty() - && name.chars().next().map_or(false, |c| c.is_ascii_alphabetic() || c == '_') + && name + .chars() + .next() + .map_or(false, |c| c.is_ascii_alphabetic() || c == '_') && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_'); if bare { name.to_string() @@ -306,8 +308,7 @@ mod walk { // nested Nullable (`Nullable(Nullable(X))` is rejected by the // server), so single-strip is sufficient. if let Some(inner) = strip_wrapper(&rendered, "nullable") { - let (canonical, _already_optional) = - clickhouse_type_to_ogar(&dummy_data_type(&inner)); + let (canonical, _already_optional) = clickhouse_type_to_ogar(&dummy_data_type(&inner)); return (canonical, true); } @@ -403,7 +404,10 @@ mod tests { let c = Class::new("users"); let ddl = emit_clickhouse_ddl(&[c]); assert!(ddl.contains("CREATE TABLE users (\n"), "got: {ddl}"); - assert!(ddl.contains("ENGINE = MergeTree() ORDER BY tuple();"), "got: {ddl}"); + assert!( + ddl.contains("ENGINE = MergeTree() ORDER BY tuple();"), + "got: {ddl}" + ); } #[test] @@ -424,10 +428,7 @@ mod tests { deleted.options.required = Some(false); c.attributes.push(deleted); let ddl = emit_clickhouse_ddl(&[c]); - assert!( - ddl.contains("deleted_at Nullable(DateTime)"), - "got: {ddl}" - ); + assert!(ddl.contains("deleted_at Nullable(DateTime)"), "got: {ddl}"); } #[test] @@ -483,13 +484,17 @@ mod tests { assert_eq!(classes[0].attributes[0].name, "id"); assert_eq!(classes[0].attributes[0].type_name.as_deref(), Some("int")); assert_eq!(classes[0].attributes[1].name, "name"); - assert_eq!(classes[0].attributes[1].type_name.as_deref(), Some("string")); + assert_eq!( + classes[0].attributes[1].type_name.as_deref(), + Some("string") + ); } #[cfg(feature = "clickhouse-parser")] #[test] fn parse_nullable_lifts_to_required_false() { - let ddl = "CREATE TABLE t (deleted_at Nullable(DateTime)) ENGINE = MergeTree ORDER BY tuple();"; + let ddl = + "CREATE TABLE t (deleted_at Nullable(DateTime)) ENGINE = MergeTree ORDER BY tuple();"; let classes = parse_clickhouse_ddl(ddl).expect("parse OK"); let attr = &classes[0].attributes[0]; // IR-canonical: bare type + required=Some(false). Same shape @@ -507,12 +512,16 @@ mod tests { i FixedString(16) \ ) ENGINE = MergeTree ORDER BY tuple();"; let classes = parse_clickhouse_ddl(ddl).expect("parse OK"); - let names: Vec<&str> = classes[0].attributes.iter() + let names: Vec<&str> = classes[0] + .attributes + .iter() .map(|a| a.type_name.as_deref().unwrap_or("")) .collect(); assert_eq!( names, - vec!["string", "int", "int", "float", "decimal", "datetime", "bool", "uuid", "string"], + vec![ + "string", "int", "int", "float", "decimal", "datetime", "bool", "uuid", "string" + ], ); } @@ -579,7 +588,10 @@ mod tests { assert_eq!(recovered[0].name, "sale.order"); assert_eq!(recovered[0].attributes.len(), 1); assert_eq!(recovered[0].attributes[0].name, "amount_total"); - assert_eq!(recovered[0].attributes[0].type_name.as_deref(), Some("decimal")); + assert_eq!( + recovered[0].attributes[0].type_name.as_deref(), + Some("decimal") + ); } #[cfg(feature = "clickhouse-parser")] diff --git a/crates/ogar-adapter-postgres-ddl/src/lib.rs b/crates/ogar-adapter-postgres-ddl/src/lib.rs index 15fdc63..2977c9d 100644 --- a/crates/ogar-adapter-postgres-ddl/src/lib.rs +++ b/crates/ogar-adapter-postgres-ddl/src/lib.rs @@ -231,7 +231,10 @@ mod tests { // Nullable: no explicit `required=true` -> no NOT NULL suffix, and // no incidental match against the NOT NULL variant of the same // column name. - assert!(ddl.contains("note TEXT") && !ddl.contains("note TEXT NOT NULL"), "got: {ddl}"); + assert!( + ddl.contains("note TEXT") && !ddl.contains("note TEXT NOT NULL"), + "got: {ddl}" + ); } #[test] @@ -246,7 +249,10 @@ mod tests { let ddl = emit_facet_table_ddl("facet"); assert!(ddl.contains("classid INTEGER NOT NULL"), "got: {ddl}"); for i in 0..12 { - assert!(ddl.contains(&format!("p{i} SMALLINT")), "missing p{i}: {ddl}"); + assert!( + ddl.contains(&format!("p{i} SMALLINT")), + "missing p{i}: {ddl}" + ); assert!( ddl.contains(&format!("CREATE INDEX facet_p{i}_idx ON facet (p{i});")), "missing index for p{i}: {ddl}" @@ -290,6 +296,9 @@ mod tests { assert!(ddl.contains("beschreibung TEXT NOT NULL"), "got: {ddl}"); assert!(ddl.contains("created_at TIMESTAMP"), "got: {ddl}"); assert!(!ddl.contains("created_at TIMESTAMP NOT NULL"), "got: {ddl}"); - assert!(!ddl.contains("timesheet_id"), "FK must be deduped upstream: {ddl}"); + assert!( + !ddl.contains("timesheet_id"), + "FK must be deduped upstream: {ddl}" + ); } } diff --git a/crates/ogar-adapter-postgres-ddl/src/parity.rs b/crates/ogar-adapter-postgres-ddl/src/parity.rs index ff87b2a..a1929b1 100644 --- a/crates/ogar-adapter-postgres-ddl/src/parity.rs +++ b/crates/ogar-adapter-postgres-ddl/src/parity.rs @@ -136,7 +136,11 @@ pub fn check_parity(sink_in: &Class, legacy: &Class) -> ParityReport { } for sink_in_attr in &sink_in.attributes { - if !legacy.attributes.iter().any(|a| a.name == sink_in_attr.name) { + if !legacy + .attributes + .iter() + .any(|a| a.name == sink_in_attr.name) + { drifts.push(ParityDrift::MissingInLegacy { field: sink_in_attr.name.clone(), }); @@ -165,7 +169,8 @@ mod tests { #[test] fn identical_shapes_have_clean_parity() { let mut c = Class::new("timesheet_activities"); - c.attributes.push(attr("beschreibung", "string", Some(true))); + c.attributes + .push(attr("beschreibung", "string", Some(true))); c.attributes.push(attr("created_at", "datetime", None)); let report = check_parity(&c, &c.clone()); @@ -181,7 +186,9 @@ mod tests { let report = check_parity(&sink_in, &legacy); assert_eq!( report.drifts, - vec![ParityDrift::MissingInSinkIn { field: "foo".into() }] + vec![ParityDrift::MissingInSinkIn { + field: "foo".into() + }] ); } @@ -241,7 +248,9 @@ mod tests { sink_in .attributes .push(attr("beschreibung", "string", Some(true))); - sink_in.attributes.push(attr("created_at", "datetime", None)); + sink_in + .attributes + .push(attr("created_at", "datetime", None)); let mut legacy = Class::new("TimesheetActivity"); legacy diff --git a/crates/ogar-adapter-surrealql/src/lib.rs b/crates/ogar-adapter-surrealql/src/lib.rs index e51510e..725f1e7 100644 --- a/crates/ogar-adapter-surrealql/src/lib.rs +++ b/crates/ogar-adapter-surrealql/src/lib.rs @@ -213,9 +213,7 @@ pub fn parse_surrealql_ddl(_input: &str) -> Result, ParseError> { mod walk { use ogar_vocab::{Association, AssociationKind, Attribute, Class}; use std::collections::HashMap; - use surrealdb_ast::{ - Ast, Expr, NodeId, NodeListId, PrimeType, Query, TopLevelExpr, Type, - }; + use surrealdb_ast::{Ast, Expr, NodeId, NodeListId, PrimeType, Query, TopLevelExpr, Type}; pub(super) fn walk_query(ast: &Ast, query: Query) -> Vec { // Pass 1: visit DefineTable to register class order. @@ -233,7 +231,10 @@ mod walk { } } - order.into_iter().filter_map(|n| by_name.remove(&n)).collect() + order + .into_iter() + .filter_map(|n| by_name.remove(&n)) + .collect() } fn visit_define( @@ -271,7 +272,10 @@ mod walk { }); match lift_field_type(ast, df.ty) { - FieldShape::Primitive { type_name, optional } => { + FieldShape::Primitive { + type_name, + optional, + } => { let mut attr = Attribute::new(&field); attr.type_name = Some(type_name); if optional { @@ -282,8 +286,7 @@ mod walk { class.attributes.push(attr); } FieldShape::Record { target, optional } => { - let mut assoc = - Association::new(AssociationKind::BelongsTo, &field); + let mut assoc = Association::new(AssociationKind::BelongsTo, &field); assoc.class_name = Some(target); if optional { assoc.optional = Some(true); @@ -360,10 +363,12 @@ mod walk { None => return FieldShape::Untyped, }; let prime_list_id = match &ast[ty_id] { - Type::Any(_) => return FieldShape::Primitive { - type_name: "any".into(), - optional: false, - }, + Type::Any(_) => { + return FieldShape::Primitive { + type_name: "any".into(), + optional: false, + }; + } Type::Prime(list_id) => *list_id, }; @@ -371,8 +376,9 @@ mod walk { // PrimeType::None (see surrealdb/parser/src/parse/kind.rs at // the T![OPTION] arm). So a NodeList starting with None means // optional; the rest are the effective type(s). - let primes: Vec<&PrimeType> = - iter_node_list(ast, prime_list_id).map(|p| &ast[p]).collect(); + let primes: Vec<&PrimeType> = iter_node_list(ast, prime_list_id) + .map(|p| &ast[p]) + .collect(); if primes.is_empty() { return FieldShape::Untyped; } @@ -477,10 +483,11 @@ impl std::fmt::Display for ParseError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { ParseError::Parse(msg) => write!(f, "surrealql parse error: {msg}"), - ParseError::UnmappableField { table, field, reason } => write!( - f, - "unmappable field {field} on table {table}: {reason}" - ), + ParseError::UnmappableField { + table, + field, + reason, + } => write!(f, "unmappable field {field} on table {table}: {reason}"), ParseError::Unimplemented(msg) => write!(f, "not yet implemented: {msg}"), } } @@ -622,7 +629,10 @@ fn emit_field_enum(table: &str, enum_decl: &EnumDecl, out: &mut String) { "DEFINE FIELD {column_ident} ON {table} TYPE string ASSERT $value IN [{variants}];\n" )); } - ogar_vocab::EnumSource::Add { items, parent_selection } => { + ogar_vocab::EnumSource::Add { + items, + parent_selection, + } => { // Inherited enum: emit the added variants only; downstream // consumers reconcile against the parent via `parent_selection`. let variants = items @@ -727,7 +737,10 @@ mod tests { fn emit_minimal_class_produces_define_table() { let c = Class::new("widget"); let ddl = emit_surrealql_ddl(&[c]); - assert!(ddl.contains("DEFINE TABLE widget SCHEMAFULL;"), "got: {ddl}"); + assert!( + ddl.contains("DEFINE TABLE widget SCHEMAFULL;"), + "got: {ddl}" + ); } #[test] @@ -738,7 +751,10 @@ mod tests { c.attributes.push(email); let ddl = emit_surrealql_ddl(&[c]); assert!(ddl.contains("DEFINE TABLE account SCHEMAFULL;")); - assert!(ddl.contains("DEFINE FIELD email ON account TYPE string;"), "got: {ddl}"); + assert!( + ddl.contains("DEFINE FIELD email ON account TYPE string;"), + "got: {ddl}" + ); } #[test] @@ -806,7 +822,10 @@ mod tests { .push(Association::new(AssociationKind::HasOne, "lead")); let ddl = emit_surrealql_ddl(&[c]); assert!(!ddl.contains("DEFINE FIELD lead"), "got: {ddl}"); - assert!(ddl.contains("(no DEFINE FIELD"), "expected non-owning comment, got: {ddl}"); + assert!( + ddl.contains("(no DEFINE FIELD"), + "expected non-owning comment, got: {ddl}" + ); } #[test] @@ -820,7 +839,9 @@ mod tests { c.enums.push(status); let ddl = emit_surrealql_ddl(&[c]); assert!( - ddl.contains("DEFINE FIELD status ON ticket TYPE string ASSERT $value IN ['open', 'closed'];"), + ddl.contains( + "DEFINE FIELD status ON ticket TYPE string ASSERT $value IN ['open', 'closed'];" + ), "got: {ddl}" ); } @@ -846,9 +867,8 @@ mod tests { fn emit_class_with_computed_enum_emits_lambda_marker() { let mut c = Class::new("address"); let mut country_enum = EnumDecl::new("country"); - country_enum.source = ogar_vocab::EnumSource::Computed( - "lambda self: self.env[\'res.country\']...".into(), - ); + country_enum.source = + ogar_vocab::EnumSource::Computed("lambda self: self.env[\'res.country\']...".into()); c.enums.push(country_enum); let ddl = emit_surrealql_ddl(&[c]); assert!( @@ -964,13 +984,19 @@ mod tests { assert_eq!(classes.len(), 1); let c = &classes[0]; assert_eq!(c.name, "work_package"); - assert!(c.attributes.is_empty(), "record should NOT become an attribute"); + assert!( + c.attributes.is_empty(), + "record should NOT become an attribute" + ); assert_eq!(c.associations.len(), 1); let a = &c.associations[0]; assert_eq!(a.name, "owner"); assert!(matches!(a.kind, AssociationKind::BelongsTo)); assert_eq!(a.class_name.as_deref(), Some("user")); - assert!(a.optional.unwrap_or(false) == false, "non-optional record"); + assert!( + a.optional.unwrap_or(false) == false, + "non-optional record" + ); } #[cfg(feature = "surrealdb-parser")] diff --git a/crates/ogar-adapter-ttl/src/lib.rs b/crates/ogar-adapter-ttl/src/lib.rs index 5d17def..d023019 100644 --- a/crates/ogar-adapter-ttl/src/lib.rs +++ b/crates/ogar-adapter-ttl/src/lib.rs @@ -146,7 +146,10 @@ pub fn emit_ttl(classes: &[Class], prefix: &str) -> String { let mut by_subject: std::collections::BTreeMap> = std::collections::BTreeMap::new(); for t in triples { - by_subject.entry(t.subject).or_default().push((t.predicate, t.object)); + by_subject + .entry(t.subject) + .or_default() + .push((t.predicate, t.object)); } for (subject, predicates) in by_subject { write_subject_block(&mut out, &subject, &predicates); @@ -197,7 +200,8 @@ pub fn parse_ttl(_input: &str) -> Result, TtlParseError> { { Err(TtlParseError::Unimplemented( "ttl-parser feature not enabled; rebuild with --features ttl-parser \ - to enable Turtle parsing via oxttl".into(), + to enable Turtle parsing via oxttl" + .into(), )) } } @@ -210,12 +214,8 @@ const OGAR_BASE: &str = "https://ogar.surrealdb-graph.io"; fn prefix_block(prefix: &str) -> String { let mut out = String::new(); - out.push_str(&format!( - "@prefix ogar: <{OGAR_BASE}/vocab/ogar#> .\n" - )); - out.push_str(&format!( - "@prefix {prefix}: <{OGAR_BASE}/{prefix}/> .\n" - )); + out.push_str(&format!("@prefix ogar: <{OGAR_BASE}/vocab/ogar#> .\n")); + out.push_str(&format!("@prefix {prefix}: <{OGAR_BASE}/{prefix}/> .\n")); out.push_str("@prefix rdf: .\n"); out.push_str("@prefix rdfs: .\n"); out.push_str("@prefix owl: .\n"); @@ -333,8 +333,7 @@ mod parse { let mut by_subject: HashMap> = HashMap::new(); for result in parser.for_slice(input.as_bytes()) { - let quad = result - .map_err(|e| TtlParseError::Parse(format!("{e}")))?; + let quad = result.map_err(|e| TtlParseError::Parse(format!("{e}")))?; let s = subject_iri(&quad.subject); let p = quad.predicate.as_str().to_string(); let o = term_to_string(&quad.object); @@ -481,9 +480,7 @@ mod parse { }; let name = preds .iter() - .find_map(|(p, o)| { - (ogar_local(p) == Some("relationName")).then(|| o.clone()) - }) + .find_map(|(p, o)| (ogar_local(p) == Some("relationName")).then(|| o.clone())) .unwrap_or_else(|| local_name(subject)); let mut assoc = Association::new(kind, name); for (p, o) in preds { @@ -521,9 +518,9 @@ mod parse { #[cfg(test)] mod tests { use super::*; - use ogar_vocab::{Attribute, Class}; #[cfg(feature = "ttl-parser")] use ogar_vocab::{Association, AssociationKind}; + use ogar_vocab::{Attribute, Class}; // ── Emit-side tests (always available) ───────────────────────────── diff --git a/crates/ogar-adapter/src/lib.rs b/crates/ogar-adapter/src/lib.rs index da219fd..a1ea6d9 100644 --- a/crates/ogar-adapter/src/lib.rs +++ b/crates/ogar-adapter/src/lib.rs @@ -67,7 +67,10 @@ impl BTreeMapAdapter { /// Build a new empty adapter with the given name. #[must_use] pub fn new(name: &'static str) -> Self { - Self { name, leaves: BTreeMap::new() } + Self { + name, + leaves: BTreeMap::new(), + } } /// Insert a (canonical → target) leaf. @@ -135,7 +138,10 @@ impl OdooAdapter { inner.insert("ogar/decorator/api.constrains", "odoo:@api.constrains"); inner.insert("ogar/decorator/api.onchange", "odoo:@api.onchange"); inner.insert("ogar/decorator/api.model", "odoo:@api.model"); - inner.insert("ogar/decorator/api.model_create_multi", "odoo:@api.model_create_multi"); + inner.insert( + "ogar/decorator/api.model_create_multi", + "odoo:@api.model_create_multi", + ); Self { inner } } @@ -188,7 +194,10 @@ mod tests { a.insert("ogit-erp/sale.order", "odoo:sale.order"); a.insert("ogit-erp/sale.order.line", "odoo:sale.order.line"); a.insert("ogit-erp/account.move", "odoo:account.move"); - let under_sale: Vec<_> = a.iter_prefix("ogit-erp/sale").map(|(k, _)| k.as_str()).collect(); + let under_sale: Vec<_> = a + .iter_prefix("ogit-erp/sale") + .map(|(k, _)| k.as_str()) + .collect(); assert_eq!(under_sale.len(), 2); assert!(under_sale.iter().all(|k| k.starts_with("ogit-erp/sale"))); } @@ -198,7 +207,10 @@ mod tests { let o = OdooAdapter::new(); assert!(o.leaf_count() >= 10); assert_eq!(o.map("ogit-erp/move").as_deref(), Some("odoo:transport")); - assert_eq!(o.map("ogit-erp/sale.order").as_deref(), Some("odoo:sale.order")); + assert_eq!( + o.map("ogit-erp/sale.order").as_deref(), + Some("odoo:sale.order") + ); assert_eq!(o.map("ogit-erp/missing"), None); } @@ -208,7 +220,10 @@ mod tests { let before = o.leaf_count(); o.register("ogit-erp/custom.thing", "odoo:custom.thing"); assert_eq!(o.leaf_count(), before + 1); - assert_eq!(o.map("ogit-erp/custom.thing").as_deref(), Some("odoo:custom.thing")); + assert_eq!( + o.map("ogit-erp/custom.thing").as_deref(), + Some("odoo:custom.thing") + ); } #[test] diff --git a/crates/ogar-class-view/tests/cognition_step_mapping.rs b/crates/ogar-class-view/tests/cognition_step_mapping.rs index 6ff8ce0..f625a56 100644 --- a/crates/ogar-class-view/tests/cognition_step_mapping.rs +++ b/crates/ogar-class-view/tests/cognition_step_mapping.rs @@ -81,9 +81,8 @@ fn step_domain_has_no_erp_variant_yet() { | StepDomain::Ndarray | StepDomain::Smb | StepDomain::Medcare - | StepDomain::Kanban => {} - // no ERP arm today — when one is added, this match fails to - // compile, forcing an intentional update (the seam-closing PR). + | StepDomain::Kanban => {} // no ERP arm today — when one is added, this match fails to + // compile, forcing an intentional update (the seam-closing PR). } } let _ = assert_known; diff --git a/crates/ogar-emitter/src/lib.rs b/crates/ogar-emitter/src/lib.rs index f6e3e7f..a5c5041 100644 --- a/crates/ogar-emitter/src/lib.rs +++ b/crates/ogar-emitter/src/lib.rs @@ -63,7 +63,11 @@ pub struct Triple { impl Triple { /// Build a triple from string-like inputs. #[must_use] - pub fn new(subject: impl Into, predicate: impl Into, object: impl Into) -> Self { + pub fn new( + subject: impl Into, + predicate: impl Into, + object: impl Into, + ) -> Self { Self { subject: subject.into(), predicate: predicate.into(), @@ -165,7 +169,11 @@ impl OgarEmitter for TripleEmitter { } if let Some(ref default_scope) = class.default_scope { - triples.push(Triple::new(&subject, "ogar:defaultScope", default_scope.clone())); + triples.push(Triple::new( + &subject, + "ogar:defaultScope", + default_scope.clone(), + )); } for col in &class.ignored_columns { @@ -173,7 +181,11 @@ impl OgarEmitter for TripleEmitter { } for name in &class.scope_predeclarations { - triples.push(Triple::new(&subject, "ogar:scopePredeclaration", name.clone())); + triples.push(Triple::new( + &subject, + "ogar:scopePredeclaration", + name.clone(), + )); } if class.inheritance_column_disabled { @@ -197,13 +209,21 @@ impl OgarEmitter for TripleEmitter { triples.push(Triple::new(&subject, "ogar:recName", rn.clone())); } if let Some(b) = class.check_company_auto { - triples.push(Triple::new(&subject, "ogar:checkCompanyAuto", bool_to_str(b))); + triples.push(Triple::new( + &subject, + "ogar:checkCompanyAuto", + bool_to_str(b), + )); } if let Some(b) = class.log_access { triples.push(Triple::new(&subject, "ogar:logAccess", bool_to_str(b))); } if let Some(b) = class.auto_create_table { - triples.push(Triple::new(&subject, "ogar:autoCreateTable", bool_to_str(b))); + triples.push(Triple::new( + &subject, + "ogar:autoCreateTable", + bool_to_str(b), + )); } if class.abstract_model { triples.push(Triple::new(&subject, "ogar:abstractModel", "true")); @@ -222,7 +242,12 @@ impl OgarEmitter for TripleEmitter { } for (i, assoc) in class.associations.iter().enumerate() { - triples.extend(Self::emit_association_indexed(assoc, &class.name, prefix, i)); + triples.extend(Self::emit_association_indexed( + assoc, + &class.name, + prefix, + i, + )); } for (i, enum_decl) in class.enums.iter().enumerate() { @@ -230,7 +255,12 @@ impl OgarEmitter for TripleEmitter { } for (i, sa) in class.store_accessors.iter().enumerate() { - triples.extend(Self::emit_store_accessor_indexed(sa, &class.name, prefix, i)); + triples.extend(Self::emit_store_accessor_indexed( + sa, + &class.name, + prefix, + i, + )); } for attr in &class.attributes { @@ -250,7 +280,12 @@ impl OgarEmitter for TripleEmitter { } for (i, cf) in class.computed_fields.iter().enumerate() { - triples.extend(Self::emit_computed_field_indexed(cf, &class.name, prefix, i)); + triples.extend(Self::emit_computed_field_indexed( + cf, + &class.name, + prefix, + i, + )); } for (i, m) in class.methods.iter().enumerate() { @@ -314,10 +349,18 @@ impl OgarEmitter for TripleEmitter { triples.push(Triple::new(&attr_id, "ogar:groupAccess", g.clone())); } if let Some(b) = opts.company_dependent { - triples.push(Triple::new(&attr_id, "ogar:companyDependent", bool_to_str(b))); + triples.push(Triple::new( + &attr_id, + "ogar:companyDependent", + bool_to_str(b), + )); } if let Some(b) = opts.copy_on_duplicate { - triples.push(Triple::new(&attr_id, "ogar:copyOnDuplicate", bool_to_str(b))); + triples.push(Triple::new( + &attr_id, + "ogar:copyOnDuplicate", + bool_to_str(b), + )); } if let Some(ref h) = opts.help_text { triples.push(Triple::new(&attr_id, "ogar:helpText", h.clone())); @@ -382,7 +425,11 @@ impl TripleEmitter { ]; if let Some(ref cn) = assoc.class_name { - triples.push(Triple::new(&assoc_id, "ogar:targetClass", class_identity(prefix, cn))); + triples.push(Triple::new( + &assoc_id, + "ogar:targetClass", + class_identity(prefix, cn), + )); } if let Some(ref fk) = assoc.foreign_key { triples.push(Triple::new(&assoc_id, "ogar:foreignKey", fk.clone())); @@ -474,10 +521,17 @@ impl TripleEmitter { } } ogar_vocab::EnumSource::Computed(body) => { - triples.push(Triple::new(&enum_id, "ogar:enumSourceKind", "ogar:Computed")); + triples.push(Triple::new( + &enum_id, + "ogar:enumSourceKind", + "ogar:Computed", + )); triples.push(Triple::new(&enum_id, "ogar:enumComputedBody", body.clone())); } - ogar_vocab::EnumSource::Add { items, parent_selection } => { + ogar_vocab::EnumSource::Add { + items, + parent_selection, + } => { triples.push(Triple::new(&enum_id, "ogar:enumSourceKind", "ogar:Add")); triples.push(Triple::new( &enum_id, @@ -556,7 +610,10 @@ impl TripleEmitter { index: usize, ) -> Vec { let owner_id = class_identity(prefix, owner_class); - let cb_id = format!("{}/{}::callback::{}::{}", prefix, owner_class, index, cb.event); + let cb_id = format!( + "{}/{}::callback::{}::{}", + prefix, owner_class, index, cb.event + ); let mut triples = vec![ Triple::new(&owner_id, "ogar:hasCallback", cb_id.clone()), Triple::new(&cb_id, "rdf:type", "ogar:Callback"), @@ -578,7 +635,10 @@ impl TripleEmitter { index: usize, ) -> Vec { let owner_id = class_identity(prefix, owner_class); - let v_id = format!("{}/{}::validation::{}::{}", prefix, owner_class, index, v.target); + let v_id = format!( + "{}/{}::validation::{}::{}", + prefix, owner_class, index, v.target + ); vec![ Triple::new(&owner_id, "ogar:hasValidation", v_id.clone()), Triple::new(&v_id, "rdf:type", "ogar:Validation"), @@ -656,8 +716,16 @@ impl TripleEmitter { Triple::new(&id, "rdf:type", "ogar:ActionDef"), Triple::new(&id, "ogar:actionPredicate", def.predicate.clone()), Triple::new(&id, "ogar:actionObjectClass", def.object_class.clone()), - Triple::new(&id, "ogar:defaultSubject", subject_to_ogar(def.default_subject)), - Triple::new(&id, "ogar:defaultTemporal", temporal_to_ogar(def.default_temporal)), + Triple::new( + &id, + "ogar:defaultSubject", + subject_to_ogar(def.default_subject), + ), + Triple::new( + &id, + "ogar:defaultTemporal", + temporal_to_ogar(def.default_temporal), + ), Triple::new(&id, "ogar:defaultModal", modal_to_ogar(def.default_modal)), ]; // Action-scoped predicates — NOT the MethodDecl ones. Reusing @@ -742,7 +810,10 @@ impl TripleEmitter { fn kausal_triples(action_subject: &str, k: &ogar_vocab::KausalSpec) -> Vec { use ogar_vocab::KausalSpec; match k { - KausalSpec::StateGuard { guard_field, guard_values } => { + KausalSpec::StateGuard { + guard_field, + guard_values, + } => { let mut v = vec![ Triple::new(action_subject, "ogar:kausalKind", "ogar:StateGuard"), Triple::new(action_subject, "ogar:guardField", guard_field.clone()), @@ -762,21 +833,45 @@ fn kausal_triples(action_subject: &str, k: &ogar_vocab::KausalSpec) -> Vec { - let mut v = vec![Triple::new(action_subject, "ogar:kausalKind", "ogar:Depends")]; + let mut v = vec![Triple::new( + action_subject, + "ogar:kausalKind", + "ogar:Depends", + )]; for p in paths { - v.push(Triple::new(action_subject, "ogar:kausalDependsPath", p.clone())); + v.push(Triple::new( + action_subject, + "ogar:kausalDependsPath", + p.clone(), + )); } v } KausalSpec::ContextDepends { keys } => { - let mut v = vec![Triple::new(action_subject, "ogar:kausalKind", "ogar:ContextDepends")]; + let mut v = vec![Triple::new( + action_subject, + "ogar:kausalKind", + "ogar:ContextDepends", + )]; for k in keys { - v.push(Triple::new(action_subject, "ogar:kausalDependsContext", k.clone())); + v.push(Triple::new( + action_subject, + "ogar:kausalDependsContext", + k.clone(), + )); } v } - KausalSpec::External => vec![Triple::new(action_subject, "ogar:kausalKind", "ogar:External")], - _ => vec![Triple::new(action_subject, "ogar:kausalKind", "ogar:Unknown")], + KausalSpec::External => vec![Triple::new( + action_subject, + "ogar:kausalKind", + "ogar:External", + )], + _ => vec![Triple::new( + action_subject, + "ogar:kausalKind", + "ogar:Unknown", + )], } } @@ -921,9 +1016,11 @@ mod tests { && t.predicate == "ogar:parentClass" && t.object == "ogit-op/ApplicationRecord" })); - assert!(triples.iter().any(|t| { - t.predicate == "ogar:sourceLanguage" && t.object == "ogar:Ruby" - })); + assert!( + triples + .iter() + .any(|t| { t.predicate == "ogar:sourceLanguage" && t.object == "ogar:Ruby" }) + ); } #[test] @@ -986,21 +1083,31 @@ mod tests { // Variant name + value emitted as separate triples on a // synthetic per-variant subject — round-trippable even when // names or values contain `=` or other separators. - assert!(triples.iter().any(|t| { - t.predicate == "ogar:variantName" && t.object == "open" - })); - assert!(triples.iter().any(|t| { - t.predicate == "ogar:variantValue" && t.object == "0" - })); - assert!(triples.iter().any(|t| { - t.predicate == "ogar:variantName" && t.object == "closed" - })); - assert!(triples.iter().any(|t| { - t.predicate == "ogar:variantValue" && t.object == "1" - })); - assert!(triples.iter().any(|t| { - t.predicate == "ogar:scopesDisabled" && t.object == "true" - })); + assert!( + triples + .iter() + .any(|t| { t.predicate == "ogar:variantName" && t.object == "open" }) + ); + assert!( + triples + .iter() + .any(|t| { t.predicate == "ogar:variantValue" && t.object == "0" }) + ); + assert!( + triples + .iter() + .any(|t| { t.predicate == "ogar:variantName" && t.object == "closed" }) + ); + assert!( + triples + .iter() + .any(|t| { t.predicate == "ogar:variantValue" && t.object == "1" }) + ); + assert!( + triples + .iter() + .any(|t| { t.predicate == "ogar:scopesDisabled" && t.object == "true" }) + ); // Enum subject lives under `::enum::` namespace (distinct // from Attribute under `field_identity` and StoreAccessor // under `::store::`). @@ -1016,15 +1123,23 @@ mod tests { // Rails allows two `after_create do ... end` blocks on the same // class — they MUST emit distinct subjects, not silently overwrite. let mut class = sample_work_package(); - class.callbacks.push(Callback::block("after_create", "step_one")); - class.callbacks.push(Callback::block("after_create", "step_two")); + class + .callbacks + .push(Callback::block("after_create", "step_one")); + class + .callbacks + .push(Callback::block("after_create", "step_two")); let triples = TripleEmitter::emit_class(&class, "ogit-op"); let cb_subjects: Vec<_> = triples .iter() .filter(|t| t.predicate == "rdf:type" && t.object == "ogar:Callback") .map(|t| t.subject.as_str()) .collect(); - assert_eq!(cb_subjects.len(), 2, "two distinct callback subjects expected"); + assert_eq!( + cb_subjects.len(), + 2, + "two distinct callback subjects expected" + ); assert_ne!(cb_subjects[0], cb_subjects[1]); } @@ -1065,9 +1180,11 @@ mod tests { assoc.after_remove = Some("module_disabled".into()); class.associations.push(assoc); let triples = TripleEmitter::emit_class(&class, "ogit-op"); - assert!(triples.iter().any(|t| { - t.predicate == "ogar:afterRemove" && t.object == "module_disabled" - })); + assert!( + triples + .iter() + .any(|t| { t.predicate == "ogar:afterRemove" && t.object == "module_disabled" }) + ); } #[test] @@ -1101,12 +1218,36 @@ mod tests { class.transient = false; class.check_company_auto = Some(true); let triples = TripleEmitter::emit_class(&class, "ogit-erp"); - assert!(triples.iter().any(|t| t.predicate == "ogar:description" && t.object == "Sale Order")); - assert!(triples.iter().any(|t| t.predicate == "ogar:recordOrder" && t.object == "date desc, id")); - assert!(triples.iter().any(|t| t.predicate == "ogar:recName" && t.object == "name")); - assert!(triples.iter().any(|t| t.predicate == "ogar:declaredIn" && t.object == "sale")); - assert!(triples.iter().any(|t| t.predicate == "ogar:sourceVersion" && t.object == "17.0")); - assert!(triples.iter().any(|t| t.predicate == "ogar:checkCompanyAuto" && t.object == "true")); + assert!( + triples + .iter() + .any(|t| t.predicate == "ogar:description" && t.object == "Sale Order") + ); + assert!( + triples + .iter() + .any(|t| t.predicate == "ogar:recordOrder" && t.object == "date desc, id") + ); + assert!( + triples + .iter() + .any(|t| t.predicate == "ogar:recName" && t.object == "name") + ); + assert!( + triples + .iter() + .any(|t| t.predicate == "ogar:declaredIn" && t.object == "sale") + ); + assert!( + triples + .iter() + .any(|t| t.predicate == "ogar:sourceVersion" && t.object == "17.0") + ); + assert!( + triples + .iter() + .any(|t| t.predicate == "ogar:checkCompanyAuto" && t.object == "true") + ); } #[test] @@ -1114,7 +1255,11 @@ mod tests { let mut class = Class::new("mail.thread"); class.abstract_model = true; let triples = TripleEmitter::emit_class(&class, "ogit-erp"); - assert!(triples.iter().any(|t| t.predicate == "ogar:abstractModel" && t.object == "true")); + assert!( + triples + .iter() + .any(|t| t.predicate == "ogar:abstractModel" && t.object == "true") + ); assert!(!triples.iter().any(|t| t.predicate == "ogar:transientModel")); } @@ -1129,11 +1274,27 @@ mod tests { assoc.delegate = Some(false); class.associations.push(assoc); let triples = TripleEmitter::emit_class(&class, "ogit-erp"); - assert!(triples.iter().any(|t| t.predicate == "ogar:ondelete" && t.object == "restrict")); - assert!(triples.iter().any(|t| t.predicate == "ogar:autoJoin" && t.object == "true")); + assert!( + triples + .iter() + .any(|t| t.predicate == "ogar:ondelete" && t.object == "restrict") + ); + assert!( + triples + .iter() + .any(|t| t.predicate == "ogar:autoJoin" && t.object == "true") + ); assert!(triples.iter().any(|t| t.predicate == "ogar:contextSource")); - assert!(triples.iter().any(|t| t.predicate == "ogar:checkCompany" && t.object == "true")); - assert!(triples.iter().any(|t| t.predicate == "ogar:delegateField" && t.object == "false")); + assert!( + triples + .iter() + .any(|t| t.predicate == "ogar:checkCompany" && t.object == "true") + ); + assert!( + triples + .iter() + .any(|t| t.predicate == "ogar:delegateField" && t.object == "false") + ); } #[test] @@ -1152,16 +1313,51 @@ mod tests { attr.options.default_source = Some("New".into()); class.attributes.push(attr); let triples = TripleEmitter::emit_class(&class, "ogit-erp"); - assert!(triples.iter().any(|t| t.predicate == "ogar:required" && t.object == "true")); - assert!(triples.iter().any(|t| t.predicate == "ogar:translate" && t.object == "true")); - assert!(triples.iter().any(|t| t.predicate == "ogar:tracking" && t.object == "10")); - assert!(triples.iter().any(|t| t.predicate == "ogar:indexed" && t.object == "true")); - assert!(triples.iter().any(|t| t.predicate == "ogar:fieldSize" && t.object == "64")); - assert!(triples.iter().any(|t| t.predicate == "ogar:helpText" && t.object == "Order reference")); - assert!(triples.iter().any(|t| t.predicate == "ogar:fieldLabel" && t.object == "Order")); - assert!(triples.iter().any(|t| t.predicate == "ogar:default" && t.object == "New")); + assert!( + triples + .iter() + .any(|t| t.predicate == "ogar:required" && t.object == "true") + ); + assert!( + triples + .iter() + .any(|t| t.predicate == "ogar:translate" && t.object == "true") + ); + assert!( + triples + .iter() + .any(|t| t.predicate == "ogar:tracking" && t.object == "10") + ); + assert!( + triples + .iter() + .any(|t| t.predicate == "ogar:indexed" && t.object == "true") + ); + assert!( + triples + .iter() + .any(|t| t.predicate == "ogar:fieldSize" && t.object == "64") + ); + assert!( + triples + .iter() + .any(|t| t.predicate == "ogar:helpText" && t.object == "Order reference") + ); + assert!( + triples + .iter() + .any(|t| t.predicate == "ogar:fieldLabel" && t.object == "Order") + ); + assert!( + triples + .iter() + .any(|t| t.predicate == "ogar:default" && t.object == "New") + ); // Two groups → two triples. - let group_triples: Vec<_> = triples.iter().filter(|t| t.predicate == "ogar:groupAccess").collect(); + let group_triples: Vec<_> = triples + .iter() + .filter(|t| t.predicate == "ogar:groupAccess") + .collect(); assert_eq!(group_triples.len(), 2); } @@ -1174,9 +1370,21 @@ mod tests { let mut class = Class::new("sale.order"); class.attributes.push(attr); let triples = TripleEmitter::emit_class(&class, "ogit-erp"); - assert!(triples.iter().any(|t| t.predicate == "ogar:precision" && t.object == "16")); - assert!(triples.iter().any(|t| t.predicate == "ogar:scale" && t.object == "2")); - assert!(triples.iter().any(|t| t.predicate == "ogar:currencyField" && t.object == "currency_id")); + assert!( + triples + .iter() + .any(|t| t.predicate == "ogar:precision" && t.object == "16") + ); + assert!( + triples + .iter() + .any(|t| t.predicate == "ogar:scale" && t.object == "2") + ); + assert!( + triples + .iter() + .any(|t| t.predicate == "ogar:currencyField" && t.object == "currency_id") + ); } #[test] @@ -1191,14 +1399,42 @@ mod tests { cf.inverse_method = Some("_inverse_total".into()); class.computed_fields.push(cf); let triples = TripleEmitter::emit_class(&class, "ogit-erp"); - assert!(triples.iter().any(|t| t.predicate == "ogar:hasComputedField")); - assert!(triples.iter().any(|t| t.predicate == "ogar:computedFieldRef" && t.object == "amount_total")); + assert!( + triples + .iter() + .any(|t| t.predicate == "ogar:hasComputedField") + ); + assert!( + triples + .iter() + .any(|t| t.predicate == "ogar:computedFieldRef" && t.object == "amount_total") + ); assert!(triples.iter().any(|t| t.predicate == "ogar:computeMethod" && t.object == "_compute_amount_total")); - assert!(triples.iter().any(|t| t.predicate == "ogar:stored" && t.object == "true")); - assert!(triples.iter().any(|t| t.predicate == "ogar:dependsPath" && t.object == "order_line.price_total")); - assert!(triples.iter().any(|t| t.predicate == "ogar:dependsPath" && t.object == "currency_id")); - assert!(triples.iter().any(|t| t.predicate == "ogar:dependsContext" && t.object == "company_id")); - assert!(triples.iter().any(|t| t.predicate == "ogar:inverseMethod" && t.object == "_inverse_total")); + assert!( + triples + .iter() + .any(|t| t.predicate == "ogar:stored" && t.object == "true") + ); + assert!( + triples + .iter() + .any(|t| t.predicate == "ogar:dependsPath" && t.object == "order_line.price_total") + ); + assert!( + triples + .iter() + .any(|t| t.predicate == "ogar:dependsPath" && t.object == "currency_id") + ); + assert!( + triples + .iter() + .any(|t| t.predicate == "ogar:dependsContext" && t.object == "company_id") + ); + assert!( + triples + .iter() + .any(|t| t.predicate == "ogar:inverseMethod" && t.object == "_inverse_total") + ); assert!(!triples.iter().any(|t| t.predicate == "ogar:searchMethod")); } @@ -1214,10 +1450,22 @@ mod tests { class.methods.push(m); let triples = TripleEmitter::emit_class(&class, "ogit-erp"); assert!(triples.iter().any(|t| t.predicate == "ogar:hasMethod")); - assert!(triples.iter().any(|t| t.predicate == "ogar:methodName" && t.object == "create")); + assert!( + triples + .iter() + .any(|t| t.predicate == "ogar:methodName" && t.object == "create") + ); assert!(triples.iter().any(|t| t.predicate == "ogar:methodKind" && t.object == "ogar:ApiModelCreateMulti")); - assert!(triples.iter().any(|t| t.predicate == "ogar:recordSemantics" && t.object == "ogar:ClassLevel")); - assert!(triples.iter().any(|t| t.predicate == "ogar:decoratorName" && t.object == "api.model_create_multi")); + assert!( + triples + .iter() + .any(|t| t.predicate == "ogar:recordSemantics" && t.object == "ogar:ClassLevel") + ); + assert!( + triples.iter().any( + |t| t.predicate == "ogar:decoratorName" && t.object == "api.model_create_multi" + ) + ); } #[test] @@ -1261,12 +1509,36 @@ mod tests { def.body_source = Some("if self.state != 'draft': raise...".into()); def.decorators = vec!["api.depends".into()]; let triples = TripleEmitter::emit_action_def(&def); - assert!(triples.iter().any(|t| t.predicate == "rdf:type" && t.object == "ogar:ActionDef")); - assert!(triples.iter().any(|t| t.predicate == "ogar:actionPredicate" && t.object == "action_confirm")); - assert!(triples.iter().any(|t| t.predicate == "ogar:defaultSubject" && t.object == "ogar:User")); - assert!(triples.iter().any(|t| t.predicate == "ogar:defaultModal" && t.object == "ogar:Atomic")); - assert!(triples.iter().any(|t| t.predicate == "ogar:kausalKind" && t.object == "ogar:StateGuard")); - assert!(triples.iter().any(|t| t.predicate == "ogar:guardField" && t.object == "state")); + assert!( + triples + .iter() + .any(|t| t.predicate == "rdf:type" && t.object == "ogar:ActionDef") + ); + assert!( + triples + .iter() + .any(|t| t.predicate == "ogar:actionPredicate" && t.object == "action_confirm") + ); + assert!( + triples + .iter() + .any(|t| t.predicate == "ogar:defaultSubject" && t.object == "ogar:User") + ); + assert!( + triples + .iter() + .any(|t| t.predicate == "ogar:defaultModal" && t.object == "ogar:Atomic") + ); + assert!( + triples + .iter() + .any(|t| t.predicate == "ogar:kausalKind" && t.object == "ogar:StateGuard") + ); + assert!( + triples + .iter() + .any(|t| t.predicate == "ogar:guardField" && t.object == "state") + ); let guard_values: Vec<_> = triples .iter() .filter(|t| t.predicate == "ogar:guardValue") @@ -1293,13 +1565,41 @@ mod tests { inv.idempotency_key = Some("confirm-sale-order-42".into()); inv.emitted_at_millis = Some(1717500000000); let triples = TripleEmitter::emit_action_invocation(&inv); - assert!(triples.iter().any(|t| t.predicate == "rdf:type" && t.object == "ogar:ActionInvocation")); - assert!(triples.iter().any(|t| t.predicate == "ogar:actionSubject" && t.object == "ogar:User")); - assert!(triples.iter().any(|t| t.predicate == "ogar:actionState" && t.object == "ogar:Pending")); - assert!(triples.iter().any(|t| t.predicate == "ogar:traceId" && t.object == "00-trace-id-here")); - assert!(triples.iter().any(|t| t.predicate == "ogar:parentInvocation")); - assert!(triples.iter().any(|t| t.predicate == "ogar:idempotencyKey" && t.object == "confirm-sale-order-42")); - assert!(triples.iter().any(|t| t.predicate == "ogar:actionTenant" && t.object == "acme")); + assert!( + triples + .iter() + .any(|t| t.predicate == "rdf:type" && t.object == "ogar:ActionInvocation") + ); + assert!( + triples + .iter() + .any(|t| t.predicate == "ogar:actionSubject" && t.object == "ogar:User") + ); + assert!( + triples + .iter() + .any(|t| t.predicate == "ogar:actionState" && t.object == "ogar:Pending") + ); + assert!( + triples + .iter() + .any(|t| t.predicate == "ogar:traceId" && t.object == "00-trace-id-here") + ); + assert!( + triples + .iter() + .any(|t| t.predicate == "ogar:parentInvocation") + ); + assert!( + triples.iter().any( + |t| t.predicate == "ogar:idempotencyKey" && t.object == "confirm-sale-order-42" + ) + ); + assert!( + triples + .iter() + .any(|t| t.predicate == "ogar:actionTenant" && t.object == "acme") + ); } #[test] @@ -1321,19 +1621,25 @@ mod tests { .find(|x| x.predicate == "ogar:onEnter") .expect("ogar:onEnter link triple emitted"); let node = link.object.clone(); - assert!(node.ends_with("::on_enter"), "onEnter node should be derived from def id, got: {node}"); - assert!(t - .iter() - .any(|x| x.subject == node && x.predicate == "ogar:enterField" && x.object == "state")); - assert!(t - .iter() - .any(|x| x.subject == node && x.predicate == "ogar:enterToValue" && x.object == "sale")); + assert!( + node.ends_with("::on_enter"), + "onEnter node should be derived from def id, got: {node}" + ); + assert!( + t.iter().any(|x| x.subject == node + && x.predicate == "ogar:enterField" + && x.object == "state") + ); + assert!(t.iter().any(|x| x.subject == node + && x.predicate == "ogar:enterToValue" + && x.object == "sale")); assert!(t .iter() .any(|x| x.predicate == "ogar:guardFailurePolicy" && x.object == "ogar:Postponable")); - assert!(t - .iter() - .any(|x| x.predicate == "ogar:stateTimeoutMillis" && x.object == "30000")); + assert!( + t.iter() + .any(|x| x.predicate == "ogar:stateTimeoutMillis" && x.object == "30000") + ); } #[test] @@ -1356,22 +1662,41 @@ mod tests { let mut def = ogar_vocab::ActionDef::new("a", "p", "ogit-op/Foo"); def.kausal = Some(ogar_vocab::KausalSpec::lifecycle("before_save")); let t = TripleEmitter::emit_action_def(&def); - assert!(t.iter().any(|t| t.predicate == "ogar:kausalKind" && t.object == "ogar:LifecycleTrigger")); - assert!(t.iter().any(|t| t.predicate == "ogar:triggerEvent" && t.object == "before_save")); + assert!( + t.iter() + .any(|t| t.predicate == "ogar:kausalKind" && t.object == "ogar:LifecycleTrigger") + ); + assert!( + t.iter() + .any(|t| t.predicate == "ogar:triggerEvent" && t.object == "before_save") + ); - def.kausal = Some(ogar_vocab::KausalSpec::depends(vec!["partner_id".into(), "amount".into()])); + def.kausal = Some(ogar_vocab::KausalSpec::depends(vec![ + "partner_id".into(), + "amount".into(), + ])); let t = TripleEmitter::emit_action_def(&def); - assert!(t.iter().any(|t| t.predicate == "ogar:kausalKind" && t.object == "ogar:Depends")); + assert!( + t.iter() + .any(|t| t.predicate == "ogar:kausalKind" && t.object == "ogar:Depends") + ); // Kausal-scoped predicate, distinct from ComputedField's ogar:dependsPath // (so "enumerate computed fields" queries don't catch actions). - let paths: Vec<_> = t.iter().filter(|t| t.predicate == "ogar:kausalDependsPath").map(|t| t.object.as_str()).collect(); + let paths: Vec<_> = t + .iter() + .filter(|t| t.predicate == "ogar:kausalDependsPath") + .map(|t| t.object.as_str()) + .collect(); assert_eq!(paths.len(), 2); // The ComputedField predicate must NOT appear on an ActionDef. assert!(!t.iter().any(|t| t.predicate == "ogar:dependsPath")); def.kausal = Some(ogar_vocab::KausalSpec::External); let t = TripleEmitter::emit_action_def(&def); - assert!(t.iter().any(|t| t.predicate == "ogar:kausalKind" && t.object == "ogar:External")); + assert!( + t.iter() + .any(|t| t.predicate == "ogar:kausalKind" && t.object == "ogar:External") + ); } #[test] @@ -1402,7 +1727,8 @@ mod tests { ); // The field paths are NOT projected to TTL yet (the debt). assert!( - !t.iter().any(|t| t.object == "state" || t.object == "partner_id"), + !t.iter() + .any(|t| t.object == "state" || t.object == "partner_id"), "interim: Arm B field paths are dropped at TTL emission (§6 deferred)" ); } @@ -1419,22 +1745,34 @@ mod tests { let mut inv = ogar_vocab::ActionInvocation::new("i", "d", "o"); inv.state = state; let triples = TripleEmitter::emit_action_invocation(&inv); - assert!(triples.iter().any(|t| t.predicate == "ogar:actionState" && t.object == expected)); + assert!( + triples + .iter() + .any(|t| t.predicate == "ogar:actionState" && t.object == expected) + ); } } #[test] fn callback_two_forms_emit_distinct_triples() { let mut class = sample_work_package(); - class.callbacks.push(Callback::method("before_save", "touch_parent")); - class.callbacks.push(Callback::block("after_create", "notify_subscribers")); + class + .callbacks + .push(Callback::method("before_save", "touch_parent")); + class + .callbacks + .push(Callback::block("after_create", "notify_subscribers")); let triples = TripleEmitter::emit_class(&class, "ogit-op"); - assert!(triples.iter().any(|t| { - t.predicate == "ogar:targetMethod" && t.object == "touch_parent" - })); - assert!(triples.iter().any(|t| { - t.predicate == "ogar:callbackBody" && t.object == "notify_subscribers" - })); + assert!( + triples + .iter() + .any(|t| { t.predicate == "ogar:targetMethod" && t.object == "touch_parent" }) + ); + assert!( + triples.iter().any(|t| { + t.predicate == "ogar:callbackBody" && t.object == "notify_subscribers" + }) + ); } #[test] diff --git a/crates/ogar-from-elixir/src/lib.rs b/crates/ogar-from-elixir/src/lib.rs index a072c60..665077d 100644 --- a/crates/ogar-from-elixir/src/lib.rs +++ b/crates/ogar-from-elixir/src/lib.rs @@ -124,7 +124,11 @@ pub fn extract_action_defs(source_tree: &Path) -> Vec { } } // gen_statem is conventionally `@behaviour :gen_statem`, not `use`. - if module.use_directives.iter().any(|u| u == ":gen_statem" || u == "GenStateMachine") { + if module + .use_directives + .iter() + .any(|u| u == ":gen_statem" || u == "GenStateMachine") + { defs.extend(extract_gen_statem_actions(module)); } } @@ -249,7 +253,9 @@ fn extract_gen_statem_actions(_module: &ElixirModule) -> Vec { /// - LiveView `def handle_event(event, params, socket)` → /// `ActionDef { default_subject: User }` fn extract_phoenix_actions(_module: &ElixirModule) -> Vec { - todo!("extract controller / channel / liveview handlers per ELIXIR-HIRO-PREFETCH §2.2 Phoenix.* rows") + todo!( + "extract controller / channel / liveview handlers per ELIXIR-HIRO-PREFETCH §2.2 Phoenix.* rows" + ) } /// Extract `ActionDef`s from `Oban.Worker.perform/1` definitions and @@ -420,8 +426,14 @@ mod tests { // This test documents the recognized `use` / `@behaviour` // directives so a parser wiring change can't silently drop // dispatch keys. - let recognized_use = ["Ecto.Schema", "GenServer", "Phoenix.Controller", - "Phoenix.Channel", "Phoenix.LiveView", "Oban.Worker"]; + let recognized_use = [ + "Ecto.Schema", + "GenServer", + "Phoenix.Controller", + "Phoenix.Channel", + "Phoenix.LiveView", + "Oban.Worker", + ]; let recognized_behaviour = [":gen_statem", "GenStateMachine"]; // Compile-time presence is the assertion; the list is the spec. assert_eq!(recognized_use.len(), 6); diff --git a/crates/ogar-from-rails/src/lib.rs b/crates/ogar-from-rails/src/lib.rs index 7852ac8..cd09c03 100644 --- a/crates/ogar-from-rails/src/lib.rs +++ b/crates/ogar-from-rails/src/lib.rs @@ -416,7 +416,8 @@ mod tests { #[test] #[ignore = "requires a Rails source tree with db/migrate/tables (REDMINE_SRC or /home/user/openproject)"] fn mainline_extract_app_routes_through_schema_aware_extractor() { - let src = std::env::var("REDMINE_SRC").unwrap_or_else(|_| "/home/user/openproject".to_string()); + let src = + std::env::var("REDMINE_SRC").unwrap_or_else(|_| "/home/user/openproject".to_string()); let path = PathBuf::from(&src); if !path.exists() { eprintln!("skipping: {} not present", path.display()); diff --git a/crates/ogar-from-ruff/src/lib.rs b/crates/ogar-from-ruff/src/lib.rs index 682e400..e941b5e 100644 --- a/crates/ogar-from-ruff/src/lib.rs +++ b/crates/ogar-from-ruff/src/lib.rs @@ -74,8 +74,8 @@ pub mod sqlalchemy; // WS-G-D use std::collections::HashMap; use ogar_vocab::{ - canonical_concept, ActionDef, Association, AssociationKind, Attribute, Callback, Class, - ComputedField, EnumDecl, EnumSource, Inheritance, KausalSpec, Language, Scope, Validation, + ActionDef, Association, AssociationKind, Attribute, Callback, Class, ComputedField, EnumDecl, + EnumSource, Inheritance, KausalSpec, Language, Scope, Validation, canonical_concept, }; use ruff_spo_triplet::{ AssocDecl, AssocKind, AttrDecl, AttrKind, Callback as RuffCallback, ConcernKind, Model, @@ -102,7 +102,9 @@ pub fn lift_model_graph_python(graph: &ModelGraph) -> Vec { fn lift_model_graph_with_language(graph: &ModelGraph, language: Language) -> Vec { let domain = classify_domain(&graph.namespace); - let concept_domain = domain.as_deref().and_then(ogar_vocab::source_domain_concept); + let concept_domain = domain + .as_deref() + .and_then(ogar_vocab::source_domain_concept); // The harvest namespace IS the curator id (`"openproject"`, // `"redmine"`, `"odoo"`, …). `source_domain` is the coarse bucket it // maps to; `source_curator` keeps the specific product so two curators @@ -127,8 +129,10 @@ fn lift_model_graph_with_language(graph: &ModelGraph, language: Language) -> Vec // `Role` in a non-project curator must stay `role`, not become // `project_role` (codex P2 on #72). Cross-domain bridges like // `billable_work_entry` are exempt and still converge. - class.canonical_concept = - Some(ogar_vocab::canonical_concept_in_domain(&m.name, concept_domain)); + class.canonical_concept = Some(ogar_vocab::canonical_concept_in_domain( + &m.name, + concept_domain, + )); class }) .collect() @@ -181,7 +185,11 @@ fn lift_model_with_language(model: &Model, language: Language) -> Class { class.parent = model.sti.as_ref().and_then(sti_parent); class.inheritance = lift_inheritance(model); class.canonical_concept = Some(canonical_concept(&model.name)); - class.associations = model.associations.iter().filter_map(lift_association).collect(); + class.associations = model + .associations + .iter() + .filter_map(lift_association) + .collect(); class.mixins = lift_mixins(model); // Odoo `_inherit` (multi-parent mixin composition) lands on the same // mixins shelf the vocab designates for it — `Class::mixins` doc names @@ -197,7 +205,11 @@ fn lift_model_with_language(model: &Model, language: Language) -> Class { class.scopes = model.scopes.iter().filter_map(lift_scope).collect(); class.scope_predeclarations = lift_scope_predeclarations(model); class.callbacks = model.callbacks.iter().map(lift_callback).collect(); - class.validations = model.validations.iter().filter_map(lift_validation).collect(); + class.validations = model + .validations + .iter() + .filter_map(lift_validation) + .collect(); class.default_scope = lift_default_scope(model); // Rails carries its DECLARED schema in the AR-DSL vectors lifted above // (`attribute :x, :type`, `belongs_to :y`); an Odoo model instead @@ -240,7 +252,8 @@ fn lift_model_with_language(model: &Model, language: Language) -> Class { fn project_odoo_fields(class: &mut Class, model: &Model) { for field in &model.fields { if let Some(comodel) = &field.target { - let kind = odoo_relation_kind(field.relation_kind.as_deref(), field.inverse_name.is_some()); + let kind = + odoo_relation_kind(field.relation_kind.as_deref(), field.inverse_name.is_some()); let mut assoc = Association::new(kind, &field.name); assoc.class_name = Some(comodel.clone()); assoc.inverse_of = field.inverse_name.clone(); @@ -317,7 +330,8 @@ fn project_odoo_fields(class: &mut Class, model: &Model) { pub(crate) fn project_total_schema_fields(class: &mut Class, model: &Model) { for field in &model.fields { if let Some(comodel) = &field.target { - let kind = odoo_relation_kind(field.relation_kind.as_deref(), field.inverse_name.is_some()); + let kind = + odoo_relation_kind(field.relation_kind.as_deref(), field.inverse_name.is_some()); let mut assoc = Association::new(kind, &field.name); assoc.class_name = Some(comodel.clone()); assoc.inverse_of = field.inverse_name.clone(); @@ -458,9 +472,7 @@ pub fn project_work_item_role_from_mixin(mixin: &str) -> Option<&'static str> { /// ([`project_work_item_role_from_mixin`]). Returns the empty set when /// the class has no project-work-item shape. #[must_use] -pub fn project_work_item_canonical_roles( - class: &Class, -) -> std::collections::HashSet<&'static str> { +pub fn project_work_item_canonical_roles(class: &Class) -> std::collections::HashSet<&'static str> { let mut set = std::collections::HashSet::new(); for a in &class.associations { if let Some(role) = project_work_item_role(&a.name) { @@ -499,9 +511,7 @@ pub fn project_role(curator_name: &str) -> Option<&'static str> { // in both curators: members / memberships / users (Redmine + OP) / // member_principals / principals (OP only — OP adds the // through-Principal hop). - "members" | "memberships" | "users" | "member_principals" | "principals" => { - Some("members") - } + "members" | "memberships" | "users" | "member_principals" | "principals" => Some("members"), _ => None, } } @@ -1362,7 +1372,9 @@ mod tests { let class = lift_model(&mk_model()); assert_eq!( class.inheritance, - Inheritance::Concrete { parent: "Issue".to_string() }, + Inheritance::Concrete { + parent: "Issue".to_string() + }, ); } @@ -1386,11 +1398,16 @@ mod tests { }); assert_eq!( lift_model(&r).inheritance, - Inheritance::RootedAt { root: "Principal".to_string() }, + Inheritance::RootedAt { + root: "Principal".to_string() + }, ); // no STI info at all → Root. - assert_eq!(lift_model(&Model::new("Plain")).inheritance, Inheritance::Root); + assert_eq!( + lift_model(&Model::new("Plain")).inheritance, + Inheritance::Root + ); } #[test] @@ -1426,7 +1443,10 @@ mod tests { let class = lift_model(&mk_model()); assert_eq!(class.callbacks.len(), 1); assert_eq!(class.callbacks[0].event, "before_save"); - assert_eq!(class.callbacks[0].target_method.as_deref(), Some("set_status")); + assert_eq!( + class.callbacks[0].target_method.as_deref(), + Some("set_status") + ); } #[test] @@ -1568,11 +1588,17 @@ mod tests { // OpenProject → "project" let mut op = ModelGraph::new("openproject"); op.models.push(Model::new("WorkPackage")); - assert_eq!(lift_model_graph(&op)[0].source_domain.as_deref(), Some("project")); + assert_eq!( + lift_model_graph(&op)[0].source_domain.as_deref(), + Some("project") + ); // Odoo → "erp" let mut odoo = ModelGraph::new("odoo"); odoo.models.push(Model::new("AccountMove")); - assert_eq!(lift_model_graph(&odoo)[0].source_domain.as_deref(), Some("erp")); + assert_eq!( + lift_model_graph(&odoo)[0].source_domain.as_deref(), + Some("erp") + ); // Unrecognized → None (not guessed). let mut other = ModelGraph::new("mystery"); other.models.push(Model::new("X")); @@ -1600,7 +1626,10 @@ mod tests { assert_eq!(concept("odoo", "Role"), "role"); // Cross-domain bridge survives the gate from any domain. assert_eq!(concept("openproject", "TimeEntry"), "billable_work_entry"); - assert_eq!(concept("odoo", "account_analytic_line"), "billable_work_entry"); + assert_eq!( + concept("odoo", "account_analytic_line"), + "billable_work_entry" + ); // `lift_model` itself stays domain-blind (all-domains best guess). assert_eq!( lift_model(&Model::new("Role")).canonical_concept.as_deref(), @@ -1683,8 +1712,15 @@ mod tests { let roles = project_work_item_canonical_roles(&redmine); assert_eq!(roles.len(), 9); for r in [ - "project", "status", "type", "priority", "author", "assignee", - "journals", "relations", "time_entries", + "project", + "status", + "type", + "priority", + "author", + "assignee", + "journals", + "relations", + "time_entries", ] { assert!(roles.contains(r), "Redmine projection missing role {r}"); } @@ -1708,7 +1744,10 @@ mod tests { "WorkPackages::Relations".to_string(), ]; let op_roles = project_work_item_canonical_roles(&op); - assert_eq!(op_roles, roles, "OP must project to the same canonical role set as Redmine"); + assert_eq!( + op_roles, roles, + "OP must project to the same canonical role set as Redmine" + ); } #[test] @@ -1721,7 +1760,13 @@ mod tests { assert_eq!(project_role("work_packages"), Some("work_items")); // The through-association actor chain — both spellings + the OP // extra hops (member_principals, principals) land at `members`. - for src in ["members", "memberships", "users", "member_principals", "principals"] { + for src in [ + "members", + "memberships", + "users", + "member_principals", + "principals", + ] { assert_eq!(project_role(src), Some("members"), "{src} -> members"); } // Off-shape names return None (real but not yet promoted into the @@ -1766,9 +1811,17 @@ mod tests { // a real cross-curator concept but lives behind mixins and is // not yet decoded (see ogar_vocab::project doc). let expected: std::collections::HashSet<&'static str> = - ["work_items", "time_entries", "members"].into_iter().collect(); - assert_eq!(r_roles, expected, "Redmine projection must cover the 3-role canonical surface"); - assert_eq!(o_roles, expected, "OP projection must cover the same surface"); + ["work_items", "time_entries", "members"] + .into_iter() + .collect(); + assert_eq!( + r_roles, expected, + "Redmine projection must cover the 3-role canonical surface" + ); + assert_eq!( + o_roles, expected, + "OP projection must cover the same surface" + ); assert_eq!(r_roles, o_roles, "lineage-transcode parity for Project"); } @@ -1776,24 +1829,32 @@ mod tests { fn lift_model_sets_canonical_concept_including_promoted_invariant() { // Plain class with no promoted invariant → lexical concept. assert_eq!( - lift_model(&Model::new("Account")).canonical_concept.as_deref(), + lift_model(&Model::new("Account")) + .canonical_concept + .as_deref(), Some("account"), ); // Promoted ERP-bridge concept (BillableWorkEntry) — OpenProject // `TimeEntry` deterministically wired into the cross-domain bridge. assert_eq!( - lift_model(&Model::new("TimeEntry")).canonical_concept.as_deref(), + lift_model(&Model::new("TimeEntry")) + .canonical_concept + .as_deref(), Some("billable_work_entry"), ); // Promoted project-domain concept (ProjectWorkItem) — Redmine // `Issue` and OpenProject `WorkPackage` both wire into the // same-domain work-item invariant. assert_eq!( - lift_model(&Model::new("Issue")).canonical_concept.as_deref(), + lift_model(&Model::new("Issue")) + .canonical_concept + .as_deref(), Some("project_work_item"), ); assert_eq!( - lift_model(&Model::new("WorkPackage")).canonical_concept.as_deref(), + lift_model(&Model::new("WorkPackage")) + .canonical_concept + .as_deref(), Some("project_work_item"), ); } @@ -1841,7 +1902,10 @@ mod tests { fn lift_actions_is_facts_only() { let acts = lift_actions(&mk_model_with_functions()); let a = &acts[0]; - assert!(a.kausal.is_none(), "reads must NOT become a causal dependency"); + assert!( + a.kausal.is_none(), + "reads must NOT become a causal dependency" + ); assert!(a.body_source.is_none()); assert!(a.decorators.is_empty()); } @@ -1927,7 +1991,10 @@ mod tests { assert_eq!(acts.len(), 1); assert_eq!( acts[0].kausal, - Some(KausalSpec::depends(vec!["qty".to_string(), "price".to_string()])), + Some(KausalSpec::depends(vec![ + "qty".to_string(), + "price".to_string() + ])), ); } diff --git a/crates/ogar-from-ruff/src/mint.rs b/crates/ogar-from-ruff/src/mint.rs index 3d0e9be..d404c19 100644 --- a/crates/ogar-from-ruff/src/mint.rs +++ b/crates/ogar-from-ruff/src/mint.rs @@ -29,9 +29,9 @@ //! [`OpenProjectPort`](ogar_vocab::ports::OpenProjectPort) — different render //! skins, the same conceptual identity where they converge. -use ogar_vocab::{ActionDef, Class}; use ogar_vocab::app::render_classid_for; use ogar_vocab::ports::PortSpec; +use ogar_vocab::{ActionDef, Class}; use ruff_spo_address::{Facet, Mint, mint_with_classid}; use ruff_spo_triplet::{ModelGraph, expand}; @@ -98,7 +98,11 @@ pub fn compile_graph_python(graph: &ModelGraph) -> Vec(&node), [0; 6], [0; 6])); - CompiledClass { class, facet, actions: lift_actions(model) } + CompiledClass { + class, + facet, + actions: lift_actions(model), + } }) .collect() } @@ -153,7 +157,11 @@ pub fn compile_graph_sqlalchemy(graph: &ModelGraph) -> Vec(&node), [0; 6], [0; 6])); - CompiledClass { class, facet, actions: lift_actions(model) } + CompiledClass { + class, + facet, + actions: lift_actions(model), + } }) .collect() } @@ -188,7 +196,11 @@ pub fn compile_graph_ruby(graph: &ModelGraph) -> Vec let facet = mint .facet(&node) .unwrap_or_else(|| Facet::from_parts(classid_for_node::

(&node), [0; 6], [0; 6])); - CompiledClass { class, facet, actions: lift_actions(model) } + CompiledClass { + class, + facet, + actions: lift_actions(model), + } }) .collect() } @@ -528,7 +540,10 @@ mod tests { "TimesheetActivity is an aliased convergence pin (BILLABLE_WORK_ENTRY | WoA), \ not the bootstrap address SPEC-5 assumed" ); - assert_eq!(ogar_vocab::app::concept_of(cc.facet.facet_classid()), 0x0103); + assert_eq!( + ogar_vocab::app::concept_of(cc.facet.facet_classid()), + 0x0103 + ); assert_eq!( ogar_vocab::app::app_of(cc.facet.facet_classid()), WoaPort::APP_PREFIX diff --git a/crates/ogar-from-ruff/src/sqlalchemy.rs b/crates/ogar-from-ruff/src/sqlalchemy.rs index a7a7cc1..9440466 100644 --- a/crates/ogar-from-ruff/src/sqlalchemy.rs +++ b/crates/ogar-from-ruff/src/sqlalchemy.rs @@ -61,7 +61,11 @@ pub fn lift_model_sqlalchemy(model: &Model) -> Class { // AR-DSL-shaped associations (`db.relationship(...)`) lift the same way // Rails' `belongs_to`/`has_many` do — `lift_association` is producer // agnostic and already `pub` in `lib.rs`. - class.associations = model.associations.iter().filter_map(lift_association).collect(); + class.associations = model + .associations + .iter() + .filter_map(lift_association) + .collect(); project_sqlalchemy_fields(&mut class, model); class } @@ -73,7 +77,9 @@ pub fn lift_model_sqlalchemy(model: &Model) -> Class { #[must_use] pub fn lift_model_graph_sqlalchemy(graph: &ModelGraph) -> Vec { let domain = classify_woa_domain(&graph.namespace); - let concept_domain = domain.as_deref().and_then(ogar_vocab::source_domain_concept); + let concept_domain = domain + .as_deref() + .and_then(ogar_vocab::source_domain_concept); let curator = if graph.namespace.is_empty() { None } else { @@ -86,8 +92,10 @@ pub fn lift_model_graph_sqlalchemy(graph: &ModelGraph) -> Vec { let mut class = lift_model_sqlalchemy(m); class.source_domain = domain.clone(); class.source_curator = curator.clone(); - class.canonical_concept = - Some(ogar_vocab::canonical_concept_in_domain(&m.name, concept_domain)); + class.canonical_concept = Some(ogar_vocab::canonical_concept_in_domain( + &m.name, + concept_domain, + )); class }) .collect() @@ -256,10 +264,16 @@ mod tests { assert_eq!(title.options.required, Some(true)); } - let ruby_names: std::collections::BTreeSet<_> = - ruby_classes[0].attributes.iter().map(|a| a.name.clone()).collect(); - let sqlalchemy_names: std::collections::BTreeSet<_> = - sqlalchemy_classes[0].attributes.iter().map(|a| a.name.clone()).collect(); + let ruby_names: std::collections::BTreeSet<_> = ruby_classes[0] + .attributes + .iter() + .map(|a| a.name.clone()) + .collect(); + let sqlalchemy_names: std::collections::BTreeSet<_> = sqlalchemy_classes[0] + .attributes + .iter() + .map(|a| a.name.clone()) + .collect(); assert_eq!( ruby_names, sqlalchemy_names, "both producers must project the identical attribute name set, proving shared \ diff --git a/crates/ogar-from-ruff/tests/woa_parity_probe.rs b/crates/ogar-from-ruff/tests/woa_parity_probe.rs index 0baab5b..618257e 100644 --- a/crates/ogar-from-ruff/tests/woa_parity_probe.rs +++ b/crates/ogar-from-ruff/tests/woa_parity_probe.rs @@ -159,7 +159,11 @@ fn woa_parity_probe_prints_the_d_parity_probe_wp_1_metric() { .any(|a| &a.name == *n && a.options.required.is_some()) }) .count(); - assert_eq!(cc.class.attributes.len(), 3, "no extra/missing attributes (FK deduped, no dup)"); + assert_eq!( + cc.class.attributes.len(), + 3, + "no extra/missing attributes (FK deduped, no dup)" + ); // ── associations axis ── let timesheet_assoc = cc.class.associations.iter().find(|a| a.name == "timesheet"); @@ -201,7 +205,11 @@ sonst bare (Spiegel emit_rust)" associations_axis.1, actions_axis.0, actions_axis.1, - if drift_list.is_empty() { "none".to_string() } else { drift_list.join("; ") }, + if drift_list.is_empty() { + "none".to_string() + } else { + drift_list.join("; ") + }, ); println!("{metric}"); println!("--- emitted Python ---\n{py}"); @@ -212,8 +220,14 @@ sonst bare (Spiegel emit_rust)" assert_eq!(classes_axis.0, classes_axis.1, "classes axis must be N/N"); assert_eq!(typed, 3, "columns typed axis must be 3/3"); assert_eq!(nullability_wired, 3, "nullability axis must be 3/3"); - assert_eq!(associations_axis.0, associations_axis.1, "associations axis must be N/N"); - assert_eq!(actions_axis.0, actions_axis.1, "action names axis must be N/N"); + assert_eq!( + associations_axis.0, associations_axis.1, + "associations axis must be N/N" + ); + assert_eq!( + actions_axis.0, actions_axis.1, + "action names axis must be N/N" + ); assert!(metric.contains("classes: 1/1")); assert!(metric.contains("columns typed: 3/3")); @@ -258,10 +272,14 @@ fn woa_dataclass_py_compiles_and_instantiates() { .args(["-m", "py_compile", py_path.to_str().unwrap()]) .status() .expect("python3 available"); - assert!(compile_status.success(), "py_compile must exit 0 on the emitted module"); + assert!( + compile_status.success(), + "py_compile must exit 0 on the emitted module" + ); // Gate 2: usable dataclass (instantiate_check.py imports + instantiates it). - let checker = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/py/instantiate_check.py"); + let checker = + std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/py/instantiate_check.py"); let check_status = std::process::Command::new("python3") .arg(&checker) .arg(&py_path) diff --git a/crates/ogar-knowable-from/src/lib.rs b/crates/ogar-knowable-from/src/lib.rs index b7ce289..ac8e0a4 100644 --- a/crates/ogar-knowable-from/src/lib.rs +++ b/crates/ogar-knowable-from/src/lib.rs @@ -257,7 +257,8 @@ pub fn register_class_knowable_from( return Err(KnowableFromError::MalformedClass( "class_identity is empty; pass an OGIT-prefixed canonical \ identity such as \"ogit-erp/sale.order\" (see \ - ogar_ontology::class_identity)".into(), + ogar_ontology::class_identity)" + .into(), )); } // The `schema_ddl_hint` parameter — `None` by default (lightweight @@ -343,8 +344,8 @@ impl std::error::Error for KnowableFromError {} pub mod vart_backend { use super::{KnowableFromError, KnowableFromStore}; use std::sync::Mutex; - use vart::art::Tree; use vart::VariableSizeKey; + use vart::art::Tree; /// `KnowableFromStore` impl backed by an in-memory versioned /// adaptive radix trie. Each [`register`] call advances the trie's @@ -378,7 +379,9 @@ pub mod vart_backend { /// `version()` starts at `0` for an empty trie). #[must_use] pub fn new() -> Self { - Self { tree: Mutex::new(Tree::new()) } + Self { + tree: Mutex::new(Tree::new()), + } } /// Current max version across the trie. `0` if no `register` @@ -413,9 +416,10 @@ pub mod vart_backend { class_identity: &str, _schema_ddl_hint: Option<&str>, ) -> Result { - let mut tree = self.tree.lock().map_err(|e| { - KnowableFromError::Backend(format!("vart mutex poisoned: {e}")) - })?; + let mut tree = self + .tree + .lock() + .map_err(|e| KnowableFromError::Backend(format!("vart mutex poisoned: {e}")))?; // Advance the trie's logical version monotonically — the new // version IS the knowable_from stamp we return. saturating_add // guards the theoretical wrap (registering 2^64 times). @@ -426,9 +430,7 @@ pub mod vart_backend { // the runtime side's `inv.object_instance` trie-append also uses // — every commit is a new version of the entry). tree.insert_or_replace(&key, new_version, new_version, 0) - .map_err(|e| { - KnowableFromError::Backend(format!("vart insert: {e:?}")) - })?; + .map_err(|e| KnowableFromError::Backend(format!("vart insert: {e:?}")))?; Ok(new_version) } @@ -477,10 +479,10 @@ mod tests { class_identity: &str, schema_ddl_hint: Option<&str>, ) -> Result { - self.register_calls - .lock() - .unwrap() - .push((class_identity.to_string(), schema_ddl_hint.map(String::from))); + self.register_calls.lock().unwrap().push(( + class_identity.to_string(), + schema_ddl_hint.map(String::from), + )); let mut next = self.next_version.lock().unwrap(); let v = *next; *next = v + 1; @@ -500,8 +502,7 @@ mod tests { fn register_simple_class_returns_store_version() { let c = Class::new("Account"); let store = MockKnowableFromStore::new(42); - let v = register_class_knowable_from(&c, "ogit-erp/Account", &store) - .expect("register OK"); + let v = register_class_knowable_from(&c, "ogit-erp/Account", &store).expect("register OK"); assert_eq!(v, 42); let calls = store.register_calls.lock().unwrap(); assert_eq!(calls.len(), 1); @@ -536,17 +537,19 @@ mod tests { // conflate them; the new prefix-aware identity keeps them // distinct. let store = MockKnowableFromStore::new(50); - let v_op = register_class_knowable_from( - &Class::new("WorkPackage"), - "ogit-op/WorkPackage", - &store, - ).unwrap(); + let v_op = + register_class_knowable_from(&Class::new("WorkPackage"), "ogit-op/WorkPackage", &store) + .unwrap(); let v_erp = register_class_knowable_from( &Class::new("WorkPackage"), "ogit-erp/WorkPackage", &store, - ).unwrap(); - assert_ne!(v_op, v_erp, "different prefixes must produce distinct versions"); + ) + .unwrap(); + assert_ne!( + v_op, v_erp, + "different prefixes must produce distinct versions" + ); assert_eq!(store.knowable_from("ogit-op/WorkPackage"), Some(v_op)); assert_eq!(store.knowable_from("ogit-erp/WorkPackage"), Some(v_erp)); } @@ -557,7 +560,10 @@ mod tests { let store = MockKnowableFromStore::new(0); match register_class_knowable_from(&c, "ogit-erp/X", &store) { Err(KnowableFromError::MalformedClass(msg)) => { - assert!(msg.contains("Class.name"), "expected Class.name message, got: {msg}"); + assert!( + msg.contains("Class.name"), + "expected Class.name message, got: {msg}" + ); } other => panic!("expected MalformedClass, got: {other:?}"), } @@ -571,8 +577,10 @@ mod tests { let store = MockKnowableFromStore::new(0); match register_class_knowable_from(&c, "", &store) { Err(KnowableFromError::MalformedClass(msg)) => { - assert!(msg.contains("class_identity"), - "expected class_identity message, got: {msg}"); + assert!( + msg.contains("class_identity"), + "expected class_identity message, got: {msg}" + ); } other => panic!("expected MalformedClass, got: {other:?}"), } @@ -583,11 +591,7 @@ mod tests { fn register_propagates_backend_errors() { struct FailingStore; impl KnowableFromStore for FailingStore { - fn register( - &self, - _: &str, - _: Option<&str>, - ) -> Result { + fn register(&self, _: &str, _: Option<&str>) -> Result { Err(KnowableFromError::Backend("disk full".into())) } fn knowable_from(&self, _: &str) -> Option { @@ -667,7 +671,10 @@ mod tests { let calls = store.register_calls.lock().unwrap(); assert_eq!(calls.len(), 1); - let hint = calls[0].1.as_deref().expect("feature-on path must populate the hint"); + let hint = calls[0] + .1 + .as_deref() + .expect("feature-on path must populate the hint"); // The DDL must mention the table and the field — the canonical // shape `emit_surrealql_ddl` produces. assert!( @@ -756,16 +763,15 @@ mod tests { // VART-backed end-to-end through `register_class_knowable_from`. use ogar_vocab::Class; let store = crate::vart_backend::VartKnowableFromStore::new(); - let v_op = register_class_knowable_from( - &Class::new("WorkPackage"), - "ogit-op/WorkPackage", - &store, - ).unwrap(); + let v_op = + register_class_knowable_from(&Class::new("WorkPackage"), "ogit-op/WorkPackage", &store) + .unwrap(); let v_erp = register_class_knowable_from( &Class::new("WorkPackage"), "ogit-erp/WorkPackage", &store, - ).unwrap(); + ) + .unwrap(); assert_ne!(v_op, v_erp); assert_eq!(store.knowable_from("ogit-op/WorkPackage"), Some(v_op)); assert_eq!(store.knowable_from("ogit-erp/WorkPackage"), Some(v_erp)); diff --git a/crates/ogar-ontology/src/lib.rs b/crates/ogar-ontology/src/lib.rs index 2745ed7..fd4ea09 100644 --- a/crates/ogar-ontology/src/lib.rs +++ b/crates/ogar-ontology/src/lib.rs @@ -30,13 +30,7 @@ /// should pick a deeper segment under an existing prefix /// (`ogar-extensions//...` or `ogit-/...`) rather than /// claiming a new top-level. -pub const RESERVED_PREFIXES: &[&str] = &[ - "ogar", - "ogar-extensions", - "ogit", - "ogit-erp", - "ogit-op", -]; +pub const RESERVED_PREFIXES: &[&str] = &["ogar", "ogar-extensions", "ogit", "ogit-erp", "ogit-op"]; /// The `ogar/` prefix for core vocabulary terms. pub const OGAR_PREFIX: &str = "ogar"; @@ -154,7 +148,10 @@ mod tests { #[test] fn identity_helpers() { - assert_eq!(class_identity("ogit-op", "WorkPackage"), "ogit-op/WorkPackage"); + assert_eq!( + class_identity("ogit-op", "WorkPackage"), + "ogit-op/WorkPackage" + ); assert_eq!( field_identity("ogit-op", "WorkPackage", "subject"), "ogit-op/WorkPackage.subject" @@ -175,7 +172,10 @@ mod tests { #[test] fn dotted_names_preserved() { // Odoo class names like `sale.order` stay verbatim. - assert_eq!(class_identity("ogit-erp", "sale.order"), "ogit-erp/sale.order"); + assert_eq!( + class_identity("ogit-erp", "sale.order"), + "ogit-erp/sale.order" + ); } #[test] diff --git a/crates/ogar-proposal/src/lib.rs b/crates/ogar-proposal/src/lib.rs index 3bdc0a4..f91e487 100644 --- a/crates/ogar-proposal/src/lib.rs +++ b/crates/ogar-proposal/src/lib.rs @@ -399,7 +399,10 @@ fn infer_semantic(attr: &Attribute) -> SemanticDraft { /// `Internal`. `company_dependent` does not itself imply a marking. fn infer_marking(attr: &Attribute, semantic: &SemanticDraft) -> MarkingDraft { match semantic { - SemanticDraft::Iban | SemanticDraft::Email | SemanticDraft::Phone | SemanticDraft::Address => { + SemanticDraft::Iban + | SemanticDraft::Email + | SemanticDraft::Phone + | SemanticDraft::Address => { return MarkingDraft::Pii; } SemanticDraft::TaxId => return MarkingDraft::Restricted, @@ -545,8 +548,14 @@ mod tests { #[test] fn class_yields_one_entity_plus_one_edge_per_assoc() { let drafts = class_to_drafts(&work_package(), "ogit-op"); - let entities = drafts.iter().filter(|d| matches!(d.kind, DraftKind::Entity { .. })).count(); - let edges = drafts.iter().filter(|d| matches!(d.kind, DraftKind::Edge { .. })).count(); + let entities = drafts + .iter() + .filter(|d| matches!(d.kind, DraftKind::Entity { .. })) + .count(); + let edges = drafts + .iter() + .filter(|d| matches!(d.kind, DraftKind::Edge { .. })) + .count(); assert_eq!(entities, 1); assert_eq!(edges, 2); } @@ -554,10 +563,13 @@ mod tests { #[test] fn entity_schema_name_is_class_identity() { let drafts = class_to_drafts(&work_package(), "ogit-op"); - let entity = drafts.iter().find_map(|d| match &d.kind { - DraftKind::Entity { schema } => Some(schema), - _ => None, - }).unwrap(); + let entity = drafts + .iter() + .find_map(|d| match &d.kind { + DraftKind::Entity { schema } => Some(schema), + _ => None, + }) + .unwrap(); assert_eq!(entity.name, "ogit-op/WorkPackage"); assert_eq!(entity.properties.len(), 3); } @@ -565,11 +577,18 @@ mod tests { #[test] fn required_attribute_maps_to_required_passthrough() { let drafts = class_to_drafts(&work_package(), "ogit-op"); - let entity = drafts.iter().find_map(|d| match &d.kind { - DraftKind::Entity { schema } => Some(schema), - _ => None, - }).unwrap(); - let subject = entity.properties.iter().find(|p| p.predicate == "subject").unwrap(); + let entity = drafts + .iter() + .find_map(|d| match &d.kind { + DraftKind::Entity { schema } => Some(schema), + _ => None, + }) + .unwrap(); + let subject = entity + .properties + .iter() + .find(|p| p.predicate == "subject") + .unwrap(); assert_eq!(subject.kind, PropertyKindDraft::Required); assert_eq!(subject.codec_route, CodecRouteDraft::Passthrough); } @@ -577,11 +596,18 @@ mod tests { #[test] fn email_field_infers_pii_marking() { let drafts = class_to_drafts(&work_package(), "ogit-op"); - let entity = drafts.iter().find_map(|d| match &d.kind { - DraftKind::Entity { schema } => Some(schema), - _ => None, - }).unwrap(); - let email = entity.properties.iter().find(|p| p.predicate == "author_email").unwrap(); + let entity = drafts + .iter() + .find_map(|d| match &d.kind { + DraftKind::Entity { schema } => Some(schema), + _ => None, + }) + .unwrap(); + let email = entity + .properties + .iter() + .find(|p| p.predicate == "author_email") + .unwrap(); assert_eq!(email.semantic, SemanticDraft::Email); assert_eq!(email.marking, MarkingDraft::Pii); } @@ -589,11 +615,18 @@ mod tests { #[test] fn monetary_field_infers_currency_and_financial() { let drafts = class_to_drafts(&work_package(), "ogit-op"); - let entity = drafts.iter().find_map(|d| match &d.kind { - DraftKind::Entity { schema } => Some(schema), - _ => None, - }).unwrap(); - let total = entity.properties.iter().find(|p| p.predicate == "amount_total").unwrap(); + let entity = drafts + .iter() + .find_map(|d| match &d.kind { + DraftKind::Entity { schema } => Some(schema), + _ => None, + }) + .unwrap(); + let total = entity + .properties + .iter() + .find(|p| p.predicate == "amount_total") + .unwrap(); assert_eq!(total.semantic, SemanticDraft::Currency("EUR".into())); assert_eq!(total.marking, MarkingDraft::Financial); } @@ -602,7 +635,10 @@ mod tests { fn heuristic_semantic_lowers_entity_confidence() { // work_package has email + monetary → heuristic semantics present. let drafts = class_to_drafts(&work_package(), "ogit-op"); - let entity = drafts.iter().find(|d| matches!(d.kind, DraftKind::Entity { .. })).unwrap(); + let entity = drafts + .iter() + .find(|d| matches!(d.kind, DraftKind::Entity { .. })) + .unwrap(); assert!(entity.confidence < 1.0); // A plain class with only PlainText attributes stays at 1.0. @@ -611,17 +647,23 @@ mod tests { n.type_name = Some("Char".into()); plain.attributes.push(n); let plain_drafts = class_to_drafts(&plain, "ogit-op"); - let plain_entity = plain_drafts.iter().find(|d| matches!(d.kind, DraftKind::Entity { .. })).unwrap(); + let plain_entity = plain_drafts + .iter() + .find(|d| matches!(d.kind, DraftKind::Entity { .. })) + .unwrap(); assert_eq!(plain_entity.confidence, 1.0); } #[test] fn belongs_to_maps_one_to_one_with_explicit_target() { let drafts = class_to_drafts(&work_package(), "ogit-op"); - let link = drafts.iter().find_map(|d| match &d.kind { - DraftKind::Edge { link } if link.predicate == "project" => Some(link), - _ => None, - }).unwrap(); + let link = drafts + .iter() + .find_map(|d| match &d.kind { + DraftKind::Edge { link } if link.predicate == "project" => Some(link), + _ => None, + }) + .unwrap(); assert_eq!(link.cardinality, CardinalityDraft::OneToOne); assert_eq!(link.subject_type, "ogit-op/WorkPackage"); assert_eq!(link.object_type, "ogit-op/Project"); @@ -630,10 +672,13 @@ mod tests { #[test] fn has_many_maps_one_to_many_with_classified_target() { let drafts = class_to_drafts(&work_package(), "ogit-op"); - let link = drafts.iter().find_map(|d| match &d.kind { - DraftKind::Edge { link } if link.predicate == "time_entries" => Some(link), - _ => None, - }).unwrap(); + let link = drafts + .iter() + .find_map(|d| match &d.kind { + DraftKind::Edge { link } if link.predicate == "time_entries" => Some(link), + _ => None, + }) + .unwrap(); assert_eq!(link.cardinality, CardinalityDraft::OneToMany); // No class_name override → naive classify (singularize 's' + // capitalize). "time_entries" → "Time_entrie" — deliberately @@ -650,10 +695,13 @@ mod tests { a.polymorphic = Some(true); c.associations.push(a); let drafts = class_to_drafts(&c, "ogit-op"); - let link = drafts.iter().find_map(|d| match &d.kind { - DraftKind::Edge { link } => Some(link), - _ => None, - }).unwrap(); + let link = drafts + .iter() + .find_map(|d| match &d.kind { + DraftKind::Edge { link } => Some(link), + _ => None, + }) + .unwrap(); assert_eq!(link.object_type, ""); } @@ -676,6 +724,10 @@ mod tests { #[test] fn created_by_carries_language_tag() { let drafts = class_to_drafts(&work_package(), "ogit-op"); - assert!(drafts.iter().all(|d| d.created_by == "ogar_producer_ruby_v1")); + assert!( + drafts + .iter() + .all(|d| d.created_by == "ogar_producer_ruby_v1") + ); } } diff --git a/crates/ogar-render-askama/examples/render_osm.rs b/crates/ogar-render-askama/examples/render_osm.rs index d6662e9..ac9bcb0 100644 --- a/crates/ogar-render-askama/examples/render_osm.rs +++ b/crates/ogar-render-askama/examples/render_osm.rs @@ -36,8 +36,9 @@ fn ground(rails: &str) -> Option<&'static str> { "Way" | "OldWay" => "osm_way", "Relation" | "OldRelation" => "osm_relation", "Changeset" => "osm_changeset", - "NodeTag" | "WayTag" | "RelationTag" | "OldNodeTag" | "OldWayTag" - | "OldRelationTag" => "osm_element_tag", + "NodeTag" | "WayTag" | "RelationTag" | "OldNodeTag" | "OldWayTag" | "OldRelationTag" => { + "osm_element_tag" + } "RelationMember" | "OldRelationMember" => "osm_relation_member", "WayNode" | "OldWayNode" => "osm_way_node", "Note" => "osm_note", @@ -88,11 +89,7 @@ fn snake(name: &str) -> String { } } let s = out.trim_matches('_').to_string(); - if s.is_empty() { - "anon".into() - } else { - s - } + if s.is_empty() { "anon".into() } else { s } } fn main() { @@ -201,7 +198,10 @@ fn main() { let rail_root: &Path = if src_path.join("app/controllers").is_dir() { src_path } else if src_path.ends_with("app/models") { - src_path.parent().and_then(|p| p.parent()).unwrap_or(src_path) + src_path + .parent() + .and_then(|p| p.parent()) + .unwrap_or(src_path) } else { src_path }; diff --git a/crates/ogar-render-askama/examples/render_python.rs b/crates/ogar-render-askama/examples/render_python.rs index 6f2b24d..2e24e82 100644 --- a/crates/ogar-render-askama/examples/render_python.rs +++ b/crates/ogar-render-askama/examples/render_python.rs @@ -16,7 +16,7 @@ //! as a dict + `class_id(concept)` / `render_classid(concept, app_prefix)`, the //! Python mirror of the Rust `lance_graph_contract::ogar_codebook` contract. The //! same SDK pulls odoo (`0x02`) concepts — the shape is domain-agnostic. -use ogar_vocab::{canonical_concept_id, AssociationKind}; +use ogar_vocab::{AssociationKind, canonical_concept_id}; use std::collections::BTreeMap; use std::fs; use std::path::Path; @@ -29,8 +29,9 @@ fn ground(rails: &str) -> Option<&'static str> { "Way" | "OldWay" => "osm_way", "Relation" | "OldRelation" => "osm_relation", "Changeset" => "osm_changeset", - "NodeTag" | "WayTag" | "RelationTag" | "OldNodeTag" | "OldWayTag" - | "OldRelationTag" => "osm_element_tag", + "NodeTag" | "WayTag" | "RelationTag" | "OldNodeTag" | "OldWayTag" | "OldRelationTag" => { + "osm_element_tag" + } "RelationMember" | "OldRelationMember" => "osm_relation_member", "WayNode" | "OldWayNode" => "osm_way_node", "Note" => "osm_note", @@ -59,11 +60,7 @@ fn snake(name: &str) -> String { } } let s = out.trim_matches('_').to_string(); - if s.is_empty() { - "anon".into() - } else { - s - } + if s.is_empty() { "anon".into() } else { s } } /// A safe Python identifier: snake + escape the handful of keywords a container @@ -103,9 +100,9 @@ fn main() { let src = args .next() .unwrap_or_else(|| "/home/user/_src-osm-website".into()); - let out_arg = args.next().unwrap_or_else(|| { - "/home/user/OGAR/.claude/harvest/osm-website-rs/python".into() - }); + let out_arg = args + .next() + .unwrap_or_else(|| "/home/user/OGAR/.claude/harvest/osm-website-rs/python".into()); let out = Path::new(&out_arg); let pkg = out.join("osm"); fs::create_dir_all(&pkg).unwrap(); @@ -181,7 +178,10 @@ fn main() { let rail_root: &Path = if src_path.join("app/controllers").is_dir() { src_path } else if src_path.ends_with("app/models") { - src_path.parent().and_then(|p| p.parent()).unwrap_or(src_path) + src_path + .parent() + .and_then(|p| p.parent()) + .unwrap_or(src_path) } else { src_path }; @@ -215,7 +215,9 @@ fn main() { for (is_a, sources) in isas { let entry = by_fname.entry(pyify(is_a)).or_default(); entry.0.push(is_a.clone()); - entry.1.extend(sources.iter().map(|(c, a)| format!("{c}#{a}"))); + entry + .1 + .extend(sources.iter().map(|(c, a)| format!("{c}#{a}"))); } for (fname, (mut labels, mut srcs)) in by_fname { labels.sort(); diff --git a/crates/ogar-render-askama/src/artifact_kinds/cells.rs b/crates/ogar-render-askama/src/artifact_kinds/cells.rs index 147b0b1..986082b 100644 --- a/crates/ogar-render-askama/src/artifact_kinds/cells.rs +++ b/crates/ogar-render-askama/src/artifact_kinds/cells.rs @@ -171,7 +171,10 @@ pub(crate) fn render_cell_body(data: &CellData<'_>) -> Result { let mapped: Vec> = users diff --git a/crates/ogar-render-askama/src/artifact_kinds/html_detail_view.rs b/crates/ogar-render-askama/src/artifact_kinds/html_detail_view.rs index 081eb84..a38cb0a 100644 --- a/crates/ogar-render-askama/src/artifact_kinds/html_detail_view.rs +++ b/crates/ogar-render-askama/src/artifact_kinds/html_detail_view.rs @@ -13,8 +13,8 @@ use askama::Template; -use super::html_list_view::{CellData, CellSource}; use super::ArtifactEmitter; +use super::html_list_view::{CellData, CellSource}; use crate::list_view::{ColumnKind, RenderColumn}; use crate::spec::ArtifactSpec; use ogar_vocab::canonical_concept_id; diff --git a/crates/ogar-render-askama/src/artifact_kinds/html_form.rs b/crates/ogar-render-askama/src/artifact_kinds/html_form.rs index e745057..0bbb08d 100644 --- a/crates/ogar-render-askama/src/artifact_kinds/html_form.rs +++ b/crates/ogar-render-askama/src/artifact_kinds/html_form.rs @@ -12,9 +12,9 @@ use askama::Template; -use super::inputs::{render_input_body, InputData}; use super::ArtifactEmitter; -use crate::form_view::{default_input_kind_for, InputKind}; +use super::inputs::{InputData, render_input_body}; +use crate::form_view::{InputKind, default_input_kind_for}; use crate::list_view::RenderColumn; use crate::spec::ArtifactSpec; use ogar_vocab::canonical_concept_id; @@ -180,7 +180,7 @@ impl ArtifactEmitter for HtmlFormEmitter { .attributes .iter() .map(|attr| { - use crate::list_view::{default_kind_for, ColumnKind}; + use crate::list_view::{ColumnKind, default_kind_for}; let kind = default_kind_for(&attr.name, attr.type_name.as_deref()); let col = RenderColumn::new(&attr.name, &attr.name, kind); if matches!(kind, ColumnKind::RichText) { diff --git a/crates/ogar-render-askama/src/artifact_kinds/html_list_view.rs b/crates/ogar-render-askama/src/artifact_kinds/html_list_view.rs index 1b39c10..5ea2a15 100644 --- a/crates/ogar-render-askama/src/artifact_kinds/html_list_view.rs +++ b/crates/ogar-render-askama/src/artifact_kinds/html_list_view.rs @@ -110,7 +110,9 @@ pub enum CellData<'a> { /// Formatted duration (hours / decimal). Hours { hours: &'a str, href: &'a str }, /// List of `project_attachment` refs. - AttachmentList { attachments: Vec }, + AttachmentList { + attachments: Vec, + }, /// List of `project_actor` refs (watchers, assignees, …). UserList { users: Vec }, } diff --git a/crates/ogar-render-askama/src/artifact_kinds/mod.rs b/crates/ogar-render-askama/src/artifact_kinds/mod.rs index 1bf5004..ccffa40 100644 --- a/crates/ogar-render-askama/src/artifact_kinds/mod.rs +++ b/crates/ogar-render-askama/src/artifact_kinds/mod.rs @@ -41,11 +41,11 @@ pub mod rust_struct; pub mod stub; pub mod surrealql_table; -pub use html_detail_view::{render_detail, HtmlDetailViewEmitter}; -pub use html_form::{render_form, FormFieldSource, FormSource, HtmlFormEmitter}; +pub use html_detail_view::{HtmlDetailViewEmitter, render_detail}; +pub use html_form::{FormFieldSource, FormSource, HtmlFormEmitter, render_form}; pub use html_list_view::{ - render_list, AttachmentEntryOwned, CellData, CellSource, GroupHeader, HtmlListViewEmitter, - RelationEntryOwned, RowSource, UserEntryOwned, + AttachmentEntryOwned, CellData, CellSource, GroupHeader, HtmlListViewEmitter, + RelationEntryOwned, RowSource, UserEntryOwned, render_list, }; pub use inputs::{InputData, SelectOptionOwned}; pub use surrealql_table::SurrealqlTableEmitter; diff --git a/crates/ogar-render-askama/src/artifact_kinds/rust_struct.rs b/crates/ogar-render-askama/src/artifact_kinds/rust_struct.rs index f42b183..d82278a 100644 --- a/crates/ogar-render-askama/src/artifact_kinds/rust_struct.rs +++ b/crates/ogar-render-askama/src/artifact_kinds/rust_struct.rs @@ -12,7 +12,7 @@ use askama::Template; use super::ArtifactEmitter; use crate::spec::ArtifactSpec; -use ogar_vocab::{canonical_concept_id, AssociationKind}; +use ogar_vocab::{AssociationKind, canonical_concept_id}; /// askama-bound context for `templates/dispatch/rust_struct.askama`. /// @@ -119,15 +119,14 @@ impl ArtifactEmitter for RustStructEmitter { pub(crate) fn escape_rust_ident(name: &str) -> String { const RESERVED: &[&str] = &[ // Rust 2015+ strict keywords: - "as", "break", "const", "continue", "crate", "else", "enum", "extern", - "false", "fn", "for", "if", "impl", "in", "let", "loop", "match", "mod", - "move", "mut", "pub", "ref", "return", "self", "Self", "static", "struct", - "super", "trait", "true", "type", "unsafe", "use", "where", "while", - // Rust 2018+ strict keywords: + "as", "break", "const", "continue", "crate", "else", "enum", "extern", "false", "fn", "for", + "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub", "ref", "return", + "self", "Self", "static", "struct", "super", "trait", "true", "type", "unsafe", "use", + "where", "while", // Rust 2018+ strict keywords: "async", "await", "dyn", // Reserved-future (lexable but unusable as raw idents anyway): - "abstract", "become", "box", "do", "final", "macro", "override", "priv", - "typeof", "unsized", "virtual", "yield", "try", + "abstract", "become", "box", "do", "final", "macro", "override", "priv", "typeof", + "unsized", "virtual", "yield", "try", ]; if RESERVED.contains(&name) { format!("r#{name}") diff --git a/crates/ogar-render-askama/src/artifact_kinds/surrealql_table.rs b/crates/ogar-render-askama/src/artifact_kinds/surrealql_table.rs index edabe7f..a878829 100644 --- a/crates/ogar-render-askama/src/artifact_kinds/surrealql_table.rs +++ b/crates/ogar-render-askama/src/artifact_kinds/surrealql_table.rs @@ -16,7 +16,7 @@ use askama::Template; use super::ArtifactEmitter; use crate::spec::ArtifactSpec; -use ogar_vocab::{canonical_concept_id, AssociationKind, Class}; +use ogar_vocab::{AssociationKind, Class, canonical_concept_id}; #[derive(Template)] #[template(path = "dispatch/surrealql_table.askama", escape = "none")] @@ -119,10 +119,37 @@ fn table_name_from(class: &Class, concept: &str) -> String { /// applying it only when needed keeps emission readable. fn escape_surql_ident(name: &str) -> String { const RESERVED: &[&str] = &[ - "select", "from", "where", "create", "update", "delete", "let", - "if", "then", "else", "end", "in", "and", "or", "not", "true", - "false", "null", "none", "return", "begin", "commit", "transaction", - "define", "table", "field", "index", "type", "value", "for", "id", + "select", + "from", + "where", + "create", + "update", + "delete", + "let", + "if", + "then", + "else", + "end", + "in", + "and", + "or", + "not", + "true", + "false", + "null", + "none", + "return", + "begin", + "commit", + "transaction", + "define", + "table", + "field", + "index", + "type", + "value", + "for", + "id", ]; if RESERVED.contains(&name) { format!("`{name}`") diff --git a/crates/ogar-render-askama/src/form_view.rs b/crates/ogar-render-askama/src/form_view.rs index 20f6805..b941c19 100644 --- a/crates/ogar-render-askama/src/form_view.rs +++ b/crates/ogar-render-askama/src/form_view.rs @@ -155,29 +155,58 @@ mod tests { #[test] fn default_input_kind_hides_ids() { assert_eq!(default_input_kind_for("id", None), InputKind::Hidden); - assert_eq!(default_input_kind_for("project_id", None), InputKind::Hidden); - assert_eq!(default_input_kind_for("author_id", Some("integer")), InputKind::Hidden); + assert_eq!( + default_input_kind_for("project_id", None), + InputKind::Hidden + ); + assert_eq!( + default_input_kind_for("author_id", Some("integer")), + InputKind::Hidden + ); } #[test] fn default_input_kind_picks_range_for_ratios() { assert_eq!(default_input_kind_for("done_ratio", None), InputKind::Range); - assert_eq!(default_input_kind_for("complete_pct", None), InputKind::Range); + assert_eq!( + default_input_kind_for("complete_pct", None), + InputKind::Range + ); } #[test] fn default_input_kind_picks_textarea_for_prose() { // Prose-named text fields get a textarea; non-prose text fields // also get a textarea (text is long enough to warrant it). - assert_eq!(default_input_kind_for("description", Some("text")), InputKind::TextArea); - assert_eq!(default_input_kind_for("body", Some("text")), InputKind::TextArea); - assert_eq!(default_input_kind_for("notes", Some("text")), InputKind::TextArea); - assert_eq!(default_input_kind_for("status_label", Some("text")), InputKind::TextArea); + assert_eq!( + default_input_kind_for("description", Some("text")), + InputKind::TextArea + ); + assert_eq!( + default_input_kind_for("body", Some("text")), + InputKind::TextArea + ); + assert_eq!( + default_input_kind_for("notes", Some("text")), + InputKind::TextArea + ); + assert_eq!( + default_input_kind_for("status_label", Some("text")), + InputKind::TextArea + ); } #[test] fn default_input_kind_picks_numeric_for_numbers() { - for t in ["integer", "big_integer", "bigint", "float", "double", "decimal", "monetary"] { + for t in [ + "integer", + "big_integer", + "bigint", + "float", + "double", + "decimal", + "monetary", + ] { assert_eq!( default_input_kind_for("position", Some(t)), InputKind::Number, @@ -188,20 +217,41 @@ mod tests { #[test] fn default_input_kind_picks_checkbox_for_boolean() { - assert_eq!(default_input_kind_for("active", Some("boolean")), InputKind::Checkbox); - assert_eq!(default_input_kind_for("locked", Some("bool")), InputKind::Checkbox); + assert_eq!( + default_input_kind_for("active", Some("boolean")), + InputKind::Checkbox + ); + assert_eq!( + default_input_kind_for("locked", Some("bool")), + InputKind::Checkbox + ); } #[test] fn default_input_kind_picks_date_types_for_temporal() { - assert_eq!(default_input_kind_for("start_date", Some("date")), InputKind::Date); - assert_eq!(default_input_kind_for("created_at", Some("datetime")), InputKind::DateTime); - assert_eq!(default_input_kind_for("updated_at", Some("timestamp")), InputKind::DateTime); + assert_eq!( + default_input_kind_for("start_date", Some("date")), + InputKind::Date + ); + assert_eq!( + default_input_kind_for("created_at", Some("datetime")), + InputKind::DateTime + ); + assert_eq!( + default_input_kind_for("updated_at", Some("timestamp")), + InputKind::DateTime + ); } #[test] fn default_input_kind_falls_back_to_text() { - assert_eq!(default_input_kind_for("subject", Some("string")), InputKind::Text); - assert_eq!(default_input_kind_for("unknown_field", None), InputKind::Text); + assert_eq!( + default_input_kind_for("subject", Some("string")), + InputKind::Text + ); + assert_eq!( + default_input_kind_for("unknown_field", None), + InputKind::Text + ); } } diff --git a/crates/ogar-render-askama/src/lib.rs b/crates/ogar-render-askama/src/lib.rs index 2a571bd..1bf2868 100644 --- a/crates/ogar-render-askama/src/lib.rs +++ b/crates/ogar-render-askama/src/lib.rs @@ -75,13 +75,13 @@ pub mod rust_class; pub mod spec; pub use artifact_kinds::{ - for_kind, render_detail, render_form, render_list, ArtifactEmitter, AttachmentEntryOwned, - CellData, CellSource, FormFieldSource, FormSource, GroupHeader, InputData, - RelationEntryOwned, RowSource, SelectOptionOwned, UserEntryOwned, + ArtifactEmitter, AttachmentEntryOwned, CellData, CellSource, FormFieldSource, FormSource, + GroupHeader, InputData, RelationEntryOwned, RowSource, SelectOptionOwned, UserEntryOwned, + for_kind, render_detail, render_form, render_list, }; -pub use form_view::{default_input_kind_for, InputKind}; -pub use list_view::{default_kind_for, ColumnKind, RenderColumn, SortOrder}; -pub use rust_class::{render_class_with_methods, render_class_with_methods_wide, RenderError}; +pub use form_view::{InputKind, default_input_kind_for}; +pub use list_view::{ColumnKind, RenderColumn, SortOrder, default_kind_for}; +pub use rust_class::{RenderError, render_class_with_methods, render_class_with_methods_wide}; pub use spec::{ArtifactKind, ArtifactSpec}; use ogar_vocab::Class; @@ -228,10 +228,7 @@ mod tests { src.contains("data-class-id=\"0x0102\""), "expected data-class-id=\"0x0102\" in:\n{src}" ); - assert!( - src.contains("data-concept=\"project_work_item\""), - "{src}" - ); + assert!(src.contains("data-concept=\"project_work_item\""), "{src}"); // Empty-state row appears when no rows are supplied. assert!(src.contains("No data."), "expected empty-state in:\n{src}"); } @@ -241,13 +238,14 @@ mod tests { // The substantive path: build columns + rows and assert the // spine template substitutes them correctly. let inline = vec![ - RenderColumn::new("id", "#", ColumnKind::IdLink).sortable().frozen(), + RenderColumn::new("id", "#", ColumnKind::IdLink) + .sortable() + .frozen(), RenderColumn::new("subject", "Subject", ColumnKind::PrimaryLink).sortable(), RenderColumn::new("done_ratio", "% Done", ColumnKind::ProgressBar), ]; - let block = vec![ - RenderColumn::new("description", "Description", ColumnKind::RichText).block(), - ]; + let block = + vec![RenderColumn::new("description", "Description", ColumnKind::RichText).block()]; let row = RowSource { record_id: 42, @@ -257,7 +255,10 @@ mod tests { CellSource { column: &inline[0], css_classes: "num", - data: CellData::IdLink { id: 42, href: "/issues/42" }, + data: CellData::IdLink { + id: 42, + href: "/issues/42", + }, }, CellSource { column: &inline[1], @@ -295,10 +296,16 @@ mod tests { // Header + columns assert!(src.contains("

Work items

"), "{src}"); assert!(src.contains("data-class-id=\"0x0102\"")); - assert!(src.contains("Subject"), "expected `Subject` column header in:\n{src}"); + assert!( + src.contains("Subject"), + "expected `Subject` column header in:\n{src}" + ); assert!(src.contains("% Done")); // Inline row + cells - assert!(src.contains("id=\"record-42\""), "expected id=\"record-42\":\n{src}"); + assert!( + src.contains("id=\"record-42\""), + "expected id=\"record-42\":\n{src}" + ); assert!(src.contains("href=\"/issues/42\""), "{src}"); assert!(src.contains("#42"), "id link body should be `#42`:\n{src}"); assert!(src.contains("Fix the foo"), "{src}"); @@ -323,11 +330,17 @@ mod tests { let row = RowSource { record_id: 1, css_classes: "", - group: Some(GroupHeader { label: "Open", count: 5 }), + group: Some(GroupHeader { + label: "Open", + count: 5, + }), inline: vec![CellSource { column: &col, css_classes: "", - data: CellData::PrimaryLink { label: "T1", href: "/i/1" }, + data: CellData::PrimaryLink { + label: "T1", + href: "/i/1", + }, }], block: vec![], }; @@ -611,14 +624,23 @@ mod tests { // Smoke: the resolver lib.rs re-exports is the one consumers // call to pick cell kinds. Pin the contract. assert_eq!(default_kind_for("id", None), ColumnKind::IdLink); - assert_eq!(default_kind_for("subject", Some("string")), ColumnKind::PrimaryLink); - assert_eq!(default_kind_for("done_ratio", None), ColumnKind::ProgressBar); + assert_eq!( + default_kind_for("subject", Some("string")), + ColumnKind::PrimaryLink + ); + assert_eq!( + default_kind_for("done_ratio", None), + ColumnKind::ProgressBar + ); assert_eq!(default_kind_for("estimated_hours", None), ColumnKind::Hours); assert_eq!( default_kind_for("description", Some("text")), ColumnKind::RichText ); - assert_eq!(default_kind_for("position", Some("integer")), ColumnKind::Plain); + assert_eq!( + default_kind_for("position", Some("integer")), + ColumnKind::Plain + ); } // ── T4 (HtmlForm) tests ───────────────────────────────────────── @@ -740,9 +762,18 @@ mod tests { value: "2".to_string(), required: false, options: vec![ - SelectOptionOwned { value: "1".to_string(), label: "New".to_string() }, - SelectOptionOwned { value: "2".to_string(), label: "In Progress".to_string() }, - SelectOptionOwned { value: "3".to_string(), label: "Closed".to_string() }, + SelectOptionOwned { + value: "1".to_string(), + label: "New".to_string(), + }, + SelectOptionOwned { + value: "2".to_string(), + label: "In Progress".to_string(), + }, + SelectOptionOwned { + value: "3".to_string(), + label: "Closed".to_string(), + }, ], }, }, @@ -776,31 +807,67 @@ mod tests { // Header attrs assert!(src.contains("method=\"patch\""), "{src}"); assert!(src.contains("action=\"/issues/42\""), "{src}"); - assert!(src.contains("name=\"authenticity_token\" value=\"xyz\""), "{src}"); + assert!( + src.contains("name=\"authenticity_token\" value=\"xyz\""), + "{src}" + ); // record_id hidden field for the edit form - assert!(src.contains("name=\"id\" value=\"42\""), "expected record-id hidden:\n{src}"); + assert!( + src.contains("name=\"id\" value=\"42\""), + "expected record-id hidden:\n{src}" + ); assert!(src.contains("Edit issue"), "{src}"); // Each input kind fires its own sub-template: - assert!(src.contains("In Progress"), "{src}"); + assert!( + src.contains(""), + "{src}" + ); // hidden field has NO label wrapper; emitted bare - assert!(src.contains(""), "css raw: {src}"); assert!(!src.contains(""), "value raw: {src}"); assert!(!src.contains(""), "placeholder raw: {src}"); - assert!(!src.contains("