From 4f14690e66df1420403e21f3a20284ff15f0aef1 Mon Sep 17 00:00:00 2001 From: Azeem Date: Tue, 25 Aug 2026 01:15:28 +0000 Subject: [PATCH] fix(query): attach the learning sidecar so CLI query output carries lesson annotations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The subgraph renderer has supported learning=[:stale] NODE annotations since the work-memory overlay shipped, and both explain and the MCP server attach the .graphify_learning.json sidecar at load time. The CLI query loader builds its graph directly from graph.json and never attached the overlay, so the same question answered over MCP showed the lessons while the CLI answer silently dropped them — despite the docs promising query surfaces a Lesson hint. Attach the overlay in the CLI query path exactly like serve._load_graph: display-only, fail-safe to an empty overlay, byte-identical output when no sidecar exists. The :stale marker renders too. Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 4 +++ graphify/cli.py | 11 +++++++ tests/test_query_cli.py | 65 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c82baccf9e..3ea02861fe 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 bd2956815b..02620a5082 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 0db4e6fa8a..5f429673a8 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