Skip to content

feat(scripts): check-override-consistency 增加 override 消费者体检与自失效上界报告 - #6093

Merged
hotlong merged 2 commits into
mainfrom
claude/issue-6046-override-consumer-visibility
Aug 7, 2026
Merged

feat(scripts): check-override-consistency 增加 override 消费者体检与自失效上界报告#6093
hotlong merged 2 commits into
mainfrom
claude/issue-6046-override-consumer-visibility

Conversation

@hotlong

@hotlong hotlong commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Fixes #6046

#5835 裁 A 的配套正交项落地:scripts/check-override-consistency.mjs 增加两份体检,
都是 report,一律不改退出码

一、前提复核(落地前对 origin/main 实测,非照抄 issue 正文)

issue 正文的说法 实测结果 结论
form-data#5825 失去唯一消费者、现为空转 依赖树中 0 个消费者 ✅ 成立
undici 同样空转 2 个消费者:@ai-sdk/provider-utils@5.0.16 依赖 undici 7.29.0;jsdom@30.0.1 依赖 undici 8.9.0 不成立,undici 仍在树里
空转的就这两条 实际 4 条:form-data@tootallnate/oncereact-router@sveltejs/kit ⚠️ issue 低估了

两点说明:

  • issue 的核心前提成立且比正文更强 —— 「空转 override 完全不可见」正是被这次实测坐实的:
    除了正文点名的 form-data,另有三条空转 override 同样没人发现。立项理由不但没被推翻,还更充分。
  • undici 仍然被点名,但走的是第二份体检(自失效上界形状),而不是「零消费者」。
    验收要求的「两条 override 都被点名报告、exit 仍 0」在实跑里成立,只是 undici 的点名理由
    换成了真实的那个。⛔ 没有为了凑 issue 正文而把 undici 硬报成零消费者。

二、设计:为什么消费者普查做在包名一级

一个很自然的替代方案是「统计落在 override selector 范围内的解析数」。这个方案是错的,
因为 pnpm-lock.yaml 记录的是 override 生效之后的解析结果:一条正在干活的 override
早就把命中项抬到了 target 之上、也就抬出了自己的 selector。

实测:28 条 override 里,落在自己 selector 内的解析数全部为 0 —— 正在干活的那些也一样。
也就是说该信号根本分不出「空转」和「正在生效」,一用就是全员误报。

包名的有无是 lockfile 唯一能诚实给出的信号:名字在 importers + snapshots 里都不出现,
这条 override 就确实没有任何可改写的对象。脚本头注释里把这条限制写死了,免得下一个人再走一遍。

三、为什么是 report 而不是 red

裁决原文即「报告而非 red」。空转 override 是合法的纵深防御姿态 —— 不花成本,且一旦哪个依赖
把包重新引进来就立刻生效。所以体检输出里带了明确的保留指引,防止下一个 agent 顺手删:

#5835 ruling A decided exactly this for the form-data / undici OSV pins — KEEP THEM.
Do not "tidy up" an entry listed here without a ruling that says to.

