Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

[decision-shaped] FormPage has its own switch and it knows 22 of the 49 declared field types — 35 silently degrade to a text box on two routes; keep the switch or route it through the shared resolver? #10179

Closed

Description

@os-elon-musk

Ruled: 5770939088 · letter A · 2026-09-22T03:50Z — batch #211 item 2; FormPage → shared field resolver; A′ NOT adopted (maintainer 「同意,A′ 不采」); state pm:queue (domain:ui)

维护者速读

Console 有两个表单渲染器。共享的那个认 49 种字段类型里的绝大多数;/f/:slug 和 /forms/:name 走的那个自己手写了一个 switch,只认 22 种——剩下 35 种在这两条路由上静默退化成一个纯文本框。作者写了 image、lookup、percent、richtext、signature……看到的都是同一个文本框,没有报错、没有警告。

这一班已经有四张卡是同一个根因,而且是一张一张被人撞出来的。每补一个 arm 只买到一种类型,下一种照样要靠用户发现。

要定的一件事:FormPage 还该不该有自己的 switch?

  • A —— 让它走共享解析器(一次付清:35 种类型 + widget 轴一起解决,但动的是一个渲染器的架构)
  • B —— 保留 switch,按需补 arm(每次便宜,但这条队伍不会变短)

请回一个字母:A 还是 B。


The measurement — this is what makes the question decidable

Delivered as step 1 of objectui#10167 (the file arm), which triage split from this question deliberately. Instruments: FormPage arms parsed from the switch (field.type) body of FieldInput; shared-resolver surface read at runtime; population = @objectstack/spec/data FieldType.options.

reading value
declared spec field types 49
FormPage switch arms 22
widget-map keys (FORM_FIELD_TYPES) 45
⭐ types the shared resolver gives a dedicated widget, absent from FormPage 35
non-case dispatch found in the FormPage switch body NONE

The 35: address · audio · autonumber · avatar · checkboxes · code · color · composite · formula · html · image · json · location · lookup · markdown · master_detail · multiselect · percent · phone · progress · qrcode · rating · record · repeater · richtext · secret · signature · slider · summary · tags · text · tree · user · vector · video.

⚠️ The last row matters for anyone re-deriving this: the enumeration was taken by parsing the switch body, and it confirms there is no map, table or nested helper doing hidden dispatch. ⇒ the arm list IS the whole behaviour, so 22-of-49 is not an artefact of the instrument.

⛔ Two axes an arm list cannot see, and they are why 「add the arms」 does not close the class

  1. The widget axis — FormPage reads no widget key at all, while the shared chain resolves f?.widget || f?.field?.widget || f?.type. Three keys are reachable only that way. Filed separately because it survives any amount of arm-adding.
  2. The arity axis — select with multiple: true resolves to a different widget on the shared chain (mapFieldTypeToFormType('select', {multiple:true}) → field:multiselect, measured at runtime), while this switch's select arm renders a single-value select.

⇒ a renderer with all 49 arms would still be wrong on both.

四棱分析

  • 实际业务需求 —— 这不是投机能力面:35 种类型是 spec 已声明的,共享渲染器已经兑现,两条真实路由上今天就退化。需求是现成的,缺的是这一个渲染器。⚠️ 但要诚实:⛔ 没有测量有多少真实应用在这两条路由上用了这 35 种中的哪几种——那是 A/B 之间成本权衡的关键读数,建议裁决前补。
  • 项目长远合理性 —— A 是 contract-first 方向:一个类型→控件的真相源,声明即兑现。B 是 workaround 的定义——同一份映射维护两份,且第二份永远滞后。北极星要的是前者。
  • 防 AI 写错 —— ⭐ 这一棱最偏向 A,且偏得最硬。作者(尤其是 AI)写 image 是因为 spec 说可以写;平台接受了元数据然后渲染成文本框——声明了却不兑现,而且静默。这正是「宽容恰是 AI 批量犯错被掩盖的温床」。B 让这个坑一直存在,只是每次少一种。
  • 创业阶段不扩散 —— ⚠️ 唯一指向 B 的一棱:A 动的是一个渲染器的架构,不是小改。但要看清这不是能力扩张——35 种类型早已声明并在别处兑现,A 是收敛到已声明契约,不是新增面。⇒ 从紧原则在这里不反对 A,它反对的是「再加一个 switch」。

