Skip to content

Deleted files leak into the graph permanently — incremental rebuild has no reconciliation backstop #2580

Description

@angmeng

Version: graphifyy 0.8.35 · macOS · post-commit hook

Summary

On a repo where the post-commit hook has been running since June, graph.json accumulated
683 nodes (3.9%) whose source_file no longer exists, plus 1,068 edges touching them.
84% came from a single directory removed in one commit six weeks ago. Three separate
issues combine to make this permanent:

1. _rebuild_code full-reconciliation branch filters on _CODE_EXTENSIONS, so docs are never evicted

In watch.py, the changed_paths is None branch reconciles existing nodes against the
current corpus to evict deleted files (#1007), but skips anything that is not a code file:

if Path(sf).suffix.lower() not in _CODE_EXTENSIONS:
    continue

_CODE_EXTENSIONS does not include .md. For a docs-heavy repo this means markdown nodes
are never evicted by any code path. In our graph, 182 of the 683 stale nodes are .md
and are unreachable by both branches.

This looks unintended: the branch exists specifically to evict vanished sources, and the
extension filter defeats that for exactly the file types many users index.

2. Rebuild already in progress - changes queued discards the change list; nothing is queued

acquire_lock=True takes a non-blocking flock and returns False when held — the
docstring says so. But the log line says "changes queued", and no queue exists. That
commit's changed_paths (including its deletions) is lost permanently.

Our log has 116 occurrences of this line, plus 40 hard failures
(exceeded 600s, Refusing to overwrite, No such file or directory: 'graphify-out').
Every one is a potential permanent leak, because the incremental path never revisits
anything outside the current commit's diff.

Suggest either wording it as "skipped" or actually recording the paths for the next run.

3. No reconciliation backstop

The post-commit hook always passes changed_paths, so the full-reconciliation branch
never runs in normal operation. Combined with (2), a single skipped run leaks forever.

A graphify prune subcommand — drop nodes whose source_file no longer exists, plus their
edges — would make this self-healing regardless of root cause. We wrote a 90-line local
script that does exactly this; happy to open a PR if the shape is wanted.

Repro sketch

  1. Index a repo containing a directory of .md files.
  2. git rm -r that directory and commit while another rebuild holds the lock
    (or simply let one of the 600s timeouts fire).
  3. Commit anything else. The removed files' nodes are still in graph.json, and no
    subsequent hook-driven rebuild removes them.

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