@seanyao/roll 4.630.2 → 4.702.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/README.md +65 -56
  3. package/conventions/global/AGENTS.md +8 -7
  4. package/dist/roll.mjs +12909 -8532
  5. package/docs/INDEX.md +32 -0
  6. package/docs/architecture.md +444 -0
  7. package/docs/difftest-freeze-paradigm.md +113 -0
  8. package/docs/live-console.md +203 -0
  9. package/docs/manifesto.md +65 -0
  10. package/docs/migration/role-taxonomy-v4.md +60 -0
  11. package/docs/verification.md +83 -0
  12. package/guide/INDEX.md +86 -0
  13. package/guide/assets/layouts/cards-2.png +0 -0
  14. package/guide/assets/layouts/cards-3.png +0 -0
  15. package/guide/assets/layouts/cards-4.png +0 -0
  16. package/guide/assets/layouts/compare.png +0 -0
  17. package/guide/assets/layouts/highlight.png +0 -0
  18. package/guide/assets/layouts/pipeline.png +0 -0
  19. package/guide/assets/layouts/plain.png +0 -0
  20. package/guide/assets/layouts/quote.png +0 -0
  21. package/guide/assets/layouts/timeline.png +0 -0
  22. package/guide/en/acceptance-evidence.md +231 -0
  23. package/guide/en/ai-agents.md +185 -0
  24. package/guide/en/backlog-github-sync.md +108 -0
  25. package/guide/en/changelog.md +66 -0
  26. package/guide/en/configuration.md +112 -0
  27. package/guide/en/consistency.md +58 -0
  28. package/guide/en/conventions.md +113 -0
  29. package/guide/en/dream.md +121 -0
  30. package/guide/en/faq.md +855 -0
  31. package/guide/en/feedback.md +31 -0
  32. package/guide/en/getting-started.md +103 -0
  33. package/guide/en/installation.md +86 -0
  34. package/guide/en/legacy-onboarding.md +195 -0
  35. package/guide/en/loop-data-layout.md +256 -0
  36. package/guide/en/loop-driven-architecture.md +186 -0
  37. package/guide/en/loop.md +1324 -0
  38. package/guide/en/methodology.md +715 -0
  39. package/guide/en/migration-2.0.md +154 -0
  40. package/guide/en/overview.md +190 -0
  41. package/guide/en/pairing.md +151 -0
  42. package/guide/en/patterns/README.md +76 -0
  43. package/guide/en/patterns/graft-pattern.md +110 -0
  44. package/guide/en/patterns/replant-pattern.md +114 -0
  45. package/guide/en/patterns/seed-pattern.md +132 -0
  46. package/guide/en/peer.md +71 -0
  47. package/guide/en/pr-review.md +62 -0
  48. package/guide/en/practices/engineering-common-sense.md +395 -0
  49. package/guide/en/pricing.md +116 -0
  50. package/guide/en/project-setup.md +126 -0
  51. package/guide/en/roll-doc-audit.md +98 -0
  52. package/guide/en/skills.md +206 -0
  53. package/guide/en/test-isolation.md +51 -0
  54. package/guide/en/testing/quality-rubric.md +340 -0
  55. package/guide/en/testing.md +123 -0
  56. package/guide/en/tools.md +173 -0
  57. package/guide/skills.md +30 -0
  58. package/guide/zh/acceptance-evidence.md +194 -0
  59. package/guide/zh/ai-agents.md +170 -0
  60. package/guide/zh/backlog-github-sync.md +105 -0
  61. package/guide/zh/changelog.md +57 -0
  62. package/guide/zh/configuration.md +99 -0
  63. package/guide/zh/consistency.md +48 -0
  64. package/guide/zh/conventions.md +96 -0
  65. package/guide/zh/dream.md +97 -0
  66. package/guide/zh/faq.md +773 -0
  67. package/guide/zh/feedback.md +30 -0
  68. package/guide/zh/getting-started.md +96 -0
  69. package/guide/zh/installation.md +83 -0
  70. package/guide/zh/legacy-onboarding.md +192 -0
  71. package/guide/zh/loop-data-layout.md +236 -0
  72. package/guide/zh/loop-driven-architecture.md +186 -0
  73. package/guide/zh/loop.md +1124 -0
  74. package/guide/zh/methodology.md +702 -0
  75. package/guide/zh/migration-2.0.md +154 -0
  76. package/guide/zh/overview.md +186 -0
  77. package/guide/zh/pairing.md +117 -0
  78. package/guide/zh/patterns/README.md +74 -0
  79. package/guide/zh/patterns/graft-pattern.md +108 -0
  80. package/guide/zh/patterns/replant-pattern.md +112 -0
  81. package/guide/zh/patterns/seed-pattern.md +130 -0
  82. package/guide/zh/peer.md +63 -0
  83. package/guide/zh/pr-review.md +54 -0
  84. package/guide/zh/practices/engineering-common-sense.md +393 -0
  85. package/guide/zh/pricing.md +97 -0
  86. package/guide/zh/project-setup.md +114 -0
  87. package/guide/zh/roll-doc-audit.md +90 -0
  88. package/guide/zh/skills.md +191 -0
  89. package/guide/zh/test-isolation.md +46 -0
  90. package/guide/zh/testing/quality-rubric.md +284 -0
  91. package/guide/zh/testing.md +116 -0
  92. package/guide/zh/tools.md +173 -0
  93. package/package.json +4 -1
  94. package/skills/README.md +1 -0
  95. package/skills/roll-.qa/SKILL.md +1 -1
  96. package/skills/roll-.review/SKILL.md +1 -1
  97. package/skills/roll-build/SKILL.md +1 -1
  98. package/skills/roll-build/references/full-contract.md +16 -13
  99. package/skills/roll-design/SKILL.md +3 -3
  100. package/skills/roll-design/references/full-contract.md +17 -13
  101. package/skills/roll-fix/SKILL.md +1 -1
  102. package/skills/roll-fix/references/full-contract.md +13 -10
  103. package/skills/roll-peer/SKILL.md +1 -1
  104. package/skills/roll-prime/SKILL.md +77 -0
  105. package/skills/roll-prime/references/explorer-annex.md +39 -0
  106. package/skills/roll-prime/references/supervisor-prompt.md +165 -0
  107. package/skills/route-cases/skills.json +10 -0
  108. package/template/AGENTS.md +3 -1
