@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/src/exec/env.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
|
+
import { ESSENTIAL_ENV } from '../util/index.js'
|
|
3
4
|
|
|
4
5
|
// Build the environment exposed to a task.
|
|
5
6
|
//
|
|
@@ -9,37 +10,6 @@ import path from 'node:path'
|
|
|
9
10
|
// 3. define: explicit name=value pairs from the task config.
|
|
10
11
|
// 4. binPaths prepended to PATH (after all the above resolve PATH).
|
|
11
12
|
|
|
12
|
-
/**
|
|
13
|
-
* Exported so the schema doc's enumeration of it can be PINNED against the
|
|
14
|
-
* real list: a hand-copied allowlist is how the two drift, and a reader
|
|
15
|
-
* asking "what does my build script actually see?" is asking a
|
|
16
|
-
* security-shaped question that a stale list answers wrongly.
|
|
17
|
-
*/
|
|
18
|
-
export const ESSENTIAL_ENV: readonly string[] = [
|
|
19
|
-
'PATH',
|
|
20
|
-
'HOME',
|
|
21
|
-
'SHELL',
|
|
22
|
-
'USER',
|
|
23
|
-
'LOGNAME',
|
|
24
|
-
'TMPDIR',
|
|
25
|
-
'TEMP',
|
|
26
|
-
'TMP',
|
|
27
|
-
'LANG',
|
|
28
|
-
'LC_ALL',
|
|
29
|
-
'LC_CTYPE',
|
|
30
|
-
'TERM',
|
|
31
|
-
'COLORTERM',
|
|
32
|
-
'FORCE_COLOR',
|
|
33
|
-
'NO_COLOR',
|
|
34
|
-
'CI',
|
|
35
|
-
'NODE_OPTIONS',
|
|
36
|
-
// Where corepack and pnpm keep their own binaries, as HOME is: a machine
|
|
37
|
-
// path, never task output. Without them a corepack shim re-downloads its
|
|
38
|
-
// manager (offline or sandboxed: fails) and pnpm loses its global dir.
|
|
39
|
-
'COREPACK_HOME',
|
|
40
|
-
'PNPM_HOME',
|
|
41
|
-
]
|
|
42
|
-
|
|
43
13
|
/**
|
|
44
14
|
* The two variables vx sets on every task it spawns, over the layers
|
|
45
15
|
* above: the workspace root the run is in and the `project#task` running.
|
package/src/exec/executor.ts
CHANGED
|
@@ -73,12 +73,8 @@ export interface TaskInputs {
|
|
|
73
73
|
readonly hash: string
|
|
74
74
|
readonly outputs: readonly string[]
|
|
75
75
|
}>
|
|
76
|
-
/** Digest of the project's own `package.json` (folded even when no glob lists it). */
|
|
77
|
-
readonly packageJsonDigest: string
|
|
78
76
|
/** Digest of the resolved task config. */
|
|
79
77
|
readonly configDigest: string
|
|
80
|
-
/** The workspace fingerprint (root manifests + lockfile). */
|
|
81
|
-
readonly workspaceFingerprint: string
|
|
82
78
|
}
|
|
83
79
|
|
|
84
80
|
export interface ExecuteRequest {
|
|
@@ -146,7 +142,7 @@ export interface ExecuteRequest {
|
|
|
146
142
|
* work and returns: a child an executor spawned is not one core can
|
|
147
143
|
* reach (H-10, H-12). One already aborted is work not to start (B-55).
|
|
148
144
|
* Any exit after the timeout's abort, a 0 included, is a timeout. Core
|
|
149
|
-
* waits the kill grace (`
|
|
145
|
+
* waits the kill grace (`VX_RUN_KILL_GRACE`, 2 s) after the abort, then
|
|
150
146
|
* settles the attempt without `execute` and abandons it (H-14).
|
|
151
147
|
*/
|
|
152
148
|
readonly signal?: AbortSignal
|
package/src/exec/kill-tree.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// takes even that.
|
|
12
12
|
|
|
13
13
|
import { closeSync, readdirSync, readFileSync, writeSync } from 'node:fs'
|
|
14
|
-
import { procfsIsOwn, taskShell } from '../util/index.js'
|
|
14
|
+
import { helperEnv, procfsIsOwn, taskShell } from '../util/index.js'
|
|
15
15
|
|
|
16
16
|
export type Child = ReturnType<typeof Bun.spawn>
|
|
17
17
|
|
|
@@ -104,6 +104,7 @@ function startGuard(): void {
|
|
|
104
104
|
try {
|
|
105
105
|
const guard = Bun.spawn([taskShell(), '-c', GUARD_SCRIPT], {
|
|
106
106
|
argv0: 'vx-group-guard',
|
|
107
|
+
env: helperEnv(),
|
|
107
108
|
stdio: ['ignore', 'ignore', 'ignore', 'pipe'],
|
|
108
109
|
// Out of vx's group, so a Ctrl-C at the terminal leaves it to the
|
|
109
110
|
// pipe; vx's own teardown owns that path.
|
|
@@ -62,6 +62,7 @@ import {
|
|
|
62
62
|
import {
|
|
63
63
|
BUN_GLOB_WILDCARDS,
|
|
64
64
|
executablePath,
|
|
65
|
+
helperEnv,
|
|
65
66
|
taskShell,
|
|
66
67
|
grantPrefix,
|
|
67
68
|
isTmpdirRefusal,
|
|
@@ -1806,10 +1807,11 @@ function spawnHostBridges(ports: readonly number[], tag: string): void {
|
|
|
1806
1807
|
? Bun.spawn(argv, {
|
|
1807
1808
|
stdio: ['ignore', 'ignore', 'ignore'],
|
|
1808
1809
|
detached: true,
|
|
1810
|
+
env: helperEnv(),
|
|
1809
1811
|
})
|
|
1810
1812
|
: Bun.spawn(
|
|
1811
1813
|
[taskShell(), '-c', `${guardLine(3)}exec ${argv.map(shellQuote).join(' ')}`],
|
|
1812
|
-
{ stdio: ['ignore', 'ignore', 'ignore', guard], detached: true },
|
|
1814
|
+
{ stdio: ['ignore', 'ignore', 'ignore', guard], detached: true, env: helperEnv() },
|
|
1813
1815
|
),
|
|
1814
1816
|
),
|
|
1815
1817
|
)
|
|
@@ -2691,6 +2693,15 @@ function expandGrants(
|
|
|
2691
2693
|
// names a wall literally never reaches this loop and stays, and one
|
|
2692
2694
|
// spelled with escaped brackets names it as surely.
|
|
2693
2695
|
if (namedPath(p) === undefined && walls.some((w) => atOrUnder(real, w))) continue
|
|
2696
|
+
// A write hit that HOLDS a wall would bind it writable: `.*` in a root
|
|
2697
|
+
// project matched `.vx`, which holds the cache. A read hit is punched
|
|
2698
|
+
// around it instead (`punchWalls`).
|
|
2699
|
+
if (
|
|
2700
|
+
unmatched !== undefined &&
|
|
2701
|
+
namedPath(p) === undefined &&
|
|
2702
|
+
walls.some((w) => atOrUnder(w, real))
|
|
2703
|
+
)
|
|
2704
|
+
continue
|
|
2694
2705
|
out.push(abs)
|
|
2695
2706
|
hits++
|
|
2696
2707
|
}
|
|
@@ -744,6 +744,9 @@ export function refusedConnections(records: readonly string[]): SandboxViolation
|
|
|
744
744
|
}))
|
|
745
745
|
}
|
|
746
746
|
|
|
747
|
+
/** Write syscalls that act on a path's last component, not what a link there names. */
|
|
748
|
+
const NAME_SYSCALL = /^(?:symlink|unlink|rename|rmdir|mknod|link)/
|
|
749
|
+
|
|
747
750
|
/**
|
|
748
751
|
* The writes SRT's Linux observer saw that this task's binds do not cover.
|
|
749
752
|
* The observer reports every write-intent syscall as `deny <syscall>
|
|
@@ -774,7 +777,12 @@ export function refusedWrites(
|
|
|
774
777
|
// `5>out.log` in the project reported a write to `out.log` and failed
|
|
775
778
|
// a clean task (2026-10-02).
|
|
776
779
|
if (/^\/(?:dev|proc)\//.test(raw)) continue
|
|
777
|
-
|
|
780
|
+
// These act on the name itself, never through it: resolving the link
|
|
781
|
+
// `ln -s ../src/a.ts dist/a` just made judged the write by `src/a.ts`,
|
|
782
|
+
// and a clean task failed with a fix that granted `write: ['src/']`.
|
|
783
|
+
const abs = NAME_SYSCALL.test(syscall)
|
|
784
|
+
? path.join(toRealPath(path.dirname(raw)), path.basename(raw))
|
|
785
|
+
: toRealPath(raw)
|
|
778
786
|
if (isUnderAny(abs, binds) || underGlob(abs, globs)) continue
|
|
779
787
|
// A directory a write bind lies in exists, since bwrap made it to mount
|
|
780
788
|
// the bind: `mkdir -p node_modules/.cache/tool` under a grant of
|
package/src/graph/index.ts
CHANGED
package/src/graph/priorities.ts
CHANGED
|
@@ -161,7 +161,7 @@ export function computeReverseDepCount(nodes: Map<string, TaskNode>): Map<string
|
|
|
161
161
|
|
|
162
162
|
/**
|
|
163
163
|
* The ready-queue baseline a workspace may pick in place of the default
|
|
164
|
-
* (`schedule` in vx
|
|
164
|
+
* (`schedule` in .vx/workspace.ts). Each ranks with no timings; a `schedule`
|
|
165
165
|
* plugin's weights still sort above it, and it breaks their ties.
|
|
166
166
|
* - `most-work`: transitive dependents (`computeReverseDepCount`), the default.
|
|
167
167
|
* - `critical-path`: steps on the longest chain of dependents below a task.
|
package/src/graph/scheduler.ts
CHANGED
|
@@ -121,14 +121,14 @@ export interface TaskOutcome {
|
|
|
121
121
|
groupUpstream?: readonly TaskOutcome[]
|
|
122
122
|
/**
|
|
123
123
|
* This task RAN over inputs its key no longer describes: one moved while
|
|
124
|
-
* it ran,
|
|
124
|
+
* it ran, a claimed root file moved, or an upstream carried this
|
|
125
125
|
* mark. Its save was withheld, and its outputs are not what `hash` names,
|
|
126
126
|
* so a dependant that folds `hash` saves nothing either (A-12).
|
|
127
127
|
*/
|
|
128
128
|
unkeyed?: true
|
|
129
129
|
/**
|
|
130
130
|
* The run's cache policy read and wrote nothing (`--no-cache`,
|
|
131
|
-
* `--cache=local:`): no cache answered for this task, so a run of it is
|
|
131
|
+
* `--cache-policy=local:`): no cache answered for this task, so a run of it is
|
|
132
132
|
* `no-cache`, not a miss. Set by `run()`; `ranNoCache` reads it.
|
|
133
133
|
*/
|
|
134
134
|
cacheOff?: true
|
|
@@ -152,19 +152,33 @@ export interface TaskOutcome {
|
|
|
152
152
|
* bounded to its first 8 KiB and last 56 KiB. The run's history keeps it.
|
|
153
153
|
*/
|
|
154
154
|
failedOutput?: string
|
|
155
|
+
/**
|
|
156
|
+
* When the run keeps its bytes in a remote, a task that ran and saved no
|
|
157
|
+
* entry (failed, or uncached): its whole output as `encodeOutputLog`
|
|
158
|
+
* stores it, secrets masked, at the entry's bounds.
|
|
159
|
+
*/
|
|
160
|
+
runLog?: string
|
|
155
161
|
/**
|
|
156
162
|
* On a failed persistent task: why it never became ready — the readiness
|
|
157
163
|
* deadline fired, the child exited first (`exitCode` is then its own), or
|
|
158
164
|
* the spawn itself failed. Every label reads this instead of the exit.
|
|
159
165
|
*/
|
|
160
166
|
notReady?: 'timeout' | 'exited' | 'spawn'
|
|
167
|
+
/**
|
|
168
|
+
* The input that changed while the command ran, failing the task under
|
|
169
|
+
* `inputStability.severity: 'error'` (workspace-relative; '' when vx
|
|
170
|
+
* could not name it). The exit is vx's 1; the label reads this instead.
|
|
171
|
+
*/
|
|
172
|
+
inputMoved?: string
|
|
161
173
|
/** Executor-reported placement label (`ExecuteResult.where`) — set only
|
|
162
174
|
* when the task ran somewhere other than this host. Telemetry-only. */
|
|
163
175
|
where?: string
|
|
164
176
|
/** `'deferred'` when the task's outputs were left in the remote store
|
|
165
|
-
* (`--download=none`) instead of landing on this machine. Absent for
|
|
177
|
+
* (`--cache-download=none`) instead of landing on this machine. Absent for
|
|
166
178
|
* every ordinary outcome. Telemetry-only. */
|
|
167
179
|
outputs?: 'deferred'
|
|
180
|
+
/** The task ran here and its result went to the cache. The footer's `↑`. */
|
|
181
|
+
saved?: true
|
|
168
182
|
/**
|
|
169
183
|
* v11 analytics: hrtime span relative to the parent run's t=0.
|
|
170
184
|
* Lets downstream analytics reconstruct the actual parallel timeline
|
|
@@ -183,7 +197,7 @@ export interface TaskOutcome {
|
|
|
183
197
|
restored?: boolean
|
|
184
198
|
/**
|
|
185
199
|
* How many times the task executed this run, when `exec.retries` /
|
|
186
|
-
* `--
|
|
200
|
+
* `--retries` re-ran failed attempts. Set only when > 1 — a plain
|
|
187
201
|
* single-attempt run carries nothing. Not persisted; telemetry-side
|
|
188
202
|
* flaky detection reads it off the outcome stream.
|
|
189
203
|
*/
|
|
@@ -216,6 +230,12 @@ export interface TaskOutcome {
|
|
|
216
230
|
systemInfo?: string[]
|
|
217
231
|
/** Credential paths denied and left out of the grants: an agent applies the fix as given. */
|
|
218
232
|
withheld?: string[]
|
|
233
|
+
/**
|
|
234
|
+
* Reads of the project the task's `cache.inputs.files` does not match:
|
|
235
|
+
* add them there too when the output depends on them, or a hit replays
|
|
236
|
+
* output built from an older copy. Cached tasks only.
|
|
237
|
+
*/
|
|
238
|
+
inputs?: string[]
|
|
219
239
|
}
|
|
220
240
|
}
|
|
221
241
|
|
|
@@ -267,7 +287,7 @@ export interface ScheduleOptions {
|
|
|
267
287
|
* outcome lands. Absent = stderr. The run gives it the task's own
|
|
268
288
|
* stderr, so its frame carries the reason.
|
|
269
289
|
*/
|
|
270
|
-
onError?: (node: TaskNode, line: string) => void
|
|
290
|
+
onError?: (node: TaskNode, line: string, err: unknown) => void
|
|
271
291
|
/**
|
|
272
292
|
* What an outcome still owes before its dependents may start — a cache
|
|
273
293
|
* save that runs off the execution slot (the orchestrator's save lane):
|
|
@@ -294,7 +314,7 @@ export interface ScheduleOptions {
|
|
|
294
314
|
*/
|
|
295
315
|
priorities?: ReadonlyMap<string, number>
|
|
296
316
|
/**
|
|
297
|
-
* The baseline ranking under `priorities` (`schedule` in vx
|
|
317
|
+
* The baseline ranking under `priorities` (`schedule` in .vx/workspace.ts).
|
|
298
318
|
* Undefined or `most-work` is the reverse-deps count above.
|
|
299
319
|
*/
|
|
300
320
|
strategy?: ScheduleStrategy
|
|
@@ -1027,7 +1047,7 @@ export async function runGraph(options: ScheduleOptions): Promise<Map<string, Ta
|
|
|
1027
1047
|
}
|
|
1028
1048
|
try {
|
|
1029
1049
|
if (options.onError === undefined) process.stderr.write(line)
|
|
1030
|
-
else options.onError(node, line)
|
|
1050
|
+
else options.onError(node, line, err)
|
|
1031
1051
|
} catch {
|
|
1032
1052
|
process.stderr.write(line)
|
|
1033
1053
|
}
|
package/src/graph/task-graph.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
splitNegations,
|
|
8
8
|
staticPrefix,
|
|
9
9
|
taskGlob,
|
|
10
|
+
refusal,
|
|
10
11
|
UserError,
|
|
11
12
|
wholeSubtreePrefixes,
|
|
12
13
|
} from '../util/index.js'
|
|
@@ -288,10 +289,16 @@ export interface BuildGraphOptions {
|
|
|
288
289
|
* with other root-anchored ones.
|
|
289
290
|
*/
|
|
290
291
|
workspaceRoot?: string
|
|
291
|
-
/** The workspace's `rules`; each is on unless set to `false`. */
|
|
292
|
-
rules?:
|
|
292
|
+
/** The workspace's `run.rules`; each is on unless set to `false`. */
|
|
293
|
+
rules?: GraphRules | undefined
|
|
293
294
|
}
|
|
294
295
|
|
|
296
|
+
/**
|
|
297
|
+
* `run.rules` as the graph applies them. `locked`: a run, which never
|
|
298
|
+
* checks them; `vx lock` and `vx lock --check` do (owner, 2026-10-14).
|
|
299
|
+
*/
|
|
300
|
+
export type GraphRules = WorkspaceRules & { readonly locked?: boolean }
|
|
301
|
+
|
|
295
302
|
/**
|
|
296
303
|
* The refusal of a literal `^name` that no project in the workspace
|
|
297
304
|
* declares: it has no holder anywhere, so it can only be a typo (nx#32779
|
|
@@ -301,7 +308,7 @@ export interface BuildGraphOptions {
|
|
|
301
308
|
*/
|
|
302
309
|
export function undeclaredDepsError(taskId: string, name: string): UserError {
|
|
303
310
|
return new UserError(
|
|
304
|
-
|
|
311
|
+
`${taskId}: dependsOn ^${name} but no project in the workspace declares ${name}`,
|
|
305
312
|
'VX_E_DEPENDS_ON',
|
|
306
313
|
)
|
|
307
314
|
}
|
|
@@ -493,7 +500,7 @@ export function buildTaskGraph(options: BuildGraphOptions): Map<string, TaskNode
|
|
|
493
500
|
spec = parseDependencySpec(raw)
|
|
494
501
|
} catch (err) {
|
|
495
502
|
if (err instanceof DependencySpecError) {
|
|
496
|
-
throw new UserError(
|
|
503
|
+
throw new UserError(`${id}: ${err.message}`, 'VX_E_DEPENDS_ON')
|
|
497
504
|
}
|
|
498
505
|
throw err
|
|
499
506
|
}
|
|
@@ -505,19 +512,19 @@ export function buildTaskGraph(options: BuildGraphOptions): Map<string, TaskNode
|
|
|
505
512
|
// they name a namespace of tasks to add (Nx 19.5 parity).
|
|
506
513
|
if (spec.kind === 'wildcardSelf' || spec.kind === 'wildcardDeps') {
|
|
507
514
|
throw new UserError(
|
|
508
|
-
|
|
515
|
+
`${id}: dependsOn does not accept bare wildcards (got "${raw}")`,
|
|
509
516
|
'VX_E_DEPENDS_ON',
|
|
510
517
|
)
|
|
511
518
|
}
|
|
512
519
|
if (spec.negated) {
|
|
513
520
|
throw new UserError(
|
|
514
|
-
|
|
521
|
+
`${id}: dependsOn does not accept negation (got "${raw}")`,
|
|
515
522
|
'VX_E_DEPENDS_ON',
|
|
516
523
|
)
|
|
517
524
|
}
|
|
518
525
|
if (spec.kind === 'cross' && (isTaskPattern(spec.task) || isTaskPattern(spec.project))) {
|
|
519
526
|
throw new UserError(
|
|
520
|
-
|
|
527
|
+
`${id}: dependsOn patterns are not supported in the "pkg#task" form (got "${raw}")`,
|
|
521
528
|
'VX_E_DEPENDS_ON',
|
|
522
529
|
)
|
|
523
530
|
}
|
|
@@ -537,7 +544,7 @@ export function buildTaskGraph(options: BuildGraphOptions): Map<string, TaskNode
|
|
|
537
544
|
// doesn't resolve in this project.
|
|
538
545
|
if (!visit(frame, projectName, spec.task, false)) {
|
|
539
546
|
throw new UserError(
|
|
540
|
-
|
|
547
|
+
`${id}: dependsOn ${taskId(projectName, spec.task)} but no such task is declared`,
|
|
541
548
|
'VX_E_DEPENDS_ON',
|
|
542
549
|
)
|
|
543
550
|
}
|
|
@@ -570,6 +577,17 @@ export function buildTaskGraph(options: BuildGraphOptions): Map<string, TaskNode
|
|
|
570
577
|
// last (`resolveDeferred`), and a walk that meets it on a cycle takes
|
|
571
578
|
// the edge and goes on past it, so the builds it could not depend on
|
|
572
579
|
// still come first.
|
|
580
|
+
//
|
|
581
|
+
// With no edge source every `^` resolves to nothing and its upstream
|
|
582
|
+
// folds into no key: a stale hit, green. With one, a project with no
|
|
583
|
+
// deps is a leaf. The default `build`'s `^build` is no exception: a
|
|
584
|
+
// task behind it would fold no upstream either.
|
|
585
|
+
if (!packageGraph.edgeSource) {
|
|
586
|
+
throw new UserError(
|
|
587
|
+
`${id}: ^${spec.task} needs project edges; add pnpm()/bun()/npm()/yarn() or a plugin that returns \`deps\``,
|
|
588
|
+
'VX_E_DEPENDS_ON',
|
|
589
|
+
)
|
|
590
|
+
}
|
|
573
591
|
const visited = new Set<string>([projectName])
|
|
574
592
|
if (!isTaskPattern(spec.task)) {
|
|
575
593
|
const held = walkHolders(frame, spec.task, visited, projectName)
|
|
@@ -602,11 +620,11 @@ export function buildTaskGraph(options: BuildGraphOptions): Map<string, TaskNode
|
|
|
602
620
|
// The loader spells `//` as the root project's name when there is one.
|
|
603
621
|
if (spec.project === '//')
|
|
604
622
|
throw new UserError(
|
|
605
|
-
|
|
623
|
+
`${id}: dependsOn //#${spec.task}, the workspace root's task, but the root declares no such task: add a vx.config at the root declaring it and run \`vx lock\``,
|
|
606
624
|
'VX_E_DEPENDS_ON',
|
|
607
625
|
)
|
|
608
626
|
throw new UserError(
|
|
609
|
-
|
|
627
|
+
`${id}: dependsOn ${taskId(spec.project, spec.task)} but no such project or task is declared`,
|
|
610
628
|
'VX_E_DEPENDS_ON',
|
|
611
629
|
)
|
|
612
630
|
}
|
|
@@ -746,7 +764,7 @@ function reachingTo(nodes: Map<string, TaskNode>, target: string): Set<string> {
|
|
|
746
764
|
export function checkGraph(
|
|
747
765
|
nodes: Map<string, TaskNode>,
|
|
748
766
|
workspaceRoot?: string,
|
|
749
|
-
rules?:
|
|
767
|
+
rules?: GraphRules,
|
|
750
768
|
): void {
|
|
751
769
|
for (const [key, node] of nodes) {
|
|
752
770
|
if (node.id !== key) {
|
|
@@ -762,8 +780,12 @@ export function checkGraph(
|
|
|
762
780
|
}
|
|
763
781
|
for (const node of nodes.values()) refuseSelfClean(node)
|
|
764
782
|
detectCycle(nodes)
|
|
765
|
-
|
|
766
|
-
|
|
783
|
+
const exclusive = rules?.exclusiveOutputs !== false
|
|
784
|
+
const locked = rules?.locked === true
|
|
785
|
+
// Under a lock an exclusive graph has no overlap to mark; a non-exclusive
|
|
786
|
+
// one still needs the addition marks the walk derives.
|
|
787
|
+
if (!locked || !exclusive) detectOutputCollisions(nodes, workspaceRoot, exclusive)
|
|
788
|
+
if (rules?.upfrontKeys !== false && !locked) detectInputOverlaps(nodes, workspaceRoot)
|
|
767
789
|
}
|
|
768
790
|
|
|
769
791
|
/**
|
|
@@ -948,6 +970,19 @@ function witnessed(a: string, b: string): boolean {
|
|
|
948
970
|
if (ga.match(candidate) && gb.match(candidate)) return true
|
|
949
971
|
}
|
|
950
972
|
}
|
|
973
|
+
// Each glob filled in whole, every wildcard a name and `**` one level or
|
|
974
|
+
// none: `**/dist/**` beside `**/*` meets at `x/dist/x`, a path neither
|
|
975
|
+
// last segment builds (solid, M 2026-10-15).
|
|
976
|
+
for (const glob of [a, b]) {
|
|
977
|
+
const segs = glob.split('/')
|
|
978
|
+
if (segs.some((s) => /[[\]{}()!+@]/.test(s))) continue
|
|
979
|
+
for (const drop of [false, true]) {
|
|
980
|
+
const candidate = segs
|
|
981
|
+
.flatMap((s) => (s === '**' ? (drop ? [] : ['x']) : [s.replace(/\*+|\?/g, 'x')]))
|
|
982
|
+
.join('/')
|
|
983
|
+
if (candidate !== '' && ga.match(candidate) && gb.match(candidate)) return true
|
|
984
|
+
}
|
|
985
|
+
}
|
|
951
986
|
return false
|
|
952
987
|
}
|
|
953
988
|
|
|
@@ -1352,13 +1387,13 @@ function neverWritesLocally(n: TaskNode): boolean {
|
|
|
1352
1387
|
}
|
|
1353
1388
|
|
|
1354
1389
|
/**
|
|
1355
|
-
* The way out of an edge-ordered overlap under `rules.exclusiveOutputs`: an
|
|
1390
|
+
* The way out of an edge-ordered overlap under `run.rules.exclusiveOutputs`: an
|
|
1356
1391
|
* ordered pair is correct (the addition shape, item 588), only slower to
|
|
1357
1392
|
* clean and restore, so the rule may be turned off.
|
|
1358
1393
|
*/
|
|
1359
1394
|
const EXCLUSIVE_OUTPUTS_FIX =
|
|
1360
|
-
'Give each task its own output path, or set rules
|
|
1361
|
-
"vx
|
|
1395
|
+
'Give each task its own output path, or set run.rules.exclusiveOutputs: false in ' +
|
|
1396
|
+
".vx/workspace.ts to let a dependant add to its upstream's outputs."
|
|
1362
1397
|
|
|
1363
1398
|
function collide(
|
|
1364
1399
|
a: TaskNode,
|
|
@@ -1383,11 +1418,22 @@ function collide(
|
|
|
1383
1418
|
// dependant runs after its upstream and adds to that tree, so the
|
|
1384
1419
|
// order is fixed and the dependant's own set can be told apart from
|
|
1385
1420
|
// what it found. Marked on both, and the pair is allowed when
|
|
1386
|
-
// `rules.exclusiveOutputs` is off (X-53). Without an edge the two run
|
|
1421
|
+
// `run.rules.exclusiveOutputs` is off (X-53). Without an edge the two run
|
|
1387
1422
|
// in either order, and the refusal below stands.
|
|
1388
1423
|
const [up, down] = reaches(b.id, a.id) ? [a, b] : reaches(a.id, b.id) ? [b, a] : []
|
|
1424
|
+
const parts = {
|
|
1425
|
+
headline: `${a.id} and ${b.id} both write ${ga === gb ? ga : `${ga} / ${gb}`}`,
|
|
1426
|
+
...(field === 'files' ? {} : { what: ['in cache.outputs.workspaceFiles'] }),
|
|
1427
|
+
why: "vx clears a task's outputs before it runs, so one deletes the other's",
|
|
1428
|
+
}
|
|
1389
1429
|
if (up !== undefined && exclusive)
|
|
1390
|
-
throw
|
|
1430
|
+
throw refusal(
|
|
1431
|
+
{
|
|
1432
|
+
...parts,
|
|
1433
|
+
fix: 'give each task its own output path, or set run.rules.exclusiveOutputs: false in .vx/workspace.ts',
|
|
1434
|
+
},
|
|
1435
|
+
{ code: 'VX_E_OUTPUT_OVERLAP', message: `${head}. ${EXCLUSIVE_OUTPUTS_FIX}` },
|
|
1436
|
+
)
|
|
1391
1437
|
if (up !== undefined && down !== undefined) {
|
|
1392
1438
|
const downGlobs = splitNegations(
|
|
1393
1439
|
(field === 'files'
|
|
@@ -1398,9 +1444,12 @@ function collide(
|
|
|
1398
1444
|
;(up.outputsAddedToBy ??= []).push(...(downGlobs ?? []))
|
|
1399
1445
|
return
|
|
1400
1446
|
}
|
|
1401
|
-
throw
|
|
1402
|
-
|
|
1403
|
-
|
|
1447
|
+
throw refusal(
|
|
1448
|
+
{ ...parts, fix: 'give each task its own output path' },
|
|
1449
|
+
{
|
|
1450
|
+
code: 'VX_E_OUTPUT_OVERLAP',
|
|
1451
|
+
message: `${head} and the run still reports success. Give each task its own output path.`,
|
|
1452
|
+
},
|
|
1404
1453
|
)
|
|
1405
1454
|
}
|
|
1406
1455
|
}
|
|
@@ -1504,7 +1553,7 @@ interface Side {
|
|
|
1504
1553
|
}
|
|
1505
1554
|
|
|
1506
1555
|
/**
|
|
1507
|
-
* `rules.upfrontKeys` (X-54): refuse a task whose input globs can match
|
|
1556
|
+
* `run.rules.upfrontKeys` (X-54): refuse a task whose input globs can match
|
|
1508
1557
|
* another task's declared outputs. Such a key reads bytes a producer writes
|
|
1509
1558
|
* this run, so it is not known until the producer ran (`stable-keys.ts`
|
|
1510
1559
|
* keeps it waiting); refused, every key reads only what no task writes and
|
|
@@ -1677,7 +1726,7 @@ function readsOutputs(
|
|
|
1677
1726
|
`${writer.node.id}'s output ${JSON.stringify(shown)} — a task's key must not read ` +
|
|
1678
1727
|
`another task's outputs (the dependency's key already cascades through dependsOn). ` +
|
|
1679
1728
|
`Exclude it: add ${JSON.stringify(`!${out}`)} to ${reader.node.id}'s ` +
|
|
1680
|
-
`cache.inputs.${field}, or set rules
|
|
1729
|
+
`cache.inputs.${field}, or set run.rules.upfrontKeys: false in .vx/workspace.ts to ` +
|
|
1681
1730
|
`let it wait for its producer.`,
|
|
1682
1731
|
'VX_E_INPUT_IS_OUTPUT',
|
|
1683
1732
|
)
|
|
@@ -1725,7 +1774,10 @@ function detectCycle(nodes: Map<string, TaskNode>): void {
|
|
|
1725
1774
|
for (let i = 0; i < stack.length; i += 2) path.push(idsArr[stack[i]!]!)
|
|
1726
1775
|
const startInPath = path.indexOf(idsArr[depIdx]!)
|
|
1727
1776
|
const cycle = [...path.slice(startInPath), idsArr[depIdx]!].join(' -> ')
|
|
1728
|
-
throw new UserError(
|
|
1777
|
+
throw new UserError(
|
|
1778
|
+
`dependsOn cycle: ${cycle} — remove one edge of the loop`,
|
|
1779
|
+
'VX_E_CYCLE',
|
|
1780
|
+
)
|
|
1729
1781
|
}
|
|
1730
1782
|
color[depIdx] = GRAY
|
|
1731
1783
|
stack.push(depIdx, 0)
|