autosnippet
Version:
Extract code patterns into a knowledge base for AI coding assistants
917 lines (916 loc) • 37.3 kB
JavaScript
import { and, count, desc, eq, gt, inArray, isNotNull, like, ne, or, sql } from 'drizzle-orm';
import { inferKind, KnowledgeEntry } from '../../domain/knowledge/index.js';
import { COUNTABLE_LIFECYCLES } from '../../domain/knowledge/Lifecycle.js';
import { getDrizzle } from '../../infrastructure/database/drizzle/index.js';
import { knowledgeEntries } from '../../infrastructure/database/drizzle/schema.js';
import Logger from '../../infrastructure/logging/Logger.js';
import { safeJsonParse, safeJsonStringify, unixNow } from '../../shared/utils/common.js';
/** Only allow safe SQL identifier characters */
const SAFE_IDENTIFIER_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
/**
* KnowledgeRepositoryImpl — 统一知识实体仓储实现 (Drizzle ORM)
*
* 面向 knowledge_entries 表的 SQLite 持久化。
* 全链路 camelCase — DB 列名 = 实体属性名。
*
* Drizzle 迁移策略:
* - CRUD (create/findById/update/delete/findActiveRules) → drizzle 类型安全 API
* - 复杂动态查询 (findWithPagination/getStats) → 保留 raw SQL→渐进迁移
*/
export class KnowledgeRepositoryImpl {
/** Raw DB for complex dynamic queries (ORM limitation — used within repository layer) */
db;
logger;
#drizzle;
/** Lazily-populated column whitelist for SQL-injection prevention */
#columnWhitelist = null;
constructor(database, drizzle) {
this.db = database.getDb();
this.logger = Logger.getInstance();
this.#drizzle = drizzle ?? getDrizzle();
}
/**
* Validate column name is safe for SQL interpolation (copied from retired BaseRepository).
* Rejects anything that doesn't match /^[a-zA-Z_]\w*$/ or is not a real column.
*/
_assertSafeColumn(key) {
if (!SAFE_IDENTIFIER_RE.test(key)) {
throw new Error(`Invalid column name: ${key}`);
}
if (!this.#columnWhitelist) {
try {
const cols = this.db
.prepare('PRAGMA table_info(knowledge_entries)')
.all();
this.#columnWhitelist = new Set(cols.map((c) => c.name));
}
catch {
this.#columnWhitelist = new Set();
}
}
if (this.#columnWhitelist.size > 0 && !this.#columnWhitelist.has(key)) {
throw new Error(`Unknown column "${key}" for table knowledge_entries`);
}
}
/* ═══ CRUD ═══════════════════════════════════════════ */
/**
* 按 ID 查找
* ★ Drizzle 类型安全 SELECT
*/
async findById(id) {
try {
const row = this.#drizzle
.select()
.from(knowledgeEntries)
.where(eq(knowledgeEntries.id, id))
.limit(1)
.get();
return row ? this._rowToEntity(row) : null;
}
catch (error) {
const message = error instanceof Error ? error.message : String(error);
this.logger.error('Error finding knowledge entry by id', { id, error: message });
throw error;
}
}
/**
* 创建 KnowledgeEntry
* ★ Drizzle 类型安全 INSERT — 列名拼写编译期检查
*/
async create(entry) {
if (!entry || !entry.isValid()) {
throw new Error('Invalid knowledge entry: title + content required');
}
try {
const row = this._entityToRow(entry);
this.#drizzle.insert(knowledgeEntries).values(row).run();
return this.findById(entry.id);
}
catch (error) {
const message = error instanceof Error ? error.message : String(error);
this.logger.error('Error creating knowledge entry', {
entryId: entry.id,
error: message,
});
throw error;
}
}
/**
* 按标题精确查找(大小写不敏感)
*/
async findByTitle(title) {
const rows = this.#drizzle
.select()
.from(knowledgeEntries)
.where(sql `lower(${knowledgeEntries.title}) = lower(${title})`)
.limit(1)
.all();
if (rows.length === 0) {
return null;
}
return this._rowToEntity(rows[0]);
}
/**
* 更新 KnowledgeEntry(接受完整实体或部分数据)
* ★ Drizzle 类型安全 UPDATE
*/
async update(id, updates) {
try {
const existing = (await this.findById(id));
if (!existing) {
throw new Error(`Knowledge entry not found: ${id}`);
}
if (updates instanceof KnowledgeEntry) {
const fullRow = this._entityToRow(updates);
const { id: _id, createdAt: _ca, ...row } = fullRow;
row.updatedAt = unixNow();
this.#drizzle.update(knowledgeEntries).set(row).where(eq(knowledgeEntries.id, id)).run();
return this.findById(id);
}
// 部分更新 — 合并到现有实体
const merged = KnowledgeEntry.fromJSON({
...existing.toJSON(),
...updates,
updatedAt: unixNow(),
});
const fullRow2 = this._entityToRow(merged);
const { id: _id2, createdAt: _ca2, ...row } = fullRow2;
this.#drizzle.update(knowledgeEntries).set(row).where(eq(knowledgeEntries.id, id)).run();
return this.findById(id);
}
catch (error) {
const message = error instanceof Error ? error.message : String(error);
this.logger.error('Error updating knowledge entry', {
id,
error: message,
});
throw error;
}
}
/**
* 删除
* ★ Drizzle 类型安全 DELETE
*/
async delete(id) {
try {
const result = this.#drizzle
.delete(knowledgeEntries)
.where(eq(knowledgeEntries.id, id))
.run();
return result.changes > 0;
}
catch (error) {
const message = error instanceof Error ? error.message : String(error);
this.logger.error('Error deleting knowledge entry', { id, error: message });
throw error;
}
}
/* ═══ 查询 ═══════════════════════════════════════════ */
/**
* 更新生命周期状态
* ★ Drizzle 类型安全 UPDATE — 供 RecipeLifecycleSupervisor / ProposalExecutor 使用
*/
async updateLifecycle(id, lifecycle) {
const result = this.#drizzle
.update(knowledgeEntries)
.set({ lifecycle, updatedAt: unixNow() })
.where(eq(knowledgeEntries.id, id))
.run();
return result.changes > 0;
}
/**
* 更新 stats JSON 字段
* ★ Drizzle 类型安全 UPDATE — 供 HitRecorder / RecipeLifecycleSupervisor 使用
*/
async updateStats(id, stats) {
const result = this.#drizzle
.update(knowledgeEntries)
.set({ stats: safeJsonStringify(stats), updatedAt: unixNow() })
.where(eq(knowledgeEntries.id, id))
.run();
return result.changes > 0;
}
/**
* 分页查询
* @override
*/
async findWithPagination(filters = {}, options = {}) {
const { page = 1, pageSize = 20, orderBy = 'createdAt', order = 'DESC' } = options;
const offset = (page - 1) * pageSize;
const conditions = [];
const params = [];
const { _tagLike, _search, lifecycle: lcFilter, ...normalFilters } = filters;
if (lcFilter) {
if (Array.isArray(lcFilter)) {
const placeholders = lcFilter.map(() => '?').join(', ');
conditions.push(`lifecycle IN (${placeholders})`);
params.push(...lcFilter);
}
else {
conditions.push(`lifecycle = ?`);
params.push(lcFilter);
}
}
for (const [key, value] of Object.entries(normalFilters)) {
if (value == null) {
continue;
}
this._assertSafeColumn(key);
conditions.push(`${key} = ?`);
params.push(value);
}
if (_tagLike) {
conditions.push(`tags LIKE ?`);
const escaped = _tagLike.replace(/[%_\\]/g, (ch) => `\\${ch}`);
params.push(`%"${escaped}"%`);
}
if (_search) {
const escaped = _search.replace(/[%_\\]/g, (ch) => `\\${ch}`);
const like = `%${escaped}%`;
conditions.push(`(title LIKE ? ESCAPE '\\' OR description LIKE ? ESCAPE '\\' OR trigger LIKE ? ESCAPE '\\' OR content LIKE ? ESCAPE '\\' OR tags LIKE ? ESCAPE '\\')`);
params.push(like, like, like, like, like);
}
const where = conditions.length > 0 ? ` WHERE ${conditions.join(' AND ')}` : '';
this._assertSafeColumn(orderBy);
const orderClause = ` ORDER BY ${orderBy} ${order === 'ASC' ? 'ASC' : 'DESC'}`;
const total = this.db
.prepare(`SELECT COUNT(*) as count FROM knowledge_entries${where}`)
.get(...params).count;
const data = this.db
.prepare(`SELECT * FROM knowledge_entries${where}${orderClause} LIMIT ? OFFSET ?`)
.all(...params, pageSize, offset);
return {
data: data.map((row) => this._rowToEntity(row)),
pagination: { page, pageSize, total, pages: Math.ceil(total / pageSize) },
};
}
/** 根据生命周期状态查询 */
async findByLifecycle(lifecycle, pagination = {}) {
return this.findWithPagination({ lifecycle }, pagination);
}
/** 根据 kind 查询 */
async findByKind(kind, options = {}) {
const { lifecycle, ...pagination } = options;
const filters = { kind };
if (lifecycle) {
filters.lifecycle = lifecycle;
}
return this.findWithPagination(filters, pagination);
}
/**
* 查询所有 active 的 rule 类型(Guard 消费热路径)
* ★ Drizzle 类型安全查询
*/
async findActiveRules() {
try {
const rows = this.#drizzle
.select()
.from(knowledgeEntries)
.where(and(eq(knowledgeEntries.kind, 'rule'), eq(knowledgeEntries.lifecycle, 'active')))
.all();
return rows.map((row) => this._rowToEntity(row));
}
catch (error) {
const message = error instanceof Error ? error.message : String(error);
this.logger.error('Error finding active rules', { error: message });
throw error;
}
}
/**
* Guard 专用:active 的 rule + boundary-constraint
* ★ Phase 5b: supply guard.ts _loadRuleRecipes
*/
async findActiveGuardRecipes() {
const rows = this.#drizzle
.select()
.from(knowledgeEntries)
.where(and(eq(knowledgeEntries.lifecycle, 'active'), or(eq(knowledgeEntries.kind, 'rule'), eq(knowledgeEntries.knowledgeType, 'boundary-constraint'))))
.all();
return rows
.map((row) => this._rowToEntity(row))
.filter(Boolean);
}
/**
* 按 source 字段查询 ID 列表
* ★ Phase 5b: supply ai.ts mock cleanup
*/
async findIdsBySource(source) {
const rows = this.#drizzle
.select({ id: knowledgeEntries.id })
.from(knowledgeEntries)
.where(eq(knowledgeEntries.source, source))
.all();
return rows.map((r) => r.id);
}
/**
* 统计指定 lifecycle 集合中的条目数量
* ★ Phase 5b: supply recipes.ts pre-check
*/
async countByLifecycles(lifecycles) {
const rows = this.#drizzle
.select({ cnt: count() })
.from(knowledgeEntries)
.where(inArray(knowledgeEntries.lifecycle, lifecycles))
.all();
return Number(rows[0]?.cnt ?? 0);
}
/**
* 查询指定 lifecycle 集合中的所有条目(不分页)
* ★ Phase 5c: supply Evolution domain services (ContradictionDetector, RedundancyAnalyzer, etc.)
*/
async findAllByLifecycles(lifecycles) {
const rows = this.#drizzle
.select()
.from(knowledgeEntries)
.where(inArray(knowledgeEntries.lifecycle, lifecycles))
.all();
return rows
.map((row) => this._rowToEntity(row))
.filter(Boolean);
}
/**
* 查询指定 lifecycle + category 的条目(带 limit)
* ★ Phase 5c: supply ConsolidationAdvisor category-filtered query
*/
async findAllByLifecyclesAndCategory(lifecycles, category, limit) {
const rows = this.#drizzle
.select()
.from(knowledgeEntries)
.where(and(inArray(knowledgeEntries.lifecycle, lifecycles), eq(knowledgeEntries.category, category)))
.limit(limit)
.all();
return rows
.map((row) => this._rowToEntity(row))
.filter(Boolean);
}
/**
* 查询指定 lifecycle 中 trigger 匹配前缀且排除指定 category 的条目
* ★ Phase 5c: supply ConsolidationAdvisor trigger-prefix fallback
*/
async findByLifecyclesAndTriggerPrefix(lifecycles, excludeCategory, triggerPrefix, limit) {
const rows = this.#drizzle
.select()
.from(knowledgeEntries)
.where(and(inArray(knowledgeEntries.lifecycle, lifecycles), ne(knowledgeEntries.category, excludeCategory), like(knowledgeEntries.trigger, `${triggerPrefix}%`)))
.limit(limit)
.all();
return rows
.map((row) => this._rowToEntity(row))
.filter(Boolean);
}
/**
* 按 lifecycle 分组统计全部条目数量
* ★ Phase 5c: supply RecipeLifecycleSupervisor health summary
*/
async countGroupByLifecycle() {
const rows = this.#drizzle
.select({
lifecycle: knowledgeEntries.lifecycle,
cnt: count(),
})
.from(knowledgeEntries)
.groupBy(knowledgeEntries.lifecycle)
.all();
const result = {};
for (const row of rows) {
result[row.lifecycle ?? ''] = Number(row.cnt);
}
return result;
}
/**
* 反向查找 relations JSON 中包含指定 nodeId 的条目
* ★ Phase 5b: supply structure.ts relation graph
*/
async findByRelationLike(nodeId, excludeId) {
const rows = this.#drizzle
.select({
id: knowledgeEntries.id,
title: knowledgeEntries.title,
relations: knowledgeEntries.relations,
})
.from(knowledgeEntries)
.where(and(like(knowledgeEntries.relations, `%${nodeId}%`), ne(knowledgeEntries.id, excludeId)))
.all();
return rows.map((r) => ({ id: r.id, title: r.title ?? '', relations: r.relations ?? '{}' }));
}
/** 根据语言查询 */
async findByLanguage(language, pagination = {}) {
return this.findWithPagination({ language }, pagination);
}
/** 根据分类查询 */
async findByCategory(category, pagination = {}) {
return this.findWithPagination({ category }, pagination);
}
/** 搜索 */
async search(keyword, pagination = {}) {
return this.findWithPagination({ _search: keyword }, pagination);
}
/** 获取统计信息 */
async getStats() {
try {
return this.db
.prepare(`
SELECT
COUNT(*) as total,
SUM(CASE WHEN lifecycle = 'pending' THEN 1 ELSE 0 END) as pending,
SUM(CASE WHEN lifecycle = 'staging' THEN 1 ELSE 0 END) as staging,
SUM(CASE WHEN lifecycle = 'active' THEN 1 ELSE 0 END) as active,
SUM(CASE WHEN lifecycle = 'evolving' THEN 1 ELSE 0 END) as evolving,
SUM(CASE WHEN lifecycle = 'decaying' THEN 1 ELSE 0 END) as decaying,
SUM(CASE WHEN lifecycle = 'deprecated' THEN 1 ELSE 0 END) as deprecated,
SUM(CASE WHEN kind = 'rule' THEN 1 ELSE 0 END) as rules,
SUM(CASE WHEN kind = 'pattern' THEN 1 ELSE 0 END) as patterns,
SUM(CASE WHEN kind = 'fact' THEN 1 ELSE 0 END) as facts
FROM knowledge_entries
`)
.get();
}
catch (error) {
const message = error instanceof Error ? error.message : String(error);
this.logger.error('Error getting knowledge stats', { error: message });
throw error;
}
}
/**
* Find all entries with non-empty reasoning (for SourceRefReconciler)
* ★ Drizzle 类型安全 SELECT — 仅返回 id + reasoning
*/
async findAllIdAndReasoning() {
const rows = this.#drizzle
.select({
id: knowledgeEntries.id,
reasoning: knowledgeEntries.reasoning,
})
.from(knowledgeEntries)
.where(and(sql `${knowledgeEntries.reasoning} IS NOT NULL`, sql `${knowledgeEntries.reasoning} != '{}'`))
.all();
return rows
.filter((r) => r.reasoning != null)
.map((r) => ({ id: r.id, reasoning: r.reasoning }));
}
/**
* Find sourceFile and reasoning for a single entry (for SourceRefReconciler.applyRepairs)
* ★ Drizzle 类型安全 SELECT — 仅返回 sourceFile + reasoning
*/
async findSourceFileAndReasoning(id) {
const row = this.#drizzle
.select({
sourceFile: knowledgeEntries.sourceFile,
reasoning: knowledgeEntries.reasoning,
})
.from(knowledgeEntries)
.where(eq(knowledgeEntries.id, id))
.limit(1)
.get();
if (!row) {
return null;
}
return { sourceFile: row.sourceFile ?? null, reasoning: row.reasoning ?? null };
}
/**
* Update reasoning JSON field directly (for SourceRefReconciler.applyRepairs)
* ★ Drizzle 类型安全 UPDATE — 精确更新 reasoning + updatedAt
*/
async updateReasoning(id, reasoning, updatedAt) {
const result = this.#drizzle
.update(knowledgeEntries)
.set({ reasoning, updatedAt })
.where(eq(knowledgeEntries.id, id))
.run();
return result.changes > 0;
}
/* ─── Panorama 域查询 (Phase 5e) ─── */
/**
* 获取活跃 Recipe 的元数据 (title, category, topicHint, kind)
* 用于 DimensionAnalyzer 维度分类分析
*/
async findRecipeMetadata(lifecycles) {
const rows = this.#drizzle
.select({
title: knowledgeEntries.title,
category: knowledgeEntries.category,
topicHint: knowledgeEntries.topicHint,
kind: knowledgeEntries.kind,
})
.from(knowledgeEntries)
.where(inArray(knowledgeEntries.lifecycle, lifecycles))
.all();
return rows.map((r) => ({
title: r.title ?? '',
category: r.category ?? '',
topicHint: r.topicHint ?? '',
kind: r.kind ?? '',
}));
}
/**
* 按模块相关关键词搜索 Recipe (PanoramaService.#findModuleRecipes)
* @param lifecycles - 活跃生命周期
* @param moduleName - 模块名
* @param categories - 角色关联的分类列表
* @param limit - 结果上限
*/
async findModuleRecipes(lifecycles, moduleName, categories, limit = 20) {
const conditions = [
or(like(knowledgeEntries.title, `%${moduleName}%`), like(knowledgeEntries.trigger, `%${moduleName}%`), ...categories.map((cat) => eq(knowledgeEntries.category, cat))),
];
const rows = this.#drizzle
.select({
id: knowledgeEntries.id,
title: knowledgeEntries.title,
trigger: knowledgeEntries.trigger,
kind: knowledgeEntries.kind,
})
.from(knowledgeEntries)
.where(and(inArray(knowledgeEntries.lifecycle, lifecycles), ...conditions))
.orderBy(knowledgeEntries.lifecycle)
.limit(limit)
.all();
return rows.map((r) => ({
id: r.id ?? '',
title: r.title ?? '',
trigger: r.trigger ?? '',
kind: r.kind ?? '',
}));
}
/**
* 统计 COUNTABLE_LIFECYCLES 范围内的知识条目数 (PanoramaAggregator.#getProjectRecipeCount)
*/
async countByCountableLifecycles() {
const rows = this.#drizzle
.select({ cnt: count() })
.from(knowledgeEntries)
.where(inArray(knowledgeEntries.lifecycle, COUNTABLE_LIFECYCLES))
.all();
return Number(rows[0]?.cnt ?? 0);
}
/* ═══ Guard / Skills 用查询 ═══════════════════════════ */
/**
* Guard 规则查询 — kind='rule' OR knowledgeType='boundary-constraint' + lifecycle 过滤
* (GuardCheckEngine._loadCustomRules)
*/
findGuardRulesSync(lifecycles) {
return this.#drizzle
.select({
id: knowledgeEntries.id,
title: knowledgeEntries.title,
description: knowledgeEntries.description,
language: knowledgeEntries.language,
scope: knowledgeEntries.scope,
constraints: knowledgeEntries.constraints,
lifecycle: knowledgeEntries.lifecycle,
})
.from(knowledgeEntries)
.where(and(or(eq(knowledgeEntries.kind, 'rule'), eq(knowledgeEntries.knowledgeType, 'boundary-constraint')), inArray(knowledgeEntries.lifecycle, lifecycles)))
.all();
}
/**
* Guard 命中次数递增 — stats.guardHits += count
* (GuardCheckEngine._recordHits)
*/
incrementGuardHitsSync(id, hits) {
this.#drizzle
.update(knowledgeEntries)
.set({
stats: sql `json_set(COALESCE(${knowledgeEntries.stats}, '{}'), '$.guardHits', COALESCE(json_extract(${knowledgeEntries.stats}, '$.guardHits'), 0) + ${hits})`,
updatedAt: unixNow(),
})
.where(eq(knowledgeEntries.id, id))
.run();
}
/**
* 活跃规则 + content 中的 coreCode / pattern 字段 + stats
* (ReverseGuard.#loadActiveRules)
*/
findActiveRulesWithContentSync() {
return this.#drizzle
.select({
id: knowledgeEntries.id,
title: knowledgeEntries.title,
coreCode: sql `json_extract(${knowledgeEntries.content}, '$.coreCode')`.as('coreCode'),
guardPattern: sql `json_extract(${knowledgeEntries.content}, '$.pattern')`.as('guardPattern'),
stats: knowledgeEntries.stats,
})
.from(knowledgeEntries)
.where(and(eq(knowledgeEntries.lifecycle, 'active'), eq(knowledgeEntries.kind, 'rule')))
.all();
}
/**
* 获取单条记录的 guardHits 数
* (ReverseGuard.#historicalGuardHits)
*/
getGuardHitsSync(id) {
const row = this.#drizzle
.select({
hits: sql `json_extract(${knowledgeEntries.stats}, '$.guardHits')`.as('hits'),
})
.from(knowledgeEntries)
.where(eq(knowledgeEntries.id, id))
.get();
return Number(row?.hits ?? 0);
}
/**
* 活跃规则的 id + language (CoverageAnalyzer.#loadActiveRules) — sync
*/
findActiveRuleIdsSync() {
return this.#drizzle
.select({
id: knowledgeEntries.id,
language: knowledgeEntries.language,
})
.from(knowledgeEntries)
.where(and(eq(knowledgeEntries.lifecycle, 'active'), eq(knowledgeEntries.kind, 'rule')))
.all();
}
/**
* 活跃条目按 category 分布
* (SkillAdvisor.#getKBDistribution)
*/
async countGroupByCategory() {
return this.#drizzle
.select({
category: knowledgeEntries.category,
cnt: count(),
})
.from(knowledgeEntries)
.where(and(eq(knowledgeEntries.lifecycle, 'active'), isNotNull(knowledgeEntries.category), ne(knowledgeEntries.category, '')))
.groupBy(knowledgeEntries.category)
.orderBy(desc(count()))
.all();
}
/**
* 活跃条目按 language 分布
* (SkillAdvisor.#getKBDistribution)
*/
async countGroupByLanguage() {
return this.#drizzle
.select({
language: knowledgeEntries.language,
cnt: count(),
})
.from(knowledgeEntries)
.where(and(eq(knowledgeEntries.lifecycle, 'active'), isNotNull(knowledgeEntries.language), ne(knowledgeEntries.language, '')))
.groupBy(knowledgeEntries.language)
.orderBy(desc(count()))
.all();
}
/**
* 高使用率活跃 Recipe (adoptions + applications >= minUsage)
* (SkillAdvisor.#getKBDistribution)
*/
async findHotRecipesByUsage(minUsage, limit) {
return this.#drizzle
.select({
title: knowledgeEntries.title,
category: knowledgeEntries.category,
totalUsage: sql `(COALESCE(json_extract(${knowledgeEntries.stats}, '$.adoptions'), 0) + COALESCE(json_extract(${knowledgeEntries.stats}, '$.applications'), 0))`.as('totalUsage'),
})
.from(knowledgeEntries)
.where(and(eq(knowledgeEntries.lifecycle, 'active'), sql `(COALESCE(json_extract(${knowledgeEntries.stats}, '$.adoptions'), 0) + COALESCE(json_extract(${knowledgeEntries.stats}, '$.applications'), 0)) >= ${minUsage}`))
.orderBy(desc(sql `(COALESCE(json_extract(${knowledgeEntries.stats}, '$.adoptions'), 0) + COALESCE(json_extract(${knowledgeEntries.stats}, '$.applications'), 0))`))
.limit(limit)
.all();
}
/**
* 全库生命周期统计 (total / pending / deprecated)
* (SkillAdvisor.#getKBDistribution)
*/
async getLifecycleCounts() {
const row = this.#drizzle
.select({
total: count(),
pending: sql `SUM(CASE WHEN ${knowledgeEntries.lifecycle} = 'pending' THEN 1 ELSE 0 END)`.as('pending'),
deprecated: sql `SUM(CASE WHEN ${knowledgeEntries.lifecycle} = 'deprecated' THEN 1 ELSE 0 END)`.as('deprecated'),
})
.from(knowledgeEntries)
.get();
return {
total: Number(row?.total ?? 0),
pending: Number(row?.pending ?? 0),
deprecated: Number(row?.deprecated ?? 0),
};
}
/**
* 活跃 Recipe 信号 (SignalCollector.#collectRecipeSignals)
*/
async findActiveRecipeSignals(limit) {
return this.#drizzle
.select({
id: knowledgeEntries.id,
title: knowledgeEntries.title,
knowledgeType: knowledgeEntries.knowledgeType,
category: knowledgeEntries.category,
language: knowledgeEntries.language,
adoptionCount: sql `json_extract(${knowledgeEntries.stats}, '$.adoptions')`.as('adoptionCount'),
applicationCount: sql `json_extract(${knowledgeEntries.stats}, '$.applications')`.as('applicationCount'),
qualityOverall: sql `json_extract(${knowledgeEntries.quality}, '$.overall')`.as('qualityOverall'),
updatedAt: knowledgeEntries.updatedAt,
})
.from(knowledgeEntries)
.where(eq(knowledgeEntries.lifecycle, 'active'))
.orderBy(desc(knowledgeEntries.updatedAt))
.limit(limit)
.all();
}
/**
* 待审核 Candidate (SignalCollector.#collectCandidateSignals)
*/
async findPendingCandidates(limit) {
return this.#drizzle
.select({
id: knowledgeEntries.id,
source: knowledgeEntries.source,
status: knowledgeEntries.lifecycle,
language: knowledgeEntries.language,
category: knowledgeEntries.category,
title: knowledgeEntries.title,
createdAt: knowledgeEntries.createdAt,
})
.from(knowledgeEntries)
.where(eq(knowledgeEntries.lifecycle, 'pending'))
.orderBy(desc(knowledgeEntries.createdAt))
.limit(limit)
.all();
}
/* ═══ 行 ↔ 实体 映射 ═══════════════════════════════ */
/** DB Row → KnowledgeEntry (camelCase 列名 = 属性名,直传) */
_rowToEntity(row) {
if (!row) {
return null;
}
return new KnowledgeEntry({
...row,
// JSON 列需要 parse
lifecycleHistory: safeJsonParse(row.lifecycleHistory),
tags: safeJsonParse(row.tags),
content: safeJsonParse(row.content),
relations: safeJsonParse(row.relations),
constraints: safeJsonParse(row.constraints),
reasoning: safeJsonParse(row.reasoning),
quality: safeJsonParse(row.quality),
stats: safeJsonParse(row.stats),
headers: safeJsonParse(row.headers),
headerPaths: safeJsonParse(row.headerPaths),
agentNotes: safeJsonParse(row.agentNotes, null),
// SQLite INTEGER → boolean
autoApprovable: !!row.autoApprovable,
includeHeaders: !!row.includeHeaders,
// Staging support
stagingDeadline: row.stagingDeadline || null,
});
}
/** KnowledgeEntry → DB Row (camelCase 列名 = 属性名,直传) */
_entityToRow(e) {
const now = unixNow();
return {
id: e.id,
title: e.title,
description: e.description || '',
lifecycle: e.lifecycle,
lifecycleHistory: safeJsonStringify(e.lifecycleHistory || [], '[]'),
autoApprovable: e.autoApprovable ? 1 : 0,
language: e.language,
category: e.category,
kind: e.kind || inferKind(e.knowledgeType),
knowledgeType: e.knowledgeType || 'code-pattern',
complexity: e.complexity || 'intermediate',
scope: e.scope || null,
difficulty: e.difficulty || null,
tags: safeJsonStringify(e.tags || [], '[]'),
trigger: e.trigger || '',
topicHint: e.topicHint || '',
whenClause: e.whenClause || '',
doClause: e.doClause || '',
dontClause: e.dontClause || '',
coreCode: e.coreCode || '',
content: safeJsonStringify(e.content || {}),
relations: safeJsonStringify(e.relations || {}),
constraints: safeJsonStringify(e.constraints || {}),
reasoning: safeJsonStringify(e.reasoning || {}),
quality: safeJsonStringify(e.quality || {}),
stats: safeJsonStringify(e.stats || {}),
headers: safeJsonStringify(e.headers || [], '[]'),
headerPaths: safeJsonStringify(e.headerPaths || [], '[]'),
moduleName: e.moduleName || null,
includeHeaders: e.includeHeaders ? 1 : 0,
agentNotes: e.agentNotes ? safeJsonStringify(e.agentNotes) : null,
aiInsight: e.aiInsight || null,
reviewedBy: e.reviewedBy || null,
reviewedAt: e.reviewedAt || null,
rejectionReason: e.rejectionReason || null,
source: e.source || 'manual',
sourceFile: e.sourceFile || null,
sourceCandidateId: e.sourceCandidateId || null,
createdBy: e.createdBy || 'system',
createdAt: e.createdAt || now,
updatedAt: e.updatedAt || now,
publishedAt: e.publishedAt || null,
publishedBy: e.publishedBy || null,
staging_deadline: e.stagingDeadline || null,
};
}
/* ═══════════════════════════════════════════════════════
* SearchEngine 用同步方法
* ═══════════════════════════════════════════════════════ */
/** 查询所有非 deprecated 条目(buildIndex 用) */
findNonDeprecatedSync() {
return this.#drizzle
.select({
id: knowledgeEntries.id,
title: knowledgeEntries.title,
description: knowledgeEntries.description,
language: knowledgeEntries.language,
category: knowledgeEntries.category,
knowledgeType: knowledgeEntries.knowledgeType,
kind: knowledgeEntries.kind,
content: knowledgeEntries.content,
lifecycle: knowledgeEntries.lifecycle,
tags: knowledgeEntries.tags,
trigger: knowledgeEntries.trigger,
difficulty: knowledgeEntries.difficulty,
quality: knowledgeEntries.quality,
stats: knowledgeEntries.stats,
updatedAt: knowledgeEntries.updatedAt,
createdAt: knowledgeEntries.createdAt,
})
.from(knowledgeEntries)
.where(ne(knowledgeEntries.lifecycle, 'deprecated'))
.all();
}
/** LIKE 关键词搜索(_keywordSearch 用) */
keywordSearchSync(pattern, limit) {
return this.#drizzle
.select({
id: knowledgeEntries.id,
title: knowledgeEntries.title,
description: knowledgeEntries.description,
language: knowledgeEntries.language,
category: knowledgeEntries.category,
knowledgeType: knowledgeEntries.knowledgeType,
kind: knowledgeEntries.kind,
lifecycle: knowledgeEntries.lifecycle,
content: knowledgeEntries.content,
trigger: knowledgeEntries.trigger,
headers: knowledgeEntries.headers,
moduleName: knowledgeEntries.moduleName,
})
.from(knowledgeEntries)
.where(and(ne(knowledgeEntries.lifecycle, 'deprecated'), or(like(knowledgeEntries.title, pattern), like(knowledgeEntries.description, pattern), like(knowledgeEntries.trigger, pattern), like(knowledgeEntries.content, pattern))))
.limit(limit)
.all();
}
/** 按 ID 列表查询详情(_supplementDetails 用) */
findByIdsDetailSync(ids) {
if (ids.length === 0) {
return [];
}
return this.#drizzle
.select({
id: knowledgeEntries.id,
content: knowledgeEntries.content,
description: knowledgeEntries.description,
trigger: knowledgeEntries.trigger,
headers: knowledgeEntries.headers,
moduleName: knowledgeEntries.moduleName,
tags: knowledgeEntries.tags,
language: knowledgeEntries.language,
category: knowledgeEntries.category,
updatedAt: knowledgeEntries.updatedAt,
createdAt: knowledgeEntries.createdAt,
quality: knowledgeEntries.quality,
stats: knowledgeEntries.stats,
difficulty: knowledgeEntries.difficulty,
whenClause: knowledgeEntries.whenClause,
doClause: knowledgeEntries.doClause,
})
.from(knowledgeEntries)
.where(inArray(knowledgeEntries.id, ids))
.all();
}
/** 查询指定时间之后更新的条目(refreshIndex 用) */
findUpdatedSinceSync(sinceIso) {
const sinceEpoch = Math.floor(new Date(sinceIso).getTime() / 1000);
return this.#drizzle
.select({
id: knowledgeEntries.id,
title: knowledgeEntries.title,
description: knowledgeEntries.description,
language: knowledgeEntries.language,
category: knowledgeEntries.category,
knowledgeType: knowledgeEntries.knowledgeType,
kind: knowledgeEntries.kind,
content: knowledgeEntries.content,
lifecycle: knowledgeEntries.lifecycle,
tags: knowledgeEntries.tags,
trigger: knowledgeEntries.trigger,
difficulty: knowledgeEntries.difficulty,
quality: knowledgeEntries.quality,
stats: knowledgeEntries.stats,
updatedAt: knowledgeEntries.updatedAt,
createdAt: knowledgeEntries.createdAt,
})
.from(knowledgeEntries)
.where(gt(knowledgeEntries.updatedAt, sinceEpoch))
.all();
}
}
export default KnowledgeRepositoryImpl;