pi-jev-guard 0.5.0 → 0.6.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,7 +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
+ - **Verifica progetto** (opt-in): footer deterministico da `edit`/`write` osservati e check autorizzati da override locale, manifest globale utente o profilo comune integrato; il modello può eseguire soltanto quegli id, mai shell arbitraria.
10
10
 
11
11
  ## Installazione
12
12
 
@@ -183,54 +183,101 @@ grounding (`warned`/`repaired`/`held`).
183
183
 
184
184
  ### Verifica progetto deterministica (opt-in)
185
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”.
186
+ Il grounding è probabilistico; per **compilazione, lint e test** serve una
187
+ prova deterministica. La verifica progetto conserva, solo nella sessione,
188
+ `edit`/`write` riusciti e gli esiti osservati dei check. Il footer viene
189
+ generato dall'estensione, non dal modello: un test non eseguito non può
190
+ diventare “test passati”.
191
191
 
192
- 1. Copia e adatta il manifest versionato/localmente controllato:
192
+ #### Nuovi progetti: zero manifest locale
193
+
194
+ Una volta, in una sessione col twin `automatic`, abilita la modalità scelta:
195
+
196
+ ```text
197
+ /jev verification repair
198
+ ```
199
+
200
+ Da quel momento ogni progetto usa questa precedenza:
201
+
202
+ 1. `.pi/jev-verification.json` locale, se presente;
203
+ 2. `verification.globalManifestPath`, se l'hai configurato nella config
204
+ utente;
205
+ 3. profilo integrato `common` (default).
206
+
207
+ Il profilo `common` non legge comandi dal repository: abilita esclusivamente
208
+ comandi fissi dell'estensione quando trova segnali convenzionali:
209
+
210
+ | Rilevamento | Check disponibili |
211
+ |---|---|
212
+ | `package.json` con script | `npm run typecheck`, `npm run lint`, uno tra `npm run test:unit`, `npm run test:ci`, `npm test` |
213
+ | `Cargo.toml` | `cargo check`, `cargo test` |
214
+ | `go.mod` | `go test ./...` |
215
+ | configurazione pytest | `python -m pytest` |
216
+ | repository Git | `git diff --check` |
217
+
218
+ Il profilo sceglie un solo ecosistema (Node → Rust → Go → Python) e aggiunge
219
+ il check Git quando applicabile. I comandi sono bounded da
220
+ `verification.maxCommands` (default `4`) e timeout. Se nessun segnale è
221
+ rilevato, non esegue nulla e il footer dichiara la limitazione.
222
+
223
+ Dopo un edit il modello può scegliere solo id già acquisiti:
224
+
225
+ ```text
226
+ /jev verify recommended # check pertinenti non ancora freschi
227
+ /jev verify all # tutti, fino a verification.maxCommands
228
+ /jev verify node-typecheck # un id esplicito della policy corrente
229
+ ```
230
+
231
+ Il tool LLM `jev_verify` ha lo stesso vincolo, richiede un progetto trusted e
232
+ non accetta shell arbitraria. Footer/repair/hold operano nel twin
233
+ `automatic`; `/jev verify` è utilizzabile manualmente anche senza footer.
234
+
235
+ #### Override per progetti speciali
236
+
237
+ Per una pipeline specifica, aggiungi un manifest al progetto: prevale sempre
238
+ sul profilo globale.
193
239
 
194
240
  ```bash
195
241
  mkdir -p .pi
196
242
  cp config/jev-verification.example.json .pi/jev-verification.json
197
243
  ```
198
244
 
199
- 2. Controllalo tu e acquisiscilo nella sessione (o riacquisiscilo dopo una
200
- modifica manuale):
245
+ Controllalo tu, poi acquisiscilo (e ripeti dopo ogni modifica manuale):
201
246
 
202
247
  ```text
203
248
  /jev verification reload
204
- /jev verification warn # footer trasparente, nessuna rigenerazione
205
- # oppure: /jev verification repair|hold
206
249
  ```
207
250
 
208
- 3. Dopo un edit, il modello può eseguire soltanto gli id dichiarati nel
209
- manifest — mai una shell string fornita dal modello:
251
+ In alternativa puoi definire **una** policy comune per tutti i tuoi progetti
252
+ nella config utente, purché esista davvero e sia sotto il tuo controllo:
210
253
 
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
254
+ ```json
255
+ {
256
+ "verification": {
257
+ "globalManifestPath": "~/.pi/agent/jev-verification.global.json",
258
+ "defaultProfile": "common"
259
+ }
260
+ }
215
261
  ```
216
262
 
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.
263
+ Un `globalManifestPath` configurato ma assente o malformato non degrada
264
+ silenziosamente al profilo integrato: la verifica segnala l'errore. Per
265
+ disattivare il fallback integrato imposta `"defaultProfile": "off"`.
266
+ `/jev verification` e `/jev status` mostrano l'origine effettiva della policy.
267
+
268
+ Manifest e profilo sono acquisiti a inizio sessione, all'attivazione esplicita
269
+ o con `/jev verification reload`. Se un manifest, un `package.json` o un file
270
+ di configurazione che abilita il profilo cambia dopo l'acquisizione, nessun
271
+ check parte fino al reload utente. In automatic, con tool policy e verifica
272
+ attive, `write`/`edit` del modello su tali file sono bloccati; una modifica via shell
273
+ viene comunque rilevata dal fingerprint. Questo evita che il modello trasformi
274
+ uno script o un manifest in un nuovo comando autorizzato.
226
275
 
227
276
  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.
277
+ che chiede `jev_verify`; `hold` trattiene dopo una failure concreta, o dopo il
278
+ tentativo bounded se restano check mancanti. Nessun check pertinente non è un
279
+ fallimento del codice e non produce hold. Le modifiche rilevate restano solo
280
+ quelle di `edit`/`write`: una modifica shell non viene attribuita al modello.
234
281
 
235
282
  ## Configurazione
236
283
 
@@ -69,10 +69,11 @@
69
69
  "enabled": false,
70
70
  "mode": "warn",
71
71
  "manifestPath": ".pi/jev-verification.json",
72
+ "defaultProfile": "common",
72
73
  "maxRepairs": 1,
73
74
  "commandTimeoutMs": 120000,
74
75
  "outputChars": 6000,
75
- "maxCommands": 3
76
+ "maxCommands": 4
76
77
  },
