Skip to content

Commit ffacf21

Browse files
authored
Merge pull request #149 from AdaWorldAPI/claude/medcare-bridge-lance-graph-wmx76z
ogar-from-ruff: lift Odoo `_inherit` onto Class.mixins (transpile-chain LEG 2)
2 parents 75d955b + e8679f5 commit ffacf21

2 files changed

Lines changed: 69 additions & 0 deletions

File tree

crates/ogar-from-ruff/src/lib.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
//! | `scopes` | `scopes` / `default_scope` | `Scope` / `Scopes` → `Class.scopes`; `DefaultScope` → `Class.default_scope` |
3636
//! | `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) |
3737
//! | `sti.inherits_from` | `parent` | STI parent — matches `Class.parent` slot |
38+
//! | `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 |
3839
//! | `functions` | `Vec<ActionDef>` (DO-arm) | [`lift_actions`] — one `ActionDef` per method; standalone, not on `Class` |
3940
//!
4041
//! Fields NOT lifted today (no equivalent on the ruff side OR no clean
@@ -177,6 +178,15 @@ fn lift_model_with_language(model: &Model, language: Language) -> Class {
177178
class.canonical_concept = Some(canonical_concept(&model.name));
178179
class.associations = model.associations.iter().filter_map(lift_association).collect();
179180
class.mixins = lift_mixins(model);
181+
// Odoo `_inherit` (multi-parent mixin composition) lands on the same
182+
// mixins shelf the vocab designates for it — `Class::mixins` doc names
183+
// `_inherit = 'mixin.thread'`, and `Class::inheritance` explicitly
184+
// excludes mixins ("Mixins / concerns are a SEPARATE axis"). The ruff
185+
// frontend already normalised the names (dot→underscore→verbatim),
186+
// deduped, and excluded the bare-`_inherit` reopen self-edge. Only the
187+
// Odoo frontend populates `Model::inherits`, so this is a no-op for the
188+
// Rails (`sti`) and C++ (`bases`) producers — hence unconditional.
189+
class.mixins.extend(model.inherits.iter().cloned());
180190
class.attributes = model.attributes.iter().filter_map(lift_attribute).collect();
181191
class.enums = model.attributes.iter().filter_map(lift_enum).collect();
182192
class.scopes = model.scopes.iter().filter_map(lift_scope).collect();
@@ -885,6 +895,36 @@ mod tests {
885895
assert!(class.computed_fields.is_empty());
886896
}
887897

898+
#[test]
899+
fn odoo_inherit_lands_on_mixins_not_parent() {
900+
// The is_a input end of the transpile chain: `ruff_python_spo`
901+
// populates the frontend-agnostic `Model::inherits` from Odoo
902+
// `_inherit` (self-reopen already excluded upstream). The lift routes
903+
// it to `Class::mixins` — the vocab's designated multi-parent shelf —
904+
// NOT to the single `parent` / `inheritance` is_a spine (those stay
905+
// Rails-STI-shaped; the vocab excludes mixins from `inheritance`).
906+
let mut m = mk_odoo_model();
907+
m.inherits = vec!["mail_thread".to_string(), "mail_activity_mixin".to_string()];
908+
let class = lift_model_python(&m);
909+
910+
// Both parents preserved on the mixins shelf, order kept.
911+
assert!(class.mixins.contains(&"mail_thread".to_string()));
912+
assert!(class.mixins.contains(&"mail_activity_mixin".to_string()));
913+
// The is_a spine is untouched — Odoo `_inherit` is NOT STI.
914+
assert_eq!(class.parent, None);
915+
assert_eq!(class.inheritance, Inheritance::Root);
916+
}
917+
918+
#[test]
919+
fn empty_inherits_adds_no_mixins() {
920+
// Frontend-agnostic no-op: the Rails / C++ producers never populate
921+
// `Model::inherits`, so the lift must not fabricate mixins. A bare
922+
// model (no concerns, no acts_as, no `_inherit`) lifts with an empty
923+
// mixins shelf — the `inherits` extension contributes nothing.
924+
let class = lift_model(&Model::new("Bare"));
925+
assert!(class.mixins.is_empty());
926+
}
927+
888928
#[test]
889929
fn lift_inheritance_concrete_from_sti_parent() {
890930
// mk_model's StiInfo has inherits_from = Some("Issue").

docs/DISCOVERY-MAP.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -710,3 +710,32 @@ isolation. The map's job is to keep them visible.
710710
no guard needed beyond this line. Cite this entry instead of
711711
re-deriving; a "two ledgers disagree" claim checks the le-contract /
712712
primer line FIRST.
713+
714+
- **D-OGAR-ODOO-INHERIT-MIXINS (transpile-chain LEG 2; 2026-07-04;
715+
[G] — CODED + tested):** the middle leg of the operator's transpile
716+
chain (`ruff *_spo harvest → ogar-from-ruff lift → CompiledClass →
717+
ClassView × FieldMask → askama render`). ruff PR #40 shipped the input
718+
end: a frontend-agnostic `ruff_spo_triplet::Model.inherits: Vec<String>`
719+
populated by the Odoo frontend from `_inherit` (self-reopen self-edge
720+
excluded upstream). `ogar-from-ruff` previously consumed only
721+
`sti.inherits_from → Class.parent` (Rails STI) and **dropped**
722+
`Model.inherits`, so the Odoo is_a linkage never reached the Core.
723+
**Resolution (this commit):** bump the ruff pin to merged main
724+
(`61ce2b49`), then `class.mixins.extend(model.inherits)` in
725+
`lift_model_with_language`. **The multi-parent "decision required" I
726+
forwarded was already answered by the vocab:** `Class::mixins` doc
727+
explicitly names `_inherit = 'mixin.thread'`, and `Class::inheritance`
728+
doc states "Mixins / concerns are a SEPARATE axis … never folded in
729+
here." So Odoo `_inherit``mixins` (the multi-parent `Vec` shelf),
730+
NOT `parent`/`inheritance` (STI single-parent spine) — no `parent`
731+
widening, no information loss, no vocab-axis violation. **Consequence
732+
for LEG 3 (V3/D-VCW-3 render):** the FieldMask compose step must union
733+
over `parent``mixins` when materialising Odoo inherited fields —
734+
the is_a spine and the mixin shelf are BOTH inheritance surfaces for
735+
the render; `render_rows` itself stays concept-local (the union is the
736+
caller's compose-time `FieldMask::inherit` bitwise-or). 48 tests green
737+
in `ogar-from-ruff` (2 new: `odoo_inherit_lands_on_mixins_not_parent`,
738+
`empty_inherits_adds_no_mixins`); workspace `cargo check` clean;
739+
clippy clean. Supersedes the "widen parent vs primary+relation"
740+
framing in the 2026-07-04 lance-graph broadcast — the vocab's mixins
741+
axis is the answer.

0 commit comments

Comments
 (0)