You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Removing a label class that has annotations walks the user through three dialogs that contradict
each other, and two of them render raw API text — a machine code, a project UUID, and SDK keyword
spelling.
All citations are at e61d3ed, in frontend/ui-core/src/screens/SchemaEditor.tsx.
The observed flow
Dialog 1 — RemoveClassDialog (:892). Correctly predicts the refusal, in the user's face, and
then offers to proceed anyway. Its own docstring at :880-890 says it "is not asking permission for
something that will then work: it is saying, before a version is composed that cannot be published,
that this is where it will fail" — and the primary button is nonetheless "Remove from draft"
(:920). The copy at :913 reads:
N annotations already use "lane". Saving a version without it is refused outright —
SCHEMA_CHANGE_WOULD_ORPHAN has no override — so this removal cannot be published until those
annotations are gone.
A machine code in a sentence a person reads, and a confirm button that starts a path the same
sentence says is doomed.
Dialog 2 — DestructiveDialog (:927). Reached on save. Title "This narrows the schema", body message={failure?.message ?? ""} (:588) — the raw kernel sentence, which carries the project UUID
and the phrase pass allow_destructive=True to proceed. Its second paragraph promises: "Saving
anyway publishes the new version and leaves earlier ones exactly as they are." Primary button "Save anyway" (:961).
Dialog 3 — OrphanDialog (:977). Reached after pressing "Save anyway". Title "Annotations
already use these classes", body message={failure?.message ?? ""} (:601) — again the raw kernel
sentence with the project UUID. There is no confirm button, correctly, and the copy says "There is no
override for this one."
So dialog 2 promises publication and dialog 3 refutes it, one click apart. The user was told the
outcome accurately in dialog 1, then walked through two more to arrive back at it.
What this violates
DESIGN.md copy rules (:759-766). "An error is what happened plus what to do, one sentence
each. The API's error contract already separates the two (docs/api.md); the UI does not merge them
back together." Here the UI does not separate them at all — it renders error.message verbatim,
including a UUID and an SDK keyword argument.
DESIGN.md principle 9 (:148) and ui-capabilities' fourth banned pattern: "Refusals
render through the shared code→prose vocabulary (one map, product-wide). A bare BATCH_NOT_IN_ANNOTATION badge is not a message." SCHEMA_CHANGE_WOULD_ORPHAN appears as literal
user-facing copy at :913.
DESIGN.md "Destructive actions state their blast radius" (:642-647): a confirmation names
what will be destroyed, counted. Dialog 2's blast radius is whatever prose the kernel happened to
interpolate.
One terminal dialog at the moment of intent for a class that has annotations. It states the fact
(N annotations use "lane"), the way out (delete or relabel them first), and links to the
annotations filtered to that class. No doomed path — no button that begins a save the dialog
already knows will be refused.
A real narrowing confirmation for a class with no annotations: blast radius counted per DESIGN.md:642, one confirm, and the save succeeds.
Neither dialog renders a machine code, a UUID, or SDK keyword spelling. Refusals that still reach the
UI render through the shared code→prose vocabulary.
Removing a label class that has annotations walks the user through three dialogs that contradict
each other, and two of them render raw API text — a machine code, a project UUID, and SDK keyword
spelling.
All citations are at
e61d3ed, infrontend/ui-core/src/screens/SchemaEditor.tsx.The observed flow
Dialog 1 —
RemoveClassDialog(:892). Correctly predicts the refusal, in the user's face, andthen offers to proceed anyway. Its own docstring at
:880-890says it "is not asking permission forsomething that will then work: it is saying, before a version is composed that cannot be published,
that this is where it will fail" — and the primary button is nonetheless "Remove from draft"
(
:920). The copy at:913reads:A machine code in a sentence a person reads, and a confirm button that starts a path the same
sentence says is doomed.
Dialog 2 —
DestructiveDialog(:927). Reached on save. Title "This narrows the schema", bodymessage={failure?.message ?? ""}(:588) — the raw kernel sentence, which carries the project UUIDand the phrase
pass allow_destructive=True to proceed. Its second paragraph promises: "Savinganyway publishes the new version and leaves earlier ones exactly as they are." Primary button
"Save anyway" (
:961).Dialog 3 —
OrphanDialog(:977). Reached after pressing "Save anyway". Title "Annotationsalready use these classes", body
message={failure?.message ?? ""}(:601) — again the raw kernelsentence with the project UUID. There is no confirm button, correctly, and the copy says "There is no
override for this one."
So dialog 2 promises publication and dialog 3 refutes it, one click apart. The user was told the
outcome accurately in dialog 1, then walked through two more to arrive back at it.
What this violates
DESIGN.mdcopy rules (:759-766). "An error is what happened plus what to do, one sentenceeach. The API's error contract already separates the two (
docs/api.md); the UI does not merge themback together." Here the UI does not separate them at all — it renders
error.messageverbatim,including a UUID and an SDK keyword argument.
DESIGN.mdprinciple 9 (:148) andui-capabilities' fourth banned pattern: "Refusalsrender through the shared code→prose vocabulary (one map, product-wide). A bare
BATCH_NOT_IN_ANNOTATIONbadge is not a message."SCHEMA_CHANGE_WOULD_ORPHANappears as literaluser-facing copy at
:913.DESIGN.md"Destructive actions state their blast radius" (:642-647): a confirmation nameswhat will be destroyed, counted. Dialog 2's blast radius is whatever prose the kernel happened to
interpolate.
ui-capabilities' one rule — the frontend renders what the wire declares. Dialog 1's count comesfrom
ProjectService.statsvia a separate query (SchemaEditor.tsx:315-317), not from anything thatdeclares removability. cf. server/wire: schema orphan and narrowing refusals are prose-only, and the wire declares nothing ahead of time #587.
Target
One terminal dialog at the moment of intent for a class that has annotations. It states the fact
(N annotations use "lane"), the way out (delete or relabel them first), and links to the
annotations filtered to that class. No doomed path — no button that begins a save the dialog
already knows will be refused.
A real narrowing confirmation for a class with no annotations: blast radius counted per
DESIGN.md:642, one confirm, and the save succeeds.Neither dialog renders a machine code, a UUID, or SDK keyword spelling. Refusals that still reach the
UI render through the shared code→prose vocabulary.
Blocked on
doomed round-trip and without the client deriving legality from a stats count.
annotations are an error to be cleared; under the other they are a tolerated historical fact, and the
sentence differs.
Do not start this before both are settled.
Provenance
Found by a read-only audit of the schema-narrowing / orphan gate at
e61d3ed.