Skip to content

Incremental update loses cross-file edges from changed→unchanged files (batch-scoped import resolution + replace-on-re-extract) #2406

Open

Description

@Unreal4598

Version: graphifyy 0.9.19 · Windows 11 · Python 3.14 · TS/TSX corpus (~540 code files)

Symptom

After an incremental update (re-extracting only changed files, then build_merge), the graph silently loses every cross-file edge from a changed file to an unchanged file. In our repo a utility imported by ~every component (v2/src/lib/cn.ts, byte-identical import { cn } from "@/lib/cn" across files) dropped from ~100 edges to 4, and its neighborhood became a disconnected island. ~610 edges were lost in one update.

The failure signature is exact and makes the cause easy to confirm:

edge outcome
changed → changed (case-detail.tsx → text.ts) survives
unchanged → unchanged (Kbd.tsx → cn.ts) survives
changed → unchanged (case-detail.tsx → cn.ts) deleted

Root cause

  1. extract() resolves cross-file imports only against the batch it is given (_augment_symbol_resolution_edges(paths, ...)). When the update batch contains only changed files, imports of unchanged files resolve to nothing — those edges are simply never produced.
  2. build_merge's replace-on-re-extract then drops the changed file's old (complete) edge set and replaces it with the new (gutted) one.

Each behavior is individually reasonable; combined they guarantee edge loss on every incremental update of a codebase.

Minimal repro

from pathlib import Path
from graphify.extract import extract
from graphify.build import build_merge
# A.ts imports { b } from "./B"
r_full = extract([Path("A.ts"), Path("B.ts")], cache_root=None)   # A→B edge present
r_incr = extract([Path("A.ts")], cache_root=None)                 # A→B edge absent (B not in batch)
# build_merge([r_incr], graph_path=...) replaces A's edges → A→B edge deleted from graph.json

Suggested fix

Since AST extraction is deterministic and cheap (~30 s for 500 files, no LLM), the update path could always run structural extraction over the full corpus and be incremental only for semantic (LLM) extraction. Alternatively, _augment_symbol_resolution_edges could resolve against the existing graph.json node index in addition to the batch.

Secondary finding: dangling edges to never-created node ids

In a full, clean extraction of the same corpus, ~356 edge endpoints reference node ids that no node pass ever creates:

  • Barrel re-exports: imports of Button/Surface/Input through v2/src/design/primitives/index.ts emit edges to e.g. ..._primitives_index_button, but no such node exists (the components live in Button.tsx as ..._primitives_button_button).
  • Wrapper consts: re-exported consts like const Dialog = DialogPrimitive.Root get edges (..._ui_dialog_dialog) but no node.
  • Some of these ids also carry an absolute-path prefix (c_users_<user>_dev_<repo>_v2_src_...) that escaped the relativization/id-remap pass, so they can't even be suffix-matched to real nodes.

Happy to provide the full dangling-id list or run diagnostics against a branch.

Activity

  1. aryanbonigala commented on Aug 3, 2026

    @aryanbonigala
    Contributor

    I’m reproducing this against the current v8 head and investigating a focused fix that preserves changed-to-unchanged cross-file edges without forcing full-corpus re-extraction.

  2. omsenjalia commented on Oct 18, 2026

    @omsenjalia
    Contributor

    This looks fixed on the v8 branch by 4e7e6b1f (fix(extract,watch): C# assembly-aware partial merge, incremental edge preservation, extract crash recovery; bump to 0.9.33). Could a maintainer confirm and close?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions