Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion graphify/diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,22 @@ def _canonical_edge(edge: Any) -> dict[str, str]:
}


def _is_external_import_edge(edge: dict[str, str], node_ids: set[str]) -> bool:
"""Return whether a missing endpoint is Graphify's intentional JS external stub.

Unresolved JS/TS package imports use the ``ref_*`` namespace so they cannot
alias onto an unrelated local node (#1638). The build intentionally drops
those edges because external packages are outside the scanned corpus. They
are useful extraction evidence, but they are not malformed internal edges.
"""
return (
edge["relation"] == "imports_from"
and edge["source"] in node_ids
and edge["target"] not in node_ids
and edge["target"].startswith("ref_")
)


def _exact_signature(edge: Any) -> str:
if not isinstance(edge, dict):
return "<non-object>"
Expand Down Expand Up @@ -161,7 +177,7 @@ 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 edge integrity and collapse risk for one graph/extraction dict."""
from graphify.build import build_from_json

node_ids = _node_ids(extraction)
Expand All @@ -183,6 +199,7 @@ def diagnose_extraction(

non_object_edges = 0
missing_endpoint_edges = 0
external_import_edges = 0
dangling_endpoint_edges = 0
self_loop_edges = 0
valid_candidate_edges = 0
Expand All @@ -196,6 +213,9 @@ def diagnose_extraction(
if not source or not target:
missing_endpoint_edges += 1
continue
if _is_external_import_edge(edge, node_ids):
external_import_edges += 1
continue
if source not in node_ids or target not in node_ids:
dangling_endpoint_edges += 1
continue
Expand Down Expand Up @@ -251,6 +271,7 @@ def diagnose_extraction(
"raw_edge_count": len(raw_edges),
"non_object_edges": non_object_edges,
"missing_endpoint_edges": missing_endpoint_edges,
"external_import_edges": external_import_edges,
"dangling_endpoint_edges": dangling_endpoint_edges,
"self_loop_edges": self_loop_edges,
"valid_candidate_edges": valid_candidate_edges,
Expand Down Expand Up @@ -357,6 +378,7 @@ def format_diagnostic_report(summary: dict[str, Any]) -> str:
f"raw_edges: {summary['raw_edge_count']}",
f"valid_candidate_edges: {summary['valid_candidate_edges']}",
f"missing_endpoint_edges: {summary['missing_endpoint_edges']}",
f"external_import_edges: {summary.get('external_import_edges', 0)}",
f"dangling_endpoint_edges: {summary['dangling_endpoint_edges']}",
f"self_loop_edges: {summary['self_loop_edges']}",
f"exact_duplicate_edges: {summary['exact_duplicate_edges']}",
Expand All @@ -381,6 +403,11 @@ def format_diagnostic_report(summary: dict[str, Any]) -> str:
]
if summary.get("post_build_error"):
lines.append(f"post_build_error: {summary['post_build_error']}")
if summary.get("external_import_edges"):
lines.append(
"note: external_import_edges are intentional out-of-corpus ref_* "
"imports, not malformed internal endpoints."
)
if suppression.get("error"):
lines.append(f"producer_suppression_error: {suppression['error']}")
if suppression.get("sites"):
Expand Down
43 changes: 43 additions & 0 deletions tests/test_multigraph_diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,49 @@ def test_diagnose_extraction_categorizes_same_endpoint_collapse() -> None:
assert summary["post_build_edge_count"] == 2


def test_external_js_import_is_not_reported_as_dangling(tmp_path: Path) -> None:
"""External ref_* imports are intentional extraction evidence, not corruption."""
from graphify.extract import extract

source = tmp_path / "main.js"
source.write_text('const fs = require("fs");\n', encoding="utf-8")
extraction = extract(
[source],
cache_root=tmp_path,
root=tmp_path,
parallel=False,
)

external_imports = [
edge
for edge in extraction["edges"]
if edge.get("relation") == "imports_from"
and str(edge.get("target", "")).startswith("ref_")
]
assert len(external_imports) == 1

# Keep a genuinely invalid edge in the same fixture so the classification
# cannot hide arbitrary missing endpoints.
extraction["edges"].append(
{
"source": external_imports[0]["source"],
"target": "missing_internal_target",
"relation": "calls",
"confidence": "EXTRACTED",
"source_file": str(source),
}
)

summary = diagnose_extraction(extraction, directed=True, root=tmp_path)

assert summary["external_import_edges"] == 1
assert summary["dangling_endpoint_edges"] == 1
report = format_diagnostic_report(summary)
assert "external_import_edges: 1" in report
assert "dangling_endpoint_edges: 1" in report
assert "intentional out-of-corpus ref_* imports" in report


def test_diagnose_extraction_accepts_node_link_links_key() -> None:
extraction = _diagnostic_fixture()
extraction["links"] = extraction.pop("edges")
Expand Down