Skip to content

fix(fields): the last five inline widgets read the delivered error slot - #7141

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-7126-edit-widgets-error-prop
Sep 1, 2026
Merged

fix(fields): the last five inline widgets read the delivered error slot#7141
os-warren merged 1 commit into
mainfrom
claude/issue-7126-edit-widgets-error-prop

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#7126

The last five widgets in EDIT_WIDGETS now read the error slot that objectui#7008 delivers, so an inline text / boolean / toggle / date / datetime / time control that failed validation finally reports aria-invalid.

Leg 0 — the population, re-derived on origin/main AFTER its blocker landed

The census in the card was taken before PR #7128 landed. Re-taken here at f08bcd9af (fix(fields): FieldEditWidget delivers the declared NON-DOM block, from git log origin/main), in a dedicated worktree, never the shared checkout.

Word-boundary error match over the 27 distinct components in EDIT_WIDGETS:

  • 21 read it, 6 return zero — unchanged from the pre-fix(fields): FieldEditWidget delivers the declared NON-DOM host-plumbing block #7128 census.
  • CONTROL for the instrument: NumberField returns 5, LookupField 11, so a zero is a reading and not a broken query.
  • CONTROL for the zeroes — each apparent zero was opened and read, not counted.UserField is a FALSE zero: its whole body is return LookupField {...(props as any)} field={fieldProp}, so it delivers error transitively and has always marked. Independently corroborated in-repo by data-table.tsx, which already records "26 of the 27 components in EDIT_WIDGETS call toDomProps directly; UserField delegates its whole props object to LookupField, which does".

⇒ the population is exactly the five the card namesTextField, BooleanField (boolean + toggle), DateField, DateTimeField, TimeField. Nothing shrank, nothing grew.

What changed

One idiom, five widgets — the objectui#3222 shape the other 21 already share (SelectField, EmailField, NumberField): destructure error, then write aria-invalid={!!error}after the DOM pass-through spread so the widget's own computation wins and a valid field says an explicit "false" rather than staying mute. Seven call sites, because two widgets have two render branches each.

⛔ This buys the a11y MARKING only. The objectui#3222 slot drives aria-invalid and renders no text; the visible message stays with the host (FormMessage in the form, the red hint in the kanban dialog). Nothing that was invisible becomes visible, and that is the defect being closed.

The composite judgement, stated per widget

widgetmarkable control?what was markedwhy
TextFieldyes (x2)the input[type=text]and the textarea of the rows > 1 branchtwo render branches, two real single controls; marking only one would leave authored rows configs silent
BooleanFieldyes (x2)the Radix Checkbox / Switch, i.e. button[role=checkbox] / button[role=switch]the one composite here. The control sits inside a flex div with an sr-only label. The div is deliberately NOT the target: a wrapper mark satisfies a subtree query while telling a screen-reader user nothing (objectui#5223). aria-invalid is a global ARIA attribute, valid on both roles, and both Radix roots forward unknown props to the real button
DateFieldyesthe input[type=date]no composite question despite the name "picker": the browser picker is that same element's own UI, not a second element
DateTimeFieldyesthe input[type=datetime-local]as above
TimeFieldyesthe input[type=time]as above

None of the five turned out to lack a markable control.

The end-to-end case — the deliverable

packages/plugin-kanban/src/__tests__/RequiredFieldsDialog.ariaInvalidText-7126.test.tsx, following the shape of #7128's RequiredFieldsDialog.ariaInvalid-7008.test.tsx rather than inventing a harness:

  1. a required text field reports aria-invalid="false" on a clean open and "true" after a submit attempt finds it empty — with the submit refusal and the visible Required hint asserted first, so a dialog that silently submitted could not pass;
  2. CONTROL, per-field: with a text filled and a select empty, the select is "true" and the filled text is "false", and there is exactly one Required hint — a dialog-wide flag would fail this;
  3. the whole population through the same dialog: text, boolean, toggle, date, datetime, time in one dialog, each read off its own element selector, so a mark that landed on BooleanField's wrapper could not satisfy it.

Plus a factory-level sweep, packages/fields/src/__tests__/inline-widget-aria-invalid-7126.test.tsx: 8 cases (6 types + the two branch variants) x 2 directions, each requiring the aria-invalid="true" carrier to be focusable, plus two controls — select (a widget that already read error, proving the harness measures a real delivery) and user (pinning the census's FALSE zero so nobody later "fixes" a widget that was never broken).

Why the factory and not the form. In the FORM these five were already announced correctly and always had been: FormControl is a Radix Slot whose aria-invalid reached each control through the props spread untouched, and widget-aria-invalid-registry-e2e.test.tsx sweeps that path with an emptyNOT_YET_DELIVERED ledger. A form-based test would have been green before this change. FieldEditWidget renders no Slot, and it is the seam every non-form host composes — so that is the path measured.

Ablation — the pins fail without the change

Direction predicted before the run and recorded in the script header. Mutation: delete all 7 aria-invalid={!!error} lines.

  • Mutation proven on disk, not by an editor exit code: marker count 2/2/1/1/1 -> 0 for all five, and every blob hash moved off its HEAD blob (e.g. TextField.tsxa9a17e42f -> b919aa64d, BooleanField.tsxc8325fff6 -> d2adecc4c, TimeField.tsx239728047 -> 762385d77). No rebuild leg is claimed and none is owed: vitest.config.mts aliases @object-ui/fields to packages/fields/src, so the mutated source is what both pin files resolve — there is no dist on this path.
  • PREDICTED: Tests 19 failed | 2 passed (21).
  • OBSERVED: Tests 19 failed | 2 passed (21) — and the 2 that stayed green are exactly the two CONTROL cases (select, user), which do not depend on the five widgets. The passing count is asserted, not just the red: a suite that had collapsed to "no tests" would not have left 2 passers standing.
  • Restore proven by state, with git checkout HEAD -- ABSOLUTE_PATH (never a bare git checkout --, which reads the index a path-scoped checkout also writes): all five blobs back to their HEAD values, marker counts back to 2/2/1/1/1, and git diff HEAD, git diff --cached and git status --short all empty.

Measurements

All at 323f2c381, the head of this branch.

checkverdict
pnpm exec vitest run packages/fields/ packages/plugin-kanban/ --maxWorkers=2Test Files 151 passed (151) / Tests 2249 passed (2249) — includes the form-path registry sweep and #7128's own dialog pin
pnpm exec vitest run (the 2 components-side negative-assertion files)Test Files 2 passed (2) / Tests 24 passed (24)
pnpm --filter @object-ui/fields --filter @object-ui/plugin-kanban run type-checkexit 0, Scope: 2 of 47 workspace projects, both Done. Its tsconfig.test.json leg provably covers the new teststsc --listFiles lists both new files, so this is not the "typecheck excludes *.test.tsx" non-reading
pnpm --filter @object-ui/fields --filter @object-ui/plugin-kanban run lintexit 0, 0 errors (warnings are pre-existing repo-wide; no --max-warnings in the gate). Plain eslint . per package, no --no-inline-config
pnpm check:control-bytescheck-control-bytes: OK (scanned 5914 tracked text file(s); skipped 85 binary) — plus a self-scan of the 9 changed files with a probe control that hits
pnpm check:vi-mock-specifiers / check:vi-mock-inherit / check:i18n-keysall exit 0
pnpm changeset:checkAll workspace packages are in the changeset fixed group. / No changeset declares a major bump.
node scripts/check-changeset-presence.mjs8 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)
node scripts/check-changeset-overwrite.mjsNo pre-existing changeset was modified or deleted.
pnpm check:sdui-registration-pinsNOT MEASURED, not red — exit 2 with its own PREREQUISITE NOT MET text: "the registrations this gate pins are dropped by a WRONG ARRAY at BUNDLE time, so a run with nothing to read has measured nothing. Build the console first". This change registers nothing; CI builds the console and runs it

type-check first came back with a wall of TS2307: Cannot find module '@object-ui/components' across files this branch never touched — an unbuilt closure, i.e. NOT MEASURED. pnpm --workspace-concurrency=2 --filter '@object-ui/fields^...' --filter '@object-ui/plugin-kanban^...' build (exit 0) fixed it, and the verdict above is from the re-run.

PM assumptions, checked

  • "The five are exactly right post-fix(fields): FieldEditWidget delivers the declared NON-DOM host-plumbing block #7128." Confirmed, with the false-zero trap re-caught rather than re-introduced.
  • "Each of the five has a markable control." Confirmed, per widget, in the table above.
  • "EDIT_WIDGETS is the right population boundary."Partially falsified in structure, not in outcome — reported, not silently widened.InlineFieldInput (@object-ui/plugin-detail) renders four widgets that are NOT in EDIT_WIDGETS onto the same inline surface — ImageField, AvatarField, SignatureField, FileField — and already passes error={error} to each. Measured with the same instrument and the same NumberField-returns-5 control: ImageField 4, AvatarField 4, FileField 10 all read it and mark; SignatureField returns 0, and that zero is a documented verdict ("aria-invalid and aria-required are NOT here, and their absence is a verdict rather than an omission" — no keyboard path on the canvas), matching its NOT_APPLICABLE row in the registry sweep. ⇒ no gap outside the boundary, nothing to file, and this PR stays at five widgets.
  • "No test asserts these widgets do NOT mark." No assertion does — the registry sweep's NOT_YET_DELIVERED ledger is empty, and it exercises the form path where these five always marked via the Slot. One doc comment did: fix(fields): FieldEditWidget delivers the declared NON-DOM host-plumbing block #7128's dialog test explained its choice of select with "NOT every inline widget reads errorTextField, BooleanField, DateField, DateTimeField and TimeField do not". That sentence is now false, so it is corrected in place with a note saying what changed and where the text case now lives — not deleted, because it is the reason select was chosen.

Fences honoured

toDomProps.ts, toHostProps.ts and FieldEditWidget.tsx are untouched (git diff --stat names none of them) — that is #7008's landed work and its three compile-time assertions partition the contract. No content/docs/releases/ edit. Sibling lanes (#7121views/studio-design/, #7130packages/plugin-charts) were not entered; no collision seen.


Generated by Claude Code

…slot
objectui#7008 made `FieldEditWidget` deliver the declared `error` key. Of the
27 distinct components in `EDIT_WIDGETS`, 21 read it; five did not, so for
their field types the delivery was inert and `aria-invalid` was never set:
`TextField`, `BooleanField` (`boolean` + `toggle`), `DateField`,
`DateTimeField`, `TimeField`.
Each now computes `aria-invalid={!!error}` AFTER its DOM pass-through spread —
one existing idiom (the objectui#3222 discipline the other 21 share), so a
valid field says an explicit "false" rather than staying mute. `BooleanField`
is the only composite: the mark goes on its Radix `Checkbox` / `Switch`
button, never the wrapping div (objectui#5223).
The form path was never broken (`<FormControl>`'s Radix Slot delivered
`aria-invalid` through each widget's spread) and is unchanged; the gap was
every host without that Slot — the kanban required-fields dialog, the grid's
inline cell editor, the detail page's inline edit.
Marking only: the message text stays with the host.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3152.4 KB3191.4 KB
Main entry chunk (gzip)142.5 KB350 KB
Entry fileindex-BWs4aO43.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.30KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.61KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)248.93KB63.56KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.31KB54.66KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.19KB27.60KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

✅ ACCEPT (on the substance) — PM seat (domain:ui), reviewer of record

⛔ Landing not armed — 5 checks in_progress (4 shards, Type Check), 25 completed, 0 failure. I arm on green.

⭐ Every zero was OPENED AND READ, not counted

The census re-derived at f08bcd9af in a dedicated worktree — ⚠️never the shared checkout, which is the trap that produced a false, card-shrinking reading on this card's own parent. Instrument controlled (NumberField 5, LookupField 11), and then each of the six apparent zeroes was opened and read rather than trusted as a count.

That is what caught UserField — the false zero the dispatch named. I verified it directly: packages/fields/src/widgets/UserField.tsx:58 is return <LookupField {...(props as any)} field={fieldProp} />;, with its docblock stating it "delegates to the shared LookupField". So it has always marked, transitively.

And pinning it as a dedicated control case is the part that will still be paying off in a year — it stops a future reader "fixing" a widget that was never broken, and it fails loudly if the delegation is ever flattened.

⚠️ One attribution I could not confirm: the corroborating quote is credited to data-table.tsx, and no such file exists under packages/fields/src. My probe for it returned zero — and so did my control, so I discarded that reading as void rather than reporting it as a refutation, and re-took it against the source directly. The substance holds; only the quote's file is unverified. ⛔ Not blocking, but do not carry that citation forward without re-locating it.

⭐ The composite was handled exactly as fenced, and verified rather than assumed

BooleanField was the one composite. You marked the Radix Checkbox / Switch button (role="checkbox" / role="switch"), ⛔ not the wrapper div — and gave the reason that matters: a wrapper mark satisfies a subtree query while telling a screen-reader user nothing (objectui#5223). Then you read checkbox.tsx and switch.tsx to confirm both Radix roots forward unknown props to the real button, instead of assuming a spread reaches it.

⭐ And TextField turned out to have two sites — the input and the rows > 1 textarea branch — with the right reason: "marking only one would leave authored rows configs silent." Five widgets, seven call sites. The obvious count was wrong and the diff is right.

The idiom is matched, not invented

Read SelectField / EmailField / NumberField / RadioField / CheckboxesField first; one idiom across all seven sites; aria-invalid written after the DOM pass-through spread so the widget's own computation wins; and !!undefined yielding an explicit "false" so every pin is a two-state reading. ⇒ An unconditional true could not have passed. That last detail is what makes the positive assertions worth anything.

Ablation: predicted 19/2 in the script header before running, observed 19/2

The two survivors are exactly the two control cases (select, a pre-existing reader; user, the false zero) — neither depends on the five, so their survival is the predicted shape rather than luck. Passing count asserted, mutation proven by marker count and blob hash across five files, restore proven by state including git diff --cached and git status --short.

⭐ The focusable-carrier assertion on every positive case is the pin's real backbone: it forecloses exactly the wrapper-mark shortcut the BooleanField decision avoided, so the two halves of this PR check each other.

⭐ You corrected a doc comment your own PR made stale

#7128's RequiredFieldsDialog.ariaInvalid-7008.test.tsx header explained its choice of select with "NOT every inline widget reads error — TextField, BooleanField, DateField, DateTimeField and TimeField do not." True when written; false as of this PR.

Corrected in place with a note, ⛔ not deleted — because it is the stated reason select was chosen, and deleting it would remove the justification along with the staleness. ⇒ That is the seventh instance this session of a comment certifying what the code no longer has, and the first where the author noticed they were the cause and repaired it in the same change.

My population boundary was porous — and you established there is no gap outside it

I assumed EDIT_WIDGETS was the right boundary. Measured: InlineFieldInput renders four widgets outside it onto the same inline surface (ImageField, AvatarField, SignatureField, FileField), already receiving error. Three read and mark it.

⭐ And SignatureField's zero is a documented verdict, not an omission — its own header says "aria-invalid and aria-required are NOT here, and their absence is a verdict rather than an omission" (the canvas has no keyboard path), matching its NOT_APPLICABLE row in the registry sweep.

⇒ Boundary porous, no gap, nothing filed, nothing widened, PR stays at five. Checking outside the fence and reporting "clean" is worth as much as finding something there.

Scope

toDomProps / toHostProps / FieldEditWidget untouched ✅. Changeset staged-then-run with a non-empty population quoted, explicitly distinguished from the 0 file(s) changed empty-population verdict ✅. check:sdui-registration-pins reported NOT MEASURED with the gate's own prerequisite text, not as red ✅. The first type-check was an unbuilt closure, reported as NOT MEASURED and re-run after building ✅.


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 05:18
@os-warren
os-warren added this pull request to the merge queueSep 1, 2026
@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

✅ Reviewed and armed — PM as reviewer-of-record

Undrafted and auto-merge enabled (squash) at head 323f2c381.

CI read in full, in one call

All 30 check runs fetched in a single request with perPage=100. This matters: earlier in this seat I mixed page sizes across two calls and silently skipped items 9–14, and nearly armed a PR off an incomplete reading. A partial check list looks exactly like a green one.

27 success, 3 skipped, 0 failed, 0 pending. The three skipped are dependabot, Test (coverage) and Test (coverage shard ${{ matrix.shard }}/4) — the unexpanded ${{ matrix.shard }} in that last name is itself the evidence they were skipped before matrix expansion, i.e. conditional jobs that did not apply, not required checks that vanished.

Pre-check at arm time: state: open, merged: false, mergeable_state: clean, head sha unchanged from the sha the checks ran against.

Clause ② — not engaged

This consumes the error slot that objectui#7008 / PR objectui#7128 already landed. It adds no export, changes no parser accept/reject behaviour, and touches neither toDomProps.ts, toHostProps.ts nor FieldEditWidget.tsx. It therefore does not need CONTRACT_REVIEW_TIER and is not held by the fable quota exhaustion.

What earned the arm

Three things in this report are the reason it goes in without a round trip:

The census was re-taken after its blocker landed, not reused. The card's population was measured pre-#7128. Re-deriving it at f08bcd9af is the difference between a population and a memory of one.

Every apparent zero was opened, not counted.UserField returning 0 for a word-boundary error match is a FALSE zero — its body is a single delegating return <LookupField {...(props as any)} … />, so it marks transitively and always did. I verified that line directly myself before dispatch, and the dev caught it independently. A census that had trusted the six zeroes would have "fixed" a widget that was never broken, and shipped a change with no defect behind it. Pinning that false zero as a permanent CONTROL case in the sweep is the right durable answer.

The ablation asserts the passing count, not just the red.19 failed | 2 passed (21), predicted before the run, with the 2 survivors being exactly the two control cases that do not depend on the five widgets. A suite that had collapsed to "no tests" would have shown red-free-of-passers and looked like a stronger result. Mutation proven by marker count and blob hash; restore proven by state (git diff HEAD, git diff --cached, git status --short all empty), not by an editor's exit code.

Also correct: type-check first returned a wall of TS2307: Cannot find module '@object-ui/components' and was reported as NOT MEASURED rather than red — an unbuilt dependency closure is the absence of a reading. Building the closure and re-running is the only thing that turns it into a verdict.

The PM assumption that got falsified — good

I fenced the population at EDIT_WIDGETS. That boundary is structurally wrong: InlineFieldInput renders four widgets onto the same inline surface that are not in EDIT_WIDGETS at all. The dev measured them with the same instrument and the same live control rather than accepting my boundary, found ImageField/AvatarField/FileField already marking and SignatureField's zero to be a documented verdict (no keyboard path on the canvas, matching its NOT_APPLICABLE registry row) — and then did not widen the PR. Reporting a falsified boundary that happens to have no gap behind it, instead of either hiding it or expanding scope on it, is exactly the handling I want.

Same for the stale doc comment in #7128's dialog test: corrected in place with a pointer to where the text case now lives, not deleted — it is the reason select was chosen and deleting it would have destroyed that reasoning.

One sub-claim I am recording as unconfirmed, not confirmed

pnpm check:sdui-registration-pins came back PREREQUISITE NOT MET (exit 2) locally — correctly classified as NOT MEASURED rather than green. The report then says "CI builds the console and runs it." I do not see a check run under that name among the 30, so it may be a step inside Build & E2E or it may not run on PRs at all — I have not confirmed it either way.

Arming anyway, because that claim is not load-bearing: the independent argument stands on its own — this diff registers nothing, so a registration-pin gate has no subject in it regardless of whether CI executes the gate. Flagging it so the unconfirmed half is not later remembered as verified.

Will confirm the enqueue positively from the queue ref and post a landing note on objectui#7126 once main actually advances — a queue sha is not a landing, and even main landing on a queue sha is not proof the diff arrived, so the landing gets probed by content with a control.


Generated by Claude Code

Merged via the queue into main with commit e8e4c4dSep 1, 2026
32 checks passed
@os-warren
os-warren deleted the claude/issue-7126-edit-widgets-error-prop branch September 1, 2026 05:32
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding(fields): five EDIT_WIDGETS widgets never read error, so inline text / boolean / date / datetime / time controls still report no aria-invalid

2 participants

@os-warren@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
fix(fields): the last five inline widgets read the delivered `error` slot by os-warren · Pull Request #7141 · objectstack-ai/objectui · GitHub
Skip to content

fix(fields): the last five inline widgets read the delivered error slot - #7141

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-7126-edit-widgets-error-prop
Sep 1, 2026
Merged

fix(fields): the last five inline widgets read the delivered error slot#7141
os-warren merged 1 commit into
mainfrom
claude/issue-7126-edit-widgets-error-prop

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#7126

The last five widgets in EDIT_WIDGETS now read the error slot that objectui#7008 delivers, so an inline text / boolean / toggle / date / datetime / time control that failed validation finally reports aria-invalid.

Leg 0 — the population, re-derived on origin/main AFTER its blocker landed

The census in the card was taken before PR #7128 landed. Re-taken here at f08bcd9af (fix(fields): FieldEditWidget delivers the declared NON-DOM block, from git log origin/main), in a dedicated worktree, never the shared checkout.

Word-boundary error match over the 27 distinct components in EDIT_WIDGETS:

  • 21 read it, 6 return zero — unchanged from the pre-fix(fields): FieldEditWidget delivers the declared NON-DOM host-plumbing block #7128 census.
  • CONTROL for the instrument: NumberField returns 5, LookupField 11, so a zero is a reading and not a broken query.
  • CONTROL for the zeroes — each apparent zero was opened and read, not counted.UserField is a FALSE zero: its whole body is return LookupField {...(props as any)} field={fieldProp}, so it delivers error transitively and has always marked. Independently corroborated in-repo by data-table.tsx, which already records "26 of the 27 components in EDIT_WIDGETS call toDomProps directly; UserField delegates its whole props object to LookupField, which does".

⇒ the population is exactly the five the card namesTextField, BooleanField (boolean + toggle), DateField, DateTimeField, TimeField. Nothing shrank, nothing grew.

What changed

One idiom, five widgets — the objectui#3222 shape the other 21 already share (SelectField, EmailField, NumberField): destructure error, then write aria-invalid={!!error}after the DOM pass-through spread so the widget's own computation wins and a valid field says an explicit "false" rather than staying mute. Seven call sites, because two widgets have two render branches each.

⛔ This buys the a11y MARKING only. The objectui#3222 slot drives aria-invalid and renders no text; the visible message stays with the host (FormMessage in the form, the red hint in the kanban dialog). Nothing that was invisible becomes visible, and that is the defect being closed.

The composite judgement, stated per widget

widgetmarkable control?what was markedwhy
TextFieldyes (x2)the input[type=text]and the textarea of the rows > 1 branchtwo render branches, two real single controls; marking only one would leave authored rows configs silent
BooleanFieldyes (x2)the Radix Checkbox / Switch, i.e. button[role=checkbox] / button[role=switch]the one composite here. The control sits inside a flex div with an sr-only label. The div is deliberately NOT the target: a wrapper mark satisfies a subtree query while telling a screen-reader user nothing (objectui#5223). aria-invalid is a global ARIA attribute, valid on both roles, and both Radix roots forward unknown props to the real button
DateFieldyesthe input[type=date]no composite question despite the name "picker": the browser picker is that same element's own UI, not a second element
DateTimeFieldyesthe input[type=datetime-local]as above
TimeFieldyesthe input[type=time]as above

None of the five turned out to lack a markable control.

The end-to-end case — the deliverable

packages/plugin-kanban/src/__tests__/RequiredFieldsDialog.ariaInvalidText-7126.test.tsx, following the shape of #7128's RequiredFieldsDialog.ariaInvalid-7008.test.tsx rather than inventing a harness:

  1. a required text field reports aria-invalid="false" on a clean open and "true" after a submit attempt finds it empty — with the submit refusal and the visible Required hint asserted first, so a dialog that silently submitted could not pass;
  2. CONTROL, per-field: with a text filled and a select empty, the select is "true" and the filled text is "false", and there is exactly one Required hint — a dialog-wide flag would fail this;
  3. the whole population through the same dialog: text, boolean, toggle, date, datetime, time in one dialog, each read off its own element selector, so a mark that landed on BooleanField's wrapper could not satisfy it.

Plus a factory-level sweep, packages/fields/src/__tests__/inline-widget-aria-invalid-7126.test.tsx: 8 cases (6 types + the two branch variants) x 2 directions, each requiring the aria-invalid="true" carrier to be focusable, plus two controls — select (a widget that already read error, proving the harness measures a real delivery) and user (pinning the census's FALSE zero so nobody later "fixes" a widget that was never broken).

Why the factory and not the form. In the FORM these five were already announced correctly and always had been: FormControl is a Radix Slot whose aria-invalid reached each control through the props spread untouched, and widget-aria-invalid-registry-e2e.test.tsx sweeps that path with an emptyNOT_YET_DELIVERED ledger. A form-based test would have been green before this change. FieldEditWidget renders no Slot, and it is the seam every non-form host composes — so that is the path measured.

Ablation — the pins fail without the change

Direction predicted before the run and recorded in the script header. Mutation: delete all 7 aria-invalid={!!error} lines.

  • Mutation proven on disk, not by an editor exit code: marker count 2/2/1/1/1 -> 0 for all five, and every blob hash moved off its HEAD blob (e.g. TextField.tsxa9a17e42f -> b919aa64d, BooleanField.tsxc8325fff6 -> d2adecc4c, TimeField.tsx239728047 -> 762385d77). No rebuild leg is claimed and none is owed: vitest.config.mts aliases @object-ui/fields to packages/fields/src, so the mutated source is what both pin files resolve — there is no dist on this path.
  • PREDICTED: Tests 19 failed | 2 passed (21).
  • OBSERVED: Tests 19 failed | 2 passed (21) — and the 2 that stayed green are exactly the two CONTROL cases (select, user), which do not depend on the five widgets. The passing count is asserted, not just the red: a suite that had collapsed to "no tests" would not have left 2 passers standing.
  • Restore proven by state, with git checkout HEAD -- ABSOLUTE_PATH (never a bare git checkout --, which reads the index a path-scoped checkout also writes): all five blobs back to their HEAD values, marker counts back to 2/2/1/1/1, and git diff HEAD, git diff --cached and git status --short all empty.

Measurements

All at 323f2c381, the head of this branch.

checkverdict
pnpm exec vitest run packages/fields/ packages/plugin-kanban/ --maxWorkers=2Test Files 151 passed (151) / Tests 2249 passed (2249) — includes the form-path registry sweep and #7128's own dialog pin
pnpm exec vitest run (the 2 components-side negative-assertion files)Test Files 2 passed (2) / Tests 24 passed (24)
pnpm --filter @object-ui/fields --filter @object-ui/plugin-kanban run type-checkexit 0, Scope: 2 of 47 workspace projects, both Done. Its tsconfig.test.json leg provably covers the new teststsc --listFiles lists both new files, so this is not the "typecheck excludes *.test.tsx" non-reading
pnpm --filter @object-ui/fields --filter @object-ui/plugin-kanban run lintexit 0, 0 errors (warnings are pre-existing repo-wide; no --max-warnings in the gate). Plain eslint . per package, no --no-inline-config
pnpm check:control-bytescheck-control-bytes: OK (scanned 5914 tracked text file(s); skipped 85 binary) — plus a self-scan of the 9 changed files with a probe control that hits
pnpm check:vi-mock-specifiers / check:vi-mock-inherit / check:i18n-keysall exit 0
pnpm changeset:checkAll workspace packages are in the changeset fixed group. / No changeset declares a major bump.
node scripts/check-changeset-presence.mjs8 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)
node scripts/check-changeset-overwrite.mjsNo pre-existing changeset was modified or deleted.
pnpm check:sdui-registration-pinsNOT MEASURED, not red — exit 2 with its own PREREQUISITE NOT MET text: "the registrations this gate pins are dropped by a WRONG ARRAY at BUNDLE time, so a run with nothing to read has measured nothing. Build the console first". This change registers nothing; CI builds the console and runs it

type-check first came back with a wall of TS2307: Cannot find module '@object-ui/components' across files this branch never touched — an unbuilt closure, i.e. NOT MEASURED. pnpm --workspace-concurrency=2 --filter '@object-ui/fields^...' --filter '@object-ui/plugin-kanban^...' build (exit 0) fixed it, and the verdict above is from the re-run.

PM assumptions, checked

  • "The five are exactly right post-fix(fields): FieldEditWidget delivers the declared NON-DOM host-plumbing block #7128." Confirmed, with the false-zero trap re-caught rather than re-introduced.
  • "Each of the five has a markable control." Confirmed, per widget, in the table above.
  • "EDIT_WIDGETS is the right population boundary."Partially falsified in structure, not in outcome — reported, not silently widened.InlineFieldInput (@object-ui/plugin-detail) renders four widgets that are NOT in EDIT_WIDGETS onto the same inline surface — ImageField, AvatarField, SignatureField, FileField — and already passes error={error} to each. Measured with the same instrument and the same NumberField-returns-5 control: ImageField 4, AvatarField 4, FileField 10 all read it and mark; SignatureField returns 0, and that zero is a documented verdict ("aria-invalid and aria-required are NOT here, and their absence is a verdict rather than an omission" — no keyboard path on the canvas), matching its NOT_APPLICABLE row in the registry sweep. ⇒ no gap outside the boundary, nothing to file, and this PR stays at five widgets.
  • "No test asserts these widgets do NOT mark." No assertion does — the registry sweep's NOT_YET_DELIVERED ledger is empty, and it exercises the form path where these five always marked via the Slot. One doc comment did: fix(fields): FieldEditWidget delivers the declared NON-DOM host-plumbing block #7128's dialog test explained its choice of select with "NOT every inline widget reads errorTextField, BooleanField, DateField, DateTimeField and TimeField do not". That sentence is now false, so it is corrected in place with a note saying what changed and where the text case now lives — not deleted, because it is the reason select was chosen.

Fences honoured

toDomProps.ts, toHostProps.ts and FieldEditWidget.tsx are untouched (git diff --stat names none of them) — that is #7008's landed work and its three compile-time assertions partition the contract. No content/docs/releases/ edit. Sibling lanes (#7121views/studio-design/, #7130packages/plugin-charts) were not entered; no collision seen.


Generated by Claude Code

…slot
objectui#7008 made `FieldEditWidget` deliver the declared `error` key. Of the
27 distinct components in `EDIT_WIDGETS`, 21 read it; five did not, so for
their field types the delivery was inert and `aria-invalid` was never set:
`TextField`, `BooleanField` (`boolean` + `toggle`), `DateField`,
`DateTimeField`, `TimeField`.
Each now computes `aria-invalid={!!error}` AFTER its DOM pass-through spread —
one existing idiom (the objectui#3222 discipline the other 21 share), so a
valid field says an explicit "false" rather than staying mute. `BooleanField`
is the only composite: the mark goes on its Radix `Checkbox` / `Switch`
button, never the wrapping div (objectui#5223).
The form path was never broken (`<FormControl>`'s Radix Slot delivered
`aria-invalid` through each widget's spread) and is unchanged; the gap was
every host without that Slot — the kanban required-fields dialog, the grid's
inline cell editor, the detail page's inline edit.
Marking only: the message text stays with the host.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3152.4 KB3191.4 KB
Main entry chunk (gzip)142.5 KB350 KB
Entry fileindex-BWs4aO43.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.30KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.61KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)248.93KB63.56KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.31KB54.66KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.19KB27.60KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

✅ ACCEPT (on the substance) — PM seat (domain:ui), reviewer of record

⛔ Landing not armed — 5 checks in_progress (4 shards, Type Check), 25 completed, 0 failure. I arm on green.

⭐ Every zero was OPENED AND READ, not counted

The census re-derived at f08bcd9af in a dedicated worktree — ⚠️never the shared checkout, which is the trap that produced a false, card-shrinking reading on this card's own parent. Instrument controlled (NumberField 5, LookupField 11), and then each of the six apparent zeroes was opened and read rather than trusted as a count.

That is what caught UserField — the false zero the dispatch named. I verified it directly: packages/fields/src/widgets/UserField.tsx:58 is return <LookupField {...(props as any)} field={fieldProp} />;, with its docblock stating it "delegates to the shared LookupField". So it has always marked, transitively.

And pinning it as a dedicated control case is the part that will still be paying off in a year — it stops a future reader "fixing" a widget that was never broken, and it fails loudly if the delegation is ever flattened.

⚠️ One attribution I could not confirm: the corroborating quote is credited to data-table.tsx, and no such file exists under packages/fields/src. My probe for it returned zero — and so did my control, so I discarded that reading as void rather than reporting it as a refutation, and re-took it against the source directly. The substance holds; only the quote's file is unverified. ⛔ Not blocking, but do not carry that citation forward without re-locating it.

⭐ The composite was handled exactly as fenced, and verified rather than assumed

BooleanField was the one composite. You marked the Radix Checkbox / Switch button (role="checkbox" / role="switch"), ⛔ not the wrapper div — and gave the reason that matters: a wrapper mark satisfies a subtree query while telling a screen-reader user nothing (objectui#5223). Then you read checkbox.tsx and switch.tsx to confirm both Radix roots forward unknown props to the real button, instead of assuming a spread reaches it.

⭐ And TextField turned out to have two sites — the input and the rows > 1 textarea branch — with the right reason: "marking only one would leave authored rows configs silent." Five widgets, seven call sites. The obvious count was wrong and the diff is right.

The idiom is matched, not invented

Read SelectField / EmailField / NumberField / RadioField / CheckboxesField first; one idiom across all seven sites; aria-invalid written after the DOM pass-through spread so the widget's own computation wins; and !!undefined yielding an explicit "false" so every pin is a two-state reading. ⇒ An unconditional true could not have passed. That last detail is what makes the positive assertions worth anything.

Ablation: predicted 19/2 in the script header before running, observed 19/2

The two survivors are exactly the two control cases (select, a pre-existing reader; user, the false zero) — neither depends on the five, so their survival is the predicted shape rather than luck. Passing count asserted, mutation proven by marker count and blob hash across five files, restore proven by state including git diff --cached and git status --short.

⭐ The focusable-carrier assertion on every positive case is the pin's real backbone: it forecloses exactly the wrapper-mark shortcut the BooleanField decision avoided, so the two halves of this PR check each other.

⭐ You corrected a doc comment your own PR made stale

#7128's RequiredFieldsDialog.ariaInvalid-7008.test.tsx header explained its choice of select with "NOT every inline widget reads error — TextField, BooleanField, DateField, DateTimeField and TimeField do not." True when written; false as of this PR.

Corrected in place with a note, ⛔ not deleted — because it is the stated reason select was chosen, and deleting it would remove the justification along with the staleness. ⇒ That is the seventh instance this session of a comment certifying what the code no longer has, and the first where the author noticed they were the cause and repaired it in the same change.

My population boundary was porous — and you established there is no gap outside it

I assumed EDIT_WIDGETS was the right boundary. Measured: InlineFieldInput renders four widgets outside it onto the same inline surface (ImageField, AvatarField, SignatureField, FileField), already receiving error. Three read and mark it.

⭐ And SignatureField's zero is a documented verdict, not an omission — its own header says "aria-invalid and aria-required are NOT here, and their absence is a verdict rather than an omission" (the canvas has no keyboard path), matching its NOT_APPLICABLE row in the registry sweep.

⇒ Boundary porous, no gap, nothing filed, nothing widened, PR stays at five. Checking outside the fence and reporting "clean" is worth as much as finding something there.

Scope

toDomProps / toHostProps / FieldEditWidget untouched ✅. Changeset staged-then-run with a non-empty population quoted, explicitly distinguished from the 0 file(s) changed empty-population verdict ✅. check:sdui-registration-pins reported NOT MEASURED with the gate's own prerequisite text, not as red ✅. The first type-check was an unbuilt closure, reported as NOT MEASURED and re-run after building ✅.


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 05:18
@os-warren
os-warren added this pull request to the merge queueSep 1, 2026
@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

✅ Reviewed and armed — PM as reviewer-of-record

Undrafted and auto-merge enabled (squash) at head 323f2c381.

CI read in full, in one call

All 30 check runs fetched in a single request with perPage=100. This matters: earlier in this seat I mixed page sizes across two calls and silently skipped items 9–14, and nearly armed a PR off an incomplete reading. A partial check list looks exactly like a green one.

27 success, 3 skipped, 0 failed, 0 pending. The three skipped are dependabot, Test (coverage) and Test (coverage shard ${{ matrix.shard }}/4) — the unexpanded ${{ matrix.shard }} in that last name is itself the evidence they were skipped before matrix expansion, i.e. conditional jobs that did not apply, not required checks that vanished.

Pre-check at arm time: state: open, merged: false, mergeable_state: clean, head sha unchanged from the sha the checks ran against.

Clause ② — not engaged

This consumes the error slot that objectui#7008 / PR objectui#7128 already landed. It adds no export, changes no parser accept/reject behaviour, and touches neither toDomProps.ts, toHostProps.ts nor FieldEditWidget.tsx. It therefore does not need CONTRACT_REVIEW_TIER and is not held by the fable quota exhaustion.

What earned the arm

Three things in this report are the reason it goes in without a round trip:

The census was re-taken after its blocker landed, not reused. The card's population was measured pre-#7128. Re-deriving it at f08bcd9af is the difference between a population and a memory of one.

Every apparent zero was opened, not counted.UserField returning 0 for a word-boundary error match is a FALSE zero — its body is a single delegating return <LookupField {...(props as any)} … />, so it marks transitively and always did. I verified that line directly myself before dispatch, and the dev caught it independently. A census that had trusted the six zeroes would have "fixed" a widget that was never broken, and shipped a change with no defect behind it. Pinning that false zero as a permanent CONTROL case in the sweep is the right durable answer.

The ablation asserts the passing count, not just the red.19 failed | 2 passed (21), predicted before the run, with the 2 survivors being exactly the two control cases that do not depend on the five widgets. A suite that had collapsed to "no tests" would have shown red-free-of-passers and looked like a stronger result. Mutation proven by marker count and blob hash; restore proven by state (git diff HEAD, git diff --cached, git status --short all empty), not by an editor's exit code.

Also correct: type-check first returned a wall of TS2307: Cannot find module '@object-ui/components' and was reported as NOT MEASURED rather than red — an unbuilt dependency closure is the absence of a reading. Building the closure and re-running is the only thing that turns it into a verdict.

The PM assumption that got falsified — good

I fenced the population at EDIT_WIDGETS. That boundary is structurally wrong: InlineFieldInput renders four widgets onto the same inline surface that are not in EDIT_WIDGETS at all. The dev measured them with the same instrument and the same live control rather than accepting my boundary, found ImageField/AvatarField/FileField already marking and SignatureField's zero to be a documented verdict (no keyboard path on the canvas, matching its NOT_APPLICABLE registry row) — and then did not widen the PR. Reporting a falsified boundary that happens to have no gap behind it, instead of either hiding it or expanding scope on it, is exactly the handling I want.

Same for the stale doc comment in #7128's dialog test: corrected in place with a pointer to where the text case now lives, not deleted — it is the reason select was chosen and deleting it would have destroyed that reasoning.

One sub-claim I am recording as unconfirmed, not confirmed

pnpm check:sdui-registration-pins came back PREREQUISITE NOT MET (exit 2) locally — correctly classified as NOT MEASURED rather than green. The report then says "CI builds the console and runs it." I do not see a check run under that name among the 30, so it may be a step inside Build & E2E or it may not run on PRs at all — I have not confirmed it either way.

Arming anyway, because that claim is not load-bearing: the independent argument stands on its own — this diff registers nothing, so a registration-pin gate has no subject in it regardless of whether CI executes the gate. Flagging it so the unconfirmed half is not later remembered as verified.

Will confirm the enqueue positively from the queue ref and post a landing note on objectui#7126 once main actually advances — a queue sha is not a landing, and even main landing on a queue sha is not proof the diff arrived, so the landing gets probed by content with a control.


Generated by Claude Code

Merged via the queue into main with commit e8e4c4dSep 1, 2026
32 checks passed
@os-warren
os-warren deleted the claude/issue-7126-edit-widgets-error-prop branch September 1, 2026 05:32
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding(fields): five EDIT_WIDGETS widgets never read error, so inline text / boolean / date / datetime / time controls still report no aria-invalid

2 participants

@os-warren@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(fields): the last five inline widgets read the delivered `error` slot by os-warren · Pull Request #7141 · objectstack-ai/objectui · GitHub
Skip to content

fix(fields): the last five inline widgets read the delivered error slot - #7141

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-7126-edit-widgets-error-prop
Sep 1, 2026
Merged

fix(fields): the last five inline widgets read the delivered error slot#7141
os-warren merged 1 commit into
mainfrom
claude/issue-7126-edit-widgets-error-prop

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#7126

The last five widgets in EDIT_WIDGETS now read the error slot that objectui#7008 delivers, so an inline text / boolean / toggle / date / datetime / time control that failed validation finally reports aria-invalid.

Leg 0 — the population, re-derived on origin/main AFTER its blocker landed

The census in the card was taken before PR #7128 landed. Re-taken here at f08bcd9af (fix(fields): FieldEditWidget delivers the declared NON-DOM block, from git log origin/main), in a dedicated worktree, never the shared checkout.

Word-boundary error match over the 27 distinct components in EDIT_WIDGETS:

  • 21 read it, 6 return zero — unchanged from the pre-fix(fields): FieldEditWidget delivers the declared NON-DOM host-plumbing block #7128 census.
  • CONTROL for the instrument: NumberField returns 5, LookupField 11, so a zero is a reading and not a broken query.
  • CONTROL for the zeroes — each apparent zero was opened and read, not counted.UserField is a FALSE zero: its whole body is return LookupField {...(props as any)} field={fieldProp}, so it delivers error transitively and has always marked. Independently corroborated in-repo by data-table.tsx, which already records "26 of the 27 components in EDIT_WIDGETS call toDomProps directly; UserField delegates its whole props object to LookupField, which does".

⇒ the population is exactly the five the card namesTextField, BooleanField (boolean + toggle), DateField, DateTimeField, TimeField. Nothing shrank, nothing grew.

What changed

One idiom, five widgets — the objectui#3222 shape the other 21 already share (SelectField, EmailField, NumberField): destructure error, then write aria-invalid={!!error}after the DOM pass-through spread so the widget's own computation wins and a valid field says an explicit "false" rather than staying mute. Seven call sites, because two widgets have two render branches each.

⛔ This buys the a11y MARKING only. The objectui#3222 slot drives aria-invalid and renders no text; the visible message stays with the host (FormMessage in the form, the red hint in the kanban dialog). Nothing that was invisible becomes visible, and that is the defect being closed.

The composite judgement, stated per widget

widgetmarkable control?what was markedwhy
TextFieldyes (x2)the input[type=text]and the textarea of the rows > 1 branchtwo render branches, two real single controls; marking only one would leave authored rows configs silent
BooleanFieldyes (x2)the Radix Checkbox / Switch, i.e. button[role=checkbox] / button[role=switch]the one composite here. The control sits inside a flex div with an sr-only label. The div is deliberately NOT the target: a wrapper mark satisfies a subtree query while telling a screen-reader user nothing (objectui#5223). aria-invalid is a global ARIA attribute, valid on both roles, and both Radix roots forward unknown props to the real button
DateFieldyesthe input[type=date]no composite question despite the name "picker": the browser picker is that same element's own UI, not a second element
DateTimeFieldyesthe input[type=datetime-local]as above
TimeFieldyesthe input[type=time]as above

None of the five turned out to lack a markable control.

The end-to-end case — the deliverable

packages/plugin-kanban/src/__tests__/RequiredFieldsDialog.ariaInvalidText-7126.test.tsx, following the shape of #7128's RequiredFieldsDialog.ariaInvalid-7008.test.tsx rather than inventing a harness:

  1. a required text field reports aria-invalid="false" on a clean open and "true" after a submit attempt finds it empty — with the submit refusal and the visible Required hint asserted first, so a dialog that silently submitted could not pass;
  2. CONTROL, per-field: with a text filled and a select empty, the select is "true" and the filled text is "false", and there is exactly one Required hint — a dialog-wide flag would fail this;
  3. the whole population through the same dialog: text, boolean, toggle, date, datetime, time in one dialog, each read off its own element selector, so a mark that landed on BooleanField's wrapper could not satisfy it.

Plus a factory-level sweep, packages/fields/src/__tests__/inline-widget-aria-invalid-7126.test.tsx: 8 cases (6 types + the two branch variants) x 2 directions, each requiring the aria-invalid="true" carrier to be focusable, plus two controls — select (a widget that already read error, proving the harness measures a real delivery) and user (pinning the census's FALSE zero so nobody later "fixes" a widget that was never broken).

Why the factory and not the form. In the FORM these five were already announced correctly and always had been: FormControl is a Radix Slot whose aria-invalid reached each control through the props spread untouched, and widget-aria-invalid-registry-e2e.test.tsx sweeps that path with an emptyNOT_YET_DELIVERED ledger. A form-based test would have been green before this change. FieldEditWidget renders no Slot, and it is the seam every non-form host composes — so that is the path measured.

Ablation — the pins fail without the change

Direction predicted before the run and recorded in the script header. Mutation: delete all 7 aria-invalid={!!error} lines.

  • Mutation proven on disk, not by an editor exit code: marker count 2/2/1/1/1 -> 0 for all five, and every blob hash moved off its HEAD blob (e.g. TextField.tsxa9a17e42f -> b919aa64d, BooleanField.tsxc8325fff6 -> d2adecc4c, TimeField.tsx239728047 -> 762385d77). No rebuild leg is claimed and none is owed: vitest.config.mts aliases @object-ui/fields to packages/fields/src, so the mutated source is what both pin files resolve — there is no dist on this path.
  • PREDICTED: Tests 19 failed | 2 passed (21).
  • OBSERVED: Tests 19 failed | 2 passed (21) — and the 2 that stayed green are exactly the two CONTROL cases (select, user), which do not depend on the five widgets. The passing count is asserted, not just the red: a suite that had collapsed to "no tests" would not have left 2 passers standing.
  • Restore proven by state, with git checkout HEAD -- ABSOLUTE_PATH (never a bare git checkout --, which reads the index a path-scoped checkout also writes): all five blobs back to their HEAD values, marker counts back to 2/2/1/1/1, and git diff HEAD, git diff --cached and git status --short all empty.

Measurements

All at 323f2c381, the head of this branch.

checkverdict
pnpm exec vitest run packages/fields/ packages/plugin-kanban/ --maxWorkers=2Test Files 151 passed (151) / Tests 2249 passed (2249) — includes the form-path registry sweep and #7128's own dialog pin
pnpm exec vitest run (the 2 components-side negative-assertion files)Test Files 2 passed (2) / Tests 24 passed (24)
pnpm --filter @object-ui/fields --filter @object-ui/plugin-kanban run type-checkexit 0, Scope: 2 of 47 workspace projects, both Done. Its tsconfig.test.json leg provably covers the new teststsc --listFiles lists both new files, so this is not the "typecheck excludes *.test.tsx" non-reading
pnpm --filter @object-ui/fields --filter @object-ui/plugin-kanban run lintexit 0, 0 errors (warnings are pre-existing repo-wide; no --max-warnings in the gate). Plain eslint . per package, no --no-inline-config
pnpm check:control-bytescheck-control-bytes: OK (scanned 5914 tracked text file(s); skipped 85 binary) — plus a self-scan of the 9 changed files with a probe control that hits
pnpm check:vi-mock-specifiers / check:vi-mock-inherit / check:i18n-keysall exit 0
pnpm changeset:checkAll workspace packages are in the changeset fixed group. / No changeset declares a major bump.
node scripts/check-changeset-presence.mjs8 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)
node scripts/check-changeset-overwrite.mjsNo pre-existing changeset was modified or deleted.
pnpm check:sdui-registration-pinsNOT MEASURED, not red — exit 2 with its own PREREQUISITE NOT MET text: "the registrations this gate pins are dropped by a WRONG ARRAY at BUNDLE time, so a run with nothing to read has measured nothing. Build the console first". This change registers nothing; CI builds the console and runs it

type-check first came back with a wall of TS2307: Cannot find module '@object-ui/components' across files this branch never touched — an unbuilt closure, i.e. NOT MEASURED. pnpm --workspace-concurrency=2 --filter '@object-ui/fields^...' --filter '@object-ui/plugin-kanban^...' build (exit 0) fixed it, and the verdict above is from the re-run.

PM assumptions, checked

  • "The five are exactly right post-fix(fields): FieldEditWidget delivers the declared NON-DOM host-plumbing block #7128." Confirmed, with the false-zero trap re-caught rather than re-introduced.
  • "Each of the five has a markable control." Confirmed, per widget, in the table above.
  • "EDIT_WIDGETS is the right population boundary."Partially falsified in structure, not in outcome — reported, not silently widened.InlineFieldInput (@object-ui/plugin-detail) renders four widgets that are NOT in EDIT_WIDGETS onto the same inline surface — ImageField, AvatarField, SignatureField, FileField — and already passes error={error} to each. Measured with the same instrument and the same NumberField-returns-5 control: ImageField 4, AvatarField 4, FileField 10 all read it and mark; SignatureField returns 0, and that zero is a documented verdict ("aria-invalid and aria-required are NOT here, and their absence is a verdict rather than an omission" — no keyboard path on the canvas), matching its NOT_APPLICABLE row in the registry sweep. ⇒ no gap outside the boundary, nothing to file, and this PR stays at five widgets.
  • "No test asserts these widgets do NOT mark." No assertion does — the registry sweep's NOT_YET_DELIVERED ledger is empty, and it exercises the form path where these five always marked via the Slot. One doc comment did: fix(fields): FieldEditWidget delivers the declared NON-DOM host-plumbing block #7128's dialog test explained its choice of select with "NOT every inline widget reads errorTextField, BooleanField, DateField, DateTimeField and TimeField do not". That sentence is now false, so it is corrected in place with a note saying what changed and where the text case now lives — not deleted, because it is the reason select was chosen.

Fences honoured

toDomProps.ts, toHostProps.ts and FieldEditWidget.tsx are untouched (git diff --stat names none of them) — that is #7008's landed work and its three compile-time assertions partition the contract. No content/docs/releases/ edit. Sibling lanes (#7121views/studio-design/, #7130packages/plugin-charts) were not entered; no collision seen.


Generated by Claude Code

…slot
objectui#7008 made `FieldEditWidget` deliver the declared `error` key. Of the
27 distinct components in `EDIT_WIDGETS`, 21 read it; five did not, so for
their field types the delivery was inert and `aria-invalid` was never set:
`TextField`, `BooleanField` (`boolean` + `toggle`), `DateField`,
`DateTimeField`, `TimeField`.
Each now computes `aria-invalid={!!error}` AFTER its DOM pass-through spread —
one existing idiom (the objectui#3222 discipline the other 21 share), so a
valid field says an explicit "false" rather than staying mute. `BooleanField`
is the only composite: the mark goes on its Radix `Checkbox` / `Switch`
button, never the wrapping div (objectui#5223).
The form path was never broken (`<FormControl>`'s Radix Slot delivered
`aria-invalid` through each widget's spread) and is unchanged; the gap was
every host without that Slot — the kanban required-fields dialog, the grid's
inline cell editor, the detail page's inline edit.
Marking only: the message text stays with the host.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3152.4 KB3191.4 KB
Main entry chunk (gzip)142.5 KB350 KB
Entry fileindex-BWs4aO43.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.30KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.61KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)248.93KB63.56KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.31KB54.66KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.19KB27.60KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

✅ ACCEPT (on the substance) — PM seat (domain:ui), reviewer of record

⛔ Landing not armed — 5 checks in_progress (4 shards, Type Check), 25 completed, 0 failure. I arm on green.

⭐ Every zero was OPENED AND READ, not counted

The census re-derived at f08bcd9af in a dedicated worktree — ⚠️never the shared checkout, which is the trap that produced a false, card-shrinking reading on this card's own parent. Instrument controlled (NumberField 5, LookupField 11), and then each of the six apparent zeroes was opened and read rather than trusted as a count.

That is what caught UserField — the false zero the dispatch named. I verified it directly: packages/fields/src/widgets/UserField.tsx:58 is return <LookupField {...(props as any)} field={fieldProp} />;, with its docblock stating it "delegates to the shared LookupField". So it has always marked, transitively.

And pinning it as a dedicated control case is the part that will still be paying off in a year — it stops a future reader "fixing" a widget that was never broken, and it fails loudly if the delegation is ever flattened.

⚠️ One attribution I could not confirm: the corroborating quote is credited to data-table.tsx, and no such file exists under packages/fields/src. My probe for it returned zero — and so did my control, so I discarded that reading as void rather than reporting it as a refutation, and re-took it against the source directly. The substance holds; only the quote's file is unverified. ⛔ Not blocking, but do not carry that citation forward without re-locating it.

⭐ The composite was handled exactly as fenced, and verified rather than assumed

BooleanField was the one composite. You marked the Radix Checkbox / Switch button (role="checkbox" / role="switch"), ⛔ not the wrapper div — and gave the reason that matters: a wrapper mark satisfies a subtree query while telling a screen-reader user nothing (objectui#5223). Then you read checkbox.tsx and switch.tsx to confirm both Radix roots forward unknown props to the real button, instead of assuming a spread reaches it.

⭐ And TextField turned out to have two sites — the input and the rows > 1 textarea branch — with the right reason: "marking only one would leave authored rows configs silent." Five widgets, seven call sites. The obvious count was wrong and the diff is right.

The idiom is matched, not invented

Read SelectField / EmailField / NumberField / RadioField / CheckboxesField first; one idiom across all seven sites; aria-invalid written after the DOM pass-through spread so the widget's own computation wins; and !!undefined yielding an explicit "false" so every pin is a two-state reading. ⇒ An unconditional true could not have passed. That last detail is what makes the positive assertions worth anything.

Ablation: predicted 19/2 in the script header before running, observed 19/2

The two survivors are exactly the two control cases (select, a pre-existing reader; user, the false zero) — neither depends on the five, so their survival is the predicted shape rather than luck. Passing count asserted, mutation proven by marker count and blob hash across five files, restore proven by state including git diff --cached and git status --short.

⭐ The focusable-carrier assertion on every positive case is the pin's real backbone: it forecloses exactly the wrapper-mark shortcut the BooleanField decision avoided, so the two halves of this PR check each other.

⭐ You corrected a doc comment your own PR made stale

#7128's RequiredFieldsDialog.ariaInvalid-7008.test.tsx header explained its choice of select with "NOT every inline widget reads error — TextField, BooleanField, DateField, DateTimeField and TimeField do not." True when written; false as of this PR.

Corrected in place with a note, ⛔ not deleted — because it is the stated reason select was chosen, and deleting it would remove the justification along with the staleness. ⇒ That is the seventh instance this session of a comment certifying what the code no longer has, and the first where the author noticed they were the cause and repaired it in the same change.

My population boundary was porous — and you established there is no gap outside it

I assumed EDIT_WIDGETS was the right boundary. Measured: InlineFieldInput renders four widgets outside it onto the same inline surface (ImageField, AvatarField, SignatureField, FileField), already receiving error. Three read and mark it.

⭐ And SignatureField's zero is a documented verdict, not an omission — its own header says "aria-invalid and aria-required are NOT here, and their absence is a verdict rather than an omission" (the canvas has no keyboard path), matching its NOT_APPLICABLE row in the registry sweep.

⇒ Boundary porous, no gap, nothing filed, nothing widened, PR stays at five. Checking outside the fence and reporting "clean" is worth as much as finding something there.

Scope

toDomProps / toHostProps / FieldEditWidget untouched ✅. Changeset staged-then-run with a non-empty population quoted, explicitly distinguished from the 0 file(s) changed empty-population verdict ✅. check:sdui-registration-pins reported NOT MEASURED with the gate's own prerequisite text, not as red ✅. The first type-check was an unbuilt closure, reported as NOT MEASURED and re-run after building ✅.


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 05:18
@os-warren
os-warren added this pull request to the merge queueSep 1, 2026
@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

✅ Reviewed and armed — PM as reviewer-of-record

Undrafted and auto-merge enabled (squash) at head 323f2c381.

CI read in full, in one call

All 30 check runs fetched in a single request with perPage=100. This matters: earlier in this seat I mixed page sizes across two calls and silently skipped items 9–14, and nearly armed a PR off an incomplete reading. A partial check list looks exactly like a green one.

27 success, 3 skipped, 0 failed, 0 pending. The three skipped are dependabot, Test (coverage) and Test (coverage shard ${{ matrix.shard }}/4) — the unexpanded ${{ matrix.shard }} in that last name is itself the evidence they were skipped before matrix expansion, i.e. conditional jobs that did not apply, not required checks that vanished.

Pre-check at arm time: state: open, merged: false, mergeable_state: clean, head sha unchanged from the sha the checks ran against.

Clause ② — not engaged

This consumes the error slot that objectui#7008 / PR objectui#7128 already landed. It adds no export, changes no parser accept/reject behaviour, and touches neither toDomProps.ts, toHostProps.ts nor FieldEditWidget.tsx. It therefore does not need CONTRACT_REVIEW_TIER and is not held by the fable quota exhaustion.

What earned the arm

Three things in this report are the reason it goes in without a round trip:

The census was re-taken after its blocker landed, not reused. The card's population was measured pre-#7128. Re-deriving it at f08bcd9af is the difference between a population and a memory of one.

Every apparent zero was opened, not counted.UserField returning 0 for a word-boundary error match is a FALSE zero — its body is a single delegating return <LookupField {...(props as any)} … />, so it marks transitively and always did. I verified that line directly myself before dispatch, and the dev caught it independently. A census that had trusted the six zeroes would have "fixed" a widget that was never broken, and shipped a change with no defect behind it. Pinning that false zero as a permanent CONTROL case in the sweep is the right durable answer.

The ablation asserts the passing count, not just the red.19 failed | 2 passed (21), predicted before the run, with the 2 survivors being exactly the two control cases that do not depend on the five widgets. A suite that had collapsed to "no tests" would have shown red-free-of-passers and looked like a stronger result. Mutation proven by marker count and blob hash; restore proven by state (git diff HEAD, git diff --cached, git status --short all empty), not by an editor's exit code.

Also correct: type-check first returned a wall of TS2307: Cannot find module '@object-ui/components' and was reported as NOT MEASURED rather than red — an unbuilt dependency closure is the absence of a reading. Building the closure and re-running is the only thing that turns it into a verdict.

The PM assumption that got falsified — good

I fenced the population at EDIT_WIDGETS. That boundary is structurally wrong: InlineFieldInput renders four widgets onto the same inline surface that are not in EDIT_WIDGETS at all. The dev measured them with the same instrument and the same live control rather than accepting my boundary, found ImageField/AvatarField/FileField already marking and SignatureField's zero to be a documented verdict (no keyboard path on the canvas, matching its NOT_APPLICABLE registry row) — and then did not widen the PR. Reporting a falsified boundary that happens to have no gap behind it, instead of either hiding it or expanding scope on it, is exactly the handling I want.

Same for the stale doc comment in #7128's dialog test: corrected in place with a pointer to where the text case now lives, not deleted — it is the reason select was chosen and deleting it would have destroyed that reasoning.

One sub-claim I am recording as unconfirmed, not confirmed

pnpm check:sdui-registration-pins came back PREREQUISITE NOT MET (exit 2) locally — correctly classified as NOT MEASURED rather than green. The report then says "CI builds the console and runs it." I do not see a check run under that name among the 30, so it may be a step inside Build & E2E or it may not run on PRs at all — I have not confirmed it either way.

Arming anyway, because that claim is not load-bearing: the independent argument stands on its own — this diff registers nothing, so a registration-pin gate has no subject in it regardless of whether CI executes the gate. Flagging it so the unconfirmed half is not later remembered as verified.

Will confirm the enqueue positively from the queue ref and post a landing note on objectui#7126 once main actually advances — a queue sha is not a landing, and even main landing on a queue sha is not proof the diff arrived, so the landing gets probed by content with a control.


Generated by Claude Code

Merged via the queue into main with commit e8e4c4dSep 1, 2026
32 checks passed
@os-warren
os-warren deleted the claude/issue-7126-edit-widgets-error-prop branch September 1, 2026 05:32
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding(fields): five EDIT_WIDGETS widgets never read error, so inline text / boolean / date / datetime / time controls still report no aria-invalid

2 participants

@os-warren@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(fields): the last five inline widgets read the delivered `error` slot by os-warren · Pull Request #7141 · objectstack-ai/objectui · GitHub
Skip to content

fix(fields): the last five inline widgets read the delivered error slot - #7141

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-7126-edit-widgets-error-prop
Sep 1, 2026
Merged

fix(fields): the last five inline widgets read the delivered error slot#7141
os-warren merged 1 commit into
mainfrom
claude/issue-7126-edit-widgets-error-prop

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#7126

The last five widgets in EDIT_WIDGETS now read the error slot that objectui#7008 delivers, so an inline text / boolean / toggle / date / datetime / time control that failed validation finally reports aria-invalid.

Leg 0 — the population, re-derived on origin/main AFTER its blocker landed

The census in the card was taken before PR #7128 landed. Re-taken here at f08bcd9af (fix(fields): FieldEditWidget delivers the declared NON-DOM block, from git log origin/main), in a dedicated worktree, never the shared checkout.

Word-boundary error match over the 27 distinct components in EDIT_WIDGETS:

  • 21 read it, 6 return zero — unchanged from the pre-fix(fields): FieldEditWidget delivers the declared NON-DOM host-plumbing block #7128 census.
  • CONTROL for the instrument: NumberField returns 5, LookupField 11, so a zero is a reading and not a broken query.
  • CONTROL for the zeroes — each apparent zero was opened and read, not counted.UserField is a FALSE zero: its whole body is return LookupField {...(props as any)} field={fieldProp}, so it delivers error transitively and has always marked. Independently corroborated in-repo by data-table.tsx, which already records "26 of the 27 components in EDIT_WIDGETS call toDomProps directly; UserField delegates its whole props object to LookupField, which does".

⇒ the population is exactly the five the card namesTextField, BooleanField (boolean + toggle), DateField, DateTimeField, TimeField. Nothing shrank, nothing grew.

What changed

One idiom, five widgets — the objectui#3222 shape the other 21 already share (SelectField, EmailField, NumberField): destructure error, then write aria-invalid={!!error}after the DOM pass-through spread so the widget's own computation wins and a valid field says an explicit "false" rather than staying mute. Seven call sites, because two widgets have two render branches each.

⛔ This buys the a11y MARKING only. The objectui#3222 slot drives aria-invalid and renders no text; the visible message stays with the host (FormMessage in the form, the red hint in the kanban dialog). Nothing that was invisible becomes visible, and that is the defect being closed.

The composite judgement, stated per widget

widgetmarkable control?what was markedwhy
TextFieldyes (x2)the input[type=text]and the textarea of the rows > 1 branchtwo render branches, two real single controls; marking only one would leave authored rows configs silent
BooleanFieldyes (x2)the Radix Checkbox / Switch, i.e. button[role=checkbox] / button[role=switch]the one composite here. The control sits inside a flex div with an sr-only label. The div is deliberately NOT the target: a wrapper mark satisfies a subtree query while telling a screen-reader user nothing (objectui#5223). aria-invalid is a global ARIA attribute, valid on both roles, and both Radix roots forward unknown props to the real button
DateFieldyesthe input[type=date]no composite question despite the name "picker": the browser picker is that same element's own UI, not a second element
DateTimeFieldyesthe input[type=datetime-local]as above
TimeFieldyesthe input[type=time]as above

None of the five turned out to lack a markable control.

The end-to-end case — the deliverable

packages/plugin-kanban/src/__tests__/RequiredFieldsDialog.ariaInvalidText-7126.test.tsx, following the shape of #7128's RequiredFieldsDialog.ariaInvalid-7008.test.tsx rather than inventing a harness:

  1. a required text field reports aria-invalid="false" on a clean open and "true" after a submit attempt finds it empty — with the submit refusal and the visible Required hint asserted first, so a dialog that silently submitted could not pass;
  2. CONTROL, per-field: with a text filled and a select empty, the select is "true" and the filled text is "false", and there is exactly one Required hint — a dialog-wide flag would fail this;
  3. the whole population through the same dialog: text, boolean, toggle, date, datetime, time in one dialog, each read off its own element selector, so a mark that landed on BooleanField's wrapper could not satisfy it.

Plus a factory-level sweep, packages/fields/src/__tests__/inline-widget-aria-invalid-7126.test.tsx: 8 cases (6 types + the two branch variants) x 2 directions, each requiring the aria-invalid="true" carrier to be focusable, plus two controls — select (a widget that already read error, proving the harness measures a real delivery) and user (pinning the census's FALSE zero so nobody later "fixes" a widget that was never broken).

Why the factory and not the form. In the FORM these five were already announced correctly and always had been: FormControl is a Radix Slot whose aria-invalid reached each control through the props spread untouched, and widget-aria-invalid-registry-e2e.test.tsx sweeps that path with an emptyNOT_YET_DELIVERED ledger. A form-based test would have been green before this change. FieldEditWidget renders no Slot, and it is the seam every non-form host composes — so that is the path measured.

Ablation — the pins fail without the change

Direction predicted before the run and recorded in the script header. Mutation: delete all 7 aria-invalid={!!error} lines.

  • Mutation proven on disk, not by an editor exit code: marker count 2/2/1/1/1 -> 0 for all five, and every blob hash moved off its HEAD blob (e.g. TextField.tsxa9a17e42f -> b919aa64d, BooleanField.tsxc8325fff6 -> d2adecc4c, TimeField.tsx239728047 -> 762385d77). No rebuild leg is claimed and none is owed: vitest.config.mts aliases @object-ui/fields to packages/fields/src, so the mutated source is what both pin files resolve — there is no dist on this path.
  • PREDICTED: Tests 19 failed | 2 passed (21).
  • OBSERVED: Tests 19 failed | 2 passed (21) — and the 2 that stayed green are exactly the two CONTROL cases (select, user), which do not depend on the five widgets. The passing count is asserted, not just the red: a suite that had collapsed to "no tests" would not have left 2 passers standing.
  • Restore proven by state, with git checkout HEAD -- ABSOLUTE_PATH (never a bare git checkout --, which reads the index a path-scoped checkout also writes): all five blobs back to their HEAD values, marker counts back to 2/2/1/1/1, and git diff HEAD, git diff --cached and git status --short all empty.

Measurements

All at 323f2c381, the head of this branch.

checkverdict
pnpm exec vitest run packages/fields/ packages/plugin-kanban/ --maxWorkers=2Test Files 151 passed (151) / Tests 2249 passed (2249) — includes the form-path registry sweep and #7128's own dialog pin
pnpm exec vitest run (the 2 components-side negative-assertion files)Test Files 2 passed (2) / Tests 24 passed (24)
pnpm --filter @object-ui/fields --filter @object-ui/plugin-kanban run type-checkexit 0, Scope: 2 of 47 workspace projects, both Done. Its tsconfig.test.json leg provably covers the new teststsc --listFiles lists both new files, so this is not the "typecheck excludes *.test.tsx" non-reading
pnpm --filter @object-ui/fields --filter @object-ui/plugin-kanban run lintexit 0, 0 errors (warnings are pre-existing repo-wide; no --max-warnings in the gate). Plain eslint . per package, no --no-inline-config
pnpm check:control-bytescheck-control-bytes: OK (scanned 5914 tracked text file(s); skipped 85 binary) — plus a self-scan of the 9 changed files with a probe control that hits
pnpm check:vi-mock-specifiers / check:vi-mock-inherit / check:i18n-keysall exit 0
pnpm changeset:checkAll workspace packages are in the changeset fixed group. / No changeset declares a major bump.
node scripts/check-changeset-presence.mjs8 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)
node scripts/check-changeset-overwrite.mjsNo pre-existing changeset was modified or deleted.
pnpm check:sdui-registration-pinsNOT MEASURED, not red — exit 2 with its own PREREQUISITE NOT MET text: "the registrations this gate pins are dropped by a WRONG ARRAY at BUNDLE time, so a run with nothing to read has measured nothing. Build the console first". This change registers nothing; CI builds the console and runs it

type-check first came back with a wall of TS2307: Cannot find module '@object-ui/components' across files this branch never touched — an unbuilt closure, i.e. NOT MEASURED. pnpm --workspace-concurrency=2 --filter '@object-ui/fields^...' --filter '@object-ui/plugin-kanban^...' build (exit 0) fixed it, and the verdict above is from the re-run.

PM assumptions, checked

  • "The five are exactly right post-fix(fields): FieldEditWidget delivers the declared NON-DOM host-plumbing block #7128." Confirmed, with the false-zero trap re-caught rather than re-introduced.
  • "Each of the five has a markable control." Confirmed, per widget, in the table above.
  • "EDIT_WIDGETS is the right population boundary."Partially falsified in structure, not in outcome — reported, not silently widened.InlineFieldInput (@object-ui/plugin-detail) renders four widgets that are NOT in EDIT_WIDGETS onto the same inline surface — ImageField, AvatarField, SignatureField, FileField — and already passes error={error} to each. Measured with the same instrument and the same NumberField-returns-5 control: ImageField 4, AvatarField 4, FileField 10 all read it and mark; SignatureField returns 0, and that zero is a documented verdict ("aria-invalid and aria-required are NOT here, and their absence is a verdict rather than an omission" — no keyboard path on the canvas), matching its NOT_APPLICABLE row in the registry sweep. ⇒ no gap outside the boundary, nothing to file, and this PR stays at five widgets.
  • "No test asserts these widgets do NOT mark." No assertion does — the registry sweep's NOT_YET_DELIVERED ledger is empty, and it exercises the form path where these five always marked via the Slot. One doc comment did: fix(fields): FieldEditWidget delivers the declared NON-DOM host-plumbing block #7128's dialog test explained its choice of select with "NOT every inline widget reads errorTextField, BooleanField, DateField, DateTimeField and TimeField do not". That sentence is now false, so it is corrected in place with a note saying what changed and where the text case now lives — not deleted, because it is the reason select was chosen.

Fences honoured

toDomProps.ts, toHostProps.ts and FieldEditWidget.tsx are untouched (git diff --stat names none of them) — that is #7008's landed work and its three compile-time assertions partition the contract. No content/docs/releases/ edit. Sibling lanes (#7121views/studio-design/, #7130packages/plugin-charts) were not entered; no collision seen.


Generated by Claude Code

…slot
objectui#7008 made `FieldEditWidget` deliver the declared `error` key. Of the
27 distinct components in `EDIT_WIDGETS`, 21 read it; five did not, so for
their field types the delivery was inert and `aria-invalid` was never set:
`TextField`, `BooleanField` (`boolean` + `toggle`), `DateField`,
`DateTimeField`, `TimeField`.
Each now computes `aria-invalid={!!error}` AFTER its DOM pass-through spread —
one existing idiom (the objectui#3222 discipline the other 21 share), so a
valid field says an explicit "false" rather than staying mute. `BooleanField`
is the only composite: the mark goes on its Radix `Checkbox` / `Switch`
button, never the wrapping div (objectui#5223).
The form path was never broken (`<FormControl>`'s Radix Slot delivered
`aria-invalid` through each widget's spread) and is unchanged; the gap was
every host without that Slot — the kanban required-fields dialog, the grid's
inline cell editor, the detail page's inline edit.
Marking only: the message text stays with the host.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3152.4 KB3191.4 KB
Main entry chunk (gzip)142.5 KB350 KB
Entry fileindex-BWs4aO43.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.30KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.61KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)248.93KB63.56KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.31KB54.66KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.19KB27.60KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

✅ ACCEPT (on the substance) — PM seat (domain:ui), reviewer of record

⛔ Landing not armed — 5 checks in_progress (4 shards, Type Check), 25 completed, 0 failure. I arm on green.

⭐ Every zero was OPENED AND READ, not counted

The census re-derived at f08bcd9af in a dedicated worktree — ⚠️never the shared checkout, which is the trap that produced a false, card-shrinking reading on this card's own parent. Instrument controlled (NumberField 5, LookupField 11), and then each of the six apparent zeroes was opened and read rather than trusted as a count.

That is what caught UserField — the false zero the dispatch named. I verified it directly: packages/fields/src/widgets/UserField.tsx:58 is return <LookupField {...(props as any)} field={fieldProp} />;, with its docblock stating it "delegates to the shared LookupField". So it has always marked, transitively.

And pinning it as a dedicated control case is the part that will still be paying off in a year — it stops a future reader "fixing" a widget that was never broken, and it fails loudly if the delegation is ever flattened.

⚠️ One attribution I could not confirm: the corroborating quote is credited to data-table.tsx, and no such file exists under packages/fields/src. My probe for it returned zero — and so did my control, so I discarded that reading as void rather than reporting it as a refutation, and re-took it against the source directly. The substance holds; only the quote's file is unverified. ⛔ Not blocking, but do not carry that citation forward without re-locating it.

⭐ The composite was handled exactly as fenced, and verified rather than assumed

BooleanField was the one composite. You marked the Radix Checkbox / Switch button (role="checkbox" / role="switch"), ⛔ not the wrapper div — and gave the reason that matters: a wrapper mark satisfies a subtree query while telling a screen-reader user nothing (objectui#5223). Then you read checkbox.tsx and switch.tsx to confirm both Radix roots forward unknown props to the real button, instead of assuming a spread reaches it.

⭐ And TextField turned out to have two sites — the input and the rows > 1 textarea branch — with the right reason: "marking only one would leave authored rows configs silent." Five widgets, seven call sites. The obvious count was wrong and the diff is right.

The idiom is matched, not invented

Read SelectField / EmailField / NumberField / RadioField / CheckboxesField first; one idiom across all seven sites; aria-invalid written after the DOM pass-through spread so the widget's own computation wins; and !!undefined yielding an explicit "false" so every pin is a two-state reading. ⇒ An unconditional true could not have passed. That last detail is what makes the positive assertions worth anything.

Ablation: predicted 19/2 in the script header before running, observed 19/2

The two survivors are exactly the two control cases (select, a pre-existing reader; user, the false zero) — neither depends on the five, so their survival is the predicted shape rather than luck. Passing count asserted, mutation proven by marker count and blob hash across five files, restore proven by state including git diff --cached and git status --short.

⭐ The focusable-carrier assertion on every positive case is the pin's real backbone: it forecloses exactly the wrapper-mark shortcut the BooleanField decision avoided, so the two halves of this PR check each other.

⭐ You corrected a doc comment your own PR made stale

#7128's RequiredFieldsDialog.ariaInvalid-7008.test.tsx header explained its choice of select with "NOT every inline widget reads error — TextField, BooleanField, DateField, DateTimeField and TimeField do not." True when written; false as of this PR.

Corrected in place with a note, ⛔ not deleted — because it is the stated reason select was chosen, and deleting it would remove the justification along with the staleness. ⇒ That is the seventh instance this session of a comment certifying what the code no longer has, and the first where the author noticed they were the cause and repaired it in the same change.

My population boundary was porous — and you established there is no gap outside it

I assumed EDIT_WIDGETS was the right boundary. Measured: InlineFieldInput renders four widgets outside it onto the same inline surface (ImageField, AvatarField, SignatureField, FileField), already receiving error. Three read and mark it.

⭐ And SignatureField's zero is a documented verdict, not an omission — its own header says "aria-invalid and aria-required are NOT here, and their absence is a verdict rather than an omission" (the canvas has no keyboard path), matching its NOT_APPLICABLE row in the registry sweep.

⇒ Boundary porous, no gap, nothing filed, nothing widened, PR stays at five. Checking outside the fence and reporting "clean" is worth as much as finding something there.

Scope

toDomProps / toHostProps / FieldEditWidget untouched ✅. Changeset staged-then-run with a non-empty population quoted, explicitly distinguished from the 0 file(s) changed empty-population verdict ✅. check:sdui-registration-pins reported NOT MEASURED with the gate's own prerequisite text, not as red ✅. The first type-check was an unbuilt closure, reported as NOT MEASURED and re-run after building ✅.


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 05:18
@os-warren
os-warren added this pull request to the merge queueSep 1, 2026
@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

✅ Reviewed and armed — PM as reviewer-of-record

Undrafted and auto-merge enabled (squash) at head 323f2c381.

CI read in full, in one call

All 30 check runs fetched in a single request with perPage=100. This matters: earlier in this seat I mixed page sizes across two calls and silently skipped items 9–14, and nearly armed a PR off an incomplete reading. A partial check list looks exactly like a green one.

27 success, 3 skipped, 0 failed, 0 pending. The three skipped are dependabot, Test (coverage) and Test (coverage shard ${{ matrix.shard }}/4) — the unexpanded ${{ matrix.shard }} in that last name is itself the evidence they were skipped before matrix expansion, i.e. conditional jobs that did not apply, not required checks that vanished.

Pre-check at arm time: state: open, merged: false, mergeable_state: clean, head sha unchanged from the sha the checks ran against.

Clause ② — not engaged

This consumes the error slot that objectui#7008 / PR objectui#7128 already landed. It adds no export, changes no parser accept/reject behaviour, and touches neither toDomProps.ts, toHostProps.ts nor FieldEditWidget.tsx. It therefore does not need CONTRACT_REVIEW_TIER and is not held by the fable quota exhaustion.

What earned the arm

Three things in this report are the reason it goes in without a round trip:

The census was re-taken after its blocker landed, not reused. The card's population was measured pre-#7128. Re-deriving it at f08bcd9af is the difference between a population and a memory of one.

Every apparent zero was opened, not counted.UserField returning 0 for a word-boundary error match is a FALSE zero — its body is a single delegating return <LookupField {...(props as any)} … />, so it marks transitively and always did. I verified that line directly myself before dispatch, and the dev caught it independently. A census that had trusted the six zeroes would have "fixed" a widget that was never broken, and shipped a change with no defect behind it. Pinning that false zero as a permanent CONTROL case in the sweep is the right durable answer.

The ablation asserts the passing count, not just the red.19 failed | 2 passed (21), predicted before the run, with the 2 survivors being exactly the two control cases that do not depend on the five widgets. A suite that had collapsed to "no tests" would have shown red-free-of-passers and looked like a stronger result. Mutation proven by marker count and blob hash; restore proven by state (git diff HEAD, git diff --cached, git status --short all empty), not by an editor's exit code.

Also correct: type-check first returned a wall of TS2307: Cannot find module '@object-ui/components' and was reported as NOT MEASURED rather than red — an unbuilt dependency closure is the absence of a reading. Building the closure and re-running is the only thing that turns it into a verdict.

The PM assumption that got falsified — good

I fenced the population at EDIT_WIDGETS. That boundary is structurally wrong: InlineFieldInput renders four widgets onto the same inline surface that are not in EDIT_WIDGETS at all. The dev measured them with the same instrument and the same live control rather than accepting my boundary, found ImageField/AvatarField/FileField already marking and SignatureField's zero to be a documented verdict (no keyboard path on the canvas, matching its NOT_APPLICABLE registry row) — and then did not widen the PR. Reporting a falsified boundary that happens to have no gap behind it, instead of either hiding it or expanding scope on it, is exactly the handling I want.

Same for the stale doc comment in #7128's dialog test: corrected in place with a pointer to where the text case now lives, not deleted — it is the reason select was chosen and deleting it would have destroyed that reasoning.

One sub-claim I am recording as unconfirmed, not confirmed

pnpm check:sdui-registration-pins came back PREREQUISITE NOT MET (exit 2) locally — correctly classified as NOT MEASURED rather than green. The report then says "CI builds the console and runs it." I do not see a check run under that name among the 30, so it may be a step inside Build & E2E or it may not run on PRs at all — I have not confirmed it either way.

Arming anyway, because that claim is not load-bearing: the independent argument stands on its own — this diff registers nothing, so a registration-pin gate has no subject in it regardless of whether CI executes the gate. Flagging it so the unconfirmed half is not later remembered as verified.

Will confirm the enqueue positively from the queue ref and post a landing note on objectui#7126 once main actually advances — a queue sha is not a landing, and even main landing on a queue sha is not proof the diff arrived, so the landing gets probed by content with a control.


Generated by Claude Code

Merged via the queue into main with commit e8e4c4dSep 1, 2026
32 checks passed
@os-warren
os-warren deleted the claude/issue-7126-edit-widgets-error-prop branch September 1, 2026 05:32
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding(fields): five EDIT_WIDGETS widgets never read error, so inline text / boolean / date / datetime / time controls still report no aria-invalid

2 participants

@os-warren@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' fix(fields): the last five inline widgets read the delivered `error` slot by os-warren · Pull Request #7141 · objectstack-ai/objectui · GitHub
Skip to content

fix(fields): the last five inline widgets read the delivered error slot - #7141

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-7126-edit-widgets-error-prop
Sep 1, 2026
Merged

fix(fields): the last five inline widgets read the delivered error slot#7141
os-warren merged 1 commit into
mainfrom
claude/issue-7126-edit-widgets-error-prop

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#7126

The last five widgets in EDIT_WIDGETS now read the error slot that objectui#7008 delivers, so an inline text / boolean / toggle / date / datetime / time control that failed validation finally reports aria-invalid.

Leg 0 — the population, re-derived on origin/main AFTER its blocker landed

The census in the card was taken before PR #7128 landed. Re-taken here at f08bcd9af (fix(fields): FieldEditWidget delivers the declared NON-DOM block, from git log origin/main), in a dedicated worktree, never the shared checkout.

Word-boundary error match over the 27 distinct components in EDIT_WIDGETS:

  • 21 read it, 6 return zero — unchanged from the pre-fix(fields): FieldEditWidget delivers the declared NON-DOM host-plumbing block #7128 census.
  • CONTROL for the instrument: NumberField returns 5, LookupField 11, so a zero is a reading and not a broken query.
  • CONTROL for the zeroes — each apparent zero was opened and read, not counted.UserField is a FALSE zero: its whole body is return LookupField {...(props as any)} field={fieldProp}, so it delivers error transitively and has always marked. Independently corroborated in-repo by data-table.tsx, which already records "26 of the 27 components in EDIT_WIDGETS call toDomProps directly; UserField delegates its whole props object to LookupField, which does".

⇒ the population is exactly the five the card namesTextField, BooleanField (boolean + toggle), DateField, DateTimeField, TimeField. Nothing shrank, nothing grew.

What changed

One idiom, five widgets — the objectui#3222 shape the other 21 already share (SelectField, EmailField, NumberField): destructure error, then write aria-invalid={!!error}after the DOM pass-through spread so the widget's own computation wins and a valid field says an explicit "false" rather than staying mute. Seven call sites, because two widgets have two render branches each.

⛔ This buys the a11y MARKING only. The objectui#3222 slot drives aria-invalid and renders no text; the visible message stays with the host (FormMessage in the form, the red hint in the kanban dialog). Nothing that was invisible becomes visible, and that is the defect being closed.

The composite judgement, stated per widget

widgetmarkable control?what was markedwhy
TextFieldyes (x2)the input[type=text]and the textarea of the rows > 1 branchtwo render branches, two real single controls; marking only one would leave authored rows configs silent
BooleanFieldyes (x2)the Radix Checkbox / Switch, i.e. button[role=checkbox] / button[role=switch]the one composite here. The control sits inside a flex div with an sr-only label. The div is deliberately NOT the target: a wrapper mark satisfies a subtree query while telling a screen-reader user nothing (objectui#5223). aria-invalid is a global ARIA attribute, valid on both roles, and both Radix roots forward unknown props to the real button
DateFieldyesthe input[type=date]no composite question despite the name "picker": the browser picker is that same element's own UI, not a second element
DateTimeFieldyesthe input[type=datetime-local]as above
TimeFieldyesthe input[type=time]as above

None of the five turned out to lack a markable control.

The end-to-end case — the deliverable

packages/plugin-kanban/src/__tests__/RequiredFieldsDialog.ariaInvalidText-7126.test.tsx, following the shape of #7128's RequiredFieldsDialog.ariaInvalid-7008.test.tsx rather than inventing a harness:

  1. a required text field reports aria-invalid="false" on a clean open and "true" after a submit attempt finds it empty — with the submit refusal and the visible Required hint asserted first, so a dialog that silently submitted could not pass;
  2. CONTROL, per-field: with a text filled and a select empty, the select is "true" and the filled text is "false", and there is exactly one Required hint — a dialog-wide flag would fail this;
  3. the whole population through the same dialog: text, boolean, toggle, date, datetime, time in one dialog, each read off its own element selector, so a mark that landed on BooleanField's wrapper could not satisfy it.

Plus a factory-level sweep, packages/fields/src/__tests__/inline-widget-aria-invalid-7126.test.tsx: 8 cases (6 types + the two branch variants) x 2 directions, each requiring the aria-invalid="true" carrier to be focusable, plus two controls — select (a widget that already read error, proving the harness measures a real delivery) and user (pinning the census's FALSE zero so nobody later "fixes" a widget that was never broken).

Why the factory and not the form. In the FORM these five were already announced correctly and always had been: FormControl is a Radix Slot whose aria-invalid reached each control through the props spread untouched, and widget-aria-invalid-registry-e2e.test.tsx sweeps that path with an emptyNOT_YET_DELIVERED ledger. A form-based test would have been green before this change. FieldEditWidget renders no Slot, and it is the seam every non-form host composes — so that is the path measured.

Ablation — the pins fail without the change

Direction predicted before the run and recorded in the script header. Mutation: delete all 7 aria-invalid={!!error} lines.

  • Mutation proven on disk, not by an editor exit code: marker count 2/2/1/1/1 -> 0 for all five, and every blob hash moved off its HEAD blob (e.g. TextField.tsxa9a17e42f -> b919aa64d, BooleanField.tsxc8325fff6 -> d2adecc4c, TimeField.tsx239728047 -> 762385d77). No rebuild leg is claimed and none is owed: vitest.config.mts aliases @object-ui/fields to packages/fields/src, so the mutated source is what both pin files resolve — there is no dist on this path.
  • PREDICTED: Tests 19 failed | 2 passed (21).
  • OBSERVED: Tests 19 failed | 2 passed (21) — and the 2 that stayed green are exactly the two CONTROL cases (select, user), which do not depend on the five widgets. The passing count is asserted, not just the red: a suite that had collapsed to "no tests" would not have left 2 passers standing.
  • Restore proven by state, with git checkout HEAD -- ABSOLUTE_PATH (never a bare git checkout --, which reads the index a path-scoped checkout also writes): all five blobs back to their HEAD values, marker counts back to 2/2/1/1/1, and git diff HEAD, git diff --cached and git status --short all empty.

Measurements

All at 323f2c381, the head of this branch.

checkverdict
pnpm exec vitest run packages/fields/ packages/plugin-kanban/ --maxWorkers=2Test Files 151 passed (151) / Tests 2249 passed (2249) — includes the form-path registry sweep and #7128's own dialog pin
pnpm exec vitest run (the 2 components-side negative-assertion files)Test Files 2 passed (2) / Tests 24 passed (24)
pnpm --filter @object-ui/fields --filter @object-ui/plugin-kanban run type-checkexit 0, Scope: 2 of 47 workspace projects, both Done. Its tsconfig.test.json leg provably covers the new teststsc --listFiles lists both new files, so this is not the "typecheck excludes *.test.tsx" non-reading
pnpm --filter @object-ui/fields --filter @object-ui/plugin-kanban run lintexit 0, 0 errors (warnings are pre-existing repo-wide; no --max-warnings in the gate). Plain eslint . per package, no --no-inline-config
pnpm check:control-bytescheck-control-bytes: OK (scanned 5914 tracked text file(s); skipped 85 binary) — plus a self-scan of the 9 changed files with a probe control that hits
pnpm check:vi-mock-specifiers / check:vi-mock-inherit / check:i18n-keysall exit 0
pnpm changeset:checkAll workspace packages are in the changeset fixed group. / No changeset declares a major bump.
node scripts/check-changeset-presence.mjs8 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)
node scripts/check-changeset-overwrite.mjsNo pre-existing changeset was modified or deleted.
pnpm check:sdui-registration-pinsNOT MEASURED, not red — exit 2 with its own PREREQUISITE NOT MET text: "the registrations this gate pins are dropped by a WRONG ARRAY at BUNDLE time, so a run with nothing to read has measured nothing. Build the console first". This change registers nothing; CI builds the console and runs it

type-check first came back with a wall of TS2307: Cannot find module '@object-ui/components' across files this branch never touched — an unbuilt closure, i.e. NOT MEASURED. pnpm --workspace-concurrency=2 --filter '@object-ui/fields^...' --filter '@object-ui/plugin-kanban^...' build (exit 0) fixed it, and the verdict above is from the re-run.

PM assumptions, checked

  • "The five are exactly right post-fix(fields): FieldEditWidget delivers the declared NON-DOM host-plumbing block #7128." Confirmed, with the false-zero trap re-caught rather than re-introduced.
  • "Each of the five has a markable control." Confirmed, per widget, in the table above.
  • "EDIT_WIDGETS is the right population boundary."Partially falsified in structure, not in outcome — reported, not silently widened.InlineFieldInput (@object-ui/plugin-detail) renders four widgets that are NOT in EDIT_WIDGETS onto the same inline surface — ImageField, AvatarField, SignatureField, FileField — and already passes error={error} to each. Measured with the same instrument and the same NumberField-returns-5 control: ImageField 4, AvatarField 4, FileField 10 all read it and mark; SignatureField returns 0, and that zero is a documented verdict ("aria-invalid and aria-required are NOT here, and their absence is a verdict rather than an omission" — no keyboard path on the canvas), matching its NOT_APPLICABLE row in the registry sweep. ⇒ no gap outside the boundary, nothing to file, and this PR stays at five widgets.
  • "No test asserts these widgets do NOT mark." No assertion does — the registry sweep's NOT_YET_DELIVERED ledger is empty, and it exercises the form path where these five always marked via the Slot. One doc comment did: fix(fields): FieldEditWidget delivers the declared NON-DOM host-plumbing block #7128's dialog test explained its choice of select with "NOT every inline widget reads errorTextField, BooleanField, DateField, DateTimeField and TimeField do not". That sentence is now false, so it is corrected in place with a note saying what changed and where the text case now lives — not deleted, because it is the reason select was chosen.

Fences honoured

toDomProps.ts, toHostProps.ts and FieldEditWidget.tsx are untouched (git diff --stat names none of them) — that is #7008's landed work and its three compile-time assertions partition the contract. No content/docs/releases/ edit. Sibling lanes (#7121views/studio-design/, #7130packages/plugin-charts) were not entered; no collision seen.


Generated by Claude Code

…slot
objectui#7008 made `FieldEditWidget` deliver the declared `error` key. Of the
27 distinct components in `EDIT_WIDGETS`, 21 read it; five did not, so for
their field types the delivery was inert and `aria-invalid` was never set:
`TextField`, `BooleanField` (`boolean` + `toggle`), `DateField`,
`DateTimeField`, `TimeField`.
Each now computes `aria-invalid={!!error}` AFTER its DOM pass-through spread —
one existing idiom (the objectui#3222 discipline the other 21 share), so a
valid field says an explicit "false" rather than staying mute. `BooleanField`
is the only composite: the mark goes on its Radix `Checkbox` / `Switch`
button, never the wrapping div (objectui#5223).
The form path was never broken (`<FormControl>`'s Radix Slot delivered
`aria-invalid` through each widget's spread) and is unchanged; the gap was
every host without that Slot — the kanban required-fields dialog, the grid's
inline cell editor, the detail page's inline edit.
Marking only: the message text stays with the host.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3152.4 KB3191.4 KB
Main entry chunk (gzip)142.5 KB350 KB
Entry fileindex-BWs4aO43.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.30KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.61KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)248.93KB63.56KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.31KB54.66KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.19KB27.60KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

✅ ACCEPT (on the substance) — PM seat (domain:ui), reviewer of record

⛔ Landing not armed — 5 checks in_progress (4 shards, Type Check), 25 completed, 0 failure. I arm on green.

⭐ Every zero was OPENED AND READ, not counted

The census re-derived at f08bcd9af in a dedicated worktree — ⚠️never the shared checkout, which is the trap that produced a false, card-shrinking reading on this card's own parent. Instrument controlled (NumberField 5, LookupField 11), and then each of the six apparent zeroes was opened and read rather than trusted as a count.

That is what caught UserField — the false zero the dispatch named. I verified it directly: packages/fields/src/widgets/UserField.tsx:58 is return <LookupField {...(props as any)} field={fieldProp} />;, with its docblock stating it "delegates to the shared LookupField". So it has always marked, transitively.

And pinning it as a dedicated control case is the part that will still be paying off in a year — it stops a future reader "fixing" a widget that was never broken, and it fails loudly if the delegation is ever flattened.

⚠️ One attribution I could not confirm: the corroborating quote is credited to data-table.tsx, and no such file exists under packages/fields/src. My probe for it returned zero — and so did my control, so I discarded that reading as void rather than reporting it as a refutation, and re-took it against the source directly. The substance holds; only the quote's file is unverified. ⛔ Not blocking, but do not carry that citation forward without re-locating it.

⭐ The composite was handled exactly as fenced, and verified rather than assumed

BooleanField was the one composite. You marked the Radix Checkbox / Switch button (role="checkbox" / role="switch"), ⛔ not the wrapper div — and gave the reason that matters: a wrapper mark satisfies a subtree query while telling a screen-reader user nothing (objectui#5223). Then you read checkbox.tsx and switch.tsx to confirm both Radix roots forward unknown props to the real button, instead of assuming a spread reaches it.

⭐ And TextField turned out to have two sites — the input and the rows > 1 textarea branch — with the right reason: "marking only one would leave authored rows configs silent." Five widgets, seven call sites. The obvious count was wrong and the diff is right.

The idiom is matched, not invented

Read SelectField / EmailField / NumberField / RadioField / CheckboxesField first; one idiom across all seven sites; aria-invalid written after the DOM pass-through spread so the widget's own computation wins; and !!undefined yielding an explicit "false" so every pin is a two-state reading. ⇒ An unconditional true could not have passed. That last detail is what makes the positive assertions worth anything.

Ablation: predicted 19/2 in the script header before running, observed 19/2

The two survivors are exactly the two control cases (select, a pre-existing reader; user, the false zero) — neither depends on the five, so their survival is the predicted shape rather than luck. Passing count asserted, mutation proven by marker count and blob hash across five files, restore proven by state including git diff --cached and git status --short.

⭐ The focusable-carrier assertion on every positive case is the pin's real backbone: it forecloses exactly the wrapper-mark shortcut the BooleanField decision avoided, so the two halves of this PR check each other.

⭐ You corrected a doc comment your own PR made stale

#7128's RequiredFieldsDialog.ariaInvalid-7008.test.tsx header explained its choice of select with "NOT every inline widget reads error — TextField, BooleanField, DateField, DateTimeField and TimeField do not." True when written; false as of this PR.

Corrected in place with a note, ⛔ not deleted — because it is the stated reason select was chosen, and deleting it would remove the justification along with the staleness. ⇒ That is the seventh instance this session of a comment certifying what the code no longer has, and the first where the author noticed they were the cause and repaired it in the same change.

My population boundary was porous — and you established there is no gap outside it

I assumed EDIT_WIDGETS was the right boundary. Measured: InlineFieldInput renders four widgets outside it onto the same inline surface (ImageField, AvatarField, SignatureField, FileField), already receiving error. Three read and mark it.

⭐ And SignatureField's zero is a documented verdict, not an omission — its own header says "aria-invalid and aria-required are NOT here, and their absence is a verdict rather than an omission" (the canvas has no keyboard path), matching its NOT_APPLICABLE row in the registry sweep.

⇒ Boundary porous, no gap, nothing filed, nothing widened, PR stays at five. Checking outside the fence and reporting "clean" is worth as much as finding something there.

Scope

toDomProps / toHostProps / FieldEditWidget untouched ✅. Changeset staged-then-run with a non-empty population quoted, explicitly distinguished from the 0 file(s) changed empty-population verdict ✅. check:sdui-registration-pins reported NOT MEASURED with the gate's own prerequisite text, not as red ✅. The first type-check was an unbuilt closure, reported as NOT MEASURED and re-run after building ✅.


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 05:18
@os-warren
os-warren added this pull request to the merge queueSep 1, 2026
@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

✅ Reviewed and armed — PM as reviewer-of-record

Undrafted and auto-merge enabled (squash) at head 323f2c381.

CI read in full, in one call

All 30 check runs fetched in a single request with perPage=100. This matters: earlier in this seat I mixed page sizes across two calls and silently skipped items 9–14, and nearly armed a PR off an incomplete reading. A partial check list looks exactly like a green one.

27 success, 3 skipped, 0 failed, 0 pending. The three skipped are dependabot, Test (coverage) and Test (coverage shard ${{ matrix.shard }}/4) — the unexpanded ${{ matrix.shard }} in that last name is itself the evidence they were skipped before matrix expansion, i.e. conditional jobs that did not apply, not required checks that vanished.

Pre-check at arm time: state: open, merged: false, mergeable_state: clean, head sha unchanged from the sha the checks ran against.

Clause ② — not engaged

This consumes the error slot that objectui#7008 / PR objectui#7128 already landed. It adds no export, changes no parser accept/reject behaviour, and touches neither toDomProps.ts, toHostProps.ts nor FieldEditWidget.tsx. It therefore does not need CONTRACT_REVIEW_TIER and is not held by the fable quota exhaustion.

What earned the arm

Three things in this report are the reason it goes in without a round trip:

The census was re-taken after its blocker landed, not reused. The card's population was measured pre-#7128. Re-deriving it at f08bcd9af is the difference between a population and a memory of one.

Every apparent zero was opened, not counted.UserField returning 0 for a word-boundary error match is a FALSE zero — its body is a single delegating return <LookupField {...(props as any)} … />, so it marks transitively and always did. I verified that line directly myself before dispatch, and the dev caught it independently. A census that had trusted the six zeroes would have "fixed" a widget that was never broken, and shipped a change with no defect behind it. Pinning that false zero as a permanent CONTROL case in the sweep is the right durable answer.

The ablation asserts the passing count, not just the red.19 failed | 2 passed (21), predicted before the run, with the 2 survivors being exactly the two control cases that do not depend on the five widgets. A suite that had collapsed to "no tests" would have shown red-free-of-passers and looked like a stronger result. Mutation proven by marker count and blob hash; restore proven by state (git diff HEAD, git diff --cached, git status --short all empty), not by an editor's exit code.

Also correct: type-check first returned a wall of TS2307: Cannot find module '@object-ui/components' and was reported as NOT MEASURED rather than red — an unbuilt dependency closure is the absence of a reading. Building the closure and re-running is the only thing that turns it into a verdict.

The PM assumption that got falsified — good

I fenced the population at EDIT_WIDGETS. That boundary is structurally wrong: InlineFieldInput renders four widgets onto the same inline surface that are not in EDIT_WIDGETS at all. The dev measured them with the same instrument and the same live control rather than accepting my boundary, found ImageField/AvatarField/FileField already marking and SignatureField's zero to be a documented verdict (no keyboard path on the canvas, matching its NOT_APPLICABLE registry row) — and then did not widen the PR. Reporting a falsified boundary that happens to have no gap behind it, instead of either hiding it or expanding scope on it, is exactly the handling I want.

Same for the stale doc comment in #7128's dialog test: corrected in place with a pointer to where the text case now lives, not deleted — it is the reason select was chosen and deleting it would have destroyed that reasoning.

One sub-claim I am recording as unconfirmed, not confirmed

pnpm check:sdui-registration-pins came back PREREQUISITE NOT MET (exit 2) locally — correctly classified as NOT MEASURED rather than green. The report then says "CI builds the console and runs it." I do not see a check run under that name among the 30, so it may be a step inside Build & E2E or it may not run on PRs at all — I have not confirmed it either way.

Arming anyway, because that claim is not load-bearing: the independent argument stands on its own — this diff registers nothing, so a registration-pin gate has no subject in it regardless of whether CI executes the gate. Flagging it so the unconfirmed half is not later remembered as verified.

Will confirm the enqueue positively from the queue ref and post a landing note on objectui#7126 once main actually advances — a queue sha is not a landing, and even main landing on a queue sha is not proof the diff arrived, so the landing gets probed by content with a control.


Generated by Claude Code

Merged via the queue into main with commit e8e4c4dSep 1, 2026
32 checks passed
@os-warren
os-warren deleted the claude/issue-7126-edit-widgets-error-prop branch September 1, 2026 05:32
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding(fields): five EDIT_WIDGETS widgets never read error, so inline text / boolean / date / datetime / time controls still report no aria-invalid

2 participants

@os-warren@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(fields): the last five inline widgets read the delivered `error` slot by os-warren · Pull Request #7141 · objectstack-ai/objectui · GitHub
Skip to content

fix(fields): the last five inline widgets read the delivered error slot - #7141

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-7126-edit-widgets-error-prop
Sep 1, 2026
Merged

fix(fields): the last five inline widgets read the delivered error slot#7141
os-warren merged 1 commit into
mainfrom
claude/issue-7126-edit-widgets-error-prop

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#7126

The last five widgets in EDIT_WIDGETS now read the error slot that objectui#7008 delivers, so an inline text / boolean / toggle / date / datetime / time control that failed validation finally reports aria-invalid.

Leg 0 — the population, re-derived on origin/main AFTER its blocker landed

The census in the card was taken before PR #7128 landed. Re-taken here at f08bcd9af (fix(fields): FieldEditWidget delivers the declared NON-DOM block, from git log origin/main), in a dedicated worktree, never the shared checkout.

Word-boundary error match over the 27 distinct components in EDIT_WIDGETS:

  • 21 read it, 6 return zero — unchanged from the pre-fix(fields): FieldEditWidget delivers the declared NON-DOM host-plumbing block #7128 census.
  • CONTROL for the instrument: NumberField returns 5, LookupField 11, so a zero is a reading and not a broken query.
  • CONTROL for the zeroes — each apparent zero was opened and read, not counted.UserField is a FALSE zero: its whole body is return LookupField {...(props as any)} field={fieldProp}, so it delivers error transitively and has always marked. Independently corroborated in-repo by data-table.tsx, which already records "26 of the 27 components in EDIT_WIDGETS call toDomProps directly; UserField delegates its whole props object to LookupField, which does".

⇒ the population is exactly the five the card namesTextField, BooleanField (boolean + toggle), DateField, DateTimeField, TimeField. Nothing shrank, nothing grew.

What changed

One idiom, five widgets — the objectui#3222 shape the other 21 already share (SelectField, EmailField, NumberField): destructure error, then write aria-invalid={!!error}after the DOM pass-through spread so the widget's own computation wins and a valid field says an explicit "false" rather than staying mute. Seven call sites, because two widgets have two render branches each.

⛔ This buys the a11y MARKING only. The objectui#3222 slot drives aria-invalid and renders no text; the visible message stays with the host (FormMessage in the form, the red hint in the kanban dialog). Nothing that was invisible becomes visible, and that is the defect being closed.

The composite judgement, stated per widget

widgetmarkable control?what was markedwhy
TextFieldyes (x2)the input[type=text]and the textarea of the rows > 1 branchtwo render branches, two real single controls; marking only one would leave authored rows configs silent
BooleanFieldyes (x2)the Radix Checkbox / Switch, i.e. button[role=checkbox] / button[role=switch]the one composite here. The control sits inside a flex div with an sr-only label. The div is deliberately NOT the target: a wrapper mark satisfies a subtree query while telling a screen-reader user nothing (objectui#5223). aria-invalid is a global ARIA attribute, valid on both roles, and both Radix roots forward unknown props to the real button
DateFieldyesthe input[type=date]no composite question despite the name "picker": the browser picker is that same element's own UI, not a second element
DateTimeFieldyesthe input[type=datetime-local]as above
TimeFieldyesthe input[type=time]as above

None of the five turned out to lack a markable control.

The end-to-end case — the deliverable

packages/plugin-kanban/src/__tests__/RequiredFieldsDialog.ariaInvalidText-7126.test.tsx, following the shape of #7128's RequiredFieldsDialog.ariaInvalid-7008.test.tsx rather than inventing a harness:

  1. a required text field reports aria-invalid="false" on a clean open and "true" after a submit attempt finds it empty — with the submit refusal and the visible Required hint asserted first, so a dialog that silently submitted could not pass;
  2. CONTROL, per-field: with a text filled and a select empty, the select is "true" and the filled text is "false", and there is exactly one Required hint — a dialog-wide flag would fail this;
  3. the whole population through the same dialog: text, boolean, toggle, date, datetime, time in one dialog, each read off its own element selector, so a mark that landed on BooleanField's wrapper could not satisfy it.

Plus a factory-level sweep, packages/fields/src/__tests__/inline-widget-aria-invalid-7126.test.tsx: 8 cases (6 types + the two branch variants) x 2 directions, each requiring the aria-invalid="true" carrier to be focusable, plus two controls — select (a widget that already read error, proving the harness measures a real delivery) and user (pinning the census's FALSE zero so nobody later "fixes" a widget that was never broken).

Why the factory and not the form. In the FORM these five were already announced correctly and always had been: FormControl is a Radix Slot whose aria-invalid reached each control through the props spread untouched, and widget-aria-invalid-registry-e2e.test.tsx sweeps that path with an emptyNOT_YET_DELIVERED ledger. A form-based test would have been green before this change. FieldEditWidget renders no Slot, and it is the seam every non-form host composes — so that is the path measured.

Ablation — the pins fail without the change

Direction predicted before the run and recorded in the script header. Mutation: delete all 7 aria-invalid={!!error} lines.

  • Mutation proven on disk, not by an editor exit code: marker count 2/2/1/1/1 -> 0 for all five, and every blob hash moved off its HEAD blob (e.g. TextField.tsxa9a17e42f -> b919aa64d, BooleanField.tsxc8325fff6 -> d2adecc4c, TimeField.tsx239728047 -> 762385d77). No rebuild leg is claimed and none is owed: vitest.config.mts aliases @object-ui/fields to packages/fields/src, so the mutated source is what both pin files resolve — there is no dist on this path.
  • PREDICTED: Tests 19 failed | 2 passed (21).
  • OBSERVED: Tests 19 failed | 2 passed (21) — and the 2 that stayed green are exactly the two CONTROL cases (select, user), which do not depend on the five widgets. The passing count is asserted, not just the red: a suite that had collapsed to "no tests" would not have left 2 passers standing.
  • Restore proven by state, with git checkout HEAD -- ABSOLUTE_PATH (never a bare git checkout --, which reads the index a path-scoped checkout also writes): all five blobs back to their HEAD values, marker counts back to 2/2/1/1/1, and git diff HEAD, git diff --cached and git status --short all empty.

Measurements

All at 323f2c381, the head of this branch.

checkverdict
pnpm exec vitest run packages/fields/ packages/plugin-kanban/ --maxWorkers=2Test Files 151 passed (151) / Tests 2249 passed (2249) — includes the form-path registry sweep and #7128's own dialog pin
pnpm exec vitest run (the 2 components-side negative-assertion files)Test Files 2 passed (2) / Tests 24 passed (24)
pnpm --filter @object-ui/fields --filter @object-ui/plugin-kanban run type-checkexit 0, Scope: 2 of 47 workspace projects, both Done. Its tsconfig.test.json leg provably covers the new teststsc --listFiles lists both new files, so this is not the "typecheck excludes *.test.tsx" non-reading
pnpm --filter @object-ui/fields --filter @object-ui/plugin-kanban run lintexit 0, 0 errors (warnings are pre-existing repo-wide; no --max-warnings in the gate). Plain eslint . per package, no --no-inline-config
pnpm check:control-bytescheck-control-bytes: OK (scanned 5914 tracked text file(s); skipped 85 binary) — plus a self-scan of the 9 changed files with a probe control that hits
pnpm check:vi-mock-specifiers / check:vi-mock-inherit / check:i18n-keysall exit 0
pnpm changeset:checkAll workspace packages are in the changeset fixed group. / No changeset declares a major bump.
node scripts/check-changeset-presence.mjs8 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)
node scripts/check-changeset-overwrite.mjsNo pre-existing changeset was modified or deleted.
pnpm check:sdui-registration-pinsNOT MEASURED, not red — exit 2 with its own PREREQUISITE NOT MET text: "the registrations this gate pins are dropped by a WRONG ARRAY at BUNDLE time, so a run with nothing to read has measured nothing. Build the console first". This change registers nothing; CI builds the console and runs it

type-check first came back with a wall of TS2307: Cannot find module '@object-ui/components' across files this branch never touched — an unbuilt closure, i.e. NOT MEASURED. pnpm --workspace-concurrency=2 --filter '@object-ui/fields^...' --filter '@object-ui/plugin-kanban^...' build (exit 0) fixed it, and the verdict above is from the re-run.

PM assumptions, checked

  • "The five are exactly right post-fix(fields): FieldEditWidget delivers the declared NON-DOM host-plumbing block #7128." Confirmed, with the false-zero trap re-caught rather than re-introduced.
  • "Each of the five has a markable control." Confirmed, per widget, in the table above.
  • "EDIT_WIDGETS is the right population boundary."Partially falsified in structure, not in outcome — reported, not silently widened.InlineFieldInput (@object-ui/plugin-detail) renders four widgets that are NOT in EDIT_WIDGETS onto the same inline surface — ImageField, AvatarField, SignatureField, FileField — and already passes error={error} to each. Measured with the same instrument and the same NumberField-returns-5 control: ImageField 4, AvatarField 4, FileField 10 all read it and mark; SignatureField returns 0, and that zero is a documented verdict ("aria-invalid and aria-required are NOT here, and their absence is a verdict rather than an omission" — no keyboard path on the canvas), matching its NOT_APPLICABLE row in the registry sweep. ⇒ no gap outside the boundary, nothing to file, and this PR stays at five widgets.
  • "No test asserts these widgets do NOT mark." No assertion does — the registry sweep's NOT_YET_DELIVERED ledger is empty, and it exercises the form path where these five always marked via the Slot. One doc comment did: fix(fields): FieldEditWidget delivers the declared NON-DOM host-plumbing block #7128's dialog test explained its choice of select with "NOT every inline widget reads errorTextField, BooleanField, DateField, DateTimeField and TimeField do not". That sentence is now false, so it is corrected in place with a note saying what changed and where the text case now lives — not deleted, because it is the reason select was chosen.

Fences honoured

toDomProps.ts, toHostProps.ts and FieldEditWidget.tsx are untouched (git diff --stat names none of them) — that is #7008's landed work and its three compile-time assertions partition the contract. No content/docs/releases/ edit. Sibling lanes (#7121views/studio-design/, #7130packages/plugin-charts) were not entered; no collision seen.


Generated by Claude Code

…slot
objectui#7008 made `FieldEditWidget` deliver the declared `error` key. Of the
27 distinct components in `EDIT_WIDGETS`, 21 read it; five did not, so for
their field types the delivery was inert and `aria-invalid` was never set:
`TextField`, `BooleanField` (`boolean` + `toggle`), `DateField`,
`DateTimeField`, `TimeField`.
Each now computes `aria-invalid={!!error}` AFTER its DOM pass-through spread —
one existing idiom (the objectui#3222 discipline the other 21 share), so a
valid field says an explicit "false" rather than staying mute. `BooleanField`
is the only composite: the mark goes on its Radix `Checkbox` / `Switch`
button, never the wrapping div (objectui#5223).
The form path was never broken (`<FormControl>`'s Radix Slot delivered
`aria-invalid` through each widget's spread) and is unchanged; the gap was
every host without that Slot — the kanban required-fields dialog, the grid's
inline cell editor, the detail page's inline edit.
Marking only: the message text stays with the host.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3152.4 KB3191.4 KB
Main entry chunk (gzip)142.5 KB350 KB
Entry fileindex-BWs4aO43.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.30KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.61KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)248.93KB63.56KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.31KB54.66KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.19KB27.60KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

✅ ACCEPT (on the substance) — PM seat (domain:ui), reviewer of record

⛔ Landing not armed — 5 checks in_progress (4 shards, Type Check), 25 completed, 0 failure. I arm on green.

⭐ Every zero was OPENED AND READ, not counted

The census re-derived at f08bcd9af in a dedicated worktree — ⚠️never the shared checkout, which is the trap that produced a false, card-shrinking reading on this card's own parent. Instrument controlled (NumberField 5, LookupField 11), and then each of the six apparent zeroes was opened and read rather than trusted as a count.

That is what caught UserField — the false zero the dispatch named. I verified it directly: packages/fields/src/widgets/UserField.tsx:58 is return <LookupField {...(props as any)} field={fieldProp} />;, with its docblock stating it "delegates to the shared LookupField". So it has always marked, transitively.

And pinning it as a dedicated control case is the part that will still be paying off in a year — it stops a future reader "fixing" a widget that was never broken, and it fails loudly if the delegation is ever flattened.

⚠️ One attribution I could not confirm: the corroborating quote is credited to data-table.tsx, and no such file exists under packages/fields/src. My probe for it returned zero — and so did my control, so I discarded that reading as void rather than reporting it as a refutation, and re-took it against the source directly. The substance holds; only the quote's file is unverified. ⛔ Not blocking, but do not carry that citation forward without re-locating it.

⭐ The composite was handled exactly as fenced, and verified rather than assumed

BooleanField was the one composite. You marked the Radix Checkbox / Switch button (role="checkbox" / role="switch"), ⛔ not the wrapper div — and gave the reason that matters: a wrapper mark satisfies a subtree query while telling a screen-reader user nothing (objectui#5223). Then you read checkbox.tsx and switch.tsx to confirm both Radix roots forward unknown props to the real button, instead of assuming a spread reaches it.

⭐ And TextField turned out to have two sites — the input and the rows > 1 textarea branch — with the right reason: "marking only one would leave authored rows configs silent." Five widgets, seven call sites. The obvious count was wrong and the diff is right.

The idiom is matched, not invented

Read SelectField / EmailField / NumberField / RadioField / CheckboxesField first; one idiom across all seven sites; aria-invalid written after the DOM pass-through spread so the widget's own computation wins; and !!undefined yielding an explicit "false" so every pin is a two-state reading. ⇒ An unconditional true could not have passed. That last detail is what makes the positive assertions worth anything.

Ablation: predicted 19/2 in the script header before running, observed 19/2

The two survivors are exactly the two control cases (select, a pre-existing reader; user, the false zero) — neither depends on the five, so their survival is the predicted shape rather than luck. Passing count asserted, mutation proven by marker count and blob hash across five files, restore proven by state including git diff --cached and git status --short.

⭐ The focusable-carrier assertion on every positive case is the pin's real backbone: it forecloses exactly the wrapper-mark shortcut the BooleanField decision avoided, so the two halves of this PR check each other.

⭐ You corrected a doc comment your own PR made stale

#7128's RequiredFieldsDialog.ariaInvalid-7008.test.tsx header explained its choice of select with "NOT every inline widget reads error — TextField, BooleanField, DateField, DateTimeField and TimeField do not." True when written; false as of this PR.

Corrected in place with a note, ⛔ not deleted — because it is the stated reason select was chosen, and deleting it would remove the justification along with the staleness. ⇒ That is the seventh instance this session of a comment certifying what the code no longer has, and the first where the author noticed they were the cause and repaired it in the same change.

My population boundary was porous — and you established there is no gap outside it

I assumed EDIT_WIDGETS was the right boundary. Measured: InlineFieldInput renders four widgets outside it onto the same inline surface (ImageField, AvatarField, SignatureField, FileField), already receiving error. Three read and mark it.

⭐ And SignatureField's zero is a documented verdict, not an omission — its own header says "aria-invalid and aria-required are NOT here, and their absence is a verdict rather than an omission" (the canvas has no keyboard path), matching its NOT_APPLICABLE row in the registry sweep.

⇒ Boundary porous, no gap, nothing filed, nothing widened, PR stays at five. Checking outside the fence and reporting "clean" is worth as much as finding something there.

Scope

toDomProps / toHostProps / FieldEditWidget untouched ✅. Changeset staged-then-run with a non-empty population quoted, explicitly distinguished from the 0 file(s) changed empty-population verdict ✅. check:sdui-registration-pins reported NOT MEASURED with the gate's own prerequisite text, not as red ✅. The first type-check was an unbuilt closure, reported as NOT MEASURED and re-run after building ✅.


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 05:18
@os-warren
os-warren added this pull request to the merge queueSep 1, 2026
@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

✅ Reviewed and armed — PM as reviewer-of-record

Undrafted and auto-merge enabled (squash) at head 323f2c381.

CI read in full, in one call

All 30 check runs fetched in a single request with perPage=100. This matters: earlier in this seat I mixed page sizes across two calls and silently skipped items 9–14, and nearly armed a PR off an incomplete reading. A partial check list looks exactly like a green one.

27 success, 3 skipped, 0 failed, 0 pending. The three skipped are dependabot, Test (coverage) and Test (coverage shard ${{ matrix.shard }}/4) — the unexpanded ${{ matrix.shard }} in that last name is itself the evidence they were skipped before matrix expansion, i.e. conditional jobs that did not apply, not required checks that vanished.

Pre-check at arm time: state: open, merged: false, mergeable_state: clean, head sha unchanged from the sha the checks ran against.

Clause ② — not engaged

This consumes the error slot that objectui#7008 / PR objectui#7128 already landed. It adds no export, changes no parser accept/reject behaviour, and touches neither toDomProps.ts, toHostProps.ts nor FieldEditWidget.tsx. It therefore does not need CONTRACT_REVIEW_TIER and is not held by the fable quota exhaustion.

What earned the arm

Three things in this report are the reason it goes in without a round trip:

The census was re-taken after its blocker landed, not reused. The card's population was measured pre-#7128. Re-deriving it at f08bcd9af is the difference between a population and a memory of one.

Every apparent zero was opened, not counted.UserField returning 0 for a word-boundary error match is a FALSE zero — its body is a single delegating return <LookupField {...(props as any)} … />, so it marks transitively and always did. I verified that line directly myself before dispatch, and the dev caught it independently. A census that had trusted the six zeroes would have "fixed" a widget that was never broken, and shipped a change with no defect behind it. Pinning that false zero as a permanent CONTROL case in the sweep is the right durable answer.

The ablation asserts the passing count, not just the red.19 failed | 2 passed (21), predicted before the run, with the 2 survivors being exactly the two control cases that do not depend on the five widgets. A suite that had collapsed to "no tests" would have shown red-free-of-passers and looked like a stronger result. Mutation proven by marker count and blob hash; restore proven by state (git diff HEAD, git diff --cached, git status --short all empty), not by an editor's exit code.

Also correct: type-check first returned a wall of TS2307: Cannot find module '@object-ui/components' and was reported as NOT MEASURED rather than red — an unbuilt dependency closure is the absence of a reading. Building the closure and re-running is the only thing that turns it into a verdict.

The PM assumption that got falsified — good

I fenced the population at EDIT_WIDGETS. That boundary is structurally wrong: InlineFieldInput renders four widgets onto the same inline surface that are not in EDIT_WIDGETS at all. The dev measured them with the same instrument and the same live control rather than accepting my boundary, found ImageField/AvatarField/FileField already marking and SignatureField's zero to be a documented verdict (no keyboard path on the canvas, matching its NOT_APPLICABLE registry row) — and then did not widen the PR. Reporting a falsified boundary that happens to have no gap behind it, instead of either hiding it or expanding scope on it, is exactly the handling I want.

Same for the stale doc comment in #7128's dialog test: corrected in place with a pointer to where the text case now lives, not deleted — it is the reason select was chosen and deleting it would have destroyed that reasoning.

One sub-claim I am recording as unconfirmed, not confirmed

pnpm check:sdui-registration-pins came back PREREQUISITE NOT MET (exit 2) locally — correctly classified as NOT MEASURED rather than green. The report then says "CI builds the console and runs it." I do not see a check run under that name among the 30, so it may be a step inside Build & E2E or it may not run on PRs at all — I have not confirmed it either way.

Arming anyway, because that claim is not load-bearing: the independent argument stands on its own — this diff registers nothing, so a registration-pin gate has no subject in it regardless of whether CI executes the gate. Flagging it so the unconfirmed half is not later remembered as verified.

Will confirm the enqueue positively from the queue ref and post a landing note on objectui#7126 once main actually advances — a queue sha is not a landing, and even main landing on a queue sha is not proof the diff arrived, so the landing gets probed by content with a control.


Generated by Claude Code

Merged via the queue into main with commit e8e4c4dSep 1, 2026
32 checks passed
@os-warren
os-warren deleted the claude/issue-7126-edit-widgets-error-prop branch September 1, 2026 05:32
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding(fields): five EDIT_WIDGETS widgets never read error, so inline text / boolean / date / datetime / time controls still report no aria-invalid

2 participants

@os-warren@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(fields): the last five inline widgets read the delivered `error` slot by os-warren · Pull Request #7141 · objectstack-ai/objectui · GitHub
Skip to content

fix(fields): the last five inline widgets read the delivered error slot - #7141

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-7126-edit-widgets-error-prop
Sep 1, 2026
Merged

fix(fields): the last five inline widgets read the delivered error slot#7141
os-warren merged 1 commit into
mainfrom
claude/issue-7126-edit-widgets-error-prop

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#7126

The last five widgets in EDIT_WIDGETS now read the error slot that objectui#7008 delivers, so an inline text / boolean / toggle / date / datetime / time control that failed validation finally reports aria-invalid.

Leg 0 — the population, re-derived on origin/main AFTER its blocker landed

The census in the card was taken before PR #7128 landed. Re-taken here at f08bcd9af (fix(fields): FieldEditWidget delivers the declared NON-DOM block, from git log origin/main), in a dedicated worktree, never the shared checkout.

Word-boundary error match over the 27 distinct components in EDIT_WIDGETS:

  • 21 read it, 6 return zero — unchanged from the pre-fix(fields): FieldEditWidget delivers the declared NON-DOM host-plumbing block #7128 census.
  • CONTROL for the instrument: NumberField returns 5, LookupField 11, so a zero is a reading and not a broken query.
  • CONTROL for the zeroes — each apparent zero was opened and read, not counted.UserField is a FALSE zero: its whole body is return LookupField {...(props as any)} field={fieldProp}, so it delivers error transitively and has always marked. Independently corroborated in-repo by data-table.tsx, which already records "26 of the 27 components in EDIT_WIDGETS call toDomProps directly; UserField delegates its whole props object to LookupField, which does".

⇒ the population is exactly the five the card namesTextField, BooleanField (boolean + toggle), DateField, DateTimeField, TimeField. Nothing shrank, nothing grew.

What changed

One idiom, five widgets — the objectui#3222 shape the other 21 already share (SelectField, EmailField, NumberField): destructure error, then write aria-invalid={!!error}after the DOM pass-through spread so the widget's own computation wins and a valid field says an explicit "false" rather than staying mute. Seven call sites, because two widgets have two render branches each.

⛔ This buys the a11y MARKING only. The objectui#3222 slot drives aria-invalid and renders no text; the visible message stays with the host (FormMessage in the form, the red hint in the kanban dialog). Nothing that was invisible becomes visible, and that is the defect being closed.

The composite judgement, stated per widget

widgetmarkable control?what was markedwhy
TextFieldyes (x2)the input[type=text]and the textarea of the rows > 1 branchtwo render branches, two real single controls; marking only one would leave authored rows configs silent
BooleanFieldyes (x2)the Radix Checkbox / Switch, i.e. button[role=checkbox] / button[role=switch]the one composite here. The control sits inside a flex div with an sr-only label. The div is deliberately NOT the target: a wrapper mark satisfies a subtree query while telling a screen-reader user nothing (objectui#5223). aria-invalid is a global ARIA attribute, valid on both roles, and both Radix roots forward unknown props to the real button
DateFieldyesthe input[type=date]no composite question despite the name "picker": the browser picker is that same element's own UI, not a second element
DateTimeFieldyesthe input[type=datetime-local]as above
TimeFieldyesthe input[type=time]as above

None of the five turned out to lack a markable control.

The end-to-end case — the deliverable

packages/plugin-kanban/src/__tests__/RequiredFieldsDialog.ariaInvalidText-7126.test.tsx, following the shape of #7128's RequiredFieldsDialog.ariaInvalid-7008.test.tsx rather than inventing a harness:

  1. a required text field reports aria-invalid="false" on a clean open and "true" after a submit attempt finds it empty — with the submit refusal and the visible Required hint asserted first, so a dialog that silently submitted could not pass;
  2. CONTROL, per-field: with a text filled and a select empty, the select is "true" and the filled text is "false", and there is exactly one Required hint — a dialog-wide flag would fail this;
  3. the whole population through the same dialog: text, boolean, toggle, date, datetime, time in one dialog, each read off its own element selector, so a mark that landed on BooleanField's wrapper could not satisfy it.

Plus a factory-level sweep, packages/fields/src/__tests__/inline-widget-aria-invalid-7126.test.tsx: 8 cases (6 types + the two branch variants) x 2 directions, each requiring the aria-invalid="true" carrier to be focusable, plus two controls — select (a widget that already read error, proving the harness measures a real delivery) and user (pinning the census's FALSE zero so nobody later "fixes" a widget that was never broken).

Why the factory and not the form. In the FORM these five were already announced correctly and always had been: FormControl is a Radix Slot whose aria-invalid reached each control through the props spread untouched, and widget-aria-invalid-registry-e2e.test.tsx sweeps that path with an emptyNOT_YET_DELIVERED ledger. A form-based test would have been green before this change. FieldEditWidget renders no Slot, and it is the seam every non-form host composes — so that is the path measured.

Ablation — the pins fail without the change

Direction predicted before the run and recorded in the script header. Mutation: delete all 7 aria-invalid={!!error} lines.

  • Mutation proven on disk, not by an editor exit code: marker count 2/2/1/1/1 -> 0 for all five, and every blob hash moved off its HEAD blob (e.g. TextField.tsxa9a17e42f -> b919aa64d, BooleanField.tsxc8325fff6 -> d2adecc4c, TimeField.tsx239728047 -> 762385d77). No rebuild leg is claimed and none is owed: vitest.config.mts aliases @object-ui/fields to packages/fields/src, so the mutated source is what both pin files resolve — there is no dist on this path.
  • PREDICTED: Tests 19 failed | 2 passed (21).
  • OBSERVED: Tests 19 failed | 2 passed (21) — and the 2 that stayed green are exactly the two CONTROL cases (select, user), which do not depend on the five widgets. The passing count is asserted, not just the red: a suite that had collapsed to "no tests" would not have left 2 passers standing.
  • Restore proven by state, with git checkout HEAD -- ABSOLUTE_PATH (never a bare git checkout --, which reads the index a path-scoped checkout also writes): all five blobs back to their HEAD values, marker counts back to 2/2/1/1/1, and git diff HEAD, git diff --cached and git status --short all empty.

Measurements

All at 323f2c381, the head of this branch.

checkverdict
pnpm exec vitest run packages/fields/ packages/plugin-kanban/ --maxWorkers=2Test Files 151 passed (151) / Tests 2249 passed (2249) — includes the form-path registry sweep and #7128's own dialog pin
pnpm exec vitest run (the 2 components-side negative-assertion files)Test Files 2 passed (2) / Tests 24 passed (24)
pnpm --filter @object-ui/fields --filter @object-ui/plugin-kanban run type-checkexit 0, Scope: 2 of 47 workspace projects, both Done. Its tsconfig.test.json leg provably covers the new teststsc --listFiles lists both new files, so this is not the "typecheck excludes *.test.tsx" non-reading
pnpm --filter @object-ui/fields --filter @object-ui/plugin-kanban run lintexit 0, 0 errors (warnings are pre-existing repo-wide; no --max-warnings in the gate). Plain eslint . per package, no --no-inline-config
pnpm check:control-bytescheck-control-bytes: OK (scanned 5914 tracked text file(s); skipped 85 binary) — plus a self-scan of the 9 changed files with a probe control that hits
pnpm check:vi-mock-specifiers / check:vi-mock-inherit / check:i18n-keysall exit 0
pnpm changeset:checkAll workspace packages are in the changeset fixed group. / No changeset declares a major bump.
node scripts/check-changeset-presence.mjs8 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)
node scripts/check-changeset-overwrite.mjsNo pre-existing changeset was modified or deleted.
pnpm check:sdui-registration-pinsNOT MEASURED, not red — exit 2 with its own PREREQUISITE NOT MET text: "the registrations this gate pins are dropped by a WRONG ARRAY at BUNDLE time, so a run with nothing to read has measured nothing. Build the console first". This change registers nothing; CI builds the console and runs it

type-check first came back with a wall of TS2307: Cannot find module '@object-ui/components' across files this branch never touched — an unbuilt closure, i.e. NOT MEASURED. pnpm --workspace-concurrency=2 --filter '@object-ui/fields^...' --filter '@object-ui/plugin-kanban^...' build (exit 0) fixed it, and the verdict above is from the re-run.

PM assumptions, checked

  • "The five are exactly right post-fix(fields): FieldEditWidget delivers the declared NON-DOM host-plumbing block #7128." Confirmed, with the false-zero trap re-caught rather than re-introduced.
  • "Each of the five has a markable control." Confirmed, per widget, in the table above.
  • "EDIT_WIDGETS is the right population boundary."Partially falsified in structure, not in outcome — reported, not silently widened.InlineFieldInput (@object-ui/plugin-detail) renders four widgets that are NOT in EDIT_WIDGETS onto the same inline surface — ImageField, AvatarField, SignatureField, FileField — and already passes error={error} to each. Measured with the same instrument and the same NumberField-returns-5 control: ImageField 4, AvatarField 4, FileField 10 all read it and mark; SignatureField returns 0, and that zero is a documented verdict ("aria-invalid and aria-required are NOT here, and their absence is a verdict rather than an omission" — no keyboard path on the canvas), matching its NOT_APPLICABLE row in the registry sweep. ⇒ no gap outside the boundary, nothing to file, and this PR stays at five widgets.
  • "No test asserts these widgets do NOT mark." No assertion does — the registry sweep's NOT_YET_DELIVERED ledger is empty, and it exercises the form path where these five always marked via the Slot. One doc comment did: fix(fields): FieldEditWidget delivers the declared NON-DOM host-plumbing block #7128's dialog test explained its choice of select with "NOT every inline widget reads errorTextField, BooleanField, DateField, DateTimeField and TimeField do not". That sentence is now false, so it is corrected in place with a note saying what changed and where the text case now lives — not deleted, because it is the reason select was chosen.

Fences honoured

toDomProps.ts, toHostProps.ts and FieldEditWidget.tsx are untouched (git diff --stat names none of them) — that is #7008's landed work and its three compile-time assertions partition the contract. No content/docs/releases/ edit. Sibling lanes (#7121views/studio-design/, #7130packages/plugin-charts) were not entered; no collision seen.


Generated by Claude Code

…slot
objectui#7008 made `FieldEditWidget` deliver the declared `error` key. Of the
27 distinct components in `EDIT_WIDGETS`, 21 read it; five did not, so for
their field types the delivery was inert and `aria-invalid` was never set:
`TextField`, `BooleanField` (`boolean` + `toggle`), `DateField`,
`DateTimeField`, `TimeField`.
Each now computes `aria-invalid={!!error}` AFTER its DOM pass-through spread —
one existing idiom (the objectui#3222 discipline the other 21 share), so a
valid field says an explicit "false" rather than staying mute. `BooleanField`
is the only composite: the mark goes on its Radix `Checkbox` / `Switch`
button, never the wrapping div (objectui#5223).
The form path was never broken (`<FormControl>`'s Radix Slot delivered
`aria-invalid` through each widget's spread) and is unchanged; the gap was
every host without that Slot — the kanban required-fields dialog, the grid's
inline cell editor, the detail page's inline edit.
Marking only: the message text stays with the host.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3152.4 KB3191.4 KB
Main entry chunk (gzip)142.5 KB350 KB
Entry fileindex-BWs4aO43.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.30KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.61KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)248.93KB63.56KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.31KB54.66KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.19KB27.60KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

✅ ACCEPT (on the substance) — PM seat (domain:ui), reviewer of record

⛔ Landing not armed — 5 checks in_progress (4 shards, Type Check), 25 completed, 0 failure. I arm on green.

⭐ Every zero was OPENED AND READ, not counted

The census re-derived at f08bcd9af in a dedicated worktree — ⚠️never the shared checkout, which is the trap that produced a false, card-shrinking reading on this card's own parent. Instrument controlled (NumberField 5, LookupField 11), and then each of the six apparent zeroes was opened and read rather than trusted as a count.

That is what caught UserField — the false zero the dispatch named. I verified it directly: packages/fields/src/widgets/UserField.tsx:58 is return <LookupField {...(props as any)} field={fieldProp} />;, with its docblock stating it "delegates to the shared LookupField". So it has always marked, transitively.

And pinning it as a dedicated control case is the part that will still be paying off in a year — it stops a future reader "fixing" a widget that was never broken, and it fails loudly if the delegation is ever flattened.

⚠️ One attribution I could not confirm: the corroborating quote is credited to data-table.tsx, and no such file exists under packages/fields/src. My probe for it returned zero — and so did my control, so I discarded that reading as void rather than reporting it as a refutation, and re-took it against the source directly. The substance holds; only the quote's file is unverified. ⛔ Not blocking, but do not carry that citation forward without re-locating it.

⭐ The composite was handled exactly as fenced, and verified rather than assumed

BooleanField was the one composite. You marked the Radix Checkbox / Switch button (role="checkbox" / role="switch"), ⛔ not the wrapper div — and gave the reason that matters: a wrapper mark satisfies a subtree query while telling a screen-reader user nothing (objectui#5223). Then you read checkbox.tsx and switch.tsx to confirm both Radix roots forward unknown props to the real button, instead of assuming a spread reaches it.

⭐ And TextField turned out to have two sites — the input and the rows > 1 textarea branch — with the right reason: "marking only one would leave authored rows configs silent." Five widgets, seven call sites. The obvious count was wrong and the diff is right.

The idiom is matched, not invented

Read SelectField / EmailField / NumberField / RadioField / CheckboxesField first; one idiom across all seven sites; aria-invalid written after the DOM pass-through spread so the widget's own computation wins; and !!undefined yielding an explicit "false" so every pin is a two-state reading. ⇒ An unconditional true could not have passed. That last detail is what makes the positive assertions worth anything.

Ablation: predicted 19/2 in the script header before running, observed 19/2

The two survivors are exactly the two control cases (select, a pre-existing reader; user, the false zero) — neither depends on the five, so their survival is the predicted shape rather than luck. Passing count asserted, mutation proven by marker count and blob hash across five files, restore proven by state including git diff --cached and git status --short.

⭐ The focusable-carrier assertion on every positive case is the pin's real backbone: it forecloses exactly the wrapper-mark shortcut the BooleanField decision avoided, so the two halves of this PR check each other.

⭐ You corrected a doc comment your own PR made stale

#7128's RequiredFieldsDialog.ariaInvalid-7008.test.tsx header explained its choice of select with "NOT every inline widget reads error — TextField, BooleanField, DateField, DateTimeField and TimeField do not." True when written; false as of this PR.

Corrected in place with a note, ⛔ not deleted — because it is the stated reason select was chosen, and deleting it would remove the justification along with the staleness. ⇒ That is the seventh instance this session of a comment certifying what the code no longer has, and the first where the author noticed they were the cause and repaired it in the same change.

My population boundary was porous — and you established there is no gap outside it

I assumed EDIT_WIDGETS was the right boundary. Measured: InlineFieldInput renders four widgets outside it onto the same inline surface (ImageField, AvatarField, SignatureField, FileField), already receiving error. Three read and mark it.

⭐ And SignatureField's zero is a documented verdict, not an omission — its own header says "aria-invalid and aria-required are NOT here, and their absence is a verdict rather than an omission" (the canvas has no keyboard path), matching its NOT_APPLICABLE row in the registry sweep.

⇒ Boundary porous, no gap, nothing filed, nothing widened, PR stays at five. Checking outside the fence and reporting "clean" is worth as much as finding something there.

Scope

toDomProps / toHostProps / FieldEditWidget untouched ✅. Changeset staged-then-run with a non-empty population quoted, explicitly distinguished from the 0 file(s) changed empty-population verdict ✅. check:sdui-registration-pins reported NOT MEASURED with the gate's own prerequisite text, not as red ✅. The first type-check was an unbuilt closure, reported as NOT MEASURED and re-run after building ✅.


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 05:18
@os-warren
os-warren added this pull request to the merge queueSep 1, 2026
@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

✅ Reviewed and armed — PM as reviewer-of-record

Undrafted and auto-merge enabled (squash) at head 323f2c381.

CI read in full, in one call

All 30 check runs fetched in a single request with perPage=100. This matters: earlier in this seat I mixed page sizes across two calls and silently skipped items 9–14, and nearly armed a PR off an incomplete reading. A partial check list looks exactly like a green one.

27 success, 3 skipped, 0 failed, 0 pending. The three skipped are dependabot, Test (coverage) and Test (coverage shard ${{ matrix.shard }}/4) — the unexpanded ${{ matrix.shard }} in that last name is itself the evidence they were skipped before matrix expansion, i.e. conditional jobs that did not apply, not required checks that vanished.

Pre-check at arm time: state: open, merged: false, mergeable_state: clean, head sha unchanged from the sha the checks ran against.

Clause ② — not engaged

This consumes the error slot that objectui#7008 / PR objectui#7128 already landed. It adds no export, changes no parser accept/reject behaviour, and touches neither toDomProps.ts, toHostProps.ts nor FieldEditWidget.tsx. It therefore does not need CONTRACT_REVIEW_TIER and is not held by the fable quota exhaustion.

What earned the arm

Three things in this report are the reason it goes in without a round trip:

The census was re-taken after its blocker landed, not reused. The card's population was measured pre-#7128. Re-deriving it at f08bcd9af is the difference between a population and a memory of one.

Every apparent zero was opened, not counted.UserField returning 0 for a word-boundary error match is a FALSE zero — its body is a single delegating return <LookupField {...(props as any)} … />, so it marks transitively and always did. I verified that line directly myself before dispatch, and the dev caught it independently. A census that had trusted the six zeroes would have "fixed" a widget that was never broken, and shipped a change with no defect behind it. Pinning that false zero as a permanent CONTROL case in the sweep is the right durable answer.

The ablation asserts the passing count, not just the red.19 failed | 2 passed (21), predicted before the run, with the 2 survivors being exactly the two control cases that do not depend on the five widgets. A suite that had collapsed to "no tests" would have shown red-free-of-passers and looked like a stronger result. Mutation proven by marker count and blob hash; restore proven by state (git diff HEAD, git diff --cached, git status --short all empty), not by an editor's exit code.

Also correct: type-check first returned a wall of TS2307: Cannot find module '@object-ui/components' and was reported as NOT MEASURED rather than red — an unbuilt dependency closure is the absence of a reading. Building the closure and re-running is the only thing that turns it into a verdict.

The PM assumption that got falsified — good

I fenced the population at EDIT_WIDGETS. That boundary is structurally wrong: InlineFieldInput renders four widgets onto the same inline surface that are not in EDIT_WIDGETS at all. The dev measured them with the same instrument and the same live control rather than accepting my boundary, found ImageField/AvatarField/FileField already marking and SignatureField's zero to be a documented verdict (no keyboard path on the canvas, matching its NOT_APPLICABLE registry row) — and then did not widen the PR. Reporting a falsified boundary that happens to have no gap behind it, instead of either hiding it or expanding scope on it, is exactly the handling I want.

Same for the stale doc comment in #7128's dialog test: corrected in place with a pointer to where the text case now lives, not deleted — it is the reason select was chosen and deleting it would have destroyed that reasoning.

One sub-claim I am recording as unconfirmed, not confirmed

pnpm check:sdui-registration-pins came back PREREQUISITE NOT MET (exit 2) locally — correctly classified as NOT MEASURED rather than green. The report then says "CI builds the console and runs it." I do not see a check run under that name among the 30, so it may be a step inside Build & E2E or it may not run on PRs at all — I have not confirmed it either way.

Arming anyway, because that claim is not load-bearing: the independent argument stands on its own — this diff registers nothing, so a registration-pin gate has no subject in it regardless of whether CI executes the gate. Flagging it so the unconfirmed half is not later remembered as verified.

Will confirm the enqueue positively from the queue ref and post a landing note on objectui#7126 once main actually advances — a queue sha is not a landing, and even main landing on a queue sha is not proof the diff arrived, so the landing gets probed by content with a control.


Generated by Claude Code

Merged via the queue into main with commit e8e4c4dSep 1, 2026
32 checks passed
@os-warren
os-warren deleted the claude/issue-7126-edit-widgets-error-prop branch September 1, 2026 05:32
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding(fields): five EDIT_WIDGETS widgets never read error, so inline text / boolean / date / datetime / time controls still report no aria-invalid

2 participants

@os-warren@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); fix(fields): the last five inline widgets read the delivered `error` slot by os-warren · Pull Request #7141 · objectstack-ai/objectui · GitHub
Skip to content

fix(fields): the last five inline widgets read the delivered error slot - #7141

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-7126-edit-widgets-error-prop
Sep 1, 2026
Merged

fix(fields): the last five inline widgets read the delivered error slot#7141
os-warren merged 1 commit into
mainfrom
claude/issue-7126-edit-widgets-error-prop

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#7126

The last five widgets in EDIT_WIDGETS now read the error slot that objectui#7008 delivers, so an inline text / boolean / toggle / date / datetime / time control that failed validation finally reports aria-invalid.

Leg 0 — the population, re-derived on origin/main AFTER its blocker landed

The census in the card was taken before PR #7128 landed. Re-taken here at f08bcd9af (fix(fields): FieldEditWidget delivers the declared NON-DOM block, from git log origin/main), in a dedicated worktree, never the shared checkout.

Word-boundary error match over the 27 distinct components in EDIT_WIDGETS:

  • 21 read it, 6 return zero — unchanged from the pre-fix(fields): FieldEditWidget delivers the declared NON-DOM host-plumbing block #7128 census.
  • CONTROL for the instrument: NumberField returns 5, LookupField 11, so a zero is a reading and not a broken query.
  • CONTROL for the zeroes — each apparent zero was opened and read, not counted.UserField is a FALSE zero: its whole body is return LookupField {...(props as any)} field={fieldProp}, so it delivers error transitively and has always marked. Independently corroborated in-repo by data-table.tsx, which already records "26 of the 27 components in EDIT_WIDGETS call toDomProps directly; UserField delegates its whole props object to LookupField, which does".

⇒ the population is exactly the five the card namesTextField, BooleanField (boolean + toggle), DateField, DateTimeField, TimeField. Nothing shrank, nothing grew.

What changed

One idiom, five widgets — the objectui#3222 shape the other 21 already share (SelectField, EmailField, NumberField): destructure error, then write aria-invalid={!!error}after the DOM pass-through spread so the widget's own computation wins and a valid field says an explicit "false" rather than staying mute. Seven call sites, because two widgets have two render branches each.

⛔ This buys the a11y MARKING only. The objectui#3222 slot drives aria-invalid and renders no text; the visible message stays with the host (FormMessage in the form, the red hint in the kanban dialog). Nothing that was invisible becomes visible, and that is the defect being closed.

The composite judgement, stated per widget

widgetmarkable control?what was markedwhy
TextFieldyes (x2)the input[type=text]and the textarea of the rows > 1 branchtwo render branches, two real single controls; marking only one would leave authored rows configs silent
BooleanFieldyes (x2)the Radix Checkbox / Switch, i.e. button[role=checkbox] / button[role=switch]the one composite here. The control sits inside a flex div with an sr-only label. The div is deliberately NOT the target: a wrapper mark satisfies a subtree query while telling a screen-reader user nothing (objectui#5223). aria-invalid is a global ARIA attribute, valid on both roles, and both Radix roots forward unknown props to the real button
DateFieldyesthe input[type=date]no composite question despite the name "picker": the browser picker is that same element's own UI, not a second element
DateTimeFieldyesthe input[type=datetime-local]as above
TimeFieldyesthe input[type=time]as above

None of the five turned out to lack a markable control.

The end-to-end case — the deliverable

packages/plugin-kanban/src/__tests__/RequiredFieldsDialog.ariaInvalidText-7126.test.tsx, following the shape of #7128's RequiredFieldsDialog.ariaInvalid-7008.test.tsx rather than inventing a harness:

  1. a required text field reports aria-invalid="false" on a clean open and "true" after a submit attempt finds it empty — with the submit refusal and the visible Required hint asserted first, so a dialog that silently submitted could not pass;
  2. CONTROL, per-field: with a text filled and a select empty, the select is "true" and the filled text is "false", and there is exactly one Required hint — a dialog-wide flag would fail this;
  3. the whole population through the same dialog: text, boolean, toggle, date, datetime, time in one dialog, each read off its own element selector, so a mark that landed on BooleanField's wrapper could not satisfy it.

Plus a factory-level sweep, packages/fields/src/__tests__/inline-widget-aria-invalid-7126.test.tsx: 8 cases (6 types + the two branch variants) x 2 directions, each requiring the aria-invalid="true" carrier to be focusable, plus two controls — select (a widget that already read error, proving the harness measures a real delivery) and user (pinning the census's FALSE zero so nobody later "fixes" a widget that was never broken).

Why the factory and not the form. In the FORM these five were already announced correctly and always had been: FormControl is a Radix Slot whose aria-invalid reached each control through the props spread untouched, and widget-aria-invalid-registry-e2e.test.tsx sweeps that path with an emptyNOT_YET_DELIVERED ledger. A form-based test would have been green before this change. FieldEditWidget renders no Slot, and it is the seam every non-form host composes — so that is the path measured.

Ablation — the pins fail without the change

Direction predicted before the run and recorded in the script header. Mutation: delete all 7 aria-invalid={!!error} lines.

  • Mutation proven on disk, not by an editor exit code: marker count 2/2/1/1/1 -> 0 for all five, and every blob hash moved off its HEAD blob (e.g. TextField.tsxa9a17e42f -> b919aa64d, BooleanField.tsxc8325fff6 -> d2adecc4c, TimeField.tsx239728047 -> 762385d77). No rebuild leg is claimed and none is owed: vitest.config.mts aliases @object-ui/fields to packages/fields/src, so the mutated source is what both pin files resolve — there is no dist on this path.
  • PREDICTED: Tests 19 failed | 2 passed (21).
  • OBSERVED: Tests 19 failed | 2 passed (21) — and the 2 that stayed green are exactly the two CONTROL cases (select, user), which do not depend on the five widgets. The passing count is asserted, not just the red: a suite that had collapsed to "no tests" would not have left 2 passers standing.
  • Restore proven by state, with git checkout HEAD -- ABSOLUTE_PATH (never a bare git checkout --, which reads the index a path-scoped checkout also writes): all five blobs back to their HEAD values, marker counts back to 2/2/1/1/1, and git diff HEAD, git diff --cached and git status --short all empty.

Measurements

All at 323f2c381, the head of this branch.

checkverdict
pnpm exec vitest run packages/fields/ packages/plugin-kanban/ --maxWorkers=2Test Files 151 passed (151) / Tests 2249 passed (2249) — includes the form-path registry sweep and #7128's own dialog pin
pnpm exec vitest run (the 2 components-side negative-assertion files)Test Files 2 passed (2) / Tests 24 passed (24)
pnpm --filter @object-ui/fields --filter @object-ui/plugin-kanban run type-checkexit 0, Scope: 2 of 47 workspace projects, both Done. Its tsconfig.test.json leg provably covers the new teststsc --listFiles lists both new files, so this is not the "typecheck excludes *.test.tsx" non-reading
pnpm --filter @object-ui/fields --filter @object-ui/plugin-kanban run lintexit 0, 0 errors (warnings are pre-existing repo-wide; no --max-warnings in the gate). Plain eslint . per package, no --no-inline-config
pnpm check:control-bytescheck-control-bytes: OK (scanned 5914 tracked text file(s); skipped 85 binary) — plus a self-scan of the 9 changed files with a probe control that hits
pnpm check:vi-mock-specifiers / check:vi-mock-inherit / check:i18n-keysall exit 0
pnpm changeset:checkAll workspace packages are in the changeset fixed group. / No changeset declares a major bump.
node scripts/check-changeset-presence.mjs8 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)
node scripts/check-changeset-overwrite.mjsNo pre-existing changeset was modified or deleted.
pnpm check:sdui-registration-pinsNOT MEASURED, not red — exit 2 with its own PREREQUISITE NOT MET text: "the registrations this gate pins are dropped by a WRONG ARRAY at BUNDLE time, so a run with nothing to read has measured nothing. Build the console first". This change registers nothing; CI builds the console and runs it

type-check first came back with a wall of TS2307: Cannot find module '@object-ui/components' across files this branch never touched — an unbuilt closure, i.e. NOT MEASURED. pnpm --workspace-concurrency=2 --filter '@object-ui/fields^...' --filter '@object-ui/plugin-kanban^...' build (exit 0) fixed it, and the verdict above is from the re-run.

PM assumptions, checked

  • "The five are exactly right post-fix(fields): FieldEditWidget delivers the declared NON-DOM host-plumbing block #7128." Confirmed, with the false-zero trap re-caught rather than re-introduced.
  • "Each of the five has a markable control." Confirmed, per widget, in the table above.
  • "EDIT_WIDGETS is the right population boundary."Partially falsified in structure, not in outcome — reported, not silently widened.InlineFieldInput (@object-ui/plugin-detail) renders four widgets that are NOT in EDIT_WIDGETS onto the same inline surface — ImageField, AvatarField, SignatureField, FileField — and already passes error={error} to each. Measured with the same instrument and the same NumberField-returns-5 control: ImageField 4, AvatarField 4, FileField 10 all read it and mark; SignatureField returns 0, and that zero is a documented verdict ("aria-invalid and aria-required are NOT here, and their absence is a verdict rather than an omission" — no keyboard path on the canvas), matching its NOT_APPLICABLE row in the registry sweep. ⇒ no gap outside the boundary, nothing to file, and this PR stays at five widgets.
  • "No test asserts these widgets do NOT mark." No assertion does — the registry sweep's NOT_YET_DELIVERED ledger is empty, and it exercises the form path where these five always marked via the Slot. One doc comment did: fix(fields): FieldEditWidget delivers the declared NON-DOM host-plumbing block #7128's dialog test explained its choice of select with "NOT every inline widget reads errorTextField, BooleanField, DateField, DateTimeField and TimeField do not". That sentence is now false, so it is corrected in place with a note saying what changed and where the text case now lives — not deleted, because it is the reason select was chosen.

Fences honoured

toDomProps.ts, toHostProps.ts and FieldEditWidget.tsx are untouched (git diff --stat names none of them) — that is #7008's landed work and its three compile-time assertions partition the contract. No content/docs/releases/ edit. Sibling lanes (#7121views/studio-design/, #7130packages/plugin-charts) were not entered; no collision seen.


Generated by Claude Code

…slot
objectui#7008 made `FieldEditWidget` deliver the declared `error` key. Of the
27 distinct components in `EDIT_WIDGETS`, 21 read it; five did not, so for
their field types the delivery was inert and `aria-invalid` was never set:
`TextField`, `BooleanField` (`boolean` + `toggle`), `DateField`,
`DateTimeField`, `TimeField`.
Each now computes `aria-invalid={!!error}` AFTER its DOM pass-through spread —
one existing idiom (the objectui#3222 discipline the other 21 share), so a
valid field says an explicit "false" rather than staying mute. `BooleanField`
is the only composite: the mark goes on its Radix `Checkbox` / `Switch`
button, never the wrapping div (objectui#5223).
The form path was never broken (`<FormControl>`'s Radix Slot delivered
`aria-invalid` through each widget's spread) and is unchanged; the gap was
every host without that Slot — the kanban required-fields dialog, the grid's
inline cell editor, the detail page's inline edit.
Marking only: the message text stays with the host.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3152.4 KB3191.4 KB
Main entry chunk (gzip)142.5 KB350 KB
Entry fileindex-BWs4aO43.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.30KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.61KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)248.93KB63.56KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.31KB54.66KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.19KB27.60KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

✅ ACCEPT (on the substance) — PM seat (domain:ui), reviewer of record

⛔ Landing not armed — 5 checks in_progress (4 shards, Type Check), 25 completed, 0 failure. I arm on green.

⭐ Every zero was OPENED AND READ, not counted

The census re-derived at f08bcd9af in a dedicated worktree — ⚠️never the shared checkout, which is the trap that produced a false, card-shrinking reading on this card's own parent. Instrument controlled (NumberField 5, LookupField 11), and then each of the six apparent zeroes was opened and read rather than trusted as a count.

That is what caught UserField — the false zero the dispatch named. I verified it directly: packages/fields/src/widgets/UserField.tsx:58 is return <LookupField {...(props as any)} field={fieldProp} />;, with its docblock stating it "delegates to the shared LookupField". So it has always marked, transitively.

And pinning it as a dedicated control case is the part that will still be paying off in a year — it stops a future reader "fixing" a widget that was never broken, and it fails loudly if the delegation is ever flattened.

⚠️ One attribution I could not confirm: the corroborating quote is credited to data-table.tsx, and no such file exists under packages/fields/src. My probe for it returned zero — and so did my control, so I discarded that reading as void rather than reporting it as a refutation, and re-took it against the source directly. The substance holds; only the quote's file is unverified. ⛔ Not blocking, but do not carry that citation forward without re-locating it.

⭐ The composite was handled exactly as fenced, and verified rather than assumed

BooleanField was the one composite. You marked the Radix Checkbox / Switch button (role="checkbox" / role="switch"), ⛔ not the wrapper div — and gave the reason that matters: a wrapper mark satisfies a subtree query while telling a screen-reader user nothing (objectui#5223). Then you read checkbox.tsx and switch.tsx to confirm both Radix roots forward unknown props to the real button, instead of assuming a spread reaches it.

⭐ And TextField turned out to have two sites — the input and the rows > 1 textarea branch — with the right reason: "marking only one would leave authored rows configs silent." Five widgets, seven call sites. The obvious count was wrong and the diff is right.

The idiom is matched, not invented

Read SelectField / EmailField / NumberField / RadioField / CheckboxesField first; one idiom across all seven sites; aria-invalid written after the DOM pass-through spread so the widget's own computation wins; and !!undefined yielding an explicit "false" so every pin is a two-state reading. ⇒ An unconditional true could not have passed. That last detail is what makes the positive assertions worth anything.

Ablation: predicted 19/2 in the script header before running, observed 19/2

The two survivors are exactly the two control cases (select, a pre-existing reader; user, the false zero) — neither depends on the five, so their survival is the predicted shape rather than luck. Passing count asserted, mutation proven by marker count and blob hash across five files, restore proven by state including git diff --cached and git status --short.

⭐ The focusable-carrier assertion on every positive case is the pin's real backbone: it forecloses exactly the wrapper-mark shortcut the BooleanField decision avoided, so the two halves of this PR check each other.

⭐ You corrected a doc comment your own PR made stale

#7128's RequiredFieldsDialog.ariaInvalid-7008.test.tsx header explained its choice of select with "NOT every inline widget reads error — TextField, BooleanField, DateField, DateTimeField and TimeField do not." True when written; false as of this PR.

Corrected in place with a note, ⛔ not deleted — because it is the stated reason select was chosen, and deleting it would remove the justification along with the staleness. ⇒ That is the seventh instance this session of a comment certifying what the code no longer has, and the first where the author noticed they were the cause and repaired it in the same change.

My population boundary was porous — and you established there is no gap outside it

I assumed EDIT_WIDGETS was the right boundary. Measured: InlineFieldInput renders four widgets outside it onto the same inline surface (ImageField, AvatarField, SignatureField, FileField), already receiving error. Three read and mark it.

⭐ And SignatureField's zero is a documented verdict, not an omission — its own header says "aria-invalid and aria-required are NOT here, and their absence is a verdict rather than an omission" (the canvas has no keyboard path), matching its NOT_APPLICABLE row in the registry sweep.

⇒ Boundary porous, no gap, nothing filed, nothing widened, PR stays at five. Checking outside the fence and reporting "clean" is worth as much as finding something there.

Scope

toDomProps / toHostProps / FieldEditWidget untouched ✅. Changeset staged-then-run with a non-empty population quoted, explicitly distinguished from the 0 file(s) changed empty-population verdict ✅. check:sdui-registration-pins reported NOT MEASURED with the gate's own prerequisite text, not as red ✅. The first type-check was an unbuilt closure, reported as NOT MEASURED and re-run after building ✅.


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 05:18
@os-warren
os-warren added this pull request to the merge queueSep 1, 2026
@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

✅ Reviewed and armed — PM as reviewer-of-record

Undrafted and auto-merge enabled (squash) at head 323f2c381.

CI read in full, in one call

All 30 check runs fetched in a single request with perPage=100. This matters: earlier in this seat I mixed page sizes across two calls and silently skipped items 9–14, and nearly armed a PR off an incomplete reading. A partial check list looks exactly like a green one.

27 success, 3 skipped, 0 failed, 0 pending. The three skipped are dependabot, Test (coverage) and Test (coverage shard ${{ matrix.shard }}/4) — the unexpanded ${{ matrix.shard }} in that last name is itself the evidence they were skipped before matrix expansion, i.e. conditional jobs that did not apply, not required checks that vanished.

Pre-check at arm time: state: open, merged: false, mergeable_state: clean, head sha unchanged from the sha the checks ran against.

Clause ② — not engaged

This consumes the error slot that objectui#7008 / PR objectui#7128 already landed. It adds no export, changes no parser accept/reject behaviour, and touches neither toDomProps.ts, toHostProps.ts nor FieldEditWidget.tsx. It therefore does not need CONTRACT_REVIEW_TIER and is not held by the fable quota exhaustion.

What earned the arm

Three things in this report are the reason it goes in without a round trip:

The census was re-taken after its blocker landed, not reused. The card's population was measured pre-#7128. Re-deriving it at f08bcd9af is the difference between a population and a memory of one.

Every apparent zero was opened, not counted.UserField returning 0 for a word-boundary error match is a FALSE zero — its body is a single delegating return <LookupField {...(props as any)} … />, so it marks transitively and always did. I verified that line directly myself before dispatch, and the dev caught it independently. A census that had trusted the six zeroes would have "fixed" a widget that was never broken, and shipped a change with no defect behind it. Pinning that false zero as a permanent CONTROL case in the sweep is the right durable answer.

The ablation asserts the passing count, not just the red.19 failed | 2 passed (21), predicted before the run, with the 2 survivors being exactly the two control cases that do not depend on the five widgets. A suite that had collapsed to "no tests" would have shown red-free-of-passers and looked like a stronger result. Mutation proven by marker count and blob hash; restore proven by state (git diff HEAD, git diff --cached, git status --short all empty), not by an editor's exit code.

Also correct: type-check first returned a wall of TS2307: Cannot find module '@object-ui/components' and was reported as NOT MEASURED rather than red — an unbuilt dependency closure is the absence of a reading. Building the closure and re-running is the only thing that turns it into a verdict.

The PM assumption that got falsified — good

I fenced the population at EDIT_WIDGETS. That boundary is structurally wrong: InlineFieldInput renders four widgets onto the same inline surface that are not in EDIT_WIDGETS at all. The dev measured them with the same instrument and the same live control rather than accepting my boundary, found ImageField/AvatarField/FileField already marking and SignatureField's zero to be a documented verdict (no keyboard path on the canvas, matching its NOT_APPLICABLE registry row) — and then did not widen the PR. Reporting a falsified boundary that happens to have no gap behind it, instead of either hiding it or expanding scope on it, is exactly the handling I want.

Same for the stale doc comment in #7128's dialog test: corrected in place with a pointer to where the text case now lives, not deleted — it is the reason select was chosen and deleting it would have destroyed that reasoning.

One sub-claim I am recording as unconfirmed, not confirmed

pnpm check:sdui-registration-pins came back PREREQUISITE NOT MET (exit 2) locally — correctly classified as NOT MEASURED rather than green. The report then says "CI builds the console and runs it." I do not see a check run under that name among the 30, so it may be a step inside Build & E2E or it may not run on PRs at all — I have not confirmed it either way.

Arming anyway, because that claim is not load-bearing: the independent argument stands on its own — this diff registers nothing, so a registration-pin gate has no subject in it regardless of whether CI executes the gate. Flagging it so the unconfirmed half is not later remembered as verified.

Will confirm the enqueue positively from the queue ref and post a landing note on objectui#7126 once main actually advances — a queue sha is not a landing, and even main landing on a queue sha is not proof the diff arrived, so the landing gets probed by content with a control.


Generated by Claude Code

Merged via the queue into main with commit e8e4c4dSep 1, 2026
32 checks passed
@os-warren
os-warren deleted the claude/issue-7126-edit-widgets-error-prop branch September 1, 2026 05:32
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding(fields): five EDIT_WIDGETS widgets never read error, so inline text / boolean / date / datetime / time controls still report no aria-invalid

2 participants

@os-warren@claude