diff --git a/CHANGELOG.md b/CHANGELOG.md index c95be258..45474c92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Full release notes with details on each version: [GitHub Releases](https://githu - Feat: `graphify extract --global --as ` flag -- after building a project graph, auto-registers it into the global graph in one step (#729) - Feat: `merge-graphs` now prefix-relabels each input graph before composing, preventing silent node ID collisions when two projects share entity names (#729) - Fix: `deduplicate_entities` raises `ValueError` if called with nodes spanning multiple repos (cross-project dedup disabled by design -- per-project graphs are deduplicated in isolation) (#729) +- Fix: `detect_incremental()` now accepts and forwards `follow_symlinks` to `detect()`. Without this, `--update` runs silently miss any files reached through a symlinked sub-tree (e.g. `state_of_truth/` symlinking to a directory outside the corpus root), even when the original full run had detected them. Previously the flag was on `detect()` and `collect_files()` only. (#736) ## 0.7.6 (2026-05-05) diff --git a/graphify/detect.py b/graphify/detect.py index 50f9e2c5..4b879636 100644 --- a/graphify/detect.py +++ b/graphify/detect.py @@ -771,7 +771,12 @@ def save_manifest(files: dict[str, list[str]], manifest_path: str = _MANIFEST_PA Path(manifest_path).write_text(json.dumps(manifest, indent=2), encoding="utf-8") -def detect_incremental(root: Path, manifest_path: str = _MANIFEST_PATH) -> dict: +def detect_incremental( + root: Path, + manifest_path: str = _MANIFEST_PATH, + *, + follow_symlinks: bool = False, +) -> dict: """Like detect(), but returns only new or modified files since the last run. Fast path: mtime unchanged → unchanged (free, no hash). @@ -779,8 +784,13 @@ def detect_incremental(root: Path, manifest_path: str = _MANIFEST_PATH) -> dict: treat as unchanged. Different hash = actually changed, re-extract. Backwards compatible with legacy manifests storing plain float mtime values. + + The ``follow_symlinks`` flag is forwarded to :func:`detect` so corpora that + rely on symlinked sub-trees (e.g. a ``state_of_truth/`` symlink pointing to a + directory outside the scan root) are scanned consistently between full and + incremental runs. """ - full = detect(root) + full = detect(root, follow_symlinks=follow_symlinks) manifest = load_manifest(manifest_path) if not manifest: diff --git a/tests/test_detect.py b/tests/test_detect.py index 6ded2fd0..000de351 100644 --- a/tests/test_detect.py +++ b/tests/test_detect.py @@ -1,5 +1,5 @@ from pathlib import Path -from graphify.detect import classify_file, count_words, detect, FileType, _looks_like_paper, _is_ignored, _load_graphifyignore +from graphify.detect import classify_file, count_words, detect, detect_incremental, save_manifest, FileType, _looks_like_paper, _is_ignored, _load_graphifyignore FIXTURES = Path(__file__).parent / "fixtures" @@ -220,6 +220,33 @@ def test_detect_handles_circular_symlinks(tmp_path): assert any("main.py" in f for f in result["files"]["code"]) +def test_detect_incremental_propagates_follow_symlinks(tmp_path, monkeypatch): + """detect_incremental must forward follow_symlinks so symlinked sub-trees + appear in incremental scans the same way they appear in full scans.""" + monkeypatch.chdir(tmp_path) + + real_dir = tmp_path / "real_corpus" + real_dir.mkdir() + (real_dir / "note.md").write_text("# real note\n\nsome content") + (tmp_path / "linked_corpus").symlink_to(real_dir) + + manifest_path = str(tmp_path / "manifest.json") + + # Without following symlinks, the symlinked dir contents are invisible. + no_link = detect_incremental(tmp_path, manifest_path, follow_symlinks=False) + assert not any("linked_corpus" in f for f in no_link["files"]["document"]) + + # With follow_symlinks=True, the symlinked dir contents appear and are new. + yes_link = detect_incremental(tmp_path, manifest_path, follow_symlinks=True) + assert any("linked_corpus" in f for f in yes_link["files"]["document"]) + assert yes_link["new_total"] >= 2 # real + linked + + # After saving manifest, a second incremental scan should see no changes. + save_manifest(yes_link["files"], manifest_path) + second = detect_incremental(tmp_path, manifest_path, follow_symlinks=True) + assert second["new_total"] == 0 + + def test_classify_video_extensions(): """Video and audio file extensions should classify as VIDEO.""" from graphify.detect import FileType