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.
- package/README.md +12 -12
- package/bin/cli.js +53 -40
- package/config/constitution.yaml +9 -2
- package/dashboard/dist/assets/{icons-CH-H9x0E.js → icons-D4IWpDIk.js} +105 -100
- package/dashboard/dist/assets/index-CWBNcF9z.css +1 -0
- package/dashboard/dist/assets/index-DHtzhbuG.js +120 -0
- package/dashboard/dist/index.html +3 -3
- package/lib/cli/AiScanService.js +35 -36
- package/lib/cli/KnowledgeSyncService.js +345 -0
- package/lib/cli/SetupService.js +8 -26
- package/lib/cli/UpgradeService.js +28 -0
- package/lib/core/gateway/GatewayActionRegistry.js +48 -58
- package/lib/domain/index.js +16 -11
- package/lib/domain/knowledge/KnowledgeEntry.js +289 -0
- package/lib/domain/knowledge/KnowledgeRepository.js +123 -0
- package/lib/domain/knowledge/Lifecycle.js +99 -0
- package/lib/domain/knowledge/index.js +27 -0
- package/lib/domain/knowledge/values/Constraints.js +128 -0
- package/lib/domain/knowledge/values/Content.js +69 -0
- package/lib/domain/knowledge/values/Quality.js +81 -0
- package/lib/domain/knowledge/values/Reasoning.js +70 -0
- package/lib/domain/knowledge/values/Relations.js +142 -0
- package/lib/domain/knowledge/values/Stats.js +72 -0
- package/lib/domain/knowledge/values/index.js +9 -0
- package/lib/external/ai/AiProvider.js +85 -11
- package/lib/external/mcp/McpServer.js +7 -5
- package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +18 -2
- package/lib/external/mcp/handlers/bootstrap.js +116 -11
- package/lib/external/mcp/handlers/browse.js +76 -73
- package/lib/external/mcp/handlers/candidate.js +26 -275
- package/lib/external/mcp/handlers/guard.js +2 -0
- package/lib/external/mcp/handlers/knowledge.js +267 -0
- package/lib/external/mcp/handlers/structure.js +25 -23
- package/lib/external/mcp/handlers/system.js +10 -12
- package/lib/external/mcp/tools.js +134 -140
- package/lib/http/HttpServer.js +14 -8
- package/lib/http/routes/ai.js +4 -3
- package/lib/http/routes/extract.js +48 -4
- package/lib/http/routes/knowledge.js +246 -0
- package/lib/http/routes/search.js +12 -17
- package/lib/infrastructure/database/migrations/016_unified_knowledge_entries.js +395 -0
- package/lib/infrastructure/database/migrations/017_camelcase_knowledge_entries.js +107 -0
- package/lib/infrastructure/external/XcodeAutomation.js +187 -103
- package/lib/injection/ServiceContainer.js +69 -60
- package/lib/repository/knowledge/KnowledgeRepository.impl.js +338 -0
- package/lib/service/automation/DirectiveDetector.js +2 -3
- package/lib/service/automation/FileWatcher.js +59 -28
- package/lib/service/automation/XcodeIntegration.js +931 -156
- package/lib/service/automation/handlers/AlinkHandler.js +5 -4
- package/lib/service/automation/handlers/CreateHandler.js +53 -19
- package/lib/service/automation/handlers/DraftHandler.js +1 -1
- package/lib/service/automation/handlers/GuardHandler.js +183 -20
- package/lib/service/automation/handlers/SearchHandler.js +25 -22
- package/lib/service/candidate/SimilarityService.js +2 -2
- package/lib/service/chat/AnalystAgent.js +9 -0
- package/lib/service/chat/CandidateGuardrail.js +22 -11
- package/lib/service/chat/ChatAgent.js +132 -54
- package/lib/service/chat/ContextWindow.js +5 -5
- package/lib/service/chat/HandoffProtocol.js +1 -0
- package/lib/service/chat/ProducerAgent.js +40 -13
- package/lib/service/chat/ReasoningLayer.js +854 -0
- package/lib/service/chat/ReasoningTrace.js +329 -0
- package/lib/service/chat/tools.js +308 -205
- package/lib/service/cursor/CursorDeliveryPipeline.js +279 -0
- package/lib/service/cursor/KnowledgeCompressor.js +87 -0
- package/lib/service/cursor/RulesGenerator.js +168 -0
- package/lib/service/cursor/SkillsSyncer.js +268 -0
- package/lib/service/cursor/TokenBudget.js +58 -0
- package/lib/service/cursor/TopicClassifier.js +141 -0
- package/lib/service/guard/GuardCheckEngine.js +99 -10
- package/lib/service/guard/GuardService.js +57 -46
- package/lib/service/knowledge/ConfidenceRouter.js +159 -0
- package/lib/service/knowledge/KnowledgeFileWriter.js +595 -0
- package/lib/service/knowledge/KnowledgeService.js +802 -0
- package/lib/service/recipe/RecipeParser.js +3 -12
- package/lib/service/search/SearchEngine.js +67 -22
- package/lib/service/skills/SignalCollector.js +14 -9
- package/lib/service/skills/SkillAdvisor.js +13 -11
- package/lib/service/snippet/SnippetFactory.js +5 -5
- package/lib/service/spm/SpmService.js +15 -48
- package/lib/shared/RecipeReadinessChecker.js +6 -11
- package/package.json +1 -1
- package/scripts/install-cursor-skill.js +0 -6
- package/scripts/migrate-md-to-knowledge.mjs +364 -0
- package/skills/autosnippet-analysis/SKILL.md +15 -7
- package/skills/autosnippet-candidates/SKILL.md +8 -8
- package/skills/autosnippet-coldstart/SKILL.md +8 -4
- package/skills/autosnippet-concepts/SKILL.md +7 -6
- package/skills/autosnippet-create/SKILL.md +13 -13
- package/skills/autosnippet-intent/SKILL.md +3 -2
- package/skills/autosnippet-lifecycle/SKILL.md +5 -5
- package/skills/autosnippet-recipes/SKILL.md +18 -6
- package/templates/constitution.yaml +1 -1
- package/templates/copilot-instructions.md +6 -6
- package/templates/recipes-setup/README.md +3 -3
- package/dashboard/dist/assets/index-CqJRvYRL.js +0 -197
- package/dashboard/dist/assets/index-DICm9PNa.css +0 -1
- package/lib/cli/CandidateSyncService.js +0 -261
- package/lib/cli/SyncService.js +0 -356
- package/lib/domain/candidate/Candidate.js +0 -196
- package/lib/domain/candidate/CandidateRepository.js +0 -107
- package/lib/domain/candidate/Reasoning.js +0 -52
- package/lib/domain/recipe/Recipe.js +0 -421
- package/lib/domain/recipe/RecipeRepository.js +0 -54
- package/lib/domain/types/CandidateStatus.js +0 -52
- package/lib/http/routes/candidates.js +0 -559
- package/lib/http/routes/recipes.js +0 -397
- package/lib/repository/candidate/CandidateRepository.impl.js +0 -230
- package/lib/repository/recipe/RecipeRepository.impl.js +0 -498
- package/lib/service/candidate/CandidateAggregator.js +0 -52
- package/lib/service/candidate/CandidateFileWriter.js +0 -383
- package/lib/service/candidate/CandidateService.js +0 -1001
- package/lib/service/recipe/RecipeFileWriter.js +0 -514
- package/lib/service/recipe/RecipeService.js +0 -786
- 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
|
-
//
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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.
|
|
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
|
-
//
|
|
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
|
-
|
|
381
|
-
|
|
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
|
*/
|