fix(reflect): dedupe dead-ends and corrections by question

Saving the same Q&A more than once duplicated lines in the "known dead ends" and
"corrections" sections: both lists were appended per memory doc with no key, while
node scoring already dedups by node. They now collapse by question, keeping the
most recent entry (docs are processed oldest-first, so a re-corrected question
shows its latest correction). Output stays deterministic, ordered by (date,
question). Applied to both the flat lists and the per-community buckets.

Found by a user running it on a 104-file Go codebase. Added a regression test
covering the dedupe and the recency-wins correction. Full suite 2389 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
safishamsi
2026-06-24 12:48:57 +01:00
co-authored by Claude Opus 4.8
parent d193b6277d
commit 1a14e94e53
3 changed files with 34 additions and 3 deletions
+1
View File
@@ -4,6 +4,7 @@ Full release notes with details on each version: [GitHub Releases](https://githu
## Unreleased
- Fix: `graphify reflect` no longer duplicates lines in the "known dead ends" and "corrections" sections when the same Q&A is saved more than once. Those lists were appended per memory doc with no key (node scoring already dedups by node, but these two did not); they now collapse by question, keeping the most recent entry — so a re-corrected question shows its latest correction. Output stays deterministic (ordered by date then question).
- Fix: the work-memory loop no longer depends on the git hook. The skill now tells the agent to run `graphify reflect --if-stale` itself at the start of graph work (cheap, deterministic, a no-op when no outcomes have been saved), then read `LESSONS.md`. Previously a skill-only install (without `graphify hook install`) would keep recording outcomes via `save-result` but never regenerate `LESSONS.md`, so the lessons never surfaced. The post-commit hook is now an optimization for between-session freshness rather than a requirement. The new `--if-stale` flag skips the run when `LESSONS.md` is already newer than every input (the memory docs and the graph), so when the hook just refreshed it the agent's session-start run costs almost nothing.
## 0.8.47 (2026-06-24)
+18 -3
View File
@@ -328,6 +328,20 @@ def _finalize_sources(bucket: dict[str, Any],
return {"preferred": preferred, "tentative": tentative, "contested": contested}
def _dedupe_by_question(items: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""Collapse repeated questions to one entry. Docs are processed oldest-first, so
the last write per question wins (recency — e.g. the most recent correction text).
Output is deterministically ordered by (date, question). Without this, saving the
same Q&A twice duplicated lines in the dead-ends / corrections lists, even though
node scoring already dedups by node.
"""
latest: dict[str, dict[str, Any]] = {}
for it in items:
latest[it.get("question", "")] = it
return sorted(latest.values(),
key=lambda it: (it.get("date", ""), it.get("question", "")))
def aggregate_lessons(docs: list[dict[str, Any]],
node_community: dict[str, str] | None = None,
*,
@@ -384,7 +398,8 @@ def aggregate_lessons(docs: list[dict[str, Any]],
if node_community:
community_out = {
label: {"counts": b["counts"], **_finalize_sources(b, min_corroboration),
"dead_ends": b["dead_ends"], "corrections": b["corrections"]}
"dead_ends": _dedupe_by_question(b["dead_ends"]),
"corrections": _dedupe_by_question(b["corrections"])}
for label, b in by_community.items()
}
@@ -393,8 +408,8 @@ def aggregate_lessons(docs: list[dict[str, Any]],
"counts": overall["counts"],
"min_corroboration": min_corroboration,
**_finalize_sources(overall, min_corroboration),
"dead_ends": overall["dead_ends"],
"corrections": overall["corrections"],
"dead_ends": _dedupe_by_question(overall["dead_ends"]),
"corrections": _dedupe_by_question(overall["corrections"]),
"by_community": community_out,
}
+15
View File
@@ -631,3 +631,18 @@ def test_cli_reflect_if_stale_skips_when_fresh(tmp_path):
ran = _run(["reflect", "--if-stale"], tmp_path)
assert ran.returncode == 0
assert "up to date" not in (ran.stdout + ran.stderr).lower()
def test_dead_ends_and_corrections_dedupe_by_question():
"""Saving the same Q&A more than once must not duplicate lines in the dead-ends
/ corrections lists; for a re-corrected question the most recent text wins."""
docs = [
_doc("dead_end", question="ws server?", date="2026-01-01"),
_doc("dead_end", question="ws server?", date="2026-01-02"), # duplicate
_doc("corrected", question="hash?", correction="SHA-1", date="2026-01-01"),
_doc("corrected", question="hash?", correction="SHA-256", date="2026-01-03"), # newer
]
agg = aggregate_lessons(docs, now=_NOW)
assert [d["question"] for d in agg["dead_ends"]] == ["ws server?"]
assert len(agg["corrections"]) == 1
assert agg["corrections"][0]["correction"] == "SHA-256" # recency wins