diff --git a/graphify/diagnostics.py b/graphify/diagnostics.py index fcb9a11cf..32896494b 100644 --- a/graphify/diagnostics.py +++ b/graphify/diagnostics.py @@ -161,7 +161,12 @@ def diagnose_extraction( max_examples: int = 5, extract_path: str | Path | None = None, ) -> dict[str, Any]: - """Summarize same-endpoint edge-collapse risk for one JSON graph/extraction dict.""" + """Summarize graph-integrity risks for one JSON graph/extraction dict. + + Missing ``ref_*`` targets are expected external references produced for + dependencies outside the scanned corpus. They are counted separately from + actionable internal dangling endpoints. + """ from graphify.build import build_from_json node_ids = _node_ids(extraction) @@ -184,6 +189,7 @@ def diagnose_extraction( non_object_edges = 0 missing_endpoint_edges = 0 dangling_endpoint_edges = 0 + external_reference_edges = 0 self_loop_edges = 0 valid_candidate_edges = 0 @@ -197,7 +203,10 @@ def diagnose_extraction( missing_endpoint_edges += 1 continue if source not in node_ids or target not in node_ids: - dangling_endpoint_edges += 1 + if source in node_ids and target.startswith("ref_"): + external_reference_edges += 1 + else: + dangling_endpoint_edges += 1 continue if source == target: self_loop_edges += 1 @@ -252,6 +261,7 @@ def diagnose_extraction( "non_object_edges": non_object_edges, "missing_endpoint_edges": missing_endpoint_edges, "dangling_endpoint_edges": dangling_endpoint_edges, + "external_reference_edges": external_reference_edges, "self_loop_edges": self_loop_edges, "valid_candidate_edges": valid_candidate_edges, "exact_duplicate_edges": _count_extra(exact_counts), @@ -358,6 +368,7 @@ def format_diagnostic_report(summary: dict[str, Any]) -> str: f"valid_candidate_edges: {summary['valid_candidate_edges']}", f"missing_endpoint_edges: {summary['missing_endpoint_edges']}", f"dangling_endpoint_edges: {summary['dangling_endpoint_edges']}", + f"external_reference_edges: {summary.get('external_reference_edges', 0)}", f"self_loop_edges: {summary['self_loop_edges']}", f"exact_duplicate_edges: {summary['exact_duplicate_edges']}", f"directed_unique_endpoint_pairs: {summary['directed_unique_endpoint_pairs']}", diff --git a/tests/test_multigraph_diagnostics.py b/tests/test_multigraph_diagnostics.py index 8c39b8e23..0206a0c13 100644 --- a/tests/test_multigraph_diagnostics.py +++ b/tests/test_multigraph_diagnostics.py @@ -103,6 +103,37 @@ def test_diagnose_extraction_categorizes_same_endpoint_collapse() -> None: assert summary["post_build_edge_count"] == 2 +def test_diagnose_extraction_separates_external_reference_edges() -> None: + extraction = { + "nodes": [ + {"id": "app", "label": "app", "file_type": "code", "source_file": "app.ts"}, + ], + "edges": [ + { + "source": "app", + "target": "ref_react", + "relation": "imports_from", + "confidence": "EXTRACTED", + "source_file": "app.ts", + }, + { + "source": "app", + "target": "missing_internal", + "relation": "references", + "confidence": "INFERRED", + "source_file": "app.ts", + }, + ], + } + + summary = diagnose_extraction(extraction, directed=True) + report = format_diagnostic_report(summary) + + assert summary["external_reference_edges"] == 1 + assert summary["dangling_endpoint_edges"] == 1 + assert "external_reference_edges: 1" in report + + def test_diagnose_extraction_accepts_node_link_links_key() -> None: extraction = _diagnostic_fixture() extraction["links"] = extraction.pop("edges")