"یه کامپلیت دنچر برای فک بالا میخوام" resolved the upper arch and the
complete denture correctly, but the review sheet also showed
position_out_of_range against «فک بالا» and asked which tooth was meant. No
tooth was said.
The model names the jaw in the top-level `teeth` array as well as in the
prosthesis target it belongs to. resolveVoiceIntent passed that array
straight to resolveToothIntents, extraction.wire.ts turns `position: null`
into NaN, and unresolvedReason tests positionBad first — so it reported a
range fault for a value that was never a number, before ever reaching the
quadrant branch.
resolveVoiceIntent now drops jaw-shaped entries — an arch with no usable
position — before the tooth resolver sees them. The jaw already reaches the
form through its assignment, so the duplicate carries no information worth
reporting. An entry that DOES give a position survives: arch plus position
without a side is a real tooth described without its quadrant, and must keep
offering its candidate chips.
Two invitations removed as well, both ours: the `teeth` property in
VOICE_INTENT_JSON_SCHEMA had no description at all, and no prompt rule said a
jaw must stay out of it, while TOOTH_SCHEMA — shared with
prosthesis[].targets — describes jaws as acceptable. Adds the description and
HARD RULE 6.
Four tests. The two jaw cases fail without the filter; the out-of-range and
missing-quadrant cases pass either way and exist to prove the filter does not
over-reach.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
origin/master removed the dispatch panel's remembered-prosthesis auto-fill
(f52ad6b), so the exemption this branch carried for it went away in the
rebase. The spec named the mechanism and a verification step for a feature
that no longer exists.
The rule it existed to enforce still stands and is worth keeping written
down — confirm fills exactly what the sheet previewed — so it stays, marked
as a constraint on whatever gets added next rather than a description of
something in the code.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
The spec still described the flow as designed, not as it works after the
first live recordings.
§2 gains what confirm actually writes: the detail and its lab case are
persisted on confirm, because the autosave effect watches `details` only
and a lab draft left in component state loses the lab, the due date and the
prosthesis map on reload — while the detail survives, which is what makes
that loss look like a save. Plus the guard: a preview treatment comes back
instead when any detail is still untyped, and confirm skips the lab-case
save rather than posting an id the server has never seen.
§9 corrects three codes: VOICE_MIC_DENIED is now only a real permission
failure, VOICE_UNSUPPORTED_FORMAT covers every "this browser cannot record"
path, and both it and VOICE_CLIP_TOO_LONG are named on their DTO
constraints rather than falling through the shared map to an unrelated
message. §8 no longer claims there is no duration cap — there is, 2
minutes, decided before implementation.
§12 gains the checks these changes need, including the reload that catches
an unsaved lab case, and decisions 31-33 record the three rules the live
testing settled.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review findings on this branch.
The lab-case save could be posted against a detail the server has never
seen. persistDraft returns a *preview* treatment instead of saving when any
detail lacks a treatment type — the blank one the workspace opens with is
enough — and a preview's detail id falls back to the client id. Recording
straight after opening a visit and confirming a result with a lab or due
date would send that id and fail the whole save. It now checks what came
back rather than the precondition, so it holds for every early return
persistDraft has.
stop() optional-chained into a no-op when the recorder was already gone,
leaving the bar recording forever with a live timer and only Cancel as a
way out.
Three "this browser cannot record" paths reported VOICE_MIC_DENIED — no
MediaRecorder at all, no container the API accepts, and a recorder that
throws after permission was already granted. Telling clinicians their
microphone was denied sends them hunting for a permission nothing asked
for; they now report VOICE_UNSUPPORTED_FORMAT.
The voice route's large-body match stripped every trailing slash while
Express ignores exactly one, so '/api/voice/extract//' bought a 10 MB
buffer for a request that then 404s.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Dictating "12 روکش PFM, 13 روکش PFZ" previewed correctly and then landed in
the form as PFM on both teeth.
The stored data was never wrong — the dev database holds 12 → pfm_crown and
13 → pfz_crown with selectionGroupIds matching the detail's groups exactly,
and a page reload renders it correctly. The damage was live client state:
the dispatch panel's "last type used for this lab" default rebuilt the
*entire* map from one code, so a single row reading as unfilled destroyed
every type already set.
Two changes:
- It fills blanks now, and leaves every entry that already carries a type
alone. The bulk "apply to all" select only pre-sets itself when the fill
really did cover every tooth, instead of claiming one type while the rows
below disagree.
- A lab case created by confirming a voice result is exempt from the
default entirely. The review sheet is a contract: topping the case up
with a type for a tooth the preview never showed makes the confirmation
step a lie about what it was going to fill.
The exemption is tracked in workspace state rather than on LabCaseDraft
because a draft field is dropped by mapLabCaseDraftFromApi on the first
server round-trip — exactly the window this failure lives in.
isProsthesisMapComplete is deliberately untouched: its strict
selectionGroupId match succeeds on the real data, so loosening it would
have been a blind change to a working path.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The backend returns the transcript on VOICE_EXTRACT_FAILED and the client
never reads it, so dictation the clinic paid for is shipped in an error
body and dropped. The spec claimed the whole feature was implemented; it
now names the gap and the two ways out.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>