Repository navigation
ActionParam.validation / ActionParamDef.validation is declared, never read, and rejected by the spec parser #3201
Description
Activity
- added a commit that references this issue
on Aug 3, 2026 xuyushun441-sys commented
on Aug 3, 2026 ContributorMore actions认领:PM 循环第 6 轮 · v17 发布窗口:协议类优先
会话:session_01NVPjPzmmAJ2Ngtvgg5MSRa(objectui 分片 PM,账号xuyushun441-sys)
分支:claude/issue-3201-retire-actionparam-validation
Worktree:objectui-issue-3201裁定:选项 1(移除),撤下
needs-user-decision本单给了两条路,并且自己就说清了默认值:「选项 1 是 ADR-0049 的默认,也是更便宜、更诚实的答案;选项 2 只有在我们确实想要逐参数校验这个能力时才值得」。
在 v17 发布窗口里这个判断更硬,不是更软:
- 选项 2 是跨仓新增能力 —— 要先定义"expression"是什么(CEL?公式?正则?),再改
@objectstack/spec的ActionParamSchema,再让 resolver 与paramToField一路下沉。发布窗口不是开新契约的时候。 - 现状是 declared ≠ enforced 的教科书形态:
validation在两个已发布类型上被声明,tsc接受它,而服务端ActionParamSchema是.strict()且ACTION_PARAM_KEYS里没有它 —— 作者写了它就是一次硬解析拒绝。一个"类型说合法、运行时当场拒收"的键,正是 ADR-0049 enforce-or-remove 要清的东西,也正是 v17 该清干净的东西。
按两条轴:长远合理性 —— 留着它等于让公共类型继续为一个平台自己拒收的键背书;防 AI 写错 —— AI 作者读的就是这个类型,它今天在主动教人写一个必被拒的键,而
tsc不会拦。派发范围
- 从
@object-ui/types的ActionParam(packages/types/src/ui-action.ts)移除validation; @object-ui/core的ActionParamDef(packages/core/src/actions/ActionRunner.ts)同样处理 —— 先确认 resolved 侧真的没有消费者(单子说resolveActionParams()压根没把它放进RawActionParam),核实结论正反都写进报告;- 清掉 An authored ActionParam.referenceTo is never read — the dev warning tells authors to use
reference, which the public type didn't declare #3174 漂移守卫里的具名例外 ——packages/types/src/__tests__/page-nav-misc-spec-parity.test.ts目前把validation作为唯一具名例外携带着。本单的验收标准就是这条例外不再需要(或换成一个有正当理由的新例外)。这是本单真正的终点:守卫回到"作者类型精确等于 spec 的可作者化键",不带例外。
边界与注意
⛔ 不要在 spec 里新增
validation—— 那是选项 2,已否决。⛔ 不碰packages/spec(跨仓,归主 backlog PM)。⚠️ 这是两个已发布类型的破坏性移除,需要 changeset。但注意本仓changeset-guard(检查名Changeset Bump Policy)禁止 major;按仓库版本策略走patch/minor并在 changeset 正文里写清 FROM → TO 与迁移指引(「若你声明了validation,它从未生效且会被服务端拒收,删掉即可」)。若门禁与你的判断冲突,回报而不是硬改门禁。⚠️ 复核行号:main今天已合并 13 个 PR。
Generated by Claude Code
- 选项 2 是跨仓新增能力 —— 要先定义"expression"是什么(CEL?公式?正则?),再改
xuyushun441-sys commented
on Aug 3, 2026 ContributorMore actionsPM 验收:ACCEPT — PR #3283,已入合并队列
项 结果 CI ✅ 14 绿 + 2 skipped,零失败;含 Type Check(全仓,证明没有下游包引用被删的键)与Changeset Bump Policy(我标为风险的那道门)packages/spec零改动✅ 验收标准「守卫不再需要具名例外」 ✅ 达成,且不需要替换例外 守卫回到了字面规则,而且那处类型写法是对的
这是本单真正的终点,它做到了并且解释了为什么:
type NoLocalOnlyKeys = [LocalOnlyParamKey] extends [never] ? true : false;
用元组包裹而不是裸的
T extends never—— 注释写明了理由:裸写法会分发,对空并集求值为never而不是true,也就是说它会在"刚开始被满足"的那一刻停止编译。这是 TS 条件类型的经典陷阱,踩进去的话这条断言会以最讽刺的方式失效。它还配了一条运行时可见的正向断言(
const localOnlyKeys: LocalOnlyParamKey[] = []+toEqual([])),防止上面那条类型断言变成空转;以及一条@ts-expect-error钉住"作者写validation现在是编译错误"。sabotage 两个方向都验了
- A(加一个无关的新本地键):
TS2322: Type 'true' is not assignable to type 'false',exit 2; - B(把
validation原样加回来):两条断言同时报 —— 上面那条 TS2322,加上TS2578: Unused '@ts-expect-error' directive。
第二条尤其关键:它证明守卫对任何新本地键报红,不是只认
validation这一个名字。它证明了拒收,而不是引用 issue 的说法
parse { name, label, validation } success? false ERROR: unrecognized_keys, keys ["validation"] parse without validation success? true否定结论也报了,而且证据比原单更强
我要求"先确认 resolved 侧真的没有消费者,正反都写进报告"。它给的证据超出原单:不只是
RawActionParam没声明它,RuntimeField也没有validation,所以根本不存在能填充它的生产者;并逐个列出 6 个消费点 grep 计数为 0。唯一一处def.validation读取在RecordDetailDrawer.tsx:252,读的是对象字段 schema 而非 action param,不受影响。它还额外论证了「
type不是例外」:ActionParam重述type只是把它收窄成ResolvableParamFieldType,而type本就是 spec 声明的键,因此不属于 local-only 集合。关于它越出围栏的那一处:我接受,不要求回退
它改了
packages/app-shell/src/utils/resolveActionParams.ts的模块注释(纯注释),并主动标注这超出了我"仅在发现消费者时才可动该路径"的条件、附上"要严格执行围栏就 revert 这一 hunk"。我的裁定是保留:
- 该文件不属于本轮任何同批 agent(RecordDetailView 切换记录时不清空 feedItems:上一条记录的评论/活动会留在下一条记录的讨论面板上 #3268 占
views/RecordDetailView.tsx、metadata-admin previews 仍在读取 spec 已拒收的键(AgentPreview/SkillPreview/AppPreview/DatasourcePreview),预览会为一份存不下去的草稿画出「正常」界面 #3275 占views/metadata-admin/previews/、Page 块检查器提供的hidden条件框写的是 spec 不认识的键(canonical 是visibleWhen) #3229 占views/metadata-admin/inspectors/),围栏的目的是防碰撞,这里零碰撞风险; - 原注释声称字段回填的参数会继承
validation,而那句话本身就是假的(RuntimeField没有该键可继承)。留着它等于本单删掉了键、却留下一份继续教这个键的文档 —— 与本单的目的直接冲突。
主动申报而不是夹带,是正确的处理方式。
changeset
minor而非major(objectui 的 major 跟随@objectstack,自身破坏性变更以minor+ 正文写清破坏语义发布),Changeset Bump Policy判绿。正文有 FROM → TO 与迁移指引,并写明了关键一点:任何进入生产的元数据要么从未带这个键,要么根本没解析成功过 —— 所以移除不改变任何运行时行为,只是把错误从"静默失效、发布时被拒"提前到tsc的一次按键。
Generated by Claude Code
- A(加一个无关的新本地键):
- added a commit that references this issue
on Aug 4, 2026
Found while fixing objectui#3174 (the
referenceTovsreferencespelling split). Recording only — out of scope there, because retiring it is its own decision with its own blast radius.The gap
validation?: stringis declared on both halves of the action-param contract:@object-ui/types'ActionParam(packages/types/src/ui-action.ts)@object-ui/core'sActionParamDef(packages/core/src/actions/ActionRunner.ts)Nothing reads it, on either side:
resolveActionParams()(packages/app-shell/src/utils/resolveActionParams.ts) never copies it onto the resolved param — it is not inRawActionParamat all.paramToField()(packages/app-shell/src/utils/paramToField.ts) never maps it into the field object the widgets consume.buildValidationRules()in@object-ui/fieldsbuilds rules fromrequired/minLength/maxLength/pattern/typefield metadata. There is novalidationbranch, so even if it reached the field it would be inert.And it is not authorable either:
ActionParamSchemain@objectstack/spec/uiis.strict()with a custom unknown-key error, andvalidationis not in itsACTION_PARAM_KEYS. So an authoredvalidation: '...'is a hard parse rejection on the server, whiletscagainst the public type accepts it.That is exactly the objectui#3174 shape — declared, inert, and blessed by a type the platform's own parser refuses — minus the second spelling that made #3174 worse.
Why it was left standing in #3174
#3174's fix removed the nine resolved-side picker keys (
referenceTo,displayField,idField,descriptionField,titleFormat,lookupColumns,lookupFilters,lookupPageSize,dependsOn) from the authoringActionParam, and installed a drift guard pinning the rule "the authoring type declares exactly the spec's authorable keys".validationis carried as the one named exception in that guard, so a second undeclared local key cannot appear without a decision — but the exception itself is debt, and this issue is the ledger entry for it.The decision this needs
ActionParam(and probablyActionParamDeftoo), on the enforce-or-remove reading: it can never work as authored, so declaring it can only mislead. Breaking change to two published types; needs a changeset.ActionParamSchemain@objectstack/spec(cross-repo contract change), have the resolver carry it andparamToFieldlower it into a real validation rule.Option 1 is the ADR-0049 default and the cheaper honest answer; option 2 is only worth it if per-param validation is a capability we actually want, in which case the spec is where it starts. Either way the gate is a test asserting that whatever the authoring type declares, the param dialog enforces.
Acceptance
ActionParamandActionParamDefagree with what the runtime does withvalidation.reference, which the public type didn't declare #3174 drift guard inpackages/types/src/__tests__/page-nav-misc-spec-parity.test.tsno longer needs a named exception (or names a different, justified one).