feat(voice): adapt voice entry to the stacked-jobs prosthesis model
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>
This commit is contained in:
@@ -22,25 +22,25 @@ Status legend: ⬜ not started · 🟡 in progress · ✅ done · ⛔ blocked
|
||||
|
||||
| # | Work item | Repo | Status | Notes / refs |
|
||||
|---|-----------|------|--------|--------------|
|
||||
| 1 | `pickRecordingMimeType` falls back to the empty hint instead of `null` | `dyolink` | ⬜ | §9. The Safari failure |
|
||||
| 2 | `voiceForEditor` also checks `isMediaRecorderSupported()` | `dyolink` | ⬜ | §2 render policy, `TreatmentWorkspace.tsx` |
|
||||
| 3 | Add Vitest for the frontend's pure helpers; update `CLAUDE.md` | `dyolink` | ⬜ | §12. One dev dep, one config, one script |
|
||||
| 4 | `buildCatalog` passes `category` / `subcategory` / `chartRegion` / `stackGroup` through | `dyolink` | ⬜ | §5. `voice.service.ts` already receives all four |
|
||||
| 5 | Wire schema: `prosthesisAssignments`, `arch: 'both'`; drop default + overrides | `dyolink` | ⬜ | §5, `extraction.wire.ts` |
|
||||
| 6 | Resolver: assignments, arch derivation, leaf-vs-category classification | `dyolink` | ⬜ | §5, §6, `extraction.resolver.ts` |
|
||||
| 7 | Resolver: new unresolved reasons; retire `tooth_not_selected` | `dyolink` | ⬜ | §6 reason table |
|
||||
| 8 | Resolver: a resolved assignment forces `treatmentType` to `prosthesis` | `dyolink` | ⬜ | §5. Only `prosthesis` is `labDependent` |
|
||||
| 9 | Prompt: present the catalog as a tree; teach stacks and jaw-level codes | `dyolink` | ⬜ | §5, `extraction.prompt.ts` |
|
||||
| 10 | Backend Jest suites for items 6–8, including the namespace-disjointness assertion | `dyolink` | ⬜ | §12 |
|
||||
| 11 | Frontend types follow the new `ResolvedExtraction` | `dyolink` | ⬜ | `types/voice.ts` |
|
||||
| 12 | `voiceReviewRows`: merged row, chip folding, retire `complete` as a blocker | `dyolink` | ⬜ | §6, §7 |
|
||||
| 13 | `VoiceReviewSheet`: merged row, chart colours, three chip kinds | `dyolink` | ⬜ | §7. `crownColors` / `rootColors` / `archHighlight` already exist |
|
||||
| 14 | `applyVoiceResult` writes through `applyLeafToJobs`; handles arch rows | `dyolink` | ⬜ | §6, §7 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 1 | `pickRecordingMimeType` falls back to the empty hint instead of `null` | `dyolink` | ✅ | §9. `frontend/src/lib/voice/audioFormat.ts` — the final `return null` after the loop is now `return ''` |
|
||||
| 2 | `voiceForEditor` also checks `isMediaRecorderSupported()` | `dyolink` | ✅ | §2 render policy, `TreatmentWorkspace.tsx` |
|
||||
| 3 | Add Vitest for the frontend's pure helpers; update `CLAUDE.md` | `dyolink` | ✅ | §12. `vitest@3.2.7`, `frontend/vitest.config.ts`, `npm run test` script; `CLAUDE.md` Tests section rewritten |
|
||||
| 4 | `buildCatalog` passes `category` / `subcategory` / `chartRegion` / `stackGroup` through | `dyolink` | ✅ | §5. `voice.service.ts` now forwards the full `ProsthesisTypeCatalogEntry[]` plus category/subcategory lists |
|
||||
| 5 | Wire schema: `prosthesisAssignments`, `arch: 'both'`; drop default + overrides | `dyolink` | ✅ | §5, `extraction.wire.ts` — `prosthesis: WireProsthesisAssignment[]` |
|
||||
| 6 | Resolver: assignments, arch derivation, leaf-vs-category classification | `dyolink` | ✅ | §5, §6, `extraction.resolver.ts` — `resolveProsthesisAssignment`, `resolveAssignmentTarget`, `classifyTypeCode` |
|
||||
| 7 | Resolver: new unresolved reasons; retire `tooth_not_selected` | `dyolink` | ✅ | §6 reason table — `voice.types.ts` |
|
||||
| 8 | Resolver: a resolved assignment forces `treatmentType` to `prosthesis` | `dyolink` | ✅ | §5. `resolveVoiceIntent` — `hasResolvedAssignment` |
|
||||
| 9 | Prompt: present the catalog as a tree; teach stacks and jaw-level codes | `dyolink` | ✅ | §5, `extraction.prompt.ts` — `prosthesisTree()` renders CATEGORY/SUBCATEGORY/leaf from data, no hardcoded catalog knowledge |
|
||||
| 10 | Backend Jest suites for items 6–8, including the namespace-disjointness assertion | `dyolink` | ✅ | §12. `extraction.resolver.spec.ts` — disjointness test reads `catalog-seed-data.ts`'s live `PROSTHESIS_TYPES`, not a written count |
|
||||
| 11 | Frontend types follow the new `ResolvedExtraction` | `dyolink` | ✅ | `types/voice.ts` — `VoiceProsthesisAssignment[]`, `assignmentIndex` |
|
||||
| 12 | `voiceReviewRows`: merged row, chip folding, retire `complete` as a blocker | `dyolink` | ✅ | §6, §7 — `prosthesisTargetLines`, `joblessProsthesisTargets`, `withChosenArch`/`withChosenProsthesisLeaf` |
|
||||
| 13 | `VoiceReviewSheet`: merged row, chart colours, three chip kinds | `dyolink` | ✅ | §7. Merged "Teeth and prosthesis" row; tooth / jaw / material chip kinds |
|
||||
| 14 | `applyVoiceResult` writes through `applyLeafToJobs`; handles arch rows | `dyolink` | ✅ | §6, §7 — via `prosthesisTargetLines`, which routes every stack through `applyLeafToJobs` |
|
||||
| 15 | Vitest specs for `prosthesisTree.ts` and `voiceReviewRows.ts` | `dyolink` | ✅ | §12. 37 tests total, `npx vitest run` green |
|
||||
| 16 | New user-visible strings in `en.json`, `fa.json`, `nl.json` | `dyolink` | ✅ | i18n is mandatory, not a follow-up — new `voiceUnresolved.*` reasons, `voiceTeethAndProsthesis`, `voicePickJaw`, `voiceNoProsthesisHeard`, `voiceStackRefused`; retired `voiceProsthesisIncomplete` (all-or-nothing gone) |
|
||||
| 17 | Run every gate in §12, then the manual pass including Safari and iPad | `dyolink` | 🟡 | Machine gates green (below). Manual pass and `prisma:migrate`/`prisma:seed` against a live DB **not run** — this sandbox has no working Docker daemon (see note below) |
|
||||
| 18 | `PROSTHESIS_CATEGORY` + `PROSTHESIS_SUBCATEGORY` in `CatalogEntityKind`; migration; seed fa/en/nl translations | `dyolink` | 🟡 | §5, decision 47. Enum + migration SQL + seed data all written and `prisma generate` succeeded; migration **not applied** to a running Postgres (Docker unavailable) |
|
||||
| 19 | Unresolved items carry `assignmentIndex`; a picked chip inherits that assignment's jobs | `dyolink` | ✅ | §6, decision 50. `UnresolvedItem.assignmentIndex`; `VoiceReviewSheet` chips inherit via `withChosenTeeth(...,index)` / `withChosenArch` / `withChosenProsthesisLeaf` |
|
||||
|
||||
## Key decisions
|
||||
|
||||
@@ -261,3 +261,183 @@ no code to revisit.
|
||||
| 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.
|
||||
|
||||
---
|
||||
|
||||
## 2026-09-07 — Implementation (dyolink, extraction contract + resolvers + review sheet)
|
||||
|
||||
All 19 work items above are built. Nothing committed or pushed — that is the orchestrator's job.
|
||||
|
||||
### Backend
|
||||
|
||||
- `backend/prisma/schema.prisma` — `PROSTHESIS_CATEGORY`, `PROSTHESIS_SUBCATEGORY` added to
|
||||
`CatalogEntityKind` (additive, no data loss).
|
||||
- `backend/prisma/migrations/20260907120000_prosthesis_category_catalog_kinds/migration.sql` —
|
||||
hand-written `ALTER TYPE ... ADD VALUE` migration, following the exact pattern of the repo's
|
||||
one precedent (`20260718180000_lab_case_activity_task_assigned`). **Not applied** — see
|
||||
"Not run" below.
|
||||
- `backend/prisma/catalog-seed-data.ts` — `PROSTHESIS_CATEGORY_LABELS` (7) and
|
||||
`PROSTHESIS_SUBCATEGORY_LABELS` (5), worded from the frontend's existing `category_*`/`sub_*`
|
||||
message keys (decision 47/48), folded into `CATALOG_TRANSLATIONS`.
|
||||
- `backend/src/common/fdi.ts` — `ARCH_TOOTH_UPPER`/`ARCH_TOOTH_LOWER` sentinels, mirroring the
|
||||
frontend's `prosthesisTree.ts` convention so both sides speak the same jaw target.
|
||||
- `backend/src/modules/voice/voice.types.ts` — `ProsthesisAssignment` replaces
|
||||
`ProsthesisIntent` (default+overrides retired, decision 35); `UnresolvedReason` gains
|
||||
`prosthesis_type_ambiguous`, `arch_not_spoken`, `code_not_valid_for_target`, loses
|
||||
`tooth_not_selected`; `UnresolvedItem.assignmentIndex` added (decision 50).
|
||||
- `backend/src/modules/voice/extraction.wire.ts` — wire schema carries `prosthesis:
|
||||
WireProsthesisAssignment[]`; `WireToothIntent.arch` gains `'both'`.
|
||||
- `backend/src/modules/voice/extraction.resolver.ts` — rewritten: `resolveAssignmentTarget`
|
||||
(tooth vs. jaw, mirrors the old `tooth_missing_quadrant`/new `arch_not_spoken` split),
|
||||
`classifyTypeCode` (leaf / category / subcategory, disjoint namespaces), and
|
||||
`resolveProsthesisAssignment` composing both plus the region-validity check (deferred for a
|
||||
mixed-region category — `removable` today, decision 49 — computed generically from the
|
||||
catalog's own chart regions rather than hardcoding the category name). `resolveVoiceIntent`
|
||||
forces `treatmentType` to `prosthesis` when any assignment resolves a target (decision 41).
|
||||
`resolveProsthesis`/`ResolvedProsthesis` retired outright.
|
||||
- `backend/src/modules/voice/extraction.prompt.ts` — `prosthesisTree()` renders the catalog as
|
||||
CATEGORY → (SUBCATEGORY →) leaf from the data `buildCatalog` supplies; no catalog knowledge
|
||||
hardcoded in the prompt text itself.
|
||||
- `backend/src/modules/voice/voice.providers.ts`, `voice.service.ts` — `ExtractionCatalog`
|
||||
carries the full leaf shape plus `prosthesisCategories`/`prosthesisSubcategories`;
|
||||
`buildCatalog` no longer strips `category`/`subcategory`/`chartRegion`/`stackGroup`.
|
||||
- `backend/src/modules/prosthesis-catalog/prosthesis-catalog.service.ts` —
|
||||
`listCategories()`/`listSubcategories()`, resolved through `CatalogLabelService` like every
|
||||
other catalog label.
|
||||
- Jest: `extraction.resolver.spec.ts` rewritten around `resolveProsthesisAssignment` (stacks,
|
||||
jaw targets, both-jaws, leaf/category/subcategory classification, disjointness against the
|
||||
live `PROSTHESIS_TYPES`, region validity both ways, the `removable` deferral, assignment-index
|
||||
attribution); `extraction.wire.spec.ts` and `openrouter.provider.spec.ts` updated for the new
|
||||
wire shape. 209 backend tests pass (121 in `modules/voice`).
|
||||
|
||||
### Frontend
|
||||
|
||||
- `frontend/src/lib/voice/audioFormat.ts` — `pickRecordingMimeType`'s final fallback is now `''`
|
||||
instead of `null` (item 1's Safari fix — modern Safari's `isTypeSupported` can reject every
|
||||
preferred container yet still record when let choose).
|
||||
- `frontend/src/components/ui/treatment/TreatmentWorkspace.tsx` — `voiceForEditor` now also
|
||||
requires `isMediaRecorderSupported()`; `applyVoiceResult` rewritten: teeth vs. prosthesis are
|
||||
mutually exclusive per `isLabDependentResult`, the prosthesis stack is built through
|
||||
`prosthesisTargetLines` (which routes every leaf through `applyLeafToJobs`), and jaw targets
|
||||
write `LabCaseToothProsthesisDraft` rows keyed on the `UA`/`LA` sentinels — no separate
|
||||
arch-specific code path needed beyond what `prosthesisTree.ts` already provides.
|
||||
- `frontend/src/types/voice.ts` — `VoiceProsthesisAssignment[]` replaces the byTooth map;
|
||||
`VoiceUnresolvedItem.assignmentIndex`.
|
||||
- `frontend/src/components/treatment/voiceReviewRows.ts` — rewritten: `isLabDependentResult`,
|
||||
merged-row `voiceRowAvailability`, `withChosenArch`/`withChosenProsthesisLeaf` (decision 50),
|
||||
`prosthesisTargetLines` (previews the stack via `applyLeafToJobs`, names refused jobs),
|
||||
`joblessProsthesisTargets` (decision 40 — named, struck through, never silently dropped or
|
||||
silently applied), `prosthesisChartData` (crown/root/arch tints for the merged row's chart).
|
||||
- `frontend/src/components/ui/treatment/VoiceReviewSheet.tsx` — merged "Teeth and prosthesis"
|
||||
row for a labDependent type; three independent candidate-chip kinds (tooth, jaw, leaf) each
|
||||
folding through their own `voiceReviewRows` helper; `voiceProsthesisIncomplete` warning
|
||||
removed (all-or-nothing retired).
|
||||
- i18n: `en.json`/`fa.json`/`nl.json` — `voiceUnresolved.*` updated for the new/retired reasons,
|
||||
`voiceTeethAndProsthesis`, `voicePickJaw`, `voiceNoProsthesisHeard`, `voiceStackRefused` added,
|
||||
`voiceProsthesisIncomplete` removed.
|
||||
- `frontend/package.json`, `frontend/vitest.config.ts` — `vitest@3.2.7` (pinned to a version
|
||||
whose peer `@types/node` range still includes the repo's `^20`; vitest 4/5 require `>=22`),
|
||||
`npm run test` → `vitest run`, alias-only config (`@` → `src/`).
|
||||
- `frontend/src/components/treatment/prosthesisTree.spec.ts`,
|
||||
`voiceReviewRows.spec.ts` — 37 Vitest cases covering stack legality, `applyLeafToJobs`
|
||||
precedence, `toothRegionColors`, row availability, chip folding, the merged-row preview, and
|
||||
the jobless/pending distinction.
|
||||
- `CLAUDE.md` — Tests section rewritten; frontend command table gains `npx vitest run`.
|
||||
|
||||
### Verification run
|
||||
|
||||
- `cd backend && npm test` — 209/209 pass (121 in `modules/voice`).
|
||||
- `cd backend && npm run build` — clean (after `npm install`, which pulled in `@sentry/nestjs`
|
||||
that `node_modules` was missing — unrelated to this change, pre-existing on this checkout).
|
||||
- `cd backend && npx prisma generate` — succeeds against the updated schema (no DB needed);
|
||||
confirms `CatalogEntityKind.PROSTHESIS_CATEGORY`/`PROSTHESIS_SUBCATEGORY` compile everywhere
|
||||
they're used.
|
||||
- `cd frontend && npx tsc --noEmit` — clean.
|
||||
- `cd frontend && npx vitest run` — 37/37 pass.
|
||||
- `cd frontend && npm run build` — production build succeeds.
|
||||
- ESLint on every touched file — 0 errors, 0 new warnings (pre-existing warnings elsewhere in
|
||||
`TreatmentWorkspace.tsx`, unrelated to this change, left untouched).
|
||||
|
||||
### Not run (environment limitation, not a design gap)
|
||||
|
||||
- `npm run prisma:migrate && npm run prisma:seed` against a live Postgres — this sandbox has no
|
||||
running Docker daemon (`docker info` never came up after several minutes and `open -a Docker`
|
||||
did not launch it), so the migration was never applied to a database and the new
|
||||
`CatalogTranslation` rows were never seeded. The migration SQL and seed data are written and
|
||||
reviewed against the one existing precedent in this repo; **run both before merging**.
|
||||
- The full manual pass in §12 (Safari/iPad recording, live extraction against the real OpenRouter
|
||||
API, the specific stack/jaw/ambiguity scenarios) — needs a browser and a live backend, neither
|
||||
available in this session.
|
||||
|
||||
### Deviations from spec / judgement calls made while implementing
|
||||
|
||||
- **A target with empty `types[]` still resolves as a target**, with `types: []` on its
|
||||
assignment — not excluded from the assignment's `targets` array. The sheet (frontend) treats
|
||||
`types.length === 0` with no matching `prosthesis_type_ambiguous` unresolved item as "jobless,
|
||||
struck through" (decision 40), and the same empty-types-plus-ambiguous-item combination as
|
||||
"pending a material pick" instead. This keeps the wire contract simple (no extra field) at the
|
||||
cost of the frontend doing that one bit of inference from `unresolved` — documented in both
|
||||
`extraction.resolver.ts` and `voiceReviewRows.ts`.
|
||||
- **The mixed-region deferral (decision 49) is computed generically** from each category's
|
||||
actual leaf chart-regions (`regions.size === 1` → validate immediately, else defer) rather than
|
||||
special-cased for `removable` by name. This also defers `implant` (which spans `root` and
|
||||
`crown` via `screw_retained`) — the spec's prose says "only `removable` today" as an
|
||||
observation about the current catalog, not an instruction to hardcode that name, and deferring
|
||||
a category no test forbids deferring is the safer default.
|
||||
- **`voice.dto.ts`/`voice.controller.ts` needed no changes.** The scout's item 6 ("validation for
|
||||
new schema shape") does not apply: the DTO validates the client's audio submission
|
||||
(`audio`/`format`/`timeZone`/`durationMs`/`locale`), which is unrelated to the LLM's structured
|
||||
output shape that changed. Confirmed by reading both files; not a silent skip.
|
||||
|
||||
---
|
||||
|
||||
## 2026-09-07 — Re-run reached Ship; stopped, and the challenge phase is incomplete
|
||||
|
||||
Recorded by hand: the `record-stop:Ship` agent failed on the session spend limit before it
|
||||
could write this.
|
||||
|
||||
**Stop reason:** `no MR template found in dyolink — refusing to invent a description.` Expected
|
||||
and flagged at preflight: `.gitea/` holds only `workflows/`, and the remote is Gitea, not GitLab.
|
||||
A `--dry` run pushes nothing regardless.
|
||||
|
||||
| Phase | Result |
|
||||
|---|---|
|
||||
| Gap-check | **clear** — the five gaps closed in `77e2ed4` were accepted |
|
||||
| Scout | 1 of 3 Explore sweeps returned; 2 ended without structured output |
|
||||
| Implement | **done** — all 19 work items, ~3,100 insertions across 25 files + 4 new files |
|
||||
| Gate | **green** — backend 16 suites / 209 tests, `nest build` 0, `prisma validate` ok; frontend Vitest 2 files / 37 tests, `tsc --noEmit` 0, `next build` 0 |
|
||||
| Refute | **incomplete** — `correctness` refuted with 2 findings; `regression-risk` never ran (spend limit) |
|
||||
| Ship | stopped, no template. `clerk` also failed on the spend limit |
|
||||
|
||||
Gate repairs: 0. Referee relays: 0. Nothing committed, nothing pushed.
|
||||
|
||||
### Two confirmed findings — verified by hand, not taken on the critic's word
|
||||
|
||||
1. **`VoiceReviewSheet.tsx:169` — a picked tooth chip is silently dropped.** `pickCandidate`
|
||||
sets `teeth: prev.teeth || available.teeth`, but `available` is memoised from `effective`,
|
||||
which depends on `chosenTeeth`. Both `setChosenTeeth` and `setSelection` run in the same
|
||||
handler, so the updater closes over the pre-pick `available`, where `available.teeth` is
|
||||
`false` because `result.teeth` is empty. `prev.teeth` is false too, so it stays false
|
||||
permanently. The row then renders with the tooth on the chart and the box unticked, and Apply
|
||||
drops it. This is a variant of the original live-test failure — "ترمیم برای دندون دو".
|
||||
2. **`VoiceReviewSheet.tsx:213` + `TreatmentWorkspace.tsx:2215` — decision 41 not implemented.**
|
||||
The treatmentType row is a plain `toggle('treatmentType')` with no lock, and
|
||||
`applyVoiceResult` derives `labDependent` from `result.treatmentType` rather than the
|
||||
`detail.treatmentType` it writes. Untick the type row on a forced-prosthesis recording and a
|
||||
`restoration` detail is saved carrying prosthesis lab rows — the state decision 41 exists to
|
||||
make unreachable.
|
||||
|
||||
### Lint
|
||||
|
||||
Backend touched files: 0 errors, 0 warnings. Frontend touched files: 10 warnings, all
|
||||
pre-existing in `TreatmentWorkspace.tsx`. The repo-wide backend baseline (1,288 errors, 1,107
|
||||
prettier-fixable) is untouched by this diff.
|
||||
|
||||
## Next steps / open questions
|
||||
|
||||
- The diff is **unreviewed on the `regression-risk` lens**. Green gate plus one refuting lens is
|
||||
not the design's bar; the script continued only because the threshold counts refusals and the
|
||||
second critic errored rather than refused.
|
||||
- Fix the two findings, then resume from Refute so both lenses grade the same diff.
|
||||
- The Ship phase cannot pass in this repo until there is an MR template, or until the MR is
|
||||
opened by hand. Opening one is gated regardless.
|
||||
|
||||
Reference in New Issue
Block a user