sillyspec 3.12.8 → 3.14.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sillyspec",
3
- "version": "3.12.8",
3
+ "version": "3.14.0",
4
4
  "description": "SillySpec CLI — 流程状态机,让 AI 严格按步骤来",
5
5
  "icon": "logo.jpg",
6
6
  "homepage": "https://sillyspec.ppdmq.top/",
@@ -27,6 +27,7 @@
27
27
  "@inquirer/prompts": "^7.10.1",
28
28
  "chalk": "^5.6.2",
29
29
  "chokidar": "^4.0",
30
+ "js-yaml": "^4.2.0",
30
31
  "open": "^10.1",
31
32
  "ora": "^9.3.0",
32
33
  "sql.js": "^1.14.1",
package/src/index.js CHANGED
@@ -94,6 +94,7 @@ async function main() {
94
94
 
95
95
  // 解析全局选项
96
96
  let json = false;
97
+ let saveWorkflowRunFlag = false;
97
98
  let targetDir = process.cwd();
98
99
  let tool = null;
99
100
  let interactive = false;
@@ -102,6 +103,8 @@ async function main() {
102
103
  for (let i = 0; i < args.length; i++) {
103
104
  if (args[i] === '--json') {
104
105
  json = true;
106
+ } else if (args[i] === '--save') {
107
+ saveWorkflowRunFlag = true;
105
108
  } else if (args[i] === '--dir' && args[i + 1]) {
106
109
  targetDir = resolve(args[i + 1]);
107
110
  i++;
@@ -474,6 +477,176 @@ SillySpec platform — SillyHub 平台同步
474
477
  }
475
478
  break;
476
479
  }
480
+ case 'change-rename': {
481
+ const oldName = filteredArgs[1];
482
+ const newName = filteredArgs[2];
483
+ if (!oldName || !newName) {
484
+ console.error('❌ 用法: sillyspec change-rename <旧变更名> <新变更名>');
485
+ process.exit(1);
486
+ }
487
+ const pm = new ProgressManager();
488
+ await pm.renameChange(dir, oldName, newName);
489
+ break;
490
+ }
491
+ case 'workflow': {
492
+ const wfSub = filteredArgs[1];
493
+ if (!wfSub || wfSub === 'help' || wfSub === '--help') {
494
+ console.log(`
495
+ SillySpec workflow — 工作流管理
496
+
497
+ 用法:
498
+ sillyspec workflow check <name> [--project <project>] [--json]
499
+ sillyspec workflow list
500
+ `);
501
+ break;
502
+ }
503
+ if (wfSub === 'list') {
504
+ const { listWorkflows } = await import('./workflow.js');
505
+ const names = listWorkflows(dir);
506
+ if (names.length === 0) {
507
+ console.log('未找到 workflow 定义(.sillyspec/workflows/*.yaml)');
508
+ } else {
509
+ console.log(`可用 workflow:`);
510
+ for (const name of names) {
511
+ const { loadWorkflow } = await import('./workflow.js');
512
+ const wf = loadWorkflow(dir, name);
513
+ const specVer = wf?.spec_version || wf?.version || '?';
514
+ const mode = wf?.orchestration?.mode || '?';
515
+ const roles = wf?.roles?.length || 0;
516
+ console.log(` ${name} (spec v${specVer}, ${mode}, ${roles} roles)`);
517
+ }
518
+ }
519
+ break;
520
+ }
521
+ if (wfSub === 'check') {
522
+ const { loadWorkflow, runPostCheck, listWorkflows, saveWorkflowRun } = await import('./workflow.js');
523
+ const wfName = filteredArgs[2];
524
+ if (!wfName) {
525
+ console.error('❌ 请指定 workflow 名称,例如:sillyspec workflow check scan-docs --project sillyspec');
526
+ process.exit(2);
527
+ }
528
+ const wf = loadWorkflow(dir, wfName);
529
+ if (!wf) {
530
+ console.error(`❌ 未找到 workflow: ${wfName}`);
531
+ console.error(`可用 workflow:${listWorkflows(dir).join(', ') || '无'}`);
532
+ process.exit(2);
533
+ }
534
+ // depends_on 校验
535
+ if (wf._validationErrors && wf._validationErrors.length > 0) {
536
+ console.error('❌ workflow YAML 校验失败:');
537
+ for (const err of wf._validationErrors) {
538
+ console.error(` ${err}`);
539
+ }
540
+ process.exit(2);
541
+ }
542
+ // spec_version 校验
543
+ const specVer = wf.spec_version || wf.version;
544
+ if (!specVer) {
545
+ console.error('❌ workflow YAML 缺少 spec_version 字段');
546
+ process.exit(2);
547
+ }
548
+ const SUPPORTED_SPECS = [1];
549
+ if (!SUPPORTED_SPECS.includes(specVer)) {
550
+ console.error(`❌ 不支持的 spec_version: ${specVer}(支持: ${SUPPORTED_SPECS.join(', ')})`);
551
+ process.exit(2);
552
+ }
553
+ // 解析 --project
554
+ const projectIdx = filteredArgs.indexOf('--project');
555
+ const project = projectIdx !== -1 && filteredArgs[projectIdx + 1] ? filteredArgs[projectIdx + 1] : null;
556
+ // 解析 --json(已在顶层解析)
557
+ const isJson = json;
558
+ // 解析 --change
559
+ const changeIdx = filteredArgs.indexOf('--change');
560
+ const changeName = changeIdx !== -1 && filteredArgs[changeIdx + 1] ? filteredArgs[changeIdx + 1] : null;
561
+
562
+ if (!project && wfName !== 'archive-impact') {
563
+ console.error('❌ 请指定 --project,例如:--project sillyspec');
564
+ process.exit(2);
565
+ }
566
+
567
+ // 执行检查
568
+ let resolvedWf = wf;
569
+ const placeholders = {};
570
+ if (changeName) placeholders['change-name'] = changeName;
571
+ // 替换占位符
572
+ let jsonStr = JSON.stringify(resolvedWf);
573
+ if (changeName) jsonStr = jsonStr.replace(/<change-name>/g, changeName);
574
+ resolvedWf = JSON.parse(jsonStr);
575
+
576
+ const projectName = project || 'sillyspec';
577
+ const result = runPostCheck(resolvedWf, dir, projectName, placeholders);
578
+
579
+ if (isJson) {
580
+ console.log(JSON.stringify(result, null, 2));
581
+ } else {
582
+ // 带项目维度前缀的输出(从统一结果对象格式化)
583
+ const lines = [`\n📋 Workflow Post-Check: ${result.workflow} (project: ${result.project})\n`];
584
+ for (const r of (result.roles || [])) {
585
+ const icon = r.status === 'pass' ? '✅' : '❌';
586
+ lines.push(`${icon} [${result.project}] ${r.name} (${r.id})`);
587
+ const roleFailures = (result.failures || []).filter(f => f.role_id === r.id);
588
+ for (const f of roleFailures) {
589
+ lines.push(` └─ ${f.message}`);
590
+ }
591
+ }
592
+ const wfFailures = (result.workflow_checks || []).filter(c => c.status === 'fail');
593
+ if (wfFailures.length > 0) {
594
+ lines.push('');
595
+ for (const f of wfFailures) {
596
+ lines.push(`❌ [${result.project}] 全局: ${f.detail}`);
597
+ }
598
+ }
599
+ lines.push('');
600
+ if (result.status === 'pass') {
601
+ lines.push('✅ 全部检查通过');
602
+ } else {
603
+ lines.push('❌ 存在失败项');
604
+ }
605
+ console.log(lines.join('\n'));
606
+ }
607
+
608
+ // exit code: 0=通过, 1=检查失败, 2=参数/YAML错误
609
+ if (saveWorkflowRunFlag) {
610
+ const saved = saveWorkflowRun(result, { cwd: dir, source: 'cli' });
611
+ if (saved) {
612
+ if (!isJson) console.log(`\n📁 结果已归档:${saved}`);
613
+ }
614
+ }
615
+ process.exit(result.status === 'pass' ? 0 : 1);
616
+ } else {
617
+ console.error(`❌ 未知子命令: workflow ${wfSub}`);
618
+ process.exit(1);
619
+ }
620
+ break;
621
+ }
622
+ case 'modules': {
623
+ const modulesSub = filteredArgs[1];
624
+ if (!modulesSub || modulesSub === 'help' || modulesSub === '--help') {
625
+ console.log(`
626
+ SillySpec modules — 模块文档管理
627
+
628
+ 用法:
629
+ sillyspec modules rebuild 从模块卡片 + 源码重建 _module-map.yaml
630
+ sillyspec modules status 显示模块索引状态
631
+ sillyspec modules migrate 旧格式模块文档迁移到新格式
632
+ `);
633
+ break;
634
+ }
635
+ if (modulesSub === 'rebuild') {
636
+ const { rebuildModuleMap } = await import('./modules.js');
637
+ await rebuildModuleMap(dir);
638
+ } else if (modulesSub === 'status') {
639
+ const { showModuleStatus } = await import('./modules.js');
640
+ await showModuleStatus(dir);
641
+ } else if (modulesSub === 'migrate') {
642
+ const { migrateModuleDocs } = await import('./modules.js');
643
+ await migrateModuleDocs(dir);
644
+ } else {
645
+ console.error(`❌ 未知子命令: modules ${modulesSub}`);
646
+ process.exit(1);
647
+ }
648
+ break;
649
+ }
477
650
  default:
478
651
  console.error(`❌ 未知命令: ${command}`);
479
652
  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'