autosnippet 2.8.3 → 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 (110) hide show
  1. package/README.md +5 -5
  2. package/bin/cli.js +5 -33
  3. package/config/constitution.yaml +9 -2
  4. package/dashboard/dist/assets/{icons-B_Xg4B-s.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 +9 -27
  11. package/lib/core/ast/ProjectGraph.js +160 -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 +351 -0
  15. package/lib/domain/knowledge/KnowledgeRepository.js +123 -0
  16. package/lib/domain/knowledge/Lifecycle.js +109 -0
  17. package/lib/domain/knowledge/index.js +27 -0
  18. package/lib/domain/knowledge/values/Constraints.js +125 -0
  19. package/lib/domain/knowledge/values/Content.js +86 -0
  20. package/lib/domain/knowledge/values/Quality.js +93 -0
  21. package/lib/domain/knowledge/values/Reasoning.js +69 -0
  22. package/lib/domain/knowledge/values/Relations.js +168 -0
  23. package/lib/domain/knowledge/values/Stats.js +87 -0
  24. package/lib/domain/knowledge/values/index.js +9 -0
  25. package/lib/external/ai/AiProvider.js +48 -0
  26. package/lib/external/ai/providers/GoogleGeminiProvider.js +12 -3
  27. package/lib/external/mcp/McpServer.js +7 -5
  28. package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +3 -2
  29. package/lib/external/mcp/handlers/bootstrap.js +121 -12
  30. package/lib/external/mcp/handlers/browse.js +77 -73
  31. package/lib/external/mcp/handlers/candidate.js +29 -276
  32. package/lib/external/mcp/handlers/guard.js +2 -0
  33. package/lib/external/mcp/handlers/knowledge.js +205 -0
  34. package/lib/external/mcp/handlers/skill.js +4 -2
  35. package/lib/external/mcp/handlers/structure.js +25 -23
  36. package/lib/external/mcp/handlers/system.js +10 -12
  37. package/lib/external/mcp/tools.js +125 -138
  38. package/lib/http/HttpServer.js +4 -8
  39. package/lib/http/middleware/requestLogger.js +3 -3
  40. package/lib/http/routes/ai.js +17 -1
  41. package/lib/http/routes/extract.js +48 -4
  42. package/lib/http/routes/knowledge.js +246 -0
  43. package/lib/http/routes/search.js +12 -17
  44. package/lib/http/routes/skills.js +44 -1
  45. package/lib/infrastructure/cache/GraphCache.js +143 -0
  46. package/lib/infrastructure/database/migrations/015_create_token_usage.js +27 -0
  47. package/lib/infrastructure/database/migrations/016_unified_knowledge_entries.js +395 -0
  48. package/lib/infrastructure/external/XcodeAutomation.js +187 -103
  49. package/lib/infrastructure/realtime/RealtimeService.js +14 -2
  50. package/lib/injection/ServiceContainer.js +164 -63
  51. package/lib/repository/knowledge/KnowledgeRepository.impl.js +373 -0
  52. package/lib/repository/token/TokenUsageStore.js +162 -0
  53. package/lib/service/automation/DirectiveDetector.js +2 -3
  54. package/lib/service/automation/FileWatcher.js +67 -28
  55. package/lib/service/automation/XcodeIntegration.js +931 -156
  56. package/lib/service/automation/handlers/AlinkHandler.js +6 -4
  57. package/lib/service/automation/handlers/CreateHandler.js +53 -18
  58. package/lib/service/automation/handlers/GuardHandler.js +183 -20
  59. package/lib/service/automation/handlers/SearchHandler.js +35 -17
  60. package/lib/service/chat/AnalystAgent.js +25 -14
  61. package/lib/service/chat/CandidateGuardrail.js +1 -1
  62. package/lib/service/chat/ChatAgent.js +280 -48
  63. package/lib/service/chat/ContextWindow.js +92 -8
  64. package/lib/service/chat/HandoffProtocol.js +26 -1
  65. package/lib/service/chat/ProducerAgent.js +11 -9
  66. package/lib/service/chat/tools.js +298 -194
  67. package/lib/service/guard/GuardCheckEngine.js +114 -10
  68. package/lib/service/guard/GuardService.js +59 -48
  69. package/lib/service/knowledge/ConfidenceRouter.js +159 -0
  70. package/lib/service/knowledge/KnowledgeFileWriter.js +602 -0
  71. package/lib/service/knowledge/KnowledgeService.js +725 -0
  72. package/lib/service/search/SearchEngine.js +92 -19
  73. package/lib/service/skills/SignalCollector.js +15 -9
  74. package/lib/service/skills/SkillAdvisor.js +13 -11
  75. package/lib/service/snippet/SnippetFactory.js +5 -5
  76. package/lib/service/spm/SpmService.js +119 -18
  77. package/package.json +1 -1
  78. package/scripts/install-cursor-skill.js +0 -6
  79. package/scripts/migrate-md-to-knowledge.mjs +364 -0
  80. package/skills/autosnippet-analysis/SKILL.md +15 -7
  81. package/skills/autosnippet-candidates/SKILL.md +6 -6
  82. package/skills/autosnippet-coldstart/SKILL.md +7 -3
  83. package/skills/autosnippet-concepts/SKILL.md +7 -6
  84. package/skills/autosnippet-create/SKILL.md +13 -13
  85. package/skills/autosnippet-intent/SKILL.md +3 -2
  86. package/skills/autosnippet-lifecycle/SKILL.md +5 -5
  87. package/skills/autosnippet-recipes/SKILL.md +16 -4
  88. package/templates/constitution.yaml +1 -1
  89. package/templates/copilot-instructions.md +6 -6
  90. package/templates/recipes-setup/README.md +3 -3
  91. package/dashboard/dist/assets/index-CkIih2CC.css +0 -1
  92. package/dashboard/dist/assets/index-Duc8Qk-c.js +0 -197
  93. package/lib/cli/CandidateSyncService.js +0 -261
  94. package/lib/cli/SyncService.js +0 -356
  95. package/lib/domain/candidate/Candidate.js +0 -196
  96. package/lib/domain/candidate/CandidateRepository.js +0 -107
  97. package/lib/domain/candidate/Reasoning.js +0 -52
  98. package/lib/domain/recipe/Recipe.js +0 -421
  99. package/lib/domain/recipe/RecipeRepository.js +0 -54
  100. package/lib/domain/types/CandidateStatus.js +0 -52
  101. package/lib/http/routes/candidates.js +0 -559
  102. package/lib/http/routes/recipes.js +0 -397
  103. package/lib/repository/candidate/CandidateRepository.impl.js +0 -230
  104. package/lib/repository/recipe/RecipeRepository.impl.js +0 -498
  105. package/lib/service/candidate/CandidateAggregator.js +0 -52
  106. package/lib/service/candidate/CandidateFileWriter.js +0 -383
  107. package/lib/service/candidate/CandidateService.js +0 -973
  108. package/lib/service/recipe/RecipeFileWriter.js +0 -514
  109. package/lib/service/recipe/RecipeService.js +0 -786
  110. 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-Duc8Qk-c.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-B_Xg4B-s.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-CkIih2CC.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
 
@@ -664,7 +646,7 @@ export class SetupService {
664
646
  '# 完整配置说明见 .env.example',
665
647
  '',
666
648
  'ASD_AI_PROVIDER=google',
667
- 'ASD_AI_MODEL=gemini-2.0-flash',
649
+ 'ASD_AI_MODEL=gemini-3-flash-preview',
668
650
  '# ASD_GOOGLE_API_KEY=',
669
651
  '',
670
652
  ].join('\n'));
