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,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CursorDeliveryPipeline — 4 通道交付主入口
|
|
3
|
+
*
|
|
4
|
+
* 读取知识库 → 筛选 + 分类 + 排序 + 压缩 → 写入 4 个 Cursor 通道
|
|
5
|
+
*
|
|
6
|
+
* 触发时机:
|
|
7
|
+
* 1. bootstrap 完成后自动触发
|
|
8
|
+
* 2. `asd cursor-rules` CLI 命令手动触发
|
|
9
|
+
* 3. Recipe 状态变更(pending → active)后触发
|
|
10
|
+
* 4. `asd upgrade` 时作为升级步骤执行
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { KnowledgeCompressor } from './KnowledgeCompressor.js';
|
|
14
|
+
import { TopicClassifier } from './TopicClassifier.js';
|
|
15
|
+
import { RulesGenerator } from './RulesGenerator.js';
|
|
16
|
+
import { SkillsSyncer } from './SkillsSyncer.js';
|
|
17
|
+
import { estimateTokens, BUDGET } from './TokenBudget.js';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
|
|
20
|
+
export class CursorDeliveryPipeline {
|
|
21
|
+
/**
|
|
22
|
+
* @param {Object} options
|
|
23
|
+
* @param {Object} options.knowledgeService - KnowledgeService 实例
|
|
24
|
+
* @param {string} options.projectRoot - 用户项目根目录
|
|
25
|
+
* @param {string} [options.projectName] - 项目名称
|
|
26
|
+
* @param {Object} [options.logger] - 日志器
|
|
27
|
+
*/
|
|
28
|
+
constructor({ knowledgeService, projectRoot, projectName, logger }) {
|
|
29
|
+
this.knowledgeService = knowledgeService;
|
|
30
|
+
this.projectRoot = projectRoot;
|
|
31
|
+
this.projectName = projectName || this._inferProjectName(projectRoot);
|
|
32
|
+
this.logger = logger || console;
|
|
33
|
+
|
|
34
|
+
// 子模块
|
|
35
|
+
this.compressor = new KnowledgeCompressor();
|
|
36
|
+
this.topicClassifier = new TopicClassifier(this.projectName);
|
|
37
|
+
this.rulesGenerator = new RulesGenerator(projectRoot, this.projectName);
|
|
38
|
+
this.skillsSyncer = new SkillsSyncer(projectRoot, this.projectName, knowledgeService);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 完整交付流程 — 生成 4 通道 Cursor 消费物料
|
|
43
|
+
* @returns {Promise<{ channelA: Object, channelB: Object, channelC: Object, stats: Object }>}
|
|
44
|
+
*/
|
|
45
|
+
async deliver() {
|
|
46
|
+
const startTime = Date.now();
|
|
47
|
+
const stats = {
|
|
48
|
+
channelA: { rulesCount: 0, tokensUsed: 0 },
|
|
49
|
+
channelB: { topicCount: 0, patternsCount: 0, totalTokens: 0 },
|
|
50
|
+
channelC: { synced: 0, skipped: 0, errors: 0 },
|
|
51
|
+
totalTokensUsed: 0,
|
|
52
|
+
duration: 0,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
// 1. 加载所有 active + pending 知识
|
|
57
|
+
const entries = await this._loadEntries();
|
|
58
|
+
this.logger.info?.(`[CursorDelivery] Loaded ${entries.length} knowledge entries`);
|
|
59
|
+
|
|
60
|
+
// 2. 分类:rules vs patterns vs facts
|
|
61
|
+
const { rules, patterns } = this._classify(entries);
|
|
62
|
+
this.logger.info?.(`[CursorDelivery] Classified: ${rules.length} rules, ${patterns.length} patterns`);
|
|
63
|
+
|
|
64
|
+
// 3. 清理旧的动态生成文件
|
|
65
|
+
this.rulesGenerator.cleanDynamicFiles();
|
|
66
|
+
|
|
67
|
+
// ── Channel A: Always-On Rules ──
|
|
68
|
+
const channelA = this._generateChannelA(rules);
|
|
69
|
+
stats.channelA = channelA;
|
|
70
|
+
|
|
71
|
+
// ── Channel B: Smart Rules (by topic) ──
|
|
72
|
+
const channelB = this._generateChannelB(patterns);
|
|
73
|
+
stats.channelB = channelB;
|
|
74
|
+
|
|
75
|
+
// ── Channel C: Skills Sync ──
|
|
76
|
+
const channelC = await this._generateChannelC();
|
|
77
|
+
stats.channelC = channelC;
|
|
78
|
+
|
|
79
|
+
// 统计
|
|
80
|
+
stats.totalTokensUsed = channelA.tokensUsed + channelB.totalTokens;
|
|
81
|
+
stats.duration = Date.now() - startTime;
|
|
82
|
+
|
|
83
|
+
this.logger.info?.(`[CursorDelivery] Done in ${stats.duration}ms — ` +
|
|
84
|
+
`A: ${channelA.rulesCount} rules (${channelA.tokensUsed} tokens), ` +
|
|
85
|
+
`B: ${channelB.topicCount} topics (${channelB.totalTokens} tokens), ` +
|
|
86
|
+
`C: ${channelC.synced} skills synced`);
|
|
87
|
+
|
|
88
|
+
return { channelA, channelB, channelC, stats };
|
|
89
|
+
} catch (error) {
|
|
90
|
+
this.logger.error?.(`[CursorDelivery] Error: ${error.message}`);
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ─── 内部方法 ───────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 加载知识条目(active + high-confidence pending)
|
|
99
|
+
* @private
|
|
100
|
+
*/
|
|
101
|
+
async _loadEntries() {
|
|
102
|
+
const allEntries = [];
|
|
103
|
+
|
|
104
|
+
// 加载 active
|
|
105
|
+
try {
|
|
106
|
+
const active = await this.knowledgeService.list(
|
|
107
|
+
{ lifecycle: 'active' },
|
|
108
|
+
{ page: 1, pageSize: 200 }
|
|
109
|
+
);
|
|
110
|
+
const activeItems = this._extractItems(active);
|
|
111
|
+
allEntries.push(...activeItems);
|
|
112
|
+
} catch (e) {
|
|
113
|
+
this.logger.warn?.(`[CursorDelivery] Failed to load active entries: ${e.message}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 加载 pending(高置信度的也纳入)
|
|
117
|
+
try {
|
|
118
|
+
const pending = await this.knowledgeService.list(
|
|
119
|
+
{ lifecycle: 'pending' },
|
|
120
|
+
{ page: 1, pageSize: 200 }
|
|
121
|
+
);
|
|
122
|
+
const pendingItems = this._extractItems(pending);
|
|
123
|
+
// 过滤高置信度 pending(quality.confidence >= 0.7 或无 quality 字段)
|
|
124
|
+
const highConfPending = pendingItems.filter(e => {
|
|
125
|
+
const conf = e.quality?.confidence;
|
|
126
|
+
return conf === undefined || conf === null || conf >= 0.7;
|
|
127
|
+
});
|
|
128
|
+
allEntries.push(...highConfPending);
|
|
129
|
+
} catch (e) {
|
|
130
|
+
this.logger.warn?.(`[CursorDelivery] Failed to load pending entries: ${e.message}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return allEntries;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* 从 KnowledgeService.list() 返回值提取条目数组
|
|
138
|
+
* @private
|
|
139
|
+
*/
|
|
140
|
+
_extractItems(result) {
|
|
141
|
+
if (Array.isArray(result)) return result;
|
|
142
|
+
if (result?.items) return result.items;
|
|
143
|
+
if (result?.data) return result.data;
|
|
144
|
+
return [];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* 按 kind 分类知识条目
|
|
149
|
+
* @private
|
|
150
|
+
*/
|
|
151
|
+
_classify(entries) {
|
|
152
|
+
const rules = [], patterns = [], facts = [];
|
|
153
|
+
for (const entry of entries) {
|
|
154
|
+
if (entry.kind === 'rule') rules.push(entry);
|
|
155
|
+
else if (entry.kind === 'fact') facts.push(entry);
|
|
156
|
+
else patterns.push(entry); // 无 kind 或 kind='pattern' → pattern
|
|
157
|
+
}
|
|
158
|
+
return { rules, patterns, facts };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* 排序 — 质量分 + 统计使用量
|
|
163
|
+
* @private
|
|
164
|
+
*/
|
|
165
|
+
_rank(entries) {
|
|
166
|
+
return [...entries].sort((a, b) => {
|
|
167
|
+
const scoreA = this._rankScore(a);
|
|
168
|
+
const scoreB = this._rankScore(b);
|
|
169
|
+
return scoreB - scoreA;
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* 计算排名分
|
|
175
|
+
* @private
|
|
176
|
+
*/
|
|
177
|
+
_rankScore(entry) {
|
|
178
|
+
let score = 0;
|
|
179
|
+
score += (entry.quality?.confidence || 0.5) * 50;
|
|
180
|
+
score += (entry.quality?.authorityScore || 0) * 30;
|
|
181
|
+
score += Math.min(entry.stats?.useCount || 0, 10) * 2;
|
|
182
|
+
if (entry.lifecycle === 'active') score += 10;
|
|
183
|
+
return score;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Channel A 生成
|
|
188
|
+
* @private
|
|
189
|
+
*/
|
|
190
|
+
_generateChannelA(rules) {
|
|
191
|
+
const topRules = this._rank(rules).slice(0, BUDGET.CHANNEL_A_MAX_RULES);
|
|
192
|
+
const ruleLines = this.compressor.compressToRuleLine(topRules);
|
|
193
|
+
|
|
194
|
+
if (ruleLines.length === 0) {
|
|
195
|
+
this.logger.info?.('[CursorDelivery] Channel A: No rules to generate');
|
|
196
|
+
return { rulesCount: 0, tokensUsed: 0, filePath: null };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const result = this.rulesGenerator.writeAlwaysOnRules(ruleLines);
|
|
200
|
+
this.logger.info?.(`[CursorDelivery] Channel A: ${result.rulesCount} rules → ${result.filePath}`);
|
|
201
|
+
return result;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Channel B 生成
|
|
206
|
+
* @private
|
|
207
|
+
*/
|
|
208
|
+
_generateChannelB(patterns) {
|
|
209
|
+
const result = { topicCount: 0, patternsCount: 0, totalTokens: 0, topics: {} };
|
|
210
|
+
|
|
211
|
+
if (patterns.length === 0) {
|
|
212
|
+
this.logger.info?.('[CursorDelivery] Channel B: No patterns to generate');
|
|
213
|
+
return result;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// 按主题分组
|
|
217
|
+
const grouped = this.topicClassifier.group(patterns);
|
|
218
|
+
|
|
219
|
+
for (const [topic, topicPatterns] of Object.entries(grouped)) {
|
|
220
|
+
// 排序并取 Top N
|
|
221
|
+
const top = this._rank(topicPatterns).slice(0, BUDGET.CHANNEL_B_MAX_PATTERNS);
|
|
222
|
+
|
|
223
|
+
// 压缩为 When/Do/Don't
|
|
224
|
+
const compressed = this.compressor.compressToWhenDoDont(top);
|
|
225
|
+
if (compressed.length === 0) continue;
|
|
226
|
+
|
|
227
|
+
// 格式化为 Markdown
|
|
228
|
+
const body = this.compressor.formatWhenDoDont(compressed);
|
|
229
|
+
|
|
230
|
+
// 构建 description
|
|
231
|
+
const description = this.topicClassifier.buildDescription(topic, topicPatterns);
|
|
232
|
+
|
|
233
|
+
// 写入 .mdc
|
|
234
|
+
const writeResult = this.rulesGenerator.writeSmartRules(topic, body, description);
|
|
235
|
+
|
|
236
|
+
result.topicCount++;
|
|
237
|
+
result.patternsCount += compressed.length;
|
|
238
|
+
result.totalTokens += writeResult.tokensUsed;
|
|
239
|
+
result.topics[topic] = { patternsCount: compressed.length, tokensUsed: writeResult.tokensUsed };
|
|
240
|
+
|
|
241
|
+
this.logger.info?.(`[CursorDelivery] Channel B: ${topic} — ${compressed.length} patterns → ${writeResult.filePath}`);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return result;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Channel C 生成
|
|
249
|
+
* @private
|
|
250
|
+
*/
|
|
251
|
+
async _generateChannelC() {
|
|
252
|
+
try {
|
|
253
|
+
const syncResult = await this.skillsSyncer.sync();
|
|
254
|
+
this.logger.info?.(
|
|
255
|
+
`[CursorDelivery] Channel C: ${syncResult.synced.length} synced, ` +
|
|
256
|
+
`${syncResult.skipped.length} skipped, ${syncResult.errors.length} errors`
|
|
257
|
+
);
|
|
258
|
+
return {
|
|
259
|
+
synced: syncResult.synced.length,
|
|
260
|
+
skipped: syncResult.skipped.length,
|
|
261
|
+
errors: syncResult.errors.length,
|
|
262
|
+
details: syncResult,
|
|
263
|
+
};
|
|
264
|
+
} catch (err) {
|
|
265
|
+
this.logger.error?.(`[CursorDelivery] Channel C error: ${err.message}`);
|
|
266
|
+
return { synced: 0, skipped: 0, errors: 1, details: { synced: [], skipped: [], errors: [err.message] } };
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* 从项目路径推断项目名称
|
|
272
|
+
* @private
|
|
273
|
+
*/
|
|
274
|
+
_inferProjectName(projectRoot) {
|
|
275
|
+
return path.basename(projectRoot);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export default CursorDeliveryPipeline;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KnowledgeCompressor — 知识条目压缩器(v2 无降级版)
|
|
3
|
+
*
|
|
4
|
+
* 将 KnowledgeEntry(含 AI 预计算字段)格式化为 Cursor 交付格式:
|
|
5
|
+
* - Channel A: compressToRuleLine() → 一行式强制规则
|
|
6
|
+
* - Channel B: compressToWhenDoDont() → When/Do/Don't + Template 格式
|
|
7
|
+
*
|
|
8
|
+
* 原则:只做格式化,无字段 = 不输出,不做启发式猜测。
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export class KnowledgeCompressor {
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Channel A — 一行式规则
|
|
15
|
+
* @param {Array<Object>} entries - KnowledgeEntry 数组 (kind='rule')
|
|
16
|
+
* @returns {Array<string>}
|
|
17
|
+
*/
|
|
18
|
+
compressToRuleLine(entries) {
|
|
19
|
+
return entries
|
|
20
|
+
.filter(e => e.doClause) // 无 doClause → 跳过,不猜
|
|
21
|
+
.map(e => {
|
|
22
|
+
let line = e.doClause;
|
|
23
|
+
if (e.dontClause) {
|
|
24
|
+
// AI 可能返回 "Don't ..." / "Do not ..." 开头,去掉冗余前缀
|
|
25
|
+
const stripped = e.dontClause.replace(/^(Don't|Do not|Never)\s+/i, '');
|
|
26
|
+
line += `. Do NOT ${stripped}`;
|
|
27
|
+
}
|
|
28
|
+
return `- ${line}.`;
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Channel B — When/Do/Don't + Template
|
|
34
|
+
* @param {Array<Object>} entries - KnowledgeEntry 数组 (kind='pattern')
|
|
35
|
+
* @returns {Array<{ trigger: string, when: string, do: string, dont: string, template: string }>}
|
|
36
|
+
*/
|
|
37
|
+
compressToWhenDoDont(entries) {
|
|
38
|
+
const seen = new Set();
|
|
39
|
+
return entries
|
|
40
|
+
.filter(e => e.trigger && e.whenClause && e.doClause) // 缺任一 → 跳过
|
|
41
|
+
.map(e => {
|
|
42
|
+
let trigger = e.trigger.startsWith('@') ? e.trigger : `@${e.trigger}`;
|
|
43
|
+
// trigger 去重(AI 应保证唯一,但防御性检查)
|
|
44
|
+
if (seen.has(trigger)) {
|
|
45
|
+
let i = 2;
|
|
46
|
+
while (seen.has(`${trigger}-${i}`)) i++;
|
|
47
|
+
trigger = `${trigger}-${i}`;
|
|
48
|
+
}
|
|
49
|
+
seen.add(trigger);
|
|
50
|
+
return {
|
|
51
|
+
trigger,
|
|
52
|
+
when: e.whenClause,
|
|
53
|
+
do: e.doClause,
|
|
54
|
+
dont: e.dontClause || '',
|
|
55
|
+
template: e.coreCode || '',
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 将 When/Do/Don't 结果格式化为 Markdown 字符串
|
|
62
|
+
* @param {Array<Object>} compressed - compressToWhenDoDont 输出
|
|
63
|
+
* @param {string} [language=''] - 代码围栏语言标识
|
|
64
|
+
* @returns {string}
|
|
65
|
+
*/
|
|
66
|
+
formatWhenDoDont(compressed, language = '') {
|
|
67
|
+
const lang = language || '';
|
|
68
|
+
return compressed.map(item => {
|
|
69
|
+
const lines = [`### ${item.trigger}`];
|
|
70
|
+
lines.push(`- **When**: ${item.when}`);
|
|
71
|
+
lines.push(`- **Do**: ${item.do}`);
|
|
72
|
+
if (item.dont) {
|
|
73
|
+
const stripped = item.dont.replace(/^(Don't|Do not|Never)\s+/i, '');
|
|
74
|
+
lines.push(`- **Don't**: ${stripped}`);
|
|
75
|
+
}
|
|
76
|
+
if (item.template) {
|
|
77
|
+
lines.push('');
|
|
78
|
+
lines.push(`\`\`\`${lang}`);
|
|
79
|
+
lines.push(item.template);
|
|
80
|
+
lines.push('```');
|
|
81
|
+
}
|
|
82
|
+
return lines.join('\n');
|
|
83
|
+
}).join('\n\n');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export default KnowledgeCompressor;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RulesGenerator — .mdc 文件生成器
|
|
3
|
+
*
|
|
4
|
+
* 生成 Cursor Rules 格式的 .mdc 文件到 .cursor/rules/ 目录:
|
|
5
|
+
* - Channel A: autosnippet-project-rules.mdc (alwaysApply: true)
|
|
6
|
+
* - Channel B: autosnippet-patterns-{topic}.mdc (alwaysApply: false)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { estimateTokens, BUDGET } from './TokenBudget.js';
|
|
12
|
+
|
|
13
|
+
export class RulesGenerator {
|
|
14
|
+
/**
|
|
15
|
+
* @param {string} projectRoot - 用户项目根目录
|
|
16
|
+
* @param {string} projectName - 项目名称(用于 description/标题)
|
|
17
|
+
*/
|
|
18
|
+
constructor(projectRoot, projectName = 'Project') {
|
|
19
|
+
this.projectRoot = projectRoot;
|
|
20
|
+
this.projectName = projectName;
|
|
21
|
+
this.rulesDir = path.join(projectRoot, '.cursor', 'rules');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Channel A — 写入 Always-On Rules 文件
|
|
26
|
+
*
|
|
27
|
+
* @param {string[]} ruleLines - 一行式规则列表 (来自 KnowledgeCompressor.compressToRuleLine)
|
|
28
|
+
* @returns {{ filePath: string, tokensUsed: number, rulesCount: number }}
|
|
29
|
+
*/
|
|
30
|
+
writeAlwaysOnRules(ruleLines) {
|
|
31
|
+
this._ensureDir();
|
|
32
|
+
|
|
33
|
+
// Token 预算控制
|
|
34
|
+
const kept = [];
|
|
35
|
+
let tokens = 0;
|
|
36
|
+
const headerFooterBudget = 100;
|
|
37
|
+
const ruleBudget = BUDGET.CHANNEL_A_MAX - headerFooterBudget;
|
|
38
|
+
|
|
39
|
+
for (const line of ruleLines) {
|
|
40
|
+
const lineTokens = estimateTokens(line);
|
|
41
|
+
if (tokens + lineTokens <= ruleBudget && kept.length < BUDGET.CHANNEL_A_MAX_RULES) {
|
|
42
|
+
kept.push(line);
|
|
43
|
+
tokens += lineTokens;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const content = this._renderChannelA(kept);
|
|
48
|
+
const filePath = path.join(this.rulesDir, 'autosnippet-project-rules.mdc');
|
|
49
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
filePath,
|
|
53
|
+
tokensUsed: estimateTokens(content),
|
|
54
|
+
rulesCount: kept.length,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Channel B — 写入 Smart Rules 文件(按主题)
|
|
60
|
+
*
|
|
61
|
+
* @param {string} topic - 主题名 (networking, ui, data, architecture, conventions, general)
|
|
62
|
+
* @param {string} compressedContent - 格式化后的 When/Do/Don't Markdown 内容
|
|
63
|
+
* @param {string} description - Agent 关联性判断用 description
|
|
64
|
+
* @returns {{ filePath: string, tokensUsed: number }}
|
|
65
|
+
*/
|
|
66
|
+
writeSmartRules(topic, compressedContent, description) {
|
|
67
|
+
this._ensureDir();
|
|
68
|
+
|
|
69
|
+
// Token 预算控制
|
|
70
|
+
let body = compressedContent;
|
|
71
|
+
const totalTokens = estimateTokens(body) + estimateTokens(description) + 50;
|
|
72
|
+
if (totalTokens > BUDGET.CHANNEL_B_MAX_PER_FILE) {
|
|
73
|
+
// 截断尾部
|
|
74
|
+
const lines = body.split('\n');
|
|
75
|
+
const truncated = [];
|
|
76
|
+
let used = estimateTokens(description) + 50;
|
|
77
|
+
for (const line of lines) {
|
|
78
|
+
used += estimateTokens(line + '\n');
|
|
79
|
+
if (used <= BUDGET.CHANNEL_B_MAX_PER_FILE) {
|
|
80
|
+
truncated.push(line);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
body = truncated.join('\n');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const content = this._renderChannelB(topic, body, description);
|
|
87
|
+
const fileName = `autosnippet-patterns-${topic}.mdc`;
|
|
88
|
+
const filePath = path.join(this.rulesDir, fileName);
|
|
89
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
filePath,
|
|
93
|
+
tokensUsed: estimateTokens(content),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 清理旧的动态生成文件
|
|
99
|
+
* 保留静态模板文件(autosnippet-conventions.mdc, autosnippet-skills.mdc)
|
|
100
|
+
*/
|
|
101
|
+
cleanDynamicFiles() {
|
|
102
|
+
if (!fs.existsSync(this.rulesDir)) return;
|
|
103
|
+
|
|
104
|
+
const dynamicPrefixes = ['autosnippet-project-rules', 'autosnippet-patterns-'];
|
|
105
|
+
const files = fs.readdirSync(this.rulesDir);
|
|
106
|
+
for (const file of files) {
|
|
107
|
+
if (dynamicPrefixes.some(p => file.startsWith(p))) {
|
|
108
|
+
const filePath = path.join(this.rulesDir, file);
|
|
109
|
+
try { fs.unlinkSync(filePath); } catch { /* ignore */ }
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ─── 渲染方法 ───────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* @private
|
|
118
|
+
*/
|
|
119
|
+
_renderChannelA(ruleLines) {
|
|
120
|
+
const desc = `${this.projectName} mandatory rules — coding constraints that must never be violated. Auto-generated by AutoSnippet.`;
|
|
121
|
+
const lines = [
|
|
122
|
+
'---',
|
|
123
|
+
`description: "${desc}"`,
|
|
124
|
+
'alwaysApply: true',
|
|
125
|
+
'---',
|
|
126
|
+
'',
|
|
127
|
+
`# ${this.projectName} — Mandatory Rules`,
|
|
128
|
+
'',
|
|
129
|
+
...ruleLines,
|
|
130
|
+
'',
|
|
131
|
+
'For detailed patterns and recipes, AutoSnippet MCP tools are available:',
|
|
132
|
+
'- `autosnippet_search(query)` — search knowledge base',
|
|
133
|
+
'- `autosnippet_context_search(query)` — context-aware search with history',
|
|
134
|
+
];
|
|
135
|
+
return lines.join('\n') + '\n';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* @private
|
|
140
|
+
*/
|
|
141
|
+
_renderChannelB(topic, body, description) {
|
|
142
|
+
const topicLabel = topic.charAt(0).toUpperCase() + topic.slice(1);
|
|
143
|
+
const lines = [
|
|
144
|
+
'---',
|
|
145
|
+
`description: "${description}"`,
|
|
146
|
+
'alwaysApply: false',
|
|
147
|
+
'---',
|
|
148
|
+
'',
|
|
149
|
+
`# ${topicLabel} Patterns`,
|
|
150
|
+
'',
|
|
151
|
+
body,
|
|
152
|
+
'',
|
|
153
|
+
`For full code examples: \`autosnippet_search("${topic}")\``,
|
|
154
|
+
];
|
|
155
|
+
return lines.join('\n') + '\n';
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* @private
|
|
160
|
+
*/
|
|
161
|
+
_ensureDir() {
|
|
162
|
+
if (!fs.existsSync(this.rulesDir)) {
|
|
163
|
+
fs.mkdirSync(this.rulesDir, { recursive: true });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export default RulesGenerator;
|