sillyspec 3.20.2 → 3.20.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/sillyspec-archive/SKILL.md +21 -21
- package/.claude/skills/sillyspec-auto/SKILL.md +83 -83
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +44 -44
- package/.claude/skills/sillyspec-commit/SKILL.md +106 -106
- package/.claude/skills/sillyspec-continue/SKILL.md +45 -45
- package/.claude/skills/sillyspec-doctor/SKILL.md +31 -31
- package/.claude/skills/sillyspec-execute/SKILL.md +30 -30
- package/.claude/skills/sillyspec-explore/SKILL.md +109 -109
- package/.claude/skills/sillyspec-knowledge/SKILL.md +269 -269
- package/.claude/skills/sillyspec-plan/SKILL.md +21 -21
- package/.claude/skills/sillyspec-propose/SKILL.md +21 -21
- package/.claude/skills/sillyspec-quick/SKILL.md +21 -21
- package/.claude/skills/sillyspec-resume/SKILL.md +68 -68
- package/.claude/skills/sillyspec-scan/SKILL.md +21 -21
- package/.claude/skills/sillyspec-state/SKILL.md +54 -54
- package/.claude/skills/sillyspec-status/SKILL.md +21 -21
- package/.claude/skills/sillyspec-verify/SKILL.md +21 -21
- package/.claude/skills/sillyspec-workspace/SKILL.md +157 -157
- package/.husky/pre-push +13 -13
- package/CLAUDE.md +18 -18
- package/README.md +198 -188
- package/SKILL.md +90 -91
- package/bin/sillyspec.js +2 -2
- package/docs/brainstorm-plan-contract.md +64 -64
- package/docs/plan-execute-contract.md +123 -123
- package/docs/platform-scan-protocol.md +298 -298
- package/docs/revision-mode.md +115 -115
- package/docs/sillyspec/file-lifecycle/known-implementation-gaps.md +99 -99
- package/docs/sillyspec/file-lifecycle/platform-workflows-sync.md +218 -218
- package/docs/sillyspec/file-lifecycle/stage-artifacts.md +167 -167
- package/docs/sillyspec/file-lifecycle/storage-and-state.md +148 -148
- package/docs/sillyspec/file-lifecycle/worktree-and-guard.md +211 -193
- package/docs/sillyspec/file-lifecycle.md +125 -125
- package/docs/workflow-contract-regression.md +106 -106
- package/docs/worktree-isolation.md +252 -252
- package/package.json +40 -40
- package/packages/dashboard/dist/assets/index-Bq_Z2hne.js +7446 -7446
- package/packages/dashboard/dist/assets/index-O2W5RV4z.css +1 -1
- package/packages/dashboard/dist/index.html +16 -16
- package/packages/dashboard/index.html +15 -15
- package/packages/dashboard/package-lock.json +2384 -2384
- package/packages/dashboard/package.json +25 -25
- package/packages/dashboard/server/executor.js +86 -86
- package/packages/dashboard/server/index.js +588 -588
- package/packages/dashboard/server/parser.js +526 -526
- package/packages/dashboard/server/watcher.js +344 -344
- package/packages/dashboard/src/App.vue +558 -558
- package/packages/dashboard/src/components/ActionBar.vue +93 -93
- package/packages/dashboard/src/components/CommandPalette.vue +96 -96
- package/packages/dashboard/src/components/DetailPanel.vue +137 -137
- package/packages/dashboard/src/components/LogStream.vue +65 -65
- package/packages/dashboard/src/components/PipelineStage.vue +95 -95
- package/packages/dashboard/src/components/PipelineView.vue +156 -156
- package/packages/dashboard/src/components/ProjectList.vue +210 -210
- package/packages/dashboard/src/components/StageBadge.vue +67 -67
- package/packages/dashboard/src/components/StepCard.vue +94 -94
- package/packages/dashboard/src/components/detail/DocsDetail.vue +48 -48
- package/packages/dashboard/src/components/detail/GitDetail.vue +61 -61
- package/packages/dashboard/src/components/detail/TechDetail.vue +43 -43
- package/packages/dashboard/src/composables/useDashboard.js +170 -170
- package/packages/dashboard/src/composables/useKeyboard.js +119 -119
- package/packages/dashboard/src/composables/useWebSocket.js +129 -129
- package/packages/dashboard/src/main.js +8 -8
- package/packages/dashboard/src/style.css +132 -132
- package/packages/dashboard/vite.config.js +18 -18
- package/src/brainstorm-postcheck.js +158 -158
- package/src/change-list.js +52 -52
- package/src/change-risk-profile.js +352 -352
- package/src/classify-change.js +73 -73
- package/src/constants.js +70 -70
- package/src/contract-matrix.js +278 -278
- package/src/db.js +201 -201
- package/src/endpoint-extractor.js +315 -315
- package/src/hooks/claude-pre-tool-use.cjs +125 -125
- package/src/hooks/worktree-guard.js +653 -653
- package/src/index.js +922 -900
- package/src/init.js +431 -431
- package/src/knowledge-match.js +130 -130
- package/src/migrate.js +117 -117
- package/src/modules.js +482 -482
- package/src/progress.js +1734 -1734
- package/src/run.js +3465 -3358
- package/src/scan-postcheck.js +387 -383
- package/src/setup.js +398 -398
- package/src/stage-contract.js +700 -700
- package/src/stages/archive.js +160 -160
- package/src/stages/brainstorm-auto.js +229 -229
- package/src/stages/brainstorm.js +645 -645
- package/src/stages/doctor.js +365 -365
- package/src/stages/execute.js +625 -625
- package/src/stages/explore.js +34 -34
- package/src/stages/index.js +29 -29
- package/src/stages/knowledge.js +498 -498
- package/src/stages/plan-postcheck.js +511 -513
- package/src/stages/plan.js +582 -582
- package/src/stages/propose.js +174 -174
- package/src/stages/quick.js +82 -82
- package/src/stages/scan.js +558 -558
- package/src/stages/status.js +65 -65
- package/src/stages/verify.js +322 -322
- package/src/sync.js +497 -497
- package/src/task-review.js +346 -346
- package/src/workflow.js +785 -785
- package/src/worktree-apply.js +549 -549
- package/src/worktree-deps.js +185 -0
- package/src/worktree.js +982 -932
- package/templates/workflows/archive-impact.yaml +79 -79
- package/templates/workflows/scan-docs.yaml +132 -132
- package/test/brainstorm-plan-contract.test.mjs +273 -273
- package/test/check-syntax.mjs +26 -26
- package/test/contract-artifacts.test.mjs +323 -323
- package/test/decision-supersede.test.mjs +277 -277
- package/test/knowledge-match.test.mjs +231 -231
- package/test/plan-execute-contract.test.mjs +330 -330
- package/test/plan-optimization.test.mjs +572 -572
- package/test/platform-artifacts.test.mjs +166 -166
- package/test/platform-failure-samples.test.mjs +199 -199
- package/test/platform-recovery-chain.test.mjs +167 -167
- package/test/platform-recovery.test.mjs +136 -136
- package/test/platform-scan-p0.test.mjs +168 -168
- package/test/revision-v1.test.mjs +1145 -1145
- package/test/run-scan-project-parse.test.mjs +200 -200
- package/test/run-tests.mjs +48 -48
- package/test/scan-knowledge.test.mjs +175 -175
- package/test/scan-paths.test.mjs +68 -68
- package/test/scan-postcheck.test.mjs +197 -197
- package/test/spec-dir.test.mjs +206 -206
- package/test/stage-contract.test.mjs +299 -299
- package/test/stage-definitions.test.mjs +39 -39
- package/test/wait-gates.test.mjs +496 -496
- package/test/worktree-deps-provision.test.mjs +148 -0
- package/test/worktree-guard.test.mjs +71 -71
- package/test/worktree-native-overlay.test.mjs +188 -188
package/src/modules.js
CHANGED
|
@@ -1,482 +1,482 @@
|
|
|
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
|
-
* 从模块卡片 + 源码重建 _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: 关联文档路径
|
|
62
|
-
*/
|
|
63
|
-
export async function rebuildModuleMap(cwd) {
|
|
64
|
-
const mapPath = findModuleMapPath(cwd);
|
|
65
|
-
if (!mapPath) {
|
|
66
|
-
console.error('❌ 未找到 .sillyspec/docs/<project>/modules/ 目录');
|
|
67
|
-
console.log(' 提示:先运行 sillyspec run scan 生成模块文档');
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const modulesDir = join(mapPath, '..');
|
|
72
|
-
const existingMap = existsSync(mapPath) ? readFileSync(mapPath, 'utf8') : null;
|
|
73
|
-
|
|
74
|
-
// 收集现有模块卡片
|
|
75
|
-
const cards = [];
|
|
76
|
-
if (existsSync(modulesDir)) {
|
|
77
|
-
const files = readdirSync(modulesDir).filter(f => f.endsWith('.md') && f !== '_module-map.yaml');
|
|
78
|
-
for (const f of files) {
|
|
79
|
-
const content = readFileSync(join(modulesDir, f), 'utf8');
|
|
80
|
-
const fm = parseModuleCardFrontmatter(content);
|
|
81
|
-
const moduleId = fm.module_id || parseModuleIdFromFilename(f);
|
|
82
|
-
cards.push({ filename: f, moduleId });
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// 解析现有 _module-map.yaml 保留已有字段
|
|
87
|
-
let existingModules = {};
|
|
88
|
-
if (existingMap) {
|
|
89
|
-
// 简单解析:提取 modules: 下的条目
|
|
90
|
-
const lines = existingMap.split('\n');
|
|
91
|
-
let currentModule = null;
|
|
92
|
-
let inModules = false;
|
|
93
|
-
for (const line of lines) {
|
|
94
|
-
if (line.startsWith('modules:')) { inModules = true; continue; }
|
|
95
|
-
if (!inModules) continue;
|
|
96
|
-
if (line.startsWith(' ') === false && line.trim() !== '') break;
|
|
97
|
-
const moduleMatch = line.match(/^ ([a-zA-Z0-9_-]+):/);
|
|
98
|
-
if (moduleMatch) {
|
|
99
|
-
currentModule = moduleMatch[1];
|
|
100
|
-
existingModules[currentModule] = existingModules[currentModule] || { paths: [] };
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// 如果没有模块卡片也没有已有映射
|
|
106
|
-
if (cards.length === 0 && Object.keys(existingModules).length === 0) {
|
|
107
|
-
console.log('📭 没有模块卡片,也没有已有的模块映射');
|
|
108
|
-
console.log(' 提示:先运行 sillyspec run scan 生成模块文档');
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// 生成新的 _module-map.yaml
|
|
113
|
-
// 保留已有的完整字段,补齐新卡片
|
|
114
|
-
const now = new Date().toISOString().replace('T', ' ').slice(0, 19);
|
|
115
|
-
let headCommit = '';
|
|
116
|
-
try {
|
|
117
|
-
const { execSync } = await import('child_process');
|
|
118
|
-
headCommit = execSync('git rev-parse --short HEAD', { cwd, encoding: 'utf8', timeout: 5000 }).trim();
|
|
119
|
-
} catch { /* ignore */ }
|
|
120
|
-
|
|
121
|
-
let yaml = `schema_version: 2\n`;
|
|
122
|
-
yaml += `generated_at: ${now}\n`;
|
|
123
|
-
yaml += `generator: sillyspec-modules-rebuild\n`;
|
|
124
|
-
if (headCommit) yaml += `source_commit: ${headCommit}\n`;
|
|
125
|
-
yaml += `\n# Module Context Index — 机器可读的模块上下文索引\n`;
|
|
126
|
-
yaml += `# scan 阶段自动生成,brainstorm/plan/execute 阶段按任务命中模块精准注入上下文\n`;
|
|
127
|
-
yaml += `\nmodules:\n`;
|
|
128
|
-
|
|
129
|
-
// 合并:已有映射 + 新卡片
|
|
130
|
-
const allModuleIds = new Set([
|
|
131
|
-
...Object.keys(existingModules),
|
|
132
|
-
...cards.map(c => c.moduleId)
|
|
133
|
-
]);
|
|
134
|
-
|
|
135
|
-
for (const moduleId of allModuleIds) {
|
|
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
|
-
|
|
145
|
-
yaml += ` ${moduleId}:\n`;
|
|
146
|
-
yaml += ` status: active\n`;
|
|
147
|
-
if (card) yaml += ` doc: modules/${card.filename}\n`;
|
|
148
|
-
else yaml += ` doc: modules/${moduleId}.md\n`;
|
|
149
|
-
yaml += ` needs_review: false\n`;
|
|
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`;
|
|
162
|
-
yaml += `\n`;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
writeFileSync(mapPath, yaml, 'utf8');
|
|
166
|
-
console.log(`✅ _module-map.yaml 已重建:${mapPath}`);
|
|
167
|
-
console.log(` 模块数量:${allModuleIds.size}`);
|
|
168
|
-
for (const id of allModuleIds) {
|
|
169
|
-
console.log(` - ${id}`);
|
|
170
|
-
}
|
|
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 等字段。`);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* status: 显示模块索引状态
|
|
177
|
-
*/
|
|
178
|
-
export async function showModuleStatus(cwd) {
|
|
179
|
-
const mapPath = findModuleMapPath(cwd);
|
|
180
|
-
if (!mapPath || !existsSync(mapPath)) {
|
|
181
|
-
console.log('📭 未找到 _module-map.yaml');
|
|
182
|
-
console.log(' 提示:先运行 sillyspec run scan 生成模块文档');
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const content = readFileSync(mapPath, 'utf8');
|
|
187
|
-
|
|
188
|
-
// 简单解析
|
|
189
|
-
const lines = content.split('\n');
|
|
190
|
-
const modules = [];
|
|
191
|
-
let currentModule = null;
|
|
192
|
-
let needsReview = false;
|
|
193
|
-
|
|
194
|
-
for (const line of lines) {
|
|
195
|
-
const moduleMatch = line.match(/^ ([a-zA-Z0-9_-]+):$/);
|
|
196
|
-
if (moduleMatch) {
|
|
197
|
-
if (currentModule) modules.push(currentModule);
|
|
198
|
-
currentModule = { id: moduleMatch[1], hasTags: false, hasEntryPoints: false, hasDepends: false, needsReview: false, hasRole: false, hasCoreFiles: false, hasRisk: false, hasVerify: false };
|
|
199
|
-
}
|
|
200
|
-
if (currentModule) {
|
|
201
|
-
if (line.includes('tags:')) currentModule.hasTags = true;
|
|
202
|
-
if (line.includes('entrypoints:')) currentModule.hasEntryPoints = true;
|
|
203
|
-
if (line.includes('depends_on:')) currentModule.hasDepends = true;
|
|
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;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
if (currentModule) modules.push(currentModule);
|
|
212
|
-
|
|
213
|
-
console.log(`\n_module-map.yaml: ${mapPath}`);
|
|
214
|
-
console.log(`模块数量:${modules.length}\n`);
|
|
215
|
-
|
|
216
|
-
const maxId = Math.max(5, ...modules.map(m => m.id.length));
|
|
217
|
-
console.log(` ${'模块'.padEnd(maxId)} tags entry deps role core risk review`);
|
|
218
|
-
console.log(` ${''.padEnd(maxId, '─')} ──── ───── ──── ──── ──── ──── ──────`);
|
|
219
|
-
for (const m of modules) {
|
|
220
|
-
const tags = m.hasTags ? '✅' : '⬜';
|
|
221
|
-
const entry = m.hasEntryPoints ? '✅' : '⬜';
|
|
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 ? '✅' : '⬜';
|
|
227
|
-
const review = m.needsReview ? '⚠️' : '✅';
|
|
228
|
-
console.log(` ${m.id.padEnd(maxId)} ${tags} ${entry} ${deps} ${role} ${core} ${risk} ${verify} ${review}`);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
if (needsReview) {
|
|
232
|
-
console.log(`\n⚠️ 有模块标记为 needs_review,建议检查或重新 scan`);
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
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
|
-
/**
|
|
253
|
-
*/
|
|
254
|
-
export async function generateDependenciesMd(cwd) {
|
|
255
|
-
const mapPath = findModuleMapPath(cwd);
|
|
256
|
-
if (!mapPath || !existsSync(mapPath)) return;
|
|
257
|
-
|
|
258
|
-
const content = readFileSync(mapPath, 'utf8');
|
|
259
|
-
const modules = parseModuleMapSimple(content);
|
|
260
|
-
|
|
261
|
-
if (Object.keys(modules).length === 0) return;
|
|
262
|
-
|
|
263
|
-
const depsPath = join(mapPath, '..', 'dependencies.md');
|
|
264
|
-
const maxName = Math.max(6, ...Object.keys(modules).map(k => k.length));
|
|
265
|
-
|
|
266
|
-
let md = `# Module Dependencies\n\n`;
|
|
267
|
-
md += `> 自动生成,由 sillyspec 维护\n\n`;
|
|
268
|
-
md += `| ${'Module'.padEnd(maxName)} | Depends On | Used By |\n`;
|
|
269
|
-
md += `| ${''.padEnd(maxName, '-')}:--- | :--- | :--- |\n`;
|
|
270
|
-
|
|
271
|
-
for (const [id, data] of Object.entries(modules)) {
|
|
272
|
-
const depends = (data.depends_on || []).join(', ') || '—';
|
|
273
|
-
const usedBy = (data.used_by || []).join(', ') || '—';
|
|
274
|
-
md += `| ${id.padEnd(maxName)} | ${depends} | ${usedBy} |\n`;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
writeFileSync(depsPath, md, 'utf8');
|
|
278
|
-
console.log(`✅ dependencies.md 已生成`);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
function parseModuleMapSimple(content) {
|
|
282
|
-
const modules = {};
|
|
283
|
-
let currentModule = null;
|
|
284
|
-
let currentKey = null;
|
|
285
|
-
let currentArray = null;
|
|
286
|
-
|
|
287
|
-
for (const line of content.split('\n')) {
|
|
288
|
-
const moduleMatch = line.match(/^ ([a-zA-Z0-9_-]+):$/);
|
|
289
|
-
if (moduleMatch) {
|
|
290
|
-
if (currentArray && currentModule && currentKey) {
|
|
291
|
-
modules[currentModule][currentKey] = currentArray;
|
|
292
|
-
}
|
|
293
|
-
currentModule = moduleMatch[1];
|
|
294
|
-
modules[currentModule] = {};
|
|
295
|
-
currentKey = null;
|
|
296
|
-
currentArray = null;
|
|
297
|
-
continue;
|
|
298
|
-
}
|
|
299
|
-
if (!currentModule) continue;
|
|
300
|
-
|
|
301
|
-
// Array field like depends_on:\n - xxx
|
|
302
|
-
const arrayFieldMatch = line.match(/^ (depends_on|used_by|paths|tags|aliases|entrypoints|main_symbols|review_reasons):$/);
|
|
303
|
-
if (arrayFieldMatch) {
|
|
304
|
-
if (currentArray && currentKey) modules[currentModule][currentKey] = currentArray;
|
|
305
|
-
currentKey = arrayFieldMatch[1];
|
|
306
|
-
currentArray = [];
|
|
307
|
-
continue;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// Inline array like tags: [a, b]
|
|
311
|
-
const inlineArrayMatch = line.match(/^ (depends_on|used_by|paths|tags|aliases|entrypoints|main_symbols|review_reasons): \[(.*)\]$/);
|
|
312
|
-
if (inlineArrayMatch) {
|
|
313
|
-
if (currentArray && currentKey) modules[currentModule][currentKey] = currentArray;
|
|
314
|
-
const vals = inlineArrayMatch[2].split(',').map(v => v.trim()).filter(Boolean);
|
|
315
|
-
modules[currentModule][inlineArrayMatch[1]] = vals;
|
|
316
|
-
currentKey = null;
|
|
317
|
-
currentArray = null;
|
|
318
|
-
continue;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
// Scalar field
|
|
322
|
-
const scalarMatch = line.match(/^ (status|doc|needs_review): (.+)$/);
|
|
323
|
-
if (scalarMatch) {
|
|
324
|
-
if (currentArray && currentKey) { modules[currentModule][currentKey] = currentArray; currentArray = null; currentKey = null; }
|
|
325
|
-
modules[currentModule][scalarMatch[1]] = scalarMatch[2];
|
|
326
|
-
continue;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// Array item
|
|
330
|
-
const itemMatch = line.match(/^ - (.+)$/);
|
|
331
|
-
if (itemMatch && currentArray !== null) {
|
|
332
|
-
currentArray.push(itemMatch[1].trim());
|
|
333
|
-
continue;
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
// Flush last
|
|
338
|
-
if (currentArray && currentModule && currentKey) {
|
|
339
|
-
modules[currentModule][currentKey] = currentArray;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
return modules;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
/**
|
|
346
|
-
* migrate: 旧格式模块文档迁移到新格式
|
|
347
|
-
*/
|
|
348
|
-
export async function migrateModuleDocs(cwd) {
|
|
349
|
-
const mapPath = findModuleMapPath(cwd);
|
|
350
|
-
if (!mapPath || !existsSync(mapPath)) {
|
|
351
|
-
console.error('❌ 未找到 _module-map.yaml');
|
|
352
|
-
return;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
const modulesDir = join(mapPath, '..');
|
|
356
|
-
const files = readdirSync(modulesDir).filter(f => f.endsWith('.md') && f !== '_module-map.yaml');
|
|
357
|
-
|
|
358
|
-
if (files.length === 0) {
|
|
359
|
-
console.log('📭 没有模块文档需要迁移');
|
|
360
|
-
return;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
let migrated = 0;
|
|
364
|
-
let skipped = 0;
|
|
365
|
-
|
|
366
|
-
for (const f of files) {
|
|
367
|
-
const filePath = join(modulesDir, f);
|
|
368
|
-
const content = readFileSync(filePath, 'utf8');
|
|
369
|
-
|
|
370
|
-
// 检查是否已经是新格式(有 schema_version + doc_type: module-card)
|
|
371
|
-
if (content.includes('schema_version:') && content.includes('doc_type: module-card')) {
|
|
372
|
-
skipped++;
|
|
373
|
-
continue;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
// 提取模块名(从 H1 或文件名)
|
|
377
|
-
const h1Match = content.match(/^# (.+)$/m);
|
|
378
|
-
const moduleId = h1Match ? h1Match[1].trim().replace(/\s+/g, '-').toLowerCase() : f.replace('.md', '');
|
|
379
|
-
|
|
380
|
-
// 提取旧的“职责”和“注意事项”作为语义内容保留
|
|
381
|
-
const dutyMatch = content.match(/## 职责\n([\s\S]*?)(?=\n##|$)/);
|
|
382
|
-
const notesMatch = content.match(/## 注意事项\n([\s\S]*?)(?=\n##|$)/);
|
|
383
|
-
const designMatch = content.match(/## 当前设计\n([\s\S]*?)(?=\n##|$)/);
|
|
384
|
-
const interfaceMatch = content.match(/## 对外接口[\s\S]*?(?=\n##|$)/);
|
|
385
|
-
|
|
386
|
-
const duty = dutyMatch ? dutyMatch[1].trim() : '';
|
|
387
|
-
const notes = notesMatch ? notesMatch[1].trim() : '';
|
|
388
|
-
const design = designMatch ? designMatch[1].trim() : '';
|
|
389
|
-
|
|
390
|
-
// 检查是否有人工备注
|
|
391
|
-
const manualMatch = content.match(/<!-- MANUAL_NOTES_START -->([\s\S]*?)<!-- MANUAL_NOTES_END -->/);
|
|
392
|
-
const manualContent = manualMatch ? manualMatch[1].trim() : '';
|
|
393
|
-
|
|
394
|
-
// 生成新格式
|
|
395
|
-
let newContent = `---
|
|
396
|
-
schema_version: 1
|
|
397
|
-
doc_type: module-card
|
|
398
|
-
module_id: ${moduleId}
|
|
399
|
-
---
|
|
400
|
-
|
|
401
|
-
# ${moduleId}
|
|
402
|
-
|
|
403
|
-
`;
|
|
404
|
-
|
|
405
|
-
if (duty) {
|
|
406
|
-
newContent += `## 定位
|
|
407
|
-
|
|
408
|
-
${duty}
|
|
409
|
-
|
|
410
|
-
`;
|
|
411
|
-
} else {
|
|
412
|
-
newContent += `## 定位
|
|
413
|
-
|
|
414
|
-
(待补充)
|
|
415
|
-
|
|
416
|
-
`;
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
if (design || interfaceMatch) {
|
|
420
|
-
newContent += `## 契约摘要
|
|
421
|
-
|
|
422
|
-
`;
|
|
423
|
-
if (interfaceMatch) newContent += `${interfaceMatch[0].replace('## 对外接口', '').trim()}
|
|
424
|
-
|
|
425
|
-
`;
|
|
426
|
-
else newContent += `(待从源码提取)
|
|
427
|
-
|
|
428
|
-
`;
|
|
429
|
-
} else {
|
|
430
|
-
newContent += `## 契约摘要
|
|
431
|
-
|
|
432
|
-
(待从源码提取)
|
|
433
|
-
|
|
434
|
-
`;
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
if (design) {
|
|
438
|
-
newContent += `## 关键逻辑
|
|
439
|
-
|
|
440
|
-
${design}
|
|
441
|
-
|
|
442
|
-
`;
|
|
443
|
-
} else {
|
|
444
|
-
newContent += `## 关键逻辑
|
|
445
|
-
|
|
446
|
-
(待补充)
|
|
447
|
-
|
|
448
|
-
`;
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
if (notes) {
|
|
452
|
-
newContent += `## 注意事项
|
|
453
|
-
|
|
454
|
-
${notes}
|
|
455
|
-
|
|
456
|
-
`;
|
|
457
|
-
} else {
|
|
458
|
-
newContent += `## 注意事项
|
|
459
|
-
|
|
460
|
-
(待补充)
|
|
461
|
-
|
|
462
|
-
`;
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
newContent += `## 人工备注
|
|
466
|
-
|
|
467
|
-
<!-- MANUAL_NOTES_START -->
|
|
468
|
-
${manualContent}
|
|
469
|
-
<!-- MANUAL_NOTES_END -->
|
|
470
|
-
`;
|
|
471
|
-
|
|
472
|
-
writeFileSync(filePath, newContent, 'utf8');
|
|
473
|
-
migrated++;
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
console.log(`✅ 迁移完成`);
|
|
477
|
-
console.log(` 已迁移:${migrated} 个`);
|
|
478
|
-
console.log(` 已跳过(新格式):${skipped} 个`);
|
|
479
|
-
if (migrated > 0) {
|
|
480
|
-
console.log(`\n⚠️ 迁移后的卡片可能需要补充内容,建议检查并运行 sillyspec modules status`);
|
|
481
|
-
}
|
|
482
|
-
}
|
|
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
|
+
* 从模块卡片 + 源码重建 _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: 关联文档路径
|
|
62
|
+
*/
|
|
63
|
+
export async function rebuildModuleMap(cwd) {
|
|
64
|
+
const mapPath = findModuleMapPath(cwd);
|
|
65
|
+
if (!mapPath) {
|
|
66
|
+
console.error('❌ 未找到 .sillyspec/docs/<project>/modules/ 目录');
|
|
67
|
+
console.log(' 提示:先运行 sillyspec run scan 生成模块文档');
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const modulesDir = join(mapPath, '..');
|
|
72
|
+
const existingMap = existsSync(mapPath) ? readFileSync(mapPath, 'utf8') : null;
|
|
73
|
+
|
|
74
|
+
// 收集现有模块卡片
|
|
75
|
+
const cards = [];
|
|
76
|
+
if (existsSync(modulesDir)) {
|
|
77
|
+
const files = readdirSync(modulesDir).filter(f => f.endsWith('.md') && f !== '_module-map.yaml');
|
|
78
|
+
for (const f of files) {
|
|
79
|
+
const content = readFileSync(join(modulesDir, f), 'utf8');
|
|
80
|
+
const fm = parseModuleCardFrontmatter(content);
|
|
81
|
+
const moduleId = fm.module_id || parseModuleIdFromFilename(f);
|
|
82
|
+
cards.push({ filename: f, moduleId });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 解析现有 _module-map.yaml 保留已有字段
|
|
87
|
+
let existingModules = {};
|
|
88
|
+
if (existingMap) {
|
|
89
|
+
// 简单解析:提取 modules: 下的条目
|
|
90
|
+
const lines = existingMap.split('\n');
|
|
91
|
+
let currentModule = null;
|
|
92
|
+
let inModules = false;
|
|
93
|
+
for (const line of lines) {
|
|
94
|
+
if (line.startsWith('modules:')) { inModules = true; continue; }
|
|
95
|
+
if (!inModules) continue;
|
|
96
|
+
if (line.startsWith(' ') === false && line.trim() !== '') break;
|
|
97
|
+
const moduleMatch = line.match(/^ ([a-zA-Z0-9_-]+):/);
|
|
98
|
+
if (moduleMatch) {
|
|
99
|
+
currentModule = moduleMatch[1];
|
|
100
|
+
existingModules[currentModule] = existingModules[currentModule] || { paths: [] };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 如果没有模块卡片也没有已有映射
|
|
106
|
+
if (cards.length === 0 && Object.keys(existingModules).length === 0) {
|
|
107
|
+
console.log('📭 没有模块卡片,也没有已有的模块映射');
|
|
108
|
+
console.log(' 提示:先运行 sillyspec run scan 生成模块文档');
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 生成新的 _module-map.yaml
|
|
113
|
+
// 保留已有的完整字段,补齐新卡片
|
|
114
|
+
const now = new Date().toISOString().replace('T', ' ').slice(0, 19);
|
|
115
|
+
let headCommit = '';
|
|
116
|
+
try {
|
|
117
|
+
const { execSync } = await import('child_process');
|
|
118
|
+
headCommit = execSync('git rev-parse --short HEAD', { cwd, encoding: 'utf8', timeout: 5000 }).trim();
|
|
119
|
+
} catch { /* ignore */ }
|
|
120
|
+
|
|
121
|
+
let yaml = `schema_version: 2\n`;
|
|
122
|
+
yaml += `generated_at: ${now}\n`;
|
|
123
|
+
yaml += `generator: sillyspec-modules-rebuild\n`;
|
|
124
|
+
if (headCommit) yaml += `source_commit: ${headCommit}\n`;
|
|
125
|
+
yaml += `\n# Module Context Index — 机器可读的模块上下文索引\n`;
|
|
126
|
+
yaml += `# scan 阶段自动生成,brainstorm/plan/execute 阶段按任务命中模块精准注入上下文\n`;
|
|
127
|
+
yaml += `\nmodules:\n`;
|
|
128
|
+
|
|
129
|
+
// 合并:已有映射 + 新卡片
|
|
130
|
+
const allModuleIds = new Set([
|
|
131
|
+
...Object.keys(existingModules),
|
|
132
|
+
...cards.map(c => c.moduleId)
|
|
133
|
+
]);
|
|
134
|
+
|
|
135
|
+
for (const moduleId of allModuleIds) {
|
|
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
|
+
|
|
145
|
+
yaml += ` ${moduleId}:\n`;
|
|
146
|
+
yaml += ` status: active\n`;
|
|
147
|
+
if (card) yaml += ` doc: modules/${card.filename}\n`;
|
|
148
|
+
else yaml += ` doc: modules/${moduleId}.md\n`;
|
|
149
|
+
yaml += ` needs_review: false\n`;
|
|
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`;
|
|
162
|
+
yaml += `\n`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
writeFileSync(mapPath, yaml, 'utf8');
|
|
166
|
+
console.log(`✅ _module-map.yaml 已重建:${mapPath}`);
|
|
167
|
+
console.log(` 模块数量:${allModuleIds.size}`);
|
|
168
|
+
for (const id of allModuleIds) {
|
|
169
|
+
console.log(` - ${id}`);
|
|
170
|
+
}
|
|
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 等字段。`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* status: 显示模块索引状态
|
|
177
|
+
*/
|
|
178
|
+
export async function showModuleStatus(cwd) {
|
|
179
|
+
const mapPath = findModuleMapPath(cwd);
|
|
180
|
+
if (!mapPath || !existsSync(mapPath)) {
|
|
181
|
+
console.log('📭 未找到 _module-map.yaml');
|
|
182
|
+
console.log(' 提示:先运行 sillyspec run scan 生成模块文档');
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const content = readFileSync(mapPath, 'utf8');
|
|
187
|
+
|
|
188
|
+
// 简单解析
|
|
189
|
+
const lines = content.split('\n');
|
|
190
|
+
const modules = [];
|
|
191
|
+
let currentModule = null;
|
|
192
|
+
let needsReview = false;
|
|
193
|
+
|
|
194
|
+
for (const line of lines) {
|
|
195
|
+
const moduleMatch = line.match(/^ ([a-zA-Z0-9_-]+):$/);
|
|
196
|
+
if (moduleMatch) {
|
|
197
|
+
if (currentModule) modules.push(currentModule);
|
|
198
|
+
currentModule = { id: moduleMatch[1], hasTags: false, hasEntryPoints: false, hasDepends: false, needsReview: false, hasRole: false, hasCoreFiles: false, hasRisk: false, hasVerify: false };
|
|
199
|
+
}
|
|
200
|
+
if (currentModule) {
|
|
201
|
+
if (line.includes('tags:')) currentModule.hasTags = true;
|
|
202
|
+
if (line.includes('entrypoints:')) currentModule.hasEntryPoints = true;
|
|
203
|
+
if (line.includes('depends_on:')) currentModule.hasDepends = true;
|
|
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;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (currentModule) modules.push(currentModule);
|
|
212
|
+
|
|
213
|
+
console.log(`\n_module-map.yaml: ${mapPath}`);
|
|
214
|
+
console.log(`模块数量:${modules.length}\n`);
|
|
215
|
+
|
|
216
|
+
const maxId = Math.max(5, ...modules.map(m => m.id.length));
|
|
217
|
+
console.log(` ${'模块'.padEnd(maxId)} tags entry deps role core risk review`);
|
|
218
|
+
console.log(` ${''.padEnd(maxId, '─')} ──── ───── ──── ──── ──── ──── ──────`);
|
|
219
|
+
for (const m of modules) {
|
|
220
|
+
const tags = m.hasTags ? '✅' : '⬜';
|
|
221
|
+
const entry = m.hasEntryPoints ? '✅' : '⬜';
|
|
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 ? '✅' : '⬜';
|
|
227
|
+
const review = m.needsReview ? '⚠️' : '✅';
|
|
228
|
+
console.log(` ${m.id.padEnd(maxId)} ${tags} ${entry} ${deps} ${role} ${core} ${risk} ${verify} ${review}`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (needsReview) {
|
|
232
|
+
console.log(`\n⚠️ 有模块标记为 needs_review,建议检查或重新 scan`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
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
|
+
/**
|
|
253
|
+
*/
|
|
254
|
+
export async function generateDependenciesMd(cwd) {
|
|
255
|
+
const mapPath = findModuleMapPath(cwd);
|
|
256
|
+
if (!mapPath || !existsSync(mapPath)) return;
|
|
257
|
+
|
|
258
|
+
const content = readFileSync(mapPath, 'utf8');
|
|
259
|
+
const modules = parseModuleMapSimple(content);
|
|
260
|
+
|
|
261
|
+
if (Object.keys(modules).length === 0) return;
|
|
262
|
+
|
|
263
|
+
const depsPath = join(mapPath, '..', 'dependencies.md');
|
|
264
|
+
const maxName = Math.max(6, ...Object.keys(modules).map(k => k.length));
|
|
265
|
+
|
|
266
|
+
let md = `# Module Dependencies\n\n`;
|
|
267
|
+
md += `> 自动生成,由 sillyspec 维护\n\n`;
|
|
268
|
+
md += `| ${'Module'.padEnd(maxName)} | Depends On | Used By |\n`;
|
|
269
|
+
md += `| ${''.padEnd(maxName, '-')}:--- | :--- | :--- |\n`;
|
|
270
|
+
|
|
271
|
+
for (const [id, data] of Object.entries(modules)) {
|
|
272
|
+
const depends = (data.depends_on || []).join(', ') || '—';
|
|
273
|
+
const usedBy = (data.used_by || []).join(', ') || '—';
|
|
274
|
+
md += `| ${id.padEnd(maxName)} | ${depends} | ${usedBy} |\n`;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
writeFileSync(depsPath, md, 'utf8');
|
|
278
|
+
console.log(`✅ dependencies.md 已生成`);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function parseModuleMapSimple(content) {
|
|
282
|
+
const modules = {};
|
|
283
|
+
let currentModule = null;
|
|
284
|
+
let currentKey = null;
|
|
285
|
+
let currentArray = null;
|
|
286
|
+
|
|
287
|
+
for (const line of content.split('\n')) {
|
|
288
|
+
const moduleMatch = line.match(/^ ([a-zA-Z0-9_-]+):$/);
|
|
289
|
+
if (moduleMatch) {
|
|
290
|
+
if (currentArray && currentModule && currentKey) {
|
|
291
|
+
modules[currentModule][currentKey] = currentArray;
|
|
292
|
+
}
|
|
293
|
+
currentModule = moduleMatch[1];
|
|
294
|
+
modules[currentModule] = {};
|
|
295
|
+
currentKey = null;
|
|
296
|
+
currentArray = null;
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
if (!currentModule) continue;
|
|
300
|
+
|
|
301
|
+
// Array field like depends_on:\n - xxx
|
|
302
|
+
const arrayFieldMatch = line.match(/^ (depends_on|used_by|paths|tags|aliases|entrypoints|main_symbols|review_reasons):$/);
|
|
303
|
+
if (arrayFieldMatch) {
|
|
304
|
+
if (currentArray && currentKey) modules[currentModule][currentKey] = currentArray;
|
|
305
|
+
currentKey = arrayFieldMatch[1];
|
|
306
|
+
currentArray = [];
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Inline array like tags: [a, b]
|
|
311
|
+
const inlineArrayMatch = line.match(/^ (depends_on|used_by|paths|tags|aliases|entrypoints|main_symbols|review_reasons): \[(.*)\]$/);
|
|
312
|
+
if (inlineArrayMatch) {
|
|
313
|
+
if (currentArray && currentKey) modules[currentModule][currentKey] = currentArray;
|
|
314
|
+
const vals = inlineArrayMatch[2].split(',').map(v => v.trim()).filter(Boolean);
|
|
315
|
+
modules[currentModule][inlineArrayMatch[1]] = vals;
|
|
316
|
+
currentKey = null;
|
|
317
|
+
currentArray = null;
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Scalar field
|
|
322
|
+
const scalarMatch = line.match(/^ (status|doc|needs_review): (.+)$/);
|
|
323
|
+
if (scalarMatch) {
|
|
324
|
+
if (currentArray && currentKey) { modules[currentModule][currentKey] = currentArray; currentArray = null; currentKey = null; }
|
|
325
|
+
modules[currentModule][scalarMatch[1]] = scalarMatch[2];
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Array item
|
|
330
|
+
const itemMatch = line.match(/^ - (.+)$/);
|
|
331
|
+
if (itemMatch && currentArray !== null) {
|
|
332
|
+
currentArray.push(itemMatch[1].trim());
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Flush last
|
|
338
|
+
if (currentArray && currentModule && currentKey) {
|
|
339
|
+
modules[currentModule][currentKey] = currentArray;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return modules;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* migrate: 旧格式模块文档迁移到新格式
|
|
347
|
+
*/
|
|
348
|
+
export async function migrateModuleDocs(cwd) {
|
|
349
|
+
const mapPath = findModuleMapPath(cwd);
|
|
350
|
+
if (!mapPath || !existsSync(mapPath)) {
|
|
351
|
+
console.error('❌ 未找到 _module-map.yaml');
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const modulesDir = join(mapPath, '..');
|
|
356
|
+
const files = readdirSync(modulesDir).filter(f => f.endsWith('.md') && f !== '_module-map.yaml');
|
|
357
|
+
|
|
358
|
+
if (files.length === 0) {
|
|
359
|
+
console.log('📭 没有模块文档需要迁移');
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
let migrated = 0;
|
|
364
|
+
let skipped = 0;
|
|
365
|
+
|
|
366
|
+
for (const f of files) {
|
|
367
|
+
const filePath = join(modulesDir, f);
|
|
368
|
+
const content = readFileSync(filePath, 'utf8');
|
|
369
|
+
|
|
370
|
+
// 检查是否已经是新格式(有 schema_version + doc_type: module-card)
|
|
371
|
+
if (content.includes('schema_version:') && content.includes('doc_type: module-card')) {
|
|
372
|
+
skipped++;
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// 提取模块名(从 H1 或文件名)
|
|
377
|
+
const h1Match = content.match(/^# (.+)$/m);
|
|
378
|
+
const moduleId = h1Match ? h1Match[1].trim().replace(/\s+/g, '-').toLowerCase() : f.replace('.md', '');
|
|
379
|
+
|
|
380
|
+
// 提取旧的“职责”和“注意事项”作为语义内容保留
|
|
381
|
+
const dutyMatch = content.match(/## 职责\n([\s\S]*?)(?=\n##|$)/);
|
|
382
|
+
const notesMatch = content.match(/## 注意事项\n([\s\S]*?)(?=\n##|$)/);
|
|
383
|
+
const designMatch = content.match(/## 当前设计\n([\s\S]*?)(?=\n##|$)/);
|
|
384
|
+
const interfaceMatch = content.match(/## 对外接口[\s\S]*?(?=\n##|$)/);
|
|
385
|
+
|
|
386
|
+
const duty = dutyMatch ? dutyMatch[1].trim() : '';
|
|
387
|
+
const notes = notesMatch ? notesMatch[1].trim() : '';
|
|
388
|
+
const design = designMatch ? designMatch[1].trim() : '';
|
|
389
|
+
|
|
390
|
+
// 检查是否有人工备注
|
|
391
|
+
const manualMatch = content.match(/<!-- MANUAL_NOTES_START -->([\s\S]*?)<!-- MANUAL_NOTES_END -->/);
|
|
392
|
+
const manualContent = manualMatch ? manualMatch[1].trim() : '';
|
|
393
|
+
|
|
394
|
+
// 生成新格式
|
|
395
|
+
let newContent = `---
|
|
396
|
+
schema_version: 1
|
|
397
|
+
doc_type: module-card
|
|
398
|
+
module_id: ${moduleId}
|
|
399
|
+
---
|
|
400
|
+
|
|
401
|
+
# ${moduleId}
|
|
402
|
+
|
|
403
|
+
`;
|
|
404
|
+
|
|
405
|
+
if (duty) {
|
|
406
|
+
newContent += `## 定位
|
|
407
|
+
|
|
408
|
+
${duty}
|
|
409
|
+
|
|
410
|
+
`;
|
|
411
|
+
} else {
|
|
412
|
+
newContent += `## 定位
|
|
413
|
+
|
|
414
|
+
(待补充)
|
|
415
|
+
|
|
416
|
+
`;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (design || interfaceMatch) {
|
|
420
|
+
newContent += `## 契约摘要
|
|
421
|
+
|
|
422
|
+
`;
|
|
423
|
+
if (interfaceMatch) newContent += `${interfaceMatch[0].replace('## 对外接口', '').trim()}
|
|
424
|
+
|
|
425
|
+
`;
|
|
426
|
+
else newContent += `(待从源码提取)
|
|
427
|
+
|
|
428
|
+
`;
|
|
429
|
+
} else {
|
|
430
|
+
newContent += `## 契约摘要
|
|
431
|
+
|
|
432
|
+
(待从源码提取)
|
|
433
|
+
|
|
434
|
+
`;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (design) {
|
|
438
|
+
newContent += `## 关键逻辑
|
|
439
|
+
|
|
440
|
+
${design}
|
|
441
|
+
|
|
442
|
+
`;
|
|
443
|
+
} else {
|
|
444
|
+
newContent += `## 关键逻辑
|
|
445
|
+
|
|
446
|
+
(待补充)
|
|
447
|
+
|
|
448
|
+
`;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (notes) {
|
|
452
|
+
newContent += `## 注意事项
|
|
453
|
+
|
|
454
|
+
${notes}
|
|
455
|
+
|
|
456
|
+
`;
|
|
457
|
+
} else {
|
|
458
|
+
newContent += `## 注意事项
|
|
459
|
+
|
|
460
|
+
(待补充)
|
|
461
|
+
|
|
462
|
+
`;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
newContent += `## 人工备注
|
|
466
|
+
|
|
467
|
+
<!-- MANUAL_NOTES_START -->
|
|
468
|
+
${manualContent}
|
|
469
|
+
<!-- MANUAL_NOTES_END -->
|
|
470
|
+
`;
|
|
471
|
+
|
|
472
|
+
writeFileSync(filePath, newContent, 'utf8');
|
|
473
|
+
migrated++;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
console.log(`✅ 迁移完成`);
|
|
477
|
+
console.log(` 已迁移:${migrated} 个`);
|
|
478
|
+
console.log(` 已跳过(新格式):${skipped} 个`);
|
|
479
|
+
if (migrated > 0) {
|
|
480
|
+
console.log(`\n⚠️ 迁移后的卡片可能需要补充内容,建议检查并运行 sillyspec modules status`);
|
|
481
|
+
}
|
|
482
|
+
}
|