sillyspec 3.17.3 → 3.17.5

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.3",
3
+ "version": "3.17.5",
4
4
  "description": "SillySpec CLI — 流程状态机,让 AI 严格按步骤来",
5
5
  "icon": "logo.jpg",
6
6
  "homepage": "https://sillyspec.ppdmq.top/",
package/src/run.js CHANGED
@@ -6,6 +6,8 @@
6
6
  */
7
7
  import { basename, join, resolve } from 'path'
8
8
  import { existsSync, readdirSync, mkdirSync, writeFileSync, appendFileSync, readFileSync, rmSync, statSync } from 'fs'
9
+ import { createRequire } from 'module'
10
+ const require = createRequire(import.meta.url)
9
11
  import { ProgressManager } from './progress.js'
10
12
 
11
13
  /**
@@ -392,6 +394,23 @@ async function outputStep(stageName, stepIndex, steps, cwd, changeName, dbProjec
392
394
  platformDirectives.push(`workspace_id: ${platformOpts.workspaceId}`)
393
395
  }
394
396
  promptText = platformDirectives.join('\n') + '\n\n' + promptText
397
+ }
398
+
399
+ // 注入 scanProfile 硬约束指令
400
+ if (stageName === 'scan' && platformOpts?.scanProfile) {
401
+ const sp = platformOpts.scanProfile
402
+ const profileDirectives = []
403
+ profileDirectives.push(`## 📊 Scan Profile: ${sp.mode} (${sp.reason})`)
404
+ if (sp.maxAgentCalls === 0) {
405
+ profileDirectives.push(`**⛔ 严禁使用子代理(Agent/Task 工具)。** 必须在本 turn 内完成所有工作。`)
406
+ } else if (sp.maxAgentCalls > 0) {
407
+ profileDirectives.push(`**子代理上限:${sp.maxAgentCalls} 个。** 不要超出。`)
408
+ }
409
+ if (sp.maxDocs < 99) {
410
+ profileDirectives.push(`**文档上限:${sp.maxDocs} 份。** 只生成核心文档,不要额外生成 flows/glossary/module-card。`)
411
+ }
412
+ profileDirectives.push(`--output 只需要列出文件名,不要写长篇总结。`)
413
+ promptText = profileDirectives.join('\n') + '\n\n' + promptText
395
414
  } else {
396
415
  // 非 platform 模式也要替换占位符
397
416
  if (stageName === 'scan') {
@@ -433,6 +452,198 @@ async function outputStep(stageName, stepIndex, steps, cwd, changeName, dbProjec
433
452
  console.log(`sillyspec run ${stageName} --done${changeFlag} --input "用户原始需求/反馈" --output "你的摘要"`)
434
453
  }
435
454
 
455
+ /**
456
+ * 根据 project 规模计算 scan profile
457
+ * quick: fileCount≤30 && sourceBytes≤80KB && projectCount≤3 → 3 步,0 子代理,5 份文档
458
+ * standard: fileCount≤200 && sourceBytes≤800KB → 压缩步骤,最多 1 子代理
459
+ * deep: 大项目或 --deep → 完整流程
460
+ */
461
+ function computeScanProfile(cwd, platformOpts) {
462
+ // --deep 标志强制 deep
463
+ const flags = process.argv.slice(2)
464
+ if (flags.includes('--deep')) {
465
+ return { mode: 'deep', reason: '用户指定 --deep', maxAgentCalls: 4, maxDocs: 99 }
466
+ }
467
+
468
+ const specDir = platformOpts?.specRoot || join(cwd, '.sillyspec')
469
+ const projectsDir = join(specDir, 'projects')
470
+ let projectCount = 1
471
+ try {
472
+ if (existsSync(projectsDir)) {
473
+ projectCount = readdirSync(projectsDir).filter(f => f.endsWith('.yaml')).length
474
+ }
475
+ } catch {}
476
+
477
+ // 快速估算源码规模
478
+ let fileCount = 0
479
+ let sourceBytes = 0
480
+ try {
481
+ const { execSync } = require('child_process')
482
+ const findCmd = `find "${cwd}" -type f \\( -name '*.js' -o -name '*.ts' -o -name '*.tsx' -o -name '*.py' -o -name '*.java' -o -name '*.go' -o -name '*.rs' -o -name '*.rb' -o -name '*.php' -o -name '*.c' -o -name '*.cpp' -o -name '*.h' -o -name '*.jsx' -o -name '*.vue' -o -name '*.svelte' \\) -not -path '*/node_modules/*' -not -path '*/.git/*' -not -path '*/dist/*' -not -path '*/build/*' -not -path '*/__pycache__/*' -not -path '*/.sillyspec/*' -not -path '*/.claude/*' 2>/dev/null`
483
+ const files = execSync(findCmd, { encoding: 'utf8', timeout: 10000 }).trim().split('\n').filter(Boolean)
484
+ fileCount = files.length
485
+ for (const f of files) {
486
+ try { sourceBytes += statSync(f).size } catch {}
487
+ }
488
+ } catch {
489
+ // find 失败时假设中等规模
490
+ return { mode: 'standard', reason: '无法估算项目规模', maxAgentCalls: 1, maxDocs: 8 }
491
+ }
492
+
493
+ if (fileCount <= 30 && sourceBytes <= 80_000 && projectCount <= 3) {
494
+ return { mode: 'quick', reason: `${fileCount} 源文件, ${Math.round(sourceBytes / 1024)}KB`, maxAgentCalls: 0, maxDocs: 5, _fileCount: fileCount, _sourceBytes: sourceBytes, _projectCount: projectCount }
495
+ }
496
+ if (fileCount <= 200 && sourceBytes <= 800_000) {
497
+ return { mode: 'standard', reason: `${fileCount} 源文件, ${Math.round(sourceBytes / 1024)}KB`, maxAgentCalls: 1, maxDocs: 8, _fileCount: fileCount, _sourceBytes: sourceBytes, _projectCount: projectCount }
498
+ }
499
+ return { mode: 'deep', reason: `${fileCount} 源文件, ${Math.round(sourceBytes / 1024)}KB`, maxAgentCalls: 4, maxDocs: 99, _fileCount: fileCount, _sourceBytes: sourceBytes, _projectCount: projectCount }
500
+ }
501
+
502
+ /**
503
+ * 根据 scanProfile 裁剪步骤
504
+ * quick: 3 步 — CLI preflight / AI generate / CLI postcheck
505
+ * standard: 跳过续扫检测(4), 跳过可选步骤(9)
506
+ */
507
+ function applyScanProfileSteps(stageData, profile, cwd, platformOpts) {
508
+ const steps = stageData.steps
509
+ const mode = profile.mode
510
+
511
+ if (mode === 'quick') {
512
+ const specBase = platformOpts?.specRoot || join(cwd, '.sillyspec')
513
+ const projectName = basename(cwd)
514
+ const docsRoot = join(specBase, 'docs', projectName)
515
+
516
+ // Step 1: CLI preflight(不调 AI,自动完成)
517
+ const step1 = {
518
+ name: '项目概览(自动探测)',
519
+ status: 'pending',
520
+ noAI: true,
521
+ _cliAction: 'scanPreflight',
522
+ prompt: '',
523
+ outputHint: 'preflight 结果',
524
+ optional: false
525
+ }
526
+ // Step 2: AI 生成核心文档(唯一 AI roundtrip)
527
+ const step2 = {
528
+ name: '生成核心文档',
529
+ status: 'pending',
530
+ prompt: `## Quick Scan — 核心文档生成
531
+
532
+ 项目规模较小(quick profile),请一次性生成所有核心文档。
533
+
534
+ ### 操作
535
+ 1. 读取项目结构和关键文件(package.json / pyproject.toml / README / 入口文件)
536
+ 2. 生成以下 4 份文档并写入 \`{DOCS_ROOT}/scan/\`:
537
+ - **PROJECT.md** — 项目简介、技术栈、模块划分
538
+ - **ARCHITECTURE.md** — 架构概览、模块关系、技术决策
539
+ - **CONVENTIONS.md** — 代码风格、框架隐形规则
540
+ - **STRUCTURE.md** — 目录树 + 模块说明
541
+ 3. 如发现子项目,注册到 \`{PROJECTS_ROOT}/\` 下
542
+
543
+ 每份文档头必须包含 frontmatter:\`author\` 和 \`created_at\`。
544
+
545
+ ### ⛔ 硬约束
546
+ - **严禁使用子代理(Agent/Task 工具)。** 所有文档在一个 turn 内完成。
547
+ - 不要搜索 .sillyspec/ .claude/ .git/ node_modules/ dist/ build/
548
+ - --output 只需要列出生成的文件名,不要写长篇总结
549
+
550
+ ### 输出
551
+ 生成的文件列表`,
552
+ outputHint: '文件列表',
553
+ optional: false
554
+ }
555
+ // Step 3: CLI postcheck(不调 AI,自动完成)
556
+ const selfCheck = steps.find(s => s.name === '自检和提交') || {
557
+ name: '自检和提交', status: 'pending', prompt: '', outputHint: '结果', optional: false
558
+ }
559
+ const step3 = { ...selfCheck, status: 'pending', noAI: true, _cliAction: 'scanPostcheck', prompt: '' }
560
+ stageData.steps = [step1, step2, step3]
561
+ return
562
+ }
563
+
564
+ if (mode === 'standard') {
565
+ // 跳过 Step 4(断点续扫检测),跳过 Step 9(flows+glossary,可选)
566
+ const skipNames = ['断点续扫检测', '生成业务流程和术语表(可选)']
567
+ for (const step of stageData.steps) {
568
+ if (skipNames.includes(step.name) && step.status === 'pending') {
569
+ step.status = 'skipped'
570
+ step.skippedAt = new Date().toLocaleString('zh-CN', { hour12: false })
571
+ }
572
+ }
573
+ }
574
+ }
575
+
576
+ /**
577
+ * CLI-only: quick scan preflight
578
+ * 收集项目快照,打印 summary,不调 AI
579
+ */
580
+ async function executeScanPreflight(cwd, platformOpts, scanProfile) {
581
+ const specBase = platformOpts?.specRoot || join(cwd, '.sillyspec')
582
+ const projectName = basename(cwd)
583
+ console.log(` 📁 项目: ${projectName}`)
584
+ console.log(` 📊 Profile: ${scanProfile.mode} (${scanProfile.reason})`)
585
+ // 快速列出顶层结构
586
+ try {
587
+ const { execSync } = await import('child_process')
588
+ const dirs = execSync(`ls -d */ 2>/dev/null | grep -v node_modules | grep -v '.git' | grep -v '.sillyspec' | grep -v '.claude' | head -20`, { cwd, encoding: 'utf8' }).trim()
589
+ if (dirs) {
590
+ console.log(` 📂 目录: ${dirs.split('\n').map(d => d.replace(/\/$/, '')).join(', ')}`)
591
+ }
592
+ } catch {}
593
+ console.log(` ✅ Preflight 完成,准备生成核心文档\n`)
594
+ }
595
+
596
+ /**
597
+ * CLI-only: quick scan postcheck
598
+ * 执行文件存在性 + manifest 检查,不调 AI
599
+ */
600
+ async function executeScanPostcheck(cwd, platformOpts, scanProfile) {
601
+ const { runScanPostCheck, printScanPostCheckResult } = await import('./scan-postcheck.js')
602
+ const specDir = platformOpts?.specRoot || null
603
+ const result = runScanPostCheck({
604
+ cwd,
605
+ specDir,
606
+ scanMeta: {
607
+ projectListParsed: true,
608
+ manifestWritten: undefined,
609
+ },
610
+ })
611
+ printScanPostCheckResult(result)
612
+ // 写 manifest(如果还没写)
613
+ if (platformOpts?.specRoot) {
614
+ try {
615
+ const { mkdirSync, writeFileSync } = await import('fs')
616
+ const { join: pJoin, basename: pBasename } = await import('path')
617
+ const { execSync } = await import('child_process')
618
+ const manifestDir = platformOpts.specRoot
619
+ let sourceCommit = null
620
+ try {
621
+ sourceCommit = execSync('git rev-parse HEAD', { cwd, encoding: 'utf8', timeout: 5000 }).trim()
622
+ } catch {}
623
+ mkdirSync(manifestDir, { recursive: true })
624
+ const manifest = {
625
+ scan_profile: {
626
+ mode: scanProfile.mode,
627
+ file_count: scanProfile._fileCount || 0,
628
+ source_bytes: scanProfile._sourceBytes || 0,
629
+ project_count: scanProfile._projectCount || 0,
630
+ reason: scanProfile.reason,
631
+ },
632
+ workspace_id: platformOpts.workspaceId || null,
633
+ scan_run_id: platformOpts.scanRunId || null,
634
+ source_commit: sourceCommit,
635
+ generated_at: new Date().toISOString(),
636
+ schema_version: 2,
637
+ }
638
+ const manifestPath = pJoin(manifestDir, 'manifest.json')
639
+ writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n')
640
+ console.log(` 📄 manifest.json 已写入: ${manifestPath}`)
641
+ } catch (e) {
642
+ console.warn(` ⚠️ manifest 写入失败: ${e.message}`)
643
+ }
644
+ }
645
+ }
646
+
436
647
  /**
437
648
  * sillyspec run <stage> 主命令
438
649
  */
