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,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;