sillyspec 3.25.4 → 3.25.6

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 (41) hide show
  1. package/.claude/skills/sillyspec-brainstorm/SKILL.md +27 -1
  2. package/.claude/skills/sillyspec-execute/SKILL.md +56 -0
  3. package/.claude/skills/sillyspec-plan/SKILL.md +26 -0
  4. package/.claude/skills/sillyspec-propose/SKILL.md +26 -0
  5. package/.claude/skills/sillyspec-scan/SKILL.md +12 -7
  6. package/.claude/skills/sillyspec-verify/SKILL.md +2 -2
  7. package/package.json +1 -1
  8. package/src/change-list.js +114 -12
  9. package/src/change-risk-profile.js +36 -1
  10. package/src/constants.js +11 -0
  11. package/src/index.js +4 -1
  12. package/src/machine-interface.js +5 -1
  13. package/src/progress/stage-machine.js +17 -14
  14. package/src/run/command.js +58 -3
  15. package/src/run/complete-handlers.js +9 -3
  16. package/src/run/complete.js +130 -33
  17. package/src/run/gates.js +37 -0
  18. package/src/run/prompt.js +20 -0
  19. package/src/run/quick-audit.js +8 -1
  20. package/src/run/scan-profile.js +29 -10
  21. package/src/run/shared.js +161 -4
  22. package/src/run/stage.js +4 -0
  23. package/src/scan-postcheck.js +25 -11
  24. package/src/stage-contract.js +39 -8
  25. package/src/stage-review.js +16 -5
  26. package/src/stages/archive.js +13 -9
  27. package/src/stages/brainstorm-auto.js +3 -2
  28. package/src/stages/brainstorm.js +13 -16
  29. package/src/stages/execute.js +4 -3
  30. package/src/stages/plan-postcheck.js +1 -1
  31. package/src/stages/plan.js +13 -0
  32. package/src/stages/propose.js +3 -2
  33. package/src/stages/quick.js +31 -31
  34. package/src/stages/scan.js +2 -1
  35. package/src/stages/status.js +2 -2
  36. package/src/stages/verify.js +19 -8
  37. package/src/task-review.js +308 -3
  38. package/src/verify-postcheck.js +353 -38
  39. package/src/worktree-apply.js +134 -67
  40. package/src/worktree.js +106 -32
  41. package/templates/prompts/verify-probes.md +13 -2
@@ -48,7 +48,7 @@ sillyspec run brainstorm --done --answer "..." --output "..." # 一步完成
48
48
 
49
49
  某些步骤(如"对话式探索与需求澄清")需要用户输入。两种方式:
50
50
 
51
- > 自动去重:若前置 step 已对同一问题(waitReason 归一化后相同,如"确认设计方案" vs "最终确认设计方案")确认过,后续重复 wait 会自动跳过,无需再 `--wait`。
51
+ > 自动去重:若前置 step 已对同一问题(waitReason 归一化后相同)确认过,后续重复 wait 会自动跳过,无需再 `--wait`。
52
52
 
53
53
  - **方式一(推荐)**:AI 自行与用户交互后,一步完成:
