File size: 6,032 Bytes
cc11e77
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
/**
 * 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<ClaimCategory, RegExp[]> = {
  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<ClaimCategory> {
  const out = new Set<ClaimCategory>();
  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<ClaimCategory>,
  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;
}