From 820f43411581450d514a112bd877808165d68981 Mon Sep 17 00:00:00 2001 From: mandalore Date: Thu, 6 Aug 2026 12:33:51 -0500 Subject: [PATCH] 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 --- src/main.js | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/src/main.js b/src/main.js index 1ab44b9..1e40e71 100644 --- a/src/main.js +++ b/src/main.js @@ -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