mirror of
https://github.com/safishamsi/graphify.git
synced 2026-07-12 18:37:12 +00:00
5779767fd3
Projects the verdicts `graphify reflect` already distills (preferred / tentative / contested, exponential time-decayed) into a derived experiential layer the read surfaces consume, so accumulated agent experience actually shows up where you look — without polluting the structural graph. Design (grounded in agent-memory + provenance literature; a redesign of the #1542 approach): - SIDECAR, not graph.json stamping. `reflect` writes `.graphify_learning.json` next to graph.json (an additional output, so the git hooks produce it automatically). graph.json stays purely structural; nothing leaks into GraphML; no graph.json churn. Mirrors the named-graph / event-sourcing separation of durable truth from a derived layer. - Reuses the existing reflect aggregate (its `_decay` is the recency-weighted exponential model; `_finalize_sources` the classification) — no new scoring. - PROVENANCE: each verdict carries the source questions/dates that produced it (cap 5, most-recent first). - STALENESS: each verdict stores the node's file fingerprint; on read, a changed source file flags the verdict stale ("code changed since — re-verify") rather than presenting a confident lesson on rewritten code. - CONTESTED surfaced distinctly (useful N / dead-end M), not averaged away. - DEAD-ENDS stay QUERY-SCOPED — never a node-level status; they appear only in the report as question -> nodes. - Read surfaces (explain / query+MCP / GRAPH_REPORT / graph.html) merge the overlay at read time, sanitized; un-annotated graphs are byte-identical. Deferred (logged): letting verdicts influence query/seed traversal — the recommender feedback-loop / Matthew-effect risk means that needs propensity correction + exploration, not naive biasing. Builds on the idea in #1441/#1542 (thanks @TPAteeq). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
126 lines
5.1 KiB
Python
126 lines
5.1 KiB
Python
"""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
|
|
|
|
|
|
def test_explain_source_file_path_prefers_file_level_node(monkeypatch, tmp_path, capsys):
|
|
source_file = "app/api/example/route.ts"
|
|
graph_data = {
|
|
"directed": False, "multigraph": False, "graph": {},
|
|
"nodes": [
|
|
{"id": "example_route_get", "label": "GET()",
|
|
"source_file": source_file, "source_location": "L42", "community": 0},
|
|
{"id": "example_route", "label": "route.ts",
|
|
"source_file": source_file, "source_location": "L1", "community": 0},
|
|
],
|
|
"links": [
|
|
{"source": "example_route", "target": "example_route_get",
|
|
"relation": "contains", "confidence": "EXTRACTED"},
|
|
],
|
|
}
|
|
p = tmp_path / "graph.json"
|
|
p.write_text(json.dumps(graph_data))
|
|
|
|
out = _run(monkeypatch, p, source_file, capsys)
|
|
|
|
assert "Node: route.ts" in out
|
|
assert "ID: example_route" in out
|
|
assert f"Source: {source_file} L1" in out
|
|
assert "Node: GET()" not in out
|
|
|
|
|
|
# --- work-memory overlay Lesson line ------------------------------------------
|
|
|
|
def _write_sidecar(tmp_path, nodes):
|
|
(tmp_path / ".graphify_learning.json").write_text(
|
|
json.dumps({"version": 1, "generated_at": "2026-06-01T00:00:00+00:00",
|
|
"nodes": nodes}),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
|
|
def test_explain_shows_preferred_lesson_line(monkeypatch, tmp_path, capsys):
|
|
p = _write_graph(tmp_path)
|
|
_write_sidecar(tmp_path, {
|
|
"validate": {"status": "preferred", "score": 2.4, "uses": 3,
|
|
"label": "validateSanitySession()", "source_file": "",
|
|
"code_fingerprint": "", "provenance": []},
|
|
})
|
|
out = _run(monkeypatch, p, "validateSanitySession", capsys)
|
|
assert "Lesson: preferred source (start here) — 3 useful, score=2.4" in out
|
|
assert "code changed" not in out
|
|
|
|
|
|
def test_explain_shows_contested_and_stale_lesson(monkeypatch, tmp_path, capsys):
|
|
p = _write_graph(tmp_path)
|
|
# source_file points at a path that does not exist -> loader marks it stale.
|
|
_write_sidecar(tmp_path, {
|
|
"validate": {"status": "contested", "score": -0.1, "uses": 2, "neg": 1,
|
|
"verdict": "dead end", "label": "validateSanitySession()",
|
|
"source_file": "server/sanity-validate-session.ts",
|
|
"code_fingerprint": "deadbeef", "provenance": []},
|
|
})
|
|
out = _run(monkeypatch, p, "validateSanitySession", capsys)
|
|
assert "Lesson: contested (useful 2 / dead-end 1)" in out
|
|
assert "[code changed since — re-verify]" in out
|
|
|
|
|
|
def test_explain_no_lesson_line_for_unannotated_node(monkeypatch, tmp_path, capsys):
|
|
"""No sidecar => no Lesson line; output identical to pre-feature."""
|
|
p = _write_graph(tmp_path)
|
|
out = _run(monkeypatch, p, "validateSanitySession", capsys)
|
|
assert "Lesson:" not in out
|