From c68f3b3b0559a752db5da841ba6d89bfe1a32de1 Mon Sep 17 00:00:00 2001 From: oratis Date: Sun, 9 Aug 2026 23:11:55 +0800 Subject: [PATCH] chore(git): force textual diffs so a stray NUL can't hide a file from review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #27 修了 `server/usage-ledger.js` 里两个裸 `0x00`(复合键分隔符本该写成 `\u0000` 转义)。真正的教训不在那两个字节,而在于**它是怎么活到 merge 的**: NUL 一旦落进文件前 8000 字节,git 就把整个文件判成二进制,`git diff` 退化成 `Bin N -> M bytes`,`git blame` 完全失效 —— 这个 bug 把自己藏起来了。 它也不需要谁疏忽。任何一个会吞反斜杠转义的管线都足以产生它(这次修复过程中, 同样的转换在临时脚本和 commit message 草稿里各发生过一次)。所以值得做结构性 兜底,而不是只修实例。 改动: - 新增 `.gitattributes`:给所有源码 / 文档扩展名加 `diff` 属性。git 文档明确 说明该属性让路径「即使包含正常文本里绝不出现的字节值(如 NUL)也按文本处理」 - 同时加 `* text=auto` 做行尾规范(当前 283 个受跟踪文件 100% 已是 LF) - 常见二进制资产(图片 / 字体 / pdf)显式标 `binary`,避免误规范化 - `docs/memory.md` §6「值得记住的失效模式」补第 4 条 验证: - A/B 实测该属性确实生效,对 #27 那个含 NUL 的 blob: 无 .gitattributes: server/usage-ledger.js | Bin 9679 -> 9689 bytes 有 .gitattributes: server/usage-ledger.js | 4 ++-- (2 insertions, 2 deletions) - `git add --renormalize .` 未产生任何暂存变更 —— `text=auto` 不动现有文件 - `npm test` 678/678 通过 如果本 PR 先于 #27 合入,#27 的 diff 在 GitHub 上就会直接显示为可读文本。 Co-Authored-By: Claude Opus 5 --- .gitattributes | 45 +++++++++++++++++++++++++++++++++++++++++++++ docs/memory.md | 1 + 2 files changed, 46 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..e46a523 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,45 @@ +# Force textual diffs for source and docs. +# +# Why: git classifies a whole file as binary if a NUL byte (0x00) appears in the +# first 8000 bytes. `git diff` then degrades to "Bin 9679 -> 10779 bytes" and +# `git blame` stops working entirely. That happened to server/usage-ledger.js +# (#27): a composite-key separator was written as a raw 0x00 instead of the +# `\u0000` escape, and it survived review precisely because the file could not +# be diffed. Nobody was careless — a single pipeline that eats backslash escapes +# is enough to produce it, and the damage hides itself. +# +# The `diff` attribute tells git to treat these paths as text even when they +# contain bytes that never occur in text files, so the next occurrence shows up +# as a normal +/- diff instead of silently going invisible. It changes nothing +# on disk or in the object store — it only affects how git renders changes. + +# Normalize line endings to LF in the repo (the tree is already 100% LF). +* text=auto + +# Always diff these as text, NUL or not. +*.js diff +*.jsx diff +*.mjs diff +*.cjs diff +*.json diff +*.css diff +*.html diff +*.md diff +*.sh diff +*.yml diff +*.yaml diff +*.sql diff +.env.example diff +Dockerfile diff + +# Genuinely binary — never try to normalize or diff these as text. +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.ico binary +*.webp binary +*.pdf binary +*.woff binary +*.woff2 binary +*.ttf binary diff --git a/docs/memory.md b/docs/memory.md index f1c396c..4630c30 100644 --- a/docs/memory.md +++ b/docs/memory.md @@ -291,6 +291,7 @@ kill %1 1. **静默错配比崩溃危险**:引用不存在的 CSS token(ErrorCard 白底白字)、不存在的 class(`className="input"` 原生控件)、错误的函数契约(`llm.embed` 永远返回 null)——三者都不报错,只是安静地渲染成错的样子或永远返回空。**"看起来像空状态"要当成 bug 线索查。** 2. **只在新库上测 = 测不到生产**:session 索引写进基础 schema,全新库没问题,已有库启动即死。563 个测试全绿也拦不住。**改 schema 必须在有数据的库上启动一次。** 3. **列可空 = 隐形数据丢失**:`workspace_id` 一直 nullable,任何忘记带它的 INSERT 都静默成功、然后从所有 scoped 读里消失。#11 用条件 NOT NULL 做了结构性根治。 +4. **源码里的裸控制字节 = 文件从 review 里消失**:`usage-ledger.js` 的复合键分隔符是直接写进源码的裸 `0x00`,而不是 `\u0000` 转义。NUL 一旦落在文件前 8000 字节内,git 就把**整个文件**判成二进制 —— `git diff` 只剩 `Bin N -> M bytes`,`git blame` 彻底失效。**它不需要谁疏忽:任何一个会吞反斜杠转义的管线都能产生它,而且产生之后会把自己藏起来。** #27 修了这个实例,`.gitattributes` 的 `diff` 属性做了结构性兜底(对 NUL 文件也强制文本 diff)。**在全量 diff 里看到源码文件显示 `Bin`,立刻当 bug 查。** ## 7. 与协作者的协议