docs: close the gaps the orchestrate surveyor found
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>
This commit is contained in:
@@ -39,6 +39,8 @@ Status legend: ⬜ not started · 🟡 in progress · ✅ done · ⛔ blocked
|
|||||||
| 15 | Vitest specs for `prosthesisTree.ts` and `voiceReviewRows.ts` | `dyolink` | ⬜ | §12 |
|
| 15 | Vitest specs for `prosthesisTree.ts` and `voiceReviewRows.ts` | `dyolink` | ⬜ | §12 |
|
||||||
| 16 | New user-visible strings in `en.json`, `fa.json`, `nl.json` | `dyolink` | ⬜ | i18n is mandatory, not a follow-up |
|
| 16 | New user-visible strings in `en.json`, `fa.json`, `nl.json` | `dyolink` | ⬜ | i18n is mandatory, not a follow-up |
|
||||||
| 17 | Run every gate in §12, then the manual pass including Safari and iPad | `dyolink` | ⬜ | §12 |
|
| 17 | Run every gate in §12, then the manual pass including Safari and iPad | `dyolink` | ⬜ | §12 |
|
||||||
|
| 18 | `PROSTHESIS_CATEGORY` + `PROSTHESIS_SUBCATEGORY` in `CatalogEntityKind`; migration; seed fa/en/nl translations | `dyolink` | ⬜ | §5, decision 47. **Must land before item 9** |
|
||||||
|
| 19 | Unresolved items carry `assignmentIndex`; a picked chip inherits that assignment's jobs | `dyolink` | ⬜ | §6, decision 50. Without it the quadrant chip is dead on a prosthesis detail |
|
||||||
|
|
||||||
## Key decisions
|
## Key decisions
|
||||||
|
|
||||||
@@ -55,6 +57,10 @@ Full table in the spec's §13, rows 34–46. The load-bearing ones:
|
|||||||
save an empty detail today, because `persistDraft` prunes lab-dependent details to their jobs.
|
save an empty detail today, because `persistDraft` prunes lab-dependent details to their jobs.
|
||||||
- **Nothing is guessed.** A missing material, a missing jaw and a missing quadrant all become
|
- **Nothing is guessed.** A missing material, a missing jaw and a missing quadrant all become
|
||||||
chips. A jobless tooth is named and left out.
|
chips. A jobless tooth is named and left out.
|
||||||
|
- **Categories become real catalog entities** (decision 47). Two new `CatalogEntityKind` values
|
||||||
|
and seeded translations, rather than bare untranslated codes in the prompt on `fa`.
|
||||||
|
- **A chip must inherit its assignment's jobs** (decision 50), or it resolves to a jobless tooth
|
||||||
|
and the tap does nothing.
|
||||||
|
|
||||||
## Deviations from spec
|
## Deviations from spec
|
||||||
|
|
||||||
@@ -62,6 +68,8 @@ Full table in the spec's §13, rows 34–46. The load-bearing ones:
|
|||||||
|
|
||||||
## Next steps / open questions
|
## Next steps / open questions
|
||||||
|
|
||||||
|
- Re-run `/orchestrate --dry` with a `gapNote`, so the surveyor re-runs instead of replaying its
|
||||||
|
cached `gaps_found` verdict.
|
||||||
- Amin reviews and approves this spec revision. That approval is the only human gate before
|
- Amin reviews and approves this spec revision. That approval is the only human gate before
|
||||||
`/orchestrate --dry`.
|
`/orchestrate --dry`.
|
||||||
- Open items 15 and 16 in §11 are carried forward unchanged and are **not** in this branch:
|
- Open items 15 and 16 in §11 are carried forward unchanged and are **not** in this branch:
|
||||||
@@ -69,3 +77,187 @@ Full table in the spec's §13, rows 34–46. The load-bearing ones:
|
|||||||
specified but not built.
|
specified but not built.
|
||||||
- `feat/voice-treatment-entry` and `backup/pre-rebase-voice` are stale local branches from the
|
- `feat/voice-treatment-entry` and `backup/pre-rebase-voice` are stale local branches from the
|
||||||
v1 work. Safe to delete once this lands.
|
v1 work. Safe to delete once this lands.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-09-07 — Gap-check halted (nothing built, nothing pushed)
|
||||||
|
|
||||||
|
- **Phase:** Gap-check
|
||||||
|
- **Reason:** the spec has 1 gap that blocks implementation — nothing was built.
|
||||||
|
- **Repo:** dyolink @ `feat/treatment/add-voice-input-for-new-treatment-form`
|
||||||
|
|
||||||
|
### Blocking gap 1 — `missing_constraint` (dyolink)
|
||||||
|
|
||||||
|
**Summary:** The prompt must present prosthesis categories and subcategories so the model can
|
||||||
|
return `crown` for "روکش", but no localized label for a category or subcategory exists anywhere
|
||||||
|
the backend can read. `CatalogEntityKind` has only TREATMENT_TYPE / PROSTHESIS_TYPE /
|
||||||
|
LAB_WORKFLOW_STEP, and the only category labels in the repo are frontend message keys
|
||||||
|
(`category_crown`, `sub_night_guard`) plus a hardcoded single-locale map in a dev export script.
|
||||||
|
|
||||||
|
**Spec quotes:**
|
||||||
|
|
||||||
|
> **The prosthesis list is no longer flat.** `buildCatalog` already receives `category`, `subcategory`, `chartRegion` and `stackGroup` from `ProsthesisCatalogService` and throws all four away. It now passes them through, so the prompt presents the catalog as the tree it is and marks which codes are jaw-level. Still no catalog knowledge hardcoded in the prompt — the shape comes from the data.
|
||||||
|
|
||||||
|
> Treatment types and prosthesis types come from `CatalogLabelService` in the actor's locale, so the model sees "پروتز" and "زیرکونیا مونولیتیک" as the spoken forms of `prosthesis` and `monolithic_zirconia` rather than being asked to translate. Catalog entities store a stable `code` and no label — never hardcode a label.
|
||||||
|
|
||||||
|
> "روکش" is *crown* — a category with nine leaves, not a material. The model returns the category code it actually heard rather than guessing `pfm_crown`, and the review sheet offers the leaves as chips (§7).
|
||||||
|
|
||||||
|
**Why blocking:** Work item 9 ("Prompt: present the catalog as a tree") cannot be built.
|
||||||
|
`ProsthesisCatalogService.list()` returns `category` and `subcategory` as bare codes with no label
|
||||||
|
(prosthesis-catalog.service.ts:82-95), and `CatalogLabelService` cannot resolve them because
|
||||||
|
`CatalogEntityKind` has no category kind (schema.prisma:331-335). The three ways out have
|
||||||
|
different costs and one breaks an explicit repo rule, so a builder cannot choose: (a) add
|
||||||
|
PROSTHESIS_CATEGORY / PROSTHESIS_SUBCATEGORY to the enum plus a migration and seeded translations
|
||||||
|
— a schema change the spec never mentions; (b) hardcode a fa/en/nl label map in the backend, which
|
||||||
|
the spec and CLAUDE.md both forbid; (c) send bare codes only, which makes the whole category
|
||||||
|
feature unmeasured on the weakest locale.
|
||||||
|
|
||||||
|
**Question:** Where do the localized labels for the 7 prosthesis categories and 5 subcategories
|
||||||
|
come from for the extraction prompt: a new `CatalogEntityKind` (PROSTHESIS_CATEGORY /
|
||||||
|
PROSTHESIS_SUBCATEGORY) with a migration and seeded `CatalogTranslation` rows, or bare codes with
|
||||||
|
no labels in the prompt?
|
||||||
|
|
||||||
|
**Suggested spec change:** Add to §5: "Category and subcategory labels have no backend source
|
||||||
|
today — `CatalogEntityKind` covers only TREATMENT_TYPE, PROSTHESIS_TYPE and LAB_WORKFLOW_STEP, and
|
||||||
|
`category_*` / `sub_*` exist only in `frontend/messages/*.json`. Add `PROSTHESIS_CATEGORY` and
|
||||||
|
`PROSTHESIS_SUBCATEGORY` to `CatalogEntityKind` (one migration), seed their fa/en/nl translations
|
||||||
|
from the existing message keys, and resolve them through `CatalogLabelService` like every other
|
||||||
|
catalog label."
|
||||||
|
|
||||||
|
**Evidence:** backend/prisma/schema.prisma:331; backend/src/modules/prosthesis-catalog/prosthesis-catalog.service.ts:82; frontend/messages/en.json:1124; backend/prisma/export-prosthesis-catalog.ts:16
|
||||||
|
|
||||||
|
### Non-blocking notes
|
||||||
|
|
||||||
|
**Note 1 — `ambiguity` (dyolink).** An unresolved item carries only `spoken`, `reason` and
|
||||||
|
`candidates`, with no link back to the assignment it came from. On a prosthesis detail a
|
||||||
|
`tooth_missing_quadrant` chip therefore resolves to a tooth with no job, which the "tooth with no
|
||||||
|
job is named and left out" rule then discards — so tapping the chip does nothing.
|
||||||
|
|
||||||
|
> **A single digit alone is never resolved.** "دندون دو" names four teeth. It is reported as `tooth_missing_quadrant` **with the candidate codes attached**
|
||||||
|
|
||||||
|
> **A tooth with no job is named and left out.**
|
||||||
|
|
||||||
|
> Picking one folds the choice into the result, so an under-specified item is one tap from resolved instead of a dead end.
|
||||||
|
|
||||||
|
*Why not blocking:* a builder can add an assignment reference to the unresolved item. But §6's
|
||||||
|
reason table reads as a complete contract and its "Carries" column names only `candidates`, so a
|
||||||
|
builder following it literally ships a dead chip on exactly the prosthesis flow §12 asks them to
|
||||||
|
test.
|
||||||
|
*Question:* Should `VoiceUnresolvedItem` gain an assignment reference (for example
|
||||||
|
`assignmentIndex`) so a chosen quadrant chip inherits that assignment's job stack?
|
||||||
|
*Suggested spec change:* In §6's reason table, add: "`tooth_missing_quadrant` and
|
||||||
|
`arch_not_spoken` also carry the index of the assignment they came from, so a picked chip inherits
|
||||||
|
that assignment's `types[]` rather than becoming a jobless tooth."
|
||||||
|
*Evidence:* frontend/src/types/voice.ts:15-25; frontend/src/components/treatment/voiceReviewRows.ts:66
|
||||||
|
|
||||||
|
**Note 2 — `ambiguity` (dyolink).** "The resolver confirms [jaw vs tooth] against the
|
||||||
|
`chartRegion` of the assignment's codes" is undefined when `types[]` holds a category whose leaves
|
||||||
|
have mixed regions. `removable` is exactly that: `complete_denture` and `overdenture` are
|
||||||
|
`chartRegion: 'arch'`, `partial_denture` is `'crown'`.
|
||||||
|
|
||||||
|
> A target that names an arch and no position is a jaw, and the resolver confirms that against the `chartRegion` of the assignment's codes.
|
||||||
|
|
||||||
|
> `partial_denture` — a `removable` code that is nonetheless assigned per tooth — already ships with `chartRegion: 'crown'` in `catalog-seed-data.ts`, so the exception needs no special case either.
|
||||||
|
|
||||||
|
> A code whose region contradicts its target is reported, never coerced.
|
||||||
|
|
||||||
|
*Why not blocking:* deferring the region check until the ambiguity chip is picked is a reasonable
|
||||||
|
default. Worth stating so the builder does not emit `code_not_valid_for_target` for a category that
|
||||||
|
has not been narrowed yet.
|
||||||
|
*Question:* When `types[]` holds a category whose leaves span both `crown` and `arch` regions (only
|
||||||
|
`removable` today), is the region check deferred until the clinician picks a leaf chip?
|
||||||
|
*Suggested spec change:* In §5 under "A target is a tooth or a jaw": "When `types[]` holds a
|
||||||
|
category whose leaves have more than one `chartRegion`, the region check is deferred — the item
|
||||||
|
resolves to `prosthesis_type_ambiguous` and the region is confirmed against the leaf the clinician
|
||||||
|
picks."
|
||||||
|
*Evidence:* backend/prisma/catalog-seed-data.ts:266-288
|
||||||
|
|
||||||
|
**Note 3 — `brownfield` (dyolink).** §3 names the endpoint `POST /treatments/voice-extract`; the
|
||||||
|
shipped route is `POST /voice/extract` (`@Controller('voice')` + `@Post('extract')`), and
|
||||||
|
`common/body-parsers.ts` hardcodes `/api/voice/extract` to select the 10 MB body limit.
|
||||||
|
|
||||||
|
> `POST /treatments/voice-extract`
|
||||||
|
|
||||||
|
*Why not blocking:* no work item asks to move the route. But if a builder aligned the route with
|
||||||
|
§3, `isVoiceExtractPath` would stop matching and every real recording would 413 against the 100 kb
|
||||||
|
default, which reads as a broken microphone rather than a route change.
|
||||||
|
*Question:* Should §3 be corrected to the shipped path `POST /voice/extract`, since no work item
|
||||||
|
moves the route?
|
||||||
|
*Suggested spec change:* Change §3's heading to `POST /voice/extract` and note that
|
||||||
|
`VOICE_EXTRACT_PATH` in `common/body-parsers.ts` must move with it if it ever changes.
|
||||||
|
*Evidence:* backend/src/modules/voice/voice.controller.ts:23,36; backend/src/common/body-parsers.ts:10
|
||||||
|
|
||||||
|
**Note 4 — `contradiction` (dyolink).** §11 item 13 says v1 ships with "no availability endpoint",
|
||||||
|
but §4 requires the frontend to learn enabled locales from the API, and `GET /voice/availability`
|
||||||
|
exists and is what `voiceForEditor` reads today.
|
||||||
|
|
||||||
|
> ~~**Availability API**~~ — **resolved for v1:** voice ships **open to everyone** with a configured locale profile. No plan check, no availability endpoint.
|
||||||
|
|
||||||
|
> The frontend must learn which locales are enabled **from the API**, not from a `NEXT_PUBLIC_*` var
|
||||||
|
|
||||||
|
*Why not blocking:* work item 2 ("`voiceForEditor` also checks `isMediaRecorderSupported()`")
|
||||||
|
presupposes `voiceAvailability` stays, so the working reading is obvious. The stale line just needs
|
||||||
|
correcting so nobody deletes a live endpoint.
|
||||||
|
*Question:* Confirm the availability endpoint stays in v1 and only the plan check is deferred?
|
||||||
|
*Suggested spec change:* Reword item 13 to "no plan check; the availability endpoint ships and
|
||||||
|
reports configured locales and `maxRecordingMs` only."
|
||||||
|
*Evidence:* backend/src/modules/voice/voice.controller.ts:28; frontend/src/components/ui/treatment/TreatmentWorkspace.tsx:625
|
||||||
|
|
||||||
|
**Note 5 — `brownfield` (dyolink).** §5's disjointness counts are slightly off: the seed defines 41
|
||||||
|
distinct prosthesis codes (not 42) and 5 subcategory names — veneer, inlay, onlay, overlay,
|
||||||
|
night_guard (not 4). The disjointness claim itself holds: no leaf code equals any category or
|
||||||
|
subcategory name.
|
||||||
|
|
||||||
|
> This needs no extra field because the namespaces are disjoint: 42 leaf codes against 7 category names and 4 subcategory names, no collisions.
|
||||||
|
|
||||||
|
*Why not blocking:* the test asserts disjointness, not counts, and disjointness is true against the
|
||||||
|
current seed. Flagged so the builder writes the test against the live catalog rather than a
|
||||||
|
hardcoded 42/7/4.
|
||||||
|
*Question:* May the disjointness test assert against the live catalog instead of the counts written
|
||||||
|
in §5?
|
||||||
|
*Suggested spec change:* Drop the exact counts and keep only "the leaf, category and subcategory
|
||||||
|
namespaces are disjoint; a test asserts it against the live catalog".
|
||||||
|
*Evidence:* backend/prisma/catalog-seed-data.ts:121-400
|
||||||
|
|
||||||
|
### Files read during the gap-check
|
||||||
|
|
||||||
|
`docs/specs/voice-treatment-entry/spec.md`, `docs/specs/voice-treatment-entry/progress.md`,
|
||||||
|
`backend/src/modules/voice/extraction.prompt.ts`, `backend/src/modules/voice/extraction.wire.ts`,
|
||||||
|
`backend/src/modules/voice/voice.providers.ts`, `backend/src/modules/voice/voice.service.ts:243`,
|
||||||
|
`backend/src/modules/voice/voice.controller.ts:23`,
|
||||||
|
`backend/src/modules/prosthesis-catalog/prosthesis-catalog.service.ts:72`,
|
||||||
|
`backend/prisma/catalog-seed-data.ts`, `backend/prisma/schema.prisma:331`,
|
||||||
|
`backend/prisma/export-prosthesis-catalog.ts:16`, `backend/src/common/fdi.ts`,
|
||||||
|
`backend/src/common/body-parsers.ts:10`,
|
||||||
|
`backend/src/modules/treatments/lab-case-send.validation.ts:116`,
|
||||||
|
`frontend/src/components/treatment/prosthesisTree.ts`,
|
||||||
|
`frontend/src/components/treatment/voiceReviewRows.ts`, `frontend/src/types/voice.ts`,
|
||||||
|
`frontend/src/lib/voice/audioFormat.ts`, `frontend/src/lib/voice/useVoiceCapture.ts:157`,
|
||||||
|
`frontend/src/components/ui/treatment/TreatmentWorkspace.tsx:625` and `:2184`,
|
||||||
|
`frontend/src/components/ui/treatment/FdiToothChart.tsx:218`,
|
||||||
|
`frontend/src/components/ui/treatment/ProsthesisJobPopover.tsx:200`,
|
||||||
|
`frontend/messages/en.json:1124`, `frontend/package.json`
|
||||||
|
(all paths relative to `/Users/aminmsvi/PersonalProjects/dyolink`)
|
||||||
|
|
||||||
|
### How to resume
|
||||||
|
|
||||||
|
Close the gaps in the spec, then re-run. Pass `gapNote` describing what you changed, or the
|
||||||
|
analyst replays this same verdict from cache.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-09-07 — Gaps closed, ready to re-run
|
||||||
|
|
||||||
|
All five findings are now answered in the spec. Nothing was built in the halted run, so there is
|
||||||
|
no code to revisit.
|
||||||
|
|
||||||
|
| Finding | Resolution | Spec |
|
||||||
|
|---|---|---|
|
||||||
|
| **Blocking** — no backend source for category labels | Add `PROSTHESIS_CATEGORY` + `PROSTHESIS_SUBCATEGORY` to `CatalogEntityKind`, one migration, seed fa/en/nl from the existing `prosthesis.category_*` / `sub_*` keys, resolve via `CatalogLabelService`. Frontend keeps its own keys — migrating it is out of scope | §5, decisions 47–48 |
|
||||||
|
| Note 1 — chip with no assignment link | Unresolved items raised inside an assignment carry `assignmentIndex`; a picked chip inherits that assignment's `types[]` | §6, decision 50 |
|
||||||
|
| Note 2 — mixed-region category | `removable` defers its region check to the picked leaf; never `code_not_valid_for_target` while still a category | §5, decision 49 |
|
||||||
|
| Note 3 — wrong endpoint path in §3 | Corrected to `POST /voice/extract`, with the `body-parsers.ts` coupling called out. No work item moves the route | §3 |
|
||||||
|
| Note 4 — availability endpoint contradiction | §11 item 13 corrected: the endpoint exists, only the plan check is deferred | §11 |
|
||||||
|
| Note 5 — disjointness counts | Verified independently: **42** leaf codes (the surveyor's 41 missed `screw_retained`, a multi-line `implant(` call) and **5** subcategories (the spec said 4, missing `night_guard`). The test now asserts against the live catalog, not a written count | §5, §12 |
|
||||||
|
|
||||||
|
Referee relays: 0. Gate repairs: 0. Neither phase was reached.
|
||||||
|
|||||||
@@ -272,7 +272,14 @@ mirroring and Jalali conversion — testable instead of hopeful.
|
|||||||
|
|
||||||
### Endpoint
|
### Endpoint
|
||||||
|
|
||||||
`POST /treatments/voice-extract`
|
`POST /voice/extract` — `@Controller('voice')` + `@Post('extract')`, so the full path behind the
|
||||||
|
global prefix is `/api/voice/extract`. Earlier revisions of this spec named it
|
||||||
|
`/treatments/voice-extract`, which was never the shipped route. **No work item moves it.**
|
||||||
|
|
||||||
|
> ⚠ The path is duplicated in `common/body-parsers.ts`, which matches `/api/voice/extract` to
|
||||||
|
> select the large JSON body limit. Moving the route without moving `VOICE_EXTRACT_PATH` makes
|
||||||
|
> every real recording 413 against the 100 kb default, which reads as a broken microphone rather
|
||||||
|
> than a routing change.
|
||||||
|
|
||||||
- Guards: `JwtAuthGuard` + `ClinicOrgGuard`.
|
- Guards: `JwtAuthGuard` + `ClinicOrgGuard`.
|
||||||
- Service-level check of `TAB_TREATMENT_EDIT`. The plan flag is **not** checked in v1
|
- Service-level check of `TAB_TREATMENT_EDIT`. The plan flag is **not** checked in v1
|
||||||
@@ -463,16 +470,52 @@ A code whose region contradicts its target is reported, never coerced. An arch a
|
|||||||
aimed at tooth 12, or a crown aimed at the upper jaw, resolves to
|
aimed at tooth 12, or a crown aimed at the upper jaw, resolves to
|
||||||
`code_not_valid_for_target` (§6).
|
`code_not_valid_for_target` (§6).
|
||||||
|
|
||||||
|
**A category whose leaves span both regions defers the check.** `removable` is the only one
|
||||||
|
today: `complete_denture` and `overdenture` are `chartRegion: 'arch'` while `partial_denture` is
|
||||||
|
`'crown'`. There is no region to check until a leaf is chosen, so such an assignment resolves to
|
||||||
|
`prosthesis_type_ambiguous` and the region is confirmed against the leaf the clinician picks —
|
||||||
|
never emitted as `code_not_valid_for_target` while it is still a category.
|
||||||
|
|
||||||
### `types[]` may hold a leaf or a category
|
### `types[]` may hold a leaf or a category
|
||||||
|
|
||||||
"روکش" is *crown* — a category with nine leaves, not a material. The model returns the
|
"روکش" is *crown* — a category with nine leaves, not a material. The model returns the
|
||||||
category code it actually heard rather than guessing `pfm_crown`, and the review sheet offers
|
category code it actually heard rather than guessing `pfm_crown`, and the review sheet offers
|
||||||
the leaves as chips (§7).
|
the leaves as chips (§7).
|
||||||
|
|
||||||
This needs no extra field because the namespaces are disjoint: 42 leaf codes against 7
|
This needs no extra field because the namespaces are disjoint: **42** leaf codes against **7**
|
||||||
category names and 4 subcategory names, no collisions. The resolver classifies by lookup. A
|
category names and **5** subcategory names (`veneer`, `inlay`, `onlay`, `overlay`,
|
||||||
test asserts the disjointness, because a future catalog entry named `crown` would quietly turn
|
`night_guard`), no collisions. The resolver classifies by lookup. The test asserts disjointness
|
||||||
a leaf into an ambiguity.
|
against the **live catalog**, not against these counts, because a future entry named `crown`
|
||||||
|
would quietly turn a leaf into an ambiguity and a hardcoded count would not notice.
|
||||||
|
|
||||||
|
#### Category labels need a backend source — build it
|
||||||
|
|
||||||
|
The prompt cannot offer a category the model can name unless that category has a label in the
|
||||||
|
actor's locale, and today it has none. `CatalogEntityKind` covers only `TREATMENT_TYPE`,
|
||||||
|
`PROSTHESIS_TYPE` and `LAB_WORKFLOW_STEP` (`schema.prisma`), so `CatalogLabelService` cannot
|
||||||
|
resolve a category at all. The only category labels in the repo are the `prosthesis.category_*`
|
||||||
|
and `prosthesis.sub_*` keys in `frontend/messages/*.json`, plus a hardcoded single-locale map in
|
||||||
|
the dev export script. Sending bare codes would leave `crown` and `post_core` untranslated
|
||||||
|
beside fully-labelled leaves, on the locale this feature exists for.
|
||||||
|
|
||||||
|
So this branch adds them as first-class catalog entities:
|
||||||
|
|
||||||
|
- add `PROSTHESIS_CATEGORY` and `PROSTHESIS_SUBCATEGORY` to `CatalogEntityKind` — one
|
||||||
|
migration, no data loss, the enum is additive;
|
||||||
|
- seed `CatalogTranslation` rows for the 7 categories and the 5 subcategories in `fa`, `en` and
|
||||||
|
`nl`, taking the wording from the existing `prosthesis.category_*` / `prosthesis.sub_*` keys
|
||||||
|
so the two surfaces read identically on day one;
|
||||||
|
- resolve them through `CatalogLabelService` like every other catalog label. No label is
|
||||||
|
hardcoded in backend code, and the house rule holds unchanged.
|
||||||
|
|
||||||
|
The `sub_*` key set is wider than the subcategory set — it also carries technique names
|
||||||
|
(`sub_full_contour`, `sub_layered`) and two leaf codes. Seed only the five real subcategories.
|
||||||
|
|
||||||
|
> **Accepted consequence:** the frontend keeps its own `category_*` / `sub_*` message keys, so
|
||||||
|
> the wording now lives in two places. Migrating the frontend to read these labels from the API
|
||||||
|
> is **out of scope for this branch** — it would touch the prosthesis picker, the case sheet and
|
||||||
|
> the print layout, none of which this task otherwise opens. The seed is written from the message
|
||||||
|
> files precisely so the two agree at the point they diverge.
|
||||||
|
|
||||||
### Prosthesis work implies the treatment type
|
### Prosthesis work implies the treatment type
|
||||||
|
|
||||||
@@ -601,6 +644,15 @@ understand. Three of them carry candidates and become chips (§7).
|
|||||||
|
|
||||||
`tooth_not_selected` is removed — assignments now define their own teeth.
|
`tooth_not_selected` is removed — assignments now define their own teeth.
|
||||||
|
|
||||||
|
**An unresolved item that came from an assignment carries that assignment's index.**
|
||||||
|
Without it a chip is decorative: picking a quadrant for "دندون دو" on a prosthesis detail
|
||||||
|
produces a tooth with no job, which the *jobless tooth is left out* rule above then discards, so
|
||||||
|
the tap changes nothing. `tooth_missing_quadrant`, `arch_not_spoken` and
|
||||||
|
`prosthesis_type_ambiguous` therefore carry `assignmentIndex`, and a picked chip inherits that
|
||||||
|
assignment's `types[]` — or, for `prosthesis_type_ambiguous`, supplies the missing leaf to that
|
||||||
|
assignment's existing targets. An item raised outside any assignment (a tooth spoken in the
|
||||||
|
`teeth` array alone) carries no index and folds into `teeth` as it does today.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7. Review sheet
|
## 7. Review sheet
|
||||||
@@ -876,8 +928,13 @@ enabling this for real clinics.
|
|||||||
(§2). Still worth timing a realistic worst-case prosthesis dictation during item 1 to
|
(§2). Still worth timing a realistic worst-case prosthesis dictation during item 1 to
|
||||||
confirm 2 minutes is comfortable rather than tight.
|
confirm 2 minutes is comfortable rather than tight.
|
||||||
13. ~~**Availability API**~~ — **resolved for v1:** voice ships **open to everyone** with a
|
13. ~~**Availability API**~~ — **resolved for v1:** voice ships **open to everyone** with a
|
||||||
configured locale profile. No plan check, no availability endpoint. The `Plan.features`
|
configured locale profile. **No plan check.** The `Plan.features` design in §8 is deferred,
|
||||||
design in §8 is deferred, not dropped.
|
not dropped.
|
||||||
|
|
||||||
|
*Corrected 2026-09-07:* this item previously read "no availability endpoint", which
|
||||||
|
contradicted §4 and the shipped code. `GET /voice/availability` **does** exist and is what
|
||||||
|
`voiceForEditor` reads; it reports the configured locales and `maxRecordingMs`, and carries
|
||||||
|
no plan check. Only the plan gate is deferred, not the endpoint.
|
||||||
|
|
||||||
### New
|
### New
|
||||||
|
|
||||||
@@ -920,11 +977,19 @@ enabling this for real clinics.
|
|||||||
- an arch code aimed at a tooth, and a tooth code aimed at a jaw, both resolve to
|
- an arch code aimed at a tooth, and a tooth code aimed at a jaw, both resolve to
|
||||||
`code_not_valid_for_target`;
|
`code_not_valid_for_target`;
|
||||||
- a resolved assignment forces `treatmentType` to `prosthesis`;
|
- a resolved assignment forces `treatmentType` to `prosthesis`;
|
||||||
- a target with no types is reported and excluded, and does not fail the whole assignment.
|
- a target with no types is reported and excluded, and does not fail the whole assignment;
|
||||||
|
- a `removable` category defers its region check instead of emitting
|
||||||
|
`code_not_valid_for_target`;
|
||||||
|
- every one of the 7 categories and 5 subcategories resolves a non-empty label in `fa`, `en`
|
||||||
|
and `nl` — the check that catches a missed `CatalogTranslation` seed row;
|
||||||
|
- an unresolved item raised inside an assignment carries that assignment's index, and one
|
||||||
|
raised outside any assignment does not.
|
||||||
The pre-existing suites stay: `resolveToothIntent` in all four quadrants, out-of-range
|
The pre-existing suites stay: `resolveToothIntent` in all four quadrants, out-of-range
|
||||||
rejection, deciduous → unresolved, `resolveDueDate` per-locale week start, "this" vs "next",
|
rejection, deciduous → unresolved, `resolveDueDate` per-locale week start, "this" vs "next",
|
||||||
Jalali leap year and month-end, and connected-span validation.
|
Jalali leap year and month-end, and connected-span validation.
|
||||||
- `cd backend && npm run build` — cross-cutting backend gate.
|
- `cd backend && npm run build` — cross-cutting backend gate.
|
||||||
|
- `cd backend && npm run prisma:migrate && npm run prisma:seed` — the new `CatalogEntityKind`
|
||||||
|
values and their translation rows. The seed never wipes, so re-running it is safe.
|
||||||
- `cd frontend && npx vitest run` — **new**. One dev dependency, one config, one script,
|
- `cd frontend && npx vitest run` — **new**. One dev dependency, one config, one script,
|
||||||
covering the pure helpers only: `prosthesisTree.ts` (stack legality, `applyLeafToJobs`
|
covering the pure helpers only: `prosthesisTree.ts` (stack legality, `applyLeafToJobs`
|
||||||
precedence, `toothRegionColors`) and `voiceReviewRows.ts` (row availability, the merged
|
precedence, `toothRegionColors`) and `voiceReviewRows.ts` (row availability, the merged
|
||||||
@@ -963,8 +1028,11 @@ fa locale, editable day, prosthesis detail with a bridge, dispatch to a linked l
|
|||||||
- **jaw appliance** — "نایت گارد فک بالا" → the sheet shows an upper-jaw row, the treatment
|
- **jaw appliance** — "نایت گارد فک بالا" → the sheet shows an upper-jaw row, the treatment
|
||||||
type row reads *prosthesis* and is locked, and apply produces a `UA` row with no teeth;
|
type row reads *prosthesis* and is locked, and apply produces a `UA` row with no teeth;
|
||||||
- **jaw not spoken** — "نایت گارد" → Upper / Lower chips; tapping both produces `UA` and `LA`;
|
- **jaw not spoken** — "نایت گارد" → Upper / Lower chips; tapping both produces `UA` and `LA`;
|
||||||
- **material not spoken** — "دندون ۱۲ روکش" → chips for the nine crown leaves; nothing is
|
- **material not spoken** — "دندون ۱۲ روکش" → chips for the nine crown leaves, and the row
|
||||||
applied until one is picked;
|
reads *روکش* in Persian, not `crown`. Nothing is applied until one is picked;
|
||||||
|
- **quadrant chip on a prosthesis detail** — "دندون دو روکش زیرکونیا" → picking a quadrant chip
|
||||||
|
produces a tooth **carrying that assignment's job**, not a jobless tooth that is then
|
||||||
|
discarded;
|
||||||
- **tooth with no job** — "۱۲ و ۱۳، روکش پیافام برای ۱۲" → 13 is struck through in the sheet,
|
- **tooth with no job** — "۱۲ و ۱۳، روکش پیافام برای ۱۲" → 13 is struck through in the sheet,
|
||||||
apply adds only 12, and 13 is **not** silently saved and then deleted;
|
apply adds only 12, and 13 is **not** silently saved and then deleted;
|
||||||
- **illegal stack** — "دندون ۱۲ ایمپلنت و پست و کور" → the refused job is struck through and
|
- **illegal stack** — "دندون ۱۲ ایمپلنت و پست و کور" → the refused job is struck through and
|
||||||
@@ -1045,3 +1113,17 @@ Adapting to the overhauled treatment form, in a grilling session on 2026-09-07.
|
|||||||
| 44 | Frontend tests | Vitest added for the pure helpers. The split between backend and frontend resolvers becomes a judgement rather than a constraint (§6, §12) |
|
| 44 | Frontend tests | Vitest added for the pure helpers. The split between backend and frontend resolvers becomes a judgement rather than a constraint (§6, §12) |
|
||||||
| 45 | Recording defects | Carried in this branch: the container fallback that refused Safari, and the render gate that never checked `isMediaRecorderSupported()` (§2, §9) |
|
| 45 | Recording defects | Carried in this branch: the container fallback that refused Safari, and the render gate that never checked `isMediaRecorderSupported()` (§2, §9) |
|
||||||
| 46 | Delivery | One merge request |
|
| 46 | Delivery | One merge request |
|
||||||
|
|
||||||
|
Closing the gaps the `/orchestrate` surveyor found on 2026-09-07, before any code was written.
|
||||||
|
|
||||||
|
| # | Question | Decision |
|
||||||
|
|---|---|---|
|
||||||
|
| 47 | Category labels | Add `PROSTHESIS_CATEGORY` and `PROSTHESIS_SUBCATEGORY` to `CatalogEntityKind`, one migration, translations seeded from the existing `prosthesis.category_*` / `sub_*` message keys, resolved through `CatalogLabelService`. Bare codes were the alternative and would have been weakest on `fa` (§5) |
|
||||||
|
| 48 | Frontend label source | Out of scope. The frontend keeps its own message keys; the wording lives in two places by choice, and the seed is written from the message files so they agree (§5) |
|
||||||
|
| 49 | Mixed-region categories | A category whose leaves span `crown` and `arch` — only `removable` today — defers its region check to the picked leaf, and is never reported as `code_not_valid_for_target` while still a category (§5) |
|
||||||
|
| 50 | Chips that came from an assignment | `tooth_missing_quadrant`, `arch_not_spoken` and `prosthesis_type_ambiguous` carry `assignmentIndex`, so a picked chip inherits that assignment's jobs. Without it the chip resolves to a jobless tooth, which decision 40 discards — a chip that does nothing (§6) |
|
||||||
|
|
||||||
|
Corrections to the v1 text found in the same pass: the endpoint is `POST /voice/extract`, not
|
||||||
|
`/treatments/voice-extract` (§3); `GET /voice/availability` does exist and only the plan check is
|
||||||
|
deferred (§11 item 13); the catalog has 5 subcategories, not 4, and the disjointness test asserts
|
||||||
|
against the live catalog rather than a written count (§5).
|
||||||
|
|||||||
Reference in New Issue
Block a user