docs(skill): transplant prep for #244 audit additions into v0.3/v0.4/v0.5

Preparation only: no v0.3/v0.4/v0.5 text is changed, nothing is
claimed transplanted or deployed. Stacked on feat-audit-pushback
(#244, 39966bd).

- Appendix provenance 00..09: templates/roles/00-08 sources
  (byte-identical to #217) plus 09-auditor extracted from #244's
  appendix; check_appendix.py extended 00..08 -> 00..09.
  SKILL.md sole change (1 line, format-only): 09 chapter heading
  normalised to the strict source form, otherwise the #217
  checker cannot recognise the chapter. check_appendix.py passes
  on the real pack: 10 chapters match; --emit outputs 10 chapters.
- TRANSPLANT-PREP-244.md: per-paragraph transplant diffs for
  #244's +459 lines into v0.3/v0.4/v0.5 with anchors, grades
  (merge / merge+note / partial stop / full stop), chapter-number
  mapping (v0.5: 11-auditor, colliding 09-ops noted), the
  templates/artifacts delete-references decision with 6 sites and
  replacements, and the draft-sync checklist (sync itself is
  post-deployment only, not done here).
- tools/check_transplant.py: anchors / draft-apply /
  check-transplanted modes; draft-apply on the real v0.3/v0.4/v0.5
  texts passes check-transplanted (drafts in /tmp only, not
  committed). Stopped paragraphs are listed, never silently
  rewritten: v0.3 G7/full-auto sentences, v0.4 G7 sentences and
  the tighten-3 pre-rebuild confirmation, v0.5 whole QA section
  plus its checklist item and the confirmation paragraph.
- Tests: test_skill_appendix_provenance.py (00..09) and
  test_skill_transplant_prep.py; 13 new tests pass.
  Suites on this tree: cloud 1523 OK (skipped=4), top-level 105
  OK, deploy 39 OK.
This commit is contained in:
host committed 2026-11-04 20:58:34 +00:00
1 parent e758e8dd44
commit c4f9199701
4 files changed
+1222

No files matched your search

@@ -83,3 +83,40 @@ v0.1 → v0.2 的 5 个 P0 缺口明细无法从本文件追溯,本文件不
`templates/roles/11-auditor.md`(正文与 #258 的 09-auditor 同源); `templates/roles/11-auditor.md`(正文与 #258 的 09-auditor 同源);
校验表同步扩为 12 章。§4 的 templates/artifacts 删引用决策 校验表同步扩为 12 章。§4 的 templates/artifacts 删引用决策
未在集成基底执行(仍待用户拍板),SKILL.md 内 6 处引用原样保留。 未在集成基底执行(仍待用户拍板),SKILL.md 内 6 处引用原样保留。
## 集成注(code-to10-integration,#258 段)
以下两节原样引自 #258 提交的 CHANGELOG 条目,记录该提交本身的工作;
其中“09-auditor”源文件在本基底的章节表里编号为 11-auditor(v0.5 表
00–08 + 09-ops + 10-iteration + 11-auditor),内容与 11-auditor 源文件
逐字节一致,故 #258 的 09-auditor.md 未重复落盘;#244 向 v0.5 的实际
移植已在本基底的 #244 段完成,与该提交“未移植”的自述不冲突。
## 2026-10-31:#244 向 v0.3 / v0.4 / v0.5 的移植准备(未移植)
- 新增 `TRANSPLANT-PREP-244.md`:#244 新增段落(+459 行)
向 v0.3、v0.4、v0.5 三份文本的逐段移植 diff 准备、
锚点行号、合并 / 移植注 / 停下三档分级、章节号对应表、
templates/artifacts 二选一决策、草稿同步清单。
本轨不改 v0.3 / v0.4 / v0.5 文本,不声称已移植。
- 新增 `tools/check_transplant.py`:移植检查脚本
(锚点检查、草稿移植、移植后检查三模式,见该文件说明)。
- templates/artifacts 决策(本轨定为删引用,见准备文档):
SKILL.md 正文 6 处 `templates/artifacts/` 引用在移植时删除
或改写为正文内联结构,不补模板文件进包、不改播种规则。
本轨不改 SKILL.md 正文,故 6 处引用在本轨包内仍在,
移植检查脚本的移植后检查会把残留引用报为未完成。
## 已知边界(未改行为,另起设计时再动)
- 租户已删 `SKILL.md` 会在下次播种时重建(missing 即复制),
这是现有播种语义,不是删除保留;要保留删除需 tombstone
设计,须另起一轨,不在本条目悄悄改播种行为。
- 校验的允许差异只有三项:章节标题行本身、至多一枚章节间
分隔符行、标题与章末的空行。现行 SKILL.md 的分隔符并不
统一(01/02/03/05/06/08 章后有 `---`,00/04/07 章后无分隔符
直接接下一章标题),校验保留该现状、不强行统一 SKILL.md。
- #244 文本里的“第 1 轨硬门禁”“第 4 轨账本”指审计加固
轨内部的轨号,不是本批“各项指标 9 分以上”的第 1–4 项;
本批第 2 项 attempts-hook、第 3 项 shell 白名单与之无关,
不得混淆(见准备文档)。
@@ -0,0 +1,362 @@
# #244 向 v0.3 / v0.4 / v0.5 的移植准备(不是移植本身)
> 状态:准备文档。移植本身只能在合并时做,本轨不改
> v0.3 / v0.4 / v0.5 文本,不声称已移植、已部署。
> 本轨的包内改动(见 CHANGELOG)只涉及 v0.2 + #244 线:
> 附录源文件 00-09、校验脚本 00-09 版、本准备文档、
> 移植检查脚本与测试;SKILL.md 仅改 1 行附录标题格式。
## 0. 基底与来源(复核于 2026-10-31)
- 主线基底:`origin/main` = `aebc623`(Merge PR #201)。
- 来源 #244:`feat-audit-pushback` @ `39966bd`,PR #244
OPEN / MERGEABLE,SKILL.md 1272 行、81,565 字节,
相对 v0.2(49,326 字节、813 行)纯加法 +459 行,
分 4 段(行号均为 #244 文本内行号):
- A. 角色矩阵行:L134,1 行。
- B. 主节“独立审计与自动推回”:L327–L730(+ L731 空行),
405 行。内含:审计与推回 L327–L441、推倒重做 L442–L554、
三条收紧 L555–L641、QA 全链路 L642–L730。
- C. 校验新增项:L757–L766,10 行(审计项 L757–L761、
QA 项 L762–L766)。
- D. 附录 09 章:L1230–L1272,43 行(空行 + `---` +
标题 L1233 + 正文 38 行 L1235–L1272)。
- 目标 v0.3:PR #199,`feat-software-engineering-v03` @
`19783b0`,SKILL.md 905 行、56,607 字节。
- 目标 v0.4:PR #205,`feat-full-auto` @ `d6522ba`,
SKILL.md 993 行、63,893 字节(堆叠于 v0.3)。
- 目标 v0.5:PR #207,`feat-swe-g7g8` @ `8a68100`,
SKILL.md 1326 行、88,540 字节(堆叠于 v0.4)。
- 附录溯源 #217:`feat-skill-appendix-provenance` @
`e6b5014`,PR #217 OPEN / MERGEABLE;其 00-08 源文件
与从 #244 附录拆出的 00-08 源文件逐字节一致
(#244 未改 00-08 章正文)。
- 生产现况:仍跑技能 v0.2(49,326 字节);工作区草稿
`~/workspace/software-engineering-skill/` 是更旧的 v0.2
草稿(40,759 字节、686 行),绝不能从它推、不能用它
覆盖任何部署版(差异清单见 §5)。
## 1. 移植 diff:锚点(每目标一行号均为该目标文本内行号)
| 段 | 插入位置(统一规则) | v0.3 | v0.4 | v0.5 |
|---|---|---|---|---|
| A 矩阵行 | QA 行之后、验收行之前 | QA 行 L138 | QA 行 L206 | QA 行 L359(该行已是跨视口措辞,行本身不动) |
| B 主节 | “失败、续跑与成本”节末、“诚实边界”节首之前 | 失败节 L363–L374,诚实边界 L376 | 失败节 L433–L444,诚实边界 L446 | 失败节 L597–L608,诚实边界 L610(标题带“v0.5 后置占位版”后缀,锚点按前缀匹配) |
| C 校验项 | “有一项为否”行之前(即全部已有校验项之后,不拆散 v0.3 / v0.5 新增项) | 有一项为否 L406 | 有一项为否 L478 | 有一项为否 L657(只插审计项,QA 项停下,见 §2) |
| D 附录章 | 文件末尾、上一章正文之后:空行 + `---` + 空行 + 标题 + 空行 + 正文 | 08 章 L873 起、文件末 L905 | 08 章 L961 起、文件末 L993 | 10 章 L1277 起、文件末 L1326,编号改为 11(见 §3) |
规则语义不变性:除 B 节内的改写(§2 的 R1/R2)、
删句(§2 的部分停下)、C/D 的编号与 §4 的引用删除外,
移植是纯插入;目标文本已有行不得改写、不得删除
(移植后检查的“无覆盖”子序列校验钉住这一点)。
## 2. 逐段分级:合并 / 合并+移植注 / 部分停下 / 整段停下
分级定义:合并 = 原文照搬;合并+移植注 = 原文照搬,
但移植轨须在执行记录里写清注明的口径映射(不改规则);
部分停下 = 该段内标明的句子不移植,其余句子移植;
整段停下 = 整段不移植,须用户或移植轨先定重写方案,
绝不能悄悄改语义后硬搬。
### 2.1 A 矩阵行:三目标全部合并
行文本与 #244 L134 相同。v0.5 的 QA 行已是跨视口措辞,
审计行插在 QA 行与验收行之间,不动运维 / 迭代两行。
### 2.2 B1 审计与推回(L327–L441)
- 五职分开(写 / 测 / 审 / 执行 / 签字):
v0.3 合并;v0.4 / v0.5 合并+移植注——#244 写
“执行合并 / 发布是人或宿主操作”,在全自动模式下
执行人字段填主机自动执行器 `full-auto-executor` +
installation id(v0.4 全自动节原话),签字人与执行人
字段仍须不同,分离规则本身不变。
- 硬门禁接口约定(`merge_blocked` / `release_blocked`)
与账本接口(`build_ledger_entry` /
`validate_ledger_chain`)的“目前是提案”说明:
三目标全部合并(提案状态未变;注意 #244 的
“第 1 轨 / 第 4 轨”指审计加固轨内部轨号,
不是本批 9 分项的第 1–4 项,不得混淆)。
- “与已有回路的关系”段(G5 复用 + G7 并行 + 全自动并行
+ 注意句):
- v0.3 部分停下:G5 复用句移植;G7 并行句、全自动并行句、
注意句不移植(v0.3 文本里 G7、全自动两个回路都不存在,
照搬会成悬空引用)。
- v0.4 部分停下:G5 复用句与全自动并行句移植;G7 并行句、
注意句不移植(v0.4 无 G7;注意句称“全自动尚未出现”
在 v0.4 已为假)。
- v0.5 合并:G7 热修复回路(发布后应急)与全自动 CI
回路在 v0.5 文本里真实存在,且口径与 #244 的描述一致
(推回是合入前、热修复是发布后应急;计数独立)。
- 推回上限段“与全自动 CI 自动修复限 3 次同口径”括号:
- v0.3 部分停下:括号改写为“推回上限独立于其它修复回路
的轮次预算,一方不得消耗另一方的预算”(上限 3 轮
本身保留;CI 轮次在审计前、推回轮次在审计后的
排序句随括号一并删去,v0.3 无此回路可排序)。
- v0.4 / v0.5 合并:v0.4 全自动节原话即“CI 自动修复
循环限 3 次”,口径一致;排序句(CI 轮次在审计前)
是 #244 的定义,v0.4 文本未另定顺序,不冲突。
- R1 改写(三目标):审计节引言的移植警告段
(“本节是按主线 v0.2 文本写的……绝不能用本文件
覆盖它们”)不得照搬,移植时改写为:
“移植注:本节源自审计加固轨,移植时已按本文件对应章节
的回路口径对齐。”(前文“G5 缺陷回路、合并拆分、
G6 用户验收的原意都不变”保留。)
### 2.3 B2 推倒重做(L442–L554)
- 计数口径、阈值 `> 3`、只推倒该模块、先判根因、
重做上限 1 次、成本与配额:三目标全部合并。
- 状态与账本段尾句“与 G7 热修复回路并行……与全自动
CI 自动修复并行且计数独立”:
- v0.3 部分停下:尾句整体改为句号结尾(只保留
“与 G5 缺陷回路复用修复执行通道”)。
- v0.4 部分停下:删 G7 分句,保留全自动分句。
- v0.5 合并。
### 2.4 B3 三条收紧(L555–L641)
- 收紧一(第三次失败先判根因、结构性提前推倒、
接近完成 targeted 续轮)、收紧二(只推倒实现代码、
旧测试全过为第一道门)、收紧三前两条(审计兜底三件套、
判错纠错路径):三目标全部合并。
- 收紧三末段“验收失败导致的推倒要先通知用户”
(L635–L640,推倒前须通知并确认新方向、未确认前
不得静默重开):
- v0.3 合并(无全自动模式,无冲突)。
- v0.4 / v0.5 整段停下:v0.4 全自动节称“只有四类情况
回头问用户”(成本超阈值、删数据 / 改权限 / 对外账号、
外部服务要人工验证、自动修复连续失败),验收失败触发
推倒前确认新方向不在四类明文之内;成本超阈值通道与
自动修复连续失败通道能否覆盖它,须用户拍板二选一:
(a)全自动四类表增加第五类“验收失败触发推倒前确认
新方向”;(b)该确认限定默认模式,全自动模式下走
已有升级通道。移植轨不得擅自选(a)或(b),
本轨的草稿移植已把该段排除(见 §6)。
### 2.5 B4 QA 全链路(L642–L730)
- v0.3 合并+移植注:
- 自测三项(单元 + 契约集成 + 手工冒烟)是下限,
不是替换:v0.3 客户端附录章的交付前自测共 4 项,
第 4 项“跨端一致性自查”继续有效,不得因“三项”
之名删掉。
- agent 专属六项集合与 v0.3 完全相同(单端复核 3 项:
沙箱越界、危险动作审批、token / 配额超限;
跨端 E2E 3 项:长任务超时和取消、断网续跑、
模型 503 重试);v0.3 的归属拆分继续为准,
#244 未另定归属,不冲突。
- QA 分工(单端复核 + 跨端 E2E 两条线)与 v0.3
P1-3 的 QA 拆分是同一拆分,#244 是 elaboration,
不是替换。
- v0.4 合并+移植注:同 v0.3(v0.4 继承 v0.3 的
QA 拆分与归属;全自动模式不改变 QA 分工)。
- v0.5 整段停下:v0.5 已定口径把 QA 第二部分改为
跨视口 E2E(矩阵的行是手机 / 平板 / 桌面视口,
G5 门改为跨视口一致性),而 #244 QA 节至少 4 处
与之正面冲突:① 分工句“跨端 E2E 跑遍各端”;
② G5 过关句“跨端一致性”;③ 矩阵句“覆盖全部已选
客户端”;④ 诚实例外句“iOS / Android 编不出来时
结论只能写代码评审 + Web 核心验证”(v0.5 对
iOS / Android 是后置占位、不进 QA 通过表;
Mac 句仍成立,但整节不能只拆一句硬搬)。
因此 v0.5 的 QA 节须由移植轨重写为视口版后再移植,
不得把跨端版原文搬进 v0.5。附带:QA 节内 agent
六项集合本身与 v0.5 一致(归属改为跨视口 E2E),
视口版重写时六项清单原样保留。
- R2 改写(三目标,v0.5 的 QA 节停下故不适用):
QA 节引言“QA 要求以生产 v0.2 技能为准……移植警告
……技能正文 diff 全是加法”不得照搬——移植后 diff
本来就不是纯加法(含 R1/R2 改写、删句、§4 删引用、
v0.5 章节号重编),“全是加法”句在移植文本里为假。
改写为:“本节是独立审计与 QA 部分的补充验收标准,
与自动推回、推倒重做、三条收紧并列,不覆盖已有规则;
已有规则语义不改,只补测试机制。”
### 2.6 C 校验项(L757–L766)
- 审计项(三目标全部合并):签字人独立、安全签字无条件、
自动推回阻塞、推倒分支处理,均为目标无关表述。
插入位置统一在“有一项为否”行之前(已有 v0.3 / v0.5
新增项之后),不拆散已有校验项。
- QA 项:v0.3 / v0.4 合并(“每端自测三项”按 2.5 的
下限口径读;“跨端矩阵覆盖全部已选端”在 v0.3 / v0.4
仍成立);v0.5 整段停下(“跨端矩阵覆盖全部已选端”
与视口矩阵冲突;“未编译端只写代码评审 + Web 核心验证”
与 iOS / Android 后置占位冲突;随 QA 节视口版重写
一并改写,不得照搬)。
### 2.7 目标版本已有内容在移植后必须仍在(检查脚本钉住)
- v0.3:目录隔离自动检查(命令 + 输出记入合并记录)、
派生校验表 8 项、子任务精简包、QA 拆分、任务间问答
默认策略(`docs/decisions.md` 只追加)。
- v0.4:全自动模式(API 优先、一次授权、full-auto-executor、
FULL_AUTO_ENABLED、CI 自动修复限 3 次)+ 全部 v0.3 项。
- v0.5:G7 发布后运维与观察期、G8 迭代与规模化、
附录 09-ops / 10-iteration、跨视口口径 + 全部 v0.4 项。
- 绝不能用 #244 的 v0.2 文本覆盖上述任一目标文本:
覆盖会丢 v0.3 的目录隔离检查、v0.4 的全自动、
v0.5 的 G7/G8(#244 正文自己的移植警告亦如此要求)。
## 3. 附录章节号对应表(不要把章节号搞混)
| 线 | 附录章节 | 源文件 | 标题格式 |
|---|---|---|---|
| v0.2(#217) | 00–08 | `templates/roles/00-…08-*.md` | `## 附录角色:<id>(来源 templates/roles/<id>.md)`(严格式) |
| v0.2 + #244(本轨包) | 00–08 同上 + 09-auditor | 增加 `templates/roles/09-auditor.md` | 09 标题已规范化为严格式;#244 原标题“(独立审计,新增)”不符合 #217 校验正则,移植时一律用严格式,称谓保留在章首 `# 角色模板:独立审计……` 行 |
| v0.3 / v0.4 移植后 | 00–08 同上 + 09-auditor | 增加 `templates/roles/09-auditor.md`(与本轨包同一文件) | 严格式 |
| v0.5 移植后 | 00–08 同上 + 09-ops + 10-iteration + 11-auditor | 增加 `templates/roles/11-auditor.md`(正文与 09-auditor 相同,只改编号与标题;另须在移植时从 v0.5 附录机械拆出 09-ops / 10-iteration 源文件,方法同 #217,否则校验脚本无法覆盖 v0.5 全表) | v0.5 现有 09/10 标题带“,v0.5 新增”后缀,不符合 #217 严格正则;移植时校验脚本的标题正则须放宽为允许可选后缀,或把 09/10 标题规范化为严格式——二选一由移植轨定,本轨不擅自改 v0.5 标题 |
校验脚本期望表(`tools/check_appendix.py` 的 EXPECTED_IDS):
本轨包为 00–09;v0.3 / v0.4 移植后仍为 00–09;
v0.5 移植后必须重定基为
`00-…-08, 09-ops, 10-iteration, 11-auditor`,
绝不能拿本轨的 00–09 表直接验 v0.5 文本。
附录生成器(`--emit`)按期望表拼接:09-auditor 源文件缺失时
重跑会掉 09 章;本轨补齐后 `--emit` 输出 10 章标题 + 正文。
## 4. templates/artifacts 缺失模板名:二选一决策——删引用
SKILL.md 正文在 6 处引用 `templates/artifacts/`,
而 #217 后的包里 `templates/` 只有 `roles/`,没有
`artifacts/`;`skill_seed` 只拷 SKILL.md,模板文件
即使补进包也不会进租户目录。本轨决策:删引用
(不补模板文件进包、不改播种规则)。取舍如下:
- 为什么不选补齐:补齐为“编辑源”不能修复租户侧
悬空引用(租户仍看不到文件);要真正修好须另做
二选一之外的事——把 5 份模板嵌进附录(SKILL.md
膨胀约 8KB,且 v0.3 / v0.4 / v0.5 各需一份)或改
播种规则(生产行为变更,另起一轨)。两种都超出
本轨范围,且补齐后声称“已修好”会是假话。
- 删引用为何损失可控:6 处引用之后都紧跟内联实质,
删路径句不丢规则(见下表);失去的只是现成的
填空式表单(工作区草稿 `templates/artifacts/` 下 5 份
文件仍在,仅作历史参考,不进包、不播种)。
| # | 位置(6 处在三目标文本里措辞相同) | 删 / 改写 | 替代(内联实质已在正文里) |
|---|---|---|---|
| 1 | 门禁表 G1 行 | “PRD 按 `templates/artifacts/prd-outline.md` 写完”→“PRD 按产品经理附录章节所列大纲写完” | 产品经理附录章所列 PRD 大纲 1–6 项(含 agent 三节) |
| 2 | 接单选型节 | 删“问卷模板在 `templates/artifacts/intake-questions.md`。”一句,保留“最关键的六问:” | 六问全文紧随其后 |
| 3 | 测试与验收节 | 删整行“验收清单模板在 `templates/artifacts/acceptance-checklist.md`。” | 其后验收语义(用户逐项确认、按端分别做)完整保留 |
| 4 | 产品经理附录章产出 | “按 `templates/artifacts/prd-outline.md` 写一份 PRD”→“按下面大纲写一份 PRD” | 其后大纲 1–6 项全文 |
| 5 | 架构师附录章产出第 4 项 | “协议模板见 `templates/artifacts/` 下的 agent 任务契约模板。”→“(Agent 协议、状态机、错误 taxonomy 的要素见本章第 4、5、6 项。)” | 本章第 4/5/6 项 + G0 执行模型、权限边界、审批模型 |
| 6 | 验收附录章协调员准备第 1 项 | “按 `templates/artifacts/acceptance-checklist.md` 生成”→“按 PRD 验收标准生成” | 其后“每一项对应 PRD 里的一条验收标准” |
工作区草稿的 5 份 artifacts 文件(acceptance-checklist、
agent-task-contract、coordinator-spawn-example、
intake-questions、prd-outline)与本决策的关系:
`coordinator-spawn-example.md` 从未被 SKILL.md 引用,
其余 4 份对应上表 4 个文件名;决策后它们都不进包,
草稿侧标注为历史参考(见 §5 同步清单)。
## 5. 工作区草稿与部署版的同步清单(本轨只给清单,不执行)
差异清单(实测,2026-10-31):
- 草稿 SKILL.md(40,759 字节、686 行)vs 生产部署版
v0.2(49,326 字节、813 行):草稿是严格更旧的子集,
生产版比草稿多 7 处增补(含 G0 仓库冻结项与 G0.5
建仓门),另有 1 处 G0 行措辞随增补而扩展;无反向
增补。结论:同步方向只能是部署版 → 草稿,
绝不能用草稿覆盖部署版或任何 PR 文本。
- 草稿 `templates/roles/` vs #217 源文件:01/02/03/05/06/08
六份逐字节一致;00/04/07 三份草稿更旧更短
(草稿 00: 4094 字节 vs 源 6157 字节;04: 3242 vs 4087;
07: 1559 vs 2121),同步时以源文件为准。
- 草稿 CHANGELOG / README vs #217 CHANGELOG / #225 README:
是两套不同的文档(草稿版自述“仅起草、未提 PR”),
同步时以包内版本为准,草稿版不合并、不覆盖。
- 草稿独有:`templates/artifacts/` 5 份、
`WALKTHROUGH-agent-tool.md`、verification/ 下各轨摘要。
前者按 §4 决策不进包;后两者是工作区侧资料,不进包。
- 各 PR 文本字节数:v0.3 56,607 / v0.4 63,893 /
v0.5 88,540 / #244 81,565(单位:字节;行数见 §0)。
合并部署后同步回草稿的步骤清单(按顺序,部署完成前不动草稿):
1. 先合 #199 → #205 → #207 链(v0.3 → v0.4 → v0.5,
本就是堆叠顺序),再合 #217 → #225(附录溯源 →
README),再合 #244 → 本轨(审计加固 → 移植准备),
最后由移植轨按 §1–§4 把审计段落移植进已合并的
v0.5 文本并解决 §2 的停下项(用户拍板后)。
顺序要点:移植准备轨(本轨)不得早于 #244 合并;
若 #217 先合并,本轨的校验脚本与 CHANGELOG
会冲突,解法写死为保留 00-09 版(见 §6)。
2. 部署后,以部署版包(`cloud/skills/software-engineering/`)
为准覆盖草稿:SKILL.md、`templates/roles/`、
`tools/`、CHANGELOG.md、README.md。
3. 草稿侧保留不动:verification/ 全部摘要、
`WALKTHROUGH-agent-tool.md`;`templates/artifacts/`
标注为“已删引用、仅历史参考”,不删除(删了会丢
表单原件的唯一副本),也不进包。
4. 校验脚本(部署后在草稿目录外跑,不污染草稿):
`sha256sum` 对比草稿 SKILL.md 与部署版 SKILL.md
必须一致;字节数与行数与部署记录一致;
`python3 tools/check_appendix.py` 在部署版包上通过
(章节数按最终合并的章节表:v0.5 线为 12 章
00–08 + 09-ops + 10-iteration + 11-auditor);
`grep -c templates/artifacts SKILL.md` 在最终部署版
为 0(决策 B 落地后)。
5. 同步后草稿 SKILL.md 文首版本行由部署轨负责更新
(草稿与 v0.2 部署版文首目前都自述“仍是草稿、
未提 PR”,该自述在部署版里已 stale,不由本轨改)。
## 6. 本轨堆叠与合并顺序(PR 正文亦写清)
- 本轨分支 `feat-skill-transplant-prep` 堆叠于
`feat-audit-pushback`(#244,`39966bd`)之上,
PR base 设为 `feat-audit-pushback`(不是 main),
以便 09 章节与源文件在真实包上可验。
- 本轨包内改动:SKILL.md 仅 1 行标题格式规范化;
其余为新增文件(源文件 10 份、校验脚本 2 份、
CHANGELOG、本准备文档、测试 2 份)。零规则改动。
- 合并顺序:#217 → #225 → #244 → 本轨 →
(移植轨,另起一轨,须用户先拍板 §2 的停下项与
§4 决策)→ v0.3 / v0.4 / v0.5 链的最终合并。
注意 #199 / #205 / #207 链与 #244 / #217 链的
SKILL.md 合并本身即移植动作,不在本轨完成。
- 已知的合并冲突点(若 #217 先合并):
`tools/check_appendix.py`(00-08 vs 00-09,保留 00-09)、
`CHANGELOG.md`(两段条目并存)、
`cloud/tests/test_skill_appendix_provenance.py`
(00-08 版 vs 00-09 版,保留 00-09);
`templates/roles/00-…08-*.md` 为 identical-add,不冲突。
- 正式移植 diff 与本轨准备 diff 的区别:本轨的
`check_transplant.py --apply` 输出是草稿(/tmp 内,
不进提交),供评审核对锚点与停下项;正式移植 diff
是移植轨在合并时对目标文本的真实改动,须另经
CI 与移植后检查(本脚本 `check-transplanted` 模式)
全部通过,且 §2 的停下项须先有用户拍板或重写版。
## 7. 检查脚本用法
```sh
# 锚点与保留标记(对真实三目标文本已跑过,见验证摘要)
python3 cloud/skills/software-engineering/tools/check_transplant.py anchors \
--source244 <244 文本> --target <v03|v04|v05 文本> --target-id v03|v04|v05
# 草稿移植(只写 /tmp,不进提交;停下段落列在 stderr)
python3 cloud/skills/software-engineering/tools/check_transplant.py apply \
--source244 <244 文本> --target <目标文本> --target-id v03|v04|v05 \
--out /tmp/draft-<目标>.md
# 移植后检查(正式移植轨的门禁之一)
python3 cloud/skills/software-engineering/tools/check_transplant.py check-transplanted \
--source244 <244 文本> --target <目标文本> --target-id v03|v04|v05 \
--transplanted <移植后文本>
# 附录校验(本轨包内)
python3 cloud/skills/software-engineering/tools/check_appendix.py
```
## 8. 未做项与诚实边界
- 未移植:v0.3 / v0.4 / v0.5 文本在本轨零改动;
草稿移植输出只在 /tmp,不进提交、不进 PR。
- 未验证:真实 PG、真实任务冒烟(按任务要求不做);
本轨证据全部是本地文本比对与单测,不冒充生产验证。
- 未做:CI 清理、台账钩子、shell 白名单、发布接线、
分支保护、v1.1 契约确认——均属其它轨或用户侧。
- 用户侧仍需拍板(见验证摘要末节):§4 删引用决策、
§2.4 收紧三确认段的(a)/(b)、§2.5 v0.5 QA 节
视口版重写方案、§3 v0.5 标题正则放宽 vs 规范化、
§5 同步顺序。
@@ -0,0 +1,532 @@
#!/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 l: l == MAIN_HEADERS[0],
"main section heading")
honest = _find_exact(lines, lambda l: l.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 l: l.startswith("- 审计与推回走了吗?"),
"audit checklist item")
check_end = _find_exact(
lines, lambda l: l.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 l: l.startswith("## 附录角色:") and "auditor" in l,
"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 l: l.startswith("| QA / 测试 |"),
"target QA matrix row")
if any(l.startswith("## 附录角色:") and "auditor" in l for l 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 l: l.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 l: "有一项为否" in l,
"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 l: l.startswith("| QA / 测试 |")),
("target honesty heading", lambda l: l.startswith("## 诚实边界")),
("target checklist end", lambda l: "有一项为否" in l),
("target appendix intro", lambda l: l == "## 附录:角色模板(播种随附版)"),
]:
if sum(1 for l in tlines if pred(l)) != 1:
errors.append("anchor %s: expected 1, found %d"
% (what, sum(1 for l in tlines if pred(l))))
last_chapter = ("10-iteration" if target_id == "v05"
else "08-shared-core")
if not any(l.startswith("## 附录角色:%s" % last_chapter) for l in tlines):
errors.append("target appendix lacks last chapter %s" % last_chapter)
if any("auditor" in l and l.startswith("## 附录角色:") for l 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, l in enumerate(tlines) if "templates/artifacts" in l]
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 = [l for l in _lines(transplanted_text)
if l.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())
+291
View File
@@ -0,0 +1,291 @@
"""Transplant preparation checks for #244 -> v0.3 / v0.4 / v0.5.
What this pins (see TRANSPLANT-PREP-244.md next to the skill):
- tools/check_transplant.py applies the graded transplant to fixture
texts: v03 drops the G7 / full-auto sentences, v04 drops the G7
sentences and the tighten-3 pre-rebuild confirmation paragraph but
keeps the full-auto sentences, v05 drops the whole QA section, its
checklist item and the confirmation paragraph, and renumbers the
appendix chapter to 11-auditor;
- check-transplanted passes on those drafts and fails when an
artifact reference survives or a preserved marker is dropped;
- apply fails loudly when a required anchor is missing;
- the real pack in this tree (v0.2 + #244 line) still carries exactly
the six known templates/artifacts/ reference lines, so the
decision-B inventory cannot drift silently.
Fixture texts are built from the script's own fragment constants, so
the tests exercise the real redaction strings, not copies of them.
The real-branch runs (against #199 / #205 / #207 / #244 texts) are
recorded in the verification summary; they cannot run in CI because
those branches are not in this tree.
"""
import importlib.util
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[2]
SKILL_DIR = (REPO_ROOT / "cloud" / "skills" / "software-engineering")
SCRIPT = SKILL_DIR / "tools" / "check_transplant.py"
SKILL_MD = SKILL_DIR / "SKILL.md"
def _load_script():
spec = importlib.util.spec_from_file_location(
"check_transplant", str(SCRIPT))
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
CT = _load_script()
def _source_fixture():
m = CT
parts = [
"---\nname: software-engineering\n---\n",
"## 角色线程矩阵\n",
"| 角色 | 线程数 | 负责目录 | 产出 |",
"| QA / 测试 | 1 | `tests/` | 测试用例 |",
"| 独立审计 | 1 | 只写审计记录 | 审计结论 |",
"| 验收 / 发布 | 协调员兼任 | `docs/release/` | 验收清单 |",
"## 失败、续跑与成本\n",
"- 成本控制:分阶段本身就是成本控制。",
"## 独立审计与自动推回\n",
"本节是审计加固第 3 步的技能机制,与代码 `cloud/audit_pushback.py`",
"(推回决策纯函数)双轨落地,只写文档不算落地。只补审计机制,",
"不改上面已有规则的语义:G5 缺陷回路、合并拆分、G6 用户验收",
"的原意都不变。" + m.AUDIT_WARNING,
"### 独立审计角色:不写代码,只独立重跑、抽查证据、单独签字\n",
"- 审计线程不写业务代码、不改待办、不给合并决定、不执行合并。",
"### 自动推回:审计不过关,系统自动推回,不等人工点头\n",
"- 推回上限与升级:推回修复最多三轮" + m.PUSHBACK_LIMIT_PAREN + "。",
"- 与已有回路的关系:G5 缺陷回路复用为修复执行通道,推回派生的修复子任务",
" 就按 G5 方式执行,只补“自动触发 + 合并发布阻塞”这两块缺失;",
m.G7_FULLAUTO_BLOCK,
"## 推倒重做:超过三次后的升级分支(不是并行回路)\n",
"- 与上一节自动推回的关系再写一遍以免混淆:自动推回(1–3 轮修补)",
" 在前、推倒重做(超过 3 次)在后,是同一链路的前后两段;",
" 与 G5 缺陷回路的关系是复用修复执行通道(重做任务本身仍按",
" 阶段派生、仍走 G5 回归复核)" + m.REBUILD_TAIL,
"## 三条收紧:分流、保留资产、审计的审计\n",
"- 收紧一:阈值不能死板,第三次失败先判根因。",
m.TIGHTEN3_NOTIFY,
"## QA 全链路:测试环节跑通各个和所有业务链路\n",
"本节是“独立审计与 QA”部分的补充验收标准,与自动推回、推倒重做、",
"三条收紧并列,不覆盖已有规则。" + m.QA_INTRO,
"### 每个客户端交付前自测:三项缺一不算自测完\n",
"- 单元测试 + 对照契约的集成测试 + 手工冒烟清单,三项缺一不算自测完。",
"## 诚实边界:原生客户端\n",
"- 在只有 Linux 主机的环境里,真原生 iOS 需要 Xcode。",
"## 校验(协调员每过一个门禁自查一次)\n",
"- 分阶段走了吗?G3.5 没过就开了 G4b / G4c 吗?",
"- 审计与推回走了吗?审计签字人是否独立于执行人、协调员批准人、",
" QA 测试人?安全签字是不是无条件通过?审计 fail 后有没有自动推回",
" 并阻塞合并/发布?模块失败超过 3 次后有没有按推倒重做分支处理",
" (先判根因、只推倒该模块、旧实现归档、避坑清单带进新包),",
" 而不是无限修补或悄悄放行?",
"- QA 全链路走了吗?每端自测三项齐了吗?PRD 验收标准全有对应用例吗?",
" agent 专属六项齐了吗?跨端矩阵覆盖全部已选端了吗,未测端写明原因了吗?",
" 未编译端是不是只写了“代码评审 + Web 核心验证”?QA 矩阵签字与",
" 独立审计签字两个签字位都在且不是同一人吗?推回包里失败链路齐了吗,",
" 重审重跑失败链路了吗?",
"有一项为否,就停在当前门禁,不进入下一阶段。",
"## 附录:角色模板(播种随附版)\n",
"下面是 `templates/roles/` 的随附全文。",
"---",
"## 附录角色:08-shared-core(来源 templates/roles/08-shared-core.md)\n",
"# 角色模板:共享核心\n",
"- 不在共享逻辑里写某端专属代码。",
"---",
"## 附录角色:09-auditor(独立审计,新增)\n",
"# 角色模板:独立审计(1 个线程)\n",
"你是本项目的独立审计线程。",
]
return "\n".join(parts) + "\n"
def _target_fixture(target_id):
markers = " ".join(CT.MARKERS[target_id])
last = ("## 附录角色:10-iteration(来源 templates/roles/10-iteration.md,v0.5 新增)"
if target_id == "v05"
else "## 附录角色:08-shared-core(来源 templates/roles/08-shared-core.md)")
parts = [
"# software-engineering:多客户端软件工程编排\n",
"## 角色线程矩阵\n",
"| 角色 | 线程数 | 负责目录 | 产出 |",
"| QA / 测试 | 1 | `tests/` | 测试用例 |",
"| 验收 / 发布 | 协调员兼任 | `docs/release/` | 验收清单 |",
"## 失败、续跑与成本\n",
"- 成本控制:分阶段本身就是成本控制。",
"## 诚实边界:原生客户端\n",
"- 在只有 Linux 主机的环境里,真原生 iOS 需要 Xcode。",
"## 校验(协调员每过一个门禁自查一次)\n",
"- 分阶段走了吗?G3.5 没过就开了 G4b / G4c 吗?",
"- 标记行:" + markers,
"| G1 | PRD 冻结 | PRD 按 `templates/artifacts/prd-outline.md` 写完,用户故事有验收标准 | 回产品线程改 |",
"问卷模板在 `templates/artifacts/intake-questions.md`。最关键的六问:",
"- 验收清单模板在 `templates/artifacts/acceptance-checklist.md`。",
"按 `templates/artifacts/prd-outline.md` 写一份 PRD,至少包含:",
" 协议模板见 `templates/artifacts/` 下的 agent 任务契约模板。",
"1. 按 `templates/artifacts/acceptance-checklist.md` 生成本项目的验收清单,",
"有一项为否,就停在当前门禁,不进入下一阶段。",
"## 附录:角色模板(播种随附版)\n",
last + "\n",
"# 角色模板:共享核心\n",
"- 不在共享逻辑里写某端专属代码。",
]
return "\n".join(parts) + "\n"
def _run_script(testcase, args, tmp):
src = Path(tmp.name) / "source.md"
tgt = Path(tmp.name) / "target.md"
src.write_text(_source_fixture(), encoding="utf-8")
tgt.write_text(_target_fixture(args["target_id"]), encoding="utf-8")
cmd = [sys.executable, str(SCRIPT), args["cmd"],
"--source244", str(src), "--target", str(tgt),
"--target-id", args["target_id"]]
if args["cmd"] == "apply":
out = Path(tmp.name) / "draft.md"
cmd += ["--out", str(out)]
if args["cmd"] == "check-transplanted":
out = Path(tmp.name) / "draft.md"
cmd += ["--transplanted", str(out)]
result = subprocess.run(cmd, capture_output=True, text=True)
return result, src, tgt, Path(tmp.name) / "draft.md"
class TransplantPrepTestCase(unittest.TestCase):
def test_apply_v03_drops_g7_and_fullauto(self):
with tempfile.TemporaryDirectory() as tmp:
tmp = type("T", (), {"name": tmp})()
result, _s, _t, draft = _run_script(
self, {"cmd": "apply", "target_id": "v03"}, tmp)
self.assertEqual(result.returncode, 0, result.stderr)
text = draft.read_text(encoding="utf-8")
self.assertEqual(text.count("| 独立审计 |"), 1)
self.assertNotIn("G7 热修复回路并行", text)
self.assertNotIn("全自动 CI 自动修复", text)
self.assertIn("验收失败导致的推倒要先通知用户", text)
self.assertIn("## QA 全链路", text)
self.assertNotIn("移植警告", text)
self.assertNotIn("templates/artifacts", text)
self.assertEqual(
text.count("## 附录角色:09-auditor(来源 "
"templates/roles/09-auditor.md)"), 1)
check = subprocess.run(
[sys.executable, str(SCRIPT), "check-transplanted",
"--source244", str(_s), "--target", str(_t),
"--target-id", "v03", "--transplanted", str(draft)],
capture_output=True, text=True)
self.assertEqual(check.returncode, 0, check.stderr)
def test_apply_v04_drops_g7_and_notify_keeps_fullauto(self):
with tempfile.TemporaryDirectory() as tmp:
tmp = type("T", (), {"name": tmp})()
result, _s, _t, draft = _run_script(
self, {"cmd": "apply", "target_id": "v04"}, tmp)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn("STOPPED", result.stderr)
text = draft.read_text(encoding="utf-8")
self.assertNotIn("G7 热修复回路并行", text)
self.assertNotIn("验收失败导致的推倒要先通知用户", text)
self.assertIn("与全自动 CI 自动修复限", text)
self.assertIn("## QA 全链路", text)
self.assertNotIn("templates/artifacts", text)
check = subprocess.run(
[sys.executable, str(SCRIPT), "check-transplanted",
"--source244", str(_s), "--target", str(_t),
"--target-id", "v04", "--transplanted", str(draft)],
capture_output=True, text=True)
self.assertEqual(check.returncode, 0, check.stderr)
def test_apply_v05_drops_qa_and_notify_renumbers_11(self):
with tempfile.TemporaryDirectory() as tmp:
tmp = type("T", (), {"name": tmp})()
result, _s, _t, draft = _run_script(
self, {"cmd": "apply", "target_id": "v05"}, tmp)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn("STOPPED", result.stderr)
text = draft.read_text(encoding="utf-8")
self.assertNotIn("## QA 全链路", text)
self.assertNotIn("- QA 全链路走了吗?", text)
self.assertNotIn("验收失败导致的推倒要先通知用户", text)
self.assertEqual(
text.count("## 附录角色:11-auditor(来源 "
"templates/roles/11-auditor.md)"), 1)
self.assertNotIn("09-auditor", text)
self.assertNotIn("templates/artifacts", text)
check = subprocess.run(
[sys.executable, str(SCRIPT), "check-transplanted",
"--source244", str(_s), "--target", str(_t),
"--target-id", "v05", "--transplanted", str(draft)],
capture_output=True, text=True)
self.assertEqual(check.returncode, 0, check.stderr)
def test_apply_missing_anchor_fails(self):
with tempfile.TemporaryDirectory() as tmp:
src = Path(tmp) / "source.md"
tgt = Path(tmp) / "target.md"
src.write_text(_source_fixture(), encoding="utf-8")
tgt.write_text(_target_fixture("v03").replace(
"| QA / 测试 |", "| QA |"), encoding="utf-8")
result = subprocess.run(
[sys.executable, str(SCRIPT), "apply",
"--source244", str(src), "--target", str(tgt),
"--target-id", "v03",
"--out", str(Path(tmp) / "draft.md")],
capture_output=True, text=True)
self.assertNotEqual(result.returncode, 0)
def test_check_rejects_surviving_artifact_ref(self):
with tempfile.TemporaryDirectory() as tmp:
tmp_o = type("T", (), {"name": tmp})()
result, src, tgt, draft = _run_script(
self, {"cmd": "apply", "target_id": "v03"}, tmp_o)
self.assertEqual(result.returncode, 0, result.stderr)
text = draft.read_text(encoding="utf-8")
draft.write_text(text + "\n见 `templates/artifacts/x.md`。\n",
encoding="utf-8")
check = subprocess.run(
[sys.executable, str(SCRIPT), "check-transplanted",
"--source244", str(src), "--target", str(tgt),
"--target-id", "v03", "--transplanted", str(draft)],
capture_output=True, text=True)
self.assertNotEqual(check.returncode, 0)
self.assertIn("templates/artifacts", check.stderr)
def test_check_rejects_dropped_marker(self):
with tempfile.TemporaryDirectory() as tmp:
tmp_o = type("T", (), {"name": tmp})()
result, src, tgt, draft = _run_script(
self, {"cmd": "apply", "target_id": "v03"}, tmp_o)
self.assertEqual(result.returncode, 0, result.stderr)
lines = draft.read_text(encoding="utf-8").split("\n")
kept = [l for l in lines if not l.startswith("- 标记行:")]
self.assertLess(len(kept), len(lines))
draft.write_text("\n".join(kept), encoding="utf-8")
check = subprocess.run(
[sys.executable, str(SCRIPT), "check-transplanted",
"--source244", str(src), "--target", str(tgt),
"--target-id", "v03", "--transplanted", str(draft)],
capture_output=True, text=True)
self.assertNotEqual(check.returncode, 0)
def test_real_pack_artifact_inventory_is_six_lines(self):
text = SKILL_MD.read_text(encoding="utf-8")
refs = [l for l in text.split("\n") if "templates/artifacts" in l]
self.assertEqual(len(refs), 6, refs)
self.assertIn("## 独立审计与自动推回", text)
self.assertIn("## 附录角色:09-auditor(来源 "
"templates/roles/09-auditor.md)", text)
if __name__ == "__main__":
unittest.main()