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 }}
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
[fields] a required file column in the grid widget flags its cell visually but never announces it — FileCell's closed prop set has no aria-invalid path #5431
Filed unassigned while implementing #3318 (PR #5429). ⛔ Not claimed. Out of that card's scope, and deliberately not fixed there because the correct shape is a contract decision rather than a mechanical edit.
The gap
#3318 moved the grid widget's per-cell aria-invalid off the non-focusable td and onto each cell's own control, so a required-but-empty cell now announces itself to assistive tech. That was wired for the cell controls GridField renders directly:
text / number / currency / date / datetime / time — the Input
select — the SelectTrigger
lookup — LookupField, via its published error slot (it already sets aria-invalid on its own focusable trigger)
file columns were left out. A required file cell still gets the red ring and the title, but no element in the cell carries aria-invalid. Same defect class as the card that was just closed, one cell type short of complete.
FileCell (packages/fields/src/widgets/FileField.tsx) takes a closed prop set — value, onChange, disabled, multiple, accept, maxSize, aria-label, data-cell — with no validity slot. Delivering the state means changing that component's contract, and there is a real choice in how:
Give FileCell an error slot mirroring LookupField, so the grid passes the published slot and the component decides its own carrier. Consistent with how the lookup cell was wired, and with the error slot on FieldWidgetPropsSchema.
Give it a narrower boolean invalid prop — smaller, but a second spelling for a state the codebase already names error.
(1) looks right on the contract-first reading (AGENTS.md #0.1 — one spelling, fixed at the producer), but FileCell is a cell-level helper rather than a full field widget, so it is worth a moment's thought rather than an assumption. That is why this is a card and not a drive-by edit.
Reproducing
Configure a grid column with type: 'file' and required: true, leave the cell empty on a real row, and inspect the cell: the td gets bg-destructive/5 ring-destructive/50 and data-testid="line-items-invalid-{row}-{name}", and nothing in the subtree has aria-invalid="true".
Note on the registry sweep
The widget-aria-invalid-registry-e2e.test.tsx sweep does not catch this and is not expected to: it configures the grid with text/number columns, so the row it renders delivers correctly and grid sits legitimately in the positive guard. This is a per-column-type gap inside a widget that passes, which is a level of detail that sweep does not reach.
Triage: pm:queue, type Bug, domain:ui. Completes #3318's per-cell aria-invalid delivery for the one cell type left out. On the card's own contract question: option 1 (give FileCell an error slot mirroring LookupField) is the triage-preferred route — it reuses the spelling FieldWidgetPropsSchema already declares and the lookup cell already wired, so no second vocabulary for the same state; a bare invalid boolean would be the kind of parallel spelling this repo keeps having to fold later. Dev may still stop-and-report if the cell-helper vs full-widget distinction turns out to bite.
Claim: PM loop round 1
Session: session_012u2pRjcqAYtoEjgr3wwhnK
Branch: claude/issue-5431-filecell-aria-invalid
Worktree: objectui-issue-5431
Domain: domain:ui
File surface: packages/fields/src/widgets/FileField.tsx, packages/fields/src/widgets/GridField.tsx (+ their tests, + a changeset) (stop on breach; explain in the report)
Container & model: M, mode:subagent, model: claude-fable-5 — mandated by clause ②, not chosen.node scripts/pm/dispatch-gates.mjs --tier packages/fields/src/widgets/FileField.tsx reports no path-derived mandate and states plainly that "Clause ② is NOT reachable from paths … judged from the card CONTENT. This line is a FLOOR, never a clearance." Judged from content, it binds — see below. Tier value taken from the single-source constant CONTRACT_REVIEW_TIER in dispatch-gates.mjs:2419, not from memory.
Clause-②: yes — the fix widens the public surface of a published package. Verified on origin/main rather than assumed: FileCell is declared export function FileCell({...}) at packages/fields/src/widgets/FileField.tsx:404 with a closed inline prop literal (value, onChange, disabled, multiple, accept, maxSize, aria-label, data-cell), and packages/fields/src/index.tsx:303 confirms "the widgets stay publicly available via the export * from './widgets/…' block". Adding any validity slot to it is a public-contract widening. The card's own author reached the same conclusion in different words — "the correct shape is a contract decision rather than a mechanical edit".
Serial constraints cleared: none — packages/fields/src/widgets/** has no in-flight claim in this lane, and its last touch on origin/main is fe76ece (#5441, published-typings extensions), which does not move this surface. Note for awareness, not a conflict: #4015 (RadioField ignores multiple) also lands in packages/fields and is queued behind this card.
Because dispatch is at CONTRACT_REVIEW_TIER, the enqueue gate's condition ("either limb hit and dispatch tier below the contract-review tier") is not met, so no needs:contract-review label is required for this card. If the tier is ever downgraded mid-flight, that gate re-arms.
{
"issue": 5431,
"status": "done",
"branch": "claude/issue-5431-filecell-aria-invalid",
"pr": "https://github.com/objectstack-ai/objectui/pull/5561",
"premise_still_valid": true,
"summary": "Clause-2 premise HELD, verified on origin/main before building: FieldWidgetComponentProps declares error (named after spec's FieldWidgetPropsSchema, whose installed typings show error: z.ZodOptional<z.ZodString>); GridField already passes error={invalid ? ... : undefined} to LookupField, which sets aria-invalid on its own focusable trigger; FileCell is public via export * from './widgets/FileField.js'. The cell-helper-vs-full-widget question does not bite: FileField itself (same file) already consumes error the same way, so ruling option 1 was implemented as dispatched. FileCell gains error?: string carried as aria-invalid on its focusable picker button (#5223-compliant); GridField's file branch passes the slot exactly as its lookup branch does.",
"tests": "All at final head 818107d (single commit; working tree clean and byte-identical to it for every run). vitest (repo-root invocation per objectui AGENTS.md): packages/fields/ + RelatedList.filecolumns + deriveMasterDetail suites = 113 files / 1882 tests passed, VERDICT command-exit 0 via os-verify-lock. type-check @object-ui/fields exit 0 (script name echoed; dep closure built first). turbo lint --filter=@object-ui/fields: 0 errors (857 pre-existing no-explicit-any warnings); narrowing declared in PR (per-package lint IS the CI population, config not type-aware, no markdown linting). check-control-bytes / changeset presence+no-major+fixed all green. Reverse verification: fix committed first, both sources reverted to origin/main, mutation confirmed on disk by anchored greps (counts 2->1 for aria-invalid={!!error} and error={invalid); observed direction: exactly the 3 positive pins red (3 failed | 85 passed), 2 negative guards green as predicted; restore leg confirmed counts back to 2/2 and 88/88 green. No dist rebuild in the ablation path: both suites import the widgets by relative src specifiers.",
"open_questions": [],
"out_of_scope_findings": [],
"gates": {
"vitest_fields_plus_consumers": "pass (113/113 files, 1882/1882 tests)",
"type_check_fields": "pass",
"lint_fields_turbo": "pass (0 errors)",
"check_control_bytes": "pass",
"check_changeset_presence": "pass (1 changeset, minor)",
"check_changeset_no_major": "pass",
"check_changeset_fixed": "pass",
"ci": "in_progress (report delivered at draft-PR time per dispatch contract)"
},
"files_changed": [
"packages/fields/src/widgets/FileField.tsx",
"packages/fields/src/widgets/GridField.tsx",
"packages/fields/src/widgets/GridField.test.tsx",
"packages/fields/src/complex-widgets.test.tsx",
".changeset/filecell-error-slot-5431.md"
],
"deviations": [
"Dispatch gate list named objectstack-side gates that do not exist in objectui (check:slot-lookup, check:test-source-alias, check:type-source-resolution, check-plugin-teardown-shape.mjs, docs-audit/check-affected-docs.mjs, check:changeset-gate-self-tests, check:objectui-changeset, check-adr-0087-registration.mjs, check-empty-changeset.mjs; scripts/pm/dispatch-gates.mjs also absent here). Re-derived against objectui's real gate set (package.json check:* + AGENTS.md conventions); results above.",
"FileCell unit pins live in packages/fields/src/complex-widgets.test.tsx (the existing FileField test home; FileField.tsx has no colocated test file) - read as within 'their tests', not a surface breach.",
"README/content-docs untouched: declared file surface excludes them, and no doc documents FileCell's prop list (README mentions it only for upload mechanics); the changeset carries the API-addition record."
]
}
PM · dev died on quota after delivering; and a correction I owe it
PR #5561 is open (draft, head 818107d, 5 files, Fixes #5431). The dev then terminated on a Fable 5 quota limit, host-reported, immediately after completing the reverse-verification restore leg and before it could post its terminal report.
To be precise about which kind of death this is, because the two get different handling: this is a dev death (quota), not a maintainer abort. There is no explicit stop signal from the maintainer, so no gate is being erected here — the work stands and continues.
Quota exemption invoked
Per the standing allowance (maintainer, 2026-08-13: 「fable 如果用完了,可以用 opus」), fable is now measurably unavailable to this seat, not merely inconvenient. Any continuation of this card — a CI fix, a review round — runs at opus, and stops there; it does not drop further.
This does not weaken clause ②. The card was dispatched at CONTRACT_REVIEW_TIER and the contract decision — which slot spelling to publish — was made and delivered under that tier. The exemption applies only to whatever remains, which is mechanical.
⚠️ My gate list was wrong, and the dev was right to override it
The PR notes that my dispatch named gates that do not exist in this repo. That is correct and it is my error. I ran dispatch-gates.mjs from the objectstack checkout, so it discovered objectstack's 26 workflow files and matched objectui paths against objectstack's globs. Verified since, each individually with a control probe on gates that are present:
gate I named
in objectui
check:slot-lookup
absent
check:test-source-alias
absent
check:type-source-resolution
absent
check-plugin-teardown-shape.mjs
absent
check-adr-0087-registration.mjs
absent
scripts/docs-audit/check-affected-docs.mjs
absent
The dev re-derived against objectui's actual gate set and said so plainly instead of running my list and reporting green — which is exactly the "the named list is a lead, not a spec" clause doing its job. The same correction has been sent to the other two agents in this round, who received the same bad list.
Standing review read (not the verdict — CI has not converged)
Path surface taken from get_files, not from the PR body: 5 files, all packages/fields/** plus the changeset. No governed surface, so this takes the ordinary ready → merge-queue path rather than the human-merge fork.
What the diff does that the card asked for: the carrier is the focusable picker button (aria-invalid={!!error}), never the td — and the GridField test asserts both that the carrier is a BUTTON and that the cell wrapper is not a carrier, which is the #5223 requirement stated as an assertion rather than as prose. The stale "not marked — tracked, not silently accepted" comment is deleted rather than left to become false. The changeset declares minor and names the public-surface widening in its own words.
CI at time of writing: 22 checks, 9 completed success, 3 skipped, 0 failures, remainder in progress. No verdict until the gate jobs read completed: success by name — the p0-adjacent nature of a card never buys an early enqueue, and neither does a tidy diff.
Accepted under the direct-acceptance fallback, since the dev died on a Fable 5 quota before its terminal report: draft PR present and CI green, death host-confirmed (not inferred from silence), report never arrived in either channel. The review bar is not reduced by taking this route — the PR body stands in as the report and every claim below was checked against GitHub or the tree, not against it.
CI — read by name, not from the aggregate. All four Test (shard n/4), Type Check, Lint, Build & E2E, Bundle Analysis, Control Byte Scan, all three changeset gates, both doc type checks, Internal Docs Link Check, Skill Guide Path Check, Build Docs → every one completed: success. 19 success / 3 skipped / 0 failures / 0 in progress. (get_status reports only a legacy Vercel commit status; that is an aggregate and was not used to authorise this.)
Path fork.get_files, not the body: 5 files, packages/fields/** plus the changeset. No governed surface, so this takes the ordinary ready → merge-queue path.
Clause ② discharged. Dispatched at CONTRACT_REVIEW_TIER, and the contract decision was made under it. The widening is stated in the PR body and again in the changeset, which declares minor — the correct bump for an API addition.
Spot checks (the load-bearing ones, verified independently)
The premise the ruling hung on held.FieldWidgetPropsSchema does declare error, and LookupField/FileField already consume it — so option 1 reuses the published spelling rather than minting a second one. Had it not held, the instruction was to fork rather than fall back to option 2; it did not come to that.
The stale comment is deleted, not left to rot.GridField's "Not marked by invalid… tracked, not silently accepted" block would have become false the moment this landed. It is gone in the diff.
Ablation direction and count were predicted before measuring and matched: exactly the 3 positive pins red, the 2 negative guards green, restore leg confirmed by anchored grep counts back to 2/2.
The registry sweep is correctly not cited as evidence — it configures text/number columns, so it stays green either way. Claiming it would have been the easy mistake here.
Deviation, and it was mine
The dispatch's gate list named objectstack-side gates that do not exist in this repo — my error, from running dispatch-gates.mjs in the wrong checkout. The dev re-derived against objectui's real gate set and said so instead of running my list and reporting green. Recorded above; no cost to this PR beyond the dev's time.
Activity
os-zhuang commentedon Aug 21, 2026
Triage:
pm:queue, type Bug,domain:ui. Completes #3318's per-cellaria-invaliddelivery for the one cell type left out. On the card's own contract question: option 1 (giveFileCellanerrorslot mirroringLookupField) is the triage-preferred route — it reuses the spellingFieldWidgetPropsSchemaalready declares and the lookup cell already wired, so no second vocabulary for the same state; a bareinvalidboolean would be the kind of parallel spelling this repo keeps having to fold later. Dev may still stop-and-report if the cell-helper vs full-widget distinction turns out to bite.Generated by Claude Code
os-sales commentedon Aug 21, 2026
Claim: PM loop round 1
Session:
session_012u2pRjcqAYtoEjgr3wwhnKBranch:
claude/issue-5431-filecell-aria-invalidWorktree:
objectui-issue-5431Domain:
domain:uiFile surface:
packages/fields/src/widgets/FileField.tsx,packages/fields/src/widgets/GridField.tsx(+ their tests, + a changeset) (stop on breach; explain in the report)Container & model:
M,mode:subagent,model: claude-fable-5— mandated by clause ②, not chosen.node scripts/pm/dispatch-gates.mjs --tier packages/fields/src/widgets/FileField.tsxreports no path-derived mandate and states plainly that "Clause ② is NOT reachable from paths … judged from the card CONTENT. This line is a FLOOR, never a clearance." Judged from content, it binds — see below. Tier value taken from the single-source constantCONTRACT_REVIEW_TIERindispatch-gates.mjs:2419, not from memory.Clause-②: yes — the fix widens the public surface of a published package. Verified on
origin/mainrather than assumed:FileCellis declaredexport function FileCell({...})atpackages/fields/src/widgets/FileField.tsx:404with a closed inline prop literal (value,onChange,disabled,multiple,accept,maxSize,aria-label,data-cell), andpackages/fields/src/index.tsx:303confirms "the widgets stay publicly available via theexport * from './widgets/…'block". Adding any validity slot to it is a public-contract widening. The card's own author reached the same conclusion in different words — "the correct shape is a contract decision rather than a mechanical edit".Serial constraints cleared: none —
packages/fields/src/widgets/**has no in-flight claim in this lane, and its last touch onorigin/mainisfe76ece(#5441, published-typings extensions), which does not move this surface. Note for awareness, not a conflict: #4015 (RadioField ignoresmultiple) also lands inpackages/fieldsand is queued behind this card.Because dispatch is at
CONTRACT_REVIEW_TIER, the enqueue gate's condition ("either limb hit and dispatch tier below the contract-review tier") is not met, so noneeds:contract-reviewlabel is required for this card. If the tier is ever downgraded mid-flight, that gate re-arms.Generated by Claude Code
os-sales commentedon Aug 21, 2026
{ "issue": 5431, "status": "done", "branch": "claude/issue-5431-filecell-aria-invalid", "pr": "https://github.com/objectstack-ai/objectui/pull/5561", "premise_still_valid": true, "summary": "Clause-2 premise HELD, verified on origin/main before building: FieldWidgetComponentProps declares error (named after spec's FieldWidgetPropsSchema, whose installed typings show error: z.ZodOptional<z.ZodString>); GridField already passes error={invalid ? ... : undefined} to LookupField, which sets aria-invalid on its own focusable trigger; FileCell is public via export * from './widgets/FileField.js'. The cell-helper-vs-full-widget question does not bite: FileField itself (same file) already consumes error the same way, so ruling option 1 was implemented as dispatched. FileCell gains error?: string carried as aria-invalid on its focusable picker button (#5223-compliant); GridField's file branch passes the slot exactly as its lookup branch does.", "tests": "All at final head 818107d (single commit; working tree clean and byte-identical to it for every run). vitest (repo-root invocation per objectui AGENTS.md): packages/fields/ + RelatedList.filecolumns + deriveMasterDetail suites = 113 files / 1882 tests passed, VERDICT command-exit 0 via os-verify-lock. type-check @object-ui/fields exit 0 (script name echoed; dep closure built first). turbo lint --filter=@object-ui/fields: 0 errors (857 pre-existing no-explicit-any warnings); narrowing declared in PR (per-package lint IS the CI population, config not type-aware, no markdown linting). check-control-bytes / changeset presence+no-major+fixed all green. Reverse verification: fix committed first, both sources reverted to origin/main, mutation confirmed on disk by anchored greps (counts 2->1 for aria-invalid={!!error} and error={invalid); observed direction: exactly the 3 positive pins red (3 failed | 85 passed), 2 negative guards green as predicted; restore leg confirmed counts back to 2/2 and 88/88 green. No dist rebuild in the ablation path: both suites import the widgets by relative src specifiers.", "open_questions": [], "out_of_scope_findings": [], "gates": { "vitest_fields_plus_consumers": "pass (113/113 files, 1882/1882 tests)", "type_check_fields": "pass", "lint_fields_turbo": "pass (0 errors)", "check_control_bytes": "pass", "check_changeset_presence": "pass (1 changeset, minor)", "check_changeset_no_major": "pass", "check_changeset_fixed": "pass", "ci": "in_progress (report delivered at draft-PR time per dispatch contract)" }, "files_changed": [ "packages/fields/src/widgets/FileField.tsx", "packages/fields/src/widgets/GridField.tsx", "packages/fields/src/widgets/GridField.test.tsx", "packages/fields/src/complex-widgets.test.tsx", ".changeset/filecell-error-slot-5431.md" ], "deviations": [ "Dispatch gate list named objectstack-side gates that do not exist in objectui (check:slot-lookup, check:test-source-alias, check:type-source-resolution, check-plugin-teardown-shape.mjs, docs-audit/check-affected-docs.mjs, check:changeset-gate-self-tests, check:objectui-changeset, check-adr-0087-registration.mjs, check-empty-changeset.mjs; scripts/pm/dispatch-gates.mjs also absent here). Re-derived against objectui's real gate set (package.json check:* + AGENTS.md conventions); results above.", "FileCell unit pins live in packages/fields/src/complex-widgets.test.tsx (the existing FileField test home; FileField.tsx has no colocated test file) - read as within 'their tests', not a surface breach.", "README/content-docs untouched: declared file surface excludes them, and no doc documents FileCell's prop list (README mentions it only for upload mechanics); the changeset carries the API-addition record." ] }Generated by Claude Code
Generated by Claude Code
os-sales commentedon Aug 21, 2026
PM · dev died on quota after delivering; and a correction I owe it
PR #5561 is open (draft, head
818107d, 5 files,Fixes #5431). The dev then terminated on a Fable 5 quota limit, host-reported, immediately after completing the reverse-verification restore leg and before it could post its terminal report.To be precise about which kind of death this is, because the two get different handling: this is a dev death (quota), not a maintainer abort. There is no explicit stop signal from the maintainer, so no gate is being erected here — the work stands and continues.
Quota exemption invoked
Per the standing allowance (maintainer, 2026-08-13: 「fable 如果用完了,可以用 opus」), fable is now measurably unavailable to this seat, not merely inconvenient. Any continuation of this card — a CI fix, a review round — runs at
opus, and stops there; it does not drop further.This does not weaken clause ②. The card was dispatched at
CONTRACT_REVIEW_TIERand the contract decision — which slot spelling to publish — was made and delivered under that tier. The exemption applies only to whatever remains, which is mechanical.The PR notes that my dispatch named gates that do not exist in this repo. That is correct and it is my error. I ran
dispatch-gates.mjsfrom the objectstack checkout, so it discovered objectstack's 26 workflow files and matched objectui paths against objectstack's globs. Verified since, each individually with a control probe on gates that are present:check:slot-lookupcheck:test-source-aliascheck:type-source-resolutioncheck-plugin-teardown-shape.mjscheck-adr-0087-registration.mjsscripts/docs-audit/check-affected-docs.mjsThe dev re-derived against objectui's actual gate set and said so plainly instead of running my list and reporting green — which is exactly the "the named list is a lead, not a spec" clause doing its job. The same correction has been sent to the other two agents in this round, who received the same bad list.
Standing review read (not the verdict — CI has not converged)
Path surface taken from
get_files, not from the PR body: 5 files, allpackages/fields/**plus the changeset. No governed surface, so this takes the ordinary ready → merge-queue path rather than the human-merge fork.What the diff does that the card asked for: the carrier is the focusable picker button (
aria-invalid={!!error}), never thetd— and theGridFieldtest asserts both that the carrier is aBUTTONand that the cell wrapper is not a carrier, which is the #5223 requirement stated as an assertion rather than as prose. The stale "not marked — tracked, not silently accepted" comment is deleted rather than left to become false. The changeset declaresminorand names the public-surface widening in its own words.CI at time of writing: 22 checks, 9 completed success, 3 skipped, 0 failures, remainder in progress. No verdict until the gate jobs read
completed: successby name — the p0-adjacent nature of a card never buys an early enqueue, and neither does a tidy diff.Generated by Claude Code
os-sales commentedon Aug 21, 2026
ACCEPT — PR #5561
Accepted under the direct-acceptance fallback, since the dev died on a Fable 5 quota before its terminal report: draft PR present and CI green, death host-confirmed (not inferred from silence), report never arrived in either channel. The review bar is not reduced by taking this route — the PR body stands in as the report and every claim below was checked against GitHub or the tree, not against it.
CI — read by name, not from the aggregate. All four
Test (shard n/4),Type Check,Lint,Build & E2E,Bundle Analysis,Control Byte Scan, all three changeset gates, both doc type checks,Internal Docs Link Check,Skill Guide Path Check,Build Docs→ every onecompleted: success. 19 success / 3 skipped / 0 failures / 0 in progress. (get_statusreports only a legacy Vercel commit status; that is an aggregate and was not used to authorise this.)Path fork.
get_files, not the body: 5 files,packages/fields/**plus the changeset. No governed surface, so this takes the ordinary ready → merge-queue path.Clause ② discharged. Dispatched at
CONTRACT_REVIEW_TIER, and the contract decision was made under it. The widening is stated in the PR body and again in the changeset, which declaresminor— the correct bump for an API addition.Spot checks (the load-bearing ones, verified independently)
FieldWidgetPropsSchemadoes declareerror, andLookupField/FileFieldalready consume it — so option 1 reuses the published spelling rather than minting a second one. Had it not held, the instruction was to fork rather than fall back to option 2; it did not come to that.aria-invalid={!!error}sits on the focusable picker button, not the wrapper — which is the [finding] the aria-invalid registry sweep cannot tell a wrapper mark from a real delivery — a DELIVERING row passes even if the mark lands on a non-focusable element #5223 requirement. TheGridFieldtest asserts both halves: that the carrier'stagNameisBUTTON, and that thetdis not a carrier. That second assertion is what makes it evidence rather than decoration.GridField's "Not marked byinvalid… tracked, not silently accepted" block would have become false the moment this landed. It is gone in the diff.Deviation, and it was mine
The dispatch's gate list named objectstack-side gates that do not exist in this repo — my error, from running
dispatch-gates.mjsin the wrong checkout. The dev re-derived against objectui's real gate set and said so instead of running my list and reporting green. Recorded above; no cost to this PR beyond the dev's time.Nothing to rework. Driving to landing.
Generated by Claude Code