(options: SemanticIdentifierSearchOptions)
| 345 | } |
| 346 | |
| 347 | export async function semanticIdentifierSearch(options: SemanticIdentifierSearchOptions): Promise<string> { |
| 348 | const topK = Math.max(1, Math.floor(options.topK ?? 5)); |
| 349 | const topCalls = Math.max(1, Math.floor(options.topCallsPerIdentifier ?? 10)); |
| 350 | const semanticWeight = normalizeWeight(options.semanticWeight, 0.78); |
| 351 | const keywordWeight = normalizeWeight(options.keywordWeight, 0.22); |
| 352 | const includeKinds = normalizeKinds(options.includeKinds); |
| 353 | |
| 354 | const index = await buildIdentifierIndex(options.rootDir); |
| 355 | if (index.docs.length === 0) { |
| 356 | return "No supported identifiers found for semantic identifier search."; |
| 357 | } |
| 358 | |
| 359 | const [queryVec] = await fetchEmbedding(options.query); |
| 360 | const queryTerms = new Set(splitTerms(options.query)); |
| 361 | |
| 362 | const scored: RankedIdentifier[] = []; |
| 363 | for (let i = 0; i < index.docs.length; i++) { |
| 364 | const doc = index.docs[i]; |
| 365 | if (includeKinds && !includeKinds.has(doc.kind.toLowerCase())) continue; |
| 366 | |
| 367 | const semanticScore = Math.max(cosine(queryVec, index.vectors[i]), 0); |
| 368 | const keywordScore = getKeywordCoverage(queryTerms, `${doc.name} ${doc.signature} ${doc.path} ${doc.header}`); |
| 369 | const totalWeight = semanticWeight + keywordWeight; |
| 370 | const score = totalWeight > 0 |
| 371 | ? clamp01((semanticWeight * semanticScore + keywordWeight * keywordScore) / totalWeight) |
| 372 | : semanticScore; |
| 373 | |
| 374 | scored.push({ doc, semanticScore, keywordScore, score }); |
| 375 | } |
| 376 | |
| 377 | if (scored.length === 0) { |
| 378 | return "No identifiers matched the requested kind filters."; |
| 379 | } |
| 380 | |
| 381 | const top = scored.sort((a, b) => b.score - a.score).slice(0, topK); |
| 382 | const cache = await loadEmbeddingCache(options.rootDir, IDENTIFIER_CACHE_FILE); |
| 383 | |
| 384 | const lines: string[] = [ |
| 385 | `Top ${top.length} identifier matches for: "${options.query}"`, |
| 386 | "", |
| 387 | ]; |
| 388 | |
| 389 | for (let i = 0; i < top.length; i++) { |
| 390 | const item = top[i]; |
| 391 | const range = formatLineRange(item.doc.line, item.doc.endLine); |
| 392 | lines.push(`${i + 1}. ${item.doc.kind} ${item.doc.name} - ${item.doc.path} (${range})`); |
| 393 | lines.push(` Score: ${Math.round(item.score * 1000) / 10}% | Semantic: ${Math.round(item.semanticScore * 1000) / 10}% | Keyword: ${Math.round(item.keywordScore * 1000) / 10}%`); |
| 394 | lines.push(` Signature: ${item.doc.signature}`); |
| 395 | if (item.doc.parentName) lines.push(` Parent: ${item.doc.parentName}`); |
| 396 | |
| 397 | const calls = await rankCallSites( |
| 398 | options.rootDir, |
| 399 | cache, |
| 400 | queryTerms, |
| 401 | queryVec, |
| 402 | item.doc, |
| 403 | index.fileLines, |
| 404 | topCalls, |
no test coverage detected