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
@@ -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-DHtzhbuG.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-D4IWpDIk.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-CWBNcF9z.css">
16
16
  </head>
17
17
  <body>
18
18
  <div id="root"></div>
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * AiScanService — `asd ais [Target]` 的核心逻辑
3
3
  *
4
- * 按文件粒度扫描 Target 源码,调用 AI Provider 提取 Recipe 候选,
5
- * 自动创建 PENDING Candidate Dashboard 审核。
4
+ * 按文件粒度扫描 Target 源码,调用 AI Provider 提取 Recipe
5
+ * 创建后自动发布(PENDING ACTIVE),无需 Dashboard 人工审核。
6
6
  *
7
7
  * 与 bootstrap.js 的区别:
8
8
  * - bootstrap 是纯启发式(正则),本服务全程使用 LLM
9
- * - bootstrap 输出 9 条概要 Candidate,本服务按文件输出细粒度 Candidate
9
+ * - bootstrap 输出 9 条概要 Recipe,本服务按文件输出细粒度 Recipe
10
10
  * - 本服务可脱离 MCP 独立在 CLI 运行
11
11
  */
12
12
 
@@ -29,14 +29,14 @@ export class AiScanService {
29
29
  }
30
30
 
31
31
  /**
32
- * 扫描指定 Target(或全部 Target)的源文件并提取候选
32
+ * 扫描指定 Target(或全部 Target)的源文件并提取 Recipe,创建后直接发布
33
33
  * @param {string|null} targetName Target 名称;null 时扫描全部
34
34
  * @param {object} opts { maxFiles, dryRun, concurrency }
35
- * @returns {{ candidates: number, files: number, errors: string[] }}
35
+ * @returns {{ published: number, files: number, errors: string[] }}
36
36
  */
