(cherry picked from commit 4411186681584bf55e065c654fe909a3320d4a22) Reconcile-conflict: test_skill_transplant_prep artifact count — kept HEAD's finalized 0 refs (2026-11-02 user sign-off deleting §4's 6 references, already in target) with incoming's l→line rename; incoming's 6-ref assertion is pre-finalization and must not overwrite.
533 lines
25 KiB
Python
533 lines
25 KiB
Python
#!/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())
|