pi-jev-guard 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,6 +6,7 @@ Verifica le risposte con [TypeSafe Jev](https://docs.typesafe.ai) dentro [pi](ht
6
6
  - **Domande tipate**: tool `jev_ask` per giudizi calibrati definiti dal modello (noul/choice/score) invece di prosa.
7
7
  - **Automatica**: gemello guarded `<modello>__jev` dentro lo stesso provider, che trattiene la risposta, la verifica con Jev e pubblica solo l'output approvato (con rigenerazione privata su `block`). Stessa auth del provider: nessun login extra, funziona anche con Codex/OAuth.
8
8
  - **Giudice output** (solo `automatic`, fail-open): dopo ogni `bash` cerca secret nel testo e classifica i fallimenti, allegando un consiglio al risultato.
9
+ - **Verifica progetto** (opt-in): footer deterministico da `edit`/`write` osservati e check esplicitamente autorizzati nel manifest locale; il modello può eseguire soltanto quegli id, mai shell arbitraria.
9
10
 
10
11
  ## Installazione
11
12
 
@@ -180,6 +181,57 @@ le contraddizioni ad alta confidenza. Fail-open: se il verificatore non
180
181
  risponde, il testo passa senza nota. `/jev last` mostra anche l'esito
181
182
  grounding (`warned`/`repaired`/`held`).
182
183
 
184
+ ### Verifica progetto deterministica (opt-in)
185
+
186
+ Il grounding è probabilistico; per **compilazione, lint e test** è meglio una
187
+ prova deterministica. La verifica progetto mantiene, solo nella sessione,
188
+ una lista di `edit`/`write` riusciti e degli esiti osservati dei check. Il
189
+ footer della risposta viene generato dall'estensione, non dal modello:
190
+ quindi non può trasformare un test non eseguito in “test passati”.
191
+
192
+ 1. Copia e adatta il manifest versionato/localmente controllato:
193
+
194
+ ```bash
195
+ mkdir -p .pi
196
+ cp config/jev-verification.example.json .pi/jev-verification.json
197
+ ```
198
+
199
+ 2. Controllalo tu e acquisiscilo nella sessione (o riacquisiscilo dopo una
200
+ modifica manuale):
201
+
202
+ ```text
203
+ /jev verification reload
204
+ /jev verification warn # footer trasparente, nessuna rigenerazione
205
+ # oppure: /jev verification repair|hold
206
+ ```
207
+
208
+ 3. Dopo un edit, il modello può eseguire soltanto gli id dichiarati nel
209
+ manifest — mai una shell string fornita dal modello:
210
+
211
+ ```text
212
+ /jev verify recommended # i check pertinenti non ancora freschi
213
+ /jev verify all # tutti, fino a verification.maxCommands
214
+ /jev verify typecheck # un id esplicito del manifest
215
+ ```
216
+
217
+ Il tool LLM `jev_verify` ha lo stesso vincolo. Richiede un progetto trusted e
218
+ non accetta comandi arbitrari. Footer/repair/hold operano nel twin in
219
+ `automatic`; `/jev verify` può essere usato manualmente anche per controllare
220
+ un progetto senza pubblicare un footer. Il manifest è acquisito a inizio sessione o
221
+ con `/jev verification reload`; se cambia dopo, la verifica rifiuta di
222
+ eseguire fino a una nuova acquisizione utente. `write`/`edit` del modello sul
223
+ manifest sono bloccati; anche modifiche via shell vengono rilevate dal
224
+ fingerprint e non aggiornano i comandi autorizzati. Il manifest è quindi una
225
+ policy dell'utente/progetto, non una superficie di prompt injection.
226
+
227
+ Modalità: `warn` aggiunge il footer; `repair` rigenera una volta con feedback
228
+ che chiede di invocare `jev_verify`; `hold` trattiene le risposte quando un
229
+ check è fallito, e dopo il tentativo bounded anche quando resta mancante.
230
+ Nessun manifest o nessun check pertinente è una limitazione dichiarata, non
231
+ un fallimento del codice e non produce hold. La raccolta delle modifiche è
232
+ limitata a `edit`/`write` osservati: una modifica fatta con shell non viene
233
+ attribuita automaticamente al modello.
234
+
183
235
  ## Configurazione
184
236
 
185
237
  `~/.pi/agent/jev-config.json` (o `$JEV_CONFIG`). Env: `JEV_MODE`, `JEV_BACKEND`
@@ -234,3 +286,6 @@ echo '{"requirements":"...","candidate":"..."}' | npm run jev-check
234
286
  - Il grounding controlla solo affermazioni verificabili sulla sessione
235
287
  (comandi, file, test, modifiche): niente fact-check di fatti esterni,
236
288
  e con contesti lunghi l'evidenza oltre budget viene elisa.
289
+ - La verifica progetto dimostra solo i check dichiarati ed eseguiti dopo gli
290
+ `edit`/`write` osservati; non è una prova formale di correttezza, non vede
291
+ modifiche effettuate via shell e non sostituisce review umana.
@@ -65,6 +65,15 @@
65
65
  "minConfidence": 0.6,
66
66
  "cacheSeconds": 120
67
67
  },