@@ -605,6 +816,9 @@ export async function runCommand(args, cwd, specDir = null) {
605
816
  }
606
817
 
607
818
  const isAuxiliary = auxiliaryStages.includes(stageName)
819
+ // scan 元数据追踪(供 postcheck 使用)
820
+ let _scanProjectListParsed = undefined // undefined=未到达step2, true=解析成功, false=解析失败
821
+ let _scanManifestWritten = undefined // undefined=未尝试, true=成功, false=失败
608
822
 
609
823
  const pm = new ProgressManager({ specDir: specRoot })
610
824
  let progress = await pm.read(cwd, changeName)
@@ -750,6 +964,24 @@ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval =
750
964
  const steps = stageData.steps
751
965
  let currentIdx = steps.findIndex(s => s.status !== 'completed' && s.status !== 'skipped')
752
966
 
967
+ // ── scanProfile: 根据 project 规模动态裁剪步骤 ──
968
+ let scanProfile = null
969
+ if (stageName === 'scan' && steps.length > 0 && currentIdx === 0) {
970
+ scanProfile = computeScanProfile(cwd, platformOpts)
971
+ console.log(`\n📊 Scan Profile: ${scanProfile.mode} (原因: ${scanProfile.reason})`)
972
+ if (scanProfile.mode !== 'deep') {
973
+ applyScanProfileSteps(stageData, scanProfile, cwd, platformOpts)
974
+ // 步骤被裁剪后 currentIdx 需要重新计算
975
+ currentIdx = 0
976
+ }
977
+ // 保存 profile 供后续 postcheck 使用
978
+ stageData.scanProfile = scanProfile
979
+ await pm._write(cwd, progress, changeName)
980
+ } else if (stageName === 'scan' && stageData.scanProfile) {
981
+ scanProfile = stageData.scanProfile
982
+ }
983
+ if (scanProfile) platformOpts.scanProfile = scanProfile
984
+
753
985
  if (currentIdx === -1) {
754
986
  // 已完成 → 自动重置,重新开始
755
987
  const freshSteps = await getStageSteps(stageName, cwd, progress)
@@ -807,6 +1039,33 @@ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval =
807
1039
 
808
1040
  const defSteps = await getStageSteps(stageName, cwd, progress)
809
1041
  if (defSteps && defSteps[currentIdx]) {
1042
+ // noAI 步骤自动完成(CLI-only,不需要 Agent 参与)
1043
+ if (defSteps[currentIdx].noAI || stageData.steps[currentIdx]?.noAI) {
1044
+ const stepName = defSteps[currentIdx].name
1045
+ const cliAction = defSteps[currentIdx]._cliAction || stageData.steps[currentIdx]?._cliAction
1046
+ console.log(`⚙️ Step ${currentIdx + 1}/${stageData.steps.length}: ${stepName}(CLI 自动执行,无需 Agent)`)
1047
+ if (cliAction === 'scanPreflight') {
1048
+ await executeScanPreflight(cwd, platformOpts, scanProfile)
1049
+ } else if (cliAction === 'scanPostcheck') {
1050
+ await executeScanPostcheck(cwd, platformOpts, scanProfile)
1051
+ }
1052
+ stageData.steps[currentIdx].status = 'completed'
1053
+ stageData.steps[currentIdx].completedAt = new Date().toLocaleString('zh-CN', { hour12: false })
1054
+ await pm._write(cwd, progress, changeName)
1055
+ // 自动前进到下一步
1056
+ const nextIdx = stageData.steps.findIndex(s => s.status === 'pending')
1057
+ if (nextIdx !== -1 && defSteps[nextIdx]) {
1058
+ console.log('')
1059
+ await outputStep(stageName, nextIdx, defSteps, cwd, changeName, progress.project || null, platformOpts)
1060
+ } else {
1061
+ // 所有步骤完成
1062
+ stageData.status = 'completed'
1063
+ stageData.completedAt = new Date().toLocaleString('zh-CN', { hour12: false })
1064
+ await pm._write(cwd, progress, changeName)
1065
+ console.log(`\n✅ ${stageName} 阶段全部完成。`)
1066
+ }
1067
+ return
1068
+ }
810
1069
  await outputStep(stageName, currentIdx, defSteps, cwd, changeName, progress.project || null, platformOpts)
811
1070
  }
812
1071
  }
