diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml
index 18bebc5..f55d339 100644
--- a/.github/workflows/validate.yml
+++ b/.github/workflows/validate.yml
@@ -24,6 +24,7 @@ jobs:
- run: python tests/test_generated_views.py
- run: python tests/test_markdown_links.py
- run: python tests/test_locale_invariants.py
+ - run: python tests/test_locale_freshness_regressions.py
- name: Check localized source freshness
env:
BEFORE: ${{ github.event.before }}
diff --git a/docs/localization/TRANSLATION_SURFACE.md b/docs/localization/TRANSLATION_SURFACE.md
index 41e4d22..83e41f7 100644
--- a/docs/localization/TRANSLATION_SURFACE.md
+++ b/docs/localization/TRANSLATION_SURFACE.md
@@ -1,6 +1,6 @@
# Translation Surface Inventory
-Status: **implemented first-release inventory; Wave 4 source-alignment update in progress**
+Status: **current supported-locale inventory**
Canonical source language: English (`en`)
First supported locale: Simplified Chinese (`zh-CN`)
@@ -11,7 +11,7 @@ First supported locale: Simplified Chinese (`zh-CN`)
- **C - keep canonical, localize around it:** machine/protocol structure should remain unchanged; translated explanation may be added elsewhere.
- **D - defer:** historical, empty runtime, binary, or otherwise outside the first localization release.
-For `zh-CN`, the first-release Class A route and selected Class B support surfaces were implemented and reviewed against their then-current English sources. Canonical changes merged after that review are explicitly tracked as Wave 4 source-alignment work; a class is a treatment decision, not a claim that its current English revision is already localized. Current locale support status is recorded in `locales/README.md`.
+For `zh-CN`, the Class A operational route and selected Class B support surfaces are implemented. A class is a treatment decision, not a claim that every current English document in that class has a localized counterpart. Current locale support status is recorded in `locales/README.md`; source/counterpart freshness is enforced for existing pairs.
## Top-level files
@@ -30,6 +30,7 @@ For `zh-CN`, the first-release Class A route and selected Class B support surfac
| Path | Class | Treatment |
| --- | --- | --- |
+| `docs/README.md` | A | Localized documentation index; route readers to localized operational guidance when a counterpart exists |
| `docs/ACQUISITION.md` | A | Full localized onboarding; commands/URLs unchanged |
| `docs/quickstart.md` | A | Full localized walkthrough; paths/identifiers unchanged |
| `docs/visitor_lobby_model.md` | A | Translate carefully; `visitor` has project-specific meaning |
@@ -44,10 +45,16 @@ For `zh-CN`, the first-release Class A route and selected Class B support surfac
| `docs/CONNECTOR_SAFE_WORDING.md` | B | Translate after core route, preserving compatibility intent |
| `docs/branding.md` | A | Translate explanation; product names remain unchanged |
| `docs/UPGRADING.md` | B | Translate as an upgrade companion after the core operational route is current |
-| `docs/registry/*.md` | B | Translate as Registry Contract v1 reference companions; preserve fields, enums, paths, and examples structurally |
+| `docs/WHY_LABNOTE.md` | B | Localized conceptual companion explaining how LabNote fits beside context files and model memory |
+| `docs/PROJECT_PROVENANCE.md` | B | Localized project-provenance record |
+| active Registry Contract v1 references under `docs/registry/` | B | Canonical English reference set; localized companions are deferred until a dedicated reference-translation need is established |
+| `docs/registry/REGISTRY_SURFACE_AUDIT.md` | D | Keep as a canonical historical pre-v1 audit; it is not current operational guidance |
| `docs/localization/GLOSSARY.md` | B | Maintain the canonical terminology source together with its explicitly mapped locale glossary |
| `docs/localization/TAG_DISPLAY_CATALOG.md` | B | Maintain canonical display text together with its explicitly mapped localized catalogue |
-| other `docs/localization/` process files | D | Keep canonical unless a separate localization-maintenance need is established |
+| `docs/localization/COMMUNICATION_REGISTRY_SOURCE_DRIFT.md` | D | Keep canonical as localization-maintenance history |
+| `docs/localization/LOCALIZATION_CONTRACT.md` | D | Keep canonical as the maintainer-facing localization contract |
+| `docs/localization/TRANSLATION_SURFACE.md` | D | Keep canonical as the maintainer-facing inventory |
+| `docs/localization/ZH_CN_VALIDATION.md` | D | Keep canonical as the zh-CN validation record |
## `lobby/`
diff --git a/locales/zh-CN/docs/README.md b/locales/zh-CN/docs/README.md
index 96ff2d6..4e2ff84 100644
--- a/locales/zh-CN/docs/README.md
+++ b/locales/zh-CN/docs/README.md
@@ -12,7 +12,7 @@
## 工作
-- [进行常规投递](../../../lobby/ROUTINE_DEPOSIT_QUICKSTART.md) —— 以最小但有用的记录,把一项普通工作放到正确路径中。
+- [进行常规投递](../lobby/ROUTINE_DEPOSIT_QUICKSTART.md) —— 以最小但有用的记录,把一项普通工作放到正确路径中。
- [理解审阅与决定](review_workflow.md) —— 查看回复如何成为经过审阅的结果。
- [理解访客会话与会话身份](visitor_lobby_model.md) —— 了解实际工作区中的会话记录代表什么。
- [理解消息路由](message_routing_model.md) —— 遵循会话或角色之间的消息模型。
@@ -22,7 +22,7 @@
- [阅读存储策略](storage_policy.md) —— 让台账保持精简、可检查,并符合工作区可见性的要求。
- [阅读文档投递策略](DOCUMENT_DEPOSIT_POLICY.md) —— 处理文档和二进制文件,避免把 LabNote 变成普通文件堆。
-- [阅读安全说明](../../../SECURITY.md) —— 不要把凭证、token、密钥或无意暴露的私密材料放入错误的工作区。
+- [阅读安全说明](../SECURITY.md) —— 不要把凭证、token、密钥或无意暴露的私密材料放入错误的工作区。
## 了解项目
diff --git a/tests/test_locale_freshness.py b/tests/test_locale_freshness.py
index 19aa632..46a22ed 100644
--- a/tests/test_locale_freshness.py
+++ b/tests/test_locale_freshness.py
@@ -10,6 +10,7 @@
import hashlib
import json
+import re
import subprocess
import sys
from pathlib import Path
@@ -26,17 +27,51 @@
}
-def paired_surfaces() -> dict[str, str]:
- pairs: dict[str, str] = {}
+def source_for_locale(relative: str) -> str:
+ return SOURCE_OVERRIDES.get(relative, relative)
+
+
+def locale_surfaces() -> dict[str, str]:
+ surfaces: dict[str, str] = {}
for locale_path in sorted(LOCALE_ROOT.rglob("*.md")):
locale_rel = locale_path.relative_to(ROOT).as_posix()
relative = locale_path.relative_to(LOCALE_ROOT).as_posix()
- source_rel = SOURCE_OVERRIDES.get(relative, relative)
+ surfaces[locale_rel] = source_for_locale(relative)
+ return surfaces
+
+
+def paired_surfaces() -> dict[str, str]:
+ pairs: dict[str, str] = {}
+ for locale_rel, source_rel in locale_surfaces().items():
if (ROOT / source_rel).is_file():
pairs[source_rel] = locale_rel
return pairs
+def files_at(revision: str) -> set[str]:
+ result = subprocess.run(
+ ["git", "ls-tree", "-r", "--name-only", revision],
+ cwd=ROOT,
+ check=True,
+ text=True,
+ capture_output=True,
+ )
+ return {line for line in result.stdout.splitlines() if line}
+
+
+def paired_surfaces_at(revision: str) -> dict[str, str]:
+ files = files_at(revision)
+ pairs: dict[str, str] = {}
+ prefix = "locales/zh-CN/"
+ for locale_rel in sorted(
+ path for path in files if path.startswith(prefix) and path.endswith(".md")
+ ):
+ source_rel = source_for_locale(locale_rel.removeprefix(prefix))
+ if source_rel in files:
+ pairs[source_rel] = locale_rel
+ return pairs
+
+
def digest(path: Path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest()
@@ -64,7 +99,9 @@ def acknowledgements(pairs: dict[str, str]) -> dict[str, str]:
reason = entry.get("reason")
if not isinstance(source, str) or source not in pairs:
raise AssertionError(f"invalid acknowledgement source: {source!r}")
- if not isinstance(source_sha256, str) or len(source_sha256) != 64:
+ if not isinstance(source_sha256, str) or not re.fullmatch(
+ r"[0-9a-f]{64}", source_sha256
+ ):
raise AssertionError(f"invalid source_sha256 for {source!r}")
if not isinstance(reason, str) or not reason.strip():
raise AssertionError(f"missing review reason for {source!r}")
@@ -79,10 +116,24 @@ def main() -> int:
raise SystemExit("usage: test_locale_freshness.py