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
@@ -0,0 +1,268 @@
1
+ /**
2
+ * SkillsSyncer — AutoSnippet Skills to .cursor/skills/ 同步器
3
+ *
4
+ * Channel C: 将 AutoSnippet/skills/ 下的项目级 SKILL.md 同步到
5
+ * .cursor/skills/autosnippet-{name}/ 目录,适配 Cursor Agent Skills 标准格式。
6
+ *
7
+ * 同时为每个 Skill 生成 references/RECIPES.md(相关 Recipe 摘要)。
8
+ */
9
+
10
+ import fs from 'node:fs';
11
+ import path from 'node:path';
12
+
13
+ /**
14
+ * 技能名称映射:AutoSnippet/skills/ → .cursor/skills/
15
+ * AutoSnippet/skills/ 下面是 bootstrap 动态生成的项目级 skills,
16
+ * 如 project-architecture/, project-code-standard/ 等。
17
+ */
18
+ const SKILL_NAME_MAP = {
19
+ 'project-architecture': 'autosnippet-architecture',
20
+ 'project-code-standard': 'autosnippet-code-standard',
21
+ 'project-profile': 'autosnippet-profile',
22
+ 'project-agent-guidelines': 'autosnippet-guidelines',
23
+ 'project-event-and-data-flow': 'autosnippet-data-flow',
24
+ 'project-code-pattern': 'autosnippet-code-pattern',
25
+ 'project-objc-deep-scan': 'autosnippet-objc-deep-scan',
26
+ 'project-category-scan': 'autosnippet-category-scan',
27
+ 'project-best-practice': 'autosnippet-best-practice',
28
+ };
29
+
30
+ /**
31
+ * 用途描述模板(英文,Cursor 优先)
32
+ */
33
+ const SKILL_DESC_MAP = {
34
+ 'autosnippet-architecture': 'Architecture patterns, module boundaries, and dependency rules for {project}. Use when creating new modules, reviewing architecture, or understanding dependencies.',
35
+ 'autosnippet-code-standard': 'Coding standards and style conventions for {project}. Use when writing new code, reviewing formatting, or enforcing naming conventions.',
36
+ 'autosnippet-profile': 'Project overview and profile for {project}. Use when needing background on the project, its tech stack, or structure.',
37
+ 'autosnippet-guidelines': 'Agent interaction guidelines for {project}. Use when understanding how to work with this specific project.',
38
+ 'autosnippet-data-flow': 'Event and data flow patterns for {project}. Use when working with events, state management, or data pipelines.',
39
+ 'autosnippet-code-pattern': 'Common code patterns and idioms for {project}. Use when implementing features following project conventions.',
40
+ 'autosnippet-objc-deep-scan': 'Objective-C deep scan results for {project}. Use when working with Objective-C code, method swizzling, or runtime features.',
41
+ 'autosnippet-category-scan': 'Category and extension analysis for {project}. Use when working with categories or finding existing utility methods.',
42
+ 'autosnippet-best-practice': 'Best practices and proven patterns for {project}. Use when making design decisions or code review.',
43
+ };
44
+
45
+ export class SkillsSyncer {
46
+ /**
47
+ * @param {string} projectRoot - 用户项目根目录
48
+ * @param {string} projectName - 项目名称
49
+ * @param {Object} [knowledgeService] - 可选,用于生成 references/RECIPES.md
50
+ */
51
+ constructor(projectRoot, projectName = 'Project', knowledgeService = null) {
52
+ this.projectRoot = projectRoot;
53
+ this.projectName = projectName;
54
+ this.knowledgeService = knowledgeService;
55
+ this.sourceDir = path.join(projectRoot, 'AutoSnippet', 'skills');
56
+ this.targetDir = path.join(projectRoot, '.cursor', 'skills');
57
+ }
58
+
59
+ /**
60
+ * 执行完整同步流程
61
+ * @returns {{ synced: string[], skipped: string[], errors: string[] }}
62
+ */
63
+ async sync() {
64
+ const result = { synced: [], skipped: [], errors: [] };
65
+
66
+ // 检查源目录是否存在
67
+ if (!fs.existsSync(this.sourceDir)) {
68
+ return result;
69
+ }
70
+
71
+ // 扫描源目录
72
+ const skillDirs = fs.readdirSync(this.sourceDir, { withFileTypes: true })
73
+ .filter(d => d.isDirectory())
74
+ .map(d => d.name);
75
+
76
+ for (const dirName of skillDirs) {
77
+ try {
78
+ const sourceSkillPath = path.join(this.sourceDir, dirName, 'SKILL.md');
79
+ if (!fs.existsSync(sourceSkillPath)) {
80
+ result.skipped.push(dirName);
81
+ continue;
82
+ }
83
+
84
+ const targetName = SKILL_NAME_MAP[dirName] || `autosnippet-${dirName.replace(/^project-/, '')}`;
85
+ const targetSkillDir = path.join(this.targetDir, targetName);
86
+
87
+ // 创建目标目录
88
+ fs.mkdirSync(targetSkillDir, { recursive: true });
89
+
90
+ // 读取源 SKILL.md
91
+ const sourceContent = fs.readFileSync(sourceSkillPath, 'utf8');
92
+
93
+ // 转换格式
94
+ const targetContent = this._convertSkillMd(sourceContent, targetName, dirName);
95
+
96
+ // 写入目标 SKILL.md
97
+ fs.writeFileSync(path.join(targetSkillDir, 'SKILL.md'), targetContent, 'utf8');
98
+
99
+ // 生成 references/RECIPES.md
100
+ await this._generateRecipes(targetSkillDir, dirName);
101
+
102
+ result.synced.push(targetName);
103
+ } catch (err) {
104
+ result.errors.push(`${dirName}: ${err.message}`);
105
+ }
106
+ }
107
+
108
+ return result;
109
+ }
110
+
111
+ /**
112
+ * 转换 SKILL.md 格式 — 从 AutoSnippet 格式到 Cursor Agent Skills 标准
113
+ * @private
114
+ */
115
+ _convertSkillMd(source, targetName, sourceDirName) {
116
+ // 提取原始内容(去掉 frontmatter)
117
+ const bodyMatch = source.match(/^---\n[\s\S]*?\n---\n([\s\S]*)$/);
118
+ const body = bodyMatch ? bodyMatch[1].trim() : source.trim();
119
+
120
+ // 获取描述
121
+ const descTemplate = SKILL_DESC_MAP[targetName] || `Knowledge and patterns from {project}. Use when working with ${sourceDirName.replace(/^project-/, '')} related code.`;
122
+ const description = descTemplate.replace(/\{project\}/g, this.projectName);
123
+
124
+ // 构建 Cursor 标准格式
125
+ const dimensionLabel = sourceDirName.replace(/^project-/, '').replace(/-/g, ' ');
126
+ const lines = [
127
+ '---',
128
+ `name: ${targetName}`,
129
+ `description: "${description}"`,
130
+ '---',
131
+ '',
132
+ `# ${this._capitalizeWords(dimensionLabel)} — ${this.projectName}`,
133
+ '',
134
+ 'Use this skill when:',
135
+ ...this._generateUseCases(sourceDirName),
136
+ '',
137
+ '## Instructions',
138
+ '',
139
+ body,
140
+ '',
141
+ '## Deeper Knowledge',
142
+ '',
143
+ `For detailed recipes and code examples:`,
144
+ `- \`autosnippet_search("${dimensionLabel}")\``,
145
+ '',
146
+ '## Referenced Files',
147
+ '',
148
+ 'See `references/RECIPES.md` for related recipe summaries.',
149
+ ];
150
+ return lines.join('\n') + '\n';
151
+ }
152
+
153
+ /**
154
+ * 生成 references/RECIPES.md
155
+ * @private
156
+ */
157
+ async _generateRecipes(targetSkillDir, sourceDirName) {
158
+ const refsDir = path.join(targetSkillDir, 'references');
159
+ fs.mkdirSync(refsDir, { recursive: true });
160
+
161
+ // 如果有 knowledgeService,查询该维度的 recipes
162
+ let recipes = [];
163
+ if (this.knowledgeService) {
164
+ try {
165
+ const dimension = sourceDirName.replace(/^project-/, '');
166
+ const result = await this.knowledgeService.list(
167
+ { lifecycle: 'active', category: dimension },
168
+ { page: 1, pageSize: 50 }
169
+ );
170
+ recipes = result?.items || result?.data || [];
171
+ if (Array.isArray(result)) recipes = result;
172
+ } catch {
173
+ // 忽略查询错误
174
+ }
175
+ }
176
+
177
+ // 生成 RECIPES.md
178
+ const dimensionLabel = sourceDirName.replace(/^project-/, '').replace(/-/g, ' ');
179
+ const lines = [
180
+ `# ${this._capitalizeWords(dimensionLabel)} Recipes`,
181
+ '',
182
+ ];
183
+
184
+ if (recipes.length > 0) {
185
+ lines.push('| Title | Trigger | Summary |');
186
+ lines.push('|---|---|---|');
187
+ for (const entry of recipes.slice(0, 20)) {
188
+ const title = (entry.title || '').replace(/\|/g, '/');
189
+ const trigger = entry.trigger || '-';
190
+ const summary = (entry.summaryCn || entry.description || '').replace(/\|/g, '/').slice(0, 80);
191
+ lines.push(`| ${title} | ${trigger} | ${summary} |`);
192
+ }
193
+ } else {
194
+ lines.push('No recipes available yet. Run `asd bootstrap` to generate knowledge.');
195
+ }
196
+
197
+ lines.push('');
198
+ lines.push(`For full content, use: \`autosnippet_search("${dimensionLabel}")\``);
199
+
200
+ fs.writeFileSync(path.join(refsDir, 'RECIPES.md'), lines.join('\n') + '\n', 'utf8');
201
+ }
202
+
203
+ /**
204
+ * 生成使用场景列表
205
+ * @private
206
+ */
207
+ _generateUseCases(sourceDirName) {
208
+ const casesMap = {
209
+ 'project-architecture': [
210
+ '- Creating new modules, services, or managers',
211
+ '- Reviewing architectural decisions',
212
+ '- Understanding module boundaries and dependency rules',
213
+ ],
214
+ 'project-code-standard': [
215
+ '- Writing new code and need to follow coding standards',
216
+ '- Reviewing code formatting and naming conventions',
217
+ '- Setting up new files with proper structure',
218
+ ],
219
+ 'project-profile': [
220
+ '- Need background on the project and tech stack',
221
+ '- Understanding the overall project structure',
222
+ '- Onboarding or getting project context',
223
+ ],
224
+ 'project-agent-guidelines': [
225
+ '- Understanding project-specific workflow requirements',
226
+ '- Following project conventions for AI-assisted coding',
227
+ ],
228
+ 'project-event-and-data-flow': [
229
+ '- Working with events, notifications, or callbacks',
230
+ '- Implementing data flow or state management',
231
+ '- Understanding how data moves through the system',
232
+ ],
233
+ 'project-code-pattern': [
234
+ '- Implementing features using project conventions',
235
+ '- Looking for common code patterns and idioms',
236
+ '- Need a code template for a typical operation',
237
+ ],
238
+ 'project-objc-deep-scan': [
239
+ '- Working with Objective-C runtime features',
240
+ '- Understanding method swizzling or hook registries',
241
+ '- Modifying sensitive Objective-C code',
242
+ ],
243
+ 'project-category-scan': [
244
+ '- Looking for existing utility methods',
245
+ '- Working with categories or extensions',
246
+ '- Avoiding duplicate implementations',
247
+ ],
248
+ 'project-best-practice': [
249
+ '- Making design decisions',
250
+ '- Code review and quality improvements',
251
+ '- Choosing between implementation approaches',
252
+ ],
253
+ };
254
+ return casesMap[sourceDirName] || [
255
+ '- Working with code related to this dimension',
256
+ '- Need guidance on project-specific patterns',
257
+ ];
258
+ }
259
+
260
+ /**
261
+ * @private
262
+ */
263
+ _capitalizeWords(str) {
264
+ return str.replace(/\b\w/g, c => c.toUpperCase());
265
+ }
266
+ }
267
+
268
+ export default SkillsSyncer;
@@ -0,0 +1,58 @@
1
+ /**
2
+ * TokenBudget — Token 预算控制
3
+ *
4
+ * 简易 token 估算器(1 token ≈ 4 chars for English, 2 chars for CJK),
5
+ * 用于确保 .mdc 文件不超出 Cursor 上下文预算。
6
+ */
7
+
8
+ /** 默认预算配置 */
9
+ export const BUDGET = {
10
+ CHANNEL_A_MAX: 400, // Always-On Rules 最大 token
11
+ CHANNEL_B_MAX_PER_FILE: 750, // Smart Rules 每个主题文件最大 token
12
+ CHANNEL_B_MAX_PATTERNS: 5, // Smart Rules 每个主题最多模式数
13
+ CHANNEL_A_MAX_RULES: 8, // Always-On Rules 最多规则数
14
+ };
15
+
16
+ /**
17
+ * 估算文本 token 数
18
+ * 简易算法:英文按 4 chars/token,CJK 按 2 chars/token
19
+ * @param {string} text
20
+ * @returns {number}
21
+ */
22
+ export function estimateTokens(text) {
23
+ if (!text) return 0;
24
+ let tokens = 0;
25
+ for (const ch of text) {
26
+ // CJK Unified Ideographs + common CJK ranges
27
+ if (ch.charCodeAt(0) > 0x2e80) {
28
+ tokens += 0.5; // ~2 chars per token for CJK
29
+ } else {
30
+ tokens += 0.25; // ~4 chars per token for English
31
+ }
32
+ }
33
+ return Math.ceil(tokens);
34
+ }
35
+
36
+ /**
37
+ * 按 token 预算截断内容行
38
+ * @param {string[]} lines - 内容行
39
+ * @param {number} budget - token 上限
40
+ * @returns {{ kept: string[], dropped: number, tokensUsed: number }}
41
+ */
42
+ export function truncateToTokenBudget(lines, budget) {
43
+ const kept = [];
44
+ let tokensUsed = 0;
45
+ let dropped = 0;
46
+
47
+ for (const line of lines) {
48
+ const lineTokens = estimateTokens(line);
49
+ if (tokensUsed + lineTokens <= budget) {
50
+ kept.push(line);
51
+ tokensUsed += lineTokens;
52
+ } else {
53
+ dropped++;
54
+ }
55
+ }
56
+
57
+ return { kept, dropped, tokensUsed };
58
+ }
@@ -0,0 +1,141 @@
1
+ /**
2
+ * TopicClassifier — 按主题分组 KnowledgeEntry
3
+ *
4
+ * 将 kind='pattern' 的知识条目按主题分组,用于 Channel B(Smart Rules)。
5
+ * 每个主题对应一个 .mdc 文件,设 alwaysApply: false + 丰富的 description。
6
+ */
7
+
8
+ /**
9
+ * 主题定义
10
+ * - dimensions: 关联的 bootstrap 维度
11
+ * - keywords: 用于匹配 entry 分类的关键词
12
+ * - descriptionKeywords: 用于 .mdc description 字段的关键词(Agent 关联性判断依据)
13
+ */
14
+ const TOPIC_MAP = {
15
+ networking: {
16
+ dimensions: ['event-and-data-flow'],
17
+ descriptionKeywords: 'network, HTTP, API, request, response, URL, fetch, socket, REST, download, upload, error handling, retry, timeout',
18
+ },
19
+ ui: {
20
+ dimensions: ['code-pattern'],
21
+ descriptionKeywords: 'view, controller, UI, layout, animation, cell, table, collection, button, scroll, navigation, auto layout, gesture, storyboard',
22
+ },
23
+ data: {
24
+ dimensions: ['code-pattern', 'architecture'],
25
+ descriptionKeywords: 'model, storage, database, cache, CoreData, Realm, SQLite, keychain, UserDefaults, JSON, parsing, serialization, persistence',
26
+ },
27
+ architecture: {
28
+ dimensions: ['architecture', 'best-practice'],
29
+ descriptionKeywords: 'singleton, delegate, factory, observer, protocol, manager, service, dependency injection, module, MVVM, MVC, coordinator, router, design pattern',
30
+ },
31
+ conventions: {
32
+ dimensions: ['code-standard'],
33
+ descriptionKeywords: 'naming, format, style, import, header, prefix, convention, documentation, file organization, constants, enum, typedef, pragma mark',
34
+ },
35
+ };
36
+
37
+ export class TopicClassifier {
38
+ /**
39
+ * @param {string} projectName - 项目名称
40
+ */
41
+ constructor(projectName = 'Project') {
42
+ this.projectName = projectName;
43
+ }
44
+
45
+ /**
46
+ * 将 patterns 按主题分组
47
+ * @param {Array<Object>} entries - KnowledgeEntry 数组 (kind='pattern')
48
+ * @returns {Object<string, Array<Object>>} { topic: [entries] }
49
+ */
50
+ group(entries) {
51
+ const grouped = {};
52
+ const unmatched = [];
53
+
54
+ for (const entry of entries) {
55
+ const topic = this._classifyEntry(entry);
56
+ if (topic) {
57
+ if (!grouped[topic]) grouped[topic] = [];
58
+ grouped[topic].push(entry);
59
+ } else {
60
+ unmatched.push(entry);
61
+ }
62
+ }
63
+
64
+ // 未匹配的归入 'general' — 但只在有内容时
65
+ if (unmatched.length > 0) {
66
+ grouped.general = unmatched;
67
+ }
68
+
69
+ return grouped;
70
+ }
71
+
72
+ /**
73
+ * 为主题构建 description — Agent 判断关联性的唯一依据
74
+ * @param {string} topic
75
+ * @param {Array<Object>} entries
76
+ * @returns {string}
77
+ */
78
+ buildDescription(topic, entries) {
79
+ const topicDef = TOPIC_MAP[topic];
80
+ const baseKeywords = topicDef
81
+ ? topicDef.descriptionKeywords
82
+ : entries.map(e => (e.title || '')).filter(Boolean).join(', ');
83
+
84
+ // 从 entries 提取额外关键词
85
+ const entryKeywords = entries
86
+ .map(e => this._extractKeywords(e))
87
+ .flat()
88
+ .filter(Boolean);
89
+ const unique = [...new Set(entryKeywords)].slice(0, 10);
90
+ const extra = unique.length > 0 ? `, ${unique.join(', ')}` : '';
91
+
92
+ return `${this._topicLabel(topic)} patterns for ${this.projectName} — ${baseKeywords}${extra}. Use when writing or reviewing ${this._topicLabel(topic).toLowerCase()}-related code.`;
93
+ }
94
+
95
+ /**
96
+ * 分类单个 entry 到主题 — 直读 AI 预计算的 topicHint
97
+ * @private
98
+ */
99
+ _classifyEntry(entry) {
100
+ return entry.topicHint || null; // AI 没给 → null → 归入 general
101
+ }
102
+
103
+ /**
104
+ * 从 entry 提取关键词
105
+ * @private
106
+ */
107
+ _extractKeywords(entry) {
108
+ const text = (entry.title || '') + ' ' + (entry.description || '');
109
+ // 提取英文关键词(3+ 字母)
110
+ const words = text.match(/[a-zA-Z]{3,}/g) || [];
111
+ const filtered = words
112
+ .map(w => w.toLowerCase())
113
+ .filter(w => !STOP_WORDS.has(w));
114
+ return [...new Set(filtered)].slice(0, 5);
115
+ }
116
+
117
+ /**
118
+ * @private
119
+ */
120
+ _topicLabel(topic) {
121
+ const labels = {
122
+ networking: 'Networking',
123
+ ui: 'UI',
124
+ data: 'Data',
125
+ architecture: 'Architecture',
126
+ conventions: 'Conventions',
127
+ general: 'General',
128
+ };
129
+ return labels[topic] || topic.charAt(0).toUpperCase() + topic.slice(1);
130
+ }
131
+ }
132
+
133
+ /** @type {Set<string>} */
134
+ const STOP_WORDS = new Set([
135
+ 'the', 'and', 'for', 'this', 'that', 'with', 'from', 'use', 'using',
136
+ 'when', 'not', 'all', 'are', 'has', 'have', 'been', 'will', 'can',
137
+ 'should', 'must', 'may', 'each', 'which', 'their', 'your', 'its',
138
+ 'project', 'code', 'file', 'class', 'method', 'function', 'bootstrap',
139
+ ]);
140
+
141
+ export default TopicClassifier;
@@ -237,18 +237,24 @@ export class GuardCheckEngine {
237
237
  getRules(language = null) {
238
238
  let rules = [];
239
239
 
240
- // 从数据库加载自定义规则(kind='rule' 的 Recipe,覆盖 code-standard/code-style/best-practice/boundary-constraint)
240
+ // 从数据库加载自定义规则
241
+ // 优先从 knowledge_entries 表查询(V3),回退到 recipes 表(V2)
241
242
  try {
242
243
  const now = Date.now();
243
244
  if (!this._customRulesCache || now - this._cacheTime > this._cacheTTL) {
244
- const rows = this.db.prepare(
245
- `SELECT id, title, description, language, scope, constraints_json
246
- FROM recipes WHERE (kind = 'rule' OR knowledge_type = 'boundary-constraint') AND status = 'active'`
247
- ).all();
245
+ let rows = [];
246
+ try {
247
+ rows = this.db.prepare(
248
+ `SELECT id, title, description, language, scope, constraints
249
+ FROM knowledge_entries
250
+ WHERE (kind = 'rule' OR knowledgeType = 'boundary-constraint')
251
+ AND lifecycle = 'active'`
252
+ ).all();
253
+ } catch { /* table may not exist */ }
248
254
  this._customRulesCache = rows.map(r => {
249
255
  let guards = [];
250
256
  try {
251
- const constraints = JSON.parse(r.constraints_json || '{}');
257
+ const constraints = JSON.parse(r.constraints || '{}');
252
258
  guards = constraints.guards || [];
253
259
  } catch { /* ignore */ }
254
260
  // Each guard entry becomes a rule
@@ -267,7 +273,7 @@ export class GuardCheckEngine {
267
273
  }
268
274
  rules.push(...this._customRulesCache);
269
275
  } catch {
270
- // recipes table or knowledge_type column may not exist
276
+ // table or column may not exist
271
277
  }
272
278
 
273
279
  // 合并内置规则(不覆盖同名数据库规则)
@@ -377,9 +383,16 @@ export class GuardCheckEngine {
377
383
  hitMap.set(v.ruleId, count + 1);
378
384
  }
379
385
 
380
- const updateStmt = this.db.prepare(
381
- `UPDATE recipes SET guard_hit_count = guard_hit_count + ?, updated_at = ? WHERE id = ?`
382
- );
386
+ let updateStmt;
387
+ try {
388
+ updateStmt = this.db.prepare(
389
+ `UPDATE knowledge_entries
390
+ SET stats = json_set(COALESCE(stats, '{}'), '$.guardHits',
391
+ COALESCE(json_extract(stats, '$.guardHits'), 0) + ?),
392
+ updatedAt = ?
393
+ WHERE id = ?`
394
+ );
395
+ } catch { /* table may not exist */ }
383
396
  const now = Math.floor(Date.now() / 1000);
384
397
 
385
398
  for (const [ruleId, count] of hitMap) {
@@ -464,6 +477,9 @@ export class GuardCheckEngine {
464
477
 
465
478
  /**
466
479
  * 批量文件审计
480
+ * @param {Array<{path: string, content: string}>} files
481
+ * @param {object} options - {scope: 'file'|'target'|'project'}
482
+ * @returns {{files, summary, crossFileViolations}}
467
483
  */
468
484
  auditFiles(files, options = {}) {
469
485
  const results = [];
@@ -477,8 +493,14 @@ export class GuardCheckEngine {
477
493
  totalErrors += result.summary.errors;
478
494
  }
479
495
 
496
+ // ── 跨文件检查 ──
497
+ const crossFileViolations = this._runCrossFileChecks(files);
498
+ totalViolations += crossFileViolations.length;
499
+ totalErrors += crossFileViolations.filter(v => v.severity === 'error').length;
500
+
480
501
  return {
481
502
  files: results,
503
+ crossFileViolations,
482
504
  summary: {
483
505
  filesChecked: results.length,
484
506
  totalViolations,
@@ -488,6 +510,73 @@ export class GuardCheckEngine {
488
510
  };
489
511
  }
490
512
 
513
+ /**
514
+ * 跨文件检查 — 需要多文件上下文才能发现的问题
515
+ * @param {Array<{path: string, content: string}>} files
516
+ * @returns {Array<{ruleId, message, severity, locations}>}
517
+ */
518
+ _runCrossFileChecks(files) {
519
+ const violations = [];
520
+
521
+ // ── ObjC Category 跨文件重名检查 ──
522
+ // 收集所有文件中的 @interface ClassName(CategoryName) 声明
523
+ const categoryMap = new Map(); // key: "ClassName(CategoryName)" → [{filePath, line, snippet}]
524
+ const categoryRegex = /@interface\s+(\w+)\s*\(\s*(\w+)\s*\)/g;
525
+
526
+ for (const { path: filePath, content } of files) {
527
+ const ext = filePath.split('.').pop()?.toLowerCase();
528
+ if (ext !== 'm' && ext !== 'mm' && ext !== 'h') continue;
529
+
530
+ const lines = content.split(/\r?\n/);
531
+ for (let i = 0; i < lines.length; i++) {
532
+ categoryRegex.lastIndex = 0;
533
+ let m;
534
+ while ((m = categoryRegex.exec(lines[i])) !== null) {
535
+ const key = `${m[1]}(${m[2]})`;
536
+ if (!categoryMap.has(key)) categoryMap.set(key, []);
537
+ categoryMap.get(key).push({
538
+ filePath,
539
+ line: i + 1,
540
+ snippet: lines[i].trim().slice(0, 120),
541
+ });
542
+ }
543
+ }
544
+ }
545
+
546
+ // .h 和 .m 成对出现是正常的(声明 + 实现),只有同类型文件重名才是问题
547
+ // 或者超过 2 处声明就一定有问题
548
+ for (const [key, locations] of categoryMap) {
549
+ if (locations.length <= 1) continue;
550
+
551
+ // 按文件扩展名分组: .h 和 .m/.mm 各一个是合法的
552
+ const hFiles = locations.filter(l => l.filePath.endsWith('.h'));
553
+ const mFiles = locations.filter(l => !l.filePath.endsWith('.h'));
554
+
555
+ // 同类型文件中有多个声明 → 重名冲突
556
+ const hasDuplicateH = hFiles.length > 1;
557
+ const hasDuplicateM = mFiles.length > 1;
558
+ // 超过 2 处总声明(如 3 个文件都声明了同一个 Category)→ 一定有问题
559
+ const tooMany = locations.length > 2;
560
+
561
+ if (hasDuplicateH || hasDuplicateM || tooMany) {
562
+ // 收集冲突的那些位置
563
+ const conflictLocations = tooMany ? locations
564
+ : hasDuplicateH && hasDuplicateM ? locations
565
+ : hasDuplicateH ? hFiles
566
+ : mFiles;
567
+
568
+ violations.push({
569
+ ruleId: 'objc-cross-file-duplicate-category',
570
+ message: `Category ${key} 在 ${conflictLocations.length} 个文件中重复声明,可能导致方法覆盖或未定义行为`,
571
+ severity: 'warning',
572
+ locations: conflictLocations,
573
+ });
574
+ }
575
+ }
576
+
577
+ return violations;
578
+ }
579
+
491
580
  /**
492
581
  * 清除规则缓存
493
582
  */