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.
内建 form 分支的 input 字段:legacy max_length 拼法拿不到任何 maxlength 上限,只在 DOM 上留下一个失效属性 #5201
Triage: promoted to pm:queue, type Bug — one declaration must yield one behaviour on both render paths (the #3272/#3400/#3402/#3404/#3439 family), and max_length is a live authoring spelling on the exact path (hand-written FormSchema) that has no normalizing producer in between. Scope ruling to keep this dispatchable without a design decision: the ceiling-parsing half only — dual-read maxLength ?? max_length in the built-in input branch the way PR #5200 did for textarea, and keep max_length out of the DOM (add it to stripRendererOnlyProps if that is the mechanism). The visible counter / accessible-limit half for single-line inputs is not commissioned — if the implementer believes it is inseparable, stop and report rather than widening.
Dedup note: the filer could not search at filing time (API rate limit). This seat's spot-check against the known family cards found no open duplicate for the input-branch ceiling; if one surfaces, merge there.
Claim: PM loop round 6
Session: session_01RV6yuVCxymHYE16PL9vQkE
Branch: claude/issue-5201-input-max-length-dual-read
Worktree: objectui-issue-5201
Domain: repo:objectui (execution seat)
File surface: packages/components/src/renderers/form/form.tsx (the built-in case 'input' branch), plus a test under packages/components/src/renderers/form/__tests__/. ⛔ OUT: packages/plugin-view (#5097 in flight), content/docs/** (#5229 in flight), packages/cli (#5237, PR5245 open), examples/hello-world (#5236, PR5244 open), packages/types/**.
Container & model: S, mode:subagent, model: opus — the edit is small and has a reference implementation 20 lines away, but the scope boundary is a real cliff.
Clause-②: no — it makes an already-declared, already-dual-read spelling work on one more render path. No accept-set change, no public surface widening.
Serial constraints cleared: packages/components/src/renderers/form/form.tsx is FREE as of this round — #4259's PR5238 merged at 19:54Z and nothing else open touches it. Note this file is a hot spot with two landings today (#3439/PR5200, #4259/PR5238), so re-verify at your claim and take every anchor from the tree.
Zone 1 — triage's scope ruling, and it is a cliff not a preference
> Scope ruling to keep this dispatchable without a design decision: the ceiling-parsing half only — dual-read maxLength ?? max_length in the built-in input branch the way PR #5200 did for textarea, and keep max_length out of the DOM (add it to stripRendererOnlyProps if that is the mechanism). The visible counter / accessible-limit half for single-line inputs is not commissioned — if the implementer believes it is inseparable, stop and report rather than widening.
⛔ No counter, no accessible-limit announcement, no aria-describedby wiring for a limit on the single-line input. The card is explicit that whether a single-line input should carry a visible count is an independent design trade-off that does not follow from the textarea conclusion. The ceiling-parsing half is mechanical; the counter half is not. If you conclude they cannot be separated, stop and report — do not widen.
Premise — verified on origin/main this round
The defect holds, and the reference implementation is in the same file:
form.tsx:3078 const { max_length: _maxLengthLegacy, ...textareaProps } = fieldProps as any;
form.tsx:3079 const maxLength = (fieldProps as any).maxLength ?? (fieldProps as any).max_length;
form.tsx:3080 const rest = stripRendererOnlyProps(textareaProps);
That is the case 'textarea' branch after PR5200. The built-in case 'input' branch does none of it — it spreads stripRendererOnlyProps(fieldProps) and never reads the declared ceiling, so max_length: 50 produces no cap at all plus a stray, inert max_length="50" attribute (invalid HTML), while maxLength: 50 works only by the coincidence that it happens to be a valid DOM attribute name.
Line numbers will have moved — this file took two landings today. Take them from the tree.
One judgement the ruling leaves you, and I want it decided deliberately
Triage wrote "add it to stripRendererOnlyProps if that is the mechanism". The in-file precedent does not do that — PR5200 used a local destructure (const { max_length: _maxLengthLegacy, ...textareaProps }) rather than extending the shared strip list.
That difference matters: stripRendererOnlyProps is shared by every branch, so adding max_length there changes what reaches the DOM for widgets you are not fixing and have not tested. The local destructure is the narrower, reversible choice and it is what the neighbouring code already does.
Follow the local-destructure precedent unless you find a concrete reason not to — and if you do use the shared list, enumerate every branch it affects and say why that is safe. ⛔ Do not make a repo-wide behaviour change as a side effect of a one-branch fix.
Why this is a real user-facing defect and not style
max_length is a live authoring spelling, not a fossil: TextAreaField on the registered path dual-reads it (framework#1878 §3), three form-field producers dual-read and normalise to maxLength (ObjectForm.tsx, sectionFields.ts, EmbeddableForm.applyDefaultMaxLengths), and packages/types/src/field-types.ts declares it on several field types. Every reader in the repo honours it except this branch — and this branch is exactly the one serving hand-written FormSchema fed straight to the renderer, where there is no normalising producer in between. On that path the author is the producer.
Tests
max_length: 50 on a built-in input yields maxlength="50" on the element, and no stray max_length attribute. Assert both halves — the missing cap and the invalid attribute are two distinct defects and a test that only checks the cap would let the stray attribute survive.
maxLength: 50 keeps working (it does today, by coincidence — pin it so the fix cannot break the spelling that accidentally worked).
Both spellings together: maxLength wins, matching maxLength ?? max_length.
Neither declared: no maxlength attribute at all, no stray attribute.
Assert via getAttributeNames() / getAttribute('maxlength'), the way the card measured it — that is what makes the stray-attribute half observable.
Reverse-verify: restore the branch, keep the tests, predict the reds before running, report predicted vs observed.
Dedup — the filer could not do it
The card records that search_issues / list_issues returned API rate limit already exceeded three times at filing, so the required pre-file dedup never ran. My spot-check against the family cards found no open duplicate for the input-branch ceiling. Do one more check before you start — if an open duplicate surfaces, merge there and report rather than landing a second fix.
Gates
Derive from your diff. scripts/pm/dispatch-gates.mjs is objectstack-only — here derive from .github/workflows and package.json. Changeset required (@object-ui/components); patch or minor, ⛔ never major.
⛔ Do not background a long check and end your turn. Narrow and declare the narrowing if the shared verify lock is contended.
Deliver
Draft PR against main, first body line Fixes #5201. Push the branch early. Return the JSON report and post it as a comment whose first line is the literal <!-- os-dev-report --> marker.
{
"issue": 5201,
"status": "done",
"branch": "claude/issue-5201-input-max-length-dual-read",
"pr": "https://github.com/objectstack-ai/objectui/pull/5255",
"premise_still_valid": true,
"summary": "The built-in `case 'input'` branch of packages/components/src/renderers/form/form.tsx now resolves `maxLength ?? max_length` inside the branch and keeps the legacy key off the DOM, so a declared ceiling caps in both authored spellings and no inert `max_length` attribute is rendered. Followed the in-file local-destructure precedent (PR #5200 on the `textarea` branch) rather than extending `stripRendererOnlyProps`: that shared list feeds `checkbox`/`switch`/`select`/`default` through the common `domFieldProps`, so extending it would change DOM output for four widget families this card neither fixes nor tests — the judgement triage explicitly left open, decided and written into the PR body and the code comment. Ceiling half only: no counter, no announced limit, no aria-describedby wiring for a cap, per the triage cliff. Dedup re-run before starting (two keyword searches): no open duplicate; the only related card is the closed #3439.",
"tests": "All runs from the repo root — the repo's vitest guard rejects package-directory runs as false-green, and it caught exactly that on the first attempt. Dependency closure built first: `pnpm --workspace-concurrency=2 --filter '@object-ui/components^...' build` (the package's own `prebuild` is insufficient — it stops at types/core/react and react then fails on unbuilt i18n and data-objectstack). NEW TEST: `pnpm exec vitest run packages/components/src/renderers/form/__tests__/form-builtin-input-max-length.test.tsx --maxWorkers=2` -> `Test Files 1 passed (1) / Tests 5 passed (5)`. REGRESSION: `pnpm exec vitest run packages/components/src/renderers/form/ --maxWorkers=2` -> `Test Files 46 passed (46) / Tests 281 passed (281)`. GATES at short HEAD `c14aa9881` with `git status --porcelain` empty (so the tree is that commit): `pnpm --filter @object-ui/components run type-check` exit 0 with the script name echoed (not a zero-match no-op); `pnpm --filter @object-ui/components run lint` exit 0, 0 errors / 887 pre-existing warnings, none in the changed files; `node scripts/check-control-bytes.mjs` OK (4652 files) plus a manual `grep -naP '[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]'` over the three changed files, no hits; `node scripts/check-changeset-presence.mjs` OK (1 changeset for 2 changed source files); `node scripts/check-changeset-no-major.mjs` OK (`patch`). Gate set derived from `.github/workflows` (lint.yml, ci.yml, control-bytes.yml, changeset-presence.yml, changeset-guard.yml) since dispatch-gates.mjs is objectstack-only. REVERSE-VERIFICATION: fix committed first, then `git checkout origin/main -- packages/components/src/renderers/form/form.tsx` with the tests kept. Predicted BEFORE running: 3 failed / 2 passed — camelCase and neither-declared stay green; legacy-cap, stray-attribute and both-spellings (passes its maxlength assert, fails on the stray key) go red. Observed: `Tests 3 failed | 2 passed (5)`, direction and count matching, failures reproducing the card's measured output verbatim: `expected null to be '50'`, `expected [ 'class', 'max_length', 'id', ...(5) ] to not include 'max_length'`, `expected [ 'class', 'maxlength', ...(7) ] to not include 'max_length'`. NO REBUILD LEG APPLIES and none was needed: the test imports the subject by relative path into this package's own source (`../../../renderers`), so neither the mutated nor the restored leg reads a `dist/` — the mutation was observable immediately, which the reds prove. Restored with `git checkout HEAD -- <path>`; `git status --porcelain` empty afterwards, i.e. byte-identical to the committed fix.",
"open_questions": [],
"out_of_scope_findings": [
"filed as #5253: the `default` fallback branch of the same switch has the identical ceiling defect for every non-builtin, unregistered field type — measured with `type: 'zzunknown'`, `max_length: 50` yields `maxlength=null` plus a stray `max_length` attribute while `maxLength: 50` caps. Not fixed here: the ruling scoped the work to the `input` branch and the fallback serves an open-ended set of types, so it deserves its own scope call rather than riding along.",
"filed as #5254: a form field of `type: 'email'` resolves through the bare-name fallback to the `ui`-namespace SDUI node renderer registered at packages/components/src/renderers/form/input.tsx:89 (ComponentRegistry.get('email') is true, 'field:email' is false), which leaks `field` (the raw metadata object) and `max_length` onto the DOM and applies no cap. Filed without a proposed fix: it has two competing readings (DOM-leak whitelist, the #4425 family, vs. the type resolution not falling back to node renderers at all) and needs triage."
]
}
Activity
os-support-ai commentedon Aug 18, 2026
Triage: promoted to
pm:queue, type Bug — one declaration must yield one behaviour on both render paths (the #3272/#3400/#3402/#3404/#3439 family), andmax_lengthis a live authoring spelling on the exact path (hand-writtenFormSchema) that has no normalizing producer in between. Scope ruling to keep this dispatchable without a design decision: the ceiling-parsing half only — dual-readmaxLength ?? max_lengthin the built-ininputbranch the way PR #5200 did fortextarea, and keepmax_lengthout of the DOM (add it tostripRendererOnlyPropsif that is the mechanism). The visible counter / accessible-limit half for single-line inputs is not commissioned — if the implementer believes it is inseparable, stop and report rather than widening.Dedup note: the filer could not search at filing time (API rate limit). This seat's spot-check against the known family cards found no open duplicate for the
input-branch ceiling; if one surfaces, merge there.Generated by Claude Code
os-support-ai commentedon Aug 18, 2026
Claim: PM loop round 6
Session:
session_01RV6yuVCxymHYE16PL9vQkEBranch:
claude/issue-5201-input-max-length-dual-readWorktree:
objectui-issue-5201Domain:
repo:objectui(execution seat)File surface:
packages/components/src/renderers/form/form.tsx(the built-incase 'input'branch), plus a test underpackages/components/src/renderers/form/__tests__/. ⛔ OUT:packages/plugin-view(#5097 in flight),content/docs/**(#5229 in flight),packages/cli(#5237, PR5245 open),examples/hello-world(#5236, PR5244 open),packages/types/**.Container & model:
S,mode:subagent,model: opus— the edit is small and has a reference implementation 20 lines away, but the scope boundary is a real cliff.Clause-②: no — it makes an already-declared, already-dual-read spelling work on one more render path. No accept-set change, no public surface widening.
Serial constraints cleared:
packages/components/src/renderers/form/form.tsxis FREE as of this round — #4259's PR5238 merged at 19:54Z and nothing else open touches it. Note this file is a hot spot with two landings today (#3439/PR5200, #4259/PR5238), so re-verify at your claim and take every anchor from the tree.Zone 1 — triage's scope ruling, and it is a cliff not a preference
> Scope ruling to keep this dispatchable without a design decision: the ceiling-parsing half only — dual-read
maxLength ?? max_lengthin the built-ininputbranch the way PR #5200 did fortextarea, and keepmax_lengthout of the DOM (add it tostripRendererOnlyPropsif that is the mechanism). The visible counter / accessible-limit half for single-line inputs is not commissioned — if the implementer believes it is inseparable, stop and report rather than widening.⛔ No counter, no accessible-limit announcement, no
aria-describedbywiring for a limit on the single-line input. The card is explicit that whether a single-line input should carry a visible count is an independent design trade-off that does not follow from the textarea conclusion. The ceiling-parsing half is mechanical; the counter half is not. If you conclude they cannot be separated, stop and report — do not widen.Premise — verified on
origin/mainthis roundThe defect holds, and the reference implementation is in the same file:
That is the
case 'textarea'branch after PR5200. The built-incase 'input'branch does none of it — it spreadsstripRendererOnlyProps(fieldProps)and never reads the declared ceiling, somax_length: 50produces no cap at all plus a stray, inertmax_length="50"attribute (invalid HTML), whilemaxLength: 50works only by the coincidence that it happens to be a valid DOM attribute name.Line numbers will have moved — this file took two landings today. Take them from the tree.
One judgement the ruling leaves you, and I want it decided deliberately
Triage wrote "add it to
stripRendererOnlyPropsif that is the mechanism". The in-file precedent does not do that — PR5200 used a local destructure (const { max_length: _maxLengthLegacy, ...textareaProps }) rather than extending the shared strip list.That difference matters:
stripRendererOnlyPropsis shared by every branch, so addingmax_lengththere changes what reaches the DOM for widgets you are not fixing and have not tested. The local destructure is the narrower, reversible choice and it is what the neighbouring code already does.Follow the local-destructure precedent unless you find a concrete reason not to — and if you do use the shared list, enumerate every branch it affects and say why that is safe. ⛔ Do not make a repo-wide behaviour change as a side effect of a one-branch fix.
Why this is a real user-facing defect and not style
max_lengthis a live authoring spelling, not a fossil:TextAreaFieldon the registered path dual-reads it (framework#1878 §3), three form-field producers dual-read and normalise tomaxLength(ObjectForm.tsx,sectionFields.ts,EmbeddableForm.applyDefaultMaxLengths), andpackages/types/src/field-types.tsdeclares it on several field types. Every reader in the repo honours it except this branch — and this branch is exactly the one serving hand-writtenFormSchemafed straight to the renderer, where there is no normalising producer in between. On that path the author is the producer.Tests
max_length: 50on a built-ininputyieldsmaxlength="50"on the element, and no straymax_lengthattribute. Assert both halves — the missing cap and the invalid attribute are two distinct defects and a test that only checks the cap would let the stray attribute survive.maxLength: 50keeps working (it does today, by coincidence — pin it so the fix cannot break the spelling that accidentally worked).maxLengthwins, matchingmaxLength ?? max_length.maxlengthattribute at all, no stray attribute.getAttributeNames()/getAttribute('maxlength'), the way the card measured it — that is what makes the stray-attribute half observable.Reverse-verify: restore the branch, keep the tests, predict the reds before running, report predicted vs observed.
Dedup — the filer could not do it
The card records that
search_issues/list_issuesreturnedAPI rate limit already exceededthree times at filing, so the required pre-file dedup never ran. My spot-check against the family cards found no open duplicate for theinput-branch ceiling. Do one more check before you start — if an open duplicate surfaces, merge there and report rather than landing a second fix.Gates
Derive from your diff.
scripts/pm/dispatch-gates.mjsis objectstack-only — here derive from.github/workflowsandpackage.json. Changeset required (@object-ui/components);patchorminor, ⛔ nevermajor.⛔ Do not background a long check and end your turn. Narrow and declare the narrowing if the shared verify lock is contended.
Deliver
Draft PR against
main, first body lineFixes #5201. Push the branch early. Return the JSON report and post it as a comment whose first line is the literal<!-- os-dev-report -->marker.Generated by Claude Code
fix(components): built-in input branch reads the declared ceiling in …
os-support-ai commentedon Aug 18, 2026
{ "issue": 5201, "status": "done", "branch": "claude/issue-5201-input-max-length-dual-read", "pr": "https://github.com/objectstack-ai/objectui/pull/5255", "premise_still_valid": true, "summary": "The built-in `case 'input'` branch of packages/components/src/renderers/form/form.tsx now resolves `maxLength ?? max_length` inside the branch and keeps the legacy key off the DOM, so a declared ceiling caps in both authored spellings and no inert `max_length` attribute is rendered. Followed the in-file local-destructure precedent (PR #5200 on the `textarea` branch) rather than extending `stripRendererOnlyProps`: that shared list feeds `checkbox`/`switch`/`select`/`default` through the common `domFieldProps`, so extending it would change DOM output for four widget families this card neither fixes nor tests — the judgement triage explicitly left open, decided and written into the PR body and the code comment. Ceiling half only: no counter, no announced limit, no aria-describedby wiring for a cap, per the triage cliff. Dedup re-run before starting (two keyword searches): no open duplicate; the only related card is the closed #3439.", "tests": "All runs from the repo root — the repo's vitest guard rejects package-directory runs as false-green, and it caught exactly that on the first attempt. Dependency closure built first: `pnpm --workspace-concurrency=2 --filter '@object-ui/components^...' build` (the package's own `prebuild` is insufficient — it stops at types/core/react and react then fails on unbuilt i18n and data-objectstack). NEW TEST: `pnpm exec vitest run packages/components/src/renderers/form/__tests__/form-builtin-input-max-length.test.tsx --maxWorkers=2` -> `Test Files 1 passed (1) / Tests 5 passed (5)`. REGRESSION: `pnpm exec vitest run packages/components/src/renderers/form/ --maxWorkers=2` -> `Test Files 46 passed (46) / Tests 281 passed (281)`. GATES at short HEAD `c14aa9881` with `git status --porcelain` empty (so the tree is that commit): `pnpm --filter @object-ui/components run type-check` exit 0 with the script name echoed (not a zero-match no-op); `pnpm --filter @object-ui/components run lint` exit 0, 0 errors / 887 pre-existing warnings, none in the changed files; `node scripts/check-control-bytes.mjs` OK (4652 files) plus a manual `grep -naP '[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]'` over the three changed files, no hits; `node scripts/check-changeset-presence.mjs` OK (1 changeset for 2 changed source files); `node scripts/check-changeset-no-major.mjs` OK (`patch`). Gate set derived from `.github/workflows` (lint.yml, ci.yml, control-bytes.yml, changeset-presence.yml, changeset-guard.yml) since dispatch-gates.mjs is objectstack-only. REVERSE-VERIFICATION: fix committed first, then `git checkout origin/main -- packages/components/src/renderers/form/form.tsx` with the tests kept. Predicted BEFORE running: 3 failed / 2 passed — camelCase and neither-declared stay green; legacy-cap, stray-attribute and both-spellings (passes its maxlength assert, fails on the stray key) go red. Observed: `Tests 3 failed | 2 passed (5)`, direction and count matching, failures reproducing the card's measured output verbatim: `expected null to be '50'`, `expected [ 'class', 'max_length', 'id', ...(5) ] to not include 'max_length'`, `expected [ 'class', 'maxlength', ...(7) ] to not include 'max_length'`. NO REBUILD LEG APPLIES and none was needed: the test imports the subject by relative path into this package's own source (`../../../renderers`), so neither the mutated nor the restored leg reads a `dist/` — the mutation was observable immediately, which the reds prove. Restored with `git checkout HEAD -- <path>`; `git status --porcelain` empty afterwards, i.e. byte-identical to the committed fix.", "open_questions": [], "out_of_scope_findings": [ "filed as #5253: the `default` fallback branch of the same switch has the identical ceiling defect for every non-builtin, unregistered field type — measured with `type: 'zzunknown'`, `max_length: 50` yields `maxlength=null` plus a stray `max_length` attribute while `maxLength: 50` caps. Not fixed here: the ruling scoped the work to the `input` branch and the fallback serves an open-ended set of types, so it deserves its own scope call rather than riding along.", "filed as #5254: a form field of `type: 'email'` resolves through the bare-name fallback to the `ui`-namespace SDUI node renderer registered at packages/components/src/renderers/form/input.tsx:89 (ComponentRegistry.get('email') is true, 'field:email' is false), which leaks `field` (the raw metadata object) and `max_length` onto the DOM and applies no cap. Filed without a proposed fix: it has two competing readings (DOM-leak whitelist, the #4425 family, vs. the type resolution not falling back to node renderers at all) and needs triage." ] }Generated by Claude Code
fix(components): enforce declared max_length on the form default fall…
fix(components): built-in input branch reads the declared ceiling in …
fix(components): enforce declared max_length on the form default fall…