sillyspec 3.17.1 → 3.17.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/package.json +1 -1
- package/src/run.js +34 -37
- package/test/platform-recovery.test.mjs +4 -2
package/package.json
CHANGED
package/src/run.js
CHANGED
|
@@ -475,19 +475,12 @@ export async function runCommand(args, cwd, specDir = null) {
|
|
|
475
475
|
// 首次 scan 时写入,所有后续调用(包括 run、--done、--skip)都读取
|
|
476
476
|
// 优先在 specDir 下查找,否则回退到 cwd/.sillyspec/.runtime/
|
|
477
477
|
const specRoot = platformOpts.specRoot || resolveSpecDir(cwd)
|
|
478
|
-
//
|
|
479
|
-
//
|
|
480
|
-
//
|
|
481
|
-
const
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
}
|
|
485
|
-
if (resolvedSpecDir) {
|
|
486
|
-
candidatePaths.push(join(resolve(resolvedSpecDir), '.runtime', 'platform-scan.json'))
|
|
487
|
-
}
|
|
488
|
-
candidatePaths.push(join(specRoot, '.runtime', 'platform-scan.json')) // cwd/.sillyspec/.runtime/
|
|
489
|
-
|
|
490
|
-
let platformOptsFile = candidatePaths.find(p => existsSync(p)) || candidatePaths[0]
|
|
478
|
+
// 平台参数恢复策略:
|
|
479
|
+
// 1. 优先检查 cwd/.sillyspec-platform.json(轻量指针文件,不污染 .sillyspec 结构)
|
|
480
|
+
// 2. 然后检查 specRoot/.runtime/platform-scan.json(首次 scan 写入)
|
|
481
|
+
const platformPointer = join(cwd, '.sillyspec-platform.json')
|
|
482
|
+
const platformScanFile = join(specRoot, '.runtime', 'platform-scan.json')
|
|
483
|
+
let platformOptsFile = existsSync(platformPointer) ? platformPointer : platformScanFile
|
|
491
484
|
let platformFileExists = existsSync(platformOptsFile)
|
|
492
485
|
// 如果命令行没传 spec-root,尝试从持久化文件恢复
|
|
493
486
|
if (!platformOpts.specRoot && !platformOpts.runtimeRoot) {
|
|
@@ -528,10 +521,9 @@ export async function runCommand(args, cwd, specDir = null) {
|
|
|
528
521
|
scanRunId: platformOpts.scanRunId,
|
|
529
522
|
savedAt: new Date().toISOString(),
|
|
530
523
|
}, null, 2) + '\n')
|
|
531
|
-
// 恢复指针:在 cwd
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
writeFileSync(join(cwdRuntimeDir, 'platform-scan.json'), JSON.stringify({
|
|
524
|
+
// 恢复指针:在 cwd 下写 .sillyspec-platform.json(不在 .sillyspec 内,不污染源码结构)
|
|
525
|
+
// 供后续 --done(不带 --spec-root)找到 specDir
|
|
526
|
+
writeFileSync(join(cwd, '.sillyspec-platform.json'), JSON.stringify({
|
|
535
527
|
specRoot: platformOpts.specRoot,
|
|
536
528
|
runtimeRoot: platformOpts.runtimeRoot,
|
|
537
529
|
workspaceId: platformOpts.workspaceId,
|
|
@@ -543,6 +535,10 @@ export async function runCommand(args, cwd, specDir = null) {
|
|
|
543
535
|
}
|
|
544
536
|
}
|
|
545
537
|
|
|
538
|
+
// 统一规范基路径:平台模式用 specRoot,本地模式用 cwd/.sillyspec
|
|
539
|
+
// runCommand 后续所有 .sillyspec/ 操作必须用 specBase
|
|
540
|
+
const specBase = platformOpts.specRoot || join(cwd, '.sillyspec')
|
|
541
|
+
|
|
546
542
|
// 解析 --output
|
|
547
543
|
let outputText = null
|
|
548
544
|
const outputIdx = flags.indexOf('--output')
|
|
@@ -777,7 +773,7 @@ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval =
|
|
|
777
773
|
startedAt: new Date().toISOString(),
|
|
778
774
|
}
|
|
779
775
|
// 写入 quick-guard.json 供 worktree-guard hook 读取
|
|
780
|
-
const guardFile = join(
|
|
776
|
+
const guardFile = join(specBase, '.runtime', 'quick-guard.json')
|
|
781
777
|
writeFileSync(guardFile, JSON.stringify(progress.quickGuard, null, 2))
|
|
782
778
|
const parts = [`${baselineFiles.length} 个已有脏文件`]
|
|
783
779
|
if (allowedFiles.length > 0) parts.push(`${allowedFiles.length} 个 allowedFiles`)
|
|
@@ -802,8 +798,8 @@ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval =
|
|
|
802
798
|
}
|
|
803
799
|
}
|
|
804
800
|
|
|
805
|
-
function validateMetadata(cwd, stageName) {
|
|
806
|
-
const changesDir = join(
|
|
801
|
+
function validateMetadata(cwd, stageName, specBase) {
|
|
802
|
+
const changesDir = join(specBase, 'changes')
|
|
807
803
|
if (!existsSync(changesDir)) return
|
|
808
804
|
|
|
809
805
|
const cutoff = Date.now() - 10 * 60 * 1000
|
|
@@ -837,11 +833,11 @@ function validateMetadata(cwd, stageName) {
|
|
|
837
833
|
* 验证关键文件是否存在于正确的变更目录下
|
|
838
834
|
* 防止 AI 将文件写到错误的路径
|
|
839
835
|
*/
|
|
840
|
-
function validateFileLocations(cwd, stageName, progress, changeName) {
|
|
836
|
+
function validateFileLocations(cwd, stageName, progress, changeName, specBase) {
|
|
841
837
|
const effectiveChange = changeName || progress.currentChange
|
|
842
838
|
if (!effectiveChange) return
|
|
843
839
|
|
|
844
|
-
const changeDir = join(
|
|
840
|
+
const changeDir = join(specBase, 'changes', effectiveChange)
|
|
845
841
|
if (!existsSync(changeDir)) return
|
|
846
842
|
|
|
847
843
|
// 每个阶段完成后预期存在的文件
|
|
@@ -867,7 +863,7 @@ function validateFileLocations(cwd, stageName, progress, changeName) {
|
|
|
867
863
|
console.log(` 变更目录:${changeDir.replace(cwd + '/', '')}/`)
|
|
868
864
|
for (const f of missing) {
|
|
869
865
|
// 检查是否写到了错误的位置
|
|
870
|
-
const wrongPath = join(
|
|
866
|
+
const wrongPath = join(specBase, 'changes', 'change', effectiveChange, f)
|
|
871
867
|
if (existsSync(wrongPath)) {
|
|
872
868
|
console.log(` ❌ ${f} — 不存在,但发现了错误路径:${wrongPath.replace(cwd + '/', '')}`)
|
|
873
869
|
console.log(` 提示:应该写入 ${changeDir.replace(cwd + '/', '')}/${f}`)
|
|
@@ -887,7 +883,7 @@ async function archiveChangeDirectory(pm, cwd, progress) {
|
|
|
887
883
|
console.error('❌ 归档失败:未找到当前变更名(currentChange)')
|
|
888
884
|
process.exit(1)
|
|
889
885
|
}
|
|
890
|
-
const changesDir = join(
|
|
886
|
+
const changesDir = join(specBase, 'changes')
|
|
891
887
|
const archiveDir = join(changesDir, 'archive')
|
|
892
888
|
const srcDir = join(changesDir, archiveChangeName)
|
|
893
889
|
const date = new Date().toISOString().slice(0, 10)
|
|
@@ -915,6 +911,7 @@ async function archiveChangeDirectory(pm, cwd, progress) {
|
|
|
915
911
|
|
|
916
912
|
async function completeStep(pm, progress, stageName, cwd, outputText, inputText = null, options = {}) {
|
|
917
913
|
const { printNext = true, confirm = false, changeName, platformOpts = {} } = options
|
|
914
|
+
const specBase = platformOpts.specRoot || join(cwd, '.sillyspec')
|
|
918
915
|
const stageData = progress.stages[stageName]
|
|
919
916
|
if (!stageData || !stageData.steps) {
|
|
920
917
|
console.error(`❌ 阶段 ${stageName} 未初始化`)
|
|
@@ -938,7 +935,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
938
935
|
// 平台模式:artifact 写入 runtime-root,否则写 .sillyspec/.runtime/artifacts
|
|
939
936
|
const artifactBase = platformOpts?.runtimeRoot
|
|
940
937
|
? join(platformOpts.runtimeRoot, 'scan-runs', platformOpts.scanRunId || 'unknown')
|
|
941
|
-
: join(
|
|
938
|
+
: join(specBase, '.runtime', 'artifacts')
|
|
942
939
|
mkdirSync(artifactBase, { recursive: true })
|
|
943
940
|
const ts = new Date().toISOString().slice(0,19).replace(/[-T:]/g, '')
|
|
944
941
|
writeFileSync(join(artifactBase, `${changeName || 'unknown'}-${stageName}-step${currentIdx + 1}-${ts}.txt`), outputText)
|
|
@@ -1019,7 +1016,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
1019
1016
|
if (projectNames.length === 0) {
|
|
1020
1017
|
// 回退:读取所有已注册项目
|
|
1021
1018
|
console.warn('⚠️ 未能从 step 2 输出解析项目列表,回退扫描所有注册项目')
|
|
1022
|
-
const projectsDir = join(
|
|
1019
|
+
const projectsDir = join(specBase, 'projects')
|
|
1023
1020
|
if (existsSync(projectsDir)) {
|
|
1024
1021
|
projectNames = readdirSync(projectsDir)
|
|
1025
1022
|
.filter(f => f.endsWith('.yaml'))
|
|
@@ -1031,8 +1028,8 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
1031
1028
|
}
|
|
1032
1029
|
|
|
1033
1030
|
// 保存到 runtime 供后续使用 + 防重复展开
|
|
1034
|
-
const scanStatePath = join(
|
|
1035
|
-
mkdirSync(join(
|
|
1031
|
+
const scanStatePath = join(specBase, '.runtime', 'scan-projects.json')
|
|
1032
|
+
mkdirSync(join(specBase, '.runtime'), { recursive: true })
|
|
1036
1033
|
let scanState = { projects: projectNames, expanded: false }
|
|
1037
1034
|
if (existsSync(scanStatePath)) {
|
|
1038
1035
|
try { scanState = JSON.parse(readFileSync(scanStatePath, 'utf8')) } catch {}
|
|
@@ -1051,14 +1048,14 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
1051
1048
|
let insertPos = insertBase
|
|
1052
1049
|
for (const pName of projectNames) {
|
|
1053
1050
|
// 读取项目配置获取 projectRoot
|
|
1054
|
-
const projYaml = join(
|
|
1051
|
+
const projYaml = join(specBase, 'projects', `${pName}.yaml`)
|
|
1055
1052
|
let projectRoot = '.'
|
|
1056
1053
|
if (existsSync(projYaml)) {
|
|
1057
1054
|
const yamlContent = readFileSync(projYaml, 'utf8')
|
|
1058
1055
|
const pathMatch = yamlContent.match(/^path:\s*(.+)/m)
|
|
1059
1056
|
if (pathMatch) projectRoot = pathMatch[1].trim()
|
|
1060
1057
|
}
|
|
1061
|
-
const docOutputDir = `.sillyspec/docs/${pName}`
|
|
1058
|
+
const docOutputDir = platformOpts.specRoot ? `${specBase}/docs/${pName}` : `.sillyspec/docs/${pName}`
|
|
1062
1059
|
const contextPrefix = `\n---\n## 当前项目\n- **项目名**: ${pName}\n- **项目路径**: ${projectRoot}\n- **文档输出**: ${docOutputDir}\n\n⚠️ 本步骤只处理上面这个项目,不要处理其他项目。\n---\n\n`
|
|
1063
1060
|
|
|
1064
1061
|
for (const ppStep of perProjectSteps) {
|
|
@@ -1098,7 +1095,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
1098
1095
|
|
|
1099
1096
|
// Append to user-inputs.md
|
|
1100
1097
|
if (outputText) {
|
|
1101
|
-
const inputsPath = join(
|
|
1098
|
+
const inputsPath = join(specBase, '.runtime', 'user-inputs.md')
|
|
1102
1099
|
const entry = `\n## ${new Date().toLocaleString('zh-CN',{hour12:false})} | ${changeName || '?'} | ${stageName}: ${steps[currentIdx].name}\n${inputText ? "- 输入:" + inputText + "\n" : ""}- 输出:${outputText}\n`
|
|
1103
1100
|
appendFileSync(inputsPath, entry)
|
|
1104
1101
|
}
|
|
@@ -1109,7 +1106,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
1109
1106
|
const { mkdirSync, writeFileSync } = await import('fs')
|
|
1110
1107
|
const { join } = await import('path')
|
|
1111
1108
|
const { execSync } = await import('child_process')
|
|
1112
|
-
const manifestDir =
|
|
1109
|
+
const manifestDir = platformOpts.specRoot
|
|
1113
1110
|
mkdirSync(manifestDir, { recursive: true })
|
|
1114
1111
|
let sourceCommit = null
|
|
1115
1112
|
try {
|
|
@@ -1176,10 +1173,10 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
1176
1173
|
printScanPostCheckResult(postResult)
|
|
1177
1174
|
}
|
|
1178
1175
|
|
|
1179
|
-
validateMetadata(cwd, stageName)
|
|
1176
|
+
validateMetadata(cwd, stageName, specBase)
|
|
1180
1177
|
|
|
1181
1178
|
// 验证关键文件是否在正确的变更目录下
|
|
1182
|
-
validateFileLocations(cwd, stageName, progress, changeName)
|
|
1179
|
+
validateFileLocations(cwd, stageName, progress, changeName, specBase)
|
|
1183
1180
|
|
|
1184
1181
|
// 辅助阶段完成后重置步骤
|
|
1185
1182
|
const stageDef = stageRegistry[stageName]
|
|
@@ -1251,7 +1248,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
1251
1248
|
// 清理 quick-guard.json
|
|
1252
1249
|
try {
|
|
1253
1250
|
const { unlinkSync } = await import('fs')
|
|
1254
|
-
const guardFile = join(
|
|
1251
|
+
const guardFile = join(specBase, '.runtime', 'quick-guard.json')
|
|
1255
1252
|
unlinkSync(guardFile)
|
|
1256
1253
|
} catch {}
|
|
1257
1254
|
if (review.status === 'blocked') {
|
|
@@ -1276,7 +1273,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
1276
1273
|
|
|
1277
1274
|
// Append to user-inputs.md
|
|
1278
1275
|
if (outputText) {
|
|
1279
|
-
const inputsPath = join(
|
|
1276
|
+
const inputsPath = join(specBase, '.runtime', 'user-inputs.md')
|
|
1280
1277
|
const entry = `\n## ${new Date().toLocaleString('zh-CN',{hour12:false})} | ${changeName || '?'} | ${stageName}: ${steps[currentIdx].name}\n${inputText ? "- 输入:" + inputText + "\n" : ""}- 输出:${outputText}\n`
|
|
1281
1278
|
appendFileSync(inputsPath, entry)
|
|
1282
1279
|
}
|
|
@@ -1302,7 +1299,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
1302
1299
|
projectsToCheck = [currentProjectName]
|
|
1303
1300
|
} else {
|
|
1304
1301
|
// 兼容旧模式(未展开):检查所有项目
|
|
1305
|
-
const projectsDir = join(
|
|
1302
|
+
const projectsDir = join(specBase, 'projects')
|
|
1306
1303
|
const projectFiles = existsSync(projectsDir)
|
|
1307
1304
|
? readdirSync(projectsDir).filter(f => f.endsWith('.yaml'))
|
|
1308
1305
|
: []
|
|
@@ -60,14 +60,16 @@ console.log('\n=== Test 1: platform-scan.json 写入位置 ===')
|
|
|
60
60
|
run(`node "${binCLI}" --dir "${cwd}" --spec-dir "${sd}" run scan --spec-root "${sd}" --runtime-root "${sd}/runtime" --workspace-id ws1 --scan-run-id sr1`)
|
|
61
61
|
|
|
62
62
|
const inSpecDir = join(sd, '.runtime', 'platform-scan.json')
|
|
63
|
-
const
|
|
63
|
+
const pointerFile = join(cwd, '.sillyspec-platform.json')
|
|
64
64
|
assert(existsSync(inSpecDir), `platform-scan.json 在 specDir/.runtime/`)
|
|
65
|
-
assert(existsSync(
|
|
65
|
+
assert(existsSync(pointerFile), `恢复指针在 cwd/.sillyspec-platform.json(不在 .sillyspec 内)`)
|
|
66
66
|
|
|
67
67
|
const content = JSON.parse(readFileSync(inSpecDir, 'utf8'))
|
|
68
68
|
assert(content.specRoot === sd, `specRoot 指向 specDir`)
|
|
69
69
|
assert(content.workspaceId === 'ws1', `workspaceId 保存正确`)
|
|
70
70
|
assert(content.scanRunId === 'sr1', `scanRunId 保存正确`)
|
|
71
|
+
// 关键:cwd/.sillyspec/ 不应被创建
|
|
72
|
+
assert(!existsSync(join(cwd, '.sillyspec')), `cwd/.sillyspec/ 未被创建(源码零污染)`)
|
|
71
73
|
clean(cwd, sd)
|
|
72
74
|
}
|
|
73
75
|
|