sillyspec 3.25.1 → 3.25.2
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/.claude/skills/sillyspec-doctor/SKILL.md +9 -0
- package/.playwright-mcp/console-2026-07-27T15-45-50-621Z.log +1 -0
- package/.playwright-mcp/page-2026-07-27T15-45-51-459Z.yml +144 -0
- package/.playwright-mcp/prompt-final.png +0 -0
- package/.playwright-mcp/prompt-view.png +0 -0
- package/CLAUDE.md +18 -0
- package/package.json +1 -1
- package/src/change-risk-profile.js +60 -12
- package/src/check-primitives.js +65 -0
- package/src/index.js +135 -6
- package/src/progress/change-registry.js +217 -0
- package/src/progress/consistency-doctor.js +259 -0
- package/src/progress/shared.js +33 -0
- package/src/progress/stage-machine.js +535 -0
- package/src/progress/step-store.js +256 -0
- package/src/progress.js +59 -1191
- package/src/quicklog.js +3 -1
- package/src/run/command.js +127 -50
- package/src/run/complete-handlers.js +2 -1
- package/src/run/complete.js +725 -695
- package/src/run/gates.js +326 -321
- package/src/run/prompt.js +619 -604
- package/src/run/quick-audit.js +107 -102
- package/src/run/shared.js +43 -0
- package/src/stage-contract-engine.js +150 -0
- package/src/stage-contract-spec.js +517 -0
- package/src/stage-contract.js +83 -148
- package/src/stage-review.js +63 -0
- package/src/stages/brainstorm.js +3 -1
- package/src/stages/execute.js +5 -2
- package/src/stages/plan-postcheck.js +38 -27
- package/src/stages/plan.js +12 -49
- package/src/stages/propose.js +2 -1
- package/src/stages/scan.js +3 -1
- package/src/workflow.js +9 -17
- package/src/worktree-apply.js +5 -2
- package/src/worktree-deps.js +114 -32
- package/src/worktree.js +6 -3
- package/src/brainstorm-postcheck.js +0 -158
package/src/stage-contract.js
CHANGED
|
@@ -8,8 +8,10 @@
|
|
|
8
8
|
import { existsSync, readdirSync, readFileSync } from 'fs'
|
|
9
9
|
import { join, basename } from 'path'
|
|
10
10
|
import { execFileSync } from 'child_process'
|
|
11
|
-
import { detectChangeRisk, checkIntegrationEvidence } from './change-risk-profile.js'
|
|
11
|
+
import { detectChangeRisk, checkIntegrationEvidence, VERIFICATION_NEEDS, RISK_LEVEL_CAUSES } from './change-risk-profile.js'
|
|
12
12
|
import { SCAN_REQUIRED_DOCS, AUXILIARY_STAGES } from './constants.js'
|
|
13
|
+
import { evaluateRules } from './stage-contract-engine.js'
|
|
14
|
+
import { getRule } from './stage-contract-spec.js'
|
|
13
15
|
|
|
14
16
|
/**
|
|
15
17
|
* 校验结果
|
|
@@ -74,7 +76,10 @@ export function validateChangeExists(specBase, stageName, changeName) {
|
|
|
74
76
|
return {
|
|
75
77
|
changeName,
|
|
76
78
|
specBase,
|
|
77
|
-
message: `变更 "${changeName}" 在当前 spec 下不存在:${join(specBase, 'changes', changeName)}
|
|
79
|
+
message: `变更 "${changeName}" 在当前 spec 下不存在:${join(specBase, 'changes', changeName)}\n` +
|
|
80
|
+
` 排查方向(三选一):① 变更名是否打错(sillyspec status 或 progress show 查看活跃变更名);` +
|
|
81
|
+
`② cwd 是否漂到了子项目目录(回到项目根再跑,或加 --spec-dir <根>/.sillyspec);` +
|
|
82
|
+
`③ 是否该先跑 sillyspec run brainstorm 新建该变更。`,
|
|
78
83
|
}
|
|
79
84
|
}
|
|
80
85
|
|
|
@@ -198,7 +203,7 @@ function warnMissingIds(warnings, ids, targetContent, targetName, sourceName) {
|
|
|
198
203
|
const base = id.replace(/@V\d+$/, '')
|
|
199
204
|
const re = new RegExp(`\\b${base}\\b`)
|
|
200
205
|
if (!re.test(targetUpper)) {
|
|
201
|
-
warnings.push(
|
|
206
|
+
warnings.push(getRule('shared.id-traceability').failMessage.replaceAll('${target}', targetName).replaceAll('${source}', sourceName).replaceAll('${id}', id))
|
|
202
207
|
}
|
|
203
208
|
}
|
|
204
209
|
}
|
|
@@ -216,16 +221,10 @@ function validateScanOutputs(cwd, changeName, context = {}) {
|
|
|
216
221
|
? join(specBase, 'docs', projectName, 'scan')
|
|
217
222
|
: join(specBase, 'docs', 'scan')
|
|
218
223
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const errors = []
|
|
222
|
-
const warnings = []
|
|
223
|
-
|
|
224
|
-
for (const doc of requiredDocs) {
|
|
225
|
-
if (!existsSync(join(docsRoot, doc))) {
|
|
226
|
-
errors.push(`scan 文档缺失: ${join(docsRoot, doc)}`)
|
|
227
|
-
}
|
|
228
|
-
}
|
|
224
|
+
// 7 份 scan 文档存在性由引擎消费 manifest(与 SCAN_REQUIRED_DOCS 单源,文案/事前契约同源)
|
|
225
|
+
const engineResult = evaluateRules('scan', { docsRoot })
|
|
226
|
+
const errors = [...engineResult.errors]
|
|
227
|
+
const warnings = [...engineResult.warnings]
|
|
229
228
|
|
|
230
229
|
// 检查 modules 目录
|
|
231
230
|
const modulesRoot = projectName
|
|
@@ -253,90 +252,39 @@ function validateBrainstormOutputs(cwd, changeName, context = {}) {
|
|
|
253
252
|
return { ok: false, errors: [`平台模式 specRoot 缺少 changes 目录: ${changesRoot}`], warnings: [] }
|
|
254
253
|
}
|
|
255
254
|
const changeDir = resolveChangeDir(cwd, changeName, specRoot)
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
if (!existsSync(join(changeDir, file))) {
|
|
263
|
-
errors.push(`brainstorm 产物缺失: ${join(changeDir, file)}`)
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// 内容校验(文件存在时检查关键章节)
|
|
268
|
-
if (existsSync(join(changeDir, 'proposal.md'))) {
|
|
269
|
-
const content = readFileSync(join(changeDir, 'proposal.md'), 'utf8')
|
|
270
|
-
if (!content.includes('不在范围内') && !content.includes('Non-Goals') && !content.includes('非目标')) {
|
|
271
|
-
warnings.push('proposal.md 缺少「不在范围内/Non-Goals」章节')
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
if (existsSync(join(changeDir, 'requirements.md'))) {
|
|
276
|
-
const content = readFileSync(join(changeDir, 'requirements.md'), 'utf8')
|
|
277
|
-
if (!/FR-\d+/i.test(content)) {
|
|
278
|
-
warnings.push('requirements.md 缺少 FR 编号的需求项')
|
|
279
|
-
}
|
|
280
|
-
}
|
|
255
|
+
// 纯 kind 规则(四件套存在性 + proposal/requirements/design 章节 + tasks 列表)由引擎消费
|
|
256
|
+
// stage-contract-spec.js manifest,与报错文案 + prompt 事前契约严格同源(事前给的 == 事后查的)。
|
|
257
|
+
// custom kind(lifecycle / decisions)保留下方旧逻辑,数据/文案在 Batch 3 迁入 manifest。
|
|
258
|
+
const engineResult = evaluateRules('brainstorm', { changeDir })
|
|
259
|
+
const errors = [...engineResult.errors]
|
|
260
|
+
const warnings = [...engineResult.warnings]
|
|
281
261
|
|
|
282
262
|
if (existsSync(join(changeDir, 'design.md'))) {
|
|
283
263
|
const content = readFileSync(join(changeDir, 'design.md'), 'utf8')
|
|
284
|
-
if (!content.includes('文件变更清单') && !content.includes('File Changes') && !content.includes('文件清单')) {
|
|
285
|
-
warnings.push('design.md 缺少「文件变更清单」章节')
|
|
286
|
-
}
|
|
287
|
-
if (!content.includes('风险登记') && !content.includes('Risk') && !content.includes('风险')) {
|
|
288
|
-
warnings.push('design.md 缺少「风险登记」章节')
|
|
289
|
-
}
|
|
290
|
-
if (!content.includes('自审') && !content.includes('Self-Review') && !content.includes('Self-review')) {
|
|
291
|
-
warnings.push('design.md 缺少「自审」章节')
|
|
292
|
-
}
|
|
293
264
|
|
|
294
|
-
//
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
// 收紧原则(修:正常 design 不应被误判「已豁免」):
|
|
301
|
-
// - 否定词必须是明确多字短语(不涉及/不适用/未涉及/不包含/没有/n\/a/not applicable/none),
|
|
302
|
-
// 杜绝裸单字「无」与裸「na」在 40 字符宽窗口内任意命中——
|
|
303
|
-
// 「lifecycle 状态无变化」「本变更无需 lifecycle 事件」「lifecycle canal 不涉及」等不再误判;
|
|
304
|
-
// - 否定词必须与「生命周期(契约)/lifecycle(contract)」紧邻(仅允许少量空白/分隔符/「任何」),
|
|
305
|
-
// 不再用 40 字符宽松窗口;工具错误信息本身就指引写「不涉及生命周期契约」这个规范短语。
|
|
306
|
-
const declaresNotApplicable =
|
|
307
|
-
// 否定在前:「不涉及生命周期(契约)」「不适用 lifecycle contract」
|
|
308
|
-
/(?:不涉及|不适用|未涉及|不包含|没有(?:任何)?)\s?(?:任何\s?)?(?:生命周期(?:契约)?|lifecycle(?:[ _=-]?contract)?)/i.test(content) ||
|
|
309
|
-
// 主题在前(表格/列表单元「生命周期契约:不涉及 / N/A / 无」——分隔符强制,杜绝宽窗口)
|
|
310
|
-
/(?:生命周期(?:契约)?|lifecycle(?:[ _=-]?contract)?)\s?[::=]\s?(?:不涉及|不适用|未涉及|无|n\/?a\b|not[ _=-]?applicable|none\b)/i.test(content) ||
|
|
311
|
-
// 英文谓语句:「does not involve / not applicable ... lifecycle」
|
|
312
|
-
/(?:does[ _-]?not[ _-]?involve|not[ _-]?applicable)[^\n]{0,15}lifecycle/i.test(content)
|
|
265
|
+
// lifecycle-exemption(custom kind):判定算法保留(trigger/exemption/table 三段短路),
|
|
266
|
+
// data + failMessage/exemptionPassedMessage 从 stage-contract-spec.js manifest 同源。
|
|
267
|
+
const lcRule = getRule('brainstorm.design.lifecycle-table')
|
|
268
|
+
const { trigger: lcTrigger, exemptions: lcExempts, table: lcTable } = lcRule.data
|
|
269
|
+
if (new RegExp(lcTrigger.pattern, lcTrigger.flags).test(content)) {
|
|
270
|
+
const declaresNotApplicable = lcExempts.some(e => new RegExp(e.pattern, e.flags).test(content))
|
|
313
271
|
if (declaresNotApplicable) {
|
|
314
|
-
warnings.push(
|
|
272
|
+
warnings.push(lcRule.exemptionPassedMessage)
|
|
315
273
|
} else {
|
|
316
|
-
const hasLifecycleTable =
|
|
317
|
-
/生命周期契约表|lifecycle[._-]?contract|lifecycle[._-]?matrix|Lifecycle Contract/i.test(content) ||
|
|
318
|
-
/事件.*发起方.*接收方.*必需字段.*状态变化/.test(content)
|
|
274
|
+
const hasLifecycleTable = lcTable.some(t => new RegExp(t.pattern, t.flags).test(content))
|
|
319
275
|
if (!hasLifecycleTable) {
|
|
320
|
-
errors.push(
|
|
276
|
+
errors.push(lcRule.failMessage)
|
|
321
277
|
}
|
|
322
278
|
}
|
|
323
279
|
}
|
|
324
280
|
}
|
|
325
281
|
|
|
326
|
-
if (existsSync(join(changeDir, 'tasks.md'))) {
|
|
327
|
-
const content = readFileSync(join(changeDir, 'tasks.md'), 'utf8')
|
|
328
|
-
const lines = content.split('\n').filter(l => l.trim().startsWith('-') || l.trim().startsWith('*') || /^\d+\./.test(l.trim()))
|
|
329
|
-
if (lines.length === 0) {
|
|
330
|
-
warnings.push('tasks.md 没有任务列表项')
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
282
|
const decisionsFile = join(changeDir, 'decisions.md')
|
|
335
283
|
if (existsSync(decisionsFile)) {
|
|
336
284
|
const decisions = readFileSync(decisionsFile, 'utf8')
|
|
337
285
|
const blockers = findBlockingDecisionIssues(decisions)
|
|
338
286
|
for (const issue of blockers) {
|
|
339
|
-
errors.push(
|
|
287
|
+
errors.push(getRule('shared.decision-blocker').failMessage.replace('${issue}', issue))
|
|
340
288
|
}
|
|
341
289
|
const decisionIds = extractCurrentDecisionIds(decisions)
|
|
342
290
|
if (decisionIds.length === 0) {
|
|
@@ -361,13 +309,12 @@ function validatePlanOutputs(cwd, changeName, context = {}) {
|
|
|
361
309
|
const { specRoot } = context
|
|
362
310
|
const changeDir = resolveChangeDir(cwd, changeName, specRoot)
|
|
363
311
|
const planFile = join(changeDir, 'plan.md')
|
|
364
|
-
const errors = []
|
|
365
312
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
313
|
+
// plan.md 存在性由引擎消费 manifest。entryPoint/id-trace/decisions 为 custom kind(下方保留)。
|
|
314
|
+
const engineResult = evaluateRules('plan', { changeDir })
|
|
315
|
+
const errors = [...engineResult.errors]
|
|
316
|
+
const warnings = [...engineResult.warnings]
|
|
369
317
|
|
|
370
|
-
const warnings = []
|
|
371
318
|
if (existsSync(planFile)) {
|
|
372
319
|
const plan = readFileSync(planFile, 'utf8')
|
|
373
320
|
const requirements = readIfExists(join(changeDir, 'requirements.md'))
|
|
@@ -377,26 +324,25 @@ function validatePlanOutputs(cwd, changeName, context = {}) {
|
|
|
377
324
|
const decisions = readIfExists(join(changeDir, 'decisions.md'))
|
|
378
325
|
const blockers = findBlockingDecisionIssues(decisions)
|
|
379
326
|
for (const issue of blockers) {
|
|
380
|
-
errors.push(
|
|
327
|
+
errors.push(getRule('shared.decision-blocker').failMessage.replace('${issue}', issue))
|
|
381
328
|
}
|
|
382
329
|
const decisionIds = extractCurrentDecisionIds(decisions)
|
|
383
330
|
warnMissingIds(warnings, decisionIds, plan, 'plan.md', 'decisions.md')
|
|
384
331
|
}
|
|
385
332
|
// ── P0: 生产接线路径检查:design 提到入口但 task 的 allowed_paths 不含入口文件 ──
|
|
333
|
+
// entry-point-wiring(custom):trigger/file 抽取/exemption/failMessage 从 manifest 同源
|
|
334
|
+
// (stage-contract-spec.js),保留多源 allowed_paths 收集 + 逐文件对账 + 豁免算法。
|
|
386
335
|
const designContent = readIfExists(join(changeDir, 'design.md'))
|
|
387
336
|
if (designContent) {
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
/\b(?:在|from)\s+['"]?(cli\.ts|main\.ts|server\.(?:js|ts)|index\.(?:js|ts))['"]?/gi,
|
|
392
|
-
/\b(?:注入|inject)\b.*\b(?:构造|constructor|初始化|init|实例化|instantiate)\b/gi,
|
|
393
|
-
/\b(?:启动路径|startup|entrypoint|bootstrap|daemon[._-]?start|main.*entry)\b/gi,
|
|
394
|
-
]
|
|
337
|
+
const epRule = getRule('plan.entry-point-wiring')
|
|
338
|
+
const { entryPointPatterns, fileExtractionPattern, exemptionPattern } = epRule.data
|
|
339
|
+
const fileExtractRe = new RegExp(fileExtractionPattern.pattern, fileExtractionPattern.flags)
|
|
395
340
|
const mentionedFiles = new Set()
|
|
396
|
-
for (const
|
|
341
|
+
for (const ep of entryPointPatterns) {
|
|
342
|
+
const pattern = new RegExp(ep.pattern, ep.flags)
|
|
397
343
|
pattern.lastIndex = 0
|
|
398
344
|
for (const match of designContent.matchAll(pattern)) {
|
|
399
|
-
const fileMatch = match[0].match(
|
|
345
|
+
const fileMatch = match[0].match(fileExtractRe)
|
|
400
346
|
if (fileMatch) mentionedFiles.add(fileMatch[1].toLowerCase())
|
|
401
347
|
}
|
|
402
348
|
}
|
|
@@ -426,10 +372,9 @@ function validatePlanOutputs(cwd, changeName, context = {}) {
|
|
|
426
372
|
for (const mentionedFile of mentionedFiles) {
|
|
427
373
|
const found = [...allAllowedPaths].some(p => p.includes(mentionedFile))
|
|
428
374
|
if (!found) {
|
|
429
|
-
const noChangePattern = new RegExp(
|
|
375
|
+
const noChangePattern = new RegExp(exemptionPattern.replaceAll('${file}', mentionedFile), 'i')
|
|
430
376
|
if (!noChangePattern.test(designContent)) {
|
|
431
|
-
errors.push(
|
|
432
|
-
warnings.push(`提示: 如果确实不需要修改 ${mentionedFile},请在 design.md 中明确写明理由`)
|
|
377
|
+
errors.push(epRule.failMessage.replaceAll('${file}', mentionedFile))
|
|
433
378
|
}
|
|
434
379
|
}
|
|
435
380
|
}
|
|
@@ -457,35 +402,24 @@ function extractVerifyConclusion(verify) {
|
|
|
457
402
|
function validateVerifyOutputs(cwd, changeName, context = {}) {
|
|
458
403
|
const { specRoot } = context
|
|
459
404
|
const changeDir = resolveChangeDir(cwd, changeName, specRoot)
|
|
460
|
-
const errors = []
|
|
461
|
-
const warnings = []
|
|
462
405
|
|
|
463
406
|
if (!existsSync(changeDir)) {
|
|
464
|
-
errors
|
|
465
|
-
return { ok: false, errors, warnings }
|
|
407
|
+
return { ok: false, errors: [`变更目录缺失: ${changeDir}`], warnings: [] }
|
|
466
408
|
}
|
|
467
409
|
|
|
468
|
-
// verify
|
|
469
|
-
//
|
|
470
|
-
const
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
// 确保核心规范文件仍然存在
|
|
476
|
-
const requiredDocs = ['design.md', 'plan.md']
|
|
477
|
-
for (const doc of requiredDocs) {
|
|
478
|
-
if (!existsSync(join(changeDir, doc))) {
|
|
479
|
-
errors.push(`核心文档缺失: ${join(changeDir, doc)}`)
|
|
480
|
-
}
|
|
481
|
-
}
|
|
410
|
+
// 核心文档存在性(verify-result.md / design.md / plan.md)由引擎消费 manifest。
|
|
411
|
+
// verify-result.md 不存在则不能完成——历史教训:AI 可能跳过报告直接 --done 导致"假完成"。
|
|
412
|
+
const engineResult = evaluateRules('verify', { changeDir })
|
|
413
|
+
const errors = [...engineResult.errors]
|
|
414
|
+
const warnings = [...engineResult.warnings]
|
|
482
415
|
|
|
416
|
+
const verifyResult = join(changeDir, 'verify-result.md')
|
|
483
417
|
if (existsSync(verifyResult)) {
|
|
484
418
|
const verify = readFileSync(verifyResult, 'utf8')
|
|
485
419
|
const decisions = readIfExists(join(changeDir, 'decisions.md'))
|
|
486
420
|
const blockers = findBlockingDecisionIssues(decisions)
|
|
487
421
|
for (const issue of blockers) {
|
|
488
|
-
errors.push(
|
|
422
|
+
errors.push(getRule('shared.decision-blocker').failMessage.replace('${issue}', issue))
|
|
489
423
|
}
|
|
490
424
|
const decisionIds = extractCurrentDecisionIds(decisions)
|
|
491
425
|
warnMissingIds(warnings, decisionIds, verify, 'verify-result.md', 'decisions.md')
|
|
@@ -495,9 +429,9 @@ function validateVerifyOutputs(cwd, changeName, context = {}) {
|
|
|
495
429
|
// 历史教训:CLI 曾不校验结论,AI 写 FAIL 后 verify 仍被标记完成并提示"验证通过可以归档"。
|
|
496
430
|
const conclusionStr = extractVerifyConclusion(verify)
|
|
497
431
|
if (conclusionStr === 'FAIL') {
|
|
498
|
-
errors.push('verify-
|
|
432
|
+
errors.push(getRule('verify.conclusion.fail-gate').failMessage)
|
|
499
433
|
} else if (!conclusionStr) {
|
|
500
|
-
warnings.push('verify-
|
|
434
|
+
warnings.push(getRule('verify.conclusion.fail-gate').noConclusionWarning)
|
|
501
435
|
}
|
|
502
436
|
|
|
503
437
|
// ── P0: Change Risk Gate — 核心功能缺少真实集成验证时 FAIL ──
|
|
@@ -510,8 +444,28 @@ function validateVerifyOutputs(cwd, changeName, context = {}) {
|
|
|
510
444
|
if (conclusion === 'PASS WITH NOTES' || conclusion === 'PASS') {
|
|
511
445
|
const evidenceCheck = checkIntegrationEvidence(verify, changeRiskProfile.requiredVerification)
|
|
512
446
|
if (!evidenceCheck.ok) {
|
|
513
|
-
|
|
514
|
-
|
|
447
|
+
// A: 报错说人话 —— 把「缺哪一项、要写/做什么、判级原因」逐条列出,
|
|
448
|
+
// 让 agent 不必靠改结论文案撞墙。detail 指明真实启动须是本变更实际改动的
|
|
449
|
+
// 部署/启动入口(非无关进程),以及每项的字面期望。
|
|
450
|
+
const needs = changeRiskProfile.requiredVerification
|
|
451
|
+
.filter(k => VERIFICATION_NEEDS[k] && VERIFICATION_NEEDS[k].desc)
|
|
452
|
+
.map(k => {
|
|
453
|
+
const vn = VERIFICATION_NEEDS[k]
|
|
454
|
+
const lit = vn.literals && vn.literals.length ? '字面命中其一:' + vn.literals.join(' / ') : ''
|
|
455
|
+
return `\n 〔${k}〕${vn.desc}${lit}`
|
|
456
|
+
})
|
|
457
|
+
.join('')
|
|
458
|
+
const cause = RISK_LEVEL_CAUSES[changeRiskProfile.level] || ''
|
|
459
|
+
errors.push(
|
|
460
|
+
`[${changeRiskProfile.level}] 验证结论为 ${conclusion},但缺少真实集成证据。\n` +
|
|
461
|
+
` 缺失项(需在 verify-result.md 如实提供并满足):${evidenceCheck.errors.join('; ')}\n` +
|
|
462
|
+
` 每项要提供什么:${needs}\n` +
|
|
463
|
+
` 风险判级原因:${cause}\n` +
|
|
464
|
+
` 命中触发词:${changeRiskProfile.triggers.join(', ')}\n` +
|
|
465
|
+
` 出路:① 补全上述缺失的真实集成证据(真实启动 daemon/backend、集成测试、运行日志)后保持 PASS;` +
|
|
466
|
+
`或 ② 如实改结论 FAIL(承认端到端未验,留待部署后补)。` +
|
|
467
|
+
`仅改结论文案/措辞蹭字面关键词会被对账,PASS WITH NOTES 在此等级下不被允许。`
|
|
468
|
+
)
|
|
515
469
|
}
|
|
516
470
|
warnings.push(...evidenceCheck.warnings)
|
|
517
471
|
}
|
|
@@ -525,35 +479,18 @@ function validateVerifyOutputs(cwd, changeName, context = {}) {
|
|
|
525
479
|
* archive 完成校验:检查归档目录完整性
|
|
526
480
|
*/
|
|
527
481
|
function validateArchiveOutputs(cwd, changeName) {
|
|
528
|
-
const errors = []
|
|
529
|
-
const warnings = []
|
|
530
482
|
const archiveDir = join(cwd, '.sillyspec', 'changes', 'archive')
|
|
531
483
|
const date = new Date().toISOString().slice(0, 10)
|
|
532
484
|
const destDir = join(archiveDir, `${date}-${changeName}`)
|
|
533
485
|
|
|
534
|
-
//
|
|
486
|
+
// 归档目录不存在 early return(引擎在存在时才跑)
|
|
535
487
|
if (!existsSync(destDir)) {
|
|
536
|
-
errors
|
|
537
|
-
return { ok: false, errors, warnings }
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
// 检查核心文档
|
|
541
|
-
const requiredDocs = ['plan.md']
|
|
542
|
-
const recommendedDocs = ['design.md', 'module-impact.md']
|
|
543
|
-
|
|
544
|
-
for (const doc of requiredDocs) {
|
|
545
|
-
if (!existsSync(join(destDir, doc))) {
|
|
546
|
-
errors.push(`归档目录缺失核心文档: ${doc}`)
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
for (const doc of recommendedDocs) {
|
|
551
|
-
if (!existsSync(join(destDir, doc))) {
|
|
552
|
-
warnings.push(`归档目录缺少推荐文档: ${doc}`)
|
|
553
|
-
}
|
|
488
|
+
return { ok: false, errors: [`归档目录缺失: ${destDir}`], warnings: [] }
|
|
554
489
|
}
|
|
555
490
|
|
|
556
|
-
|
|
491
|
+
// plan.md(必备)/design.md/module-impact.md(推荐)存在性由引擎消费 manifest
|
|
492
|
+
const engineResult = evaluateRules('archive', { archiveDir: destDir }, undefined, { source: 'validateArchiveOutputs' })
|
|
493
|
+
return { ok: engineResult.errors.length === 0, errors: engineResult.errors, warnings: engineResult.warnings }
|
|
557
494
|
}
|
|
558
495
|
|
|
559
496
|
/**
|
|
@@ -572,11 +509,9 @@ function validateChangeClosed(cwd, changeName) {
|
|
|
572
509
|
return { ok: false, errors, warnings }
|
|
573
510
|
}
|
|
574
511
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
return { ok: errors.length === 0, errors, warnings }
|
|
512
|
+
// plan.md 存在性由引擎消费 manifest(source 区分 validateChangeClosed vs validateArchiveOutputs)
|
|
513
|
+
const engineResult = evaluateRules('archive', { changeDir }, undefined, { source: 'validateChangeClosed' })
|
|
514
|
+
return { ok: engineResult.errors.length === 0, errors: engineResult.errors, warnings: engineResult.warnings }
|
|
580
515
|
}
|
|
581
516
|
|
|
582
517
|
// ============ Execute 代码变更客观核验 ============
|
package/src/stage-review.js
CHANGED
|
@@ -24,6 +24,69 @@ export const STAGE_REVIEW_TYPES = ['design', 'plan', 'proposal', 'code', 'accept
|
|
|
24
24
|
// checklist 每项 result 的合法值
|
|
25
25
|
const CHECKLIST_RESULTS = ['pass', 'gap', 'fail']
|
|
26
26
|
|
|
27
|
+
// stage → 主审查文档(brainstorm/execute 审 design.md;plan 审 plan.md;propose 审 proposal.md)
|
|
28
|
+
const STAGE_MAIN_DOC = { brainstorm: 'design.md', plan: 'plan.md', propose: 'proposal.md', execute: 'design.md' }
|
|
29
|
+
// stage → reviewType(对齐 gates.js Stage Review Gate 的映射)
|
|
30
|
+
const STAGE_REVIEW_TYPE = { brainstorm: 'design', plan: 'plan', propose: 'proposal', execute: 'acceptance' }
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 渲染 review.json 产物契约(markdown)给 review 子代理事前看 —— schema 表 + 完整 JSON 示例 +
|
|
34
|
+
* docHash 算法 + 重算提示 + 位置。复用 validateStageReviewSchema 的同源常量,事前给的 == 事后查的。
|
|
35
|
+
*
|
|
36
|
+
* 历史教训:review 子代理靠读 stage-review.js 源码 + 翻模板才搞清 schema,常错(漏 schemaVersion /
|
|
37
|
+
* checklist 按层嵌套对象 / docHash 用主文档改版前的旧 sha256)。本函数把契约前置进 prompt。
|
|
38
|
+
*
|
|
39
|
+
* @param {{ stage?: string, changeDir?: string, reviewRunId?: string, tier?: string }} opts
|
|
40
|
+
* @returns {string} markdown 段;tier=self 返回简短提示(无需 review.json)
|
|
41
|
+
*/
|
|
42
|
+
export function renderReviewJsonContract({ stage, changeDir, reviewRunId, tier } = {}) {
|
|
43
|
+
if (tier === 'self') {
|
|
44
|
+
return '> review.json 契约:tier=self(变更规模小),无需产出 review.json,当前 agent 自审即可。'
|
|
45
|
+
}
|
|
46
|
+
const reviewType = STAGE_REVIEW_TYPE[stage] || 'design'
|
|
47
|
+
const mainDoc = STAGE_MAIN_DOC[stage] || 'design.md'
|
|
48
|
+
const mainDocRel = changeDir ? 'changes/<change>/' + mainDoc : mainDoc
|
|
49
|
+
const stageLbl = stage || '<stage>'
|
|
50
|
+
const runIdLbl = reviewRunId || '<reviewRunId>'
|
|
51
|
+
const L = [
|
|
52
|
+
'## review.json 产物契约(CLI Stage Review Gate 将硬校验,以下为精确 schema —— 事前给的 == 事后查的)',
|
|
53
|
+
'',
|
|
54
|
+
'> 路径:`{SPEC_ROOT}/.runtime/stage-reviews/' + stageLbl + '-' + runIdLbl + '/review.json`(本次 reviewType=' + reviewType + ',主审查文档=' + mainDocRel + ')',
|
|
55
|
+
'',
|
|
56
|
+
'### 必填字段(validateStageReviewSchema 硬校验,缺任一即 schema 失败阻断)',
|
|
57
|
+
'- `schemaVersion`: ' + REVIEW_SCHEMA_VERSION,
|
|
58
|
+
'- `reviewType`: "' + reviewType + '"(必须与本 stage 一致;合法值 ' + STAGE_REVIEW_TYPES.join(' / ') + ')',
|
|
59
|
+
'- `specVerdict` / `qualityVerdict`: ∈ { ' + VALID_VERDICTS.join(' / ') + ' }',
|
|
60
|
+
'- `reviewedFiles`: 非空数组,[0] = 主审查文档 = `' + mainDocRel + '`',
|
|
61
|
+
'- `docHash`: reviewedFiles[0] 文件内容的 sha256(hex,见下方算法)',
|
|
62
|
+
'- `requiredEvidence`: 非空数组(specVerdict 或 qualityVerdict = cannot_verify 时必填,反逃逸)',
|
|
63
|
+
'- `checklist`(可选): 扁平数组 —— 每项 { item: string, result: ∈ { ' + CHECKLIST_RESULTS.join(' / ') + ' }, note?: string }。注意是**扁平数组**,不是按层(定义/一致/可行性)嵌套对象',
|
|
64
|
+
'- `reviewerNotes`: 说明(verdict=fail 时写明阻断项)',
|
|
65
|
+
'',
|
|
66
|
+
'### docHash 算法(必须等于 reviewedFiles[0] 的 sha256)',
|
|
67
|
+
"- node: `crypto.createHash('sha256').update(require('fs').readFileSync(reviewedFiles[0])).digest('hex')`(对原始字节)",
|
|
68
|
+
'- shell: `sha256sum <reviewedFiles[0]>` 取首列(hex)',
|
|
69
|
+
'- ⚠️ **review.json 写入后若 ' + mainDoc + ' 再被改,必须重算 docHash** —— gate 会重算 reviewedFiles[0] 的 sha256 比对,不符判伪造(历史翻车根因:design 改了 rev 后 docHash 仍是旧值)',
|
|
70
|
+
'',
|
|
71
|
+
'### 完整 JSON 示例(照抄改值)',
|
|
72
|
+
'```json',
|
|
73
|
+
'{',
|
|
74
|
+
' "schemaVersion": ' + REVIEW_SCHEMA_VERSION + ',',
|
|
75
|
+
' "reviewType": "' + reviewType + '",',
|
|
76
|
+
' "specVerdict": "pass",',
|
|
77
|
+
' "qualityVerdict": "pass",',
|
|
78
|
+
' "reviewedFiles": ["' + mainDocRel + '"],',
|
|
79
|
+
' "docHash": "<上面算出的 sha256 hex>",',
|
|
80
|
+
' "checklist": [',
|
|
81
|
+
' { "item": "<审查项>", "result": "pass", "note": "<证据/说明>" }',
|
|
82
|
+
' ],',
|
|
83
|
+
' "reviewerNotes": "<总结>"',
|
|
84
|
+
'}',
|
|
85
|
+
'```',
|
|
86
|
+
]
|
|
87
|
+
return L.join('\n')
|
|
88
|
+
}
|
|
89
|
+
|
|
27
90
|
/**
|
|
28
91
|
* 计算文件内容的 sha256(用于 docHash 真实性校验,防 agent 凭空伪造 review.json)
|
|
29
92
|
* @param {string} filePath - 绝对路径
|
package/src/stages/brainstorm.js
CHANGED
|
@@ -349,7 +349,9 @@ design.md 文件路径 + 自审结果
|
|
|
349
349
|
### 审查执行方式(CLI 按变更规模判定,占位符由 run.js 注入)
|
|
350
350
|
tier: {REVIEW_TIER}({REVIEW_TIER_REASON})
|
|
351
351
|
- tier=self:当前 agent 直接执行下方交叉审查(小变更)
|
|
352
|
-
- tier=independent:必须用 Agent tool 启动一个独立的设计审查子代理(独立上下文,不共享你的分析与倾向),子代理按下方"交叉审查模型"审查 design.md 并输出 review.json
|
|
352
|
+
- tier=independent:必须用 Agent tool 启动一个独立的设计审查子代理(独立上下文,不共享你的分析与倾向),子代理按下方"交叉审查模型"审查 design.md 并输出 review.json。review.json 产物契约(CLI Stage Review Gate 将硬校验,schema + 完整示例 + docHash 算法如下,照抄改值):
|
|
353
|
+
{REVIEW_JSON_CONTRACT}
|
|
354
|
+
子代理只产出 review + Unresolved Blockers,**是否调用 sillyspec run brainstorm --wait 仍由你(主 agent)根据其 verdict 决定**(子代理不直接操作 CLI 状态机)。
|
|
353
355
|
|
|
354
356
|
### 默认行为
|
|
355
357
|
1. 默认必须执行一次交叉审查;不要让用户凭主观判断决定"要不要 Grill"。
|
package/src/stages/execute.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync, readdirSync } from 'fs'
|
|
2
2
|
import path from 'path'
|
|
3
3
|
import { buildContractMatrix, buildConsumerInjection, buildContractFieldInjection } from '../contract-matrix.js'
|
|
4
|
+
import { getRule } from '../stage-contract-spec.js'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* 校验 plan.md 是否满足 execute 执行契约
|
|
@@ -56,7 +57,7 @@ export function validatePlanForExecute(planContent) {
|
|
|
56
57
|
if (ids[0] === 1) {
|
|
57
58
|
for (let i = 0; i < ids.length; i++) {
|
|
58
59
|
if (ids[i] !== i + 1) {
|
|
59
|
-
errors.push(
|
|
60
|
+
errors.push(getRule('plan.task-id-continuity').failMessage.replaceAll('${expected}', String(i + 1).padStart(2, '0')).replaceAll('${actual}', String(ids[i]).padStart(2, '0')))
|
|
60
61
|
break
|
|
61
62
|
}
|
|
62
63
|
}
|
|
@@ -217,7 +218,9 @@ const acceptanceSteps = [
|
|
|
217
218
|
### 执行方式(CLI 按变更规模判定,占位符由 run.js 注入)
|
|
218
219
|
tier: {REVIEW_TIER}({REVIEW_TIER_REASON})
|
|
219
220
|
- tier=self:当前 agent 汇总执行(对照 design.md 逐项检查 + 偏差说明)
|
|
220
|
-
- tier=independent:必须用 Agent tool 启动一个独立的 QA 子代理(独立上下文,不共享实现者的分析),子代理对照 design.md 逐项检查实现一致性并输出 review.json
|
|
221
|
+
- tier=independent:必须用 Agent tool 启动一个独立的 QA 子代理(独立上下文,不共享实现者的分析),子代理对照 design.md 逐项检查实现一致性并输出 review.json。review.json 产物契约(CLI Stage Review Gate 将硬校验,schema + 完整示例 + docHash 算法如下,照抄改值;reviewedFiles 除主文档 design.md 外可追加 git diff 涉及的源码文件):
|
|
222
|
+
{REVIEW_JSON_CONTRACT}
|
|
223
|
+
该 acceptance review 同时覆盖"代码审查"视角(风格/bug/安全/冗余),后续代码审查步骤仅需轻量复审。
|
|
221
224
|
|
|
222
225
|
### 操作
|
|
223
226
|
1. 读取 design.md(技术方案)
|