…talog-failure arm
`PickerLoadFailure` is a `div[role="status"]`, and four widgets `WIDGET_LABELLING`
declares `labelling: 'control'` rendered it INSTEAD of their picker on a failed
catalog. The `id` `FieldRow` hands a `'control'` widget therefore landed on no
element, the visible label's `for` dangled, and the field had no accessible name
in the card layout as well as in a grid cell.
`object-selector`, `field-selector`, `field-ref` and `view-ref` now render the
failure banner beside a freeform entry that carries `controlNaming` and keeps the
stored value editable while the catalog is unknown -- the shape `ref:object`
already used on this arm. The picker itself is still not rendered there.
The `'control'` docblock names the failure branch in its list of example
branches; the rule it glosses ("in EVERY branch it can render") is unchanged and
no widget's declaration moved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HrVaotisyhgmot9o2MLRq
Fixes #9931
PickerLoadFailureis adiv[role="status"]. Four widgets theWIDGET_LABELLINGtable declareslabelling: 'control'rendered it INSTEAD of their picker when the option catalog failed to load, so theidFieldRowhands a'control'widget landed on no element at all: the visible label'sforattribute dangled and the field had no accessible name — in the CARD layout as well as in a grid cell, which is the worse half, because there the label is right on screen and points at nothing.forattribute, BUTTON) — GitHub's body sanitizer eats them, including inside backticks and fences.1. The
'control'population, DERIVED — ⛔ not taken from the card's fourRead on
edbcf1e7a, this branch's base and the same commit the claim measured.The derivation is two mechanical steps:
'control'set is whateverObject.entries(WIDGET_LABELLING).filter(([, v]) => v === 'control')returns. On this base that is eleven keys:ref:object,ref:component,object-selector,field-selector,field-ref,filter-builder,view-ref,icon,color-input,string-tags,secret;grep -n "status === 'error'" -e loadErrorfinds inwidgets.tsx. Every one of them rendersPickerLoadFailure; there is no second failure shape in the file.⭐ The claim's reading that
PickerLoadFailurehas eleven occurrences is confirmed and resolved: one is the declaration, ten are render sites. Six are on'control'widgets, four on'group'ones — so the six the card's body assumed are the six, and the other four are ⛔ not this card's business:edbcf1e7awidgets.tsx:401ref:objectcontrolNamingwidgets.tsx:656object-selectorwidgets.tsx:854field-selectorwidgets.tsx:1380field-refwidgets.tsx:1457view-refwidgets.tsx:1605field-multirole="group"container still renders and the banner decorates itwidgets.tsx:2014filter-mode(fields)widgets.tsx:2090filter-mode(tabs)widgets.tsx:2143action-multiwidgets.tsx:2290filter-buildercontrolNamingrenders on every arm⇒ no fifth
'control'widget has an unnamed failure arm, and none of the card's four is mis-declared. The other five'control'keys read no catalog at all, so they have no failure arm to check — which the new pin asserts rather than assumes: it renders all eleven with every catalog failed.⛔ Rejected on the record, per the stop condition:
ref:objectandfilter-builderalready rendered a labelable element on their failure arm on this base. Neither is touched, and both are still asserted here so the repair cannot quietly regress them.2. Re-taken line numbers (all on
edbcf1e7a)edbcf1e7aobject-selector:593:656field-selector:790:854field-ref:1316:1380view-ref:1393:1457widgets.tsx:2793, the'control'docblock parenthetical:2793SchemaForm.tsx:1606labelAssociationconst labelAssociation = groupLabelled ? { id: labelId } : { htmlFor: id };WIDGET_LABELLINGoccurrences inwidgets.tsxThese addresses are a dated reading on a named commit and are ⛔ not written into any source file: the comments this PR adds cite by symbol name and card number only.
3. ⭐ The fork: reading (a) — a VIOLATION, ⛔ not a contract gap
Quoted verbatim from the
'control'half's own docblock onedbcf1e7a:Four readings decided it — two from the text, two from what the other
'control'branches actually do:field-multiandaction-multiare excluded from'control'because they were measured DANGLING in the read-only one — dangling in ONE arm disqualifies a widget, whichever arm that is. Read as exhaustive, the list would let a widget that dangles in a FOURTH arm keep the declaration — exactly the state this card found — and the table would then assert "the host label reaches a labelable element" about widgets where it provably does not. A reading that makes the declaration false about its own members is not the reading.ref:objectis the counter-example already in the file. It is the one'control'picker whose failure arm was authored with the naming on it, under a comment reasoning about that arm specifically: "The freeform input is kept, and enabled, so a failed catalog does not also block authoring."filter-builderlikewise keeps its named trigger in every arm. Under (b) both would be doing unrequired work; under (a) they are simply the two that already comply — which is why the card could point at them as the shapes to copy.PickerLoadFailure's own docblock states the invariant the four violate: "Every picker keeps whatever control lets the author see and edit the value already stored, because a failed catalog must not also block authoring." A branch two independent declarations govern is ⛔ not an uncovered branch.⛔ The sub-option triage offered — put the
idonPickerLoadFailure's container and make it labelable — is unavailable, and that is worth stating rather than leaving implied: the container is a DIV, and theforattribute addresses only labelable elements (INPUT / TEXTAREA / SELECT / BUTTON / METER / OUTPUT / PROGRESS). Making that container labelable means not being a DIV.What moved in the contract text, and what did not. The parenthetical now names the failed-catalog branch, so the examples match the rule they gloss, and a short paragraph records the settled reading and points at the new pin as the instrument that re-derives it. That is a record, ⛔ not (b) performed quietly: the rule phrase in EVERY branch it can render is unchanged, no key changed labelling, and
WIDGET_LABELLINGitself is untouched.4. The repair shape — the
ref:objectshape for all four, ⛔ never thefilter-buildershapeThe card named this as the decision, not a detail of it. All four take the editable-anchor shape, and the reason is written in each widget's own comment:
field-ref/view-ref— the banner now renders beside a freeform Input carryingcontrolNaming, showing the stored name and editable unlessreadOnly. Same stored shape (one name) and same situation asref:object's error arm, so this is that arm copied rather than a shape invented here.object-selector/field-selector— the selected chips already answer see and remove; what the arm had no way to do at all was add. The freeform entry is that arm's primary control: chips beside one entry box that takescontrolNaming, which isstring-tags' shape in this same file. It commits on Enter or blur and writes the SAME value shape the picker writes — an array whenmultiple, a bare name otherwise — so a name typed while the catalog is unknown round-trips exactly like a picked one.⛔ The picker itself is still not rendered on any of the four.
field-selector's own comment argues against it — "with no options it could only render as a dead, disabled dropdown next to a banner saying the options are unknown, which is the very conflation this arm exists to end" — and objectui#5227's pin asserts no combobox on that arm. A disabled trigger would have undone objectui#5170's repair and turned that pin red.No new i18n keys. The new inputs carry no placeholder, exactly as
ref:object's error-arm input carries none.i18n.tssits outside the declared file face and was not touched.SchemaForm.tsxwas read as evidence and ⛔ not edited — the fix needed nothing outside the declared face, so there is no stop-and-report here.5. ⭐ Acceptance: the RED leg, and the SUCCESS leg that stayed green beside it
New pin:
packages/app-shell/src/views/metadata-admin/__tests__/SchemaForm.controlWidgetFailureArmNaming-9931.test.tsx, 34 assertions. Its first test asserts the case table equals the'control'key set read offWIDGET_LABELLING, so the population is derived at run time and a twelfth'control'widget cannot be added without a reading here.The ablation was run from the committed fix, so the restore had a real commit to come back to. The source file was reverted to its pre-fix blob on disk and both directions were proven by blob hash, ⛔ never by an exit code:
RED leg —
Test Files 1 failed (1)·Tests 8 failed | 26 passed (34). Exactly the four in-scope arms, in BOTH layouts:GREEN in that same run — this is what makes the eight reds a measurement rather than a switch being flipped, and it is the half objectui#9889 lost:
PickerLoadFailureeach key renders on a failed catalog, read off the DOM and compared with the declared column. A widget that stopped entering its failure arm goes red here even while the naming assertions go green — which is exactly the "the check was switched off" move this lane already paid for once;ref:objectandfilter-builderin both layouts — the two rejected-on-the-record arms, which must stay named.With the fix restored:
Test Files 1 passed (1)·Tests 34 passed (34).6. What was run
Test Files 1 passed (1)·Tests 34 passed (34)git grep -lfor importers ofwidgets/SchemaForm, the-load-failedtest ids andWIDGET_LABELLING)Test Files 35 passed (35)·Tests 397 passed (397)pnpm --filter @object-ui/app-shell run type-check(tsc --noEmitandtsc -p tsconfig.test.json, so the new test file is covered)pnpm --filter '@object-ui/app-shell^...' build— the first attempt was a PREREQUISITE NOT MET (unbuilt workspace.d.ts), ⛔ not a red gatepnpm --filter @object-ui/app-shell run lintnode scripts/check-changeset-presence.mjscheck:control-bytes,check:new-line-citations,check:test-path-roots,check:changeset-claims,check:pending-changeset-literals,check:vi-mock-specifiers,check:i18n-keys,check:unreferenced-sourcesnode scripts/check-governed-queue-guard.mjs --testover the three changed pathsDeclared narrowing: the whole-repo
turbo run lintand the fullvitest runare CI's; what ran locally is the affected package's own lint and type-check plus the derived 35-file test set.git log -Sresolves both strings to the shallow-boundary commit, which is an artifact and not a reading; a deepen of 300 commits did not reach that era. The fork above rests on the text and on the code, both of which are self-contained on this base.⛔ Draft on purpose: ⛔ not marked ready, ⛔ not enqueued, ⛔ not merged.
Generated by Claude Code