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
40 changes: 40 additions & 0 deletions crates/ogar-from-ruff/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
//! | `scopes` | `scopes` / `default_scope` | `Scope` / `Scopes` → `Class.scopes`; `DefaultScope` → `Class.default_scope` |
//! | `acts_as` | `mixins` | rendered as `acts_as_<variant>` so they survive on the same shelf as concerns (`ogar-vocab` has no separate `acts_as` slot) |
//! | `sti.inherits_from` | `parent` | STI parent — matches `Class.parent` slot |
//! | `inherits` | `mixins` (appended) | Odoo `_inherit` multi-parent mixin composition — the vocab's `mixins` doc names `_inherit`; the `inheritance` axis excludes mixins. Frontend-agnostic field, populated only by the Odoo frontend |
//! | `functions` | `Vec<ActionDef>` (DO-arm) | [`lift_actions`] — one `ActionDef` per method; standalone, not on `Class` |
//!
//! Fields NOT lifted today (no equivalent on the ruff side OR no clean
Expand Down Expand Up @@ -177,6 +178,15 @@ fn lift_model_with_language(model: &Model, language: Language) -> Class {
class.canonical_concept = Some(canonical_concept(&model.name));
class.associations = model.associations.iter().filter_map(lift_association).collect();
class.mixins = lift_mixins(model);
// Odoo `_inherit` (multi-parent mixin composition) lands on the same
// mixins shelf the vocab designates for it — `Class::mixins` doc names
// `_inherit = 'mixin.thread'`, and `Class::inheritance` explicitly
// excludes mixins ("Mixins / concerns are a SEPARATE axis"). The ruff
// frontend already normalised the names (dot→underscore→verbatim),
// deduped, and excluded the bare-`_inherit` reopen self-edge. Only the
// Odoo frontend populates `Model::inherits`, so this is a no-op for the
// Rails (`sti`) and C++ (`bases`) producers — hence unconditional.
class.mixins.extend(model.inherits.iter().cloned());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Pin ruff before reading Model::inherits

This access assumes the ruff PR #40 shape where ruff_spo_triplet::Model has an inherits field, but crates/ogar-from-ruff/Cargo.toml still depends on ruff_spo_triplet via the floating branch = "main" and this repo commits no Cargo.lock. On a clean build that resolves main to a revision before/without that field, ogar-from-ruff no longer compiles at this line; the commit message says the dependency is bumped to 61ce2b49, so this should be pinned to that rev (or otherwise locked) with the source change.

Useful? React with 👍 / 👎.

class.attributes = model.attributes.iter().filter_map(lift_attribute).collect();
class.enums = model.attributes.iter().filter_map(lift_enum).collect();
class.scopes = model.scopes.iter().filter_map(lift_scope).collect();
Expand Down Expand Up @@ -885,6 +895,36 @@ mod tests {
assert!(class.computed_fields.is_empty());
}

#[test]
fn odoo_inherit_lands_on_mixins_not_parent() {
// The is_a input end of the transpile chain: `ruff_python_spo`
// populates the frontend-agnostic `Model::inherits` from Odoo
// `_inherit` (self-reopen already excluded upstream). The lift routes
// it to `Class::mixins` — the vocab's designated multi-parent shelf —
// NOT to the single `parent` / `inheritance` is_a spine (those stay
// Rails-STI-shaped; the vocab excludes mixins from `inheritance`).
let mut m = mk_odoo_model();
m.inherits = vec!["mail_thread".to_string(), "mail_activity_mixin".to_string()];
let class = lift_model_python(&m);

// Both parents preserved on the mixins shelf, order kept.
assert!(class.mixins.contains(&"mail_thread".to_string()));
assert!(class.mixins.contains(&"mail_activity_mixin".to_string()));
// The is_a spine is untouched — Odoo `_inherit` is NOT STI.
assert_eq!(class.parent, None);
assert_eq!(class.inheritance, Inheritance::Root);
}

#[test]
fn empty_inherits_adds_no_mixins() {
// Frontend-agnostic no-op: the Rails / C++ producers never populate
// `Model::inherits`, so the lift must not fabricate mixins. A bare
// model (no concerns, no acts_as, no `_inherit`) lifts with an empty
// mixins shelf — the `inherits` extension contributes nothing.
let class = lift_model(&Model::new("Bare"));
assert!(class.mixins.is_empty());
}

#[test]
fn lift_inheritance_concrete_from_sti_parent() {
// mk_model's StiInfo has inherits_from = Some("Issue").
Expand Down
29 changes: 29 additions & 0 deletions docs/DISCOVERY-MAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -710,3 +710,32 @@ isolation. The map's job is to keep them visible.
no guard needed beyond this line. Cite this entry instead of
re-deriving; a "two ledgers disagree" claim checks the le-contract /
primer line FIRST.

- **D-OGAR-ODOO-INHERIT-MIXINS (transpile-chain LEG 2; 2026-07-04;
[G] — CODED + tested):** the middle leg of the operator's transpile
chain (`ruff *_spo harvest → ogar-from-ruff lift → CompiledClass →
ClassView × FieldMask → askama render`). ruff PR #40 shipped the input
end: a frontend-agnostic `ruff_spo_triplet::Model.inherits: Vec<String>`
populated by the Odoo frontend from `_inherit` (self-reopen self-edge
excluded upstream). `ogar-from-ruff` previously consumed only
`sti.inherits_from → Class.parent` (Rails STI) and **dropped**
`Model.inherits`, so the Odoo is_a linkage never reached the Core.
**Resolution (this commit):** bump the ruff pin to merged main
(`61ce2b49`), then `class.mixins.extend(model.inherits)` in
`lift_model_with_language`. **The multi-parent "decision required" I
forwarded was already answered by the vocab:** `Class::mixins` doc
explicitly names `_inherit = 'mixin.thread'`, and `Class::inheritance`
doc states "Mixins / concerns are a SEPARATE axis … never folded in
here." So Odoo `_inherit` → `mixins` (the multi-parent `Vec` shelf),
NOT `parent`/`inheritance` (STI single-parent spine) — no `parent`
widening, no information loss, no vocab-axis violation. **Consequence
for LEG 3 (V3/D-VCW-3 render):** the FieldMask compose step must union
over `parent` ∪ `mixins` when materialising Odoo inherited fields —
the is_a spine and the mixin shelf are BOTH inheritance surfaces for
the render; `render_rows` itself stays concept-local (the union is the
caller's compose-time `FieldMask::inherit` bitwise-or). 48 tests green
in `ogar-from-ruff` (2 new: `odoo_inherit_lands_on_mixins_not_parent`,
`empty_inherits_adds_no_mixins`); workspace `cargo check` clean;
clippy clean. Supersedes the "widen parent vs primary+relation"
framing in the 2026-07-04 lance-graph broadcast — the vocab's mixins
axis is the answer.
Loading