sillyspec 3.18.1 → 3.18.3

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 (37) hide show
  1. package/.claude/skills/sillyspec-brainstorm/SKILL.md +24 -23
  2. package/.claude/skills/sillyspec-execute/SKILL.md +8 -1
  3. package/docs/brainstorm-plan-contract.md +64 -0
  4. package/docs/plan-execute-contract.md +123 -0
  5. package/docs/revision-mode.md +115 -0
  6. package/docs/sillyspec/file-lifecycle.md +13 -4
  7. package/docs/workflow-contract-regression.md +106 -0
  8. package/package.json +1 -1
  9. package/packages/dashboard/dist/assets/{index-DpLHK4jv.js → index-Bq_Z2hne.js} +568 -568
  10. package/packages/dashboard/dist/assets/{index-BcM2J-hv.css → index-O2W5RV4z.css} +1 -1
  11. package/packages/dashboard/dist/index.html +16 -16
  12. package/packages/dashboard/src/components/PipelineStage.vue +22 -2
  13. package/packages/dashboard/src/components/PipelineView.vue +10 -2
  14. package/packages/dashboard/src/components/StageBadge.vue +17 -3
  15. package/packages/dashboard/src/components/StepCard.vue +7 -2
  16. package/src/change-risk-profile.js +167 -0
  17. package/src/db.js +6 -0
  18. package/src/index.js +17 -1
  19. package/src/knowledge-match.js +130 -0
  20. package/src/progress.js +464 -11
  21. package/src/run.js +269 -29
  22. package/src/scan-postcheck.js +34 -2
  23. package/src/stage-contract.js +90 -5
  24. package/src/stages/brainstorm.js +23 -0
  25. package/src/stages/execute.js +122 -16
  26. package/src/stages/plan.js +82 -0
  27. package/src/stages/scan.js +40 -0
  28. package/src/stages/verify.js +38 -2
  29. package/test/brainstorm-plan-contract.test.mjs +273 -0
  30. package/test/knowledge-match.test.mjs +231 -0
  31. package/test/plan-execute-contract.test.mjs +330 -0
  32. package/test/platform-failure-samples.test.mjs +4 -0
  33. package/test/revision-v1.test.mjs +1145 -0
  34. package/test/scan-knowledge.test.mjs +175 -0
  35. package/test/scan-postcheck.test.mjs +3 -0
  36. package/test/spec-dir.test.mjs +8 -3
  37. package/test/stage-definitions.test.mjs +1 -1
@@ -1,6 +1,86 @@
1
1
  import { existsSync, readFileSync } from 'fs'
2
2
  import path from 'path'
3
3
 
