UNPKG

autosnippet

Version:

Extract code patterns into a knowledge base for AI coding assistants

885 lines (884 loc) 34.6 kB
/** * CodeEntityGraph — 代码实体关系图谱 * * Phase E: 在 Semantic Memory 之上构建代码实体图谱 * * 节点类型: * - class : ObjC @interface / Swift class/struct * - protocol : ObjC @protocol / Swift protocol * - category : ObjC Category / Swift Extension * - module : SPM/CocoaPods module * - pattern : 设计模式 (singleton, delegate, etc.) * * 边类型 (复用 knowledge_edges 表): * - inherits : 类继承 * - conforms : 协议遵循 * - extends : Category/Extension * - depends_on : 模块依赖 * - uses_pattern: 使用设计模式 * - is_part_of : 属于模块 * - calls : 方法调用 (Phase 5) * - data_flow : 数据流向 (Phase 5) * * @module CodeEntityGraph */ import Logger from '../../infrastructure/logging/Logger.js'; const logger = Logger.getInstance(); export class CodeEntityGraph { projectRoot; #entityRepo; #edgeRepo; log; constructor(entityRepo, edgeRepo, options = {}) { this.#entityRepo = entityRepo; this.#edgeRepo = edgeRepo; this.projectRoot = options.projectRoot || ''; this.log = options.logger || logger; } // ──────────────────────────────────────────── // Public API — 图谱构建 // ──────────────────────────────────────────── /** * 从 AST ProjectAstSummary 填充图谱 (Phase 1.5 → Phase 1.6) * * 写入: class/protocol/category 实体 + inherits/conforms/extends 边 * * @param astSummary analyzeProject() 产出的 ProjectAstSummary */ async populateFromAst(astSummary) { if (!astSummary) { return { entitiesUpserted: 0, edgesCreated: 0, durationMs: 0 }; } const t0 = Date.now(); let entities = 0; let edges = 0; // ── 类 ── for (const cls of astSummary.classes || []) { await this.#upsertEntity({ entityId: cls.name, entityType: cls.isCategory ? 'category' : 'class', name: cls.name, filePath: cls.file || null, line: cls.line || null, superclass: cls.superclass || null, protocols: cls.protocols || [], metadata: { endLine: cls.endLine, isCategory: cls.isCategory || false, }, }); entities++; } // ── 协议 ── for (const proto of astSummary.protocols || []) { await this.#upsertEntity({ entityId: proto.name, entityType: 'protocol', name: proto.name, filePath: proto.file || null, line: proto.line || null, protocols: proto.inherits || [], metadata: { methodCount: proto.methods?.length || 0, }, }); entities++; } // ── Category ── for (const cat of astSummary.categories || []) { const catId = `${cat.className}(${cat.categoryName})`; await this.#upsertEntity({ entityId: catId, entityType: 'category', name: catId, filePath: cat.file || null, line: cat.line || null, protocols: cat.protocols || [], metadata: { className: cat.className, categoryName: cat.categoryName, methodCount: cat.methods?.length || 0, }, }); entities++; } // ── 继承/遵循/扩展 边 (从 AST inheritanceGraph) ── for (const edge of astSummary.inheritanceGraph || []) { const fromType = this.#inferEntityType(edge.from, astSummary); const toType = this.#inferEntityType(edge.to, astSummary); await this.#addEdge(edge.from, fromType, edge.to, toType, edge.type, { weight: 1.0, source: 'ast-bootstrap', }); edges++; } // ── 设计模式 (从 patternStats) ── for (const [patternType, stat] of Object.entries(astSummary.patternStats || {})) { const patternId = `pattern:${patternType}`; await this.#upsertEntity({ entityId: patternId, entityType: 'pattern', name: patternType, metadata: { count: stat.count, files: stat.files?.slice(0, 10), }, }); entities++; // 实例 → uses_pattern 边 for (const inst of (stat.instances || []).slice(0, 50)) { const className = inst.className || inst.name; if (className) { await this.#addEdge(className, 'class', patternId, 'pattern', 'uses_pattern', { weight: 0.8, source: 'ast-pattern-detection', file: inst.file, }); edges++; } } } const result = { entitiesUpserted: entities, edgesCreated: edges, durationMs: Date.now() - t0 }; this.log.info(`[CodeEntityGraph] AST populate: ${entities} entities, ${edges} edges (${result.durationMs}ms)`); return result; } /** * 从 SPM 依赖图填充模块实体 (Phase 2) * * 当前 bootstrap.js 已将 SPM 边写入 knowledge_edges, * 此方法补充 module 实体节点。 * * @param depGraphData spm.getDependencyGraph() 产出 */ async populateFromSpm(depGraphData) { if (!depGraphData) { return { entitiesUpserted: 0, edgesCreated: 0, durationMs: 0 }; } const t0 = Date.now(); let entities = 0; for (const node of depGraphData.nodes || []) { const nodeObj = typeof node === 'string' ? { id: node, label: node } : node; await this.#upsertEntity({ entityId: nodeObj.id || nodeObj.label || String(node), entityType: 'module', name: nodeObj.label || nodeObj.id || String(node), metadata: { nodeType: nodeObj.type || 'module', ...(nodeObj.layer != null ? { layer: nodeObj.layer } : {}), ...(nodeObj.version != null ? { version: nodeObj.version } : {}), ...(nodeObj.group != null ? { group: nodeObj.group } : {}), ...(nodeObj.fullPath != null ? { fullPath: nodeObj.fullPath } : {}), ...(nodeObj.indirect != null ? { indirect: nodeObj.indirect } : {}), }, }); entities++; } // 存储 layers 元数据(如果存在)到特殊实体 const layers = depGraphData.layers; if (layers?.length) { await this.#upsertEntity({ entityId: '__config_layers__', entityType: 'config', name: 'Config Layers', metadata: { layers }, }); entities++; } const result = { entitiesUpserted: entities, edgesCreated: 0, durationMs: Date.now() - t0 }; this.log.info(`[CodeEntityGraph] SPM populate: ${entities} module entities (${result.durationMs}ms)`); return result; } /** * 从候选的 Relations 字段提取边写入图谱 (Phase 5/6) * * @param candidates 扁平关系数组或 Relations 对象 */ async populateFromCandidateRelations(candidates) { if (!candidates?.length) { return { entitiesUpserted: 0, edgesCreated: 0, durationMs: 0 }; } const t0 = Date.now(); let edges = 0; for (const candidate of candidates) { const title = candidate.title || candidate.id || ''; if (!title) { continue; } // 处理 Relations 对象或扁平数组 let flatRelations; const rels = candidate.relations; if (typeof rels?.toFlatArray === 'function') { flatRelations = rels.toFlatArray(); } else if (Array.isArray(candidate.relations)) { flatRelations = candidate.relations; } else if (candidate.relations && typeof candidate.relations === 'object') { // 桶结构 → 扁平 flatRelations = []; for (const [type, list] of Object.entries(candidate.relations)) { for (const r of Array.isArray(list) ? list : []) { flatRelations.push({ type, target: r.target, description: r.description }); } } } else { continue; } for (const rel of flatRelations) { if (!rel.target) { continue; } // 映射关系类型到边类型 const relation = this.#mapRelationType(rel.type); await this.#addEdge(title, 'recipe', rel.target, 'recipe', relation, { weight: 0.7, source: 'candidate-relations', description: rel.description || '', }); edges++; } } const result = { entitiesUpserted: 0, edgesCreated: edges, durationMs: Date.now() - t0 }; this.log.info(`[CodeEntityGraph] Candidate relations: ${edges} edges (${result.durationMs}ms)`); return result; } // ──────────────────────────────────────────── // Public API — 图谱查询 // ──────────────────────────────────────────── /** 获取单个实体信息 */ async getEntity(entityId, entityType) { let entity; if (entityType) { entity = await this.#entityRepo.findByEntityId(entityId, entityType, this.projectRoot); } else { entity = await this.#entityRepo.findByEntityIdOnly(entityId, this.projectRoot); } return entity ? this.#mapRepoEntity(entity) : null; } /** * 按类型列出所有实体 * @param entityType 'class'|'protocol'|'category'|'module'|'pattern' */ async listEntities(entityType, limit = 200) { const entities = await this.#entityRepo.listByType(entityType, this.projectRoot, limit); return entities.map((e) => this.#mapRepoEntity(e)); } /** * 搜索实体 (名称模糊匹配) * @param [options.type] 过滤类型 */ async searchEntities(query, options = {}) { const entities = await this.#entityRepo.searchByName(query, this.projectRoot, { entityType: options.type, limit: options.limit || 20, }); return entities.map((e) => this.#mapRepoEntity(e)); } /** * 获取实体的所有关系边 */ async getEntityEdges(entityId, entityType, direction = 'both') { const outgoing = direction === 'both' || direction === 'out' ? await this.#edgeRepo.findOutgoing(entityId, entityType) : []; const incoming = direction === 'both' || direction === 'in' ? await this.#edgeRepo.findIncoming(entityId, entityType) : []; return { outgoing: outgoing.map((e) => this.#mapRepoEdge(e)), incoming: incoming.map((e) => this.#mapRepoEdge(e)), }; } /** * 获取继承链 (向上遍历 inherits 边) * @returns 继承链 [class, parent, grandparent, ...] */ async getInheritanceChain(className, maxDepth = 10) { const chain = [className]; let current = className; for (let i = 0; i < maxDepth; i++) { const parentId = await this.#edgeRepo.findOutgoingToId(current, 'class', 'inherits'); if (!parentId) { break; } chain.push(parentId); current = parentId; } return chain; } /** * 获取所有子类/实现者 (向下遍历) * @param entityType 'class'|'protocol' */ async getDescendants(entityId, entityType, maxDepth = 3) { const results = []; const visited = new Set(); const queue = [{ id: entityId, type: entityType, depth: 0 }]; // 类的子类/Category + 协议的遵循者 const relations = entityType === 'protocol' ? ['conforms', 'inherits'] : ['inherits', 'extends']; while (queue.length > 0) { const current = queue.shift(); if (!current) { break; } const { id, type, depth } = current; if (depth >= maxDepth) { continue; } const key = `${type}:${id}`; if (visited.has(key)) { continue; } visited.add(key); for (const rel of relations) { const children = await this.#edgeRepo.findIncomingByFromTypes(id, type, rel); for (const child of children) { const childKey = `${child.fromType}:${child.fromId}`; if (!visited.has(childKey)) { results.push({ id: child.fromId, type: child.fromType, depth: depth + 1, relation: rel, }); queue.push({ id: child.fromId, type: child.fromType, depth: depth + 1, }); } } } } return results; } /** 获取协议遵循关系 (className → 遵循的协议列表) */ async getConformances(className) { return this.#edgeRepo.findConformances(className); } /** * 查找两个实体间的路径 (BFS) */ async findPath(fromId, fromType, toId, toType, maxDepth = 5) { const visited = new Set(); const queue = [ { id: fromId, type: fromType, path: [], }, ]; while (queue.length > 0) { const current = queue.shift(); if (!current) { break; } const { id, type, path } = current; if (path.length >= maxDepth) { continue; } const key = `${type}:${id}`; if (visited.has(key)) { continue; } visited.add(key); const neighbors = await this.#edgeRepo.findOutgoing(id, type); for (const n of neighbors) { const step = { from: { id, type }, to: { id: n.toId, type: n.toType }, relation: n.relation, }; const newPath = [...path, step]; if (n.toId === toId && n.toType === toType) { return { found: true, path: newPath, depth: newPath.length }; } queue.push({ id: n.toId, type: n.toType, path: newPath }); } } return { found: false, path: [], depth: -1, }; } /** * 影响分析: 修改某实体后,哪些实体可能受影响 */ async getImpactRadius(entityId, entityType, maxDepth = 3) { const impacted = []; const visited = new Set(); const queue = [{ id: entityId, type: entityType, depth: 0 }]; while (queue.length > 0) { const current = queue.shift(); if (!current) { break; } const { id, type, depth } = current; if (depth >= maxDepth) { continue; } const key = `${type}:${id}`; if (visited.has(key)) { continue; } visited.add(key); // 找出所有"依赖/引用此实体"的上游 const dependents = await this.#edgeRepo.findIncoming(id, type); for (const dep of dependents) { const depKey = `${dep.fromType}:${dep.fromId}`; if (!visited.has(depKey)) { impacted.push({ id: dep.fromId, type: dep.fromType, relation: dep.relation, depth: depth + 1, }); queue.push({ id: dep.fromId, type: dep.fromType, depth: depth + 1, }); } } } return impacted; } /** 项目拓扑概览 — 统计信息 + 关键度排名 */ async getTopology() { const entityStats = await this.#entityRepo.countByType(this.projectRoot); const edgeStats = await this.#edgeRepo.countByRelation(); const hotNodes = await this.#edgeRepo.getHotNodes(15); const totalEntities = Object.values(entityStats).reduce((sum, c) => sum + c, 0); const totalEdges = Object.values(edgeStats).reduce((sum, c) => sum + c, 0); return { entities: entityStats, edges: edgeStats, totalEntities, totalEdges, hotNodes: hotNodes.map((n) => ({ id: n.id, type: n.type, inDegree: n.inDegree, })), }; } /** 生成 Agent 可用的图谱上下文 (Markdown) */ async generateContextForAgent(options = {}) { const maxEntities = options.maxEntities || 30; const topo = await this.getTopology(); if (topo.totalEntities === 0) { return ''; } const lines = [ '## 代码实体图谱 (Code Entity Graph)', '', `### 统计`, ...Object.entries(topo.entities).map(([t, c]) => `- ${t}: ${c}`), `- 总边数: ${topo.totalEdges}`, '', ]; // 核心实体 (入度最高) if (topo.hotNodes.length > 0) { lines.push('### 核心实体 (被依赖最多)'); for (const n of topo.hotNodes.slice(0, 10)) { lines.push(`- \`${n.id}\` (${n.type}, 入度=${n.inDegree})`); } lines.push(''); } // 类继承概览 const classes = await this.listEntities('class', maxEntities); if (classes.length > 0) { lines.push('### 类继承关系'); for (const cls of classes) { const chain = await this.getInheritanceChain(cls.entityId, 5); if (chain.length > 1) { lines.push(`- \`${chain.join(' → ')}\``); } } lines.push(''); } // 协议 const protocols = await this.listEntities('protocol', 15); if (protocols.length > 0) { lines.push('### 协议'); for (const p of protocols) { const conformers = await this.getDescendants(p.entityId, 'protocol', 1); const cNames = conformers.map((c) => c.id).slice(0, 5); lines.push(`- \`${p.name}\` ← ${cNames.length > 0 ? cNames.map((n) => `\`${n}\``).join(', ') : '(无遵循者)'}`); } lines.push(''); } // 调用图热路径 (Phase 5) try { const hotCallees = await this.#edgeRepo.getHotNodes(15); // Filter for 'calls' relation — use countIncomingByRelation for each const callHotPaths = []; for (const node of hotCallees) { const callCount = await this.#edgeRepo.countIncomingByRelation(node.id, 'calls'); if (callCount > 0) { const topCallers = await this.#edgeRepo.findIncomingByRelation(node.id, 'calls'); const callerNames = topCallers .slice(0, 3) .map((c) => `\`${c.fromId}\``) .join(', '); callHotPaths.push({ toId: node.id, callCount, callerNames: `${callerNames}${topCallers.length > 3 ? '...' : ''}`, }); } } if (callHotPaths.length > 0) { lines.push('### 调用图热路径 (Call Graph Hot Paths)'); for (const row of callHotPaths.slice(0, 15)) { lines.push(`- \`${row.toId}\` ← ${row.callCount} 次调用 (${row.callerNames})`); } lines.push(''); } // 数据流边摘要 const dataFlowCount = await this.#edgeRepo.countByRelationType('data_flow'); if (dataFlowCount > 0) { lines.push(`### 数据流`); lines.push(`- 数据流边: ${dataFlowCount} 条`); lines.push(''); } } catch (_e) { // 调用图数据可能尚未填充, 静默跳过 } return lines.join('\n'); } // ──────────────────────────────────────────── // Public API — Phase 5: 调用图 // ──────────────────────────────────────────── /** * 从解析后的调用边填充图谱 (Phase 5) * * @param callEdges * @param dataFlowEdges */ async populateCallGraph(callEdges, dataFlowEdges) { const t0 = Date.now(); let edges = 0; let entities = 0; // ── 注册方法实体 (确保 from/to 的 entity 存在) ── const registeredMethods = new Set(); for (const edge of callEdges) { for (const fqn of [edge.caller, edge.callee]) { if (registeredMethods.has(fqn)) { continue; } registeredMethods.add(fqn); const entityId = this._extractEntityId(fqn); const entityName = entityId; // 短名 const filePath = fqn.includes('::') ? fqn.split('::')[0] : null; await this.#upsertEntity({ entityId, entityType: 'method', name: entityName, filePath, metadata: { fqn, source: 'phase5-call-graph' }, }); entities++; } } // ── 调用边 (聚合同一 caller-callee 对的多次调用,解决 Issue #4) ── const aggregated = new Map(); // key = "callerId|calleeId" → aggregated metadata for (const edge of callEdges) { const callerId = this._extractEntityId(edge.caller); const calleeId = this._extractEntityId(edge.callee); const key = `${callerId}|${calleeId}`; if (aggregated.has(key)) { const agg = aggregated.get(key); agg.callCount++; agg.callSites.push({ line: edge.line, isAwait: edge.isAwait }); // 提升权重: direct 优先 if (edge.resolveMethod === 'direct') { agg.resolveMethod = 'direct'; } if (edge.isAwait) { agg.hasAwait = true; } } else { aggregated.set(key, { callerId, calleeId, callType: edge.callType, resolveMethod: edge.resolveMethod, file: edge.file, hasAwait: edge.isAwait, callCount: 1, callSites: [{ line: edge.line, isAwait: edge.isAwait }], }); } } for (const agg of aggregated.values()) { await this.#addEdge(agg.callerId, 'method', agg.calleeId, 'method', 'calls', { weight: agg.resolveMethod === 'direct' ? 1.0 : 0.6, source: 'phase5-call-graph', callType: agg.callType, resolveMethod: agg.resolveMethod, file: agg.file, isAwait: agg.hasAwait, callCount: agg.callCount, callSites: agg.callSites.slice(0, 10), // 最多保留 10 个调用点 }); edges++; } // ── 数据流边 ── for (const flow of dataFlowEdges) { const fromId = this._extractEntityId(flow.from || ''); const toId = this._extractEntityId(flow.to || ''); await this.#addEdge(fromId, 'method', toId, 'method', 'data_flow', { weight: 0.5, source: 'phase5-data-flow', flowType: flow.flowType || '', direction: flow.direction || '', }); edges++; } const result = { entitiesUpserted: entities, edgesCreated: edges, durationMs: Date.now() - t0 }; this.log.info(`[CodeEntityGraph] Call graph: ${callEdges.length} call edges, ${dataFlowEdges.length} data flow edges, ${entities} method entities (${result.durationMs}ms)`); return result; } /** * 获取调用者 — 谁调用了这个方法? * * @param methodId "ClassName.methodName" 或 FQN * @returns >} */ async getCallers(methodId, maxDepth = 2) { const results = []; const visited = new Set(); const queue = [{ id: methodId, depth: 0 }]; while (queue.length > 0) { const current = queue.shift(); if (!current) { break; } const { id, depth } = current; if (depth >= maxDepth || visited.has(id)) { continue; } visited.add(id); const callers = await this.#edgeRepo.findIncomingByRelation(id, 'calls'); for (const edge of callers) { results.push({ caller: edge.fromId, depth: depth + 1, callType: edge.metadata?.callType || 'unknown', }); if (depth + 1 < maxDepth) { queue.push({ id: edge.fromId, depth: depth + 1 }); } } } return results; } /** * 获取被调用者 — 这个方法调用了谁? * * @param methodId "ClassName.methodName" 或 FQN * @returns >} */ async getCallees(methodId, maxDepth = 2) { const results = []; const visited = new Set(); const queue = [{ id: methodId, depth: 0 }]; while (queue.length > 0) { const current = queue.shift(); if (!current) { break; } const { id, depth } = current; if (depth >= maxDepth || visited.has(id)) { continue; } visited.add(id); const callees = await this.#edgeRepo.findOutgoingByRelation(id, 'calls'); for (const edge of callees) { results.push({ callee: edge.toId, depth: depth + 1, callType: edge.metadata?.callType || 'unknown', }); if (depth + 1 < maxDepth) { queue.push({ id: edge.toId, depth: depth + 1 }); } } } return results; } /** * 获取方法的 Impact Radius (基于调用图) * — 修改此方法可能影响哪些上游方法? * * @param methodId "ClassName.methodName" * @returns } */ async getCallImpactRadius(methodId) { const callers = await this.getCallers(methodId, 3); const affectedFiles = new Set(); for (const c of callers) { const entity = await this.getEntity(c.caller, 'method'); if (entity?.filePath) { affectedFiles.add(entity.filePath); } } return { directCallers: callers.filter((c) => c.depth === 1).length, transitiveCallers: callers.length, affectedFiles: [...affectedFiles], }; } /** * 从 FQN 中提取短 Entity ID * * "src/service/UserService.ts::UserService.getUser" → "UserService.getUser" * "src/utils/helpers.ts::formatDate" → "formatDate" */ _extractEntityId(fqn) { if (fqn.includes('::')) { return fqn.split('::')[1]; } return fqn; } /** 清除项目的所有代码实体 (重新 populate 前调用) */ async clearProject() { await this.#entityRepo.clearProject(this.projectRoot); // 清除 AST 产出的边 + Phase 5 调用图边 (保留 recipe/module 边) await this.#edgeRepo.deleteByMetadataLike('%ast-bootstrap%'); await this.#edgeRepo.deleteByMetadataLike('%ast-pattern-detection%'); await this.#edgeRepo.deleteByMetadataLike('%phase5-%'); this.log.info(`[CodeEntityGraph] Cleared entities for project: ${this.projectRoot}`); } /** * 增量清除 — 仅删除指定文件的 call graph 边和 method 实体 * * @param filePaths 变更文件的相对路径列表 * @returns } */ async clearCallGraphForFiles(filePaths) { if (!filePaths?.length) { return { deletedEdges: 0, deletedEntities: 0 }; } let deletedEdges = 0; let deletedEntities = 0; // 1. 删除相关 call edges (metadata_json 包含 file 字段) for (const filePath of filePaths) { // 匹配 metadata 中 "file":"xxx" 字段 const changes = await this.#edgeRepo.deleteByMetadataLike(`%"file":"${filePath}"%`, [ 'calls', 'data_flow', ]); deletedEdges += changes; } // 2. 删除相关 method 实体 for (const filePath of filePaths) { const changes = await this.#entityRepo.deleteByFileAndType(filePath, 'method', this.projectRoot); deletedEntities += changes; } this.log.info(`[CodeEntityGraph] Incremental clear: ${deletedEdges} edges, ${deletedEntities} entities ` + `for ${filePaths.length} files`); return { deletedEdges, deletedEntities }; } // ──────────────────────────────────────────── // Private — Helpers // ──────────────────────────────────────────── async #upsertEntity(entity) { await this.#entityRepo.upsert({ entityId: entity.entityId, entityType: entity.entityType, projectRoot: this.projectRoot, name: entity.name, filePath: entity.filePath ?? null, lineNumber: entity.line ?? null, superclass: entity.superclass ?? null, protocols: entity.protocols ?? [], metadata: entity.metadata ?? {}, }); } async #addEdge(fromId, fromType, toId, toType, relation, metadata = {}) { try { await this.#edgeRepo.upsertEdge({ fromId, fromType, toId, toType, relation, weight: metadata.weight || 1.0, metadata, }); } catch (err) { // Ignore duplicate edge errors if (err instanceof Error && !err.message.includes('UNIQUE constraint')) { this.log.warn(`[CodeEntityGraph] addEdge failed: ${err.message}`); } } } /** 从 AST 数据推断实体类型 */ #inferEntityType(name, astSummary) { if (!name) { return 'class'; // guard against undefined } if (astSummary.protocols?.some((p) => p.name === name)) { return 'protocol'; } if (name.includes('(') && name.includes(')')) { return 'category'; } return 'class'; } /** 映射 Relations 桶名到图谱边类型 */ #mapRelationType(type) { const mapping = { inherits: 'inherits', implements: 'conforms', calls: 'calls', depends_on: 'depends_on', data_flow: 'data_flow', conflicts: 'conflicts', extends: 'extends', related: 'related', alternative: 'related', prerequisite: 'depends_on', deprecated_by: 'related', solves: 'related', enforces: 'enforces', references: 'references', }; return mapping[type] || 'related'; } #mapRepoEdge(edge) { return { fromId: edge.fromId, fromType: edge.fromType, toId: edge.toId, toType: edge.toType, relation: edge.relation, weight: edge.weight, metadata: edge.metadata, }; } #mapRepoEntity(entity) { return { entityId: entity.entityId, entityType: entity.entityType, name: entity.name, filePath: entity.filePath, line: entity.lineNumber, superclass: entity.superclass, protocols: entity.protocols, metadata: entity.metadata, projectRoot: entity.projectRoot, createdAt: entity.createdAt, updatedAt: entity.updatedAt, }; } } export default CodeEntityGraph;