#!/usr/bin/env python3 """校验 Vibe Coding 文档结构、链接、占位符和功能追溯。""" from __future__ import annotations import argparse import re import sys from pathlib import Path PLACEHOLDER = re.compile(r"\{\{[^{}\n]+\}\}") MARKDOWN_LINK = re.compile(r"\[[^\]]+\]\(([^)]+)\)") FUNCTION_ID = re.compile(r"\bF-\d{3}\b") OLD_NAMES = { "03.冒烟自测用例.md", "06.逻辑验证报告.md", "07.验收与交付报告.md", } class Report: def __init__(self) -> None: self.errors: list[str] = [] self.warnings: list[str] = [] def error(self, message: str) -> None: self.errors.append(message) def warning(self, message: str) -> None: self.warnings.append(message) def read_text(path: Path, report: Report) -> str: try: return path.read_text(encoding="utf-8") except UnicodeDecodeError: report.error(f"文件不是有效 UTF-8:{path}") except OSError as exc: report.error(f"无法读取文件:{path}({exc})") return "" def check_links(path: Path, text: str, report: Report) -> None: for match in MARKDOWN_LINK.finditer(text): target = match.group(1).strip() if ( not target or target.startswith(("#", "http://", "https://", "mailto:")) or "{{" in target ): continue relative = target.split("#", 1)[0] if not (path.parent / relative).resolve().exists(): report.error(f"失效链接:{path} -> {target}") def required_root_files(root: Path) -> list[Path]: return [ root / "README.md", root / "STATUS.md", root / "技术侧需求分析.md", root / "source" / "README.md", root / "code" / "MANIFEST.md", ] def task_directories(root: Path) -> list[Path]: tasks_root = root / "tasks" if not tasks_root.is_dir(): return [] return sorted(path for path in tasks_root.iterdir() if path.is_dir()) def required_task_names(phase: str) -> list[str]: names = ["01.需求分析.md"] if phase in {"g2", "g3", "archive"}: names += ["02.技术实现方案.md", "03.冒烟与逻辑验证.md"] if phase in {"g3", "archive"}: names += ["04.技术实现记录.md", "05.代码Review报告.md"] if phase == "archive": names += ["06.验收与交付报告.md"] return names def check_traceability(task: Path, report: Report) -> None: requirement = read_text(task / "01.需求分析.md", report) design = read_text(task / "02.技术实现方案.md", report) validation = read_text(task / "03.冒烟与逻辑验证.md", report) required_ids = set(FUNCTION_ID.findall(requirement)) if not required_ids: report.warning(f"未在需求分析中发现功能编号:{task}") return for function_id in sorted(required_ids): if function_id not in design: report.error(f"{task.name} 的 {function_id} 未出现在技术方案中") if function_id not in validation: report.error(f"{task.name} 的 {function_id} 未出现在验证文档中") def validate_l1( root: Path, allow_placeholders: bool, report: Report, ) -> None: document = root / "01.精简变更日志.md" if not document.is_file(): report.error(f"缺少 L1 文档:{document}") return text = read_text(document, report) if not allow_placeholders and PLACEHOLDER.search(text): report.error(f"存在未填写占位符:{document}") check_links(document, text, report) def validate_l2( root: Path, phase: str, allow_placeholders: bool, report: Report, ) -> None: required = required_root_files(root) tasks = task_directories(root) if not tasks: report.error(f"未发现子任务目录:{root / 'tasks'}") for task in tasks: for name in required_task_names(phase): required.append(task / name) for child in task.iterdir(): if child.name in OLD_NAMES: report.error(f"发现旧版文档名称:{child}") if phase == "archive": required.append(root / "归档记录.md") existing_required: list[Path] = [] for path in required: if not path.is_file(): report.error(f"缺少必需文件:{path}") else: existing_required.append(path) status = root / "STATUS.md" if status.is_file(): status_text = read_text(status, report) for heading in ("原始需求版本", "确认文档及版本", "代码版本标识"): if heading not in status_text: report.error(f"STATUS.md 缺少门禁绑定字段:{heading}") for path in existing_required: text = read_text(path, report) if not allow_placeholders and PLACEHOLDER.search(text): report.error(f"存在未填写占位符:{path}") check_links(path, text, report) if phase in {"g2", "g3", "archive"}: for task in tasks: if all((task / name).is_file() for name in required_task_names("g2")): check_traceability(task, report) def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( description="校验 Vibe Coding 的 L1/L2 文档完整性。", ) parser.add_argument("path", help="L1 所在目录或 L2 PM 目录") parser.add_argument( "--phase", choices=["l1", "structure", "g1", "g2", "g3", "archive"], required=True, help="目标校验阶段", ) parser.add_argument( "--allow-placeholders", action="store_true", help="允许模板占位符,适合刚初始化后的结构检查", ) return parser def main() -> int: args = build_parser().parse_args() root = Path(args.path).resolve() if not root.is_dir(): print(f"校验失败:目录不存在:{root}", file=sys.stderr) return 1 report = Report() if args.phase == "l1": validate_l1(root, args.allow_placeholders, report) else: validate_l2(root, args.phase, args.allow_placeholders, report) for warning in report.warnings: print(f"警告:{warning}") for error in report.errors: print(f"错误:{error}", file=sys.stderr) if report.errors: print(f"校验失败:{len(report.errors)} 个错误,{len(report.warnings)} 个警告") return 1 print(f"校验通过:0 个错误,{len(report.warnings)} 个警告") return 0 if __name__ == "__main__": raise SystemExit(main())