fix(stt): anchor callsign digits verbatim, fuzzy only the airline name

The whole-string fuzzy fallback in callsignMatches() ran before the
digit-anchored check and let single-digit typos slip through (e.g.
"Lufthansa 350" matched expected "DLH359" at Levenshtein distance 1).
Spoken-digit-word candidates ("three five niner") bypassed the digit
anchor entirely since they contain no digit characters pre-fold.

Fold candidates through denormalizeSpokenAtc before checking for
digits, then require the flight number verbatim (word-boundary regex,
zero tolerance) while keeping the existing ~25% fuzzy tolerance on the
airline-name portion. Single-digit flight numbers (DLH4) now get the
same anchoring as longer ones.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
itsrubberduck
2026-07-14 17:47:11 +02:00
parent 46224dd70d
commit e4859c418e
2 changed files with 89 additions and 16 deletions

View File

@@ -276,6 +276,64 @@ describe('matchTranscriptionToFields — callsign tolerance', () => {
})
})
describe('matchTranscriptionToFields — callsign digit strictness', () => {
// A single wrong digit is a DIFFERENT flight. The generous fuzzy tolerance
// may only ever forgive the airline name, never the flight number.
const lufthansa = {
key: 'callsign',
expected: 'DLH359',
alternatives: ['DLH359', 'DLH 359', 'Lufthansa 359', 'Lufthansa three five niner'],
isCallsign: true,
} as const
it('does not match a flight number that differs in the last digit (spoken form)', () => {
// Whisper heard "three five zero" — that is DLH350, not DLH359.
const result = matchTranscriptionToFields('Lufthansa three five zero', [lufthansa])
assert.equal(result.matches['callsign'], undefined)
})
it('does not match a flight number that differs in the last digit (written form)', () => {
const result = matchTranscriptionToFields('Lufthansa 350', [lufthansa])
assert.equal(result.matches['callsign'], undefined)
})
it('does not match transposed flight-number digits (271 vs spoken 217)', () => {
const result = matchTranscriptionToFields('Speedbird two one seven', [
{
key: 'callsign',
expected: 'BAW271',
alternatives: ['BAW271', 'BAW 271', 'Speedbird 271', 'Speedbird two seven one'],
isCallsign: true,
},
])
assert.equal(result.matches['callsign'], undefined)
})
it('matches a single-digit flight number despite an airline-name typo', () => {
const result = matchTranscriptionToFields('Lufthana four', [
{
key: 'callsign',
expected: 'DLH4',
alternatives: ['DLH4', 'DLH 4', 'Lufthansa 4', 'Lufthansa four'],
isCallsign: true,
},
])
assert.equal(result.matches['callsign'], 'DLH4')
})
it('does not match a single-digit flight number with the wrong digit', () => {
const result = matchTranscriptionToFields('Lufthansa five', [
{
key: 'callsign',
expected: 'DLH4',
alternatives: ['DLH4', 'DLH 4', 'Lufthansa 4', 'Lufthansa four'],
isCallsign: true,
},
])
assert.equal(result.matches['callsign'], undefined)
})
})
describe('matchTranscriptionToFields — realistic radio-check', () => {
it('handles a full spoken radio check readback', () => {
const result = matchTranscriptionToFields(