From 0d7da75d50f42c35cc2f6cffb31c9e203b629033 Mon Sep 17 00:00:00 2001 From: mandalore Date: Thu, 6 Aug 2026 12:30:05 -0500 Subject: [PATCH] fix: match disposition abbreviations like "Recon" via prefix, not just typos Levenshtein distance alone scored "Recon" too far from "Reconnaissance" (9 edits) to pass the typo threshold, since it's a truncation rather than a misspelling. Add a prefix check against each canonical name (and a filler-word-stripped "core" form, so "Take Hold" matches "Take and Hold") gated to 3+ characters to avoid collisions. Co-Authored-By: Claude Sonnet 5 --- src/main.js | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/src/main.js b/src/main.js index fbf88ae..1ab44b9 100644 --- a/src/main.js +++ b/src/main.js @@ -137,17 +137,31 @@ function levenshtein(a, b) { return prev[n]; } +const normalizeDispositionText = s => String(s || "").trim().toLowerCase().replace(/[^a-z ]/g, "").replace(/\s+/g, " "); + +// Filler-word-stripped form of each canonical name, so abbreviations that +// drop "and"/"the" (e.g. "Take Hold") still line up: "Take and Hold" -> "take hold". +const dispositionCore = canonical => normalizeDispositionText(canonical).replace(/\b(and|the)\b/g, "").replace(/\s+/g, " ").trim(); + // Nearest canonical disposition, or null if nothing is close enough to be -// confident it's a typo rather than unrelated text. Threshold scales with -// name length (~25%) so e.g. "Priority Assests" (1 char off "Priority -// Assets") matches, but a short unrelated line doesn't accidentally match -// "Disruption". +// confident it's a typo/abbreviation rather than unrelated text. +// Two ways to match: +// - Prefix: shorthand like "Recon" or "Disrupt" that's a truncation, not a +// typo, so Levenshtein distance alone would score it as far away. Gated +// to 3+ chars so short unrelated words can't collide (the five names' +// first three letters — pur/tak/pri/dis/rec — are all distinct). +// - Levenshtein: everything else, e.g. "Priority Assests". Threshold scales +// with name length (~25%) so a short unrelated line doesn't accidentally +// match "Disruption". function closestDisposition(name) { - const norm = String(name || "").trim().toLowerCase().replace(/[^a-z ]/g, "").replace(/\s+/g, " "); + const norm = normalizeDispositionText(name); if (!norm) return null; let best = null, bestDist = Infinity; for (const canonical of CANONICAL_DISPOSITIONS) { - const dist = levenshtein(norm, canonical.toLowerCase()); + const full = canonical.toLowerCase(); + const core = dispositionCore(canonical); + if (norm.length >= 3 && (full.startsWith(norm) || core.startsWith(norm))) return canonical; + const dist = Math.min(levenshtein(norm, full), levenshtein(norm, core)); if (dist < bestDist) { bestDist = dist; best = canonical; } } const threshold = Math.max(1, Math.round(best.length * 0.25));