Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ cargo run -p od-ontology --example emit_account_move # print the DDL

## Slice 2 — `account.move` + `account.move.line` + `res.partner` + `res.company`

The smallest meaningful expansion. **2 739 triples across 4 models** surface
The smallest meaningful expansion. **3 065 triples across 4 models** surface
three shapes single-model focus can't:

1. **Back-ref resolution within the focus set.** `account_move.line_ids` lowers
Expand All @@ -100,7 +100,7 @@ three shapes single-model focus can't:
cargo run -p od-ontology --example emit_slice_2
# -- summary: 4 tables, 369 fields, 401 functions (deferred bodies),
# 64 events (reactive + guards), 24 unresolved-child audit notes —
# from 2 739 triples
# from 3 065 triples
```

### Typed-lift bridge — `RelationMap`
Expand Down Expand Up @@ -238,7 +238,7 @@ odoo-rs/
│ └── tests/ # slice-1 + slice-2 against real fixtures
└── data/
├── account_move.spo.ndjson # 1 647-triple slice-1 fixture
└── slice_2.spo.ndjson # 2 739-triple slice-2 fixture (4 models)
└── slice_2.spo.ndjson # 3 065-triple slice-2 fixture (4 models)
```

## Provenance
Expand Down
45 changes: 45 additions & 0 deletions crates/od-ontology/examples/real_corpus_probe.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//! **Real-corpus probe** — compile a REAL Odoo addon tree through the V3
//! substrate (`compile_source`), no toy fixtures (5+3 council R5 finding:
//! every prior `compile_source` call site was an inline snippet).
//!
//! Run: `cargo run -p od-ontology --example real_corpus_probe`
//! (env `ODOO_MODELS` overrides the addon dir; defaults to the account
//! addon of a sibling odoo checkout).
//!
//! Measured 2026-07-07 on /home/user/odoo addons/account/models (55 files,
//! incl. the 7380-line account_move.py):
//! files=55 models=71 attrs=642 assocs=293 actions=1496 kausal=347
//! classid_resolved=14
fn main() {
let dir = std::env::var("ODOO_MODELS")
.unwrap_or_else(|_| "/home/user/odoo/addons/account/models".into());
let Ok(entries) = std::fs::read_dir(&dir) else {
eprintln!("real_corpus_probe: {dir} not present — run next to an odoo checkout");
return;
Comment on lines +14 to +18

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Fail the probe when the corpus path is absent

When ODOO_MODELS is unset outside the author's machine, this hard-coded /home/user/odoo default misses the documented sibling checkout and then returns exit 0. That makes cargo run -p od-ontology --example real_corpus_probe look successful while compiling zero real files, so the real-corpus proof can be silently skipped in local or CI verification runs; derive the default relative to the repo or exit with an error.

Useful? React with 👍 / 👎.

};
let (mut files, mut models, mut actions, mut kausal, mut classid_hits, mut attrs, mut assocs) =
(0usize, 0usize, 0usize, 0usize, 0usize, 0usize, 0usize);
for e in entries {
let p = e.expect("dir entry").path();
if p.extension().is_none_or(|x| x != "py") {
continue;
}
let src = std::fs::read_to_string(&p).expect("read source");
files += 1;
let ccs = od_ontology::compile_source(&src);
models += ccs.len();
for cc in &ccs {
attrs += cc.class.attributes.len();
assocs += cc.class.associations.len();
actions += cc.actions.len();
kausal += cc.actions.iter().filter(|a| a.kausal.is_some()).count();
if cc.facet.facet_classid() != 0 {
classid_hits += 1;
}
}
}
println!(
"REAL-CORPUS: files={files} models={models} attrs={attrs} assocs={assocs} \
actions={actions} kausal={kausal} classid_resolved={classid_hits}"
);
}
7 changes: 5 additions & 2 deletions crates/od-ontology/src/ogar_actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ use crate::triple::{member_of, model_of, strip_ns, Triple};
/// write-backs, `_search_*`, plain helpers) are intentionally not lowered —
/// they are not the reactive/guard behavioral arm. The `action_*` state-
/// crossing arm is deferred (see the module docs — a producer-side fact gap).
#[deprecated(since = "0.5.0", note = "the DO-arm lives in OGAR (operator ruling 2026-07-06): actions arrive on `CompiledClass.actions` via `compile_source`; this corpus-side duplicate pipeline is retained only as the kausal-parity witness until AT-CARRY-2 lands upstream")]
#[deprecated(since = "0.5.0", note = "the DO-arm lives in OGAR (operator ruling 2026-07-06): actions arrive on `CompiledClass.actions` via `compile_source`; this corpus-side pipeline is retained as (1) the kausal-parity witness and (2) the implementation behind `od-codegen --actions` (which reads ndjson corpora, not source) — do NOT remove until that CLI mode is migrated or retired")]
#[must_use]
pub fn corpus_to_actions(triples: &[Triple]) -> Vec<ActionDef> {
let mut methods: BTreeSet<&str> = BTreeSet::new();
Expand Down Expand Up @@ -116,7 +116,7 @@ pub fn corpus_to_actions(triples: &[Triple]) -> Vec<ActionDef> {
/// lowering without depending on `ogar-vocab` or matching its
/// `#[non_exhaustive]` enums. Order mirrors [`corpus_to_actions`].
#[allow(deprecated)] // internally rides corpus_to_actions, deprecated together
#[deprecated(since = "0.5.0", note = "the DO-arm lives in OGAR (operator ruling 2026-07-06): actions arrive on `CompiledClass.actions` via `compile_source`; this corpus-side duplicate pipeline is retained only as the kausal-parity witness until AT-CARRY-2 lands upstream")]
#[deprecated(since = "0.5.0", note = "the DO-arm lives in OGAR (operator ruling 2026-07-06): actions arrive on `CompiledClass.actions` via `compile_source`; this corpus-side pipeline is retained as (1) the kausal-parity witness and (2) the implementation behind `od-codegen --actions` (which reads ndjson corpora, not source) — do NOT remove until that CLI mode is migrated or retired")]
#[must_use]
pub fn corpus_action_rows(triples: &[Triple]) -> Vec<(String, String, String, String)> {
corpus_to_actions(triples)
Expand All @@ -140,6 +140,9 @@ pub fn corpus_action_rows(triples: &[Triple]) -> Vec<(String, String, String, St
}

#[cfg(test)]
// The module under test IS the deprecated corpus witness — its tests
// legitimately exercise the deprecated surface (that is their whole job).
#[allow(deprecated)]
mod tests {
use super::*;

Expand Down
64 changes: 64 additions & 0 deletions crates/od-ontology/tests/real_source_compile.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
//! **Real-source compile pin** — the V3 substrate over a REAL Odoo model file,
//! not a toy fixture.
//!
//! 5+3 council finding (R5, 2026-07-07): every `compile_source` call site was
//! an inline 4–20-line snippet, so "the transpile is complete" had never been
//! exercised against real Odoo source. This test compiles the VERBATIM
//! `addons/account/models/account_move.py` (7 380 lines, vendored at
//! `data/account_move_real.py` exactly like the `account_move_form_view.xml`
//! fixture precedent) through `compile_source` and pins what comes out.
//!
//! The full-addon sweep lives in `examples/real_corpus_probe.rs` (55 files →
//! 71 models / 1 496 actions / 347 kausal, measured 2026-07-07); this test is
//! the committed, always-running slice of that run.

use od_ontology::compile_source;

#[test]
fn real_account_move_compiles_through_the_v3_substrate() {
let src = include_str!("../../../data/account_move_real.py");
assert!(src.lines().count() > 7_000, "the fixture is the real file, not a stub");

let compiled = compile_source(src);

// The file defines exactly account.move (account.move.line lives in its
// own file). Counts below are drift-fuses from the 2026-07-07 run — a
// change means the frontend's harvest or the vendored fixture moved;
// re-run, re-pin, and say so in the commit.
assert_eq!(compiled.len(), 1, "models harvested from the real file");

let move_cc = compiled
.iter()
.find(|cc| cc.class.name == "account_move")
.expect("account.move is lifted");

// Identity: the canon-high render classid rides the facet.
assert_eq!(move_cc.facet.facet_classid(), 0x0202_0002);

// THINK arm: the real model is field-rich (109 declared fields in the
// briefing; the frontend's harvest of THIS file's declarations lands
// attributes + associations — pinned from the run).
assert_eq!(move_cc.class.attributes.len(), 104, "attrs (2026-07-07 fuse)");
assert_eq!(move_cc.class.associations.len(), 38, "assocs (2026-07-07 fuse)");
assert_eq!(move_cc.class.computed_fields.len(), 93, "computed fields (2026-07-07 fuse)");

// DO arm: hundreds of real methods arrive as ActionDefs, a substantial
// subset with a causal trigger (depends/constrains/onchange).
assert_eq!(move_cc.actions.len(), 354, "DO-arm ActionDefs (2026-07-07 fuse)");
assert_eq!(
move_cc.actions.iter().filter(|a| a.kausal.is_some()).count(),
92,
"kausal-carrying subset (2026-07-07 fuse)"
);

// Spot identity: the flagship compute exists with its body facts.
let amount_compute = move_cc
.actions
.iter()
.find(|a| a.predicate == "_compute_amount")
.expect("the real _compute_amount arrives as an ActionDef");
assert!(
!amount_compute.reads.is_empty() || amount_compute.kausal.is_some(),
"_compute_amount carries facts (reads or kausal)"
);
}
5 changes: 4 additions & 1 deletion crates/od-ontology/tests/recipe_redundancy_probe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ fn strip_ns(iri: &str) -> &str {

fn corpus() -> Vec<Triple> {
// Same richest committed corpus the `corpus_to_actions` unit test uses:
// account_move + account_move_line + res_partner + res_company (2 739 rows).
// account_move + account_move_line + res_partner + res_company (3 065 rows).
let ndjson = include_str!("../../../data/slice_2.spo.ndjson");
parse_ndjson(ndjson).expect("slice 2 corpus parses")
}
Expand Down Expand Up @@ -249,6 +249,9 @@ fn ar_lifecycle_override_redundancy() {
// with the REAL lift (`corpus_action_rows`), so the two classifications can
// never silently drift, and every guard must render ONE recipe detail.
{
// Deprecated witness consumed on purpose: this probe pins the corpus
// arm against the real lift (see ogar_actions.rs deprecation note).
#[allow(deprecated)]
let rows = od_ontology::corpus_action_rows(&triples);
let guard_rows: Vec<&(String, String, String, String)> =
rows.iter().filter(|r| r.2 == "guard").collect();
Expand Down
Loading