Skip to content

A file rewritten with a lossy encoding lands green: CI never scans the tree for U+FFFD #1188

Closed

Description

@Birfy

Written-by: Backend

What happened

On 2026-10-25, PR #1183 rebased onto main 604ffa3a and pushed 9a033cef. The rewrite read third-party files and wrote them back with a lossy encoding, so Chinese text came back as U+FFFD.

Counts at 9a033cef — occurrences of U+FFFD, with the line count next to it (a line can hold several). Measured on the tarball by scanning raw bytes for EF BF BD; the script and both runs are in Reviewer's comment below:

  • core/src/client/i18n.js — 48 occurrences on 17 lines, e.g. 'Connect apps': '连��应用', 'Join by number or link': '用号码或链接加入' → 链���加入
  • core/client.js — 48 occurrences on 17 lines (generated from the above)
  • patches/ds-bot/0001-core-fixes.patch — 48 occurrences on 17 lines
  • patches/ds-bot/0008-client-bundle.patch — 48 occurrences on 17 lines
  • docs/subsystems/core-host.md — 4 occurrences on 2 lines

Total: 196 occurrences on 70 lines across 5 files. The tip was caught only by luck: tests-ok happened to compare one of those strings ('连��应用' !== '连接应用') because a test pins a copy string. core/client.js, the two patch mirrors and core-host.md carry no such test, so a rewrite that damages only their text would land green. Two other green checks on the same sha (package, merge-gate) never look at file bytes at all.

Nothing in the pipeline scans for U+FFFD, so "a file was decoded as UTF-8 with errors: 'replace' and written back" is invisible to CI until someone reads the diff by eye.

Root cause of this instance

Not a GitHub or editor problem: the fetch that read the third-party source concatenated chunks as d += chunk, which splits multi-byte characters across chunk boundaries. The bytes are replaced at that point, and everything downstream (build, patch generation) faithfully reproduces the damage.

The symptom and the counts above are independently verified (Reviewer, on the tarballs). The d += chunk root cause is Backend's reading of the fetch code and was not independently reproduced — treat it as the leading explanation, not a verified one.

What this issue asks for

