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 +1 -1
- package/src/run.js +299 -0
- package/src/scan-postcheck.js +24 -2
package/package.json
CHANGED
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
|
|
package/src/scan-postcheck.js
CHANGED
|
@@ -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.
|
|
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
|
|