sillyspec 3.20.2 → 3.20.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/.claude/skills/sillyspec-archive/SKILL.md +21 -21
  2. package/.claude/skills/sillyspec-auto/SKILL.md +83 -83
  3. package/.claude/skills/sillyspec-brainstorm/SKILL.md +44 -44
  4. package/.claude/skills/sillyspec-commit/SKILL.md +106 -106
  5. package/.claude/skills/sillyspec-continue/SKILL.md +45 -45
  6. package/.claude/skills/sillyspec-doctor/SKILL.md +31 -31
  7. package/.claude/skills/sillyspec-execute/SKILL.md +30 -30
  8. package/.claude/skills/sillyspec-explore/SKILL.md +109 -109
  9. package/.claude/skills/sillyspec-knowledge/SKILL.md +269 -269
  10. package/.claude/skills/sillyspec-plan/SKILL.md +21 -21
  11. package/.claude/skills/sillyspec-propose/SKILL.md +21 -21
  12. package/.claude/skills/sillyspec-quick/SKILL.md +21 -21
  13. package/.claude/skills/sillyspec-resume/SKILL.md +68 -68
  14. package/.claude/skills/sillyspec-scan/SKILL.md +21 -21
  15. package/.claude/skills/sillyspec-state/SKILL.md +54 -54
  16. package/.claude/skills/sillyspec-status/SKILL.md +21 -21
  17. package/.claude/skills/sillyspec-verify/SKILL.md +21 -21
  18. package/.claude/skills/sillyspec-workspace/SKILL.md +157 -157
  19. package/.husky/pre-push +13 -13
  20. package/CLAUDE.md +18 -18
  21. package/README.md +198 -188
  22. package/SKILL.md +90 -91
  23. package/bin/sillyspec.js +2 -2
  24. package/docs/brainstorm-plan-contract.md +64 -64
  25. package/docs/plan-execute-contract.md +123 -123
  26. package/docs/platform-scan-protocol.md +298 -298
  27. package/docs/revision-mode.md +115 -115
  28. package/docs/sillyspec/file-lifecycle/known-implementation-gaps.md +99 -99
  29. package/docs/sillyspec/file-lifecycle/platform-workflows-sync.md +218 -218
  30. package/docs/sillyspec/file-lifecycle/stage-artifacts.md +167 -167
  31. package/docs/sillyspec/file-lifecycle/storage-and-state.md +148 -148
  32. package/docs/sillyspec/file-lifecycle/worktree-and-guard.md +211 -193
  33. package/docs/sillyspec/file-lifecycle.md +125 -125
  34. package/docs/workflow-contract-regression.md +106 -106
  35. package/docs/worktree-isolation.md +252 -252
  36. package/package.json +40 -40
  37. package/packages/dashboard/dist/assets/index-Bq_Z2hne.js +7446 -7446
  38. package/packages/dashboard/dist/assets/index-O2W5RV4z.css +1 -1
  39. package/packages/dashboard/dist/index.html +16 -16
  40. package/packages/dashboard/index.html +15 -15
  41. package/packages/dashboard/package-lock.json +2384 -2384
  42. package/packages/dashboard/package.json +25 -25
  43. package/packages/dashboard/server/executor.js +86 -86
  44. package/packages/dashboard/server/index.js +588 -588
  45. package/packages/dashboard/server/parser.js +526 -526
  46. package/packages/dashboard/server/watcher.js +344 -344
  47. package/packages/dashboard/src/App.vue +558 -558
  48. package/packages/dashboard/src/components/ActionBar.vue +93 -93
  49. package/packages/dashboard/src/components/CommandPalette.vue +96 -96
  50. package/packages/dashboard/src/components/DetailPanel.vue +137 -137
  51. package/packages/dashboard/src/components/LogStream.vue +65 -65
  52. package/packages/dashboard/src/components/PipelineStage.vue +95 -95
  53. package/packages/dashboard/src/components/PipelineView.vue +156 -156
  54. package/packages/dashboard/src/components/ProjectList.vue +210 -210
  55. package/packages/dashboard/src/components/StageBadge.vue +67 -67
  56. package/packages/dashboard/src/components/StepCard.vue +94 -94
  57. package/packages/dashboard/src/components/detail/DocsDetail.vue +48 -48
  58. package/packages/dashboard/src/components/detail/GitDetail.vue +61 -61
  59. package/packages/dashboard/src/components/detail/TechDetail.vue +43 -43
  60. package/packages/dashboard/src/composables/useDashboard.js +170 -170
  61. package/packages/dashboard/src/composables/useKeyboard.js +119 -119
  62. package/packages/dashboard/src/composables/useWebSocket.js +129 -129
  63. package/packages/dashboard/src/main.js +8 -8
  64. package/packages/dashboard/src/style.css +132 -132
  65. package/packages/dashboard/vite.config.js +18 -18
  66. package/src/brainstorm-postcheck.js +158 -158
  67. package/src/change-list.js +52 -52
  68. package/src/change-risk-profile.js +352 -352
  69. package/src/classify-change.js +73 -73
  70. package/src/constants.js +70 -70
  71. package/src/contract-matrix.js +278 -278
  72. package/src/db.js +201 -201
  73. package/src/endpoint-extractor.js +315 -315
  74. package/src/hooks/claude-pre-tool-use.cjs +125 -125
  75. package/src/hooks/worktree-guard.js +653 -653
  76. package/src/index.js +922 -900
  77. package/src/init.js +431 -431
  78. package/src/knowledge-match.js +130 -130
  79. package/src/migrate.js +117 -117
  80. package/src/modules.js +482 -482
  81. package/src/progress.js +1734 -1734
  82. package/src/run.js +3465 -3358
  83. package/src/scan-postcheck.js +387 -383
  84. package/src/setup.js +398 -398
  85. package/src/stage-contract.js +700 -700
  86. package/src/stages/archive.js +160 -160
  87. package/src/stages/brainstorm-auto.js +229 -229
  88. package/src/stages/brainstorm.js +645 -645
  89. package/src/stages/doctor.js +365 -365
  90. package/src/stages/execute.js +625 -625
  91. package/src/stages/explore.js +34 -34
  92. package/src/stages/index.js +29 -29
  93. package/src/stages/knowledge.js +498 -498
  94. package/src/stages/plan-postcheck.js +511 -513
  95. package/src/stages/plan.js +582 -582
  96. package/src/stages/propose.js +174 -174
  97. package/src/stages/quick.js +82 -82
  98. package/src/stages/scan.js +558 -558
  99. package/src/stages/status.js +65 -65
  100. package/src/stages/verify.js +322 -322
  101. package/src/sync.js +497 -497
  102. package/src/task-review.js +346 -346
  103. package/src/workflow.js +785 -785
  104. package/src/worktree-apply.js +549 -549
  105. package/src/worktree-deps.js +185 -0
  106. package/src/worktree.js +982 -932
  107. package/templates/workflows/archive-impact.yaml +79 -79
  108. package/templates/workflows/scan-docs.yaml +132 -132
  109. package/test/brainstorm-plan-contract.test.mjs +273 -273
  110. package/test/check-syntax.mjs +26 -26
  111. package/test/contract-artifacts.test.mjs +323 -323
  112. package/test/decision-supersede.test.mjs +277 -277
  113. package/test/knowledge-match.test.mjs +231 -231
  114. package/test/plan-execute-contract.test.mjs +330 -330
  115. package/test/plan-optimization.test.mjs +572 -572
  116. package/test/platform-artifacts.test.mjs +166 -166
  117. package/test/platform-failure-samples.test.mjs +199 -199
  118. package/test/platform-recovery-chain.test.mjs +167 -167
  119. package/test/platform-recovery.test.mjs +136 -136
  120. package/test/platform-scan-p0.test.mjs +168 -168
  121. package/test/revision-v1.test.mjs +1145 -1145
  122. package/test/run-scan-project-parse.test.mjs +200 -200
  123. package/test/run-tests.mjs +48 -48
  124. package/test/scan-knowledge.test.mjs +175 -175
  125. package/test/scan-paths.test.mjs +68 -68
  126. package/test/scan-postcheck.test.mjs +197 -197
  127. package/test/spec-dir.test.mjs +206 -206
  128. package/test/stage-contract.test.mjs +299 -299
  129. package/test/stage-definitions.test.mjs +39 -39
  130. package/test/wait-gates.test.mjs +496 -496
  131. package/test/worktree-deps-provision.test.mjs +148 -0
  132. package/test/worktree-guard.test.mjs +71 -71
  133. package/test/worktree-native-overlay.test.mjs +188 -188
@@ -1,700 +1,700 @@
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 }
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 }