UNPKG

autosnippet

Version:

Extract code patterns into a knowledge base for AI coding assistants

326 lines (325 loc) 12.9 kB
/** * RuleLearner — Guard 规则学习系统 * 追踪规则触发与用户反馈,计算 P/R/F1,识别高误报规则并给出优化建议 * 持久化到 AutoSnippet/guard-learner.json(Git 友好) */ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import Logger from '../../infrastructure/logging/Logger.js'; import { RULE_LEARNER } from '../../shared/constants.js'; import pathGuard from '../../shared/PathGuard.js'; import { DEFAULT_KNOWLEDGE_BASE_DIR } from '../../shared/ProjectMarkers.js'; const PROBLEMATIC_THRESHOLD = { falsePositiveRate: RULE_LEARNER.PROBLEMATIC_FALSE_POSITIVE_RATE, minTriggers: RULE_LEARNER.PROBLEMATIC_MIN_TRIGGERS, }; export class RuleLearner { #learnerPath; #data; #signalBus; constructor(projectRoot, options = {}) { const kbDir = options.knowledgeBaseDir || DEFAULT_KNOWLEDGE_BASE_DIR; this.#learnerPath = join(projectRoot, kbDir, 'guard-learner.json'); pathGuard.assertProjectWriteSafe(this.#learnerPath); this.#migrateOldPath(projectRoot, options.internalDir || '.autosnippet'); this.#data = this.#load(); this.#signalBus = options.signalBus || null; } /** * 记录规则触发 * @param context */ recordTrigger(ruleId, _context = {}) { const stat = this.#ensureStat(ruleId); stat.triggers++; const now = new Date().toISOString(); stat.lastTriggered = now; if (!stat.firstTriggered) { stat.firstTriggered = now; } this.#save(); } /** 记录用户反馈 */ recordFeedback(ruleId, feedbackType) { const stat = this.#ensureStat(ruleId); if (feedbackType === 'correct') { stat.correct++; } else if (feedbackType === 'falsePositive') { stat.falsePositive++; } else if (feedbackType === 'falseNegative') { stat.falseNegative++; } stat.lastFeedback = new Date().toISOString(); this.#save(); // ── Signal: quality feedback ── if (this.#signalBus) { const metrics = this.getMetrics(ruleId); this.#signalBus.send('quality', 'RuleLearner', 1 - metrics.falsePositiveRate, { target: ruleId, metadata: { feedbackType, precision: metrics.precision }, }); } } /** * 获取规则精准度指标 * @returns } */ getMetrics(ruleId) { const stat = this.#data.ruleStats[ruleId]; if (!stat || stat.triggers === 0) { return { precision: 1, recall: 1, f1: 1, triggers: 0, falsePositiveRate: 0 }; } const tp = stat.correct; const fp = stat.falsePositive; const fn = stat.falseNegative; const precision = tp + fp > 0 ? tp / (tp + fp) : 1; const recall = tp + fn > 0 ? tp / (tp + fn) : 1; const f1 = precision + recall > 0 ? (2 * precision * recall) / (precision + recall) : 0; const falsePositiveRate = stat.triggers > 0 ? fp / stat.triggers : 0; return { precision, recall, f1, triggers: stat.triggers, falsePositiveRate }; } /** * 识别问题规则(高误报) * @returns >} */ getProblematicRules() { const results = []; for (const [ruleId, stat] of Object.entries(this.#data.ruleStats)) { if (stat.triggers < PROBLEMATIC_THRESHOLD.minTriggers) { continue; } const metrics = this.getMetrics(ruleId); if (metrics.falsePositiveRate >= PROBLEMATIC_THRESHOLD.falsePositiveRate) { let recommendation; if (metrics.falsePositiveRate > 0.7) { recommendation = 'disable'; } else if (metrics.precision < 0.5) { recommendation = 'tune'; } else { recommendation = 'review'; } results.push({ ruleId, metrics, recommendation }); } } return results.sort((a, b) => b.metrics.falsePositiveRate - a.metrics.falsePositiveRate); } /** 获取所有规则统计 */ getAllStats() { const result = {}; for (const [ruleId] of Object.entries(this.#data.ruleStats)) { result[ruleId] = { ...this.#data.ruleStats[ruleId], metrics: this.getMetrics(ruleId), }; } return result; } /** 重置指定规则或全部统计 */ resetStats(ruleId = null) { if (ruleId) { delete this.#data.ruleStats[ruleId]; } else { this.#data.ruleStats = {}; } this.#save(); } /** * 基于历史数据提出规则优化建议 * 策略 1: 高误报规则 → 建议调整 * 策略 2: 高触发且高精度 → 建议创建项目特化版本 * @returns >} */ suggestRules() { const suggestions = []; // 策略 1: 从高误报规则推导改进建议 const problematic = this.getProblematicRules(); for (const p of problematic) { if (p.recommendation === 'tune') { suggestions.push({ type: 'tune_existing', ruleId: p.ruleId, message: `规则 ${p.ruleId} 误报率 ${(p.metrics.falsePositiveRate * 100).toFixed(0)}%,建议调整正则或收窄语言范围`, confidence: RULE_LEARNER.CONFIDENCE_TUNE, evidence: p.metrics, }); } else if (p.recommendation === 'disable') { suggestions.push({ type: 'disable', ruleId: p.ruleId, message: `规则 ${p.ruleId} 误报率 ${(p.metrics.falsePositiveRate * 100).toFixed(0)}%,建议禁用`, confidence: RULE_LEARNER.CONFIDENCE_DISABLE, evidence: p.metrics, }); } } // 策略 2: 高触发 + 高精度内置规则 → 建议创建项目定制版 const allStats = this.getAllStats(); for (const [ruleId, stat] of Object.entries(allStats)) { if (stat.triggers > RULE_LEARNER.HIGH_TRIGGER_COUNT && (stat.metrics?.precision ?? 1) > RULE_LEARNER.HIGH_PRECISION) { suggestions.push({ type: 'specialize', ruleId, message: `规则 ${ruleId} 触发 ${stat.triggers} 次且精准度高 (${((stat.metrics?.precision ?? 1) * 100).toFixed(0)}%),建议创建项目定制版本`, confidence: RULE_LEARNER.CONFIDENCE_SPECIALIZE, evidence: stat.metrics, }); } } // 策略 3: 长期无触发的规则 → 可能不适用 for (const [ruleId, stat] of Object.entries(allStats)) { if (stat.triggers === 0 && stat.lastTriggered) { const daysSinceLastTrigger = (Date.now() - new Date(stat.lastTriggered).getTime()) / 86400000; if (daysSinceLastTrigger > RULE_LEARNER.UNUSED_DAYS_THRESHOLD) { suggestions.push({ type: 'review_unused', ruleId, message: `规则 ${ruleId} 超过 ${RULE_LEARNER.UNUSED_DAYS_THRESHOLD} 天未触发,建议审查是否仍需保留`, confidence: RULE_LEARNER.CONFIDENCE_REVIEW, evidence: { daysSinceLastTrigger: Math.round(daysSinceLastTrigger), triggers: stat.triggers, }, }); } } } return suggestions.sort((a, b) => b.confidence - a.confidence); } /** * 追踪规则创建后的效果 * 对比首次触发后的表现,判断规则是否有效 * @returns } */ trackRuleEffectiveness(ruleId) { const stat = this.#data.ruleStats[ruleId]; if (!stat) { return { status: 'no_data', triggers: 0, precision: 1, recommendation: 'monitor' }; } const firstTriggered = stat.firstTriggered || stat.lastTriggered; if (!firstTriggered) { return { status: 'no_triggers', triggers: 0, precision: 1, recommendation: 'monitor' }; } const daysSinceFirstTrigger = (Date.now() - new Date(firstTriggered).getTime()) / 86400000; // 不足 14 天 → 观察期 if (daysSinceFirstTrigger < 14) { return { status: 'monitoring', triggers: stat.triggers, precision: this.getMetrics(ruleId).precision, recommendation: 'wait', daysSinceFirstTrigger: Math.round(daysSinceFirstTrigger), }; } const metrics = this.getMetrics(ruleId); // 14 天后判定 if (metrics.precision < RULE_LEARNER.LOW_PRECISION && stat.triggers >= PROBLEMATIC_THRESHOLD.minTriggers) { return { status: 'ineffective', triggers: stat.triggers, precision: metrics.precision, recommendation: 'review_or_disable', daysSinceFirstTrigger: Math.round(daysSinceFirstTrigger), }; } return { status: 'effective', triggers: stat.triggers, precision: metrics.precision, recommendation: 'keep', daysSinceFirstTrigger: Math.round(daysSinceFirstTrigger), }; } /** * RuleLearner→Recipe 桥接: 检查是否有高误报规则需要触发衰退 * 当 FP > 40% && triggers >= minTriggers 时,发射衰退信号到 SignalBus * @returns 需要衰退检查的规则列表 */ checkPrecisionDrop() { const problematic = this.getProblematicRules(); const results = []; for (const p of problematic) { results.push({ ruleId: p.ruleId, falsePositiveRate: p.metrics.falsePositiveRate, recommendation: p.recommendation, }); // 发射衰退信号 if (this.#signalBus) { this.#signalBus.send('quality', 'RuleLearner.precisionDrop', p.metrics.falsePositiveRate, { target: p.ruleId, metadata: { recommendation: p.recommendation, precision: p.metrics.precision, triggers: p.metrics.triggers, }, }); } } return results; } // ─── 私有 ───────────────────────────────────────────── #ensureStat(ruleId) { if (!this.#data.ruleStats[ruleId]) { this.#data.ruleStats[ruleId] = { triggers: 0, correct: 0, falsePositive: 0, falseNegative: 0, firstTriggered: null, lastTriggered: null, lastFeedback: null, }; } return this.#data.ruleStats[ruleId]; } #load() { try { if (existsSync(this.#learnerPath)) { return JSON.parse(readFileSync(this.#learnerPath, 'utf-8')); } } catch { /* silent */ } return { ruleStats: {} }; } #save() { try { const dir = dirname(this.#learnerPath); if (!existsSync(dir)) { mkdirSync(dir, { recursive: true }); } writeFileSync(this.#learnerPath, JSON.stringify(this.#data, null, 2)); } catch (err) { Logger.getInstance().warn('RuleLearner: failed to persist learner data', { error: err.message, }); } } #migrateOldPath(projectRoot, internalDir) { try { const oldPath = join(projectRoot, internalDir, 'guard-learner.json'); if (existsSync(oldPath) && !existsSync(this.#learnerPath)) { const content = readFileSync(oldPath, 'utf-8'); const dir = dirname(this.#learnerPath); if (!existsSync(dir)) { mkdirSync(dir, { recursive: true }); } writeFileSync(this.#learnerPath, content); unlinkSync(oldPath); } } catch { /* 迁移失败不阻断启动 */ } } }