54
54
  ```bash
@@ -75,6 +75,32 @@ brainstorm 完成时按 design.md frontmatter 的 `scale` 分叉:
75
75
 
76
76
  > 规模由 AI 在 brainstorm 最后一步评估并写入 design.md frontmatter。判错可手动改 `scale` 后再跑相应阶段。
77
77
 
78
+ ### Stage Review Gate(brainstorm 末尾的 design review.json)
79
+
80
+ brainstorm 在 `tier=independent` 规模下除 design.md 等四件套外,还需产出一个 stage 级 `review.json`,CLI `Stage Review Gate` 硬校验其 schema 与 `docHash` 真实性。
81
+
82
+ - 路径:`.sillyspec/.runtime/stage-reviews/brainstorm-review-<stage-review-run-id>/review.json`(目录可能不存在需手建;run-id 由该步 `--done` prompt 输出指定)。marker 文件 `.runtime/current-stage-review-run-id-brainstorm-<变更名>`。
83
+ - 字段(`schemaVersion:1`,`reviewType=design` —— 区别于 execute 的 `acceptance`):
84
+
85
+ ```json
86
+ {
87
+ "schemaVersion": 1,
88
+ "reviewType": "design",
89
+ "reviewedFiles": ["changes/<变更名>/design.md"], // [0] 为主文档
90
+ "docHash": "<sha256(reviewedFiles[0] 文件内容,hex)>",
91
+ "specVerdict": "pass", // pass | fail | cannot_verify
92
+ "qualityVerdict": "pass", // pass | fail | cannot_verify
93
+ "checklist": [ // 扁平数组,逐条对照 design 章节核验
94
+ { "item": "背景与目标", "result": "pass", "note": "..." } // result: pass | gap | fail
95
+ ],
96
+ "requiredEvidence": [], // cannot_verify 时必填非空
97
+ "reviewerNotes": "..."
98
+ }
99
+ ```
100
+
101
+ - `docHash` = `sha256(主审查文档内容)`(hex)—— brainstorm 主文档是 `design.md`(即 `reviewedFiles[0]`)。CLI 重算 sha256 比对,不符判伪造(fail-closed)。改 design.md 后须重算 docHash。`tier=self`(≤3 文件)降级为当前 agent 自审,不强制独立子代理。
102
+ - 运行时 CLI 通过 `{REVIEW_JSON_CONTRACT}` 注入精确 schema+示例,以注入版为权威逐字模板。
103
+
78
104
  ## 铁律
79
105
 
80
106
  - **必须用 exec 工具(shell)执行 CLI,不要自己编造流程**
@@ -60,6 +60,58 @@ sillyspec worktree doctor --fix --change <变更名>
60
60
 
61
61
  execute 完成时,每个 task 必须有 `review.json` 且 verdict 通过,否则阻断完成。**例外**:task 在 `tasks/task-XX.md` frontmatter 声明 `low_risk: true`(type-only / 机械迁移等低逻辑风险)时,缺 review.json 只发 warning 不阻断。`cannot_verify` 的 task 会写入 `verify-required-evidence.json`,由 verify 阶段消费。
62
62
 
63
+ #### task 级 review.json 契约(CLI 硬校验)
64
+
65
+ - 路径:`.sillyspec/.runtime/execute-runs/<execute-run-id>/tasks/task-XX/review.json`(目录不存在需先建)。
66
+ - `execute-run-id` 取自**第一次** `--done` prompt 输出(形如 `exec-2026-07-28-112833`),也写在 marker 文件 `.runtime/current-execute-run-id-<变更名>`。多个 run 时 gate 读最新 marker。
67
+ - 字段(`schemaVersion:1`):
68
+
69
+ ```json
70
+ {
71
+ "schemaVersion": 1,
72
+ "task": "task-01",
73
+ "base": "<git-base-commit-hash>",
74
+ "head": "<git-head-commit-hash>",
75
+ "changedFiles": ["src/..."],
76
+ "specVerdict": "pass", // pass | fail | cannot_verify
77
+ "qualityVerdict": "pass", // pass | fail | cannot_verify
78
+ "reviewerNotes": "...",
79
+ "requiredEvidence": [] // cannot_verify 时必须非空
80
+ }
81
+ ```
82
+
83
+ - `base` / `head` 必须是**真实 git commit**(`git rev-parse --verify`),否则判伪造并阻断。`changedFiles` 与实际 `git diff base..head` 完全不相交也判伪造。base..head 空 commit diff 但 working-tree 有未提交改动 → 视为有效改动(warning 不阻断)。
84
+ - 后端 router task 另需在 `.sillyspec/.runtime/contract-artifacts/<task-name>/endpoints.json` 写 API 端点清单(扫 `@router.get/post/...`)。
85
+
86
+ ### Stage Review Gate(execute 末尾的 acceptance review.json)
87
+
88
+ execute 还有**第二道**独立的 stage 级审查:除逐 task review.json 外,整个 execute 阶段完成还需一个 stage 级 `review.json`(在 "完成确认"/acceptance 步骤产出)。CLI `Stage Review Gate` 硬校验其 schema 与 `docHash` 真实性。
89
+
90
+ - 路径:`.sillyspec/.runtime/stage-reviews/execute-review-<stage-review-run-id>/review.json`(目录可能不存在需手建;run-id 由该步 `--done` prompt 输出指定)。marker 文件 `.runtime/current-stage-review-run-id-execute-<变更名>`。
91
+ - 字段(`schemaVersion:1`,`reviewType=acceptance` —— 区别于 brainstorm/plan/propose 的 `"design"`):
92
+
93
+ ```json
94
+ {
95
+ "schemaVersion": 1,
96
+ "reviewType": "acceptance",
97
+ "reviewedFiles": ["changes/<变更名>/design.md", "<可选追加 git diff 涉及的源码>"],
98
+ "docHash": "<sha256(reviewedFiles[0] 文件内容,hex)>",
99
+ "specVerdict": "pass", // pass | fail | cannot_verify
100
+ "qualityVerdict": "pass", // pass | fail | cannot_verify
101
+ "checklist": [ // 扁平数组(非按层嵌套!),逐条对照 design 章节 + FR/NFR/决策核验代码
102
+ { "item": "FR-01 排序", "result": "pass", "note": "..." } // result: pass | gap | fail
103
+ ],
104
+ "requiredEvidence": [], // cannot_verify 时必填非空
105
+ "reviewerNotes": "..."
106
+ }
107
+ ```
108
+
109
+ - `docHash` = `sha256(主审查文档内容)`(hex)—— execute 主审查文档是 `design.md`,即 `reviewedFiles[0]`。CLI 会重算 sha256 比对,不符判伪造(fail-closed);找不到主文档也 fail。**改 design.md 后须重算 docHash 再写**,可用 `sillyspec run execute --done` 触发的 prompt 注入版契约(`{REVIEW_JSON_CONTRACT}`)逐字改值。
110
+ - `tier=independent` 时必须由独立 QA 子代理产出该 review.json(独立上下文,不共享实现者分析);`tier=self`(变更 ≤3 文件)降级为当前 agent 自审。
111
+ - 该 acceptance review 同时覆盖"代码审查"视角,后续代码审查步骤仅需轻量复审。
112
+
113
+ > 运行时 CLI 会把精确 schema 表 + 完整 JSON 示例 + docHash 算法通过 `{REVIEW_JSON_CONTRACT}` 占位符注入到该步 prompt(源码 `src/stage-review.js: renderReviewJsonContract()`)。本段为常驻摘要;以运行时注入的契约为权威逐字模板。
114
+
63
115
  ## worktree 子命令(execute 相关)
64
116
 
65
117
  ```bash
@@ -80,6 +132,10 @@ plan → execute → verify
80
132
 
81
133
  execute 完成后(所有 Wave/task 完成 + Task Review Gate 通过),运行 `sillyspec run verify --change <变更名>` 验证。
82
134
 
135
+ ### 批量完成(一次 --done 收尾)
136
+
137
+ 当 `plan.md` 所有 task checkbox 已勾(人工勾或基于各 task `review.json` pass 由 CLI 自动勾)且代码客观核验通过(`checkExecuteCodeEvidence` 非"零变更")时,任一 execute `--done` 会**一次性补完所有剩余 step** 直达阶段完成,不必逐次 +1 推进。日志会打印 `🚀 execute 批量完成:plan 全勾 + 代码核验通过,一次性补完 N 个剩余 step`。条件不满足(plan 未全勾 / 代码零变更)时仍按单步推进,要求补 review 后重跑 `--done` 直至满足。
138
+
83
139
  ## 铁律
