Skip to content

Commit 7783c9d

Browse files
committed
feat(ogar-from-ruff): compile_graph_ruby — the Rails CompiledClass assembler
Adds the keystone gap named in E-KEEP-AR-REMOVE-ORM / the OP convergence assessment: a Rails-correct sibling of compile_graph_python. Identical shape (mint_graph::<P> + per-class facet resolution) but routes through the existing lift_model_graph (Language::Ruby) instead of lift_model_graph_python — pure operator-reuse, no new lift, and project_odoo_fields is correctly never invoked for Rails (it would double-count; lift_model_graph_python's own doc-comment says so). Proves the convergence claim in code: compile_graph_ruby::<OpenProjectPort> on a WorkPackage graph and compile_graph_ruby::<RedminePort> on an Issue graph mint to the SAME low-u16 concept (0x0102 project_work_item) and DIFFERENT high-u16 render prefixes (0x0001 vs 0x0007) — one canonical concept, two render skins, machine-checked rather than asserted. Drive-by fix: 3 pre-existing Function{...} literal constructions (emit.rs, mint.rs's account_move fixture, lib.rs) broke against the already-merged ruff#38 (writes/calls fields) because this crate's ruff_spo_triplet dep floats on branch=main. Added ..Default::default() to each — no behavior change, restores compilation. Verification: standalone probe workspace (path-dep ogar-vocab + ogar-from-ruff, git-dep ruff branch=main) — the OGAR workspace itself can't resolve in-sandbox (ogar-adapter-surrealql's surrealdb-ast git dep 403s), the same pattern prior PRs (#131/#132/#136/#138/#141) used. 44/44 tests pass (3 new + 41 pre-existing unbroken); clippy --no-deps -D warnings clean at the pinned 1.95.0 toolchain (ogar-vocab itself has pre-existing unrelated clippy debt from never being --workspace-gated, out of scope here); doc-links resolve.
1 parent 76dbc8b commit 7783c9d

3 files changed

Lines changed: 121 additions & 3 deletions

File tree

crates/ogar-from-ruff/src/emit.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,7 @@ mod tests {
383383
reads: Vec::new(),
384384
raises: Vec::new(),
385385
traverses: Vec::new(),
386+
..Default::default()
386387
});
387388
let mut g = ModelGraph::new("odoo");
388389
g.models.push(m);

crates/ogar-from-ruff/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1334,12 +1334,14 @@ mod tests {
13341334
reads: vec!["status".to_string()],
13351335
raises: Vec::new(),
13361336
traverses: Vec::new(),
1337+
..Default::default()
13371338
});
13381339
m.functions.push(Function {
13391340
name: "close!".to_string(),
13401341
reads: Vec::new(),
13411342
raises: vec!["ArgumentError".to_string()],
13421343
traverses: Vec::new(),
1344+
..Default::default()
13431345
});
13441346
m
13451347
}

crates/ogar-from-ruff/src/mint.rs

Lines changed: 118 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ use ogar_vocab::Class;
3434
use ruff_spo_address::{mint_with_classid, Facet, Mint};
3535
use ruff_spo_triplet::{expand, ModelGraph};
3636

37-
use crate::lift_model_graph_python;
37+
use crate::{lift_model_graph, lift_model_graph_python};
3838

3939
/// A class compiled to its rail-shaped, language-agnostic form: the lifted
4040
/// schema ([`Class`]) plus its 16-byte address ([`Facet`]). This is what a
@@ -83,6 +83,32 @@ pub fn compile_graph_python<P: PortSpec>(graph: &ModelGraph) -> Vec<CompiledClas
8383
.collect()
8484
}
8585

