Files
2026-07-25 23:45:09 +08:00

209 lines
6.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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())