84
140
 
85
141
  - **必须用 exec 工具(shell)执行 CLI,不要自己编造流程**
@@ -46,6 +46,32 @@ sillyspec run plan --done --answer "..." --output "..." # 一步完成 wait+d
46
46
 
47
47
  plan 的步骤是动态的:`generate_plan`(生成分级计划)→ `review_plan`(审查计划,按规模分级:tier=self 当前 agent 自审 / tier=independent 启动独立审查子代理产出 stage review.json,避免生成+自审同一次输出的偏差)→ CLI 从 `plan.md` 解析出 task 自动插入"任务蓝图协调器"步骤(per-task)。这是正常行为,不要手动添加。
48
48
 
49
+ ### Stage Review Gate(plan 的 design review.json)
50
+
51
+ `review_plan` 步骤在 `tier=independent` 时产出一个 stage 级 `review.json`,CLI `Stage Review Gate` 硬校验其 schema 与 `docHash` 真实性。
52
+
53
+ - 路径:`.sillyspec/.runtime/stage-reviews/plan-review-<stage-review-run-id>/review.json`(目录可能不存在需手建;run-id 由该步 `--done` prompt 输出指定)。marker 文件 `.runtime/current-stage-review-run-id-plan-<变更名>`。
54
+ - 字段(`schemaVersion:1`,`reviewType=plan` —— plan 阶段主审查文档是 `plan.md`;execute 阶段才是 `acceptance`):
55
+
56
+ ```json
57
+ {
58
+ "schemaVersion": 1,
59
+ "reviewType": "plan",
60
+ "reviewedFiles": ["changes/<变更名>/plan.md"], // [0] 为主文档
61
+ "docHash": "<sha256(reviewedFiles[0] 文件内容,hex)>",
62
+ "specVerdict": "pass", // pass | fail | cannot_verify
63
+ "qualityVerdict": "pass", // pass | fail | cannot_verify
64
+ "checklist": [ // 扁平数组,逐条对照 plan 章节/Wave 结构/task 完整性核验
65
+ { "item": "Wave 分级合理", "result": "pass", "note": "..." } // result: pass | gap | fail
66
+ ],
67
+ "requiredEvidence": [], // cannot_verify 时必填非空
68
+ "reviewerNotes": "..."
69
+ }
70
+ ```
71
+
72
+ - `docHash` = `sha256(主审查文档内容)`(hex)—— plan 主文档是 `plan.md`(`reviewedFiles[0]`)。CLI 重算 sha256 比对,不符判伪造(fail-closed)。改 plan.md 后须重算 docHash。`tier=self`(≤3 文件)降级为当前 agent 自审。
73
+ - 运行时 CLI 通过 `{REVIEW_JSON_CONTRACT}` 注入精确 schema+示例,以注入版为权威逐字模板。
74
+
49
75
  ### 契约门控(阻断完成)
50
76
 
51
77
  - **plan 启动前**:CLI 校验 `design.md` 是否满足 plan 契约(缺文件变更清单/风险登记/自审章节会阻断)。若失败需先 `sillyspec run brainstorm --reopen --from-step N` 修订设计。
@@ -42,6 +42,32 @@ sillyspec run propose --reopen --from-step N # 重新打开已完成阶段修
42
42
 
43
43
  propose 含自检步骤(按规模分级:tier=self 当前 agent 自审 / tier=independent 启动独立审查子代理产出 stage review.json),完成时校验四件套文档齐全 + 内容章节(如 proposal 的 Non-Goals、design 的文件变更清单/风险登记/自审)。缺失会阻断完成。
44
44
 
45
+ ### Stage Review Gate(propose 的 proposal review.json)
46
+
47
+ propose 在 `tier=independent` 时产出一个 stage 级 `review.json`,CLI `Stage Review Gate` 硬校验其 schema 与 `docHash` 真实性。
48
+
49
+ - 路径:`.sillyspec/.runtime/stage-reviews/propose-review-<stage-review-run-id>/review.json`(目录可能不存在需手建;run-id 由该步 `--done` prompt 输出指定)。marker 文件 `.runtime/current-stage-review-run-id-propose-<变更名>`。
50
+ - 字段(`schemaVersion:1`,`reviewType=proposal` —— propose 阶段主审查文档是 `proposal.md`):
51
+
52
+ ```json
53
+ {
54
+ "schemaVersion": 1,
55
+ "reviewType": "proposal",
56
+ "reviewedFiles": ["changes/<变更名>/proposal.md"], // [0] 为主文档
57
+ "docHash": "<sha256(reviewedFiles[0] 文件内容,hex)>",
58
+ "specVerdict": "pass", // pass | fail | cannot_verify
59
+ "qualityVerdict": "pass", // pass | fail | cannot_verify
60
+ "checklist": [ // 扁平数组,逐条对照 proposal 章节/Non-Goals 核验
61
+ { "item": "Non-Goals 明确", "result": "pass", "note": "..." } // result: pass | gap | fail
62
+ ],
63
+ "requiredEvidence": [], // cannot_verify 时必填非空
64
+ "reviewerNotes": "..."
65
+ }
66
+ ```
67
+
68
+ - `docHash` = `sha256(主审查文档内容)`(hex)—— propose 主文档是 `proposal.md`(`reviewedFiles[0]`)。CLI 重算 sha256 比对,不符判伪造(fail-closed)。改 proposal.md 后须重算 docHash。`tier=self`(≤3 文件)降级为当前 agent 自审。
69
+ - 运行时 CLI 通过 `{REVIEW_JSON_CONTRACT}` 注入精确 schema+示例,以注入版为权威逐字模板。
70
+
45
71
  ## 阶段流转
