verifika 0.0.1 → 0.0.3

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.
@@ -1,4 +1,5 @@
1
- import { describeProblem, duration, isFailureStatus, logsHint, MAX_PREVIEWED_FAILURES, problems, selectionLabel, } from "./reporter.js";
1
+ import { formatSeconds } from "../format.js";
2
+ import { describeProblem, duration, isFailureStatus, logsHint, previewDecider, problems, selectionLabel, STOPPING_MESSAGE, } from "./reporter.js";
2
3
  const wrap = (open, close) => (text) => `\u001b[${open}m${text}\u001b[${close}m`;
3
4
  export const colors = {
4
5
  green: wrap(32, 39),
@@ -25,20 +26,13 @@ const HEARTBEAT_MS = 30_000;
25
26
  export function humanReporter(output, style, options = {}) {
26
27
  const running = new Map();
27
28
  let heartbeat;
29
+ let width = 0;
28
30
  return {
29
- onStart({ selection, selected }) {
30
- const skipped = selection.checks.length - selected.length;
31
- output.out(`${style.bold("verifika")} ${selectionLabel(selection, skipped)}\n`);
32
- if (selected.length === 0) {
33
- output.out(style.dim("Nothing to verify for these changes. Full run: verifika --all\n"));
31
+ onStart(info) {
32
+ output.out(renderStart(info, style, options.skipEstimate));
33
+ width = nameWidth(info.selected);
34
+ if (info.selected.length === 0)
34
35
  return;
35
- }
36
- if (skipped > 0) {
37
- const names = selection.checks
38
- .filter((check) => !check.selected)
39
- .map((check) => check.name);
40
- output.out(style.dim(`skipped: ${names.join(", ")}\n`));
41
- }
42
36
  if (options.heartbeat) {
43
37
  heartbeat = setInterval(() => {
44
38
  if (running.size === 0)
@@ -58,33 +52,75 @@ export function humanReporter(output, style, options = {}) {
58
52
  },
59
53
  onCheckEnd(check) {
60
54
  running.delete(check.name);
61
- const detail = check.status === "passed" ? "" : ` ${describeProblem(check)}`;
62
- output.out(`${symbol(check.status, style)} ${check.name} ${style.dim(duration(check.durationMs))}${detail}\n`);
55
+ output.out(renderCheckEnd(check, style, width));
63
56
  },
64
57
  onFinish(result) {
65
58
  if (heartbeat)
66
59
  clearInterval(heartbeat);
67
60
  output.out(renderSummary(result, style));
68
61
  },
62
+ onInterrupt() {
63
+ output.err(style.yellow(STOPPING_MESSAGE));
64
+ },
69
65
  };
70
66
  }
67
+ export function nameWidth(names) {
68
+ return Math.max(0, ...names.map((name) => name.length));
69
+ }
70
+ /**
71
+ * The run header (`verifika · <selection> · <n> checks · <j> parallel`), then
72
+ * the skipped checks with the time skipping them saved.
73
+ */
74
+ export function renderStart({ selection, selected, concurrency }, style, estimate) {
75
+ const header = [style.bold("verifika"), selectionLabel(selection, 0)];
76
+ header.push(`${selected.length} check${selected.length === 1 ? "" : "s"}`);
77
+ if (selected.length > 0)
78
+ header.push(`${concurrency} parallel`);
79
+ let text = `${header.join(" · ")}\n`;
80
+ const skipped = selection.checks.filter((check) => !check.selected).map((check) => check.name);
81
+ if (skipped.length > 0) {
82
+ text += style.dim(`${skipped.length} skipped${savedNote(skipped.length, estimate)}: ${skipped.join(", ")}\n`);
83
+ }
84
+ if (selected.length === 0) {
85
+ text += style.dim("Nothing to verify for these changes. Full run: vfk --all\n");
86
+ }
87
+ return text;
88
+ }
89
+ /** ` (~40s saved)`, ` (≥40s saved)` when some durations are unknown, or nothing when none are known. */
90
+ function savedNote(skipped, estimate) {
91
+ if (!estimate || estimate.unknown >= skipped)
92
+ return "";
93
+ return ` (${estimate.unknown > 0 ? "≥" : "~"}${formatSeconds(estimate.knownMs)} saved)`;
94
+ }
95
+ /** The permanent line printed when a check finishes; durations line up in a column. */
96
+ export function renderCheckEnd(check, style, width = 0) {
97
+ const time = style.dim(duration(check.durationMs).padStart(5));
98
+ const detail = check.status === "passed" ? "" : ` ${describeProblem(check)}`;
99
+ return `${symbol(check.status, style)} ${check.name.padEnd(width)} ${time}${detail}\n`;
100
+ }
71
101
  export function renderSummary(result, style) {
72
102
  const { summary } = result;
73
103
  const lines = [];
74
104
  const failing = problems(result);
75
- let previewed = 0;
105
+ const decide = previewDecider(result);
106
+ const width = nameWidth(failing.map((check) => check.name));
76
107
  if (failing.length > 0)
77
108
  lines.push("");
78
109
  for (const check of failing) {
110
+ const name = check.name.padEnd(width);
111
+ if (check.status === "blocked") {
112
+ lines.push(style.dim(`${symbol(check.status, plain)} ${name} ${blockedReason(check, result)}`));
113
+ continue;
114
+ }
79
115
  if (!isFailureStatus(check.status)) {
80
- lines.push(`${symbol(check.status, style)} ${check.name}: ${describeProblem(check)}`);
116
+ lines.push(`${symbol(check.status, style)} ${name} ${describeProblem(check)}`);
81
117
  continue;
82
118
  }
83
- lines.push(`${symbol(check.status, style)} ${style.bold(check.name)}: ${describeProblem(check)}`);
84
- if (check.preview.length > 0 && previewed < MAX_PREVIEWED_FAILURES) {
85
- previewed++;
119
+ const preview = decide(check);
120
+ const rerun = `rerun: vfk -c ${check.name}`;
121
+ lines.push(`${symbol(check.status, style)} ${style.bold(name)} ${check.command} → ${describeProblem(check)}${style.dim(` · ${rerun}${preview.note}`)}`);
122
+ if (preview.show)
86
123
  lines.push(...check.preview.map((line) => style.dim(` ${line}`)));
87
- }
88
124
  }
89
125
  const time = duration(result.durationMs);
90
126
  if (result.status === "passed") {
@@ -99,6 +135,18 @@ export function renderSummary(result, style) {
99
135
  lines.push(style.dim(`Logs: ${hint}`));
100
136
  return `${lines.join("\n")}\n`;
101
137
  }
138
+ /** `not run: build failed`, naming what happened to the dependency that blocked the check. */
139
+ function blockedReason(check, result) {
140
+ const dependency = result.checks.find((other) => other.name === check.blockedBy);
141
+ if (!dependency)
142
+ return `not run: ${check.reason}`;
143
+ const outcome = dependency.status === "timed_out"
144
+ ? "timed out"
145
+ : isFailureStatus(dependency.status)
146
+ ? "failed"
147
+ : "did not run";
148
+ return `not run: ${dependency.name} ${outcome}`;
149
+ }
102
150
  export function symbol(status, style) {
103
151
  switch (status) {
104
152
  case "passed":
@@ -109,6 +157,8 @@ export function symbol(status, style) {
109
157
  return style.red("✗");
110
158
  case "skipped":
111
159
  return style.dim("–");
160
+ case "blocked":
161
+ return style.dim("○");
112
162
  default:
113
163
  return style.yellow("!");
114
164
  }
@@ -0,0 +1,209 @@
1
+ import { formatSeconds } from "../format.js";
2
+ import { nameWidth, renderCheckEnd, renderStart, renderSummary, } from "./human.js";
3
+ import { STOPPING_MESSAGE } from "./reporter.js";
4
+ /** Redraw interval: about 10 frames a second, which also throttles output-driven updates. */
5
+ const FRAME_MS = 100;
6
+ const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
7
+ const CSI = "\u001b[";
8
+ const HIDE_CURSOR = `${CSI}?25l`;
9
+ const SHOW_CURSOR = `${CSI}?25h`;
10
+ // Autowrap stays off while the block is drawn, so an overlong line (wide characters
11
+ // the width estimate missed) is clipped by the terminal instead of wrapping and
12
+ // breaking the cursor-up count of the next redraw.
13
+ const NO_WRAP = `${CSI}?7l`;
14
+ const WRAP = `${CSI}?7h`;
15
+ /**
16
+ * The redrawn part of the live display: a progress header, one line per running
17
+ * check (spinner, elapsed time, latest output) and the checks still waiting.
18
+ * Lines fit the width; the block never exceeds the height minus one row, so the
19
+ * cursor can always move back to its top.
20
+ */
21
+ export function renderBlock(state, view, style) {
22
+ const max = Math.max(1, view.width - 1);
23
+ const elapsed = formatSeconds(view.now - state.startedAt);
24
+ const header = `[${state.finished.size}/${state.selected.length}] ${elapsed}${state.stopping ? " · stopping…" : ""}`;
25
+ const namePad = nameWidth(state.selected);
26
+ const frame = SPINNER[Math.floor(view.now / FRAME_MS) % SPINNER.length];
27
+ const running = state.selected
28
+ .filter((name) => state.running.has(name))
29
+ .map((name) => {
30
+ const { since, line } = state.running.get(name);
31
+ const time = formatSeconds(view.now - since).padStart(5);
32
+ const prefix = `${frame} ${name.padEnd(namePad)} ${time}`;
33
+ if (prefix.length + 2 >= max)
34
+ return fit(prefix, max);
35
+ const shown = fit(line, max - prefix.length - 2);
36
+ return `${frame} ${name.padEnd(namePad)} ${style.dim(time)}${shown ? ` ${style.dim(shown)}` : ""}`;
37
+ });
38
+ const groups = waitingGroups(state);
39
+ const waiting = groups.map(({ names, why }) => style.dim(fit(` ${names.join(", ")} ${why}`, max)));
40
+ // Keep the header, then as many running checks as fit, then waiting groups;
41
+ // collapse whatever does not fit into a count.
42
+ const room = Math.max(1, view.height - 1) - 1;
43
+ let shownRunning = running.length;
44
+ let shownWaiting = waiting.length;
45
+ const total = () => shownRunning +
46
+ (shownRunning < running.length ? 1 : 0) +
47
+ shownWaiting +
48
+ (shownWaiting < waiting.length ? 1 : 0);
49
+ while (total() > room && (shownWaiting > 0 || shownRunning > 0)) {
50
+ if (shownWaiting > 0)
51
+ shownWaiting--;
52
+ else
53
+ shownRunning--;
54
+ }
55
+ const lines = [style.bold(fit(header, max)), ...running.slice(0, shownRunning)];
56
+ if (shownRunning < running.length)
57
+ lines.push(style.dim(fit(` +${running.length - shownRunning} more running`, max)));
58
+ lines.push(...waiting.slice(0, shownWaiting));
59
+ if (shownWaiting < waiting.length) {
60
+ const hidden = groups
61
+ .slice(shownWaiting)
62
+ .reduce((count, group) => count + group.names.length, 0);
63
+ lines.push(style.dim(fit(` +${hidden} more waiting`, max)));
64
+ }
65
+ return lines.slice(0, Math.max(1, view.height - 1));
66
+ }
67
+ /** Checks not yet started, grouped by what they wait for, e.g. `test, pack waiting on build`. */
68
+ function waitingGroups(state) {
69
+ const selected = new Set(state.selected);
70
+ // A check whose dependency failed (directly or down a chain) will never start.
71
+ const blocked = new Set();
72
+ for (let changed = true; changed;) {
73
+ changed = false;
74
+ for (const name of state.selected) {
75
+ if (blocked.has(name) || state.finished.has(name) || state.running.has(name))
76
+ continue;
77
+ const deps = state.dependsOn[name] ?? [];
78
+ if (deps.some((dep) => state.finished.get(dep) === false || blocked.has(dep))) {
79
+ blocked.add(name);
80
+ changed = true;
81
+ }
82
+ }
83
+ }
84
+ const groups = new Map();
85
+ for (const name of state.selected) {
86
+ if (state.running.has(name) || state.finished.has(name) || blocked.has(name))
87
+ continue;
88
+ const unmet = (state.dependsOn[name] ?? []).filter((dep) => selected.has(dep) && state.finished.get(dep) !== true);
89
+ const why = state.stopping
90
+ ? "will not start"
91
+ : unmet.length > 0
92
+ ? `waiting on ${unmet.join(", ")}`
93
+ : "queued";
94
+ groups.set(why, [...(groups.get(why) ?? []), name]);
95
+ }
96
+ return [...groups].map(([why, names]) => ({ names, why }));
97
+ }
98
+ /** Truncates to `width` characters (code points), marking the cut with an ellipsis. */
99
+ export function fit(text, width) {
100
+ const chars = Array.from(text);
101
+ if (chars.length <= width)
102
+ return text;
103
+ return width <= 0 ? "" : `${chars.slice(0, width - 1).join("")}…`;
104
+ }
105
+ /**
106
+ * Human output with a live status block. Finished checks print permanently above
107
+ * the block exactly as the line reporter prints them, so scrollback reads like
108
+ * line mode; the block below is redrawn in place (cursor up, erase down) and
109
+ * removed when the run ends. The cursor is hidden while live and restored on
110
+ * finish, on dispose (errors) and on process exit.
111
+ */
112
+ export function liveReporter(terminal, style, options = {}) {
113
+ let state = null;
114
+ let drawn = 0;
115
+ let stopTicker;
116
+ let unregisterExit;
117
+ let width = 0;
118
+ const clear = () => (drawn > 0 ? `\r${CSI}${drawn}A${CSI}J` : "");
119
+ const draw = (above = "") => {
120
+ if (!state)
121
+ return;
122
+ const lines = renderBlock(state, { width: terminal.columns(), height: terminal.rows(), now: terminal.now() }, style);
123
+ terminal.write(`${clear()}${above}${NO_WRAP}${lines.map((line) => `${line}\n`).join("")}${WRAP}`);
124
+ drawn = lines.length;
125
+ };
126
+ const release = (after = "") => {
127
+ if (!state) {
128
+ if (after)
129
+ terminal.write(after);
130
+ return;
131
+ }
132
+ stopTicker?.();
133
+ unregisterExit?.();
134
+ terminal.write(`${clear()}${SHOW_CURSOR}${after}`);
135
+ state = null;
136
+ drawn = 0;
137
+ };
138
+ return {
139
+ onStart(info) {
140
+ terminal.write(renderStart(info, style, options.skipEstimate));
141
+ width = nameWidth(info.selected);
142
+ if (info.selected.length === 0)
143
+ return;
144
+ state = {
145
+ selected: info.selected,
146
+ dependsOn: info.dependsOn,
147
+ running: new Map(),
148
+ finished: new Map(),
149
+ startedAt: terminal.now(),
150
+ stopping: false,
151
+ };
152
+ terminal.write(HIDE_CURSOR);
153
+ unregisterExit = terminal.onExit(() => terminal.write(`${SHOW_CURSOR}${WRAP}`));
154
+ stopTicker = terminal.every(FRAME_MS, () => draw());
155
+ draw();
156
+ },
157
+ onCheckStart(name) {
158
+ state?.running.set(name, { since: terminal.now(), line: "" });
159
+ draw();
160
+ },
161
+ onCheckOutput(name, line) {
162
+ // Picked up by the next frame; output alone never triggers a redraw.
163
+ const entry = state?.running.get(name);
164
+ if (entry)
165
+ entry.line = line;
166
+ },
167
+ onCheckEnd(check) {
168
+ state?.running.delete(check.name);
169
+ state?.finished.set(check.name, check.status === "passed");
170
+ draw(renderCheckEnd(check, style, width));
171
+ },
172
+ onFinish(result) {
173
+ release(renderSummary(result, style));
174
+ },
175
+ onInterrupt() {
176
+ // Clear the block first so the message lands above it, not inside it (or on top of
177
+ // the ^C the terminal echoed below it); the redrawn block then says it is stopping.
178
+ if (state) {
179
+ state.stopping = true;
180
+ terminal.write(clear());
181
+ drawn = 0;
182
+ }
183
+ terminal.writeError(style.yellow(STOPPING_MESSAGE));
184
+ draw();
185
+ },
186
+ dispose() {
187
+ release();
188
+ },
189
+ };
190
+ }
191
+ /** The real terminal behind stdout. */
192
+ export function systemTerminal(write, writeError, stream) {
193
+ return {
194
+ write,
195
+ writeError,
196
+ columns: () => stream.columns || 80,
197
+ rows: () => stream.rows || 24,
198
+ now: () => performance.now(),
199
+ every(ms, frame) {
200
+ const timer = setInterval(frame, ms);
201
+ timer.unref();
202
+ return () => clearInterval(timer);
203
+ },
204
+ onExit(restore) {
205
+ process.once("exit", restore);
206
+ return () => process.off("exit", restore);
207
+ },
208
+ };
209
+ }
@@ -1,3 +1,4 @@
1
+ import { plain } from "./human.js";
1
2
  import { selectionLabel } from "./reporter.js";
2
3
  const STATUS_LETTER = {
3
4
  added: "A",
@@ -7,7 +8,7 @@ const STATUS_LETTER = {
7
8
  changed: "~",
8
9
  };
9
10
  /** Text rendering of a plan. `maxFiles` bounds the changed-file list. */
10
- export function renderPlan(selection, maxFiles) {
11
+ export function renderPlan(selection, maxFiles, style = plain) {
11
12
  const skipped = selection.checks.filter((check) => !check.selected).length;
12
13
  const lines = [`Plan: ${selectionLabel(selection, skipped)}`];
13
14
  const files = selection.changes?.files;
@@ -16,12 +17,13 @@ export function renderPlan(selection, maxFiles) {
16
17
  for (const file of files.slice(0, maxFiles))
17
18
  lines.push(` ${STATUS_LETTER[file.status]} ${file.path}`);
18
19
  if (files.length > maxFiles)
19
- lines.push(` … ${files.length - maxFiles} more (verifika plan --mode json lists all)`);
20
+ lines.push(` … ${files.length - maxFiles} more (vfk plan --mode json lists all)`);
20
21
  }
21
22
  lines.push("Checks:");
22
23
  const width = Math.max(...selection.checks.map((check) => check.name.length));
23
24
  for (const check of selection.checks) {
24
- lines.push(` ${check.selected ? "run " : "skip"} ${check.name.padEnd(width)} ${check.reason}`);
25
+ const row = `${check.name.padEnd(width)} ${check.reason}`;
26
+ lines.push(check.selected ? ` ${style.green("run ")} ${row}` : style.dim(` skip ${row}`));
25
27
  }
26
28
  return `${lines.join("\n")}\n`;
27
29
  }
@@ -1,5 +1,7 @@
1
1
  import { formatSeconds } from "../format.js";
2
2
  export const OUTPUT_MODES = ["human", "agent", "json", "quiet"];
3
+ /** Printed to stderr in human mode on the first interrupt. */
4
+ export const STOPPING_MESSAGE = "stopping… (Ctrl-C again to force)\n";
3
5
  /** Previews are shown for at most this many failed checks, to keep output bounded. */
4
6
  export const MAX_PREVIEWED_FAILURES = 3;
5
7
  export function isFailureStatus(status) {
@@ -28,9 +30,31 @@ export function logsHint(result) {
28
30
  const withLogs = problems(result).filter((check) => isFailureStatus(check.status) && check.log);
29
31
  if (withLogs.length === 0)
30
32
  return null;
31
- const command = withLogs.length === 1 ? `verifika logs ${withLogs[0]?.name}` : "verifika logs --failed";
33
+ return logsCommand(result, withLogs.length === 1 ? (withLogs[0]?.name ?? "") : "--failed");
34
+ }
35
+ /** `vfk logs <target>`, naming the run when a later run has become the latest. */
36
+ function logsCommand(result, target) {
37
+ const command = `vfk logs ${target}`;
32
38
  return result.latest ? command : `${command} --run ${result.runId}`;
33
39
  }
40
+ /**
41
+ * Decides which failed checks get their preview printed: at most
42
+ * MAX_PREVIEWED_FAILURES, in config order. The rest get a note pointing at their
43
+ * log, so a hidden preview never looks like one that found nothing.
44
+ */
45
+ export function previewDecider(result) {
46
+ let previewed = 0;
47
+ return (check) => {
48
+ if (!isFailureStatus(check.status) || check.preview.length === 0) {
49
+ return { show: false, note: "" };
50
+ }
51
+ if (previewed < MAX_PREVIEWED_FAILURES) {
52
+ previewed++;
53
+ return { show: true, note: "" };
54
+ }
55
+ return { show: false, note: ` (preview omitted: ${logsCommand(result, check.name)})` };
56
+ };
57
+ }
34
58
  export function verdict(result) {
35
59
  return result.status === "passed"
36
60
  ? "PASS"
package/dist/runner.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { constants } from "node:os";
2
2
  import path from "node:path";
3
3
  import { formatSeconds } from "./format.js";
4
+ import { LastLine } from "./preview.js";
4
5
  import { execCheck } from "./process.js";
5
6
  import { createRun, logPath, pruneRuns, readLatest, releaseRun, writeRunResult } from "./runs.js";
6
7
  import { schedule } from "./scheduler.js";
@@ -47,6 +48,7 @@ export async function runVerification(options) {
47
48
  selection,
48
49
  selected: selected.map((check) => check.name),
49
50
  concurrency: options.concurrency,
51
+ dependsOn: Object.fromEntries(selected.map((check) => [check.name, check.dependsOn])),
50
52
  });
51
53
  const env = { ...(options.env ?? process.env), VERIFIKA: "1", VERIFIKA_RUN_ID: runId };
52
54
  let firstFailure = null;
@@ -54,6 +56,11 @@ export async function runVerification(options) {
54
56
  const check = byName.get(task.name);
55
57
  reporter.onCheckStart?.(check.name);
56
58
  const log = logPath(runId, check.name);
59
+ const onCheckOutput = reporter.onCheckOutput?.bind(reporter);
60
+ const followers = onCheckOutput && {
61
+ stdout: new LastLine((line) => onCheckOutput(check.name, line)),
62
+ stderr: new LastLine((line) => onCheckOutput(check.name, line)),
63
+ };
57
64
  const outcome = await execCheck({
58
65
  command: check.command,
59
66
  cwd: root,
@@ -62,6 +69,7 @@ export async function runVerification(options) {
62
69
  logFile: path.join(root, log),
63
70
  previewLines: options.previewLines,
64
71
  signal,
72
+ ...(followers ? { onOutput: (stream, text) => followers[stream].write(text) } : {}),
65
73
  });
66
74
  const checkResult = completedResult(results.get(check.name), check, outcome, log);
67
75
  results.set(check.name, checkResult);
@@ -172,7 +180,12 @@ function completedResult(base, check, outcome, log) {
172
180
  }
173
181
  function notRunResult(base, outcome, interrupted, firstFailure) {
174
182
  if (outcome.state === "blocked") {
175
- return { ...base, status: "blocked", reason: `dependency ${outcome.dependency} did not pass` };
183
+ return {
184
+ ...base,
185
+ status: "blocked",
186
+ reason: `dependency ${outcome.dependency} did not pass`,
187
+ blockedBy: outcome.dependency,
188
+ };
176
189
  }
177
190
  if (outcome.state !== "not_started")
178
191
  return null;
package/dist/runs.js CHANGED
@@ -155,10 +155,49 @@ function isProcessAlive(pid) {
155
155
  return error.code === "EPERM";
156
156
  }
157
157
  }
158
+ /**
159
+ * Estimates the time saved by skipping `names`: each check's duration in the most
160
+ * recent retained run where it ran to completion (passed or failed).
161
+ */
162
+ export async function estimateSkipped(root, names) {
163
+ const found = new Map();
164
+ let runs = [];
165
+ try {
166
+ runs = (await readdir(path.join(root, STATE_DIR, RUNS_DIR)))
167
+ .filter((name) => RUN_ID_PATTERN.test(name))
168
+ .sort()
169
+ .reverse();
170
+ }
171
+ catch {
172
+ // No runs yet: nothing is known.
173
+ }
174
+ const wanted = new Set(names);
175
+ for (const id of runs) {
176
+ if (found.size === wanted.size)
177
+ break;
178
+ let run;
179
+ try {
180
+ run = JSON.parse(await readFile(path.join(runDirectory(root, id), RESULT_FILE), "utf8"));
181
+ }
182
+ catch {
183
+ continue;
184
+ }
185
+ for (const check of run.checks ?? []) {
186
+ if (wanted.has(check.name) &&
187
+ !found.has(check.name) &&
188
+ (check.status === "passed" || check.status === "failed") &&
189
+ typeof check.durationMs === "number") {
190
+ found.set(check.name, check.durationMs);
191
+ }
192
+ }
193
+ }
194
+ const knownMs = [...found.values()].reduce((total, ms) => total + ms, 0);
195
+ return { knownMs, unknown: wanted.size - found.size };
196
+ }
158
197
  export async function readRun(root, runId) {
159
198
  const id = runId ?? (await readLatest(root));
160
199
  if (id === null) {
161
- throw new VerifikaError("no verification runs recorded yet", "Run `verifika` first.");
200
+ throw new VerifikaError("no verification runs recorded yet", "Run `vfk` first.");
162
201
  }
163
202
  if (!RUN_ID_PATTERN.test(id))
164
203
  throw new VerifikaError(`invalid run id "${id}"`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "verifika",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "Deterministic, agent-friendly repository verification: run the checks your changes need, concurrently, with minimal output.",
5
5
  "keywords": [
6
6
  "verify",
@@ -51,8 +51,8 @@
51
51
  "test": "vitest run",
52
52
  "test:pack": "node scripts/pack-smoke.mjs",
53
53
  "check:skill": "node scripts/check-skill.mjs",
54
- "verifika": "node src/cli.ts",
55
- "verify": "node src/cli.ts --all",
54
+ "verify": "node src/cli.ts",
55
+ "verify:ci": "node src/cli.ts --all",
56
56
  "prepack": "npm run build"
57
57
  },
58
58
  "dependencies": {
@@ -10,37 +10,37 @@ Verifika runs a repository's configured checks (lint, typecheck, tests, build, .
10
10
 
11
11
  ## Is it set up here?
12
12
 
13
- Look for `verifika.config.json` in the repository root (or the package directory in a monorepo). Its `checks` object lists every check name and command. Run the CLI the way the repository does: `npx verifika` when it is a devDependency, `verifika` when installed globally, or a documented wrapper script such as `npm run verify`. The examples below write `verifika`.
13
+ Look for `verifika.config.json` in the repository root (or the package directory in a monorepo). Its `checks` object lists every check name and command. Run the CLI the way the repository does: `npx vfk` when it is a devDependency, `vfk` when installed globally, or the repository's scripts. Projects set up with the init command have `npm run verify` (runs `vfk`: selective) and `npm run verify:ci` (runs `vfk --all`: the full gate). `npm run verify` is not the full gate. `vfk` is short for `verifika`; both commands are identical. The examples below write `vfk`.
14
14
 
15
15
  ## Workflow
16
16
 
17
17
  1. **While implementing**, after a meaningful change:
18
18
 
19
19
  ```sh
20
- verifika
20
+ vfk
21
21
  ```
22
22
 
23
- Runs only the checks matching files changed since the configured `changes.base` (by default `HEAD`: your uncommitted and untracked changes). If the environment sets `CI` (some agent sandboxes do), plain `verifika` runs everything; use `verifika --changed` for a selective run there.
23
+ Runs only the checks matching files changed since the configured `changes.base` (by default `HEAD`: your uncommitted and untracked changes). If the environment sets `CI` (some agent sandboxes do), plain `vfk` runs everything; use `vfk --changed` for a selective run there.
24
24
 
25
25
  2. **To see what would run and why**, without running anything:
26
26
 
27
27
  ```sh
28
- verifika plan
28
+ vfk plan
29
29
  ```
30
30
 
31
31
  3. **To rerun one check** while fixing it (its dependencies run too):
32
32
 
33
33
  ```sh
34
- verifika --check typecheck
34
+ vfk --check typecheck
35
35
  ```
36
36
 
37
- 4. **Before claiming the task is complete, committing, or opening a pull request**, always run the full suite:
37
+ 4. **Before claiming the task is complete, committing, or opening a pull request**, always run the full suite (`npm run verify:ci` where that script exists):
38
38
 
39
39
  ```sh
40
- verifika --all
40
+ vfk --all
41
41
  ```
42
42
 
43
- 5. **To verify a whole branch** rather than just uncommitted work: `verifika --changed-since origin/main` (compares against the merge-base, plus uncommitted and untracked files). Useful for review; it does not replace `verifika --all` before delivery.
43
+ 5. **To verify a whole branch** rather than just uncommitted work: `vfk --changed-since origin/main` (compares against the merge-base, plus uncommitted and untracked files). Useful for review; it does not replace `vfk --all` before delivery.
44
44
 
45
45
  Wait for the command to finish. Long suites can take minutes; allow enough time and never report a result you have not seen.
46
46
 
@@ -59,7 +59,22 @@ On failure, one line per problem, a short preview, and the command for full logs
59
59
  FAIL 3/4 (3.2s) full run (--all)
60
60
  typecheck: exit 2
61
61
  src/cart.ts(14,7): error TS2322: Type 'string' is not assignable to type 'number'.
62
- Run: verifika logs typecheck
62
+ Run: vfk logs typecheck
63
+ ```
64
+
65
+ Previews are shown for at most three failed checks. Any further failed check says where its output is instead:
66
+
67
+ ```text
68
+ FAIL 1/5 (9.8s) full run (--all)
69
+ format: exit 1
70
+ [warn] src/cart.ts
71
+ lint: exit 1
72
+ /repo/src/cart.ts
73
+ 12:9 error 'total' is assigned a value but never used no-unused-vars
74
+ typecheck: exit 2
75
+ src/cart.ts(14,7): error TS2322: Type 'string' is not assignable to type 'number'.
76
+ test: exit 1 (preview omitted: vfk logs test)
77
+ Run: vfk logs --failed
63
78
  ```
64
79
 
65
80
  | Exit code | Meaning |
@@ -75,18 +90,18 @@ Check statuses: `passed`, `failed`, `timed_out`, `error` (could not start, or le
75
90
  ## When verification fails
76
91
 
77
92
  1. Read the failing check names and previews first; they usually identify the problem.
78
- 2. If you need more, read only the failing logs: `verifika logs typecheck`, or `verifika logs --failed` for all of them. For long logs use `verifika logs typecheck --tail 80`.
79
- 3. Fix the root cause in the code. Then rerun the failed check (`verifika --check typecheck`), and finish with `verifika --all`.
93
+ 2. If you need more, read only the failing logs: `vfk logs typecheck`, or `vfk logs --failed` for all of them. For long logs use `vfk logs typecheck --tail 80`.
94
+ 3. Fix the root cause in the code. Then rerun the failed check (`vfk --check typecheck`), and finish with `vfk --all`.
80
95
  4. `blocked` checks never ran; fix the dependency they name and rerun.
81
96
  5. If a failure is unrelated to your change (for example it also fails on the base branch), report it with evidence instead of working around it.
82
97
 
83
98
  ## Rules
84
99
 
85
100
  - Do not claim success unless you saw `PASS` and exit code 0 from a finished run. Say what ran: "selective verification passed (2 checks)" is not "all checks pass".
86
- - A selective `PASS` only covers the checks it selected. Skipped checks were not verified. Run `verifika --all` before delivery, every time.
101
+ - A selective `PASS` only covers the checks it selected. Skipped checks were not verified. Run `vfk --all` (or `npm run verify:ci`) before delivery, every time; `npm run verify` is selective and does not count.
87
102
  - `PASS 0/0` means nothing matched your changes and nothing was verified.
88
- - Do not read logs of passing checks or use `verifika logs` after a `PASS`; there is nothing to diagnose.
103
+ - Do not read logs of passing checks or use `vfk logs` after a `PASS`; there is nothing to diagnose.
89
104
  - Do not edit `verifika.config.json`, remove checks, narrow `paths`, raise timeouts, or pass `--check` to dodge a failing check, unless the user asked for that change.
90
105
  - Exit code 2 is not a code failure: read the message (bad flag, unknown check, invalid config, missing git ref) and fix the invocation, or report it.
91
106
  - Do not raise concurrency (`-j`) beyond what the config sets: the project chose it, and checks that share build outputs fail spuriously when run together.
92
- - Prefer the default agent output. Use `verifika --mode json` only when a script needs structured results; avoid `verifika --mode human` in agent sessions.
107
+ - Prefer the default agent output. Use `vfk --mode json` only when a script needs structured results; avoid `vfk --mode human` in agent sessions.