sillyspec 3.18.2 → 3.18.4

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 (40) hide show
  1. package/docs/brainstorm-plan-contract.md +64 -0
  2. package/docs/plan-execute-contract.md +123 -0
  3. package/docs/revision-mode.md +115 -0
  4. package/docs/sillyspec/file-lifecycle.md +13 -4
  5. package/docs/workflow-contract-regression.md +106 -0
  6. package/package.json +1 -1
  7. package/packages/dashboard/dist/assets/{index-DpLHK4jv.js → index-Bq_Z2hne.js} +568 -568
  8. package/packages/dashboard/dist/assets/{index-BcM2J-hv.css → index-O2W5RV4z.css} +1 -1
  9. package/packages/dashboard/dist/index.html +16 -16
  10. package/packages/dashboard/src/components/PipelineStage.vue +22 -2
  11. package/packages/dashboard/src/components/PipelineView.vue +10 -2
  12. package/packages/dashboard/src/components/StageBadge.vue +17 -3
  13. package/packages/dashboard/src/components/StepCard.vue +7 -2
  14. package/src/change-risk-profile.js +167 -0
  15. package/src/contract-matrix.js +278 -0
  16. package/src/db.js +6 -0
  17. package/src/endpoint-extractor.js +315 -0
  18. package/src/index.js +53 -6
  19. package/src/init.js +31 -4
  20. package/src/knowledge-match.js +130 -0
  21. package/src/progress.js +464 -11
  22. package/src/run.js +287 -7
  23. package/src/scan-postcheck.js +34 -2
  24. package/src/stage-contract.js +86 -6
  25. package/src/stages/brainstorm.js +23 -0
  26. package/src/stages/execute.js +158 -4
  27. package/src/stages/plan.js +82 -0
  28. package/src/stages/scan.js +40 -0
  29. package/src/stages/verify.js +63 -2
  30. package/src/worktree.js +264 -35
  31. package/test/brainstorm-plan-contract.test.mjs +273 -0
  32. package/test/contract-artifacts.test.mjs +323 -0
  33. package/test/knowledge-match.test.mjs +231 -0
  34. package/test/plan-execute-contract.test.mjs +330 -0
  35. package/test/platform-failure-samples.test.mjs +4 -0
  36. package/test/revision-v1.test.mjs +1145 -0
  37. package/test/scan-knowledge.test.mjs +175 -0
  38. package/test/scan-postcheck.test.mjs +3 -0
  39. package/test/spec-dir.test.mjs +8 -3
  40. 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,6 +126,27 @@ 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: '上下文摘要',
@@ -91,6 +192,12 @@ sillyspec run execute --done --output "worktree 路径 + 分支名 + 模式"`,
91
192
  - auto — 全部自动执行
92
193
  5. 查询知识库:读取 \`.sillyspec/knowledge/INDEX.md\`,根据 Task 关键词匹配
93
194
 
195
+ ### 知识命中报告
196
+ {KNOWLEDGE_HIT_REPORT}
197
+
198
+ 如上所示的知识条目与本次任务相关。请阅读这些条目以获取项目约定和已知模式。
199
+ 如无命中条目(Status: no matches),跳过本节。
200
+
94
201
  ### 铁律
95
202
  - **不要询问用户确认频率**,确认模式由 CLI \`--confirm-mode\` 参数决定
96
203
  - 如果未检测到 \`--confirm-mode\`,默认使用 wave 模式`,
