docs: equal-authority pairing with sidecar consistency records

Redesign per review: neither language is canonical. A pair is three
sibling files — foo.md, foo.zh.md, foo.i18n.yaml — and either language
may be authored first (a Chinese-first RFC is as legitimate as an
English-first one). The sidecar record holds the FULL git blob hash of
both sides as of the last confirmed-consistent state, replacing the
in-file one-directional fingerprint; editing either side without
re-confirming the pair goes red. New --write mode re-records a pair
after both sides are brought in line, making the confirmation a
reviewable yaml diff. Pairs merge whole (completeness enforced).

- gate rewritten around pair anchors (union of .zh.md and .i18n.yaml
  remnants) so half-deleted pairs are caught from either side; red/green
  proven for en-only edit, zh-only edit, missing record, and a record
  for an excluded file
- verify-rfc-classification now skips .zh.md counterparts (same RFC,
  indexed via its English filename; the pairing gate owns consistency)
- docs/i18n/README.md + translation-rules.md reframed bidirectionally
  (terminology table binds both directions; typography section governs
  the Chinese side); zh counterparts updated; skill workflow updated
- RFC amended to the shipped design, records the English-canonical
  in-file-fingerprint model as considered-and-revised; RFC translated
  (docs/rfc/.../2026-07-02-bilingual-docs-and-pairing-gate.zh.md) and
  added to the required frontier
- generated docs stay excluded with the follow-up recorded: teach the
  generators to emit Chinese, then de-list
