feat(voice): write a note only when the clinician asks for one
The comment instruction told the model to sweep up leftovers: "clinical notes, in the language spoken. Omit the parts already captured as treatment type, teeth, prosthesis work or deadline." So filler, small talk and an unrequested diagnosis all became a persisted clinical note — and comment is the one durable trace a recording leaves (§10). The model now reports the exact words that asked for the note in a new commentTrigger field, and comment holds only what was dictated after them. resolveVoiceIntent keeps comment only when a trigger is present, so the rule is enforced by code instead of trusted to the prompt: a model that decides leftover speech was a note loses that note. Per-locale trigger vocabulary goes in the prompt's locale notes, beside the tooth vocabulary — «بنویس که», "write this in the notes", "noteer". The resolver only checks that a trigger was reported, so it needs no per-locale knowledge and stays locale-neutral, as §6 requires. Also adds HARD RULE 7: never decide something is a note; speech that fits no field is simply not reported. Four resolver tests. The two that drop an unasked note fail without the guard; the trigger-only and trigger-with-note cases pass either way and exist to prove the guard does not swallow a real note. Frontend untouched — it only ever saw the resolved comment, never the trigger. Spec: §5 gains the rule and the field, decision 54 added. Gates: backend 220 tests, nest build, ESLint clean on the voice module; frontend tsc --noEmit clean, 52 Vitest tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -411,7 +411,8 @@ type VoiceIntent = {
|
||||
treatmentType: string | null; // catalog code, from the supplied closed list
|
||||
teeth: ToothIntent[];
|
||||
connectedSpans: { from: ToothIntent; to: ToothIntent }[];
|
||||
comment: string | null;
|
||||
comment: string | null; // only when asked for — see below
|
||||
commentTrigger: string | null; // the words that asked; null discards the comment
|
||||
prosthesis: ProsthesisAssignment[]; // empty array, never null
|
||||
labId: string | null; // must be one of the supplied linked-lab ids
|
||||
labMatchExact: boolean;
|
||||
@@ -517,6 +518,25 @@ The `sub_*` key set is wider than the subcategory set — it also carries techni
|
||||
> the print layout, none of which this task otherwise opens. The seed is written from the message
|
||||
> files precisely so the two agree at the point they diverge.
|
||||
|
||||
### A note is written only when the clinician asks for one
|
||||
|
||||
The model does not decide that something was a note. It reports the exact words that asked —
|
||||
«بنویس که», "write this in the notes" — in `commentTrigger`, and `comment` holds what was
|
||||
dictated after them, without the asking words. The resolver keeps `comment` only when a trigger
|
||||
is present, so the rule is enforced by code rather than trusted to the prompt.
|
||||
|
||||
The instruction this replaces said "clinical notes, in the language spoken. Omit the parts
|
||||
already captured as treatment type, teeth, prosthesis work or deadline" — which told the model to
|
||||
sweep up whatever was left over. Filler, small talk and an unrequested diagnosis all became a
|
||||
persisted clinical note, and `comment` is the one durable trace of a recording (§10).
|
||||
|
||||
Speech that fits no field and was not asked to be a note is simply not reported. The sheet shows
|
||||
what was captured, so nothing is hidden by leaving it out.
|
||||
|
||||
Per-locale trigger vocabulary lives in the prompt's locale notes, exactly like the tooth
|
||||
vocabulary. The resolver only checks that a trigger was reported, so it needs no per-locale
|
||||
knowledge and stays locale-neutral by construction (§6).
|
||||
|
||||
### Prosthesis work implies the treatment type
|
||||
|
||||
`prosthesis` is the only `labDependent` treatment type. Any resolved assignment therefore
|
||||
@@ -1149,3 +1169,9 @@ Transcript handling revised on 2026-09-10.
|
||||
| 51 | Who sees the transcript | Nobody outside the server. It is absent from the success response and from every error body, and the review sheet does not render it — a raw dictation can carry the patient's spoken name, and what is not sent cannot leak through the network tab or an error reporter (§7, §10) |
|
||||
| 52 | Where it goes instead | One **info**-level server log line per recording, written before extraction so a failed extraction still records it, and outside `logTelemetry` so that method stays patient-free. Accepted consequence: patient words persist in production logs at default level; `debug` is a one-word change (§10) |
|
||||
| 53 | Transcript salvage | Dropped, not deferred. Reversing it needs a decision about the transcript leaving the server, not just client code. This supersedes decision 25 (§9) |
|
||||
|
||||
Notes made explicit on 2026-09-10.
|
||||
|
||||
| # | Question | Decision |
|
||||
|---|---|---|
|
||||
| 54 | What becomes a note | Only what the clinician explicitly asked to be written. The model reports the asking words in `commentTrigger`; the resolver discards `comment` without one, so a prompt drift cannot quietly persist unrequested speech as a clinical note (§5) |
|
||||
|
||||
Reference in New Issue
Block a user