Uh oh!
There was an error while loading. Please reload this page.
fix(app-shell): datetime action 参数提交带显式时区的 ISO instant - #3930
Merged
Conversation
An action declaring a `type: 'datetime'` param was unusable from the Console: no value a user could pick could pass validation. The dialog renders the param as `datetime-local` (zone-less by nature) and `serializeParamValues` then converted the value back to that control's naive wall clock — the one shape the platform's `datetime` value contract rejects. Since 17.0 the dispatcher validates a params bag against the action's declaration before the handler runs (ADR-0104 D2, `validateActionParams` -> `InstantValueSchema`), so every UI submission earned a 400: "expected an ISO-8601 instant with explicit zone". Renderer and validator wanted disjoint shapes and the app author had no seam between them. The fix is a removal, not a conversion. `DateTimeField` has been ISO-canonical on both sides since objectui#3127/#3565 — it takes the record's ISO instant in and hands an ISO instant back out, seconds, milliseconds and zone included — so the widget's value already satisfies the contract. #3565 added the back-conversion to keep the wire shape byte-identical while fixing a display bug and named the follow-up in its own commit message: moving action params onto ISO is a contract change of its own. This is that change. Dropping the branch also makes the boundary idempotent for an already-zoned value (a `+08:00` offset survives byte-for-byte instead of being re-derived and re-cut to the minute) and leaves an empty or unfilled param alone. Deliberately NOT normalized: an authored `defaultValue` written as a zone-less wall clock. That is ambiguous metadata (whose zone?), and coercing it in the renderer would make it "work" in the UI while the identical literal kept 400ing from REST/MCP. It stays loud until the spec validates a param default against the param's own value contract — filed as objectstack#6970, which is not datetime-specific (the same hole lets a `number` param default to 'abc'). The render proof that pinned the old shape was replaced rather than re-spelled: it asserted `2026-07-20T14:30` and was green while the feature was 100% broken, because the shape it pinned is the one shape nothing accepts. It now drives the real widget and asks the real `validateActionParams` — the exact function that produced the 400 — whether the resolved bag is acceptable. Reverse-verified: restoring the deleted branch turns three of the new assertions red with that same 400 message. The datetime assertions hold in every timezone (run under Asia/Shanghai, UTC and America/Los_Angeles), since a zone-shaped test that only holds in UTC goes green on CI while the defect is live for every user east or west of it. The `paramValueShape` sample row is re-spelled but noted as documentation, not a guard: both the old and new strings classify to the same `'string'` tag, so that row could not have caught this. Fixesobjectstack-ai/objectstack#5061 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
…by spec's sentence The "zone-less authored default is still rejected" case pinned `validateActionParams`' full message string, which is owned by `@objectstack/spec` and reachable here only through a versioned dependency. A reword upstream would turn this repo's CI red for a reason that has nothing to do with the renderer. Assert the verdict structurally (`param` + `code`) plus the one phrase that identifies the rule, so the test still says which contract rejected the value without locking a cross-repo string. Refs objectstack-ai/objectstack#5061 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
yinlianghui
commented
Aug 9, 2026
CollaboratorAuthor
✅ 验收通过(objectui 分片 PM,session 实物核验:head 验收要点:
范围外 finding objectstack#6970 立单规范(domain:spec 路由),冻结期不派,归 spec 车道分诊。 Generated by Claude Code |
yinlianghui
marked this pull request as ready for review
August 9, 2026 07:01
Uh oh!
There was an error while loading. Please reload this page.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixesobjectstack-ai/objectstack#5061
问题
声明
type: 'datetime'参数的 action 在 Console 上完全不可用 —— 没有任何用户能输入的值可以通过校验。对话框把参数渲染成
datetime-local(天然无时区),serializeParamValues在提交前又把值转回该控件自己的无时区墙上时钟(如2026-08-10T15:00)—— 而这恰好是平台datetime值契约唯一拒绝的形状。自 17.0 起 dispatcher 会在 handler 之前按 action 声明校验 params bag(ADR-0104 D2,validateActionParams走InstantValueSchema),于是每一次 UI 提交都拿到:渲染器与校验器要的形状不相交,app 作者在两者之间没有任何可作用的缝隙。
修法:是删除,不是新增转换
DateTimeField自 objectui#3127 / objectui#3565 起就是两向 ISO 规范的 —— 读入记录的 ISO instant,交回 ISO instant(秒、毫秒、时区都在)。也就是说 widget 交给对话框的值本来就满足契约,提交边界不需要任何转换。objectui#3565 当时为了在修显示 bug 时保持线上形状逐字节不变而加了这段回转,并在自己的 commit message 里点名了后续:「moving action params onto ISO is a contract change of its own」。本 PR 就是那个 contract change,它只是把回转删掉。
顺带得到的性质:
+08:00偏移逐字节存活,不会被重新推导后再截到分钟;toISOString()天然补全;date参数不受影响 —— 它的契约是日历日,加时区反而会让格林尼治以西整体错一天。刻意不兜的一种形状
作者手写成无时区墙上时钟的
defaultValue。这是歧义元数据(按谁的时区?),在渲染器里归一化会让同一份元数据对不同用户产生不同 instant;更糟的是造成分裂 —— UI 里「能用了」,而 REST / MCP 提交同一个字面量依旧 400,这是最难排查的一种不一致。按 contract-first(AGENTS.md #0.1)正确的位置是生产端在授权期被拒绝,已另开 objectstack-ai/objectstack#6970(该洞不限于 datetime:同样允许number参数默认值写成'abc')。本 PR 用一条测试把「渲染器不掩盖它」钉住。测试
原来那条 render proof 是整条替换而不是改写字面量的:它断言产出等于
2026-07-20T14:30,一直是绿的,而功能 100% 坏着 —— 因为它钉的形状正是无人接受的那一个。现在它驱动真实 widget,然后把对话框 resolve 出来的 bag 交给真的validateActionParams(即产生 issue 里那条 400 的同一个函数)判定。同时在packages/fields补了生产端的钉子:fromDateTimeInputValue的产出必须被InstantValueSchema接受。paramValueShape.test.ts的样例值改了,但注释里说明它是文档而非守卫 —— 新旧字符串都归类成同一个'string'tag,那一行本来就抓不到这个缺陷。反向验证(方向为事先预测的「红」):把删掉的回转分支放回去,三条新断言转红,报错文案与 issue 里的 400 逐字一致:
时区形状的断言全部写成任意时区成立(仓库既有约定,见
nativeDateValue.test.ts的说明:只在 UTC 下成立的测试会在 CI 绿着,而缺陷对每一个东西向用户都活着)。三个时区各跑一遍:TZ=Asia/Shanghai vitest run …/ActionParamDialog.test.tsx …/nativeDateValue.test.tsTZ=UTC同上TZ=America/Los_Angeles同上TZ=Asia/Shanghai vitest run packages/app-shell/src/utils/TZ=UTC vitest run packages/app-shell/src/utils/ packages/fields/src/widgets/turbo run type-check --concurrency=2pnpm run check:control-bytespnpm run changeset:checkeslint(改动文件)ADR-0059 两向约定核对
按任务要求核对了 objectui#3127 的反方向(其修复已随 objectui#3565 落地),未把它折叠进本单:
本 PR 之前,action 参数是唯一跑在另一个基准上的表面;之后两向都以 ISO instant 为规范形式,本地墙上时钟只存在于控件内部。这是让 ADR-0059 更一致,而不是引入新分歧。
变更文件
packages/app-shell/src/views/ActionParamDialog.tsx—— 删掉 datetime 回转分支与toDateTimeInputValue导入,注释改写为记录真实契约与「为什么不兜 defaultValue」packages/app-shell/src/utils/paramValueShape.ts——datetime的契约说明改为带显式时区的 ISO instantpackages/fields/src/index.tsx—— 导出注释里已过时的理由(原文写着「app-shell 的契约是无时区墙上时钟」)packages/app-shell/README.md、docs/adr/0059-action-params-shared-field-widgets.md—— 契约表ActionParamDialog.test.tsx(替换 + 新增)、paramValueShape.test.ts(样例)、packages/fields/src/widgets/nativeDateValue.test.ts(生产端钉子).changeset/action-param-datetime-zoned-instant-os5061.md—— patch@object-ui/app-shell🤖 Generated with Claude Code
https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
Generated by Claude Code