fix: default to first disposition when text lists two, e.g. "X (Y)"

Some sources list two dispositions together (a team template noting a
secondary/alternate, e.g. "Purge the Foe (Reconnaissance)"). Split on the
separators that join them and match segment-by-segment left to right, so
the first one listed wins instead of whichever scored closer across the
combined string.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-06 12:33:51 -05:00
co-authored by Claude Sonnet 5
parent 0d7da75d50
commit 820f434115
+19 -5
View File
@@ -143,9 +143,9 @@ const normalizeDispositionText = s => String(s || "").trim().toLowerCase().repla
// 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/abbreviation rather than unrelated text.
// Two ways to match:
// Nearest canonical disposition for a single (already-normalized) segment, or
// null if nothing is close enough to be 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'
@@ -153,8 +153,7 @@ const dispositionCore = canonical => normalizeDispositionText(canonical).replace
// - 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 = normalizeDispositionText(name);
function matchDispositionSegment(norm) {
if (!norm) return null;
let best = null, bestDist = Infinity;
for (const canonical of CANONICAL_DISPOSITIONS) {
@@ -168,6 +167,21 @@ function closestDisposition(name) {
return bestDist <= threshold ? best : null;
}
// Some sources list two dispositions together, e.g. "Purge the Foe
// (Reconnaissance)" (a team template noting a secondary/alternate) or
// "Take and Hold / Priority Assets". Split on the separators that join them
// and match segment-by-segment left to right, so whichever is listed first
// wins rather than whichever happens to score closer across the whole string.
function closestDisposition(name) {
const raw = String(name || "");
const segments = raw.split(/[()\/|,;]+|\s+vs\.?\s+/i).map(s => s.trim()).filter(Boolean);
for (const seg of segments.length ? segments : [raw]) {
const match = matchDispositionSegment(normalizeDispositionText(seg));
if (match) return match;
}
return null;
}
const correctDisposition = name => closestDisposition(name) || name;
// The vocabulary of disposition names is whatever this event's own roster