#!/usr/bin/env python3 """Check AI citation entry files for local path and anchor drift.""" from __future__ import annotations import re import sys import urllib.parse from pathlib import Path from lib.taxonomy import taxonomy_document_paths ROOT = Path(__file__).resolve().parents[1] AI_ENTRY_FILES = [ Path("llms.txt"), Path("assets/ai-citation/llms-full.txt"), ] AI_MARKDOWN_DIR = Path("assets/ai-citation") PATH_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) if AI_MARKDOWN_DIR.exists(): files.extend(sorted(AI_MARKDOWN_DIR.glob("*.md"))) 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_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 destination = (ROOT / path_part).resolve() if "/" not in path_part and source.parent == AI_MARKDOWN_DIR and not destination.exists(): destination = (ROOT / source.parent / path_part).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 for lineno, target in extract_targets(path): error = validate_target(rel_path, lineno, target, anchor_cache) if error: errors.append(error) llms_full = ROOT / "assets/ai-citation/llms-full.txt" if not llms_full.exists(): errors.append("assets/ai-citation/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"assets/ai-citation/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 and anchors checked") return 0 if __name__ == "__main__": sys.exit(main())