UNPKG

autosnippet

Version:

Extract code patterns into a knowledge base for AI coding assistants

254 lines (253 loc) 9.52 kB
/** * ViolationsStore — Guard 违反记录存储(DB 版) * 记录每次 as:audit 运行的审计结果,持久化到 SQLite guard_violations 表。 * 最多保留 200 条。 * * 所有操作使用 Drizzle 类型安全 API(零 raw SQL)。 */ import { asc, count, desc, eq, sql } from 'drizzle-orm'; import { getDrizzle } from '../../infrastructure/database/drizzle/index.js'; import { guardViolations } from '../../infrastructure/database/drizzle/schema.js'; const MAX_RUNS = 200; export class ViolationsStore { #drizzle; /** @param _db 保留签名兼容 (不再使用) */ constructor(_db, drizzle) { this.#drizzle = drizzle ?? getDrizzle(); } // ─── 写入 ───────────────────────────────────────────── /** * 追加一次 Guard 运行记录 * ★ 去重:同一文件、同一违规集合不重复入库,仅更新时间戳 * ★ 全 Drizzle 类型安全 */ appendRun(run) { const filePath = run.filePath || ''; const violations = run.violations || []; const violationsJson = JSON.stringify(violations); // ── 去重:查最近一条同文件记录,比较违规指纹 ── const fingerprint = this.#violationFingerprint(violations); if (filePath) { const lastRow = this.#drizzle .select({ id: guardViolations.id, violationsJson: guardViolations.violationsJson, }) .from(guardViolations) .where(eq(guardViolations.filePath, filePath)) .orderBy(desc(guardViolations.createdAt)) .limit(1) .get(); if (lastRow) { const lastFingerprint = this.#violationFingerprint(JSON.parse(lastRow.violationsJson || '[]')); if (fingerprint === lastFingerprint) { // 违规未变化:仅刷新时间戳,不新增行 this.#drizzle .update(guardViolations) .set({ triggeredAt: new Date().toISOString(), createdAt: Math.floor(Date.now() / 1000), }) .where(eq(guardViolations.id, lastRow.id)) .run(); return lastRow.id; } } } const id = `run_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; const now = Math.floor(Date.now() / 1000); this.#drizzle .insert(guardViolations) .values({ id, filePath, triggeredAt: new Date().toISOString(), violationCount: violations.length, summary: run.summary || '', violationsJson, createdAt: now, }) .run(); // 超限截断:保留最新 MAX_RUNS 条 this.#drizzle .delete(guardViolations) .where(sql `${guardViolations.id} NOT IN ( SELECT ${guardViolations.id} FROM ${guardViolations} ORDER BY ${guardViolations.createdAt} DESC LIMIT ${MAX_RUNS} )`) .run(); return id; } /** * 违规指纹:按 ruleId+severity+line 排序后拼接,用于去重比较 */ #violationFingerprint(violations) { return violations .map((v) => `${v.ruleId || ''}|${v.severity || ''}|${v.line ?? ''}`) .sort() .join('\n'); } // ─── 查询 ───────────────────────────────────────────── /** * 获取所有运行记录(最新在后) */ getRuns() { const rows = this.#drizzle .select() .from(guardViolations) .orderBy(asc(guardViolations.createdAt)) .all(); return rows.map((r) => this.#rowToRun(r)); } /** * 按文件路径查询历史 */ getRunsByFile(filePath) { const rows = this.#drizzle .select() .from(guardViolations) .where(eq(guardViolations.filePath, filePath)) .orderBy(asc(guardViolations.createdAt)) .all(); return rows.map((r) => this.#rowToRun(r)); } /** * 获取最近 N 条记录 */ getRecentRuns(n = 20) { const rows = this.#drizzle .select() .from(guardViolations) .orderBy(desc(guardViolations.createdAt), sql `rowid DESC`) .limit(n) .all(); return rows.reverse().map((r) => this.#rowToRun(r)); } /** 获取统计汇总 */ getStats() { const [row] = this.#drizzle .select({ totalRuns: count(), totalViolations: sql `COALESCE(SUM(${guardViolations.violationCount}), 0)`, lastRunAt: sql `MAX(${guardViolations.triggeredAt})`, }) .from(guardViolations) .all(); const totalRuns = row?.totalRuns ?? 0; const totalViolations = row?.totalViolations ?? 0; return { totalRuns, totalViolations, averageViolationsPerRun: totalRuns > 0 ? (totalViolations / totalRuns).toFixed(2) : 0, lastRunAt: row?.lastRunAt || null, }; } /** * 按规则 ID 聚合统计 * 利用 SQLite json_each 展开 violations_json 数组 * * json_each 是 SQLite 专有函数,Drizzle 无 typed API (ORM limitation) */ getStatsByRule() { try { return this.#drizzle.all(sql ` SELECT json_extract(j.value, '$.ruleId') AS ruleId, json_extract(j.value, '$.severity') AS severity, COUNT(*) AS count FROM ${guardViolations} gv, json_each(gv.violations_json) j WHERE json_extract(j.value, '$.ruleId') IS NOT NULL GROUP BY ruleId, severity ORDER BY count DESC `); } catch { return []; } } /** * 获取趋势数据 — 对比最近两次运行 */ getTrend() { const recent = this.getRecentRuns(2); if (recent.length < 2) { const latest = recent[0]?.violations || []; return { errorsChange: 0, warningsChange: 0, latestErrors: latest.filter((v) => v.severity === 'error').length, latestWarnings: latest.filter((v) => v.severity === 'warning').length, previousErrors: 0, previousWarnings: 0, hasHistory: false, }; } const [prev, latest] = recent; const latestErrors = latest.violations.filter((v) => v.severity === 'error').length; const latestWarnings = latest.violations.filter((v) => v.severity === 'warning').length; const previousErrors = prev.violations.filter((v) => v.severity === 'error').length; const previousWarnings = prev.violations.filter((v) => v.severity === 'warning').length; return { errorsChange: latestErrors - previousErrors, warningsChange: latestWarnings - previousWarnings, latestErrors, latestWarnings, previousErrors, previousWarnings, hasHistory: true, }; } // ─── 清除 ───────────────────────────────────────────── /** 清空所有记录 */ clearRuns() { this.#drizzle.delete(guardViolations).run(); } /** 清除指定规则或文件的记录 */ async clearAll() { this.clearRuns(); } async clear({ ruleId, file } = {}) { if (file) { this.#drizzle.delete(guardViolations).where(eq(guardViolations.filePath, file)).run(); } else { this.clearRuns(); } } /** 分页查询 */ async list(filters = {}, { page = 1, limit = 20 } = {}) { const offset = (page - 1) * limit; const condition = filters.file ? eq(guardViolations.filePath, filters.file) : undefined; const rows = this.#drizzle .select() .from(guardViolations) .where(condition) .orderBy(desc(guardViolations.createdAt)) .limit(limit) .offset(offset) .all(); const [totalRow] = this.#drizzle .select({ c: count() }) .from(guardViolations) .where(condition) .all(); const total = totalRow?.c ?? 0; return { data: rows.map((r) => this.#rowToRun(r)), pagination: { page, limit, total, pages: Math.ceil(total / limit) }, }; } // ─── 内部 ───────────────────────────────────────────── /** Drizzle camelCase 行 → RunOutput */ #rowToRun(row) { return { id: row.id, filePath: row.filePath, triggeredAt: row.triggeredAt, violations: row.violationsJson ? JSON.parse(row.violationsJson) : [], violationCount: row.violationCount ?? 0, summary: row.summary || '', }; } }