autosnippet
Version:
Extract code patterns into a knowledge base for AI coding assistants
313 lines (312 loc) • 12.1 kB
JavaScript
/**
* ProposalRepository — evolution_proposals 表 CRUD (Drizzle ORM)
*
* 操作 evolution_proposals 表,存储进化提案(merge/supersede/enhance/deprecate/
* reorganize/contradiction/correction)。
*
* 设计要求:
* - 去重:同 target + 同 type 不允许多个 observing 状态的 Proposal
* - Rate Limit:同一 target 不允许同时存在多个相同类型的 observing Proposal
* - JSON 字段(evidence/related_recipe_ids)序列化/反序列化
*
* Drizzle 迁移策略 (Phase 5a):
* - 全部 raw SQL → Drizzle 类型安全 API
* - 构造器接收 DrizzleDB(不再需要 raw Database)
*/
import { randomBytes } from 'node:crypto';
import { and, count, desc, eq, inArray, lte } from 'drizzle-orm';
import { evolutionProposals } from '../../infrastructure/database/drizzle/schema.js';
/* ────────────────────── Constants ────────────────────── */
/** 默认观察窗口:7 天 */
const DEFAULT_OBSERVATION_WINDOW = 7 * 24 * 60 * 60 * 1000;
/** 各 Proposal 类型的默认观察窗口(ms) */
const OBSERVATION_WINDOWS = {
enhance: 48 * 60 * 60 * 1000, // 48h
correction: 24 * 60 * 60 * 1000, // 24h
merge: 72 * 60 * 60 * 1000, // 72h
supersede: 72 * 60 * 60 * 1000, // 72h
deprecate: 7 * 24 * 60 * 60 * 1000, // 7d
contradiction: 7 * 24 * 60 * 60 * 1000, // 7d
reorganize: 7 * 24 * 60 * 60 * 1000, // 7d
};
/** 自动进入观察状态的置信度阈值 */
const AUTO_OBSERVE_THRESHOLDS = {
enhance: 0.7,
correction: 0.7,
merge: 0.75,
supersede: 0.8,
deprecate: 0.0, // decayScore ≤ 40 即可
contradiction: Infinity, // 需开发者确认
reorganize: Infinity, // 需开发者确认
};
/* ────────────────────── Class ────────────────────── */
export class ProposalRepository {
#drizzle;
constructor(drizzle) {
this.#drizzle = drizzle;
}
/* ═══════════════════ Create ═══════════════════ */
/**
* 创建 Proposal 并写入 DB。
*
* - 自动生成 ID(ep-{timestamp}-{random})
* - 自动设定 expiresAt(按 type 默认窗口)
* - 自动判断 status(低风险 + 高置信度 → observing,否则 pending)
* - 去重:同 target + 同 type 已有 pending/observing 时拒绝创建
*/
create(input) {
const now = Date.now();
// 去重检查
if (this.#hasDuplicate(input.targetRecipeId, input.type)) {
return null;
}
const id = ProposalRepository.#generateId(now);
const expiresAt = input.expiresAt ?? now + (OBSERVATION_WINDOWS[input.type] ?? DEFAULT_OBSERVATION_WINDOW);
const status = input.status ?? this.#resolveInitialStatus(input.type, input.confidence);
const record = {
id,
type: input.type,
targetRecipeId: input.targetRecipeId,
relatedRecipeIds: input.relatedRecipeIds ?? [],
confidence: input.confidence,
source: input.source,
description: input.description,
evidence: input.evidence ?? [],
status,
proposedAt: now,
expiresAt,
resolvedAt: null,
resolvedBy: null,
resolution: null,
};
this.#drizzle
.insert(evolutionProposals)
.values({
id: record.id,
type: record.type,
targetRecipeId: record.targetRecipeId,
relatedRecipeIds: JSON.stringify(record.relatedRecipeIds),
confidence: record.confidence,
source: record.source,
description: record.description,
evidence: JSON.stringify(record.evidence),
status: record.status,
proposedAt: record.proposedAt,
expiresAt: record.expiresAt,
})
.run();
return record;
}
/* ═══════════════════ Read ═══════════════════ */
/** 按 ID 查询 */
findById(id) {
const row = this.#drizzle
.select()
.from(evolutionProposals)
.where(eq(evolutionProposals.id, id))
.limit(1)
.get();
return row ? ProposalRepository.#mapRow(row) : null;
}
/** 按条件查询 */
find(filter = {}) {
const conditions = [];
if (filter.status) {
if (Array.isArray(filter.status)) {
conditions.push(inArray(evolutionProposals.status, filter.status));
}
else {
conditions.push(eq(evolutionProposals.status, filter.status));
}
}
if (filter.type) {
conditions.push(eq(evolutionProposals.type, filter.type));
}
if (filter.targetRecipeId) {
conditions.push(eq(evolutionProposals.targetRecipeId, filter.targetRecipeId));
}
if (filter.source) {
conditions.push(eq(evolutionProposals.source, filter.source));
}
if (filter.expiredBefore) {
conditions.push(lte(evolutionProposals.expiresAt, filter.expiredBefore));
}
const condition = conditions.length > 0 ? and(...conditions) : undefined;
const rows = this.#drizzle
.select()
.from(evolutionProposals)
.where(condition)
.orderBy(desc(evolutionProposals.proposedAt))
.all();
return rows.map((r) => ProposalRepository.#mapRow(r));
}
/** 查询已到期的 observing 状态 Proposal */
findExpiredObserving() {
return this.find({
status: 'observing',
expiredBefore: Date.now(),
});
}
/** 查询所有未完成的 Proposal(pending + observing) */
findActive() {
return this.find({
status: ['pending', 'observing'],
});
}
/** 按 target Recipe ID 查询活跃 Proposal */
findByTarget(targetRecipeId) {
return this.find({
targetRecipeId,
status: ['pending', 'observing'],
});
}
/* ═══════════════════ Update ═══════════════════ */
/** 将 Proposal 状态转为 observing */
startObserving(id) {
const now = Date.now();
const proposal = this.findById(id);
if (!proposal || proposal.status !== 'pending') {
return false;
}
const expiresAt = now + (OBSERVATION_WINDOWS[proposal.type] ?? DEFAULT_OBSERVATION_WINDOW);
const result = this.#drizzle
.update(evolutionProposals)
.set({ status: 'observing', expiresAt })
.where(and(eq(evolutionProposals.id, id), eq(evolutionProposals.status, 'pending')))
.run();
return result.changes > 0;
}
/** 标记 Proposal 为已执行 */
markExecuted(id, resolution, resolvedBy = 'auto') {
const result = this.#drizzle
.update(evolutionProposals)
.set({
status: 'executed',
resolvedAt: Date.now(),
resolvedBy,
resolution,
})
.where(and(eq(evolutionProposals.id, id), eq(evolutionProposals.status, 'observing')))
.run();
return result.changes > 0;
}
/** 标记 Proposal 为已拒绝 */
markRejected(id, resolution, resolvedBy = 'auto') {
const result = this.#drizzle
.update(evolutionProposals)
.set({
status: 'rejected',
resolvedAt: Date.now(),
resolvedBy,
resolution,
})
.where(and(eq(evolutionProposals.id, id), inArray(evolutionProposals.status, ['pending', 'observing'])))
.run();
return result.changes > 0;
}
/** 标记 Proposal 为过期 */
markExpired(id) {
const result = this.#drizzle
.update(evolutionProposals)
.set({
status: 'expired',
resolvedAt: Date.now(),
})
.where(and(eq(evolutionProposals.id, id), inArray(evolutionProposals.status, ['pending', 'observing'])))
.run();
return result.changes > 0;
}
/** 更新 evidence(用于追加观察期指标快照) */
updateEvidence(id, evidence) {
const result = this.#drizzle
.update(evolutionProposals)
.set({ evidence: JSON.stringify(evidence) })
.where(eq(evolutionProposals.id, id))
.run();
return result.changes > 0;
}
/* ═══════════════════ Delete ═══════════════════ */
/** 按 target Recipe ID 删除所有 Proposal(用于知识删除时清理关联提案) */
deleteByTargetRecipeId(targetRecipeId) {
const result = this.#drizzle
.delete(evolutionProposals)
.where(eq(evolutionProposals.targetRecipeId, targetRecipeId))
.run();
return result.changes;
}
/* ═══════════════════ Stats ═══════════════════ */
/** 统计各状态的 Proposal 数量 */
stats() {
const rows = this.#drizzle
.select({
status: evolutionProposals.status,
count: count(),
})
.from(evolutionProposals)
.groupBy(evolutionProposals.status)
.all();
const result = {
pending: 0,
observing: 0,
executed: 0,
rejected: 0,
expired: 0,
};
for (const row of rows) {
result[row.status] = row.count;
}
return result;
}
/* ═══════════════════ Private ═══════════════════ */
/** 去重检查:同 target + 同 type 是否已有 pending/observing Proposal */
#hasDuplicate(targetRecipeId, type) {
const row = this.#drizzle
.select({ id: evolutionProposals.id })
.from(evolutionProposals)
.where(and(eq(evolutionProposals.targetRecipeId, targetRecipeId), eq(evolutionProposals.type, type), inArray(evolutionProposals.status, ['pending', 'observing'])))
.limit(1)
.get();
return row !== undefined;
}
/** 根据 type + confidence 判断初始状态 */
#resolveInitialStatus(type, confidence) {
const threshold = AUTO_OBSERVE_THRESHOLDS[type];
return confidence >= threshold ? 'observing' : 'pending';
}
/** 生成 Proposal ID */
static #generateId(timestamp) {
const rand = randomBytes(4).toString('hex');
return `ep-${timestamp}-${rand}`;
}
/** Drizzle Row → ProposalRecord */
static #mapRow(row) {
return {
id: row.id,
type: row.type,
targetRecipeId: row.targetRecipeId,
relatedRecipeIds: safeJsonParse(row.relatedRecipeIds, []),
confidence: row.confidence,
source: row.source,
description: row.description ?? '',
evidence: safeJsonParse(row.evidence, []),
status: row.status,
proposedAt: row.proposedAt,
expiresAt: row.expiresAt,
resolvedAt: row.resolvedAt ?? null,
resolvedBy: row.resolvedBy ?? null,
resolution: row.resolution ?? null,
};
}
}
/* ────────────────────── Util ────────────────────── */
function safeJsonParse(json, fallback) {
if (!json) {
return fallback;
}
try {
return JSON.parse(json);
}
catch {
return fallback;
}
}