Skip to content

Commit 3575076

Browse files
committed
ogar: record + pin the Arm B emitter debt (Opus review P2), add constrains/onchange constructors
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.
1 parent 9bfb77e commit 3575076

3 files changed

Lines changed: 81 additions & 0 deletions

File tree

crates/ogar-emitter/src/lib.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1374,6 +1374,40 @@ mod tests {
13741374
assert!(t.iter().any(|t| t.predicate == "ogar:kausalKind" && t.object == "ogar:External"));
13751375
}
13761376

1377+
#[test]
1378+
fn kausal_constrains_onchange_currently_emit_unknown_pending_emitter_wiring() {
1379+
// CHARACTERIZATION (not aspiration): SPEC-ATC2-OGAR §6 defers the TTL
1380+
// emitter wiring for the Arm B variants, so `kausal_triples` has no
1381+
// Constrains/Onchange arm — they fall through the mandatory wildcard
1382+
// (`KausalSpec` is #[non_exhaustive], so cross-crate matches CANNOT be
1383+
// wildcard-free; the `kausal_spec_match_is_exhaustive` fuse only
1384+
// protects ogar-vocab, never this consumer). The IR struct carries the
1385+
// paths correctly; the TTL projection currently drops them and labels
1386+
// the kind `ogar:Unknown`. This test PINS that lossy interim so the
1387+
// drop is documented, not silent — when §6 lands (predicates
1388+
// `ogar:Constrains`/`ogar:Onchange` + a path predicate), this test
1389+
// fails loudly and forces the implementer to update it. See ledger
1390+
// D-ATC2-KAUSAL-RUFF-GATED.
1391+
for k in [
1392+
ogar_vocab::KausalSpec::constrains(vec!["state".into()]),
1393+
ogar_vocab::KausalSpec::onchange(vec!["partner_id".into()]),
1394+
] {
1395+
let mut def = ogar_vocab::ActionDef::new("a", "p", "ogit-op/Foo");
1396+
def.kausal = Some(k);
1397+
let t = TripleEmitter::emit_action_def(&def);
1398+
assert!(
1399+
t.iter()
1400+
.any(|t| t.predicate == "ogar:kausalKind" && t.object == "ogar:Unknown"),
1401+
"interim: unwired Arm B variants label as ogar:Unknown"
1402+
);
1403+
// The field paths are NOT projected to TTL yet (the debt).
1404+
assert!(
1405+
!t.iter().any(|t| t.object == "state" || t.object == "partner_id"),
1406+
"interim: Arm B field paths are dropped at TTL emission (§6 deferred)"
1407+
);
1408+
}
1409+
}
1410+
13771411
#[test]
13781412
fn action_state_lifecycle_serialization() {
13791413
for (state, expected) in [

crates/ogar-vocab/src/lib.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -722,6 +722,18 @@ impl KausalSpec {
722722
pub fn depends(paths: Vec<String>) -> Self {
723723
Self::Depends { paths }
724724
}
725+
726+
/// Convenience: build a Constrains spec (`@api.constrains` targets).
727+
#[must_use]
728+
pub fn constrains(paths: Vec<String>) -> Self {
729+
Self::Constrains { paths }
730+
}
731+
732+
/// Convenience: build an Onchange spec (`@api.onchange` targets).
733+
#[must_use]
734+
pub fn onchange(paths: Vec<String>) -> Self {
735+
Self::Onchange { paths }
736+
}
725737
}
726738

727739
/// The four canonical Active Record relation kinds. Cross-ORM mapping:

docs/DISCOVERY-MAP.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1083,3 +1083,38 @@ isolation. The map's job is to keep them visible.
10831083
index — safe under Odoo semantics (`@api.depends` is method-level,
10841084
co-computed fields carry identical `depends_on`), revisit if a
10851085
frontend ever emits divergent `depends_on` per co-computed field.
1086+
1087+
- **D-ATC2-KAUSAL-RUFF-GATED** — 2026-07-06 `[G]`: AT-CARRY-2 arms B+D
1088+
landed (OGAR #169) after ruff #49 put `Function::{constrains,onchange}`
1089+
+ `Field::stored` on ruff main. Arm B: `KausalSpec::{Constrains,Onchange}`
1090+
populated in `lift_actions` ONLY when `kausal.is_none()` (SPEC §3b) — so
1091+
Depends (Arm A) always wins, Constrains beats Onchange; sourced purely
1092+
from the decorator fields, never `reads`/`writes` (regression
1093+
`lift_actions_depends_arm_a_regression_unaffected_by_arm_b`). Arm D:
1094+
`ComputedField.stored = field.stored.unwrap_or(false)` at both projection
1095+
sites (§5: Odoo's not-stored default). Post-merge verification: OGAR
1096+
workspace green against ruff main `9ef26c1``cargo build --workspace`
1097+
clean incl. `ogar-from-rails` (the float-on-main risk), `cargo test
1098+
--workspace` 479/0. Opus adversarial review: CLEAN impl, one P2 below.
1099+
- **HONEST CORRECTION to the D-ATC2-KAUSAL-AUTARK fuse rationale.** That
1100+
entry (and the B-arm commit message) claimed the exhaustive
1101+
`kausal_spec_match_is_exhaustive` guard makes *consumers* "loud break
1102+
instead of silently defaulting" on new variants. That is TRUE only
1103+
intra-crate. `KausalSpec` is `#[non_exhaustive]`, so every cross-crate
1104+
`match` is FORCED to carry a wildcard — the fuse cannot protect any
1105+
consumer outside ogar-vocab. The claim was overstated.
1106+
- **DEBT (P2, spec-deferred per §6): the TTL emitter silently mislabels
1107+
Arm B.** `ogar-emitter::kausal_triples` has no Constrains/Onchange arm;
1108+
both fall through `_ => ogar:Unknown` (lib.rs:779) and their field paths
1109+
are DROPPED at TTL emission. The IR struct is correct (consumers that
1110+
read `ActionDef.kausal` directly — e.g. the odoo-rs AT-CONSUME parity
1111+
pin — are unaffected); only the TTL projection is lossy. §6 defers the
1112+
emitter/SurrealQL wiring, so this is not a spec violation, but it is
1113+
actively-wrong (not merely missing) TTL. Converted from silent to
1114+
documented: characterization test
1115+
`kausal_constrains_onchange_currently_emit_unknown_pending_emitter_wiring`
1116+
PINS the interim `ogar:Unknown` + dropped-paths behaviour so it flips
1117+
LOUDLY when §6 lands (defining `ogar:Constrains`/`ogar:Onchange` kinds
1118+
+ a kausal path predicate — a governed vocab mint, NOT done here).
1119+
- Additive API gap closed: `KausalSpec::{constrains,onchange}` constructors
1120+
added to mirror `depends()`/`lifecycle()` (consumers + the test need them).

0 commit comments

Comments
 (0)