Skip to content

test(scripts): 补门禁——package.json files 声明的条目必须在磁盘真实存在 (#3663) - #3667

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-3663-files-exist-gate
Aug 7, 2026
Merged

test(scripts): 补门禁——package.json files 声明的条目必须在磁盘真实存在 (#3663)#3667
yinlianghui merged 1 commit into
mainfrom
claude/issue-3663-files-exist-gate

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#3663

问题

npm 对 package.jsonfiles不存在的条目是静默跳过的:不报错、不警告、退出码 0。于是「声明要打包 X、而 X 不存在」这种 manifest 与事实不符的状态,npm publishnpm pack、CI 全都发现不了。没有输出可读,所以这类漂移不是「不容易注意到」,而是不可见

#3647 就是该机制的标本:@object-ui/plugin-treefiles 写着 "LICENSE",而 packages/plugin-tree/LICENSE 根本不存在 —— 已发布的每一个 tarball 在整个包生命周期里都缺这份 MIT 文本。它是靠人工逐包核对 39 个包的 files 才被翻出来的(#3622 的排查副产物),不是靠门禁。PR #3662 补回了文件,本 PR 补的是让下一次不可能漏掉的那道门禁

改动

只新增一个文件:scripts/__tests__/package-files-exist.test.ts(纯静态 vitest 断言)。

不新开 workflow:scripts/**/*.test.ts 已被 vitest.config.mtsunit project 收进(第 166 行),随既有 CI 分片跑;.ts 也已被 tsconfig.scripts.jsonscripts/**/*.ts glob 覆盖,pnpm type-check:scripts 自动纳管。仓内同类门禁(如 workspace-peer-dependency-edges.test.ts)正是这个形态,从先例。

形态测量(origin/main @ dae1ac4)

先量后定策。pnpm-workspace.yaml 的四个根(packages/*apps/*examples/*docs)共 40 个包声明了 files,合计 159 条条目

关于 39 与 40:issue 正文的「39 包」指 packages/* 的 39 个目录,其中 vscode-extension 是 private 且没有files 字段,故 packages/* 实际声明 files 的是 38 个;另加 apps/console(非 private)与 examples/schema-catalog(private),合计 40。

条目类型全集(无一条使用 glob 或 ! 取反,全是字面路径):

条目出现次数磁盘(未构建 worktree)git 跟踪git-ignore判定
dist40❌ 不存在构建产物,豁免
README.md37源条目,严格校验
CHANGELOG.md36源条目,严格校验
LICENSE36源条目,严格校验
src3源条目,严格校验
templates2不存在⚠️存量违规,见下
src/styles.css1源条目,严格校验
src/schemas1源条目,严格校验
plugin.ts1源条目,严格校验
plugin.js1❌ 不存在构建产物,豁免
plugin.d.ts1❌ 不存在构建产物,豁免

合计 159 条:117 条在磁盘存在,42 条是合法缺席的构建产物,2 条是真实违规

豁免策略的取舍

不能简单地「路径必须存在」:159 条里 42 条是构建产物,在未构建的 worktree(CI 跑这条测试时的状态、以及任何人刚 clone 完的状态)里合法缺席。要求它们存在会让门禁对所有人恒红,而恒红的门禁会被删掉或 .skip

所以缺席条目只在仓库已经就别的理由声明过该路径是生成物时才豁免,三个条件同时成立:

  1. 被 git-ignore(.gitignore 说构建会写这里),
  2. 未被 git 跟踪(force-add 进来的被忽略文件是真文件,必须存在),
  3. 所属包有 build 脚本(一个什么都不构建的包,不可能正要生成什么)。

关键取舍:这个判据是「推导」出来的,不是手写清单。 备选方案是在测试文件里维护一张「受祝福的条目名」白名单(disttemplatesplugin.js …)。否决它的理由是:白名单必须靠人编辑才能保持正确,而**「掩盖一个真实缺陷」的那次编辑,和「教会门禁认识一个新构建目录」的那次编辑,长得一模一样**。改成推导,则豁免一条路径的唯一途径是在 .gitignore 里声明构建会生成它 —— 这是一个可评审的主张,错的时候错得很显眼。

按 PM 裁定,npm 恒包含的条目(LICENSE / README)不豁免 —— 门禁断言的是「声明与磁盘一致」,与 npm 是否兜底无关。#3662 已经证明:npm 的静默兜底正是潜伏机制本身。

本门禁刻意证明的事

不证明构建产物真的会被生成。证明那个需要真的跑一次构建,不是静态检查的职责,dist 在这里是采信了仓库的说法。这个缺口窄且被写明:#3647 那一整族缺陷 —— 源文件(LICENSE、README、CHANGELOG、srctemplates)被声明却不存在 —— 全部在门禁射程之内,而漂移实际发生的正是那里。

另外,files 支持 glob 与 ! 取反,今天一条都没用。单独有一条断言在第一条 glob 落地时报「教会这个 guard 展开 glob」,而不是把它误报成缺失文件。

⚠️ 落地基线不是绿的:2 条存量(需 PM 复裁)

全量扫描复核后,除 #3647另有 2 条同类存量,均为残留声明:

条目从未在 git 历史中存在build 会生成?模板实际位置
@object-ui/clitemplates✅ 从未❌ tsup 只出 dist/内联于 src/commands/init.ts:17 的对象字面量
@object-ui/create-plugintemplates✅ 从未❌ tsup 只出 dist/代码里逐字段构造,src/index.ts:115templateDir赋值后再无引用

今天没有用户会撞上(模板已内联,功能正常;npm 又静默跳过),失真的只有 manifest 本身。已按仓规开单 #3665(finding 标签,观察类)。

**处置为「挂账」,并说明原因:**本单文件面明确 ⛔ 不碰任何 package.json,而「先清」的正确修法恰恰是从两个 package.jsonfiles 里删掉这两行 —— 越出文件面。因此这里采用仓内既有的 ratchet 形态(scripts/i18n-call-site-key-baseline.json 确立、其自述为「A RATCHET, not an allowlist」):KNOWN_MISSING 挂账 2 条并指向 #3665,新增违规会红,而条目一旦被修好、变成陈旧条目也会红,逼着把该行删掉。所以这张表只能缩小,不会沉淀成永久白名单。

若 PM 复裁为「先清」(单独一个 PR 删掉那两行 files 条目),则本 PR 的 KNOWN_MISSING 应随之清空 —— 两种路径都收敛到同一个终态。

逆向验证(先预测,后跑)

五个方向,含一个反直觉的:

#扰动预测实测
A给 plugin-tree 的 files 塞一条不存在的 "NOTICE"存在性断言红,且点名包+条目;其余 4 条绿1 failed | 4 passed,报 @object-ui/plugin-tree declares "NOTICE" ... packages/plugin-tree/NOTICE does not exist✅ 命中
B删掉 packages/plugin-tree/LICENSE(重造 #3647 本体)两条红:存在性断言 + #3647 具名 pin2 failed | 3 passed,两条都点名✅ 命中
C建出 packages/cli/templates/(即修好#3665)反直觉方向:存在性断言保持绿,但 ratchet 断言转红报陈旧1 failed | 4 passed,红的正是 the objectui#3647 baseline only shrinks,报 A KNOWN_MISSING entry is stale✅ 命中
D摘掉 @object-ui/authbuild 脚本dist 失去豁免 → 红,证明判据第 3 条是承重的1 failed | 4 passed,报 ... packages/auth/dist does not exist (and the package has no 'build' script that could create it)✅ 命中
E全部还原5 绿Test Files 1 passed, Tests 5 passed✅ 命中

C 值得单独说:修好一个缺陷会让测试套件转红,直到把基线里那一行也删掉。 这是 ratchet 的设计意图(缺了这一半,陈旧条目会永远躺在那里,下一个读者会把它当成活着的缺陷),因为反直觉所以显式钉住。

反空绿

门禁自身也可能「什么都没检查却报绿」—— 这恰恰是 npm 静默跳过的同构失败。故有一条发现量下限断言:声明 files 的包数 ≥ 38、条目数 ≥ 150(实测 40 / 159),并按名字断言 @object-ui/plugin-tree 仍在扫描范围内且其 files 仍含 LICENSE。另外 git ls-files / git check-ignore 任一失败都抛错而非返回空集 —— 返回空集会让所有条目看起来「未跟踪」,从而悄悄放宽豁免,正是本文件存在的意义所反对的。

验证

$ pnpm exec vitest run scripts/__tests__/package-files-exist.test.ts
Test Files 1 passed (1)
Tests 5 passed (5)
$ pnpm exec vitest run scripts/__tests__/ # 全量 scripts 门禁套件,确认无干扰
Test Files 17 passed (17)
Tests 305 passed (305)
$ pnpm type-check:scripts # tsconfig.scripts.json 已自动纳管新文件
(exit 0)
$ npx eslint scripts/__tests__/package-files-exist.test.ts
(exit 0)
$ node scripts/check-control-bytes.mjs
✅ check-control-bytes: OK (scanned 3667 tracked text file(s); skipped 85 binary).

另:写这个文件时踩到了 tsconfig.scripts.json 头注记载的同一个坑 —— 在块注释里写一个含 */ 的 glob 会提前闭合注释。当时是用零宽空格绕开的,自查(逐码点扫 U+200B/U+FEFF/U+2028 等)发现后已改写措辞消除,不留不可见字符;上面 check-control-bytes 的 3667 是把新文件 git add 后的计数,确认它确实被扫到。

关于 changeset:判定为不加

AGENTS.md 第 151 行 —— 功能改进(feature)需写 changeset,纯 bug 修复不需要。本 PR 是纯测试/门禁改动,不改任何发布产物、不产生任何用户可见变化,连 bug 修复都不是。CI 也无「每个 PR 必须带 changeset」的门禁(changeset-guard.yml 只在 .changeset/** 变更时触发)。


🤖 Generated with Claude Code

https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt


Generated by Claude Code

…3663)
npm skips a missing `files` entry silently -- no error, no warning, exit
code 0 -- so a manifest can promise a path that is not in the tarball and
nothing downstream ever says so. objectui#3647 was that mechanism's
specimen: @object-ui/plugin-tree declared "LICENSE" while
packages/plugin-tree/LICENSE did not exist, and every published tarball
shipped without it for the package's entire life. It was found by a human
diffing all 39 packages by hand, not by any gate.
This adds the gate. Every workspace package's `files` entries must resolve
on disk, unless the repo has already declared the path to be build output:
git-ignored, not git-tracked, and belonging to a package with a `build`
script. That criterion is derived from git rather than hand-listed, so the
only way to exempt a path is to state in .gitignore that a build produces
it -- an edit that silences a real defect cannot masquerade as one that
teaches the guard about a new build directory.
Measured on main@dae1ac41e: 40 packages declare `files`, 159 entries, 42
of them legitimately-absent build output. Two entries are real pre-existing
defects -- @object-ui/cli and @object-ui/create-plugin both declare a
`templates` directory that has never existed in this repo's history. They
are carried in a KNOWN_MISSING ratchet keyed to objectui#3665, in the shape
scripts/i18n-call-site-key-baseline.json established: a new violation fails
the build and a stale entry fails it too, so the map can only shrink.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
@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)
objectuiIgnoredIgnoredAug 7, 2026 5:58pm

Request Review

@yinlianghui
yinlianghui marked this pull request as ready for review August 7, 2026 18:04
@yinlianghui
yinlianghui added this pull request to the merge queueAug 7, 2026
Merged via the queue into main with commit fe4d4daAug 7, 2026
17 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-3663-files-exist-gate branch August 7, 2026 18:05
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

没有门禁校验「package.json 的 files 条目在磁盘上真实存在」——npm 静默跳过缺失条目,#3647 因此长期无人察觉

2 participants

@yinlianghui@claude