From 562ef2ae6ea8245f4ccb72bc205c1565de3c1516 Mon Sep 17 00:00:00 2001 From: Amin Mousavi Date: Fri, 21 Aug 2026 18:54:40 +0800 Subject: [PATCH] docs: bring the voice spec in line with the flow as built MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The spec still described the flow as designed, not as it works after the first live recordings. §2 gains what confirm actually writes: the detail and its lab case are persisted on confirm, because the autosave effect watches `details` only and a lab draft left in component state loses the lab, the due date and the prosthesis map on reload — while the detail survives, which is what makes that loss look like a save. Plus the guard: a preview treatment comes back instead when any detail is still untyped, and confirm skips the lab-case save rather than posting an id the server has never seen. §9 corrects three codes: VOICE_MIC_DENIED is now only a real permission failure, VOICE_UNSUPPORTED_FORMAT covers every "this browser cannot record" path, and both it and VOICE_CLIP_TOO_LONG are named on their DTO constraints rather than falling through the shared map to an unrelated message. §8 no longer claims there is no duration cap — there is, 2 minutes, decided before implementation. §12 gains the checks these changes need, including the reload that catches an unsaved lab case, and decisions 31-33 record the three rules the live testing settled. Co-Authored-By: Claude Opus 5 (1M context) --- docs/specs/voice-treatment-entry/spec.md | 63 +++++++++++++++++++++--- 1 file changed, 57 insertions(+), 6 deletions(-) diff --git a/docs/specs/voice-treatment-entry/spec.md b/docs/specs/voice-treatment-entry/spec.md index 8c37d76..396b261 100644 --- a/docs/specs/voice-treatment-entry/spec.md +++ b/docs/specs/voice-treatment-entry/spec.md @@ -145,9 +145,25 @@ path**. `onAddDetail` is not called and not changed. `setEntryStep('treatment')` matters: `showChrome` is always on, so the control is visible during the **Lab** wizard step too. Confirming there returns to the treatment step. +**Confirm also saves.** The new detail is persisted immediately (`persistDraft({force:true})`), +and when the result carries a lab, a due date or a prosthesis map the lab case is saved with it +(`persistLabCases`). Not politeness — the autosave effect watches `details` only, so a lab draft +left in component state alone loses the destination lab, the due date and the whole prosthesis +map on the next reload. The detail survives, which is what makes that loss look like a +successful save. + +One guard on it: `persistDraft` returns a **preview** treatment instead of saving when any +detail still lacks a treatment type — the blank chip the workspace opens with is enough — and a +preview's detail id falls back to the client id. Confirm therefore checks *what came back*, not +the precondition, and skips the lab-case save when it did not get a real id; posting a lab case +against an id the server has never seen fails the whole save. Checking the result rather than +the condition keeps this true for every early return `persistDraft` has. + > Accepted consequences: > - Tapping Add and then 🎤 leaves behind the blank chip that Add created. It carries the > usual trash affordance. +> - That same blank chip blocks confirm's immediate lab-case save until it is given a type +> or removed; the lab rows stay in local state until the ordinary Lab-step save. > - Dictating into an existing detail is not supported in v1 — voice always makes a new > one. @@ -522,8 +538,8 @@ who can edit treatments, in every configured locale. `Plan.features.voiceTreatme and the availability API stay documented here as the intended gate, deferred rather than dropped, so turning them on later is additive. -Consequence to accept deliberately: with no plan gate and no duration cap (§2), the -per-user throttle is the **only** control on metered vendor spend. See open item 14. +Consequence to accept deliberately: with no plan gate, the per-user throttle and the 2-minute +recording cap are the **only** controls on metered vendor spend. See open item 14. --- @@ -535,15 +551,31 @@ English Nest exception for a user-facing failure. | Code | When | |---|---| -| `VOICE_MIC_DENIED` | browser permission refused — **client-side only**: needs the `errors.X` key in all three message files, but no `ErrorCode` entry and no throw site | -| `VOICE_CLIP_TOO_LONG` | over `maxMs` (server-side re-check), or over vendor limits | -| `VOICE_UNSUPPORTED_FORMAT` | recorder produced a container the profile rejects | +| `VOICE_MIC_DENIED` | microphone permission actually refused, or no input device — **client-side only**: needs the `errors.X` key in all three message files, but no `ErrorCode` entry and no throw site. Reserved for a real permission failure: see the note below | +| `VOICE_CLIP_TOO_LONG` | over `maxMs` (server-side re-check), over vendor limits, or a request body past the DTO's size cap | +| `VOICE_UNSUPPORTED_FORMAT` | **the browser cannot record at all** — no `MediaRecorder`, no container both it and the API accept, or a recorder that throws after permission was granted; and server-side, a `format` outside `VOICE_AUDIO_FORMATS` | | `VOICE_ASR_FAILED` | transcription stage failed | | `VOICE_EXTRACT_FAILED` | transcript obtained, structuring failed | | `VOICE_NOTHING_RECOGNIZED` | empty or unusable transcript | | `VOICE_NOT_AVAILABLE` | no profile for locale (v1); plan flag off, once enforced | | `VOICE_RATE_LIMITED` | throttle | +**Two of these are raised by DTO validation, not by a throw site.** +`validationExceptionFactory` returns a constraint's `message` verbatim when the message is +itself a known `ErrorCode`, so the voice DTO names its own failures: +`@MaxLength(…, { message: ErrorCode.VOICE_CLIP_TOO_LONG })` and +`@IsIn(…, { message: ErrorCode.VOICE_UNSUPPORTED_FORMAT })`. Left to the shared constraint map +they fall through to `VALIDATION_FIELD_REQUIRED` and `VALIDATION_LANGUAGE_INVALID` — an +oversized recording telling the clinician a field is missing, and an unsupported container +telling them their language is invalid. Any new voice constraint should name its code the same +way. + +**`VOICE_MIC_DENIED` is only for a real permission failure.** Three client paths used to report +it for something else entirely — no `MediaRecorder`, no acceptable container, and a recorder +that throws after permission was already granted. All three are "this browser cannot record" +and now report `VOICE_UNSUPPORTED_FORMAT`; blaming the microphone sends the clinician hunting +in site settings for a permission nothing ever asked for. + **Transcript salvage — specified, NOT built.** The backend half exists: `VOICE_EXTRACT_FAILED` carries `details.transcript` and `HttpExceptionFilter` forwards it. The client half was never written — `onError` only resolves a message through `getUserFacingError`, which never @@ -701,7 +733,18 @@ enabling this for real clinics. - no layout shift in the header row on record start, stop, or the 2:00 auto-stop; - **hold past 2:00** → auto-stops and proceeds to processing, not an error; - **cancel during processing** → the vendor request is actually aborted; - - **review sheet on mobile** → full-screen overlay; closing it leaves the draft intact. + - **review sheet on mobile** → full-screen overlay; closing it leaves the draft intact; + - **confirm with a lab, a due date or a prosthesis map, then reload** → all three are still + there. They live on the lab case, which the autosave effect does not watch, so this is + the check that catches a lab draft left unsaved in component state; + - **record straight after opening a visit**, while the blank chip is still untyped, and + confirm with a lab ticked → no error toast: confirm detects the preview treatment and + skips the lab-case save rather than posting an id the server has never seen; + - **dictate two different prosthesis types** ("۱۲ روکش PFM، ۱۳ روکش PFZ") → the form shows + both, and the dispatch panel's remembered "last type for this lab" does **not** overwrite + either. Also check the bulk «اعمال برای همه دندان‌ها» select stays on its placeholder; + - **a hand-made prosthesis detail with an empty map** → still gets the remembered default + pre-filled. The exemption is for previewed cases only, not a removal of the convenience. --- @@ -733,6 +776,14 @@ Settled in a grilling session on 2026-08-20. | 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 | + +Added while getting the first live recordings working (2026-08-21): + +| # | Question | Decision | +|---|---|---| +| 31 | Tooth numbering | A spoken number **is** its FDI code, in all three locales. A lone digit stays unresolved and offers its candidate teeth as chips (§6, §7) | +| 32 | What confirm writes | Confirm persists the detail *and* its lab case, because autosave watches `details` only — but skips the lab-case save when it got a preview treatment back (§2) | +| 33 | Preview as contract | Applying a voice result fills exactly what the sheet showed. Per-detail conveniences that would add more are suppressed for that case (§7) | | 15 | Gating | `Plan.features` flag — its first consumer | UI placement settled in a second grilling session on 2026-08-20.