Skip to content

Commit 4e004c6

Browse files
heqikaicursoragent
andcommitted
fix(wiki): require LLM for Ask and bootstrap vague questions
Wiki Ask now always uses a configured provider, bootstraps entry-point evidence when lexical retrieval is empty, and surfaces provider errors instead of a silent deterministic fallback. Desktop v0.2.1. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent ba17fb2 commit 4e004c6

9 files changed

Lines changed: 131 additions & 41 deletions

File tree

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,8 @@ Generate a per-commit wiki from the structural snapshot — inspired by DeepWiki
6868
- **TOC planned deterministically** from the graph: overview, architecture, one page per top module, plus routes/components when present
6969
- **Mermaid diagrams serialized from real edges** (module import graph, call flows) — never invented by the model
7070
- **Per-page citations** to symbols with file/line ranges; symbol citations deep-link into **Panorama**
71-
- **Ask this repo** — conversational Q&A over the commit; retrieval is lexical scoring + graph traversal (no embeddings, no vector DB), answers cite the evidence whitelist
72-
- **Works without any LLM**: structural pages, tables, and diagrams are always generated; configure a provider to add narrated prose and richer answers
71+
- **Ask this repo** — conversational Q&A over the commit (requires a configured LLM provider); retrieval is lexical scoring + graph traversal, answers cite the evidence whitelist
72+
- **Works without any LLM** for page generation: structural pages, tables, and diagrams are always generated; configure a provider for narrated pages and for Ask
7373
- Cached under `.codedelta/wiki/` per commit + wiki version; generation runs as a background job with progress
7474

7575
### Commit timeline & import
@@ -170,7 +170,7 @@ This creates or updates `~/.codex/auth.json` (ChatGPT OAuth). You can override t
170170

171171
Open **Trace View**, enter a concrete question (file paths, symbols, or config names help), and click **Run trace**.
172172

173-
Deterministic results always appear; if Codex is configured, the model may refine the narrative. Model output is **non-authoritative** — evidence and Delta verification are the source of truth. The same provider also powers **Wiki** page narration and **Ask** answers; without it, both fall back to deterministic structural output.
173+
Deterministic results always appear; if Codex is configured, the model may refine the narrative. Model output is **non-authoritative** — evidence and Delta verification are the source of truth. The same provider also powers **Wiki** page narration and **Ask** (Ask requires a provider; wiki pages still ship structurally without one).
174174

175175
### Codex troubleshooting
176176

