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 } ;
3333use ogar_vocab:: app:: render_classid_for;
3434use ogar_vocab:: ports:: PortSpec ;
3535use ruff_spo_address:: { Facet , Mint , mint_with_classid} ;
3636use ruff_spo_triplet:: { ModelGraph , expand} ;
3737
3838use 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]
7178pub 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]
120138pub 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]
146173pub 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