日常更新

This commit is contained in:
2026-03-29 09:08:01 +08:00
parent 31709425e2
commit befdefd222
224 changed files with 7240 additions and 3297 deletions

View File

@@ -0,0 +1,201 @@
#!/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())