Skip to content
Open
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -633,6 +633,8 @@ graphify-out/
/graphify query "what connects attention to the optimizer?"
/graphify query "..." --dfs --budget 1500
/graphify query "..." --seed-file graphify-out/.graphify_reranked_nodes.json
Seed files are JSON arrays of string node ids; their order becomes the traversal seed order.
/graphify path "DigestAuth" "Response"
/graphify explain "SwinTransformer"
Expand Down
1 change: 1 addition & 0 deletions graphify/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,7 @@ def main() -> None:
print(" --dfs use depth-first instead of breadth-first")
print(" --context C explicit edge-context filter (repeatable)")
print(" --budget N cap output at N tokens (default 2000)")
print(" --seed-file <path> JSON array of string seed node ids")
print(" --graph <path> path to graph.json (default graphify-out/graph.json)")
print(" affected \"X\" reverse traversal to find nodes impacted by X")
print(" --relation R edge relation to traverse in reverse (repeatable)")
Expand Down
39 changes: 38 additions & 1 deletion graphify/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,21 @@

def _default_graph_path() -> str:
return str(Path(_GRAPHIFY_OUT) / "graph.json")


def _load_query_seed_file(path: str) -> list[str]:
data = json.loads(Path(path).expanduser().read_text(encoding="utf-8"))
if not isinstance(data, list):
raise ValueError("seed file must contain a JSON array")

seeds: list[str] = []
for item in data:
if not isinstance(item, str):
raise ValueError("seed file entries must be string node ids")
seeds.append(item)
return seeds


