From e96e5be131654ccb64ea4071f8da2c662f832d67 Mon Sep 17 00:00:00 2001 From: critesjosh Date: Sat, 2 May 2026 01:00:52 +0000 Subject: [PATCH 1/3] fix(lookup_error): weak fuzzy hits no longer suppress semantic fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reported in v1.20.0 dogfood: `aztec_lookup_error("note already nullified")` returned the unrelated catalog entry "Contract already initialized" with a Jaccard word-overlap score of 54, and the semantic-documentation fallback never fired because the early-return treated any catalog hit — regardless of confidence — as authoritative. Fix: introduce STRONG_MATCH_THRESHOLD = 70. Below that, the catalog hits are kept in the result (so the formatter can still render them as low-confidence cues), but the tool falls through to the semantic fallback. The threshold aligns with the score system in utils/error-lookup.ts: 100 exact-code / hex-signature → strong, always short-circuits 95 exact-pattern → strong 70-80 substring → strong (boundary at 70) 50-65 word-overlap (Jaccard) → weak, falls through When weak hints exist alongside the semantic results, the message field now names the situation ("No strong static match — N low-confidence fuzzy hint(s) shown below. Showing relevant documentation.") instead of pretending nothing matched. Tests added (6): - threshold boundary: score === 70 still short-circuits - codeMatch alone (no catalog) still short-circuits - regression: "note already nullified" with score-54 word-overlap → semanticHealth='ok', semanticResults populated, weak hint preserved in result.catalogMatches, message contains "low-confidence" - multiple weak hints (max score 65) still fall through - weak hints + no client → "skipped", message names the weak-hint situation and points at API_KEY - weak hints + semantic returning empty → "no_results", message acknowledges both signals All 247 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/tools/error-lookup.ts | 69 ++++++++++++--- tests/tools/error-lookup.test.ts | 145 +++++++++++++++++++++++++++---- 2 files changed, 187 insertions(+), 27 deletions(-) diff --git a/src/tools/error-lookup.ts b/src/tools/error-lookup.ts index 3b81e25..2c726c0 100644 --- a/src/tools/error-lookup.ts +++ b/src/tools/error-lookup.ts @@ -1,8 +1,12 @@ /** * Error lookup tool — diagnose any Aztec error by message, code, or hex signature. * - * Enhanced: when the static catalog + dynamic parsers produce no matches, - * falls back to semantic search via DocsGPT for broader documentation context. + * Enhanced: when the static catalog + dynamic parsers produce no STRONG + * matches, falls back to semantic search via DocsGPT for broader + * documentation context. Weak fuzzy hints (word-overlap, score < 70) + * no longer suppress the semantic fallback — they would shadow the + * better answer with a misleading top hit (e.g. "note already nullified" + * matching "Contract already initialized" with a Jaccard score of 54). */ import { lookupError } from "../utils/error-lookup.js"; @@ -12,6 +16,21 @@ import type { DocsGPTClient } from "../backends/docsgpt-client.js"; import type { SemanticSearchResult } from "../backends/docsgpt-client.js"; import { checkVersionGate, formatMismatchMessage } from "../utils/version-check.js"; +/** + * Minimum catalog-match score that counts as "strong enough to short- + * circuit the semantic fallback." Aligned with the score system in + * ``utils/error-lookup.ts``: + * - 100 exact-code / hex-signature + * - 95 exact-pattern + * - 70-80 substring + * - 50-65 word-overlap (Jaccard) + * + * Threshold of 70 keeps every "real" match type and excludes only the + * Jaccard fuzzy band, which is exactly the noise floor we want to fall + * through past. + */ +const STRONG_MATCH_THRESHOLD = 70; + export type SemanticHealth = | "ok" // semantic returned results | "no_results" // semantic ran cleanly, returned empty @@ -49,10 +68,19 @@ export async function lookupAztecError( const result = lookupError(query, { category, maxResults }); - const totalMatches = result.catalogMatches.length + result.codeMatches.length; + const hasStrongCatalogMatch = result.catalogMatches.some( + (m) => m.score >= STRONG_MATCH_THRESHOLD + ); + const hasCodeMatch = result.codeMatches.length > 0; + const hasStrongMatch = hasStrongCatalogMatch || hasCodeMatch; - // Static catalog hit: return immediately, semantic call not needed. - if (totalMatches > 0) { + // Strong static hit: return immediately, semantic call not needed. + // Weak fuzzy hits (word-overlap only) deliberately fall through to + // the semantic path below — they remain in ``result.catalogMatches`` + // so the formatter can still render them as low-confidence hints, + // but they no longer suppress the semantic-fallback signal that + // produces the actually-useful answer. + if (hasStrongMatch) { return { success: true, result, @@ -61,16 +89,31 @@ export async function lookupAztecError( }; } - // No static match. Try semantic fallback if a client exists. + const weakHintsCount = result.catalogMatches.length; + + // Below the strong-match threshold (or zero matches). Try semantic + // fallback if a client exists; otherwise return the weak hints + // (if any) with a "skipped" health. if (!docsgptClient) { return { success: true, result, semanticHealth: "skipped", - message: `No matches found for "${query}". Try a different error message, code, or hex signature.`, + message: + weakHintsCount > 0 + ? `No strong match for "${query}" — only ${weakHintsCount} low-confidence fuzzy hint(s) (word-overlap). An API_KEY would enable semantic-documentation fallback. Try a different error message, code, or hex signature.` + : `No matches found for "${query}". Try a different error message, code, or hex signature.`, }; } + // ``preface`` describes the static-catalog state; the semantic-result + // branch appends what semantic produced. Keeps phrasing accurate when + // weak fuzzy hints exist alongside the semantic results. + const preface = + weakHintsCount > 0 + ? `No strong static match for "${query}" — ${weakHintsCount} low-confidence fuzzy hint(s) shown below.` + : `No exact error match found for "${query}".`; + // Version gate before invoking semantic. Mirrors aztec_search_docs. if (!allowVersionMismatch) { const gate = await checkVersionGate(docsgptClient); @@ -81,7 +124,7 @@ export async function lookupAztecError( semanticHealth: "version_mismatch", versionMismatch: { localVersion: gate.localVersion, corpusVersion: gate.corpusVersion }, message: - `No exact error match found for "${query}", and the semantic fallback was blocked by a version mismatch.\n\n` + + `${preface} The semantic fallback was blocked by a version mismatch.\n\n` + formatMismatchMessage(gate.localVersion, gate.corpusVersion), }; } @@ -99,7 +142,7 @@ export async function lookupAztecError( result, semanticResults, semanticHealth: "ok", - message: `No exact error match found for "${query}". Showing relevant documentation.`, + message: `${preface} Showing relevant documentation.`, }; } @@ -107,7 +150,10 @@ export async function lookupAztecError( success: true, result, semanticHealth: "no_results", - message: `No matches found for "${query}". Try a different error message, code, or hex signature.`, + message: + weakHintsCount > 0 + ? `${preface} Semantic search also returned no relevant documentation.` + : `No matches found for "${query}". Try a different error message, code, or hex signature.`, }; } catch (err) { // Sanitize: don't echo the raw upstream error string to the user. @@ -133,8 +179,7 @@ export async function lookupAztecError( success: true, result, semanticHealth: "failed", - message: - `No exact error match found for "${query}", and ${userFacing}.`, + message: `${preface} The semantic fallback was unavailable: ${userFacing}.`, }; } } diff --git a/tests/tools/error-lookup.test.ts b/tests/tools/error-lookup.test.ts index 068e4d6..5ddba57 100644 --- a/tests/tools/error-lookup.test.ts +++ b/tests/tools/error-lookup.test.ts @@ -57,25 +57,33 @@ beforeEach(() => { }); }); +/** + * Helper for building catalog match objects with a configurable score. + * Lets a test simulate the various confidence bands from + * ``utils/error-lookup.ts``: exact-code/hex (100), exact-pattern (95), + * substring (70-80), word-overlap (50-65). + */ +function catalogHit(score: number, name = "MatchingError", matchType: any = "substring") { + return { + entry: { + id: name.toLowerCase(), + name, + patterns: [name.toLowerCase()], + cause: "c", + fix: "f", + category: "contract" as const, + source: "s", + }, + matchType, + score, + }; +} + describe("lookupAztecError — static catalog hits", () => { it("returns immediately with semanticHealth='skipped' when catalog matches", async () => { mockLookupError.mockReturnValue({ query: "boom", - catalogMatches: [ - { - entry: { - id: "x", - name: "BoomError", - patterns: ["boom"], - cause: "c", - fix: "f", - category: "contract", - source: "s", - }, - matchType: "exact-name", - score: 100, - }, - ], + catalogMatches: [catalogHit(100, "BoomError", "exact-name")], codeMatches: [], }); @@ -88,6 +96,113 @@ describe("lookupAztecError — static catalog hits", () => { expect(client.search).not.toHaveBeenCalled(); expect(client.getCorpusVersion).not.toHaveBeenCalled(); }); + + it("short-circuits at the threshold boundary (score === 70)", async () => { + mockLookupError.mockReturnValue({ + query: "boundary", + catalogMatches: [catalogHit(70, "EdgeMatch", "substring")], + codeMatches: [], + }); + const client = makeClient({ search: vi.fn() }); + const result = await lookupAztecError({ query: "boundary" }, client); + expect(result.semanticHealth).toBe("skipped"); + expect(client.search).not.toHaveBeenCalled(); + }); + + it("short-circuits when there's only a codeMatch (ripgrep over cloned source)", async () => { + mockLookupError.mockReturnValue({ + query: "RpgRet", + catalogMatches: [], + codeMatches: [ + { file: "f.sol", line: 1, content: "x", repo: "aztec-packages" }, + ], + }); + const client = makeClient({ search: vi.fn() }); + const result = await lookupAztecError({ query: "RpgRet" }, client); + expect(result.semanticHealth).toBe("skipped"); + expect(client.search).not.toHaveBeenCalled(); + }); +}); + +describe("lookupAztecError — weak fuzzy matches DO NOT suppress semantic fallback", () => { + /** + * Regression for the "note already nullified" → "Contract already + * initialized" misfire reported in the v1.20.0 dogfood test. The + * Jaccard word-overlap matcher returned a score-54 hit on an + * unrelated catalog entry, and the early-return in lookupAztecError + * suppressed the semantic-documentation fallback that would have + * returned the actually-relevant chunks. + */ + it("falls through to semantic when catalog has only word-overlap hits (score < 70)", async () => { + mockLookupError.mockReturnValue({ + query: "note already nullified", + catalogMatches: [ + catalogHit(54, "Contract already initialized", "word-overlap"), + ], + codeMatches: [], + }); + + const client = makeClient({ + search: vi.fn().mockResolvedValue([ + { text: "Notes are nullified by...", title: "Note Lifecycle", source: "docs/notes.md" }, + ]), + }); + + const result = await lookupAztecError({ query: "note already nullified" }, client); + expect(result.semanticHealth).toBe("ok"); + expect(result.semanticResults).toHaveLength(1); + expect(client.search).toHaveBeenCalledWith("Aztec error: note already nullified", 3); + // The weak hint stays in the result so the formatter can still render + // it as a low-confidence cue — it just no longer shadows the semantic answer. + expect(result.result.catalogMatches).toHaveLength(1); + expect(result.result.catalogMatches[0].score).toBe(54); + // Message acknowledges the weak hint instead of pretending nothing matched. + expect(result.message).toContain("low-confidence"); + }); + + it("does not suppress semantic fallback for a mix of weak hints (max score 65)", async () => { + mockLookupError.mockReturnValue({ + query: "x", + catalogMatches: [ + catalogHit(65, "WeakA", "word-overlap"), + catalogHit(50, "WeakB", "word-overlap"), + ], + codeMatches: [], + }); + const client = makeClient({ + search: vi.fn().mockResolvedValue([ + { text: "doc", title: "T", source: "S" }, + ]), + }); + const result = await lookupAztecError({ query: "x" }, client); + expect(result.semanticHealth).toBe("ok"); + expect(client.search).toHaveBeenCalled(); + }); + + it("with no client and only weak catalog hints, returns 'skipped' but message names the weak-hint situation", async () => { + mockLookupError.mockReturnValue({ + query: "x", + catalogMatches: [catalogHit(54, "Weak", "word-overlap")], + codeMatches: [], + }); + const result = await lookupAztecError({ query: "x" }, null); + expect(result.semanticHealth).toBe("skipped"); + expect(result.message).toContain("low-confidence"); + expect(result.message).toContain("API_KEY"); + }); + + it("with weak hints and semantic returning empty, message acknowledges both signals", async () => { + mockLookupError.mockReturnValue({ + query: "x", + catalogMatches: [catalogHit(54, "Weak", "word-overlap")], + codeMatches: [], + }); + const client = makeClient({ search: vi.fn().mockResolvedValue([]) }); + const result = await lookupAztecError({ query: "x" }, client); + expect(result.semanticHealth).toBe("no_results"); + expect(result.message).toContain("low-confidence"); + expect(result.message).toMatch(/no relevant documentation|Semantic search/i); + }); }); describe("lookupAztecError — semantic fallback", () => { From db925e90e0c30312f172ff69582b0422788967eb Mon Sep 17 00:00:00 2001 From: critesjosh Date: Sat, 2 May 2026 01:08:40 +0000 Subject: [PATCH 2/3] fix(lookup_error): address codex review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Formatter: when semantic results exist alongside weak-only catalog hits, render Documentation FIRST, then "## Lower-Confidence Catalog Hints" with an italicized note that the docs above are likely more authoritative. Prevents the LLM consumer from anchoring on a misleading top hit (the original "Contract already initialized" failure mode rendered the bogus entry under "## Known Errors" with full **bold name** + cause/fix before the actually-relevant docs). - category filter preserved: when the caller passes `category` and the catalog produced any in-category match (even a weak one), keep the pre-PR short-circuit. Falling through to a category-agnostic semantic search would surface out-of-scope docs and confuse a user who explicitly narrowed the request. - API_KEY guidance reworded from "An API_KEY would enable..." to "Set API_KEY... (get a free key by running /mcp-key in the Aztec/Noir Discord: https://discord.gg/xMud5StFyA)" — actionable next-step phrasing matches the rest of the project's wording. - 3 more tests covering the codex coverage gaps: * weak-only + version-mismatch: gate blocks semantic, weak hint preserved, message names both signals. * weak-only + allowVersionMismatch=true: gate skipped, semantic runs. * category filter + weak-only: short-circuits (does NOT fall through to category-agnostic semantic). 250 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/tools/error-lookup.ts | 26 ++++++++--- src/utils/format.ts | 77 +++++++++++++++++++++++--------- tests/tools/error-lookup.test.ts | 58 ++++++++++++++++++++++++ 3 files changed, 132 insertions(+), 29 deletions(-) diff --git a/src/tools/error-lookup.ts b/src/tools/error-lookup.ts index 2c726c0..6d706c3 100644 --- a/src/tools/error-lookup.ts +++ b/src/tools/error-lookup.ts @@ -72,14 +72,26 @@ export async function lookupAztecError( (m) => m.score >= STRONG_MATCH_THRESHOLD ); const hasCodeMatch = result.codeMatches.length > 0; - const hasStrongMatch = hasStrongCatalogMatch || hasCodeMatch; + const hasAnyCatalogMatch = result.catalogMatches.length > 0; + + // When the caller passed an explicit ``category`` filter and the + // catalog produced any in-category match (even a weak one), keep + // the pre-PR short-circuit: falling through to a category-agnostic + // semantic search would surface out-of-scope docs and confuse the + // user who explicitly narrowed the request. The semantic backend + // doesn't honor the same category taxonomy, so respecting the + // filter means trusting the catalog at face value. + const hasCategoryFilteredHit = !!category && hasAnyCatalogMatch; + + const hasStrongMatch = + hasStrongCatalogMatch || hasCodeMatch || hasCategoryFilteredHit; // Strong static hit: return immediately, semantic call not needed. - // Weak fuzzy hits (word-overlap only) deliberately fall through to - // the semantic path below — they remain in ``result.catalogMatches`` - // so the formatter can still render them as low-confidence hints, - // but they no longer suppress the semantic-fallback signal that - // produces the actually-useful answer. + // Weak fuzzy hits (word-overlap only, no category filter) fall + // through to the semantic path below — they remain in + // ``result.catalogMatches`` so the formatter can still render them + // as low-confidence hints, but they no longer suppress the + // semantic-fallback signal that produces the actually-useful answer. if (hasStrongMatch) { return { success: true, @@ -101,7 +113,7 @@ export async function lookupAztecError( semanticHealth: "skipped", message: weakHintsCount > 0 - ? `No strong match for "${query}" — only ${weakHintsCount} low-confidence fuzzy hint(s) (word-overlap). An API_KEY would enable semantic-documentation fallback. Try a different error message, code, or hex signature.` + ? `No strong match for "${query}" — only ${weakHintsCount} low-confidence fuzzy hint(s) (word-overlap). Set API_KEY to enable semantic-documentation fallback (get a free key by running /mcp-key in the Aztec/Noir Discord: https://discord.gg/xMud5StFyA). Or try a different error message, code, or hex signature.` : `No matches found for "${query}". Try a different error message, code, or hex signature.`, }; } diff --git a/src/utils/format.ts b/src/utils/format.ts index 841e448..5cd7347 100644 --- a/src/utils/format.ts +++ b/src/utils/format.ts @@ -160,8 +160,51 @@ export function formatErrorLookupResult(result: ErrorLookupToolResult): string { const { catalogMatches, codeMatches } = result.result; - if (catalogMatches.length > 0) { - lines.push("## Known Errors"); + // When semantic results exist AND every catalog match is below the + // strong-match threshold, the catalog hits are low-confidence cues + // that shouldn't visually dominate the response. Render semantic + // first under "## Related Documentation", and the catalog after + // under "## Lower-Confidence Catalog Hints" so the LLM consumer + // doesn't anchor on a misleading top hit (e.g. "note already + // nullified" matching "Contract already initialized" with score 54). + const semanticHasResults = + !!result.semanticResults && result.semanticResults.length > 0; + const catalogIsWeakOnly = + catalogMatches.length > 0 && + catalogMatches.every((m) => m.score < 70); + const renderSemanticFirst = semanticHasResults && catalogIsWeakOnly; + + function renderSemantic() { + if (!result.semanticResults || result.semanticResults.length === 0) return; + lines.push("## Related Documentation"); + lines.push(""); + for (const match of result.semanticResults) { + if (match.title) { + lines.push(`**${match.title}**`); + } + if (match.source) { + lines.push(`Source: ${match.source}`); + } + lines.push(""); + lines.push(match.text); + lines.push(""); + lines.push("---"); + lines.push(""); + } + } + + function renderCatalog() { + if (catalogMatches.length === 0) return; + lines.push( + catalogIsWeakOnly + ? "## Lower-Confidence Catalog Hints" + : "## Known Errors" + ); + if (catalogIsWeakOnly) { + lines.push( + "_These are word-overlap fuzzy matches, not direct hits — the documentation results above are likely more authoritative._" + ); + } lines.push(""); for (const m of catalogMatches) { @@ -178,10 +221,10 @@ export function formatErrorLookupResult(result: ErrorLookupToolResult): string { } } - if (codeMatches.length > 0) { + function renderCode() { + if (codeMatches.length === 0) return; lines.push("## Related Code References"); lines.push(""); - for (const match of codeMatches) { lines.push(`**${match.file}:${match.line}**`); lines.push("```"); @@ -191,24 +234,14 @@ export function formatErrorLookupResult(result: ErrorLookupToolResult): string { } } - // Semantic fallback results from DocsGPT - if (result.semanticResults && result.semanticResults.length > 0) { - lines.push("## Related Documentation"); - lines.push(""); - - for (const match of result.semanticResults) { - if (match.title) { - lines.push(`**${match.title}**`); - } - if (match.source) { - lines.push(`Source: ${match.source}`); - } - lines.push(""); - lines.push(match.text); - lines.push(""); - lines.push("---"); - lines.push(""); - } + if (renderSemanticFirst) { + renderSemantic(); + renderCatalog(); + renderCode(); + } else { + renderCatalog(); + renderCode(); + renderSemantic(); } if ( diff --git a/tests/tools/error-lookup.test.ts b/tests/tools/error-lookup.test.ts index 5ddba57..ed2e785 100644 --- a/tests/tools/error-lookup.test.ts +++ b/tests/tools/error-lookup.test.ts @@ -203,6 +203,64 @@ describe("lookupAztecError — weak fuzzy matches DO NOT suppress semantic fallb expect(result.message).toContain("low-confidence"); expect(result.message).toMatch(/no relevant documentation|Semantic search/i); }); + + it("weak-only + version-mismatch: gate blocks semantic, weak hint preserved, message names mismatch", async () => { + mockGetRepoTag.mockResolvedValue("v4.1.0"); + mockLookupError.mockReturnValue({ + query: "x", + catalogMatches: [catalogHit(54, "Weak", "word-overlap")], + codeMatches: [], + }); + const client = makeClient({ + search: vi.fn().mockResolvedValue([{ text: "doc", title: "T", source: "S" }]), + getCorpusVersion: vi.fn().mockResolvedValue({ aztec_corpus_version: "v4.2.0" }), + }); + const result = await lookupAztecError({ query: "x" }, client); + expect(result.semanticHealth).toBe("version_mismatch"); + expect(client.search).not.toHaveBeenCalled(); + // Weak hint preserved so the formatter can still render it + expect(result.result.catalogMatches).toHaveLength(1); + // Message names BOTH the weak-hint situation AND the version mismatch + expect(result.message).toContain("low-confidence"); + expect(result.message).toContain("v4.1.0"); + expect(result.message).toContain("v4.2.0"); + }); + + it("weak-only + allowVersionMismatch=true: gate skipped, semantic runs", async () => { + mockGetRepoTag.mockResolvedValue("v4.1.0"); + mockLookupError.mockReturnValue({ + query: "x", + catalogMatches: [catalogHit(54, "Weak", "word-overlap")], + codeMatches: [], + }); + const client = makeClient({ + search: vi.fn().mockResolvedValue([{ text: "doc", title: "T", source: "S" }]), + getCorpusVersion: vi.fn().mockResolvedValue({ aztec_corpus_version: "v4.2.0" }), + }); + const result = await lookupAztecError( + { query: "x", allowVersionMismatch: true }, + client + ); + expect(result.semanticHealth).toBe("ok"); + expect(client.getCorpusVersion).not.toHaveBeenCalled(); + expect(client.search).toHaveBeenCalledWith("Aztec error: x", 3); + }); + + it("category filter + weak-only: short-circuits (does NOT fall through to category-agnostic semantic)", async () => { + mockLookupError.mockReturnValue({ + query: "x", + catalogMatches: [catalogHit(54, "WeakInCategory", "word-overlap")], + codeMatches: [], + }); + const client = makeClient({ + search: vi.fn().mockResolvedValue([{ text: "doc", title: "T", source: "S" }]), + }); + const result = await lookupAztecError({ query: "x", category: "circuit" }, client); + // Category filter is authoritative — falling through to a + // category-agnostic semantic search would surface out-of-scope docs. + expect(result.semanticHealth).toBe("skipped"); + expect(client.search).not.toHaveBeenCalled(); + }); }); describe("lookupAztecError — semantic fallback", () => { From 23d47f251baeab0875810b9689f23b9f389faa4d Mon Sep 17 00:00:00 2001 From: critesjosh Date: Sat, 2 May 2026 01:21:27 +0000 Subject: [PATCH 3/3] fix(lookup_error): only point at "documentation above" when semantic ran MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex round-2 finding: the weak-only catalog note `_These are word-overlap fuzzy matches, not direct hits — the documentation results above are likely more authoritative._` rendered even when there were no semantic results above (no client, version mismatch, backend failed, semantic returned empty). Stale copy could mislead a user into thinking they should look up at a documentation section that doesn't exist. Now the "documentation above" phrasing is gated on `renderSemanticFirst` (which already encodes "semantic returned hits AND we reordered"). The other weak-only paths use neutral copy: `_Treat as low-confidence cues only._` Co-Authored-By: Claude Opus 4.7 (1M context) --- src/utils/format.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/utils/format.ts b/src/utils/format.ts index 5cd7347..8316a93 100644 --- a/src/utils/format.ts +++ b/src/utils/format.ts @@ -201,8 +201,17 @@ export function formatErrorLookupResult(result: ErrorLookupToolResult): string { : "## Known Errors" ); if (catalogIsWeakOnly) { + // Only point at "documentation results above" when there + // actually is a semantic section above (semantic ran AND + // returned hits, AND we reordered to render it first). In + // every other weak-only state — no client, version mismatch, + // backend failed, semantic returned empty — there's no docs + // section to point at, so use neutral copy that names the + // weakness without implying a better answer is below. lines.push( - "_These are word-overlap fuzzy matches, not direct hits — the documentation results above are likely more authoritative._" + renderSemanticFirst + ? "_These are word-overlap fuzzy matches, not direct hits — the documentation results above are likely more authoritative._" + : "_These are word-overlap fuzzy matches, not direct hits. Treat as low-confidence cues only._" ); } lines.push("");