autosnippet 2.9.0 → 2.10.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.
Files changed (97) hide show
  1. package/README.md +4 -4
  2. package/bin/cli.js +5 -33
  3. package/config/constitution.yaml +9 -2
  4. package/dashboard/dist/assets/{icons-CH-H9x0E.js → icons-BkT3XrKf.js} +105 -100
  5. package/dashboard/dist/assets/index-BsB7DzW4.css +1 -0
  6. package/dashboard/dist/assets/index-DdmQMrJJ.js +155 -0
  7. package/dashboard/dist/index.html +3 -3
  8. package/lib/cli/AiScanService.js +13 -11
  9. package/lib/cli/KnowledgeSyncService.js +343 -0
  10. package/lib/cli/SetupService.js +8 -26
  11. package/lib/core/gateway/GatewayActionRegistry.js +48 -58
  12. package/lib/domain/index.js +16 -11
  13. package/lib/domain/knowledge/KnowledgeEntry.js +351 -0
  14. package/lib/domain/knowledge/KnowledgeRepository.js +123 -0
  15. package/lib/domain/knowledge/Lifecycle.js +109 -0
  16. package/lib/domain/knowledge/index.js +27 -0
  17. package/lib/domain/knowledge/values/Constraints.js +125 -0
  18. package/lib/domain/knowledge/values/Content.js +86 -0
  19. package/lib/domain/knowledge/values/Quality.js +93 -0
  20. package/lib/domain/knowledge/values/Reasoning.js +69 -0
  21. package/lib/domain/knowledge/values/Relations.js +168 -0
  22. package/lib/domain/knowledge/values/Stats.js +87 -0
  23. package/lib/domain/knowledge/values/index.js +9 -0
  24. package/lib/external/ai/AiProvider.js +48 -0
  25. package/lib/external/mcp/McpServer.js +7 -5
  26. package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +2 -2
  27. package/lib/external/mcp/handlers/bootstrap.js +116 -11
  28. package/lib/external/mcp/handlers/browse.js +77 -73
  29. package/lib/external/mcp/handlers/candidate.js +29 -276
  30. package/lib/external/mcp/handlers/guard.js +2 -0
  31. package/lib/external/mcp/handlers/knowledge.js +205 -0
  32. package/lib/external/mcp/handlers/structure.js +25 -23
  33. package/lib/external/mcp/handlers/system.js +10 -12
  34. package/lib/external/mcp/tools.js +125 -138
  35. package/lib/http/HttpServer.js +4 -8
  36. package/lib/http/routes/extract.js +48 -4
  37. package/lib/http/routes/knowledge.js +246 -0
  38. package/lib/http/routes/search.js +12 -17
  39. package/lib/infrastructure/database/migrations/016_unified_knowledge_entries.js +395 -0
  40. package/lib/infrastructure/external/XcodeAutomation.js +187 -103
  41. package/lib/injection/ServiceContainer.js +49 -60
  42. package/lib/repository/knowledge/KnowledgeRepository.impl.js +373 -0
  43. package/lib/service/automation/DirectiveDetector.js +2 -3
  44. package/lib/service/automation/FileWatcher.js +67 -28
  45. package/lib/service/automation/XcodeIntegration.js +931 -156
  46. package/lib/service/automation/handlers/AlinkHandler.js +6 -4
  47. package/lib/service/automation/handlers/CreateHandler.js +53 -18
  48. package/lib/service/automation/handlers/GuardHandler.js +183 -20
  49. package/lib/service/automation/handlers/SearchHandler.js +35 -17
  50. package/lib/service/chat/CandidateGuardrail.js +1 -1
  51. package/lib/service/chat/ChatAgent.js +46 -45
  52. package/lib/service/chat/ContextWindow.js +5 -5
  53. package/lib/service/chat/ProducerAgent.js +7 -7
  54. package/lib/service/chat/tools.js +130 -123
  55. package/lib/service/guard/GuardCheckEngine.js +114 -10
  56. package/lib/service/guard/GuardService.js +59 -48
  57. package/lib/service/knowledge/ConfidenceRouter.js +159 -0
  58. package/lib/service/knowledge/KnowledgeFileWriter.js +602 -0
  59. package/lib/service/knowledge/KnowledgeService.js +725 -0
  60. package/lib/service/search/SearchEngine.js +92 -19
  61. package/lib/service/skills/SignalCollector.js +12 -7
  62. package/lib/service/skills/SkillAdvisor.js +13 -11
  63. package/lib/service/snippet/SnippetFactory.js +5 -5
  64. package/package.json +1 -1
  65. package/scripts/install-cursor-skill.js +0 -6
  66. package/scripts/migrate-md-to-knowledge.mjs +364 -0
  67. package/skills/autosnippet-analysis/SKILL.md +15 -7
  68. package/skills/autosnippet-candidates/SKILL.md +6 -6
  69. package/skills/autosnippet-coldstart/SKILL.md +7 -3
  70. package/skills/autosnippet-concepts/SKILL.md +7 -6
  71. package/skills/autosnippet-create/SKILL.md +13 -13
  72. package/skills/autosnippet-intent/SKILL.md +3 -2
  73. package/skills/autosnippet-lifecycle/SKILL.md +5 -5
  74. package/skills/autosnippet-recipes/SKILL.md +16 -4
  75. package/templates/constitution.yaml +1 -1
  76. package/templates/copilot-instructions.md +6 -6
  77. package/templates/recipes-setup/README.md +3 -3
  78. package/dashboard/dist/assets/index-CqJRvYRL.js +0 -197
  79. package/dashboard/dist/assets/index-DICm9PNa.css +0 -1
  80. package/lib/cli/CandidateSyncService.js +0 -261
  81. package/lib/cli/SyncService.js +0 -356
  82. package/lib/domain/candidate/Candidate.js +0 -196
  83. package/lib/domain/candidate/CandidateRepository.js +0 -107
  84. package/lib/domain/candidate/Reasoning.js +0 -52
  85. package/lib/domain/recipe/Recipe.js +0 -421
  86. package/lib/domain/recipe/RecipeRepository.js +0 -54
  87. package/lib/domain/types/CandidateStatus.js +0 -52
  88. package/lib/http/routes/candidates.js +0 -559
  89. package/lib/http/routes/recipes.js +0 -397
  90. package/lib/repository/candidate/CandidateRepository.impl.js +0 -230
  91. package/lib/repository/recipe/RecipeRepository.impl.js +0 -498
  92. package/lib/service/candidate/CandidateAggregator.js +0 -52
  93. package/lib/service/candidate/CandidateFileWriter.js +0 -383
  94. package/lib/service/candidate/CandidateService.js +0 -1001
  95. package/lib/service/recipe/RecipeFileWriter.js +0 -514
  96. package/lib/service/recipe/RecipeService.js +0 -786
  97. package/lib/service/recipe/RecipeStatsTracker.js +0 -148