class _StageTimer:
"""Print per-stage wall-clock timings to stderr when --timing is set (#1490).

Expand Down Expand Up @@ -352,7 +367,11 @@ def dispatch_command(cmd: str) -> None:
sys.exit(1)
elif cmd == "query":
if len(sys.argv) < 3:
print("Usage: graphify query \"<question>\" [--dfs] [--context C] [--budget N] [--graph path]", file=sys.stderr)
print(
"Usage: graphify query \"<question>\" [--dfs] [--context C] "
"[--budget N] [--seed-file path] [--graph path]",
file=sys.stderr,
)
sys.exit(1)
from graphify.serve import _query_graph_text
from graphify.security import sanitize_label
Expand All @@ -364,6 +383,7 @@ def dispatch_command(cmd: str) -> None:
budget = 2000
graph_path = _default_graph_path()
context_filters: list[str] = []
start_nodes: list[str] = []
args = sys.argv[3:]
i = 0
while i < len(args):
Expand All @@ -390,8 +410,16 @@ def dispatch_command(cmd: str) -> None:
elif args[i] == "--graph" and i + 1 < len(args):
graph_path = args[i + 1]
i += 2
elif args[i] == "--seed-file" and i + 1 < len(args):
try:
start_nodes.extend(_load_query_seed_file(args[i + 1]))
except (OSError, ValueError, json.JSONDecodeError) as exc:
print(f"error: could not load seed file: {exc}", file=sys.stderr)
sys.exit(1)
i += 2
else:
i += 1
start_nodes = list(dict.fromkeys(node_id for node_id in start_nodes if node_id))
gp = Path(graph_path).resolve()
if not gp.exists():
print(f"error: graph file not found: {gp}", file=sys.stderr)
Expand Down Expand Up @@ -425,6 +453,13 @@ def dispatch_command(cmd: str) -> None:
except Exception as exc:
print(f"error: could not load graph: {exc}", file=sys.stderr)
sys.exit(1)
if start_nodes:
missing = [node_id for node_id in start_nodes if node_id not in G]
if missing:
shown = ", ".join(sanitize_label(node_id) for node_id in missing[:5])
suffix = f" (+{len(missing) - 5} more)" if len(missing) > 5 else ""
print(f"error: seed node id not found in graph: {shown}{suffix}", file=sys.stderr)
sys.exit(1)
import time as _time
_t0 = _time.perf_counter()
_mode = "dfs" if use_dfs else "bfs"
Expand All @@ -435,6 +470,7 @@ def dispatch_command(cmd: str) -> None:
depth=2,
token_budget=budget,
context_filters=context_filters,
start_nodes=start_nodes or None,
)
querylog.log_query(
kind="query",
Expand All @@ -444,6 +480,7 @@ def dispatch_command(cmd: str) -> None:
mode=_mode,
depth=2,
token_budget=budget,
seed_nodes=start_nodes or None,
duration_ms=(_time.perf_counter() - _t0) * 1000,
)
print(_result)
Expand Down
8 changes: 5 additions & 3 deletions graphify/serve.py
Original file line number Diff line number Diff line change
Expand Up @@ -664,10 +664,12 @@ def _query_graph_text(
depth: int = 3,
token_budget: int = 2000,
context_filters: list[str] | None = None,
start_nodes: list[str] | None = None,
) -> str:
terms = _query_terms(question)
scored = _score_nodes(G, terms)
start_nodes = _pick_seeds(scored, G=G, terms=terms)
if start_nodes is None:
scored = _score_nodes(G, terms)
start_nodes = _pick_seeds(scored, G=G, terms=terms)
if not start_nodes:
return "No matching nodes found."
resolved_filters, filter_source = _resolve_context_filters(question, context_filters)
Expand All @@ -681,7 +683,7 @@ def _query_graph_text(
header_parts.append(f"Context: {', '.join(resolved_filters)} ({filter_source})")
header_parts.append(f"{len(nodes)} nodes found")
header = " | ".join(header_parts) + "\n\n"
return header + _subgraph_to_text(traversal_graph, nodes, edges, token_budget)
return header + _subgraph_to_text(traversal_graph, nodes, edges, token_budget, seeds=start_nodes)


def _find_node(G: nx.Graph, label: str) -> list[str]:
Expand Down
1 change: 1 addition & 0 deletions graphify/skills/agents/references/query.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ Prefer the CLI when it is installed:
graphify query "QUESTION"
# or: graphify query "QUESTION" --dfs --budget 3000
```
If you already have ordered graph node ids from external retrieval or reranking, pass them as `--seed-file PATH`; the file must be a JSON array of string node ids, and its order becomes the traversal seed order.

If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal inline:

Expand Down
1 change: 1 addition & 0 deletions graphify/skills/amp/references/query.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ Prefer the CLI when it is installed:
graphify query "QUESTION"
# or: graphify query "QUESTION" --dfs --budget 3000
```
If you already have ordered graph node ids from external retrieval or reranking, pass them as `--seed-file PATH`; the file must be a JSON array of string node ids, and its order becomes the traversal seed order.

If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal inline:

Expand Down
1 change: 1 addition & 0 deletions graphify/skills/claude/references/query.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ Prefer the CLI when it is installed:
graphify query "QUESTION"
# or: graphify query "QUESTION" --dfs --budget 3000
```
If you already have ordered graph node ids from external retrieval or reranking, pass them as `--seed-file PATH`; the file must be a JSON array of string node ids, and its order becomes the traversal seed order.

If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal inline:

Expand Down
1 change: 1 addition & 0 deletions graphify/skills/claw/references/query.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ Prefer the CLI when it is installed:
graphify query "QUESTION"
# or: graphify query "QUESTION" --dfs --budget 3000
```
If you already have ordered graph node ids from external retrieval or reranking, pass them as `--seed-file PATH`; the file must be a JSON array of string node ids, and its order becomes the traversal seed order.

If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal inline:

Expand Down
1 change: 1 addition & 0 deletions graphify/skills/codex/references/query.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ Prefer the CLI when it is installed:
graphify query "QUESTION"
# or: graphify query "QUESTION" --dfs --budget 3000
```
If you already have ordered graph node ids from external retrieval or reranking, pass them as `--seed-file PATH`; the file must be a JSON array of string node ids, and its order becomes the traversal seed order.

If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal inline:

Expand Down
1 change: 1 addition & 0 deletions graphify/skills/copilot/references/query.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ Prefer the CLI when it is installed:
graphify query "QUESTION"
# or: graphify query "QUESTION" --dfs --budget 3000
```
If you already have ordered graph node ids from external retrieval or reranking, pass them as `--seed-file PATH`; the file must be a JSON array of string node ids, and its order becomes the traversal seed order.

If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal inline:

Expand Down
1 change: 1 addition & 0 deletions graphify/skills/droid/references/query.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ Prefer the CLI when it is installed:
graphify query "QUESTION"
# or: graphify query "QUESTION" --dfs --budget 3000
```
If you already have ordered graph node ids from external retrieval or reranking, pass them as `--seed-file PATH`; the file must be a JSON array of string node ids, and its order becomes the traversal seed order.

If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal inline:

Expand Down
1 change: 1 addition & 0 deletions graphify/skills/kilo/references/query.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ Prefer the CLI when it is installed:
graphify query "QUESTION"
# or: graphify query "QUESTION" --dfs --budget 3000
```
If you already have ordered graph node ids from external retrieval or reranking, pass them as `--seed-file PATH`; the file must be a JSON array of string node ids, and its order becomes the traversal seed order.

If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal inline:

Expand Down
1 change: 1 addition & 0 deletions graphify/skills/kiro/references/query.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ Prefer the CLI when it is installed:
graphify query "QUESTION"
# or: graphify query "QUESTION" --dfs --budget 3000
```
If you already have ordered graph node ids from external retrieval or reranking, pass them as `--seed-file PATH`; the file must be a JSON array of string node ids, and its order becomes the traversal seed order.

If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal inline:

Expand Down
1 change: 1 addition & 0 deletions graphify/skills/opencode/references/query.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ Prefer the CLI when it is installed:
graphify query "QUESTION"
# or: graphify query "QUESTION" --dfs --budget 3000
```
If you already have ordered graph node ids from external retrieval or reranking, pass them as `--seed-file PATH`; the file must be a JSON array of string node ids, and its order becomes the traversal seed order.

If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal inline:

Expand Down
1 change: 1 addition & 0 deletions graphify/skills/pi/references/query.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ Prefer the CLI when it is installed:
graphify query "QUESTION"
# or: graphify query "QUESTION" --dfs --budget 3000
```
If you already have ordered graph node ids from external retrieval or reranking, pass them as `--seed-file PATH`; the file must be a JSON array of string node ids, and its order becomes the traversal seed order.

If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal inline:

Expand Down
1 change: 1 addition & 0 deletions graphify/skills/trae/references/query.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ Prefer the CLI when it is installed:
graphify query "QUESTION"
# or: graphify query "QUESTION" --dfs --budget 3000
```
If you already have ordered graph node ids from external retrieval or reranking, pass them as `--seed-file PATH`; the file must be a JSON array of string node ids, and its order becomes the traversal seed order.

If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal inline:

Expand Down
1 change: 1 addition & 0 deletions graphify/skills/vscode/references/query.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ Prefer the CLI when it is installed:
graphify query "QUESTION"
# or: graphify query "QUESTION" --dfs --budget 3000
```
If you already have ordered graph node ids from external retrieval or reranking, pass them as `--seed-file PATH`; the file must be a JSON array of string node ids, and its order becomes the traversal seed order.

If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal inline:

Expand Down
1 change: 1 addition & 0 deletions graphify/skills/windows/references/query.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ Prefer the CLI when it is installed:
graphify query "QUESTION"
# or: graphify query "QUESTION" --dfs --budget 3000
```
If you already have ordered graph node ids from external retrieval or reranking, pass them as `--seed-file PATH`; the file must be a JSON array of string node ids, and its order becomes the traversal seed order.

If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal inline:

Expand Down
68 changes: 68 additions & 0 deletions tests/test_query_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,74 @@ def test_query_cli_heuristic_context_filter(monkeypatch, tmp_path, capsys):
assert "build" not in out


def test_query_cli_seed_file_accepts_node_id_strings(monkeypatch, tmp_path, capsys):
graph_path = _write_graph(tmp_path)
seed_path = tmp_path / "seeds.json"
seed_path.write_text(json.dumps(["n3"]))
monkeypatch.setattr(mainmod, "_check_skill_version", lambda _: None)
monkeypatch.setattr(
mainmod.sys,
"argv",
["graphify", "query", "extract", "--seed-file", str(seed_path), "--graph", str(graph_path)],
)
mainmod.main()
out = capsys.readouterr().out
assert "Start: ['build']" in out


def test_query_cli_seed_file_deduplicates_node_ids(monkeypatch, tmp_path, capsys):
graph_path = _write_graph(tmp_path)
seed_path = tmp_path / "seeds.json"
seed_path.write_text(json.dumps(["n3", "n2", "n2"]))
monkeypatch.setattr(mainmod, "_check_skill_version", lambda _: None)
monkeypatch.setattr(
mainmod.sys,
"argv",
["graphify", "query", "unmatched", "--seed-file", str(seed_path), "--graph", str(graph_path)],
)
mainmod.main()
out = capsys.readouterr().out
assert "Start: ['build', 'cluster']" in out
assert out.index("NODE build") < out.index("NODE cluster")


def test_query_cli_seed_file_rejects_unknown_node(monkeypatch, tmp_path, capsys):
import pytest

graph_path = _write_graph(tmp_path)
seed_path = tmp_path / "seeds.json"
seed_path.write_text(json.dumps(["missing"]))
monkeypatch.setattr(mainmod, "_check_skill_version", lambda _: None)
monkeypatch.setattr(
mainmod.sys,
"argv",
["graphify", "query", "extract", "--seed-file", str(seed_path), "--graph", str(graph_path)],
)
with pytest.raises(SystemExit):
mainmod.main()
err = capsys.readouterr().err
assert "seed node id not found" in err
assert "missing" in err


def test_query_cli_seed_file_rejects_non_string_entries(monkeypatch, tmp_path, capsys):
import pytest

graph_path = _write_graph(tmp_path)
seed_path = tmp_path / "seeds.json"
seed_path.write_text(json.dumps([{"id": "n2"}]))
monkeypatch.setattr(mainmod, "_check_skill_version", lambda _: None)
monkeypatch.setattr(
mainmod.sys,
"argv",
["graphify", "query", "extract", "--seed-file", str(seed_path), "--graph", str(graph_path)],
)
with pytest.raises(SystemExit):
mainmod.main()
err = capsys.readouterr().err
assert "seed file entries must be string node ids" in err


def test_query_cli_rejects_oversized_graph(monkeypatch, tmp_path, capsys):
"""#F4: query CLI must refuse to parse a graph.json that exceeds the cap."""
import pytest
Expand Down
11 changes: 11 additions & 0 deletions tests/test_querylog.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,17 @@ def test_explicit_nodes_returned_takes_precedence(tmp_path, monkeypatch):
assert rec["nodes_returned"] == 3


