Six refusal surfaces put the kernel's error code where the headline goes, so a person who hits one
reads SCHEMA_CHANGE_WOULD_ORPHAN or BATCH_NOT_IN_ANNOTATION as the first and largest thing on
the notice. The shared vocabulary that exists to prevent this is already imported by four of the
six, and used for the body text directly underneath the code.
All citations are at 2e4346d.
The rule this breaks
The ui-capabilities skill lists it fourth among its banned patterns: refusals render through the
shared code-to-prose vocabulary, one map product-wide, and a bare BATCH_NOT_IN_ANNOTATION badge is
not a message. data/refusals.ts is that map. Its own docstring records why it exists — a refusal
used to reach the user in three different shapes depending on the screen, and the bare-code sites
were named as the ones worth eliminating, because a kernel identifier in front of a person is not an
error message.
REFUSAL_PROSE has entries for the codes these six surfaces actually produce, including
SCHEMA_CHANGE_WOULD_ORPHAN, DESTRUCTIVE_SCHEMA_CHANGE, STALE_WRITE, BATCH_NOT_IN_ANNOTATION
and ASSET_NOT_WRITABLE. Nothing needs to be written for the common paths; the map needs to be
reached one line higher up.
The six sites, in two grades
Four render prose in the body and a machine code as the title:
| file |
line |
body |
frontend/ui-core/src/annotator/AnnotationPage.tsx |
2714 |
refusalProse(openingRefusal) |
frontend/ui-core/src/annotator/AnnotationPage.tsx |
2733 |
refusalProse(save.error) |
frontend/ui-core/src/annotator/AnnotationPage.tsx |
2763 |
refusalProse(actionRefusal) |
frontend/ui-core/src/screens/SchemaEditor.tsx |
812 |
refusalProse(publish.error) |
These are half-converted: somebody reached the vocabulary for the sentence and left the identifier
above it, which is the shape the map's docstring describes as the thing being removed one screen at
a time.
Two render the raw kernel sentence in the body as well, and are the worse pair:
| file |
line |
body |
frontend/ui-core/src/screens/SchemaEditor.tsx |
796 |
{draftFailure.message} |
frontend/ui-core/src/annotator/AddClassDialog.tsx |
732 |
{failure.message} |
ErrorBody.message's own field description states that its wording is not part of the contract, and
several kernel messages interpolate a project UUID or name an SDK keyword argument, so these two can
put both a machine code and an internal identifier in front of somebody at once.
The design question this issue has to answer
AlertProps.title is optional (frontend/ui-core/src/primitives/Badge.tsx:53), so there are two
honest shapes and this issue should pick one rather than leave each call site to choose:
- No title. The prose is the whole notice. Simplest, and correct wherever the sentence stands
alone.
- A prose headline. A short human phrase above the sentence, for the surfaces where the notice
competes with other content and needs a scannable line — AnnotationPage's save refusal already
does this inside its body, with This frame could not be saved above the prose.
Whichever is chosen, the code stops being the headline. Where a code genuinely helps somebody
filing a bug report, it belongs somewhere a person can copy without it being the first thing they
read.
Acceptance criteria
- No user-facing surface renders an error
code as its title, headline, or badge text. A grep for
title={.*\.code} across frontend/ returns nothing.
- The two sites rendering
message verbatim render prose from the shared map instead, so no project
UUID and no SDK keyword spelling can reach a person through them.
- Every refusal these six surfaces can produce has a
REFUSAL_PROSE entry, or falls through to the
server message deliberately, which the map already documents as its intended behaviour for codes
whose server wording is good.
- One test per converted surface forces the refusal and asserts the person sees prose rather than an
identifier. The ui-capabilities skill already requires a refusal-rendering test for every
mutation touched, so these largely exist and need their assertions tightened rather than written.
Notes
This was found while auditing the schema class-removal flow (#588), which requires that refusals
still reaching the UI render through the shared vocabulary. It is filed separately because five of
the six sites are outside the schema editor and the pattern spans the annotator, so correcting it
inside that flow would fix one screen and leave the rule with nowhere to live — the same failure the
refusal map was created to end. #588 is not settled by this issue and remains open on its own terms.
Six refusal surfaces put the kernel's error code where the headline goes, so a person who hits one
reads
SCHEMA_CHANGE_WOULD_ORPHANorBATCH_NOT_IN_ANNOTATIONas the first and largest thing onthe notice. The shared vocabulary that exists to prevent this is already imported by four of the
six, and used for the body text directly underneath the code.
All citations are at
2e4346d.The rule this breaks
The
ui-capabilitiesskill lists it fourth among its banned patterns: refusals render through theshared code-to-prose vocabulary, one map product-wide, and a bare
BATCH_NOT_IN_ANNOTATIONbadge isnot a message.
data/refusals.tsis that map. Its own docstring records why it exists — a refusalused to reach the user in three different shapes depending on the screen, and the bare-code sites
were named as the ones worth eliminating, because a kernel identifier in front of a person is not an
error message.
REFUSAL_PROSEhas entries for the codes these six surfaces actually produce, includingSCHEMA_CHANGE_WOULD_ORPHAN,DESTRUCTIVE_SCHEMA_CHANGE,STALE_WRITE,BATCH_NOT_IN_ANNOTATIONand
ASSET_NOT_WRITABLE. Nothing needs to be written for the common paths; the map needs to bereached one line higher up.
The six sites, in two grades
Four render prose in the body and a machine code as the title:
frontend/ui-core/src/annotator/AnnotationPage.tsxrefusalProse(openingRefusal)frontend/ui-core/src/annotator/AnnotationPage.tsxrefusalProse(save.error)frontend/ui-core/src/annotator/AnnotationPage.tsxrefusalProse(actionRefusal)frontend/ui-core/src/screens/SchemaEditor.tsxrefusalProse(publish.error)These are half-converted: somebody reached the vocabulary for the sentence and left the identifier
above it, which is the shape the map's docstring describes as the thing being removed one screen at
a time.
Two render the raw kernel sentence in the body as well, and are the worse pair:
frontend/ui-core/src/screens/SchemaEditor.tsx{draftFailure.message}frontend/ui-core/src/annotator/AddClassDialog.tsx{failure.message}ErrorBody.message's own field description states that its wording is not part of the contract, andseveral kernel messages interpolate a project UUID or name an SDK keyword argument, so these two can
put both a machine code and an internal identifier in front of somebody at once.
The design question this issue has to answer
AlertProps.titleis optional (frontend/ui-core/src/primitives/Badge.tsx:53), so there are twohonest shapes and this issue should pick one rather than leave each call site to choose:
alone.
competes with other content and needs a scannable line —
AnnotationPage's save refusal alreadydoes this inside its body, with
This frame could not be savedabove the prose.Whichever is chosen, the code stops being the headline. Where a code genuinely helps somebody
filing a bug report, it belongs somewhere a person can copy without it being the first thing they
read.
Acceptance criteria
codeas its title, headline, or badge text. A grep fortitle={.*\.code}acrossfrontend/returns nothing.messageverbatim render prose from the shared map instead, so no projectUUID and no SDK keyword spelling can reach a person through them.
REFUSAL_PROSEentry, or falls through to theserver message deliberately, which the map already documents as its intended behaviour for codes
whose server wording is good.
identifier. The
ui-capabilitiesskill already requires a refusal-rendering test for everymutation touched, so these largely exist and need their assertions tightened rather than written.
Notes
This was found while auditing the schema class-removal flow (#588), which requires that refusals
still reaching the UI render through the shared vocabulary. It is filed separately because five of
the six sites are outside the schema editor and the pattern spans the annotator, so correcting it
inside that flow would fix one screen and leave the rule with nowhere to live — the same failure the
refusal map was created to end. #588 is not settled by this issue and remains open on its own terms.