sillyspec 3.16.0 → 3.16.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/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 +279 -41
- package/src/stage-contract.js +347 -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 +128 -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
|
|
@@ -367,11 +480,22 @@ export async function runCommand(args, cwd) {
|
|
|
367
480
|
changeName = flags[changeIdx + 1]
|
|
368
481
|
}
|
|
369
482
|
|
|
483
|
+
// 解析 --files a.js,b.js(quick 专用:显式声明 allowedFiles)
|
|
484
|
+
let quickFiles = []
|
|
485
|
+
const filesIdx = flags.indexOf('--files')
|
|
486
|
+
if (filesIdx !== -1 && flags[filesIdx + 1]) {
|
|
487
|
+
quickFiles = flags[filesIdx + 1].split(',').map(f => f.trim()).filter(Boolean)
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const isAllowNew = flags.includes('--allow-new')
|
|
491
|
+
const isForceBaseline = flags.includes('--force-baseline')
|
|
492
|
+
|
|
370
493
|
// 未知参数 fail-fast
|
|
371
494
|
const knownFlags = new Set([
|
|
372
495
|
'--done', '--skip', '--status', '--reset', '--confirm', '--skip-approval',
|
|
373
496
|
'--output', '--input', '--change',
|
|
374
497
|
'--spec-root', '--runtime-root', '--workspace-id', '--scan-run-id',
|
|
498
|
+
'--files', '--allow-new', '--force-baseline',
|
|
375
499
|
'--json', '--dir', '--help',
|
|
376
500
|
])
|
|
377
501
|
for (let i = 0; i < flags.length; i++) {
|
|
@@ -482,6 +606,18 @@ function resolveChangeNameAuto(cwd) {
|
|
|
482
606
|
}
|
|
483
607
|
|
|
484
608
|
async function runStage(pm, progress, stageName, cwd, changeName, skipApproval = false, platformOpts = {}) {
|
|
609
|
+
// 状态转换校验
|
|
610
|
+
const prevStage = progress.currentStage || ''
|
|
611
|
+
const transition = checkTransition(prevStage, stageName)
|
|
612
|
+
if (!transition.allowed) {
|
|
613
|
+
console.error(`❌ 阶段转换不允许: ${prevStage || '(起始)'} → ${stageName}`)
|
|
614
|
+
console.error(` 原因: ${transition.reason}`)
|
|
615
|
+
console.error(` 提示: 使用 --skip-approval 绕过(需明确意图)`)
|
|
616
|
+
if (!skipApproval) {
|
|
617
|
+
process.exit(1)
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
485
621
|
// execute 阶段启动前检查审批
|
|
486
622
|
if (stageName === 'execute' && !skipApproval) {
|
|
487
623
|
const approval = await checkApproval(cwd, changeName)
|
|
@@ -536,6 +672,39 @@ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval =
|
|
|
536
672
|
console.log(`🔄 ${stageName} 阶段已自动重置,重新开始。\n`)
|
|
537
673
|
}
|
|
538
674
|
|
|
675
|
+
// quick 阶段:记录 baselineFiles
|
|
676
|
+
if (stageName === 'quick' && !progress.quickGuard) {
|
|
677
|
+
try {
|
|
678
|
+
const { execSync } = await import('child_process')
|
|
679
|
+
const gitStatus = execSync('git status --porcelain', { cwd, encoding: 'utf8', timeout: 10000 })
|
|
680
|
+
const baselineFiles = gitStatus
|
|
681
|
+
.trim().split('\n').filter(Boolean)
|
|
682
|
+
.map(line => line.slice(3).trim())
|
|
683
|
+
.filter(f => !f.startsWith('.sillyspec/'))
|
|
684
|
+
const allowedFiles = quickFiles || []
|
|
685
|
+
const allowNew = isAllowNew || false
|
|
686
|
+
const forceBaseline = isForceBaseline || false
|
|
687
|
+
progress.quickGuard = {
|
|
688
|
+
baselineCommit: execSync('git rev-parse HEAD', { cwd, encoding: 'utf8', timeout: 5000 }).trim(),
|
|
689
|
+
baselineFiles,
|
|
690
|
+
allowedFiles,
|
|
691
|
+
allowNew,
|
|
692
|
+
forceBaseline,
|
|
693
|
+
startedAt: new Date().toISOString(),
|
|
694
|
+
}
|
|
695
|
+
// 写入 quick-guard.json 供 worktree-guard hook 读取
|
|
696
|
+
const guardFile = join(cwd, '.sillyspec', '.runtime', 'quick-guard.json')
|
|
697
|
+
writeFileSync(guardFile, JSON.stringify(progress.quickGuard, null, 2))
|
|
698
|
+
const parts = [`${baselineFiles.length} 个已有脏文件`]
|
|
699
|
+
if (allowedFiles.length > 0) parts.push(`${allowedFiles.length} 个 allowedFiles`)
|
|
700
|
+
if (allowNew) parts.push('允许新增文件')
|
|
701
|
+
console.log(`🛡️ quick 变更边界已记录: ${parts.join(', ')}`)
|
|
702
|
+
await pm._write(cwd, progress, changeName)
|
|
703
|
+
} catch (e) {
|
|
704
|
+
console.warn(`⚠️ baseline 记录失败: ${e.message}`)
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
539
708
|
if (currentIdx > 0) {
|
|
540
709
|
const completed = currentIdx
|
|
541
710
|
const total = steps.length
|
|
@@ -627,6 +796,39 @@ function validateFileLocations(cwd, stageName, progress, changeName) {
|
|
|
627
796
|
}
|
|
628
797
|
}
|
|
629
798
|
|
|
799
|
+
async function archiveChangeDirectory(pm, cwd, progress) {
|
|
800
|
+
const { renameSync } = await import('fs')
|
|
801
|
+
const archiveChangeName = progress.currentChange
|
|
802
|
+
if (!archiveChangeName) {
|
|
803
|
+
console.error('❌ 归档失败:未找到当前变更名(currentChange)')
|
|
804
|
+
process.exit(1)
|
|
805
|
+
}
|
|
806
|
+
const changesDir = join(cwd, '.sillyspec', 'changes')
|
|
807
|
+
const archiveDir = join(changesDir, 'archive')
|
|
808
|
+
const srcDir = join(changesDir, archiveChangeName)
|
|
809
|
+
const date = new Date().toISOString().slice(0, 10)
|
|
810
|
+
const destDir = join(archiveDir, `${date}-${archiveChangeName}`)
|
|
811
|
+
|
|
812
|
+
if (!existsSync(srcDir)) {
|
|
813
|
+
console.error(`❌ 归档失败:源目录不存在 ${srcDir}`)
|
|
814
|
+
process.exit(1)
|
|
815
|
+
}
|
|
816
|
+
if (existsSync(destDir)) {
|
|
817
|
+
console.error(`❌ 归档失败:目标目录已存在 ${destDir}`)
|
|
818
|
+
process.exit(1)
|
|
819
|
+
}
|
|
820
|
+
mkdirSync(archiveDir, { recursive: true })
|
|
821
|
+
renameSync(srcDir, destDir)
|
|
822
|
+
|
|
823
|
+
if (!existsSync(destDir) || existsSync(srcDir)) {
|
|
824
|
+
console.error('❌ 归档校验失败:移动操作异常')
|
|
825
|
+
process.exit(1)
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
await pm.unregisterChange(cwd, archiveChangeName)
|
|
829
|
+
console.log(`📦 已归档:${archiveChangeName} → archive/${date}-${archiveChangeName}/`)
|
|
830
|
+
}
|
|
831
|
+
|
|
630
832
|
async function completeStep(pm, progress, stageName, cwd, outputText, inputText = null, options = {}) {
|
|
631
833
|
const { printNext = true, confirm = false, changeName, platformOpts = {} } = options
|
|
632
834
|
const stageData = progress.stages[stageName]
|
|
@@ -661,6 +863,36 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
661
863
|
}
|
|
662
864
|
}
|
|
663
865
|
|
|
866
|
+
if (stageName === 'archive' && steps[currentIdx]?.name === '确认归档') {
|
|
867
|
+
if (!confirm) {
|
|
868
|
+
steps[currentIdx].status = 'pending'
|
|
869
|
+
steps[currentIdx].completedAt = null
|
|
870
|
+
if (outputText) steps[currentIdx].output = null
|
|
871
|
+
await pm._write(cwd, progress, changeName)
|
|
872
|
+
console.log('⚠️ 请添加 --confirm 确认归档,例如:sillyspec run archive --done --confirm --output "确认归档"')
|
|
873
|
+
return { stageCompleted: false, currentIdx, nextPendingIdx: currentIdx }
|
|
874
|
+
}
|
|
875
|
+
await archiveChangeDirectory(pm, cwd, progress)
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// archive "确认归档" 步骤完成后,校验归档完整性
|
|
879
|
+
if (stageName === 'archive' && steps[currentIdx]?.name === '确认归档' && confirm) {
|
|
880
|
+
const projectName = progress.project || basename(cwd)
|
|
881
|
+
const contractResult = runValidators('archive', cwd, changeName, { projectName })
|
|
882
|
+
if (contractResult.errors.length > 0) {
|
|
883
|
+
console.error(`\n❌ 归档校验失败:`)
|
|
884
|
+
for (const err of contractResult.errors) {
|
|
885
|
+
console.error(` - ${err}`)
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
if (contractResult.warnings.length > 0) {
|
|
889
|
+
console.warn(`\n⚠️ 归档校验警告:`)
|
|
890
|
+
for (const w of contractResult.warnings) {
|
|
891
|
+
console.warn(` - ${w}`)
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
664
896
|
// plan 阶段 "展开任务" 完成后,动态插入任务蓝图协调器步骤
|
|
665
897
|
if (stageName === 'plan' && steps[currentIdx]?.name === '展开任务并分组') {
|
|
666
898
|
const changeDir = resolveChangeDir(cwd, progress)
|
|
@@ -842,45 +1074,6 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
842
1074
|
// 验证关键文件是否在正确的变更目录下
|
|
843
1075
|
validateFileLocations(cwd, stageName, progress, changeName)
|
|
844
1076
|
|
|
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
1077
|
// 辅助阶段完成后重置步骤
|
|
885
1078
|
const stageDef = stageRegistry[stageName]
|
|
886
1079
|
if (stageDef?.auxiliary) {
|
|
@@ -893,6 +1086,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
893
1086
|
stageData.steps = freshSteps
|
|
894
1087
|
stageData.status = 'pending'
|
|
895
1088
|
stageData.completedAt = null
|
|
1089
|
+
if (progress.currentStage === stageName) progress.currentStage = ''
|
|
896
1090
|
await pm._write(cwd, progress, changeName)
|
|
897
1091
|
}
|
|
898
1092
|
|
|
@@ -925,6 +1119,50 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
925
1119
|
return { stageCompleted: true, currentIdx, nextPendingIdx: -1 }
|
|
926
1120
|
}
|
|
927
1121
|
|
|
1122
|
+
// 阶段完成校验
|
|
1123
|
+
const projectName = progress.project || basename(cwd)
|
|
1124
|
+
const contractResult = runValidators(stageName, cwd, changeName, { projectName })
|
|
1125
|
+
if (contractResult.errors.length > 0) {
|
|
1126
|
+
console.error(`\n❌ 阶段 ${stageName} 校验失败:`)
|
|
1127
|
+
for (const err of contractResult.errors) {
|
|
1128
|
+
console.error(` - ${err}`)
|
|
1129
|
+
}
|
|
1130
|
+
console.error(`\n 提示:修复缺失产物后重新运行此步骤,或使用 --skip-approval 跳过校验`)
|
|
1131
|
+
}
|
|
1132
|
+
if (contractResult.warnings.length > 0) {
|
|
1133
|
+
console.warn(`\n⚠️ 阶段 ${stageName} 校验警告:`)
|
|
1134
|
+
for (const w of contractResult.warnings) {
|
|
1135
|
+
console.warn(` - ${w}`)
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// quick 阶段完成审计
|
|
1140
|
+
if (stageName === 'quick' && progress.quickGuard) {
|
|
1141
|
+
const review = await auditQuickCompletion(cwd, progress.quickGuard, { isConfirm })
|
|
1142
|
+
progress.quickGuard.review = review
|
|
1143
|
+
progress.quickGuard.completedAt = new Date().toISOString()
|
|
1144
|
+
// 清理 quick-guard.json
|
|
1145
|
+
try {
|
|
1146
|
+
const { unlinkSync } = await import('fs')
|
|
1147
|
+
const guardFile = join(cwd, '.sillyspec', '.runtime', 'quick-guard.json')
|
|
1148
|
+
unlinkSync(guardFile)
|
|
1149
|
+
} catch {}
|
|
1150
|
+
if (review.status === 'blocked') {
|
|
1151
|
+
console.error(`\n🚫 quick 变更边界审计 — BLOCKED:`)
|
|
1152
|
+
for (const r of review.reasons) {
|
|
1153
|
+
console.error(` - ${r}`)
|
|
1154
|
+
}
|
|
1155
|
+
console.error(`\n 这些文件是 baseline 保护的,不应被修改。`)
|
|
1156
|
+
} else if (review.status === 'warning') {
|
|
1157
|
+
console.warn(`\n⚠️ quick 变更边界审计 — WARNING:`)
|
|
1158
|
+
for (const r of review.reasons) {
|
|
1159
|
+
console.warn(` - ${r}`)
|
|
1160
|
+
}
|
|
1161
|
+
} else {
|
|
1162
|
+
console.log(`\n✅ quick 变更边界审计 — SAFE (变更 ${review.changedFiles.length} 个文件)`)
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
|
|
928
1166
|
progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
929
1167
|
await pm._write(cwd, progress, changeName)
|
|
930
1168
|
triggerSync(cwd, changeName)
|