From a26695ca4d123e6edaa220126a94db95661cd4e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=9E=AC=ED=98=84?= <231584193+kimdzhekhon@users.noreply.github.com> Date: Thu, 16 Jul 2026 07:17:10 +0900 Subject: [PATCH] fix(watch): check-update walks the corpus, not just the pending flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit check_update() only ever inspected the needs_update flag file, which is written exclusively by the `graphify watch` daemon. Anyone who doesn't run that daemon got silent `exit 0` from `graphify check-update` no matter how far the corpus had drifted since the last extract — new files (never in the manifest at all) and deleted files were both invisible. Wire detect_incremental(kind="ast") into check_update using the same persisted --exclude patterns as a real rebuild (_read_build_excludes), so it now reports new/changed and deleted file counts against the manifest. Also make success non-silent: an unambiguous "Up to date" line now distinguishes a real clean check from a check that couldn't run (e.g. no manifest yet). Fixes #1765. --- graphify/watch.py | 45 ++++++++++++++++++++++++++++++++++++++++----- tests/test_watch.py | 37 ++++++++++++++++++++++++++++++++++++- 2 files changed, 76 insertions(+), 6 deletions(-) diff --git a/graphify/watch.py b/graphify/watch.py index a4eee5883..b7366f740 100644 --- a/graphify/watch.py +++ b/graphify/watch.py @@ -233,6 +233,7 @@ def _git_head() -> str | None: IMAGE_EXTENSIONS, _load_graphifyignore, _is_ignored, + detect_incremental, ) _WATCHED_EXTENSIONS = CODE_EXTENSIONS | DOC_EXTENSIONS | PAPER_EXTENSIONS | IMAGE_EXTENSIONS @@ -1266,17 +1267,51 @@ def _add_deleted_source(path: Path) -> None: def check_update(watch_path: Path) -> bool: - """Check for pending semantic update flag and notify the user if set. + """Check for pending semantic update flag and corpus drift, notify if either is set. Cron-safe: always returns True so cron jobs do not alarm. Non-code file changes (docs, papers, images) require LLM-backed - re-extraction via `/graphify --update` — this function only signals - that the update is needed. + re-extraction via `/graphify --update` — the pending-flag check only + signals that the update is needed. + + Also walks the corpus with the same include/exclude rules as extract and + diffs it against the manifest, so files created (not just modified) since + the last extract are caught too — the flag alone only covers changes seen + by a running `graphify watch` daemon, and stayed silent for anyone who + doesn't run one (#1765). """ - flag = Path(watch_path) / _GRAPHIFY_OUT / "needs_update" - if flag.exists(): + watch_path = Path(watch_path) + out = watch_path / _GRAPHIFY_OUT + flag_pending = (out / "needs_update").exists() + if flag_pending: print(f"[graphify check-update] Pending non-code changes in {watch_path}.") + + drift_reported = False + if (out / "manifest.json").exists(): + try: + result = detect_incremental( + watch_path, + str(out / "manifest.json"), + kind="ast", + extra_excludes=_read_build_excludes(out) or None, + ) + new_total = result.get("new_total", 0) + deleted_total = len(result.get("deleted_files", [])) + if new_total or deleted_total: + drift_reported = True + parts = [] + if new_total: + parts.append(f"{new_total} new/changed") + if deleted_total: + parts.append(f"{deleted_total} deleted") + print(f"[graphify check-update] {', '.join(parts)} file(s) since the last extract in {watch_path}.") + except Exception as exc: + print(f"[graphify check-update] warning: could not check corpus drift: {exc}", file=sys.stderr) + + if flag_pending or drift_reported: print("[graphify check-update] Run `/graphify --update` to apply semantic re-extraction.") + else: + print(f"[graphify check-update] Up to date — no changes detected in {watch_path}.") return True diff --git a/tests/test_watch.py b/tests/test_watch.py index fc91cf181..f573f4bf6 100644 --- a/tests/test_watch.py +++ b/tests/test_watch.py @@ -59,7 +59,8 @@ def test_watched_extensions_excludes_noise(): # --- watch() import error without watchdog --- def test_check_update_no_flag_returns_true(tmp_path): - """check_update returns True and is silent when needs_update flag is absent.""" + """check_update returns True and reports up-to-date when nothing is pending + and there's no manifest yet to diff against.""" from graphify.watch import check_update assert check_update(tmp_path) is True @@ -86,6 +87,40 @@ def test_check_update_does_not_clear_flag(tmp_path): assert flag.exists() +def test_check_update_reports_new_file_absent_from_manifest(tmp_path, capsys): + """A file created after the last extract has no manifest row at all — the + old flag-only check never caught this since only a running `graphify + watch` daemon set the flag. check_update must now detect it directly by + diffing the corpus against the manifest (#1765).""" + from graphify.watch import check_update + from graphify.detect import save_manifest + + (tmp_path / "existing.py").write_text("pass\n") + manifest_path = tmp_path / "graphify-out" / "manifest.json" + save_manifest({"code": [str(tmp_path / "existing.py")]}, str(manifest_path), root=tmp_path) + + (tmp_path / "new_module.py").write_text("pass\n") + + assert check_update(tmp_path) is True + out = capsys.readouterr().out + assert "1 new/changed" in out + assert "graphify --update" in out + + +def test_check_update_reports_up_to_date_when_manifest_matches_corpus(tmp_path, capsys): + """No drift, no pending flag -> explicit up-to-date status, not silence.""" + from graphify.watch import check_update + from graphify.detect import save_manifest + + (tmp_path / "existing.py").write_text("pass\n") + manifest_path = tmp_path / "graphify-out" / "manifest.json" + save_manifest({"code": [str(tmp_path / "existing.py")]}, str(manifest_path), root=tmp_path) + + assert check_update(tmp_path) is True + out = capsys.readouterr().out + assert "Up to date" in out + + def test_watch_raises_without_watchdog(tmp_path, monkeypatch): import builtins real_import = builtins.__import__