def test_log_query_writes_seed_nodes(tmp_path, monkeypatch):
log_file = tmp_path / "q.log"
monkeypatch.setenv("GRAPHIFY_QUERY_LOG", str(log_file))
monkeypatch.delenv("GRAPHIFY_QUERY_LOG_DISABLE", raising=False)

log_query(kind="query", question="q", corpus="/g.json", seed_nodes=["n1", "n2"])

rec = json.loads(log_file.read_text())
assert rec["seed_nodes"] == ["n1", "n2"]


def test_kind_mcp_query(tmp_path, monkeypatch):
log_file = tmp_path / "q.log"
monkeypatch.setenv("GRAPHIFY_QUERY_LOG", str(log_file))
Expand Down
1 change: 1 addition & 0 deletions tests/test_skillgen.py
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,7 @@ def test_every_platform_query_has_expansion_and_fallback():
q = refs["query.md"]
assert "Constrained query expansion" in q
assert "If the CLI is unavailable" in q
assert "--seed-file PATH" in q
assert "## For /graphify path" in q
assert "## For /graphify explain" in q

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ Prefer the CLI when it is installed:
graphify query "QUESTION"
# or: graphify query "QUESTION" --dfs --budget 3000
```
If you already have ordered graph node ids from external retrieval or reranking, pass them as `--seed-file PATH`; the file must be a JSON array of string node ids, and its order becomes the traversal seed order.

If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal inline:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ Prefer the CLI when it is installed:
graphify query "QUESTION"
# or: graphify query "QUESTION" --dfs --budget 3000
```
If you already have ordered graph node ids from external retrieval or reranking, pass them as `--seed-file PATH`; the file must be a JSON array of string node ids, and its order becomes the traversal seed order.

