pi-jev-guard 0.3.3 → 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 +68 -14
- package/config/jev-config.example.json +9 -0
- package/config/jev-verification.example.json +22 -0
- package/extensions/index.ts +489 -124
- package/package.json +1 -1
- package/src/automatic/guardian.ts +26 -2
- package/src/automatic/overlay.ts +160 -68
- package/src/commands.ts +106 -1
- package/src/config.ts +13 -0
- package/src/verification-runner.ts +177 -0
- package/src/verification-tool.ts +65 -0
- package/src/verification.ts +589 -0
package/package.json
CHANGED
|
@@ -62,6 +62,8 @@ export interface PostPublishCheckResult {
|
|
|
62
62
|
holdText?: string;
|
|
63
63
|
/** Dettaglio per il report (publish con nota, hold). */
|
|
64
64
|
grounding?: GateVerdictReport["grounding"];
|
|
65
|
+
/** Stato deterministico delle verifiche progetto osservate. */
|
|
66
|
+
verification?: GateVerdictReport["verification"];
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
/** Esito finale del gate con verdetto, per `/jev last` e diagnostica. */
|
|
@@ -76,6 +78,12 @@ export interface GateVerdictReport {
|
|
|
76
78
|
action: "warned" | "repaired" | "held";
|
|
77
79
|
reasons: string[];
|
|
78
80
|
};
|
|
81
|
+
/** Mai generato dal modello: file edit/write + check configurati osservati. */
|
|
82
|
+
verification?: {
|
|
83
|
+
action: "passed" | "warned" | "repaired" | "held";
|
|
84
|
+
changedFiles: string[];
|
|
85
|
+
checks: Array<{ id: string; state: "passed" | "failed" | "pending" }>;
|
|
86
|
+
};
|
|
79
87
|
}
|
|
80
88
|
|
|
81
89
|
export class GuardianLimitError extends Error {
|
|
@@ -183,6 +191,10 @@ function withNotice(message: AssistantMessage, notice: string): AssistantMessage
|
|
|
183
191
|
return { ...message, content };
|
|
184
192
|
}
|
|
185
193
|
|
|
194
|
+
function hasToolCalls(message: AssistantMessage): boolean {
|
|
195
|
+
return message.content.some((block) => block.type === "toolCall");
|
|
196
|
+
}
|
|
197
|
+
|
|
186
198
|
function assertCompleteCandidate(message: AssistantMessage): void {
|
|
187
199
|
// Rifiuta terminali non supportati per pubblicazione.
|
|
188
200
|
if (
|
|
@@ -284,12 +296,14 @@ export function runGuardedStream(
|
|
|
284
296
|
const publishCandidate = async (
|
|
285
297
|
notice?: string,
|
|
286
298
|
grounding?: GateVerdictReport["grounding"],
|
|
299
|
+
verification?: GateVerdictReport["verification"],
|
|
287
300
|
): Promise<void> => {
|
|
288
301
|
deps.onVerdict?.({
|
|
289
302
|
verdict,
|
|
290
303
|
attempts: attempt + 1,
|
|
291
304
|
outcome: "published",
|
|
292
305
|
...(grounding ? { grounding } : {}),
|
|
306
|
+
...(verification ? { verification } : {}),
|
|
293
307
|
});
|
|
294
308
|
const approved = notice ? withNotice(candidate, notice) : candidate;
|
|
295
309
|
const aggregated = aggregateUsage(usages);
|
|
@@ -339,6 +353,15 @@ export function runGuardedStream(
|
|
|
339
353
|
const runPostCheck = async (
|
|
340
354
|
baseNotice?: string,
|
|
341
355
|
): Promise<"published" | "held" | undefined> => {
|
|
356
|
+
// I toolcall devono raggiungere il runtime: un post-check su una
|
|
357
|
+
// risposta intermedia potrebbe rigenerarla/holdarla e impedire il
|
|
358
|
+
// check stesso (es. `jev_verify`) prima che venga eseguito. Safety
|
|
359
|
+
// e tool policy restano già applicate; il post-check corre sul
|
|
360
|
+
// successivo messaggio finale dopo i tool result.
|
|
361
|
+
if (hasToolCalls(candidate)) {
|
|
362
|
+
await publishCandidate(baseNotice);
|
|
363
|
+
return "published";
|
|
364
|
+
}
|
|
342
365
|
if (!deps.postPublishCheck) {
|
|
343
366
|
await publishCandidate(baseNotice);
|
|
344
367
|
return "published";
|
|
@@ -357,7 +380,7 @@ export function runGuardedStream(
|
|
|
357
380
|
signal?.throwIfAborted();
|
|
358
381
|
if (check.action === "publish") {
|
|
359
382
|
const notice = [baseNotice, check.notice].filter(Boolean).join("") || undefined;
|
|
360
|
-
await publishCandidate(notice, check.grounding);
|
|
383
|
+
await publishCandidate(notice, check.grounding, check.verification);
|
|
361
384
|
return "published";
|
|
362
385
|
}
|
|
363
386
|
if (check.action === "hold") {
|
|
@@ -367,6 +390,7 @@ export function runGuardedStream(
|
|
|
367
390
|
attempts,
|
|
368
391
|
outcome: "held",
|
|
369
392
|
...(check.grounding ? { grounding: check.grounding } : {}),
|
|
393
|
+
...(check.verification ? { verification: check.verification } : {}),
|
|
370
394
|
});
|
|
371
395
|
await publishHoldText(check.holdText ?? "[JEV — OUTPUT NON PUBBLICATO]");
|
|
372
396
|
return "held";
|
|
@@ -376,7 +400,7 @@ export function runGuardedStream(
|
|
|
376
400
|
rejectedText: rawCandidate,
|
|
377
401
|
feedback:
|
|
378
402
|
check.feedback ??
|
|
379
|
-
"
|
|
403
|
+
"Post-publication check failed; correct the draft using the supplied evidence.",
|
|
380
404
|
});
|
|
381
405
|
return undefined;
|
|
382
406
|
};
|
package/src/automatic/overlay.ts
CHANGED
|
@@ -128,54 +128,129 @@ function stripTwins(models: readonly Model<Api>[]): Model<Api>[] {
|
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
/**
|
|
131
|
-
*
|
|
132
|
-
*
|
|
131
|
+
* Patch in-place del gate gemellato sullo STESSO oggetto provider.
|
|
132
|
+
*
|
|
133
|
+
* Nessuna (ri-)registrazione nel registry: l'identità dell'oggetto e la sua
|
|
134
|
+
* config restano quelle pubblicate dal proprietario (builtin o altre
|
|
135
|
+
* estensioni come pi-accounts), quindi i loro controlli di ownership e i
|
|
136
|
+
* loro snapshot non vedono mai una "configuration changed" e non armano
|
|
137
|
+
* abort. Rieseguibile: se l'oggetto è già patchato riparte dai metodi
|
|
138
|
+
* originali con la nuova lista twin. Ritorna gli id gemello pubblicati.
|
|
133
139
|
*/
|
|
134
|
-
export
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
140
|
+
export interface PatchDeps {
|
|
141
|
+
/** Id modelli (originali) da gemellare. */
|
|
142
|
+
twinModelIds: string[];
|
|
143
|
+
getConfig: () => JevConfig;
|
|
144
|
+
getReview: () => ReviewFn;
|
|
145
|
+
/** Riceve l'esito finale di ogni turno sul twin (per `/jev last`). */
|
|
146
|
+
onVerdict?: (report: GateVerdictReport) => void;
|
|
147
|
+
/**
|
|
148
|
+
* Factory del controllo post-safety, una per richiesta: il budget repair
|
|
149
|
+
* vive nella closure restituita, non nel loop del guardian.
|
|
150
|
+
*/
|
|
151
|
+
createPostCheck?: () => PostPublishCheckFn | undefined;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Metodi originali prima della patch (per ripristino su unpatch). */
|
|
155
|
+
interface PatchedOriginals {
|
|
156
|
+
getModels: Provider["getModels"];
|
|
157
|
+
stream: Provider["stream"] | undefined;
|
|
158
|
+
streamSimple: Provider["streamSimple"];
|
|
159
|
+
refreshModels?: Provider["refreshModels"];
|
|
160
|
+
filterModels?: Provider["filterModels"];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
interface PatchRecord {
|
|
164
|
+
/** Originali veri (mai i nostri wrapper): evolve con le adozioni. */
|
|
165
|
+
orig: PatchedOriginals;
|
|
166
|
+
/** I nostri wrapper, per distinguerli dalle riassegnazioni altrui. */
|
|
167
|
+
wrapGetModels: Provider["getModels"];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const patchedOriginals = new WeakMap<object, PatchRecord>();
|
|
171
|
+
|
|
172
|
+
/** True se l'oggetto provider è attualmente patchato da noi. */
|
|
173
|
+
export function isPatched(provider: Provider): boolean {
|
|
174
|
+
return patchedOriginals.has(provider);
|
|
175
|
+
}
|
|
139
176
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
177
|
+
export function patchProvider(provider: Provider, deps: PatchDeps): string[] {
|
|
178
|
+
const previous = patchedOriginals.get(provider);
|
|
179
|
+
const rawStream: unknown = previous
|
|
180
|
+
? previous.orig.stream
|
|
181
|
+
: (provider as { stream?: unknown }).stream;
|
|
182
|
+
const base: PatchedOriginals = previous?.orig ?? {
|
|
183
|
+
// Raw, mai bind: invocati con this=provider al momento della chiamata.
|
|
184
|
+
getModels: provider.getModels,
|
|
185
|
+
stream: typeof rawStream === "function"
|
|
186
|
+
? (rawStream as NonNullable<Provider["stream"]>)
|
|
187
|
+
: undefined,
|
|
188
|
+
streamSimple: provider.streamSimple,
|
|
189
|
+
...(provider.refreshModels
|
|
190
|
+
? { refreshModels: provider.refreshModels }
|
|
191
|
+
: {}),
|
|
192
|
+
...(provider.filterModels
|
|
193
|
+
? { filterModels: provider.filterModels }
|
|
194
|
+
: {}),
|
|
195
|
+
};
|
|
196
|
+
const twinIds = [...deps.twinModelIds];
|
|
197
|
+
const providerId = provider.id;
|
|
198
|
+
|
|
199
|
+
// Risoluzione pigra: legge SEMPRE i modelli correnti (nessuna snapshot
|
|
200
|
+
// che diventa stale dopo refresh altrui o cataloghi dinamici). Se il
|
|
201
|
+
// proprietario riassegna getModels dopo la patch, la nuova fn viene
|
|
202
|
+
// adottata come base (i nostri wrapper si riconoscono e non si adottano).
|
|
203
|
+
function currentBase(): PatchedOriginals {
|
|
204
|
+
const rec = patchedOriginals.get(provider);
|
|
205
|
+
const orig = rec?.orig ?? base;
|
|
206
|
+
const cur = provider.getModels as Provider["getModels"];
|
|
207
|
+
if (rec && cur !== rec.wrapGetModels && cur !== orig.getModels) {
|
|
208
|
+
orig.getModels = cur;
|
|
209
|
+
}
|
|
210
|
+
return orig;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function currentOriginals(): Model<Api>[] {
|
|
214
|
+
return mergeModelsById(stripTwins(currentBase().getModels.call(provider)), []);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function resolveTwin(modelId: string): Model<Api> | undefined {
|
|
218
|
+
if (!isTwinId(modelId)) return undefined;
|
|
219
|
+
const wanted = untwinId(modelId);
|
|
220
|
+
if (!twinIds.includes(wanted)) return undefined;
|
|
221
|
+
return currentOriginals().find((m) => m.id === wanted);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function twinEntries(): Model<Api>[] {
|
|
225
|
+
const originals = currentOriginals();
|
|
226
|
+
const chosen = new Set(twinIds);
|
|
146
227
|
const twins: Model<Api>[] = [];
|
|
228
|
+
const seen = new Set<string>();
|
|
147
229
|
for (const original of originals) {
|
|
148
|
-
if (chosen.has(original.id))
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
230
|
+
if (!chosen.has(original.id)) continue;
|
|
231
|
+
const twin = twinModelFor(original, providerId);
|
|
232
|
+
if (seen.has(twin.id)) continue;
|
|
233
|
+
seen.add(twin.id);
|
|
234
|
+
twins.push(twin);
|
|
154
235
|
}
|
|
155
|
-
|
|
156
|
-
return currentModels;
|
|
236
|
+
return twins;
|
|
157
237
|
}
|
|
158
238
|
|
|
159
|
-
rebuildTwins(upstream.getModels());
|
|
160
|
-
|
|
161
239
|
function upstreamCall(
|
|
162
240
|
model: Model<Api>,
|
|
163
241
|
context: Context,
|
|
164
242
|
options?: SimpleStreamOptions,
|
|
165
243
|
): AssistantMessageEventStream {
|
|
166
|
-
//
|
|
167
|
-
//
|
|
244
|
+
// Gli alias pubblici non sono cambi di modello: normalizza la cronologia
|
|
245
|
+
// preservando firme reasoning e tool id nel transcript upstream.
|
|
168
246
|
const originalContext: Context = {
|
|
169
247
|
...context,
|
|
170
248
|
messages: context.messages.map((message) =>
|
|
171
|
-
message.role === "assistant" && message.provider ===
|
|
249
|
+
message.role === "assistant" && message.provider === providerId && isTwinId(message.model)
|
|
172
250
|
? { ...message, model: untwinId(message.model) }
|
|
173
251
|
: message),
|
|
174
252
|
};
|
|
175
|
-
|
|
176
|
-
return deps.upstreamStreams.streamSimple(model, originalContext, options);
|
|
177
|
-
}
|
|
178
|
-
return upstream.streamSimple(model, originalContext, options);
|
|
253
|
+
return base.streamSimple.call(provider, model, originalContext, options);
|
|
179
254
|
}
|
|
180
255
|
|
|
181
256
|
function guardedStreamSimple(
|
|
@@ -183,9 +258,9 @@ export function buildOverlaidProvider(deps: OverlayDeps): Provider {
|
|
|
183
258
|
context: Context,
|
|
184
259
|
options?: SimpleStreamOptions,
|
|
185
260
|
): AssistantMessageEventStream {
|
|
186
|
-
const twinOf =
|
|
187
|
-
//
|
|
188
|
-
//
|
|
261
|
+
const twinOf = resolveTwin(model.id);
|
|
262
|
+
// Un alias stale/sconosciuto non deve mai arrivare all'API upstream,
|
|
263
|
+
// anche se un refresh ha rimosso l'originale dal catalogo.
|
|
189
264
|
if (!twinOf) {
|
|
190
265
|
if (!isTwinId(model.id)) return upstreamCall(model, context, options);
|
|
191
266
|
const stream = createAssistantMessageEventStream();
|
|
@@ -221,31 +296,29 @@ export function buildOverlaidProvider(deps: OverlayDeps): Provider {
|
|
|
221
296
|
});
|
|
222
297
|
}
|
|
223
298
|
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
getModels: () => currentModels,
|
|
232
|
-
stream: (model, context, options) =>
|
|
299
|
+
const wrapGetModels: Provider["getModels"] = () => [
|
|
300
|
+
...currentOriginals().map((m) => ({ ...m })),
|
|
301
|
+
...twinEntries(),
|
|
302
|
+
];
|
|
303
|
+
provider.getModels = wrapGetModels;
|
|
304
|
+
if (base.stream) {
|
|
305
|
+
provider.stream = ((model: any, context: any, options: any) =>
|
|
233
306
|
guardedStreamSimple(
|
|
234
307
|
model as Model<Api>,
|
|
235
308
|
context,
|
|
236
309
|
options as SimpleStreamOptions | undefined,
|
|
237
|
-
)
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
310
|
+
)) as Provider["stream"];
|
|
311
|
+
}
|
|
312
|
+
provider.streamSimple = ((model: any, context: any, options: any) =>
|
|
313
|
+
guardedStreamSimple(model as Model<Api>, context, options)) as Provider["streamSimple"];
|
|
241
314
|
|
|
242
|
-
// Forward refresh con
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
if (
|
|
246
|
-
const upstreamRefresh =
|
|
247
|
-
|
|
248
|
-
await upstreamRefresh({
|
|
315
|
+
// Forward refresh con persist depurato dai twin: lo store è letto anche
|
|
316
|
+
// dal builtin a overlay assente, che li scambierebbe per modelli normali
|
|
317
|
+
// (senza gate!). La risoluzione pigra non ha snapshot da ricostruire.
|
|
318
|
+
if (base.refreshModels) {
|
|
319
|
+
const upstreamRefresh = base.refreshModels;
|
|
320
|
+
provider.refreshModels = (async (ctx: RefreshModelsContext): Promise<void> => {
|
|
321
|
+
await upstreamRefresh.call(provider, {
|
|
249
322
|
...ctx,
|
|
250
323
|
publish: (publication) =>
|
|
251
324
|
ctx.publish({
|
|
@@ -260,41 +333,60 @@ export function buildOverlaidProvider(deps: OverlayDeps): Provider {
|
|
|
260
333
|
},
|
|
261
334
|
update: publication.update
|
|
262
335
|
? () => {
|
|
263
|
-
// Applica prima l'update upstream (popola il suo oggetto),
|
|
264
|
-
// poi ricostruisci originali + twin da quelli freschi.
|
|
265
336
|
publication.update?.();
|
|
266
|
-
rebuildTwins(upstream.getModels());
|
|
267
337
|
}
|
|
268
338
|
: undefined,
|
|
269
339
|
}),
|
|
270
340
|
});
|
|
271
|
-
};
|
|
341
|
+
}) as Provider["refreshModels"];
|
|
272
342
|
}
|
|
273
343
|
|
|
274
344
|
// Forward filtro credenziali: si applica agli originali, i twin seguono
|
|
275
345
|
// il destino dell'originale corrispondente.
|
|
276
|
-
if (
|
|
277
|
-
const upstreamFilter =
|
|
278
|
-
|
|
346
|
+
if (base.filterModels) {
|
|
347
|
+
const upstreamFilter = base.filterModels;
|
|
348
|
+
provider.filterModels = ((
|
|
279
349
|
models: readonly Model<Api>[],
|
|
280
350
|
credential: Credential | undefined,
|
|
281
351
|
): readonly Model<Api>[] => {
|
|
282
352
|
const originals = models.filter((m) => !isTwinId(m.id));
|
|
283
353
|
const kept = new Set(
|
|
284
|
-
upstreamFilter(originals as ModelT<Api>[], credential).map((m) => m.id),
|
|
354
|
+
upstreamFilter.call(provider, originals as ModelT<Api>[], credential).map((m) => m.id),
|
|
285
355
|
);
|
|
286
356
|
return models.filter((m) =>
|
|
287
357
|
isTwinId(m.id) ? kept.has(untwinId(m.id)) : kept.has(m.id),
|
|
288
358
|
);
|
|
289
|
-
};
|
|
359
|
+
}) as Provider["filterModels"];
|
|
290
360
|
}
|
|
291
361
|
|
|
292
|
-
if (
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
362
|
+
if (previous) {
|
|
363
|
+
// Re-patch: i wrapper si ricreano, gli originali veri restano.
|
|
364
|
+
// Aggiorna il riferimento al wrapper corrente o l'adozione
|
|
365
|
+
// scambierebbe il nuovo wrapper per una fn altrui.
|
|
366
|
+
previous.wrapGetModels = wrapGetModels;
|
|
367
|
+
patchedOriginals.set(provider, previous);
|
|
368
|
+
} else {
|
|
369
|
+
patchedOriginals.set(provider, { orig: base, wrapGetModels });
|
|
297
370
|
}
|
|
371
|
+
return twinEntries().map((t) => t.id);
|
|
372
|
+
}
|
|
298
373
|
|
|
299
|
-
|
|
374
|
+
/**
|
|
375
|
+
* Ripristina i metodi originali. Ritorna false se l'oggetto non era patchato
|
|
376
|
+
* (es. già sostituito dal proprietario: niente da ripristinare).
|
|
377
|
+
*/
|
|
378
|
+
export function unpatchProvider(provider: Provider): boolean {
|
|
379
|
+
const rec = patchedOriginals.get(provider);
|
|
380
|
+
const originals = rec?.orig;
|
|
381
|
+
if (!originals) return false;
|
|
382
|
+
provider.getModels = originals.getModels;
|
|
383
|
+
if (originals.stream) provider.stream = originals.stream;
|
|
384
|
+
else delete (provider as { stream?: unknown }).stream;
|
|
385
|
+
provider.streamSimple = originals.streamSimple;
|
|
386
|
+
if (originals.refreshModels) provider.refreshModels = originals.refreshModels;
|
|
387
|
+
else delete (provider as { refreshModels?: unknown }).refreshModels;
|
|
388
|
+
if (originals.filterModels) provider.filterModels = originals.filterModels;
|
|
389
|
+
else delete (provider as { filterModels?: unknown }).filterModels;
|
|
390
|
+
patchedOriginals.delete(provider);
|
|
391
|
+
return true;
|
|
300
392
|
}
|
package/src/commands.ts
CHANGED
|
@@ -19,6 +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
23
|
|
|
23
24
|
export interface GateStatusView {
|
|
24
25
|
active: boolean;
|
|
@@ -79,6 +80,27 @@ export interface JevCommandDeps {
|
|
|
79
80
|
hasStaleTests: boolean;
|
|
80
81
|
};
|
|
81
82
|
setGrounding: (mode: "off" | "warn" | "repair" | "hold") => JevConfig;
|
|
83
|
+
verificationStatus?: (cwd: string) => {
|
|
84
|
+
enabled: boolean;
|
|
85
|
+
mode: VerificationMode;
|
|
86
|
+
manifestPath: string;
|
|
87
|
+
manifestLoaded: boolean;
|
|
88
|
+
manifestError?: string;
|
|
89
|
+
changedFiles: string[];
|
|
90
|
+
checks: Array<{ id: string; state: "passed" | "failed" | "pending" }>;
|
|
91
|
+
};
|
|
92
|
+
setVerification?: (mode: "off" | VerificationMode) => JevConfig;
|
|
93
|
+
reloadVerificationManifest?: (cwd: string) => {
|
|
94
|
+
ok: boolean;
|
|
95
|
+
error?: string;
|
|
96
|
+
path?: string;
|
|
97
|
+
};
|
|
98
|
+
runVerification?: (args: {
|
|
99
|
+
cwd: string;
|
|
100
|
+
selection: string | undefined;
|
|
101
|
+
signal?: AbortSignal;
|
|
102
|
+
trusted: boolean;
|
|
103
|
+
}) => Promise<{ ok: boolean; message: string; results: unknown[] }>;
|
|
82
104
|
}
|
|
83
105
|
|
|
84
106
|
export const JEV_SUBCOMMANDS = [
|
|
@@ -96,6 +118,8 @@ export const JEV_SUBCOMMANDS = [
|
|
|
96
118
|
"output",
|
|
97
119
|
"last",
|
|
98
120
|
"grounding",
|
|
121
|
+
"verification",
|
|
122
|
+
"verify",
|
|
99
123
|
"backend",
|
|
100
124
|
"save-key",
|
|
101
125
|
"help",
|
|
@@ -121,6 +145,10 @@ export const JEV_HELP_TEXT = [
|
|
|
121
145
|
" /jev last Ultimo verdetto del gate (twin): esito e controlli",
|
|
122
146
|
" /jev grounding off|warn|repair|hold",
|
|
123
147
|
" Grounding affermazioni sessione (solo automatic, opt-in)",
|
|
148
|
+
" /jev verification off|warn|repair|hold|reload",
|
|
149
|
+
" Footer deterministico + repair dai check progetto (opt-in; reload acquisisce manifest)",
|
|
150
|
+
" /jev verify [recommended|all|id]",
|
|
151
|
+
" Esegue solo check dichiarati in .pi/jev-verification.json",
|
|
124
152
|
" /jev backend Mostra backend attivo e chiavi rilevate",
|
|
125
153
|
" /jev backend auto|openrouter|typesafe",
|
|
126
154
|
" Cambia backend (salvato in jev-config.json)",
|
|
@@ -140,7 +168,7 @@ export const JEV_HELP_TEXT = [
|
|
|
140
168
|
export function registerJevCommand(pi: ExtensionAPI, deps: JevCommandDeps) {
|
|
141
169
|
pi.registerCommand("jev", {
|
|
142
170
|
description:
|
|
143
|
-
"Jev guard:
|
|
171
|
+
"Jev guard: gate, grounding e verifica progetto. Sottocomandi: status, mode, upstream, check, verification, verify, stats, help.",
|
|
144
172
|
getArgumentCompletions: (argumentPrefix) => {
|
|
145
173
|
const prefix = argumentPrefix.trim().toLowerCase();
|
|
146
174
|
return JEV_SUBCOMMANDS.filter((s) => s.startsWith(prefix)).map(
|
|
@@ -188,6 +216,7 @@ export function registerJevCommand(pi: ExtensionAPI, deps: JevCommandDeps) {
|
|
|
188
216
|
const info = deps.reviewerInfo();
|
|
189
217
|
const gate = deps.gateStatus();
|
|
190
218
|
const twinView = deps.twinStatus();
|
|
219
|
+
const verification = deps.verificationStatus?.(ctx.cwd);
|
|
191
220
|
const authProvider = twinView.primary?.provider ?? cfg.automatic.upstreamProvider;
|
|
192
221
|
let guardedAuth = "—";
|
|
193
222
|
if (gate.active) {
|
|
@@ -250,6 +279,11 @@ export function registerJevCommand(pi: ExtensionAPI, deps: JevCommandDeps) {
|
|
|
250
279
|
`Exfil check: ${cfg.exfilCheck.enabled ? "attivo in automatic (confirm in TUI, fail-open)" : "disattivo (config)"}`,
|
|
251
280
|
`Output judge: ${cfg.outputJudge.enabled ? `attivo in automatic (tool: ${cfg.outputJudge.tools.join(",") || "—"}, fail-open)` : "disattivo (config)"}`,
|
|
252
281
|
`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
|
+
verification
|
|
283
|
+
? `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})`}`
|
|
286
|
+
: "Verifica progetto: non disponibile in questa estensione",
|
|
253
287
|
cfg.mode === "automatic"
|
|
254
288
|
? "ATTENZIONE: in automatic testo/codice vengono inviati al verificatore. Crea il twin con /jev upstream e seleziona il modello __jev per enforcement."
|
|
255
289
|
: "on-demand: nessuna verifica automatica.",
|
|
@@ -465,6 +499,9 @@ export function registerJevCommand(pi: ExtensionAPI, deps: JevCommandDeps) {
|
|
|
465
499
|
last.report.grounding
|
|
466
500
|
? `grounding: ${last.report.grounding.action} (${last.report.grounding.reasons.join(", ") || "n/a"})`
|
|
467
501
|
: "grounding: non intervenuto",
|
|
502
|
+
last.report.verification
|
|
503
|
+
? `verifica progetto: ${last.report.verification.action} (file=${last.report.verification.changedFiles.join(",") || "—"}; check=${last.report.verification.checks.map((check) => `${check.id}:${check.state}`).join(",") || "—"})`
|
|
504
|
+
: "verifica progetto: non intervenuta",
|
|
468
505
|
v.errorCode ? `errore: ${v.errorCode}` : `backend: ${v.backend ?? "?"} · modello: ${v.model ?? "?"}`,
|
|
469
506
|
v.truncated ? `troncato: ${JSON.stringify(v.truncated)}` : "copertura intera",
|
|
470
507
|
`tempo verifica: ${Math.round(v.elapsedMs)}ms`,
|
|
@@ -521,6 +558,74 @@ export function registerJevCommand(pi: ExtensionAPI, deps: JevCommandDeps) {
|
|
|
521
558
|
return;
|
|
522
559
|
}
|
|
523
560
|
|
|
561
|
+
if (sub === "verification") {
|
|
562
|
+
if (!deps.verificationStatus || !deps.setVerification) {
|
|
563
|
+
ctx.ui.notify("Verifica progetto non disponibile in questa versione dell'estensione.", "warning");
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
const value = parts[1];
|
|
567
|
+
if (value === undefined) {
|
|
568
|
+
const st = deps.verificationStatus(ctx.cwd);
|
|
569
|
+
ctx.ui.notify(
|
|
570
|
+
st.enabled
|
|
571
|
+
? [
|
|
572
|
+
`Verifica progetto attiva in automatic (mode=${st.mode}).`,
|
|
573
|
+
`Manifest: ${st.manifestPath} — ${st.manifestLoaded ? "OK" : st.manifestError ?? "non disponibile"}.`,
|
|
574
|
+
`Modifiche osservate: ${st.changedFiles.join(", ") || "nessuna"}.`,
|
|
575
|
+
`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.",
|
|
577
|
+
].join("\n")
|
|
578
|
+
: `Verifica progetto disattiva (opt-in). Manifest previsto: ${st.manifestPath}. Attiva con /jev verification warn|repair|hold.`,
|
|
579
|
+
"info",
|
|
580
|
+
);
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
if (value === "reload") {
|
|
584
|
+
if (!deps.reloadVerificationManifest) {
|
|
585
|
+
ctx.ui.notify("Reload manifest non disponibile in questa versione dell'estensione.", "warning");
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
const reloaded = deps.reloadVerificationManifest(ctx.cwd);
|
|
589
|
+
ctx.ui.notify(
|
|
590
|
+
reloaded.ok
|
|
591
|
+
? `Manifest verifica acquisito: ${reloaded.path}. I comandi verranno eseguiti solo da questa snapshot.`
|
|
592
|
+
: `Manifest verifica non acquisito: ${reloaded.error}`,
|
|
593
|
+
reloaded.ok ? "info" : "warning",
|
|
594
|
+
);
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
if (value !== "off" && value !== "warn" && value !== "repair" && value !== "hold") {
|
|
598
|
+
ctx.ui.notify("Uso: /jev verification off|warn|repair|hold|reload", "warning");
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
deps.setVerification(value);
|
|
602
|
+
ctx.ui.notify(
|
|
603
|
+
value === "off"
|
|
604
|
+
? "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.`,
|
|
606
|
+
"info",
|
|
607
|
+
);
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
if (sub === "verify") {
|
|
612
|
+
if (!deps.runVerification) {
|
|
613
|
+
ctx.ui.notify("Verifica progetto non disponibile in questa versione dell'estensione.", "warning");
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
const trusted = typeof (ctx as { isProjectTrusted?: unknown }).isProjectTrusted === "function"
|
|
617
|
+
? ctx.isProjectTrusted()
|
|
618
|
+
: false;
|
|
619
|
+
const result = await deps.runVerification({
|
|
620
|
+
cwd: ctx.cwd,
|
|
621
|
+
selection: parts[1],
|
|
622
|
+
signal: ctx.signal ?? undefined,
|
|
623
|
+
trusted,
|
|
624
|
+
});
|
|
625
|
+
ctx.ui.notify(result.message, result.ok ? "info" : "warning");
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
|
|
524
629
|
if (sub === "reload") {
|
|
525
630
|
const cfg = deps.reload();
|
|
526
631
|
ctx.ui.notify(`Config ricaricata. mode=${cfg.mode} backend=${cfg.jev.backend}`, "info");
|
package/src/config.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
4
|
import type { JevBackend } from "./reviewer.ts";
|
|
5
|
+
import type { VerificationSettings } from "./verification.ts";
|
|
5
6
|
|
|
6
7
|
export type JevMode = "on-demand" | "automatic";
|
|
7
8
|
export type JevBackendSetting = JevBackend | "auto";
|
|
@@ -97,6 +98,7 @@ export interface JevConfig {
|
|
|
97
98
|
/** Una modifica riuscita invalida i test precedenti (stale). */
|
|
98
99
|
invalidateTestsOnEdit: boolean;
|
|
99
100
|
};
|
|
101
|
+
verification: VerificationSettings;
|
|
100
102
|
exfilCheck: {
|
|
101
103
|
/** Check semantico sui comandi con tool di rete (solo automatic). */
|
|
102
104
|
enabled: boolean;
|
|
@@ -194,6 +196,17 @@ export const DEFAULT_CONFIG: JevConfig = {
|
|
|
194
196
|
cacheSeconds: 120,
|
|
195
197
|
invalidateTestsOnEdit: true,
|
|
196
198
|
},
|
|
199
|
+
verification: {
|
|
200
|
+
// Opt-in: il manifest può eseguire solo comandi esplicitamente approvati
|
|
201
|
+
// dal progetto; non abilitarlo silenziosamente in installazioni esistenti.
|
|
202
|
+
enabled: false,
|
|
203
|
+
mode: "warn",
|
|
204
|
+
manifestPath: ".pi/jev-verification.json",
|
|
205
|
+
maxRepairs: 1,
|
|
206
|
+
commandTimeoutMs: 120000,
|
|
207
|
+
outputChars: 6000,
|
|
208
|
+
maxCommands: 3,
|
|
209
|
+
},
|
|
197
210
|
exfilCheck: {
|
|
198
211
|
enabled: true,
|
|
199
212
|
tools: ["bash", "powershell"],
|