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 +14 -2
- package/package.json +1 -1
- package/src/automatic/guardian.ts +46 -3
- package/src/commands.ts +23 -3
- package/src/config.ts +3 -0
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`,
|
|
80
|
-
|
|
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
|
@@ -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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
316
|
-
|
|
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
|
-
|
|
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
|