diff --git a/graphify/report.py b/graphify/report.py index 673c2f2bb..0f1c5e709 100644 --- a/graphify/report.py +++ b/graphify/report.py @@ -137,6 +137,21 @@ def generate( f"- {detection_result['total_files']} files ยท ~{detection_result['total_words']:,} words", "- Verdict: corpus is large enough that graph structure adds value.", ] + # #3511: files detect() saw but could not classify (no supported + # extension/shebang) were counted nowhere -- a corpus that is mostly + # an unsupported language reported the same "well covered" verdict as + # one that was actually extracted. Surface the count and its biggest + # extensions so a near-total miss (e.g. a Lean/Zig/whatever repo with + # no matching extractor) is visible here instead of silent. + unclassified = detection_result.get("unclassified") or [] + if unclassified: + from collections import Counter as _Counter + ext_counts = _Counter(Path(p).suffix or "(none)" for p in unclassified) + top = ", ".join(f"{ext} {n}" for ext, n in ext_counts.most_common(3)) + lines.append( + f"- Unclassified: {len(unclassified)} file(s) not represented in " + f"the graph (top: {top})" + ) from .analyze import _is_file_node as _ifn diff --git a/graphify/watch.py b/graphify/watch.py index 58028eae0..5d8c07080 100644 --- a/graphify/watch.py +++ b/graphify/watch.py @@ -1428,6 +1428,20 @@ def _rebuild_code( ) code_files = [Path(f) for f in detected['files']['code']] + # #3511: `graphify extract` has surfaced files it saw but could not + # classify since #1692; this update/watch rebuild path never did, + # so a corpus in a language with no extractor (no supported + # extension or shebang) rebuilt "successfully" with those files + # silently absent from the graph. Same wording as the extract path. + _unclassified = detected.get("unclassified", []) if isinstance(detected, dict) else [] + if _unclassified: + _names = ", ".join(sorted({Path(p).name for p in _unclassified})[:6]) + _more = f" (+{len(_unclassified) - 6} more)" if len(_unclassified) > 6 else "" + print( + f"[graphify watch] {len(_unclassified)} file(s) not classified " + f"(no supported extension or shebang), skipped: {_names}{_more}" + ) + # #2495: hand reconcile the same ignore decisions the detect() call # above made, so a newly-ignored file that still exists on disk is # purged from the graph instead of preserved forever by the fail-closed @@ -1876,6 +1890,7 @@ def _failed(f: str) -> bool: "files": {"code": [str(f) for f in code_files], "document": [], "paper": [], "image": []}, "total_files": len(code_files), "total_words": detected.get("total_words", 0), + "unclassified": detected.get("unclassified", []), } # Inherit the existing graph's directed flag (#2342) so `graphify diff --git a/tests/test_report.py b/tests/test_report.py index 03bc67acb..98f256907 100644 --- a/tests/test_report.py +++ b/tests/test_report.py @@ -29,6 +29,29 @@ def test_report_contains_corpus_check(): report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, "./project") assert "## Corpus Check" in report + +def test_report_surfaces_unclassified_files(): + """#3511: detect() already tracks files it saw but could not classify + (no supported extension), but nothing surfaced them -- a corpus that is + mostly an unsupported language got the same "well covered" verdict as + one that was actually extracted.""" + G, communities, cohesion, labels, gods, surprises, detection, tokens = make_inputs() + detection = { + **detection, + "unclassified": ["Main.lean", "Util.lean", "a.toml", "b.toml", "c.toml", "readme"], + } + report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, "./project") + assert "Unclassified: 6 file(s)" in report + assert ".lean 2" in report + assert ".toml 3" in report + + +def test_report_omits_unclassified_line_when_none(): + """Backward compatible: no unclassified files, no new line.""" + G, communities, cohesion, labels, gods, surprises, detection, tokens = make_inputs() + report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, "./project") + assert "Unclassified:" not in report + def test_report_contains_god_nodes(): G, communities, cohesion, labels, gods, surprises, detection, tokens = make_inputs() report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, "./project") diff --git a/tests/test_watch.py b/tests/test_watch.py index da7d05933..412edb218 100644 --- a/tests/test_watch.py +++ b/tests/test_watch.py @@ -133,6 +133,25 @@ def test_doc_only_deletion_full_rebuild_evicts_md_nodes(tmp_path): assert "run()" in labels +def test_rebuild_code_reports_unclassified_files(tmp_path, capsys): + """#3511: `graphify extract` has surfaced files it saw but could not + classify (no supported extension/shebang) since #1692; the update/watch + rebuild path never did, so a corpus mostly in an unsupported language + (e.g. Lean, per the report) rebuilt "successfully" with those files + silently absent and nothing said about it.""" + corpus = tmp_path / "corpus" + corpus.mkdir() + (corpus / "app.py").write_text("def run(): pass\n", encoding="utf-8") + (corpus / "Main.lean").write_text("def main := 0\n", encoding="utf-8") + (corpus / "Util.lean").write_text("def util := 1\n", encoding="utf-8") + + assert _rebuild_code(corpus, acquire_lock=False) is True + out = capsys.readouterr().out + assert "2 file(s) not classified" in out + assert "Main.lean" in out + assert "Util.lean" in out + + # --- watch() import error without watchdog --- def test_check_update_no_flag_returns_true(tmp_path):