If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal inline:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ Prefer the CLI when it is installed:
graphify query "QUESTION"
# or: graphify query "QUESTION" --dfs --budget 3000
```
If you already have ordered graph node ids from external retrieval or reranking, pass them as `--seed-file PATH`; the file must be a JSON array of string node ids, and its order becomes the traversal seed order.

If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal inline:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ Prefer the CLI when it is installed:
graphify query "QUESTION"
# or: graphify query "QUESTION" --dfs --budget 3000
```
If you already have ordered graph node ids from external retrieval or reranking, pass them as `--seed-file PATH`; the file must be a JSON array of string node ids, and its order becomes the traversal seed order.

If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal inline:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ Prefer the CLI when it is installed:
graphify query "QUESTION"
# or: graphify query "QUESTION" --dfs --budget 3000
```
If you already have ordered graph node ids from external retrieval or reranking, pass them as `--seed-file PATH`; the file must be a JSON array of string node ids, and its order becomes the traversal seed order.

If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal inline:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ Prefer the CLI when it is installed:
graphify query "QUESTION"
# or: graphify query "QUESTION" --dfs --budget 3000
```
If you already have ordered graph node ids from external retrieval or reranking, pass them as `--seed-file PATH`; the file must be a JSON array of string node ids, and its order becomes the traversal seed order.

If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal inline:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ Prefer the CLI when it is installed:
graphify query "QUESTION"
# or: graphify query "QUESTION" --dfs --budget 3000
```
If you already have ordered graph node ids from external retrieval or reranking, pass them as `--seed-file PATH`; the file must be a JSON array of string node ids, and its order becomes the traversal seed order.

If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal inline:

Expand Down
Loading