Repository navigation
Conversation
Keep same-named declarations distinct under their enclosing types, and resolve Java, C#, Kotlin and Swift receivers against the selected lexical, imported or inherited owner rather than unrelated namesakes. Preserve portable resolution context in full, cached and incremental CLI/watch extraction. Retire stale extraction facts with cache schema 47 and document the required full graph/source-evidence rebuild. Add seven-language identity coverage, scoped-call precision controls and regression tests for the review findings. Fixes Graphify-Labs#5185 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Thanks for the pull request, @y-src. 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.
No issues found by static checks — no coupling regressions or blocking issues in the code graph. That is not the same as safe to merge: see what was not checked.
Not checked: tests were not run; formal verification proved 0 of 21 changed function(s) (3 sampled, not proven, 18 not verified).
Formal verification. PR-changed functions: 3/21 verified (0 proven, 3 may-equivalent, 0 distinguished) · 18 not verified (15 vacuous, 3 unsupported).
Not verified on this run: dispatch\_command (vacuous: never exercised), \_bind\_member\_field\_tables (vacuous: never exercised), \_resolve\_csharp\_extension\_calls (vacuous: never exercised), \_resolve\_java\_member\_calls (vacuous: never exercised), \_resolve\_kotlin\_import\_targets (unsupported), \_resolve\_kotlin\_member\_calls (vacuous: never exercised), \_resolve\_swift\_member\_calls (unsupported), extract (vacuous: never exercised), \_\_init\_\_ (vacuous: never exercised), \_resolve\_holder\_nid (vacuous: never exercised), and 8 more.
Graphify review — findings
Fixes nested type identity and receiver resolution across the shared extractor, so same-named nested types in one file (Left.Builder / Right.Builder) get distinct IDs and calls resolve in the right lexical, package or import scope. Java, C#, Kotlin, Swift and TypeScript now resolve qualified receivers, inherited member types and fields, and namespace-qualified types against their declaring context, and ambiguous or absent targets stay unlinked instead of falling back to an unrelated name. Type metadata records a label-based type_path (including C# generic arity) so exported field tables survive cache relocation and ID remapping, and the AST cache schema moves to 47, so existing graphs with old IDs need graphify extract . --code-only --force.
Review partial — this diff was larger than one review pass covers, so later files were not reviewed; some findings may be missing.
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 6248 functions depend on the 1323 functions this change touches.
Health — this change adds coupling hotspots:
- worse:
extract()— 1235 callers, 78 callees - worse:
save_cached()— 62 callers, 10 callees - worse:
walk()— 1 callers, 93 callees - worse:
walk_calls()— 1 callers, 45 callees - worse:
_csharp_pre_scan_name_collisions()— 1 callers, 6 callees
Verification — 6248 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: 6036 function(s) in the blast radius were not formally verified this run
Test selection
Test selection
567 of 567 test file(s) selected (100%) via static blast radius.
Escalated to a full run for safety (
noncode-change,uncovered-code) — the selection is not trustworthy on its own (see below). CI should run the whole suite.
tests/pi.test.ts— full-run-safetytests/test_4874_jvm_resolution.py— impact, full-run-safetytests/test_4909_reference_binding_scale.py— full-run-safetytests/test_4978_reconcile_cost.py— impact, full-run-safetytests/test_4992_ts_import_types.py— impact, full-run-safetytests/test_5072_splittable_under_cap_retry.py— full-run-safetytests/test_affected_cli.py— impact, full-run-safetytests/test_affected_defines.py— full-run-safetytests/test_affected_member_seed.py— full-run-safetytests/test_affected_overrides.py— impact, full-run-safetytests/test_affected_path_file_node.py— full-run-safetytests/test_affected_seed_reference_forms.py— impact, full-run-safetytests/test_agents_platform.py— impact, full-run-safetytests/test_alias_folding.py— impact, full-run-safetytests/test_analyze.py— impact, full-run-safetytests/test_anthropic_custom_endpoint.py— full-run-safetytests/test_antigravity_install.py— full-run-safetytests/test_apm_fallback_version.py— full-run-safetytests/test_architecture_doc.py— full-run-safetytests/test_astro_extraction.py— impact, full-run-safetytests/test_astro_import_ids.py— impact, full-run-safetytests/test_atomic_canvas_export.py— full-run-safetytests/test_atomic_version_stamp.py— full-run-safetytests/test_atomic_writes.py— full-run-safetytests/test_backend_env_isolation.py— full-run-safetytests/test_backend_extras.py— full-run-safetytests/test_backend_opt_in.py— full-run-safetytests/test_benchmark.py— full-run-safetytests/test_benchmark_raw_graph.py— full-run-safetytests/test_benchmark_real_query.py— full-run-safetytests/test_binary_media_words.py— impact, full-run-safetytests/test_blade_extractor.py— impact, full-run-safetytests/test_build.py— impact, full-run-safetytests/test_build_located_semantic_identity.py— impact, full-run-safetytests/test_build_merge_dedup_scope.py— impact, full-run-safetytests/test_build_merge_ghost_fold_4955.py— impact, full-run-safetytests/test_build_merge_hyperedges_and_prune.py— impact, full-run-safetytests/test_build_merge_shrink_guard.py— impact, full-run-safetytests/test_builtin_global_type_refs.py— impact, full-run-safetytests/test_c_include_resolution.py— impact, full-run-safetytests/test_cache.py— impact, full-run-safetytests/test_cache_long_paths.py— impact, full-run-safetytests/test_cache_stale_import_target.py— impact, full-run-safetytests/test_cache_windows_case_4925.py— impact, full-run-safetytests/test_callflow_html.py— full-run-safetytests/test_cargo_introspect.py— full-run-safetytests/test_cargo_missing_manifest.py— full-run-safetytests/test_carried_hyperedge_remap.py— impact, full-run-safetytests/test_case_only_decl_ids_4904.py— impact, full-run-safetytests/test_case_sensitive_resolution.py— impact, full-run-safety- … and 517 more
non-code file(s) changed (
ARCHITECTURE.md) → running the full suite for safety (a code graph can't see config/fixture/data deps)
changed code file(s) with no mapped test (
ARCHITECTURE.md) — a coverage gap or a missing link — running the full suite rather than only the selected tests
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)
CHANGELOG.md§ 0.9.103 (2026-11-05) (lines 110-131): references changed symbolsrebuildCHANGELOG.md§ 0.9.94 (2026-10-22) (lines 326-347): references changed symbolstargetsCHANGELOG.md§ 0.9.83 (2026-10-09) (lines 554-568): references changed symbolsrebuildCHANGELOG.md§ 0.9.74 (2026-10-02) (lines 694-709): references changed symbolstargetsCHANGELOG.md§ 0.9.55 (2026-09-05) (lines 925-939): references changed symbolsrebuildCHANGELOG.md§ 0.9.46 (2026-08-17) (lines 1059-1075): references changed symbolsrebuildCHANGELOG.md§ 0.9.20 (2026-07-18) (lines 1351-1357): references changed symbolstargetsCHANGELOG.md§ 0.9.18 (2026-07-17) (lines 1369-1382): references changed symbolsrebuildCHANGELOG.md§ 0.9.16 (2026-07-14) (lines 1413-1444): references changed symbolsrebuildCHANGELOG.md§ 0.9.14 (2026-07-12) (lines 1453-1472): references changed symbolsrebuild
…and 10 more.
Formal verification
Could not verify: Could not verify dispatch\_command.
The verifier did not have enough to check dispatch\_command, 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: all 40 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly SystemExit — names the real obstacle, not a sampling gap)
Could not verify: Could not verify \_bind\_member\_field\_tables.
The verifier did not have enough to check \_bind\_member\_field\_tables, 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: all 11 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly TypeError — names the real obstacle, not a sampling gap)
No difference found (not proven): No behavior difference found in \_csharp\_keyword\_type (not a proof).
The verifier ran both versions of \_csharp\_keyword\_type on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
Could not verify: Could not verify \_resolve\_csharp\_extension\_calls.
The verifier did not have enough to check \_resolve\_csharp\_extension\_calls, 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: non-vacuity: domain too small (only 1 distinct inputs exercised, need 3) — 'no divergence' would be near-vacuous
Could not verify: Could not verify \_resolve\_java\_member\_calls.
The verifier did not have enough to check \_resolve\_java\_member\_calls, 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: non-vacuity: domain too small (only 1 distinct inputs exercised, need 3) — 'no divergence' would be near-vacuous
Could not verify: Could not verify \_resolve\_kotlin\_import\_targets.
The verifier did not have enough to check \_resolve\_kotlin\_import\_targets, 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 verified: a runtime divergence was observed, but the two versions' package snapshots loaded differently in the sandbox (old: {'fallback': {'graphify.cache': 'ModuleNotFoundError', 'graphify.csharp_dispatch': 'ModuleNotFoundError', 'graphify.extractors': 'ModuleNotFoundError', 'graphify.extractors.apex': 'ImportError', 'graphify.extractors.base': 'ModuleNotFoundError', 'graphify.extractors.bash': 'ImportError', 'graphify.extractors.commonlisp': 'ImportError', 'graphify.extractors.dart': 'ImportError', 'graphify.extractors.dm': 'ImportError', 'graphify.extractors.elixir': 'ImportError', 'graphify.extractors.engine': 'ImportError', 'graphify.extractors.erlang': 'ImportError', 'graphify.extractors.fortran': 'ImportError', 'graphify.extractors.go': 'ModuleNotFoundError', 'graphify.extractors.json_config': 'ImportError', 'graphify.extractors.markdown': 'ModuleNotFoundError', 'graphify.extractors\…
Could not verify: Could not verify \_resolve\_kotlin\_member\_calls.
The verifier did not have enough to check \_resolve\_kotlin\_member\_calls, 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: non-vacuity: domain too small (only 1 distinct inputs exercised, need 3) — 'no divergence' would be near-vacuous
Could not verify: Could not verify \_resolve\_swift\_member\_calls.
The verifier did not have enough to check \_resolve\_swift\_member\_calls, 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 verified: a runtime divergence was observed, but the two versions' package snapshots loaded differently in the sandbox (old: {'fallback': {'graphify.cache': 'ModuleNotFoundError', 'graphify.csharp_dispatch': 'ModuleNotFoundError', 'graphify.extractors': 'ModuleNotFoundError', 'graphify.extractors.apex': 'ImportError', 'graphify.extractors.base': 'ModuleNotFoundError', 'graphify.extractors.bash': 'ImportError', 'graphify.extractors.commonlisp': 'ImportError', 'graphify.extractors.dart': 'ImportError', 'graphify.extractors.dm': 'ImportError', 'graphify.extractors.elixir': 'ImportError', 'graphify.extractors.engine': 'ImportError', 'graphify.extractors.erlang': 'ImportError', 'graphify.extractors.fortran': 'ImportError', 'graphify.extractors.go': 'ModuleNotFoundError', 'graphify.extractors.json_config': 'ImportError', 'graphify.extractors.markdown': 'ModuleNotFoundError', 'graphify.extractors\…
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: no capturable inputs from the test suite; property tier: not verifiable: all 90 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly TypeError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify \_\_init\_\_.
The verifier did not have enough to check \_\_init\_\_, 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: all 29 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly __init__ raised on the synthesized receiver args — names the real obstacle, not a sampling gap)
Could not verify: Could not verify \_resolve\_holder\_nid.
The verifier did not have enough to check \_resolve\_holder\_nid, 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: all 87 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly __init__ raised on the synthesized receiver args — names the real obstacle, not a sampling gap)
Could not verify: Could not verify resolve\_type\_name.
The verifier did not have enough to check resolve\_type\_name, 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: all 247 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly __init__ raised on the synthesized receiver args — names the real obstacle, not a sampling gap)
Could not verify: Could not verify \_csharp\_pre\_scan\_name\_collisions.
The verifier did not have enough to check \_csharp\_pre\_scan\_name\_collisions, 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: all 253 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)
Could not verify: Could not verify \_csharp\_receiver\_type\_name.
The verifier did not have enough to check \_csharp\_receiver\_type\_name, 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 66 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)
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: parameter `config` is annotated `LanguageConfig` — outside the synthesizable primitive/collection set
Could not verify: Could not verify \_java\_receiver\_type\_name.
The verifier did not have enough to check \_java\_receiver\_type\_name, 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 66 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)
No difference found (not proven): No behavior difference found in \_java\_type\_chain\_leaf (not a proof).
The verifier ran both versions of \_java\_type\_chain\_leaf on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
Could not verify: Could not verify \_swift\_constructor\_type.
The verifier did not have enough to check \_swift\_constructor\_type, 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 66 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)
Could not verify: Could not verify \_swift\_receiver\_name.
The verifier did not have enough to check \_swift\_receiver\_name, 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 66 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)
Could not verify: Could not verify \_ts\_extra\_walk.
The verifier did not have enough to check \_ts\_extra\_walk, 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 264 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)
No difference found (not proven): No behavior difference found in \_rebuild\_code (not a proof).
The verifier ran both versions of \_rebuild\_code on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
· 5 more finding(s) on lines outside this diff (see the check run).
What does this PR do?
Fixes #5185.
Preserve distinct nested declarations across Java, C#, C++, Kotlin, Scala, Swift and TypeScript. Preserve qualified/imported calls in the demonstrated Java, C#, Kotlin and Swift cases without guessing an unrelated namesake.
Summary
Carry owner paths and language-specific lookup context through full extraction, cache reuse, and CLI/watch updates. Cover lexical/import/inheritance precedence, aliases, field declaration context, overload-local shadowing, and qualified Swift constructor locals.
Type of change
Verification & Invariants
Distinct owners must retain distinct declarations. An unrelated competing type must not redirect or erase a correctly scoped call in the covered cases. Missing or ambiguous targets stay unresolved rather than falling back to an unrelated bare method name. Preserve Ruby's already-qualified identities and C# overload/partial-type conventions.
Nested node IDs change; cache schema advances from released 45 to 47. Existing graphs require a full rebuild (
graphify extract . --code-only --force); regenerate dependent source-evidence artifacts.Limitations: this is not general compiler-grade type inference. Scala cross-file qualified/imported-call support is not added. The full repository test suite and global Pyright were not rerun for this final patch; the scoped Pyright check still reports 33 existing diagnostics, not a clean type check.
Merge Danger
Door: two-way
Code can be reverted, but upgrade and rollback both require rebuilding affected persisted graphs and dependent evidence.
Blast Radius: Extraction
Nested identities and contextual receiver lookup affect structural graphs and their incremental consumers.
How was this tested?
Windows, isolated CPython 3.14.3 checking environment with language extras. Commands below use
pythonfor that interpreter. No provider/API calls or private source data were used.Also executed both exact Python reproduction snippets published in #5185 in an isolated CPython 3.13.14 environment: all 14 same-file rows and 10 cross-file rows matched their expected declaration/call counts.
Pre-push scanning of the single unpublished commit passed: pinned gitleaks 8.27.2 plus all 12 local sensitive-data pattern rules, zero findings, no allowlists used. Only the 14 contribution files are included; no generated graphs, source-evidence payloads, environments or local validation logs are committed.
Evidence
Graphify-specific checklist
uv run python -m tools.skillgen --bless) when changing their source fragments.Skill generation is not applicable: no source fragments changed; the generated-artifact check passed.