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.
- package/README.md +4 -4
- package/bin/cli.js +5 -33
- package/config/constitution.yaml +9 -2
- package/dashboard/dist/assets/{icons-CH-H9x0E.js → icons-BkT3XrKf.js} +105 -100
- package/dashboard/dist/assets/index-BsB7DzW4.css +1 -0
- package/dashboard/dist/assets/index-DdmQMrJJ.js +155 -0
- package/dashboard/dist/index.html +3 -3
- package/lib/cli/AiScanService.js +13 -11
- package/lib/cli/KnowledgeSyncService.js +343 -0
- package/lib/cli/SetupService.js +8 -26
- package/lib/core/gateway/GatewayActionRegistry.js +48 -58
- package/lib/domain/index.js +16 -11
- package/lib/domain/knowledge/KnowledgeEntry.js +351 -0
- package/lib/domain/knowledge/KnowledgeRepository.js +123 -0
- package/lib/domain/knowledge/Lifecycle.js +109 -0
- package/lib/domain/knowledge/index.js +27 -0
- package/lib/domain/knowledge/values/Constraints.js +125 -0
- package/lib/domain/knowledge/values/Content.js +86 -0
- package/lib/domain/knowledge/values/Quality.js +93 -0
- package/lib/domain/knowledge/values/Reasoning.js +69 -0
- package/lib/domain/knowledge/values/Relations.js +168 -0
- package/lib/domain/knowledge/values/Stats.js +87 -0
- package/lib/domain/knowledge/values/index.js +9 -0
- package/lib/external/ai/AiProvider.js +48 -0
- package/lib/external/mcp/McpServer.js +7 -5
- package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +2 -2
- package/lib/external/mcp/handlers/bootstrap.js +116 -11
- package/lib/external/mcp/handlers/browse.js +77 -73
- package/lib/external/mcp/handlers/candidate.js +29 -276
- package/lib/external/mcp/handlers/guard.js +2 -0
- package/lib/external/mcp/handlers/knowledge.js +205 -0
- package/lib/external/mcp/handlers/structure.js +25 -23
- package/lib/external/mcp/handlers/system.js +10 -12
- package/lib/external/mcp/tools.js +125 -138
- package/lib/http/HttpServer.js +4 -8
- package/lib/http/routes/extract.js +48 -4
- package/lib/http/routes/knowledge.js +246 -0
- package/lib/http/routes/search.js +12 -17
- package/lib/infrastructure/database/migrations/016_unified_knowledge_entries.js +395 -0
- package/lib/infrastructure/external/XcodeAutomation.js +187 -103
- package/lib/injection/ServiceContainer.js +49 -60
- package/lib/repository/knowledge/KnowledgeRepository.impl.js +373 -0
- package/lib/service/automation/DirectiveDetector.js +2 -3
- package/lib/service/automation/FileWatcher.js +67 -28
- package/lib/service/automation/XcodeIntegration.js +931 -156
- package/lib/service/automation/handlers/AlinkHandler.js +6 -4
- package/lib/service/automation/handlers/CreateHandler.js +53 -18
- package/lib/service/automation/handlers/GuardHandler.js +183 -20
- package/lib/service/automation/handlers/SearchHandler.js +35 -17
- package/lib/service/chat/CandidateGuardrail.js +1 -1
- package/lib/service/chat/ChatAgent.js +46 -45
- package/lib/service/chat/ContextWindow.js +5 -5
- package/lib/service/chat/ProducerAgent.js +7 -7
- package/lib/service/chat/tools.js +130 -123
- package/lib/service/guard/GuardCheckEngine.js +114 -10
- package/lib/service/guard/GuardService.js +59 -48
- package/lib/service/knowledge/ConfidenceRouter.js +159 -0
- package/lib/service/knowledge/KnowledgeFileWriter.js +602 -0
- package/lib/service/knowledge/KnowledgeService.js +725 -0
- package/lib/service/search/SearchEngine.js +92 -19
- package/lib/service/skills/SignalCollector.js +12 -7
- package/lib/service/skills/SkillAdvisor.js +13 -11
- package/lib/service/snippet/SnippetFactory.js +5 -5
- package/package.json +1 -1
- package/scripts/install-cursor-skill.js +0 -6
- package/scripts/migrate-md-to-knowledge.mjs +364 -0
- package/skills/autosnippet-analysis/SKILL.md +15 -7
- package/skills/autosnippet-candidates/SKILL.md +6 -6
- package/skills/autosnippet-coldstart/SKILL.md +7 -3
- package/skills/autosnippet-concepts/SKILL.md +7 -6
- package/skills/autosnippet-create/SKILL.md +13 -13
- package/skills/autosnippet-intent/SKILL.md +3 -2
- package/skills/autosnippet-lifecycle/SKILL.md +5 -5
- package/skills/autosnippet-recipes/SKILL.md +16 -4
- package/templates/constitution.yaml +1 -1
- package/templates/copilot-instructions.md +6 -6
- package/templates/recipes-setup/README.md +3 -3
- package/dashboard/dist/assets/index-CqJRvYRL.js +0 -197
- package/dashboard/dist/assets/index-DICm9PNa.css +0 -1
- package/lib/cli/CandidateSyncService.js +0 -261
- package/lib/cli/SyncService.js +0 -356
- package/lib/domain/candidate/Candidate.js +0 -196
- package/lib/domain/candidate/CandidateRepository.js +0 -107
- package/lib/domain/candidate/Reasoning.js +0 -52
- package/lib/domain/recipe/Recipe.js +0 -421
- package/lib/domain/recipe/RecipeRepository.js +0 -54
- package/lib/domain/types/CandidateStatus.js +0 -52
- package/lib/http/routes/candidates.js +0 -559
- package/lib/http/routes/recipes.js +0 -397
- package/lib/repository/candidate/CandidateRepository.impl.js +0 -230
- package/lib/repository/recipe/RecipeRepository.impl.js +0 -498
- package/lib/service/candidate/CandidateAggregator.js +0 -52
- package/lib/service/candidate/CandidateFileWriter.js +0 -383
- package/lib/service/candidate/CandidateService.js +0 -1001
- package/lib/service/recipe/RecipeFileWriter.js +0 -514
- package/lib/service/recipe/RecipeService.js +0 -786
- 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
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
|
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: '
|
|
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
|
-
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
-
|
|
355
|
-
|
|
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
|
|
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
|
|
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 统计(
|
|
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
|
-
(
|
|
240
|
-
FROM
|
|
241
|
-
WHERE
|
|
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
|
|
269
|
-
SUM(CASE WHEN
|
|
270
|
-
FROM
|
|
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} [
|
|
48
|
+
* @param {object} [knowledgeRepository] — KnowledgeRepositoryImpl(可选,用于列表查询)
|
|
49
49
|
*/
|
|
50
|
-
constructor(
|
|
51
|
-
this._recipeRepo =
|
|
50
|
+
constructor(knowledgeRepository) {
|
|
51
|
+
this._recipeRepo = knowledgeRepository || null;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
/**
|
|
55
|
-
* 运行时注入
|
|
55
|
+
* 运行时注入 knowledgeRepository(用于延迟绑定场景)
|
|
56
56
|
*/
|
|
57
|
-
|
|
57
|
+
setKnowledgeRepository(repo) {
|
|
58
58
|
this._recipeRepo = repo;
|
|
59
59
|
}
|
|
60
60
|
|
package/package.json
CHANGED
|
@@ -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('');
|