feat(spec,service-automation,devx): 未声明 resumeAuthority 的 pausing 节点类型不再静默 fail-open (#5561) - #5725
Merged
Merged
Conversation
…型不再静默 fail-open (#5561) #3801 的 resume 闸门按「暂停在哪个节点」判权,所以它只覆盖作者记得声明 resumeAuthority 的 pausing 类型。而 `.default('any')` 让这个遗漏根本无法被观测: Zod 在 defineActionDescriptor 里就把键填上,「作者选了 'any'」与「作者从未想过」 产出逐字节相同的描述符。#3823 就是这样发生的 —— ADR-0044 把 revise 边指向通用 `wait`,`wait` 的 'any' 本身正确,而站在 service 所有位置上的那次暂停继承了一个 没人选过的 fail-open 值。 本次落地维护者裁决的第一步(两处探测 + 声明补齐,均非 breaking): - spec: resumeAuthority 去掉 .default('any') 改 .optional() —— 缺失即缺失。 运行时语义零变化,engine 的 `?? 'any'` 原样保留(它本来就在做默认值的事), 所以未声明者今天仍然可被通用路由 resume,只是不再无声。 - service-automation: registerNodeExecutor 对 supportsPause 且未声明 resumeAuthority 的描述符按类型去重打一次响亮 warn,文本写明两个合法值、 并说明显式声明 'any' 即可消音且不改变任何行为(避免把本就开放的节点逼向 'service')。screen / wait / subflow / map 四个 builtin 补上显式 resumeAuthority: 'any' —— 它们各自语义本就正确,只是此前在继承而非声明, 所以告警今天零点名,只捕未来的遗漏。 - devx: 新增 check:resume-authority-declared 静态门(AST 扫 defineActionDescriptor 字面量),仓内 pausing 描述符漏声明直接 CI 红。 #3823 是我们自己仓内的漏声明,PR 时刻才是该告诉作者的时刻;只扫发货源码, 测试夹具故意保留未声明形状以便测告警本身。 第二步(缺失即 fail-closed)仍挂 #5561 等 breaking 窗口 —— 经此改动后它收缩成 resolveResumeAuthority 里那一个表达式,而不再需要 schema 迁移。 supportsPause 自身是运行时零强制的声明(#5703):暂停的事实来自 execute() 返回 suspend: true。声明它的执行器漏声明 supportsPause 时,告警与静态门都看不见 —— 这一点在门的报错文本、告警的 TSDoc 与 spec 字段注释里都写明了。 Refs #5561, #3801, #3823, #3853, #5703; ADR-0044 修正案。 Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BWS4heBoAitLmzCLhcYdbK
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 2 package(s): 109 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
os-zhuang
marked this pull request as ready for review
August 6, 2026 02:20
This was referenced Aug 6, 2026
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.
Refs #5561
维护者 2026-08-06 裁决的第一步(修正案 C + A,均非 breaking)。第二步(缺失即 fail-closed)仍挂 #5561 等 breaking 窗口,本 PR 不做。
前提重验(origin/main @ 6ddb2ec)
ActionDescriptor.resumeAuthority确实.default('any')(packages/spec/src/automation/node-executor.zod.ts);issue 作者记忆中的supportsPause字段真实存在(同 schema,z.boolean().default(false)),不是误记。resolveResumeAuthority(engine.ts,行号已从立单时的 :2575 漂到 :2766),其descriptor?.resumeAuthority ?? 'any'是本 PR 刻意不动的那一行。.default('any')让「未声明」在注册边界上不可表示 —— 运行时侧,未声明与显式'any'经defineActionDescriptor后逐字节相同(zod 4.4.3 实测distinguishable? false);类型侧,z.infer让该字段在输出类型里必填(tsc TS2741),所以连「运行时 undefined」都不是正当信号。四条候选路里 A 是唯一干净且非 breaking 的,C 与之互补且零 spec 依赖。A —— spec 一行 + 声明补齐 + 注册告警
resumeAuthority由.default('any')改.optional()。运行时语义零变化:resolveResumeAuthority的?? 'any'本来就在做默认值的事,未声明者今天依旧可被通用 resume 路由继续,只是不再无声。第二步因此收缩成这一个表达式,不再需要 schema 迁移。registerNodeExecutor新增warnIfResumeAuthorityUndeclared:supportsPause === true且resumeAuthority === undefined→ 按类型去重、每 engine 实例一次的响亮 warn(去重字段沿用同包 嵌入式 host 直接 new AutomationEngine() 而不调用 sealNodeTypeVocabulary(),就完全拿不到节点类型校验 —— #4771 的连带回退,靠文档而非强制 #4792nodeTypeSealOmissionWarned的 per-instance 先例)。文本单行自足、写明两个合法值,并明确「显式声明'any'即可消音且不改变任何行为」—— 否则会把本就该开放的节点逼向'service'。告警只陈述该描述符缺了声明这个作者时刻就固定的静态事实,不读任何注册表、不对「本次启动会不会有人后来注册」下判词,所以check:startup-registry-verdict绿(实测),与 嵌入式 host 直接 new AutomationEngine() 而不调用 sealNodeTypeVocabulary(),就完全拿不到节点类型校验 —— #4771 的连带回退,靠文档而非强制 #4792 同款约束。screen/wait(通用路由本就是它们的正门)、subflow/map(闸门会顺subflow:/map:关联走到子 run 判词,automation: the #3801 resume gate followssubflow:but notmap:, and the resume body can write engine-internal variables #3853,所以本节点这个'any'不是最终生效的权威 —— 显式写出来才能让「告警在这里沉默」是决定而非缺口)。告警今天会点名的现役类型清单
screenbuiltin/screen-nodes.ts'any'waitbuiltin/wait-node.ts'any'subflowbuiltin/subflow-node.ts'any'mapbuiltin/map-node.ts'any'approvalplugin-approvals/src/approval-node.ts'service'approval_reviseplugin-approvals/src/approval-revise-node.ts'service'即落地后零点名,只捕未来遗漏。这个零由一条测试钉住,且同一条测试同时断言「四个 pausing builtin 确实被注册且都声明了
'any'」—— 否则零点名可能是「什么都没注册」的空绿。C —— 仓内静态门
check:resume-authority-declaredAST(TypeScript compiler API,与
check-engine-double-contract.mjs同路数)扫defineActionDescriptor({...})字面量:supportsPause: true且缺resumeAuthority→ CI 红,指名文件、行号、类型与两种修法。为什么与运行时告警并存:告警说给读服务器日志的人听(第三方插件的正确频道,而本仓今天一个都没有),而 #3823 是我们自己仓内的漏声明,该告诉作者的时刻是 PR。三个刻意的边界(都写进脚本头与
--self-test):screen的描述符嵌着上百行 configSchema JSON Schema,其properties里可以出现任何同名键 —— 那是作者数据,不是能力声明。正则无法区分,所以读结构。自测两侧都钉了。resume-authority-declaration.test.ts必须构造未声明的描述符才能测告警本身;把夹具纳入门会让被测机制无法被测。(与只扫测试的check-engine-double-contract.mjs正好互为镜像:每个门的范围是它的主体,不是全仓扫射。)merge.os-regen.driver指向「上一个装过依赖的 worktree」的绝对路径 —— 该 worktree 一删,全容器的生成物合并驱动就坏了 #4868 家族)。管线接线照最近先例(#5694):
package.json的check:resume-authority-declared先跑--self-test再跑扫描,lint.yml的 lint job 里紧随Engine test-double contract gate。regen 生成物 + 契约面存活核验
pnpm --filter @objectstack/spec check:generated十项全绿,唯一实际 stale 的是check:docs,--fix只重生成了它:content/docs/references/automation/node-executor.mdx一行(resumeAuthority由✅变optional+ 新描述)。check:api-surface与check:authorable-surface在真实重建 dist 之后依旧绿 —— 实测结论:api-surface.json记录导出符号,不记录 schema 成员的可选性(dist/node-executor.zod-*.d.ts已确实变成z.ZodOptional),authorable-surface.json记录键路径而键仍在。我在决策卡里预估的 api-surface 记账成本实测为零,如实更正。RuntimeProtocolSchema.actions对未声明者不再带该键。三仓实测零读者(framework 内唯一读者是resolveResumeAuthority;objectui 与 cloud 对resumeAuthority/supportsPause/isAsync全仓零命中)。gen:schema把authorable-surface.base.json的baseRev重锚到当前 merge base 并带进别人已 merge 的键。那不是本 PR 的改动,已 revert;还原后check:generated仍全绿。#5703 的缝(已在三处写明)
supportsPause自身是运行时零强制的声明:暂停的事实来自execute()返回suspend: true(engine.ts的if (result.suspend)),#3801 闸门也只读resumeAuthority。所以一个execute()会暂停、却把supportsPause留在默认false的执行器,告警与静态门都看不见,而且照样 fail-open。这一点写进了:门的报错尾注、warnIfResumeAuthorityUndeclared的 TSDoc、spec 里supportsPause的字段注释。#5703 走正常分诊。类型债台账的一处如实更正
scripts/check-type-check-coverage.mjs里@objectstack/service-automation的 DEBT 注记原文是「TS2741: engine.test.ts misses resumeAuthority」。.optional()之后实测那两处 TS2741 没有消失而是转移了:同两行字面量同时也漏handlerContract,TS 一次只报一个,于是现在报handlerContract。计数 2 不变,我只更正了归因(这句话是被本 PR 改坏的,所以归我修)。另有一处独立事实:该包实测 raw 总数为 5(多出 3 处nested-region-parity.test.ts的 TS2341,与本 PR 无关、main上即如此),而台账冻结值是 2 —— 该门不重跑 tsc 所以不红;我没有动那个数字(抬高冻结值会把别人的债洗成基线),另行记到 #4311。验证
@objectstack/spec:317 files / 8083 tests 全绿(含新增 2 条 + 替换 1 条 resumeAuthority 用例)。@objectstack/service-automation:62 files / 741 tests 全绿(新增resume-authority-declaration.test.ts11 条;resume-authority-gate.test.ts19 条零回归)。@objectstack/plugin-approvals(消费半径):19 files / 446 tests 全绿。typecheck:spec(含check:test-typecheck,债账 79 files / 691 errors 不变)、plugin-approvals、runtime三包全绿。check:resume-authority-declared(自测 + 扫描 6/6)、check:startup-registry-verdict、check:adr-anchors、check:nul-bytes、check:durability-log-level、check:type-check-coverage全绿;改动文件 eslint 干净;控制字符自查(grep -naP覆盖 gate 盲区)零命中。反向验证(先预测方向,后实测)
wait-node.ts:157被点名、exit 1、计数 6→5。已还原。.default('any')→ 告警不可达:预测「计数类用例转红、沉默类用例保持绿但是空绿」。实测正是如此 —— 11 条里 5 条红(全部是数告警条数的),6 条绿;其中「零点名」那条也保持绿,因为四个 builtin 已显式声明,所以它钉的是零点名这个事实,机制由那 5 条钉。spec 侧 2 条(toBeUndefined/Object.hasOwn可区分性)同时转红。已还原并重建 dist 复绿。check:resume-authority-declared保持绿,如预测 —— 它读源码字面量,不读 schema 默认值。这也正是两者并存的意义。Generated by Claude Code