Skip to content

chore(scripts): 门禁扫描面纳入 DEL(0x7f),并转义 login.ts / register.ts 各一枚裸字节 (#5460) - #5479

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-5460-del-byte
Aug 5, 2026
Merged

chore(scripts): 门禁扫描面纳入 DEL(0x7f),并转义 login.ts / register.ts 各一枚裸字节 (#5460)#5479
os-zhuang merged 1 commit into
mainfrom
claude/issue-5460-del-byte

Conversation

@os-zhuang

@os-zhuang os-zhuang commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Fixes #5460

本文全程用 U+007F / U+0003 这种不含反斜杠转义的写法指代字节。原因见下面「事故源在本 PR 里又复现三次」一节 —— 写反斜杠转义正是它物化成真字节的时机。本文第一版就在「行为不变的证明」那段栽了一次(见该节末尾的订正说明)。

前提已核实(rule 6)

按 issue 正文逐条对 origin/main(#5461 合并后,577cd27be)验证:

  • 两枚裸 0x7f 仍在,位置与正文一致:packages/cli/src/commands/login.ts:47packages/cli/src/commands/register.ts:43,od -c 实证为八进制 177。
  • 「同一个 switch、隔九行」属实:两文件里 47/38 行、43/34 行,中间隔着 Ctrl+C 与 Enter 两组 case;Ctrl+C 那条已被 check:nul-bytes 只扫 NUL(0x00)—— 0x01-0x08 等控制字节不在扫描面,#5140 实测一个 0x01 会从 NUL-only 修复下溜走 #5157 转义成 U+0003 的转义序列,读得出来。
  • 「全仓其余含 0x7f 的受追踪文件只有 4 个 PNG + 1 个 ICO」属实,实测复现(见下表)。
  • 自测里那条故意把 DEL 钉在扫描面之外的断言确实存在,原文 tab / CR / LF / DEL are outside the scanned set and stay green

前提全部成立,按 08-05 12:56Z 裁定实施方案 A(纳入)。

为什么 0x7f 该纳入

#5157 把扫描面定为「C0 控制集去掉 tab/LF/CR」。DEL 是控制字符但不是 C0 —— 它单独坐在 ASCII 表末尾、印刷字符区之后,所以一个用连续区间表达的集合根本够不到它

这个缺口是偶然而非深思熟虑,最硬的证据就是剩下那两枚裸字节被发现的位置:#5157 刚转义了同一个 switch 里的一枚裸 0x03,往下九行的 Backspace 仍是裸字节。于是转义后的 case 与裸字节的 case 并排:一条读得出是个按键,另一条渲染为空、读起来是一个空串 case。区分二者的只有 ASCII 当年把这个字节放在了 0x1f 的哪一侧。

#5157 对 C0 论证的三条危害,逐条原样落在 0x7f 上(渲染为空 / 两种拼写都搜不到 / 事故源不挑字节值)。iscntrl 与正则的 Cc 类也都把 0x7f 算作控制字符 —— 「C0」是个更窄的读法。

改动

文件 改动
scripts/check-nul-bytes.mjs 查找表加一行 IS_SCANNED[0x7f] = 1(带注释说明「虽非 C0,但按事故源不挑字节纳入」);脚本头/报错文案的扫描面描述如实改为「ASCII 控制字符」;自测 34 → 48 断言
.github/workflows/lint.yml CI 步骤注释里那句逐字列举扫描集的描述同步(#5461 把语义写在脚本头、报错文案、CI 步骤三处,少改一处就是一句成文的假话)
packages/cli/src/commands/login.ts / register.ts 各一枚裸 0x7f 转义
.changeset/control-byte-gate-scans-del.md #5461control-byte-gate-scans-all-c0.md 先例

扫描面现在是 [\x00-\x08\x0b\x0c\x0e-\x1f\x7f]

二进制判据:扩面是承重的,不是顺手对齐

判据是「剔除全部扫描字节后整文件 UTF-8 严格解码」。0x7f 纳入剔除集与 #5157 对 0x01 的论证同构:E4 B8 7F AD 是「中」被塞进一枚 0x7f,只剔 C0 的话解码失败 → 判二进制 → 跳过 → 那枚 0x7f 成了自己的不在场证明。反方向不会出错:扫描集全部 0x7f 及以下,而合法 UTF-8 多字节序列只由 0x80 及以上的字节构成。

实测全部 5457 个受追踪常规文件,扩面前后文本/二进制判定零变化(#5461 的验收先例):

before (C0 only):   {"text":5452,"binary":5}
after  (C0 + DEL):  {"text":5452,"binary":5}
files whose text/binary verdict MOVED: 0

改前含裸 0x7f 的 7 个受追踪文件:

文件 判定 0x7f 枚数
apps/docs/app/favicon.ico binary 27
docs/screenshots/architecture.png binary 1317
docs/screenshots/automation.png binary 1881
docs/screenshots/layers.png binary 500
docs/screenshots/modeling.png binary 1609
packages/cli/src/commands/login.ts text 1
packages/cli/src/commands/register.ts text 1

PNG/ICO 里的 0x7f 不受影响:它们是整文件解码失败才判的二进制,与任何单个字节无关。改后 text 类里 0x7f 归零。

反向验证(方向在跑之前先定)

预判方向:经典的红向 —— 因为两个新样本文件整个都不含 C0 字节,前一版 C0-only 的扫描确实无事可做。两条都按预判走:

(a) 摘掉 IS_SCANNED[0x7f] = 1,自测转红:

✗ check-nul-bytes --self-test -- 7 failure(s)
  • #5460: a raw 0x7f Backspace literal must be flagged
  • #5460: the offending byte is reported as 0x7f, got undefined
  • #5460: a 0x7f inside a multi-byte sequence must be flagged
  • #5460: widened stripping keeps the DEL split-sequence file scannable
  • exactly the 2 binary assets skip, got 3
  • DEL is a control-byte hit (#5460)
  • the scanned set is C0-minus-tab/LF/CR plus DEL, got 29 bytes

其中 exactly the 2 binary assets skip, got 3 这条最值得看:它不是重复计数,而是那个「中」被塞了 0x7f 的样本从文本掉进了二进制 —— 不在场证明当场生效。这正是二进制判据必须同步扩面的实证。

(b) 把两个 cli 文件恢复成裸字节,扩面后的门禁判红,行号与 issue 正文逐字对上:

check-nul-bytes: 2 files contain a raw ASCII control byte

  • packages/cli/src/commands/login.ts:47:15 -- 1 occurrence of 0x7f, first at byte offset 1574
  • packages/cli/src/commands/register.ts:43:15 -- 1 occurrence of 0x7f, first at byte offset 1405

报错并给出处方:0x7f → 对应的 U+007F 转义序列。

Backspace 行为不变的证明

packages/cli没有覆盖这个按键处理器的测试(grep 只命中三个源文件,无测试文件),按派发口径以字节等价论证,并跑全包测试证明无回归。

把两个版本的 case 标签按 TS/JS 解析器的规则求值后比较码点,结果:

版本 拼写 求值后码点 长度
before 一对单引号,中间夹一枚裸 0x7f [127] 1
after 一对单引号,中间是 U+007F 的转义序列 [127] 1

IDENTICAL AT RUNTIME: true —— 两种拼写求值后都是单个码点 127、长度 1,switch 命中完全相同。

订正说明(本身就是证据): 本文第一版这里贴的是程序原始输出,两行分别是 before / after 的拼写。可是 before 那行的裸字节渲染为空、after 那行的反斜杠转义在我组装 PR 正文时又被物化掉,结果两行长得一模一样,一段本该证明「两种拼写不同但求值相同」的证据,变成了两行相同的字符 —— 看起来什么也没证明。改成上表这种「描述拼写而不粘贴字节」的写法。这正是本 PR 要治的病:渲染为空的字节让代码和文档对每一个读者说谎。

另一条独立佐证:同包的 packages/cli/src/commands/cloud/login.ts:50 本来就用 U+007F 的转义序列拼写同一个按键。仓内三个密码输入实现里,规范拼写早已存在,只有这两个漂移成了裸字节;本次是把它们收回既有惯例,不是发明新写法。

事故源在本 PR 里又复现四次

issue 正文记录了它自己的第一版被这个坑绊倒(写着写着落进两枚真的 0x03)。实施过程中又复现四次,每次都被逐字节自扫或工具校验当场抓住:

  1. 写临时测量脚本时,split(...) 的参数里落进一枚真的 NUL(渲染为空,直到 Edit 工具匹配不上那行才暴露);
  2. 往脚本头部写那段正在描述该事故的注释时,落进一枚真的 0x03(第 95 行);
  3. 写 changeset 时落进三枚 0x7f;
  4. 写本次 commit message 时又落进一枚 0x7f,被工具的控制字符校验拦下。

外加上面那条订正说明里的第五次。这些都不在 issue 的记录里,写在这里是因为它们正是「事故源不挑字节值、也不挑文件类型」的当场实测 —— 也顺带说明为什么本文用 U+007F 而不用反斜杠转义。

另注一个门禁边界(非本单缺陷,不扩范围): changeset 那三枚 0x7f 落地时,node scripts/check-nul-bytes.mjs 报的是 OK —— 因为门禁扫的是 git ls-files受追踪文件,而新建的 changeset 当时还没 git addgit add 之后立刻判红。这是门禁既有的、正确的设计(未追踪文件不归它管),提一句是因为它恰好说明 AGENTS.md 里「除了跑门禁,还要逐字节自扫」那条不是冗余。

验证

$ pnpm check:nul-bytes
✓ check-nul-bytes --self-test: 48 assertions over a temp git repo (real scan() path)
check-nul-bytes: OK (scanned 5452 tracked text file(s); skipped 5 binary, 1 non-regular; no raw ASCII control bytes).

$ pnpm --filter @objectstack/cli test
Test Files  79 passed (79)
     Tests  767 passed (767)

$ pnpm --filter @objectstack/cli typecheck
(clean)

$ npx eslint scripts/check-nul-bytes.mjs packages/cli/src/commands/{login,register}.ts --no-inline-config
(clean)

自测新增了什么

正反两向都钉在代码旁边,外加一条「处方本身可测」:

  • 正向:裸 0x7f 判红;报错报出 0x7f 与对应转义处方;多字节序列里的 0x7f 判红。
  • 反向:两个新样本整个文件都不含 C0 字节(断言里现算,不靠注释声称),所以前一版 C0-only 的扫描确实无事可做;以及那个被塞了 0x7f 的「中」在 C0-only 剔除下解不出来 —— 扩面前它连扫都不会被扫
  • 处方绿:转义后的拼写保持绿。门禁教作者写转义,那这条处方必须可测 —— 只有红样本、没有绿样本,只证明了什么会被拒,没证明照做会被接受。
  • 边界整体断言:把扫描集与「ASCII 控制字符减 tab/LF/CR」逐字节比对,并钉住印刷字符永不入集,这样将来改查找表的人必须正面面对边界,而不是悄悄挪一格。

原先那条故意把 DEL 钉在扫描面外的断言按裁定反转而非删除:它当年是用来说明这条边界是选出来的而不是漏掉的;这次边界被重新选择,选到了另一边,注释里记下了这次改选的来由。

…C0 set (#5460)

#5157 drew this gate's scan surface as "the C0 controls minus tab/LF/CR".
DEL (U+007F) is a control character but not a C0 one -- it sits alone past the
printable range -- so a set expressed as a contiguous range could not reach it.

That the gap was arbitrary rather than considered is shown by where the two
remaining raw specimens sat: nine lines below the 0x03 #5157 had just escaped,
in the same switch, in both login.ts and register.ts, as the Backspace key
literal. The escaped case reads as a key; the raw one renders as nothing and
reads as an empty-string case.

All three harms #5157 argued for C0 land on DEL verbatim, and the decisive one
is that the accident source -- an editing tool materialising an escape into its
byte while an author writes about the byte -- does not pick byte values.

The binary criterion widens with it, and is load-bearing there: E4 B8 7F AD is
the character 中 with a stray 0x7f in it, which a C0-only strip reads as
binary, making the byte its own alibi. Measured over all 5457 tracked regular
files: zero movement between the text and binary verdicts.

Both specimens are escaped to the U+007F escape sequence, which is
byte-identical at runtime and already the spelling used by the sibling prompt
in cloud/login.ts.

Self-test: 34 -> 48 assertions, both directions pinned. The assertion that
deliberately held DEL outside the set is inverted rather than deleted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GX3sL71LFq8m2usg6VqTSE
@vercel

vercel Bot commented Aug 5, 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 5, 2026 1:45pm

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation ci/cd tooling size/m labels Aug 5, 2026
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cli.

21 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/skills-reference.mdx (via packages/cli)
  • content/docs/api/client-sdk.mdx (via @objectstack/cli)
  • content/docs/api/data-flow.mdx (via @objectstack/cli)
  • content/docs/api/environment-routing.mdx (via @objectstack/cli)
  • content/docs/api/error-catalog.mdx (via @objectstack/cli)
  • content/docs/automation/hook-bodies.mdx (via packages/cli)
  • content/docs/deployment/backup-restore.mdx (via @objectstack/cli)
  • content/docs/deployment/cli.mdx (via @objectstack/cli)
  • content/docs/deployment/self-hosting.mdx (via @objectstack/cli)
  • content/docs/deployment/validating-metadata.mdx (via packages/cli)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/cli)
  • content/docs/kernel/runtime-services/data-service.mdx (via packages/cli)
  • content/docs/kernel/runtime-services/index.mdx (via packages/cli)
  • content/docs/permissions/authentication.mdx (via @objectstack/cli)
  • content/docs/plugins/index.mdx (via @objectstack/cli)
  • content/docs/plugins/packages.mdx (via @objectstack/cli)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/cli)
  • content/docs/protocol/kernel/realtime-protocol.mdx (via @objectstack/cli)
  • content/docs/releases/implementation-status.mdx (via @objectstack/cli)
  • content/docs/releases/v16.mdx (via @objectstack/cli)
  • content/docs/releases/v17.mdx (via @objectstack/cli)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

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

Labels

ci/cd documentation Improvements or additions to documentation size/m tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

DEL(0x7f)在 C0 扫描面之外:login.ts / register.ts 各有一枚裸 0x7f 当 Backspace 键值,与刚转义的 0x03 同处一个 switch

2 participants