|
1 | 1 | import type { CodeGraphSnapshot, CodeNode, WikiCitation, WikiEvidenceItem } from '@codedelta/types'; |
| 2 | +import { detectEntryPoints } from '@codedelta/graph-subgraph'; |
2 | 3 | import { evidenceIdForSymbol, type ReadSource } from './page'; |
3 | 4 | import { isDocumentableSymbol } from './toc'; |
4 | 5 |
|
@@ -160,7 +161,85 @@ export function retrieveAskEvidence( |
160 | 161 | return { evidence, matchedNodes }; |
161 | 162 | } |
162 | 163 |
|
163 | | -/** Deterministic Ask answer used with the `none` provider or on LLM failure. */ |
| 164 | +/** |
| 165 | + * When lexical retrieval finds no symbol matches, seed the LLM with entry points |
| 166 | + * and a repo overview so conversational questions still get a useful answer. |
| 167 | + */ |
| 168 | +export function bootstrapAskEvidence( |
| 169 | + snapshot: CodeGraphSnapshot, |
| 170 | + readSource: ReadSource, |
| 171 | + options: AskRetrievalOptions = {}, |
| 172 | +): AskRetrievalResult { |
| 173 | + const maxSymbols = options.maxSymbols ?? 8; |
| 174 | + const nodeById = new Map(snapshot.nodes.map((n) => [n.id, n])); |
| 175 | + const entryIds = detectEntryPoints(snapshot, { limit: maxSymbols }); |
| 176 | + const matchedNodes = entryIds |
| 177 | + .map((id) => nodeById.get(id)) |
| 178 | + .filter((n): n is CodeNode => n !== undefined && isDocumentableSymbol(n)); |
| 179 | + |
| 180 | + const areas = new Map<string, number>(); |
| 181 | + for (const f of snapshot.files) { |
| 182 | + const top = f.split('/')[0] ?? f; |
| 183 | + areas.set(top, (areas.get(top) ?? 0) + 1); |
| 184 | + } |
| 185 | + const topAreas = [...areas.entries()] |
| 186 | + .sort((a, b) => b[1] - a[1]) |
| 187 | + .slice(0, 8) |
| 188 | + .map(([a, n]) => `${a} (${n} files)`) |
| 189 | + .join(', '); |
| 190 | + |
| 191 | + let readme = ''; |
| 192 | + for (const candidate of ['README.md', 'readme.md']) { |
| 193 | + const raw = readSource(candidate); |
| 194 | + if (raw) { |
| 195 | + readme = raw.slice(0, 800); |
| 196 | + break; |
| 197 | + } |
| 198 | + } |
| 199 | + |
| 200 | + const evidence: WikiEvidenceItem[] = [ |
| 201 | + { |
| 202 | + id: 'ctx-repo', |
| 203 | + kind: 'source', |
| 204 | + title: 'Repository overview (no direct symbol match for this question)', |
| 205 | + detail: [ |
| 206 | + `Commit graph: ${snapshot.files.length} files, ${snapshot.nodeCount} indexed symbols.`, |
| 207 | + topAreas ? `Top-level areas: ${topAreas}.` : '', |
| 208 | + readme ? `README excerpt:\n${readme}` : '', |
| 209 | + 'Use the entry-point symbols below as starting points for vague or high-level questions.', |
| 210 | + ] |
| 211 | + .filter(Boolean) |
| 212 | + .join('\n\n'), |
| 213 | + file: 'README.md', |
| 214 | + }, |
| 215 | + ...matchedNodes.map((node) => ({ |
| 216 | + id: evidenceIdForSymbol(node), |
| 217 | + kind: 'symbol' as const, |
| 218 | + title: node.qualifiedName, |
| 219 | + detail: `(entry point) ${node.signature ?? `${node.kind} ${node.name}`}`, |
| 220 | + file: node.filePath, |
| 221 | + symbol: node.qualifiedName, |
| 222 | + startLine: node.startLine, |
| 223 | + endLine: node.endLine, |
| 224 | + })), |
| 225 | + ]; |
| 226 | + |
| 227 | + return { evidence, matchedNodes }; |
| 228 | +} |
| 229 | + |
| 230 | +/** Lexical retrieval, falling back to entry-point bootstrap when nothing matches. */ |
| 231 | +export function prepareAskRetrieval( |
| 232 | + snapshot: CodeGraphSnapshot, |
| 233 | + question: string, |
| 234 | + readSource: ReadSource, |
| 235 | + options: AskRetrievalOptions = {}, |
| 236 | +): AskRetrievalResult { |
| 237 | + const result = retrieveAskEvidence(snapshot, question, readSource, options); |
| 238 | + if (result.matchedNodes.length > 0) return result; |
| 239 | + return bootstrapAskEvidence(snapshot, readSource, options); |
| 240 | +} |
| 241 | + |
| 242 | +/** Deterministic Ask answer (legacy; Wiki Ask now requires a configured provider). */ |
164 | 243 | export function deterministicAskAnswer( |
165 | 244 | question: string, |
166 | 245 | result: AskRetrievalResult, |
|
0 commit comments