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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sillyspec",
3
- "version": "3.17.0",
3
+ "version": "3.17.2",
4
4
  "description": "SillySpec CLI — 流程状态机,让 AI 严格按步骤来",
5
5
  "icon": "logo.jpg",
6
6
  "homepage": "https://sillyspec.ppdmq.top/",
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 changeDir = join('.sillyspec', 'changes', changeName)
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 changeDir = join('.sillyspec', 'changes', changeName)
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
- const platformOptsFile = join(specRoot, '.runtime', 'platform-scan.json')
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(cwd, '.sillyspec', '.runtime', 'quick-guard.json')
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(cwd, '.sillyspec', 'changes')
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(cwd, '.sillyspec', 'changes', effectiveChange)
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(cwd, '.sillyspec', 'changes', 'change', effectiveChange, f)
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(cwd, '.sillyspec', 'changes')
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(cwd, '.sillyspec', '.runtime', 'artifacts')
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(cwd, '.sillyspec', 'projects')
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(cwd, '.sillyspec', '.runtime', 'scan-projects.json')
1008
- mkdirSync(join(cwd, '.sillyspec', '.runtime'), { recursive: true })
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(cwd, '.sillyspec', 'projects', `${pName}.yaml`)
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(cwd, '.sillyspec', '.runtime', 'user-inputs.md')
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 = join(platformOpts.specRoot, '.sillyspec')
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(cwd, '.sillyspec', '.runtime', 'quick-guard.json')
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(cwd, '.sillyspec', '.runtime', 'user-inputs.md')
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(cwd, '.sillyspec', 'projects')
1302
+ const projectsDir = join(specBase, 'projects')
1279
1303
  const projectFiles = existsSync(projectsDir)
1280
1304
  ? readdirSync(projectsDir).filter(f => f.endsWith('.yaml'))
1281
1305
  : []
@@ -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, '.sillyspec', 'docs', projectName, 'scan')
38
- : join(base, '.sillyspec', 'docs', 'scan')
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, '.sillyspec', 'docs', projectName, 'modules')
62
- : join(base, '.sillyspec', 'docs', 'modules')
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)