fix(voice): say the quadrant is missing instead of "could not be read"

"ترمیم برای دندون دو" set the treatment type but reported the tooth as
unreadable. Nothing was misheard: position 2 arrived intact, with no
quadrant, because none was spoken — four teeth carry position 2 and the
resolver correctly refused to pick one. Only the label was wrong, and it
sent the clinician looking for a transcription fault.

Adds a tooth_missing_quadrant reason that names what is missing and shows
how to say it ("دو بالا راست"), and tells the model explicitly to report a
quadrant-less number with arch and side null rather than guessing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-21 04:02:39 +08:00
parent afb30691cf
commit 1be735a7ab
8 changed files with 69 additions and 1 deletions

View File

@@ -150,6 +150,57 @@ describe('resolveToothIntents', () => {
}
});
it('says the quadrant is missing rather than blaming the words', () => {
// Regression: "ترمیم برای دندون دو" reported "could not be read", sending the
// clinician to look for a transcription fault. Position 2 was understood fine —
// what is missing is the quadrant, and four teeth carry position 2.
const bare = {
kind: 'positional',
arch: null,
side: null,
position: 2,
spoken: 'دندون دو',
} as unknown as ToothIntent;
const result = resolveToothIntents([bare]);
expect(result.teeth).toEqual([]);
expect(result.unresolved).toEqual([
{ spoken: 'دندون دو', reason: 'tooth_missing_quadrant' },
]);
});
it('reports a missing quadrant for a half-specified tooth too', () => {
// "دو بالا" narrows it to 12 or 22 — still not one tooth, and still not our guess.
for (const half of [
{ arch: 'upper', side: null },
{ arch: null, side: 'patient_right' },
]) {
const result = resolveToothIntents([
{
kind: 'positional',
...half,
position: 2,
spoken: 'دو',
} as unknown as ToothIntent,
]);
expect(result.unresolved[0].reason).toBe('tooth_missing_quadrant');
}
});
it('still calls an out-of-range position out of range when the quadrant is missing', () => {
// Position wins: "nine" is wrong however completely it was said.
const result = resolveToothIntents([
{
kind: 'positional',
arch: null,
side: null,
position: 9,
spoken: 'نه',
} as unknown as ToothIntent,
]);
expect(result.unresolved[0].reason).toBe('position_out_of_range');
});
it('keeps unresolved items separate when the model omits the spoken span', () => {
// Without `spoken` these are indistinguishable; collapsing them would hide a lost tooth.
const result = resolveToothIntents([