diff --git a/CHANGELOG.md b/CHANGELOG.md index c82baccf9..3ea02861f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ Full release notes with details on each version: [GitHub Releases](https://github.com/safishamsi/graphify/releases) +## Unreleased + +- Fix: `graphify query` now surfaces the work-memory lesson annotations. The renderer has carried `learning=[:stale]` NODE-line support since the overlay shipped, and both `graphify explain` and the MCP server attach the `.graphify_learning.json` sidecar at load time — but the CLI `query` loader never did, so the same question answered over MCP showed the lessons while the CLI answer silently dropped them (the docs promise `query` surfaces a lesson hint). The CLI query path now attaches the overlay exactly like `serve._load_graph` (display-only, fail-safe to empty), so annotations — including the `:stale` "code changed since — re-verify" marker — render identically on both surfaces; without a sidecar the output is byte-identical to before. + ## 0.9.50 (2026-08-25) - Fix: Ruby methods whose names end in `!`, `?`, or `=` now keep distinct node ids, so `save` and `save!` (or `foo` and `foo=`) no longer collide into one node; the label keeps the raw spelling and member-call resolution still matches (#3077, thanks @hopstreax). diff --git a/graphify/cli.py b/graphify/cli.py index bd2956815..02620a508 100644 --- a/graphify/cli.py +++ b/graphify/cli.py @@ -1162,6 +1162,17 @@ def dispatch_command(cmd: str) -> None: except Exception as exc: print(f"error: could not load graph: {exc}", file=sys.stderr) sys.exit(1) + # Work-memory overlay: attach the .graphify_learning.json sidecar the + # same way the MCP loader does (serve._load_graph), so the renderer's + # learning=[:stale] NODE annotations appear in CLI `query` + # output too — `explain` and MCP already surface them, and the docs + # promise `query` does as well. Empty overlay on any error, leaving + # un-annotated output byte-identical. + try: + from graphify.reflect import load_learning_overlay as _llo + G.graph["_learning_overlay"] = _llo(gp) + except Exception: + G.graph["_learning_overlay"] = {} import time as _time _t0 = _time.perf_counter() _mode = "dfs" if use_dfs else "bfs" diff --git a/tests/test_query_cli.py b/tests/test_query_cli.py index 0db4e6fa8..5f429673a 100644 --- a/tests/test_query_cli.py +++ b/tests/test_query_cli.py @@ -123,3 +123,68 @@ def test_query_cli_rejects_oversized_graph(monkeypatch, tmp_path, capsys): err = capsys.readouterr().err assert "exceeds" in err assert "byte cap" in err + + +# --- work-memory overlay: lesson annotations in CLI query output --------------- + + +def _write_sidecar(tmp_path, nodes: dict) -> None: + """A .graphify_learning.json next to graph.json, in the writer's shape.""" + (tmp_path / ".graphify_learning.json").write_text( + json.dumps({"version": 1, "generated_at": "2026-01-01T00:00:00+00:00", + "nodes": nodes}), + encoding="utf-8", + ) + + +def test_query_cli_annotates_nodes_with_learning_status(monkeypatch, tmp_path, capsys): + """`graphify query` must merge the learning sidecar into NODE lines with the + same learning= form the MCP server renders — the docs promise + lessons surface in `query` output, not only in `explain`/MCP.""" + graph_path = _write_graph(tmp_path) + _write_sidecar(tmp_path, {"n1": {"status": "preferred"}}) + monkeypatch.setattr(mainmod, "_check_skill_version", lambda _: None) + monkeypatch.setattr( + mainmod.sys, + "argv", + ["graphify", "query", "extract", "--graph", str(graph_path)], + ) + mainmod.main() + out = capsys.readouterr().out + assert "learning=preferred]" in out + # Only the annotated node carries the suffix. + assert out.count("learning=") == 1 + + +def test_query_cli_marks_stale_learning_entries(monkeypatch, tmp_path, capsys): + """An entry whose cited code changed (or vanished) since the verdict must + carry the :stale marker, matching the MCP annotation form.""" + graph_path = _write_graph(tmp_path) + _write_sidecar( + tmp_path, + {"n1": {"status": "tentative", "source_file": "gone.py", + "code_fingerprint": "0123abcd"}}, + ) + monkeypatch.setattr(mainmod, "_check_skill_version", lambda _: None) + monkeypatch.setattr( + mainmod.sys, + "argv", + ["graphify", "query", "extract", "--graph", str(graph_path)], + ) + mainmod.main() + out = capsys.readouterr().out + assert "learning=tentative:stale]" in out + + +def test_query_cli_without_sidecar_stays_unannotated(monkeypatch, tmp_path, capsys): + """No sidecar -> no learning= anywhere; un-annotated output is unchanged.""" + graph_path = _write_graph(tmp_path) + monkeypatch.setattr(mainmod, "_check_skill_version", lambda _: None) + monkeypatch.setattr( + mainmod.sys, + "argv", + ["graphify", "query", "extract", "--graph", str(graph_path)], + ) + mainmod.main() + out = capsys.readouterr().out + assert "learning=" not in out