实施 #4821(PR #4957)时发现,与该 issue 的修复本身不相干,按 Prime Directive #10 单独记录,未在那个 PR 里扩大范围。
机制
仓内有三处把裸控制字节 U+0001 直接写进源码当复合键分隔符:
| 文件 |
行 |
用途 |
packages/services/service-analytics/src/dataset-executor.ts |
924(修复前) |
维度合并键 —— 已随 #4821 去掉 |
packages/services/service-analytics/src/strategies/cross-object-rebucket.ts |
131 |
跨对象重分桶键 |
packages/services/service-storage/src/verify-file-references.ts |
107 |
object/recordId/field 复合键 |
裸控制字符渲染为空 —— 在终端、在编辑器、在 GitHub issue/PR 正文、在从文件复制出来的
引文里都一样。它不像 NUL 那样让 grep 把文件判成二进制(所以代码搜索仍然工作),但任何
人肉眼读到的都是一个不存在的分隔符。
已经造成的实际损失
#4821 整张 issue 就是这么来的:报告者从 dataset-executor.ts 复制出键函数,字节在复制中
消失,于是正文写成
dimensions.map((d) => String(row[d] ?? '')).join('') // ← 实际是 join(U+0001)
并据此断言 ['ab','c'] 与 ['a','bc'] 同键。该复现其实不成立(分隔符一直在)。派发
前的复核注释基于同一份被吃掉字节的引文,又在其上建了一整套方向裁决。真正的缺陷是另外
两条(null vs '' 合并、值本身含该字符时碰撞)—— 一张 bug 单从机制到复现到裁决全部
建立在一个看不见的字节上,直到实施阶段逐字节 cat -A 才发现。
cross-object-rebucket.ts:122 的注释本身也是这个形状:
// Bucket key = base dims (unchanged) + resolved attributes. `` is a
// separator no group value contains, matching the engine's own convention.
反引号之间本应是那个字符,读起来是空的 —— 一条自我抹除的注释。
与既有门禁的关系
scripts/check-nul-bytes.mjs 只拦 0x00。它的头注对自己的适用范围有一段很到位的推理
(#4890:「危害是 grep 的属性,不是 JavaScript 的属性」,所以按载体而不是按用途
划范围)。同一条推理往前推一步就落到这里:让读者读到一个不存在的字符 这件事,是裸控制
字符的属性,不是 NUL 独有的。NUL 的额外危害(grep 判二进制)更重,但不是唯一的危害。
顺带:本会话的 agent harness 也拒绝执行含裸控制字符的命令,写文件时同样会把转义序列
物化成裸字节 —— 也就是说工具链本身就会不断重新引入这类字节,人工守则挡不住。
建议方向(需维护者定,故本单不带实现)
- 把
check:nul-bytes 扩成 check:control-bytes:拒绝 C0 控制字符(制表 \t、换行
\n、回车 \r 除外),要求写成转义序列 —— 与现行 NUL 规则完全同构,运行时字节相同,
改动零语义。范围沿用现有的「按载体」判定。
- 或维持门禁不变,仅把这两处改写为转义序列并加注释。成本最低,但不能防复发 —— 而复发
的代价刚刚以一整张写错前提的 issue 结算过一次。
倾向 (1):这类错误的特征是写的人和读的人都收不到任何信号,只有机器能可靠地看见,
正是门禁而不是约定该管的事。
相关:#4821 / PR #4957(把 dataset-executor.ts 那一处改成了长度前缀键,顺带消除了该
文件对分隔符字符的依赖)、#4890(NUL 门禁按载体划范围的推理)、#3127(六个文件积累同一
缺陷的原始批次)。
实施 #4821(PR #4957)时发现,与该 issue 的修复本身不相干,按 Prime Directive #10 单独记录,未在那个 PR 里扩大范围。
机制
仓内有三处把裸控制字节 U+0001 直接写进源码当复合键分隔符:
packages/services/service-analytics/src/dataset-executor.tspackages/services/service-analytics/src/strategies/cross-object-rebucket.tspackages/services/service-storage/src/verify-file-references.tsobject/recordId/field复合键裸控制字符渲染为空 —— 在终端、在编辑器、在 GitHub issue/PR 正文、在从文件复制出来的
引文里都一样。它不像 NUL 那样让 grep 把文件判成二进制(所以代码搜索仍然工作),但任何
人肉眼读到的都是一个不存在的分隔符。
已经造成的实际损失
#4821 整张 issue 就是这么来的:报告者从
dataset-executor.ts复制出键函数,字节在复制中消失,于是正文写成
并据此断言
['ab','c']与['a','bc']同键。该复现其实不成立(分隔符一直在)。派发前的复核注释基于同一份被吃掉字节的引文,又在其上建了一整套方向裁决。真正的缺陷是另外
两条(
nullvs''合并、值本身含该字符时碰撞)—— 一张 bug 单从机制到复现到裁决全部建立在一个看不见的字节上,直到实施阶段逐字节
cat -A才发现。cross-object-rebucket.ts:122的注释本身也是这个形状:反引号之间本应是那个字符,读起来是空的 —— 一条自我抹除的注释。
与既有门禁的关系
scripts/check-nul-bytes.mjs只拦 0x00。它的头注对自己的适用范围有一段很到位的推理(#4890:「危害是 grep 的属性,不是 JavaScript 的属性」,所以按载体而不是按用途
划范围)。同一条推理往前推一步就落到这里:让读者读到一个不存在的字符 这件事,是裸控制
字符的属性,不是 NUL 独有的。NUL 的额外危害(grep 判二进制)更重,但不是唯一的危害。
顺带:本会话的 agent harness 也拒绝执行含裸控制字符的命令,写文件时同样会把转义序列
物化成裸字节 —— 也就是说工具链本身就会不断重新引入这类字节,人工守则挡不住。
建议方向(需维护者定,故本单不带实现)
check:nul-bytes扩成check:control-bytes:拒绝 C0 控制字符(制表\t、换行\n、回车\r除外),要求写成转义序列 —— 与现行 NUL 规则完全同构,运行时字节相同,改动零语义。范围沿用现有的「按载体」判定。
的代价刚刚以一整张写错前提的 issue 结算过一次。
倾向 (1):这类错误的特征是写的人和读的人都收不到任何信号,只有机器能可靠地看见,
正是门禁而不是约定该管的事。
相关:#4821 / PR #4957(把
dataset-executor.ts那一处改成了长度前缀键,顺带消除了该文件对分隔符字符的依赖)、#4890(NUL 门禁按载体划范围的推理)、#3127(六个文件积累同一
缺陷的原始批次)。