@@ -926,6 +1185,13 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
926
1185
  const { printNext = true, confirm = false, changeName, platformOpts = {} } = options
927
1186
  const specBase = platformOpts.specRoot || join(cwd, '.sillyspec')
928
1187
  const stageData = progress.stages[stageName]
1188
+ const scanProfile = stageData?.scanProfile || null
1189
+
1190
+ // scanProfile 非 deep 模式:截断 outputText 减少 token 传递
1191
+ let effectiveOutput = outputText
1192
+ if (scanProfile && scanProfile.mode !== 'deep' && outputText && outputText.length > 1000) {
1193
+ effectiveOutput = outputText.slice(0, 1000) + '\n\n…[输出已截断,完整内容见 artifact]'
1194
+ }
929
1195
  if (!stageData || !stageData.steps) {
930
1196
  console.error(`❌ 阶段 ${stageName} 未初始化`)
931
1197
  process.exit(1)
@@ -945,6 +1211,8 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
945
1211
  const MAX_OUTPUT = 200
946
1212
  if (outputText.length > MAX_OUTPUT) {
947
1213
  steps[currentIdx].output = outputText.slice(0, MAX_OUTPUT) + '…'
1214
+ steps[currentIdx].output_truncated = true
1215
+ steps[currentIdx].output_original_length = outputText.length
948
1216
  // 平台模式:artifact 写入 runtime-root,否则写 .sillyspec/.runtime/artifacts
949
1217
  const artifactBase = platformOpts?.runtimeRoot
950
1218
  ? join(platformOpts.runtimeRoot, 'scan-runs', platformOpts.scanRunId || 'unknown')
@@ -1024,6 +1292,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
1024
1292
  const numbered = outputText.match(/^\s*\d+\.\s+(\S+)/gm)
1025
1293
  if (numbered) {
1026
1294
  projectNames = numbered.map(m => m.replace(/^\s*\d+\.\s+/, '').replace(/[—\-:].*$/, '').trim())
1295
+ if (projectNames.length > 0) _scanProjectListParsed = true
1027
1296
  }
1028
1297
  // 匹配方式 2: 括号枚举 "子项目frontend/order-service/user-service" 或 "项目: a, b, c"
1029
1298
  if (projectNames.length === 0) {
@@ -1033,6 +1302,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
1033
1302
  .split(/[\/、,,]+/)
1034
1303
  .map(s => s.trim())
1035
1304
  .filter(Boolean)
1305
+ if (projectNames.length > 0) _scanProjectListParsed = true
1036
1306
  }
1037
1307
  }
1038
1308
  // 匹配方式 3: 结构化 YAML block "scan_projects:\n - id: name"
@@ -1040,12 +1310,14 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
1040
1310
  const yamlMatch = outputText.match(/scan_projects:\s*\n((?:\s+-\s+id:\s+\S+\s*\n?)+)/)
1041
1311
  if (yamlMatch) {
1042
1312
  projectNames = [...yamlMatch[1].matchAll(/-\s+id:\s*(\S+)/g)].map(m => m[1])
1313
+ if (projectNames.length > 0) _scanProjectListParsed = true
1043
1314
  }
1044
1315
  }
