Skip to content

Commit 507eafe

Browse files
committed
review fixes (#164 P1/P2): DO-arm carries private hook bodies; zip guard survives release
P1 (CATCH-LATENT): lift_actions now chains model.helpers — since ruff #45 the Ruby walker splits private/protected defs there, and Rails lifecycle hook targets are conventionally private (Redmine: 67/84 hook targets). Without the chain, compile_graph_ruby's actions silently omitted exactly the W3.3 delete-blocking hook bodies AT-CARRY-1 exists to carry. The producer emits effect annotations; routability enrichment stays a registration concern (join Model::callbacks by name to tell hooks from routable actions). Regression test: a private helper hook body arrives with its reads/writes intact. P2: the three zip guards are plain assert_eq! with the invariant named — debug_assert compiled out in release, where a future model-level filter would truncate the zip silently and cross-attribute every subsequent class's actions. Once-per-graph, effectively free. cargo test -p ogar-from-ruff: 65+3 green; clippy --all-targets -D warnings clean.
1 parent f5bb789 commit 507eafe

2 files changed

Lines changed: 56 additions & 3 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: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,12 @@ pub fn compile_graph_python<P: PortSpec>(graph: &ModelGraph) -> Vec<CompiledClas
8181
// The lift maps `graph.models` 1:1 in declaration order (member-level
8282
// filter_maps only), so zipping recovers each class's source `Model` —
8383
// the carrier `lift_actions` needs (AT-CARRY-1).
84-
debug_assert_eq!(classes.len(), graph.models.len());
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+
);
8590
classes
8691
.into_iter()
8792
.zip(&graph.models)
@@ -134,7 +139,12 @@ pub fn compile_graph_sqlalchemy<P: PortSpec>(graph: &ModelGraph) -> Vec<Compiled
134139
let mint = mint_graph::<P>(graph);
135140
let classes = lift_model_graph_sqlalchemy(graph);
136141
// 1:1 with `graph.models` in declaration order (see compile_graph_python).
137-
debug_assert_eq!(classes.len(), graph.models.len());
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+
);
138148
classes
139149
.into_iter()
140150
.zip(&graph.models)
@@ -164,7 +174,12 @@ pub fn compile_graph_ruby<P: PortSpec>(graph: &ModelGraph) -> Vec<CompiledClass>
164174
let mint = mint_graph::<P>(graph);
165175
let classes = lift_model_graph(graph);
166176
// 1:1 with `graph.models` in declaration order (see compile_graph_python).
167-
debug_assert_eq!(classes.len(), graph.models.len());
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+
);
168183
classes
169184
.into_iter()
170185
.zip(&graph.models)

0 commit comments

Comments
 (0)