diff --git a/graphify/callflow_html.py b/graphify/callflow_html.py index 3c8a34b41..b4d768bcd 100644 --- a/graphify/callflow_html.py +++ b/graphify/callflow_html.py @@ -51,6 +51,7 @@ h4 { font-size: 1.05rem; margin: 20px 0 8px; color: var(--warn); } p { margin: 8px 0; color: var(--muted); } .subtitle { color: var(--muted); font-size: 1.1rem; margin-bottom: 32px; } +.diagram-coverage { color: var(--muted); font-size: 0.85rem; margin: -4px 0 16px; font-style: italic; } .mermaid { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; padding: 24px; margin: 20px 0; overflow-x: auto; position: relative; } .mermaid.is-enhanced { padding: 0; overflow: hidden; min-height: 260px; } .mermaid-viewport { padding: 54px 24px 24px; overflow: hidden; cursor: grab; touch-action: none; min-height: 260px; } @@ -1117,6 +1118,55 @@ def generate_overview_graph(sections: list, section_nodes_map: dict, return "\n".join(lines) +def select_diagram_content(nodes: list, edges: list, max_nodes: int) -> tuple[list, list]: + """Nodes and edges a section diagram will actually draw. + + Shared by the flowchart and its coverage note so the two cannot drift: a + disclosure that is computed separately from the drawing is a disclosure + that will eventually lie. + """ + selected_nodes = select_diagram_nodes(nodes, edges, max_nodes) + selected_ids = {node.get("id") for node in selected_nodes} + visible_edges = [ + edge for edge in preferred_edges(edges, allow_structure=False) + if edge.get("source") in selected_ids and edge.get("target") in selected_ids + ] + if not visible_edges: + visible_edges = [ + edge for edge in preferred_edges(edges, allow_structure=True) + if edge.get("source") in selected_ids and edge.get("target") in selected_ids + ] + return selected_nodes, visible_edges + + +def generate_diagram_coverage(nodes: list, edges: list, lang: str, + max_nodes: int, max_edges: int) -> str: + """State how much of a section its diagram actually draws. + + The counts were already computed inside generate_section_flowchart, but + emitted as a Mermaid ``%%`` comment -- which Mermaid never renders, so no + reader could see them. The section intro says only that the diagram shows + "representative relationships", never how much is missing, so a reader who + is told the section has 54 nodes reasonably assumes the diagram holds all + 54. On a real graph an audited section drew 18 of 54. + """ + if not nodes: + return "" + selected_nodes, visible_edges = select_diagram_content(nodes, edges, max_nodes) + shown_nodes, total_nodes = len(selected_nodes), len(nodes) + shown_edges, total_edges = min(len(visible_edges), max_edges), len(visible_edges) + if shown_nodes >= total_nodes and shown_edges >= total_edges: + return "" # complete -- no disclosure needed + text = pick_text( + lang, + f"图中绘制了 {total_nodes} 个节点中的 {shown_nodes} 个、" + f"{total_edges} 条边中的 {shown_edges} 条;其余已省略以保持可读性。", + f"Diagram draws {shown_nodes} of {total_nodes} nodes and " + f"{shown_edges} of {total_edges} edges; the rest are omitted for readability.", + ) + return f'

{escape(text)}

' + + def generate_section_flowchart(section_id: str, section_name: str, nodes: list, edges: list, lang: str, diagram_scale: float, max_nodes: int, @@ -1131,17 +1181,7 @@ def generate_section_flowchart(section_id: str, section_name: str, lines.extend(mermaid_class_defs()) return "\n".join(lines) - selected_nodes = select_diagram_nodes(nodes, edges, max_nodes) - selected_ids = {node.get("id") for node in selected_nodes} - visible_edges = [ - edge for edge in preferred_edges(edges, allow_structure=False) - if edge.get("source") in selected_ids and edge.get("target") in selected_ids - ] - if not visible_edges: - visible_edges = [ - edge for edge in preferred_edges(edges, allow_structure=True) - if edge.get("source") in selected_ids and edge.get("target") in selected_ids - ] + selected_nodes, visible_edges = select_diagram_content(nodes, edges, max_nodes) groups = group_nodes_by_file(selected_nodes) class_lines = [] @@ -1760,6 +1800,7 @@ def write_callflow_html(
{generate_section_flowchart(sid, name, sec_nodes, sec_edges, lang, args.diagram_scale, args.max_diagram_nodes, args.max_diagram_edges)}
+{generate_diagram_coverage(sec_nodes, sec_edges, lang, args.max_diagram_nodes, args.max_diagram_edges)}

{h3_title}

diff --git a/tests/test_callflow_html.py b/tests/test_callflow_html.py index e42d1b429..7fe261f41 100644 --- a/tests/test_callflow_html.py +++ b/tests/test_callflow_html.py @@ -345,3 +345,25 @@ def test_call_table_rows_without_whole_graph_params_unchanged(tmp_path): # semantics other call sites may rely on). export_node = [n for n in nodes if n["id"] == "export"] assert "External entry" in generate_call_table_rows(export_node, section_edges, "en") + + +def test_diagram_coverage_states_what_is_drawn(): + """The section diagram is capped, but the counts were only ever emitted as + a Mermaid `%%` comment, which Mermaid does not render.""" + from graphify.callflow_html import generate_diagram_coverage + + nodes = [{"id": f"n{i}", "label": f"fn{i}()", "source_file": "a.py"} for i in range(40)] + edges = [{"source": f"n{i}", "target": f"n{i+1}", "relation": "calls"} for i in range(39)] + out = generate_diagram_coverage(nodes, edges, "en", 18, 24) + assert "of 40 nodes" in out, out + assert 'class="diagram-coverage"' in out + + +def test_diagram_coverage_silent_when_nothing_is_omitted(): + """A complete diagram needs no disclosure.""" + from graphify.callflow_html import generate_diagram_coverage + + nodes = [{"id": "a", "label": "a()", "source_file": "a.py"}, + {"id": "b", "label": "b()", "source_file": "a.py"}] + edges = [{"source": "a", "target": "b", "relation": "calls"}] + assert generate_diagram_coverage(nodes, edges, "en", 18, 24) == ""