Skip to content

fix(spec): FlowFunctionEntrySchema 收下 lowered declaration,声明形写手重新扛得住 objectstack build (#4976) - #6278

Merged
qq9340100 merged 2 commits into
mainfrom
claude/issue-4976-flow-function-lowered-entry
Aug 7, 2026
Merged

fix(spec): FlowFunctionEntrySchema 收下 lowered declaration,声明形写手重新扛得住 objectstack build (#4976)#6278
qq9340100 merged 2 commits into
mainfrom
claude/issue-4976-flow-function-lowered-entry

Conversation

@qq9340100

Copy link
Copy Markdown
Collaborator

Fixes #4976

前提复核(先证伪,再动手)

origin/main @ 773f80a 上直接跑真实流水线,前提成立:

map bare lowered        -> OK
map declared lowered    -> FAIL invalid_union@functions   ← 本 issue
array bare lowered      -> FAIL invalid_union@functions   ← 另立 #6238
array declared lowered  -> FAIL invalid_union@functions   ← 另立 #6238

normalizeFlowFunctionEntry({ handler: 'syncBilling', effect: 'writes' }) 返回 undefined,与正文所述一致。

改了什么

FlowFunctionEntrySchema 补第四个 union 成员 —— lowered declaration,即 handler 已被降级成字符串 ref 的声明形:

functions: {
  sweepProjectHealth: { handler: 'sweepProjectHealth', effect: 'writes' },
}

作者写法一字未变。这个形状由 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:走真实流水线(defineStacknormalizeStackInputlowerCallables → 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 应转红(常规方向)。

实测与预期一致,且红得精确:

× parses every entry it emits for a declared writer
× parses the whole lowered stack for a declared writer
× parses every entry it emits for a declaration that states the pure default
× parses the whole lowered stack for a declaration that states the pure default
× parses every entry it emits for a declaration that states nothing
× parses the whole lowered stack for a declaration that states nothing
× parses every entry it emits for both spellings side by side
× parses the whole lowered stack for both spellings side by side
× carries the declaration into the artifact, not just past the parse
Tests  9 failed | 9 passed (18)

失败的只有声明形用例;裸 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 的复现命令):

✓ Build complete (1445ms)
  Artifact: dist/objectstack.json (677.2 KB)
  Runtime: dist/objectstack-runtime.f266f59d0eb58097.mjs (1325.1 KB, 2 handlers)

产物内容:

{
  "summarizeCompletedTask": "summarizeCompletedTask",
  "sweepProjectHealth": { "handler": "sweepProjectHealth", "effect": "writes" }
}

验证

结果
check:generated 十门 ✓ 全部 up to date,无需重生成(union 成员不改可授权键面,新 schema 未导出)
「不代跑」6 源审计整组 check:liveness / check:empty-state / check:skill-examples / check:variant-docs / check:exported-any / check:dual-source-exports 全 PASS
pnpm lint ✓ 干净
ESLint job 家族门(20 项) ✓ 全 PASS,含 check:spec-parsed-aliascheck:engine-double-contractcheck:shard-attestationcheck:empty-changeset
pnpm test spec 8383 passed / 47 skipped;2 个文件红,原因是容器 git 签名器(gpg.ssh.program=/tmp/code-sign 的 MCP 调用超时)使临时仓 git commit 失败,与本 diff 无关 —— 关掉签名后同两文件 70 passed
pnpm test cli ✓ 89 files / 913 passed
pnpm test runtime ✓ 105 files / 1506 passed
pnpm test showcase ✓ 13 files / 146 passed
pnpm typecheck ×4 ✓ spec / cli / runtime / showcase 全绿;spec 的 check:test-typecheck 债务台账未增(79 files / 691 errors 不变)
check:nul-bytes + 控制字节自扫 ✓ 5930 个文件 OK;改动文件自扫无命中

changeset 定级论证

@objectstack/spec: minor。union 接受的输入集合严格变大,没有任何原本能 parse 的输入变得不能 parse(唯一变化是 { handler: 'name' } 由拒转收,属加宽而非破坏),故为加性 → minor 而非 patch;也不是 major,因为无任何 FROM → TO 迁移要求作者做。

与在飞 issue 的相交(实测,非推断)

Issue 分支 与本 PR 文件相交
#6083 claude/issue-6083-adr0122-phase2-bare-name-flip —— 同改 flow-function.zod.ts纯文本冲突,无语义冲突:#6083 只翻三条类型别名 z.inferz.input 并删 FlowFunctionDeclarationInput,这些行我一行未碰;冲突落在它 hunk 的上下文行(union 体 + 其 .describe()、以及我重写的 FlowFunctionEntrySchema docblock),两处均为机械解冲突。语义上还相互补足:z.input 作用于新成员后,lowered 形的 effect 也正确地成为可选输入
#5051 已并入 main(#6240) 无 —— 它只动 stack.zod.tscomposeStacks 段,我完全未动该文件
#5016 claude/issue-5016-action-param-options-vocab
#5599 claude/issue-5599-view-union-identity-precondition
#5775 claude/issue-5775-sdui-props-enforce-or-remove
#5945 claude/issue-5945-hook-context-api-typed
#5948 claude/issue-5948-getuiview-slim-body
#5055 本容器内无本地/远端分支 无(按现存分支实测)

与 PM 预期一致:除 #6083 外全部零相交。

范围外发现


Generated by Claude Code

…住 `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 范围。
@vercel

vercel Bot commented Aug 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 7, 2026 1:18pm

Request Review

@github-actions github-actions Bot added the size/m label Aug 7, 2026
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec.

112 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via @objectstack/spec)
  • content/docs/automation/connectors.mdx (via @objectstack/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via @objectstack/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/tenancy-modes.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/data-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/examples.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/spec)
  • content/docs/permissions/authorization.mdx (via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/apps.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/field-grouping-and-order.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Aug 7, 2026
…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`。静默丢失变成声明式省略。
@qq9340100
qq9340100 marked this pull request as ready for review August 7, 2026 13:37
@qq9340100
qq9340100 added this pull request to the merge queue Aug 7, 2026
Merged via the queue into main with commit a80302a Aug 7, 2026
25 checks passed
@qq9340100
qq9340100 deleted the claude/issue-4976-flow-function-lowered-entry branch August 7, 2026 13:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

functions: { fn: { handler, effect: 'writes' } } cannot survive objectstack build — lowering emits a shape FlowFunctionEntrySchema rejects

2 participants