Repository navigation
Conversation
…ocal defs (Graphify-Labs#2732) `@pytest.fixture` reaches the extractor as the bare head symbol `fixture`, becomes a sourceless stub, and _rewire_unique_stub_nodes collapsed it onto the corpus's one top-level `def fixture()`: an unrelated helper in another file, which then topped god_nodes() (24 decorator edges, degree 25, on a 13-file repro). Same for `from pytest import fixture` and `import pytest as pt`. The extractor now reads each Python file's module-level imports once (_python_import_bindings) and records on the decorator `references` edge the module the decorator was imported from (metadata.ref_module). The rewire gains a per-edge guard, _other_module_candidate, beside _cross_language_candidate: an edge with ref_module only moves onto a definition whose file is that module (m.py / m/__init__.py, dotted modules matched as a path suffix). Stub ids are unchanged, so `@app.route` still targets `route`; corpus decorators (`import myhelpers` -> `@myhelpers.retry`) still link; edges with no import evidence follow the existing rules. Complements Graphify-Labs#2356, which handles locally assigned receivers and deliberately leaves imported ones on bare names. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks for the pull request, @hementhu. A maintainer will review it soon. Want to talk it through while it is in review? Come join us on our Discord server. For longer-form discussion there is also GitHub Discussions. A couple of things that speed up review: make sure the test suite passes on Python 3.10 and 3.13, and that the change keeps extraction deterministic. |
There was a problem hiding this comment.
Graphify reviewed this change.
Worth a look — the grounded gate found no coupling regressions or blocking issues, but 2 advisory finding(s) below merit a look before merge.
Not checked: tests were not run; formal verification proved 0 of 2 changed function(s) (2 not verified).
Formal verification. PR-changed functions: 0/2 verified (0 proven, 0 may-equivalent, 0 distinguished) · 2 not verified (2 unsupported).
Not verified on this run: \_rewire\_unique\_stub\_nodes (unsupported), \_extract\_generic (unsupported).
Graphify review — findings
Fixes decorators like @pytest.fixture collapsing onto an unrelated same-named local def fixture() during the corpus-wide stub rewire. The extractor now records the imported module on each Python decorator edge as ref_module, using module-level import and from … import bindings, and _other_module_candidate refuses any rewire target whose file isn't that module (m.py or m/__init__.py). Decorators with no import evidence, or reached through relative imports, keep the old unique-definition behaviour. The module match is strict, so a decorator resolved to a package re-export, an imported class or object (via the module.name fallback), or a multi-level attribute can lose its correct rewire or still mis-bind.
Worth a look
- Package re-exported decorators no longer rewire to their real definition —
graphify/extract.py:4064· Escalate · medium- NOT verified (no proof, no reproducing execution) — needs human review
- Decorator on an imported class/object is given a fake submodule as ref_module, which blocks correct rewires —
graphify/extractors/engine.py:10325· Escalate · medium- NOT verified (no proof, no reproducing execution) — needs human review
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 3588 functions depend on the 702 functions this change touches.
Health — this change adds coupling hotspots:
- worse:
extract()— 989 callers, 59 callees - worse:
_extract_generic()— 20 callers, 47 callees - worse:
_rewire_unique_stub_nodes()— 14 callers, 11 callees - worse:
walk()— 1 callers, 77 callees
Verification — 3588 functions in the blast radius were not formally verified this run (proofs are advisory here).
Gate & verification
graphify gate
PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.
Advisory (not blocking):
- verification_scope: 3343 function(s) in the blast radius were not formally verified this run
Test selection
Test selection
203 of 437 test file(s) selected (46%) via static blast radius.
tests/test_affected_overrides.py— impacttests/test_astro_extraction.py— impacttests/test_astro_import_ids.py— impacttests/test_blade_extractor.py— impacttests/test_build.py— impacttests/test_builtin_global_type_refs.py— impacttests/test_c_include_resolution.py— impacttests/test_cache.py— impacttests/test_case_sensitive_resolution.py— impacttests/test_cjs_module_extension.py— impacttests/test_cobol_extractor.py— impacttests/test_cpp_method_declarations.py— impacttests/test_cpp_nested_and_cli.py— impacttests/test_cpp_objc_cross_file_calls.py— impacttests/test_cpp_template_type_params.py— impacttests/test_cross_extension_reexport_self_cycle.py— impacttests/test_cross_language_call_resolution.py— impacttests/test_cross_repo_external_call_guards.py— impacttests/test_cross_repo_member_calls.py— impacttests/test_csharp_call_site_generic_args.py— impacttests/test_csharp_delegate_declarations.py— impacttests/test_csharp_enum_members.py— impacttests/test_csharp_field_generic_args.py— impacttests/test_csharp_foreach_loop_variable.py— impacttests/test_csharp_generic_callsites.py— impacttests/test_csharp_interface_dispatch.py— impacttests/test_csharp_member_calls.py— impacttests/test_csharp_member_chain_calls.py— impacttests/test_csharp_member_nodes.py— impacttests/test_csharp_object_creation.py— impacttests/test_csharp_partial_classes.py— impacttests/test_csharp_tuple_type_refs.py— impacttests/test_csharp_type_resolution.py— impacttests/test_csharp_underscore_collisions.py— impacttests/test_csharp_var_call_return_type.py— impacttests/test_csharp_verbatim_interpolation.py— impacttests/test_definition_file_portability.py— impacttests/test_detect.py— impacttests/test_dotnet.py— impacttests/test_duplicate_annotation_edges.py— impacttests/test_ejs_extractor.py— impacttests/test_elixir_defdelegate.py— impacttests/test_elixir_import_resolution.py— impacttests/test_elixir_keyword_def_calls.py— impacttests/test_elixir_qualified_calls.py— impacttests/test_elixir_unqualified_call_scope.py— impacttests/test_erlang_extractor.py— impacttests/test_ext_tagged_id_clash.py— impacttests/test_extract.py— impacttests/test_extract_cache_location.py— impact- … and 153 more
Selection is safe under the controlled-regression assumption; always-run tests + a periodic full run are the backstops. Advisory — it never changes the check verdict.
Docs that may be stale (advisory)
docs/superpowers/plans/2026-05-04-incremental-updates-dedup.md§ Task 4: Incremental updates — semantic cache + manifest in __main__.py (lines 603-911): references changed symbols_run
Formal verification
Could not verify: Could not verify \_rewire\_unique\_stub\_nodes.
The verifier did not have enough to check \_rewire\_unique\_stub\_nodes, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: no capturable inputs from the test suite; property tier: not verifiable: the input domain has 81 values but only 11 distinct were tested — a small finite domain must be EXHAUSTED, not sampled (an untested input could invert the result)
Could not verify: Could not verify \_extract\_generic.
The verifier did not have enough to check \_extract\_generic, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: no capturable inputs from the test suite; property tier: parameter `config` is annotated `LanguageConfig` — outside the synthesizable primitive/collection set
· 4 more finding(s) on lines outside this diff (see the check run).
…receivers (Graphify-Labs#2732) Review follow-ups on the imported-module decorator guard (graphify-labs bot on Graphify-Labs#4622); both were regressions against v8: - A decorator re-exported by a package (`from mypkg import retry`, defined in mypkg/decorators.py) lost its rewire, because the guard required the definition to be in mypkg.py or mypkg/__init__.py. The guard now accepts any definition inside the module's package (consecutive path segments). - `from mylib import api` + `@api.route` recorded a fake `mylib.api` module (`api` is an object) and blocked the rewire onto mylib's `route`. A from-imported receiver now records only the module it came from. Two regression tests; both pass here and on unmodified v8. The pytest case stays fixed: tests/test_helper.py is not inside `pytest`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

What broke
@pytest.fixturereaches the extractor as the bare head symbolfixture(_python_decorator_namekeeps the attribute, not the module), becomes a sourcelessfixturestub, and_rewire_unique_stub_nodescollapses that stub onto the corpus's one top-leveldef fixture(), an unrelated helper in another file.On a 13-file repro (12 test modules with two
@pytest.fixturefunctions each, plustests/test_helper.pywith a localdef fixture()):fixture()fixturestub (sourceless)god_nodes()top entryfixture(), degree 25env1(), degree 2The same happens for
from pytest import fixture+@fixtureandimport pytest as pt+@pt.fixture, and for any third-party decorator whose bare name matches one corpus def.Fix
extractors/engine.py):_python_import_bindingsreads the file's module-level imports once (import m,import m as a,import a.b,from m import x [as a]; relative and function-local imports are skipped)._python_decorator_modulereturns the module a decorator's symbol was imported from, and the decoratorreferencesedge carries it asmetadata.ref_module. Stub ids are unchanged, sotest_attribute_decorator_targets_the_symbol_not_the_module(@app.route→route) still holds.extract.py): a per-edge guard,_other_module_candidate, alongside_cross_language_candidateand_names_own_builtin_base. An edge withref_moduleonly moves onto a candidate that lives in that module: its own file (m.py,m/__init__.py) or a file inside the package (the target of a re-export); dotted modules match as consecutive path segments. Afrom m import xreceiver records onlym. Edges without the marker follow the existing rules.Because the guard is per edge, a genuine bare reference to the same name elsewhere in the corpus still rewires onto the local def; only the pytest edges stay on the stub. Sourceless stubs are already excluded from
god_nodes()by_is_concept_node, which is why the fake hub disappears.Relation to #2356
#2356 handles attribute receivers that are locally assigned (
app = Flask()→@app.get) and deliberately keeps imported receivers on bare names. This PR handles exactly that imported case, so the two compose; I'll rebase whichever lands second.Tests
tests/test_python_imported_decorator_rewire.py: 11 tests. 5 fail on v8 (attribute, aliased and from-import pytest decorators; a decorator from one module not binding to a same-named def in another; per-edge behaviour with a genuine local user). 4 guard that corpus decorators still link:import myhelpers→@myhelpers.retry,from myhelpers import retry,import pkg.tools as tools, a decorator with no import evidence, a package re-export (from mypkg import retry→mypkg/decorators.py) and an imported-object receiver (from mylib import api→@api.route). The 13 existing tests intests/test_python_decorators.pypass unchanged.uv run pytest tests/: 8480 passed, 15 skipped on 3.12 and 3.13; 8481 passed, 14 skipped on 3.10. One failure,tests/test_skill_no_backtick_substitution.py::test_control_a_backticked_unknown_command_is_caught, fails identically on unmodifiedv8(b90b9df) and is unrelated to this change.uv run ruff check .: cleanuv run pyright: 641 errors, the same as unmodifiedv8; none in the changed codeNotes
Out of scope: using
ref_moduleto disambiguate when several same-named defs exist (today the stub stays ambiguous, which is the safe outcome).Cache:
ref_moduleis set at extraction, so cached AST results for affected files keep the old edges until the AST cache schema is bumped (left to the release bump, as with earlier fixes).Review follow-ups (graphify-labs bot): package re-exports and imported-object receivers keep v8's rewires (c50fd79).
🤖 Generated with Claude Code