Skip to content

fix(odoo): codex review on #532/#533 — selection replace/extend/poison + mro rustfmt#536

Merged
AdaWorldAPI merged 2 commits into
mainfrom
claude/odoo-codex-followups
Jun 18, 2026
Merged

fix(odoo): codex review on #532/#533 — selection replace/extend/poison + mro rustfmt#536
AdaWorldAPI merged 2 commits into
mainfrom
claude/odoo-codex-followups

Conversation

@AdaWorldAPI

@AdaWorldAPI AdaWorldAPI commented Jun 18, 2026

Copy link
Copy Markdown
Owner

Summary

Addresses the codex P2 review left on merged #532 (two real semantic bugs now live in main) and the rustfmt miss on merged #533.

Both #532 findings produce a wrong ASSERT $value IN [...] — the silent-data-rejection class that matters for "Odoo as faithful DDL."

#532 finding 1 — selection_add must not close a dynamic domain

Selection(selection_add=[('x','X')]) onto a base that is dynamic (Selection(STATE_CONST), selection='_compute_x') was emitting ['x'] as if it were the complete closed set — so the projected ASSERT $value IN ['x'] would reject every legal base value.

#532 finding 2 — full redeclarations must replace, not union

The cross-class merge unioned every fields.Selection declaration, so an _inherit class that redeclares the field with selection=[...] (replace/reorder) left stale keys from the earlier definition in the set.

The correct three-way semantics

_extract_selection(call) -> (base_dynamic, base_keys, add_keys):

idiom behaviour
static selection=[...] REPLACE — a redeclaration drops earlier keys
selection_add=[...] EXTEND — unions across classes
provided-but-dynamic base POISON — sentinel None; never closed; sticky

A closed ASSERT IN is emitted only when the whole domain is statically known. A poisoned field is recorded as None in the selections map and skipped at emission (selection_skip_open_domain stat).

#533mro.rs rustfmt

mro.rs landed un-cargo fmt'd (codex flagged the diff). Reformatted; 7/7 mro tests still pass.

Tests

  • python3 -m unittest tests.test_spo_enrich61/61 (was 57)
    • test_dynamic_base_plus_add_stays_poisoned (finding 1)
    • test_full_redeclaration_replaces_not_unions (finding 2)
    • test_open_domain_none_emits_nothing
    • extraction tests rewritten for the (dynamic, base, add) contract
  • cargo test -p lance-graph-ontology --lib mro7/7
  • rustfmt --check mro.rs — clean

EPIPHANIES.md prepends E-ODOO-SELECTION-REPLACE-EXTEND-POISON.

Note on the OGAR / ar_shape discussion

