fix(python): resolve aliased imports in subdirectories - #2946
Conversation
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 plain import mod statements in _import_python to the actual module file path (via _resolve_python_module_path) so subdirectory imports point at the canonical module node instead of a bare-name id, attaching a target_file hint when the file exists. Adds tests covering aliased/unaliased subdirectory imports, same-stem disambiguation across sibling directories, and warm-cache survival without transient target_file/local_alias hints leaking into output.
Worth a look
- _import_python references str_path which may be undefined —
graphify/extract.py:327· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Python import edge target IDs became filesystem-path dependent —
graphify/extract.py:328· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Python import resolution now depends on process-global extract root —
graphify/extract.py:328· 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 — 1767 functions depend on the 575 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 478 callers, 42 callees - new:
_rebuild_code()— 98 callers, 50 callees - new:
extract_xaml()— 19 callers, 17 callees - new:
extract_js()— 80 callers, 3 callees - new:
dispatch_command()— 2 callers, 119 callees - new:
_get_extractor()— 26 callers, 6 callees - new:
run_pipeline()— 8 callers, 13 callees - new:
collect_files()— 17 callers, 6 callees - …and 23 more — each is listed as a finding
Verification — 1767 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: 1619 function(s) in the blast radius were not formally verified this run
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)
· 31 more finding(s) on lines outside this diff (see the check run).
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
Adds _repoint_python_script_imports and wires it into extract() after the package-import repointing pass, redirecting bare import mod_a targets in non-package directories to their canonical sibling file nodes (e.g. scripts_mod_a) so member-call resolvers can bind cross-module calls edges (#2943). Guards against repointing inside package dirs, self-imports, and imports_from edges. Covers the new behavior with tests for aliased/unaliased subdir imports, same-stem disambiguation across directories, warm-cache survival, and imports_from being left untouched.
No blocking issues surfaced. 4 lower-confidence candidates did not survive cross-model review.
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 1771 functions depend on the 579 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 479 callers, 43 callees - new:
_rebuild_code()— 98 callers, 50 callees - new:
extract_xaml()— 19 callers, 17 callees - new:
extract_js()— 80 callers, 3 callees - new:
dispatch_command()— 2 callers, 119 callees - new:
_get_extractor()— 26 callers, 6 callees - new:
run_pipeline()— 8 callers, 13 callees - new:
collect_files()— 17 callers, 6 callees - …and 23 more — each is listed as a finding
Verification — 1771 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: 1623 function(s) in the blast radius were not formally verified this run
Formal verification
Could not verify: Could not verify extract.
The verifier did not have enough to check extract, 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: parameter `cache_root` is annotated `Path | None` — outside the synthesizable primitive/collection set
· 31 more finding(s) on lines outside this diff (see the check run).
Summary
Fixes #2943.
Cross-module
callsedges were being dropped for Python aliased imports whenthe imported module lived in a loose subdirectory.
For example:
with
scripts/mod_a.pydefiningtarget().The import was resolved, but the imported module remained represented by the
syntactic target
mod_ainstead of the canonical file nodescripts_mod_a. As a result, Python member-call resolution could not connectm.target()to the target function.Root Cause
Graphify already performs corpus-level canonicalization for Python package
imports, but loose-script imports from sibling modules were not being
repointed to their canonical file nodes.
The initial implementation attempted to resolve these imports directly
during AST extraction. Full-suite testing revealed that this could incorrectly
repoint ambiguous package imports and create self-loops for imports such as
import builtins.The implementation was therefore moved to a dedicated post-extraction
repointing pass.
Implementation
Added
_repoint_python_script_imports()to canonicalize bare imports fromnon-package directories.
The extraction pipeline is now:
For:
with:
the import is repointed to:
allowing the existing member-call resolver to produce:
The loose-script pass is directory-scoped and does not globally match module
filenames.
It also:
imports_fromhandling to the existing symbol-resolution pipeline.Tests
Added regression coverage for:
from ... import ...behavior;Real Behavior Proof
Exact command / steps:
uv run --frozen pytest tests/test_extract.py -k "alias or python_import or test_python_subdirectory_" -v uv run --frozen pytest tests/test_src_layout_import_resolution.py::test_ambiguous_package_alias_is_not_repointed -vv uv run --frozen pytest tests/test_import_self_loops.py::test_python_external_import_matching_current_basename_has_no_self_loop -vv uv run --frozen pytest tests/test_extract.py -q uv run ruff check graphify/ tests/Minimal reproduction:
Observed result:
The
callsedge is emitted withEXTRACTEDconfidence.No
target_fileorlocal_aliashints leak into the final graph.Focused validation:
tests/test_extract.py: 197 passed, 4 skippedThe full repository suite was also run locally:
Result:
The 20 failures were Windows/environment-specific tests involving Unix-only
primitives, Windows symlink privileges, encoding, file locking, and Windows
path-length behavior. No failures were observed in the Python import
resolution, extraction, build, merge, caching, or related regression areas.
Not tested