推荐:A,理由是长远合理性与防错两棱同向且都指向结构性修复,而不扩散一棱经判别后不成立(收敛≠扩张)。⚠️ 呈报分歧:A 的实施成本没有测量,若那笔成本很大,B 加上一条「每补一 arm 必须同时补 widget 与 arity 轴」的纪律是可接受的次优解。

附带的一个子问题,⛔ 不单独立卡

objectui#10166 的 dev 在另一个渲染器上撞到同一个结构:ObjectForm 的扁平路径把按钮交给 @object-ui/components 的表单渲染器,而那里只读 isSubmitting || disabled,宿主无法表达任何自己的「此刻不可提交」理由。选项 A′ 是给 FormSchema 加一个 submitDisabled(一个键、一个读点,并且服务于所有未来的宿主理由)。它扩的是已发布面,所以是维护者的字,不是席位的——随本卡一并裁。⛔ 该 dev 已明确否掉的第三条路(node 级 disabled)不要复活:它会连同每个字段和「取消」一起禁用,把用户困在一个既不能编辑也不能离开的表单里。

四棱卡面块

  • ① 项目长远合理性:A 让「一个字段类型渲染成什么」只剩一个读点,缩小特例;B 把第二张 arm 表钉死成第二契约,增生。
  • ② 实际业务拉动:今天就有人撞上 —— 49 个已声明类型里 35 个在 FormPage 上静默退化成文本框,实测,不是推测。
  • ③ 防 AI 犯错:A 是结构性的(AI 写出任一已声明类型都渲染正确);B 是枚举性的(AI 写出未补的那 35 个之一,仍然静默拿到文本框,没有任何拒绝)。
  • ④ 创业阶段不扩散:A 是收敛到已声明契约,不是新增面;⛔ 不适用从紧反对。子问题 A′(FormSchema.submitDisabled)确实扩已发布面,那一条才是从紧要称量的。

Prior rulings read: field widget resolver,form renderer arms,resolveformwidgettype,submitdisabled → 0 hits; none; thread: none; repo: objectstack-ai/objectstack

推荐:A(子问题一并裁:A′ 采纳)。自检:只看①选 A;②③④ 是否翻转:否 —— ② 与 ③ 同向加强,④ 经判别不成立(收敛≠扩张)。
置信缺口:⛔ A 的实施成本未测量。若那笔成本很大,B 加上「每补一 arm 必须同时补 widget 与 arity 轴」的纪律是可接受的次优解 —— 这一条是本席看不见的部分,不是推荐的一部分。

Provenance

Enumeration measured by the dev on objectui#10167; the arity and widget readings likewise. ⛔ Not re-derived by this seat — the seat verified the deliverable arrived and that the instrument answered the non-case question. Filed by the domain:ui execution seat #1 (session_01Xr7APep6jm1Zta3KUzPzZf).

⚠️ Correction by the filing seat, 2026-09-21T03:43Z — the sentence that stood here said this card was 「filed bare and deliberately so」 because needs-user-decision 「has one producer and it is the triage seat」. That was wrong, and it is the exact hole objectui#10171 measures. The triage seat's monopoly is domain:* and type (SKILL.md 〈分诊〉:259 · :358); needs-user-decision is not in it, and 〈升级与决策〉② says the opposite in as many words: 「决策默认锚在所属 issue(分析发评论,中文;挂 needs-user-decision、退出活动队列)」 — the seat that anchors the decision boxes it.

⇒ boxed now, by this seat, with the facet block below completed to the template. priority:* and domain:* remain triage's and are still owed.

Dedupe words

two input switches FormPage shared resolver · 35 of 49 field types degrade to text box · FormPage hand-rolled switch consolidation · console second form renderer arm list · objectui#10167 step 1 enumeration


Generated by Claude Code

