sillyspec 3.17.4 → 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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/run.js +265 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sillyspec",
3
- "version": "3.17.4",
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
  */
@@ -753,6 +964,24 @@ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval =
753
964
  const steps = stageData.steps
754
965
  let currentIdx = steps.findIndex(s => s.status !== 'completed' && s.status !== 'skipped')
755
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
+
756
985
  if (currentIdx === -1) {
757
986
  // 已完成 → 自动重置,重新开始
758
987
  const freshSteps = await getStageSteps(stageName, cwd, progress)
@@ -810,6 +1039,33 @@ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval =
810
1039
 
811
1040
  const defSteps = await getStageSteps(stageName, cwd, progress)
812
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
+ }
813
1069
  await outputStep(stageName, currentIdx, defSteps, cwd, changeName, progress.project || null, platformOpts)
814
1070
  }
815
1071
  }
@@ -929,6 +1185,13 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
929
1185
  const { printNext = true, confirm = false, changeName, platformOpts = {} } = options
930
1186
  const specBase = platformOpts.specRoot || join(cwd, '.sillyspec')
931
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
+ }
932
1195
  if (!stageData || !stageData.steps) {
933
1196
  console.error(`❌ 阶段 ${stageName} 未初始化`)
934
1197
  process.exit(1)
@@ -948,6 +1211,8 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
948
1211
  const MAX_OUTPUT = 200
949
1212
  if (outputText.length > MAX_OUTPUT) {
950
1213
  steps[currentIdx].output = outputText.slice(0, MAX_OUTPUT) + '…'
1214
+ steps[currentIdx].output_truncated = true
1215
+ steps[currentIdx].output_original_length = outputText.length
951
1216
  // 平台模式:artifact 写入 runtime-root,否则写 .sillyspec/.runtime/artifacts
952
1217
  const artifactBase = platformOpts?.runtimeRoot
953
1218
  ? join(platformOpts.runtimeRoot, 'scan-runs', platformOpts.scanRunId || 'unknown')