fix(python): resolve absolute sibling imports to the importing file's directory - #3468
Conversation
… directory An absolute from-import used the bare module name as the edge target id. That id only matches a file node when the basename is unique across the scan, so a repo carrying a vendored copy of the same module left the target matching nothing: the edge dangled, was pruned, and a real dependency vanished from the graph. The symbol-level pass does not cover it either, since it resolves imported functions and classes -- so `from m import CONST` left no trace while `from m import func` in the same file resolved normally. Probe the importing file's own directory first, which is how the import resolves at runtime for a script that does `sys.path.insert(0, os.path.dirname(__file__))`. Setting target_path lets the existing target_file stamp canonicalize the id, exactly as the relative-import branch already does. The self-resolution guard is load-bearing: a module named contracting.py doing `from contracting import constants` imports the external package of that name, not itself, and must not gain a fabricated self-loop. Only absolute from-imports that resolve to an existing sibling file change; everything else keeps the bare-name behaviour. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
Formal verification. No changes could be formally verified in this run.
Graphify review — findings
Resolves absolute from X import ... targets in _import_python against the importing file's own directory first, probing for a sibling .py module before falling back to the bare module name. This fixes dangling/pruned import edges when a same-named module is vendored elsewhere in the scan, since the bare name only resolves when the basename is unique. Skips the sibling when it resolves to the importing file itself, so a module can still import an external package of the same name without gaining a self-loop.
Worth a look
- Sibling absolute import probe misses package directories —
graphify/extract.py:391· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Multi-level absolute import builds wrong sibling path —
graphify/extract.py:400· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 1859 functions depend on the 248 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 565 callers, 43 callees - new:
_rebuild_code()— 115 callers, 51 callees - new:
extract_js()— 85 callers, 4 callees - new:
extract_xaml()— 19 callers, 17 callees - new:
dispatch_command()— 2 callers, 124 callees - new:
_get_extractor()— 26 callers, 6 callees - new:
run_pipeline()— 8 callers, 13 callees - new:
collect_files()— 17 callers, 6 callees - …and 28 more — each is listed as a finding
Verification — 1859 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: 1694 function(s) in the blast radius were not formally verified this run
Test selection
Test selection
104 of 262 test file(s) selected (40%) via static blast radius.
tests/test_astro_extraction.py— impacttests/test_astro_import_ids.py— impacttests/test_build.py— impacttests/test_builtin_global_type_refs.py— impacttests/test_case_sensitive_resolution.py— impacttests/test_cjs_module_extension.py— impacttests/test_cpp_nested_and_cli.py— impacttests/test_cpp_objc_cross_file_calls.py— impacttests/test_cross_extension_reexport_self_cycle.py— impacttests/test_cross_language_call_resolution.py— impacttests/test_cross_repo_member_calls.py— impacttests/test_csharp_call_site_generic_args.py— impacttests/test_csharp_enum_members.py— impacttests/test_csharp_field_generic_args.py— impacttests/test_csharp_generic_callsites.py— impacttests/test_csharp_interface_dispatch.py— impacttests/test_csharp_member_calls.py— impacttests/test_csharp_member_nodes.py— impacttests/test_csharp_object_creation.py— impacttests/test_csharp_partial_classes.py— impacttests/test_csharp_type_resolution.py— impacttests/test_definition_file_portability.py— impacttests/test_detect.py— impacttests/test_dotnet.py— impacttests/test_duplicate_annotation_edges.py— impacttests/test_extract.py— impacttests/test_extract_cache_location.py— impacttests/test_file_label_disambiguation.py— impacttests/test_file_node_id_spec.py— impacttests/test_forwarding_review_findings.py— impacttests/test_go_builtin_call_targets.py— impacttests/test_go_qualified_resolution.py— impacttests/test_import_extension_resolution.py— impacttests/test_import_self_loops.py— impacttests/test_imported_export_forwarding.py— impacttests/test_incremental.py— impacttests/test_indirect_call_arrow_single_param_shadow.py— impacttests/test_indirect_call_catch_binding_shadow.py— impacttests/test_indirect_call_external_import_shadow.py— impacttests/test_indirect_call_for_of_binding_shadow.py— impacttests/test_indirect_call_function_expression_shadow.py— impacttests/test_indirect_call_nested_closure_shadow.py— impacttests/test_indirect_dispatch.py— impacttests/test_indirect_dispatch_assign_return.py— impacttests/test_indirect_dispatch_getattr.py— impacttests/test_inferred_confidence_rubric.py— impacttests/test_inherited_field_receivers.py— impacttests/test_java_member_calls.py— impacttests/test_java_type_resolution.py— impacttests/test_js_callback_calls.py— impact- … and 54 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.
Formal verification
Could not verify: Could not verify \_import\_python.
The verifier did not have enough to check \_import\_python, 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: not verifiable: all 200 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly AttributeError — names the real obstacle, not a sampling gap)
· 36 more finding(s) on lines outside this diff (see the check run).
The sibling probe added in the previous commit looked only for `<name>.py`, so an absolute `from pkg import X` whose sibling is a package directory fell through to the bare module name. That is the same dangling edge the fix set out to remove: the bare id only matches a file node when the basename is unique, so a vendored copy of the same package left the edge matching nothing. Probe with _probe_python_module_candidate instead — the resolver the relative-import branch already uses — which resolves a directory to its __init__.py, an exact file, or a .py suffix. The self-resolution guard is unchanged. Multi-level names were checked at the same time and already resolved correctly: `from a.b import V` lands on a/b.py, and on a/b/__init__.py when a.b is itself a package. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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 3 advisory finding(s) below merit a look before merge.
Formal verification. No changes could be formally verified in this run.
Graphify review — findings
Resolves absolute from X import ... statements against the importing file's own directory before falling back to the bare module name, so a script that does sys.path.insert(0, os.path.dirname(__file__)) links to its real sibling module or package (landing on __init__.py) instead of dangling when a same-named vendored copy makes the basename non-unique. Guards against fabricating a self-loop when a module imports an external package that shares its name, and leaves genuinely external imports pointing at the bare name when no sibling exists on disk.
Worth a look
- Absolute from-imports are now resolved file-relative whenever a sibling exists —
graphify/extract.py:391· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Absolute from-import now silently resolves to sibling file, breaking external-package imports when a same-named local file exists —
graphify/extract.py:400· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Absolute sibling probe can create an unintended edge to a same-named local module for genuinely external imports —
graphify/extract.py:405· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 1860 functions depend on the 249 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 566 callers, 43 callees - new:
_rebuild_code()— 115 callers, 51 callees - new:
extract_js()— 85 callers, 4 callees - new:
extract_xaml()— 19 callers, 17 callees - new:
dispatch_command()— 2 callers, 124 callees - new:
_get_extractor()— 26 callers, 6 callees - new:
run_pipeline()— 8 callers, 13 callees - new:
collect_files()— 17 callers, 6 callees - …and 28 more — each is listed as a finding
Verification — 1860 functions in the blast radius were not formally verified this run (proofs are advisory here).
Health delta baseline: last indexed commit 23f2ffa (diverged from this PR's base — delta is approximate).
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: 1695 function(s) in the blast radius were not formally verified this run
Test selection
Test selection
104 of 262 test file(s) selected (40%) via static blast radius.
tests/test_astro_extraction.py— impacttests/test_astro_import_ids.py— impacttests/test_build.py— impacttests/test_builtin_global_type_refs.py— impacttests/test_case_sensitive_resolution.py— impacttests/test_cjs_module_extension.py— impacttests/test_cpp_nested_and_cli.py— impacttests/test_cpp_objc_cross_file_calls.py— impacttests/test_cross_extension_reexport_self_cycle.py— impacttests/test_cross_language_call_resolution.py— impacttests/test_cross_repo_member_calls.py— impacttests/test_csharp_call_site_generic_args.py— impacttests/test_csharp_enum_members.py— impacttests/test_csharp_field_generic_args.py— impacttests/test_csharp_generic_callsites.py— impacttests/test_csharp_interface_dispatch.py— impacttests/test_csharp_member_calls.py— impacttests/test_csharp_member_nodes.py— impacttests/test_csharp_object_creation.py— impacttests/test_csharp_partial_classes.py— impacttests/test_csharp_type_resolution.py— impacttests/test_definition_file_portability.py— impacttests/test_detect.py— impacttests/test_dotnet.py— impacttests/test_duplicate_annotation_edges.py— impacttests/test_extract.py— impacttests/test_extract_cache_location.py— impacttests/test_file_label_disambiguation.py— impacttests/test_file_node_id_spec.py— impacttests/test_forwarding_review_findings.py— impacttests/test_go_builtin_call_targets.py— impacttests/test_go_qualified_resolution.py— impacttests/test_import_extension_resolution.py— impacttests/test_import_self_loops.py— impacttests/test_imported_export_forwarding.py— impacttests/test_incremental.py— impacttests/test_indirect_call_arrow_single_param_shadow.py— impacttests/test_indirect_call_catch_binding_shadow.py— impacttests/test_indirect_call_external_import_shadow.py— impacttests/test_indirect_call_for_of_binding_shadow.py— impacttests/test_indirect_call_function_expression_shadow.py— impacttests/test_indirect_call_nested_closure_shadow.py— impacttests/test_indirect_dispatch.py— impacttests/test_indirect_dispatch_assign_return.py— impacttests/test_indirect_dispatch_getattr.py— impacttests/test_inferred_confidence_rubric.py— impacttests/test_inherited_field_receivers.py— impacttests/test_java_member_calls.py— impacttests/test_java_type_resolution.py— impacttests/test_js_callback_calls.py— impact- … and 54 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.
Formal verification
Could not verify: Could not verify \_import\_python.
The verifier did not have enough to check \_import\_python, 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: not verifiable: all 200 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly AttributeError — names the real obstacle, not a sampling gap)
· 36 more finding(s) on lines outside this diff (see the check run).
Python 3 removed implicit relative imports, so inside a package `from models import Thing` is absolute and means the installed distribution — not the sibling module sitting next to the importer. Probing there could invent an edge to a same-named local file and hide a genuine third-party dependency. Skip the probe when the importing file's own directory holds an __init__.py. What is left is exactly the case the probe was added for: a plain script directory, which the interpreter puts on sys.path itself, so the sibling really does shadow any installed package of that name. No coverage is lost on this repo — all 18 bare sibling imports under worked/ live in non-package directories, and all 18 still resolve to a real file with none dangling and no self-loops. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Graphify reviewed this change.
Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).
Formal verification. No changes could be formally verified in this run.
Graphify review — findings
Resolves absolute from X import Y statements against the importing file's own directory when that file lives in a non-package (script) directory, matching the interpreter's sys.path.insert(0, dirname(__file__)) behaviour so the sibling module or package wins over a bare-name match that would dangle and get pruned whenever a vendored copy makes the basename non-unique. Guards against self-loops (a module importing the external package of its own name) and skips the probe entirely inside packages, where Python 3 absolute imports mean the installed distribution rather than the sibling; with no sibling on disk the target stays the bare module name as before.
No blocking issues surfaced. 3 lower-confidence candidates did not survive cross-model review.
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 1861 functions depend on the 250 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 567 callers, 43 callees - new:
_rebuild_code()— 115 callers, 51 callees - new:
extract_js()— 85 callers, 4 callees - new:
extract_xaml()— 19 callers, 17 callees - new:
dispatch_command()— 2 callers, 124 callees - new:
_get_extractor()— 26 callers, 6 callees - new:
run_pipeline()— 8 callers, 13 callees - new:
collect_files()— 17 callers, 6 callees - …and 28 more — each is listed as a finding
Verification — 1861 functions in the blast radius were not formally verified this run (proofs are advisory here).
Health delta baseline: last indexed commit 23f2ffa (diverged from this PR's base — delta is approximate).
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: 1696 function(s) in the blast radius were not formally verified this run
Test selection
Test selection
104 of 262 test file(s) selected (40%) via static blast radius.
tests/test_astro_extraction.py— impacttests/test_astro_import_ids.py— impacttests/test_build.py— impacttests/test_builtin_global_type_refs.py— impacttests/test_case_sensitive_resolution.py— impacttests/test_cjs_module_extension.py— impacttests/test_cpp_nested_and_cli.py— impacttests/test_cpp_objc_cross_file_calls.py— impacttests/test_cross_extension_reexport_self_cycle.py— impacttests/test_cross_language_call_resolution.py— impacttests/test_cross_repo_member_calls.py— impacttests/test_csharp_call_site_generic_args.py— impacttests/test_csharp_enum_members.py— impacttests/test_csharp_field_generic_args.py— impacttests/test_csharp_generic_callsites.py— impacttests/test_csharp_interface_dispatch.py— impacttests/test_csharp_member_calls.py— impacttests/test_csharp_member_nodes.py— impacttests/test_csharp_object_creation.py— impacttests/test_csharp_partial_classes.py— impacttests/test_csharp_type_resolution.py— impacttests/test_definition_file_portability.py— impacttests/test_detect.py— impacttests/test_dotnet.py— impacttests/test_duplicate_annotation_edges.py— impacttests/test_extract.py— impacttests/test_extract_cache_location.py— impacttests/test_file_label_disambiguation.py— impacttests/test_file_node_id_spec.py— impacttests/test_forwarding_review_findings.py— impacttests/test_go_builtin_call_targets.py— impacttests/test_go_qualified_resolution.py— impacttests/test_import_extension_resolution.py— impacttests/test_import_self_loops.py— impacttests/test_imported_export_forwarding.py— impacttests/test_incremental.py— impacttests/test_indirect_call_arrow_single_param_shadow.py— impacttests/test_indirect_call_catch_binding_shadow.py— impacttests/test_indirect_call_external_import_shadow.py— impacttests/test_indirect_call_for_of_binding_shadow.py— impacttests/test_indirect_call_function_expression_shadow.py— impacttests/test_indirect_call_nested_closure_shadow.py— impacttests/test_indirect_dispatch.py— impacttests/test_indirect_dispatch_assign_return.py— impacttests/test_indirect_dispatch_getattr.py— impacttests/test_inferred_confidence_rubric.py— impacttests/test_inherited_field_receivers.py— impacttests/test_java_member_calls.py— impacttests/test_java_type_resolution.py— impacttests/test_js_callback_calls.py— impact- … and 54 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.
Formal verification
Could not verify: Could not verify \_import\_python.
The verifier did not have enough to check \_import\_python, 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: not verifiable: all 200 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly AttributeError — names the real obstacle, not a sampling gap)
· 36 more finding(s) on lines outside this diff (see the check run).
Base:
v8The bug
In
graphify/extract.py,_import_pythonuses the bare module name as theimports_fromedge target for an absolute from-import. That id only matches afile node when the basename is unique across the scan. Two things go wrong:
same name leaves the target matching nothing. The edge dangles, is pruned, and
a real dependency disappears from the graph.
dependency is drawn as a third-party package instead of the local file.
The symbol-level pass in
extractors/resolution.pydoesn't cover it either: itresolves imported functions and classes, so
from m import CONSTleaves no tracewhile
from m import funcin the same file resolves normally.The fix
Probe the importing file's own directory before falling back to the bare name.
That is exactly how the import resolves at runtime for a script that does
sys.path.insert(0, os.path.dirname(__file__)). Settingtarget_pathlets theexisting
target_filestamp canonicalize the id, the same way the relative-importbranch already does.
The self-resolution guard is load-bearing: a module named
contracting.pydoingfrom contracting import constantsimports the external package of that name, notitself, and must not gain a fabricated self-loop
(
tests/test_import_self_loops.pycatches this).Only absolute from-imports that resolve to an existing sibling file change
behaviour. Everything else keeps the bare-name target it had before.
Tests
Two added to
tests/test_python_import_resolution.py:test_absolute_sibling_import_resolves_to_importing_files_directory—live/layouts.py, a duplicatevendor/layouts.py, andlive/build.pydoingsys.path.insertthenfrom layouts import LAYOUTS. Asserts theimports_fromedge lands on
live/layouts.pyand never on the vendored twin. Fails onunpatched code, passes with the fix.
test_absolute_import_without_sibling_keeps_bare_module_target— pins theunchanged path: no sibling on disk means the target stays the bare module id.
Verification
Run on Python 3.12 via
uv run --frozen pytest tests/ -q, on this branch's basecommit and again with the change:
v8@ 3f82bf7)The 17 failures are byte-identical sets before and after (
test_skillgen.py,test_ollama_retry_cap.py,test_install.py, and one timing-sensitivetest_ts_import_type_arguments.pycase) — all pre-existing and unrelated. The +2passed are the two new tests.
ruff check --config pyproject.tomlclean on both changed files.Effect on a real graph
graphify extract . --code-onlyover this repo itself, before and after:worked/example/raw/{api,validator}.pypreviously pointed at a phantom externalnode
processor; they now point at the realworked/example/raw/processor.py,and the phantom node is gone (that is the −1 node).
worked/httpx/raw/{auth,client,transport,utils}.py→worked/httpx/raw/models.pyare 4 edges that did not exist before.
models.pyhas a duplicate basename inthis repo (
graphify/extractors/models.py), which is exactly the ambiguity thatmade them dangle; they resolve to the correct sibling, not the twin.
imports/imports_from/re_exportsself-loops in the resulting graph.🤖 Generated with Claude Code