Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 40 additions & 5 deletions graphify/watch.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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


Expand Down
37 changes: 36 additions & 1 deletion tests/test_watch.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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__
Expand Down