Skip to content

feat(lint): never-fire 族三条规则 warning → error,注册表 tier 同步 gating (#5762) - #5952

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-5762-neverfire-severity
Aug 6, 2026
Merged

feat(lint): never-fire 族三条规则 warning → error,注册表 tier 同步 gating (#5762)#5952
os-zhuang merged 1 commit into
mainfrom
claude/issue-5762-neverfire-severity

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes #5762

按维护者 2026-08-06 批复(三问全批)执行。族内同批定级,单独 PR,不逐条搭车。

定级标准:一个问题,不是「后果有多严重」

本文件的规则此前一律 warning。复审后按一个标准重新定级 —— 仅凭这份 stack,是否已经足够断定这个 flow 不会运行。按后果排序是没用的:这里每条规则描述的都是「不运行的 flow」。

升 error(三条)

规则 判据归属 为什么没有别的读法
flow-time-relative-descriptor-invalid (#5496) TimeRelativeTriggerSchema 判它的正是 trigger 在 bind 时 safeParse同一个 schema —— schema 拒的描述符运行时同样被拒,sweep 永不安装
flow-time-relative-descriptor-unroutable (#5647) 引擎的路由谓词 typeof … === 'object' 判据就是引擎自己的谓词,任何部署都不会把它路由到 time-relative trigger;全族最硬,也是唯一连 bind 时那行 warn 都没有的一条
flow-trigger-unknown-event (#3427/#3457/#3481) triggerTypeToHookEvents 的封闭文法 见下,随批复审的实测结论

三条的共同点:判据都是本仓库内发布的契约,装任何包都改不了答案。

维持 warning(两条,反例)

这两条正是本次不是「整个文件都升 error」的原因,已作为反例钉进新增测试:

  • flow-trigger-unknown-object —— 本 stack 没定义的对象名可能由另一个已安装包提供,而规则看不见那个包的对象。免责是真的,规则自己的 hint 就这么写(新增测试断言:error 级 finding 的 hint 里不得出现 "another installed package" / "can be ignored",warning 级必须出现)。⛔ 按裁定未动。
  • flow-draft-status-ambiguous —— draft flow 确实会触发(只有 obsolete/invalid 停用),这是意图歧义而非死 flow。升它会拒收一个所有 flow 都正常的 stack。

随批复审:flow-trigger-unknown-event 实测为同性质,故同 PR 升

裁定要求实测其性质:若同为「确定性死 flow、无跨包免责」则同 PR 升;若有真 advisory 理由则维持并写进报告。实测结论:同性质,无跨包免责。 证据:

  1. 路由不可扩展。 engine.ts:1532硬编码前缀判定 triggerType.startsWith('record-') 即路由给 record_change —— 不查注册表。所以装任何包都无法声明一个新的 record-* token。
  2. 文法封闭且本地。 record-change-trigger.ts:115/^record-(before|after)-(create|insert|update|delete|write)$/ 与本规则的 VALID_RECORD_TRIGGER 逐字对应;不匹配即 hookEvents.length === 0,绑不上任何 hook。
  3. 无第二个提供方。 全仓 record_change trigger 只有 packages/triggers/trigger-record-change 一个实现,没有可替换的竞争实现给出「另一种读法」。
  4. 数组形式同理,且引擎那段注释本就把本规则点名为 primary catch —— 一个只 warn 的 primary catch 正是 tier 机制要消除的「declared ≠ enforced」。

对照 flow-trigger-unknown-object:对象名的跨包免责是结构性的(对象由包提供、规则看不见),而 token 文法的免责不存在(文法不由安装决定)。两者不同性质,故一升一留。

影响面:P1 运行时发布门 —— 实测边界与立单时的假设不同

注册表 tier advisorygating(两条 surface 本就是 CLI_AND_RUNTIME)。error 会让 state: 'active' 的元数据写入被拒。

⚠️ 立单/派发时写的影响面是「升 error 后租户保存任意 flow 时既有死 flow 被连带拒收」—— 实测该说法不成立,方向恰好对租户有利。 validateFlowTriggerReadiness(stack) 确实收整个 stack,但运行时门从不把租户的整份 flow 集合交给它:

  • runtime-gate.ts 构造的候选快照是 candidate = { objects, [stackKey]: [item] } —— flow 写入时 flows 数组里只有正在写入的那一个 item;
  • 门还会做差分,减掉 baseline(不含该 item 的上下文)已产出的 finding;
  • RuntimeStackContext 只有 objects 一个通道 —— 已存储的 flow 在结构上进不了快照

实测(直接调 runRuntimeAuthoringRules):

publishing the DEAD flow itself
  errors=1  errorRules=["flow-time-relative-descriptor-unroutable"]
publishing a GOOD flow (dead one already stored elsewhere)
  errors=0  errorRules=[]

真实影响面:

zod history 句噪音(裁定第三问):已实测,本 PR 折叠,理由如下

裁定允许「若碍事则同 PR 折叠展示」。实测噪音是真的:

折叠机制在本 PR 的文件面内都不干净,故未做:

  1. 规则内按结构剥离(正则匹配 Unrecognized key(s) on …: …. + history 句 + Did you mean …?)= 把生产者的消息结构复制进消费者 —— 恰是本文件模块注释声明要避免的漂移,也违反 contract-first;
  2. 按长度截断会把最有用的 Did you mean 切掉(它在末尾),使情况更糟;
  3. 生产者侧没有可查询的把手:strictObjectDeclarations() / directAliasTables()刻意不进 barrel(内部接缝),消费者拿不到 history 原文;
  4. 干净的修法在生产者(strictObject 把 history 排到末尾或单独暴露),但那涉及 62 个 strictObject 站点 / 293 处 history 声明与跨包钉死测试,是 spec 的授权错误契约对外变更,应单独立单、单独拍板。

已另立 finding 单记录测量与建议。不在本 PR 内以「消费者容忍」的方式绕过。

反向验证(方向先预判,两个不同机制,均如预判转红)

  1. tier 声明非自述。 把 tier 改回 advisory(严重度保持 error)→ 预判 authoring-rule-wiring.test.ts 的 "every advisory rule really is advisory" 转红(它读规则源码核对声明)。实测:× every advisory rule really is advisory / A rule that can emit `error` is `gating` and must run on all three commands.
  2. 严重度断言非空转。…-unroutable 降回 warning → 预判新增 severity map 测试转红。实测 2 条红:× flow-time-relative-descriptor-unroutable is error标量 config.timeRelative(如 timeRelative: 'daily')= 引擎解析不出任何 trigger,flow 永不触发且全层零输出 #5647 原有那条。

仓内自查:三个示例 app 的 os validate A/B

裁定要求「若任何 in-repo example/fixture 因此转红,在同 PR 内修掉」。实测无一转红,无需修改。

example A(升级前) B(升级后) 输出
app-showcase exit 0 exit 0 逐字一致
app-crm exit 0 exit 0 逐字一致
app-todo exit 0 exit 0 逐字一致

原因可从 diff 直接读出:规则文件的非注释改动只有 severity: 字面量,判据一行未动,所以任何 stack 的 finding 集合main 逐字节相同,只有四处 severity 字段变了。三个示例本就不产出这三条 id 的 finding(showcase 唯一的 timeRelative 描述符是已被测试钉为 spec-valid 的 Task Due Reminder;全部 record-* token 都在文法内)。app-todo 的两条 flow-draft-status-ambiguous 仍是 warning —— 正是维持 advisory 的那条在真实示例上的体现。

record-created(全仓 12 处、文法外)全部位于其他规则的测试 fixture 里,那些测试直接调用各自的规则、不经过本规则,故不受影响(已逐一核对;validateFlowTriggerReadiness 不在 reference-integrity suite 成员内)。

文档

content/** 无任何 lint 规则 severity 表引用这一族 id(已实测搜索),packages/lint/src/docs 不存在,故无同步项。⛔ 未动 content/docs/releases/**

发布

真 changeset:@objectstack/lint minor(已发布规则严格度的对外行为变更),正文含升级原因、P1 实测边界与迁移指引(修死 flow,而非降级规则)。未用 skip-changeset


Generated by Claude Code

`validateFlowTriggerReadiness` 的规则此前一律 warning。族内复审后按**一个**标准
重新定级 —— 仅凭这份 stack 是否已足够断定 flow 不会运行 —— 三条答「是」升 error:

- `flow-time-relative-descriptor-invalid`(#5496):判它的正是 trigger 在 bind 时
  `safeParse` 的同一个 `TimeRelativeTriggerSchema`,schema 拒的描述符运行时同样被
  拒,sweep 永不安装。
- `flow-time-relative-descriptor-unroutable`(#5647):判据就是引擎自己的路由谓词
  `typeof … === 'object'`,任何部署都不会把它路由到 time-relative trigger ——
  全族最硬的判定,也是唯一连 bind 时那行 warn 都没有的一条。
- `flow-trigger-unknown-event`(#3427/#3457/#3481):引擎用**硬编码前缀**把任何
  `record-` token 路由给 record-change trigger(不查注册表,故装任何包都无法声明
  新 `record-*` token),该 trigger 再用 `triggerTypeToHookEvents` 的**封闭文法**
  映射 —— 文法外即零 hook event。与 unknown-object 不同,没有跨包免责的读法。

两条对照维持 warning,这正是本次不是「整个文件都升 error」的原因:
`flow-trigger-unknown-object` 的跨包免责是真的(对象可能由另一个已安装包提供,
规则看不见);`flow-draft-status-ambiguous` 的 draft flow **确实会**触发,是意图
歧义而非死 flow。两者作为反例钉在新增的 severity map 测试里。

注册表 tier `advisory` → `gating`。tier 声明并非自述:`authoring-rule-wiring.test.ts`
会读规则源码核对,故留 advisory 必红 —— 已反向验证。

P1 运行时发布门边界经实测,与直觉相反且对租户有利:门交给规则的快照里 `flows` 只装
正在写入的那一个 item,且减掉 baseline 的 finding,`RuntimeStackContext` 也只有
`objects` 一个通道 —— 已存储的 flow 进不了快照。实测:发布死 flow 自身 errors=1
(拒收),已存死 flow 时发布好 flow errors=0(不受连带)。被拒的是死 flow 自己的
发布,以及 CLI 侧含死 flow 的包。

仓内 showcase / CRM / todo 三个示例 `os validate` 升级前后输出逐字一致、均通过,
无需修改任何示例。

Claude-Session: https://claude.ai/code/session_01GX3sL71LFq8m2usg6VqTSE
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 6, 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 6, 2026 12:54pm

Request Review

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

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/automation/hook-bodies.mdx (via @objectstack/lint)
  • content/docs/permissions/authorization.mdx (via @objectstack/lint)
  • content/docs/releases/v17.mdx (via @objectstack/lint)

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 6, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review August 6, 2026 13:04
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 6, 2026
Merged via the queue into main with commit 5582e18 Aug 6, 2026
24 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-5762-neverfire-severity branch August 6, 2026 13:15
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.

lint never-fire 族 severity 复审:两条 warning 实感更接近 error(#5496/#5647 dev 报告建议)—— 同批升、单独 PR,不逐条搭车

2 participants