Five migrations were pending, not one: this dev database had not been
migrated since the prosthesis overhaul landed on master. Used migrate deploy
rather than migrate dev so no drift prompt could offer a reset, and counted
the DELETE first — 0 rows matched against 2 total.
Seeded translations verified: 7 categories and 5 subcategories in fa, en and
nl, with crown resolving to روکشها. Both login accounts survived.
Also records that .gitea/workflows run no test, lint or typecheck step at
all, which is why two spec-file type errors reached the branch unnoticed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Decision 39 couples teeth and jobs on a lab-dependent type, so unticking the
merged row and then the type row applies a detail with no teeth at all. master
kept them, because the rows were separate. Approved as an accepted consequence
rather than patched, since applying teeth the clinician just unticked would
break the sheet-is-a-contract rule.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Second correctness pass on e271858 found 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. The e271858 type-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 at 15ddb9a.
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>
Both found by the /orchestrate correctness critic against 15ddb9a, and both
verified by hand before fixing.
1. A picked tooth chip was dropped on Apply. pickCandidate ticked the teeth
row from `available`, which is memoised from `effective` — the value the
same handler is in the middle of changing. On "ترمیم برای دندون دو" the
backend returns no teeth, so `available.teeth` was false at pick time and
`prev.teeth` was false too; `false || false` stuck permanently. A variant
of the original live-test failure. Each branch now ticks the row it feeds
and returns, so nothing reads the stale memo.
2. Decision 41's type-row lock was missing. The treatmentType row was a plain
toggle, and applyVoiceResult derived `labDependent` from
`result.treatmentType` rather than the `detail.treatmentType` it writes —
so unticking the type row saved prosthesis lab rows on whatever type the
appointment purpose had seeded. Row gains a `locked` state, the type row
locks while the prosthesis row is ticked, Apply sends the forced tick, and
`labDependent` now comes from the detail being written.
Gates: backend 16 suites / 209 tests, frontend 37 Vitest tests,
tsc --noEmit clean, next build clean, ESLint 0 new warnings
(VoiceReviewSheet.tsx 0 issues; the 10 in TreatmentWorkspace.tsx are
pre-existing and unchanged in count).
Not covered by a test: both fixes live in component state logic, which the
chosen Vitest scope — pure helpers, no React, no DOM — cannot reach. §12's
manual checks cover them.
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>
The Gap-check phase halted the run on one blocking gap and reported four
notes. Nothing was built.
Blocking: the prompt cannot offer a prosthesis category the model can name,
because no category label exists anywhere the backend can read —
CatalogEntityKind covers only TREATMENT_TYPE, PROSTHESIS_TYPE and
LAB_WORKFLOW_STEP. Adds PROSTHESIS_CATEGORY and PROSTHESIS_SUBCATEGORY as
catalog entities with seeded fa/en/nl translations, rather than sending bare
codes that would read untranslated on the locale this feature exists for.
Also closed: a chip that resolved to a jobless tooth and was then discarded
by the jobless-tooth rule, so the tap did nothing; the undefined region check
for a category whose leaves span crown and arch; the wrong endpoint path in
§3; and the availability-endpoint contradiction in §11.
Corrects two counts of my own: 5 subcategories, not 4 (night_guard was
missed), and the disjointness test now asserts against the live catalog
rather than a number written in prose.
Decisions 47-50. Work items 18-19 added to the ledger.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The treatment form was overhauled after voice v1 merged (a3c14a1, 7f92e73,
72f885d, 5d3597f): one prosthesis type per tooth became stacked jobs,
jaw-level appliances and a category tree. Voice still compiled against it
but could no longer express it, and in two places wrote data the form
itself refuses.
Rewrites the extraction contract (§5), the resolver rules and the
unresolved-reason table (§6), the review sheet (§7) and verification
(§12), and records decisions 34-46. Adds the repos: block and a progress
ledger so the task resolves from the branch.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Mobinnet holds public port 80, so staging is served on 443. Examples and the host nginx template now match FRONTEND_URL=https://wixur.ir.
Co-authored-by: Cursor <cursoragent@cursor.com>