@blzsky/variant 0.2.0 → 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 (49) hide show
  1. package/README.md +241 -136
  2. package/dist/{build-2LO7GD2S.js → build-GKWNLU4Q.js} +11 -10
  3. package/dist/{check-ZTKGGF4M.js → check-KO5VY67Z.js} +9 -9
  4. package/dist/{chunk-4LLK2S4R.js → chunk-7NC7AO75.js} +19 -32
  5. package/dist/{chunk-RVXUCJYA.js → chunk-ACTY42RE.js} +27 -0
  6. package/dist/{chunk-SWPVR2DF.js → chunk-CN25SRKD.js} +9 -9
  7. package/dist/{chunk-LXKLCLL6.js → chunk-CWF6QXEW.js} +1 -1
  8. package/dist/{chunk-MZZOE7L4.js → chunk-DMDAJPFJ.js} +1 -1
  9. package/dist/{chunk-O4LZKXNZ.js → chunk-DSQDMFQ6.js} +59 -2
  10. package/dist/{chunk-V22IPJ7A.js → chunk-EISTWXDW.js} +5 -1
  11. package/dist/chunk-H5C4SE73.js +184 -0
  12. package/dist/chunk-IFFZ46YS.js +575 -0
  13. package/dist/chunk-IHXJ72XA.js +31 -0
  14. package/dist/chunk-KCEGA56S.js +670 -0
  15. package/dist/{chunk-LJDDS4RR.js → chunk-MAK3HG46.js} +54 -8
  16. package/dist/chunk-MT4BOAAV.js +167 -0
  17. package/dist/{chunk-VTJ67TT5.js → chunk-NFFXYYM6.js} +122 -27
  18. package/dist/{chunk-ISRF5F3Q.js → chunk-QRPI6TJE.js} +1 -1
  19. package/dist/{chunk-RGD422OM.js → chunk-RJBFCURK.js} +1 -1
  20. package/dist/{chunk-XUBWCTJZ.js → chunk-RSIOYQOQ.js} +85 -8
  21. package/dist/chunk-SGP5JX6I.js +377 -0
  22. package/dist/chunk-X3VVGTEV.js +268 -0
  23. package/dist/{chunk-NSNZOH5P.js → chunk-YII4TAKS.js} +10 -3
  24. package/dist/cli.js +76 -37
  25. package/dist/{diff-GFI6RAEA.js → diff-A3TQWWUJ.js} +9 -5
  26. package/dist/{doctor-GH4RZVEH.js → doctor-6ONS6LKY.js} +5 -5
  27. package/dist/{env-QORDD6FQ.js → env-XF2US3CB.js} +8 -8
  28. package/dist/git-W6N3WMYQ.js +24 -0
  29. package/dist/impact-DPZIAJR4.js +263 -0
  30. package/dist/index.js +2 -2
  31. package/dist/init-VNK5E4H3.js +777 -0
  32. package/dist/{insight-DZ5NQY6D.js → insight-LNYVMCAE.js} +9 -9
  33. package/dist/pr-6CYMM2XU.js +707 -0
  34. package/dist/{run-6PNH4QRA.js → run-44PKMOV5.js} +11 -10
  35. package/dist/{symbol-graph-YYUEMZGR.js → symbol-graph-OB4AJCLW.js} +3 -3
  36. package/dist/{tsconfig-paths-E3UEDGXZ.js → tsconfig-paths-SV3PULCW.js} +3 -1
  37. package/dist/vitest.cjs +629 -0
  38. package/dist/vitest.d.cts +181 -0
  39. package/dist/vitest.d.ts +181 -0
  40. package/dist/vitest.js +356 -0
  41. package/dist/workspace-PIQXG4ZD.js +65 -0
  42. package/package.json +15 -4
  43. package/dist/chunk-DRMASVYN.js +0 -25
  44. package/dist/chunk-KSHWC2XN.js +0 -78
  45. package/dist/chunk-XTYVVUWW.js +0 -279
  46. package/dist/impact-AF453PLK.js +0 -702
  47. package/dist/init-KJIUNGAN.js +0 -158
  48. package/dist/pr-UFZRZX6S.js +0 -152
  49. package/dist/workspace-X5FUBTY3.js +0 -186
package/README.md CHANGED
@@ -1,176 +1,281 @@
1
- # variant
1
+ <p align="center">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="./public/new-dark-mode.png">
4
+ <source media="(prefers-color-scheme: light)" srcset="./public/new-light-mode.png">
5
+ <img alt="variant: know what changed. know what matters." src="./public/new-light-mode.png" width="560">
6
+ </picture>
7
+ </p>
2
8
 
