From cd11c8957a62e92c309caaee1b772201e004a483 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 22 Jun 2026 19:54:12 +0000 Subject: [PATCH] =?UTF-8?q?feat(od-ontology):=20od-codegen=20--actions=20?= =?UTF-8?q?=E2=80=94=20operator=20inspection=20of=20the=20behavioral=20low?= =?UTF-8?q?ering?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors the --classids flag for the behavioral arm (corpus_to_actions, #9): one `model.method kind detail` row per ActionDef, kind = depends|guard, respecting --focus. Lets an operator inspect the lifecycle lowering on any corpus the same way --classids exposes the classid map. $ od-codegen data/slice_2.spo.ndjson --focus account_move --actions account_move._check_expense_ids guard before_save (reject) account_move._compute_amount depends account_move.line_ids, … New `corpus_action_rows(&[Triple]) -> Vec<(model, predicate, kind, detail)>` keeps the ogar_vocab enum match INSIDE od_ontology, so the bin prints the rows without depending on ogar-vocab or matching its #[non_exhaustive] enums. The --actions arm is self-gated under ogar-emit (flag recognized without the feature; prints a rebuild directive). Verified end-to-end: 358 action rows on the full slice_2 corpus. 39 lib + 7 bin tests (incl. action_rows_format_depends_and_guard + parse_args_actions_flag), clippy 0, fmt-clean. Co-Authored-By: Claude Opus 4.8 --- crates/od-ontology/src/bin/od_codegen.rs | 38 +++++++++++++++ crates/od-ontology/src/lib.rs | 2 +- crates/od-ontology/src/ogar_actions.rs | 59 ++++++++++++++++++++++++ 3 files changed, 98 insertions(+), 1 deletion(-) diff --git a/crates/od-ontology/src/bin/od_codegen.rs b/crates/od-ontology/src/bin/od_codegen.rs index dec4f7a..b719430 100644 --- a/crates/od-ontology/src/bin/od_codegen.rs +++ b/crates/od-ontology/src/bin/od_codegen.rs @@ -174,6 +174,29 @@ fn main() { } } + // ── 4b. `--actions` — print the behavioral-arm lowering (ActionDef) ── + if parsed.actions { + #[cfg(feature = "ogar-emit")] + { + for (model, predicate, kind, detail) in od_ontology::corpus_action_rows(&triples) { + // Respect --focus: only the focused models, when given. + if !focus_refs.is_empty() && !focus_refs.iter().any(|&f| f == model) { + continue; + } + println!("{model}.{predicate}\t{kind}\t{detail}"); + } + return; + } + #[cfg(not(feature = "ogar-emit"))] + { + eprintln!( + "error: --actions requires the `ogar-emit` feature \ + (rebuild: cargo build -p od-ontology --features cli,ogar-emit)" + ); + process::exit(1); + } + } + let sql = schema.to_sql(); // ── 5. `--validate` (deferred stub) ── @@ -204,6 +227,7 @@ struct ParsedArgs { stats: bool, validate: bool, classids: bool, + actions: bool, help: bool, } @@ -227,6 +251,7 @@ fn parse_args(args: &[String]) -> Result { let mut stats = false; let mut validate = false; let mut classids = false; + let mut actions = false; let mut help = false; let mut i = 0; while i < args.len() { @@ -235,6 +260,7 @@ fn parse_args(args: &[String]) -> Result { "--stats" => stats = true, "--validate" => validate = true, "--classids" => classids = true, + "--actions" => actions = true, "-f" | "--focus" => { i += 1; let value = args.get(i).ok_or_else(|| { @@ -277,6 +303,7 @@ fn parse_args(args: &[String]) -> Result { stats, validate, classids, + actions, help, }) } @@ -340,6 +367,9 @@ OPTIONS: Currently a no-op stub (warns and passes through). --classids Print the `table → canonical OGAR render classid (0xAABBCCDD)` map instead of DDL. Requires the `ogar-emit` feature. + --actions Print the behavioral-arm lowering — one + `model.method kind detail` row per ActionDef + (kind = depends|guard). Respects --focus. Requires `ogar-emit`. -h, --help Show this help. EXIT CODES (lance-graph#512 convention): @@ -362,6 +392,7 @@ mod tests { assert!(!p.stats); assert!(!p.validate); assert!(!p.classids); + assert!(!p.actions); } #[test] @@ -370,6 +401,13 @@ mod tests { assert!(p.classids); } + #[test] + fn parse_args_actions_flag() { + let p = parse_args(&["--actions".into()]).unwrap(); + assert!(p.actions); + assert!(!p.classids); + } + #[test] fn parse_args_focus_splits_on_comma_and_trims() { let p = diff --git a/crates/od-ontology/src/lib.rs b/crates/od-ontology/src/lib.rs index a9b9b5a..d72948d 100644 --- a/crates/od-ontology/src/lib.rs +++ b/crates/od-ontology/src/lib.rs @@ -67,7 +67,7 @@ pub use ogar_bridge::{ /// Behavioral-arm lowering — Odoo's reactive lifecycle → `ogar_vocab::ActionDef` /// (the sibling of [`schema_to_classes`]). See `specs/W3-BEHAVIORAL-ARM-SCOPE.md`. #[cfg(feature = "ogar-emit")] -pub use ogar_actions::corpus_to_actions; +pub use ogar_actions::{corpus_action_rows, corpus_to_actions}; /// Re-export the canonical OGAR codebook constants (e.g. /// `class_ids::BILLABLE_WORK_ENTRY`) so consumers can symbol-bind to the diff --git a/crates/od-ontology/src/ogar_actions.rs b/crates/od-ontology/src/ogar_actions.rs index 2a50329..dd16e0d 100644 --- a/crates/od-ontology/src/ogar_actions.rs +++ b/crates/od-ontology/src/ogar_actions.rs @@ -105,6 +105,35 @@ pub fn corpus_to_actions(triples: &[Triple]) -> Vec { out } +/// Inspection-friendly rows over [`corpus_to_actions`] — one +/// `(object_class, predicate, kind, detail)` per `ActionDef`, where `kind` is +/// `"depends"` or `"guard"` and `detail` is the joined dependency paths or the +/// `"event (policy)"` of the guard. Keeps the `ogar_vocab` enum match *inside* +/// this crate so a consumer (the `od-codegen --actions` CLI) can print the +/// lowering without depending on `ogar-vocab` or matching its +/// `#[non_exhaustive]` enums. Order mirrors [`corpus_to_actions`]. +#[must_use] +pub fn corpus_action_rows(triples: &[Triple]) -> Vec<(String, String, String, String)> { + corpus_to_actions(triples) + .into_iter() + .map(|a| { + let (kind, detail) = match &a.kausal { + Some(KausalSpec::Depends { paths }) => ("depends".to_string(), paths.join(", ")), + Some(KausalSpec::LifecycleTrigger { event }) => { + let policy = match a.guard_failure_policy { + Some(GuardFailurePolicy::Reject) => "reject", + Some(GuardFailurePolicy::Postponable) => "postpone", + _ => "?", + }; + ("guard".to_string(), format!("{event} ({policy})")) + } + _ => ("other".to_string(), String::new()), + }; + (a.object_class, a.predicate, kind, detail) + }) + .collect() +} + #[cfg(test)] mod tests { use super::*; @@ -264,4 +293,34 @@ mod tests { } } } + + #[test] + fn action_rows_format_depends_and_guard() { + let triples = vec![ + t("odoo:m", "has_function", "odoo:m._compute_x"), + t("odoo:m._compute_x", "reads_field", "odoo:m.a"), + t("odoo:m._compute_x", "reads_field", "odoo:m.b"), + t("odoo:m", "has_function", "odoo:m._check_y"), + t("odoo:m._check_y", "raises", "exc:ValidationError"), + ]; + let rows = corpus_action_rows(&triples); + // sorted by method identity: _check_y before _compute_x + assert_eq!( + rows, + vec![ + ( + "m".to_string(), + "_check_y".to_string(), + "guard".to_string(), + "before_save (reject)".to_string() + ), + ( + "m".to_string(), + "_compute_x".to_string(), + "depends".to_string(), + "m.a, m.b".to_string() + ), + ] + ); + } }