feat(detect): auto-detect symlinked children when follow_symlinks is unset#887
Merged
safishamsi merged 1 commit intoMay 16, 2026
Merged
Conversation
…unset
When `root` has at least one direct symlinked child, default to following
symlinks instead of silently dropping their contents. This makes "fake
working dir" patterns (a folder full of symlinks pointing at scattered
source dirs) work transparently — the caller no longer has to know to
pass `follow_symlinks=True`.
Concrete motivating shape:
~/projects/research-corpus/
├── papers -> /external/drive/papers
├── notes -> /external/drive/notes
└── code -> /external/drive/code
Before: `--update` invoked `detect_incremental(root)` without passing
`follow_symlinks=True`, so the scan saw the small set of files actually
inside the root and missed everything reachable only through a symlink.
Result: every legitimate new file was missed, and the manifest paths
were marked as deleted.
After: when `follow_symlinks` is left at its default `None`, `detect()`
runs `_auto_follow_symlinks(root)` (one cheap `iterdir()` + `is_symlink()`
loop) and follows symlinks if any direct child is one. Behaviour is
unchanged for ordinary scans (no direct symlinks → False, as before).
Override is always possible by passing an explicit `follow_symlinks=True`
or `follow_symlinks=False`; existing tests confirming the explicit
behaviour continue to pass unchanged.
Backwards compatibility:
- Type annotation: `bool` -> `bool | None`. Callers passing `True`/`False`
continue to work identically. Callers passing nothing get the new
auto-detect.
- No new dependencies.
- Cheap: one `iterdir()` call once per detect() invocation.
Tests:
- 3 new tests in tests/test_detect.py:
- test_detect_auto_detects_direct_symlink_child
- test_detect_default_does_not_follow_when_no_symlinks
- test_detect_explicit_false_overrides_auto_detect
- Full tests/test_detect.py + tests/test_incremental.py: 49/49 pass.
alphanury
force-pushed
the
feat/auto-detect-symlinks-default
branch
from
May 16, 2026 00:23
97ed77f to
5e178b9
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Default
follow_symlinksbehaviour indetect()/detect_incremental()changes from "always False" to "auto-detect when unset". When the scan root contains at least one direct symlinked child, symlinks are followed by default. Behaviour is unchanged for ordinary scans (no direct symlinks → False, as before) and explicit overrides keep working.Motivation
A common pattern is a "fake working dir" — a folder full of symlinks pointing at scattered source dirs across the user's machine, e.g.:
graphify --update(skill-driven) callsdetect_incremental(root)withoutfollow_symlinks=True. Pre-fix, the scan saw only the few internalgraphify-out/artefacts and missed the real source files reachable through symlinks. Result: the manifest reported the previously-scanned paths as deleted and every legitimate new file was missed silently.The earlier fix added
follow_symlinkspropagation fromdetect_incrementaltodetect. This PR is the next logical step: make the default Just Work for symlinked roots, since the user has no opportunity to pass the flag when invoking via the standard skill / CLI.Implementation
_auto_follow_symlinks(root: Path) -> bool— oneiterdir()+is_symlink()loop, cheap (~µs).detect(root, *, follow_symlinks: bool | None = None, ...)— whenNone, auto-detect via the helper. ExplicitTrue/Falsestill respected.detect_incremental(root, ..., *, follow_symlinks: bool | None = None, ...)— same default, propagated todetect().Backwards compatibility
bool→bool | None. Callers passingTrue/Falsework identically. Callers passing nothing now get auto-detect (instead of False).follow_symlinks=Falseexplicitly — kept and tested.Tests
3 new tests in
tests/test_detect.py:test_detect_auto_detects_direct_symlink_child— root with symlinked child + no kwarg → follows.test_detect_default_does_not_follow_when_no_symlinks— root without symlinks + no kwarg → legacy False, scan succeeds.test_detect_explicit_false_overrides_auto_detect— root with symlinked child + explicitfollow_symlinks=False→ does NOT follow (override preserved).Full suite locally:
tests/test_detect.py+tests/test_incremental.py→ 49/49 pass.Test plan
python3 -m pytest tests/test_detect.py tests/test_incremental.py(49 passed)graphify --updateon a symlink-heavy corpus — symlinked sub-trees are now scanned consistently between full and incremental runs (delta detected vs all-marked-deleted pre-fix)🤖 Generated with Claude Code