Skip to content

fix: capture removed files' cross-directory neighbors before purge#1983

Open
carlos-alm wants to merge 1 commit into
fix/issue-1838-bug-track-edits-sh-posttooluse-hookfrom
fix/issue-1839-directory-fan-in-fan-out-stale-for-a
Open

fix: capture removed files' cross-directory neighbors before purge#1983
carlos-alm wants to merge 1 commit into
fix/issue-1838-bug-track-edits-sh-posttooluse-hookfrom
fix/issue-1839-directory-fan-in-fan-out-stale-for-a

Conversation

@carlos-alm

@carlos-alm carlos-alm commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Follow-up to #1738/#1840. The incremental small-file fast path's directory-metrics refresh (refreshAffectedDirectoryMetrics in src/domain/graph/builder/stages/build-structure.ts, mirrored by refresh_affected_directory_metrics in crates/codegraph-core/src/features/structure.rs) discovers cross-directory neighbors by querying live import edges from the affected directories — one hop out from the ancestor directories of the touched files. This works correctly for added/modified files (their edges are freshly rebuilt and still present), but not for removed files: purgeFilesFromGraph/purgeFilesData deletes both directions of a removed file's edges before the structure stage runs, so there is no live evidence left to discover a neighbor directory that was connected only through that file.

Root cause: detectChanges/detect_changes never captured a removed file's cross-directory import neighbors before purging it.

Fix: Added captureRemovedFileNeighbors (WASM, in detect-changes.ts) and capture_removed_file_neighbors (native, in detect_changes.rs), which read a removed file's forward+reverse import-neighbor file set before the purge step runs (mirroring the existing save-then-purge pattern already used for reverse-dep edge reconnection). The result is threaded through PipelineContext.removedFileNeighbors (JS) / an extra return value from save_and_purge_changed (Rust) into refreshAffectedDirectoryMetrics/refresh_affected_directory_metrics, which now unions the neighbor files' ancestor directories into the affected-directory set before recomputing metrics.

Repro (from the issue)

// src/pkgA/a1.js — the ONLY file in pkgA that imports from pkgB
import { b1 } from '../pkgB/b1.js';
export function a1() { return b1(); }

// src/pkgA/a2.js — unrelated, no import of pkgB
export function a2() { return 2; }

// src/pkgB/b1.js
export function b1() { return 1; }

Full build → pkgB reports fanIn: 1 (correct). Delete a1.js, run an incremental rebuild (fast path fires) → pkgB still reported fanIn: 1 (stale) before this fix; now correctly reports fanIn: 0, matching a full rebuild. Verified on both engines.

