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
@@ -5,14 +5,14 @@
5
5
  <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <title>AutoSnippet Dashboard</title>
8
- <script type="module" crossorigin src="/assets/index-CqJRvYRL.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-DdmQMrJJ.js"></script>
9
9
  <link rel="modulepreload" crossorigin href="/assets/yaml-qRaU8Ldn.js">
10
10
  <link rel="modulepreload" crossorigin href="/assets/vendor-BotF760a.js">
11
11
  <link rel="modulepreload" crossorigin href="/assets/axios-C0Zqfgkc.js">
12
- <link rel="modulepreload" crossorigin href="/assets/icons-CH-H9x0E.js">
12
+ <link rel="modulepreload" crossorigin href="/assets/icons-BkT3XrKf.js">
13
13
  <link rel="modulepreload" crossorigin href="/assets/syntax-highlighter-CVLHn9O5.js">
14
14
  <link rel="modulepreload" crossorigin href="/assets/react-markdown-BA6FB2NP.js">
15
- <link rel="stylesheet" crossorigin href="/assets/index-DICm9PNa.css">
15
+ <link rel="stylesheet" crossorigin href="/assets/index-BsB7DzW4.css">
16
16
  </head>
17
17
  <body>
18
18
  <div id="root"></div>
@@ -56,7 +56,7 @@ export class AiScanService {
56
56
  }
57
57
 
58
58
  report.files = files.length;
59
- const candidateService = this.container.get('candidateService');
59
+ const knowledgeService = this.container.get('knowledgeService');
60
60
 
61
61
  // 3. 按文件调用 AI 提取
