From 31b478b7ac49d27bcf3142785634c0664d9a35cb Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Fri, 18 Sep 2026 18:39:25 -0400 Subject: [PATCH] fix(dedup): the two new report kinds say what to DO about a duplicate (#4164) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `_REPORT_KINDS` gained `lesson` and `process` without `_KIND_SUGGESTION` gaining either, so the report would have listed pairs with no advice — and the suggestion is the report's point. CI's per-kind guard caught it. Lesson: read both triggers first (a lesson is retrieved by the situation it names, so alike insights under different triggers are two lessons); same trigger means one lesson learned twice, so fold the `taught_by` union into the survivor and delete the other. Process: a process arrives as a skill, so a duplicate runs the wrong procedure rather than merely cluttering a list — keep the one in use, fold the missing steps, delete the loser. The guard now asserts the property per kind rather than spot-checking two: no non-snippet suggestion may propose merge_snippets, so a sixth kind inherits the bar without anyone editing the test. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy --- src/scribe/services/dedup.py | 19 +++++++++++++++++++ tests/test_services_dedup.py | 4 ++++ 2 files changed, 23 insertions(+) diff --git a/src/scribe/services/dedup.py b/src/scribe/services/dedup.py index 7d5c24a..24bb184 100644 --- a/src/scribe/services/dedup.py +++ b/src/scribe/services/dedup.py @@ -523,6 +523,25 @@ _KIND_SUGGESTION = { "a work-log, and cancel the other with a pointer. If one CORRECTS the " "other's conclusions, supersession also works for tasks." ), + "lesson": ( + "Read both TRIGGERS before anything else — a lesson is retrieved by " + "the situation it names, so two alike insights under different " + "triggers are two lessons and belong apart. Same trigger, same " + "teaching means one lesson learned twice: keep the clearer one, " + "update_lesson it with the union of both `taught_by` lists and " + "anything the other said that it doesn't, then delete_lesson the " + "other. The sources are the point — a lesson that loses an incident " + "loses its evidence." + ), + "process": ( + "A process arrives at the agent as a skill, so two alike processes " + "compete for the same moment and whichever wins retrieval is the one " + "that runs — a duplicate here executes the wrong procedure rather " + "than merely cluttering a list. Keep the one actually in use, " + "update_process it with any step the other has, then delete_process " + "the loser. If they are genuinely different procedures that share " + "vocabulary, sharpen the titles instead so the right one wins." + ), } # kind → the Note-model predicate for BOTH sides of the self-join. Tasks are diff --git a/tests/test_services_dedup.py b/tests/test_services_dedup.py index ae9a9e1..005f1ac 100644 --- a/tests/test_services_dedup.py +++ b/tests/test_services_dedup.py @@ -344,6 +344,10 @@ def test_every_kind_has_a_suggestion_and_none_proposes_merging_notes(): for kind in _REPORT_KINDS: assert _KIND_SUGGESTION.get(kind), f"no suggestion for {kind}" + if kind != "snippet": + # The property, not a spot-check: a sixth kind added to the + # report inherits the bar without anyone editing this test. + assert "merge_snippets" not in _KIND_SUGGESTION[kind] assert "merge" in _KIND_SUGGESTION["snippet"] assert "NOT merge" in _KIND_SUGGESTION["note"] assert "supersedes" in _KIND_SUGGESTION["note"]