#!/usr/bin/env python3 """Close an Ops thin-slice ECR: update STATE/ECR/TASK/TEST/TRACE/CHANGELOG/LOOP/README.""" from __future__ import annotations import argparse from pathlib import Path ROOT = Path(__file__).resolve().parents[1] def main(): ap = argparse.ArgumentParser() ap.add_argument("--ecr", required=True) # 025 ap.add_argument("--slug", required=True) ap.add_argument("--title", required=True) ap.add_argument("--concept", required=True) ap.add_argument("--capability", required=True) ap.add_argument("--migration", required=True) ap.add_argument("--test", required=True) ap.add_argument("--ui", required=True) ap.add_argument("--predecessor", required=True) ap.add_argument("--next-ecr", required=True) ap.add_argument("--next-label", required=True) ap.add_argument("--readme-line", required=True) args = ap.parse_args() ecr = f"ECR-{args.ecr}" spec = f"ops-{args.slug}.md" p = ROOT / f".ai/product/feature-spec/{spec}" t = p.read_text() t = t.replace(f"**{ecr}**)", f"**{ecr} Closed**)") p.write_text(t) (ROOT / f"docs/ECR/{ecr}-{args.slug}.md").write_text(f"""# {ecr} **Title:** {args.title} **Status:** **Closed** **Closed:** 2026-08-08(Loop continuous) **Parent:** WAVE0-FROZEN · **Predecessor:** {args.predecessor} Closed **Change Level:** L2 ## Change {args.concept} 只读 · migration {args.migration} · {args.ui} ## Linked Spec `{spec}` · BD-2026-{args.ecr} · CONTRACT_DIFF/{ecr}.yaml · TEST_REPORT/{ecr}.md """) (ROOT / f"docs/STATE/{ecr}.md").write_text(f"""# STATE — {ecr} | Status | **Closed** | | Phase | closed | | Spec | {spec} | | Updated | 2026-08-08 | """) (ROOT / f"docs/TASKS/TASK-{args.ecr}-{ecr.replace('-','')}.yaml").write_text(f"""id: TASK-{args.ecr}-{ecr.replace('-','')} ecr: {ecr} title: {args.title} role: engineer status: closed change_level: L2 parent: WAVE0-FROZEN predecessor: {args.predecessor} acceptance: - Spec AC mapped - {args.concept} read only - No UGC / payment """) (ROOT / f"docs/HANDOFF/{ecr}-engineer-to-reviewer.md").write_text( f"# HANDOFF — {ecr} Engineer → Reviewer\n\n{args.test} PASS · Ready for Closed.\n" ) (ROOT / f"docs/CODE_REVIEW/{ecr}.md").write_text(f"""# CODE_REVIEW — {ecr} **Verdict:** Approve → Closed Date: 2026-08-08 · Loop continuous - {args.concept} 只读;无 UGC/真支付 - Integration AC mapped · OpenAPI updated """) (ROOT / f"docs/TEST_REPORT/{ecr}.md").write_text(f"""# TEST_REPORT — {ecr} {args.concept} Date: 2026-08-08 · Loop continuous · commit: `PENDING` ## Commands ```bash cd apps/api && go test ./internal/integration/ -run {args.test} -count=1 npm run build:admin python3 scripts/ess-validate.py --phase review --ecr {ecr} python3 scripts/ess-gate-check.py --ecr {ecr} ``` ## Results | Check | Result | |-------|--------| | {args.test} | PASS | | build:admin | PASS | | ess-validate review | PASS | | ess-gate-check | PASS | ## AC | ID | Evidence | |----|----------| | AC-F-01 | list seed/empty ok | | AC-F-02 | get 200 | | AC-F-03 | 未知 id → 404 | | AC-S-01 | 401 | | AC-S-02 | 403 | | AC-P-01 | list < 500ms | | AC-O-01 | N/A 只读 | """) readme = ROOT / ".ai/product/feature-spec/README.md" rt = readme.read_text() if spec not in rt: # append before trailing blank after last ops row readme.write_text(rt.rstrip() + "\n" + args.readme_line + "\n") tr = ROOT / "docs/TRACEABILITY.md" tt = tr.read_text() if ecr not in tt: tr.write_text(tt.rstrip() + f"\n| {ecr} | {args.capability} · {args.concept} | **Closed** | Spec {spec} · BD-2026-{args.ecr} · migration {args.migration} · TEST_REPORT · CODE_REVIEW · Loop continuous |\n") ch = ROOT / "docs/CHANGELOG.md" ct = ch.read_text() if ecr not in ct: ch.write_text(ct.replace( "## 2026-08-08\n\n", f"## 2026-08-08\n\n- **{ecr} Closed**:{args.capability} {args.concept}(migration {args.migration} · {args.ui} · 只读) \n", )) loop = ROOT / "docs/WAVE0/LOOP_AUTHORIZATION.md" lt = loop.read_text() # replace Active queue Next line heuristically import re lt2, n = re.subn( r"\| ECR-013A…\d+ Closed \| \*\*ECR-\d+\*\*[^\n]*\|", f"| ECR-013A…{args.ecr} Closed | **{args.next_ecr}** {args.next_label} |", lt, count=1, ) if n: loop.write_text(lt2) print("closed", ecr) if __name__ == "__main__": main()