autosnippet
Version:
Extract code patterns into a knowledge base for AI coding assistants
196 lines (195 loc) • 7.71 kB
JavaScript
/**
* RulesGenerator — .mdc 文件生成器
*
* 生成 Cursor Rules 格式的 .mdc 文件到 .cursor/rules/ 目录:
* - Channel A: autosnippet-project-rules.mdc (alwaysApply: true)
* - Channel B: autosnippet-patterns-{topic}.mdc (alwaysApply: false)
*/
import fs from 'node:fs';
import path from 'node:path';
import { BUDGET, estimateTokens } from './TokenBudget.js';
export class RulesGenerator {
projectName;
projectRoot;
rulesDir;
/**
* @param projectRoot 用户项目根目录
* @param projectName 项目名称(用于 description/标题)
*/
constructor(projectRoot, projectName = 'Project') {
this.projectRoot = projectRoot;
this.projectName = projectName;
this.rulesDir = path.join(projectRoot, '.cursor', 'rules');
}
/**
* Channel A — 写入 Always-On Rules 文件
*
* @param ruleLines 一行式规则列表 (来自 KnowledgeCompressor.compressToRuleLine)
* @returns }
*/
writeAlwaysOnRules(ruleLines) {
this._ensureDir();
// Token 预算控制
const kept = [];
let tokens = 0;
const headerFooterBudget = 100;
const ruleBudget = BUDGET.CHANNEL_A_MAX - headerFooterBudget;
for (const line of ruleLines) {
const lineTokens = estimateTokens(line);
if (tokens + lineTokens <= ruleBudget && kept.length < BUDGET.CHANNEL_A_MAX_RULES) {
kept.push(line);
tokens += lineTokens;
}
}
const content = this._renderChannelA(kept);
const filePath = path.join(this.rulesDir, 'autosnippet-project-rules.mdc');
fs.writeFileSync(filePath, content, 'utf8');
return {
filePath,
tokensUsed: estimateTokens(content),
rulesCount: kept.length,
};
}
/**
* Channel B — 写入 Smart Rules 文件(按主题)
*
* @param topic 主题名 (networking, ui, data, architecture, conventions, general)
* @param compressedContent 格式化后的 When/Do/Don't Markdown 内容
* @param description Agent 关联性判断用 description
* @returns }
*/
writeSmartRules(topic, compressedContent, description) {
this._ensureDir();
// Token 预算控制
let body = compressedContent;
const totalTokens = estimateTokens(body) + estimateTokens(description) + 50;
if (totalTokens > BUDGET.CHANNEL_B_MAX_PER_FILE) {
// 截断尾部
const lines = body.split('\n');
const truncated = [];
let used = estimateTokens(description) + 50;
for (const line of lines) {
used += estimateTokens(`${line}\n`);
if (used <= BUDGET.CHANNEL_B_MAX_PER_FILE) {
truncated.push(line);
}
}
body = truncated.join('\n');
}
const content = this._renderChannelB(topic, body, description);
const fileName = `autosnippet-patterns-${topic}.mdc`;
const filePath = path.join(this.rulesDir, fileName);
fs.writeFileSync(filePath, content, 'utf8');
return {
filePath,
tokensUsed: estimateTokens(content),
};
}
/**
* 清理旧的动态生成文件
* 保留静态模板文件(autosnippet-conventions.mdc, autosnippet-skills.mdc)
*/
cleanDynamicFiles() {
if (!fs.existsSync(this.rulesDir)) {
return;
}
const dynamicPrefixes = ['autosnippet-project-rules', 'autosnippet-patterns-'];
const files = fs.readdirSync(this.rulesDir);
for (const file of files) {
if (dynamicPrefixes.some((p) => file.startsWith(p))) {
const filePath = path.join(this.rulesDir, file);
try {
fs.unlinkSync(filePath);
}
catch {
/* ignore */
}
}
}
}
// ─── 渲染方法 ───────────────────────────────────────
_renderChannelA(ruleLines) {
const desc = `${this.projectName} mandatory rules — coding constraints that must never be violated. Auto-generated by AutoSnippet.`;
const lines = [
'---',
`description: "${desc}"`,
'alwaysApply: true',
'---',
'',
`# ${this.projectName} — Mandatory Rules`,
'',
...ruleLines,
'',
'For detailed patterns and recipes, AutoSnippet MCP tools are available:',
'- `autosnippet_search({ query })` — search knowledge base (auto mode: BM25 + semantic)',
'- `autosnippet_search({ query, mode: "context" })` — context-aware search with history',
];
return `${lines.join('\n')}\n`;
}
_renderChannelB(topic, body, description) {
const topicLabel = topic.charAt(0).toUpperCase() + topic.slice(1);
const lines = [
'---',
`description: "${description}"`,
'alwaysApply: false',
'---',
'',
`# ${topicLabel} Patterns`,
'',
body,
'',
`For full code examples: \`autosnippet_search("${topic}")\``,
];
return `${lines.join('\n')}\n`;
}
/**
* Baseline Rules — 零知识库时写入基础引导文件
* 告知 Agent 可用的 MCP 工具和推荐工作流
*/
writeBaselineRules() {
this._ensureDir();
const content = this._renderBaseline();
const filePath = path.join(this.rulesDir, 'autosnippet-project-rules.mdc');
fs.writeFileSync(filePath, content, 'utf8');
return {
filePath,
tokensUsed: estimateTokens(content),
rulesCount: 0,
};
}
_renderBaseline() {
const lines = [
'---',
`description: "${this.projectName} — AutoSnippet baseline guidance. Available MCP tools and recommended workflows."`,
'alwaysApply: true',
'---',
'',
`# ${this.projectName} — AutoSnippet Baseline`,
'',
'This project has AutoSnippet enabled but no knowledge entries yet.',
'Use the following MCP tools to build and query the knowledge base:',
'',
'## Available MCP Tools',
'',
'- `autosnippet_bootstrap` — Cold-start: analyze the project and generate initial knowledge entries',
'- `autosnippet_search({ query })` — Search knowledge base (BM25 + semantic)',
'- `autosnippet_submit_knowledge` — Submit a knowledge candidate (strict validation)',
'- `autosnippet_guard` — Run compliance review on current changes',
'- `autosnippet_task` — Task & decision management (prime/create/claim/close/record_decision)',
'- `autosnippet_panorama` — Project panorama (overview/module/gaps/health)',
'',
'## Recommended First Steps',
'',
'1. Run `autosnippet_bootstrap` to analyze the codebase and generate initial recipes',
'2. Use `autosnippet_search` to query knowledge while coding',
'3. Run `autosnippet_guard` before committing to check compliance',
];
return `${lines.join('\n')}\n`;
}
_ensureDir() {
if (!fs.existsSync(this.rulesDir)) {
fs.mkdirSync(this.rulesDir, { recursive: true });
}
}
}
export default RulesGenerator;