@@ -15,6 +15,8 @@ const BM25_B = 0.75;
15
15
 
16
16
  /**
17
17
  * 分词: 中英文混合分词
18
+ * 英文: camelCase / PascalCase 拆分 + 小写化
19
+ * 中文: 单字 + 二元组(bigram)— 无需分词词典即可支持子串匹配
18
20
  */
19
21
  export function tokenize(text) {
20
22
  if (!text) return [];
@@ -23,7 +25,35 @@ export function tokenize(text) {
23
25
  // 拆全大写前缀:URLSession → URL Session, UITableView → UI Table View
24
26
  expanded = expanded.replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2');
25
27
  const normalized = expanded.toLowerCase().replace(/[^\p{L}\p{N}\s_-]/gu, ' ');
26
- const tokens = normalized.split(/[\s_-]+/).filter(t => t.length >= 2);
28
+ const rawTokens = normalized.split(/[\s_-]+/).filter(t => t.length >= 1);
29
+
30
+ const tokens = [];
31
+ // CJK 正则(中日韩统一表意文字 + 扩展区)
32
+ const cjkRe = /[\u4e00-\u9fff\u3400-\u4dbf\uf900-\ufaff]/;
33
+
34
+ for (const raw of rawTokens) {
35
+ if (cjkRe.test(raw)) {
36
+ // 中文片段:提取所有 CJK 连续子串,生成单字 + bigram 覆盖
37
+ const cjkChars = raw.match(/[\u4e00-\u9fff\u3400-\u4dbf\uf900-\ufaff]+/g) || [];
38
+ for (const seg of cjkChars) {
39
+ // 单字
40
+ for (const ch of seg) tokens.push(ch);
41
+ // bigram
42
+ for (let i = 0; i < seg.length - 1; i++) tokens.push(seg[i] + seg[i + 1]);
43
+ // 完整片段(≥3 字时额外保留,提升精确匹配权重)
44
+ if (seg.length >= 3) tokens.push(seg);
45
+ }
46
+ // 非 CJK 部分(英文/数字)也保留
47
+ const nonCjk = raw.replace(/[\u4e00-\u9fff\u3400-\u4dbf\uf900-\ufaff]+/g, ' ').trim();
48
+ if (nonCjk) {
49
+ for (const t of nonCjk.split(/\s+/)) {
50
+ if (t.length >= 2) tokens.push(t);
51
+ }
52
+ }
53
+ } else if (raw.length >= 2) {
54
+ tokens.push(raw);
55
+ }
56
+ }
27
57
  return [...new Set(tokens)];
28
58
  }
29
59
 
@@ -126,13 +156,33 @@ export class SearchEngine {
126
156
  this._cache.clear();
127
157
 
128
158
  try {
129
- // 索引 Recipes(统一模型,包含所有知识类型)
130
- const recipes = this.db.prepare(
131
- `SELECT id, title, description, language, category, knowledge_type, kind, content_json, status, tags_json, "trigger"
132
- FROM recipes WHERE status != 'deprecated'`
133
- ).all();
159
+ let entries = [];
160
+
161
+ // V3: 优先从 knowledge_entries 表索引
162
+ try {
163
+ entries = this.db.prepare(
164
+ `SELECT id, title, description, language, category, knowledge_type, kind,
165
+ content AS content_json, lifecycle, tags AS tags_json, trigger_key
166
+ FROM knowledge_entries WHERE lifecycle != 'deprecated'`
167
+ ).all();
168
+ // 标准化字段名以便后续处理
169
+ entries = entries.map(e => ({
170
+ ...e,
171
+ trigger: e.trigger_key,
172
+ status: e.lifecycle,
173
+ }));
174
+ } catch {
175
+ // V3 表不存在,回退到 V2 recipes 表
176
+ try {
177
+ entries = this.db.prepare(
178
+ `SELECT id, title, description, language, category, knowledge_type, kind,
179
+ content_json, status, tags_json, "trigger"
180
+ FROM recipes WHERE status != 'deprecated'`
181
+ ).all();
182
+ } catch { /* neither table exists */ }
183
+ }
134
184
 
135
- for (const r of recipes) {
185
+ for (const r of entries) {
136
186
  let contentText = '';
137
187
  try {
138
188
  const content = JSON.parse(r.content_json || '{}');
@@ -143,12 +193,12 @@ export class SearchEngine {
143
193
  try { tagText = JSON.parse(r.tags_json || '[]').join(' '); } catch { /* ignore */ }
144
194
  const text = [r.title, r.description, r.trigger, r.language, r.category, r.knowledge_type, tagText, contentText]
145
195
  .filter(Boolean).join(' ');
146
- this.scorer.addDocument(r.id, text, { type: 'recipe', title: r.title, trigger: r.trigger || '', status: r.status, knowledgeType: r.knowledge_type, kind: r.kind || 'pattern', language: r.language || '', category: r.category || '' });
196
+ this.scorer.addDocument(r.id, text, { type: 'knowledge', title: r.title, trigger: r.trigger || '', status: r.status, knowledgeType: r.knowledge_type, kind: r.kind || 'pattern', language: r.language || '', category: r.category || '' });
147
197
  }
148
198
 
149
199
  this._indexed = true;
150
200
  this.logger.info('Search index built', {
151
- recipes: recipes.length,
201
+ entries: entries.length,
152
202
  total: this.scorer.totalDocs,
153
203
  });
154
204
  } catch (err) {
@@ -229,14 +279,26 @@ export class SearchEngine {
229
279
  const escaped = query.replace(/[%_\\]/g, ch => `\\${ch}`);
230
280
  const pattern = `%${escaped}%`;
231
281
 
232
- if (type === 'all' || type === 'recipe' || type === 'rule' || type === 'solution') {
282
+ if (type === 'all' || type === 'recipe' || type === 'knowledge' || type === 'rule' || type === 'solution') {
233
283
  try {
234
- const rows = this.db.prepare(
235
- `SELECT id, title, description, language, category, knowledge_type, kind, status, content_json, dimensions_json, "trigger", 'recipe' as type
236
- FROM recipes
237
- WHERE status != 'deprecated' AND (title LIKE ? ESCAPE '\\' OR description LIKE ? ESCAPE '\\' OR "trigger" LIKE ? ESCAPE '\\' OR content_json LIKE ? ESCAPE '\\')
238
- LIMIT ?`
239
- ).all(pattern, pattern, pattern, pattern, limit);
284
+ let rows = [];
285
+ // V3: 优先 knowledge_entries
286
+ try {
287
+ rows = this.db.prepare(
288
+ `SELECT id, title, description, language, category, knowledge_type, kind, lifecycle as status, content AS content_json, trigger_key as "trigger", headers, module_name, 'knowledge' as type
289
+ FROM knowledge_entries
290
+ WHERE lifecycle != 'deprecated' AND (title LIKE ? ESCAPE '\\' OR description LIKE ? ESCAPE '\\' OR trigger_key LIKE ? ESCAPE '\\' OR content LIKE ? ESCAPE '\\')
291
+ LIMIT ?`
292
+ ).all(pattern, pattern, pattern, pattern, limit);
293
+ } catch {
294
+ // V3 表不存在,回退到 V2
295
+ rows = this.db.prepare(
296
+ `SELECT id, title, description, language, category, knowledge_type, kind, status, content_json, dimensions_json, "trigger", 'recipe' as type
297
+ FROM recipes
298
+ WHERE status != 'deprecated' AND (title LIKE ? ESCAPE '\\' OR description LIKE ? ESCAPE '\\' OR "trigger" LIKE ? ESCAPE '\\' OR content_json LIKE ? ESCAPE '\\')
299
+ LIMIT ?`
300
+ ).all(pattern, pattern, pattern, pattern, limit);
301
+ }
240
302
  // 基础相关性排序:trigger 精确 > 标题匹配 > 描述匹配 > 内容匹配
241
303
  const lowerQ = query.toLowerCase();
242
304
  results.push(...rows.map(r => {
@@ -349,9 +411,17 @@ export class SearchEngine {
349
411
  try {
350
412
  const ids = items.map(it => it.id);
351
413
  const placeholders = ids.map(() => '?').join(',');
352
- const rows = this.db.prepare(
353
- `SELECT id, content_json, dimensions_json, description, "trigger" FROM recipes WHERE id IN (${placeholders})`
354
- ).all(...ids);
414
+ let rows = [];
415
+ // V3: 优先 knowledge_entries
416
+ try {
417
+ rows = this.db.prepare(
418
+ `SELECT id, content AS content_json, description, trigger_key as "trigger", headers, module_name FROM knowledge_entries WHERE id IN (${placeholders})`
419
+ ).all(...ids);
420
+ } catch {
421
+ rows = this.db.prepare(
422
+ `SELECT id, content_json, dimensions_json, description, "trigger" FROM recipes WHERE id IN (${placeholders})`
423
+ ).all(...ids);
424
+ }
355
425
  const rowMap = new Map(rows.map(r => [r.id, r]));
356
426
  for (const item of items) {
357
427
  const row = rowMap.get(item.id);
@@ -360,6 +430,9 @@ export class SearchEngine {
360
430
  item.dimensions_json = row.dimensions_json || null;
361
431
  item.description = item.description || row.description || '';
362
432
  item.trigger = item.trigger || row.trigger || '';
433
+ // V3: headers & module_name 是独立列
434
+ if (row.headers) item.headers = row.headers;
435
+ if (row.module_name) item.module_name = row.module_name;
363
436
  }
364
437
  }
365
438
  } catch { /* DB may not be available */ }
@@ -336,11 +336,16 @@ export class SignalCollector {
336
336
  #collectRecipeSignals() {
337
337
  try {
338
338
  if (!this.#db) return [];
339
+ // V3: knowledge_entries 统一表,统计字段在 stats/quality JSON 中
339
340
  const rows = this.#db.prepare(
340
341
  `SELECT id, title, knowledge_type, category, language,
341
- adoption_count, application_count, success_count,
342
- quality_overall, updated_at
343
- FROM recipes ORDER BY updated_at DESC LIMIT 30`
342
+ json_extract(stats, '$.adoptions') as adoption_count,
343
+ json_extract(stats, '$.applications') as application_count,
344
+ json_extract(quality, '$.overall') as quality_overall,
345
+ updated_at
346
+ FROM knowledge_entries
347
+ WHERE lifecycle = 'active'
348
+ ORDER BY updated_at DESC LIMIT 30`
344
349
  ).all();
345
350
  return rows;
346
351
  } catch { return []; }
@@ -349,11 +354,11 @@ export class SignalCollector {
349
354
  #collectCandidateSignals() {
350
355
  try {
351
356
  if (!this.#db) return [];
357
+ // V3: candidates 已合并到 knowledge_entries,lifecycle='pending' 即为候选
352
358
  const rows = this.#db.prepare(
353
- `SELECT id, source, status, language, category,
354
- json_extract(metadata_json, '$.title') as title,
355
- created_at
356
- FROM candidates WHERE status = 'pending'
359
+ `SELECT id, source, lifecycle as status, language, category,
360
+ title, created_at
361
+ FROM knowledge_entries WHERE lifecycle = 'pending'
357
362
  ORDER BY created_at DESC LIMIT 30`
358
363
  ).all();
359
364
  return rows;
@@ -202,8 +202,8 @@ export class SkillAdvisor {
202
202
  // 按 category 分布
203
203
  const categories = this.#db.prepare(`
204
204
  SELECT category, COUNT(*) as cnt
205
- FROM recipes
206
- WHERE category IS NOT NULL AND category != ''
205
+ FROM knowledge_entries
206
+ WHERE lifecycle = 'active' AND category IS NOT NULL AND category != ''
207
207
  GROUP BY category
208
208
  ORDER BY cnt DESC
209
209
  `).all();
@@ -211,8 +211,8 @@ export class SkillAdvisor {
211
211
  // 按 language 分布
212
212
  const languages = this.#db.prepare(`
213
213
  SELECT language, COUNT(*) as cnt
214
- FROM recipes
215
- WHERE language IS NOT NULL AND language != ''
214
+ FROM knowledge_entries
215
+ WHERE lifecycle = 'active' AND language IS NOT NULL AND language != ''
216
216
  GROUP BY language
217
217
  ORDER BY cnt DESC
218
218
  `).all();
@@ -231,14 +231,15 @@ export class SkillAdvisor {
231
231
  });
232
232
  }
233
233
 
234
- // 高使用量 Recipe 统计(adoption_count + application_count >= 5
234
+ // 高使用量 Recipe 统计(V3: stats JSON 中的 adoptions + applications
235
235
  let hotRecipes = [];
236
236
  try {
237
237
  hotRecipes = this.#db.prepare(`
238
238
  SELECT title, category,
239
- (adoption_count + application_count) as total_usage
240
- FROM recipes
241
- WHERE (adoption_count + application_count) >= 5
239
+ (COALESCE(json_extract(stats, '$.adoptions'), 0) + COALESCE(json_extract(stats, '$.applications'), 0)) as total_usage
240
+ FROM knowledge_entries
241
+ WHERE lifecycle = 'active'
242
+ AND (COALESCE(json_extract(stats, '$.adoptions'), 0) + COALESCE(json_extract(stats, '$.applications'), 0)) >= 5
242
243
  ORDER BY total_usage DESC
243
244
  LIMIT 10
244
245
  `).all();
@@ -262,12 +263,13 @@ export class SkillAdvisor {
262
263
  if (!this.#db) return { summary: 'DB 不可用', suggestions };
263
264
 
264
265
  try {
266
+ // V3: candidates 已合并到 knowledge_entries,ifecycle='pending' 即为候选
265
267
  const stats = this.#db.prepare(`
266
268
  SELECT
267
269
  COUNT(*) as total,
268
- SUM(CASE WHEN status='pending' THEN 1 ELSE 0 END) as pending,
269
- SUM(CASE WHEN status='rejected' THEN 1 ELSE 0 END) as rejected
270
- FROM candidates
270
+ SUM(CASE WHEN lifecycle='pending' THEN 1 ELSE 0 END) as pending,
271
+ SUM(CASE WHEN lifecycle='deprecated' THEN 1 ELSE 0 END) as rejected
272
+ FROM knowledge_entries
271
273
  `).get();
272
274
 
273
275
  // 大量被拒绝 → 提示候选质量 Skill
@@ -45,16 +45,16 @@ const SNIPPET_TEMPLATE = `<?xml version="1.0" encoding="UTF-8"?>
45
45
 
46
46
  export class SnippetFactory {
47
47
  /**
48
- * @param {object} [recipeRepository] — RecipeRepositoryImpl (可选,用于列表查询)
48
+ * @param {object} [knowledgeRepository] — KnowledgeRepositoryImpl(可选,用于列表查询)
49
49
  */
50
- constructor(recipeRepository) {
51
- this._recipeRepo = recipeRepository || null;
50
+ constructor(knowledgeRepository) {
51
+ this._recipeRepo = knowledgeRepository || null;
52
52
  }
53
53
 
54
54
  /**
55
- * 运行时注入 recipeRepository(用于延迟绑定场景)
55
+ * 运行时注入 knowledgeRepository(用于延迟绑定场景)
56
56
  */
57
- setRecipeRepository(repo) {
57
+ setKnowledgeRepository(repo) {
58
58
  this._recipeRepo = repo;
59
59
  }
60
60
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autosnippet",
3
- "version": "2.9.0",
3
+ "version": "2.10.0",
4
4
  "description": "AutoSnippet - 连接开发者、AI 与项目知识库的工具",
5
5
  "type": "module",
6
6
  "main": "lib/bootstrap.js",
@@ -22,7 +22,6 @@ const require = createRequire(import.meta.url);
22
22
  import fs from 'node:fs';
23
23
  import path from 'node:path';
24
24
  import * as defaults from '../lib/infrastructure/config/Defaults.js';
25
- import { RecipeStatsTracker } from '../lib/service/recipe/RecipeStatsTracker.js';
26
25
 
27
26
  const autoSnippetRoot = path.join(__dirname, '..');
28
27
  const skillsSource = path.join(autoSnippetRoot, 'skills');
@@ -156,11 +155,6 @@ function buildProjectRecipesContext(projectRoot) {
156
155
  if (!fs.existsSync(recipesDir)) return null;
157
156
  const mdFiles = collectMdFiles(recipesDir, recipesDir).sort();
158
157
  if (mdFiles.length === 0) return null;
159
- let stats = { byFile: {} };
160
- try {
161
- const tracker = new RecipeStatsTracker(projectRoot);
162
- stats = tracker.getStats();
163
- } catch (_) {}
164
158
 
165
159
  const lines = [
166
160
  '# Project Recipes Index\n\n',
@@ -0,0 +1,364 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * migrate-md-to-knowledge.mjs — 将旧格式 .md 文件迁移为统一 Knowledge 格式
4
+ *
5
+ * 扫描 AutoSnippet/candidates/ 和 AutoSnippet/recipes/ 下的旧格式 .md 文件,
6
+ * 用旧 parser 解析 → 转换为 KnowledgeEntry → 用 KnowledgeFileWriter 重新序列化。
7
+ *
8
+ * 旧文件会备份到 AutoSnippet/_backup/{candidates|recipes}/ 目录下。
9
+ *
10
+ * 用法:
11
+ * node scripts/migrate-md-to-knowledge.mjs [projectRoot]
12
+ *
13
+ * --dry-run 只报告,不写入
14
+ * --no-backup 不创建备份
15
+ */
16
+
17
+ import fs from 'node:fs';
18
+ import path from 'node:path';
19
+ import { fileURLToPath } from 'node:url';
20
+
21
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
22
+
23
+ // ── 解析命令行 ──
24
+ const args = process.argv.slice(2);
25
+ const dryRun = args.includes('--dry-run');
26
+ const noBackup = args.includes('--no-backup');
27
+ const projectRoot = args.find(a => !a.startsWith('--')) || process.cwd();
28
+
29
+ // ── 动态导入(兼容 ESM) ──
30
+ const { parseRecipeMarkdown } = await import('../lib/service/recipe/RecipeFileWriter.js');
31
+ const { parseCandidateMarkdown } = await import('../lib/service/candidate/CandidateFileWriter.js');
32
+ const { KnowledgeEntry } = await import('../lib/domain/knowledge/KnowledgeEntry.js');
33
+ const { KnowledgeFileWriter } = await import('../lib/service/knowledge/KnowledgeFileWriter.js');
34
+ const { Lifecycle } = await import('../lib/domain/knowledge/Lifecycle.js');
35
+ const { inferKind: inferKindV3 } = await import('../lib/domain/knowledge/Lifecycle.js');
36
+
37
+ const RECIPES_DIR = 'AutoSnippet/recipes';
38
+ const CANDIDATES_DIR = 'AutoSnippet/candidates';
39
+ const BACKUP_DIR = 'AutoSnippet/_backup';
40
+
41
+ const recipesDir = path.join(projectRoot, RECIPES_DIR);
42
+ const candidatesDir = path.join(projectRoot, CANDIDATES_DIR);
43
+ const backupDir = path.join(projectRoot, BACKUP_DIR);
44
+
45
+ const report = {
46
+ recipes: { total: 0, migrated: 0, skipped: 0, errors: [] },
47
+ candidates: { total: 0, migrated: 0, skipped: 0, errors: [] },
48
+ };
49
+
50
+ // ── 工具函数 ──
51
+
52
+ function collectMdFiles(dir) {
53
+ if (!fs.existsSync(dir)) return [];
54
+ const results = [];
55
+ const walk = (curDir, base) => {
56
+ for (const entry of fs.readdirSync(curDir, { withFileTypes: true })) {
57
+ const full = path.join(curDir, entry.name);
58
+ const rel = base ? `${base}/${entry.name}` : entry.name;
59
+ if (entry.isDirectory()) {
60
+ walk(full, rel);
61
+ } else if (entry.name.endsWith('.md') && !entry.name.startsWith('_')) {
62
+ results.push({ absPath: full, relPath: rel });
63
+ }
64
+ }
65
+ };
66
+ walk(dir, '');
67
+ return results;
68
+ }
69
+
70
+ function backupFile(absPath, type) {
71
+ if (noBackup || dryRun) return;
72
+ const rel = path.relative(
73
+ type === 'recipe' ? recipesDir : candidatesDir,
74
+ absPath
75
+ );
76
+ const dest = path.join(backupDir, type === 'recipe' ? 'recipes' : 'candidates', rel);
77
+ const destDir = path.dirname(dest);
78
+ if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
79
+ fs.copyFileSync(absPath, dest);
80
+ }
81
+
82
+ /**
83
+ * 判断文件是否已经是新格式(包含 lifecycle 和 _content 字段)
84
+ */
85
+ function isAlreadyNewFormat(content) {
86
+ const fm = content.match(/^---\s*\r?\n([\s\S]*?)\r?\n---/);
87
+ if (!fm) return false;
88
+ return fm[1].includes('lifecycle:') && fm[1].includes('_content:');
89
+ }
90
+
91
+ // ═══ Recipe 迁移 ═══
92
+
93
+ function migrateRecipe(absPath, relPath) {
94
+ const content = fs.readFileSync(absPath, 'utf8');
95
+
96
+ // 跳过已迁移的文件
97
+ if (isAlreadyNewFormat(content)) {
98
+ report.recipes.skipped++;
99
+ return;
100
+ }
101
+
102
+ const parsed = parseRecipeMarkdown(content, relPath);
103
+
104
+ // 映射旧 Recipe 字段 → KnowledgeEntry wire format
105
+ const statusMap = {
106
+ active: Lifecycle.ACTIVE,
107
+ deprecated: Lifecycle.DEPRECATED,
108
+ draft: Lifecycle.DRAFT,
109
+ };
110
+
111
+ // 提取旧 dimensions
112
+ const quality = parsed.quality || {};
113
+ const stats = parsed.statistics || {};
114
+
115
+ const wireData = {
116
+ id: parsed.id,
117
+ title: parsed.title,
118
+ trigger: parsed.trigger || '',
119
+ description: parsed.summaryCn || parsed.summaryEn || '',
120
+ lifecycle: statusMap[parsed.status] || Lifecycle.ACTIVE,
121
+ lifecycle_history: [],
122
+ probation: false,
123
+ language: parsed.language || 'swift',
124
+ category: parsed.category || 'general',
125
+ kind: parsed.kind || inferKindV3(parsed.knowledgeType || 'code-pattern'),
126
+ knowledge_type: parsed.knowledgeType || 'code-pattern',
127
+ complexity: parsed.complexity || 'intermediate',
128
+ scope: parsed.scope || 'universal',
129
+ difficulty: parsed.difficulty || null,
130
+ tags: parsed.tags || [],
131
+ summary_cn: parsed.summaryCn || '',
132
+ summary_en: parsed.summaryEn || '',
133
+ usage_guide_cn: parsed.usageGuideCn || '',
134
+ usage_guide_en: parsed.usageGuideEn || '',
135
+ content: {
136
+ pattern: '',
137
+ markdown: '',
138
+ rationale: '',
139
+ steps: [],
140
+ code_changes: [],
141
+ verification: null,
142
+ },
143
+ relations: parsed.relations || {},
144
+ constraints: parsed.constraints || {},
145
+ reasoning: {},
146
+ quality: {
147
+ completeness: quality.codeCompleteness ?? quality.completeness ?? 0,
148
+ adaptation: quality.projectAdaptation ?? quality.adaptation ?? 0,
149
+ documentation: quality.documentationClarity ?? quality.documentation ?? 0,
150
+ overall: quality.overall ?? 0,
151
+ grade: quality.grade || 'F',
152
+ },
153
+ stats: {
154
+ views: stats.viewCount ?? stats.views ?? 0,
155
+ adoptions: stats.adoptionCount ?? stats.adoptions ?? 0,
156
+ applications: stats.applicationCount ?? stats.applications ?? 0,
157
+ guard_hits: stats.guardHitCount ?? stats.guard_hits ?? 0,
158
+ search_hits: stats.searchHits ?? stats.search_hits ?? 0,
159
+ authority: parsed.authority ?? stats.authority ?? 0,
160
+ },
161
+ headers: parsed.headers || [],
162
+ header_paths: [],
163
+ module_name: '',
164
+ include_headers: false,
165
+ agent_notes: null,
166
+ ai_insight: null,
167
+ reviewed_by: null,
168
+ reviewed_at: null,
169
+ rejection_reason: parsed.deprecationReason || null,
170
+ source: 'migration',
171
+ source_file: `${RECIPES_DIR}/${relPath}`,
172
+ source_candidate_id: parsed.sourceCandidate || null,
173
+ created_by: parsed.createdBy || 'system',
174
+ created_at: parsed.createdAt || Math.floor(Date.now() / 1000),
175
+ updated_at: parsed.updatedAt || Math.floor(Date.now() / 1000),
176
+ published_at: parsed.publishedAt || null,
177
+ published_by: parsed.publishedBy || null,
178
+ };
179
+
180
+ // 提取 body 内容到 content
181
+ const bodyMatch = content.match(/^---[\s\S]*?---\s*\r?\n([\s\S]*)$/);
182
+ if (bodyMatch) {
183
+ const body = bodyMatch[1].trim();
184
+ const codeMatch = body.match(/```\w*\n([\s\S]*?)```/);
185
+ if (codeMatch) {
186
+ wireData.content.pattern = codeMatch[1].trimEnd();
187
+ }
188
+ // 如果有完整 markdown 内容(项目特写)
189
+ if (body.includes('— 项目特写') || (body.length > 500 && body.startsWith('#'))) {
190
+ wireData.content.markdown = body;
191
+ wireData.content.pattern = '';
192
+ }
193
+ }
194
+
195
+ // 构建实体并重新序列化
196
+ const entry = KnowledgeEntry.fromJSON(wireData);
197
+ const writer = new KnowledgeFileWriter(projectRoot);
198
+ const newMarkdown = writer.serialize(entry);
199
+
200
+ if (!dryRun) {
201
+ backupFile(absPath, 'recipe');
202
+ fs.writeFileSync(absPath, newMarkdown, 'utf8');
203
+ }
204
+
205
+ report.recipes.migrated++;
206
+ }
207
+
208
+ // ═══ Candidate 迁移 ═══
209
+
210
+ function migrateCandidate(absPath, relPath) {
211
+ const content = fs.readFileSync(absPath, 'utf8');
212
+
213
+ // 跳过已迁移的文件
214
+ if (isAlreadyNewFormat(content)) {
215
+ report.candidates.skipped++;
216
+ return;
217
+ }
218
+
219
+ const parsed = parseCandidateMarkdown(content, relPath);
220
+
221
+ // 映射旧 Candidate 状态 → lifecycle
222
+ const statusMap = {
223
+ pending: Lifecycle.PENDING,
224
+ approved: Lifecycle.APPROVED,
225
+ rejected: Lifecycle.REJECTED,
226
+ applied: Lifecycle.ACTIVE,
227
+ draft: Lifecycle.DRAFT,
228
+ };
229
+
230
+ const meta = parsed._metadata || {};
231
+ const reasoning = parsed._reasoning || {};
232
+ const code = parsed._bodyCode || '';
233
+
234
+ // 判断内容类型
235
+ const isMarkdown = code && (
236
+ code.includes('— 项目特写') || /^#{1,3}\s/.test(code.trimStart())
237
+ );
238
+
239
+ const wireData = {
240
+ id: parsed.id,
241
+ title: meta.title || meta.description || (code ? code.substring(0, 60) : ''),
242
+ trigger: meta.trigger || '',
243
+ description: meta.description || '',
244
+ lifecycle: statusMap[parsed.status] || Lifecycle.PENDING,
245
+ lifecycle_history: parsed._statusHistory || [],
246
+ probation: false,
247
+ language: parsed.language || 'swift',
248
+ category: meta.category || parsed.category || 'general',
249
+ kind: inferKindV3(meta.knowledgeType || 'code-pattern'),
250
+ knowledge_type: meta.knowledgeType || 'code-pattern',
251
+ complexity: meta.complexity || 'intermediate',
252
+ scope: meta.scope || 'universal',
253
+ difficulty: meta.difficulty || null,
254
+ tags: meta.tags || [],
255
+ summary_cn: meta.summary || meta.summary_cn || '',
256
+ summary_en: meta.summary_en || '',
257
+ usage_guide_cn: meta.usageGuide || meta.usageGuide_cn || '',
258
+ usage_guide_en: meta.usageGuide_en || '',
259
+ content: {
260
+ pattern: isMarkdown ? '' : code,
261
+ markdown: isMarkdown ? code : '',
262
+ rationale: meta.rationale || (typeof reasoning === 'object' ? reasoning.whyStandard : '') || '',
263
+ steps: meta.steps || [],
264
+ code_changes: meta.codeChanges || [],
265
+ verification: meta.verification || null,
266
+ },
267
+ relations: Array.isArray(meta.relations)
268
+ ? { related: meta.relations.map(r => typeof r === 'string' ? { target: r, description: '' } : r) }
269
+ : (meta.relations || {}),
270
+ constraints: meta.constraints || {},
271
+ reasoning: {
272
+ why_standard: (typeof reasoning === 'object' ? reasoning.whyStandard : '') || '',
273
+ sources: (typeof reasoning === 'object' ? reasoning.sources : []) || [],
274
+ confidence: (typeof reasoning === 'object' ? reasoning.confidence : 0.7) ?? 0.7,
275
+ quality_signals: (typeof reasoning === 'object' ? reasoning.qualitySignals : {}) || {},
276
+ alternatives: (typeof reasoning === 'object' ? reasoning.alternatives : []) || [],
277
+ },
278
+ quality: meta.quality || {},
279
+ stats: {},
280
+ headers: meta.headers || [],
281
+ header_paths: [],
282
+ module_name: '',
283
+ include_headers: false,
284
+ agent_notes: null,
285
+ ai_insight: null,
286
+ reviewed_by: parsed.approvedBy || parsed.rejectedBy || null,
287
+ reviewed_at: parsed.approvedAt || null,
288
+ rejection_reason: parsed.rejectionReason || null,
289
+ source: parsed.source || 'migration',
290
+ source_file: `${CANDIDATES_DIR}/${relPath}`,
291
+ source_candidate_id: null,
292
+ created_by: parsed.createdBy || 'system',
293
+ created_at: parsed.createdAt || Math.floor(Date.now() / 1000),
294
+ updated_at: parsed.updatedAt || Math.floor(Date.now() / 1000),
295
+ published_at: null,
296
+ published_by: null,
297
+ };
298
+
299
+ // 构建实体并重新序列化
300
+ const entry = KnowledgeEntry.fromJSON(wireData);
301
+ const writer = new KnowledgeFileWriter(projectRoot);
302
+ const newMarkdown = writer.serialize(entry);
303
+
304
+ if (!dryRun) {
305
+ backupFile(absPath, 'candidate');
306
+ fs.writeFileSync(absPath, newMarkdown, 'utf8');
307
+ }
308
+
309
+ report.candidates.migrated++;
310
+ }
311
+
312
+ // ═══ 主流程 ═══
313
+
314
+ console.log(`\n🔄 Knowledge .md Migration`);
315
+ console.log(` Project: ${projectRoot}`);
316
+ console.log(` Mode: ${dryRun ? 'DRY RUN' : 'LIVE'}`);
317
+ console.log(` Backup: ${noBackup ? 'disabled' : 'enabled'}`);
318
+ console.log('');
319
+
320
+ // ── 迁移 Recipes ──
321
+ const recipeFiles = collectMdFiles(recipesDir);
322
+ report.recipes.total = recipeFiles.length;
323
+ console.log(`📦 Recipes: ${recipeFiles.length} files found`);
324
+
325
+ for (const { absPath, relPath } of recipeFiles) {
326
+ try {
327
+ migrateRecipe(absPath, relPath);
328
+ } catch (err) {
329
+ report.recipes.errors.push({ file: relPath, error: err.message });
330
+ console.error(` ❌ ${relPath}: ${err.message}`);
331
+ }
332
+ }
333
+
334
+ // ── 迁移 Candidates ──
335
+ const candidateFiles = collectMdFiles(candidatesDir);
336
+ report.candidates.total = candidateFiles.length;
337
+ console.log(`📦 Candidates: ${candidateFiles.length} files found`);
338
+
339
+ for (const { absPath, relPath } of candidateFiles) {
340
+ try {
341
+ migrateCandidate(absPath, relPath);
342
+ } catch (err) {
343
+ report.candidates.errors.push({ file: relPath, error: err.message });
344
+ console.error(` ❌ ${relPath}: ${err.message}`);
345
+ }
346
+ }
347
+
348
+ // ── 报告 ──
349
+ console.log('\n═══ Migration Report ═══');
350
+ console.log(` Recipes: ${report.recipes.migrated}/${report.recipes.total} migrated, ${report.recipes.skipped} already new format, ${report.recipes.errors.length} errors`);
351
+ console.log(` Candidates: ${report.candidates.migrated}/${report.candidates.total} migrated, ${report.candidates.skipped} already new format, ${report.candidates.errors.length} errors`);
352
+
353
+ if (!noBackup && !dryRun) {
354
+ console.log(`\n Backups saved to: ${backupDir}`);
355
+ }
356
+
357
+ if (report.recipes.errors.length + report.candidates.errors.length > 0) {
358
+ console.log('\n Errors:');
359
+ for (const e of [...report.recipes.errors, ...report.candidates.errors]) {
360
+ console.log(` - ${e.file}: ${e.error}`);
361
+ }
362
+ }
363
+
364
+ console.log('');