pi-jev-guard 0.2.1 → 0.2.2

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
@@ -76,8 +76,15 @@ ne rimuove uno solo, `/jev off` li rimuove tutti
76
76
  `/jev off` conserva il catalogo salvato: al reload gli alias tornano disponibili,
77
77
  ma il gate rimane disattivato.
78
78
 
79
- Se filtri i modelli con `enabledModels`, tieni le voci `__jev` dei twin che usi
80
- o restano nascosti.
79
+ Se filtri i modelli con `enabledModels`, usa i glob invece delle voci esplicite,
80
+ così i twin futuri non restano nascosti:
81
+
82
+ ```json
83
+ "enabledModels": ["openai-codex/*__jev", "deepseek/*__jev", "...altri pattern..."]
84
+ ```
85
+
86
+ Dopo aver aggiunto un twin serve `/reload` (lo scope è risolto all'avvio della sessione).
87
+ `/jev upstream` avvisa subito quando il twin appena creato è fuori dal picker.
81
88
  L'installazione punta alla cartella: dopo un aggiornamento del codice basta `/reload`.
82
89
 
83
90
  Headless/CI: `JEV_MODE=automatic JEV_AUTO_UPSTREAM=provider/model` crea e attiva
@@ -85,6 +92,11 @@ il guarded nelle sessioni senza uno stato Jev salvato.
85
92
 
86
93
  Un hold `[JEV — OUTPUT NON PUBBLICATO]` interrompe intenzionalmente il turno:
87
94
  controlla `/jev last`. Non viene aggirato con un proseguimento automatico.
95
+
96
+ Verdetto **incerto** (`review`): il gate tenta prima una riga riparazione privata
97
+ (`automatic.repairOnReview`, default `true`), poi applica la policy configurata:
98
+ `policy.onUncertain` e `policy.onUnavailable` (`"hold"` default, oppure `"pass"`).
99
+ Con `"pass"` il testo viene pubblicato con una nota di trasparenza sul verdetto.
88
100
  Le conferme per comandi di rete sospetti attendono invece una decisione esplicita;
89
101
  la normale sequenza tool → risposta successiva continua senza un nuovo prompt.
90
102
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-jev-guard",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
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": [
@@ -130,6 +130,26 @@ export async function consumePrivate(
130
130
  return final;
131
131
  }
132
132
 
133
+ /** Aggiunge una riga di trasparenza sul verdetto non conclusivo. */
134
+ function uncertainNotice(verdict: ReviewResult): string {
135
+ const top = [...verdict.checks].sort((a, b) => b.pFlaw - a.pFlaw)[0];
136
+ const detail = top ? ` (${top.ruleId} p=${top.pFlaw.toFixed(3)})` : "";
137
+ const reason = verdict.errorCode ? ` errore=${verdict.errorCode}` : "";
138
+ return `\n\n> [jev] verdetto non conclusivo: ${verdict.status}${detail}${reason} — pubblicato per policy onUncertain/onUnavailable=pass.`;
139
+ }
140
+
141
+ /** Copia del candidato con la nota accodata all'ultimo blocco di testo. */
142
+ function withNotice(message: AssistantMessage, notice: string): AssistantMessage {
143
+ const content = message.content.map((block) => ({ ...block }));
144
+ const last = [...content].reverse().find((block) => block.type === "text");
145
+ if (last && last.type === "text") {
146
+ last.text = `${last.text}${notice}`;
147
+ } else {
148
+ content.push({ type: "text", text: notice.trimStart() });
149
+ }
150
+ return { ...message, content };
151
+ }
152
+
133
153
  function assertCompleteCandidate(message: AssistantMessage): void {
134
154
  // Rifiuta terminali non supportati per pubblicazione.
135
155
  if (
@@ -177,6 +197,10 @@ export function runGuardedStream(
177
197
  const usages: Usage[] = [];
178
198
  let attemptContext = originalContext;
179
199
  const maxRegenerations = deps.config.automatic.maxRegenerations;
200
+ // Un verdetto incerto non è un difetto accertato: un solo tentativo di
201
+ // riparazione mirata evita l'hold immediato su prosa borderline.
202
+ const maxReviewRepairs = deps.config.automatic.repairOnReview === false ? 0 : 1;
203
+ let reviewRepairs = 0;
180
204
 
181
205
  try {
182
206
  for (let attempt = 0; attempt <= maxRegenerations; attempt++) {
@@ -224,10 +248,11 @@ export function runGuardedStream(
224
248
  }
225
249
  signal?.throwIfAborted();
226
250
 
227
- if (verdict.status === "pass") {
251
+ const publishCandidate = async (notice?: string): Promise<void> => {
228
252
  deps.onVerdict?.({ verdict, attempts: attempt + 1, outcome: "published" });
253
+ const approved = notice ? withNotice(candidate, notice) : candidate;
229
254
  const aggregated = aggregateUsage(usages);
230
- const replayed = replayApproved(candidate, {
255
+ const replayed = replayApproved(approved, {
231
256
  provider: deps.publishedProvider,
232
257
  model: deps.publishedModel,
233
258
  usage: aggregated,
@@ -239,10 +264,19 @@ export function runGuardedStream(
239
264
  }
240
265
  const final = await replayed.result();
241
266
  out.end(final);
267
+ };
268
+
269
+ if (verdict.status === "pass") {
270
+ await publishCandidate();
242
271
  return;
243
272
  }
244
273
 
245
- if (verdict.status === "block" && attempt < maxRegenerations) {
274
+ const canRepair =
275
+ verdict.status === "block"
276
+ ? attempt < maxRegenerations
277
+ : verdict.status === "review" && reviewRepairs < maxReviewRepairs;
278
+ if (canRepair) {
279
+ if (verdict.status === "review") reviewRepairs++;
246
280
  attemptContext = buildRepairContext({
247
281
  originalContext,
248
282
  rejectedText: rawCandidate,
@@ -252,6 +286,15 @@ export function runGuardedStream(
252
286
  continue;
253
287
  }
254
288
 
289
+ // Policy esplicita per i verdetti non conclusivi (config, non silenziosa).
290
+ const uncertainPass = verdict.status === "review" && deps.config.policy.onUncertain === "pass";
291
+ const unavailablePass =
292
+ verdict.status === "unavailable" && deps.config.policy.onUnavailable === "pass";
293
+ if (uncertainPass || unavailablePass) {
294
+ await publishCandidate(uncertainNotice(verdict));
295
+ return;
296
+ }
297
+
255
298
  // Hold: pubblica failure sicura, mai il candidato bocciato.
256
299
  const attempts = attempt + 1;
257
300
  deps.onVerdict?.({ verdict, attempts, outcome: "held" });
package/src/commands.ts CHANGED
@@ -133,6 +133,20 @@ export function registerJevCommand(pi: ExtensionAPI, deps: JevCommandDeps) {
133
133
  const parts = args.trim().split(/\s+/).filter(Boolean);
134
134
  const sub = parts[0] ?? "status";
135
135
 
136
+ // Terza domanda del picker: registrato, visibile, selezionato. Il twin
137
+ // appena creato non è mai nello scope risolto a inizio sessione.
138
+ function twinInScope(provider: string, modelId: string): boolean {
139
+ try {
140
+ const scoped = ctx.scopedModels ?? [];
141
+ if (scoped.length === 0) return true;
142
+ return scoped.some(
143
+ (s) => s.model.provider === provider && s.model.id === `${modelId}__jev`,
144
+ );
145
+ } catch {
146
+ return true;
147
+ }
148
+ }
149
+
136
150
  // Never unregister the route while a synthetic model is still selected.
137
151
  // The caller must disable automatic enforcement before switching away.
138
152
  async function leaveSelectedTwin(): Promise<boolean> {
@@ -310,10 +324,15 @@ export function registerJevCommand(pi: ExtensionAPI, deps: JevCommandDeps) {
310
324
  return;
311
325
  }
312
326
  const total = deps.twinStatus().active.length;
327
+ const visible = twinInScope(parsed.provider, parsed.model);
313
328
  ctx.ui.notify(
314
329
  `Twin aggiunto: ${res.twin} (totale attivi: ${total}, stessa auth del provider). ` +
315
- `Selezionalo con /model per enforcement in automatic.`,
316
- "info",
330
+ (visible
331
+ ? "Selezionalo con /model per enforcement in automatic."
332
+ : `ATTENZIONE: non compare in /model — enabledModels non lo copre. ` +
333
+ `Aggiungi "${parsed.provider}/*__jev" a enabledModels e fai /reload, ` +
334
+ `oppure attivalo direttamente con /jev mode automatic.`),
335
+ visible ? "info" : "warning",
317
336
  );
318
337
  return;
319
338
  }
@@ -336,7 +355,8 @@ export function registerJevCommand(pi: ExtensionAPI, deps: JevCommandDeps) {
336
355
  const key = `${ref.provider}/${ref.model}`;
337
356
  const mark = activeKeys.has(key) ? "● " : "○ ";
338
357
  const star = key === primaryKey ? "★" : " ";
339
- lines.push(`${mark}${star} ${key}${activeKeys.has(key) ? "" : " (non attivo)"}`);
358
+ const hidden = twinInScope(ref.provider, ref.model) ? "" : " (fuori dal picker)";
359
+ lines.push(`${mark}${star} ${key}${activeKeys.has(key) ? "" : " (non attivo)"}${hidden}`);
340
360
  }
341
361
  ctx.ui.notify(
342
362
  [
package/src/config.ts CHANGED
@@ -31,6 +31,8 @@ export interface JevConfig {
31
31
  automatic: {
32
32
  transport: "provider-gate";
33
33
  maxRegenerations: number;
34
+ /** Tentativo privato extra quando il verdetto è incerto (review). */
35
+ repairOnReview: boolean;
34
36
  requireGuardedModel: boolean;
35
37
  /** Twin salvati: più modelli guarded insieme (uno o più per provider). */
36
38
  twins: TwinRef[];
@@ -121,6 +123,7 @@ export const DEFAULT_CONFIG: JevConfig = {
121
123
  automatic: {
122
124
  transport: "provider-gate",
123
125
  maxRegenerations: 2,
126
+ repairOnReview: true,
124
127
  requireGuardedModel: true,
125
128
  twins: [],
126
129
  // Vuoti di default: nessun twin "salvato" finché l'utente non ne aggiunge