// Pure mappings from the Go /retrieve response (SDK/RAG-owned; board 2026-07-01 12:25) // to the FE-facing advisor contract. Kept pure + testable; the orchestration (route.ts) wires them. import type { Citation, ClarityInfo, EvidenceUnit, Footnote, VisualEvidence } from './contract' export interface RetrieveClarity { mode?: string // 'clarify' | 'answer' reason?: string // e.g. 'middle_band_llm_needed' concentration?: number domain_count?: number dominant_context?: string candidate_contexts?: { id: string; label: string; hits?: number }[] } export interface RetrieveFootnote { id?: string ref?: string number?: number regulation_code?: string regulation_short?: string regulation_name?: string section?: string text?: string } export interface RetrieveResponse { evidence?: EvidenceUnit[] visual_evidence?: VisualEvidence[] footnotes?: RetrieveFootnote[] clarity?: RetrieveClarity results?: unknown[] tables?: unknown[] // C6 — not in the FE contract yet (future TablesPane) } /** clarify unless a context was chosen; /retrieve's clarity.mode decides for un-scoped queries. */ export function resolveMode(clarityMode: string | undefined, hasContext: boolean): 'clarify' | 'answer' { if (hasContext) return 'answer' return clarityMode === 'clarify' ? 'clarify' : 'answer' } export function mapClarity(c: RetrieveClarity | undefined, mode: 'clarify' | 'answer'): ClarityInfo { return { is_underspecified: mode === 'clarify', concentration: c?.concentration ?? 0, dominant_context: c?.dominant_context, suggested_contexts: mode === 'clarify' ? (c?.candidate_contexts ?? []).map((cc) => ({ id: cc.id, label: cc.label })) : undefined, } } export function mapFootnotes(fns: RetrieveFootnote[] | undefined): Footnote[] { return (fns ?? []).map((f) => ({ footnote_id: f.id, ref: f.ref ?? (f.number != null ? `Fußnote ${f.number}` : undefined), document: f.regulation_short || f.regulation_name || f.regulation_code, section: f.section, text: f.text, })) } /** Citations are generated by the orchestration (not by /retrieve): [n] -> nth evidence unit. */ export function buildCitations(evidence: EvidenceUnit[]): Citation[] { return evidence.map((e, i) => ({ citation_id: `c${i + 1}`, number: i + 1, evidence_id: e.evidence_id, document: e.document, section: e.section ?? null, paragraph: e.paragraph ?? null, footnote: null, figure: null, })) } /** Numbered evidence list injected into the L2 prompt so the LLM can cite [n]. */ export function numberedEvidenceForPrompt(evidence: EvidenceUnit[]): string { return evidence .map((e, i) => { const loc = [e.document, e.section, e.paragraph].filter(Boolean).join(' ') return `[${i + 1}] ${loc}\n${e.snippet ?? ''}`.trim() }) .join('\n\n') } /** * Backward-compat discriminator: legacy consumers (e.g. breakpilot-workspace) send `{message}` * and read a plain-text stream; the new FE sends `{question}` and expects the JSON contract. */ export function isLegacyRequest(body: { question?: unknown; message?: unknown }): boolean { return body.question == null && typeof body.message === 'string' }