From 3b428c6637d22f8e23ee8817636a3d45ba253117 Mon Sep 17 00:00:00 2001 From: tukuaiai Date: Tue, 5 May 2026 03:48:41 +0800 Subject: [PATCH] chore: govern metadata redirects --- metadata/AGENTS.md | 2 ++ metadata/README.md | 2 +- metadata/redirects.yml | 21 +++++++++++-------- scripts/check-metadata.py | 43 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 59 insertions(+), 9 deletions(-) diff --git a/metadata/AGENTS.md b/metadata/AGENTS.md index 27b4ea0..ea9a675 100644 --- a/metadata/AGENTS.md +++ b/metadata/AGENTS.md @@ -8,6 +8,8 @@ - 历史路径仍需被 AI 或外部说明理解时,维护 `redirects.yml`。 - 术语口径变化时,同步 `glossary.yml`。 - 不确定的映射不要猜;先查当前文件和锚点,再修改。 +- `redirects.yml` 只记录已经不存在的历史路径到当前入口的映射;禁止添加指向自身的自映射。 +- `redirects.yml` 的 `from` 必须唯一,且不能仍然存在于仓库中。 ## 验证 diff --git a/metadata/README.md b/metadata/README.md index 07bf8a4..057990b 100644 --- a/metadata/README.md +++ b/metadata/README.md @@ -6,7 +6,7 @@ - `taxonomy.yml`:知识库分类、阅读路径和关键文档入口。 - `glossary.yml`:项目术语表。 -- `redirects.yml`:历史路径到当前入口的映射,供维护和 AI 上下文使用。 +- `redirects.yml`:已经不存在的历史路径到当前入口的映射,供维护和 AI 上下文使用。 ## 使用 diff --git a/metadata/redirects.yml b/metadata/redirects.yml index 1b362c3..acac388 100644 --- a/metadata/redirects.yml +++ b/metadata/redirects.yml @@ -1,4 +1,5 @@ redirects: + # Legacy directory moves. - from: docs/guides/getting-started/ to: docs/getting-started/ - from: docs/principles/fundamentals/ @@ -7,6 +8,8 @@ redirects: to: docs/philosophy/ - from: docs/concepts/philosophy/ to: docs/philosophy/ + + # Merged concept documents. - from: docs/concepts/思维模型.md to: docs/philosophy/README.md#philosophy-thinking-models - from: docs/concepts/编程之道.md @@ -33,6 +36,8 @@ redirects: to: docs/philosophy/README.md#philosophy-methodology-toolbox - from: docs/philosophy/理解世界、描述变化、整理知识的一套较小框架.md to: docs/philosophy/README.md#philosophy-compositional-description-model + + # Removed playbook directories and documents. - from: docs/guides/playbook/ to: docs/README.md - from: docs/playbooks/ @@ -45,6 +50,8 @@ redirects: to: docs/README.md - from: docs/playbooks/tradecat-sheets-api-usage.md to: docs/README.md + + # Merged getting-started documents. - from: docs/getting-started/Codex-CLI配置.md to: docs/getting-started/README.md - from: docs/getting-started/学习地图.md @@ -57,6 +64,8 @@ redirects: to: docs/getting-started/README.md - from: docs/getting-started/开发环境搭建.md to: docs/getting-started/README.md + + # Merged reference documents. - from: docs/references/通用项目架构模板.md to: docs/references/README.md#reference-engineering-practice - from: docs/references/数据集导向数据服务模板.md @@ -77,12 +86,8 @@ redirects: to: docs/references/README.md#reference-engineering-practice - from: docs/references/底层程序逻辑设计与工程优化项.md to: docs/references/README.md#reference-engineering-practice - - from: skills/ - to: skills/ - - from: prompts/ - to: prompts/ - - from: tools/external/ - to: tools/external/ + + # Moved top-level resource directories. - from: assets/documents/ to: docs/ - from: assets/skills/ @@ -93,8 +98,8 @@ redirects: to: tools/external/ - from: assets/config/ to: tools/config/ - - from: tools/config/ - to: tools/config/ + + # Stable legacy per-topic entry points. - from: docs/concepts/问题求解.md to: docs/concepts/README.md#concept-problem-solving - from: docs/concepts/拼好码.md diff --git a/scripts/check-metadata.py b/scripts/check-metadata.py index fed911a..e6b44f8 100644 --- a/scripts/check-metadata.py +++ b/scripts/check-metadata.py @@ -82,6 +82,47 @@ def metadata_targets(path: Path) -> list[tuple[int, str]]: return targets +def redirect_pairs(path: Path) -> list[tuple[int, str, str]]: + pairs: list[tuple[int, str, str]] = [] + current_from: tuple[int, str] | None = None + + for lineno, line in enumerate(path.read_text(encoding="utf-8", errors="ignore").splitlines(), start=1): + stripped = line.strip() + if stripped.startswith("- from:"): + _, value = stripped.split(":", 1) + current_from = (lineno, value.strip().strip("\"'")) + continue + if stripped.startswith("to:") and current_from: + _, value = stripped.split(":", 1) + pairs.append((current_from[0], current_from[1], value.strip().strip("\"'"))) + current_from = None + + return pairs + + +def validate_redirects(path: Path) -> list[str]: + errors: list[str] = [] + seen_from: dict[str, int] = {} + + for lineno, source, target in redirect_pairs(path): + normalized_source = source.rstrip("/") + normalized_target = target.split("#", 1)[0].rstrip("/") + + if normalized_source == normalized_target: + errors.append(f"{path}:{lineno}: no-op redirect maps '{source}' to itself") + + if source in seen_from: + errors.append(f"{path}:{lineno}: duplicate redirect source '{source}', first seen at line {seen_from[source]}") + else: + seen_from[source] = lineno + + source_path = ROOT / source + if source_path.exists(): + errors.append(f"{path}:{lineno}: redirect source still exists in repository: {source}") + + return errors + + def validate_target(source: Path, lineno: int, raw: str, anchor_cache: dict[Path, set[str]]) -> str | None: if not raw or raw.startswith(("http://", "https://", "mailto:", "tel:")): return None @@ -122,6 +163,8 @@ def main() -> int: error = validate_target(rel_path, lineno, target, anchor_cache) if error: errors.append(error) + if rel_path == Path("metadata/redirects.yml"): + errors.extend(validate_redirects(rel_path)) if errors: print("METADATA_ERRORS")