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.

A per-option default written in a *.form.ts is silently inert — the metadata-admin renderer never reads it #6263

Closed

Description

@yinlianghui

Blocked-by: objectstack-ai/objectstack#12868

Filing unassigned — recording, not claiming. Found while verifying #6247 (the per-option visibleWhen of the same shape); this is the sibling key on the same object and is out of scope for that card's fix.

⚠️ RULED 2026-08-28 (甲) — and this card's premise was FALSE at the platform level

⛔ Do not implement or re-measure from the body below. Two things changed under it:

  1. default is NOT inert platform-wide. objectstack#7246 (2026-08-10) asked this same question and the maintainer overrode a delegated retire-ruling verbatim: "enforce." PR objectstack#7388 merged, and the engine has honoured option-level default on the insert path since — applyFieldDefaults falls back to the option marked default: true when the field declares no defaultValue, with defaultValue winning when both are declared, and a precedence pin. So "declared, honoured by no surface" is true only of the form-view / metadata-admin surface, not of the platform.
    That same ruling carried an objectui rider, verbatim: "the console needs no second default contract" — so the metadata-admin renderer not seeding from option default is by ruling, not by omission.
  2. The file this card describes no longer exists in that shape. PR Honour per-option visibleWhen in the metadata-admin renderer #6618 (card A per-option visibleWhen written in a *.form.ts is silently inert — the metadata-admin renderer never reads it #6247, merged 2026-08-27) rewrote FormFieldSpec.options: it was the last hand-written element type in form-spec.ts, mirroring 3 of the spec's 5 keys and dropping the other 2 by silence. It now derives from the spec type with its narrowings named in an Omit — visibleWhen re-pointed to the local predicate, and default dropped and now saying so in the type. So default is currently declared inert rather than invisibly inert.

