pi-jev-guard 0.7.0 → 0.7.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 +8 -3
- package/extensions/index.ts +66 -1
- package/package.json +1 -1
- package/src/grounding.ts +36 -4
- package/src/tools.ts +1 -0
- package/src/verification-tool.ts +1 -0
- package/src/verification.ts +69 -7
package/README.md
CHANGED
|
@@ -148,7 +148,9 @@ Non blocca mai; `/jev output` mostra l'ultimo verdetto.
|
|
|
148
148
|
|
|
149
149
|
Sempre in `automatic`, il **grounding** (opt-in, default spento) confronta le
|
|
150
150
|
affermazioni sulla sessione — "ho letto il file", "i test passano", "ho
|
|
151
|
-
eseguito X" — con un registro evidenza costruito dai
|
|
151
|
+
eseguito X", "il guard riporta X" — con un registro evidenza costruito dai
|
|
152
|
+
`tool_result` e dai fatti generati dal guard (footer di verifica, run
|
|
153
|
+
`jev_verify`)
|
|
152
154
|
(comandi, test, letture, modifiche; redatto, max 30 voci, solo sessione
|
|
153
155
|
corrente). Due domande Jev strette: contraddizione con l'evidenza e
|
|
154
156
|
affermazione non supportata. Senza evidenza non si giudica: "senza
|
|
@@ -165,7 +167,9 @@ Il grounding è probabilistico; per **compilazione, lint e test** serve una
|
|
|
165
167
|
prova deterministica. La verifica progetto conserva, solo nella sessione,
|
|
166
168
|
`edit`/`write` riusciti e gli esiti osservati dei check. Il footer viene
|
|
167
169
|
generato dall'estensione, non dal modello: un test non eseguito non può
|
|
168
|
-
diventare “test passati”.
|
|
170
|
+
diventare “test passati”. Un report di “verifica” scritto dal modello non ha
|
|
171
|
+
valore: il footer autorevole è solo quello del guard (marcato `generata dal
|
|
172
|
+
guard, non dal modello`) e una guideline ai tool scoraggia le imitazioni.
|
|
169
173
|
|
|
170
174
|
#### Nuovi progetti: zero manifest locale
|
|
171
175
|
|
|
@@ -190,10 +194,11 @@ comandi fissi dell'estensione quando trova segnali convenzionali:
|
|
|
190
194
|
| `package.json` con script | `npm run typecheck`, `npm run lint`, uno tra `npm run test:unit`, `npm run test:ci`, `npm test` |
|
|
191
195
|
| `Cargo.toml` | `cargo check`, `cargo test` |
|
|
192
196
|
| `go.mod` | `go test ./...` |
|
|
197
|
+
| `*.sln` / `*.csproj` (root o 2 livelli) | `dotnet build`, `dotnet test` |
|
|
193
198
|
| configurazione pytest | `python -m pytest` |
|
|
194
199
|
| repository Git | `git diff --check` |
|
|
195
200
|
|
|
196
|
-
Il profilo sceglie un solo ecosistema (Node → Rust → Go → Python) e aggiunge
|
|
201
|
+
Il profilo sceglie un solo ecosistema (Node → Rust → Go → .NET → Python) e aggiunge
|
|
197
202
|
il check Git quando applicabile. I comandi sono bounded da
|
|
198
203
|
`verification.maxCommands` (default `4`) e timeout. Se nessun segnale è
|
|
199
204
|
rilevato, non esegue nulla e il footer dichiara la limitazione.
|
package/extensions/index.ts
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
OUTPUT_QUESTIONS,
|
|
33
33
|
buildOutputState,
|
|
34
34
|
createVerdictCache,
|
|
35
|
+
elideText,
|
|
35
36
|
evaluateOutput,
|
|
36
37
|
outputKey,
|
|
37
38
|
toolResultText,
|
|
@@ -52,6 +53,7 @@ import {
|
|
|
52
53
|
decideGroundingAction,
|
|
53
54
|
evaluateGrounding,
|
|
54
55
|
groundingKey,
|
|
56
|
+
guardEvidenceEntry,
|
|
55
57
|
recordEvidence,
|
|
56
58
|
recordToolResult,
|
|
57
59
|
parseExitCode,
|
|
@@ -73,6 +75,7 @@ import {
|
|
|
73
75
|
selectVerificationChecks,
|
|
74
76
|
trustVerificationPolicy,
|
|
75
77
|
trustedVerificationPolicy,
|
|
78
|
+
verificationNotice,
|
|
76
79
|
type VerificationManifestTrust,
|
|
77
80
|
type VerificationStatus,
|
|
78
81
|
} from "../src/verification.ts";
|
|
@@ -224,7 +227,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
224
227
|
* Esegue SOLO check estratti dalla policy acquisita (locale, globale o
|
|
225
228
|
* profilo integrato). Serve sia al tool del modello sia a /jev verify.
|
|
226
229
|
*/
|
|
227
|
-
async function
|
|
230
|
+
async function executeProjectVerification(args: {
|
|
228
231
|
cwd: string;
|
|
229
232
|
selection: string | undefined;
|
|
230
233
|
signal?: AbortSignal;
|
|
@@ -273,6 +276,27 @@ export default function (pi: ExtensionAPI) {
|
|
|
273
276
|
};
|
|
274
277
|
}
|
|
275
278
|
|
|
279
|
+
/** Wrapper audit: ogni run (tool o comando) è evidenza per il grounding. */
|
|
280
|
+
async function runProjectVerification(args: {
|
|
281
|
+
cwd: string;
|
|
282
|
+
selection: string | undefined;
|
|
283
|
+
signal?: AbortSignal;
|
|
284
|
+
trusted: boolean;
|
|
285
|
+
}): Promise<VerificationToolRunResult> {
|
|
286
|
+
const outcome = await executeProjectVerification(args);
|
|
287
|
+
recordGuardFact({
|
|
288
|
+
tool: "jev_verify",
|
|
289
|
+
summary: `jev_verify: ${args.selection ?? "recommended"} → ${
|
|
290
|
+
outcome.results.length > 0
|
|
291
|
+
? outcome.results.map((r) => `${r.id}:${r.ok ? "pass" : "fail"}`).join(", ")
|
|
292
|
+
: "nessun check eseguito"
|
|
293
|
+
}`,
|
|
294
|
+
ok: outcome.results.length > 0 && outcome.results.every((r) => r.ok),
|
|
295
|
+
excerpt: outcome.message,
|
|
296
|
+
});
|
|
297
|
+
return outcome;
|
|
298
|
+
}
|
|
299
|
+
|
|
276
300
|
/** Provider live corrente dal registry (sempre fresco, mai snapshot). */
|
|
277
301
|
function liveUpstreamProvider(
|
|
278
302
|
registry: ModelRegistry,
|
|
@@ -860,6 +884,39 @@ export default function (pi: ExtensionAPI) {
|
|
|
860
884
|
}
|
|
861
885
|
}
|
|
862
886
|
|
|
887
|
+
/**
|
|
888
|
+
* I fatti generati dal guard (footer di verifica, run jev_verify) sono
|
|
889
|
+
* evidenza di prima classe per il grounding: senza questo, citare "il guard
|
|
890
|
+
* riporta X" risulterebbe sempre un'affermazione non supportata.
|
|
891
|
+
*/
|
|
892
|
+
function recordGuardFact(args: {
|
|
893
|
+
tool: "jev-guard" | "jev_verify";
|
|
894
|
+
summary: string;
|
|
895
|
+
ok: boolean;
|
|
896
|
+
excerpt?: string;
|
|
897
|
+
}): void {
|
|
898
|
+
try {
|
|
899
|
+
if (sessionMode !== "automatic" || !config.grounding.enabled) return;
|
|
900
|
+
const excerpt = args.excerpt?.trim()
|
|
901
|
+
? elideText(args.excerpt.trim(), Math.max(0, config.grounding.excerptChars))
|
|
902
|
+
: undefined;
|
|
903
|
+
const entry = guardEvidenceEntry({
|
|
904
|
+
tool: args.tool,
|
|
905
|
+
summary: args.summary,
|
|
906
|
+
ok: args.ok,
|
|
907
|
+
...(excerpt ? { excerpt } : {}),
|
|
908
|
+
seq: ++evidenceSeq,
|
|
909
|
+
at: Date.now(),
|
|
910
|
+
});
|
|
911
|
+
recordEvidence(evidenceLedger, entry, {
|
|
912
|
+
maxEntries: config.grounding.maxEntries,
|
|
913
|
+
invalidateTestsOnEdit: config.grounding.invalidateTestsOnEdit,
|
|
914
|
+
});
|
|
915
|
+
} catch {
|
|
916
|
+
// Mai rompere il flusso per il ledger.
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
|
|
863
920
|
function throttledGroundingError(error: unknown): void {
|
|
864
921
|
const now = Date.now();
|
|
865
922
|
if (now - lastGroundingErrorAt < 60_000) return;
|
|
@@ -996,6 +1053,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
996
1053
|
return async () => {
|
|
997
1054
|
const { status } = verificationSnapshot(cwd);
|
|
998
1055
|
if (status.changedFiles.length === 0) return { action: "publish" };
|
|
1056
|
+
// Il footer è un fatto del guard: entra nell'evidenza grounding,
|
|
1057
|
+
// così citarlo ("il guard riporta X") resta un claim verificabile.
|
|
1058
|
+
recordGuardFact({
|
|
1059
|
+
tool: "jev-guard",
|
|
1060
|
+
summary: `jev-guard: policy=${status.manifestSourceLabel ?? "sconosciuta"}; modifiche=${status.changedFiles.join(", ") || "nessuna"}; check=${status.checks.map((item) => `${item.check.id}:${item.state}`).join(", ") || "nessuno"}`,
|
|
1061
|
+
ok: status.allPassed,
|
|
1062
|
+
...(verificationNotice(status) ? { excerpt: verificationNotice(status) } : {}),
|
|
1063
|
+
});
|
|
999
1064
|
const decision = decideVerificationAction({
|
|
1000
1065
|
status,
|
|
1001
1066
|
mode,
|
package/package.json
CHANGED
package/src/grounding.ts
CHANGED
|
@@ -21,7 +21,7 @@ import type {
|
|
|
21
21
|
|
|
22
22
|
export type GroundingMode = "warn" | "repair" | "hold";
|
|
23
23
|
|
|
24
|
-
export type EvidenceKind = "command" | "test" | "read" | "modify";
|
|
24
|
+
export type EvidenceKind = "command" | "test" | "read" | "modify" | "verification";
|
|
25
25
|
|
|
26
26
|
export interface EvidenceEntry {
|
|
27
27
|
seq: number;
|
|
@@ -49,6 +49,38 @@ export function isTestCommand(command: string): boolean {
|
|
|
49
49
|
return TEST_COMMAND.test(command);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Prove di esito (test o verifica progetto): una modifica successiva le rende
|
|
54
|
+
* stale — non provano più lo stato attuale del codice.
|
|
55
|
+
*/
|
|
56
|
+
export function isOutcomeEvidence(kind: EvidenceKind): boolean {
|
|
57
|
+
return kind === "test" || kind === "verification";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Fatto generato dal guard (footer di verifica, run jev_verify): entra
|
|
62
|
+
* nell'evidenza al pari dei tool_result, così le affermazioni
|
|
63
|
+
* "il guard riporta X" sono verificabili invece di sembrare inventate.
|
|
64
|
+
*/
|
|
65
|
+
export function guardEvidenceEntry(args: {
|
|
66
|
+
tool: "jev-guard" | "jev_verify";
|
|
67
|
+
summary: string;
|
|
68
|
+
ok: boolean;
|
|
69
|
+
seq: number;
|
|
70
|
+
at: number;
|
|
71
|
+
excerpt?: string;
|
|
72
|
+
}): EvidenceEntry {
|
|
73
|
+
return {
|
|
74
|
+
seq: args.seq,
|
|
75
|
+
at: args.at,
|
|
76
|
+
tool: args.tool,
|
|
77
|
+
kind: "verification",
|
|
78
|
+
summary: args.summary,
|
|
79
|
+
ok: args.ok,
|
|
80
|
+
...(args.excerpt ? { excerpt: args.excerpt } : {}),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
52
84
|
const EXIT_CODE = /Command exited with code (\d+)/;
|
|
53
85
|
|
|
54
86
|
/** Exit code dal testo d'errore pi ("Command exited with code N"). */
|
|
@@ -152,7 +184,7 @@ export function recordEvidence(
|
|
|
152
184
|
let invalidated = 0;
|
|
153
185
|
if (opts.invalidateTestsOnEdit && entry.kind === "modify" && entry.ok) {
|
|
154
186
|
for (const prior of ledger) {
|
|
155
|
-
if (prior.kind
|
|
187
|
+
if (isOutcomeEvidence(prior.kind) && !prior.stale && prior.seq < entry.seq) {
|
|
156
188
|
prior.stale = true;
|
|
157
189
|
invalidated++;
|
|
158
190
|
}
|
|
@@ -197,7 +229,7 @@ export function buildEvidenceDigest(
|
|
|
197
229
|
}
|
|
198
230
|
return {
|
|
199
231
|
entries,
|
|
200
|
-
hasStaleTests: recent.some((e) => e.kind
|
|
232
|
+
hasStaleTests: recent.some((e) => isOutcomeEvidence(e.kind) && e.stale),
|
|
201
233
|
};
|
|
202
234
|
}
|
|
203
235
|
|
|
@@ -296,7 +328,7 @@ export interface GroundingDecision {
|
|
|
296
328
|
|
|
297
329
|
/** Nota accodata al testo pubblicato quando il grounding scatta senza hold. */
|
|
298
330
|
export function groundingNotice(reasons: string[]): string {
|
|
299
|
-
return `\n\n> [jev] grounding: affermazioni sulla sessione non supportate dall'evidenza (${reasons.join(", ")}) —
|
|
331
|
+
return `\n\n> [jev] grounding: affermazioni sulla sessione non supportate dall'evidenza (${reasons.join(", ")}) — controlla i file/comandi citati prima di darli per verificati.`;
|
|
300
332
|
}
|
|
301
333
|
|
|
302
334
|
/** Feedback per la rigenerazione: correggere, non accusare. */
|
package/src/tools.ts
CHANGED
|
@@ -20,6 +20,7 @@ export function registerJevTool(
|
|
|
20
20
|
"Use jev_validate when an independent targeted check is useful.",
|
|
21
21
|
"Do not describe review or unavailable as successful validation.",
|
|
22
22
|
"Jev validation does not replace compilation or tests.",
|
|
23
|
+
"Do not write your own verification-status sections in replies: the guard appends the only authoritative footer.",
|
|
23
24
|
],
|
|
24
25
|
parameters: Type.Object({
|
|
25
26
|
requirements: Type.String({ minLength: 1, maxLength: 16000 }),
|
package/src/verification-tool.ts
CHANGED
|
@@ -34,6 +34,7 @@ export function registerVerificationTool(
|
|
|
34
34
|
"After a successful edit/write, call jev_verify with check='recommended' when verification is enabled.",
|
|
35
35
|
"Use only this tool's declared check ids; never claim code compiles or tests pass without its observed result.",
|
|
36
36
|
"If a check fails, inspect its actual output, repair the code, then run the check again.",
|
|
37
|
+
"Never write your own verification report or imitate the guard footer in replies: the guard appends the only authoritative footer.",
|
|
37
38
|
],
|
|
38
39
|
parameters: Type.Object({
|
|
39
40
|
check: Type.Optional(
|
package/src/verification.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
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, realpathSync, statSync } from "node:fs";
|
|
10
|
+
import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
11
11
|
import { homedir } from "node:os";
|
|
12
12
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
13
13
|
|
|
@@ -340,6 +340,14 @@ interface ProfileInput {
|
|
|
340
340
|
text: string;
|
|
341
341
|
}
|
|
342
342
|
|
|
343
|
+
/** Selezione del profilo: comandi fissi + input che decidono se applicarsi. */
|
|
344
|
+
interface ProfileSelection {
|
|
345
|
+
checks: VerificationCheck[];
|
|
346
|
+
inputs: ProfileInput[];
|
|
347
|
+
/** Solo nomi dei marker (es. .sln/.csproj): il contenuto non sceglie comandi. */
|
|
348
|
+
markers?: string[];
|
|
349
|
+
}
|
|
350
|
+
|
|
343
351
|
function readProfileInput(path: string): ProfileInput | { error: string } {
|
|
344
352
|
try {
|
|
345
353
|
if (statSync(path).size > MAX_PROFILE_INPUT_BYTES) {
|
|
@@ -370,7 +378,7 @@ function findGitRoot(cwd: string): string | undefined {
|
|
|
370
378
|
}
|
|
371
379
|
|
|
372
380
|
function nodeProfile(root: string):
|
|
373
|
-
|
|
|
381
|
+
| ProfileSelection
|
|
374
382
|
| { error: string }
|
|
375
383
|
| undefined {
|
|
376
384
|
const path = join(root, "package.json");
|
|
@@ -408,7 +416,7 @@ function markerProfile(args: {
|
|
|
408
416
|
root: string;
|
|
409
417
|
marker: string;
|
|
410
418
|
checks: VerificationCheck[];
|
|
411
|
-
}):
|
|
419
|
+
}): ProfileSelection | { error: string } | undefined {
|
|
412
420
|
const path = join(args.root, args.marker);
|
|
413
421
|
if (!existsSync(path)) return undefined;
|
|
414
422
|
const input = readProfileInput(path);
|
|
@@ -416,8 +424,59 @@ function markerProfile(args: {
|
|
|
416
424
|
return { checks: args.checks, inputs: [input] };
|
|
417
425
|
}
|
|
418
426
|
|
|
427
|
+
const DOTNET_MARKER_EXT = [".sln", ".csproj", ".fsproj", ".vbproj"];
|
|
428
|
+
const DOTNET_SKIP_DIRS = new Set(["node_modules", ".git", "bin", "obj"]);
|
|
429
|
+
const DOTNET_FILES = [
|
|
430
|
+
"**/*.cs", "**/*.fs", "**/*.vb",
|
|
431
|
+
"**/*.csproj", "**/*.fsproj", "**/*.vbproj", "**/*.sln",
|
|
432
|
+
"**/*.props", "**/*.targets", "global.json",
|
|
433
|
+
];
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Marker .NET in root e due livelli di sottocartelle (bounded: max 40):
|
|
437
|
+
* solo nomi, il contenuto dei progetti non seleziona comandi — `dotnet
|
|
438
|
+
* build`/`dotnet test` sono fissi, quindi niente superficie di iniezione.
|
|
439
|
+
*/
|
|
440
|
+
function findDotnetMarkers(root: string): string[] {
|
|
441
|
+
const found: string[] = [];
|
|
442
|
+
const visit = (dir: string, depth: number): void => {
|
|
443
|
+
if (found.length >= 40 || depth > 2) return;
|
|
444
|
+
let entries;
|
|
445
|
+
try {
|
|
446
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
447
|
+
} catch {
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
451
|
+
if (found.length >= 40) return;
|
|
452
|
+
const full = join(dir, entry.name);
|
|
453
|
+
if (entry.isDirectory()) {
|
|
454
|
+
if (entry.name.startsWith(".") || DOTNET_SKIP_DIRS.has(entry.name)) continue;
|
|
455
|
+
visit(full, depth + 1);
|
|
456
|
+
} else if (DOTNET_MARKER_EXT.some((ext) => entry.name.endsWith(ext))) {
|
|
457
|
+
found.push(relative(root, full).split(sep).join("/"));
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
visit(root, 0);
|
|
462
|
+
return found.sort();
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function dotnetProfile(root: string): ProfileSelection | undefined {
|
|
466
|
+
const markers = findDotnetMarkers(root);
|
|
467
|
+
if (markers.length === 0) return undefined;
|
|
468
|
+
return {
|
|
469
|
+
checks: [
|
|
470
|
+
{ id: "dotnet-build", command: "dotnet build", description: ".NET build", files: DOTNET_FILES },
|
|
471
|
+
{ id: "dotnet-test", command: "dotnet test", description: ".NET tests", files: [...DOTNET_FILES, "**/tests/**"] },
|
|
472
|
+
],
|
|
473
|
+
inputs: [],
|
|
474
|
+
markers,
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
419
478
|
function pythonProfile(root: string):
|
|
420
|
-
|
|
|
479
|
+
| ProfileSelection
|
|
421
480
|
| { error: string }
|
|
422
481
|
| undefined {
|
|
423
482
|
const candidates = ["pytest.ini", "pyproject.toml", "setup.cfg"];
|
|
@@ -470,9 +529,10 @@ export function loadCommonVerificationProfile(cwd: string): LoadedVerificationMa
|
|
|
470
529
|
checks: [{ id: "go-test", command: "go test ./...", description: "Go tests", files: ["**/*.go", "go.mod", "go.sum"] }],
|
|
471
530
|
});
|
|
472
531
|
if (go && "error" in go) return { ok: false, error: go.error };
|
|
473
|
-
const
|
|
532
|
+
const dotnet = node || cargo || go ? undefined : dotnetProfile(root);
|
|
533
|
+
const python = node || cargo || go || dotnet ? undefined : pythonProfile(root);
|
|
474
534
|
if (python && "error" in python) return { ok: false, error: python.error };
|
|
475
|
-
const selected = node ?? cargo ?? go ?? python;
|
|
535
|
+
const selected = node ?? cargo ?? go ?? dotnet ?? python;
|
|
476
536
|
const checks = selected ? [...selected.checks] : [];
|
|
477
537
|
const gitRoot = findGitRoot(root);
|
|
478
538
|
if (gitRoot) {
|
|
@@ -481,16 +541,18 @@ export function loadCommonVerificationProfile(cwd: string): LoadedVerificationMa
|
|
|
481
541
|
if (checks.length === 0) {
|
|
482
542
|
return {
|
|
483
543
|
ok: false,
|
|
484
|
-
error: "nessun manifest locale e profilo globale common non applicabile (nessun progetto Node/Rust/Go/Python o repository Git rilevato)",
|
|
544
|
+
error: "nessun manifest locale e profilo globale common non applicabile (nessun progetto Node/Rust/Go/.NET/Python o repository Git rilevato)",
|
|
485
545
|
};
|
|
486
546
|
}
|
|
487
547
|
const inputs = selected?.inputs ?? [];
|
|
548
|
+
const markers = selected?.markers ?? [];
|
|
488
549
|
const manifest: VerificationManifest = { version: 1, checks };
|
|
489
550
|
const fingerprint = fingerprintText(JSON.stringify({
|
|
490
551
|
profile: "common-v1",
|
|
491
552
|
root,
|
|
492
553
|
gitRoot: gitRoot ?? null,
|
|
493
554
|
inputs: inputs.map((input) => ({ path: input.path, text: input.text })),
|
|
555
|
+
markers,
|
|
494
556
|
manifest,
|
|
495
557
|
}));
|
|
496
558
|
return {
|