68
+ "verification": {
69
+ "enabled": false,
70
+ "mode": "warn",
71
+ "manifestPath": ".pi/jev-verification.json",
72
+ "maxRepairs": 1,
73
+ "commandTimeoutMs": 120000,
74
+ "outputChars": 6000,
75
+ "maxCommands": 3
76
+ },
68
77
  "exfilCheck": {
69
78
  "enabled": true,
70
79
  "tools": [
@@ -0,0 +1,22 @@
1
+ {
2
+ "version": 1,
3
+ "checks": [
4
+ {
5
+ "id": "typecheck",
6
+ "description": "TypeScript static check",
7
+ "command": "npm run typecheck",
8
+ "files": ["src/**/*.ts", "extensions/**/*.ts", "test/**/*.ts"]
9
+ },
10
+ {
11
+ "id": "unit",
12
+ "description": "Unit tests",
13
+ "command": "npm run test:unit",
14
+ "files": ["src/**/*.ts", "extensions/**/*.ts", "test/**/*.ts"]
15
+ },
16
+ {
17
+ "id": "diff-check",
18
+ "description": "Whitespace and patch integrity",
19
+ "command": "git diff --check"
20
+ }
21
+ ]
22
+ }
@@ -3,6 +3,7 @@ import type { Model, Provider } from "@earendil-works/pi-ai";
3
3
  import type { Api } from "@earendil-works/pi-ai";
4
4
  import { registerJevCommand, type UpstreamResult } from "../src/commands.ts";
5
5
  import { registerJevAskTool } from "../src/ask.ts";
6
+ import { registerVerificationTool, type VerificationToolRunResult } from "../src/verification-tool.ts";
6
7
  import {
7
8
  configSnapshot,
8
9
  loadConfig,
@@ -58,11 +59,33 @@ import {
58
59
  groundingKey,
59
60
  recordEvidence,
60
61
  recordToolResult,
62
+ parseExitCode,
61
63
  GROUNDING_QUESTIONS,
62
64
  type EvidenceEntry,
63
65
  type GroundingEvaluation,
64
66
  } from "../src/grounding.ts";
65
- import type { PostPublishCheckFn } from "../src/automatic/guardian.ts";
67
+ import type { PostPublishCheckFn, PostPublishCheckResult } from "../src/automatic/guardian.ts";
68
+ import {
69
+ buildVerificationStatus,
70
+ createVerificationState,
71
+ isVerificationManifestPath,
72
+ decideVerificationAction,
73
+ loadVerificationManifest,
74
+ manifestCheckForCommand,
75
+ normalizeObservedPath,
76
+ recordObservedModification,
77
+ recordVerificationRun,
78
+ selectVerificationChecks,
79
+ trustVerificationManifest,
80
+ trustedVerificationManifest,
81
+ type VerificationManifestTrust,
82
+ type VerificationStatus,
83
+ } from "../src/verification.ts";
84
+ import {
85
+ formatVerificationExecutions,
86
+ runVerificationChecks,
87
+ type VerificationExecution,
88
+ } from "../src/verification-runner.ts";
66
89
 
67
90
  export interface GateStatus {
68
91
  active: boolean;
@@ -100,6 +123,17 @@ export default function (pi: ExtensionAPI) {
100
123
  // Resettato a ogni session_start; solo automatic lo alimenta e lo usa.
101
124
  let evidenceLedger: EvidenceEntry[] = [];
102
125
  let evidenceSeq = 0;
126
+ // Stato distinto dal grounding: resta utile anche quando il grounding
127
+ // probabilistico è spento. Registra solo edit/write osservati e run di
128
+ // comandi approvati dal manifest del progetto.
129
+ let verificationState = createVerificationState();
130
+ let verificationSeq = 0;
131
+ /** Check bash/powershell iniziati davvero: seq=start, non result order. */
132
+ const verificationCheckStarts = new Map<string, { id: string; command: string; seq: number; source: "bash" | "powershell" }>();
133
+ let verificationCwd: string | undefined;
134
+ // Acquisito solo all'avvio sessione o con un comando utente esplicito.
135
+ // Il modello non può trasformare un edit al manifest in un comando eseguito.
136
+ let verificationManifestTrust: VerificationManifestTrust | undefined;
103
137
  let lastOutput: { tool: string; verdict: OutputVerdict; at: number } | undefined;
104
138
  let lastGate: { report: GateVerdictReport; at: number } | undefined;
105
139
 
@@ -189,6 +223,104 @@ export default function (pi: ExtensionAPI) {
189
223
  return getConfig();
190
224
  }
191
225
 
226
+ function captureVerificationManifest(cwd: string): VerificationManifestTrust {
227
+ verificationCwd = cwd;
228
+ verificationManifestTrust = trustVerificationManifest(cwd, config.verification.manifestPath);
229
+ return verificationManifestTrust;
230
+ }
231
+
232
+ function verificationSnapshot(cwd: string): {
233
+ loaded: ReturnType<typeof loadVerificationManifest>;
234
+ status: VerificationStatus;
235
+ } {
236
+ const loaded = trustedVerificationManifest(
237
+ verificationManifestTrust,
238
+ cwd,
239
+ config.verification.manifestPath,
240
+ );
241
+ return { loaded, status: buildVerificationStatus(verificationState, loaded) };
242
+ }
243
+
244
+ function verificationReport(
245
+ status: VerificationStatus,
246
+ action: "passed" | "warned" | "repaired" | "held",
247
+ ): NonNullable<GateVerdictReport["verification"]> {
248
+ return {
249
+ action,
250
+ changedFiles: [...status.changedFiles],
251
+ checks: status.checks.map((item) => ({ id: item.check.id, state: item.state })),
252
+ };
253
+ }
254
+
255
+ function recordVerificationExecution(
256
+ execution: VerificationExecution,
257
+ source: "jev_verify" | "bash" | "powershell" | "manual",
258
+ /** Marker al momento dell'avvio, non al tool_result. */
259
+ startedSeq = ++verificationSeq,
260
+ ): void {
261
+ recordVerificationRun(verificationState, {
262
+ id: execution.id,
263
+ command: execution.command,
264
+ ok: execution.ok,
265
+ ...(execution.exitCode !== undefined ? { exitCode: execution.exitCode } : {}),
266
+ seq: startedSeq,
267
+ source,
268
+ });
269
+ }
270
+
271
+ /**
272
+ * Esegue SOLO check estratti dal manifest locale validato. Questa funzione
273
+ * serve sia al tool del modello sia al comando utente /jev verify.
274
+ */
275
+ async function runProjectVerification(args: {
276
+ cwd: string;
277
+ selection: string | undefined;
278
+ signal?: AbortSignal;
279
+ trusted: boolean;
280
+ }): Promise<VerificationToolRunResult> {
281
+ const settings = config.verification;
282
+ if (!settings.enabled) {
283
+ return {
284
+ ok: false,
285
+ message: "[jev_verify] Verifica progetto disattivata. Attivala con /jev verification warn|repair|hold.",
286
+ results: [],
287
+ };
288
+ }
289
+ if (!args.trusted) {
290
+ return {
291
+ ok: false,
292
+ message: "[jev_verify] Progetto non trusted: nessun comando del manifest è stato eseguito.",
293
+ results: [],
294
+ };
295
+ }
296
+ const { loaded, status } = verificationSnapshot(args.cwd);
297
+ const selected = selectVerificationChecks(
298
+ loaded,
299
+ status,
300
+ args.selection,
301
+ settings.maxCommands,
302
+ );
303
+ if (!selected.ok) {
304
+ return { ok: false, message: `[jev_verify] ${selected.error}`, results: [] };
305
+ }
306
+ // Snapshot dell'ordine PRIMA dell'esecuzione: se un edit concorrente
307
+ // termina mentre il runner è attivo, questi risultati restano
308
+ // conservativamente stale/pending invece di essere falsamente freschi.
309
+ const startedSeq = ++verificationSeq;
310
+ const results = await runVerificationChecks({
311
+ checks: selected.checks,
312
+ cwd: args.cwd,
313
+ settings,
314
+ signal: args.signal,
315
+ });
316
+ for (const result of results) recordVerificationExecution(result, "jev_verify", startedSeq);
317
+ return {
318
+ ok: results.length > 0 && results.every((result) => result.ok),
319
+ message: formatVerificationExecutions(results),
320
+ results,
321
+ };
322
+ }
323
+
192
324
  /** Provider live corrente dal registry (sempre fresco, mai snapshot). */
193
325
  function liveUpstreamProvider(
194
326
  registry: ModelRegistry,
@@ -226,7 +358,7 @@ export default function (pi: ExtensionAPI) {
226
358
  getConfig,
227
359
  getReview: getGateReview,
228
360
  onVerdict: recordGateVerdict,
229
- createPostCheck: createGroundingCheck,
361
+ createPostCheck,
230
362
  });
231
363
  }
232
364
 
@@ -435,6 +567,8 @@ export default function (pi: ExtensionAPI) {
435
567
  keyPresent: () => resolveBackend(getConfig()).apiKeyPresent,
436
568
  });
437
569
 
570
+ registerVerificationTool(pi, { run: runProjectVerification });
571
+
438
572
  registerJevCommand(pi, {
439
573
  config: getConfig,
440
574
  setMode: (mode) => {
@@ -476,6 +610,36 @@ export default function (pi: ExtensionAPI) {
476
610
  saveConfig(next);
477
611
  return refresh();
478
612
  },
613
+ verificationStatus: (cwd) => {
614
+ const { loaded, status } = verificationSnapshot(cwd);
615
+ return {
616
+ enabled: config.verification.enabled,
617
+ mode: config.verification.mode,
618
+ manifestPath: config.verification.manifestPath,
619
+ ...(loaded.ok ? { manifestLoaded: true } : { manifestLoaded: false, manifestError: loaded.error }),
620
+ changedFiles: status.changedFiles,
621
+ checks: status.checks.map((item) => ({ id: item.check.id, state: item.state })),
622
+ };
623
+ },
624
+ setVerification: (mode) => {
625
+ const next = configSnapshot(config);
626
+ if (mode === "off") {
627
+ next.verification.enabled = false;
628
+ } else {
629
+ next.verification.enabled = true;
630
+ next.verification.mode = mode;
631
+ }
632
+ saveConfig(next);
633
+ return refresh();
634
+ },
635
+ reloadVerificationManifest: (cwd) => {
636
+ const trusted = captureVerificationManifest(cwd);
637
+ return trusted.ok
638
+ ? { ok: true, path: trusted.path }
639
+ : { ok: false, error: trusted.error };
640
+ },
641
+ runVerification: ({ cwd, selection, signal, trusted }) =>
642
+ runProjectVerification({ cwd, selection, signal, trusted }),
479
643
  getLastGate: () => lastGate,
480
644
  });
481
645
 
@@ -492,6 +656,10 @@ export default function (pi: ExtensionAPI) {
492
656
  // Nuova sessione = nuova evidenza: il ledger non attraversa le sessioni.
493
657
  evidenceLedger = [];
494
658
  evidenceSeq = 0;
659
+ verificationState = createVerificationState();
660
+ verificationSeq = 0;
661
+ verificationCheckStarts.clear();
662
+ captureVerificationManifest(ctx.cwd);
495
663
  // Patch dei twin salvati in OGNI modalità (nessun setModel qui): senza,
496
664
  // un twin persistito non esisterebbe nel catalogo. Silenzioso: la
497
665
  // diagnostica resta agli hook di turno.
@@ -637,6 +805,7 @@ export default function (pi: ExtensionAPI) {
637
805
  // before_agent_start, ripatcha in silenzio (mai register/unregister).
638
806
  pi.on("turn_start", async (_event, ctx) => {
639
807
  try {
808
+ verificationCwd = ctx.cwd;
640
809
  ensurePatches(ctx.modelRegistry);
641
810
  } catch {
642
811
  // Mai bloccare il turno.
@@ -645,6 +814,7 @@ export default function (pi: ExtensionAPI) {
645
814
 
646
815
  pi.on("before_agent_start", async (_event, ctx) => {
647
816
  try {
817
+ verificationCwd = ctx.cwd;
648
818
  noteReassert(ensurePatches(ctx.modelRegistry));
649
819
  } catch {
650
820
  // mai bloccare il turno
@@ -749,6 +919,38 @@ export default function (pi: ExtensionAPI) {
749
919
  }
750
920
  });
751
921
 
922
+ /**
923
+ * Marca l'INIZIO reale di un check shell già autorizzato. Non basta
924
+ * l'ordine dei tool_result: bash e edit possono essere concorrenti e un
925
+ * test avviato prima di un edit non deve risultare fresco dopo di esso.
926
+ */
927
+ pi.on("tool_execution_start", async (event, ctx) => {
928
+ try {
929
+ if (sessionMode !== "automatic") return;
930
+ if (event.toolName !== "bash" && event.toolName !== "powershell") return;
931
+ verificationCwd = ctx.cwd;
932
+ const { loaded } = verificationSnapshot(ctx.cwd);
933
+ if (!loaded.ok) return;
934
+ const args = event.args as { command?: unknown };
935
+ const check = manifestCheckForCommand(loaded.manifest, args.command);
936
+ if (!check) return;
937
+ verificationCheckStarts.set(event.toolCallId, {
938
+ id: check.id,
939
+ command: check.command,
940
+ seq: ++verificationSeq,
941
+ source: event.toolName,
942
+ });
943
+ // Difesa memoria contro toolcall senza result (abort esterno).
944
+ while (verificationCheckStarts.size > 100) {
945
+ const oldest = verificationCheckStarts.keys().next().value;
946
+ if (oldest === undefined) break;
947
+ verificationCheckStarts.delete(oldest);
948
+ }
949
+ } catch {
950
+ // Nessun impatto sull'esecuzione del tool.
951
+ }
952
+ });
953
+
752
954
  // Policy pre-esecuzione tool (solo automatic).
753
955
  pi.on("tool_call", async (event, ctx) => {
754
956
  try {
@@ -780,6 +982,20 @@ export default function (pi: ExtensionAPI) {
780
982
 
781
983
  if (event.toolName === "write" || event.toolName === "edit") {
782
984
  const input = event.input as { path?: unknown };
985
+ if (isVerificationManifestPath(input.path, ctx.cwd, config.verification.manifestPath)) {
986
+ pi.appendEntry("jev-tool-block", {
987
+ tool: event.toolName,
988
+ ruleId: "verification-manifest-immutable",
989
+ revision: TOOL_POLICY_REVISION,
990
+ });
991
+ if (ctx.hasUI) {
992
+ ctx.ui.notify("Jev tool policy: il modello non può modificare il manifest che autorizza i suoi check.", "warning");
993
+ }
994
+ return {
995
+ block: true,
996
+ reason: "Bloccato da jev tool policy (verification-manifest-immutable): modifica il manifest manualmente e poi usa /jev verification reload.",
997
+ };
998
+ }
783
999
  const verdict = checkFilePath(input.path);
784
1000
  if (verdict.blocked) {
785
1001
  pi.appendEntry("jev-tool-block", {
@@ -872,6 +1088,60 @@ export default function (pi: ExtensionAPI) {
872
1088
  return allow ? undefined : { block: true, reason: `Bloccato da jev exfil check (${summary}) (declined).` };
873
1089
  }
874
1090
 
1091
+ /**
1092
+ * Registra modifiche e risultati dei check configurati. È deliberatamente
1093
+ * separato dal grounding: il footer deterministico deve funzionare anche
1094
+ * senza una chiamata Jev aggiuntiva. I bash nativi contano soltanto se il
1095
+ * loro comando coincide ESATTAMENTE con un id del manifest.
1096
+ */
1097
+ function recordVerificationResult(event: {
1098
+ toolCallId?: string;
1099
+ toolName: string;
1100
+ input: unknown;
1101
+ content: unknown;
1102
+ isError: boolean;
1103
+ }, cwd: string): void {
1104
+ try {
1105
+ // Raccolta locale senza costo anche se il footer è spento: attivarlo a
1106
+ // metà sessione conserva gli edit/write già osservati. Pubblicazione,
1107
+ // repair ed esecuzione restano rigorosamente opt-in in config.
1108
+ if (sessionMode !== "automatic") return;
1109
+ verificationCwd = cwd;
1110
+ if (event.toolName === "edit" || event.toolName === "write") {
1111
+ if (event.isError) return;
1112
+ const input = event.input as { path?: unknown };
1113
+ const path = normalizeObservedPath(input.path, cwd);
1114
+ if (path) recordObservedModification(verificationState, path, ++verificationSeq);
1115
+ return;
1116
+ }
1117
+ if (event.toolName !== "bash" && event.toolName !== "powershell") return;
1118
+ const input = event.input as { command?: unknown };
1119
+ const started = event.toolCallId
1120
+ ? verificationCheckStarts.get(event.toolCallId)
1121
+ : undefined;
1122
+ if (event.toolCallId) verificationCheckStarts.delete(event.toolCallId);
1123
+ const { loaded } = verificationSnapshot(cwd);
1124
+ if (!loaded.ok) return;
1125
+ const check = manifestCheckForCommand(loaded.manifest, input.command);
1126
+ if (!check) return;
1127
+ const output = toolResultText(event.content);
1128
+ const exitCode = parseExitCode(output);
1129
+ recordVerificationRun(verificationState, {
1130
+ id: check.id,
1131
+ command: check.command,
1132
+ ok: !event.isError,
1133
+ ...(exitCode !== undefined ? { exitCode } : {}),
1134
+ // In runtime il marker esiste perché tool_execution_start precede il
1135
+ // result. Fallback conservativo per adapter/test legacy senza evento:
1136
+ // allocato qui, mai riusa un pass precedente.
1137
+ seq: started?.seq ?? ++verificationSeq,
1138
+ source: event.toolName,
1139
+ });
1140
+ } catch {
1141
+ // Un footer non deve mai cambiare il risultato di un tool.
1142
+ }
1143
+ }
1144
+
875
1145
  /** Registra un tool_result nel ledger evidenza (solo automatic, mai bloccare). */
876
1146
  function recordGroundingResult(event: {
877
1147
  toolName: string;
@@ -1020,12 +1290,127 @@ export default function (pi: ExtensionAPI) {
1020
1290
  }
1021
1291
  }
1022
1292
 
1293
+ /**
1294
+ * Post-check deterministico del codice: non dichiara il codice corretto,
1295
+ * ma allega il fatto osservato (edit/write + check manifest). In repair il
1296
+ * modello ha un solo giro per invocare `jev_verify` e correggere l'errore;
1297
+ * in hold una failure concreta non viene pubblicata.
1298
+ */
1299
+ function createVerificationCheck(): PostPublishCheckFn | undefined {
1300
+ try {
1301
+ if (sessionMode !== "automatic" || !config.verification.enabled) return undefined;
1302
+ const cwd = verificationCwd;
1303
+ if (!cwd) return undefined;
1304
+ const mode = config.verification.mode;
1305
+ const maxRepairs = config.verification.maxRepairs;
1306
+ let repairsUsed = 0;
1307
+ return async () => {
1308
+ const { status } = verificationSnapshot(cwd);
1309
+ if (status.changedFiles.length === 0) return { action: "publish" };
1310
+ const decision = decideVerificationAction({
1311
+ status,
1312
+ mode,
1313
+ repairsUsed,
1314
+ maxRepairs,
1315
+ });
1316
+ if (decision.action === "regenerate") {
1317
+ repairsUsed++;
1318
+ try {
1319
+ pi.appendEntry("jev-verification-repair", {
1320
+ files: status.changedFiles,
1321
+ checks: status.checks.map((item) => ({ id: item.check.id, state: item.state })),
1322
+ });
1323
+ } catch {
1324
+ // solo diagnostica
1325
+ }
1326
+ return {
1327
+ action: "regenerate",
1328
+ feedback: decision.feedback,
1329
+ verification: verificationReport(status, "repaired"),
1330
+ };
1331
+ }
1332
+ if (decision.action === "hold") {
1333
+ try {
1334
+ pi.appendEntry("jev-verification-hold", {
1335
+ files: status.changedFiles,
1336
+ checks: status.checks.map((item) => ({ id: item.check.id, state: item.state })),
1337
+ });
1338
+ } catch {
1339
+ // solo diagnostica
1340
+ }
1341
+ return {
1342
+ action: "hold",
1343
+ holdText: decision.holdText,
1344
+ verification: verificationReport(status, "held"),
1345
+ };
1346
+ }
1347
+ // "repaired" solo se il giro di repair è arrivato davvero a check
1348
+ // freschi tutti passati; altrimenti resta un warning, mai un successo
1349
+ // inventato solo perché il modello ha ricevuto feedback.
1350
+ const action = status.allPassed
1351
+ ? repairsUsed > 0 ? "repaired" : "passed"
1352
+ : "warned";
1353
+ try {
1354
+ pi.appendEntry("jev-verification-status", {
1355
+ action,
1356
+ files: status.changedFiles,
1357
+ checks: status.checks.map((item) => ({ id: item.check.id, state: item.state })),
1358
+ ...(status.manifestError ? { manifestError: status.manifestError } : {}),
1359
+ });
1360
+ } catch {
1361
+ // solo diagnostica
1362
+ }
1363
+ return {
1364
+ action: "publish",
1365
+ ...(decision.notice ? { notice: decision.notice } : {}),
1366
+ verification: verificationReport(status, action),
1367
+ };
1368
+ };
1369
+ } catch {
1370
+ // Fail-open: errore del footer non deve bloccare il gate.
1371
+ return undefined;
1372
+ }
1373
+ }
1374
+
1375
+ /** Compone grounding probabilistico e verifica deterministica senza perderne le note. */
1376
+ function createPostCheck(): PostPublishCheckFn | undefined {
1377
+ const grounding = createGroundingCheck();
1378
+ const verification = createVerificationCheck();
1379
+ if (!grounding && !verification) return undefined;
1380
+ return async (args) => {
1381
+ let groundingResult: PostPublishCheckResult | undefined;
1382
+ if (grounding) {
1383
+ groundingResult = await grounding(args);
1384
+ if (groundingResult.action !== "publish") return groundingResult;
1385
+ }
1386
+ let verificationResult: PostPublishCheckResult | undefined;
1387
+ if (verification) {
1388
+ verificationResult = await verification(args);
1389
+ if (verificationResult.action !== "publish") {
1390
+ return {
1391
+ ...verificationResult,
1392
+ ...(groundingResult?.grounding ? { grounding: groundingResult.grounding } : {}),
1393
+ };
1394
+ }
1395
+ }
1396
+ const notices = [groundingResult?.notice, verificationResult?.notice]
1397
+ .filter((notice): notice is string => Boolean(notice));
1398
+ return {
1399
+ action: "publish",
1400
+ ...(notices.length > 0 ? { notice: notices.join("") } : {}),
1401
+ ...(groundingResult?.grounding ? { grounding: groundingResult.grounding } : {}),
1402
+ ...(verificationResult?.verification ? { verification: verificationResult.verification } : {}),
1403
+ };
1404
+ };
1405
+ }
1406
+
1023
1407
  // Giudice output (solo automatic, fail-open): dopo l'esecuzione cerca
1024
1408
  // secret nel testo e classifica i fallimenti. Non blocca mai: allega
1025
1409
  // una riga al risultato che il modello legge.
1026
1410
  pi.on("tool_result", async (event, ctx) => {
1027
1411
  try {
1028
1412
  // Ledger prima di tutto: vale anche con outputJudge disattivato.
1413
+ recordVerificationResult(event, ctx.cwd);
1029
1414
  recordGroundingResult(event);
1030
1415
  if (sessionMode !== "automatic") return;
1031
1416
  if (!config.outputJudge.enabled) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-jev-guard",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "description": "Jev validation guard for pi: on-demand tool + automatic provider gate. Dual backend: TypeSafe direct + OpenRouter.",
6
6
  "keywords": [