This commit is contained in:
Ziya
2026-07-03 07:41:24 -07:00
parent bb10931478
commit ec05295a0c
18 changed files with 307 additions and 171 deletions
@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
2026-07-02-bilingual-docs-and-pairing-gate.md: 517a6371eca5d747313c7efdb2756a50257701e4
2026-07-02-bilingual-docs-and-pairing-gate.zh.md: f8f68bf5d4d7e6795318d9dd435a525f20a4f407
@@ -1,31 +1,36 @@
# Bilingual documentation via paired sibling files and a pairing gate
English | [中文](2026-07-02-bilingual-docs-and-pairing-gate.zh.md)
## Context
This repo's README and docs tree are read by people and agents inside and outside the company, in both English and Chinese. Maintaining a second language by hand, with no mechanism, is how translations rot: the English file moves on, the Chinese file silently lies, and no gate notices. The repo's standing answer to invariants of this kind is to encode them as a mechanical check (see [quality gates](2026-06-11-quality-gates.md) and [doc-sync enforcement](2026-06-11-doc-sync-enforcement.md)), so the bilingual policy ships with one.
This repo's README and docs tree are read by people and agents inside and outside the company, in both English and Chinese. Maintaining a second language by hand, with no mechanism, is how translations rot: one side moves on, the other silently lies, and no gate notices. The repo's standing answer to invariants of this kind is to encode them as a mechanical check (see [quality gates](2026-06-11-quality-gates.md) and [doc-sync enforcement](2026-06-11-doc-sync-enforcement.md)), so the bilingual policy ships with one.
## Decision
- **Paired sibling files, English canonical.** The translation of `foo.md` is `foo.zh.md` in the same directory; English is the only authoring language and translation flows EN → ZH. Policy: [docs/i18n/README.md](../../../i18n/README.md); translation rules: [docs/i18n/translation-rules.md](../../../i18n/translation-rules.md); terminology source of truth: [docs/i18n/terminology.md](../../../i18n/terminology.md).
- **A blob-hash fingerprint makes freshness checkable.** The first line of every `.zh.md` records the repo-relative path and the first 12 hex digits of the git blob hash of the English source it renders. Staleness is then a pure content comparison — no history lookup — and the hash is computable for a source edited in the same PR, which a commit-hash fingerprint (the MDN `l10n.sourceCommit` model) is not.
- **`verify-translation-pairing` joins `doc-sync`.** The gate ([scripts/verify-translation-pairing.ts](../../../../scripts/verify-translation-pairing.ts)) enforces: required pairs exist, every existing translation is fresh/switched/structure-matched/non-orphaned, and excluded (generated or bilingual-by-construction) files stay unpaired. The `required` list in [scripts/translation-pairing.manifest.json](../../../../scripts/translation-pairing.manifest.json) is a ratchet: each merged translation batch adds its files, so coverage only grows.
- **Paired sibling files with equal authority.** A documentation pair is three sibling files: English `foo.md`, Chinese `foo.zh.md`, and a consistency record `foo.i18n.yaml`. Neither language is canonical — a document may be authored and reviewed Chinese-first and translated to English afterwards, or the reverse; what binds the pair is that both sides must say the same thing, and pairs merge whole (both languages plus the record, never one alone). Policy: [docs/i18n/README.md](../../../i18n/README.md); translation rules: [docs/i18n/translation-rules.md](../../../i18n/translation-rules.md); terminology source of truth: [docs/i18n/terminology.md](../../../i18n/terminology.md).
- **A sidecar record of both blob hashes makes consistency checkable.** `foo.i18n.yaml` holds the full git blob hash of each side as of the last confirmed-consistent state. An edit to either side without re-confirming the pair is then mechanically detectable as a pure content comparison — no history lookup — and the hashes are computable for files edited in the same PR, which a commit-hash record is not. Re-recording (`verify-translation-pairing --write`) produces a reviewable yaml diff: confirming consistency is an explicit, visible act in the PR.
- **`verify-translation-pairing` joins `doc-sync`.** The gate ([scripts/verify-translation-pairing.ts](../../../../scripts/verify-translation-pairing.ts)) enforces: required pairs exist, every existing pair is complete (all three files) and consistent (both hashes match, switcher links both ways, structural signatures identical), and excluded (generated or bilingual-by-construction) files stay unpaired. The `required` list in [scripts/translation-pairing.manifest.json](../../../../scripts/translation-pairing.manifest.json) is a ratchet: each merged translation batch adds its files, so coverage only grows.
- **Translation is agent work with human review.** The committed workflow is [.agents/skills/dsh-translate-docs](../../../../.agents/skills/dsh-translate-docs/SKILL.md), following the same pattern as [dsh-code-review](../../../../.agents/skills/dsh-code-review/SKILL.md): the skill carries the workflow and defers to the docs as sources of truth.
## Alternatives considered
- **English as the canonical source with a fingerprint inside the translation** — the design first proposed for this RFC: `.zh.md` files carried an HTML comment recording the English source's blob hash, and translation flowed EN → ZH only. Revised in review: the team wants Chinese-first authoring (write and review a Chinese RFC, then translate to English) with the two languages holding equal authority, which a one-directional canonical model cannot express. The sidecar record covering BOTH sides replaced the in-file one-directional fingerprint; the blob-hash mechanics survived unchanged.
- **Locale directories (`docs/en/` + `docs/zh/`, the Kubernetes/ECharts model)** — rejected: this repo has no docs-site framework to map locales to routes, moving every English file would churn every existing cross-reference, and `verify-md-links`/`verify-doc-refs` would need path-mapping logic instead of working unchanged.
- **A separate translation repo (the PingCAP `docs`/`docs-cn` model)** — rejected: right for a docs product with independent release trains, overkill for a monorepo's own documentation; it also puts the translation outside the reach of this repo's gates.
- **Interleaved bilingual files (single file, both languages)** — rejected: doubles every diff, breaks the one-line-per-paragraph convention's diff ergonomics, and makes partial staleness invisible.
- **Commit-hash fingerprints (MDN `l10n.sourceCommit`)** — rejected in favor of blob hashes: a same-PR source edit has no commit hash yet, so the MDN model cannot express "translated against the version this PR introduces", and verifying it requires git history instead of file content.
- **Comparing git timestamps of the pair (no fingerprint)** — rejected: formatting-only English edits would false-positive, and a translation committed after an unrelated English edit would false-negative; content identity is the only signal that means what the gate claims.
- **Interleaved bilingual files (single file, both languages)** — rejected: doubles every diff, breaks the one-line-per-paragraph convention's diff ergonomics, and makes partial inconsistency invisible.
- **Commit-hash records (the MDN `l10n.sourceCommit` model)** — rejected in favor of blob hashes: a same-PR edit has no commit hash yet, so the MDN model cannot express "consistent as of the state this PR introduces", and verifying it requires git history instead of file content.
- **Comparing git timestamps of the pair (no record)** — rejected: formatting-only edits would false-positive, and a counterpart committed after an unrelated edit would false-negative; content identity is the only signal that means what the gate claims.
## Industry precedent
Paired sibling files with locale suffixes are the dominant Chinese big-tech convention (ant-design `index.zh-CN.md`/`index.en-US.md`; arco-design `README.zh-CN.md` with a top-of-file switcher; Apache ShardingSphere's 387 `.cn.md`/`.en.md` pairs) — but none of those repos *enforce* pairing or freshness in CI; the convention holds by review alone. Freshness automation exists outside China: MDN's `l10n.sourceCommit` front-matter fingerprint, Vue's Ryu-Cho action (upstream-commit watcher that opens issues/PRs for stale translations), Kubernetes' localization drift scripts, and Microsoft's Azure co-op-translator (source-hash-driven LLM re-translation in CI). This design combines the two: the Chinese-ecosystem file layout with a fingerprint gate, plus a committed agent skill in place of a bot service.
Paired sibling files with locale suffixes are the dominant Chinese big-tech convention (ant-design `index.zh-CN.md`/`index.en-US.md`; arco-design `README.zh-CN.md` with a top-of-file switcher; Apache ShardingSphere's 387 `.cn.md`/`.en.md` pairs) — but none of those repos *enforce* pairing or consistency in CI; the convention holds by review alone. Consistency automation exists outside China: MDN's `l10n.sourceCommit` front-matter fingerprint, Vue's Ryu-Cho action (upstream-commit watcher that opens issues/PRs for stale translations), Kubernetes' localization drift scripts, and Microsoft's Azure co-op-translator (source-hash-driven LLM re-translation in CI). This design combines the two: the Chinese-ecosystem file layout with a hash-pair gate, plus a committed agent skill in place of a bot service.
## Consequences
- Editing an English doc that has a `.zh.md` sibling obligates the same PR to update the translation — the gate makes the doc-sync rule bilingual, and CI (not reviewer memory) carries the invariant.
- Generated docs (`cordis-catalog/`, `tool-catalog/`, `module-graph.md`) are never paired; their generators emit English only, and the gate rejects a stray translation of them.
- Rollout is incremental by design: documents outside `required` are visible backlog (`--list`), not red CI, so translation lands in reviewable batches without a big-bang PR.
- The fingerprint doubles as the update tool (`git cat-file -p <hash>` recovers the exact translated-from text for a minimal diff-based update), so re-translation of whole files is never forced by the mechanism.
- Editing either side of a paired document obligates the same PR to update the counterpart and re-record the pair — the gate makes the doc-sync rule bilingual, and CI (not reviewer memory) carries the invariant.
- Every pair adds a third file to the tree. The record is machine-written (`--write`), so the cost is directory noise, not maintenance effort; in exchange, "who confirmed these consistent, and when" is answerable from git blame on the yaml.
- When the two sides disagree, no mechanical rule picks a winner — the PR review does. That is the price of equal authority, accepted deliberately: the alternative (a canonical language) forbids Chinese-first authoring.
- Generated docs (`cordis-catalog/`, `tool-catalog/`, `module-graph.md`) are excluded for now; the planned follow-up is to teach their generators to emit Chinese alongside English, at which point they leave the exclusion list.
- Rollout is incremental by design: documents outside `required` are visible backlog (`--list`), not red CI, so pairs land in reviewable batches without a big-bang PR.
- The recorded hashes double as the update tool (`git cat-file -p <hash>` recovers either side's last-confirmed text for a minimal diff-based update), so re-translation of whole files is never forced by the mechanism.
@@ -0,0 +1,36 @@
# 通过配对兄弟文件与配对门禁实现双语文档
[English](2026-07-02-bilingual-docs-and-pairing-gate.md) | 中文
## 背景
本仓库的 README 与 docs 目录树会被公司内外的人和 agent(智能体)以中英两种语言阅读。没有机制、纯靠手工维护第二语言,正是译文腐烂的方式:一侧继续演进,另一侧默默地说谎,而没有门禁会注意到。对这类不变式,本仓库一贯的答案是把它编码成机械检查(见[质量门禁](2026-06-11-quality-gates.md)与 [doc-sync 强制](2026-06-11-doc-sync-enforcement.md)),因此双语政策随附一道门禁一起交付。
## 决策
- **配对兄弟文件,两种语言同权。**一对文档是三个兄弟文件:英文 `foo.md`、中文 `foo.zh.md`,加一份一致性记录 `foo.i18n.yaml`。没有哪种语言是正典——一篇文档可以先用中文撰写和评审、之后再译成英文,反之亦可;约束这对文件的是两侧必须说同样的话,且配对整体合入(两种语言加记录,绝不单独落一侧)。政策见 [docs/i18n/README.md](../../../i18n/README.md);翻译规则见 [docs/i18n/translation-rules.md](../../../i18n/translation-rules.md);术语真源见 [docs/i18n/terminology.md](../../../i18n/terminology.md)。
- **旁挂记录两侧 blob hash,使一致性可检查。**`foo.i18n.yaml` 保存两侧文件在上一次确认一致状态下各自的完整 git blob hash。此后改了任一侧而没重新确认配对,都能被机械检测出来——纯内容比较、无需查询历史——而且同一个 PR 里改动的文件也能算出 hash,commit hash 式的记录做不到这一点。重新记录(`verify-translation-pairing --write`)产生一份可评审的 yaml diff:确认一致在 PR 里是一个显式、可见的动作。
- **`verify-translation-pairing` 加入 `doc-sync`。**门禁([scripts/verify-translation-pairing.ts](../../../../scripts/verify-translation-pairing.ts))强制执行:required 的配对存在;任何已存在的配对完整(三个文件齐全)且一致(两个 hash 都匹配、切换行双向互链、结构签名一致);被排除的文件(生成物或本身即双语的)保持不配对。[scripts/translation-pairing.manifest.json](../../../../scripts/translation-pairing.manifest.json) 中的 `required` 清单是一个棘轮:每个合入的翻译批次把自己的文件加进去,覆盖面只增不减。
- **翻译是 agent 的工作,由人评审。**进仓的工作流是 [.agents/skills/dsh-translate-docs](../../../../.agents/skills/dsh-translate-docs/SKILL.md),与 [dsh-code-review](../../../../.agents/skills/dsh-code-review/SKILL.md) 同一模式:skill 承载工作流,并把真源让给文档。
## 曾考虑的替代方案
- **英文为正典源、指纹放在译文内**——本 RFC 最初提出的设计:`.zh.md` 文件携带一条 HTML 注释记录英文源的 blob hash,翻译只沿 EN → ZH 单向流动。评审中修订:团队需要中文先行的撰写方式(先写、先审中文 RFC,再译英文),两种语言同权,而单向正典模型无法表达这一点。覆盖**两侧**的旁挂记录取代了文件内的单向指纹;blob hash 的机制原样保留。
- **语言目录(`docs/en/` + `docs/zh/`Kubernetes/ECharts 模式)**——否决:本仓库没有把 locale 映射到路由的文档站框架,挪动每个英文文件会搅动所有既有交叉引用,且 `verify-md-links`/`verify-doc-refs` 将需要路径映射逻辑而不是原样工作。
- **独立翻译仓库(PingCAP `docs`/`docs-cn` 模式)**——否决:适合有独立发布节奏的文档产品,对 monorepo 自己的文档而言过重;还会把译文置于本仓库门禁够不到的地方。
- **中英混排单文件(一个文件、两种语言)**——否决:每个 diff 都翻倍,破坏一段一行约定的 diff 工效,且局部不一致不可见。
- **Commit hash 式记录(MDN `l10n.sourceCommit` 模式)**——否决,改用 blob hash:同一个 PR 内的改动还没有 commit hash,MDN 模式无法表达「与本 PR 引入的状态一致」,且校验它需要 git 历史而非文件内容。
- **比较配对两侧的 git 时间戳(无记录)**——否决:纯格式化的改动会误报,一次无关改动之后提交的另一侧会漏报;只有内容同一性这个信号与门禁的承诺名实相符。
## 业界先例
带语言后缀的配对兄弟文件是中国大厂的主流约定(ant-design 的 `index.zh-CN.md`/`index.en-US.md`arco-design 的 `README.zh-CN.md` 加顶部切换行;Apache ShardingSphere 的 387 对 `.cn.md`/`.en.md`)——但这些仓库都没有在 CI 里**强制**配对或一致性;约定纯靠评审维系。一致性自动化存在于中国之外:MDN 的 `l10n.sourceCommit` front-matter 指纹、Vue 的 Ryu-Cho action(监视上游 commit、为陈旧译文自动开 issue/PR)、Kubernetes 的本地化漂移脚本、微软 Azure co-op-translatorCI 中由源 hash 驱动的 LLM 重译)。本设计把两者结合:中文生态的文件布局,加 hash 对门禁,再加一个进仓 agent skill(技能)替代 bot 服务。
## 后果
- 修改已配对文档的任一侧,同一个 PR 就有义务更新另一侧并重新记录配对——门禁把 doc-sync 规则双语化,不变式由 CI(而非评审者的记忆)承载。
- 每个配对给目录树多添一个文件。记录由机器写入(`--write`),代价是目录噪音而非维护负担;换来的是「谁在何时确认过这对一致」可以从 yaml 的 git blame 直接回答。
- 两侧说法冲突时,没有机械规则裁决谁赢——由 PR 评审裁决。这是同权的代价,是有意接受的:另一个选项(正典语言)禁止中文先行撰写。
- 生成文档(`cordis-catalog/``tool-catalog/``module-graph.md`)暂被排除;计划中的后续工作是让它们的生成器在输出英文的同时输出中文,届时移出排除清单。
- 推进天然是渐进的:`required` 之外的文档是可见的 backlog`--list`),不是红的 CI,因此配对按可评审的批次落地,无需一个巨型 PR。
- 记录的 hash 兼作更新工具(`git cat-file -p <hash>` 能还原任一侧上次确认的文本,用于基于 diff 的最小更新),所以这套机制从不强迫整篇重译。