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 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 `tool_result`
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.
@@ -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 runProjectVerification(args: {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-jev-guard",
3
- "version": "0.7.0",
3
+ "version": "0.7.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": [
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 === "test" && !prior.stale && prior.seq < entry.seq) {
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 === "test" && e.stale),
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(", ")}) — verifica col modello i file/comandi citati.`;
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 }),
@@ -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(
@@ -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
- | { checks: VerificationCheck[]; inputs: ProfileInput[] }
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
- }): { checks: VerificationCheck[]; inputs: ProfileInput[] } | { error: string } | undefined {
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
- | { checks: VerificationCheck[]; inputs: ProfileInput[] }
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 python = node || cargo || go ? undefined : pythonProfile(root);
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 {