Files
Deploy-Laboratory/ansible/tools/validate_stories_by_doc.py
2026-03-29 09:08:01 +08:00

202 lines
7.2 KiB
Python
Raw 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
"""批量 VSValidate Story校验 stories-by-doc/story-doc-*.md 与仓库契约一致。
对应 BMad `bmad-create-story:validate` 在本仓库的**可自动化**子集结构、路径、noop 与 playbook 一致)。
深度内容评审(对照 PRD/架构全文)仍需人工或单篇会话。
用法:
python3 ansible/tools/validate_stories_by_doc.py
python3 ansible/tools/validate_stories_by_doc.py --write-report
python3 ansible/tools/validate_stories_by_doc.py --json
"""
from __future__ import annotations
import argparse
import json
import re
import sys
from dataclasses import dataclass, field
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent.parent
STORIES_DIR = ROOT / "_bmad-output" / "implementation-artifacts" / "stories-by-doc"
VERIFY_DIR = ROOT / "ansible" / "playbooks" / "verify"
DOCS_DIR = ROOT / "docs"
FILES_PREFIX = ROOT / "ansible" / "files"
STORY_NAME_RE = re.compile(r"^story-doc-([0-9]{2}-[0-9]{2})\.md$")
META_DOC_RE = re.compile(r"\|\s*\*\*文档\*\*\s*\|\s*`([^`]+)`\s*\|")
META_PB_RE = re.compile(r"\|\s*\*\*verify playbook\*\*\s*\|\s*`([^`]+)`\s*\|")
META_ID_RE = re.compile(r"\|\s*\*\*doc_id\*\*\s*\|\s*`([0-9]{2}-[0-9]{2})`\s*\|")
META_NOOP_RE = re.compile(r"\|\s*\*\*playbook 形态\*\*\s*\|\s*(.+)\s*\|")
def playbook_is_noop(doc_id: str) -> bool:
yml = VERIFY_DIR / f"{doc_id}.yml"
if not yml.is_file():
return False
content = yml.read_text(encoding="utf-8", errors="ignore")
return "noop verify" in content.lower() or "noop-doc-verify" in content
def meta_declares_noop(meta_line: str) -> bool:
"""与 gen_stories_by_doc.py 一致noop 篇写「noop + verify_common 基线」,勿用子串 noop「非纯 noop」会误判"""
m = meta_line.lower()
return "noop + verify_common" in meta_line or "noop+verify_common" in m.replace(" ", "")
@dataclass
class Row:
doc_id: str
ok: bool
errors: list[str] = field(default_factory=list)
warnings: list[str] = field(default_factory=list)
def validate_one(path: Path) -> Row:
m = STORY_NAME_RE.match(path.name)
if not m:
return Row(doc_id="?", ok=False, errors=[f"文件名不符合 story-doc-XX-YY.md{path.name}"])
doc_id = m.group(1)
row = Row(doc_id=doc_id, ok=True)
text = path.read_text(encoding="utf-8", errors="replace")
if "Status: ready-for-dev" not in text:
row.errors.append("缺少 Status: ready-for-dev")
row.ok = False
for sec in ("## Meta", "## Acceptance Criteria", "## Tasks / Subtasks", "## Story completion status"):
if sec not in text:
row.errors.append(f"缺少章节 {sec}")
row.ok = False
mid = META_ID_RE.search(text)
if not mid:
row.errors.append("Meta 表中无法解析 doc_id")
row.ok = False
elif mid.group(1) != doc_id:
row.errors.append(f"Meta doc_id `{mid.group(1)}` 与文件名 `{doc_id}` 不一致")
row.ok = False
md = META_DOC_RE.search(text)
if not md:
row.errors.append("Meta 表中无法解析 **文档**")
row.ok = False
else:
rel = md.group(1).strip()
doc_path = ROOT / rel
if not doc_path.is_file():
row.errors.append(f"文档路径不存在:{rel}")
row.ok = False
mp = META_PB_RE.search(text)
if not mp:
row.errors.append("Meta 表中无法解析 **verify playbook**")
row.ok = False
else:
rel_pb = mp.group(1).strip()
pb_path = ROOT / rel_pb
if not pb_path.is_file():
row.errors.append(f"playbook 不存在:{rel_pb}")
row.ok = False
mn = META_NOOP_RE.search(text)
if mn:
meta_line = mn.group(1).strip()
expected_noop = playbook_is_noop(doc_id)
declared_noop = meta_declares_noop(meta_line)
if expected_noop != declared_noop:
row.errors.append(
f"playbook 形态与 verify 实际不一致:期望 noop={expected_noop}Meta 表述 noop={declared_noop}"
)
row.ok = False
else:
row.warnings.append("未解析 playbook 形态行,跳过 noop 一致性检查")
files_dir = FILES_PREFIX / doc_id
if not files_dir.is_dir():
row.errors.append(f"缺少目录 ansible/files/{doc_id}/")
row.ok = False
if f"verify.sh run {doc_id}" not in text and f"run {doc_id}" not in text:
row.warnings.append("正文中未出现 verify.sh run {doc_id} 类引用(可忽略若已改写)")
return row
def main() -> int:
ap = argparse.ArgumentParser(description="批量校验 story-doc-*.mdVS 自动化子集)")
ap.add_argument("--write-report", action="store_true", help=f"写入 {STORIES_DIR}/VS-all-report.md")
ap.add_argument("--json", action="store_true", help="stdout 输出 JSON")
args = ap.parse_args()
paths = sorted(STORIES_DIR.glob("story-doc-*.md"))
if not paths:
print("ERR: 未发现 story-doc-*.md", file=sys.stderr)
return 2
rows: list[Row] = [validate_one(p) for p in paths]
failed = [r for r in rows if not r.ok]
if args.json:
print(
json.dumps(
[
{
"doc_id": r.doc_id,
"ok": r.ok,
"errors": r.errors,
"warnings": r.warnings,
}
for r in rows
],
ensure_ascii=False,
indent=2,
)
)
else:
print(f"VS-all自动化{len(rows)} 篇,通过 {len(rows) - len(failed)},失败 {len(failed)}")
for r in rows:
if r.ok and not r.warnings:
continue
print(f"\n## {r.doc_id}")
if r.errors:
for e in r.errors:
print(f" ERR: {e}")
for w in r.warnings:
print(f" WARN: {w}")
if not failed:
print("\n全部通过(结构 + 路径 + noop 一致性)。")
if args.write_report:
lines = [
"# VS-all 报告Validate Story 自动化子集)",
"",
f"- 校验篇数:**{len(rows)}**",
f"- 通过:**{len(rows) - len(failed)}**",
f"- 失败:**{len(failed)}**",
"",
"本报告由 `python3 ansible/tools/validate_stories_by_doc.py --write-report` 生成。",
"覆盖:文件名与 Meta `doc_id`、必备章节、`docs` / `verify` / `ansible/files` 路径、`noop` 与 playbook 一致。",
"不覆盖:对照 PRD/架构的语义深度评审(请单篇使用 BMad VS 或人工)。",
"",
"---",
"",
]
for r in rows:
status = "" if r.ok else ""
lines.append(f"- {status} **`{r.doc_id}`**")
for e in r.errors:
lines.append(f" - ERR: {e}")
for w in r.warnings:
lines.append(f" - WARN: {w}")
out = STORIES_DIR / "VS-all-report.md"
out.write_text("\n".join(lines) + "\n", encoding="utf-8")
print(f"\n[OK] 已写入 {out.relative_to(ROOT)}")
return 1 if failed else 0
if __name__ == "__main__":
raise SystemExit(main())