From d6ff6e0984336891972bfff2e9f82813186ee6b2 Mon Sep 17 00:00:00 2001 From: ayushcodes10 Date: Fri, 25 Sep 2026 22:36:53 +0530 Subject: [PATCH] Reuse the shared real node filter in the community listing loop The per community Nodes count and listing loop only excluded file nodes, so a rationale docstring fragment node or a concept node leaked into Nodes (N) as if it were a real code declaration, and inflated the reported count by including it. The Knowledge Gaps section a few lines below already excluded file, concept and rationale nodes together, the correct set an earlier issue established for this same function, the community listing loop was never given the same treatment. Consolidates every real node check this function makes, the summary thin and shown counts, the community listing, and the gaps section isolated and thin community counts, onto one shared predicate instead of four separate inline filters that had drifted apart. Fixing only the display line without also fixing the count computations that decide which communities are thin or shown would have reintroduced the exact header versus render disagreement an earlier issue fixed, since the header counts and the rendered content need to agree on what counts as a real node. Fixes issue 3794. (cherry picked from commit 57fab42e811c0f6988c9d16da293328edb601755) --- graphify/report.py | 39 ++++++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/graphify/report.py b/graphify/report.py index 65e1ffada..168197d54 100644 --- a/graphify/report.py +++ b/graphify/report.py @@ -153,13 +153,29 @@ def generate( f"the graph (top: {top})" ) - from .analyze import _is_file_node as _ifn + from .analyze import _is_file_node, _is_concept_node + + def _real_node(n) -> bool: + # The one definition of "real" (structural, not file/concept/ + # rationale) every figure this function prints about itself must + # agree on (#3148) -- file nodes are structural scaffolding, concept + # nodes are semantic-layer entities, and rationale nodes are + # docstring fragments the LLM extracted for context, none of which + # are a code declaration a reader would recognize as a "node" in + # this list. A community-listing loop that filtered only file nodes + # let rationale/concept content leak into "Nodes (N): ..." as if it + # were a real declaration, and inflated N by counting it (#3794). + return ( + not _is_file_node(G, n) + and not _is_concept_node(G, n) + and G.nodes[n].get("file_type") != "rationale" + ) def _real_count(nodes) -> int: - return sum(1 for n in nodes if not _ifn(G, n)) + return sum(1 for n in nodes if _real_node(n)) non_empty = {cid: nodes for cid, nodes in communities.items() - if any(not _ifn(G, n) for n in nodes)} + if any(_real_node(n) for n in nodes)} # One predicate for every figure the report prints about itself (#3148): # "thin" is real < min_community_size (ZERO real nodes included), and # "shown" is what the render loop below actually renders (real >= @@ -280,8 +296,9 @@ def generate( for cid, nodes in communities.items(): label = community_labels.get(cid, f"Community {cid}") score = cohesion_scores.get(cid, 0.0) - # Filter method/function stubs from display - they're structural noise - real_nodes = [n for n in nodes if not _ifn(G, n)] + # Filter file/concept/rationale nodes from display - they're + # structural noise or docstring prose, not a code declaration (#3794). + real_nodes = [n for n in nodes if _real_node(n)] if not real_nodes: continue if len(real_nodes) < min_community_size: @@ -307,15 +324,7 @@ def generate( ] # --- Gaps section --- - from .analyze import _is_file_node, _is_concept_node - - 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) - and G.nodes[n].get("file_type") != "rationale" - ] + isolated = [n for n in G.nodes() if G.degree(n) <= 1 and _real_node(n)] # Same threshold the Summary and Communities headers used (#3148): this # was a hardcoded 3, so with --min-community-size anything else the count # here disagreed with the label text beside it, which already printed @@ -324,7 +333,7 @@ def generate( # as a genuinely thin one, so it belongs in this count too. thin_communities = { cid: nodes for cid, nodes in communities.items() - if sum(1 for n in nodes if not _is_file_node(G, n)) < min_community_size + if _real_count(nodes) < min_community_size } gap_count = len(isolated) + len(thin_communities)