fix(dedup): a rule or preference create surfaces what it overlaps by meaning (#4134)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 14s
CI & Build / TypeScript typecheck (push) Successful in 56s
CI & Build / integration (push) Successful in 1m4s
CI & Build / Python tests (push) Failing after 1m16s
CI & Build / Build & push image (push) Skipped

find_duplicate_rule was title-only, on the stated premise that rules are not
a semantic-retrieval surface - false since rules were embedded. A preference
restating a rule under another title passed untouched, and since both kinds
share one ranking, the weaker label could arrive alone.

find_overlapping_rules queries semantic_search_rules with the rule_document
shape, both kinds, in the scope the new record ranks in (global: every rule
the caller owns; project: global + that project). All three MCP create doors
call it before creating and return overlaps + overlap_note on the reply.

It advises rather than blocks, on measurement: across 16 sampled records the
nearest DISTINCT neighbour reached 0.853, while a true rewording scored 0.850.
No threshold separates the bands, so the floor (0.80) sits below the
restatement and the author judges. The stale docstring is corrected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy
This commit is contained in:
2026-09-21 22:39:02 -04:00
co-authored by Claude Opus 5
parent 22bb6d7a1a
commit 108b12eeb0
4 changed files with 453 additions and 10 deletions
+30 -6
View File
@@ -489,13 +489,20 @@ async def create_rule(
order_index: Display order within the topic (default 0). order_index: Display order within the topic (default 0).
force: Bypass the near-duplicate gate. By default, a title-identical rule force: Bypass the near-duplicate gate. By default, a title-identical rule
already in this topic BLOCKS creation and returns its id so you update already in this topic BLOCKS creation and returns its id so you update
it instead. Set true only for a genuinely distinct rule. it instead. Set true only for a genuinely distinct rule. A rule or
preference that answers the same MOMENT under another title does
not block: the create goes through and the reply carries
`overlaps` and `overlap_note` — read the top one and decide.
""" """
uid = current_user_id() uid = current_user_id()
if not force: if not force:
dup = await dedup_svc.find_duplicate_rule(title, topic_id=topic_id) dup = await dedup_svc.find_duplicate_rule(title, topic_id=topic_id)
if dup is not None: if dup is not None:
return dedup_svc.duplicate_response(dup, "rule") return dedup_svc.duplicate_response(dup, "rule")
# Before the create, so the new rule cannot find itself (#4134).
overlaps = await dedup_svc.find_overlapping_rules(
uid, title, statement, when_to_apply,
)
rule = await rulebooks_svc.create_rule( rule = await rulebooks_svc.create_rule(
topic_id=topic_id, user_id=uid, topic_id=topic_id, user_id=uid,
title=title, statement=statement, when_to_apply=when_to_apply, title=title, statement=statement, when_to_apply=when_to_apply,
@@ -503,7 +510,9 @@ async def create_rule(
why=why, how_to_apply=how_to_apply, order_index=order_index, why=why, how_to_apply=how_to_apply, order_index=order_index,
verify_with=verify_with, expires_when=expires_when, verify_with=verify_with, expires_when=expires_when,
) )
return await rulebooks_svc.rule_detail(uid, rule, system_ids) data = await rulebooks_svc.rule_detail(uid, rule, system_ids)
data.update(dedup_svc.overlap_response(overlaps, "rule"))
return data
async def create_project_rule( async def create_project_rule(
@@ -582,7 +591,9 @@ async def create_project_rule(
order_index: Display order within the project's rule list (default 0). order_index: Display order within the project's rule list (default 0).
force: Bypass the near-duplicate gate. By default, a title-identical rule force: Bypass the near-duplicate gate. By default, a title-identical rule
already on this project BLOCKS creation and returns its id so you already on this project BLOCKS creation and returns its id so you
update it instead. Set true only for a genuinely distinct rule. update it instead. Set true only for a genuinely distinct rule. An
overlap by meaning never blocks; it arrives as `overlaps` and
`overlap_note` on the reply — see create_rule.
""" """
uid = current_user_id() uid = current_user_id()
derived_title = title.strip() or statement.strip().split(".")[0][:50] derived_title = title.strip() or statement.strip().split(".")[0][:50]
@@ -590,6 +601,9 @@ async def create_project_rule(
dup = await dedup_svc.find_duplicate_rule(derived_title, project_id=project_id) dup = await dedup_svc.find_duplicate_rule(derived_title, project_id=project_id)
if dup is not None: if dup is not None:
return dedup_svc.duplicate_response(dup, "rule") return dedup_svc.duplicate_response(dup, "rule")
overlaps = await dedup_svc.find_overlapping_rules(
uid, derived_title, statement, when_to_apply, project_id=project_id,
)
rule = await rulebooks_svc.create_project_rule( rule = await rulebooks_svc.create_project_rule(
project_id=project_id, user_id=uid, project_id=project_id, user_id=uid,
title=derived_title, statement=statement, when_to_apply=when_to_apply, title=derived_title, statement=statement, when_to_apply=when_to_apply,
@@ -597,7 +611,9 @@ async def create_project_rule(
why=why, how_to_apply=how_to_apply, order_index=order_index, why=why, how_to_apply=how_to_apply, order_index=order_index,
verify_with=verify_with, expires_when=expires_when, verify_with=verify_with, expires_when=expires_when,
) )
return await rulebooks_svc.rule_detail(uid, rule, system_ids) data = await rulebooks_svc.rule_detail(uid, rule, system_ids)
data.update(dedup_svc.overlap_response(overlaps, "rule"))
return data
async def update_rule( async def update_rule(
@@ -784,7 +800,10 @@ async def create_preference(
a preference could only be filed after the fact (#4249). a preference could only be filed after the fact (#4249).
force: Bypass the near-duplicate gate. For a genuinely distinct force: Bypass the near-duplicate gate. For a genuinely distinct
preference, not for one that is "mostly" different — a mostly preference, not for one that is "mostly" different — a mostly
different preference is an update. different preference is an update. A RULE that already answers
this moment comes back as `overlaps` / `overlap_note` on the reply
rather than blocking; if it says the same thing, the preference is
the weaker copy of it and should go.
""" """
uid = current_user_id() uid = current_user_id()
if not when_to_apply.strip(): if not when_to_apply.strip():
@@ -803,13 +822,18 @@ async def create_preference(
dup = await dedup_svc.find_duplicate_rule(title, topic_id=topic_id) dup = await dedup_svc.find_duplicate_rule(title, topic_id=topic_id)
if dup is not None: if dup is not None:
return dedup_svc.duplicate_response(dup, "rule") return dedup_svc.duplicate_response(dup, "rule")
overlaps = await dedup_svc.find_overlapping_rules(
uid, title, statement, when_to_apply,
)
rule = await rulebooks_svc.create_rule( rule = await rulebooks_svc.create_rule(
topic_id=topic_id, user_id=uid, topic_id=topic_id, user_id=uid,
title=title, statement=statement, when_to_apply=when_to_apply, title=title, statement=statement, when_to_apply=when_to_apply,
kind="preference", arose_from_id=arose_from_id, kind="preference", arose_from_id=arose_from_id,
why=why, how_to_apply=how_to_apply, order_index=order_index, why=why, how_to_apply=how_to_apply, order_index=order_index,
) )
return await rulebooks_svc.rule_detail(uid, rule, system_ids) data = await rulebooks_svc.rule_detail(uid, rule, system_ids)
data.update(dedup_svc.overlap_response(overlaps, "preference"))
return data
async def update_preference( async def update_preference(
+133 -4
View File
@@ -772,10 +772,17 @@ async def find_duplicate_rule(
topic_id: int | None = None, topic_id: int | None = None,
project_id: int | None = None, project_id: int | None = None,
) -> DuplicateMatch | None: ) -> DuplicateMatch | None:
"""Title-based near-duplicate of a rule, scoped to the same topic (a rulebook """Title-identical rule in the same topic (a rulebook rule) or the same
rule) or the same project (a project rule). Rules aren't a semantic-retrieval project (a project rule) — the one signal certain enough to BLOCK on.
surface, so a normalized-title match is the right (and only) signal. Fail-open Fail-open like find_duplicate_note.
like find_duplicate_note."""
This is not the only duplicate signal for rules. It said so until #4134 —
"rules aren't a semantic-retrieval surface" — which stopped being true
when rules were embedded (rule_document, semantic_search_rules), and a
title is the field LEAST likely to collide when someone is deliberately
writing a second record about the same moment. find_overlapping_rules is
the meaning half; it surfaces rather than blocks, for the reason recorded
above _RULE_OVERLAP_FLOOR."""
norm = " ".join((title or "").split()).lower() norm = " ".join((title or "").split()).lower()
if not norm or (topic_id is None and project_id is None): if not norm or (topic_id is None and project_id is None):
return None return None
@@ -797,6 +804,128 @@ async def find_duplicate_rule(
return None return None
# --- rule / preference overlap (#4134) ----------------------------------------
# Rules and preferences are one table and one ranking: every hook arm searches
# them with no `kind` filter. So a preference that restates a rule is not a
# harmless near-copy — when only the preference places, a session receives
# binding guidance labelled "preference" and treats it as optional. The title
# gate above cannot see it: a second record about the same moment is exactly
# the case where someone chose a different title.
#
# WHY THIS SURFACES INSTEAD OF BLOCKING. Measured 2026-09-21 on bge-small-en-
# v1.5, querying with the gate's own rule_document shape across 16 sampled
# records (10 preferences, 6 rules) and reading the nearest OTHER record (#4134
# has the ids):
#
# a preference rewording an existing rule 0.850
# nearest distinct neighbours, 16 samples 0.672 0.853
# "when to delegate" beside "never delegate writing" 0.853
# "work lands on the working branch" beside "nothing reaches
# the release branch unasked" 0.850
# "let each action finish" beside "poll CI yourself" 0.847
#
# The two bands overlap: records that are deliberately distinct about one
# moment — a rule for what must happen beside a rule for what must not — sit
# exactly where a true restatement does. No threshold separates them, so a
# block would refuse legitimate records and teach force=true on every create.
# What the embedding CAN say reliably is "these answer the same moment", and
# whether they say the same THING is a reading, which is the author's. So the
# create goes through and carries the records it overlaps, with what to do if
# they are the same.
#
# 0.80 is the floor because the one measured true duplicate sat at 0.850 and a
# floor at the edge of it would miss the next, slightly looser rewording;
# 6 of the 16 distinct neighbours also clear it, which is the cost, paid in
# one line on the create's reply rather than in a refused write. Retune with
# the embedder, not the corpus.
_RULE_OVERLAP_FLOOR = 0.80
_RULE_OVERLAP_LIMIT = 3
@dataclass
class RuleOverlap:
"""An existing rule or preference that answers the same moment."""
id: int
title: str
kind: str # "rule" | "preference"
project_id: int | None
similarity: float
async def find_overlapping_rules(
user_id: int,
title: str,
statement: str,
when_to_apply: str,
*,
project_id: int | None = None,
) -> list[RuleOverlap]:
"""Existing rules AND preferences whose trigger reads as this one's.
Queried with rule_document — the exact shape the corpus is embedded as —
so the score compares like with like. Both kinds, because the harm is
across them (#4134).
Scope follows the new record's home. A project rule is compared with
global rules plus that project's own, the set it will rank against. A
global record (project_id None) applies everywhere, so it is compared with
every rule the caller owns: a global rule restating one project's rule is
the same overlap, arriving in that project.
Run BEFORE the create, so the new record cannot match itself. Never
raises: an overlap is advice, and a create must not depend on it.
"""
doc_title, doc_body = embeddings_svc.rule_document(title, statement, when_to_apply)
query = "\n\n".join(p for p in (doc_title, doc_body) if p)
# The note gate's floor, for the same reason: a short document sits in a
# tight neighbourhood and resembles everything.
if len(query.strip()) < _MIN_BODY_FOR_SEMANTIC:
return []
try:
hits = await embeddings_svc.semantic_search_rules(
user_id, query, limit=_RULE_OVERLAP_LIMIT,
threshold=_RULE_OVERLAP_FLOOR,
project_id=project_id, everywhere=project_id is None,
)
except Exception:
logger.debug("rule overlap check skipped", exc_info=True)
return []
return [
RuleOverlap(
id=rule.id, title=rule.title, kind=rule.kind or "rule",
project_id=rule.project_id, similarity=round(score, 3),
)
for score, rule in hits
]
def overlap_response(overlaps: list[RuleOverlap], new_kind: str) -> dict:
"""The keys a rule/preference create adds to its reply when the record it
just wrote answers the same moment as an existing one. Empty when none."""
if not overlaps:
return {}
top = overlaps[0]
named = "; ".join(
f'{o.kind} {o.id} "{o.title}" ({o.similarity})' for o in overlaps
)
return {
"overlaps": [
{"id": o.id, "title": o.title, "kind": o.kind,
"project_id": o.project_id, "similarity": o.similarity}
for o in overlaps
],
"overlap_note": (
f"Created — and it answers the same moment as: {named}. Read "
f"{top.kind} {top.id} now. If it says the same thing, fold what is "
f"new into it (update_{top.kind}) and delete this {new_kind}: two "
f"records ranked together split one instruction, and the weaker "
f"one can arrive alone. If they say different things about one "
f"moment, keep both — that is common and fine."
),
}
# --- the plan gate (milestone 415) ------------------------------------------- # --- the plan gate (milestone 415) -------------------------------------------
# A session asked "what work is open?" that cannot see an existing plan makes a # A session asked "what work is open?" that cannot see an existing plan makes a
# second one: a new milestone beside the one that already covers the work, or # second one: a new milestone beside the one that already covers the work, or
+17
View File
@@ -147,3 +147,20 @@ def _no_rule_arm():
with patch("scribe.services.plugin_context.semantic_search_rules", with patch("scribe.services.plugin_context.semantic_search_rules",
AsyncMock(return_value=[])): AsyncMock(return_value=[])):
yield yield
@pytest.fixture(autouse=True)
def _no_rule_overlap():
"""Stub the rule/preference create path's overlap check (#4134).
The same reason as _no_rule_arm, one door over: every create_rule /
create_project_rule / create_preference now asks semantic_search_rules
whether an existing record answers the same moment, so each existing
rule-tool unit test would load the embedding model through a call it never
meant to make. The check's own behaviour is tested in
tests/test_rule_overlap_gate.py, which binds the real function at import
time — before this patch runs — and stubs the search beneath it instead.
"""
with patch("scribe.services.dedup.find_overlapping_rules",
AsyncMock(return_value=[])):
yield
+273
View File
@@ -0,0 +1,273 @@
"""Rule and preference creates surface the records they overlap (#4134).
Rules and preferences share one table and one ranking, so a preference that
restates a rule splits one instruction in two — and when only the preference
places, binding guidance arrives labelled optional. The title gate could not
see it: a second record about the same moment is exactly the one written
under a different title.
These tests pin three things:
- WHAT is compared: the new record's rule_document (the shape the corpus is
embedded as), against BOTH kinds, in the scope the new record will rank in.
- THAT it advises rather than blocks. The measurement above
dedup._RULE_OVERLAP_FLOOR found distinct neighbours scoring as high as a
true restatement, so a block would refuse legitimate records.
- WHERE it runs: all three create doors, before the create, so the new record
cannot match itself.
`find_overlapping_rules` is bound here at import time, before conftest's
autouse `_no_rule_overlap` replaces the module attribute, so the service tests
exercise the real function and stub the search beneath it.
"""
from __future__ import annotations
import ast
import inspect
import pathlib
from unittest.mock import AsyncMock, patch
import pytest
from scribe.services import dedup
from scribe.services.dedup import (
RuleOverlap,
find_overlapping_rules,
overlap_response,
)
from tests.helpers import fake_rule
from tests.helpers import plain_rule_detail as _plain_detail
pytestmark = pytest.mark.usefixtures("_bind_user")
SEARCH = "scribe.services.dedup.embeddings_svc.semantic_search_rules"
TOOLS = "scribe.mcp.tools.rulebooks"
# Long enough to clear _MIN_BODY_FOR_SEMANTIC in the rule_document shape.
TRIGGER = (
"About to push a commit to dev and stop to ask whether pushing is allowed, "
"or ending a turn with a question about landing work that was already "
"committed on the branch the operator treats as home."
)
STATEMENT = "Push to dev after committing without asking first."
# ── what is compared ──────────────────────────────────────────────────────
@pytest.mark.asyncio
async def test_the_query_is_the_embedded_document_shape():
search = AsyncMock(return_value=[])
with patch(SEARCH, search):
await find_overlapping_rules(7, "Push without asking", STATEMENT, TRIGGER)
query = search.call_args.args[1]
title, body = dedup.embeddings_svc.rule_document(
"Push without asking", STATEMENT, TRIGGER,
)
assert query == f"{title}\n\n{body}"
assert "When to apply:" in query
@pytest.mark.asyncio
async def test_both_kinds_are_searched():
"""The harm is ACROSS kinds; a kind filter would hide exactly it."""
search = AsyncMock(return_value=[])
with patch(SEARCH, search):
await find_overlapping_rules(7, "t", STATEMENT, TRIGGER)
assert search.call_args.kwargs.get("kind") is None
@pytest.mark.asyncio
async def test_a_global_record_is_compared_with_every_rule_the_caller_owns():
search = AsyncMock(return_value=[])
with patch(SEARCH, search):
await find_overlapping_rules(7, "t", STATEMENT, TRIGGER)
kw = search.call_args.kwargs
assert kw["everywhere"] is True
assert kw["project_id"] is None
@pytest.mark.asyncio
async def test_a_project_rule_is_compared_with_what_it_will_rank_against():
search = AsyncMock(return_value=[])
with patch(SEARCH, search):
await find_overlapping_rules(7, "t", STATEMENT, TRIGGER, project_id=5)
kw = search.call_args.kwargs
assert kw["project_id"] == 5
assert kw["everywhere"] is False
@pytest.mark.asyncio
async def test_the_floor_is_the_measured_one():
search = AsyncMock(return_value=[])
with patch(SEARCH, search):
await find_overlapping_rules(7, "t", STATEMENT, TRIGGER)
assert search.call_args.kwargs["threshold"] == dedup._RULE_OVERLAP_FLOOR
def test_the_floor_sits_below_the_measured_restatement():
"""A reworded duplicate of a real rule scored 0.850 (the measurement above
_RULE_OVERLAP_FLOOR). A floor at or above that misses the case the check
exists for; this fails if someone raises it there by analogy with the
blocking gates' 0.90+."""
assert dedup._RULE_OVERLAP_FLOOR < 0.85
@pytest.mark.asyncio
async def test_a_short_document_is_not_searched():
search = AsyncMock(return_value=[])
with patch(SEARCH, search):
out = await find_overlapping_rules(7, "t", "s", "when")
assert out == []
search.assert_not_called()
@pytest.mark.asyncio
async def test_a_failed_search_lets_the_create_through():
with patch(SEARCH, AsyncMock(side_effect=RuntimeError("no embedder"))):
out = await find_overlapping_rules(7, "t", STATEMENT, TRIGGER)
assert out == []
@pytest.mark.asyncio
async def test_hits_carry_their_kind():
hits = [
(0.8504, fake_rule(id=1, title="`dev` is home", kind="rule")),
(0.81, fake_rule(id=9, title="Report pushes", kind="preference", project_id=3)),
]
with patch(SEARCH, AsyncMock(return_value=hits)):
out = await find_overlapping_rules(7, "t", STATEMENT, TRIGGER)
assert out == [
RuleOverlap(1, "`dev` is home", "rule", None, 0.85),
RuleOverlap(9, "Report pushes", "preference", 3, 0.81),
]
# ── what it says ──────────────────────────────────────────────────────────
def test_no_overlap_adds_nothing():
assert overlap_response([], "rule") == {}
def test_the_note_names_the_record_to_read_and_both_outcomes():
out = overlap_response(
[RuleOverlap(1, "`dev` is home", "rule", None, 0.85)], "preference",
)
assert out["overlaps"] == [{
"id": 1, "title": "`dev` is home", "kind": "rule",
"project_id": None, "similarity": 0.85,
}]
note = out["overlap_note"]
assert "Created" in note # it did not block
assert "update_rule" in note # the top match's own door
assert "delete this preference" in note # the new record's kind
assert "keep both" in note # distinct records are legitimate
# ── where it runs ─────────────────────────────────────────────────────────
def _rule_one_overlaps():
return [RuleOverlap(1, "`dev` is home", "rule", None, 0.85)]
@pytest.mark.asyncio
async def test_create_rule_checks_before_creating_and_reports():
order: list[str] = []
find = AsyncMock(side_effect=lambda *a, **k: order.append("find") or _rule_one_overlaps())
create = AsyncMock(side_effect=lambda **k: order.append("create") or fake_rule(id=50))
with patch(f"{TOOLS}.dedup_svc.find_duplicate_rule", AsyncMock(return_value=None)), \
patch(f"{TOOLS}.dedup_svc.find_overlapping_rules", find), \
patch(f"{TOOLS}.rulebooks_svc.create_rule", create), _plain_detail():
from scribe.mcp.tools.rulebooks import create_rule
out = await create_rule(
topic_id=10, title="Push without asking", statement=STATEMENT,
when_to_apply=TRIGGER,
)
assert order == ["find", "create"]
assert find.call_args.args == (7, "Push without asking", STATEMENT, TRIGGER)
assert out["id"] == 50
assert out["overlaps"][0]["id"] == 1
@pytest.mark.asyncio
async def test_create_project_rule_checks_in_its_project():
find = AsyncMock(return_value=_rule_one_overlaps())
with patch(f"{TOOLS}.dedup_svc.find_duplicate_rule", AsyncMock(return_value=None)), \
patch(f"{TOOLS}.dedup_svc.find_overlapping_rules", find), \
patch(f"{TOOLS}.rulebooks_svc.create_project_rule",
AsyncMock(return_value=fake_rule(id=51, project_id=5, topic_id=None))), \
_plain_detail():
from scribe.mcp.tools.rulebooks import create_project_rule
out = await create_project_rule(
project_id=5, title="Push without asking", statement=STATEMENT,
when_to_apply=TRIGGER,
)
assert find.call_args.kwargs == {"project_id": 5}
assert "overlap_note" in out
@pytest.mark.asyncio
async def test_create_preference_checks_and_names_itself_a_preference():
find = AsyncMock(return_value=_rule_one_overlaps())
with patch(f"{TOOLS}.dedup_svc.find_duplicate_rule", AsyncMock(return_value=None)), \
patch(f"{TOOLS}.dedup_svc.find_overlapping_rules", find), \
patch(f"{TOOLS}.rulebooks_svc.create_rule",
AsyncMock(return_value=fake_rule(id=52, kind="preference"))), \
_plain_detail():
from scribe.mcp.tools.rulebooks import create_preference
out = await create_preference(
topic_id=10, title="Push without asking", statement=STATEMENT,
when_to_apply=TRIGGER, arose_from_id=42,
)
find.assert_awaited_once()
assert "delete this preference" in out["overlap_note"]
@pytest.mark.asyncio
async def test_a_create_with_no_overlap_reads_as_before():
with patch(f"{TOOLS}.dedup_svc.find_duplicate_rule", AsyncMock(return_value=None)), \
patch(f"{TOOLS}.dedup_svc.find_overlapping_rules", AsyncMock(return_value=[])), \
patch(f"{TOOLS}.rulebooks_svc.create_rule",
AsyncMock(return_value=fake_rule(id=53))), _plain_detail():
from scribe.mcp.tools.rulebooks import create_rule
out = await create_rule(
topic_id=10, title="t", statement=STATEMENT, when_to_apply=TRIGGER,
)
assert "overlaps" not in out and "overlap_note" not in out
CREATE_DOORS = ("create_rule", "create_project_rule", "create_preference")
def test_every_rule_create_door_asks():
"""Structural, so a fourth create door fails here rather than shipping
with the title gate alone. Keyed on the doors that call
find_duplicate_rule: any door gated by title must also be checked by
meaning, because the title gate is the one that cannot see this."""
root = pathlib.Path(__file__).resolve().parents[1] / "src" / "scribe"
tree = ast.parse((root / "mcp" / "tools" / "rulebooks.py").read_text())
title_gated, overlap_checked = set(), set()
for fn in tree.body:
if not isinstance(fn, ast.AsyncFunctionDef):
continue
for node in ast.walk(fn):
if isinstance(node, ast.Attribute):
if node.attr == "find_duplicate_rule":
title_gated.add(fn.name)
elif node.attr == "find_overlapping_rules":
overlap_checked.add(fn.name)
assert title_gated >= set(CREATE_DOORS), "registry drifted from the module"
assert title_gated <= overlap_checked, (
f"title-gated but never checked by meaning: "
f"{sorted(title_gated - overlap_checked)}"
)
def test_the_stale_premise_is_gone():
"""find_duplicate_rule claimed rules were not a semantic-retrieval surface
— false since rules were embedded, and the reason this gap survived."""
doc = inspect.getdoc(dedup.find_duplicate_rule) or ""
assert "aren't a semantic-retrieval" not in doc
assert "find_overlapping_rules" in doc