Skip to content

fix(devx): check:nul-bytes 把「已写入但尚未 git add」的文件纳入扫描面 (#6984) - #7104

Merged
os-project-manager merged 1 commit into
mainfrom
claude/issue-6984-nul-bytes-untracked
Aug 9, 2026
Merged

fix(devx): check:nul-bytes 把「已写入但尚未 git add」的文件纳入扫描面 (#6984)#7104
os-project-manager merged 1 commit into
mainfrom
claude/issue-6984-nul-bytes-untracked

Conversation

@os-project-manager

Copy link
Copy Markdown
Collaborator

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-ignoredM 为 0 时照样打印 —— 否则 CI 的绿和 pnpm check:nul-bytes enumerates git 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 之后:

$ git ls-files -z --others | tr '\0' '\n' | grep -c .
76122
$ git ls-files -z --others --exclude-standard | tr '\0' '\n' | grep -c .
0

它是剪枝而不是遍历:16ms,对比索引枚举的 5ms。EXCLUDED 仍叠加在上面,所以有人把 node_modules 取消忽略也点不红。没有为了让它安静而收窄修复 —— 它本来就是安静的。

2. 真仓库上的前后对照(同一棵树,同一个未跟踪的 packages/cli/test/probe-6984.test.ts,内含裸 0x1b):

--- BASELINE(origin/main 的枚举)---
check-nul-bytes: OK (scanned 6539 tracked text file(s); ...).
baseline exit=0

--- NEW ---
check-nul-bytes: 1 file contains a raw ASCII control byte
  • packages/cli/test/probe-6984.test.ts:1:14 [untracked] -- 1 occurrence of 0x1b, first at byte offset 13
new exit=1

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() 的未跟踪那一半,样本就不在扫描集里,新钉子应当转红。实跑:

✗ check-nul-bytes --self-test -- 6 failure(s)
  • #6984: an untracked-but-not-ignored file carrying a raw 0x1b must be flagged
  • #6984: an offender found in the untracked half is marked untracked
  • #6984: the offending byte is reported as 0x1b, got undefined
  • #6984: 3 untracked paths enumerated and all 3 scanned as text, got 0/0
  • #6984: the untracked half is reported by path, got
  • #6984: staging changes no verdict -- the same files are flagged either way

诚实的负面结论:两条否定式断言(被忽略的样本不判红、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.jsonscripts/**/* 是包内相对路径),因此不构成 tsc 输入;本地未跑该 re-measure,以 CI 上 TypeScript Type Check 的真实结论为准。


Generated by Claude Code

`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
@vercel

vercel Bot commented Aug 9, 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 9, 2026 3:56pm

Request Review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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.

pnpm check:nul-bytes enumerates git ls-files, so a brand-new file reads green locally until it is staged

1 participant