#!/usr/bin/env python3 """校验执行域 verify/doc/files 一致性。 该脚本是“离线门禁”的一部分:发现 doc_id 三元契约不一致时应 fail-fast, 并输出可定位的冲突清单(doc_id + 路径集合)。 """ from __future__ import annotations import re import sys from pathlib import Path ROOT = Path(__file__).resolve().parent.parent.parent VERIFY_DIR = ROOT / "ansible" / "playbooks" / "verify" DOCS_DIR = ROOT / "docs" EXEC_ID_RE = re.compile(r"^(0[1-9]|[1-9][0-9])-(0[1-9]|[1-9][0-9])$") def is_exec_domain(doc_id: str) -> bool: return EXEC_ID_RE.fullmatch(doc_id) is not None def main() -> None: if not VERIFY_DIR.is_dir() or not DOCS_DIR.is_dir(): print("ERR: verify/docs 目录缺失", file=sys.stderr) sys.exit(2) # --- scan verify playbooks (.yml/.yaml) --- verify_by_doc_id: dict[str, list[Path]] = {} invalid_verify_names: list[str] = [] for p in sorted(VERIFY_DIR.iterdir()): if not p.is_file(): continue if p.name.startswith("_"): # private helpers are allowed but not part of doc_id contract continue if p.suffix not in {".yml", ".yaml"}: continue # Only accept .yml|yaml stem = p.stem if len(stem) != len("00-00") or stem[2:3] != "-": invalid_verify_names.append(p.name) continue if not is_exec_domain(stem): invalid_verify_names.append(p.name) continue verify_by_doc_id.setdefault(stem, []).append(p) # Same doc_id with multiple verify entrypoints is a hard conflict (EC1). verify_conflicts: dict[str, list[str]] = {} for did, paths in verify_by_doc_id.items(): if len(paths) > 1: verify_conflicts[did] = [str(p.relative_to(ROOT)) for p in sorted(paths)] missing_docs: list[str] = [] missing_files_dir: list[str] = [] weak_doc_exec_refs: list[str] = [] multi_docs: dict[str, list[str]] = {} for did in sorted(verify_by_doc_id.keys()): matches = sorted(DOCS_DIR.glob(f"{did}-*.md")) if not matches: missing_docs.append(did) continue if len(matches) > 1: multi_docs[did] = [str(p.relative_to(ROOT)) for p in matches] doc = matches[0] content = doc.read_text(encoding="utf-8", errors="ignore") if f"ansible/files/{did}/" not in content and "```yaml" in content: weak_doc_exec_refs.append(did) # Execution-domain doc_id must always have a files truth-source directory (ADR-002 / R4). if not (ROOT / "ansible" / "files" / did).is_dir(): missing_files_dir.append(did) if invalid_verify_names: print(f"ERR: verify 仅允许执行域命名: {sorted(invalid_verify_names)}", file=sys.stderr) sys.exit(2) if verify_conflicts: print("ERR: verify 入口冲突(同一 doc_id 多个入口,必须 fail-fast):", file=sys.stderr) for did in sorted(verify_conflicts.keys()): paths = verify_conflicts[did] print(f" - {did}: {paths}", file=sys.stderr) sys.exit(2) if missing_docs: print(f"ERR: 缺少 docs/-*.md: {missing_docs}", file=sys.stderr) sys.exit(2) if multi_docs: print("ERR: docs 命名冲突(同一 doc_id 匹配到多篇 docs/-*.md):", file=sys.stderr) for did in sorted(multi_docs.keys()): print(f" - {did}: {multi_docs[did]}", file=sys.stderr) sys.exit(2) if missing_files_dir: print(f"ERR: 缺少 ansible/files// 目录: {missing_files_dir}", file=sys.stderr) sys.exit(2) if weak_doc_exec_refs: print(f"ERR: 文档 YAML 未映射 ansible/files// 真源: {weak_doc_exec_refs}", file=sys.stderr) sys.exit(2) print(f"[OK] 执行域 verify/doc/files 一致性通过({len(verify_by_doc_id)} 条)") if __name__ == "__main__": main()