diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2ef64b8..7b89926 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,6 +28,8 @@ jobs: run: python3 scripts/check-local-links.py - name: Check markdown details and summaries run: python3 scripts/check-markdown-details.py + - name: Check metadata paths and anchors + run: python3 scripts/check-metadata.py link-checker: runs-on: ubuntu-latest diff --git a/AGENTS.md b/AGENTS.md index 4ce652c..c3e8ef4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,7 +9,7 @@ ### 允许的操作 - 读取、修改顶层文档:`README.md`、`AGENTS.md`、`CONTRIBUTING.md` 等 - 读取、修改 `docs/`、`prompts/`、`skills/`、`tools/config/`、`tools/external/` 下的文档与代码 -- 执行 `make lint`、`make check-links`、`make check-details`、prompts-library 转换工具 +- 执行 `make lint`、`make check-links`、`make check-details`、`make check-metadata`、prompts-library 转换工具 - 新增/修改提示词、技能、文档 - 提交符合规范的 commit @@ -66,6 +66,7 @@ git push origin develop | `make lint` | 校验全仓库 Markdown | 需安装 markdownlint-cli | | `make check-links` | 校验仓库内 Markdown 相对链接 | Python 3 | | `make check-details` | 校验 Markdown 折叠块 `
/` 结构 | Python 3 | +| `make check-metadata` | 校验 metadata 路径与锚点 | Python 3 | | `make test` | 执行本地质量门禁 | Node.js 22+、Python 3 | | `git submodule update --init --recursive` | 初始化外部 Git 仓库指针 | Git | | `cd tools/prompts-library && python3 main.py` | 提示词格式转换 | `pip install -r tools/prompts-library/requirements.txt` | @@ -211,6 +212,7 @@ git push origin develop - `.github/workflows/ci.yml` - GitHub Actions:develop/master 分支 markdown-lint + link-checker - `scripts/check-local-links.py` - 仓库内 Markdown 相对链接与锚点检查脚本,供 `make check-links` 与 CI 使用 - `scripts/check-markdown-details.py` - 仓库内 Markdown 折叠块结构检查脚本,供 `make check-details` 与 CI 使用 +- `scripts/check-metadata.py` - metadata 路径与锚点检查脚本,供 `make check-metadata` 与 CI 使用 - `tools/prompts-library/main.py` - 提示词转换工具入口 - `docs/getting-started/README.md` - 从零开始完整入门,包含学习地图、Vibe Coding 经验、网络配置、CLI 配置与开发环境搭建 - `docs/concepts/README.md#concept-problem-solving` - 问题定义与求解路径底层模型 @@ -259,7 +261,8 @@ feat|fix|docs|chore|refactor|test: scope - summary 1. `markdown-lint` - Markdown 格式检查 2. `check local markdown links and anchors` - 仓库内相对链接与锚点检查 3. `check markdown details and summaries` - Markdown 折叠块结构检查 -4. `link-checker` - 链接有效性检查 +4. `check metadata paths and anchors` - metadata 路径与锚点检查 +5. `link-checker` - 链接有效性检查 ### 提交前清单 - [ ] 运行 `make lint` 通过 @@ -316,7 +319,7 @@ make test 2. **Conversion Tool**: 使用 Python + pandas + openpyxl 3. **Documentation Standard**: 用户文档使用中文;代码/文件名使用英文 4. **Skills**: 每个技能有独立的 `SKILL.md` -5. **Quality Gates**: `make test` 执行 Markdown lint、本地相对链接/锚点检查与折叠块结构检查 +5. **Quality Gates**: `make test` 执行 Markdown lint、本地相对链接/锚点检查、折叠块结构检查与 metadata 路径检查 ## Development Workflow diff --git a/Makefile b/Makefile index 701caf5..c8d9404 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ # Makefile for Vibe Coding Guide -.PHONY: help lint check-links check-details build test clean +.PHONY: help lint check-links check-details check-metadata build test clean help: @echo "Makefile for Vibe Coding Guide" @@ -10,6 +10,7 @@ help: @echo " lint - Lint all markdown files" @echo " check-links - Check local markdown links and anchors" @echo " check-details - Check markdown details/summary blocks" + @echo " check-metadata - Check metadata paths and anchors" @echo " build - Verify knowledge base has no build step" @echo " test - Run repository quality gates" @echo " clean - Remove ignored generated caches" @@ -28,10 +29,14 @@ check-details: @echo "Checking markdown details/summary blocks..." @python3 scripts/check-markdown-details.py +check-metadata: + @echo "Checking metadata paths and anchors..." + @python3 scripts/check-metadata.py + build: @echo "No build step: this repository is a documentation and knowledge-base project." -test: lint check-links check-details +test: lint check-links check-details check-metadata @echo "Quality gates complete." clean: diff --git a/README.md b/README.md index faf67eb..5e126d7 100644 --- a/README.md +++ b/README.md @@ -216,6 +216,7 @@ pip install -r tools/prompts-library/scripts/requirements.txt | 全仓 Markdown lint | `make lint` | `Makefile` + `.github/lint_config.json` | | 本地相对链接检查 | `make check-links` | `scripts/check-local-links.py` | | 折叠块结构检查 | `make check-details` | `scripts/check-markdown-details.py` | +| Metadata 路径检查 | `make check-metadata` | `scripts/check-metadata.py` | | 全部本地质量门禁 | `make test` | `Makefile` | | 提示词格式转换 | `cd tools/prompts-library && python3 main.py` | `tools/prompts-library/main.py` | | Skill 严格校验示例 | `skills/auto-skill/scripts/validate-skill.sh skills/auto-skill --strict` | `skills/auto-skill/scripts/validate-skill.sh` | diff --git a/metadata/redirects.yml b/metadata/redirects.yml index b6e611e..1b362c3 100644 --- a/metadata/redirects.yml +++ b/metadata/redirects.yml @@ -2,7 +2,7 @@ redirects: - from: docs/guides/getting-started/ to: docs/getting-started/ - from: docs/principles/fundamentals/ - to: docs/concepts/ 或 docs/references/ + to: docs/README.md - from: docs/principles/philosophy/ to: docs/philosophy/ - from: docs/concepts/philosophy/ diff --git a/scripts/README.md b/scripts/README.md index 919b6e4..a768181 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -6,3 +6,4 @@ - `check-local-links.py`:仓库内 Markdown 相对链接与锚点检查脚本。 - `check-markdown-details.py`:仓库内 Markdown `
/` 折叠块结构检查脚本。 +- `check-metadata.py`:`metadata/taxonomy.yml` 与 `metadata/redirects.yml` 路径和锚点检查脚本。 diff --git a/scripts/check-metadata.py b/scripts/check-metadata.py new file mode 100644 index 0000000..fed911a --- /dev/null +++ b/scripts/check-metadata.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python3 +"""Check metadata path references resolve to real repository paths and anchors.""" + +from __future__ import annotations + +import re +import sys +import urllib.parse +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +METADATA_FILES = [ + Path("metadata/taxonomy.yml"), + Path("metadata/redirects.yml"), +] + + +def github_slug(title: str) -> str: + title = re.sub(r"<[^>]+>", "", title.strip().lower()) + title = re.sub(r"[`*_~]", "", title) + title = re.sub(r"[^\w\u4e00-\u9fff\- ]+", "", title) + title = re.sub(r"\s+", "-", title).strip("-") + return title + + +def markdown_anchors(path: Path) -> set[str]: + text = path.read_text(encoding="utf-8", errors="ignore") + anchors = set(re.findall(r" list[tuple[int, str]]: + targets: list[tuple[int, str]] = [] + text = path.read_text(encoding="utf-8", errors="ignore") + + for lineno, line in enumerate(text.splitlines(), start=1): + stripped = line.strip() + if stripped.startswith(("path:", "entry:", "agent_guide:", "to:")): + _, value = stripped.split(":", 1) + targets.append((lineno, value.strip().strip("\"'"))) + continue + if stripped.startswith("- "): + value = stripped[2:].strip().strip("\"'") + if value.startswith("from:"): + continue + if value.startswith(("path:", "entry:", "agent_guide:", "to:")): + _, value = value.split(":", 1) + targets.append((lineno, value.strip().strip("\"'"))) + continue + if value.startswith((".", "/")) or "/" in value or value.endswith(".md"): + targets.append((lineno, value)) + + return targets + + +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 + if " 或 " in raw: + return f"{source}:{lineno}: ambiguous metadata target: {raw}" + + target = urllib.parse.unquote(raw.strip("<>")) + path_part, _, anchor = target.partition("#") + destination = (ROOT / path_part).resolve() + + try: + destination.relative_to(ROOT) + except ValueError: + return None + + if not destination.exists(): + return f"{source}:{lineno}: missing metadata target: {raw}" + if anchor and destination.suffix.lower() == ".md": + if destination not in anchor_cache: + anchor_cache[destination] = markdown_anchors(destination) + if anchor not in anchor_cache[destination]: + rel = destination.relative_to(ROOT) + return f"{source}:{lineno}: missing metadata anchor: {rel}#{anchor}" + + return None + + +def main() -> int: + errors: list[str] = [] + anchor_cache: dict[Path, set[str]] = {} + + for rel_path in METADATA_FILES: + path = ROOT / rel_path + if not path.exists(): + errors.append(f"{rel_path}: missing metadata file") + continue + for lineno, target in metadata_targets(path): + error = validate_target(rel_path, lineno, target, anchor_cache) + if error: + errors.append(error) + + if errors: + print("METADATA_ERRORS") + for error in errors: + print(error) + print(f"TOTAL={len(errors)}") + return 1 + + print("OK metadata paths checked") + return 0 + + +if __name__ == "__main__": + sys.exit(main())