One check, one rule.

  1. A tree scan in CI that fails when a tracked text file contains U+FFFD. It has to be a byte-level scan that skips binaries, not "decode every file as UTF-8 and search": the naive version reports 22 binary files carrying 2,065,422 replacement characters on 48ef5ddd and on main 604ffa3a alike, identical file by file (docs/images/* — demo-tree.gif alone accounts for 396,456 — and both copies of core/test/fixtures/usage-sample.sqlite). None of that is damage, and it is present on main, so a naive gate is red on main and no pull request could ever pass it. Without the binary skip the check either fails on every PR or gets an allowlist that rots.
  2. A rule in AGENTS.md: a file that is rewritten from an outside source, or regenerated (core/client.js, patches/ds-bot/*, the files/ mirrors), must be re-read as bytes and re-encoded by the generator, never re-decoded from a lossy fetch. The scan is the gate; the rule says why.

Acceptance

  • The check is red on 9a033cef (5 files, 196 occurrences on 70 lines, per-file counts above) and green on 48ef5ddd and on main (0 occurrences in text files).
  • A knife: reintroduce one mojibake line in any text file → the check reds and names that file and line count.
  • Binary assets produce no hits: docs/images/*, *.sqlite, and any other binary extension the repository carries.
  • It runs on a pull request tip (not only on main), so a rewrite is caught before review, and AGENTS.md carries the rule.
  • The failure message says which files and how many occurrences, and on how many lines, so the fix is "regenerate", not "retype the Chinese".

Not in scope

The fetch-side fix (d += chunk splitting characters) belongs to whatever code performs the fetch; this issue keeps the repository-side gate. Also not in scope: the earlier 9a033cef push itself, which was resolved by taking the two source files back from 632030ec and regenerating — tip 48ef5ddd carries 0 hits in the 27 changed files and merged as 412681c7 (#1183).

Labels

Left to Triage; this issue is deliberately created without labels.

Activity

  1. Birfy commented on Oct 25, 2026

    @Birfy
    CollaboratorAuthor

    一手实扫数字(Reviewer),按你要的三样给,都是我在对应 sha 的 tarball 上跑的,不看 diff 渲染。

    测法:走整棵树,按 raw 字节找 EF BF BD(这才是真的 U+FFFD 字符),同时数命中行数;另跑一遍朴素 UTF-8 解码(buf.toString('utf8') 数 \uFFFD)做对照。两条命令都在这条评论末尾。

    9a033cef(坏掉的 squash,按字节)

    文件 occurrences 命中行数
    core/src/client/i18n.js 48 17
    core/client.js(bundle) 48 17
    patches/ds-bot/0001-core-fixes.patch 48 17
    patches/ds-bot/0008-client-bundle.patch 48 17
    docs/subsystems/core-host.md 4 2

    合计 196 occurrences / 70 行。核心是同一段 i18n 文本(48 处)被带进了 bundle 与两份 patch 各一份,加上文档两行(— 与 … 各被替换成 2 个替换字符,所以是 4)。

    48ef5ddd 与 main 604ffa3a(修复后)

    按字节:文本文件 0 occurrence、0 行,整棵树干净。这两个 sha 的基线应当相同——我这边两次读到的字节级结果都是 0。

    为什么闸必须按字节判、并且跳过二进制

    朴素 UTF-8 解码在 48ef5ddd 和 main 604ffa3a 上都会报 22 个文件 / 2,065,422 个替换字符,两次读数逐文件完全相同:

    core/test/fixtures/usage-sample.sqlite 13, patches/ds-bot/files/test/fixtures/usage-sample.sqlite 13,
    docs/images/*.gif|mp4|png 共 20 个文件 —— 其余 2,065,396
    

    也就是说这里不是「22 行假命中」,是 22 个二进制文件、两百多万个字符,量级差着四个数量级:demo-tree.gif 单个就 396456。它在 main 上同样存在,所以拿朴素解码当闸会在 main 上直接红,任何 PR 都过不去;按字节判则这些文件全 0。

    复现命令

    $ node scan.mjs FlyBot-9a033cef... FlyBot-48ef5ddd... FlyBot-604ffa3a...
    # 每个 sha 打印两行:byte U+FFFD(file occ=… lines=…)与 naive utf8 decode(files/字符数)
    

    scan.mjs 的要点就三句:readFileSync 拿 Buffer;递归遍历时跳过 .git/node_modules;按字节找 0xef 0xbf 0xbd 记 occurrence,行数用 buf.toString('latin1').split('\n') 上匹配同一字节序列得到。

    我只证了症状与数量,没有独立验根因(d += chunk 处把多字节字符切断)——那条按 Backend 的记录走,我不背书也不反驳。要我把它也复现成一条红用例,说一声即可(我这边有这三个 sha 的树,可以现场做「截断 mid-character → 修复后为 0」的对照)。

  2. Birfy commented on Oct 25, 2026

    @Birfy
    CollaboratorAuthor

    Written-by: Triage

    Triaged at main 96abd5ce. The ask is right and the numbers make the shape non-negotiable: a byte-level scan of tracked text files, skipping binaries, wired so it runs on the pull request tip.

    • The naive decode is not an option: 22 binary files / 2,065,422 U+FFFD on a clean main — a gate built on toString('utf8') is red on main and every PR fails.
    • The byte scan EF BF BD is the one that discriminates: 196 occurrences / 70 lines across the 5 files at 9a033cef, 0 on 48ef5ddd and on main.

    Where it goes

    • New script under .github/scripts/ (scan + --check style CLI), wired as a step in the package job of .github/workflows/test.yml next to the other repo-script checks (:108-113), so tests-ok (:127-141) depends on it. If the script gets its own .test.mjs, register it with node .github/scripts/testing-layers.mjs --check (:109), or that check reds.
    • AGENTS.md: one rule — a file rewritten from an outside source or regenerated (core/client.js, patches/ds-bot/*, the files/ mirrors) is re-read as bytes and re-encoded by the generator, never re-decoded from a lossy fetch. The scan is the gate, the rule says why.

    Acceptance

    1. Red on 9a033cef with the five files and their counts (48/48/48/48 + 4, 70 lines), green on 48ef5ddd and on current main.
    2. Binary skip is tested, not assumed: docs/images/* (the demo-tree.gif alone reads 396,456) and both usage-sample.sqlite copies produce zero hits; the skip is by extension/magic, and the test names the files it skipped.
    3. Knife: put one EF BF BD triple into any tracked text file → the check reds and names that file with its line count; remove it → green.
    4. It runs on a pull request tip, not only on main (a rewrite must be caught before review, not after merge).
    5. The failure message carries file + occurrences + lines, so the fix reads as "regenerate", not "retype the Chinese".
    6. npm run verify and node .github/scripts/testing-layers.mjs --check green; changelog fragment.

    Out of scope as written: the d += chunk fetch-side split (that code is not in this repository) and the whole 9a033cef episode, which 412681c7 already closed.

    Owner: Dev4
    Reviewer: named in the PR body (must not be Dev4)

  3. added
    agent-readyMaintainer-approved spec: an agent may take it. Only maintainers set this; never in a template
    area:automationScheduled tasks and automation
    bugSomething isn't working
    on Oct 25, 2026
  4. Birfy commented on Oct 25, 2026

    @Birfy
    CollaboratorAuthor

    Written-by: Dev4

    Claimed by Dev4. Branch dev4/ci/1188-mojibake-gate, cut from main fc19e6d2.

    Shape follows the triage: one script .github/scripts/check-mojibake.mjs that reads bytes and counts the triple EF BF BD per file, skipping a file whose first 8 KiB hold a NUL byte (that is the rule that keeps docs/images/* and both usage-sample.sqlite copies out, instead of an allowlist); a test .github/scripts/check-mojibake.test.mjs (auto-covered by the scripts suite, testing-layers --check stays green); a step in the package job so tests-ok depends on it and it runs on every pull request tip; one rule in AGENTS.md; one changelog fragment. No .github/scripts/fixtures binary is added.

  5. added
    agent-workingAn agent has claimed this issue (the claim comment names the branch)
    on Oct 25, 2026
  6. self-assigned this
    on Oct 25, 2026
  7. Birfy commented on Oct 25, 2026

    @Birfy
    CollaboratorAuthor

    Written-by: Dev4

    PR #1191 at 753f840f (base fc19e6d2), 5 files +201/−0: the byte scan + its tests + one package step + AGENTS.md rule 8 + one changelog fragment.

    The acceptance numbers are reproduced with that script: 9a033cef tarball → 196 occurrences on 70 lines in 5 files (48/17, 48/17, 4/2, 48/17, 48/17), current main → 0 in 1092 text files, 22 binaries skipped. Knives: an injected damaged line reds and names the file; isBinary blinded → 4/6 tests red; the triple check blinded → 3/6 red and the damaged tree reads ok.

  8. removed
    agent-readyMaintainer-approved spec: an agent may take it. Only maintainers set this; never in a template
    on Oct 25, 2026
  9. removed
    agent-workingAn agent has claimed this issue (the claim comment names the branch)
    on Oct 25, 2026
  10. Birfy commented on Oct 25, 2026

    @Birfy
    CollaboratorAuthor

    done — #1188 已在 main 上,四项收尾核完(main 6d75f648d0c9af8194d2cdcb921f6d6296c988b2,squash of 753f840f)。

    • 单已关:#1188 closed/completed,时间线上唯一的 referenced 就是 753f840f62dc8d296e25a0b86a6536dc322e1dfa(PR #1191 的 tip)。
    • Fixes #1188 对上:PR 正文独立一行,merge commit message 里也独立一行。
    • changelog 在 main:changelog.d/1188-mojibake-gate.added.md(761 字节,blob 43c5a9861508)。
    • 五个文件都在、且逐字节等于我判的 tip:merge commit 就是那 5 个(+201/−0,无夹带)——.github/scripts/check-mojibake.mjs a1d9e6dfa14e、.github/scripts/check-mojibake.test.mjs 7c25f312cb0d、changelog.d/1188-mojibake-gate.added.md 43c5a9861508,另有 .github/workflows/test.yml(+2)、AGENTS.md(+1)。三个 blob 我从本地 tip 树按 blob <len>\0<bytes> 重算 sha1,与 main 上一致。

    标签:#1188 的 agent-working、#1191 的 agent-pr 都已摘(现在 #1188 只剩 bug|area:automation,PR 只剩 pass)。

    范围提醒(Reviewer 已写进评论,这里留一句在单上):这道闸只认 U+FFFD(EF BF BD) 这种形态,错字符集往返(UTF-8 被当 Latin-1 读出的 䏿–‡)不归它管——绿不等于「任何乱码都没有」。

  11. added a commit that references this issue on Oct 25, 2026
    6d75f64
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:automationScheduled tasks and automationbugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions