sillyspec 3.17.0 → 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 +48 -24
- package/src/stage-contract.js +7 -4
- package/test/platform-recovery.test.mjs +141 -0
package/package.json
CHANGED
package/src/run.js
CHANGED
|
@@ -299,7 +299,9 @@ async function outputStep(stageName, stepIndex, steps, cwd, changeName, dbProjec
|
|
|
299
299
|
console.log(`project: ${projectName}`)
|
|
300
300
|
if (changeName) {
|
|
301
301
|
console.log(`change: ${changeName}`)
|
|
302
|
-
const
|
|
302
|
+
const isPlatform = platformOpts?.specRoot || platformOpts?.runtimeRoot
|
|
303
|
+
const changeDirBase = isPlatform ? platformOpts.specRoot : '.sillyspec'
|
|
304
|
+
const changeDir = join(changeDirBase, 'changes', changeName)
|
|
303
305
|
console.log(`changeDir: ${changeDir}`)
|
|
304
306
|
}
|
|
305
307
|
console.log(`---\n`)
|
|
@@ -418,7 +420,9 @@ async function outputStep(stageName, stepIndex, steps, cwd, changeName, dbProjec
|
|
|
418
420
|
}
|
|
419
421
|
// 路径安全规则:防止 AI 拼错变更目录
|
|
420
422
|
if (changeName) {
|
|
421
|
-
const
|
|
423
|
+
const isPlatform = platformOpts?.specRoot || platformOpts?.runtimeRoot
|
|
424
|
+
const changeDirBase = isPlatform ? platformOpts.specRoot : '.sillyspec'
|
|
425
|
+
const changeDir = join(changeDirBase, 'changes', changeName)
|
|
422
426
|
console.log(`- **文件路径规则:所有变更文件必须写入 \`${changeDir}/\` 目录下。不要自己拼接路径,直接使用 changeDir 值。示例:\`${changeDir}/proposal.md\`**`)
|
|
423
427
|
}
|
|
424
428
|
const changeFlag = changeName ? ` --change ${changeName}` : ''
|
|
@@ -471,7 +475,12 @@ export async function runCommand(args, cwd, specDir = null) {
|
|
|
471
475
|
// 首次 scan 时写入,所有后续调用(包括 run、--done、--skip)都读取
|
|
472
476
|
// 优先在 specDir 下查找,否则回退到 cwd/.sillyspec/.runtime/
|
|
473
477
|
const specRoot = platformOpts.specRoot || resolveSpecDir(cwd)
|
|
474
|
-
|
|
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
|
|
475
484
|
let platformFileExists = existsSync(platformOptsFile)
|
|
476
485
|
// 如果命令行没传 spec-root,尝试从持久化文件恢复
|
|
477
486
|
if (!platformOpts.specRoot && !platformOpts.runtimeRoot) {
|
|
@@ -499,7 +508,8 @@ export async function runCommand(args, cwd, specDir = null) {
|
|
|
499
508
|
}
|
|
500
509
|
}
|
|
501
510
|
}
|
|
502
|
-
// 持久化 platformOpts
|
|
511
|
+
// 持久化 platformOpts
|
|
512
|
+
// 在 specRoot/.runtime/ 写主文件,同时在 cwd/.sillyspec/.runtime/ 写恢复指针
|
|
503
513
|
if (platformOpts.specRoot || platformOpts.runtimeRoot) {
|
|
504
514
|
try {
|
|
505
515
|
const { mkdirSync, writeFileSync } = await import('fs')
|
|
@@ -511,11 +521,24 @@ export async function runCommand(args, cwd, specDir = null) {
|
|
|
511
521
|
scanRunId: platformOpts.scanRunId,
|
|
512
522
|
savedAt: new Date().toISOString(),
|
|
513
523
|
}, null, 2) + '\n')
|
|
524
|
+
// 恢复指针:在 cwd 下写 .sillyspec-platform.json(不在 .sillyspec 内,不污染源码结构)
|
|
525
|
+
// 供后续 --done(不带 --spec-root)找到 specDir
|
|
526
|
+
writeFileSync(join(cwd, '.sillyspec-platform.json'), JSON.stringify({
|
|
527
|
+
specRoot: platformOpts.specRoot,
|
|
528
|
+
runtimeRoot: platformOpts.runtimeRoot,
|
|
529
|
+
workspaceId: platformOpts.workspaceId,
|
|
530
|
+
scanRunId: platformOpts.scanRunId,
|
|
531
|
+
savedAt: new Date().toISOString(),
|
|
532
|
+
}, null, 2) + '\n')
|
|
514
533
|
} catch {
|
|
515
534
|
// 静默失败,不影响主流程
|
|
516
535
|
}
|
|
517
536
|
}
|
|
518
537
|
|
|
538
|
+
// 统一规范基路径:平台模式用 specRoot,本地模式用 cwd/.sillyspec
|
|
539
|
+
// runCommand 后续所有 .sillyspec/ 操作必须用 specBase
|
|
540
|
+
const specBase = platformOpts.specRoot || join(cwd, '.sillyspec')
|
|
541
|
+
|
|
519
542
|
// 解析 --output
|
|
520
543
|
let outputText = null
|
|
521
544
|
const outputIdx = flags.indexOf('--output')
|
|
@@ -750,7 +773,7 @@ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval =
|
|
|
750
773
|
startedAt: new Date().toISOString(),
|
|
751
774
|
}
|
|
752
775
|
// 写入 quick-guard.json 供 worktree-guard hook 读取
|
|
753
|
-
const guardFile = join(
|
|
776
|
+
const guardFile = join(specBase, '.runtime', 'quick-guard.json')
|
|
754
777
|
writeFileSync(guardFile, JSON.stringify(progress.quickGuard, null, 2))
|
|
755
778
|
const parts = [`${baselineFiles.length} 个已有脏文件`]
|
|
756
779
|
if (allowedFiles.length > 0) parts.push(`${allowedFiles.length} 个 allowedFiles`)
|
|
@@ -775,8 +798,8 @@ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval =
|
|
|
775
798
|
}
|
|
776
799
|
}
|
|
777
800
|
|
|
778
|
-
function validateMetadata(cwd, stageName) {
|
|
779
|
-
const changesDir = join(
|
|
801
|
+
function validateMetadata(cwd, stageName, specBase) {
|
|
802
|
+
const changesDir = join(specBase, 'changes')
|
|
780
803
|
if (!existsSync(changesDir)) return
|
|
781
804
|
|
|
782
805
|
const cutoff = Date.now() - 10 * 60 * 1000
|
|
@@ -810,11 +833,11 @@ function validateMetadata(cwd, stageName) {
|
|
|
810
833
|
* 验证关键文件是否存在于正确的变更目录下
|
|
811
834
|
* 防止 AI 将文件写到错误的路径
|
|
812
835
|
*/
|
|
813
|
-
function validateFileLocations(cwd, stageName, progress, changeName) {
|
|
836
|
+
function validateFileLocations(cwd, stageName, progress, changeName, specBase) {
|
|
814
837
|
const effectiveChange = changeName || progress.currentChange
|
|
815
838
|
if (!effectiveChange) return
|
|
816
839
|
|
|
817
|
-
const changeDir = join(
|
|
840
|
+
const changeDir = join(specBase, 'changes', effectiveChange)
|
|
818
841
|
if (!existsSync(changeDir)) return
|
|
819
842
|
|
|
820
843
|
// 每个阶段完成后预期存在的文件
|
|
@@ -840,7 +863,7 @@ function validateFileLocations(cwd, stageName, progress, changeName) {
|
|
|
840
863
|
console.log(` 变更目录:${changeDir.replace(cwd + '/', '')}/`)
|
|
841
864
|
for (const f of missing) {
|
|
842
865
|
// 检查是否写到了错误的位置
|
|
843
|
-
const wrongPath = join(
|
|
866
|
+
const wrongPath = join(specBase, 'changes', 'change', effectiveChange, f)
|
|
844
867
|
if (existsSync(wrongPath)) {
|
|
845
868
|
console.log(` ❌ ${f} — 不存在,但发现了错误路径:${wrongPath.replace(cwd + '/', '')}`)
|
|
846
869
|
console.log(` 提示:应该写入 ${changeDir.replace(cwd + '/', '')}/${f}`)
|
|
@@ -860,7 +883,7 @@ async function archiveChangeDirectory(pm, cwd, progress) {
|
|
|
860
883
|
console.error('❌ 归档失败:未找到当前变更名(currentChange)')
|
|
861
884
|
process.exit(1)
|
|
862
885
|
}
|
|
863
|
-
const changesDir = join(
|
|
886
|
+
const changesDir = join(specBase, 'changes')
|
|
864
887
|
const archiveDir = join(changesDir, 'archive')
|
|
865
888
|
const srcDir = join(changesDir, archiveChangeName)
|
|
866
889
|
const date = new Date().toISOString().slice(0, 10)
|
|
@@ -888,6 +911,7 @@ async function archiveChangeDirectory(pm, cwd, progress) {
|
|
|
888
911
|
|
|
889
912
|
async function completeStep(pm, progress, stageName, cwd, outputText, inputText = null, options = {}) {
|
|
890
913
|
const { printNext = true, confirm = false, changeName, platformOpts = {} } = options
|
|
914
|
+
const specBase = platformOpts.specRoot || join(cwd, '.sillyspec')
|
|
891
915
|
const stageData = progress.stages[stageName]
|
|
892
916
|
if (!stageData || !stageData.steps) {
|
|
893
917
|
console.error(`❌ 阶段 ${stageName} 未初始化`)
|
|
@@ -911,7 +935,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
911
935
|
// 平台模式:artifact 写入 runtime-root,否则写 .sillyspec/.runtime/artifacts
|
|
912
936
|
const artifactBase = platformOpts?.runtimeRoot
|
|
913
937
|
? join(platformOpts.runtimeRoot, 'scan-runs', platformOpts.scanRunId || 'unknown')
|
|
914
|
-
: join(
|
|
938
|
+
: join(specBase, '.runtime', 'artifacts')
|
|
915
939
|
mkdirSync(artifactBase, { recursive: true })
|
|
916
940
|
const ts = new Date().toISOString().slice(0,19).replace(/[-T:]/g, '')
|
|
917
941
|
writeFileSync(join(artifactBase, `${changeName || 'unknown'}-${stageName}-step${currentIdx + 1}-${ts}.txt`), outputText)
|
|
@@ -992,7 +1016,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
992
1016
|
if (projectNames.length === 0) {
|
|
993
1017
|
// 回退:读取所有已注册项目
|
|
994
1018
|
console.warn('⚠️ 未能从 step 2 输出解析项目列表,回退扫描所有注册项目')
|
|
995
|
-
const projectsDir = join(
|
|
1019
|
+
const projectsDir = join(specBase, 'projects')
|
|
996
1020
|
if (existsSync(projectsDir)) {
|
|
997
1021
|
projectNames = readdirSync(projectsDir)
|
|
998
1022
|
.filter(f => f.endsWith('.yaml'))
|
|
@@ -1004,8 +1028,8 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
1004
1028
|
}
|
|
1005
1029
|
|
|
1006
1030
|
// 保存到 runtime 供后续使用 + 防重复展开
|
|
1007
|
-
const scanStatePath = join(
|
|
1008
|
-
mkdirSync(join(
|
|
1031
|
+
const scanStatePath = join(specBase, '.runtime', 'scan-projects.json')
|
|
1032
|
+
mkdirSync(join(specBase, '.runtime'), { recursive: true })
|
|
1009
1033
|
let scanState = { projects: projectNames, expanded: false }
|
|
1010
1034
|
if (existsSync(scanStatePath)) {
|
|
1011
1035
|
try { scanState = JSON.parse(readFileSync(scanStatePath, 'utf8')) } catch {}
|
|
@@ -1024,14 +1048,14 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
1024
1048
|
let insertPos = insertBase
|
|
1025
1049
|
for (const pName of projectNames) {
|
|
1026
1050
|
// 读取项目配置获取 projectRoot
|
|
1027
|
-
const projYaml = join(
|
|
1051
|
+
const projYaml = join(specBase, 'projects', `${pName}.yaml`)
|
|
1028
1052
|
let projectRoot = '.'
|
|
1029
1053
|
if (existsSync(projYaml)) {
|
|
1030
1054
|
const yamlContent = readFileSync(projYaml, 'utf8')
|
|
1031
1055
|
const pathMatch = yamlContent.match(/^path:\s*(.+)/m)
|
|
1032
1056
|
if (pathMatch) projectRoot = pathMatch[1].trim()
|
|
1033
1057
|
}
|
|
1034
|
-
const docOutputDir = `.sillyspec/docs/${pName}`
|
|
1058
|
+
const docOutputDir = platformOpts.specRoot ? `${specBase}/docs/${pName}` : `.sillyspec/docs/${pName}`
|
|
1035
1059
|
const contextPrefix = `\n---\n## 当前项目\n- **项目名**: ${pName}\n- **项目路径**: ${projectRoot}\n- **文档输出**: ${docOutputDir}\n\n⚠️ 本步骤只处理上面这个项目,不要处理其他项目。\n---\n\n`
|
|
1036
1060
|
|
|
1037
1061
|
for (const ppStep of perProjectSteps) {
|
|
@@ -1071,7 +1095,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
1071
1095
|
|
|
1072
1096
|
// Append to user-inputs.md
|
|
1073
1097
|
if (outputText) {
|
|
1074
|
-
const inputsPath = join(
|
|
1098
|
+
const inputsPath = join(specBase, '.runtime', 'user-inputs.md')
|
|
1075
1099
|
const entry = `\n## ${new Date().toLocaleString('zh-CN',{hour12:false})} | ${changeName || '?'} | ${stageName}: ${steps[currentIdx].name}\n${inputText ? "- 输入:" + inputText + "\n" : ""}- 输出:${outputText}\n`
|
|
1076
1100
|
appendFileSync(inputsPath, entry)
|
|
1077
1101
|
}
|
|
@@ -1082,7 +1106,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
1082
1106
|
const { mkdirSync, writeFileSync } = await import('fs')
|
|
1083
1107
|
const { join } = await import('path')
|
|
1084
1108
|
const { execSync } = await import('child_process')
|
|
1085
|
-
const manifestDir =
|
|
1109
|
+
const manifestDir = platformOpts.specRoot
|
|
1086
1110
|
mkdirSync(manifestDir, { recursive: true })
|
|
1087
1111
|
let sourceCommit = null
|
|
1088
1112
|
try {
|
|
@@ -1149,10 +1173,10 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
1149
1173
|
printScanPostCheckResult(postResult)
|
|
1150
1174
|
}
|
|
1151
1175
|
|
|
1152
|
-
validateMetadata(cwd, stageName)
|
|
1176
|
+
validateMetadata(cwd, stageName, specBase)
|
|
1153
1177
|
|
|
1154
1178
|
// 验证关键文件是否在正确的变更目录下
|
|
1155
|
-
validateFileLocations(cwd, stageName, progress, changeName)
|
|
1179
|
+
validateFileLocations(cwd, stageName, progress, changeName, specBase)
|
|
1156
1180
|
|
|
1157
1181
|
// 辅助阶段完成后重置步骤
|
|
1158
1182
|
const stageDef = stageRegistry[stageName]
|
|
@@ -1224,7 +1248,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
1224
1248
|
// 清理 quick-guard.json
|
|
1225
1249
|
try {
|
|
1226
1250
|
const { unlinkSync } = await import('fs')
|
|
1227
|
-
const guardFile = join(
|
|
1251
|
+
const guardFile = join(specBase, '.runtime', 'quick-guard.json')
|
|
1228
1252
|
unlinkSync(guardFile)
|
|
1229
1253
|
} catch {}
|
|
1230
1254
|
if (review.status === 'blocked') {
|
|
@@ -1249,7 +1273,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
1249
1273
|
|
|
1250
1274
|
// Append to user-inputs.md
|
|
1251
1275
|
if (outputText) {
|
|
1252
|
-
const inputsPath = join(
|
|
1276
|
+
const inputsPath = join(specBase, '.runtime', 'user-inputs.md')
|
|
1253
1277
|
const entry = `\n## ${new Date().toLocaleString('zh-CN',{hour12:false})} | ${changeName || '?'} | ${stageName}: ${steps[currentIdx].name}\n${inputText ? "- 输入:" + inputText + "\n" : ""}- 输出:${outputText}\n`
|
|
1254
1278
|
appendFileSync(inputsPath, entry)
|
|
1255
1279
|
}
|
|
@@ -1275,7 +1299,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
1275
1299
|
projectsToCheck = [currentProjectName]
|
|
1276
1300
|
} else {
|
|
1277
1301
|
// 兼容旧模式(未展开):检查所有项目
|
|
1278
|
-
const projectsDir = join(
|
|
1302
|
+
const projectsDir = join(specBase, 'projects')
|
|
1279
1303
|
const projectFiles = existsSync(projectsDir)
|
|
1280
1304
|
? readdirSync(projectsDir).filter(f => f.endsWith('.yaml'))
|
|
1281
1305
|
: []
|
package/src/stage-contract.js
CHANGED
|
@@ -33,9 +33,12 @@ function validateScanOutputs(cwd, changeName, context = {}) {
|
|
|
33
33
|
const { projectName, specRoot } = context
|
|
34
34
|
// 平台模式使用 specRoot,本地模式使用 cwd
|
|
35
35
|
const base = specRoot || cwd
|
|
36
|
+
// 如果 base 已经是 specDir(有 docs/ 子目录),直接用 base/docs/
|
|
37
|
+
// 否则按传统模式拼接 .sillyspec/docs/
|
|
38
|
+
const isSpecDir = existsSync(join(base, 'docs'))
|
|
36
39
|
const docsRoot = projectName
|
|
37
|
-
? join(base, '
|
|
38
|
-
: join(base, '
|
|
40
|
+
? join(base, isSpecDir ? 'docs' : '.sillyspec/docs', projectName, 'scan')
|
|
41
|
+
: join(base, isSpecDir ? 'docs' : '.sillyspec/docs', 'scan')
|
|
39
42
|
|
|
40
43
|
const requiredDocs = [
|
|
41
44
|
'ARCHITECTURE.md',
|
|
@@ -58,8 +61,8 @@ function validateScanOutputs(cwd, changeName, context = {}) {
|
|
|
58
61
|
|
|
59
62
|
// 检查 modules 目录
|
|
60
63
|
const modulesRoot = projectName
|
|
61
|
-
? join(base, '
|
|
62
|
-
: join(base, '
|
|
64
|
+
? join(base, isSpecDir ? 'docs' : '.sillyspec/docs', projectName, 'modules')
|
|
65
|
+
: join(base, isSpecDir ? 'docs' : '.sillyspec/docs', 'modules')
|
|
63
66
|
if (!existsSync(modulesRoot)) {
|
|
64
67
|
warnings.push('modules 目录不存在')
|
|
65
68
|
} else {
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* platform-recovery.test.mjs — 平台模式参数恢复 + stage-contract 路径测试
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { join, resolve, dirname, basename } from 'path'
|
|
6
|
+
import { existsSync, mkdirSync, writeFileSync, rmSync, readFileSync } from 'fs'
|
|
7
|
+
import { fileURLToPath, pathToFileURL } from 'url'
|
|
8
|
+
import { execSync } from 'child_process'
|
|
9
|
+
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
11
|
+
const __dirname = dirname(__filename)
|
|
12
|
+
const root = resolve(__dirname, '..')
|
|
13
|
+
const binCLI = join(root, 'bin', 'sillyspec.js')
|
|
14
|
+
|
|
15
|
+
let passed = 0, failed = 0
|
|
16
|
+
|
|
17
|
+
function assert(cond, msg) {
|
|
18
|
+
if (cond) { console.log(` ✅ PASS: ${msg}`); passed++ }
|
|
19
|
+
else { console.log(` ❌ FAIL: ${msg}`); failed++ }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const P = 'recover'
|
|
23
|
+
function setup(name) {
|
|
24
|
+
const d = join('/tmp', `${P}-${name}`)
|
|
25
|
+
mkdirSync(d, { recursive: true })
|
|
26
|
+
return d
|
|
27
|
+
}
|
|
28
|
+
function spec(name) {
|
|
29
|
+
const d = join('/tmp', `${P}-${name}-spec`)
|
|
30
|
+
mkdirSync(d, { recursive: true })
|
|
31
|
+
return d
|
|
32
|
+
}
|
|
33
|
+
function clean(...dirs) { for (const d of dirs) try { rmSync(d, { recursive: true, force: true }) } catch {} }
|
|
34
|
+
|
|
35
|
+
function run(cmd) {
|
|
36
|
+
return execSync(cmd, { encoding: 'utf8', timeout: 15000, stdio: ['pipe', 'pipe', 'pipe'] })
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const DOCS = ['ARCHITECTURE.md','CONVENTIONS.md','STRUCTURE.md','INTEGRATIONS.md','TESTING.md','CONCERNS.md','PROJECT.md']
|
|
40
|
+
function writeSpecDocs(dir) {
|
|
41
|
+
for (const d of DOCS) {
|
|
42
|
+
const p = join(dir, 'scan', d)
|
|
43
|
+
mkdirSync(dirname(p), { recursive: true })
|
|
44
|
+
writeFileSync(p, 'author: bot\ncreated_at: now\n# doc\n')
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function writeLocalDocs(cwd) {
|
|
48
|
+
for (const d of DOCS) {
|
|
49
|
+
const p = join(cwd, '.sillyspec', 'docs', basename(cwd), 'scan', d)
|
|
50
|
+
mkdirSync(dirname(p), { recursive: true })
|
|
51
|
+
writeFileSync(p, 'author: bot\ncreated_at: now\n# doc\n')
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ── Test 1: platform-scan.json 写入位置 ──
|
|
56
|
+
console.log('\n=== Test 1: platform-scan.json 写入位置 ===')
|
|
57
|
+
{
|
|
58
|
+
const cwd = setup('t1'), sd = spec('t1')
|
|
59
|
+
run(`node "${binCLI}" init "${cwd}" --spec-dir "${sd}"`)
|
|
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
|
+
|
|
62
|
+
const inSpecDir = join(sd, '.runtime', 'platform-scan.json')
|
|
63
|
+
const pointerFile = join(cwd, '.sillyspec-platform.json')
|
|
64
|
+
assert(existsSync(inSpecDir), `platform-scan.json 在 specDir/.runtime/`)
|
|
65
|
+
assert(existsSync(pointerFile), `恢复指针在 cwd/.sillyspec-platform.json(不在 .sillyspec 内)`)
|
|
66
|
+
|
|
67
|
+
const content = JSON.parse(readFileSync(inSpecDir, 'utf8'))
|
|
68
|
+
assert(content.specRoot === sd, `specRoot 指向 specDir`)
|
|
69
|
+
assert(content.workspaceId === 'ws1', `workspaceId 保存正确`)
|
|
70
|
+
assert(content.scanRunId === 'sr1', `scanRunId 保存正确`)
|
|
71
|
+
// 关键:cwd/.sillyspec/ 不应被创建
|
|
72
|
+
assert(!existsSync(join(cwd, '.sillyspec')), `cwd/.sillyspec/ 未被创建(源码零污染)`)
|
|
73
|
+
clean(cwd, sd)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ── Test 2: --done 不带 --spec-root 时恢复 ──
|
|
77
|
+
console.log('\n=== Test 2: --done 恢复平台参数 ===')
|
|
78
|
+
{
|
|
79
|
+
const cwd = setup('t2'), sd = spec('t2')
|
|
80
|
+
run(`node "${binCLI}" init "${cwd}" --spec-dir "${sd}"`)
|
|
81
|
+
run(`node "${binCLI}" --dir "${cwd}" --spec-dir "${sd}" run scan --spec-root "${sd}" --runtime-root "${sd}/runtime" --workspace-id ws2 --scan-run-id sr2`)
|
|
82
|
+
// --done 不带任何平台参数
|
|
83
|
+
const output = run(`node "${binCLI}" --dir "${cwd}" run scan --done --change default --dir "${cwd}" --input "test" --output "test done" 2>&1`)
|
|
84
|
+
assert(output.includes('平台模式'), `恢复成功:包含平台模式指令`)
|
|
85
|
+
assert(output.includes(sd), `恢复成功:包含 specDir 路径`)
|
|
86
|
+
clean(cwd, sd)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── Test 3-6: stage-contract 路径(通过 runValidators) ──
|
|
90
|
+
const { runValidators } = await import(pathToFileURL(join(root, 'src', 'stage-contract.js')).href)
|
|
91
|
+
|
|
92
|
+
console.log('\n=== Test 3: specDir 有文档 → 校验通过 ===')
|
|
93
|
+
{
|
|
94
|
+
const cwd = setup('t3'), sd = spec('t3')
|
|
95
|
+
const proj = basename(cwd)
|
|
96
|
+
const scanDir = join(sd, 'docs', proj)
|
|
97
|
+
writeSpecDocs(scanDir)
|
|
98
|
+
const result = runValidators('scan', cwd, 'default', { projectName: proj, specRoot: sd })
|
|
99
|
+
assert(result.ok, `specDir 有文档: ok=${result.ok}, errors=${JSON.stringify(result.errors)}`)
|
|
100
|
+
clean(cwd, sd)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
console.log('\n=== Test 4: specDir 缺文档 → 校验失败,路径不含 .sillyspec ===')
|
|
104
|
+
{
|
|
105
|
+
const cwd = setup('t4'), sd = spec('t4')
|
|
106
|
+
const proj = basename(cwd)
|
|
107
|
+
mkdirSync(join(sd, 'docs'), { recursive: true })
|
|
108
|
+
const result = runValidators('scan', cwd, 'default', { projectName: proj, specRoot: sd })
|
|
109
|
+
assert(!result.ok, `specDir 缺文档: ok=${result.ok}`)
|
|
110
|
+
assert(result.errors.length > 0, `有 errors`)
|
|
111
|
+
const errMsg = result.errors[0]
|
|
112
|
+
assert(!errMsg.includes('.sillyspec/docs'), `路径不含 .sillyspec: ${errMsg}`)
|
|
113
|
+
assert(errMsg.includes('/docs/'), `路径含 /docs/: ${errMsg}`)
|
|
114
|
+
clean(cwd, sd)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
console.log('\n=== Test 5: 非平台模式有文档 → 校验通过 ===')
|
|
118
|
+
{
|
|
119
|
+
const cwd = setup('t5')
|
|
120
|
+
const proj = basename(cwd)
|
|
121
|
+
writeLocalDocs(cwd)
|
|
122
|
+
const result = runValidators('scan', cwd, 'default', { projectName: proj })
|
|
123
|
+
assert(result.ok, `非平台有文档: ok=${result.ok}`)
|
|
124
|
+
clean(cwd)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
console.log('\n=== Test 6: 非平台模式缺文档 → 路径含 .sillyspec ===')
|
|
128
|
+
{
|
|
129
|
+
const cwd = setup('t6')
|
|
130
|
+
const proj = basename(cwd)
|
|
131
|
+
const result = runValidators('scan', cwd, 'default', { projectName: proj })
|
|
132
|
+
assert(!result.ok, `非平台缺文档: ok=${result.ok}`)
|
|
133
|
+
const errMsg = result.errors[0]
|
|
134
|
+
assert(errMsg.includes('.sillyspec/docs'), `路径含 .sillyspec/docs/: ${errMsg}`)
|
|
135
|
+
clean(cwd)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
console.log(`\n${'='.repeat(50)}`)
|
|
139
|
+
console.log(`✅ 通过: ${passed} ❌ 失败: ${failed}`)
|
|
140
|
+
console.log(`${'='.repeat(50)}`)
|
|
141
|
+
process.exit(failed > 0 ? 1 : 0)
|