sillyspec 3.17.8 → 3.17.10
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/modules.js +63 -8
- package/src/run.js +181 -3
- package/src/scan-postcheck.js +133 -1
package/package.json
CHANGED
package/src/modules.js
CHANGED
|
@@ -47,7 +47,18 @@ function parseModuleCardFrontmatter(content) {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
/**
|
|
50
|
-
*
|
|
50
|
+
* 从模块卡片 + 源码重建 _module-map.yaml
|
|
51
|
+
*
|
|
52
|
+
* schema_version 2 扩展字段:
|
|
53
|
+
* - role: 模块职责描述
|
|
54
|
+
* - core_files: 核心源文件列表
|
|
55
|
+
* - test_files: 测试文件列表
|
|
56
|
+
* - entrypoints: 入口点
|
|
57
|
+
* - depends_on: 依赖的其他模块
|
|
58
|
+
* - used_by: 被哪些模块使用
|
|
59
|
+
* - risk_level: 风险等级(low/medium/high)
|
|
60
|
+
* - verify_commands: 验证命令(build/test/lint)
|
|
61
|
+
* - related_docs: 关联文档路径
|
|
51
62
|
*/
|
|
52
63
|
export async function rebuildModuleMap(cwd) {
|
|
53
64
|
const mapPath = findModuleMapPath(cwd);
|
|
@@ -107,10 +118,12 @@ export async function rebuildModuleMap(cwd) {
|
|
|
107
118
|
headCommit = execSync('git rev-parse --short HEAD', { cwd, encoding: 'utf8', timeout: 5000 }).trim();
|
|
108
119
|
} catch { /* ignore */ }
|
|
109
120
|
|
|
110
|
-
let yaml = `schema_version:
|
|
121
|
+
let yaml = `schema_version: 2\n`;
|
|
111
122
|
yaml += `generated_at: ${now}\n`;
|
|
112
123
|
yaml += `generator: sillyspec-modules-rebuild\n`;
|
|
113
124
|
if (headCommit) yaml += `source_commit: ${headCommit}\n`;
|
|
125
|
+
yaml += `\n# Module Context Index — 机器可读的模块上下文索引\n`;
|
|
126
|
+
yaml += `# scan 阶段自动生成,brainstorm/plan/execute 阶段按任务命中模块精准注入上下文\n`;
|
|
114
127
|
yaml += `\nmodules:\n`;
|
|
115
128
|
|
|
116
129
|
// 合并:已有映射 + 新卡片
|
|
@@ -121,13 +134,31 @@ export async function rebuildModuleMap(cwd) {
|
|
|
121
134
|
|
|
122
135
|
for (const moduleId of allModuleIds) {
|
|
123
136
|
const card = cards.find(c => c.moduleId === moduleId);
|
|
137
|
+
const cardContent = card ? readFileSync(join(modulesDir, card.filename), 'utf8') : '';
|
|
138
|
+
|
|
139
|
+
// 从模块卡片提取 context index 字段
|
|
140
|
+
const role = extractSection(cardContent, '定位') || '';
|
|
141
|
+
const contract = extractSection(cardContent, '契约摘要') || '';
|
|
142
|
+
const logic = extractSection(cardContent, '关键逻辑') || '';
|
|
143
|
+
const notes = extractSection(cardContent, '注意事项') || '';
|
|
144
|
+
|
|
124
145
|
yaml += ` ${moduleId}:\n`;
|
|
125
146
|
yaml += ` status: active\n`;
|
|
126
147
|
if (card) yaml += ` doc: modules/${card.filename}\n`;
|
|
127
148
|
else yaml += ` doc: modules/${moduleId}.md\n`;
|
|
128
|
-
// 保留已有 _module-map.yaml 中的 paths 等字段(如果有的话)
|
|
129
149
|
yaml += ` needs_review: false\n`;
|
|
130
150
|
yaml += ` review_reasons: []\n`;
|
|
151
|
+
// context index 字段(v2)
|
|
152
|
+
if (role) yaml += ` role: "${escapeYamlString(role)}"\n`;
|
|
153
|
+
// core_files / test_files / entrypoints 从已有 _module-map 保留,或从卡片文件名推导
|
|
154
|
+
const existingPaths = existingModules[moduleId]?.paths || [];
|
|
155
|
+
if (existingPaths.length > 0) {
|
|
156
|
+
yaml += ` core_files:\n`;
|
|
157
|
+
for (const p of existingPaths) yaml += ` - ${p}\n`;
|
|
158
|
+
}
|
|
159
|
+
if (card) yaml += ` verify_commands: []\n`;
|
|
160
|
+
yaml += ` risk_level: low\n`;
|
|
161
|
+
if (contract || logic) yaml += ` related_docs: []\n`;
|
|
131
162
|
yaml += `\n`;
|
|
132
163
|
}
|
|
133
164
|
|
|
@@ -138,6 +169,7 @@ export async function rebuildModuleMap(cwd) {
|
|
|
138
169
|
console.log(` - ${id}`);
|
|
139
170
|
}
|
|
140
171
|
console.log(`\n⚠️ 注意:rebuild 只重建骨架。tags/entrypoints/main_symbols/depends_on/used_by 需要重新运行 scan 或手动补充。`);
|
|
172
|
+
console.log(`ℹ️ schema 已升级到 v2,支持 role/core_files/test_files/entrypoints/depends_on/risk_level/verify_commands 等字段。`);
|
|
141
173
|
}
|
|
142
174
|
|
|
143
175
|
/**
|
|
@@ -163,13 +195,17 @@ export async function showModuleStatus(cwd) {
|
|
|
163
195
|
const moduleMatch = line.match(/^ ([a-zA-Z0-9_-]+):$/);
|
|
164
196
|
if (moduleMatch) {
|
|
165
197
|
if (currentModule) modules.push(currentModule);
|
|
166
|
-
currentModule = { id: moduleMatch[1], hasTags: false, hasEntryPoints: false, hasDepends: false, needsReview: false };
|
|
198
|
+
currentModule = { id: moduleMatch[1], hasTags: false, hasEntryPoints: false, hasDepends: false, needsReview: false, hasRole: false, hasCoreFiles: false, hasRisk: false, hasVerify: false };
|
|
167
199
|
}
|
|
168
200
|
if (currentModule) {
|
|
169
201
|
if (line.includes('tags:')) currentModule.hasTags = true;
|
|
170
202
|
if (line.includes('entrypoints:')) currentModule.hasEntryPoints = true;
|
|
171
203
|
if (line.includes('depends_on:')) currentModule.hasDepends = true;
|
|
172
204
|
if (line.includes('needs_review: true')) { currentModule.needsReview = true; needsReview = true; }
|
|
205
|
+
if (line.includes('role:')) currentModule.hasRole = true;
|
|
206
|
+
if (line.includes('core_files:')) currentModule.hasCoreFiles = true;
|
|
207
|
+
if (line.includes('risk_level:')) currentModule.hasRisk = true;
|
|
208
|
+
if (line.includes('verify_commands:')) currentModule.hasVerify = true;
|
|
173
209
|
}
|
|
174
210
|
}
|
|
175
211
|
if (currentModule) modules.push(currentModule);
|
|
@@ -178,14 +214,18 @@ export async function showModuleStatus(cwd) {
|
|
|
178
214
|
console.log(`模块数量:${modules.length}\n`);
|
|
179
215
|
|
|
180
216
|
const maxId = Math.max(5, ...modules.map(m => m.id.length));
|
|
181
|
-
console.log(` ${'模块'.padEnd(maxId)} tags entry deps review`);
|
|
182
|
-
console.log(` ${'
|
|
217
|
+
console.log(` ${'模块'.padEnd(maxId)} tags entry deps role core risk review`);
|
|
218
|
+
console.log(` ${''.padEnd(maxId, '─')} ──── ───── ──── ──── ──── ──── ──────`);
|
|
183
219
|
for (const m of modules) {
|
|
184
220
|
const tags = m.hasTags ? '✅' : '⬜';
|
|
185
221
|
const entry = m.hasEntryPoints ? '✅' : '⬜';
|
|
186
222
|
const deps = m.hasDepends ? '✅' : '⬜';
|
|
223
|
+
const role = m.hasRole ? '✅' : '⬜';
|
|
224
|
+
const core = m.hasCoreFiles ? '✅' : '⬜';
|
|
225
|
+
const risk = m.hasRisk ? '✅' : '⬜';
|
|
226
|
+
const verify = m.hasVerify ? '✅' : '⬜';
|
|
187
227
|
const review = m.needsReview ? '⚠️' : '✅';
|
|
188
|
-
console.log(` ${m.id.padEnd(maxId)} ${tags} ${entry} ${deps} ${review}`);
|
|
228
|
+
console.log(` ${m.id.padEnd(maxId)} ${tags} ${entry} ${deps} ${role} ${core} ${risk} ${verify} ${review}`);
|
|
189
229
|
}
|
|
190
230
|
|
|
191
231
|
if (needsReview) {
|
|
@@ -194,7 +234,22 @@ export async function showModuleStatus(cwd) {
|
|
|
194
234
|
}
|
|
195
235
|
|
|
196
236
|
/**
|
|
197
|
-
*
|
|
237
|
+
* 从模块卡片提取指定 section 的内容
|
|
238
|
+
*/
|
|
239
|
+
function extractSection(content, sectionName) {
|
|
240
|
+
const match = content.match(new RegExp(`## ${escapeRegExp(sectionName)}\\n([\\s\\S]*?)(?=\\n##|$)`));
|
|
241
|
+
return match ? match[1].trim().split('\n')[0] : ''; // 只取第一行作为摘要
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function escapeRegExp(s) {
|
|
245
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function escapeYamlString(s) {
|
|
249
|
+
return s.replace(/"/g, '\\"').replace(/\n/g, ' ').slice(0, 200); // 截断避免过长
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
198
253
|
*/
|
|
199
254
|
export async function generateDependenciesMd(cwd) {
|
|
200
255
|
const mapPath = findModuleMapPath(cwd);
|
package/src/run.js
CHANGED
|
@@ -64,6 +64,149 @@ import { buildExecuteSteps } from './stages/execute.js'
|
|
|
64
64
|
import { buildPlanSteps } from './stages/plan.js'
|
|
65
65
|
import { formatExecuteSummary } from './worktree-apply.js'
|
|
66
66
|
|
|
67
|
+
/**
|
|
68
|
+
* 从 _module-map.yaml 读取模块上下文索引
|
|
69
|
+
* 用于 brainstorm/plan/execute 阶段按任务命中模块精准注入上下文
|
|
70
|
+
*
|
|
71
|
+
* @param {string} specBase - 规范目录(.sillyspec 或 specRoot)
|
|
72
|
+
* @param {string} projectName - 项目名
|
|
73
|
+
* @returns {object|null} 解析后的模块索引,null 表示无索引
|
|
74
|
+
*/
|
|
75
|
+
function loadModuleContextIndex(specBase, projectName) {
|
|
76
|
+
try {
|
|
77
|
+
const { existsSync, readFileSync } = require('fs')
|
|
78
|
+
const { join } = require('path')
|
|
79
|
+
const mapPath = join(specBase, 'docs', projectName, 'modules', '_module-map.yaml')
|
|
80
|
+
if (!existsSync(mapPath)) return null
|
|
81
|
+
const content = readFileSync(mapPath, 'utf8')
|
|
82
|
+
return parseModuleMapSimple(content)
|
|
83
|
+
} catch {
|
|
84
|
+
return null
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 根据 AI 输出的任务描述,匹配相关模块并生成上下文注入文本
|
|
90
|
+
* 匹配策略:模块 id / role / doc 路径中的关键词
|
|
91
|
+
*
|
|
92
|
+
* @param {string} taskDescription - 任务描述(来自 plan.md / step prompt / outputText)
|
|
93
|
+
* @param {object} moduleIndex - loadModuleContextIndex 返回值
|
|
94
|
+
* @param {string} specBase - 规范目录
|
|
95
|
+
* @param {string} projectName - 项目名
|
|
96
|
+
* @returns {string} 上下文注入文本,空字符串表示无匹配模块
|
|
97
|
+
*/
|
|
98
|
+
function buildModuleContextInjection(taskDescription, moduleIndex, specBase, projectName) {
|
|
99
|
+
if (!moduleIndex || !taskDescription) return ''
|
|
100
|
+
const { existsSync } = require('fs')
|
|
101
|
+
const { join } = require('path')
|
|
102
|
+
|
|
103
|
+
const taskLower = taskDescription.toLowerCase()
|
|
104
|
+
const matched = []
|
|
105
|
+
|
|
106
|
+
for (const [moduleId, data] of Object.entries(moduleIndex)) {
|
|
107
|
+
let score = 0
|
|
108
|
+
let matchReasons = []
|
|
109
|
+
// 模块 id 匹配
|
|
110
|
+
if (taskLower.includes(moduleId.toLowerCase())) { score += 3; matchReasons.push(`id:${moduleId}`) }
|
|
111
|
+
// role 描述匹配
|
|
112
|
+
if (data.role && taskLower.includes(data.role.toLowerCase())) { score += 2; matchReasons.push('role') }
|
|
113
|
+
// core_files 路径匹配
|
|
114
|
+
const coreFiles = data.paths || data.core_files || []
|
|
115
|
+
for (const p of coreFiles) {
|
|
116
|
+
if (taskLower.includes(p.toLowerCase())) { score += 1; matchReasons.push(`file:${p}`); break }
|
|
117
|
+
}
|
|
118
|
+
if (score > 0) matched.push({ moduleId, data, score, matchReasons })
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (matched.length === 0) return ''
|
|
122
|
+
|
|
123
|
+
matched.sort((a, b) => b.score - a.score)
|
|
124
|
+
|
|
125
|
+
let injection = '\n### 📦 模块上下文(按相关性排序,来自 Module Context Index)\n\n'
|
|
126
|
+
injection += `> 以下模块上下文由 scan 阶段生成的 _module-map.yaml 自动匹配。\n`
|
|
127
|
+
injection += `> Matched modules: ${matched.map(m => m.moduleId).join(', ')}\n`
|
|
128
|
+
injection += `> Reasons: ${matched.map(m => m.matchReasons.join(', ')).join('; ')}\n\n`
|
|
129
|
+
|
|
130
|
+
for (const { moduleId, data } of matched) {
|
|
131
|
+
injection += `#### ${moduleId}\n`
|
|
132
|
+
if (data.role) injection += `- **职责**: ${String(data.role).slice(0, 100)}\n`
|
|
133
|
+
const riskLevel = data.risk_level || 'medium'
|
|
134
|
+
injection += `- **风险等级**: ${riskLevel}\n`
|
|
135
|
+
const coreFiles = data.paths || data.core_files || []
|
|
136
|
+
if (coreFiles.length > 0) injection += `- **核心文件**: ${coreFiles.join(', ')}\n`
|
|
137
|
+
if (data.doc) {
|
|
138
|
+
const docPath = join(specBase, 'docs', projectName, data.doc)
|
|
139
|
+
const exists = existsSync(docPath)
|
|
140
|
+
injection += `- **模块文档**: ${data.doc}${exists ? ' ✅' : ' ⚠️ 不存在'}\n`
|
|
141
|
+
}
|
|
142
|
+
const deps = data.depends_on || []
|
|
143
|
+
if (deps.length > 0) injection += `- **依赖**: ${deps.join(', ')}\n`
|
|
144
|
+
const usedBy = data.used_by || []
|
|
145
|
+
if (usedBy.length > 0) injection += `- **被引用**: ${usedBy.join(', ')}\n`
|
|
146
|
+
injection += '\n'
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return injection
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 复用 modules.js 的简单 YAML 解析(避免循环依赖)
|
|
153
|
+
function parseModuleMapSimple(content) {
|
|
154
|
+
const modules = {}
|
|
155
|
+
let currentModule = null
|
|
156
|
+
let currentKey = null
|
|
157
|
+
let currentArray = null
|
|
158
|
+
|
|
159
|
+
for (const line of content.split('\n')) {
|
|
160
|
+
const moduleMatch = line.match(/^ ([a-zA-Z0-9_-]+):$/)
|
|
161
|
+
if (moduleMatch) {
|
|
162
|
+
if (currentArray && currentModule && currentKey) {
|
|
163
|
+
modules[currentModule][currentKey] = currentArray
|
|
164
|
+
}
|
|
165
|
+
currentModule = moduleMatch[1]
|
|
166
|
+
modules[currentModule] = {}
|
|
167
|
+
currentKey = null
|
|
168
|
+
currentArray = null
|
|
169
|
+
continue
|
|
170
|
+
}
|
|
171
|
+
if (!currentModule) continue
|
|
172
|
+
|
|
173
|
+
const arrayFieldMatch = line.match(/^ (depends_on|used_by|paths|tags|aliases|entrypoints|main_symbols|review_reasons|core_files|test_files|related_docs|verify_commands):$/)
|
|
174
|
+
if (arrayFieldMatch) {
|
|
175
|
+
if (currentArray && currentKey) modules[currentModule][currentKey] = currentArray
|
|
176
|
+
currentKey = arrayFieldMatch[1]
|
|
177
|
+
currentArray = []
|
|
178
|
+
continue
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const inlineArrayMatch = line.match(/^ (depends_on|used_by|paths|tags|aliases|entrypoints|main_symbols|review_reasons|core_files|test_files|related_docs|verify_commands): \[(.*)\]$/)
|
|
182
|
+
if (inlineArrayMatch) {
|
|
183
|
+
if (currentArray && currentKey) modules[currentModule][currentKey] = currentArray
|
|
184
|
+
const vals = inlineArrayMatch[2].split(',').map(v => v.trim()).filter(Boolean)
|
|
185
|
+
modules[currentModule][inlineArrayMatch[1]] = vals
|
|
186
|
+
currentKey = null
|
|
187
|
+
currentArray = null
|
|
188
|
+
continue
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const scalarMatch = line.match(/^ (status|doc|needs_review|role|risk_level): (.+)$/)
|
|
192
|
+
if (scalarMatch) {
|
|
193
|
+
if (currentArray && currentKey) { modules[currentModule][currentKey] = currentArray; currentArray = null; currentKey = null }
|
|
194
|
+
modules[currentModule][scalarMatch[1]] = scalarMatch[2]
|
|
195
|
+
continue
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const itemMatch = line.match(/^ - (.+)$/)
|
|
199
|
+
if (itemMatch && currentArray !== null) {
|
|
200
|
+
currentArray.push(itemMatch[1].trim())
|
|
201
|
+
continue
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (currentArray && currentModule && currentKey) {
|
|
205
|
+
modules[currentModule][currentKey] = currentArray
|
|
206
|
+
}
|
|
207
|
+
return modules
|
|
208
|
+
}
|
|
209
|
+
|
|
67
210
|
/**
|
|
68
211
|
* 同步触发辅助函数:_write 后 best-effort 同步到平台
|
|
69
212
|
*/
|
|
@@ -487,6 +630,19 @@ async function outputStep(stageName, stepIndex, steps, cwd, changeName, dbProjec
|
|
|
487
630
|
}
|
|
488
631
|
}
|
|
489
632
|
|
|
633
|
+
// 注入模块上下文(brainstorm/plan/execute 阶段,基于 Module Context Index)
|
|
634
|
+
if (['brainstorm', 'plan', 'execute'].includes(stageName) && projectName) {
|
|
635
|
+
const moduleIndex = loadModuleContextIndex(specBase || join(cwd, '.sillyspec'), projectName)
|
|
636
|
+
if (moduleIndex && Object.keys(moduleIndex).length > 0) {
|
|
637
|
+
// 尝试从 step prompt / changeName 匹配模块
|
|
638
|
+
const taskDesc = step.prompt || changeName || ''
|
|
639
|
+
const injection = buildModuleContextInjection(taskDesc, moduleIndex, specBase || join(cwd, '.sillyspec'), projectName)
|
|
640
|
+
if (injection) {
|
|
641
|
+
promptText = injection + '\n' + promptText
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
490
646
|
// 平台模式 prompt 自检:确保没有裸相对输出路径
|
|
491
647
|
// 只匹配正向写入指令中的裸路径,避免误杀「禁止写入 .sillyspec/」等安全说明
|
|
492
648
|
if ((platformOpts?.specRoot || platformOpts?.runtimeRoot) && stageName === 'scan') {
|
|
@@ -1749,7 +1905,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
1749
1905
|
try { unlinkSync(platformOptsFile) } catch {}
|
|
1750
1906
|
|
|
1751
1907
|
// CLI 层 post-check(替代旧的简单检查)
|
|
1752
|
-
const { runScanPostCheck, printScanPostCheckResult } = await import('./scan-postcheck.js')
|
|
1908
|
+
const { runScanPostCheck, printScanPostCheckResult, formatStructuredResult, writeStructuredResult } = await import('./scan-postcheck.js')
|
|
1753
1909
|
const postResult = runScanPostCheck({
|
|
1754
1910
|
cwd,
|
|
1755
1911
|
specDir: platformOpts.specRoot,
|
|
@@ -1761,6 +1917,22 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
1761
1917
|
})
|
|
1762
1918
|
printScanPostCheckResult(postResult)
|
|
1763
1919
|
|
|
1920
|
+
// 生成结构化 JSON 并写入 runtime(供 SillyHub 消费)
|
|
1921
|
+
const structured = formatStructuredResult(postResult, {
|
|
1922
|
+
workspace_id: platformOpts.workspaceId,
|
|
1923
|
+
scan_run_id: platformOpts.scanRunId,
|
|
1924
|
+
source_root: cwd,
|
|
1925
|
+
spec_root: platformOpts.specRoot,
|
|
1926
|
+
runtime_root: platformOpts.runtimeRoot,
|
|
1927
|
+
})
|
|
1928
|
+
const postcheckJsonPath = writeStructuredResult(structured, platformOpts.specRoot, {
|
|
1929
|
+
runtimeRoot: platformOpts.runtimeRoot,
|
|
1930
|
+
scanRunId: platformOpts.scanRunId,
|
|
1931
|
+
})
|
|
1932
|
+
if (postcheckJsonPath) {
|
|
1933
|
+
console.log(`📄 postcheck-result.json 已写入: ${postcheckJsonPath}`)
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1764
1936
|
// 将 post-check 结果写入 manifest
|
|
1765
1937
|
manifest.scan_post_check = {
|
|
1766
1938
|
status: postResult.status,
|
|
@@ -1788,11 +1960,17 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
1788
1960
|
}
|
|
1789
1961
|
}
|
|
1790
1962
|
|
|
1791
|
-
// 非 platform 模式 scan 也做轻量 post-check
|
|
1963
|
+
// 非 platform 模式 scan 也做轻量 post-check + 结构化输出
|
|
1792
1964
|
if (stageName === 'scan' && !platformOpts.specRoot && !platformOpts.runtimeRoot) {
|
|
1793
|
-
const { runScanPostCheck, printScanPostCheckResult } = await import('./scan-postcheck.js')
|
|
1965
|
+
const { runScanPostCheck, printScanPostCheckResult, formatStructuredResult, writeStructuredResult } = await import('./scan-postcheck.js')
|
|
1794
1966
|
const postResult = runScanPostCheck({ cwd, specDir: null, outputText })
|
|
1795
1967
|
printScanPostCheckResult(postResult)
|
|
1968
|
+
// 结构化结果写入 .sillyspec/.runtime/
|
|
1969
|
+
const structured = formatStructuredResult(postResult, { source_root: cwd })
|
|
1970
|
+
const postcheckJsonPath = writeStructuredResult(structured, join(cwd, '.sillyspec'))
|
|
1971
|
+
if (postcheckJsonPath) {
|
|
1972
|
+
console.log(`📄 postcheck-result.json 已写入: ${postcheckJsonPath}`)
|
|
1973
|
+
}
|
|
1796
1974
|
}
|
|
1797
1975
|
|
|
1798
1976
|
validateMetadata(cwd, stageName, specBase)
|
package/src/scan-postcheck.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* 平台模式下必须通过所有 check 才能 success,否则降级。
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { existsSync, readdirSync, readFileSync } from 'fs'
|
|
8
|
+
import { existsSync, readdirSync, readFileSync, mkdirSync, writeFileSync } from 'fs'
|
|
9
9
|
import { join, basename } from 'path'
|
|
10
10
|
|
|
11
11
|
const REQUIRED_SCAN_DOCS = [
|
|
@@ -198,6 +198,138 @@ export function runScanPostCheck({ cwd, specDir, outputText = '', scanMeta = {}
|
|
|
198
198
|
return { status, checks }
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
+
/**
|
|
202
|
+
* 将 postcheck 结果转换为结构化 JSON(SillyHub 可消费格式)
|
|
203
|
+
*
|
|
204
|
+
* failure_category 标准化:
|
|
205
|
+
* - warning : 非致命问题,不阻塞流程
|
|
206
|
+
* - error : 文档缺失/内容不完整,需要修复
|
|
207
|
+
* - critical : 安全问题(source_root 泄漏/路径污染)
|
|
208
|
+
*
|
|
209
|
+
* 结构化字段:
|
|
210
|
+
* - violations : 明确违反约束的条目(source_root 泄漏等)
|
|
211
|
+
* - missing_outputs : 预期文件不存在
|
|
212
|
+
* - path_pollution : 产物写入了错误路径
|
|
213
|
+
* - bad_references : 引用了不存在的命令/资源
|
|
214
|
+
* - quality_warnings: AI 输出中包含错误标记等质量信号
|
|
215
|
+
*
|
|
216
|
+
* @param {object} result - runScanPostCheck 返回值
|
|
217
|
+
* @param {object} [meta] - 附带元数据(workspace_id, scan_run_id, timestamp 等)
|
|
218
|
+
* @returns {object} 结构化 JSON
|
|
219
|
+
*/
|
|
220
|
+
export function formatStructuredResult(result, meta = {}) {
|
|
221
|
+
const structured = {
|
|
222
|
+
schema_version: 1,
|
|
223
|
+
generated_at: new Date().toISOString(),
|
|
224
|
+
overall_status: result.status,
|
|
225
|
+
// 路径溯源(供平台消费)
|
|
226
|
+
...(meta.workspace_id ? { workspace_id: meta.workspace_id } : {}),
|
|
227
|
+
...(meta.scan_run_id ? { scan_run_id: meta.scan_run_id } : {}),
|
|
228
|
+
...(meta.source_root ? { source_root: meta.source_root } : {}),
|
|
229
|
+
...(meta.spec_root ? { spec_root: meta.spec_root } : {}),
|
|
230
|
+
...(meta.runtime_root ? { runtime_root: meta.runtime_root } : {}),
|
|
231
|
+
summary: {
|
|
232
|
+
total_checks: result.checks.length,
|
|
233
|
+
critical: 0,
|
|
234
|
+
error: 0,
|
|
235
|
+
warning: 0,
|
|
236
|
+
},
|
|
237
|
+
failure_categories: {
|
|
238
|
+
violations: [],
|
|
239
|
+
missing_outputs: [],
|
|
240
|
+
path_pollution: [],
|
|
241
|
+
bad_references: [],
|
|
242
|
+
quality_warnings: [],
|
|
243
|
+
},
|
|
244
|
+
checks: result.checks.map(c => ({
|
|
245
|
+
name: c.name,
|
|
246
|
+
severity: c.severity === 'failed' ? 'critical' : c.severity,
|
|
247
|
+
detail: c.detail,
|
|
248
|
+
})),
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// 分类到 failure_categories
|
|
252
|
+
for (const check of result.checks) {
|
|
253
|
+
const severity = check.severity === 'failed' ? 'critical' : check.severity
|
|
254
|
+
const entry = { name: check.name, detail: check.detail, severity }
|
|
255
|
+
|
|
256
|
+
// 路径污染类
|
|
257
|
+
if (check.name === 'source_root_leak') {
|
|
258
|
+
structured.failure_categories.path_pollution.push(entry)
|
|
259
|
+
structured.failure_categories.violations.push(entry)
|
|
260
|
+
}
|
|
261
|
+
// 文档缺失类
|
|
262
|
+
else if (check.name === 'all_docs_missing' || check.name === 'partial_docs_missing' || check.name === 'missing_docs') {
|
|
263
|
+
structured.failure_categories.missing_outputs.push(entry)
|
|
264
|
+
}
|
|
265
|
+
// 引用无效类
|
|
266
|
+
else if (check.name === 'local_config_invalid') {
|
|
267
|
+
structured.failure_categories.bad_references.push(entry)
|
|
268
|
+
}
|
|
269
|
+
// AI 输出质量类
|
|
270
|
+
else if (['tool_use_error', 'api_error_529', 'rate_limit_exhausted', 'fallback_or_skip'].includes(check.name)) {
|
|
271
|
+
structured.failure_categories.quality_warnings.push(entry)
|
|
272
|
+
}
|
|
273
|
+
// manifest/project 列表问题
|
|
274
|
+
else if (check.name === 'manifest_write_failed' || check.name === 'project_list_parse_failed') {
|
|
275
|
+
structured.failure_categories.violations.push(entry)
|
|
276
|
+
}
|
|
277
|
+
// 文档缺少 header
|
|
278
|
+
else if (check.name === 'docs_missing_header') {
|
|
279
|
+
structured.failure_categories.quality_warnings.push(entry)
|
|
280
|
+
}
|
|
281
|
+
// 兜底:归入 violations
|
|
282
|
+
else {
|
|
283
|
+
structured.failure_categories.violations.push(entry)
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// 汇总计数
|
|
288
|
+
for (const check of result.checks) {
|
|
289
|
+
if (check.severity === 'failed') structured.summary.critical++
|
|
290
|
+
else structured.summary.warning++
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return structured
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* 将结构化结果写入 JSON 文件(平台模式供 SillyHub 消费)
|
|
298
|
+
*
|
|
299
|
+
* 本地模式:写入 specDir/.runtime/postcheck-result.json
|
|
300
|
+
* 平台模式:写入 runtimeRoot/scan-runs/{scan_run_id}/postcheck-result.json
|
|
301
|
+
*
|
|
302
|
+
* @param {object} structured - formatStructuredResult 返回值
|
|
303
|
+
* @param {string} specDir - 规范目录(本地模式使用)
|
|
304
|
+
* @param {object} [opts] - 平台模式选项
|
|
305
|
+
* @param {string} [opts.runtimeRoot] - 平台模式运行时根目录
|
|
306
|
+
* @param {string} [opts.scanRunId] - scan run ID
|
|
307
|
+
* @returns {string|null} 写入的文件路径,失败时返回 null
|
|
308
|
+
*/
|
|
309
|
+
export function writeStructuredResult(structured, specDir, opts = {}) {
|
|
310
|
+
if (!specDir && !opts.runtimeRoot) return null
|
|
311
|
+
try {
|
|
312
|
+
let outPath
|
|
313
|
+
if (opts.runtimeRoot && opts.scanRunId) {
|
|
314
|
+
const scanRunDir = join(opts.runtimeRoot, 'scan-runs', opts.scanRunId)
|
|
315
|
+
mkdirSync(scanRunDir, { recursive: true })
|
|
316
|
+
outPath = join(scanRunDir, 'postcheck-result.json')
|
|
317
|
+
} else if (specDir) {
|
|
318
|
+
const runtimeDir = join(specDir, '.runtime')
|
|
319
|
+
mkdirSync(runtimeDir, { recursive: true })
|
|
320
|
+
outPath = join(runtimeDir, 'postcheck-result.json')
|
|
321
|
+
} else {
|
|
322
|
+
return null
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
writeFileSync(outPath, JSON.stringify(structured, null, 2) + '\n')
|
|
326
|
+
return outPath
|
|
327
|
+
} catch (e) {
|
|
328
|
+
console.warn(` ⚠️ postcheck-result.json 写入失败: ${e.message}`)
|
|
329
|
+
return null
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
201
333
|
/**
|
|
202
334
|
* 打印 post-check 结果到 stdout
|
|
203
335
|
*/
|