Test plan

  • New regression test tests/integration/issue-1839-directory-fanin-fanout-stale-removal.test.ts reproduces the exact repro above for both the WASM and native engines, diffing the incremental result against an independent from-scratch full build (ground truth). Verified the test fails without the fix (reverted each side independently and confirmed the corresponding engine's test fails) and passes with it.
  • Added Rust unit tests for capture_removed_file_neighbors (forward+reverse directions, empty input, and exclusion of other files in the same removal batch).
  • npm test — full suite green (232 files, 3779 tests passed)
  • npm run lint — clean (files touched by this change)
  • cargo check -p codegraph-core / cargo test -p codegraph-core --lib detect_changes — clean
  • Native addon rebuilt locally, codesigned, and verified directly against the regression test
  • codegraph diff-impact --staged -T — contained blast radius (6 functions changed, 10 transitive callers, all within the builder pipeline)

Stacked on #1838 (base branch fix/issue-1838-bug-track-edits-sh-posttooluse-hook) — only this issue's diff-metrics-refresh change is included.

Fixes #1839

The small-incremental fast path's directory-metrics refresh
(refreshAffectedDirectoryMetrics / refresh_affected_directory_metrics)
discovers cross-directory neighbors by querying live import edges from
the affected directories. This works for added/modified files but not
for removed files, since purgeFilesFromGraph/purgeFilesData deletes
both directions of their edges before the structure stage runs, leaving
no live evidence to discover a lone connecting neighbor directory from.

Add captureRemovedFileNeighbors (WASM) / capture_removed_file_neighbors
(native), which reads a removed file's forward+reverse import-neighbor
file set in detectChanges/detect_changes, before the purge step, and
threads it through to the directory-metrics refresh so the neighbor
directory's ancestor chain still gets folded into the affected set.

Fixes #1839

Impact: 6 functions changed, 10 affected
@greptile-apps

greptile-apps Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes stale directory metrics after deleting files in the incremental graph build. The main changes are:

  • Captures removed files' forward and reverse import neighbors before purge.
  • Threads captured neighbor paths into JS and Rust structure refresh.
  • Adds regression coverage for stale fan-in and fan-out after removal.

Confidence Score: 4/5

The scoped JS rebuild path can still leave directory metrics stale after a file deletion.

  • Normal incremental and native flows thread the captured neighbors through the refresh path.
  • The scoped JS path can capture from an empty removed-file list before purge.
  • That leaves the same stale fan-in and fan-out behavior for scoped deletions.

src/domain/graph/builder/stages/detect-changes.ts

Important Files Changed

Filename Overview
src/domain/graph/builder/stages/detect-changes.ts Adds JS removed-file neighbor capture before purge, but scoped rebuilds can call it before the removed list is populated.
src/domain/graph/builder/stages/build-structure.ts Passes captured removed-file neighbors into the small incremental directory refresh.
src/domain/graph/builder/context.ts Adds a default removedFileNeighbors array to pipeline context.
crates/codegraph-core/src/domain/graph/builder/stages/detect_changes.rs Adds native removed-file neighbor capture with focused unit coverage.
crates/codegraph-core/src/domain/graph/builder/pipeline.rs Threads native removed-file neighbors from purge preparation into the structure phase.
crates/codegraph-core/src/features/structure.rs Includes captured removed-file neighbors when selecting directories to refresh.
tests/integration/issue-1839-directory-fanin-fanout-stale-removal.test.ts Adds an incremental-versus-full-build regression test for deleted cross-directory imports.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Detect removed files] --> B[Capture import neighbors before purge]
  B --> C[Purge removed file graph data]
  C --> D[Run structure refresh]
  D --> E[Include changed, removed, and captured neighbor ancestors]
  E --> F[Recompute directory metrics]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
  A[Detect removed files] --> B[Capture import neighbors before purge]
  B --> C[Purge removed file graph data]
  C --> D[Run structure refresh]
  D --> E[Include changed, removed, and captured neighbor ancestors]
  E --> F[Recompute directory metrics]
Loading

Fix All in Claude Code

Reviews (1): Last reviewed commit: "fix: capture removed files' cross-direct..." | Re-trigger Greptile

@@ -579,6 +614,7 @@ function handleScopedBuild(ctx: PipelineContext): void {
const changedRelPaths = new Set<string>([...changePaths, ...ctx.removed]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Scoped Removals Lose Neighbors

When a scoped incremental rebuild deletes a file, this call captures neighbors from ctx.removed, but the scoped path does not go through the normal branch that populates that list from change detection. removedFileNeighbors stays empty before the purge, so cross-directory metrics can remain stale for the deleted file's import neighbors in scoped builds.

Fix in Claude Code

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Codegraph Impact Analysis

6 functions changed10 callers affected across 5 files

  • PipelineContext in src/domain/graph/builder/context.ts:22 (4 transitive callers)
  • buildDirectoryStructure in src/domain/graph/builder/stages/build-structure.ts:51 (3 transitive callers)
  • refreshAffectedDirectoryMetrics in src/domain/graph/builder/stages/build-structure.ts:289 (3 transitive callers)
  • captureRemovedFileNeighbors in src/domain/graph/builder/stages/detect-changes.ts:394 (4 transitive callers)
  • handleScopedBuild in src/domain/graph/builder/stages/detect-changes.ts:606 (3 transitive callers)
  • handleIncrementalBuild in src/domain/graph/builder/stages/detect-changes.ts:637 (3 transitive callers)

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant