Skip to content

feat(detect): auto-detect symlinked children when follow_symlinks is unset#887

Merged
safishamsi merged 1 commit into
Graphify-Labs:v8from
alphanury:feat/auto-detect-symlinks-default
May 16, 2026
Merged

feat(detect): auto-detect symlinked children when follow_symlinks is unset#887
safishamsi merged 1 commit into
Graphify-Labs:v8from
alphanury:feat/auto-detect-symlinks-default

Conversation

@alphanury

@alphanury alphanury commented May 16, 2026

Copy link
Copy Markdown
Contributor

Summary

Default follow_symlinks behaviour in detect() / 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.:

~/projects/research-corpus/
├── papers   -> /external/drive/papers
├── notes    -> /external/drive/notes
└── code     -> /external/drive/code

graphify --update (skill-driven) calls detect_incremental(root) without follow_symlinks=True. Pre-fix, the scan saw only the few internal graphify-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_symlinks propagation from detect_incremental to detect. 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

  • New helper _auto_follow_symlinks(root: Path) -> bool — one iterdir() + is_symlink() loop, cheap (~µs).
  • detect(root, *, follow_symlinks: bool | None = None, ...) — when None, auto-detect via the helper. Explicit True/False still respected.
  • detect_incremental(root, ..., *, follow_symlinks: bool | None = None, ...) — same default, propagated to detect().
  • Docstring updated to document the new auto-detect behaviour.

Backwards compatibility

  • Type annotation boolbool | None. Callers passing True/False work identically. Callers passing nothing now get auto-detect (instead of False).
  • Behaviour unchanged when root has no direct symlinks (the auto-detect returns False, matching the old default).
  • Behaviour can be reverted at the call site at any time by passing follow_symlinks=False explicitly — kept and tested.
  • No new dependencies.

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 + explicit follow_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)
  • Manual run of graphify --update on 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

…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
alphanury force-pushed the feat/auto-detect-symlinks-default branch from 97ed77f to 5e178b9 Compare May 16, 2026 00:23
@safishamsi
safishamsi merged commit 1ba3e37 into Graphify-Labs:v8 May 16, 2026
2 checks passed
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.

2 participants