#!/usr/bin/env python3 """Check bilingual per-directory README coverage for the maintained source tree.""" from __future__ import annotations import re import subprocess import sys from pathlib import Path, PurePosixPath from urllib.parse import unquote ROOT = Path(__file__).resolve().parents[1] # These trees are third-party, generated, or byte-exact fixtures. Their owning # parent README must describe the boundary, but documentation files must not be # injected into the trees themselves. EXCLUDED_PREFIXES = ( PurePosixPath("openai4s/server/webui/vendor"), PurePosixPath("tests/fixtures"), ) # Pinned, mechanically imported third-party resource collections. Their root # boundary files remain maintained and checked; only generated recipe # descendants are excluded, so deleting the bilingual boundary pair cannot # make the directory disappear from this gate. PINNED_COLLECTION_ROOTS = (PurePosixPath("skills/bioskills"),) PINNED_COLLECTION_BOUNDARY_FILES = frozenset( {"COLLECTION.json", "LICENSE", "MANIFEST.json", "README.md", "README_zh.md"} ) EXCLUDED_PARTS = frozenset( { ".git", ".venv", ".openai4s-runtime", # Kernel-readable scratch the runtime writes beside a workspace; # `openai4s run` in the checkout puts it at the repo root. ".openai4s", "node_modules", "__pycache__", ".pytest_cache", ".build", } ) # A directory documents itself with README.md + README_zh.md, or — where a # README.md would collide with the way a tool treats the folder, as # `.github/README.md` does on GitHub (it overrides the repository's displayed # profile) — with CONTENTS.md + CONTENTS_zh.md. Either bilingual pair satisfies # coverage; the first pair present (README preferred) is the one checked. DOC_PAIRS = (("README.md", "README_zh.md"), ("CONTENTS.md", "CONTENTS_zh.md")) # Every doc-file name, so a directory's own documentation is never counted among # the direct files its documentation must list. DOC_NAMES = frozenset(name for pair in DOC_PAIRS for name in pair) def _excluded(path: PurePosixPath) -> bool: if any(part in EXCLUDED_PARTS for part in path.parts): return True if any(path == prefix or prefix in path.parents for prefix in EXCLUDED_PREFIXES): return True for root in PINNED_COLLECTION_ROOTS: if path == root: return False if root in path.parents: return not ( path.parent == root and path.name in PINNED_COLLECTION_BOUNDARY_FILES ) return False def _source_files() -> set[PurePosixPath]: result = subprocess.run( ["git", "ls-files", "--cached", "--others", "--exclude-standard", "-z"], cwd=ROOT, check=True, capture_output=True, ) files: set[PurePosixPath] = set() for raw in result.stdout.decode("utf-8").split("\0"): if not raw: continue path = PurePosixPath(raw) if not _excluded(path): files.add(path) return files def _source_directories(files: set[PurePosixPath]) -> set[PurePosixPath]: directories: set[PurePosixPath] = set() for path in files: parent = path.parent while parent != PurePosixPath("."): if not _excluded(parent): directories.add(parent) parent = parent.parent return directories def _structure(text: str) -> tuple[list[str], int]: headings = re.findall(r"^(#{1,6})\s+", text, flags=re.MULTILINE) table_rows = sum( 1 for line in text.splitlines() if line.startswith("|") and line.endswith("|") ) return headings, table_rows def _relative_links(text: str) -> list[str]: """Return local Markdown link destinations, excluding URLs and anchors.""" links: list[str] = [] for match in re.finditer(r"!?\[[^\]]*\]\(([^)]+)\)", text): target = match.group(1).strip() if target.startswith("<") and target.endswith(">"): target = target[1:-1] if target.startswith(("#", "http://", "https://", "mailto:")): continue target = unquote(target.split("#", 1)[0].split("?", 1)[0]) if target: links.append(target) return links def main() -> int: files = _source_files() directories = _source_directories(files) errors: list[str] = [] for directory in sorted(directories, key=str): local_dir = ROOT / directory english_path = None chinese_path = None for english_name, chinese_name in DOC_PAIRS: if (local_dir / english_name).is_file(): english_path = local_dir / english_name chinese_path = local_dir / chinese_name break if english_path is None: errors.append(f"missing {directory}/README.md") continue if not chinese_path.is_file(): errors.append(f"missing {directory}/{chinese_path.name}") continue english = english_path.read_text(encoding="utf-8") chinese = chinese_path.read_text(encoding="utf-8") if _structure(english) != _structure(chinese): errors.append(f"bilingual structure mismatch: {directory}") for readme_path, text in ( (english_path, english), (chinese_path, chinese), ): for target in _relative_links(text): if not (readme_path.parent / target).resolve().exists(): errors.append( f"broken relative link in {readme_path.relative_to(ROOT)}: " f"{target}" ) direct_files = sorted( path.name for path in files if path.parent == directory and path.name not in DOC_NAMES ) for name in direct_files: marker = f"`{name}`" if marker not in english: errors.append( f"{directory}/{english_path.name} does not mention {marker}" ) if marker not in chinese: errors.append( f"{directory}/{chinese_path.name} does not mention {marker}" ) children = sorted( child.name for child in directories if child.parent == directory ) for name in children: marker = f"`{name}/`" if marker not in english: errors.append( f"{directory}/{english_path.name} does not mention {marker}" ) if marker not in chinese: errors.append( f"{directory}/{chinese_path.name} does not mention {marker}" ) if errors: for error in errors: print(f"directory docs: {error}", file=sys.stderr) return 1 documented_files = { path for path in files if path.parent in directories and path.name not in DOC_NAMES } print( f"directory docs: {len(directories)} maintained directories, " f"{len(documented_files)} direct files/assets, complete bilingual coverage" ) return 0 if __name__ == "__main__": raise SystemExit(main())