@vzn/vx 0.0.636 → 0.0.637
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 +213 -207
- package/docs/cli.md +699 -655
- package/docs/execution.md +67 -75
- package/docs/features.md +44 -40
- package/docs/patterns.md +26 -25
- package/docs/schema.md +376 -198
- 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 +2 -2
- package/schemas/show.json +5 -3
- package/schemas/why.json +15 -1
- package/skills/vx/SKILL.md +16 -12
- package/src/bin.ts +17 -2
- package/src/cache/archive.ts +11 -0
- package/src/cache/cache.ts +105 -53
- package/src/cache/git-inputs.ts +277 -63
- package/src/cache/index.ts +7 -1
- package/src/cache/inputs.ts +125 -35
- package/src/cache/key-fold.ts +8 -7
- package/src/cache/layer.ts +8 -17
- package/src/cache/layered-cache.ts +16 -102
- package/src/cache/policy.ts +25 -29
- package/src/cache/run-history.ts +36 -14
- package/src/cache/schema.ts +21 -23
- package/src/cache/scoped-cache.ts +224 -0
- package/src/cli/cache.ts +150 -28
- package/src/cli/completions.ts +2 -2
- package/src/cli/core-alias.ts +1 -1
- package/src/cli/foreign-flags.ts +13 -12
- package/src/cli/help.ts +94 -63
- package/src/cli/index.ts +42 -13
- package/src/cli/info.ts +38 -23
- package/src/cli/init.ts +115 -406
- package/src/cli/last.ts +145 -4
- package/src/cli/lock.ts +333 -111
- package/src/cli/plan-format.ts +2 -2
- package/src/cli/plugin-commands.ts +5 -9
- package/src/cli/plugin-templates.ts +10 -10
- package/src/cli/run.ts +103 -149
- package/src/cli/select.ts +33 -39
- package/src/cli/show.ts +17 -19
- package/src/cli/task-verb.ts +2 -9
- package/src/cli/upgrade.ts +402 -145
- package/src/cli/watch-cycle.ts +9 -2
- package/src/cli/watch-filter.ts +33 -60
- package/src/cli/watch-fs.ts +29 -16
- package/src/cli/watch-judge.ts +12 -3
- package/src/cli/watch-set.ts +16 -42
- package/src/cli/watch.ts +188 -215
- package/src/cli/why.ts +28 -17
- package/src/cli/workspace-config.ts +31 -68
- 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 +19 -7
- package/src/graph/task-graph.ts +44 -20
- package/src/handoff.ts +169 -0
- package/src/index.ts +24 -57
- package/src/orchestrator/affected-tasks.ts +7 -5
- 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 +158 -63
- package/src/orchestrator/fingerprint-watch.ts +18 -15
- package/src/orchestrator/forecast.ts +10 -2
- package/src/orchestrator/framed-output.ts +8 -3
- package/src/orchestrator/graph-stage.ts +5 -5
- package/src/orchestrator/history.ts +70 -30
- package/src/orchestrator/hit-restore.ts +15 -3
- package/src/orchestrator/index.ts +18 -4
- package/src/orchestrator/local-shortcircuit.ts +2 -1
- package/src/orchestrator/lockfile-claim.ts +79 -20
- package/src/orchestrator/logger.ts +71 -39
- package/src/orchestrator/metrics.ts +71 -6
- 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 +87 -63
- package/src/orchestrator/plugin.ts +94 -60
- package/src/orchestrator/prepare.ts +113 -72
- package/src/orchestrator/projects.ts +102 -99
- package/src/orchestrator/remote-prefetch.ts +2 -1
- 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-report.ts +9 -9
- package/src/orchestrator/run-settings.ts +48 -0
- package/src/orchestrator/run.ts +121 -95
- 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 +48 -6
- package/src/orchestrator/summary.ts +46 -62
- package/src/orchestrator/task-hash.ts +12 -83
- package/src/orchestrator/telemetry.ts +3 -3
- package/src/settings.ts +417 -0
- package/src/util/errors.ts +1 -1
- package/src/util/essential-env.ts +31 -0
- package/src/util/hide-env.ts +107 -0
- package/src/util/index.ts +8 -2
- package/src/util/paths.ts +10 -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/ulid.ts +1 -1
- package/src/util/verbs.ts +0 -15
- package/src/workspace/affected.ts +77 -222
- 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 +57 -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 +242 -146
- 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 +362 -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
|
@@ -13,11 +13,13 @@ import { beginRun, mark, nearest, UserError } from '../util/index.js'
|
|
|
13
13
|
import {
|
|
14
14
|
Cache,
|
|
15
15
|
type CacheLayer,
|
|
16
|
+
applyCacheFlags,
|
|
16
17
|
type CachePolicy,
|
|
17
18
|
FULL_CACHE_POLICY,
|
|
18
|
-
|
|
19
|
+
parseCachePolicy,
|
|
19
20
|
GitFilesCache,
|
|
20
21
|
LayeredCache,
|
|
22
|
+
ScopedCache,
|
|
21
23
|
applyGitEnumeration,
|
|
22
24
|
gitPathspecs,
|
|
23
25
|
MAX_SCOPED_PATHSPECS,
|
|
@@ -27,15 +29,11 @@ import {
|
|
|
27
29
|
import {
|
|
28
30
|
buildPackageGraph,
|
|
29
31
|
computeNestedProjectDirs,
|
|
30
|
-
computeWorkspaceFingerprints,
|
|
31
32
|
findWorkspaceRoot,
|
|
32
33
|
cacheScopeEnvError,
|
|
33
34
|
isCacheScope,
|
|
34
35
|
type LoadReads,
|
|
35
|
-
|
|
36
|
-
FROZEN_WITHOUT_LOCK,
|
|
37
|
-
type Lockfile,
|
|
38
|
-
readLockfile,
|
|
36
|
+
readClaimedFiles,
|
|
39
37
|
resolveCacheDir,
|
|
40
38
|
resolveStoreRoot,
|
|
41
39
|
type ProjectEntry,
|
|
@@ -44,6 +42,7 @@ import {
|
|
|
44
42
|
buildTaskGraph,
|
|
45
43
|
excludeDependencies,
|
|
46
44
|
expandRequested,
|
|
45
|
+
type GraphRules,
|
|
47
46
|
isGroupTask,
|
|
48
47
|
type TaskNode,
|
|
49
48
|
undeclaredDepsError,
|
|
@@ -64,6 +63,8 @@ import {
|
|
|
64
63
|
isDefaultBuild,
|
|
65
64
|
loadProjects,
|
|
66
65
|
loadWorkspacePlugins,
|
|
66
|
+
lockOfDiscovery,
|
|
67
|
+
lockReader,
|
|
67
68
|
type LoadedProjects,
|
|
68
69
|
} from './projects.js'
|
|
69
70
|
import { keptByAffected } from './affected-tasks.js'
|
|
@@ -71,10 +72,12 @@ import { applyGraphStage } from './graph-stage.js'
|
|
|
71
72
|
import { keyExcludedDependencies } from './excluded-keys.js'
|
|
72
73
|
import { FingerprintWatch } from './fingerprint-watch.js'
|
|
73
74
|
import type { VxPlugin } from './plugin.js'
|
|
75
|
+
import { currentBranch } from './run-context.js'
|
|
74
76
|
import { createHashCache, type HashCache } from './task-hash.js'
|
|
75
77
|
import type { Logger } from './logger.js'
|
|
76
78
|
import type { RunOptions } from './options.js'
|
|
77
79
|
import { refuseFiltersThroughGroups } from './upstream.js'
|
|
80
|
+
import { runRules } from '../settings.js'
|
|
78
81
|
|
|
79
82
|
export interface PreparedRun {
|
|
80
83
|
workspaceRoot: string
|
|
@@ -97,7 +100,7 @@ export interface PreparedRun {
|
|
|
97
100
|
* would be saved still cleans its outputs before executing.
|
|
98
101
|
*/
|
|
99
102
|
hasRemoteLayer: boolean
|
|
100
|
-
/** `--cache` with the workspace's `cacheScope` applied: what the layers were built under. */
|
|
103
|
+
/** `--cache-policy` with the workspace's `cacheScope` applied: what the layers were built under. */
|
|
101
104
|
cachePolicy: CachePolicy
|
|
102
105
|
/**
|
|
103
106
|
* Scheduling priorities from plugins' `schedule` stage (task id → weight,
|
|
@@ -107,6 +110,12 @@ export interface PreparedRun {
|
|
|
107
110
|
* nothing-changed run never dispatches. Runs the stage once.
|
|
108
111
|
*/
|
|
109
112
|
priorities: () => Promise<ReadonlyMap<string, number>>
|
|
113
|
+
/**
|
|
114
|
+
* The run's branch (`run.branch`, else git's, else the CI provider's):
|
|
115
|
+
* what its history records and the timings a forecast reads first.
|
|
116
|
+
* Learned on first ask, once per run.
|
|
117
|
+
*/
|
|
118
|
+
branch: () => string | null
|
|
110
119
|
nodes: Map<string, TaskNode>
|
|
111
120
|
/**
|
|
112
121
|
* The tasks `--exclude-dependencies` took out of the schedule: keyed as a
|
|
@@ -135,8 +144,7 @@ export interface PreparedRun {
|
|
|
135
144
|
* mistaken for a workspace vx was never set up in.
|
|
136
145
|
*/
|
|
137
146
|
anyProjectConfig: boolean
|
|
138
|
-
|
|
139
|
-
/** Whether a task has since rewritten what that fingerprint folded (fingerprint-watch.ts). */
|
|
147
|
+
/** The claimed root files, and whether a task has since rewritten one (fingerprint-watch.ts). */
|
|
140
148
|
fingerprintWatch: FingerprintWatch
|
|
141
149
|
nestedDirsByProject: Map<string, string[]>
|
|
142
150
|
/**
|
|
@@ -149,11 +157,9 @@ export interface PreparedRun {
|
|
|
149
157
|
/** Every project discovery found, in or out of scope — header scope bar. */
|
|
150
158
|
workspaceProjectCount: number
|
|
151
159
|
/**
|
|
152
|
-
* Per-run memo for derived hashes —
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
* the same project's package.json (and the same task config
|
|
156
|
-
* object) aren't re-hashed on every task in that project.
|
|
160
|
+
* Per-run memo for derived hashes — task-config hash keyed by config
|
|
161
|
+
* object identity, resolved file lists. Shared across every task's
|
|
162
|
+
* `computeTaskHash` call so the same config object isn't re-hashed.
|
|
157
163
|
*/
|
|
158
164
|
hashCache: HashCache
|
|
159
165
|
/**
|
|
@@ -188,22 +194,19 @@ export interface PreparedRun {
|
|
|
188
194
|
export async function prepareRun(options: RunOptions, log: Logger): Promise<PreparedRun> {
|
|
189
195
|
beginRun()
|
|
190
196
|
mark('startup')
|
|
191
|
-
//
|
|
192
|
-
//
|
|
197
|
+
// A claimed root file is read once. A watch cycle is a new run and reads
|
|
198
|
+
// them afresh.
|
|
193
199
|
const reads: LoadReads = new Map()
|
|
194
|
-
const workspaceRoot = await findWorkspaceRoot(options.cwd
|
|
195
|
-
//
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
(
|
|
203
|
-
|
|
204
|
-
return read
|
|
205
|
-
}))
|
|
206
|
-
if (options.frozen === true && options.staged === undefined) void readLock().catch(() => {})
|
|
200
|
+
const workspaceRoot = await findWorkspaceRoot(options.cwd)
|
|
201
|
+
// The lock is where a run's projects come from, so the read (1.1 MB at
|
|
202
|
+
// 1,000 projects, ~10 ms to read and parse) starts here and overlaps the
|
|
203
|
+
// workspace config's evaluation (X-181). A missing or malformed lock
|
|
204
|
+
// refuses at discovery, after any earlier refusal.
|
|
205
|
+
const reused = options.discovered?.root === workspaceRoot ? options.discovered : undefined
|
|
206
|
+
const readLock =
|
|
207
|
+
(reused !== undefined ? lockOfDiscovery(reused.projects) : undefined) ??
|
|
208
|
+
lockReader(workspaceRoot)
|
|
209
|
+
if (reused === undefined) void readLock().catch(() => {})
|
|
207
210
|
// An UNSCOPED run (no explicit scope, at least one bare task name)
|
|
208
211
|
// enumerates the whole tree whatever the configs say, so git starts
|
|
209
212
|
// HERE — the walk needs only the root — and overlaps the workspace
|
|
@@ -221,18 +224,21 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
221
224
|
// whole-tree enumeration a scoped run would otherwise scope later, here or
|
|
222
225
|
// in the CLI's selection pass; the run reuses it instead of walking the
|
|
223
226
|
// tree twice (G-75).
|
|
224
|
-
const reused = options.discovered?.root === workspaceRoot ? options.discovered : undefined
|
|
225
227
|
const git =
|
|
226
228
|
(reused !== undefined ? gitOfDiscovery(reused.projects) : undefined) ??
|
|
227
229
|
lazyGitEnumeration(workspaceRoot)
|
|
228
230
|
if (wholeTree) void git.start()
|
|
229
|
-
const workspace = await loadWorkspace(workspaceRoot, reads)
|
|
230
231
|
const { workspaceConfig, plugins } = await loadWorkspacePlugins(workspaceRoot, (m) =>
|
|
231
232
|
log.status(m),
|
|
232
233
|
)
|
|
233
234
|
mark('workspace config')
|
|
235
|
+
let learnedBranch: string | null | undefined
|
|
236
|
+
const branch = (): string | null =>
|
|
237
|
+
learnedBranch === undefined
|
|
238
|
+
? (learnedBranch = currentBranch(workspaceRoot, workspaceConfig, options.branch))
|
|
239
|
+
: learnedBranch
|
|
234
240
|
// `--cache-dir <path>` (RunOptions.cacheDir) overrides the workspace
|
|
235
|
-
// `
|
|
241
|
+
// `cache.dir` + the `.vx/cache` default; resolved relative to cwd.
|
|
236
242
|
// A cache dir named anywhere holds everything; the default shares its
|
|
237
243
|
// entries through the user's store.
|
|
238
244
|
const cacheDir = options.cacheDir
|
|
@@ -244,7 +250,14 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
244
250
|
const projectMetas =
|
|
245
251
|
reused !== undefined
|
|
246
252
|
? reused.projects
|
|
247
|
-
: await discoverProjects(
|
|
253
|
+
: await discoverProjects(
|
|
254
|
+
workspaceRoot,
|
|
255
|
+
readLock,
|
|
256
|
+
plugins,
|
|
257
|
+
cacheDir,
|
|
258
|
+
(m) => log.status(m),
|
|
259
|
+
git,
|
|
260
|
+
)
|
|
248
261
|
mark('discover projects')
|
|
249
262
|
|
|
250
263
|
// SCOPED config loading: configs are programs, and evaluating 1090
|
|
@@ -264,7 +277,7 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
264
277
|
// anchors alone are the scope and nothing else needs its config
|
|
265
278
|
// evaluated.
|
|
266
279
|
// `vx run //#lint` is Turbo's `turbo run //#lint`: the root project's
|
|
267
|
-
// task, the
|
|
280
|
+
// task, under the name it has when something names it (D-39).
|
|
268
281
|
const root = path.resolve(workspaceRoot)
|
|
269
282
|
const rootName = projectMetas.find((m) => path.resolve(m.dir) === root)?.name
|
|
270
283
|
const tasks =
|
|
@@ -282,20 +295,26 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
282
295
|
options.projects !== undefined ? [...options.projects, ...anchored] : hasBare ? 'all' : anchored
|
|
283
296
|
// Its own mark: the package graph is the bulk of what used to be charged
|
|
284
297
|
// to `open cache` (measured on a 1,000-project tree, 2026-09-20 — the
|
|
285
|
-
// graph 3-8 ms against the cache open's ~1 ms and the
|
|
298
|
+
// graph 3-8 ms against the cache open's ~1 ms and the root files' ~0.3),
|
|
286
299
|
// so a reader profiling that stage was sent to the wrong code.
|
|
287
300
|
mark('package graph')
|
|
288
301
|
|
|
289
|
-
// The local cache opens BEFORE the configs load: it is also where their
|
|
290
|
-
// cached evaluations live.
|
|
291
302
|
// `--cache-scope`, then `VX_CACHE_SCOPE`, win over the workspace (and `github()`'s guess):
|
|
292
303
|
// a CI job that knows its trust level says so without editing config.
|
|
293
304
|
const envScope = process.env['VX_CACHE_SCOPE'] || undefined
|
|
294
305
|
if (envScope !== undefined && !isCacheScope(envScope)) throw cacheScopeEnvError()
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
306
|
+
const workspacePolicy = workspaceCachePolicy(workspaceConfig)
|
|
307
|
+
const cacheScope = options.cacheScope ?? envScope ?? workspaceConfig?.cache?.scope
|
|
308
|
+
const asked =
|
|
309
|
+
options.cache ?? applyCacheFlags(workspacePolicy ?? FULL_CACHE_POLICY, options.cacheFlags)
|
|
310
|
+
// A laptop reads the shared cache and does not write it unless a policy
|
|
311
|
+
// names the remote (owner, 2026-10-03; Security F1, 2026-10-14): a key
|
|
312
|
+
// holds only declared inputs, so a laptop's write could be replayed on CI.
|
|
313
|
+
// A workspace policy naming the remote asks to write it, as `--cache-policy` does.
|
|
314
|
+
const readOnly =
|
|
315
|
+
options.remoteReadOnlyByDefault === true &&
|
|
316
|
+
!(options.cache === undefined && namesRemote(workspaceConfig?.cache?.policy))
|
|
317
|
+
const policy: CachePolicy = readOnly ? { ...asked, remoteWrite: false } : asked
|
|
299
318
|
const localCache = new Cache(
|
|
300
319
|
cacheDir,
|
|
301
320
|
{ read: policy.localRead, write: policy.localWrite },
|
|
@@ -305,29 +324,20 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
305
324
|
await storeRoot,
|
|
306
325
|
)
|
|
307
326
|
localCache.assertWritable()
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
// plugin claims (`VxPlugin.fingerprint`).
|
|
327
|
+
// The root files the plugins claim (`VxPlugin.fingerprint`), whose bytes
|
|
328
|
+
// their claimants fold: read now so a task that rewrites one is caught.
|
|
311
329
|
const fingerprintsAt = Date.now()
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
new Set(fingerprintClaims(plugins).keys()),
|
|
315
|
-
reads,
|
|
316
|
-
)
|
|
317
|
-
const workspaceFingerprint = fingerprints.unclaimed
|
|
330
|
+
const claimed = [...fingerprintClaims(plugins).keys()]
|
|
331
|
+
const claimedBytes = await readClaimedFiles(workspaceRoot, claimed, reads)
|
|
318
332
|
mark('open cache')
|
|
319
333
|
|
|
320
|
-
//
|
|
321
|
-
//
|
|
322
|
-
//
|
|
323
|
-
// their locked values. Default (local) runs ALWAYS evaluate live:
|
|
324
|
-
// a byte hash can't see a config's import closure (shared presets),
|
|
325
|
-
// so consuming the lock by default would silently serve stale
|
|
326
|
-
// freezes. `vx lock --check` is the full re-evaluation audit.
|
|
334
|
+
// Configs load FROM .vx/projects.lock.json with no evaluation and no check of
|
|
335
|
+
// the configs (`vx lock --check` re-evaluates them; a run does not):
|
|
336
|
+
// only `vx lock` evaluates (owner, 2026-10-14).
|
|
327
337
|
// See docs/design/config-lock-2026-06.md.
|
|
328
338
|
// Read once, and only when a config is to be read from it: the CLI's
|
|
329
339
|
// selection pass may have staged every config (`options.staged`), and
|
|
330
|
-
// its load refused a
|
|
340
|
+
// its load refused a run without a lock. A 1,000-project lock is
|
|
331
341
|
// 1.1 MB of JSON (I-27); a run with nothing staged started its read
|
|
332
342
|
// beside discovery, above.
|
|
333
343
|
const staged = options.staged
|
|
@@ -336,8 +346,7 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
336
346
|
projectMetas.every(
|
|
337
347
|
(m) => typeof m.configPath !== 'string' || m.configPath === '' || staged.has(m.name),
|
|
338
348
|
)
|
|
339
|
-
if (
|
|
340
|
-
const lock = options.frozen === true ? readLock : null
|
|
349
|
+
if (!allStaged) await readLock()
|
|
341
350
|
|
|
342
351
|
const loadArgs = {
|
|
343
352
|
workspaceRoot,
|
|
@@ -345,8 +354,7 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
345
354
|
plugins,
|
|
346
355
|
projectMetas,
|
|
347
356
|
packageGraph,
|
|
348
|
-
|
|
349
|
-
evalCache: { store: localCache, workspaceRoot, workspaceFingerprint: fingerprints.all },
|
|
357
|
+
configs: readLock,
|
|
350
358
|
warn: (m: string) => log.status(m),
|
|
351
359
|
}
|
|
352
360
|
let loaded: LoadedProjects
|
|
@@ -358,8 +366,8 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
358
366
|
...(options.staged !== undefined ? { staged: options.staged } : {}),
|
|
359
367
|
})
|
|
360
368
|
} catch (err) {
|
|
361
|
-
// The cache opened before the configs loaded
|
|
362
|
-
//
|
|
369
|
+
// The cache opened before the configs loaded; a config error must not
|
|
370
|
+
// leak the handle.
|
|
363
371
|
localCache.close()
|
|
364
372
|
throw err
|
|
365
373
|
}
|
|
@@ -453,6 +461,7 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
453
461
|
workspaceRoot,
|
|
454
462
|
cacheDir,
|
|
455
463
|
warn: (m) => log.status(m),
|
|
464
|
+
scope: cacheScope,
|
|
456
465
|
localCache,
|
|
457
466
|
policy,
|
|
458
467
|
})
|
|
@@ -465,6 +474,8 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
465
474
|
localCache.close()
|
|
466
475
|
throw err
|
|
467
476
|
}
|
|
477
|
+
// One scoping rule above every layer, local and remote alike.
|
|
478
|
+
if (cacheScope !== undefined) cache = new ScopedCache(cache, cacheScope)
|
|
468
479
|
// Ask the LAYER, don't infer. Identity against `localCache` answers a
|
|
469
480
|
// DIFFERENT question — "did the plugin hand back something other than the
|
|
470
481
|
// handle I passed in?" — which an ordinary pass-through decorator (a
|
|
@@ -480,9 +491,14 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
480
491
|
// returns.
|
|
481
492
|
try {
|
|
482
493
|
const hashCache = createHashCache()
|
|
483
|
-
const fingerprintWatch = new FingerprintWatch(
|
|
494
|
+
const fingerprintWatch = new FingerprintWatch(
|
|
495
|
+
workspaceRoot,
|
|
496
|
+
claimed,
|
|
497
|
+
claimedBytes,
|
|
498
|
+
fingerprintsAt,
|
|
499
|
+
)
|
|
484
500
|
|
|
485
|
-
// Empty-cases bookkeeping. We still construct the cache +
|
|
501
|
+
// Empty-cases bookkeeping. We still construct the cache + watch
|
|
486
502
|
// so the caller's try/finally pattern can close it uniformly.
|
|
487
503
|
const emptyRun = (reason?: 'none-affected'): PreparedRun => ({
|
|
488
504
|
workspaceRoot,
|
|
@@ -494,6 +510,7 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
494
510
|
hasRemoteLayer,
|
|
495
511
|
cachePolicy: policy,
|
|
496
512
|
priorities: () => Promise.resolve(new Map()),
|
|
513
|
+
branch,
|
|
497
514
|
nodes: new Map(),
|
|
498
515
|
keyOnly: new Map(),
|
|
499
516
|
unresolvedTasks,
|
|
@@ -501,7 +518,6 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
501
518
|
projects,
|
|
502
519
|
hintProjects,
|
|
503
520
|
anyProjectConfig: projectsWithConfigs.length > 0,
|
|
504
|
-
workspaceFingerprint,
|
|
505
521
|
fingerprintWatch,
|
|
506
522
|
nestedDirsByProject,
|
|
507
523
|
gitFilesCache: new GitFilesCache(),
|
|
@@ -521,6 +537,10 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
521
537
|
// the whole workspace: a `^name` nothing loaded declares may be declared
|
|
522
538
|
// by a config the scope left out, so the builder hands it back instead of
|
|
523
539
|
// refusing it, and the rest decide.
|
|
540
|
+
// A run never checks the graph rules: `vx lock` and `vx lock --check`
|
|
541
|
+
// do (owner, 2026-10-14). `exclusiveOutputs: false` still asks the walk
|
|
542
|
+
// for its addition marks.
|
|
543
|
+
const graphRules: GraphRules = { ...runRules(workspaceConfig), locked: true }
|
|
524
544
|
const unproven: Array<[taskId: string, name: string]> = []
|
|
525
545
|
const graphOf = (req: typeof requested) =>
|
|
526
546
|
buildTaskGraph({
|
|
@@ -528,7 +548,7 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
528
548
|
packageGraph,
|
|
529
549
|
requested: req,
|
|
530
550
|
workspaceRoot,
|
|
531
|
-
rules:
|
|
551
|
+
rules: graphRules,
|
|
532
552
|
...(partialLoad
|
|
533
553
|
? { undeclaredDeps: (id: string, name: string) => void unproven.push([id, name]) }
|
|
534
554
|
: {}),
|
|
@@ -541,7 +561,7 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
541
561
|
cacheDir,
|
|
542
562
|
projectMetas,
|
|
543
563
|
warn: (m) => log.status(m),
|
|
544
|
-
rules:
|
|
564
|
+
rules: graphRules,
|
|
545
565
|
})
|
|
546
566
|
// `--affected` selects over the FINAL graph: an edge or an input a
|
|
547
567
|
// `graph` hook adds moves a key, so the hooks run first, over every
|
|
@@ -681,7 +701,6 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
681
701
|
dropped,
|
|
682
702
|
cache,
|
|
683
703
|
workspaceRoot,
|
|
684
|
-
workspaceFingerprint,
|
|
685
704
|
forwardArgs: options.forwardArgs,
|
|
686
705
|
nestedDirsByProject,
|
|
687
706
|
gitFilesCache,
|
|
@@ -697,6 +716,7 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
697
716
|
cacheDir,
|
|
698
717
|
warn: (m) => log.status(m),
|
|
699
718
|
localCache,
|
|
719
|
+
branch: branch(),
|
|
700
720
|
})
|
|
701
721
|
: Promise.resolve(new Map()))
|
|
702
722
|
|
|
@@ -710,6 +730,7 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
710
730
|
hasRemoteLayer,
|
|
711
731
|
cachePolicy: policy,
|
|
712
732
|
priorities,
|
|
733
|
+
branch,
|
|
713
734
|
nodes,
|
|
714
735
|
keyOnly,
|
|
715
736
|
unresolvedTasks,
|
|
@@ -717,7 +738,6 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
717
738
|
projects,
|
|
718
739
|
hintProjects,
|
|
719
740
|
anyProjectConfig: projectsWithConfigs.length > 0,
|
|
720
|
-
workspaceFingerprint,
|
|
721
741
|
fingerprintWatch,
|
|
722
742
|
nestedDirsByProject,
|
|
723
743
|
gitFilesCache,
|
|
@@ -771,7 +791,7 @@ function undeclaredIn(
|
|
|
771
791
|
|
|
772
792
|
/**
|
|
773
793
|
* Refuses the first `^name` in `unproven` that no project in the whole
|
|
774
|
-
* workspace declares. `load`
|
|
794
|
+
* workspace declares. `load` reads the locked configs a scoped run left out;
|
|
775
795
|
* it is reached only when a `^name` found no holder and nothing loaded
|
|
776
796
|
* declares it, so a run that names only declared tasks never pays for it.
|
|
777
797
|
* The rest failing to load leaves the names unjudged: an out-of-scope
|
|
@@ -815,3 +835,24 @@ function pruneToClosure(nodes: Map<string, TaskNode>, roots: ReadonlySet<string>
|
|
|
815
835
|
else if (!roots.has(id)) n.requested = false
|
|
816
836
|
}
|
|
817
837
|
}
|
|
838
|
+
|
|
839
|
+
const namesRemote = (spec: string | undefined): boolean =>
|
|
840
|
+
spec !== undefined && /remote:[rw]/.test(spec)
|
|
841
|
+
|
|
842
|
+
/**
|
|
843
|
+
* The workspace's `cache.policy`, parsed: the schema checks only its shape,
|
|
844
|
+
* since the grammar lives in the cache module the workspace module may not
|
|
845
|
+
* import. Undefined when it names none.
|
|
846
|
+
*/
|
|
847
|
+
function workspaceCachePolicy(config: WorkspaceConfig | null | undefined): CachePolicy | undefined {
|
|
848
|
+
const spec = config?.cache?.policy
|
|
849
|
+
if (spec === undefined) return undefined
|
|
850
|
+
try {
|
|
851
|
+
return parseCachePolicy(spec)
|
|
852
|
+
} catch (err) {
|
|
853
|
+
throw new UserError(
|
|
854
|
+
`.vx/workspace: \`cache.policy\`: ${err instanceof Error ? err.message : String(err)}`,
|
|
855
|
+
'VX_E_CONFIG',
|
|
856
|
+
)
|
|
857
|
+
}
|
|
858
|
+
}
|