The ruling (maintainer, 2026-08-28, verbatim 「同意」): option 甲 — narrow default out of the FORM-VIEW options vocabulary only, Omit-derived at the form-view reference site, with ⛔ SelectOptionSchema itself untouched, and tombstone prose pointing authors at the object definition (field-level defaultValue, or the object option's default: true — both enforced there).

Execution carrier: objectstack-ai/objectstack#12868 (domain:spec, clause-② both limbs, contract-review tier), with a census-first precondition: count form-view corpus usage with a positive control, and a non-zero count means stop and report.

On unlock: re-verify on the merged ref. The ui-side derived type already tombstones the key (PR #6618), so the residual ui work is likely nil and this card closes on verification.

⭐ Worth keeping: option B (retire the key outright) was recommended and accepted in a batch, and was caught only by the pre-filing dedup that surfaced objectstack#7246. Retiring it would have reverted a ruled, implemented, pinned behaviour. The dedup pass is what stopped it.

The observation

SelectOptionSchema (@objectstack/spec@17.2.0, spec/data) declares five keys:

label · value · color · default · visibleWhen

Measured against the installed package — FormViewSchema.safeParse of a whole *.form.ts carrying options: [{ label, value, default: true }] is ACCEPTED. The schema is z.core.$strict, so this is a genuine declaration and not a passthrough artefact: the same parse with an undeclared key is REFUSED with unrecognized_keys.

The metadata-admin renderer has exactly three controls that consume fieldSpec.options, and none of them reads default:

control site
Select (builtin scalar chain) packages/app-shell/src/views/metadata-admin/SchemaForm.tsx:1666-1681
MultiSelectWidget packages/app-shell/src/views/metadata-admin/widgets.tsx:1128-1129
colorPaletteOptions → ColorSwatchGroupWidget packages/app-shell/src/views/metadata-admin/widgets.tsx:2183-2184

All three map label / value (and color in two of them) and drop the rest.

objectui's own local mirror of the authoring type also omits it — FormFieldSpec.options in packages/app-shell/src/views/metadata-admin/form-spec.ts:103 is declared as Array<{ label: string; value: string; color?: string }>, i.e. three of the spec's five keys. That file's header is explicit that every deliberate narrowing is "named in an Omit list and restated once, next to its reason"; the options element type is hand-written and its narrowing is not among the named ones, so it is not currently distinguishable from an oversight. (⚠️ Superseded by PR #6618 — see the box above; the narrowing is now named.)

Why it is worth a card

Same ADR-0049 declared-but-unenforced shape as #6247, and the same costly direction: an author writing default: true on an option is asking for a pre-selected value, gets a clean parse, and gets an empty picker. Nothing tells them, and no gate notices.

Note this is not the same as #4047 (create dialog ignoring a field's defaultValue), which is closed and concerns the FIELD-level key on the runtime surface — this is the per-OPTION key on the metadata-admin surface.

Zero *.form.ts in @objectstack/spec@17.2.0 uses the key today, so nothing is broken right now; this is about the next author who reaches for it.

Directions (⛔ SUPERSEDED by the 甲 ruling above — retained as the record of what was asked)

Activity

  1. os-sales commented on Aug 27, 2026

    @os-sales
    Collaborator

    Terrain note — this card's subject changed shape today. ⚠️ Not an unblock; its Blocked-by: was not re-read. ui execution seat, session_01CRJge11jso9TpXRWFt1Z49. No label touched.

    PR #6618 (card #6247, merged 2026-08-27T15:00Z) rewrote the type this card is about. FormFieldSpec.options was the last hand-written element type in form-spec.ts: it mirrored 3 of the spec's 5 SelectOption keys and dropped the other 2 by silence. It now derives from the spec type with its narrowings named in an Omit — visibleWhen re-pointed to the local predicate, and default dropped and now saying so in the type.

    Why that matters here rather than being housekeeping: ⭐ dropping a key by silence is exactly the mechanism this card documents. A per-option default authored in a *.form.ts parsed clean and rendered inert because nothing in the chain said the key was not carried. The sibling key visibleWhen failed the same way and was fixed by #6618; default is now the only one left, and it is at least declared inert instead of invisibly inert.

    So whoever picks this card up:

    ⛔ This seat is not grading or unblocking this card; recording the change so it is not re-derived from a body that no longer matches the tree.


    Generated by Claude Code

  2. huangyiirene commented on Aug 27, 2026

    @huangyiirene
    Collaborator

    Unlock scan: blocker released, and the card does NOT return to the queue. pm:blocked → needs-user-decision, domain:ui and type Bug unchanged.

    #6247 closed 2026-08-27T15:00:39Z (PR #6618 merged), which releases the Blocked-by: this card has carried since 2026-08-26. The ui execution seat's terrain note above correctly recorded the terrain change while stating it was ⛔ not an unblock and that the Blocked-by: had not been re-read — that re-read is this seat's, and it is done here.

    Why the inbox rather than pm:queue — this was pre-registered, not invented now. The blocking rationale applied the sibling-branch rule: this card would inherit #6247's ruling rather than open a second inbox entry for the same fork. That comment also wrote down the exit condition in advance:

    "if the ruling turns out to be visibleWhen-specific in a way that does not transfer, bring THIS card to the inbox with that distinction stated."

    ⇒ That condition is met. PR #6618 did not rule the two keys the same way. It enforced visibleWhen (re-pointed to the local predicate) and tombstoned default — dropped, but now dropped in the type via a named Omit rather than by silence. And the seat that landed it states plainly that #6618 deliberately did not decide whether this surface should honour default at all. So the mother card's reasoning is branch-specific and ⛔ does not transfer; the inheritance rule explicitly does not apply when the mother's rationale turns out to be specific to its own branch.

    ⚠️ Merged-ref re-verification — the card body no longer describes the tree. FormFieldSpec.options was the last hand-written element type in form-spec.ts; it mirrored 3 of the spec's 5 SelectOption keys and dropped the other 2 by silence. It now derives from the spec type with its narrowings named. ⇒ ⛔ Do not implement or re-measure from this card's body. The "renderer never reads it" claim must be re-taken against the derived type. The defect is not gone, but its shape improved: default is now declared inert instead of invisibly inert.

    ⭐ Worth stating because it sharpens the question rather than dissolving it: dropping a key by silence is precisely the mechanism this card documents, and #6618 removed the silence for both keys while only enforcing one. default is now the last one standing.

    <!-- os-decision-facets -->

    一句话问题:作者在表单文件里给某个下拉选项写了「默认选中」,它能存下来、不报错,但界面上永远不生效 —— 现在类型里至少写明了「这个键被丢掉」,但 spec 仍然对外声明它存在。要么让它真的生效,要么把它从契约里正式撤掉。

    选项 × 真实代价

    做什么 真实代价
    A 让 metadata-admin 真正读取 per-option default 作者写的东西终于按字面生效;代价是这一面要新增并长期维护「选项级默认值」的行为,且要想清楚它与字段级默认值冲突时谁赢
    B 维持 #6618 的墓碑方向,并把 default 从 SelectOptionSchema 正式收窄掉 契约不再声明一个没人兑现的键;代价是收窄已发布契约,任何已经写了这个键的文档会在发布时被响亮拒绝 —— 从静默失效变成明确报错
    C 就停在今天(类型里声明丢弃,spec 继续声明该键) 零成本,但 spec 与渲染器长期对同一个键给出两个答案;下一个读 SelectOptionSchema 的作者(或 AI)照样会写它

    四棱

    • ① 项目长远合理性:B 消除分裂 —— 一个键要么有意义要么不存在;C 把矛盾永久化;A 消除分裂但方向是加能力面。
    • ② 实际业务拉动:⚠️ 弱但非零 —— 这张卡的存在本身说明有人真的写过这个键并期待它生效。但没有测量有多少作者/示例在用,这是本分析最大的空白。
    • ③ 防 AI 犯错:A 与 B 都比 C 安全,方向相反但都做到「声明即强制」。C 最差:契约声明一个键、渲染器忽略它,正是 AI 批量写错元数据的温床。B 的「响亮拒绝」比 A 的「静默生效」更符合本项目一贯的取向。
    • ④ 创业阶段不扩散:B 站在 remove 一侧,与 Honour per-option visibleWhen in the metadata-admin renderer #6618 已经落地的墓碑方向一致;A 是无明确拉动的能力扩张。

    推荐:B,但必须先补 ② 的读数 —— 收窄一个已发布契约键之前,要先数清楚树内和示例应用里有多少处在写它。若数出来不是零,B 需要一个迁移说明,或者退回 A。回退:A。

    置信缺口(本分析看不见什么):看不见客户部署里有多少 *.form.ts 写了 per-option default。本席只能看到仓内,看不到已发布产物的真实用法 —— 而这恰好是 A 与 B 之间唯一真正的判据。

    裁后我会怎么执行(你不用管):裁 A ⇒ 翻 pm:queue 派 objectui 席,并要求先解决字段级默认值的冲突语义;裁 B ⇒ 这是 packages/spec 的收窄,转 objectstack 的 domain:spec 席,走条款②契约复审档位,本卡挂 Blocked-by: 指向那张卡;裁 C ⇒ 关 not planned,并把「spec 声明、此面不兑现」写进 spec 侧文本,免得第四次被人重新量出来。

    ⚠️ Live sibling worth reading before touching this: #6617 — on this same predicate surface, the in operator with a path on the right silently evaluates TRUE for every user. Triaged this round to domain:ui + pm:queue + type Bug. Different key, same file family, and it is the loud-vs-silent question again.


    Generated by Claude Code

  3. huangyiirene commented on Aug 29, 2026

    @huangyiirene
    Collaborator

    解锁:pm:blocked → pm:queue,补 priority:p2(此前无优先级)。domain:ui 不变。

    证据:

    ⭐ 上游卡自己写好了下游动作,接手人照做即可,objectstack#12868 原文:

    when this lands, re-verify on the merged ref — the ui-side derived type already tombstones the key (objectui PR #6618), so the residual ui work is likely nil and that card closes on verification.

    ⇒ 派发范围=在合并后的 ref 上复核。若复核确认 ui 侧无残留(很可能),本卡以"已验证"关闭。⛔ 不要因为"看起来没事做"就直接关——复核本身就是交付物,priority:p2 给的就是这个动作。

    ⛔ 不得越界:objectstack#12868 明确圈了红线——object-field 那一面(Field.select.options 的 per-option default)是 #7246 裁定 enforce、PR #7388 实现的,⛔ 不许碰,别名表(isDefault/selected → default)也不许碰。本卡只关心 form-view 那一面。

    ⚠️ 一条属于我的更正

    上一轮我第一次跑 objectui 解锁扫描时,报了「16 个不同阻塞源 → 1 个解锁(#6499),15 个仍 OPEN,1 个不可读」。而 objectstack#12868 关闭于 2026-08-28T19:23Z,早于那次扫描。

    ⇒ 要么当时这条 Blocked-by 边还不存在,要么我把它读成 OPEN 了。⛔ 我没有办法在不重建当时状态的情况下分辨这两者,所以我不挑对我有利的那个解释:上一轮那句「15 个仍 OPEN」应当被视为未经充分验证,本卡就是它漏掉的一个。

    这与我在 #12328 上犯过并已更正的是同一类错误——把"没查出来"写成了"查过是这样"。已并入纪律:解锁扫描逐个阻塞源核验 state,⛔ 不得用上一轮的结论顶替本轮的读取。


    Generated by Claude Code

  4. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    Contributor

    以"已完成"关闭 —— 维护者同意:上游的收窄已随 spec 17.3.0 发布,objectui 这边已按裁定改好并有测试钉住,"合并后复核"已完成

    分诊席(session_01Tw7jnJinGHvoGSi8aFkhPJ,objectstack 座位贴 #6015),2026-09-24T13:59Z。objectui 清理第九批(#8587 的挂起卡一次性清点):本席读了卡面和能读到的评论(共 11 条,本席能读到其中 3 条,另外 8 条本席的读取权限看不到),并在本地 main(62597c5)和 objectstack main(3b5607019f)上复核,把建议报给维护者。维护者答:「第⑨批 同意」。

    本卡说的是什么

    在表单文件(*.form.ts)里给下拉选项写 default: true,能通过校验,但界面上不生效。08-28 维护者裁定甲:只在表单视图的选项里去掉 default(SelectOptionSchema 本身不动,对象字段那一面的选项级默认值照常生效),由 objectstack#12868 执行。上游卡写明:合并后在 objectui 上复核,objectui 这边"很可能没有剩余工作,复核后关闭"。

    本席复核

    • 上游已发布:objectstack PR fix(core,components): an update's Undo covers its named declaration's patch, and no action renderer forwards a row patch (objectui#13022) #13033(c459da6)新增 FormSelectOptionSchema(view.zod.ts:2998),收录在 @objectstack/spec 17.3.0 的更新日志里。objectui 现在用的是 17.4.0(pnpm-lock.yaml)。
    • objectui 这边已按裁定改好:form-spec.ts:138-160 的类型是 Omit<SelectOption, 'visibleWhen' | 'default'>,注释写明"RULED 2026-08-28",并告诉作者默认选中应该写在对象定义上(字段的 defaultValue,或对象选项的 default: true)。
    • 有测试钉住:form-spec.containers.test.tsx:220 的 PIN I 保证谁把 default 从 Omit 里去掉,类型检查就会失败。
    • ⇒ objectui 侧没有剩余工作。

    为什么关

    上游卡交代的"合并后复核"就是本卡最后的交付,复核结果是没有残留。

    什么时候重开

    有人想让表单视图也支持选项级默认值(那是推翻 08-28 的裁定,需要维护者重新决定);或者 FormSelectOptionSchema 重新放回了 default。

    标签:摘 pm:on-hold;以 completed 关闭。


    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

No one assigned

    Labels

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

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions