@blzsky/variant 0.2.1 → 0.3.0-canary.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.
Files changed (58) hide show
  1. package/README.md +222 -85
  2. package/dist/{build-3DSBMMI3.js → build-GCUFPPIB.js} +11 -10
  3. package/dist/check-AKYABBIV.js +1459 -0
  4. package/dist/{chunk-43VLELTL.js → chunk-2UVLKBLV.js} +1 -1
  5. package/dist/chunk-34VGKPEM.js +59 -0
  6. package/dist/chunk-BNACT47L.js +590 -0
  7. package/dist/chunk-C3NZIFW6.js +174 -0
  8. package/dist/{chunk-ZH2PQUNL.js → chunk-EFCMBBBT.js} +72 -110
  9. package/dist/chunk-H55GYSQ6.js +102 -0
  10. package/dist/chunk-HL65IPEQ.js +167 -0
  11. package/dist/chunk-IHXJ72XA.js +31 -0
  12. package/dist/{chunk-FZEXZBXB.js → chunk-JEJRXPCU.js} +41 -9
  13. package/dist/chunk-LUTP2TVU.js +94 -0
  14. package/dist/{chunk-7OPTWU6E.js → chunk-MKUYZIXF.js} +1 -1
  15. package/dist/chunk-MNYKLWRC.js +377 -0
  16. package/dist/{chunk-Q7RLUZSN.js → chunk-NJSTAWDM.js} +10 -3
  17. package/dist/{chunk-26DHEXGS.js → chunk-NOC53Z5Q.js} +1 -1
  18. package/dist/chunk-O6FCFZDF.js +127 -0
  19. package/dist/{chunk-6BXSZPHC.js → chunk-OON6O4S5.js} +99 -6
  20. package/dist/{chunk-FA64KWJ2.js → chunk-TPMHY4L6.js} +16 -31
  21. package/dist/chunk-TRUK3TJF.js +575 -0
  22. package/dist/chunk-WTXWEEI2.js +21 -0
  23. package/dist/{chunk-RC23JQKI.js → chunk-WULBOSG5.js} +15 -2
  24. package/dist/chunk-X3VVGTEV.js +268 -0
  25. package/dist/{chunk-TCISS2AS.js → chunk-XXMD4NC5.js} +120 -25
  26. package/dist/cli.js +89 -30
  27. package/dist/{diff-HSJBZFMC.js → diff-ST5KHT7P.js} +5 -5
  28. package/dist/{doctor-B7MHFIYJ.js → doctor-OEIY4Y5X.js} +41 -8
  29. package/dist/{env-I7JZRWDN.js → env-EQAOZCFR.js} +8 -9
  30. package/dist/git-VGHUF2SY.js +30 -0
  31. package/dist/impact-AXLZTLVT.js +266 -0
  32. package/dist/index.js +2 -2
  33. package/dist/init-QCOZWLOO.js +793 -0
  34. package/dist/{insight-JQLTGFYB.js → insight-KRWT6Z47.js} +9 -10
  35. package/dist/pr-7JXUC5BQ.js +710 -0
  36. package/dist/{run-C6VAQOVM.js → run-UOPS5FRU.js} +11 -10
  37. package/dist/stryker-host.d.ts +2 -0
  38. package/dist/stryker-host.js +135 -0
  39. package/dist/stryker-plugins.d.ts +33 -0
  40. package/dist/stryker-plugins.js +142 -0
  41. package/dist/{symbol-graph-E5GXKDFZ.js → symbol-graph-GYOI4GU4.js} +3 -3
  42. package/dist/{tsconfig-paths-E3UEDGXZ.js → tsconfig-paths-SV3PULCW.js} +3 -1
  43. package/dist/vitest.cjs +629 -0
  44. package/dist/vitest.d.cts +181 -0
  45. package/dist/vitest.d.ts +181 -0
  46. package/dist/vitest.js +358 -0
  47. package/dist/workspace-IQFLR3RY.js +65 -0
  48. package/package.json +17 -3
  49. package/dist/check-SIQJU6Z2.js +0 -44
  50. package/dist/chunk-DRMASVYN.js +0 -25
  51. package/dist/chunk-KSHWC2XN.js +0 -78
  52. package/dist/chunk-QRPI6TJE.js +0 -68
  53. package/dist/chunk-VYWPQKMS.js +0 -88
  54. package/dist/chunk-XTYVVUWW.js +0 -279
  55. package/dist/impact-PRZ6HEP5.js +0 -735
  56. package/dist/init-O2BY3FOY.js +0 -158
  57. package/dist/pr-Y724QAHE.js +0 -165
  58. package/dist/workspace-HRTFGWKN.js +0 -186
