veriskit 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.0 — 2026-07-13
4
+
5
+ ### Added
6
+ - Publish to GitHub. `veris verify --github` posts and updates one sticky PR comment with the verdict and report, and creates a Check Run (verified passes, failed fails, partial is neutral). Token read from `GITHUB_TOKEN`; publishing never changes the verdict or exit code. `veris badge` writes a shields.io endpoint JSON. GitHub API over built-in fetch, no new dependency.
7
+ - Browser tests. A real Playwright runner, opt-in with `veris verify --browser` (or a `browser` entry in `.veris/config.json`). Detected Playwright now shows as an available capability.
8
+ - `veris log` lists past runs from the stored evidence records, and `veris log --flaky` flags checks that both passed and failed across recent runs. Local per-machine history.
9
+
3
10
  ## 0.4.1 — 2026-07-10
4
11
 
5
12
  ### Added
package/README.md CHANGED
@@ -237,6 +237,64 @@ VerisKit says what it cannot do as plainly as what it can:
237
237
  - **Scanner fallback on plain-JS or TS 7.x-native projects.** The accurate resolver needs the classic TypeScript compiler API. Without it you get the labeled, relative-imports-only graph described above, and no dependency is added to paper over the gap.
238
238
  - **No keyless or identity-bound signing.** Evidence can be signed with a local Ed25519 key (see Signing), but sigstore-style keyless signing that ties a signature to an identity is not built yet.
239
239
 
240
+ ## Publish to a pull request
241
+
242
+ In CI, surface the verdict where reviewers look. Opt in with `--github`; VerisKit
243
+ reads `GITHUB_TOKEN` from the environment and never stores it.
244
+
245
+ ```bash
246
+ veris verify --github # posts/updates a sticky PR comment + a Check Run
247
+ ```
248
+
249
+ It edits one comment on re-runs (no spam) and creates a Check Run whose
250
+ conclusion follows the verdict (verified passes, failed fails, partial is
251
+ neutral). Publishing is a side channel: if there is no token or PR, VerisKit
252
+ prints a notice and the exit code still reflects the verdict, and a GitHub API
253
+ error is reported but never changes the result.
254
+
255
+ The workflow needs permission to write them:
256
+
257
+ ```yaml
258
+ permissions:
259
+ contents: read
260
+ pull-requests: write
261
+ checks: write
262
+ ```
263
+
264
+ For a README badge, write a shields.io endpoint file:
265
+
266
+ ```bash
267
+ veris badge # writes .veris/badge.json
268
+ ```
269
+
270
+ ```markdown
271
+ ![VerisKit](https://img.shields.io/endpoint?url=<raw-url-to>/.veris/badge.json)
272
+ ```
273
+
274
+ ## Browser tests
275
+
276
+ VerisKit can run your Playwright suite as part of the verdict. It is opt-in, so
277
+ a normal `veris verify` stays fast:
278
+
279
+ ```bash
280
+ veris verify --browser # also runs `playwright test`, folded into the verdict
281
+ ```
282
+
283
+ When Playwright is detected, `veris doctor` lists `browser` as available. You can
284
+ also add `browser` to the `checks` array in `.veris/config.json` to run it every
285
+ time.
286
+
287
+ ## History
288
+
289
+ Every run leaves an evidence record, so VerisKit can show you a trend:
290
+
291
+ ```bash
292
+ veris log # past runs, newest first
293
+ veris log --flaky # checks that both passed and failed across recent runs
294
+ ```
295
+
296
+ History is local to your machine (the `.veris/runs` directory is gitignored).
297
+
240
298
  ## Part of Baseframe Labs
241
299
 
