@blzsky/variant 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +197 -146
- package/dist/{build-2LO7GD2S.js → build-3DSBMMI3.js} +9 -9
- package/dist/{check-ZTKGGF4M.js → check-SIQJU6Z2.js} +8 -8
- package/dist/{chunk-RGD422OM.js → chunk-26DHEXGS.js} +1 -1
- package/dist/{chunk-LXKLCLL6.js → chunk-43VLELTL.js} +1 -1
- package/dist/{chunk-XUBWCTJZ.js → chunk-6BXSZPHC.js} +18 -0
- package/dist/{chunk-MZZOE7L4.js → chunk-7OPTWU6E.js} +1 -1
- package/dist/{chunk-V22IPJ7A.js → chunk-EISTWXDW.js} +5 -1
- package/dist/{chunk-4LLK2S4R.js → chunk-FA64KWJ2.js} +8 -8
- package/dist/{chunk-LJDDS4RR.js → chunk-FZEXZBXB.js} +16 -2
- package/dist/{chunk-NSNZOH5P.js → chunk-Q7RLUZSN.js} +2 -2
- package/dist/{chunk-ISRF5F3Q.js → chunk-QRPI6TJE.js} +1 -1
- package/dist/{chunk-O4LZKXNZ.js → chunk-RC23JQKI.js} +50 -1
- package/dist/{chunk-VTJ67TT5.js → chunk-TCISS2AS.js} +4 -4
- package/dist/{chunk-RVXUCJYA.js → chunk-VYWPQKMS.js} +7 -0
- package/dist/{chunk-SWPVR2DF.js → chunk-ZH2PQUNL.js} +7 -7
- package/dist/cli.js +30 -26
- package/dist/{diff-GFI6RAEA.js → diff-HSJBZFMC.js} +9 -5
- package/dist/{doctor-GH4RZVEH.js → doctor-B7MHFIYJ.js} +3 -3
- package/dist/{env-QORDD6FQ.js → env-I7JZRWDN.js} +7 -7
- package/dist/{impact-AF453PLK.js → impact-PRZ6HEP5.js} +54 -21
- package/dist/index.js +2 -2
- package/dist/{init-KJIUNGAN.js → init-O2BY3FOY.js} +3 -3
- package/dist/{insight-DZ5NQY6D.js → insight-JQLTGFYB.js} +8 -8
- package/dist/{pr-UFZRZX6S.js → pr-Y724QAHE.js} +19 -6
- package/dist/{run-6PNH4QRA.js → run-C6VAQOVM.js} +9 -9
- package/dist/{symbol-graph-YYUEMZGR.js → symbol-graph-E5GXKDFZ.js} +3 -3
- package/dist/{workspace-X5FUBTY3.js → workspace-HRTFGWKN.js} +4 -4
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,176 +1,227 @@
|
|
|
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>
|
|
8
|
+
|
|
9
|
+
## What is variant?
|
|
10
|
+
|
|
11
|
+
variant tells you which tests a change in a TypeScript monorepo actually needs.
|
|
12
|
+
It reads the diff at the level of the syntax tree, not the line, classifies
|
|
13
|
+
each changed file by what it does to the file's exported surface, and follows
|
|
14
|
+
every test file's imports across workspace packages to see which ones reach the
|
|
15
|
+
change.
|
|
2
16
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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?**
|
|
7
|
-
|
|
8
|
-
```console
|
|
9
|
-
$ npx @blzsky/variant impact --base HEAD~1
|
|
10
|
-
|
|
11
|
-
Base ref: HEAD~1
|
|
12
|
-
|
|
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)
|
|
17
|
+
```bash
|
|
18
|
+
npx @blzsky/variant impact --base origin/main
|
|
23
19
|
```
|
|
24
20
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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).
|
|
21
|
+
<p align="center">
|
|
22
|
+
<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">
|
|
23
|
+
</p>
|
|
34
24
|
|
|
35
|
-
|
|
25
|
+
In that repository, `shop` and `blog` both depend on `utils`. A tool that works
|
|
26
|
+
from the package graph would run all four test files. variant follows the
|
|
27
|
+
imports: only `shop` reaches the changed `price.ts`, and a doc comment on
|
|
28
|
+
`slug.ts` selects nothing.
|
|
36
29
|
|
|
37
|
-
|
|
30
|
+
It does not skip anything. Every prediction is logged, and
|
|
31
|
+
`variant impact verify` checks it against the test run you did anyway,
|
|
32
|
+
counting each failure the prediction would have missed. That count, the
|
|
33
|
+
false-skip rate, is what has to be measured before skipping is worth turning
|
|
34
|
+
on, and it is the point of the project right now.
|
|
38
35
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
| pnpm | ≥ 10 | Pinned by `packageManager` |
|
|
43
|
-
| git | any recent | Needed to exercise `--affected`, `diff`, `impact`, and `pr *` — they no-op without a repo |
|
|
36
|
+
The same analysis powers `pr check`, which rolls a branch's changes into one
|
|
37
|
+
build verdict for a pull request comment, and `workspace check`, which fails CI
|
|
38
|
+
when a package imports something it never declared.
|
|
44
39
|
|
|
45
|
-
|
|
40
|
+
> **Status**: `0.x`, report-only. `impact`, `impact verify`, `diff`,
|
|
41
|
+
> `pr check`, `pr report` and `workspace check` work today and are where
|
|
42
|
+
> development happens. Test skipping is **not implemented**, deliberately, and
|
|
43
|
+
> will not be until the false-skip rate is measured on real repositories; see
|
|
44
|
+
> [Help measure it](#help-measure-it). The task runner underneath (`build`,
|
|
45
|
+
> `run`, `insight`) works but is frozen. Any minor release can break, and every
|
|
46
|
+
> break is in the [CHANGELOG](./CHANGELOG.md).
|
|
46
47
|
|
|
47
|
-
|
|
48
|
+
## Install
|
|
48
49
|
|
|
49
|
-
|
|
50
|
+
variant needs Node 20 or newer and a git repository with at least one commit
|
|
51
|
+
before the one you are checking. There is nothing to configure.
|
|
50
52
|
|
|
51
53
|
```bash
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
pnpm install
|
|
55
|
-
pnpm build # required before the CLI or E2E tests can run
|
|
56
|
-
node dist/cli.js --help # smoke test
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
---
|
|
60
|
-
|
|
61
|
-
## Development
|
|
54
|
+
# run it once
|
|
55
|
+
npx @blzsky/variant impact --base origin/main
|
|
62
56
|
|
|
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)
|
|
57
|
+
# keep it in a project
|
|
58
|
+
npm install -D @blzsky/variant # or pnpm add -D / yarn add -D
|
|
72
59
|
```
|
|
73
60
|
|
|
74
|
-
The
|
|
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.
|
|
61
|
+
The package is `@blzsky/variant`; the binary it installs is `variant`.
|
|
62
|
+
`npx variant` fetches an unrelated package, so always use the scope with `npx`.
|
|
79
63
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
---
|
|
83
|
-
|
|
84
|
-
## Testing
|
|
85
|
-
|
|
86
|
-
Three tiers, each answering a different question. Write a test at the cheapest tier that can answer yours.
|
|
87
|
-
|
|
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/` |
|
|
64
|
+
### Upgrade
|
|
93
65
|
|
|
94
66
|
```bash
|
|
95
|
-
|
|
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
|
|
67
|
+
npm install -D @blzsky/variant@latest
|
|
101
68
|
```
|
|
102
69
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
---
|
|
70
|
+
Because variant is `0.x`, pin a version in CI (`npx --yes @blzsky/variant@0.2.1`)
|
|
71
|
+
and read the CHANGELOG before moving to a new minor version.
|
|
106
72
|
|
|
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:
|
|
110
|
-
|
|
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. |
|
|
119
|
-
|
|
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 |
|
|
134
|
-
|
|
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`.
|
|
138
|
-
|
|
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.
|
|
140
|
-
|
|
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).
|
|
142
|
-
|
|
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.
|
|
144
|
-
|
|
145
|
-
---
|
|
146
|
-
|
|
147
|
-
## Deployment
|
|
148
|
-
|
|
149
|
-
variant ships as an npm package; there is no server to deploy.
|
|
73
|
+
## Usage
|
|
150
74
|
|
|
151
75
|
```bash
|
|
152
|
-
|
|
76
|
+
variant impact --base origin/main # which tests does this branch need?
|
|
77
|
+
npx vitest run --reporter=json --outputFile=report.json
|
|
78
|
+
variant impact verify report.json # did the prediction miss a failure?
|
|
79
|
+
variant pr check --base origin/main # one build verdict for the branch
|
|
80
|
+
variant workspace check # undeclared dependencies (exits 1)
|
|
153
81
|
```
|
|
154
82
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
83
|
+
| Command | What it does |
|
|
84
|
+
|---|---|
|
|
85
|
+
| `impact` | Predict which test files a change requires, and how much of the import graph resolved. Report-only. |
|
|
86
|
+
| `impact verify <report>` | Reconcile the last prediction against a Vitest or Jest JSON report and report false skips |
|
|
87
|
+
| `diff <file>` | Classify one file's change and list the exported symbols that changed |
|
|
88
|
+
| `pr check` | Classify every TypeScript change on the branch and roll them into one build verdict |
|
|
89
|
+
| `pr report` | The `pr check` verdict as JSON, or as markdown for a PR comment |
|
|
90
|
+
| `workspace check` | Fail when a package imports a dependency it does not declare |
|
|
91
|
+
|
|
92
|
+
Run `variant <command> --help` for its flags, or see the
|
|
93
|
+
[CLI reference](./docs/cli-reference.md). The [tutorial](./docs/tutorial.md)
|
|
94
|
+
walks through every command on a sample monorepo in about fifteen minutes.
|
|
95
|
+
|
|
96
|
+
Every changed file gets one of three classifications. `non-impacting` means
|
|
97
|
+
only comments or formatting changed, and selects no tests. `internal` means the
|
|
98
|
+
body changed but the exported surface did not, and selects every test that
|
|
99
|
+
imports the file. `breaking` means an exported signature changed, and also
|
|
100
|
+
follows the importers of the changed names for `pr check`'s verdict. A change
|
|
101
|
+
to `package.json`, a lockfile, `tsconfig*.json`, a test runner config or a test
|
|
102
|
+
setup file selects every test.
|
|
103
|
+
|
|
104
|
+
### In CI
|
|
105
|
+
|
|
106
|
+
Check out with `fetch-depth: 0` and pass `--base origin/<branch>`. A shallow
|
|
107
|
+
clone has no base commit to compare against, and `actions/checkout` creates no
|
|
108
|
+
local branch for the target; variant stops with `GIT_REF_ERROR` rather than
|
|
109
|
+
guess.
|
|
110
|
+
|
|
111
|
+
Ready-to-copy GitHub Actions workflows are in
|
|
112
|
+
[`examples/github-actions`](./examples/github-actions):
|
|
113
|
+
|
|
114
|
+
- [`pr-report.yml`](./examples/github-actions/pr-report.yml) keeps one sticky
|
|
115
|
+
comment with the verdict on every pull request.
|
|
116
|
+
- [`workspace-check.yml`](./examples/github-actions/workspace-check.yml) fails
|
|
117
|
+
the build on an undeclared dependency.
|
|
118
|
+
- [`impact-shadow.yml`](./examples/github-actions/impact-shadow.yml) logs a
|
|
119
|
+
prediction, runs the full suite anyway, and reconciles the two.
|
|
120
|
+
|
|
121
|
+
### Help measure it
|
|
122
|
+
|
|
123
|
+
A false skip is a test that failed and that the prediction did not select. The
|
|
124
|
+
false-skip rate is false skips divided by failed test files, with flaky
|
|
125
|
+
failures counted against variant. One repository is not enough to trust it.
|
|
126
|
+
|
|
127
|
+
If you add `impact-shadow.yml` to your CI, it changes nothing about your build.
|
|
128
|
+
It uploads the counts as a `variant-reconciliation` artifact: commit SHAs,
|
|
129
|
+
branch names and numbers, no file paths and no source.
|
|
130
|
+
[Share them in an issue](https://github.com/saintparish4/variant/issues/new?template=3.shadow_results.yml),
|
|
131
|
+
and they go into the measurement that decides whether skipping ever ships.
|
|
132
|
+
|
|
133
|
+
## Benchmarks
|
|
134
|
+
|
|
135
|
+
Measured by CI on 2026-09-28 with [`benchmarks/bench.mjs`](./benchmarks/bench.mjs),
|
|
136
|
+
on a generated git repository of 300 TypeScript files with 25 changed. Each
|
|
137
|
+
number is the median of the runs shown. Machine: AMD EPYC 9V74 (4 cores),
|
|
138
|
+
16 GB RAM, Linux, Node 22.23.2, hyperfine 1.18.0.
|
|
139
|
+
|
|
140
|
+
| | Median | Runs |
|
|
141
|
+
|---|---:|---:|
|
|
142
|
+
| `variant --help` (startup) | 36.7 ms | 20 |
|
|
143
|
+
| `variant impact`, 25 changed files of 300 | 988 ms | 10 |
|
|
144
|
+
|
|
145
|
+
- **Startup is a CI gate.** The benchmark workflow fails when the `--help`
|
|
146
|
+
median passes 200 ms, because every command is loaded only when it runs.
|
|
147
|
+
- **The symbol index is cached.** `.variant/graph/symbols.json` keeps each
|
|
148
|
+
file's exports and imports keyed by content hash, so a later run re-parses
|
|
149
|
+
only the files whose content changed.
|
|
150
|
+
- **Large real repositories are not measured yet.** 300 generated files say
|
|
151
|
+
how the pipeline behaves, not what it costs on your monorepo. Whether the
|
|
152
|
+
analysis pays for itself depends on how long your suite takes, and that
|
|
153
|
+
number has to come from real runs.
|
|
154
|
+
|
|
155
|
+
Reproduce with `pnpm build && pnpm bench`; the methodology is in
|
|
156
|
+
[`benchmarks/README.md`](./benchmarks/README.md).
|
|
157
|
+
|
|
158
|
+
## How it compares
|
|
159
|
+
|
|
160
|
+
**Nx `affected`, Turborepo `--filter`.** These work at the level of projects
|
|
161
|
+
and tasks: a change inside `utils` reruns the test target of `utils` and of
|
|
162
|
+
every project that depends on it. In the example above, that is all four test
|
|
163
|
+
files. variant works at the level of test files and follows each one's imports
|
|
164
|
+
across packages, so it runs two. Nx and Turborepo also cache and orchestrate
|
|
165
|
+
tasks; variant's `impact` does neither, and the two can be used together.
|
|
166
|
+
|
|
167
|
+
**`vitest related`, `jest --findRelatedTests`.** These follow imports at the
|
|
168
|
+
file level too, within one project. variant adds two things: import chains that
|
|
169
|
+
cross workspace packages, and classification, so a change to comments or
|
|
170
|
+
formatting selects no tests at all.
|
|
171
|
+
|
|
172
|
+
**Not function-level.** Test selection is by file: a test that imports a changed
|
|
173
|
+
file runs, whichever functions it calls. The exported-surface classification
|
|
174
|
+
decides `pr check`'s build verdict and whether a change selects anything, not
|
|
175
|
+
which functions a test touches.
|
|
176
|
+
|
|
177
|
+
## Limitations
|
|
178
|
+
|
|
179
|
+
Worth knowing before you trust a prediction. variant reads source statically,
|
|
180
|
+
so it sees imports and nothing else. It widens the selection or lowers its
|
|
181
|
+
confidence wherever it can tell it is missing something, but some edges it
|
|
182
|
+
cannot see at all:
|
|
183
|
+
|
|
184
|
+
- **Runtime-only wiring is invisible.** Dependency injection by token,
|
|
185
|
+
string-keyed registries, computed `require()` and `eval` never appear as
|
|
186
|
+
imports. A test that reaches code only this way is not selected, and nothing
|
|
187
|
+
in the output says so. This is the largest blind spot.
|
|
188
|
+
- **Service boundaries end the graph.** An end-to-end test that calls a running
|
|
189
|
+
server imports none of the code it exercises.
|
|
190
|
+
- **Setup files are recognized by name.** `vitest.setup.ts`, `setupTests.ts`,
|
|
191
|
+
`global-setup.ts` and the like select every test; a setup file named
|
|
192
|
+
anything else selects none. variant does not read the runner config.
|
|
193
|
+
- **Fixtures, snapshots and non-TypeScript assets** a test reads at runtime are
|
|
194
|
+
outside its import closure. Unresolved imports lower the confidence score and
|
|
195
|
+
add a note, but the missing edges stay missing.
|
|
196
|
+
- **Dynamic `import()` is assumed to use everything.** The change propagates
|
|
197
|
+
and a note is printed; the run gets wider, not narrower.
|
|
198
|
+
- **Type-only changes are not narrowed.** Changing an interface selects every
|
|
199
|
+
test that imports the file, though no runtime behavior changed.
|
|
200
|
+
- **The confidence score is not a safety number.** It says how much of the
|
|
201
|
+
import graph resolved. Only the false-skip rate says how safe a skip would
|
|
202
|
+
have been.
|
|
203
|
+
|
|
204
|
+
The full list, with how each case is handled, is in
|
|
205
|
+
[Impact & workspace](./docs/impact-and-workspace.md#limitations).
|
|
206
|
+
|
|
207
|
+
## Documentation
|
|
208
|
+
|
|
209
|
+
| | |
|
|
210
|
+
|---|---|
|
|
211
|
+
| [Tutorial](./docs/tutorial.md) | Every command, step by step, on a sample monorepo |
|
|
212
|
+
| [Getting started](./docs/getting-started.md) | Run it on your own repository, then set up the task runner |
|
|
213
|
+
| [Impact & workspace](./docs/impact-and-workspace.md) | How `impact` and `workspace check` work, and what static analysis cannot see |
|
|
214
|
+
| [PR commands](./docs/pr-commands.md) | `pr check` and `pr report` |
|
|
215
|
+
| [CLI reference](./docs/cli-reference.md) | Every command, flag, exit code and file |
|
|
216
|
+
| [API reference](./docs/api.md) | `defineConfig`, the config types, and every JSON output |
|
|
217
|
+
| [Monorepo setup](./docs/monorepo.md) | Workspace tasks and `--affected` |
|
|
218
|
+
| [Config reference](./docs/config-reference.md) | Every `variant.config.ts` key |
|
|
219
|
+
| [Troubleshooting](./docs/troubleshooting.md) | Common problems and what they mean |
|
|
162
220
|
|
|
163
221
|
## Contributing
|
|
164
222
|
|
|
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
|
-
---
|
|
223
|
+
See [CONTRIBUTORS.md](./CONTRIBUTORS.md) to set up the repository, run the
|
|
224
|
+
tests, and check a change before opening a pull request.
|
|
174
225
|
|
|
175
226
|
## License
|
|
176
227
|
|
|
@@ -2,23 +2,23 @@ import {
|
|
|
2
2
|
executeTarget,
|
|
3
3
|
renderDryRunPlan,
|
|
4
4
|
reportRunInsights
|
|
5
|
-
} from "./chunk-
|
|
6
|
-
import "./chunk-
|
|
7
|
-
import "./chunk-
|
|
5
|
+
} from "./chunk-FA64KWJ2.js";
|
|
6
|
+
import "./chunk-43VLELTL.js";
|
|
7
|
+
import "./chunk-QRPI6TJE.js";
|
|
8
8
|
import {
|
|
9
9
|
createContext,
|
|
10
10
|
toRunOptions
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-ZH2PQUNL.js";
|
|
12
12
|
import "./chunk-V5CTGXMM.js";
|
|
13
|
-
import "./chunk-
|
|
13
|
+
import "./chunk-26DHEXGS.js";
|
|
14
14
|
import "./chunk-YYGCSKIQ.js";
|
|
15
15
|
import "./chunk-GDSZFIHM.js";
|
|
16
|
-
import "./chunk-
|
|
16
|
+
import "./chunk-7OPTWU6E.js";
|
|
17
17
|
import "./chunk-3E5LBU3O.js";
|
|
18
|
-
import "./chunk-
|
|
19
|
-
import "./chunk-XUBWCTJZ.js";
|
|
18
|
+
import "./chunk-6BXSZPHC.js";
|
|
20
19
|
import "./chunk-6J4XDG44.js";
|
|
21
|
-
import "./chunk-
|
|
20
|
+
import "./chunk-EISTWXDW.js";
|
|
21
|
+
import "./chunk-VYWPQKMS.js";
|
|
22
22
|
|
|
23
23
|
// src/core/scope/task-filter.ts
|
|
24
24
|
function taskPackage(taskName) {
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createContext
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-ZH2PQUNL.js";
|
|
4
4
|
import "./chunk-V5CTGXMM.js";
|
|
5
|
-
import "./chunk-
|
|
5
|
+
import "./chunk-26DHEXGS.js";
|
|
6
6
|
import "./chunk-YYGCSKIQ.js";
|
|
7
|
-
import "./chunk-
|
|
7
|
+
import "./chunk-7OPTWU6E.js";
|
|
8
8
|
import "./chunk-3E5LBU3O.js";
|
|
9
|
-
import
|
|
10
|
-
ConfigError
|
|
11
|
-
} from "./chunk-RVXUCJYA.js";
|
|
12
|
-
import "./chunk-XUBWCTJZ.js";
|
|
9
|
+
import "./chunk-6BXSZPHC.js";
|
|
13
10
|
import {
|
|
14
11
|
lines
|
|
15
12
|
} from "./chunk-6J4XDG44.js";
|
|
16
13
|
import {
|
|
17
14
|
getPrinter
|
|
18
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-EISTWXDW.js";
|
|
16
|
+
import {
|
|
17
|
+
ConfigError
|
|
18
|
+
} from "./chunk-VYWPQKMS.js";
|
|
19
19
|
|
|
20
20
|
// src/core/graph/validation.ts
|
|
21
21
|
function validateTaskGraph(tasks, graph) {
|
|
@@ -11,11 +11,15 @@ async function git(cwd, args) {
|
|
|
11
11
|
return null;
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
|
+
function isOptionLike(ref) {
|
|
15
|
+
return ref.startsWith("-");
|
|
16
|
+
}
|
|
14
17
|
function toFileList(stdout) {
|
|
15
18
|
if (stdout === null) return null;
|
|
16
19
|
return stdout.split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
17
20
|
}
|
|
18
21
|
async function readFileAtRef(cwd, ref, relPath) {
|
|
22
|
+
if (isOptionLike(ref)) return null;
|
|
19
23
|
return git(cwd, ["show", `${ref}:${toPosix(relPath)}`]);
|
|
20
24
|
}
|
|
21
25
|
var CAT_FILE_BATCH_SIZE = 256;
|
|
@@ -82,10 +86,23 @@ async function readHeadSha(cwd) {
|
|
|
82
86
|
const sha = stdout?.trim();
|
|
83
87
|
return sha === void 0 || sha === "" ? null : sha;
|
|
84
88
|
}
|
|
89
|
+
async function resolveCommit(cwd, ref) {
|
|
90
|
+
if (isOptionLike(ref)) return null;
|
|
91
|
+
const stdout = await git(cwd, [
|
|
92
|
+
"rev-parse",
|
|
93
|
+
"--verify",
|
|
94
|
+
"--quiet",
|
|
95
|
+
`${ref}^{commit}`
|
|
96
|
+
]);
|
|
97
|
+
const sha = stdout?.trim();
|
|
98
|
+
return sha === void 0 || sha === "" ? null : sha;
|
|
99
|
+
}
|
|
85
100
|
async function listChangedFiles(cwd, ref) {
|
|
101
|
+
if (isOptionLike(ref)) return null;
|
|
86
102
|
return toFileList(await git(cwd, ["diff", "--name-only", ref, "--"]));
|
|
87
103
|
}
|
|
88
104
|
async function listChangedFilesSinceMergeBase(cwd, ref) {
|
|
105
|
+
if (isOptionLike(ref)) return null;
|
|
89
106
|
return toFileList(
|
|
90
107
|
await git(cwd, ["diff", "--name-only", `${ref}...HEAD`, "--"])
|
|
91
108
|
);
|
|
@@ -95,6 +112,7 @@ export {
|
|
|
95
112
|
readFileAtRef,
|
|
96
113
|
readFilesAtRef,
|
|
97
114
|
readHeadSha,
|
|
115
|
+
resolveCommit,
|
|
98
116
|
listChangedFiles,
|
|
99
117
|
listChangedFilesSinceMergeBase
|
|
100
118
|
};
|
|
@@ -17,8 +17,12 @@ var Printer = class _Printer {
|
|
|
17
17
|
if (flags.noProgress) return new _Printer("no-progress", streams);
|
|
18
18
|
return new _Printer("default", streams);
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* As in uv, `-q` quiets the commentary, not the answer: a script that asks
|
|
22
|
+
* for `--json` with `-q` still gets its JSON. Only `-qq` silences stdout.
|
|
23
|
+
*/
|
|
20
24
|
get stdoutEnabled() {
|
|
21
|
-
return this.mode !== "silent"
|
|
25
|
+
return this.mode !== "silent";
|
|
22
26
|
}
|
|
23
27
|
get stderrEnabled() {
|
|
24
28
|
return this.mode !== "silent" && this.mode !== "quiet";
|
|
@@ -4,30 +4,30 @@ import {
|
|
|
4
4
|
readCache,
|
|
5
5
|
writeCache,
|
|
6
6
|
writeCacheSync
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-43VLELTL.js";
|
|
8
8
|
import {
|
|
9
9
|
renderInsights
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-QRPI6TJE.js";
|
|
11
11
|
import {
|
|
12
12
|
recordCacheMiss,
|
|
13
13
|
runScheduled
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-ZH2PQUNL.js";
|
|
15
15
|
import {
|
|
16
16
|
getColors
|
|
17
17
|
} from "./chunk-YYGCSKIQ.js";
|
|
18
18
|
import {
|
|
19
19
|
mapLimit
|
|
20
20
|
} from "./chunk-GDSZFIHM.js";
|
|
21
|
-
import {
|
|
22
|
-
TaskExecutionError,
|
|
23
|
-
VariantError
|
|
24
|
-
} from "./chunk-RVXUCJYA.js";
|
|
25
21
|
import {
|
|
26
22
|
lines
|
|
27
23
|
} from "./chunk-6J4XDG44.js";
|
|
28
24
|
import {
|
|
29
25
|
getPrinter
|
|
30
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-EISTWXDW.js";
|
|
27
|
+
import {
|
|
28
|
+
TaskExecutionError,
|
|
29
|
+
VariantError
|
|
30
|
+
} from "./chunk-VYWPQKMS.js";
|
|
31
31
|
|
|
32
32
|
// src/core/execution/runner.ts
|
|
33
33
|
import { createHash as createHash2 } from "crypto";
|
|
@@ -1,7 +1,20 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveCommit
|
|
3
|
+
} from "./chunk-6BXSZPHC.js";
|
|
1
4
|
import {
|
|
2
5
|
collectExportedSurface,
|
|
3
6
|
significantText
|
|
4
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-RC23JQKI.js";
|
|
8
|
+
import {
|
|
9
|
+
GitRefError
|
|
10
|
+
} from "./chunk-VYWPQKMS.js";
|
|
11
|
+
|
|
12
|
+
// src/core/vcs/base-ref.ts
|
|
13
|
+
async function assertBaseRef(cwd, ref) {
|
|
14
|
+
if (await resolveCommit(cwd, ref) === null) {
|
|
15
|
+
throw new GitRefError(`"${ref}" does not name a commit in this repository`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
5
18
|
|
|
6
19
|
// src/core/semantic/differ.ts
|
|
7
20
|
function createClassifier() {
|
|
@@ -87,5 +100,6 @@ function compareSurfaces(input, beforeFile, afterFile, tsm) {
|
|
|
87
100
|
|
|
88
101
|
export {
|
|
89
102
|
createClassifier,
|
|
90
|
-
classifyChange
|
|
103
|
+
classifyChange,
|
|
104
|
+
assertBaseRef
|
|
91
105
|
};
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
classifyChange
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-FZEXZBXB.js";
|
|
4
4
|
import {
|
|
5
5
|
readFileAtRef
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-6BXSZPHC.js";
|
|
7
7
|
|
|
8
8
|
// src/core/semantic/file-change.ts
|
|
9
9
|
import { readFile } from "fs/promises";
|