Skip to content

Commit 0ef3925

Browse files
committed
feat(ogar-from-ruff): per-class minting — OGAR transpile substrate addressing
Per the operator's framing: OGAR is the transpile substrate that holds the ~85% mechanical business logic as per-class minted, rail-shaped, language-agnostic compiled classes; consumers (odoo-rs, medcare-rs, …) collapse to thin compiler-store callers + a few adapters for the "impossible" 15% (intrusive/stateful logic → custom adapter + ClassView + ontological grounding). Each language adds only a thin wrapper contract (lance-graph-contract is Rust's). This adds the substrate's addressing step — the other half of the lift: - `mint_graph<P: PortSpec>(&ModelGraph) -> Mint` — mints the 16-byte rail facet for every node, resolving each render classid through a language/app port (OdooPort / OpenProjectPort / …) via the canonical `ogar_vocab::app::render_classid_for`. Frontend-agnostic: every ogar-from-<lang> yields the same ModelGraph. - `CompiledClass { class, facet }` + `compile_graph_python<P>` — pairs the lifted schema with its rail address: the unit a consumer pulls back. - classid = (APP_PREFIX << 16) | concept. The low u16 is the shared concept (PortSpec::class_id → OGAR codebook); the high u16 is the app render skin. account.move → 0x0002_0202 (Odoo | commercial_document); OpenProject WorkPackage → 0x0001_0102 (OpenProject | project_work_item) — different skins, the same identity where they converge. Unmapped model → classid 0 (bootstrap address), never mis-stamped. Consumes ruff's mint (ruff_spo_address, on ruff main). Members inherit their class's render classid. Tests: account.move compiles to a commercial_document rail class (schema + facet 0x0002_0202); member facets inherit the class id; port-agnostic mint (WorkPackage → 0x0001_0102); unmapped model → bootstrap 0. Verified via probe (local ruff + real ogar-vocab): 34 tests pass, clippy -D warnings clean. OGAR workspace can't resolve offline (surrealdb 403); CI builds it. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 984f787 commit 0ef3925

3 files changed

Lines changed: 229 additions & 0 deletions

File tree

crates/ogar-from-ruff/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,5 @@ serde = ["dep:serde", "ogar-vocab/serde"]
1515
[dependencies]
1616
ogar-vocab = { path = "../ogar-vocab" }
1717
ruff_spo_triplet = { git = "https://github.com/AdaWorldAPI/ruff", branch = "main" }
18+
ruff_spo_address = { git = "https://github.com/AdaWorldAPI/ruff", branch = "main" }
1819
serde = { workspace = true, optional = true }

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@
6464
#![forbid(unsafe_code)]
6565
#![warn(missing_docs)]
6666

