Skip to content

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

@os-sales

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.tsx buildSections merges a FormView's field
overrides with the target object's field definitions. Its docstring states the rule, at
:340 on the merged ref cad512fe1:

 * Field-level FormField overrides take precedence over object defaults.

Every key in the loop is built that way — override.label ?? def.label,
override.required ?? def.required, override.readonly ?? false, and so on. One is
not, at :367:

maxLength: def.maxLength,

No override.maxLength ??. The object schema's ceiling wins unconditionally, and the
form's own is discarded.

It is load-bearing, not decorative — the value reaches the DOM twice, at :875 and
:983:

maxLength={field.maxLength}

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 FormFieldSpec did not declare maxLength at all until #5542, so
nobody 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 explicitly
rather than assuming it:

expect(row.maxLength).toBeUndefined();

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:

  • honour it — maxLength: override.maxLength ?? def.maxLength, matching the
    docstring and every sibling key. One line, and the docstring becomes true.
  • declare it object-only — if a per-form ceiling is deliberately not a thing (a
    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_length ceilings
failing 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

Activity

  1. huangyiirene commented on Aug 21, 2026

    @huangyiirene
    Collaborator

    Triage: lands in apps/console/src/components/FormPage.tsx buildSections — domain:ui, type Bug, promoted to pm: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 pin expect(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

  2. added
    bugSomething isn't working
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Aug 21, 2026
  3. added theissue type on Aug 21, 2026
  4. self-assigned this
    on Aug 21, 2026
  5. os-sales commented on Aug 21, 2026

    @os-sales
    CollaboratorAuthor

    Claim — domain:ui execution 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.tsx changed in that PR, so the dev re-derives buildSections and both maxLength={field.maxLength} DOM sites on current origin/main rather than trusting the card's :340 / :367 / :875 / :983 against cad512fe1.


    Generated by Claude Code

  6. os-sales commented on Aug 21, 2026

    @os-sales
    CollaboratorAuthor
    {
      "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.ts question does not arise. No packages/** file is touched — the diff is
      four files under apps/console/src/components/ plus one .changeset/*.md. Saying so
      rather than measuring nothing and calling it identical, per dispatch.
    • skip-changeset label: 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 real patch changeset for @object-ui/console rather 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpm:dispatched

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions