sillyspec 3.16.0 → 3.16.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/README.md +1 -1
- package/docs/sillyspec/file-lifecycle/known-implementation-gaps.md +99 -0
- package/docs/sillyspec/file-lifecycle/platform-workflows-sync.md +218 -0
- package/docs/sillyspec/file-lifecycle/stage-artifacts.md +167 -0
- package/docs/sillyspec/file-lifecycle/storage-and-state.md +148 -0
- package/docs/sillyspec/file-lifecycle/worktree-and-guard.md +193 -0
- package/docs/sillyspec/file-lifecycle.md +106 -1297
- package/package.json +3 -3
- package/src/hooks/worktree-guard.js +166 -47
- package/src/progress.js +37 -0
- package/src/run.js +309 -56
- package/src/stage-contract.js +349 -0
- package/src/stages/archive.js +6 -10
- package/src/stages/brainstorm.js +4 -1
- package/src/stages/doctor.js +1 -2
- package/src/stages/propose.js +4 -1
- package/src/stages/scan.js +3 -3
- package/test/check-syntax.mjs +26 -0
- package/test/run-tests.mjs +20 -0
- package/test/stage-contract.test.mjs +185 -0
- package/test/stage-definitions.test.mjs +43 -0
- package/test/worktree-guard.test.mjs +78 -0
package/src/run.js
CHANGED
|
@@ -8,6 +8,7 @@ import { basename, join } from 'path'
|
|
|
8
8
|
import { existsSync, readdirSync, mkdirSync, writeFileSync, appendFileSync, readFileSync, statSync } from 'fs'
|
|
9
9
|
import { ProgressManager } from './progress.js'
|
|
10
10
|
import { stageRegistry, auxiliaryStages } from './stages/index.js'
|
|
11
|
+
import { checkTransition, runValidators } from './stage-contract.js'
|
|
11
12
|
import { buildExecuteSteps } from './stages/execute.js'
|
|
12
13
|
import { buildPlanSteps } from './stages/plan.js'
|
|
13
14
|
import { formatExecuteSummary } from './worktree-apply.js'
|
|
@@ -15,10 +16,122 @@ import { formatExecuteSummary } from './worktree-apply.js'
|
|
|
15
16
|
/**
|
|
16
17
|
* 同步触发辅助函数:_write 后 best-effort 同步到平台
|
|
17
18
|
*/
|
|
19
|
+
/**
|
|
20
|
+
* quick 完成审计:对比 baseline 与实际变更
|
|
21
|
+
*/
|
|
22
|
+
async function auditQuickCompletion(cwd, guard, options = {}) {
|
|
23
|
+
const { baselineFiles, allowedFiles = [], allowNew = false, forceBaseline = false } = guard
|
|
24
|
+
const { isConfirm } = options
|
|
25
|
+
const result = { status: 'safe', reasons: [], changedFiles: [], newFiles: [], deletedFiles: [], baselineHit: [] }
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const { execSync } = await import('child_process')
|
|
29
|
+
const gitStatus = execSync('git status --porcelain', { cwd, encoding: 'utf8', timeout: 10000 })
|
|
30
|
+
const currentEntries = gitStatus.trim().split('\n').filter(Boolean)
|
|
31
|
+
|
|
32
|
+
const DANGEROUS_PATTERNS = [
|
|
33
|
+
'.sillyspec/',
|
|
34
|
+
'package.json',
|
|
35
|
+
'package-lock.json',
|
|
36
|
+
'yarn.lock',
|
|
37
|
+
'pnpm-lock.yaml',
|
|
38
|
+
'.eslintrc',
|
|
39
|
+
'tsconfig.json',
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
for (const entry of currentEntries) {
|
|
43
|
+
const status = entry.slice(0, 2).trim()
|
|
44
|
+
const file = entry.slice(3).trim()
|
|
45
|
+
if (!file || file.startsWith('??. ')) continue
|
|
46
|
+
|
|
47
|
+
result.changedFiles.push(file)
|
|
48
|
+
if (status === 'D' || status === ' D') result.deletedFiles.push(file)
|
|
49
|
+
if (status === '??') result.newFiles.push(file)
|
|
50
|
+
|
|
51
|
+
// 检查是否命中 baseline protected files
|
|
52
|
+
if (baselineFiles.includes(file)) {
|
|
53
|
+
result.baselineHit.push(file)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 检查危险文件(除非 force-baseline)
|
|
57
|
+
if (DANGEROUS_PATTERNS.some(p => file.includes(p)) && !forceBaseline) {
|
|
58
|
+
result.reasons.push(`危险文件变更: ${file}`)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 检查 deleted files
|
|
63
|
+
for (const f of result.deletedFiles) {
|
|
64
|
+
result.reasons.push(`删除文件: ${f}`)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 检查 baseline hit(除非 force-baseline)
|
|
68
|
+
if (!forceBaseline) {
|
|
69
|
+
for (const f of result.baselineHit) {
|
|
70
|
+
result.reasons.push(`覆盖 baseline 文件: ${f}`)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 检查 new files(除非 allow-new)
|
|
75
|
+
if (!allowNew) {
|
|
76
|
+
for (const f of result.newFiles) {
|
|
77
|
+
if (!f.startsWith('.sillyspec/quicklog/') && !f.startsWith('.sillyspec/.runtime/')) {
|
|
78
|
+
result.reasons.push(`新增文件(需 --allow-new): ${f}`)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 检查 allowedFiles 范围
|
|
84
|
+
if (allowedFiles.length > 0) {
|
|
85
|
+
for (const f of result.changedFiles) {
|
|
86
|
+
if (!allowedFiles.includes(f) && !f.startsWith('.sillyspec/')) {
|
|
87
|
+
result.reasons.push(`超出 allowedFiles: ${f}`)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 判定结果
|
|
93
|
+
if (result.baselineHit.length > 0 || result.deletedFiles.length > 0 || result.reasons.some(r => r.startsWith('危险') || r.startsWith('删除'))) {
|
|
94
|
+
result.status = 'blocked'
|
|
95
|
+
} else if (result.newFiles.length > 0 || (allowedFiles.length > 0 && result.reasons.some(r => r.startsWith('超出')))) {
|
|
96
|
+
result.status = 'warning'
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// --confirm 模式:展示 diff 并等待确认
|
|
100
|
+
if (isConfirm && (result.status === 'warning' || result.status === 'blocked')) {
|
|
101
|
+
console.log(`\n📋 quick 变更概览:`)
|
|
102
|
+
console.log(` 新增: ${result.newFiles.length}, 修改: ${result.changedFiles.length - result.newFiles.length - result.deletedFiles.length}, 删除: ${result.deletedFiles.length}`)
|
|
103
|
+
if (result.changedFiles.length > 0) {
|
|
104
|
+
console.log(`\n 变更文件:`)
|
|
105
|
+
for (const f of result.changedFiles) {
|
|
106
|
+
const isBaseline = baselineFiles.includes(f)
|
|
107
|
+
const isDangerous = DANGEROUS_PATTERNS.some(p => f.includes(p))
|
|
108
|
+
const marker = isBaseline ? '🔴' : isDangerous ? '⚠️' : ' '
|
|
109
|
+
console.log(` ${marker} ${f}`)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
console.log(`\n 状态: ${result.status.toUpperCase()}`)
|
|
113
|
+
if (result.reasons.length > 0) {
|
|
114
|
+
console.log(` 原因:`)
|
|
115
|
+
for (const r of result.reasons) {
|
|
116
|
+
console.log(` - ${r}`)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
console.log(`\n 如确认接受这些变更,重新运行:sillyspec run quick --done --confirm --output "..."`)
|
|
120
|
+
console.log(` 或使用 --force-baseline 允许覆盖 baseline 文件,--allow-new 允许新增文件`)
|
|
121
|
+
}
|
|
122
|
+
} catch (e) {
|
|
123
|
+
result.reasons.push(`审计失败: ${e.message}`)
|
|
124
|
+
result.status = 'warning'
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return result
|
|
128
|
+
}
|
|
129
|
+
|
|
18
130
|
async function triggerSync(cwd, changeName) {
|
|
19
131
|
try {
|
|
132
|
+
if (changeName && !existsSync(join(cwd, '.sillyspec', 'changes', changeName))) return
|
|
20
133
|
const syncMod = await import('./sync.js')
|
|
21
|
-
await syncMod.sync(
|
|
134
|
+
await syncMod.sync(changeName, cwd)
|
|
22
135
|
} catch (e) {
|
|
23
136
|
// sync.js 不存在或同步失败,静默跳过
|
|
24
137
|
console.warn('⚠️ 同步失败:', e.message)
|
|
@@ -32,7 +145,7 @@ async function triggerSync(cwd, changeName) {
|
|
|
32
145
|
async function checkApproval(cwd, changeName) {
|
|
33
146
|
try {
|
|
34
147
|
const syncMod = await import('./sync.js')
|
|
35
|
-
return await syncMod.checkApproval(
|
|
148
|
+
return await syncMod.checkApproval(changeName, cwd)
|
|
36
149
|
} catch (e) {
|
|
37
150
|
// sync.js 不存在或检查失败,静默跳过
|
|
38
151
|
return null
|
|
@@ -314,23 +427,38 @@ export async function runCommand(args, cwd) {
|
|
|
314
427
|
scanRunId: getFlagValue('--scan-run-id'),
|
|
315
428
|
}
|
|
316
429
|
|
|
317
|
-
// 跨 --done
|
|
318
|
-
// 首次 scan
|
|
430
|
+
// 跨 --done 生命周期:从 metadata 文件恢复 platformOpts
|
|
431
|
+
// 首次 scan 时写入,所有后续调用(包括 run、--done、--skip)都读取
|
|
319
432
|
const platformOptsFile = join(cwd, '.sillyspec', '.runtime', 'platform-scan.json')
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
433
|
+
let platformFileExists = existsSync(platformOptsFile)
|
|
434
|
+
// 如果命令行没传 spec-root,尝试从持久化文件恢复
|
|
435
|
+
if (!platformOpts.specRoot && !platformOpts.runtimeRoot) {
|
|
436
|
+
if (platformFileExists) {
|
|
437
|
+
try {
|
|
438
|
+
const { readFileSync } = await import('fs')
|
|
439
|
+
const saved = JSON.parse(readFileSync(platformOptsFile, 'utf8'))
|
|
440
|
+
if (saved.specRoot) platformOpts.specRoot = saved.specRoot
|
|
441
|
+
if (saved.runtimeRoot) platformOpts.runtimeRoot = saved.runtimeRoot
|
|
442
|
+
if (saved.workspaceId) platformOpts.workspaceId = saved.workspaceId
|
|
443
|
+
if (saved.scanRunId) platformOpts.scanRunId = saved.scanRunId
|
|
444
|
+
// 平台模式 fail-fast:文件存在但缺少 specRoot
|
|
445
|
+
if (!platformOpts.specRoot && !platformOpts.runtimeRoot) {
|
|
446
|
+
console.error(`❌ 平台模式参数文件存在但缺少 specRoot/runtimeRoot: ${platformOptsFile}`)
|
|
447
|
+
console.error(' 可能原因:platform-scan.json 损坏或写入不完整')
|
|
448
|
+
console.error(' 解决:重新运行首次 scan 并传入 --spec-root')
|
|
449
|
+
process.exit(1)
|
|
450
|
+
}
|
|
451
|
+
} catch (e) {
|
|
452
|
+
console.error(`❌ 平台模式参数文件读取失败: ${platformOptsFile}`)
|
|
453
|
+
console.error(` 错误: ${e.message}`)
|
|
454
|
+
console.error(' 可能原因:文件损坏')
|
|
455
|
+
console.error(' 解决:删除该文件并重新运行首次 scan 传入 --spec-root')
|
|
456
|
+
process.exit(1)
|
|
457
|
+
}
|
|
331
458
|
}
|
|
332
|
-
}
|
|
333
|
-
|
|
459
|
+
}
|
|
460
|
+
// 持久化 platformOpts(命令行传入或已恢复的都持久化)
|
|
461
|
+
if (platformOpts.specRoot || platformOpts.runtimeRoot) {
|
|
334
462
|
try {
|
|
335
463
|
const { mkdirSync, writeFileSync } = await import('fs')
|
|
336
464
|
mkdirSync(join(cwd, '.sillyspec', '.runtime'), { recursive: true })
|
|
@@ -367,11 +495,22 @@ export async function runCommand(args, cwd) {
|
|
|
367
495
|
changeName = flags[changeIdx + 1]
|
|
368
496
|
}
|
|
369
497
|
|
|
498
|
+
// 解析 --files a.js,b.js(quick 专用:显式声明 allowedFiles)
|
|
499
|
+
let quickFiles = []
|
|
500
|
+
const filesIdx = flags.indexOf('--files')
|
|
501
|
+
if (filesIdx !== -1 && flags[filesIdx + 1]) {
|
|
502
|
+
quickFiles = flags[filesIdx + 1].split(',').map(f => f.trim()).filter(Boolean)
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const isAllowNew = flags.includes('--allow-new')
|
|
506
|
+
const isForceBaseline = flags.includes('--force-baseline')
|
|
507
|
+
|
|
370
508
|
// 未知参数 fail-fast
|
|
371
509
|
const knownFlags = new Set([
|
|
372
510
|
'--done', '--skip', '--status', '--reset', '--confirm', '--skip-approval',
|
|
373
511
|
'--output', '--input', '--change',
|
|
374
512
|
'--spec-root', '--runtime-root', '--workspace-id', '--scan-run-id',
|
|
513
|
+
'--files', '--allow-new', '--force-baseline',
|
|
375
514
|
'--json', '--dir', '--help',
|
|
376
515
|
])
|
|
377
516
|
for (let i = 0; i < flags.length; i++) {
|
|
@@ -482,6 +621,18 @@ function resolveChangeNameAuto(cwd) {
|
|
|
482
621
|
}
|
|
483
622
|
|
|
484
623
|
async function runStage(pm, progress, stageName, cwd, changeName, skipApproval = false, platformOpts = {}) {
|
|
624
|
+
// 状态转换校验
|
|
625
|
+
const prevStage = progress.currentStage || ''
|
|
626
|
+
const transition = checkTransition(prevStage, stageName)
|
|
627
|
+
if (!transition.allowed) {
|
|
628
|
+
console.error(`❌ 阶段转换不允许: ${prevStage || '(起始)'} → ${stageName}`)
|
|
629
|
+
console.error(` 原因: ${transition.reason}`)
|
|
630
|
+
console.error(` 提示: 使用 --skip-approval 绕过(需明确意图)`)
|
|
631
|
+
if (!skipApproval) {
|
|
632
|
+
process.exit(1)
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
485
636
|
// execute 阶段启动前检查审批
|
|
486
637
|
if (stageName === 'execute' && !skipApproval) {
|
|
487
638
|
const approval = await checkApproval(cwd, changeName)
|
|
@@ -536,6 +687,39 @@ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval =
|
|
|
536
687
|
console.log(`🔄 ${stageName} 阶段已自动重置,重新开始。\n`)
|
|
537
688
|
}
|
|
538
689
|
|
|
690
|
+
// quick 阶段:记录 baselineFiles
|
|
691
|
+
if (stageName === 'quick' && !progress.quickGuard) {
|
|
692
|
+
try {
|
|
693
|
+
const { execSync } = await import('child_process')
|
|
694
|
+
const gitStatus = execSync('git status --porcelain', { cwd, encoding: 'utf8', timeout: 10000 })
|
|
695
|
+
const baselineFiles = gitStatus
|
|
696
|
+
.trim().split('\n').filter(Boolean)
|
|
697
|
+
.map(line => line.slice(3).trim())
|
|
698
|
+
.filter(f => !f.startsWith('.sillyspec/'))
|
|
699
|
+
const allowedFiles = quickFiles || []
|
|
700
|
+
const allowNew = isAllowNew || false
|
|
701
|
+
const forceBaseline = isForceBaseline || false
|
|
702
|
+
progress.quickGuard = {
|
|
703
|
+
baselineCommit: execSync('git rev-parse HEAD', { cwd, encoding: 'utf8', timeout: 5000 }).trim(),
|
|
704
|
+
baselineFiles,
|
|
705
|
+
allowedFiles,
|
|
706
|
+
allowNew,
|
|
707
|
+
forceBaseline,
|
|
708
|
+
startedAt: new Date().toISOString(),
|
|
709
|
+
}
|
|
710
|
+
// 写入 quick-guard.json 供 worktree-guard hook 读取
|
|
711
|
+
const guardFile = join(cwd, '.sillyspec', '.runtime', 'quick-guard.json')
|
|
712
|
+
writeFileSync(guardFile, JSON.stringify(progress.quickGuard, null, 2))
|
|
713
|
+
const parts = [`${baselineFiles.length} 个已有脏文件`]
|
|
714
|
+
if (allowedFiles.length > 0) parts.push(`${allowedFiles.length} 个 allowedFiles`)
|
|
715
|
+
if (allowNew) parts.push('允许新增文件')
|
|
716
|
+
console.log(`🛡️ quick 变更边界已记录: ${parts.join(', ')}`)
|
|
717
|
+
await pm._write(cwd, progress, changeName)
|
|
718
|
+
} catch (e) {
|
|
719
|
+
console.warn(`⚠️ baseline 记录失败: ${e.message}`)
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
539
723
|
if (currentIdx > 0) {
|
|
540
724
|
const completed = currentIdx
|
|
541
725
|
const total = steps.length
|
|
@@ -627,6 +811,39 @@ function validateFileLocations(cwd, stageName, progress, changeName) {
|
|
|
627
811
|
}
|
|
628
812
|
}
|
|
629
813
|
|
|
814
|
+
async function archiveChangeDirectory(pm, cwd, progress) {
|
|
815
|
+
const { renameSync } = await import('fs')
|
|
816
|
+
const archiveChangeName = progress.currentChange
|
|
817
|
+
if (!archiveChangeName) {
|
|
818
|
+
console.error('❌ 归档失败:未找到当前变更名(currentChange)')
|
|
819
|
+
process.exit(1)
|
|
820
|
+
}
|
|
821
|
+
const changesDir = join(cwd, '.sillyspec', 'changes')
|
|
822
|
+
const archiveDir = join(changesDir, 'archive')
|
|
823
|
+
const srcDir = join(changesDir, archiveChangeName)
|
|
824
|
+
const date = new Date().toISOString().slice(0, 10)
|
|
825
|
+
const destDir = join(archiveDir, `${date}-${archiveChangeName}`)
|
|
826
|
+
|
|
827
|
+
if (!existsSync(srcDir)) {
|
|
828
|
+
console.error(`❌ 归档失败:源目录不存在 ${srcDir}`)
|
|
829
|
+
process.exit(1)
|
|
830
|
+
}
|
|
831
|
+
if (existsSync(destDir)) {
|
|
832
|
+
console.error(`❌ 归档失败:目标目录已存在 ${destDir}`)
|
|
833
|
+
process.exit(1)
|
|
834
|
+
}
|
|
835
|
+
mkdirSync(archiveDir, { recursive: true })
|
|
836
|
+
renameSync(srcDir, destDir)
|
|
837
|
+
|
|
838
|
+
if (!existsSync(destDir) || existsSync(srcDir)) {
|
|
839
|
+
console.error('❌ 归档校验失败:移动操作异常')
|
|
840
|
+
process.exit(1)
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
await pm.unregisterChange(cwd, archiveChangeName)
|
|
844
|
+
console.log(`📦 已归档:${archiveChangeName} → archive/${date}-${archiveChangeName}/`)
|
|
845
|
+
}
|
|
846
|
+
|
|
630
847
|
async function completeStep(pm, progress, stageName, cwd, outputText, inputText = null, options = {}) {
|
|
631
848
|
const { printNext = true, confirm = false, changeName, platformOpts = {} } = options
|
|
632
849
|
const stageData = progress.stages[stageName]
|
|
@@ -661,6 +878,36 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
661
878
|
}
|
|
662
879
|
}
|
|
663
880
|
|
|
881
|
+
if (stageName === 'archive' && steps[currentIdx]?.name === '确认归档') {
|
|
882
|
+
if (!confirm) {
|
|
883
|
+
steps[currentIdx].status = 'pending'
|
|
884
|
+
steps[currentIdx].completedAt = null
|
|
885
|
+
if (outputText) steps[currentIdx].output = null
|
|
886
|
+
await pm._write(cwd, progress, changeName)
|
|
887
|
+
console.log('⚠️ 请添加 --confirm 确认归档,例如:sillyspec run archive --done --confirm --output "确认归档"')
|
|
888
|
+
return { stageCompleted: false, currentIdx, nextPendingIdx: currentIdx }
|
|
889
|
+
}
|
|
890
|
+
await archiveChangeDirectory(pm, cwd, progress)
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// archive "确认归档" 步骤完成后,校验归档完整性
|
|
894
|
+
if (stageName === 'archive' && steps[currentIdx]?.name === '确认归档' && confirm) {
|
|
895
|
+
const projectName = progress.project || basename(cwd)
|
|
896
|
+
const contractResult = runValidators('archive', cwd, changeName, { projectName, specRoot: platformOpts?.specRoot })
|
|
897
|
+
if (contractResult.errors.length > 0) {
|
|
898
|
+
console.error(`\n❌ 归档校验失败:`)
|
|
899
|
+
for (const err of contractResult.errors) {
|
|
900
|
+
console.error(` - ${err}`)
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
if (contractResult.warnings.length > 0) {
|
|
904
|
+
console.warn(`\n⚠️ 归档校验警告:`)
|
|
905
|
+
for (const w of contractResult.warnings) {
|
|
906
|
+
console.warn(` - ${w}`)
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
|
|
664
911
|
// plan 阶段 "展开任务" 完成后,动态插入任务蓝图协调器步骤
|
|
665
912
|
if (stageName === 'plan' && steps[currentIdx]?.name === '展开任务并分组') {
|
|
666
913
|
const changeDir = resolveChangeDir(cwd, progress)
|
|
@@ -842,45 +1089,6 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
842
1089
|
// 验证关键文件是否在正确的变更目录下
|
|
843
1090
|
validateFileLocations(cwd, stageName, progress, changeName)
|
|
844
1091
|
|
|
845
|
-
// archive 阶段确认归档
|
|
846
|
-
if (stageName === 'archive' && steps[currentIdx]?.name === '确认归档') {
|
|
847
|
-
if (confirm) {
|
|
848
|
-
const { renameSync } = await import('fs')
|
|
849
|
-
const archiveChangeName = progress.currentChange
|
|
850
|
-
if (!archiveChangeName) {
|
|
851
|
-
console.error('❌ 归档失败:未找到当前变更名(currentChange)')
|
|
852
|
-
process.exit(1)
|
|
853
|
-
}
|
|
854
|
-
const changesDir = join(cwd, '.sillyspec', 'changes')
|
|
855
|
-
const archiveDir = join(changesDir, 'archive')
|
|
856
|
-
const srcDir = join(changesDir, archiveChangeName)
|
|
857
|
-
const date = new Date().toISOString().slice(0, 10)
|
|
858
|
-
const destDir = join(archiveDir, `${date}-${archiveChangeName}`)
|
|
859
|
-
|
|
860
|
-
if (!existsSync(srcDir)) {
|
|
861
|
-
console.error(`❌ 归档失败:源目录不存在 ${srcDir}`)
|
|
862
|
-
process.exit(1)
|
|
863
|
-
}
|
|
864
|
-
if (existsSync(destDir)) {
|
|
865
|
-
console.error(`❌ 归档失败:目标目录已存在 ${destDir}`)
|
|
866
|
-
process.exit(1)
|
|
867
|
-
}
|
|
868
|
-
mkdirSync(archiveDir, { recursive: true })
|
|
869
|
-
renameSync(srcDir, destDir)
|
|
870
|
-
|
|
871
|
-
if (!existsSync(destDir) || existsSync(srcDir)) {
|
|
872
|
-
console.error('❌ 归档校验失败:移动操作异常')
|
|
873
|
-
process.exit(1)
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
// 从全局活跃列表移除
|
|
877
|
-
await pm.unregisterChange(cwd, archiveChangeName)
|
|
878
|
-
console.log(`📦 已归档:${archiveChangeName} → archive/${date}-${archiveChangeName}/`)
|
|
879
|
-
} else {
|
|
880
|
-
console.log('⚠️ 请添加 --confirm 确认归档,例如:sillyspec run archive --done --confirm --output "确认归档"')
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
|
|
884
1092
|
// 辅助阶段完成后重置步骤
|
|
885
1093
|
const stageDef = stageRegistry[stageName]
|
|
886
1094
|
if (stageDef?.auxiliary) {
|
|
@@ -893,6 +1101,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
893
1101
|
stageData.steps = freshSteps
|
|
894
1102
|
stageData.status = 'pending'
|
|
895
1103
|
stageData.completedAt = null
|
|
1104
|
+
if (progress.currentStage === stageName) progress.currentStage = ''
|
|
896
1105
|
await pm._write(cwd, progress, changeName)
|
|
897
1106
|
}
|
|
898
1107
|
|
|
@@ -925,6 +1134,50 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
925
1134
|
return { stageCompleted: true, currentIdx, nextPendingIdx: -1 }
|
|
926
1135
|
}
|
|
927
1136
|
|
|
1137
|
+
// 阶段完成校验
|
|
1138
|
+
const projectName = progress.project || basename(cwd)
|
|
1139
|
+
const contractResult = runValidators(stageName, cwd, changeName, { projectName, specRoot: platformOpts?.specRoot })
|
|
1140
|
+
if (contractResult.errors.length > 0) {
|
|
1141
|
+
console.error(`\n❌ 阶段 ${stageName} 校验失败:`)
|
|
1142
|
+
for (const err of contractResult.errors) {
|
|
1143
|
+
console.error(` - ${err}`)
|
|
1144
|
+
}
|
|
1145
|
+
console.error(`\n 提示:修复缺失产物后重新运行此步骤,或使用 --skip-approval 跳过校验`)
|
|
1146
|
+
}
|
|
1147
|
+
if (contractResult.warnings.length > 0) {
|
|
1148
|
+
console.warn(`\n⚠️ 阶段 ${stageName} 校验警告:`)
|
|
1149
|
+
for (const w of contractResult.warnings) {
|
|
1150
|
+
console.warn(` - ${w}`)
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
// quick 阶段完成审计
|
|
1155
|
+
if (stageName === 'quick' && progress.quickGuard) {
|
|
1156
|
+
const review = await auditQuickCompletion(cwd, progress.quickGuard, { isConfirm })
|
|
1157
|
+
progress.quickGuard.review = review
|
|
1158
|
+
progress.quickGuard.completedAt = new Date().toISOString()
|
|
1159
|
+
// 清理 quick-guard.json
|
|
1160
|
+
try {
|
|
1161
|
+
const { unlinkSync } = await import('fs')
|
|
1162
|
+
const guardFile = join(cwd, '.sillyspec', '.runtime', 'quick-guard.json')
|
|
1163
|
+
unlinkSync(guardFile)
|
|
1164
|
+
} catch {}
|
|
1165
|
+
if (review.status === 'blocked') {
|
|
1166
|
+
console.error(`\n🚫 quick 变更边界审计 — BLOCKED:`)
|
|
1167
|
+
for (const r of review.reasons) {
|
|
1168
|
+
console.error(` - ${r}`)
|
|
1169
|
+
}
|
|
1170
|
+
console.error(`\n 这些文件是 baseline 保护的,不应被修改。`)
|
|
1171
|
+
} else if (review.status === 'warning') {
|
|
1172
|
+
console.warn(`\n⚠️ quick 变更边界审计 — WARNING:`)
|
|
1173
|
+
for (const r of review.reasons) {
|
|
1174
|
+
console.warn(` - ${r}`)
|
|
1175
|
+
}
|
|
1176
|
+
} else {
|
|
1177
|
+
console.log(`\n✅ quick 变更边界审计 — SAFE (变更 ${review.changedFiles.length} 个文件)`)
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
|
|
928
1181
|
progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
929
1182
|
await pm._write(cwd, progress, changeName)
|
|
930
1183
|
triggerSync(cwd, changeName)
|