A parallel thread raised whether OGAR producer types should move into lance-graph-contract (a lance-graph-contract::ar_shape mirror, dropping ogar-proposal's owned MappingProposal mirror, adding ogar-from-ruff/ogar-from-rails). That's a separate architectural proposal — not in this PR, which is scoped to the concrete codex review feedback. Happy to take it up separately if wanted; #534's class_id_for_guid wiring is the natural predecessor.

Summary by CodeRabbit

Bug Fixes

  • Fixed selection domain handling in the Odoo blueprint extraction tool to properly distinguish between field replacement and extension semantics.
  • Improved handling of dynamic selection bases by treating them as open domains, preventing incorrect closed domain assertions.
  • Enhanced test coverage with 61 regression tests validating correct replacement vs. extension behavior and dynamic base handling.

@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@AdaWorldAPI, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 53 minutes and 6 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a2300dd7-8899-4137-8a68-382c746793a9

📥 Commits

Reviewing files that changed from the base of the PR and between ab385e6 and 618d1c6.

📒 Files selected for processing (3)
  • .claude/board/EPIPHANIES.md
  • tools/odoo-blueprint-extractor/odoo_blueprint_extractor/spo_enrich.py
  • tools/odoo-blueprint-extractor/tests/test_spo_enrich.py
📝 Walkthrough

Walkthrough

spo_enrich.py replaces the flat selection-key union logic with a three-way semantic model: selection= replaces a static domain, selection_add= extends it, and a dynamic/unresolvable base "poisons" the field so no closed key set is emitted. The Optional[List[str]] representation (with None = poisoned) propagates through build_all_facts, enrich, and run. Tests are rewritten to assert the new (base_dynamic, base_keys, add_keys) extraction contract and poison/replace/extend scan behavior. mro.rs receives unrelated whitespace reformatting.

Changes

spo_enrich Selection Domain Semantics Fix

Layer / File(s) Summary
_extract_selection contract and module docs
tools/odoo-blueprint-extractor/odoo_blueprint_extractor/spo_enrich.py
Module docs are rewritten to describe replace/extend/poison rules, and _extract_selection_values is replaced by _extract_selection returning (base_dynamic, base_keys, add_keys).
_scan_file accumulators and merge logic
tools/odoo-blueprint-extractor/odoo_blueprint_extractor/spo_enrich.py
Single local_selections accumulator is replaced by local_sel_replace, local_sel_extend, local_sel_poison; global merge loop is rewritten with sticky-None poison, replacement-not-union for selection=, and union-only-when-clean for selection_add=.
Public API types, emission loop, and stats
tools/odoo-blueprint-extractor/odoo_blueprint_extractor/spo_enrich.py
build_all_facts and enrich signatures updated to Optional[List[str]]; selection_skip_open_domain counter added; emission loop skips None fields; run excludes None entries from selections_entries count.
Extraction, scan, and emission tests + epiphany doc
tools/odoo-blueprint-extractor/tests/test_spo_enrich.py, .claude/board/EPIPHANIES.md
_classify_sel and TestSelectionExtraction rewritten for 3-tuple assertions; TestSelectionScan updated and extended with poison-stays-None and static-replacement cases; test_open_domain_none_emits_nothing added; EPIPHANIES.md records the fix.

mro.rs Formatting Cleanup

Layer / File(s) Summary
mro.rs whitespace reformatting
crates/lance-graph-ontology/src/odoo_blueprint/mro.rs
frontier VecDeque initialization and three test assert!/let expressions are reflowed to multi-line form without logic changes.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐇 A selection= replaces, neat and complete,
A selection_add= extends with a beat,
But a dynamic base? Poison! No key emits here—
None keeps the domain wide open and clear.
Sixty-one tests now guard what we found,
The rabbit hops onward on rock-solid ground! 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 41.94% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: a fix for selection domain handling semantics (replace/extend/poison) in Odoo code and a rustfmt issue in mro.rs.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ca67c5d5fc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

for field_name, keys in local_sel_replace.items():
if selections.get((mu, field_name), 0) is None:
continue
selections[(mu, field_name)] = list(keys)

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 Preserve selection_add keys across scan order

When a selection_add extension is scanned before the base/static declaration, this replacement assignment discards the accumulated extension keys. build_all_facts walks addon files with glob.iglob, so the scanner can see an _inherit class adding ('reviewed', ...) before the class that declares selection=[('draft', ...)]; the final map then becomes only ['draft'], and the emitted ASSERT IN rejects the legal added value. The replace and extend state needs to be tracked separately or extensions reapplied after replacements so selection_add remains an extension independent of traversal order.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 618d1c6. Correct and subtle — glob.iglob gives no order guarantee, and my replace/extend/poison fix collapsed the three into one accumulated list during the scan, so a selection_add seen before the base selection= got clobbered by the REPLACE.

Now replace / extend / poison are tracked separately per (model, field) for the whole scan (_new_sel_acc) and collapsed only at the end (_resolve_selections in build_all_facts): None if poisoned, else replace ++ extend de-duped (base first). selection_add lives in its own bucket and is never overwritten by a replace — order-independent.

Regression test test_selection_add_before_base_is_order_independent declares the extension class before the base and asserts the resolved domain is still [draft, posted, reviewed]. 62/62.

…ce/extend/poison + mro rustfmt

Two codex P2 findings on merged #532 were real semantic bugs (live in main),
both producing a WRONG `ASSERT $value IN [...]` — the silent-data-rejection
class that matters for faithful DDL — plus a rustfmt miss on merged #533.

# #532 finding 1 — selection_add must not close a dynamic domain

A `Selection(selection_add=[('x','X')])` extension onto a base that is
DYNAMIC (`Selection(STATE_CONST)` / `selection='_compute_x'`) was emitting
`['x']` as if it were the complete closed set, so the projected
`ASSERT $value IN ['x']` would reject every legal base value.

# #532 finding 2 — full redeclarations must replace, not union

The cross-class merge unioned EVERY `fields.Selection` declaration, so an
`_inherit` class that REDECLARES the whole field with `selection=[...]`
(replace/reorder) left stale keys from the earlier definition in the set.

# The correct three-way semantics

`_extract_selection(call) -> (base_dynamic, base_keys, add_keys)`:
  - static `selection=`  → REPLACE (a redeclaration drops earlier keys)
  - `selection_add=`     → EXTEND  (unions across classes)
  - provided-but-dynamic → POISON  (sentinel `None`; never closed; sticky)

A closed `ASSERT IN` is emitted only when the whole domain is statically
known. Poison is recorded as `None` in the `selections` map and skipped at
emission (`selection_skip_open_domain` stat).

# #533 — mro.rs rustfmt

`mro.rs` landed un-`cargo fmt`'d (codex flagged the diff). Reformatted;
7/7 mro tests still pass.

# Tests

  python3 -m unittest tests.test_spo_enrich : 61/61 (was 57)
    + test_dynamic_base_plus_add_stays_poisoned   (finding 1)
    + test_full_redeclaration_replaces_not_unions (finding 2)
    + test_open_domain_none_emits_nothing
    + rewrote extraction tests for the (dynamic, base, add) contract
  cargo test -p lance-graph-ontology --lib mro : 7/7
  rustfmt --check mro.rs : clean

EPIPHANIES: E-ODOO-SELECTION-REPLACE-EXTEND-POISON.
@AdaWorldAPI AdaWorldAPI force-pushed the claude/odoo-codex-followups branch from ca67c5d to ab385e6 Compare June 18, 2026 08:03
)

