From c95d2a2d837beedce54937a1a508bac38839943d Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 7 Jul 2026 21:39:39 +0000 Subject: [PATCH 1/3] vocab: mint ogar:Constrains/Onchange KausalKind + path predicates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SPEC-MINT-ARM-B-TTL §1: register the two Arm B KausalSpec variants in the ogar.ttl OWL registry in lockstep with the emitter (Council-S3 correction — there IS a separate registry, contra the earlier claim). Same pattern as the five pre-existing ogar:KausalKind instances and the ogar:kausalDependsPath/-Context precedent. Council-B1 nebenbefund folded in: ogar:Unknown was declared only as ogar:EnumSourceKind, never as ogar:KausalKind, even though the emitter's _ => ogar:Unknown wildcard has always emitted it as a kausalKind object — a pre-existing declaration gap, now closed alongside this mint. --- vocab/ogar.ttl | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/vocab/ogar.ttl b/vocab/ogar.ttl index 95a25b6..08d9a30 100644 --- a/vocab/ogar.ttl +++ b/vocab/ogar.ttl @@ -303,6 +303,9 @@ ogar:LifecycleTrigger a ogar:KausalKind . ogar:Depends a ogar:KausalKind . ogar:ContextDepends a ogar:KausalKind . ogar:External a ogar:KausalKind . +ogar:Constrains a ogar:KausalKind . +ogar:Onchange a ogar:KausalKind . +ogar:Unknown a ogar:KausalKind . ogar:guardField a rdf:Property ; rdfs:domain ogar:ActionDef ; rdfs:range xsd:string . ogar:guardValue a rdf:Property ; rdfs:domain ogar:ActionDef ; rdfs:range xsd:string . ogar:triggerEvent a rdf:Property ; rdfs:domain ogar:ActionDef ; rdfs:range xsd:string . @@ -311,6 +314,8 @@ ogar:triggerEvent a rdf:Property ; rdfs:domain ogar:ActionDef ; rdfs:range # action is NOT inferred as a ComputedField. Codex review. ogar:kausalDependsPath a rdf:Property ; rdfs:domain ogar:ActionDef ; rdfs:range xsd:string . ogar:kausalDependsContext a rdf:Property ; rdfs:domain ogar:ActionDef ; rdfs:range xsd:string . +ogar:kausalConstrainsPath a rdf:Property ; rdfs:domain ogar:ActionDef ; rdfs:range xsd:string . +ogar:kausalOnchangePath a rdf:Property ; rdfs:domain ogar:ActionDef ; rdfs:range xsd:string . # Enumerations ogar:ActionSubjectKind a owl:Class . From 95125f849c0568edc75a2df3c722bc2d2f83e6e4 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 7 Jul 2026 21:39:52 +0000 Subject: [PATCH 2/3] ogar-emitter: wire Constrains/Onchange kausal_triples arms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SPEC-MINT-ARM-B-TTL §2: add KausalSpec::Constrains/Onchange match arms to kausal_triples, exactly the Depends shape (lib.rs:836-848) — one ogar:kausalKind triple plus one ogar:kausalConstrainsPath / ogar:kausalOnchangePath triple per field path. The `_ => ogar:Unknown` wildcard stays, now covering only genuine future variants. Council-S5 pathology: dotted paths (Odoo silently drops these in @api.constrains/@api.onchange, odoo/orm/decorators.py:106-108/213-215) are drop-with-no-triple for that path only; the kausalKind triple still emits. Filter applied to both new arms. Test-flip (§3): the characterization test kausal_constrains_onchange_currently_emit_unknown_pending_emitter_wiring is replaced by two tests — kausal_constrains_onchange_emit_declared_kinds_and_paths (positive kausalKind + path assertions, plus the Konflations-Fuse negative guards: no ogar:Unknown, no ogar:dependsPath, no ogar:kausalDependsPath) and kausal_constrains_onchange_drop_dotted_paths_without_triple. Also extends kausal_spec_variants_emit_distinct_kinds to cover both new variants. Roundtrip: verified ogar-adapter-ttl has no kausal-consumer/parser (its module doc lists ActionDef/KausalSpec under "Not yet supported"), so TTL remains write-only for kausal; no roundtrip case added. --- crates/ogar-emitter/src/lib.rs | 162 +++++++++++++++++++++++++++++---- 1 file changed, 142 insertions(+), 20 deletions(-) diff --git a/crates/ogar-emitter/src/lib.rs b/crates/ogar-emitter/src/lib.rs index a5c5041..03dd0f1 100644 --- a/crates/ogar-emitter/src/lib.rs +++ b/crates/ogar-emitter/src/lib.rs @@ -847,6 +847,48 @@ fn kausal_triples(action_subject: &str, k: &ogar_vocab::KausalSpec) -> Vec { + let mut v = vec![Triple::new( + action_subject, + "ogar:kausalKind", + "ogar:Constrains", + )]; + for p in paths { + if p.contains('.') { + continue; + } + v.push(Triple::new( + action_subject, + "ogar:kausalConstrainsPath", + p.clone(), + )); + } + v + } + KausalSpec::Onchange { paths } => { + let mut v = vec![Triple::new( + action_subject, + "ogar:kausalKind", + "ogar:Onchange", + )]; + for p in paths { + if p.contains('.') { + continue; + } + v.push(Triple::new( + action_subject, + "ogar:kausalOnchangePath", + p.clone(), + )); + } + v + } KausalSpec::ContextDepends { keys } => { let mut v = vec![Triple::new( action_subject, @@ -1697,39 +1739,119 @@ mod tests { t.iter() .any(|t| t.predicate == "ogar:kausalKind" && t.object == "ogar:External") ); + + def.kausal = Some(ogar_vocab::KausalSpec::constrains(vec!["state".into()])); + let t = TripleEmitter::emit_action_def(&def); + assert!( + t.iter() + .any(|t| t.predicate == "ogar:kausalKind" && t.object == "ogar:Constrains") + ); + assert!( + t.iter() + .any(|t| t.predicate == "ogar:kausalConstrainsPath" && t.object == "state") + ); + + def.kausal = Some(ogar_vocab::KausalSpec::onchange(vec!["partner_id".into()])); + let t = TripleEmitter::emit_action_def(&def); + assert!( + t.iter() + .any(|t| t.predicate == "ogar:kausalKind" && t.object == "ogar:Onchange") + ); + assert!( + t.iter() + .any(|t| t.predicate == "ogar:kausalOnchangePath" && t.object == "partner_id") + ); } #[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()]), + fn kausal_constrains_onchange_emit_declared_kinds_and_paths() { + // §6-Mint (SPEC-MINT-ARM-B-TTL): Constrains/Onchange now emit their + // own declared kausalKind + kausal-scoped path predicates, replacing + // the prior ogar:Unknown characterization (D-ATC2-KAUSAL-RUFF-GATED, + // debt resolved per 5+3-Council GO). + for (k, expected_kind, expected_path_predicate, path) in [ + ( + ogar_vocab::KausalSpec::constrains(vec!["state".into()]), + "ogar:Constrains", + "ogar:kausalConstrainsPath", + "state", + ), + ( + ogar_vocab::KausalSpec::onchange(vec!["partner_id".into()]), + "ogar:Onchange", + "ogar:kausalOnchangePath", + "partner_id", + ), ] { 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 == expected_kind), + "expected kausalKind {expected_kind}" + ); assert!( t.iter() + .any(|t| t.predicate == expected_path_predicate && t.object == path), + "expected {expected_path_predicate} triple for {path}" + ); + + // Negative guards (Konflations-Fuse, Codex-Regel 2026-06-04): + // Arm B must never fall back to Unknown, and must never reuse + // the ComputedField or Depends path predicates. + assert!( + !t.iter() .any(|t| t.predicate == "ogar:kausalKind" && t.object == "ogar:Unknown"), - "interim: unwired Arm B variants label as ogar:Unknown" + "Arm B variants must not label as ogar:Unknown" + ); + assert!( + !t.iter().any(|t| t.predicate == "ogar:dependsPath"), + "Arm B variants must not emit the ComputedField ogar:dependsPath" + ); + assert!( + !t.iter().any(|t| t.predicate == "ogar:kausalDependsPath"), + "Arm B variants must not emit the Depends-scoped ogar:kausalDependsPath" + ); + } + } + + #[test] + fn kausal_constrains_onchange_drop_dotted_paths_without_triple() { + // Council-S5 pathology: Odoo silently ignores dotted paths in + // `@api.constrains` / `@api.onchange` (odoo/orm/decorators.py: + // 106-108/213-215). Mirror that: drop-with-no-triple for the dotted + // path, but the kausalKind triple still stands — NOT ogar:Unknown. + for (k, expected_kind, expected_path_predicate) in [ + ( + ogar_vocab::KausalSpec::constrains(vec!["partner_id.name".into()]), + "ogar:Constrains", + "ogar:kausalConstrainsPath", + ), + ( + ogar_vocab::KausalSpec::onchange(vec!["partner_id.name".into()]), + "ogar:Onchange", + "ogar:kausalOnchangePath", + ), + ] { + 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 == expected_kind), + "kausalKind triple must remain even when the only path is dotted" + ); + assert!( + !t.iter().any(|t| t.predicate == expected_path_predicate), + "dotted path must be dropped, not emitted as a path triple" ); - // 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)" + .any(|t| t.predicate == "ogar:kausalKind" && t.object == "ogar:Unknown"), + "dropped dotted path must not reclassify the kind as ogar:Unknown" ); } } From 1358d50de9e0f7ddeb109857fae4d44425ca1ea2 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 7 Jul 2026 21:39:58 +0000 Subject: [PATCH 3/3] =?UTF-8?q?docs:=20ledger=20=E2=80=94=20D-ATC2-KAUSAL-?= =?UTF-8?q?RUFF-GATED=20debt=20resolved=20(=C2=A76-Mint)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Append-only correction paragraph on the existing 2026-07-06 entry, analogous to its HONEST CORRECTION sub-bullet: the emitter-debt P2 is closed by SPEC-MINT-ARM-B-TTL — kausal_triples arms + ogar.ttl registry now in lockstep, the characterization test flipped to a positive test as designed, 5+3-Council GO. No new ad-hoc tag, no deletion. --- docs/DISCOVERY-MAP.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/docs/DISCOVERY-MAP.md b/docs/DISCOVERY-MAP.md index d9eb591..afc71e1 100644 --- a/docs/DISCOVERY-MAP.md +++ b/docs/DISCOVERY-MAP.md @@ -1125,3 +1125,33 @@ isolation. The map's job is to keep them visible. + 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). + - **DEBT RESOLVED (§6-Mint, 2026-07-07, 5+3-Council GO; + SPEC-MINT-ARM-B-TTL).** The P2 above is closed: `ogar-emitter:: + kausal_triples` now carries `KausalSpec::{Constrains,Onchange}` arms + emitting `ogar:Constrains` / `ogar:Onchange` + `ogar:kausalConstrainsPath` + / `ogar:kausalOnchangePath` per field path (the `_ => ogar:Unknown` + wildcard still stands, now covering only genuine future variants). + `vocab/ogar.ttl` carries the matching registry in lockstep (two new + `ogar:KausalKind` instances + two new `rdf:Property` path predicates, + same pattern as the five pre-existing instances) — the Council-S3 + "no separate registry" correction from this same entry, now actually + closed. Council-B1 nebenbefund folded in: `ogar:Unknown` is now ALSO + declared `a ogar:KausalKind` (it was only ever `a ogar:EnumSourceKind`; + the wildcard fallback was emitting an undeclared kind IRI all along — + a pre-existing declaration gap, not new behaviour). Dotted paths + (Council-S5: Odoo silently drops them in `@api.constrains` / + `@api.onchange`, `odoo/orm/decorators.py:106-108/213-215`) are + drop-with-no-triple for that path; the `kausalKind` triple still + stands, NOT `ogar:Unknown`. The characterization test + `kausal_constrains_onchange_currently_emit_unknown_pending_emitter_wiring` + flipped as designed — replaced by + `kausal_constrains_onchange_emit_declared_kinds_and_paths` (positive + kausalKind + path assertions, plus the Konflations-Fuse negative + guards: no `ogar:Unknown`, no `ogar:dependsPath`, no + `ogar:kausalDependsPath`) and + `kausal_constrains_onchange_drop_dotted_paths_without_triple`; + `kausal_spec_variants_emit_distinct_kinds` extended to cover both + variants. Roundtrip: `ogar-adapter-ttl` has no kausal-consumer/parser + (verified — its module doc lists `ActionDef` / `KausalSpec` under + "Not yet supported"), so TTL stays write-only for kausal; no + roundtrip case added, per the spec's documented fallback.