67+
pub mod mint;
68+
6769
use ogar_vocab::{
6870
canonical_concept, ActionDef, Association, AssociationKind, Attribute, Callback, Class,
6971
ComputedField, EnumDecl, EnumSource, Inheritance, Language, Scope, Validation,

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

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
//! Per-class minting — the OGAR transpile substrate's addressing step.
2+
//!
3+
//! [`lift_model_graph_python`](crate::lift_model_graph_python) turns a
4+
//! [`ModelGraph`] into the schema-carrying [`Class`]es. This module adds the
5+
//! other half: the 16-byte **rail facet** for each class, with its render
6+
//! classid resolved through a language/app [`PortSpec`]
7+
//! (`OdooPort` / `OpenProjectPort` / …). The pair is the unit a consumer
8+
//! pulls back — language-agnostic compiled business logic, addressed by
9+
//! classid:
10+
//!
11+
//! ```text
12+
//! ModelGraph (any ogar-from-<lang> frontend)
13+
//! │ lift_model_graph_* -> Vec<Class> (the schema)
14+
//! │ expand + mint_with_classid -> Mint (the rail facets)
15+
//! ▼
16+
//! Vec<CompiledClass { class, facet }>
17+
//! ▲
18+
//! a consumer (odoo-rs, medcare-rs, …) pulls these via its thin wrapper
19+
//! contract (lance-graph-contract is Rust's) — the "impossible" 15 %
20+
//! (intrusive / stateful logic) stays a per-language adapter + ClassView.
21+
//! ```
22+
//!
23+
//! The classid is the cross-app join key: the **low u16** is the shared
24+
//! concept ([`PortSpec::class_id`] → the OGAR codebook), the **high u16** is
25+
//! the app render prefix ([`PortSpec::APP_PREFIX`]). So `account.move` lands
26+
//! as `0x0002_0202` under [`OdooPort`](ogar_vocab::ports::OdooPort) and an
27+
//! OpenProject `WorkPackage` lands as `0x0001_0102` under
28+
//! [`OpenProjectPort`](ogar_vocab::ports::OpenProjectPort) — different render
29+
//! skins, the same conceptual identity where they converge.
30+
31+
use ogar_vocab::app::render_classid_for;
32+
use ogar_vocab::ports::PortSpec;
33+
use ogar_vocab::Class;
34+
use ruff_spo_address::{mint_with_classid, Facet, Mint};
35+
use ruff_spo_triplet::{expand, ModelGraph};
36+
37+
use crate::lift_model_graph_python;
38+
39+
/// A class compiled to its rail-shaped, language-agnostic form: the lifted
40+
/// schema ([`Class`]) plus its 16-byte address ([`Facet`]). This is what a
41+
/// consumer pulls from the OGAR substrate and renders through its own
42+
/// `ClassView` — the schema says *what the class is*, the facet says *where
43+
/// it lives* (classid + the `part_of` / `is_a` cascade).
44+
#[derive(Debug, Clone)]
45+
pub struct CompiledClass {
46+
/// The lifted schema — attributes / associations / computed fields.
47+
pub class: Class,
48+
/// The 16-byte rail facet: render classid + the `part_of` / `is_a`
49+
/// tier chains.
50+
pub facet: Facet,
51+
}
52+
53+
/// Mint the rail facet for every node in `graph`, resolving each node's
54+
/// render classid through port `P`. Frontend-agnostic: any
55+
/// `ogar-from-<lang>` produces the same [`ModelGraph`], so the mint is
56+
/// shared. The returned [`Mint`] addresses every node (model *and* member)
57+
/// — look one up with [`Mint::facet`].
58+
#[must_use]
59+
pub fn mint_graph<P: PortSpec>(graph: &ModelGraph) -> Mint {
60+
let triples = expand(graph);
61+
mint_with_classid(&triples, |node| classid_for_node::<P>(node))
62+
}
63+
64+
/// Compile a Python/Odoo [`ModelGraph`] into rail-shaped [`CompiledClass`]es:
65+
/// lift each model's schema and pair it with its minted facet, classid via
66+
/// port `P`. Declaration order is preserved (mirrors
67+
/// [`lift_model_graph_python`]).
68+
#[must_use]
69+
pub fn compile_graph_python<P: PortSpec>(graph: &ModelGraph) -> Vec<CompiledClass> {
70+
let mint = mint_graph::<P>(graph);
71+
lift_model_graph_python(graph)
72+
.into_iter()
73+
.map(|class| {
74+
let node = format!("{}:{}", graph.namespace, class.name);
75+
// A model always appears as a subject node (`rdf:type ObjectType`),
76+
// so it mints; the fallback covers a degenerate graph by stamping
77+
// the classid with empty tier chains (still a valid rail address).
78+
let facet = mint
79+
.facet(&node)
80+
.unwrap_or_else(|| Facet::from_parts(classid_for_node::<P>(&node), [0; 6], [0; 6]));
81+
CompiledClass { class, facet }
82+
})
83+
.collect()
84+
}
85+
86+
/// Resolve a node IRI's full render classid via port `P`.
87+
///
88+
/// The IRI is `<ns>:<model>` or `<ns>:<model>.<member>`; members inherit
89+
/// their class's id, so we resolve the *model*'s concept and compose the
90+
/// render classid `(APP_PREFIX << 16) | concept`. Odoo models arrive
91+
/// underscore-normalised in the IRI (`account_move`) while ports key on the
92+
/// dotted name (`account.move`), so we try the dotted form first, then the
93+
/// raw form (mirrors `od-ontology`'s `concept_classid`). An unmapped model
94+
/// resolves to `0` — the bootstrap address, left for the registry /
95+
/// ref-escape path, never silently mis-stamped.
96+
fn classid_for_node<P: PortSpec>(node: &str) -> u32 {
97+
let model = model_of(node);
98+
P::class_id(&model.replace('_', "."))
99+
.or_else(|| P::class_id(model))
100+
.map_or(0, render_classid_for::<P>)
101+
}
102+
103+
/// The model segment of a node IRI: strip the `<ns>:` prefix, take up to the
104+
/// first `.` (the model↔member separator).
105+
fn model_of(node: &str) -> &str {
106+
let body = node.split_once(':').map_or(node, |(_, rest)| rest);
107+
body.split_once('.').map_or(body, |(model, _)| model)
108+
}
109+
110+
#[cfg(test)]
111+
mod tests {
112+
use super::*;
113+
use ogar_vocab::ports::{OdooPort, OpenProjectPort};
114+
use ruff_spo_triplet::{Field, Function, Model};
115+
116+
// A representative `account.move` `ModelGraph`, constructed directly (the
117+
// source→ModelGraph parse is `ruff_python_spo`'s job, tested there). Carries
118+
// the relation-aware shape this session's ruff#34/#35 work added.
119+
fn account_move_graph() -> ModelGraph {
120+
let mut m = Model::new("account_move");
121+
m.fields.push(Field {
122+
name: "name".to_string(),
123+
..Default::default()
124+
});
125+
m.fields.push(Field {
126+
name: "partner_id".to_string(),
127+
target: Some("res.partner".to_string()),
128+
relation_kind: Some("many2one".to_string()),
129+
..Default::default()
130+
});
131+
m.fields.push(Field {
132+
name: "line_ids".to_string(),
133+
target: Some("account.move.line".to_string()),
134+
inverse_name: Some("move_id".to_string()),
135+
relation_kind: Some("one2many".to_string()),
136+
..Default::default()
137+
});
138+
m.fields.push(Field {
139+
name: "amount_total".to_string(),
140+
emitted_by: Some("_compute_amount".to_string()),
141+
depends_on: vec!["line_ids.balance".to_string()],
142+
..Default::default()
143+
});
144+
m.functions.push(Function {
145+
name: "_compute_amount".to_string(),
146+
reads: Vec::new(),
147+
raises: Vec::new(),
148+
traverses: Vec::new(),
149+
});
150+
let mut g = ModelGraph::new("odoo");
151+
g.models.push(m);
152+
g
153+
}
154+
155+
#[test]
156+
fn account_move_compiles_to_commercial_document_rail_class() {
157+
let graph = account_move_graph();
158+
let compiled = compile_graph_python::<OdooPort>(&graph);
159+
assert_eq!(compiled.len(), 1);
160+
let cc = &compiled[0];
161+
162+
// The schema is present (the #132 field projection): partner_id /
163+
// line_ids land as associations, amount_total as a computed field.
164+
assert_eq!(cc.class.name, "account_move");
165+
assert!(
166+
!cc.class.associations.is_empty(),
167+
"relational fields project into associations"
168+
);
169+
assert!(
170+
!cc.class.computed_fields.is_empty(),
171+
"amount_total projects into computed_fields"
172+
);
173+
174+
// The rail facet carries the canonical render classid — Odoo prefix
175+
// 0x0002 | commercial_document concept 0x0202.
176+
assert_eq!(cc.facet.facet_classid(), 0x0002_0202);
177+
assert_eq!(cc.facet.facet_classid() & 0xFFFF, 0x0202, "shared concept");
178+
assert_eq!(
179+
(cc.facet.facet_classid() >> 16) as u16,
180+
OdooPort::APP_PREFIX,
181+
"Odoo render prefix",
182+
);
183+
}
184+
185+
#[test]
186+
fn member_facets_inherit_the_class_render_classid() {
187+
let graph = account_move_graph();
188+
let mint = mint_graph::<OdooPort>(&graph);
189+
// Members are addressed within their class: same render classid.
190+
for node in ["odoo:account_move", "odoo:account_move.partner_id"] {
191+
let f = mint.facet(node).unwrap_or_else(|| panic!("{node} mints"));
192+
assert_eq!(f.facet_classid(), 0x0002_0202, "{node} classid");
193+
}
194+
}
195+
196+
#[test]
197+
fn mint_is_port_agnostic_same_concept_different_render_prefix() {
198+
// The mint doesn't care about the source language — only the port
199+
// lens. An OpenProject WorkPackage graph minted through OpenProjectPort
200+
// lands as 0x0001_0102 (OpenProject prefix | project_work_item), the
201+
// SAME low-u16 concept Odoo/Redmine converge on, a different prefix.
202+
let mut graph = ModelGraph::new("openproject");
203+
graph.models.push(Model::new("WorkPackage"));
204+
let mint = mint_graph::<OpenProjectPort>(&graph);
205+
let facet = mint
206+
.facet("openproject:WorkPackage")
207+
.expect("WorkPackage mints");
208+
assert_eq!(facet.facet_classid(), 0x0001_0102);
209+
assert_eq!(facet.facet_classid() & 0xFFFF, 0x0102, "project_work_item");
210+
assert_eq!(
211+
(facet.facet_classid() >> 16) as u16,
212+
OpenProjectPort::APP_PREFIX,
213+
);
214+
}
215+
216+
#[test]
217+
fn unmapped_model_mints_the_bootstrap_address_not_a_wrong_classid() {
218+
// A model with no codebook entry resolves to classid 0 (bootstrap),
219+
// never a mis-stamped foreign id.
220+
let mut graph = ModelGraph::new("odoo");
221+
graph.models.push(Model::new("ir_cron"));
222+
let mint = mint_graph::<OdooPort>(&graph);
223+
let facet = mint.facet("odoo:ir_cron").expect("node mints");
224+
assert_eq!(facet.facet_classid(), 0, "unmapped -> bootstrap address");
225+
}
226+
}

0 commit comments

Comments
 (0)