Skip to content

fix(automation,approvals): 节点类型校验推迟到插件词汇表封闭之后 —— approval flow 不再被误报「运行时会失败」 - #4791

Merged
os-zhuang merged 3 commits into
mainfrom
claude/issue-4771-flow-validation-kernel-ready
Aug 3, 2026
Merged

fix(automation,approvals): 节点类型校验推迟到插件词汇表封闭之后 —— approval flow 不再被误报「运行时会失败」#4791
os-zhuang merged 3 commits into
mainfrom
claude/issue-4771-flow-validation-kernel-ready

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #4771

问题

AutomationServicePlugin.start() 从 ObjectQL registry 拉起 flow 并当场校验节点类型;
ApprovalsServicePlugin.start() 在 0.8 秒后才注册 approval 执行器。校验器在词汇表还没成型
的时候就下了结论 —— showcase 每次冷启的那 8 条「will fail at execution time」全是假的。

而 ADR-0018 明确把节点词汇表定义为开放、可运行时扩展的(插件通过 registerNodeExecutor(type)
贡献类型)。在扩展完成前拍板的校验器,是在校验一个还没成型的世界。

真正的代价不是噪音,是信号丢失:真的没装 approvals 插件的部署会得到一模一样的 8 条告警,
这条 warn 的信噪比因此是 0。本 PR 之后它才第一次具备拦截价值。

改动(PM 裁定的修法 1)

packages/services/service-automation

  • AutomationEngine.sealNodeTypeVocabulary() —— 宣告词汇表封闭,对所有已启用的已注册 flow
    跑一次权威校验,每个有问题的 flow warn 一条。幂等:只有第一次会打日志,之后仍返回当前 audit。
  • AutomationEngine.getUnknownNodeTypeAudit(): UnknownNodeTypeAuditEntry[] —— 同一发现的
    状态形态(供 CLI 启动摘要 / 健康检查直接读引擎,而不是 grep 日志)。与
    getTriggerBindingAudit() 同一套路。
  • AutomationServicePluginkernel:bootstrapped 调用它。
  • 封闭之后 registerFlow 恢复即时告警:Studio 发布 / dev reload 进正在运行的服务器时,
    词汇表确实完整,那句断言此时为真。这是时序修复,不是把告警静音。
  • 被禁用的 flow(status: 'obsolete'/'invalid')不再上报 —— 跑不起来的 flow 谈不上「运行时
    失败」,这正是本 issue 要消灭的那类假断言(与 getTriggerBindingAudit 的口径一致)。
  • 告警文案改成它现在能承诺的事,并带上补救动作。

为什么是 kernel:bootstrapped 而不是 kernel:ready

裁定的语义是「所有插件 start() 完成之后」,kernel:bootstrapped 严格更晚且更安全:

  1. 本插件自己的 kernel:ready handler 还会再注册一批 flow(syncFlowsFromProtocol,即
    inline app flow 的冷启绑定);
  2. 排在本插件之后启动的插件,仍可能在它自己的 kernel:ready 里贡献执行器;
  3. packages/core/src/kernel.ts 对这个 hook 的定义原文就是:「Reconcile/backfill work that
    consumes data produced by a later-starting plugin's kernel:ready handler belongs here, not in
    kernel:ready (where handler order would race the data)」。本审计正是这种消费者。

