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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sillyspec",
3
- "version": "3.17.2",
3
+ "version": "3.17.4",
4
4
  "description": "SillySpec CLI — 流程状态机,让 AI 严格按步骤来",
5
5
  "icon": "logo.jpg",
6
6
  "homepage": "https://sillyspec.ppdmq.top/",
package/src/init.js CHANGED
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, statSync } from 'fs';
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
- const specRoot = platformOpts.specRoot || resolveSpecDir(cwd)
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
- // 匹配 "1. project-name" 格式
1011
- const matches = outputText.match(/^\s*\d+\.\s+(\S+)/gm)
1012
- if (matches) {
1013
- projectNames = matches.map(m => m.replace(/^\s*\d+\.\s+/, '').replace(/[—\-:].*$/, '').trim())
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(specRoot, '.runtime', 'platform-scan.json')
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 || '无原因'}`)
@@ -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. 计算 finalStatus
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: --done 不带 --spec-root 时恢复 ──
77
- console.log('\n=== Test 2: --done 恢复平台参数 ===')
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 3: specDir 有文档 → 校验通过 ===')
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 4: specDir 缺文档 → 校验失败,路径不含 .sillyspec ===')
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 5: 非平台模式有文档 → 校验通过 ===')
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 6: 非平台模式缺文档 → 路径含 .sillyspec ===')
145
+ console.log('\n=== Test 7: 非平台模式缺文档 → 路径含 .sillyspec ===')
128
146
  {
129
147
  const cwd = setup('t6')
130
148
  const proj = basename(cwd)