feat(lint): 新增 gating 规则 flow-trigger-unroutable —— record_change flow 声明了引擎无法路由的 triggerType (#6637) - #6884
Merged
Conversation
…w 声明了引擎无法路由的 triggerType (#6637) `type: 'record_change'` 的 flow 若在 start 节点写下引擎无法路由的 `triggerType` (`onCreate`、`on_update`、`['onCreate']` 等),会被悄悄降级成手动 flow:声明得 像装好了,校验通过、启动通过,然后永远不触发。 先测量再下判断,因为 issue 的措辞("silently degrades")是概括,而诊断信息必须 为真。实际路径: - `AutomationEngine.resolveTriggerBinding` (engine.ts:1625-1719) 用字面量 `startsWith('record-')` 认领 record-change flow,落空后依次试数组形式、 `timeRelative`、`config.schedule`/`flow.type === 'schedule'`、 `flow.type === 'api'`/`triggerType === 'api'`,全不匹配则在 :1718 返回 `undefined`; - `activateFlowTrigger` (:1729) 以 `if (!resolved) return;` 开头,flow 从未绑定, 也不写一行日志; - 专为「悄悄没绑上」而建的 `getTriggerBindingAudit` (:2387-2400) 调用同一个 resolver,以 `manual / screen flow — nothing to bind` 跳过它。于是它的两个消费者 ——automation 插件的 `kernel:bootstrapped` 告警循环 (plugin.ts:907-910) 与 CLI 启动摘要的 `unbound` 列表 (serve.ts:3428)——都不会点名。 唯一痕迹是 banner 里 flow 总数比 bound 数多一:一个没有名字、没有原因的数字。 所以规则的 message 说的是「没有任何地方点名它」,而不是「完全没有信号」——后者会 差一个计数。 范围刻意收窄到「声明了 `type: 'record_change'`」的 flow。落空到无绑定本身也正是 一个 flow 合法地成为手动 flow 的机制(`lint-flow-patterns.test.ts` 已经为邻近规则 钉了一条 `does NOT flag record_change` 的判例),所以只按落空判定会误伤世上每一个 手动 flow。而 `autolaunched` / `screen` 才是手动 flow 声明的类型,二者都到不了这里 ——这正是它在编写期可判定的原因。 严重级取 `error`,按 #5762 的家族判据:判决完全由本仓库内的引擎路由链决定。这一条 是唯一需要实测而非假定的:插件确实可以提供 trigger 实现,所以「装个包就好了」对它 是个真假设。实测为假——`registerTrigger` 以**解析后**的类型为键,而「作者写的 token → 解析后类型」这张表是一串写死的 `startsWith` / `typeof` 判断,全链路没有任何注册表 查询。没有任何包能教会引擎一个新的作者 token。 两种形状刻意不归本规则,各由测试钉住: - `triggerType` 完全缺失 —— 同样必死,但属于遗漏而非自相矛盾,且语料实测在 `examples/app-todo` 有活实例(#6882,且它的谓词还写在没人读的 `triggerCondition` 键上,不是改一个 token 的事)。判据要求该键**存在**,因此 放宽是一次刻意编辑,而不是改动谓词的副作用。 - 同时声明了引擎能路由的东西(`config.schedule`、`triggerType: 'api'`)—— 这类 flow 会绑定并触发,只是按错误的 trigger 语义。是另一种缺陷(错绑,而非未绑), 有它自己的严重级论证要做。 语料实测(origin/main,三个 example app 共 34 个 authored flow):本规则命中 0 处; `record_change` 类型共 2 个,`showcase_urgent_task_alert` 接线正确。仓库内两处 fixture 写了本规则所判之形状,按 fixture 三分法就地改写而非加基线豁免: `authoring-rule-command-parity.test.ts` 的 `onCreate`(该用例的植入缺陷是审批表达式, trigger token 只是顺带),以及 `lint-flow-patterns.test.ts` 里把 flow 类型回写进 trigger 槽的 `'record_change'`。 Fixes #6637 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F8q5J1MQyocgtNspb15fSn
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 3 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
os-project-manager
marked this pull request as ready for review
August 9, 2026 02:55
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 #6637
type: 'record_change'的 flow 若在 start 节点写下引擎无法路由的triggerType(onCreate、on_update、['onCreate']等),会被悄悄降级成手动 flow:声明得像装好了,os validate通过、启动通过,然后永远不触发。本 PR 在validate-flow-trigger-readiness.ts里加一条 gating 规则flow-trigger-unroutable(error,surfaces: CLI_AND_RUNTIME,随该文件既有注册项一起生效)。一、先测量退化,再写诊断
issue 的措辞「silently degrades」是概括,而诊断信息必须为真,所以先按 file:line 把真实路径量出来:
service-automation/src/engine.ts:1625-1719resolveTriggerBindingstartsWith('record-')认领 record-change flow;落空后依次试数组形式、timeRelative、config.schedule/flow.type === 'schedule'、flow.type === 'api'/triggerType === 'api',全不匹配则在 :1718 返回undefinedengine.ts:1729activateFlowTriggerif (!resolved) return;开头 —— flow 从未绑定,也不写一行日志engine.ts:2387-2400getTriggerBindingAuditif (!resolved) continue; // manual / screen flow — nothing to bind跳过它service-automation/src/plugin.ts:907-910kernel:bootstrapped的告警循环遍历上面这份审计 → 一条都不发cli/src/commands/serve.ts:3428unbound列表同样来自该审计 → 空cli/src/commands/serve.ts:3435-3441s.bound && s.triggerType === 'record_change'→ 也跳过唯一痕迹:
getFlowRuntimeStates确实以bound: false报出该 flow,但它只汇入 banner 的计数行(flow 总数比 bound 数多一),没有名字、没有原因。所以规则的 message 写的是「没有任何地方点名它」,而不是「完全没有信号」—— 后者会差一个计数。这条差别是刻意的:
flow-trigger-unknown-event的 flow 是会绑定、会有 bind-time warn 的,只说「never fires」两者就分不开了。二、判据与范围
判据逐字镜像引擎的路由链,只在其之外说话:
为什么必须收窄到
type: 'record_change':落空到无绑定本身也正是一个 flow 合法地成为手动 flow 的机制 ——lint-flow-patterns.test.ts已经为邻近规则钉了一条does NOT flag record_change判例。只按落空判定会误伤世上每一个手动 flow。而autolaunched/screen才是手动 flow 声明的类型,二者都到不了这里,这正是它在编写期可判定的原因。新 id 而非复用
flow-trigger-unknown-event:沿用timeRelative家族-invalid/-unroutable的同一条分界 —— 引擎是否路由该值。带record-前缀正是「被路由」的充要条件,所以两个 id 对死 token 构成划分,永不同时命中同一个值(有测试钉住)。两种形状刻意不归本规则,各由测试钉住,避免日后被顺手放宽:
triggerType完全缺失 —— 同样必死,但属于遗漏而非自相矛盾,且语料实测在examples/app-todo有活实例(见下)。判据要求该键存在。config.schedule、triggerType: 'api')—— 这类 flow 会绑定并触发,只是按错误的 trigger 语义。是另一种缺陷(错绑,而非未绑),有它自己的严重级论证要做。三、严重级:
error,以及唯一需要实测的那一步按 #5762 的家族判据(本 stack 是否足以判定该 flow 已死)取
error。这一条是家族里唯一需要实测而非假定的:插件确实可以提供 trigger 实现,所以「装个包就好了」对它是个真假设。实测为假 ——
registerTrigger以解析后的类型为键(record_change/schedule/time_relative/api),而「作者写的 token → 解析后类型」这张表是resolveTriggerBinding里一串写死的startsWith/typeof判断,全链路没有任何注册表查询。没有任何包能教会引擎一个新的作者 token;而且该 flow 根本走不到「向谁要 trigger」那一步。四、语料实测(
origin/main,三个 example app 共 34 个 authored flow)机械跑了一遍(同时跑本 PR 判据与放宽判据):
flow.type分布:autolaunched24 ·schedule4 ·screen3 ·record_change2 ·api1triggerType缺失)命中:1 ——examples/app-todo的task_completionrecord_change类型共 2 个,showcase_urgent_task_alert接线正确(record-after-write)。那唯一的死 flow 已单独立卡 #6882(它的谓词还写在没人读的triggerCondition键上,所以「补个 token」并不是它的修法,需要对该 example app 的语义做判断)—— 按 dispatch 的约束,没有用基线条目把它压掉。五、Fixture 三分法:就地改写,不加豁免
仓库内两处 fixture 写了本规则所判之形状,均属「改写拼写」一类:
packages/cli/test/authoring-rule-command-parity.test.ts的triggerType: 'onCreate'(issue 点名的那一处)—— 该用例的植入缺陷是审批表达式,trigger token 只是顺带;留着会让用例靠一个它并不针对的 finding 通过。改为record-after-create。packages/lint/src/lint-flow-patterns.test.ts把 flow 类型回写进 trigger 槽的triggerType: 'record_change'—— 那不是一个作者 token,该 fixture 描述的是一个永不触发的 flow。改为record-after-update(与同一文件里相邻的守卫用例一致)。改写后,tracked tree 上本规则所判之形状为零。
六、消融验证(先写预测,再测)
新规则没有「改前」行为,所以方向就是常规的红向;三次消融各自预测并实测:
flags…、severity map 的非空性守卫、以及每条does NOT flag配对的植入坏值那一半flow.type === 'record_change'收窄a genuinely manual flow — autolaunched/screenconfig.triggerType != null存在性守卫ABSENT triggerType那一条;语料上应命中examples/app-todotask_completionB 有一点值得单独记下:既有的
does not flag non-record triggerTypes (schedule/api/manual)用例在消融 B 下仍然是绿的 —— 它只断言FLOW_TRIGGER_UNKNOWN_EVENT不出现,对新 id 是盲的。真正拦住这次越界的是本 PR 新加的手动 flow 判例。每条「这是合法的」断言都配了同一 fixture 上的植入坏值(如
record-after-update→record_after_update、autolaunched→record_change、删键 → 补键),所以绿不是空跑 —— 消融 A 下这些配对半边全部转红即为证据。七、门禁
pnpm --filter @objectstack/lint test→ 67 files / 1759 passedpnpm --filter @objectstack/lint typecheck、pnpm --filter @objectstack/cli typecheck→ cleanpackages/cliauthoring-rule-command-parity.test.ts(真实 spawn CLI 的消费者)→ 11 passedpackages/metadata-protocol五个 record_change flow 发布闸门用例 → 65 passednode scripts/check-nul-bytes.mjs→ OK;改动文件控制字符自扫 → cleaneslint改动文件 → clean八、刻意没做
triggerTypeis silently dead at every layer (no lint, no audit, no runtime warn) #3481 的 runtime 半边仍悬着:值得考虑的最小动作是让getTriggerBindingAudit能看见「声明了record_change却没解析出绑定」的 flow —— 目前它按!resolved跳过,恰恰把这一类漏掉。留给维护者与 flow record trigger: array-formtriggerTypeis silently dead at every layer (no lint, no audit, no runtime warn) #3481 一起裁决。docs/adr/**。examples/app-todo的死 flow 立卡 examples/app-todotask_completiondeclarestype: 'record_change'with notriggerType— the flow is dead, and its trigger condition is written to a key nothing reads #6882,未在本 PR 修。Generated by Claude Code