Skip to content

feat(odoo-spo): inherits_from (P1b/ruff#19) + validation_kind (P2/ruff#21)#526

Merged
AdaWorldAPI merged 1 commit into
mainfrom
claude/odoo-spo-inherits-from-validation-kind
Jun 18, 2026
Merged

feat(odoo-spo): inherits_from (P1b/ruff#19) + validation_kind (P2/ruff#21)#526
AdaWorldAPI merged 1 commit into
mainfrom
claude/odoo-spo-inherits-from-validation-kind

Conversation

@AdaWorldAPI

Copy link
Copy Markdown
Owner

Summary

Extends spo_enrich.py with two more enrichment passes covering the remaining AdaWorldAPI/odoo-rs UPSTREAM_WISHLIST.md items. Mirrors the same wire shapes the ruff stack already ratified.

New predicate Wishlist item Cross-language ratification
inherits_from P1 (_inherit) + P1 (_inherits) ruff#19 (Rails STI + C++ class inheritance share (class, inherits_from, <base>))
validation_kind P2 (@api.constrains) ruff#21 (Predicate::ValidationKind, per-attribute typed validation)

What's new

inherits_from(odoo:<this>, inherits_from, odoo:<base>)

Wire shape identical to ruff#19's cross-language predicate. Sources:

  • _inherit = 'mail.thread' (single string)
  • _inherit = ['mail.thread', 'mail.activity.mixin'] (list)
  • _inherits = {'mail.thread': 'thread_id'} (delegation; dict keys lift)

Drops at scan time:

  • Odoo's "extend-in-place" idiom (_inherit == _name)
  • Bases not declared as ogit:ObjectType in the corpus (counted in inherits_skip_unknown_base)

Truth (0.95, 0.90) — class-level declaration is authoritative.

validation_kind(odoo:<model>.<method>, validation_kind, "<kind>")

Wire shape mirrors ruff#21's per-attribute keying, with subject flipped to the method IRI (Odoo's @api.constrains is method-level, not attribute-level). Five recognised kinds detected by conservative AST patterns inside @api.constrains-decorated method bodies:

kind detector
format re.match / re.fullmatch / re.search
uniqueness <rs>.search_count(...)
range < / > / <= / >= numeric comparisons
lookup <rs>.search(...) / <rs>.browse(...)
presence not <expr> / <expr> is None / <expr> == None

A body matching no pattern emits nothing — the existing raises triple still records that it can raise. Unknown kinds (forward-compat extension) silently dropped at emission. Truth (0.85, 0.75) — inferred.

Architecture

build_all_facts(addons_root) is a single-pass AST walk that returns the existing relmap plus the new inherits and constrains dicts. enrich() gains two optional kwargs (inherits=None, constrains=None) so existing callers keep working unchanged; the new emission loops run only when the facts are provided. Full backwards-compat.

Corpus regen — pending Odoo source

The shipped odoo_ontology.spo.ndjson does not yet carry the new triples — regenerating requires running the script against a live Odoo source tree (/home/user/odoo/addons), which is not present on this host:

python -m odoo_blueprint_extractor.spo_enrich \
  --corpus crates/lance-graph/src/graph/spo/odoo_ontology.spo.ndjson \
  --addons /home/user/odoo/addons

The Rust loader's predicate-histogram match arm gained inherits_from + validation_kind so a future regenerated corpus drops into the same harness without code changes. parses_all_triples count assertion stays at 23 701; re-locks the moment a session with the source re-runs enrichment.

Test plan

  • python -m unittest tests.test_spo_enrich32/32 OK (was 14)
    • 4 new inherits_from emission tests
    • 4 new validation_kind emission tests
    • 6 new AST classifier unit tests (synthetic Python source per kind)
    • 4 new inherit-extraction tests (_scan_file shape lock)
  • cargo test -p lance-graph --lib odoo_ontology9/9 OK
  • Corpus regenerated against live /home/user/odoo/addonspending next session with Odoo source

Consumer-side follow-up (separate PR on AdaWorldAPI/odoo-rs)

When the corpus regenerates, od_ontology::RecomputeDag automatically sees the new inheritance edges; the consumer can mark wishlist items P1 (#1 _inherit, #2 _inherits) + P2 (#3 validation_kind) as RESOLVED upstream and add a ClassView-style lookup over inherits_from for MRO flattening (the P2 / virtually_overrides ask remains for a future ClassView design session).


Generated by Claude Code

@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown

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 24 minutes and 58 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: b1527304-77a5-497a-9b95-83344a940fb9

📥 Commits

Reviewing files that changed from the base of the PR and between 00e33d5 and 7947487.

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

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: c946e9d0d8

ℹ️ 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".

Comment thread tools/odoo-blueprint-extractor/odoo_blueprint_extractor/spo_enrich.py Outdated
Comment thread tools/odoo-blueprint-extractor/odoo_blueprint_extractor/spo_enrich.py Outdated
Comment thread tools/odoo-blueprint-extractor/odoo_blueprint_extractor/spo_enrich.py Outdated
AdaWorldAPI pushed a commit that referenced this pull request Jun 18, 2026
… + extend constraint binding to _inherit-only classes

All three codex findings are real correctness issues. Fixing them tightens
the validation_kind classifier so a regenerated corpus does not pollute
downstream queries with false-positive kinds, and lifts the Odoo
"extend-in-place" idiom (the same shape #525 fixed for relational fields).

# codex #1 — `_inherit`-only classes' constraints attach to the bases

The original block flushed `local_constrains` inside `if model_name is not
None`, silently dropping `@api.constrains` methods on the common Odoo
extension form (`_inherit='account.move'` without `_name`). Now
`model_targets = [_name]` if set, else `list(_inherit)` underscored, and
constraints attach to *each* target.

Same shape #525 took for relational fields — kept consistent.

# codex #2 — `search_count(...) > 1` is uniqueness ONLY, not also range

The unconditional `Compare` walker added `range` for any
`< | > | <= | >=`, including the canonical uniqueness pattern
`<rs>.search_count(...) > 1`. That polluted every uniqueness method with
a stray `validation_kind=range`. Added `_is_uniqueness_call(left)` guard
so range is skipped when the LHS is a `search_count` call. Direct field-vs-
bound checks (`self.amount < 0`, `self.amount > 1000000`) still classify
as range.

# codex #3 — `not re.match(...)` is format ONLY, not also presence

The broad `UnaryOp(Not)` rule added `presence` for `if not re.match(...):`
and `if not <rs>.search(...):` — the negated-call wrap of format / lookup
checks, not presence checks. Restricted presence to truthiness of a
*value-like* operand (`Name` / `Attribute` / `Subscript`); negated calls
are skipped. Direct field truthiness (`if not self.partner:`) still
classifies as presence.

# Tests

8 new regression tests under `TestCodexFindings` (32 → 40 OK):

  - `test_constraints_bind_to_inherit_only_extension`
  - `test_constraints_bind_to_each_inherit_when_list`
  - `test_constraints_with_name_skip_inherit_targets`  (specificity)
  - `test_search_count_gt_is_only_uniqueness`
  - `test_field_range_check_still_classifies_range`  (specificity)
  - `test_negated_re_match_is_only_format`
  - `test_negated_search_is_only_lookup`
  - `test_direct_field_truthiness_is_still_presence`  (specificity)

Each kind has a paired specificity test ensuring the fix did not break
the canonical pattern.

  python3 -m unittest tests.test_spo_enrich : 40/40 OK (was 32)
…f#21)

Rebased on top of #525 (multi-emitter deep-reads + `_inherit`-only field
binding). All three codex P2 findings from the pre-rebase iteration of
#526 are baked in: `range` skipped when LHS is `search_count(...)`,
`presence` restricted to `not <Name|Attribute|Subscript>`, and
constraint binding mirrors #525's `model_names` decision (no mixin
broadcast).

# inherits_from (P1b, ruff#19 ratified shape)

Wire shape identical to C++ / Rails: `(odoo:<this>, inherits_from,
odoo:<base>)`.

Sources:
  - `_inherit = 'mail.thread'` (single string)
  - `_inherit = ['mail.thread', 'mail.activity.mixin']` (list)
  - `_inherits = {'mail.thread': 'thread_id'}` (delegation; dict keys)

Drops at scan time:
  - `_inherit == _name` ("extend-in-place" idiom)
  - `_inherit`-only classes (no `_name`) — the inheritance edge already
    exists at the original declaration site; extending in place does
    not create a new edge.

Bases not in the corpus's `ogit:ObjectType` set are skipped at emission
time + counted (`inherits_skip_unknown_base`).

Truth `(0.95, 0.90)` — class-level declaration is authoritative.

# validation_kind (P2, ruff#21 ratified shape)

Wire shape mirrors ruff#21's per-attribute keying, subject flipped to
the method IRI (Odoo's `@api.constrains` is method-level).

Recognised kinds:
  - `format`     — `re.match` / `re.fullmatch` / `re.search`
  - `uniqueness` — `<rs>.search_count(...)`
  - `range`      — `< | > | <= | >=` where LHS is NOT a `search_count`
                   call (codex P2 fix from pre-rebase #526)
  - `lookup`     — `<rs>.search(...)` / `<rs>.browse(...)`
  - `presence`   — `not <Name|Attribute|Subscript>` (codex P2 fix:
                   `not <Call>` is the negated-call wrap for
                   format/lookup, NOT presence) / `<expr> is None`

Truth `(0.85, 0.75)` — AST classification is inferred.

Constraint binding follows #525's `model_names`:
  - `_name = 'X'`                  → bind to `X` only
  - `_inherit = 'X'` (no `_name`)  → bind to `X` only
  - `_inherit = ['X','Y']` (no `_name`) → bind to `X` only (inherit[0])

No mixin broadcast — the same conservative rule #525 codex review
arrived at for fields.

# Corpus regen pending

The shipped `odoo_ontology.spo.ndjson` does not yet carry the new
triples — regenerating requires running the script against a live
Odoo source tree (`/home/user/odoo/addons`), which is not present on
this host. The Rust loader's predicate-histogram match arm gained
`inherits_from` + `validation_kind` so a future regenerated corpus
drops into the harness without code changes. The `parses_all_triples`
count assertion stays at 23 701 from #525; re-locks the moment a
session with the source re-runs enrichment.

# Files

  tools/odoo-blueprint-extractor/odoo_blueprint_extractor/spo_enrich.py:
    +helpers (_is_uniqueness_call, _is_api_constrains,
     _classify_constrains_body), +`inherits`/`constrains` kwargs on
    _scan_file + build_all_facts, +`_inherits` dict-key extraction,
    P1b + P2 emission loops, CLI status print extended.

  tools/odoo-blueprint-extractor/tests/test_spo_enrich.py:
    +18 tests over 5 classes: TestP1bInheritsFrom (8), TestP2ValidationKind
    (4), TestAstClassifier (6 — codex P2 specificity locks paired with
    canonical patterns), TestConstrainsScan (3 — model_names binding).

  crates/lance-graph/src/graph/spo/odoo_ontology.rs:
    +doc table row for `inherits_from` + `validation_kind`;
    +match arm in predicate_histogram_matches_extraction.

  .claude/board/EPIPHANIES.md: prepended E-ODOO-SPO-INHERITS-VALKIND.

# Tests

  python3 -m unittest tests.test_spo_enrich : 41/41 OK (was 23 post-#525)
  cargo test -p lance-graph --lib odoo_ontology : 11/11 OK
@AdaWorldAPI AdaWorldAPI force-pushed the claude/odoo-spo-inherits-from-validation-kind branch from c12a542 to 7947487 Compare June 18, 2026 03:56
@AdaWorldAPI AdaWorldAPI merged commit 01b9509 into main Jun 18, 2026
6 checks passed
AdaWorldAPI added a commit that referenced this pull request Jun 18, 2026
feat(odoo-spo): regenerate corpus with inherits_from + validation_kind (#526 follow-up)
AdaWorldAPI pushed a commit that referenced this pull request Jun 18, 2026
…dation_kind

Runs the deferred spo_enrich corpus regen (flagged by E-ODOO-SPO-INHERITS-VALKIND
as "corpus regen pending Odoo source") against /home/user/odoo/addons. Completes
the odoo-rs UPSTREAM_WISHLIST: P0 deep-reads (#525) was already materialized;
this lands the P1b/P2 layer #526 added to the extractor but had not yet emitted.

Corpus odoo_ontology.spo.ndjson: +413 triples, 24 166 -> 24 579, purely additive
(0 deletions):
  - +166 inherits_from  (ruff#19 cross-language inheritance predicate; _inherit /
    _inherits bases)
  - +247 validation_kind (ruff#21 per-@api.constrains kind)
Idempotent on the P0 layer: 0 new target/inverse_name/deep reads_field.

odoo_ontology.rs: count assert 24_166 -> 24_579; histogram test gains
inherits_from=166 + validation_kind=247 asserts (now locked); provenance doc
updated. Python extractor 41/41 green. Counts verified against the regenerated
corpus histogram.

Board: LATEST_STATE narrative entry; EPIPHANIES E-ODOO-SPO-INHERITS-VALKIND
Status flipped (per-corpus CONJECTURE -> FINDING).

Co-Authored-By: Claude <noreply@anthropic.com>
https://claude.ai/code/session_016b33swuXE23hKtqxsHu9p1
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