242
300
  VerisKit is one of four developer tools from [Baseframe Labs](https://www.baseframelabs.com), each answering a different question about your work:
package/dist/index.js CHANGED
@@ -105,13 +105,9 @@ function detectLint(root, has) {
105
105
  return { id: "lint", available: false, reason: "no linter configured" };
106
106
  }
107
107
  function detectBrowser(root, has) {
108
- if (has("@playwright/test") || existsSync2(join(root, "playwright.config.ts")))
109
- return {
110
- id: "browser",
111
- available: false,
112
- runner: "playwright",
113
- reason: "detected; browser execution deferred to v0.5"
114
- };
108
+ if (has("@playwright/test") || existsSync2(join(root, "playwright.config.ts")) || existsSync2(join(root, "playwright.config.js"))) {
109
+ return { id: "browser", available: true, runner: "playwright" };
110
+ }
115
111
  return {
116
112
  id: "browser",
117
113
  available: false,
@@ -548,6 +544,76 @@ var init_node_test = __esm({
548
544
  }
549
545
  });
550
546
 
547
+ // src/runners/playwright.ts
548
+ function parsePlaywrightStats(stdout) {
549
+ try {
550
+ const json = JSON.parse(stdout);
551
+ return json.stats ?? null;
552
+ } catch {
553
+ return null;
554
+ }
555
+ }
556
+ var TAIL_LINES2, playwrightRunner;
557
+ var init_playwright = __esm({
558
+ "src/runners/playwright.ts"() {
559
+ "use strict";
560
+ init_store();
561
+ init_exec();
562
+ init_base();
563
+ TAIL_LINES2 = 20;
564
+ playwrightRunner = {
565
+ id: "playwright",
566
+ toCheck(project, _cap) {
567
+ return {
568
+ id: "browser",
569
+ title: "Browser tests",
570
+ runner: "playwright",
571
+ cmd: localBin(project.root, "playwright"),
572
+ args: ["test", "--reporter=json"]
573
+ };
574
+ },
575
+ async run(check, ctx) {
576
+ const r = await exec(check.cmd, check.args, {
577
+ cwd: ctx.root,
578
+ timeoutMs: 15 * 6e4
579
+ });
580
+ const output = [r.stdout, r.stderr].filter(Boolean).join("\n").trim();
581
+ const logRef = await writeLog(ctx.runDir, check.id, `${output}
582
+ `);
583
+ const stats = parsePlaywrightStats(r.stdout);
584
+ let status;
585
+ if (r.timedOut) {
586
+ status = "unknown";
587
+ } else if (stats) {
588
+ status = r.code === 0 && (stats.unexpected ?? 0) === 0 ? "passed" : "failed";
589
+ } else {
590
+ status = r.code === 0 ? "unknown" : "failed";
591
+ }
592
+ const result = {
593
+ checkId: "browser",
594
+ status,
595
+ durationMs: r.durationMs,
596
+ summary: status === "passed" ? "browser tests passed" : r.timedOut ? "timed out" : status === "unknown" ? "browser tests ran but the results could not be parsed" : "browser tests failed",
597
+ logRef
598
+ };
599
+ if (stats) {
600
+ const passed = stats.expected ?? 0;
601
+ const failed = stats.unexpected ?? 0;
602
+ result.counts = {
603
+ passed,
604
+ failed,
605
+ total: passed + failed + (stats.flaky ?? 0) + (stats.skipped ?? 0)
606
+ };
607
+ }
608
+ if (status !== "passed" && output) {
609
+ result.outputTail = output.split("\n").slice(-TAIL_LINES2).join("\n");
610
+ }
611
+ return result;
612
+ }
613
+ };
614
+ }
615
+ });
616
+
551
617
  // src/runners/tsc.ts
552
618
  var tscRunner;
553
619
  var init_tsc = __esm({
@@ -614,6 +680,7 @@ var init_runners = __esm({
614
680
  init_eslint();
615
681
  init_jest();
616
682
  init_node_test();
683
+ init_playwright();
617
684
  init_tsc();
618
685
  init_vitest();
619
686
  init_base();
@@ -623,7 +690,8 @@ var init_runners = __esm({
623
690
  jest: jestRunner,
624
691
  "node-test": nodeTestRunner,
625
692
  eslint: eslintRunner,
626
- biome: biomeRunner
693
+ biome: biomeRunner,
694
+ playwright: playwrightRunner
627
695
  };
628
696
  }
629
697
  });
@@ -997,15 +1065,205 @@ var init_markdown = __esm({
997
1065
  }
998
1066
  });
999
1067
 
1068
+ // src/publish/comment.ts
1069
+ function renderComment(run, record) {
1070
+ const summary = run.results.filter((r) => r.status !== "skipped").map((r) => `${r.checkId} ${r.status}`).join(" \xB7 ");
1071
+ return [
1072
+ MARKER,
1073
+ `**VerisKit: ${STATE_LABEL2[run.verdict.state]}**`,
1074
+ "",
1075
+ summary,
1076
+ "",
1077
+ "<details><summary>Report</summary>",
1078
+ "",
1079
+ renderMarkdown(run, record),
1080
+ "",
1081
+ "</details>",
1082
+ ""
1083
+ ].join("\n");
1084
+ }
1085
+ var MARKER, STATE_LABEL2;
1086
+ var init_comment = __esm({
1087
+ "src/publish/comment.ts"() {
1088
+ "use strict";
1089
+ init_markdown();
1090
+ MARKER = "<!-- veriskit-report -->";
1091
+ STATE_LABEL2 = {
1092
+ verified: "verified",
1093
+ failed: "failed",
1094
+ partial: "partial"
1095
+ };
1096
+ }
1097
+ });
1098
+
1099
+ // src/publish/context.ts
1100
+ import { readFileSync as readFileSync2 } from "fs";
1101
+ function defaultReadEvent(path) {
1102
+ try {
1103
+ return JSON.parse(readFileSync2(path, "utf8"));
1104
+ } catch {
1105
+ return null;
1106
+ }
1107
+ }
1108
+ function resolvePublishContext(env = process.env, readEvent = defaultReadEvent) {
1109
+ const repository = env.GITHUB_REPOSITORY;
1110
+ const token = env.GITHUB_TOKEN;
1111
+ if (!repository || !token) return null;
1112
+ const [owner, repo] = repository.split("/");
1113
+ if (!owner || !repo) return null;
1114
+ let prNumber = null;
1115
+ let headSha = env.GITHUB_SHA ?? "";
1116
+ const refMatch = (env.GITHUB_REF ?? "").match(/^refs\/pull\/(\d+)\//);
1117
+ if (refMatch) prNumber = Number(refMatch[1]);
1118
+ const event = readEvent(env.GITHUB_EVENT_PATH ?? "");
1119
+ const pr = event?.pull_request;
1120
+ if (pr) {
1121
+ if (prNumber === null && typeof pr.number === "number") {
1122
+ prNumber = pr.number;
1123
+ }
1124
+ if (pr.head?.sha) headSha = pr.head.sha;
1125
+ }
1126
+ if (prNumber === null || !headSha) return null;
1127
+ return { owner, repo, token, prNumber, headSha };
1128
+ }
1129
+ var init_context = __esm({
1130
+ "src/publish/context.ts"() {
1131
+ "use strict";
1132
+ }
1133
+ });
1134
+
1135
+ // src/publish/github.ts
1136
+ function headers(token) {
1137
+ return {
1138
+ Authorization: `Bearer ${token}`,
1139
+ Accept: "application/vnd.github+json",
1140
+ "X-GitHub-Api-Version": "2022-11-28",
1141
+ "User-Agent": "veriskit",
1142
+ "Content-Type": "application/json"
1143
+ };
1144
+ }
1145
+ async function gh(method, url, token, body) {
1146
+ const res = await fetch(url, {
1147
+ method,
1148
+ headers: headers(token),
1149
+ body: body === void 0 ? void 0 : JSON.stringify(body)
1150
+ });
1151
+ if (!res.ok) {
1152
+ const path = url.replace("https://api.github.com", "");
1153
+ throw new GitHubApiError(
1154
+ res.status,
1155
+ `GitHub API ${method} ${path} -> ${res.status}`
1156
+ );
1157
+ }
1158
+ return res.json();
1159
+ }
1160
+ function repoBase(ctx) {
1161
+ return `https://api.github.com/repos/${ctx.owner}/${ctx.repo}`;
1162
+ }
1163
+ async function upsertComment(ctx, body) {
1164
+ const base = repoBase(ctx);
1165
+ const comments = await gh(
1166
+ "GET",
1167
+ `${base}/issues/${ctx.prNumber}/comments?per_page=100`,
1168
+ ctx.token
1169
+ );
1170
+ const existing = comments.find((c) => (c.body ?? "").includes(MARKER));
1171
+ if (existing) {
1172
+ await gh("PATCH", `${base}/issues/comments/${existing.id}`, ctx.token, {
1173
+ body
1174
+ });
1175
+ } else {
1176
+ await gh("POST", `${base}/issues/${ctx.prNumber}/comments`, ctx.token, {
1177
+ body
1178
+ });
1179
+ }
1180
+ }
1181
+ async function createCheckRun(ctx, opts) {
1182
+ await gh("POST", `${repoBase(ctx)}/check-runs`, ctx.token, {
1183
+ name: opts.name,
1184
+ head_sha: ctx.headSha,
1185
+ status: "completed",
1186
+ conclusion: opts.conclusion,
1187
+ output: { title: opts.title, summary: opts.summary }
1188
+ });
1189
+ }
1190
+ var GitHubApiError;
1191
+ var init_github = __esm({
1192
+ "src/publish/github.ts"() {
1193
+ "use strict";
1194
+ init_comment();
1195
+ GitHubApiError = class extends Error {
1196
+ status;
1197
+ constructor(status, message) {
1198
+ super(message);
1199
+ this.name = "GitHubApiError";
1200
+ this.status = status;
1201
+ }
1202
+ };
1203
+ }
1204
+ });
1205
+
1206
+ // src/publish/publish.ts
1207
+ async function publishToGitHub(run, record) {
1208
+ const ctx = resolvePublishContext();
1209
+ if (!ctx) {
1210
+ process.stdout.write(
1211
+ "veris: --github set but no GitHub PR context found (need GITHUB_TOKEN + a pull_request event); skipping publish.\n"
1212
+ );
1213
+ return;
1214
+ }
1215
+ try {
1216
+ await upsertComment(ctx, renderComment(run, record));
1217
+ await createCheckRun(ctx, {
1218
+ name: "VerisKit",
1219
+ conclusion: CONCLUSION[run.verdict.state],
1220
+ title: `VerisKit: ${run.verdict.state}`,
1221
+ summary: renderMarkdown(run, record)
1222
+ });
1223
+ process.stdout.write(
1224
+ `veris: published the verdict to PR #${ctx.prNumber}
1225
+ `
1226
+ );
1227
+ } catch (err) {
1228
+ const msg = err instanceof Error ? err.message : String(err);
1229
+ process.stderr.write(`veris: GitHub publish failed: ${msg}
1230
+ `);
1231
+ }
1232
+ }
1233
+ var CONCLUSION;
1234
+ var init_publish = __esm({
1235
+ "src/publish/publish.ts"() {
1236
+ "use strict";
1237
+ init_markdown();
1238
+ init_comment();
1239
+ init_context();
1240
+ init_github();
1241
+ CONCLUSION = {
1242
+ verified: "success",
1243
+ failed: "failure",
1244
+ partial: "neutral"
1245
+ };
1246
+ }
1247
+ });
1248
+
1000
1249
  // src/cli/commands/verify.ts
1001
1250
  var verify_exports = {};
1002
1251
  __export(verify_exports, {
1252
+ resolveChecks: () => resolveChecks,
1003
1253
  runVerify: () => runVerify
1004
1254
  });
1255
+ function resolveChecks(configChecks, project, opts) {
1256
+ let checks = configChecks?.length ? configChecks : DEFAULT_CHECKS;
1257
+ if (opts.browser && !checks.includes("browser")) {
1258
+ const cap = project.capabilities.find((c) => c.id === "browser");
1259
+ if (cap?.available) checks = [...checks, "browser"];
1260
+ }
1261
+ return checks;
1262
+ }
1005
1263
  async function runVerify(root, opts = {}) {
1006
1264
  const project = await detectProject(root);
1007
1265
  const config = await loadConfig(root);
1008
- const checks = config?.checks?.length ? config.checks : DEFAULT_CHECKS;
1266
+ const checks = resolveChecks(config?.checks, project, opts);
1009
1267
  const run = await runChecks(project, checks, root);
1010
1268
  const git = await gitAnchor(root);
1011
1269
  const logDigests = await digestLogs(run);
@@ -1020,6 +1278,7 @@ async function runVerify(root, opts = {}) {
1020
1278
  await writeEvidence(runDir, record);
1021
1279
  process.stdout.write(`${renderRun(run, record)}
1022
1280
  `);
1281
+ if (opts.github) await publishToGitHub(run, record);
1023
1282
  return verdictExitCode(run.verdict, opts);
1024
1283
  }
1025
1284
  var DEFAULT_CHECKS;
@@ -1033,6 +1292,7 @@ var init_verify = __esm({
1033
1292
  init_record();
1034
1293
  init_store();
1035
1294
  init_changes();
1295
+ init_publish();
1036
1296
  init_markdown();
1037
1297
  init_terminal();
1038
1298
  init_version();
@@ -1045,7 +1305,7 @@ var report_exports = {};
1045
1305
  __export(report_exports, {
1046
1306
  runReport: () => runReport
1047
1307
  });
1048
- import { readdirSync as readdirSync2, readFileSync as readFileSync2 } from "fs";
1308
+ import { readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
1049
1309
  import { join as join6 } from "path";
1050
1310
  async function runReport(root) {
1051
1311
  const dir = join6(root, ".veris", "reports");
@@ -1061,7 +1321,7 @@ async function runReport(root) {
1061
1321
  process.stdout.write("No reports yet. Run `veris verify` first.\n");
1062
1322
  return 0;
1063
1323
  }
1064
- process.stdout.write(`${readFileSync2(join6(dir, latest), "utf8")}
1324
+ process.stdout.write(`${readFileSync3(join6(dir, latest), "utf8")}
1065
1325
  `);
1066
1326
  return 0;
1067
1327
  }
@@ -1176,7 +1436,7 @@ var init_discover = __esm({
1176
1436
  });
1177
1437
 
1178
1438
  // src/project-graph/scanner-resolver.ts
1179
- import { existsSync as existsSync3, readFileSync as readFileSync3, statSync as statSync2 } from "fs";
1439
+ import { existsSync as existsSync3, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
1180
1440
  import { dirname, join as join8, relative as relative3, resolve } from "path";
1181
1441
  function extractSpecifiers(text) {
1182
1442
  const specs = [];
@@ -1215,7 +1475,7 @@ function resolveRelative(fromDir, spec) {
1215
1475
  function scannerImports(root, file) {
1216
1476
  let text;
1217
1477
  try {
1218
- text = readFileSync3(join8(root, file), "utf8");
1478
+ text = readFileSync4(join8(root, file), "utf8");
1219
1479
  } catch {
1220
1480
  return [];
1221
1481
  }
@@ -1239,7 +1499,7 @@ var init_scanner_resolver = __esm({
1239
1499
  });
1240
1500
 
1241
1501
  // src/project-graph/ts-resolver.ts
1242
- import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
1502
+ import { existsSync as existsSync4, readFileSync as readFileSync5 } from "fs";
1243
1503
  import { createRequire } from "module";
1244
1504
  import { join as join9, relative as relative4, sep as sep2 } from "path";
1245
1505
  function hasClassicApi(mod) {
@@ -1266,7 +1526,7 @@ function loadCompilerOptions(tsmod, root) {
1266
1526
  function tsImports(tsmod, root, options, file) {
1267
1527
  let text;
1268
1528
  try {
1269
- text = readFileSync4(join9(root, file), "utf8");
1529
+ text = readFileSync5(join9(root, file), "utf8");
1270
1530
  } catch {
1271
1531
  return [];
1272
1532
  }
@@ -1518,6 +1778,7 @@ async function runAffected(root, opts = {}) {
1518
1778
  `);
1519
1779
  if (narrowedNote) process.stdout.write(`${narrowedNote}
1520
1780
  `);
1781
+ if (opts.github) await publishToGitHub(run, record);
1521
1782
  return verdictExitCode(run.verdict, opts);
1522
1783
  }
1523
1784
  var init_affected = __esm({
@@ -1530,6 +1791,7 @@ var init_affected = __esm({
1530
1791
  init_record();
1531
1792
  init_store();
1532
1793
  init_changes();
1794
+ init_publish();
1533
1795
  init_markdown();
1534
1796
  init_terminal();
1535
1797
  init_version();
@@ -2143,7 +2405,7 @@ __export(evidence_exports, {
2143
2405
  runEvidenceSign: () => runEvidenceSign,
2144
2406
  runEvidenceVerify: () => runEvidenceVerify
2145
2407
  });
2146
- import { chmodSync, existsSync as existsSync6, readFileSync as readFileSync5, writeFileSync } from "fs";
2408
+ import { chmodSync, existsSync as existsSync6, readFileSync as readFileSync6, writeFileSync } from "fs";
2147
2409
  import { writeFile as writeFile4 } from "fs/promises";
2148
2410
  import { basename as basename3, dirname as dirname2, join as join12 } from "path";
2149
2411
  import pc5 from "picocolors";
@@ -2151,7 +2413,7 @@ async function runEvidenceVerify(path, opts = {}) {
2151
2413
  let expectedPubKeyPem;
2152
2414
  if (opts.pubkey) {
2153
2415
  try {
2154
- expectedPubKeyPem = readFileSync5(opts.pubkey, "utf8");
2416
+ expectedPubKeyPem = readFileSync6(opts.pubkey, "utf8");
2155
2417
  } catch {
2156
2418
  process.stderr.write(`veris: cannot read public key at ${opts.pubkey}
2157
2419
  `);
@@ -2206,8 +2468,8 @@ async function runEvidenceKeygen(root, opts = {}) {
2206
2468
  );
2207
2469
  return 1;
2208
2470
  }
2209
- const { mkdir: mkdir2 } = await import("fs/promises");
2210
- await mkdir2(dirname2(out), { recursive: true });
2471
+ const { mkdir: mkdir3 } = await import("fs/promises");
2472
+ await mkdir3(dirname2(out), { recursive: true });
2211
2473
  const kp = generateKeyPair();
2212
2474
  writeFileSync(out, kp.privateKeyPem, { mode: 384 });
2213
2475
  chmodSync(out, 384);
@@ -2228,7 +2490,7 @@ async function runEvidenceSign(evidencePath, opts = {}) {
2228
2490
  privateKeyPem = process.env.VERISKIT_SIGNING_KEY;
2229
2491
  } else if (opts.key) {
2230
2492
  try {
2231
- privateKeyPem = readFileSync5(opts.key, "utf8");
2493
+ privateKeyPem = readFileSync6(opts.key, "utf8");
2232
2494
  } catch {
2233
2495
  process.stderr.write(`veris: cannot read signing key at ${opts.key}
2234
2496
  `);
@@ -2242,7 +2504,7 @@ async function runEvidenceSign(evidencePath, opts = {}) {
2242
2504
  }
2243
2505
  let record;
2244
2506
  try {
2245
- record = JSON.parse(readFileSync5(evidencePath, "utf8"));
2507
+ record = JSON.parse(readFileSync6(evidencePath, "utf8"));
2246
2508
  } catch {
2247
2509
  process.stderr.write(`veris: cannot read evidence at ${evidencePath}
2248
2510
  `);
@@ -2285,7 +2547,7 @@ async function runEvidenceBundle(root, opts = {}) {
2285
2547
  let record;
2286
2548
  try {
2287
2549
  record = JSON.parse(
2288
- readFileSync5(join12(runDir, "evidence.json"), "utf8")
2550
+ readFileSync6(join12(runDir, "evidence.json"), "utf8")
2289
2551
  );
2290
2552
  } catch {
2291
2553
  process.stderr.write(`veris: no evidence.json in ${runDir}
@@ -2294,7 +2556,7 @@ async function runEvidenceBundle(root, opts = {}) {
2294
2556
  }
2295
2557
  let report = "";
2296
2558
  try {
2297
- report = readFileSync5(
2559
+ report = readFileSync6(
2298
2560
  join12(root, ".veris", "reports", `verify-${record.id}.md`),
2299
2561
  "utf8"
2300
2562
  );
@@ -2306,7 +2568,7 @@ async function runEvidenceBundle(root, opts = {}) {
2306
2568
  const sigPath = join12(runDir, "evidence.json.sig");
2307
2569
  if (existsSync6(sigPath)) {
2308
2570
  try {
2309
- signature = JSON.parse(readFileSync5(sigPath, "utf8"));
2571
+ signature = JSON.parse(readFileSync6(sigPath, "utf8"));
2310
2572
  } catch {
2311
2573
  }
2312
2574
  }
@@ -2330,7 +2592,7 @@ async function runEvidenceShow(root, path) {
2330
2592
  }
2331
2593
  let record;
2332
2594
  try {
2333
- record = JSON.parse(readFileSync5(target, "utf8"));
2595
+ record = JSON.parse(readFileSync6(target, "utf8"));
2334
2596
  } catch {
2335
2597
  process.stderr.write(`veris: cannot read evidence at ${target}
2336
2598
  `);
@@ -2365,6 +2627,181 @@ var init_evidence = __esm({
2365
2627
  }
2366
2628
  });
2367
2629
 
2630
+ // src/publish/badge.ts
2631
+ function buildBadge(verdict) {
2632
+ const s = STYLE[verdict.state];
2633
+ return {
2634
+ schemaVersion: 1,
2635
+ label: "veriskit",
2636
+ message: s.message,
2637
+ color: s.color
2638
+ };
2639
+ }
2640
+ var STYLE;
2641
+ var init_badge = __esm({
2642
+ "src/publish/badge.ts"() {
2643
+ "use strict";
2644
+ STYLE = {
2645
+ verified: { message: "verified", color: "14b8a6" },
2646
+ failed: { message: "failed", color: "e5484d" },
2647
+ partial: { message: "partial", color: "f5a623" }
2648
+ };
2649
+ }
2650
+ });
2651
+
2652
+ // src/cli/commands/badge.ts
2653
+ var badge_exports = {};
2654
+ __export(badge_exports, {
2655
+ runBadge: () => runBadge
2656
+ });
2657
+ import { readFileSync as readFileSync7 } from "fs";
2658
+ import { mkdir as mkdir2, writeFile as writeFile5 } from "fs/promises";
2659
+ import { dirname as dirname3, join as join13 } from "path";
2660
+ async function runBadge(root, opts = {}) {
2661
+ const dir = latestRunDir(root);
2662
+ if (!dir) {
2663
+ process.stdout.write("No runs yet. Run `veris verify` first.\n");
2664
+ return 1;
2665
+ }
2666
+ let record;
2667
+ try {
2668
+ record = JSON.parse(
2669
+ readFileSync7(join13(dir, "evidence.json"), "utf8")
2670
+ );
2671
+ } catch {
2672
+ process.stderr.write(`veris: no evidence.json in ${dir}
2673
+ `);
2674
+ return 1;
2675
+ }
2676
+ const out = opts.out ?? join13(root, ".veris", "badge.json");
2677
+ await mkdir2(dirname3(out), { recursive: true });
2678
+ await writeFile5(
2679
+ out,
2680
+ `${JSON.stringify(buildBadge(record.verdict), null, 2)}
2681
+ `,
2682
+ "utf8"
2683
+ );
2684
+ process.stdout.write(`Wrote badge ${out}
2685
+ `);
2686
+ return 0;
2687
+ }
2688
+ var init_badge2 = __esm({
2689
+ "src/cli/commands/badge.ts"() {
2690
+ "use strict";
2691
+ init_store();
2692
+ init_badge();
2693
+ }
2694
+ });
2695
+
2696
+ // src/history/flaky.ts
2697
+ function detectFlaky(records) {
2698
+ const byId = /* @__PURE__ */ new Map();
2699
+ for (const rec of records) {
2700
+ for (const c of rec.checks ?? []) {
2701
+ const arr = byId.get(c.id) ?? [];
2702
+ arr.push(c.status);
2703
+ byId.set(c.id, arr);
2704
+ }
2705
+ }
2706
+ const flaky = [];
2707
+ for (const [id, statuses] of byId) {
2708
+ if (statuses.includes("passed") && statuses.includes("failed")) {
2709
+ flaky.push({ id, statuses });
2710
+ }
2711
+ }
2712
+ return flaky;
2713
+ }
2714
+ var init_flaky = __esm({
2715
+ "src/history/flaky.ts"() {
2716
+ "use strict";
2717
+ }
2718
+ });
2719
+
2720
+ // src/history/read.ts
2721
+ import { readdirSync as readdirSync5, readFileSync as readFileSync8 } from "fs";
2722
+ import { join as join14 } from "path";
2723
+ function loadRuns(root, limit = 20) {
2724
+ const runs = join14(root, ".veris", "runs");
2725
+ let names;
2726
+ try {
2727
+ names = readdirSync5(runs, { withFileTypes: true }).filter((d) => d.isDirectory() && d.name !== "watch").map((d) => d.name);
2728
+ } catch {
2729
+ return [];
2730
+ }
2731
+ const records = [];
2732
+ for (const name of names) {
2733
+ try {
2734
+ const rec = JSON.parse(
2735
+ readFileSync8(join14(runs, name, "evidence.json"), "utf8")
2736
+ );
2737
+ if (rec?.schema === "veriskit/evidence@1") records.push(rec);
2738
+ } catch {
2739
+ }
2740
+ }
2741
+ records.sort(
2742
+ (a, b) => a.startedAt < b.startedAt ? 1 : a.startedAt > b.startedAt ? -1 : 0
2743
+ );
2744
+ return records.slice(0, limit);
2745
+ }
2746
+ var init_read = __esm({
2747
+ "src/history/read.ts"() {
2748
+ "use strict";
2749
+ }
2750
+ });
2751
+
2752
+ // src/cli/commands/log.ts
2753
+ var log_exports = {};
2754
+ __export(log_exports, {
2755
+ runLog: () => runLog
2756
+ });
2757
+ async function runLog(root, opts = {}) {
2758
+ const limit = Number.isFinite(opts.limit) && opts.limit > 0 ? opts.limit : 20;
2759
+ const records = loadRuns(root, limit);
2760
+ if (records.length === 0) {
2761
+ process.stdout.write("No runs yet. Run `veris verify` first.\n");
2762
+ return 0;
2763
+ }
2764
+ if (opts.flaky) {
2765
+ const flaky = detectFlaky(records);
2766
+ if (flaky.length === 0) {
2767
+ process.stdout.write(
2768
+ `No flaky checks in the last ${records.length} run(s).
2769
+ `
2770
+ );
2771
+ return 0;
2772
+ }
2773
+ process.stdout.write(
2774
+ `Flaky checks (both passed and failed in the last ${records.length} run(s), newest first):
2775
+ `
2776
+ );
2777
+ for (const f of flaky) {
2778
+ process.stdout.write(` ${f.id.padEnd(8)} ${f.statuses.join(" ")}
2779
+ `);
2780
+ }
2781
+ return 0;
2782
+ }
2783
+ for (const rec of records) {
2784
+ const date = rec.startedAt.slice(0, 19).replace("T", " ");
2785
+ const checks = (rec.checks ?? []).map((c) => `${c.id}:${c.status}`).join(" ");
2786
+ const commit = rec.git ? rec.git.commit.slice(0, 7) : "-";
2787
+ process.stdout.write(
2788
+ `${date} ${rec.verdict.state.padEnd(9)} ${checks} ${commit}
2789
+ `
2790
+ );
2791
+ }
2792
+ process.stdout.write(
2793
+ "\nHistory is local to this machine (.veris/runs is gitignored).\n"
2794
+ );
2795
+ return 0;
2796
+ }
2797
+ var init_log = __esm({
2798
+ "src/cli/commands/log.ts"() {
2799
+ "use strict";
2800
+ init_flaky();
2801
+ init_read();
2802
+ }
2803
+ });
2804
+
2368
2805
  // src/cli/index.ts
2369
2806
  init_version();
2370
2807
  import { realpathSync } from "fs";
@@ -2387,23 +2824,36 @@ function buildCli() {
2387
2824
  const { runInit: runInit2 } = await Promise.resolve().then(() => (init_init(), init_exports));
2388
2825
  process.exitCode = await runInit2(process.cwd());
2389
2826
  });
2390
- cli.command("verify", "Run the full check set and produce a verdict + report").option("--partial-ok", "Exit 0 even when the verdict is partial").action(async (opts) => {
2391
- const { runVerify: runVerify2 } = await Promise.resolve().then(() => (init_verify(), verify_exports));
2392
- process.exitCode = await runVerify2(process.cwd(), {
2393
- partialOk: opts.partialOk
2394
- });
2395
- });
2827
+ cli.command("verify", "Run the full check set and produce a verdict + report").option("--partial-ok", "Exit 0 even when the verdict is partial").option(
2828
+ "--github",
2829
+ "Publish the verdict to the GitHub PR (comment + check run)"
2830
+ ).option("--browser", "Also run browser tests (Playwright), when available").action(
2831
+ async (opts) => {
2832
+ const { runVerify: runVerify2 } = await Promise.resolve().then(() => (init_verify(), verify_exports));
2833
+ process.exitCode = await runVerify2(process.cwd(), {
2834
+ partialOk: opts.partialOk,
2835
+ github: opts.github,
2836
+ browser: opts.browser
2837
+ });
2838
+ }
2839
+ );
2396
2840
  cli.command("report", "Print the latest verification report").action(async () => {
2397
2841
  const { runReport: runReport2 } = await Promise.resolve().then(() => (init_report(), report_exports));
2398
2842
  process.exitCode = await runReport2(process.cwd());
2399
2843
  });
2400
- cli.command("affected", "Run only the checks affected by changed files").option("--base <ref>", "Compare against a git ref instead of HEAD").option("--partial-ok", "Exit 0 even when the verdict is partial").action(async (opts) => {
2401
- const { runAffected: runAffected2 } = await Promise.resolve().then(() => (init_affected(), affected_exports));
2402
- process.exitCode = await runAffected2(process.cwd(), {
2403
- base: opts.base,
2404
- partialOk: opts.partialOk
2405
- });
2406
- });
2844
+ cli.command("affected", "Run only the checks affected by changed files").option("--base <ref>", "Compare against a git ref instead of HEAD").option("--partial-ok", "Exit 0 even when the verdict is partial").option(
2845
+ "--github",
2846
+ "Publish the verdict to the GitHub PR (comment + check run)"
2847
+ ).action(
2848
+ async (opts) => {
2849
+ const { runAffected: runAffected2 } = await Promise.resolve().then(() => (init_affected(), affected_exports));
2850
+ process.exitCode = await runAffected2(process.cwd(), {
2851
+ base: opts.base,
2852
+ partialOk: opts.partialOk,
2853
+ github: opts.github
2854
+ });
2855
+ }
2856
+ );
2407
2857
  cli.command("watch", "Re-run affected checks as files change (Ctrl-C to stop)").option("--poll", "Use mtime polling instead of native fs.watch").action(async (opts) => {
2408
2858
  const { runWatch: runWatch2 } = await Promise.resolve().then(() => (init_watch(), watch_exports));
2409
2859
  process.exitCode = await runWatch2(process.cwd(), { poll: opts.poll });
@@ -2465,6 +2915,20 @@ function buildCli() {
2465
2915
  }
2466
2916
  }
2467
2917
  );
2918
+ cli.command(
2919
+ "badge",
2920
+ "Write a shields.io endpoint JSON from the latest verdict"
2921
+ ).option("--out <file>", "Output path (default .veris/badge.json)").action(async (opts) => {
2922
+ const { runBadge: runBadge2 } = await Promise.resolve().then(() => (init_badge2(), badge_exports));
2923
+ process.exitCode = await runBadge2(process.cwd(), { out: opts.out });
2924
+ });
2925
+ cli.command("log", "List past verification runs (local history)").option("--limit <n>", "How many runs to show (default 20)").option("--flaky", "Show only checks that flip-flop across runs").action(async (opts) => {
2926
+ const { runLog: runLog2 } = await Promise.resolve().then(() => (init_log(), log_exports));
2927
+ process.exitCode = await runLog2(process.cwd(), {
2928
+ limit: opts.limit ? Number(opts.limit) : void 0,
2929
+ flaky: opts.flaky
2930
+ });
2931
+ });
2468
2932
  return { raw: cli, version: VERSION };
2469
2933
  }
2470
2934
  async function main(argv2) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "veriskit",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "The fastest way to prove your software works — a zero-config verification CLI.",
5
5
  "scripts": {
6
6
  "test": "vitest run",