autosnippet
Version:
Extract code patterns into a knowledge base for AI coding assistants
476 lines (475 loc) • 18.6 kB
JavaScript
/**
* KnowledgeEdgeRepository — 知识图谱边的仓储实现
*
* 从 KnowledgeGraphService 提取的数据操作,使用 Drizzle 类型安全 API。
* KnowledgeGraphService 将从直接 db.prepare() 迁移为调用此 Repository。
*/
import { and, count, desc, eq, exists, inArray, like, or, sql } from 'drizzle-orm';
import { codeEntities, knowledgeEdges } from '../../infrastructure/database/drizzle/schema.js';
import { LanguageProfiles } from '../../shared/LanguageProfiles.js';
import { unixNow } from '../../shared/utils/common.js';
import { RepositoryBase } from '../base/RepositoryBase.js';
/* ═══ Repository 实现 ═══ */
export class KnowledgeEdgeRepositoryImpl extends RepositoryBase {
constructor(drizzle) {
super(drizzle, knowledgeEdges);
}
/* ─── 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) {
return this.upsertEdge(data);
}
async delete(id) {
const result = this.drizzle.delete(this.table).where(eq(this.table.id, id)).run();
return result.changes > 0;
}
/* ─── 核心操作 ─── */
/** INSERT OR REPLACE — 按 (fromId, fromType, toId, toType, relation) 唯一约束 upsert */
async upsertEdge(edge) {
const now = unixNow();
const metaJson = JSON.stringify(edge.metadata ?? {});
this.drizzle
.insert(this.table)
.values({
fromId: edge.fromId,
fromType: edge.fromType ?? 'recipe',
toId: edge.toId,
toType: edge.toType ?? 'recipe',
relation: edge.relation,
weight: edge.weight ?? 1.0,
metadataJson: metaJson,
createdAt: now,
updatedAt: now,
})
.onConflictDoUpdate({
target: [
this.table.fromId,
this.table.fromType,
this.table.toId,
this.table.toType,
this.table.relation,
],
set: {
weight: sql `${edge.weight ?? 1.0}`,
metadataJson: metaJson,
updatedAt: now,
},
})
.run();
// 返回 upserted 行
const rows = this.drizzle
.select()
.from(this.table)
.where(and(eq(this.table.fromId, edge.fromId), eq(this.table.fromType, edge.fromType ?? 'recipe'), eq(this.table.toId, edge.toId), eq(this.table.toType, edge.toType ?? 'recipe'), eq(this.table.relation, edge.relation)))
.limit(1)
.all();
return this.#mapRow(rows[0]);
}
/** 删除指定的边 */
async removeEdge(fromId, fromType, toId, toType, relation) {
this.drizzle
.delete(this.table)
.where(and(eq(this.table.fromId, fromId), eq(this.table.fromType, fromType), eq(this.table.toId, toId), eq(this.table.toType, toType), eq(this.table.relation, relation)))
.run();
}
/* ─── 查询 ─── */
/** 查询指定节点的出边 */
async findOutgoing(nodeId, nodeType) {
const rows = this.drizzle
.select()
.from(this.table)
.where(and(eq(this.table.fromId, nodeId), eq(this.table.fromType, nodeType)))
.all();
return rows.map((r) => this.#mapRow(r));
}
/** 查询指定节点的入边 */
async findIncoming(nodeId, nodeType) {
const rows = this.drizzle
.select()
.from(this.table)
.where(and(eq(this.table.toId, nodeId), eq(this.table.toType, nodeType)))
.all();
return rows.map((r) => this.#mapRow(r));
}
/** 查询指定节点的入边(仅限指定关系类型) */
async findIncomingByRelations(nodeId, nodeType, relations) {
const rows = this.drizzle
.select()
.from(this.table)
.where(and(eq(this.table.toId, nodeId), eq(this.table.toType, nodeType), inArray(this.table.relation, relations)))
.all();
return rows.map((r) => this.#mapRow(r));
}
/** 查询指定节点按特定关系的入边 */
async findIncomingByRelation(nodeId, relation) {
const rows = this.drizzle
.select()
.from(this.table)
.where(and(eq(this.table.toId, nodeId), eq(this.table.relation, relation)))
.all();
return rows.map((r) => this.#mapRow(r));
}
/** 查询指定节点按特定关系的出边 */
async findOutgoingByRelation(nodeId, relation) {
const rows = this.drizzle
.select()
.from(this.table)
.where(and(eq(this.table.fromId, nodeId), eq(this.table.relation, relation)))
.all();
return rows.map((r) => this.#mapRow(r));
}
/** 查询指定节点按关系+类型条件的出边(仅 to_id 字段) */
async findOutgoingToId(fromId, fromType, relation) {
const rows = this.drizzle
.select({ toId: this.table.toId })
.from(this.table)
.where(and(eq(this.table.fromId, fromId), eq(this.table.fromType, fromType), eq(this.table.relation, relation)))
.limit(1)
.all();
return rows.length > 0 ? rows[0].toId : null;
}
/** 查询指定节点按多类型条件的入边 */
async findIncomingByFromTypes(toId, toType, relation) {
const rows = this.drizzle
.select()
.from(this.table)
.where(and(eq(this.table.toId, toId), eq(this.table.toType, toType), eq(this.table.relation, relation)))
.all();
return rows.map((r) => this.#mapRow(r));
}
/** 查询 from_id 在指定类型列表中的遵循边 — 用于 getConformances */
async findConformances(fromId) {
const rows = this.drizzle
.select({ toId: this.table.toId })
.from(this.table)
.where(and(eq(this.table.fromId, fromId), inArray(this.table.fromType, ['class', 'category']), eq(this.table.relation, 'conforms')))
.all();
return rows.map((r) => r.toId);
}
/** 按关系分组统计 */
async countByRelation() {
const rows = this.drizzle
.select({
relation: this.table.relation,
cnt: count(),
})
.from(this.table)
.groupBy(this.table.relation)
.all();
const result = {};
for (const row of rows) {
result[row.relation] = row.cnt;
}
return result;
}
/** 获取入度最高的节点(被引用最多),排除多语言基类和框架根类 */
async getHotNodes(limit = 15) {
const exclusions = LanguageProfiles.baseClassExclusions;
const exclusionList = [...exclusions].map((v) => sql `${v}`);
const rows = this.drizzle
.select({
toId: this.table.toId,
toType: this.table.toType,
inDegree: count(),
})
.from(this.table)
.where(sql `${this.table.toId} NOT IN (${sql.join(exclusionList, sql `, `)})`)
.groupBy(this.table.toId, this.table.toType)
.orderBy(sql `count(*) DESC`)
.limit(limit)
.all();
return rows.map((r) => ({
id: r.toId,
type: r.toType,
inDegree: r.inDegree,
}));
}
/** 按关系类型统计某节点的入边数 */
async countIncomingByRelation(toId, relation) {
const [row] = this.drizzle
.select({ cnt: count() })
.from(this.table)
.where(and(eq(this.table.toId, toId), eq(this.table.relation, relation)))
.all();
return row?.cnt ?? 0;
}
/** 按关系类型查询总数 */
async countByRelationType(relation) {
const [row] = this.drizzle
.select({ cnt: count() })
.from(this.table)
.where(eq(this.table.relation, relation))
.all();
return row?.cnt ?? 0;
}
/** 按 metadata_json LIKE 模式删除边(可选过滤关系类型) */
async deleteByMetadataLike(pattern, relations) {
const conditions = [like(this.table.metadataJson, pattern)];
if (relations?.length) {
conditions.push(inArray(this.table.relation, relations));
}
const result = this.drizzle
.delete(this.table)
.where(and(...conditions))
.run();
return result.changes;
}
/** 删除指定节点的所有出边(按 fromId + fromType) */
async deleteOutgoing(fromId, fromType) {
const result = this.drizzle
.delete(this.table)
.where(and(eq(this.table.fromId, fromId), eq(this.table.fromType, fromType)))
.run();
return result.changes;
}
/** 根据 entry ID 删除所有相关边(用于知识删除时清理图谱) */
async deleteByEntryId(entryId) {
const result = this.drizzle
.delete(this.table)
.where(or(eq(this.table.fromId, entryId), eq(this.table.toId, entryId)))
.run();
return result.changes;
}
/** 按关系类型查询 */
async findByRelation(nodeId, nodeType, relation) {
const rows = this.drizzle
.select()
.from(this.table)
.where(or(and(eq(this.table.fromId, nodeId), eq(this.table.fromType, nodeType), eq(this.table.relation, relation)), and(eq(this.table.toId, nodeId), eq(this.table.toType, nodeType), eq(this.table.relation, relation))))
.all();
return rows.map((r) => this.#mapRow(r));
}
/** 获取所有边(可选类型过滤 + 限制数量) */
async findAll(options = {}) {
const { nodeType, limit = 1000 } = options;
let query = this.drizzle.select().from(this.table);
if (nodeType) {
query = query.where(and(eq(this.table.fromType, nodeType), eq(this.table.toType, nodeType)));
}
const rows = query.limit(limit).all();
return rows.map((r) => this.#mapRow(r));
}
/** 统计信息 */
async getStats(nodeType) {
// 总数
const condition = nodeType
? and(eq(this.table.fromType, nodeType), eq(this.table.toType, nodeType))
: undefined;
const [totalRow] = this.drizzle
.select({ cnt: count() })
.from(this.table)
.where(condition)
.all();
const totalEdges = totalRow?.cnt ?? 0;
// 按关系类型分组
const relationRows = this.drizzle
.select({
relation: this.table.relation,
cnt: count(),
})
.from(this.table)
.where(condition)
.groupBy(this.table.relation)
.all();
const byRelation = {};
for (const row of relationRows) {
byRelation[row.relation] = row.cnt;
}
// 节点类型
const fromTypes = this.drizzle
.selectDistinct({ t: this.table.fromType })
.from(this.table)
.all()
.map((r) => r.t);
const toTypes = this.drizzle
.selectDistinct({ t: this.table.toType })
.from(this.table)
.all()
.map((r) => r.t);
const nodeTypes = [...new Set([...fromTypes, ...toTypes])];
return { totalEdges, byRelation, nodeTypes };
}
/** 在事务中执行批量边操作 */
async batchInTransaction(fn) {
this.transaction(fn);
}
/* ─── Panorama 域查询 (Phase 5e) ─── */
/**
* 统计 knowledge_edges JOIN code_entities 的边数 (fan-in/fan-out 分析)
* direction='from': JOIN on from_id 侧 (fan-out: 模块内实体发出的边)
* direction='to': JOIN on to_id 侧 (fan-in: 模块内实体接收的边)
*/
async countEdgesJoinedByEntityFiles(projectRoot, filePaths, relation, direction) {
if (filePaths.length === 0) {
return 0;
}
const joinOn = direction === 'from'
? and(eq(this.table.fromId, codeEntities.entityId), eq(this.table.fromType, codeEntities.entityType))
: and(eq(this.table.toId, codeEntities.entityId), eq(this.table.toType, codeEntities.entityType));
const [row] = this.drizzle
.select({ cnt: count() })
.from(this.table)
.innerJoin(codeEntities, joinOn)
.where(and(eq(codeEntities.projectRoot, projectRoot), inArray(codeEntities.filePath, filePaths), eq(this.table.relation, relation)))
.all();
return row?.cnt ?? 0;
}
/**
* 查询实体使用的设计模式名称 (uses_pattern 边)
* 限定实体在指定项目的指定文件路径内
*/
async findPatternsUsedByEntities(projectRoot, filePaths) {
if (filePaths.length === 0) {
return [];
}
const rows = this.drizzle
.select({ patternName: this.table.toId })
.from(this.table)
.innerJoin(codeEntities, eq(this.table.fromId, codeEntities.entityId))
.where(and(eq(codeEntities.projectRoot, projectRoot), eq(this.table.relation, 'uses_pattern'), inArray(codeEntities.filePath, filePaths)))
.all();
return rows.map((r) => r.patternName);
}
/** 最频繁被调用的节点 (calls 关系 GROUP BY to_id) */
async findTopCalledNodes(limit) {
const rows = this.drizzle
.select({
toId: this.table.toId,
callCount: count(),
})
.from(this.table)
.where(eq(this.table.relation, 'calls'))
.groupBy(this.table.toId)
.orderBy(desc(count()))
.limit(limit)
.all();
return rows.map((r) => ({ toId: r.toId, callCount: r.callCount }));
}
/** 入口点: 只有 calls 出度没有 calls 入度的节点 */
async findEntryPoints(limit) {
const rows = this.drizzle
.selectDistinct({ fromId: this.table.fromId })
.from(this.table)
.where(and(eq(this.table.relation, 'calls'), sql `${this.table.fromId} NOT IN (SELECT ${this.table.toId} FROM ${this.table} WHERE ${this.table.relation} = 'calls')`))
.limit(limit)
.all();
return rows.map((r) => r.fromId);
}
/** 数据生产者: data_flow 出度 > threshold 的节点 */
async findTopDataFlowSources(limit, threshold) {
const rows = this.drizzle
.select({
fromId: this.table.fromId,
outCnt: count(),
})
.from(this.table)
.where(eq(this.table.relation, 'data_flow'))
.groupBy(this.table.fromId)
.having(sql `count(*) > ${threshold}`)
.orderBy(desc(count()))
.limit(limit)
.all();
return rows.map((r) => r.fromId);
}
/** 数据消费者: data_flow 入度 > threshold 的节点 */
async findTopDataFlowSinks(limit, threshold) {
const rows = this.drizzle
.select({
toId: this.table.toId,
inCnt: count(),
})
.from(this.table)
.where(eq(this.table.relation, 'data_flow'))
.groupBy(this.table.toId)
.having(sql `count(*) > ${threshold}`)
.orderBy(desc(count()))
.limit(limit)
.all();
return rows.map((r) => r.toId);
}
/**
* 查询指定关系的边,过滤条件:from 侧是 module 或在指定项目的 code_entities 中存在
* (用于 CouplingAnalyzer 构建模块间依赖边)
*/
async findEdgesFilteredByEntityExistence(relation, projectRoot) {
const rows = this.drizzle
.select({
fromId: this.table.fromId,
fromType: this.table.fromType,
toId: this.table.toId,
toType: this.table.toType,
})
.from(this.table)
.where(and(eq(this.table.relation, relation), or(eq(this.table.fromType, 'module'), exists(this.drizzle
.select({ one: sql `1` })
.from(codeEntities)
.where(and(eq(codeEntities.entityId, this.table.fromId), eq(codeEntities.projectRoot, projectRoot)))))))
.all();
return rows;
}
/** 查询 module→module 的 depends_on 边 (fromId, toId) */
async findModuleDependencyPairs() {
const rows = this.drizzle
.select({
fromId: this.table.fromId,
toId: this.table.toId,
})
.from(this.table)
.where(and(eq(this.table.relation, 'depends_on'), eq(this.table.fromType, 'module'), eq(this.table.toType, 'module')))
.all();
return rows;
}
/** 批量 INSERT OR IGNORE 边 (不更新已存在的行) */
async bulkInsertIgnore(edges) {
if (edges.length === 0) {
return 0;
}
let inserted = 0;
const now = unixNow();
this.transaction((tx) => {
for (const edge of edges) {
tx.insert(this.table)
.values({
fromId: edge.fromId,
fromType: edge.fromType ?? 'entity',
toId: edge.toId,
toType: edge.toType ?? 'recipe',
relation: edge.relation,
weight: edge.weight ?? 1.0,
metadataJson: JSON.stringify(edge.metadata ?? {}),
createdAt: now,
updatedAt: now,
})
.onConflictDoNothing()
.run();
inserted++;
}
});
return inserted;
}
/* ─── 内部辅助 ─── */
#mapRow(row) {
let metadata = {};
try {
metadata = JSON.parse(row.metadataJson ?? '{}');
}
catch {
/* ignore parse errors */
}
return {
id: row.id,
fromId: row.fromId,
fromType: row.fromType,
toId: row.toId,
toType: row.toType,
relation: row.relation,
weight: row.weight ?? 1.0,
metadata,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
};
}
}