first commit
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user