feat(reflect): annotate the graph with work-memory verdicts and surface them in explain/query (#1441)#1542
feat(reflect): annotate the graph with work-memory verdicts and surface them in explain/query (#1441)#1542TPAteeq wants to merge 4 commits into
Conversation
…ce them The work-memory loop (Graphify-Labs#1441) ingests the raw Q&A memory docs into the graph, but the distilled verdicts graphify reflect computes (preferred / tentative / contested / dead_end, time-decay scored) only ever land in reflections/LESSONS.md -- a flat file, overwritten each run, that nothing reads back. Write side: graphify reflect --annotate-graph stamps each cited source node's verdict onto the graph as reserved learning_status / learning_score / learning_uses attributes (matched by node id or cited label). Persistence is by recompute, not storage: annotate_graph clears all learning_* then re-stamps from the aggregate, so it is deterministic and idempotent (a re-run is byte-identical; a no-op annotation rewrites graph.json byte-for-byte, matching to_json's format), touches only learning_* keys, and never changes node count or any real field. The durable source of truth stays graphify-out/memory/*.md, so a full rebuild that drops the attributes is healed by the next reflect. The git post-commit/post-checkout hooks pass annotate=True after each rebuild so the graph self-heals; learning_* is excluded from both watch graph-compare functions so an annotation never forces a needless rebuild. Read side (so the verdicts are used, not write-only): graphify explain prints a Lesson: line for an annotated node, and the shared query renderer (graphify query CLI + the MCP query_graph tool) tags each node line with learning=<status>. The status is one of four fixed strings but is sanitized like every other field on the MCP output path (F-010), and appended inside the existing NODE line so the token-budget truncation accounting is unchanged. Missing id/label is never coerced to the string "None"; a non-dict graph.json returns 0 per the best-effort contract. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Surface the learning_status verdicts (graphify reflect --annotate-graph) in the human-facing report, grouped by status (preferred / tentative / contested / dead end) with the per-node useful counts, so the lessons show up in the broad overview and not only when you explain or query a specific node. The section is omitted entirely on an un-annotated graph, so existing reports are unchanged. Deterministic ordering (score desc, then label), each group capped at 8 with a "+N more" suffix. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Color the interactive graph.html by learning_status (graphify reflect --annotate-graph): an annotated node gets a status-colored border ring -- green preferred, light-green tentative, amber contested, red dead end -- while the community color stays the node fill, so the two signals read independently. The verdict is also shown as a "Lesson" field in the node info panel. Only the four known statuses produce a ring, and the value is sanitized before reaching the HTML; un-annotated graphs render byte-identically (no ring, no Lesson field). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Resolve CHANGELOG.md: keep the reflect --annotate-graph entry under ## Unreleased, above the new ## 0.9.2 release section. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Thanks for this, and for pushing the work-memory loop forward — surfacing the distilled verdicts where you actually look (explain/query/report/html) is exactly the right instinct, and it shipped on We landed it with one architectural change from this PR: instead of stamping So I'm closing this PR as implemented-differently rather than merged — the idea and the read-surface design are yours and credited in the commit. Really appreciate it; please keep them coming. |
…aph 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>
README: document that `reflect --graph` writes the .graphify_learning.json overlay and that explain/query surface a Lesson hint (with the code-changed staleness flag). CHANGELOG: add an Unreleased section for the post-0.9.2 work — the work-memory overlay (#1441/#1542), this.field.method() injected-field resolution (#1316), TS wildcard path aliases (#1544), JS namespace re-exports (#1552), and the ObjC dot-syntax/@selector edges (#1475/#1543). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Summary
Follow-up to #1441. The work-memory loop re-ingests the raw Q&A memory docs into the graph,
but the distilled verdicts
graphify reflectcomputes (preferred / tentative / contested /dead_end, time-decay scored) only land in
reflections/LESSONS.md— a flat file, overwritteneach run, that nothing reads back. This makes those verdicts first-class graph data and
surfaces them where you actually look.
Write side —
reflect --annotate-graphStamps each cited source node's verdict onto the graph (matched by node id or cited label):
learning_statuspreferred/tentative/contested/dead_endlearning_scorelearning_usesPersistence is by recompute, not storage.
annotate_graphclears everylearning_*thenre-stamps from the aggregate: deterministic and idempotent (a re-run is byte-identical, a no-op
rewrites
graph.jsonbyte-for-byte matchingexport.to_json), touches onlylearning_*, neverchanges node count or real fields. The durable truth stays
graphify-out/memory/*.md→ a fullrebuild that drops the attributes is healed by the next reflect. The git
post-commit/post-checkout hooks pass
annotate=Trueafter each rebuild so the graph self-heals;learning_*is excluded from both watch compare functions so an annotation never forces aneedless rebuild.
Read side — the verdicts are actually used (four surfaces)
graphify explain <node>→Lesson: preferred source (start here) — 2× useful, score=2.0graphify query+ MCPquery_graph→NODE Transformer [src=… learning=preferred]GRAPH_REPORT.md→ a "Work-memory lessons" section listing preferred sources / known dead endsgraph.html→ annotated nodes get a status-colored ring (green = preferred, red = dead end;the community color stays the fill) and a
Lesson:field in the node info panelThe status is one of four fixed strings but is
sanitize_label'd on every output path (MCP F-010and HTML), and on the query path appended inside the existing NODE line so the token-budget
truncation accounting is unchanged. Un-annotated graphs are unaffected: no
Lesson:line, nolearning=suffix, no report section, no node ring (graph.htmlgains only a few constanttemplate lines; the rendered graph is visually identical).
Tests
annotate_graph: by-id/by-label matching, all four verdicts, idempotency, self-healing,byte-identical no-op, non-dict graph → 0, label-less node not matched by a
"None"citation,end-to-end via
reflect(annotate=True)on a realbuild_from_jsongraph.learning_*(the--no-clusterone without mutating originals).explainshows/omits theLesson:line;_subgraph_to_text(query CLI + MCP)tags/omits
learning=; the GRAPH_REPORT section is present/absent; the HTML ring is present/absent.skillgen --check, ruff clean.Follow-ups (separate)
learning_status(batched to amortize the skillgen14-copy regen).
Out of scope by design: no new edges / "Lessons" node — node attributes only (no topology change);
corrections stay question-level in
LESSONS.md.🤖 Generated with Claude Code