2222//!
2323//! # Scope (named, not hidden)
2424//!
25- //! - Only Arm A (`compute=` + `@api.depends`) is pinned here. Arm B
26- //! (`@api.constrains` / `@api.onchange` -> a `KausalSpec` shape) and
27- //! `computed.stored` parity remain out of scope pending ruff #49 (no
28- //! `ActionDef`/`Class` slot exists yet for either).
25+ //! - Arm A (`compute=` + `@api.depends` -> `KausalSpec::Depends`) AND Arm B
26+ //! (`@api.constrains` -> `KausalSpec::Constrains`) are both pinned here —
27+ //! Arm B opened up once ruff #49 landed `Function::constrains` on main
28+ //! (`D-ATC2-KAUSAL-RUFF-GATED`). The fixture has no `@api.onchange`, so the
29+ //! Onchange arm is not exercised by this witness. `computed.stored` (Arm D)
30+ //! is a `Class`-side field, out of scope for this DO-arm/kausal probe.
2931//! - `account.payment.term` / `account.payment.term.line` are NOT in
3032//! `ogar_vocab::ports::ODOO_ALIASES`, so both mint the bootstrap facet
3133//! classid `0` (see `mint.rs`'s
3436//! bootstrap value is asserted as a fact, not treated as a defect.
3537
3638use ogar_from_ruff:: mint:: compile_graph_python;
37- use ogar_vocab:: ports:: OdooPort ;
3839use ogar_vocab:: KausalSpec ;
40+ use ogar_vocab:: ports:: OdooPort ;
3941use ruff_python_spo:: extract_from_source;
4042
4143/// Verbatim Odoo 19 source, `addons/account/models/account_payment_term.py`
@@ -108,14 +110,35 @@ const PAYMENT_TERM_LINE_DEPENDS: &[ExpectedDepends] = &[
108110 } ,
109111] ;
110112
111- /// Plain (non-compute) methods on `AccountPaymentTerm`, read off the same
112- /// source, that must NOT carry a `kausal` fact — the facts-only guard
113- /// (`D-ATC2-KAUSAL-AUTARK`: a method's own body `reads` never gets promoted
114- /// into a reactive `KausalSpec::Depends` trigger).
113+ /// `AccountPaymentTerm` methods carrying `@api.constrains(...)` — the Arm-B
114+ /// witness that opened up once ruff #49 landed `Function::constrains` on main
115+ /// (`D-ATC2-KAUSAL-RUFF-GATED`). Each becomes
116+ /// `KausalSpec::Constrains { paths }` (validation trigger), Depends still
117+ /// wins when both are present (`kausal.is_none()` gate in `lift_actions`).
118+ const PAYMENT_TERM_CONSTRAINS : & [ ExpectedDepends ] = & [ ExpectedDepends {
119+ method : "_check_lines" ,
120+ paths : & [ "line_ids" , "early_discount" ] ,
121+ } ] ;
122+
123+ /// `AccountPaymentTermLine` methods carrying `@api.constrains(...)`.
124+ const PAYMENT_TERM_LINE_CONSTRAINS : & [ ExpectedDepends ] = & [
125+ ExpectedDepends {
126+ method : "_check_valid_char_value" ,
127+ paths : & [ "days_next_month" ] ,
128+ } ,
129+ ExpectedDepends {
130+ method : "_check_percent" ,
131+ paths : & [ "value" , "value_amount" ] ,
132+ } ,
133+ ] ;
134+
135+ /// Plain (non-compute, non-constrains) methods on `AccountPaymentTerm`, read
136+ /// off the same source, that must NOT carry a `kausal` fact — the facts-only
137+ /// guard (`D-ATC2-KAUSAL-AUTARK`: a method's own body `reads` never gets
138+ /// promoted into a reactive `KausalSpec` trigger).
115139const PAYMENT_TERM_PLAIN_METHODS : & [ & str ] = & [
116140 "_get_amount_due_after_discount" ,
117141 "_get_amount_by_date" ,
118- "_check_lines" ,
119142 "_compute_terms" , // named like a compute target but has no `compute=` field pointing at it
120143 "_unlink_except_referenced_terms" ,
121144 "_get_last_discount_date" ,
@@ -141,6 +164,24 @@ fn assert_kausal_depends(
141164 ) ;
142165}
143166
167+ fn assert_kausal_constrains (
168+ actions : & [ ogar_vocab:: ActionDef ] ,
169+ expected : & ExpectedDepends ,
170+ model : & str ,
171+ ) {
172+ let action = actions
173+ . iter ( )
174+ . find ( |a| a. predicate == expected. method )
175+ . unwrap_or_else ( || panic ! ( "{model}: no ActionDef for method {:?}" , expected. method) ) ;
176+ let expected_paths: Vec < String > = expected. paths . iter ( ) . map ( |s| s. to_string ( ) ) . collect ( ) ;
177+ assert_eq ! (
178+ action. kausal,
179+ Some ( KausalSpec :: constrains( expected_paths) ) ,
180+ "{model}::{} kausal constrains-paths mismatch" ,
181+ expected. method
182+ ) ;
183+ }
184+
144185#[ test]
145186fn odoo_kausal_parity_pin_account_payment_term ( ) {
146187 let graph = extract_from_source ( ACCOUNT_PAYMENT_TERM_PY ) ;
@@ -179,17 +220,32 @@ fn odoo_kausal_parity_pin_account_payment_term() {
179220 ) ;
180221 }
181222
182- // Exactly one ActionDef per compute METHOD, not per field —
183- // _compute_example_preview emits two fields but appears once.
223+ // ── Arm B pin: every @api.constrains method → KausalSpec::Constrains ──
224+ // (opened up by ruff #49 / D-ATC2-KAUSAL-RUFF-GATED; Depends still wins
225+ // when both are present, so these are the pure-constrains methods.)
226+ for expected in PAYMENT_TERM_CONSTRAINS {
227+ assert_kausal_constrains ( & payment_term. actions , expected, "account_payment_term" ) ;
228+ }
229+ for expected in PAYMENT_TERM_LINE_CONSTRAINS {
230+ assert_kausal_constrains (
231+ & payment_term_line. actions ,
232+ expected,
233+ "account_payment_term_line" ,
234+ ) ;
235+ }
236+
237+ // Exactly one kausal-bearing ActionDef per @api.depends compute METHOD
238+ // (not per field — _compute_example_preview emits two fields, appears once)
239+ // PLUS one per @api.constrains method.
184240 let kausal_bearing = payment_term
185241 . actions
186242 . iter ( )
187243 . filter ( |a| a. kausal . is_some ( ) )
188244 . count ( ) ;
189245 assert_eq ! (
190246 kausal_bearing,
191- PAYMENT_TERM_DEPENDS . len( ) ,
192- "one kausal-bearing ActionDef per @api.depends compute method (account_payment_term)"
247+ PAYMENT_TERM_DEPENDS . len( ) + PAYMENT_TERM_CONSTRAINS . len ( ) ,
248+ "one kausal-bearing ActionDef per @api.depends + @api.constrains method (account_payment_term)"
193249 ) ;
194250 let kausal_bearing_line = payment_term_line
195251 . actions
@@ -198,8 +254,8 @@ fn odoo_kausal_parity_pin_account_payment_term() {
198254 . count ( ) ;
199255 assert_eq ! (
200256 kausal_bearing_line,
201- PAYMENT_TERM_LINE_DEPENDS . len( ) ,
202- "one kausal-bearing ActionDef per @api.depends compute method (account_payment_term_line)"
257+ PAYMENT_TERM_LINE_DEPENDS . len( ) + PAYMENT_TERM_LINE_CONSTRAINS . len ( ) ,
258+ "one kausal-bearing ActionDef per @api.depends + @api.constrains method (account_payment_term_line)"
203259 ) ;
204260
205261 // ── facts-only guard: plain methods carry no kausal fact ──
@@ -215,10 +271,16 @@ fn odoo_kausal_parity_pin_account_payment_term() {
215271 ) ;
216272 }
217273
274+ let expected_kausal = PAYMENT_TERM_DEPENDS . len ( )
275+ + PAYMENT_TERM_LINE_DEPENDS . len ( )
276+ + PAYMENT_TERM_CONSTRAINS . len ( )
277+ + PAYMENT_TERM_LINE_CONSTRAINS . len ( ) ;
218278 println ! (
219- "OK: account_payment_term(+line) kausal_actions={}/{} plain_checked={} total_actions={}/{}" ,
279+ "OK: account_payment_term(+line) kausal_actions={}/{} (depends={} constrains={}) plain_checked={} total_actions={}/{}" ,
220280 kausal_bearing + kausal_bearing_line,
281+ expected_kausal,
221282 PAYMENT_TERM_DEPENDS . len( ) + PAYMENT_TERM_LINE_DEPENDS . len( ) ,
283+ PAYMENT_TERM_CONSTRAINS . len( ) + PAYMENT_TERM_LINE_CONSTRAINS . len( ) ,
222284 PAYMENT_TERM_PLAIN_METHODS . len( ) ,
223285 payment_term. actions. len( ) ,
224286 payment_term_line. actions. len( ) ,
0 commit comments