feat(dedup): the create gate's similarity bars are settings (#4385, rule 25)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 10s
CI & Build / TypeScript typecheck (push) Successful in 57s
CI & Build / integration (push) Successful in 56s
CI & Build / Python tests (push) Successful in 1m38s
CI & Build / Build & push image (push) Successful in 38s

gate_bars(user_id, note_type) resolves the block bar and, for notes and
tasks, the overlap floor from kb_gate_* settings, with the old constants
as defaults. Fail-open on an unreadable value; a block bar clamps at 0.80
and the overlap floor at 0.70 and never above the bar. Five fields in
Settings beside the duplicate-report floors.

CLAIM_LEASE stays a constant, with the reason written at it: a per-user
lease would make one shared task live to one reader and dead to another.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-24 07:11:10 -04:00
co-authored by Claude Opus 5.5
parent baf22179ef
commit 4502f0a1ae
5 changed files with 286 additions and 11 deletions
+118
View File
@@ -120,6 +120,12 @@ const loadingTuning = ref(false);
const kbDupThresholdSnippet = ref("0.82");
const kbDupThresholdNote = ref("0.93");
const kbDupThresholdTask = ref("0.93");
// The create gate's bars (#4385). Defaults mirror services/dedup.py.
const kbGateThreshold = ref("0.9");
const kbGateThresholdSnippet = ref("0.96");
const kbGateThresholdLesson = ref("0.96");
const kbGateThresholdNoteCopy = ref("0.98");
const kbGateNoteOverlapFloor = ref("0.87");
// PLAN_MATCH_DEFAULT_THRESHOLD in services/dedup.py.
const kbPlanMatchThreshold = ref("0.80");
const savingKbInject = ref(false);
@@ -277,6 +283,14 @@ async function saveKbInject() {
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));
// The gate BLOCKS a write, so its bars have a floor the server enforces too:
// 0.80 for a block, 0.70 for the overlap list.
const gateAt = (v: string, d: number, lo: number) => Math.min(1, Math.max(lo, Number(v) || d));
const gate = gateAt(kbGateThreshold.value, 0.9, 0.8);
const gateSnip = gateAt(kbGateThresholdSnippet.value, 0.96, 0.8);
const gateLesson = gateAt(kbGateThresholdLesson.value, 0.96, 0.8);
const gateCopy = gateAt(kbGateThresholdNoteCopy.value, 0.98, 0.8);
const gateOverlap = gateAt(kbGateNoteOverlapFloor.value, 0.87, 0.7);
// Same `|| default` guard: a floor of 0 would hand back an existing plan
// for every new one, and no plan could be started without force.
const planT = Math.min(1, Math.max(0, Number(kbPlanMatchThreshold.value) || 0.8));
@@ -317,6 +331,11 @@ async function saveKbInject() {
kbDupThresholdSnippet.value = String(dupSnip);
kbDupThresholdNote.value = String(dupNote);
kbDupThresholdTask.value = String(dupTask);
kbGateThreshold.value = String(gate);
kbGateThresholdSnippet.value = String(gateSnip);
kbGateThresholdLesson.value = String(gateLesson);
kbGateThresholdNoteCopy.value = String(gateCopy);
kbGateNoteOverlapFloor.value = String(gateOverlap);
kbPlanMatchThreshold.value = String(planT);
kbWritePathThreshold.value = String(wpT);
kbRuleHintThreshold.value = String(rhT);
@@ -369,6 +388,11 @@ async function saveKbInject() {
kb_duplicate_threshold_snippet: String(dupSnip),
kb_duplicate_threshold_note: String(dupNote),
kb_duplicate_threshold_task: String(dupTask),
kb_gate_threshold: String(gate),
kb_gate_threshold_snippet: String(gateSnip),
kb_gate_threshold_lesson: String(gateLesson),
kb_gate_threshold_note_copy: String(gateCopy),
kb_gate_note_overlap_floor: String(gateOverlap),
kb_plan_match_threshold: String(planT),
});
kbInjectSaved.value = true;
@@ -866,6 +890,15 @@ onMounted(async () => {
if (allSettings.kb_duplicate_threshold_task !== undefined) {
kbDupThresholdTask.value = allSettings.kb_duplicate_threshold_task;
}
for (const [key, target] of [
["kb_gate_threshold", kbGateThreshold],
["kb_gate_threshold_snippet", kbGateThresholdSnippet],
["kb_gate_threshold_lesson", kbGateThresholdLesson],
["kb_gate_threshold_note_copy", kbGateThresholdNoteCopy],
["kb_gate_note_overlap_floor", kbGateNoteOverlapFloor],
] as const) {
if (allSettings[key] !== undefined) target.value = allSettings[key];
}
if (allSettings.kb_plan_match_threshold !== undefined) {
kbPlanMatchThreshold.value = allSettings.kb_plan_match_threshold;
}
@@ -2019,6 +2052,91 @@ async function deleteUser(userId: number) {
</p>
</div>
<div class="field">
<label for="kb-gate-threshold">Create gate — general block threshold</label>
<input
id="kb-gate-threshold"
v-model="kbGateThreshold"
type="number"
min="0.8"
max="1"
step="0.01"
class="fs-input input"
style="max-width: 8rem"
/>
<p class="field-hint">
When a new record is at least this alike to an existing one of the same kind, the create is refused and pointed at the existing record to update. Applies to kinds without their own bar below (processes, and anything new). Higher = fewer refusals, more duplicates let through. Cannot go below 0.80: the gate blocks a write, and a low bar refuses everything on a shared topic.
</p>
</div>
<div class="field">
<label for="kb-gate-threshold-snippet">Create gate — snippets</label>
<input
id="kb-gate-threshold-snippet"
v-model="kbGateThresholdSnippet"
type="number"
min="0.8"
max="1"
step="0.01"
class="fs-input input"
style="max-width: 8rem"
/>
<p class="field-hint">
The semantic backstop for snippets. Code and location are checked first and exactly, so this only catches a reworded copy; it sits above the band where deliberate siblings (a button and its outline variant) land.
</p>
</div>
<div class="field">
<label for="kb-gate-threshold-lesson">Create gate — lessons</label>
<input
id="kb-gate-threshold-lesson"
v-model="kbGateThresholdLesson"
type="number"
min="0.8"
max="1"
step="0.01"
class="fs-input input"
style="max-width: 8rem"
/>
<p class="field-hint">
Lessons about one area read alike without being the same lesson, so this is high. A refusal loses a real lesson outright; a miss leaves two you can merge.
</p>
</div>
<div class="field">
<label for="kb-gate-threshold-note-copy">Create gate — notes and tasks (copy)</label>
<input
id="kb-gate-threshold-note-copy"
v-model="kbGateThresholdNoteCopy"
type="number"
min="0.8"
max="1"
step="0.01"
class="fs-input input"
style="max-width: 8rem"
/>
<p class="field-hint">
Notes and tasks are refused only as a near-exact copy. Consecutive dev-logs and parts of one design score 0.90–0.98 without being duplicates, so a lower bar refuses the next one.
</p>
</div>
<div class="field">
<label for="kb-gate-note-overlap-floor">Create gate — notes and tasks (listed overlaps)</label>
<input
id="kb-gate-note-overlap-floor"
v-model="kbGateNoteOverlapFloor"
type="number"
min="0.7"
max="1"
step="0.01"
class="fs-input input"
style="max-width: 8rem"
/>
<p class="field-hint">
Below the copy bar, matches above this are listed on the create reply (up to three) for the session to judge, and the record is still created. Lower = more listed, more of them noise. Never above the copy bar.
</p>
</div>
<div class="field">
<label for="kb-plan-match-threshold">Existing-plan match threshold</label>
<input