fix(devx): check:nul-bytes 把「已写入但尚未 git add」的文件纳入扫描面 (#6984) - #7104
Merged
Conversation
`scan()` 原本只枚举 `git ls-files`,也就是索引。一个刚写出来、还没 `git add` 的文件根本不在扫描集里,门禁照样 exit 0 并打印它惯常的成功 行 —— 读起来是「这棵树没有裸控制字节」,而不是「你刚写的那个文件没被 看过」。 这个洞的形状恰好等于本门禁自己的事故源:每一例裸字节都来自编辑工具在 作者「正在写关于这个字节的内容」时把转义落成真字节(#4763 / #4890 / PR #5140 / #5460 的 issue 正文),而落点必然是一个**新文件**。于是 agent 指令要求的那次「push 前跑一遍」,正是最可能在看一个索引枚举够不 到的文件的那次运行。#6984 实测过:一个未跟踪文件里躺着裸 0x1b, AGENTS.md 的 self-scan 找得到,本门禁绿,唯一的差别是索引。 - 扫描集改为「索引 + `--others --exclude-standard`」,即 git 既不跟踪 也不忽略的工作区文件。 - 成功行改为同时报出两半:`scanned N text file(s) -- N tracked, M untracked-not-ignored`。M 为 0 时**照样打印**,否则 CI 的绿和 #6984 之前那种绿会渲染成同一句话,而后者正是本单要消掉的假信心。 - 未跟踪的命中在报错行上标 `[untracked]`——「还没 add」是作者拿到 一个被拒文件后第一个需要知道的事实。 - `--list` 逐条列出未跟踪的那一半路径。 三项实测,不是断言: 1. `--exclude-standard` 让本地垃圾留在外面。本仓库在完整 `pnpm install` 之后,未跟踪路径 76122 条,过 `--exclude-standard` 之后 0 条;它是剪 枝而不是遍历,16ms vs 索引枚举的 5ms。EXCLUDED 仍叠加在上面,所以有 人把 node_modules 取消忽略也点不红。 2. CI 里是 no-op:workflow 检出的是一个 commit,全树受跟踪,未跟踪那一 半恒为空。`--self-test` 用「把样本 stage 掉再扫一遍」证明,而不是在 注释里声称。 3. 反向:回退 `enumerate()` 的未跟踪那一半,6 条新断言立刻转红。 `--self-test` 断言数 48 → 75。临时仓库额外把 `core.excludesFile` 指向 一个空文件,否则开发者的全局 ignore 会让「EXCLUDED 仍然生效」那条样本 因为另一个原因通过。 工具链改动,不涉及任何 package,不发版 —— 走 `skip-changeset`,与近期 scripts/ + workflow-only PR 的做法一致(#7048 / #7008 / #6983 / #6917)。 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
|
os-project-manager
marked this pull request as ready for review
August 9, 2026 17:09
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 #6984
问题
scan()原本只枚举git ls-files,也就是索引。一个刚写出来、还没git add的文件根本不在扫描集里,门禁照样 exit 0 并打印它惯常的成功行 —— 读起来是「这棵树没有裸控制字节」,而不是「你刚写的那个文件没被看过」。这个洞的形状恰好等于本门禁自己的事故源:每一例裸字节都来自编辑工具在作者「正在写关于这个字节的内容」时把转义落成真字节(#4763 / #4890 / PR #5140 / #5460 的 issue 正文),而落点必然是一个新文件。于是 agent 指令要求的那次「push 前跑一遍」,正是最可能在看一个索引枚举够不到的文件的那次运行。
实施本 PR 的过程中又复现了一次:写
enumerate()的反向验证时,一枚裸 NUL 落进了scripts/check-nul-bytes.mjs第 994 行,grep当场把整个文件判成 binary(grep: scripts/check-nul-bytes.mjs: binary file matches)。这是第五次记录在案的同源事故,也是本单价值的现场证据。改动(triage 已定的 shape 1)
git ls-files --others --exclude-standard」,即 git 既不跟踪也不忽略的工作区文件。两次调用而非一次--cached --others,因为两半要分别计数。scanned N text file(s) -- N tracked, M untracked-not-ignored。M 为 0 时照样打印 —— 否则 CI 的绿和pnpm check:nul-bytesenumeratesgit ls-files, so a brand-new file reads green locally until it is staged #6984 之前那种绿会渲染成同一句话,而后者正是本单要消掉的假信心。[untracked]。「还没 add」是作者拿到一个被拒文件后第一个需要知道的事实。--list逐条列出未跟踪的那一半路径。.github/workflows/lint.yml的步骤注释同步(它此前写的是 "every tracked TEXT file")。实测,不是断言
1.
--exclude-standard让本地垃圾留在外面。 本仓库在完整pnpm install之后:它是剪枝而不是遍历:16ms,对比索引枚举的 5ms。EXCLUDED 仍叠加在上面,所以有人把
node_modules取消忽略也点不红。没有为了让它安静而收窄修复 —— 它本来就是安静的。2. 真仓库上的前后对照(同一棵树,同一个未跟踪的
packages/cli/test/probe-6984.test.ts,内含裸 0x1b):3. CI 里是 no-op —— 证明而非声称。 workflow 检出的是一个 commit,全树受跟踪,未跟踪那一半恒为空。
--self-test里把上面那批样本git add掉再扫一遍,断言staged.untracked === 0、offender 集合逐路径相同、成功行仍写着0 untracked-not-ignored。真仓库上同样为 0(见上面的成功行)。Fixture
样本写在
git add之后,这是 fixture 的全部机制 —— stage 掉就等于把盲点重建一遍而不是去踩它:packages/cli/test/new-case.test.ts—— 未跟踪、裸 0x1b,判红,并且untracked === true。packages/cli/test/new-case-fixed.test.ts—— 同一个字面量写成转义文本,保持绿。门禁教作者写转义,这条处方在新的这一半上必须同样可测。local-junk/scratch.md+ 一个同样未跟踪的.gitignore—— 被忽略的文件连枚举都进不去(git 照读未跟踪的.gitignore,这正是「被忽略」和「尚未 stage」是两种状态而不是一种)。node_modules/pkg/index.js—— 该临时仓库的.gitignore故意不写 node_modules,所以这条断言钉的是 EXCLUDED 本身。core.excludesFile指向一个空文件(放在.git/内,不在扫描面上),否则开发者的全局 ignore 会让上面那条样本因为另一个原因通过。反向验证 —— 方向是事先定的:红
预测:回退
enumerate()的未跟踪那一半,样本就不在扫描集里,新钉子应当转红。实跑:诚实的负面结论:两条否定式断言(被忽略的样本不判红、EXCLUDED 的样本不判红)在回退后依然是绿的,因为枚举为空时它们空转通过。它们本身证不了任何事 —— 真正兜住这个洞的是那条计数/路径断言(
got 0/0),这也是它存在的理由。--self-test断言数 48 → 75。发版
工具链改动,不涉及任何 package,不产出 CHANGELOG 条目 —— 走
skip-changeset而不是 changeset。这与近期 scripts/ + workflow-only PR 的一致做法相同(#7048 / #7008 / #6983 / #6917 均无 changeset),也与check:empty-changeset的判例一致:空 frontmatter 的 changeset 是changesets/action的真实输入,比标签多担一份 #4898 的风险而不多买任何东西。本地验证
check:nul-bytes/check:changeset-gate-self-tests/check:required-contexts/check:workflow-status-functions/check:node-version/check:shard-attestation/check:merge-driver/pnpm lint全绿。check:type-check-debt需要先构建整个 workspace 闭包,本 PR 的 diff 里.ts文件数为 0,根目录scripts/也不在任何 tsconfig 的 include 内(packages/spec/tsconfig.scripts.json的scripts/**/*是包内相对路径),因此不构成 tsc 输入;本地未跑该 re-measure,以 CI 上TypeScript Type Check的真实结论为准。Generated by Claude Code