2026-08-21 04:42:09 +08:00
|
|
|
import { normalizeFdiCode } from '../../common/fdi';
|
2026-08-20 18:00:53 +03:30
|
|
|
import type {
|
|
|
|
|
ConnectedSpanIntent,
|
|
|
|
|
DueIntent,
|
feat(voice): adapt voice entry to the stacked-jobs prosthesis model
Authored by the /orchestrate builder agent, committed unrepaired so the
fixes that follow are reviewable against it.
Backend: replaces the flat prosthesisDefaultType/prosthesisOverrides wire
shape with a prosthesis: ProsthesisAssignment[] list whose targets can be a
tooth or a jaw; adds resolveAssignmentTarget / classifyTypeCode /
resolveProsthesisAssignment for leaf-vs-category classification, region
validity with mixed-region deferral, and assignmentIndex on unresolved
items; adds PROSTHESIS_CATEGORY and PROSTHESIS_SUBCATEGORY to
CatalogEntityKind with a migration and seeded fa/en/nl translations; and
rewrites the extraction prompt to render the catalog as a tree.
Frontend: merged "teeth and prosthesis" row, stack preview through the
existing applyLeafToJobs, three chip-fold paths, rewritten applyVoiceResult
and voiceForEditor, and the two carried-forward recording fixes — the
container fallback that refused Safari and the render gate that never
checked isMediaRecorderSupported().
Adds Vitest for the frontend's pure helpers, and updates CLAUDE.md.
Gate was green: backend 16 suites / 209 tests, nest build, prisma validate;
frontend 37 Vitest tests, tsc --noEmit, next build.
KNOWN DEFECTS, fixed in the commits that follow:
- VoiceReviewSheet.tsx:169 — a picked tooth chip is dropped on Apply
- VoiceReviewSheet.tsx:213 / TreatmentWorkspace.tsx:2215 — decision 41's
type-row lock is missing, so unticking it saves prosthesis lab rows on a
non-prosthesis detail
Reviewed on the correctness lens only; regression-risk never ran. The
migration was validated but never applied.
Spec: docs/specs/voice-treatment-entry/spec.md
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-07 12:23:58 +08:00
|
|
|
ProsthesisAssignment,
|
2026-08-20 18:00:53 +03:30
|
|
|
ToothIntent,
|
|
|
|
|
VoiceIntent,
|
|
|
|
|
Weekday,
|
|
|
|
|
} from './voice.types';
|
|
|
|
|
import { WEEKDAYS } from './voice.types';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Deliberately flat: strict `json_schema` mode has poor support for discriminated unions,
|
docs: cut the comments that were not earning their place
I wrote 731 comment lines on this branch against 4,530 lines of code — 14%,
where the rest of the repo runs at 1.8%. CLAUDE.md asks for code that reads
like its surroundings, and this did not.
Removed by genre rather than by taste:
- restating the code, e.g. "JS getUTCDay() numbering: Sunday = 0" above the
map that literally shows it, and a docblock on startOfWeek explaining that
it returns the start of the week;
- narrating history — "this used to rebuild the whole map", "left the bar
recording forever" — which the commit message and git blame already carry;
- saying the same thing in several places: the "cannot record is not a
denied microphone" reason appeared three times in one file, and the
"aborting stops a per-minute metered call" reason across three files. Each
now lives once, where the behaviour it explains lives;
- defending decisions nobody would question, like why toLatinDigits is its
own module;
- over-explaining defensive branches, three separate comments to distinguish
null from missing-kind from unrecognised-kind.
What stays is what the code cannot say: the patient-right convention in
toFdi, whose failure mode is a valid code for the wrong tooth; the
"this"-vs-"next" week anchoring; StrictMode re-arming mountedRef; Safari
accepting no mimeType hint; and the invariants whose violation already cost
a bug — the body parser's middleware ordering and the dispatch panel's
auto-fill rules.
Comments only. The diff contains no non-comment line.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 23:46:56 +08:00
|
|
|
* so every variant field is present and nullable. `toVoiceIntent` narrows it into the
|
|
|
|
|
* internal union and is total — anything it cannot classify becomes a shape the resolvers
|
|
|
|
|
* report as unresolved rather than something that throws here.
|
2026-08-20 18:00:53 +03:30
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
export type WireToothIntent = {
|
|
|
|
|
spoken: string;
|
2026-08-21 04:43:19 +08:00
|
|
|
/** The two-digit FDI code the clinician spoke; null when the tooth was described. */
|
2026-08-20 18:00:53 +03:30
|
|
|
fdi: string | null;
|
feat(voice): adapt voice entry to the stacked-jobs prosthesis model
Authored by the /orchestrate builder agent, committed unrepaired so the
fixes that follow are reviewable against it.
Backend: replaces the flat prosthesisDefaultType/prosthesisOverrides wire
shape with a prosthesis: ProsthesisAssignment[] list whose targets can be a
tooth or a jaw; adds resolveAssignmentTarget / classifyTypeCode /
resolveProsthesisAssignment for leaf-vs-category classification, region
validity with mixed-region deferral, and assignmentIndex on unresolved
items; adds PROSTHESIS_CATEGORY and PROSTHESIS_SUBCATEGORY to
CatalogEntityKind with a migration and seeded fa/en/nl translations; and
rewrites the extraction prompt to render the catalog as a tree.
Frontend: merged "teeth and prosthesis" row, stack preview through the
existing applyLeafToJobs, three chip-fold paths, rewritten applyVoiceResult
and voiceForEditor, and the two carried-forward recording fixes — the
container fallback that refused Safari and the render gate that never
checked isMediaRecorderSupported().
Adds Vitest for the frontend's pure helpers, and updates CLAUDE.md.
Gate was green: backend 16 suites / 209 tests, nest build, prisma validate;
frontend 37 Vitest tests, tsc --noEmit, next build.
KNOWN DEFECTS, fixed in the commits that follow:
- VoiceReviewSheet.tsx:169 — a picked tooth chip is dropped on Apply
- VoiceReviewSheet.tsx:213 / TreatmentWorkspace.tsx:2215 — decision 41's
type-row lock is missing, so unticking it saves prosthesis lab rows on a
non-prosthesis detail
Reviewed on the correctness lens only; regression-risk never ran. The
migration was validated but never applied.
Spec: docs/specs/voice-treatment-entry/spec.md
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-07 12:23:58 +08:00
|
|
|
arch: 'upper' | 'lower' | 'both' | null;
|
2026-08-20 18:00:53 +03:30
|
|
|
side: 'patient_right' | 'patient_left' | null;
|
|
|
|
|
position: number | null;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export type WireDue = {
|
|
|
|
|
kind: 'weekday' | 'offset' | 'jalali' | 'gregorian' | 'none';
|
|
|
|
|
weekday: Weekday | null;
|
|
|
|
|
which: 'this' | 'next' | null;
|
|
|
|
|
unit: 'day' | 'week' | 'month' | null;
|
|
|
|
|
amount: number | null;
|
|
|
|
|
jy: number | null;
|
|
|
|
|
jm: number | null;
|
|
|
|
|
jd: number | null;
|
|
|
|
|
y: number | null;
|
|
|
|
|
m: number | null;
|
|
|
|
|
d: number | null;
|
|
|
|
|
};
|
|
|
|
|
|
feat(voice): adapt voice entry to the stacked-jobs prosthesis model
Authored by the /orchestrate builder agent, committed unrepaired so the
fixes that follow are reviewable against it.
Backend: replaces the flat prosthesisDefaultType/prosthesisOverrides wire
shape with a prosthesis: ProsthesisAssignment[] list whose targets can be a
tooth or a jaw; adds resolveAssignmentTarget / classifyTypeCode /
resolveProsthesisAssignment for leaf-vs-category classification, region
validity with mixed-region deferral, and assignmentIndex on unresolved
items; adds PROSTHESIS_CATEGORY and PROSTHESIS_SUBCATEGORY to
CatalogEntityKind with a migration and seeded fa/en/nl translations; and
rewrites the extraction prompt to render the catalog as a tree.
Frontend: merged "teeth and prosthesis" row, stack preview through the
existing applyLeafToJobs, three chip-fold paths, rewritten applyVoiceResult
and voiceForEditor, and the two carried-forward recording fixes — the
container fallback that refused Safari and the render gate that never
checked isMediaRecorderSupported().
Adds Vitest for the frontend's pure helpers, and updates CLAUDE.md.
Gate was green: backend 16 suites / 209 tests, nest build, prisma validate;
frontend 37 Vitest tests, tsc --noEmit, next build.
KNOWN DEFECTS, fixed in the commits that follow:
- VoiceReviewSheet.tsx:169 — a picked tooth chip is dropped on Apply
- VoiceReviewSheet.tsx:213 / TreatmentWorkspace.tsx:2215 — decision 41's
type-row lock is missing, so unticking it saves prosthesis lab rows on a
non-prosthesis detail
Reviewed on the correctness lens only; regression-risk never ran. The
migration was validated but never applied.
Spec: docs/specs/voice-treatment-entry/spec.md
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-07 12:23:58 +08:00
|
|
|
export type WireProsthesisAssignment = {
|
|
|
|
|
targets: WireToothIntent[];
|
|
|
|
|
types: string[];
|
|
|
|
|
spoken: string;
|
|
|
|
|
};
|
|
|
|
|
|
2026-08-20 18:00:53 +03:30
|
|
|
export type WireVoiceIntent = {
|
|
|
|
|
treatmentType: string | null;
|
|
|
|
|
teeth: WireToothIntent[];
|
|
|
|
|
connectedSpans: { from: WireToothIntent; to: WireToothIntent }[];
|
|
|
|
|
comment: string | null;
|
feat(voice): adapt voice entry to the stacked-jobs prosthesis model
Authored by the /orchestrate builder agent, committed unrepaired so the
fixes that follow are reviewable against it.
Backend: replaces the flat prosthesisDefaultType/prosthesisOverrides wire
shape with a prosthesis: ProsthesisAssignment[] list whose targets can be a
tooth or a jaw; adds resolveAssignmentTarget / classifyTypeCode /
resolveProsthesisAssignment for leaf-vs-category classification, region
validity with mixed-region deferral, and assignmentIndex on unresolved
items; adds PROSTHESIS_CATEGORY and PROSTHESIS_SUBCATEGORY to
CatalogEntityKind with a migration and seeded fa/en/nl translations; and
rewrites the extraction prompt to render the catalog as a tree.
Frontend: merged "teeth and prosthesis" row, stack preview through the
existing applyLeafToJobs, three chip-fold paths, rewritten applyVoiceResult
and voiceForEditor, and the two carried-forward recording fixes — the
container fallback that refused Safari and the render gate that never
checked isMediaRecorderSupported().
Adds Vitest for the frontend's pure helpers, and updates CLAUDE.md.
Gate was green: backend 16 suites / 209 tests, nest build, prisma validate;
frontend 37 Vitest tests, tsc --noEmit, next build.
KNOWN DEFECTS, fixed in the commits that follow:
- VoiceReviewSheet.tsx:169 — a picked tooth chip is dropped on Apply
- VoiceReviewSheet.tsx:213 / TreatmentWorkspace.tsx:2215 — decision 41's
type-row lock is missing, so unticking it saves prosthesis lab rows on a
non-prosthesis detail
Reviewed on the correctness lens only; regression-risk never ran. The
migration was validated but never applied.
Spec: docs/specs/voice-treatment-entry/spec.md
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-07 12:23:58 +08:00
|
|
|
prosthesis: WireProsthesisAssignment[];
|
2026-08-20 18:00:53 +03:30
|
|
|
labId: string | null;
|
|
|
|
|
labMatchExact: boolean;
|
|
|
|
|
due: WireDue;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const TOOTH_SCHEMA = {
|
|
|
|
|
type: 'object',
|
|
|
|
|
additionalProperties: false,
|
|
|
|
|
required: ['spoken', 'fdi', 'arch', 'side', 'position'],
|
|
|
|
|
properties: {
|
|
|
|
|
spoken: {
|
|
|
|
|
type: 'string',
|
feat(voice): adapt voice entry to the stacked-jobs prosthesis model
Authored by the /orchestrate builder agent, committed unrepaired so the
fixes that follow are reviewable against it.
Backend: replaces the flat prosthesisDefaultType/prosthesisOverrides wire
shape with a prosthesis: ProsthesisAssignment[] list whose targets can be a
tooth or a jaw; adds resolveAssignmentTarget / classifyTypeCode /
resolveProsthesisAssignment for leaf-vs-category classification, region
validity with mixed-region deferral, and assignmentIndex on unresolved
items; adds PROSTHESIS_CATEGORY and PROSTHESIS_SUBCATEGORY to
CatalogEntityKind with a migration and seeded fa/en/nl translations; and
rewrites the extraction prompt to render the catalog as a tree.
Frontend: merged "teeth and prosthesis" row, stack preview through the
existing applyLeafToJobs, three chip-fold paths, rewritten applyVoiceResult
and voiceForEditor, and the two carried-forward recording fixes — the
container fallback that refused Safari and the render gate that never
checked isMediaRecorderSupported().
Adds Vitest for the frontend's pure helpers, and updates CLAUDE.md.
Gate was green: backend 16 suites / 209 tests, nest build, prisma validate;
frontend 37 Vitest tests, tsc --noEmit, next build.
KNOWN DEFECTS, fixed in the commits that follow:
- VoiceReviewSheet.tsx:169 — a picked tooth chip is dropped on Apply
- VoiceReviewSheet.tsx:213 / TreatmentWorkspace.tsx:2215 — decision 41's
type-row lock is missing, so unticking it saves prosthesis lab rows on a
non-prosthesis detail
Reviewed on the correctness lens only; regression-risk never ran. The
migration was validated but never applied.
Spec: docs/specs/voice-treatment-entry/spec.md
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-07 12:23:58 +08:00
|
|
|
description: 'The exact transcript words for this tooth (or jaw).',
|
2026-08-20 18:00:53 +03:30
|
|
|
},
|
|
|
|
|
fdi: {
|
|
|
|
|
type: ['string', 'null'],
|
|
|
|
|
description:
|
2026-08-21 04:43:19 +08:00
|
|
|
'The two-digit FDI code the clinician said for this tooth, e.g. "26". Null only ' +
|
feat(voice): adapt voice entry to the stacked-jobs prosthesis model
Authored by the /orchestrate builder agent, committed unrepaired so the
fixes that follow are reviewable against it.
Backend: replaces the flat prosthesisDefaultType/prosthesisOverrides wire
shape with a prosthesis: ProsthesisAssignment[] list whose targets can be a
tooth or a jaw; adds resolveAssignmentTarget / classifyTypeCode /
resolveProsthesisAssignment for leaf-vs-category classification, region
validity with mixed-region deferral, and assignmentIndex on unresolved
items; adds PROSTHESIS_CATEGORY and PROSTHESIS_SUBCATEGORY to
CatalogEntityKind with a migration and seeded fa/en/nl translations; and
rewrites the extraction prompt to render the catalog as a tree.
Frontend: merged "teeth and prosthesis" row, stack preview through the
existing applyLeafToJobs, three chip-fold paths, rewritten applyVoiceResult
and voiceForEditor, and the two carried-forward recording fixes — the
container fallback that refused Safari and the render gate that never
checked isMediaRecorderSupported().
Adds Vitest for the frontend's pure helpers, and updates CLAUDE.md.
Gate was green: backend 16 suites / 209 tests, nest build, prisma validate;
frontend 37 Vitest tests, tsc --noEmit, next build.
KNOWN DEFECTS, fixed in the commits that follow:
- VoiceReviewSheet.tsx:169 — a picked tooth chip is dropped on Apply
- VoiceReviewSheet.tsx:213 / TreatmentWorkspace.tsx:2215 — decision 41's
type-row lock is missing, so unticking it saves prosthesis lab rows on a
non-prosthesis detail
Reviewed on the correctness lens only; regression-risk never ran. The
migration was validated but never applied.
Spec: docs/specs/voice-treatment-entry/spec.md
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-07 12:23:58 +08:00
|
|
|
'when the tooth was described in words instead of numbered, or the target is a jaw.',
|
|
|
|
|
},
|
|
|
|
|
arch: {
|
|
|
|
|
type: ['string', 'null'],
|
|
|
|
|
enum: ['upper', 'lower', 'both', null],
|
|
|
|
|
description:
|
|
|
|
|
'"both" is only ever used for a jaw-level prosthesis target (e.g. an appliance for ' +
|
|
|
|
|
'both jaws), never for a single tooth.',
|
2026-08-20 18:00:53 +03:30
|
|
|
},
|
|
|
|
|
side: {
|
|
|
|
|
type: ['string', 'null'],
|
|
|
|
|
enum: ['patient_right', 'patient_left', null],
|
|
|
|
|
description: "The PATIENT's side, never the viewer's.",
|
|
|
|
|
},
|
|
|
|
|
position: {
|
|
|
|
|
type: ['integer', 'null'],
|
2026-08-21 04:43:19 +08:00
|
|
|
description:
|
feat(voice): adapt voice entry to the stacked-jobs prosthesis model
Authored by the /orchestrate builder agent, committed unrepaired so the
fixes that follow are reviewable against it.
Backend: replaces the flat prosthesisDefaultType/prosthesisOverrides wire
shape with a prosthesis: ProsthesisAssignment[] list whose targets can be a
tooth or a jaw; adds resolveAssignmentTarget / classifyTypeCode /
resolveProsthesisAssignment for leaf-vs-category classification, region
validity with mixed-region deferral, and assignmentIndex on unresolved
items; adds PROSTHESIS_CATEGORY and PROSTHESIS_SUBCATEGORY to
CatalogEntityKind with a migration and seeded fa/en/nl translations; and
rewrites the extraction prompt to render the catalog as a tree.
Frontend: merged "teeth and prosthesis" row, stack preview through the
existing applyLeafToJobs, three chip-fold paths, rewritten applyVoiceResult
and voiceForEditor, and the two carried-forward recording fixes — the
container fallback that refused Safari and the render gate that never
checked isMediaRecorderSupported().
Adds Vitest for the frontend's pure helpers, and updates CLAUDE.md.
Gate was green: backend 16 suites / 209 tests, nest build, prisma validate;
frontend 37 Vitest tests, tsc --noEmit, next build.
KNOWN DEFECTS, fixed in the commits that follow:
- VoiceReviewSheet.tsx:169 — a picked tooth chip is dropped on Apply
- VoiceReviewSheet.tsx:213 / TreatmentWorkspace.tsx:2215 — decision 41's
type-row lock is missing, so unticking it saves prosthesis lab rows on a
non-prosthesis detail
Reviewed on the correctness lens only; regression-risk never ran. The
migration was validated but never applied.
Spec: docs/specs/voice-treatment-entry/spec.md
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-07 12:23:58 +08:00
|
|
|
'Position from the midline: 1 = central incisor … 8 = third molar. Never an FDI ' +
|
|
|
|
|
'code. Null when this target is a jaw rather than a tooth.',
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
} as const;
|
|
|
|
|
|
|
|
|
|
const PROSTHESIS_ASSIGNMENT_SCHEMA = {
|
|
|
|
|
type: 'object',
|
|
|
|
|
additionalProperties: false,
|
|
|
|
|
required: ['targets', 'types', 'spoken'],
|
|
|
|
|
properties: {
|
|
|
|
|
targets: {
|
|
|
|
|
type: 'array',
|
|
|
|
|
description: 'The teeth or jaws this instruction applies to.',
|
|
|
|
|
items: TOOTH_SCHEMA,
|
|
|
|
|
},
|
|
|
|
|
types: {
|
|
|
|
|
type: 'array',
|
|
|
|
|
description:
|
|
|
|
|
'Prosthesis type codes to apply to every target above — a stack, e.g. an abutment ' +
|
|
|
|
|
'plus a crown on the same tooth. A code from the CATEGORY or SUBCATEGORY lists is ' +
|
|
|
|
|
'fine when only the general term was said.',
|
|
|
|
|
items: { type: 'string' },
|
|
|
|
|
},
|
|
|
|
|
spoken: {
|
|
|
|
|
type: 'string',
|
|
|
|
|
description: 'The exact transcript words for this instruction.',
|
2026-08-20 18:00:53 +03:30
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
} as const;
|
|
|
|
|
|
|
|
|
|
export const VOICE_INTENT_JSON_SCHEMA = {
|
|
|
|
|
type: 'object',
|
|
|
|
|
additionalProperties: false,
|
|
|
|
|
required: [
|
|
|
|
|
'treatmentType',
|
|
|
|
|
'teeth',
|
|
|
|
|
'connectedSpans',
|
|
|
|
|
'comment',
|
feat(voice): adapt voice entry to the stacked-jobs prosthesis model
Authored by the /orchestrate builder agent, committed unrepaired so the
fixes that follow are reviewable against it.
Backend: replaces the flat prosthesisDefaultType/prosthesisOverrides wire
shape with a prosthesis: ProsthesisAssignment[] list whose targets can be a
tooth or a jaw; adds resolveAssignmentTarget / classifyTypeCode /
resolveProsthesisAssignment for leaf-vs-category classification, region
validity with mixed-region deferral, and assignmentIndex on unresolved
items; adds PROSTHESIS_CATEGORY and PROSTHESIS_SUBCATEGORY to
CatalogEntityKind with a migration and seeded fa/en/nl translations; and
rewrites the extraction prompt to render the catalog as a tree.
Frontend: merged "teeth and prosthesis" row, stack preview through the
existing applyLeafToJobs, three chip-fold paths, rewritten applyVoiceResult
and voiceForEditor, and the two carried-forward recording fixes — the
container fallback that refused Safari and the render gate that never
checked isMediaRecorderSupported().
Adds Vitest for the frontend's pure helpers, and updates CLAUDE.md.
Gate was green: backend 16 suites / 209 tests, nest build, prisma validate;
frontend 37 Vitest tests, tsc --noEmit, next build.
KNOWN DEFECTS, fixed in the commits that follow:
- VoiceReviewSheet.tsx:169 — a picked tooth chip is dropped on Apply
- VoiceReviewSheet.tsx:213 / TreatmentWorkspace.tsx:2215 — decision 41's
type-row lock is missing, so unticking it saves prosthesis lab rows on a
non-prosthesis detail
Reviewed on the correctness lens only; regression-risk never ran. The
migration was validated but never applied.
Spec: docs/specs/voice-treatment-entry/spec.md
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-07 12:23:58 +08:00
|
|
|
'prosthesis',
|
2026-08-20 18:00:53 +03:30
|
|
|
'labId',
|
|
|
|
|
'labMatchExact',
|
|
|
|
|
'due',
|
|
|
|
|
],
|
|
|
|
|
properties: {
|
|
|
|
|
treatmentType: {
|
|
|
|
|
type: ['string', 'null'],
|
|
|
|
|
description: 'A treatment type CODE from the supplied list, or null.',
|
|
|
|
|
},
|
2026-09-10 16:56:54 +08:00
|
|
|
teeth: {
|
|
|
|
|
type: 'array',
|
|
|
|
|
description:
|
|
|
|
|
'Individual teeth only. A whole jaw NEVER belongs here — put it in ' +
|
|
|
|
|
'prosthesis[].targets with "arch" set and "position" null.',
|
|
|
|
|
items: TOOTH_SCHEMA,
|
|
|
|
|
},
|
2026-08-20 18:00:53 +03:30
|
|
|
connectedSpans: {
|
|
|
|
|
type: 'array',
|
|
|
|
|
description: 'Bridges / splinted units. Endpoints inclusive.',
|
|
|
|
|
items: {
|
|
|
|
|
type: 'object',
|
|
|
|
|
additionalProperties: false,
|
|
|
|
|
required: ['from', 'to'],
|
|
|
|
|
properties: { from: TOOTH_SCHEMA, to: TOOTH_SCHEMA },
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
comment: {
|
|
|
|
|
type: ['string', 'null'],
|
|
|
|
|
description: 'Clinical notes, in the spoken language.',
|
|
|
|
|
},
|
feat(voice): adapt voice entry to the stacked-jobs prosthesis model
Authored by the /orchestrate builder agent, committed unrepaired so the
fixes that follow are reviewable against it.
Backend: replaces the flat prosthesisDefaultType/prosthesisOverrides wire
shape with a prosthesis: ProsthesisAssignment[] list whose targets can be a
tooth or a jaw; adds resolveAssignmentTarget / classifyTypeCode /
resolveProsthesisAssignment for leaf-vs-category classification, region
validity with mixed-region deferral, and assignmentIndex on unresolved
items; adds PROSTHESIS_CATEGORY and PROSTHESIS_SUBCATEGORY to
CatalogEntityKind with a migration and seeded fa/en/nl translations; and
rewrites the extraction prompt to render the catalog as a tree.
Frontend: merged "teeth and prosthesis" row, stack preview through the
existing applyLeafToJobs, three chip-fold paths, rewritten applyVoiceResult
and voiceForEditor, and the two carried-forward recording fixes — the
container fallback that refused Safari and the render gate that never
checked isMediaRecorderSupported().
Adds Vitest for the frontend's pure helpers, and updates CLAUDE.md.
Gate was green: backend 16 suites / 209 tests, nest build, prisma validate;
frontend 37 Vitest tests, tsc --noEmit, next build.
KNOWN DEFECTS, fixed in the commits that follow:
- VoiceReviewSheet.tsx:169 — a picked tooth chip is dropped on Apply
- VoiceReviewSheet.tsx:213 / TreatmentWorkspace.tsx:2215 — decision 41's
type-row lock is missing, so unticking it saves prosthesis lab rows on a
non-prosthesis detail
Reviewed on the correctness lens only; regression-risk never ran. The
migration was validated but never applied.
Spec: docs/specs/voice-treatment-entry/spec.md
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-07 12:23:58 +08:00
|
|
|
prosthesis: {
|
2026-08-20 18:00:53 +03:30
|
|
|
type: 'array',
|
feat(voice): adapt voice entry to the stacked-jobs prosthesis model
Authored by the /orchestrate builder agent, committed unrepaired so the
fixes that follow are reviewable against it.
Backend: replaces the flat prosthesisDefaultType/prosthesisOverrides wire
shape with a prosthesis: ProsthesisAssignment[] list whose targets can be a
tooth or a jaw; adds resolveAssignmentTarget / classifyTypeCode /
resolveProsthesisAssignment for leaf-vs-category classification, region
validity with mixed-region deferral, and assignmentIndex on unresolved
items; adds PROSTHESIS_CATEGORY and PROSTHESIS_SUBCATEGORY to
CatalogEntityKind with a migration and seeded fa/en/nl translations; and
rewrites the extraction prompt to render the catalog as a tree.
Frontend: merged "teeth and prosthesis" row, stack preview through the
existing applyLeafToJobs, three chip-fold paths, rewritten applyVoiceResult
and voiceForEditor, and the two carried-forward recording fixes — the
container fallback that refused Safari and the render gate that never
checked isMediaRecorderSupported().
Adds Vitest for the frontend's pure helpers, and updates CLAUDE.md.
Gate was green: backend 16 suites / 209 tests, nest build, prisma validate;
frontend 37 Vitest tests, tsc --noEmit, next build.
KNOWN DEFECTS, fixed in the commits that follow:
- VoiceReviewSheet.tsx:169 — a picked tooth chip is dropped on Apply
- VoiceReviewSheet.tsx:213 / TreatmentWorkspace.tsx:2215 — decision 41's
type-row lock is missing, so unticking it saves prosthesis lab rows on a
non-prosthesis detail
Reviewed on the correctness lens only; regression-risk never ran. The
migration was validated but never applied.
Spec: docs/specs/voice-treatment-entry/spec.md
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-07 12:23:58 +08:00
|
|
|
description:
|
|
|
|
|
'One entry per spoken instruction: these targets get these jobs. No default and no ' +
|
|
|
|
|
'overrides — every entry names its own targets.',
|
|
|
|
|
items: PROSTHESIS_ASSIGNMENT_SCHEMA,
|
2026-08-20 18:00:53 +03:30
|
|
|
},
|
|
|
|
|
labId: {
|
|
|
|
|
type: ['string', 'null'],
|
|
|
|
|
description: 'An id from the supplied lab list. Never invent one.',
|
|
|
|
|
},
|
|
|
|
|
labMatchExact: {
|
|
|
|
|
type: 'boolean',
|
|
|
|
|
description: 'True only when the spoken name matched a lab name exactly.',
|
|
|
|
|
},
|
|
|
|
|
due: {
|
|
|
|
|
type: 'object',
|
|
|
|
|
additionalProperties: false,
|
|
|
|
|
required: [
|
|
|
|
|
'kind',
|
|
|
|
|
'weekday',
|
|
|
|
|
'which',
|
|
|
|
|
'unit',
|
|
|
|
|
'amount',
|
|
|
|
|
'jy',
|
|
|
|
|
'jm',
|
|
|
|
|
'jd',
|
|
|
|
|
'y',
|
|
|
|
|
'm',
|
|
|
|
|
'd',
|
|
|
|
|
],
|
|
|
|
|
properties: {
|
|
|
|
|
kind: {
|
|
|
|
|
type: 'string',
|
|
|
|
|
enum: ['weekday', 'offset', 'jalali', 'gregorian', 'none'],
|
|
|
|
|
},
|
|
|
|
|
weekday: { type: ['string', 'null'], enum: [...WEEKDAYS, null] },
|
|
|
|
|
which: { type: ['string', 'null'], enum: ['this', 'next', null] },
|
|
|
|
|
unit: {
|
|
|
|
|
type: ['string', 'null'],
|
|
|
|
|
enum: ['day', 'week', 'month', null],
|
|
|
|
|
},
|
|
|
|
|
amount: { type: ['integer', 'null'] },
|
|
|
|
|
jy: { type: ['integer', 'null'] },
|
|
|
|
|
jm: { type: ['integer', 'null'] },
|
|
|
|
|
jd: { type: ['integer', 'null'] },
|
|
|
|
|
y: { type: ['integer', 'null'] },
|
|
|
|
|
m: { type: ['integer', 'null'] },
|
|
|
|
|
d: { type: ['integer', 'null'] },
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
} as const;
|
|
|
|
|
|
|
|
|
|
/** Two digits, quadrant 1-8, position 1-8 — the only thing that can be an FDI code. */
|
|
|
|
|
const FDI_SHAPE = /^[1-8][1-8]$/;
|
|
|
|
|
|
|
|
|
|
function toToothIntent(wire: WireToothIntent | undefined | null): ToothIntent {
|
|
|
|
|
const spoken = typeof wire?.spoken === 'string' ? wire.spoken : '';
|
docs: cut the comments that were not earning their place
I wrote 731 comment lines on this branch against 4,530 lines of code — 14%,
where the rest of the repo runs at 1.8%. CLAUDE.md asks for code that reads
like its surroundings, and this did not.
Removed by genre rather than by taste:
- restating the code, e.g. "JS getUTCDay() numbering: Sunday = 0" above the
map that literally shows it, and a docblock on startOfWeek explaining that
it returns the start of the week;
- narrating history — "this used to rebuild the whole map", "left the bar
recording forever" — which the commit message and git blame already carry;
- saying the same thing in several places: the "cannot record is not a
denied microphone" reason appeared three times in one file, and the
"aborting stops a per-minute metered call" reason across three files. Each
now lives once, where the behaviour it explains lives;
- defending decisions nobody would question, like why toLatinDigits is its
own module;
- over-explaining defensive branches, three separate comments to distinguish
null from missing-kind from unrecognised-kind.
What stays is what the code cannot say: the patient-right convention in
toFdi, whose failure mode is a valid code for the wrong tooth; the
"this"-vs-"next" week anchoring; StrictMode re-arming mountedRef; Safari
accepting no mimeType hint; and the invariants whose violation already cost
a bug — the body parser's middleware ordering and the dispatch panel's
auto-fill rules.
Comments only. The diff contains no non-comment line.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 23:46:56 +08:00
|
|
|
// "۲۶" and "2 6" are FDI codes that do not match literally; unnormalised they fall
|
|
|
|
|
// through to the positional branch with no quadrant and read as unresolved.
|
2026-08-21 04:42:09 +08:00
|
|
|
const fdi = normalizeFdiCode(wire?.fdi);
|
2026-08-20 18:00:53 +03:30
|
|
|
// Only take the explicit branch for something actually FDI-shaped. A model that emits
|
|
|
|
|
// fdi:"6" alongside correct arch/side/position would otherwise lose the tooth entirely.
|
|
|
|
|
if (FDI_SHAPE.test(fdi)) {
|
|
|
|
|
return { kind: 'explicit', fdi, spoken };
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
kind: 'positional',
|
feat(voice): adapt voice entry to the stacked-jobs prosthesis model
Authored by the /orchestrate builder agent, committed unrepaired so the
fixes that follow are reviewable against it.
Backend: replaces the flat prosthesisDefaultType/prosthesisOverrides wire
shape with a prosthesis: ProsthesisAssignment[] list whose targets can be a
tooth or a jaw; adds resolveAssignmentTarget / classifyTypeCode /
resolveProsthesisAssignment for leaf-vs-category classification, region
validity with mixed-region deferral, and assignmentIndex on unresolved
items; adds PROSTHESIS_CATEGORY and PROSTHESIS_SUBCATEGORY to
CatalogEntityKind with a migration and seeded fa/en/nl translations; and
rewrites the extraction prompt to render the catalog as a tree.
Frontend: merged "teeth and prosthesis" row, stack preview through the
existing applyLeafToJobs, three chip-fold paths, rewritten applyVoiceResult
and voiceForEditor, and the two carried-forward recording fixes — the
container fallback that refused Safari and the render gate that never
checked isMediaRecorderSupported().
Adds Vitest for the frontend's pure helpers, and updates CLAUDE.md.
Gate was green: backend 16 suites / 209 tests, nest build, prisma validate;
frontend 37 Vitest tests, tsc --noEmit, next build.
KNOWN DEFECTS, fixed in the commits that follow:
- VoiceReviewSheet.tsx:169 — a picked tooth chip is dropped on Apply
- VoiceReviewSheet.tsx:213 / TreatmentWorkspace.tsx:2215 — decision 41's
type-row lock is missing, so unticking it saves prosthesis lab rows on a
non-prosthesis detail
Reviewed on the correctness lens only; regression-risk never ran. The
migration was validated but never applied.
Spec: docs/specs/voice-treatment-entry/spec.md
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-07 12:23:58 +08:00
|
|
|
arch: wire?.arch as 'upper' | 'lower' | 'both',
|
2026-08-20 18:00:53 +03:30
|
|
|
side: wire?.side as 'patient_right' | 'patient_left',
|
|
|
|
|
position: typeof wire?.position === 'number' ? wire.position : Number.NaN,
|
|
|
|
|
spoken,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toDueIntent(wire: WireDue | undefined | null): DueIntent | null {
|
|
|
|
|
switch (wire?.kind) {
|
|
|
|
|
case 'weekday':
|
|
|
|
|
return {
|
|
|
|
|
kind: 'weekday',
|
|
|
|
|
weekday: wire.weekday as Weekday,
|
|
|
|
|
which: wire.which as 'this',
|
|
|
|
|
};
|
|
|
|
|
case 'offset':
|
|
|
|
|
return {
|
|
|
|
|
kind: 'offset',
|
|
|
|
|
unit: wire.unit as 'day',
|
|
|
|
|
amount: typeof wire.amount === 'number' ? wire.amount : Number.NaN,
|
|
|
|
|
};
|
|
|
|
|
case 'jalali':
|
|
|
|
|
return {
|
|
|
|
|
kind: 'jalali',
|
|
|
|
|
jy: wire.jy as number,
|
|
|
|
|
jm: wire.jm as number,
|
|
|
|
|
jd: wire.jd as number,
|
|
|
|
|
};
|
|
|
|
|
case 'gregorian':
|
|
|
|
|
return {
|
|
|
|
|
kind: 'gregorian',
|
|
|
|
|
y: wire.y as number,
|
|
|
|
|
m: wire.m as number,
|
|
|
|
|
d: wire.d as number,
|
|
|
|
|
};
|
|
|
|
|
case 'none':
|
|
|
|
|
case undefined:
|
|
|
|
|
return null;
|
|
|
|
|
default:
|
|
|
|
|
// An unrecognised kind means a deadline WAS spoken and we failed to classify it.
|
|
|
|
|
// Passing it through lets the resolver flag it; collapsing it to null would make a
|
|
|
|
|
// misunderstood deadline indistinguishable from no deadline at all.
|
|
|
|
|
return { kind: wire?.kind } as unknown as DueIntent;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
feat(voice): adapt voice entry to the stacked-jobs prosthesis model
Authored by the /orchestrate builder agent, committed unrepaired so the
fixes that follow are reviewable against it.
Backend: replaces the flat prosthesisDefaultType/prosthesisOverrides wire
shape with a prosthesis: ProsthesisAssignment[] list whose targets can be a
tooth or a jaw; adds resolveAssignmentTarget / classifyTypeCode /
resolveProsthesisAssignment for leaf-vs-category classification, region
validity with mixed-region deferral, and assignmentIndex on unresolved
items; adds PROSTHESIS_CATEGORY and PROSTHESIS_SUBCATEGORY to
CatalogEntityKind with a migration and seeded fa/en/nl translations; and
rewrites the extraction prompt to render the catalog as a tree.
Frontend: merged "teeth and prosthesis" row, stack preview through the
existing applyLeafToJobs, three chip-fold paths, rewritten applyVoiceResult
and voiceForEditor, and the two carried-forward recording fixes — the
container fallback that refused Safari and the render gate that never
checked isMediaRecorderSupported().
Adds Vitest for the frontend's pure helpers, and updates CLAUDE.md.
Gate was green: backend 16 suites / 209 tests, nest build, prisma validate;
frontend 37 Vitest tests, tsc --noEmit, next build.
KNOWN DEFECTS, fixed in the commits that follow:
- VoiceReviewSheet.tsx:169 — a picked tooth chip is dropped on Apply
- VoiceReviewSheet.tsx:213 / TreatmentWorkspace.tsx:2215 — decision 41's
type-row lock is missing, so unticking it saves prosthesis lab rows on a
non-prosthesis detail
Reviewed on the correctness lens only; regression-risk never ran. The
migration was validated but never applied.
Spec: docs/specs/voice-treatment-entry/spec.md
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-07 12:23:58 +08:00
|
|
|
function toProsthesisAssignment(
|
|
|
|
|
wire: WireProsthesisAssignment | undefined | null,
|
|
|
|
|
): ProsthesisAssignment {
|
|
|
|
|
const targets = Array.isArray(wire?.targets) ? wire.targets : [];
|
|
|
|
|
const types = Array.isArray(wire?.types) ? wire.types : [];
|
|
|
|
|
return {
|
|
|
|
|
targets: targets.map(toToothIntent),
|
|
|
|
|
types: types.filter((code): code is string => typeof code === 'string'),
|
|
|
|
|
spoken: typeof wire?.spoken === 'string' ? wire.spoken : '',
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-20 18:00:53 +03:30
|
|
|
export function toVoiceIntent(wire: WireVoiceIntent): VoiceIntent {
|
|
|
|
|
const teeth = Array.isArray(wire?.teeth) ? wire.teeth : [];
|
|
|
|
|
const spans = Array.isArray(wire?.connectedSpans) ? wire.connectedSpans : [];
|
feat(voice): adapt voice entry to the stacked-jobs prosthesis model
Authored by the /orchestrate builder agent, committed unrepaired so the
fixes that follow are reviewable against it.
Backend: replaces the flat prosthesisDefaultType/prosthesisOverrides wire
shape with a prosthesis: ProsthesisAssignment[] list whose targets can be a
tooth or a jaw; adds resolveAssignmentTarget / classifyTypeCode /
resolveProsthesisAssignment for leaf-vs-category classification, region
validity with mixed-region deferral, and assignmentIndex on unresolved
items; adds PROSTHESIS_CATEGORY and PROSTHESIS_SUBCATEGORY to
CatalogEntityKind with a migration and seeded fa/en/nl translations; and
rewrites the extraction prompt to render the catalog as a tree.
Frontend: merged "teeth and prosthesis" row, stack preview through the
existing applyLeafToJobs, three chip-fold paths, rewritten applyVoiceResult
and voiceForEditor, and the two carried-forward recording fixes — the
container fallback that refused Safari and the render gate that never
checked isMediaRecorderSupported().
Adds Vitest for the frontend's pure helpers, and updates CLAUDE.md.
Gate was green: backend 16 suites / 209 tests, nest build, prisma validate;
frontend 37 Vitest tests, tsc --noEmit, next build.
KNOWN DEFECTS, fixed in the commits that follow:
- VoiceReviewSheet.tsx:169 — a picked tooth chip is dropped on Apply
- VoiceReviewSheet.tsx:213 / TreatmentWorkspace.tsx:2215 — decision 41's
type-row lock is missing, so unticking it saves prosthesis lab rows on a
non-prosthesis detail
Reviewed on the correctness lens only; regression-risk never ran. The
migration was validated but never applied.
Spec: docs/specs/voice-treatment-entry/spec.md
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-07 12:23:58 +08:00
|
|
|
const assignments = Array.isArray(wire?.prosthesis) ? wire.prosthesis : [];
|
2026-08-20 18:00:53 +03:30
|
|
|
|
|
|
|
|
const connectedSpans: ConnectedSpanIntent[] = spans.map((span) => ({
|
|
|
|
|
from: toToothIntent(span?.from),
|
|
|
|
|
to: toToothIntent(span?.to),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
treatmentType: wire?.treatmentType ?? null,
|
|
|
|
|
teeth: teeth.map(toToothIntent),
|
|
|
|
|
connectedSpans,
|
|
|
|
|
comment: wire?.comment ?? null,
|
feat(voice): adapt voice entry to the stacked-jobs prosthesis model
Authored by the /orchestrate builder agent, committed unrepaired so the
fixes that follow are reviewable against it.
Backend: replaces the flat prosthesisDefaultType/prosthesisOverrides wire
shape with a prosthesis: ProsthesisAssignment[] list whose targets can be a
tooth or a jaw; adds resolveAssignmentTarget / classifyTypeCode /
resolveProsthesisAssignment for leaf-vs-category classification, region
validity with mixed-region deferral, and assignmentIndex on unresolved
items; adds PROSTHESIS_CATEGORY and PROSTHESIS_SUBCATEGORY to
CatalogEntityKind with a migration and seeded fa/en/nl translations; and
rewrites the extraction prompt to render the catalog as a tree.
Frontend: merged "teeth and prosthesis" row, stack preview through the
existing applyLeafToJobs, three chip-fold paths, rewritten applyVoiceResult
and voiceForEditor, and the two carried-forward recording fixes — the
container fallback that refused Safari and the render gate that never
checked isMediaRecorderSupported().
Adds Vitest for the frontend's pure helpers, and updates CLAUDE.md.
Gate was green: backend 16 suites / 209 tests, nest build, prisma validate;
frontend 37 Vitest tests, tsc --noEmit, next build.
KNOWN DEFECTS, fixed in the commits that follow:
- VoiceReviewSheet.tsx:169 — a picked tooth chip is dropped on Apply
- VoiceReviewSheet.tsx:213 / TreatmentWorkspace.tsx:2215 — decision 41's
type-row lock is missing, so unticking it saves prosthesis lab rows on a
non-prosthesis detail
Reviewed on the correctness lens only; regression-risk never ran. The
migration was validated but never applied.
Spec: docs/specs/voice-treatment-entry/spec.md
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-07 12:23:58 +08:00
|
|
|
prosthesis: assignments.map(toProsthesisAssignment),
|
2026-08-20 18:00:53 +03:30
|
|
|
labId: wire?.labId ?? null,
|
|
|
|
|
labMatchExact: wire?.labMatchExact === true,
|
|
|
|
|
due: toDueIntent(wire?.due),
|
|
|
|
|
};
|
|
|
|
|
}
|