37
37
  async scan(targetName, opts = {}) {
38
38
  const { maxFiles = 200, dryRun = false } = opts;
39
- const report = { candidates: 0, files: 0, errors: [], skipped: 0 };
39
+ const report = { published: 0, files: 0, errors: [], skipped: 0 };
40
40
 
41
41
  // 1. 初始化 AI Provider
42
42
  try {
@@ -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) {
@@ -85,43 +85,42 @@ export class AiScanService {
85
85
  continue;
86
86
  }
87
87
 
88
- // 4. 创建 Candidate
88
+ // 4. 创建并发布 Recipe — AI 输出完整 V3 结构直透
89
89
  for (const recipe of recipes) {
90
- if (!recipe.code || recipe.code.length < 20) continue;
90
+ if (!recipe.content?.pattern || recipe.content.pattern.length < 20) continue;
91
91
 
92
92
  if (dryRun) {
93
- report.candidates++;
93
+ report.published++;
94
94
  continue;
95
95
  }
96
96
 
97
97
  try {
98
- await candidateService.createCandidate({
99
- code: recipe.code,
100
- language: recipe.language || this._inferLanguage(file.name),
101
- category: recipe.category || 'ai-scan',
102
- source: 'ai-scan',
103
- reasoning: {
104
- whyStandard: recipe.summary_cn || recipe.summary_en || recipe.title || '',
105
- sources: [file.relativePath || file.name],
106
- confidence: 0.7,
107
- qualitySignals: { origin: 'ai-scan', completeness: 'full' },
108
- },
109
- 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
- trigger: recipe.trigger || '',
115
- scope: 'project-specific',
116
- usageGuideCn: recipe.usageGuide_cn || '',
117
- usageGuideEn: recipe.usageGuide_en || '',
118
- headers: recipe.headers || [],
119
- },
120
- }, { userId: 'ai-scan' });
121
-
122
- report.candidates++;
98
+ // 来源标记(非 AI 职责)
99
+ recipe.source = 'ai-scan';
100
+ recipe.tags = [...new Set([...(recipe.tags || []), 'ai-scan', file.targetName])];
101
+
102
+ // V3 字段注入:moduleName + sourceFile
103
+ recipe.moduleName = file.targetName;
104
+ recipe.sourceFile = file.relativePath || file.name;
105
+
106
+ // 7.3.9 aiInsight — AI 已输出则直透,否则从 description 生成
107
+ if (!recipe.aiInsight && recipe.description) {
108
+ recipe.aiInsight = recipe.description;
109
+ }
110
+
111
+ const saved = await knowledgeService.create(recipe, { userId: 'ai-scan' });
112
+
113
+ // QualityScorer 自动评分
114
+ try {
115
+ await knowledgeService.updateQuality(saved.id, { userId: 'ai-scan' });
116
+ } catch { /* best effort */ }
117
+
118
+ // 直接发布:PENDING ACTIVE
119
+ await knowledgeService.publish(saved.id, { userId: 'ai-scan' });
120
+
121
+ report.published++;
123
122
  } catch (err) {
124
- report.errors.push(`${file.name}: candidate create failed — ${err.message}`);
123
+ report.errors.push(`${file.name}: recipe publish failed — ${err.message}`);
125
124
  }
126
125
  }
127
126
  } catch (err) {
@@ -0,0 +1,345 @@
1
+ /**
2
+ * KnowledgeSyncService — 将 .md 文件增量同步到 SQLite DB(knowledge_entries 表)
3
+ *
4
+ * 统一替代 SyncService (Recipe) + CandidateSyncService。
5
+ *
6
+ * 设计原则:
7
+ * - .md 文件 = 完整唯一数据源(Source of Truth),DB = 索引缓存
8
+ * - 通过 contentHash 检测手写/手改 .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.contentHash;
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: parsed.trigger || '',
185
+ description: parsed.description || '',
186
+ lifecycle: parsed.lifecycle || 'pending',
187
+ lifecycleHistory: JSON.stringify(parsed.lifecycleHistory || []),
188
+ autoApprovable: parsed.autoApprovable ? 1 : 0,
189
+ language: parsed.language || 'swift',
190
+ category: parsed.category || 'general',
191
+ kind: parsed.kind || 'pattern',
192
+ knowledgeType: parsed.knowledgeType || 'code-pattern',
193
+ complexity: parsed.complexity || 'intermediate',
194
+ scope: parsed.scope || 'universal',
195
+ difficulty: parsed.difficulty || null,
196
+ tags: JSON.stringify(parsed.tags || []),
197
+ content: JSON.stringify(parsed.content || {}),
198
+ relations: JSON.stringify(parsed.relations || {}),
199
+ constraints: JSON.stringify(parsed.constraints || {}),
200
+ reasoning: JSON.stringify(parsed.reasoning || {}),
201
+ quality: JSON.stringify(parsed.quality || {}),
202
+ stats: JSON.stringify(parsed.stats || {}),
203
+ headers: JSON.stringify(parsed.headers || []),
204
+ headerPaths: JSON.stringify(parsed.headerPaths || []),
205
+ moduleName: parsed.moduleName || '',
206
+ includeHeaders: parsed.includeHeaders ? 1 : 0,
207
+ topicHint: parsed.topicHint || null,
208
+ whenClause: parsed.whenClause || null,
209
+ doClause: parsed.doClause || null,
210
+ dontClause: parsed.dontClause || null,
211
+ coreCode: parsed.coreCode || null,
212
+ agentNotes: parsed.agentNotes ? JSON.stringify(parsed.agentNotes) : null,
213
+ aiInsight: parsed.aiInsight || null,
214
+ reviewedBy: parsed.reviewedBy || null,
215
+ reviewedAt: parsed.reviewedAt || null,
216
+ rejectionReason: parsed.rejectionReason || null,
217
+ source: parsed.source || 'file-sync',
218
+ sourceFile: relPath,
219
+ sourceCandidateId: parsed.sourceCandidateId || null,
220
+ createdBy: parsed.createdBy || 'file-sync',
221
+ createdAt: parsed.createdAt || now,
222
+ updatedAt: parsed.updatedAt || now,
223
+ publishedAt: parsed.publishedAt || null,
224
+ publishedBy: parsed.publishedBy || null,
225
+ contentHash: contentHash,
226
+ };
227
+ }
228
+
229
+ /**
230
+ * 准备 upsert 语句(INSERT ... ON CONFLICT DO UPDATE 全字段)
231
+ */
232
+ _prepareUpsert(db) {
233
+ const cols = [
234
+ 'id', 'title', 'trigger', 'description',
235
+ 'lifecycle', 'lifecycleHistory', 'autoApprovable',
236
+ 'language', 'category', 'kind', 'knowledgeType', 'complexity', 'scope', 'difficulty',
237
+ 'tags',
238
+ 'content', 'relations', 'constraints', 'reasoning', 'quality', 'stats',
239
+ 'headers', 'headerPaths', 'moduleName', 'includeHeaders',
240
+ 'topicHint', 'whenClause', 'doClause', 'dontClause', 'coreCode',
241
+ 'agentNotes', 'aiInsight',
242
+ 'reviewedBy', 'reviewedAt', 'rejectionReason',
243
+ 'source', 'sourceFile', 'sourceCandidateId',
244
+ 'createdBy', 'createdAt', 'updatedAt',
245
+ 'publishedAt', 'publishedBy',
246
+ 'contentHash',
247
+ ];
248
+
249
+ // ON CONFLICT 更新除 id, createdBy, createdAt 以外的所有列
250
+ const updateCols = cols.filter(c => !['id', 'createdBy', 'createdAt'].includes(c));
251
+ const setClauses = updateCols.map(c => `${c} = excluded.${c}`).join(',\n ');
252
+
253
+ const sql = `
254
+ INSERT INTO knowledge_entries (${cols.join(', ')})
255
+ VALUES (${cols.map(() => '?').join(', ')})
256
+ ON CONFLICT(id) DO UPDATE SET
257
+ ${setClauses}
258
+ `;
259
+
260
+ return db.prepare(sql);
261
+ }
262
+
263
+ /**
264
+ * 检查 entry 是否已存在于 DB
265
+ */
266
+ _entryExists(db, id) {
267
+ const row = db.prepare('SELECT 1 FROM knowledge_entries WHERE id = ?').get(id);
268
+ return !!row;
269
+ }
270
+
271
+ /* ═══ 违规记录 ═══════════════════════════════════════════ */
272
+
273
+ _prepareAuditInsert(db) {
274
+ try {
275
+ return db.prepare(`
276
+ INSERT INTO audit_logs (id, timestamp, actor, actor_context, action, resource, operation_data, result, error_message, duration)
277
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
278
+ `);
279
+ } catch {
280
+ return null;
281
+ }
282
+ }
283
+
284
+ _logViolation(stmt, entryId, filePath, expectedHash, actualHash) {
285
+ try {
286
+ stmt.run(
287
+ randomUUID(),
288
+ Math.floor(Date.now() / 1000),
289
+ 'sync',
290
+ JSON.stringify({ source: 'cli' }),
291
+ 'manual_knowledge_edit',
292
+ entryId,
293
+ JSON.stringify({ file: filePath, expectedHash, actualHash }),
294
+ 'violation_detected',
295
+ null,
296
+ 0,
297
+ );
298
+ } catch (err) {
299
+ this.logger.warn('KnowledgeSyncService: failed to log violation', {
300
+ entryId,
301
+ error: err.message,
302
+ });
303
+ }
304
+ }
305
+
306
+ /* ═══ 孤儿检测 ═══════════════════════════════════════════ */
307
+
308
+ /**
309
+ * 检测 DB 中存在但 .md 已删除的 Entry → 标记 deprecated
310
+ * @returns {string[]} 孤儿 entry id 列表
311
+ */
312
+ _detectOrphans(db, syncedIds, dryRun) {
313
+ const orphanIds = [];
314
+ try {
315
+ const rows = db.prepare(
316
+ `SELECT id, sourceFile FROM knowledge_entries
317
+ WHERE lifecycle NOT IN ('deprecated')
318
+ AND sourceFile IS NOT NULL`
319
+ ).all();
320
+
321
+ for (const row of rows) {
322
+ if (!syncedIds.has(row.id)) {
323
+ orphanIds.push(row.id);
324
+ if (!dryRun) {
325
+ const now = Math.floor(Date.now() / 1000);
326
+ db.prepare(
327
+ `UPDATE knowledge_entries
328
+ SET lifecycle = 'deprecated',
329
+ rejectionReason = ?,
330
+ updatedAt = ?
331
+ WHERE id = ?`
332
+ ).run('source file deleted (orphan)', now, row.id);
333
+ }
334
+ }
335
+ }
336
+ } catch (err) {
337
+ this.logger.warn('KnowledgeSyncService: orphan detection failed', {
338
+ error: err.message,
339
+ });
340
+ }
341
+ return orphanIds;
342
+ }
343
+ }
344
+
345
+ 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
 
