fix path/explain arrow direction and bedrock CLI guard (#846, #849, #853)

This commit is contained in:
Safi
2026-05-13 23:19:15 +01:00
parent d0e09aa89d
commit 7bb0919c72
3 changed files with 141 additions and 12 deletions
+37 -12
View File
@@ -1514,6 +1514,8 @@ def main() -> None:
_raw = json.loads(gp.read_text(encoding="utf-8"))
if "links" not in _raw and "edges" in _raw:
_raw = dict(_raw, links=_raw["edges"])
# Force directed so the renderer can recover stored caller→callee direction.
_raw = {**_raw, "directed": True}
try:
G = json_graph.node_link_graph(_raw, edges="links")
except TypeError:
@@ -1547,7 +1549,7 @@ def main() -> None:
file=sys.stderr,
)
try:
path_nodes = _nx.shortest_path(G, src_nid, tgt_nid)
path_nodes = _nx.shortest_path(G.to_undirected(as_view=True), src_nid, tgt_nid)
except (_nx.NetworkXNoPath, _nx.NodeNotFound):
print(f"No path found between '{source_label}' and '{target_label}'.")
sys.exit(0)
@@ -1556,13 +1558,22 @@ def main() -> None:
from graphify.build import edge_data
for i in range(len(path_nodes) - 1):
u, v = path_nodes[i], path_nodes[i + 1]
edata = edge_data(G, u, v)
# Check which direction the stored edge points.
if G.has_edge(u, v):
edata = edge_data(G, u, v)
forward = True
else:
edata = edge_data(G, v, u)
forward = False
rel = edata.get("relation", "")
conf = edata.get("confidence", "")
conf_str = f" [{conf}]" if conf else ""
if i == 0:
segments.append(G.nodes[u].get("label", u))
segments.append(f"--{rel}{conf_str}--> {G.nodes[v].get('label', v)}")
if forward:
segments.append(f"--{rel}{conf_str}--> {G.nodes[v].get('label', v)}")
else:
segments.append(f"<--{rel}{conf_str}-- {G.nodes[v].get('label', v)}")
print(f"Shortest path ({hops} hops):\n " + " ".join(segments))
elif cmd == "explain":
@@ -1584,6 +1595,8 @@ def main() -> None:
_raw = json.loads(gp.read_text(encoding="utf-8"))
if "links" not in _raw and "edges" in _raw:
_raw = dict(_raw, links=_raw["edges"])
# Force directed so the renderer can recover stored caller→callee direction.
_raw = {**_raw, "directed": True}
try:
G = json_graph.node_link_graph(_raw, edges="links")
except TypeError:
@@ -1600,17 +1613,22 @@ def main() -> None:
print(f" Type: {d.get('file_type', '')}")
print(f" Community: {d.get('community', '')}")
print(f" Degree: {G.degree(nid)}")
neighbors = list(G.neighbors(nid))
if neighbors:
from graphify.build import edge_data
print(f"\nConnections ({len(neighbors)}):")
for nb in sorted(neighbors, key=lambda n: G.degree(n), reverse=True)[:20]:
edata = edge_data(G, nid, nb)
from graphify.build import edge_data
connections: list[tuple[str, str, dict]] = [] # (direction, neighbor_id, edge_data)
for nb in G.successors(nid):
connections.append(("out", nb, edge_data(G, nid, nb)))
for nb in G.predecessors(nid):
connections.append(("in", nb, edge_data(G, nb, nid)))
if connections:
print(f"\nConnections ({len(connections)}):")
connections.sort(key=lambda c: G.degree(c[1]), reverse=True)
for direction, nb, edata in connections[:20]:
rel = edata.get("relation", "")
conf = edata.get("confidence", "")
print(f" --> {G.nodes[nb].get('label', nb)} [{rel}] [{conf}]")
if len(neighbors) > 20:
print(f" ... and {len(neighbors) - 20} more")
arrow = "-->" if direction == "out" else "<--"
print(f" {arrow} {G.nodes[nb].get('label', nb)} [{rel}] [{conf}]")
if len(connections) > 20:
print(f" ... and {len(connections) - 20} more")
elif cmd == "add":
if len(sys.argv) < 3:
@@ -2422,6 +2440,13 @@ def main() -> None:
host in ("localhost", "127.0.0.1", "::1")
or host.startswith("127.")
)
elif backend == "bedrock":
allow_no_key = bool(
os.environ.get("AWS_PROFILE")
or os.environ.get("AWS_REGION")
or os.environ.get("AWS_DEFAULT_REGION")
or os.environ.get("AWS_ACCESS_KEY_ID")
)
if not allow_no_key:
print(
f"error: backend '{backend}' requires {_format_backend_env_keys(backend)} to be set.",
+56
View File
@@ -0,0 +1,56 @@
"""Regression tests for `graphify explain` arrow direction (#853)."""
from __future__ import annotations
import json
import graphify.__main__ as mainmod
def _write_graph(tmp_path):
graph_data = {
"directed": False, "multigraph": False, "graph": {},
"nodes": [
{"id": "validate", "label": "validateSanitySession()",
"source_file": "server/sanity-validate-session.ts", "community": 0},
{"id": "create_patch", "label": "createPatchHandler()",
"source_file": "server/create-patch-handler.ts", "community": 0},
{"id": "create_edit", "label": "createEditHandler()",
"source_file": "server/create-edit-handler.ts", "community": 0},
{"id": "stable_stringify", "label": "stableStringify()",
"source_file": "shared/stringify.ts", "community": 0},
],
"links": [
{"source": "create_patch", "target": "validate",
"relation": "calls", "confidence": "EXTRACTED"},
{"source": "create_edit", "target": "validate",
"relation": "calls", "confidence": "EXTRACTED"},
{"source": "validate", "target": "stable_stringify",
"relation": "calls", "confidence": "EXTRACTED"},
],
}
p = tmp_path / "graph.json"
p.write_text(json.dumps(graph_data))
return p
def _run(monkeypatch, graph_path, label, capsys):
monkeypatch.setattr(mainmod, "_check_skill_version", lambda _: None)
monkeypatch.setattr(mainmod.sys, "argv",
["graphify", "explain", label, "--graph", str(graph_path)])
mainmod.main()
return capsys.readouterr().out
def test_callee_shows_callers_as_inbound(monkeypatch, tmp_path, capsys):
p = _write_graph(tmp_path)
out = _run(monkeypatch, p, "validateSanitySession", capsys)
assert "<-- createPatchHandler() [calls]" in out
assert "<-- createEditHandler() [calls]" in out
assert "--> stableStringify() [calls]" in out
assert "--> createPatchHandler() [calls]" not in out
assert "--> createEditHandler() [calls]" not in out
def test_caller_shows_callee_as_outbound(monkeypatch, tmp_path, capsys):
p = _write_graph(tmp_path)
out = _run(monkeypatch, p, "createPatchHandler", capsys)
assert "--> validateSanitySession() [calls]" in out
assert "<-- " not in out
+48
View File
@@ -0,0 +1,48 @@
"""Regression tests for `graphify path` arrow direction (#849)."""
from __future__ import annotations
import json
import networkx as nx
from networkx.readwrite import json_graph
import graphify.__main__ as mainmod
def _write_graph(tmp_path):
graph_data = {
"directed": False, "multigraph": False, "graph": {},
"nodes": [
{"id": "create_patch", "label": "createPatchHandler()",
"source_file": "server/create-patch-handler.ts", "community": 0},
{"id": "validate", "label": "validateSanitySession()",
"source_file": "server/sanity-validate-session.ts", "community": 0},
],
"links": [
{"source": "create_patch", "target": "validate",
"relation": "calls", "confidence": "EXTRACTED"},
],
}
p = tmp_path / "graph.json"
p.write_text(json.dumps(graph_data))
return p
def _run(monkeypatch, graph_path, src, tgt, capsys):
monkeypatch.setattr(mainmod, "_check_skill_version", lambda _: None)
monkeypatch.setattr(mainmod.sys, "argv",
["graphify", "path", src, tgt, "--graph", str(graph_path)])
mainmod.main()
return capsys.readouterr().out
def test_forward_arrow(monkeypatch, tmp_path, capsys):
p = _write_graph(tmp_path)
out = _run(monkeypatch, p, "createPatchHandler", "validateSanitySession", capsys)
assert "Shortest path (1 hops):" in out
assert "createPatchHandler() --calls [EXTRACTED]--> validateSanitySession()" in out
def test_reverse_arrow(monkeypatch, tmp_path, capsys):
p = _write_graph(tmp_path)
out = _run(monkeypatch, p, "validateSanitySession", "createPatchHandler", capsys)
assert "Shortest path (1 hops):" in out
assert "validateSanitySession() <--calls [EXTRACTED]-- createPatchHandler()" in out
assert "validateSanitySession() --calls [EXTRACTED]--> createPatchHandler()" not in out