46
72
 
47
73
  ```
@@ -38,22 +38,27 @@ sillyspec run scan --reset # 重置阶段(从头开始)
38
38
 
39
39
  | 参数 | 说明 |
40
40
  |---|---|
41
- | `--deep` | 强制 deep 扫描 profile(完整流程,不按规模裁剪) |
41
+ | `--quick` | 强制 quick profile(快速接入,仅 4 份核心文档,0 子代理) |
42
+ | `--standard` | 强制 standard profile(压缩步骤,最多 1 子代理) |
43
+ | `--deep` | 强制 deep profile(完整流程,不按规模裁剪) |
42
44
  | `--force-rescan` | 覆盖已有 scan 文档的保护(默认覆盖需 source_commit/updated_at 匹配) |
43
45
 
44
- ### scanProfile(按项目规模自动裁剪)
46
+ ### scanProfile(显式选择优先,否则按规模自动裁剪)
45
47
 
46
- CLI 根据源码规模自动选择 profile,无需手动指定:
48
+ 可用 `--quick` / `--standard` / `--deep` **显式指定 profile**(三档互斥,优先于自动判定)。
49
+ 不带 flag 时 CLI 按源码规模自动选择:
47
50
 
48
51
  | profile | 触发条件 | 行为 |
49
52
  |---|---|---|
50
- | quick | ≤30 文件 且 ≤80KB 且 ≤3 项目 | 3 步,0 子代理,5 份核心文档 |
51
- | standard | ≤200 文件 且 ≤800KB | 压缩步骤,最多 1 子代理 |
52
- | deep | 大项目或 `--deep` | 完整流程 |
53
+ | quick | `--quick` 或 ≤30 文件 且 ≤80KB 且 ≤3 项目 | 3 步,0 子代理,4 份核心文档 |
54
+ | standard | `--standard` 或 ≤200 文件 且 ≤800KB | 压缩步骤,最多 1 子代理 |
55
+ | deep | `--deep` 或 大项目 | 完整流程 |
56
+
57
+ > **平台快速接入**:对大项目用 `sillyspec run scan --quick`(含平台参数 `--spec-root`/`--workspace-id`/`--scan-run-id`)只生成 4 份核心文档完成接入,后续 `sillyspec run scan --deep` 覆盖升级为完整 7 份。quick 文档 frontmatter 标 `scan_depth: quick`,深度扫描识别后允许覆盖。
53
58
 
54
59
  ### post-check
55
60
 
56
- scan 完成时 CLI 自动校验 7 份文档齐全。缺失会设状态为 `failed_post_check`,阻断进入主流程下游(brainstorm/plan 等),需修复后重跑 scan。
61
+ scan 完成时 CLI **按 profile 校验文档齐全**:quick 模式只要求 4 份核心文档(PROJECT/ARCHITECTURE/CONVENTIONS/STRUCTURE),standard/deep 要求完整 7 份。缺失会设状态为 `failed_post_check`,阻断进入主流程下游(brainstorm/plan 等),需修复后重跑 scan。quick 模式带一条 informational warning(`quick_profile_notice`),状态落 `completed_with_warnings`,不阻断完成。
57
62
 
58
63
  ## 阶段流转
59
64
 
@@ -43,7 +43,7 @@ verify 是只读阶段(**禁止改代码/改 git 状态**,只检查 + 写报
43
43
 
44
44
  - **必须产出 `verify-result.md`**——不存在则阻断完成(不能跳过报告直接 `--done`)
45
45
  - **结论为 `FAIL` 则阻断完成**——不能带着 FAIL 标记 verify 完成
46
- - **`integration-critical` / `deployment-critical` 变更**(design/plan 含 daemon/session/lease/lifecycle 等关键词):结论 PASS WITH NOTES 降级为 FAIL,必须有真实集成证据(Runtime Evidence section)
46
+ - **`integration-critical` / `deployment-critical` 变更**(design/plan 含 daemon/session/lease/lifecycle 等关键词):结论 PASS WITH NOTES 降级为 FAIL,必须有真实集成证据(Runtime Evidence section)——该证据为 Agent 自报告,CLI 仅校验其字面存在、不独立运行时核验,须真实执行过
47
47
  - `verify-required-evidence.json`(execute 写入)中每条 missing evidence → 阻断
48
48
 
49
49
  被阻断时 CLI 打印 ❌ 校验失败,不会提示"验证通过"。修复 `verify-result.md` 后重新 `--done`。
@@ -59,7 +59,7 @@ PASS / PASS WITH NOTES / FAIL ← 必须有结论章节(标题含"结论/
59
59
  ## 探针结果
60
60
  ## 测试结果
61
61
  ## 变更风险等级
62
- ## Runtime Evidence(integration/deployment-critical 必填)
62
+ ## Runtime Evidence(integration/deployment-critical 必填;自报告,CLI 仅字面校验、不独立核验)
63
63
  ```
64
64
 
65
65
  ## 阶段流转
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sillyspec",
3
- "version": "3.25.4",
3
+ "version": "3.25.6",
4
4
  "description": "SillySpec CLI — 流程状态机,让 AI 严格按步骤来",
5
5
  "icon": "logo.jpg",
6
6
  "homepage": "https://sillyspec.ppdmq.top/",
@@ -81,6 +81,43 @@ const FILE_LIST_SECTION_RE = /^#{2,3}\s*(?:\d+[.)]\s*)?(文件变更清单|变
81
81
  */
