diff --git a/CHANGELOG.md b/CHANGELOG.md index 759758b8..ada1b19d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ Full release notes with details on each version: [GitHub Releases](https://githu ## 0.9.13 (unreleased) +- Fix: the report's "Suggested Questions" weakly-connected-node count now matches its "Knowledge Gaps" count (#1768, thanks @balloon72). `suggest_questions()` omitted the `file_type != "rationale"` filter that `report.py`'s Knowledge Gaps section applies, so the same `GRAPH_REPORT.md` showed two different numbers for the same concept (e.g. 757 vs 245), making a healthy graph look like it had a major documentation gap. Both computations now use the same filter. + - Fix: Bash scripts that run each other by execution now get a cross-file edge (#1756, thanks @balloon72). `extract_bash` only linked `source x.sh` / `. x.sh`; the two most common forms — `bash x.sh` and `./x.sh` — produced no edge, so execution topology was missing. They now emit a `calls` edge (context `script_invocation`) to the invoked script's entry node when the target resolves to a real file on disk (script runners `bash`/`sh`/`zsh`/`ksh`/`dash` and bare `./x.sh`), skipping missing or shadowed targets. - Fix: Ruby `.rake` files are now extracted and participate in Ruby cross-file resolution like `.rb` (#1784, thanks @krishnateja7). `.rake` is plain Ruby but the extension was gated out of seven places (classification, extractor dispatch, the language-name/family maps, the `ruby_member_calls` resolver's suffix set, both `.rb`-suffix filters in `ruby_resolution.py`, and the build repo-tag map), so every rake task was skipped and its calls were invisible. All seven now include `.rake`; `Widget.tally` from a `.rake` task resolves to its `.rb` definition. diff --git a/graphify/analyze.py b/graphify/analyze.py index 6babbc03..7f3eb72f 100644 --- a/graphify/analyze.py +++ b/graphify/analyze.py @@ -504,7 +504,10 @@ def suggest_questions( # 4. Isolated or weakly-connected nodes → exploration questions isolated = [ n for n in G.nodes() - if G.degree(n) <= 1 and not _is_file_node(G, n) and not _is_concept_node(G, n) + if G.degree(n) <= 1 + and not _is_file_node(G, n) + and not _is_concept_node(G, n) + and G.nodes[n].get("file_type") != "rationale" ] if isolated: labels = [G.nodes[n].get("label", n) for n in isolated[:3]] diff --git a/tests/test_analyze.py b/tests/test_analyze.py index ecf1555d..7bff432c 100644 --- a/tests/test_analyze.py +++ b/tests/test_analyze.py @@ -5,7 +5,7 @@ import pytest from pathlib import Path from graphify.build import build_from_json from graphify.cluster import cluster -from graphify.analyze import god_nodes, surprising_connections, _is_concept_node, graph_diff, _surprise_score, _file_category, _is_json_key_node, find_import_cycles +from graphify.analyze import god_nodes, surprising_connections, _is_concept_node, graph_diff, _surprise_score, _file_category, _is_json_key_node, find_import_cycles, suggest_questions from graphify.extract import _make_id FIXTURES = Path(__file__).parent / "fixtures" @@ -603,6 +603,19 @@ def test_god_nodes_filter_is_case_insensitive(): assert variant not in labels, f"`{variant}` should be filtered as JSON-key noise" +def test_suggest_questions_excludes_rationale_nodes_from_isolated_count(): + G = nx.Graph() + G.add_node("service", label="Service", file_type="code", source_file="service.py") + G.add_node("reason", label="Explains service", file_type="rationale", source_file="service.py") + + questions = suggest_questions(G, communities={}, community_labels={}, top_n=10) + isolated = next(question for question in questions if question["type"] == "isolated_nodes") + + assert isolated["why"].startswith("1 weakly-connected node") + assert "`Service`" in isolated["question"] + assert "Explains service" not in isolated["question"] + + # ── find_import_cycles tests ──────────────────────────────────────────────────