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 <noreply@anthropic.com>
This commit is contained in:
+20
-6
@@ -137,17 +137,31 @@ function levenshtein(a, b) {
|
|||||||
return prev[n];
|
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
|
// Nearest canonical disposition, or null if nothing is close enough to be
|
||||||
// confident it's a typo rather than unrelated text. Threshold scales with
|
// confident it's a typo/abbreviation rather than unrelated text.
|
||||||
// name length (~25%) so e.g. "Priority Assests" (1 char off "Priority
|
// Two ways to match:
|
||||||
// Assets") matches, but a short unrelated line doesn't accidentally match
|
// - Prefix: shorthand like "Recon" or "Disrupt" that's a truncation, not a
|
||||||
// "Disruption".
|
// 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) {
|
function closestDisposition(name) {
|
||||||
const norm = String(name || "").trim().toLowerCase().replace(/[^a-z ]/g, "").replace(/\s+/g, " ");
|
const norm = normalizeDispositionText(name);
|
||||||
if (!norm) return null;
|
if (!norm) return null;
|
||||||
let best = null, bestDist = Infinity;
|
let best = null, bestDist = Infinity;
|
||||||
for (const canonical of CANONICAL_DISPOSITIONS) {
|
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; }
|
if (dist < bestDist) { bestDist = dist; best = canonical; }
|
||||||
}
|
}
|
||||||
const threshold = Math.max(1, Math.round(best.length * 0.25));
|
const threshold = Math.max(1, Math.round(best.length * 0.25));
|
||||||
|
|||||||
Reference in New Issue
Block a user