Files
graphify/tests/test_report.py
T
safishamsi 5779767fd3 feat(reflect): work-memory overlay — surface learned verdicts as a graph sidecar (#1441)
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>
2026-06-30 11:19:54 +01:00

108 lines
5.3 KiB
Python

import json
from pathlib import Path
from graphify.build import build_from_json
from graphify.cluster import cluster, score_all
from graphify.analyze import god_nodes, surprising_connections
from graphify.report import generate
FIXTURES = Path(__file__).parent / "fixtures"
def make_inputs():
extraction = json.loads((FIXTURES / "extraction.json").read_text())
G = build_from_json(extraction)
communities = cluster(G)
cohesion = score_all(G, communities)
labels = {cid: f"Community {cid}" for cid in communities}
gods = god_nodes(G)
surprises = surprising_connections(G)
detection = {"total_files": 4, "total_words": 62400, "needs_graph": True, "warning": None}
tokens = {"input": extraction["input_tokens"], "output": extraction["output_tokens"]}
return G, communities, cohesion, labels, gods, surprises, detection, tokens
def test_report_contains_header():
G, communities, cohesion, labels, gods, surprises, detection, tokens = make_inputs()
report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, "./project")
assert "# Graph Report" in report
def test_report_contains_corpus_check():
G, communities, cohesion, labels, gods, surprises, detection, tokens = make_inputs()
report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, "./project")
assert "## Corpus Check" in report
def test_report_contains_god_nodes():
G, communities, cohesion, labels, gods, surprises, detection, tokens = make_inputs()
report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, "./project")
assert "## God Nodes" in report
def test_report_contains_surprising_connections():
G, communities, cohesion, labels, gods, surprises, detection, tokens = make_inputs()
report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, "./project")
assert "## Surprising Connections" in report
def test_report_contains_communities():
G, communities, cohesion, labels, gods, surprises, detection, tokens = make_inputs()
report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, "./project")
assert "## Communities" in report
def test_report_contains_ambiguous_section():
G, communities, cohesion, labels, gods, surprises, detection, tokens = make_inputs()
report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, "./project")
assert "## Ambiguous Edges" in report
def test_report_shows_token_cost():
G, communities, cohesion, labels, gods, surprises, detection, tokens = make_inputs()
report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, "./project")
assert "Token cost" in report
assert "1,200" in report
def test_report_shows_raw_cohesion_scores():
G, communities, cohesion, labels, gods, surprises, detection, tokens = make_inputs()
report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, "./project", min_community_size=1)
assert "Cohesion:" in report
assert "✓" not in report
assert "⚠" not in report
# --- work-memory lessons section ----------------------------------------------
def test_report_work_memory_section_present_with_overlay_and_dead_ends():
"""When a work-memory overlay (preferred sources) and query-scoped dead-ends
are supplied, the report grows a `## Work-memory lessons` section listing the
preferred sources and, separately, the dead-ends as question -> nodes."""
G, communities, cohesion, labels, gods, surprises, detection, tokens = make_inputs()
learning = {
"overlay": {
"auth_login": {"status": "preferred", "uses": 3, "score": 2.4,
"label": "login()", "stale": False},
"redis": {"status": "tentative", "uses": 1, "score": 0.5,
"label": "RedisClient", "stale": False},
},
"dead_ends": [
{"question": "does it use websockets?", "nodes": ["WSServer"], "date": "2026-05-01"},
],
}
report = generate(G, communities, cohesion, labels, gods, surprises, detection,
tokens, "./project", learning=learning)
assert "## Work-memory lessons" in report
assert "**Preferred sources**" in report
assert "`login()`" in report
# Tentative is not listed in the report's preferred block.
assert "RedisClient" not in report
# Dead-ends are query-scoped: question -> nodes, NOT a node-level status.
assert "**Known dead ends**" in report
assert "does it use websockets?" in report
assert "`WSServer`" in report
def test_report_work_memory_section_absent_without_overlay():
"""No learning input => no section; report identical to pre-feature."""
G, communities, cohesion, labels, gods, surprises, detection, tokens = make_inputs()
before = generate(G, communities, cohesion, labels, gods, surprises, detection,
tokens, "./project")
assert "## Work-memory lessons" not in before
# Explicit empty learning also omits the section.
empty = generate(G, communities, cohesion, labels, gods, surprises, detection,
tokens, "./project", learning={"overlay": {}, "dead_ends": []})
assert "## Work-memory lessons" not in empty
assert before == empty