veriskit 0.4.1 → 0.5.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/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.1 — 2026-07-13
4
+
5
+ ### Changed
6
+ - `--github` publish errors now include GitHub's own message (for example "Resource not accessible by integration" when the workflow token lacks `pull-requests: write` or `checks: write`), so a failed publish is diagnosable. The token still never appears in any error.
7
+ - `homepage` now points to the VerisKit product page.
8
+
9
+ ## 0.5.0 — 2026-07-13
10
+
11
+ ### Added
12
+ - 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.
13
+ - 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.
14
+ - `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.
15
+
3
16
  ## 0.4.1 — 2026-07-10
4
17
 
5
18
  ### 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,211 @@ 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
+ let detail = "";
1154
+ try {
1155
+ const errBody = await res.json();
1156
+ if (errBody?.message) detail = `: ${errBody.message}`;
1157
+ } catch {
1158
+ }
1159
+ throw new GitHubApiError(
1160
+ res.status,
1161
+ `GitHub API ${method} ${path} -> ${res.status}${detail}`
1162
+ );
1163
+ }
1164
+ return res.status === 204 ? null : res.json();
1165
+ }
1166
+ function repoBase(ctx) {
1167
+ return `https://api.github.com/repos/${ctx.owner}/${ctx.repo}`;
1168
+ }
1169
+ async function upsertComment(ctx, body) {
1170
+ const base = repoBase(ctx);
1171
+ const comments = await gh(
1172
+ "GET",
1173
+ `${base}/issues/${ctx.prNumber}/comments?per_page=100`,
1174
+ ctx.token
1175
+ );
1176
+ const existing = comments.find((c) => (c.body ?? "").includes(MARKER));
1177
+ if (existing) {
1178
+ await gh("PATCH", `${base}/issues/comments/${existing.id}`, ctx.token, {
1179
+ body
1180
+ });
1181
+ } else {
1182
+ await gh("POST", `${base}/issues/${ctx.prNumber}/comments`, ctx.token, {
1183
+ body
1184
+ });
1185
+ }
1186
+ }
1187
+ async function createCheckRun(ctx, opts) {
1188
+ await gh("POST", `${repoBase(ctx)}/check-runs`, ctx.token, {
1189
+ name: opts.name,
1190
+ head_sha: ctx.headSha,
1191
+ status: "completed",
1192
+ conclusion: opts.conclusion,
1193
+ output: { title: opts.title, summary: opts.summary }
1194
+ });
1195
+ }
1196
+ var GitHubApiError;
1197
+ var init_github = __esm({
1198
+ "src/publish/github.ts"() {
1199
+ "use strict";
1200
+ init_comment();
1201
+ GitHubApiError = class extends Error {
1202
+ status;
1203
+ constructor(status, message) {
1204
+ super(message);
1205
+ this.name = "GitHubApiError";
1206
+ this.status = status;
1207
+ }
1208
+ };
1209
+ }
1210
+ });
1211
+
1212
+ // src/publish/publish.ts
1213
+ async function publishToGitHub(run, record) {
1214
+ const ctx = resolvePublishContext();
1215
+ if (!ctx) {
1216
+ process.stdout.write(
1217
+ "veris: --github set but no GitHub PR context found (need GITHUB_TOKEN + a pull_request event); skipping publish.\n"
1218
+ );
1219
+ return;
1220
+ }
1221
+ try {
1222
+ await upsertComment(ctx, renderComment(run, record));
1223
+ await createCheckRun(ctx, {
1224
+ name: "VerisKit",
1225
+ conclusion: CONCLUSION[run.verdict.state],
1226
+ title: `VerisKit: ${run.verdict.state}`,
1227
+ summary: renderMarkdown(run, record)
1228
+ });
1229
+ process.stdout.write(
1230
+ `veris: published the verdict to PR #${ctx.prNumber}
1231
+ `
1232
+ );
1233
+ } catch (err) {
1234
+ const msg = err instanceof Error ? err.message : String(err);
1235
+ process.stderr.write(`veris: GitHub publish failed: ${msg}
1236
+ `);
1237
+ }
1238
+ }
1239
+ var CONCLUSION;
1240
+ var init_publish = __esm({
1241
+ "src/publish/publish.ts"() {
1242
+ "use strict";
1243
+ init_markdown();
1244
+ init_comment();
1245
+ init_context();
1246
+ init_github();
1247
+ CONCLUSION = {
1248
+ verified: "success",
1249
+ failed: "failure",
1250
+ partial: "neutral"
1251
+ };
1252
+ }
1253
+ });
1254
+
1000
1255
  // src/cli/commands/verify.ts
