pi-jev-guard 0.7.0 → 0.7.1

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
@@ -190,10 +190,11 @@ comandi fissi dell'estensione quando trova segnali convenzionali:
190
190
  | `package.json` con script | `npm run typecheck`, `npm run lint`, uno tra `npm run test:unit`, `npm run test:ci`, `npm test` |
191
191
  | `Cargo.toml` | `cargo check`, `cargo test` |
192
192
  | `go.mod` | `go test ./...` |
193
+ | `*.sln` / `*.csproj` (root o 2 livelli) | `dotnet build`, `dotnet test` |
193
194
  | configurazione pytest | `python -m pytest` |
194
195
  | repository Git | `git diff --check` |
195
196
 
196
- Il profilo sceglie un solo ecosistema (Node → Rust → Go → Python) e aggiunge
197
+ Il profilo sceglie un solo ecosistema (Node → Rust → Go → .NET → Python) e aggiunge
197
198
  il check Git quando applicabile. I comandi sono bounded da
198
199
  `verification.maxCommands` (default `4`) e timeout. Se nessun segnale è
199
200
  rilevato, non esegue nulla e il footer dichiara la limitazione.
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.1",
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
@@ -296,7 +296,7 @@ export interface GroundingDecision {
296
296
 
297
297
  /** Nota accodata al testo pubblicato quando il grounding scatta senza hold. */
298
298
  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.`;
299
+ 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
300
  }
301
301
 
302
302
  /** Feedback per la rigenerazione: correggere, non accusare. */
@@ -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 {