告警可见性不受影响:CLI 的 boot-quiet 窗口会截获整个 boot 期的 logger.warn 并在 banner 下
重放(#4012),kernel:bootstrappedbootstrap() 之内。

packages/plugins/plugin-approvals(同一缺陷类的另一半)

  • 拿不到 automation 引擎时由 info 改为 warn —— dev 默认日志级别是 warn,原来真降级发生时
    反而看不见
    ([convention] best-effort 降级导致"看起来正常、实则不持久"时不应记 warn——把 #4460 的点状修复定成规则 #4632)。文案写明后果(该部署里每个 ADR-0019 approval flow 都会以 NO_EXECUTOR
    失败)与补救(装 @objectstack/service-automation)。按 AGENTS.md「Degradation log levels」,
    这是功能性降级(能力没装上,不是数据没落盘),warn 是正确档位。
  • catch 收窄到「服务查找」这一步:registerApprovalNode 内部真出错时以自己的身份抛出,不再被
    贴上 "no automation engine" 的错误标签。
  • automation 服务存在但不接受节点执行器的分支,从前一条日志都不打,现在同样 warn。

测试(两种部署必须可区分 —— 验收标准 1 & 2)

packages/services/service-automation/src/flow-node-type-audit.test.ts(新增,LiteKernel 真启动)

复现条件是断言出来的,不是假设的:每个用例都从 boot 日志证明 Flow registered: … 出现在
Node executor registered: approval 之前 —— 即修复前误报的那个窗口确实发生了。

用例 断言
装了贡献插件 0 条节点类型告警,getUnknownNodeTypeAudit() 为空
没装 每个 flow 1 条告警,带 flow 名 + approval + 补救动作;audit 有对应条目
两者对照 0 !== 1 —— 修复前这两个 boot 打印的东西完全一样
封闭后再发布 flow 立即 warn 1 条(证明这是时序修复而非静音)

反向验证:把 nodeTypeVocabularySealed 临时改成初值 true(等价于修复前的即时校验),
该文件 4 个用例里 3 个失败(其中「没装」那条从 2 变成 4 条告警 —— 即注册时 + 封闭时各报一次)。

engine.test.ts 增补:封闭前不告警 / 封闭时按 flow 报 / 封闭后即时报 / 禁用 flow 不报 /
幂等封闭只 warn 一次 / 「插件后注册」不再误报。

plugin-approvals/src/approval-node-degradation.test.ts(新增):无 automation 服务 → warn
且不再是 info;服务在但不收执行器 → 同样 warn;正常时注册 approval 且不告警。

@objectstack/service-automation:test  Test Files 55 passed (55)   Tests 662 passed (662)
@objectstack/plugin-approvals:test    Test Files 17 passed (17)   Tests 370 passed (370)
@objectstack/plugin-approvals:typecheck  tsc --noEmit  ✓

service-automation 没有 typecheck 脚本(ratchet 里是 DEBT 条目),直接跑 tsc --noEmit:
改动前后都是同样的 5 个既有报错(engine.test.ts 2 个 TS2739 + nested-region-parity.test.ts
3 个 TS2341,后者访问私有 flows,与本 PR 无关),本 PR 零新增

另跑:check-durability-degradation-log-levelcheck-changeset-fixedcheck-changeset-no-major
check-adr-anchors 全绿;改动文件 eslint 无输出。

范围

只碰 packages/service-automation + packages/plugins/plugin-approvals(+ 一个 changeset),
未触碰本轮并行批的 objectql / service-storage / plugin-auth

嵌入式 host 注意

直接 new AutomationEngine() 而不经过 AutomationServicePlugin 的宿主,需要在自己的插件都装好
之后调用一次 sealNodeTypeVocabulary(),才能拿到这条告警(以及之后的即时校验)。已写进 changeset。

🤖 Generated with Claude Code

https://claude.ai/code/session_015Br2xsJsczFsTR9bvbh2Ny


Generated by Claude Code

…plugin vocabulary is closed (#4771)

ADR-0018 makes the flow node-type vocabulary open and runtime-extensible: a
plugin contributes types via registerNodeExecutor() from its own init()/start().
AutomationServicePlugin.start() pulls flows from the ObjectQL registry and
validated each node type on the spot — ~0.8s before ApprovalsServicePlugin
registered the `approval` executor. Every showcase cold boot therefore asserted
that eight ADR-0019 approval flows "will fail at execution time", and all eight
were false. Worse, a deployment that genuinely lacks the approvals plugin
produced the identical eight, so the warning could not distinguish the two.

- AutomationEngine.sealNodeTypeVocabulary() declares the vocabulary closed and
  runs the authoritative audit, warning once per offending flow. Idempotent.
- AutomationEngine.getUnknownNodeTypeAudit() exposes the same finding as state
  (mirrors getTriggerBindingAudit) for hosts that read the engine, not the log.
- AutomationServicePlugin seals at kernel:bootstrapped — strictly after every
  plugin's start() AND every kernel:ready handler (its own registers more flows).
- After sealing, registerFlow warns inline again: a Studio publish / dev reload
  into a running server IS judged against a complete vocabulary. Timing fix, not
  a mute.
- Disabled flows (obsolete/invalid) are skipped: a flow that cannot run cannot
  fail at run time.

Same defect class, second half: ApprovalsServicePlugin logged "no automation
engine — approval node not registered" at info while dev defaults to warn, so
the real degradation was invisible exactly when it happened (#4632). Now warn,
naming consequence and remedy; the catch is narrowed to the service lookup so a
failure inside registerApprovalNode surfaces as itself; and the
service-exists-but-takes-no-executors branch, which logged nothing at all, warns
too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Br2xsJsczFsTR9bvbh2Ny
@vercel

vercel Bot commented Aug 3, 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 3, 2026 6:52am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/l labels Aug 3, 2026
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/plugin-approvals, @objectstack/service-automation.

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

  • content/docs/automation/approvals.mdx (via @objectstack/plugin-approvals)
  • content/docs/automation/flows.mdx (via @objectstack/service-automation)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/plugin-approvals, @objectstack/service-automation)
  • content/docs/plugins/packages.mdx (via @objectstack/plugin-approvals, @objectstack/service-automation)
  • content/docs/releases/implementation-status.mdx (via @objectstack/plugin-approvals, @objectstack/service-automation)
  • content/docs/releases/v9.mdx (via @objectstack/plugin-approvals, @objectstack/service-automation)

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.

表格原文说节点 type「is checked against the live action registry at
`registerFlow()`」。本 PR 之后这句不再为真:启动期注册的 flow 在词汇表封闭
(所有插件 start() 完毕)时统一校验,封闭之后的注册才回到即时校验。顺带写明
这条检查是 warn 而非拒绝,以及执行时的 NO_EXECUTOR 后果。

同文件其余提到 `registerFlow()` 的句子(config 键、条件表达式、ADR-0031 区域)
描述的是没有改动的校验,保持原样。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Br2xsJsczFsTR9bvbh2Ny
@os-zhuang
os-zhuang marked this pull request as ready for review August 3, 2026 07:03
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 3, 2026
Merged via the queue into main with commit 25784cf Aug 3, 2026
22 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-4771-flow-validation-kernel-ready branch August 3, 2026 07:14
os-zhuang pushed a commit that referenced this pull request Aug 3, 2026
`AuditAbortSignal` 从包根导出,加上 `DanglingReferenceAuditOptions.signal`
与 `DanglingReferenceReport.aborted` 两个新字段:新增公开面按仓内先例
(#4791 因新增 `sealNodeTypeVocabulary()` 等公共 API 定 minor)是 minor,
不是 patch。`@objectstack/cli` 只动了内部的 schema-migrate.ts,维持 patch。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Br2xsJsczFsTR9bvbh2Ny
os-zhuang pushed a commit that referenced this pull request Aug 3, 2026
`fillEmptyGroups` 从包根 index.ts 导出,属于新增公共 API,按 #4815
(objectql / AuditAbortSignal)与 #4791(service-automation /
sealNodeTypeVocabulary)确立的同一条 bar,changeset 从 patch 改为 minor,
并在正文点名这个新导出。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Br2xsJsczFsTR9bvbh2Ny
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/l tests tooling

Projects

None yet

2 participants