+| F17 | **Body-triage / accidentally-imperative ratio** (`PROBE-OGAR-BODY-TRIAGE`) | run the body pass over real hooks → `(target classid, verb-class, order-signature)` per hook → **round-trip-order-free** each coarse group (does the order-free `(verb,criteria)` reproduce the source output?). PASS-rate = the accidentally-imperative (recoverable) fraction; FAIL-rate = the order-dependent / essentially-foreign (preserve/escape) tail. Control = Odoo `_compute_*` (already declarative); test = Rails `before_*`/`after_*`. **Odoo control-leg RUN** (2026-07-06, `odoo-rs tests/body_triage_probe.rs`, default build, slice_2, run verified green, numbers pinned as drift-fuses in the probe): 393 lifecycle hooks; verb-classes guard-pure 44 / compute-pure 298 / self-feedback 30 / write+raise 2 / read-only 15 / no-facts 4; headline (behavioural arm = guards + computes: 357 hooks, 354 resolved): **PASS 336 (94.9%)** accidentally imperative, order-free recoverable / **FAIL 18 (5.1%)** order-dependent tail — decomposing into self-feedback (read-modify-write inside one hook; conservative, includes `@api.depends` extractor artifacts, so true tail ≤ measured) + write+raise (partial-write escape order); 3 unresolved (no facts captured) excluded; context: onchange 15/17 resolved FAIL — cooperative loops are the genuinely order-dependent shape, as predicted; cross-hook order NOT counted (recompute-DAG Kahn-orderability re-asserted inside the probe); method = static order-signature from harvested facts — writes = inverted `emitted_by` (Odoo's declarative `compute=` target; the ledger note below, "Python frontend leaves them empty", is exactly why the declarative target is used), reads = `reads_field`, raise = `raises` | D‑ACCIDENTAL‑IMPERATIVE / D‑FUNCTION‑CATALOG `[H]→[G]`. ~~**Gated on a ruff extension:** capture writes/calls per function (today: reads/raises/traverses only) so "calls update on X" is extractable~~ **Prerequisite SHIPPED 2026-06-30** (AdaWorldAPI/ruff @ `claude/odoo-rs-transcode-lf8ya5`, commit `dd70588`): `ruff_spo_triplet::Function` gains `writes` (self.<field>= setters → `writes_field`, Authoritative) + `calls` (lifecycle-mutator dispatches as "receiver.method" → `calls`, Inferred); the `ruff_ruby_spo` body walker populates both from the Rails AST (closed `AR_MUTATORS` set). "calls update on X" is now extractable → **F17 is RUNNABLE** (probe still to author/run). Python frontend leaves them empty (Odoo write target already declarative via `Field::emitted_by`). **Odoo CONTROL leg MEASURED 2026-07-06** (94.9% order-free recoverable — consistent with "already declarative", the control behaving as a control should); D‑ACCIDENTAL‑IMPERATIVE stays `[H]` until the Rails TEST leg (`before_*`/`after_*` via `ruff_ruby_spo` writes/calls) runs. **Rails TEST leg MEASURED 2026-07-06** (openproject-nexgen-rs `crates/ruff_openproject/tests/body_triage_probe.rs`, Redmine corpus incl. `Model::helpers` private-visibility split + one-Callback-per-symbol fixes, numbers pinned as drift-fuses): 114 lifecycle hooks — coarse verb-classes blocks 8 / no-facts 43 / self-feedback 3 / write+raise 1 / guard-pure 0 / compute-pure 58 / read-only 1; behavioural arm 62; coarse **PASS 93.5%**; the FAIL tail was then rolled through the recipe codebook (ruff #45; J1 `writes_if_blank` ruff #47 splits Default from Normalize) → **Cascade 46 / Compute 12 / Default 2 / Normalize 1 / WriteRaise 0 / Compensate 1 = 98.4% recipe-recoverable, exactly 1 essential imperative core**. Both legs measured → **D‑ACCIDENTAL‑IMPERATIVE REGRADED `[H]→[G]`** (dated note on the DISCOVERY-MAP row) |
0 commit comments