From 35750761027a8e420b941cb9b6a33aa26722625f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 7 Jul 2026 04:47:27 +0000 Subject: [PATCH 1/2] ogar: record + pin the Arm B emitter debt (Opus review P2), add constrains/onchange constructors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Post-merge Opus review of #169 (AT-CARRY-2 arms B+D) found one P2: the TTL emitter kausal_triples has no Constrains/Onchange arm, so both fall through the mandatory wildcard to ogar:Unknown and drop their field paths. KausalSpec is #[non_exhaustive], so cross-crate matches CANNOT be wildcard-free — the kausal_spec_match_is_exhaustive fuse only guards ogar-vocab, never the emitter. Spec §6 defers emitter wiring, so this is spec-conform, not a violation, but it is actively-wrong TTL for any @api.constrains/@api.onchange model. - ogar-emitter: characterization test pins the interim ogar:Unknown + dropped-paths behaviour so it is documented, not silent, and flips loudly when §6 wires the emitter. - ogar-vocab: add KausalSpec::{constrains,onchange} constructors mirroring depends()/lifecycle() (additive-API gap from #169; the test + consumers need them). - DISCOVERY-MAP: D-ATC2-KAUSAL-RUFF-GATED — the owed B+D ledger entry, with an honest correction of the overstated cross-crate 'loud break' rationale and the recorded emitter debt. Post-merge workspace verification (479/0 vs ruff main 9ef26c1) noted. No vocab mint (ogar:Constrains/Onchange kinds + path predicate) — that is the §6 governed follow-up. --- crates/ogar-emitter/src/lib.rs | 34 +++++++++++++++++++++++++++++++++ crates/ogar-vocab/src/lib.rs | 12 ++++++++++++ docs/DISCOVERY-MAP.md | 35 ++++++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+) diff --git a/crates/ogar-emitter/src/lib.rs b/crates/ogar-emitter/src/lib.rs index 9d5a34a..f6e3e7f 100644 --- a/crates/ogar-emitter/src/lib.rs +++ b/crates/ogar-emitter/src/lib.rs @@ -1374,6 +1374,40 @@ mod tests { assert!(t.iter().any(|t| t.predicate == "ogar:kausalKind" && t.object == "ogar:External")); } + #[test] + fn kausal_constrains_onchange_currently_emit_unknown_pending_emitter_wiring() { + // CHARACTERIZATION (not aspiration): SPEC-ATC2-OGAR §6 defers the TTL + // emitter wiring for the Arm B variants, so `kausal_triples` has no + // Constrains/Onchange arm — they fall through the mandatory wildcard + // (`KausalSpec` is #[non_exhaustive], so cross-crate matches CANNOT be + // wildcard-free; the `kausal_spec_match_is_exhaustive` fuse only + // protects ogar-vocab, never this consumer). The IR struct carries the + // paths correctly; the TTL projection currently drops them and labels + // the kind `ogar:Unknown`. This test PINS that lossy interim so the + // drop is documented, not silent — when §6 lands (predicates + // `ogar:Constrains`/`ogar:Onchange` + a path predicate), this test + // fails loudly and forces the implementer to update it. See ledger + // D-ATC2-KAUSAL-RUFF-GATED. + for k in [ + ogar_vocab::KausalSpec::constrains(vec!["state".into()]), + ogar_vocab::KausalSpec::onchange(vec!["partner_id".into()]), + ] { + let mut def = ogar_vocab::ActionDef::new("a", "p", "ogit-op/Foo"); + def.kausal = Some(k); + let t = TripleEmitter::emit_action_def(&def); + assert!( + t.iter() + .any(|t| t.predicate == "ogar:kausalKind" && t.object == "ogar:Unknown"), + "interim: unwired Arm B variants label as ogar:Unknown" + ); + // The field paths are NOT projected to TTL yet (the debt). + assert!( + !t.iter().any(|t| t.object == "state" || t.object == "partner_id"), + "interim: Arm B field paths are dropped at TTL emission (§6 deferred)" + ); + } + } + #[test] fn action_state_lifecycle_serialization() { for (state, expected) in [ diff --git a/crates/ogar-vocab/src/lib.rs b/crates/ogar-vocab/src/lib.rs index 8a5943d..6e0bfa4 100644 --- a/crates/ogar-vocab/src/lib.rs +++ b/crates/ogar-vocab/src/lib.rs @@ -722,6 +722,18 @@ impl KausalSpec { pub fn depends(paths: Vec) -> Self { Self::Depends { paths } } + + /// Convenience: build a Constrains spec (`@api.constrains` targets). + #[must_use] + pub fn constrains(paths: Vec) -> Self { + Self::Constrains { paths } + } + + /// Convenience: build an Onchange spec (`@api.onchange` targets). + #[must_use] + pub fn onchange(paths: Vec) -> Self { + Self::Onchange { paths } + } } /// The four canonical Active Record relation kinds. Cross-ORM mapping: diff --git a/docs/DISCOVERY-MAP.md b/docs/DISCOVERY-MAP.md index 58b0775..3245f08 100644 --- a/docs/DISCOVERY-MAP.md +++ b/docs/DISCOVERY-MAP.md @@ -1083,3 +1083,38 @@ isolation. The map's job is to keep them visible. index — safe under Odoo semantics (`@api.depends` is method-level, co-computed fields carry identical `depends_on`), revisit if a frontend ever emits divergent `depends_on` per co-computed field. + +- **D-ATC2-KAUSAL-RUFF-GATED** — 2026-07-06 `[G]`: AT-CARRY-2 arms B+D + landed (OGAR #169) after ruff #49 put `Function::{constrains,onchange}` + + `Field::stored` on ruff main. Arm B: `KausalSpec::{Constrains,Onchange}` + populated in `lift_actions` ONLY when `kausal.is_none()` (SPEC §3b) — so + Depends (Arm A) always wins, Constrains beats Onchange; sourced purely + from the decorator fields, never `reads`/`writes` (regression + `lift_actions_depends_arm_a_regression_unaffected_by_arm_b`). Arm D: + `ComputedField.stored = field.stored.unwrap_or(false)` at both projection + sites (§5: Odoo's not-stored default). Post-merge verification: OGAR + workspace green against ruff main `9ef26c1` — `cargo build --workspace` + clean incl. `ogar-from-rails` (the float-on-main risk), `cargo test + --workspace` 479/0. Opus adversarial review: CLEAN impl, one P2 below. + - **HONEST CORRECTION to the D-ATC2-KAUSAL-AUTARK fuse rationale.** That + entry (and the B-arm commit message) claimed the exhaustive + `kausal_spec_match_is_exhaustive` guard makes *consumers* "loud break + instead of silently defaulting" on new variants. That is TRUE only + intra-crate. `KausalSpec` is `#[non_exhaustive]`, so every cross-crate + `match` is FORCED to carry a wildcard — the fuse cannot protect any + consumer outside ogar-vocab. The claim was overstated. + - **DEBT (P2, spec-deferred per §6): the TTL emitter silently mislabels + Arm B.** `ogar-emitter::kausal_triples` has no Constrains/Onchange arm; + both fall through `_ => ogar:Unknown` (lib.rs:779) and their field paths + are DROPPED at TTL emission. The IR struct is correct (consumers that + read `ActionDef.kausal` directly — e.g. the odoo-rs AT-CONSUME parity + pin — are unaffected); only the TTL projection is lossy. §6 defers the + emitter/SurrealQL wiring, so this is not a spec violation, but it is + actively-wrong (not merely missing) TTL. Converted from silent to + documented: characterization test + `kausal_constrains_onchange_currently_emit_unknown_pending_emitter_wiring` + PINS the interim `ogar:Unknown` + dropped-paths behaviour so it flips + LOUDLY when §6 lands (defining `ogar:Constrains`/`ogar:Onchange` kinds + + a kausal path predicate — a governed vocab mint, NOT done here). + - Additive API gap closed: `KausalSpec::{constrains,onchange}` constructors + added to mirror `depends()`/`lifecycle()` (consumers + the test need them). From e806be7f6b8705fd9b9c9ab4bcdc376d712726f4 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 7 Jul 2026 05:09:36 +0000 Subject: [PATCH 2/2] ledger: drop stale 'SurrealQL' as the deferred forward target MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The D-ATC2-KAUSAL-RUFF-GATED debt note echoed SPEC-ATC2-OGAR §6's stale 'SurrealQL-Adapter' wording. Per the operator ruling already in this ledger (2026-07-05: 'Odoo's spog lives now in V3 substrate in lance-graph <>OGAR, not surrealdb AST'), the forward substrate is OGAR-v3 / lance-graph; the SurrealQL-AST adapter is deprecated. The live emitter debt is the TTL path (ogar-emitter → ogar-adapter-ttl + v3 substrate), not SurrealQL. --- docs/DISCOVERY-MAP.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/DISCOVERY-MAP.md b/docs/DISCOVERY-MAP.md index 3245f08..d9eb591 100644 --- a/docs/DISCOVERY-MAP.md +++ b/docs/DISCOVERY-MAP.md @@ -1109,8 +1109,15 @@ isolation. The map's job is to keep them visible. are DROPPED at TTL emission. The IR struct is correct (consumers that read `ActionDef.kausal` directly — e.g. the odoo-rs AT-CONSUME parity pin — are unaffected); only the TTL projection is lossy. §6 defers the - emitter/SurrealQL wiring, so this is not a spec violation, but it is - actively-wrong (not merely missing) TTL. Converted from silent to + downstream-consumer wiring, so this is not a spec violation, but it is + actively-wrong (not merely missing) TTL. (NB: SPEC-ATC2-OGAR §6 line 219 + still names a "SurrealQL-Adapter" as a deferred consumer — that naming is + STALE. Per the operator ruling recorded above (2026-07-05: "Odoo's spog + lives now in V3 substrate in lance-graph <>OGAR, not surrealdb AST"), the + forward substrate is the OGAR-v3 / lance-graph path; the SurrealQL-AST + adapter is deprecated. The live debt is the `ogar-emitter` TTL path, which + `ogar-adapter-ttl` and the v3 substrate consume — not SurrealQL.) + Converted from silent to documented: characterization test `kausal_constrains_onchange_currently_emit_unknown_pending_emitter_wiring` PINS the interim `ogar:Unknown` + dropped-paths behaviour so it flips