autosnippet 2.9.0 → 2.11.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 (115) hide show
  1. package/README.md +12 -12
  2. package/bin/cli.js +53 -40
  3. package/config/constitution.yaml +9 -2
  4. package/dashboard/dist/assets/{icons-CH-H9x0E.js → icons-D4IWpDIk.js} +105 -100
  5. package/dashboard/dist/assets/index-CWBNcF9z.css +1 -0
  6. package/dashboard/dist/assets/index-DHtzhbuG.js +120 -0
  7. package/dashboard/dist/index.html +3 -3
  8. package/lib/cli/AiScanService.js +35 -36
  9. package/lib/cli/KnowledgeSyncService.js +345 -0
  10. package/lib/cli/SetupService.js +8 -26
  11. package/lib/cli/UpgradeService.js +28 -0
  12. package/lib/core/gateway/GatewayActionRegistry.js +48 -58
  13. package/lib/domain/index.js +16 -11
  14. package/lib/domain/knowledge/KnowledgeEntry.js +289 -0
  15. package/lib/domain/knowledge/KnowledgeRepository.js +123 -0
  16. package/lib/domain/knowledge/Lifecycle.js +99 -0
  17. package/lib/domain/knowledge/index.js +27 -0
  18. package/lib/domain/knowledge/values/Constraints.js +128 -0
  19. package/lib/domain/knowledge/values/Content.js +69 -0
  20. package/lib/domain/knowledge/values/Quality.js +81 -0
  21. package/lib/domain/knowledge/values/Reasoning.js +70 -0
  22. package/lib/domain/knowledge/values/Relations.js +142 -0
  23. package/lib/domain/knowledge/values/Stats.js +72 -0
  24. package/lib/domain/knowledge/values/index.js +9 -0
  25. package/lib/external/ai/AiProvider.js +85 -11
  26. package/lib/external/mcp/McpServer.js +7 -5
  27. package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +18 -2
  28. package/lib/external/mcp/handlers/bootstrap.js +116 -11
  29. package/lib/external/mcp/handlers/browse.js +76 -73
  30. package/lib/external/mcp/handlers/candidate.js +26 -275
  31. package/lib/external/mcp/handlers/guard.js +2 -0
  32. package/lib/external/mcp/handlers/knowledge.js +267 -0
  33. package/lib/external/mcp/handlers/structure.js +25 -23
  34. package/lib/external/mcp/handlers/system.js +10 -12
  35. package/lib/external/mcp/tools.js +134 -140
  36. package/lib/http/HttpServer.js +14 -8
  37. package/lib/http/routes/ai.js +4 -3
  38. package/lib/http/routes/extract.js +48 -4
  39. package/lib/http/routes/knowledge.js +246 -0
  40. package/lib/http/routes/search.js +12 -17
  41. package/lib/infrastructure/database/migrations/016_unified_knowledge_entries.js +395 -0
  42. package/lib/infrastructure/database/migrations/017_camelcase_knowledge_entries.js +107 -0
  43. package/lib/infrastructure/external/XcodeAutomation.js +187 -103
  44. package/lib/injection/ServiceContainer.js +69 -60
  45. package/lib/repository/knowledge/KnowledgeRepository.impl.js +338 -0
  46. package/lib/service/automation/DirectiveDetector.js +2 -3
  47. package/lib/service/automation/FileWatcher.js +59 -28
  48. package/lib/service/automation/XcodeIntegration.js +931 -156
  49. package/lib/service/automation/handlers/AlinkHandler.js +5 -4
  50. package/lib/service/automation/handlers/CreateHandler.js +53 -19
  51. package/lib/service/automation/handlers/DraftHandler.js +1 -1
  52. package/lib/service/automation/handlers/GuardHandler.js +183 -20
  53. package/lib/service/automation/handlers/SearchHandler.js +25 -22
  54. package/lib/service/candidate/SimilarityService.js +2 -2
  55. package/lib/service/chat/AnalystAgent.js +9 -0
  56. package/lib/service/chat/CandidateGuardrail.js +22 -11
  57. package/lib/service/chat/ChatAgent.js +132 -54
  58. package/lib/service/chat/ContextWindow.js +5 -5
  59. package/lib/service/chat/HandoffProtocol.js +1 -0
  60. package/lib/service/chat/ProducerAgent.js +40 -13
  61. package/lib/service/chat/ReasoningLayer.js +854 -0
  62. package/lib/service/chat/ReasoningTrace.js +329 -0
  63. package/lib/service/chat/tools.js +308 -205
  64. package/lib/service/cursor/CursorDeliveryPipeline.js +279 -0
  65. package/lib/service/cursor/KnowledgeCompressor.js +87 -0
  66. package/lib/service/cursor/RulesGenerator.js +168 -0
  67. package/lib/service/cursor/SkillsSyncer.js +268 -0
  68. package/lib/service/cursor/TokenBudget.js +58 -0
  69. package/lib/service/cursor/TopicClassifier.js +141 -0
  70. package/lib/service/guard/GuardCheckEngine.js +99 -10
  71. package/lib/service/guard/GuardService.js +57 -46
  72. package/lib/service/knowledge/ConfidenceRouter.js +159 -0
  73. package/lib/service/knowledge/KnowledgeFileWriter.js +595 -0
  74. package/lib/service/knowledge/KnowledgeService.js +802 -0
  75. package/lib/service/recipe/RecipeParser.js +3 -12
  76. package/lib/service/search/SearchEngine.js +67 -22
  77. package/lib/service/skills/SignalCollector.js +14 -9
  78. package/lib/service/skills/SkillAdvisor.js +13 -11
  79. package/lib/service/snippet/SnippetFactory.js +5 -5
  80. package/lib/service/spm/SpmService.js +15 -48
  81. package/lib/shared/RecipeReadinessChecker.js +6 -11
  82. package/package.json +1 -1
  83. package/scripts/install-cursor-skill.js +0 -6
  84. package/scripts/migrate-md-to-knowledge.mjs +364 -0
  85. package/skills/autosnippet-analysis/SKILL.md +15 -7
  86. package/skills/autosnippet-candidates/SKILL.md +8 -8
  87. package/skills/autosnippet-coldstart/SKILL.md +8 -4
  88. package/skills/autosnippet-concepts/SKILL.md +7 -6
  89. package/skills/autosnippet-create/SKILL.md +13 -13
  90. package/skills/autosnippet-intent/SKILL.md +3 -2
  91. package/skills/autosnippet-lifecycle/SKILL.md +5 -5
  92. package/skills/autosnippet-recipes/SKILL.md +18 -6
  93. package/templates/constitution.yaml +1 -1
  94. package/templates/copilot-instructions.md +6 -6
  95. package/templates/recipes-setup/README.md +3 -3
  96. package/dashboard/dist/assets/index-CqJRvYRL.js +0 -197
  97. package/dashboard/dist/assets/index-DICm9PNa.css +0 -1
  98. package/lib/cli/CandidateSyncService.js +0 -261
  99. package/lib/cli/SyncService.js +0 -356
  100. package/lib/domain/candidate/Candidate.js +0 -196
  101. package/lib/domain/candidate/CandidateRepository.js +0 -107
  102. package/lib/domain/candidate/Reasoning.js +0 -52
  103. package/lib/domain/recipe/Recipe.js +0 -421
  104. package/lib/domain/recipe/RecipeRepository.js +0 -54
  105. package/lib/domain/types/CandidateStatus.js +0 -52
  106. package/lib/http/routes/candidates.js +0 -559
  107. package/lib/http/routes/recipes.js +0 -397
  108. package/lib/repository/candidate/CandidateRepository.impl.js +0 -230
  109. package/lib/repository/recipe/RecipeRepository.impl.js +0 -498
  110. package/lib/service/candidate/CandidateAggregator.js +0 -52
  111. package/lib/service/candidate/CandidateFileWriter.js +0 -383
  112. package/lib/service/candidate/CandidateService.js +0 -1001
  113. package/lib/service/recipe/RecipeFileWriter.js +0 -514
  114. package/lib/service/recipe/RecipeService.js +0 -786
  115. package/lib/service/recipe/RecipeStatsTracker.js +0 -148
