Repository navigation
Console buildSections drops the field-level maxLength override while its own docstring promises overrides win — the input gets the object's ceiling, not the form's #5595
Description
Activity
huangyiirene commented
on Aug 21, 2026 CollaboratorMore actionsTriage: lands in
apps/console/src/components/FormPage.tsxbuildSections—domain:ui, type Bug, promoted topm:queue. Ruling on the fork the card poses: honour the override (maxLength: override.maxLength ?? def.maxLength). This is a declared≠enforced restoration, not a feature: the docstring promises overrides win, every sibling key implements it, and #5542 deliberately made the key writable by converging onto the shared declaration — an object-only carve-out would be narrowing a declared authoring surface, which would need a ruling this card gives no business case for (a tighter per-form ceiling over a generous object ceiling is a legitimate authoring intent; note the override can only matter within the object's storage ceiling at submit time regardless). Dispatch note: the #5542 pinexpect(row.maxLength).toBeUndefined()records the OLD answer — its flip to the override value is the pre-registered evidence of the fix, not collateral damage; delete-or-invert, do not green it around. Same file as #5594 — fold-or-serial answer: serial (different functions but one hot file and #5594 carries its own pin work; two S diffs, cheap to sequence, folding fails five-gate ① different defect shape/fix).
Generated by Claude Code
- addedbugSomething isn't workingSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatobjectui ui stream: fix lands on the published library or apps — objectui execution seat
on Aug 21, 2026 Claim —
domain:uiexecution seat (#5560), round 9.- Session:
session_012u2pRjcqAYtoEjgr3wwhnK - Branch:
claude/issue-5595-buildsections-maxlength-override - Label:
pm:queue→pm:dispatched
Triage's ruling on the fork the card poses — honour the override (
maxLength: override.maxLength ?? def.maxLength) — is applied, not relitigated. The dev does not re-open the object-only alternative.Dispatch note carried verbatim: the #5542 pin
expect(row.maxLength).toBeUndefined()records the OLD answer, so its flip to the override value is the pre-registered evidence of the fix, not collateral damage. ⛔ Delete-or-invert it deliberately and say which; do not green it around.Serialization: triage answered serial against #5594, and #5594 has since landed (PR #5624, 19:26:29Z). This is unblocked — but
FormPage.tsxchanged in that PR, so the dev re-derivesbuildSectionsand bothmaxLength={field.maxLength}DOM sites on currentorigin/mainrather than trusting the card's:340/:367/:875/:983againstcad512fe1.
Generated by Claude Code
- Session:
- added a commit that references this issue
on Aug 21, 2026 { "issue": 5595, "status": "done", "branch": "claude/issue-5595-buildsections-maxlength-override", "pr": "https://github.com/objectstack-ai/objectui/pull/5635", "premise_still_valid": true, "summary": "Applied triage's ruling verbatim: buildSections now builds `maxLength: override.maxLength ?? def.maxLength` instead of `def.maxLength`, matching the function's own docstring and every sibling key. `??` not `||`, so an explicitly declared 0 stays a value the author wrote. The card's premise held in full on current origin/main — the docstring promise, the one unconditional key, and both `maxLength={field.maxLength}` DOM sites are all still there, and the shared app-shell FormFieldSpec (packages/app-shell/src/views/metadata-admin/form-spec.ts:26) does declare `maxLength?: number`, so the override really is writable and really was landing on an inert branch. Only the four line numbers had drifted (#5594 / PR #5624 landed in this file); every one was re-derived by code. Four buildSections cases added to FormPage.test.ts and a new 152-line FormPage.maxLength.test.tsx pinning the rendered attribute at both DOM sites — that end-to-end leg is not belt-and-braces, since this repo has twice shipped a declared max_length that never reached the DOM (#5201, #5253) by mechanisms a merge-level pin cannot see.", "tests": "All runs from the repo root (package-cwd vitest is refused by the repo guard), all against the FINAL commit f078f7a9c with a clean tree (`git status` empty, `git diff HEAD` empty). Heavy runs serialized through the shared container lock; each result below quotes the tool's own verdict line, and exit codes were captured before any pipe (`cmd > file 2>&1; EXIT=$?`).\n\n- `pnpm exec vitest run apps/console/` -> `Test Files 67 passed (67)` / `Tests 730 passed (730)`; wrapper `VERDICT command-exit 0 · held the lock 313s`.\n- Baseline before any edit (2 files): `Tests 62 passed (62)`. After: 70 across 3 files (+4 unit, +4 DOM).\n- `pnpm --filter @object-ui/console type-check` -> 0 errors, with the script line echoed (`> @object-ui/console@17.6.0 type-check` / `> tsc --noEmit && tsc -b tsconfig.node.json --force`) so this was not a zero-match `--filter` reading as green. NOTE: the FIRST run returned 340 errors, all cascading TS2307/TS2882 against workspace packages — the unbuilt-dependency-closure trap in a fresh worktree, not this change. `pnpm --workspace-concurrency=2 --filter '@object-ui/console^...' build` first (321s), then 0. This also re-judges the tsc-only PIN A-E type assertions in FormPage.fieldSpec.test.ts, which still hold.\n- eslint, declared narrowing (see `changeset`/PR body for the three required pieces): ran `eslint .` inside apps/console — that package's own lint script verbatim, i.e. the COMPLETE population for the one package touched, not a file subset. `--format json`: 167 files enumerated by eslint itself, errorCount 0, warningCount 201 (all pre-existing; none in the edited region FormPage.tsx:425-432). The new FormPage.maxLength.test.tsx appears in that enumeration at 0 errors / 0 warnings, so it is covered rather than silently ignored. Invariance for the other 45 packages: eslint.config.js sets only `ecmaVersion` + `globals` under `languageOptions` — no `parserOptions.project`, no `projectService` — so typescript-eslint runs WITHOUT type information and a diff confined to apps/console cannot move a verdict on any untouched file. Repo-wide `pnpm lint` is CI's run (measured >5min held by a sibling agent in this container).\n- Derived gates, all exit 0: check-control-bytes (`✅ OK (scanned 4681 tracked text file(s); skipped 85 binary)`), check-changeset-presence, check-changeset-no-major, check-changeset-fixed, check-lint-coverage (`✅ 46/46 packages linted, 0 with outstanding errors`), check-type-check-coverage (`✅ 45/46 via type-check` + `✅ 41/41 packages compile their tests`), check-phantom-dependencies (`✅ Every in-scope import is declared by the package that publishes it`), check-package-self-import. Control-byte self-scan of my five files with `grep -naP '[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]'`: no matches.\n- Broken gauges noted, not fixed, per dispatch: check-eager-closure-budget exits 2 until a console build writes its report; check-doc-snippet-types exits 1 until the workspace is built.\n- Toolchain note worth recording: a first console-suite attempt used `--project '|@object-ui/console|'`, copying the `|…|` decoration vitest prints around project names. It failed LOUDLY (`No projects matched the filter`) rather than passing while running nothing.", "evidence": { "verified_line_numbers": "All four card numbers were against cad512fe1 and ALL FOUR had drifted; each was re-derived by CODE on origin/main @ f52d36c96. Docstring 'Field-level FormField overrides take precedence over object defaults': card :340 -> actual :393. `maxLength: def.maxLength,`: card :367 -> actual :425 (now :432 after the fix, which adds a 7-line comment above it). `maxLength={field.maxLength}` textarea arm (textarea/paragraph/long_text): card :875 -> actual :978. `maxLength={field.maxLength}` default `input type=\"text\"` arm: card :983 -> actual :1086. Drift is uniform in direction, +53 to +103 lines, consistent with #5594/PR #5624 landing predicate wiring, an exported isFieldVisible and a RenderableField field in this file. `buildSections` itself is declared at :395. The card's MECHANISM was exactly right; only its coordinates were stale.", "existing_pin_disposition": "INVERTED, not deleted, and not greened around. FormPage.fieldSpec.test.ts `expect(row.maxLength).toBeUndefined()` -> `expect(row.maxLength).toBe(40)` (verified on disk: old-assert count 1->0, new-assert count 0->1). Rationale: the assertion is the mechanism that produced this finding — #5542 chose to RECORD whether each newly-declarable key was honoured rather than assume it, and the one recorded as not-honoured became this card; deleting it would retire that mechanism, while inverting makes the fix's effect visible at exactly the place the old answer was written. That call's `objectSchema` argument is `null`, so the 40 can only have come from the field override. Its 6-line explanatory comment (which argued the old behaviour was correct) was rewritten in the same commit, as was the file's top docstring sentence 'the keys this renderer does not honour are recorded as not honoured rather than assumed' — both were false about this key after the fix. Confirmed load-bearing: this assertion is one of the 5 reds in ablation leg A.", "ablation": "THREE legs, each a script carrying `trap restore EXIT INT TERM` (restore = `git checkout <branch> -- <path>` against the already-committed fix, so a foreground-cap SIGTERM mid-mutation cannot leave a mutated tree). Every leg asserted anchored `grep -c` counts in BOTH directions BEFORE measuring — fixed-anchor 1->0 AND mutant-anchor 0->1, plus the byte delta and the mutated line echoed from disk — and aborts with 'MUTATION NOT CONFIRMED ON DISK' rather than trusting the editor's exit code. REBUILD: none required, and that is a property of the imports, stated rather than assumed — the tests import `./FormPage`, a RELATIVE path inside the same app, not an `exports`-resolved package boundary, so nothing resolves through any dist/ and the mutated source is what vitest loads. (The console's dependency CLOSURE was built before the type-check; separate requirement.) LEG A, the fix removed (`maxLength: def.maxLength,`, bytes 64351->64329): predicted red, observed red — `Tests 5 failed | 65 passed (70)`, the 5 being the inverted pin (expected undefined to be 40), the override case (expected 200 to be 40), the explicit-0 case (expected 200 to be +0), and both DOM sites reproducing the card's symptom literally (maxlength=\"200\" where the form asked 40; maxlength=\"5000\" where it asked 120). LEG B, object fallback dropped (`maxLength: override.maxLength,`, bytes 64351->64334): `Tests 3 failed | 67 passed (70)` — exactly the three fallback CONTROLS turn red (including the pre-existing 'merges section fields with object schema definitions'), while all 5 leg-A cases stay green. LEG C, a fabricated floor (`override.maxLength ?? def.maxLength ?? 255`, bytes 64351->64358): `Tests 2 failed | 68 passed (70)` — the two both-absent controls turn red. Legs B and C exist specifically to satisfy the dispatch bar that a control be scoped to the SAME thing as the probe and be CAPABLE of failing: leg A alone leaves all four controls green, which proves nothing about them. After every leg the trap restored the tree byte-identically (`git diff HEAD` empty, fixed-anchor back to 1)." }, "changeset": { "present": true, "verdict_line": "✅ 4 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s): .changeset/formpage-maxlength-override-5595.md." }, "open_questions": [], "out_of_scope_findings": [] }Notes for PM, outside the machine-read block:
dist/*.d.tsquestion does not arise. Nopackages/**file is touched — the diff is
four files underapps/console/src/components/plus one.changeset/*.md. Saying so
rather than measuring nothing and calling it identical, per dispatch.skip-changesetlabel: not applicable and not created. This repo has no such
mechanism (that is the framework-side flow), and this change publishes real behaviour, so
it carries a realpatchchangeset for@object-ui/consolerather than an empty
frontmatter. No label was added to the PR.- PR is draft and stays draft — not flipped ready, no auto-merge, not merged.
- Two nested-backtick cells in the PR body's gate table render slightly oddly; left
unedited deliberately, since a body edit triggers the server-side footer append for no
substantive gain. The verdict lines are legible as-is.
Generated by Claude Code
- added a commit that references this issue
on Aug 23, 2026
Found while implementing #5542. Filed unassigned for PM triage — outside that card's
declared file surface (the type declaration and its pin, not the merge logic).
Mechanism
apps/console/src/components/FormPage.tsxbuildSectionsmerges a FormView's fieldoverrides with the target object's field definitions. Its docstring states the rule, at
:340on the merged refcad512fe1:Every key in the loop is built that way —
override.label ?? def.label,override.required ?? def.required,override.readonly ?? false, and so on. One isnot, at
:367:No
override.maxLength ??. The object schema's ceiling wins unconditionally, and theform's own is discarded.
It is load-bearing, not decorative — the value reaches the DOM twice, at
:875and:983:So an author who sets a tighter per-form limit (a short public intake form over a field
whose object-level ceiling is generous) gets the generous one, silently. There is no
diagnostic, and the form still submits, so the symptom is a value the author believed
was refused at the input being accepted.
Why it survived
The console's local
FormFieldSpecdid not declaremaxLengthat all until #5542, sonobody typing a spec in this app could write the override in the first place — the key
was unreachable from the type side, and the merge gap it exposes was therefore never
exercised. #5542 converged that type onto the shared app-shell declaration, which does
declare
maxLength, so the key is now writable here and lands on the inert branch.The behaviour is unchanged by #5542 in both directions: the runtime always ignored the
override, and it still does. What changed is that the gap is now expressible, which is
why it is worth recording rather than leaving as an accident of a too-narrow type.
FormPage.fieldSpec.test.ts(added by #5542) records the current answer explicitlyrather than assuming it:
so whichever way triage rules, the existing pin says which behaviour is being changed.
The decision triage owns
Two coherent outcomes, and picking by "which is less work" would be wrong:
maxLength: override.maxLength ?? def.maxLength, matching thedocstring and every sibling key. One line, and the docstring becomes true.
storage constraint is not a presentation choice), then the docstring's blanket claim
is what is wrong, and it should name the exception.
Related but distinct: #5201 and #5253 (both closed) recorded
max_lengthceilingsfailing to reach the DOM in the built-in form branches. Same key, different renderer and
different mechanism — those were spelling/lookup failures, this one is a merge branch
that was never written.
Boundary
override precedence phrasings). Nothing open covers this renderer's merge gap.