fix(voice): validate a code's region against its target properly
Second correctness pass one271858found three ways an arch-only appliance could be written as a per-tooth job, plus a gap in the previous repair. 1. The deferred region check was never completed. A mixed-region category resolved with no region recorded, and nothing re-validated the leaf the clinician then picked — "پروتز متحرک برای دندون ۱۲" wrote a complete denture onto tooth 12, which the manual chart cannot produce and which task generation would expand as denture steps. Targets now resolve BEFORE types, so the target kind narrows a category's candidates and an impossible leaf is never offered. The deferral disappears with it. 2. `some` over the stack's regions let one legal code admit every other. `types: ['pfm_crown','night_guard_soft']` on tooth 12 passed, because `crown` suited the tooth, and wrote a night guard onto that tooth. Now every named code must suit the target. 3. `regions.size === 1` also deferred `implant`, whose leaves span root and crown — both tooth regions, so not a tooth/arch category at all. An implant aimed at a jaw resolved as a UA target. Narrowing by target kind rejects it instead. 4. Thee271858type-row lock ticked the row and the payload but not the count, so the sheet read "Apply 1 field" while two landed. One `effectiveSelection` now drives the count and the payload. Also narrows a `filter(Boolean)` in the disjointness test that left two implicit-any errors under the full tsconfig (nest build excludes specs, so the repo gate never saw them). Pre-existing at15ddb9a. Adds four resolver tests: candidate narrowing per target kind, a category with no usable leaf, a stack where only one code suits, and a legal stack. All three defects passed the previous 209 tests. Gates: backend 212 tests, nest build, ESLint clean on the voice module; frontend 37 Vitest tests, tsc --noEmit, ESLint clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -116,8 +116,15 @@ export function VoiceReviewSheet({
|
||||
const typeForcedByProsthesis =
|
||||
available.prosthesis && selection.prosthesis && effective.treatmentType != null;
|
||||
|
||||
// What Apply actually sends. The locked type row is ticked on screen, so it has to be
|
||||
// ticked in the count and in the payload alike — counting the raw `selection` showed
|
||||
// "Apply 1" while two fields landed.
|
||||
const effectiveSelection: VoiceApplySelection = typeForcedByProsthesis
|
||||
? { ...selection, treatmentType: true }
|
||||
: selection;
|
||||
|
||||
const nothingToApply = !hasAnythingToApply(effective, labDependentCodes);
|
||||
const selectedCount = countSelected(selection, available);
|
||||
const selectedCount = countSelected(effectiveSelection, available);
|
||||
|
||||
const targetLabel = (target: string): string => {
|
||||
if (target === ARCH_TOOTH_UPPER) return t('upperArch');
|
||||
@@ -394,13 +401,7 @@ export function VoiceReviewSheet({
|
||||
type="button"
|
||||
variant="primary"
|
||||
disabled={selectedCount === 0}
|
||||
onClick={() =>
|
||||
onApply(
|
||||
// The locked type row is ticked in the UI, so it must be ticked in what applies.
|
||||
typeForcedByProsthesis ? { ...selection, treatmentType: true } : selection,
|
||||
effective,
|
||||
)
|
||||
}
|
||||
onClick={() => onApply(effectiveSelection, effective)}
|
||||
fullWidth
|
||||
className="sm:w-auto"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user