@@ -69,16 +69,13 @@ export class RecipeParser {
69
69
  return {
70
70
  title,
71
71
  summary: frontmatter.summary || frontmatter.description || '',
72
- summary_cn: frontmatter.summary_cn || '',
73
- summary_en: frontmatter.summary_en || '',
72
+ description: frontmatter.description || frontmatter.summary || '',
74
73
  trigger: frontmatter.trigger || this.#generateTrigger(title),
75
74
  category: frontmatter.category || 'general',
76
75
  language: frontmatter.language || (codeBlocks[0]?.language !== 'text' ? codeBlocks[0]?.language : 'swift'),
77
76
  code: codeBlocks.map(b => b.code).join('\n\n'),
78
77
  codeBlocks,
79
78
  usageGuide,
80
- usageGuide_cn: frontmatter.usageGuide_cn || '',
81
- usageGuide_en: frontmatter.usageGuide_en || '',
82
79
  headers,
83
80
  includeHeaders: headers.length > 0,
84
81
  frontmatter,
@@ -169,16 +166,13 @@ export class RecipeParser {
169
166
  items: [{
170
167
  title,
171
168
  summary: '',
172
- summary_cn: '',
173
- summary_en: '',
169
+ description: '',
174
170
  trigger: this.#generateTrigger(title),
175
171
  category: 'Utility',
176
172
  language,
177
173
  code: content,
178
174
  codeBlocks: [{ language, code: content }],
179
175
  usageGuide: '',
180
- usageGuide_cn: '',
181
- usageGuide_en: '',
182
176
  headers: this.#extractHeaders(content),
183
177
  includeHeaders: false,
184
178
  frontmatter: {},
@@ -252,16 +246,13 @@ export class RecipeParser {
252
246
  return {
253
247
  title,
254
248
  summary: '',
255
- summary_cn: '',
256
- summary_en: '',
249
+ description: '',
257
250
  trigger: this.#generateTrigger(title),
258
251
  category: 'Utility',
259
252
  language,
260
253
  code,
261
254
  codeBlocks: codeBlocks.length > 0 ? codeBlocks : [{ language, code }],
262
255
  usageGuide: '',
263
- usageGuide_cn: '',
264
- usageGuide_en: '',
265
256
  headers: this.#extractHeaders(code),
266
257
  includeHeaders: false,
267
258
  frontmatter: {},
@@ -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,21 @@ 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
+ try {
162
+ entries = this.db.prepare(
163
+ `SELECT id, title, description, language, category, knowledgeType, kind,
164
+ content AS content_json, lifecycle, tags AS tags_json, trigger
165
+ FROM knowledge_entries WHERE lifecycle != 'deprecated'`
166
+ ).all();
167
+ entries = entries.map(e => ({
168
+ ...e,
169
+ status: e.lifecycle,
170
+ }));
171
+ } catch { /* table may not exist */ }
134
172
 
135
- for (const r of recipes) {
173
+ for (const r of entries) {
136
174
  let contentText = '';
137
175
  try {
138
176
  const content = JSON.parse(r.content_json || '{}');
@@ -141,14 +179,14 @@ export class SearchEngine {
141
179
  // 包含 tags + trigger 提升召回率
142
180
  let tagText = '';
143
181
  try { tagText = JSON.parse(r.tags_json || '[]').join(' '); } catch { /* ignore */ }
144
- const text = [r.title, r.description, r.trigger, r.language, r.category, r.knowledge_type, tagText, contentText]
182
+ const text = [r.title, r.description, r.trigger, r.language, r.category, r.knowledgeType, tagText, contentText]
145
183
  .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 || '' });
184
+ this.scorer.addDocument(r.id, text, { type: 'knowledge', title: r.title, trigger: r.trigger || '', status: r.status, knowledgeType: r.knowledgeType, kind: r.kind || 'pattern', language: r.language || '', category: r.category || '' });
147
185
  }
148
186
 
149
187
  this._indexed = true;
150
188
  this.logger.info('Search index built', {
151
- recipes: recipes.length,
189
+ entries: entries.length,
152
190
  total: this.scorer.totalDocs,
153
191
  });
154
192
  } catch (err) {
@@ -229,14 +267,17 @@ export class SearchEngine {
229
267
  const escaped = query.replace(/[%_\\]/g, ch => `\\${ch}`);
230
268
  const pattern = `%${escaped}%`;
231
269
 
232
- if (type === 'all' || type === 'recipe' || type === 'rule' || type === 'solution') {
270
+ if (type === 'all' || type === 'recipe' || type === 'knowledge' || type === 'rule' || type === 'solution') {
233
271
  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);
272
+ let rows = [];
273
+ try {
274
+ rows = this.db.prepare(
275
+ `SELECT id, title, description, language, category, knowledgeType, kind, lifecycle as status, content AS content_json, trigger, headers, moduleName, 'knowledge' as type
276
+ FROM knowledge_entries
277
+ WHERE lifecycle != 'deprecated' AND (title LIKE ? ESCAPE '\\' OR description LIKE ? ESCAPE '\\' OR trigger LIKE ? ESCAPE '\\' OR content LIKE ? ESCAPE '\\')
278
+ LIMIT ?`
279
+ ).all(pattern, pattern, pattern, pattern, limit);
280
+ } catch { /* table may not exist */ }
240
281
  // 基础相关性排序:trigger 精确 > 标题匹配 > 描述匹配 > 内容匹配
241
282
  const lowerQ = query.toLowerCase();
242
283
  results.push(...rows.map(r => {
@@ -341,7 +382,7 @@ export class SearchEngine {
341
382
  }
342
383
 
343
384
  /**
344
- * 补充详细字段(content_json / dimensions_json / description / trigger)— 批量 IN 查询
385
+ * 补充详细字段(content / description / trigger)— 批量 IN 查询
345
386
  * 用于向量搜索结果与 BM25 结果的一致性
346
387
  */
347
388
  _supplementDetails(items) {
@@ -349,17 +390,21 @@ export class SearchEngine {
349
390
  try {
350
391
  const ids = items.map(it => it.id);
351
392
  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);
393
+ let rows = [];
394
+ try {
395
+ rows = this.db.prepare(
396
+ `SELECT id, content AS content_json, description, trigger, headers, moduleName FROM knowledge_entries WHERE id IN (${placeholders})`
397
+ ).all(...ids);
398
+ } catch { /* table may not exist */ }
355
399
  const rowMap = new Map(rows.map(r => [r.id, r]));
356
400
  for (const item of items) {
357
401
  const row = rowMap.get(item.id);
358
402
  if (row) {
359
403
  item.content_json = row.content_json || null;
360
- item.dimensions_json = row.dimensions_json || null;
361
404
  item.description = item.description || row.description || '';
362
405
  item.trigger = item.trigger || row.trigger || '';
406
+ if (row.headers) item.headers = row.headers;
407
+ if (row.moduleName) item.moduleName = row.moduleName;
363
408
  }
364
409
  }
365
410
  } 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
- `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`
341
+ `SELECT id, title, knowledgeType, category, language,
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
+ updatedAt
346
+ FROM knowledge_entries
347
+ WHERE lifecycle = 'active'
348
+ ORDER BY updatedAt DESC LIMIT 30`
344
349
  ).all();
345
350
  return rows;
346
351
  } catch { return []; }
@@ -349,12 +354,12 @@ 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'
357
- ORDER BY created_at DESC LIMIT 30`
359
+ `SELECT id, source, lifecycle as status, language, category,
360
+ title, createdAt
361
+ FROM knowledge_entries WHERE lifecycle = 'pending'
362
+ ORDER BY createdAt DESC LIMIT 30`
358
363
  ).all();
359
364
  return rows;
360
365
  } catch { return []; }
@@ -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
 
@@ -777,33 +777,25 @@ export class SpmService {
777
777
  }
778
778
 
779
779
  /**
780
- * 共用增强管线:语义字段标准化 + RecipeExtractor 标签 + QualityScorer 评分
780
+ * 共用增强管线:补充 extract_recipes handler 未覆盖的增量增强。
781
+ * extract_recipes handler 已负责:normalizeSemanticFields + RecipeExtractor + QualityScorer + V3 结构化。
782
+ * 此方法仅做以下增量处理:
783
+ * - 确保 normalizeSemanticFields 已执行(幂等,防止非 ChatAgent 路径跳过)
784
+ * - 若 handler 未注入质量评分,由 SpmService 补充
781
785
  */
782
786
  _enrichRecipes(recipes) {
783
787
  for (const recipe of recipes) {
784
- SpmService.normalizeSemanticFields(recipe);
785
-
786
- if (this.#recipeExtractor && recipe.code) {
787
- try {
788
- const extracted = this.#recipeExtractor.extractFromContent(
789
- recipe.code, `${recipe.title || 'unknown'}.${recipe.language || 'swift'}`, ''
790
- );
791
- if (extracted.semanticTags?.length > 0) {
792
- recipe.tags = [...new Set([...(recipe.tags || []), ...extracted.semanticTags])];
793
- }
794
- if ((!recipe.category || recipe.category === 'Utility') && extracted.category && extracted.category !== 'general') {
795
- recipe.category = extracted.category;
796
- }
797
- } catch (e) {
798
- this.#logger.debug(`[SpmService] RecipeExtractor enrichment failed: ${e.message}`);
799
- }
800
- }
801
-
802
- if (this.#qualityScorer) {
788
+ // QualityScorer 评分(程序化,仅在未评分时补充)
789
+ if (!recipe.quality && this.#qualityScorer) {
803
790
  try {
804
791
  const scoreResult = this.#qualityScorer.score(recipe);
805
- recipe.qualityScore = scoreResult.score;
806
- recipe.qualityGrade = scoreResult.grade;
792
+ recipe.quality = {
793
+ completeness: 0,
794
+ adaptation: 0,
795
+ documentation: 0,
796
+ overall: scoreResult.score ?? 0,
797
+ grade: scoreResult.grade || '',
798
+ };
807
799
  } catch (e) {
808
800
  this.#logger.debug(`[SpmService] QualityScorer failed: ${e.message}`);
809
801
  }
@@ -1157,32 +1149,7 @@ export class SpmService {
1157
1149
  * - 确保 rationale / knowledgeType / complexity / scope 就位
1158
1150
  */
1159
1151
  static normalizeSemanticFields(recipe) {
1160
- // preconditions constraints.preconditions
1161
- if (Array.isArray(recipe.preconditions) && recipe.preconditions.length > 0) {
1162
- if (!recipe.constraints) recipe.constraints = {};
1163
- if (!recipe.constraints.preconditions) {
1164
- recipe.constraints.preconditions = recipe.preconditions;
1165
- }
1166
- delete recipe.preconditions;
1167
- }
1168
-
1169
- // steps: string[] → [{title, description}]
1170
- if (Array.isArray(recipe.steps)) {
1171
- recipe.steps = recipe.steps.map((s, i) => {
1172
- if (typeof s === 'string') {
1173
- return { title: `Step ${i + 1}`, description: s, code: '' };
1174
- }
1175
- return s; // already structured
1176
- });
1177
- }
1178
-
1179
- // knowledgeType 默认值
1180
- if (!recipe.knowledgeType) recipe.knowledgeType = 'code-pattern';
1181
- // complexity 默认值
1182
- if (!recipe.complexity) recipe.complexity = 'intermediate';
1183
- // scope 默认值
1184
- if (!recipe.scope) recipe.scope = 'project-specific';
1185
-
1152
+ // V3: AI 已输出完整结构,不再填充默认值
1186
1153
  return recipe;
1187
1154
  }
1188
1155
 
@@ -19,7 +19,7 @@ const WHITELISTED_CATEGORIES = ['bootstrap', 'knowledge', 'general'];
19
19
  /**
20
20
  * 检查候选是否具备直接提升为 Recipe 的所有必要字段。
21
21
  *
22
- * @param {object} item 扁平字段对象(title, trigger, summary_cn …)
22
+ * @param {object} item 扁平字段对象(title, trigger, description …)
23
23
  * —— MCP handler 传入 tool params;
24
24
  * ChatAgent / bootstrap 需先从 metadata 展开。
25
25
  * @returns {{ ready: boolean, missing: string[], suggestions: string[] }}
@@ -43,15 +43,10 @@ export function checkRecipeReadiness(item) {
43
43
  suggestions.push(`trigger "${item.trigger}" 应以 @ 开头`);
44
44
  }
45
45
 
46
- const summaryCn = item.summary_cn || item.summary;
47
- if (!summaryCn) {
48
- missing.push('summary_cn');
49
- suggestions.push('请提供中文摘要(≤100字)');
50
- }
51
-
52
- if (!item.summary_en) {
53
- missing.push('summary_en');
54
- suggestions.push('请提供英文摘要(≤100 words),提升检索与 AI 理解');
46
+ const description = item.description || item.summary;
47
+ if (!description) {
48
+ missing.push('description');
49
+ suggestions.push('请提供描述(≤100字)');
55
50
  }
56
51
 
57
52
  if (!Array.isArray(item.headers) || item.headers.length === 0) {
@@ -60,7 +55,7 @@ export function checkRecipeReadiness(item) {
60
55
  }
61
56
 
62
57
  // ── 建议 ──
63
- if (!item.usageGuide && !item.usageGuide_cn) {
58
+ if (!item.usageGuide) {
64
59
  missing.push('usageGuide');
65
60
  suggestions.push('请提供使用指南(Markdown ### 章节格式)');
66
61
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autosnippet",
3
- "version": "2.9.0",
3
+ "version": "2.11.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',