Backfill edge source_file from endpoint nodes (#1279)

Semantic/LLM edges occasionally omit source_file, which build only normalized
when already present, so the field reached graph.json empty and downstream
validation flagged it. Backfill from the source (then target) node in
build_from_json and in the --no-cluster raw-write path, which bypasses it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Safi
2026-06-17 13:49:31 +01:00
co-authored by Claude Opus 4.8
parent 9c8bd4d47c
commit 8437ef465e
3 changed files with 34 additions and 0 deletions
+8
View File
@@ -4453,6 +4453,14 @@ def main() -> None:
merged["nodes"] = _dedupe_nodes(merged["nodes"])
merged["edges"] = _dedupe_edges(merged["edges"])
# Backfill source_file from endpoint nodes — this raw path bypasses
# build_from_json's backfill, and semantic edges sometimes omit it (#1279).
_node_sf = {n.get("id"): n.get("source_file") for n in merged["nodes"]}
for _e in merged["edges"]:
if not _e.get("source_file"):
_e["source_file"] = (
_node_sf.get(_e.get("source")) or _node_sf.get(_e.get("target")) or ""
)
_backup(graphify_out)
graph_json_path.write_text(
json.dumps(merged, indent=2), encoding="utf-8"
+9
View File
@@ -296,6 +296,15 @@ def build_from_json(extraction: dict, *, directed: bool = False, root: str | Pat
if src not in node_set or tgt not in node_set:
continue # skip edges to external/stdlib nodes - expected, not an error
attrs = {k: v for k, v in edge.items() if k not in ("source", "target")}
# Backfill source_file from the endpoint nodes (every node carries one).
# Semantic/LLM edges occasionally omit it, which downstream validation
# flags and leaves query results with no file reference (#1279).
if not attrs.get("source_file"):
attrs["source_file"] = (
G.nodes[src].get("source_file")
or G.nodes[tgt].get("source_file")
or ""
)
if "source_file" in attrs:
attrs["source_file"] = _norm_source_file(attrs["source_file"], _root)
# Drop cross-language INFERRED `calls` edges — same short names (render,
+17
View File
@@ -108,6 +108,23 @@ def test_source_file_backslash_normalized():
assert sources == {"src/middleware/auth.py"}
def test_edge_missing_source_file_backfilled_from_node():
"""#1279: a semantic/LLM edge lacking source_file must inherit it from its
source node rather than reach graph.json with no file reference."""
extraction = {
"nodes": [
{"id": "n1", "label": "A", "file_type": "concept", "source_file": "docs/a.md"},
{"id": "n2", "label": "B", "file_type": "concept", "source_file": "docs/b.md"},
],
# No source_file on the edge (as LLM output sometimes omits it).
"edges": [{"source": "n1", "target": "n2", "relation": "relates_to", "confidence": "INFERRED"}],
"input_tokens": 0, "output_tokens": 0,
}
G = build_from_json(extraction)
sf = edge_data(G, "n1", "n2").get("source_file")
assert sf == "docs/a.md" # backfilled from the source node
def test_build_merges_multiple_extractions():
ext1 = {"nodes": [{"id": "n1", "label": "A", "file_type": "code", "source_file": "a.py"}],
"edges": [], "input_tokens": 0, "output_tokens": 0}