@@ -163,6 +163,34 @@ export class UpgradeService {
163
163
  mkdirSync(destDir, { recursive: true });
164
164
  copyFileSync(src, dest);
165
165
  console.log(' ✅ .cursor/rules/autosnippet-conventions.mdc');
166
+
167
+ // 动态生成 4 通道交付物料(如 ServiceContainer 已初始化)
168
+ this._triggerCursorDelivery();
169
+ }
170
+
171
+ /**
172
+ * 触发 Cursor Delivery Pipeline 动态生成
173
+ * 非阻塞 — 失败不影响 upgrade 流程
174
+ */
175
+ _triggerCursorDelivery() {
176
+ import('../injection/ServiceContainer.js')
177
+ .then(({ getServiceContainer }) => {
178
+ const container = getServiceContainer();
179
+ if (container.services.cursorDeliveryPipeline) {
180
+ const pipeline = container.get('cursorDeliveryPipeline');
181
+ pipeline.deliver()
182
+ .then(result => {
183
+ console.log(` ✅ Cursor Delivery: ${result.channelA.rulesCount} rules, ` +
184
+ `${result.channelB.topicCount} topics, ${result.channelC.synced} skills`);
185
+ })
186
+ .catch(err => {
187
+ console.log(` ⚠️ Cursor Delivery 跳过: ${err.message}`);
188
+ });
189
+ }
190
+ })
191
+ .catch(() => {
192
+ // ServiceContainer 未初始化 — 正常(upgrade 可能在无 DB 环境执行)
193
+ });
166
194
  }
167
195
 
168
196
  /* ═══ Copilot Instructions ══════════════════════════ */