autosnippet
Version:
Extract code patterns into a knowledge base for AI coding assistants
247 lines (246 loc) • 9.79 kB
JavaScript
/**
* BootstrapRepository — Bootstrap 快照的仓储实现
*
* 从 BootstrapSnapshot 提取的数据操作,
* 使用 Drizzle 类型安全 API 操作 bootstrap_snapshots + bootstrap_dim_files 表。
*/
import { and, count, desc, eq, sql } from 'drizzle-orm';
import { bootstrapDimFiles, bootstrapSnapshots, } from '../../infrastructure/database/drizzle/schema.js';
import { RepositoryBase } from '../base/RepositoryBase.js';
/* ═══ Repository 实现 ═══ */
export class BootstrapRepositoryImpl extends RepositoryBase {
/** 默认快照保留数量 */
static MAX_SNAPSHOTS = 5;
constructor(drizzle) {
super(drizzle, bootstrapSnapshots);
}
/* ─── CRUD ─── */
async findById(id) {
const rows = this.drizzle.select().from(this.table).where(eq(this.table.id, id)).limit(1).all();
return rows.length > 0 ? this.#mapRow(rows[0]) : null;
}
async create(data) {
this.drizzle
.insert(this.table)
.values({
id: data.id,
sessionId: data.sessionId ?? null,
projectRoot: data.projectRoot,
createdAt: data.createdAt,
durationMs: data.durationMs ?? 0,
fileCount: data.fileCount ?? 0,
dimensionCount: data.dimensionCount ?? 0,
candidateCount: data.candidateCount ?? 0,
primaryLang: data.primaryLang ?? null,
fileHashes: JSON.stringify(data.fileHashes),
dimensionMeta: JSON.stringify(data.dimensionMeta),
episodicData: data.episodicData ? JSON.stringify(data.episodicData) : null,
isIncremental: data.isIncremental ? 1 : 0,
parentId: data.parentId ?? null,
changedFiles: JSON.stringify(data.changedFiles ?? []),
affectedDims: JSON.stringify(data.affectedDims ?? []),
status: data.status ?? 'complete',
})
.run();
return (await this.findById(data.id));
}
async delete(id) {
const result = this.drizzle.delete(this.table).where(eq(this.table.id, id)).run();
return result.changes > 0;
}
/* ─── 快照查询 ─── */
/** 获取项目最新完成的快照 */
async getLatest(projectRoot) {
const rows = this.drizzle
.select()
.from(this.table)
.where(and(eq(this.table.projectRoot, projectRoot), eq(this.table.status, 'complete')))
.orderBy(desc(this.table.createdAt))
.limit(1)
.all();
return rows.length > 0 ? this.#mapRow(rows[0]) : null;
}
/** 获取项目的快照列表 (按时间降序) */
async listByProject(projectRoot, limit = 10) {
const rows = this.drizzle
.select()
.from(this.table)
.where(eq(this.table.projectRoot, projectRoot))
.orderBy(desc(this.table.createdAt))
.limit(limit)
.all();
return rows.map((r) => this.#mapRow(r));
}
/* ─── 维度-文件关联 ─── */
/** 批量插入维度-文件关联 (INSERT OR IGNORE) */
async saveDimFiles(entries) {
if (entries.length === 0) {
return 0;
}
let inserted = 0;
this.transaction((tx) => {
for (const entry of entries) {
tx.insert(bootstrapDimFiles)
.values({
snapshotId: entry.snapshotId,
dimId: entry.dimId,
filePath: entry.filePath,
role: entry.role ?? 'referenced',
})
.onConflictDoNothing()
.run();
inserted++;
}
});
return inserted;
}
/** 获取快照的维度-文件关联 */
async getDimFiles(snapshotId) {
const rows = this.drizzle
.select({
dimId: bootstrapDimFiles.dimId,
filePath: bootstrapDimFiles.filePath,
})
.from(bootstrapDimFiles)
.where(eq(bootstrapDimFiles.snapshotId, snapshotId))
.all();
return rows;
}
/** 获取快照中每个维度引用的文件集合 */
async getDimFileMap(snapshotId) {
const entries = await this.getDimFiles(snapshotId);
const map = {};
for (const row of entries) {
if (!map[row.dimId]) {
map[row.dimId] = new Set();
}
map[row.dimId].add(row.filePath);
}
return map;
}
/* ─── 容量控制 ─── */
/** 保留项目最新 N 个快照,删除旧的 */
async enforceCapacity(projectRoot, maxSnapshots = BootstrapRepositoryImpl.MAX_SNAPSHOTS) {
const result = this.drizzle
.delete(this.table)
.where(and(eq(this.table.projectRoot, projectRoot), sql `${this.table.id} NOT IN (
SELECT ${this.table.id} FROM ${this.table}
WHERE ${this.table.projectRoot} = ${projectRoot}
ORDER BY ${this.table.createdAt} DESC
LIMIT ${maxSnapshots}
)`))
.run();
return result.changes;
}
/** 清除项目的所有快照 */
async clearProject(projectRoot) {
const snapshots = await this.listByProject(projectRoot, 9999);
let deleted = 0;
for (const snap of snapshots) {
if (await this.delete(snap.id)) {
deleted++;
}
}
return deleted;
}
/* ─── 事务保存 ─── */
/**
* 事务保存快照 + 维度-文件关联 + 容量控制
* 替代 BootstrapSnapshot.save() 中的事务逻辑
*/
async saveWithDimFiles(snapshot, dimFiles) {
this.transaction((tx) => {
// 主记录
tx.insert(this.table)
.values({
id: snapshot.id,
sessionId: snapshot.sessionId ?? null,
projectRoot: snapshot.projectRoot,
createdAt: snapshot.createdAt,
durationMs: snapshot.durationMs ?? 0,
fileCount: snapshot.fileCount ?? 0,
dimensionCount: snapshot.dimensionCount ?? 0,
candidateCount: snapshot.candidateCount ?? 0,
primaryLang: snapshot.primaryLang ?? null,
fileHashes: JSON.stringify(snapshot.fileHashes),
dimensionMeta: JSON.stringify(snapshot.dimensionMeta),
episodicData: snapshot.episodicData ? JSON.stringify(snapshot.episodicData) : null,
isIncremental: snapshot.isIncremental ? 1 : 0,
parentId: snapshot.parentId ?? null,
changedFiles: JSON.stringify(snapshot.changedFiles ?? []),
affectedDims: JSON.stringify(snapshot.affectedDims ?? []),
status: snapshot.status ?? 'complete',
})
.run();
// 维度-文件关联
for (const df of dimFiles) {
tx.insert(bootstrapDimFiles)
.values({
snapshotId: df.snapshotId,
dimId: df.dimId,
filePath: df.filePath,
role: df.role ?? 'referenced',
})
.onConflictDoNothing()
.run();
}
// 容量控制
tx.delete(this.table)
.where(and(eq(this.table.projectRoot, snapshot.projectRoot), sql `${this.table.id} NOT IN (
SELECT ${this.table.id} FROM ${this.table}
WHERE ${this.table.projectRoot} = ${snapshot.projectRoot}
ORDER BY ${this.table.createdAt} DESC
LIMIT ${BootstrapRepositoryImpl.MAX_SNAPSHOTS}
)`))
.run();
});
return (await this.findById(snapshot.id));
}
/** 获取项目最新的主语言 (Panorama 域用于维度/角色检测) */
async getLatestPrimaryLang(projectRoot) {
const rows = this.drizzle
.select({ primaryLang: this.table.primaryLang })
.from(this.table)
.where(eq(this.table.projectRoot, projectRoot))
.orderBy(desc(this.table.createdAt))
.limit(1)
.all();
return rows.length > 0 ? (rows[0].primaryLang ?? null) : null;
}
/** 获取快照总数 */
async getSnapshotCount(projectRoot) {
const condition = projectRoot ? eq(this.table.projectRoot, projectRoot) : undefined;
const [row] = this.drizzle.select({ cnt: count() }).from(this.table).where(condition).all();
return row?.cnt ?? 0;
}
/* ─── 内部辅助 ─── */
#mapRow(row) {
return {
id: row.id,
sessionId: row.sessionId ?? null,
projectRoot: row.projectRoot,
createdAt: row.createdAt,
durationMs: row.durationMs ?? 0,
fileCount: row.fileCount ?? 0,
dimensionCount: row.dimensionCount ?? 0,
candidateCount: row.candidateCount ?? 0,
primaryLang: row.primaryLang ?? null,
fileHashes: safeParseJSON(row.fileHashes, {}),
dimensionMeta: safeParseJSON(row.dimensionMeta, {}),
episodicData: safeParseJSON(row.episodicData, null),
isIncremental: !!row.isIncremental,
parentId: row.parentId ?? null,
changedFiles: safeParseJSON(row.changedFiles, []),
affectedDims: safeParseJSON(row.affectedDims, []),
status: row.status ?? 'complete',
};
}
}
function safeParseJSON(str, fallback) {
try {
return str ? JSON.parse(str) : fallback;
}
catch {
return fallback;
}
}