fix(backend): read a tooth code whatever script its digits are in

The extraction model transcribes Persian speech, so it can hand back "۲۶"
in Persian digits or "2 6" from a digit-by-digit dictation. Both were
compared literally against /^[1-8][1-8]$/, missed, and fell through to the
positional branch with no quadrant — where the tooth was reported as "not
understood". The clinician loses a tooth and is told the words were the
problem.

normalizeFdiCode() now runs at both the branch choice and the final
validation, so the two cannot disagree. toLatinDigits moves out of
jalali.ts into common/digits.ts: it was exported but unused in production,
and a tooth module reaching into the calendar module would read as an
accident.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-21 04:42:09 +08:00
parent efff258910
commit c09698aea2
9 changed files with 115 additions and 54 deletions

View File

@@ -1,4 +1,4 @@
import { isFdiTooth, toFdi } from '../../common/fdi';
import { isFdiTooth, normalizeFdiCode, toFdi } from '../../common/fdi';
import type { ToothIntent, UnresolvedItem } from './voice.types';
export type ToothResolution = {
@@ -9,10 +9,10 @@ export type ToothResolution = {
/** Everything here parses untrusted model output, so nothing may throw. */
function normalizedFdi(intent: ToothIntent): string {
const raw = (intent as { fdi?: unknown }).fdi;
// Trimmed for parity with normalizeTeeth — '14 ' is tooth 14 through the treatment API
// and must not be "malformed" here.
return typeof raw === 'string' ? raw.trim() : '';
// Same normalisation the wire layer used to pick this branch, so the two cannot
// disagree: '14 ' is tooth 14 through the treatment API and '۲۶' is tooth 26, and
// neither may be reported as malformed here.
return normalizeFdiCode((intent as { fdi?: unknown }).fdi);
}
/**