fix(spec): FlowFunctionEntrySchema 收下 lowered declaration,声明形写手重新扛得住 objectstack build (#4976) - #6278
Merged
Merged
Conversation
…住 `objectstack build` (#4976) `lowerCallables` 自 #4396 起会把声明形 `functions` 条目降级成 `{ handler: '<ref>', effect: 'writes' }` —— 保留声明、只把可调用体换成字符串 ref。同一次改动没有同步扩 union,于是 CLI 亲手产出的形状被它自己必须通过的 schema 拒收,`objectstack build` 报 `invalid_union: Invalid input`,路径止步于 `functions`,不点名键、不点名条目、不给原因。 补上第四个 union 成员:lowered declaration。它由 `FlowFunctionDeclarationSchema.extend({ handler })` 派生而来,而不是在旁边重抄 一份 —— 两者只差一个字段,所以严格性、surface 名、别名表与 `` `efect` → `effect` `` 处方原样随行,声明形将来加键也自动带过去。别名区未 触碰(不新增 `strictObject` 注册)。 `effect` 在此保持 optional + 默认值:走 `defineStack` 的路径会先把 `'pure'` 默认值落实,但 `{ strict: false }` 会跳过那次 parse,要求必填就会把同一个 `invalid_union` 还给那条路径。 runtime 半边本就正确,未改一行:`normalizeFlowFunctionEntry` 对两种 lowered 形状都返回 `undefined`(都不携带可调用体),而 `mergeRuntimeModule` 在任何 collector 之前就把 sidecar 模块的函数重新挂回 JSON 携带的声明上,所以 `effect` 在构建路径上完整抵达 `collectBundleFunctionEntries`。 两半之间补上跨界 pin:走真实流水线(`defineStack` → `normalizeStackInput` → `lowerCallables` → parse),而不是手写一份「以为 lowering 会产出什么」的样例 —— 正是这条从未有人跨过的边界,让两侧各自全绿而 build 死在接缝上。 行为面唯一变化:手写 `{ handler: 'someName' }` 由拒收变为接受。该拒收无法与本 成员共存,也本不该存在 —— 裸字符串条目(`functions: { foo: 'foo' }`)自 #4343 起就被接受并附带「注册不到任何东西」的说明,只拒 record 拼法而放行 string 拼法 是同一个契约的两种方言。两者失败方式一致且响亮:execute 时 `no function named '…' is registered`(#1870)。 showcase 换回诚实拼法(`{ handler: sweepProjectHealth, effect: 'writes' }`), 并把「钉死裸形」的守卫倒转为「唯一真正写数据的条目必须声明」—— 那才是值得守的 事实。相邻缺口(数组形 `functions: [{ name, handler }]` 同样无法往返)另立 #6238,不在本 PR 范围。
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 112 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
…ingify 偷偷丢掉
`showcase-declarative-endpoints.dogfood.test.ts` 用
`JSON.stringify(showcaseStack)` 手搓一份 artifact 顶替 `objectstack build`,
但只做了后者的一半:真实构建先跑 `lowerCallables` 把每个 callable 换成字符串
ref,并把函数本体放进 sibling ESM 模块;`JSON.stringify` 没有这一步,它只是把
函数值的键整个省略。
于是这份 artifact 从来就没携带过 showcase 的任何 function —— 它只是"看起来"
携带了:裸条目(`sweepProjectHealth: fn`)连键一起消失,剩下 `functions: {}`,
照样 parse 通过。两个函数都被静默丢弃,没人看得见。
showcase 换成诚实的声明形之后这份静默就破了:`{ handler: fn, effect: 'writes' }`
会保留对象、只丢 `handler`,留下 `{ effect: 'writes' }` —— 一个为自己并不携带
的函数声明了 effect 的条目,`FlowFunctionEntrySchema` 四个成员一致拒收,完全
正确。
改为显式剔除 `functions` 并写明原因:本 boot 真正运行的函数来自交给
`bootStack` 的**活栈**,不来自这个文件;该文件的职责是把 `apis:` 块喂给
`MetadataPlugin`。静默丢失变成声明式省略。
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 free
to 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.
Fixes #4976
前提复核(先证伪,再动手)
在
origin/main@ 773f80a 上直接跑真实流水线,前提成立:normalizeFlowFunctionEntry({ handler: 'syncBilling', effect: 'writes' })返回undefined,与正文所述一致。改了什么
FlowFunctionEntrySchema补第四个 union 成员 —— lowered declaration,即handler已被降级成字符串 ref 的声明形:作者写法一字未变。这个形状由 CLI 产出、不由人手敲:
lowerCallables在 parse 之前把每个内联可调用体换成可序列化 ref(必须如此 ——z.function()会包裹 callable,直接毁掉 ref 映射),而自 #4396 起它会保留声明、只替换 handler。当时 union 没有同步扩,于是 CLI 亲手产出的产物被它自己必须通过的 schema 拒收。成员是派生的,不是重抄的。
FlowFunctionDeclarationSchema.extend({ handler })—— 两者只差一个字段,所以严格性、surface 名、别名表与处方原样随行:{ handler: 'fn', efect: 'writes' }依然报Unrecognized key(s) on this `functions` entry并给出`efect` → `effect`;未知effect值依然拒收;空 ref 依然不是名字。声明形将来加键也自动带过去,不会再漂移一次。别名区未触碰 —— 用.extend()而非新开一次strictObject,不向strictObjectDeclarations()注册第二条记录,与在飞的 #6083 无语义冲突。effect保持 optional + 默认值。 走defineStack的路径会先把'pure'默认值落实(实测:{ handler: fn }出来是{ handler, effect: 'pure' }),但defineStack(…, { strict: false })会跳过那次 parse;要求必填就等于把同一个invalid_union原样还给那条路径。runtime 半边:核对结论是「本就正确,一行未改」
issue 正文说「预计无需改动,但值得确认」。确认结果:
normalizeFlowFunctionEntry对两种 lowered 形状都返回undefined—— 裸 ref 和 lowered declaration 都不携带可调用体,注册一个指向空的名字比不注册更糟。effect并非在此丢失:mergeRuntimeModule在任何 collector 之前就把 sidecar 模块的函数重新挂回 JSON 携带的声明上({ ...declared, handler: fn }),所以 boot 时 normalizer 看到的 handler 已经是真 callable,声明完好。packages/runtime/src/artifact-function-declarations.test.ts从另一侧钉住这条缝,本 PR 未改动它,全绿。顺序才是这件事安全的原因,已写进
normalizeFlowFunctionEntry的 docblock。跨界 pin(issue 点名要的那条)
packages/cli/src/utils/lower-callables.test.ts新增 round-trip:走真实流水线(defineStack→normalizeStackInput→lowerCallables→ parse),而不是手写一份「以为 lowering 会产出什么」的样例 —— 手写样例是真相的第三份拷贝,会以两半已经漂移过的同样方式再漂移一次。5 组用例 × 2(逐条目 parse + 整栈
ObjectStackDefinitionSchema.parse),外加一条「声明必须进产物、不只是过 parse」—— 过了 parse 却把effect丢在路上,等于用一个绿色的 build 重演 #4396 的静默去声明。范围:map 形。数组形
functions: [{ name, handler }]同样无法往返(裸形与声明形皆然,#4343 / #4976 都只碰过 map),其成员在stack.zod.ts而非FlowFunctionEntrySchema,已另立 #6238,并在测试注释里写明这条边界是刻意的、不是遗漏。反向验证(方向事先声明)
事先声明的预期方向:摘掉第四成员 → round-trip pin 应转红(常规方向)。
实测与预期一致,且红得精确:
失败的只有声明形用例;裸 handler 用例保持绿色 —— 第三个成员本就覆盖它们。报错正是 issue 里那条
invalid_union@functions。spec 侧同步 3 红。恢复后全绿。一处行为倒转,明说而非藏着
手写
{ handler: 'someName' }由拒收变为接受,原断言rejects a declaration whose handler is not callable因此被就地重判。这不是让步。裸字符串条目(
functions: { foo: 'foo' })自 #4343 起就被接受,并在 docblock 里附带「注册不到任何东西」的说明;只拒 record 拼法而放行 string 拼法,是同一个契约的两种方言。两者失败方式完全一致且响亮 —— execute 时no function named '…' is registered(#1870)。存活下来的是真正的判决:handler 既不是 callable 也不是名字(42/ 空串 / 缺失)依然拒收。按 fixture 三分法,这条属于「逐条重判」而非「批量改写」:旧断言钉的正是本 PR 要接纳的形状,留着它只会在合入后转红。
showcase:issue 自己的复现点,同 PR 关闭
examples/app-showcase里的注释写着 "switch back once it lands" 和 "Delete this guard — don't work around it — when #4976 lands",照办:objectstack.config.ts换回诚实拼法{ handler: sweepProjectHealth, effect: 'writes' };test/inert-wirings.test.ts的「钉死裸形」守卫倒转为「唯一真正写数据的条目必须声明」—— 值得守的从来不是「裸」,而是那个夜间 sweep 说出了自己在写。直接删掉会白丢覆盖。端到端实证(即 issue 的复现命令):
产物内容:
{ "summarizeCompletedTask": "summarizeCompletedTask", "sweepProjectHealth": { "handler": "sweepProjectHealth", "effect": "writes" } }验证
check:generated十门check:liveness/check:empty-state/check:skill-examples/check:variant-docs/check:exported-any/check:dual-source-exports全 PASSpnpm lintcheck:spec-parsed-alias、check:engine-double-contract、check:shard-attestation、check:empty-changesetpnpm testspecgpg.ssh.program=/tmp/code-sign的 MCP 调用超时)使临时仓git commit失败,与本 diff 无关 —— 关掉签名后同两文件 70 passedpnpm testclipnpm testruntimepnpm testshowcasepnpm typecheck×4check:test-typecheck债务台账未增(79 files / 691 errors 不变)check:nul-bytes+ 控制字节自扫changeset 定级论证
@objectstack/spec: minor。union 接受的输入集合严格变大,没有任何原本能 parse 的输入变得不能 parse(唯一变化是{ handler: 'name' }由拒转收,属加宽而非破坏),故为加性 → minor 而非 patch;也不是 major,因为无任何 FROM → TO 迁移要求作者做。与在飞 issue 的相交(实测,非推断)
claude/issue-6083-adr0122-phase2-bare-name-flipflow-function.zod.ts。纯文本冲突,无语义冲突:#6083 只翻三条类型别名z.infer→z.input并删FlowFunctionDeclarationInput,这些行我一行未碰;冲突落在它 hunk 的上下文行(union 体 + 其.describe()、以及我重写的FlowFunctionEntrySchemadocblock),两处均为机械解冲突。语义上还相互补足:z.input作用于新成员后,lowered 形的effect也正确地成为可选输入stack.zod.ts的composeStacks段,我完全未动该文件claude/issue-5016-action-param-options-vocabclaude/issue-5599-view-union-identity-preconditionclaude/issue-5775-sdui-props-enforce-or-removeclaude/issue-5945-hook-context-api-typedclaude/issue-5948-getuiview-slim-body与 PM 预期一致:除 #6083 外全部零相交。
范围外发现
functions: [{ name, handler }](the array form) cannot surviveobjectstack buildeither — #4343/#4976 both fixed only the map form #6238 —— 数组形functions: [{ name, handler }]同样无法往返objectstack build,且裸形/声明形皆然(script的 config 契约要接入 #4277 的执行期 parse,先得有判别式(actionType)形态 #4343 与functions: { fn: { handler, effect: 'writes' } }cannot surviveobjectstack build— lowering emits a shape FlowFunctionEntrySchema rejects #4976 都只碰过 map 形)。已按 Prime Directive chore: version packages #10 未认领立单,附实测四行对照与「修 or 退役」的取舍(该形状无任何 example / doc / skill 消费者)。Generated by Claude Code