@vzn/vx 0.0.636 → 0.0.638
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 +39 -11
- package/docs/caching.md +246 -213
- package/docs/cli.md +791 -705
- package/docs/execution.md +68 -76
- package/docs/features.md +47 -42
- package/docs/patterns.md +26 -25
- package/docs/schema.md +392 -205
- package/docs/security.md +76 -39
- package/launcher.sh +43 -0
- package/package.json +5 -5
- package/schemas/cache.json +41 -19
- package/schemas/info.json +23 -11
- package/schemas/last.json +48 -2
- package/schemas/lock.json +1 -1
- package/schemas/plan.json +4 -4
- package/schemas/show.json +5 -3
- package/schemas/why.json +15 -1
- package/skills/vx/SKILL.md +28 -13
- package/src/bin.ts +30 -9
- package/src/cache/archive.ts +11 -0
- package/src/cache/cache.ts +124 -53
- package/src/cache/chained-cache.ts +4 -0
- package/src/cache/git-inputs.ts +277 -63
- package/src/cache/index.ts +15 -1
- package/src/cache/inputs.ts +102 -36
- package/src/cache/key-fold.ts +8 -7
- package/src/cache/layer.ts +15 -18
- package/src/cache/layered-cache.ts +20 -102
- package/src/cache/plugin-cache.ts +248 -0
- package/src/cache/policy.ts +32 -27
- package/src/cache/run-history.ts +104 -14
- package/src/cache/run-store.ts +77 -0
- package/src/cache/schema.ts +58 -31
- package/src/cache/scoped-cache.ts +237 -0
- package/src/cli/cache.ts +173 -34
- package/src/cli/completions.ts +11 -7
- package/src/cli/core-alias.ts +1 -1
- package/src/cli/foreign-flags.ts +13 -12
- package/src/cli/help.ts +110 -70
- package/src/cli/index.ts +67 -24
- package/src/cli/info.ts +43 -27
- package/src/cli/init.ts +115 -407
- package/src/cli/last.ts +157 -9
- package/src/cli/lock.ts +356 -122
- package/src/cli/plan-format.ts +7 -5
- package/src/cli/plugin-commands.ts +81 -14
- package/src/cli/plugin-templates.ts +11 -11
- package/src/cli/run.ts +116 -160
- package/src/cli/select.ts +50 -47
- package/src/cli/show.ts +83 -59
- package/src/cli/task-verb.ts +6 -13
- package/src/cli/upgrade.ts +415 -149
- package/src/cli/watch-cycle.ts +9 -2
- package/src/cli/watch-filter.ts +39 -61
- package/src/cli/watch-fs.ts +29 -16
- package/src/cli/watch-judge.ts +18 -4
- package/src/cli/watch-set.ts +16 -42
- package/src/cli/watch.ts +227 -230
- package/src/cli/why.ts +38 -18
- package/src/cli/workspace-config.ts +33 -70
- package/src/config.ts +169 -70
- package/src/exec/env.ts +1 -31
- package/src/exec/executor.ts +1 -5
- package/src/exec/kill-tree.ts +2 -1
- package/src/exec/sandbox-runtime.ts +12 -1
- package/src/exec/sandbox-violations.ts +9 -1
- package/src/graph/dependency-spec.ts +1 -1
- package/src/graph/index.ts +1 -0
- package/src/graph/priorities.ts +1 -1
- package/src/graph/scheduler.ts +27 -7
- package/src/graph/task-graph.ts +76 -24
- package/src/handoff.ts +169 -0
- package/src/index.ts +35 -58
- package/src/orchestrator/affected-tasks.ts +15 -8
- package/src/orchestrator/command-windows.ts +44 -0
- package/src/orchestrator/deferred-outputs.ts +12 -10
- package/src/orchestrator/doctor.ts +98 -110
- package/src/orchestrator/download-policy.ts +3 -3
- package/src/orchestrator/events.ts +19 -3
- package/src/orchestrator/excluded-keys.ts +0 -2
- package/src/orchestrator/execute-task.ts +147 -69
- package/src/orchestrator/failure-mode.ts +19 -10
- package/src/orchestrator/fingerprint-watch.ts +18 -15
- package/src/orchestrator/forecast.ts +10 -2
- package/src/orchestrator/framed-output.ts +9 -5
- package/src/orchestrator/graph-stage.ts +5 -5
- package/src/orchestrator/history.ts +126 -30
- package/src/orchestrator/hit-restore.ts +15 -3
- package/src/orchestrator/index.ts +20 -5
- package/src/orchestrator/local-shortcircuit.ts +2 -1
- package/src/orchestrator/lockfile-claim.ts +79 -20
- package/src/orchestrator/logger.ts +80 -41
- package/src/orchestrator/metrics.ts +78 -9
- package/src/orchestrator/miss-save.ts +3 -0
- package/src/orchestrator/options.ts +42 -29
- package/src/orchestrator/output-host.ts +72 -0
- package/src/orchestrator/plan.ts +6 -8
- package/src/orchestrator/plugin-host.ts +100 -66
- package/src/orchestrator/plugin.ts +173 -67
- package/src/orchestrator/prepare.ts +163 -78
- package/src/orchestrator/projects.ts +105 -99
- package/src/orchestrator/remote-prefetch.ts +11 -6
- package/src/orchestrator/run-context.ts +25 -6
- package/src/orchestrator/run-failures.ts +17 -8
- package/src/orchestrator/run-lock.ts +1 -1
- package/src/orchestrator/run-records.ts +1 -1
- package/src/orchestrator/run-report.ts +9 -9
- package/src/orchestrator/run-settings.ts +48 -0
- package/src/orchestrator/run.ts +244 -119
- package/src/orchestrator/sandbox-fix.ts +39 -20
- package/src/orchestrator/sandbox-request.ts +84 -13
- package/src/orchestrator/signals.ts +1 -1
- package/src/orchestrator/stable-keys.ts +13 -11
- package/src/orchestrator/status-line.ts +49 -7
- package/src/orchestrator/summary.ts +115 -106
- package/src/orchestrator/task-hash.ts +12 -83
- package/src/orchestrator/telemetry.ts +4 -4
- package/src/settings.ts +428 -0
- package/src/{orchestrator → util}/colors.ts +5 -0
- package/src/util/config-frame.ts +31 -2
- package/src/util/errors.ts +33 -1
- package/src/util/essential-env.ts +31 -0
- package/src/util/hide-env.ts +107 -0
- package/src/util/index.ts +16 -3
- package/src/util/paths.ts +10 -0
- package/src/util/refusal-notice.ts +111 -0
- package/src/util/report.ts +31 -0
- package/src/util/settle.ts +49 -30
- package/src/util/size.ts +23 -4
- package/src/util/timing.ts +3 -2
- package/src/util/ui.ts +186 -0
- package/src/util/ulid.ts +1 -1
- package/src/util/verbs.ts +0 -15
- package/src/workspace/affected.ts +84 -232
- package/src/workspace/config-eval.ts +15 -20
- package/src/workspace/config-imports.ts +78 -3
- package/src/workspace/config-schema.ts +209 -268
- package/src/workspace/dependency-rules.ts +65 -0
- package/src/workspace/filter.ts +2 -2
- package/src/workspace/fingerprint.ts +11 -121
- package/src/workspace/index.ts +20 -27
- package/src/workspace/load-reads.ts +4 -4
- package/src/workspace/lockfile.ts +96 -140
- package/src/workspace/migration.ts +307 -215
- package/src/workspace/package-graph.ts +30 -216
- package/src/workspace/project-loader.ts +80 -285
- package/src/workspace/repo-id.ts +2 -1
- package/src/workspace/workspace.ts +363 -831
- package/src/cache/config-evals.ts +0 -130
- package/src/workspace/config-cache.ts +0 -661
- package/src/workspace/migrate-scripts.ts +0 -1121
package/docs/caching.md
CHANGED
|
@@ -31,11 +31,18 @@ opt _out_ via `cache: false`. We chose the strictest of the three.
|
|
|
31
31
|
|
|
32
32
|
## Cache key derivation
|
|
33
33
|
|
|
34
|
+
Core keys no package file: no `package.json`, lockfile or install
|
|
35
|
+
setting is a part of its own. A `package.json` keys a task as an input
|
|
36
|
+
it lists (step 10), a lockfile or install setting as a plugin's key
|
|
37
|
+
material (step 9); `@vzn/vx-lockfile`'s `pnpm()`, `bun()`, `npm()` and
|
|
38
|
+
`yarn()` add both. Without a lockfile plugin an install moves no key, so
|
|
39
|
+
a JS workspace declares one (`vx init` and `@vzn/vx-migrate` write it).
|
|
40
|
+
|
|
34
41
|
The cache key for one task is a **16-hex xxHash3 digest**, seed-chained
|
|
35
42
|
over (in order):
|
|
36
43
|
|
|
37
44
|
1. **`CACHE_VERSION`** — the key-derivation sentinel
|
|
38
|
-
(currently `'vx-cache-
|
|
45
|
+
(currently `'vx-cache-v43'`, in `src/cache/key-fold.ts`). Bumped when
|
|
39
46
|
the key derivation or the artifact container changes, or stored bytes
|
|
40
47
|
are wrong under an unchanged key. See
|
|
41
48
|
[§ Bumping CACHE_VERSION](#bumping-cache_version).
|
|
@@ -43,54 +50,7 @@ over (in order):
|
|
|
43
50
|
identical everything else still produce distinct keys — protects
|
|
44
51
|
against e.g. `pkg-a#build` accidentally cache-hitting on a
|
|
45
52
|
`pkg-b#build` entry.
|
|
46
|
-
3. **
|
|
47
|
-
marker found at the root (see
|
|
48
|
-
[`modules/fingerprint.md`](./modules/fingerprint.md)):
|
|
49
|
-
`pnpm-lock.yaml`, `package-lock.json`, `npm-shrinkwrap.json`,
|
|
50
|
-
`yarn.lock`, `bun.lock`, `bun.lockb`, `pnpm-workspace.yaml`,
|
|
51
|
-
`.yarnrc.yml` (Yarn 4 catalogs, which `yarn.lock` does not record),
|
|
52
|
-
`.npmrc` and `bunfig.toml` (install settings no lockfile records),
|
|
53
|
-
and the content of each patch the Bun lockfile names under
|
|
54
|
-
patchedDependencies (it records a patch by path alone). Any
|
|
55
|
-
install-resolved change (a `bun install` that bumps `bun.lock`) or
|
|
56
|
-
any workspace-shape change invalidates _every_ cache entry. This is
|
|
57
|
-
the single global "the world changed" lever — and a plugin can take
|
|
58
|
-
one file off it: `VxPlugin.fingerprint` claims a lockfile, core
|
|
59
|
-
leaves it out of this digest, and the plugin's `key` hook folds what
|
|
60
|
-
the file means for each project instead (`@vzn/vx-lockfile`'s `pnpm()`
|
|
61
|
-
folds the project's own resolved dependency closure and the root
|
|
62
|
-
package's, so `pnpm update foo` re-keys only the projects that depend
|
|
63
|
-
on `foo` — every project when the root does, since the root's tools
|
|
64
|
-
run from the root `node_modules/.bin` on every task's PATH; its `bun()` is the same
|
|
65
|
-
for `bun.lock`, and this repo declares that one). The
|
|
66
|
-
config-evaluation cache still keys on every file: a config may import
|
|
67
|
-
a dependency the lockfile resolved.
|
|
68
|
-
|
|
69
|
-
The fingerprint is read once per run, before any task. A task in the
|
|
70
|
-
root project may rewrite one of these files — `pnpm install` without
|
|
71
|
-
`--frozen-lockfile` updates the lockfile and the installed tree — and
|
|
72
|
-
every key taken before it names the old one. So a task that may
|
|
73
|
-
(unsandboxed in the root project, or granted a write over one) tells
|
|
74
|
-
the run when its command ran, the run re-checks the files then (one
|
|
75
|
-
`stat` each, following a symlink as both reads do — an `lstat` missed
|
|
76
|
-
a lockfile rewritten through a link, item 760 — the bytes compared
|
|
77
|
-
for any written since the read), and
|
|
78
|
-
once one moved nothing keyed on the old digest is probed or saved for
|
|
79
|
-
the rest of the run, with one status line naming the file. Every
|
|
80
|
-
reader after such a task is keyed late, not up front. Without it the
|
|
81
|
-
run after a lockfile rewrite restored a build made against the old
|
|
82
|
-
install, and a save filed a build against the new install under the
|
|
83
|
-
old lockfile's key, replayed whenever the tree went back (item 750,
|
|
84
|
-
[`modules/fingerprint-watch.md`](./modules/fingerprint-watch.md)).
|
|
85
|
-
|
|
86
|
-
4. **Project `package.json` hash** — the git blob OID of the project's
|
|
87
|
-
`package.json`: the index OID when the file is tracked and clean,
|
|
88
|
-
else `cache.hashFile`'s blob OID of the worktree bytes (empty when
|
|
89
|
-
there is none). Folded in implicitly (Turbo / Nx parity).
|
|
90
|
-
Covers the case where `cache.inputs.files: ['src/**']` is narrow
|
|
91
|
-
and a `package.json` dep change would otherwise leak undetected.
|
|
92
|
-
(Added at v12; rationale in [§ History](#history).)
|
|
93
|
-
5. **Task config hash** — `xxh3(JSON.stringify(hashableConfig(node.config)))`
|
|
53
|
+
3. **Task config hash** — `xxh3(JSON.stringify(hashableConfig(node.config)))`
|
|
94
54
|
of the _evaluated_ task config. The projection is one field wide:
|
|
95
55
|
`exec.remote` is dropped, because it says WHERE a task runs and not
|
|
96
56
|
what it does (§ What's NOT in the key). Captures:
|
|
@@ -107,55 +67,87 @@ over (in order):
|
|
|
107
67
|
`process.env`-read at config-load time injected. Bun's native
|
|
108
68
|
`await import()` evaluates the module and bakes those values into
|
|
109
69
|
the object before we serialize.
|
|
110
|
-
|
|
70
|
+
4. **`forwardArgs`** — CLI args passed after `--`. Folded into the
|
|
111
71
|
key so `vx run test -- --watch` doesn't cache-hit a previous
|
|
112
72
|
`vx run test`. Scoped to the user-requested tasks only — dependsOn-
|
|
113
73
|
pulled deps don't see them (their cache identity stays clean) — and
|
|
114
74
|
to a command: a requested default `build` runs none and folds none
|
|
115
75
|
(X-119).
|
|
116
|
-
|
|
76
|
+
5. **`cache.inputs.env` resolved values** — `[name, value]` pairs
|
|
117
77
|
read from host `process.env` at hash time (delimited `name\0value`
|
|
118
78
|
so boundaries are unambiguous). Listed names get their current
|
|
119
79
|
values; an unset name folds its bare name, with no `\0`, so unset
|
|
120
80
|
and set-to-empty are two keys (the child tells them apart:
|
|
121
81
|
`passThrough` leaves an unset name out). A name holding a NUL is
|
|
122
82
|
refused at load.
|
|
123
|
-
|
|
83
|
+
6. **`cache.inputs.runtime` resolved output** — `[command, output]`
|
|
124
84
|
pairs, where `output` is the trimmed stdout of each command run via
|
|
125
85
|
`sh -c` in the **project dir** at hash time; a command with stderr
|
|
126
86
|
(or a NUL in stdout) folds `\0<stdout length>\0<stdout><stderr>`,
|
|
127
87
|
so bytes moved between the streams move the key.
|
|
128
|
-
The runtime-output analog of step
|
|
129
|
-
the resolved config (step
|
|
88
|
+
The runtime-output analog of step 5: the command _strings_ are in
|
|
89
|
+
the resolved config (step 3), their _output_ is resolved live every
|
|
130
90
|
run. Folded with the command count + each `command\0output` pair.
|
|
131
|
-
|
|
91
|
+
7. **`cache.inputs.workspaceRuntime` resolved output** — same as step
|
|
132
92
|
8 but commands run at the **workspace root**, and the pairs fold
|
|
133
93
|
into a **distinct namespace** (`ws-runtime-values:`) so an identical
|
|
134
94
|
`(command, output)` never aliases the project-cwd `runtime` values.
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
95
|
+
8. **Filtered upstream task cache hashes** — every upstream task's
|
|
96
|
+
own cache key, filtered by `cache.inputs.tasks` (default: all of
|
|
97
|
+
them). Sorted by hash before folding so the ordering of `dependsOn`
|
|
98
|
+
doesn't change the key. This is the cascade mechanism: if anything
|
|
99
|
+
beneath you changes, your hash changes too. The set is `dependsOn`'s,
|
|
100
|
+
never the run's selection: a dependency `--exclude-dependencies`
|
|
101
|
+
keeps from running is still folded, with the key a full run would
|
|
102
|
+
derive for it (nx#35234), so the flag moves no key. Its outputs are
|
|
103
|
+
whatever is on disk, which nothing proves current, so a task whose
|
|
104
|
+
key folds one, and everything built on it, may hit but does not
|
|
105
|
+
save (`orchestrator/excluded-keys.ts`); a task with
|
|
106
|
+
`cache.inputs.tasks: []` folds none and saves as usual.
|
|
107
|
+
9. **Plugin key material** — the `{ name: value }` pairs a plugin's
|
|
108
|
+
`key(task, ctx)` stage returned for this task, stored on the node as
|
|
109
|
+
sorted `plugin/name` pairs and folded after the upstream keys and BEFORE the input files,
|
|
110
|
+
ONLY when non-empty — so a workspace with no `key` plugin derives
|
|
111
|
+
byte-identical keys to one before the stage existed (no
|
|
112
|
+
`CACHE_VERSION` bump when it shipped). `vx why` names a changed pair
|
|
113
|
+
as `plugin <plugin>/<name>`. Two plugins of one package (a plugin's
|
|
114
|
+
name is its package's) that return one name are told apart as
|
|
115
|
+
`<name>`, `<name>#2`, … in value order, so declaration order still
|
|
116
|
+
keys nothing (item 1028).
|
|
117
|
+
|
|
118
|
+
A plugin that reads a root file claims it (`VxPlugin.fingerprint`).
|
|
119
|
+
`@vzn/vx-lockfile`'s `pnpm()` folds the project's own resolved
|
|
120
|
+
dependency closure from `pnpm-lock.yaml` and the root package's, so
|
|
121
|
+
`pnpm update foo` re-keys only the projects that depend on `foo` —
|
|
122
|
+
every project when the root does, since the root's tools run from the
|
|
123
|
+
root `node_modules/.bin` on every task's PATH; `bun()`, `npm()` and
|
|
124
|
+
`yarn()` are the same for their lockfiles (`npm()` reads
|
|
125
|
+
`npm-shrinkwrap.json` over `package-lock.json`, as npm does; `bun()`
|
|
126
|
+
refuses a `bun.lockb` with no `bun.lock`, which it cannot read), and
|
|
127
|
+
this repo declares `bun()`. Each also claims its manager's install
|
|
128
|
+
settings (`pnpm()`: `pnpm-workspace.yaml`, `.npmrc`; `bun()`:
|
|
129
|
+
`bunfig.toml`, `.npmrc`; `npm()`: `.npmrc`; `yarn()`: `.yarnrc.yml`)
|
|
130
|
+
and folds each whole into every task.
|
|
131
|
+
|
|
132
|
+
The run reads each claimed root file once, before any task. A task
|
|
133
|
+
in the root project may rewrite one — `pnpm install` without
|
|
134
|
+
`--frozen-lockfile` updates the lockfile and the installed tree — and
|
|
135
|
+
every key taken before it names the old one. So a task that may
|
|
136
|
+
(unsandboxed in the root project, or granted a write over one) tells
|
|
137
|
+
the run when its command ran, the run re-checks the claimed files
|
|
138
|
+
then (one `stat` each, following a symlink as both reads do — an
|
|
139
|
+
`lstat` missed a lockfile rewritten through a link, item 760 — the
|
|
140
|
+
bytes compared for any written since the read), and once one moved
|
|
141
|
+
nothing keyed on the old bytes is probed or saved for the rest of the
|
|
142
|
+
run, with one status line naming the file. Every reader after such a
|
|
143
|
+
task is keyed late, not up front. Without it the run after a lockfile
|
|
144
|
+
rewrite restored a build made against the old install, and a save
|
|
145
|
+
filed a build against the new install under the old lockfile's key,
|
|
146
|
+
replayed whenever the tree went back (item 750,
|
|
147
|
+
[`modules/fingerprint-watch.md`](./modules/fingerprint-watch.md)).
|
|
148
|
+
With nothing claimed, no root file is read or watched.
|
|
149
|
+
|
|
150
|
+
10. **Input files' content hashes** — `cache.inputs.files` resolved to
|
|
159
151
|
a concrete list of project-relative paths (gitignore-aware,
|
|
160
152
|
declared-outputs-excluded, nested-projects-excluded), each file
|
|
161
153
|
contributing its **git blob OID** (v20). On a clean tree the OID
|
|
@@ -225,7 +217,7 @@ add` under a clean filter (`core.autocrlf=true`, a `text` rule)
|
|
|
225
217
|
and every project under it is selected.
|
|
226
218
|
|
|
227
219
|
A task with **no `cache` block** derives a key too — its dependents
|
|
228
|
-
fold it (step
|
|
220
|
+
fold it (step 8) — and, declaring nothing, folds **every file in
|
|
229
221
|
its project**: `**/*`, gitignore-aware, untracked files included,
|
|
230
222
|
and its own outputs not excluded, since it declared none. So its
|
|
231
223
|
key moves whenever it writes a file git does not ignore: a cached
|
|
@@ -238,8 +230,7 @@ add` under a clean filter (`core.autocrlf=true`, a `text` rule)
|
|
|
238
230
|
|
|
239
231
|
Declaring nothing, such a task may also have **written** anywhere
|
|
240
232
|
in its project, so what the run learned about that project at its
|
|
241
|
-
start — the git listing
|
|
242
|
-
— is dropped once its command exits, pass or fail, and a later key
|
|
233
|
+
start — the git listing and the index OIDs — is dropped once its command exits, pass or fail, and a later key
|
|
243
234
|
re-lists and hashes by content. For the same reason a task after it
|
|
244
235
|
in the same project (`dependsOn`, even with `tasks: []`) is never
|
|
245
236
|
keyed up front by the local short-circuit or the remote prefetch.
|
|
@@ -562,7 +553,7 @@ Hard invariants of the remote prefetch:
|
|
|
562
553
|
later `get` finds it as a now-local hit — still reports
|
|
563
554
|
`source: 'remote'`, so the outcome is `cache-hit-remote`.
|
|
564
555
|
- **A remote-read-off policy** (`--no-cache`, `--force`, or
|
|
565
|
-
`--cache=remote:`) fires no prefetch.
|
|
556
|
+
`--cache-policy=remote:`) fires no prefetch.
|
|
566
557
|
- **Never fail.** Every remote path (get / put / ingest / prefetch)
|
|
567
558
|
catches all errors and degrades to a cache miss. A remote 500, a
|
|
568
559
|
network drop, a corrupt artifact, or a failed integrity check can
|
|
@@ -606,8 +597,7 @@ Caching is controlled by a four-axis `CachePolicy` — **localRead**,
|
|
|
606
597
|
**localWrite**, **remoteRead**, **remoteWrite** — independent toggles,
|
|
607
598
|
each enforced inside the matching cache layer at construction time. The
|
|
608
599
|
local `Cache` gets a `{ read, write }` slice gating its task artifact
|
|
609
|
-
get/save
|
|
610
|
-
file-hash memo's writes (never `recordRun` / `stats` / `prune` / ingest
|
|
600
|
+
get/save and the file-hash memo's writes (never `recordRun` / `stats` / `prune` / ingest
|
|
611
601
|
/ key derivation); the `LayeredCache` additionally gates its own remote
|
|
612
602
|
read-through (`remoteRead`), upload (`remoteWrite`), and prefetch
|
|
613
603
|
(`remoteRead`). The orchestrator derives two booleans per task:
|
|
@@ -623,14 +613,23 @@ restore, no output clean, no local save), and its result lives in the
|
|
|
623
613
|
remote executor's own store.
|
|
624
614
|
|
|
625
615
|
The CLI maps three flags to a policy (precedence: start all-on → apply
|
|
626
|
-
`--cache` → `--no-cache` forces all off → `--force` forces both reads
|
|
616
|
+
`--cache-policy` → `--no-cache` forces all off → `--force` forces both reads
|
|
627
617
|
off): `--no-cache` = everything off (no read, no write, no output
|
|
628
618
|
clean); `--force` = reads off / writes on (re-execute and refresh the
|
|
629
|
-
cache, outputs cleaned); `--cache=<spec>` = explicit per-layer control.
|
|
630
|
-
|
|
619
|
+
cache, outputs cleaned); `--cache-policy=<spec>` = explicit per-layer control.
|
|
620
|
+
Off CI, unless a spec names the remote, `remoteWrite` starts off: a key
|
|
621
|
+
holds only declared inputs, so a laptop's undeclared tool or file could
|
|
622
|
+
replay on CI. See `docs/cli.md` § Cache control.
|
|
623
|
+
|
|
624
|
+
The setup we recommend for a shared remote: the write token only in the
|
|
625
|
+
default branch's CI (a protected environment), a read-only token on
|
|
626
|
+
laptops and in pull request jobs, and `cache.scope` (`github()` sets it)
|
|
627
|
+
so a PR's own writes, when its token allows them, land apart. The token
|
|
628
|
+
is the boundary; the policy and the scope are the client's half
|
|
629
|
+
(`docs/security.md` § Cache poisoning).
|
|
631
630
|
|
|
632
631
|
One subtlety: when `localWrite` is off but `remoteWrite` is on
|
|
633
|
-
(`--cache=local:,remote:rw`), there's no on-disk artifact for the
|
|
632
|
+
(`--cache-policy=local:,remote:rw`), there's no on-disk artifact for the
|
|
634
633
|
`LayeredCache` to read before uploading — so it packs the tar.zst bytes
|
|
635
634
|
in memory (`Cache.packArtifactBytes`) and uploads those.
|
|
636
635
|
|
|
@@ -684,8 +683,8 @@ before the command ran — at the task's start, or up front by the local
|
|
|
684
683
|
short-circuit — and the save files the outputs under it, so the inputs
|
|
685
684
|
must still be what it describes. Two checks, in order. The key
|
|
686
685
|
re-derived just before the command (`describeTaskInputs`, which the
|
|
687
|
-
executor seam needs anyway) must equal it. Then each input file
|
|
688
|
-
|
|
686
|
+
executor seam needs anyway) must equal it. Then each input file is
|
|
687
|
+
`lstat`ed once: a file whose ctime falls
|
|
689
688
|
after, or within `FILE_HASH_RACY_MS` before, the moment its digest was
|
|
690
689
|
learned — the enumeration's start for an index OID, the describe for a
|
|
691
690
|
hashed file — is hashed again and compared, and a file that is gone has
|
|
@@ -698,9 +697,9 @@ edit, was filed under the key and restored over the original (item
|
|
|
698
697
|
one status line names the file (``[vx] app#format: `packages/app/a.ts`
|
|
699
698
|
changed after its key was taken — …``), and the run forgets what it
|
|
700
699
|
knew about the project, as after an uncached task (§ Cache key
|
|
701
|
-
derivation, step
|
|
700
|
+
derivation, step 10). This covers a formatter rewriting its own input
|
|
702
701
|
(turborepo#10111) and a user's edit mid-run (turborepo#1146). A task
|
|
703
|
-
left remote (`--download=none`) is held to the same checks: a moved
|
|
702
|
+
left remote (`--cache-download=none`) is held to the same checks: a moved
|
|
704
703
|
key still lets a local consumer fetch its outputs, but the fetch saves
|
|
705
704
|
no entry; until X-123 it saved them under the old key. A task
|
|
706
705
|
that rewrites its own input to the SAME bytes (`sed -i` always writes)
|
|
@@ -713,7 +712,10 @@ task of the same project running alongside can change an input and put
|
|
|
713
712
|
it back. A task that rewrites its own input to identical bytes (tsdown's
|
|
714
713
|
`package.json`) sets `cache.inputStability: { strategy: 'mutable' }`, and a write
|
|
715
714
|
during its command is then judged by its bytes, as an earlier write is;
|
|
716
|
-
different bytes still withhold the entry
|
|
715
|
+
different bytes still withhold the entry, and so does a write made while
|
|
716
|
+
another command that may write the file ran (`command-windows.ts`: one
|
|
717
|
+
in the same project, or one with a write grant elsewhere in the
|
|
718
|
+
workspace), named on the line as `while <task> ran`. `VX_CACHE_INPUT_STABILITY_STRATEGY=mutable`
|
|
717
719
|
makes that the default (owner, 2026-10-12); `'unchecked'` skips the
|
|
718
720
|
check, and `severity: 'error'` fails the task instead of warning
|
|
719
721
|
(`docs/schema.md`). A file ADDED under an
|
|
@@ -737,14 +739,14 @@ transitively: that key does not name the bytes they built from (`[vx] app#use: r
|
|
|
737
739
|
over app#gen's outputs, which its key no longer describes — …`). Until
|
|
738
740
|
2026-09-27 (A-12) they saved, and once the input was put back they hit
|
|
739
741
|
the edit's output.
|
|
740
|
-
A
|
|
741
|
-
|
|
742
|
+
A claimed root file a task rewrote since the run read it (§ Cache key
|
|
743
|
+
derivation, step 9) withholds the save the same way, and no key
|
|
742
744
|
taken before it is probed or restored: a hit the up-front probe found
|
|
743
745
|
goes back to the scheduler and runs once its deps are done. Until X-124
|
|
744
746
|
that hit restored the old install's bytes.
|
|
745
747
|
|
|
746
748
|
A miss that ran here and saves **nothing** — it failed, the cache
|
|
747
|
-
policy writes nothing (`--cache=local:r,remote:r`), an upstream failed
|
|
749
|
+
policy writes nothing (`--cache-policy=local:r,remote:r`), an upstream failed
|
|
748
750
|
under `--continue` — runs the same input re-check and, when an input
|
|
749
751
|
moved, forgets the project as above; otherwise its declared outputs are
|
|
750
752
|
resolved and marked in the git snapshot exactly as a save marks them.
|
|
@@ -807,25 +809,40 @@ Declared `cache.outputs.files` are wiped in two distinct places:
|
|
|
807
809
|
list instead of walking again: nothing is awaited between them (X-161).
|
|
808
810
|
|
|
809
811
|
Both branches use the same `cleanOutputs` helper (`src/cache/inputs.ts`)
|
|
810
|
-
with the same boundary rules, and
|
|
811
|
-
whatever the glob: `.git` and
|
|
812
|
+
with the same boundary rules, and vx's own paths stay off the wipe
|
|
813
|
+
whatever the glob: `.git`, `.vx` and each `vx.lock.json` (`OUTPUT_NEVER`;
|
|
814
|
+
`.vx` holds the workspace config and the lock, which a clean deleted and a
|
|
815
|
+
hit restored stale while `.vx/cache` alone was listed). `node_modules` is
|
|
812
816
|
off it too unless a glob names it (`node_modules/**`, an install task's
|
|
813
817
|
legitimate output); until 2026-09-27 (A-13) `**/*.js` cleaned every
|
|
814
818
|
installed `.js`, and a `workspaceFiles` output glob reached even `.git`.
|
|
815
|
-
|
|
816
|
-
is still
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
819
|
+
No clean deletes a file git tracks (owner, 2026-10-14). Under an output
|
|
820
|
+
glob it is still an output: the task may rewrite it (TanStack Router's
|
|
821
|
+
committed `routeTree.gen.ts`), the artifact saves the bytes the run left,
|
|
822
|
+
and a restore writes them. It also stays in the task's key, even under its
|
|
823
|
+
own output glob, so an edit moves the key and the saved bytes follow from
|
|
824
|
+
it; the task's rewrite is no input that moved mid-run. Until then the
|
|
825
|
+
clean deleted a committed file under an output, and a reader with no edge
|
|
826
|
+
to its producer failed naming only the file (A-48, vueuse's
|
|
827
|
+
`metadata/index.json`). Untracked files under the glob are cleaned as
|
|
828
|
+
before. Paths compare as
|
|
829
|
+
git compares them: case-folded under `core.ignorecase` (macOS), and a
|
|
830
|
+
file inside a submodule counts as committed.
|
|
820
831
|
Workspace outputs take the same rules, and a path an output `!` entry
|
|
821
832
|
takes back (A-44) is off the wipe, the artifact and the restore alike.
|
|
822
833
|
Skipped when:
|
|
823
834
|
|
|
824
835
|
- `cache.outputs.files` is empty (nothing declared as output).
|
|
825
836
|
- The task's `willWrite` is false — no write axis is enabled (e.g.
|
|
826
|
-
`--no-cache`, or a read-only `--cache=local:r`). The user is debugging
|
|
837
|
+
`--no-cache`, or a read-only `--cache-policy=local:r`). The user is debugging
|
|
827
838
|
and managing the tree, so vx leaves it alone. `--force` keeps writes
|
|
828
839
|
on, so it DOES clean (the saved snapshot must be clean).
|
|
840
|
+
- `cache.outputs.clean` is `false` (on the task, else the workspace's
|
|
841
|
+
`cache.outputs.clean`): the command sees last run's outputs (tsc's
|
|
842
|
+
`.tsbuildinfo`). You opt out of the clean-tree guarantee: the save
|
|
843
|
+
stores every file the outputs match after the run, so a leftover from
|
|
844
|
+
an older build is cached and restored everywhere. A retry cleans as
|
|
845
|
+
usual; a hit still cleans before its restore.
|
|
829
846
|
|
|
830
847
|
A declared output the process cannot remove (a `dist/` another user
|
|
831
848
|
wrote, a read-only checkout) fails the task with `cannot remove declared
|
|
@@ -851,7 +868,7 @@ function of the cache key.
|
|
|
851
868
|
Two cached tasks whose declared outputs overlap are refused at graph
|
|
852
869
|
build. By default that holds even when one depends on the other: the
|
|
853
870
|
workspace rule `rules.exclusiveOutputs` (on unless set to `false` in
|
|
854
|
-
|
|
871
|
+
`.vx/workspace.ts`, X-53) keeps one path to one task, and its refusal says
|
|
855
872
|
how to turn it off. The shape below is correct, only slower: a stamp
|
|
856
873
|
before every run, a diff after, and a clean by rows. Give each task its
|
|
857
874
|
own output path where you can (`dist-individual` beside `dist`).
|
|
@@ -895,28 +912,28 @@ the next warm run; the design note keeps that shape out of scope.
|
|
|
895
912
|
|
|
896
913
|
A task's cache becomes invalid when any of these change:
|
|
897
914
|
|
|
898
|
-
| Trigger | Mechanism
|
|
899
|
-
| ---------------------------------------------------------------------------------------------------------------------------------------- |
|
|
900
|
-
| Edit a file in the task's `inputs.files` set | step
|
|
901
|
-
| Edit a file in the task's `inputs.workspaceFiles` set (root-anchored; may live in ANY project's dir — the documented boundary exception) | step
|
|
902
|
-
| Any package manager updates a lockfile (`pnpm`, `npm`, `yarn`, `bun`) | step
|
|
903
|
-
| Edit `pnpm-workspace.yaml`
|
|
904
|
-
| Edit `package.json`'s `workspaces` field |
|
|
905
|
-
| Edit the project's `package.json` (dep / version / scripts change) | step
|
|
906
|
-
| Edit the task's `vx.config.ts` | step
|
|
907
|
-
| Edit a config file that the task config imports | step
|
|
908
|
-
| Change CLI `forwardArgs` (after `--`) | step
|
|
909
|
-
| Change a `cache.inputs.env` host value | step
|
|
910
|
-
| Change the combined stdout+stderr of a `cache.inputs.runtime` command (resolved at hash time) | step
|
|
911
|
-
| Change the combined stdout+stderr of a `cache.inputs.workspaceRuntime` command (resolved at hash time) | step
|
|
912
|
-
| Upstream task's cache key changes (because its inputs changed) | step
|
|
913
|
-
| Bump `CACHE_VERSION` | step 1 — orphans every entry
|
|
914
|
-
| Change `exec.env.passThrough` _values_ alone | **NOT a trigger** by design — passThrough values are host-specific
|
|
915
|
-
| Change a file not in `inputs.files` / `inputs.workspaceFiles` | **NOT a trigger** by design — declare it explicitly
|
|
916
|
-
| Edit
|
|
917
|
-
| Change a file in a nested project's dir | **NOT a trigger** for the parent's `files` globs — project boundaries are hard (workspaceFiles is the explicit exception)
|
|
918
|
-
|
|
919
|
-
The cascade in step
|
|
915
|
+
| Trigger | Mechanism |
|
|
916
|
+
| ---------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
917
|
+
| Edit a file in the task's `inputs.files` set | step 10 of key derivation |
|
|
918
|
+
| Edit a file in the task's `inputs.workspaceFiles` set (root-anchored; may live in ANY project's dir — the documented boundary exception) | step 10 — resolved workspace files join the same input-file list |
|
|
919
|
+
| Any package manager updates a lockfile (`pnpm`, `npm`, `yarn`, `bun`) | step 9 — the `key` material of the plugin that claims it (`@vzn/vx-lockfile`: only the projects whose dependency closure moved); **NOT a trigger** with no such plugin, since core keys no package file |
|
|
920
|
+
| Edit an install setting (`.npmrc`, `bunfig.toml`, `pnpm-workspace.yaml`, `.yarnrc.yml`) | step 9 — the claiming lockfile plugin folds it whole into every task; **NOT a trigger** without one |
|
|
921
|
+
| Edit `package.json`'s `workspaces` field | membership: a project that joins or leaves changes which projects exist and which nested dirs its parent's globs exclude (step 10) |
|
|
922
|
+
| Edit the project's `package.json` (dep / version / scripts change) | step 10 — an input file of every cached task when `@vzn/vx-lockfile` is declared (it adds `package.json` to `cache.inputs.files`), or when a task lists it; **NOT a trigger** otherwise |
|
|
923
|
+
| Edit the task's `vx.config.ts` | step 3 (task config hash) |
|
|
924
|
+
| Edit a config file that the task config imports | step 3 (configHash sees the resolved object after Bun evaluates imports) |
|
|
925
|
+
| Change CLI `forwardArgs` (after `--`) | step 4 |
|
|
926
|
+
| Change a `cache.inputs.env` host value | step 5 |
|
|
927
|
+
| Change the combined stdout+stderr of a `cache.inputs.runtime` command (resolved at hash time) | step 6 |
|
|
928
|
+
| Change the combined stdout+stderr of a `cache.inputs.workspaceRuntime` command (resolved at hash time) | step 7 |
|
|
929
|
+
| Upstream task's cache key changes (because its inputs changed) | step 8 |
|
|
930
|
+
| Bump `CACHE_VERSION` | step 1 — orphans every entry |
|
|
931
|
+
| Change `exec.env.passThrough` _values_ alone | **NOT a trigger** by design — passThrough values are host-specific |
|
|
932
|
+
| Change a file not in `inputs.files` / `inputs.workspaceFiles` | **NOT a trigger** by design — declare it explicitly |
|
|
933
|
+
| Edit `.vx/projects.lock.json` | **NOT a trigger** — globally excluded from inputs (v24); it's vx's own metadata, never a task input |
|
|
934
|
+
| Change a file in a nested project's dir | **NOT a trigger** for the parent's `files` globs — project boundaries are hard (workspaceFiles is the explicit exception) |
|
|
935
|
+
|
|
936
|
+
The cascade in step 8 is what makes monorepo caching work: edit a
|
|
920
937
|
file in `lib/`, and every package that depends on `lib`'s `build` task
|
|
921
938
|
re-runs automatically.
|
|
922
939
|
|
|
@@ -934,7 +951,7 @@ them (X-57). The same set fences the output clean, so a root
|
|
|
934
951
|
glob pass drops a path under one of them (an ancestor lookup,
|
|
935
952
|
A-11). The only way
|
|
936
953
|
for project A to depend on project B's state via project-relative
|
|
937
|
-
globs is `dependsOn` + upstream-hash propagation (step
|
|
954
|
+
globs is `dependsOn` + upstream-hash propagation (step 8).
|
|
938
955
|
|
|
939
956
|
For a task with `exec.sandbox` and `cache`, the sandbox enforces that
|
|
940
957
|
"only". A workspace package reached through a `node_modules` link is
|
|
@@ -1020,7 +1037,7 @@ exactly on `cache.inputs.env`, and they share its asymmetry with the
|
|
|
1020
1037
|
lockfile:
|
|
1021
1038
|
|
|
1022
1039
|
- **The command _strings_ live in the resolved config**, so `vx lock`
|
|
1023
|
-
freezes them into
|
|
1040
|
+
freezes them into `.vx/projects.lock.json` (just as it freezes the env _names_
|
|
1024
1041
|
a task declares).
|
|
1025
1042
|
- **The command _output_ is resolved live at hash time on every run**
|
|
1026
1043
|
— inside `resolveInputs`, the same place env _values_ are read from
|
|
@@ -1052,10 +1069,10 @@ A probe runs in its own process group. One still running when vx exits
|
|
|
1052
1069
|
SIGKILLed with its whole tree; until 2026-09-27 (A-9) it ran on under
|
|
1053
1070
|
init. A `kill -9` of vx runs no exit hook, and leaves it.
|
|
1054
1071
|
|
|
1055
|
-
So
|
|
1056
|
-
them and folds their current output into the key. A `node -v`
|
|
1057
|
-
from `v20` to `v22` after the lock was written busts the
|
|
1058
|
-
|
|
1072
|
+
So a run loads the frozen command strings from the lock but still
|
|
1073
|
+
spawns them and folds their current output into the key. A `node -v`
|
|
1074
|
+
that goes from `v20` to `v22` after the lock was written busts the
|
|
1075
|
+
cache, exactly as a changed `NODE_ENV` value would — the TypeScript
|
|
1059
1076
|
escape hatch (`define: { TSC_VERSION: execSync(...) }`) goes stale here
|
|
1060
1077
|
because its value was baked into the config object at lock time, whereas
|
|
1061
1078
|
a runtime input re-resolves.
|
|
@@ -1107,7 +1124,7 @@ has staged). Machines sharing a workspace over a network file system
|
|
|
1107
1124
|
do not share a temp directory, so they do not share the lock.
|
|
1108
1125
|
|
|
1109
1126
|
An eviction can still land between a hit's probe and its restore: a
|
|
1110
|
-
`vx cache
|
|
1127
|
+
`vx cache clean` in another shell, or the `cache.retention` of another
|
|
1111
1128
|
workspace that shares the `--cache-dir` (a different workspace, so a
|
|
1112
1129
|
different lock), which cannot see this run's pending `accessed_at`
|
|
1113
1130
|
bumps and reads its fresh hits as old. The restore then finds no
|
|
@@ -1132,16 +1149,15 @@ only read keeps the entry (`…; left it (this cache is read-only)`).
|
|
|
1132
1149
|
|
|
1133
1150
|
The run must be able to write here — it records its history at the
|
|
1134
1151
|
end of every run, hit or miss — so a directory this user cannot write
|
|
1135
|
-
into (another user's `.vx`, a read-only checkout) fails the run before
|
|
1152
|
+
into (another user's `.vx/cache`, a read-only checkout) fails the run before
|
|
1136
1153
|
any task: `cache directory <path> is not writable (EACCES: …)`, with
|
|
1137
|
-
`--cache-dir <path>` as the way out. `vx cache
|
|
1154
|
+
`--cache-dir <path>` as the way out. `vx cache clean` is refused the
|
|
1138
1155
|
same way (its `--dry-run` only reads, and reads). The readers (`vx show`, `why`,
|
|
1139
|
-
`last`, `info`) open such a directory read-only and go on
|
|
1140
|
-
that misses the evaluation cache is evaluated live and not stored.
|
|
1156
|
+
`last`, `info`) open such a directory read-only and go on.
|
|
1141
1157
|
Where the directory cannot be created at all (a read-only checkout with
|
|
1142
1158
|
no cache yet) a run says `cannot create cache directory <path>
|
|
1143
|
-
(EACCES: …)`, naming the workspace `
|
|
1144
|
-
the readers and `vx cache
|
|
1159
|
+
(EACCES: …)`, naming the workspace `cache.dir` field and `--cache-dir`;
|
|
1160
|
+
the readers and `vx cache clean` find no cache and go on.
|
|
1145
1161
|
A full disk is the same kind of failure and is reported the same way,
|
|
1146
1162
|
never as a corrupt artifact and never as a stack: a save that runs out
|
|
1147
1163
|
of room is `[vx] cache save failed: ENOSPC …` (the task's work ran, the
|
|
@@ -1175,6 +1191,9 @@ By default the entries and their artifacts live in a **shared store**
|
|
|
1175
1191
|
in `~/.vx/<id>/cache`, and each workspace keeps only its own index in
|
|
1176
1192
|
its `.vx/cache`: every checkout of the repository hits what another
|
|
1177
1193
|
saved (a second clone, a worktree), and none reads another's history.
|
|
1194
|
+
Task timings are shared too: `task_durations`, what a duration forecast
|
|
1195
|
+
reads, so a new worktree has one on its first run. So are plugins' own
|
|
1196
|
+
entries (`plugin_blobs`, a plugin's `ctx.cache`).
|
|
1178
1197
|
The id is Nx 23's: 16 hex of a sha256 of the remote (`origin`, then
|
|
1179
1198
|
`upstream`, `base`, the first; `host/owner/repo` in lower case, so ssh
|
|
1180
1199
|
and https agree) and the workspace's path in the repository; with no
|
|
@@ -1185,7 +1204,8 @@ owner-only; vx closes one of yours that others can read (chmod 700), and one oth
|
|
|
1185
1204
|
```
|
|
1186
1205
|
~/.vx/<id>/cache/ the shared store
|
|
1187
1206
|
├── store.db entries, entry_stdout, output_files,
|
|
1188
|
-
│ entry_inputs,
|
|
1207
|
+
│ entry_inputs, task_durations,
|
|
1208
|
+
│ plugin_blobs, store_meta; artifacts
|
|
1189
1209
|
│ of at most 32 KiB compressed (v33)
|
|
1190
1210
|
└── <hash>.tar.zst the larger artifacts (below)
|
|
1191
1211
|
|
|
@@ -1204,7 +1224,7 @@ when its task next hits. The check, drop, re-create and stamp are one
|
|
|
1204
1224
|
write transaction, so another version's open waits rather than landing
|
|
1205
1225
|
between them. A home this user cannot write keeps the store
|
|
1206
1226
|
in `<workspaceRoot>/.vx/cache/` instead, silently. Name a
|
|
1207
|
-
cache directory (`
|
|
1227
|
+
cache directory (`cache.dir` in .vx/workspace.ts, `--cache-dir`, or
|
|
1208
1228
|
`VX_CACHE_DIR`, in that order of precedence, relative to the workspace
|
|
1209
1229
|
root) and it holds everything, shared with no other workspace:
|
|
1210
1230
|
|
|
@@ -1404,7 +1424,7 @@ miss, and the save that follows replaces it. An inline artifact with no
|
|
|
1404
1424
|
row is indexed the same way, from its bytes, in one transaction; a reset
|
|
1405
1425
|
keeps the `artifacts` table, but deleting `store.db` or `cache.db` by
|
|
1406
1426
|
hand takes the inline artifacts with it. A `.tmp-*` a crashed save
|
|
1407
|
-
left is never a hit. `vx cache
|
|
1427
|
+
left is never a hit. `vx cache clean` sweeps row-less files, and so does a run whose workspace declares `cache.retention`, at
|
|
1408
1428
|
most once an hour (the sweep's clock is `schema_meta.orphans_swept_at`;
|
|
1409
1429
|
the policy sums index rows, so orphans alone never make it due). A
|
|
1410
1430
|
row-less artifact may be in use: two vx versions share one store, and
|
|
@@ -1437,39 +1457,29 @@ hit replays it with pure SQL, never decompressing the artifact).
|
|
|
1437
1457
|
`SCHEMA_VERSION`, earlier or newer, is reset by the first run that opens
|
|
1438
1458
|
it (pre-alpha: no migrations; the index is an inventory, owner
|
|
1439
1459
|
2026-10-06): every table but `schema_meta` is dropped and recreated,
|
|
1440
|
-
so each comes back in its current shape (A-54: `
|
|
1441
|
-
|
|
1460
|
+
so each comes back in its current shape (A-54: `output_dirs` kept an
|
|
1461
|
+
earlier vx's columns). The check, drop, re-create
|
|
1442
1462
|
and stamp are one write transaction, so another version's open waits
|
|
1443
1463
|
rather than landing between them; an index already current is opened
|
|
1444
1464
|
without the lock.
|
|
1445
1465
|
A reading verb (`vx why`, `vx last`, `vx info`) leaves it untouched and
|
|
1446
|
-
says why (item 896; `vx cache
|
|
1466
|
+
says why (item 896; `vx cache clean --dry-run` previews the reset
|
|
1447
1467
|
instead, item 1083).
|
|
1448
1468
|
That open prints nothing (owner, 2026-10-06: the cache is vx's to
|
|
1449
1469
|
keep). The artifacts stay: each is indexed again from its own bytes
|
|
1450
1470
|
when its task next asks for its key (below).
|
|
1451
1471
|
|
|
1452
1472
|
```sql
|
|
1453
|
-
-- src/cache/schema.ts (SCHEMA_VERSION = '
|
|
1454
|
-
-- With a shared store, entries, entry_stdout, output_files, entry_inputs
|
|
1455
|
-
-- and
|
|
1473
|
+
-- src/cache/schema.ts (SCHEMA_VERSION = 'v38', in cache.ts)
|
|
1474
|
+
-- With a shared store, entries, entry_stdout, output_files, entry_inputs,
|
|
1475
|
+
-- store_meta, task_durations, task_outcomes, blob_sizes and plugin_blobs live in its store.db, attached as `store`; the rest is
|
|
1456
1476
|
-- the workspace's cache.db. A named cache dir holds all of them.
|
|
1457
1477
|
|
|
1458
1478
|
CREATE TABLE schema_meta (
|
|
1459
|
-
key TEXT PRIMARY KEY, -- 'version', 'cache_version', 'orphans_swept_at', 'file_hashes_swept_at', '
|
|
1479
|
+
key TEXT PRIMARY KEY, -- 'version', 'cache_version', 'orphans_swept_at', 'file_hashes_swept_at', 'store_dir'
|
|
1460
1480
|
value TEXT NOT NULL
|
|
1461
1481
|
);
|
|
1462
1482
|
|
|
1463
|
-
-- The config-evaluation cache (§ Config evaluation cache): the validated,
|
|
1464
|
-
-- JSON-serialised result of a provably pure config, keyed by everything
|
|
1465
|
-
-- the evaluation could have observed. Machine-local. Rows not written in
|
|
1466
|
-
-- 30 days are swept at most once a day (`config_evals_swept_at`).
|
|
1467
|
-
CREATE TABLE config_evals (
|
|
1468
|
-
key TEXT PRIMARY KEY,
|
|
1469
|
-
json TEXT NOT NULL,
|
|
1470
|
-
created_at INTEGER NOT NULL
|
|
1471
|
-
);
|
|
1472
|
-
|
|
1473
1483
|
CREATE TABLE entries (
|
|
1474
1484
|
hash TEXT PRIMARY KEY, -- the 16-hex xxh3 cache key
|
|
1475
1485
|
project TEXT NOT NULL,
|
|
@@ -1528,10 +1538,6 @@ CREATE TABLE runs (
|
|
|
1528
1538
|
-- the history reader bounds its scan by rowid instead (see history.md).
|
|
1529
1539
|
CREATE INDEX runs_started_at ON runs(started_at);
|
|
1530
1540
|
CREATE INDEX runs_run_id ON runs(run_id);
|
|
1531
|
-
-- The one keyed index, PARTIAL over failed rows: a green run's inserts only
|
|
1532
|
-
-- evaluate its predicate, and the flakiness probe after a miss
|
|
1533
|
-
-- (failure-mode.ts, "did this key ever fail?") reads a few leaves.
|
|
1534
|
-
CREATE INDEX runs_failed ON runs(hash) WHERE status = 'failed';
|
|
1535
1541
|
|
|
1536
1542
|
-- Every non-group, non-aborted outcome of a run gets a row, so
|
|
1537
1543
|
-- `invocations.task_count` always equals `COUNT(*)` here for that run_id
|
|
@@ -1550,7 +1556,7 @@ CREATE INDEX runs_failed ON runs(hash) WHERE status = 'failed';
|
|
|
1550
1556
|
|
|
1551
1557
|
-- The file-hash memo: a content hash per input file that git could not
|
|
1552
1558
|
-- answer (untracked or dirty), keyed by the stat identity that proves
|
|
1553
|
-
-- the bytes unchanged. Machine-local; § Cache key derivation step
|
|
1559
|
+
-- the bytes unchanged. Machine-local; § Cache key derivation step 10.
|
|
1554
1560
|
-- A writing close drops rows unwritten for 30 days, at most once a day
|
|
1555
1561
|
-- (a memo miss is the whole cost of a dropped row; item 1082).
|
|
1556
1562
|
CREATE TABLE file_hashes (
|
|
@@ -1566,7 +1572,8 @@ CREATE TABLE file_hashes (
|
|
|
1566
1572
|
-- The size of each index blob the enumeration checked against the
|
|
1567
1573
|
-- worktree size git recorded (§ Cache key derivation, A-60): fixed for
|
|
1568
1574
|
-- its OID, so a warm run asks git for none. Swept with file_hashes by
|
|
1569
|
-
-- seen_at, the time the row was written.
|
|
1575
|
+
-- seen_at, the time the row was written. v38: in the store, so a worktree
|
|
1576
|
+
-- reads what another learned.
|
|
1570
1577
|
CREATE TABLE blob_sizes (
|
|
1571
1578
|
oid TEXT PRIMARY KEY,
|
|
1572
1579
|
size INTEGER NOT NULL,
|
|
@@ -1608,15 +1615,6 @@ CREATE TABLE output_stamps (
|
|
|
1608
1615
|
PRIMARY KEY (entry_hash, path)
|
|
1609
1616
|
);
|
|
1610
1617
|
|
|
1611
|
-
-- Each config's ORDERED import closure (the config first), so a warm
|
|
1612
|
-
-- load keys it by stat-hashing the list through file_hashes instead of
|
|
1613
|
-
-- reading every file. Machine-local; pruned with config_evals.
|
|
1614
|
-
CREATE TABLE config_closures (
|
|
1615
|
-
config_path TEXT PRIMARY KEY,
|
|
1616
|
-
files_json TEXT NOT NULL,
|
|
1617
|
-
created_at INTEGER NOT NULL
|
|
1618
|
-
);
|
|
1619
|
-
|
|
1620
1618
|
-- Every directory under a whole-subtree output glob, with its mtime as
|
|
1621
1619
|
-- of the last save or restore on THIS machine: unchanged mtimes prove
|
|
1622
1620
|
-- the output SET unchanged without a glob walk. Machine-local — a remote
|
|
@@ -1636,7 +1634,7 @@ CREATE TABLE invocations (
|
|
|
1636
1634
|
run_id TEXT PRIMARY KEY, -- UUIDv7, == runs.run_id
|
|
1637
1635
|
command TEXT NOT NULL, -- full argv, e.g. "vx run build --all"
|
|
1638
1636
|
requested_tasks TEXT NOT NULL, -- JSON string[] of options.tasks
|
|
1639
|
-
cache_policy TEXT NOT NULL, --
|
|
1637
|
+
cache_policy TEXT NOT NULL, -- the --cache-policy spec, e.g. "local:rw,remote:r"
|
|
1640
1638
|
concurrency INTEGER NOT NULL,
|
|
1641
1639
|
flow TEXT, -- 'focused' | 'broad' | NULL (programmatic)
|
|
1642
1640
|
started_at INTEGER NOT NULL, -- ms-epoch
|
|
@@ -1693,6 +1691,49 @@ CREATE TABLE store_meta (
|
|
|
1693
1691
|
value TEXT NOT NULL
|
|
1694
1692
|
);
|
|
1695
1693
|
|
|
1694
|
+
-- v36: each executed success's duration, appended in the run's history
|
|
1695
|
+
-- transaction, in the store so every worktree forecasts from it.
|
|
1696
|
+
-- source: 'branch:<name>'; the forecast reads the newest five per task.
|
|
1697
|
+
-- v37: 'remote' rows are a default-branch commit's timings a plugin
|
|
1698
|
+
-- imported, one per (sha, task).
|
|
1699
|
+
CREATE TABLE task_durations (
|
|
1700
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1701
|
+
project TEXT NOT NULL,
|
|
1702
|
+
task TEXT NOT NULL,
|
|
1703
|
+
source TEXT NOT NULL,
|
|
1704
|
+
sha TEXT,
|
|
1705
|
+
duration_ms INTEGER NOT NULL,
|
|
1706
|
+
recorded_at INTEGER NOT NULL
|
|
1707
|
+
);
|
|
1708
|
+
CREATE INDEX task_durations_task ON task_durations(project, task, source, id);
|
|
1709
|
+
CREATE UNIQUE INDEX task_durations_remote
|
|
1710
|
+
ON task_durations(sha, project, task) WHERE source = 'remote';
|
|
1711
|
+
|
|
1712
|
+
-- v38: the flaky record (failure-mode.ts), in the store so a key that
|
|
1713
|
+
-- passed in one worktree and failed in another is flaky in both. Every
|
|
1714
|
+
-- executed success or failure that names its key (keyed, cached, not run
|
|
1715
|
+
-- behind a failure), and a key's first hit: a later hit replays a pass on
|
|
1716
|
+
-- record, so a warm run on passed keys writes no row (one index probe,
|
|
1717
|
+
-- ~1 ms per 1,000 hits). Pruned by age with the run history.
|
|
1718
|
+
CREATE TABLE task_outcomes (
|
|
1719
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1720
|
+
project TEXT NOT NULL,
|
|
1721
|
+
task TEXT NOT NULL,
|
|
1722
|
+
hash TEXT NOT NULL,
|
|
1723
|
+
status TEXT NOT NULL,
|
|
1724
|
+
attempts INTEGER,
|
|
1725
|
+
recorded_at INTEGER NOT NULL
|
|
1726
|
+
);
|
|
1727
|
+
CREATE INDEX task_outcomes_hash ON task_outcomes(hash, status);
|
|
1728
|
+
|
|
1729
|
+
-- v37: a plugin's own entries (`ctx.cache`), by core's key for its
|
|
1730
|
+
-- name; written once, pruned by age with the run history.
|
|
1731
|
+
CREATE TABLE plugin_blobs (
|
|
1732
|
+
key TEXT PRIMARY KEY,
|
|
1733
|
+
bytes BLOB NOT NULL,
|
|
1734
|
+
at INTEGER NOT NULL
|
|
1735
|
+
);
|
|
1736
|
+
|
|
1696
1737
|
-- v33: an artifact of at most 32 KiB compressed, its exact <hash>.tar.zst
|
|
1697
1738
|
-- bytes, written in the transaction that writes its entry rows. In the
|
|
1698
1739
|
-- store (or a named cache dir's cache.db), and outside every drop a
|
|
@@ -1749,28 +1790,6 @@ moat.
|
|
|
1749
1790
|
- **One handle, one schema-meta sentinel.** An older schema drops
|
|
1750
1791
|
and recreates every table but `schema_meta` (pre-alpha) — there's no migration code to maintain.
|
|
1751
1792
|
|
|
1752
|
-
## Config evaluation cache
|
|
1753
|
-
|
|
1754
|
-
Evaluating configs is the largest fixed cost of a warm run on a big
|
|
1755
|
-
workspace (~80 ms for 1000 synthetic configs; reading them as data is
|
|
1756
|
-
~12 ms). A config that is **provably pure** — every import relative or
|
|
1757
|
-
`@vzn/vx`, and no mention of `process`, `Bun`, `Date`, `fetch`,
|
|
1758
|
-
`import.meta`, `require`, a dynamic `import()`, `await`, … outside string
|
|
1759
|
-
literals and comments — is served from `cache.db`'s `config_evals` table,
|
|
1760
|
-
keyed by the git blob id of the config and of every file in its
|
|
1761
|
-
relative import closure, the workspace fingerprint (lockfiles) and Bun's
|
|
1762
|
-
version. Editing a shared preset moves the key. On a warm run the
|
|
1763
|
-
closure is remembered per config, so the key comes from a stat-backed
|
|
1764
|
-
identity per file — no read, no scan — for every config whose relative
|
|
1765
|
-
imports name their files outright: an explicit extension, the file
|
|
1766
|
-
itself, no symlink on the way (an extensionless import, a `.js` Bun
|
|
1767
|
-
answers with a `.ts`, or a link could be re-resolved without touching a
|
|
1768
|
-
listed file, so such a config keeps the scan). Anything the static check cannot prove
|
|
1769
|
-
pure evaluates live, exactly as before, so the cache can be slower but
|
|
1770
|
-
not wrong for a config written in good faith (the check is syntactic; one
|
|
1771
|
-
built to defeat it can). Details and the deny-list:
|
|
1772
|
-
[`modules/config-cache.md`](./modules/config-cache.md).
|
|
1773
|
-
|
|
1774
1793
|
## Performance characteristics
|
|
1775
1794
|
|
|
1776
1795
|
- **Hashing cost** on a clean tree is near-zero per file: git index
|
|
@@ -1789,15 +1808,16 @@ built to defeat it can). Details and the deny-list:
|
|
|
1789
1808
|
transaction, which holds a small artifact's bytes itself; a large one
|
|
1790
1809
|
adds a temp write and an atomic rename. Hashing dominates the run; storage itself
|
|
1791
1810
|
is cheap. The remote upload (if any) is backgrounded.
|
|
1792
|
-
- **
|
|
1793
|
-
|
|
1794
|
-
one
|
|
1811
|
+
- **Claimed root files** (a lockfile plugin's lockfile and settings)
|
|
1812
|
+
are read once per `vx run` invocation; after a task that may rewrite
|
|
1813
|
+
one ran, one `stat` per file re-checks it (item 750). With no plugin
|
|
1814
|
+
claiming one, nothing is read.
|
|
1795
1815
|
|
|
1796
1816
|
## What's NOT in the key (and why)
|
|
1797
1817
|
|
|
1798
1818
|
- **`exec.env.passThrough` _values_.** Would force cache misses across
|
|
1799
1819
|
machines with different CI flags, regions, or shell prompts. The
|
|
1800
|
-
_names_ are in the config hash (step
|
|
1820
|
+
_names_ are in the config hash (step 3) so adding/removing a
|
|
1801
1821
|
passthrough still bumps the key for affected tasks.
|
|
1802
1822
|
- **Files outside the project directory that aren't declared.**
|
|
1803
1823
|
Workspace-root configs (`tsconfig.base.json`, etc.) are not
|
|
@@ -1807,11 +1827,13 @@ built to defeat it can). Details and the deny-list:
|
|
|
1807
1827
|
The canonical mechanism is `cache.inputs.runtime` /
|
|
1808
1828
|
`workspaceRuntime` (e.g. `workspaceRuntime: ['node -v']`): the
|
|
1809
1829
|
command output is resolved live at hash time, so it stays correct
|
|
1810
|
-
under
|
|
1830
|
+
under the lock. Avoid baking versions via
|
|
1811
1831
|
`define: { X: execSync(...) }` — that value freezes into the config
|
|
1812
1832
|
object at lock time and goes stale.
|
|
1813
|
-
-
|
|
1814
|
-
`--affected` change sets.
|
|
1833
|
+
- **`.vx/projects.lock.json`** — globally excluded (v24); also filtered out of
|
|
1834
|
+
`--affected` change sets. So are the files `vx lock` and `vx init`
|
|
1835
|
+
write beside it: each project's `vx.lock.json` review copy,
|
|
1836
|
+
`.vx/.gitattributes` and `.vx/.gitignore`.
|
|
1815
1837
|
- **`exec.remote`.** Pure PLACEMENT: it decides _where_ a task runs,
|
|
1816
1838
|
never what it produces, so pinning a task to this machine does not
|
|
1817
1839
|
bust its cache. It must not — the
|
|
@@ -1833,8 +1855,8 @@ The cache records the version it was written under
|
|
|
1833
1855
|
(`schema_meta.cache_version`). After an upgrade every cached task
|
|
1834
1856
|
misses once and re-saves, with nothing printed (owner, 2026-10-06: the
|
|
1835
1857
|
cache is vx's to keep). The index survives, so the old entries stay
|
|
1836
|
-
until they age out under `vx cache
|
|
1837
|
-
`
|
|
1858
|
+
until they age out under `vx cache clean --older-than` or
|
|
1859
|
+
`cache.retention`; no key derives to them again.
|
|
1838
1860
|
|
|
1839
1861
|
Required when:
|
|
1840
1862
|
|
|
@@ -1854,6 +1876,9 @@ Not required when:
|
|
|
1854
1876
|
background uploads) — key derivation and artifact bytes untouched.
|
|
1855
1877
|
- Doc-only updates.
|
|
1856
1878
|
- Refactors that don't change the bytes fed into the hash.
|
|
1879
|
+
- A key part leaves the chain: every key moves, so no unchanged key
|
|
1880
|
+
holds wrong bytes (`v43` stayed when core stopped folding the
|
|
1881
|
+
`workspace:` and `pkg:` parts, 2026-10-14).
|
|
1857
1882
|
|
|
1858
1883
|
`tests/contract-stored-format.test.ts` holds the layout rows: it
|
|
1859
1884
|
records the index's DDL beside `SCHEMA_VERSION` and a fixture
|
|
@@ -1878,6 +1903,14 @@ breaking footer).
|
|
|
1878
1903
|
|
|
1879
1904
|
### History
|
|
1880
1905
|
|
|
1906
|
+
- **v43 kept** (2026-10-14): core stopped folding the project's
|
|
1907
|
+
`package.json` and the root fingerprint files; every key moved, so
|
|
1908
|
+
nothing replays wrong bytes and no bump was needed.
|
|
1909
|
+
- **v42 → v43**: stored bytes wrong under an unchanged key (Security
|
|
1910
|
+
F4, 2026-10-14). A scoped run stored its local entries under the
|
|
1911
|
+
default key, so a later default run on the same disk replayed a PR's
|
|
1912
|
+
bytes. A scoped run now stores under the scope's key locally as it
|
|
1913
|
+
does remotely; the fix cannot reach an entry already saved that way.
|
|
1881
1914
|
- **v41 → v42**: the stored output became both streams in the order
|
|
1882
1915
|
the run printed them (`orchestrator/output-log.ts`), so a hit replays
|
|
1883
1916
|
stderr too. A v41 entry holds stdout alone.
|
|
@@ -2076,7 +2109,7 @@ breaking footer).
|
|
|
2076
2109
|
artifact is gone or cannot produce an output the index recorded (the
|
|
2077
2110
|
caller has already wiped the declared outputs by then, so a quiet
|
|
2078
2111
|
return reported a green hit over an emptied tree — reachable via a
|
|
2079
|
-
concurrent `vx cache
|
|
2112
|
+
concurrent `vx cache clean`); the tar reader rejects an entry whose
|
|
2080
2113
|
declared size runs past the end of the archive (`subarray` clamps, so
|
|
2081
2114
|
it used to install short, NUL-padded content as a cache hit instead of
|
|
2082
2115
|
degrading to a miss); and directory entries now get the same
|
|
@@ -2120,7 +2153,7 @@ breaking footer).
|
|
|
2120
2153
|
so existing artifacts stay valid and `CACHE_VERSION` stays `v24`. The
|
|
2121
2154
|
schema gate drops + recreates on the version mismatch (pre-alpha, no
|
|
2122
2155
|
migration).
|
|
2123
|
-
- **v23 → v24**: exclude
|
|
2156
|
+
- **v23 → v24**: exclude `.vx/projects.lock.json` from the input file set
|
|
2124
2157
|
globally (`ALWAYS_IGNORE` in `cache/inputs.ts`). The lockfile is
|
|
2125
2158
|
committed, so git enumerates it, but it's vx's own frozen-config
|
|
2126
2159
|
metadata — never a task input. Without this, a `vx lock` re-write
|
|
@@ -2131,7 +2164,7 @@ breaking footer).
|
|
|
2131
2164
|
|
|
2132
2165
|
- **v22 → v23**: fold `cache.inputs.runtime` / `workspaceRuntime`
|
|
2133
2166
|
command output into the key (two namespaced sections after
|
|
2134
|
-
env-values). The command _strings_ stay in the config hash (step
|
|
2167
|
+
env-values). The command _strings_ stay in the config hash (step 3);
|
|
2135
2168
|
their combined trimmed stdout+stderr is resolved live at hash time
|
|
2136
2169
|
and folded as `runtime-values:` (project-cwd) and `ws-runtime-values:`
|
|
2137
2170
|
(root-cwd) sections, each `command\0output`. No SCHEMA bump — only
|
|
@@ -2164,7 +2197,7 @@ breaking footer).
|
|
|
2164
2197
|
- **v9 → v10** (PR #7): on-disk layout switched from per-entry
|
|
2165
2198
|
`meta.json` + `outputs/` directory to a workspace-wide `cache.db`
|
|
2166
2199
|
(SQLite) plus output files directly at `<hash>/` and log files at
|
|
2167
|
-
`logs/<hash>.{stdout,stderr}`. Adds run history for `vx
|
|
2200
|
+
`logs/<hash>.{stdout,stderr}`. Adds run history for `vx info`.
|
|
2168
2201
|
Removes the per-entry manifest.
|
|
2169
2202
|
- **v10 → v11** (PR #19): analytics columns added to the `runs`
|
|
2170
2203
|
table: `run_id` (UUIDv7), `cpu_ms`, `peak_rss_bytes`,
|
|
@@ -2234,7 +2267,7 @@ breaking footer).
|
|
|
2234
2267
|
transitive-deps to nearest-holder frontier semantics (Turbo/Nx
|
|
2235
2268
|
direct-deps parity, plus vx's sparse bridging through deps that
|
|
2236
2269
|
don't declare the task). Task graphs lose the redundant deep edges,
|
|
2237
|
-
so the filtered-upstream-hash set (step
|
|
2270
|
+
so the filtered-upstream-hash set (step 8) shrinks for any task
|
|
2238
2271
|
whose deps chain `'^task'` themselves — same inputs now derive a
|
|
2239
2272
|
different key. Reachability/ordering is unchanged whenever holders
|
|
2240
2273
|
chain `'^task'` (the universal pattern); a holder that doesn't is
|