sillyspec 3.20.2 → 3.20.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 (208) hide show
  1. package/.claude/skills/sillyspec-archive/SKILL.md +1 -5
  2. package/.claude/skills/sillyspec-auto/SKILL.md +2 -8
  3. package/.claude/skills/sillyspec-brainstorm/SKILL.md +1 -28
  4. package/.claude/skills/sillyspec-commit/SKILL.md +3 -4
  5. package/.claude/skills/sillyspec-continue/SKILL.md +4 -5
  6. package/.claude/skills/sillyspec-doctor/SKILL.md +3 -12
  7. package/.claude/skills/sillyspec-execute/SKILL.md +2 -15
  8. package/.claude/skills/sillyspec-explore/SKILL.md +1 -14
  9. package/.claude/skills/sillyspec-plan/SKILL.md +36 -5
  10. package/.claude/skills/sillyspec-propose/SKILL.md +0 -4
  11. package/.claude/skills/sillyspec-quick/SKILL.md +1 -5
  12. package/.claude/skills/sillyspec-resume/SKILL.md +66 -23
  13. package/.claude/skills/sillyspec-scan/SKILL.md +1 -5
  14. package/.claude/skills/sillyspec-state/SKILL.md +8 -8
  15. package/.claude/skills/sillyspec-status/SKILL.md +1 -5
  16. package/.claude/skills/sillyspec-verify/SKILL.md +1 -5
  17. package/.claude/skills/sillyspec-workspace/SKILL.md +3 -11
  18. package/.sillyspec/changes/archive/2026-04-08-derive-state/design.md +97 -0
  19. package/.sillyspec/changes/archive/2026-04-08-derive-state/plan.md +51 -0
  20. package/.sillyspec/changes/archive/2026-04-08-derive-state/proposal.md +29 -0
  21. package/.sillyspec/changes/archive/2026-04-08-derive-state/requirements.md +34 -0
  22. package/.sillyspec/changes/archive/2026-04-08-derive-state/tasks.md +13 -0
  23. package/.sillyspec/changes/archive/2026-04-08-derive-state/verify-result.md +43 -0
  24. package/.sillyspec/changes/auto-mode/design.md +50 -0
  25. package/.sillyspec/changes/auto-mode/proposal.md +19 -0
  26. package/.sillyspec/changes/auto-mode/requirements.md +21 -0
  27. package/.sillyspec/changes/auto-mode/tasks.md +7 -0
  28. package/.sillyspec/changes/brainstorm-archive/2026-04-05-dashboard-design.md +206 -0
  29. package/.sillyspec/changes/brainstorm-archive/2026-04-05-unified-docs-design.md +199 -0
  30. package/.sillyspec/changes/dashboard/design.md +219 -0
  31. package/.sillyspec/changes/dashboard/design.md.braindraft +206 -0
  32. package/.sillyspec/changes/run-command-design/design.md +1230 -0
  33. package/.sillyspec/changes/unified-docs-design/design.md +199 -0
  34. package/.sillyspec/docs/sillyspec/scan/.gitkeep +0 -0
  35. package/.sillyspec/knowledge/INDEX.md +8 -0
  36. package/.sillyspec/knowledge/uncategorized.md +3 -0
  37. package/.sillyspec/plans/2026-04-05-dashboard.md +737 -0
  38. package/.sillyspec/projects/sillyspec.yaml +3 -0
  39. package/README.md +11 -13
  40. package/SKILL.md +40 -44
  41. package/dist/steps/brainstorm/01-load-context.md +30 -0
  42. package/dist/steps/brainstorm/02-reuse-check.md +6 -0
  43. package/dist/steps/brainstorm/03-prototype-analysis.md +11 -0
  44. package/dist/steps/brainstorm/04-module-split.md +23 -0
  45. package/dist/steps/brainstorm/05-dialog-explore.md +8 -0
  46. package/dist/steps/brainstorm/06-propose-approaches.md +3 -0
  47. package/dist/steps/brainstorm/07-present-design.md +3 -0
  48. package/dist/steps/brainstorm/08-write-design.md +21 -0
  49. package/dist/steps/brainstorm/09-self-review.md +15 -0
  50. package/dist/steps/brainstorm/10-user-confirm.md +3 -0
  51. package/dist/steps/brainstorm/11-output-spec.md +7 -0
  52. package/dist/steps/brainstorm/manifest.yaml +26 -0
  53. package/dist/steps/execute/01-load-context.md +41 -0
  54. package/dist/steps/execute/02-scan-conventions.md +47 -0
  55. package/dist/steps/execute/03-skill-mcp.md +19 -0
  56. package/dist/steps/execute/04-assign-task.md +22 -0
  57. package/dist/steps/execute/04b-prompt-template.md +54 -0
  58. package/dist/steps/execute/05-write-test.md +7 -0
  59. package/dist/steps/execute/06-write-code.md +8 -0
  60. package/dist/steps/execute/07-run-test.md +26 -0
  61. package/dist/steps/execute/08-fix-issues.md +28 -0
  62. package/dist/steps/execute/09-next-task.md +33 -0
  63. package/dist/steps/execute/manifest.yaml +28 -0
  64. package/dist/steps/plan/01-load-context.md +22 -0
  65. package/dist/steps/plan/02-anchor-confirm.md +1 -0
  66. package/dist/steps/plan/03-expand-tasks.md +33 -0
  67. package/dist/steps/plan/04-mark-order.md +15 -0
  68. package/dist/steps/plan/05-e2e-planning.md +17 -0
  69. package/dist/steps/plan/06-self-check.md +16 -0
  70. package/dist/steps/plan/07-save.md +1 -0
  71. package/dist/steps/plan/manifest.yaml +18 -0
  72. package/dist/steps/scan/01-env-detect.md +51 -0
  73. package/dist/steps/scan/02-tech-stack.md +16 -0
  74. package/dist/steps/scan/03-conventions.md +16 -0
  75. package/dist/steps/scan/04-structure.md +19 -0
  76. package/dist/steps/scan/05-quality.md +18 -0
  77. package/dist/steps/scan/06-complete.md +49 -0
  78. package/dist/steps/scan/manifest.yaml +16 -0
  79. package/dist/steps/verify/01-load-specs.md +28 -0
  80. package/dist/steps/verify/02-check-tasks.md +1 -0
  81. package/dist/steps/verify/03-check-design.md +6 -0
  82. package/dist/steps/verify/04-run-tests.md +7 -0
  83. package/dist/steps/verify/05-e2e-tests.md +27 -0
  84. package/dist/steps/verify/05b-e2e-fix.md +33 -0
  85. package/dist/steps/verify/06-code-quality.md +25 -0
  86. package/dist/steps/verify/07-lint-check.md +27 -0
  87. package/dist/steps/verify/08-output-report.md +14 -0
  88. package/dist/steps/verify/manifest.yaml +22 -0
  89. package/package.json +1 -7
  90. package/packages/dashboard/dist/assets/{index-Bq_Z2hne.js → index-D1EVTLmc.js} +1264 -1264
  91. package/packages/dashboard/dist/assets/index-DGe8CqeP.css +1 -0
  92. package/packages/dashboard/dist/index.html +2 -2
  93. package/packages/dashboard/package-lock.json +6 -6
  94. package/packages/dashboard/server/executor.js +1 -1
  95. package/packages/dashboard/server/index.js +18 -98
  96. package/packages/dashboard/server/parser.js +71 -139
  97. package/packages/dashboard/server/watcher.js +6 -14
  98. package/packages/dashboard/src/App.vue +185 -418
  99. package/packages/dashboard/src/components/ActionBar.vue +1 -10
  100. package/packages/dashboard/src/components/CommandPalette.vue +1 -5
  101. package/packages/dashboard/src/components/DocPreview.vue +8 -105
  102. package/packages/dashboard/src/components/DocTree.vue +19 -75
  103. package/packages/dashboard/src/components/PipelineStage.vue +2 -22
  104. package/packages/dashboard/src/components/PipelineView.vue +5 -32
  105. package/packages/dashboard/src/components/ProjectOverview.vue +139 -113
  106. package/packages/dashboard/src/components/StageBadge.vue +3 -17
  107. package/packages/dashboard/src/components/StepCard.vue +2 -7
  108. package/packages/dashboard/src/composables/useDashboard.js +0 -28
  109. package/src/derive.js +147 -0
  110. package/src/index.js +40 -688
  111. package/src/init.js +63 -119
  112. package/src/migrate.js +7 -7
  113. package/src/progress.js +248 -1474
  114. package/src/run.js +302 -3008
  115. package/src/setup.js +64 -2
  116. package/src/stages/archive.js +17 -123
  117. package/src/stages/brainstorm.js +48 -454
  118. package/src/stages/doctor.js +46 -99
  119. package/src/stages/execute.js +59 -420
  120. package/src/stages/index.js +18 -12
  121. package/src/stages/plan.js +189 -492
  122. package/src/stages/propose.js +11 -70
  123. package/src/stages/quick.js +13 -52
  124. package/src/stages/scan.js +68 -485
  125. package/src/stages/status.js +1 -1
  126. package/src/stages/verify.js +16 -203
  127. package/src/step.js +543 -0
  128. package/.claude/skills/sillyspec-knowledge/SKILL.md +0 -270
  129. package/.husky/pre-push +0 -13
  130. package/CLAUDE.md +0 -18
  131. package/docs/brainstorm-plan-contract.md +0 -64
  132. package/docs/plan-execute-contract.md +0 -123
  133. package/docs/platform-scan-protocol.md +0 -298
  134. package/docs/revision-mode.md +0 -115
  135. package/docs/sillyspec/file-lifecycle/known-implementation-gaps.md +0 -99
  136. package/docs/sillyspec/file-lifecycle/platform-workflows-sync.md +0 -218
  137. package/docs/sillyspec/file-lifecycle/stage-artifacts.md +0 -167
  138. package/docs/sillyspec/file-lifecycle/storage-and-state.md +0 -148
  139. package/docs/sillyspec/file-lifecycle/worktree-and-guard.md +0 -193
  140. package/docs/sillyspec/file-lifecycle.md +0 -125
  141. package/docs/workflow-contract-regression.md +0 -106
  142. package/docs/worktree-isolation.md +0 -252
  143. package/packages/dashboard/dist/assets/index-O2W5RV4z.css +0 -1
  144. package/packages/dashboard/dist/prototype-dashboard.html +0 -836
  145. package/packages/dashboard/dist/prototype-overview.html +0 -256
  146. package/packages/dashboard/public/prototype-dashboard.html +0 -836
  147. package/packages/dashboard/public/prototype-overview.html +0 -256
  148. package/packages/dashboard/src/components/HResizeHandle.vue +0 -48
  149. package/packages/dashboard/src/components/ProjectCard.vue +0 -187
  150. package/packages/dashboard/src/components/VResizeHandle.vue +0 -61
  151. package/packages/dashboard/src/composables/useLayout.js +0 -131
  152. package/src/brainstorm-postcheck.js +0 -158
  153. package/src/change-list.js +0 -52
  154. package/src/change-risk-profile.js +0 -352
  155. package/src/classify-change.js +0 -73
  156. package/src/constants.js +0 -70
  157. package/src/contract-matrix.js +0 -278
  158. package/src/db.js +0 -201
  159. package/src/endpoint-extractor.js +0 -315
  160. package/src/hooks/claude-pre-tool-use.cjs +0 -125
  161. package/src/hooks/worktree-guard.js +0 -761
  162. package/src/knowledge-match.js +0 -130
  163. package/src/modules.js +0 -482
  164. package/src/scan-postcheck.js +0 -383
  165. package/src/stage-contract.js +0 -700
  166. package/src/stages/brainstorm-auto.js +0 -229
  167. package/src/stages/explore.js +0 -34
  168. package/src/stages/knowledge.js +0 -498
  169. package/src/stages/plan-postcheck.js +0 -513
  170. package/src/sync.js +0 -497
  171. package/src/task-review.js +0 -346
  172. package/src/workflow.js +0 -785
  173. package/src/worktree-apply.js +0 -549
  174. package/src/worktree.js +0 -932
  175. package/templates/workflows/archive-impact.yaml +0 -79
  176. package/templates/workflows/scan-docs.yaml +0 -132
  177. package/test/brainstorm-plan-contract.test.mjs +0 -273
  178. package/test/check-syntax.mjs +0 -26
  179. package/test/cli-top-level-aliases.test.mjs +0 -174
  180. package/test/contract-artifacts.test.mjs +0 -323
  181. package/test/decision-supersede.test.mjs +0 -277
  182. package/test/knowledge-match.test.mjs +0 -231
  183. package/test/plan-execute-contract.test.mjs +0 -330
  184. package/test/plan-optimization.test.mjs +0 -572
  185. package/test/platform-artifacts.test.mjs +0 -190
  186. package/test/platform-failure-samples.test.mjs +0 -199
  187. package/test/platform-recovery-chain.test.mjs +0 -179
  188. package/test/platform-recovery.test.mjs +0 -167
  189. package/test/platform-scan-p0.test.mjs +0 -175
  190. package/test/revision-v1.test.mjs +0 -1145
  191. package/test/run-sanitize-project-name.test.mjs +0 -51
  192. package/test/run-scan-postcheck-fail.test.mjs +0 -64
  193. package/test/run-scan-project-parse.test.mjs +0 -200
  194. package/test/run-tests.mjs +0 -48
  195. package/test/scan-docs-yaml-placeholders.test.mjs +0 -84
  196. package/test/scan-knowledge.test.mjs +0 -175
  197. package/test/scan-paths.test.mjs +0 -68
  198. package/test/scan-postcheck-project-priority.test.mjs +0 -85
  199. package/test/scan-postcheck.test.mjs +0 -197
  200. package/test/scan-workflow-anyfailed-block.test.mjs +0 -52
  201. package/test/spec-dir.test.mjs +0 -206
  202. package/test/stage-contract-failed-post-check.test.mjs +0 -102
  203. package/test/stage-contract.test.mjs +0 -299
  204. package/test/stage-definitions.test.mjs +0 -39
  205. package/test/wait-gates.test.mjs +0 -501
  206. package/test/workflow-spec-base.test.mjs +0 -142
  207. package/test/worktree-guard.test.mjs +0 -136
  208. package/test/worktree-native-overlay.test.mjs +0 -188
