sillyspec 3.12.8 → 3.13.0
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/index.js +39 -0
- package/src/modules.js +427 -0
- package/src/progress.js +51 -0
- package/src/run.js +2 -0
- package/src/stages/archive.js +58 -54
- package/src/stages/brainstorm.js +17 -4
- package/src/stages/doctor.js +30 -1
- package/src/stages/execute.js +5 -1
- package/src/stages/plan.js +6 -1
- package/src/stages/scan.js +200 -42
- package/src/stages/verify.js +2 -1
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -474,6 +474,45 @@ SillySpec platform — SillyHub 平台同步
|
|
|
474
474
|
}
|
|
475
475
|
break;
|
|
476
476
|
}
|
|
477
|
+
case 'change-rename': {
|
|
478
|
+
const oldName = filteredArgs[1];
|
|
479
|
+
const newName = filteredArgs[2];
|
|
480
|
+
if (!oldName || !newName) {
|
|
481
|
+
console.error('❌ 用法: sillyspec change-rename <旧变更名> <新变更名>');
|
|
482
|
+
process.exit(1);
|
|
483
|
+
}
|
|
484
|
+
const pm = new ProgressManager();
|
|
485
|
+
await pm.renameChange(dir, oldName, newName);
|
|
486
|
+
break;
|
|
487
|
+
}
|
|
488
|
+
case 'modules': {
|
|
489
|
+
const modulesSub = filteredArgs[1];
|
|
490
|
+
if (!modulesSub || modulesSub === 'help' || modulesSub === '--help') {
|
|
491
|
+
console.log(`
|
|
492
|
+
SillySpec modules — 模块文档管理
|
|
493
|
+
|
|
494
|
+
用法:
|
|
495
|
+
sillyspec modules rebuild 从模块卡片 + 源码重建 _module-map.yaml
|
|
496
|
+
sillyspec modules status 显示模块索引状态
|
|
497
|
+
sillyspec modules migrate 旧格式模块文档迁移到新格式
|
|
498
|
+
`);
|
|
499
|
+
break;
|
|
500
|
+
}
|
|
501
|
+
if (modulesSub === 'rebuild') {
|
|
502
|
+
const { rebuildModuleMap } = await import('./modules.js');
|
|
503
|
+
await rebuildModuleMap(dir);
|
|
504
|
+
} else if (modulesSub === 'status') {
|
|
505
|
+
const { showModuleStatus } = await import('./modules.js');
|
|
506
|
+
await showModuleStatus(dir);
|
|
507
|
+
} else if (modulesSub === 'migrate') {
|
|
508
|
+
const { migrateModuleDocs } = await import('./modules.js');
|
|
509
|
+
await migrateModuleDocs(dir);
|
|
510
|
+
} else {
|
|
511
|
+
console.error(`❌ 未知子命令: modules ${modulesSub}`);
|
|
512
|
+
process.exit(1);
|
|
513
|
+
}
|
|
514
|
+
break;
|
|
515
|
+
}
|
|
477
516
|
default:
|
|
478
517
|
console.error(`❌ 未知命令: ${command}`);
|
|
479
518
|
printUsage();
|
package/src/modules.js
ADDED
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { DB } from './db.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 查找项目下的 _module-map.yaml 路径
|
|
7
|
+
*/
|
|
8
|
+
function findModuleMapPath(cwd) {
|
|
9
|
+
// 查找 .sillyspec/docs/<project>/modules/_module-map.yaml
|
|
10
|
+
const docsDir = join(cwd, '.sillyspec', 'docs');
|
|
11
|
+
if (!existsSync(docsDir)) return null;
|
|
12
|
+
|
|
13
|
+
const projects = readdirSync(docsDir, { withFileTypes: true })
|
|
14
|
+
.filter(e => e.isDirectory());
|
|
15
|
+
|
|
16
|
+
for (const proj of projects) {
|
|
17
|
+
const mapPath = join(docsDir, proj.name, 'modules', '_module-map.yaml');
|
|
18
|
+
if (existsSync(mapPath)) return mapPath;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// 如果没有找到已有的,返回第一个项目的路径(用于创建)
|
|
22
|
+
if (projects.length > 0) {
|
|
23
|
+
return join(docsDir, projects[0].name, 'modules', '_module-map.yaml');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 从模块卡片文件名提取 module_id
|
|
31
|
+
*/
|
|
32
|
+
function parseModuleIdFromFilename(filename) {
|
|
33
|
+
return filename.replace(/\.md$/, '');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 从模块卡片读取 frontmatter 中的 module_id
|
|
38
|
+
*/
|
|
39
|
+
function parseModuleCardFrontmatter(content) {
|
|
40
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
41
|
+
if (!fmMatch) return {};
|
|
42
|
+
const fm = fmMatch[1];
|
|
43
|
+
const moduleIdMatch = fm.match(/^module_id:\s*(.+)$/m);
|
|
44
|
+
return {
|
|
45
|
+
module_id: moduleIdMatch ? moduleIdMatch[1].trim() : null
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* rebuild: 从模块卡片 + 源码重建 _module-map.yaml
|
|
51
|
+
*/
|
|
52
|
+
export async function rebuildModuleMap(cwd) {
|
|
53
|
+
const mapPath = findModuleMapPath(cwd);
|
|
54
|
+
if (!mapPath) {
|
|
55
|
+
console.error('❌ 未找到 .sillyspec/docs/<project>/modules/ 目录');
|
|
56
|
+
console.log(' 提示:先运行 sillyspec run scan 生成模块文档');
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const modulesDir = join(mapPath, '..');
|
|
61
|
+
const existingMap = existsSync(mapPath) ? readFileSync(mapPath, 'utf8') : null;
|
|
62
|
+
|
|
63
|
+
// 收集现有模块卡片
|
|
64
|
+
const cards = [];
|
|
65
|
+
if (existsSync(modulesDir)) {
|
|
66
|
+
const files = readdirSync(modulesDir).filter(f => f.endsWith('.md') && f !== '_module-map.yaml');
|
|
67
|
+
for (const f of files) {
|
|
68
|
+
const content = readFileSync(join(modulesDir, f), 'utf8');
|
|
69
|
+
const fm = parseModuleCardFrontmatter(content);
|
|
70
|
+
const moduleId = fm.module_id || parseModuleIdFromFilename(f);
|
|
71
|
+
cards.push({ filename: f, moduleId });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 解析现有 _module-map.yaml 保留已有字段
|
|
76
|
+
let existingModules = {};
|
|
77
|
+
if (existingMap) {
|
|
78
|
+
// 简单解析:提取 modules: 下的条目
|
|
79
|
+
const lines = existingMap.split('\n');
|
|
80
|
+
let currentModule = null;
|
|
81
|
+
let inModules = false;
|
|
82
|
+
for (const line of lines) {
|
|
83
|
+
if (line.startsWith('modules:')) { inModules = true; continue; }
|
|
84
|
+
if (!inModules) continue;
|
|
85
|
+
if (line.startsWith(' ') === false && line.trim() !== '') break;
|
|
86
|
+
const moduleMatch = line.match(/^ ([a-zA-Z0-9_-]+):/);
|
|
87
|
+
if (moduleMatch) {
|
|
88
|
+
currentModule = moduleMatch[1];
|
|
89
|
+
existingModules[currentModule] = existingModules[currentModule] || { paths: [] };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 如果没有模块卡片也没有已有映射
|
|
95
|
+
if (cards.length === 0 && Object.keys(existingModules).length === 0) {
|
|
96
|
+
console.log('📭 没有模块卡片,也没有已有的模块映射');
|
|
97
|
+
console.log(' 提示:先运行 sillyspec run scan 生成模块文档');
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 生成新的 _module-map.yaml
|
|
102
|
+
// 保留已有的完整字段,补齐新卡片
|
|
103
|
+
const now = new Date().toISOString().replace('T', ' ').slice(0, 19);
|
|
104
|
+
let headCommit = '';
|
|
105
|
+
try {
|
|
106
|
+
const { execSync } = await import('child_process');
|
|
107
|
+
headCommit = execSync('git rev-parse --short HEAD', { cwd, encoding: 'utf8', timeout: 5000 }).trim();
|
|
108
|
+
} catch { /* ignore */ }
|
|
109
|
+
|
|
110
|
+
let yaml = `schema_version: 1\n`;
|
|
111
|
+
yaml += `generated_at: ${now}\n`;
|
|
112
|
+
yaml += `generator: sillyspec-modules-rebuild\n`;
|
|
113
|
+
if (headCommit) yaml += `source_commit: ${headCommit}\n`;
|
|
114
|
+
yaml += `\nmodules:\n`;
|
|
115
|
+
|
|
116
|
+
// 合并:已有映射 + 新卡片
|
|
117
|
+
const allModuleIds = new Set([
|
|
118
|
+
...Object.keys(existingModules),
|
|
119
|
+
...cards.map(c => c.moduleId)
|
|
120
|
+
]);
|
|
121
|
+
|
|
122
|
+
for (const moduleId of allModuleIds) {
|
|
123
|
+
const card = cards.find(c => c.moduleId === moduleId);
|
|
124
|
+
yaml += ` ${moduleId}:\n`;
|
|
125
|
+
yaml += ` status: active\n`;
|
|
126
|
+
if (card) yaml += ` doc: modules/${card.filename}\n`;
|
|
127
|
+
else yaml += ` doc: modules/${moduleId}.md\n`;
|
|
128
|
+
// 保留已有 _module-map.yaml 中的 paths 等字段(如果有的话)
|
|
129
|
+
yaml += ` needs_review: false\n`;
|
|
130
|
+
yaml += ` review_reasons: []\n`;
|
|
131
|
+
yaml += `\n`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
writeFileSync(mapPath, yaml, 'utf8');
|
|
135
|
+
console.log(`✅ _module-map.yaml 已重建:${mapPath}`);
|
|
136
|
+
console.log(` 模块数量:${allModuleIds.size}`);
|
|
137
|
+
for (const id of allModuleIds) {
|
|
138
|
+
console.log(` - ${id}`);
|
|
139
|
+
}
|
|
140
|
+
console.log(`\n⚠️ 注意:rebuild 只重建骨架。tags/entrypoints/main_symbols/depends_on/used_by 需要重新运行 scan 或手动补充。`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* status: 显示模块索引状态
|
|
145
|
+
*/
|
|
146
|
+
export async function showModuleStatus(cwd) {
|
|
147
|
+
const mapPath = findModuleMapPath(cwd);
|
|
148
|
+
if (!mapPath || !existsSync(mapPath)) {
|
|
149
|
+
console.log('📭 未找到 _module-map.yaml');
|
|
150
|
+
console.log(' 提示:先运行 sillyspec run scan 生成模块文档');
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const content = readFileSync(mapPath, 'utf8');
|
|
155
|
+
|
|
156
|
+
// 简单解析
|
|
157
|
+
const lines = content.split('\n');
|
|
158
|
+
const modules = [];
|
|
159
|
+
let currentModule = null;
|
|
160
|
+
let needsReview = false;
|
|
161
|
+
|
|
162
|
+
for (const line of lines) {
|
|
163
|
+
const moduleMatch = line.match(/^ ([a-zA-Z0-9_-]+):$/);
|
|
164
|
+
if (moduleMatch) {
|
|
165
|
+
if (currentModule) modules.push(currentModule);
|
|
166
|
+
currentModule = { id: moduleMatch[1], hasTags: false, hasEntryPoints: false, hasDepends: false, needsReview: false };
|
|
167
|
+
}
|
|
168
|
+
if (currentModule) {
|
|
169
|
+
if (line.includes('tags:')) currentModule.hasTags = true;
|
|
170
|
+
if (line.includes('entrypoints:')) currentModule.hasEntryPoints = true;
|
|
171
|
+
if (line.includes('depends_on:')) currentModule.hasDepends = true;
|
|
172
|
+
if (line.includes('needs_review: true')) { currentModule.needsReview = true; needsReview = true; }
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (currentModule) modules.push(currentModule);
|
|
176
|
+
|
|
177
|
+
console.log(`\n_module-map.yaml: ${mapPath}`);
|
|
178
|
+
console.log(`模块数量:${modules.length}\n`);
|
|
179
|
+
|
|
180
|
+
const maxId = Math.max(5, ...modules.map(m => m.id.length));
|
|
181
|
+
console.log(` ${'模块'.padEnd(maxId)} tags entry deps review`);
|
|
182
|
+
console.log(` ${'─'.repeat(maxId)} ──── ───── ──── ──────`);
|
|
183
|
+
for (const m of modules) {
|
|
184
|
+
const tags = m.hasTags ? '✅' : '⬜';
|
|
185
|
+
const entry = m.hasEntryPoints ? '✅' : '⬜';
|
|
186
|
+
const deps = m.hasDepends ? '✅' : '⬜';
|
|
187
|
+
const review = m.needsReview ? '⚠️' : '✅';
|
|
188
|
+
console.log(` ${m.id.padEnd(maxId)} ${tags} ${entry} ${deps} ${review}`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (needsReview) {
|
|
192
|
+
console.log(`\n⚠️ 有模块标记为 needs_review,建议检查或重新 scan`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* 从 _module-map.yaml 聚合生成 dependencies.md
|
|
198
|
+
*/
|
|
199
|
+
export async function generateDependenciesMd(cwd) {
|
|
200
|
+
const mapPath = findModuleMapPath(cwd);
|
|
201
|
+
if (!mapPath || !existsSync(mapPath)) return;
|
|
202
|
+
|
|
203
|
+
const content = readFileSync(mapPath, 'utf8');
|
|
204
|
+
const modules = parseModuleMapSimple(content);
|
|
205
|
+
|
|
206
|
+
if (Object.keys(modules).length === 0) return;
|
|
207
|
+
|
|
208
|
+
const depsPath = join(mapPath, '..', 'dependencies.md');
|
|
209
|
+
const maxName = Math.max(6, ...Object.keys(modules).map(k => k.length));
|
|
210
|
+
|
|
211
|
+
let md = `# Module Dependencies\n\n`;
|
|
212
|
+
md += `> 自动生成,由 sillyspec 维护\n\n`;
|
|
213
|
+
md += `| ${'Module'.padEnd(maxName)} | Depends On | Used By |\n`;
|
|
214
|
+
md += `| ${''.padEnd(maxName, '-')}:--- | :--- | :--- |\n`;
|
|
215
|
+
|
|
216
|
+
for (const [id, data] of Object.entries(modules)) {
|
|
217
|
+
const depends = (data.depends_on || []).join(', ') || '—';
|
|
218
|
+
const usedBy = (data.used_by || []).join(', ') || '—';
|
|
219
|
+
md += `| ${id.padEnd(maxName)} | ${depends} | ${usedBy} |\n`;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
writeFileSync(depsPath, md, 'utf8');
|
|
223
|
+
console.log(`✅ dependencies.md 已生成`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function parseModuleMapSimple(content) {
|
|
227
|
+
const modules = {};
|
|
228
|
+
let currentModule = null;
|
|
229
|
+
let currentKey = null;
|
|
230
|
+
let currentArray = null;
|
|
231
|
+
|
|
232
|
+
for (const line of content.split('\n')) {
|
|
233
|
+
const moduleMatch = line.match(/^ ([a-zA-Z0-9_-]+):$/);
|
|
234
|
+
if (moduleMatch) {
|
|
235
|
+
if (currentArray && currentModule && currentKey) {
|
|
236
|
+
modules[currentModule][currentKey] = currentArray;
|
|
237
|
+
}
|
|
238
|
+
currentModule = moduleMatch[1];
|
|
239
|
+
modules[currentModule] = {};
|
|
240
|
+
currentKey = null;
|
|
241
|
+
currentArray = null;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (!currentModule) continue;
|
|
245
|
+
|
|
246
|
+
// Array field like depends_on:\n - xxx
|
|
247
|
+
const arrayFieldMatch = line.match(/^ (depends_on|used_by|paths|tags|aliases|entrypoints|main_symbols|review_reasons):$/);
|
|
248
|
+
if (arrayFieldMatch) {
|
|
249
|
+
if (currentArray && currentKey) modules[currentModule][currentKey] = currentArray;
|
|
250
|
+
currentKey = arrayFieldMatch[1];
|
|
251
|
+
currentArray = [];
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Inline array like tags: [a, b]
|
|
256
|
+
const inlineArrayMatch = line.match(/^ (depends_on|used_by|paths|tags|aliases|entrypoints|main_symbols|review_reasons): \[(.*)\]$/);
|
|
257
|
+
if (inlineArrayMatch) {
|
|
258
|
+
if (currentArray && currentKey) modules[currentModule][currentKey] = currentArray;
|
|
259
|
+
const vals = inlineArrayMatch[2].split(',').map(v => v.trim()).filter(Boolean);
|
|
260
|
+
modules[currentModule][inlineArrayMatch[1]] = vals;
|
|
261
|
+
currentKey = null;
|
|
262
|
+
currentArray = null;
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Scalar field
|
|
267
|
+
const scalarMatch = line.match(/^ (status|doc|needs_review): (.+)$/);
|
|
268
|
+
if (scalarMatch) {
|
|
269
|
+
if (currentArray && currentKey) { modules[currentModule][currentKey] = currentArray; currentArray = null; currentKey = null; }
|
|
270
|
+
modules[currentModule][scalarMatch[1]] = scalarMatch[2];
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Array item
|
|
275
|
+
const itemMatch = line.match(/^ - (.+)$/);
|
|
276
|
+
if (itemMatch && currentArray !== null) {
|
|
277
|
+
currentArray.push(itemMatch[1].trim());
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Flush last
|
|
283
|
+
if (currentArray && currentModule && currentKey) {
|
|
284
|
+
modules[currentModule][currentKey] = currentArray;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return modules;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* migrate: 旧格式模块文档迁移到新格式
|
|
292
|
+
*/
|
|
293
|
+
export async function migrateModuleDocs(cwd) {
|
|
294
|
+
const mapPath = findModuleMapPath(cwd);
|
|
295
|
+
if (!mapPath || !existsSync(mapPath)) {
|
|
296
|
+
console.error('❌ 未找到 _module-map.yaml');
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const modulesDir = join(mapPath, '..');
|
|
301
|
+
const files = readdirSync(modulesDir).filter(f => f.endsWith('.md') && f !== '_module-map.yaml');
|
|
302
|
+
|
|
303
|
+
if (files.length === 0) {
|
|
304
|
+
console.log('📭 没有模块文档需要迁移');
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
let migrated = 0;
|
|
309
|
+
let skipped = 0;
|
|
310
|
+
|
|
311
|
+
for (const f of files) {
|
|
312
|
+
const filePath = join(modulesDir, f);
|
|
313
|
+
const content = readFileSync(filePath, 'utf8');
|
|
314
|
+
|
|
315
|
+
// 检查是否已经是新格式(有 schema_version + doc_type: module-card)
|
|
316
|
+
if (content.includes('schema_version:') && content.includes('doc_type: module-card')) {
|
|
317
|
+
skipped++;
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// 提取模块名(从 H1 或文件名)
|
|
322
|
+
const h1Match = content.match(/^# (.+)$/m);
|
|
323
|
+
const moduleId = h1Match ? h1Match[1].trim().replace(/\s+/g, '-').toLowerCase() : f.replace('.md', '');
|
|
324
|
+
|
|
325
|
+
// 提取旧的“职责”和“注意事项”作为语义内容保留
|
|
326
|
+
const dutyMatch = content.match(/## 职责\n([\s\S]*?)(?=\n##|$)/);
|
|
327
|
+
const notesMatch = content.match(/## 注意事项\n([\s\S]*?)(?=\n##|$)/);
|
|
328
|
+
const designMatch = content.match(/## 当前设计\n([\s\S]*?)(?=\n##|$)/);
|
|
329
|
+
const interfaceMatch = content.match(/## 对外接口[\s\S]*?(?=\n##|$)/);
|
|
330
|
+
|
|
331
|
+
const duty = dutyMatch ? dutyMatch[1].trim() : '';
|
|
332
|
+
const notes = notesMatch ? notesMatch[1].trim() : '';
|
|
333
|
+
const design = designMatch ? designMatch[1].trim() : '';
|
|
334
|
+
|
|
335
|
+
// 检查是否有人工备注
|
|
336
|
+
const manualMatch = content.match(/<!-- MANUAL_NOTES_START -->([\s\S]*?)<!-- MANUAL_NOTES_END -->/);
|
|
337
|
+
const manualContent = manualMatch ? manualMatch[1].trim() : '';
|
|
338
|
+
|
|
339
|
+
// 生成新格式
|
|
340
|
+
let newContent = `---
|
|
341
|
+
schema_version: 1
|
|
342
|
+
doc_type: module-card
|
|
343
|
+
module_id: ${moduleId}
|
|
344
|
+
---
|
|
345
|
+
|
|
346
|
+
# ${moduleId}
|
|
347
|
+
|
|
348
|
+
`;
|
|
349
|
+
|
|
350
|
+
if (duty) {
|
|
351
|
+
newContent += `## 定位
|
|
352
|
+
|
|
353
|
+
${duty}
|
|
354
|
+
|
|
355
|
+
`;
|
|
356
|
+
} else {
|
|
357
|
+
newContent += `## 定位
|
|
358
|
+
|
|
359
|
+
(待补充)
|
|
360
|
+
|
|
361
|
+
`;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (design || interfaceMatch) {
|
|
365
|
+
newContent += `## 契约摘要
|
|
366
|
+
|
|
367
|
+
`;
|
|
368
|
+
if (interfaceMatch) newContent += `${interfaceMatch[0].replace('## 对外接口', '').trim()}
|
|
369
|
+
|
|
370
|
+
`;
|
|
371
|
+
else newContent += `(待从源码提取)
|
|
372
|
+
|
|
373
|
+
`;
|
|
374
|
+
} else {
|
|
375
|
+
newContent += `## 契约摘要
|
|
376
|
+
|
|
377
|
+
(待从源码提取)
|
|
378
|
+
|
|
379
|
+
`;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (design) {
|
|
383
|
+
newContent += `## 关键逻辑
|
|
384
|
+
|
|
385
|
+
${design}
|
|
386
|
+
|
|
387
|
+
`;
|
|
388
|
+
} else {
|
|
389
|
+
newContent += `## 关键逻辑
|
|
390
|
+
|
|
391
|
+
(待补充)
|
|
392
|
+
|
|
393
|
+
`;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (notes) {
|
|
397
|
+
newContent += `## 注意事项
|
|
398
|
+
|
|
399
|
+
${notes}
|
|
400
|
+
|
|
401
|
+
`;
|
|
402
|
+
} else {
|
|
403
|
+
newContent += `## 注意事项
|
|
404
|
+
|
|
405
|
+
(待补充)
|
|
406
|
+
|
|
407
|
+
`;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
newContent += `## 人工备注
|
|
411
|
+
|
|
412
|
+
<!-- MANUAL_NOTES_START -->
|
|
413
|
+
${manualContent}
|
|
414
|
+
<!-- MANUAL_NOTES_END -->
|
|
415
|
+
`;
|
|
416
|
+
|
|
417
|
+
writeFileSync(filePath, newContent, 'utf8');
|
|
418
|
+
migrated++;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
console.log(`✅ 迁移完成`);
|
|
422
|
+
console.log(` 已迁移:${migrated} 个`);
|
|
423
|
+
console.log(` 已跳过(新格式):${skipped} 个`);
|
|
424
|
+
if (migrated > 0) {
|
|
425
|
+
console.log(`\n⚠️ 迁移后的卡片可能需要补充内容,建议检查并运行 sillyspec modules status`);
|
|
426
|
+
}
|
|
427
|
+
}
|
package/src/progress.js
CHANGED
|
@@ -393,6 +393,57 @@ export class ProgressManager {
|
|
|
393
393
|
});
|
|
394
394
|
}
|
|
395
395
|
|
|
396
|
+
/**
|
|
397
|
+
* 重命名变更:同步更新 DB + 目录
|
|
398
|
+
* @param {string} cwd - 项目根目录
|
|
399
|
+
* @param {string} oldName - 旧变更名
|
|
400
|
+
* @param {string} newName - 新变更名
|
|
401
|
+
*/
|
|
402
|
+
async renameChange(cwd, oldName, newName) {
|
|
403
|
+
if (!oldName || !newName) {
|
|
404
|
+
console.warn('⚠️ renameChange: 旧名或新名为空,跳过');
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
if (oldName === newName) {
|
|
408
|
+
console.warn('⚠️ renameChange: 新旧名称相同,跳过');
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
const db = await this._ensureDB(cwd);
|
|
412
|
+
// 检查旧名是否存在
|
|
413
|
+
const existing = db.transaction((sqlDb) => {
|
|
414
|
+
const row = sqlDb.exec(`SELECT name, status FROM changes WHERE name = ?`, [oldName]);
|
|
415
|
+
if (!row || !row[0] || row[0].values.length === 0) return null;
|
|
416
|
+
return { name: row[0].values[0][0], status: row[0].values[0][1] };
|
|
417
|
+
});
|
|
418
|
+
if (!existing) {
|
|
419
|
+
console.error(`❌ 变更 ${oldName} 不存在`);
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
// 检查新名是否已存在
|
|
423
|
+
const conflict = db.transaction((sqlDb) => {
|
|
424
|
+
const row = sqlDb.exec(`SELECT name FROM changes WHERE name = ?`, [newName]);
|
|
425
|
+
return row && row[0] && row[0].values.length > 0;
|
|
426
|
+
});
|
|
427
|
+
if (conflict) {
|
|
428
|
+
console.error(`❌ 变更 ${newName} 已存在`);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
// 重命名目录
|
|
432
|
+
const oldDir = this._changePath(cwd, oldName);
|
|
433
|
+
const newDir = this._changePath(cwd, newName);
|
|
434
|
+
if (existsSync(oldDir)) {
|
|
435
|
+
renameSync(oldDir, newDir);
|
|
436
|
+
} else {
|
|
437
|
+
mkdirSync(newDir, { recursive: true });
|
|
438
|
+
}
|
|
439
|
+
// 更新 DB
|
|
440
|
+
const now = new Date().toISOString();
|
|
441
|
+
db.transaction((sqlDb) => {
|
|
442
|
+
sqlDb.run(`UPDATE changes SET name = ?, last_active = ? WHERE name = ?`, [newName, now, oldName]);
|
|
443
|
+
});
|
|
444
|
+
console.log(`✅ 变更已重命名:${oldName} → ${newName}`);
|
|
445
|
+
}
|
|
446
|
+
|
|
396
447
|
/**
|
|
397
448
|
* 从活跃列表移除变更(归档时调用,不物理删除)
|
|
398
449
|
* SQL: UPDATE changes SET status = 'archived'
|
package/src/run.js
CHANGED
|
@@ -224,6 +224,7 @@ async function outputStep(stageName, stepIndex, steps, cwd, changeName, dbProjec
|
|
|
224
224
|
console.log('- 不要回头修改已完成的步骤')
|
|
225
225
|
console.log('- 不要编造不存在的 CLI 子命令')
|
|
226
226
|
console.log('- 完成后立即执行 --done 命令,不得跳过')
|
|
227
|
+
console.log('- 不要用 mv/rename 重命名变更目录,必须用 `sillyspec change-rename <旧名> <新名>`')
|
|
227
228
|
console.log('- 文档类型文件(.md/.yaml/.json 等)头部必须包含 author(git 用户名)和 created_at(精确到秒)')
|
|
228
229
|
console.log('- 执行构建/测试前必须先读 local.yaml,优先使用其中配置的命令、路径和环境变量;未配置时才使用默认值')
|
|
229
230
|
// 路径安全规则:防止 AI 拼错变更目录
|
|
@@ -310,6 +311,7 @@ export async function runCommand(args, cwd) {
|
|
|
310
311
|
const autoName = `${date}-new-change`
|
|
311
312
|
console.log(`🔄 自动创建变更:${autoName}`)
|
|
312
313
|
console.log(` 提示:可以用 --change <名称> 指定自定义变更名`)
|
|
314
|
+
console.log(` 或事后重命名:sillyspec change-rename ${autoName} <新名称>`)
|
|
313
315
|
progress = await pm.initChange(cwd, autoName)
|
|
314
316
|
changeName = autoName
|
|
315
317
|
} else {
|
package/src/stages/archive.js
CHANGED
|
@@ -35,10 +35,11 @@ export const definition = {
|
|
|
35
35
|
5. 将 git diff 文件按 \`_module-map.yaml\` 的 paths glob 匹配到模块
|
|
36
36
|
6. 生成模块影响矩阵:
|
|
37
37
|
|
|
38
|
-
| 模块 | 影响类型 | 相关文件 | 更新内容摘要 |
|
|
39
|
-
|
|
38
|
+
| 模块 | 影响类型 | 相关文件 | 更新内容摘要 | needs_review |
|
|
39
|
+
|------|----------|----------|-------------|-------------|
|
|
40
40
|
|
|
41
41
|
影响类型:逻辑变更 / 数据结构变更 / 接口变更 / 调用关系变更 / 配置变更 / 新增
|
|
42
|
+
needs_review:如果影响无法完全确定,标记为 true
|
|
42
43
|
|
|
43
44
|
7. 未匹配到任何模块的文件归入"未匹配文件"表格
|
|
44
45
|
8. 生成 \`.sillyspec/changes/<change-name>/module-impact.md\`,格式:
|
|
@@ -52,8 +53,8 @@ created_at: <now-datetime>
|
|
|
52
53
|
## 变更:<change-name>
|
|
53
54
|
|
|
54
55
|
## 模块影响矩阵
|
|
55
|
-
| 模块 | 影响类型 | 相关文件 | 更新内容摘要 |
|
|
56
|
-
|
|
56
|
+
| 模块 | 影响类型 | 相关文件 | 更新内容摘要 | needs_review |
|
|
57
|
+
|------|----------|----------|-------------|-------------|
|
|
57
58
|
|
|
58
59
|
## 未匹配文件
|
|
59
60
|
| 文件路径 | 说明 |
|
|
@@ -61,8 +62,8 @@ created_at: <now-datetime>
|
|
|
61
62
|
|
|
62
63
|
## 更新结果
|
|
63
64
|
(sync-module-docs 步骤完成后回填)
|
|
64
|
-
|
|
|
65
|
-
|
|
65
|
+
| 目标 | 操作 | 状态 |
|
|
66
|
+
|------|------|------|
|
|
66
67
|
\`\`\`
|
|
67
68
|
|
|
68
69
|
### 输出
|
|
@@ -72,71 +73,74 @@ module-impact.md 路径 + 影响模块数量 + 未匹配文件数量`,
|
|
|
72
73
|
},
|
|
73
74
|
{
|
|
74
75
|
name: 'sync-module-docs',
|
|
75
|
-
prompt: `根据 module-impact.md
|
|
76
|
+
prompt: `根据 module-impact.md 同步更新模块索引和卡片文档。
|
|
76
77
|
|
|
77
|
-
###
|
|
78
|
-
-
|
|
79
|
-
-
|
|
80
|
-
-
|
|
78
|
+
### ⚠️ 核心原则:结构化事实改 _module-map.yaml,语义解释改模块卡片
|
|
79
|
+
- \`_module-map.yaml\` 是唯一的结构化索引源(paths/tags/entrypoints/depends_on/used_by/status/needs_review)
|
|
80
|
+
- 模块卡片只负责语义说明(定位/契约摘要/关键逻辑/注意事项/人工备注)
|
|
81
|
+
- 一个信息只维护一次,不要两边重复
|
|
81
82
|
|
|
82
83
|
### 操作
|
|
83
84
|
1. 读取 \`.sillyspec/changes/<change-name>/module-impact.md\`
|
|
84
85
|
2. 如果没有受影响模块(只有 unmapped)→ 提示用户,跳过同步
|
|
85
|
-
3.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
86
|
+
3. 对每个受影响模块,按影响类型分别更新:
|
|
87
|
+
|
|
88
|
+
#### 更新 _module-map.yaml 的规则
|
|
89
|
+
- **路径变化** → 更新对应模块的 paths
|
|
90
|
+
- **依赖变化** → 更新 depends_on / used_by(同时更新反向模块的 used_by / depends_on)
|
|
91
|
+
- **导出符号变化** → 更新 entrypoints / main_symbols
|
|
92
|
+
- **新增模块** → 添加完整条目
|
|
93
|
+
- **模块废弃** → status: deprecated
|
|
94
|
+
- **不确定的影响** → needs_review: true, review_reasons 追加原因
|
|
95
|
+
- 如果 _module-map.yaml 的 generated_at 已过时,更新为当前时间
|
|
96
|
+
|
|
97
|
+
#### 更新模块卡片(modules/<module-id>.md)的规则
|
|
98
|
+
- **契约语义变化**(新增/删除对外能力) → 更新"契约摘要"
|
|
99
|
+
- **关键逻辑变化** → 更新"关键逻辑"
|
|
100
|
+
- **边界变化**(模块职责扩大/缩小) → 更新"定位"
|
|
101
|
+
- **注意事项变化** → 更新"注意事项"
|
|
102
|
+
- **内部实现变化**(不影响对外接口) → 通常不更新卡片
|
|
103
|
+
- **人工备注** → 永远保护,不覆盖
|
|
104
|
+
|
|
105
|
+
#### 人工备注保护
|
|
106
|
+
1. 用正则提取 \`<!-- MANUAL_NOTES_START -->\` 到 \`<!-- MANUAL_NOTES_END -->\` 之间的内容
|
|
107
|
+
2. 生成新卡片后,原样回填到人工备注区域
|
|
108
|
+
3. 如果标记缺失或重复 → 在 _module-map.yaml 中标记 needs_review: true
|
|
109
|
+
|
|
110
|
+
#### 新建模块卡片模板
|
|
100
111
|
\`\`\`markdown
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
112
|
+
---
|
|
113
|
+
schema_version: 1
|
|
114
|
+
doc_type: module-card
|
|
115
|
+
module_id: <module-id>
|
|
116
|
+
---
|
|
106
117
|
|
|
107
|
-
|
|
108
|
-
(一句话说清这个模块做什么)
|
|
118
|
+
# <module-id>
|
|
109
119
|
|
|
110
|
-
##
|
|
111
|
-
(架构、数据流、关键逻辑 — 描述当前状态,不是历史)
|
|
120
|
+
## 定位
|
|
112
121
|
|
|
113
|
-
##
|
|
114
|
-
| 接口 | 说明 | 调用方 |
|
|
115
|
-
|------|------|--------|
|
|
122
|
+
## 契约摘要
|
|
116
123
|
|
|
117
|
-
##
|
|
118
|
-
\`\`\`text
|
|
119
|
-
调用方 → 模块.方法() → 依赖模块.方法() → 返回结果
|
|
120
|
-
\`\`\`
|
|
124
|
+
## 关键逻辑
|
|
121
125
|
|
|
122
|
-
##
|
|
123
|
-
| 决策 | 理由 | 来源 |
|
|
124
|
-
|------|------|------|
|
|
126
|
+
## 注意事项
|
|
125
127
|
|
|
126
|
-
##
|
|
127
|
-
### 依赖本模块
|
|
128
|
-
### 本模块依赖
|
|
128
|
+
## 人工备注
|
|
129
129
|
|
|
130
|
-
|
|
131
|
-
(维护提醒、已知限制、修改时需同步检查的模块)
|
|
130
|
+
<!-- MANUAL_NOTES_START -->
|
|
132
131
|
|
|
133
|
-
|
|
134
|
-
| 日期 | 变更 | 摘要 |
|
|
135
|
-
|------|------|------|
|
|
132
|
+
<!-- MANUAL_NOTES_END -->
|
|
136
133
|
\`\`\`
|
|
137
134
|
|
|
135
|
+
4. 展示所有更新内容(diff 摘要),请用户确认
|
|
136
|
+
5. 用户确认后,写入 _module-map.yaml 和受影响的模块卡片
|
|
137
|
+
6. 用户拒绝时,不写入,但提示"module-impact.md 已保留,可稍后手动同步"
|
|
138
|
+
7. 回填 module-impact.md 的"更新结果"表格,区分目标:
|
|
139
|
+
- 目标列写 "\`_module-map.yaml: <module-id>\`" 或 "\`modules/<module-id>.md\`"
|
|
140
|
+
8. **同步完成后**,运行 \`sillyspec modules rebuild\` 刷新索引(如果需要),或手动更新 dependencies.md
|
|
141
|
+
|
|
138
142
|
### 输出
|
|
139
|
-
|
|
143
|
+
已更新的文件路径列表 + 用户确认状态`,
|
|
140
144
|
outputHint: '模块文档更新结果',
|
|
141
145
|
optional: false
|
|
142
146
|
},
|
package/src/stages/brainstorm.js
CHANGED
|
@@ -12,13 +12,15 @@ export const definition = {
|
|
|
12
12
|
2. 确认 currentStage 为 "brainstorm"
|
|
13
13
|
3. 如果有进行中的 brainstorm,提示选择继续或重新开始
|
|
14
14
|
4. 如果未初始化,提示先运行 sillyspec init
|
|
15
|
+
5. **检查变更名称是否有意义**:如果当前变更名是自动生成的(如 \`2026-06-02-new-change\`),先询问用户确认实际变更名,然后运行 \`sillyspec change-rename <旧名> <新名>\` 重命名
|
|
15
16
|
|
|
16
17
|
### 输出
|
|
17
18
|
当前状态摘要(1-2 句话)
|
|
18
19
|
|
|
19
20
|
### 注意
|
|
20
21
|
- 以 CLI 返回为准,不要自行推断阶段
|
|
21
|
-
-
|
|
22
|
+
- 如果阶段不对,输出正确提示并停止
|
|
23
|
+
- **不要用 mv 命令重命名变更目录**,必须使用 \`sillyspec change-rename\`,否则 DB 和目录会脱节`,
|
|
22
24
|
outputHint: '状态摘要',
|
|
23
25
|
optional: false
|
|
24
26
|
},
|
|
@@ -32,14 +34,25 @@ export const definition = {
|
|
|
32
34
|
3. 加载本地配置:\`cat .sillyspec/local.yaml 2>/dev/null\`
|
|
33
35
|
4. 询问本次需求属于哪个子项目
|
|
34
36
|
5. 棕地项目:读取 .sillyspec/docs/<project>/scan/ 下的 STRUCTURE.md、CONVENTIONS.md、ARCHITECTURE.md
|
|
35
|
-
6.
|
|
37
|
+
6. **加载模块索引**:读取 \`.sillyspec/docs/<project>/modules/_module-map.yaml\`(如存在)
|
|
38
|
+
- 这一步是高频操作,_module-map.yaml 回答“哪个文件属于哪个模块、模块之间怎么依赖”
|
|
39
|
+
- 用 tags/aliases 字段做需求关键词→模块的粗匹配
|
|
40
|
+
- 用 entrypoints 字段快速了解模块对外能力
|
|
41
|
+
7. 查看进行中的变更:\`ls .sillyspec/changes/ | grep -v archive\`
|
|
42
|
+
|
|
43
|
+
### 模块匹配方法
|
|
44
|
+
读取 _module-map.yaml 后,根据用户描述的需求关键词,匹配相关模块:
|
|
45
|
+
- 需求中提到"登录""认证""token" → 匹配 tags/aliases 中含这些词的模块
|
|
46
|
+
- 需求中提到特定文件路径 → 匹配 paths 字段
|
|
47
|
+
- 匹配结果用于后续 design.md 的文件变更清单
|
|
36
48
|
|
|
37
49
|
### 输出
|
|
38
|
-
项目现状理解摘要(3-5
|
|
50
|
+
项目现状理解摘要(3-5 句话,关键约定和架构决策)+ 可能涉及的模块列表
|
|
39
51
|
|
|
40
52
|
### 注意
|
|
41
53
|
- 始终询问本次需求属于哪个子项目
|
|
42
|
-
-
|
|
54
|
+
- 棕地项目必须读取数据模型章节
|
|
55
|
+
- 模块匹配只是粗筛,后续步骤会细化`,
|
|
43
56
|
outputHint: '上下文摘要',
|
|
44
57
|
optional: false
|
|
45
58
|
},
|
package/src/stages/doctor.js
CHANGED
|
@@ -238,9 +238,38 @@ timeout 5 which docker 2>/dev/null && echo "✅ Docker 可用" || echo "ℹ️ D
|
|
|
238
238
|
outputHint: '外部依赖检查结果',
|
|
239
239
|
optional: false
|
|
240
240
|
},
|
|
241
|
+
{
|
|
242
|
+
name: '模块文档健康检查',
|
|
243
|
+
prompt: `检查模块索引和卡片文档的健康状态。
|
|
244
|
+
|
|
245
|
+
### 操作
|
|
246
|
+
1. 运行 \`sillyspec modules status\` 查看模块索引概览
|
|
247
|
+
2. 读取 \`.sillyspec/docs/<project>/modules/_module-map.yaml\`
|
|
248
|
+
3. 对每个模块,检查:
|
|
249
|
+
- module_id 是否有对应的卡片文件(modules/<module-id>.md)
|
|
250
|
+
- 卡片文件是否有有效的 frontmatter(schema_version, doc_type, module_id)
|
|
251
|
+
- 人工备注标记是否配对(MANUAL_NOTES_START 和 MANUAL_NOTES_END 必须成对出现)
|
|
252
|
+
- needs_review 为 true 的模块列表
|
|
253
|
+
4. 汇总结果
|
|
254
|
+
|
|
255
|
+
### 输出格式
|
|
256
|
+
\`\`\`
|
|
257
|
+
📋 模块文档健康检查
|
|
258
|
+
✅ _module-map.yaml — 存在,N 个模块
|
|
259
|
+
⚠️ auth-service — needs_review=true,原因:...
|
|
260
|
+
❌ payment-service — 卡片文件缺失
|
|
261
|
+
✅ 所有模块人工备注标记配对正常
|
|
262
|
+
\`\`\`
|
|
263
|
+
|
|
264
|
+
### 注意
|
|
265
|
+
- 如果 _module-map.yaml 不存在,输出"模块索引未生成,建议运行 scan"
|
|
266
|
+
`,
|
|
267
|
+
outputHint: '模块文档健康状态',
|
|
268
|
+
optional: false
|
|
269
|
+
},
|
|
241
270
|
{
|
|
242
271
|
name: '汇总报告',
|
|
243
|
-
prompt:
|
|
272
|
+
prompt: `汇总前四步的所有检查结果,生成最终的自检报告。
|
|
244
273
|
|
|
245
274
|
### 输出格式
|
|
246
275
|
\`\`\`
|
package/src/stages/execute.js
CHANGED
|
@@ -41,9 +41,13 @@ const fixedPrefix = [
|
|
|
41
41
|
7. 根据 plan.md 中的任务文件路径匹配 _module-map.yaml 中的模块
|
|
42
42
|
8. 读取匹配到的 \`.sillyspec/docs/<project>/modules/<module>.md\`
|
|
43
43
|
9. 实现代码时遵循模块文档中描述的接口约定、数据流和依赖关系
|
|
44
|
+
10. **利用模块索引快速定位源码**:
|
|
45
|
+
- 用 entrypoints 字段直接找到模块对外 API 的源码位置
|
|
46
|
+
- 用 main_symbols 字段找到核心类/函数的定义位置
|
|
47
|
+
- 子代理优先读模块卡片理解语义,再读 entrypoints/main_symbols 对应的源码
|
|
44
48
|
|
|
45
49
|
### 输出
|
|
46
|
-
|
|
50
|
+
已加载的上下文摘要(含模块文档 + 源码锚点)`,
|
|
47
51
|
outputHint: '上下文摘要',
|
|
48
52
|
optional: false
|
|
49
53
|
},
|
package/src/stages/plan.js
CHANGED
|
@@ -38,9 +38,14 @@ export const fixedPrefix = [
|
|
|
38
38
|
6. 根据 design.md 的文件变更清单匹配 _module-map.yaml 中的模块
|
|
39
39
|
7. 读取匹配到的 \`.sillyspec/docs/<project>/modules/<module>.md\`
|
|
40
40
|
8. 将模块文档作为制定计划的上下文,确保计划符合模块当前设计
|
|
41
|
+
9. **利用模块依赖关系辅助分析**:
|
|
42
|
+
- 用 depends_on 判断哪些模块会被间接影响
|
|
43
|
+
- 用 used_by 判断变更会不会影响下游模块
|
|
44
|
+
- 将依赖关系纳入 Wave 分组决策(依赖同一模块的任务尽量同 Wave)
|
|
45
|
+
- 如果变更涉及多个有依赖关系的模块,在 plan.md 的任务总表中标注模块依赖
|
|
41
46
|
|
|
42
47
|
### 输出
|
|
43
|
-
|
|
48
|
+
已加载的文件清单(含模块文档 + 模块依赖关系摘要)`,
|
|
44
49
|
outputHint: '文件清单',
|
|
45
50
|
optional: false
|
|
46
51
|
},
|
package/src/stages/scan.js
CHANGED
|
@@ -191,52 +191,127 @@ local.yaml 生成结果(已存在/已生成)`,
|
|
|
191
191
|
},
|
|
192
192
|
{
|
|
193
193
|
name: '生成模块映射',
|
|
194
|
-
prompt:
|
|
194
|
+
prompt: `生成模块索引文件 \`_module-map.yaml\`,它是 agent 高频读取的机器索引。
|
|
195
|
+
|
|
196
|
+
### ⚠️ 重要:这个文件是唯一的结构化索引源
|
|
197
|
+
所有结构化事实(paths/tags/entrypoints/depends_on/used_by)只维护在这个文件里。
|
|
198
|
+
模块卡片(modules/*.md)只负责人类语义说明,不重复索引信息。
|
|
195
199
|
|
|
196
200
|
### 操作
|
|
197
|
-
1. 检查
|
|
198
|
-
2.
|
|
199
|
-
- 用 \`find . -maxdepth
|
|
200
|
-
-
|
|
201
|
-
- 路径用 glob
|
|
202
|
-
3.
|
|
203
|
-
|
|
204
|
-
|
|
201
|
+
1. 检查 \`.sillyspec/docs/<project>/modules/_module-map.yaml\` 是否已存在,已存在则跳过
|
|
202
|
+
2. 分析项目源码目录结构,识别模块划分:
|
|
203
|
+
- 用 \`find . -maxdepth 3 -type d -not -path "*/node_modules/*" -not -path "*/.git/*"\` 查看目录结构
|
|
204
|
+
- 每个有明确职责的独立目录识别为一个模块
|
|
205
|
+
- 路径用 glob 模式
|
|
206
|
+
3. 用 grep/rg 分析每个模块:
|
|
207
|
+
- \`main_symbols\`:模块导出的主要函数/类/常量(grep export / module.exports / def / class)
|
|
208
|
+
- \`entrypoints\`:对外 API 端点或命令入口(grep route / router / @Controller / @GetMapping 等)
|
|
209
|
+
- \`tags\`:模块相关关键词标签
|
|
210
|
+
- \`aliases\`:模块的别名(其他开发者可能怎么称呼这个模块)
|
|
211
|
+
4. 分析跨模块依赖关系:
|
|
212
|
+
- 用 grep import/require 分析模块间的引用链
|
|
213
|
+
- 填充 depends_on(本模块依赖谁)和 used_by(谁依赖本模块)
|
|
214
|
+
5. 生成 \`.sillyspec/docs/<project>/modules/_module-map.yaml\`
|
|
215
|
+
6. 如果 modules/ 目录不存在,先创建
|
|
216
|
+
7. 原子写入(先写 tmp 文件再 rename)
|
|
205
217
|
|
|
206
218
|
### YAML 格式
|
|
207
219
|
\`\`\`yaml
|
|
208
|
-
|
|
209
|
-
|
|
220
|
+
schema_version: 1
|
|
221
|
+
project: <project-name>
|
|
222
|
+
source_commit: <git-head-short>
|
|
223
|
+
generated_at: <now-datetime>
|
|
224
|
+
generator: sillyspec-scan
|
|
225
|
+
|
|
210
226
|
modules:
|
|
211
|
-
<module-
|
|
227
|
+
<module-id>:
|
|
228
|
+
status: active
|
|
229
|
+
doc: modules/<module-id>.md
|
|
212
230
|
paths:
|
|
213
231
|
- <glob-pattern>
|
|
214
|
-
|
|
232
|
+
tags:
|
|
233
|
+
- <tag1>
|
|
234
|
+
- <tag2>
|
|
235
|
+
aliases:
|
|
236
|
+
- <alias1>
|
|
237
|
+
entrypoints:
|
|
238
|
+
- <exported-symbol-or-api-endpoint>
|
|
239
|
+
main_symbols:
|
|
240
|
+
- <exported-class-or-function>
|
|
241
|
+
depends_on:
|
|
242
|
+
- <other-module-id>
|
|
243
|
+
used_by:
|
|
244
|
+
- <other-module-id>
|
|
245
|
+
needs_review: false
|
|
246
|
+
concerns: []
|
|
247
|
+
review_reasons: []
|
|
215
248
|
\`\`\`
|
|
216
249
|
|
|
217
250
|
### 示例
|
|
218
251
|
\`\`\`yaml
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
description: 核心工具和公共逻辑
|
|
252
|
+
schema_version: 1
|
|
253
|
+
project: multi-agent-platform
|
|
254
|
+
source_commit: abc1234
|
|
255
|
+
generated_at: 2026-06-02 22:00:00
|
|
256
|
+
generator: sillyspec-scan
|
|
225
257
|
|
|
226
|
-
|
|
258
|
+
modules:
|
|
259
|
+
auth-service:
|
|
260
|
+
status: active
|
|
261
|
+
doc: modules/auth-service.md
|
|
227
262
|
paths:
|
|
228
|
-
- src/
|
|
229
|
-
|
|
263
|
+
- src/modules/auth/**
|
|
264
|
+
- src/middleware/auth.js
|
|
265
|
+
tags:
|
|
266
|
+
- auth
|
|
267
|
+
- jwt
|
|
268
|
+
- rbac
|
|
269
|
+
- middleware
|
|
270
|
+
aliases:
|
|
271
|
+
- login
|
|
272
|
+
- token
|
|
273
|
+
- authentication
|
|
274
|
+
entrypoints:
|
|
275
|
+
- authenticate
|
|
276
|
+
- authorize
|
|
277
|
+
- signToken
|
|
278
|
+
- refreshToken
|
|
279
|
+
main_symbols:
|
|
280
|
+
- AuthService
|
|
281
|
+
- AuthController
|
|
282
|
+
- hashPassword
|
|
283
|
+
- verifyPassword
|
|
284
|
+
depends_on:
|
|
285
|
+
- users
|
|
286
|
+
- redis
|
|
287
|
+
used_by:
|
|
288
|
+
- api-routes
|
|
289
|
+
- admin-routes
|
|
290
|
+
needs_review: false
|
|
291
|
+
concerns: []
|
|
292
|
+
review_reasons: []
|
|
230
293
|
\`\`\`
|
|
231
294
|
|
|
295
|
+
### 关键规则
|
|
296
|
+
- module-id 用 kebab-case(如 auth-service)
|
|
297
|
+
- depends_on / used_by 引用其他模块的 module-id
|
|
298
|
+
- tags 和 aliases 用于 brainstorm 阶段的需求→模块匹配,尽量覆盖开发者可能用的词
|
|
299
|
+
- entrypoints 和 main_symbols 用于 execute 阶段快速定位源码
|
|
300
|
+
- 不要编造无法从源码 grep 到的符号
|
|
301
|
+
- 如果无法确定依赖关系,depends_on/used_by 留空列表,不要猜
|
|
302
|
+
|
|
232
303
|
### 输出
|
|
233
304
|
_module-map.yaml 生成结果(已存在/已生成/模块列表)`,
|
|
234
305
|
outputHint: '_module-map.yaml 生成状态',
|
|
235
306
|
optional: true
|
|
236
307
|
},
|
|
237
308
|
{
|
|
238
|
-
name: '
|
|
239
|
-
prompt: `根据 _module-map.yaml
|
|
309
|
+
name: '生成模块卡片文档',
|
|
310
|
+
prompt: `根据 _module-map.yaml 中的模块划分,为每个模块生成精简卡片文档。
|
|
311
|
+
|
|
312
|
+
### ⚠️ 重要:模块卡片只负责人类语义说明
|
|
313
|
+
结构化索引(paths/tags/entrypoints/depends_on/used_by)已经在 _module-map.yaml 里维护。
|
|
314
|
+
卡片里不要重复这些信息,只写 _module-map.yaml 无法表达的语义内容。
|
|
240
315
|
|
|
241
316
|
### 操作
|
|
242
317
|
1. 读取 \`.sillyspec/docs/<project>/modules/_module-map.yaml\`,获取模块列表和路径
|
|
@@ -255,32 +330,48 @@ _module-map.yaml 生成结果(已存在/已生成/模块列表)`,
|
|
|
255
330
|
- 环境探测结果摘要(构建工具、语言框架)
|
|
256
331
|
- scan 文档关键信息摘要(ARCHITECTURE.md 的技术栈、CONVENTIONS.md 的代码风格要点,如已生成)
|
|
257
332
|
\`\`\`
|
|
258
|
-
模块名:<module-
|
|
333
|
+
模块名:<module-id>
|
|
259
334
|
模块路径:<glob patterns>
|
|
260
|
-
目标文件:.sillyspec/docs/<project>/modules/<module>.md
|
|
335
|
+
目标文件:.sillyspec/docs/<project>/modules/<module-id>.md
|
|
261
336
|
|
|
262
337
|
操作:
|
|
263
338
|
1. 用 grep/rg 搜索模块路径范围内的源码(禁止读源码全文)
|
|
264
|
-
2.
|
|
339
|
+
2. 提取:模块职责、对外接口、关键逻辑、注意事项
|
|
265
340
|
3. 按以下模板写入目标文件:
|
|
266
341
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
##
|
|
276
|
-
|
|
277
|
-
|
|
342
|
+
---
|
|
343
|
+
schema_version: 1
|
|
344
|
+
doc_type: module-card
|
|
345
|
+
module_id: <module-id>
|
|
346
|
+
---
|
|
347
|
+
|
|
348
|
+
# <module-id>
|
|
349
|
+
|
|
350
|
+
## 定位
|
|
351
|
+
(负责什么,不负责什么 — 明确边界)
|
|
352
|
+
|
|
353
|
+
## 契约摘要
|
|
354
|
+
(核心能力列表,具体导出符号以 _module-map.yaml 的 entrypoints/main_symbols 为准)
|
|
355
|
+
|
|
356
|
+
## 关键逻辑
|
|
357
|
+
(最核心的流程摘要,用 text 伪代码,不超过 3-5 行)
|
|
358
|
+
|
|
278
359
|
## 注意事项
|
|
279
|
-
|
|
360
|
+
(维护提醒、已知限制、修改时需同步检查的模块)
|
|
361
|
+
|
|
362
|
+
## 人工备注
|
|
363
|
+
|
|
364
|
+
<!-- MANUAL_NOTES_START -->
|
|
365
|
+
|
|
366
|
+
<!-- MANUAL_NOTES_END -->
|
|
280
367
|
|
|
281
368
|
规则:
|
|
282
369
|
- 不要编造接口或依赖,只写 grep/rg 能搜到的
|
|
283
|
-
-
|
|
370
|
+
- 目标长度:500-1000 字 / 80-150 行
|
|
371
|
+
- 如果模块特别复杂(状态机、多角色交互、复杂领域规则),可以在 modules/details/ 下生成扩展文档(如 details/<module-id>-flow.md),agent 默认不读
|
|
372
|
+
- 不要重复 _module-map.yaml 中的索引信息
|
|
373
|
+
- 不要写设计决策表、完整依赖表、变更索引长表
|
|
374
|
+
- 人工备注区域保持空标记,留给用户填写
|
|
284
375
|
\`\`\`
|
|
285
376
|
|
|
286
377
|
等待所有子代理完成,验证文件是否生成且非空。
|
|
@@ -290,6 +381,72 @@ _module-map.yaml 生成结果(已存在/已生成/模块列表)`,
|
|
|
290
381
|
outputHint: '模块文档生成状态',
|
|
291
382
|
optional: true
|
|
292
383
|
},
|
|
384
|
+
{
|
|
385
|
+
name: '生成业务流程和术语表(可选)',
|
|
386
|
+
prompt: `根据模块依赖关系和源码,生成跨模块业务流程文档和术语表。
|
|
387
|
+
|
|
388
|
+
⚠️ 这一步是可选的。如果项目模块简单、流程不明显,可以跳过。
|
|
389
|
+
|
|
390
|
+
### flows/ 目录
|
|
391
|
+
目标目录:\`.sillyspec/docs/<project>/flows/\`
|
|
392
|
+
|
|
393
|
+
根据 _module-map.yaml 中的模块依赖关系,识别跨模块业务流程:
|
|
394
|
+
1. 读取 \`_module-map.yaml\`,分析 used_by 链条
|
|
395
|
+
2. 用 grep/rg 搜索路由定义、API 端点、事件处理
|
|
396
|
+
3. 识别跨模块的完整业务流程(如登录→下单→支付)
|
|
397
|
+
4. 每个流程生成一个文件:\`flows/<flow-name>.md\`
|
|
398
|
+
|
|
399
|
+
文件格式:
|
|
400
|
+
\`\`\`markdown
|
|
401
|
+
# <flow-name>
|
|
402
|
+
|
|
403
|
+
## 目标
|
|
404
|
+
(一句话描述这个流程的业务目的)
|
|
405
|
+
|
|
406
|
+
## 参与模块
|
|
407
|
+
- module-a:做什么
|
|
408
|
+
- module-b:做什么
|
|
409
|
+
|
|
410
|
+
## 流程摘要
|
|
411
|
+
\`\`\`text
|
|
412
|
+
step1 → step2 → step3
|
|
413
|
+
\`\`\`
|
|
414
|
+
|
|
415
|
+
## 失败回滚
|
|
416
|
+
| 失败点 | 处理 |
|
|
417
|
+
|---|---|
|
|
418
|
+
\`\`\`
|
|
419
|
+
|
|
420
|
+
### glossary.md
|
|
421
|
+
目标文件:\`.sillyspec/docs/<project>/glossary.md\`
|
|
422
|
+
|
|
423
|
+
提取项目专有术语:
|
|
424
|
+
1. 用 grep 搜索 TODO/FIXME 注释中的术语定义
|
|
425
|
+
2. 从数据库表注释提取
|
|
426
|
+
3. 从 README 和文档中提取定义段落
|
|
427
|
+
|
|
428
|
+
文件格式:
|
|
429
|
+
\`\`\`markdown
|
|
430
|
+
# Glossary
|
|
431
|
+
|
|
432
|
+
## Session
|
|
433
|
+
在本项目中,session 指...(项目内特殊含义)
|
|
434
|
+
|
|
435
|
+
## Order
|
|
436
|
+
订单主实体,代表...(业务定义)
|
|
437
|
+
\`\`\`
|
|
438
|
+
|
|
439
|
+
### 操作
|
|
440
|
+
1. 分析模块依赖关系,识别可能的业务流程
|
|
441
|
+
2. 如果发现 2+ 个跨模块流程,生成 flows/ 文档
|
|
442
|
+
3. 提取术语生成 glossary.md
|
|
443
|
+
4. 如果没有明显的流程或术语,跳过此步
|
|
444
|
+
|
|
445
|
+
### 输出
|
|
446
|
+
生成的文件路径列表(或"已跳过")`,
|
|
447
|
+
outputHint: '流程和术语表生成状态',
|
|
448
|
+
optional: true
|
|
449
|
+
},
|
|
293
450
|
{
|
|
294
451
|
name: '自检和提交',
|
|
295
452
|
prompt: `验证扫描完整性,清理并提交。
|
|
@@ -298,8 +455,9 @@ _module-map.yaml 生成结果(已存在/已生成/模块列表)`,
|
|
|
298
455
|
1. 检查 7 份 scan 文档是否全部生成
|
|
299
456
|
2. 检查模块文档状态(如有)
|
|
300
457
|
3. 自检门控:ARCHITECTURE(技术栈+Schema摘要)、CONVENTIONS(隐形规则+代码风格)、STRUCTURE(目录结构)、INTEGRATIONS(外部依赖)、TESTING(测试现状)、CONCERNS(技术债务)、PROJECT(项目概览)
|
|
301
|
-
4.
|
|
302
|
-
5.
|
|
458
|
+
4. 检查 flows/ 和 glossary.md 是否已生成(如有)
|
|
459
|
+
5. 清理:\`rm -f .sillyspec/docs/<project>/scan/_env-detect.md\`
|
|
460
|
+
6. \`git add .sillyspec/\` — 暂存扫描结果(不要 commit,由用户通过统一提交工具处理)
|
|
303
461
|
|
|
304
462
|
### 输出
|
|
305
463
|
扫描完整性报告
|
package/src/stages/verify.js
CHANGED
|
@@ -63,9 +63,10 @@ export const definition = {
|
|
|
63
63
|
6. 读取 \`.sillyspec/docs/<project>/modules/_module-map.yaml\`(不存在则跳过以下步骤)
|
|
64
64
|
7. 根据 design.md 的文件变更清单匹配 _module-map.yaml 中的模块
|
|
65
65
|
8. 读取匹配到的 \`.sillyspec/docs/<project>/modules/<module>.md\`
|
|
66
|
+
9. **检查模块索引可信度**:如果相关模块的 needs_review 为 true,提示"该模块索引可能不可信,需要回看模块卡片或源码"
|
|
66
67
|
|
|
67
68
|
### 输出
|
|
68
|
-
|
|
69
|
+
文件加载确认清单(含模块文档 + 索引可信度)`,
|
|
69
70
|
outputHint: '文件确认清单',
|
|
70
71
|
optional: false
|
|
71
72
|
},
|