3
- [![npm version](https://img.shields.io/npm/v/@blzsky/variant.svg)](https://www.npmjs.com/package/@blzsky/variant)
4
- [![CI](https://github.com/saintparish4/variant/actions/workflows/ci.yml/badge.svg)](https://github.com/saintparish4/variant/actions/workflows/ci.yml)
9
+ ## What is variant?
5
10
 
6
- Change intelligence for TypeScript monorepos. variant reads a diff at the AST level and answers one question: **which tests does this change actually need, and how sure are we?**
11
+ variant answers one question about a change to a TypeScript repository: what
12
+ has to be verified before this is safe to merge?
7
13
 
8
- ```console
9
- $ npx @blzsky/variant impact --base HEAD~1
14
+ It reads the diff at the level of the syntax tree, not the line, follows every
15
+ import across workspace packages, and says, on the pull request:
10
16
 
11
- Base ref: HEAD~1
17
+ - how risky each changed file is, and why;
18
+ - which test files reach the change, with the import chain from each test to
19
+ the file that changed;
20
+ - which typecheck, build and lint commands apply to the packages it affects;
21
+ - which changed files no test reaches at all.
12
22
 
13
- You changed 1 file.
14
- internal src/math.ts
15
-
16
- Impact: 1 file
17
-
18
- Run: 1 test file
19
- Skip: 1 test file (of 2 total)
20
-
21
- Verdict: build recommended
22
- Confidence: 100% (report-only — run the full suite; skipping unlocks after shadow-mode validation)
23
- ```
24
-
25
- `internal` means the exported signature did not change — only the body — so a dependent that never imports the changed names is not selected. That distinction is the point: test runners and build orchestrators decide "affected" from the file or package graph; variant decides it from the exported surface.
26
-
27
- No config file is required. Published to npm as **`@blzsky/variant`**; the command it installs is **`variant`**.
28
-
29
- **`impact` is report-only and stays that way until it is measured.** Every prediction is appended to `.variant/history/impact.jsonl` so a false-skip rate can be reconciled against real test results. The printed confidence is a graph-resolution score, not a safety number. Until that rate is published, run the full suite.
30
-
31
- A cached task DAG (`build`, `run`, `insight`) sits underneath, and dogfoods the repo.
32
-
33
- The rest of this README is for working **on** variant. Using it in your own project is documented in [docs/](./docs/getting-started.md).
34
-
35
- ---
36
-
37
- ## Requirements
38
-
39
- | | Version | Notes |
40
- |---|---|---|
41
- | Node | ≥ 20 | Enforced via `engines`; CI tests 20, 22, and 24 |
42
- | pnpm | ≥ 10 | Pinned by `packageManager` |
43
- | git | any recent | Needed to exercise `--affected`, `diff`, `impact`, and `pr *` — they no-op without a repo |
44
-
45
- Optional: **[hyperfine](https://github.com/sharkdp/hyperfine)**, required by `pnpm bench`.
46
-
47
- ---
48
-
49
- ## Installation
23
+ You set it up once. After that nobody types a variant command: your own test
24
+ command and your pull requests are the interface.
50
25
 
51
26
  ```bash
52
- git clone https://github.com/saintparish4/variant.git
53
- cd variant
54
- pnpm install
55
- pnpm build # required before the CLI or E2E tests can run
56
- node dist/cli.js --help # smoke test
27
+ npx @blzsky/variant init
57
28
  ```
58
29
 
59
- ---
30
+ `init` finds out how the repository installs, tests and runs CI, shows every
31
+ change it would make as a diff, and makes them when you agree. It adds one
32
+ line to your Vitest config and one workflow that keeps a comment on each pull
33
+ request.
60
34
 
61
- ## Development
35
+ It does not skip tests. Your full suite runs as it always did; in CI variant
36
+ predicts beside it which test files the change needs, then checks that
37
+ prediction against what failed and prints one line:
62
38
 
63
- ```bash
64
- pnpm build # compile all three entry points to dist/ via tsup
65
- pnpm clean # delete dist/
66
- pnpm format # biome format --write .
67
- pnpm format:check # biome format (read-only)
68
- pnpm lint # biome check . (static analysis, read-only)
69
- pnpm typecheck # tsc --noEmit
70
- pnpm check # biome check --write — local autofix (format + lint + organize imports)
71
- pnpm bench # benchmark harness (pnpm bench:quick for a fast pass)
39
+ ```
40
+ variant: predicted 8 of 55 test files (high). 2 failed, all predicted.
72
41
  ```
73
42
 
74
- The default branch is `master`, and it is expected to be lint-clean — if `pnpm format:check`, `pnpm lint`, or `pnpm typecheck` is red, your change caused it.
75
-
76
- There is no watch build. The loop is `pnpm build && node dist/cli.js <command>`, run against a scratch project or one of the fixture workspaces in `src/__tests__/fixtures/`.
77
-
78
- `src/cli/index.ts` registers every command with a dynamic `import()` inside its `action()` callback, deferring execa, jiti, and fast-glob until a command actually runs. That is what keeps `variant --help` fast, and it is the one sanctioned exception to the project's prefer-top-level-imports rule — the benchmark job fails if startup regresses past 200 ms.
79
-
80
- Benchmark methodology and how to reproduce the published numbers: [benchmarks/README.md](./benchmarks/README.md).
81
-
82
- ---
43
+ Leaving tests out is something variant has to earn in each repository, from
44
+ that record, and is not built yet.
83
45
 
84
- ## Testing
46
+ > **Status**: `0.x`. What is described here is published as a canary,
47
+ > `0.3.0-canary.0`, under the `canary` tag: write `@blzsky/variant@canary`
48
+ > wherever this page says `@blzsky/variant`. Without the tag npm serves
49
+ > 0.2.1, which has `impact`, `pr check`, `pr report` and `workspace check`,
50
+ > an `init` that only writes a task-runner config, and no adapter. The
51
+ > adapter is for Vitest; a Jest repository gets the pull request report
52
+ > only. Test skipping is **not implemented**, deliberately.
53
+ > The task runner underneath (`build`, `run`, `insight`) works but is frozen.
54
+ > Any minor release can break, and every break is in the
55
+ > [CHANGELOG](./CHANGELOG.md).
85
56
 
86
- Three tiers, each answering a different question. Write a test at the cheapest tier that can answer yours.
57
+ ## Install
87
58
 
88
- | Tier | Tests | Lives in | Runs against |
89
- |------|-------|----------|--------------|
90
- | Unit | Isolated behavior — one module, collaborators substituted | `src/**/__tests__/*.test.ts` | Source |
91
- | Integration | Boundaries — real modules meeting, or a real edge (filesystem, git, config) | `src/__tests__/integration/` | Source |
92
- | E2E | User workflows through the shipped binary | `src/__tests__/e2e/` | Built `dist/` |
59
+ variant needs Node 20 or newer and a git repository.
93
60
 
94
61
  ```bash
95
- pnpm test # watch mode
96
- pnpm test:run # every tier, once
97
- pnpm test:integration # boundaries only
98
- pnpm test:e2e # workflows only — run pnpm build first
99
- pnpm test:all # everything + coverage
100
- pnpm vitest run src/core/graph/__tests__/dag.test.ts # a single file
62
+ npx @blzsky/variant init # shows the changes, asks, then applies
63
+ npx @blzsky/variant init --dry-run # only look
101
64
  ```
102
65
 
103
- Unit tests never shell out — `runTasksWithDeps` takes a `TaskExecutor`, so tests inject a mock. Fixture workspaces are shared across tiers at `src/__tests__/fixtures/`. Coverage gates in CI: 70% lines/statements/functions, 60% branches.
104
-
105
- ---
106
-
107
- ## Environment Variables
108
-
109
- variant takes no configuration from the environment — that lives in `variant.config.ts`. What it reads are standard terminal and CI signals:
66
+ `init` installs `@blzsky/variant` as a dev dependency with your package
67
+ manager (npm, pnpm, Yarn or Bun), adds the
68
+ [Vitest adapter](./docs/vitest-adapter.md) to your test config, ignores
69
+ `.variant/`, gives your test workflow's checkout the history a prediction
70
+ needs, and writes `.github/workflows/variant.yml`. Anything it cannot edit
71
+ with confidence it leaves alone and tells you the lines to add.
110
72
 
111
- | Variable | Read by | Effect |
112
- |---|---|---|
113
- | `NO_COLOR` | `visuals/color.ts` | Disables color everywhere. Highest-priority env signal. |
114
- | `FORCE_COLOR` | `visuals/color.ts` | Forces color on when output is not a TTY. |
115
- | `CLICOLOR_FORCE` | `visuals/color.ts` | Same as `FORCE_COLOR`. |
116
- | `CI` | picocolors, indirectly | Nothing in `src/` reads `CI`. picocolors counts it as color *support*, so CI logs keep color unless `NO_COLOR` is set. Animated progress stops in CI because stderr is not a TTY (`printer.ts:82`), not because of this variable. |
117
- | `JPY_SESSION_NAME` | `visuals/progress.ts` | Detects a Jupyter session and falls back to line-based output. |
118
- | `VARIANT_TEST_NO_CLI_PROGRESS` | `visuals/printer.ts`, `visuals/progress.ts` | Test-only. Suppresses progress bars so concurrent output stays assertable. |
73
+ The package is `@blzsky/variant`; the binary it installs is `variant`.
74
+ `npx variant` fetches an unrelated package, so always use the scope with `npx`.
119
75
 
120
- Color precedence: `--color <when>` → `--no-color` → `NO_COLOR` → `FORCE_COLOR`/`CLICOLOR_FORCE` → TTY detection. All of it resolves once in `visuals/color.ts:resolveColorChoice()`, applied process-wide by `writeGlobalColorChoice()`; renderers call `getColors()` and never consult the environment themselves. Adding a second color path is the mistake this design exists to prevent.
121
-
122
- ---
123
-
124
- ## Architecture
125
-
126
- Layered, dependencies pointing one way: `cli → core → adapters`. Ports are owned by `core`; adapters implement them and import nothing from `core` except types.
127
-
128
- | Directory | Role |
129
- |---|---|
130
- | `src/cli/` | Commander wiring, option parsing, terminal rendering |
131
- | `src/core/` | Orchestration logic, grouped by capability — `semantic`, `impact`, `pr`, `cache`, `graph`, `execution`, `detection`, `plugins` |
132
- | `src/adapters/` | The outside world — package managers, runtimes, frameworks |
133
- | `src/types/` | Contracts shared across layers |
76
+ ### Upgrade
134
77
 
135
- `tsup` builds two entry points: `dist/index.js` (library API) and `dist/cli.js` (the `variant` binary).
136
-
137
- The request path for most commands: `cli/context.ts:createContext()` loads config, detects PM/runtime/framework, builds the task DAG, and computes git-diff scoping — then `core/execution:runTasksWithDeps()` walks DAG levels while `core/cache` hashes inputs against `.variant/cache/cache.json`.
78
+ ```bash
79
+ npm install -D @blzsky/variant@latest
80
+ ```
138
81
 
139
- Along the way `core/provenance` records why each task was selected to run — cache miss, affected by the diff, or never cached. The runner attaches that record to the `VariantError` a failing task throws, and `cli/render/error.ts` prints it under the failure. It explains *selection*, never *cause*; see [docs/troubleshooting.md](./docs/troubleshooting.md) for the output and that distinction.
82
+ Because variant is `0.x`, read the CHANGELOG before moving to a new minor
83
+ version.
140
84
 
141
- Two rules to know before your first PR: business logic does not live in command handlers, and `core` never prints or exits — it throws `VariantError` subclasses and lets the CLI top level map them to exit codes (`VariantError` → 1, unexpected → 2). Full detail in [CLAUDE.md](./CLAUDE.md).
85
+ ## Usage
142
86
 
143
- Note the naming: `core/progress/reporter.ts` is a *port* — a bare `TaskEvent` interface with no output code. Everything that actually draws lives in `cli/visuals/` (progress, spinners, task events) and `cli/render/` (command output, errors). A file under `core/` never prints, whatever its name suggests.
87
+ After `init` there is nothing to run. What you see:
144
88
 
145
- ---
89
+ - **On every pull request**, one comment, kept up to date: the risk of each
90
+ changed file, the tests to run and why, the checks, and what is not
91
+ verified. [An example](./docs/api.md#pr-report).
92
+ - **In every CI test run**, one line saying what was predicted and whether a
93
+ failure fell outside it.
146
94
 
147
- ## Deployment
95
+ The first runs after setup select every test, because the setup itself changes
96
+ the lockfile and the test config. Narrowed predictions start with the next
97
+ pull request.
148
98
 
149
- variant ships as an npm package; there is no server to deploy.
99
+ The commands underneath are still there when you want them:
150
100
 
151
101
  ```bash
152
- pnpm publish # prepublishOnly runs `pnpm test:all && pnpm build`
102
+ variant pr report --markdown # the pull request comment, locally
103
+ variant impact # which tests does this change need?
104
+ variant workspace check # undeclared dependencies (exits 1)
153
105
  ```
154
106
 
155
- Only `dist/` is published (`files: ["dist"]`). The package exposes `.` through the `exports` map, ESM-only (`"type": "module"`).
156
-
157
- Releases are **manual**; the only workflows in the repository are `ci.yml` and `benchmark.yml`. Before publishing: bump the version, update [CHANGELOG.md](./CHANGELOG.md), and confirm CI is green on `master`.
158
-
159
- The package is `0.x` and makes no semver stability promise yet. Breaking changes to the `@blzsky/variant` public API can ship in any minor release; each one is recorded in [CHANGELOG.md](./CHANGELOG.md).
107
+ <p align="center">
108
+ <img alt="Running `variant impact --base main` after changing formatPrice's signature and documenting slugify: price.ts is breaking, slug.ts is non-impacting, 2 of 4 test files run" src="./docs/assets/impact.gif" width="840">
109
+ </p>
160
110
 
161
- ---
111
+ | Command | What it does |
112
+ |---|---|
113
+ | `impact` | Predict which test files a change requires, and how much of the import graph resolved. Report-only. |
114
+ | `impact verify <report...>` | Reconcile the last prediction against one or more Vitest or Jest JSON reports and report false skips |
115
+ | `diff <file>` | Classify one file's change and list the exported symbols that changed |
116
+ | `init` | Set a repository up: the dependency, the adapter, the workflow. Once. |
117
+ | `pr check` | Classify every TypeScript change on the branch and roll them into one build verdict |
118
+ | `pr report` | What the branch changes and what has to be verified, as JSON or as markdown for a PR comment |
119
+ | `workspace check` | Fail when a package imports a dependency it does not declare |
120
+
121
+ Run `variant <command> --help` for its flags, or see the
122
+ [CLI reference](./docs/cli-reference.md). The [tutorial](./docs/tutorial.md)
123
+ walks through every command on a sample monorepo in about fifteen minutes.
124
+
125
+ Every changed file gets one of three classifications. `non-impacting` means
126
+ only comments or formatting changed, and selects no tests. `internal` means the
127
+ body changed but the exported surface did not, and selects every test that
128
+ imports the file. `breaking` means an exported signature changed, and also
129
+ follows the importers of the changed names for `pr check`'s verdict. A change
130
+ to `package.json`, a lockfile, `tsconfig*.json`, a test runner config or a test
131
+ setup file selects every test.
132
+
133
+ ### In CI
134
+
135
+ `init` writes what CI needs. To wire the commands in by hand: check out
136
+ with `fetch-depth: 0`; the base is detected on GitHub Actions, and elsewhere
137
+ pass `--base origin/<branch>`. A shallow
138
+ clone has no base commit to compare against, and `actions/checkout` creates no
139
+ local branch for the target; variant stops with `GIT_REF_ERROR` rather than
140
+ guess.
141
+
142
+ Ready-to-copy GitHub Actions workflows are in
143
+ [`examples/github-actions`](./examples/github-actions):
144
+
145
+ - [`pr-report.yml`](./examples/github-actions/pr-report.yml) keeps one sticky
146
+ comment with the verdict on every pull request.
147
+ - [`workspace-check.yml`](./examples/github-actions/workspace-check.yml) fails
148
+ the build on an undeclared dependency.
149
+ - [`impact-shadow.yml`](./examples/github-actions/impact-shadow.yml) logs a
150
+ prediction, runs the full suite anyway, and reconciles the two.
151
+ - [`impact-shadow-workspace.yml`](./examples/github-actions/impact-shadow-workspace.yml)
152
+ does the same for a pnpm workspace that tests each package on its own. It
153
+ needs a release newer than 0.2.1.
154
+
155
+ ### Help measure it
156
+
157
+ A false skip is a test that failed and that the prediction did not select. The
158
+ false-skip rate is false skips divided by failed test files, with flaky
159
+ failures counted against variant. One repository is not enough to trust it.
160
+
161
+ If you add `impact-shadow.yml` to your CI, it changes nothing about your build.
162
+ It uploads the counts as a `variant-reconciliation` artifact: commit SHAs,
163
+ branch names and numbers, no file paths and no source.
164
+ [Share them in an issue](https://github.com/saintparish4/variant/issues/new?template=3.shadow_results.yml),
165
+ and they go into the measurement that decides whether skipping ever ships. The
166
+ [measurement log](./docs/measurement.md) has every number collected so far.
167
+
168
+ ## Benchmarks
169
+
170
+ Measured by CI on 2026-09-28 with [`benchmarks/bench.mjs`](./benchmarks/bench.mjs),
171
+ on a generated git repository of 300 TypeScript files with 25 changed. Each
172
+ number is the median of the runs shown. Machine: AMD EPYC 9V74 (4 cores),
173
+ 16 GB RAM, Linux, Node 22.23.2, hyperfine 1.18.0.
174
+
175
+ | | Median | Runs |
176
+ |---|---:|---:|
177
+ | `variant --help` (startup) | 36.7 ms | 20 |
178
+ | `variant impact`, 25 changed files of 300 | 988 ms | 10 |
179
+
180
+ - **Startup is a CI gate.** The benchmark workflow fails when the `--help`
181
+ median passes 200 ms, because every command is loaded only when it runs.
182
+ - **The symbol index is cached.** `.variant/graph/symbols.json` keeps each
183
+ file's exports and imports keyed by content hash, so a later run re-parses
184
+ only the files whose content changed.
185
+ - **Large real repositories are not measured yet.** 300 generated files say
186
+ how the pipeline behaves, not what it costs on your monorepo. Whether the
187
+ analysis pays for itself depends on how long your suite takes, and that
188
+ number has to come from real runs.
189
+
190
+ Reproduce with `pnpm build && pnpm bench`; the methodology is in
191
+ [`benchmarks/README.md`](./benchmarks/README.md).
192
+
193
+ ## How it compares
194
+
195
+ **Nx `affected`, Turborepo `--filter`.** These work at the level of projects
196
+ and tasks: a change inside `utils` reruns the test target of `utils` and of
197
+ every project that depends on it. In the example above, that is all four test
198
+ files. variant works at the level of test files and follows each one's imports
199
+ across packages, so it runs two. Nx and Turborepo also cache and orchestrate
200
+ tasks; variant's `impact` does neither, and the two can be used together.
201
+
202
+ **`vitest related`, `jest --findRelatedTests`.** These follow imports at the
203
+ file level too, within one project. variant adds two things: import chains that
204
+ cross workspace packages, and classification, so a change to comments or
205
+ formatting selects no tests at all.
206
+
207
+ **Not function-level.** Test selection is by file: a test that imports a changed
208
+ file runs, whichever functions it calls. The exported-surface classification
209
+ decides `pr check`'s build verdict and whether a change selects anything, not
210
+ which functions a test touches.
211
+
212
+ ## Limitations
213
+
214
+ Worth knowing before you trust a prediction. variant reads source statically,
215
+ so it sees imports and nothing else. It widens the selection or lowers its
216
+ confidence wherever it can tell it is missing something, but some edges it
217
+ cannot see at all:
218
+
219
+ - **Runtime-only wiring is invisible.** Dependency injection by token,
220
+ string-keyed registries and `eval` never appear as imports. A test that
221
+ reaches code only this way is not selected, and nothing in the output says
222
+ so. This is the largest blind spot.
223
+ - **Service boundaries end the graph.** An end-to-end test that calls a running
224
+ server imports none of the code it exercises.
225
+ - **Setup files are recognized by name.** `vitest.setup.ts`, `setupTests.ts`,
226
+ `global-setup.ts` and the like select every test; a setup file named
227
+ anything else selects none. variant does not read the runner config.
228
+ - **Files read at runtime are outside the closure.** A changed stylesheet or
229
+ JSON file that a TypeScript file imports selects that file's tests, but a
230
+ fixture, snapshot or asset a test reads with `fs` is invisible. A changed
231
+ file that reaches no test is listed as `Unreached`.
232
+ - **Only the directory it runs in is indexed.** A file changed elsewhere in
233
+ the repository selects the tests of the files that import it by relative
234
+ path, and is listed as `Unreached` otherwise.
235
+ - **Unreached is not the same as untested.** A changed file no test reaches
236
+ is listed as `Unreached` and the prediction reads `low`. variant cannot tell
237
+ an untested file from one a runtime lookup uses.
238
+ - **JavaScript is indexed when git says it is yours.** Tracked or new
239
+ `.js`, `.jsx`, `.mjs` and `.cjs` files are read like TypeScript; build
240
+ output and bundles are not. A changed CommonJS module reaches everything
241
+ that requires it.
242
+ - **Computed imports are followed only as far as their literal start.**
243
+ `` import(`./locales/${lang}.js`) `` reaches everything under `./locales/`;
244
+ `import(name)` could load anything, and is a standing note on every run.
245
+ - **Dynamic `import()` is assumed to use everything.** The change propagates
246
+ and a note is printed; the run gets wider, not narrower.
247
+ - **Workspace discovery can miss a package**, and a bare import of a package it
248
+ missed counts as external. `impact` prints how many packages it found and
249
+ notes imports no discovered package or manifest accounts for.
250
+ - **Type-only changes are not narrowed.** Changing an interface selects every
251
+ test that imports the file, though no runtime behavior changed.
252
+ - **The confidence score is not a safety number.** It says how much of the
253
+ import graph resolved, as `high`, `medium` or `low` and a percentage. Only
254
+ the false-skip rate says how safe a skip would have been.
255
+
256
+ The full list, with how each case is handled, is in
257
+ [Impact & workspace](./docs/impact-and-workspace.md#limitations).
258
+
259
+ ## Documentation
260
+
261
+ | | |
262
+ |---|---|
263
+ | [Tutorial](./docs/tutorial.md) | Every command, step by step, on a sample monorepo |
264
+ | [Getting started](./docs/getting-started.md) | Run it on your own repository, then set up the task runner |
265
+ | [Impact & workspace](./docs/impact-and-workspace.md) | How `impact` and `workspace check` work, and what static analysis cannot see |
266
+ | [PR commands](./docs/pr-commands.md) | `pr check` and `pr report` |
267
+ | [Vitest adapter](./docs/vitest-adapter.md) | One config line in place of the commands: predict and check on every test run |
268
+ | [CLI reference](./docs/cli-reference.md) | Every command, flag, exit code and file |
269
+ | [API reference](./docs/api.md) | `defineConfig`, the config types, and every JSON output |
270
+ | [Monorepo setup](./docs/monorepo.md) | Workspace tasks and `--affected` |
271
+ | [Config reference](./docs/config-reference.md) | Every `variant.config.ts` key |
272
+ | [Troubleshooting](./docs/troubleshooting.md) | Common problems and what they mean |
273
+ | [Measurement log](./docs/measurement.md) | Every prediction checked against a full run so far, and the gate skipping has to pass |
162
274
 
163
275
  ## Contributing
164
276
 
165
- Read [CONTRIBUTORS.md](./CONTRIBUTORS.md) for the full workflow and [CLAUDE.md](./CLAUDE.md) for architecture and code-style rules. The short version:
166
-
167
- - All PRs must pass `pnpm format:check`, `pnpm lint`, `pnpm typecheck`, and `pnpm build`.
168
- - Changed behavior needs a test; a bug fix needs a test that fails before it.
169
- - Behavior changes that users can observe need a `docs/` update in the same PR.
170
- - Conventional commits: `<type>: <subject>`, imperative mood, ≤ 72 chars, no trailing period.
171
- - Bump one dependency at a time — never a blanket `pnpm update` — so lockfile diffs stay reviewable.
172
-
173
- ---
277
+ See [CONTRIBUTORS.md](./CONTRIBUTORS.md) to set up the repository, run the
278
+ tests, and check a change before opening a pull request.
174
279
 
175
280
  ## License
176
281
 
@@ -2,23 +2,24 @@ import {
2
2
  executeTarget,
3
3
  renderDryRunPlan,
4
4
  reportRunInsights
5
- } from "./chunk-4LLK2S4R.js";
6
- import "./chunk-LXKLCLL6.js";
7
- import "./chunk-ISRF5F3Q.js";
5
+ } from "./chunk-7NC7AO75.js";
6
+ import "./chunk-IHXJ72XA.js";
7
+ import "./chunk-CWF6QXEW.js";
8
+ import "./chunk-QRPI6TJE.js";
8
9
  import {
9
10
  createContext,
10
11
  toRunOptions
11
- } from "./chunk-SWPVR2DF.js";
12
+ } from "./chunk-CN25SRKD.js";
12
13
  import "./chunk-V5CTGXMM.js";
13
- import "./chunk-RGD422OM.js";
14
- import "./chunk-YYGCSKIQ.js";
14
+ import "./chunk-RJBFCURK.js";
15
15
  import "./chunk-GDSZFIHM.js";
16
- import "./chunk-MZZOE7L4.js";
16
+ import "./chunk-DMDAJPFJ.js";
17
17
  import "./chunk-3E5LBU3O.js";
18
- import "./chunk-RVXUCJYA.js";
19
- import "./chunk-XUBWCTJZ.js";
18
+ import "./chunk-YYGCSKIQ.js";
20
19
  import "./chunk-6J4XDG44.js";
21
- import "./chunk-V22IPJ7A.js";
20
+ import "./chunk-EISTWXDW.js";
21
+ import "./chunk-RSIOYQOQ.js";
22
+ import "./chunk-ACTY42RE.js";
22
23
 
23
24
  // src/core/scope/task-filter.ts
24
25
  function taskPackage(taskName) {
@@ -1,21 +1,21 @@
1
1
  import {
2
2
  createContext
3
- } from "./chunk-SWPVR2DF.js";
3
+ } from "./chunk-CN25SRKD.js";
4
4
  import "./chunk-V5CTGXMM.js";
5
- import "./chunk-RGD422OM.js";
6
- import "./chunk-YYGCSKIQ.js";
7
- import "./chunk-MZZOE7L4.js";
5
+ import "./chunk-RJBFCURK.js";
6
+ import "./chunk-DMDAJPFJ.js";
8
7
  import "./chunk-3E5LBU3O.js";
9
- import {
10
- ConfigError
11
- } from "./chunk-RVXUCJYA.js";
12
- import "./chunk-XUBWCTJZ.js";
8
+ import "./chunk-YYGCSKIQ.js";
13
9
  import {
14
10
  lines
15
11
  } from "./chunk-6J4XDG44.js";
16
12
  import {
17
13
  getPrinter
18
- } from "./chunk-V22IPJ7A.js";
14
+ } from "./chunk-EISTWXDW.js";
15
+ import "./chunk-RSIOYQOQ.js";
16
+ import {
17
+ ConfigError
18
+ } from "./chunk-ACTY42RE.js";
19
19
 
20
20
  // src/core/graph/validation.ts
21
21
  function validateTaskGraph(tasks, graph) {
@@ -1,33 +1,42 @@
1
+ import {
2
+ CLEAR_LINE,
3
+ HIDE_CURSOR,
4
+ SHOW_CURSOR,
5
+ SYNC_END,
6
+ SYNC_START,
7
+ cursorUp,
8
+ fitWidth
9
+ } from "./chunk-IHXJ72XA.js";
1
10
  import {
2
11
  computeInsights,
3
12
  evictStaleEntries,
4
13
  readCache,
5
14
  writeCache,
6
15
  writeCacheSync
7
- } from "./chunk-LXKLCLL6.js";
16
+ } from "./chunk-CWF6QXEW.js";
8
17
  import {
9
18
  renderInsights
10
- } from "./chunk-ISRF5F3Q.js";
19
+ } from "./chunk-QRPI6TJE.js";
11
20
  import {
12
21
  recordCacheMiss,
13
22
  runScheduled
14
- } from "./chunk-SWPVR2DF.js";
15
- import {
16
- getColors
17
- } from "./chunk-YYGCSKIQ.js";
23
+ } from "./chunk-CN25SRKD.js";
18
24
  import {
19
25
  mapLimit
20
26
  } from "./chunk-GDSZFIHM.js";
21
27
  import {
22
- TaskExecutionError,
23
- VariantError
24
- } from "./chunk-RVXUCJYA.js";
28
+ getColors
29
+ } from "./chunk-YYGCSKIQ.js";
25
30
  import {
26
31
  lines
27
32
  } from "./chunk-6J4XDG44.js";
28
33
  import {
29
34
  getPrinter
30
- } from "./chunk-V22IPJ7A.js";
35
+ } from "./chunk-EISTWXDW.js";
36
+ import {
37
+ TaskExecutionError,
38
+ VariantError
39
+ } from "./chunk-ACTY42RE.js";
31
40
 
32
41
  // src/core/execution/runner.ts
33
42
  import { createHash as createHash2 } from "crypto";
@@ -283,28 +292,6 @@ async function runTasksWithDeps(target, graph, options, executor = executeTask)
283
292
  return results;
284
293
  }
285
294
 
286
- // src/cli/visuals/ansi.ts
287
- var ESC = "\x1B";
288
- var HIDE_CURSOR = `${ESC}[?25l`;
289
- var SHOW_CURSOR = `${ESC}[?25h`;
290
- var CLEAR_LINE = `${ESC}[2K`;
291
- var SYNC_START = `${ESC}[?2026h`;
292
- var SYNC_END = `${ESC}[?2026l`;
293
- function cursorUp(lines2) {
294
- return lines2 > 0 ? `${ESC}[${lines2}A` : "";
295
- }
296
- var STYLE_PATTERN = new RegExp(`${ESC}\\[[0-9;]*m`, "g");
297
- function stripStyles(text) {
298
- return text.replace(STYLE_PATTERN, "");
299
- }
300
- function visibleLength(line) {
301
- return stripStyles(line).length;
302
- }
303
- function fitWidth(line, columns) {
304
- if (visibleLength(line) <= columns) return line;
305
- return stripStyles(line).slice(0, Math.max(0, columns - 1));
306
- }
307
-
308
295
  // src/cli/visuals/progress.ts
309
296
  var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
310
297
  var TICK_MS = 80;
@@ -56,12 +56,36 @@ var GraphError = class extends VariantError {
56
56
  this.hint = "Delete `.variant/graph/` and retry.";
57
57
  }
58
58
  };
59
+ var GitRefError = class extends VariantError {
60
+ constructor(message) {
61
+ super("GIT_REF_ERROR", message);
62
+ 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`.";
63
+ }
64
+ };
65
+ var NoBaseCommitError = class extends VariantError {
66
+ constructor() {
67
+ super(
68
+ "NO_BASE_COMMIT",
69
+ "This push has no previous commit to compare against"
70
+ );
71
+ 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.";
72
+ }
73
+ };
59
74
  var ImpactReportError = class extends VariantError {
60
75
  constructor(message) {
61
76
  super("IMPACT_REPORT_ERROR", `Could not read the test report: ${message}`);
62
77
  this.hint = "Pass a Vitest `--reporter=json --outputFile=<path>` or Jest `--json --outputFile=<path>` report.";
63
78
  }
64
79
  };
80
+ var NoTestFilesError = class extends VariantError {
81
+ constructor() {
82
+ super(
83
+ "NO_TEST_FILES",
84
+ "No test files found, so there is nothing to predict"
85
+ );
86
+ 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.";
87
+ }
88
+ };
65
89
  var CliUsageError = class extends VariantError {
66
90
  constructor(message) {
67
91
  super("CLI_USAGE", message);
@@ -76,6 +100,9 @@ export {
76
100
  TaskExecutionError,
77
101
  CacheError,
78
102
  GraphError,
103
+ GitRefError,
104
+ NoBaseCommitError,
79
105
  ImpactReportError,
106
+ NoTestFilesError,
80
107
  CliUsageError
81
108
  };
@@ -7,29 +7,29 @@ import {
7
7
  } from "./chunk-V5CTGXMM.js";
8
8
  import {
9
9
  loadConfig
10
- } from "./chunk-RGD422OM.js";
11
- import {
12
- getColors
13
- } from "./chunk-YYGCSKIQ.js";
10
+ } from "./chunk-RJBFCURK.js";
14
11
  import {
15
12
  changedFilesToPackages,
16
13
  getChangedFiles
17
- } from "./chunk-MZZOE7L4.js";
14
+ } from "./chunk-DMDAJPFJ.js";
18
15
  import {
19
16
  computeAffectedPackages,
20
17
  loadPackageGraph,
21
18
  tasksFromPackageGraph
22
19
  } from "./chunk-3E5LBU3O.js";
23
20
  import {
24
- ConfigError,
25
- CycleError
26
- } from "./chunk-RVXUCJYA.js";
21
+ getColors
22
+ } from "./chunk-YYGCSKIQ.js";
27
23
  import {
28
24
  errorLines
29
25
  } from "./chunk-6J4XDG44.js";
30
26
  import {
31
27
  getPrinter
32
- } from "./chunk-V22IPJ7A.js";
28
+ } from "./chunk-EISTWXDW.js";
29
+ import {
30
+ ConfigError,
31
+ CycleError
32
+ } from "./chunk-ACTY42RE.js";
33
33
 
34
34
  // src/adapters/frameworks/plugin.ts
35
35
  var wrapFrameworkAsPlugin = (a) => ({