@@ -0,0 +1,191 @@
1
+ # Roll 技能选择指南
2
+
3
+ 快速选择正确的技能或工具。
4
+
5
+ ## 核心技能
6
+
7
+ | 用户意图 | 技能 | 说明 |
8
+ |---------|------|------|
9
+ | **"不确定怎么做"** / **"有几个方案"** | `roll-design` | 探索方案、比较选项、人工决策 |
10
+ | **"帮我做一个..."** / **"实现 US-001"** / **"修 FIX-001"** | `roll-build` | 万能入口:US-XXX 故事模式、FIX-XXX 修复模式、自由文本飞行模式——一个技能全覆盖 |
11
+ | **"这个逻辑很关键"** / **"涉及支付"** | `roll-spar` | 对抗式 TDD,高风险场景激活 |
12
+ | **"修个 bug"** / **"改文案"** | `roll-fix` | 快速修复,无需完整工作流 |
13
+ | **"规划需求"** / **"拆成故事"** | `roll-design` | 仅规划,不实现,输出 BACKLOG.md |
14
+ | **"并行跑多个 Action"** | `roll-build` | 拆分 Action 后自动判断是否并行 |
15
+ | **"交付了什么 / 队列里还有什么?"** | `roll status` / `roll loop cycle` / Story 报告 | CLI-first 交付状态、cycle 轨迹和按 Story 收口的证据 |
16
+ | **"调试这个页面"** | `roll-debug` | 深度诊断,采集日志/网络/DOM |
17
+
18
+ ## 支撑技能
19
+
20
+ | 场景 | 技能 | 触发时机 |
21
+ |------|------|---------|
22
+ | 代码自审 | `roll-.review` | Commit 前,或手动触发 |
23
+ | 生成变更日志 | `roll-.changelog` | 成功 Deploy 后自动触发 |
24
+ | QA 测试参考 | `roll-.qa` | 写测试时参考 |
25
+ | 意图澄清 | `roll-.echo` | 用户输入模糊或不清晰时自动激活 |
26
+ | 文档/产品一致性审计 | `roll-doc-audit` | 核对 README、指南、网站、CLI help、文档与真实实现;需要时建索引、补缺口 |
27
+
28
+ ## roll-doc-audit —— 文档/产品一致性审计
29
+
30
+ `roll-doc-audit` 先核对用户可见文档表面与真实行为: README、指南、网站、CLI help、
31
+ 测试和源码。需要文档盘点时,它仍运行四个 phase——扫描/索引 → 缺口分析 → 填充 →
32
+ 报告——外加深度读取的 **Phase 3b**。Phase 3 填充目录级缺口;Phase 3b 构建完整
33
+ 项目符号表,侦测目录级填充单独无法发现的 **6 类跨目录主题**:
34
+
35
+ | 主题 | 触发条件 | 输出 |
36
+ |------|----------|------|
37
+ | 数据流 / 调用链 | import 链跨越 ≥ 3 个源目录 | `docs/data-flows.md` |
38
+ | 状态机 | `*State` / `*Status` 枚举被 ≥ 2 个文件引用 | `docs/state-machines.md` |
39
+ | 外部集成 | 存在 `fetch` / `axios` / `*_URL` 常量 | `docs/integrations.md` |
40
+ | 部署管线 | 存在 CI 配置文件加部署 URL 模式 | `docs/deployment.md` |
41
+ | Agent 入口 | 无 `AGENTS.md` 且源码根有 ≥ 3 个子目录 | `AGENTS.md` |
42
+ | 高引用目录 | 某目录被 ≥ 5 个源文件引用 | `<dir>/README.md` |
43
+
44
+ `$roll-doc-audit --dry-run` 运行 Phase 1–2 并打印 Phase 3 / 3b 计划而不写文件;
45
+ `$roll-doc-audit --force` 即便目标已存在也重新生成草稿。完整指南见
46
+ [roll-doc-audit.md](roll-doc-audit.md)。
47
+
48
+ ## 快速决策树
49
+
50
+ ```
51
+ 用户输入
52
+ |
53
+ +----------------------+
54
+ | "不确定方案?" |--> roll-design
55
+ +----------------------+
56
+ | 否
57
+ +----------------------+
58
+ | "一句话需求?" |--> roll-build(飞行模式)
59
+ +----------------------+
60
+ | 否
61
+ +----------------------+
62
+ | "有 US-XXX ID?" |--> roll-build(故事模式)
63
+ +----------------------+
64
+ | 否
65
+ +----------------------+
66
+ | "有 FIX-XXX ID?" |--> roll-fix
67
+ +----------------------+
68
+ | 否
69
+ +----------------------+
70
+ | "修 bug?" |--> roll-fix
71
+ +----------------------+
72
+ | 否
73
+ +----------------------+
74
+ | "规划/拆分?" |--> roll-design
75
+ +----------------------+
76
+ | 否
77
+ +----------------------+
78
+ | "高风险逻辑?" |--> roll-spar
79
+ +----------------------+
80
+ | 否
81
+ 人工判断
82
+ ```
83
+
84
+ ## Review Score(US-SKILL-010..014, FIX-343)
85
+
86
+ skill 不自评。工作 agent 绝不给自己的故事打分;**Review Score** 是 runner 侧的
87
+ 同行评审产物,由一个全新独立会话里的 Reviewer 产出(绝非 builder 的子 agent)。
88
+ `roll-build` / `roll-fix` cycle 交付后,runner 拉起全新会话的 Reviewer,写一条
89
+ 结构化 Review Score 笔记进故事的卡片文件夹(US-META-008——卡片文件夹是故事唯一的家;
90
+ 平铺的 `.roll/notes/` 留给项目日记与迁移前的历史档,看板趋势与故事档案双源合并读):
91
+
92
+ ```
93
+ .roll/features/<epic>/US-AUTH-001/notes/2026-05-29-roll-build-US-AUTH-001-1717000000.md
94
+ .roll/features/<epic>/FIX-072/notes/2026-05-29-roll-fix-FIX-072-1717000123.md
95
+ ```
96
+
97
+ 每条笔记是 YAML frontmatter + 评审理由:
98
+
99
+ ```markdown
100
+ ---
101
+ skill: roll-build
102
+ story: US-AUTH-001
103
+ score: 8
104
+ verdict: good
105
+ ts: 2026-05-29T03:14:15Z
106
+ ---
107
+
108
+ 故事干净交付,AC 全部命中。auth-cookie 测试 TCR 重试一次(setup 漏初始化)。
109
+ Peer review 有一条 nit,inline 解决。
110
+ ```
111
+
112
+ `roll loop status` 在 ROLLUP 区块底部汇总趋势:
113
+
114
+ ```
115
+ review-score: mean 7.8 / min 4 / redo 2 (last 14)
116
+ ```
117
+
118
+ `redo` 计入 `verdict: regression` 和 `verdict: ok` 且 `score < 6` 的
119
+ 低置信交付——两者都提示该轮 cycle 值得回看。mean 和 min 覆盖整个
120
+ 窗口,避免一次糟糕 cycle 被平均掩盖。
121
+
122
+ The trend line shows mean, minimum, and `redo` count (regression
123
+ verdicts plus low-confidence "ok"s) for the last 14 Review Score notes.
124
+
125
+ 这些笔记是 `.roll/` 的一部分,跟代码一起提交,质量轨迹在不同机器、
126
+ 不同协作者之间都可复现,从项目历史里直接可见。
127
+
128
+ ## 新增 skill
129
+
130
+ 一个 skill 就是 `skills/<name>/` 目录下的一个 `SKILL.md`,其 YAML frontmatter
131
+ 至少声明 `name` 和 `description`。注册新 skill 按以下步骤走——你永远不需要手工
132
+ 维护一份能力清单:
133
+
134
+ 1. 创建 `skills/<name>/SKILL.md`,写好 frontmatter(`name`、`description`、
135
+ `license`,以及 `allowed-tools`——见下文)。
136
+ 2. 重新生成能力清单:
137
+
138
+ ```bash
139
+ roll setup skills
140
+ ```
141
+
142
+ 该命令重新扫描每个 `skills/*/SKILL.md`,从 frontmatter 重写 `guide/skills.md`。
143
+ `guide/skills.md` 是**生成产物**——文件头写明
144
+ `GENERATED by roll setup skills — do not edit by hand`。新增或删除 skill 后,
145
+ 下一次重生成会自动反映;切勿手工编辑 `guide/skills.md`。
146
+ 3. 把新建的 `SKILL.md` 和重新生成的 `guide/skills.md` 一起提交。
147
+
148
+ ### 漂移防护
149
+
150
+ 提交到仓库的清单不会与实际 skill 悄悄漂移:
151
+
152
+ - `roll doctor skills` 重新扫描,若 `guide/skills.md` 与 `skills/*/SKILL.md` 不一致
153
+ 就失败(非零退出并打印 diff)。CI 跑这道关卡,手改或漏跑重生成都会在合并前被抓住。
154
+ - `roll doctor` 在 skills 区块里,当清单过期时打印一条不致失败的提醒,作为本地
155
+ 「记得跑 `roll setup skills`」的提示。
156
+
157
+ 扫描兼容 bash 3.2(基于 awk 的解析器;不用 `declare -A`、`mapfile` 或 `${var^^}`),
158
+ 因此能在 macOS 系统自带 bash 上运行。
159
+
160
+ ## 声明工具范围(`allowed-tools`)
161
+
162
+ 每个 `SKILL.md` 的 frontmatter 都应声明一行 `allowed-tools`,列出该 skill 被允许
163
+ 使用的工具:
164
+
165
+ ```yaml
166
+ ---
167
+ name: roll-design
168
+ license: MIT
169
+ allowed-tools: "Read, Edit, Write, Glob, Grep, Bash(git:*), WebSearch, WebFetch, Skill"
170
+ description: ...
171
+ ---
172
+ ```
173
+
174
+ - **怎么写**:用逗号分隔列出该 skill 实际需要的工具(如
175
+ `Read, Edit, Write, Glob, Grep`),能收窄就收窄 Bash——用 `Bash(git:*)` 这种 glob
176
+ 形式,而不是放开无限制的 `Bash`。
177
+ - **为何要写**:声明把每个 skill 意图使用的工具面记录下来,使工具范围逐 skill 可审计、
178
+ 可评审,且与整份清单约定一致。
179
+ - **对 Roll 是什么**:仅是**声明 + lint**。Roll 把声明呈现出来并检查其存在性;工具的
180
+ 真正**强制权(enforcement)**在内层 agent harness,不在 Roll。写 `allowed-tools`
181
+ 本身并不会沙箱化该 skill。
182
+
183
+ ## 自动触发关键词
184
+
185
+ | 技能 | 触发关键词 |
186
+ |------|----------|
187
+ | `roll-design` | 讨论、比较方案、怎么选、权衡、不确定用哪个、设计、规划、拆分、写故事、需求分析 |
188
+ | `roll-build` | 帮我做、加个功能、改一下、重构、实现 US-、做这个 story、做这个需求、并行、同时开发 |
189
+ | `roll-fix` | 修个 bug、改文案、调颜色、报错了、修复 |
190
+ | `roll-spar` | 对抗式、攻防、高风险、核心逻辑、支付、权限、安全 |
191
+ | `roll-debug` | 调试、诊断、页面有问题、排查 |
@@ -0,0 +1,46 @@
1
+ # Roll — 测试隔离(`roll test`)
2
+
3
+ `roll test` 通过 `.roll/local.yaml` 中选择的**隔离适配器**运行项目测试套件:
4
+
5
+ ```yaml
6
+ test_isolation:
7
+ type: none # 默认
8
+ ```
9
+
10
+ ## `type: none`(唯一内置适配器)
11
+
12
+ 宿主直接执行——与 `npm test` 同一个 shell。`roll test` 转发为
13
+ `npm test -- <参数>`,默认参数是 `--affected`:
14
+
15
+ ```bash
16
+ roll test # npm test -- --affected
17
+ roll test -- tests/ # 显式全量
18
+ roll test -- --tier=fast # 任意参数透传给 npm test
19
+ ```
20
+
21
+ v3 测试套件构造性 hermetic——伪造 `$HOME`、PATH shim 假二进制、`file://`
22
+ 远端、网络黑洞——宿主执行不会碰你的 launchd、共享 roll 状态或真实网络。
23
+
24
+ ## 路由:`--where`
25
+
26
+ 只报告测试将在哪里运行,不实际执行:
27
+
28
+ | 配置的 type | `--where` 输出 |
29
+ |---|---|
30
+ | `none`(或无配置) | `host` |
31
+ | 其他任意值 | `unknown:<type>` |
32
+
33
+ `<type>:<detail>` 的 token 形状是**扩展位**:未来若有容器适配器,会以
34
+ `docker:running` 这类同形 token 输出。
35
+
36
+ ## 未知类型大声失败
37
+
38
+ `test_isolation.type` 配成 `none` 以外的任何值,`roll test` 都会非零退出并
39
+ 明确报错、列出支持的类型。**绝不静默回落宿主执行**——隔离配置错了应该拦住
40
+ 你,而不是悄悄改变测试运行的位置。
41
+
42
+ ## `--reset`
43
+
44
+ 重置隔离环境。`type: none` 没有可重置的东西(宿主执行无状态):打印提示并
45
+ 以 0 退出。reset 期间持有 `.roll/.iso-reset.lock` 锁文件;并发的 `roll test`
46
+ 调用会快速失败并给出清晰报错。
@@ -0,0 +1,284 @@
1
+ # 测试质量评分卷(中文)
2
+
3
+ > 适用范围:`packages/*/test/` 下的 Vitest 测试。`roll-.dream` Scan 7 据此扫描代码并输出结构化 REFACTOR 条目。
4
+ > 英文版本:[quality-rubric.md](./quality-rubric.md)
5
+
6
+ 本评分卷把六类常见反模式公开化:要么让测试在无关改动上误报红,要么让真正的回归绿着过线。
7
+ 每类按相同四节展开:
8
+
9
+ - **定义** — 这条反模式是什么
10
+ - **判定信号** — 看测试文件本身就能识别
11
+ - **最小修复模板** — 最少改动的修复路径,不要求整体重写
12
+ - **真实代码反例** — 当前仓库里真实存在的一处
13
+
14
+ 类目编号 ❶ ~ ❽。❶–❻ 为提醒级(dream 标出、维护者分诊)。❼ 和 ❽ 为**阻断级**:
15
+ loop cycle 的测试质量合并门(US-QA-012)会拒绝引入新的 ❼ 或 ❽ 违规的 PR,
16
+ 哪怕 CI 是绿的也不放行。
17
+
18
+ ---
19
+
20
+ ## ❶ 断言体里硬编码业务数据
21
+
22
+ ### 定义
23
+
24
+ 测试在断言里直接写死业务值(价格、版本号、产品文案、模型 ID),而不是从被测模块或版本化 fixture
25
+ 读取。业务值一调,所有把它写死的测试一起红,看上去全军覆没,其实没动到任何逻辑。
26
+
27
+ ### 判定信号
28
+
29
+ - `[[ "$output" == *"..."* ]]` / `[ "$output" = "..." ]` 里出现的裸数字 / 裸字符串字面量
30
+ 正好等于源模块里的值。
31
+ - 同一个字面量在 ≥2 个测试文件出现(价格表、版本号、模型名)。
32
+ - 测试文件不是该值的"产权方"(比如 runner 测试断言价格,价格定义在 `packages/core/src/cost/prices.ts`)。
33
+
34
+ ### 最小修复模板
35
+
36
+ ```ts
37
+ // 改之前
38
+ it("opus rate is 5/25", () => {
39
+ expect(computeListCost("claude-opus-4-7", usage)).toBe("5.0 25.0");
40
+ });
41
+
42
+ // 改之后 —— 喂固定 fixture 费率表,断"公式"而非线上费率
43
+ it("computeListCost 按 注入费率 × token 算", () => {
44
+ const rates = { m: { in: 2, out: 4 } };
45
+ expect(computeListCost("m", { input_tokens: 1000, output_tokens: 500 }, rates)).toBe(0.004);
46
+ });
47
+ ```
48
+
49
+ 或者对线上费率只断结构不变量(如 `cache_read < input`、`out ≥ in`),调价就不会无谓打红。
50
+
51
+ ### 真实代码反例
52
+
53
+ `packages/core/test/prices.difftest.test.ts` 早先在断言里直接读线上 opus/sonnet/haiku
54
+ 费率,每次调价就一起红、却没暴露任何回归。现在算术喂固定 fixture 表,对线上费率只断
55
+ 结构不变量。
56
+
57
+ ---
58
+
59
+ ## ❷ 过度 mock 边界
60
+
61
+ ### 定义
62
+
63
+ 测试把本应实打实跑的边界 mock 掉了 —— 数据库、文件系统、子进程 spawn、git 调用 —— 测试
64
+ 对着虚假实现绿了,第一次跑真实集成时就崩。
65
+
66
+ ### 判定信号
67
+
68
+ - 单元测试顶部出现 `function git() { … }` / `function gh() { … }` 这种全局覆写,
69
+ 而被测代码本来就该跑真 git / 真 gh。
70
+ - 把 SQL 或文件系统调用换成内联返回手写串的 stub。
71
+ - mock 写在测试文件自己里、不在共享的测试 helper 模块里,说明是临时打补丁,不是共享。
72
+
73
+ ### 最小修复模板
74
+
75
+ ```ts
76
+ // 用真实的临时基底(tmp git repo / tmpdir),afterEach/afterAll 清掉。
77
+ let dir: string;
78
+ beforeEach(() => { dir = realpathSync(mkdtempSync(join(tmpdir(), "t-"))); execSync("git init -q", { cwd: dir }); });
79
+ afterEach(() => { rmSync(dir, { recursive: true, force: true }); });
80
+ ```
81
+
82
+ 如果某条边界在单元层确实跑不动(网络、gh、launchctl),把它做成可注入的 port/依赖、
83
+ 测试里传 fake —— 即 `runner-executor.test.ts` 用 `fakePorts()` 的那套。
84
+
85
+ ### 真实代码反例
86
+
87
+ 任何在测试里零散用 `const gh = () => ({...})` 来模拟 loop PR 路由的写法。修法是注入
88
+ `GithubPort`(见 `packages/cli/test/runner-executor.test.ts`),让同一个 fake 共享、明确、可被发现。
89
+
90
+ ---
91
+
92
+ ## ❸ 断言实现细节
93
+
94
+ ### 定义
95
+
96
+ 测试断言的不是观察得到的行为,而是内部状态的"形状":私有函数名、中间变量值、内部缓存
97
+ 文件路径。一次保留行为的重构就让测试红,本来该绿色放行的改动被卡死。
98
+
99
+ ### 判定信号
100
+
101
+ - `grep -q '_internal_helper' "$output"` —— 断言一个私有符号名被外露。
102
+ - `[[ "$(cat .roll/internal/_cache.tmp)" == ... ]]` —— 断言一个公共 API 从未承诺的
103
+ 内部缓存路径。
104
+ - 即使发生真正的行为回归,这种断言还能过,因为它在错的层做检查。
105
+
106
+ ### 最小修复模板
107
+
108
+ 把断言"重新锚"到**公共效果**:退出码、用户可见输出、调用方能感知到的状态。如果内部细节
109
+ 是唯一可见的东西,那往往说明生产代码缺一个对外的薄 API 层,应当主动加上。
110
+
111
+ ### 真实代码反例
112
+
113
+ 断言 `grep -q 'some_internal_helper' <output>` 的测试,函数一改名就红,但 gating
114
+ 逻辑没动。正确的断言是"故事 X 因依赖 Y 没满足被跳过",从公共副作用(故事仍是 📋 Todo、
115
+ 日志行被打出来)来观察。
116
+
117
+ ---
118
+
119
+ ## ❹ Fixture 顺序耦合
120
+
121
+ ### 定义
122
+
123
+ 测试之间共享可变状态(文件、环境变量、临时目录),依赖一个固定的运行顺序。并发跑、
124
+ `--filter` 单跑、或者重排顺序都会引发偶发失败,看上去像 flaky test,其实是耦合。
125
+
126
+ ### 判定信号
127
+
128
+ - A 测试读 B 测试在同文件里写下的状态。
129
+ - 模块级(或 `beforeAll`)fixture 改了共享状态、没人重置,后面的测试默默依赖它。
130
+ - 单跑能过,跑全套就红;反过来也算。
131
+
132
+ ### 最小修复模板
133
+
134
+ 每条测试的状态在 `beforeEach` 里建(自己的 tmpdir / env / fixture),断言,再在
135
+ `afterEach` 清掉。跨测试依赖只在真有必要时显式存在,且走一个有名字的 helper,
136
+ 绝不靠隐式顺序或共享可变量。
137
+
138
+ ### 真实代码反例
139
+
140
+ 任何 `beforeAll` 改写 `$HOME` 或写共享 state-`<slug>.yaml`、然后下一个测试不重置
141
+ 就直接读的测试文件都是候选。修法是每条测试用一个新 `mkdtempSync` + 注入 home/root 做隔离。
142
+
143
+ ---
144
+
145
+ ## ❺ 测私有函数 / 绕过公共 API
146
+
147
+ ### 定义
148
+
149
+ 测试直接伸进模块去调私有 helper、对它的返回值做断言。这个 helper 可以被改名、内联、
150
+ 甚至删掉而不改变行为 —— 但测试会喊"回归了"。
151
+
152
+ ### 判定信号
153
+
154
+ - 测试从模块内部深 import 一个未导出的 helper、直接喊它名字。
155
+ - 函数名以 `_` 开头(项目约定为私有),但测试却依赖它的签名。
156
+ - 整个测试文件根本没碰公共 API;它测的是内部分解方式。
157
+
158
+ ### 最小修复模板
159
+
160
+ 把调用走回公共入口(`roll <cmd>` / `my-tool foo`)。如果公共 API 覆盖不到要测的场景,
161
+ 那是真实的功能缺口 —— 要么该场景根本不可达(删测试),要么公共 API 应该补一个 flag
162
+ (明确地加上去)。
163
+
164
+ ### 真实代码反例
165
+
166
+ 从后门 import 一个未导出的私有函数(如 `_loopCheckDependsOn`)来测,把函数名锁死了。正确
167
+ 做法是跑命令、然后从 run log 里观察 skip 决策 —— 测用户在乎的行为。
168
+
169
+ ---
170
+
171
+ ## ❻ 断言框架行为
172
+
173
+ ### 定义
174
+
175
+ 测试在测 Vitest 本身(或任何框架),不是项目代码:断言 `beforeEach` 会在测试
176
+ 之前跑、mock 记录了调用、`expect` 存在……框架是对的,所以测试是绿的,但对项目
177
+ 没传递任何信息。
178
+
179
+ ### 判定信号
180
+
181
+ - 断言框架内部 / 测试运行器自身的行为。
182
+ - 测试体里全是 setup/teardown 自检,没有任何对项目代码的调用。
183
+ - 框架升级之后新增的"确认 Vitest 还能跑"的测试。
184
+
185
+ ### 最小修复模板
186
+
187
+ 删掉。框架验证应该在上游做。如果项目真的依赖某个框架契约,把它写进一个共享
188
+ 测试 helper 模块的文档里 + 一条 smoke 测试,而不是一整类断言。
189
+
190
+ ### 真实代码反例
191
+
192
+ 断言测试运行器内部计数的测试,每次 CI 都跑、从来没拦下过一次项目回归。如果
193
+ 真要保留这种保障,放进 CI 配置里跑一次就够,不必落到测试套里。
194
+
195
+ ---
196
+
197
+ ## ❼ 测试内联外部工具行为
198
+
199
+ ### 定义
200
+
201
+ 测试体用内联的 shell 管道(`sed`、`grep`、`find`、`awk`、`tr`)把外部工具
202
+ 的行为重新实现了一遍,而不是调用项目里已有的封装函数。当项目替换工具或改变内部
203
+ 解析逻辑时,所有抄了这段管道的测试一起红,但公共 API 的输出根本没变。
204
+
205
+ ### 判定信号
206
+
207
+ - 测试体里出现 `foo=$(echo "$output" | grep ... | sed ... | awk ...)` 这种链式调用。
208
+ - 同样的管道在 ≥2 个测试文件出现(解析逻辑被复制粘贴)。
209
+ - 项目里已经(或者本该)有一个函数封装了这个解析,但测试绕过了它自己搞了一遍。
210
+
211
+ ### 最小修复模板
212
+
213
+ ```ts
214
+ // 改之前 —— 内联正则复刻了项目函数已经做的事
215
+ const label = /<key>Label<\/key>\s*<string>([^<]*)<\/string>/.exec(plist)?.[1];
216
+
217
+ // 改之后 —— 调项目里处理这件事的函数
218
+ import { plistString } from "../src/lib/plist.js";
219
+ const label = plistString(plist, "Label");
220
+ ```
221
+
222
+ 如果项目里还没有对应函数,把解析抽成一个有名字的 helper 模块,
223
+ 让逻辑共享、可被发现。
224
+
225
+ ### 真实代码反例
226
+
227
+ 一条集成测试用手搓的正则链去解析 plist XML 提取 `<string>` 值,而不是 import 项目
228
+ 自己的 plist 解析器。将来 schema 一改,所有抄了这段的测试都得跟着修——解析器才是
229
+ 唯一该改的产权方。
230
+
231
+ ---
232
+
233
+ ## ❽ 测试断言触及仓库外的文件
234
+
235
+ ### 定义
236
+
237
+ 测试断言里读或检查的文件路径在仓库根目录之外(如 `~/.roll/`、`~/.codex/`、
238
+ `/etc/`、`/tmp/other-project/`)。换了机器或用户 home 目录状态不同时,测试要么
239
+ 误报失败,要么更糟——恰好通过了却在测错的东西。
240
+
241
+ ### 判定信号
242
+
243
+ - 断言里读/检查 `~/.xxx/...` 下的文件。
244
+ - 一个 tmp 路径不是测试文件自己创建的。
245
+ - 路径以 `~`、`${HOME}`、`/Users/` 或 `/home/` 开头,并且不是测试文件自己建的。
246
+
247
+ ### 最小修复模板
248
+
249
+ ```ts
250
+ // 改之前 —— 断言了一个仓库外、依赖本机环境的文件
251
+ it("skill file is synced", () => {
252
+ expect(readFileSync(`${homedir()}/.roll/skills/roll-.dream/SKILL.md`, "utf8")).toMatch(/Scan 6/);
253
+ });
254
+
255
+ // 改之后 —— 在测试自己控制的 tmpdir 里重建最小 fixture
256
+ it("skill file includes Scan 6", () => {
257
+ const home = mkdtempSync(join(tmpdir(), "h-"));
258
+ mkdirSync(join(home, ".roll/skills/roll-.dream"), { recursive: true });
259
+ writeFileSync(join(home, ".roll/skills/roll-.dream/SKILL.md"), "### Scan 6 — Doc Freshness\n");
260
+ expect(checkSkillHasScan6({ home })).toBe(true);
261
+ });
262
+ ```
263
+
264
+ 如果测试的目的确实是与外部文件打交道,注入 home/root 路径(一个 tmpdir),
265
+ 让测试在不同机器上结果一致。
266
+
267
+ ### 真实代码反例
268
+
269
+ 一条 dream-scan 测试断言 `${HOME}/.roll/skills/roll-.dream/SKILL.md`——一个仓库外的
270
+ 文件,内容取决于用户有没有跑过 `roll setup`。没装 Roll 的机器、或装了旧版本的机器上,
271
+ 这条测试会挂,但项目本身代码其实没问题。改法:把 fixture 沙箱进一个临时 `HOME`。
272
+
273
+ ---
274
+
275
+ ## `roll-.dream` 怎么消费这份评分卷
276
+
277
+ `roll-.dream` Scan 7 扫描测试套,按每类的判定信号匹配,每轮 ≤ 5 条 REFACTOR
278
+ (避免把 backlog 淹没),每条标上对应类目编号:
279
+
280
+ ```markdown
281
+ | REFACTOR-XXX | docs: <一句人话描述> [test-quality:❶] — flagged by dream YYYY-MM-DD | 📋 Todo |
282
+ ```
283
+
284
+ 维护者在早晨 brief 时分诊 REFACTOR 队列。
@@ -0,0 +1,116 @@
1
+ # Roll — 测试工作流
2
+
3
+ Roll 在整个交付过程中强制执行测试优先原则:
4
+
5
+ - **TCR**(Test && Commit || Revert)— 每个微步骤通过测试后才提交。
6
+ - **E2E Deposit** — 每个完成的 Story 留下一个 E2E 测试,覆盖其核心用户路径。
7
+ - **CI E2E Gate** — Deposit 的 E2E 在每次推送时运行,失败则阻止合并。
8
+ - **proof-of-pass** — pre-commit hook 物理拦截未经测试的提交。
9
+
10
+ ## E2E Deposit
11
+
12
+ TCR 微步骤通过后,`$roll-build` Phase 5.5 自动 Deposit E2E 测试:
13
+
14
+ 1. 检测项目已有的 E2E 基础设施(框架、目录、命名规范)。
15
+ 2. 编写一个覆盖 Story 关键用户路径的 E2E 测试。
16
+ 3. 运行它——若红则通过 TCR 修复。
17
+ 4. 提交:`tcr: e2e deposit for <story-id>`。
18
+
19
+ Deposit 的测试成为持久的回归守门,CI 在每次推送时重放,失败则阻止合并。
20
+
21
+ ## Pre-commit Hook(proof-of-pass)
22
+
23
+ Roll 的 pre-commit hook 要求:测试必须在 **60 秒内**、**与当前暂存树完全匹配**的情况下通过:
24
+
25
+ ```bash
26
+ # 测试运行器写入:
27
+ # .roll/last-test-pass ← 时间戳 + 树哈希
28
+
29
+ # 提交时 hook 检查:
30
+ # - 距离上次测试通过 < 60 s
31
+ # - 树哈希与当前暂存树匹配
32
+ ```
33
+
34
+ 使用 TCR(roll-build 的默认节奏)时此过程自动完成。
35
+
36
+ ## CI E2E Gate
37
+
38
+ 模板 CI 工作流(`.github/workflows/ci.yml`)将 E2E 测试作为独立任务,必须通过才能合并。失败时:
39
+
40
+ 1. 查看失败测试名——对应一个 Story ID。
41
+ 2. 在本地复现。
42
+ 3. 在 `BACKLOG.md` 开 `FIX-XXX` 条目,或直接用 `$roll-fix` 修复。
43
+
44
+ ## 失败分诊
45
+
46
+ `$roll-.qa` 为测试金字塔每层提供结构化诊断指引:
47
+
48
+ | 层级 | 运行命令 | 分诊入口 |
49
+ |------|----------|----------|
50
+ | 单元测试 | `pnpm --filter @roll/<pkg> test` | 失败测试文件 → 函数名 |
51
+ | 集成测试 | `pnpm --filter @roll/cli test` | 捕获的 stdout/退出码、fixture cwd |
52
+ | E2E | `<项目 E2E 命令>` | 用户路径、环境 |
53
+ | Smoke | `roll doctor` | 工具链健康 |
54
+
55
+ ## TCR 测试策略(Phase 3.0)
56
+
57
+ TCR 的每个 micro-step 都需要秒级反馈。测试套件是 pnpm 工作区上的 **Vitest**;
58
+ 门只跑 diff 触及的部分。
59
+
60
+ ### `roll test` 只跑被 diff 覆盖的测试
61
+
62
+ ```bash
63
+ roll test # 仅 affected(TCR micro-step 闸);写测试通过证明
64
+ pnpm --filter @roll/cli exec vitest run test/<file>.test.ts # 单文件
65
+ pnpm -r test # 全套(pre-push / CI / release)
66
+ pnpm test:cov # 全套 + v8 覆盖率
67
+ ```
68
+
69
+ `roll test` 把 diff 映射到受影响的 Vitest 文件、跑它们、并写下提交闸要检查的
70
+ 通过证明(见下)。纯文档改动无受影响测试,exit 0。pre-push / CI / release
71
+ 一律跑全套 `pnpm -r test`。
72
+
73
+ ## 测试质量评分卷(rubric)
74
+
75
+ `guide/zh/testing/quality-rubric.md`(由 `$roll-.dream` Scan 7 消费)列出
76
+ 夜检扫描会按 `REFACTOR-XXX [test-quality:❶|❷|...|❽]` 输出的八类反模式:
77
+
78
+ | # | 反模式 | 修复方向 |
79
+ |---|--------|---------|
80
+ | ❶ | 硬编码业务数据(价格、版本号、产品文案) | 通过 monkey-patch / 构造注入 fixture;断言行为而非数据表 |
81
+ | ❷ | 过度 mock(数据库、文件系统、真实边界) | 用真实子系统配小型适配 mock;优先内存测试替身 |
82
+ | ❸ | 断言实现细节(私有符号名、内部数据形状) | 通过 public API 断言可观察行为 |
83
+ | ❹ | Fixture 顺序耦合(测试间共享可变状态) | 每个测试独立 setup/teardown;用不可变 fixture |
84
+ | ❺ | 测私有函数 / 绕过 public API | 改走 public 入口;如果难以到达,说明 API 设计有问题 |
85
+ | ❻ | 断言框架行为(在测 Vitest 本身) | 删测试;信任框架 |
86
+ | ❼ | 内联外部工具行为(测试体里复制 `sed`/`grep`/`awk` 流水线) | 调项目自己的 helper;或抽到测试 helper 模块共享 |
87
+ | ❽ | 断言 repo 之外的文件(`~/.codex`/`~/.kimi`/`~/.roll` 或系统路径) | 用临时目录(`mkdtempSync`)沙箱化,环境变量重定向到那里,不碰用户真实配置 |
88
+
89
+ dream 每轮最多 emit 5 条 REFACTOR,避免 backlog 被噪音淹没。
90
+ 按优先级逐个收拾。
91
+
92
+ ### 测试质量合并门(US-QA-012 / 013)
93
+
94
+ ❼ 和 ❽ 两类是**硬阻断**:CI 绿后 loop 自动合并前会跑
95
+ `roll loop test-quality-check <改动的测试文件>`。命中违规时 loop 写
96
+ `ALERT-<slug>.md` 并卡住 PR,要么改测试要么 PR 描述加
97
+ `[skip-test-quality]` 标记放行(大小写不敏感)。
98
+
99
+ ❼ and ❽ are **blocking**: PR auto-merge is held until violations are fixed
100
+ or the PR description carries `[skip-test-quality]`.
101
+
102
+ 绕过请谨慎使用——dream 仍会把违规登记为 REFACTOR,不会因为 skip 就被遗忘。
103
+
104
+ ❶..❻ 是建议性,dream 标 REFACTOR 但门不卡。常规迭代里慢慢清。
105
+
106
+ 带 `# test-quality:allow` 注释的行会被扫描器跳过(文档校验类测试里
107
+ 合法使用 `awk` 解析 markdown 时用,不触碰生产代码)。
108
+
109
+ `packages/core/test/prices.difftest.test.ts` 是 ❶ 类范例 —— 之前的断言读
110
+ 生产费率表,每次价格调整就把套件打红,即便算术没动。现在算术喂固定
111
+ fixture 价格表,对生产费率只断结构不变量(cache_read < input 等)。
112
+
113
+ ## 另见
114
+
115
+ - [loop.md](loop.md) — loop 如何在每个 Story 中强制 TCR 纪律
116
+ - [skills.md](skills.md) — `$roll-build`(交付 + Deposit E2E)