UNPKG

autosnippet

Version:

Extract code patterns into a knowledge base for AI coding assistants

196 lines (195 loc) 7.71 kB
/** * 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;