62
62
  for (const file of files) {
@@ -95,29 +95,31 @@ export class AiScanService {
95
95
  }
96
96
 
97
97
  try {
98
- await candidateService.createCandidate({
99
- code: recipe.code,
98
+ await knowledgeService.create({
99
+ content: {
100
+ pattern: recipe.code,
101
+ },
100
102
  language: recipe.language || this._inferLanguage(file.name),
101
103
  category: recipe.category || 'ai-scan',
102
104
  source: 'ai-scan',
105
+ knowledge_type: recipe.knowledgeType || 'code-pattern',
106
+ title: recipe.title || `[AI Scan] ${file.name}`,
107
+ summary_cn: recipe.summary_cn || recipe.summary_en || '',
108
+ tags: [...(recipe.tags || []), 'ai-scan', file.targetName],
109
+ scope: 'project-specific',
103
110
  reasoning: {
104
- whyStandard: recipe.summary_cn || recipe.summary_en || recipe.title || '',
111
+ why_standard: recipe.summary_cn || recipe.summary_en || recipe.title || '',
105
112
  sources: [file.relativePath || file.name],
106
113
  confidence: 0.7,
107
- qualitySignals: { origin: 'ai-scan', completeness: 'full' },
114
+ quality_signals: { origin: 'ai-scan', completeness: 'full' },
108
115
  },
109
116
  metadata: {
110
- title: recipe.title || `[AI Scan] ${file.name}`,
111
- description: recipe.summary_cn || recipe.summary_en || '',
112
- knowledgeType: recipe.knowledgeType || 'code-pattern',
113
- tags: [...(recipe.tags || []), 'ai-scan', file.targetName],
114
117
  trigger: recipe.trigger || '',
115
- scope: 'project-specific',
116
118
  usageGuideCn: recipe.usageGuide_cn || '',
117
119
  usageGuideEn: recipe.usageGuide_en || '',
118
120
  headers: recipe.headers || [],
119
121
  },
120
- }, { userId: 'ai-scan' });
122
+ });
121
123
 
122
124
  report.candidates++;
123
125
  } catch (err) {
@@ -0,0 +1,343 @@
1
+ /**
2
+ * KnowledgeSyncService — 将 .md 文件增量同步到 SQLite DB(knowledge_entries 表)
3
+ *
4
+ * 统一替代 SyncService (Recipe) + CandidateSyncService。
5
+ *
6
+ * 设计原则:
7
+ * - .md 文件 = 完整唯一数据源(Source of Truth),DB = 索引缓存
8
+ * - 通过 _content_hash 检测手写/手改 .md → 进入违规统计(audit_logs)
9
+ * - 孤儿 Entry(DB 有但 .md 不存在)→ 自动标记 deprecated
10
+ * - 同时扫描 AutoSnippet/candidates/ 和 AutoSnippet/recipes/ 两个目录
11
+ *
12
+ * 使用方式:
13
+ * - CLI: `asd sync` 委托调用
14
+ * - 内部: SetupService.stepDatabase() 委托调用(skipViolations = true)
15
+ */
16
+
17
+ import fs from 'node:fs';
18
+ import path from 'node:path';
19
+ import { randomUUID } from 'node:crypto';
20
+ import { RECIPES_DIR, CANDIDATES_DIR } from '../infrastructure/config/Defaults.js';
21
+ import { parseKnowledgeMarkdown, computeKnowledgeHash } from '../service/knowledge/KnowledgeFileWriter.js';
22
+ import { KnowledgeEntry } from '../domain/knowledge/KnowledgeEntry.js';
23
+ import Logger from '../infrastructure/logging/Logger.js';
24
+
25
+ export class KnowledgeSyncService {
26
+ /**
27
+ * @param {string} projectRoot
28
+ */
29
+ constructor(projectRoot) {
30
+ this.projectRoot = projectRoot;
31
+ this.recipesDir = path.join(projectRoot, RECIPES_DIR);
32
+ this.candidatesDir = path.join(projectRoot, CANDIDATES_DIR);
33
+ this.logger = Logger.getInstance();
34
+ }
35
+
36
+ /**
37
+ * 执行增量同步:.md → DB(knowledge_entries 表)
38
+ *
39
+ * 同时扫描 candidates/ 和 recipes/ 两个目录。
40
+ *
41
+ * @param {import('better-sqlite3').Database} db better-sqlite3 原始句柄
42
+ * @param {Object} [opts={}]
43
+ * @param {boolean} [opts.dryRun=false] 只报告不写入
44
+ * @param {boolean} [opts.force=false] 忽略 hash,强制覆盖
45
+ * @param {boolean} [opts.skipViolations=false] 跳过违规记录(setup 场景)
46
+ * @returns {{ synced: number, created: number, updated: number, violations: string[], orphaned: string[], skipped: number }}
47
+ */
48
+ sync(db, opts = {}) {
49
+ const { dryRun = false, force = false, skipViolations = false } = opts;
50
+
51
+ const report = {
52
+ synced: 0,
53
+ created: 0,
54
+ updated: 0,
55
+ violations: [], // 手动编辑的文件列表
56
+ orphaned: [], // DB 有但 .md 不存在
57
+ skipped: 0,
58
+ };
59
+
60
+ // ── 1. 收集 .md 文件(两个目录) ──
61
+ const mdFiles = [
62
+ ...this._collectMdFiles(this.candidatesDir, CANDIDATES_DIR),
63
+ ...this._collectMdFiles(this.recipesDir, RECIPES_DIR),
64
+ ];
65
+
66
+ if (mdFiles.length === 0) {
67
+ this.logger.info('KnowledgeSyncService: no .md files found');
68
+ return report;
69
+ }
70
+
71
+ // ── 2. 准备 upsert 语句 ──
72
+ const upsertStmt = dryRun ? null : this._prepareUpsert(db);
73
+ const auditStmt = (dryRun || skipViolations) ? null : this._prepareAuditInsert(db);
74
+
75
+ // ── 3. 逐文件同步 ──
76
+ const syncedIds = new Set();
77
+
78
+ for (const { absPath, relPath } of mdFiles) {
79
+ try {
80
+ const content = fs.readFileSync(absPath, 'utf8');
81
+ const parsed = parseKnowledgeMarkdown(content, relPath);
82
+
83
+ if (!parsed.id) {
84
+ this.logger.warn(`KnowledgeSyncService: skip file without id — ${relPath}`);
85
+ report.skipped++;
86
+ continue;
87
+ }
88
+
89
+ syncedIds.add(parsed.id);
90
+
91
+ // ── 检测手动编辑 ──
92
+ const actualHash = computeKnowledgeHash(content);
93
+ const storedHash = parsed.content_hash;
94
+ const isManualEdit = storedHash && storedHash !== actualHash && !force;
95
+
96
+ if (isManualEdit) {
97
+ report.violations.push(relPath);
98
+ if (auditStmt) {
99
+ this._logViolation(auditStmt, parsed.id, relPath, storedHash, actualHash);
100
+ }
101
+ }
102
+
103
+ // ── upsert ──
104
+ if (!dryRun) {
105
+ const existed = this._entryExists(db, parsed.id);
106
+ const row = this._buildDbRow(parsed, relPath, content);
107
+ upsertStmt.run(...Object.values(row));
108
+
109
+ if (existed) {
110
+ report.updated++;
111
+ } else {
112
+ report.created++;
113
+ }
114
+ }
115
+
116
+ report.synced++;
117
+ } catch (err) {
118
+ this.logger.error(`KnowledgeSyncService: failed to sync ${relPath}`, { error: err.message });
119
+ report.skipped++;
120
+ }
121
+ }
122
+
123
+ // ── 4. 检测孤儿 ──
124
+ report.orphaned = this._detectOrphans(db, syncedIds, dryRun);
125
+
126
+ this.logger.info('KnowledgeSyncService: sync complete', {
127
+ synced: report.synced,
128
+ created: report.created,
129
+ updated: report.updated,
130
+ violations: report.violations.length,
131
+ orphaned: report.orphaned.length,
132
+ skipped: report.skipped,
133
+ });
134
+
135
+ return report;
136
+ }
137
+
138
+ /* ═══ 文件收集 ═══════════════════════════════════════════ */
139
+
140
+ /**
141
+ * 递归收集指定目录下所有 .md 文件(跳过 _ 前缀模板)
142
+ * @param {string} dir 绝对目录路径
143
+ * @param {string} prefix 相对路径前缀 (e.g. 'AutoSnippet/candidates')
144
+ * @returns {{ absPath: string, relPath: string }[]}
145
+ */
146
+ _collectMdFiles(dir, prefix) {
147
+ if (!fs.existsSync(dir)) return [];
148
+
149
+ const results = [];
150
+ const walk = (curDir, base) => {
151
+ for (const entry of fs.readdirSync(curDir, { withFileTypes: true })) {
152
+ const full = path.join(curDir, entry.name);
153
+ const rel = base ? `${base}/${entry.name}` : entry.name;
154
+
155
+ if (entry.isDirectory()) {
156
+ walk(full, rel);
157
+ } else if (entry.name.endsWith('.md') && !entry.name.startsWith('_')) {
158
+ results.push({
159
+ absPath: full,
160
+ relPath: `${prefix}/${rel}`,
161
+ });
162
+ }
163
+ }
164
+ };
165
+ walk(dir, '');
166
+ return results;
167
+ }
168
+
169
+ /* ═══ DB 操作 ═══════════════════════════════════════════ */
170
+
171
+ /**
172
+ * 从 parseKnowledgeMarkdown 的结果构建 DB row
173
+ * wire format → DB 列映射(与 KnowledgeRepository.impl 对齐)
174
+ */
175
+ _buildDbRow(parsed, relPath, rawContent) {
176
+ const now = Math.floor(Date.now() / 1000);
177
+
178
+ // 内容 hash
179
+ const contentHash = computeKnowledgeHash(rawContent);
180
+
181
+ return {
182
+ id: parsed.id,
183
+ title: parsed.title || '',
184
+ trigger_key: parsed.trigger || '',
185
+ description: parsed.description || '',
186
+ lifecycle: parsed.lifecycle || 'pending',
187
+ lifecycle_history: JSON.stringify(parsed.lifecycle_history || []),
188
+ probation: (parsed.auto_approvable ?? parsed.probation) ? 1 : 0,
189
+ language: parsed.language || 'swift',
190
+ category: parsed.category || 'general',
191
+ kind: parsed.kind || 'pattern',
192
+ knowledge_type: parsed.knowledge_type || 'code-pattern',
193
+ complexity: parsed.complexity || 'intermediate',
194
+ scope: parsed.scope || 'universal',
195
+ difficulty: parsed.difficulty || null,
196
+ tags: JSON.stringify(parsed.tags || []),
197
+ summary_cn: parsed.summary_cn || '',
198
+ summary_en: parsed.summary_en || '',
199
+ usage_guide_cn: parsed.usage_guide_cn || '',
200
+ usage_guide_en: parsed.usage_guide_en || '',
201
+ content: JSON.stringify(parsed.content || {}),
202
+ relations: JSON.stringify(parsed.relations || {}),
203
+ constraints: JSON.stringify(parsed.constraints || {}),
204
+ reasoning: JSON.stringify(parsed.reasoning || {}),
205
+ quality: JSON.stringify(parsed.quality || {}),
206
+ stats: JSON.stringify(parsed.stats || {}),
207
+ headers: JSON.stringify(parsed.headers || []),
208
+ header_paths: JSON.stringify(parsed.header_paths || []),
209
+ module_name: parsed.module_name || '',
210
+ include_headers: parsed.include_headers ? 1 : 0,
211
+ agent_notes: parsed.agent_notes ? JSON.stringify(parsed.agent_notes) : null,
212
+ ai_insight: parsed.ai_insight || null,
213
+ reviewed_by: parsed.reviewed_by || null,
214
+ reviewed_at: parsed.reviewed_at || null,
215
+ rejection_reason: parsed.rejection_reason || null,
216
+ source: parsed.source || 'file-sync',
217
+ source_file: relPath,
218
+ source_candidate_id: parsed.source_candidate_id || null,
219
+ created_by: parsed.created_by || 'file-sync',
220
+ created_at: parsed.created_at || now,
221
+ updated_at: parsed.updated_at || now,
222
+ published_at: parsed.published_at || null,
223
+ published_by: parsed.published_by || null,
224
+ content_hash: contentHash,
225
+ };
226
+ }
227
+
228
+ /**
229
+ * 准备 upsert 语句(INSERT ... ON CONFLICT DO UPDATE 全字段)
230
+ */
231
+ _prepareUpsert(db) {
232
+ const cols = [
233
+ 'id', 'title', 'trigger_key', 'description',
234
+ 'lifecycle', 'lifecycle_history', 'probation',
235
+ 'language', 'category', 'kind', 'knowledge_type', 'complexity', 'scope', 'difficulty',
236
+ 'tags', 'summary_cn', 'summary_en', 'usage_guide_cn', 'usage_guide_en',
237
+ 'content', 'relations', 'constraints', 'reasoning', 'quality', 'stats',
238
+ 'headers', 'header_paths', 'module_name', 'include_headers',
239
+ 'agent_notes', 'ai_insight',
240
+ 'reviewed_by', 'reviewed_at', 'rejection_reason',
241
+ 'source', 'source_file', 'source_candidate_id',
242
+ 'created_by', 'created_at', 'updated_at',
243
+ 'published_at', 'published_by',
244
+ 'content_hash',
245
+ ];
246
+
247
+ // ON CONFLICT 更新除 id, created_by, created_at 以外的所有列
248
+ const updateCols = cols.filter(c => !['id', 'created_by', 'created_at'].includes(c));
249
+ const setClauses = updateCols.map(c => `${c} = excluded.${c}`).join(',\n ');
250
+
251
+ const sql = `
252
+ INSERT INTO knowledge_entries (${cols.join(', ')})
253
+ VALUES (${cols.map(() => '?').join(', ')})
254
+ ON CONFLICT(id) DO UPDATE SET
255
+ ${setClauses}
256
+ `;
257
+
258
+ return db.prepare(sql);
259
+ }
260
+
261
+ /**
262
+ * 检查 entry 是否已存在于 DB
263
+ */
264
+ _entryExists(db, id) {
265
+ const row = db.prepare('SELECT 1 FROM knowledge_entries WHERE id = ?').get(id);
266
+ return !!row;
267
+ }
268
+
269
+ /* ═══ 违规记录 ═══════════════════════════════════════════ */
270
+
271
+ _prepareAuditInsert(db) {
272
+ try {
273
+ return db.prepare(`
274
+ INSERT INTO audit_logs (id, timestamp, actor, actor_context, action, resource, operation_data, result, error_message, duration)
275
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
276
+ `);
277
+ } catch {
278
+ return null;
279
+ }
280
+ }
281
+
282
+ _logViolation(stmt, entryId, filePath, expectedHash, actualHash) {
283
+ try {
284
+ stmt.run(
285
+ randomUUID(),
286
+ Math.floor(Date.now() / 1000),
287
+ 'sync',
288
+ JSON.stringify({ source: 'cli' }),
289
+ 'manual_knowledge_edit',
290
+ entryId,
291
+ JSON.stringify({ file: filePath, expectedHash, actualHash }),
292
+ 'violation_detected',
293
+ null,
294
+ 0,
295
+ );
296
+ } catch (err) {
297
+ this.logger.warn('KnowledgeSyncService: failed to log violation', {
298
+ entryId,
299
+ error: err.message,
300
+ });
301
+ }
302
+ }
303
+
304
+ /* ═══ 孤儿检测 ═══════════════════════════════════════════ */
305
+
306
+ /**
307
+ * 检测 DB 中存在但 .md 已删除的 Entry → 标记 deprecated
308
+ * @returns {string[]} 孤儿 entry id 列表
309
+ */
310
+ _detectOrphans(db, syncedIds, dryRun) {
311
+ const orphanIds = [];
312
+ try {
313
+ const rows = db.prepare(
314
+ `SELECT id, source_file FROM knowledge_entries
315
+ WHERE lifecycle NOT IN ('deprecated')
316
+ AND source_file IS NOT NULL`
317
+ ).all();
318
+
319
+ for (const row of rows) {
320
+ if (!syncedIds.has(row.id)) {
321
+ orphanIds.push(row.id);
322
+ if (!dryRun) {
323
+ const now = Math.floor(Date.now() / 1000);
324
+ db.prepare(
325
+ `UPDATE knowledge_entries
326
+ SET lifecycle = 'deprecated',
327
+ rejection_reason = ?,
328
+ updated_at = ?
329
+ WHERE id = ?`
330
+ ).run('source file deleted (orphan)', now, row.id);
331
+ }
332
+ }
333
+ }
334
+ } catch (err) {
335
+ this.logger.warn('KnowledgeSyncService: orphan detection failed', {
336
+ error: err.message,
337
+ });
338
+ }
339
+ return orphanIds;
340
+ }
341
+ }
342
+
343
+ export default KnowledgeSyncService;
@@ -288,7 +288,7 @@ export class SetupService {
288
288
  ' requires_capability: ["git_write"]',
289
289
  ' - id: "external_agent"',
290
290
  ' name: "External Agent"',
291
- ' permissions: ["read:recipes", "read:guard_rules", "create:candidates", "submit:candidates"]',
291
+ ' permissions: ["read:recipes", "read:guard_rules", "create:candidates", "submit:knowledge"]',
292
292
  ' - id: "chat_agent"',
293
293
  ' name: "ChatAgent"',
294
294
  ' permissions: ["read:recipes", "read:candidates", "create:candidates", "read:guard_rules"]',
@@ -571,40 +571,22 @@ export class SetupService {
571
571
  }
572
572
 
573
573
  /**
574
- * @private 从 AutoSnippet/recipes/*.md 同步到 DB 缓存
575
- * 委托 SyncService 执行全字段同步(setup 场景跳过违规记录)
574
+ * @private 从 AutoSnippet/recipes/*.md + candidates/*.md 同步到 DB 缓存
575
+ * 委托 KnowledgeSyncService 执行全字段同步(setup 场景跳过违规记录)
576
576
  */
577
577
  async _syncRecipesToDB(db) {
578
- const { SyncService } = await import('./SyncService.js');
579
- const syncService = new SyncService(this.projectRoot);
578
+ const { KnowledgeSyncService } = await import('./KnowledgeSyncService.js');
579
+ const syncService = new KnowledgeSyncService(this.projectRoot);
580
580
  const report = syncService.sync(db, { skipViolations: true });
581
581
 
582
582
  if (report.synced > 0) {
583
- console.log(` ✅ 已同步 ${report.synced} Recipe 文件到 DB 缓存(新增 ${report.created},更新 ${report.updated})`);
583
+ console.log(` ✅ 已同步 ${report.synced} 个知识文件到 DB 缓存(新增 ${report.created},更新 ${report.updated})`);
584
584
  } else {
585
- console.log(' ℹ️ recipes/ 暂无 .md 文件,跳过同步');
585
+ console.log(' ℹ️ 暂无 .md 文件,跳过同步');
586
586
  }
587
587
 
588
588
  if (report.orphaned.length > 0) {
589
- console.log(` ℹ️ ${report.orphaned.length} 个孤儿 Recipe 已标记 deprecated`);
590
- }
591
-
592
- // ── Candidate 文件同步 ──
593
- await this._syncCandidatesToDB(db);
594
- }
595
-
596
- /**
597
- * @private 从 AutoSnippet/candidates/*.md 同步到 DB 缓存
598
- */
599
- async _syncCandidatesToDB(db) {
600
- const { CandidateSyncService } = await import('./CandidateSyncService.js');
601
- const syncService = new CandidateSyncService(this.projectRoot);
602
- const report = syncService.sync(db, { skipViolations: true });
603
-
604
- if (report.synced > 0) {
605
- console.log(` ✅ 已同步 ${report.synced} 个 Candidate 文件到 DB 缓存(新增 ${report.created},更新 ${report.updated})`);
606
- } else {
607
- console.log(' ℹ️ candidates/ 暂无 .md 文件,跳过同步');
589
+ console.log(` ℹ️ ${report.orphaned.length} 个孤儿条目已标记 deprecated`);
608
590
  }
609
591
  }
610
592
 
@@ -17,133 +17,125 @@ const logger = Logger.getInstance();
17
17
  * @param {import('../../injection/ServiceContainer.js').ServiceContainer} container
18
18
  */
19
19
  export function registerGatewayActions(gateway, container) {
20
- // ========== Candidate Actions ==========
20
+ // ========== Knowledge Actions (V3: replaces Candidate + Recipe) ==========
21
21
 
22
22
  gateway.register('candidate:create', async (ctx) => {
23
- const service = container.get('candidateService');
24
- return service.createCandidate(ctx.data, {
23
+ const service = container.get('knowledgeService');
24
+ return service.create(ctx.data, {
25
25
  userId: ctx.actor,
26
- ip: ctx.data._ip,
27
- userAgent: ctx.data._userAgent,
28
26
  });
29
27
  });
30
28
 
31
29
  gateway.register('candidate:approve', async (ctx) => {
32
- const service = container.get('candidateService');
33
- return service.approveCandidate(ctx.data.candidateId, {
30
+ const service = container.get('knowledgeService');
31
+ return service.approve(ctx.data.candidateId, {
34
32
  userId: ctx.actor,
35
33
  });
36
34
  });
37
35
 
38
36
  gateway.register('candidate:reject', async (ctx) => {
39
- const service = container.get('candidateService');
40
- return service.rejectCandidate(ctx.data.candidateId, ctx.data.reason, {
37
+ const service = container.get('knowledgeService');
38
+ return service.reject(ctx.data.candidateId, ctx.data.reason, {
41
39
  userId: ctx.actor,
42
40
  });
43
41
  });
44
42
 
45
43
  gateway.register('candidate:apply_to_recipe', async (ctx) => {
46
- const service = container.get('candidateService');
47
- return service.applyToRecipe(ctx.data.candidateId, ctx.data.recipeId, {
48
- userId: ctx.actor,
49
- });
44
+ const service = container.get('knowledgeService');
45
+ return service.publish(ctx.data.candidateId, { userId: ctx.actor });
50
46
  });
51
47
 
52
48
  gateway.register('candidate:list', async (ctx) => {
53
- const service = container.get('candidateService');
54
- return service.listCandidates(ctx.data.filters, ctx.data.pagination);
49
+ const service = container.get('knowledgeService');
50
+ return service.list(ctx.data.filters, ctx.data.pagination);
55
51
  });
56
52
 
57
53
  gateway.register('candidate:search', async (ctx) => {
58
- const service = container.get('candidateService');
59
- return service.searchCandidates(ctx.data.keyword, ctx.data.pagination);
54
+ const service = container.get('knowledgeService');
55
+ return service.search(ctx.data.keyword, ctx.data.pagination);
60
56
  });
61
57
 
62
58
  gateway.register('candidate:get_stats', async (ctx) => {
63
- const service = container.get('candidateService');
64
- return service.getCandidateStats();
59
+ const service = container.get('knowledgeService');
60
+ return service.getStats();
65
61
  });
66
62
 
67
63
  gateway.register('candidate:get', async (ctx) => {
68
- const repo = container.get('candidateRepository');
69
- return repo.findById(ctx.data.id);
64
+ const service = container.get('knowledgeService');
65
+ return service.get(ctx.data.id);
70
66
  });
71
67
 
72
68
  gateway.register('candidate:delete', async (ctx) => {
73
- const service = container.get('candidateService');
74
- return service.deleteCandidate(ctx.data.candidateId, { userId: ctx.actor });
69
+ const service = container.get('knowledgeService');
70
+ return service.delete(ctx.data.candidateId, { userId: ctx.actor });
75
71
  });
76
72
 
77
- // ========== Recipe Actions ==========
73
+ // ========== Recipe Actions (V3: routed to knowledgeService) ==========
78
74
 
79
75
  gateway.register('recipe:create', async (ctx) => {
80
- const service = container.get('recipeService');
81
- return service.createRecipe(ctx.data, {
76
+ const service = container.get('knowledgeService');
77
+ return service.create(ctx.data, {
82
78
  userId: ctx.actor,
83
- ip: ctx.data._ip,
84
- userAgent: ctx.data._userAgent,
85
79
  });
86
80
  });
87
81
 
88
82
  gateway.register('recipe:publish', async (ctx) => {
89
- const service = container.get('recipeService');
90
- return service.publishRecipe(ctx.data.recipeId, {
83
+ const service = container.get('knowledgeService');
84
+ return service.publish(ctx.data.recipeId, {
91
85
  userId: ctx.actor,
92
86
  });
93
87
  });
94
88
 
95
89
  gateway.register('recipe:deprecate', async (ctx) => {
96
- const service = container.get('recipeService');
97
- return service.deprecateRecipe(ctx.data.recipeId, ctx.data.reason, {
90
+ const service = container.get('knowledgeService');
91
+ return service.deprecate(ctx.data.recipeId, ctx.data.reason, {
98
92
  userId: ctx.actor,
99
93
  });
100
94
  });
101
95
 
102
96
  gateway.register('recipe:update_quality', async (ctx) => {
103
- const service = container.get('recipeService');
104
- return service.updateQuality(ctx.data.recipeId, ctx.data.metrics, {
105
- userId: ctx.actor,
106
- });
97
+ const service = container.get('knowledgeService');
98
+ return service.updateQuality(ctx.data.recipeId, ctx.data.metrics);
107
99
  });
108
100
 
109
101
  gateway.register('recipe:adopt', async (ctx) => {
110
- const service = container.get('recipeService');
111
- return service.incrementAdoption(ctx.data.recipeId);
102
+ const service = container.get('knowledgeService');
103
+ return service.incrementUsage(ctx.data.recipeId, 'adoption');
112
104
  });
113
105
 
114
106
  gateway.register('recipe:apply', async (ctx) => {
115
- const service = container.get('recipeService');
116
- return service.incrementApplication(ctx.data.recipeId);
107
+ const service = container.get('knowledgeService');
108
+ return service.incrementUsage(ctx.data.recipeId, 'application');
117
109
  });
118
110
 
119
111
  gateway.register('recipe:list', async (ctx) => {
120
- const service = container.get('recipeService');
121
- return service.listRecipes(ctx.data.filters, ctx.data.pagination);
112
+ const service = container.get('knowledgeService');
113
+ return service.list(ctx.data.filters, ctx.data.pagination);
122
114
  });
123
115
 
124
116
  gateway.register('recipe:search', async (ctx) => {
125
- const service = container.get('recipeService');
126
- return service.searchRecipes(ctx.data.keyword, ctx.data.pagination);
117
+ const service = container.get('knowledgeService');
118
+ return service.search(ctx.data.keyword, ctx.data.pagination);
127
119
  });
128
120
 
129
121
  gateway.register('recipe:get_stats', async (ctx) => {
130
- const service = container.get('recipeService');
131
- return service.getRecipeStats();
122
+ const service = container.get('knowledgeService');
123
+ return service.getStats();
132
124
  });
133
125
 
134
126
  gateway.register('recipe:get', async (ctx) => {
135
- const repo = container.get('recipeRepository');
136
- return repo.findById(ctx.data.id);
127
+ const service = container.get('knowledgeService');
128
+ return service.get(ctx.data.id);
137
129
  });
138
130
 
139
131
  gateway.register('recipe:get_recommendations', async (ctx) => {
140
- const service = container.get('recipeService');
141
- return service.getRecommendations(ctx.data.limit);
132
+ const service = container.get('knowledgeService');
133
+ return service.list({ lifecycle: 'active' }, { page: 1, pageSize: ctx.data.limit || 10 });
142
134
  });
143
135
 
144
136
  gateway.register('recipe:delete', async (ctx) => {
145
- const service = container.get('recipeService');
146
- return service.deleteRecipe(ctx.data.recipeId, {
137
+ const service = container.get('knowledgeService');
138
+ return service.delete(ctx.data.recipeId, {
147
139
  userId: ctx.actor,
148
140
  });
149
141
  });
@@ -200,19 +192,17 @@ export function registerGatewayActions(gateway, container) {
200
192
  });
201
193
 
202
194
  gateway.register('guard_rule:get', async (ctx) => {
203
- const repo = container.get('recipeRepository');
195
+ const repo = container.get('knowledgeRepository');
204
196
  return repo.findById(ctx.data.id);
205
197
  });
206
198
 
207
199
  // ========== Search Actions ==========
208
200
 
209
- // ========== Candidate Update (enrich/refine) ==========
201
+ // ========== Knowledge Update (enrich/refine) ==========
210
202
 
211
203
  gateway.register('candidate:update', async (ctx) => {
212
- const service = container.get('candidateService');
213
- return service.updateCandidate
214
- ? service.updateCandidate(ctx.data.id, ctx.data, { userId: ctx.actor })
215
- : service.createCandidate(ctx.data, { userId: ctx.actor });
204
+ const service = container.get('knowledgeService');
205
+ return service.update(ctx.data.id, ctx.data, { userId: ctx.actor });
216
206
  });
217
207
 
218
208
  // ========== Search ==========