82
82
  const EXCLUDE_SUBSECTION_RE = /不修改|不变|保留|无变更|未变更|不改动|暂不|暂缓|暂定|待定|后续|未改|无需|不涉及/
83
83
 
84
+ /**
85
+ * operation 词→归一化标签。design 清单「操作」列 cell 与分类列表子标题共用。
86
+ * 用途:verify 删除探针对账——声明「新增/修改」却整文件删除 = 高风险信号。
87
+ */
88
+ const OP_KEYWORDS = [
89
+ { re: /^(新增|新建|创建|添加|add|new|create)$/i, op: '新增' },
90
+ { re: /^(修改|更新|调整|改动|变更|重写|modify|update|change)$/i, op: '修改' },
91
+ { re: /^(删除|移除|去掉|remove|delete|drop)$/i, op: '删除' },
92
+ { re: /^(重命名|改名|rename|move)$/i, op: '重命名' },
93
+ ]
94
+ function classifyOperation(raw) {
95
+ if (!raw) return null
96
+ const hit = OP_KEYWORDS.find(k => k.re.test(raw.trim()))
97
+ return hit ? hit.op : null
98
+ }
99
+
100
+ /**
101
+ * 分类列表子标题 → operation(`### 删除文件` → '删除')。不命中返回 null。
102
+ * ⚠️ 调用方必须先判 EXCLUDE_SUBSECTION_RE:「### 不修改文件」含「修改」二字,
103
+ * 会被此处误匹配为「修改」——exclude 永远优先,命中 exclude 不跑 OP 映射。
104
+ */
105
+ const OP_SUBSECTION_RE = [
106
+ { re: /新增|新建|创建/, op: '新增' },
107
+ { re: /修改|更新|改动|重写/, op: '修改' },
108
+ { re: /删除|移除|去掉/, op: '删除' },
109
+ { re: /重命名|改名/, op: '重命名' },
110
+ ]
111
+ function classifySubsectionOp(title) {
112
+ const hit = OP_SUBSECTION_RE.find(k => k.re.test(title))
113
+ return hit ? hit.op : null
114
+ }
115
+
116
+ /** 表头列定位:cell 整体匹配操作类列名(与 isPathHeaderCell 对偶,用于定位 operation 列) */
117
+ function isOperationHeaderCell(c) {
118
+ return /^(操作|类型|变更类型|改动类型|变更|operation|type|action|op)$/i.test(c)
119
+ }
120
+
84
121
  function isPlaceholder(p) {
85
122
  return !p || p === '—' || p === '-' || /^(n\/?a|无|none|-+)$/i.test(p)
86
123
  }
@@ -91,23 +128,45 @@ function isPathHeaderCell(c) {
91
128
  }
92
129
 
93
130
  /**
94
- * 从 design.md 解析文件变更清单。兼容两种真实写法:
131
+ * 路径合理性兜底:合法路径必含目录分隔 `/` 或扩展名点;
132
+ * 两者皆无时仅保留纯 ASCII 单词 token(Dockerfile / Makefile / LICENSE 这类无扩展名文件名),
133
+ * 丢弃含空格/中文的脏描述(如 design 表格第二列误写的「frontend 组件测试」「后端逻辑」)。
134
+ * 防止把自由文本当路径计入 fileCount,虚高 review-tier 分级(误推 independent)。
135
+ */
136
+ function looksLikePath(p) {
137
+ if (!p) return false
138
+ if (p.includes('/') || p.includes('.')) return true
139
+ return /^[\w@-]+$/.test(p)
140
+ }
141
+
142
+ /**
143
+ * 「顺带修复」标记:design §6 清单里标注的、不属本次变更 task 边界但合规修的预存债文件
144
+ * (CLAUDE.md 规则20 鼓励)。assess 的 allowed_paths 校验对 incidental 文件豁免(坑
145
+ * worktree-execute-apply-friction 坑1/4)。标记载体:表格「说明」列写「顺带修复:xxx」,
146
+ * 或路径 cell / 列表项的括号注释「(顺带修复)」。
147
+ */
148
+ const INCIDENTAL_RE = /顺带修复|附带修复|顺带|drive-?by|incidental/i
149
+
150
+ /**
151
+ * 从 design.md 解析文件变更清单(含 incidental 标记)。兼容两种真实写法:
95
152
  * ① 表格:`| 操作 | 文件路径 | 说明 |`(brainstorm 模板默认)
96
153
  * ② 分类列表:`### 新增文件` / `### 修改文件` / `### 不修改文件` 下的 `- path`
97
154
  * 表头列顺序自适应(定位「文件/路径/file/path」列,列顺序写反时不会把操作名当路径);
98
155
  * 忽略 `.sillyspec/` 与占位符(`—`/`-`/`N/A`/`无`);「不修改/暂缓」子段下的路径会被排除;
99
- * CRLF 容错。
156
+ * CRLF 容错。incidental 嗅探:表格非路径列(说明列)+ 路径 cell 原始值(剥注释前的括号内容)+ 列表项原文。
157
+ *
158
+ * 内核函数:parseFileChangeList(Set 包装,向后兼容)与 parseFileChangeListDetailed 共用,
159
+ * 单一真相源,避免两处各自重写清单解析漂移。
100
160
  * @param {string} designMdPath - design.md 文件路径
101
- * @returns {Set<string>} 文件路径集合(相对路径,如 "src/worktree.js")
161
+ * @returns {Array<{ path: string, operation: string|null, incidental: boolean }>}(顺序按首次出现,exclude 子段移除)
102
162
  */
