@blzsky/variant 0.2.1 → 0.3.0-canary.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +110 -56
  2. package/dist/{build-3DSBMMI3.js → build-GKWNLU4Q.js} +9 -8
  3. package/dist/{check-SIQJU6Z2.js → check-KO5VY67Z.js} +6 -6
  4. package/dist/{chunk-FA64KWJ2.js → chunk-7NC7AO75.js} +15 -28
  5. package/dist/{chunk-VYWPQKMS.js → chunk-ACTY42RE.js} +20 -0
  6. package/dist/{chunk-ZH2PQUNL.js → chunk-CN25SRKD.js} +6 -6
  7. package/dist/{chunk-43VLELTL.js → chunk-CWF6QXEW.js} +1 -1
  8. package/dist/{chunk-7OPTWU6E.js → chunk-DMDAJPFJ.js} +1 -1
  9. package/dist/{chunk-RC23JQKI.js → chunk-DSQDMFQ6.js} +9 -1
  10. package/dist/chunk-H5C4SE73.js +184 -0
  11. package/dist/chunk-IFFZ46YS.js +575 -0
  12. package/dist/chunk-IHXJ72XA.js +31 -0
  13. package/dist/chunk-KCEGA56S.js +670 -0
  14. package/dist/{chunk-FZEXZBXB.js → chunk-MAK3HG46.js} +41 -9
  15. package/dist/chunk-MT4BOAAV.js +167 -0
  16. package/dist/{chunk-TCISS2AS.js → chunk-NFFXYYM6.js} +120 -25
  17. package/dist/{chunk-26DHEXGS.js → chunk-RJBFCURK.js} +1 -1
  18. package/dist/{chunk-6BXSZPHC.js → chunk-RSIOYQOQ.js} +67 -8
  19. package/dist/chunk-SGP5JX6I.js +377 -0
  20. package/dist/chunk-X3VVGTEV.js +268 -0
  21. package/dist/{chunk-Q7RLUZSN.js → chunk-YII4TAKS.js} +10 -3
  22. package/dist/cli.js +62 -27
  23. package/dist/{diff-HSJBZFMC.js → diff-A3TQWWUJ.js} +5 -5
  24. package/dist/{doctor-B7MHFIYJ.js → doctor-6ONS6LKY.js} +4 -4
  25. package/dist/{env-I7JZRWDN.js → env-XF2US3CB.js} +6 -6
  26. package/dist/git-W6N3WMYQ.js +24 -0
  27. package/dist/impact-DPZIAJR4.js +263 -0
  28. package/dist/index.js +2 -2
  29. package/dist/init-VNK5E4H3.js +777 -0
  30. package/dist/{insight-JQLTGFYB.js → insight-LNYVMCAE.js} +7 -7
  31. package/dist/pr-6CYMM2XU.js +707 -0
  32. package/dist/{run-C6VAQOVM.js → run-44PKMOV5.js} +9 -8
  33. package/dist/{symbol-graph-E5GXKDFZ.js → symbol-graph-OB4AJCLW.js} +3 -3
  34. package/dist/{tsconfig-paths-E3UEDGXZ.js → tsconfig-paths-SV3PULCW.js} +3 -1
  35. package/dist/vitest.cjs +629 -0
  36. package/dist/vitest.d.cts +181 -0
  37. package/dist/vitest.d.ts +181 -0
  38. package/dist/vitest.js +356 -0
  39. package/dist/workspace-PIQXG4ZD.js +65 -0
  40. package/package.json +13 -2
  41. package/dist/chunk-DRMASVYN.js +0 -25
  42. package/dist/chunk-KSHWC2XN.js +0 -78
  43. package/dist/chunk-XTYVVUWW.js +0 -279
  44. package/dist/impact-PRZ6HEP5.js +0 -735
  45. package/dist/init-O2BY3FOY.js +0 -158
  46. package/dist/pr-Y724QAHE.js +0 -165
  47. package/dist/workspace-HRTFGWKN.js +0 -186