4
+ /**
5
+ * 校验 plan.md 是否满足 execute 执行契约
6
+ * @param {string} planContent - plan.md 文件内容
7
+ * @returns {{ ok: boolean, errors: string[], warnings: string[], tasks: object[], waves: object[] }}
8
+ */
9
+ export function validatePlanForExecute(planContent) {
10
+ const errors = []
11
+ const warnings = []
12
+
13
+ if (!planContent || !planContent.trim()) {
14
+ return { ok: false, errors: ['plan.md 内容为空'], warnings, tasks: [], waves: [] }
15
+ }
16
+
17
+ const waves = parseWavesFromPlan(planContent)
18
+
19
+ // 收集所有 task
20
+ const allTasks = []
21
+ for (const wave of waves) {
22
+ for (const task of wave.tasks) {
23
+ allTasks.push(task)
24
+ }
25
+ }
26
+
27
+ // 检查 1: 至少有一个 checkbox task
28
+ if (allTasks.length === 0) {
29
+ errors.push('plan.md 中没有找到 checkbox task(格式: "- [ ] task-XX: 任务名")')
30
+ return { ok: false, errors, warnings, tasks: allTasks, waves }
31
+ }
32
+
33
+ // 检查 2: task id 唯一性
34
+ const idCounts = {}
35
+ for (const task of allTasks) {
36
+ if (task.index != null) {
37
+ const key = `task-${task.index}`
38
+ idCounts[key] = (idCounts[key] || 0) + 1
39
+ }
40
+ }
41
+ for (const [id, count] of Object.entries(idCounts)) {
42
+ if (count > 1) {
43
+ errors.push(`task id 重复: ${id} 出现 ${count} 次`)
44
+ }
45
+ }
46
+
47
+ // 检查 3: task id 连续性(从 1 开始)
48
+ const ids = allTasks
49
+ .map(t => t.index)
50
+ .filter(i => i != null)
51
+ .sort((a, b) => a - b)
52
+ if (ids.length > 0) {
53
+ const expected = Array.from({ length: ids.length }, (_, i) => ids[0] + i)
54
+ // 只检查以 task-01 起始的情况(常见模式)
55
+ if (ids[0] === 1) {
56
+ for (let i = 0; i < ids.length; i++) {
57
+ if (ids[i] !== i + 1) {
58
+ errors.push(`task id 不连续: 期望 task-${String(i + 1).padStart(2, '0')}, 实际 task-${String(ids[i]).padStart(2, '0')}`)
59
+ break
60
+ }
61
+ }
62
+ }
63
+ }
64
+
65
+ // 检查 4: task name 非空
66
+ for (const task of allTasks) {
67
+ if (!task.name || !task.name.trim()) {
68
+ errors.push(`task-${String(task.index || '?').padStart(2, '0')}: 任务名为空`)
69
+ }
70
+ }
71
+
72
+ // 检查 5: task 无 id 的 warning(不限制只在有 id 时检查)
73
+ for (const wave of waves) {
74
+ for (const task of wave.tasks) {
75
+ if (task.index == null) {
76
+ warnings.push(`Wave ${wave.index}: task "${task.name}" 没有 task id(建议格式 task-XX: 名称)`)
77
+ }
78
+ }
79
+ }
80
+
81
+ return { ok: errors.length === 0, errors, warnings, tasks: allTasks, waves }
82
+ }
83
+
4
84
  export const definition = {
5
85
  name: 'execute',
6
86
  title: '波次执行',
@@ -46,33 +126,52 @@ const fixedPrefix = [
46
126
  - 用 main_symbols 字段找到核心类/函数的定义位置
47
127
  - 子代理优先读模块卡片理解语义,再读 entrypoints/main_symbols 对应的源码
48
128
 
129
+ ### 符号影响面扩展检查
130
+ 11. **符号影响面扫描**(Critical — execute 前必做):
131
+ - 读取所有 tasks/task-NN.md,提取每个任务涉及的修改文件
132
+ - 对每个修改文件,检查是否涉及以下变更类型:
133
+ - class 构造函数参数变更(新增/删除/修改参数)
134
+ - 接口(interface)定义变更
135
+ - DTO / 类型定义变更
136
+ - API client 方法签名变更
137
+ - 函数/方法签名变更(参数增删改)
138
+ - 如果涉及上述变更类型,执行调用点搜索:
139
+ \`\`\`bash
140
+ rg "new ClassName\(" src/
141
+ rg "ClassName\(" src/
142
+ rg "methodName\(" src/
143
+ rg "import.*from.*filePath" src/
144
+ \`\`\`
145
+ - 将搜索到的调用点与 plan.md 和 tasks/task-NN.md 的 allowed_paths 对比
146
+ - **发现调用点不在任何 task 的 allowed_paths 中 → 直接阻断 execute**
147
+ - 报告:列出每个受影响符号、调用点位置、是否在任务范围内
148
+ - 如果调用点不在范围内但任务明确写了"不改原因",记录但不阻断
149
+
49
150
  ### 输出
50
151
  已加载的上下文摘要(含模块文档 + 源码锚点)`,
51
152
  outputHint: '上下文摘要',
52
153
  optional: false
53
154
  },
54
155
  {
55
- name: '创建 worktree',
56
- prompt: `为本次执行创建隔离的 git worktree。
156
+ name: '确认 worktree 路径',
157
+ prompt: `确认当前 worktree 状态,提取隔离路径。
57
158
 
58
159
  ### 操作
59
- 1. 运行 \`sillyspec worktree create <change-name>\`
60
- 2. 记录输出的 worktree 路径
61
- 3. 后续所有子代理的 cwd 设为该 worktree 路径
62
- 4. 如果创建失败 → 报错并停止(不要在无隔离状态下继续)
160
+ 1. 运行 \`sillyspec worktree meta <change-name>\` 读取 meta.json
161
+ 2. 从输出中提取 worktreePath、branch、mode 字段
162
+ 3. 确认 worktree 目录存在(如果是 worktree/native-worktree 模式)
63
163
 
64
- ### 降级模式
65
- CLI 可能自动降级(sandbox 限制、已在 linked worktree 中):
66
- - \`mode: native-worktree\` 已在 linked worktree,直接复用
67
- - \`mode: in-place-fallback\` — git worktree add 失败,降级为 in-place + baseline protection
68
- - 这两种模式都会输出 worktree 路径和分支名,正常继续即可
164
+ ### 铁律
165
+ - **worktree 已由 CLI execute 阶段启动时自动创建,不要自行创建或跳过**
166
+ - **后续所有子代理的 cwd 必须设为该 worktree 路径**
167
+ - 如果 meta.json 不存在(说明创建失败),停止并报错
69
168
 
70
169
  ### 输出
71
- worktree 路径 + 分支名 + 模式(如果有)
170
+ worktree 路径 + 分支名 + 模式
72
171
 
73
172
  ### 完成后执行
74
- sillyspec run execute --done --output "worktree 路径 + 分支名"`,
75
- outputHint: 'worktree 路径 + 分支名',
173
+ sillyspec run execute --done --output "worktree 路径 + 分支名 + 模式"`,
174
+ outputHint: 'worktree 路径 + 分支名 + 模式',
76
175
  optional: false
77
176
  },
78
177
  {
@@ -93,6 +192,12 @@ sillyspec run execute --done --output "worktree 路径 + 分支名"`,
93
192
  - auto — 全部自动执行
94
193
  5. 查询知识库:读取 \`.sillyspec/knowledge/INDEX.md\`,根据 Task 关键词匹配
95
194
 
195
+ ### 知识命中报告
196
+ {KNOWLEDGE_HIT_REPORT}
197
+
198
+ 如上所示的知识条目与本次任务相关。请阅读这些条目以获取项目约定和已知模式。
199
+ 如无命中条目(Status: no matches),跳过本节。
200
+
96
201
  ### 铁律
97
202
  - **不要询问用户确认频率**,确认模式由 CLI \`--confirm-mode\` 参数决定
98
203
  - 如果未检测到 \`--confirm-mode\`,默认使用 wave 模式`,
@@ -405,12 +510,13 @@ export function buildExecuteSteps(planFilePath = null, options = {}) {
405
510
 
406
511
  if (planFilePath && existsSync(planFilePath)) {
407
512
  const planContent = readFileSync(planFilePath, 'utf8')
513
+ // Plan → Execute 契约由 plan 阶段完成时的 postcheck 把关(run.js completeStep),
514
+ // 此处只负责解析 waves,避免 buildExecuteSteps 与进程退出耦合。
408
515
  waves = parseWavesFromPlan(planContent)
409
- // 从 planFilePath 推导 changeDir: .sillyspec/changes/<name>/plan.md
410
516
  changeDir = path.dirname(planFilePath)
411
517
  }
412
518
 
413
- // 如果没解析出 Wave,生成默认 3
519
+ // 没解析出 Wave(plan 不存在或不含可识别 task)→ 默认 3 Wave(向后兼容)
414
520
  if (!waves || waves.length === 0) {
415
521
  waves = []
416
522
  for (let i = 1; i <= 3; i++) {
@@ -1,6 +1,64 @@
1
1
  import { existsSync, readFileSync } from 'fs'
2
2
  import path from 'path'
3
3
 
4
+ /**
5
+ * 校验 design.md 是否满足 plan 执行契约
6
+ * 第一版是轻量 markdown 结构检查,不强 schema。
7
+ * @param {string} designContent - design.md 文件内容
8
+ * @returns {{ ok: boolean, errors: string[], warnings: string[] }}
9
+ */
10
+ export function validateDesignForPlan(designContent) {
11
+ const errors = []
12
+ const warnings = []
13
+
14
+ if (!designContent || !designContent.trim()) {
15
+ return { ok: false, errors: ['design.md 内容为空'], warnings }
16
+ }
17
+
18
+ const lower = designContent.toLowerCase()
19
+
20
+ // 检查 1: 必须包含目标/问题描述(error)
21
+ const hasGoal = /(^|\n)#{2,}\s*.*(目标|goal|objective|背景|background|问题|problem|purpose|目的)/i.test(designContent)
22
+ if (!hasGoal) {
23
+ errors.push('design.md 缺少「目标/背景/问题描述」章节 — plan 需要知道要达成什么')
24
+ }
25
+
26
+ // 检查 2: 必须包含范围/scope(error)
27
+ const hasScope = /(^|\n)#{2,}\s*.*(范围|scope|总体方案|方案|approach|solution|设计|design)/i.test(designContent)
28
+ if (!hasScope) {
29
+ errors.push('design.md 缺少「范围/总体方案/设计」章节 — plan 需要知道做什么和怎么做')
30
+ }
31
+
32
+ // 检查 3: 必须包含决策/方案选择(error)
33
+ const hasDecisions = /(^|\n)#{2,}\s*.*(决策|decision|选择|choice|方案选择)/i.test(designContent)
34
+ || /d-\d+@v\d+/i.test(designContent) // decisions.md 引用 ID
35
+ || /decisions?\.md/i.test(designContent) // 引用 decisions.md
36
+ if (!hasDecisions) {
37
+ errors.push('design.md 缺少「决策/方案选择」— plan 需要基于明确的技术决策来拆分任务')
38
+ }
39
+
40
+ // 检查 4 (warning): 缺非目标/non-goals
41
+ const hasNonGoals = /(^|\n)#{2,}\s*.*(非目标|non-goals?|不做|out of scope|不在范围)/i.test(designContent)
42
+ if (!hasNonGoals) {
43
+ warnings.push('design.md 缺少「非目标/Non-goals」— 建议明确不做什么,防止 scope creep')
44
+ }
45
+
46
+ // 检查 5 (warning): 缺约束/风险
47
+ const hasConstraints = /(^|\n)#{2,}\s*.*(约束|constraint|限制|limitation|风险|risk|trade-?off)/i.test(designContent)
48
+ if (!hasConstraints) {
49
+ warnings.push('design.md 缺少「约束/风险/Trade-off」— 建议记录已知约束和风险')
50
+ }
51
+
52
+ // 检查 6 (warning): 缺文件变更清单
53
+ const hasFileChanges = /文件变更|file change|变更清单|changed files/i.test(designContent)
54
+ || /^\|\s*(新增|修改|删除|new|modify|delete|update)\s*\|/im.test(designContent)
55
+ if (!hasFileChanges) {
56
+ warnings.push('design.md 缺少「文件变更清单」— 建议列出预期改动的文件')
57
+ }
58
+
59
+ return { ok: errors.length === 0, errors, warnings }
60
+ }
61
+
4
62
  export const definition = {
5
63
  name: 'plan',
6
64
  title: '实现计划',
@@ -326,6 +384,8 @@ plan_level + 计划内容(none 级别输出建议操作)`,
326
384
  - [ ] (brownfield)全局验收包含兼容性条款
327
385
  - [ ] 没有实现细节(接口定义、代码示例等不应该在 plan.md 里)
328
386
  - [ ] plan.md 与 design.md 的文件变更清单一致
387
+ - [ ] 如果涉及构造函数/接口/DTO/client 方法变更,是否搜索了所有调用点并纳入任务范围?
388
+ - [ ] 调用点搜索命令的输出是否记录在 plan.md 或 task-NN.md 中?
329
389
  - [ ] 如果有 Mermaid 图,依赖关系确实非平凡(非线性/非全并行)
330
390
  - [ ] 没有泛泛风险分析(如"需要充分测试")
331
391
 
@@ -376,6 +436,28 @@ export const fixedSuffix = [
376
436
  - 依赖关系和 plan.md 的 Wave 分组是否一致
377
437
  - 验收标准和 plan.md 的全局标准是否矛盾
378
438
  - 接口定义是否自洽
439
+ 5. **生产接线路径检查**(Critical):
440
+ - 读取 design.md,搜索以下关键词:
441
+ - 注入 / inject / 构造 / constructor / 初始化 / init / 启动路径 / startup / main / cli / entrypoint / daemon start / bootstrap
442
+ - 如果命中,提取提到的具体文件名(如 "在 cli.ts 中"、"main.ts 中实例化"、"Daemon 构造函数")
443
+ - 收集所有 tasks/task-NN.md 的 frontmatter 中 \`allowed_paths\` 列表
444
+ - 检查:design 中提到的生产入口文件是否在某个 task 的 allowed_paths 中?
445
+ - 如果 design 提到了入口文件但所有 task 的 allowed_paths 都不含该文件:
446
+ - **这是 plan contract 失败**
447
+ - 列出具体矛盾:\`design says: X 实例化 Y,但 allowed_paths 不含 Z\`
448
+ - **不自动修复**,暂停等待用户决定
449
+ - 如果 design 明确说"不需要改入口文件"并给出了理由,视为通过
450
+ - 调用:\`sillyspec run plan --wait --reason "生产接线路径检查失败" --options "修改 allowed_paths,修改 design,确认不需要改入口" --output "矛盾清单"\`
451
+ 6. **符号影响面检查**(Critical):
452
+ - 对每个 task 涉及的文件,检查是否修改了构造函数、接口、DTO、API client 方法签名
453
+ - 如果是,搜索所有调用点:
454
+ \`\`\`bash
455
+ rg "new <ClassName>" src/
456
+ rg "<methodName>" src/ --type ts --type js
457
+ \`\`\`
458
+ - 对比调用点与 plan.md/task 的 allowed_paths
459
+ - 发现调用点不在任务范围内 → **plan contract 失败**
460
+ - 调用:\`sillyspec run plan --wait --reason "符号影响面检查发现遗漏调用点" --options "扩展 allowed_paths,添加新任务,确认不需要改" --output "遗漏调用点清单"\`
379
461
  3. 发现问题 → 列出问题清单,暂停等待用户决定
380
462
  - 调用:\`sillyspec run plan --wait --reason "审查发现一致性问题" --options "自动修复,手动修复,忽略并继续" --output "问题清单"\`
381
463
  - **绝对禁止**:自己决定修复方向然后自动修复
@@ -454,6 +454,46 @@ step1 → step2 → step3
454
454
  outputHint: '流程和术语表生成状态',
455
455
  optional: true
456
456
  },
457
+ {
458
+ name: 'Extract Project Knowledge',
459
+ perProject: true,
460
+ prompt: `从本次 scan 产物中提取长期有效、跨变更复用的项目知识,写入知识库。
461
+
462
+ ### 知识分类
463
+ | 文件 | 内容 |
464
+ |------|------|
465
+ | conventions.md | 项目约定:目录规范、命名规范、提交规范、测试规范 |
466
+ | patterns.md | 可复用模式:鉴权方式、错误处理方式、模块组织方式 |
467
+ | known-issues.md | 已知坑:不可直接改的模块、历史兼容问题、代理限制 |
468
+ | uncategorized.md | 不确定分类、需要人工确认的知识 |
469
+
470
+ INDEX.md 维护索引,格式(每行:关键词1|关键词2 → [条目名](文件#锚点);关键词用于 execute 阶段命中匹配,必须给出能区分该知识的词):
471
+ \`\`\`markdown
472
+ # Knowledge Index
473
+
474
+ ## Conventions
475
+ - ESM|module|import → [ESM Only](conventions.md#esm-only)
476
+ \`\`\`
477
+
478
+ ### ⛔ 硬规则(必须遵守)
479
+ 1. **只写未来变更会反复用到的知识** — 不要把 scan 报告摘要塞进知识库
480
+ 2. **不要重复 knowledge 文件中已有的内容** — 读取现有文件,追加新条目,不覆盖
481
+ 3. **不确定分类或不确定长期有效 → uncategorized.md** — 宁可不确定也不要放错
482
+ 4. **每个正式分类条目必须更新 INDEX.md** — 添加对应分类下的链接
483
+ 5. **每个条目用 markdown 锚点格式** — 文件内用 \`## 标题\`,INDEX 用 \`[#标题]\` 或 \`(文件名#标题)\`
484
+
485
+ ### 操作
486
+ 1. 读取现有 knowledge 文件:\`{KNOWLEDGE_ROOT}/INDEX.md\`、\`{KNOWLEDGE_ROOT}/conventions.md\` 等
487
+ 2. 遍历 scan 产物(\`{DOCS_ROOT}/scan/*.md\`、\`{DOCS_ROOT}/modules/*.md\`),识别可复用知识
488
+ 3. 将新知识按分类写入对应文件(追加模式,不覆盖已有内容)
489
+ 4. 更新 INDEX.md 索引
490
+ 5. 如果确实没有新知识可提取(已有文件已覆盖),输出"无新知识"而非创建空条目
491
+
492
+ ### 输出
493
+ 新增知识条目数量 + 分类分布(或"无新知识")`,
494
+ outputHint: '知识条目数量',
495
+ optional: false
496
+ },
457
497
  {
458
498
  name: '自检和提交',
459
499
  perProject: true,
@@ -202,8 +202,32 @@ grep -rl "<关键词>" <源码目录>/ --include="*.java" --include="*.js" --inc
202
202
 
203
203
  ### 操作
204
204
  1. 汇总以上所有检查结果
205
- 2. 生成 verify-result.md 文件,保存到 \`.sillyspec/changes/<change-name>/verify-result.md\`
206
- 3. 给出结论:PASS / PASS WITH NOTES / FAIL
205
+ 2. **判定变更风险等级(change_risk_profile)**:
206
+
207
+ ### 变更风险分级规则
208
+ 扫描 design.md 和 plan.md 的关键词,自动判定 verify 强度:
209
+
210
+ | 触发条件 | verify 要求 |
211
+ |---|---|
212
+ | 只改文案/文档 | 静态检查即可 |
213
+ | 单模块纯函数 | 单测即可 |
214
+ | API contract / DTO / client | 单测 + contract test |
215
+ | daemon/backend 跨进程 | **必须真实集成** |
216
+ | session/lease/run 状态机 | **必须生命周期端到端验证** |
217
+ | 部署启动路径 | **必须真实启动一次** |
218
+
219
+ 触发关键词:daemon, backend, session, lease, agent_run, lifecycle, state_transition, claim, heartbeat, cross-process, cli.ts, main.ts, server.ts, bootstrap, entrypoint
220
+
221
+ ### 风险门控规则
222
+ - **integration-critical** 或 **deployment-critical** 变更:
223
+ - 结论为 PASS WITH NOTES → **降级为 FAIL**
224
+ - mock 单测通过但没有真实集成证据 → **FAIL**
225
+ - 必须在 verify-result.md 中包含 **Runtime Evidence** section
226
+ - **contract-required** 变更:需要 contract test 证据
227
+ - **unit-sufficient** 变更:单测即可
228
+
229
+ 3. 生成 verify-result.md 文件,保存到 \`.sillyspec/changes/<change-name>/verify-result.md\`
230
+ 4. 给出结论:PASS / PASS WITH NOTES / FAIL(受风险门控约束)
207
231
 
208
232
  ### verify-result.md 格式
209
233
  \`\`\`markdown
@@ -235,6 +259,18 @@ PASS / PASS WITH NOTES / FAIL
235
259
  ## 技术债务
236
260
  (TODO/FIXME/HACK 统计)
237
261
 
262
+ ## 变更风险等级
263
+ (自动检测的 change_risk_profile: doc-only / unit-sufficient / contract-required / integration-critical / deployment-critical)
264
+
265
+ ## Runtime Evidence(integration-critical / deployment-critical 必填)
266
+ - daemon 启动命令:
267
+ - backend 地址:
268
+ - 创建 session / 调用核心 API 的请求:
269
+ - daemon 日志关键片段(不能出现 session_control_no_manager / fallback to task_runner / submitMessages agent_run_id empty / 422):
270
+ - backend 状态(AgentRun running -> completed/failed):
271
+ - session / lease end 状态:
272
+ - 失败模式排除:
273
+
238
274
  ## 代码审查
239
275
  (问题列表 + 总体评价)
240
276
  \`\`\`
@@ -0,0 +1,273 @@
1
+ /**
2
+ * Brainstorm → Plan Contract v1 测试
3
+ *
4
+ * 验证 design.md 到 plan 的输入契约:
5
+ * 1. 合法 design 通过
6
+ * 2. 缺关键章节失败
7
+ * 3. warning 不阻断
8
+ */
9
+ import { validateDesignForPlan } from '../src/stages/plan.js'
10
+
11
+ let failed = 0
12
+ const failures = []
13
+
14
+ function assert(condition, msg) {
15
+ if (!condition) {
16
+ failed++
17
+ failures.push(msg)
18
+ console.log(` ❌ FAIL: ${msg}`)
19
+ } else {
20
+ console.log(` ✅ PASS: ${msg}`)
21
+ }
22
+ }
23
+
24
+ console.log('=== Brainstorm → Plan Contract v1 测试 ===\n')
25
+
26
+ // ─────────────────────────────────────────
27
+ // Case 1: valid design 通过
28
+ // ─────────────────────────────────────────
29
+ console.log('--- Case 1: valid design 通过 ---')
30
+ {
31
+ const design = `# Design: 用户认证系统
32
+
33
+ ## 背景
34
+ 需要实现用户认证。
35
+
36
+ ## 设计目标
37
+ - 支持 OAuth2
38
+ - 支持手机号登录
39
+
40
+ ## 非目标
41
+ - 不做 SSO
42
+
43
+ ## 总体方案
44
+ 使用 JWT + Refresh Token。
45
+
46
+ ## 决策
47
+ - D-001@v1: 选择 JWT 而非 Session
48
+
49
+ ## 约束
50
+ - 必须兼容现有 API
51
+
52
+ ## 文件变更清单
53
+ | 操作 | 文件路径 | 说明 |
54
+ |------|---------|------|
55
+ | 新增 | src/auth.js | 认证模块 |
56
+ `
57
+ const result = validateDesignForPlan(design)
58
+ assert(result.ok, '完整 design 应校验通过')
59
+ assert(result.errors.length === 0, '不应有 errors')
60
+ assert(result.warnings.length === 0, '不应有 warnings')
61
+ }
62
+
63
+ // ─────────────────────────────────────────
64
+ // Case 2: empty design 失败
65
+ // ─────────────────────────────────────────
66
+ console.log('\n--- Case 2: empty design 失败 ---')
67
+ {
68
+ assert(!validateDesignForPlan('').ok, '空字符串应失败')
69
+ assert(!validateDesignForPlan(null).ok, 'null 应失败')
70
+ assert(!validateDesignForPlan(' ').ok, '纯空格应失败')
71
+ }
72
+
73
+ // ─────────────────────────────────────────
74
+ // Case 3: missing goal 失败
75
+ // ─────────────────────────────────────────
76
+ console.log('\n--- Case 3: missing goal/背景 失败 ---')
77
+ {
78
+ const design = `# Design
79
+
80
+ ## 总体方案
81
+ 用 JWT。
82
+
83
+ ## 决策
84
+ D-001@v1: 选 JWT
85
+ `
86
+ const result = validateDesignForPlan(design)
87
+ assert(!result.ok, '缺目标/背景应失败')
88
+ assert(result.errors.some(e => e.includes('目标') || e.includes('背景')), '错误应提到目标/背景')
89
+ }
90
+
91
+ // ─────────────────────────────────────────
92
+ // Case 4: missing scope/方案 失败
93
+ // ─────────────────────────────────────────
94
+ console.log('\n--- Case 4: missing scope/方案 失败 ---')
95
+ {
96
+ const design = `# Design
97
+
98
+ ## 背景
99
+ 需要认证。
100
+
101
+ ## 决策
102
+ D-001@v1: 选 JWT
103
+ `
104
+ const result = validateDesignForPlan(design)
105
+ assert(!result.ok, '缺范围/方案应失败')
106
+ assert(result.errors.some(e => e.includes('范围') || e.includes('方案')), '错误应提到范围/方案')
107
+ }
108
+
109
+ // ─────────────────────────────────────────
110
+ // Case 5: missing decisions 失败
111
+ // ─────────────────────────────────────────
112
+ console.log('\n--- Case 5: missing decisions 失败 ---')
113
+ {
114
+ const design = `# Design
115
+
116
+ ## 背景
117
+ 需要认证。
118
+
119
+ ## 总体方案
120
+ 用 JWT。
121
+ `
122
+ const result = validateDesignForPlan(design)
123
+ assert(!result.ok, '缺决策应失败')
124
+ assert(result.errors.some(e => e.includes('决策')), '错误应提到决策')
125
+ }
126
+
127
+ // ─────────────────────────────────────────
128
+ // Case 6: decisions.md 引用也算决策
129
+ // ─────────────────────────────────────────
130
+ console.log('\n--- Case 6: decisions.md 引用算决策 ---')
131
+ {
132
+ const design = `# Design
133
+
134
+ ## 背景
135
+ 需要认证。
136
+
137
+ ## 总体方案
138
+ 用 JWT。详见 decisions.md。
139
+
140
+ ## 文件变更清单
141
+ | 操作 | 文件 | 说明 |
142
+ `
143
+ const result = validateDesignForPlan(design)
144
+ assert(result.ok, 'decisions.md 引用应满足决策检查')
145
+ }
146
+
147
+ // ─────────────────────────────────────────
148
+ // Case 7: missing non-goals 只有 warning
149
+ // ─────────────────────────────────────────
150
+ console.log('\n--- Case 7: missing non-goals warning ---')
151
+ {
152
+ const design = `# Design
153
+
154
+ ## 背景
155
+ 需要认证。
156
+
157
+ ## 总体方案
158
+ 用 JWT。
159
+
160
+ ## 决策
161
+ D-001@v1: 选 JWT
162
+
163
+ ## 约束
164
+ 必须兼容现有 API。
165
+ `
166
+ const result = validateDesignForPlan(design)
167
+ assert(result.ok, '缺非目标不应阻断')
168
+ assert(result.warnings.some(w => w.includes('非目标') || w.includes('Non-goals')), '应有非目标 warning')
169
+ }
170
+
171
+ // ─────────────────────────────────────────
172
+ // Case 8: missing constraints 只有 warning
173
+ // ─────────────────────────────────────────
174
+ console.log('\n--- Case 8: missing constraints warning ---')
175
+ {
176
+ const design = `# Design
177
+
178
+ ## 目标
179
+ 实现认证。
180
+
181
+ ## 设计方案
182
+ 用 JWT。
183
+
184
+ ## 决策
185
+ 选择 JWT。
186
+
187
+ ## 非目标
188
+ 不做 SSO。
189
+ `
190
+ const result = validateDesignForPlan(design)
191
+ assert(result.ok, '缺约束不应阻断')
192
+ assert(result.warnings.some(w => w.includes('约束') || w.includes('风险') || w.includes('Trade-off')), '应有约束/风险 warning')
193
+ }
194
+
195
+ // ─────────────────────────────────────────
196
+ // Case 9: missing 文件变更清单 warning
197
+ // ─────────────────────────────────────────
198
+ console.log('\n--- Case 9: missing 文件变更清单 warning ---')
199
+ {
200
+ const design = `# Design
201
+
202
+ ## 背景
203
+ 需要认证。
204
+
205
+ ## 总体方案
206
+ 用 JWT。
207
+
208
+ ## 决策
209
+ D-001@v1: 选 JWT
210
+ `
211
+ const result = validateDesignForPlan(design)
212
+ assert(result.ok, '缺文件变更清单不应阻断')
213
+ assert(result.warnings.some(w => w.includes('文件变更')), '应有文件变更 warning')
214
+ }
215
+
216
+ // ─────────────────────────────────────────
217
+ // Case 10: 英文 design 通过
218
+ // ─────────────────────────────────────────
219
+ console.log('\n--- Case 10: 英文 design 通过 ---')
220
+ {
221
+ const design = `# Design: Auth System
222
+
223
+ ## Background
224
+ We need authentication.
225
+
226
+ ## Solution
227
+ Use JWT + Refresh Token.
228
+
229
+ ## Decision
230
+ D-001@v1: Choose JWT over Session
231
+
232
+ ## Non-goals
233
+ No SSO.
234
+
235
+ ## Constraints
236
+ Must be backwards compatible.
237
+ `
238
+ const result = validateDesignForPlan(design)
239
+ assert(result.ok, '英文 design 应校验通过')
240
+ assert(result.errors.length === 0, '不应有 errors')
241
+ }
242
+
243
+ // ─────────────────────────────────────────
244
+ // Case 11: 最小合法 design
245
+ // ─────────────────────────────────────────
246
+ console.log('\n--- Case 11: 最小合法 design ---')
247
+ {
248
+ const design = `# Design
249
+
250
+ ## 目标
251
+ 修 bug。
252
+
253
+ ## 方案
254
+ 改代码。
255
+
256
+ ## 决策
257
+ D-001@v1: 直接改。
258
+ `
259
+ const result = validateDesignForPlan(design)
260
+ assert(result.ok, '最小合法 design 应通过')
261
+ // 可能有 warning 但 ok
262
+ }
263
+
264
+ // ── 结果 ──
265
+ console.log(`\n${'='.repeat(50)}`)
266
+ console.log(`✅ 通过: ${11 - failed} ❌ 失败: ${failed}`)
267
+ if (failures.length > 0) {
268
+ console.log(`失败项:`)
269
+ failures.forEach(f => console.log(` - ${f}`))
270
+ }
271
+ console.log(`${'='.repeat(50)}`)
272
+
273
+ if (failed > 0) process.exit(1)