1045
1316
  }
1046
1317
  if (projectNames.length === 0) {
1047
1318
  // 回退:读取所有已注册项目
1048
1319
  console.warn('⚠️ 未能从 step 2 输出解析项目列表,回退扫描所有注册项目')
1320
+ _scanProjectListParsed = false
1049
1321
  const projectsDir = join(specBase, 'projects')
1050
1322
  if (existsSync(projectsDir)) {
1051
1323
  projectNames = readdirSync(projectsDir)
@@ -1057,6 +1329,27 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
1057
1329
  projectNames = ['sillyspec'] // 最终兜底
1058
1330
  }
1059
1331
 
1332
+ // 自动注册未注册的子项目(确保 projects/*.yaml 存在,避免展开时 projectRoot 缺失)
1333
+ const projectsDir = join(specBase, 'projects')
1334
+ for (const pName of projectNames) {
1335
+ const projYaml = join(projectsDir, `${pName}.yaml`)
1336
+ if (!existsSync(projYaml)) {
1337
+ mkdirSync(projectsDir, { recursive: true })
1338
+ // 子项目路径推测:检查 cwd 下是否有同名目录
1339
+ const candidates = [
1340
+ join(cwd, pName), // cwd/frontend
1341
+ join(cwd, 'backend', pName), // cwd/backend/user-service
1342
+ join(cwd, 'packages', pName), // monorepo packages
1343
+ join(cwd, 'apps', pName), // monorepo apps
1344
+ join(cwd, 'services', pName), // monorepo services
1345
+ ]
1346
+ const detected = candidates.find(c => existsSync(c))
1347
+ const regPath = detected || join(cwd, pName)
1348
+ writeFileSync(projYaml, `name: ${pName}\npath: ${regPath}\nstatus: active\n`)
1349
+ console.log(` 📝 自动注册子项目: ${pName} → ${regPath}`)
1350
+ }
1351
+ }
1352
+
1060
1353
  // 保存到 runtime 供后续使用 + 防重复展开
1061
1354
  const scanStatePath = join(specBase, '.runtime', 'scan-projects.json')
1062
1355
  mkdirSync(join(specBase, '.runtime'), { recursive: true })
@@ -1133,6 +1426,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
1133
1426
  // 平台模式:scan 完成后生成 manifest.json + post-check
1134
1427
  if (stageName === 'scan' && (platformOpts.specRoot || platformOpts.runtimeRoot)) {
1135
1428
  try {
1429
+ _scanManifestWritten = false // 默认失败
1136
1430
  const { mkdirSync, writeFileSync } = await import('fs')
1137
1431
  const { join } = await import('path')
1138
1432
  const { execSync } = await import('child_process')
@@ -1152,6 +1446,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
1152
1446
  const manifestPath = join(manifestDir, 'manifest.json')
1153
1447
  writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n')
1154
1448
  console.log(`📄 manifest.json 已写入: ${manifestPath}`)
1449
+ _scanManifestWritten = true
1155
1450
  if (!sourceCommit) {
1156
1451
  console.log(`⚠️ source_commit 无法获取(可能非 git 目录),已设为 null`)
1157
1452
  }
@@ -1166,6 +1461,10 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
1166
1461
  cwd,
1167
1462
  specDir: platformOpts.specRoot,
1168
1463
  outputText,
1464
+ scanMeta: {
1465
+ projectListParsed: _scanProjectListParsed,
1466
+ manifestWritten: _scanManifestWritten,
1467
+ },
1169
1468
  })
1170
1469
  printScanPostCheckResult(postResult)
1171
1470
 
@@ -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