A bridge is a contiguous span, but every operation that filtered teeth out of
a group kept the `connected` kind as long as two teeth remained. Reduce a
12-13-14 span to 12 and 14 and you get a "bridge" with no pontic — and worse,
both teeth keep one selectionGroupId, so the lab receives them as a single
unit and task generation builds work that cannot be made.
linkedEdgesFromGroups already guarded the drawn marks with areArchNeighbors,
so the chart looked right while the data was wrong.
Adds splitDisconnectedRuns, which breaks what remains into contiguous runs: a
run of two or more stays connected, a run of one becomes a single. The first
run keeps the original groupId so lab rows pointing at it stay valid, and
pruneToothProsthesisForGroups re-maps the rest.
Applied at all three sites that filter a group's teeth, which each carried
their own copy of the length check:
- groupsFromFlatTeeth, when teeth are no longer selected
- applyShiftRange, when a shift-range takes teeth from an existing group
- removeTeethFromGroups, the path pruneDetailTeethToJobs uses
toggleToothInGroups already split into runs by hand for the single-removal
case; this is the same rule, shared.
Tests: a new toothSelectionGroups.spec.ts (13) plus two for
removeTeethFromGroups. The middle-tooth cases fail without the fix; the
end-tooth and contiguous cases pass either way and exist to prove it does not
over-reach. CLAUDE.md updated for the third Vitest file.
Not fixed: a pre-existing unused `leftIdx` warning in unlinkAdjacentTeeth,
unrelated to this change.
Gates: 52 Vitest tests, tsc --noEmit clean, next build clean, ESLint
unchanged at 1 pre-existing warning.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>