From c4f919970196f5a548494289746a4295f91495c8 Mon Sep 17 00:00:00 2001 From: host Date: Sat, 31 Oct 2026 07:06:55 +0000 Subject: [PATCH] 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. --- .../skills/software-engineering/CHANGELOG.md | 37 ++ .../TRANSPLANT-PREP-244.md | 362 ++++++++++++ .../tools/check_transplant.py | 532 ++++++++++++++++++ cloud/tests/test_skill_transplant_prep.py | 291 ++++++++++ 4 files changed, 1222 insertions(+) create mode 100644 cloud/skills/software-engineering/TRANSPLANT-PREP-244.md create mode 100644 cloud/skills/software-engineering/tools/check_transplant.py create mode 100644 cloud/tests/test_skill_transplant_prep.py diff --git a/cloud/skills/software-engineering/CHANGELOG.md b/cloud/skills/software-engineering/CHANGELOG.md index ea44486..e756285 100644 --- a/cloud/skills/software-engineering/CHANGELOG.md +++ b/cloud/skills/software-engineering/CHANGELOG.md @@ -83,3 +83,40 @@ v0.1 → v0.2 的 5 个 P0 缺口明细无法从本文件追溯,本文件不 `templates/roles/11-auditor.md`(正文与 #258 的 09-auditor 同源); 校验表同步扩为 12 章。§4 的 templates/artifacts 删引用决策 未在集成基底执行(仍待用户拍板),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 白名单与之无关, + 不得混淆(见准备文档)。 diff --git a/cloud/skills/software-engineering/TRANSPLANT-PREP-244.md b/cloud/skills/software-engineering/TRANSPLANT-PREP-244.md new file mode 100644 index 0000000..89af3e9 --- /dev/null +++ b/cloud/skills/software-engineering/TRANSPLANT-PREP-244.md @@ -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` | `## 附录角色:(来源 templates/roles/.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 --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 同步顺序。 diff --git a/cloud/skills/software-engineering/tools/check_transplant.py b/cloud/skills/software-engineering/tools/check_transplant.py new file mode 100644 index 0000000..3b22de6 --- /dev/null +++ b/cloud/skills/software-engineering/tools/check_transplant.py @@ -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()) diff --git a/cloud/tests/test_skill_transplant_prep.py b/cloud/tests/test_skill_transplant_prep.py new file mode 100644 index 0000000..28f3816 --- /dev/null +++ b/cloud/tests/test_skill_transplant_prep.py @@ -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()