Skip to content

fix(components): the ui:select label names its combobox through htmlFor and the trigger id (objectui#10435) - #10548

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-10435-select-label-association
Sep 25, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-10435-select-label-association

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #10435

Clause-②: no

What changed

ui:select (packages/components/src/renderers/form/select.tsx) now writes htmlFor={schema.id} on its Label and id={schema.id} on the Radix SelectTrigger. This is the record-picker-label-association-5771 shape the triage ruled. Before, neither end carried the wiring. The trigger is a button with role="combobox", a role that takes no name from its content, so a labelled select's combobox had an empty accessible name.

  • A labelled select authored with an id is found by getByRole('combobox', { name: 'Title' }).
  • A required one's name stays Title: the objectui#10368 marker span is aria-hidden.
  • A select authored without an id mints none. Its caption stays unassociated, as on the sibling input / textarea renderers and element:record_picker. This half is pinned as deliberate. See Acceptance notes for the dispatch hypothesis it contradicts.

Files (vs base 111fa4c)

File + / -
packages/components/src/renderers/form/select.tsx +13 / -2
packages/components/src/renderers/form/__tests__/select-label-association.test.tsx (new, 7 cases) +128 / -0
packages/components/src/__tests__/required-marker-markup.test.tsx +3 / -5
.changeset/10435-select-label-association.md (@object-ui/components patch) +20 / -0

required-marker-markup.test.tsx is outside the claim's declared file surface. Its select site carried the comment "This label carries no for today", which this change made false. The comment is rewritten, and the locator becomes label[for="sel-ctl"], the same form as the other five sites.

Verification, on final head 4e99a54 (merge of origin/main d461b0d)

  • pnpm exec vitest run packages/components/src/renderers/form/ packages/components/src/__tests__/required-marker-markup.test.tsx packages/components/src/__tests__/form-renderers.test.tsx gave exit 0, with Test Files 67 passed (67) and Tests 494 passed | 10 skipped (504).
  • pnpm turbo run type-check --filter @object-ui/components --concurrency=2 gave exit 0 with Tasks: 9 successful, 9 total. It ran on a75bf59. The merge after that only touched packages/app-shell, packages/plugin-dashboard, content/docs and two changesets, all outside @object-ui/components and its @object-ui/* dependencies. tsc -p tsconfig.test.json --listFilesOnly lists both touched test files.
  • eslint on the 3 changed source and test files gave exit 0 with 0 errors and 4 warnings. All 4 warnings are pre-existing on the unchanged signature line, and the BASE select.tsx also shows 4.
  • These gates each gave exit 0: check:control-bytes, check:test-path-roots, check:vi-mock-specifiers, check:vi-mock-inherit, check:vi-mock-override-shape, check:new-line-citations (0 new), check:changeset-claims, check:pending-changeset-literals, check-changeset-presence, check-changeset-no-major, check-changeset-overwrite, check-changeset-fixed, check-lint-coverage.
  • NOT MEASURED: check:sdui-registration-pins exits 2 with a prerequisite refusal, because it needs a console build. This diff does not change the registration block. That gate is declared to CI.

Reverse verification (ablation)

The fix was committed first as a75bf59. The script then overwrote select.tsx with the BASE blob 4296933c: htmlFor={schema.id} count went 1 to 0 on disk and SelectTrigger id={schema.id} count went 1 to 0. It ran the pin and required-marker-markup.test.tsx, then restored with git checkout HEAD -- under an EXIT INT TERM trap.

  • Mutated run: exit 1, Tests 8 failed | 14 passed (22).
    • 6 of the 7 new cases failed, starting with Unable to find an accessible element with the role "combobox" and name "Title".
    • The no-id case stayed green, as expected.
    • Both select-site cases of required-marker-markup failed, because of the tightened label[for] locator.
  • Restore: file hash 8fc729cb equals the HEAD blob, and git diff HEAD is empty.

Acceptance notes

  • The dispatch's hypothesis H2 conflicts with the ruled shape. H2 said a select with no schema.id still needs a minted id, for example from useId. The ruled 5771 shape explicitly declines a useId() fallback, and its own test pins the no-id case as unassociated. The ruling was followed. The alternative is raised to the dispatching seat as an open question, not decided here.
  • Related to that question: the docs catalog examples components-form-select/basic-select and components-form-input/with-label author a label with no id. They render an unnamed control under the ruled shape. This is noted, not filed.

Implementation session: https://claude.ai/code/session_01KUxVUa7e39aNjhkKi1gsoy (dispatched by the domain:ui seat 5).


Generated by Claude Code

…or and the trigger id

The `select` form renderer drew its Label with no htmlFor and the Radix
SelectTrigger with no id, so a labelled select's combobox had an empty
accessible name. The trigger now gets schema.id and the label htmlFor to
it, the element:record_picker shape (objectui#5771). A select without an
id mints none, as on the sibling input and textarea renderers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KUxVUa7e39aNjhkKi1gsoy
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3046.3 KB 3104.5 KB
Main entry chunk (gzip) 147.8 KB 350 KB
Entry file index-DooXfICS.js —
Status PASS —

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.68KB 6.20KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.52KB 3.45KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.13KB 7.95KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 545.01KB 130.34KB
core (index.js) 9.22KB 3.71KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 223.86KB 62.25KB
fields (index.js) 257.16KB 65.08KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.28KB 11.09KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.01KB 3.93KB
plugin-calendar (index.js) 51.38KB 14.60KB
plugin-charts (index.js) 74.95KB 20.88KB
plugin-chatbot (index.js) 198.36KB 47.20KB
plugin-dashboard (index.js) 133.50KB 35.37KB
plugin-designer (index.js) 216.25KB 44.39KB
plugin-detail (index.js) 232.25KB 61.40KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 146.50KB 37.51KB
plugin-gantt (index.js) 169.64KB 41.92KB
plugin-grid (index.js) 215.37KB 58.92KB
plugin-kanban (index.js) 49.30KB 15.39KB
plugin-list (index.js) 114.55KB 28.29KB
plugin-map (index.js) 23.82KB 7.42KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.55KB 11.99KB
plugin-timeline (index.js) 30.64KB 8.94KB
plugin-tree (index.js) 10.52KB 3.69KB
plugin-view (index.js) 87.85KB 21.96KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 114.58KB 37.60KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.03KB 1.86KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.78KB 2.56KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 18.27KB 6.20KB
types (ai.js) 4.11KB 2.06KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 17.15KB 6.32KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 4e99a54a6a5d2ec5dcf99b07fe689757ac131807

Reviewed against the merge-base d461b0de87b49693bebc0a853abe5c5e4d1d9613 (the second parent of the head's merge commit; git diff of that base to the head is exactly the four files below, and the fix commit a75bf59 diffed against its own parent 111fa4c is byte-identical on those four paths). Everything below was read from refs/review/pr-10548 and refs/review/main; the shared tree was not checked out. The os-dev-report comment's claims were not used as evidence. The PR's local test and gate numbers are the author's claims, not this review's measurements: the objectui tree has no node_modules, and the only run this review counts is CI at the head.

① Derived judgments

Behaviour changes the diff introduces, judged against the card, the triage comment and the ruled shape.

  1. packages/components/src/renderers/form/select.tsx:62 — Label htmlFor={schema.id} (base: Label with no htmlFor). Card: "Give the trigger the schema's id, and give the label htmlFor to it." Triage (comment 5827499486): "the trigger gets the schema's id, the label gets htmlFor." RIGHT. The Label primitive (packages/components/src/ui/label.tsx:26-30) spreads its props onto the Radix label root, so htmlFor reaches the label element.
  2. packages/components/src/renderers/form/select.tsx:86 — SelectTrigger id={schema.id} (base: no id). Same ruling. RIGHT. SelectTrigger (packages/components/src/ui/select.tsx:57-64) spreads its props onto the Radix trigger, and that trigger is the button with role="combobox" that the existing green pin on main already locates with screen.getByRole('combobox') against this very renderer (required-marker-markup.test.tsx, select site).
  3. packages/components/src/renderers/form/select.tsx:50-60 — an 11-line comment explaining the pair and the no-fallback choice. Prose checked below; every sentence is true against the code. Within the claimed file surface.
  4. No useId() fallback: a select authored without schema.id still renders no for and no id. The card and the triage both bind the wiring to "the schema's id", and the ruled record-picker-label-association-5771 changeset says verbatim: "wired when schema.id exists, matching element:text_input's precedent rather than an always-on useId() fallback. Only the author can supply schema.id, so a picker authored without one keeps its pre-fix, unassociated caption text exactly as before; nothing about this change mints an id the author did not provide." RIGHT to follow it; a minted id would be a family-wide reversal of the components: element:text_input's description renders as an unassociated paragraph — no aria-describedby, so assistive tech never announces it with the field #5735 / components: element:record_picker renders a label element with no htmlFor against a trigger with no id — the caption its own docs advertise names nothing #5771 answer that no card asks for. id is already on BaseSchema (packages/types/src/base.ts:83), so no new key is consumed.
  5. The Radix Select root, the registration block (select.tsx:99-125, inputs and defaultProps) and the SelectRenderer export are untouched. Nothing else in the renderer moved.

Asked and delivered.

  • Card pin ask: "a labelled ui:select is found by getByRole('combobox', { name: 'Title' })" — select-label-association.test.tsx:50. Delivered.
  • Card pin ask: "a required one's name stays Title" — select-label-association.test.tsx:56-66 (marker present and aria-hidden, name Title, aria-required="true"). Delivered.
  • Claim file surface: select.tsx (label htmlFor plus trigger id only) — delivered as items 1 and 2 plus a comment; "a pin beside the existing select renderer tests" — the new file sits in packages/components/src/renderers/form/__tests__/, beside the six form-builtin-select-*.test.tsx files; "one .changeset/10435-…md" — .changeset/10435-select-label-association.md. Delivered.
  • components: element:record_picker renders a label element with no htmlFor against a trigger with no id — the caption its own docs advertise names nothing #5771's resolution rule ("pin the resolution … not the two attributes separately") is honoured: every associated case resolves end to end (getByRole by name, toHaveAccessibleName, getElementById on the for value, trigger.labels), never htmlFor and id as two strings.

Done but not asked.

  • packages/components/src/__tests__/required-marker-markup.test.tsx:136-138 is outside the claim's declared file surface. What changed: the select site's locator went from document.querySelector('label') to document.querySelector('label[for="sel-ctl"]'), and the comment "This label carries no for today … deliberately not pinned either way" was replaced by "The label names the trigger through for since objectui#10435". The old comment became false the moment item 1 landed, so leaving it would have shipped prose that contradicts the code. Test-only, unpublished, explained in the PR body. Justified; not a defect.
  • Nothing else outside the ask. No docs, no registration prose, no types.

Existing pins edited or deleted.

  • Edited: required-marker-markup.test.tsx select site (above). The flip is a tightening, not a weakening: the old locator matched any label and would still have passed after the fix; the new one returns null when the for is lost, so expect(el).not.toBeNull() at :149 and :167 goes red on a regression of item 1. The two assertions the file exists for (no generated content, one aria-hidden marker, text outside aria-hidden is Title, aria-required on the combobox) are unchanged. Load-bearing.
  • Deleted: none.

Can the new pins fail? (select-label-association.test.tsx, 7 cases)

  • Negative leg, by construction against the base blob 4296933c: cases 1, 2, 4, 5, 7 call getByRole('combobox', { name: 'Title' }) or toHaveAccessibleName('Title'); on the base the trigger has no id, no for points at it, and combobox takes no name from content, so the name is '' and each throws. Case 3 queries label[for], null on the base, and fails at :73. Case 6 (no-id) is green on the base by design: it pins the half that does not change. That the name reader returns '' for an unlabelled Radix trigger with placeholder text is not assumed: the green main pin record-picker-label-association.test.tsx:130-132 asserts exactly that on the same SelectTrigger primitive.
  • Lit control: case 6 uses the same instrument (toHaveAccessibleName) and reads '', so a reader that trivially echoed the label would fail it; cases 4 and 5 assert toHaveTextContent('Choose…') / 'Ms' next to toHaveAccessibleName('Title'), so the name is shown to come from the label and not the trigger's content. Case 3 and case 7 read trigger.labels (happy-dom implements it: the same read is green on main at record-picker-label-association.test.tsx:104-107), and ?? [] does not soften them, since [] never equals [label].
  • Case 7 renders through SchemaRenderer and asserts labels equals the one label[for="title"]; that also guards against a duplicate id landing on a wrapper (SchemaRenderer.tsx:2180 writes data-obj-id, not id, so none is expected).
  • The file is under a directory the dom vitest project already runs (65 sibling test files there), it registers the renderers at module scope, and the components tsconfig.test.json includes *.test.tsx. Its prose cites by symbol and test name only; no path:line address anywhere in the diff (AGENTS.md [WIP] Update documentation for project #11).

② Semver level

.changeset/10435-select-label-association.md: '@object-ui/components': patch. The diff changes published src of a released package in the fixed group, so a changeset is owed (AGENTS.md, changeset presence rule) and one is declared; it is not major (AGENTS.md 版本号策略: never major; CI Changeset Bump Policy green at the head). An a11y bug fix that adds two DOM attributes and changes no schema key, no export and no registration is a patch. Not a docs-only change, so the empty-frontmatter form does not apply. RIGHT.

Changeset prose, sentence by sentence, against the diff:

  1. "The ui:select form renderer's label now names its combobox (objectui#10435)." TRUE when id is authored; the closing sentence states the other half.
  2. "Before, the renderer drew its Label with no htmlFor and the Radix SelectTrigger with no id, and nothing set aria-label or aria-labelledby." TRUE: the base hunk is Label className=… and SelectTrigger className={className}; the head file has no aria-label / aria-labelledby and the diff adds none, so the base had none.
  3. "The trigger is a button with role="combobox", a role that takes no name from its content, so the placeholder and value text inside it did not name it either: a labelled select's combobox had an empty accessible name, and a screen reader announced an unnamed combobox." TRUE: role confirmed by the existing getByRole('combobox') pin on this renderer; combobox is not a name-from-content role; the card measured '' in Chromium's tree.
  4. "The fix is the shape element:record_picker landed for the same defect (objectui#5771): the trigger gets the node's id, and the label gets htmlFor to it." TRUE: record-picker.tsx:267 Label htmlFor={schema?.id}, :277 id={schema?.id}.
  5. "A labelled select authored with an id is now named by its label, and a required one's name leaves out the *, because the required marker is an aria-hidden element (objectui#10368)." TRUE: select.tsx:71 marker span carries aria-hidden="true"; pinned at test :61-65.
  6. "A select authored without an id is unchanged: the renderer mints no id, so its caption stays unassociated, as on the sibling input and textarea renderers." TRUE: no useId in the file; input.tsx:48/64 and textarea.tsx:43/58 wire htmlFor={schema.id} / id={schema.id} with no fallback.

Code-comment prose added by the diff (select.tsx:50-60): "the shape element:record_picker landed … (objectui#5771)" TRUE; "SelectTrigger renders a button with role="combobox", a labelable element" TRUE (a button is labelable); "that role takes no name from its content" TRUE; "Only the author supplies schema.id" TRUE (BaseSchema.id optional, no fallback); "as on the sibling input / textarea renderers, rather than a minted useId() fallback" TRUE.

Test-file prose: the header restates the card (TRUE as above); "the no-id case pins that the renderer mints none, the same deliberate half that shape pins for the picker" TRUE (record-picker-label-association.test.tsx:130-132); "two values that are each present and never match is the failure mode objectui#3341 documents" TRUE (#3341 item 2: htmlFor pointing at an id the SelectTrigger never carried). required-marker-markup.test.tsx:136-137: "The label names the trigger through for since objectui#10435, whose own pin (select-label-association.test.tsx) reads the resulting name." TRUE.

No content/docs/** or README prose is added by the diff.

③ Boundary flags

The PR body's own flags and open questions.

  • "required-marker-markup.test.tsx is outside the claim's declared file surface." Confirmed, and judged above: a comment that this change made false, plus a tightened locator; not a weakening.
  • "NOT MEASURED: check:sdui-registration-pins … That gate is declared to CI." CI did measure it: .github/workflows/performance-budget.yml (check name Bundle Analysis) triggers on packages/** and runs pnpm check:sdui-registration-pins as an unconditional step after the build; Bundle Analysis is success at the head. The registration block is unchanged in the diff, consistent with that.
  • "type-check … ran on a75bf59" not on the merge head: the CI Type Check run at the head is the measurement that matters; see the counts below.
  • Open question (H2, a useId fallback when schema.id is absent): the card, the triage note and the ruled 5771 changeset all bind the wiring to the authored schema.id, and the ruling explicitly declines the fallback, so following it is correct for this card. A minted id would move every Label/control renderer in the family, and the no-id pins here and in the picker would have to flip together; that is a separate decision, correctly not taken in this PR. The catalog observation is accurate: examples/schema-catalog/src/schemas/components-form-select/basic-select.json authors label with no id (and components-form-input/with-label.json likewise), so those examples render an unnamed control under the ruled shape, as they did before this change. Noted, not filed, correctly out of scope.
  • "Files (vs base 111fa4c)" table: +13/-2, +128/-0, +3/-5, +20/-0 — matches git diff --stat against the merge-base exactly.

CI at the head 4e99a54 (42 check-runs at polls 1 and 2, 43 at poll 3):

  • Poll 1 (about 08:26 UTC): 28 success, 3 skipped (Test (coverage), Test (coverage shard), dependabot), 11 in progress.
  • Poll 2 (08:30 UTC): 31 success, 3 skipped, 8 in progress (Test (shard 1,3,4,5,6,7/8), Type Check, Spec Main Shape Gate).
  • Poll 3 (08:33 UTC, the last permitted): 43 check-runs (the Test roll-up job appeared and completed), 39 success, 3 skipped, 1 in progress (Spec Main Shape Gate only). All eight Test (shard n/8) runs, the Test roll-up, Type Check and Lint are success at the head.
  • No failure, cancellation or neutral conclusion at any poll. Green gates that bear on this diff: the full Test matrix and roll-up (the lit run of the new pin and of the edited pin, and of every other components test against the new DOM), Type Check (on the merge head, which closes the PR body's own gap), Lint, Changeset Bump Policy, Changeset Declaration, Changeset Claim Re-read, Changeset Fixed Group Check, Line Citation Gate, Control Byte Scan, Inert vi.mock Specifier Check, Governed Surface Queue Guard, Test (dist pins), Bundle Analysis (which runs check:sdui-registration-pins), Build & E2E, Build Docs, README Export Check. The one still running, Spec Main Shape Gate, reads @objectstack/spec shape; this diff consumes no spec symbol.

Is the PR draft? Yes (draft: true). Correct for a dispatched PR under AGENTS.md: it stays draft until the dispatching seat flips it.

Does Clause-②: no hold? Yes. No accept set moves: id was already an accepted BaseSchema key and the select registration inputs are untouched. No published export moves: SelectRenderer's registration, signature and props are unchanged; the ui/select.tsx and ui/label.tsx primitives are untouched. What moves is rendered DOM (two attributes when schema.id is authored), which is the behaviour the card asked for.

Did main move under the PR's files since the merge-base? No. git log d461b0d..refs/review/main on the four PR paths is empty. Main advanced 11 commits (to 2ad3671); one of them, 641fb55, touches packages/components/src/__tests__/filter-builder-operator-alias-trigger-7561.test.tsx and filter-builder sources, none of them PR files. GitHub reports mergeable: true, mergeable_state: behind.

Not measured by this review: the Chromium accessibility tree after the fix (the card's measurement was pre-fix; no browser harness exists in this package, and the PR says so); the local vitest / eslint / gate runs the PR body reports (the author's claims, superseded by CI at the head).

Implemented-by: claude/issue-10435-select-label-association
Reviewed-by: session_01KUxVUa7e39aNjhkKi1gsoy

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 25, 2026 08:37
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 25, 2026
Merged via the queue into main with commit 93a689d Sep 25, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-10435-select-label-association branch September 25, 2026 08:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants