Skip to content

Commit de8dfa8

Browse files
committed
ogar-from-ruff: extend odoo kausal pin to Arm B (constrains) post ruff #49
The rebase onto main (which carries arms B+D via ruff #49 / D-ATC2-KAUSAL-RUFF-GATED) surfaced that @api.constrains methods now get KausalSpec::Constrains. Extend the W1 witness to pin all 3 constrains methods verbatim (was: asserted kausal==None). Now 11/11 kausal (8 depends + 3 constrains); _check_lines moved from the plain-methods list to the Constrains pin. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NSfzujtWL1BnHgnJGAhg9N
1 parent e134dff commit de8dfa8

1 file changed

Lines changed: 79 additions & 17 deletions

File tree

crates/ogar-from-ruff/tests/odoo_kausal_parity_probe.rs

Lines changed: 79 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,12 @@
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
@@ -34,8 +36,8 @@
3436
//! bootstrap value is asserted as a fact, not treated as a defect.
3537
3638
use ogar_from_ruff::mint::compile_graph_python;
37-
use ogar_vocab::ports::OdooPort;
3839
use ogar_vocab::KausalSpec;
40+
use ogar_vocab::ports::OdooPort;
3941
use 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).
115139
const 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]
145186
fn 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

Comments
 (0)