veriskit 0.2.0 → 0.4.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.
Files changed (4) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +163 -109
  3. package/dist/index.js +1028 -80
  4. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -8,6 +8,20 @@ var __export = (target, all) => {
8
8
  __defProp(target, name, { get: all[name], enumerable: true });
9
9
  };
10
10
 
11
+ // src/version.ts
12
+ import { readFileSync } from "fs";
13
+ import { fileURLToPath } from "url";
14
+ var pkgUrl, VERSION;
15
+ var init_version = __esm({
16
+ "src/version.ts"() {
17
+ "use strict";
18
+ pkgUrl = new URL("../package.json", import.meta.url);
19
+ VERSION = JSON.parse(
20
+ readFileSync(fileURLToPath(pkgUrl), "utf8")
21
+ ).version;
22
+ }
23
+ });
24
+
11
25
  // src/util/fs-safe.ts
12
26
  import { existsSync } from "fs";
13
27
  import { mkdir, readFile, writeFile } from "fs/promises";
@@ -57,6 +71,7 @@ async function detectProject(root) {
57
71
  ];
58
72
  return {
59
73
  root,
74
+ name: pkg.name ?? void 0,
60
75
  packageManager: detectPackageManager(root),
61
76
  frameworks,
62
77
  languages,
@@ -153,7 +168,7 @@ function renderDoctor(project, env) {
153
168
  const ok = (s) => plain ? s : pc.green(s);
154
169
  const dim = (s) => plain ? s : pc.dim(s);
155
170
  const lines = [];
156
- lines.push("Veris doctor");
171
+ lines.push("VerisKit doctor");
157
172
  lines.push("");
158
173
  lines.push(`Package manager ${project.packageManager}`);
159
174
  lines.push(`Node ${env.node}`);
@@ -189,8 +204,80 @@ var init_doctor = __esm({
189
204
  }
190
205
  });
191
206
 
207
+ // src/evidence/record.ts
208
+ import { createHash } from "crypto";
209
+ import { basename } from "path";
210
+ function canonicalize(value) {
211
+ return JSON.stringify(sortValue(value));
212
+ }
213
+ function sortValue(value) {
214
+ if (Array.isArray(value)) return value.map(sortValue);
215
+ if (value && typeof value === "object") {
216
+ const src = value;
217
+ const out = {};
218
+ for (const key of Object.keys(src).sort()) {
219
+ if (src[key] !== void 0) out[key] = sortValue(src[key]);
220
+ }
221
+ return out;
222
+ }
223
+ return value;
224
+ }
225
+ function sha256(text) {
226
+ return `sha256:${createHash("sha256").update(text, "utf8").digest("hex")}`;
227
+ }
228
+ function computeDigest(record) {
229
+ const { digest: _omit, ...rest } = record;
230
+ return sha256(canonicalize(rest));
231
+ }
232
+ function buildRecord(run, git, logDigests, toolVersion) {
233
+ const runnerOf = new Map(
234
+ run.project.capabilities.map((c) => [c.id, c.runner])
235
+ );
236
+ const checks = run.results.map((r) => {
237
+ const check = {
238
+ id: r.checkId,
239
+ status: r.status,
240
+ durationMs: r.durationMs,
241
+ summary: r.summary
242
+ };
243
+ const runner = runnerOf.get(r.checkId);
244
+ if (runner) check.runner = runner;
245
+ if (r.counts) check.counts = r.counts;
246
+ const digest = logDigests[r.checkId];
247
+ if (digest) check.logDigest = digest;
248
+ return check;
249
+ });
250
+ const scope = run.scope ? { kind: run.scope.kind, changedCount: run.scope.changedCount } : { kind: "full", changedCount: 0 };
251
+ const base = {
252
+ schema: EVIDENCE_SCHEMA,
253
+ id: run.id,
254
+ startedAt: run.startedAt,
255
+ tool: { name: "veriskit", version: toolVersion },
256
+ git,
257
+ env: run.env,
258
+ project: {
259
+ name: run.project.name ?? basename(run.project.root),
260
+ packageManager: run.project.packageManager,
261
+ frameworks: run.project.frameworks,
262
+ languages: run.project.languages
263
+ },
264
+ scope,
265
+ checks,
266
+ verdict: run.verdict
267
+ };
268
+ return { ...base, digest: computeDigest(base) };
269
+ }
270
+ var EVIDENCE_SCHEMA;
271
+ var init_record = __esm({
272
+ "src/evidence/record.ts"() {
273
+ "use strict";
274
+ EVIDENCE_SCHEMA = "veriskit/evidence@1";
275
+ }
276
+ });
277
+
192
278
  // src/evidence/store.ts
193
- import { writeFile as writeFile2 } from "fs/promises";
279
+ import { readdirSync } from "fs";
280
+ import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
194
281
  import { join as join2 } from "path";
195
282
  function newRunId() {
196
283
  counter += 1;
@@ -207,11 +294,6 @@ async function writeLog(runDir, checkId, content) {
207
294
  await writeFile2(ref, content, "utf8");
208
295
  return ref;
209
296
  }
210
- async function writeMetadata(runDir, run) {
211
- const ref = join2(runDir, "metadata.json");
212
- await writeFile2(ref, JSON.stringify(run, null, 2), "utf8");
213
- return ref;
214
- }
215
297
  async function writeReport(root, id, markdown) {
216
298
  const dir = join2(root, ".veris", "reports");
217
299
  await ensureDir(dir);
@@ -219,11 +301,54 @@ async function writeReport(root, id, markdown) {
219
301
  await writeFile2(ref, markdown, "utf8");
220
302
  return ref;
221
303
  }
304
+ async function writeEvidence(runDir, record) {
305
+ const ref = join2(runDir, "evidence.json");
306
+ await writeFile2(ref, `${JSON.stringify(record, null, 2)}
307
+ `, "utf8");
308
+ return ref;
309
+ }
310
+ async function digestLogs(run) {
311
+ const out = {};
312
+ for (const r of run.results) {
313
+ if (!r.logRef) continue;
314
+ try {
315
+ out[r.checkId] = sha256(await readFile2(r.logRef, "utf8"));
316
+ } catch {
317
+ }
318
+ }
319
+ return out;
320
+ }
321
+ async function readRunLogs(runDir, checkIds) {
322
+ const out = {};
323
+ for (const id of checkIds) {
324
+ try {
325
+ out[id] = await readFile2(join2(runDir, `${id}.log`), "utf8");
326
+ } catch {
327
+ }
328
+ }
329
+ return out;
330
+ }
331
+ function latestRunDir(root) {
332
+ const runs = join2(root, ".veris", "runs");
333
+ try {
334
+ const dirs = readdirSync(runs, { withFileTypes: true }).filter((d) => d.isDirectory() && d.name !== "watch").map((d) => d.name).sort();
335
+ const latest = dirs.at(-1);
336
+ return latest ? join2(runs, latest) : null;
337
+ } catch {
338
+ return null;
339
+ }
340
+ }
341
+ async function ensureEvidenceDir(root) {
342
+ const dir = join2(root, ".veris", "evidence");
343
+ await ensureDir(dir);
344
+ return dir;
345
+ }
222
346
  var counter;
223
347
  var init_store = __esm({
224
348
  "src/evidence/store.ts"() {
225
349
  "use strict";
226
350
  init_fs_safe();
351
+ init_record();
227
352
  counter = 0;
228
353
  }
229
354
  });
@@ -231,7 +356,7 @@ var init_store = __esm({
231
356
  // src/util/exec.ts
232
357
  import { spawn } from "child_process";
233
358
  function exec(cmd, args, opts = {}) {
234
- return new Promise((resolve) => {
359
+ return new Promise((resolve2) => {
235
360
  const start = performance.now();
236
361
  const child = spawn(cmd, args, {
237
362
  cwd: opts.cwd,
@@ -249,7 +374,7 @@ function exec(cmd, args, opts = {}) {
249
374
  child.stderr.on("data", (d) => stderr += d.toString());
250
375
  child.on("close", (code) => {
251
376
  if (timer) clearTimeout(timer);
252
- resolve({
377
+ resolve2({
253
378
  code: code ?? 1,
254
379
  stdout,
255
380
  stderr,
@@ -259,7 +384,7 @@ function exec(cmd, args, opts = {}) {
259
384
  });
260
385
  child.on("error", () => {
261
386
  if (timer) clearTimeout(timer);
262
- resolve({
387
+ resolve2({
263
388
  code: 127,
264
389
  stdout,
265
390
  stderr: stderr || `failed to spawn ${cmd}`,
@@ -375,13 +500,13 @@ var init_jest = __esm({
375
500
  init_base();
376
501
  jestRunner = {
377
502
  id: "jest",
378
- toCheck(project, _cap) {
503
+ toCheck(project, _cap, opts) {
379
504
  return {
380
505
  id: "unit",
381
506
  title: "Unit tests",
382
507
  runner: "jest",
383
508
  cmd: localBin(project.root, "jest"),
384
- args: ["--ci"]
509
+ args: ["--ci", ...opts?.targetFiles ?? []]
385
510
  };
386
511
  },
387
512
  run(check, ctx) {
@@ -459,13 +584,14 @@ var init_vitest = __esm({
459
584
  init_base();
460
585
  vitestRunner = {
461
586
  id: "vitest",
462
- toCheck(project, _cap) {
587
+ toCheck(project, _cap, opts) {
588
+ const files = opts?.targetFiles ?? [];
463
589
  return {
464
590
  id: "unit",
465
591
  title: "Unit tests",
466
592
  runner: "vitest",
467
593
  cmd: localBin(project.root, "vitest"),
468
- args: ["run", "--reporter=json"]
594
+ args: ["run", "--reporter=json", ...files]
469
595
  };
470
596
  },
471
597
  run(check, ctx) {
@@ -544,7 +670,7 @@ var init_verdict = __esm({
544
670
  });
545
671
 
546
672
  // src/core/orchestrator.ts
547
- async function runChecks(project, ids, root) {
673
+ async function runChecks(project, ids, root, opts = {}) {
548
674
  const known = new Set(project.capabilities.map((c) => c.id));
549
675
  const unknown = ids.filter((id2) => !known.has(id2));
550
676
  if (unknown.length > 0) {
@@ -565,7 +691,9 @@ async function runChecks(project, ids, root) {
565
691
  summary
566
692
  };
567
693
  }
568
- const check = runner.toCheck(project, cap);
694
+ const check = runner.toCheck(project, cap, {
695
+ targetFiles: opts.targetFiles?.[capId]
696
+ });
569
697
  return runner.run(check, ctx);
570
698
  });
571
699
  const results = await Promise.all(tasks);
@@ -608,18 +736,18 @@ function glyph(status, plain) {
608
736
  function secs(ms) {
609
737
  return ms === 0 ? "" : `${(ms / 1e3).toFixed(1)}s`;
610
738
  }
611
- function renderRun(run) {
739
+ function renderRun(run, record) {
612
740
  const plain = isPlain();
613
741
  const bold = (s) => plain ? s : pc2.bold(s);
614
742
  const dim = (s) => plain ? s : pc2.dim(s);
615
743
  const lines = [];
616
744
  const scoped = run.scope?.kind;
617
745
  if (scoped) {
618
- lines.push(bold(`Veris \u2014 ${scoped}`));
746
+ lines.push(bold(`VerisKit \u2014 ${scoped}`));
619
747
  lines.push("");
620
748
  lines.push(`Scope ${run.scope?.changedCount ?? 0} changed file(s)`);
621
749
  } else {
622
- lines.push(bold("Veris"));
750
+ lines.push(bold("VerisKit"));
623
751
  lines.push("");
624
752
  lines.push(
625
753
  `Project ${run.project.root.split("/").pop() ?? run.project.root}`
@@ -643,6 +771,13 @@ function renderRun(run) {
643
771
  const label = run.verdict.state === "verified" ? scoped ? "\u2713 Affected checks passed" : "\u2713 Verified" : run.verdict.state === "failed" ? scoped ? "\u2717 Affected checks failed" : "\u2717 Failed" : scoped ? "\u25B2 Affected: partial" : "\u25B2 Partial";
644
772
  const color = run.verdict.state === "verified" ? pc2.green : run.verdict.state === "failed" ? pc2.red : pc2.yellow;
645
773
  lines.push(` ${plain ? label : color(label)}`);
774
+ if (record?.git) {
775
+ const g = record.git;
776
+ lines.push("");
777
+ lines.push(
778
+ `Commit ${g.commit.slice(0, 7)} ${g.dirty ? "\xB7 tree dirty" : "\xB7 tree clean"}`
779
+ );
780
+ }
646
781
  if (run.reportRef) {
647
782
  lines.push("");
648
783
  lines.push("Report");
@@ -698,7 +833,7 @@ async function runInit(root) {
698
833
  await writeIfAbsent(join4(dir, ".gitignore"), GITIGNORE);
699
834
  process.stdout.write(
700
835
  wroteConfig ? `Veris initialized. Detected checks: ${defaultChecks.join(", ") || "none"}.
701
- ` : "Veris already initialized (.veris/config.json exists). Nothing overwritten.\n"
836
+ ` : "VerisKit already initialized (.veris/config.json exists). Nothing overwritten.\n"
702
837
  );
703
838
  return 0;
704
839
  }
@@ -708,7 +843,14 @@ var init_init = __esm({
708
843
  "use strict";
709
844
  init_detect();
710
845
  init_fs_safe();
711
- GITIGNORE = ["runs/", "reports/", "cache/", ""].join("\n");
846
+ GITIGNORE = [
847
+ "runs/",
848
+ "reports/",
849
+ "cache/",
850
+ "graph.json",
851
+ "evidence/",
852
+ ""
853
+ ].join("\n");
712
854
  }
713
855
  });
714
856
 
@@ -724,15 +866,60 @@ var init_load = __esm({
724
866
  }
725
867
  });
726
868
 
869
+ // src/git/changes.ts
870
+ function collect(set, out) {
871
+ for (const line of out.split("\n")) {
872
+ const f = line.trim();
873
+ if (f) set.add(f);
874
+ }
875
+ }
876
+ async function changedFiles(root, opts = {}) {
877
+ const base = opts.base ?? null;
878
+ const files = /* @__PURE__ */ new Set();
879
+ const diffArgs = base ? ["diff", "--name-only", base] : ["diff", "--name-only", "HEAD"];
880
+ const diff = await exec("git", diffArgs, { cwd: root });
881
+ if (diff.code === 0) collect(files, diff.stdout);
882
+ const untracked = await exec(
883
+ "git",
884
+ ["ls-files", "--others", "--exclude-standard"],
885
+ { cwd: root }
886
+ );
887
+ if (untracked.code === 0) collect(files, untracked.stdout);
888
+ return { files: [...files].sort(), base };
889
+ }
890
+ async function gitAnchor(root) {
891
+ const head = await exec("git", ["rev-parse", "HEAD"], { cwd: root });
892
+ if (head.code !== 0) return null;
893
+ const commit = head.stdout.trim();
894
+ const branchRes = await exec("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
895
+ cwd: root
896
+ });
897
+ const branch = branchRes.code === 0 ? branchRes.stdout.trim() : "HEAD";
898
+ const status = await exec("git", ["status", "--porcelain"], { cwd: root });
899
+ const lines = status.code === 0 ? status.stdout.split("\n").map((l) => l.trim()).filter(Boolean) : [];
900
+ return {
901
+ commit,
902
+ branch,
903
+ dirty: lines.length > 0,
904
+ changedFiles: lines.length
905
+ };
906
+ }
907
+ var init_changes = __esm({
908
+ "src/git/changes.ts"() {
909
+ "use strict";
910
+ init_exec();
911
+ }
912
+ });
913
+
727
914
  // src/reporters/markdown.ts
728
915
  import { relative } from "path";
729
916
  function cell(value) {
730
917
  return value.replace(/\|/g, "\\|");
731
918
  }
732
- function renderMarkdown(run) {
919
+ function renderMarkdown(run, record) {
733
920
  const root = run.project.root;
734
921
  const lines = [];
735
- lines.push("# Veris Verification Report");
922
+ lines.push("# VerisKit Verification Report");
736
923
  if (run.scope?.kind) {
737
924
  lines.push("");
738
925
  lines.push(
@@ -745,6 +932,13 @@ function renderMarkdown(run) {
745
932
  lines.push(
746
933
  `**Node:** ${run.env.node} \xB7 **OS:** ${run.env.os} \xB7 **PM:** ${run.env.pm} \xB7 **CI:** ${run.env.ci}`
747
934
  );
935
+ if (record?.git) {
936
+ const g = record.git;
937
+ const tree = g.dirty ? `tree dirty, ${g.changedFiles} uncommitted file(s)` : "tree clean";
938
+ lines.push(`**Commit:** ${g.commit.slice(0, 7)} (${g.branch}) \xB7 ${tree}`);
939
+ } else if (record) {
940
+ lines.push("**Commit:** no git anchor available");
941
+ }
748
942
  lines.push("");
749
943
  lines.push("## Checks");
750
944
  lines.push("");
@@ -776,9 +970,16 @@ function renderMarkdown(run) {
776
970
  lines.push("```");
777
971
  }
778
972
  }
973
+ if (record) {
974
+ lines.push("");
975
+ lines.push(`**Evidence digest:** \`${record.digest}\``);
976
+ lines.push(
977
+ "_Integrity digest over the canonical record. Detects edits and corruption; it is not forgery-proof on its own. Publish the digest separately or sign it to prove authorship._"
978
+ );
979
+ }
779
980
  lines.push("");
780
981
  lines.push(
781
- "_Generated by Veris. A `Partial` verdict means one or more capabilities did not run \u2014 it is not a pass._"
982
+ "_Generated by VerisKit. A `Partial` verdict means one or more capabilities did not run \u2014 it is not a pass._"
782
983
  );
783
984
  lines.push("");
784
985
  return lines.join("\n");
@@ -805,11 +1006,18 @@ async function runVerify(root, opts = {}) {
805
1006
  const config = await loadConfig(root);
806
1007
  const checks = config?.checks?.length ? config.checks : DEFAULT_CHECKS;
807
1008
  const run = await runChecks(project, checks, root);
808
- const reportRef = await writeReport(root, run.id, renderMarkdown(run));
1009
+ const git = await gitAnchor(root);
1010
+ const logDigests = await digestLogs(run);
1011
+ const record = buildRecord(run, git, logDigests, VERSION);
1012
+ const reportRef = await writeReport(
1013
+ root,
1014
+ run.id,
1015
+ renderMarkdown(run, record)
1016
+ );
809
1017
  run.reportRef = reportRef;
810
1018
  const runDir = await createRunDir(root, run.id);
811
- await writeMetadata(runDir, run);
812
- process.stdout.write(`${renderRun(run)}
1019
+ await writeEvidence(runDir, record);
1020
+ process.stdout.write(`${renderRun(run, record)}
813
1021
  `);
814
1022
  return verdictExitCode(run.verdict, opts);
815
1023
  }
@@ -821,9 +1029,12 @@ var init_verify = __esm({
821
1029
  init_load();
822
1030
  init_orchestrator();
823
1031
  init_verdict();
1032
+ init_record();
824
1033
  init_store();
1034
+ init_changes();
825
1035
  init_markdown();
826
1036
  init_terminal();
1037
+ init_version();
827
1038
  DEFAULT_CHECKS = ["types", "lint", "unit"];
828
1039
  }
829
1040
  });
@@ -833,13 +1044,13 @@ var report_exports = {};
833
1044
  __export(report_exports, {
834
1045
  runReport: () => runReport
835
1046
  });
836
- import { readdirSync, readFileSync as readFileSync2 } from "fs";
1047
+ import { readdirSync as readdirSync2, readFileSync as readFileSync2 } from "fs";
837
1048
  import { join as join6 } from "path";
838
1049
  async function runReport(root) {
839
1050
  const dir = join6(root, ".veris", "reports");
840
1051
  let files;
841
1052
  try {
842
- files = readdirSync(dir).filter((f) => f.endsWith(".md")).sort();
1053
+ files = readdirSync2(dir).filter((f) => f.endsWith(".md")).sort();
843
1054
  } catch {
844
1055
  process.stdout.write("No reports yet. Run `veris verify` first.\n");
845
1056
  return 0;
@@ -912,31 +1123,335 @@ var init_gate = __esm({
912
1123
  }
913
1124
  });
914
1125
 
915
- // src/git/changes.ts
916
- function collect(set, out) {
917
- for (const line of out.split("\n")) {
918
- const f = line.trim();
919
- if (f) set.add(f);
1126
+ // src/project-graph/discover.ts
1127
+ import { readdirSync as readdirSync3, statSync } from "fs";
1128
+ import { join as join7, relative as relative2, sep } from "path";
1129
+ function toPosix(p) {
1130
+ return sep === "/" ? p : p.split(sep).join("/");
1131
+ }
1132
+ function classify(rel) {
1133
+ if (TEST_RE2.test(rel)) return "test";
1134
+ if (CONFIG_RE2.test(rel)) return "config";
1135
+ return "source";
1136
+ }
1137
+ function discoverFiles(root) {
1138
+ const out = [];
1139
+ const walk = (dir) => {
1140
+ let entries;
1141
+ try {
1142
+ entries = readdirSync3(dir);
1143
+ } catch {
1144
+ return;
1145
+ }
1146
+ for (const name of entries) {
1147
+ const abs = join7(dir, name);
1148
+ const rel = toPosix(relative2(root, abs));
1149
+ if (IGNORE.test(rel)) continue;
1150
+ let st;
1151
+ try {
1152
+ st = statSync(abs);
1153
+ } catch {
1154
+ continue;
1155
+ }
1156
+ if (st.isDirectory()) {
1157
+ walk(abs);
1158
+ } else if (CODE_RE.test(rel)) {
1159
+ out.push({ file: rel, kind: classify(rel) });
1160
+ }
1161
+ }
1162
+ };
1163
+ walk(root);
1164
+ return out.sort((a, b) => a.file.localeCompare(b.file));
1165
+ }
1166
+ var IGNORE, CODE_RE, TEST_RE2, CONFIG_RE2;
1167
+ var init_discover = __esm({
1168
+ "src/project-graph/discover.ts"() {
1169
+ "use strict";
1170
+ IGNORE = /(^|\/)(\.git|\.claude|\.veris|\.agentloop|\.agentflight|node_modules|dist|coverage|build|fixtures|__fixtures__)(\/|$)/;
1171
+ CODE_RE = /\.[cm]?[jt]sx?$/;
1172
+ TEST_RE2 = /(\.(test|spec)\.[cm]?[jt]sx?$)|(^|\/)(test|tests|__tests__)\//;
1173
+ CONFIG_RE2 = /(^|\/)([^/]+\.config\.[cm]?[jt]sx?)$/;
1174
+ }
1175
+ });
1176
+
1177
+ // src/project-graph/scanner-resolver.ts
1178
+ import { existsSync as existsSync3, readFileSync as readFileSync3, statSync as statSync2 } from "fs";
1179
+ import { dirname, join as join8, relative as relative3, resolve } from "path";
1180
+ function extractSpecifiers(text) {
1181
+ const specs = [];
1182
+ SPEC_RE.lastIndex = 0;
1183
+ let m;
1184
+ m = SPEC_RE.exec(text);
1185
+ while (m !== null) {
1186
+ const s = m[1] ?? m[2] ?? m[3] ?? m[4];
1187
+ if (s) specs.push(s);
1188
+ m = SPEC_RE.exec(text);
920
1189
  }
1190
+ return specs;
921
1191
  }
922
- async function changedFiles(root, opts = {}) {
923
- const base = opts.base ?? null;
924
- const files = /* @__PURE__ */ new Set();
925
- const diffArgs = base ? ["diff", "--name-only", base] : ["diff", "--name-only", "HEAD"];
926
- const diff = await exec("git", diffArgs, { cwd: root });
927
- if (diff.code === 0) collect(files, diff.stdout);
928
- const untracked = await exec(
929
- "git",
930
- ["ls-files", "--others", "--exclude-standard"],
931
- { cwd: root }
932
- );
933
- if (untracked.code === 0) collect(files, untracked.stdout);
934
- return { files: [...files].sort(), base };
1192
+ function isFile(p) {
1193
+ try {
1194
+ return statSync2(p).isFile();
1195
+ } catch {
1196
+ return false;
1197
+ }
935
1198
  }
936
- var init_changes = __esm({
937
- "src/git/changes.ts"() {
1199
+ function resolveRelative(fromDir, spec) {
1200
+ const base = resolve(fromDir, spec);
1201
+ const candidates = [base];
1202
+ for (const ext of EXTS) candidates.push(base + ext);
1203
+ const extMatch = base.match(/\.[cm]?[jt]sx?$/);
1204
+ if (extMatch) {
1205
+ const noExt = base.slice(0, -extMatch[0].length);
1206
+ for (const ext of EXTS) candidates.push(noExt + ext);
1207
+ }
1208
+ for (const ext of EXTS) candidates.push(join8(base, `index${ext}`));
1209
+ for (const c of candidates) {
1210
+ if (existsSync3(c) && isFile(c)) return c;
1211
+ }
1212
+ return null;
1213
+ }
1214
+ function scannerImports(root, file) {
1215
+ let text;
1216
+ try {
1217
+ text = readFileSync3(join8(root, file), "utf8");
1218
+ } catch {
1219
+ return [];
1220
+ }
1221
+ const dir = dirname(join8(root, file));
1222
+ const out = /* @__PURE__ */ new Set();
1223
+ for (const spec of extractSpecifiers(text)) {
1224
+ if (!spec.startsWith(".")) continue;
1225
+ const resolved = resolveRelative(dir, spec);
1226
+ if (resolved) out.add(toPosix(relative3(root, resolved)));
1227
+ }
1228
+ return [...out];
1229
+ }
1230
+ var EXTS, SPEC_RE;
1231
+ var init_scanner_resolver = __esm({
1232
+ "src/project-graph/scanner-resolver.ts"() {
1233
+ "use strict";
1234
+ init_discover();
1235
+ EXTS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
1236
+ SPEC_RE = /(?:import|export)\b[^'"]*?\bfrom\s*['"]([^'"]+)['"]|(?:^|[^.\w])import\s*['"]([^'"]+)['"]|\bimport\(\s*['"]([^'"]+)['"]\s*\)|\brequire\(\s*['"]([^'"]+)['"]\s*\)/g;
1237
+ }
1238
+ });
1239
+
1240
+ // src/project-graph/ts-resolver.ts
1241
+ import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
1242
+ import { createRequire } from "module";
1243
+ import { join as join9, relative as relative4, sep as sep2 } from "path";
1244
+ function hasClassicApi(mod) {
1245
+ const m = mod;
1246
+ return !!m && typeof m.preProcessFile === "function" && typeof m.resolveModuleName === "function" && typeof m.readConfigFile === "function" && typeof m.parseJsonConfigFileContent === "function" && typeof m.sys === "object" && m.sys !== null;
1247
+ }
1248
+ function loadTypeScript(root) {
1249
+ try {
1250
+ const require2 = createRequire(join9(root, "__veris__.js"));
1251
+ const tsPath = require2.resolve("typescript");
1252
+ const mod = require2(tsPath);
1253
+ return hasClassicApi(mod) ? mod : null;
1254
+ } catch {
1255
+ return null;
1256
+ }
1257
+ }
1258
+ function loadCompilerOptions(tsmod, root) {
1259
+ const configPath = join9(root, "tsconfig.json");
1260
+ const read = tsmod.readConfigFile(configPath, tsmod.sys.readFile);
1261
+ if (read.error || !read.config) return {};
1262
+ const parsed = tsmod.parseJsonConfigFileContent(read.config, tsmod.sys, root);
1263
+ return parsed.options;
1264
+ }
1265
+ function tsImports(tsmod, root, options, file) {
1266
+ let text;
1267
+ try {
1268
+ text = readFileSync4(join9(root, file), "utf8");
1269
+ } catch {
1270
+ return [];
1271
+ }
1272
+ const abs = join9(root, file);
1273
+ const out = /* @__PURE__ */ new Set();
1274
+ try {
1275
+ const pre = tsmod.preProcessFile(text, true, true);
1276
+ for (const imp of pre.importedFiles) {
1277
+ const res = tsmod.resolveModuleName(
1278
+ imp.fileName,
1279
+ abs,
1280
+ options,
1281
+ tsmod.sys
1282
+ );
1283
+ const resolved = res.resolvedModule?.resolvedFileName;
1284
+ if (resolved && !resolved.includes("node_modules") && resolved.startsWith(root + sep2)) {
1285
+ out.add(toPosix(relative4(root, resolved)));
1286
+ }
1287
+ }
1288
+ } catch {
1289
+ }
1290
+ return [...out];
1291
+ }
1292
+ function selectResolver(root) {
1293
+ const tsmod = existsSync4(join9(root, "tsconfig.json")) ? loadTypeScript(root) : null;
1294
+ if (tsmod) {
1295
+ try {
1296
+ const options = loadCompilerOptions(tsmod, root);
1297
+ return {
1298
+ resolver: "typescript",
1299
+ importsOf: (file) => tsImports(tsmod, root, options, file)
1300
+ };
1301
+ } catch {
1302
+ }
1303
+ }
1304
+ return {
1305
+ resolver: "scanner",
1306
+ importsOf: (file) => scannerImports(root, file)
1307
+ };
1308
+ }
1309
+ var init_ts_resolver = __esm({
1310
+ "src/project-graph/ts-resolver.ts"() {
1311
+ "use strict";
1312
+ init_discover();
1313
+ init_scanner_resolver();
1314
+ }
1315
+ });
1316
+
1317
+ // src/project-graph/graph.ts
1318
+ var graph_exports = {};
1319
+ __export(graph_exports, {
1320
+ buildGraph: () => buildGraph
1321
+ });
1322
+ async function buildGraph(project) {
1323
+ const root = project.root;
1324
+ const files = discoverFiles(root);
1325
+ const known = new Set(files.map((f) => f.file));
1326
+ const { resolver, importsOf } = selectResolver(root);
1327
+ const nodes = {};
1328
+ for (const f of files) {
1329
+ nodes[f.file] = { file: f.file, kind: f.kind, imports: [], importedBy: [] };
1330
+ }
1331
+ for (const f of files) {
1332
+ const imps = importsOf(f.file).filter((i) => known.has(i) && i !== f.file);
1333
+ nodes[f.file].imports = imps;
1334
+ for (const i of imps) nodes[i]?.importedBy.push(f.file);
1335
+ }
1336
+ return {
1337
+ root,
1338
+ resolver,
1339
+ nodes,
1340
+ sourceFiles: files.filter((f) => f.kind === "source").map((f) => f.file),
1341
+ testFiles: files.filter((f) => f.kind === "test").map((f) => f.file)
1342
+ };
1343
+ }
1344
+ var init_graph = __esm({
1345
+ "src/project-graph/graph.ts"() {
1346
+ "use strict";
1347
+ init_discover();
1348
+ init_ts_resolver();
1349
+ }
1350
+ });
1351
+
1352
+ // src/project-graph/analyze.ts
1353
+ function transitiveDependents(graph, file) {
1354
+ const seen = /* @__PURE__ */ new Set();
1355
+ const stack = [...graph.nodes[file]?.importedBy ?? []];
1356
+ while (stack.length) {
1357
+ const f = stack.pop();
1358
+ if (!f || seen.has(f)) continue;
1359
+ seen.add(f);
1360
+ for (const d of graph.nodes[f]?.importedBy ?? []) stack.push(d);
1361
+ }
1362
+ return seen;
1363
+ }
1364
+ function reachableFromTests(graph) {
1365
+ const seen = /* @__PURE__ */ new Set();
1366
+ const stack = [...graph.testFiles];
1367
+ while (stack.length) {
1368
+ const f = stack.pop();
1369
+ if (!f || seen.has(f)) continue;
1370
+ seen.add(f);
1371
+ for (const i of graph.nodes[f]?.imports ?? []) stack.push(i);
1372
+ }
1373
+ return seen;
1374
+ }
1375
+ function analyze(graph, changed = []) {
1376
+ const blastRadius = {};
1377
+ for (const file of Object.keys(graph.nodes)) {
1378
+ blastRadius[file] = transitiveDependents(graph, file).size;
1379
+ }
1380
+ const reached = reachableFromTests(graph);
1381
+ const byBlast = (a, b) => (blastRadius[b] ?? 0) - (blastRadius[a] ?? 0);
1382
+ const untested = graph.sourceFiles.filter((f) => !reached.has(f)).sort(byBlast);
1383
+ const changedSet = new Set(changed);
1384
+ const untestedSet = new Set(untested);
1385
+ const risky = graph.sourceFiles.filter(
1386
+ (f) => (blastRadius[f] ?? 0) > 0 && (untestedSet.has(f) || changedSet.has(f))
1387
+ ).sort(byBlast);
1388
+ return { untested, blastRadius, risky };
1389
+ }
1390
+ var init_analyze = __esm({
1391
+ "src/project-graph/analyze.ts"() {
938
1392
  "use strict";
939
- init_exec();
1393
+ }
1394
+ });
1395
+
1396
+ // src/affected/select.ts
1397
+ var select_exports = {};
1398
+ __export(select_exports, {
1399
+ selectAffectedTests: () => selectAffectedTests
1400
+ });
1401
+ function selectAffectedTests(graph, changed) {
1402
+ if (graph.resolver !== "typescript") {
1403
+ return {
1404
+ mode: "full",
1405
+ testFiles: [],
1406
+ reason: "scanner graph can miss aliased/subpath imports \u2014 running the full suite"
1407
+ };
1408
+ }
1409
+ for (const f of changed) {
1410
+ if (GLOBAL_RE.test(f)) {
1411
+ return {
1412
+ mode: "full",
1413
+ testFiles: [],
1414
+ reason: `global/config change (${f})`
1415
+ };
1416
+ }
1417
+ if (!(f in graph.nodes)) {
1418
+ return {
1419
+ mode: "full",
1420
+ testFiles: [],
1421
+ reason: `unresolved changed file (${f})`
1422
+ };
1423
+ }
1424
+ }
1425
+ const tests = /* @__PURE__ */ new Set();
1426
+ for (const f of changed) {
1427
+ if (graph.nodes[f]?.kind === "test") tests.add(f);
1428
+ for (const dep of transitiveDependents(graph, f)) {
1429
+ if (graph.nodes[dep]?.kind === "test") tests.add(dep);
1430
+ }
1431
+ }
1432
+ if (tests.size === 0) {
1433
+ return {
1434
+ mode: "full",
1435
+ testFiles: [],
1436
+ reason: "no tests reach the changed files"
1437
+ };
1438
+ }
1439
+ const selected = [...tests].sort();
1440
+ if (selected.some((t) => t.startsWith("-"))) {
1441
+ return {
1442
+ mode: "full",
1443
+ testFiles: [],
1444
+ reason: "a reaching test path starts with '-' (unsafe as a CLI argument)"
1445
+ };
1446
+ }
1447
+ return { mode: "graph", testFiles: selected, reason: "" };
1448
+ }
1449
+ var GLOBAL_RE;
1450
+ var init_select = __esm({
1451
+ "src/affected/select.ts"() {
1452
+ "use strict";
1453
+ init_analyze();
1454
+ GLOBAL_RE = /(^|\/)(tsconfig[^/]*\.json|package\.json|biome\.jsonc?|\.eslintrc[^/]*|eslint\.config\.[^/]+|veris\.config\.[^/]+|[^/]+\.config\.[cm]?[jt]sx?|[^/]+\.setup\.[cm]?[jt]sx?)$/;
940
1455
  }
941
1456
  });
942
1457
 
@@ -956,7 +1471,25 @@ async function runAffected(root, opts = {}) {
956
1471
  );
957
1472
  return 0;
958
1473
  }
959
- const run = await runChecks(project, plan.checks, root);
1474
+ let targetFiles;
1475
+ let narrowedNote = "";
1476
+ const unitRunner = project.capabilities.find((c) => c.id === "unit")?.runner;
1477
+ const canNarrow = unitRunner === "vitest" || unitRunner === "jest";
1478
+ if (plan.checks.includes("unit") && canNarrow) {
1479
+ const { buildGraph: buildGraph2 } = await Promise.resolve().then(() => (init_graph(), graph_exports));
1480
+ const { selectAffectedTests: selectAffectedTests2 } = await Promise.resolve().then(() => (init_select(), select_exports));
1481
+ const graph = await buildGraph2(project);
1482
+ const sel = selectAffectedTests2(graph, files);
1483
+ if (sel.mode === "graph") {
1484
+ targetFiles = { unit: sel.testFiles };
1485
+ narrowedNote = `unit narrowed to ${sel.testFiles.length} of ${graph.testFiles.length} test file(s) via ${graph.resolver} graph`;
1486
+ } else {
1487
+ narrowedNote = `unit ran in full \u2014 ${sel.reason}`;
1488
+ }
1489
+ } else if (plan.checks.includes("unit") && unitRunner) {
1490
+ narrowedNote = `unit ran in full \u2014 the ${unitRunner} runner does not support file filtering`;
1491
+ }
1492
+ const run = await runChecks(project, plan.checks, root, { targetFiles });
960
1493
  run.scope = { kind: "affected", changedCount: files.length };
961
1494
  const affected = new Set(plan.checks);
962
1495
  for (const cap of project.capabilities) {
@@ -969,11 +1502,20 @@ async function runAffected(root, opts = {}) {
969
1502
  });
970
1503
  }
971
1504
  }
972
- const reportRef = await writeReport(root, run.id, renderMarkdown(run));
1505
+ const git = await gitAnchor(root);
1506
+ const logDigests = await digestLogs(run);
1507
+ const record = buildRecord(run, git, logDigests, VERSION);
1508
+ const reportRef = await writeReport(
1509
+ root,
1510
+ run.id,
1511
+ renderMarkdown(run, record)
1512
+ );
973
1513
  run.reportRef = reportRef;
974
1514
  const runDir = await createRunDir(root, run.id);
975
- await writeMetadata(runDir, run);
976
- process.stdout.write(`${renderRun(run)}
1515
+ await writeEvidence(runDir, record);
1516
+ process.stdout.write(`${renderRun(run, record)}
1517
+ `);
1518
+ if (narrowedNote) process.stdout.write(`${narrowedNote}
977
1519
  `);
978
1520
  return verdictExitCode(run.verdict, opts);
979
1521
  }
@@ -984,23 +1526,25 @@ var init_affected = __esm({
984
1526
  init_detect();
985
1527
  init_orchestrator();
986
1528
  init_verdict();
1529
+ init_record();
987
1530
  init_store();
988
1531
  init_changes();
989
1532
  init_markdown();
990
1533
  init_terminal();
1534
+ init_version();
991
1535
  }
992
1536
  });
993
1537
 
994
1538
  // src/watch/watcher.ts
995
1539
  import {
996
1540
  watch as fsWatch,
997
- readdirSync as readdirSync2,
998
- statSync
1541
+ readdirSync as readdirSync4,
1542
+ statSync as statSync3
999
1543
  } from "fs";
1000
- import { basename, join as join7, relative as relative2 } from "path";
1544
+ import { basename as basename2, join as join10, relative as relative5 } from "path";
1001
1545
  function watch(root, opts, onBatch) {
1002
1546
  const debounceMs = opts.debounceMs ?? 150;
1003
- const rootBase = basename(root);
1547
+ const rootBase = basename2(root);
1004
1548
  let pending = /* @__PURE__ */ new Set();
1005
1549
  let timer = null;
1006
1550
  const flush = () => {
@@ -1010,7 +1554,7 @@ function watch(root, opts, onBatch) {
1010
1554
  if (batch.length) onBatch(batch);
1011
1555
  };
1012
1556
  const schedule = (rel) => {
1013
- if (!rel || rel === rootBase || IGNORE.test(rel)) return;
1557
+ if (!rel || rel === rootBase || IGNORE2.test(rel)) return;
1014
1558
  pending.add(rel);
1015
1559
  if (timer) clearTimeout(timer);
1016
1560
  timer = setTimeout(flush, debounceMs);
@@ -1051,17 +1595,17 @@ function scanMtimes(root) {
1051
1595
  const walk = (dir) => {
1052
1596
  let entries;
1053
1597
  try {
1054
- entries = readdirSync2(dir);
1598
+ entries = readdirSync4(dir);
1055
1599
  } catch {
1056
1600
  return;
1057
1601
  }
1058
1602
  for (const name of entries) {
1059
- const abs = join7(dir, name);
1060
- const rel = relative2(root, abs);
1061
- if (IGNORE.test(rel)) continue;
1603
+ const abs = join10(dir, name);
1604
+ const rel = relative5(root, abs);
1605
+ if (IGNORE2.test(rel)) continue;
1062
1606
  let st;
1063
1607
  try {
1064
- st = statSync(abs);
1608
+ st = statSync3(abs);
1065
1609
  } catch {
1066
1610
  continue;
1067
1611
  }
@@ -1072,11 +1616,11 @@ function scanMtimes(root) {
1072
1616
  walk(root);
1073
1617
  return out;
1074
1618
  }
1075
- var IGNORE;
1619
+ var IGNORE2;
1076
1620
  var init_watcher = __esm({
1077
1621
  "src/watch/watcher.ts"() {
1078
1622
  "use strict";
1079
- IGNORE = /(^|\/)(\.git|\.veris|\.agentloop|\.agentflight|node_modules|dist)(\/|$)/;
1623
+ IGNORE2 = /(^|\/)(\.git|\.veris|\.agentloop|\.agentflight|node_modules|dist)(\/|$)/;
1080
1624
  }
1081
1625
  });
1082
1626
 
@@ -1122,9 +1666,29 @@ async function runWatch(root, opts = {}) {
1122
1666
  const project = await detectProject(root);
1123
1667
  const { files } = await changedFiles(root);
1124
1668
  const checks = initial ? availableIds(project) : affectedChecks(files, project).checks;
1669
+ let targetFiles;
1670
+ let narrowedNote = "";
1671
+ const unitRunner = project.capabilities.find(
1672
+ (c) => c.id === "unit"
1673
+ )?.runner;
1674
+ const canNarrow = unitRunner === "vitest" || unitRunner === "jest";
1675
+ if (!initial && checks.includes("unit") && canNarrow) {
1676
+ const { buildGraph: buildGraph2 } = await Promise.resolve().then(() => (init_graph(), graph_exports));
1677
+ const { selectAffectedTests: selectAffectedTests2 } = await Promise.resolve().then(() => (init_select(), select_exports));
1678
+ const graph = await buildGraph2(project);
1679
+ const sel = selectAffectedTests2(graph, files);
1680
+ if (sel.mode === "graph") {
1681
+ targetFiles = { unit: sel.testFiles };
1682
+ narrowedNote = `unit narrowed to ${sel.testFiles.length} of ${graph.testFiles.length} test file(s) via ${graph.resolver} graph`;
1683
+ } else {
1684
+ narrowedNote = `unit ran in full \u2014 ${sel.reason}`;
1685
+ }
1686
+ } else if (!initial && checks.includes("unit") && unitRunner) {
1687
+ narrowedNote = `unit ran in full \u2014 the ${unitRunner} runner does not support file filtering`;
1688
+ }
1125
1689
  let fresh = [];
1126
1690
  if (checks.length) {
1127
- const r = await runChecks(project, checks, root);
1691
+ const r = await runChecks(project, checks, root, { targetFiles });
1128
1692
  fresh = r.results;
1129
1693
  for (const result of fresh) cache.set(result.checkId, { ...result });
1130
1694
  }
@@ -1149,6 +1713,8 @@ async function runWatch(root, opts = {}) {
1149
1713
  scope: { kind: "watch", changedCount: files.length }
1150
1714
  };
1151
1715
  process.stdout.write(`${renderRun(run)}
1716
+ `);
1717
+ if (narrowedNote) process.stdout.write(`${narrowedNote}
1152
1718
  `);
1153
1719
  } finally {
1154
1720
  running = false;
@@ -1156,7 +1722,7 @@ async function runWatch(root, opts = {}) {
1156
1722
  };
1157
1723
  process.stdout.write("veris watch \u2014 press Ctrl-C to stop\n");
1158
1724
  await tick(true);
1159
- return await new Promise((resolve) => {
1725
+ return await new Promise((resolve2) => {
1160
1726
  let stop;
1161
1727
  try {
1162
1728
  stop = watch(root, { poll: opts.poll }, () => {
@@ -1167,13 +1733,13 @@ async function runWatch(root, opts = {}) {
1167
1733
  `veris: ${err instanceof Error ? err.message : String(err)}
1168
1734
  `
1169
1735
  );
1170
- resolve(1);
1736
+ resolve2(1);
1171
1737
  return;
1172
1738
  }
1173
1739
  process.on("SIGINT", () => {
1174
1740
  stop();
1175
1741
  process.stdout.write("\nStopped.\n");
1176
- resolve(0);
1742
+ resolve2(0);
1177
1743
  });
1178
1744
  });
1179
1745
  }
@@ -1190,21 +1756,364 @@ var init_watch = __esm({
1190
1756
  }
1191
1757
  });
1192
1758
 
1759
+ // src/cli/commands/scan.ts
1760
+ var scan_exports = {};
1761
+ __export(scan_exports, {
1762
+ renderScan: () => renderScan,
1763
+ runScan: () => runScan
1764
+ });
1765
+ import { writeFile as writeFile3 } from "fs/promises";
1766
+ import { join as join11 } from "path";
1767
+ import pc3 from "picocolors";
1768
+ function renderScan(graph, analysis) {
1769
+ const plain = isPlain();
1770
+ const bold = (s) => plain ? s : pc3.bold(s);
1771
+ const dim = (s) => plain ? s : pc3.dim(s);
1772
+ const warn = (s) => plain ? s : pc3.yellow(s);
1773
+ const lines = [];
1774
+ lines.push(bold("VerisKit \u2014 scan"));
1775
+ lines.push("");
1776
+ lines.push(
1777
+ `Resolver ${graph.resolver}${graph.resolver === "scanner" ? dim(" (no TypeScript found \u2014 relative imports only)") : ""}`
1778
+ );
1779
+ lines.push(
1780
+ `Modules ${Object.keys(graph.nodes).length} \xB7 Source ${graph.sourceFiles.length} \xB7 Tests ${graph.testFiles.length}`
1781
+ );
1782
+ lines.push("");
1783
+ lines.push("Untested (top by impact)");
1784
+ const top = analysis.untested.slice(0, 10);
1785
+ if (top.length === 0)
1786
+ lines.push(dim(" none \u2014 every source file is reached by a test"));
1787
+ for (const f of top) {
1788
+ lines.push(
1789
+ ` ${warn("!")} ${f} ${dim(`${analysis.blastRadius[f] ?? 0} dependents`)}`
1790
+ );
1791
+ }
1792
+ return lines.join("\n");
1793
+ }
1794
+ async function runScan(root) {
1795
+ const project = await detectProject(root);
1796
+ const graph = await buildGraph(project);
1797
+ const analysis = analyze(graph);
1798
+ const dir = join11(root, ".veris");
1799
+ await ensureDir(dir);
1800
+ await writeFile3(
1801
+ join11(dir, "graph.json"),
1802
+ JSON.stringify({ resolver: graph.resolver, nodes: graph.nodes }, null, 2),
1803
+ "utf8"
1804
+ );
1805
+ process.stdout.write(`${renderScan(graph, analysis)}
1806
+ `);
1807
+ process.stdout.write(`
1808
+ Graph ${join11(".veris", "graph.json")}
1809
+ `);
1810
+ return 0;
1811
+ }
1812
+ var init_scan = __esm({
1813
+ "src/cli/commands/scan.ts"() {
1814
+ "use strict";
1815
+ init_detect();
1816
+ init_analyze();
1817
+ init_graph();
1818
+ init_fs_safe();
1819
+ init_tty();
1820
+ }
1821
+ });
1822
+
1823
+ // src/cli/commands/plan.ts
1824
+ var plan_exports = {};
1825
+ __export(plan_exports, {
1826
+ renderPlan: () => renderPlan,
1827
+ runPlan: () => runPlan
1828
+ });
1829
+ import pc4 from "picocolors";
1830
+ function renderPlan(project, graph, analysis, changed) {
1831
+ const plain = isPlain();
1832
+ const bold = (s) => plain ? s : pc4.bold(s);
1833
+ const dim = (s) => plain ? s : pc4.dim(s);
1834
+ const warn = (s) => plain ? s : pc4.yellow(s);
1835
+ const lines = [];
1836
+ lines.push(bold("VerisKit \u2014 plan"));
1837
+ lines.push(dim(`(graph via ${graph.resolver})`));
1838
+ lines.push("");
1839
+ lines.push("Test these first (high impact, untested)");
1840
+ const top = analysis.untested.slice(0, 5);
1841
+ if (top.length === 0)
1842
+ lines.push(
1843
+ dim(
1844
+ " none \u2014 untested files have no dependents or all files are covered"
1845
+ )
1846
+ );
1847
+ top.forEach((f, i) => {
1848
+ lines.push(
1849
+ ` ${i + 1}. ${f} ${dim(`(${analysis.blastRadius[f] ?? 0} dependents, no test reaches it)`)}`
1850
+ );
1851
+ });
1852
+ lines.push("");
1853
+ lines.push("Verification setup");
1854
+ const weak = project.capabilities.filter(
1855
+ (c) => !c.available && c.id !== "browser"
1856
+ );
1857
+ if (weak.length === 0)
1858
+ lines.push(dim(" \u2713 types, lint, and unit are all configured"));
1859
+ for (const c of weak)
1860
+ lines.push(
1861
+ ` ${warn("\u26A0")} ${c.id} capability unavailable \u2014 ${c.reason ?? "not configured"}`
1862
+ );
1863
+ if (changed.length) {
1864
+ lines.push("");
1865
+ lines.push(`Risky changes (${changed.length} changed file(s))`);
1866
+ if (analysis.risky.length === 0) lines.push(dim(" none flagged"));
1867
+ for (const f of analysis.risky.slice(0, 8)) {
1868
+ const untested = analysis.untested.includes(f) ? ", untested" : "";
1869
+ lines.push(
1870
+ ` ${warn("!")} ${f} ${dim(`${analysis.blastRadius[f] ?? 0} dependents${untested}`)}`
1871
+ );
1872
+ }
1873
+ }
1874
+ lines.push("");
1875
+ lines.push(
1876
+ dim("Next: `veris affected` runs only the tests that reach your changes.")
1877
+ );
1878
+ return lines.join("\n");
1879
+ }
1880
+ async function runPlan(root, opts = {}) {
1881
+ const project = await detectProject(root);
1882
+ const graph = await buildGraph(project);
1883
+ const { files } = await changedFiles(root, { base: opts.base });
1884
+ const analysis = analyze(graph, files);
1885
+ process.stdout.write(`${renderPlan(project, graph, analysis, files)}
1886
+ `);
1887
+ return 0;
1888
+ }
1889
+ var init_plan = __esm({
1890
+ "src/cli/commands/plan.ts"() {
1891
+ "use strict";
1892
+ init_detect();
1893
+ init_changes();
1894
+ init_analyze();
1895
+ init_graph();
1896
+ init_tty();
1897
+ }
1898
+ });
1899
+
1900
+ // src/evidence/verify-evidence.ts
1901
+ import { readFile as readFile3 } from "fs/promises";
1902
+ function verifyRecord(record) {
1903
+ const recomputed = computeDigest(
1904
+ record
1905
+ );
1906
+ const ok = recomputed === record.digest;
1907
+ const checks = [
1908
+ {
1909
+ name: "record digest",
1910
+ ok,
1911
+ detail: ok ? "matches the canonical record" : `recomputed ${recomputed}, record claims ${record.digest}`
1912
+ }
1913
+ ];
1914
+ return { ok, kind: "record", record, checks };
1915
+ }
1916
+ async function verifyEvidenceFile(path) {
1917
+ const parsed = JSON.parse(await readFile3(path, "utf8"));
1918
+ if (parsed?.schema === "veriskit/bundle@1") {
1919
+ const { verifyBundle: verifyBundle2 } = await Promise.resolve().then(() => (init_bundle(), bundle_exports));
1920
+ return verifyBundle2(parsed);
1921
+ }
1922
+ return verifyRecord(parsed);
1923
+ }
1924
+ var init_verify_evidence = __esm({
1925
+ "src/evidence/verify-evidence.ts"() {
1926
+ "use strict";
1927
+ init_record();
1928
+ }
1929
+ });
1930
+
1931
+ // src/evidence/bundle.ts
1932
+ var bundle_exports = {};
1933
+ __export(bundle_exports, {
1934
+ BUNDLE_SCHEMA: () => BUNDLE_SCHEMA,
1935
+ buildBundle: () => buildBundle,
1936
+ verifyBundle: () => verifyBundle
1937
+ });
1938
+ function buildBundle(record, report, logs) {
1939
+ const manifest = {
1940
+ record: record.digest,
1941
+ report: sha256(report),
1942
+ logs: Object.fromEntries(
1943
+ Object.entries(logs).map(([k, v]) => [k, sha256(v)])
1944
+ )
1945
+ };
1946
+ const base = { schema: BUNDLE_SCHEMA, record, report, logs, manifest };
1947
+ return { ...base, bundleDigest: sha256(canonicalize(base)) };
1948
+ }
1949
+ function verifyBundle(bundle) {
1950
+ const checks = [];
1951
+ const recordResult = verifyRecord(bundle.record);
1952
+ checks.push(...recordResult.checks);
1953
+ const reportDigest = sha256(bundle.report);
1954
+ checks.push({
1955
+ name: "report",
1956
+ ok: reportDigest === bundle.manifest.report,
1957
+ detail: reportDigest === bundle.manifest.report ? "matches manifest" : "mismatch"
1958
+ });
1959
+ for (const [id, body] of Object.entries(bundle.logs)) {
1960
+ const d = sha256(body);
1961
+ checks.push({
1962
+ name: `log:${id}`,
1963
+ ok: d === bundle.manifest.logs[id],
1964
+ detail: d === bundle.manifest.logs[id] ? "matches manifest" : "mismatch"
1965
+ });
1966
+ }
1967
+ const { bundleDigest: _omit, ...rest } = bundle;
1968
+ const recomputed = sha256(canonicalize(rest));
1969
+ checks.push({
1970
+ name: "bundle digest",
1971
+ ok: recomputed === bundle.bundleDigest,
1972
+ detail: recomputed === bundle.bundleDigest ? "matches" : "mismatch"
1973
+ });
1974
+ return {
1975
+ ok: checks.every((c) => c.ok),
1976
+ kind: "bundle",
1977
+ record: bundle.record,
1978
+ checks
1979
+ };
1980
+ }
1981
+ var BUNDLE_SCHEMA;
1982
+ var init_bundle = __esm({
1983
+ "src/evidence/bundle.ts"() {
1984
+ "use strict";
1985
+ init_record();
1986
+ init_verify_evidence();
1987
+ BUNDLE_SCHEMA = "veriskit/bundle@1";
1988
+ }
1989
+ });
1990
+
1991
+ // src/cli/commands/evidence.ts
1992
+ var evidence_exports = {};
1993
+ __export(evidence_exports, {
1994
+ runEvidenceBundle: () => runEvidenceBundle,
1995
+ runEvidenceShow: () => runEvidenceShow,
1996
+ runEvidenceVerify: () => runEvidenceVerify
1997
+ });
1998
+ import { readFileSync as readFileSync5 } from "fs";
1999
+ import { writeFile as writeFile4 } from "fs/promises";
2000
+ import { basename as basename3, join as join12 } from "path";
2001
+ import pc5 from "picocolors";
2002
+ async function runEvidenceVerify(path) {
2003
+ let result;
2004
+ try {
2005
+ result = await verifyEvidenceFile(path);
2006
+ } catch (err) {
2007
+ const msg = err instanceof Error ? err.message : String(err);
2008
+ process.stderr.write(`veris: cannot read evidence at ${path}: ${msg}
2009
+ `);
2010
+ return 1;
2011
+ }
2012
+ const plain = isPlain();
2013
+ const mark = (ok) => plain ? ok ? "ok" : "FAIL" : ok ? pc5.green("\u2713") : pc5.red("\u2717");
2014
+ for (const check of result.checks) {
2015
+ process.stdout.write(
2016
+ ` ${mark(check.ok)} ${check.name}: ${check.detail}
2017
+ `
2018
+ );
2019
+ }
2020
+ const g = result.record.git;
2021
+ const anchor = g ? `commit ${g.commit.slice(0, 7)} \xB7 ${g.dirty ? "tree dirty" : "tree clean"}` : "no git anchor";
2022
+ process.stdout.write(
2023
+ `
2024
+ ${result.ok ? "digest OK" : "TAMPERED"} \xB7 verdict ${result.record.verdict.state} \xB7 ${anchor}
2025
+ `
2026
+ );
2027
+ process.stdout.write(`
2028
+ ${HONESTY}
2029
+ `);
2030
+ return result.ok ? 0 : 1;
2031
+ }
2032
+ async function runEvidenceBundle(root, opts = {}) {
2033
+ const runDir = latestRunDir(root);
2034
+ if (!runDir) {
2035
+ process.stdout.write("No evidence yet. Run `veris verify` first.\n");
2036
+ return 1;
2037
+ }
2038
+ let record;
2039
+ try {
2040
+ record = JSON.parse(
2041
+ readFileSync5(join12(runDir, "evidence.json"), "utf8")
2042
+ );
2043
+ } catch {
2044
+ process.stderr.write(`veris: no evidence.json in ${runDir}
2045
+ `);
2046
+ return 1;
2047
+ }
2048
+ let report = "";
2049
+ try {
2050
+ report = readFileSync5(
2051
+ join12(root, ".veris", "reports", `verify-${record.id}.md`),
2052
+ "utf8"
2053
+ );
2054
+ } catch {
2055
+ }
2056
+ const logIds = record.checks.filter((c) => c.logDigest).map((c) => c.id);
2057
+ const logs = await readRunLogs(runDir, logIds);
2058
+ const bundle = buildBundle(record, report, logs);
2059
+ const outDir = await ensureEvidenceDir(root);
2060
+ const out = opts.out ?? join12(outDir, `${record.id}.bundle.json`);
2061
+ await writeFile4(out, `${JSON.stringify(bundle, null, 2)}
2062
+ `, "utf8");
2063
+ process.stdout.write(`Wrote portable evidence bundle: ${out}
2064
+ `);
2065
+ return 0;
2066
+ }
2067
+ async function runEvidenceShow(root, path) {
2068
+ const target = path ?? (() => {
2069
+ const dir = latestRunDir(root);
2070
+ return dir ? join12(dir, "evidence.json") : null;
2071
+ })();
2072
+ if (!target) {
2073
+ process.stdout.write("No evidence yet. Run `veris verify` first.\n");
2074
+ return 0;
2075
+ }
2076
+ let record;
2077
+ try {
2078
+ record = JSON.parse(readFileSync5(target, "utf8"));
2079
+ } catch {
2080
+ process.stderr.write(`veris: cannot read evidence at ${target}
2081
+ `);
2082
+ return 1;
2083
+ }
2084
+ const g = record.git;
2085
+ process.stdout.write(
2086
+ [
2087
+ `Evidence ${basename3(target)}`,
2088
+ `Verdict ${record.verdict.state}`,
2089
+ `Project ${record.project.name}`,
2090
+ `Scope ${record.scope.kind} (${record.scope.changedCount} changed)`,
2091
+ `Commit ${g ? `${g.commit.slice(0, 7)} (${g.branch}) ${g.dirty ? "\xB7 tree dirty" : "\xB7 tree clean"}` : "no git anchor"}`,
2092
+ `Checks ${record.checks.map((c) => `${c.id}:${c.status}`).join(", ")}`,
2093
+ `Digest ${record.digest}`,
2094
+ ""
2095
+ ].join("\n")
2096
+ );
2097
+ return 0;
2098
+ }
2099
+ var HONESTY;
2100
+ var init_evidence = __esm({
2101
+ "src/cli/commands/evidence.ts"() {
2102
+ "use strict";
2103
+ init_bundle();
2104
+ init_store();
2105
+ init_verify_evidence();
2106
+ init_tty();
2107
+ HONESTY = "An integrity digest confirms the record was not edited or corrupted since it was written.\nIt is not forgery-proof on its own: publish the digest separately (CI log, PR) or sign it (planned) to prove authorship.";
2108
+ }
2109
+ });
2110
+
1193
2111
  // src/cli/index.ts
2112
+ init_version();
1194
2113
  import { realpathSync } from "fs";
1195
2114
  import { argv } from "process";
1196
2115
  import { pathToFileURL } from "url";
1197
2116
  import cac from "cac";
1198
-
1199
- // src/version.ts
1200
- import { readFileSync } from "fs";
1201
- import { fileURLToPath } from "url";
1202
- var pkgUrl = new URL("../package.json", import.meta.url);
1203
- var VERSION = JSON.parse(
1204
- readFileSync(fileURLToPath(pkgUrl), "utf8")
1205
- ).version;
1206
-
1207
- // src/cli/index.ts
1208
2117
  function buildCli() {
1209
2118
  const cli = cac("veris");
1210
2119
  cli.version(VERSION);
@@ -1242,6 +2151,45 @@ function buildCli() {
1242
2151
  const { runWatch: runWatch2 } = await Promise.resolve().then(() => (init_watch(), watch_exports));
1243
2152
  process.exitCode = await runWatch2(process.cwd(), { poll: opts.poll });
1244
2153
  });
2154
+ cli.command("scan", "Map the import graph and untested areas (read-only)").action(async () => {
2155
+ const { runScan: runScan2 } = await Promise.resolve().then(() => (init_scan(), scan_exports));
2156
+ process.exitCode = await runScan2(process.cwd());
2157
+ });
2158
+ cli.command(
2159
+ "plan",
2160
+ "Recommend what to test, from the import graph (read-only)"
2161
+ ).option("--base <ref>", "Also factor in changes vs a git ref").action(async (opts) => {
2162
+ const { runPlan: runPlan2 } = await Promise.resolve().then(() => (init_plan(), plan_exports));
2163
+ process.exitCode = await runPlan2(process.cwd(), { base: opts.base });
2164
+ });
2165
+ cli.command(
2166
+ "evidence <action> [file]",
2167
+ "Evidence tools: verify <file> | bundle | show [file]"
2168
+ ).option("--out <file>", "For bundle: write to a specific path").action(
2169
+ async (action, file, opts) => {
2170
+ const mod = await Promise.resolve().then(() => (init_evidence(), evidence_exports));
2171
+ if (action === "verify") {
2172
+ if (!file) {
2173
+ process.stderr.write("veris: evidence verify needs a <file>\n");
2174
+ process.exitCode = 1;
2175
+ return;
2176
+ }
2177
+ process.exitCode = await mod.runEvidenceVerify(file);
2178
+ } else if (action === "bundle") {
2179
+ process.exitCode = await mod.runEvidenceBundle(process.cwd(), {
2180
+ out: opts.out
2181
+ });
2182
+ } else if (action === "show") {
2183
+ process.exitCode = await mod.runEvidenceShow(process.cwd(), file);
2184
+ } else {
2185
+ process.stderr.write(
2186
+ `veris: unknown evidence action '${action}' (use verify | bundle | show)
2187
+ `
2188
+ );
2189
+ process.exitCode = 1;
2190
+ }
2191
+ }
2192
+ );
1245
2193
  return { raw: cli, version: VERSION };
1246
2194
  }
1247
2195
  async function main(argv2) {