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:
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user