@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.
- package/README.md +241 -136
- package/dist/{build-2LO7GD2S.js → build-GKWNLU4Q.js} +11 -10
- package/dist/{check-ZTKGGF4M.js → check-KO5VY67Z.js} +9 -9
- package/dist/{chunk-4LLK2S4R.js → chunk-7NC7AO75.js} +19 -32
- package/dist/{chunk-RVXUCJYA.js → chunk-ACTY42RE.js} +27 -0
- package/dist/{chunk-SWPVR2DF.js → chunk-CN25SRKD.js} +9 -9
- package/dist/{chunk-LXKLCLL6.js → chunk-CWF6QXEW.js} +1 -1
- package/dist/{chunk-MZZOE7L4.js → chunk-DMDAJPFJ.js} +1 -1
- package/dist/{chunk-O4LZKXNZ.js → chunk-DSQDMFQ6.js} +59 -2
- package/dist/{chunk-V22IPJ7A.js → chunk-EISTWXDW.js} +5 -1
- package/dist/chunk-H5C4SE73.js +184 -0
- package/dist/chunk-IFFZ46YS.js +575 -0
- package/dist/chunk-IHXJ72XA.js +31 -0
- package/dist/chunk-KCEGA56S.js +670 -0
- package/dist/{chunk-LJDDS4RR.js → chunk-MAK3HG46.js} +54 -8
- package/dist/chunk-MT4BOAAV.js +167 -0
- package/dist/{chunk-VTJ67TT5.js → chunk-NFFXYYM6.js} +122 -27
- package/dist/{chunk-ISRF5F3Q.js → chunk-QRPI6TJE.js} +1 -1
- package/dist/{chunk-RGD422OM.js → chunk-RJBFCURK.js} +1 -1
- package/dist/{chunk-XUBWCTJZ.js → chunk-RSIOYQOQ.js} +85 -8
- package/dist/chunk-SGP5JX6I.js +377 -0
- package/dist/chunk-X3VVGTEV.js +268 -0
- package/dist/{chunk-NSNZOH5P.js → chunk-YII4TAKS.js} +10 -3
- package/dist/cli.js +76 -37
- package/dist/{diff-GFI6RAEA.js → diff-A3TQWWUJ.js} +9 -5
- package/dist/{doctor-GH4RZVEH.js → doctor-6ONS6LKY.js} +5 -5
- package/dist/{env-QORDD6FQ.js → env-XF2US3CB.js} +8 -8
- package/dist/git-W6N3WMYQ.js +24 -0
- package/dist/impact-DPZIAJR4.js +263 -0
- package/dist/index.js +2 -2
- package/dist/init-VNK5E4H3.js +777 -0
- package/dist/{insight-DZ5NQY6D.js → insight-LNYVMCAE.js} +9 -9
- package/dist/pr-6CYMM2XU.js +707 -0
- package/dist/{run-6PNH4QRA.js → run-44PKMOV5.js} +11 -10
- package/dist/{symbol-graph-YYUEMZGR.js → symbol-graph-OB4AJCLW.js} +3 -3
- package/dist/{tsconfig-paths-E3UEDGXZ.js → tsconfig-paths-SV3PULCW.js} +3 -1
- package/dist/vitest.cjs +629 -0
- package/dist/vitest.d.cts +181 -0
- package/dist/vitest.d.ts +181 -0
- package/dist/vitest.js +356 -0
- package/dist/workspace-PIQXG4ZD.js +65 -0
- package/package.json +15 -4
- package/dist/chunk-DRMASVYN.js +0 -25
- package/dist/chunk-KSHWC2XN.js +0 -78
- package/dist/chunk-XTYVVUWW.js +0 -279
- package/dist/impact-AF453PLK.js +0 -702
- package/dist/init-KJIUNGAN.js +0 -158
- package/dist/pr-UFZRZX6S.js +0 -152
- package/dist/workspace-X5FUBTY3.js +0 -186
package/README.md
CHANGED
|
@@ -1,176 +1,281 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
4
|
-
[](https://github.com/saintparish4/variant/actions/workflows/ci.yml)
|
|
9
|
+
## What is variant?
|
|
5
10
|
|
|
6
|
-
|
|
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
|
-
|
|
9
|
-
|
|
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
|
-
|
|
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
|
|
14
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
```
|
|
64
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
57
|
+
## Install
|
|
87
58
|
|
|
88
|
-
|
|
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
|
-
|
|
96
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
78
|
+
```bash
|
|
79
|
+
npm install -D @blzsky/variant@latest
|
|
80
|
+
```
|
|
138
81
|
|
|
139
|
-
|
|
82
|
+
Because variant is `0.x`, read the CHANGELOG before moving to a new minor
|
|
83
|
+
version.
|
|
140
84
|
|
|
141
|
-
|
|
85
|
+
## Usage
|
|
142
86
|
|
|
143
|
-
|
|
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
|
-
|
|
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
|
-
|
|
99
|
+
The commands underneath are still there when you want them:
|
|
150
100
|
|
|
151
101
|
```bash
|
|
152
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
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-
|
|
6
|
-
import "./chunk-
|
|
7
|
-
import "./chunk-
|
|
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-
|
|
12
|
+
} from "./chunk-CN25SRKD.js";
|
|
12
13
|
import "./chunk-V5CTGXMM.js";
|
|
13
|
-
import "./chunk-
|
|
14
|
-
import "./chunk-YYGCSKIQ.js";
|
|
14
|
+
import "./chunk-RJBFCURK.js";
|
|
15
15
|
import "./chunk-GDSZFIHM.js";
|
|
16
|
-
import "./chunk-
|
|
16
|
+
import "./chunk-DMDAJPFJ.js";
|
|
17
17
|
import "./chunk-3E5LBU3O.js";
|
|
18
|
-
import "./chunk-
|
|
19
|
-
import "./chunk-XUBWCTJZ.js";
|
|
18
|
+
import "./chunk-YYGCSKIQ.js";
|
|
20
19
|
import "./chunk-6J4XDG44.js";
|
|
21
|
-
import "./chunk-
|
|
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-
|
|
3
|
+
} from "./chunk-CN25SRKD.js";
|
|
4
4
|
import "./chunk-V5CTGXMM.js";
|
|
5
|
-
import "./chunk-
|
|
6
|
-
import "./chunk-
|
|
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-
|
|
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-
|
|
16
|
+
} from "./chunk-CWF6QXEW.js";
|
|
8
17
|
import {
|
|
9
18
|
renderInsights
|
|
10
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-QRPI6TJE.js";
|
|
11
20
|
import {
|
|
12
21
|
recordCacheMiss,
|
|
13
22
|
runScheduled
|
|
14
|
-
} from "./chunk-
|
|
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
|
-
|
|
23
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
25
|
-
|
|
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-
|
|
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) => ({
|