日常更新

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,247 @@
#!/usr/bin/env python3
"""按执行域 doc_id 生成 BMad [CS] story-doc-XX-YY.md与 verify playbook 一一对应)。
输出目录_bmad-output/implementation-artifacts/stories-by-doc/
索引:同目录 README.md
用法:
python3 ansible/tools/gen_stories_by_doc.py # 全量
python3 ansible/tools/gen_stories_by_doc.py --doc-id 02-05 # 单篇
python3 ansible/tools/gen_stories_by_doc.py --readme-only # 仅刷新 README含队列表
python3 ansible/tools/gen_stories_by_doc.py --dry-run
"""
from __future__ import annotations
import argparse
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent.parent
VERIFY_DIR = ROOT / "ansible" / "playbooks" / "verify"
DOCS_DIR = ROOT / "docs"
OUT_DIR = ROOT / "_bmad-output" / "implementation-artifacts" / "stories-by-doc"
EXEC_ID_RE = re.compile(r"^(0[1-9]|[1-9][0-9])-(0[1-9]|[1-9][0-9])\.yml$")
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 doc_title(doc_path: Path | None) -> str:
if doc_path is None or not doc_path.exists():
return "(文档待补)"
text = doc_path.read_text(encoding="utf-8", errors="ignore")
first = text.splitlines()[0].strip() if text else ""
if first.startswith("#"):
return first.lstrip("#").strip()
return doc_path.stem
def story_body(doc_id: str, rel_doc: str, title: str, noop: bool) -> str:
pb_rel = f"ansible/playbooks/verify/{doc_id}.yml"
story_rel = f"_bmad-output/implementation-artifacts/stories-by-doc/story-doc-{doc_id}.md"
noop_line = (
"**noop + verify_common 基线**(文档步骤以手工/Helm 为主)"
if noop
else "**自定义断言**(非纯 noop"
)
return f"""# Story [CS] doc_id {doc_id}{title}
Status: ready-for-dev
<!-- BMad [CS]:与执行域 doc_id 一一对应;由 ansible/tools/gen_stories_by_doc.py 生成。实施入口Cursor `/bmad-dev-story`,并指定本 Story 文件路径 `{story_rel}`;工作流见 `.cursor/skills/bmad-dev-story/workflow.md`。 -->
## Meta
| 字段 | 值 |
|------|-----|
| **doc_id** | `{doc_id}` |
| **文档** | `{rel_doc}` |
| **verify playbook** | `{pb_rel}` |
| **清单/示例目录** | `ansible/files/{doc_id}/` |
| **playbook 形态** | {noop_line} |
## bmad-dev-story 入口
- **实施**:在 Cursor 使用 **`/bmad-dev-story`**,并在对话中提供本文件路径:`{story_rel}`(仓库根相对路径)。
- **工作流**:技能 **`bmad-dev-story`** → `.cursor/skills/bmad-dev-story/workflow.md`Agent 仅可改本 Story 内 Tasks 勾选、Dev Agent Record、File List、Change Log、Status与 workflow 一致)。
## Story
作为一名 **实验室维护者**
我希望 **`./ansible/bin/verify.sh run {doc_id}`**(或 `./scripts/cs {doc_id}`)的行为与 **`{rel_doc}`** 中的 TL;DR / 成功判据 / 自动化验收描述一致,并满足仓库 **doc_id 三元契约** 与 **Output Contract`[OC]` / `[OC-ASSERT]`** 约定,
以便 **状态板、offline-check 与文档** 不漂移;**实施与收尾一律按 `/bmad-dev-story` 工作流、以本 Story 为规格真源**。
## Acceptance Criteria
1. **契约**:存在 `docs/{doc_id}-*.md`、`ansible/playbooks/verify/{doc_id}.yml`、`ansible/files/{doc_id}/``python3 ansible/tools/validate_matrix_playbooks.py` 通过。
2. **离线门禁**`./scripts/offline-check.sh` 通过(含 R3 链接、syntax-check
3. **运行验收**:在已连通集群的实验室环境执行 `./ansible/bin/verify.sh run {doc_id}`,结果符合该篇文档预期(`verified` / `gated` 均需可解释;失败则修 playbook 或文档)。
4. **输出语义**playbook 日志中 **`[OC]`** 行可被状态板/脚本解析;含断言处优先带 **`[OC-ASSERT]`**(与 `project-context.md`、ADR-004 一致)。
5. **文档真源**:篇内「契约与真源」或等价段落指向 `ansible/files/{doc_id}/`(若含带 yaml 标注的 fenced 代码块,须出现 `ansible/files/{doc_id}/` 路径以满足弱门禁)。
## Tasks / Subtasks
- [ ] **T1** 对照 **`{rel_doc}`** 的 TL;DR / 验证命令 / 预期,列出与 playbook 任务映射表(缺项 = 未覆盖)。
- [ ] **T2** 若存在差距:改 **`{pb_rel}`** 或 **`verify_common` role**,或修订文档(禁止双真源)。
- [ ] **T3** 回归:`./scripts/offline-check.sh` + `./ansible/bin/verify.sh run {doc_id}`。
- [ ] **T4**(可选)将本轮结论记入 **`_bmad-output/implementation-artifacts/`** 下 OC 笔记(与本 Story 文件名并列索引)。
## Dev Notes
- **执行域**`XX>0 && YY>0`;导航页 `YY=00` 无对应 story。
- **Helm / 手工步骤**noop 篇以文档与 `ansible/files` 示例为准;自动化安装若扩展,需 **env 门控 + teardown**(见 `00-03` §2.1)。
- **禁止**:用 noop **冒充** 已覆盖文档中要求集群变更/探针的判据(见 `epics-and-stories` Epic 2 Story 2.3)。
- **主键**:以 `doc_id` 为准;见 `project-context.md`「doc_id 与验证框架」。
- **实施入口**:本目录下每篇 Story 均通过 **`/bmad-dev-story`** 执行;勿绕过 workflow 随意改规格外文件而不更新 Tasks / File List。
### References
- [Source: `{rel_doc}`]
- [Source: `{pb_rel}`]
- [Source: `ansible/files/{doc_id}/README.md`](若存在)
- [Source: `project-context.md` — 契约 / OC]
- [Source: `docs/00-03-测试与验证框架.md`]
## Dev Agent Record
### Agent Model Used
dev-story 填写)
### Completion Notes List
### File List
---
## Story completion status
- **ready-for-dev**
"""
def write_readme(ids: list[str]) -> str:
lines = [
"# stories-by-doc 索引",
"",
"> BMad **[CS] Create Story**:每个执行域 `doc_id` 一篇。**实施入口统一为 Cursor `/bmad-dev-story`**:在对话中附上对应 `story-doc-<doc_id>.md` 的仓库根相对路径;工作流见 `.cursor/skills/bmad-dev-story/workflow.md`。",
"",
"**生成 / 更新**:在仓库根执行 `python3 ansible/tools/gen_stories_by_doc.py`(或 `--doc-id XX-YY` 只生成一篇)。仅刷新本 README、不覆盖各篇 Story 时:`python3 ansible/tools/gen_stories_by_doc.py --readme-only`。",
"",
"## 开发队列与工作方式50 篇 ≠ 50 条 Cursor Todo",
"",
"### 1. backlog 从哪来",
"",
"- **优先队列**[`sprint-status.md`](../sprint-status.md) **§8 后续 Sprint Backlog** — 先做其中仍标 **ready-for-dev** 或缺口明显的 `doc_id`(与 §8 中某行叙事对上的 `doc_id` 共享同一工程真源:`verify` + docs",
"- **全量索引**:下表列出全部执行域 `doc_id`;需要按系列清扫时,可自 **01-xx → 07-xx** 顺序推进。",
"",
"### 2. WIP 与 IDE Todo",
"",
"- **WIP**[`sprint-status.md`](../sprint-status.md) **§7** — 同一时间最多 **1** 篇 Story `in-progress`。",
"- **Cursor**:每次只跑 **`/bmad-dev-story`** + **一篇** `story-doc-XX-YY.md`IDE Todo 建议只保留 **当前 `doc_id`**(及可选一条回归命令如 `offline-check` / `verify.sh run XX-YY`**不要**把 50 篇拆成 50 条并行 Todo。",
"",
"### 3. 收尾与 §8 对齐",
"",
"- 完成条件以该篇 **AC + Tasks** 为准;按 `.cursor/skills/bmad-dev-story/workflow.md` 更新 Story 内 **Tasks 勾选、Dev Agent Record、File List、Change Log、Status**(如进入 `review`)。",
"- **`sprint-status.md` §8**:可与 Story 状态交叉备注;避免长期双真源 — **规格与闭合状态以 Story 文件 + `ansible/playbooks/verify/<doc_id>.yml` + docs 为准**§8 由维护者 **周期性** 与已闭合项对齐(不必每篇同时改两处)。",
"",
"## 优先级(以 doc_id 为准)",
"",
"- **主键**`doc_id``XX-YY`)→ 本目录 **`story-doc-<doc_id>.md`**;与 **`ansible/playbooks/verify/<doc_id>.yml`**、**`docs/<doc_id>-*.md`**、**`ansible/files/<doc_id>/`** 一一对齐。",
"- **补充 backlog**:若 `_bmad-output/implementation-artifacts/` 下另有 **同主题** 文件(如 **`*-baseline-verify-oc.md`**、Epic 编号命名的 `3-2-*.md`),视为**同一 `doc_id` 或跨多篇 `doc_id` 的深化说明**;实施与验收仍以 **`verify/<doc_id>.yml` + 对应 docs** 为真源,规划文件名仅作辅助索引。",
"",
"下表 **Playbook** 列由本脚本根据 `verify/*.yml` 内容自动标注(`noop+verify_common` ≈ 文档/手工为主;`自定义断言` ≈ 含非 noop 任务),便于拣选体量与回归方式。",
"",
f"共 **{len(ids)}** 篇。",
"",
"| doc_id | Playbook | Story 文件 |",
"|--------|----------|------------|",
]
for did in ids:
kind = "noop+verify_common" if playbook_is_noop(did) else "自定义断言"
lines.append(f"| {did} | {kind} | [story-doc-{did}.md](story-doc-{did}.md) |")
lines.append("")
return "\n".join(lines)
def collect_doc_ids(single: str | None) -> list[str]:
ids: list[str] = []
for p in sorted(VERIFY_DIR.glob("*.yml")):
if p.name.startswith("_"):
continue
if not EXEC_ID_RE.match(p.name):
continue
stem = p.stem
if single and stem != single:
continue
ids.append(stem)
if single and single not in ids:
print(f"ERR: doc_id 无对应 verify playbook{single}", file=sys.stderr)
sys.exit(2)
return sorted(ids)
def main() -> int:
ap = argparse.ArgumentParser(description="按 doc_id 生成 BMad CSstory-doc-*.md")
ap.add_argument("--doc-id", help="仅生成该 XX-YY默认全量")
ap.add_argument("--dry-run", action="store_true", help="只打印将写入的路径")
ap.add_argument(
"--readme-only",
action="store_true",
help="只重写 stories-by-doc/README.md不重写各 story-doc-*.md",
)
args = ap.parse_args()
if args.readme_only and args.doc_id:
print("ERR: --readme-only 与 --doc-id 不能同时使用", file=sys.stderr)
return 2
doc_ids = collect_doc_ids(args.doc_id)
if not doc_ids:
print("ERR: 未发现执行域 verify playbook", file=sys.stderr)
return 2
if not args.dry_run:
OUT_DIR.mkdir(parents=True, exist_ok=True)
if args.readme_only:
if args.dry_run:
print(f"would write {OUT_DIR.relative_to(ROOT)}/README.md")
return 0
readme = write_readme(doc_ids)
(OUT_DIR / "README.md").write_text(readme, encoding="utf-8")
print(f"[OK] {OUT_DIR.relative_to(ROOT)}/README.md ({len(doc_ids)} 条, --readme-only)")
return 0
for doc_id in doc_ids:
noop = playbook_is_noop(doc_id)
matches = sorted(DOCS_DIR.glob(f"{doc_id}-*.md"))
doc_path = matches[0] if matches else None
rel_doc = str(doc_path.relative_to(ROOT)) if doc_path else f"docs/{doc_id}-*.md缺失"
title = doc_title(doc_path)
body = story_body(doc_id, rel_doc, title, noop)
out_path = OUT_DIR / f"story-doc-{doc_id}.md"
if args.dry_run:
print(f"would write {out_path.relative_to(ROOT)}")
continue
out_path.write_text(body, encoding="utf-8")
print(f"[OK] {out_path.relative_to(ROOT)}")
if args.dry_run:
return 0
readme = write_readme(doc_ids)
(OUT_DIR / "README.md").write_text(readme, encoding="utf-8")
print(f"[OK] {OUT_DIR.relative_to(ROOT)}/README.md ({len(doc_ids)} 条)")
return 0
if __name__ == "__main__":
raise SystemExit(main())