@@ -537,6 +537,166 @@ export default class ProjectGraph {
537
537
  }
538
538
  }
539
539
  }
540
+
541
+ // ── 序列化 / 反序列化 ──────────────────────────────────────
542
+
543
+ /**
544
+ * 序列化为可 JSON.stringify 的纯对象
545
+ * @returns {object}
546
+ */
547
+ toJSON() {
548
+ const mapToObj = (map) => Object.fromEntries(map);
549
+ const mapOfSetsToObj = (map) => {
550
+ const obj = {};
551
+ for (const [k, v] of map) obj[k] = [...v];
552
+ return obj;
553
+ };
554
+
555
+ return {
556
+ projectRoot: this.#projectRoot,
557
+ buildTimeMs: this.#buildTimeMs,
558
+ classes: mapToObj(this.#classes),
559
+ protocols: mapToObj(this.#protocols),
560
+ categories: mapToObj(this.#categories),
561
+ inheritance: mapToObj(this.#inheritance),
562
+ conformance: mapOfSetsToObj(this.#conformance),
563
+ files: mapToObj(this.#files),
564
+ methodsByClass: mapToObj(this.#methodsByClass),
565
+ };
566
+ }
567
+
568
+ /**
569
+ * 从缓存数据恢复 ProjectGraph 实例
570
+ * @param {object} data toJSON() 输出的对象
571
+ * @returns {ProjectGraph}
572
+ */
573
+ static fromJSON(data) {
574
+ const graph = new ProjectGraph();
575
+ graph.#projectRoot = data.projectRoot || '';
576
+ graph.#buildTimeMs = data.buildTimeMs || 0;
577
+
578
+ // 恢复 classes
579
+ for (const [name, info] of Object.entries(data.classes || {})) {
580
+ graph.#classes.set(name, info);
581
+ }
582
+
583
+ // 恢复 protocols
584
+ for (const [name, info] of Object.entries(data.protocols || {})) {
585
+ graph.#protocols.set(name, info);
586
+ }
587
+
588
+ // 恢复 categories
589
+ for (const [name, arr] of Object.entries(data.categories || {})) {
590
+ graph.#categories.set(name, arr);
591
+ }
592
+
593
+ // 恢复 inheritance
594
+ for (const [child, parent] of Object.entries(data.inheritance || {})) {
595
+ graph.#inheritance.set(child, parent);
596
+ }
597
+
598
+ // 恢复 conformance (Set)
599
+ for (const [cls, protos] of Object.entries(data.conformance || {})) {
600
+ graph.#conformance.set(cls, new Set(protos));
601
+ }
602
+
603
+ // 恢复 files
604
+ for (const [path, symbols] of Object.entries(data.files || {})) {
605
+ graph.#files.set(path, symbols);
606
+ }
607
+
608
+ // 恢复 methodsByClass
609
+ for (const [cls, methods] of Object.entries(data.methodsByClass || {})) {
610
+ graph.#methodsByClass.set(cls, methods);
611
+ }
612
+
613
+ return graph;
614
+ }
615
+
616
+ /**
617
+ * 增量更新:仅重新解析变更文件,合并到现有图中
618
+ * @param {string[]} changedPaths 变更文件的绝对路径
619
+ * @param {string[]} deletedPaths 删除文件的相对路径
620
+ * @param {object} [options]
621
+ * @returns {Promise<{ added: number, updated: number, deleted: number }>}
622
+ */
623
+ async incrementalUpdate(changedPaths, deletedPaths = [], options = {}) {
624
+ const { analyzeFile, isAvailable } = await import('../AstAnalyzer.js');
625
+ if (!isAvailable()) return { added: 0, updated: 0, deleted: 0 };
626
+
627
+ const extToLang = options.extensionToLang || DEFAULTS.extensionToLang;
628
+ let added = 0, updated = 0, deleted = 0;
629
+
630
+ // 1. 删除已移除文件的索引
631
+ for (const relPath of deletedPaths) {
632
+ if (this.#files.has(relPath)) {
633
+ const symbols = this.#files.get(relPath);
634
+ // 清除该文件贡献的类、协议、Category
635
+ for (const cls of symbols.classes || []) {
636
+ this.#classes.delete(cls);
637
+ this.#inheritance.delete(cls);
638
+ this.#conformance.delete(cls);
639
+ this.#methodsByClass.delete(cls);
640
+ }
641
+ for (const proto of symbols.protocols || []) {
642
+ this.#protocols.delete(proto);
643
+ }
644
+ for (const catKey of symbols.categories || []) {
645
+ const className = catKey.split('(')[0];
646
+ this.#categories.delete(className);
647
+ }
648
+ this.#files.delete(relPath);
649
+ deleted++;
650
+ }
651
+ }
652
+
653
+ // 2. 重新解析变更文件
654
+ for (const filePath of changedPaths) {
655
+ try {
656
+ const content = fs.readFileSync(filePath, 'utf-8');
657
+ const ext = path.extname(filePath);
658
+ const lang = extToLang[ext];
659
+ if (!lang) continue;
660
+
661
+ const relativePath = path.relative(this.#projectRoot, filePath);
662
+ const isUpdate = this.#files.has(relativePath);
663
+
664
+ // 先清除旧索引(如果是更新)
665
+ if (isUpdate) {
666
+ const oldSymbols = this.#files.get(relativePath);
667
+ for (const cls of oldSymbols.classes || []) {
668
+ this.#classes.delete(cls);
669
+ this.#inheritance.delete(cls);
670
+ this.#conformance.delete(cls);
671
+ this.#methodsByClass.delete(cls);
672
+ }
673
+ for (const proto of oldSymbols.protocols || []) {
674
+ this.#protocols.delete(proto);
675
+ }
676
+ for (const catKey of oldSymbols.categories || []) {
677
+ const className = catKey.split('(')[0];
678
+ this.#categories.delete(className);
679
+ }
680
+ }
681
+
682
+ const summary = analyzeFile(content, lang);
683
+ if (!summary) continue;
684
+
685
+ this.#indexFileSummary(relativePath, summary);
686
+ isUpdate ? updated++ : added++;
687
+ } catch {
688
+ // 单文件解析失败不阻塞
689
+ }
690
+ }
691
+
692
+ // 3. 重建反向索引
693
+ if (added + updated + deleted > 0) {
694
+ this.#buildReverseIndices();
695
+ this.#overview = null; // 清除统计缓存
696
+ }
697
+
698
+ return { added, updated, deleted };
699
+ }
540
700
  }
541
701
 
542
702
  // ──────────────────────────────────────────────────────────────────