Spaces:
Sleeping
Sleeping
| /** | |
| * validationGate.ts — S220: Unified Validation Gate | |
| * | |
| * REGOLA: tutto l'output LLM grezzo deve passare attraverso questo gate | |
| * prima di essere renderizzato nella UI o usato da un tool. | |
| * | |
| * Unifica in un unico punto: | |
| * - outputValidator.ts (quality score, rifiuti, truncation, garbage) | |
| * - normalizeLlmOutput.ts (whitespace, encoding, BOM) | |
| * - sanitizeMarkdown.ts (markdown safety) | |
| * - repairCodeBlocks.ts (code block chiusi male) | |
| * - ai/output-validator.ts (detectHallucination, validateJson) | |
| * | |
| * Percorso: raw → normalize → sanitize → repairCode → validate → gate | |
| * | |
| * Il gate è ADDITIVO: non rompe nessun caller esistente. | |
| * I moduli originali rimangono — questo li aggrega. | |
| * | |
| * Design: | |
| * - Zero LLM, zero React, zero side-effects globali | |
| * - DI: _setTestDeps() / _resetDeps() | |
| * - Mai lancia — fallback a raw text in caso di errore interno | |
| * - Safari-safe, synchronous (eccetto validateGateAsync per diff) | |
| * | |
| * @module validationGate | |
| */ | |
| import { normalizeLlmOutput } from "@/lib/utils/normalizeLlmOutput"; | |
| import { sanitizeMarkdown } from "@/lib/utils/sanitizeMarkdown"; | |
| import { repairCodeBlocks } from "@/lib/utils/repairCodeBlocks"; | |
| import { validateAgentOutput as validateOutput } from "@/lib/ai/output-validator"; | |
| import { detectHallucination } from "@/lib/ai/output-validator"; | |
| import { auditOutput } from "@/lib/antiRewriteGuard"; | |
| import type { AntiRewriteAudit } from "@/lib/antiRewriteGuard"; | |
| // ── Tipi ──────────────────────────────────────────────────────────────────── | |
| export type GateVerdict = "accept" | "repaired" | "retry" | "reject"; | |
| export interface ValidationGateResult { | |
| /** Testo finale da usare (potrebbe essere riparato) */ | |
| text: string; | |
| /** Decisione del gate */ | |
| verdict: GateVerdict; | |
| /** Qualità normalizzata 0–10 */ | |
| quality: number; | |
| /** Elenco di problemi trovati */ | |
| issues: string[]; | |
| /** Elenco di riparazioni applicate */ | |
| repairs: string[]; | |
| /** true = il modello ha inventato fatti (score allucinazione elevato) */ | |
| hallucination: boolean; | |
| /** Suggerimento per il retry prompt (presente se verdict === "retry") */ | |
| retryHint?: string; | |
| /** S-ARCH-3: true se l'output rimuove pattern critici presenti nel codice di riferimento */ | |
| regressionRisk?: boolean; | |
| /** Pattern critici mancanti rispetto al codice di riferimento */ | |
| regressionPatterns?: string[]; | |
| } | |
| export interface GateOptions { | |
| /** Qualità minima per "accept" (default 4) */ | |
| minQuality?: number; | |
| /** Qualità minima dopo repair per "repaired" invece di "retry" (default 2) */ | |
| repairFloor?: number; | |
| /** Hint sul tipo di task: usato per validateOutput */ | |
| taskHint?: string; | |
| /** Se true, esegui hallucination check (default true) */ | |
| checkHallucination?: boolean; | |
| /** | |
| * S-ARCH-3: Snapshot del codice "noto buono" per rilevare regressioni. | |
| * Se fornito, il gate confronta l'output con il codice di riferimento | |
| * e segnala i pattern critici rimossi (export, function, class signatures). | |
| */ | |
| referenceCode?: string; | |
| } | |
| // ── Soglie ─────────────────────────────────────────────────────────────────── | |
| const DEFAULT_MIN_QUALITY = 4; | |
| const DEFAULT_REPAIR_FLOOR = 2; | |
| // ── DI per testing ────────────────────────────────────────────────────────── | |
| interface GateDeps { | |
| normalize: typeof normalizeLlmOutput; | |
| sanitize: typeof sanitizeMarkdown; | |
| repair: typeof repairCodeBlocks; | |
| validate: typeof validateOutput; | |
| detectHall: typeof detectHallucination; | |
| } | |
| const defaultDeps: GateDeps = { | |
| normalize: normalizeLlmOutput, | |
| sanitize: sanitizeMarkdown, | |
| repair: repairCodeBlocks, | |
| validate: validateOutput, | |
| detectHall: detectHallucination, | |
| }; | |
| let _deps: GateDeps = { ...defaultDeps }; | |
| export function _setTestDeps(d: Partial<GateDeps>): void { _deps = { ...defaultDeps, ...d }; } | |
| export function _resetDeps(): void { _deps = { ...defaultDeps }; } | |
| // ── Gate principale ─────────────────────────���──────────────────────────────── | |
| /** | |
| * Unico punto di verità per la validazione dell'output LLM. | |
| * | |
| * Uso: | |
| * const gate = validateGate(rawLLMOutput, { taskHint: "codice TypeScript" }); | |
| * if (gate.verdict === "reject") return; // scarta | |
| * if (gate.verdict === "retry") retryWithHint(gate.retryHint!); | |
| * renderUI(gate.text); // "accept" o "repaired" | |
| */ | |
| // S765 Gap 6: pattern per comandi distruttivi operativi. | |
| // Nessun output LLM legittimo dovrebbe contenere questi pattern come testo da rendere all'utente. | |
| // Hard-coded (non configurabile) — reject immediato, quality=0. | |
| const _OPERATIONAL_HALLUCINATION_RE = | |
| /\b(sudo\s+rm\s+-rf|DROP\s+TABLE|DELETE\s+FROM\s+.{0,60}WHERE\s+1\s*=\s*1|format\s+c:|mkfs\.|dd\s+if=\/dev\/zero)/i; | |
| // ── S-ARCH-3: Regression Risk Detector ──────────────────────────────────────── | |
| // Estrae le "firma critiche" dal codice (export, function, class, interface). | |
| // Confronta vecchio codice con nuovo: se firme critiche spariscono → regression risk. | |
| const _CRITICAL_SIGNATURE_RE = /(?:export\s+(?:default\s+)?(?:function|class|interface|type|const|enum)\s+\w+|^(?:function|class)\s+\w+)/gm; // S-ARCH-3-FIX: backslash mancanti | |
| function _extractSignatures(code: string): string[] { | |
| const sigs: string[] = []; | |
| let m: RegExpExecArray | null; | |
| const re = new RegExp(_CRITICAL_SIGNATURE_RE.source, 'gm'); | |
| while ((m = re.exec(code)) !== null) { | |
| sigs.push(m[0].trim().slice(0, 80)); | |
| } | |
| return sigs; | |
| } | |
| /** | |
| * Rileva rischio di regressione confrontando output con codice di riferimento. | |
| * Ritorna le firme critiche presenti nel riferimento ma assenti nell'output. | |
| * Zero LLM, Zero rete — puramente sintattico. | |
| */ | |
| function _detectRegressionRisk( | |
| output: string, | |
| reference: string, | |
| ): { risk: boolean; missing: string[] } { | |
| if (!reference.trim() || !output.trim()) return { risk: false, missing: [] }; | |
| const refSigs = _extractSignatures(reference); | |
| if (!refSigs.length) return { risk: false, missing: [] }; | |
| const outputLower = output.toLowerCase(); | |
| const missing = refSigs.filter(sig => { | |
| // Estrai il nome identificatore dalla firma per una ricerca flessibile | |
| const name = sig.replace(/^.*?(\w+)\s*$/, '$1') // S-ARCH-3-FIX.toLowerCase(); | |
| return name.length > 2 && !outputLower.includes(name); | |
| }); | |
| return { risk: missing.length > 0, missing }; | |
| } | |
| export function validateGate( | |
| raw: string, | |
| opts: GateOptions = {}, | |
| ): ValidationGateResult { | |
| const minQuality = opts.minQuality ?? DEFAULT_MIN_QUALITY; | |
| const repairFloor = opts.repairFloor ?? DEFAULT_REPAIR_FLOOR; | |
| const checkHall = opts.checkHallucination !== false; | |
| const repairs: string[] = []; | |
| const issues: string[] = []; | |
| let text = raw; | |
| try { | |
| // ── 1. Normalize ──────────────────────────────────────────────────── | |
| const norm = _deps.normalize(text); | |
| if (norm.repairs.length) repairs.push(...norm.repairs); | |
| text = norm.text; | |
| // ── 2. Sanitize markdown ───────────────────────────────────────────── | |
| const san = _deps.sanitize(text); | |
| if (san.repairs.length) repairs.push(...san.repairs); | |
| text = san.text; | |
| // ── 3. Repair code blocks ───────────────────────────────────────────── | |
| const rep = _deps.repair(text); | |
| if (rep.repairs.length) repairs.push(...rep.repairs); | |
| text = rep.text; | |
| // ── 4. Quality validation ────────────────────────────────────────────── | |
| const val = _deps.validate(text, opts.taskHint as "code" | "markdown" | "tool_result" | "chat" | undefined); | |
| issues.push(...val.issues); | |
| // quality: 10=perfect, ≥4=accept, ≥2=repair, <2=reject | |
| const quality = !val.valid ? 1 : val.issues.length === 0 ? 10 : Math.max(3, 8 - val.issues.length * 2); | |
| // ── 5. Hallucination check ────────────────────────────────────────────��� | |
| let hallucination = false; | |
| // S765 Gap 6: comandi distruttivi — reject immediato prima del check semantico | |
| if (_OPERATIONAL_HALLUCINATION_RE.test(text)) { | |
| issues.push("hallucination operativa: comando distruttivo rilevato nel testo"); | |
| return { text, verdict: "reject", quality: 0, issues, repairs, hallucination: true }; | |
| } | |
| if (checkHall) { | |
| const hall = _deps.detectHall(text); | |
| if (hall.suspect) { | |
| hallucination = true; | |
| issues.push(`hallucination_suspect: ${hall.reasons.slice(0, 2).join("; ")}`); | |
| } | |
| } | |
| // ── 6. S-ARCH-3: Regression risk check ──────────────────────────────────── | |
| let regressionRisk = false; | |
| let regressionPatterns: string[] = []; | |
| if (opts.referenceCode) { | |
| const reg = _detectRegressionRisk(text, opts.referenceCode); | |
| if (reg.risk) { | |
| regressionRisk = true; | |
| regressionPatterns = reg.missing; | |
| issues.push(`regression_risk: ${reg.missing.slice(0, 3).join(", ")}`); | |
| } | |
| } | |
| // ── 7. S-ANTIREGRESS: Anti-rewrite audit ──────────────────────────────── | |
| let antiRewriteAuditResult: AntiRewriteAudit | undefined; | |
| if (opts.referenceCode) { | |
| try { | |
| antiRewriteAuditResult = auditOutput(opts.referenceCode, text); | |
| if (antiRewriteAuditResult.hasIssues) { | |
| if (antiRewriteAuditResult.rewrite.isRewrite) { | |
| issues.push(`anti_rewrite: similarity=${antiRewriteAuditResult.rewrite.similarity.toFixed(2)} — probabile riscrittura completa`); | |
| } | |
| if (antiRewriteAuditResult.incomplete.incomplete) { | |
| issues.push(`incompleteness: ${antiRewriteAuditResult.incomplete.missingCount} strutture mancanti (${antiRewriteAuditResult.incomplete.missingNames.slice(0, 3).join(', ')})`); | |
| } | |
| if (antiRewriteAuditResult.importInject.injected) { | |
| issues.push(`import_injection: nuovi pacchetti (${antiRewriteAuditResult.importInject.newPackages.slice(0, 3).join(', ')})`); | |
| } | |
| } | |
| } catch { /* non-fatal — guard non deve bloccare il gate */ } | |
| } | |
| // ── 8. Verdict ───────────────────────────────────────────────────────── | |
| let verdict: GateVerdict; | |
| let retryHint: string | undefined; | |
| if (text.length < 3) { | |
| verdict = "reject"; | |
| retryHint = "Output vuoto o troppo breve. Fornisci una risposta completa."; | |
| } else if (quality >= minQuality && !hallucination) { | |
| verdict = repairs.length > 0 ? "repaired" : "accept"; | |
| } else if (quality >= repairFloor) { | |
| verdict = "retry"; | |
| retryHint = buildRetryHint(issues, quality); | |
| } else { | |
| verdict = "reject"; | |
| retryHint = buildRetryHint(issues, quality); | |
| } | |
| // Arricchisci retryHint con i suggerimenti anti-rewrite se presenti | |
| if (antiRewriteAuditResult?.hasIssues && antiRewriteAuditResult.combinedHint) { | |
| retryHint = retryHint | |
| ? `${antiRewriteAuditResult.combinedHint} ${retryHint}` | |
| : antiRewriteAuditResult.combinedHint; | |
| } | |
| return { text, verdict, quality, issues, repairs, hallucination, retryHint, regressionRisk, regressionPatterns }; | |
| } catch (e) { | |
| // Gate non deve mai lanciare — fallback a raw text | |
| return { | |
| text: raw, | |
| verdict: "accept", // fallback conservativo | |
| quality: 3, | |
| issues: [`gate_error: ${String(e).slice(0, 80)}`], | |
| repairs: [], | |
| hallucination: false, | |
| regressionRisk: false, | |
| }; | |
| } | |
| } | |
| // ── Retry hint builder ─────────────────────────────────────────────────────── | |
| function buildRetryHint(issues: string[], quality: number): string { | |
| const parts: string[] = []; | |
| if (issues.some(i => /refus|cannot|don't|non posso/i.test(i))) { | |
| parts.push("Non rifiutare il task. Rispondi direttamente alla richiesta."); | |
| } | |
| if (issues.some(i => /truncat|cut.*off|incomplet/i.test(i))) { | |
| // S266-fix: non dire "risposta precedente" — il LLM lo echeggia all'utente | |
| parts.push("Completa la risposta senza troncarla."); | |
| } | |
| if (issues.some(i => /repetiti|repeat/i.test(i))) { | |
| parts.push("Evita le ripetizioni. Sii conciso e diretto."); | |
| } | |
| if (issues.some(i => /hallucination/i.test(i))) { | |
| parts.push("Usa solo fatti verificabili. Se non sei sicuro, dì esplicitamente che non lo sai."); | |
| } | |
| if (quality < 2) { | |
| // S266-fix: non usare "la qualità della risposta è troppo bassa" — il LLM lo cita | |
| parts.push("Rispondi in modo preciso e strutturato."); | |
| } | |
| return parts.length > 0 | |
| ? `Non commentare problemi precedenti. ${parts.join(" ")}` | |
| : `Rispondi in modo più completo e accurato.`; | |
| } | |
| // ── Batch validation (per tool calls multipli) ──────────────────────────────── | |
| export interface BatchGateResult { | |
| results: ValidationGateResult[]; | |
| allAccepted: boolean; | |
| worstVerdict: GateVerdict; | |
| summary: string; | |
| } | |
| /** | |
| * Valida un array di output (es. risposte parallele da consensusMode). | |
| * Ritorna il risultato migliore tra quelli accettati. | |
| */ | |
| export function validateGateBatch( | |
| raws: string[], | |
| opts: GateOptions = {}, | |
| ): BatchGateResult { | |
| const results = raws.map(r => validateGate(r, opts)); | |
| const verdictPriority: Record<GateVerdict, number> = { | |
| accept: 0, | |
| repaired: 1, | |
| retry: 2, | |
| reject: 3, | |
| }; | |
| const worstVerdict = results.reduce<GateVerdict>( | |
| (worst, r) => verdictPriority[r.verdict] > verdictPriority[worst] ? r.verdict : worst, | |
| "accept" | |
| ); | |
| const allAccepted = results.every(r => r.verdict === "accept" || r.verdict === "repaired"); | |
| const accepted = results.filter(r => r.verdict === "accept" || r.verdict === "repaired"); | |
| const best = accepted.sort((a, b) => b.quality - a.quality)[0]; | |
| const summary = best | |
| ? `${accepted.length}/${raws.length} accettati — miglior quality: ${best.quality}/10` | |
| : `Nessun output accettato su ${raws.length}`; | |
| return { results, allAccepted, worstVerdict, summary }; | |
| } | |