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

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-consistencyexit 0,两份体检均报出
npx eslint scripts/check-override-consistency.mjs干净
node scripts/check-nul-bytes.mjsOK(5834 个文件,无裸控制字节)
grep -naP 自查改动三个文件的控制字节无命中
node scripts/check-workflow-status-functions.mjsOK(工作流改动后复跑)

八、范围


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

vercelBot commented Aug 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 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 queueAug 7, 2026
Merged via the queue into main with commit 0664488Aug 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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cddependenciesPull requests that update a dependency filesize/mskip-changesetPR 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

@hotlong@claude