From 4bf1bf43897fe13cb6e1fb3fdb4f5ad9be114c3f Mon Sep 17 00:00:00 2001 From: Amin Mousavi Date: Fri, 21 Aug 2026 04:54:31 +0800 Subject: [PATCH] docs: record the FDI-first tooth rule in the voice spec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- docs/specs/voice-treatment-entry/spec.md | 44 +++++++++++++++--------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/docs/specs/voice-treatment-entry/spec.md b/docs/specs/voice-treatment-entry/spec.md index 6e76713..70fd5e0 100644 --- a/docs/specs/voice-treatment-entry/spec.md +++ b/docs/specs/voice-treatment-entry/spec.md @@ -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 |