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

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:
2026-08-09 10:33:11 -04:00
co-authored by Claude Fable 5
parent 23cb396e65
commit 272b7dbddf
3 changed files with 135 additions and 38 deletions
+72 -16
View File
@@ -27,10 +27,14 @@ const kbWritePathEnabled = ref(true);
// code embeddings sit on a much higher similarity floor than prose, so 0.55 let
// unrelated code through (#2223). Shares top-k, not the threshold.
const kbWritePathThreshold = ref("0.68");
// Near-duplicate report floor. Deliberately looser than the 0.90 write-time
// gate: that one BLOCKS a create and must be unforgiving of noise, this one only
// suggests a merge the operator reviews (services/dedup.py).
const kbDuplicateThreshold = ref("0.82");
// Near-duplicate report floors, one per record kind (services/dedup.py).
// Snippets are single-chunk, so their floor sits below the 0.90 write-time
// gate and catches what it lets through. Notes/tasks are scored at chunk
// grain (#280) — related families clear 0.90 easily — so their floor sits
// above the gate to keep the report pointed at genuinely-alike records.
const kbDupThresholdSnippet = ref("0.82");
const kbDupThresholdNote = ref("0.93");
const kbDupThresholdTask = ref("0.93");
const savingKbInject = ref(false);
const kbInjectSaved = ref(false);
@@ -76,16 +80,20 @@ async function saveRetention() {
async function saveKbInject() {
const t = Math.min(1, Math.max(0, Number(kbInjectThreshold.value) || 0));
const k = Math.min(10, Math.max(1, Math.floor(Number(kbInjectTopK.value) || 1)));
// `|| 0.82` not `|| 0`: an unparseable value here should fall back to the
// default, not to 0 — a 0 floor would report every snippet as a duplicate of
// every other one.
const dupT = Math.min(1, Math.max(0, Number(kbDuplicateThreshold.value) || 0.82));
// Same `|| default` reasoning as dupT: falling back to 0 would surface every
// `|| default` not `|| 0`: an unparseable value here should fall back to the
// per-kind default, not to 0 — a 0 floor would report every record as a
// duplicate of every other one.
const dupSnip = Math.min(1, Math.max(0, Number(kbDupThresholdSnippet.value) || 0.82));
const dupNote = Math.min(1, Math.max(0, Number(kbDupThresholdNote.value) || 0.93));
const dupTask = Math.min(1, Math.max(0, Number(kbDupThresholdTask.value) || 0.93));
// Same `|| default` reasoning: falling back to 0 would surface every
// snippet in the corpus on every edit, which is the failure this knob fixes.
const wpT = Math.min(1, Math.max(0, Number(kbWritePathThreshold.value) || 0.68));
kbInjectThreshold.value = String(t);
kbInjectTopK.value = String(k);
kbDuplicateThreshold.value = String(dupT);
kbDupThresholdSnippet.value = String(dupSnip);
kbDupThresholdNote.value = String(dupNote);
kbDupThresholdTask.value = String(dupTask);
kbWritePathThreshold.value = String(wpT);
savingKbInject.value = true;
kbInjectSaved.value = false;
@@ -99,7 +107,9 @@ async function saveKbInject() {
// measurements that split them.
kb_writepath_enabled: kbWritePathEnabled.value ? 'true' : 'false',
kb_writepath_threshold: String(wpT),
kb_duplicate_threshold: String(dupT),
kb_duplicate_threshold_snippet: String(dupSnip),
kb_duplicate_threshold_note: String(dupNote),
kb_duplicate_threshold_task: String(dupTask),
});
kbInjectSaved.value = true;
setTimeout(() => (kbInjectSaved.value = false), 2000);
@@ -487,8 +497,14 @@ onMounted(async () => {
if (allSettings.kb_writepath_threshold !== undefined) {
kbWritePathThreshold.value = allSettings.kb_writepath_threshold;
}
if (allSettings.kb_duplicate_threshold !== undefined) {
kbDuplicateThreshold.value = allSettings.kb_duplicate_threshold;
if (allSettings.kb_duplicate_threshold_snippet !== undefined) {
kbDupThresholdSnippet.value = allSettings.kb_duplicate_threshold_snippet;
}
if (allSettings.kb_duplicate_threshold_note !== undefined) {
kbDupThresholdNote.value = allSettings.kb_duplicate_threshold_note;
}
if (allSettings.kb_duplicate_threshold_task !== undefined) {
kbDupThresholdTask.value = allSettings.kb_duplicate_threshold_task;
}
if (allSettings.notify_task_reminders !== undefined) {
notifyTaskReminders.value = allSettings.notify_task_reminders !== "false";
@@ -1267,10 +1283,10 @@ function formatUserDate(iso: string): string {
is for (#274). -->
<div class="field">
<label for="kb-duplicate-threshold">Near-duplicate report threshold</label>
<label for="kb-duplicate-threshold-snippet">Near-duplicate report threshold snippets</label>
<input
id="kb-duplicate-threshold"
v-model="kbDuplicateThreshold"
id="kb-duplicate-threshold-snippet"
v-model="kbDupThresholdSnippet"
type="number"
min="0"
max="1"
@@ -1285,6 +1301,46 @@ function formatUserDate(iso: string): string {
you review it never acts on its own.
</p>
</div>
<div class="field">
<label for="kb-duplicate-threshold-note">Near-duplicate report threshold notes</label>
<input
id="kb-duplicate-threshold-note"
v-model="kbDupThresholdNote"
type="number"
min="0"
max="1"
step="0.01"
class="input"
style="max-width: 8rem"
/>
<p class="field-hint">
The floor for the Knowledge page's note report. Notes are compared
section by section, so related records — a run of dev-logs, notes on
one topic — score high without being duplicates. Stricter than the
snippet floor on purpose; lower it to browse related families rather
than hunt true duplicates.
</p>
</div>
<div class="field">
<label for="kb-duplicate-threshold-task">Near-duplicate report threshold — tasks</label>
<input
id="kb-duplicate-threshold-task"
v-model="kbDupThresholdTask"
type="number"
min="0"
max="1"
step="0.01"
class="input"
style="max-width: 8rem"
/>
<p class="field-hint">
Same as the note floor, for the task report. Step tasks from different
milestones ("Verify on CI") legitimately resemble each other, so this
stays strict to keep the report pointed at work opened twice.
</p>
</div>
<div class="actions">
<button class="btn-primary" @click="saveKbInject" :disabled="savingKbInject">
{{ savingKbInject ? 'Saving' : 'Save' }}