77
78
  "exfilCheck": {
78
79
  "enabled": true,
@@ -68,16 +68,16 @@ import type { PostPublishCheckFn, PostPublishCheckResult } from "../src/automati
68
68
  import {
69
69
  buildVerificationStatus,
70
70
  createVerificationState,
71
- isVerificationManifestPath,
71
+ isVerificationPolicyPath,
72
72
  decideVerificationAction,
73
- loadVerificationManifest,
73
+ loadVerificationPolicy,
74
74
  manifestCheckForCommand,
75
75
  normalizeObservedPath,
76
76
  recordObservedModification,
77
77
  recordVerificationRun,
78
78
  selectVerificationChecks,
79
- trustVerificationManifest,
80
- trustedVerificationManifest,
79
+ trustVerificationPolicy,
80
+ trustedVerificationPolicy,
81
81
  type VerificationManifestTrust,
82
82
  type VerificationStatus,
83
83
  } from "../src/verification.ts";
@@ -225,18 +225,18 @@ export default function (pi: ExtensionAPI) {
225
225
 
226
226
  function captureVerificationManifest(cwd: string): VerificationManifestTrust {
227
227
  verificationCwd = cwd;
228
- verificationManifestTrust = trustVerificationManifest(cwd, config.verification.manifestPath);
228
+ verificationManifestTrust = trustVerificationPolicy(cwd, config.verification);
229
229
  return verificationManifestTrust;
230
230
  }
231
231
 
232
232
  function verificationSnapshot(cwd: string): {
233
- loaded: ReturnType<typeof loadVerificationManifest>;
233
+ loaded: ReturnType<typeof loadVerificationPolicy>;
234
234
  status: VerificationStatus;
235
235
  } {
236
- const loaded = trustedVerificationManifest(
236
+ const loaded = trustedVerificationPolicy(
237
237
  verificationManifestTrust,
238
238
  cwd,
239
- config.verification.manifestPath,
239
+ config.verification,
240
240
  );
241
241
  return { loaded, status: buildVerificationStatus(verificationState, loaded) };
242
242
  }
@@ -269,8 +269,8 @@ export default function (pi: ExtensionAPI) {
269
269
  }
270
270
 
271
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.
272
+ * Esegue SOLO check estratti dalla policy acquisita (locale, globale o
273
+ * profilo integrato). Serve sia al tool del modello sia a /jev verify.
274
274
  */
275
275
  async function runProjectVerification(args: {
276
276
  cwd: string;
@@ -616,7 +616,10 @@ export default function (pi: ExtensionAPI) {
616
616
  enabled: config.verification.enabled,
617
617
  mode: config.verification.mode,
618
618
  manifestPath: config.verification.manifestPath,
619
- ...(loaded.ok ? { manifestLoaded: true } : { manifestLoaded: false, manifestError: loaded.error }),
619
+ defaultProfile: config.verification.defaultProfile,
620
+ ...(loaded.ok
621
+ ? { manifestLoaded: true, manifestSource: loaded.source, manifestSourceLabel: loaded.sourceLabel }
622
+ : { manifestLoaded: false, manifestError: loaded.error }),
620
623
  changedFiles: status.changedFiles,
621
624
  checks: status.checks.map((item) => ({ id: item.check.id, state: item.state })),
622
625
  };
@@ -630,12 +633,16 @@ export default function (pi: ExtensionAPI) {
630
633
  next.verification.mode = mode;
631
634
  }
632
635
  saveConfig(next);
633
- return refresh();
636
+ const refreshed = refresh();
637
+ // L'attivazione è un'azione utente esplicita: acquisisce gli input
638
+ // correnti del fallback invece di riusare una snapshot presa quando era off.
639
+ if (mode !== "off" && verificationCwd) captureVerificationManifest(verificationCwd);
640
+ return refreshed;
634
641
  },
635
642
  reloadVerificationManifest: (cwd) => {
636
643
  const trusted = captureVerificationManifest(cwd);
637
644
  return trusted.ok
638
- ? { ok: true, path: trusted.path }
645
+ ? { ok: true, path: trusted.path, sourceLabel: trusted.sourceLabel }
639
646
  : { ok: false, error: trusted.error };
640
647
  },
641
648
  runVerification: ({ cwd, selection, signal, trusted }) =>
@@ -982,18 +989,34 @@ export default function (pi: ExtensionAPI) {
982
989
 
983
990
  if (event.toolName === "write" || event.toolName === "edit") {
984
991
  const input = event.input as { path?: unknown };
985
- if (isVerificationManifestPath(input.path, ctx.cwd, config.verification.manifestPath)) {
992
+ const profileInput = verificationManifestTrust?.ok && verificationManifestTrust.source === "global-profile";
993
+ if (
994
+ isVerificationPolicyPath(input.path, ctx.cwd, verificationManifestTrust) &&
995
+ // Il profilo comune è opt-in: non rendere package.json immutabile
996
+ // nelle installazioni che non hanno attivato la verifica progetto.
997
+ (!profileInput || config.verification.enabled)
998
+ ) {
999
+ const ruleId = profileInput
1000
+ ? "verification-profile-input-immutable"
1001
+ : "verification-manifest-immutable";
986
1002
  pi.appendEntry("jev-tool-block", {
987
1003
  tool: event.toolName,
988
- ruleId: "verification-manifest-immutable",
1004
+ ruleId,
989
1005
  revision: TOOL_POLICY_REVISION,
990
1006
  });
991
1007
  if (ctx.hasUI) {
992
- ctx.ui.notify("Jev tool policy: il modello non può modificare il manifest che autorizza i suoi check.", "warning");
1008
+ ctx.ui.notify(
1009
+ profileInput
1010
+ ? "Jev tool policy: il modello non può modificare un file che decide i check del profilo globale acquisito."
1011
+ : "Jev tool policy: il modello non può modificare il manifest che autorizza i suoi check.",
1012
+ "warning",
1013
+ );
993
1014
  }
994
1015
  return {
995
1016
  block: true,
996
- reason: "Bloccato da jev tool policy (verification-manifest-immutable): modifica il manifest manualmente e poi usa /jev verification reload.",
1017
+ reason: profileInput
1018
+ ? "Bloccato da jev tool policy (verification-profile-input-immutable): modifica manualmente la configurazione del progetto e poi usa /jev verification reload."
1019
+ : "Bloccato da jev tool policy (verification-manifest-immutable): modifica il manifest manualmente e poi usa /jev verification reload.",
997
1020
  };
998
1021
  }
999
1022
  const verdict = checkFilePath(input.path);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-jev-guard",
3
- "version": "0.5.0",
3
+ "version": "0.6.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": [
package/src/commands.ts CHANGED
@@ -19,7 +19,7 @@ import { reviewWithCoverage } from "./reviewer.ts";
19
19
  import type { Metrics } from "./metrics.ts";
20
20
  import type { OutputVerdict } from "./output-judge.ts";
21
21
  import type { GateVerdictReport } from "./automatic/guardian.ts";
22
- import type { VerificationMode } from "./verification.ts";
22
+ import type { VerificationDefaultProfile, VerificationManifestSource, VerificationMode } from "./verification.ts";
23
23
 
24
24
  export interface GateStatusView {
25
25
  active: boolean;
@@ -84,7 +84,10 @@ export interface JevCommandDeps {
84
84
  enabled: boolean;
85
85
  mode: VerificationMode;
86
86
  manifestPath: string;
87
+ defaultProfile: VerificationDefaultProfile;
87
88
  manifestLoaded: boolean;
89
+ manifestSource?: VerificationManifestSource;
90
+ manifestSourceLabel?: string;
88
91
  manifestError?: string;
89
92
  changedFiles: string[];
90
93
  checks: Array<{ id: string; state: "passed" | "failed" | "pending" }>;
@@ -94,6 +97,7 @@ export interface JevCommandDeps {
94
97
  ok: boolean;
95
98
  error?: string;
96
99
  path?: string;
100
+ sourceLabel?: string;
97
101
  };
98
102
  runVerification?: (args: {
99
103
  cwd: string;
@@ -146,9 +150,9 @@ export const JEV_HELP_TEXT = [
146
150
  " /jev grounding off|warn|repair|hold",
147
151
  " Grounding affermazioni sessione (solo automatic, opt-in)",
148
152
  " /jev verification off|warn|repair|hold|reload",
149
- " Footer deterministico + repair dai check progetto (opt-in; reload acquisisce manifest)",
153
+ " Footer deterministico; locale > manifest globale > profilo common (opt-in)",
150
154
  " /jev verify [recommended|all|id]",
151
- " Esegue solo check dichiarati in .pi/jev-verification.json",
155
+ " Esegue solo check della policy acquisita, mai shell arbitraria",
152
156
  " /jev backend Mostra backend attivo e chiavi rilevate",
153
157
  " /jev backend auto|openrouter|typesafe",
154
158
  " Cambia backend (salvato in jev-config.json)",
@@ -281,8 +285,8 @@ export function registerJevCommand(pi: ExtensionAPI, deps: JevCommandDeps) {
281
285
  `Grounding: ${cfg.grounding.enabled ? `attivo in automatic (mode=${cfg.grounding.mode}, evidenza=${deps.groundingStatus().entries}${deps.groundingStatus().hasStaleTests ? ", test stale" : ""})` : "disattivo (opt-in: /jev grounding warn)"}`,
282
286
  verification
283
287
  ? `Verifica progetto: ${verification.enabled
284
- ? `attiva (mode=${verification.mode}, manifest=${verification.manifestLoaded ? "OK" : verification.manifestError ?? "assente"}, modifiche=${verification.changedFiles.length}, check=${verification.checks.map((check) => `${check.id}:${check.state}`).join(",") || "—"})`
285
- : `disattiva (opt-in: /jev verification warn; manifest=${verification.manifestPath})`}`
288
+ ? `attiva (mode=${verification.mode}, policy=${verification.manifestLoaded ? verification.manifestSourceLabel ?? "OK" : verification.manifestError ?? "assente"}, modifiche=${verification.changedFiles.length}, check=${verification.checks.map((check) => `${check.id}:${check.state}`).join(",") || "—"})`
289
+ : `disattiva (opt-in: /jev verification warn; locale=${verification.manifestPath}, fallback=${verification.defaultProfile})`}`
286
290
  : "Verifica progetto: non disponibile in questa estensione",
287
291
  cfg.mode === "automatic"
288
292
  ? "ATTENZIONE: in automatic testo/codice vengono inviati al verificatore. Crea il twin con /jev upstream e seleziona il modello __jev per enforcement."
@@ -570,12 +574,13 @@ export function registerJevCommand(pi: ExtensionAPI, deps: JevCommandDeps) {
570
574
  st.enabled
571
575
  ? [
572
576
  `Verifica progetto attiva in automatic (mode=${st.mode}).`,
573
- `Manifest: ${st.manifestPath} — ${st.manifestLoaded ? "OK" : st.manifestError ?? "non disponibile"}.`,
577
+ `Policy: ${st.manifestLoaded ? st.manifestSourceLabel ?? "OK" : st.manifestError ?? "non disponibile"}.`,
578
+ `Override locale: ${st.manifestPath}; fallback integrato: ${st.defaultProfile}.`,
574
579
  `Modifiche osservate: ${st.changedFiles.join(", ") || "nessuna"}.`,
575
580
  `Check: ${st.checks.map((check) => `${check.id}:${check.state}`).join(", ") || "nessuno pertinente"}.`,
576
- "Esegui quelli mancanti con /jev verify recommended. Dopo una modifica manuale del manifest: /jev verification reload.",
581
+ "Esegui quelli mancanti con /jev verify recommended. Dopo una modifica manuale della policy: /jev verification reload.",
577
582
  ].join("\n")
578
- : `Verifica progetto disattiva (opt-in). Manifest previsto: ${st.manifestPath}. Attiva con /jev verification warn|repair|hold.`,
583
+ : `Verifica progetto disattiva (opt-in). Override locale: ${st.manifestPath}; fallback: ${st.defaultProfile}. Attiva con /jev verification warn|repair|hold.`,
579
584
  "info",
580
585
  );
581
586
  return;
@@ -588,8 +593,8 @@ export function registerJevCommand(pi: ExtensionAPI, deps: JevCommandDeps) {
588
593
  const reloaded = deps.reloadVerificationManifest(ctx.cwd);
589
594
  ctx.ui.notify(
590
595
  reloaded.ok
591
- ? `Manifest verifica acquisito: ${reloaded.path}. I comandi verranno eseguiti solo da questa snapshot.`
592
- : `Manifest verifica non acquisito: ${reloaded.error}`,
596
+ ? `Policy verifica acquisita: ${reloaded.sourceLabel ?? reloaded.path}. I comandi verranno eseguiti solo da questa snapshot.`
597
+ : `Policy verifica non acquisita: ${reloaded.error}`,
593
598
  reloaded.ok ? "info" : "warning",
594
599
  );
595
600
  return;
@@ -602,7 +607,7 @@ export function registerJevCommand(pi: ExtensionAPI, deps: JevCommandDeps) {
602
607
  ctx.ui.notify(
603
608
  value === "off"
604
609
  ? "Verifica progetto disattivata."
605
- : `Verifica progetto attiva (mode=${value}). Crea ${deps.verificationStatus(ctx.cwd).manifestPath} con i soli comandi approvati, poi usa /jev verify recommended.`,
610
+ : `Verifica progetto attiva (mode=${value}). Usa il profilo globale common automaticamente; un manifest locale in ${deps.verificationStatus(ctx.cwd).manifestPath} lo sovrascrive. Poi usa /jev verify recommended.`,
606
611
  "info",
607
612
  );
608
613
  return;
package/src/config.ts CHANGED
@@ -197,15 +197,18 @@ export const DEFAULT_CONFIG: JevConfig = {
197
197
  invalidateTestsOnEdit: true,
198
198
  },
199
199
  verification: {
200
- // Opt-in: il manifest può eseguire solo comandi esplicitamente approvati
201
- // dal progetto; non abilitarlo silenziosamente in installazioni esistenti.
200
+ // Opt-in: solo una policy acquisita può autorizzare check; non abilitarla
201
+ // silenziosamente in installazioni esistenti.
202
202
  enabled: false,
203
203
  mode: "warn",
204
204
  manifestPath: ".pi/jev-verification.json",
205
+ // Profilo built-in per nuovi progetti: si attiva solo dopo il consenso
206
+ // esplicito /jev verification warn|repair|hold (enabled resta false).
207
+ defaultProfile: "common",
205
208
  maxRepairs: 1,
206
209
  commandTimeoutMs: 120000,
207
210
  outputChars: 6000,
208
- maxCommands: 3,
211
+ maxCommands: 4,
209
212
  },
210
213
  exfilCheck: {
211
214
  enabled: true,
@@ -254,6 +257,21 @@ export function loadConfig(configPath?: string): JevConfig {
254
257
  fileConfig,
255
258
  ) as JevConfig;
256
259
 
260
+ // Normalizza il fallback verifica: config precedenti non lo contengono;
261
+ // valori manuali ignoti non devono disattivare/alterare la policy a caso.
262
+ if (!isRecord(loaded.verification)) {
263
+ loaded.verification = { ...DEFAULT_CONFIG.verification };
264
+ }
265
+ if (typeof loaded.verification.manifestPath !== "string" || !loaded.verification.manifestPath.trim()) {
266
+ loaded.verification.manifestPath = DEFAULT_CONFIG.verification.manifestPath;
267
+ }
268
+ if (loaded.verification.defaultProfile !== "common" && loaded.verification.defaultProfile !== "off") {
269
+ loaded.verification.defaultProfile = DEFAULT_CONFIG.verification.defaultProfile;
270
+ }
271
+ if (typeof loaded.verification.globalManifestPath !== "string") {
272
+ delete loaded.verification.globalManifestPath;
273
+ }
274
+
257
275
  // Normalizza la lista twin (config scritta a mano o da versioni vecchie):
258
276
  // dedup, scarta voci malformate, garantisce che il primario sia incluso.
259
277
  let merged = withTwinRefs(loaded, twinRefs(loaded));
@@ -1,4 +1,4 @@
1
- /** Esecuzione bounded dei soli comandi dichiarati nel manifest di verifica. */
1
+ /** Esecuzione bounded dei soli comandi dichiarati dalla policy di verifica acquisita. */
2
2
  import { spawn } from "node:child_process";
3
3
  import type { VerificationCheck, VerificationSettings } from "./verification.ts";
4
4
 
@@ -34,8 +34,8 @@ function appendBounded(state: { text: string; elided: number }, chunk: Buffer, m
34
34
  }
35
35
 
36
36
  /**
37
- * Esegue un comando scelto dal manifest. `command` non arriva mai dal modello;
38
- * il chiamante deve aver già selezionato un VerificationCheck validato.
37
+ * Esegue un comando scelto dalla policy. `command` non arriva mai dal modello;
38
+ * il chiamante deve aver già selezionato un VerificationCheck acquisito.
39
39
  */
40
40
  export async function runVerificationCheck(args: {
41
41
  check: VerificationCheck;
@@ -98,8 +98,8 @@ export async function runVerificationCheck(args: {
98
98
  };
99
99
 
100
100
  try {
101
- // shell è necessario perché il manifest contiene comandi normali come
102
- // `npm run typecheck`; la fiducia arriva dal file locale + project trust.
101
+ // shell è necessario per comandi normali come `npm run typecheck`; la
102
+ // fiducia arriva dalla policy acquisita + project trust.
103
103
  child = spawn(check.command, {
104
104
  cwd,
105
105
  shell: true,
@@ -27,7 +27,7 @@ export function registerVerificationTool(
27
27
  name: "jev_verify",
28
28
  label: "Jev project verification",
29
29
  description:
30
- "Run a project-approved typecheck, lint, or test declared in .pi/jev-verification.json. " +
30
+ "Run a typecheck, lint, or test authorized by the acquired local/global verification policy. " +
31
31
  "It never accepts arbitrary shell commands.",
32
32
  promptSnippet: "Run approved project verification checks after code edits.",
33
33
  promptGuidelines: [
@@ -7,12 +7,16 @@
7
7
  * dati, non dal testo del modello, quindi non può inventare test superati.
8
8
  */
9
9
  import { createHash } from "node:crypto";
10
- import { existsSync, readFileSync } from "node:fs";
11
- import { isAbsolute, relative, resolve, sep } from "node:path";
10
+ import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
11
+ import { homedir } from "node:os";
12
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
12
13
 
13
14
  export type VerificationMode = "warn" | "repair" | "hold";
15
+ /** Profilo integrato usato quando non esiste un override locale/globale. */
16
+ export type VerificationDefaultProfile = "common" | "off";
17
+ export type VerificationManifestSource = "project" | "global-manifest" | "global-profile";
14
18
 
15
- /** Impostazioni condivise; i comandi eseguibili restano nel manifest locale. */
19
+ /** Impostazioni condivise; i comandi eseguibili arrivano solo dalla policy acquisita. */
16
20
  export interface VerificationSettings {
17
21
  /** Opt-in: nessun footer, repair o comando automatico finché è false. */
18
22
  enabled: boolean;
@@ -20,6 +24,10 @@ export interface VerificationSettings {
20
24
  mode: VerificationMode;
21
25
  /** File JSON relativo alla root del progetto, mai un comando generato dal modello. */
22
26
  manifestPath: string;
27
+ /** Manifest opzionale dell'utente, assoluto o con `~/`; fallback dopo quello locale. */
28
+ globalManifestPath?: string;
29
+ /** Fallback integrato per nuovi progetti; resta inattivo finché enabled=false. */
30
+ defaultProfile: VerificationDefaultProfile;
23
31
  /** Rigenerazioni massime per risposta quando mancano/falliscono check. */
24
32
  maxRepairs: number;
25
33
  /** Timeout massimo predefinito dei check avviati da jev_verify o /jev verify. */
@@ -33,7 +41,7 @@ export interface VerificationSettings {
33
41
  export interface VerificationCheck {
34
42
  /** Id stabile, usato da /jev verify e dal tool jev_verify. */
35
43
  id: string;
36
- /** Comando esplicitamente approvato dal manifest del progetto. */
44
+ /** Comando esplicitamente approvato dalla policy acquisita. */
37
45
  command: string;
38
46
  /** Glob dei file a cui il check si applica; assente = tutte le modifiche. */
39
47
  files?: string[];
@@ -48,12 +56,31 @@ export interface VerificationManifest {
48
56
  }
49
57
 
50
58
  export type LoadedVerificationManifest =
51
- | { ok: true; path: string; manifest: VerificationManifest; fingerprint: string }
59
+ | {
60
+ ok: true;
61
+ /** Percorso assoluto per manifest file, identificatore stabile per profilo integrato. */
62
+ path: string;
63
+ source: VerificationManifestSource;
64
+ sourceLabel: string;
65
+ /** File che influenzano i comandi autorizzati e che il modello non può alterare. */
66
+ protectedPaths: string[];
67
+ manifest: VerificationManifest;
68
+ fingerprint: string;
69
+ }
52
70
  | { ok: false; path?: string; error: string };
53
71
 
54
- /** Manifest acquisito a inizio sessione o su comando utente esplicito. */
72
+ /** Manifest/profilo acquisito a inizio sessione o su comando utente esplicito. */
55
73
  export type VerificationManifestTrust =
56
- | { ok: true; cwd: string; path: string; manifest: VerificationManifest; fingerprint: string }
74
+ | {
75
+ ok: true;
76
+ cwd: string;
77
+ path: string;
78
+ source: VerificationManifestSource;
79
+ sourceLabel: string;
80
+ protectedPaths: string[];
81
+ manifest: VerificationManifest;
82
+ fingerprint: string;
83
+ }
57
84
  | { ok: false; cwd: string; error: string };
58
85
 
59
86
  /** Un edit/write osservato con successo; non pretende di vedere modifiche via shell. */
@@ -94,6 +121,9 @@ export interface VerificationCheckStatus {
94
121
 
95
122
  export interface VerificationStatus {
96
123
  changedFiles: string[];
124
+ /** Origine della policy che ha autorizzato i check correnti. */
125
+ manifestSource?: VerificationManifestSource;
126
+ manifestSourceLabel?: string;
97
127
  /** Errore/mancanza manifest: non è una prova negativa sul codice. */
98
128
  manifestError?: string;
99
129
  checks: VerificationCheckStatus[];
@@ -110,8 +140,8 @@ export function createVerificationState(): VerificationState {
110
140
 
111
141
  /**
112
142
  * Trasforma un path tool in un path relativo al progetto. I path fuori root
113
- * non entrano nel manifest locale: possono essere riportati dal modello, ma
114
- * non diventano falsamente coperti da un check del progetto corrente.
143
+ * non entrano nella policy del progetto: possono essere riportati dal modello,
144
+ * ma non diventano falsamente coperti da un check del progetto corrente.
115
145
  */
116
146
  export function normalizeObservedPath(path: unknown, cwd: string): string | undefined {
117
147
  if (typeof path !== "string" || !path.trim()) return undefined;
@@ -237,6 +267,16 @@ export function resolveManifestPath(cwd: string, manifestPath: string): string |
237
267
  return target;
238
268
  }
239
269
 
270
+ /** Un manifest globale è una scelta esplicita della config dell'utente: solo path assoluto o `~/`. */
271
+ export function resolveGlobalManifestPath(manifestPath: string | undefined): string | undefined {
272
+ const raw = manifestPath?.trim();
273
+ if (!raw) return undefined;
274
+ const expanded = raw === "~"
275
+ ? homedir()
276
+ : raw.startsWith("~/") ? join(homedir(), raw.slice(2)) : raw;
277
+ return isAbsolute(expanded) ? resolve(expanded) : undefined;
278
+ }
279
+
240
280
  /** Il modello non può alterare la policy che autorizza i suoi comandi. */
241
281
  export function isVerificationManifestPath(
242
282
  path: unknown,
@@ -246,7 +286,37 @@ export function isVerificationManifestPath(
246
286
  if (typeof path !== "string" || !path.trim()) return false;
247
287
  const manifest = resolveManifestPath(cwd, manifestPath);
248
288
  if (!manifest) return false;
249
- return resolve(cwd, path.trim()) === manifest;
289
+ return canonicalPolicyPath(resolve(cwd, path.trim())) === canonicalPolicyPath(manifest);
290
+ }
291
+
292
+ function canonicalPolicyPath(path: string): string {
293
+ const resolved = resolve(path);
294
+ try {
295
+ // Il confronto passa dal target reale: una symlink non consente di
296
+ // aggirare l'immutabilità del manifest o di package.json acquisito.
297
+ return realpathSync(resolved);
298
+ } catch {
299
+ return resolved;
300
+ }
301
+ }
302
+
303
+ function normalizedToolPath(path: string, cwd: string): string {
304
+ const trimmed = path.trim();
305
+ const expanded = trimmed === "~"
306
+ ? homedir()
307
+ : trimmed.startsWith("~/") ? join(homedir(), trimmed.slice(2)) : trimmed;
308
+ return canonicalPolicyPath(resolve(cwd, expanded));
309
+ }
310
+
311
+ /** Protegge anche input (es. package.json) che decidono un profilo globale. */
312
+ export function isVerificationPolicyPath(
313
+ path: unknown,
314
+ cwd: string,
315
+ trust: VerificationManifestTrust | undefined,
316
+ ): boolean {
317
+ if (typeof path !== "string" || !path.trim() || !trust?.ok || trust.cwd !== resolve(cwd)) return false;
318
+ const target = normalizedToolPath(path, cwd);
319
+ return trust.protectedPaths.includes(target);
250
320
  }
251
321
 
252
322
  function fingerprintText(text: string): string {
@@ -255,85 +325,345 @@ function fingerprintText(text: string): string {
255
325
  return createHash("sha256").update(text).digest("hex");
256
326
  }
257
327
 
258
- export function loadVerificationManifest(
259
- cwd: string,
260
- manifestPath: string,
261
- ): LoadedVerificationManifest {
262
- const path = resolveManifestPath(cwd, manifestPath);
263
- if (!path) {
264
- return { ok: false, error: "percorso manifest non valido: deve restare dentro il progetto" };
328
+ const COMMON_PROFILE_PATH = "<global-profile:common-v1>";
329
+ const MAX_MANIFEST_BYTES = 1_000_000;
330
+ const MAX_PROFILE_INPUT_BYTES = 1_000_000;
331
+ const MAX_PROFILE_INPUT_CHARS = 1_000_000;
332
+ const NODE_CODE_FILES = [
333
+ "**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", "**/*.mjs", "**/*.cjs",
334
+ "package.json", "tsconfig.json", "tsconfig.*.json",
335
+ ];
336
+ const NODE_TEST_FILES = [...NODE_CODE_FILES, "test/**", "tests/**", "__tests__/**"];
337
+
338
+ interface ProfileInput {
339
+ path: string;
340
+ text: string;
341
+ }
342
+
343
+ function readProfileInput(path: string): ProfileInput | { error: string } {
344
+ try {
345
+ if (statSync(path).size > MAX_PROFILE_INPUT_BYTES) {
346
+ return { error: `${path} supera ${MAX_PROFILE_INPUT_BYTES} byte` };
347
+ }
348
+ const text = readFileSync(path, "utf8");
349
+ if (text.length > MAX_PROFILE_INPUT_CHARS) {
350
+ return { error: `${path} supera ${MAX_PROFILE_INPUT_CHARS} caratteri` };
351
+ }
352
+ return { path, text };
353
+ } catch (error) {
354
+ return { error: `${path}: ${error instanceof Error ? error.message : String(error)}` };
265
355
  }
266
- if (!existsSync(path)) {
267
- return { ok: false, path, error: `manifest non trovato: ${manifestPath}` };
356
+ }
357
+
358
+ function isProfileInput(value: ProfileInput | { error: string }): value is ProfileInput {
359
+ return "text" in value;
360
+ }
361
+
362
+ function findGitRoot(cwd: string): string | undefined {
363
+ let current = resolve(cwd);
364
+ while (true) {
365
+ if (existsSync(join(current, ".git"))) return current;
366
+ const parent = dirname(current);
367
+ if (parent === current) return undefined;
368
+ current = parent;
268
369
  }
370
+ }
371
+
372
+ function nodeProfile(root: string):
373
+ | { checks: VerificationCheck[]; inputs: ProfileInput[] }
374
+ | { error: string }
375
+ | undefined {
376
+ const path = join(root, "package.json");
377
+ if (!existsSync(path)) return undefined;
378
+ const input = readProfileInput(path);
379
+ if (!isProfileInput(input)) return input;
380
+ let parsed: unknown;
269
381
  try {
270
- const text = readFileSync(path, "utf8");
382
+ parsed = JSON.parse(input.text);
383
+ } catch (error) {
384
+ return { error: `profilo globale: package.json non valido: ${error instanceof Error ? error.message : String(error)}` };
385
+ }
386
+ const scripts = isRecord(parsed) && isRecord(parsed.scripts) ? parsed.scripts : {};
387
+ const hasScript = (name: string) => typeof scripts[name] === "string" && scripts[name].trim().length > 0;
388
+ const checks: VerificationCheck[] = [];
389
+ if (hasScript("typecheck")) {
390
+ checks.push({ id: "node-typecheck", command: "npm run typecheck", description: "Node/TypeScript typecheck", files: NODE_CODE_FILES });
391
+ }
392
+ if (hasScript("lint")) {
393
+ checks.push({ id: "node-lint", command: "npm run lint", description: "Node lint", files: NODE_CODE_FILES });
394
+ }
395
+ const testScript = ["test:unit", "test:ci", "test"].find(hasScript);
396
+ if (testScript) {
397
+ checks.push({
398
+ id: "node-test",
399
+ command: testScript === "test" ? "npm test" : `npm run ${testScript}`,
400
+ description: `Node test (${testScript})`,
401
+ files: NODE_TEST_FILES,
402
+ });
403
+ }
404
+ return checks.length > 0 ? { checks, inputs: [input] } : undefined;
405
+ }
406
+
407
+ function markerProfile(args: {
408
+ root: string;
409
+ marker: string;
410
+ checks: VerificationCheck[];
411
+ }): { checks: VerificationCheck[]; inputs: ProfileInput[] } | { error: string } | undefined {
412
+ const path = join(args.root, args.marker);
413
+ if (!existsSync(path)) return undefined;
414
+ const input = readProfileInput(path);
415
+ if (!isProfileInput(input)) return input;
416
+ return { checks: args.checks, inputs: [input] };
417
+ }
418
+
419
+ function pythonProfile(root: string):
420
+ | { checks: VerificationCheck[]; inputs: ProfileInput[] }
421
+ | { error: string }
422
+ | undefined {
423
+ const candidates = ["pytest.ini", "pyproject.toml", "setup.cfg"];
424
+ const inputs: ProfileInput[] = [];
425
+ for (const name of candidates) {
426
+ const path = join(root, name);
427
+ if (!existsSync(path)) continue;
428
+ const input = readProfileInput(path);
429
+ if (!isProfileInput(input)) return input;
430
+ inputs.push(input);
431
+ }
432
+ const pytestConfigured = inputs.some((input) =>
433
+ input.path.endsWith("pytest.ini") ||
434
+ /\[(?:tool\.pytest(?:\.ini_options)?|tool:pytest)\]/.test(input.text) ||
435
+ /["']pytest(?:[<>=~!][^"']*)?["']/.test(input.text),
436
+ );
437
+ if (!pytestConfigured) return undefined;
438
+ return {
439
+ checks: [{
440
+ id: "python-pytest",
441
+ command: "python -m pytest",
442
+ description: "Python pytest",
443
+ files: ["**/*.py", "pyproject.toml", "pytest.ini", "setup.cfg"],
444
+ }],
445
+ inputs,
446
+ };
447
+ }
448
+
449
+ /**
450
+ * Profilo built-in, deliberatamente conservativo. I comandi sono nel codice
451
+ * dell'estensione, non arrivano dal repository o dal modello; i file che ne
452
+ * abilitano la selezione entrano nel fingerprint/protectedPaths della snapshot.
453
+ */
454
+ export function loadCommonVerificationProfile(cwd: string): LoadedVerificationManifest {
455
+ const root = resolve(cwd);
456
+ const node = nodeProfile(root);
457
+ if (node && "error" in node) return { ok: false, error: node.error };
458
+ const cargo = node ? undefined : markerProfile({
459
+ root,
460
+ marker: "Cargo.toml",
461
+ checks: [
462
+ { id: "cargo-check", command: "cargo check", description: "Rust compile check", files: ["**/*.rs", "Cargo.toml", "Cargo.lock", "build.rs"] },
463
+ { id: "cargo-test", command: "cargo test", description: "Rust tests", files: ["**/*.rs", "Cargo.toml", "Cargo.lock", "build.rs", "tests/**"] },
464
+ ],
465
+ });
466
+ if (cargo && "error" in cargo) return { ok: false, error: cargo.error };
467
+ const go = node || cargo ? undefined : markerProfile({
468
+ root,
469
+ marker: "go.mod",
470
+ checks: [{ id: "go-test", command: "go test ./...", description: "Go tests", files: ["**/*.go", "go.mod", "go.sum"] }],
471
+ });
472
+ if (go && "error" in go) return { ok: false, error: go.error };
473
+ const python = node || cargo || go ? undefined : pythonProfile(root);
474
+ if (python && "error" in python) return { ok: false, error: python.error };
475
+ const selected = node ?? cargo ?? go ?? python;
476
+ const checks = selected ? [...selected.checks] : [];
477
+ const gitRoot = findGitRoot(root);
478
+ if (gitRoot) {
479
+ checks.push({ id: "git-diff-check", command: "git diff --check", description: "Whitespace and patch integrity" });
480
+ }
481
+ if (checks.length === 0) {
482
+ return {
483
+ ok: false,
484
+ error: "nessun manifest locale e profilo globale common non applicabile (nessun progetto Node/Rust/Go/Python o repository Git rilevato)",
485
+ };
486
+ }
487
+ const inputs = selected?.inputs ?? [];
488
+ const manifest: VerificationManifest = { version: 1, checks };
489
+ const fingerprint = fingerprintText(JSON.stringify({
490
+ profile: "common-v1",
491
+ root,
492
+ gitRoot: gitRoot ?? null,
493
+ inputs: inputs.map((input) => ({ path: input.path, text: input.text })),
494
+ manifest,
495
+ }));
496
+ return {
497
+ ok: true,
498
+ path: COMMON_PROFILE_PATH,
499
+ source: "global-profile",
500
+ sourceLabel: "profilo globale common",
501
+ protectedPaths: inputs.map((input) => canonicalPolicyPath(input.path)),
502
+ manifest,
503
+ fingerprint,
504
+ };
505
+ }
506
+
507
+ function loadManifestAtPath(args: {
508
+ path: string;
509
+ source: "project" | "global-manifest";
510
+ sourceLabel: string;
511
+ }): LoadedVerificationManifest {
512
+ if (!existsSync(args.path)) {
513
+ return { ok: false, path: args.path, error: `manifest non trovato: ${args.sourceLabel}` };
514
+ }
515
+ try {
516
+ if (statSync(args.path).size > MAX_MANIFEST_BYTES) {
517
+ return { ok: false, path: args.path, error: `manifest non valido: supera ${MAX_MANIFEST_BYTES} byte` };
518
+ }
519
+ const text = readFileSync(args.path, "utf8");
271
520
  const parsed = JSON.parse(text);
272
521
  return {
273
522
  ok: true,
274
- path,
523
+ path: args.path,
524
+ source: args.source,
525
+ sourceLabel: args.sourceLabel,
526
+ protectedPaths: [canonicalPolicyPath(args.path)],
275
527
  manifest: parseVerificationManifest(parsed),
276
528
  fingerprint: fingerprintText(text),
277
529
  };
278
530
  } catch (error) {
279
531
  return {
280
532
  ok: false,
281
- path,
533
+ path: args.path,
282
534
  error: `manifest non valido: ${error instanceof Error ? error.message : String(error)}`,
283
535
  };
284
536
  }
285
537
  }
286
538
 
287
- /** Acquisisce la policy di esecuzione dal disco in un punto esplicitamente utente. */
288
- export function trustVerificationManifest(
539
+ /** Carica solo il manifest locale; API mantenuta per use case per-progetto espliciti. */
540
+ export function loadVerificationManifest(
289
541
  cwd: string,
290
542
  manifestPath: string,
291
- ): VerificationManifestTrust {
292
- const loaded = loadVerificationManifest(cwd, manifestPath);
543
+ ): LoadedVerificationManifest {
544
+ const path = resolveManifestPath(cwd, manifestPath);
545
+ if (!path) {
546
+ return { ok: false, error: "percorso manifest non valido: deve restare dentro il progetto" };
547
+ }
548
+ return loadManifestAtPath({ path, source: "project", sourceLabel: `manifest locale: ${manifestPath}` });
549
+ }
550
+
551
+ /**
552
+ * Precedenza: manifest locale → manifest globale dell'utente → profilo common.
553
+ * Un file esplicito ma malformato/non trovato non degrada silenziosamente a un
554
+ * altro fallback: l'utente deve accorgersi della policy che ha configurato.
555
+ */
556
+ export function loadVerificationPolicy(
557
+ cwd: string,
558
+ settings: VerificationSettings,
559
+ ): LoadedVerificationManifest {
560
+ const localPath = resolveManifestPath(cwd, settings.manifestPath);
561
+ if (!localPath) {
562
+ return { ok: false, error: "percorso manifest locale non valido: deve restare dentro il progetto" };
563
+ }
564
+ if (existsSync(localPath)) return loadVerificationManifest(cwd, settings.manifestPath);
565
+
566
+ const configuredGlobal = settings.globalManifestPath?.trim();
567
+ if (configuredGlobal) {
568
+ const globalPath = resolveGlobalManifestPath(configuredGlobal);
569
+ if (!globalPath) {
570
+ return { ok: false, error: "percorso manifest globale non valido: usa un path assoluto o ~/" };
571
+ }
572
+ return loadManifestAtPath({
573
+ path: globalPath,
574
+ source: "global-manifest",
575
+ sourceLabel: `manifest globale: ${globalPath}`,
576
+ });
577
+ }
578
+
579
+ if (settings.defaultProfile === "off") {
580
+ return { ok: false, path: localPath, error: `manifest locale non trovato: ${settings.manifestPath}; profilo globale disattivato` };
581
+ }
582
+ return loadCommonVerificationProfile(cwd);
583
+ }
584
+
585
+ function trustLoaded(cwd: string, loaded: LoadedVerificationManifest): VerificationManifestTrust {
293
586
  return loaded.ok
294
587
  ? {
295
588
  ok: true,
296
589
  cwd: resolve(cwd),
297
590
  path: loaded.path,
591
+ source: loaded.source,
592
+ sourceLabel: loaded.sourceLabel,
593
+ protectedPaths: [...loaded.protectedPaths],
298
594
  manifest: loaded.manifest,
299
595
  fingerprint: loaded.fingerprint,
300
596
  }
301
597
  : { ok: false, cwd: resolve(cwd), error: loaded.error };
302
598
  }
303
599
 
304
- /**
305
- * Ritorna il manifest acquisito, ma rifiuta mutazioni del file dopo il trust.
306
- * I comandi vengono poi selezionati dall'oggetto in memoria acquisito, non
307
- * dal file corrente: un agente non può scrivere un comando e farlo eseguire
308
- * nella stessa sessione.
309
- */
310
- export function trustedVerificationManifest(
311
- trust: VerificationManifestTrust | undefined,
600
+ /** Acquisisce un manifest locale esplicito; compatibilità con l'API v0.5. */
601
+ export function trustVerificationManifest(
312
602
  cwd: string,
313
603
  manifestPath: string,
604
+ ): VerificationManifestTrust {
605
+ return trustLoaded(cwd, loadVerificationManifest(cwd, manifestPath));
606
+ }
607
+
608
+ /** Acquisisce la policy risolta (locale, globale o profilo integrato). */
609
+ export function trustVerificationPolicy(
610
+ cwd: string,
611
+ settings: VerificationSettings,
612
+ ): VerificationManifestTrust {
613
+ return trustLoaded(cwd, loadVerificationPolicy(cwd, settings));
614
+ }
615
+
616
+ function trustedLoaded(
617
+ trust: VerificationManifestTrust | undefined,
618
+ cwd: string,
619
+ current: LoadedVerificationManifest,
314
620
  ): LoadedVerificationManifest {
315
- if (!trust) return { ok: false, error: "manifest non ancora acquisito; usa /jev verification reload" };
621
+ if (!trust) return { ok: false, error: "policy verifica non ancora acquisita; usa /jev verification reload" };
316
622
  if (!trust.ok) return { ok: false, error: trust.error };
317
623
  if (trust.cwd !== resolve(cwd)) {
318
- return { ok: false, error: "manifest acquisito per un'altra directory; usa /jev verification reload" };
624
+ return { ok: false, error: "policy verifica acquisita per un'altra directory; usa /jev verification reload" };
319
625
  }
320
- const current = loadVerificationManifest(cwd, manifestPath);
321
626
  if (!current.ok) return current;
322
- if (current.path !== trust.path || current.fingerprint !== trust.fingerprint) {
627
+ if (
628
+ current.path !== trust.path ||
629
+ current.source !== trust.source ||
630
+ current.fingerprint !== trust.fingerprint
631
+ ) {
323
632
  return {
324
633
  ok: false,
325
634
  path: current.path,
326
- error: "manifest modificato dopo l'acquisizione; controllalo e usa /jev verification reload",
635
+ error: `${trust.source === "global-profile" ? "profilo globale" : "manifest"} modificato dopo l'acquisizione; controllalo e usa /jev verification reload`,
327
636
  };
328
637
  }
329
638
  return {
330
639
  ok: true,
331
640
  path: trust.path,
641
+ source: trust.source,
642
+ sourceLabel: trust.sourceLabel,
643
+ protectedPaths: [...trust.protectedPaths],
332
644
  manifest: trust.manifest,
333
645
  fingerprint: trust.fingerprint,
334
646
  };
335
647
  }
336
648
 
649
+ /** Ritorna il manifest locale acquisito, rifiutando mutazioni dopo il trust. */
650
+ export function trustedVerificationManifest(
651
+ trust: VerificationManifestTrust | undefined,
652
+ cwd: string,
653
+ manifestPath: string,
654
+ ): LoadedVerificationManifest {
655
+ return trustedLoaded(trust, cwd, loadVerificationManifest(cwd, manifestPath));
656
+ }
657
+
658
+ /** Ritorna la policy acquisita, rifiutando mutazioni di manifest o input profilo. */
659
+ export function trustedVerificationPolicy(
660
+ trust: VerificationManifestTrust | undefined,
661
+ cwd: string,
662
+ settings: VerificationSettings,
663
+ ): LoadedVerificationManifest {
664
+ return trustedLoaded(trust, cwd, loadVerificationPolicy(cwd, settings));
665
+ }
666
+
337
667
  function globToRegExp(pattern: string): RegExp {
338
668
  let source = "^";
339
669
  for (let i = 0; i < pattern.length; i++) {
@@ -385,7 +715,9 @@ export function buildVerificationStatus(
385
715
  if (changedFiles.length === 0) {
386
716
  return {
387
717
  changedFiles,
388
- ...(loaded.ok ? {} : { manifestError: loaded.error }),
718
+ ...(loaded.ok
719
+ ? { manifestSource: loaded.source, manifestSourceLabel: loaded.sourceLabel }
720
+ : { manifestError: loaded.error }),
389
721
  checks: [],
390
722
  actionable: false,
391
723
  hasPending: false,
@@ -421,6 +753,8 @@ export function buildVerificationStatus(
421
753
  const hasFailed = checks.some((check) => check.state === "failed");
422
754
  return {
423
755
  changedFiles,
756
+ manifestSource: loaded.source,
757
+ manifestSourceLabel: loaded.sourceLabel,
424
758
  checks,
425
759
  actionable: checks.length > 0,
426
760
  hasPending,
@@ -450,9 +784,12 @@ export function verificationNotice(status: VerificationStatus): string | undefin
450
784
  lines.push(`> - Stato: ${status.manifestError}; nessuna verifica configurata è stata attribuita.`);
451
785
  return lines.join("\n");
452
786
  }
787
+ if (status.manifestSourceLabel) {
788
+ lines.push(`> - Policy: ${status.manifestSourceLabel}.`);
789
+ }
453
790
  if (!status.actionable) {
454
791
  lines.push(
455
- "> - Stato: nessun check del manifest corrisponde ai file modificati; correttezza non verificata.",
792
+ "> - Stato: nessun check della policy corrisponde ai file modificati; correttezza non verificata.",
456
793
  );
457
794
  return lines.join("\n");
458
795
  }