@@ -252,14 +252,14 @@ Roadmap and deferred work: [docs/codedelta/ROADMAP.md](docs/codedelta/ROADMAP.md
252252

253253
CodeDelta ships **desktop apps** ([`apps/desktop/`](apps/desktop/)) — Tauri 2 shells that bundle Node 22 (for CodeGraph’s `node:sqlite`) and the API server. End users do not need a separate Node install.
254254

255-
**Version** is read from `apps/desktop/src-tauri/tauri.conf.json` (currently `0.2.0`). macOS and Windows installers publish to the same GitHub Release: `codedelta-desktop-v0.2.0`.
255+
**Version** is read from `apps/desktop/src-tauri/tauri.conf.json` (currently `0.2.1`). macOS and Windows installers publish to the same GitHub Release: `codedelta-desktop-v0.2.1`.
256256

257257
### Download
258258

259259
| Platform | File | Notes |
260260
|----------|------|-------|
261-
| **macOS** (Apple Silicon) | [GitHub Releases](https://github.com/ingeniousfrog/CodeDelta/releases/tag/codedelta-desktop-v0.2.0)`CodeDelta_*_aarch64.dmg` | Unsigned; right-click → Open if blocked |
262-
| **Windows** (x64) | Same release`CodeDelta_*_x64-setup.exe` | NSIS installer |
261+
| **macOS** (Apple Silicon) | [GitHub Releases](https://github.com/ingeniousfrog/CodeDelta/releases/tag/codedelta-desktop-v0.2.1)`CodeDelta_*_aarch64.dmg` | Unsigned; right-click → Open if blocked |
262+
| **Windows** (x64) | [GitHub Releases](https://github.com/ingeniousfrog/CodeDelta/releases/tag/codedelta-desktop-v0.2.1)`CodeDelta_*_x64-setup.exe` | NSIS installer |
263263
| macOS mirror | [百度网盘](https://pan.baidu.com/s/1FQxOgNHyvU1Y5EB34RpogQ?pwd=frog) · 提取码: `frog` | |
264264

265265
**Install (macOS):** open the dmg → drag **CodeDelta** to Applications.

__tests__/codedelta/wiki-server.test.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -151,16 +151,12 @@ describe('codedelta-server wiki (none provider, deterministic path)', () => {
151151
);
152152
expect(missing.status).toBe(404);
153153

154-
// Ask without provider: deterministic answer grounded in matched symbols.
154+
// Ask requires a configured LLM provider (default settings use "none").
155155
const ask = await request(app)
156156
.post(`/api/repos/${repoId}/wiki/ask`)
157157
.send({ commit, question: 'how does login validate the user?' });
158-
expect(ask.status).toBe(200);
159-
expect(ask.body.provider.used).toBe(false);
160-
expect(ask.body.answer).toContain('login');
161-
expect(Array.isArray(ask.body.citations)).toBe(true);
162-
expect(Array.isArray(ask.body.evidence)).toBe(true);
163-
expect(ask.body.evidence.length).toBeGreaterThan(0);
158+
expect(ask.status).toBe(503);
159+
expect(ask.body.error).toContain('Provider Settings');
164160

165161
// Ask validation errors.
166162
const noQuestion = await request(app).post(`/api/repos/${repoId}/wiki/ask`).send({ commit });

apps/desktop/src-tauri/tauri.conf.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "https://schema.tauri.app/config/2",
33
"productName": "CodeDelta",
4-
"version": "0.2.0",
4+
"version": "0.2.1",
55
"identifier": "com.codedelta.desktop",
66
"build": {
77
"beforeDevCommand": "",

apps/web/src/pages/WikiPage.tsx

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -338,8 +338,9 @@ export default function WikiPage() {
338338
<div className="wiki-chat">
339339
{messages.length === 0 && (
340340
<p className="hint">
341-
Ask about symbols, flows, or modules. Answers are grounded in the structural graph
342-
and cite verifiable symbols.
341+
Ask in natural language — answers use your configured LLM provider, grounded in
342+
the structural graph (symbols, call paths, README). Configure one in{' '}
343+
<Link to="/settings/provider">Provider Settings</Link> if Ask is disabled.
343344
</p>
344345
)}
345346
{messages.map((m, i) => (
@@ -363,10 +364,7 @@ export default function WikiPage() {
363364
</ul>
364365
)}
365366
{m.role === 'assistant' && m.confidence && (
366-
<p className="hint">
367-
confidence: {m.confidence}
368-
{m.providerUsed === false ? ' · deterministic (no LLM)' : ''}
369-
</p>
367+
<p className="hint">confidence: {m.confidence}</p>
370368
)}
371369
</div>
372370
))}

packages/codedelta-server/src/services/wiki.ts

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,10 @@ import {
2828
buildWikiPageUserPayload,
2929
citationsFromEvidence,
3030
composeWikiPage,
31-
deterministicAskAnswer,
3231
extractJsonObject,
3332
normalizeWikiAssetPath,
3433
planWikiToc,
35-
retrieveAskEvidence,
34+
prepareAskRetrieval,
3635
rewriteWikiAssetUrls,
3736
validateWikiAskOutput,
3837
validateWikiPageOutput,
@@ -347,30 +346,28 @@ export async function askWiki(
347346
const question = body.question?.trim();
348347
if (!question) throw new WikiError('question is required', 400);
349348

350-
const snapshot = await loadSnapshot(registry, repoId, commitHash);
351-
const readSource = makeReadSource(ref.clonePath, commitHash);
352-
const retrieval = retrieveAskEvidence(snapshot, question, readSource);
353-
const deterministic = deterministicAskAnswer(question, retrieval);
354-
355349
const providerConfig = settings.getProvider();
356350
const provider = createProvider(providerConfig);
351+
if (provider.id === 'none' || !provider.isConfigured()) {
352+
throw new WikiError(
353+
'Wiki Ask requires a configured LLM provider. Open Settings → Provider Settings (Codex OAuth, OpenAI, or compatible).',
354+
503,
355+
);
356+
}
357+
358+
const snapshot = await loadSnapshot(registry, repoId, commitHash);
359+
const readSource = makeReadSource(ref.clonePath, commitHash);
360+
const retrieval = prepareAskRetrieval(snapshot, question, readSource);
357361

358362
const answer: WikiAskAnswer = {
359363
question,
360-
answer: deterministic.answer,
361-
citations: citationsFromEvidence(
362-
retrieval.evidence.filter((e) => e.kind === 'symbol').map((e) => e.id),
363-
retrieval.evidence,
364-
),
364+
answer: '',
365+
citations: [],
365366
evidence: retrieval.evidence,
366-
confidence: deterministic.confidence,
367+
confidence: 'low',
367368
provider: { type: provider.id, model: providerConfig.model, used: false },
368369
};
369370

370-
if (provider.id === 'none' || !provider.isConfigured() || retrieval.evidence.length === 0) {
371-
return answer;
372-
}
373-
374371
try {
375372
const history = (body.history ?? []).slice(-6);
376373
const modelText = await provider.complete({
@@ -406,15 +403,24 @@ export async function askWiki(
406403
answer.confidence = validated.value.confidence;
407404
answer.provider = { type: provider.id, model: providerConfig.model, used: true };
408405
} else {
406+
answer.answer =
407+
'The model returned a response that could not be validated against the evidence whitelist. Try rephrasing with a concrete symbol, file, or module name.';
409408
answer.provider = {
410409
type: provider.id,
411410
model: providerConfig.model,
412411
used: true,
413412
nonAuthoritativeText: modelText,
414413
};
415414
}
416-
} catch {
417-
// Keep the deterministic answer on provider failure.
415+
} catch (err) {
416+
throw new WikiError(
417+
`Wiki Ask failed: ${err instanceof Error ? err.message : String(err)}`,
418+
502,
419+
);
420+
}
421+
422+
if (!answer.answer.trim()) {
423+
throw new WikiError('Wiki Ask returned an empty answer from the provider.', 502);
418424
}
419425

420426
return answer;

packages/codedelta-wiki-engine/__tests__/wiki-engine.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
mermaidCallFlow,
1010
mermaidModuleGraph,
1111
planWikiToc,
12+
prepareAskRetrieval,
1213
renderDeterministicPage,
1314
retrieveAskEvidence,
1415
tokenizeQuestion,
@@ -259,6 +260,13 @@ describe('ask retrieval', () => {
259260
expect(answer.answer).toContain('No symbols');
260261
});
261262

263+
it('prepareAskRetrieval bootstraps entry points when lexical match is empty', () => {
264+
const snapshot = makeSnapshot();
265+
const result = prepareAskRetrieval(snapshot, 'what is wrong here', readSource);
266+
expect(result.matchedNodes.length).toBeGreaterThan(0);
267+
expect(result.evidence.some((e) => e.id === 'ctx-repo')).toBe(true);
268+
});
269+
262270
it('deterministicAskAnswer lists matched symbols and call relationships', () => {
263271
const snapshot = makeSnapshot();
264272
const result = retrieveAskEvidence(snapshot, 'handleCompare buildSnapshot diffGraphs', readSource);

packages/codedelta-wiki-engine/src/ask.ts

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { CodeGraphSnapshot, CodeNode, WikiCitation, WikiEvidenceItem } from '@codedelta/types';
2+
import { detectEntryPoints } from '@codedelta/graph-subgraph';
23
import { evidenceIdForSymbol, type ReadSource } from './page';
34
import { isDocumentableSymbol } from './toc';
45

@@ -160,7 +161,85 @@ export function retrieveAskEvidence(
160161
return { evidence, matchedNodes };
161162
}
162163

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). */
164243
export function deterministicAskAnswer(
165244
question: string,
166245
result: AskRetrievalResult,

packages/codedelta-wiki-engine/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,10 @@ export {
3333
type ValidatedWikiPage,
3434
} from './provider-io';
3535
export {
36+
bootstrapAskEvidence,
3637
citationsFromEvidence,
3738
deterministicAskAnswer,
39+
prepareAskRetrieval,
3840
retrieveAskEvidence,
3941
tokenizeQuestion,
4042
type AskRetrievalOptions,

packages/codedelta-wiki-engine/src/provider-io.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,9 @@ export function validateWikiPageOutput(
113113
export function buildWikiAskSystemPrompt(): string {
114114
return [
115115
'You answer questions about a codebase for CodeDelta Wiki, grounded in structural evidence.',
116-
'Use ONLY the evidence items (symbols, call paths, source snippets) provided in the user message.',
116+
'Use ONLY the evidence items (symbols, call paths, source snippets, repository overview) provided in the user message.',
117117
'Never invent files, symbols, call relationships, or behavior.',
118+
'For vague questions with only entry-point / overview evidence, explain what you can from that context and suggest concrete symbols or files to ask about next.',
118119
'Return strict JSON only (no markdown fences), matching this schema:',
119120
'{',
120121
' "answer": string, // markdown; reference symbols with backticks',

0 commit comments

Comments
 (0)