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:
co-authored by
Claude Opus 4.8
parent
d193b6277d
commit
1a14e94e53
@@ -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
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user