docs: record the FDI-first tooth rule in the voice spec

The spec's tooth section described the design that the first live test
disproved — descriptive phrasing primary, bare numerals refused in en.
Rewrites §6 around the rule the product actually wants, records the chip
affordance in §7, and closes open item §11.5: a two-digit number is FDI in
all three locales, with the Universal-numbering trade-off written down
rather than left implied.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-21 04:54:31 +08:00
parent 754efdee09
commit b7ee61433e

View File

@@ -1,7 +1,8 @@
# Voice treatment entry
**Status:** Implemented on `feat/voice-treatment-entry` — unreviewed, and blocked on the
ASR spike (§11 item 1) before it is trustworthy in front of patients
**Status:** Implemented on `feat/voice-treatment-entry`. First live test on 2026-08-21
sent the tooth path back for revision — a spoken number is now read as its FDI code (§6).
Still blocked on the ASR spike (§11 item 1) before it is trustworthy in front of patients
**Area:** Treatment workspace (CLINIC orgs)
**Created:** 2026-08-20
@@ -415,12 +416,20 @@ that justified this whole design.
words, so the resolver needs no per-locale branches. The locale-specific part is the
*prompt*: each enabled locale needs its own spoken tooth vocabulary (`شش بالا راست`,
`upper right six`, `rechtsboven zes`).
- **English carries a numbering hazard the other locales do not.** A clinician trained
under Universal numbering says "tooth number 14" and means a different tooth than FDI
14. `nl` is safe — the Netherlands uses FDI — but `en` is not. The `en` prompt must
therefore not accept a bare two-digit number as `explicitFdi` without the speaker
having made the notation explicit; ambiguous English numerals resolve to
**unresolved**. See §11.
- **A spoken tooth number is an FDI code, in every locale.** This is how clinicians
actually dictate — "بیست و شش" is tooth 26 — so the prompt *teaches* the notation
(first digit = quadrant from the patient's own point of view, second = position from
the midline) rather than refusing it. `arch`/`side`/`position` is the reading of a
tooth that was **described** instead of numbered, where a single digit is a position
and the quadrant comes from words. Revised after the first live test; the original
design had this backwards and made the descriptive form the only supported path.
- **A single digit alone is never resolved.** "دندون دو" names four teeth. It is reported
as `tooth_missing_quadrant` **with the candidate codes attached** — narrowed by whatever
*was* said, so "دو بالا" offers two — and the review sheet turns them into chips. The
clinician chooses; the resolver still never guesses.
- **Digits arrive in three scripts.** `normalizeFdiCode` (`common/fdi.ts`) folds Persian
and Arabic-Indic digits to ASCII and strips the spaces of a digit-by-digit dictation
before anything is matched, at both the wire branch choice and the final validation.
### `resolveDueDate()`
@@ -472,6 +481,10 @@ that justified this whole design.
- any row carrying an unresolved item or an incomplete prosthesis map.
- Unresolved items are shown with what was heard ("دندان شیری — بازشناسی نشد"), so the
clinician can see what the system did not understand.
- An item that carries `candidates` renders them as **tappable chips** — the one place the
sheet is interactive. Picking one folds the tooth into the result (`withChosenTeeth`) and
ticks the teeth row, so an under-specified tooth is one tap from resolved instead of a
dead end. Everything the sheet renders comes from that folded result, not the raw one.
- RTL-safe: logical `text-start` / `text-end` only, never `text-left`/`text-right`.
Dates via `lib/i18n/format.ts`.
@@ -607,12 +620,11 @@ enabling this for real clinics.
per-request one: re-verify it if the API key or the OpenRouter account changes, and
remember `whisper-1` is forwarded to OpenAI, so the effective policy is OpenRouter's
plus that provider's.
5. **English tooth numbering is unresolved as a product question.** Enabling `en` means
deciding what "tooth number 14" means when the speaker's notation is unknown —
Universal or FDI. The spec's current answer is to refuse ambiguous bare numerals in
`en`, which is safe but will feel broken to a US-trained clinician. Options are: refuse
(current), an org-level notation preference, or restricting `en` to quadrant-relative
phrasing. Decide before `en` ships to a real clinic; `fa` and `nl` are unaffected.
5. ~~**English tooth numbering**~~**resolved:** a bare two-digit number is read as
**FDI in all three locales**. FDI is what the product is built on and what clinicians
dictate. Known trade-off, accepted: a clinician trained under Universal numbering says
"tooth 14" and means a different tooth, so an `en` clinic needs either training or a
later per-org notation setting. Revisit if a US clinic is onboarded.
6. **`nl` and `en` have no spike data.** The Persian spike (item 1) should be repeated per
locale before that locale's mic is enabled for real users — same protocol, same
scoring, different speaker.
@@ -688,7 +700,7 @@ Settled in a grilling session on 2026-08-20.
| 1 | Scope | Everything including lab dispatch |
| 2 | AI supply chain | Domestic provider originally; OpenRouter for v1, registry keeps both open |
| 3 | Apply model | Review sheet, then apply |
| 4 | Speech → FDI | LLM emits intent, code resolves |
| 4 | Speech → FDI | LLM emits intent, code resolves. A spoken number **is** the FDI code (revised 2026-08-21, §6) |
| 5 | Cardinality | One detail per recording |
| 6 | Lab destination | Closed list of linked labs, explicit confirm, unticked when inexact |
| 7 | Due date | Intent + deterministic resolver |
@@ -704,7 +716,7 @@ Settled in a grilling session on 2026-08-20.
| 25 | Salvage target | Creates a new detail with only `comment` set — voice never writes into an existing detail |
| 26 | Throttle | Configurable; v1 default 6 requests / 60s per user |
| 27 | Duration cap | **2 minutes**, configurable via `maxMs` |
| 28 | Review sheet | Modal on desktop, full-screen overlay (not a route) on mobile |
| 28 | Review sheet | Modal on desktop, full-screen overlay (not a route) on mobile; candidate chips are its only interactive part |
| 29 | Cancel | Aborts the in-flight vendor call |
| 30 | v1 gating | Open to everyone; `Plan.features` gate deferred, not dropped |
| 15 | Gating | `Plan.features` flag — its first consumer |