sillyspec 3.18.0 → 3.18.1
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.
- package/package.json +1 -1
- package/src/db.js +4 -0
- package/src/hooks/worktree-guard.js +97 -4
- package/src/index.js +1 -1
- package/src/progress.js +41 -14
- package/src/run.js +255 -66
- package/src/stage-contract.js +244 -12
- package/src/stages/brainstorm.js +228 -8
- package/src/stages/index.js +0 -2
- package/src/stages/plan.js +55 -18
- package/src/stages/propose.js +30 -4
- package/src/stages/quick.js +13 -10
- package/src/stages/scan.js +12 -0
- package/src/stages/verify.js +31 -13
- package/test/platform-artifacts.test.mjs +14 -5
- package/test/platform-failure-samples.test.mjs +3 -2
- package/test/platform-recovery-chain.test.mjs +10 -9
- package/test/platform-recovery.test.mjs +13 -5
- package/test/platform-scan-p0.test.mjs +3 -0
- package/test/scan-postcheck.test.mjs +3 -2
- package/test/spec-dir.test.mjs +2 -1
- package/test/stage-contract.test.mjs +119 -6
- package/test/stage-definitions.test.mjs +2 -6
- package/test/wait-gates.test.mjs +501 -0
- package/test/worktree-guard.test.mjs +58 -0
- package/.npmrc.bak +0 -0
package/src/stage-contract.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* CLI 不再相信 prompt 完成,completeStep 后必须过 validator。
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { existsSync, readdirSync } from 'fs'
|
|
8
|
+
import { existsSync, readdirSync, readFileSync } from 'fs'
|
|
9
9
|
import { join, basename } from 'path'
|
|
10
10
|
|
|
11
11
|
/**
|
|
@@ -26,6 +26,117 @@ import { join, basename } from 'path'
|
|
|
26
26
|
|
|
27
27
|
// ============ Validators ============
|
|
28
28
|
|
|
29
|
+
function resolveChangeDir(cwd, changeName, specRoot = null) {
|
|
30
|
+
const changesRoot = specRoot ? join(specRoot, 'changes') : join(cwd, '.sillyspec', 'changes')
|
|
31
|
+
return join(changesRoot, changeName)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function collectIdsFromLine(line, re, ids) {
|
|
35
|
+
for (const match of line.matchAll(re)) {
|
|
36
|
+
ids.add(match[0].toUpperCase())
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function extractIds(content, prefix) {
|
|
41
|
+
if (!content) return []
|
|
42
|
+
const ids = new Set()
|
|
43
|
+
const idRe = new RegExp(`\\b${prefix}-\\d+(?:@v\\d+)?\\b`, 'gi')
|
|
44
|
+
const headingLineRe = /^\s{0,3}#{1,6}\s+/i
|
|
45
|
+
const fieldLineRe = /^\s*(?:[-*]\s*)?(?:id|decision[-_ ]?ids?|requirement[-_ ]?ids?|covers?|coverage|references?|impacts?|覆盖(?:来源|决策|需求)?)\s*[::]/i
|
|
46
|
+
const tableLineRe = /^\s*\|/
|
|
47
|
+
const listStartsWithIdRe = new RegExp(`^\\s*(?:[-*]|\\d+\\.)\\s*(?:\\[[ xX]\\]\\s*)?${prefix}-\\d+(?:@v\\d+)?\\b`, 'i')
|
|
48
|
+
|
|
49
|
+
for (const line of content.split(/\r?\n/)) {
|
|
50
|
+
if (!headingLineRe.test(line) && !fieldLineRe.test(line) && !tableLineRe.test(line) && !listStartsWithIdRe.test(line)) continue
|
|
51
|
+
collectIdsFromLine(line, idRe, ids)
|
|
52
|
+
}
|
|
53
|
+
return [...ids].sort()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function readDecisionField(body, fieldPattern, fallback = '') {
|
|
57
|
+
const re = new RegExp(`^\\s*(?:[-*]\\s*)?(?:${fieldPattern})\\s*[::]\\s*([^\\n]+)`, 'im')
|
|
58
|
+
return (body.match(re)?.[1] || fallback).trim()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function buildDecisionRecord(id, body) {
|
|
62
|
+
const status = readDecisionField(body, 'status', 'accepted').toLowerCase()
|
|
63
|
+
const blockerValue = readDecisionField(body, 'blocker', 'false').toLowerCase()
|
|
64
|
+
const blocker = ['true', 'yes', '1'].includes(blockerValue)
|
|
65
|
+
const priorityValue = readDecisionField(body, 'priority|level|severity')
|
|
66
|
+
const priorityMissing = priorityValue.length === 0
|
|
67
|
+
const fallbackPriority = (['unresolved', 'blocking'].includes(status) || blocker) ? 'P1' : 'P2'
|
|
68
|
+
const priority = (priorityValue.match(/P[0-2]/i)?.[0] || fallbackPriority).toUpperCase()
|
|
69
|
+
return { id: id.toUpperCase(), body, status, priority, blocker, priorityMissing }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function findNextDecisionBoundary(content, startIndex) {
|
|
73
|
+
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
|
|
74
|
+
boundaryRe.lastIndex = startIndex
|
|
75
|
+
const next = boundaryRe.exec(content)
|
|
76
|
+
return next ? next.index : content.length
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function isInsideRange(index, ranges) {
|
|
80
|
+
return ranges.some(range => index >= range.start && index < range.end)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function parseDecisionRecords(content) {
|
|
84
|
+
if (!content) return []
|
|
85
|
+
const records = []
|
|
86
|
+
const ranges = []
|
|
87
|
+
const headingRe = /^\s{0,3}#{2,6}\s+(D-\d+(?:@v\d+)?)(?:\b|:)[^\n]*$/gmi
|
|
88
|
+
const headings = []
|
|
89
|
+
let match
|
|
90
|
+
while ((match = headingRe.exec(content)) !== null) {
|
|
91
|
+
headings.push({ id: match[1].toUpperCase(), index: match.index, end: headingRe.lastIndex })
|
|
92
|
+
}
|
|
93
|
+
for (let i = 0; i < headings.length; i++) {
|
|
94
|
+
const current = headings[i]
|
|
95
|
+
const next = headings[i + 1]
|
|
96
|
+
const body = content.slice(current.end, next ? next.index : content.length)
|
|
97
|
+
const end = next ? next.index : content.length
|
|
98
|
+
ranges.push({ start: current.index, end })
|
|
99
|
+
records.push(buildDecisionRecord(current.id, body))
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const idLineRe = /^\s*(?:[-*]\s*)?(?:id|decision[-_ ]?id|decision)\s*[::]\s*(D-\d+(?:@v\d+)?)(?:\b|$)/gmi
|
|
103
|
+
while ((match = idLineRe.exec(content)) !== null) {
|
|
104
|
+
if (isInsideRange(match.index, ranges)) continue
|
|
105
|
+
const bodyEnd = findNextDecisionBoundary(content, idLineRe.lastIndex)
|
|
106
|
+
const body = content.slice(match.index, bodyEnd)
|
|
107
|
+
records.push(buildDecisionRecord(match[1], body))
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return records
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function extractCurrentDecisionIds(content) {
|
|
114
|
+
const records = parseDecisionRecords(content)
|
|
115
|
+
if (records.length === 0) return extractIds(content, 'D')
|
|
116
|
+
return records
|
|
117
|
+
.filter(r => !['superseded', 'rejected'].includes(r.status))
|
|
118
|
+
.map(r => r.id)
|
|
119
|
+
.sort()
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function findBlockingDecisionIssues(content) {
|
|
123
|
+
return parseDecisionRecords(content)
|
|
124
|
+
.filter(r => (r.blocker || ['unresolved', 'blocking'].includes(r.status)) && ['P0', 'P1'].includes(r.priority))
|
|
125
|
+
.map(r => `${r.id} (${r.priority}${r.priorityMissing ? ', priority=missing->P1' : ''}, status=${r.status})`)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function readIfExists(file) {
|
|
129
|
+
return existsSync(file) ? readFileSync(file, 'utf8') : ''
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function warnMissingIds(warnings, ids, targetContent, targetName, sourceName) {
|
|
133
|
+
for (const id of ids) {
|
|
134
|
+
if (!targetContent.toUpperCase().includes(id)) {
|
|
135
|
+
warnings.push(`${targetName} 未引用 ${sourceName} 中的 ${id}`)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
29
140
|
/**
|
|
30
141
|
* scan 完成校验:检查 7 份 scan 文档 + manifest
|
|
31
142
|
*/
|
|
@@ -55,7 +166,7 @@ function validateScanOutputs(cwd, changeName, context = {}) {
|
|
|
55
166
|
|
|
56
167
|
for (const doc of requiredDocs) {
|
|
57
168
|
if (!existsSync(join(docsRoot, doc))) {
|
|
58
|
-
errors.push(`scan 文档缺失: ${docsRoot
|
|
169
|
+
errors.push(`scan 文档缺失: ${join(docsRoot, doc)}`)
|
|
59
170
|
}
|
|
60
171
|
}
|
|
61
172
|
|
|
@@ -75,12 +186,93 @@ function validateScanOutputs(cwd, changeName, context = {}) {
|
|
|
75
186
|
return { ok: errors.length === 0, errors, warnings }
|
|
76
187
|
}
|
|
77
188
|
|
|
189
|
+
/**
|
|
190
|
+
* brainstorm 完成校验:检查四件套规范文件是否生成
|
|
191
|
+
*/
|
|
192
|
+
function validateBrainstormOutputs(cwd, changeName, context = {}) {
|
|
193
|
+
const { specRoot } = context
|
|
194
|
+
const changesRoot = specRoot ? join(specRoot, 'changes') : join(cwd, '.sillyspec', 'changes')
|
|
195
|
+
if (specRoot && !existsSync(changesRoot)) {
|
|
196
|
+
return { ok: false, errors: [`平台模式 specRoot 缺少 changes 目录: ${changesRoot}`], warnings: [] }
|
|
197
|
+
}
|
|
198
|
+
const changeDir = resolveChangeDir(cwd, changeName, specRoot)
|
|
199
|
+
const errors = []
|
|
200
|
+
const warnings = []
|
|
201
|
+
|
|
202
|
+
const requiredFiles = ['design.md', 'proposal.md', 'requirements.md', 'tasks.md']
|
|
203
|
+
|
|
204
|
+
for (const file of requiredFiles) {
|
|
205
|
+
if (!existsSync(join(changeDir, file))) {
|
|
206
|
+
errors.push(`brainstorm 产物缺失: ${join(changeDir, file)}`)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// 内容校验(文件存在时检查关键章节)
|
|
211
|
+
if (existsSync(join(changeDir, 'proposal.md'))) {
|
|
212
|
+
const content = readFileSync(join(changeDir, 'proposal.md'), 'utf8')
|
|
213
|
+
if (!content.includes('不在范围内') && !content.includes('Non-Goals') && !content.includes('非目标')) {
|
|
214
|
+
warnings.push('proposal.md 缺少「不在范围内/Non-Goals」章节')
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (existsSync(join(changeDir, 'requirements.md'))) {
|
|
219
|
+
const content = readFileSync(join(changeDir, 'requirements.md'), 'utf8')
|
|
220
|
+
if (!/FR-\d+/i.test(content)) {
|
|
221
|
+
warnings.push('requirements.md 缺少 FR 编号的需求项')
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (existsSync(join(changeDir, 'design.md'))) {
|
|
226
|
+
const content = readFileSync(join(changeDir, 'design.md'), 'utf8')
|
|
227
|
+
if (!content.includes('文件变更清单') && !content.includes('File Changes') && !content.includes('文件清单')) {
|
|
228
|
+
warnings.push('design.md 缺少「文件变更清单」章节')
|
|
229
|
+
}
|
|
230
|
+
if (!content.includes('风险登记') && !content.includes('Risk') && !content.includes('风险')) {
|
|
231
|
+
warnings.push('design.md 缺少「风险登记」章节')
|
|
232
|
+
}
|
|
233
|
+
if (!content.includes('自审') && !content.includes('Self-Review') && !content.includes('Self-review')) {
|
|
234
|
+
warnings.push('design.md 缺少「自审」章节')
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (existsSync(join(changeDir, 'tasks.md'))) {
|
|
239
|
+
const content = readFileSync(join(changeDir, 'tasks.md'), 'utf8')
|
|
240
|
+
const lines = content.split('\n').filter(l => l.trim().startsWith('-') || l.trim().startsWith('*') || /^\d+\./.test(l.trim()))
|
|
241
|
+
if (lines.length === 0) {
|
|
242
|
+
warnings.push('tasks.md 没有任务列表项')
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const decisionsFile = join(changeDir, 'decisions.md')
|
|
247
|
+
if (existsSync(decisionsFile)) {
|
|
248
|
+
const decisions = readFileSync(decisionsFile, 'utf8')
|
|
249
|
+
const blockers = findBlockingDecisionIssues(decisions)
|
|
250
|
+
for (const issue of blockers) {
|
|
251
|
+
errors.push(`decisions.md 存在 P0/P1 未决阻塞: ${issue}`)
|
|
252
|
+
}
|
|
253
|
+
const decisionIds = extractCurrentDecisionIds(decisions)
|
|
254
|
+
if (decisionIds.length === 0) {
|
|
255
|
+
warnings.push('decisions.md 存在但没有当前版本 D-xxx@vN 决策 ID')
|
|
256
|
+
} else {
|
|
257
|
+
const design = readIfExists(join(changeDir, 'design.md'))
|
|
258
|
+
const requirements = readIfExists(join(changeDir, 'requirements.md'))
|
|
259
|
+
const tasks = readIfExists(join(changeDir, 'tasks.md'))
|
|
260
|
+
warnMissingIds(warnings, decisionIds, design, 'design.md', 'decisions.md')
|
|
261
|
+
warnMissingIds(warnings, decisionIds, requirements, 'requirements.md', 'decisions.md')
|
|
262
|
+
warnMissingIds(warnings, decisionIds, tasks, 'tasks.md', 'decisions.md')
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return { ok: errors.length === 0, errors, warnings }
|
|
267
|
+
}
|
|
268
|
+
|
|
78
269
|
/**
|
|
79
270
|
* plan 完成校验:检查 plan.md 生成
|
|
80
271
|
*/
|
|
81
|
-
function validatePlanOutputs(cwd, changeName) {
|
|
82
|
-
const
|
|
83
|
-
const
|
|
272
|
+
function validatePlanOutputs(cwd, changeName, context = {}) {
|
|
273
|
+
const { specRoot } = context
|
|
274
|
+
const changeDir = resolveChangeDir(cwd, changeName, specRoot)
|
|
275
|
+
const planFile = join(changeDir, 'plan.md')
|
|
84
276
|
const errors = []
|
|
85
277
|
|
|
86
278
|
if (!existsSync(planFile)) {
|
|
@@ -88,20 +280,60 @@ function validatePlanOutputs(cwd, changeName) {
|
|
|
88
280
|
}
|
|
89
281
|
|
|
90
282
|
const warnings = []
|
|
283
|
+
if (existsSync(planFile)) {
|
|
284
|
+
const plan = readFileSync(planFile, 'utf8')
|
|
285
|
+
const requirements = readIfExists(join(changeDir, 'requirements.md'))
|
|
286
|
+
const requirementIds = extractIds(requirements, 'FR')
|
|
287
|
+
warnMissingIds(warnings, requirementIds, plan, 'plan.md', 'requirements.md')
|
|
288
|
+
|
|
289
|
+
const decisions = readIfExists(join(changeDir, 'decisions.md'))
|
|
290
|
+
const blockers = findBlockingDecisionIssues(decisions)
|
|
291
|
+
for (const issue of blockers) {
|
|
292
|
+
errors.push(`decisions.md 存在 P0/P1 未决阻塞: ${issue}`)
|
|
293
|
+
}
|
|
294
|
+
const decisionIds = extractCurrentDecisionIds(decisions)
|
|
295
|
+
warnMissingIds(warnings, decisionIds, plan, 'plan.md', 'decisions.md')
|
|
296
|
+
}
|
|
91
297
|
return { ok: errors.length === 0, errors, warnings }
|
|
92
298
|
}
|
|
93
299
|
|
|
94
300
|
/**
|
|
95
|
-
* verify
|
|
301
|
+
* verify 完成校验:检查变更目录和 verify 产物
|
|
96
302
|
*/
|
|
97
|
-
function validateVerifyOutputs(cwd, changeName) {
|
|
98
|
-
const
|
|
303
|
+
function validateVerifyOutputs(cwd, changeName, context = {}) {
|
|
304
|
+
const { specRoot } = context
|
|
305
|
+
const changeDir = resolveChangeDir(cwd, changeName, specRoot)
|
|
99
306
|
const errors = []
|
|
100
307
|
const warnings = []
|
|
101
308
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
errors
|
|
309
|
+
if (!existsSync(changeDir)) {
|
|
310
|
+
errors.push(`变更目录缺失: ${changeDir}`)
|
|
311
|
+
return { ok: false, errors, warnings }
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// verify 阶段应该产出 verify-result.md(或类似报告)
|
|
315
|
+
const verifyResult = join(changeDir, 'verify-result.md')
|
|
316
|
+
if (!existsSync(verifyResult)) {
|
|
317
|
+
warnings.push('verify-result.md 不存在(verify 阶段建议产出验证报告)')
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// 确保核心规范文件仍然存在
|
|
321
|
+
const requiredDocs = ['design.md', 'plan.md']
|
|
322
|
+
for (const doc of requiredDocs) {
|
|
323
|
+
if (!existsSync(join(changeDir, doc))) {
|
|
324
|
+
errors.push(`核心文档缺失: ${join(changeDir, doc)}`)
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (existsSync(verifyResult)) {
|
|
329
|
+
const verify = readFileSync(verifyResult, 'utf8')
|
|
330
|
+
const decisions = readIfExists(join(changeDir, 'decisions.md'))
|
|
331
|
+
const blockers = findBlockingDecisionIssues(decisions)
|
|
332
|
+
for (const issue of blockers) {
|
|
333
|
+
errors.push(`decisions.md 存在 P0/P1 未决阻塞: ${issue}`)
|
|
334
|
+
}
|
|
335
|
+
const decisionIds = extractCurrentDecisionIds(decisions)
|
|
336
|
+
warnMissingIds(warnings, decisionIds, verify, 'verify-result.md', 'decisions.md')
|
|
105
337
|
}
|
|
106
338
|
|
|
107
339
|
return { ok: errors.length === 0, errors, warnings }
|
|
@@ -187,7 +419,7 @@ const contracts = {
|
|
|
187
419
|
description: '需求分析与设计',
|
|
188
420
|
allowedFrom: [], // 任何变更的起始阶段
|
|
189
421
|
allowedTo: ['plan'],
|
|
190
|
-
validators: [],
|
|
422
|
+
validators: [validateBrainstormOutputs],
|
|
191
423
|
},
|
|
192
424
|
plan: {
|
|
193
425
|
stage: 'plan',
|
package/src/stages/brainstorm.js
CHANGED
|
@@ -98,6 +98,9 @@ export const definition = {
|
|
|
98
98
|
},
|
|
99
99
|
{
|
|
100
100
|
name: '需求范围评估',
|
|
101
|
+
conditionalWait: true,
|
|
102
|
+
waitReason: '等待用户确认拆分/批量模式方案',
|
|
103
|
+
waitOptions: ['同意拆分', '不需要拆分', '走批量模式'],
|
|
101
104
|
prompt: `评估需求复杂度,判断是否需要拆分或走批量模式。
|
|
102
105
|
|
|
103
106
|
### 操作
|
|
@@ -146,6 +149,11 @@ export const definition = {
|
|
|
146
149
|
},
|
|
147
150
|
{
|
|
148
151
|
name: '对话式探索',
|
|
152
|
+
requiresWait: true,
|
|
153
|
+
repeatableWait: true,
|
|
154
|
+
maxWaitRounds: 3,
|
|
155
|
+
waitReason: '等待用户回答需求问题',
|
|
156
|
+
waitOptions: ['继续补充', '信息够了,进入方案讨论'],
|
|
149
157
|
prompt: `通过对话探索需求细节。
|
|
150
158
|
|
|
151
159
|
### 操作
|
|
@@ -172,13 +180,84 @@ export const definition = {
|
|
|
172
180
|
outputHint: '需求理解摘要',
|
|
173
181
|
optional: false
|
|
174
182
|
},
|
|
183
|
+
{
|
|
184
|
+
name: '需求澄清 Grill',
|
|
185
|
+
conditionalWait: true,
|
|
186
|
+
repeatableWait: true,
|
|
187
|
+
maxWaitRounds: 8,
|
|
188
|
+
waitReason: '等待用户回答需求澄清 Grill',
|
|
189
|
+
waitOptions: ['回答见--answer', '信息够了,结束需求澄清'],
|
|
190
|
+
prompt: `执行可选的需求澄清 Grill pass。
|
|
191
|
+
|
|
192
|
+
### 定位
|
|
193
|
+
这是 design.md 之前的需求澄清,不是设计后的 Design Grill。目标是把需求/术语/边界中仍需要人类判断的点问清楚;Design Grill 后续仍会默认执行,用来审查已经写出的 design.md 是否自洽。
|
|
194
|
+
|
|
195
|
+
### 入口判断
|
|
196
|
+
1. 汇总「对话式探索」后仍未稳定的歧义点,按类型列出:
|
|
197
|
+
- 术语歧义:同一个词可能指向不同实体/角色/状态
|
|
198
|
+
- 边界歧义:哪些场景做、哪些不做、失败怎么处理
|
|
199
|
+
- 前提风险:这个需求是否不该存在,是否已有更简单的现有方案
|
|
200
|
+
- 代码冲突:用户描述与现有代码/scan/module 文档不一致
|
|
201
|
+
2. 能通过代码或文档确认的不要问用户,先读取:
|
|
202
|
+
- \`.sillyspec/docs/<project>/scan/ARCHITECTURE.md\`
|
|
203
|
+
- \`.sillyspec/docs/<project>/scan/CONVENTIONS.md\`
|
|
204
|
+
- \`.sillyspec/docs/<project>/modules/_module-map.yaml\`
|
|
205
|
+
- 相关源码文件
|
|
206
|
+
3. 给每个未解决歧义分级:
|
|
207
|
+
- P0:影响数据模型、权限边界、状态机/工作流、兼容策略、不可逆架构取舍、跨模块所有权
|
|
208
|
+
- P1:影响用户场景、验收标准、错误处理、默认值
|
|
209
|
+
- P2:文案、展示细节、低风险交互偏好
|
|
210
|
+
4. 执行规则:
|
|
211
|
+
- P1/P2 歧义 0-2 个且无 P0:输出"需求澄清 Grill skipped",在后续设计中内联处理并记录依据
|
|
212
|
+
- P1/P2 歧义 >= 3 个:进入本 pass,按优先级逐个澄清
|
|
213
|
+
- 任意 P0 歧义:进入本 pass;如果需要用户判断,必须暂停问一个问题
|
|
214
|
+
5. 不要问用户"要不要 Grill"。本步骤由 AI 根据歧义风险决定是否执行;只在需要业务判断/取舍时等待用户回答。
|
|
215
|
+
|
|
216
|
+
### 追问策略
|
|
217
|
+
1. **一次只问一个问题**:按 P0 → P1 → P2 顺序,深度优先处理最关键歧义。
|
|
218
|
+
2. **能查代码就不问**:如果问题可由源码、scan 文档、模块文档回答,先查证并给出结论;只有业务判断/取舍才问用户。
|
|
219
|
+
3. **术语碰撞立即指出**:用户用词与 glossary/代码实体/模块文档冲突时,当场说明冲突并要求选择 canonical term。
|
|
220
|
+
4. **模糊词精化**:把"账户/任务/状态/会话/执行"这类多义词拆成明确实体或状态。
|
|
221
|
+
5. **场景压力测试**:用具体 case 逼出边界,例如失败重试、部分成功、历史数据、权限不足、并发修改、兼容旧配置。
|
|
222
|
+
6. **前提挑战优先**:如果现有设计或代码已有简单路径,先说明"可能不该新增",不要直接优化错误前提。
|
|
223
|
+
|
|
224
|
+
### 决策记录草稿
|
|
225
|
+
每解决一个有实现影响的问题,生成一个稳定 ID 的记录草稿。不要把闲聊都记录进去。
|
|
226
|
+
|
|
227
|
+
\`\`\`markdown
|
|
228
|
+
## D-001@v1: <短标题>
|
|
229
|
+
- type: term | boundary | premise | architecture | compatibility | risk
|
|
230
|
+
- status: accepted | rejected | superseded
|
|
231
|
+
- source: user | code | docs
|
|
232
|
+
- question: <被解决的问题>
|
|
233
|
+
- answer: <用户确认或代码查证结果>
|
|
234
|
+
- normalized_requirement: <可测试的约束>
|
|
235
|
+
- impacts: [FR-?, task-?, verify-?]
|
|
236
|
+
- evidence: <文件路径/代码位置/用户回答轮次>
|
|
237
|
+
\`\`\`
|
|
238
|
+
|
|
239
|
+
### 铁律 — 等待用户
|
|
240
|
+
- 每轮最多提出一个问题,然后调用:
|
|
241
|
+
\`sillyspec run brainstorm --wait --reason "等待用户回答需求澄清 Grill" --options "回答见--answer,信息够了,结束需求澄清" --output "你的单个问题或查证结论"\`
|
|
242
|
+
- 用户通过 \`--continue --answer "回答"\` 回答后,本步骤会再次执行;继续处理下一个最关键歧义。
|
|
243
|
+
- 达到 maxWaitRounds=8 后,必须总结已确认内容和剩余风险,不要无限追问。
|
|
244
|
+
|
|
245
|
+
### 输出
|
|
246
|
+
需求澄清结论摘要 + D-xxx@vN 决策记录草稿 + 剩余风险(如有)`,
|
|
247
|
+
outputHint: '需求澄清和决策记录草稿',
|
|
248
|
+
optional: true
|
|
249
|
+
},
|
|
175
250
|
{
|
|
176
251
|
name: '提出 2-3 种方案',
|
|
177
|
-
|
|
252
|
+
requiresWait: true,
|
|
253
|
+
waitReason: '等待用户选择方案',
|
|
254
|
+
waitOptions: ['方案A', '方案B', '方案C'],
|
|
255
|
+
prompt: `基于需求理解和 Grill 结果,提出 2-3 种实现方案。
|
|
178
256
|
|
|
179
257
|
### 操作
|
|
180
258
|
1. 每种方案列出:核心思路、优势、劣势
|
|
181
|
-
2.
|
|
259
|
+
2. 如果 Grill 产生 D-xxx@vN 决策记录,方案必须说明覆盖/违反哪些当前版本决策
|
|
260
|
+
3. 给出推荐方案和理由
|
|
182
261
|
|
|
183
262
|
### 铁律 — 必须等待用户选择方案
|
|
184
263
|
- **不要替用户选择方案。** 列出方案对比表和推荐后,必须暂停等待用户选择。
|
|
@@ -197,6 +276,9 @@ export const definition = {
|
|
|
197
276
|
},
|
|
198
277
|
{
|
|
199
278
|
name: '分段展示设计',
|
|
279
|
+
requiresWait: true,
|
|
280
|
+
waitReason: '等待用户确认设计方案',
|
|
281
|
+
waitOptions: ['确认', '需要修改', '推翻重来'],
|
|
200
282
|
prompt: `展示完整设计方案供用户确认。
|
|
201
283
|
|
|
202
284
|
### 操作
|
|
@@ -273,14 +355,26 @@ HTML 原型文件路径(或"跳过"如果不适合)`,
|
|
|
273
355
|
|---|---|---|---|
|
|
274
356
|
| R-01 | ... | P0/P1/P2 | ... |
|
|
275
357
|
|
|
276
|
-
11.
|
|
358
|
+
11. **决策追踪**(如存在 Grill/重大决策):
|
|
359
|
+
- 列出当前版本 D-xxx@vN 决策 ID
|
|
360
|
+
- 说明每个 D-xxx@vN 被哪些 FR-xxx / 设计章节覆盖
|
|
361
|
+
- 标注仍未解决的 D-xxx@vN 或剩余风险
|
|
362
|
+
12. **自审**(AI 对自身设计的校验)
|
|
277
363
|
|
|
278
364
|
### 操作
|
|
279
365
|
1. 确认变更目录存在:\`mkdir -p .sillyspec/changes/<change-name>\`(Windows 用 \`mkdir .sillyspec\\changes\\<变更名>\` 或 PowerShell \`New-Item -ItemType Directory -Force -Path .sillyspec/changes/<change-name>\`)
|
|
280
366
|
- 变更名格式必须为 \`YYYY-MM-DD-<简短描述>\`(如 \`2026-05-13-user-auth\`)
|
|
281
367
|
2. 将确认的设计写入 \`.sillyspec/changes/<change-name>/design.md\`
|
|
282
|
-
3.
|
|
368
|
+
3. 如果 Grill 或方案讨论产生了实现相关决策,写入 \`.sillyspec/changes/<change-name>/decisions.md\`:
|
|
369
|
+
- decisions.md 是本次变更的决策台账,不是长期术语表
|
|
370
|
+
- 只记录有实现/验收影响的决策,闲聊和低风险偏好不记录
|
|
371
|
+
- 每条记录必须有稳定版本 ID:D-001@v1、D-002@v1 ...
|
|
372
|
+
- 若后续 Design Grill 修正该决策,新记录使用 D-001@v2,并写明 supersedes: D-001@v1
|
|
373
|
+
- 每条记录必须包含:type、status、source、question、answer、normalized_requirement、impacts、evidence、priority
|
|
374
|
+
- 长期术语只在 archive/scan 时再提升到 \`.sillyspec/docs/<project>/glossary.md\`
|
|
375
|
+
4. 自审检查:
|
|
283
376
|
- 需求覆盖:是否完整覆盖对话式探索中确认的需求
|
|
377
|
+
- Grill 覆盖:如果存在 decisions.md,design.md 是否引用所有当前版本 D-xxx@vN
|
|
284
378
|
- 约束一致性:是否与 CONVENTIONS.md、ARCHITECTURE.md 一致
|
|
285
379
|
- 真实性:表名/字段名/类名/方法名来自真实代码或标注"新增"
|
|
286
380
|
- YAGNI:是否包含不必要功能
|
|
@@ -288,8 +382,8 @@ HTML 原型文件路径(或"跳过"如果不适合)`,
|
|
|
288
382
|
- 非目标清晰:是否明确界定了不做的事
|
|
289
383
|
- 兼容策略(brownfield):是否说明了回退路径
|
|
290
384
|
- 风险识别:是否识别了关键技术风险和对策
|
|
291
|
-
|
|
292
|
-
|
|
385
|
+
5. 自审发现问题 → 修改后重新检查
|
|
386
|
+
6. 全部通过 → 进入下一步
|
|
293
387
|
|
|
294
388
|
### 输出
|
|
295
389
|
design.md 文件路径 + 自审结果
|
|
@@ -300,8 +394,108 @@ design.md 文件路径 + 自审结果
|
|
|
300
394
|
outputHint: 'design.md 文件路径 + 自审结果',
|
|
301
395
|
optional: false
|
|
302
396
|
},
|
|
397
|
+
{
|
|
398
|
+
name: 'Design Grill 交叉审查',
|
|
399
|
+
conditionalWait: true,
|
|
400
|
+
waitReason: '等待用户处理 Design Grill 发现的结构性问题',
|
|
401
|
+
waitOptions: ['按推荐修正', '补充回答', '显式跳过'],
|
|
402
|
+
prompt: `默认执行 Design Grill,对已经写出的 design.md 做交叉审查。
|
|
403
|
+
|
|
404
|
+
### 定位
|
|
405
|
+
这是设计完成后的质量门,不是需求探索。目标不是继续发散,而是找出 design.md 内部、四件套之间、文档与外部约束之间的结构性矛盾。
|
|
406
|
+
|
|
407
|
+
### 默认行为
|
|
408
|
+
1. 默认必须执行一次交叉审查;不要让用户凭主观判断决定"要不要 Grill"。
|
|
409
|
+
2. 只有以下情况可以轻量跳过,并必须记录原因:
|
|
410
|
+
- 用户明确要求 no-grill / 显式跳过
|
|
411
|
+
- 文档是一页以内、单模块、无状态流转、无 schema/API/兼容策略变更
|
|
412
|
+
- plan_level 明确为 none,且只改 1-2 个文件
|
|
413
|
+
3. 即使跳过,也要输出"Design Grill skipped"和原因,不能静默跳过。
|
|
414
|
+
|
|
415
|
+
### 输入材料
|
|
416
|
+
1. 必须读取完整 \`.sillyspec/changes/<change-name>/design.md\`
|
|
417
|
+
2. 读取 proposal.md、requirements.md、tasks.md、decisions.md(如存在)
|
|
418
|
+
3. 读取 scan/module docs:
|
|
419
|
+
- \`.sillyspec/docs/<project>/scan/ARCHITECTURE.md\`
|
|
420
|
+
- \`.sillyspec/docs/<project>/scan/CONVENTIONS.md\`
|
|
421
|
+
- \`.sillyspec/docs/<project>/modules/_module-map.yaml\`
|
|
422
|
+
- 命中的模块文档
|
|
423
|
+
4. 按 design.md 文件变更清单读取相关源码、测试、配置、schema 或样例数据;矛盾经常藏在设计与外部约束交叉处,素材宁可多读,不要只读摘要。
|
|
424
|
+
|
|
425
|
+
### 交叉审查模型
|
|
426
|
+
按三层检查并输出 cross-check matrix:
|
|
427
|
+
1. **定义层**:模糊概念是否有可测试定义。例如"高可用""异常数据""本地缓存""重试"。
|
|
428
|
+
2. **一致性层**:跨章节/跨产物是否打架。例如数据流 vs 容错策略、schema vs 输入格式、非目标 vs tasks。
|
|
429
|
+
3. **可行性层**:关键假设是否有来源。例如 P99 延迟、上游 SLA、缓存 TTL、数据量、权限模型、兼容旧配置。
|
|
430
|
+
|
|
431
|
+
### 交叉点抽取
|
|
432
|
+
重点找这些交叉点:
|
|
433
|
+
- 模块 A 依赖模块 B 的实体/状态/接口
|
|
434
|
+
- requirements.md 的 FR 与 design.md 的数据模型/API/状态机
|
|
435
|
+
- design.md 的容错策略与数据流、缓存、重试、回滚
|
|
436
|
+
- tasks.md 的执行范围与 design.md 的非目标
|
|
437
|
+
- decisions.md 的 D-xxx@vN 与 design.md 当前说法
|
|
438
|
+
- scan/module docs 或源码中的真实约束与 design.md 假设
|
|
439
|
+
|
|
440
|
+
### 问答处理
|
|
441
|
+
1. 先自动交叉审查,不要一上来问用户。
|
|
442
|
+
2. 没有结构性问题:正常完成,输出"Design Grill passed",附 cross-check matrix。
|
|
443
|
+
3. 发现问题:
|
|
444
|
+
- 对能从代码/文档确定的问题,直接给出推荐修正。
|
|
445
|
+
- 对需要业务判断的问题,每次只问一个最关键问题,然后等待用户。
|
|
446
|
+
- P0/P1 未决项必须进入 Unresolved Blockers,不能带着进入 plan。
|
|
447
|
+
4. 用户回答后,更新 design.md 和 decisions.md;如果推翻旧决策,新增版本 D-xxx@v2,而不是覆盖 D-xxx@v1。
|
|
448
|
+
|
|
449
|
+
### decisions.md 版本规则
|
|
450
|
+
\`\`\`markdown
|
|
451
|
+
## D-001@v2: 缓存异常时的 fallback 语义
|
|
452
|
+
- type: definition | consistency | feasibility | boundary | architecture | compatibility | risk
|
|
453
|
+
- priority: P0 | P1 | P2
|
|
454
|
+
- status: accepted | unresolved | rejected | superseded
|
|
455
|
+
- supersedes: D-001@v1
|
|
456
|
+
- source: design-grill
|
|
457
|
+
- question: §3 数据流与 §7 容错策略冲突时以哪个为准?
|
|
458
|
+
- answer: 采用 §7 的重试语义,缓存只作为只读 fallback。
|
|
459
|
+
- normalized_requirement: TTL 过期且上游仍异常时返回 stale 标记,不刷新缓存。
|
|
460
|
+
- impacts: [FR-02, task-03, verify-02]
|
|
461
|
+
- evidence: design.md §3/§7, src/cache/...
|
|
462
|
+
\`\`\`
|
|
463
|
+
|
|
464
|
+
### 输出格式
|
|
465
|
+
\`\`\`markdown
|
|
466
|
+
## Design Grill Result
|
|
467
|
+
status: passed | needs-user-input | blocked | skipped
|
|
468
|
+
|
|
469
|
+
## Cross-Check Matrix
|
|
470
|
+
| ID | 层级 | 交叉点 | 证据 A | 证据 B | 结论 | 决策 |
|
|
471
|
+
|---|---|---|---|---|---|---|
|
|
472
|
+
| X-001 | consistency | 数据流 vs 容错 | design §3 | design §7 | conflict | D-001@v2 |
|
|
473
|
+
|
|
474
|
+
## Question Distribution
|
|
475
|
+
| 分类 | 数量 | 含义 |
|
|
476
|
+
|---|---|---|
|
|
477
|
+
| immediately_answered | N | 心里清楚但文档缺失 |
|
|
478
|
+
| needs_thinking | N | 需要用户判断 |
|
|
479
|
+
| unresolved | N | 真正设计漏洞 |
|
|
480
|
+
|
|
481
|
+
## Unresolved Blockers
|
|
482
|
+
| ID | priority | 问题 | 阻塞原因 | 下一步 |
|
|
483
|
+
|---|---|---|---|---|
|
|
484
|
+
\`\`\`
|
|
485
|
+
|
|
486
|
+
### 铁律 — 等待用户
|
|
487
|
+
- 发现 P0/P1 结构性矛盾且需要用户判断时,调用:
|
|
488
|
+
\`sillyspec run brainstorm --wait --reason "等待用户处理 Design Grill 发现的结构性问题" --options "按推荐修正,补充回答,显式跳过" --output "Design Grill 问题摘要"\`
|
|
489
|
+
- 用户显式跳过时,必须在 decisions.md 记录 accepted risk;P0/P1 skip 仍必须写入 Unresolved Blockers。
|
|
490
|
+
- 完成前必须确认:没有 P0/P1 unresolved blocker;否则不能进入 plan。`,
|
|
491
|
+
outputHint: 'Design Grill 交叉审查结果',
|
|
492
|
+
optional: false
|
|
493
|
+
},
|
|
303
494
|
{
|
|
304
495
|
name: '用户确认并生成规范文件',
|
|
496
|
+
requiresWait: true,
|
|
497
|
+
waitReason: '等待用户最终确认设计方案',
|
|
498
|
+
waitOptions: ['确认', '需要修改', '推翻重来'],
|
|
305
499
|
prompt: `用户确认设计方案,生成规范文件。
|
|
306
500
|
|
|
307
501
|
### 操作
|
|
@@ -309,9 +503,10 @@ design.md 文件路径 + 自审结果
|
|
|
309
503
|
2. 暂停等待用户选择:✅ 确认 / ✏️ 修改 / ❌ 推翻重来
|
|
310
504
|
3. 确认后,在 \`.sillyspec/changes/<change-name>/\` 下生成所有规范文件:
|
|
311
505
|
- **design.md**:架构决策、文件变更清单、数据模型、API 设计、兼容策略、风险登记、自审
|
|
506
|
+
- **decisions.md**(可选):Grill/重大决策台账,使用 D-001@v1 稳定版本 ID
|
|
312
507
|
- **proposal.md**:动机、关键问题(为什么现有方案不够)、变更范围、不在范围内(显式清单)、成功标准(可验证条件)
|
|
313
|
-
- **requirements.md**:角色表 + FR 编号需求 + Given/When/Then 行为规格 + 非功能需求
|
|
314
|
-
- **tasks.md
|
|
508
|
+
- **requirements.md**:角色表 + FR 编号需求 + Given/When/Then 行为规格 + 非功能需求 + D-xxx@vN 覆盖关系
|
|
509
|
+
- **tasks.md**:任务列表(只列名称、对应文件路径、覆盖的 FR-xxx/D-xxx@vN,细节在 plan 阶段展开)
|
|
315
510
|
- \`git add .sillyspec/\` — 暂存规范文件(不要 commit)
|
|
316
511
|
|
|
317
512
|
所有规范文件头部必须包含 YAML frontmatter:
|
|
@@ -357,6 +552,7 @@ created_at: <now-datetime>
|
|
|
357
552
|
## 功能需求
|
|
358
553
|
|
|
359
554
|
### FR-01: 需求名称
|
|
555
|
+
覆盖决策:D-001@v1, D-002@v1(如适用)
|
|
360
556
|
Given 前提条件
|
|
361
557
|
When 触发动作
|
|
362
558
|
Then 期望结果
|
|
@@ -367,6 +563,28 @@ Then 期望结果
|
|
|
367
563
|
- 兼容性:...
|
|
368
564
|
- 可回退:...
|
|
369
565
|
- 可测试:...
|
|
566
|
+
|
|
567
|
+
## 决策覆盖矩阵(如存在 decisions.md)
|
|
568
|
+
| 决策 ID | 覆盖的 FR | 说明 |
|
|
569
|
+
|---|---|---|
|
|
570
|
+
| D-001@v1 | FR-01 | ... |
|
|
571
|
+
\`\`\`
|
|
572
|
+
|
|
573
|
+
### decisions.md 格式要求(仅在有 Grill/重大决策时生成)
|
|
574
|
+
\`\`\`markdown
|
|
575
|
+
# Decisions
|
|
576
|
+
|
|
577
|
+
## D-001@v1: 决策短标题
|
|
578
|
+
- type: definition | consistency | feasibility | term | boundary | premise | architecture | compatibility | risk
|
|
579
|
+
- priority: P0 | P1 | P2
|
|
580
|
+
- status: accepted | unresolved | rejected | superseded
|
|
581
|
+
- supersedes:
|
|
582
|
+
- source: user | code | docs
|
|
583
|
+
- question: 被解决的问题
|
|
584
|
+
- answer: 用户确认或代码查证结果
|
|
585
|
+
- normalized_requirement: 可测试的约束
|
|
586
|
+
- impacts: [FR-01, task-01, verify-01]
|
|
587
|
+
- evidence: 用户回答轮次或代码/文档路径
|
|
370
588
|
\`\`\`
|
|
371
589
|
|
|
372
590
|
### 后续变更包处理
|
|
@@ -395,6 +613,8 @@ Then 期望结果
|
|
|
395
613
|
- 禁止自动 commit
|
|
396
614
|
- 推翻重来回到 Step 6(对话式探索)
|
|
397
615
|
- 表名/字段名/类名必须来自真实代码或标注"新增"
|
|
616
|
+
- 如果存在 decisions.md,requirements.md 必须引用全部当前版本 D-xxx@vN;没有覆盖的 D-xxx@vN 必须标注为剩余风险
|
|
617
|
+
- 如果 Design Grill 产生 P0/P1 unresolved blocker,必须回到 design 修正,不能进入 plan
|
|
398
618
|
- tasks.md 只列任务名,细节在 plan 阶段展开`,
|
|
399
619
|
|
|
400
620
|
}
|
package/src/stages/index.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { definition as brainstorm } from './brainstorm.js'
|
|
2
|
-
import { definition as propose } from './propose.js'
|
|
3
2
|
import { definition as plan } from './plan.js'
|
|
4
3
|
import { definition as execute } from './execute.js'
|
|
5
4
|
import { definition as verify } from './verify.js'
|
|
@@ -15,7 +14,6 @@ const auxiliaryFlag = { auxiliary: true }
|
|
|
15
14
|
|
|
16
15
|
export const stageRegistry = {
|
|
17
16
|
brainstorm,
|
|
18
|
-
propose,
|
|
19
17
|
plan,
|
|
20
18
|
execute,
|
|
21
19
|
verify,
|