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
10 changes: 10 additions & 0 deletions .claude/board/EPIPHANIES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
## 2026-06-18 — E-ODOO-VIRTUALLY-OVERRIDES-COMPUTED — the wishlist's last item is a *derived* ClassView relation, not harvest predicate #6

**Status:** FINDING. `virtually_overrides` — the one open odoo-rs wishlist item — lands as a **computed** Core/ClassView capability (`odoo_blueprint::mro`), closing the wishlist consistently with the Core-first correction (E-ODOO-CORE-FIRST-STRUCTURAL) directly above. Per the doctrine the `(has_function / inherits_from / virtually_overrides)` triad is the ClassView method-resolution manifest: `has_function` + `inherits_from` are *facts*; `virtually_overrides` is the **derivation** (`M.m` overrides `B.m` iff `M` declares `m`, `B` is up `M`'s `_inherit` chain, and `B` also declares `m`). Adding a `virtually_overrides` AST pass to `spo_enrich.py` would have been the drift; computing it from the manifest is the Core-correct move.

**Shape.** `resolve_overrides(methods_of, bases_of)` is a pure resolver over an abstract manifest — nearest-base-wins BFS up the `_inherit` chain, cycle-guarded, deterministic; equals Python C3 for the linear mixin chains Odoo uses, documented breadth-first approximation for diamonds. `manifest_from_curated_core()` wires the typed Core (`curated_entities()` methods + `structural::INHERITS`) as the authoritative source; a consumer holding the SPO corpus builds the same two maps from `has_function`+`inherits_from` (388 ObjectTypes incl. mixins) and gets the full resolution — identical resolver, broader manifest. `project_virtually_overrides` emits `(odoo:<child>.<m>, virtually_overrides, odoo:<base>.<m>)` for consumers wanting the corpus shape. 7 tests.

**Honest scope.** The curated-only manifest resolves a *small* set today — most curated models inherit *uncurated* mixins (`mail.activity.mixin`, `sequence.mixin`), so few overrides have both ends present. Not fabricated: the resolver is total and the breadth grows as mixins are curated OR the SPO manifest is supplied. This is the same Curated-leg / Extracted-leg convergence the structural correction established, now for the derived relation.

**Wishlist status:** all six items are now home-correct — P0/P1/P1b/P2/P3 as typed-Core facts + Extracted-leg breadth (#523/#526/#527/#530/#532), and `virtually_overrides` as this computed ClassView relation. None of them is a standalone flat-ndjson harvest predicate; the predicate-bolt-on cadence is fully retired. Cross-ref: `mro.rs` module doc; E-ODOO-CORE-FIRST-STRUCTURAL; the #531 `RegistryClassView` (the contract-side ClassView this resolution feeds).

## 2026-06-18 — E-ODOO-CORE-FIRST-STRUCTURAL — structural Odoo facts belong in the typed OGAR Core, NOT the SPO harvest; the predicate-bolt-on cadence was drift

**Status:** FINDING (Core-first correction; operator-confirmed). **This entry supersedes the *framing* of the four prior `E-ODOO-SPO-*` / `E-ODOO-EXTRACT-*` entries below — read it before extending their pattern.** Those entries are factually correct about what shipped, but a session reading them in sequence would conclude "the way to satisfy the odoo-rs wishlist is to add another `spo_enrich.py` predicate." That conclusion is the self-fulfilling drift this entry reverses.
Expand Down
6 changes: 6 additions & 0 deletions crates/lance-graph-ontology/src/odoo_blueprint/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@ pub mod op_emitter;
/// Extracted-leg breadth feeder. See the module doc for the full rationale.
pub mod structural;

/// `virtually_overrides` — the ClassView method-resolution relation,
/// **computed** from the `has_function` + `inherits_from` manifest, NOT
/// harvested. The Core-correct home for the wishlist's last item: a derived
/// MRO-precedence resolution, not `spo_enrich` predicate #6.
pub mod mro;

// ─── Top-level entity ─────────────────────────────────────────────────────

/// Which ORM base class the entity inherits from.
Expand Down
285 changes: 285 additions & 0 deletions crates/lance-graph-ontology/src/odoo_blueprint/mro.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,285 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: Copyright The Lance Authors

//! `virtually_overrides` — the ClassView method-resolution relation, **computed**
//! from the manifest, never harvested.
//!
//! # Why this is NOT `spo_enrich` predicate #6 (Core-first, 2026-06-18)
//!
//! The Core-first correction (`EPIPHANIES.md` E-ODOO-CORE-FIRST-STRUCTURAL)
//! drew the line: a structural fact that an AST pass *reads* belongs in the
//! typed Core, and a relation that the ClassView *derives* belongs in the
//! resolver — neither is a new flat-ndjson harvest predicate.
//!
//! `virtually_overrides` is the second kind. Per the doctrine, the
//! `(has_function / inherits_from / virtually_overrides)` triad is the
//! **ClassView method-resolution manifest**: `has_function` (which methods a
//! class declares) and `inherits_from` (the `_inherit` chain) are *facts*;
//! `virtually_overrides` is the **derivation** — model `M`'s method `m`
//! virtually-overrides base `B`'s `m` iff `M` declares `m`, `B` is reachable
//! up `M`'s `_inherit` chain, and `B` also declares `m`. This module computes
//! that relation. Adding a `virtually_overrides` AST pass to `spo_enrich.py`
//! would be the drift; computing it here is the Core-correct move.
//!
//! # Manifest source is the caller's choice; the resolver is pure
//!
//! [`resolve_overrides`] takes an abstract manifest (`methods_of` +
//! `bases_of`) so the same resolver serves either source:
//!
//! - the **typed Core** ([`manifest_from_curated_core`]) — authoritative, but
//! resolves an override only where *both* the child and the shadowed base
//! are curated `OdooEntity`s. Today most curated models inherit *uncurated*
//! mixins (`mail.activity.mixin`, `sequence.mixin`), so the curated-only
//! manifest resolves a small set — honest, not fabricated.
//! - the **SPO harvest manifest** — a consumer holding the corpus builds the
//! same two maps from `has_function` + `inherits_from` triples (388
//! ObjectTypes incl. mixins) and gets the full resolution. The resolver
//! code is identical; only the manifest breadth differs.
//!
//! # Precedence
//!
//! The shadowed base is the **nearest** one up a breadth-first walk of the
//! `_inherit` chain (sorted tie-break for determinism). For the *linear*
//! mixin chains Odoo uses in practice this equals Python's C3 MRO; for
//! diamonds it is the documented breadth-first approximation, not full C3.

use std::collections::{BTreeMap, BTreeSet, VecDeque};

use super::structural::SpoTriple;

/// One resolved method-override: `child_model.method` virtually-overrides the
/// same-named method on `base_model`, reached up the `_inherit` chain. All
/// names are underscored (corpus IRI local-part convention).
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct MethodOverride {
/// Overriding model (underscored, e.g. `account_move`).
pub child_model: String,
/// Method name shared by child and base (e.g. `_post`).
pub method: String,
/// Nearest shadowed base model (underscored, e.g. `sequence_mixin`).
pub base_model: String,
}

/// The method-resolution manifest, keyed by underscored model name:
/// `methods_of[m]` = the set of method names model `m` declares
/// (`has_function`); `bases_of[m]` = model `m`'s ordered `_inherit` bases
/// (`inherits_from`).
pub type MethodsOf = BTreeMap<String, BTreeSet<String>>;
/// See [`MethodsOf`].
pub type BasesOf = BTreeMap<String, Vec<String>>;

/// Compute `virtually_overrides` from the manifest. For every model `M` with
/// methods and every method `m` it declares, walk `M`'s `_inherit` chain
/// breadth-first and emit one override against the **nearest** base that also
/// declares `m`. Cycle-guarded; deterministic (sorted output).
#[must_use]
pub fn resolve_overrides(methods_of: &MethodsOf, bases_of: &BasesOf) -> Vec<MethodOverride> {
let mut out: Vec<MethodOverride> = Vec::new();

for (child, child_methods) in methods_of {
for method in child_methods {
if let Some(base) = nearest_base_declaring(child, method, methods_of, bases_of) {
out.push(MethodOverride {
child_model: child.clone(),
method: method.clone(),
base_model: base,
});
}
}
}

out.sort();
out.dedup();
out
}

/// BFS up `start`'s `_inherit` chain; return the first base (sorted tie-break
/// within a level) that declares `method`. `None` if no base declares it.
fn nearest_base_declaring(
start: &str,
method: &str,
methods_of: &MethodsOf,
bases_of: &BasesOf,
) -> Option<String> {
let mut seen: BTreeSet<String> = BTreeSet::new();
seen.insert(start.to_string());
// Frontier holds (model) at increasing chain distance; a BTreeSet per level
// gives the sorted tie-break, then feeds the next level.
let mut frontier: VecDeque<String> = bases_of

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 Run rustfmt before landing

In this new module, rustfmt --edition 2021 --check crates/lance-graph-ontology/src/odoo_blueprint/mro.rs reports diffs starting at this initializer and in several later test assertions. The repository instructions require Rust to be formatted with cargo fmt, so landing this file as-is leaves the added module failing the formatter check; please run rustfmt/cargo fmt on the new code.

Useful? React with 👍 / 👎.

.get(start)
.into_iter()
.flatten()
.cloned()
.collect();

while let Some(node) = frontier.pop_front() {
if !seen.insert(node.clone()) {
continue; // cycle / diamond re-visit guard
}
if methods_of.get(&node).is_some_and(|ms| ms.contains(method)) {
return Some(node);
}
// enqueue this node's bases (sorted for determinism), depth-extending
if let Some(bs) = bases_of.get(&node) {
let mut next: Vec<&String> = bs.iter().collect();
next.sort();
for b in next {
if !seen.contains(b) {
frontier.push_back(b.clone());
}
}
}
}
None
}

/// Project resolved overrides into SPO triples
/// `(odoo:<child>.<method>, virtually_overrides, odoo:<base>.<method>)`.
#[must_use]
pub fn project_virtually_overrides(overrides: &[MethodOverride]) -> Vec<SpoTriple> {
overrides
.iter()
.map(|o| {
(
format!("odoo:{}.{}", o.child_model, o.method),
"virtually_overrides",
format!("odoo:{}.{}", o.base_model, o.method),
)
})
.collect()
}

/// Build the manifest from the **typed Core** — `curated_entities()` methods
/// (`has_function`) + [`super::structural::INHERITS`] (`inherits_from`).
/// Authoritative but narrow: resolves an override only where both ends are
/// curated. Model + base names are underscored to match the corpus convention.
#[must_use]
pub fn manifest_from_curated_core() -> (MethodsOf, BasesOf) {
let underscore = |s: &str| s.replace('.', "_");

let mut methods_of: MethodsOf = BTreeMap::new();
for ent in super::class_signature::curated_entities() {
let m = underscore(ent.model_name);
let set = methods_of.entry(m).or_default();
for meth in ent.methods {
set.insert(meth.name.to_string());
}
}

let mut bases_of: BasesOf = BTreeMap::new();
for row in super::structural::INHERITS {
bases_of.insert(
underscore(row.model),
row.bases.iter().map(|b| underscore(b)).collect(),
);
}

(methods_of, bases_of)
}

#[cfg(test)]
mod tests {
use super::*;

fn methods(pairs: &[(&str, &[&str])]) -> MethodsOf {
pairs
.iter()
.map(|(m, ms)| (m.to_string(), ms.iter().map(|s| s.to_string()).collect()))
.collect()
}
fn bases(pairs: &[(&str, &[&str])]) -> BasesOf {
pairs
.iter()
.map(|(m, bs)| (m.to_string(), bs.iter().map(|s| s.to_string()).collect()))
.collect()
}

#[test]
fn child_overrides_direct_base() {
let m = methods(&[("child", &["_post", "own"]), ("base", &["_post"])]);
let b = bases(&[("child", &["base"])]);
let ov = resolve_overrides(&m, &b);
assert_eq!(
ov,
vec![MethodOverride {
child_model: "child".into(),
method: "_post".into(),
base_model: "base".into(),
}]
);
}

#[test]
fn method_only_on_child_is_not_an_override() {
// `own` exists only on child → no base shadows it → no override.
let m = methods(&[("child", &["own"]), ("base", &["_post"])]);
let b = bases(&[("child", &["base"])]);
assert!(resolve_overrides(&m, &b).is_empty());
}

#[test]
fn nearest_base_wins_over_farther_base() {
// child → mid → far ; all declare _x. Override targets `mid` (nearest).
let m = methods(&[
("child", &["_x"]),
("mid", &["_x"]),
("far", &["_x"]),
]);
let b = bases(&[("child", &["mid"]), ("mid", &["far"])]);
let ov = resolve_overrides(&m, &b);
assert_eq!(ov.len(), 2, "child→mid and mid→far both resolve");
// child's override target is the nearer `mid`, not `far`.
let child = ov.iter().find(|o| o.child_model == "child").unwrap();
assert_eq!(child.base_model, "mid");
}

#[test]
fn transitive_skip_when_nearest_lacks_the_method() {
// child → mid → far ; only `far` declares _x. child overrides far.
let m = methods(&[("child", &["_x"]), ("mid", &["other"]), ("far", &["_x"])]);
let b = bases(&[("child", &["mid"]), ("mid", &["far"])]);
let ov = resolve_overrides(&m, &b);
let child = ov.iter().find(|o| o.child_model == "child").unwrap();
assert_eq!(child.base_model, "far");
}

#[test]
fn cycle_in_chain_is_guarded() {
// a → b → a (pathological). Must terminate; a._x overrides b._x.
let m = methods(&[("a", &["_x"]), ("b", &["_x"])]);
let b = bases(&[("a", &["b"]), ("b", &["a"])]);
let ov = resolve_overrides(&m, &b); // must not hang
assert!(ov.iter().any(|o| o.child_model == "a" && o.base_model == "b"));
}

#[test]
fn projection_shape_is_method_iri_both_sides() {
let ov = vec![MethodOverride {
child_model: "account_move".into(),
method: "_post".into(),
base_model: "sequence_mixin".into(),
}];
let triples = project_virtually_overrides(&ov);
assert_eq!(
triples,
vec![(
"odoo:account_move._post".to_string(),
"virtually_overrides",
"odoo:sequence_mixin._post".to_string(),
)]
);
}

#[test]
fn typed_core_manifest_builds_and_resolver_runs() {
// Smoke: the curated-core manifest builds and the resolver terminates.
// The resolved set is honestly small/empty today (curated models mostly
// inherit UNCURATED mixins, so few overrides have both ends present) —
// a fuller SPO manifest resolves more. We assert the manifest is
// non-trivial and the call is total, not that overrides exist.
let (m, b) = manifest_from_curated_core();
assert!(!m.is_empty(), "curated core declares methods");
assert!(b.contains_key("account_move"), "account_move has _inherit bases");
let _ = resolve_overrides(&m, &b); // terminates, deterministic
}
}
Loading