first commit

This commit is contained in:
xiang
2026-07-25 23:45:09 +08:00
commit c7bd957e6f
47 changed files with 3870 additions and 0 deletions
@@ -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())