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
@@ -1,514 +0,0 @@
1
- /**
2
- * RecipeFileWriter — 将 Recipe 领域对象序列化为标准 .md 文件
3
- *
4
- * 职责:
5
- * - Recipe domain object → YAML frontmatter + Markdown body
6
- * - 落盘到 AutoSnippet/recipes/{category}/ 目录
7
- * - .md 文件 = 完整唯一数据源(Source of Truth),DB = 索引缓存
8
- *
9
- * Frontmatter 分层:
10
- * - 基础字段(人类可读/可编辑):id, title, trigger, category, language, summary_cn, ...
11
- * - 机器管理字段(_ 前缀):_quality, _statistics, _relations, _constraints, _contentHash
12
- * — 由系统自动维护,手动修改会被 `asd sync` 检测并记录违规
13
- *
14
- * 文件名策略:trigger > title slug
15
- */
16
-
17
- import fs from 'node:fs';
18
- import path from 'node:path';
19
- import { createHash } from 'node:crypto';
20
- import { RECIPES_DIR } from '../../infrastructure/config/Defaults.js';
21
- import Logger from '../../infrastructure/logging/Logger.js';
22
- import pathGuard from '../../shared/PathGuard.js';
23
-
24
- export class RecipeFileWriter {
25
- /**
26
- * @param {string} projectRoot 项目根目录
27
- */
28
- constructor(projectRoot) {
29
- this.projectRoot = projectRoot;
30
- this.recipesDir = path.join(projectRoot, RECIPES_DIR);
31
- this.logger = Logger.getInstance();
32
- }
33
-
34
- /* ═══ 序列化 ═══════════════════════════════════════════ */
35
-
36
- /**
37
- * 将 Recipe 领域对象序列化为完整 .md(YAML frontmatter + body)
38
- * 包含所有运行时数据,确保 .md 是完整 Source of Truth
39
- * @param {import('../../domain/recipe/Recipe.js').Recipe} recipe
40
- * @returns {string}
41
- */
42
- serializeToMarkdown(recipe) {
43
- const lines = ['---'];
44
-
45
- // ── 基础字段(人类可读)──
46
- lines.push(`id: ${recipe.id}`);
47
- lines.push(`title: ${this.#yamlStr(recipe.title)}`);
48
- if (recipe.trigger) lines.push(`trigger: ${recipe.trigger}`);
49
- lines.push(`category: ${recipe.category || 'general'}`);
50
- lines.push(`language: ${recipe.language || 'swift'}`);
51
- if (recipe.summaryCn) lines.push(`summary_cn: ${this.#yamlStr(recipe.summaryCn)}`);
52
- if (recipe.summaryEn) lines.push(`summary_en: ${this.#yamlStr(recipe.summaryEn)}`);
53
-
54
- // ── headers ──
55
- const headers = recipe.dimensions?.headers || recipe.content?.headers || [];
56
- if (Array.isArray(headers) && headers.length > 0) {
57
- lines.push(`headers: [${headers.map(h => `"${this.#esc(h)}"`).join(', ')}]`);
58
- }
59
-
60
- // ── 分类 & 元数据 ──
61
- if (recipe.knowledgeType) lines.push(`knowledgeType: ${recipe.knowledgeType}`);
62
- if (recipe.kind) lines.push(`kind: ${recipe.kind}`);
63
- if (recipe.complexity) lines.push(`complexity: ${recipe.complexity}`);
64
- if (recipe.scope) lines.push(`scope: ${recipe.scope}`);
65
-
66
- // ── 标签 ──
67
- if (recipe.tags?.length > 0) {
68
- lines.push(`tags: [${recipe.tags.map(t => `"${this.#esc(t)}"`).join(', ')}]`);
69
- }
70
-
71
- // ── 扩展维度 ──
72
- const dims = recipe.dimensions || {};
73
- if (dims.authority != null) lines.push(`authority: ${dims.authority}`);
74
- if (dims.difficulty) lines.push(`difficulty: ${dims.difficulty}`);
75
- if (dims.version) lines.push(`version: "${dims.version}"`);
76
-
77
- // ── 状态 & 时间 ──
78
- lines.push(`status: ${recipe.status || 'active'}`);
79
- lines.push(`createdBy: ${recipe.createdBy || 'system'}`);
80
- lines.push(`createdAt: ${recipe.createdAt || Math.floor(Date.now() / 1000)}`);
81
- lines.push(`updatedAt: ${recipe.updatedAt || Math.floor(Date.now() / 1000)}`);
82
- if (recipe.publishedBy) lines.push(`publishedBy: ${recipe.publishedBy}`);
83
- if (recipe.publishedAt) lines.push(`publishedAt: ${recipe.publishedAt}`);
84
- if (recipe.sourceCandidate) lines.push(`sourceCandidate: ${recipe.sourceCandidate}`);
85
-
86
- // ── 废弃信息 ──
87
- if (recipe.deprecation) {
88
- lines.push(`deprecated: true`);
89
- if (recipe.deprecation.reason) lines.push(`deprecationReason: ${this.#yamlStr(recipe.deprecation.reason)}`);
90
- if (recipe.deprecation.deprecatedAt) lines.push(`deprecatedAt: ${recipe.deprecation.deprecatedAt}`);
91
- }
92
-
93
- // ── 人类可读关系(简化版,触发器列表)──
94
- const relatedTriggers = this.#extractRelatedTriggers(recipe.relations);
95
- if (relatedTriggers.length > 0) {
96
- lines.push(`relatedRecipes: [${relatedTriggers.map(t => `"${t}"`).join(', ')}]`);
97
- }
98
-
99
- // ── 机器管理字段(_ 前缀,单行 JSON)──
100
- const quality = recipe.quality || {};
101
- if (this.#hasValues(quality)) {
102
- lines.push(`_quality: ${JSON.stringify(quality)}`);
103
- }
104
-
105
- const stats = recipe.statistics || {};
106
- if (this.#hasValues(stats)) {
107
- lines.push(`_statistics: ${JSON.stringify(stats)}`);
108
- }
109
-
110
- const relations = recipe.relations || {};
111
- if (this.#hasNonEmptyArrays(relations)) {
112
- lines.push(`_relations: ${JSON.stringify(relations)}`);
113
- }
114
-
115
- const constraints = recipe.constraints || {};
116
- if (this.#hasNonEmptyArrays(constraints)) {
117
- lines.push(`_constraints: ${JSON.stringify(constraints)}`);
118
- }
119
-
120
- // _contentHash 占位索引(后续替换为真实 hash)
121
- const hashIdx = lines.length;
122
- lines.push(''); // 占位行
123
-
124
- lines.push('---');
125
- lines.push('');
126
-
127
- // ── Body ──
128
- if (recipe.content?.markdown) {
129
- const body = recipe.content.markdown.replace(/^---[\s\S]*?---\s*/, '').trim();
130
- lines.push(body);
131
- } else {
132
- lines.push(this.#buildBodyFromStructured(recipe));
133
- }
134
-
135
- lines.push('');
136
-
137
- // ── 计算 content hash ──
138
- // 去掉占位行后计算,确保与 SyncService 读取时 computeContentHash 的输入一致
139
- const linesForHash = [...lines];
140
- linesForHash.splice(hashIdx, 1);
141
- const hash = computeContentHash(linesForHash.join('\n'));
142
- lines[hashIdx] = `_contentHash: ${hash}`;
143
- return lines.join('\n');
144
- }
145
-
146
- /* ═══ 文件操作 ═══════════════════════════════════════════ */
147
-
148
- /**
149
- * 将 Recipe 落盘到 AutoSnippet/recipes/{category}/ 目录
150
- * @param {import('../../domain/recipe/Recipe.js').Recipe} recipe
151
- * @returns {string|null} 写入的文件路径,失败返回 null
152
- */
153
- persistRecipe(recipe) {
154
- try {
155
- if (!recipe?.id || !recipe?.title) {
156
- this.logger.warn('Cannot persist recipe: missing id or title');
157
- return null;
158
- }
159
-
160
- const filename = this.#getFilename(recipe);
161
- const category = (recipe.category || 'general').toLowerCase();
162
- const categoryDir = path.join(this.recipesDir, category);
163
-
164
- // 路径安全检查 — 阻止 category 含 ../ 导致路径逃逸
165
- pathGuard.assertProjectWriteSafe(categoryDir);
166
-
167
- if (!fs.existsSync(categoryDir)) {
168
- fs.mkdirSync(categoryDir, { recursive: true });
169
- }
170
-
171
- // 检查是否有旧文件需要清理(trigger 或 category 可能变了)
172
- this.#cleanupOldFile(recipe, path.join(categoryDir, filename));
173
-
174
- const filePath = path.join(categoryDir, filename);
175
- const markdown = this.serializeToMarkdown(recipe);
176
- fs.writeFileSync(filePath, markdown, 'utf8');
177
-
178
- // 更新 recipe 的 sourceFile 溯源
179
- recipe.sourceFile = path.relative(this.projectRoot, filePath);
180
-
181
- this.logger.info('Recipe persisted to file', {
182
- recipeId: recipe.id,
183
- path: recipe.sourceFile,
184
- });
185
-
186
- return filePath;
187
- } catch (error) {
188
- this.logger.error('Failed to persist recipe to file', {
189
- recipeId: recipe.id,
190
- error: error.message,
191
- });
192
- return null;
193
- }
194
- }
195
-
196
- /**
197
- * 删除 Recipe 对应的 .md 文件
198
- * @param {import('../../domain/recipe/Recipe.js').Recipe} recipe
199
- * @returns {boolean}
200
- */
201
- removeRecipe(recipe) {
202
- const filename = this.#getFilename(recipe);
203
- const searchPaths = this.#buildSearchPaths(recipe, filename);
204
-
205
- for (const fp of searchPaths) {
206
- if (fs.existsSync(fp)) {
207
- fs.unlinkSync(fp);
208
- this.logger.info('Recipe file removed', {
209
- recipeId: recipe.id,
210
- path: path.relative(this.projectRoot, fp),
211
- });
212
- return true;
213
- }
214
- }
215
-
216
- return this.#removeByIdScan(recipe.id);
217
- }
218
-
219
- /* ═══ 内部工具 ═══════════════════════════════════════════ */
220
-
221
- #getFilename(recipe) {
222
- if (recipe.trigger) {
223
- const clean = recipe.trigger
224
- .replace(/^@/, '')
225
- .replace(/[^a-zA-Z0-9_-]/g, '_')
226
- .slice(0, 60);
227
- return `${clean}.md`;
228
- }
229
- const slug = (recipe.title || 'untitled')
230
- .toLowerCase()
231
- .replace(/[^a-z0-9\u4e00-\u9fff]+/g, '-')
232
- .replace(/(^-|-$)/g, '')
233
- .slice(0, 60);
234
- return `${slug}.md`;
235
- }
236
-
237
- #buildSearchPaths(recipe, filename) {
238
- const category = (recipe.category || 'general').toLowerCase();
239
- return [
240
- path.join(this.recipesDir, category, filename),
241
- path.join(this.recipesDir, filename),
242
- recipe.sourceFile ? path.join(this.projectRoot, recipe.sourceFile) : null,
243
- ].filter(Boolean);
244
- }
245
-
246
- #cleanupOldFile(recipe, newPath) {
247
- if (!recipe.sourceFile) return;
248
- const oldPath = path.join(this.projectRoot, recipe.sourceFile);
249
- if (oldPath !== newPath && fs.existsSync(oldPath)) {
250
- fs.unlinkSync(oldPath);
251
- this.logger.info('Cleaned up old recipe file', {
252
- recipeId: recipe.id,
253
- oldPath: recipe.sourceFile,
254
- });
255
- }
256
- }
257
-
258
- #removeByIdScan(recipeId) {
259
- if (!fs.existsSync(this.recipesDir)) return false;
260
- try {
261
- const walk = (dir) => {
262
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
263
- const full = path.join(dir, entry.name);
264
- if (entry.isDirectory()) {
265
- if (walk(full)) return true;
266
- } else if (entry.name.endsWith('.md') && !entry.name.startsWith('_')) {
267
- const content = fs.readFileSync(full, 'utf8');
268
- if (content.includes(`id: ${recipeId}`)) {
269
- fs.unlinkSync(full);
270
- this.logger.info('Recipe file removed by id scan', { recipeId, path: full });
271
- return true;
272
- }
273
- }
274
- }
275
- return false;
276
- };
277
- return walk(this.recipesDir);
278
- } catch {
279
- return false;
280
- }
281
- }
282
-
283
- #buildBodyFromStructured(recipe) {
284
- const parts = [];
285
-
286
- parts.push('## Snippet / Code Reference\n');
287
- if (recipe.content?.pattern) {
288
- const lang = recipe.language || 'swift';
289
- parts.push(`\`\`\`${lang}`);
290
- parts.push(recipe.content.pattern);
291
- parts.push('```\n');
292
- } else {
293
- parts.push('```swift\n// TODO: 添加代码\n```\n');
294
- }
295
-
296
- parts.push('## AI Context / Usage Guide\n');
297
-
298
- if (recipe.usageGuideCn) { parts.push(recipe.usageGuideCn); parts.push(''); }
299
- if (recipe.usageGuideEn) { parts.push(recipe.usageGuideEn); parts.push(''); }
300
- if (recipe.content?.rationale) {
301
- parts.push('### 设计原理\n');
302
- parts.push(recipe.content.rationale);
303
- parts.push('');
304
- }
305
- if (recipe.content?.steps?.length > 0) {
306
- parts.push('### 使用步骤\n');
307
- recipe.content.steps.forEach((step, i) => {
308
- if (typeof step === 'string') {
309
- parts.push(`${i + 1}. ${step}`);
310
- } else {
311
- const title = step.title || '';
312
- const desc = step.description || '';
313
- const code = step.code || '';
314
- const text = [title, desc].filter(Boolean).join(': ');
315
- parts.push(`${i + 1}. ${text || '(步骤)'}`);
316
- if (code) { parts.push(''); parts.push(`\`\`\`\n${code}\n\`\`\``); }
317
- }
318
- });
319
- parts.push('');
320
- }
321
- if (recipe.constraints?.boundaries?.length > 0) {
322
- parts.push('### 约束与边界\n');
323
- for (const b of recipe.constraints.boundaries) { parts.push(`- ${b}`); }
324
- parts.push('');
325
- }
326
-
327
- return parts.join('\n');
328
- }
329
-
330
- #extractRelatedTriggers(relations) {
331
- if (!relations) return [];
332
- const triggers = [];
333
- for (const rel of ['related', 'dependsOn', 'extends', 'conflicts']) {
334
- const items = relations[rel] || [];
335
- for (const item of items) {
336
- if (typeof item === 'string') triggers.push(item);
337
- else if (item?.target) triggers.push(item.target);
338
- }
339
- }
340
- return [...new Set(triggers)];
341
- }
342
-
343
- #hasValues(obj) {
344
- return Object.values(obj).some(v => v != null && v !== 0 && v !== '');
345
- }
346
-
347
- #hasNonEmptyArrays(obj) {
348
- return Object.values(obj).some(v => Array.isArray(v) ? v.length > 0 : false);
349
- }
350
-
351
- #yamlStr(str) {
352
- if (!str) return '""';
353
- if (/[:#\[\]{}&*!|>'"`,@]/.test(str) || str.includes('\n')) {
354
- return `"${this.#esc(str)}"`;
355
- }
356
- return str;
357
- }
358
-
359
- #esc(str) {
360
- return (str || '').replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
361
- }
362
- }
363
-
364
- /* ═══ 公共工具函数(SyncService 等共用)═══════════════════ */
365
-
366
- /**
367
- * 计算 .md 内容的 SHA-256 hash(去除 _contentHash 行后)
368
- * @param {string} content
369
- * @returns {string} 16 字符 hex
370
- */
371
- export function computeContentHash(content) {
372
- // 删除整行(含尾部换行),确保 write/read 两条路径 hash 输入一致
373
- const cleaned = content.replace(/^_contentHash:.*\n?/m, '').trim();
374
- return createHash('sha256').update(cleaned, 'utf8').digest('hex').slice(0, 16);
375
- }
376
-
377
- /**
378
- * 从 .md 内容完整解析 Recipe 数据(基础字段 + 机器字段)
379
- * 供 SyncService / SetupService 共用
380
- * @param {string} content .md 文件全文
381
- * @param {string} relPath 相对于 recipes/ 的路径
382
- * @returns {object}
383
- */
384
- export function parseRecipeMarkdown(content, relPath) {
385
- const fmMatch = content.match(/^---\s*\r?\n([\s\S]*?)\r?\n---/);
386
- const fm = {};
387
-
388
- if (fmMatch) {
389
- const lines = fmMatch[1].split('\n');
390
- let currentKey = null;
391
- let multilineValue = null;
392
- let multilineIndent = 0;
393
-
394
- const flushMultiline = () => {
395
- if (currentKey && multilineValue !== null) {
396
- fm[currentKey] = multilineValue.join('\n').trimEnd();
397
- currentKey = null;
398
- multilineValue = null;
399
- }
400
- };
401
-
402
- for (let i = 0; i < lines.length; i++) {
403
- const line = lines[i];
404
- // 多行值收集中 — 检查缩进
405
- if (multilineValue !== null) {
406
- if (line.length === 0 || /^\s/.test(line)) {
407
- // 仍在多行块内(缩进行或空行)
408
- const stripped = multilineIndent > 0 ? line.slice(multilineIndent) : line;
409
- multilineValue.push(stripped);
410
- continue;
411
- } else {
412
- // 新的顶级 key — 结束多行
413
- flushMultiline();
414
- }
415
- }
416
-
417
- const colonIdx = line.indexOf(':');
418
- if (colonIdx <= 0) continue;
419
- const key = line.slice(0, colonIdx).trim();
420
- if (/\s/.test(key)) continue; // 跳过带空格的非正常 key
421
- let value = line.slice(colonIdx + 1).trim();
422
-
423
- // YAML 多行块标量: key: | 或 key: >
424
- if (value === '|' || value === '>') {
425
- currentKey = key;
426
- multilineValue = [];
427
- // 自动检测下一行缩进
428
- if (i + 1 < lines.length) {
429
- const indentMatch = lines[i + 1].match(/^(\s+)/);
430
- multilineIndent = indentMatch ? indentMatch[1].length : 2;
431
- }
432
- continue;
433
- }
434
-
435
- // 单行 JSON(_ 前缀机器字段)
436
- if (key.startsWith('_') && value.startsWith('{')) {
437
- try { fm[key] = JSON.parse(value); continue; } catch { /* fall through */ }
438
- }
439
-
440
- // 数组
441
- if (value.startsWith('[') && value.endsWith(']')) {
442
- value = value.slice(1, -1).split(',').map(s => s.trim().replace(/^['"]|['"]$/g, ''));
443
- fm[key] = value;
444
- continue;
445
- }
446
-
447
- // 布尔/数字
448
- if (value === 'true') { fm[key] = true; continue; }
449
- if (value === 'false') { fm[key] = false; continue; }
450
- if (/^\d+$/.test(value)) { fm[key] = parseInt(value, 10); continue; }
451
- if (/^\d+\.\d+$/.test(value)) { fm[key] = parseFloat(value); continue; }
452
-
453
- // 字符串(去引号)
454
- fm[key] = value.replace(/^['"]|['"]$/g, '');
455
- }
456
-
457
- // 冲刷最后一个多行值
458
- flushMultiline();
459
- }
460
-
461
- // fallback 逻辑
462
- const basename = relPath.includes('/') ? relPath.split('/').pop() : relPath;
463
- const titleMatch = content.match(/^#\s+(.+)$/m);
464
-
465
- return {
466
- // ── 基础 ──
467
- id: fm.id || basename.replace(/\.md$/i, ''),
468
- title: fm.title || (titleMatch ? titleMatch[1].trim() : basename.replace(/\.md$/i, '')),
469
- trigger: fm.trigger || '',
470
- category: fm.category || 'general',
471
- language: fm.language || 'swift',
472
- summaryCn: fm.summary_cn || '',
473
- summaryEn: fm.summary_en || '',
474
- knowledgeType: fm.knowledgeType || fm.knowledge_type || 'code-pattern',
475
- kind: fm.kind || '',
476
- complexity: fm.complexity || 'intermediate',
477
- scope: fm.scope || null,
478
- tags: Array.isArray(fm.tags) ? fm.tags : [],
479
- headers: Array.isArray(fm.headers) ? fm.headers : [],
480
- status: fm.status || 'active',
481
-
482
- // ── 时间 & 作者 ──
483
- createdBy: fm.createdBy || fm.author || 'system',
484
- createdAt: fm.createdAt || Math.floor(Date.now() / 1000),
485
- updatedAt: fm.updatedAt || Math.floor(Date.now() / 1000),
486
- publishedBy: fm.publishedBy || null,
487
- publishedAt: fm.publishedAt || null,
488
- sourceCandidate: fm.sourceCandidate || null,
489
-
490
- // ── 废弃 ──
491
- deprecated: fm.deprecated || false,
492
- deprecationReason: fm.deprecationReason || null,
493
- deprecatedAt: fm.deprecatedAt || null,
494
-
495
- // ── 维度 ──
496
- authority: fm.authority ?? null,
497
- difficulty: fm.difficulty || null,
498
- version: fm.version || null,
499
-
500
- // ── 机器管理(完整结构)──
501
- quality: fm._quality || { codeCompleteness: 0, projectAdaptation: 0, documentationClarity: 0, overall: 0 },
502
- statistics: fm._statistics || { adoptionCount: 0, applicationCount: 0, guardHitCount: 0, viewCount: 0, successCount: 0, feedbackScore: 0 },
503
- relations: fm._relations || {},
504
- constraints: fm._constraints || {},
505
-
506
- // ── 完整性 ──
507
- _contentHash: fm._contentHash || null,
508
- relatedRecipes: Array.isArray(fm.relatedRecipes) ? fm.relatedRecipes : [],
509
-
510
- // ── 原始 markdown(body 部分 + 完整内容)──
511
- markdown: content,
512
- sourceFile: relPath,
513
- };
514
- }