103
- export function parseFileChangeList(designMdPath) {
104
- const result = new Set()
105
- if (!designMdPath || !existsSync(designMdPath)) return result
163
+ function _parseFileListDetailed(designMdPath) {
164
+ if (!designMdPath || !existsSync(designMdPath)) return []
106
165
 
107
166
  const content = readFileSync(designMdPath, 'utf8').replace(/\r\n/g, '\n')
108
167
 
109
168
  const sectionMatch = content.match(FILE_LIST_SECTION_RE)
110
- if (!sectionMatch) return result
169
+ if (!sectionMatch) return []
111
170
 
112
171
  // 从标题后开始,截取到下一个 ## 标题或文件末尾
113
172
  const afterSection = content.slice(sectionMatch.index + sectionMatch[0].length)
@@ -119,13 +178,24 @@ export function parseFileChangeList(designMdPath) {
119
178
  const lines = relevantContent.split('\n')
120
179
  let headerSkipped = false
121
180
  let pathColIdx = 1 // 默认第 2 列;解析表头后可重定位
181
+ let opColIdx = -1 // 操作列下标(表头扫描后定位;-1 = 无操作列 → operation=null)
122
182
  let listMode = 'include' // include | exclude(分类列表子段)
183
+ let currentOp = null // 分类列表子标题推导的 operation(表格模式不用)
184
+ const entries = new Map() // path -> { path, operation, incidental }(exclude 子段 delete)
123
185
 
124
186
  for (const line of lines) {
125
187
  // 分类列表子标题:### 新增文件 / ### 修改文件 / ### 不修改文件
126
188
  const subHeader = line.match(/^###\s+(.+?)\s*$/)
127
189
  if (subHeader) {
128
- listMode = EXCLUDE_SUBSECTION_RE.test(subHeader[1]) ? 'exclude' : 'include'
190
+ // ⚠️ EXCLUDE 优先:「### 不修改文件」含「修改」二字,会被 classifySubsectionOp 误匹配。
191
+ // 先判 exclude 词集,命中则 exclude 且 currentOp=null,绝不跑 OP 映射。
192
+ if (EXCLUDE_SUBSECTION_RE.test(subHeader[1])) {
193
+ listMode = 'exclude'
194
+ currentOp = null
195
+ } else {
196
+ listMode = 'include'
197
+ currentOp = classifySubsectionOp(subHeader[1])
198
+ }
129
199
  continue
130
200
  }
131
201
 
@@ -140,6 +210,9 @@ export function parseFileChangeList(designMdPath) {
140
210
  // 表头列定位:找「文件/路径/file/path」列,找不到保持默认第 2 列
141
211
  const idx = cells.findIndex(isPathHeaderCell)
142
212
  if (idx >= 0) pathColIdx = idx
213
+ // 操作列定位:非路径列里找操作类列名(「操作/类型/operation」),找不到 opColIdx 保持 -1
214
+ const opIdx = cells.findIndex((c, i) => i !== pathColIdx && isOperationHeaderCell(c))
215
+ if (opIdx >= 0) opColIdx = opIdx
143
216
  continue
144
217
  }
145
218
 
@@ -147,7 +220,14 @@ export function parseFileChangeList(designMdPath) {
147
220
  // 列定位兜底:取到纯操作词(表头未命中且列顺序异常)→ 跳过,避免把「修改」当路径
148
221
  if (/^(新增|修改|删除|重命名|new|modify|update|delete|create|rename)$/i.test(filePath)) continue
149
222
  if (isPlaceholder(filePath) || filePath.startsWith('.sillyspec/')) continue
150
- result.add(filePath)
223
+ if (!looksLikePath(filePath)) continue // 脏描述兜底:丢弃非路径自由文本(防虚高 fileCount)
224
+ // operation:操作列 cell 分类(无操作列 → null)
225
+ const operation = opColIdx >= 0 ? classifyOperation(cells[opColIdx] || '') : null
226
+ // incidental:说明列(非路径列的所有 cell)+ 路径 cell 原始值(剥注释前的括号内容)
227
+ const incidental = cells.some((c, i) => i !== pathColIdx && INCIDENTAL_RE.test(c))
228
+ || INCIDENTAL_RE.test(cells[pathColIdx] || '')
229
+ if (listMode === 'exclude') { entries.delete(filePath); continue }
230
+ entries.set(filePath, { path: filePath, operation, incidental })
151
231
  continue
152
232
  }
153
233
 
@@ -156,10 +236,32 @@ export function parseFileChangeList(designMdPath) {
156
236
  if (listItem) {
157
237
  const filePath = normalizePath(listItem[1])
158
238
  if (isPlaceholder(filePath) || filePath.startsWith('.sillyspec/')) continue
159
- if (listMode === 'exclude') result.delete(filePath)
160
- else result.add(filePath)
239
+ if (!looksLikePath(filePath)) continue // 脏描述兜底
240
+ const incidental = INCIDENTAL_RE.test(listItem[1])
241
+ if (listMode === 'exclude') { entries.delete(filePath); continue }
242
+ entries.set(filePath, { path: filePath, operation: currentOp, incidental })
161
243
  }
162
244
  }
163
245
 
164
- return result
246
+ return [...entries.values()]
247
+ }
248
+
249
+ /**
250
+ * 从 design.md 解析文件变更清单(路径集合,向后兼容)。
251
+ * @param {string} designMdPath - design.md 文件路径
252
+ * @returns {Set<string>} 文件路径集合(相对路径,如 "src/worktree.js")
253
+ */
254
+ export function parseFileChangeList(designMdPath) {
255
+ return new Set(_parseFileListDetailed(designMdPath).map(e => e.path))
256
+ }
257
+
258
+ /**
259
+ * 从 design.md 解析文件变更清单(含 operation + incidental 标记)。
260
+ * operation 供 verify 删除探针对账(声明「新增/修改」却整文件删除 = 高风险);
261
+ * incidental 供 assess allowed_paths 豁免。
262
+ * @param {string} designMdPath - design.md 文件路径
263
+ * @returns {Array<{ path: string, operation: string|null, incidental: boolean }>}
264
+ */
265
+ export function parseFileChangeListDetailed(designMdPath) {
266
+ return _parseFileListDetailed(designMdPath)
165
267
  }
@@ -290,14 +290,49 @@ export const VERIFICATION_NEEDS = {
290
290
  export const RISK_LEVEL_CAUSES = {
291
291
  'deployment-critical':
292
292
  'design.md / plan.md 命中启动入口关键词(cli.ts / main.ts / server.(js|ts) / bootstrap / entrypoint)。' +
293
- '注意:这是按 design/plan 里的措辞判定的,不一定代表你真改了启动入口;若属误判可在 design 中如实缩小范围,但不建议为绕门控而规避——危险链路该有真实启动证据。',
293
+ '注意:这是按 design/plan 里的措辞判定的,不一定代表你真改了启动入口;若属误判可在 design.md frontmatter 用 risk_level 显式声明真实等级(如 unit-sufficient)覆盖——不要靠删措辞绕门控,危险链路该有真实启动证据。',
294
294
  'integration-critical':
295
295
  'design.md / plan.md 命中跨进程/状态机关键词(daemon / backend / session / lease / lifecycle / heartbeat 等)。',
296
296
  'contract-required': 'design.md / plan.md 命中 API contract 关键词(api / client / contract / dto)。',
297
+ 'explicit': 'design.md frontmatter 的 risk_level 显式声明(覆盖关键词判级)。',
298
+ }
299
+
300
+ /** design.md frontmatter 可显式声明的合法 risk_level 值(与 detectChangeRisk 判级结果同集合) */
301
+ export const RISK_LEVELS = ['doc-only', 'unit-sufficient', 'contract-required', 'integration-critical', 'deployment-critical']
302
+
303
+ /**
304
+ * 从 design.md 顶部 frontmatter 提取显式 risk_level 声明。
305
+ * 只认文档最开头 `---\n...\n---` 块内的 `risk_level: <level>` 单行(与 plan.md 的 plan_level 同款解析),
306
+ * 不扫正文——避免正文里讨论 risk_level 措辞时被误当声明。返回合法 level 或 null。
307
+ */
308
+ export function extractExplicitRiskLevel(designContent = '') {
309
+ const fm = designContent.match(/^---\r?\n([\s\S]*?)\r?\n---/)
310
+ if (!fm) return null
311
+ const line = fm[1].split('\n').map(l => l.trim()).find(l => l.startsWith('risk_level:'))
312
+ if (!line) return null
313
+ const value = line.slice('risk_level:'.length).trim().replace(/^["']|["']$/g, '').toLowerCase()
314
+ return RISK_LEVELS.includes(value) ? value : null
297
315
  }
298
316
 
299
317
  export function detectChangeRisk({ designContent = '', planContent = '', changedFiles = [] } = {}) {
300
318
  const triggers = []
319
+
320
+ // ── 显式豁免优先:design.md frontmatter 声明 risk_level → 以声明为准,跳过关键词判级 ──
321
+ // 历史教训:detectChangeRisk 是机械字面匹配,不认否定语境——design 写「本次不改动 daemon/session」
322
+ // 仍命中关键词被误判 integration/deployment-critical,强制要全套集成证据,对无状态后端变更误伤。
323
+ // 与其在正则层做脆弱的否定识别,不如给一条显式、诚实、可审计(落在 design frontmatter + verify-result)
324
+ // 的覆盖通道。声明仍走门控(结论 FAIL 仍拦),但豁免级(unit-sufficient 等)不再强制集成证据。
325
+ const explicit = extractExplicitRiskLevel(designContent)
326
+ if (explicit) {
327
+ const requiredVerification =
328
+ explicit === 'deployment-critical' ? ['unit_tests', 'contract_tests', 'real_daemon_backend_integration', 'runtime_log_evidence', 'real_startup_once'] :
329
+ explicit === 'integration-critical' ? ['unit_tests', 'contract_tests', 'real_daemon_backend_integration', 'runtime_log_evidence', 'terminal_state_assertion'] :
330
+ explicit === 'contract-required' ? ['unit_tests', 'contract_tests'] :
331
+ explicit === 'doc-only' ? ['static_check'] :
332
+ ['unit_tests']
333
+ return { level: explicit, triggers: ['risk_level (explicit)'], requiredVerification, explicit: true }
334
+ }
335
+
301
336
  const combined = [designContent, planContent].join('\n')
302
337
 
303
338
  for (const pattern of INTEGRATION_CRITICAL_PATTERNS) {
package/src/constants.js CHANGED
@@ -81,6 +81,17 @@ export const SCAN_REQUIRED_DOCS = Object.freeze([
81
81
  'PROJECT.md',
82
82
  ])
83
83
 
84
+ // ── scan quick profile 必需文档子集(4 份核心文档)──
85
+ // quick 档(--quick 显式 或 小项目自动判定)只生成这 4 份;scan-postcheck 按 profile 取清单:
86
+ // mode==='quick' → SCAN_REQUIRED_DOCS_QUICK,否则 → SCAN_REQUIRED_DOCS(7 份)。
87
+ // 由 scan-postcheck.js + complete-handlers.js 透传的 scanProfile.mode 决定取哪份。
88
+ export const SCAN_REQUIRED_DOCS_QUICK = Object.freeze([
89
+ 'PROJECT.md',
90
+ 'ARCHITECTURE.md',
91
+ 'CONVENTIONS.md',
92
+ 'STRUCTURE.md',
93
+ ])
94
+
84
95
  // ── 辅助 stage 清单(stage-contract 转移规则 与 stages/index 注册表 共用单一真相源)──
85
96
  // 曾在 stage-contract.js 与 stages/index.js 逐字重复定义,新增/调整辅助 stage 需同步改两处
86
97
  // 易分叉。现收敛一处。(注意:主流程序 STAGE_ORDER/mainFlowStages/flowStages 语义不同、
package/src/index.js CHANGED
@@ -54,7 +54,7 @@ SillySpec CLI — 规范驱动开发工具包
54
54
  --allow-new 允许新增文件(默认禁止)
55
55
  --force-baseline 允许覆盖 baseline 受保护文件
56
56
  --confirm 完成时确认接受变更审计
57
- scan: --deep 强制 deep 扫描 profile
57
+ scan: --quick | --standard | --deep 显式选择 profile(优先于规模自动判定;三档互斥)
58
58
  --force-rescan 覆盖已有 scan 文档保护
59
59
  archive: --confirm 归档确认(必须)
60
60
  auto: --mode <模式> 显式指定流程模式
@@ -1226,6 +1226,9 @@ SillySpec modules — 模块文档管理
1226
1226
  lines.push('# module_paths:');
1227
1227
  lines.push('# user-service: "user/"');
1228
1228
  lines.push('# order-service: "order/"');
1229
+ lines.push('# 已知预存失败(可选):verify 实测时这些失败被豁免、不阻断归档;fail-safe 仅豁免能检测到的失败行');
1230
+ lines.push('# known_failures:');
1231
+ lines.push('# - "tests/test_legacy.py::test_old"');
1229
1232
  const yamlText = lines.join('\n') + '\n';
1230
1233
 
1231
1234
  // 原子写(tmp + rename + Windows EPERM 重试),避免半截 local.yaml
@@ -225,7 +225,11 @@ export async function runGate(stage, changeName, { cwd, specBase, runtimeRoot }
225
225
  const vt = runVerifyTestCheck({ cwd, specBase: specRoot, changeName });
226
226
  const vtWarnings = [];
227
227
  if (vt.status === 'skipped') {
228
- vtWarnings.push('⚠️ verify-test SKIPPED — gate 未核验测试(local.yaml 未配置 commands.test 或显式无测试)。本次 gate 结论不含测试客观核验,driver 不应据 exit 0 判定测试通过;integration-critical 变更应在 verify 阶段降级 FAIL');
228
+ vtWarnings.push(`⚠️ verify-test SKIPPED — gate 未核验测试(${vt.reason || 'local.yaml 未配置 commands.test 或显式无测试'})。本次 gate 结论不含测试客观核验,driver 不应据 exit 0 判定测试通过;integration-critical 变更应在 verify 阶段降级 FAIL`);
229
+ }
230
+ // known_failures 豁免披露:本次 PASS 含豁免,driver 应提示人工复核清单是否过宽
231
+ if (vt.exemptedCount > 0) {
232
+ vtWarnings.push(`⚠️ verify-test PASS 含 ${vt.exemptedCount} 个 known_failures 豁免(${vt.reason || ''});driver 应提示人工复核豁免清单是否过宽(避免误豁免本变更引入的真实失败)`);
229
233
  }
230
234
  // 全量 fallback 明示:跑了全量 commands.test 但非变更范围子集,失败可能含未变更
231
235
  // 模块的预存错误——driver 不应据 exit 0 判定本次变更范围已客观测试(见 3.24 verify 坑1)。
@@ -287,22 +287,25 @@ export class StageMachine {
287
287
  }
288
288
  }
289
289
 
290
- // 找到第一个 pending 主流程阶段
290
+ // 找主流程里第一个「未完成且上游已就绪」的阶段,推荐它作为下一步。
291
+ // 修两个缺陷(曾导致 brainstorm 完成后误推 archive):
292
+ // ① 原 steps.length>0 要求:plan/execute/verify 首次 run 前惰性未初始化(steps 空)被跳过;
293
+ // ② 原 upstream 把 pending 当就绪:archive 的上游 plan/execute/verify 即便 pending 也算 ok。
294
+ // 叠加 → 循环跳过未初始化的中间阶段,漏到 archive(steps 非空)误推,违反流程顺序。
295
+ // 改为:不要求 steps(空=该开始了);upstream 必须全 completed(pending 不算就绪)——
296
+ // archive 只在 scan/brainstorm/plan/execute/verify 全完成后才可能被推荐。
291
297
  for (const s of STAGE_ORDER) {
292
298
  const sd = data.stages[s];
293
- if (sd && sd.status === 'pending' && sd.steps && sd.steps.length > 0) {
294
- // 检查上游是否都 completed
295
- const idx = STAGE_ORDER.indexOf(s);
296
- const upstream = STAGE_ORDER.slice(0, idx);
297
- const upstreamOk = upstream.every(us =>
298
- data.stages[us]?.status === 'completed' || !data.stages[us] || data.stages[us].status === 'pending'
299
- );
300
- if (upstreamOk) {
301
- return {
302
- text: `可以开始 ${STAGE_LABELS[s] || s}。`,
303
- command: `sillyspec run ${s}`,
304
- };
305
- }
299
+ if (!sd) continue;
300
+ if (sd.status === 'completed' || sd.status === 'skipped') continue;
301
+ const idx = STAGE_ORDER.indexOf(s);
302
+ const upstream = STAGE_ORDER.slice(0, idx);
303
+ const upstreamOk = upstream.every(us => data.stages[us]?.status === 'completed');
304
+ if (upstreamOk) {
305
+ return {
306
+ text: `可以开始 ${STAGE_LABELS[s] || s}。`,
307
+ command: `sillyspec run ${s}`,
308
+ };
306
309
  }
307
310
  }
308
311