Skip to content

ruff_python_spo: add a plain-Python SPO harvest arm - #95

Merged
AdaWorldAPI merged 3 commits into
mainfrom
claude/python-spo-plain-arm
Aug 18, 2026
Merged

ruff_python_spo: add a plain-Python SPO harvest arm#95
AdaWorldAPI merged 3 commits into
mainfrom
claude/python-spo-plain-arm

Conversation

@AdaWorldAPI

Copy link
Copy Markdown
Owner

Summary

ruff_python_spo's extract() gates on Odoo markers (models.Model / _name / _inherit), so it returns an empty graph on any plain-Python codebase. Measured, not assumed: models=0 over monarch-initiative/dismech's 90-file resolver package.

functions::analyze_method — the part that actually earns the harvest (reads / writes / raises / traverses / calls / guarded_writes) — is already language-generic. Only the is_model gate blocked it. This adds a second entry point that drops the gate and reuses analyze_method unmodified.

  • extract_plain_from_source(source, module) -> ModelGraph
  • extract_plain(root, namespace) -> ModelGraph — recursive *.py walk mirroring collect_py; module name from path (export/sepio_export.pyexport.sepio_export, dot→underscore normalised as the Odoo arm does)

Every top-level class becomes a Model; bases become Model::inherits (reusing the frontend-agnostic carrier and the existing InheritsFrom lowering); module-level defs attach to a synthetic per-module Model so free functions keep has_function + their body facts; AnnAssign and simple literal assigns become Fields. Precedent: ruff_cpp_spo's walk_free_functions.

No new Predicate variants — the enum is closed and stays closed; expand.rs was read to confirm has_field/field_type/InheritsFrom lower generically rather than Odoo-coupled.

Measurement

$ cargo run -p ruff_python_spo --example plain_census -- /tmp/dismech-up/src/dismech
odoo baseline: models=0
plain: models=658 functions=1277 fields=3587 inherits_edges=546
  body facts: reads=1148 writes=635 raises=83 traverses=2 guarded_writes=6 calls=57

The models=0 baseline is measured in the same run over the same root, not asserted.

Genericity check on a second, unrelated corpus/home/user/WoA/woa (Flask + SQLAlchemy, no Odoo, no relation to dismech), run untouched:

odoo baseline: models=0
plain: models=241 functions=1687 fields=426 inherits_edges=24
  body facts: reads=56 writes=23 raises=24 traverses=1 guarded_writes=1 calls=95

Different ORM, different framework, different shape (function-heavy rather than field-heavy, as Flask blueprints should be), no code change required.

Deliberately conservative, with the gaps named

  • Field-type inference uses one rule (annotation head: Name, or Subscript's head) and falls to None for unions / X | None / forward-ref strings. dismech itself contains dataclasses, Enums, TypedDict-likes and heavy Optional[...] — the rule had to survive within-corpus variety before it ever met a second corpus.
  • Bare-assign literals only become fields for str/int/float/bool/list/dict/set/tuple. Forced by real code: logger = logging.getLogger(__name__) must not become a field.
  • Known gaps, not fixed here (outside the class/def shape this change scopes): module-level Assign constants are invisible — which notably means CURIE-shaped constants like sepio_export.py's HAS_PATHOPHYSIOLOGY = "dismech:has_pathophysiology" are not harvested — and nested classes aren't walked (same posture as the Odoo arm). Both are contained follow-ups.

Test plan

  • cargo test -p ruff_python_spo66/66 (57 pre-existing + 9 new), independently re-run
  • cargo clippy -p ruff_python_spo --all-targets -- -D warnings — clean
  • cargo fmt -p ruff_python_spo — clean
  • uvx prek run --from-ref <base> — clean (one typos auto-fix, committed separately, tests re-verified green)
  • Census output above independently reproduced before opening this PR

New tests assert exact counts (==, not >=) and cover: dataclass fields, base + method quartet, module-level def, unparsable source, empty module, literal-inference boundary, subscript/complex-annotation boundary, module-name normalisation, tree walk.


Generated by Claude Code

claude added 2 commits August 18, 2026 03:41
extract()/extract_with() gate every class behind Odoo markers
(walk::is_model_base, or a declared _name/_inherit) before it becomes
a Model. That gate provably drops the entire monarch-initiative/dismech
resolver: 586 classes, 878 module-level defs, 201 dataclasses, zero
Odoo markers -> extract() returns an empty graph over it (confirmed
below, not assumed).

extract_plain()/extract_plain_from_source() drop the gate: every
top-level class becomes a Model (no base-class/_name/_inherit check),
class bases lower to the existing frontend-agnostic Model::inherits
carrier (no new Predicate variant -- the enum is closed), and every
method routes through the SAME functions::analyze_method the Odoo arm
already uses. That function only ever looks at `self`, for/if/raise/
Assign statements, and a closed ORM-mutator name list -- none of it is
Odoo-specific, so it needed zero changes to become reusable (mirrors
ruff_cpp_spo's walk_free_functions precedent for a language-generic
frontend body-walker). Module-level defs attach to a synthetic
per-module Model. AnnAssign fields always record; bare `x = <literal>`
fields record only when the literal kind is trivially inferable
(str/int/float/bool/list/dict/set/tuple) -- never guessed.

Measured over dismech (cargo run -p ruff_python_spo --example
plain_census -- <dismech>/src/dismech):
  odoo baseline: models=0
  plain: models=658 functions=1277 fields=3587 inherits_edges=546
  body facts: reads=1148 writes=635 raises=83 traverses=2
              guarded_writes=6 calls=57

9 new unit tests in plain.rs; 66/66 ruff_python_spo tests pass;
clippy -D warnings clean; cargo fmt clean.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@cursor

cursor Bot commented Aug 18, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_913241e8-3a15-4aa4-a7bc-02c9aaccab04)

`<module>_<Class>` and the synthetic `<module>` model can collide --
`a/b.py`'s module model against a class `b` in `a.py` both normalise to
`a_b`. `ModelGraph::models` is a Vec so nothing merges silently at this
layer, but any consumer keying by name (the SPO expansion,
ogar-from-ruff's lift to ogar_vocab::Class) would collide. Counted
rather than assumed.

Measured: 0 duplicates across all three real corpora (dismech 658
models, WoA 241, ruff/scripts 58). The counter is not vacuous -- it
fires at 69 names covering 319 models over the ruff_linter fixture
tree, where e.g. ruff/RUF053.py genuinely declares `class C` 38 times.
That is a correct harvest of an adversarial fixture, not a naming
defect, and it gives the counter both halves of the falsifiability
pair: it can fire, and it stays silent on non-trivial real input.
@AdaWorldAPI
AdaWorldAPI merged commit 10fab88 into main Aug 18, 2026
27 of 30 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