@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.
Files changed (29) hide show
  1. package/README.md +197 -146
  2. package/dist/{build-2LO7GD2S.js → build-3DSBMMI3.js} +9 -9
  3. package/dist/{check-ZTKGGF4M.js → check-SIQJU6Z2.js} +8 -8
  4. package/dist/{chunk-RGD422OM.js → chunk-26DHEXGS.js} +1 -1
  5. package/dist/{chunk-LXKLCLL6.js → chunk-43VLELTL.js} +1 -1
  6. package/dist/{chunk-XUBWCTJZ.js → chunk-6BXSZPHC.js} +18 -0
  7. package/dist/{chunk-MZZOE7L4.js → chunk-7OPTWU6E.js} +1 -1
  8. package/dist/{chunk-V22IPJ7A.js → chunk-EISTWXDW.js} +5 -1
  9. package/dist/{chunk-4LLK2S4R.js → chunk-FA64KWJ2.js} +8 -8
  10. package/dist/{chunk-LJDDS4RR.js → chunk-FZEXZBXB.js} +16 -2
  11. package/dist/{chunk-NSNZOH5P.js → chunk-Q7RLUZSN.js} +2 -2
  12. package/dist/{chunk-ISRF5F3Q.js → chunk-QRPI6TJE.js} +1 -1
  13. package/dist/{chunk-O4LZKXNZ.js → chunk-RC23JQKI.js} +50 -1
  14. package/dist/{chunk-VTJ67TT5.js → chunk-TCISS2AS.js} +4 -4
  15. package/dist/{chunk-RVXUCJYA.js → chunk-VYWPQKMS.js} +7 -0
  16. package/dist/{chunk-SWPVR2DF.js → chunk-ZH2PQUNL.js} +7 -7
  17. package/dist/cli.js +30 -26
  18. package/dist/{diff-GFI6RAEA.js → diff-HSJBZFMC.js} +9 -5
  19. package/dist/{doctor-GH4RZVEH.js → doctor-B7MHFIYJ.js} +3 -3
  20. package/dist/{env-QORDD6FQ.js → env-I7JZRWDN.js} +7 -7
  21. package/dist/{impact-AF453PLK.js → impact-PRZ6HEP5.js} +54 -21
  22. package/dist/index.js +2 -2
  23. package/dist/{init-KJIUNGAN.js → init-O2BY3FOY.js} +3 -3
  24. package/dist/{insight-DZ5NQY6D.js → insight-JQLTGFYB.js} +8 -8
  25. package/dist/{pr-UFZRZX6S.js → pr-Y724QAHE.js} +19 -6
  26. package/dist/{run-6PNH4QRA.js → run-C6VAQOVM.js} +9 -9
  27. package/dist/{symbol-graph-YYUEMZGR.js → symbol-graph-E5GXKDFZ.js} +3 -3
  28. package/dist/{workspace-X5FUBTY3.js → workspace-HRTFGWKN.js} +4 -4
  29. package/package.json +3 -3
package/README.md CHANGED
@@ -1,176 +1,227 @@
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>
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
- [![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)
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
- `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).
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
- ## Requirements
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
- | | 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 |
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
- Optional: **[hyperfine](https://github.com/sharkdp/hyperfine)**, required by `pnpm bench`.
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
- ## Installation
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
- 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
57
- ```
58
-
59
- ---
60
-
61
- ## Development
54
+ # run it once
55
+ npx @blzsky/variant impact --base origin/main
62
56
 
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)
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 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.
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
- Benchmark methodology and how to reproduce the published numbers: [benchmarks/README.md](./benchmarks/README.md).
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
- 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
67
+ npm install -D @blzsky/variant@latest
101
68
  ```
102
69
 
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
- ---
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
- ## 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:
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
- pnpm publish # prepublishOnly runs `pnpm test:all && pnpm build`
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
- 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).
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
- 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
- ---
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-4LLK2S4R.js";
6
- import "./chunk-LXKLCLL6.js";
7
- import "./chunk-ISRF5F3Q.js";
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-SWPVR2DF.js";
11
+ } from "./chunk-ZH2PQUNL.js";
12
12
  import "./chunk-V5CTGXMM.js";
13
- import "./chunk-RGD422OM.js";
13
+ import "./chunk-26DHEXGS.js";
14
14
  import "./chunk-YYGCSKIQ.js";
15
15
  import "./chunk-GDSZFIHM.js";
16
- import "./chunk-MZZOE7L4.js";
16
+ import "./chunk-7OPTWU6E.js";
17
17
  import "./chunk-3E5LBU3O.js";
18
- import "./chunk-RVXUCJYA.js";
19
- import "./chunk-XUBWCTJZ.js";
18
+ import "./chunk-6BXSZPHC.js";
20
19
  import "./chunk-6J4XDG44.js";
21
- import "./chunk-V22IPJ7A.js";
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-SWPVR2DF.js";
3
+ } from "./chunk-ZH2PQUNL.js";
4
4
  import "./chunk-V5CTGXMM.js";
5
- import "./chunk-RGD422OM.js";
5
+ import "./chunk-26DHEXGS.js";
6
6
  import "./chunk-YYGCSKIQ.js";
7
- import "./chunk-MZZOE7L4.js";
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-V22IPJ7A.js";
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) {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  ConfigError
3
- } from "./chunk-RVXUCJYA.js";
3
+ } from "./chunk-VYWPQKMS.js";
4
4
 
5
5
  // src/core/config/loader.ts
6
6
  import { existsSync } from "fs";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  CacheError
3
- } from "./chunk-RVXUCJYA.js";
3
+ } from "./chunk-VYWPQKMS.js";
4
4
 
5
5
  // src/core/cache/store.ts
6
6
  import { mkdirSync, renameSync, unlinkSync, writeFileSync } from "fs";
@@ -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
  };
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-3E5LBU3O.js";
4
4
  import {
5
5
  listChangedFiles
6
- } from "./chunk-XUBWCTJZ.js";
6
+ } from "./chunk-6BXSZPHC.js";
7
7
 
8
8
  // src/core/cache/git-diff.ts
9
9
  import path from "path";
@@ -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" && this.mode !== "quiet";
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-LXKLCLL6.js";
7
+ } from "./chunk-43VLELTL.js";
8
8
  import {
9
9
  renderInsights
10
- } from "./chunk-ISRF5F3Q.js";
10
+ } from "./chunk-QRPI6TJE.js";
11
11
  import {
12
12
  recordCacheMiss,
13
13
  runScheduled
14
- } from "./chunk-SWPVR2DF.js";
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-V22IPJ7A.js";
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-O4LZKXNZ.js";
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-LJDDS4RR.js";
3
+ } from "./chunk-FZEXZBXB.js";
4
4
  import {
5
5
  readFileAtRef
6
- } from "./chunk-XUBWCTJZ.js";
6
+ } from "./chunk-6BXSZPHC.js";
7
7
 
8
8
  // src/core/semantic/file-change.ts
9
9
  import { readFile } from "fs/promises";
@@ -6,7 +6,7 @@ import {
6
6
  } from "./chunk-6J4XDG44.js";
7
7
  import {
8
8
  getPrinter
9
- } from "./chunk-V22IPJ7A.js";
9
+ } from "./chunk-EISTWXDW.js";
10
10
 
11
11
  // src/cli/render/insight.ts
12
12
  function statusLabel(result) {