@@ -0,0 +1,670 @@
1
+ import {
2
+ buildImportGraph,
3
+ computeAffectedFiles,
4
+ importersOfUnindexed,
5
+ readManifests,
6
+ workspaceBlindSpots
7
+ } from "./chunk-IFFZ46YS.js";
8
+ import {
9
+ mapLimit
10
+ } from "./chunk-GDSZFIHM.js";
11
+ import {
12
+ getChangedFiles
13
+ } from "./chunk-DMDAJPFJ.js";
14
+ import {
15
+ loadPackageGraph
16
+ } from "./chunk-3E5LBU3O.js";
17
+ import {
18
+ appendImpactPrediction,
19
+ changeBaseDeps,
20
+ defaultHistoryDir,
21
+ resolveChangeBase
22
+ } from "./chunk-H5C4SE73.js";
23
+ import {
24
+ readPathAliases
25
+ } from "./chunk-X3VVGTEV.js";
26
+ import {
27
+ updateSymbolGraph
28
+ } from "./chunk-NFFXYYM6.js";
29
+ import {
30
+ assertBaseRef,
31
+ createClassifier
32
+ } from "./chunk-MAK3HG46.js";
33
+ import {
34
+ readFileAtRef,
35
+ readFilesAtRef,
36
+ readHeadSha
37
+ } from "./chunk-RSIOYQOQ.js";
38
+ import {
39
+ isStarReexportKey
40
+ } from "./chunk-DSQDMFQ6.js";
41
+ import {
42
+ GitRefError,
43
+ NoTestFilesError
44
+ } from "./chunk-ACTY42RE.js";
45
+
46
+ // src/core/semantic/blast-radius.ts
47
+ import { readFile } from "fs/promises";
48
+ import path from "path";
49
+ var TS_FILE = /\.(?:ts|tsx|mts|cts)$/;
50
+ var JS_FILE = /\.(?:[cm]?js|jsx)$/;
51
+ var COMMONJS_EXPORT = /\bmodule\.exports\b|\bexports\.[A-Za-z_$]|\bexports\[/;
52
+ var MODULE_FILE = /\.(?:[cm]?[jt]sx?|json)$/;
53
+ function isModuleFile(file) {
54
+ return MODULE_FILE.test(file);
55
+ }
56
+ var DECLARATION_FILE = /\.d\.(?:ts|mts|cts)$/;
57
+ var MAX_NOTE_FILES = 3;
58
+ var CLASSIFY_CONCURRENCY = 16;
59
+ function isOutsideWorkspace(file) {
60
+ return file.startsWith("../");
61
+ }
62
+ function isTypeScriptSource(file) {
63
+ return TS_FILE.test(file) && !DECLARATION_FILE.test(file);
64
+ }
65
+ function isSource(file) {
66
+ return isTypeScriptSource(file) || JS_FILE.test(file);
67
+ }
68
+ function isAnalyzable(file) {
69
+ return isSource(file) && !isOutsideWorkspace(file);
70
+ }
71
+ function widenCommonJs(impact, before, after) {
72
+ if (!JS_FILE.test(impact.filePath)) return impact;
73
+ if (impact.classification === "non-impacting") return impact;
74
+ if (!COMMONJS_EXPORT.test(`${before ?? ""}
75
+ ${after ?? ""}`)) return impact;
76
+ return {
77
+ ...impact,
78
+ classification: "breaking",
79
+ propagates: true,
80
+ ungated: true
81
+ };
82
+ }
83
+ function unanalyzedImpact(file) {
84
+ return {
85
+ filePath: file,
86
+ classification: "unanalyzed",
87
+ impactedSymbols: [],
88
+ propagates: false,
89
+ notes: []
90
+ };
91
+ }
92
+ async function batchedReaderFor(cwd, baseRef, relPaths) {
93
+ const batch = await readFilesAtRef(cwd, baseRef, relPaths);
94
+ if (batch === null) return (rel) => readFileAtRef(cwd, baseRef, rel);
95
+ return async (rel) => batch.get(rel) ?? null;
96
+ }
97
+ async function traceBlastRadius(cwd, options = {}) {
98
+ const baseRef = options.baseRef ?? "HEAD~1";
99
+ const changedFiles = options.changedFiles ?? await getChangedFiles({ cwd, baseRef });
100
+ if (changedFiles === null) return null;
101
+ const packageDirs = packageDirsFrom(cwd, options.packageGraph);
102
+ let importGraph = options.importGraph;
103
+ if (importGraph === void 0) {
104
+ const [{ graph: symbolGraph }, pathAliases] = await Promise.all([
105
+ updateSymbolGraph(
106
+ cwd,
107
+ options.graphDir === void 0 ? {} : { graphDir: options.graphDir }
108
+ ),
109
+ options.pathAliases ?? readPathAliases(cwd)
110
+ ]);
111
+ importGraph = buildImportGraph(symbolGraph, {
112
+ packageDirs,
113
+ pathAliases,
114
+ packageExports: packageExportsFrom(options.packageGraph)
115
+ });
116
+ }
117
+ const indexed = importGraph.imports;
118
+ const files = changedFiles.map(toPosix).sort();
119
+ const analyzable = files.filter(isAnalyzable);
120
+ const readBefore = options.readBefore ?? await batchedReaderFor(cwd, baseRef, analyzable);
121
+ const readAfter = options.readAfter ?? (async (rel) => {
122
+ try {
123
+ return await readFile(path.join(cwd, rel), "utf8");
124
+ } catch {
125
+ return null;
126
+ }
127
+ });
128
+ const classify = createClassifier();
129
+ const changed = await mapLimit(files, CLASSIFY_CONCURRENCY, async (file) => {
130
+ if (!isAnalyzable(file)) return unanalyzedImpact(file);
131
+ const [before, after] = await Promise.all([
132
+ readBefore(file),
133
+ readAfter(file)
134
+ ]);
135
+ if (after !== null && !indexed.has(file)) return unanalyzedImpact(file);
136
+ return widenCommonJs(
137
+ toFileImpact(
138
+ await classify({
139
+ filePath: file,
140
+ before: before ?? "",
141
+ after: after ?? ""
142
+ })
143
+ ),
144
+ before,
145
+ after
146
+ );
147
+ });
148
+ const manifests = options.manifests ?? await readManifests(cwd);
149
+ const blindSpots = workspaceBlindSpots({
150
+ externals: importGraph.externals,
151
+ packageNames: new Set(
152
+ (options.packageGraph?.packages ?? []).map((pkg) => pkg.manifest.name)
153
+ ),
154
+ manifests
155
+ });
156
+ return assembleBlastRadius(baseRef, changed, importGraph, {
157
+ packageDirs,
158
+ workspaceNotes: blindSpots.notes,
159
+ undiscoveredPackages: blindSpots.undiscovered,
160
+ packageRoots: [...manifests.keys()],
161
+ ...options.tasks === void 0 ? {} : { tasks: options.tasks }
162
+ });
163
+ }
164
+ function assembleBlastRadius(baseRef, changed, importGraph, options = {}) {
165
+ const notes = /* @__PURE__ */ new Set();
166
+ const repositoryNotes = new Set(options.workspaceNotes);
167
+ const affected = /* @__PURE__ */ new Set();
168
+ const firstHop = /* @__PURE__ */ new Set();
169
+ for (const impact of changed) {
170
+ for (const note of impact.notes) notes.add(note);
171
+ if (impact.classification === "non-impacting") continue;
172
+ affected.add(impact.filePath);
173
+ if (impact.classification === "unanalyzed" || !importGraph.imports.has(impact.filePath)) {
174
+ for (const importer of importersOfUnindexed(
175
+ importGraph,
176
+ impact.filePath
177
+ )) {
178
+ firstHop.add(importer);
179
+ }
180
+ continue;
181
+ }
182
+ if (!impact.propagates) continue;
183
+ const ungated = impact.ungated === true || impact.impactedSymbols.some(isStarReexportKey);
184
+ for (const dependent of importGraph.dependents.get(impact.filePath) ?? []) {
185
+ const edge = importGraph.edges.get(dependent)?.get(impact.filePath);
186
+ if (edge === void 0) {
187
+ firstHop.add(dependent);
188
+ continue;
189
+ }
190
+ if (edge.dynamic) {
191
+ notes.add(
192
+ `${dependent}: dynamic import of ${impact.filePath} \u2014 names unknowable`
193
+ );
194
+ firstHop.add(dependent);
195
+ continue;
196
+ }
197
+ if (ungated || edge.names.has("*")) {
198
+ firstHop.add(dependent);
199
+ continue;
200
+ }
201
+ if (impact.impactedSymbols.some((s) => edge.names.has(s))) {
202
+ firstHop.add(dependent);
203
+ }
204
+ }
205
+ }
206
+ for (const file of computeAffectedFiles(firstHop, importGraph)) {
207
+ affected.add(file);
208
+ }
209
+ const unanalyzed = changed.filter((impact) => impact.classification === "unanalyzed").map((impact) => impact.filePath);
210
+ const outside = unanalyzed.filter(isOutsideWorkspace);
211
+ const inside = unanalyzed.filter((file) => !isOutsideWorkspace(file));
212
+ const unindexed = inside.filter(isSource);
213
+ const notTypeScript = inside.filter((file) => !isSource(file));
214
+ if (notTypeScript.length > 0) {
215
+ const one = notTypeScript.length === 1;
216
+ notes.add(
217
+ `${notTypeScript.length} changed ${one ? "file is" : "files are"} not TypeScript or JavaScript and ${one ? "was" : "were"} not analyzed (${listPaths(notTypeScript)})`
218
+ );
219
+ }
220
+ if (unindexed.length > 0) {
221
+ const one = unindexed.length === 1;
222
+ notes.add(
223
+ `${unindexed.length} changed source ${one ? "file is" : "files are"} not in variant's index (ignored by git, in a build directory, or too large) and ${one ? "was" : "were"} not analyzed (${listPaths(unindexed)})`
224
+ );
225
+ }
226
+ if (outside.length > 0) {
227
+ const one = outside.length === 1;
228
+ notes.add(
229
+ `${outside.length} changed ${one ? "file is" : "files are"} outside the directory variant ran in and ${one ? "was" : "were"} not analyzed (${listPaths(outside)})`
230
+ );
231
+ }
232
+ const withUnresolved = [...affected].sort().flatMap((file) => {
233
+ const specs = importGraph.unresolved.get(file);
234
+ return specs === void 0 || specs.size === 0 ? [] : [`${file} (${[...specs].sort().join(", ")})`];
235
+ });
236
+ if (withUnresolved.length > 0) {
237
+ const one = withUnresolved.length === 1;
238
+ notes.add(
239
+ `${withUnresolved.length} affected ${one ? "file has" : "files have"} unresolved imports: ${listPaths(withUnresolved)}`
240
+ );
241
+ }
242
+ const impacting = changed.filter((impact) => impact.classification !== "non-impacting").map((impact) => impact.filePath);
243
+ if (importGraph.computed.size > 0) {
244
+ const loaders = [...importGraph.computed].sort();
245
+ repositoryNotes.add(
246
+ `${loaders.length} file(s) load a module through a fully computed import() or require() specifier (${listPaths(loaders)}); a change reached only that way selects no tests`
247
+ );
248
+ const touched = new Set(
249
+ impacting.filter(isModuleFile).map((file) => packageRootOf(file, options.packageRoots))
250
+ );
251
+ const near = loaders.filter(
252
+ (loader) => touched.has(packageRootOf(loader, options.packageRoots))
253
+ );
254
+ if (near.length > 0) {
255
+ notes.add(
256
+ `${near.length} file(s) in a package this change touches load a module through a fully computed import() or require() specifier (${listPaths(near)}); a changed file reached only that way selects no tests`
257
+ );
258
+ }
259
+ }
260
+ for (const [name, dir] of options.undiscoveredPackages ?? []) {
261
+ const prefix = dir === "" ? "" : `${dir}/`;
262
+ const inside2 = impacting.filter((file) => file.startsWith(prefix));
263
+ if (inside2.length > 0) {
264
+ const one = inside2.length === 1;
265
+ notes.add(
266
+ `${inside2.length} changed ${one ? "file belongs" : "files belong"} to ${name}, a local package variant did not find as a workspace package (${listPaths(inside2)}); files importing it by name are not reached`
267
+ );
268
+ }
269
+ }
270
+ const packageDirs = options.packageDirs ?? {};
271
+ const affectedPackages = /* @__PURE__ */ new Set();
272
+ for (const file of affected) {
273
+ const pkg = fileToPackage(file, packageDirs);
274
+ if (pkg !== void 0) affectedPackages.add(pkg);
275
+ }
276
+ const affectedTasks = [];
277
+ for (const taskName of Object.keys(options.tasks ?? {})) {
278
+ const sep = taskName.lastIndexOf(":");
279
+ if (sep === -1) continue;
280
+ if (affectedPackages.has(taskName.slice(0, sep))) {
281
+ affectedTasks.push(taskName);
282
+ }
283
+ }
284
+ const noteList = [...notes].sort();
285
+ const confidence = Math.max(
286
+ 0.3,
287
+ Math.round((1 - noteList.length * 0.1) * 100) / 100
288
+ );
289
+ return {
290
+ baseRef,
291
+ changed,
292
+ affectedFiles: [...affected].sort(),
293
+ affectedPackages: [...affectedPackages].sort(),
294
+ affectedTasks: affectedTasks.sort(),
295
+ confidence,
296
+ notes: noteList,
297
+ repositoryNotes: [...repositoryNotes].sort()
298
+ };
299
+ }
300
+ function packageRootOf(file, packageRoots) {
301
+ let best = "";
302
+ for (const root of packageRoots ?? []) {
303
+ if (root !== "" && file.startsWith(`${root}/`) && root.length > best.length) {
304
+ best = root;
305
+ }
306
+ }
307
+ return best;
308
+ }
309
+ function toFileImpact(result) {
310
+ const impactedSymbols = [
311
+ ...result.exportedSymbols.added.filter(isStarReexportKey),
312
+ ...result.exportedSymbols.removed,
313
+ ...result.exportedSymbols.changed.filter((c) => c.kind !== "body").map((c) => c.name)
314
+ ].sort();
315
+ const ungated = result.syntaxErrors || impactedSymbols.some(isStarReexportKey);
316
+ return {
317
+ filePath: result.filePath,
318
+ classification: result.classification,
319
+ impactedSymbols,
320
+ propagates: result.classification === "breaking",
321
+ ...ungated && { ungated: true },
322
+ notes: result.confidenceNotes.map((n) => `${result.filePath}: ${n}`)
323
+ };
324
+ }
325
+ function packageDirsFrom(cwd, packageGraph) {
326
+ const out = {};
327
+ for (const pkg of packageGraph?.packages ?? []) {
328
+ out[pkg.manifest.name] = toPosix(path.relative(cwd, pkg.dir));
329
+ }
330
+ return out;
331
+ }
332
+ function packageExportsFrom(packageGraph) {
333
+ const out = {};
334
+ for (const pkg of packageGraph?.packages ?? []) {
335
+ if (pkg.manifest.exports !== void 0) {
336
+ out[pkg.manifest.name] = pkg.manifest.exports;
337
+ }
338
+ }
339
+ return out;
340
+ }
341
+ function listPaths(items) {
342
+ return items.length > MAX_NOTE_FILES ? `${items.slice(0, MAX_NOTE_FILES).join(", ")}, \u2026 ${items.length - MAX_NOTE_FILES} more` : items.join(", ");
343
+ }
344
+ function fileToPackage(file, packageDirs) {
345
+ let bestName;
346
+ let bestLength = -1;
347
+ for (const [name, dir] of Object.entries(packageDirs)) {
348
+ const prefix = dir === "" ? "" : `${dir}/`;
349
+ if (file.startsWith(prefix) && prefix.length > bestLength) {
350
+ bestName = name;
351
+ bestLength = prefix.length;
352
+ }
353
+ }
354
+ return bestName;
355
+ }
356
+ function toPosix(p) {
357
+ return p.replace(/\\/g, "/");
358
+ }
359
+
360
+ // src/core/semantic/verdict.ts
361
+ function deriveVerdict(classifications, options = {}) {
362
+ if (options.forceBuild === true) return "build-required";
363
+ let verdict = "safe-to-skip";
364
+ for (const classification of classifications) {
365
+ if (classification === "breaking") return "build-required";
366
+ if (classification === "internal" || classification === "unanalyzed") {
367
+ verdict = "build-recommended";
368
+ }
369
+ }
370
+ return verdict;
371
+ }
372
+ function resolutionOf(confidence) {
373
+ if (confidence >= 0.9) return "high";
374
+ if (confidence >= 0.7) return "medium";
375
+ return "low";
376
+ }
377
+ var VERDICT_TEXT = {
378
+ "safe-to-skip": "safe to skip build",
379
+ "build-recommended": "build recommended",
380
+ "build-required": "build required"
381
+ };
382
+ function verdictText(verdict) {
383
+ return VERDICT_TEXT[verdict];
384
+ }
385
+
386
+ // src/core/semantic/test-impact.ts
387
+ var JAVASCRIPT_TEST_FILE = /\.(?:test|spec)\.(?:js|jsx|mjs|cjs)$/;
388
+ function isJavaScriptTestFile(file) {
389
+ return JAVASCRIPT_TEST_FILE.test(file);
390
+ }
391
+ function defaultIsTestFile(file) {
392
+ return /(^|\/)__tests__\//.test(file) || /\.(test|spec)\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/.test(file);
393
+ }
394
+ var TEST_CONFIG_FILE = /(^|\/)(package\.json|pnpm-lock\.yaml|pnpm-workspace\.yaml|package-lock\.json|npm-shrinkwrap\.json|yarn\.lock|bun\.lockb?|bunfig\.toml|tsconfig[^/]*\.json|(vitest|jest|playwright|vite|babel|next)\.config\.[^/.]+(\.[^/]+)?|vitest\.(workspace|projects)\.[^/.]+|\.babelrc(\.[^/]+)?)$/;
395
+ var TEST_SETUP_FILE = /(^|\/)((vitest|jest)\.setup|setup-?tests|global-?(setup|teardown)|tests?\/setup)\.[cm]?[jt]sx?$/i;
396
+ var PROSE_FILE = /\.(?:md|markdown|rst|txt|adoc)$/i;
397
+ var UNREACHED_CONFIDENCE = 0.5;
398
+ function invalidatesAllTests(filePath) {
399
+ return TEST_CONFIG_FILE.test(filePath) || TEST_SETUP_FILE.test(filePath);
400
+ }
401
+ function buildTestTrace(graph, options = {}) {
402
+ const isTest = options.isTestFile ?? defaultIsTestFile;
403
+ const closures = /* @__PURE__ */ new Map();
404
+ for (const file of [...graph.imports.keys()].sort()) {
405
+ if (isTest(file)) closures.set(file, closureOf(file, graph));
406
+ }
407
+ return { closures };
408
+ }
409
+ function buildCoverageMap(trace) {
410
+ const testsFor = /* @__PURE__ */ new Map();
411
+ for (const [test, closure] of trace.closures) {
412
+ for (const file of closure) {
413
+ let tests = testsFor.get(file);
414
+ if (tests === void 0) {
415
+ tests = /* @__PURE__ */ new Set();
416
+ testsFor.set(file, tests);
417
+ }
418
+ tests.add(test);
419
+ }
420
+ }
421
+ return { testsFor };
422
+ }
423
+ function computeTestImpact(radius, graph, options = {}) {
424
+ const trace = buildTestTrace(graph, options);
425
+ const coverage = buildCoverageMap(trace);
426
+ const allTests = [...trace.closures.keys()];
427
+ const notes = /* @__PURE__ */ new Set();
428
+ let selectAll = false;
429
+ for (const impact of radius.changed) {
430
+ if (invalidatesAllTests(impact.filePath)) {
431
+ selectAll = true;
432
+ notes.add(
433
+ `${impact.filePath}: build/test configuration changed \u2014 running all tests`
434
+ );
435
+ }
436
+ }
437
+ const affected = /* @__PURE__ */ new Set();
438
+ if (selectAll) {
439
+ for (const test of allTests) affected.add(test);
440
+ } else {
441
+ for (const file of radius.affectedFiles) {
442
+ for (const test of coverage.testsFor.get(file) ?? []) {
443
+ affected.add(test);
444
+ }
445
+ }
446
+ }
447
+ const unindexed = (file) => !graph.imports.has(file);
448
+ const javascriptTests = [...options.javascriptTests ?? []].filter(unindexed).sort();
449
+ const unselectedTests = radius.changed.map((impact) => impact.filePath).filter((file) => isJavaScriptTestFile(file) && unindexed(file));
450
+ if (unselectedTests.length > 0) {
451
+ const one = unselectedTests.length === 1;
452
+ notes.add(
453
+ `${unselectedTests.length} changed test ${one ? "file is" : "files are"} JavaScript, which variant does not index, and ${one ? "was" : "were"} not selected (${listPaths(unselectedTests)})`
454
+ );
455
+ }
456
+ const repositoryNotes = [];
457
+ if (javascriptTests.length > 0) {
458
+ const one = javascriptTests.length === 1;
459
+ repositoryNotes.push(
460
+ `${javascriptTests.length} JavaScript test ${one ? "file is" : "files are"} not indexed (${listPaths(javascriptTests)}); variant cannot select ${one ? "it" : "them"}, and code only ${one ? "it imports" : "they import"} looks untested`
461
+ );
462
+ const touched = new Set(
463
+ radius.changed.filter(
464
+ (impact) => impact.classification !== "non-impacting" && isModuleFile(impact.filePath) && !isJavaScriptTestFile(impact.filePath)
465
+ ).map((impact) => packageRootOf(impact.filePath, options.packageRoots))
466
+ );
467
+ const near = javascriptTests.filter(
468
+ (test) => touched.has(packageRootOf(test, options.packageRoots))
469
+ );
470
+ if (near.length > 0) {
471
+ const oneNear = near.length === 1;
472
+ notes.add(
473
+ `${near.length} JavaScript test ${oneNear ? "file" : "files"} in a package this change touches cannot be selected (${listPaths(near)})`
474
+ );
475
+ }
476
+ }
477
+ const tested = (file) => (coverage.testsFor.get(file)?.size ?? 0) > 0;
478
+ const unreached = [];
479
+ if (!selectAll) {
480
+ for (const impact of radius.changed) {
481
+ if (impact.classification === "non-impacting") continue;
482
+ if (PROSE_FILE.test(impact.filePath)) continue;
483
+ if (unselectedTests.includes(impact.filePath)) continue;
484
+ if (graph.imports.has(impact.filePath)) {
485
+ if (!tested(impact.filePath)) unreached.push(impact.filePath);
486
+ continue;
487
+ }
488
+ if (impact.classification !== "unanalyzed") continue;
489
+ const reached = [...importersOfUnindexed(graph, impact.filePath)].some(
490
+ tested
491
+ );
492
+ if (!reached) unreached.push(impact.filePath);
493
+ }
494
+ }
495
+ let blindSpots = 0;
496
+ for (const test of affected) {
497
+ const closure = trace.closures.get(test);
498
+ if (closure === void 0) continue;
499
+ for (const file of closure) {
500
+ const unresolvedForFile = graph.unresolved.get(file);
501
+ if (unresolvedForFile !== void 0 && unresolvedForFile.size > 0) {
502
+ blindSpots++;
503
+ break;
504
+ }
505
+ }
506
+ }
507
+ if (blindSpots > 0) {
508
+ notes.add(
509
+ `${blindSpots} selected test file(s) have unresolved imports in their closure \u2014 fixtures or assets may be missed`
510
+ );
511
+ }
512
+ const noteList = [...notes].sort();
513
+ const resolved = Math.max(
514
+ 0.3,
515
+ Math.round((radius.confidence - noteList.length * 0.1) * 100) / 100
516
+ );
517
+ const confidence = selectAll ? 1 : unreached.length > 0 || unselectedTests.length > 0 ? Math.min(resolved, UNREACHED_CONFIDENCE) : resolved;
518
+ return {
519
+ affectedTests: [...affected].sort(),
520
+ totalTests: allTests.length,
521
+ selectAll,
522
+ unreached,
523
+ unselectedTests,
524
+ javascriptTests: javascriptTests.length,
525
+ confidence,
526
+ resolution: resolutionOf(confidence),
527
+ notes: noteList,
528
+ repositoryNotes
529
+ };
530
+ }
531
+ async function traceTestImpact(cwd, options = {}) {
532
+ let importGraph = options.importGraph;
533
+ if (importGraph === void 0) {
534
+ const [{ graph: symbolGraph }, pathAliases] = await Promise.all([
535
+ updateSymbolGraph(
536
+ cwd,
537
+ options.graphDir === void 0 ? {} : { graphDir: options.graphDir }
538
+ ),
539
+ options.pathAliases ?? readPathAliases(cwd)
540
+ ]);
541
+ importGraph = buildImportGraph(symbolGraph, {
542
+ packageDirs: packageDirsFrom(cwd, options.packageGraph),
543
+ pathAliases,
544
+ packageExports: packageExportsFrom(options.packageGraph)
545
+ });
546
+ }
547
+ const [manifests, javascriptTests] = await Promise.all([
548
+ options.manifests ?? readManifests(cwd),
549
+ options.javascriptTests ?? findJavaScriptTests(cwd)
550
+ ]);
551
+ const radius = await traceBlastRadius(cwd, {
552
+ ...options,
553
+ importGraph,
554
+ manifests
555
+ });
556
+ if (radius === null) return null;
557
+ return {
558
+ radius,
559
+ graph: importGraph,
560
+ tests: computeTestImpact(radius, importGraph, {
561
+ ...options.isTestFile !== void 0 && {
562
+ isTestFile: options.isTestFile
563
+ },
564
+ javascriptTests,
565
+ packageRoots: [...manifests.keys()]
566
+ })
567
+ };
568
+ }
569
+ async function findJavaScriptTests(cwd) {
570
+ const fg = (await import("fast-glob")).default;
571
+ const found = await fg("**/*.{test,spec}.{js,jsx,mjs,cjs}", {
572
+ cwd,
573
+ onlyFiles: true,
574
+ ignore: ["**/node_modules/**", "**/dist/**", ".git/**", ".variant/**"]
575
+ });
576
+ return found.map((file) => file.replace(/\\/g, "/")).sort();
577
+ }
578
+ function closureOf(start, graph) {
579
+ const closure = /* @__PURE__ */ new Set([start]);
580
+ let frontier = /* @__PURE__ */ new Set([start]);
581
+ while (frontier.size > 0) {
582
+ const next = /* @__PURE__ */ new Set();
583
+ for (const file of frontier) {
584
+ for (const target of graph.imports.get(file) ?? []) {
585
+ if (!closure.has(target)) {
586
+ closure.add(target);
587
+ next.add(target);
588
+ }
589
+ }
590
+ }
591
+ frontier = next;
592
+ }
593
+ return closure;
594
+ }
595
+
596
+ // src/core/impact/predict.ts
597
+ var DEFAULT_IMPACT_BASE_REF = "HEAD~1";
598
+ async function analyzeImpact(cwd, options = {}) {
599
+ const base = options.changedFiles === void 0 ? await resolveChangeBase(
600
+ options.base,
601
+ options.changeBaseDeps ?? changeBaseDeps(cwd)
602
+ ) : {
603
+ ref: options.base ?? DEFAULT_IMPACT_BASE_REF,
604
+ source: options.base === void 0 ? "previous-commit" : "flag",
605
+ label: options.base ?? "previous commit"
606
+ };
607
+ const baseRef = base.ref;
608
+ if (options.changedFiles === void 0) await assertBaseRef(cwd, baseRef);
609
+ const packageGraph = await loadPackageGraph(cwd).catch(() => void 0);
610
+ const result = await traceTestImpact(cwd, {
611
+ baseRef,
612
+ ...packageGraph !== void 0 && { packageGraph },
613
+ ...options.changedFiles !== void 0 && {
614
+ changedFiles: options.changedFiles
615
+ },
616
+ ...options.readBefore !== void 0 && { readBefore: options.readBefore }
617
+ });
618
+ if (result === null) {
619
+ throw new GitRefError(
620
+ `Could not list the files changed since "${baseRef}"`
621
+ );
622
+ }
623
+ return { base, result, packageGraph };
624
+ }
625
+ async function predictImpact(cwd, options = {}) {
626
+ const { base, result, packageGraph } = await analyzeImpact(cwd, options);
627
+ const baseRef = base.ref;
628
+ if (result.tests.totalTests === 0) throw new NoTestFilesError();
629
+ const verdict = deriveVerdict(
630
+ result.radius.changed.map((change) => change.classification),
631
+ { forceBuild: result.tests.selectAll }
632
+ );
633
+ const headSha = options.headSha !== void 0 ? options.headSha : base.headSha ?? await readHeadSha(cwd);
634
+ const historyLogged = await appendImpactPrediction(defaultHistoryDir(cwd), {
635
+ at: (/* @__PURE__ */ new Date()).toISOString(),
636
+ baseRef,
637
+ headSha,
638
+ changedFiles: result.radius.changed.map((change) => change.filePath),
639
+ affectedFiles: result.radius.affectedFiles.length,
640
+ affectedPackages: result.radius.affectedPackages,
641
+ affectedTests: result.tests.affectedTests,
642
+ totalTests: result.tests.totalTests,
643
+ selectAll: result.tests.selectAll,
644
+ verdict,
645
+ confidence: result.tests.confidence,
646
+ notes: [...result.radius.notes, ...result.tests.notes]
647
+ });
648
+ return {
649
+ baseRef,
650
+ baseSource: base.source,
651
+ baseLabel: base.label,
652
+ headSha,
653
+ result,
654
+ verdict,
655
+ packagesFound: packageGraph?.packages.length ?? 0,
656
+ historyLogged
657
+ };
658
+ }
659
+
660
+ export {
661
+ packageRootOf,
662
+ deriveVerdict,
663
+ verdictText,
664
+ isJavaScriptTestFile,
665
+ defaultIsTestFile,
666
+ PROSE_FILE,
667
+ invalidatesAllTests,
668
+ analyzeImpact,
669
+ predictImpact
670
+ };