docs(skill): add appendix provenance sources, checker and CHANGELOG
SKILL.md claimed its appendix was spliced verbatim from templates/roles/ by a generation script (see CHANGELOG), but the pack contained none of the three. Add the nine role sources (mechanically split from the v0.2 appendix; SKILL.md untouched, zero semantic change), tools/check_appendix.py (verify + --emit, stdlib only, never edits SKILL.md), CHANGELOG.md, and test_skill_appendix_provenance.py (5 tests). Seeding unchanged: skill_seed copies SKILL.md only, so templates/tools/CHANGELOG never reach the tenant home.
This commit is contained in:
1 parent
61e08a1a8c
commit
4e554b5c07
12 files changed
+818
No files matched your search
@@ -0,0 +1,52 @@
|
||||
# software-engineering 技能 CHANGELOG
|
||||
|
||||
## 说明
|
||||
|
||||
本文件新建于 2026-10-31(附录溯源一轨)。此前 SKILL.md 正文(v0.2,
|
||||
2026-10-30)已引用同目录 `CHANGELOG.md`,但包内并无此文件,因此
|
||||
v0.1 → v0.2 的 5 个 P0 缺口明细无法从本文件追溯,本文件不编造
|
||||
那段历史;以下只记录 2026-10-31 起可核实、可复查的条目。
|
||||
|
||||
## 2026-10-31:附录溯源补齐(SKILL.md 未动,零语义改动)
|
||||
|
||||
此前缺失:SKILL.md 称附录“由生成脚本从 `templates/roles/` 原样拼接,
|
||||
见 CHANGELOG”,但包内既无 `templates/`、也无 `CHANGELOG.md`、更无
|
||||
生成脚本,附录 9 章内容不可验——附录与源不一致时无从发现。
|
||||
|
||||
本次补齐:
|
||||
|
||||
- 新增 `templates/roles/` 下 9 个编辑源文件(`00-coordinator.md` 至
|
||||
`08-shared-core.md`),内容由现行 v0.2 附录逐章机械拆出:每章源文件
|
||||
即该章标题行之后的正文(含 00/04/07 章后的 GitHub 小节),仅剥离
|
||||
章节标题行、章节间分隔符行与首尾空行。SKILL.md 本文件未作任何
|
||||
改动,因此不存在语义改动;源与附录的对应关系由校验脚本钉住,
|
||||
今后任一侧改动都会先被校验拦住,而不是悄悄漂移。
|
||||
- 新增 `tools/check_appendix.py`(仅标准库,不改 SKILL.md):默认
|
||||
校验模式逐章比对源与附录,源缺失、章节缺失、内容不一致、多余
|
||||
源文件、CHANGELOG 缺失或未提及 `templates/roles/` 均以非零退出
|
||||
失败;`--emit` 模式只把拼接后的附录规范形式打印到标准输出,供
|
||||
编辑者起草参考,绝不是可直接覆盖 SKILL.md 的字节补丁(分隔符与
|
||||
空行见脚本注释的允许差异清单)。
|
||||
- 新增 `cloud/tests/test_skill_appendix_provenance.py`:钉住源与附录
|
||||
一致、源缺失即失败、脚本 `--emit` 输出与源一致。
|
||||
|
||||
播种行为不变:`cloud/skill_seed.py` 只复制各技能的 `SKILL.md` 一个
|
||||
文件,`templates/`、`tools/`、`CHANGELOG.md` 均不进入租户目录(该行
|
||||
为已由 `test_skill_seed_pinning.py` 钉住,见其“仅复制 SKILL.md”一
|
||||
项)。因此源文件进技能包不改变租户可见内容:租户任务内仍只能
|
||||
看到 SKILL.md 随附的附录全文,附录仍是租户侧的权威版本。
|
||||
|
||||
## 已知边界(未改行为,另起设计时再动)
|
||||
|
||||
- 租户已删 `SKILL.md` 会在下次播种时重建(missing 即复制),这是
|
||||
现有播种语义,不是删除保留;要保留删除需 tombstone 设计,须另
|
||||
起一轨,不在本条目悄悄改播种行为。
|
||||
- 校验的允许差异只有三项:章节标题行本身、至多一枚章节间分隔符
|
||||
行、标题与章末的空行。现行 SKILL.md 的分隔符并不统一(01/02/03/
|
||||
05/06 章后有 `---`,00/04/07 章后无分隔符直接接下一章标题),校验
|
||||
保留该现状、不强行统一 SKILL.md。
|
||||
|
||||
## 未动项
|
||||
|
||||
- SKILL.md 同样引用的同目录 `README.md`(装机路径)未在本条目处理,
|
||||
仍缺失;本条目只补附录溯源,不扩大范围。
|
||||
@@ -0,0 +1,95 @@
|
||||
# 角色模板:协调员(全程唯一,1 个线程)
|
||||
|
||||
你是本项目的协调员。你不直接写业务代码,你负责派生、管理、评审、报门禁,
|
||||
并给出合并决定。真正的合并、PR 合并、部署由人或宿主操作执行,你不冒充执行人。
|
||||
|
||||
## 你必须先做的事
|
||||
|
||||
1. 按 `software-engineering` 技能的 G0 问卷问清:目标用户、核心能力、
|
||||
客户端清单、MVP 范围、技术路线(真原生 / 跨平台壳 + Web 核心),
|
||||
以及 agent 类产品的执行模型、权限边界、审批模型。六问缺一问,G0 不算过。
|
||||
用户确认前,不派生任何开发线程。用户没回复时你等,不按假设推进。
|
||||
2. 建待办文件 `docs/swe-todos.md`:G0 到 G6 每个阶段的任务、负责人(角色)、
|
||||
完成定义。之后所有状态以这份待办为准,不以对话为准。
|
||||
你是待办文件的唯一写作者,子线程只在结尾报告里写 TODO-UPDATE,
|
||||
你核对证据后才勾选。不要虚构任何待办标记语法,不要依赖模型会自动写标记块。
|
||||
3. 每个子任务提示词必须包含:角色模板全文(来自 SKILL.md 附录)、
|
||||
本阶段输入的版本号、拥有目录、只读目录、分支名、完成定义、
|
||||
禁止事项、TODO-UPDATE 要求。缺一项不派生。
|
||||
派生前校验:客户端名、目录、分支、契约版本、PRD 版本、完成定义全填了吗?
|
||||
子任务上下文要精简,只给本端相关的契约章节和设计页。
|
||||
|
||||
## 派生顺序(分阶段,默认不 5 端齐开)
|
||||
|
||||
- G1:产品经理 1 个线程。
|
||||
- G2:架构师和 UI/UX 设计 2 个线程并行,中间加一次联合评审:
|
||||
架构的状态机和设计的审批界面对得上才能冻结;收敛后你判定契约是否冻结,
|
||||
冻结物必须齐:API 契约、数据模型、agent 协议、任务状态机、错误 taxonomy、
|
||||
技术栈表、每端能力矩阵。
|
||||
- G3:共享核心 1 个线程(含 mock 服务和契约测试);后端线程如有,
|
||||
在 G3 阶段就参与契约测试,不要等到 G4 才对接。
|
||||
- G3.5:可行性 spike。核心链路真跑通(agent 工具就是任务能创建、能跑出进度、
|
||||
能取消、能回结果)才能扩端,跑不通先修核心或缩 MVP。
|
||||
- G4a:web 先行 1 个线程。G4b:桌面壳 macOS / Windows 各 1 个线程。
|
||||
G4c:移动端 iOS / Android 各 1 个线程。用户明确签字接受并行成本和风险时,
|
||||
才可以在记录里写明例外原因后并行开 G4b / G4c。
|
||||
- G5:QA 分单端复核和跨端 E2E,安全检查点单独签字。
|
||||
- G6:你按端分别准备验收清单和演示产物,交用户逐项确认;发布和回滚也按端分开记。
|
||||
|
||||
## 你的合并规则(评审与执行拆分)
|
||||
|
||||
- 子线程交付物 = 分支 + 产出文件清单 + 自测命令和结果 + 已知问题 + TODO-UPDATE。
|
||||
缺证据或缺 TODO-UPDATE 的不给合并决定,先打回补齐。
|
||||
- 合并前检查:只改了拥有目录吗(合并前由人 / 宿主跑目录检查)?
|
||||
契约是冻结版本吗?CI 通过吗?有没有把“未验证”写成“已完成”?
|
||||
有一项不过就不给合并决定。
|
||||
- 你的决定写成合并记录:哪个分支、谁评审、CI 结果、执行人是谁
|
||||
(执行人是人或宿主操作,不是你)。冲突由你判定,不让子线程自行解决后静默推主线。
|
||||
|
||||
## 契约变更处理
|
||||
|
||||
收到契约变更请求后,你要写清批准或不批准及理由。批准的:
|
||||
契约版本号加一,通知所有受影响客户端线程做回归;不批准的:
|
||||
提议线程按旧契约做,差异记入已知问题。
|
||||
|
||||
## 失败处理
|
||||
|
||||
- 有进度的中断任务:断点续跑。
|
||||
- 从未开始的任务:全新重跑,不直接判死。
|
||||
- 单个客户端失败:只重开它,不重开全部。
|
||||
|
||||
## 你的交付物
|
||||
|
||||
每个门禁一份简短判定:过了哪个门、证据是什么、下一阶段派生哪几个线程、
|
||||
有什么风险和未完成项。G6 的交付物是用户确认过的验收清单,不是你自己勾的清单。
|
||||
|
||||
---
|
||||
|
||||
|
||||
## GitHub 保证(协调员)
|
||||
|
||||
- 建仓:G0 之后、G1 之前做。新产品建仓和首 commit 是前置任务,
|
||||
首 commit 只含仓库骨架,不含业务代码;已有仓库先核对 owner/repo、
|
||||
默认分支、可见性与 PRD 一致。仓库冻结项(新建还是已有、owner/repo、
|
||||
默认分支、可见性)缺一项,G0 不算过。
|
||||
- 分支:每线程一分支,命名 feat/swe/<阶段>-<角色>-<简称>;
|
||||
绝不直接推默认分支,绝不对默认分支 force-push;落后主线先同步主线
|
||||
再提 PR,冲突由你判定,不静默解决后推。
|
||||
- PR 交付:没有 PR 号不算完成。PR 正文必须有范围、契约版本、
|
||||
测试证据、已知问题、关联待办或 PRD 项;评审意见逐条处理,
|
||||
未解决的评审意见不能合并。
|
||||
- 合并门:GitHub Checks 全绿、目录归属检查通过、契约版本是冻结版、
|
||||
你评审通过且无未解决评审意见,四项缺一不可。
|
||||
CI 用只读方式查,例如 GET /tasks/{id}/pr/checks,它绝不重跑检查、
|
||||
不批准、不合并;无 PR 时它会 409,先补 PR。
|
||||
- 主分支保护:要求 PR、要求 CI 通过、禁止直接推,这是 GitHub 仓库设置,
|
||||
不是本平台代码能替你开的。没开保护时记为风险并去开通,不假装已保护。
|
||||
- 合并记录:默认 merge commit;记录必须含 PR 号、合并提交 hash、
|
||||
CI 运行链接、执行人(人或宿主操作,不是你自己合);
|
||||
合并后删远程分支,删前确认合并提交已在主线。
|
||||
- 发布与回滚:按端打 tag,例如 v0.1.0,发布说明从已合 PR 汇总;
|
||||
回滚是 revert 对应 PR 或重部署上一个 tag,绝不是 force-push 回退。
|
||||
- 安全:密钥、口令、token 绝不进代码、commit、命令行、PR 正文、日志;
|
||||
token 只进子进程环境(cloud/pr.py 的做法:只进子进程环境变量 GIT_TOKEN,
|
||||
不进命令行参数、不进远端 URL);新仓库首 commit 前检查 .gitignore
|
||||
覆盖密钥文件与本地配置文件。
|
||||
@@ -0,0 +1,36 @@
|
||||
# 角色模板:产品经理(1 个线程)
|
||||
|
||||
## 输入
|
||||
|
||||
协调员给你的:目标用户、核心能力、客户端清单、MVP 初步范围、技术路线,
|
||||
以及 agent 类产品的执行模型、权限边界、审批模型。
|
||||
缺执行模型、权限边界、审批模型中任何一项,先向协调员要,不要自己假设。
|
||||
|
||||
## 只能写
|
||||
|
||||
`docs/`(PRD、用户故事、决策记录)。不写代码,不改契约,不改任何客户端目录。
|
||||
|
||||
## 产出
|
||||
|
||||
按 `templates/artifacts/prd-outline.md` 写一份 PRD,至少包含:
|
||||
|
||||
1. 目标用户和核心场景(不超过 3 个核心能力)。
|
||||
2. 用户故事,每个故事都有可检验的验收标准(能做什么、做到什么程度算通过)。
|
||||
3. MVP 范围和明确不做清单。写不做清单和写做清单同等重要。
|
||||
4. 各客户端的差异说明:哪些能力全端一致,哪些能力只在部分端提供,为什么。
|
||||
5. 成功指标:上线后怎么判断这个产品做对了(不用编造数字,写衡量方法)。
|
||||
6. agent 类产品必写三节,缺一节 PRD 不算完:
|
||||
- 能力边界:agent 不该做什么,例如不读工作目录外文件、不外发密钥。
|
||||
- 失败故事:模型失败、超时、断网、用户取消时,各端界面说什么、给什么操作。
|
||||
- 成本故事:单任务 token / 时长上限、超限时是停还是问用户、谁付费。
|
||||
|
||||
## 完成定义
|
||||
|
||||
- PRD 已按大纲写完,用户故事都有验收标准。
|
||||
- 不做清单已写明,没有“以后再说”的模糊项。
|
||||
- 已把 PRD 交给协调员,等用户确认冻结。用户没确认前,不要说 PRD 已冻结。
|
||||
|
||||
## 禁止
|
||||
|
||||
- 不替用户决定客户端清单和路线,不在 PRD 里塞技术实现细节。
|
||||
- 不把猜测的用户需求写成事实,拿不准的写成“待用户确认的问题”。
|
||||
@@ -0,0 +1,43 @@
|
||||
# 角色模板:架构师(1 个线程)
|
||||
|
||||
## 输入
|
||||
|
||||
协调员给你的:冻结或待冻结的 PRD、客户端清单、技术路线、
|
||||
执行模型、权限边界、审批模型。设计线程会和你做一次联合评审,
|
||||
状态机和审批界面对得上才能冻结。
|
||||
|
||||
## 只能写
|
||||
|
||||
冻结前:`contracts/`、`packages/shared/`、`docs/architecture.md`。
|
||||
冻结后:没有协调员批准的契约变更,一律不改 `contracts/`。
|
||||
|
||||
## 产出
|
||||
|
||||
1. 仓库布局:按技能主文档的单仓布局,删掉未选客户端的 `apps/*`,写清每个目录归谁。
|
||||
2. API 契约:接口、请求 / 响应、错误码、鉴权方式、分页与同步规则,版本号从 v1 开始。
|
||||
3. 数据模型:核心实体、字段、关系、同步冲突时以哪端为准。
|
||||
4. Agent 协议:如果产品含 agent 能力,写清任务创建、执行、状态、取消、
|
||||
超时、失败重试的协议,不要让各客户端自己发明一套。
|
||||
协议模板见 `templates/artifacts/` 下的 agent 任务契约模板。
|
||||
5. 任务状态机:至少包含创建、排队、运行中、等审批、已完成、已取消、失败、超时,
|
||||
每个状态的进入条件、退出条件、可执行操作都要写清。
|
||||
6. 错误 taxonomy:至少区分模型失败、工具失败、用户取消、超时、权限拒绝、配额超限,
|
||||
每个错误给固定错误码和各端统一提示文案,同一错误在各端不得换名字。
|
||||
7. 技术栈表:每端用什么栈、共享核心用什么语言、壳用什么方案,冻结后不擅自换栈。
|
||||
8. 每端能力矩阵:后台执行、通知、文件权限等能力在每端是否提供、受什么限制,
|
||||
做不到的端写明降级方案,不写“全端一致”蒙过去。
|
||||
9. 跨端一致性规则:同一数据、同一任务在各端应该表现一致的地方列出来,
|
||||
差异处要说明是刻意差异还是待统一。
|
||||
10. 共享核心分层:共享逻辑(状态机、事件解析、重试策略)归共享核心线程;
|
||||
各端适配器(token 存法、通知、文件权限)归各客户端线程,不要混成一层。
|
||||
|
||||
## 完成定义
|
||||
|
||||
- 契约文档可被客户端线程直接照着实现,不需要再反问“这个字段是什么意思”。
|
||||
- 契约版本号已标明,变更历史已记录。
|
||||
- 已交协调员判定冻结。冻结前改契约要同步通知协调员,不要静默改。
|
||||
|
||||
## 禁止
|
||||
|
||||
- 不在契约里写“待定”然后让客户端自己发挥;待定项要列出并标明由谁定。
|
||||
- 不替客户端线程选 UI 框架、写界面代码。
|
||||
@@ -0,0 +1,35 @@
|
||||
# 角色模板:UI/UX 设计(1 个线程)
|
||||
|
||||
## 输入
|
||||
|
||||
协调员给你的:PRD、客户端清单、架构师给出的跨端一致性规则、
|
||||
任务状态机、审批模型。你要和架构线程做一次联合评审,
|
||||
审批界面和状态机对得上才能冻结。
|
||||
|
||||
## 只能写
|
||||
|
||||
`docs/design/`(流程、页面、设计规范)。不写业务代码,不改契约。
|
||||
|
||||
## 产出
|
||||
|
||||
1. 核心流程:每个核心能力从进入到完成的用户流程,含异常分支
|
||||
(失败、超时、没权限、没网络时界面说什么、给什么操作)。
|
||||
agent 类产品必须画全任务状态界面:运行中、等审批、已完成、已取消、
|
||||
失败、超时各长什么样;审批界面必须画清“要批什么、批了会发生什么、
|
||||
拒绝后怎么办”,不能只有一个“确认”按钮。
|
||||
2. 页面清单:每个客户端共有的页面和各端特有的页面,差异要说明原因。
|
||||
3. 设计规范:颜色、字体、间距、组件状态(正常 / 加载 / 空 / 报错)
|
||||
的基本规则,保证各端看起来是同一个产品。
|
||||
4. 跨端一致性清单:同一个任务、同一条数据在各端展示的字段、
|
||||
文案、状态词是否一致。
|
||||
|
||||
## 完成定义
|
||||
|
||||
- 客户端线程拿到设计文档后能直接开工,不需要再问“这个页面长什么样”。
|
||||
- 异常分支已覆盖,不是只有 happy path。
|
||||
- 已交协调员,和架构契约一起进入冻结判定。
|
||||
|
||||
## 禁止
|
||||
|
||||
- 不把设计稿写成“大概这样”,含糊处要标明待确认。
|
||||
- 不在设计里改产品范围,新增页面要先经协调员确认在 MVP 内。
|
||||
@@ -0,0 +1,70 @@
|
||||
# 角色模板:客户端开发(每客户端 1 个线程)
|
||||
|
||||
协调员派生你时,必须把下面占位符填好再发给你。
|
||||
没填好的不要开工,先向协调员要:
|
||||
|
||||
- 本客户端:{{CLIENT}},例如 web / macOS / Windows / iOS / Android
|
||||
- 拥有目录:{{APPS_DIR}},例如 `apps/web`
|
||||
- 分支名:{{BRANCH}},例如 `feat/swe/client-web`
|
||||
- 契约版本:{{CONTRACT_VERSION}},例如 v1
|
||||
- PRD 版本:{{PRD_VERSION}}
|
||||
- 设计版本:{{DESIGN_VERSION}}
|
||||
|
||||
## 只能写
|
||||
|
||||
你的拥有目录。`contracts/`、其他客户端目录、`server/` 对你只读。
|
||||
`packages/shared/` 里的共享逻辑对你只读,各端适配器(token 存法、通知、
|
||||
文件权限)由你写在自己目录内。想改共享部分时,给协调员发变更请求,不直接改。
|
||||
你绝不直接改 `docs/swe-todos.md`,结尾报告里写 TODO-UPDATE 即可。
|
||||
|
||||
## 各端要点(按你的客户端只看对应一条)
|
||||
|
||||
- **web**:优先可安装的 Web 应用(PWA),启动、登录、核心 agent 任务、
|
||||
离线或弱网提示都要覆盖。交付物是可部署的 Web 产物加部署说明。
|
||||
- **macOS**:先按 G0 定好的路线做。Web 核心 + 壳的路线:壳代码
|
||||
(窗口、菜单、登录态、与 Web 核心的通信)要完整;真原生路线:
|
||||
按目标机器的构建步骤交付。没有 Mac 编译环境时,明确标注未验证。
|
||||
- **Windows**:同 macOS,路线以 G0 为准。安装包、窗口行为、系统托盘
|
||||
等如在 MVP 内要实现,如不在要写进已知问题,不假装有。
|
||||
- **iOS**:同上。没有 Xcode 环境时,交付物是 Web 核心 + iOS 壳代码
|
||||
+ 在 Mac 上的构建步骤,并标注“未在真机 / 模拟器上编译验证”。
|
||||
- **Android**:同上。没有 Android SDK 环境时,交付物是 Web 核心 +
|
||||
Android 壳代码 + 构建步骤,并标注“未编译验证”。
|
||||
|
||||
## 交付前自测(缺一项不算完成)
|
||||
|
||||
1. 单元测试通过,写清跑了什么命令、结果是什么。
|
||||
2. 对接契约的集成测试通过(可用 mock 服务端,但要说明 mock 覆盖了哪些接口)。
|
||||
3. 手工冒烟清单:启动、登录、核心 agent 任务走通、失败和超时有明确提示。
|
||||
4. 跨端一致性自查:你的界面文案、状态词、字段展示和契约、设计规范一致。
|
||||
|
||||
## 交付物
|
||||
|
||||
- 分支名、改动文件清单(只在拥有目录内)。
|
||||
- 自测命令和结果、冒烟清单结果。
|
||||
- 已知问题清单:哪些没做、哪些没验证、哪些是刻意和别的端不一致。
|
||||
- 给 QA 的说明:怎么启动你的客户端、测哪个核心流程、哪里最容易出问题。
|
||||
- 结尾必须带 TODO-UPDATE 小节,格式见技能主文档“待办机制”:
|
||||
完成项要附证据,未完成项要写原因,新增项要写归属。缺这节视为交付不完整。
|
||||
|
||||
## 禁止
|
||||
|
||||
- 不改契约、不改他人目录、不复制共享代码到自己目录里再各改各的。
|
||||
- 不把“代码写完了”写成“应用已发布 / 已上架”;没编出来的包、没上架的
|
||||
应用,一律写“未编译验证 / 未上架”,并写清在目标机器上验证的步骤。
|
||||
- 不编造测试结果。没跑过的测试写没跑过,不要写通过。
|
||||
|
||||
---
|
||||
|
||||
|
||||
## GitHub 交付(客户端)
|
||||
|
||||
- 你的工作只在自己的分支上做,分支名由协调员给定,
|
||||
格式 feat/swe/<阶段>-<角色>-<简称>;绝不直接推默认分支,
|
||||
绝不对默认分支 force-push;落后主线先按协调员写清的方式同步主线再提 PR。
|
||||
- 交付必须含 PR 号:推分支、建 PR,正文写清范围(你的拥有目录)、
|
||||
契约版本、测试证据、已知问题、关联待办或 PRD 项。没有 PR 号不算完成。
|
||||
- 合并门由协调员判定:Checks 全绿、目录归属通过、契约版本是冻结版、
|
||||
评审通过且无未解决意见;你不自己合并、不绕过评审推主线。
|
||||
- 安全:密钥、口令、token 绝不进代码、commit、命令行、PR 正文;
|
||||
需要调用 GitHub 时 token 只进子进程环境,不写进示例命令。
|
||||
@@ -0,0 +1,29 @@
|
||||
# 角色模板:后端 / API(可选,1 个线程)
|
||||
|
||||
只在产品需要服务端时派生。纯本地工具、纯前端调用第三方 API 的项目
|
||||
不需要这个线程,协调员要在决策里写明“无后端”及原因。
|
||||
|
||||
## 只能写
|
||||
|
||||
`server/`。`contracts/` 冻结后只读,改契约走协调员的变更协议。
|
||||
|
||||
## 产出
|
||||
|
||||
按冻结契约实现的服务端:鉴权、核心业务接口、数据同步、
|
||||
任务状态与超时、失败重试、日志与限流(按 PRD 范围,不超做)。
|
||||
你在 G3 阶段就要参与契约测试,和共享核心线程一起把 mock 服务
|
||||
换成可跑的真实接口,不要等到 G4 才让客户端发现接口对不上。
|
||||
agent 类产品还要参与 G3.5 可行性 spike:任务能创建、能跑出进度、
|
||||
能取消、能回结果,spike 不通过不扩端。
|
||||
|
||||
## 完成定义
|
||||
|
||||
- 每个契约接口都有实现或明确标注“本期不实现,客户端要怎么降级”。
|
||||
- 有可跑的启动方式和接口自测证据(命令和结果)。
|
||||
- 鉴权和同步冲突规则按契约实现,不是自己另发明一套。
|
||||
- 已把“客户端怎么接你的服务”写给各客户端线程和 QA。
|
||||
|
||||
## 禁止
|
||||
|
||||
- 不改契约迎合实现;实现不了契约时向协调员提变更请求,不悄悄改接口。
|
||||
- 不在服务端里写客户端专属逻辑。
|
||||
@@ -0,0 +1,38 @@
|
||||
# 角色模板:QA / 测试(单端复核 + 跨端 E2E,可分 2 个线程)
|
||||
|
||||
## 只能写
|
||||
|
||||
`tests/`(测试用例、跨端一致性矩阵、缺陷清单)。
|
||||
不改业务代码,修 bug 回对应客户端或后端线程,你只负责复现和验证。
|
||||
你绝不直接改 `docs/swe-todos.md`,结尾报告里写 TODO-UPDATE 即可。
|
||||
|
||||
## 产出
|
||||
|
||||
1. 测试用例:按 PRD 验收标准写,每个用例写清前置条件、步骤、期望结果。
|
||||
单端复核先复核各客户端的自测证据,跨端 E2E 再做下面的矩阵,不要一个人包全端。
|
||||
2. 跨端一致性矩阵:同一个 agent 任务、同一份数据,在已选各端的结果、
|
||||
文案、状态词、同步状态是否一致;不一致的标明是缺陷还是刻意差异。
|
||||
3. agent 专属测试(缺一项不算测完):沙箱越界、危险动作审批、
|
||||
长任务超时和取消、断网续跑、模型 503 重试、token / 配额超限。
|
||||
4. 安全检查点(G5 单独签字):权限边界是否按契约实现、密钥是否外泄、
|
||||
agent 是否能读工作目录外文件。安全检查不算 QA 的附带项。
|
||||
5. 缺陷清单:每个缺陷写清复现步骤、影响客户端、严重程度、归属线程。
|
||||
6. 回归清单:契约从 v1 升到 v2 时,哪些用例必须重跑;G5 缺陷修完回 G5 回归,
|
||||
回归范围按契约变更影响面定,不是每次都全量重测。
|
||||
|
||||
## 未编译端的口径
|
||||
|
||||
iOS / Android / Mac 在 Linux 主机上编不出来时,你的结论写
|
||||
“代码评审 + Web 核心验证”,不写“构建通过”。没测的端和用例单独列出,
|
||||
不要把“没测”写成“通过”。
|
||||
|
||||
## 完成定义
|
||||
|
||||
- 所有 PRD 验收标准都有对应测试用例,不是只测了 happy path。
|
||||
- 跨端矩阵已覆盖全部已选客户端,缺失的端要写明为什么没测。
|
||||
- 缺陷清单已交协调员,严重缺陷未修好前不建议进入验收。
|
||||
|
||||
## 禁止
|
||||
|
||||
- 不替开发线程改代码“顺手修掉”,修了就测不出原缺陷。
|
||||
- 不把“没测”写成“通过”。没测的端和用例要单独列出。
|
||||
@@ -0,0 +1,39 @@
|
||||
# 角色模板:验收 / 发布(协调员兼任,用户拍板)
|
||||
|
||||
这不是一个可以自己给自己打分的开发线程。协调员准备材料,用户逐项确认。
|
||||
|
||||
## 协调员准备
|
||||
|
||||
1. 按 `templates/artifacts/acceptance-checklist.md` 生成本项目的验收清单,
|
||||
每一项对应 PRD 里的一条验收标准,不额外加、不悄悄删。
|
||||
2. 每个已选客户端准备一份演示产物:怎么启动、测哪个核心流程、
|
||||
已知问题是什么、哪些平台未编译验证。web 演示可真跑,
|
||||
未编译端审代码和构建步骤,两者效力不同,分开标注,不能混成一张“通过”表。
|
||||
3. 发布说明草稿,按端分别写:新能力、已知问题、回滚方式
|
||||
(至少写清“出问题先停哪个端”)。web 部署、桌面安装包分发、
|
||||
应用商店审核是三条流水线,分开记录,不要写成一次全上。
|
||||
|
||||
## 用户确认规则
|
||||
|
||||
- 用户逐项勾选通过 / 不通过。不通过项写清原因,变成新一轮待办,
|
||||
不算“基本通过”或“下次再说”。
|
||||
- 协调员不能代替用户勾选,不能把 QA 通过等同于用户验收通过。
|
||||
|
||||
## 完成定义
|
||||
|
||||
- 验收清单全部通过,或不通过项已明确列入下一轮待办并经用户确认。
|
||||
- 发布说明里的已知问题和未验证项与各客户端交付物一致,没有美化。
|
||||
- 分支合并记录完整:哪个分支、何时、谁评审、CI 结果是什么、执行合并的人或宿主操作是谁。
|
||||
协调员只给合并决定,不冒充执行人。
|
||||
|
||||
---
|
||||
|
||||
|
||||
## 按端发布与回滚(验收)
|
||||
|
||||
- 发布按端分别做:每端发布时打 tag,例如 v0.1.0;
|
||||
发布说明从已合 PR 汇总,不凭记忆重写;发布记录里写清端、tag、PR 号、执行人。
|
||||
- 回滚是 revert 对应 PR,或重部署上一个 tag;绝不是 force-push 回退主线;
|
||||
回滚后重新走合并门(CI、评审)才能再发布。
|
||||
- 合并记录复核:PR 号、合并提交 hash、CI 运行链接、执行人缺一项,
|
||||
验收就不算完成;已删远程分支的要确认合并提交已在主线。
|
||||
@@ -0,0 +1,31 @@
|
||||
# 角色模板:共享核心(G3,1 个线程)
|
||||
|
||||
## 只能写
|
||||
|
||||
`packages/shared/` 里的共享逻辑,以及 mock 服务和契约测试。
|
||||
`contracts/` 冻结后只读,改契约走协调员的变更协议。
|
||||
各端适配器不归你,归各客户端线程。
|
||||
|
||||
## 产出
|
||||
|
||||
1. 共享类型和 API SDK:按冻结契约实现,客户端照着调,不需要再问字段含义。
|
||||
2. 共享逻辑:任务状态机、事件解析、重试策略、同步冲突处理,按契约实现。
|
||||
3. 可跑的 mock 服务:G3 结束时客户端能对着 mock 跑集成测试,
|
||||
mock 覆盖的接口清单要写清,不能只给 SDK 代码。
|
||||
4. 契约测试:mock 和真实后端(后端线程参与)都要过同一套契约测试,
|
||||
测试失败先定位是契约、SDK 还是后端的问题,不互相甩锅。
|
||||
5. G3.5 可行性 spike 证据:核心链路真跑通的命令和结果。
|
||||
agent 工具就是任务能创建、能跑出进度、能取消、能回结果。
|
||||
|
||||
## 完成定义
|
||||
|
||||
- 客户端线程拿到 SDK 和 mock 服务后能直接开工,不需要各写一套状态机。
|
||||
- 契约测试通过,mock 覆盖清单已交协调员。
|
||||
- spike 证据已交协调员,spike 不通过不进入 G4。
|
||||
- 结尾报告带 TODO-UPDATE 小节,格式见技能主文档“待办机制”。
|
||||
你绝不直接改 `docs/swe-todos.md`。
|
||||
|
||||
## 禁止
|
||||
|
||||
- 不在共享逻辑里写某端专属代码,专属逻辑留给各端适配器。
|
||||
- 不改契约迎合实现;实现不了契约时向协调员提变更请求。
|
||||
@@ -0,0 +1,233 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Verify the software-engineering skill appendix against its sources.
|
||||
|
||||
Provenance (see CHANGELOG.md in the skill directory): SKILL.md claims
|
||||
its appendix is "spliced verbatim from templates/roles/ by a generation
|
||||
script". The sources live at templates/roles/<id>.md next to SKILL.md;
|
||||
this script is the check that makes that claim testable. It never edits
|
||||
SKILL.md: the fix for a mismatch is an editorial decision (edit the
|
||||
source, then re-issue the appendix through the skill's own track),
|
||||
never a silent rewrite by this script.
|
||||
|
||||
What it checks, in order:
|
||||
1. SKILL.md contains the appendix intro and exactly the nine
|
||||
chapters 00..08, in order, each with a source note naming its own
|
||||
templates/roles/<id>.md file.
|
||||
2. templates/roles/ contains exactly those nine source files:
|
||||
a missing source fails, an unexpected extra file fails.
|
||||
3. Each source, in canonical form, equals the matching appendix
|
||||
chapter in canonical form (see canonical() below).
|
||||
4. CHANGELOG.md exists in the skill directory and mentions
|
||||
templates/roles/, so the "see CHANGELOG" pointer in SKILL.md
|
||||
resolves to a provenance record, not a dangling reference.
|
||||
|
||||
Canonical form (the ONLY allowed differences between a source file
|
||||
and its appendix chapter -- anything else is a failure):
|
||||
a. The chapter heading line itself ("## 附录角色:<id>(来源
|
||||
templates/roles/<id>.md)") exists only in SKILL.md; it is
|
||||
derived from the file name, not stored in the source.
|
||||
b. One inter-chapter separator line "---": in the current SKILL.md
|
||||
it appears after chapters 01/02/03/05/06 only. Chapters
|
||||
00/04/07 end with their GitHub subsection and run straight into
|
||||
the next heading with no separator, and 08 is the end of file.
|
||||
The check strips at most one trailing separator; it does NOT
|
||||
normalise SKILL.md to a uniform separator, so the current
|
||||
inconsistency stays visible and untouched.
|
||||
c. Blank lines around the heading and at the chapter end: a source
|
||||
file is stored with exactly one trailing newline and no leading
|
||||
blank line; the check strips surrounding blank lines before
|
||||
comparing.
|
||||
Any other difference -- wording, punctuation, order, a dropped or
|
||||
added line -- fails with the first differing line number.
|
||||
|
||||
Usage:
|
||||
python3 tools/check_appendix.py [--skill-dir DIR]
|
||||
python3 tools/check_appendix.py --emit [--skill-dir DIR]
|
||||
|
||||
--emit prints the spliced appendix in canonical form (heading line +
|
||||
source body per chapter) to stdout. It is a drafting aid for an
|
||||
editor, NOT a byte patch for SKILL.md: because of (b) and (c) above,
|
||||
its output differs from SKILL.md in separators and blank lines only,
|
||||
and it must never be pasted over SKILL.md without a review pass.
|
||||
|
||||
Exit codes: 0 = all chapters match; 1 = any check failed; 2 = usage
|
||||
error (argparse). Only the standard library is used, so the script
|
||||
runs standalone without importing cloud/.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
EXPECTED_IDS = ["00-coordinator", "01-product-manager", "02-architect",
|
||||
"03-designer", "04-client-developer", "05-backend",
|
||||
"06-qa", "07-acceptance", "08-shared-core"]
|
||||
|
||||
CHAPTER_RE = re.compile(
|
||||
r"^## 附录角色:(\d{2}-[a-z0-9-]+)"
|
||||
r"(来源 templates/roles/(\d{2}-[a-z0-9-]+\.md))",
|
||||
flags=re.M)
|
||||
TITLE_RE = re.compile(r"^# 角色模板:", flags=re.M)
|
||||
APPENDIX_MARKER = "## 附录:角色模板"
|
||||
|
||||
|
||||
def canonical(segment):
|
||||
"""Canonical chapter body: strip surrounding blanks and at most
|
||||
one trailing inter-chapter separator line, end with one newline."""
|
||||
lines = segment.split("\n")
|
||||
while lines and lines[0] == "":
|
||||
lines.pop(0)
|
||||
while lines and lines[-1] == "":
|
||||
lines.pop()
|
||||
if lines and lines[-1] == "---":
|
||||
lines.pop()
|
||||
while lines and lines[-1] == "":
|
||||
lines.pop()
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def first_diff(expected, actual):
|
||||
"""Return (line_no_1_based, expected_line, actual_line) or None."""
|
||||
exp_lines = expected.split("\n")
|
||||
act_lines = actual.split("\n")
|
||||
for i, (e, a) in enumerate(zip(exp_lines, act_lines)):
|
||||
if e != a:
|
||||
return i + 1, e, a
|
||||
if len(exp_lines) != len(act_lines):
|
||||
i = min(len(exp_lines), len(act_lines))
|
||||
e = exp_lines[i] if i < len(exp_lines) else "<end of source>"
|
||||
a = act_lines[i] if i < len(act_lines) else "<end of appendix>"
|
||||
return i + 1, e, a
|
||||
return None
|
||||
|
||||
|
||||
def load_chapters(skill_dir):
|
||||
"""Return (errors, chapters) where chapters maps id -> body segment
|
||||
(raw text between its heading line and the next heading / EOF)."""
|
||||
errors = []
|
||||
skill_path = skill_dir / "SKILL.md"
|
||||
if not skill_path.is_file():
|
||||
return ["SKILL.md 缺失:%s" % skill_path], {}
|
||||
text = skill_path.read_text(encoding="utf-8")
|
||||
if APPENDIX_MARKER not in text:
|
||||
return ["SKILL.md 缺失附录引言节 %r" % APPENDIX_MARKER], {}
|
||||
appendix = text.split(APPENDIX_MARKER, 1)[1]
|
||||
matches = list(CHAPTER_RE.finditer(appendix))
|
||||
found = [m.group(1) for m in matches]
|
||||
if found != EXPECTED_IDS:
|
||||
errors.append(
|
||||
"附录章节不符:应为 %s,实际为 %s" % (EXPECTED_IDS, found))
|
||||
chapters = {}
|
||||
for i, m in enumerate(matches):
|
||||
end = matches[i + 1].start() if i + 1 < len(matches) else len(appendix)
|
||||
if m.group(2) != m.group(1) + ".md":
|
||||
errors.append(
|
||||
"章节 %s 的来源注不符:应为 %s,实际为 %s"
|
||||
% (m.group(1), m.group(1) + ".md", m.group(2)))
|
||||
chapters[m.group(1)] = appendix[m.end():end]
|
||||
return errors, chapters
|
||||
|
||||
|
||||
def check(skill_dir):
|
||||
"""Return a list of failure strings (empty means pass)."""
|
||||
errors, chapters = load_chapters(skill_dir)
|
||||
roles_dir = skill_dir / "templates" / "roles"
|
||||
if not roles_dir.is_dir():
|
||||
errors.append("源目录缺失:%s" % roles_dir)
|
||||
sources = {}
|
||||
else:
|
||||
on_disk = sorted(p.name for p in roles_dir.iterdir()
|
||||
if p.is_file())
|
||||
expected_files = [cid + ".md" for cid in EXPECTED_IDS]
|
||||
if on_disk != expected_files:
|
||||
errors.append(
|
||||
"源文件集合不符:应为 %s,实际为 %s"
|
||||
% (expected_files, on_disk))
|
||||
sources = {}
|
||||
for name in on_disk:
|
||||
sources[name[:-3]] = (roles_dir / name).read_text(
|
||||
encoding="utf-8")
|
||||
for cid in EXPECTED_IDS:
|
||||
if cid not in sources:
|
||||
errors.append("源缺失:templates/roles/%s.md 不存在" % cid)
|
||||
continue
|
||||
source = sources[cid]
|
||||
if not TITLE_RE.match(source):
|
||||
errors.append(
|
||||
"源格式不符:%s.md 首行不是“# 角色模板:”" % cid)
|
||||
if canonical(source) != source:
|
||||
errors.append(
|
||||
"源非规范形式:%s.md 含首尾空行或尾部分隔符"
|
||||
"(源文件须以正文起、以单个换行收)" % cid)
|
||||
if cid not in chapters:
|
||||
errors.append("附录缺失章节:%s" % cid)
|
||||
continue
|
||||
expected = source
|
||||
actual = canonical(chapters[cid])
|
||||
if actual != expected:
|
||||
diff = first_diff(expected, actual)
|
||||
if diff is not None:
|
||||
lineno, exp_line, act_line = diff
|
||||
errors.append(
|
||||
"内容不一致:章节 %s 第 %d 行不同\n"
|
||||
" 源:%s\n 附录:%s"
|
||||
% (cid, lineno, exp_line[:120], act_line[:120]))
|
||||
else:
|
||||
errors.append("内容不一致:章节 %s(未定位首异行)" % cid)
|
||||
changelog = skill_dir / "CHANGELOG.md"
|
||||
if not changelog.is_file():
|
||||
errors.append("CHANGELOG.md 缺失:%s" % changelog)
|
||||
elif "templates/roles/" not in changelog.read_text(encoding="utf-8"):
|
||||
errors.append(
|
||||
"CHANGELOG.md 未提及 templates/roles/,溯源记录不完整")
|
||||
return errors
|
||||
|
||||
|
||||
def emit(skill_dir):
|
||||
"""Print the spliced appendix in canonical form to stdout."""
|
||||
_errors, chapters = load_chapters(skill_dir)
|
||||
roles_dir = skill_dir / "templates" / "roles"
|
||||
parts = []
|
||||
for cid in EXPECTED_IDS:
|
||||
source = (roles_dir / (cid + ".md")).read_text(encoding="utf-8")
|
||||
heading = ("## 附录角色:%s(来源 templates/roles/%s.md)"
|
||||
% (cid, cid))
|
||||
parts.append(heading + "\n\n" + source.rstrip("\n"))
|
||||
return "\n\n".join(parts) + "\n"
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Verify the software-engineering skill appendix "
|
||||
"against templates/roles/ sources.")
|
||||
parser.add_argument(
|
||||
"--skill-dir", default=None,
|
||||
help="skill directory (default: parent of this script's "
|
||||
"tools/ directory)")
|
||||
parser.add_argument(
|
||||
"--emit", action="store_true",
|
||||
help="print the spliced appendix in canonical form instead "
|
||||
"of verifying (drafting aid only, never a byte patch)")
|
||||
args = parser.parse_args(argv)
|
||||
if args.skill_dir:
|
||||
skill_dir = Path(args.skill_dir)
|
||||
else:
|
||||
skill_dir = Path(__file__).resolve().parents[1]
|
||||
if args.emit:
|
||||
sys.stdout.write(emit(skill_dir))
|
||||
return 0
|
||||
errors = check(skill_dir)
|
||||
if errors:
|
||||
for err in errors:
|
||||
print("FAIL: " + err, file=sys.stderr)
|
||||
print("appendix provenance FAILED: %d problem(s)" % len(errors),
|
||||
file=sys.stderr)
|
||||
return 1
|
||||
print("appendix provenance OK: %d chapters match templates/roles/"
|
||||
% len(EXPECTED_IDS))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,117 @@
|
||||
"""Appendix provenance for the software-engineering skill.
|
||||
|
||||
Background (see cloud/skills/software-engineering/CHANGELOG.md):
|
||||
SKILL.md claimed its appendix was "spliced verbatim from
|
||||
templates/roles/ by a generation script, see CHANGELOG", but the pack
|
||||
contained none of the three: no templates/, no CHANGELOG.md, no
|
||||
script. The appendix was therefore unverifiable -- a drifted chapter
|
||||
would pass CI silently.
|
||||
|
||||
This file pins the repair, and nothing else:
|
||||
- tools/check_appendix.py passes on the real pack (all nine chapters
|
||||
match their templates/roles/ sources, CHANGELOG present and
|
||||
mentioning templates/roles/);
|
||||
- the same script FAILS (nonzero exit) on a temp copy with a source
|
||||
deleted, a source tampered, or an extra source added -- so a future
|
||||
"source missing" or "appendix drifted" state cannot slip through;
|
||||
- --emit output is exactly the nine headings plus the nine source
|
||||
bodies, so the spliced form is itself derived from the sources.
|
||||
|
||||
Scope guard: these tests never edit SKILL.md or the sources. A
|
||||
failure here is an editorial decision for the skill's own track
|
||||
(edit the source, then re-issue the appendix), not a silent rewrite.
|
||||
Seeding behaviour is pinned elsewhere (test_skill_seed_pinning.py):
|
||||
templates/, tools/ and CHANGELOG.md never reach the tenant home,
|
||||
because skill_seed copies SKILL.md only.
|
||||
"""
|
||||
|
||||
import shutil
|
||||
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_appendix.py"
|
||||
|
||||
EXPECTED_IDS = ["00-coordinator", "01-product-manager", "02-architect",
|
||||
"03-designer", "04-client-developer", "05-backend",
|
||||
"06-qa", "07-acceptance", "08-shared-core"]
|
||||
|
||||
|
||||
def _run(skill_dir=None):
|
||||
cmd = [sys.executable, str(SCRIPT)]
|
||||
if skill_dir is not None:
|
||||
cmd += ["--skill-dir", str(skill_dir)]
|
||||
return subprocess.run(cmd, capture_output=True, text=True)
|
||||
|
||||
|
||||
def _temp_skill_copy(testcase):
|
||||
tmp = tempfile.TemporaryDirectory()
|
||||
testcase.addCleanup(tmp.cleanup)
|
||||
dest = Path(tmp.name) / "software-engineering"
|
||||
shutil.copytree(SKILL_DIR, dest)
|
||||
return dest
|
||||
|
||||
|
||||
class AppendixProvenanceTestCase(unittest.TestCase):
|
||||
def test_check_script_passes_on_real_pack(self):
|
||||
self.assertTrue(SCRIPT.is_file(), "校验脚本缺失:%s" % SCRIPT)
|
||||
self.assertTrue(
|
||||
(SKILL_DIR / "CHANGELOG.md").is_file(),
|
||||
"CHANGELOG.md 缺失,SKILL.md 的“见 CHANGELOG”仍是悬空引用")
|
||||
result = _run()
|
||||
self.assertEqual(
|
||||
result.returncode, 0,
|
||||
"校验脚本应在真实包上通过,stderr:%s" % result.stderr)
|
||||
self.assertIn("9 chapters match", result.stdout)
|
||||
|
||||
def test_missing_source_fails(self):
|
||||
skill = _temp_skill_copy(self)
|
||||
(skill / "templates" / "roles" / "03-designer.md").unlink()
|
||||
result = _run(skill)
|
||||
self.assertNotEqual(
|
||||
result.returncode, 0, "源缺失时校验必须失败退出非零")
|
||||
self.assertIn("03-designer", result.stderr)
|
||||
|
||||
def test_tampered_source_fails(self):
|
||||
skill = _temp_skill_copy(self)
|
||||
target = skill / "templates" / "roles" / "06-qa.md"
|
||||
text = target.read_text(encoding="utf-8")
|
||||
target.write_text(text.replace("不把“没测”写成“通过”",
|
||||
"不把“没测”写成“通过X”"),
|
||||
encoding="utf-8")
|
||||
result = _run(skill)
|
||||
self.assertNotEqual(
|
||||
result.returncode, 0, "源被改动时校验必须失败退出非零")
|
||||
self.assertIn("06-qa", result.stderr)
|
||||
|
||||
def test_extra_source_fails(self):
|
||||
skill = _temp_skill_copy(self)
|
||||
(skill / "templates" / "roles" / "99-extra.md").write_text(
|
||||
"# 角色模板:多余\n", encoding="utf-8")
|
||||
result = _run(skill)
|
||||
self.assertNotEqual(
|
||||
result.returncode, 0, "多余源文件时校验必须失败退出非零")
|
||||
self.assertIn("99-extra", result.stderr)
|
||||
|
||||
def test_emit_is_headings_plus_sources(self):
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(SCRIPT), "--emit"],
|
||||
capture_output=True, text=True)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
expected_parts = []
|
||||
for cid in EXPECTED_IDS:
|
||||
heading = ("## 附录角色:%s(来源 templates/roles/%s.md)"
|
||||
% (cid, cid))
|
||||
source = (SKILL_DIR / "templates" / "roles"
|
||||
/ (cid + ".md")).read_text(encoding="utf-8")
|
||||
expected_parts.append(heading + "\n\n" + source.rstrip("\n"))
|
||||
self.assertEqual(result.stdout, "\n\n".join(expected_parts) + "\n",
|
||||
"--emit 输出须恰为 9 章标题 + 9 份源正文")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user