sillyspec 3.17.0 → 3.17.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sillyspec",
3
- "version": "3.17.0",
3
+ "version": "3.17.1",
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,19 @@ 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
+ // platform-scan.json 搜索策略(多路径兼容):
479
+ // 平台模式首次 scan 写入 specRoot/.runtime/,但后续 --done 可能不带 --spec-root
480
+ // 需要在多个候选位置搜索
481
+ const candidatePaths = []
482
+ if (platformOpts.specRoot) {
483
+ candidatePaths.push(join(platformOpts.specRoot, '.runtime', 'platform-scan.json'))
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]
475
491
  let platformFileExists = existsSync(platformOptsFile)
476
492
  // 如果命令行没传 spec-root,尝试从持久化文件恢复
477
493
  if (!platformOpts.specRoot && !platformOpts.runtimeRoot) {
@@ -499,7 +515,8 @@ export async function runCommand(args, cwd, specDir = null) {
499
515
  }
500
516
  }
501
517
  }
502
- // 持久化 platformOpts(命令行传入或已恢复的都持久化)
518
+ // 持久化 platformOpts
519
+ // 在 specRoot/.runtime/ 写主文件,同时在 cwd/.sillyspec/.runtime/ 写恢复指针
503
520
  if (platformOpts.specRoot || platformOpts.runtimeRoot) {
504
521
  try {
505
522
  const { mkdirSync, writeFileSync } = await import('fs')
@@ -511,6 +528,16 @@ export async function runCommand(args, cwd, specDir = null) {
511
528
  scanRunId: platformOpts.scanRunId,
512
529
  savedAt: new Date().toISOString(),
513
530
  }, null, 2) + '\n')
531
+ // 恢复指针:在 cwd/.sillyspec/.runtime/ 也写一份,供后续 --done(不带 --spec-root)找到
532
+ const cwdRuntimeDir = join(cwd, '.sillyspec', '.runtime')
533
+ mkdirSync(cwdRuntimeDir, { recursive: true })
534
+ writeFileSync(join(cwdRuntimeDir, 'platform-scan.json'), JSON.stringify({
535
+ specRoot: platformOpts.specRoot,
536
+ runtimeRoot: platformOpts.runtimeRoot,
537
+ workspaceId: platformOpts.workspaceId,
538
+ scanRunId: platformOpts.scanRunId,
539
+ savedAt: new Date().toISOString(),
540
+ }, null, 2) + '\n')
514
541
  } catch {
515
542
  // 静默失败,不影响主流程
516
543
  }
@@ -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,139 @@
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 inCwd = join(cwd, '.sillyspec', '.runtime', 'platform-scan.json')
64
+ assert(existsSync(inSpecDir), `platform-scan.json 在 specDir/.runtime/`)
65
+ assert(existsSync(inCwd), `恢复指针也写入 cwd/.sillyspec/.runtime/`)
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
+ clean(cwd, sd)
72
+ }
73
+
74
+ // ── Test 2: --done 不带 --spec-root 时恢复 ──
75
+ console.log('\n=== Test 2: --done 恢复平台参数 ===')
76
+ {
77
+ const cwd = setup('t2'), sd = spec('t2')
78
+ run(`node "${binCLI}" init "${cwd}" --spec-dir "${sd}"`)
79
+ run(`node "${binCLI}" --dir "${cwd}" --spec-dir "${sd}" run scan --spec-root "${sd}" --runtime-root "${sd}/runtime" --workspace-id ws2 --scan-run-id sr2`)
80
+ // --done 不带任何平台参数
81
+ const output = run(`node "${binCLI}" --dir "${cwd}" run scan --done --change default --dir "${cwd}" --input "test" --output "test done" 2>&1`)
82
+ assert(output.includes('平台模式'), `恢复成功:包含平台模式指令`)
83
+ assert(output.includes(sd), `恢复成功:包含 specDir 路径`)
84
+ clean(cwd, sd)
85
+ }
86
+
87
+ // ── Test 3-6: stage-contract 路径(通过 runValidators) ──
88
+ const { runValidators } = await import(pathToFileURL(join(root, 'src', 'stage-contract.js')).href)
89
+
90
+ console.log('\n=== Test 3: specDir 有文档 → 校验通过 ===')
91
+ {
92
+ const cwd = setup('t3'), sd = spec('t3')
93
+ const proj = basename(cwd)
94
+ const scanDir = join(sd, 'docs', proj)
95
+ writeSpecDocs(scanDir)
96
+ const result = runValidators('scan', cwd, 'default', { projectName: proj, specRoot: sd })
97
+ assert(result.ok, `specDir 有文档: ok=${result.ok}, errors=${JSON.stringify(result.errors)}`)
98
+ clean(cwd, sd)
99
+ }
100
+
101
+ console.log('\n=== Test 4: specDir 缺文档 → 校验失败,路径不含 .sillyspec ===')
102
+ {
103
+ const cwd = setup('t4'), sd = spec('t4')
104
+ const proj = basename(cwd)
105
+ mkdirSync(join(sd, 'docs'), { recursive: true })
106
+ const result = runValidators('scan', cwd, 'default', { projectName: proj, specRoot: sd })
107
+ assert(!result.ok, `specDir 缺文档: ok=${result.ok}`)
108
+ assert(result.errors.length > 0, `有 errors`)
109
+ const errMsg = result.errors[0]
110
+ assert(!errMsg.includes('.sillyspec/docs'), `路径不含 .sillyspec: ${errMsg}`)
111
+ assert(errMsg.includes('/docs/'), `路径含 /docs/: ${errMsg}`)
112
+ clean(cwd, sd)
113
+ }
114
+
115
+ console.log('\n=== Test 5: 非平台模式有文档 → 校验通过 ===')
116
+ {
117
+ const cwd = setup('t5')
118
+ const proj = basename(cwd)
119
+ writeLocalDocs(cwd)
120
+ const result = runValidators('scan', cwd, 'default', { projectName: proj })
121
+ assert(result.ok, `非平台有文档: ok=${result.ok}`)
122
+ clean(cwd)
123
+ }
124
+
125
+ console.log('\n=== Test 6: 非平台模式缺文档 → 路径含 .sillyspec ===')
126
+ {
127
+ const cwd = setup('t6')
128
+ const proj = basename(cwd)
129
+ const result = runValidators('scan', cwd, 'default', { projectName: proj })
130
+ assert(!result.ok, `非平台缺文档: ok=${result.ok}`)
131
+ const errMsg = result.errors[0]
132
+ assert(errMsg.includes('.sillyspec/docs'), `路径含 .sillyspec/docs/: ${errMsg}`)
133
+ clean(cwd)
134
+ }
135
+
136
+ console.log(`\n${'='.repeat(50)}`)
137
+ console.log(`✅ 通过: ${passed} ❌ 失败: ${failed}`)
138
+ console.log(`${'='.repeat(50)}`)
139
+ process.exit(failed > 0 ? 1 : 0)