Persist the symbol graph: per-file payloads + sharded indices + meta.
( projectId: string, resolvedPath: string, symbolsByFile: Map<string, SymbolNode[]>, outgoingCallsByFile: Map<string, SymbolEdge[]>, )
| 248 | |
| 249 | /** Persist the symbol graph: per-file payloads + sharded indices + meta. */ |
| 250 | async function persistSymbolGraph( |
| 251 | projectId: string, |
| 252 | resolvedPath: string, |
| 253 | symbolsByFile: Map<string, SymbolNode[]>, |
| 254 | outgoingCallsByFile: Map<string, SymbolEdge[]>, |
| 255 | ): Promise<void> { |
| 256 | await ensureSymbolGraphCollections(projectId); |
| 257 | |
| 258 | // Build per-file payloads (need source bytes for contentHash). |
| 259 | const payloads: SymbolGraphFilePayload[] = []; |
| 260 | let totalSymbols = 0; |
| 261 | let totalEdges = 0; |
| 262 | for (const [relPath, symbols] of symbolsByFile.entries()) { |
| 263 | const outgoingCalls = outgoingCallsByFile.get(relPath) ?? []; |
| 264 | let language = "plaintext"; |
| 265 | const firstNonModule = symbols.find((s) => s.name !== "<module>"); |
| 266 | if (firstNonModule) language = firstNonModule.language; |
| 267 | else language = symbols[0]?.language ?? language; |
| 268 | |
| 269 | let contentHash = ""; |
| 270 | try { |
| 271 | const src = await fs.readFile(path.join(resolvedPath, relPath), "utf-8"); |
| 272 | contentHash = contentHashOf(src); |
| 273 | } catch { |
| 274 | // ignore |
| 275 | } |
| 276 | payloads.push({ |
| 277 | file: relPath, language, contentHash, symbols, outgoingCalls, |
| 278 | }); |
| 279 | totalSymbols += symbols.filter((s) => s.name !== "<module>").length; |
| 280 | totalEdges += outgoingCalls.length; |
| 281 | } |
| 282 | |
| 283 | // Build sharded indices |
| 284 | const nameShards = new Map<string, Record<string, SymbolRef[]>>(); |
| 285 | for (const key of allNameShardKeys()) nameShards.set(key, {}); |
| 286 | for (const [file, symbols] of symbolsByFile.entries()) { |
| 287 | for (const sym of symbols) { |
| 288 | if (sym.name === "<module>") continue; |
| 289 | const shardKey = nameShardKey(sym.name); |
| 290 | const shard = nameShards.get(shardKey); |
| 291 | if (!shard) continue; |
| 292 | const ref: SymbolRef = { file, id: sym.id }; |
| 293 | // Use hasOwn — `shard[sym.name]` would return Object.prototype.constructor |
| 294 | // (a function) for symbol names like "constructor" / "toString" / "hasOwnProperty". |
| 295 | const existing = Object.hasOwn(shard, sym.name) ? shard[sym.name] : undefined; |
| 296 | if (existing) existing.push(ref); |
| 297 | else shard[sym.name] = [ref]; |
| 298 | } |
| 299 | } |
| 300 | |
| 301 | const reverseShards = new Map<number, Record<string, string[]>>(); |
| 302 | for (const [callerFile, edges] of outgoingCallsByFile.entries()) { |
| 303 | for (const e of edges) { |
| 304 | for (const calleeId of e.calleeCandidates) { |
| 305 | const calleeFile = calleeId.split("::")[0]; |
| 306 | if (!calleeFile || calleeFile === callerFile) continue; |
| 307 | const bucket = reverseShardKey(calleeFile); |
no test coverage detected