1001
1256
  var verify_exports = {};
1002
1257
  __export(verify_exports, {
1258
+ resolveChecks: () => resolveChecks,
1003
1259
  runVerify: () => runVerify
1004
1260
  });
1261
+ function resolveChecks(configChecks, project, opts) {
1262
+ let checks = configChecks?.length ? configChecks : DEFAULT_CHECKS;
1263
+ if (opts.browser && !checks.includes("browser")) {
1264
+ const cap = project.capabilities.find((c) => c.id === "browser");
1265
+ if (cap?.available) checks = [...checks, "browser"];
1266
+ }
1267
+ return checks;
1268
+ }
1005
1269
  async function runVerify(root, opts = {}) {
1006
1270
  const project = await detectProject(root);
1007
1271
  const config = await loadConfig(root);
1008
- const checks = config?.checks?.length ? config.checks : DEFAULT_CHECKS;
1272
+ const checks = resolveChecks(config?.checks, project, opts);
1009
1273
  const run = await runChecks(project, checks, root);
1010
1274
  const git = await gitAnchor(root);
1011
1275
  const logDigests = await digestLogs(run);
@@ -1020,6 +1284,7 @@ async function runVerify(root, opts = {}) {
1020
1284
  await writeEvidence(runDir, record);
1021
1285
  process.stdout.write(`${renderRun(run, record)}
1022
1286
  `);
1287
+ if (opts.github) await publishToGitHub(run, record);
1023
1288
  return verdictExitCode(run.verdict, opts);
1024
1289
  }
1025
1290
  var DEFAULT_CHECKS;
@@ -1033,6 +1298,7 @@ var init_verify = __esm({
1033
1298
  init_record();
1034
1299
  init_store();
1035
1300
  init_changes();
1301
+ init_publish();
1036
1302
  init_markdown();
1037
1303
  init_terminal();
1038
1304
  init_version();
@@ -1045,7 +1311,7 @@ var report_exports = {};
1045
1311
  __export(report_exports, {
1046
1312
  runReport: () => runReport
1047
1313
  });
1048
- import { readdirSync as readdirSync2, readFileSync as readFileSync2 } from "fs";
1314
+ import { readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
1049
1315
  import { join as join6 } from "path";
1050
1316
  async function runReport(root) {
1051
1317
  const dir = join6(root, ".veris", "reports");
@@ -1061,7 +1327,7 @@ async function runReport(root) {
1061
1327
  process.stdout.write("No reports yet. Run `veris verify` first.\n");
1062
1328
  return 0;
1063
1329
  }
1064
- process.stdout.write(`${readFileSync2(join6(dir, latest), "utf8")}
1330
+ process.stdout.write(`${readFileSync3(join6(dir, latest), "utf8")}
1065
1331
  `);
1066
1332
  return 0;
1067
1333
  }
@@ -1176,7 +1442,7 @@ var init_discover = __esm({
1176
1442
  });
1177
1443
 
1178
1444
  // src/project-graph/scanner-resolver.ts
1179
- import { existsSync as existsSync3, readFileSync as readFileSync3, statSync as statSync2 } from "fs";
1445
+ import { existsSync as existsSync3, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
1180
1446
  import { dirname, join as join8, relative as relative3, resolve } from "path";
1181
1447
  function extractSpecifiers(text) {
1182
1448
  const specs = [];
@@ -1215,7 +1481,7 @@ function resolveRelative(fromDir, spec) {
1215
1481
  function scannerImports(root, file) {
1216
1482
  let text;
1217
1483
  try {
1218
- text = readFileSync3(join8(root, file), "utf8");
1484
+ text = readFileSync4(join8(root, file), "utf8");
1219
1485
  } catch {
1220
1486
  return [];
1221
1487
  }
@@ -1239,7 +1505,7 @@ var init_scanner_resolver = __esm({
1239
1505
  });
1240
1506
 
1241
1507
  // src/project-graph/ts-resolver.ts
1242
- import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
1508
+ import { existsSync as existsSync4, readFileSync as readFileSync5 } from "fs";
1243
1509
  import { createRequire } from "module";
1244
1510
  import { join as join9, relative as relative4, sep as sep2 } from "path";
1245
1511
  function hasClassicApi(mod) {
@@ -1266,7 +1532,7 @@ function loadCompilerOptions(tsmod, root) {
1266
1532
  function tsImports(tsmod, root, options, file) {
1267
1533
  let text;
1268
1534
  try {
1269
- text = readFileSync4(join9(root, file), "utf8");
1535
+ text = readFileSync5(join9(root, file), "utf8");
1270
1536
  } catch {
1271
1537
  return [];
1272
1538
  }
@@ -1518,6 +1784,7 @@ async function runAffected(root, opts = {}) {
1518
1784
  `);
1519
1785
  if (narrowedNote) process.stdout.write(`${narrowedNote}
1520
1786
  `);
1787
+ if (opts.github) await publishToGitHub(run, record);
1521
1788
  return verdictExitCode(run.verdict, opts);
1522
1789
  }
1523
1790
  var init_affected = __esm({
@@ -1530,6 +1797,7 @@ var init_affected = __esm({
1530
1797
  init_record();
1531
1798
  init_store();
1532
1799
  init_changes();
1800
+ init_publish();
1533
1801
  init_markdown();
1534
1802
  init_terminal();
1535
1803
  init_version();
@@ -2143,7 +2411,7 @@ __export(evidence_exports, {
2143
2411
  runEvidenceSign: () => runEvidenceSign,
2144
2412
  runEvidenceVerify: () => runEvidenceVerify
2145
2413
  });
2146
- import { chmodSync, existsSync as existsSync6, readFileSync as readFileSync5, writeFileSync } from "fs";
2414
+ import { chmodSync, existsSync as existsSync6, readFileSync as readFileSync6, writeFileSync } from "fs";
2147
2415
  import { writeFile as writeFile4 } from "fs/promises";
2148
2416
  import { basename as basename3, dirname as dirname2, join as join12 } from "path";
2149
2417
  import pc5 from "picocolors";
@@ -2151,7 +2419,7 @@ async function runEvidenceVerify(path, opts = {}) {
2151
2419
  let expectedPubKeyPem;
2152
2420
  if (opts.pubkey) {
2153
2421
  try {
2154
- expectedPubKeyPem = readFileSync5(opts.pubkey, "utf8");
2422
+ expectedPubKeyPem = readFileSync6(opts.pubkey, "utf8");
2155
2423
  } catch {
2156
2424
  process.stderr.write(`veris: cannot read public key at ${opts.pubkey}
2157
2425
  `);
@@ -2206,8 +2474,8 @@ async function runEvidenceKeygen(root, opts = {}) {
2206
2474
  );
2207
2475
  return 1;
2208
2476
  }
2209
- const { mkdir: mkdir2 } = await import("fs/promises");
2210
- await mkdir2(dirname2(out), { recursive: true });
2477
+ const { mkdir: mkdir3 } = await import("fs/promises");
2478
+ await mkdir3(dirname2(out), { recursive: true });
2211
2479
  const kp = generateKeyPair();
2212
2480
  writeFileSync(out, kp.privateKeyPem, { mode: 384 });
2213
2481
  chmodSync(out, 384);
@@ -2228,7 +2496,7 @@ async function runEvidenceSign(evidencePath, opts = {}) {
2228
2496
  privateKeyPem = process.env.VERISKIT_SIGNING_KEY;
2229
2497
  } else if (opts.key) {
2230
2498
  try {
2231
- privateKeyPem = readFileSync5(opts.key, "utf8");
2499
+ privateKeyPem = readFileSync6(opts.key, "utf8");
2232
2500
  } catch {
2233
2501
  process.stderr.write(`veris: cannot read signing key at ${opts.key}
2234
2502
  `);
@@ -2242,7 +2510,7 @@ async function runEvidenceSign(evidencePath, opts = {}) {
2242
2510
  }
2243
2511
  let record;
2244
2512
  try {
2245
- record = JSON.parse(readFileSync5(evidencePath, "utf8"));
2513
+ record = JSON.parse(readFileSync6(evidencePath, "utf8"));
2246
2514
  } catch {
2247
2515
  process.stderr.write(`veris: cannot read evidence at ${evidencePath}
2248
2516
  `);
@@ -2285,7 +2553,7 @@ async function runEvidenceBundle(root, opts = {}) {
2285
2553
  let record;
2286
2554
  try {
2287
2555
  record = JSON.parse(
2288
- readFileSync5(join12(runDir, "evidence.json"), "utf8")
2556
+ readFileSync6(join12(runDir, "evidence.json"), "utf8")
2289
2557
  );
2290
2558
  } catch {
2291
2559
  process.stderr.write(`veris: no evidence.json in ${runDir}
@@ -2294,7 +2562,7 @@ async function runEvidenceBundle(root, opts = {}) {
2294
2562
  }
2295
2563
  let report = "";
2296
2564
  try {
2297
- report = readFileSync5(
2565
+ report = readFileSync6(
2298
2566
  join12(root, ".veris", "reports", `verify-${record.id}.md`),
2299
2567
  "utf8"
2300
2568
  );
@@ -2306,7 +2574,7 @@ async function runEvidenceBundle(root, opts = {}) {
2306
2574
  const sigPath = join12(runDir, "evidence.json.sig");
2307
2575
  if (existsSync6(sigPath)) {
2308
2576
  try {
2309
- signature = JSON.parse(readFileSync5(sigPath, "utf8"));
2577
+ signature = JSON.parse(readFileSync6(sigPath, "utf8"));
2310
2578
  } catch {
2311
2579
  }
2312
2580
  }
@@ -2330,7 +2598,7 @@ async function runEvidenceShow(root, path) {
2330
2598
  }
2331
2599
  let record;
2332
2600
  try {
2333
- record = JSON.parse(readFileSync5(target, "utf8"));
2601
+ record = JSON.parse(readFileSync6(target, "utf8"));
2334
2602
  } catch {
2335
2603
  process.stderr.write(`veris: cannot read evidence at ${target}
2336
2604
  `);
@@ -2365,6 +2633,181 @@ var init_evidence = __esm({
2365
2633
  }
2366
2634
  });
2367
2635
 
2636
+ // src/publish/badge.ts
2637
+ function buildBadge(verdict) {
2638
+ const s = STYLE[verdict.state];
2639
+ return {
2640
+ schemaVersion: 1,
2641
+ label: "veriskit",
2642
+ message: s.message,
2643
+ color: s.color
2644
+ };
2645
+ }
2646
+ var STYLE;
2647
+ var init_badge = __esm({
2648
+ "src/publish/badge.ts"() {
2649
+ "use strict";
2650
+ STYLE = {
2651
+ verified: { message: "verified", color: "14b8a6" },
2652
+ failed: { message: "failed", color: "e5484d" },
2653
+ partial: { message: "partial", color: "f5a623" }
2654
+ };
2655
+ }
2656
+ });
2657
+
2658
+ // src/cli/commands/badge.ts
2659
+ var badge_exports = {};
2660
+ __export(badge_exports, {
2661
+ runBadge: () => runBadge
2662
+ });
2663
+ import { readFileSync as readFileSync7 } from "fs";
2664
+ import { mkdir as mkdir2, writeFile as writeFile5 } from "fs/promises";
2665
+ import { dirname as dirname3, join as join13 } from "path";
2666
+ async function runBadge(root, opts = {}) {
2667
+ const dir = latestRunDir(root);
2668
+ if (!dir) {
2669
+ process.stdout.write("No runs yet. Run `veris verify` first.\n");
2670
+ return 1;
2671
+ }
2672
+ let record;
2673
+ try {
2674
+ record = JSON.parse(
2675
+ readFileSync7(join13(dir, "evidence.json"), "utf8")
2676
+ );
2677
+ } catch {
2678
+ process.stderr.write(`veris: no evidence.json in ${dir}
2679
+ `);
2680
+ return 1;
2681
+ }
2682
+ const out = opts.out ?? join13(root, ".veris", "badge.json");
2683
+ await mkdir2(dirname3(out), { recursive: true });
2684
+ await writeFile5(
2685
+ out,
2686
+ `${JSON.stringify(buildBadge(record.verdict), null, 2)}
2687
+ `,
2688
+ "utf8"
2689
+ );
2690
+ process.stdout.write(`Wrote badge ${out}
2691
+ `);
2692
+ return 0;
2693
+ }
2694
+ var init_badge2 = __esm({
2695
+ "src/cli/commands/badge.ts"() {
2696
+ "use strict";
2697
+ init_store();
2698
+ init_badge();
2699
+ }
2700
+ });
2701
+
2702
+ // src/history/flaky.ts
2703
+ function detectFlaky(records) {
2704
+ const byId = /* @__PURE__ */ new Map();
2705
+ for (const rec of records) {
2706
+ for (const c of rec.checks ?? []) {
2707
+ const arr = byId.get(c.id) ?? [];
2708
+ arr.push(c.status);
2709
+ byId.set(c.id, arr);
2710
+ }
2711
+ }
2712
+ const flaky = [];
2713
+ for (const [id, statuses] of byId) {
2714
+ if (statuses.includes("passed") && statuses.includes("failed")) {
2715
+ flaky.push({ id, statuses });
2716
+ }
2717
+ }
2718
+ return flaky;
2719
+ }
2720
+ var init_flaky = __esm({
2721
+ "src/history/flaky.ts"() {
2722
+ "use strict";
2723
+ }
2724
+ });
2725
+
2726
+ // src/history/read.ts
2727
+ import { readdirSync as readdirSync5, readFileSync as readFileSync8 } from "fs";
2728
+ import { join as join14 } from "path";
2729
+ function loadRuns(root, limit = 20) {
2730
+ const runs = join14(root, ".veris", "runs");
2731
+ let names;
2732
+ try {
2733
+ names = readdirSync5(runs, { withFileTypes: true }).filter((d) => d.isDirectory() && d.name !== "watch").map((d) => d.name);
2734
+ } catch {
2735
+ return [];
2736
+ }
2737
+ const records = [];
2738
+ for (const name of names) {
2739
+ try {
2740
+ const rec = JSON.parse(
2741
+ readFileSync8(join14(runs, name, "evidence.json"), "utf8")
2742
+ );
2743
+ if (rec?.schema === "veriskit/evidence@1") records.push(rec);
2744
+ } catch {
2745
+ }
2746
+ }
2747
+ records.sort(
2748
+ (a, b) => a.startedAt < b.startedAt ? 1 : a.startedAt > b.startedAt ? -1 : 0
2749
+ );
2750
+ return records.slice(0, limit);
2751
+ }
2752
+ var init_read = __esm({
2753
+ "src/history/read.ts"() {
2754
+ "use strict";
2755
+ }
2756
+ });
2757
+
2758
+ // src/cli/commands/log.ts
2759
+ var log_exports = {};
2760
+ __export(log_exports, {
2761
+ runLog: () => runLog
2762
+ });
2763
+ async function runLog(root, opts = {}) {
2764
+ const limit = Number.isFinite(opts.limit) && opts.limit > 0 ? opts.limit : 20;
2765
+ const records = loadRuns(root, limit);
2766
+ if (records.length === 0) {
2767
+ process.stdout.write("No runs yet. Run `veris verify` first.\n");
2768
+ return 0;
2769
+ }
2770
+ if (opts.flaky) {
2771
+ const flaky = detectFlaky(records);
2772
+ if (flaky.length === 0) {
2773
+ process.stdout.write(
2774
+ `No flaky checks in the last ${records.length} run(s).
2775
+ `
2776
+ );
2777
+ return 0;
2778
+ }
2779
+ process.stdout.write(
2780
+ `Flaky checks (both passed and failed in the last ${records.length} run(s), newest first):
2781
+ `
2782
+ );
2783
+ for (const f of flaky) {
2784
+ process.stdout.write(` ${f.id.padEnd(8)} ${f.statuses.join(" ")}
2785
+ `);
2786
+ }
2787
+ return 0;
2788
+ }
2789
+ for (const rec of records) {
2790
+ const date = rec.startedAt.slice(0, 19).replace("T", " ");
2791
+ const checks = (rec.checks ?? []).map((c) => `${c.id}:${c.status}`).join(" ");
2792
+ const commit = rec.git ? rec.git.commit.slice(0, 7) : "-";
2793
+ process.stdout.write(
2794
+ `${date} ${rec.verdict.state.padEnd(9)} ${checks} ${commit}
2795
+ `
2796
+ );
2797
+ }
2798
+ process.stdout.write(
2799
+ "\nHistory is local to this machine (.veris/runs is gitignored).\n"
2800
+ );
2801
+ return 0;
2802
+ }
2803
+ var init_log = __esm({
2804
+ "src/cli/commands/log.ts"() {
2805
+ "use strict";
2806
+ init_flaky();
2807
+ init_read();
2808
+ }
2809
+ });
2810
+
2368
2811
  // src/cli/index.ts
2369
2812
  init_version();
2370
2813
  import { realpathSync } from "fs";
@@ -2387,23 +2830,36 @@ function buildCli() {
2387
2830
  const { runInit: runInit2 } = await Promise.resolve().then(() => (init_init(), init_exports));
2388
2831
  process.exitCode = await runInit2(process.cwd());
2389
2832
  });
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
- });
2833
+ 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(
2834
+ "--github",
2835
+ "Publish the verdict to the GitHub PR (comment + check run)"
2836
+ ).option("--browser", "Also run browser tests (Playwright), when available").action(
2837
+ async (opts) => {
2838
+ const { runVerify: runVerify2 } = await Promise.resolve().then(() => (init_verify(), verify_exports));
2839
+ process.exitCode = await runVerify2(process.cwd(), {
2840
+ partialOk: opts.partialOk,
2841
+ github: opts.github,
2842
+ browser: opts.browser
2843
+ });
2844
+ }
2845
+ );
2396
2846
  cli.command("report", "Print the latest verification report").action(async () => {
2397
2847
  const { runReport: runReport2 } = await Promise.resolve().then(() => (init_report(), report_exports));
2398
2848
  process.exitCode = await runReport2(process.cwd());
2399
2849
  });
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
- });
2850
+ 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(
2851
+ "--github",
2852
+ "Publish the verdict to the GitHub PR (comment + check run)"
2853
+ ).action(
2854
+ async (opts) => {
2855
+ const { runAffected: runAffected2 } = await Promise.resolve().then(() => (init_affected(), affected_exports));
2856
+ process.exitCode = await runAffected2(process.cwd(), {
2857
+ base: opts.base,
2858
+ partialOk: opts.partialOk,
2859
+ github: opts.github
2860
+ });
2861
+ }
2862
+ );
2407
2863
  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
2864
  const { runWatch: runWatch2 } = await Promise.resolve().then(() => (init_watch(), watch_exports));
2409
2865
  process.exitCode = await runWatch2(process.cwd(), { poll: opts.poll });
@@ -2465,6 +2921,20 @@ function buildCli() {
2465
2921
  }
2466
2922
  }
2467
2923
  );
2924
+ cli.command(
2925
+ "badge",
2926
+ "Write a shields.io endpoint JSON from the latest verdict"
2927
+ ).option("--out <file>", "Output path (default .veris/badge.json)").action(async (opts) => {
2928
+ const { runBadge: runBadge2 } = await Promise.resolve().then(() => (init_badge2(), badge_exports));
2929
+ process.exitCode = await runBadge2(process.cwd(), { out: opts.out });
2930
+ });
2931
+ 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) => {
2932
+ const { runLog: runLog2 } = await Promise.resolve().then(() => (init_log(), log_exports));
2933
+ process.exitCode = await runLog2(process.cwd(), {
2934
+ limit: opts.limit ? Number(opts.limit) : void 0,
2935
+ flaky: opts.flaky
2936
+ });
2937
+ });
2468
2938
  return { raw: cli, version: VERSION };
2469
2939
  }
2470
2940
  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.1",
4
4
  "description": "The fastest way to prove your software works — a zero-config verification CLI.",
5
5
  "scripts": {
6
6
  "test": "vitest run",
@@ -19,7 +19,7 @@
19
19
  "bugs": {
20
20
  "url": "https://github.com/abhiyoheswaran1/veris/issues"
21
21
  },
22
- "homepage": "https://github.com/abhiyoheswaran1/veris#readme",
22
+ "homepage": "https://www.baseframelabs.com/apps/veriskit",
23
23
  "bin": {
24
24
  "veris": "bin/veris"
25
25
  },