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,700 +0,0 @@
1
- /**
2
- * StageContract — 阶段协议
3
- *
4
- * 每个阶段声明:允许的前置阶段、必须的产出、校验器、后续阶段。
5
- * CLI 不再相信 prompt 完成,completeStep 后必须过 validator。
6
- */
7
-
8
- import { existsSync, readdirSync, readFileSync } from 'fs'
9
- import { join, basename } from 'path'
10
- import { detectChangeRisk, checkIntegrationEvidence } from './change-risk-profile.js'
11
-
12
- /**
13
- * 校验结果
14
- * @typedef {{ ok: boolean, errors: string[], warnings: string[] }} ValidationResult
15
- */
16
-
17
- /**
18
- * 阶段合约
19
- * @typedef {{
20
- * stage: string,
21
- * description: string,
22
- * allowedFrom: string[],
23
- * allowedTo: string[],
24
- * validators: Function[],
25
- * }} StageContract
26
- */
27
-
28
- // ============ Validators ============
29
-
30
- function resolveChangeDir(cwd, changeName, specRoot = null) {
31
- const changesRoot = specRoot ? join(specRoot, 'changes') : join(cwd, '.sillyspec', 'changes')
32
- return join(changesRoot, changeName)
33
- }
34
-
35
- function collectIdsFromLine(line, re, ids) {
36
- for (const match of line.matchAll(re)) {
37
- ids.add(match[0].toUpperCase())
38
- }
39
- }
40
-
41
- function extractIds(content, prefix) {
42
- if (!content) return []
43
- const ids = new Set()
44
- const idRe = new RegExp(`\\b${prefix}-\\d+(?:@v\\d+)?\\b`, 'gi')
45
- const headingLineRe = /^\s{0,3}#{1,6}\s+/i
46
- const fieldLineRe = /^\s*(?:[-*]\s*)?(?:id|decision[-_ ]?ids?|requirement[-_ ]?ids?|covers?|coverage|references?|impacts?|覆盖(?:来源|决策|需求)?)\s*[::]/i
47
- const tableLineRe = /^\s*\|/
48
- const listStartsWithIdRe = new RegExp(`^\\s*(?:[-*]|\\d+\\.)\\s*(?:\\[[ xX]\\]\\s*)?${prefix}-\\d+(?:@v\\d+)?\\b`, 'i')
49
-
50
- for (const line of content.split(/\r?\n/)) {
51
- if (!headingLineRe.test(line) && !fieldLineRe.test(line) && !tableLineRe.test(line) && !listStartsWithIdRe.test(line)) continue
52
- collectIdsFromLine(line, idRe, ids)
53
- }
54
- return [...ids].sort()
55
- }
56
-
57
- function readDecisionField(body, fieldPattern, fallback = '') {
58
- const re = new RegExp(`^\\s*(?:[-*]\\s*)?(?:${fieldPattern})\\s*[::]\\s*([^\\n]+)`, 'im')
59
- return (body.match(re)?.[1] || fallback).trim()
60
- }
61
-
62
- function buildDecisionRecord(id, body) {
63
- const status = readDecisionField(body, 'status', 'accepted').toLowerCase()
64
- const blockerValue = readDecisionField(body, 'blocker', 'false').toLowerCase()
65
- const blocker = ['true', 'yes', '1'].includes(blockerValue)
66
- const priorityValue = readDecisionField(body, 'priority|level|severity')
67
- const priorityMissing = priorityValue.length === 0
68
- const fallbackPriority = (['unresolved', 'blocking'].includes(status) || blocker) ? 'P1' : 'P2'
69
- const priority = (priorityValue.match(/P[0-2]/i)?.[0] || fallbackPriority).toUpperCase()
70
- // 解析 supersedes 字段:记录本条决策取代了哪个旧版本
71
- const supersedesRaw = readDecisionField(body, 'supersedes', '')
72
- const supersedes = supersedesRaw
73
- ? supersedesRaw.split(',').map(s => s.trim().toUpperCase().replace(/['"]/g, '')).filter(Boolean)
74
- : []
75
- return { id: id.toUpperCase(), body, status, priority, blocker, priorityMissing, supersedes }
76
- }
77
-
78
- function findNextDecisionBoundary(content, startIndex) {
79
- const boundaryRe = /^(\s{0,3}#{2,6}\s+D-\d+(?:@v\d+)?\b|\s*(?:[-*]\s*)?(?:id|decision[-_ ]?id|decision)\s*[::]\s*D-\d+(?:@v\d+)?\b)/gmi
80
- boundaryRe.lastIndex = startIndex
81
- const next = boundaryRe.exec(content)
82
- return next ? next.index : content.length
83
- }
84
-
85
- function isInsideRange(index, ranges) {
86
- return ranges.some(range => index >= range.start && index < range.end)
87
- }
88
-
89
- function parseDecisionRecords(content) {
90
- if (!content) return []
91
- const records = []
92
- const ranges = []
93
- const headingRe = /^\s{0,3}#{2,6}\s+(D-\d+(?:@v\d+)?)(?:\b|:)[^\n]*$/gmi
94
- const headings = []
95
- let match
96
- while ((match = headingRe.exec(content)) !== null) {
97
- headings.push({ id: match[1].toUpperCase(), index: match.index, end: headingRe.lastIndex })
98
- }
99
- for (let i = 0; i < headings.length; i++) {
100
- const current = headings[i]
101
- const next = headings[i + 1]
102
- const body = content.slice(current.end, next ? next.index : content.length)
103
- const end = next ? next.index : content.length
104
- ranges.push({ start: current.index, end })
105
- records.push(buildDecisionRecord(current.id, body))
106
- }
107
-
108
- const idLineRe = /^\s*(?:[-*]\s*)?(?:id|decision[-_ ]?id|decision)\s*[::]\s*(D-\d+(?:@v\d+)?)(?:\b|$)/gmi
109
- while ((match = idLineRe.exec(content)) !== null) {
110
- if (isInsideRange(match.index, ranges)) continue
111
- const bodyEnd = findNextDecisionBoundary(content, idLineRe.lastIndex)
112
- const body = content.slice(match.index, bodyEnd)
113
- records.push(buildDecisionRecord(match[1], body))
114
- }
115
-
116
- return records
117
- }
118
-
119
- function extractCurrentDecisionIds(content) {
120
- const records = parseDecisionRecords(content)
121
- if (records.length === 0) return extractIds(content, 'D')
122
- // 收集所有被 supersedes 声明取代的旧版本 ID
123
- const supersededIds = new Set()
124
- for (const r of records) {
125
- for (const oldId of r.supersedes) {
126
- supersededIds.add(oldId)
127
- }
128
- }
129
- return records
130
- .filter(r => !['superseded', 'rejected'].includes(r.status))
131
- .filter(r => !supersededIds.has(r.id)) // 被新版本显式取代的旧版本不再校验
132
- .map(r => r.id)
133
- .sort()
134
- }
135
-
136
- function findBlockingDecisionIssues(content) {
137
- return parseDecisionRecords(content)
138
- .filter(r => (r.blocker || ['unresolved', 'blocking'].includes(r.status)) && ['P0', 'P1'].includes(r.priority))
139
- .map(r => `${r.id} (${r.priority}${r.priorityMissing ? ', priority=missing->P1' : ''}, status=${r.status})`)
140
- }
141
-
142
- function readIfExists(file) {
143
- return existsSync(file) ? readFileSync(file, 'utf8') : ''
144
- }
145
-
146
- function warnMissingIds(warnings, ids, targetContent, targetName, sourceName) {
147
- for (const id of ids) {
148
- if (!targetContent.toUpperCase().includes(id)) {
149
- warnings.push(`${targetName} 未引用 ${sourceName} 中的 ${id}`)
150
- }
151
- }
152
- }
153
-
154
- /**
155
- * scan 完成校验:检查 7 份 scan 文档 + manifest
156
- */
157
- function validateScanOutputs(cwd, changeName, context = {}) {
158
- const { projectName, specRoot } = context
159
- // 平台模式:specRoot 直接是规范目录(含 docs/)
160
- // 本地模式:规范目录是 cwd/.sillyspec
161
- // 不用 isSpecDir 启发式猜测——很多项目根目录有自己的 docs/,会误判
162
- const specBase = specRoot || join(cwd, '.sillyspec')
163
- const docsRoot = projectName
164
- ? join(specBase, 'docs', projectName, 'scan')
165
- : join(specBase, 'docs', 'scan')
166
-
167
- const requiredDocs = [
168
- 'ARCHITECTURE.md',
169
- 'CONVENTIONS.md',
170
- 'STRUCTURE.md',
171
- 'INTEGRATIONS.md',
172
- 'TESTING.md',
173
- 'CONCERNS.md',
174
- 'PROJECT.md',
175
- ]
176
-
177
- const errors = []
178
- const warnings = []
179
-
180
- for (const doc of requiredDocs) {
181
- if (!existsSync(join(docsRoot, doc))) {
182
- errors.push(`scan 文档缺失: ${join(docsRoot, doc)}`)
183
- }
184
- }
185
-
186
- // 检查 modules 目录
187
- const modulesRoot = projectName
188
- ? join(specBase, 'docs', projectName, 'modules')
189
- : join(specBase, 'docs', 'modules')
190
- if (!existsSync(modulesRoot)) {
191
- warnings.push('modules 目录不存在')
192
- } else {
193
- const modules = readdirSync(modulesRoot).filter(f => f.endsWith('.md'))
194
- if (modules.length === 0) {
195
- warnings.push('modules 目录为空')
196
- }
197
- }
198
-
199
- return { ok: errors.length === 0, errors, warnings }
200
- }
201
-
202
- /**
203
- * brainstorm 完成校验:检查四件套规范文件是否生成
204
- */
205
- function validateBrainstormOutputs(cwd, changeName, context = {}) {
206
- const { specRoot } = context
207
- const changesRoot = specRoot ? join(specRoot, 'changes') : join(cwd, '.sillyspec', 'changes')
208
- if (specRoot && !existsSync(changesRoot)) {
209
- return { ok: false, errors: [`平台模式 specRoot 缺少 changes 目录: ${changesRoot}`], warnings: [] }
210
- }
211
- const changeDir = resolveChangeDir(cwd, changeName, specRoot)
212
- const errors = []
213
- const warnings = []
214
-
215
- const requiredFiles = ['design.md', 'proposal.md', 'requirements.md', 'tasks.md']
216
-
217
- for (const file of requiredFiles) {
218
- if (!existsSync(join(changeDir, file))) {
219
- errors.push(`brainstorm 产物缺失: ${join(changeDir, file)}`)
220
- }
221
- }
222
-
223
- // 内容校验(文件存在时检查关键章节)
224
- if (existsSync(join(changeDir, 'proposal.md'))) {
225
- const content = readFileSync(join(changeDir, 'proposal.md'), 'utf8')
226
- if (!content.includes('不在范围内') && !content.includes('Non-Goals') && !content.includes('非目标')) {
227
- warnings.push('proposal.md 缺少「不在范围内/Non-Goals」章节')
228
- }
229
- }
230
-
231
- if (existsSync(join(changeDir, 'requirements.md'))) {
232
- const content = readFileSync(join(changeDir, 'requirements.md'), 'utf8')
233
- if (!/FR-\d+/i.test(content)) {
234
- warnings.push('requirements.md 缺少 FR 编号的需求项')
235
- }
236
- }
237
-
238
- if (existsSync(join(changeDir, 'design.md'))) {
239
- const content = readFileSync(join(changeDir, 'design.md'), 'utf8')
240
- if (!content.includes('文件变更清单') && !content.includes('File Changes') && !content.includes('文件清单')) {
241
- warnings.push('design.md 缺少「文件变更清单」章节')
242
- }
243
- if (!content.includes('风险登记') && !content.includes('Risk') && !content.includes('风险')) {
244
- warnings.push('design.md 缺少「风险登记」章节')
245
- }
246
- if (!content.includes('自审') && !content.includes('Self-Review') && !content.includes('Self-review')) {
247
- warnings.push('design.md 缺少「自审」章节')
248
- }
249
-
250
- // P1: 涉及生命周期关键词时,design.md 必须包含生命周期契约表
251
- const hasLifecycleKeyword = /\b(session|lease|agent[._-]?run|daemon|lifecycle|state[._-]?transition|claim|heartbeat)\b/i.test(content)
252
- if (hasLifecycleKeyword) {
253
- const hasLifecycleTable =
254
- /生命周期契约表|lifecycle[._-]?contract|lifecycle[._-]?matrix|Lifecycle Contract/i.test(content) ||
255
- /事件.*发起方.*接收方.*必需字段.*状态变化/.test(content)
256
- if (!hasLifecycleTable) {
257
- errors.push('design.md 涉及生命周期关键词(session/lease/agent_run/daemon/lifecycle)但缺少「生命周期契约表」— 必须列出完整的事件×状态转换矩阵')
258
- }
259
- }
260
- }
261
-
262
- if (existsSync(join(changeDir, 'tasks.md'))) {
263
- const content = readFileSync(join(changeDir, 'tasks.md'), 'utf8')
264
- const lines = content.split('\n').filter(l => l.trim().startsWith('-') || l.trim().startsWith('*') || /^\d+\./.test(l.trim()))
265
- if (lines.length === 0) {
266
- warnings.push('tasks.md 没有任务列表项')
267
- }
268
- }
269
-
270
- const decisionsFile = join(changeDir, 'decisions.md')
271
- if (existsSync(decisionsFile)) {
272
- const decisions = readFileSync(decisionsFile, 'utf8')
273
- const blockers = findBlockingDecisionIssues(decisions)
274
- for (const issue of blockers) {
275
- errors.push(`decisions.md 存在 P0/P1 未决阻塞: ${issue}`)
276
- }
277
- const decisionIds = extractCurrentDecisionIds(decisions)
278
- if (decisionIds.length === 0) {
279
- warnings.push('decisions.md 存在但没有当前版本 D-xxx@vN 决策 ID')
280
- } else {
281
- const design = readIfExists(join(changeDir, 'design.md'))
282
- const requirements = readIfExists(join(changeDir, 'requirements.md'))
283
- const tasks = readIfExists(join(changeDir, 'tasks.md'))
284
- warnMissingIds(warnings, decisionIds, design, 'design.md', 'decisions.md')
285
- warnMissingIds(warnings, decisionIds, requirements, 'requirements.md', 'decisions.md')
286
- warnMissingIds(warnings, decisionIds, tasks, 'tasks.md', 'decisions.md')
287
- }
288
- }
289
-
290
- return { ok: errors.length === 0, errors, warnings }
291
- }
292
-
293
- /**
294
- * plan 完成校验:检查 plan.md 生成
295
- */
296
- function validatePlanOutputs(cwd, changeName, context = {}) {
297
- const { specRoot } = context
298
- const changeDir = resolveChangeDir(cwd, changeName, specRoot)
299
- const planFile = join(changeDir, 'plan.md')
300
- const errors = []
301
-
302
- if (!existsSync(planFile)) {
303
- errors.push(`plan.md 缺失: ${planFile}`)
304
- }
305
-
306
- const warnings = []
307
- if (existsSync(planFile)) {
308
- const plan = readFileSync(planFile, 'utf8')
309
- const requirements = readIfExists(join(changeDir, 'requirements.md'))
310
- const requirementIds = extractIds(requirements, 'FR')
311
- warnMissingIds(warnings, requirementIds, plan, 'plan.md', 'requirements.md')
312
-
313
- const decisions = readIfExists(join(changeDir, 'decisions.md'))
314
- const blockers = findBlockingDecisionIssues(decisions)
315
- for (const issue of blockers) {
316
- errors.push(`decisions.md 存在 P0/P1 未决阻塞: ${issue}`)
317
- }
318
- const decisionIds = extractCurrentDecisionIds(decisions)
319
- warnMissingIds(warnings, decisionIds, plan, 'plan.md', 'decisions.md')
320
- }
321
- // ── P0: 生产接线路径检查:design 提到入口但 task 的 allowed_paths 不含入口文件 ──
322
- const designContent = readIfExists(join(changeDir, 'design.md'))
323
- if (designContent) {
324
- const entryPointPatterns = [
325
- /\b(cli\.ts|main\.ts|server\.(?:js|ts)|index\.(?:js|ts))\b.*\b(?:实例化|instantiate|构造|new\s)/gi,
326
- /\bnew\s+(Daemon|SessionManager|App|Server|Application)\b/gi,
327
- /\b(?:在|from)\s+['"]?(cli\.ts|main\.ts|server\.(?:js|ts)|index\.(?:js|ts))['"]?/gi,
328
- /\b(?:注入|inject)\b.*\b(?:构造|constructor|初始化|init|实例化|instantiate)\b/gi,
329
- /\b(?:启动路径|startup|entrypoint|bootstrap|daemon[._-]?start|main.*entry)\b/gi,
330
- ]
331
- const mentionedFiles = new Set()
332
- for (const pattern of entryPointPatterns) {
333
- pattern.lastIndex = 0
334
- for (const match of designContent.matchAll(pattern)) {
335
- const fileMatch = match[0].match(/\b(cli\.ts|main\.ts|server\.(?:js|ts)|index\.(?:js|ts))\b/i)
336
- if (fileMatch) mentionedFiles.add(fileMatch[1].toLowerCase())
337
- }
338
- }
339
- if (mentionedFiles.size > 0) {
340
- const tasksDir = join(changeDir, 'tasks')
341
- const allAllowedPaths = new Set()
342
- if (existsSync(tasksDir)) {
343
- const taskFiles = readdirSync(tasksDir).filter(f => /^task-\d+\.md$/i.test(f))
344
- for (const taskFile of taskFiles) {
345
- const taskContent = readFileSync(join(tasksDir, taskFile), 'utf8')
346
- const allowedSection = taskContent.match(/allowed_paths:\s*\n((?:\s+-\s+.+\n?)+)/)
347
- if (allowedSection) {
348
- const paths = allowedSection[1].match(/-\s+(.+)/g) || []
349
- for (const p of paths) allAllowedPaths.add(p.replace(/^-\s+/, '').trim().toLowerCase())
350
- }
351
- }
352
- }
353
- // 也从 plan.md 文件变更清单中收集
354
- if (existsSync(planFile)) {
355
- const planContent = readFileSync(planFile, 'utf8')
356
- const planFileChanges = planContent.match(/\|\s*(?:新增|修改|new|modify|update)\s*\|\s*`?([^`|]+)`?\s*\|/gi) || []
357
- for (const line of planFileChanges) {
358
- const file = line.match(/\|\s*(?:新增|修改|new|modify|update)\s*\|\s*`?([^`|]+)`?\s*\|/i)
359
- if (file) allAllowedPaths.add(file[1].trim().toLowerCase())
360
- }
361
- }
362
- for (const mentionedFile of mentionedFiles) {
363
- const found = [...allAllowedPaths].some(p => p.includes(mentionedFile))
364
- if (!found) {
365
- const noChangePattern = new RegExp(`不需要改.*${mentionedFile}|${mentionedFile}.*不需要|不修改.*${mentionedFile}|${mentionedFile}.*不变|${mentionedFile}.*no.?change`, 'i')
366
- if (!noChangePattern.test(designContent)) {
367
- errors.push(`生产接线路径矛盾: design.md 提到了入口文件 "${mentionedFile}" 但所有 task 的 allowed_paths 中均不含该文件`)
368
- warnings.push(`提示: 如果确实不需要修改 ${mentionedFile},请在 design.md 中明确写明理由`)
369
- }
370
- }
371
- }
372
- }
373
- }
374
-
375
- return { ok: errors.length === 0, errors, warnings }
376
- }
377
- function validateVerifyOutputs(cwd, changeName, context = {}) {
378
- const { specRoot } = context
379
- const changeDir = resolveChangeDir(cwd, changeName, specRoot)
380
- const errors = []
381
- const warnings = []
382
-
383
- if (!existsSync(changeDir)) {
384
- errors.push(`变更目录缺失: ${changeDir}`)
385
- return { ok: false, errors, warnings }
386
- }
387
-
388
- // verify 阶段应该产出 verify-result.md(或类似报告)
389
- const verifyResult = join(changeDir, 'verify-result.md')
390
- if (!existsSync(verifyResult)) {
391
- warnings.push('verify-result.md 不存在(verify 阶段建议产出验证报告)')
392
- }
393
-
394
- // 确保核心规范文件仍然存在
395
- const requiredDocs = ['design.md', 'plan.md']
396
- for (const doc of requiredDocs) {
397
- if (!existsSync(join(changeDir, doc))) {
398
- errors.push(`核心文档缺失: ${join(changeDir, doc)}`)
399
- }
400
- }
401
-
402
- if (existsSync(verifyResult)) {
403
- const verify = readFileSync(verifyResult, 'utf8')
404
- const decisions = readIfExists(join(changeDir, 'decisions.md'))
405
- const blockers = findBlockingDecisionIssues(decisions)
406
- for (const issue of blockers) {
407
- errors.push(`decisions.md 存在 P0/P1 未决阻塞: ${issue}`)
408
- }
409
- const decisionIds = extractCurrentDecisionIds(decisions)
410
- warnMissingIds(warnings, decisionIds, verify, 'verify-result.md', 'decisions.md')
411
-
412
- // ── P0: Change Risk Gate — 核心功能缺少真实集成验证时 FAIL ──
413
- const changeRiskProfile = detectChangeRisk({
414
- designContent: readIfExists(join(changeDir, 'design.md')),
415
- planContent: readIfExists(join(changeDir, 'plan.md')),
416
- })
417
- if (['integration-critical', 'deployment-critical'].includes(changeRiskProfile.level)) {
418
- const conclusionMatch = verify.match(/^## 结论\s*\n\s*(PASS|PASS WITH NOTES|FAIL)/im)
419
- const conclusion = conclusionMatch ? conclusionMatch[1] : ''
420
- if (conclusion === 'PASS WITH NOTES' || conclusion === 'PASS') {
421
- const evidenceCheck = checkIntegrationEvidence(verify, changeRiskProfile.requiredVerification)
422
- if (!evidenceCheck.ok) {
423
- errors.push(`[${changeRiskProfile.level}] 验证结论为 ${conclusion},但缺少真实集成证据:${evidenceCheck.errors.join('; ')}`)
424
- errors.push(`触发词: ${changeRiskProfile.triggers.join(', ')} — PASS WITH NOTES 不被允许,必须 FAIL 或提供集成证据`)
425
- }
426
- warnings.push(...evidenceCheck.warnings)
427
- }
428
- }
429
- }
430
-
431
- return { ok: errors.length === 0, errors, warnings }
432
- }
433
-
434
- /**
435
- * archive 完成校验:检查归档目录完整性
436
- */
437
- function validateArchiveOutputs(cwd, changeName) {
438
- const errors = []
439
- const warnings = []
440
- const archiveDir = join(cwd, '.sillyspec', 'changes', 'archive')
441
- const date = new Date().toISOString().slice(0, 10)
442
- const destDir = join(archiveDir, `${date}-${changeName}`)
443
-
444
- // 检查归档目录是否存在
445
- if (!existsSync(destDir)) {
446
- errors.push(`归档目录缺失: ${destDir}`)
447
- return { ok: false, errors, warnings }
448
- }
449
-
450
- // 检查核心文档
451
- const requiredDocs = ['plan.md']
452
- const recommendedDocs = ['design.md', 'module-impact.md']
453
-
454
- for (const doc of requiredDocs) {
455
- if (!existsSync(join(destDir, doc))) {
456
- errors.push(`归档目录缺失核心文档: ${doc}`)
457
- }
458
- }
459
-
460
- for (const doc of recommendedDocs) {
461
- if (!existsSync(join(destDir, doc))) {
462
- warnings.push(`归档目录缺少推荐文档: ${doc}`)
463
- }
464
- }
465
-
466
- return { ok: errors.length === 0, errors, warnings }
467
- }
468
-
469
- /**
470
- * archive 前置校验:所有主流程阶段完成
471
- */
472
- function validateChangeClosed(cwd, changeName) {
473
- const errors = []
474
- const warnings = []
475
-
476
- // 检查前置阶段状态
477
- const progressDir = join(cwd, '.sillyspec', '.runtime')
478
- // 这里只做文件层面的检查,DB 检查在 run.js 里做
479
- const changeDir = join(cwd, '.sillyspec', 'changes', changeName)
480
- if (!existsSync(changeDir)) {
481
- errors.push(`变更目录不存在: ${changeDir}`)
482
- return { ok: false, errors, warnings }
483
- }
484
-
485
- if (!existsSync(join(changeDir, 'plan.md'))) {
486
- errors.push(`plan.md 缺失 — 请确保 plan 阶段已完成`)
487
- }
488
-
489
- return { ok: errors.length === 0, errors, warnings }
490
- }
491
-
492
- // ============ Contract Registry ============
493
-
494
- /**
495
- * 主流程阶段(有严格转换顺序)
496
- */
497
- const mainFlowStages = ['brainstorm', 'plan', 'execute', 'verify']
498
-
499
- /**
500
- * 辅助阶段(可独立运行,无严格转换顺序)
501
- */
502
- const auxiliaryStages = ['scan', 'quick', 'explore', 'archive', 'status', 'doctor']
503
-
504
- /**
505
- * @type {Object<string, StageContract>}
506
- */
507
- const contracts = {
508
- // === 主流程 ===
509
- brainstorm: {
510
- stage: 'brainstorm',
511
- description: '需求分析与设计',
512
- allowedFrom: [], // 任何变更的起始阶段
513
- allowedTo: ['plan'],
514
- validators: [validateBrainstormOutputs],
515
- },
516
- plan: {
517
- stage: 'plan',
518
- description: '任务拆解与规划',
519
- allowedFrom: ['brainstorm'],
520
- allowedTo: ['execute'],
521
- validators: [validatePlanOutputs],
522
- },
523
- execute: {
524
- stage: 'execute',
525
- description: '代码实现',
526
- allowedFrom: ['plan'],
527
- allowedTo: ['verify'],
528
- validators: [],
529
- },
530
- verify: {
531
- stage: 'verify',
532
- description: '验证与测试',
533
- allowedFrom: ['execute'],
534
- allowedTo: ['archive'],
535
- validators: [validateVerifyOutputs],
536
- },
537
- archive: {
538
- stage: 'archive',
539
- description: '归档与收口',
540
- allowedFrom: ['verify'],
541
- allowedTo: [],
542
- // 阶段级 validator 全部移除,改为 run.js 中 step 4 完成后的硬编码校验。
543
- // 理由:两个 validator 的生效窗口互斥 ——
544
- // validateChangeClosed 要求变更目录存在(step 4 --confirm 后已被移到 archive 目录)
545
- // validateArchiveOutputs 要求 archive 目录存在(step 4 前还不存在)
546
- // 注册为阶段级 validator 会导致每步都误报错误。
547
- // run.js:893-909 已在正确的时机(step 4 完成后)执行相同检查。
548
- validators: [],
549
- },
550
-
551
- // === 辅助阶段 ===
552
- scan: {
553
- stage: 'scan',
554
- description: '项目扫描',
555
- allowedFrom: [], // 无前置要求
556
- allowedTo: [], // 不进入主流程
557
- validators: [validateScanOutputs],
558
- },
559
- quick: {
560
- stage: 'quick',
561
- description: '快速任务',
562
- allowedFrom: [], // 无前置要求
563
- allowedTo: [], // 不进入主流程
564
- validators: [],
565
- },
566
- explore: {
567
- stage: 'explore',
568
- description: '代码探索',
569
- allowedFrom: [],
570
- allowedTo: [],
571
- validators: [],
572
- },
573
- status: {
574
- stage: 'status',
575
- description: '状态查看',
576
- allowedFrom: [],
577
- allowedTo: [],
578
- validators: [],
579
- },
580
- doctor: {
581
- stage: 'doctor',
582
- description: '环境诊断',
583
- allowedFrom: [],
584
- allowedTo: [],
585
- validators: [],
586
- },
587
- }
588
-
589
- // ============ Public API ============
590
-
591
- /**
592
- * 获取阶段合约
593
- */
594
- export function getContract(stageName) {
595
- return contracts[stageName] || null
596
- }
597
-
598
- /**
599
- * 校验状态转换是否允许
600
- * @param {string} fromStage - 当前阶段(空字符串表示变更起始)
601
- * @param {string} toStage - 目标阶段
602
- * @param {{ fromStageData?: { status?: string } | undefined }} [options] - 可选,从 progress.stages[prevStage] 提取
603
- * @returns {{ allowed: boolean, reason?: string }}
604
- */
605
- export function checkTransition(fromStage, toStage, options = {}) {
606
- const { fromStageData } = options // { status?: string } | undefined
607
- const contract = contracts[toStage]
608
- if (!contract) {
609
- return { allowed: false, reason: `未知阶段: ${toStage}` }
610
- }
611
-
612
- // 辅助阶段随时可执行(archive 除外:从主流程进入 archive 需要校验)
613
- if (auxiliaryStages.includes(toStage) && toStage !== 'archive') {
614
- return { allowed: true }
615
- }
616
-
617
- // 同阶段内重复运行:允许(继续执行当前阶段的下一步、或修订模式继续)
618
- if (fromStage === toStage) {
619
- return { allowed: true }
620
- }
621
-
622
- // task-07: failed_post_check 门控
623
- // scan post-check 未通过时,禁止进入主流程的下游阶段(brainstorm/plan/execute/verify/archive)
624
- // 必须先重跑 scan 修复。toStage === 'scan' 的重跑路径已被上方 fromStage === toStage 放行。
625
- // fromStageData.status 缺失(旧数据)时门控不触发(向后兼容)。
626
- if (fromStage === 'scan' && fromStageData?.status === 'failed_post_check' && toStage !== 'scan') {
627
- return {
628
- allowed: false,
629
- reason: 'scan post-check 未通过(failed_post_check),需修复后重跑 scan 再进入 ' + toStage,
630
- }
631
- }
632
-
633
- // archive 特殊处理:从 verify 来的允许,从其他主流程阶段来的需要校验
634
- if (toStage === 'archive') {
635
- if (fromStage === 'verify') {
636
- return { allowed: true }
637
- }
638
- // 独立运行 archive(无前置)也允许
639
- if (!fromStage || auxiliaryStages.includes(fromStage)) {
640
- return { allowed: true }
641
- }
642
- return { allowed: false, reason: 'archive 的前置阶段是 verify,不能从 ' + fromStage + ' 跳转' }
643
- }
644
-
645
- // 从辅助阶段进入主流程:允许
646
- if (auxiliaryStages.includes(fromStage)) {
647
- return { allowed: true }
648
- }
649
-
650
- // 无前置阶段(变更起始):只能开始 brainstorm 或辅助阶段
651
- if (!fromStage) {
652
- // 主流程必须从 brainstorm 开始
653
- if (contract.allowedFrom.length === 0) {
654
- return { allowed: true }
655
- }
656
- return { allowed: false, reason: `${toStage} 需要先完成 ${contract.allowedFrom.join(' 或 ')}` }
657
- }
658
-
659
- // 主流程内部跳转:检查目标阶段的 allowedFrom 是否包含 fromStage
660
- if (contract.allowedFrom.includes(fromStage)) {
661
- return { allowed: true }
662
- }
663
-
664
- return { allowed: false, reason: `${toStage} 的前置阶段是 ${contract.allowedFrom.join(' 或 ')},不能从 ${fromStage} 跳转` }
665
- }
666
-
667
- /**
668
- * 执行阶段完成校验
669
- * @param {string} stageName
670
- * @param {string} cwd
671
- * @param {string} changeName
672
- * @param {object} context - 额外上下文(如 projectName)
673
- * @returns {ValidationResult}
674
- */
675
- export function runValidators(stageName, cwd, changeName, context = {}) {
676
- const contract = contracts[stageName]
677
- if (!contract || contract.validators.length === 0) {
678
- return { ok: true, errors: [], warnings: [] }
679
- }
680
-
681
- const allErrors = []
682
- const allWarnings = []
683
-
684
- for (const validator of contract.validators) {
685
- try {
686
- const result = validator(cwd, changeName, context)
687
- allErrors.push(...(result.errors || []))
688
- allWarnings.push(...(result.warnings || []))
689
- } catch (e) {
690
- allErrors.push(`校验器 ${validator.name || 'unknown'} 异常: ${e.message}`)
691
- }
692
- }
693
-
694
- return { ok: allErrors.length === 0, errors: allErrors, warnings: allWarnings }
695
- }
696
-
697
- /**
698
- * 获取所有主流程阶段
699
- */
700
- export { mainFlowStages, auxiliaryStages }