sillyspec 3.17.7 → 3.17.9
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 +255 -7
- package/src/scan-postcheck.js +160 -14
- package/src/stages/quick.js +5 -3
- package/src/stages/scan.js +6 -6
- package/test/platform-scan-p0.test.mjs +172 -0
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
|
@@ -10,6 +10,24 @@ import { createRequire } from 'module'
|
|
|
10
10
|
const require = createRequire(import.meta.url)
|
|
11
11
|
import { ProgressManager } from './progress.js'
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* 在容器/Docker 环境下,git 可能因目录所有权不匹配报 dubious ownership。
|
|
15
|
+
* 使用 -c safe.directory= 临时参数,不污染全局 git config。
|
|
16
|
+
* @param {string} cwd - 仓库根目录
|
|
17
|
+
* @param {string[]} args - git 子命令及参数,如 ['rev-parse', 'HEAD']
|
|
18
|
+
* @returns {{ value: string, error: string|null }}
|
|
19
|
+
*/
|
|
20
|
+
function safeGit(cwd, args) {
|
|
21
|
+
const { execSync } = require('child_process')
|
|
22
|
+
const fullArgs = ['-c', `safe.directory=${cwd}`, '-C', cwd, ...args]
|
|
23
|
+
try {
|
|
24
|
+
const value = execSync(['git', ...fullArgs].join(' '), { encoding: 'utf8', timeout: 5000 }).trim()
|
|
25
|
+
return { value, error: null }
|
|
26
|
+
} catch (e) {
|
|
27
|
+
return { value: null, error: e.message.split('\n')[0] }
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
13
31
|
// ── Wait State Constants ──
|
|
14
32
|
// 正则匹配:只识别独立一行的标记,避免误伤文档正文引用
|
|
15
33
|
const WAIT_MARKER_RE = /^\s*\[(WAIT_FOR_USER|NEEDS_CONFIRM|NEEDS_DECISION)\]\s*$/m
|
|
@@ -46,6 +64,160 @@ import { buildExecuteSteps } from './stages/execute.js'
|
|
|
46
64
|
import { buildPlanSteps } from './stages/plan.js'
|
|
47
65
|
import { formatExecuteSummary } from './worktree-apply.js'
|
|
48
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
|
+
const MAX_MODULE_CONTEXT_CHARS = 4096 // 上下文注入硬限制(字节),防止 prompt 膨胀
|
|
99
|
+
const MAX_MODULES_PER_INJECTION = 3 // 最多注入的模块数
|
|
100
|
+
const MAX_FILES_PER_MODULE = 8 // 每个模块最多展示的文件数
|
|
101
|
+
|
|
102
|
+
function buildModuleContextInjection(taskDescription, moduleIndex, specBase, projectName) {
|
|
103
|
+
if (!moduleIndex || !taskDescription) return ''
|
|
104
|
+
const { existsSync } = require('fs')
|
|
105
|
+
const { join } = require('path')
|
|
106
|
+
|
|
107
|
+
const taskLower = taskDescription.toLowerCase()
|
|
108
|
+
const matched = []
|
|
109
|
+
|
|
110
|
+
for (const [moduleId, data] of Object.entries(moduleIndex)) {
|
|
111
|
+
let score = 0
|
|
112
|
+
let matchReasons = []
|
|
113
|
+
// 模块 id 匹配
|
|
114
|
+
if (taskLower.includes(moduleId.toLowerCase())) { score += 3; matchReasons.push(`id:${moduleId}`) }
|
|
115
|
+
// role 描述匹配
|
|
116
|
+
if (data.role && taskLower.includes(data.role.toLowerCase())) { score += 2; matchReasons.push('role') }
|
|
117
|
+
// core_files 路径匹配
|
|
118
|
+
const coreFiles = data.paths || data.core_files || []
|
|
119
|
+
for (const p of coreFiles) {
|
|
120
|
+
if (taskLower.includes(p.toLowerCase())) { score += 1; matchReasons.push(`file:${p}`); break } // 文件匹配只计一次
|
|
121
|
+
}
|
|
122
|
+
if (score > 0) matched.push({ moduleId, data, score, matchReasons })
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (matched.length === 0) return ''
|
|
126
|
+
|
|
127
|
+
matched.sort((a, b) => b.score - a.score)
|
|
128
|
+
const top = matched.slice(0, MAX_MODULES_PER_INJECTION)
|
|
129
|
+
|
|
130
|
+
let injection = '\n### 📦 模块上下文(按相关性排序,来自 Module Context Index)\n\n'
|
|
131
|
+
injection += `> 以下模块上下文由 scan 阶段生成的 _module-map.yaml 自动匹配。\n`
|
|
132
|
+
injection += `> Matched modules: ${top.map(m => m.moduleId).join(', ')}\n`
|
|
133
|
+
injection += `> Reasons: ${top.map(m => m.matchReasons.join(', ')).join('; ')}\n\n`
|
|
134
|
+
|
|
135
|
+
for (const { moduleId, data } of top) {
|
|
136
|
+
injection += `#### ${moduleId}\n`
|
|
137
|
+
if (data.role) injection += `- **职责**: ${String(data.role).slice(0, 100)}\n`
|
|
138
|
+
const riskLevel = data.risk_level || 'medium'
|
|
139
|
+
injection += `- **风险等级**: ${riskLevel}\n`
|
|
140
|
+
const coreFiles = (data.paths || data.core_files || []).slice(0, MAX_FILES_PER_MODULE)
|
|
141
|
+
if (coreFiles.length > 0) injection += `- **核心文件**: ${coreFiles.join(', ')}\n`
|
|
142
|
+
if (data.doc) {
|
|
143
|
+
const docPath = join(specBase, 'docs', projectName, data.doc)
|
|
144
|
+
const exists = existsSync(docPath)
|
|
145
|
+
injection += `- **模块文档**: ${data.doc}${exists ? ' ✅' : ' ⚠️ 不存在'}\n`
|
|
146
|
+
}
|
|
147
|
+
const deps = data.depends_on || []
|
|
148
|
+
if (deps.length > 0) injection += `- **依赖**: ${deps.slice(0, 5).join(', ')}\n`
|
|
149
|
+
const usedBy = data.used_by || []
|
|
150
|
+
if (usedBy.length > 0) injection += `- **被引用**: ${usedBy.slice(0, 5).join(', ')}\n`
|
|
151
|
+
injection += '\n'
|
|
152
|
+
|
|
153
|
+
// 长度硬限制:超过阈值截断
|
|
154
|
+
if (injection.length > MAX_MODULE_CONTEXT_CHARS) {
|
|
155
|
+
injection += `<!-- Module context truncated: exceeded ${MAX_MODULE_CONTEXT_CHARS} chars -->\n`
|
|
156
|
+
break
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return injection
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// 复用 modules.js 的简单 YAML 解析(避免循环依赖)
|
|
164
|
+
function parseModuleMapSimple(content) {
|
|
165
|
+
const modules = {}
|
|
166
|
+
let currentModule = null
|
|
167
|
+
let currentKey = null
|
|
168
|
+
let currentArray = null
|
|
169
|
+
|
|
170
|
+
for (const line of content.split('\n')) {
|
|
171
|
+
const moduleMatch = line.match(/^ ([a-zA-Z0-9_-]+):$/)
|
|
172
|
+
if (moduleMatch) {
|
|
173
|
+
if (currentArray && currentModule && currentKey) {
|
|
174
|
+
modules[currentModule][currentKey] = currentArray
|
|
175
|
+
}
|
|
176
|
+
currentModule = moduleMatch[1]
|
|
177
|
+
modules[currentModule] = {}
|
|
178
|
+
currentKey = null
|
|
179
|
+
currentArray = null
|
|
180
|
+
continue
|
|
181
|
+
}
|
|
182
|
+
if (!currentModule) continue
|
|
183
|
+
|
|
184
|
+
const arrayFieldMatch = line.match(/^ (depends_on|used_by|paths|tags|aliases|entrypoints|main_symbols|review_reasons|core_files|test_files|related_docs|verify_commands):$/)
|
|
185
|
+
if (arrayFieldMatch) {
|
|
186
|
+
if (currentArray && currentKey) modules[currentModule][currentKey] = currentArray
|
|
187
|
+
currentKey = arrayFieldMatch[1]
|
|
188
|
+
currentArray = []
|
|
189
|
+
continue
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const inlineArrayMatch = line.match(/^ (depends_on|used_by|paths|tags|aliases|entrypoints|main_symbols|review_reasons|core_files|test_files|related_docs|verify_commands): \[(.*)\]$/)
|
|
193
|
+
if (inlineArrayMatch) {
|
|
194
|
+
if (currentArray && currentKey) modules[currentModule][currentKey] = currentArray
|
|
195
|
+
const vals = inlineArrayMatch[2].split(',').map(v => v.trim()).filter(Boolean)
|
|
196
|
+
modules[currentModule][inlineArrayMatch[1]] = vals
|
|
197
|
+
currentKey = null
|
|
198
|
+
currentArray = null
|
|
199
|
+
continue
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const scalarMatch = line.match(/^ (status|doc|needs_review|role|risk_level): (.+)$/)
|
|
203
|
+
if (scalarMatch) {
|
|
204
|
+
if (currentArray && currentKey) { modules[currentModule][currentKey] = currentArray; currentArray = null; currentKey = null }
|
|
205
|
+
modules[currentModule][scalarMatch[1]] = scalarMatch[2]
|
|
206
|
+
continue
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const itemMatch = line.match(/^ - (.+)$/)
|
|
210
|
+
if (itemMatch && currentArray !== null) {
|
|
211
|
+
currentArray.push(itemMatch[1].trim())
|
|
212
|
+
continue
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (currentArray && currentModule && currentKey) {
|
|
216
|
+
modules[currentModule][currentKey] = currentArray
|
|
217
|
+
}
|
|
218
|
+
return modules
|
|
219
|
+
}
|
|
220
|
+
|
|
49
221
|
/**
|
|
50
222
|
* 同步触发辅助函数:_write 后 best-effort 同步到平台
|
|
51
223
|
*/
|
|
@@ -129,6 +301,21 @@ async function auditQuickCompletion(cwd, guard, options = {}) {
|
|
|
129
301
|
result.status = 'warning'
|
|
130
302
|
}
|
|
131
303
|
|
|
304
|
+
// quicklog 存在性检查
|
|
305
|
+
try {
|
|
306
|
+
const quicklogDir = join(cwd, '.sillyspec', 'quicklog')
|
|
307
|
+
if (existsSync(quicklogDir)) {
|
|
308
|
+
const qlFiles = readdirSync(quicklogDir).filter(f => f.endsWith('.md'))
|
|
309
|
+
if (qlFiles.length === 0) {
|
|
310
|
+
result.reasons.push('quicklog 目录为空(无任务记录)')
|
|
311
|
+
if (result.status === 'safe') result.status = 'warning'
|
|
312
|
+
}
|
|
313
|
+
} else {
|
|
314
|
+
result.reasons.push('quicklog 目录不存在(agent 未创建任务记录)')
|
|
315
|
+
if (result.status === 'safe') result.status = 'warning'
|
|
316
|
+
}
|
|
317
|
+
} catch {}
|
|
318
|
+
|
|
132
319
|
// --confirm 模式:展示 diff 并等待确认
|
|
133
320
|
if (isConfirm && (result.status === 'warning' || result.status === 'blocked')) {
|
|
134
321
|
console.log(`\n📋 quick 变更概览:`)
|
|
@@ -351,7 +538,7 @@ async function outputStep(stageName, stepIndex, steps, cwd, changeName, dbProjec
|
|
|
351
538
|
if (promptText.includes('<git-user>')) {
|
|
352
539
|
const { execSync } = await import('child_process')
|
|
353
540
|
try {
|
|
354
|
-
const gitUser =
|
|
541
|
+
const gitUser = safeGit(cwd, ['config', 'user.name']).value || 'unknown'
|
|
355
542
|
promptText = promptText.replace(/<git-user>/g, gitUser)
|
|
356
543
|
} catch {
|
|
357
544
|
promptText = promptText.replace(/<git-user>/g, 'unknown')
|
|
@@ -377,9 +564,14 @@ async function outputStep(stageName, stepIndex, steps, cwd, changeName, dbProjec
|
|
|
377
564
|
const docsRoot = join(specSillyspec, 'docs', projectName)
|
|
378
565
|
const projectsRoot = join(specSillyspec, 'projects')
|
|
379
566
|
const changesRoot = join(specSillyspec, 'changes')
|
|
567
|
+
const workflowsRoot = join(specSillyspec, 'workflows')
|
|
568
|
+
const knowledgeRoot = join(specSillyspec, 'knowledge')
|
|
380
569
|
|
|
381
570
|
promptText = promptText.replace(/\{DOCS_ROOT\}/g, docsRoot)
|
|
382
571
|
promptText = promptText.replace(/\{PROJECTS_ROOT\}/g, projectsRoot)
|
|
572
|
+
promptText = promptText.replace(/\{WORKFLOWS_ROOT\}/g, workflowsRoot)
|
|
573
|
+
promptText = promptText.replace(/\{KNOWLEDGE_ROOT\}/g, knowledgeRoot)
|
|
574
|
+
promptText = promptText.replace(/\{SPEC_ROOT\}/g, specSillyspec)
|
|
383
575
|
|
|
384
576
|
const platformDirectives = []
|
|
385
577
|
platformDirectives.push(
|
|
@@ -389,6 +581,8 @@ async function outputStep(stageName, stepIndex, steps, cwd, changeName, dbProjec
|
|
|
389
581
|
`- 文档根目录: \`${docsRoot}/\`\n` +
|
|
390
582
|
`- 项目注册表: \`${projectsRoot}/\`\n` +
|
|
391
583
|
`- 变更目录: \`${changesRoot}/\`\n` +
|
|
584
|
+
`- 工作流目录: \`${workflowsRoot}/\`\n` +
|
|
585
|
+
`- 术语目录: \`${knowledgeRoot}/\`\n` +
|
|
392
586
|
`\n` +
|
|
393
587
|
`### ⛔ 写入规则\n` +
|
|
394
588
|
`1. **所有文档、配置、产物只能写入上述路径**。严禁写入源码目录或相对路径 \`.sillyspec/\`。\n` +
|
|
@@ -437,8 +631,38 @@ async function outputStep(stageName, stepIndex, steps, cwd, changeName, dbProjec
|
|
|
437
631
|
const specSillyspec = join(cwd, '.sillyspec')
|
|
438
632
|
const docsRoot = join(specSillyspec, 'docs', projectName)
|
|
439
633
|
const projectsRoot = join(specSillyspec, 'projects')
|
|
634
|
+
const workflowsRoot = join(specSillyspec, 'workflows')
|
|
635
|
+
const knowledgeRoot = join(specSillyspec, 'knowledge')
|
|
440
636
|
promptText = promptText.replace(/\{DOCS_ROOT\}/g, docsRoot)
|
|
441
637
|
promptText = promptText.replace(/\{PROJECTS_ROOT\}/g, projectsRoot)
|
|
638
|
+
promptText = promptText.replace(/\{WORKFLOWS_ROOT\}/g, workflowsRoot)
|
|
639
|
+
promptText = promptText.replace(/\{KNOWLEDGE_ROOT\}/g, knowledgeRoot)
|
|
640
|
+
promptText = promptText.replace(/\{SPEC_ROOT\}/g, specSillyspec)
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// 注入模块上下文(brainstorm/plan/execute 阶段,基于 Module Context Index)
|
|
645
|
+
if (['brainstorm', 'plan', 'execute'].includes(stageName) && projectName) {
|
|
646
|
+
const moduleIndex = loadModuleContextIndex(specBase || join(cwd, '.sillyspec'), projectName)
|
|
647
|
+
if (moduleIndex && Object.keys(moduleIndex).length > 0) {
|
|
648
|
+
// 尝试从 step prompt / changeName 匹配模块
|
|
649
|
+
const taskDesc = step.prompt || changeName || ''
|
|
650
|
+
const injection = buildModuleContextInjection(taskDesc, moduleIndex, specBase || join(cwd, '.sillyspec'), projectName)
|
|
651
|
+
if (injection) {
|
|
652
|
+
promptText = injection + '\n' + promptText
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// 平台模式 prompt 自检:确保没有裸相对输出路径
|
|
658
|
+
// 只匹配正向写入指令中的裸路径,避免误杀「禁止写入 .sillyspec/」等安全说明
|
|
659
|
+
if ((platformOpts?.specRoot || platformOpts?.runtimeRoot) && stageName === 'scan') {
|
|
660
|
+
const writeCtx = /(?<!不要|禁止|严禁)(?:save[\s.]+to|write|create|mkdir|git add|写入|保存到|写入到)[^a-zA-Z]*\.sillyspec\/[a-z]/i
|
|
661
|
+
if (writeCtx.test(promptText)) {
|
|
662
|
+
console.error(`❌ [sillyspec] BUG: 平台模式 scan prompt 包含写入指令指向裸相对路径 .sillyspec/`)
|
|
663
|
+
console.error(` 这会导致 agent 写入源码目录而非 spec-root,属于源码污染 bug。`)
|
|
664
|
+
console.error(` 请将路径改为对应的 {DOCS_ROOT}/{PROJECTS_ROOT}/{WORKFLOWS_ROOT}/{KNOWLEDGE_ROOT}/{SPEC_ROOT} 占位符。`)
|
|
665
|
+
process.exit(1)
|
|
442
666
|
}
|
|
443
667
|
}
|
|
444
668
|
|
|
@@ -651,7 +875,7 @@ async function executeScanPostcheck(cwd, platformOpts, scanProfile) {
|
|
|
651
875
|
const manifestDir = platformOpts.specRoot
|
|
652
876
|
let sourceCommit = null
|
|
653
877
|
try {
|
|
654
|
-
sourceCommit =
|
|
878
|
+
const { value: sourceCommit, error: scErr } = safeGit(cwd, ['rev-parse', 'HEAD'])
|
|
655
879
|
} catch {}
|
|
656
880
|
mkdirSync(manifestDir, { recursive: true })
|
|
657
881
|
const manifest = {
|
|
@@ -665,6 +889,7 @@ async function executeScanPostcheck(cwd, platformOpts, scanProfile) {
|
|
|
665
889
|
workspace_id: platformOpts.workspaceId || null,
|
|
666
890
|
scan_run_id: platformOpts.scanRunId || null,
|
|
667
891
|
source_commit: sourceCommit,
|
|
892
|
+
source_commit_error: sourceCommit === null ? (scErr || 'unknown') : undefined,
|
|
668
893
|
generated_at: new Date().toISOString(),
|
|
669
894
|
schema_version: 2,
|
|
670
895
|
}
|
|
@@ -1074,7 +1299,7 @@ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval =
|
|
|
1074
1299
|
const allowNew = quickOpts?.isAllowNew || false
|
|
1075
1300
|
const forceBaseline = quickOpts?.isForceBaseline || false
|
|
1076
1301
|
progress.quickGuard = {
|
|
1077
|
-
baselineCommit:
|
|
1302
|
+
baselineCommit: safeGit(cwd, ['rev-parse', 'HEAD']).value,
|
|
1078
1303
|
baselineFiles,
|
|
1079
1304
|
allowedFiles,
|
|
1080
1305
|
allowNew,
|
|
@@ -1668,12 +1893,13 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
1668
1893
|
mkdirSync(manifestDir, { recursive: true })
|
|
1669
1894
|
let sourceCommit = null
|
|
1670
1895
|
try {
|
|
1671
|
-
sourceCommit =
|
|
1896
|
+
const { value: sourceCommit, error: scErr } = safeGit(cwd, ['rev-parse', 'HEAD'])
|
|
1672
1897
|
} catch {}
|
|
1673
1898
|
const manifest = {
|
|
1674
1899
|
workspace_id: platformOpts.workspaceId || null,
|
|
1675
1900
|
scan_run_id: platformOpts.scanRunId || null,
|
|
1676
1901
|
source_commit: sourceCommit,
|
|
1902
|
+
source_commit_error: sourceCommit === null ? (scErr || 'unknown') : undefined,
|
|
1677
1903
|
generated_at: new Date().toISOString(),
|
|
1678
1904
|
schema_version: 1,
|
|
1679
1905
|
}
|
|
@@ -1690,7 +1916,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
1690
1916
|
try { unlinkSync(platformOptsFile) } catch {}
|
|
1691
1917
|
|
|
1692
1918
|
// CLI 层 post-check(替代旧的简单检查)
|
|
1693
|
-
const { runScanPostCheck, printScanPostCheckResult } = await import('./scan-postcheck.js')
|
|
1919
|
+
const { runScanPostCheck, printScanPostCheckResult, formatStructuredResult, writeStructuredResult } = await import('./scan-postcheck.js')
|
|
1694
1920
|
const postResult = runScanPostCheck({
|
|
1695
1921
|
cwd,
|
|
1696
1922
|
specDir: platformOpts.specRoot,
|
|
@@ -1702,6 +1928,22 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
1702
1928
|
})
|
|
1703
1929
|
printScanPostCheckResult(postResult)
|
|
1704
1930
|
|
|
1931
|
+
// 生成结构化 JSON 并写入 runtime(供 SillyHub 消费)
|
|
1932
|
+
const structured = formatStructuredResult(postResult, {
|
|
1933
|
+
workspace_id: platformOpts.workspaceId,
|
|
1934
|
+
scan_run_id: platformOpts.scanRunId,
|
|
1935
|
+
source_root: cwd,
|
|
1936
|
+
spec_root: platformOpts.specRoot,
|
|
1937
|
+
runtime_root: platformOpts.runtimeRoot,
|
|
1938
|
+
})
|
|
1939
|
+
const postcheckJsonPath = writeStructuredResult(structured, platformOpts.specRoot, {
|
|
1940
|
+
runtimeRoot: platformOpts.runtimeRoot,
|
|
1941
|
+
scanRunId: platformOpts.scanRunId,
|
|
1942
|
+
})
|
|
1943
|
+
if (postcheckJsonPath) {
|
|
1944
|
+
console.log(`📄 postcheck-result.json 已写入: ${postcheckJsonPath}`)
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1705
1947
|
// 将 post-check 结果写入 manifest
|
|
1706
1948
|
manifest.scan_post_check = {
|
|
1707
1949
|
status: postResult.status,
|
|
@@ -1729,11 +1971,17 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
1729
1971
|
}
|
|
1730
1972
|
}
|
|
1731
1973
|
|
|
1732
|
-
// 非 platform 模式 scan 也做轻量 post-check
|
|
1974
|
+
// 非 platform 模式 scan 也做轻量 post-check + 结构化输出
|
|
1733
1975
|
if (stageName === 'scan' && !platformOpts.specRoot && !platformOpts.runtimeRoot) {
|
|
1734
|
-
const { runScanPostCheck, printScanPostCheckResult } = await import('./scan-postcheck.js')
|
|
1976
|
+
const { runScanPostCheck, printScanPostCheckResult, formatStructuredResult, writeStructuredResult } = await import('./scan-postcheck.js')
|
|
1735
1977
|
const postResult = runScanPostCheck({ cwd, specDir: null, outputText })
|
|
1736
1978
|
printScanPostCheckResult(postResult)
|
|
1979
|
+
// 结构化结果写入 .sillyspec/.runtime/
|
|
1980
|
+
const structured = formatStructuredResult(postResult, { source_root: cwd })
|
|
1981
|
+
const postcheckJsonPath = writeStructuredResult(structured, join(cwd, '.sillyspec'))
|
|
1982
|
+
if (postcheckJsonPath) {
|
|
1983
|
+
console.log(`📄 postcheck-result.json 已写入: ${postcheckJsonPath}`)
|
|
1984
|
+
}
|
|
1737
1985
|
}
|
|
1738
1986
|
|
|
1739
1987
|
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 = [
|
|
@@ -52,19 +52,33 @@ export function runScanPostCheck({ cwd, specDir, outputText = '', scanMeta = {}
|
|
|
52
52
|
|
|
53
53
|
const projectName = basename(cwd)
|
|
54
54
|
|
|
55
|
-
// 1. source_root
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
55
|
+
// 1. source_root 污染检查(docs/projects/workflows/knowledge/manifest/local)
|
|
56
|
+
const pollutePaths = ['docs', 'projects', 'workflows', 'knowledge']
|
|
57
|
+
const polluteFiles = ['manifest.json', 'local.yaml']
|
|
58
|
+
for (const sub of pollutePaths) {
|
|
59
|
+
const localSub = join(cwd, '.sillyspec', sub)
|
|
60
|
+
if (existsSync(localSub)) {
|
|
61
|
+
try {
|
|
62
|
+
const leaked = readdirSync(localSub, { recursive: true }).filter(e => String(e).endsWith('.md') || String(e).endsWith('.yaml') || String(e).endsWith('.json'))
|
|
63
|
+
if (leaked.length > 0) {
|
|
64
|
+
checks.push({
|
|
65
|
+
name: 'source_root_leak',
|
|
66
|
+
severity: 'failed',
|
|
67
|
+
detail: `source_root/.sillyspec/${sub}/ 下存在 ${leaked.length} 个文件(${localSub}/),agent 写入到了错误路径`
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
} catch {}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
for (const file of polluteFiles) {
|
|
74
|
+
const filePath = join(cwd, '.sillyspec', file)
|
|
75
|
+
if (existsSync(filePath)) {
|
|
76
|
+
checks.push({
|
|
77
|
+
name: 'source_root_leak',
|
|
78
|
+
severity: 'failed',
|
|
79
|
+
detail: `source_root/.sillyspec/${file} 存在,agent 写入到了错误路径(${filePath})`
|
|
80
|
+
})
|
|
81
|
+
}
|
|
68
82
|
}
|
|
69
83
|
|
|
70
84
|
// 2. spec_root 检查 7 份必需文档
|
|
@@ -184,6 +198,138 @@ export function runScanPostCheck({ cwd, specDir, outputText = '', scanMeta = {}
|
|
|
184
198
|
return { status, checks }
|
|
185
199
|
}
|
|
186
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
|
+
|
|
187
333
|
/**
|
|
188
334
|
* 打印 post-check 结果到 stdout
|
|
189
335
|
*/
|
package/src/stages/quick.js
CHANGED
|
@@ -22,8 +22,8 @@ export const definition = {
|
|
|
22
22
|
9. 根据任务描述初步判断可能涉及的模块
|
|
23
23
|
10. 读取匹配到的 \`.sillyspec/docs/<project>/modules/<module>.md\`
|
|
24
24
|
|
|
25
|
-
###
|
|
26
|
-
|
|
25
|
+
### 创建任务记录(⛔ 此步骤不能跳过,没有 quicklog 记录 = 未完成)
|
|
26
|
+
理解完任务后,**必须**立即创建记录文件,再输出任何其他内容:
|
|
27
27
|
1. 使用预注入的 git 用户名:\`<git-user>\`
|
|
28
28
|
2. 无 \`--change\`:创建 .sillyspec/quicklog/QUICKLOG-\`<git-user>\`.md\`(已存在则追加),写入:
|
|
29
29
|
\`\`\`
|
|
@@ -41,7 +41,9 @@ export const definition = {
|
|
|
41
41
|
这样 Gate 检测到 .sillyspec/\` 下有变更,就不会拦截后续的代码修改。
|
|
42
42
|
|
|
43
43
|
### 输出
|
|
44
|
-
任务理解 + 上下文摘要
|
|
44
|
+
quicklog 已创建(必须放在输出的第一行确认)+ 任务理解 + 上下文摘要
|
|
45
|
+
|
|
46
|
+
⚠️ **先创建 quicklog,再输出任务理解。** 如果 quicklog 未创建,CLI post-check 会报 warning。`,
|
|
45
47
|
outputHint: '任务理解',
|
|
46
48
|
optional: false
|
|
47
49
|
},
|
package/src/stages/scan.js
CHANGED
|
@@ -103,13 +103,13 @@ export const definition = {
|
|
|
103
103
|
{
|
|
104
104
|
name: '深度扫描 — 7 份文档(子代理并行)',
|
|
105
105
|
perProject: true,
|
|
106
|
-
prompt: `按照
|
|
106
|
+
prompt: `按照 \`{WORKFLOWS_ROOT}/scan-docs.yaml\` 中定义的角色和检查规则,使用子代理并行生成当前项目的 7 份扫描文档。
|
|
107
107
|
|
|
108
108
|
**你必须使用子代理执行,不要自己写文档。**
|
|
109
109
|
**对扫描列表中的每个项目分别执行以下流程。**
|
|
110
110
|
|
|
111
111
|
### 操作
|
|
112
|
-
1. 读取
|
|
112
|
+
1. 读取 \`{WORKFLOWS_ROOT}/scan-docs.yaml\`,了解角色定义、输出要求和检查规则
|
|
113
113
|
2. 对每个项目(扫描列表中标记为需生成/覆盖的项目):
|
|
114
114
|
a. 将 \`<project>\` 替换为实际项目名,得到该项目的目标文件路径
|
|
115
115
|
b. 为每个角色启动独立子代理(可并行),每个子代理负责 1-2 份文档
|
|
@@ -141,16 +141,16 @@ export const definition = {
|
|
|
141
141
|
},
|
|
142
142
|
{
|
|
143
143
|
name: '生成本地配置',
|
|
144
|
-
prompt: `自动生成
|
|
144
|
+
prompt: `自动生成 local.yaml 本地配置文件。
|
|
145
145
|
|
|
146
146
|
### 操作
|
|
147
|
-
1. 检查
|
|
147
|
+
1. 检查 {SPEC_ROOT}/local.yaml 是否已存在,已存在则跳过(提示"local.yaml 已存在,跳过生成")
|
|
148
148
|
2. 根据项目类型生成默认配置:
|
|
149
149
|
- **Node.js**(有 package.json):build: "npm run build", test: "npm test", lint: "npm run lint", type: nodejs
|
|
150
150
|
- **Maven**(有 pom.xml):build: "mvn compile", test: "mvn test", lint: "mvn checkstyle:check", type: maven
|
|
151
151
|
- **Gradle**(有 build.gradle):build: "./gradlew build", test: "./gradlew test", type: gradle
|
|
152
152
|
- **通用项目**:只写注释模板, type: generic
|
|
153
|
-
3. 确保目录存在:mkdir -p
|
|
153
|
+
3. 确保目录存在:mkdir -p {SPEC_ROOT}
|
|
154
154
|
4. 原子写入(先写 tmp 文件再 rename)
|
|
155
155
|
|
|
156
156
|
### 文件格式
|
|
@@ -454,7 +454,7 @@ step1 → step2 → step3
|
|
|
454
454
|
3. 自检门控:ARCHITECTURE(技术栈+Schema摘要)、CONVENTIONS(隐形规则+代码风格)、STRUCTURE(目录结构)、INTEGRATIONS(外部依赖)、TESTING(测试现状)、CONCERNS(技术债务)、PROJECT(项目概览)
|
|
455
455
|
4. 检查 flows/ 和 glossary.md 是否已生成(如有)
|
|
456
456
|
5. 清理:\`rm -f {DOCS_ROOT}/scan/_env-detect.md\`
|
|
457
|
-
6.
|
|
457
|
+
6. 如果非平台模式:\`git add .sillyspec/\` — 暂存扫描结果(不要 commit,由用户通过统一提交工具处理)。如果平台模式:跳过 git add(specRoot 不在 sourceRoot 的 git repo 内)。
|
|
458
458
|
|
|
459
459
|
### ⛔ 路径合规检查(平台模式下必须执行)
|
|
460
460
|
7. 确认所有文档都写入 \`{DOCS_ROOT}/\`(spec-root 下),**而非源码目录下的 .sillyspec/**
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* P0 补丁验证 — 4 类测试
|
|
3
|
+
*
|
|
4
|
+
* 1. 平台 scan prompt 不含正向写入 .sillyspec/ 指令
|
|
5
|
+
* 2. 安全说明文字不被 prompt 自检误杀
|
|
6
|
+
* 3. 平台模式不执行 git add {SPEC_ROOT}
|
|
7
|
+
* 4. source_root 污染检查覆盖所有子目录/文件
|
|
8
|
+
* 5. run.js 占位符替换补齐(平台+本地)
|
|
9
|
+
*
|
|
10
|
+
* 跑法: node test/platform-scan-p0.test.mjs
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { join, basename } from 'path'
|
|
14
|
+
import { existsSync, mkdirSync, writeFileSync, rmSync, readdirSync } from 'fs'
|
|
15
|
+
import { execSync } from 'child_process'
|
|
16
|
+
import { readFile } from 'fs/promises'
|
|
17
|
+
import { fileURLToPath } from 'url'
|
|
18
|
+
|
|
19
|
+
const __dirname = fileURLToPath(new URL('.', import.meta.url))
|
|
20
|
+
const passed = []
|
|
21
|
+
const failed = []
|
|
22
|
+
|
|
23
|
+
function assert(label, condition, detail) {
|
|
24
|
+
if (condition) {
|
|
25
|
+
passed.push(label)
|
|
26
|
+
} else {
|
|
27
|
+
failed.push({ label, detail })
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ── 测试 1:scan.js 模板不含裸 .sillyspec 输出路径 ──
|
|
32
|
+
{
|
|
33
|
+
const { definition } = await import('../src/stages/scan.js')
|
|
34
|
+
const prompts = definition.steps.map(s => s.prompt).join('\n')
|
|
35
|
+
|
|
36
|
+
// 所有 .sillyspec 出现都应该是「禁止说明」,不是写入指令
|
|
37
|
+
const writePatterns = [
|
|
38
|
+
/write.*\.sillyspec\/docs/i,
|
|
39
|
+
/save.*\.sillyspec\/docs/i,
|
|
40
|
+
/create.*\.sillyspec\/docs/i,
|
|
41
|
+
/mkdir.*\.sillyspec\/docs/i,
|
|
42
|
+
]
|
|
43
|
+
for (const p of writePatterns) {
|
|
44
|
+
assert(`scan 模板无写入 .sillyspec/docs: ${p}`, !p.test(prompts),
|
|
45
|
+
p.test(prompts) ? `命中: ${prompts.match(p)[0]}` : '')
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 应该使用占位符
|
|
49
|
+
assert('scan 模板使用 {WORKFLOWS_ROOT}', prompts.includes('{WORKFLOWS_ROOT}'))
|
|
50
|
+
assert('scan 模板使用 {SPEC_ROOT}', prompts.includes('{SPEC_ROOT}'))
|
|
51
|
+
assert('scan 模板使用 {DOCS_ROOT}', prompts.includes('{DOCS_ROOT}'))
|
|
52
|
+
|
|
53
|
+
// 平台模式下 git add 应该是条件判断
|
|
54
|
+
assert('scan 模板平台模式跳过 git add', prompts.includes('如果平台模式:跳过 git add'))
|
|
55
|
+
assert('scan 模板非平台模式 git add .sillyspec/', prompts.includes('git add .sillyspec/'))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ── 测试 2:prompt 自检不误杀安全说明 ──
|
|
59
|
+
{
|
|
60
|
+
// 导入 run.js 中的正则
|
|
61
|
+
const writeCtxRe = /(?<!不要|禁止|严禁)(?:save[\s.]+to|write|create|mkdir|git add|写入|保存到|写入到)[^a-zA-Z]*\.sillyspec\/[a-z]/i
|
|
62
|
+
|
|
63
|
+
// 安全说明 — 不应命中
|
|
64
|
+
const safeLines = [
|
|
65
|
+
'严禁写入源码目录或相对路径 `.sillyspec/`',
|
|
66
|
+
'⚠️ 不要写入 .sillyspec/docs',
|
|
67
|
+
'source_root 下存在 .sillyspec/docs 文件',
|
|
68
|
+
'不允许从 cwd 推导 .sillyspec 路径',
|
|
69
|
+
]
|
|
70
|
+
for (const line of safeLines) {
|
|
71
|
+
assert(`安全说明不误杀: "${line.slice(0, 40)}"`, !writeCtxRe.test(line),
|
|
72
|
+
'误杀! 命中了安全说明文字')
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 写入指令 — 应该命中
|
|
76
|
+
const badLines = [
|
|
77
|
+
'写入 `.sillyspec/docs/ARCHITECTURE.md`',
|
|
78
|
+
'save to `.sillyspec/docs/ARCHITECTURE.md`',
|
|
79
|
+
'create `.sillyspec/docs/ARCH.md`',
|
|
80
|
+
'git add .sillyspec/docs/',
|
|
81
|
+
'write 到 .sillyspec/docs/',
|
|
82
|
+
]
|
|
83
|
+
for (const line of badLines) {
|
|
84
|
+
assert(`写入指令应命中: "${line.slice(0, 40)}"`, writeCtxRe.test(line),
|
|
85
|
+
'漏杀! 没有捕获写入指令')
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── 测试 3:safeGit 使用 -c safe.directory(不污染全局 config) ──
|
|
90
|
+
{
|
|
91
|
+
const runSrc = await readFile(join(__dirname, '..', 'src', 'run.js'), 'utf8')
|
|
92
|
+
|
|
93
|
+
assert('safeGit 不含 --global', !runSrc.includes('git config --global'),
|
|
94
|
+
'发现 --global,会污染容器 git config')
|
|
95
|
+
|
|
96
|
+
assert('safeGit 使用 -c safe.directory', runSrc.includes("safe.directory=${cwd}"),
|
|
97
|
+
'未发现 -c safe.directory per-command 参数')
|
|
98
|
+
|
|
99
|
+
assert('safeGit 使用 -C cwd', runSrc.includes("-C', cwd"),
|
|
100
|
+
'未发现 -C cwd 参数')
|
|
101
|
+
|
|
102
|
+
assert('safeGit 返回 { value, error }', runSrc.includes("return { value, error: "),
|
|
103
|
+
'safeGit 返回值不是 { value, error } 结构')
|
|
104
|
+
|
|
105
|
+
assert('manifest 包含 source_commit_error 字段',
|
|
106
|
+
runSrc.includes("source_commit_error:"), 'manifest 缺少 source_commit_error')
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── 测试 4:postcheck 污染检查覆盖所有子目录 ──
|
|
110
|
+
{
|
|
111
|
+
const postcheckSrc = await readFile(join(__dirname, '..', 'src', 'scan-postcheck.js'), 'utf8')
|
|
112
|
+
|
|
113
|
+
const requiredSubs = ['docs', 'projects', 'workflows', 'knowledge']
|
|
114
|
+
for (const sub of requiredSubs) {
|
|
115
|
+
assert(`postcheck 检查 ${sub} 污染`,
|
|
116
|
+
postcheckSrc.includes(`'${sub}'`) && postcheckSrc.includes("'.sillyspec', sub)"),
|
|
117
|
+
`postcheck 未检查 .sillyspec/${sub}/ 污染`)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
assert('postcheck 检查 manifest.json', postcheckSrc.includes('manifest.json'))
|
|
121
|
+
assert('postcheck 检查 local.yaml', postcheckSrc.includes('local.yaml'))
|
|
122
|
+
assert('污染 severity 为 failed', postcheckSrc.includes("severity: 'failed'"))
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ── 测试 5:run.js 占位符替换补齐 ──
|
|
126
|
+
{
|
|
127
|
+
const runSrc = await readFile(join(__dirname, '..', 'src', 'run.js'), 'utf8')
|
|
128
|
+
|
|
129
|
+
// 平台模式块
|
|
130
|
+
const platformMarker = 'promptText.replace(/\\{WORKFLOWS_ROOT\\}'
|
|
131
|
+
assert('平台模式替换 {WORKFLOWS_ROOT}', runSrc.includes('{WORKFLOWS_ROOT}') && runSrc.includes('workflowsRoot'))
|
|
132
|
+
assert('平台模式替换 {KNOWLEDGE_ROOT}', runSrc.includes('{KNOWLEDGE_ROOT}') && runSrc.includes('knowledgeRoot'))
|
|
133
|
+
assert('平台模式替换 {SPEC_ROOT}', runSrc.includes('{SPEC_ROOT}') && runSrc.includes("specSillyspec"))
|
|
134
|
+
|
|
135
|
+
// 非平台模式块
|
|
136
|
+
assert('非平台模式替换 {WORKFLOWS_ROOT}', runSrc.includes('workflowsRoot'))
|
|
137
|
+
assert('非平台模式替换 {KNOWLEDGE_ROOT}', runSrc.includes('knowledgeRoot'))
|
|
138
|
+
assert('非平台模式替换 {SPEC_ROOT}', runSrc.includes('{SPEC_ROOT}'))
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ── 测试 6:quick step 1 prompt 强制要求 quicklog ──
|
|
142
|
+
{
|
|
143
|
+
const { definition } = await import('../src/stages/quick.js')
|
|
144
|
+
const step1Prompt = definition.steps[0].prompt
|
|
145
|
+
|
|
146
|
+
assert('quick step 1 包含 ⛔ 标记', step1Prompt.includes('⛔'))
|
|
147
|
+
assert('quick step 1 包含「不能跳过」', step1Prompt.includes('不能跳过'))
|
|
148
|
+
assert('quick step 1 包含 quicklog 未创建 warning', step1Prompt.includes('quicklog 未创建'))
|
|
149
|
+
assert('quick step 1 输出要求 quicklog 第一行', step1Prompt.includes('第一行确认'))
|
|
150
|
+
|
|
151
|
+
// run.js 审计包含 quicklog 检查
|
|
152
|
+
const runSrc = await readFile(join(__dirname, '..', 'src', 'run.js'), 'utf8')
|
|
153
|
+
assert('quick 审计检查 quicklog 目录存在', runSrc.includes('quicklog 目录不存在'))
|
|
154
|
+
assert('quick 审计检查 quicklog 为空', runSrc.includes('quicklog 目录为空'))
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ── 结果 ──
|
|
158
|
+
console.log(`\n${'='.repeat(50)}`)
|
|
159
|
+
console.log(`✅ 通过: ${passed.length}`)
|
|
160
|
+
console.log(`❌ 失败: ${failed.length}`)
|
|
161
|
+
console.log(`${'='.repeat(50)}`)
|
|
162
|
+
|
|
163
|
+
if (failed.length > 0) {
|
|
164
|
+
console.log('\n失败详情:')
|
|
165
|
+
for (const f of failed) {
|
|
166
|
+
console.log(` ❌ ${f.label}`)
|
|
167
|
+
if (f.detail) console.log(` ${f.detail}`)
|
|
168
|
+
}
|
|
169
|
+
process.exit(1)
|
|
170
|
+
} else {
|
|
171
|
+
console.log('\n🎉 全部 P0 测试通过!')
|
|
172
|
+
}
|