From f08e50534d240907d1dfb25f1669dffd767a02bd Mon Sep 17 00:00:00 2001 From: ayushcodes10 Date: Sat, 12 Sep 2026 18:58:46 +0530 Subject: [PATCH 1/2] Surface unclassified files in GRAPH_REPORT.md detect() already returns every file it saw but could not classify, no supported extension or shebang, but nothing read it: a corpus mostly in an unsupported language got the same well covered verdict as one that was actually extracted, silently. Adds an Unclassified line to the Corpus Check section naming the count and the biggest offending extensions. Toward #3511. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh --- graphify/report.py | 15 +++++++++++++++ tests/test_report.py | 23 +++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/graphify/report.py b/graphify/report.py index 673c2f2bb6..0f1c5e709e 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/tests/test_report.py b/tests/test_report.py index 03bc67acba..98f256907d 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") From 120405673f7b35840158028d14bb54a71a89e763 Mon Sep 17 00:00:00 2001 From: ayushcodes10 Date: Sat, 12 Sep 2026 18:59:04 +0530 Subject: [PATCH 2/2] Report unclassified files from graphify update too graphify extract has printed this since #1692, but the update and watch rebuild path never did, so a corpus with no extractor for its language, Lean 4 in the reported case, rebuilt successfully with those files completely absent and nothing said about it. Prints the same wording from the extract command, and threads the count into the local detection summary the rebuild path already builds for GRAPH_REPORT.md, which previously dropped it. Fixes #3511. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh --- graphify/watch.py | 15 +++++++++++++++ tests/test_watch.py | 19 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/graphify/watch.py b/graphify/watch.py index 58028eae09..5d8c07080d 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_watch.py b/tests/test_watch.py index da7d059332..412edb2183 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):