[Bug] AST extractor emits no cross-file call edges (observed on Swift; affects all non-Python languages)
Summary
graphify's AST extractor builds label_to_nid per-file (extract.py L845-849) and only emits a calls edge when the callee resolves within the caller's own file. Foo.bar() where Foo lives in another file silently produces no edge. In a 726-node Swift graph (SrutiFlow, 160+ files) this yields 0 cross-file calls edges out of 125 total. GRAPH_REPORT.md then flags widely-used utilities as "isolated nodes"/"knowledge gaps" — a false positive.
Expected
calls edges should connect caller to callee regardless of declaring file. Build a project-wide symbol table after per-file AST extraction and resolve call sites against it.
Actual
label_to_nid (L845-849) is built only from the current file's nodes.
tgt_nid = label_to_nid.get(callee_name.lower()) (L949) returns None for cross-file callees; no edge is appended.
_resolve_cross_file_imports (L2039-2170) is Python-only (L2652-2658) and handles only from .module import Name, not call sites.
- Swift resolver (L861-872) pulls the
navigation_suffix simple_identifier — the last component of Foo.bar(), "bar". The receiver is discarded, so a global table keyed on Type.method could not be queried as written.
Side bug: save_cached / save_semantic_cache (cache.py L63-77, L119-154) key AST and semantic passes with the same file_hash(path); semantic runs clobber AST cache. In SrutiFlow, NetworkConfiguration.swift therefore has no AST nodes at all in graph.json — only the LLM description node (source_location: null).
Reproduction
// util.swift
enum Util { static func run() {} }
// caller.swift
func go() { Util.run() }
Run graphifyy on the folder. Expected: calls edge go → Util.run. Actual: none; Util reported isolated.
Evidence from SrutiFlow
Graph degree from graph.json. Caller count = distinct non-declaring .swift files referencing the symbol (excluding comment-only mentions).
| Symbol |
Declared at |
Graph degree |
Non-declaring callers |
Missing edges (examples) |
NetworkConfiguration (enum) |
SrutiFlow/Services/Network/NetworkConfiguration.swift |
0 (only semantic LLM node) |
12 (11 prod + 1 test) |
{Google,Anthropic,OpenAICompatible}LLMService, LLMProviderFactory, {Deepgram,Gladia,OpenAICompatible,OpenRouter,File}STTService, AudioFileValidator, CertificatePinningDelegate, CloudLLMServiceTests |
InputSanitizer (enum) |
SrutiFlow/Services/Security/InputSanitizer.swift |
4 (all intra-file) |
4 (2 prod + 2 test) |
LLMProcessingService, KeyboardViewModel, SecurityServicesTests (9 calls), IntegrationTests (2 calls) |
ErrorRecovery (enum) |
SrutiFlow/Services/Security/ErrorRecovery.swift |
3 (all intra-file) |
1 (test only; the STT grep hit is a comment) |
SecurityServicesTests.recoverySuggestion × 8 |
Global check: of 125 calls edges, 0 span files (source_file of source ≠ target); 118 same-file, 7 with one endpoint missing metadata.
Root cause (confirmed in source)
graphify/extract.py:
- L845-849:
label_to_nid local to _extract_generic, rebuilt per file.
- L949-950: edge appended only if callee resolves locally.
- L861-872 (Swift): drops receiver of
navigation_expression.
- L972-977:
valid_ids = seen_ids filter discards any cross-file edge.
- L2652-2668 +
_resolve_cross_file_imports: cross-file linking only for .py, only for imports.
graphify/cache.py L63-77, L119-154: AST and semantic share one hash-keyed cache entry; latter overwrites former.
Impact
- "Isolated nodes" / "knowledge gaps" in
GRAPH_REPORT.md are unreliable on non-Python codebases; foundational utilities are flagged as dead code despite being project-wide dependencies.
- Community detection and centrality/betweenness run on a graph missing most real call edges; clusters and importance rankings are systematically skewed.
- Users following graphify's dead-code signal risk deleting live code. Worth a README warning until fixed.
Suggested fix direction
Generalize _resolve_cross_file_imports into a language-agnostic second pass that also resolves call sites. For navigation-style syntax (Swift/Kotlin/Scala) preserve the receiver so callees can be looked up as Type.method. Give AST and semantic extractions distinct cache keys.
Environment
- graphify (pypi
graphifyy): 0.4.1
- Python: 3.10.19
- OS: macOS / Darwin 25.4.0
- tree-sitter: 0.25.2, tree-sitter-swift: 0.0.1
- Target codebase: Swift 6, iOS 17+
[Bug] AST extractor emits no cross-file call edges (observed on Swift; affects all non-Python languages)
Summary
graphify's AST extractor buildslabel_to_nidper-file (extract.pyL845-849) and only emits acallsedge when the callee resolves within the caller's own file.Foo.bar()whereFoolives in another file silently produces no edge. In a 726-node Swift graph (SrutiFlow, 160+ files) this yields 0 cross-filecallsedges out of 125 total.GRAPH_REPORT.mdthen flags widely-used utilities as "isolated nodes"/"knowledge gaps" — a false positive.Expected
callsedges should connect caller to callee regardless of declaring file. Build a project-wide symbol table after per-file AST extraction and resolve call sites against it.Actual
label_to_nid(L845-849) is built only from the current file's nodes.tgt_nid = label_to_nid.get(callee_name.lower())(L949) returnsNonefor cross-file callees; no edge is appended._resolve_cross_file_imports(L2039-2170) is Python-only (L2652-2658) and handles onlyfrom .module import Name, not call sites.navigation_suffixsimple_identifier— the last component ofFoo.bar(),"bar". The receiver is discarded, so a global table keyed onType.methodcould not be queried as written.Side bug:
save_cached/save_semantic_cache(cache.pyL63-77, L119-154) key AST and semantic passes with the samefile_hash(path); semantic runs clobber AST cache. In SrutiFlow,NetworkConfiguration.swifttherefore has no AST nodes at all ingraph.json— only the LLM description node (source_location: null).Reproduction
Run
graphifyyon the folder. Expected:callsedgego → Util.run. Actual: none;Utilreported isolated.Evidence from SrutiFlow
Graph degree from
graph.json. Caller count = distinct non-declaring.swiftfiles referencing the symbol (excluding comment-only mentions).NetworkConfiguration(enum)SrutiFlow/Services/Network/NetworkConfiguration.swift{Google,Anthropic,OpenAICompatible}LLMService,LLMProviderFactory,{Deepgram,Gladia,OpenAICompatible,OpenRouter,File}STTService,AudioFileValidator,CertificatePinningDelegate,CloudLLMServiceTestsInputSanitizer(enum)SrutiFlow/Services/Security/InputSanitizer.swiftLLMProcessingService,KeyboardViewModel,SecurityServicesTests(9 calls),IntegrationTests(2 calls)ErrorRecovery(enum)SrutiFlow/Services/Security/ErrorRecovery.swiftSecurityServicesTests.recoverySuggestion× 8Global check: of 125
callsedges, 0 span files (source_fileof source ≠ target); 118 same-file, 7 with one endpoint missing metadata.Root cause (confirmed in source)
graphify/extract.py:label_to_nidlocal to_extract_generic, rebuilt per file.navigation_expression.valid_ids = seen_idsfilter discards any cross-file edge._resolve_cross_file_imports: cross-file linking only for.py, only forimports.graphify/cache.pyL63-77, L119-154: AST and semantic share one hash-keyed cache entry; latter overwrites former.Impact
GRAPH_REPORT.mdare unreliable on non-Python codebases; foundational utilities are flagged as dead code despite being project-wide dependencies.Suggested fix direction
Generalize
_resolve_cross_file_importsinto a language-agnostic second pass that also resolves call sites. For navigation-style syntax (Swift/Kotlin/Scala) preserve the receiver so callees can be looked up asType.method. Give AST and semantic extractions distinct cache keys.Environment
graphifyy): 0.4.1