From 7bb0919c72f191160facbd37cbdf67071c85d071 Mon Sep 17 00:00:00 2001 From: Safi Date: Wed, 13 May 2026 23:19:15 +0100 Subject: [PATCH] fix path/explain arrow direction and bedrock CLI guard (#846, #849, #853) --- graphify/__main__.py | 49 +++++++++++++++++++++++++--------- tests/test_explain_cli.py | 56 +++++++++++++++++++++++++++++++++++++++ tests/test_path_cli.py | 48 +++++++++++++++++++++++++++++++++ 3 files changed, 141 insertions(+), 12 deletions(-) create mode 100644 tests/test_explain_cli.py create mode 100644 tests/test_path_cli.py diff --git a/graphify/__main__.py b/graphify/__main__.py index 584b02b..4af7399 100644 --- a/graphify/__main__.py +++ b/graphify/__main__.py @@ -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.", diff --git a/tests/test_explain_cli.py b/tests/test_explain_cli.py new file mode 100644 index 0000000..1d00955 --- /dev/null +++ b/tests/test_explain_cli.py @@ -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 diff --git a/tests/test_path_cli.py b/tests/test_path_cli.py new file mode 100644 index 0000000..de7e883 --- /dev/null +++ b/tests/test_path_cli.py @@ -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