fix(service-automation): 从未 seal 的引擎首次执行 flow 时告警一次 (#4792) - #5595
Conversation
… node-type vocabulary (#4792) #4771 made sealNodeTypeVocabulary() the only moment the ADR-0018 node-type check runs. AutomationServicePlugin seals at kernel:bootstrapped, so plugin hosts are covered — but a host that constructs `new AutomationEngine()` and never seals lost the check entirely, in silence, discoverable only by reading a changeset. The first execute() on an unsealed engine now says so once. - Once per engine INSTANCE, not per process: a host with one engine per tenant/environment omitted the call on each of them. - The line reports the missing CALL, not the unknown-type audit: an unsealed engine's vocabulary can still grow by contract, so naming absent executors there would rebuild #4771's contradictable verdict inside the embedded path. getUnknownNodeTypeAudit() stays the read-only probe for hosts that want the findings without closing the vocabulary. - It deliberately does NOT auto-seal: authority over "closed" stays with the host, and after a seal registerFlow validates inline — auto-sealing would hand the false "will fail at execution time" warnings to any embedded host that registers executors after its first run (legal under ADR-0018). Tests: embedded host warns once; unknown/disabled flow names do not trigger it; the warn does not seal; plus two sentinels — an explicitly-sealed host and the AutomationServicePlugin boot gain no log line. decision-branch-routing's harnesses now seal (they are embedded hosts asserting zero warnings about #4414 routing), which the inversion check proves is load-bearing. Co-Authored-By: Claude Fable 5 <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
|
📓 Docs Drift CheckThis PR changes 1 package(s): 5 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
|
一个审阅者大概会问的边界,先说清楚:告警只挂
Generated by Claude Code |
Fixes #4792
前提核对(对当前 origin/main)
issue 写于 08-03,先核对现状再动手,前提成立:
packages/services/service-automation/src/engine.ts:1132—private nodeTypeVocabularySealed = false仍在,字段名未变;engine.ts:3600sealNodeTypeVocabulary()是唯一执行 ADR-0018 节点类型校验的时刻;engine.ts:2022registerFlow只在 已 seal 后才即时校验;plugin.ts:850/878—AutomationServicePlugin仍在kernel:bootstrapped调用 seal(插件路径必然早于任何execute(),不会误报);execute()里没有任何人加过相关告警(grepnever sealed/warnOnce/hasWarned全无命中)。即:直接
new AutomationEngine()且不自己 seal 的嵌入式 host,至今仍是完全静默地拿不到该校验。改动
execute()在通过「flow 存在 + 未被禁用」两道门之后调用新的warnIfNodeTypeVocabularyNeverSealed():词汇表未 seal 且本引擎还没说过,则打一条warn,写明丢了什么(ADR-0018 校验从未运行,这些节点改为运行中NO_EXECUTOR失败)和怎么修(host 在插件贡献完执行器后调用engine.sealNodeTypeVocabulary();插件路径在kernel:bootstrapped做这件事)。等级按 #4632 判为功能性降级 →warn。三个设计决定,都有代码证据:
nodeTypeSealOmissionWarned是实例字段,不是模块级):按租户/环境各建一个引擎的 host 是在每个引擎上都漏了这次调用,模块级 flag 只会报第一个跑起来的那个。getUnknownNodeTypeAudit()。engine.ts:2022显示 seal 之后registerFlow转为即时校验,所以自动 seal 会让「先执行 flow、后注册插件执行器」的嵌入式 host(ADR-0018 允许词汇表开放,这完全合法)开始收到 bug(service-automation): flow 节点类型校验跑在插件贡献的执行器注册之前 —— 每个 ADR-0019 approval flow 都被误报「will fail at execution time」 #4771 那种「will fail at execution time」误报 —— 那正是 bug(service-automation): flow 节点类型校验跑在插件贡献的执行器注册之前 —— 每个 ADR-0019 approval flow 都被误报「will fail at execution time」 #4771 要删的东西。seal 的所有权语义因此未被改变(仍由 host 决定),不构成公共契约分叉,故按代码证据裁定而非上抛。告警文本刻意避开
warnUnknownNodeTypes的no registered executor or descriptor字样:多处测试与日志过滤用这个子串计数「每 flow 的未知类型发现」,一条只是在谈论它们的行文不能被算成一条(第一版就撞上了,flow-node-type-audit.test.ts的计数器同样读这个子串)。消费半径与 fixture 处置
execute()的调用者遍及全仓,因此按规则的消费半径扫 fixture,而不是只扫被改的包:全仓new AutomationEngine(的 51 处里,生产代码只有plugin.ts(走 seal),其余全是测试。本包整套跑完暴露出builtin/decision-branch-routing.test.ts的 5 个用例 —— 它们是嵌入式 host,expect(warnings).toHaveLength(0)表达的是「#4414 路由没有告警」。处置选的是「补声明」而不是「过滤断言」:两处 harness 在注册完执行器/flow 后补
engine.sealNodeTypeVocabulary(),让 fixture 真正表达一个装配正确的 host,零容忍断言得以原样保留。若改成把新告警从断言里过滤掉,以后任何借用这个 harness 的测试都会连带对真实信号失明。测试与反向验证
反向验证的方向是先预测再跑:
execute()里的调用(还原修复前)Tests 3 failed / 18 passedTests 10 failed / 11 passed,含两条SENTINEL与 5 个 routing第二个实验同时证明我给
decision-branch-routing.test.ts补的 seal 是承重的,不是装饰。注意第 4 个新用例(「未注册/已禁用的 flow 名不触发」)断言的是「无告警」,所以在实验一里理应保持绿 —— 它确实绿,这与「修前红」的模板预期不同,如实记录。跑过的命令(真实输出摘要):
pnpm --filter @objectstack/service-automation exec vitest run --maxWorkers=2→Test Files 59 passed (59)/Tests 713 passed (713)(707 旧 + 6 新)plugin-approvals446 /trigger-schedule42 /trigger-record-change55 /connector-mcp23 /connector-rest16 /connector-slack8,全绿check:startup-registry-verdict(40 seam(s) … none recording a verdict the boot can contradict)、check:durability-log-level、check:nul-bytes、check:adr-anchors、check:type-check-coverage全过;改动文件 eslint 无输出typecheckscript,故直接tsc --noEmit -p tsconfig.json核对 —— 报错全部落在我未触碰的文件(engine.test.ts的 TS2739 即台账所记、nested-region-parity.test.ts的 TS2341),新增文件与engine.ts零报错changeset:
patch(用户可见的新告警)。未碰 wait-node /plugin.ts的绑定日志面。构建过程中packages/spec/authorable-surface.base.json被gen:schema重新锚定,已还原、未提交。分支基于81087877e;其后落在 main 的两个提交(#5577 / #5578)只动.claude/agents/os-dev.md与packages/client/vitest.integration.config.ts,与本改动零重叠,故未做无谓合并 —— PR CI 会校验合并结果。Generated by Claude Code