sillyspec 3.17.2 → 3.17.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/init.js +9 -1
- package/src/run.js +88 -24
- package/src/scan-postcheck.js +24 -2
- package/test/platform-recovery.test.mjs +24 -6
package/package.json
CHANGED
package/src/init.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync,
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'fs';
|
|
2
2
|
import { join, resolve, dirname, basename } from 'path';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
4
|
import { checkbox, confirm, input } from '@inquirer/prompts';
|
|
@@ -108,6 +108,14 @@ async function doInstall(projectDir, tools, subprojects = [], specDir = null) {
|
|
|
108
108
|
// projectDir: 源码项目根目录(用于工具检测、指令注入、.gitignore)
|
|
109
109
|
const spec = specDir || join(projectDir, '.sillyspec');
|
|
110
110
|
|
|
111
|
+
// 外部 specDir 时清理旧版本残留的 cwd/.sillyspec/(防止源码污染)
|
|
112
|
+
const legacyDir = join(projectDir, '.sillyspec');
|
|
113
|
+
if (specDir && existsSync(legacyDir)) {
|
|
114
|
+
try { rmSync(legacyDir, { recursive: true, force: true }) } catch {}
|
|
115
|
+
if (!existsSync(legacyDir)) console.log('🧹 已清理旧版本残留的源码 .sillyspec/ 目录');
|
|
116
|
+
else console.error('⚠️ 清理残留 .sillyspec/ 失败');
|
|
117
|
+
}
|
|
118
|
+
|
|
111
119
|
// 创建基础目录
|
|
112
120
|
// spec/projects/ → 项目注册表
|
|
113
121
|
// spec/docs/<name>/ → 统一文档中心
|
package/src/run.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* 支持多变更并行:每个变更状态存储在 sillyspec.db 中。
|
|
6
6
|
*/
|
|
7
7
|
import { basename, join, resolve } from 'path'
|
|
8
|
-
import { existsSync, readdirSync, mkdirSync, writeFileSync, appendFileSync, readFileSync, statSync } from 'fs'
|
|
8
|
+
import { existsSync, readdirSync, mkdirSync, writeFileSync, appendFileSync, readFileSync, rmSync, statSync } from 'fs'
|
|
9
9
|
import { ProgressManager } from './progress.js'
|
|
10
10
|
|
|
11
11
|
/**
|
|
@@ -139,7 +139,9 @@ async function auditQuickCompletion(cwd, guard, options = {}) {
|
|
|
139
139
|
return result
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
async function triggerSync(cwd, changeName) {
|
|
142
|
+
async function triggerSync(cwd, changeName, platformOpts = {}) {
|
|
143
|
+
// 平台模式(SillyHub)走自己的回传链路,不走 CLI 内置 sync
|
|
144
|
+
if (platformOpts?.specRoot || platformOpts?.runtimeRoot) return
|
|
143
145
|
try {
|
|
144
146
|
if (changeName && !existsSync(join(cwd, '.sillyspec', 'changes', changeName))) return
|
|
145
147
|
const syncMod = await import('./sync.js')
|
|
@@ -154,12 +156,13 @@ async function triggerSync(cwd, changeName) {
|
|
|
154
156
|
* 审批检查辅助函数:execute 阶段启动前检查
|
|
155
157
|
* @returns {{ status: string, reason?: string } | null}
|
|
156
158
|
*/
|
|
157
|
-
async function checkApproval(cwd, changeName) {
|
|
159
|
+
async function checkApproval(cwd, changeName, platformOpts = {}) {
|
|
160
|
+
// 平台模式不需要 CLI 内置审批检查
|
|
161
|
+
if (platformOpts?.specRoot || platformOpts?.runtimeRoot) return null
|
|
158
162
|
try {
|
|
159
163
|
const syncMod = await import('./sync.js')
|
|
160
164
|
return await syncMod.checkApproval(changeName, cwd)
|
|
161
165
|
} catch (e) {
|
|
162
|
-
// sync.js 不存在或检查失败,静默跳过
|
|
163
166
|
return null
|
|
164
167
|
}
|
|
165
168
|
}
|
|
@@ -474,7 +477,7 @@ export async function runCommand(args, cwd, specDir = null) {
|
|
|
474
477
|
// 跨 --done 生命周期:从 metadata 文件恢复 platformOpts
|
|
475
478
|
// 首次 scan 时写入,所有后续调用(包括 run、--done、--skip)都读取
|
|
476
479
|
// 优先在 specDir 下查找,否则回退到 cwd/.sillyspec/.runtime/
|
|
477
|
-
|
|
480
|
+
let specRoot = platformOpts.specRoot || resolveSpecDir(cwd)
|
|
478
481
|
// 平台参数恢复策略:
|
|
479
482
|
// 1. 优先检查 cwd/.sillyspec-platform.json(轻量指针文件,不污染 .sillyspec 结构)
|
|
480
483
|
// 2. 然后检查 specRoot/.runtime/platform-scan.json(首次 scan 写入)
|
|
@@ -499,6 +502,8 @@ export async function runCommand(args, cwd, specDir = null) {
|
|
|
499
502
|
console.error(' 解决:重新运行首次 scan 并传入 --spec-root')
|
|
500
503
|
process.exit(1)
|
|
501
504
|
}
|
|
505
|
+
// 恢复成功:更新 specRoot(初始值可能是 cwd/.sillyspec,恢复后应为真实 specDir)
|
|
506
|
+
specRoot = platformOpts.specRoot || specRoot
|
|
502
507
|
} catch (e) {
|
|
503
508
|
console.error(`❌ 平台模式参数文件读取失败: ${platformOptsFile}`)
|
|
504
509
|
console.error(` 错误: ${e.message}`)
|
|
@@ -539,6 +544,14 @@ export async function runCommand(args, cwd, specDir = null) {
|
|
|
539
544
|
// runCommand 后续所有 .sillyspec/ 操作必须用 specBase
|
|
540
545
|
const specBase = platformOpts.specRoot || join(cwd, '.sillyspec')
|
|
541
546
|
|
|
547
|
+
// 平台模式:清理旧版本残留的 cwd/.sillyspec/(防止源码污染)
|
|
548
|
+
if (platformOpts.specRoot) {
|
|
549
|
+
const legacyDir = join(cwd, '.sillyspec')
|
|
550
|
+
if (existsSync(legacyDir)) {
|
|
551
|
+
try { rmSync(legacyDir, { recursive: true, force: true }) } catch {}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
542
555
|
// 解析 --output
|
|
543
556
|
let outputText = null
|
|
544
557
|
const outputIdx = flags.indexOf('--output')
|
|
@@ -592,6 +605,9 @@ export async function runCommand(args, cwd, specDir = null) {
|
|
|
592
605
|
}
|
|
593
606
|
|
|
594
607
|
const isAuxiliary = auxiliaryStages.includes(stageName)
|
|
608
|
+
// scan 元数据追踪(供 postcheck 使用)
|
|
609
|
+
let _scanProjectListParsed = undefined // undefined=未到达step2, true=解析成功, false=解析失败
|
|
610
|
+
let _scanManifestWritten = undefined // undefined=未尝试, true=成功, false=失败
|
|
595
611
|
|
|
596
612
|
const pm = new ProgressManager({ specDir: specRoot })
|
|
597
613
|
let progress = await pm.read(cwd, changeName)
|
|
@@ -650,7 +666,7 @@ export async function runCommand(args, cwd, specDir = null) {
|
|
|
650
666
|
const changed = await ensureStageSteps(progress, stageName, cwd, specRoot)
|
|
651
667
|
if (changed && effectiveChange) {
|
|
652
668
|
await pm._write(cwd, progress, effectiveChange)
|
|
653
|
-
triggerSync(cwd, effectiveChange)
|
|
669
|
+
triggerSync(cwd, effectiveChange, platformOpts)
|
|
654
670
|
progress = await pm.read(cwd, effectiveChange) || progress
|
|
655
671
|
}
|
|
656
672
|
|
|
@@ -700,7 +716,7 @@ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval =
|
|
|
700
716
|
|
|
701
717
|
// execute 阶段启动前检查审批
|
|
702
718
|
if (stageName === 'execute' && !skipApproval) {
|
|
703
|
-
const approval = await checkApproval(cwd, changeName)
|
|
719
|
+
const approval = await checkApproval(cwd, changeName, platformOpts)
|
|
704
720
|
if (approval) {
|
|
705
721
|
if (approval.status === 'rejected') {
|
|
706
722
|
console.error(`❌ 变更 ${changeName} 的执行已被拒绝:${approval.reason || '无原因'}`)
|
|
@@ -717,7 +733,7 @@ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval =
|
|
|
717
733
|
if (autoDetectChange(progress, cwd)) {
|
|
718
734
|
progress.lastActive = new Date().toLocaleString('zh-CN', { hour12: false })
|
|
719
735
|
await pm._write(cwd, progress, changeName)
|
|
720
|
-
triggerSync(cwd, changeName)
|
|
736
|
+
triggerSync(cwd, changeName, platformOpts)
|
|
721
737
|
}
|
|
722
738
|
|
|
723
739
|
const stageData = progress.stages[stageName]
|
|
@@ -731,7 +747,7 @@ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval =
|
|
|
731
747
|
progress.currentStage = stageName
|
|
732
748
|
progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
733
749
|
await pm._write(cwd, progress, changeName)
|
|
734
|
-
triggerSync(cwd, changeName)
|
|
750
|
+
triggerSync(cwd, changeName, platformOpts)
|
|
735
751
|
}
|
|
736
752
|
|
|
737
753
|
const steps = stageData.steps
|
|
@@ -747,7 +763,7 @@ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval =
|
|
|
747
763
|
stageData.startedAt = new Date().toLocaleString('zh-CN', { hour12: false })
|
|
748
764
|
stageData.completedAt = null
|
|
749
765
|
await pm._write(cwd, progress, changeName)
|
|
750
|
-
triggerSync(cwd, changeName)
|
|
766
|
+
triggerSync(cwd, changeName, platformOpts)
|
|
751
767
|
currentIdx = 0
|
|
752
768
|
console.log(`🔄 ${stageName} 阶段已自动重置,重新开始。\n`)
|
|
753
769
|
}
|
|
@@ -1007,15 +1023,36 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
1007
1023
|
// 解析项目列表:从 step 2 输出提取,或回退读取 projects/*.yaml
|
|
1008
1024
|
let projectNames = []
|
|
1009
1025
|
if (outputText) {
|
|
1010
|
-
//
|
|
1011
|
-
const
|
|
1012
|
-
if (
|
|
1013
|
-
projectNames =
|
|
1026
|
+
// 匹配方式 1: "1. project-name" 编号列表
|
|
1027
|
+
const numbered = outputText.match(/^\s*\d+\.\s+(\S+)/gm)
|
|
1028
|
+
if (numbered) {
|
|
1029
|
+
projectNames = numbered.map(m => m.replace(/^\s*\d+\.\s+/, '').replace(/[—\-:].*$/, '').trim())
|
|
1030
|
+
if (projectNames.length > 0) _scanProjectListParsed = true
|
|
1031
|
+
}
|
|
1032
|
+
// 匹配方式 2: 括号枚举 "子项目frontend/order-service/user-service" 或 "项目: a, b, c"
|
|
1033
|
+
if (projectNames.length === 0) {
|
|
1034
|
+
const parenMatch = outputText.match(/(?:子项目|项目)[\s::]*(\S+(?:[\/、,,]+\S+)*)/)
|
|
1035
|
+
if (parenMatch) {
|
|
1036
|
+
projectNames = parenMatch[1]
|
|
1037
|
+
.split(/[\/、,,]+/)
|
|
1038
|
+
.map(s => s.trim())
|
|
1039
|
+
.filter(Boolean)
|
|
1040
|
+
if (projectNames.length > 0) _scanProjectListParsed = true
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
// 匹配方式 3: 结构化 YAML block "scan_projects:\n - id: name"
|
|
1044
|
+
if (projectNames.length === 0) {
|
|
1045
|
+
const yamlMatch = outputText.match(/scan_projects:\s*\n((?:\s+-\s+id:\s+\S+\s*\n?)+)/)
|
|
1046
|
+
if (yamlMatch) {
|
|
1047
|
+
projectNames = [...yamlMatch[1].matchAll(/-\s+id:\s*(\S+)/g)].map(m => m[1])
|
|
1048
|
+
if (projectNames.length > 0) _scanProjectListParsed = true
|
|
1049
|
+
}
|
|
1014
1050
|
}
|
|
1015
1051
|
}
|
|
1016
1052
|
if (projectNames.length === 0) {
|
|
1017
1053
|
// 回退:读取所有已注册项目
|
|
1018
1054
|
console.warn('⚠️ 未能从 step 2 输出解析项目列表,回退扫描所有注册项目')
|
|
1055
|
+
_scanProjectListParsed = false
|
|
1019
1056
|
const projectsDir = join(specBase, 'projects')
|
|
1020
1057
|
if (existsSync(projectsDir)) {
|
|
1021
1058
|
projectNames = readdirSync(projectsDir)
|
|
@@ -1027,6 +1064,27 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
1027
1064
|
projectNames = ['sillyspec'] // 最终兜底
|
|
1028
1065
|
}
|
|
1029
1066
|
|
|
1067
|
+
// 自动注册未注册的子项目(确保 projects/*.yaml 存在,避免展开时 projectRoot 缺失)
|
|
1068
|
+
const projectsDir = join(specBase, 'projects')
|
|
1069
|
+
for (const pName of projectNames) {
|
|
1070
|
+
const projYaml = join(projectsDir, `${pName}.yaml`)
|
|
1071
|
+
if (!existsSync(projYaml)) {
|
|
1072
|
+
mkdirSync(projectsDir, { recursive: true })
|
|
1073
|
+
// 子项目路径推测:检查 cwd 下是否有同名目录
|
|
1074
|
+
const candidates = [
|
|
1075
|
+
join(cwd, pName), // cwd/frontend
|
|
1076
|
+
join(cwd, 'backend', pName), // cwd/backend/user-service
|
|
1077
|
+
join(cwd, 'packages', pName), // monorepo packages
|
|
1078
|
+
join(cwd, 'apps', pName), // monorepo apps
|
|
1079
|
+
join(cwd, 'services', pName), // monorepo services
|
|
1080
|
+
]
|
|
1081
|
+
const detected = candidates.find(c => existsSync(c))
|
|
1082
|
+
const regPath = detected || join(cwd, pName)
|
|
1083
|
+
writeFileSync(projYaml, `name: ${pName}\npath: ${regPath}\nstatus: active\n`)
|
|
1084
|
+
console.log(` 📝 自动注册子项目: ${pName} → ${regPath}`)
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1030
1088
|
// 保存到 runtime 供后续使用 + 防重复展开
|
|
1031
1089
|
const scanStatePath = join(specBase, '.runtime', 'scan-projects.json')
|
|
1032
1090
|
mkdirSync(join(specBase, '.runtime'), { recursive: true })
|
|
@@ -1091,7 +1149,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
1091
1149
|
stageData.completedAt = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
1092
1150
|
progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
1093
1151
|
await pm._write(cwd, progress, changeName)
|
|
1094
|
-
triggerSync(cwd, changeName)
|
|
1152
|
+
triggerSync(cwd, changeName, platformOpts)
|
|
1095
1153
|
|
|
1096
1154
|
// Append to user-inputs.md
|
|
1097
1155
|
if (outputText) {
|
|
@@ -1103,6 +1161,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
1103
1161
|
// 平台模式:scan 完成后生成 manifest.json + post-check
|
|
1104
1162
|
if (stageName === 'scan' && (platformOpts.specRoot || platformOpts.runtimeRoot)) {
|
|
1105
1163
|
try {
|
|
1164
|
+
_scanManifestWritten = false // 默认失败
|
|
1106
1165
|
const { mkdirSync, writeFileSync } = await import('fs')
|
|
1107
1166
|
const { join } = await import('path')
|
|
1108
1167
|
const { execSync } = await import('child_process')
|
|
@@ -1122,12 +1181,13 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
1122
1181
|
const manifestPath = join(manifestDir, 'manifest.json')
|
|
1123
1182
|
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n')
|
|
1124
1183
|
console.log(`📄 manifest.json 已写入: ${manifestPath}`)
|
|
1184
|
+
_scanManifestWritten = true
|
|
1125
1185
|
if (!sourceCommit) {
|
|
1126
1186
|
console.log(`⚠️ source_commit 无法获取(可能非 git 目录),已设为 null`)
|
|
1127
1187
|
}
|
|
1128
1188
|
// 清理平台参数临时文件
|
|
1129
1189
|
const { unlinkSync } = await import('fs')
|
|
1130
|
-
const platformOptsFile = join(
|
|
1190
|
+
const platformOptsFile = join(manifestDir, '.runtime', 'platform-scan.json')
|
|
1131
1191
|
try { unlinkSync(platformOptsFile) } catch {}
|
|
1132
1192
|
|
|
1133
1193
|
// CLI 层 post-check(替代旧的简单检查)
|
|
@@ -1136,6 +1196,10 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
1136
1196
|
cwd,
|
|
1137
1197
|
specDir: platformOpts.specRoot,
|
|
1138
1198
|
outputText,
|
|
1199
|
+
scanMeta: {
|
|
1200
|
+
projectListParsed: _scanProjectListParsed,
|
|
1201
|
+
manifestWritten: _scanManifestWritten,
|
|
1202
|
+
},
|
|
1139
1203
|
})
|
|
1140
1204
|
printScanPostCheckResult(postResult)
|
|
1141
1205
|
|
|
@@ -1269,7 +1333,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
1269
1333
|
|
|
1270
1334
|
progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
1271
1335
|
await pm._write(cwd, progress, changeName)
|
|
1272
|
-
triggerSync(cwd, changeName)
|
|
1336
|
+
triggerSync(cwd, changeName, platformOpts)
|
|
1273
1337
|
|
|
1274
1338
|
// Append to user-inputs.md
|
|
1275
1339
|
if (outputText) {
|
|
@@ -1389,7 +1453,7 @@ async function skipStep(pm, progress, stageName, cwd, changeName) {
|
|
|
1389
1453
|
steps[currentIdx].skippedAt = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
1390
1454
|
progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
1391
1455
|
await pm._write(cwd, progress, changeName)
|
|
1392
|
-
triggerSync(cwd, changeName)
|
|
1456
|
+
triggerSync(cwd, changeName, platformOpts)
|
|
1393
1457
|
|
|
1394
1458
|
console.log(`⏭️ Step ${currentIdx + 1}/${steps.length} 已跳过:${steps[currentIdx].name}`)
|
|
1395
1459
|
|
|
@@ -1452,7 +1516,7 @@ async function resetStage(pm, progress, stageName, cwd, changeName) {
|
|
|
1452
1516
|
}
|
|
1453
1517
|
progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
1454
1518
|
await pm._write(cwd, progress, changeName)
|
|
1455
|
-
triggerSync(cwd, changeName)
|
|
1519
|
+
triggerSync(cwd, changeName, platformOpts)
|
|
1456
1520
|
console.log(`🔄 ${stageName} 阶段已重置`)
|
|
1457
1521
|
}
|
|
1458
1522
|
|
|
@@ -1478,7 +1542,7 @@ async function runAutoMode(pm, progress, cwd, flags, changeName) {
|
|
|
1478
1542
|
const changed = await ensureStageSteps(progress, stage, cwd)
|
|
1479
1543
|
if (stageChanged || changed) {
|
|
1480
1544
|
await pm._write(cwd, progress, changeName)
|
|
1481
|
-
triggerSync(cwd, changeName)
|
|
1545
|
+
triggerSync(cwd, changeName, platformOpts)
|
|
1482
1546
|
}
|
|
1483
1547
|
progress = await pm.read(cwd, changeName)
|
|
1484
1548
|
return progress
|
|
@@ -1529,7 +1593,7 @@ async function runAutoMode(pm, progress, cwd, flags, changeName) {
|
|
|
1529
1593
|
}
|
|
1530
1594
|
// execute 阶段启动前检查审批
|
|
1531
1595
|
if (currentStage === 'execute' && !skipApproval) {
|
|
1532
|
-
const approval = await checkApproval(cwd, changeName)
|
|
1596
|
+
const approval = await checkApproval(cwd, changeName, platformOpts)
|
|
1533
1597
|
if (approval) {
|
|
1534
1598
|
if (approval.status === 'rejected') {
|
|
1535
1599
|
console.error(`❌ 变更 ${changeName} 的执行已被拒绝:${approval.reason || '无原因'}`)
|
|
@@ -1559,7 +1623,7 @@ async function runAutoMode(pm, progress, cwd, flags, changeName) {
|
|
|
1559
1623
|
const defSteps = await getStageSteps(currentStage, cwd, progress)
|
|
1560
1624
|
// execute 阶段启动前检查审批
|
|
1561
1625
|
if (currentStage === 'execute' && !skipApproval) {
|
|
1562
|
-
const approval = await checkApproval(cwd, changeName)
|
|
1626
|
+
const approval = await checkApproval(cwd, changeName, platformOpts)
|
|
1563
1627
|
if (approval) {
|
|
1564
1628
|
if (approval.status === 'rejected') {
|
|
1565
1629
|
console.error(`❌ 变更 ${changeName} 的执行已被拒绝:${approval.reason || '无原因'}`)
|
|
@@ -1592,7 +1656,7 @@ async function runAutoMode(pm, progress, cwd, flags, changeName) {
|
|
|
1592
1656
|
progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
1593
1657
|
await ensureStageSteps(progress, next, cwd)
|
|
1594
1658
|
await pm._write(cwd, progress, changeName)
|
|
1595
|
-
triggerSync(cwd, changeName)
|
|
1659
|
+
triggerSync(cwd, changeName, platformOpts)
|
|
1596
1660
|
progress = await pm.read(cwd, changeName)
|
|
1597
1661
|
|
|
1598
1662
|
console.log(`\n${currentStage} complete. Auto advanced to ${next}.`)
|
|
@@ -1601,7 +1665,7 @@ async function runAutoMode(pm, progress, cwd, flags, changeName) {
|
|
|
1601
1665
|
if (firstPending !== -1) {
|
|
1602
1666
|
// execute 阶段启动前检查审批
|
|
1603
1667
|
if (next === 'execute' && !skipApproval) {
|
|
1604
|
-
const approval = await checkApproval(cwd, changeName)
|
|
1668
|
+
const approval = await checkApproval(cwd, changeName, platformOpts)
|
|
1605
1669
|
if (approval) {
|
|
1606
1670
|
if (approval.status === 'rejected') {
|
|
1607
1671
|
console.error(`❌ 变更 ${changeName} 的执行已被拒绝:${approval.reason || '无原因'}`)
|
package/src/scan-postcheck.js
CHANGED
|
@@ -23,9 +23,13 @@ const REQUIRED_SCAN_DOCS = [
|
|
|
23
23
|
* @param {string} opts.cwd - 源码项目根目录 (source_root)
|
|
24
24
|
* @param {string} opts.specDir - 规范目录 (spec-root),null 时为非平台模式
|
|
25
25
|
* @param {string} [opts.outputText] - 最后一步(自检)的 AI 输出文本
|
|
26
|
+
* @param {object} [opts.scanMeta] - scan 元数据(由 runCommand 传入)
|
|
27
|
+
* @param {boolean} [opts.scanMeta.projectListParsed] - Step 2 项目列表是否成功解析
|
|
28
|
+
* @param {boolean} [opts.scanMeta.manifestWritten] - manifest.json 是否写入成功
|
|
29
|
+
* @param {number} [opts.scanMeta.projectCount] - 实际展开的项目数量
|
|
26
30
|
* @returns {{ status: 'success'|'completed_with_warnings'|'failed_post_check', checks: Array<{name, severity, detail}> }}
|
|
27
31
|
*/
|
|
28
|
-
export function runScanPostCheck({ cwd, specDir, outputText = '' }) {
|
|
32
|
+
export function runScanPostCheck({ cwd, specDir, outputText = '', scanMeta = {} } ) {
|
|
29
33
|
const isPlatform = !!specDir
|
|
30
34
|
const checks = []
|
|
31
35
|
|
|
@@ -146,7 +150,25 @@ export function runScanPostCheck({ cwd, specDir, outputText = '' }) {
|
|
|
146
150
|
}
|
|
147
151
|
}
|
|
148
152
|
|
|
149
|
-
// 6.
|
|
153
|
+
// 6. manifest 写入状态检查
|
|
154
|
+
if (scanMeta.manifestWritten === false) {
|
|
155
|
+
checks.push({
|
|
156
|
+
name: 'manifest_write_failed',
|
|
157
|
+
severity: 'failed',
|
|
158
|
+
detail: 'manifest.json 写入失败,平台无法消费 scan 结果'
|
|
159
|
+
})
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// 7. 项目列表解析状态检查
|
|
163
|
+
if (scanMeta.projectListParsed === false) {
|
|
164
|
+
checks.push({
|
|
165
|
+
name: 'project_list_parse_failed',
|
|
166
|
+
severity: 'warning',
|
|
167
|
+
detail: 'Step 2 项目列表解析失败,回退到注册项目列表,可能遗漏子项目'
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 8. 计算 finalStatus
|
|
150
172
|
const hasFailed = checks.some(c => c.severity === 'failed')
|
|
151
173
|
const hasWarning = checks.some(c => c.severity === 'warning')
|
|
152
174
|
|
|
@@ -73,8 +73,26 @@ console.log('\n=== Test 1: platform-scan.json 写入位置 ===')
|
|
|
73
73
|
clean(cwd, sd)
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
// ── Test 2:
|
|
77
|
-
console.log('\n=== Test 2:
|
|
76
|
+
// ── Test 2: 残留清理:旧版本创建的 cwd/.sillyspec 会被自动删除 ──
|
|
77
|
+
console.log('\n=== Test 2: 旧版本残留清理 ===')
|
|
78
|
+
{
|
|
79
|
+
const cwd = setup('t2'), sd = spec('t2')
|
|
80
|
+
// 模拟旧版本创建的残留
|
|
81
|
+
mkdirSync(join(cwd, '.sillyspec', '.runtime'), { recursive: true })
|
|
82
|
+
writeFileSync(join(cwd, '.sillyspec', '.runtime', 'old.db'), 'x')
|
|
83
|
+
mkdirSync(join(cwd, '.sillyspec', 'changes'), { recursive: true })
|
|
84
|
+
assert(existsSync(join(cwd, '.sillyspec')), `残留存在`)
|
|
85
|
+
// init 时应清理
|
|
86
|
+
run(`node "${binCLI}" init "${cwd}" --spec-dir "${sd}"`)
|
|
87
|
+
assert(!existsSync(join(cwd, '.sillyspec')), `init 清理了 cwd/.sillyspec/`)
|
|
88
|
+
// run 时也不应再创建
|
|
89
|
+
run(`node "${binCLI}" --dir "${cwd}" --spec-dir "${sd}" run scan --spec-root "${sd}" --runtime-root "${sd}/runtime" --workspace-id ws --scan-run-id sr 2>&1`)
|
|
90
|
+
assert(!existsSync(join(cwd, '.sillyspec')), `run 后 cwd/.sillyspec/ 仍不存在`)
|
|
91
|
+
clean(cwd, sd)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── Test 3: --done 不带 --spec-root 时恢复 ──
|
|
95
|
+
console.log('\n=== Test 3: --done 恢复平台参数 ===')
|
|
78
96
|
{
|
|
79
97
|
const cwd = setup('t2'), sd = spec('t2')
|
|
80
98
|
run(`node "${binCLI}" init "${cwd}" --spec-dir "${sd}"`)
|
|
@@ -89,7 +107,7 @@ console.log('\n=== Test 2: --done 恢复平台参数 ===')
|
|
|
89
107
|
// ── Test 3-6: stage-contract 路径(通过 runValidators) ──
|
|
90
108
|
const { runValidators } = await import(pathToFileURL(join(root, 'src', 'stage-contract.js')).href)
|
|
91
109
|
|
|
92
|
-
console.log('\n=== Test
|
|
110
|
+
console.log('\n=== Test 5: specDir 有文档 → 校验通过 ===')
|
|
93
111
|
{
|
|
94
112
|
const cwd = setup('t3'), sd = spec('t3')
|
|
95
113
|
const proj = basename(cwd)
|
|
@@ -100,7 +118,7 @@ console.log('\n=== Test 3: specDir 有文档 → 校验通过 ===')
|
|
|
100
118
|
clean(cwd, sd)
|
|
101
119
|
}
|
|
102
120
|
|
|
103
|
-
console.log('\n=== Test
|
|
121
|
+
console.log('\n=== Test 5: specDir 缺文档 → 校验失败,路径不含 .sillyspec ===')
|
|
104
122
|
{
|
|
105
123
|
const cwd = setup('t4'), sd = spec('t4')
|
|
106
124
|
const proj = basename(cwd)
|
|
@@ -114,7 +132,7 @@ console.log('\n=== Test 4: specDir 缺文档 → 校验失败,路径不含 .si
|
|
|
114
132
|
clean(cwd, sd)
|
|
115
133
|
}
|
|
116
134
|
|
|
117
|
-
console.log('\n=== Test
|
|
135
|
+
console.log('\n=== Test 7: 非平台模式有文档 → 校验通过 ===')
|
|
118
136
|
{
|
|
119
137
|
const cwd = setup('t5')
|
|
120
138
|
const proj = basename(cwd)
|
|
@@ -124,7 +142,7 @@ console.log('\n=== Test 5: 非平台模式有文档 → 校验通过 ===')
|
|
|
124
142
|
clean(cwd)
|
|
125
143
|
}
|
|
126
144
|
|
|
127
|
-
console.log('\n=== Test
|
|
145
|
+
console.log('\n=== Test 7: 非平台模式缺文档 → 路径含 .sillyspec ===')
|
|
128
146
|
{
|
|
129
147
|
const cwd = setup('t6')
|
|
130
148
|
const proj = basename(cwd)
|