/** * actionClaimDetector.ts — GAP-TRUTH: rileva affermazioni di azioni completate * nel testo finale e le confronta con proofLedger. * * Problema risolto: il finalOutputGate decideva "richiede prova?" guardando * `steps` (i tool effettivamente chiamati) — ma un LLM può scrivere * "Ho fatto il commit e il deploy" SENZA aver chiamato git_push/deploy_trigger * affatto. In quel caso `steps` non contiene alcun CRIT_TOOL → il gate * (versione precedente) passava SEMPRE, perché `requiresProof` derivava * solo dai tool call, non dal TESTO. * * Questo modulo chiude il cerchio: legge il TESTO (cosa l'LLM dichiara di * aver fatto) e lo incrocia con proofLedger (cosa è stato VERAMENTE * verificato), indipendentemente da `steps`. * * Design: * - Solo pattern in PRIMA PERSONA + TEMPO COMPLETATO ("ho fatto", "I've done", * "completato", "successfully") — evita falsi positivi su spiegazioni * generiche ("quando fai il commit...", "per pushare devi..."). * - Pure, sync. Unico import: type-only da proofLedger (zero costo runtime). * - Best-effort: in caso di eccezione interna, ritorna "nessuna claim" * (mai bloccante — il blocco fail-closed vero è in finalOutputGate * per garbage/empty, non qui). * * @module actionClaimDetector */ import type { Proof } from "./proofLedger"; // GAP-TRUTH-3: ClaimCategory, CATEGORY_LABELS, CATEGORY_TO_LEDGER_ACTIONS → single source of truth import { type ClaimCategory, CATEGORY_LABELS, CATEGORY_TO_LEDGER_ACTIONS, } from "./proofToolRegistry"; // Re-export per backward compat dei consumer esistenti (emitFinalText.ts, bench-truth-gate.mjs) export type { ClaimCategory }; export { CATEGORY_LABELS, CATEGORY_TO_LEDGER_ACTIONS }; // ClaimCategory, CATEGORY_LABELS, CATEGORY_TO_LEDGER_ACTIONS → vedi proofToolRegistry.ts (re-exportati sopra) // ─── Pattern: prima persona + completamento (IT + EN) ───────────────────────── // Tutti richiedono "ho/I've/I have/è stato/has been/successfully" + verbo al // participio passato — NON matchano imperativi, futuro, o spiegazioni generiche. const _CLAIM_PATTERNS: Record = { git_push: [ /\b(ho|abbiamo)\s+(già\s+)?(fatto\s+(il\s+)?)?(commit(t?ato)?|push(at[oi])?|spint[oi])\b/i, /\b(commit|push)\s+(e\s+(commit|push)\s+)?(è\s+stat[oi]\s+|sono\s+stat[ei]\s+)?(effettuat[oi]|complet[ao]t[oi]|fatt[oi]|riuscit[oi])\b/i, /\bho\s+(già\s+)?(caricat[oi]|invi?at[oi])\s+(le\s+)?modifiche/i, /\bmodifiche\s+(caricate|pushate|inviate|già\s+su)\s+(su\s+)?(git(hub)?|repo)/i, /\bI(?:'ve| have)\s+(already\s+)?(committed|pushed|push(ed)?\s+(the\s+)?changes)\b/i, /\b(commit|push)(ing)?\s+(is\s+)?(done|completed|successful)\b/i, /\bsuccessfully\s+(pushed|committed)\b/i, /\bchanges\s+(have\s+been|are)\s+(pushed|committed)\b/i, ], deploy: [ /\b(ho|abbiamo)\s+(già\s+)?(fatto\s+(il\s+)?)?deploy(at[oi])?\b/i, /\bdeploy\s+(è\s+stat[oi]\s+|sono\s+stat[ei]\s+)?(effettuat[oi]|complet[ao]t[oi]|riuscit[oi])\b/i, /\b(il\s+)?sito\s+(è\s+|risulta\s+)?(ora\s+)?(online|pubblicat[oi]|live|aggiornat[oi])\b/i, /\bI(?:'ve| have)\s+(already\s+)?deployed\b/i, /\bdeploy(ment)?\s+(is\s+)?(done|completed|successful|live)\b/i, /\bsuccessfully\s+deployed\b/i, /\b(the\s+)?site\s+is\s+(now\s+)?(live|online|updated)\b/i, ], file_write: [ /\b(ho|abbiamo)\s+(già\s+)?(creat[oi]|salvat[oi]|scritt[oi]|aggiornat[oi]|modificat[oi]|sovrascritt[oi])\s+(il\s+)?file/i, /\bfile\s+(è\s+stat[oi]\s+|sono\s+stat[ei]\s+)?(creat[oi]|salvat[oi]|aggiornat[oi]|scritt[oi]|modificat[oi])\b/i, /\bI(?:'ve| have)\s+(already\s+)?(created|saved|written|updated|overwritten)\s+(the\s+)?file/i, /\b(the\s+)?file\s+(has\s+been|is\s+now)\s+(created|saved|updated|written)\b/i, ], code_exec: [ /\b(ho|abbiamo)\s+(già\s+)?eseguit[oi]\s+(il\s+)?codice/i, /\bcodice\s+(è\s+stat[oi]\s+)?eseguit[oi]\s+(con\s+successo)?/i, /\bI(?:'ve| have)\s+(already\s+)?(run|executed)\s+(the\s+)?code/i, /\bcode\s+(has\s+)?(ran|executed|run)\s+successfully\b/i, ], }; /** * Rileva quali categorie di azione il TESTO dichiara come completate. * Scansiona tutte le categorie; ritorna l'insieme di quelle con almeno * un pattern match in prima persona + completamento. */ export function detectClaimedCategories(text: string): Set { const out = new Set(); if (!text || text.length < 8) return out; try { // Limita la scansione ai primi 4000 char — i claim di completamento // stanno tipicamente in apertura/chiusura della risposta, non nel mezzo // di blocchi di codice lunghi. const sample = text.length > 4000 ? text.slice(0, 2000) + text.slice(-2000) : text; for (const cat of Object.keys(_CLAIM_PATTERNS) as ClaimCategory[]) { if (_CLAIM_PATTERNS[cat].some(re => re.test(sample))) out.add(cat); } } catch { /* best-effort — mai bloccante */ } return out; } /** * Per ogni categoria dichiarata, verifica se proofLedger ha almeno una prova * VERIFICATA per i tool corrispondenti. * * @returns lista di etichette leggibili per le categorie PROBLEMATICHE * (claim presente, ma nessuna prova verificata) — vuota se tutto ok. */ export function checkClaimsAgainstLedger( claimed: Set, snapshot: readonly Proof[], ): string[] { const problems: string[] = []; try { for (const cat of claimed) { const actions = CATEGORY_TO_LEDGER_ACTIONS[cat]; const entries = snapshot.filter(p => actions.includes(p.action)); if (entries.length === 0) { problems.push(`${CATEGORY_LABELS[cat]} (nessuna azione registrata)`); } else if (!entries.some(e => e.verified)) { problems.push(`${CATEGORY_LABELS[cat]} (eseguito ma non verificato)`); } // entries.some(verified) → almeno una prova verificata per questa categoria → OK } } catch { /* best-effort — mai bloccante */ } return problems; }