@@ -1,513 +0,0 @@
1
- /**
2
- * Plan Postcheck 模块
3
- *
4
- * 从 run.js 和 plan.js 拆出的确定性校验逻辑:
5
- * - 拓扑排序(topoSortWaves)
6
- * - 蓝图一致性校验(validateBlueprintConsistency)
7
- * - plan 产物校验(validatePlanArtifacts)
8
- * - 执行入口(executePlanPostcheck)
9
- *
10
- * 这些都是 noAI 步骤,不需要 LLM 参与。
11
- */
12
- import { existsSync, readFileSync, readdirSync } from 'fs'
13
- import { join as pJoin } from 'path'
14
-
15
- // ═══════════════════════════════════════════════════════════════
16
- // 解析工具(从 plan.js 迁移)
17
- // ═══════════════════════════════════════════════════════════════
18
-
19
- /**
20
- * 从 task-NN.md frontmatter 解析 depends_on 字段
21
- * @param {string} content - task 文件内容
22
- * @returns {string[]}
23
- */
24
- function parseDependsOn(content) {
25
- const fmMatch = content.match(/^---\n([\s\S]*?)\n---/)
26
- if (!fmMatch) return []
27
- const fm = fmMatch[1]
28
- const inlineMatch = fm.match(/depends_on:\s*\[([^\]]*)\]/)
29
- if (inlineMatch) {
30
- return inlineMatch[1].split(',').map(s => s.trim().replace(/['"]/g, '')).filter(Boolean)
31
- }
32
- const blockMatch = fm.match(/depends_on:\s*\n((?:\s+-\s+.+\n?)+)/)
33
- if (blockMatch) {
34
- return blockMatch[1].match(/-\s+(.+)/g)?.map(s => s.replace(/^-\s+/, '').trim().replace(/['"]/g, '')) || []
35
- }
36
- return []
37
- }
38
-
39
- /**
40
- * 解析 task-NN.md 的 task id(从 frontmatter 或文件名)
41
- * @param {string} content - task 文件内容
42
- * @param {string} filename - 文件名
43
- * @returns {string|null}
44
- */
45
- function parseTaskId(content, filename) {
46
- const fmMatch = content.match(/^---\n([\s\S]*?)\n---/)
47
- if (fmMatch) {
48
- const idMatch = fmMatch[1].match(/^id:\s*(.+)/m)
49
- if (idMatch) return idMatch[1].trim()
50
- }
51
- const fileMatch = filename.match(/(task-\d+)/i)
52
- return fileMatch ? fileMatch[1] : null
53
- }
54
-
55
- /**
56
- * 解析 task-NN.md 的 allowed_paths
57
- * @param {string} content
58
- * @returns {string[]}
59
- */
60
- function parseAllowedPaths(content) {
61
- const fmMatch = content.match(/^---\n([\s\S]*?)\n---/)
62
- if (!fmMatch) return []
63
- const fm = fmMatch[1]
64
- const inlineMatch = fm.match(/allowed_paths:\s*\[([^\]]*)\]/)
65
- if (inlineMatch) {
66
- return inlineMatch[1].split(',').map(s => s.trim().replace(/['"]/g, '')).filter(Boolean)
67
- }
68
- const blockMatch = fm.match(/allowed_paths:\s*\n((?:\s+-\s+.+\n?)+)/)
69
- if (blockMatch) {
70
- return blockMatch[1].match(/-\s+(.+)/g)?.map(s => s.replace(/^-\s+/, '').trim().replace(/['"]/g, '')) || []
71
- }
72
- return []
73
- }
74
-
75
- /**
76
- * 检查 task-NN.md 是否包含验收标准章节
77
- * @param {string} content
78
- * @returns {boolean}
79
- */
80
- function hasAcceptanceCriteria(content) {
81
- return /##\s*验收标准/.test(content) || /##\s*Acceptance/.test(content)
82
- }
83
-
84
- /**
85
- * 检查 task-NN.md 是否包含 TDD/验证步骤
86
- * @param {string} content
87
- * @returns {boolean}
88
- */
89
- function hasTddOrVerify(content) {
90
- return /##\s*TDD/.test(content) || /##\s*验证/.test(content) || /##\s*Verify/.test(content)
91
- }
92
-
93
- // ═══════════════════════════════════════════════════════════════
94
- // 核心逻辑
95
- // ═══════════════════════════════════════════════════════════════
96
-
97
- /**
98
- * 拓扑排序:根据 depends_on 计算波次
99
- * @param {Map<string, string[]>} depMap - taskId → depends_on list
100
- * @returns {{ waves: string[][], error: string|null }}
101
- */
102
- export function topoSortWaves(depMap) {
103
- const tasks = [...depMap.keys()]
104
- const waves = []
105
- const assigned = new Set()
106
- const visited = new Set()
107
-
108
- // 先做循环依赖检测(DFS)
109
- function hasCycle(task, path) {
110
- if (path.has(task)) return true
111
- if (visited.has(task)) return false
112
- path.add(task)
113
- const deps = depMap.get(task) || []
114
- for (const dep of deps) {
115
- if (!depMap.has(dep)) continue // 依赖不存在(可能是外部引用),跳过
116
- if (hasCycle(dep, path)) return true
117
- }
118
- path.delete(task)
119
- visited.add(task)
120
- return false
121
- }
122
- for (const task of tasks) {
123
- if (hasCycle(task, new Set())) {
124
- return { waves: [], error: `检测到循环依赖,涉及 task: ${task}` }
125
- }
126
- }
127
-
128
- // 逐层分配 Wave
129
- while (assigned.size < tasks.length) {
130
- const currentWave = []
131
- for (const task of tasks) {
132
- if (assigned.has(task)) continue
133
- const deps = (depMap.get(task) || []).filter(d => depMap.has(d))
134
- if (deps.every(d => assigned.has(d))) {
135
- currentWave.push(task)
136
- }
137
- }
138
- if (currentWave.length === 0) {
139
- const remaining = tasks.filter(t => !assigned.has(t))
140
- return { waves: [], error: `无法解析依赖关系,剩余 task: ${remaining.join(', ')}` }
141
- }
142
- for (const task of currentWave) {
143
- assigned.add(task)
144
- }
145
- waves.push(currentWave)
146
- }
147
-
148
- return { waves, error: null }
149
- }
150
-
151
- /**
152
- * 本地一致性校验器
153
- * @param {string} changeDir - 变更目录
154
- * @returns {{ ok: boolean, errors: string[], warnings: string[] }}
155
- */
156
- export function validateBlueprintConsistency(changeDir) {
157
- const errors = []
158
- const warnings = []
159
-
160
- const tasksDir = pJoin(changeDir, 'tasks')
161
- if (!existsSync(tasksDir)) {
162
- return { ok: false, errors: ['tasks/ 目录不存在'], warnings }
163
- }
164
-
165
- const taskFiles = readdirSync(tasksDir).filter(f => /^task-\d+\.md$/.test(f))
166
- if (taskFiles.length === 0) {
167
- return { ok: false, errors: ['tasks/ 目录下没有 task-NN.md 文件'], warnings }
168
- }
169
-
170
- const taskInfo = new Map()
171
- const pathOwners = new Map()
172
-
173
- for (const file of taskFiles) {
174
- const filePath = pJoin(tasksDir, file)
175
- const content = readFileSync(filePath, 'utf8')
176
- const taskId = parseTaskId(content, file)
177
- if (!taskId) {
178
- errors.push(`${file}: 无法解析 task id`)
179
- continue
180
- }
181
-
182
- const dependsOn = parseDependsOn(content)
183
- const allowedPaths = parseAllowedPaths(content)
184
- const hasAcceptance = hasAcceptanceCriteria(content)
185
- const hasTdd = hasTddOrVerify(content)
186
-
187
- taskInfo.set(taskId, { dependsOn, allowedPaths, hasAcceptance, hasTdd, file })
188
-
189
- if (allowedPaths.length === 0) {
190
- errors.push(`${taskId} (${file}): 缺少 allowed_paths`)
191
- }
192
- if (!hasAcceptance) {
193
- errors.push(`${taskId} (${file}): 缺少「验收标准」章节`)
194
- }
195
- if (!hasTdd) {
196
- warnings.push(`${taskId} (${file}): 缺少 TDD/验证步骤章节`)
197
- }
198
-
199
- for (const p of allowedPaths) {
200
- if (!pathOwners.has(p)) pathOwners.set(p, [])
201
- pathOwners.get(p).push(taskId)
202
- }
203
- }
204
-
205
- // 路径冲突
206
- for (const [p, owners] of pathOwners) {
207
- if (owners.length > 1) {
208
- warnings.push(`路径 ${p} 被 ${owners.length} 个 task 修改: ${owners.join(', ')}(确认是否为有意共享)`)
209
- }
210
- }
211
-
212
- // 拓扑排序 + 循环依赖
213
- const depMap = new Map()
214
- for (const [taskId, info] of taskInfo) {
215
- depMap.set(taskId, info.dependsOn)
216
- }
217
- const { error: topoError } = topoSortWaves(depMap)
218
- if (topoError) {
219
- errors.push(topoError)
220
- }
221
-
222
- return { ok: errors.length === 0, errors, warnings }
223
- }
224
-
225
- /**
226
- * Plan 可行性校验器(本地代码证明 execute 前置条件)
227
- * 检查 TaskCard 的完整性和可行性
228
- * @param {string} changeDir - 变更目录
229
- * @param {string} projectRoot - 项目根目录(用于检查 allowed_paths 是否存在)
230
- * @returns {{ ok: boolean, errors: string[], warnings: string[] }}
231
- */
232
- export function validatePlanFeasibility(changeDir, projectRoot = null) {
233
- const errors = []
234
- const warnings = []
235
-
236
- const tasksDir = pJoin(changeDir, 'tasks')
237
- if (!existsSync(tasksDir)) {
238
- return { ok: true, errors, warnings } // none/light 可能没有 tasks/
239
- }
240
-
241
- const taskFiles = readdirSync(tasksDir).filter(f => /^task-\d+\.md$/.test(f)).sort()
242
- if (taskFiles.length === 0) {
243
- return { ok: true, errors, warnings }
244
- }
245
-
246
- const allTaskIds = []
247
- const depMap = new Map()
248
-
249
- for (const file of taskFiles) {
250
- const content = readFileSync(pJoin(tasksDir, file), 'utf8')
251
- const fmMatch = content.match(/^---\n([\s\S]*?)\n---/)
252
- if (!fmMatch) {
253
- errors.push(`${file}: 缺少 YAML frontmatter`)
254
- continue
255
- }
256
- const fm = fmMatch[1]
257
- const body = content.slice(fmMatch[0].length)
258
-
259
- // 1. 必要字段检查
260
- const taskId = (fm.match(/^id:\s*(.+)/m)?.[1] || '').trim()
261
- const title = (fm.match(/^title:\s*(.+)/m)?.[1] || '').trim()
262
- const allowedPathsRaw = fm.match(/allowed_paths:\s*\n((?:\s+-\s+.+\n?)+)/)?.[1] || ''
263
- const allowedPaths = allowedPathsRaw.match(/-\s+(.+)/g)?.map(s => s.replace(/^-\s+/, '').trim()) || []
264
-
265
- if (!taskId) errors.push(`${file}: frontmatter 缺少 id`)
266
- if (!title) errors.push(`${file}: frontmatter 缺少 title`)
267
- if (taskId) allTaskIds.push(taskId)
268
-
269
- // 2. allowed_paths 不为空
270
- if (allowedPaths.length === 0) {
271
- errors.push(`${taskId || file}: allowed_paths 为空`)
272
- }
273
-
274
- // 3. allowed_paths 文件存在或父目录存在(仅在 projectRoot 提供时检查)
275
- if (projectRoot && allowedPaths.length > 0) {
276
- for (const p of allowedPaths) {
277
- const fullPath = pJoin(projectRoot, p)
278
- const parentDir = pJoin(fullPath, '..')
279
- if (!existsSync(fullPath) && !existsSync(parentDir)) {
280
- warnings.push(`${taskId}: allowed_paths 中的 ${p} 文件和父目录都不存在`)
281
- }
282
- }
283
- }
284
-
285
- // 4. depends_on 引用存在
286
- const dependsOn = parseDependsOn(content)
287
- if (taskId) depMap.set(taskId, dependsOn)
288
-
289
- // 5. body/frontmatter 包含必要字段(TaskCard 格式中这些字段在 frontmatter 内)
290
- const hasGoal = /^goal:/m.test(fm)
291
- const hasImplementation = /implementation:/m.test(fm)
292
- const hasAcceptance = /acceptance:/m.test(fm)
293
- const hasVerify = /verify:/m.test(fm)
294
- const hasConstraints = /constraints:/m.test(fm)
295
-
296
- if (!hasGoal) errors.push(`${taskId || file}: 缺少 goal 字段`)
297
- if (!hasImplementation) errors.push(`${taskId || file}: 缺少 implementation 字段`)
298
- if (!hasAcceptance) errors.push(`${taskId || file}: 缺少 acceptance 字段`)
299
- if (!hasVerify) errors.push(`${taskId || file}: 缺少 verify 字段`)
300
- if (!hasConstraints) errors.push(`${taskId || file}: 缺少 constraints 字段`)
301
- }
302
-
303
- // 4b. depends_on 引用存在性
304
- for (const [taskId, deps] of depMap) {
305
- for (const dep of deps) {
306
- if (!depMap.has(dep)) {
307
- errors.push(`${taskId}: depends_on 引用了不存在的 ${dep}`)
308
- }
309
- }
310
- }
311
-
312
- // 5b. depends_on 无环(topoSortWaves 已含循环检测)
313
- const { error: topoError } = topoSortWaves(depMap)
314
- if (topoError) {
315
- errors.push(topoError)
316
- }
317
-
318
- // 7. task id 连续性
319
- if (allTaskIds.length > 0) {
320
- const nums = allTaskIds.map(id => {
321
- const m = id.match(/task-(\d+)/i)
322
- return m ? parseInt(m[1]) : null
323
- }).filter(n => n !== null).sort((a, b) => a - b)
324
- if (nums.length > 0 && nums[0] === 1) {
325
- for (let i = 0; i < nums.length; i++) {
326
- if (nums[i] !== i + 1) {
327
- errors.push(`task id 不连续: 期望 task-${String(i + 1).padStart(2, '0')}, 实际 task-${String(nums[i]).padStart(2, '0')}`)
328
- break
329
- }
330
- }
331
- }
332
- }
333
-
334
- // 9. plan.md 通过 validatePlanForExecute(延迟导入避免循环依赖)
335
- // 这在 run.js 的 postcheck contract 中已检查,这里不重复
336
-
337
- return { ok: errors.length === 0, errors, warnings }
338
- }
339
-
340
- /**
341
- * Plan 产物校验:检查 plan.md 和 tasks/ 是否齐全
342
- * @param {string} changeDir - 变更目录
343
- * @returns {{ ok: boolean, errors: string[], warnings: string[], planExists: boolean, taskCount: number }}
344
- */
345
- export function validatePlanArtifacts(changeDir) {
346
- const errors = []
347
- const warnings = []
348
-
349
- const planPath = pJoin(changeDir, 'plan.md')
350
- if (!existsSync(planPath)) {
351
- return { ok: false, errors: ['plan.md 不存在'], warnings, planExists: false, taskCount: 0 }
352
- }
353
-
354
- const tasksDir = pJoin(changeDir, 'tasks')
355
- let taskCount = 0
356
- if (existsSync(tasksDir)) {
357
- taskCount = readdirSync(tasksDir).filter(f => /^task-\d+\.md$/.test(f)).length
358
- } else {
359
- warnings.push('tasks/ 目录不存在(plan_level=none 时可接受)')
360
- }
361
-
362
- return { ok: true, errors, warnings, planExists: true, taskCount }
363
- }
364
-
365
- // ═══════════════════════════════════════════════════════════════
366
- // 执行入口(从 run.js 迁移)
367
- // ═══════════════════════════════════════════════════════════════
368
-
369
- /**
370
- * Plan postcheck 主函数:Wave 重排 + 一致性校验 + 产物确认
371
- *
372
- * @param {{ cwd: string, specRoot?: string, resolveChangeDir: Function }} context
373
- * @throws {Error} 校验失败时抛出
374
- */
375
- export async function executePlanPostcheck(context) {
376
- const { cwd, specRoot, resolveChangeDir } = context
377
-
378
- const specDir = specRoot || pJoin(cwd, '.sillyspec')
379
- const changesDir = pJoin(specDir, 'changes')
380
- if (!existsSync(changesDir)) {
381
- console.warn(' ⚠️ 未找到 changes 目录,跳过 postcheck')
382
- return
383
- }
384
-
385
- // 找到当前变更目录
386
- const progressPath = pJoin(specDir, '.runtime', 'progress.json')
387
- let changeDir = null
388
- if (existsSync(progressPath)) {
389
- const progress = JSON.parse(readFileSync(progressPath, 'utf8'))
390
- changeDir = resolveChangeDir(cwd, progress, specDir)
391
- }
392
- if (!changeDir) {
393
- // 回退:找最新的变更目录
394
- const dirs = readdirSync(changesDir)
395
- .filter(d => existsSync(pJoin(changesDir, d, 'plan.md')))
396
- .sort().reverse()
397
- if (dirs.length > 0) changeDir = pJoin(changesDir, dirs[0])
398
- }
399
- if (!changeDir) {
400
- console.warn(' ⚠️ 未找到当前变更目录,跳过 postcheck')
401
- return
402
- }
403
-
404
- console.log(` 📂 变更目录: ${changeDir}`)
405
-
406
- // ── 1. 一致性校验 ──
407
- const consistency = validateBlueprintConsistency(changeDir)
408
- if (consistency.errors.length > 0) {
409
- console.error('\n❌ 蓝图一致性校验失败:')
410
- for (const err of consistency.errors) console.error(` - ${err}`)
411
- console.error('\n 请修复上述问题后重新完成此步骤。')
412
- throw new Error('planPostcheck: blueprint consistency check failed')
413
- }
414
- if (consistency.warnings.length > 0) {
415
- console.warn('\n⚠️ 蓝图一致性警告(不阻断):')
416
- for (const w of consistency.warnings) console.warn(` - ${w}`)
417
- }
418
-
419
- // ── 1b. 可行性校验 ──
420
- const feasibility = validatePlanFeasibility(changeDir, context.cwd)
421
- if (feasibility.errors.length > 0) {
422
- console.error('\n❌ Plan 可行性校验失败:')
423
- for (const err of feasibility.errors) console.error(` - ${err}`)
424
- throw new Error('planPostcheck: feasibility check failed')
425
- }
426
- if (feasibility.warnings.length > 0) {
427
- console.warn('\n⚠️ Plan 可行性警告(不阻断):')
428
- for (const w of feasibility.warnings) console.warn(` - ${w}`)
429
- }
430
-
431
- // ── 2. Wave 重排 ──
432
- const tasksDir = pJoin(changeDir, 'tasks')
433
- if (existsSync(tasksDir)) {
434
- const taskFiles = readdirSync(tasksDir).filter(f => /^task-\d+\.md$/.test(f))
435
- const depMap = new Map()
436
-
437
- for (const file of taskFiles) {
438
- const content = readFileSync(pJoin(tasksDir, file), 'utf8')
439
- const fmMatch = content.match(/^---\n([\s\S]*?)\n---/)
440
- let taskId = file.replace('.md', '')
441
- if (fmMatch) {
442
- const idMatch = fmMatch[1].match(/^id:\s*(.+)/m)
443
- if (idMatch) taskId = idMatch[1].trim()
444
- }
445
- depMap.set(taskId, parseDependsOn(content))
446
- }
447
-
448
- const { waves, error: topoError } = topoSortWaves(depMap)
449
- if (topoError) {
450
- console.error(`\n❌ Wave 重排失败: ${topoError}`)
451
- throw new Error('planPostcheck: ' + topoError)
452
- }
453
-
454
- console.log('\n 📊 Wave 分组(基于 depends_on 拓扑排序):')
455
- waves.forEach((wave, i) => {
456
- console.log(` Wave ${i + 1}: ${wave.join(', ')}`)
457
- })
458
-
459
- // 比较 plan.md 现有 Wave 分组
460
- if (waves.length > 1 && taskFiles.length > 1) {
461
- const planPath = pJoin(changeDir, 'plan.md')
462
- if (existsSync(planPath)) {
463
- const planContent = readFileSync(planPath, 'utf8')
464
- if (/##\s*Wave\s+\d/i.test(planContent)) {
465
- const existingWaves = []
466
- const lines = planContent.split('\n')
467
- let currentWaveTasks = null
468
- for (const line of lines) {
469
- const wm = line.match(/^#+\s*Wave\s+(\d+)/i)
470
- if (wm) {
471
- if (currentWaveTasks) existingWaves.push(currentWaveTasks)
472
- currentWaveTasks = []
473
- continue
474
- }
475
- const tm = line.match(/^[-*]\s*\[[ x]\]\s*task-(\d+)/i)
476
- if (tm && currentWaveTasks) {
477
- currentWaveTasks.push(`task-${tm[1]}`)
478
- }
479
- }
480
- if (currentWaveTasks) existingWaves.push(currentWaveTasks)
481
-
482
- const sameStructure = waves.length === existingWaves.length &&
483
- waves.every((w, i) => {
484
- const a = [...w].sort().join(',')
485
- const b = [...(existingWaves[i] || [])].sort().join(',')
486
- return a === b
487
- })
488
-
489
- if (sameStructure) {
490
- console.log(' ✅ Wave 分组与拓扑排序一致,无需更新 plan.md')
491
- } else {
492
- console.log(' ⚠️ Wave 分组与拓扑排序不一致,建议手动调整 plan.md')
493
- console.log(' 拓扑排序建议的 Wave 分组见上方')
494
- }
495
- }
496
- }
497
- }
498
- }
499
-
500
- // ── 3. 产物确认 ──
501
- const artifacts = validatePlanArtifacts(changeDir)
502
- if (!artifacts.ok) {
503
- for (const err of artifacts.errors) console.error(`❌ ${err}`)
504
- throw new Error('planPostcheck: artifact validation failed')
505
- }
506
- console.log('\n ✅ plan.md 存在')
507
-
508
- if (artifacts.taskCount > 0) {
509
- console.log(` ✅ tasks/ 目录有 ${artifacts.taskCount} 个 TaskCard 文件`)
510
- }
511
-
512
- console.log('\n ✅ Plan postcheck 完成')
513
- }