@@ -0,0 +1,174 @@
1
+ // src/core/errors.ts
2
+ var VariantError = class extends Error {
3
+ constructor(code, message, options) {
4
+ super(message, options);
5
+ this.code = code;
6
+ this.name = "VariantError";
7
+ }
8
+ code;
9
+ hint;
10
+ /** Lines of evidence printed between the message and the hint. */
11
+ details;
12
+ /**
13
+ * Why the task that produced this failure was running. Attached by the
14
+ * runner so the failure and its reason travel together; absent on errors
15
+ * belonging to no task (config, CLI usage) and on successful runs.
16
+ *
17
+ * This says nothing about why the task FAILED — see `RunReason`.
18
+ */
19
+ provenance;
20
+ };
21
+ var ConfigError = class extends VariantError {
22
+ constructor(message, options) {
23
+ super("CONFIG_ERROR", message, options);
24
+ this.hint = "Run `variant doctor` to diagnose configuration issues.";
25
+ }
26
+ };
27
+ var CycleError = class extends VariantError {
28
+ constructor(cycle) {
29
+ super("CYCLE_ERROR", `Circular dependency detected: ${cycle.join(" -> ")}`);
30
+ this.cycle = cycle;
31
+ this.hint = "Remove or reorder `dependsOn` entries to break the cycle.";
32
+ }
33
+ cycle;
34
+ };
35
+ var TaskExecutionError = class extends VariantError {
36
+ constructor(task, exitCode, message, options) {
37
+ super(
38
+ "TASK_EXECUTION_ERROR",
39
+ message ?? `Task "${task}" failed with exit code ${exitCode}`,
40
+ options
41
+ );
42
+ this.task = task;
43
+ this.exitCode = exitCode;
44
+ this.hint = `Check the output above, fix the failing command in task "${task}", then re-run.`;
45
+ }
46
+ task;
47
+ exitCode;
48
+ };
49
+ var CacheError = class extends VariantError {
50
+ constructor(message, options) {
51
+ super("CACHE_ERROR", message, options);
52
+ this.hint = "Delete `.variant/cache/` and retry.";
53
+ }
54
+ };
55
+ var GraphError = class extends VariantError {
56
+ constructor(message, options) {
57
+ super("GRAPH_ERROR", message, options);
58
+ this.hint = "Delete `.variant/graph/` and retry.";
59
+ }
60
+ };
61
+ var GitRefError = class extends VariantError {
62
+ constructor(message) {
63
+ super("GIT_REF_ERROR", message);
64
+ this.hint = "Check the ref with `git rev-parse --verify <ref>`. In CI, check out with `fetch-depth: 0` and pass a remote-tracking ref such as `origin/main`.";
65
+ }
66
+ };
67
+ var NoBaseCommitError = class extends VariantError {
68
+ constructor() {
69
+ super(
70
+ "NO_BASE_COMMIT",
71
+ "This push has no previous commit to compare against"
72
+ );
73
+ this.hint = "The first push of a branch, or a force-push whose old head is gone. Pass --base to compare against a commit of your choice.";
74
+ }
75
+ };
76
+ var ImpactReportError = class extends VariantError {
77
+ constructor(message) {
78
+ super("IMPACT_REPORT_ERROR", `Could not read the test report: ${message}`);
79
+ this.hint = "Pass a Vitest `--reporter=json --outputFile=<path>` or Jest `--json --outputFile=<path>` report.";
80
+ }
81
+ };
82
+ var NoTestFilesError = class extends VariantError {
83
+ constructor() {
84
+ super(
85
+ "NO_TEST_FILES",
86
+ "No test files found, so there is nothing to predict"
87
+ );
88
+ this.hint = "variant finds TypeScript tests named `*.test.ts` or `*.spec.ts` (also `.tsx`, `.mts`, `.cts`) or placed under `__tests__/`. JavaScript files are not indexed.";
89
+ }
90
+ };
91
+ var NoChangeBaseError = class extends VariantError {
92
+ constructor(message) {
93
+ super("NO_CHANGE_BASE", message);
94
+ this.hint = "Pass --base <ref> to say what to compare against, for example `--base HEAD~1` or `--base origin/main`.";
95
+ }
96
+ };
97
+ var DID_NOT_LOAD = " (did not load)";
98
+ function baselineMessage(failingTests) {
99
+ const files = failingTests.filter((test) => test.name.endsWith(DID_NOT_LOAD));
100
+ const tests = failingTests.length - files.length;
101
+ const parts = [
102
+ ...files.length > 0 ? [
103
+ `${files.length} test file${files.length === 1 ? "" : "s"} did not load`
104
+ ] : [],
105
+ ...tests > 0 ? [`${tests} test${tests === 1 ? " fails" : "s fail"}`] : []
106
+ ];
107
+ return parts.length === 0 ? "The tests fail before any mutation, so no result could be trusted" : `${parts.join(" and ")} before any mutation, so no result could be trusted`;
108
+ }
109
+ var BaselineFailedError = class extends VariantError {
110
+ constructor(failingTests, output) {
111
+ super("BASELINE_FAILED", baselineMessage(failingTests));
112
+ this.failingTests = failingTests;
113
+ this.output = output;
114
+ this.details = failingTests.slice(0, 10).map(
115
+ (test) => test.message === "" ? test.name : `${test.name}: ${test.message}`
116
+ );
117
+ this.hint = "Make the tests pass first. If `vitest run` already passes outside variant, the copy variant runs them in is the cause: a path outside this directory (such as a tsconfig `extends`), git used by a test, or an absolute path.";
118
+ }
119
+ failingTests;
120
+ output;
121
+ };
122
+ var CheckBackendError = class extends VariantError {
123
+ constructor(message, output) {
124
+ super("CHECK_BACKEND", message);
125
+ this.output = output;
126
+ const lines = output.split(/\r?\n/).filter((line) => line.trim() !== "");
127
+ this.details = lines.slice(-12);
128
+ this.hint = lines.length > 0 ? "Above is the engine's last output. If the tests pass with `vitest run`, please report this with it." : "If the tests pass with `vitest run`, please report this.";
129
+ }
130
+ output;
131
+ };
132
+ var CheckWorkspaceRootError = class extends VariantError {
133
+ constructor() {
134
+ super(
135
+ "CHECK_WORKSPACE_ROOT",
136
+ "`variant check` runs from a package directory, not a workspace root"
137
+ );
138
+ this.hint = "cd into the package whose code changed and run `variant check` there.";
139
+ }
140
+ };
141
+ var CheckRunnerError = class extends VariantError {
142
+ constructor(found) {
143
+ super(
144
+ "CHECK_RUNNER",
145
+ found.kind === "jest" ? `This package tests with Jest (${found.evidence}); \`variant check\` runs Vitest only` : "No Vitest here: no `vitest` dependency, script or config in this package"
146
+ );
147
+ this.hint = found.kind === "jest" ? "Jest support is not built yet. The pull request report (`variant pr report`) and `variant impact` do work with Jest; docs/check.md says what `check` supports." : "Run it from the package that runs Vitest, or pass --vitest-config <file>.";
148
+ }
149
+ };
150
+ var CliUsageError = class extends VariantError {
151
+ constructor(message) {
152
+ super("CLI_USAGE", message);
153
+ this.hint = "Run `variant --help` for usage information.";
154
+ }
155
+ };
156
+
157
+ export {
158
+ VariantError,
159
+ ConfigError,
160
+ CycleError,
161
+ TaskExecutionError,
162
+ CacheError,
163
+ GraphError,
164
+ GitRefError,
165
+ NoBaseCommitError,
166
+ ImpactReportError,
167
+ NoTestFilesError,
168
+ NoChangeBaseError,
169
+ BaselineFailedError,
170
+ CheckBackendError,
171
+ CheckWorkspaceRootError,
172
+ CheckRunnerError,
173
+ CliUsageError
174
+ };
@@ -1,3 +1,10 @@
1
+ import {
2
+ buildGraph
3
+ } from "./chunk-H55GYSQ6.js";
4
+ import {
5
+ changedFilesToPackages,
6
+ getChangedFiles
7
+ } from "./chunk-MKUYZIXF.js";
1
8
  import {
2
9
  detectProject,
3
10
  genericAdapter,
@@ -5,31 +12,24 @@ import {
5
12
  nextPlugin,
6
13
  viteAdapter
7
14
  } from "./chunk-V5CTGXMM.js";
8
- import {
9
- loadConfig
10
- } from "./chunk-26DHEXGS.js";
11
- import {
12
- getColors
13
- } from "./chunk-YYGCSKIQ.js";
14
- import {
15
- changedFilesToPackages,
16
- getChangedFiles
17
- } from "./chunk-7OPTWU6E.js";
18
15
  import {
19
16
  computeAffectedPackages,
20
17
  loadPackageGraph,
21
18
  tasksFromPackageGraph
22
19
  } from "./chunk-3E5LBU3O.js";
23
20
  import {
24
- errorLines
21
+ errorLines,
22
+ lines
25
23
  } from "./chunk-6J4XDG44.js";
24
+ import {
25
+ loadConfig
26
+ } from "./chunk-NOC53Z5Q.js";
27
+ import {
28
+ getColors
29
+ } from "./chunk-YYGCSKIQ.js";
26
30
  import {
27
31
  getPrinter
28
32
  } from "./chunk-EISTWXDW.js";
29
- import {
30
- ConfigError,
31
- CycleError
32
- } from "./chunk-VYWPQKMS.js";
33
33
 
34
34
  // src/adapters/frameworks/plugin.ts
35
35
  var wrapFrameworkAsPlugin = (a) => ({
@@ -133,100 +133,6 @@ function insertSorted(arr, item, priorityOf) {
133
133
  arr.splice(lo, 0, item);
134
134
  }
135
135
 
136
- // src/core/graph/dag.ts
137
- var TaskGraph = class {
138
- tasks = /* @__PURE__ */ new Set();
139
- deps = /* @__PURE__ */ new Map();
140
- // task -> set of tasks it depends on
141
- levelCache = /* @__PURE__ */ new Map();
142
- addTask(name) {
143
- if (!this.tasks.has(name)) {
144
- this.tasks.add(name);
145
- this.deps.set(name, /* @__PURE__ */ new Set());
146
- this.levelCache.clear();
147
- }
148
- }
149
- addDependency(task, dependsOn) {
150
- this.addTask(task);
151
- this.addTask(dependsOn);
152
- const taskDeps = this.deps.get(task);
153
- taskDeps?.add(dependsOn);
154
- this.levelCache.clear();
155
- }
156
- getDependencies(task) {
157
- return this.deps.get(task) ?? /* @__PURE__ */ new Set();
158
- }
159
- toLevels(target) {
160
- const cached = this.levelCache.get(target);
161
- if (cached !== void 0) {
162
- return cached.map((level) => [...level]);
163
- }
164
- if (!this.tasks.has(target)) {
165
- throw new ConfigError(`Task "${target}" not found in graph`);
166
- }
167
- const subgraph = /* @__PURE__ */ new Set();
168
- const stack = [target];
169
- while (stack.length) {
170
- const node = stack.pop();
171
- if (node === void 0) continue;
172
- if (subgraph.has(node)) continue;
173
- subgraph.add(node);
174
- for (const dep of this.deps.get(node) ?? []) {
175
- stack.push(dep);
176
- }
177
- }
178
- const inDegree = /* @__PURE__ */ new Map();
179
- for (const node of subgraph) inDegree.set(node, 0);
180
- for (const node of subgraph) {
181
- for (const dep of this.deps.get(node) ?? []) {
182
- if (subgraph.has(dep)) {
183
- inDegree.set(dep, (inDegree.get(dep) ?? 0) + 1);
184
- }
185
- }
186
- }
187
- const levels = [];
188
- let queue = [...subgraph].filter((n) => inDegree.get(n) === 0);
189
- let processed = 0;
190
- while (queue.length) {
191
- levels.push([...queue].sort());
192
- processed += queue.length;
193
- const next = [];
194
- for (const node of queue) {
195
- for (const dep of this.deps.get(node) ?? []) {
196
- if (subgraph.has(dep)) {
197
- const newDeg = (inDegree.get(dep) ?? 0) - 1;
198
- inDegree.set(dep, newDeg);
199
- if (newDeg === 0) next.push(dep);
200
- }
201
- }
202
- }
203
- queue = next;
204
- }
205
- if (processed < subgraph.size) {
206
- const remaining = [...subgraph].filter((n) => (inDegree.get(n) ?? 0) > 0);
207
- throw new CycleError(remaining);
208
- }
209
- const result = levels.reverse();
210
- this.levelCache.set(
211
- target,
212
- result.map((level) => [...level])
213
- );
214
- return result;
215
- }
216
- };
217
-
218
- // src/core/graph/planner.ts
219
- function buildGraph(config) {
220
- const graph = new TaskGraph();
221
- for (const [name, task] of Object.entries(config.tasks)) {
222
- graph.addTask(name);
223
- task.dependsOn?.forEach((dep) => {
224
- graph.addDependency(name, dep);
225
- });
226
- }
227
- return graph;
228
- }
229
-
230
136
  // src/core/plugins/registry.ts
231
137
  var swallowErrors = () => {
232
138
  };
@@ -460,9 +366,65 @@ function toRunOptions(ctx, overrides = {}) {
460
366
  };
461
367
  }
462
368
 
369
+ // src/cli/render/insight.ts
370
+ function statusLabel(result) {
371
+ const colors = getColors();
372
+ if (result.skipped) return colors.dim("SKIP");
373
+ return result.cacheHit ? colors.green("HIT") : colors.red("MISS");
374
+ }
375
+ function renderInsights(summary, printer = getPrinter()) {
376
+ const colors = getColors();
377
+ const results = summary.lastRunResults;
378
+ if (results.length === 0) {
379
+ const entries = Object.entries(summary.cachedStats);
380
+ if (entries.length === 0) {
381
+ lines(printer, "No cached task data yet. Run a task first.");
382
+ return;
383
+ }
384
+ lines(printer, "", "Cached task history:");
385
+ for (const [task, entry] of entries) {
386
+ const duration = entry.lastDurationMs !== void 0 ? `${entry.lastDurationMs}ms` : "unknown";
387
+ const ran = new Date(entry.lastRun).toLocaleString();
388
+ lines(
389
+ printer,
390
+ ` ${task.padEnd(20)} last run: ${ran} duration: ${duration}`
391
+ );
392
+ }
393
+ return;
394
+ }
395
+ const nameWidth = Math.max(...results.map((r) => r.task.length), 4);
396
+ const header = `${"TASK".padEnd(nameWidth)} ${"DURATION".padEnd(10)} STATUS`;
397
+ lines(printer, "", colors.bold(header), "-".repeat(header.length));
398
+ for (const result of results) {
399
+ const duration = result.skipped || result.cacheHit ? "-" : `${result.durationMs}ms`;
400
+ lines(
401
+ printer,
402
+ `${result.task.padEnd(nameWidth)} ${duration.padEnd(10)} ${statusLabel(result)}`
403
+ );
404
+ }
405
+ const percent = (summary.cacheHitRate * 100).toFixed(0);
406
+ lines(
407
+ printer,
408
+ "",
409
+ colors.dim(
410
+ `Total: ${summary.totalDurationMs}ms Cache hit rate: ${percent}%`
411
+ )
412
+ );
413
+ }
414
+ function renderEnv(pm, runtime, framework, printer = getPrinter()) {
415
+ lines(
416
+ printer,
417
+ `Package Manager : ${pm}`,
418
+ `Runtime : ${runtime.primary} (fallback: ${runtime.fallback})`,
419
+ `Framework : ${framework ?? "none detected"}`
420
+ );
421
+ }
422
+
463
423
  export {
464
424
  runScheduled,
465
425
  recordCacheMiss,
466
426
  createContext,
467
- toRunOptions
427
+ toRunOptions,
428
+ renderInsights,
429
+ renderEnv
468
430
  };
@@ -0,0 +1,102 @@
1
+ import {
2
+ ConfigError,
3
+ CycleError
4
+ } from "./chunk-C3NZIFW6.js";
5
+
6
+ // src/core/graph/dag.ts
7
+ var TaskGraph = class {
8
+ tasks = /* @__PURE__ */ new Set();
9
+ deps = /* @__PURE__ */ new Map();
10
+ // task -> set of tasks it depends on
11
+ levelCache = /* @__PURE__ */ new Map();
12
+ addTask(name) {
13
+ if (!this.tasks.has(name)) {
14
+ this.tasks.add(name);
15
+ this.deps.set(name, /* @__PURE__ */ new Set());
16
+ this.levelCache.clear();
17
+ }
18
+ }
19
+ addDependency(task, dependsOn) {
20
+ this.addTask(task);
21
+ this.addTask(dependsOn);
22
+ const taskDeps = this.deps.get(task);
23
+ taskDeps?.add(dependsOn);
24
+ this.levelCache.clear();
25
+ }
26
+ getDependencies(task) {
27
+ return this.deps.get(task) ?? /* @__PURE__ */ new Set();
28
+ }
29
+ toLevels(target) {
30
+ const cached = this.levelCache.get(target);
31
+ if (cached !== void 0) {
32
+ return cached.map((level) => [...level]);
33
+ }
34
+ if (!this.tasks.has(target)) {
35
+ throw new ConfigError(`Task "${target}" not found in graph`);
36
+ }
37
+ const subgraph = /* @__PURE__ */ new Set();
38
+ const stack = [target];
39
+ while (stack.length) {
40
+ const node = stack.pop();
41
+ if (node === void 0) continue;
42
+ if (subgraph.has(node)) continue;
43
+ subgraph.add(node);
44
+ for (const dep of this.deps.get(node) ?? []) {
45
+ stack.push(dep);
46
+ }
47
+ }
48
+ const inDegree = /* @__PURE__ */ new Map();
49
+ for (const node of subgraph) inDegree.set(node, 0);
50
+ for (const node of subgraph) {
51
+ for (const dep of this.deps.get(node) ?? []) {
52
+ if (subgraph.has(dep)) {
53
+ inDegree.set(dep, (inDegree.get(dep) ?? 0) + 1);
54
+ }
55
+ }
56
+ }
57
+ const levels = [];
58
+ let queue = [...subgraph].filter((n) => inDegree.get(n) === 0);
59
+ let processed = 0;
60
+ while (queue.length) {
61
+ levels.push([...queue].sort());
62
+ processed += queue.length;
63
+ const next = [];
64
+ for (const node of queue) {
65
+ for (const dep of this.deps.get(node) ?? []) {
66
+ if (subgraph.has(dep)) {
67
+ const newDeg = (inDegree.get(dep) ?? 0) - 1;
68
+ inDegree.set(dep, newDeg);
69
+ if (newDeg === 0) next.push(dep);
70
+ }
71
+ }
72
+ }
73
+ queue = next;
74
+ }
75
+ if (processed < subgraph.size) {
76
+ const remaining = [...subgraph].filter((n) => (inDegree.get(n) ?? 0) > 0);
77
+ throw new CycleError(remaining);
78
+ }
79
+ const result = levels.reverse();
80
+ this.levelCache.set(
81
+ target,
82
+ result.map((level) => [...level])
83
+ );
84
+ return result;
85
+ }
86
+ };
87
+
88
+ // src/core/graph/planner.ts
89
+ function buildGraph(config) {
90
+ const graph = new TaskGraph();
91
+ for (const [name, task] of Object.entries(config.tasks)) {
92
+ graph.addTask(name);
93
+ task.dependsOn?.forEach((dep) => {
94
+ graph.addDependency(name, dep);
95
+ });
96
+ }
97
+ return graph;
98
+ }
99
+
100
+ export {
101
+ buildGraph
102
+ };
@@ -0,0 +1,167 @@
1
+ import {
2
+ appendReconciliation,
3
+ defaultHistoryDir,
4
+ readImpactPredictions
5
+ } from "./chunk-34VGKPEM.js";
6
+ import {
7
+ ImpactReportError
8
+ } from "./chunk-C3NZIFW6.js";
9
+
10
+ // src/core/impact/verify.ts
11
+ import { realpathSync } from "fs";
12
+ import { readFile } from "fs/promises";
13
+ import path from "path";
14
+ function realpathOrSelf(target) {
15
+ try {
16
+ return realpathSync.native(target);
17
+ } catch {
18
+ return target;
19
+ }
20
+ }
21
+ function escapes(relative) {
22
+ return relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative);
23
+ }
24
+ function toRelativePosix(cwd, testPath, realpath) {
25
+ let relative = testPath;
26
+ if (path.isAbsolute(testPath)) {
27
+ relative = path.relative(cwd, testPath);
28
+ if (escapes(relative)) {
29
+ relative = path.relative(realpath(cwd), realpath(testPath));
30
+ }
31
+ }
32
+ return relative.replace(/\\/g, "/");
33
+ }
34
+ function parseTestRun(cwd, raw, realpath = realpathOrSelf) {
35
+ let parsed;
36
+ try {
37
+ parsed = JSON.parse(raw);
38
+ } catch {
39
+ throw new ImpactReportError("it is not valid JSON");
40
+ }
41
+ if (typeof parsed !== "object" || parsed === null) {
42
+ throw new ImpactReportError("it is not a JSON object");
43
+ }
44
+ const results = parsed.testResults;
45
+ if (!Array.isArray(results)) {
46
+ throw new ImpactReportError("it has no `testResults` array");
47
+ }
48
+ const ran = /* @__PURE__ */ new Set();
49
+ const failed = /* @__PURE__ */ new Set();
50
+ for (const entry of results) {
51
+ if (typeof entry !== "object" || entry === null) continue;
52
+ const { name, status } = entry;
53
+ if (typeof name !== "string") continue;
54
+ const file = toRelativePosix(cwd, name, realpath);
55
+ ran.add(file);
56
+ if (status === "failed") failed.add(file);
57
+ }
58
+ return { ran: [...ran].sort(), failed: [...failed].sort() };
59
+ }
60
+ function testRunFrom(cwd, results, realpath = realpathOrSelf) {
61
+ const ran = /* @__PURE__ */ new Set();
62
+ const failed = /* @__PURE__ */ new Set();
63
+ for (const result of results) {
64
+ const file = toRelativePosix(cwd, result.file, realpath);
65
+ ran.add(file);
66
+ if (result.failed) failed.add(file);
67
+ }
68
+ return { ran: [...ran].sort(), failed: [...failed].sort() };
69
+ }
70
+ function reconcile(prediction, failedTests) {
71
+ const selected = new Set(prediction.affectedTests);
72
+ const caught = [];
73
+ const falseSkips = [];
74
+ for (const test of failedTests) {
75
+ if (prediction.selectAll || selected.has(test)) caught.push(test);
76
+ else falseSkips.push(test);
77
+ }
78
+ return {
79
+ caught,
80
+ falseSkips,
81
+ falseSkipRate: failedTests.length === 0 ? 0 : falseSkips.length / failedTests.length
82
+ };
83
+ }
84
+ function selectPrediction(predictions, headSha) {
85
+ if (headSha !== void 0) {
86
+ for (let i = predictions.length - 1; i >= 0; i--) {
87
+ const candidate = predictions[i];
88
+ if (candidate?.headSha === headSha) {
89
+ return { prediction: candidate, matchedBy: "head-sha" };
90
+ }
91
+ }
92
+ return null;
93
+ }
94
+ const latest = predictions[predictions.length - 1];
95
+ return latest === void 0 ? null : { prediction: latest, matchedBy: "most-recent" };
96
+ }
97
+ async function readReport(cwd, reportPath) {
98
+ try {
99
+ return await readFile(path.resolve(cwd, reportPath), "utf8");
100
+ } catch (error) {
101
+ const code = error instanceof Error && "code" in error ? String(error.code) : "";
102
+ throw new ImpactReportError(
103
+ code === "ENOENT" ? `no file at ${reportPath}` : `${reportPath} could not be read${code === "" ? "" : ` (${code})`}`
104
+ );
105
+ }
106
+ }
107
+ async function verifyImpact(cwd, reportPaths, options = {}) {
108
+ const paths = typeof reportPaths === "string" ? [reportPaths] : reportPaths;
109
+ return verifyRun(
110
+ cwd,
111
+ async () => {
112
+ const reports = await Promise.all(
113
+ paths.map((reportPath) => readReport(cwd, reportPath))
114
+ );
115
+ return reports.map((raw) => parseTestRun(cwd, raw));
116
+ },
117
+ options
118
+ );
119
+ }
120
+ async function verifyRun(cwd, runs, options = {}) {
121
+ const historyDir = defaultHistoryDir(cwd);
122
+ const predictions = options.predictions ?? await readImpactPredictions(historyDir);
123
+ const match = selectPrediction(predictions, options.headSha);
124
+ if (match === null) return null;
125
+ const resolved = typeof runs === "function" ? await runs() : Array.isArray(runs) ? runs : [runs];
126
+ const failedTests = [
127
+ ...new Set(resolved.flatMap((run) => run.failed))
128
+ ].sort();
129
+ const ran = new Set(resolved.flatMap((run) => run.ran));
130
+ const selected = new Set(match.prediction.affectedTests);
131
+ const predictedRan = match.prediction.selectAll ? ran.size : [...ran].filter((test) => selected.has(test)).length;
132
+ const { caught, falseSkips, falseSkipRate } = reconcile(
133
+ match.prediction,
134
+ failedTests
135
+ );
136
+ const historyLogged = options.log === false ? true : await appendReconciliation(historyDir, {
137
+ at: (/* @__PURE__ */ new Date()).toISOString(),
138
+ headSha: match.prediction.headSha,
139
+ baseRef: match.prediction.baseRef,
140
+ predictedTests: match.prediction.affectedTests.length,
141
+ totalTests: match.prediction.totalTests,
142
+ failedTests: failedTests.length,
143
+ caught: caught.length,
144
+ falseSkips: falseSkips.length,
145
+ confidence: match.prediction.confidence,
146
+ selectAll: match.prediction.selectAll,
147
+ ranTests: ran.size,
148
+ predictedRan
149
+ });
150
+ return {
151
+ prediction: match.prediction,
152
+ matchedBy: match.matchedBy,
153
+ failedTests,
154
+ caught,
155
+ falseSkips,
156
+ falseSkipRate,
157
+ ranTests: ran.size,
158
+ predictedRan,
159
+ historyLogged
160
+ };
161
+ }
162
+
163
+ export {
164
+ testRunFrom,
165
+ verifyImpact,
166
+ verifyRun
167
+ };
@@ -0,0 +1,31 @@
1
+ // src/cli/visuals/ansi.ts
2
+ var ESC = "\x1B";
3
+ var HIDE_CURSOR = `${ESC}[?25l`;
4
+ var SHOW_CURSOR = `${ESC}[?25h`;
5
+ var CLEAR_LINE = `${ESC}[2K`;
6
+ var SYNC_START = `${ESC}[?2026h`;
7
+ var SYNC_END = `${ESC}[?2026l`;
8
+ function cursorUp(lines) {
9
+ return lines > 0 ? `${ESC}[${lines}A` : "";
10
+ }
11
+ var STYLE_PATTERN = new RegExp(`${ESC}\\[[0-9;]*m`, "g");
12
+ function stripStyles(text) {
13
+ return text.replace(STYLE_PATTERN, "");
14
+ }
15
+ function visibleLength(line) {
16
+ return stripStyles(line).length;
17
+ }
18
+ function fitWidth(line, columns) {
19
+ if (visibleLength(line) <= columns) return line;
20
+ return stripStyles(line).slice(0, Math.max(0, columns - 1));
21
+ }
22
+
23
+ export {
24
+ HIDE_CURSOR,
25
+ SHOW_CURSOR,
26
+ CLEAR_LINE,
27
+ SYNC_START,
28
+ SYNC_END,
29
+ cursorUp,
30
+ fitWidth
31
+ };