Story-2919 · pramaan-app · 28 Jul 2026
The list teaches itself
The Add-Matter drawer’s second question — Who do we represent? — already lets counsel pick a participant label or type a brand-new one (Story-2917). But a label chosen against a matter type that has never seen it was a one-time answer: the next matter of that type started from the same blank. This Story makes the choice stick. Picking or minting a label while creating a matter quietly curates it onto that matter type, at the end of its list — so the firm’s vocabulary builds itself from real work, and the next opener sees the label already offered.
The one idea
POST /app/matters body; the backend
resolves-or-creates it and attaches it to the matter type inside the create
transaction (Story-3055). So a cancelled drawer leaves no label and no attachment
behind (MMW-27), and because the write is a side-effect of matter creation
(matters:write), any matter-creator — editor or admin
— may attach and mint, with no settings:write (OQ-3).
Why “never on click” is the whole point
The tempting shortcut is to POST the new label the instant the user commits the inline mint, the way A1 shipped it. That is wrong twice over, and this Story fixes both:
| The bug in on-click mint | Why it fails |
|---|---|
| Orphans on cancel. | A label written the moment you type it survives when you close the drawer without creating anything — a firm-catalogue row nobody asked to keep. Matter types mint the same way and already defer to Create; participant labels now match. |
| Wrong permission. |
Every participant-label write endpoint — create,
attach, create-and-attach — is
settings:write, the curation editor’s gate. Driving the
drawer through them would block an ordinary editor from creating a matter with a new
label, violating OQ-3. The A1 on-click call used useAppCreateParticipantLabel
— a settings:write write — so it was already a latent OQ-3
defect. It is removed here.
|
The trace
For an existing pick the party carries participantLabelId instead of a
name, and the backend attaches by id. Either way the drawer sends one thing — the
chosen label on the us party — and the server does the resolve + attach.
Two kinds of chosen label
Sends participantLabelId (+ name). Backend attaches that id to the matter
type if it isn’t already offered.
Sends participantLabelName only. Backend resolves-or-creates then attaches
— all at Create, all under matters:write.
Duplication can’t sneak in (layer 2)
The mint row never manufactures a twin. Typing a name that exactly matches a live label selects that label instead of minting (no id churn). Typing a name that matches an archived label offers a Restore — the backend restores the archived row rather than creating a look-alike (layer 3, NFKC dedup). Archived rows are fetched only to power this affordance; they are never listed as pickable options.
“Intervener” was archived — it will be restored, not duplicated.
What shipped (app side)
| Piece | What it does |
|---|---|
add-matter-drawer-types.ts |
New DrawerParticipantLabelSelection — an existing pick
(carries the catalogue id) or a staged mint (name only). Mirrors
DrawerTypeSelection’s staged pattern. RepresentingSideValue
now holds this selection.
|
representing-side-field.tsx |
Removed the optimistic on-click create + its POST, reconciliation refs, and failure
toast (the A1 latent settings:write bug). Minting now just stages
{ kind: "staged", name } — synchronous, no write, no orphan.
|
use-participant-label-picker-data.ts |
Dropped useAppCreateParticipantLabel and the createLabel seam
entirely — the drawer reads only. Catalogue now fetched with
includeArchived: true so the mint row can offer restore.
|
participant-label-picker.tsx |
Trigger + highlight read the selection (staged shows its name, highlights no row). The mint row detects an archived-name match and switches its commit to Restore with an explanatory hint; live-name match still selects, never mints. |
live-matters-wizard-payload.ts + add-matter-launcher.tsx
|
participantLabelId is now optional on the adversarial input: a pick sends
id + name, a staged mint sends name only. The launcher maps the selection kind; no
client-side label create/rollback remains.
|
| i18n — en + hi |
AddMatterDrawer.restoreParticipantLabel and
participantLabelArchivedHint added in both locales.
|
| Stories + tests |
New StagedMint (picker + field) and ArchivedRestore (picker)
stories. Unit tests rewritten: staged-mint selects synchronously with no write, switching
to Advisory drops it, an archived name offers Restore, and the payload sends name-only for
a staged mint.
|
The backend half (Story-3055, pramaan-functions)
The attach itself is server work — the app cannot (and per OQ-3 must not) call a
settings:write endpoint from the drawer. MatterService.create_matter
(already matters:write) reuses the primitives Story-2918 built —
ParticipantLabelService.resolve_or_create_for_mapping (NFKC dedup /
archived-restore) and MatterTypeParticipantLabelService._attach_mapping
(idempotent, max(ordering)+1 → end of the curated list, D2c) — as a
side-effect of creation. The matter-create wire shape is unchanged, so no codegen churn: the
app already sends the label on the us party.
Verification
pnpm lint — 0 errors.
pnpm typecheck — clean.
pnpm test — green (1637 tests), incl. the staged-mint, drop-on-Advisory,
archived-restore, and payload-name-only tests.
pnpm build — succeeds.
End-to-end (system boundary). The full “next opener sees it
curated” behaviour requires Story-3055 to be deployed to the target environment,
not merely merged to functions main. Against a build without #3055 live, a
staged mint of a genuinely new name is rejected by the create validator (unknown name) —
so this app change lands paired with #3055 and the UAT below runs once #3055 is live.
UAT (once Story-3055 is live)
| Step | Expected |
|---|---|
As an editor (no settings:write), open Add-Matter, pick an
uncurated matter type, choose Adversarial, mint a new label, Create. |
✓ Matter created; reopening the drawer on that matter type offers the new label, curated at the end. |
| Open Add-Matter, mint a label in Q2, then press Cancel. | ✓ No label appears in the firm catalogue and no attachment is added. |
| In Q2’s mint row, type the exact name of an archived label. | ✓ The commit reads Restore; creating restores that label rather than making a duplicate. |
Files changed
13 file(s), +238 / -183 lines
| Path | Role | + | - |
|---|---|---|---|
| messages/en.json | Config | +2 | -0 |
| messages/hi.json | Config | +2 | -0 |
| src/components/add-matter/add-matter-drawer-types.ts | Component | +18 | -3 |
| src/components/add-matter/participant-label-picker.stories.tsx | Component | +30 | -0 |
| src/components/add-matter/participant-label-picker.tsx | Component | +53 | -15 |
| src/components/add-matter/representing-side-field.stories.tsx | Component | +14 | -7 |
| src/components/add-matter/representing-side-field.tsx | Component | +14 | -52 |
| src/components/add-matter/use-participant-label-picker-data.ts | Component | +13 | -41 |
| src/components/matters/add-matter-launcher.tsx | Component | +10 | -2 |
| src/components/matters/live-matters-wizard-payload.ts | Component | +14 | -4 |
| tests/unit/live-matters-wizard-payload.test.ts | Test | +22 | -0 |
| tests/unit/representing-side-field.test.tsx | Test | +45 | -59 |
| tests/unit/use-add-matter-drawer-form.test.tsx | Test | +1 | -0 |