2026-08-21 04:44:20 +08:00
|
|
|
import {
|
|
|
|
|
isFdiTooth,
|
|
|
|
|
normalizeFdiCode,
|
|
|
|
|
toFdi,
|
|
|
|
|
type Arch,
|
|
|
|
|
type PatientSide,
|
|
|
|
|
} from '../../common/fdi';
|
2026-08-20 17:13:26 +03:30
|
|
|
import type { ToothIntent, UnresolvedItem } from './voice.types';
|
|
|
|
|
|
2026-08-21 04:44:20 +08:00
|
|
|
const ARCHES: readonly Arch[] = ['upper', 'lower'];
|
|
|
|
|
const SIDES: readonly PatientSide[] = ['patient_right', 'patient_left'];
|
|
|
|
|
|
2026-08-20 17:13:26 +03:30
|
|
|
export type ToothResolution = {
|
|
|
|
|
/** Unique FDI codes, sorted (matching normalizeTeeth's ordering). */
|
|
|
|
|
teeth: string[];
|
|
|
|
|
unresolved: UnresolvedItem[];
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** Everything here parses untrusted model output, so nothing may throw. */
|
|
|
|
|
function normalizedFdi(intent: ToothIntent): string {
|
docs: cut the comments that were not earning their place
I wrote 731 comment lines on this branch against 4,530 lines of code — 14%,
where the rest of the repo runs at 1.8%. CLAUDE.md asks for code that reads
like its surroundings, and this did not.
Removed by genre rather than by taste:
- restating the code, e.g. "JS getUTCDay() numbering: Sunday = 0" above the
map that literally shows it, and a docblock on startOfWeek explaining that
it returns the start of the week;
- narrating history — "this used to rebuild the whole map", "left the bar
recording forever" — which the commit message and git blame already carry;
- saying the same thing in several places: the "cannot record is not a
denied microphone" reason appeared three times in one file, and the
"aborting stops a per-minute metered call" reason across three files. Each
now lives once, where the behaviour it explains lives;
- defending decisions nobody would question, like why toLatinDigits is its
own module;
- over-explaining defensive branches, three separate comments to distinguish
null from missing-kind from unrecognised-kind.
What stays is what the code cannot say: the patient-right convention in
toFdi, whose failure mode is a valid code for the wrong tooth; the
"this"-vs-"next" week anchoring; StrictMode re-arming mountedRef; Safari
accepting no mimeType hint; and the invariants whose violation already cost
a bug — the body parser's middleware ordering and the dispatch panel's
auto-fill rules.
Comments only. The diff contains no non-comment line.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 23:46:56 +08:00
|
|
|
// The same normalisation the wire layer used to pick this branch, so the two agree.
|
2026-08-21 04:42:09 +08:00
|
|
|
return normalizeFdiCode((intent as { fdi?: unknown }).fdi);
|
2026-08-20 17:13:26 +03:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
docs: cut the comments that were not earning their place
I wrote 731 comment lines on this branch against 4,530 lines of code — 14%,
where the rest of the repo runs at 1.8%. CLAUDE.md asks for code that reads
like its surroundings, and this did not.
Removed by genre rather than by taste:
- restating the code, e.g. "JS getUTCDay() numbering: Sunday = 0" above the
map that literally shows it, and a docblock on startOfWeek explaining that
it returns the start of the week;
- narrating history — "this used to rebuild the whole map", "left the bar
recording forever" — which the commit message and git blame already carry;
- saying the same thing in several places: the "cannot record is not a
denied microphone" reason appeared three times in one file, and the
"aborting stops a per-minute metered call" reason across three files. Each
now lives once, where the behaviour it explains lives;
- defending decisions nobody would question, like why toLatinDigits is its
own module;
- over-explaining defensive branches, three separate comments to distinguish
null from missing-kind from unrecognised-kind.
What stays is what the code cannot say: the patient-right convention in
toFdi, whose failure mode is a valid code for the wrong tooth; the
"this"-vs-"next" week anchoring; StrictMode re-arming mountedRef; Safari
accepting no mimeType hint; and the invariants whose violation already cost
a bug — the body parser's middleware ordering and the dispatch panel's
auto-fill rules.
Comments only. The diff contains no non-comment line.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 23:46:56 +08:00
|
|
|
* Never guesses and never clamps: position 9, a deciduous tooth or a malformed intent all
|
|
|
|
|
* resolve to null, so the caller surfaces "not understood" rather than silently selecting a
|
|
|
|
|
* neighbouring tooth.
|
2026-08-20 17:13:26 +03:30
|
|
|
*/
|
|
|
|
|
export function resolveToothIntent(intent: ToothIntent): string | null {
|
|
|
|
|
if (!intent || typeof intent !== 'object') return null;
|
|
|
|
|
|
|
|
|
|
if (intent.kind === 'explicit') {
|
|
|
|
|
const fdi = normalizedFdi(intent);
|
|
|
|
|
return isFdiTooth(fdi) ? fdi : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (intent.kind === 'positional') {
|
|
|
|
|
if (intent.arch !== 'upper' && intent.arch !== 'lower') return null;
|
|
|
|
|
if (intent.side !== 'patient_right' && intent.side !== 'patient_left')
|
|
|
|
|
return null;
|
|
|
|
|
return toFdi(intent.arch, intent.side, intent.position);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function unresolvedReason(intent: ToothIntent): UnresolvedItem['reason'] {
|
|
|
|
|
if (!intent || typeof intent !== 'object') return 'malformed';
|
|
|
|
|
|
|
|
|
|
if (intent.kind === 'explicit') {
|
|
|
|
|
const fdi = normalizedFdi(intent);
|
|
|
|
|
// Quadrants 1-4 are permanent and would already have resolved, so a well-formed
|
|
|
|
|
// quadrant+position reaching here is quadrant 5-8: deciduous. Anything else is noise.
|
|
|
|
|
return /^[1-8][1-8]$/.test(fdi) ? 'not_permanent_tooth' : 'malformed';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (intent.kind === 'positional') {
|
|
|
|
|
const positionBad =
|
|
|
|
|
!Number.isInteger(intent.position) ||
|
|
|
|
|
intent.position < 1 ||
|
|
|
|
|
intent.position > 8;
|
2026-08-21 04:02:39 +08:00
|
|
|
if (positionBad) return 'position_out_of_range';
|
docs: cut the comments that were not earning their place
I wrote 731 comment lines on this branch against 4,530 lines of code — 14%,
where the rest of the repo runs at 1.8%. CLAUDE.md asks for code that reads
like its surroundings, and this did not.
Removed by genre rather than by taste:
- restating the code, e.g. "JS getUTCDay() numbering: Sunday = 0" above the
map that literally shows it, and a docblock on startOfWeek explaining that
it returns the start of the week;
- narrating history — "this used to rebuild the whole map", "left the bar
recording forever" — which the commit message and git blame already carry;
- saying the same thing in several places: the "cannot record is not a
denied microphone" reason appeared three times in one file, and the
"aborting stops a per-minute metered call" reason across three files. Each
now lives once, where the behaviour it explains lives;
- defending decisions nobody would question, like why toLatinDigits is its
own module;
- over-explaining defensive branches, three separate comments to distinguish
null from missing-kind from unrecognised-kind.
What stays is what the code cannot say: the patient-right convention in
toFdi, whose failure mode is a valid code for the wrong tooth; the
"this"-vs-"next" week anchoring; StrictMode re-arming mountedRef; Safari
accepting no mimeType hint; and the invariants whose violation already cost
a bug — the body parser's middleware ordering and the dispatch panel's
auto-fill rules.
Comments only. The diff contains no non-comment line.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 23:46:56 +08:00
|
|
|
// The position was understood; the quadrant was never said. "دندون دو" names four
|
|
|
|
|
// teeth, so "could not be read" would send the clinician after the wrong fault.
|
2026-08-21 04:02:39 +08:00
|
|
|
const archMissing = intent.arch !== 'upper' && intent.arch !== 'lower';
|
|
|
|
|
const sideMissing =
|
|
|
|
|
intent.side !== 'patient_right' && intent.side !== 'patient_left';
|
|
|
|
|
return archMissing || sideMissing ? 'tooth_missing_quadrant' : 'malformed';
|
2026-08-20 17:13:26 +03:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return 'malformed';
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-21 04:44:20 +08:00
|
|
|
/**
|
docs: cut the comments that were not earning their place
I wrote 731 comment lines on this branch against 4,530 lines of code — 14%,
where the rest of the repo runs at 1.8%. CLAUDE.md asks for code that reads
like its surroundings, and this did not.
Removed by genre rather than by taste:
- restating the code, e.g. "JS getUTCDay() numbering: Sunday = 0" above the
map that literally shows it, and a docblock on startOfWeek explaining that
it returns the start of the week;
- narrating history — "this used to rebuild the whole map", "left the bar
recording forever" — which the commit message and git blame already carry;
- saying the same thing in several places: the "cannot record is not a
denied microphone" reason appeared three times in one file, and the
"aborting stops a per-minute metered call" reason across three files. Each
now lives once, where the behaviour it explains lives;
- defending decisions nobody would question, like why toLatinDigits is its
own module;
- over-explaining defensive branches, three separate comments to distinguish
null from missing-kind from unrecognised-kind.
What stays is what the code cannot say: the patient-right convention in
toFdi, whose failure mode is a valid code for the wrong tooth; the
"this"-vs-"next" week anchoring; StrictMode re-arming mountedRef; Safari
accepting no mimeType hint; and the invariants whose violation already cost
a bug — the body parser's middleware ordering and the dispatch panel's
auto-fill rules.
Comments only. The diff contains no non-comment line.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 23:46:56 +08:00
|
|
|
* The teeth still consistent with what *was* heard — "دو" leaves four, "دو بالا" two. Not
|
|
|
|
|
* a guess: the full set of readings, for the clinician to choose from.
|
2026-08-21 04:44:20 +08:00
|
|
|
*/
|
|
|
|
|
function quadrantCandidates(intent: ToothIntent): string[] {
|
|
|
|
|
if (intent.kind !== 'positional') return [];
|
|
|
|
|
const arches =
|
|
|
|
|
intent.arch === 'upper' || intent.arch === 'lower' ? [intent.arch] : ARCHES;
|
|
|
|
|
const sides =
|
|
|
|
|
intent.side === 'patient_right' || intent.side === 'patient_left'
|
|
|
|
|
? [intent.side]
|
|
|
|
|
: SIDES;
|
|
|
|
|
|
|
|
|
|
const codes: string[] = [];
|
|
|
|
|
for (const arch of arches) {
|
|
|
|
|
for (const side of sides) {
|
|
|
|
|
const fdi = toFdi(arch, side, intent.position);
|
|
|
|
|
if (fdi) codes.push(fdi);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return codes.sort();
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-20 17:13:26 +03:30
|
|
|
function spokenOf(intent: ToothIntent): string {
|
|
|
|
|
const spoken = (intent as { spoken?: unknown })?.spoken;
|
|
|
|
|
return typeof spoken === 'string' && spoken.trim() ? spoken.trim() : '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
docs: cut the comments that were not earning their place
I wrote 731 comment lines on this branch against 4,530 lines of code — 14%,
where the rest of the repo runs at 1.8%. CLAUDE.md asks for code that reads
like its surroundings, and this did not.
Removed by genre rather than by taste:
- restating the code, e.g. "JS getUTCDay() numbering: Sunday = 0" above the
map that literally shows it, and a docblock on startOfWeek explaining that
it returns the start of the week;
- narrating history — "this used to rebuild the whole map", "left the bar
recording forever" — which the commit message and git blame already carry;
- saying the same thing in several places: the "cannot record is not a
denied microphone" reason appeared three times in one file, and the
"aborting stops a per-minute metered call" reason across three files. Each
now lives once, where the behaviour it explains lives;
- defending decisions nobody would question, like why toLatinDigits is its
own module;
- over-explaining defensive branches, three separate comments to distinguish
null from missing-kind from unrecognised-kind.
What stays is what the code cannot say: the patient-right convention in
toFdi, whose failure mode is a valid code for the wrong tooth; the
"this"-vs-"next" week anchoring; StrictMode re-arming mountedRef; Safari
accepting no mimeType hint; and the invariants whose violation already cost
a bug — the body parser's middleware ordering and the dispatch panel's
auto-fill rules.
Comments only. The diff contains no non-comment line.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 23:46:56 +08:00
|
|
|
* Duplicates collapse; anything unresolvable is reported rather than dropped, so the sheet
|
|
|
|
|
* can show which words were not understood.
|
2026-08-20 17:13:26 +03:30
|
|
|
*/
|
|
|
|
|
export function resolveToothIntents(
|
|
|
|
|
intents: readonly ToothIntent[],
|
|
|
|
|
): ToothResolution {
|
|
|
|
|
const teeth = new Set<string>();
|
|
|
|
|
const unresolved: UnresolvedItem[] = [];
|
|
|
|
|
const seenUnresolved = new Set<string>();
|
|
|
|
|
|
|
|
|
|
// Not `intents ?? []`: a model may return an object or a number here, and a
|
|
|
|
|
// non-iterable must degrade like any other malformed shape rather than throw.
|
|
|
|
|
const list: readonly ToothIntent[] = Array.isArray(intents)
|
|
|
|
|
? (intents as readonly ToothIntent[])
|
|
|
|
|
: [];
|
|
|
|
|
|
|
|
|
|
for (const intent of list) {
|
|
|
|
|
const fdi = resolveToothIntent(intent);
|
|
|
|
|
if (fdi) {
|
|
|
|
|
teeth.add(fdi);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
const reason = unresolvedReason(intent);
|
|
|
|
|
const spoken = spokenOf(intent);
|
2026-08-21 04:44:20 +08:00
|
|
|
const candidates =
|
|
|
|
|
reason === 'tooth_missing_quadrant' ? quadrantCandidates(intent) : [];
|
docs: cut the comments that were not earning their place
I wrote 731 comment lines on this branch against 4,530 lines of code — 14%,
where the rest of the repo runs at 1.8%. CLAUDE.md asks for code that reads
like its surroundings, and this did not.
Removed by genre rather than by taste:
- restating the code, e.g. "JS getUTCDay() numbering: Sunday = 0" above the
map that literally shows it, and a docblock on startOfWeek explaining that
it returns the start of the week;
- narrating history — "this used to rebuild the whole map", "left the bar
recording forever" — which the commit message and git blame already carry;
- saying the same thing in several places: the "cannot record is not a
denied microphone" reason appeared three times in one file, and the
"aborting stops a per-minute metered call" reason across three files. Each
now lives once, where the behaviour it explains lives;
- defending decisions nobody would question, like why toLatinDigits is its
own module;
- over-explaining defensive branches, three separate comments to distinguish
null from missing-kind from unrecognised-kind.
What stays is what the code cannot say: the patient-right convention in
toFdi, whose failure mode is a valid code for the wrong tooth; the
"this"-vs-"next" week anchoring; StrictMode re-arming mountedRef; Safari
accepting no mimeType hint; and the invariants whose violation already cost
a bug — the body parser's middleware ordering and the dispatch panel's
auto-fill rules.
Comments only. The diff contains no non-comment line.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 23:46:56 +08:00
|
|
|
// Only dedupe what we can tell apart: without `spoken`, two lost references collapse
|
|
|
|
|
// into one blank row and a tooth vanishes. Candidates are part of the identity.
|
2026-08-20 17:13:26 +03:30
|
|
|
if (spoken) {
|
2026-08-21 04:44:20 +08:00
|
|
|
const key = `${spoken}::${reason}::${candidates.join(',')}`;
|
2026-08-20 17:13:26 +03:30
|
|
|
if (seenUnresolved.has(key)) continue;
|
|
|
|
|
seenUnresolved.add(key);
|
|
|
|
|
}
|
2026-08-21 04:44:20 +08:00
|
|
|
unresolved.push(
|
|
|
|
|
candidates.length > 0
|
|
|
|
|
? { spoken, reason, candidates }
|
|
|
|
|
: { spoken, reason },
|
|
|
|
|
);
|
2026-08-20 17:13:26 +03:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return { teeth: [...teeth].sort(), unresolved };
|
|
|
|
|
}
|