diff --git a/README.md b/README.md
index 071089c..48c7116 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,7 @@
# partme-codeguard-plugin Plugin
+> Parity note: `README.md` and `README.zh-CN.md` must stay structurally aligned (heading levels, local links, version strings) — enforced by `tests/test_readme_parity.py`; mirror any structural edit into both files in the same commit.
+
@@ -13,7 +15,7 @@
English ·
简体中文 ·
Architecture ·
- Technical roadmap
+ Technical roadmap
---
@@ -40,7 +42,7 @@ It is a **constraint-type plugin** for AI assistants, not a productivity-type pl
| AI uses `unwrap()` in Rust business code | `cargo clippy -- -D warnings` runs on each `.rs` file | [Architecture §2.1](docs/partme-codeguard-plugin-Architecture.zh_CN.md) |
| AI introduces `any` and unused vars in TypeScript | `eslint --max-warnings 0` blocks the AI | `linters/eslint/recommended.cjs` |
| "did it pass lint?" is asked manually after every AI session | Stop hook summarizes lint pass/fail counts | `hooks/stop_summary.py` |
-| pre-commit and CI catch issues 30s+5min late, by then the AI has moved on | Three-layer defense: hook (<2s) → pre-commit (30s) → CI (5min) | [Technical roadmap §1](docs/5、partme-codeguard-plugin-技术方案与路线.md) |
+| pre-commit and CI catch issues 30s+5min late, by then the AI has moved on | Three-layer defense: hook (<2s) → pre-commit (30s) → CI (5min) | [Technical roadmap §1](docs/technical-roadmap.zh_CN.md) |
## At a glance
@@ -65,7 +67,7 @@ AI code that passes lint on first try
|---|---|
| Plugin ID | `partme-codeguard-plugin` |
| Hosts | ZCode, Claude Code, Codex CLI, Kimi Code |
-| Current version | `0.5.4` |
+| Current version | `0.6.7` |
| ZCode manifest | `.zcode-plugin/plugin.json` |
| Codex manifest | `.codex-plugin/plugin.json` |
| MCP server | Not published until the SDK-backed protocol implementation is ready; use the CLI and hooks |
@@ -104,7 +106,7 @@ The commit gate is pre-wired in the pre-commit template (`stages: [commit-msg]`)
### External skill source
-The 68 portable skills are authored in [full-stack-skills/codeguard-skills](https://github.com/full-stack-skills/codeguard-skills), not independently inside this plugin. This repository vendors the complete `v0.1.0` snapshot so installed plugins work offline:
+The 68 portable skills are authored in [full-stack-skills/codeguard-skills](https://github.com/full-stack-skills/codeguard-skills), not independently inside this plugin. This repository vendors the complete `v0.1.2` snapshot so installed plugins work offline:
- `skills.lock.json` pins the upstream repository, immutable tag, resolved commit, managed skill names, and per-skill SHA-256 digests.
- `python3 scripts/vendor/skill_vendor.py update` refreshes only the skill names listed in the lock.
@@ -151,6 +153,8 @@ PostToolUse is the **highest-ROI** layer because it gives the AI feedback **whil
### CLI (codeguard)
+`bin/codeguard` is a bash dispatcher: each subcommand (`check` / `fix` / `cve` / `dockerfile` / `detect`) routes to the matching `scripts/*.py` implementation.
+
```bash
# Optional one-time setup: put the CLI on PATH
ln -s $PWD/bin/codeguard /usr/local/bin/codeguard
@@ -233,7 +237,7 @@ partme-codeguard-plugin/
│ └── vendor/skill_vendor.py # lock-driven external skill vendor/check
├── skills.lock.json # upstream tag/commit + managed skills + SHA-256 digests
├── plugin-local-skills.json # explicit plugin-only skill exceptions (currently empty)
-├── skills/ # 68 vendored skills from codeguard-skills v0.1.0
+├── skills/ # 68 vendored skills from codeguard-skills v0.1.2
│ ├── codeguard/ # main entry
│ ├── codeguard-init/ # one-line bootstrap
│ ├── codeguard-{java,rust,typescript,python}/
@@ -255,7 +259,7 @@ partme-codeguard-plugin/
│ └── pre-commit/ # .pre-commit-config.template.yaml
├── docs/
│ ├── partme-codeguard-plugin-Architecture.zh_CN.md
-│ └── 5、partme-codeguard-plugin-技术方案与路线.md
+│ └── technical-roadmap.zh_CN.md
├── README.md # this file
├── README.zh-CN.md # Chinese
├── LICENSE # Apache-2.0
diff --git a/README.zh-CN.md b/README.zh-CN.md
index b9a8157..637cd1b 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -1,5 +1,7 @@
# partme-codeguard-plugin 插件
+> 结构对齐:`README.md` 与 `README.zh-CN.md` 必须保持结构一致(标题层级、本地链接、版本串)——由 `tests/test_readme_parity.py` 门禁守护;结构性修改须同 commit 镜像到两份文件。
+
@@ -13,7 +15,7 @@
English ·
简体中文 ·
架构文档 ·
- 技术方案
+ 技术方案
---
@@ -40,7 +42,7 @@
| AI 在 Rust 业务代码用 `unwrap()` | `cargo clippy -- -D warnings` 跑每个 `.rs` 文件 | [架构文档 §2.1](docs/partme-codeguard-plugin-Architecture.zh_CN.md) |
| AI 写 `any` 和未用变量 | `eslint --max-warnings 0` 阻塞 AI | `linters/eslint/recommended.cjs` |
| 每次会话都要手工问「过 lint 了吗?」 | Stop 钩子自动总结本会话 lint 通过/失败次数 | `hooks/stop_summary.py` |
-| pre-commit / CI 发现时 AI 已切走,修复率 <30% | **三层防御**:钩子(<2s)→ pre-commit(30s)→ CI(5min) | [技术方案 §1](docs/5、partme-codeguard-plugin-技术方案与路线.md) |
+| pre-commit / CI 发现时 AI 已切走,修复率 <30% | **三层防御**:钩子(<2s)→ pre-commit(30s)→ CI(5min) | [技术方案 §1](docs/technical-roadmap.zh_CN.md) |
## 一览
@@ -65,7 +67,7 @@ AI 一次写出就过 lint 的代码
|---|---|
| 插件 ID | `partme-codeguard-plugin` |
| 宿主 | ZCode、Claude Code、Codex CLI、Kimi Code |
-| 当前版本 | `0.5.4` |
+| 当前版本 | `0.6.7` |
| ZCode manifest | `.zcode-plugin/plugin.json` |
| Codex manifest | `.codex-plugin/plugin.json` |
| MCP 服务 | SDK 协议实现完成前不随清单发布;当前使用 CLI 与 Hooks |
@@ -86,9 +88,11 @@ AI 一次写出就过 lint 的代码
> 此前其 lint 命令缺 glob、恒以用法错误退出,现已返回真实结论。`codeguard init` 会拷入宽松配置模板。
| **Planned**(4 种,无独立 CLI linter) | Metal、ArkTS(HarmonyOS)、COBOL、Liquid(theme-check 待接通) |
-## 外部技能来源
+## 治理技能(Git 与安全)
+
+### 外部技能来源
-68 个可复用技能统一在 [full-stack-skills/codeguard-skills](https://github.com/full-stack-skills/codeguard-skills) 编写,插件不再维护一份独立手写副本。为保证插件安装后离线可用,本仓库 vendor 了完整的 `v0.1.0` 快照:
+68 个可复用技能统一在 [full-stack-skills/codeguard-skills](https://github.com/full-stack-skills/codeguard-skills) 编写,插件不再维护一份独立手写副本。为保证插件安装后离线可用,本仓库 vendor 了完整的 `v0.1.2` 快照:
- `skills.lock.json` 固定上游仓库、不可变 tag、解析后的 commit、受管技能清单与逐技能 SHA-256。
- `python3 scripts/vendor/skill_vendor.py update` 只刷新 lock 中列出的技能。
@@ -96,7 +100,7 @@ AI 一次写出就过 lint 的代码
- 不得直接修改 lock 管理的技能目录。应先在 `codeguard-skills` 修改并发布,再更新 lock ref 并执行 vendor update。
- 只有插件内部定制技能可以直接保留在 `skills/`,且必须明确不列入 `skills.lock.json`、显式登记到 `plugin-local-skills.json`;vendor 会保留已声明目录并拒绝未声明例外。
-Hooks、linters、commands、MCP 接线和可执行脚本仍由插件仓负责。
+Hooks、linters、commands、MCP 接线和可执行脚本仍由插件仓负责。作者编写规范见 [docs/CODEGUARD_SKILLS_SPEC.md](docs/CODEGUARD_SKILLS_SPEC.md)。
## 能力与边界
@@ -135,6 +139,8 @@ PostToolUse 是 **最高 ROI** 的层,因为 AI 在它「还在乎这个问题
### CLI(codeguard)
+`bin/codeguard` 是 bash 分发器:每个子命令(`check` / `fix` / `cve` / `dockerfile` / `detect`)都路由到对应的 `scripts/*.py` 实现。
+
```bash
# 安装 CLI(可选):放到 PATH 后任意目录直接用
ln -s $PWD/bin/codeguard /usr/local/bin/codeguard
@@ -216,7 +222,7 @@ partme-codeguard-plugin/
│ └── vendor/skill_vendor.py # lock 驱动的外部技能 vendor/check
├── skills.lock.json # 上游 tag/commit + 受管技能 + SHA-256
├── plugin-local-skills.json # 插件专属技能显式例外清单(当前为空)
-├── skills/ # 从 codeguard-skills v0.1.0 vendor 的 68 个技能
+├── skills/ # 从 codeguard-skills v0.1.2 vendor 的 68 个技能
│ ├── codeguard/ # 主入口
│ ├── codeguard-init/ # 一行接入
│ ├── codeguard-{java,rust,typescript,python}/
@@ -238,7 +244,7 @@ partme-codeguard-plugin/
│ └── pre-commit/ # .pre-commit-config.template.yaml
├── docs/
│ ├── partme-codeguard-plugin-Architecture.zh_CN.md
-│ └── 5、partme-codeguard-plugin-技术方案与路线.md
+│ └── technical-roadmap.zh_CN.md
├── README.md # 本文件(英文)
├── README.zh-CN.md # 本文件(中文)
├── LICENSE # Apache-2.0
diff --git "a/docs/5\343\200\201partme-codeguard-plugin-\346\212\200\346\234\257\346\226\271\346\241\210\344\270\216\350\267\257\347\272\277.md" b/docs/technical-roadmap.zh_CN.md
similarity index 99%
rename from "docs/5\343\200\201partme-codeguard-plugin-\346\212\200\346\234\257\346\226\271\346\241\210\344\270\216\350\267\257\347\272\277.md"
rename to docs/technical-roadmap.zh_CN.md
index 9e13eee..c5b82c7 100644
--- "a/docs/5\343\200\201partme-codeguard-plugin-\346\212\200\346\234\257\346\226\271\346\241\210\344\270\216\350\267\257\347\272\277.md"
+++ b/docs/technical-roadmap.zh_CN.md
@@ -1,4 +1,4 @@
-# 5、partme-codeguard-plugin 技术方案与路线
+# PartMe CodeGuard 技术方案与路线
> **文档说明**:本文档描述 partme-codeguard-plugin 插件的技术选型、关键决策、ADR(架构决策记录)和版本路线图。
>
diff --git a/openspec/changes/archive/2026-09-22-add-readme-parity-gate/.openspec.yaml b/openspec/changes/archive/2026-09-22-add-readme-parity-gate/.openspec.yaml
new file mode 100644
index 0000000..a9eca53
--- /dev/null
+++ b/openspec/changes/archive/2026-09-22-add-readme-parity-gate/.openspec.yaml
@@ -0,0 +1,2 @@
+schema: spec-driven
+created: 2026-09-22
\ No newline at end of file
diff --git a/openspec/changes/archive/2026-09-22-add-readme-parity-gate/design.md b/openspec/changes/archive/2026-09-22-add-readme-parity-gate/design.md
new file mode 100644
index 0000000..7c89d94
--- /dev/null
+++ b/openspec/changes/archive/2026-09-22-add-readme-parity-gate/design.md
@@ -0,0 +1,47 @@
+## Context
+
+实测漂移证据(v0.6.6):
+
+1. **标题层级序列不等**(围栏外解析):
+ - 英文:`H2 Governance skills (Git & Security)` → `H3 External skill source`
+ - 中文:`H2 外部技能来源`(缺包裹 H2)
+ - 其余 20 个标题层级与顺序完全一致(H1×1 / H2×12 / H3×9 vs 修复前中文 H3×8)。
+2. **版本串停在 0.5.4**:`| Current version | 0.5.4 |`(双语),实际已发 0.6.6+。
+3. **vendor 快照串过期**:双语 4 处写 `v0.1.0`,`skills.lock.json.ref = v0.1.2`。
+4. **docs 文件名含全角逗号** `5、`:两份 README 共 6 处链接 + 文档 H1 引用。
+
+漂移根因:无门禁。曾考虑「英文 SSOT + 自动翻译生成中文」——被否:仓内无翻译管线,伪自动生成会把漂移换成假同步。选择**结构门禁 + 单 commit 镜像规则**:机器守结构(标题/链接/版本三类硬一致),人守译文。
+
+## Decisions
+
+### 门禁只守「结构可机检」三类
+
+`tests/test_readme_parity.py` 断言(全部跳过 ``` 围栏,避免把 bash 注释 `#` 当标题):
+
+1. 标题层级序列相等(`[1,2,3,2,...]`)——不要求文字相同(语言不同),只要求结构镜像。
+2. 本地链接目标集合相等(`](path)` 去掉 http/mailto/锚点)——docs 改名两边必须同时改。
+3. `x.y.z` 版本串集合相等——`Current version`、vendor 快照等串任一边漏改即失败。
+
+不守的:译文正确性(机器判不了)、图片 alt 文案、表格行数。
+
+### mirror-edit 规则写进文件与 spec
+
+两份 README 顶部各加一行 parity 注(互指 + 指向门禁测试);spec `bilingual-docs-consistency` 加 Requirement:任一 README 修改必须同 commit 镜像另一份,结构差异由门禁拦截、译文差异由 review 拦截。
+
+### 不做 SSOT 自动翻译
+
+原因如上;若未来引入翻译管线,可在本 spec 下 MODIFIED Requirement 升级为「单源生成」。
+
+### docs 改名目标与既有约定对齐
+
+`docs/` 既有 `partme-codeguard-plugin-Architecture.zh_CN.md`——新名 `technical-roadmap.zh_CN.md` 同风格(蛇形 + `.zh_CN.md` 后缀)。文档 H1 去掉序号前缀 `5、`。共 7 处引用更新(README en×3 / zh×3 / 文档 H1×1)。
+
+### 兼容性「已验证」行不升级
+
+`✅ V0.5.4 verified` 是历史验证记录;在未对 0.6.7 重做宿主安装验证前改写它 = 虚假声明。`Current version` 行是事实字段(当前发布版本),跟着 release 走,本次预写 `0.6.7`(与本 change 的发版一致)。
+
+## Risks / Trade-offs
+
+- **门禁只挡结构漂移**:译文漏改仍靠 review——两份 README 顶部的 parity 注 + spec Requirement 是软约束。
+- **`Current version` 行预写**:feat PR 合并到 release PR 之间存在短暂 README=0.6.7 / manifest=0.6.6 窗口;门禁不比对 manifest(只比对两份 README 互等),窗口内 CI 仍绿。release PR 合并后收敛。
+- **docs 改名断外部深链**:该文件是中文技术方案,外部引用概率低;GitHub 对改名文件不自动重定向(wiki/issue 内的旧链接会 404)。接受此风险换取文件名可移植性。
\ No newline at end of file
diff --git a/openspec/changes/archive/2026-09-22-add-readme-parity-gate/proposal.md b/openspec/changes/archive/2026-09-22-add-readme-parity-gate/proposal.md
new file mode 100644
index 0000000..e9a881d
--- /dev/null
+++ b/openspec/changes/archive/2026-09-22-add-readme-parity-gate/proposal.md
@@ -0,0 +1,26 @@
+## Why
+
+`README.md`(310 行,英文)与 `README.zh-CN.md`(293 行,简体中文)是双语镜像,但没有任何门禁守护一致性——MEDIUM #4 评审指出任一版本修改都需手工同步另一版本,实际已发生漂移:英文版有 `## Governance skills (Git & Security) → ### External skill source` 的两层嵌套,中文版是平铺的 `## 外部技能来源`;两份文件的 `Current version` 行都停在 `0.5.4`(实际 0.6.6+);vendor 快照串都写 `v0.1.0`(`skills.lock.json` 实为 `v0.1.2`)。
+
+## What Changes
+
+- 新增 `tests/test_readme_parity.py`:三条一致性断言(围栏外标题层级序列相等、本地链接目标集合相等、`x.y.z` 版本串集合相等),任一 README 单边修改即 CI 失败。
+- `README.zh-CN.md`:插入 `## 治理技能(Git 与安全)` 并把 `外部技能来源` 降级为 `###`,恢复与英文版一致的标题层级序列。
+- 双语同步修正(两份文件同 commit):`Current version` 行 `0.5.4 → 0.6.7`;vendor 快照串 `v0.1.0 → v0.1.2`(与 lock 一致);在 `### CLI` 段落补一句 `bin/codeguard` 分发器的说明(bash 子命令 → `scripts/*.py` 映射,回应评审 LOW #13)。
+- 两份 README 顶部各加一行 parity 说明(指向门禁测试)。
+- 文档改名(回应评审 LOW #14):`docs/5、partme-codeguard-plugin-技术方案与路线.md` → `docs/technical-roadmap.zh_CN.md`(与既有 `...Architecture.zh_CN.md` 命名对齐,消除全角逗号文件名的跨平台转义风险);同步更新两份 README 的 3 处链接(导航、表格引用、仓库树目录)与文档自身 H1。
+- `✅ V0.5.4 verified` 兼容性行**保持不动**——它们记录的是「在 v0.5.4 验证过」的历史事实,未在 0.6.7 重测就不改写。
+
+## Capabilities
+
+### New Capabilities
+
+- `bilingual-docs-consistency`: Defines the parity contract between README.md and README.zh-CN.md and the single-commit mirror-edit rule.
+
+### Modified Capabilities
+
+None.
+
+## Impact
+
+新增 1 个测试(~60 行);修改两份 README(结构修正 + 4 处同步串 + 2 行说明);`git mv` 1 个 docs 文件 + 7 处链接更新。无运行时行为变化。
\ No newline at end of file
diff --git a/openspec/changes/archive/2026-09-22-add-readme-parity-gate/specs/bilingual-docs-consistency/spec.md b/openspec/changes/archive/2026-09-22-add-readme-parity-gate/specs/bilingual-docs-consistency/spec.md
new file mode 100644
index 0000000..525217f
--- /dev/null
+++ b/openspec/changes/archive/2026-09-22-add-readme-parity-gate/specs/bilingual-docs-consistency/spec.md
@@ -0,0 +1,24 @@
+## ADDED Requirements
+
+### Requirement: README structural parity SHALL be enforced by tests/test_readme_parity.py
+
+`tests/test_readme_parity.py` SHALL assert that `README.md` and `README.zh-CN.md` have: (1) identical heading-level sequences outside fenced code blocks, (2) identical sets of local link targets, and (3) identical sets of `x.y.z` version strings. Any single-sided edit to one README SHALL fail this test.
+
+#### Scenario: The technical roadmap doc is renamed
+
+- **WHEN** `docs/5、partme-codeguard-plugin-技术方案与路线.md` is renamed and only `README.md` links are updated
+- **THEN** `test_readme_parity` fails on the local-link-target set difference
+
+#### Scenario: The current version row is bumped in one language only
+
+- **WHEN** `| Current version | 0.6.7 |` is updated in `README.md` but `README.zh-CN.md` still says `0.5.4`
+- **THEN** `test_readme_parity` fails on the version-string set difference
+
+### Requirement: README mirror edits SHALL land in the same commit
+
+Any change to one README's structure, links, or version strings SHALL be mirrored into the other README in the same commit. Both files SHALL carry a parity note near the top pointing at `tests/test_readme_parity.py`.
+
+#### Scenario: A contributor adds a new section to README.md
+
+- **WHEN** a new `## Section` is added to `README.md` with links and version strings
+- **THEN** the same commit adds the translated `## 章节` to `README.zh-CN.md` at the same position, and CI passes
\ No newline at end of file
diff --git a/openspec/changes/archive/2026-09-22-add-readme-parity-gate/tasks.md b/openspec/changes/archive/2026-09-22-add-readme-parity-gate/tasks.md
new file mode 100644
index 0000000..19c1fee
--- /dev/null
+++ b/openspec/changes/archive/2026-09-22-add-readme-parity-gate/tasks.md
@@ -0,0 +1,29 @@
+## 1. Parity gate
+
+- [x] 1.1 Create `tests/test_readme_parity.py` with three assertions (heading-level sequence / local link targets / version strings), fence-aware.
+- [x] 1.2 Run it against current READMEs; record every existing mismatch.
+
+## 2. Drift repairs (mirror both files in one commit)
+
+- [x] 2.1 zh: insert `## 治理技能(Git 与安全)` and demote `外部技能来源` to `###` so heading sequences match.
+- [x] 2.2 Both: `Current version` row `0.5.4 → 0.6.7`.
+- [x] 2.3 Both: vendor snapshot `v0.1.0 → v0.1.2` (4 places; matches `skills.lock.json`).
+- [x] 2.4 Both: add `bin/codeguard` dispatcher sentence in `### CLI` section (same position).
+- [x] 2.5 Both: add parity note after H1 pointing at the gate test.
+
+## 3. Docs rename (LOW #14)
+
+- [x] 3.1 `git mv 'docs/5、partme-codeguard-plugin-技术方案与路线.md' docs/technical-roadmap.zh_CN.md`.
+- [x] 3.2 Update 6 README links (en ×3, zh ×3) + document H1 (drop `5、` prefix).
+
+## 4. Validation
+
+- [x] 4.1 `python3 -m unittest discover -s tests -p 'test_*.py' -v` — all pass (incl. new parity test).
+- [x] 4.2 `python3 tests/run_all.py` — 141 / 0 / 0.
+- [x] 4.3 `git diff --check` — clean.
+- [x] 4.4 `openspec validate --all --strict` — pass.
+
+## 5. Release
+
+- [x] 5.1 bump patch → 0.6.7; fix in-repo marketplace pins; feat PR → merge; release PR → merge; tag v0.6.7; market repo catalog + README row → 0.6.7.
+- [x] 5.2 `openspec archive add-readme-parity-gate`.
\ No newline at end of file
diff --git a/openspec/specs/bilingual-docs-consistency/spec.md b/openspec/specs/bilingual-docs-consistency/spec.md
new file mode 100644
index 0000000..e4677cb
--- /dev/null
+++ b/openspec/specs/bilingual-docs-consistency/spec.md
@@ -0,0 +1,28 @@
+# bilingual-docs-consistency Specification
+
+## Purpose
+TBD - created by archiving change add-readme-parity-gate. Update Purpose after archive.
+## Requirements
+### Requirement: README structural parity SHALL be enforced by tests/test_readme_parity.py
+
+`tests/test_readme_parity.py` SHALL assert that `README.md` and `README.zh-CN.md` have: (1) identical heading-level sequences outside fenced code blocks, (2) identical sets of local link targets, and (3) identical sets of `x.y.z` version strings. Any single-sided edit to one README SHALL fail this test.
+
+#### Scenario: The technical roadmap doc is renamed
+
+- **WHEN** `docs/5、partme-codeguard-plugin-技术方案与路线.md` is renamed and only `README.md` links are updated
+- **THEN** `test_readme_parity` fails on the local-link-target set difference
+
+#### Scenario: The current version row is bumped in one language only
+
+- **WHEN** `| Current version | 0.6.7 |` is updated in `README.md` but `README.zh-CN.md` still says `0.5.4`
+- **THEN** `test_readme_parity` fails on the version-string set difference
+
+### Requirement: README mirror edits SHALL land in the same commit
+
+Any change to one README's structure, links, or version strings SHALL be mirrored into the other README in the same commit. Both files SHALL carry a parity note near the top pointing at `tests/test_readme_parity.py`.
+
+#### Scenario: A contributor adds a new section to README.md
+
+- **WHEN** a new `## Section` is added to `README.md` with links and version strings
+- **THEN** the same commit adds the translated `## 章节` to `README.zh-CN.md` at the same position, and CI passes
+
diff --git a/tests/test_readme_parity.py b/tests/test_readme_parity.py
new file mode 100644
index 0000000..f3360f2
--- /dev/null
+++ b/tests/test_readme_parity.py
@@ -0,0 +1,83 @@
+#!/usr/bin/env python3
+"""README.md 与 README.zh-CN.md 的结构一致性门禁(bilingual-docs-consistency spec)。
+
+守三类「机器可判定」的一致性(全部跳过 ``` 围栏,避免把 bash 注释当标题):
+1. 标题层级序列相等(语言不同、文字不同,但结构必须镜像);
+2. 本地链接目标集合相等(docs 改名必须两边同时改);
+3. `x.y.z` 版本串集合相等(Current version / vendor 快照等任一边漏改即失败)。
+
+译文正确性不在门禁范围——由两份文件顶部的 parity 注 + review 把守。
+"""
+from __future__ import annotations
+
+import re
+import unittest
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[1]
+EN = ROOT / "README.md"
+ZH = ROOT / "README.zh-CN.md"
+
+_HEADING_RE = re.compile(r"^(#{1,6})\s")
+_LINK_RE = re.compile(r"\]\(([^)]+)\)")
+_VERSION_RE = re.compile(r"\d+\.\d+\.\d+")
+
+
+def _unfenced(text: str) -> str:
+ """去掉 ``` 围栏内的行(bash 注释形如 # ... 会被误判成标题)。"""
+ out, fenced = [], False
+ for line in text.splitlines():
+ if line.lstrip().startswith("```"):
+ fenced = not fenced
+ continue
+ if not fenced:
+ out.append(line)
+ return "\n".join(out)
+
+
+def _heading_levels(text: str) -> list[int]:
+ return [len(m.group(1)) for m in
+ (_HEADING_RE.match(ln) for ln in _unfenced(text).splitlines()) if m]
+
+
+def _local_links(text: str) -> set[str]:
+ return {t for t in _LINK_RE.findall(_unfenced(text))
+ if not t.startswith(("http://", "https://", "mailto:", "#"))}
+
+
+def _versions(text: str) -> set[str]:
+ return set(_VERSION_RE.findall(text))
+
+
+class ReadmeParityTest(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls) -> None:
+ cls.en = EN.read_text(encoding="utf-8")
+ cls.zh = ZH.read_text(encoding="utf-8")
+
+ def test_both_files_exist(self) -> None:
+ self.assertTrue(EN.is_file(), "README.md missing")
+ self.assertTrue(ZH.is_file(), "README.zh-CN.md missing")
+
+ def test_parity_notes_present(self) -> None:
+ self.assertIn("tests/test_readme_parity.py", self.en, "en parity note missing")
+ self.assertIn("tests/test_readme_parity.py", self.zh, "zh parity note missing")
+
+ def test_heading_level_sequences_match(self) -> None:
+ en_h, zh_h = _heading_levels(self.en), _heading_levels(self.zh)
+ self.assertEqual(en_h, zh_h,
+ msg=f"heading-level sequence drift:\n en={en_h}\n zh={zh_h}")
+
+ def test_local_link_targets_match(self) -> None:
+ en_l, zh_l = _local_links(self.en), _local_links(self.zh)
+ self.assertEqual(en_l, zh_l,
+ msg=f"link drift: only_en={en_l - zh_l} only_zh={zh_l - en_l}")
+
+ def test_version_strings_match(self) -> None:
+ en_v, zh_v = _versions(self.en), _versions(self.zh)
+ self.assertEqual(en_v, zh_v,
+ msg=f"version drift: only_en={en_v - zh_v} only_zh={zh_v - en_v}")
+
+
+if __name__ == "__main__":
+ unittest.main()