Summary
After #701 fixed the alias-resolution + synthetic-ID problems in extract_svelte(), dynamic_import edges now resolve to real file nodes correctly. But the regex fallback still creates a stub node for the import target with the wrong source_file: it stamps the importer's path instead of the target's path.
When _extract_generic later runs on the target file and emits its own canonical file node (same ID), build_from_json does last-write-wins on attributes (G.add_node overwrite). Depending on extraction order, the importer-supplied stub can clobber the correct source_file from the real extraction.
The [contains] edges (file → its functions) are still emitted correctly, but anything that reads source_file off a file node — display, "where is X defined", blast-radius, community summaries — sees the file as living inside its importer.
Repro (real SvelteKit app, 1,873 files)
# After `graphify update .` on graphifyy 0.7.5
import json
g = json.load(open('graphify-out/graph.json'))
for n in g['nodes']:
if n['id'] == 'src_lib_components_library_manage_library_entry_popover_svelte':
print(n['label']) # $lib/components/library/manage-library-entry-popover.svelte
print(n['source_file']) # src/partials/Series/Grid/Card/series-card-library.svelte ← WRONG
The popover lives at src/lib/components/library/manage-library-entry-popover.svelte. Its source_file is stamped to a consumer (series-card-library.svelte) that does {#await import('$lib/components/library/manage-library-entry-popover.svelte')}.
Scope on this repo: 16 svelte files affected (every dynamically-imported component I sampled). Examples:
| Component (label) |
Stamped source_file (wrong) |
$lib/components/ui/confirm-delete-dialog/...svelte |
src/routes/+layout.svelte |
$lib/components/ui/sonner/sonner.svelte |
src/routes/+layout.svelte |
$lib/components/series/similar/similar-series-section.svelte |
src/routes/(series)/[id=id]/(view)/(meta)/+page.svelte |
$lib/components/widget/custom-rating.svelte |
src/lib/components/search/search-form.svelte |
Root cause
graphify/extract.py in extract_svelte() (~lines 1797–1806):
result.setdefault("nodes", []).append({
"id": node_id, "label": raw,
"file_type": "code", "source_file": str(path), # ← bug: path is the importer
"confidence": "EXTRACTED",
})
path is the file currently being extracted (the importer), but node_id represents the imported target — derived from resolved (relative) or resolved_alias (tsconfig alias) earlier in the function. The mismatch is what gets persisted.
build_from_json (graphify/build.py:81–84) does:
for node in extraction.get("nodes", []):
G.add_node(node["id"], **{k: v for k, v in node.items() if k != "id"})
NetworkX add_node overwrites attributes for existing IDs, and the build() docstring confirms "last extraction's attributes win." So whether the stub or the real node lands last is purely an artifact of file-iteration order.
Suggested fix
Thread the resolved target path through so both extractions agree on source_file:
if raw.startswith("."):
target_source_file = str(resolved)
elif resolved_alias is not None:
target_source_file = str(resolved_alias)
else:
target_source_file = "" # external / bare module — unknown
result.setdefault("nodes", []).append({
"id": node_id, "label": raw,
"file_type": "code",
"source_file": target_source_file,
"confidence": "EXTRACTED",
})
Environment
- graphifyy 0.7.5 (uv tool install)
- Python 3.14
- Real-world SvelteKit 2.59 app, 1,873 files, 968
.svelte nodes
Summary
After #701 fixed the alias-resolution + synthetic-ID problems in
extract_svelte(), dynamic_import edges now resolve to real file nodes correctly. But the regex fallback still creates a stub node for the import target with the wrongsource_file: it stamps the importer's path instead of the target's path.When
_extract_genericlater runs on the target file and emits its own canonical file node (same ID),build_from_jsondoes last-write-wins on attributes (G.add_nodeoverwrite). Depending on extraction order, the importer-supplied stub can clobber the correctsource_filefrom the real extraction.The
[contains]edges (file → its functions) are still emitted correctly, but anything that readssource_fileoff a file node — display, "where is X defined", blast-radius, community summaries — sees the file as living inside its importer.Repro (real SvelteKit app, 1,873 files)
The popover lives at
src/lib/components/library/manage-library-entry-popover.svelte. Itssource_fileis stamped to a consumer (series-card-library.svelte) that does{#await import('$lib/components/library/manage-library-entry-popover.svelte')}.Scope on this repo: 16 svelte files affected (every dynamically-imported component I sampled). Examples:
$lib/components/ui/confirm-delete-dialog/...sveltesrc/routes/+layout.svelte$lib/components/ui/sonner/sonner.sveltesrc/routes/+layout.svelte$lib/components/series/similar/similar-series-section.sveltesrc/routes/(series)/[id=id]/(view)/(meta)/+page.svelte$lib/components/widget/custom-rating.sveltesrc/lib/components/search/search-form.svelteRoot cause
graphify/extract.pyinextract_svelte()(~lines 1797–1806):pathis the file currently being extracted (the importer), butnode_idrepresents the imported target — derived fromresolved(relative) orresolved_alias(tsconfig alias) earlier in the function. The mismatch is what gets persisted.build_from_json(graphify/build.py:81–84) does:NetworkX
add_nodeoverwrites attributes for existing IDs, and thebuild()docstring confirms "last extraction's attributes win." So whether the stub or the real node lands last is purely an artifact of file-iteration order.Suggested fix
Thread the resolved target path through so both extractions agree on
source_file:Environment
.sveltenodes