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
20 changes: 20 additions & 0 deletions graphify/watch.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@
from graphify.paths import GRAPHIFY_OUT as _GRAPHIFY_OUT, is_absolute_any_platform
_PENDING_FILENAME = ".pending_changes"
_PENDING_DRAIN_MAX_PASSES = 20
# #3045: below this corpus size the post-extraction phases (build/cluster/
# analyze/report/export) finish fast enough that stage output would just be
# noise. Above it — the AST-extraction progress print in extract.py uses the
# same threshold — a large corpus can spend real time in each phase with no
# output at all, which reads as a hang instead of a build in progress.
_STAGE_PROGRESS_MIN_FILES = 100


def _queue_pending(out_dir: Path, changed_paths: list[Path]) -> None:
Expand Down Expand Up @@ -1649,6 +1655,9 @@ def _failed(f: str) -> bool:
"total_words": detected.get("total_words", 0),
}

_show_stage_progress = len(code_files) >= _STAGE_PROGRESS_MIN_FILES
if _show_stage_progress:
print("[graphify watch] Building graph...", flush=True)
# Inherit the existing graph's directed flag (#2342) so `graphify
# update` can't silently downgrade a directed graph to undirected -
# build_from_json defaults to directed=False otherwise.
Expand Down Expand Up @@ -1693,11 +1702,18 @@ def _failed(f: str) -> bool:
print("[graphify watch] No code-graph topology changes detected; outputs left untouched.")
return True

if _show_stage_progress:
print(
f"[graphify watch] Clustering graph ({G.number_of_nodes()} nodes, "
f"{G.number_of_edges()} edges)...", flush=True,
)
communities = cluster(G)
previous_node_community = _node_community_map(existing_graph_data)
if previous_node_community:
communities = remap_communities_to_previous(communities, previous_node_community)
cohesion = score_all(G, communities)
if _show_stage_progress:
print("[graphify watch] Analyzing graph structure...", flush=True)
gods = god_nodes(G)
surprises = surprising_connections(G, communities)
labels_file = out / ".graphify_labels.json"
Expand Down Expand Up @@ -1758,13 +1774,17 @@ def _failed(f: str) -> bool:
file=sys.stderr,
)
questions = suggest_questions(G, communities, labels)
if _show_stage_progress:
print("[graphify watch] Generating report...", flush=True)
from graphify.report import load_learning_for_report as _llfr
report = generate(G, communities, cohesion, labels, gods, surprises, detection,
{"input": 0, "output": 0}, report_root, suggested_questions=questions,
built_at_commit=commit, learning=_llfr(out / "graph.json"))
report_path = out / "GRAPH_REPORT.md"
labels_json = json.dumps({str(k): v for k, v in sorted(labels.items())}, ensure_ascii=False, indent=2) + "\n"
graph_tmp = out / ".graph.tmp.json"
if _show_stage_progress:
print("[graphify watch] Writing graph.json...", flush=True)
json_written = to_json(G, communities, str(graph_tmp), force=True, built_at_commit=commit, community_labels=labels)
if not json_written:
return False
Expand Down
39 changes: 39 additions & 0 deletions tests/test_watch.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,45 @@ def test_rebuild_code_writes_community_name(tmp_path):
)


def test_rebuild_code_reports_post_extraction_stage_progress(tmp_path, capsys):
"""#3045: on a large corpus, `graphify update` finished AST extraction (100%)
and then went silent through build/cluster/analyze/report/export, which read
as a hang on a slow machine instead of a build still in progress. Above the
same 100-file threshold extract.py's own AST-extraction progress print uses,
_rebuild_code must announce each post-extraction stage."""
from graphify.watch import _rebuild_code

corpus = tmp_path / "corpus"
corpus.mkdir()
for i in range(100):
(corpus / f"m{i}.py").write_text(
f"def f{i}():\n return f{(i + 1) % 100}()\n", encoding="utf-8"
)
assert _rebuild_code(corpus, acquire_lock=False) is True

out = capsys.readouterr().out
assert "[graphify watch] Building graph..." in out
assert "[graphify watch] Clustering graph (" in out
assert "[graphify watch] Analyzing graph structure..." in out
assert "[graphify watch] Generating report..." in out
assert "[graphify watch] Writing graph.json..." in out


def test_rebuild_code_stays_quiet_on_a_small_corpus(tmp_path, capsys):
"""The #3045 stage-progress prints are gated on corpus size so a normal-sized
rebuild's output is not cluttered with lines that only matter at scale."""
from graphify.watch import _rebuild_code

corpus = tmp_path / "corpus"
corpus.mkdir()
(corpus / "a.py").write_text("def alpha():\n return 1\n", encoding="utf-8")
assert _rebuild_code(corpus, acquire_lock=False) is True

out = capsys.readouterr().out
assert "[graphify watch] Building graph..." not in out
assert "[graphify watch] Clustering graph (" not in out


def test_rebuild_code_drops_labels_whose_community_changed(tmp_path):
"""An incremental rebuild must not reuse a saved label for a community whose
membership changed. Labels are keyed by cid, but re-clustering reassigns cids,
Expand Down