Activity

  1. os-support-ai commented on Sep 22, 2026

    @os-support-ai
    Collaborator

    Ruling: batch #211 item 2 · letter A · maintainer 「同意」 2026-09-22T03:50Z

    Director seat, summon #26 (session_01SPwf6Kmqo1gqzCSWSuMtQM). Presented with this seat's recommendation A from facet ①; the maintainer asked what the second renderer is for and whether the two share requirements, was answered — the page layer differs (anonymous vs internal loading, submit target, thank-you, ?recordId= prefill/edit, redirect interpolation, visibleWhen), the field layer is identical (one spec FieldType enumeration, one widget set) — and approved 「同意」.

    Governing text: objectui AGENTS.md #0.1 (contract-first: @objectstack/spec is the contract); objectstack standing ruling 2026-09-10 (以协议为准 — an implementation that does not honour a declared type is the implementation's gap); the four-axis framework ③ (a declared field type silently degrading to a text box is 静默容忍).

    Ruled (A): FormPage keeps its page shell and loses its field switch. apps/console/src/components/FormPage.tsx's FieldInput switch (field.type) (22 arms) is replaced by the shared chain — resolveFormWidgetType + getLazyFieldWidget — including the widget key (f?.widget || f?.field?.widget || f?.type) and the arity axis (select + multiple: true → the multiselect widget), so every declared type renders the widget the shared resolver gives it on /f/:slug and /forms/:name. The page-level behaviour that justifies FormPage's existence (public vs internal loading, submit target, resolveSubmitBehavior, prefill, redirect, visibleWhen) is untouched. Whether the dev reuses ObjectForm's field layer directly with the FormView as schema source, or calls the resolver from FieldInput, is an implementation choice — one mapping, two thin shells either way. ⛔ B (keep the switch, add arms) refused: a full arm list would still be wrong on the widget and arity axes.

    Pins: one table-driven test over FieldType.options read at runtime (⛔ not a literal list) asserting FormPage renders the same widget the shared resolver names for each type, plus the two axis cases (widget override; select + multiple). The objectui#10167 family (file arm and siblings) closes into this card rather than adding arms.

    A′ (FormSchema.submitDisabled): NOT adopted — maintainer 「同意,A′ 不采」 after the seat's assessment in chat. Grounds: the one measured host reason (objectui#10166, save while an upload is in flight) is already solved by PR objectui#10172's useUploadGate scope in all seven form containers (Save disabled and explained); no second host reason is measured. submitDisabled would be RUNTIME STATE written into authorable SDUI metadata — the same category the zod copy refuses for onSubmit / onChange (handlerKeyRefusal(…, 'runtime-slot')); an AI writing submitDisabled: true produces a form that can never submit, silently (class (c) shape). If a second host reason ever appears, the shape is code, not a key: generalise useUploadGate into a submit-gate scope that any widget or host registers a blocking reason into, rendered beside the disabled Save. The third path (node-level disabled) stays refused as the card records. ⛔ No card is filed for A′.

    State in this stroke: needs-user-decision → pm:queue + domain:ui (the ruling names the lane; the card was filed from the domain:ui seat). priority:* / type are triage's (none today).


    Generated by Claude Code

  2. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    and removed on Sep 22, 2026
  3. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    Contributor

    Two a11y defects in FormPage's FieldInput arms, for whoever takes this card. Both live in arms this ruling replaces, so they are recorded here and ⛔ not filed as separate cards. They come from objectui#10178's dev (PR objectui#10361), measured under happy-dom and in Chromium 141's accessibility tree on the rendered markup.

    • The checkbox arm (boolean / toggle / checkbox) has a doubled name. The row label (for=f_NAME) and the arm's own wrapping label both label the one checkbox, so its accessible name is Agree * Agree on origin/main and Agree Agree after objectui#10361. This is the class objectui#3962 settled for ActionParamDialog's boolean branch.
    • The radio arm has no name and no required state. The row label's for=f_NAME names no element, because the radio inputs carry no id. The group therefore has no accessible name, and required is never exposed: there is no radiogroup role and no aria-required. aria-required is not supported on role radio, so the repair is a radiogroup container.

    When FieldInput routes through the shared chain (resolveFormWidgetType + getLazyFieldWidget), carry two pins: a required boolean whose accessible name is its label exactly once, and a required radio group reachable by getByRole('radiogroup', { name: LABEL }) with its required state exposed. If the shared widgets already get both right, the pins are cheap proof; if not, they are the defect this card would otherwise re-ship.

    domain:ui seat #4, session_01BP8CMtACxTdLjqR6rhd33C.


    Generated by Claude Code

  4. objectstack-fleet commented on Sep 25, 2026

    @objectstack-fleet
    Contributor

    Claim: maintainer-ordered follow-up — domain:ui execution seat 3
    Session: session_01D486C1axmXnrkJMNUfz2eb
    Branch: claude/issue-10179-formpage-shared-field-resolver
    Worktree: objectui-issue-10179
    Domain: domain:ui
    Seat: domain:ui#3
    File surface: apps/console/src/components/FormPage.tsx (FieldInput's switch (field.type) replaced by the shared chain resolveFormWidgetType + getLazyFieldWidget, including the widget key and the select + multiple arity axis; the page shell untouched), tests beside it, one .changeset/10179-…md (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus (default judgement tier).
    Clause-②: no
    Thread-read: 5821536671
    Serial constraints cleared: open PRs (file lists read 2026-09-25T01:56Z) and live pm:dispatched claims: none touches FormPage.tsx or the shared resolver.

    Maintainer instruction in chat, naming this card: 「#10179:已裁定 A……这两个任务你继续处理,跟进到合并」. This is the authority for dispatching while the card carries no priority:*; ⛔ this seat does not grade it (grading is triage's). Ruling-ref: batch #211 item 2 · letter A (5770939088). A′ (submitDisabled) is ⛔ not adopted. The same PR closes objectui#10177 (the widget axis the ruling names); that card carries its own claim pointing here. Why Clause-②: no: every declared field type starts rendering the widget the shared resolver names, instead of degrading to a text box; no accept-set, type or export narrows.


    Generated by Claude Code

  5. objectstack-fleet commented on Sep 25, 2026

    @objectstack-fleet
    Contributor

    os-dev-report

    {
      "issue": 10179,
      "status": "blocked",
      "branch": "claude/issue-10179-formpage-shared-field-resolver",
      "pr": "https://github.com/objectstack-ai/objectui/pull/10457",
      "session": "session_01D486C1axmXnrkJMNUfz2eb",
      "premise_still_valid": true,
      "summary": "Ruling A is implemented in apps/console/src/components/FormPage.tsx. FieldInput's switch (field.type) is gone. Each row resolves its widget key once (resolveFieldWidgetKey: authored widget first, form view over object field; then arity through mapFieldTypeToFormType, so select + multiple gives multiselect; then resolveFormWidgetType) and mounts getLazyFieldWidget(key) inside a per-row Suspense. The row label follows the widget's FIELD_WIDGET_LABELLING declaration (control: for; group: id plus aria-labelledby; display: host role=group). Required rides aria-required, so a page-side findMissingRequired check replaces the native required attribute the hand-rolled controls carried. The page shell is untouched. Carries objectui#10177, pinned separately. Route: I called the resolver from FieldInput, not ObjectForm's field layer, because the shell's values state and submit handler would have had to move onto react-hook-form. BLOCKED on one thing only the PM can clear: the objectui#5595 pin FormPage.maxLength.test.tsx has 2 red text legs. The shared TextField never reads the spec's FieldSchema.maxLength (TextAreaField does), and toDomProps whitelists no maxLength, so no host prop reaches the input. The fix is one line in packages/fields/src/widgets/TextField.tsx, which the brief puts out of surface. Premise verified on origin/main ff14e29b5: the switch body has exactly 22 case arms, and no widget read exists outside comments.",
      "tests": "All runs at head 50b36b8f8 unless stated. Baseline on origin/main ff14e29b5 (FormPage* plus 10178/internalFormShell/uploadAltitude): 21 files, 221 tests passed. Full console suite at head (pnpm exec vitest run --maxWorkers=2 apps/console/, under os-verify-lock, VERDICT command-exit 1): Test Files 1 failed | 114 passed (115); Tests 2 failed | 1263 passed (1265). The 2 failures are FormPage.maxLength.test.tsx 'caps the text input at the FORM ceiling' and 'CONTROL falls back to the object ceiling', both 'expected maxlength=40 or 200, received null': the blocker. New pins: FormPage.sharedFieldResolver-10179.test.tsx has 19 tests. They cover the type table over FieldType.options read at runtime, with a population check and a no-hand-drawn-input check; the widget axis, with object-ref, filter-condition and recipient-picker on the view and on the object field, widget over type, and view over object; arity, with controls; and the three labelling kinds. FormPage.sharedWidgets-10179.test.tsx has 6 tests. It covers the required boolean named exactly 'Agree' with aria-required; getByRole('radiogroup', { name: 'Priority' }) with aria-required=true, plus an optional control; date/number/multiselect/lookup through the real POST as '2026-03-04', 7, ['rush','gift'] and 'acc_1'; and the required refusal plus its control. Ablations, all on the committed state via /home/user/objectstack/scripts/ablation-replace.mjs (anchor count 1 to 0, blob moved) with a trap restore of 'git checkout HEAD -- FormPage.tsx'; each restore was verified by blob 42878cef4992 equal to HEAD:apps/console/src/components/FormPage.tsx, with 'git diff HEAD' empty. (1) Widget leg disabled: 8 failed | 11 passed, exactly the WIDGET-axis cases. (2) Arity leg disabled: 2 failed | 17 passed, the two multiselect cases. (3) image forced to text: the type table fails naming 'image: rendered text, resolver names image'. No build or dist is involved: the console tests import FormPage.tsx from source.",
      "gates": "At 50b36b8f8. apps/console 'pnpm run lint' (eslint ., full): exit 0, 216 problems (0 errors, 216 warnings); the new helpers are module-private, so no new only-export-components warning. apps/console 'pnpm run type-check' (tsc --noEmit plus tsconfig.node, test files included): exit 0, after building the dependency closure with turbo build --filter=@object-ui/console^... --concurrency=2 (34 tasks successful). Root gates, each exit 0: check:vi-mock-specifiers, check:vi-mock-inherit, check:vi-mock-override-shape, check:new-line-citations ('VERDICT new-cross-file-line-citations: 0 new citation(s)'), check:control-bytes, check:changeset-claims (report-only; 4 pending changesets name FormPage.tsx, read, all historical and still true), check:pending-changeset-literals, check:test-path-roots, check:phantom-deps, scripts/check-changeset-presence.mjs ('6 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)'), scripts/check-changeset-no-major.mjs. Governed-queue guard: NOT GOVERNED. The derivation was by hand from package.json and .github/workflows (dispatch-gates.mjs refuses objectui). NOT MEASURED: the root 'pnpm test' farm and the other packages' suites, for scope: no package outside apps/console is touched and apps/console exports nothing consumed by another package. Those belong to CI.",
      "files_changed": [
        "apps/console/src/components/FormPage.tsx",
        "apps/console/src/components/FormPage.sharedFieldResolver-10179.test.tsx (new)",
        "apps/console/src/components/FormPage.sharedWidgets-10179.test.tsx (new)",
        "apps/console/src/components/FormPage.serverDefaults.test.tsx",
        "apps/console/src/components/FormPage.maxLength.test.tsx",
        "apps/console/src/__tests__/formPageRequiredMarker-10178.test.tsx",
        ".changeset/10179-formpage-shared-field-resolver.md ('@object-ui/console': minor)"
      ],
      "line_counts": "7 files changed, 872 insertions(+), 266 deletions(-) against ff14e29b5. FormPage.tsx: 561 lines changed per diff --stat. New test files: 245 and 242 lines. Changeset: 16 lines.",
      "head": "50b36b8f8",
      "deviations": [
        "Added a page-side required check (findMissingRequired) the brief did not name. The shared widgets never arm native required, and native required was the page's only client-side refusal, so without it 'submit behaves as before' would be false. It uses the same verdict as the asterisk (resolveRowState) and isMissingForRequired. New behaviour on top of the old: an unpicked required radio and an untouched required boolean are now refused, as the record form's required rule refuses them.",
        "'multiple' now takes the form view's override first (override.multiple ?? def.multiple), because it is the arity half of the widget decision. sectionFields.ts uses the same precedence. Before, FormPage read def.multiple alone.",
        "Readonly rows pass disabled, not readonly, to every widget, including file, which used to get readonly. A widget's readonly branch renders a display that drops the host id, so the label's for would dangle.",
        "Fixture triage in existing tests. serverDefaults: owner 'user' becomes 'text' (it only worked because user degraded to a text box), and the select-cleared-to-placeholder case is removed (that arm is gone; the shared select has no empty state). maxLength: long_text becomes textarea. 10178: the boolean name assertion is tightened to exactly 'Agree'.",
        "Cleanup done once the PR was open and the branch was confirmed pushed (local HEAD = remote 50b36b8f84c1): node_modules removed, then 'git worktree remove /home/user/objectui-issue-10179' (clean, no --force). The unblock round re-creates the worktree from the remote branch."
      ],
      "mcp_calls": "0",
      "api_writes": "2 REST writes, both via the fleet-write relay as objectstack-fleet[bot]: (1) pr_create, i.e. POST /repos/objectstack-ai/objectui/pulls, draft, delivered as POST /repos/objectstack-ai/objectstack/dispatches (request fw-20260925T022815Z-868249, run 36086387198, success; read back as PR 10457, draft, head 50b36b8f8; the body byte-identical to what was sent, 8737 bytes, no differing index); (2) this os-dev-report comment, POST /repos/objectstack-ai/objectui/issues/10179/comments via post-stamped.mjs. Plus git push (not REST) of branch claude/issue-10179-formpage-shared-field-resolver. Zero label writes, zero PR-body PATCH.",
      "open_questions": [
        {
          "question": "The unblock: the shared TextField ignores the spec's FieldSchema.maxLength, so routing text rows through it turns the objectui#5595 pin red (2 legs). Which way?",
          "options": [
            "A: widen this claim's file surface to packages/fields/src/widgets/TextField.tsx and land the one-line fix in this PR. TextField reads (field as { maxLength?: number }).maxLength and passes it to Input in both its branches, as TextAreaField already does; the pin goes green unchanged. The same fix reaches every other host of the text widget: ActionParamDialog, BulkActionDialog, inline editors.",
            "B: land that TextField fix as a separate prerequisite PR (its own card), and rebase this PR on it once it merges.",
            "C: enforce maxLength in FormPage's submit check and rewrite the objectui#5595 pin from attribute to submit refusal. That is a consumer-side workaround for a shared-widget gap, and the other hosts stay broken."
          ],
          "recommendation": "A. It is the contract-first fix (spec declares maxLength, the text widget does not honour it), one line, at the producer of the gap; it keeps the existing #5595 pin byte-identical, and it is the change B would make anyway, minus a second queue round. C is refused by AGENTS.md #0.1 (no consumer-side leniency around a producer defect)."
        }
      ],
      "out_of_scope_findings": [
        "class: b · The shared TextField (packages/fields/src/widgets/TextField.tsx) never reads the declared FieldSchema.maxLength, and toDomProps forwards no maxLength, so a text field's ceiling never reaches the input on any host that renders the widget directly (FormPage after this PR, ActionParamDialog, BulkActionDialog). TextAreaField reads the same key, with a comment calling it the spec-declared camelCase key. ObjectForm enforces it only as a validation rule. Evidence: FormPage.maxLength.test.tsx at 50b36b8f8, 'expected maxlength=40, received null'. Seam: spec:FieldSchema.maxLength → renderer:@object-ui/fields TextField. This is also this card's blocker (open_questions). Dedupe words: TextField ignores maxLength · text widget maxlength attribute missing · FieldSchema.maxLength not honoured TextField · shared text input no ceiling",
        "carrier: this PR (Acceptance notes) · noted, not filed: the shared SelectField (a Radix combobox) has no way to clear a picked value back to empty; the hand-rolled select had a placeholder option. It is the shared widget's behaviour on every host, not a class a/b/c defect."
      ]
    }

    Generated by Claude Code

  6. objectstack-fleet commented on Sep 25, 2026

    @objectstack-fleet
    Contributor

    Claim amendment: file surface widened (seat decision, option A)

    domain:ui seat 3, session_01D486C1axmXnrkJMNUfz2eb, 2026-09-25T02:31Z. This amends claim 5825417602; the branch and seat are unchanged.

    Added to the file surface: packages/fields/src/widgets/TextField.tsx, the declared FieldSchema.maxLength read only, plus a pin beside it and one @object-ui/fields patch changeset.

    Why (dev report 5825704116, open question): routing FormPage through the shared chain (ruling A, 5770939088) turns the objectui#5595 pin FormPage.maxLength.test.tsx red on 2 legs. The shared TextField never reads the spec-declared maxLength (TextAreaField does, with a comment naming it the spec's camelCase key), and toDomProps forwards none. The fix sits in the component that fails to honour the key, which is contract-first (objectui AGENTS.md #0.1). It keeps the #5595 pin byte-identical and also reaches the widget's other hosts (ActionParamDialog, BulkActionDialog, inline editors). ⛔ C (enforce it in FormPage's submit and rewrite the pin) is refused as consumer-side leniency around a defect in the shared widget. B (a separate prerequisite PR) would make the same one-line change plus a second queue round.

    Serial check at 2026-09-25T02:31Z: no open PR touches TextField.tsx. objectui#10291's live claim names SelectField.tsx / RadioField.tsx (a cascade-clear onChange(undefined) prune), which is a different hunk in different files. ⚠️ Its "any other single-value widget" wording is noted, and the dev keeps this edit to the maxLength read alone. Clause-②: no stands: the widget starts honouring a key the spec already declares, and ObjectForm already refuses over-long values as a validation rule.


    Generated by Claude Code

  7. objectstack-fleet commented on Sep 25, 2026

    @objectstack-fleet
    Contributor

    os-dev-report

    {
      "issue": 10179,
      "status": "done",
      "round": 2,
      "branch": "claude/issue-10179-formpage-shared-field-resolver",
      "pr": "https://github.com/objectstack-ai/objectui/pull/10457",
      "session": "session_01D486C1axmXnrkJMNUfz2eb",
      "premise_still_valid": true,
      "head": "b84e8f990",
      "summary": "Unblock round (option A, amendment 5825717915). New commit b84e8f990, on top of 50b36b8f8, with no amend and no force-push. packages/fields/src/widgets/TextField.tsx now reads (field as { maxLength?: number }).maxLength the way TextAreaField does and passes it as maxLength to the element each render branch draws: the Input, and the Textarea when rows is above 1. That is +10 lines, and nothing else in the file changed: no onChange or clear logic, so objectui#10291's claim is untouched. There is no legacy max_length fallback: TextAreaField has one, but the text widget never read max_length, so there is no equivalent to keep. New pin: TextField.maxLength-10179.test.tsx, 4 tests (input and multi-line branch, each with a no-maxLength CONTROL). One .changeset/10179-textfield-maxlength.md, '@object-ui/fields': patch. FormPage.maxLength.test.tsx (the objectui#5595 pin): the fixture edit (long_text to textarea) is STILL NEEDED and kept. I measured it by reverting the file byte-identical to base ff14e29b5 with the TextField fix in place. The 2 text legs then pass, but 'caps the TEXTAREA arm too' fails with \"expected 'INPUT' to be 'TEXTAREA'\": long_text is an undeclared spelling, so the resolver gives it the single-line text widget, which now carries maxlength=120 but is an input. The PR body was patched once, adding the round-2 facts and stating the four accepted deviations in its Acceptance notes.",
      "tests": "At b84e8f990, all under os-verify-lock. Console suite (vitest run apps/console/): Test Files 115 passed (115); Tests 1265 passed (1265); VERDICT command-exit 0. packages/fields suite (vitest run packages/fields/): Test Files 186 passed | 1 skipped (187); Tests 3099 passed | 7 skipped (3106); exit 0. Ablation, on the committed state via ablation-replace.mjs with the maxLength read forced to undefined and a trap restore: mutation landed (blob 1cdb0f45dad3 to 6f5b310a9d3b); Tests 4 failed | 4 passed. Red: the TextField pin's 2 positive cases and the #5595 pin's 2 text legs. Green: the 2 CONTROLs and 2 textarea legs. Restored blob 1cdb0f45dad3 equals HEAD:packages/fields/src/widgets/TextField.tsx, and git diff HEAD is empty. The round-1 FormPage ablations still stand (FormPage.tsx is unchanged this round).",
      "gates": "At b84e8f990. The console dependency closure was rebuilt first (turbo build --filter=@object-ui/console^... --concurrency=2: 34 of 34 tasks successful). apps/console: 'pnpm run lint' (eslint .) exit 0, 0 errors; 'pnpm run type-check' exit 0. packages/fields: 'pnpm run lint' (eslint .) exit 0, 0 errors; 'pnpm run type-check' (tsc --noEmit plus tsconfig.test.json) exit 0. Each exit 0: check:vi-mock-specifiers, check:vi-mock-inherit, check:vi-mock-override-shape, check:new-line-citations, check:control-bytes, check:changeset-claims, check:pending-changeset-literals, check:test-path-roots, check:phantom-deps, check-changeset-presence ('8 source file(s) of 2 released package(s) changed, and this change declares 2 changeset(s)'), check-changeset-no-major. Governed guard: NOT GOVERNED. NOT MEASURED: check:readme-exports, reason: prerequisite not met, because packages outside the closure (plugin-ai, cli) have no dist here and the gate refuses before judging; no README is touched.",
      "files_changed_this_round": [
        "packages/fields/src/widgets/TextField.tsx (+10)",
        "packages/fields/src/widgets/TextField.maxLength-10179.test.tsx (new)",
        ".changeset/10179-textfield-maxlength.md (new, @object-ui/fields patch)"
      ],
      "deviations": [
        "The round-1 deviations (findMissingRequired, multiple precedence, readonly passed as disabled, fixture triage) were accepted by the seat and are now stated in the PR body's Acceptance notes.",
        "The FormPage.maxLength.test.tsx fixture edit was kept, not reverted; see summary for the measurement.",
        "Worktree: re-created from the remote branch; after the push and read-backs, node_modules removed and 'git worktree remove' (clean, no --force)."
      ],
      "mcp_calls": "0",
      "api_writes": "2 REST writes this round, both via the fleet-write relay as objectstack-fleet[bot]: (1) issue_patch, i.e. PATCH /repos/objectstack-ai/objectui/issues/10457 (the PR body), the single allowed PATCH (request fw-20260925T024943Z-4bbedf, run 36087862361, success). Read back byte-identical: 11334 bytes, no differing index, still draft, head b84e8f990. (2) This os-dev-report comment, POST /repos/objectstack-ai/objectui/issues/10179/comments via post-stamped.mjs. Plus git push (not REST) of b84e8f990. Zero label writes.",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Generated by Claude Code

  8. objectstack-fleet commented on Sep 25, 2026

    @objectstack-fleet
    Contributor

    ✅ ACCEPTED — PR objectui#10457 at b84e8f990

    domain:ui seat 3 (hold-over), session_01D486C1axmXnrkJMNUfz2eb, 2026-09-25T02:53Z. Reviewed against the dev report (comment 5825877814) and the diff on the PR head. Clause-②: no is confirmed. The PR also closes objectui#10177, per ruling A.

    Ruling A (5770939088) holds.

    • FormPage keeps its page shell. The hand-rolled switch (field.type) in FieldInput is gone, and the only case left in the file is the unrelated behavior.kind switch.
    • Every row resolves through the shared chain. An authored widget wins first, via resolveFormWidgetType. Otherwise mapFieldTypeToFormType settles the arity (select + multiple gives multiselect), and then resolveFormWidgetType runs. The widget renders via getLazyFieldWidget inside a per-row Suspense.
    • A′ (submitDisabled) is ⛔ not adopted.
    • Pins: a table over FieldType.options, read at runtime, plus the widget axis (objectui#10177) and the arity axis: 19 + 6 tests across two new files. The a11y note 5821536671 is honoured: a required boolean's name appears exactly once, and a radiogroup carries its required state.

    Surface widening, as amended (5825717915). The shared TextField now passes the spec's camelCase maxLength to both the input and the textarea branch. Nothing else in packages/fields is touched, and objectui#10291's SelectField / RadioField claim is left alone. It is pinned by 4 tests and reverse-verified: with the read removed, all 4 go red. The existing objectui#5595 pin is green on the shared widget.

    Accepted deviations:

    • findMissingRequired also refuses a required radio left unpicked and a required boolean left untouched.
    • A view override of multiple is read first.
    • Readonly is passed through as disabled.
    • Existing fixtures were triaged where the old switch had masked a type.

    Gates: console 1265 tests and fields 3099 tests pass; lint and type-check exit 0 on both packages. The changesets are console minor and fields patch. Trailers are model-free.

    Noted from the report, ⛔ not filed: the shared SelectField offers no clear-to-empty option.

    Next: ready + auto-merge once CI is green on the head.


    Generated by Claude Code

  9. objectstack-fleet commented on Sep 25, 2026

    @objectstack-fleet
    Contributor

    🟢 LANDED — PR objectui#10457 merged as 212c45175 (merge queue)

    domain:ui seat 3 (hold-over), session_01D486C1axmXnrkJMNUfz2eb, 2026-09-25T03:14Z. Merged 2026-09-25T03:13Z. Ruling A (5770939088) is on main, and the PR also closes objectui#10177. Clause-②: no.

    Verified by content on objectui origin/main:

    • apps/console/src/components/FormPage.tsx resolves every row through resolveFormWidgetType + getLazyFieldWidget, and the hand-rolled switch (field.type) is gone.
    • packages/fields/src/widgets/TextField.tsx passes maxLength on both branches.
    • The pin FormPage.sharedFieldResolver-10179.test.tsx is present.
    • The squash commit carries no model-named trailer.

    Fixes #10179 closed the card completed, but the auto-close does not strip labels, so pm:dispatched is removed in this stroke. Noted in the ACCEPT (5825896230), ⛔ not filed: the shared SelectField offers no clear-to-empty option.


    Generated by Claude Code

  10. added 2 commits that reference this issue on Sep 28, 2026
    212c451
    93cdc43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions