feat(dedup): per-kind duplicate-report floors — notes/tasks default 0.93
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 10s
CI & Build / integration (push) Successful in 19s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 53s
CI & Build / Build & push image (push) Successful in 42s
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 10s
CI & Build / integration (push) Successful in 19s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 53s
CI & Build / Build & push image (push) Successful in 42s
At chunk grain (#280) a note-pair's similarity is its closest chunk pair, so the shared 0.82 floor saturated the note/task reports with related families (38 note / 155 task groups against the 200-pair cap, measured 2026-08-09). Split kb_duplicate_threshold into per-kind settings keys with per-kind defaults: snippet 0.82 (single-chunk, scale unchanged), note/task 0.93 (points the report at genuinely-alike records). Settings UI grows the two new knobs; report entrypoints inherit the change via get_duplicate_threshold(user_id, kind). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs
This commit is contained in:
@@ -10,7 +10,7 @@ import pytest
|
||||
|
||||
from scribe.services import dedup as dedup_svc
|
||||
from scribe.services.dedup import (
|
||||
DUPLICATE_DEFAULT_THRESHOLD,
|
||||
DUPLICATE_DEFAULT_THRESHOLDS,
|
||||
get_duplicate_threshold,
|
||||
group_pairs,
|
||||
)
|
||||
@@ -61,29 +61,52 @@ def test_a_node_never_forms_a_group_with_itself():
|
||||
# --- threshold ------------------------------------------------------------
|
||||
|
||||
|
||||
async def test_threshold_falls_back_to_the_default_when_unset():
|
||||
@pytest.mark.parametrize("kind", ["snippet", "note", "task"])
|
||||
async def test_threshold_falls_back_to_the_per_kind_default_when_unset(kind):
|
||||
default = DUPLICATE_DEFAULT_THRESHOLDS[kind]
|
||||
with patch("scribe.services.settings.get_setting",
|
||||
AsyncMock(return_value=str(DUPLICATE_DEFAULT_THRESHOLD))):
|
||||
assert await get_duplicate_threshold(1) == DUPLICATE_DEFAULT_THRESHOLD
|
||||
AsyncMock(return_value=str(default))):
|
||||
assert await get_duplicate_threshold(1, kind) == default
|
||||
|
||||
|
||||
async def test_each_kind_reads_its_own_settings_key():
|
||||
"""Tuning the note floor must not move the snippet report — the whole
|
||||
point of splitting the setting is that the kinds calibrate independently."""
|
||||
get = AsyncMock(return_value="0.5")
|
||||
with patch("scribe.services.settings.get_setting", get):
|
||||
await get_duplicate_threshold(1, "note")
|
||||
key, default = get.await_args.args[1], get.await_args.args[2]
|
||||
assert key == "kb_duplicate_threshold_note"
|
||||
assert default == str(DUPLICATE_DEFAULT_THRESHOLDS["note"])
|
||||
|
||||
|
||||
async def test_a_garbage_setting_falls_back_rather_than_raising():
|
||||
with patch("scribe.services.settings.get_setting",
|
||||
AsyncMock(return_value="not-a-number")):
|
||||
assert await get_duplicate_threshold(1) == DUPLICATE_DEFAULT_THRESHOLD
|
||||
assert (await get_duplicate_threshold(1, "snippet")
|
||||
== DUPLICATE_DEFAULT_THRESHOLDS["snippet"])
|
||||
|
||||
|
||||
@pytest.mark.parametrize("stored, expected", [("2.5", 1.0), ("-3", 0.0)])
|
||||
async def test_threshold_is_clamped_to_the_valid_range(stored, expected):
|
||||
with patch("scribe.services.settings.get_setting", AsyncMock(return_value=stored)):
|
||||
assert await get_duplicate_threshold(1) == expected
|
||||
assert await get_duplicate_threshold(1, "snippet") == expected
|
||||
|
||||
|
||||
def test_the_report_threshold_is_looser_than_the_write_gate():
|
||||
"""The gate BLOCKS a create and must be unforgiving of noise; this only
|
||||
suggests a merge the operator reviews, so it has to reach further or it
|
||||
would never surface the pairs the gate already let through."""
|
||||
assert DUPLICATE_DEFAULT_THRESHOLD < dedup_svc._SEMANTIC_THRESHOLD
|
||||
def test_the_snippet_report_threshold_is_looser_than_the_write_gate():
|
||||
"""The gate BLOCKS a create and must be unforgiving of noise; the snippet
|
||||
report only suggests a merge the operator reviews, so it has to reach
|
||||
further or it would never surface the pairs the gate already let through.
|
||||
Snippet-only on purpose: note/task floors sit ABOVE the gate because chunk
|
||||
grain (#280) lifts related families over it — see the module comment."""
|
||||
assert DUPLICATE_DEFAULT_THRESHOLDS["snippet"] < dedup_svc._SEMANTIC_THRESHOLD
|
||||
|
||||
|
||||
def test_the_note_and_task_floors_are_stricter_than_the_snippet_floor():
|
||||
"""At chunk grain, 0.82 is a related-families view; the report's default
|
||||
must point at genuinely-alike records (measured 2026-08-09, note #2551)."""
|
||||
assert DUPLICATE_DEFAULT_THRESHOLDS["note"] == 0.93
|
||||
assert DUPLICATE_DEFAULT_THRESHOLDS["task"] == 0.93
|
||||
|
||||
|
||||
# --- fail-open ------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user