sillyspec 3.26.10 → 3.26.11
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/.claude/skills/sillyspec-archive/SKILL.md +85 -85
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -114
- package/.claude/skills/sillyspec-commit/SKILL.md +109 -109
- package/.claude/skills/sillyspec-continue/SKILL.md +45 -45
- package/.claude/skills/sillyspec-doctor/SKILL.md +76 -76
- package/.claude/skills/sillyspec-execute/SKILL.md +200 -200
- package/.claude/skills/sillyspec-explore/SKILL.md +95 -95
- package/.claude/skills/sillyspec-knowledge/SKILL.md +269 -269
- package/.claude/skills/sillyspec-plan/SKILL.md +148 -148
- package/.claude/skills/sillyspec-quick/SKILL.md +111 -111
- package/.claude/skills/sillyspec-resume/SKILL.md +68 -68
- package/.claude/skills/sillyspec-scan/SKILL.md +79 -79
- package/.claude/skills/sillyspec-state/SKILL.md +64 -64
- package/.claude/skills/sillyspec-status/SKILL.md +54 -54
- package/.claude/skills/sillyspec-verify/SKILL.md +83 -83
- package/.claude/skills/sillyspec-workspace/SKILL.md +169 -169
- package/.gitattributes +2 -0
- package/.husky/pre-push +20 -20
- package/.npmrc-publish +2 -0
- package/README.md +201 -201
- package/SKILL.md +90 -90
- package/package.json +39 -39
- package/packages/dashboard/dist/assets/index-Bq_Z2hne.js +7446 -7446
- package/packages/dashboard/dist/assets/index-O2W5RV4z.css +1 -1
- package/packages/dashboard/dist/index.html +16 -16
- package/packages/dashboard/package.json +25 -25
- package/packages/dashboard/server/executor.js +86 -86
- package/packages/dashboard/server/index.js +601 -601
- package/packages/dashboard/server/parser.js +526 -526
- package/src/change-risk-profile.js +254 -254
- package/src/classify-change.js +109 -109
- package/src/config-schema.js +310 -310
- package/src/contract-matrix.js +379 -379
- package/src/db.js +350 -350
- package/src/dispatch/backends/local-agent.js +155 -155
- package/src/dispatch/backends/sillyhub-mcp.js +245 -245
- package/src/dispatch/probe.js +210 -210
- package/src/dispatch/strategy.js +121 -121
- package/src/docs-check.js +131 -4
- package/src/doctor-diagnostics.js +711 -711
- package/src/endpoint-extractor.js +401 -401
- package/src/fs-atomic.js +69 -69
- package/src/git-helper.js +79 -79
- package/src/hooks/claude-pre-tool-use.cjs +125 -125
- package/src/hooks/worktree-guard.js +848 -848
- package/src/index.js +90 -8
- package/src/init.js +623 -623
- package/src/knowledge-match.js +130 -130
- package/src/local-detect.js +120 -120
- package/src/machine-interface.js +543 -543
- package/src/migrate.js +127 -127
- package/src/progress/change-registry.js +297 -260
- package/src/progress/consistency-doctor.js +355 -355
- package/src/progress/shared.js +36 -36
- package/src/progress/stage-machine.js +29 -1
- package/src/progress/step-store.js +261 -261
- package/src/progress.js +1131 -1127
- package/src/quicklog.js +693 -693
- package/src/review-tier.js +77 -77
- package/src/run/command.js +1385 -1364
- package/src/run/complete-handlers.js +1443 -1399
- package/src/run/complete.js +225 -24
- package/src/run/gates.js +755 -755
- package/src/run/prompt.js +761 -761
- package/src/run/quick-audit.js +155 -146
- package/src/run/shared.js +1268 -1215
- package/src/run/stage.js +487 -479
- package/src/scan-postcheck.js +371 -371
- package/src/setup.js +416 -416
- package/src/sillyhub-mcp/client.js +529 -529
- package/src/spec-sync.js +323 -319
- package/src/stage-contract.js +897 -897
- package/src/stage-review.js +549 -549
- package/src/stages/archive.js +155 -155
- package/src/stages/brainstorm-auto.js +236 -236
- package/src/stages/brainstorm.js +596 -597
- package/src/stages/cmd-existence.js +145 -145
- package/src/stages/doctor.js +371 -371
- package/src/stages/execute.js +1003 -1003
- package/src/stages/explore.js +34 -34
- package/src/stages/index.js +29 -29
- package/src/stages/knowledge.js +498 -498
- package/src/stages/plan-postcheck.js +1302 -1302
- package/src/stages/plan.js +625 -623
- package/src/stages/quick.js +151 -151
- package/src/stages/scan.js +607 -607
- package/src/stages/status.js +64 -64
- package/src/stages/verify.js +264 -264
- package/src/sync.js +1223 -1135
- package/src/task-review.js +1019 -1019
- package/src/verify-postcheck.js +1186 -1186
- package/src/workflow.js +600 -600
- package/src/worktree-apply.js +1213 -1136
- package/src/worktree-deps.js +353 -353
- package/src/worktree.js +1481 -1481
- package/templates/workflows/archive-impact.yaml +79 -79
- package/templates/workflows/scan-docs.yaml +132 -132
package/src/scan-postcheck.js
CHANGED
|
@@ -1,371 +1,371 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* scan-postcheck.js — CLI 层 scan 完成后强制校验
|
|
3
|
-
*
|
|
4
|
-
* 不依赖 AI agent 的自检报告,由 CLI 代码直接检查文件系统。
|
|
5
|
-
* 平台模式下必须通过所有 check 才能 success,否则降级。
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { existsSync, readdirSync, readFileSync, mkdirSync, writeFileSync } from 'fs'
|
|
9
|
-
import { join, basename } from 'path'
|
|
10
|
-
import { SCAN_STATUS, CHECK_SEVERITY, SCAN_REQUIRED_DOCS, SCAN_REQUIRED_DOCS_QUICK } from './constants.js'
|
|
11
|
-
import { validateScriptCommands } from './stages/cmd-existence.js'
|
|
12
|
-
|
|
13
|
-
const REQUIRED_SCAN_DOCS = SCAN_REQUIRED_DOCS
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* @param {object} opts
|
|
17
|
-
* @param {string} opts.cwd - 源码项目根目录 (source_root)
|
|
18
|
-
* @param {string} opts.specDir - 规范目录 (spec-root),null 时为非平台模式
|
|
19
|
-
* @param {string} [opts.outputText] - 最后一步(自检)的 AI 输出文本
|
|
20
|
-
* @param {object} [opts.scanMeta] - scan 元数据(由 runCommand 传入)
|
|
21
|
-
* @param {boolean} [opts.scanMeta.projectListParsed] - Step 2 项目列表是否成功解析
|
|
22
|
-
* @param {boolean} [opts.scanMeta.manifestWritten] - manifest.json 是否写入成功
|
|
23
|
-
* @param {number} [opts.scanMeta.projectCount] - 实际展开的项目数量
|
|
24
|
-
* @returns {{ status: 'success'|'completed_with_warnings'|'failed_post_check', checks: Array<{name, severity, detail}> }}
|
|
25
|
-
*/
|
|
26
|
-
export function runScanPostCheck({ cwd, specDir, outputText = '', scanMeta = {}, scanProfile = null } ) {
|
|
27
|
-
const isPlatform = !!specDir
|
|
28
|
-
// profile 感知:quick 档只要求 4 份核心文档,否则要求完整 7 份。
|
|
29
|
-
// 否则 quick 扫描(--quick 显式 或 小项目自动判定)在平台模式会因缺 INTEGRATIONS/TESTING/CONCERNS 直接判 failed。
|
|
30
|
-
const mode = scanProfile?.mode || null
|
|
31
|
-
const requiredDocs = mode === 'quick' ? SCAN_REQUIRED_DOCS_QUICK : SCAN_REQUIRED_DOCS
|
|
32
|
-
const checks = []
|
|
33
|
-
|
|
34
|
-
if (!isPlatform) {
|
|
35
|
-
// 非平台模式:只做轻量检查
|
|
36
|
-
const localSpec = join(cwd, '.sillyspec')
|
|
37
|
-
const scanDir = join(localSpec, 'docs', basename(cwd), 'scan')
|
|
38
|
-
|
|
39
|
-
// 检查必需文档是否存在(quick profile 按 4 份清单,否则 7 份)
|
|
40
|
-
const missing = requiredDocs.filter(f => !existsSync(join(scanDir, f)))
|
|
41
|
-
if (missing.length > 0) {
|
|
42
|
-
checks.push({ name: 'missing_docs', severity: CHECK_SEVERITY.WARNING, detail: `缺少 ${missing.length} 份 scan 文档(${mode || 'full'} profile): ${missing.join(', ')}` })
|
|
43
|
-
}
|
|
44
|
-
if (mode === 'quick') {
|
|
45
|
-
checks.push({ name: 'quick_profile_notice', severity: CHECK_SEVERITY.WARNING, detail: 'quick scan:仅生成 4 份核心文档(PROJECT/ARCHITECTURE/CONVENTIONS/STRUCTURE),深度扫描待补齐' })
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const hasWarning = checks.some(c => c.severity === 'warning')
|
|
49
|
-
return { status: hasWarning ? 'completed_with_warnings' : 'success', checks }
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// ── 平台模式:严格检查 ──
|
|
53
|
-
|
|
54
|
-
const projectName = basename(cwd)
|
|
55
|
-
|
|
56
|
-
// 1. source_root 污染检查(docs/projects/workflows/knowledge/manifest/local)
|
|
57
|
-
const pollutePaths = ['docs', 'projects', 'workflows', 'knowledge']
|
|
58
|
-
const polluteFiles = ['manifest.json', 'local.yaml']
|
|
59
|
-
for (const sub of pollutePaths) {
|
|
60
|
-
const localSub = join(cwd, '.sillyspec', sub)
|
|
61
|
-
if (existsSync(localSub)) {
|
|
62
|
-
try {
|
|
63
|
-
const leaked = readdirSync(localSub, { recursive: true }).filter(e => String(e).endsWith('.md') || String(e).endsWith('.yaml') || String(e).endsWith('.json'))
|
|
64
|
-
if (leaked.length > 0) {
|
|
65
|
-
checks.push({
|
|
66
|
-
name: sub === 'docs' ? 'source_root_docs_leak' : 'source_root_leak',
|
|
67
|
-
severity: CHECK_SEVERITY.FAILED,
|
|
68
|
-
detail: `source_root/.sillyspec/${sub}/ 下存在 ${leaked.length} 个文件(${localSub}/),agent 写入到了错误路径`
|
|
69
|
-
})
|
|
70
|
-
}
|
|
71
|
-
} catch {}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
for (const file of polluteFiles) {
|
|
75
|
-
const filePath = join(cwd, '.sillyspec', file)
|
|
76
|
-
if (existsSync(filePath)) {
|
|
77
|
-
checks.push({
|
|
78
|
-
name: 'source_root_leak',
|
|
79
|
-
severity: CHECK_SEVERITY.FAILED,
|
|
80
|
-
detail: `source_root/.sillyspec/${file} 存在,agent 写入到了错误路径(${filePath})`
|
|
81
|
-
})
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// 2. spec_root 检查必需文档(quick profile 按 4 份清单,否则 7 份)
|
|
86
|
-
const specScanDir = join(specDir, 'docs', projectName, 'scan')
|
|
87
|
-
const missingDocs = requiredDocs.filter(f => !existsSync(join(specScanDir, f)))
|
|
88
|
-
if (missingDocs.length > 0) {
|
|
89
|
-
checks.push({
|
|
90
|
-
name: missingDocs.length === requiredDocs.length ? 'all_docs_missing' : 'partial_docs_missing',
|
|
91
|
-
severity: CHECK_SEVERITY.FAILED,
|
|
92
|
-
detail: missingDocs.length === requiredDocs.length
|
|
93
|
-
? `spec_root 下无任何 scan 文档(${specScanDir}/),扫描可能未执行`
|
|
94
|
-
: `spec_root 缺少必需文档: ${missingDocs.join(', ')}(${mode === 'quick' ? 'quick profile 要求 4 份核心文档' : '7 份 scan 文档均为 required'})`
|
|
95
|
-
})
|
|
96
|
-
}
|
|
97
|
-
if (mode === 'quick') {
|
|
98
|
-
checks.push({ name: 'quick_profile_notice', severity: CHECK_SEVERITY.WARNING, detail: 'quick scan:仅生成 4 份核心文档(PROJECT/ARCHITECTURE/CONVENTIONS/STRUCTURE),深度扫描待补齐' })
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// 3. 检查文档 header(author / created_at)— 只看文件头部,避免正文出现同名词被误判
|
|
102
|
-
const existingDocs = requiredDocs.filter(f => existsSync(join(specScanDir, f)))
|
|
103
|
-
const docsMissingHeader = []
|
|
104
|
-
for (const doc of existingDocs) {
|
|
105
|
-
const content = readFileSync(join(specScanDir, doc), 'utf8')
|
|
106
|
-
const headerSlice = content.slice(0, 512)
|
|
107
|
-
if (!/author\s*:/.test(headerSlice) || !/created_at\s*:/.test(headerSlice)) {
|
|
108
|
-
docsMissingHeader.push(doc)
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
if (docsMissingHeader.length > 0) {
|
|
112
|
-
checks.push({
|
|
113
|
-
name: 'docs_missing_header',
|
|
114
|
-
severity: CHECK_SEVERITY.WARNING,
|
|
115
|
-
detail: `${docsMissingHeader.length} 份文档缺少 author/created_at: ${docsMissingHeader.join(', ')}`
|
|
116
|
-
})
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// 4. local.yaml 校验
|
|
120
|
-
const localYamlPath = join(specDir, 'local.yaml')
|
|
121
|
-
if (existsSync(localYamlPath)) {
|
|
122
|
-
const yamlContent = readFileSync(localYamlPath, 'utf8')
|
|
123
|
-
// 改调共享 validateScriptCommands(H2,去重 plan/scan 两处命令校验)。
|
|
124
|
-
// 严重度维持 WARNING(design D-04:scan 阶段保守,local.yaml 命令误报不阻断 init);
|
|
125
|
-
// 不传 modules → 仅查根 package.json,与历史行为一致(monorepo 子包感知由 plan-postcheck 启用)。
|
|
126
|
-
// 仅校验 `npm|pnpm|yarn run <script>` 这类可静态对账的命令;install/typecheck/npx 等直接
|
|
127
|
-
// 包管理器调用不在范围(沿用 cmd-existence.js:126-129 注释立场,正则天然不匹配它们)。
|
|
128
|
-
const { invalid } = validateScriptCommands(yamlContent, { projectRoot: cwd })
|
|
129
|
-
if (invalid.length > 0) {
|
|
130
|
-
checks.push({
|
|
131
|
-
name: 'local_config_invalid',
|
|
132
|
-
severity: CHECK_SEVERITY.WARNING,
|
|
133
|
-
detail: `local.yaml 引用不存在的命令: ${invalid.map(i => `${i.cmd} (${i.reason})`).join('; ')}`
|
|
134
|
-
})
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// 5. 检查 AI 输出中的真实错误标记
|
|
139
|
-
// 注意:不对 agent 描述性文本做全文正则匹配,防止误报。
|
|
140
|
-
// 只检测明显是 agent 运行时失败的信号(未被捕获的错误块),
|
|
141
|
-
// 而非 agent 正常描述中提到这些词。
|
|
142
|
-
if (outputText) {
|
|
143
|
-
// 5a: 检测未被恢复的 API 错误(连续多次、非描述性提及)
|
|
144
|
-
const apiErrorCount = (outputText.match(/API Error\b.*?\b529\b/gi) || []).length
|
|
145
|
-
if (apiErrorCount >= 2) {
|
|
146
|
-
checks.push({ name: 'api_error_529', severity: CHECK_SEVERITY.WARNING, detail: 'AI 输出中包含多次 API Error 529' })
|
|
147
|
-
}
|
|
148
|
-
const rateLimitCount = (outputText.match(/rate.?limit.*?exhausted/gi) || []).length
|
|
149
|
-
if (rateLimitCount >= 2) {
|
|
150
|
-
checks.push({ name: 'rate_limit_exhausted', severity: CHECK_SEVERITY.WARNING, detail: 'AI 输出中包含多次 rate_limit exhausted' })
|
|
151
|
-
}
|
|
152
|
-
// tool_use_error 和 fallback 已移除:agent 描述性文本中正常提及这些词
|
|
153
|
-
// 不应触发 warning。真正的问题会通过文档缺失、manifest 失败等其他检查捕获。
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// 6. manifest 写入状态检查
|
|
157
|
-
if (scanMeta.manifestWritten === false) {
|
|
158
|
-
checks.push({
|
|
159
|
-
name: 'manifest_write_failed',
|
|
160
|
-
severity: CHECK_SEVERITY.FAILED,
|
|
161
|
-
detail: 'manifest.json 写入失败,平台无法消费 scan 结果'
|
|
162
|
-
})
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// 7. 项目列表解析状态检查
|
|
166
|
-
if (scanMeta.projectListParsed === false) {
|
|
167
|
-
checks.push({
|
|
168
|
-
name: 'project_list_parse_failed',
|
|
169
|
-
severity: CHECK_SEVERITY.WARNING,
|
|
170
|
-
detail: 'Step 2 项目列表解析失败,回退到注册项目列表,可能遗漏子项目'
|
|
171
|
-
})
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// 7.5 knowledge 产物校验
|
|
175
|
-
const knowledgeDir = join(specDir, 'knowledge')
|
|
176
|
-
if (existsSync(knowledgeDir)) {
|
|
177
|
-
const indexPath = join(knowledgeDir, 'INDEX.md')
|
|
178
|
-
if (!existsSync(indexPath)) {
|
|
179
|
-
checks.push({
|
|
180
|
-
name: 'knowledge_index_missing',
|
|
181
|
-
severity: CHECK_SEVERITY.WARNING,
|
|
182
|
-
detail: `knowledge/INDEX.md 不存在`
|
|
183
|
-
})
|
|
184
|
-
} else {
|
|
185
|
-
// 检查 INDEX.md 引用的文件是否真实存在
|
|
186
|
-
const indexContent = readFileSync(indexPath, 'utf8')
|
|
187
|
-
const referencedFiles = [...indexContent.matchAll(/\(([^)]+\.md)/g)].map(m => m[1])
|
|
188
|
-
const missingRefs = referencedFiles.filter(f => !existsSync(join(knowledgeDir, f)))
|
|
189
|
-
if (missingRefs.length > 0) {
|
|
190
|
-
checks.push({
|
|
191
|
-
name: 'knowledge_broken_refs',
|
|
192
|
-
severity: CHECK_SEVERITY.WARNING,
|
|
193
|
-
detail: `INDEX.md 引用了不存在的文件: ${missingRefs.join(', ')}`
|
|
194
|
-
})
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
} else {
|
|
198
|
-
checks.push({
|
|
199
|
-
name: 'knowledge_dir_missing',
|
|
200
|
-
severity: CHECK_SEVERITY.WARNING,
|
|
201
|
-
detail: `knowledge/ 目录不存在`
|
|
202
|
-
})
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// 8. 计算 finalStatus
|
|
206
|
-
const hasFailed = checks.some(c => c.severity === CHECK_SEVERITY.FAILED)
|
|
207
|
-
const hasWarning = checks.some(c => c.severity === CHECK_SEVERITY.WARNING)
|
|
208
|
-
|
|
209
|
-
let status
|
|
210
|
-
if (hasFailed) {
|
|
211
|
-
status = SCAN_STATUS.FAILED_POST_CHECK
|
|
212
|
-
} else if (hasWarning) {
|
|
213
|
-
status = SCAN_STATUS.COMPLETED_WITH_WARNINGS
|
|
214
|
-
} else {
|
|
215
|
-
status = SCAN_STATUS.SUCCESS
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
return { status, checks }
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* 将 postcheck 结果转换为结构化 JSON(SillyHub 可消费格式)
|
|
223
|
-
*
|
|
224
|
-
* failure_category 标准化:
|
|
225
|
-
* - warning : 非致命问题,不阻塞流程
|
|
226
|
-
* - error : 文档缺失/内容不完整,需要修复
|
|
227
|
-
* - critical : 安全问题(source_root 泄漏/路径污染)
|
|
228
|
-
*
|
|
229
|
-
* 结构化字段:
|
|
230
|
-
* - violations : 明确违反约束的条目(source_root 泄漏等)
|
|
231
|
-
* - missing_outputs : 预期文件不存在
|
|
232
|
-
* - path_pollution : 产物写入了错误路径
|
|
233
|
-
* - bad_references : 引用了不存在的命令/资源
|
|
234
|
-
* - quality_warnings: AI 输出中包含错误标记等质量信号
|
|
235
|
-
*
|
|
236
|
-
* @param {object} result - runScanPostCheck 返回值
|
|
237
|
-
* @param {object} [meta] - 附带元数据(workspace_id, scan_run_id, timestamp 等)
|
|
238
|
-
* @returns {object} 结构化 JSON
|
|
239
|
-
*/
|
|
240
|
-
export function formatStructuredResult(result, meta = {}) {
|
|
241
|
-
const structured = {
|
|
242
|
-
schema_version: 1,
|
|
243
|
-
generated_at: new Date().toISOString(),
|
|
244
|
-
overall_status: result.status,
|
|
245
|
-
// 路径溯源(供平台消费)
|
|
246
|
-
...(meta.workspace_id ? { workspace_id: meta.workspace_id } : {}),
|
|
247
|
-
...(meta.scan_run_id ? { scan_run_id: meta.scan_run_id } : {}),
|
|
248
|
-
...(meta.source_root ? { source_root: meta.source_root } : {}),
|
|
249
|
-
...(meta.spec_root ? { spec_root: meta.spec_root } : {}),
|
|
250
|
-
...(meta.runtime_root ? { runtime_root: meta.runtime_root } : {}),
|
|
251
|
-
summary: {
|
|
252
|
-
total_checks: result.checks.length,
|
|
253
|
-
critical: 0,
|
|
254
|
-
error: 0,
|
|
255
|
-
warning: 0,
|
|
256
|
-
},
|
|
257
|
-
failure_categories: {
|
|
258
|
-
violations: [],
|
|
259
|
-
missing_outputs: [],
|
|
260
|
-
path_pollution: [],
|
|
261
|
-
bad_references: [],
|
|
262
|
-
quality_warnings: [],
|
|
263
|
-
},
|
|
264
|
-
checks: result.checks.map(c => ({
|
|
265
|
-
name: c.name,
|
|
266
|
-
severity: c.severity === 'failed' ? 'critical' : c.severity,
|
|
267
|
-
detail: c.detail,
|
|
268
|
-
})),
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// 分类到 failure_categories
|
|
272
|
-
for (const check of result.checks) {
|
|
273
|
-
const severity = check.severity === 'failed' ? 'critical' : check.severity
|
|
274
|
-
const entry = { name: check.name, detail: check.detail, severity }
|
|
275
|
-
|
|
276
|
-
// 路径污染类
|
|
277
|
-
if (check.name === 'source_root_leak' || check.name === 'source_root_docs_leak') {
|
|
278
|
-
structured.failure_categories.path_pollution.push(entry)
|
|
279
|
-
structured.failure_categories.violations.push(entry)
|
|
280
|
-
}
|
|
281
|
-
// 文档缺失类
|
|
282
|
-
else if (check.name === 'all_docs_missing' || check.name === 'partial_docs_missing' || check.name === 'missing_docs') {
|
|
283
|
-
structured.failure_categories.missing_outputs.push(entry)
|
|
284
|
-
}
|
|
285
|
-
// 引用无效类
|
|
286
|
-
else if (check.name === 'local_config_invalid') {
|
|
287
|
-
structured.failure_categories.bad_references.push(entry)
|
|
288
|
-
}
|
|
289
|
-
// AI 输出质量类
|
|
290
|
-
else if (['api_error_529', 'rate_limit_exhausted'].includes(check.name)) {
|
|
291
|
-
structured.failure_categories.quality_warnings.push(entry)
|
|
292
|
-
}
|
|
293
|
-
// manifest/project 列表问题
|
|
294
|
-
else if (check.name === 'manifest_write_failed' || check.name === 'project_list_parse_failed') {
|
|
295
|
-
structured.failure_categories.violations.push(entry)
|
|
296
|
-
}
|
|
297
|
-
// 文档缺少 header
|
|
298
|
-
else if (check.name === 'docs_missing_header') {
|
|
299
|
-
structured.failure_categories.quality_warnings.push(entry)
|
|
300
|
-
}
|
|
301
|
-
// quick profile 说明(informational warning,非缺陷)
|
|
302
|
-
else if (check.name === 'quick_profile_notice') {
|
|
303
|
-
structured.failure_categories.quality_warnings.push(entry)
|
|
304
|
-
}
|
|
305
|
-
// 兜底:归入 violations
|
|
306
|
-
else {
|
|
307
|
-
structured.failure_categories.violations.push(entry)
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// 汇总计数
|
|
312
|
-
for (const check of result.checks) {
|
|
313
|
-
if (check.severity === 'failed') structured.summary.critical++
|
|
314
|
-
else structured.summary.warning++
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
return structured
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
/**
|
|
321
|
-
* 将结构化结果写入 JSON 文件(平台模式供 SillyHub 消费)
|
|
322
|
-
*
|
|
323
|
-
* 本地模式:写入 specDir/.runtime/postcheck-result.json
|
|
324
|
-
* 平台模式:写入 runtimeRoot/scan-runs/{scan_run_id}/postcheck-result.json
|
|
325
|
-
*
|
|
326
|
-
* @param {object} structured - formatStructuredResult 返回值
|
|
327
|
-
* @param {string} specDir - 规范目录(本地模式使用)
|
|
328
|
-
* @param {object} [opts] - 平台模式选项
|
|
329
|
-
* @param {string} [opts.runtimeRoot] - 平台模式运行时根目录
|
|
330
|
-
* @param {string} [opts.scanRunId] - scan run ID
|
|
331
|
-
* @returns {string|null} 写入的文件路径,失败时返回 null
|
|
332
|
-
*/
|
|
333
|
-
export function writeStructuredResult(structured, specDir, opts = {}) {
|
|
334
|
-
if (!specDir && !opts.runtimeRoot) return null
|
|
335
|
-
try {
|
|
336
|
-
let outPath
|
|
337
|
-
if (opts.runtimeRoot && opts.scanRunId) {
|
|
338
|
-
const scanRunDir = join(opts.runtimeRoot, 'scan-runs', opts.scanRunId)
|
|
339
|
-
mkdirSync(scanRunDir, { recursive: true })
|
|
340
|
-
outPath = join(scanRunDir, 'postcheck-result.json')
|
|
341
|
-
} else if (specDir) {
|
|
342
|
-
const runtimeDir = join(specDir, '.runtime')
|
|
343
|
-
mkdirSync(runtimeDir, { recursive: true })
|
|
344
|
-
outPath = join(runtimeDir, 'postcheck-result.json')
|
|
345
|
-
} else {
|
|
346
|
-
return null
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
writeFileSync(outPath, JSON.stringify(structured, null, 2) + '\n')
|
|
350
|
-
return outPath
|
|
351
|
-
} catch (e) {
|
|
352
|
-
console.warn(` ⚠️ postcheck-result.json 写入失败: ${e.message}`)
|
|
353
|
-
return null
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
/**
|
|
358
|
-
* 打印 post-check 结果到 stdout
|
|
359
|
-
*/
|
|
360
|
-
export function printScanPostCheckResult(result) {
|
|
361
|
-
if (result.checks.length === 0) {
|
|
362
|
-
console.log(' ✅ CLI post-check: 全部通过')
|
|
363
|
-
return
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
for (const check of result.checks) {
|
|
367
|
-
const icon = check.severity === 'failed' ? '❌' : '⚠️'
|
|
368
|
-
console.log(` ${icon} CLI post-check [${check.name}]: ${check.detail}`)
|
|
369
|
-
}
|
|
370
|
-
console.log(` 📋 最终状态: ${result.status}`)
|
|
371
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* scan-postcheck.js — CLI 层 scan 完成后强制校验
|
|
3
|
+
*
|
|
4
|
+
* 不依赖 AI agent 的自检报告,由 CLI 代码直接检查文件系统。
|
|
5
|
+
* 平台模式下必须通过所有 check 才能 success,否则降级。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, readdirSync, readFileSync, mkdirSync, writeFileSync } from 'fs'
|
|
9
|
+
import { join, basename } from 'path'
|
|
10
|
+
import { SCAN_STATUS, CHECK_SEVERITY, SCAN_REQUIRED_DOCS, SCAN_REQUIRED_DOCS_QUICK } from './constants.js'
|
|
11
|
+
import { validateScriptCommands } from './stages/cmd-existence.js'
|
|
12
|
+
|
|
13
|
+
const REQUIRED_SCAN_DOCS = SCAN_REQUIRED_DOCS
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {object} opts
|
|
17
|
+
* @param {string} opts.cwd - 源码项目根目录 (source_root)
|
|
18
|
+
* @param {string} opts.specDir - 规范目录 (spec-root),null 时为非平台模式
|
|
19
|
+
* @param {string} [opts.outputText] - 最后一步(自检)的 AI 输出文本
|
|
20
|
+
* @param {object} [opts.scanMeta] - scan 元数据(由 runCommand 传入)
|
|
21
|
+
* @param {boolean} [opts.scanMeta.projectListParsed] - Step 2 项目列表是否成功解析
|
|
22
|
+
* @param {boolean} [opts.scanMeta.manifestWritten] - manifest.json 是否写入成功
|
|
23
|
+
* @param {number} [opts.scanMeta.projectCount] - 实际展开的项目数量
|
|
24
|
+
* @returns {{ status: 'success'|'completed_with_warnings'|'failed_post_check', checks: Array<{name, severity, detail}> }}
|
|
25
|
+
*/
|
|
26
|
+
export function runScanPostCheck({ cwd, specDir, outputText = '', scanMeta = {}, scanProfile = null } ) {
|
|
27
|
+
const isPlatform = !!specDir
|
|
28
|
+
// profile 感知:quick 档只要求 4 份核心文档,否则要求完整 7 份。
|
|
29
|
+
// 否则 quick 扫描(--quick 显式 或 小项目自动判定)在平台模式会因缺 INTEGRATIONS/TESTING/CONCERNS 直接判 failed。
|
|
30
|
+
const mode = scanProfile?.mode || null
|
|
31
|
+
const requiredDocs = mode === 'quick' ? SCAN_REQUIRED_DOCS_QUICK : SCAN_REQUIRED_DOCS
|
|
32
|
+
const checks = []
|
|
33
|
+
|
|
34
|
+
if (!isPlatform) {
|
|
35
|
+
// 非平台模式:只做轻量检查
|
|
36
|
+
const localSpec = join(cwd, '.sillyspec')
|
|
37
|
+
const scanDir = join(localSpec, 'docs', basename(cwd), 'scan')
|
|
38
|
+
|
|
39
|
+
// 检查必需文档是否存在(quick profile 按 4 份清单,否则 7 份)
|
|
40
|
+
const missing = requiredDocs.filter(f => !existsSync(join(scanDir, f)))
|
|
41
|
+
if (missing.length > 0) {
|
|
42
|
+
checks.push({ name: 'missing_docs', severity: CHECK_SEVERITY.WARNING, detail: `缺少 ${missing.length} 份 scan 文档(${mode || 'full'} profile): ${missing.join(', ')}` })
|
|
43
|
+
}
|
|
44
|
+
if (mode === 'quick') {
|
|
45
|
+
checks.push({ name: 'quick_profile_notice', severity: CHECK_SEVERITY.WARNING, detail: 'quick scan:仅生成 4 份核心文档(PROJECT/ARCHITECTURE/CONVENTIONS/STRUCTURE),深度扫描待补齐' })
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const hasWarning = checks.some(c => c.severity === 'warning')
|
|
49
|
+
return { status: hasWarning ? 'completed_with_warnings' : 'success', checks }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ── 平台模式:严格检查 ──
|
|
53
|
+
|
|
54
|
+
const projectName = basename(cwd)
|
|
55
|
+
|
|
56
|
+
// 1. source_root 污染检查(docs/projects/workflows/knowledge/manifest/local)
|
|
57
|
+
const pollutePaths = ['docs', 'projects', 'workflows', 'knowledge']
|
|
58
|
+
const polluteFiles = ['manifest.json', 'local.yaml']
|
|
59
|
+
for (const sub of pollutePaths) {
|
|
60
|
+
const localSub = join(cwd, '.sillyspec', sub)
|
|
61
|
+
if (existsSync(localSub)) {
|
|
62
|
+
try {
|
|
63
|
+
const leaked = readdirSync(localSub, { recursive: true }).filter(e => String(e).endsWith('.md') || String(e).endsWith('.yaml') || String(e).endsWith('.json'))
|
|
64
|
+
if (leaked.length > 0) {
|
|
65
|
+
checks.push({
|
|
66
|
+
name: sub === 'docs' ? 'source_root_docs_leak' : 'source_root_leak',
|
|
67
|
+
severity: CHECK_SEVERITY.FAILED,
|
|
68
|
+
detail: `source_root/.sillyspec/${sub}/ 下存在 ${leaked.length} 个文件(${localSub}/),agent 写入到了错误路径`
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
} catch {}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
for (const file of polluteFiles) {
|
|
75
|
+
const filePath = join(cwd, '.sillyspec', file)
|
|
76
|
+
if (existsSync(filePath)) {
|
|
77
|
+
checks.push({
|
|
78
|
+
name: 'source_root_leak',
|
|
79
|
+
severity: CHECK_SEVERITY.FAILED,
|
|
80
|
+
detail: `source_root/.sillyspec/${file} 存在,agent 写入到了错误路径(${filePath})`
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 2. spec_root 检查必需文档(quick profile 按 4 份清单,否则 7 份)
|
|
86
|
+
const specScanDir = join(specDir, 'docs', projectName, 'scan')
|
|
87
|
+
const missingDocs = requiredDocs.filter(f => !existsSync(join(specScanDir, f)))
|
|
88
|
+
if (missingDocs.length > 0) {
|
|
89
|
+
checks.push({
|
|
90
|
+
name: missingDocs.length === requiredDocs.length ? 'all_docs_missing' : 'partial_docs_missing',
|
|
91
|
+
severity: CHECK_SEVERITY.FAILED,
|
|
92
|
+
detail: missingDocs.length === requiredDocs.length
|
|
93
|
+
? `spec_root 下无任何 scan 文档(${specScanDir}/),扫描可能未执行`
|
|
94
|
+
: `spec_root 缺少必需文档: ${missingDocs.join(', ')}(${mode === 'quick' ? 'quick profile 要求 4 份核心文档' : '7 份 scan 文档均为 required'})`
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
if (mode === 'quick') {
|
|
98
|
+
checks.push({ name: 'quick_profile_notice', severity: CHECK_SEVERITY.WARNING, detail: 'quick scan:仅生成 4 份核心文档(PROJECT/ARCHITECTURE/CONVENTIONS/STRUCTURE),深度扫描待补齐' })
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 3. 检查文档 header(author / created_at)— 只看文件头部,避免正文出现同名词被误判
|
|
102
|
+
const existingDocs = requiredDocs.filter(f => existsSync(join(specScanDir, f)))
|
|
103
|
+
const docsMissingHeader = []
|
|
104
|
+
for (const doc of existingDocs) {
|
|
105
|
+
const content = readFileSync(join(specScanDir, doc), 'utf8')
|
|
106
|
+
const headerSlice = content.slice(0, 512)
|
|
107
|
+
if (!/author\s*:/.test(headerSlice) || !/created_at\s*:/.test(headerSlice)) {
|
|
108
|
+
docsMissingHeader.push(doc)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (docsMissingHeader.length > 0) {
|
|
112
|
+
checks.push({
|
|
113
|
+
name: 'docs_missing_header',
|
|
114
|
+
severity: CHECK_SEVERITY.WARNING,
|
|
115
|
+
detail: `${docsMissingHeader.length} 份文档缺少 author/created_at: ${docsMissingHeader.join(', ')}`
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 4. local.yaml 校验
|
|
120
|
+
const localYamlPath = join(specDir, 'local.yaml')
|
|
121
|
+
if (existsSync(localYamlPath)) {
|
|
122
|
+
const yamlContent = readFileSync(localYamlPath, 'utf8')
|
|
123
|
+
// 改调共享 validateScriptCommands(H2,去重 plan/scan 两处命令校验)。
|
|
124
|
+
// 严重度维持 WARNING(design D-04:scan 阶段保守,local.yaml 命令误报不阻断 init);
|
|
125
|
+
// 不传 modules → 仅查根 package.json,与历史行为一致(monorepo 子包感知由 plan-postcheck 启用)。
|
|
126
|
+
// 仅校验 `npm|pnpm|yarn run <script>` 这类可静态对账的命令;install/typecheck/npx 等直接
|
|
127
|
+
// 包管理器调用不在范围(沿用 cmd-existence.js:126-129 注释立场,正则天然不匹配它们)。
|
|
128
|
+
const { invalid } = validateScriptCommands(yamlContent, { projectRoot: cwd })
|
|
129
|
+
if (invalid.length > 0) {
|
|
130
|
+
checks.push({
|
|
131
|
+
name: 'local_config_invalid',
|
|
132
|
+
severity: CHECK_SEVERITY.WARNING,
|
|
133
|
+
detail: `local.yaml 引用不存在的命令: ${invalid.map(i => `${i.cmd} (${i.reason})`).join('; ')}`
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 5. 检查 AI 输出中的真实错误标记
|
|
139
|
+
// 注意:不对 agent 描述性文本做全文正则匹配,防止误报。
|
|
140
|
+
// 只检测明显是 agent 运行时失败的信号(未被捕获的错误块),
|
|
141
|
+
// 而非 agent 正常描述中提到这些词。
|
|
142
|
+
if (outputText) {
|
|
143
|
+
// 5a: 检测未被恢复的 API 错误(连续多次、非描述性提及)
|
|
144
|
+
const apiErrorCount = (outputText.match(/API Error\b.*?\b529\b/gi) || []).length
|
|
145
|
+
if (apiErrorCount >= 2) {
|
|
146
|
+
checks.push({ name: 'api_error_529', severity: CHECK_SEVERITY.WARNING, detail: 'AI 输出中包含多次 API Error 529' })
|
|
147
|
+
}
|
|
148
|
+
const rateLimitCount = (outputText.match(/rate.?limit.*?exhausted/gi) || []).length
|
|
149
|
+
if (rateLimitCount >= 2) {
|
|
150
|
+
checks.push({ name: 'rate_limit_exhausted', severity: CHECK_SEVERITY.WARNING, detail: 'AI 输出中包含多次 rate_limit exhausted' })
|
|
151
|
+
}
|
|
152
|
+
// tool_use_error 和 fallback 已移除:agent 描述性文本中正常提及这些词
|
|
153
|
+
// 不应触发 warning。真正的问题会通过文档缺失、manifest 失败等其他检查捕获。
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 6. manifest 写入状态检查
|
|
157
|
+
if (scanMeta.manifestWritten === false) {
|
|
158
|
+
checks.push({
|
|
159
|
+
name: 'manifest_write_failed',
|
|
160
|
+
severity: CHECK_SEVERITY.FAILED,
|
|
161
|
+
detail: 'manifest.json 写入失败,平台无法消费 scan 结果'
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// 7. 项目列表解析状态检查
|
|
166
|
+
if (scanMeta.projectListParsed === false) {
|
|
167
|
+
checks.push({
|
|
168
|
+
name: 'project_list_parse_failed',
|
|
169
|
+
severity: CHECK_SEVERITY.WARNING,
|
|
170
|
+
detail: 'Step 2 项目列表解析失败,回退到注册项目列表,可能遗漏子项目'
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// 7.5 knowledge 产物校验
|
|
175
|
+
const knowledgeDir = join(specDir, 'knowledge')
|
|
176
|
+
if (existsSync(knowledgeDir)) {
|
|
177
|
+
const indexPath = join(knowledgeDir, 'INDEX.md')
|
|
178
|
+
if (!existsSync(indexPath)) {
|
|
179
|
+
checks.push({
|
|
180
|
+
name: 'knowledge_index_missing',
|
|
181
|
+
severity: CHECK_SEVERITY.WARNING,
|
|
182
|
+
detail: `knowledge/INDEX.md 不存在`
|
|
183
|
+
})
|
|
184
|
+
} else {
|
|
185
|
+
// 检查 INDEX.md 引用的文件是否真实存在
|
|
186
|
+
const indexContent = readFileSync(indexPath, 'utf8')
|
|
187
|
+
const referencedFiles = [...indexContent.matchAll(/\(([^)]+\.md)/g)].map(m => m[1])
|
|
188
|
+
const missingRefs = referencedFiles.filter(f => !existsSync(join(knowledgeDir, f)))
|
|
189
|
+
if (missingRefs.length > 0) {
|
|
190
|
+
checks.push({
|
|
191
|
+
name: 'knowledge_broken_refs',
|
|
192
|
+
severity: CHECK_SEVERITY.WARNING,
|
|
193
|
+
detail: `INDEX.md 引用了不存在的文件: ${missingRefs.join(', ')}`
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
} else {
|
|
198
|
+
checks.push({
|
|
199
|
+
name: 'knowledge_dir_missing',
|
|
200
|
+
severity: CHECK_SEVERITY.WARNING,
|
|
201
|
+
detail: `knowledge/ 目录不存在`
|
|
202
|
+
})
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// 8. 计算 finalStatus
|
|
206
|
+
const hasFailed = checks.some(c => c.severity === CHECK_SEVERITY.FAILED)
|
|
207
|
+
const hasWarning = checks.some(c => c.severity === CHECK_SEVERITY.WARNING)
|
|
208
|
+
|
|
209
|
+
let status
|
|
210
|
+
if (hasFailed) {
|
|
211
|
+
status = SCAN_STATUS.FAILED_POST_CHECK
|
|
212
|
+
} else if (hasWarning) {
|
|
213
|
+
status = SCAN_STATUS.COMPLETED_WITH_WARNINGS
|
|
214
|
+
} else {
|
|
215
|
+
status = SCAN_STATUS.SUCCESS
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return { status, checks }
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* 将 postcheck 结果转换为结构化 JSON(SillyHub 可消费格式)
|
|
223
|
+
*
|
|
224
|
+
* failure_category 标准化:
|
|
225
|
+
* - warning : 非致命问题,不阻塞流程
|
|
226
|
+
* - error : 文档缺失/内容不完整,需要修复
|
|
227
|
+
* - critical : 安全问题(source_root 泄漏/路径污染)
|
|
228
|
+
*
|
|
229
|
+
* 结构化字段:
|
|
230
|
+
* - violations : 明确违反约束的条目(source_root 泄漏等)
|
|
231
|
+
* - missing_outputs : 预期文件不存在
|
|
232
|
+
* - path_pollution : 产物写入了错误路径
|
|
233
|
+
* - bad_references : 引用了不存在的命令/资源
|
|
234
|
+
* - quality_warnings: AI 输出中包含错误标记等质量信号
|
|
235
|
+
*
|
|
236
|
+
* @param {object} result - runScanPostCheck 返回值
|
|
237
|
+
* @param {object} [meta] - 附带元数据(workspace_id, scan_run_id, timestamp 等)
|
|
238
|
+
* @returns {object} 结构化 JSON
|
|
239
|
+
*/
|
|
240
|
+
export function formatStructuredResult(result, meta = {}) {
|
|
241
|
+
const structured = {
|
|
242
|
+
schema_version: 1,
|
|
243
|
+
generated_at: new Date().toISOString(),
|
|
244
|
+
overall_status: result.status,
|
|
245
|
+
// 路径溯源(供平台消费)
|
|
246
|
+
...(meta.workspace_id ? { workspace_id: meta.workspace_id } : {}),
|
|
247
|
+
...(meta.scan_run_id ? { scan_run_id: meta.scan_run_id } : {}),
|
|
248
|
+
...(meta.source_root ? { source_root: meta.source_root } : {}),
|
|
249
|
+
...(meta.spec_root ? { spec_root: meta.spec_root } : {}),
|
|
250
|
+
...(meta.runtime_root ? { runtime_root: meta.runtime_root } : {}),
|
|
251
|
+
summary: {
|
|
252
|
+
total_checks: result.checks.length,
|
|
253
|
+
critical: 0,
|
|
254
|
+
error: 0,
|
|
255
|
+
warning: 0,
|
|
256
|
+
},
|
|
257
|
+
failure_categories: {
|
|
258
|
+
violations: [],
|
|
259
|
+
missing_outputs: [],
|
|
260
|
+
path_pollution: [],
|
|
261
|
+
bad_references: [],
|
|
262
|
+
quality_warnings: [],
|
|
263
|
+
},
|
|
264
|
+
checks: result.checks.map(c => ({
|
|
265
|
+
name: c.name,
|
|
266
|
+
severity: c.severity === 'failed' ? 'critical' : c.severity,
|
|
267
|
+
detail: c.detail,
|
|
268
|
+
})),
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// 分类到 failure_categories
|
|
272
|
+
for (const check of result.checks) {
|
|
273
|
+
const severity = check.severity === 'failed' ? 'critical' : check.severity
|
|
274
|
+
const entry = { name: check.name, detail: check.detail, severity }
|
|
275
|
+
|
|
276
|
+
// 路径污染类
|
|
277
|
+
if (check.name === 'source_root_leak' || check.name === 'source_root_docs_leak') {
|
|
278
|
+
structured.failure_categories.path_pollution.push(entry)
|
|
279
|
+
structured.failure_categories.violations.push(entry)
|
|
280
|
+
}
|
|
281
|
+
// 文档缺失类
|
|
282
|
+
else if (check.name === 'all_docs_missing' || check.name === 'partial_docs_missing' || check.name === 'missing_docs') {
|
|
283
|
+
structured.failure_categories.missing_outputs.push(entry)
|
|
284
|
+
}
|
|
285
|
+
// 引用无效类
|
|
286
|
+
else if (check.name === 'local_config_invalid') {
|
|
287
|
+
structured.failure_categories.bad_references.push(entry)
|
|
288
|
+
}
|
|
289
|
+
// AI 输出质量类
|
|
290
|
+
else if (['api_error_529', 'rate_limit_exhausted'].includes(check.name)) {
|
|
291
|
+
structured.failure_categories.quality_warnings.push(entry)
|
|
292
|
+
}
|
|
293
|
+
// manifest/project 列表问题
|
|
294
|
+
else if (check.name === 'manifest_write_failed' || check.name === 'project_list_parse_failed') {
|
|
295
|
+
structured.failure_categories.violations.push(entry)
|
|
296
|
+
}
|
|
297
|
+
// 文档缺少 header
|
|
298
|
+
else if (check.name === 'docs_missing_header') {
|
|
299
|
+
structured.failure_categories.quality_warnings.push(entry)
|
|
300
|
+
}
|
|
301
|
+
// quick profile 说明(informational warning,非缺陷)
|
|
302
|
+
else if (check.name === 'quick_profile_notice') {
|
|
303
|
+
structured.failure_categories.quality_warnings.push(entry)
|
|
304
|
+
}
|
|
305
|
+
// 兜底:归入 violations
|
|
306
|
+
else {
|
|
307
|
+
structured.failure_categories.violations.push(entry)
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// 汇总计数
|
|
312
|
+
for (const check of result.checks) {
|
|
313
|
+
if (check.severity === 'failed') structured.summary.critical++
|
|
314
|
+
else structured.summary.warning++
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return structured
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* 将结构化结果写入 JSON 文件(平台模式供 SillyHub 消费)
|
|
322
|
+
*
|
|
323
|
+
* 本地模式:写入 specDir/.runtime/postcheck-result.json
|
|
324
|
+
* 平台模式:写入 runtimeRoot/scan-runs/{scan_run_id}/postcheck-result.json
|
|
325
|
+
*
|
|
326
|
+
* @param {object} structured - formatStructuredResult 返回值
|
|
327
|
+
* @param {string} specDir - 规范目录(本地模式使用)
|
|
328
|
+
* @param {object} [opts] - 平台模式选项
|
|
329
|
+
* @param {string} [opts.runtimeRoot] - 平台模式运行时根目录
|
|
330
|
+
* @param {string} [opts.scanRunId] - scan run ID
|
|
331
|
+
* @returns {string|null} 写入的文件路径,失败时返回 null
|
|
332
|
+
*/
|
|
333
|
+
export function writeStructuredResult(structured, specDir, opts = {}) {
|
|
334
|
+
if (!specDir && !opts.runtimeRoot) return null
|
|
335
|
+
try {
|
|
336
|
+
let outPath
|
|
337
|
+
if (opts.runtimeRoot && opts.scanRunId) {
|
|
338
|
+
const scanRunDir = join(opts.runtimeRoot, 'scan-runs', opts.scanRunId)
|
|
339
|
+
mkdirSync(scanRunDir, { recursive: true })
|
|
340
|
+
outPath = join(scanRunDir, 'postcheck-result.json')
|
|
341
|
+
} else if (specDir) {
|
|
342
|
+
const runtimeDir = join(specDir, '.runtime')
|
|
343
|
+
mkdirSync(runtimeDir, { recursive: true })
|
|
344
|
+
outPath = join(runtimeDir, 'postcheck-result.json')
|
|
345
|
+
} else {
|
|
346
|
+
return null
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
writeFileSync(outPath, JSON.stringify(structured, null, 2) + '\n')
|
|
350
|
+
return outPath
|
|
351
|
+
} catch (e) {
|
|
352
|
+
console.warn(` ⚠️ postcheck-result.json 写入失败: ${e.message}`)
|
|
353
|
+
return null
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* 打印 post-check 结果到 stdout
|
|
359
|
+
*/
|
|
360
|
+
export function printScanPostCheckResult(result) {
|
|
361
|
+
if (result.checks.length === 0) {
|
|
362
|
+
console.log(' ✅ CLI post-check: 全部通过')
|
|
363
|
+
return
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
for (const check of result.checks) {
|
|
367
|
+
const icon = check.severity === 'failed' ? '❌' : '⚠️'
|
|
368
|
+
console.log(` ${icon} CLI post-check [${check.name}]: ${check.detail}`)
|
|
369
|
+
}
|
|
370
|
+
console.log(` 📋 最终状态: ${result.status}`)
|
|
371
|
+
}
|