fix(watch): schedule a sync when a directory is deleted (#1313)

A directory deletion arrives as ONE event on the directory's own path.
That path has no source extension, so handleChange dropped it at the
isSourceFile gate before ever scheduling a sync — and the files inside
may never get events of their own (Windows's recursive watcher reports
only the top-most removed entry; FSEvents can coalesce a tree deletion
the same way). Every child record then sat stale in the index until an
unrelated edit happened to trigger a sync (#1285).

A non-source path that no longer EXISTS on disk now schedules the
debounced sync; the sync's scan-diff removes whatever vanished (already
correct — verified: manual `codegraph sync` cascades fine). Events for
live non-source files stay fully ignored, so build churn schedules
nothing.

Fixes #1285

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-07-16 15:27:10 -05:00
committed by GitHub
co-authored by Claude Fable 5
parent 18f0745f81
commit 8dcf92f285
3 changed files with 97 additions and 3 deletions
+33 -1
View File
@@ -551,7 +551,10 @@ export class FileWatcher {
if (!rel || rel === '.' || rel.startsWith('..')) return;
if (this.isAlwaysIgnored(rel)) return;
if (this.ignoreMatcher && this.ignoreMatcher.ignores(rel)) return;
if (!isSourceFile(rel, loadExtensionOverrides(this.projectRoot))) return;
if (!isSourceFile(rel, loadExtensionOverrides(this.projectRoot))) {
this.maybeScheduleForRemovedDir(rel);
return;
}
logDebug('File change detected', { file: rel });
if (this.ready) {
@@ -565,6 +568,35 @@ export class FileWatcher {
this.scheduleSync();
}
/**
* A deleted DIRECTORY arrives as one event on the directory's own path —
* no source extension, so the source-file filter drops it, and the files
* underneath may never get events of their own (Windows's recursive
* watcher reports only the top-most removed entry; macOS FSEvents can
* coalesce a tree deletion the same way). The index then kept every child
* record until some unrelated edit happened to trigger a sync (#1285).
*
* If a non-source path no longer exists on disk, treat it as a potential
* subtree removal and schedule the debounced sync — its scan-diff removes
* whatever is gone, which is the ground truth for what was underneath.
* pendingFiles is left alone (we can't know the children from the event).
* An event for an EXISTING non-source file stays fully ignored, so build
* churn on live files never schedules work; a deleted non-source file
* costs at most one no-op scan-diff, absorbed by the debounce.
*/
private maybeScheduleForRemovedDir(rel: string): void {
try {
fs.statSync(path.join(this.projectRoot, rel));
return; // still on disk — an ordinary non-source change, ignore
} catch {
/* gone — fall through */
}
logDebug('Non-source path removed; scheduling sync for possible directory removal', {
path: rel,
});
this.scheduleSync();
}
/** Close and forget the watch for a directory that errored/was removed. */
private unwatchDir(dir: string): void {
const w = this.dirWatchers.get(dir);