Third codex P2 finding on this PR: my replace/extend/poison fix collapsed
the three into a single accumulated list DURING the scan, so under
glob.iglob's unordered file walk a `selection_add=` extension scanned
BEFORE the base `selection=[...]` was clobbered by the REPLACE assignment
— losing the legal added key, so the emitted `ASSERT $value IN [...]`
would reject it.

Fix: track replace / extend / poison SEPARATELY per `(model, field)` for
the whole scan, collapse only at the end.

  - `_new_sel_acc()` — per-field record `{replace, extend, poison}`.
  - `_scan_file` accumulates into it: poison sets the flag; a static
    `selection=` sets `replace` (last full decl wins, drops stale keys);
    `selection_add=` unions into `extend` and is NEVER overwritten by a
    replace.
  - `_resolve_selections()` (run once at the end of `build_all_facts`)
    collapses to the final domain: `None` if poisoned, else
    `replace ++ extend` de-duped, base first. Order-independent.

`enrich` is unchanged — it still receives the resolved
`Optional[List[str]]` map. `_scan_sel` test helper resolves before
asserting (mirrors `build_all_facts`).

Tests (61 -> 62): + `test_selection_add_before_base_is_order_independent`
(the EXTENSION class declared BEFORE the base; resolved domain still
[draft, posted, reviewed] regardless of order). All prior selection
tests still green.

  python3 -m unittest tests.test_spo_enrich : 62/62

EPIPHANIES E-ODOO-SELECTION-REPLACE-EXTEND-POISON updated with the
order-independence refinement.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tools/odoo-blueprint-extractor/odoo_blueprint_extractor/spo_enrich.py (1)

545-552: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

build_all_facts local selections annotation is now inconsistent with the return type.

At Line 551 the function returns Optional[List[str]] values, but the local variable is still declared as Dict[Tuple[str, str], List[str]] (Line 564). _scan_file can write None, so this type hint is stale and misleading.

