#!/usr/bin/env python3 """Check (and draft) the transplant of #244's skill additions into the v0.3 / v0.4 / v0.5 skill texts. Background (see TRANSPLANT-PREP-244.md next to this script): #244 (feat-audit-pushback) adds 459 lines to the v0.2 skill text -- an independent-auditor matrix row, a main section (independent audit + auto-pushback, rebuild branch, three tightenings, QA full-chain), two checklist items, and appendix chapter 09-auditor. The v0.3 / v0.4 / v0.5 texts live on unmerged branches; the transplant itself can only happen at merge time. This script is the preparation instrument, not the transplant: anchors -- verify that a target text still has every anchor the transplant needs, and that every rule the transplant must preserve is still present. apply -- produce a DRAFT transplanted text (stdout or --out) for review. The draft is partial by design: paragraphs graded STOP in the prep doc are NOT applied and are listed on stderr. Never commit a draft as the transplant. check-transplanted -- verify a transplanted text: hunks present, stopped paragraphs absent, preserved markers intact, no templates/artifacts/ reference left, and the target's lines intact in order (i.e. the transplant did not overwrite target content except for the documented artifact-reference replacements). Grading (see the prep doc for the full table): * v03: apply everything except the G7 / full-auto sentences (those 回路 do not exist in v0.3 yet) -- sentence-level redactions. * v04: apply everything except the G7 sentences and the收紧三 pre-rebuild user-confirmation paragraph (it would be a fifth ask-user case beyond full-auto's exhaustive four). * v05: apply everything except the whole QA full-chain section and its checklist item (their cross-client framing contradicts v0.5's cross-viewport framing), the收紧三 confirmation paragraph, and renumber the appendix chapter 09-auditor -> 11-auditor (v0.5 already has 09-ops and 10-iteration). Only the standard library is used. Exit codes: 0 = pass (apply always exits 0 once the draft is written, even when paragraphs were stopped -- the stop list on stderr is part of the result); 1 = check failed; 2 = usage error or an anchor/fragment that must exist exactly once was missing or ambiguous (fail loudly, never silently partial). """ import argparse import sys from pathlib import Path # ---------------------------------------------------------------- fragments # Every fragment below is copied verbatim from #244's SKILL.md # (feat-audit-pushback @ 39966bd). If a fragment is not found exactly # once, apply/check fails loudly instead of guessing. AUDIT_WARNING = ( "移植警告:本节是按主线 v0.2 文本写的,技能\n" "v0.3 / v0.4 / v0.5 的内容在未合并分支上,合并时必须把本节\n" "移植进 v0.3 / v0.4 / v0.5 文本,绝不能用本文件覆盖它们\n" "(覆盖会丢掉它们已有的 G7、G8、全自动等内容)。" ) AUDIT_WARNING_REPLACEMENT = ( "移植注:本节源自审计加固轨,移植时已按本文件对应章节的回路口径对齐。" ) QA_INTRO = ( "QA 要求以生产 v0.2 技能为准:\n" "已有规则语义不改,只补测试机制。移植警告与审计章节相同:\n" "本节按主线 v0.2 文本写,合并时必须把新增段落移植进\n" "v0.3 / v0.4 / v0.5 文本,绝不能用本文件覆盖它们。\n" "改动逐条列在 PR 摘要里(本节为新增,上面章节为新增,\n" "未改动已有规则的任何一句话——技能正文 diff 全是加法)。" ) QA_INTRO_REPLACEMENT = "已有规则语义不改,只补测试机制。" # G7 / full-auto parallel sentence + the stale note that follows it # (source lines 435-440, in the "与已有回路的关系" paragraph). G7_FULLAUTO_BLOCK = ( " G7 热修复回路并行(推回是合入前,热修复是发布后应急,\n" " 推回不能替代热修复、热修复也不能抵推回);全自动 CI 自动\n" " 修复并行且计数独立(见上限一段)。注意:G7 与全自动 CI 回路\n" " 在主线 v0.2 文本里尚未出现(它们在未合并的 v0.4 / v0.5 分支里),\n" " 移植进分支文本时按分支的实际回路定义对齐,不要因主线没有\n" " 就删掉这段关系说明,也不要把分支内容反向说成主线已有。" ) # v04 keeps the full-auto half; the G7 half and the stale note go. G7_HALF = ( " G7 热修复回路并行(推回是合入前,热修复是发布后应急,\n" " 推回不能替代热修复、热修复也不能抵推回);" ) G7_NOTE = ( "注意:G7 与全自动 CI 回路\n" " 在主线 v0.2 文本里尚未出现(它们在未合并的 v0.4 / v0.5 分支里),\n" " 移植进分支文本时按分支的实际回路定义对齐,不要因主线没有\n" " 就删掉这段关系说明,也不要把分支内容反向说成主线已有。" ) PUSHBACK_LIMIT_PAREN = ( "(与全自动 CI 自动修复限\n" " 3 次同口径,但计数不共用:CI 修复轮次发生在审计前,审计推回\n" " 轮次发生在审计后,一方不得消耗另一方的预算,代码里\n" " `ci_auto_fix_rounds` 只作参考输入、不计入推回上限)" ) PUSHBACK_LIMIT_PAREN_V03 = ( "(推回上限独立于其它修复回路的轮次预算,一方不得消耗另一方的预算)" ) REBUILD_TAIL = ( ",与 G7 热修复回路并行(已发布\n" " 模块的应急修复走 G7,不走推倒),与全自动 CI 自动修复并行\n" " 且计数独立(CI 修复轮次不计入模块失败计数、也不计入推倒次数)。" ) REBUILD_TAIL_V03 = "。" REBUILD_TAIL_V04 = ( ",与全自动 CI 自动修复并行\n" " 且计数独立(CI 修复轮次不计入模块失败计数、也不计入推倒次数)。" ) TIGHTEN3_NOTIFY = ( "- 验收失败导致的推倒要先通知用户:因用户验收 fail 累计触发推倒时,\n" " 推倒前必须先通知用户并确认新方向。通知内容含失败原因、推倒范围、\n" " 保留资产、新实现第一道门;用户未确认前不得静默重开(代码里\n" " 是 `rebuild_pending_user_confirmation`,合并继续阻塞、推倒次数\n" " 不消耗);通知与确认记录写进审计记录;用户确认新方向后,\n" " 推倒才自动执行,执行后用户仍保留最终 G6 验收权。" ) # Decision B (see prep doc): templates/artifacts/ references are deleted # or rewritten to inline structure at transplant time. Each pair must # occur exactly once in the target; anything else fails loudly. ARTIFACT_REPLACEMENTS = [ ("PRD 按 `templates/artifacts/prd-outline.md` 写完", "PRD 按产品经理附录章节所列大纲写完"), ("问卷模板在 `templates/artifacts/intake-questions.md`。", ""), ("- 验收清单模板在 `templates/artifacts/acceptance-checklist.md`。\n", ""), ("按 `templates/artifacts/prd-outline.md` 写一份 PRD,至少包含:", "按下面大纲写一份 PRD,至少包含:"), (" 协议模板见 `templates/artifacts/` 下的 agent 任务契约模板。", " (Agent 协议、状态机、错误 taxonomy 的要素见本章第 4、5、6 项。)"), ("按 `templates/artifacts/acceptance-checklist.md` 生成本项目的验收清单,", "按 PRD 验收标准生成本项目的验收清单,"), ] # Markers that must survive the transplant, per target (cumulative). MARKERS = { "v03": ["派生前校验表", "精简包", "任务间问答默认策略", "目录检查命令", "单端复核"], "v04": ["派生前校验表", "精简包", "任务间问答默认策略", "目录检查命令", "单端复核", "全自动模式", "full-auto-executor", "FULL_AUTO_ENABLED", "限 3 次"], "v05": ["派生前校验表", "精简包", "任务间问答默认策略", "目录检查命令", "单端复核", "全自动模式", "full-auto-executor", "FULL_AUTO_ENABLED", "限 3 次", "G7 发布后运维与观察期", "G8 迭代与规模化", "09-ops", "10-iteration", "跨视口"], } MAIN_HEADERS = [ "## 独立审计与自动推回", "### 独立审计角色:不写代码,只独立重跑、抽查证据、单独签字", "### 自动推回:审计不过关,系统自动推回,不等人工点头", "## 推倒重做:超过三次后的升级分支(不是并行回路)", "## 三条收紧:分流、保留资产、审计的审计", ] QA_HEADER = "## QA 全链路:测试环节跑通各个和所有业务链路" def fail(msg): print("FAIL: " + msg, file=sys.stderr) return 1 def _lines(text): return text.split("\n") def _find_exact(lines, predicate, what): hits = [i for i, line in enumerate(lines) if predicate(line)] if len(hits) != 1: raise ValueError("%s: expected exactly 1 hit, found %d" % (what, len(hits))) return hits[0] def extract_source(text): """Split #244's text into its four transplant hunks.""" lines = _lines(text) matrix = [line for line in lines if line.startswith("| 独立审计 |")] if len(matrix) != 1: raise ValueError("auditor matrix row: expected 1, found %d" % len(matrix)) main_start = _find_exact(lines, lambda line: line == MAIN_HEADERS[0], "main section heading") honest = _find_exact(lines, lambda line: line.startswith("## 诚实边界"), "honesty section heading") if not main_start < honest: raise ValueError("main section does not precede honesty section") main = lines[main_start:honest] while main and main[-1] == "": main.pop() check_start = _find_exact( lines, lambda line: line.startswith("- 审计与推回走了吗?"), "audit checklist item") check_end = _find_exact( lines, lambda line: line.startswith("- QA 全链路走了吗?"), "QA checklist item") audit_item = _collect_item(lines, check_start, "悄悄放行?") qa_item = _collect_item(lines, check_end, "重审重跑失败链路了吗?") app_start = _find_exact( lines, lambda line: line.startswith("## 附录角色:") and "auditor" in line, "appendix auditor chapter") body = lines[app_start + 1:] while body and body[0] == "": body.pop(0) while body and body[-1] == "": body.pop() return { "matrix_row": matrix[0], "main": main, "audit_item": audit_item, "qa_item": qa_item, "appendix_body": body, } def _collect_item(lines, start, terminator): item = [] for line in lines[start:]: item.append(line) if line.endswith(terminator): return item raise ValueError("checklist item starting at %d never ends with %r" % (start + 1, terminator)) def transform_main(main_lines, target_id): """Apply the graded rewrites/redactions; return (lines, stopped).""" text = "\n".join(main_lines) stopped = [] # R1/R2: stale self-referential paragraphs, all targets. for frag, repl, name in [ (AUDIT_WARNING, AUDIT_WARNING_REPLACEMENT, "audit warning"), (QA_INTRO, QA_INTRO_REPLACEMENT, "QA intro")]: if target_id == "v05" and name == "QA intro": continue # whole QA section is stopped for v05, handled below if text.count(frag) != 1: raise ValueError("fragment %s: expected 1, found %d" % (name, text.count(frag))) text = text.replace(frag, repl) if target_id == "v03": for frag, repl, name in [ (G7_FULLAUTO_BLOCK, "", "G7/full-auto block"), (PUSHBACK_LIMIT_PAREN, PUSHBACK_LIMIT_PAREN_V03, "pushback limit paren"), (REBUILD_TAIL, REBUILD_TAIL_V03, "rebuild tail")]: if text.count(frag) != 1: raise ValueError("fragment %s: expected 1, found %d" % (name, text.count(frag))) text = text.replace(frag, repl) elif target_id == "v04": for frag, repl, name in [ (G7_HALF, " ", "G7 half"), (G7_NOTE, "", "G7 note"), (REBUILD_TAIL, REBUILD_TAIL_V04, "rebuild tail")]: if text.count(frag) != 1: raise ValueError("fragment %s: expected 1, found %d" % (name, text.count(frag))) text = text.replace(frag, repl) if text.count(TIGHTEN3_NOTIFY) != 1: raise ValueError("tighten3 notify: expected 1, found %d" % text.count(TIGHTEN3_NOTIFY)) if (TIGHTEN3_NOTIFY + "\n") in text: text = text.replace(TIGHTEN3_NOTIFY + "\n", "") else: text = text.replace(TIGHTEN3_NOTIFY, "") stopped.append("收紧三:验收失败触发推倒前用户确认段 " "(v04 全自动四类回头问用户之外,需用户拍板第五类或限定默认模式)") elif target_id == "v05": qa_at = text.find(QA_HEADER) if qa_at < 0: raise ValueError("QA section heading not found") text = text[:qa_at].rstrip("\n") stopped.append("QA 全链路整节 (v05 跨视口口径与跨端 framing 冲突,需视口版重写)") if text.count(TIGHTEN3_NOTIFY) != 1: raise ValueError("tighten3 notify: expected 1, found %d" % text.count(TIGHTEN3_NOTIFY)) if (TIGHTEN3_NOTIFY + "\n") in text: text = text.replace(TIGHTEN3_NOTIFY + "\n", "") else: text = text.replace(TIGHTEN3_NOTIFY, "") stopped.append("收紧三:验收失败触发推倒前用户确认段 " "(v05 全自动四类回头问用户之外,需用户拍板第五类或限定默认模式)") else: raise ValueError("unknown target-id %r" % target_id) # Formatting only: a redaction can leave a double blank line where # the removed sentences sat. Collapse runs of 3+ newlines to one # blank line; no wording is touched. import re as _re text = _re.sub(r"\n{3,}", "\n\n", text) lines = text.split("\n") while lines and lines[-1] == "": lines.pop() return lines, stopped def apply_transplant(source_text, target_text, target_id): """Return (draft_text, report_lines). Raises ValueError on any miss.""" hunks = extract_source(source_text) tlines = _lines(target_text) report = [] # 1. matrix row after the QA row. qa_row = _find_exact(tlines, lambda line: line.startswith("| QA / 测试 |"), "target QA matrix row") if any(line.startswith("## 附录角色:") and "auditor" in line for line in tlines): raise ValueError("target already contains an auditor appendix chapter") out = list(tlines) out.insert(qa_row + 1, hunks["matrix_row"]) report.append("matrix row inserted after target QA row") # 2. main section before the honesty heading (re-find after insert). honest = _find_exact(out, lambda line: line.startswith("## 诚实边界"), "target honesty heading") main, stopped = transform_main(hunks["main"], target_id) report.extend("STOPPED (not applied): " + s for s in stopped) out[honest:honest] = main + [""] report.append("main section inserted before honesty heading " "(%d lines, %d stopped paragraphs)" % (len(main), len(stopped))) # 3. checklist items before "有一项为否". check_end = _find_exact(out, lambda line: "有一项为否" in line, "target checklist end") items = list(hunks["audit_item"]) if target_id != "v05": items += hunks["qa_item"] else: report.append("STOPPED (not applied): QA checklist item " "(v05 跨端矩阵/未编译端口径与视口口径冲突)") out[check_end:check_end] = items report.append("checklist item(s) inserted: %s" % ("audit + QA" if target_id != "v05" else "audit only")) # 4. appendix chapter at EOF. while out and out[-1] == "": out.pop() number = "11" if target_id == "v05" else "09" heading = ("## 附录角色:%s-auditor(来源 templates/roles/%s-auditor.md)" % (number, number)) out += ["", "---", "", heading, ""] + hunks["appendix_body"] report.append("appendix chapter appended as %s-auditor" % number) # 5. decision B: artifact references. text = "\n".join(out) for old, new in ARTIFACT_REPLACEMENTS: if text.count(old) != 1: raise ValueError("artifact replacement %r: expected 1, found %d" % (old[:40], text.count(old))) text = text.replace(old, new) report.append("decision B applied: 6 templates/artifacts/ references " "removed/rewritten") if not text.endswith("\n"): text += "\n" return text, report def check_anchors(source_text, target_text, target_id): errors = [] try: hunks = extract_source(source_text) except ValueError as exc: return ["source hunks not extractable: %s" % exc] tlines = _lines(target_text) for what, pred in [ ("target QA matrix row", lambda line: line.startswith("| QA / 测试 |")), ("target honesty heading", lambda line: line.startswith("## 诚实边界")), ("target checklist end", lambda line: "有一项为否" in line), ("target appendix intro", lambda line: line == "## 附录:角色模板(播种随附版)"), ]: if sum(1 for line in tlines if pred(line)) != 1: errors.append("anchor %s: expected 1, found %d" % (what, sum(1 for line in tlines if pred(line)))) last_chapter = ("10-iteration" if target_id == "v05" else "08-shared-core") if not any(line.startswith("## 附录角色:%s" % last_chapter) for line in tlines): errors.append("target appendix lacks last chapter %s" % last_chapter) if any("auditor" in line and line.startswith("## 附录角色:") for line in tlines): errors.append("target already contains an auditor appendix chapter") for marker in MARKERS[target_id]: if marker not in target_text: errors.append("preserved marker missing in target: %s" % marker) refs = [i + 1 for i, line in enumerate(tlines) if "templates/artifacts" in line] print("anchors: source hunks OK (matrix + main %d lines + 2 checklist " "items + appendix %d lines)" % (len(hunks["main"]), len(hunks["appendix_body"]))) print("anchors: templates/artifacts/ references in target: %d at lines %s " "(expected: 6 sites, removed at transplant time per decision B)" % (len(refs), refs)) print("anchors: appendix chapter number for %s: %s" % (target_id, "11-auditor" if target_id == "v05" else "09-auditor")) return errors def check_transplanted(target_text, transplanted_text, target_id): errors = [] for marker in MARKERS[target_id]: if marker not in transplanted_text: errors.append("preserved marker lost: %s" % marker) rows = [line for line in _lines(transplanted_text) if line.startswith("| 独立审计 |")] if len(rows) != 1: errors.append("auditor matrix row: expected 1, found %d" % len(rows)) for header in MAIN_HEADERS: if header not in transplanted_text: errors.append("main header missing: %s" % header) if target_id == "v05": if QA_HEADER in transplanted_text: errors.append("STOPPED QA section present in v05 output") if "- QA 全链路走了吗?" in transplanted_text: errors.append("STOPPED QA checklist item present in v05 output") if "验收失败导致的推倒要先通知用户" in transplanted_text: errors.append("STOPPED tighten3 notify present in v05 output") elif target_id == "v04": if QA_HEADER not in transplanted_text: errors.append("QA section missing in v04 output") if "验收失败导致的推倒要先通知用户" in transplanted_text: errors.append("STOPPED tighten3 notify present in v04 output") if "G7 热修复回路并行" in transplanted_text: errors.append("STOPPED G7 sentence present in v04 output") else: # v03 if QA_HEADER not in transplanted_text: errors.append("QA section missing in v03 output") if "G7 热修复回路并行" in transplanted_text: errors.append("STOPPED G7 sentence present in v03 output") if "全自动 CI 自动修复" in transplanted_text: errors.append("STOPPED full-auto sentence present in v03 output") if "验收失败导致的推倒要先通知用户" not in transplanted_text: errors.append("tighten3 notify missing in v03 output") number = "11" if target_id == "v05" else "09" heading = ("## 附录角色:%s-auditor(来源 templates/roles/%s-auditor.md)" % (number, number)) if transplanted_text.count(heading) != 1: errors.append("appendix heading %s: expected 1, found %d" % (heading, transplanted_text.count(heading))) if "09-auditor(独立审计,新增)" in transplanted_text: errors.append("old appendix heading form survived") for stale in ["移植警告", "以生产 v0.2 技能为准", "技能正文 diff 全是加法"]: if stale in transplanted_text: errors.append("stale transplant warning survived: %s" % stale) if "templates/artifacts" in transplanted_text: errors.append("templates/artifacts/ reference survived " "(decision B not applied)") # No-overwrite: the target, after decision-B replacements, must be a # subsequence (in order) of the transplanted text. transformed = target_text for old, new in ARTIFACT_REPLACEMENTS: transformed = transformed.replace(old, new) t_iter = iter(_lines(transplanted_text)) remaining = _lines(transformed) pos = 0 for line in t_iter: if pos < len(remaining) and line == remaining[pos]: pos += 1 if pos != len(remaining): errors.append("no-overwrite check failed: target line %d (%r) " "missing or out of order in transplanted text" % (pos + 1, remaining[pos][:80] if pos < len(remaining) else "")) return errors def main(argv=None): parser = argparse.ArgumentParser( description="Transplant preparation checker for #244 -> v0.3/v0.4/v0.5.") sub = parser.add_subparsers(dest="cmd", required=True) for name in ["anchors", "apply", "check-transplanted"]: p = sub.add_parser(name) p.add_argument("--source244", required=True) p.add_argument("--target", required=True) p.add_argument("--target-id", required=True, choices=["v03", "v04", "v05"]) if name == "apply": p.add_argument("--out", default=None) if name == "check-transplanted": p.add_argument("--transplanted", required=True) args = parser.parse_args(argv) try: source = Path(args.source244).read_text(encoding="utf-8") target = Path(args.target).read_text(encoding="utf-8") except OSError as exc: print("FAIL: cannot read input: %s" % exc, file=sys.stderr) return 2 try: if args.cmd == "anchors": errors = check_anchors(source, target, args.target_id) if errors: for err in errors: print("FAIL: " + err, file=sys.stderr) print("anchors FAILED: %d problem(s)" % len(errors), file=sys.stderr) return 1 print("anchors OK for %s" % args.target_id) return 0 if args.cmd == "apply": draft, report = apply_transplant(source, target, args.target_id) if args.out: Path(args.out).write_text(draft, encoding="utf-8") else: sys.stdout.write(draft) for line in report: print("apply: " + line, file=sys.stderr) return 0 transplanted = Path(args.transplanted).read_text(encoding="utf-8") errors = check_transplanted(target, transplanted, args.target_id) if errors: for err in errors: print("FAIL: " + err, file=sys.stderr) print("check-transplanted FAILED: %d problem(s)" % len(errors), file=sys.stderr) return 1 print("check-transplanted OK for %s" % args.target_id) return 0 except ValueError as exc: print("FAIL: %s" % exc, file=sys.stderr) return 2 if __name__ == "__main__": raise SystemExit(main())