@@ -299,6 +406,48 @@ function parseWavesFromPlan(planContent) {
299
406
  * 为 Wave 生成 prompt(强制子代理执行)
300
407
  */
301
408
  function buildWavePrompt(wave, waveIndex, changeDir, worktreePath) {
409
+ // ── Contract Matrix:检查是否有 provider/consumer 契约需要注入 ──
410
+ let contractInjection = ''
411
+ if (changeDir) {
412
+ try {
413
+ const { buildContractMatrix, buildConsumerInjection } = require('../contract-matrix.js')
414
+ const planFile = path.join(changeDir, 'plan.md')
415
+ if (existsSync(planFile)) {
416
+ const planContent = readFileSync(planFile, 'utf8')
417
+ const contracts = buildContractMatrix(planContent, changeDir)
418
+ if (contracts.length > 0) {
419
+ // 收集本 wave 所有 task 的注入内容
420
+ const waveTasks = wave.tasks.map((t, ti) => {
421
+ const num = String(t.index || (ti + 1)).padStart(2, '0')
422
+ return `task-${num}`
423
+ })
424
+ const relevantContracts = contracts.filter(c => waveTasks.includes(c.consumer))
425
+ if (relevantContracts.length > 0) {
426
+ contractInjection = `
427
+ ### API Contract Matrix
428
+ 本 Wave 存在前端/后端跨 task 契约:
429
+ ${relevantContracts.map(c => `- **${c.consumer}** 消费 **${c.provider}** 产出的 API`).join('\n')}
430
+ `
431
+ // 为每个 consumer task 生成详细注入
432
+ for (const taskName of waveTasks) {
433
+ const injection = buildConsumerInjection(changeDir, join(changeDir, '..', '..'), taskName, contracts)
434
+ if (injection) {
435
+ contractInjection += `
436
+ ### 子代理 ${taskName} 的契约注入
437
+ 为 ${taskName} 启动子代理时,在子代理 prompt 末尾追加以下内容:
438
+
439
+ <contract-injection>
440
+ ${injection}
441
+ </contract-injection>
442
+ `
443
+ }
444
+ }
445
+ }
446
+ }
447
+ }
448
+ } catch {}
449
+ }
450
+
302
451
  // 构建任务摘要(不再内联完整蓝图,减少上下文污染)
303
452
  const taskSummary = wave.tasks.map((t, ti) => {
304
453
  const taskNum = String(t.index || (ti + 1)).padStart(2, '0')
@@ -369,7 +518,7 @@ ${taskSummary}
369
518
  - 🔥 热上下文:design.md 编码铁律 + 当前 Wave 任务(必须加载)
370
519
  - 🌡️ 温上下文:CONVENTIONS.md + ARCHITECTURE.md(需要时加载)
371
520
  - ❄️ 冷上下文:其他变更的 design.md、历史 plan.md(不要主动加载,除非明确需要)
372
-
521
+ ${contractInjection}
373
522
  ### 本 Wave 任务
374
523
  ${taskList}
375
524
 
@@ -386,7 +535,11 @@ ${taskList}
386
535
  5. 遇到 BLOCKED → 记录原因,选择:重试/跳过/停止
387
536
 
388
537
  ### 完成后
389
- 运行 sillyspec run execute --done --input "用户原始反馈" --output "Wave ${waveIndex} 结果摘要"`
538
+ 1. 为每个后端 router task,扫描变更文件提取 API 端点 artifact:
539
+ - 在变更文件中搜索所有 router 注册路径(@router.get/post/put/delete)
540
+ - 将端点清单写入 .sillyspec/.runtime/contract-artifacts/<task-name>/endpoints.json
541
+ - 格式: { "task": "task-XX", "type": "backend_endpoints", "endpoints": [{ "method": "GET", "path": "/api/ppm/xxx" }] }
542
+ 2. 运行 sillyspec run execute --done --input "用户原始反馈" --output "Wave ${waveIndex} 结果摘要"`
390
543
  }
391
544
 
392
545
 
@@ -403,12 +556,13 @@ export function buildExecuteSteps(planFilePath = null, options = {}) {
403
556
 
404
557
  if (planFilePath && existsSync(planFilePath)) {
405
558
  const planContent = readFileSync(planFilePath, 'utf8')
559
+ // Plan → Execute 契约由 plan 阶段完成时的 postcheck 把关(run.js completeStep),
560
+ // 此处只负责解析 waves,避免 buildExecuteSteps 与进程退出耦合。
406
561
  waves = parseWavesFromPlan(planContent)
407
- // 从 planFilePath 推导 changeDir: .sillyspec/changes/<name>/plan.md
408
562
  changeDir = path.dirname(planFilePath)
409
563
  }
410
564
 
411
- // 如果没解析出 Wave,生成默认 3
565
+ // 没解析出 Wave(plan 不存在或不含可识别 task)→ 默认 3 Wave(向后兼容)
412
566
  if (!waves || waves.length === 0) {
413
567
  waves = []
414
568
  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,
@@ -134,6 +134,31 @@ grep -rl "<关键词>" <源码目录>/ --include="*.java" --include="*.js" --inc
134
134
  5. 任意 D-xxx@vN 无下游覆盖时标记为 ⚠️ 决策未闭环
135
135
  6. 任意 P0/P1 unresolved/blocking 决策标记为 FAIL blocker
136
136
 
137
+ **探针 5:API Contract Parity Check(跨前后端契约对账)**
138
+ 此探针仅在以下条件满足时执行:
139
+ - 存在 \.sillyspec/.runtime/contract-artifacts/ 目录(说明 execute 阶段生成了 endpoint artifact)
140
+ - 或者项目同时有 backend/ 和 frontend/ 目录
141
+
142
+ 执行步骤:
143
+ 1. 收集所有 provider endpoint artifacts:
144
+ - 读取 .sillyspec/.runtime/contract-artifacts/*/endpoints.json
145
+ - 汇总为 backend 端点清单
146
+ 2. 扫描前端 API 调用:
147
+ - 在 frontend/ 目录中搜索 apiFetch/request/axios/fetch 调用
148
+ - 提取所有 API 路径(归一化动态参数为 {param})
149
+ 3. Diff 对账:
150
+ - 前端调用路径在 backend 端点清单中找不到 → **❌ Missing backend endpoint**(FAIL blocker)
151
+ - backend 端点在前端无调用 → ⚠️ Unused backend endpoint(warning,不阻断)
152
+ 4. 输出对账结果表格:
153
+ \
154
+ \
155
+ | 状态 | 前端调用 | 后端端点 | 文件 |
156
+ |---|---|---|---|
157
+ | ❌ missing | GET /api/ppm/project-plan/{param}/plan-nodes | — | frontend/src/lib/ppm/plan.ts |
158
+ \
159
+ \
160
+ 如果发现 Missing backend endpoint,必须在验证报告中标记为 ❌ contract gap。
161
+
137
162
  ### 探针结果处理
138
163
  - 将四个探针的结果汇总为「探针报告」
139
164
  - 如果探针发现问题(未实现标记、关键词缺失、测试缺失、决策未闭环),在最终验证报告中明确标注
@@ -202,8 +227,32 @@ grep -rl "<关键词>" <源码目录>/ --include="*.java" --include="*.js" --inc
202
227
 
203
228
  ### 操作
204
229
  1. 汇总以上所有检查结果
205
- 2. 生成 verify-result.md 文件,保存到 \`.sillyspec/changes/<change-name>/verify-result.md\`
206
- 3. 给出结论:PASS / PASS WITH NOTES / FAIL
230
+ 2. **判定变更风险等级(change_risk_profile)**:
231
+
232
+ ### 变更风险分级规则
233
+ 扫描 design.md 和 plan.md 的关键词,自动判定 verify 强度:
234
+
235
+ | 触发条件 | verify 要求 |
236
+ |---|---|
237
+ | 只改文案/文档 | 静态检查即可 |
238
+ | 单模块纯函数 | 单测即可 |
239
+ | API contract / DTO / client | 单测 + contract test |
240
+ | daemon/backend 跨进程 | **必须真实集成** |
241
+ | session/lease/run 状态机 | **必须生命周期端到端验证** |
242
+ | 部署启动路径 | **必须真实启动一次** |
243
+
244
+ 触发关键词:daemon, backend, session, lease, agent_run, lifecycle, state_transition, claim, heartbeat, cross-process, cli.ts, main.ts, server.ts, bootstrap, entrypoint
245
+
246
+ ### 风险门控规则
247
+ - **integration-critical** 或 **deployment-critical** 变更:
248
+ - 结论为 PASS WITH NOTES → **降级为 FAIL**
249
+ - mock 单测通过但没有真实集成证据 → **FAIL**
250
+ - 必须在 verify-result.md 中包含 **Runtime Evidence** section
251
+ - **contract-required** 变更:需要 contract test 证据
252
+ - **unit-sufficient** 变更:单测即可
253
+
254
+ 3. 生成 verify-result.md 文件,保存到 \`.sillyspec/changes/<change-name>/verify-result.md\`
255
+ 4. 给出结论:PASS / PASS WITH NOTES / FAIL(受风险门控约束)
207
256
 
208
257
  ### verify-result.md 格式
209
258
  \`\`\`markdown
@@ -235,6 +284,18 @@ PASS / PASS WITH NOTES / FAIL
235
284
  ## 技术债务
236
285
  (TODO/FIXME/HACK 统计)
237
286
 
287
+ ## 变更风险等级
288
+ (自动检测的 change_risk_profile: doc-only / unit-sufficient / contract-required / integration-critical / deployment-critical)
289
+
290
+ ## Runtime Evidence(integration-critical / deployment-critical 必填)
291
+ - daemon 启动命令:
292
+ - backend 地址:
293
+ - 创建 session / 调用核心 API 的请求:
294
+ - daemon 日志关键片段(不能出现 session_control_no_manager / fallback to task_runner / submitMessages agent_run_id empty / 422):
295
+ - backend 状态(AgentRun running -> completed/failed):
296
+ - session / lease end 状态:
297
+ - 失败模式排除:
298
+
238
299
  ## 代码审查
239
300
  (问题列表 + 总体评价)
240
301
  \`\`\`