86+
/// Compile a Ruby/Rails [`ModelGraph`] into rail-shaped [`CompiledClass`]es:
87+
/// lift each model's schema ([`Language::Ruby`](ogar_vocab::Language), via
88+
/// [`lift_model_graph`] — **not** [`lift_model_graph_python`], which would
89+
/// mis-stamp the producer language) and pair it with its minted facet,
90+
/// classid via port `P`. Declaration order is preserved (mirrors
91+
/// [`lift_model_graph`]). The Rails counterpart of
92+
/// [`compile_graph_python`] — same mint, same assembly shape, the only
93+
/// difference is which lift stamps the [`Class::language`](ogar_vocab::Class)
94+
/// field. E.g. `openproject:WorkPackage` → `0x0001_0102`,
95+
/// `redmine:Issue` → `0x0007_0102` (same shared concept, different render
96+
/// prefix — the two-render-skins-one-concept convergence).
97+
#[must_use]
98+
pub fn compile_graph_ruby<P: PortSpec>(graph: &ModelGraph) -> Vec<CompiledClass> {
99+
let mint = mint_graph::<P>(graph);
100+
lift_model_graph(graph)
101+
.into_iter()
102+
.map(|class| {
103+
let node = format!("{}:{}", graph.namespace, class.name);
104+
let facet = mint
105+
.facet(&node)
106+
.unwrap_or_else(|| Facet::from_parts(classid_for_node::<P>(&node), [0; 6], [0; 6]));
107+
CompiledClass { class, facet }
108+
})
109+
.collect()
110+
}
111+
86112
/// Resolve a node IRI's full render classid via port `P`.
87113
///
88114
/// The IRI is `<ns>:<model>` or `<ns>:<model>.<member>`; members inherit
@@ -110,8 +136,9 @@ fn model_of(node: &str) -> &str {
110136
#[cfg(test)]
111137
mod tests {
112138
use super::*;
113-
use ogar_vocab::ports::{OdooPort, OpenProjectPort};
114-
use ruff_spo_triplet::{Field, Function, Model};
139+
use ogar_vocab::ports::{OdooPort, OpenProjectPort, RedminePort};
140+
use ogar_vocab::Language;
141+
use ruff_spo_triplet::{AssocDecl, AssocKind, Field, Function, Model};
115142

116143
// A representative `account.move` `ModelGraph`, constructed directly (the
117144
// source→ModelGraph parse is `ruff_python_spo`'s job, tested there). Carries
@@ -146,6 +173,7 @@ mod tests {
146173
reads: Vec::new(),
147174
raises: Vec::new(),
148175
traverses: Vec::new(),
176+
..Default::default()
149177
});
150178
let mut g = ModelGraph::new("odoo");
151179
g.models.push(m);
@@ -223,4 +251,91 @@ mod tests {
223251
let facet = mint.facet("odoo:ir_cron").expect("node mints");
224252
assert_eq!(facet.facet_classid(), 0, "unmapped -> bootstrap address");
225253
}
254+
255+
// ───── compile_graph_ruby (Rails: the OP/Redmine convergence path) ─────
256+
257+
/// A representative `WorkPackage` `ModelGraph`, constructed directly (the
258+
/// source→ModelGraph parse is `ruff_ruby_spo`'s job, tested there).
259+
/// Carries an association so the schema-arm projects something besides
260+
/// a bare name.
261+
fn work_package_graph(namespace: &str, model_name: &str) -> ModelGraph {
262+
let mut m = Model::new(model_name);
263+
m.associations.push(AssocDecl {
264+
kind: AssocKind::BelongsTo,
265+
name: "project".to_string(),
266+
options: Vec::new(),
267+
});
268+
m.functions.push(Function {
269+
name: "update_status".to_string(),
270+
..Default::default()
271+
});
272+
let mut g = ModelGraph::new(namespace);
273+
g.models.push(m);
274+
g
275+
}
276+
277+
#[test]
278+
fn compile_graph_ruby_stamps_language_ruby_not_python() {
279+
// The bug compile_graph_python's own doc-comment warns against:
280+
// calling the Python lift on a Rails graph mis-stamps the producer
281+
// language. compile_graph_ruby must route through `lift_model_graph`
282+
// (Language::Ruby), never `lift_model_graph_python`.
283+
let graph = work_package_graph("openproject", "WorkPackage");
284+
let compiled = compile_graph_ruby::<OpenProjectPort>(&graph);
285+
assert_eq!(compiled.len(), 1);
286+
assert_eq!(compiled[0].class.language, Language::Ruby);
287+
}
288+
289+
#[test]
290+
fn openproject_work_package_compiles_to_project_work_item_rail_class() {
291+
let graph = work_package_graph("openproject", "WorkPackage");
292+
let compiled = compile_graph_ruby::<OpenProjectPort>(&graph);
293+
assert_eq!(compiled.len(), 1);
294+
let cc = &compiled[0];
295+
296+
assert_eq!(cc.class.name, "WorkPackage");
297+
assert!(
298+
!cc.class.associations.is_empty(),
299+
"belongs_to :project projects into an association"
300+
);
301+
302+
// OpenProject prefix 0x0001 | project_work_item concept 0x0102.
303+
assert_eq!(cc.facet.facet_classid(), 0x0001_0102);
304+
assert_eq!(
305+
cc.facet.facet_classid() & 0xFFFF,
306+
0x0102,
307+
"shared concept"
308+
);
309+
assert_eq!(
310+
(cc.facet.facet_classid() >> 16) as u16,
311+
OpenProjectPort::APP_PREFIX,
312+
"OpenProject render prefix",
313+
);
314+
}
315+
316+
#[test]
317+
fn openproject_and_redmine_compile_to_the_same_concept_different_render_skin() {
318+
// The literal "one canonical concept, two render skins" proof: the
319+
// SAME Rails-shaped ModelGraph (WorkPackage / Issue) minted through
320+
// each fork's port converges on the identical low-u16 concept and
321+
// diverges only on the high-u16 app-render prefix.
322+
let op_graph = work_package_graph("openproject", "WorkPackage");
323+
let rm_graph = work_package_graph("redmine", "Issue");
324+
325+
let op_compiled = compile_graph_ruby::<OpenProjectPort>(&op_graph);
326+
let rm_compiled = compile_graph_ruby::<RedminePort>(&rm_graph);
327+
328+
let op_id = op_compiled[0].facet.facet_classid();
329+
let rm_id = rm_compiled[0].facet.facet_classid();
330+
331+
assert_eq!(op_id & 0xFFFF, rm_id & 0xFFFF, "shared concept converges");
332+
assert_eq!(op_id & 0xFFFF, 0x0102, "project_work_item");
333+
assert_ne!(
334+
(op_id >> 16) as u16,
335+
(rm_id >> 16) as u16,
336+
"render prefixes diverge (OpenProject vs Redmine skin)"
337+
);
338+
assert_eq!((op_id >> 16) as u16, OpenProjectPort::APP_PREFIX);
339+
assert_eq!((rm_id >> 16) as u16, RedminePort::APP_PREFIX);
340+
}
226341
}

0 commit comments

Comments
 (0)