Skip to content

Commit 748ba11

Browse files
authored
Merge pull request #164 from AdaWorldAPI/claude/odoo-transcode-ruff-ast-5ejqvr
feat(mint): AT-CARRY-1 — CompiledClass carries the DO-arm (actions: V…
2 parents dc02f96 + 507eafe commit 748ba11

2 files changed

Lines changed: 100 additions & 12 deletions

File tree

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -532,9 +532,19 @@ pub fn project_canonical_roles(class: &Class) -> std::collections::HashSet<&'sta
532532
/// downstream at registration, not in the producer.
533533
#[must_use]
534534
pub fn lift_actions(model: &Model) -> Vec<ActionDef> {
535+
// Public actions AND non-public helpers (AT-CARRY-1b, review on #164):
536+
// since ruff #45 the Ruby walker splits private/protected defs into
537+
// `Model::helpers` — and Rails lifecycle hook targets are conventionally
538+
// private (Redmine measurement: 67/84 hook targets live there). The W3.3
539+
// delete-blocking rows ARE those hook bodies, so the DO-arm must carry
540+
// both. Consistent with this fn's contract: the producer emits effect
541+
// annotations; routability / guard / RBAC enrichment happens downstream
542+
// at registration (a registrar can re-join `Model::callbacks` by name to
543+
// tell hook targets from routable actions).
535544
model
536545
.functions
537546
.iter()
547+
.chain(model.helpers.iter())
538548
.map(|f| {
539549
let mut a = ActionDef::new(
540550
format!("{}::action_def::{}", model.name, f.name),
@@ -1812,4 +1822,32 @@ mod tests {
18121822
// Mismatched / partial — pass-through.
18131823
assert_eq!(strip_ruby_literal_markers("\"User"), "\"User");
18141824
}
1825+
1826+
#[test]
1827+
fn lift_actions_carries_private_helper_hook_bodies() {
1828+
// AT-CARRY-1b (review on #164): Rails lifecycle hook targets are
1829+
// conventionally private -> `Model::helpers` (ruff #45). The DO-arm
1830+
// must carry their body facts, or the W3.3 delete-blocking rows
1831+
// never reach a consumer.
1832+
let mut m = Model::new("Issue");
1833+
m.functions.push(ruff_spo_triplet::Function {
1834+
name: "public_action".to_string(),
1835+
..Default::default()
1836+
});
1837+
m.helpers.push(ruff_spo_triplet::Function {
1838+
name: "update_closed_on".to_string(),
1839+
writes: vec!["closed_on".to_string()],
1840+
reads: vec!["updated_on".to_string()],
1841+
..Default::default()
1842+
});
1843+
let actions = lift_actions(&m);
1844+
assert_eq!(actions.len(), 2, "public action + private hook body");
1845+
let hook = actions
1846+
.iter()
1847+
.find(|a| a.predicate == "update_closed_on")
1848+
.expect("private hook body must arrive in the DO-arm");
1849+
assert_eq!(hook.identity, "Issue::action_def::update_closed_on");
1850+
assert_eq!(hook.writes, vec!["closed_on".to_string()]);
1851+
assert_eq!(hook.reads, vec!["updated_on".to_string()]);
1852+
}
18151853
}

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

Lines changed: 62 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,14 @@
2929
//! [`OpenProjectPort`](ogar_vocab::ports::OpenProjectPort) — different render
3030
//! skins, the same conceptual identity where they converge.
3131
32-
use ogar_vocab::Class;
32+
use ogar_vocab::{ActionDef, Class};
3333
use ogar_vocab::app::render_classid_for;
3434
use ogar_vocab::ports::PortSpec;
3535
use ruff_spo_address::{Facet, Mint, mint_with_classid};
3636
use ruff_spo_triplet::{ModelGraph, expand};
3737

3838
use crate::sqlalchemy::lift_model_graph_sqlalchemy;
39-
use crate::{lift_model_graph, lift_model_graph_python};
39+
use crate::{lift_actions, lift_model_graph, lift_model_graph_python};
4040

4141
/// A class compiled to its rail-shaped, language-agnostic form: the lifted
4242
/// schema ([`Class`]) plus its 16-byte address ([`Facet`]). This is what a
@@ -50,6 +50,13 @@ pub struct CompiledClass {
5050
/// The 16-byte rail facet: render classid + the `part_of` / `is_a`
5151
/// tier chains.
5252
pub facet: Facet,
53+
/// The lifted DO-arm: one [`ActionDef`] per harvested method, carrying
54+
/// the post-O-1 body facts (`reads` / `writes` / `calls`). AT-CARRY-1 of
55+
/// the W3.3 delete gate (odoo-rs `docs/W3.3-DELETE-GATE-MATRIX.md`):
56+
/// [`crate::lift_actions`] existed but was dropped here, so consumers
57+
/// never saw the behaviour arm — the THINK arm (`class`) and the DO arm
58+
/// (`actions`) now travel together, addressed by the same `facet`.
59+
pub actions: Vec<ActionDef>,
5360
}
5461

5562
/// Mint the rail facet for every node in `graph`, resolving each node's
@@ -70,17 +77,28 @@ pub fn mint_graph<P: PortSpec>(graph: &ModelGraph) -> Mint {
7077
#[must_use]
7178
pub fn compile_graph_python<P: PortSpec>(graph: &ModelGraph) -> Vec<CompiledClass> {
7279
let mint = mint_graph::<P>(graph);
73-
lift_model_graph_python(graph)
80+
let classes = lift_model_graph_python(graph);
81+
// The lift maps `graph.models` 1:1 in declaration order (member-level
82+
// filter_maps only), so zipping recovers each class's source `Model` —
83+
// the carrier `lift_actions` needs (AT-CARRY-1).
84+
assert_eq!(
85+
classes.len(),
86+
graph.models.len(),
87+
"lift must map graph.models 1:1 in declaration order — a model-level \
88+
filter would silently mis-zip actions onto the wrong class"
89+
);
90+
classes
7491
.into_iter()
75-
.map(|class| {
92+
.zip(&graph.models)
93+
.map(|(class, model)| {
7694
let node = format!("{}:{}", graph.namespace, class.name);
7795
// A model always appears as a subject node (`rdf:type ObjectType`),
7896
// so it mints; the fallback covers a degenerate graph by stamping
7997
// the classid with empty tier chains (still a valid rail address).
8098
let facet = mint
8199
.facet(&node)
82100
.unwrap_or_else(|| Facet::from_parts(classid_for_node::<P>(&node), [0; 6], [0; 6]));
83-
CompiledClass { class, facet }
101+
CompiledClass { class, facet, actions: lift_actions(model) }
84102
})
85103
.collect()
86104
}
@@ -119,14 +137,23 @@ pub fn compile_graph_python<P: PortSpec>(graph: &ModelGraph) -> Vec<CompiledClas
119137
#[must_use]
120138
pub fn compile_graph_sqlalchemy<P: PortSpec>(graph: &ModelGraph) -> Vec<CompiledClass> {
121139
let mint = mint_graph::<P>(graph);
122-
lift_model_graph_sqlalchemy(graph)
140+
let classes = lift_model_graph_sqlalchemy(graph);
141+
// 1:1 with `graph.models` in declaration order (see compile_graph_python).
142+
assert_eq!(
143+
classes.len(),
144+
graph.models.len(),
145+
"lift must map graph.models 1:1 in declaration order — a model-level \
146+
filter would silently mis-zip actions onto the wrong class"
147+
);
148+
classes
123149
.into_iter()
124-
.map(|class| {
150+
.zip(&graph.models)
151+
.map(|(class, model)| {
125152
let node = format!("{}:{}", graph.namespace, class.name);
126153
let facet = mint
127154
.facet(&node)
128155
.unwrap_or_else(|| Facet::from_parts(classid_for_node::<P>(&node), [0; 6], [0; 6]));
129-
CompiledClass { class, facet }
156+
CompiledClass { class, facet, actions: lift_actions(model) }
130157
})
131158
.collect()
132159
}
@@ -145,14 +172,23 @@ pub fn compile_graph_sqlalchemy<P: PortSpec>(graph: &ModelGraph) -> Vec<Compiled
145172
#[must_use]
146173
pub fn compile_graph_ruby<P: PortSpec>(graph: &ModelGraph) -> Vec<CompiledClass> {
147174
let mint = mint_graph::<P>(graph);
148-
lift_model_graph(graph)
175+
let classes = lift_model_graph(graph);
176+
// 1:1 with `graph.models` in declaration order (see compile_graph_python).
177+
assert_eq!(
178+
classes.len(),
179+
graph.models.len(),
180+
"lift must map graph.models 1:1 in declaration order — a model-level \
181+
filter would silently mis-zip actions onto the wrong class"
182+
);
183+
classes
149184
.into_iter()
150-
.map(|class| {
185+
.zip(&graph.models)
186+
.map(|(class, model)| {
151187
let node = format!("{}:{}", graph.namespace, class.name);
152188
let facet = mint
153189
.facet(&node)
154190
.unwrap_or_else(|| Facet::from_parts(classid_for_node::<P>(&node), [0; 6], [0; 6]));
155-
CompiledClass { class, facet }
191+
CompiledClass { class, facet, actions: lift_actions(model) }
156192
})
157193
.collect()
158194
}
@@ -219,7 +255,8 @@ mod tests {
219255
});
220256
m.functions.push(Function {
221257
name: "_compute_amount".to_string(),
222-
reads: Vec::new(),
258+
reads: vec!["line_ids.balance".to_string()],
259+
writes: vec!["amount_total".to_string()],
223260
raises: Vec::new(),
224261
traverses: Vec::new(),
225262
..Default::default()
@@ -261,6 +298,19 @@ mod tests {
261298
OdooPort::APP_PREFIX,
262299
"Odoo render prefix",
263300
);
301+
302+
// AT-CARRY-1 (W3.3 delete gate, odoo-rs docs/W3.3-DELETE-GATE-MATRIX.md):
303+
// the DO-arm travels WITH the compiled class — one ActionDef per
304+
// harvested method, body facts intact. Before this field, lift_actions
305+
// existed but compile_graph_* dropped it and no consumer ever saw the
306+
// behaviour arm.
307+
assert_eq!(cc.actions.len(), 1, "one ActionDef per harvested method");
308+
let act = &cc.actions[0];
309+
assert_eq!(act.predicate, "_compute_amount");
310+
assert_eq!(act.identity, "account_move::action_def::_compute_amount");
311+
assert_eq!(act.object_class, "account_move");
312+
assert_eq!(act.reads, vec!["line_ids.balance".to_string()]);
313+
assert_eq!(act.writes, vec!["amount_total".to_string()]);
264314
}
265315

266316
#[test]

0 commit comments

Comments
 (0)