Proposed fix
-    selections: Dict[Tuple[str, str], List[str]] = {}
+    selections: Dict[Tuple[str, str], Optional[List[str]]] = {}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/odoo-blueprint-extractor/odoo_blueprint_extractor/spo_enrich.py` around
lines 545 - 552, The local variable `selections` in the `build_all_facts`
function has an inconsistent type annotation. The function's return type
declares the fourth element as `Dict[Tuple[str, str], Optional[List[str]]]` to
account for None values that `_scan_file` can write, but the `selections`
variable is still annotated as `Dict[Tuple[str, str], List[str]]` without
Optional. Update the type annotation of the `selections` variable to
`Dict[Tuple[str, str], Optional[List[str]]]` to match the return type annotation
and accurately reflect that the dictionary values can be None or a List of
strings.
🧹 Nitpick comments (1)
tools/odoo-blueprint-extractor/tests/test_spo_enrich.py (1)

689-766: ⚡ Quick win

Add regression cases for related= and dynamic selection_add open domains.

Current coverage (Line 709 onward) validates dynamic base poisoning, but it does not pin the same open-domain outcome for related= and non-literal selection_add. Adding both keeps the “never close unknown domain” contract explicit.

Suggested tests
 class TestSelectionScan(unittest.TestCase):
@@
     def test_dynamic_base_poisons_field_to_none(self):
@@
         self.assertEqual(sel, {("account_move", "s"): None})

+    def test_related_field_poisons_domain(self):
+        sel = _scan_sel(
+            "class Base:\n"
+            "    _name = 'account.move'\n"
+            "    state = fields.Selection([('draft','Draft')])\n"
+            "\n"
+            "class Ext:\n"
+            "    _inherit = 'account.move'\n"
+            "    state = fields.Selection(related='x.y')\n"
+        )
+        self.assertEqual(sel, {("account_move", "state"): None})
+
+    def test_dynamic_selection_add_poisons_domain(self):
+        sel = _scan_sel(
+            "class Base:\n"
+            "    _name = 'account.move'\n"
+            "    state = fields.Selection([('draft','Draft')])\n"
+            "\n"
+            "class Ext:\n"
+            "    _inherit = 'account.move'\n"
+            "    state = fields.Selection(selection_add=STATE_CONST)\n"
+        )
+        self.assertEqual(sel, {("account_move", "state"): None})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/odoo-blueprint-extractor/tests/test_spo_enrich.py` around lines 689 -
766, The TestSelectionScan class is missing regression test cases to explicitly
verify that Selection fields with related= parameters and dynamic selection_add
(non-literal expressions) both result in poisoned (None) domains. Add two new
test methods following the existing pattern
(test_dynamic_base_poisons_field_to_none,
test_dynamic_base_plus_add_stays_poisoned) that use the _scan_sel helper to
validate these scenarios. The first should test a Selection field with
related='some_method' and assert the domain is None, and the second should test
a Selection field with selection_add=some_variable (dynamic expression) and
assert the domain is None, ensuring the "never close unknown domain" contract is
maintained for these cases.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tools/odoo-blueprint-extractor/odoo_blueprint_extractor/spo_enrich.py`:
- Around line 191-229: The _extract_selection function incompletely detects
open-domain conditions. The function needs to be enhanced to: first, check for a
related= keyword argument in the loop over call.keywords and set base_dynamic to
True if found since related= makes domains dynamic; second, detect when add_node
exists but is not a static list or tuple (dynamic selection_add), which also
creates an open domain and should be reflected in the return value or in how
base_dynamic is calculated; and third, ensure that empty static selections like
selection=[] are properly handled as valid replacements rather than being
filtered out due to the non-empty base_keys gate. These edge cases mean the
poisoning logic driven only by base_dynamic is insufficient.

---

Outside diff comments:
In `@tools/odoo-blueprint-extractor/odoo_blueprint_extractor/spo_enrich.py`:
- Around line 545-552: The local variable `selections` in the `build_all_facts`
function has an inconsistent type annotation. The function's return type
declares the fourth element as `Dict[Tuple[str, str], Optional[List[str]]]` to
account for None values that `_scan_file` can write, but the `selections`
variable is still annotated as `Dict[Tuple[str, str], List[str]]` without
Optional. Update the type annotation of the `selections` variable to
`Dict[Tuple[str, str], Optional[List[str]]]` to match the return type annotation
and accurately reflect that the dictionary values can be None or a List of
strings.

---

Nitpick comments:
In `@tools/odoo-blueprint-extractor/tests/test_spo_enrich.py`:
- Around line 689-766: The TestSelectionScan class is missing regression test
cases to explicitly verify that Selection fields with related= parameters and
dynamic selection_add (non-literal expressions) both result in poisoned (None)
domains. Add two new test methods following the existing pattern
(test_dynamic_base_poisons_field_to_none,
test_dynamic_base_plus_add_stays_poisoned) that use the _scan_sel helper to
validate these scenarios. The first should test a Selection field with
related='some_method' and assert the domain is None, and the second should test
a Selection field with selection_add=some_variable (dynamic expression) and
assert the domain is None, ensuring the "never close unknown domain" contract is
maintained for these cases.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4a19a61e-560a-4d18-b5cd-00e88969d0b0

📥 Commits

Reviewing files that changed from the base of the PR and between 947481d and ab385e6.

📒 Files selected for processing (4)
  • .claude/board/EPIPHANIES.md
  • crates/lance-graph-ontology/src/odoo_blueprint/mro.rs
  • tools/odoo-blueprint-extractor/odoo_blueprint_extractor/spo_enrich.py
  • tools/odoo-blueprint-extractor/tests/test_spo_enrich.py

@AdaWorldAPI AdaWorldAPI merged commit 54da390 into main Jun 18, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants