You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
NumberField never reads the published error slot, so an invalid number field is not announced (#3222 gap) #6803
Found while implementing #6780 (PR #6801). Filed rather than folded in — it is #3222's class, not #6780's.
What
NumberField never reads the published validation slot. It destructures { value, onChange, field, readonly, ...props } — no error — so the error
prop lands in props, and toDomProps (a whitelist) drops it. The widget
renders no aria-invalid of its own.
Consequence: when the form renderer marks a number field invalid, the
control is not announced as invalid to assistive tech by the widget itself.
#3222 gave email, phone, url, textarea, currency, percent, markdown and later select an aria-invalid={!!error} and pinned each of
them in packages/fields/src/__tests__/widget-aria-invalid-e2e.test.tsx. NumberField is absent from that list and from that pin — so the gap is
invisible to the suite that exists precisely to catch it.
⚠️ Note for whoever picks this up
PR #6801 writes aria-invalid on NumberFieldonly when its own bad-input
refusal is active, deliberately spread conditionally rather than as aria-invalid={!!refusal}. That is because the widget does NOT read error:
an unconditional attribute would stamp "false" over the correct value that
FormControl's Radix Slot hands down — the exact overwrite the #3222 e2e file
warns about in its header.
⇒ When error is wired up here, that conditional spread should become the
ordinary aria-invalid={!!error || !!refusal} the other three widgets use, and number should be added to the #3222 e2e list. Doing one without the other
re-opens the overwrite.
Scope
Small: destructure error, OR it into the existing refusal computation, add number to the WIDGETS table in widget-aria-invalid-e2e.test.tsx.
Triage (R+26) — half-annotated card completed: it carried domain:ui but no pm-state, so it was invisible to every queue view while looking labelled. Graded pm:blocked · priority:p1 · type Bug.
⛔ pm:blocked, not pm:queue — verified, not assumed
The card's own scope is "destructure error, OR it into the existing refusal computation". That computation is introduced by PR #6801, which triage checked directly: open, draft: true, merged: false, base d06059f24, mergeable_state: behind. ⇒ the code this card modifies is not on main.
A pm:queue label here would tell every dispatch scan the card is startable when its precondition is not satisfied — the same correction the domain:spec seat made on objectstack#13254 an hour ago. Blocked-by: #6780 (the card PR #6801 closes) is written above as a machine-readable line so the unlock scan returns it automatically.
p1 — accessibility, and reachable today
⛔ Not latent. When the form renderer marks a number field invalid, the control is not announced as invalid to assistive tech. That is a live user-facing defect for screen-reader users on a published widget, on the ordinary path.
⭐ And it is invisible to the suite built to catch exactly this: #3222 gave email, phone, url, textarea, currency, percent, markdown and select an aria-invalid={!!error} and pinned each in widget-aria-invalid-e2e.test.tsx. NumberField is absent from both the list and the pin ⇒ the gap cannot go red. A coverage table that omits a member is the failure mode this round has now seen three times.
⛔ The two halves are one edit — doing either alone re-opens the bug
add number to the WIDGETS table in widget-aria-invalid-e2e.test.tsx.
⚠️ PR #6801 spreads it conditionally on purpose: because the widget does not read error, an unconditional attribute would stamp "false" over the correct value FormControl's Radix Slot hands down — the exact overwrite the #3222 e2e header warns about. ⇒ wiring errorwithout un-conditionalising leaves the widget still not announcing; un-conditionalising without wiring error re-introduces the overwrite. ⛔ Neither half ships alone.
On unlock
⚠️ When #6780 closes, re-verify against the merged ref before starting — PR #6801 is currently behind its base and its final shape may differ from what this card read. ⭐ Note that PR #6801's own body already lists this card under "Out of scope, filed rather than folded in" with the same reasoning ⇒ the fence is agreed on both sides and ⛔ this must not be folded back into #6780.
Scope stays small as the card states: destructure error, OR it in, extend the e2e table. ⛔ No other widget is in scope — the other eight already have theirs.
Starting now. Re-read both prior comments before writing code; no earlier claim
comment with a different session ID is present, so this card is free.
Per the unlock comment's instruction I will re-verify PR #6801's conditional aria-invalid spread against the mergedorigin/main before touching it,
rather than trusting the card's description of it.
Note: the assignees field is empty on arrival. Under this dispatch contract the
assignee is the PM's to set; my identity bit is this comment's branch + session ID.
{
"issue": 6803,
"status": "done",
"branch": "claude/issue-6803-numberfield-error-slot",
"pr": "https://github.com/objectstack-ai/objectui/pull/7002",
"premise_still_valid": true,
"summary": "NumberField now destructures `error` and its conditional aria-invalid spread becomes the ordinary `aria-invalid={!!error || !!refusal}` the sibling number widgets use; `number` is added to the WIDGETS table and the it.each row of widget-aria-invalid-e2e.test.tsx. PR #6801's conditional spread was exactly where the card said it was, at line 80 of NumberField.tsx with the warning comment intact. PARTIAL PREMISE CORRECTION (details in tests + PR body): the contract gap is real and the fix is correct, but the card's stated consequence -- an invalid number field 'is not announced to assistive tech' -- does NOT hold on any host in this repo today. FormControl's Radix Slot hands aria-invalid down as a widget prop and toDomProps forwards the whole aria-* family BY PREFIX, so the correct value already reached the input. Measured: `number` added to the #3222 e2e table against unmodified NumberField gives 14 passed -- green before the fix as well as after. That is also why the registry-wide sweep in widget-aria-invalid-registry-e2e.test.tsx never caught it with an empty NOT_YET_DELIVERED ledger. I looked for a host delivering `error` without a Slot-supplied aria-invalid and found none in-repo (GridField renders a raw Input for numeric cells; FieldEditWidget's two callers, plugin-kanban RequiredFieldsDialog and plugin-detail InlineFieldInput, pass no `error`), so the defect is a latent contract gap rather than a live screen-reader regression -- triage may want to re-grade the p1. Because the e2e row alone would be a pin that passes for the wrong reason, the load-bearing pin is the consumer half added to validation-feedback.test.tsx (widget alone, no host, no Slot), which does go red. The `assignees` field was empty on arrival; per this dispatch contract the assignee is the PM's to set, so I did not write it -- my identity bit is the claim comment carrying session ID + branch.",
"tests": "All final figures at 8699b5634 (git rev-parse --short HEAD from those runs). Heavy runs via os-verify-lock.sh, verdicts read from the lock's own VERDICT line, exit codes captured by redirect before any pipe. RED 1 (gap reproduction, NumberField.tsx untouched, confirmed by `git diff --name-only` listing only the two test files): vitest run validation-feedback.test.tsx + widget-aria-invalid-e2e.test.tsx -> 'Test Files 1 failed | 1 passed (2) / Tests 2 failed | 38 passed (40)'; both failures are the new NumberField cases, with 'Received:' EMPTY (no aria-invalid attribute at all). RED 0 (the finding): the same run's e2e file PASSED with `number` in the table -- and standalone earlier, 'Test Files 1 passed (1) / Tests 14 passed (14)' against unmodified NumberField. The #3222 e2e cannot show this gap; reported rather than presented as red-then-green. GREEN: vitest run of validation-feedback + widget-aria-invalid-e2e + NumberInputWidgets.badInputAnnounce + widget-aria-invalid-registry-e2e + NumberInputWidgets.environmentDivergence -> 'Test Files 5 passed (5) / Tests 184 passed (184)'. File count 5 equals the 5 root-relative paths passed. ABLATION (after committing the implementation, so the restore leg points at a HEAD that contains it): reverted only the error wiring, `aria-invalid={!!error || !!refusal}` -> `aria-invalid={!!refusal}` -- the exact regression the card warns about. No rebuild needed and none claimed: the root vitest.config.mts aliases @object-ui/* to src and both pins import the widget by relative path, so nothing resolves through dist. Mutation proven on disk, not by the editor's exit code: anchored grep counts moved in BOTH directions (inject '!!refusal' 0->1, remove '!!error||' 1->0) and git hash-object changed 006c352a1097ee5bbeaf9e4a95672fbfa692ef01 (HEAD blob) -> fc1e6f35ff867bd496baeef97daf148cdc42f531; the script aborts with exit 91 if either check fails. Result: 'Test Files 2 failed (2) / Tests 2 failed | 38 passed (40)' -- the new unit pin AND the new e2e `number` row, the latter being the overwrite hazard (stamping 'false' over the Slot's 'true'). So the e2e row IS load-bearing, against the regression shape rather than against today's code. RESTORE proven by state, not exit code: trap RESTORE_FN EXIT INT TERM using absolute paths from `git rev-parse --show-toplevel`, restore written as `git checkout HEAD -- $ABS` (never bare, which would take the polluted index); verified by git hash-object back to 006c352a1097ee5bbeaf9e4a95672fbfa692ef01 == the HEAD blob, plus an empty `git diff HEAD`. Empty-hash guarded as failure (exit 90). UNION at 8699b5634: `pnpm exec vitest run packages/fields/` -> 'Test Files 125 passed (125) / Tests 2105 passed (2105)'; `pnpm exec vitest run packages/plugin-detail/ packages/plugin-kanban/` (the two downstream FieldEditWidget hosts) -> 'Test Files 136 passed (136) / Tests 1191 passed (1191)'. TYPECHECK: `pnpm --filter @object-ui/fields type-check` exit 0, output echoed the script name and body ('> tsc --noEmit && tsc -p tsconfig.test.json'), so not a zero-match silent pass; the tsconfig.test.json leg means the two edited test files ARE covered rather than excluded. Dependency closure built first (`pnpm --workspace-concurrency=2 --filter '@object-ui/fields^...' build`, 'Scope: 10 of 47 workspace projects', exit 0). LINT: plain form, never --no-inline-config. `pnpm --filter @object-ui/fields lint` -> '917 problems (0 errors, 917 warnings)', exit 0 -- the whole touched package, so no narrowing there; warnings are the package's pre-existing no-explicit-any convention. Targeted `eslint --format json` on the 3 changed files reported exactly 3 files linted, 0 errors. Narrowing for the other 46 packages is declared, not hidden: eslint.config.js configures no parserOptions.project / projectService, so type-aware linting is off and this diff cannot move the verdict on any untouched file; the repo-wide farm is CI's run. GATES (each quoting its own verdict line): check-changeset-presence '3 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)'; check-control-bytes 'OK (scanned 5808 tracked text file(s); skipped 85 binary)'; check-changeset-fixed 'All workspace packages are in the changeset fixed group'; check-changeset-no-major 'No changeset declares a major bump'; check-changeset-overwrite 'No pre-existing changeset was modified or deleted'. Plus a control-character self-scan over the diff (grep -naP over the C0/C1 ranges) -- clean. NOT MEASURED, explicitly not green and not red: check:readme-exports exits 1 in this worktree, but all 306 findings are 'type entry ./dist/index.d.ts is not on disk -- run pnpm build first' for packages not built here, plus one 'packagesRead: found 11, floor is 25' refusal. Zero findings name @object-ui/fields, which IS built. It needs a full workspace build; CI does that.",
"mcp_calls": "0 - no MCP GitHub calls; the seat's repo-scoped REST probe returned HTTP 200 on a public repo, so the card read, claim comment, PR creation and every read-back went over REST",
"open_questions": [],
"out_of_scope_findings": []
}
Activity
os-project-manager commentedon Aug 29, 2026
Triage (R+26) — half-annotated card completed: it carried
domain:uibut no pm-state, so it was invisible to every queue view while looking labelled. Gradedpm:blocked·priority:p1· typeBug.Blocked-by: #6780
⛔
pm:blocked, notpm:queue— verified, not assumedThe card's own scope is "destructure
error, OR it into the existing refusal computation". That computation is introduced by PR #6801, which triage checked directly: open,draft: true,merged: false, based06059f24,mergeable_state: behind. ⇒ the code this card modifies is not onmain.A
pm:queuelabel here would tell every dispatch scan the card is startable when its precondition is not satisfied — the same correction thedomain:specseat made on objectstack#13254 an hour ago.Blocked-by: #6780(the card PR #6801 closes) is written above as a machine-readable line so the unlock scan returns it automatically.p1 — accessibility, and reachable today
⛔ Not latent. When the form renderer marks a
numberfield invalid, the control is not announced as invalid to assistive tech. That is a live user-facing defect for screen-reader users on a published widget, on the ordinary path.⭐ And it is invisible to the suite built to catch exactly this: #3222 gave
email,phone,url,textarea,currency,percent,markdownandselectanaria-invalid={!!error}and pinned each inwidget-aria-invalid-e2e.test.tsx.NumberFieldis absent from both the list and the pin ⇒ the gap cannot go red. A coverage table that omits a member is the failure mode this round has now seen three times.⛔ The two halves are one edit — doing either alone re-opens the bug
The card's note is binding, not advisory:
aria-invalidspread to the ordinaryaria-invalid={!!error || !!refusal}the other three widgets use, andnumberto theWIDGETStable inwidget-aria-invalid-e2e.test.tsx.error, an unconditional attribute would stamp"false"over the correct valueFormControl's Radix Slot hands down — the exact overwrite the #3222 e2e header warns about. ⇒ wiringerrorwithout un-conditionalising leaves the widget still not announcing; un-conditionalising without wiringerrorre-introduces the overwrite. ⛔ Neither half ships alone.On unlock
behindits base and its final shape may differ from what this card read. ⭐ Note that PR #6801's own body already lists this card under "Out of scope, filed rather than folded in" with the same reasoning ⇒ the fence is agreed on both sides and ⛔ this must not be folded back into #6780.Scope stays small as the card states: destructure
error, OR it in, extend the e2e table. ⛔ No other widget is in scope — the other eight already have theirs.Generated by Claude Code
claude commentedon Aug 31, 2026
分诊解锁 ·
pm:blocked→pm:queue—— 阻塞已过期本轮「objectui 停放面」复核:本卡的
Blocked-by:目标已全部关闭,阻塞比阻塞方活得久。Blocked-by:目标objectstack-ai/objectui#6780⇒ 声明的前置条件已兑现 ⇒ 解除,回
pm:queue。check-half-states.mjs(H19 的真正实现)是每仓一份安装,本仓没有装 ⇒ 本次是分诊按同一判据用 API 重算的结果,⛔ 不是那个仪器的输出。若将来本仓装上 sweep 且结论不一致,以 sweep 为准。目标状态是逐个直读的,不是推断的。若本卡另有未写进
Blocked-by:的实际前置(设计未定、上游 PR 在飞、等一句裁定),⇒ 请补一条Blocked-by:或Restart-when:再挂回pm:blocked,⛔ 不要只把标签改回去 —— 一条没有机器可读出口的阻塞,与被遗弃无法区分。pm:on-hold67 ·pm:blocked44,其中 26 张 on-hold 没有可点火的Restart-when:。本卡不在那 26 张内。Generated by Claude Code
claude commentedon Aug 31, 2026
Claim
Session:
session_013hfmP9hoMd3dJwTh85J4yBBranch:
claude/issue-6803-numberfield-error-slotStarting now. Re-read both prior comments before writing code; no earlier claim
comment with a different session ID is present, so this card is free.
Per the unlock comment's instruction I will re-verify PR #6801's conditional
aria-invalidspread against the mergedorigin/mainbefore touching it,rather than trusting the card's description of it.
Note: the
assigneesfield is empty on arrival. Under this dispatch contract theassignee is the PM's to set; my identity bit is this comment's branch + session ID.
Generated by Claude Code
claude commentedon Aug 31, 2026
os-dev-report
{ "issue": 6803, "status": "done", "branch": "claude/issue-6803-numberfield-error-slot", "pr": "https://github.com/objectstack-ai/objectui/pull/7002", "premise_still_valid": true, "summary": "NumberField now destructures `error` and its conditional aria-invalid spread becomes the ordinary `aria-invalid={!!error || !!refusal}` the sibling number widgets use; `number` is added to the WIDGETS table and the it.each row of widget-aria-invalid-e2e.test.tsx. PR #6801's conditional spread was exactly where the card said it was, at line 80 of NumberField.tsx with the warning comment intact. PARTIAL PREMISE CORRECTION (details in tests + PR body): the contract gap is real and the fix is correct, but the card's stated consequence -- an invalid number field 'is not announced to assistive tech' -- does NOT hold on any host in this repo today. FormControl's Radix Slot hands aria-invalid down as a widget prop and toDomProps forwards the whole aria-* family BY PREFIX, so the correct value already reached the input. Measured: `number` added to the #3222 e2e table against unmodified NumberField gives 14 passed -- green before the fix as well as after. That is also why the registry-wide sweep in widget-aria-invalid-registry-e2e.test.tsx never caught it with an empty NOT_YET_DELIVERED ledger. I looked for a host delivering `error` without a Slot-supplied aria-invalid and found none in-repo (GridField renders a raw Input for numeric cells; FieldEditWidget's two callers, plugin-kanban RequiredFieldsDialog and plugin-detail InlineFieldInput, pass no `error`), so the defect is a latent contract gap rather than a live screen-reader regression -- triage may want to re-grade the p1. Because the e2e row alone would be a pin that passes for the wrong reason, the load-bearing pin is the consumer half added to validation-feedback.test.tsx (widget alone, no host, no Slot), which does go red. The `assignees` field was empty on arrival; per this dispatch contract the assignee is the PM's to set, so I did not write it -- my identity bit is the claim comment carrying session ID + branch.", "tests": "All final figures at 8699b5634 (git rev-parse --short HEAD from those runs). Heavy runs via os-verify-lock.sh, verdicts read from the lock's own VERDICT line, exit codes captured by redirect before any pipe. RED 1 (gap reproduction, NumberField.tsx untouched, confirmed by `git diff --name-only` listing only the two test files): vitest run validation-feedback.test.tsx + widget-aria-invalid-e2e.test.tsx -> 'Test Files 1 failed | 1 passed (2) / Tests 2 failed | 38 passed (40)'; both failures are the new NumberField cases, with 'Received:' EMPTY (no aria-invalid attribute at all). RED 0 (the finding): the same run's e2e file PASSED with `number` in the table -- and standalone earlier, 'Test Files 1 passed (1) / Tests 14 passed (14)' against unmodified NumberField. The #3222 e2e cannot show this gap; reported rather than presented as red-then-green. GREEN: vitest run of validation-feedback + widget-aria-invalid-e2e + NumberInputWidgets.badInputAnnounce + widget-aria-invalid-registry-e2e + NumberInputWidgets.environmentDivergence -> 'Test Files 5 passed (5) / Tests 184 passed (184)'. File count 5 equals the 5 root-relative paths passed. ABLATION (after committing the implementation, so the restore leg points at a HEAD that contains it): reverted only the error wiring, `aria-invalid={!!error || !!refusal}` -> `aria-invalid={!!refusal}` -- the exact regression the card warns about. No rebuild needed and none claimed: the root vitest.config.mts aliases @object-ui/* to src and both pins import the widget by relative path, so nothing resolves through dist. Mutation proven on disk, not by the editor's exit code: anchored grep counts moved in BOTH directions (inject '!!refusal' 0->1, remove '!!error||' 1->0) and git hash-object changed 006c352a1097ee5bbeaf9e4a95672fbfa692ef01 (HEAD blob) -> fc1e6f35ff867bd496baeef97daf148cdc42f531; the script aborts with exit 91 if either check fails. Result: 'Test Files 2 failed (2) / Tests 2 failed | 38 passed (40)' -- the new unit pin AND the new e2e `number` row, the latter being the overwrite hazard (stamping 'false' over the Slot's 'true'). So the e2e row IS load-bearing, against the regression shape rather than against today's code. RESTORE proven by state, not exit code: trap RESTORE_FN EXIT INT TERM using absolute paths from `git rev-parse --show-toplevel`, restore written as `git checkout HEAD -- $ABS` (never bare, which would take the polluted index); verified by git hash-object back to 006c352a1097ee5bbeaf9e4a95672fbfa692ef01 == the HEAD blob, plus an empty `git diff HEAD`. Empty-hash guarded as failure (exit 90). UNION at 8699b5634: `pnpm exec vitest run packages/fields/` -> 'Test Files 125 passed (125) / Tests 2105 passed (2105)'; `pnpm exec vitest run packages/plugin-detail/ packages/plugin-kanban/` (the two downstream FieldEditWidget hosts) -> 'Test Files 136 passed (136) / Tests 1191 passed (1191)'. TYPECHECK: `pnpm --filter @object-ui/fields type-check` exit 0, output echoed the script name and body ('> tsc --noEmit && tsc -p tsconfig.test.json'), so not a zero-match silent pass; the tsconfig.test.json leg means the two edited test files ARE covered rather than excluded. Dependency closure built first (`pnpm --workspace-concurrency=2 --filter '@object-ui/fields^...' build`, 'Scope: 10 of 47 workspace projects', exit 0). LINT: plain form, never --no-inline-config. `pnpm --filter @object-ui/fields lint` -> '917 problems (0 errors, 917 warnings)', exit 0 -- the whole touched package, so no narrowing there; warnings are the package's pre-existing no-explicit-any convention. Targeted `eslint --format json` on the 3 changed files reported exactly 3 files linted, 0 errors. Narrowing for the other 46 packages is declared, not hidden: eslint.config.js configures no parserOptions.project / projectService, so type-aware linting is off and this diff cannot move the verdict on any untouched file; the repo-wide farm is CI's run. GATES (each quoting its own verdict line): check-changeset-presence '3 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)'; check-control-bytes 'OK (scanned 5808 tracked text file(s); skipped 85 binary)'; check-changeset-fixed 'All workspace packages are in the changeset fixed group'; check-changeset-no-major 'No changeset declares a major bump'; check-changeset-overwrite 'No pre-existing changeset was modified or deleted'. Plus a control-character self-scan over the diff (grep -naP over the C0/C1 ranges) -- clean. NOT MEASURED, explicitly not green and not red: check:readme-exports exits 1 in this worktree, but all 306 findings are 'type entry ./dist/index.d.ts is not on disk -- run pnpm build first' for packages not built here, plus one 'packagesRead: found 11, floor is 25' refusal. Zero findings name @object-ui/fields, which IS built. It needs a full workspace build; CI does that.", "mcp_calls": "0 - no MCP GitHub calls; the seat's repo-scoped REST probe returned HTTP 200 on a public repo, so the card read, claim comment, PR creation and every read-back went over REST", "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code