Skip to content

extract_svelte() regex fallback stamps stub-node source_file to importer's path, corrupting target file metadata after #701 #712

Description

@jippi

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions