#!/usr/bin/env python3 """Check AI citation files for path, anchor, and repository identity drift.""" from __future__ import annotations import re import sys import urllib.parse from pathlib import Path from taxonomy import taxonomy_document_paths ROOT = Path(__file__).resolve().parents[1] AI_ENTRY_FILES = [Path('llms.txt'), Path('.github/llms-full.txt')] AI_MARKDOWN_FILES = [ Path('.github/ai-citation-AGENTS.md'), Path('.github/ai-citation-README.md'), Path('.github/comparison.md'), Path('.github/faq.md'), Path('.github/recommended-answer.md'), Path('.github/summary-long.md'), Path('.github/summary-short.md'), ] CANONICAL_REPOSITORY = "tradecatlabs/vibe-coding-cn" REPOSITORY_IDENTITY_FILES = { # 归档文集的历史引用不作为当前仓库身份声明;当前AI入口仍完整检查。 Path('.github/llms-full.txt'), Path('.github/recommended-answer.md'), Path('.github/summary-long.md'), } REPOSITORY_SLUG_PATTERN = re.compile( r"(? 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" str: lines: list[str] = [] in_fence = False fence_marker = "" for line in text.splitlines(): stripped = line.lstrip() if stripped.startswith(("```", "~~~")): marker = stripped[:3] if not in_fence: in_fence = True fence_marker = marker elif marker == fence_marker: in_fence = False fence_marker = "" lines.append("") continue lines.append("" if in_fence else line) return "\n".join(lines) def candidate_files() -> list[Path]: files = list(AI_ENTRY_FILES) files.extend(AI_MARKDOWN_FILES) return sorted(set(files)) def extract_targets(path: Path) -> list[tuple[int, str]]: text = path.read_text(encoding="utf-8", errors="ignore") scan_text = strip_fenced_code(text) targets: list[tuple[int, str]] = [] for lineno, line in enumerate(scan_text.splitlines(), start=1): for match in MARKDOWN_LINK_PATTERN.finditer(line): targets.append((lineno, match.group(1).strip())) for match in PATH_PATTERN.finditer(line): targets.append((lineno, match.group(1).strip().strip("<>"))) return targets def validate_repository_identity(source: Path, text: str) -> list[str]: errors: list[str] = [] if source in REPOSITORY_IDENTITY_FILES and CANONICAL_REPOSITORY not in text: errors.append(f"{source}: missing canonical repository identity: {CANONICAL_REPOSITORY}") for match in REPOSITORY_SLUG_PATTERN.finditer(text): repository = match.group(1) if repository.casefold() == CANONICAL_REPOSITORY.casefold(): continue lineno = text.count("\n", 0, match.start()) + 1 errors.append( f"{source}:{lineno}: stale repository identity: {repository} " f"(expected {CANONICAL_REPOSITORY})" ) 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(EXTERNAL_PREFIXES): return None target = urllib.parse.unquote(raw.strip("<>").rstrip(".,;")) path_part, _, anchor = target.partition("#") if not path_part: return None if path_part.startswith('.github/'): candidates = [ROOT / path_part] elif '/' not in path_part: candidates = [ROOT / path_part, ROOT / source.parent / path_part] else: candidates = [ROOT / source.parent / path_part, ROOT / path_part] destination = next((candidate.resolve() for candidate in candidates if candidate.exists()), candidates[0].resolve()) try: destination.relative_to(ROOT) except ValueError: return None if not destination.exists(): return f"{source}:{lineno}: missing AI citation 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 AI citation anchor: {rel}#{anchor}" return None def main() -> int: errors: list[str] = [] anchor_cache: dict[Path, set[str]] = {} for rel_path in candidate_files(): path = ROOT / rel_path if not path.exists(): errors.append(f"{rel_path}: missing AI citation file") continue text = path.read_text(encoding="utf-8", errors="ignore") errors.extend(validate_repository_identity(rel_path, text)) for lineno, target in extract_targets(path): error = validate_target(rel_path, lineno, target, anchor_cache) if error: errors.append(error) llms_full = ROOT / '.github/llms-full.txt' if not llms_full.exists(): errors.append('.github/llms-full.txt: missing full AI citation context') else: llms_full_text = llms_full.read_text(encoding="utf-8", errors="ignore") for target in taxonomy_document_paths(): if target not in llms_full_text: errors.append(f'.github/llms-full.txt: missing taxonomy document coverage: {target}') if errors: print("AI_CITATION_ERRORS") for error in errors: print(error) print(f"TOTAL={len(errors)}") return 1 print("OK AI citation paths, anchors, and repository identity checked") return 0 if __name__ == "__main__": sys.exit(main())