脚本语言:scripts/*.mjs 全族 0 处 CJK(实测),故脚本输出沿用英文保持族内一致,
中文说明放在 PR / commit 里 —— 与仓库「GitHub 可见文本用中文」的约定并不冲突。

四、self-test:17 条断言,双向

接进新增的 check:override-consistency 命令链,validate-deps.yml 改调该命令链而非裸脚本
(不接线即 phantom check)。除新增两份体检外,顺带把既有的 declared-range 主规则也补上了
双向覆盖
—— 这个脚本此前完全没有 self-test。

  ✓ lockfile parses into a consumer index
  ✓ unparseable lockfile -> census skipped, never a crash
  ✓ lockfile that is not a mapping -> census skipped
  ✓ transitive consumer present (snapshot pulls undici) -> NOT reported
  ✓ workspace importer counts as a consumer (semver) -> NOT reported
  ✓ zero consumers (nothing pulls form-data) -> REPORTED
  ✓ census reports only the idle one out of a mixed set
  ✓ bound equal to the target floor (the #5032 undici shape) -> REPORTED
  ✓ bound below the target floor (uncovered gap) -> REPORTED
  ✓ bound above the target version line (the durable shape) -> NOT reported
  ✓ selector with no upper bound -> NOT reported
  ✓ inclusive upper bound covers the target -> NOT reported
  ✓ declared range that reaches the target -> no violation
  ✓ declared range that cannot reach the target -> violation
  ✓ no implicit prereleases: ^1.7.0 does not reach 1.7.0-rc.2
  ✓ declaration outside the selector scope -> override does not apply
  ✓ declaration inside the selector scope -> override applies

✓ self-test passed (17 assertions)

五、现状实跑(pnpm check:override-consistency,exit 0)

[report] Consumer census: 4 of 28 override(s) name a package NOTHING in the dependency tree depends on.
  - 'form-data@<4.0.6': '>=4.0.6'  — 0 consumers
  - '@tootallnate/once@<2.0.1': '2.0.1'  — 0 consumers
  - 'react-router@<8.3.0': '^8.3.0'  — 0 consumers
  - '@sveltejs/kit@<2.69.1': '^2.69.1'  — 0 consumers

  These are REPORTS, not failures. An override with no consumer today is a
  legitimate defence-in-depth posture: it costs nothing and pins the version
  in advance if any dependency reintroduces the package. #5835 ruling A
  decided exactly this for the form-data / undici OSV pins — KEEP THEM.
  Do not "tidy up" an entry listed here without a ruling that says to.

[report] Self-expiring selectors: 26 of 28 override(s) use a selector that excludes their own target.
  - 'minimatch@<10.2.3' -> '10.2.3'
  - 'tar@>=2.0.0 <7.5.11' -> '^7.5.11'
  - 'form-data@<4.0.6' -> '>=4.0.6'
  - 'undici@>=7.23.0 <7.29.0' -> '^7.29.0'
  - 'better-auth@<1.7.0-rc.2' -> '1.7.0-rc.2'
  ...(better-auth 家族等 20 行省略)...
  - '@hono/node-server@<2.0.5' -> '^2.0.10'   (gap: 2.0.5 .. 2.0.10 matches nothing)
  - 'fast-uri@<3.1.5' -> '^3.1.5'
  - 'hono@<4.12.34' -> '^4.12.34'

  Also REPORTS, not failures. ...

✓ 2 published-manifest declaration(s) covered by pnpm-workspace.yaml overrides all resolve to their override targets.

⚠️ 需要维护者留意的一个量级问题:自失效上界形状实测命中 26 / 28 —— 它是本文件的
既有普遍写法,不是本次新增的回归。所以这一栏被刻意写成「standing ledger」的口气,每行只留
key -> target(逐行重复的解释合并到段末讲一次),只有严格更差的那一种(上界低于 target
下限、中间留出谁都不覆盖的空窗)才额外标 gap,当前只有 @hono/node-server 一条。
若维护者希望把这一栏收窄(例如只报 OSV 安全 pin、跳过 better-auth 预发布兼容 pin),
可另开跟进单 —— 本单按裁决「点名 undici 形状」的字面要求实现,而 undici 的形状恰好就是
这 25 条共享的那一种,所以要点名 undici 就必然要点名同形状的其余各条,不能只挑一条报。

六、反向验证(方向在跑之前先声明)

方向 预言 实测 结果
1. 删掉普查(findIdleOverrides 直接返回空) 「零消费者报出」+「混合集只报空转那条」2 条转红 正好这 2 条红,其余 15 绿 ✅ 符合
2. 给 form-data 在 fixture 里造一个真实消费者 同样 2 条转红(不再被报为空转) 正好这 2 条红 ✅ 符合
3. lte 改成 lt 恰好 1 条红(undici 等值上界),gap 那条保持绿 正好 1 条红,gap 绿 ✅ 符合
4. 真 lockfile 上注入一条 form-data 消费边(内存态,不落盘) 普查从 false 翻成 true 翻转成立,git status 确认 lockfile 未被改动 ✅ 符合

方向 2 第一次跑出来是全绿,但那是探针坏了不是断言不敏感:替换目标写成了
'undici: 7.29.0',,而源码里数组元素是 ' undici: 7.29.0',(引号在空格之前),
replace 静默变成 no-op。加了「替换目标必须命中、替换必须改变文件」的守卫后重跑才得到上表结果。
⛔ 没有把那次假绿当成结论 —— 记在这里是因为「mutation 没生效」和「断言不敏感」在输出上长得一模一样。

方向 3 的预言之所以精确到「恰好 1 条」,是要证明 undici 是被 lte 里的等值那一半接住的,
而不是碰巧被 gap 那半覆盖 —— 若两条同时转红,说明断言没有真正区分这两种子形状。

七、门禁

门禁 结果
pnpm check:override-consistency exit 0,两份体检均报出
npx eslint scripts/check-override-consistency.mjs 干净
node scripts/check-nul-bytes.mjs OK(5834 个文件,无裸控制字节)
grep -naP 自查改动三个文件的控制字节 无命中
node scripts/check-workflow-status-functions.mjs OK(工作流改动后复跑)

八、范围


Generated by Claude Code

#5835 裁 A 的配套正交项(#6046):override 只在「可发布包 declared range 里
出现过」时才被校验,没有消费者的 override 会被直接跳过 —— 于是一条不可见的安全
声明两头都危险,既可能被误信为在生效,也可能被下一个 agent 顺手删掉。

新增两份体检,都是 report,⛔ 一律不改退出码:

1. 消费者普查:从本地 pnpm-lock.yaml 的 importers + snapshots 统计每条 override
   的消费者数,零消费者点名报告,并写明「#5835 裁 A 决定保留」的姿态指引。
   普查刻意做在包名一级:pnpm-lock.yaml 记的是 override 生效之后的解析结果,
   一条正在干活的 override 早已把命中项抬到 target 之上、抬出了 selector,
   实测「落在 selector 内的解析数」对 28 条 override 全为 0(包括在干活的),
   根本分不出「空转」和「正在生效」。包名的有无是 lockfile 唯一能诚实给出的信号。

2. 自失效上界:selector 的排他上界 `<V` 落在 target 下限之上或与之相等时点名 ——
   target 所在版本线整条不在 scope 内,target 自身出公告要挪时 pin 会当场失配,
   即 #4961 / #5032 里 `undici@>=7.23.0 <7.28.0` 的形状。

`--self-test` 17 条断言双向立住(有消费者不报 / 零消费者报出 / 自失效形状报出 /
上界高于 target 版本线不报 / lockfile 不可解析时降级不崩),并把既有的 declared
range 主规则一并补上双向覆盖。self-test 接进新增的 `check:override-consistency`
命令链,validate-deps 工作流改调该命令链而非裸脚本 —— 不接线即 phantom check。

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

vercel Bot commented Aug 7, 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 7, 2026 1:36am

Request Review

…rride-consumer-visibility

# Conflicts:
#	package.json
@hotlong
hotlong marked this pull request as ready for review August 7, 2026 01:45
@hotlong
hotlong enabled auto-merge August 7, 2026 01:45
@hotlong
hotlong added this pull request to the merge queue Aug 7, 2026
Merged via the queue into main with commit 0664488 Aug 7, 2026
23 of 24 checks passed
@hotlong
hotlong deleted the claude/issue-6046-override-consumer-visibility branch August 7, 2026 02:00
qq9340100 pushed a commit that referenced this pull request Aug 7, 2026
拉入 PR #6093(0664488)落地的 check-override-consistency 消费者体检,
以便用合入后的真体检报告复核本单枚举的 12 条成员。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd dependencies Pull requests that update a dependency file size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

check-override-consistency 增加「override 有无消费者」体检(报告而非 red)—— #5835 裁 A 的配套正交项

2 participants