Summary
CLI --exclude patterns are applied only by the initial scan. Every other re-detection path — graphify update, the watcher, and the git-hook rebuilds — re-runs detect() without them, so deliberately excluded paths silently re-enter the graph on the first rebuild.
Verified on v8 HEAD (961b78e).
Repro
$ mkdir -p repro/vendor && cd repro
$ printf 'def foo():\n return 1\n' > main.py
$ printf 'def bar():\n return 2\n' > vendor/big.py
$ graphify extract . --exclude vendor --no-cluster
[graphify extract] found 1 code, 0 docs, 0 papers, 0 images
[graphify extract] wrote .../graph.json — 2 nodes, 1 edges (no clustering)
# graph source_files: ['main.py'] ✓ exclude honored
$ graphify update .
[graphify watch] Rebuilt: 4 nodes, 2 edges, 2 communities
# graph source_files: ['main.py', 'vendor/big.py'] ✗ vendor is back, silently
There is also no way to re-state the excludes at update time — update rejects the flag:
$ graphify update . --exclude vendor
error: unknown update option: --exclude
Root cause
--exclude is parsed into cli_excludes and forwarded as extra_excludes only in the extract command path (cli.py:2029-2032, cli.py:2100, cli.py:2112); detect() appends these root-anchored patterns last (detect.py:1163-1166).
extra_excludes is never persisted. The build saves the scan root (.graphify_root, watch.py:967) — which is what makes update feel like a faithful re-run of the original build — but not the exclude patterns.
_rebuild_code() (watch.py:721) has no extra_excludes parameter at all; its detect call is detect(watch_path, follow_symlinks=follow_symlinks) (watch.py:822).
- All four rebuild drivers therefore scan unfiltered:
update (cli.py:1275), the watch loop (watch.py:1251), the post-commit hook (hooks.py:134), and the post-checkout hook (hooks.py:175).
Suggested fix
Persist the build's extra_excludes alongside .graphify_root (or in manifest.json) and have _rebuild_code() re-apply them; optionally also accept --exclude on update as an explicit override. This is the same shape as #1800 (--exclude-hubs not surviving hook/watch rebuilds) — a single persisted "build options" sidecar would solve both.
Workaround (verified)
Put the patterns in a .graphifyignore at the scan root instead of using --exclude — ignore files are re-read from disk on every detect(), so they survive update/watch/hooks. One wrinkle: converting an already-polluted graph back needs graphify update --force, since the shrinking node count otherwise trips the safety check in to_json.
Context
Found while diagnosing #1873 (fix in #1885): with the nested-ignore bug live, a CLI --exclude .hypothesis workaround appeared to fix a zeroed scan, then regressed on the next update — which is how this gap surfaced.
🤖 Generated with Claude Code
Summary
CLI
--excludepatterns are applied only by the initial scan. Every other re-detection path —graphify update, the watcher, and the git-hook rebuilds — re-runsdetect()without them, so deliberately excluded paths silently re-enter the graph on the first rebuild.Verified on
v8HEAD (961b78e).Repro
There is also no way to re-state the excludes at update time —
updaterejects the flag:Root cause
--excludeis parsed intocli_excludesand forwarded asextra_excludesonly in theextractcommand path (cli.py:2029-2032,cli.py:2100,cli.py:2112);detect()appends these root-anchored patterns last (detect.py:1163-1166).extra_excludesis never persisted. The build saves the scan root (.graphify_root,watch.py:967) — which is what makesupdatefeel like a faithful re-run of the original build — but not the exclude patterns._rebuild_code()(watch.py:721) has noextra_excludesparameter at all; its detect call isdetect(watch_path, follow_symlinks=follow_symlinks)(watch.py:822).update(cli.py:1275), the watch loop (watch.py:1251), the post-commit hook (hooks.py:134), and the post-checkout hook (hooks.py:175).Suggested fix
Persist the build's
extra_excludesalongside.graphify_root(or inmanifest.json) and have_rebuild_code()re-apply them; optionally also accept--excludeonupdateas an explicit override. This is the same shape as #1800 (--exclude-hubsnot surviving hook/watch rebuilds) — a single persisted "build options" sidecar would solve both.Workaround (verified)
Put the patterns in a
.graphifyignoreat the scan root instead of using--exclude— ignore files are re-read from disk on everydetect(), so they surviveupdate/watch/hooks. One wrinkle: converting an already-polluted graph back needsgraphify update --force, since the shrinking node count otherwise trips the safety check into_json.Context
Found while diagnosing #1873 (fix in #1885): with the nested-ignore bug live, a CLI
--exclude .hypothesisworkaround appeared to fix a zeroed scan, then regressed on the nextupdate— which is how this gap surfaced.🤖 Generated with Claude Code