Voice extraction needs quadrant mapping and adjacency server-side, and
treatment.utils.ts already held a private copy of the tooth set. Lift it into
common/fdi.ts rather than create a second source of truth; treatment.utils now
imports it, behaviour unchanged (existing suites still pass).
toFdi() is the single place the patient-right convention lives: quadrant 1 is
the patient's upper right, so upper+patient_right -> 1x, upper+patient_left ->
2x, lower+patient_left -> 3x, lower+patient_right -> 4x. Getting this backwards
mirrors every quadrant and yields a valid-looking code for the wrong tooth,
which no schema check can catch — so all four quadrants are pinned by tests,
along with out-of-range positions never being clamped and deciduous teeth being
rejected outright (the chart is permanent dentition only).
Adjacency mirrors the frontend's arch-order rule, so the midline pairs 11-21
and 41-31 count as neighbours exactly as the chart treats them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Voice extraction resolves spoken Jalali dates into ISO dates server-side, so
the backend needs the conversion the frontend already had. The resolvers live
here rather than in the frontend precisely because this half of the repo has a
test runner.
Ported from frontend/src/lib/i18n/persianCalendar.ts and verified faithful by
differential test: every day from 1900-2100 (73,414 days), zero mismatches on
conversion, leap years and month lengths.
Two deliberate divergences from the original:
- jalaliToIsoDate() returns null instead of throwing. It is fed model-supplied
values, which may be nonsense, and an invalid date must degrade to
"unresolved" rather than a 500. The year guard runs before jalaliDaysInMonth
so the throwing jalCal is unreachable from it.
- toLatinDigits() also handles the Arabic-Indic block (U+0660-U+0669), not just
Persian (U+06F0-U+06F9). ASR output can carry either, sometimes mixed with
ASCII in one transcript; the frontend version only parses keystrokes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Design spec for filling a TreatmentDetail by voice, settled across three
grilling sessions (30 decisions, logged in the spec).
Key shape:
- two-stage pipeline: OpenRouter whisper-1 -> gemini-3.7-flash
- the LLM emits *intents*, never FDI codes or ISO dates; pure Jest-tested
backend resolvers own quadrant mapping and Jalali conversion
- provider registry keyed by locale so fa can diverge from en/nl
- review sheet confirms before anything touches the form
- audio and transcripts are never persisted
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Logical API errors throw stable codes so users see translated messages instead of a generic bad request.
Co-authored-by: Cursor <cursoragent@cursor.com>