#!/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 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 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 rel_path == Path("metadata/redirects.yml"): errors.extend(validate_redirects(rel_path)) 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())