@vzn/vx 0.0.16 → 0.0.21
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/LICENSE +1 -1
- package/README.md +135 -124
- package/index.ts +10 -0
- package/launcher.mjs +10 -11
- package/package.json +11 -7
- package/src/bin.ts +14 -4
- package/src/cache/archive.ts +659 -0
- package/src/cache/cache.ts +769 -1101
- package/src/cache/chained-cache.ts +194 -0
- package/src/cache/config-evals.ts +89 -0
- package/src/cache/file-hashes.ts +262 -0
- package/src/cache/git-inputs.ts +637 -0
- package/src/cache/index.ts +24 -5
- package/src/cache/inputs.ts +373 -456
- package/src/cache/layer.ts +604 -0
- package/src/cache/layered-cache.ts +96 -43
- package/src/cache/output-index.ts +224 -0
- package/src/cache/policy.ts +84 -0
- package/src/cache/run-history.ts +147 -0
- package/src/cache/tar-stream.ts +358 -0
- package/src/cache/zstd.ts +160 -0
- package/src/cli/cache.ts +75 -21
- package/src/cli/completions.ts +104 -0
- package/src/cli/core-alias.ts +25 -0
- package/src/cli/help.ts +195 -128
- package/src/cli/index.ts +116 -46
- package/src/cli/info.ts +122 -63
- package/src/cli/init.ts +75 -0
- package/src/cli/last.ts +172 -0
- package/src/cli/lock.ts +2 -1
- package/src/cli/plan-format.ts +72 -6
- package/src/cli/plugin-commands.ts +103 -0
- package/src/cli/run.ts +245 -271
- package/src/cli/select.ts +332 -0
- package/src/cli/show.ts +144 -69
- package/src/cli/upgrade.ts +9 -7
- package/src/cli/watch.ts +858 -110
- package/src/cli/why.ts +223 -0
- package/src/cli/workspace-config.ts +126 -0
- package/src/config.ts +187 -150
- package/src/exec/env.ts +18 -1
- package/src/exec/executor.ts +224 -0
- package/src/exec/index.ts +24 -2
- package/src/exec/local-executor.ts +41 -0
- package/src/exec/runner.ts +166 -40
- package/src/exec/sandbox-binds.ts +182 -0
- package/src/exec/sandbox-paths.ts +50 -0
- package/src/exec/sandbox-runtime.ts +642 -260
- package/src/exec/sandbox-violations.ts +280 -0
- package/src/graph/index.ts +4 -10
- package/src/graph/scheduler.ts +232 -203
- package/src/graph/task-graph.ts +222 -8
- package/src/index.ts +146 -153
- package/src/orchestrator/admission.ts +137 -0
- package/src/orchestrator/deferred-outputs.ts +147 -0
- package/src/orchestrator/doctor.ts +255 -0
- package/src/orchestrator/download-policy.ts +171 -0
- package/src/orchestrator/events.ts +72 -8
- package/src/orchestrator/execute-task.ts +374 -549
- package/src/orchestrator/failure-mode.ts +202 -0
- package/src/orchestrator/framed-output.ts +126 -69
- package/src/orchestrator/history.ts +126 -59
- package/src/orchestrator/hit-restore.ts +221 -0
- package/src/orchestrator/index.ts +77 -93
- package/src/orchestrator/local-shortcircuit.ts +22 -0
- package/src/orchestrator/lockfile-claim.ts +281 -0
- package/src/orchestrator/logger.ts +208 -35
- package/src/orchestrator/metrics.ts +102 -1388
- package/src/orchestrator/miss-save.ts +169 -0
- package/src/orchestrator/options.ts +55 -34
- package/src/orchestrator/persistent.ts +79 -0
- package/src/orchestrator/placement.ts +182 -0
- package/src/orchestrator/plan.ts +144 -1
- package/src/orchestrator/plugin-host.ts +357 -120
- package/src/orchestrator/plugin.ts +298 -58
- package/src/orchestrator/prepare.ts +202 -106
- package/src/orchestrator/projects.ts +259 -0
- package/src/orchestrator/remote-prefetch.ts +12 -8
- package/src/orchestrator/run-artifacts.ts +63 -16
- package/src/orchestrator/run-context.ts +167 -29
- package/src/orchestrator/run-records.ts +149 -0
- package/src/orchestrator/run-report.ts +74 -47
- package/src/orchestrator/run.ts +534 -417
- package/src/orchestrator/sandbox-request.ts +243 -0
- package/src/orchestrator/save-lane.ts +47 -0
- package/src/orchestrator/signals.ts +111 -0
- package/src/orchestrator/stable-keys.ts +129 -31
- package/src/orchestrator/status-line.ts +49 -9
- package/src/orchestrator/summary.ts +90 -26
- package/src/orchestrator/tally.ts +78 -35
- package/src/orchestrator/task-hash.ts +111 -15
- package/src/orchestrator/task-log-buffer.ts +288 -0
- package/src/orchestrator/telemetry-host.ts +34 -6
- package/src/orchestrator/telemetry.ts +182 -36
- package/src/orchestrator/upstream.ts +72 -31
- package/src/util/cgroup.ts +160 -0
- package/src/util/edit-distance.ts +70 -0
- package/src/util/errors.ts +15 -1
- package/src/util/index.ts +15 -2
- package/src/util/num.ts +37 -0
- package/src/util/paths.ts +64 -0
- package/src/util/settle.ts +67 -0
- package/src/util/size.ts +5 -1
- package/src/util/tail.ts +66 -0
- package/src/util/timing.ts +63 -0
- package/src/util/verbs.ts +39 -0
- package/src/workspace/affected.ts +250 -23
- package/src/workspace/config-cache.ts +313 -0
- package/src/workspace/config-eval.ts +217 -0
- package/src/workspace/config-imports.ts +162 -0
- package/src/workspace/config-schema.ts +837 -0
- package/src/workspace/filter.ts +65 -10
- package/src/workspace/fingerprint.ts +53 -4
- package/src/workspace/index.ts +42 -3
- package/src/workspace/lockfile.ts +25 -9
- package/src/workspace/migrate-scripts.ts +134 -0
- package/src/workspace/migration.ts +314 -0
- package/src/workspace/nested-dirs.ts +21 -7
- package/src/workspace/package-graph.ts +114 -41
- package/src/workspace/project-loader.ts +224 -450
- package/src/workspace/workspace.ts +298 -72
- package/src/cache/cas-backend.ts +0 -97
- package/src/cache/digest.ts +0 -47
- package/src/cache/tar.ts +0 -343
- package/src/cli/backend.ts +0 -23
- package/src/cli/mcp-rpc.ts +0 -292
- package/src/cli/mcp.ts +0 -92
- package/src/cli/migrate-nx.ts +0 -450
- package/src/cli/migrate-turbo.ts +0 -364
- package/src/cli/migrate.ts +0 -219
- package/src/orchestrator/devframe-surface.ts +0 -74
- package/src/orchestrator/predict.ts +0 -106
- package/src/orchestrator/protocol.ts +0 -156
- package/src/orchestrator/resources.ts +0 -59
- package/src/orchestrator/run-state.ts +0 -145
- package/src/orchestrator/verify.ts +0 -164
- package/src/orchestrator/wire-render.ts +0 -92
- package/src/orchestrator/wire.ts +0 -212
package/src/graph/task-graph.ts
CHANGED
|
@@ -23,8 +23,8 @@ export interface TaskNode {
|
|
|
23
23
|
/** Ids of tasks that must complete before this one runs. */
|
|
24
24
|
deps: string[]
|
|
25
25
|
/**
|
|
26
|
-
* True for the tasks the user actually asked for (via cwd,
|
|
27
|
-
* or `pkg#task`). False for
|
|
26
|
+
* True for the tasks the user actually asked for (via cwd, `--all`,
|
|
27
|
+
* `--filter`, or `pkg#task`). False for deps pulled in by `dependsOn`
|
|
28
28
|
* expansion. Used by the orchestrator to scope `forwardArgs` so trailing
|
|
29
29
|
* CLI args don't leak into upstream tasks the user didn't address.
|
|
30
30
|
*/
|
|
@@ -38,12 +38,38 @@ export interface TaskNode {
|
|
|
38
38
|
* them `requested`, so `forwardArgs` scoping is unaffected.
|
|
39
39
|
*/
|
|
40
40
|
surfaced?: boolean
|
|
41
|
+
/**
|
|
42
|
+
* Extra cache-key material from plugins' `key` stage, as sorted
|
|
43
|
+
* `[name, value]` pairs; set by `prepareRun`, folded by `Cache.key`.
|
|
44
|
+
* Absent when no plugin declares the stage.
|
|
45
|
+
*/
|
|
46
|
+
keyParts?: ReadonlyArray<readonly [name: string, value: string]>
|
|
41
47
|
}
|
|
42
48
|
|
|
43
49
|
export function taskId(project: string, task: string): string {
|
|
44
50
|
return `${project}#${task}`
|
|
45
51
|
}
|
|
46
52
|
|
|
53
|
+
/**
|
|
54
|
+
* The inverse of `taskId`. Splits on the FIRST `#`, so a task name that itself
|
|
55
|
+
* contains one round-trips: `taskId('a', 'b#c')` → `'a#b#c'` → `['a', 'b#c']`.
|
|
56
|
+
*
|
|
57
|
+
* This exists because it kept being written by hand as `id.split('#', 2)`,
|
|
58
|
+
* which is NOT the inverse — it discards everything after the second segment,
|
|
59
|
+
* so `'a#b#c'` reads back as task `'b'`. Six call sites had that form while
|
|
60
|
+
* `parseDependencySpec` (the surface that decides what actually runs) has
|
|
61
|
+
* always split on the first `#`, so the query layer and the graph disagreed
|
|
62
|
+
* about the identity of the same task: a lookup either found nothing or, worse,
|
|
63
|
+
* answered with a different task's history.
|
|
64
|
+
*
|
|
65
|
+
* A `#`-containing task name is legal and pinned elsewhere in the suite, so
|
|
66
|
+
* this is reachable rather than theoretical.
|
|
67
|
+
*/
|
|
68
|
+
export function splitTaskId(id: string): [project: string, task: string] {
|
|
69
|
+
const i = id.indexOf('#')
|
|
70
|
+
return i < 0 ? [id, ''] : [id.slice(0, i), id.slice(i + 1)]
|
|
71
|
+
}
|
|
72
|
+
|
|
47
73
|
/**
|
|
48
74
|
* A task is a "group" if it has no `exec` — it exists only to chain
|
|
49
75
|
* `dependsOn` (an umbrella for `vx run ci`). Group tasks:
|
|
@@ -134,16 +160,54 @@ export function expandRequested(
|
|
|
134
160
|
if (idx >= 0) {
|
|
135
161
|
const project = spec.slice(0, idx)
|
|
136
162
|
const task = spec.slice(idx + 1)
|
|
137
|
-
if (projects
|
|
163
|
+
if (declaresTask(projects, project, task)) push(project, task)
|
|
138
164
|
continue
|
|
139
165
|
}
|
|
140
166
|
for (const name of candidates) {
|
|
141
|
-
if (projects
|
|
167
|
+
if (declaresTask(projects, name, spec)) push(name, spec)
|
|
142
168
|
}
|
|
143
169
|
}
|
|
144
170
|
return out
|
|
145
171
|
}
|
|
146
172
|
|
|
173
|
+
/** Shared by `expandRequested` + `unresolvedRequests` so the two can't drift. */
|
|
174
|
+
function declaresTask(projects: Map<string, ProjectEntry>, project: string, task: string): boolean {
|
|
175
|
+
return projects.get(project)?.config.tasks?.[task] !== undefined
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* The requested specs `expandRequested` silently dropped — each one
|
|
180
|
+
* matched NO project, so nothing it asked for will run.
|
|
181
|
+
*
|
|
182
|
+
* A bare name matching only SOME projects is normal (sparse tasks across
|
|
183
|
+
* a workspace) and never reported. An empty `candidates` scope is also
|
|
184
|
+
* never reported: that is the legitimate "nothing selected" outcome
|
|
185
|
+
* (`--affected` with nothing changed), not a typo — the CLI's selection
|
|
186
|
+
* layer owns that message.
|
|
187
|
+
*
|
|
188
|
+
* Deduped so `vx run x x` names `x` once.
|
|
189
|
+
*/
|
|
190
|
+
export function unresolvedRequests(
|
|
191
|
+
tasks: readonly string[],
|
|
192
|
+
candidates: readonly string[],
|
|
193
|
+
projects: Map<string, ProjectEntry>,
|
|
194
|
+
): string[] {
|
|
195
|
+
const out: string[] = []
|
|
196
|
+
const seen = new Set<string>()
|
|
197
|
+
for (const spec of tasks) {
|
|
198
|
+
if (seen.has(spec)) continue
|
|
199
|
+
const idx = spec.indexOf('#')
|
|
200
|
+
const resolved =
|
|
201
|
+
idx >= 0
|
|
202
|
+
? declaresTask(projects, spec.slice(0, idx), spec.slice(idx + 1))
|
|
203
|
+
: candidates.length === 0 || candidates.some((name) => declaresTask(projects, name, spec))
|
|
204
|
+
if (resolved) continue
|
|
205
|
+
seen.add(spec)
|
|
206
|
+
out.push(spec)
|
|
207
|
+
}
|
|
208
|
+
return out
|
|
209
|
+
}
|
|
210
|
+
|
|
147
211
|
export interface BuildGraphOptions {
|
|
148
212
|
projects: Map<string, ProjectEntry>
|
|
149
213
|
packageGraph: PackageGraph
|
|
@@ -225,7 +289,7 @@ export function buildTaskGraph(options: BuildGraphOptions): Map<string, TaskNode
|
|
|
225
289
|
`Task ${id}: dependsOn patterns are not supported in the "pkg#task" form (got "${raw}")`,
|
|
226
290
|
)
|
|
227
291
|
}
|
|
228
|
-
// CLI `--
|
|
292
|
+
// CLI `--exclude-dependencies=name1,name2` drops edges whose target
|
|
229
293
|
// task name matches, regardless of bucket (self / deps / cross).
|
|
230
294
|
// Pattern specs re-apply the filter per EXPANDED name below.
|
|
231
295
|
if (skipNames?.has(spec.task)) continue
|
|
@@ -267,9 +331,15 @@ export function buildTaskGraph(options: BuildGraphOptions): Map<string, TaskNode
|
|
|
267
331
|
// With a pattern (`^build.*`), a holder is a package declaring
|
|
268
332
|
// AT LEAST ONE matching task and it receives edges to ALL its
|
|
269
333
|
// matches — holder-ness is about declaration, so a holder still
|
|
270
|
-
// stops the walk even when every match is --
|
|
334
|
+
// stops the walk even when every match is --exclude-dependencies'd.
|
|
335
|
+
//
|
|
336
|
+
// The declaring project seeds `visited`: package graphs may legally
|
|
337
|
+
// contain cycles (the common "b devDepends on a for its tests"
|
|
338
|
+
// shape), and a cycle walks the frontier straight back to the
|
|
339
|
+
// origin. Mirrors the self-pattern rule below — a task can never
|
|
340
|
+
// depend on itself.
|
|
271
341
|
const re = isTaskPattern(spec.task) ? compileTaskPattern(spec.task) : null
|
|
272
|
-
const visited = new Set<string>()
|
|
342
|
+
const visited = new Set<string>([projectName])
|
|
273
343
|
const frontier = [...packageGraph.directDeps(projectName)]
|
|
274
344
|
while (frontier.length > 0) {
|
|
275
345
|
const target = frontier.pop()!
|
|
@@ -320,10 +390,154 @@ export function buildTaskGraph(options: BuildGraphOptions): Map<string, TaskNode
|
|
|
320
390
|
}
|
|
321
391
|
|
|
322
392
|
detectCycle(nodes)
|
|
393
|
+
detectOutputCollisions(nodes)
|
|
323
394
|
return nodes
|
|
324
395
|
}
|
|
325
396
|
|
|
326
|
-
|
|
397
|
+
/** A glob with no wildcard — it names exactly one path. */
|
|
398
|
+
function isLiteralGlob(g: string): boolean {
|
|
399
|
+
return g.search(/[*?[\]]/) === -1
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* True only when two output globs PROVABLY select an overlapping set.
|
|
404
|
+
*
|
|
405
|
+
* Deliberately conservative, because the caller REFUSES the run: a false
|
|
406
|
+
* positive breaks a build that works today, which is worse than the defect
|
|
407
|
+
* being caught. So the three cases are exactly the ones that can be decided
|
|
408
|
+
* without a general glob-intersection algorithm:
|
|
409
|
+
*
|
|
410
|
+
* both literal — equal paths
|
|
411
|
+
* literal vs glob — ask the glob whether it matches the literal (exact)
|
|
412
|
+
* both globs — only identical strings; anything else is undecided
|
|
413
|
+
* here and deliberately allowed through
|
|
414
|
+
*
|
|
415
|
+
* The rejected alternative was comparing each glob's static prefix. It is
|
|
416
|
+
* cheaper and catches more, but it is UNSOUND for a refusal — measured:
|
|
417
|
+
* `dist/vx-*` and `dist/other.txt` share the prefix `dist` while matching
|
|
418
|
+
* disjoint sets, so a prefix check refuses a legitimate config. (vx's own
|
|
419
|
+
* `build.bun.*` tasks escape only because they declare distinct literals.)
|
|
420
|
+
*/
|
|
421
|
+
function outputsOverlap(a: string, b: string): boolean {
|
|
422
|
+
if (isLiteralGlob(a) && isLiteralGlob(b)) return a === b
|
|
423
|
+
if (isLiteralGlob(a)) return new Bun.Glob(b).match(a)
|
|
424
|
+
if (isLiteralGlob(b)) return new Bun.Glob(a).match(b)
|
|
425
|
+
return a === b
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Refuse a graph in which two tasks declare overlapping outputs.
|
|
430
|
+
*
|
|
431
|
+
* vx cleans a task's declared outputs before it runs AND before a cache-hit
|
|
432
|
+
* restore, so that the tree ends byte-identical to the cached artifact. That
|
|
433
|
+
* makes output ownership STRICT — and two tasks claiming the same path
|
|
434
|
+
* silently destroy each other's work, in whichever order they happen to run,
|
|
435
|
+
* while the run reports success. It is data loss with a green summary.
|
|
436
|
+
*
|
|
437
|
+
* This is a hazard vx CREATED: Turbo restores additively and cannot hit it,
|
|
438
|
+
* which is why no Turbo test surfaces it and why the parity research had to
|
|
439
|
+
* reproduce it end to end.
|
|
440
|
+
*
|
|
441
|
+
* Scope follows the two namespaces' reach. `outputs.files` is
|
|
442
|
+
* project-relative, so only tasks in the SAME project can collide;
|
|
443
|
+
* `outputs.workspaceFiles` is anchored at the workspace root and ignores
|
|
444
|
+
* project boundaries by design, so ANY two tasks can. No cache key changes —
|
|
445
|
+
* this only refuses a graph that was already destroying files.
|
|
446
|
+
*/
|
|
447
|
+
function detectOutputCollisions(nodes: Map<string, TaskNode>): void {
|
|
448
|
+
// INDEX FIRST, then compare — never all-pairs over the graph. A naive
|
|
449
|
+
// pairwise loop that filters by project INSIDE the loop is quadratic in the
|
|
450
|
+
// whole graph: measured 1.6 SECONDS at this project's stated target of 1000
|
|
451
|
+
// projects x 10 tasks, on every run, against a ~120ms warm run. (Same shape
|
|
452
|
+
// as the scheduler's priority closure, which took 8.5s on a 1090-package
|
|
453
|
+
// repo before it was rewritten.)
|
|
454
|
+
//
|
|
455
|
+
// Both namespaces have a much smaller natural domain:
|
|
456
|
+
// files — project-relative, so only same-project tasks can
|
|
457
|
+
// collide. Bucketing makes this O(sum of k^2) over
|
|
458
|
+
// per-project task counts, and k is single digits.
|
|
459
|
+
// workspaceFiles — root-anchored and boundary-free, so any two tasks can
|
|
460
|
+
// collide — but only tasks that DECLARE it participate,
|
|
461
|
+
// and that set is nearly always empty.
|
|
462
|
+
const byProject = new Map<string, TaskNode[]>()
|
|
463
|
+
const wsDeclarers: TaskNode[] = []
|
|
464
|
+
for (const n of nodes.values()) {
|
|
465
|
+
const outs = n.config.cache?.outputs
|
|
466
|
+
if ((outs?.files?.length ?? 0) > 0) {
|
|
467
|
+
const bucket = byProject.get(n.projectName)
|
|
468
|
+
if (bucket) bucket.push(n)
|
|
469
|
+
else byProject.set(n.projectName, [n])
|
|
470
|
+
}
|
|
471
|
+
if ((outs?.workspaceFiles?.length ?? 0) > 0) wsDeclarers.push(n)
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
for (const bucket of byProject.values()) {
|
|
475
|
+
for (let i = 0; i < bucket.length; i++) {
|
|
476
|
+
for (let j = i + 1; j < bucket.length; j++) {
|
|
477
|
+
const a = bucket[i]!
|
|
478
|
+
const b = bucket[j]!
|
|
479
|
+
collide(a, b, a.config.cache?.outputs.files, b.config.cache?.outputs.files, 'files')
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
for (let i = 0; i < wsDeclarers.length; i++) {
|
|
484
|
+
for (let j = i + 1; j < wsDeclarers.length; j++) {
|
|
485
|
+
const a = wsDeclarers[i]!
|
|
486
|
+
const b = wsDeclarers[j]!
|
|
487
|
+
collide(
|
|
488
|
+
a,
|
|
489
|
+
b,
|
|
490
|
+
a.config.cache?.outputs.workspaceFiles,
|
|
491
|
+
b.config.cache?.outputs.workspaceFiles,
|
|
492
|
+
'workspaceFiles',
|
|
493
|
+
)
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* A `remote: 'only'` task never materialises its outputs on THIS machine —
|
|
500
|
+
* execute-task turns off both the read and the write axis for it, so there is
|
|
501
|
+
* no probe, no restore, no output clean and no local save. The collision
|
|
502
|
+
* hazard is entirely about local cleaning ("whichever runs second DELETES the
|
|
503
|
+
* other's output"), so a pair including one of these cannot exhibit it: there
|
|
504
|
+
* is nothing of its on disk to delete, and it deletes nothing of anyone's.
|
|
505
|
+
*
|
|
506
|
+
* Found while trying to give two projects their own install-as-an-action:
|
|
507
|
+
* both legitimately capture the workspace-root `node_modules` under a
|
|
508
|
+
* hoisting package manager, and the refusal fired on a pair that cannot
|
|
509
|
+
* exhibit the hazard. That particular layout was abandoned for an unrelated
|
|
510
|
+
* reason (identical definitions still get different cache keys), so this is
|
|
511
|
+
* not load-bearing today — it is a refusal narrowed to what it can prove,
|
|
512
|
+
* which this file's header asks for explicitly.
|
|
513
|
+
*/
|
|
514
|
+
function neverWritesLocally(n: TaskNode): boolean {
|
|
515
|
+
return n.config.exec?.remote === 'only'
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function collide(
|
|
519
|
+
a: TaskNode,
|
|
520
|
+
b: TaskNode,
|
|
521
|
+
aGlobs: readonly string[] | undefined,
|
|
522
|
+
bGlobs: readonly string[] | undefined,
|
|
523
|
+
field: 'files' | 'workspaceFiles',
|
|
524
|
+
): void {
|
|
525
|
+
if (neverWritesLocally(a) || neverWritesLocally(b)) return
|
|
526
|
+
for (const ga of aGlobs ?? []) {
|
|
527
|
+
for (const gb of bGlobs ?? []) {
|
|
528
|
+
if (!outputsOverlap(ga, gb)) continue
|
|
529
|
+
throw new UserError(
|
|
530
|
+
`${a.id} and ${b.id} both declare the output ${JSON.stringify(ga)}` +
|
|
531
|
+
(ga === gb ? '' : ` / ${JSON.stringify(gb)}`) +
|
|
532
|
+
` in cache.outputs.${field} — vx cleans a task's declared outputs before it runs and ` +
|
|
533
|
+
`before a cache-hit restore, so whichever of these runs second DELETES the other's ` +
|
|
534
|
+
`output and the run still reports success. Give each task its own output path.`,
|
|
535
|
+
)
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
export function detectCycle(nodes: Map<string, TaskNode>): void {
|
|
327
541
|
// Iterative DFS over dense-indexed colors. Recursion + `Map<string,
|
|
328
542
|
// number>` worked, but deep workspaces (long chains of `dependsOn`)
|
|
329
543
|
// can blow V8's frame budget, and per-node Map lookups dominate the
|
package/src/index.ts
CHANGED
|
@@ -3,13 +3,33 @@
|
|
|
3
3
|
// This is the stable cross-package contract — any plugin or integration
|
|
4
4
|
// package imports everything it needs from here via the bare
|
|
5
5
|
// `'@vzn/vx'` specifier (never a deep `src/...` path). The surface is pinned
|
|
6
|
-
// by tests/package-boundaries.test.ts; a widening updates that snapshot
|
|
7
|
-
// deliberately.
|
|
6
|
+
// by tests/package-boundaries.unsafe.test.ts; a widening updates that snapshot
|
|
7
|
+
// deliberately. The boundary law: docs/architecture.md § Repository shape.
|
|
8
8
|
|
|
9
9
|
export { VERSION } from './version.js'
|
|
10
10
|
|
|
11
11
|
// Clean error type — user-input failures print a message without a stack.
|
|
12
|
-
|
|
12
|
+
// `clampInt` rides along for the same reason the status predicates do:
|
|
13
|
+
// without it on the façade an integration package writes its own, and that
|
|
14
|
+
// had already happened. A bounds helper whose floor is load-bearing (a
|
|
15
|
+
// fractional SQL LIMIT is a datatype mismatch, not a smaller page) should
|
|
16
|
+
// have one implementation.
|
|
17
|
+
export { clampInt, UserError, isUserError } from './util/index.js'
|
|
18
|
+
// "Did you mean": the hint core's own verbs give for a near-miss name, for a
|
|
19
|
+
// plugin verb to give the same one.
|
|
20
|
+
export { nearMatches } from './util/index.js'
|
|
21
|
+
// Every hook a plugin may fill, in pipeline order — the one list the
|
|
22
|
+
// loader, the host and `vx info` read; exported so a doc pin outside core
|
|
23
|
+
// (the site's guides) holds its tables to the same list.
|
|
24
|
+
export { PLUGIN_HOOKS, type PluginHook } from './config.js'
|
|
25
|
+
// The workspace doctor's facts (`vx info`), for a host that answers the
|
|
26
|
+
// same question over another surface — `@vzn/vx-mcp`'s `getWorkspaceInfo`.
|
|
27
|
+
export { collectInfo, type CollectInfoOptions, type InfoFacts } from './orchestrator/index.js'
|
|
28
|
+
// The machine as this process may use it — cores and memory capped by the
|
|
29
|
+
// cgroup a container runs under. Core's default worker count reads the
|
|
30
|
+
// first; a policy that budgets memory (`@vzn/vx-schedule-history`) reads
|
|
31
|
+
// the second, so the cgroup walk has one implementation.
|
|
32
|
+
export { machineMemoryBytes, machineParallelism } from './util/index.js'
|
|
13
33
|
|
|
14
34
|
// Schema types and helpers (used by user vx.config files and presets).
|
|
15
35
|
export type {
|
|
@@ -21,29 +41,26 @@ export type {
|
|
|
21
41
|
CacheConfig,
|
|
22
42
|
CacheInputs,
|
|
23
43
|
CacheOutputs,
|
|
24
|
-
ResourcesConfig,
|
|
25
44
|
SandboxConfig,
|
|
26
|
-
|
|
45
|
+
SandboxGrants,
|
|
46
|
+
SandboxDenials,
|
|
27
47
|
} from './config.js'
|
|
28
48
|
export { defineProject, defineWorkspace } from './config.js'
|
|
49
|
+
export { definePlugin } from './orchestrator/index.js'
|
|
29
50
|
|
|
30
|
-
// Programmatic engine API
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
// the
|
|
34
|
-
//
|
|
35
|
-
// give agents + the submitter identity before/without a telemetry run.
|
|
51
|
+
// Programmatic engine API: run / plan / prepare (docs/cli.md § Programmatic
|
|
52
|
+
// API). The graph primitives, the cache-key hashing seam and the git / host
|
|
53
|
+
// context capture used to sit beside these for a distributed submitter that
|
|
54
|
+
// left the repo (2026-08); they went with it (2026-09-10) — a seam with no
|
|
55
|
+
// consumer is re-added when one appears, shaped by its use.
|
|
36
56
|
export { run, planRun, prepareRun } from './orchestrator/index.js'
|
|
57
|
+
// The per-task duration history a `schedule` plugin learns from
|
|
58
|
+
// (`@vzn/vx-schedule-history` does; a plugin package reaches core only
|
|
59
|
+
// through this façade, which is what put these here).
|
|
60
|
+
export { LocalHistoryProvider } from './orchestrator/index.js'
|
|
61
|
+
export type { HistoryProvider, HistoryTable, TaskHistory } from './orchestrator/index.js'
|
|
37
62
|
export type { PreparedRun } from './orchestrator/index.js'
|
|
38
|
-
export {
|
|
39
|
-
export type { DeriveStableKeysArgs, HashCache, StableKey } from './orchestrator/index.js'
|
|
40
|
-
export {
|
|
41
|
-
captureGitContext,
|
|
42
|
-
captureWorkspaceIdentity,
|
|
43
|
-
resolveCacheScope,
|
|
44
|
-
} from './orchestrator/index.js'
|
|
45
|
-
export type { GitContext, WorkspaceIdentity } from './orchestrator/index.js'
|
|
46
|
-
export { FULL_CACHE_POLICY, parseCachePolicy } from './orchestrator/index.js'
|
|
63
|
+
export type { CiContext, GitContext, HostContext, WorkspaceIdentity } from './orchestrator/index.js'
|
|
47
64
|
export type {
|
|
48
65
|
CachePolicy,
|
|
49
66
|
Logger,
|
|
@@ -51,172 +68,148 @@ export type {
|
|
|
51
68
|
RunOptions,
|
|
52
69
|
RunSummary,
|
|
53
70
|
} from './orchestrator/index.js'
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
TaskStatus,
|
|
61
|
-
VerifyVerdict,
|
|
62
|
-
} from './graph/index.js'
|
|
63
|
-
// The one output-tree diff implementation (verify verdicts + a serve's
|
|
64
|
-
// cross-machine fingerprint diff both name rels through it).
|
|
65
|
-
export { diffOutputTrees } from './orchestrator/index.js'
|
|
71
|
+
// `splitTaskId` is on the façade because the alternative is what happened:
|
|
72
|
+
// with only `taskId()` to JOIN an id and nothing exported to SPLIT one,
|
|
73
|
+
// consumers roll their own `split('#', 2)` and drift from the graph, which
|
|
74
|
+
// splits on the FIRST '#'.
|
|
75
|
+
export { splitTaskId } from './graph/index.js'
|
|
76
|
+
export type { TaskNode, TaskOutcome, TaskStatus } from './graph/index.js'
|
|
66
77
|
|
|
67
|
-
// Cache classes + the layer interface (the `cache` capability's currency)
|
|
68
|
-
|
|
69
|
-
// (cas-backend.ts / digest.ts) stays module-internal until it has a
|
|
70
|
-
// consumer — no speculative public API. `cleanOutputs` is public for the
|
|
71
|
-
// distributed submitter's targeted output materialization (wipe declared
|
|
72
|
-
// outputs, then `restoreOutputs` the artifact — never a naive re-run).
|
|
73
|
-
export {
|
|
74
|
-
Cache,
|
|
75
|
-
LayeredCache,
|
|
76
|
-
GitFilesCache,
|
|
77
|
-
cleanOutputs,
|
|
78
|
-
resolveInputs,
|
|
79
|
-
resolveOutputs,
|
|
80
|
-
} from './cache/index.js'
|
|
78
|
+
// Cache classes + the layer interface (the `cache` capability's currency).
|
|
79
|
+
export { Cache, LayeredCache } from './cache/index.js'
|
|
81
80
|
export type { CacheLayer, RemoteCacheLayer, RunRecord, InvocationRecord } from './cache/index.js'
|
|
82
81
|
|
|
83
82
|
// Workspace discovery + the project/config catalog surface — an
|
|
84
|
-
// out-of-process service/CLI needs these. `
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
export { findWorkspaceRoot, loadWorkspaceConfig, resolveCacheDir } from './workspace/index.js'
|
|
91
|
-
export { readLockfile, LOCKFILE_NAME } from './workspace/index.js'
|
|
92
|
-
export type { Lockfile, LockfileEntry } from './workspace/index.js'
|
|
83
|
+
// out-of-process service/CLI needs these. `loadProjectConfig` is the RAW
|
|
84
|
+
// per-file load (`vx lock` freezes exactly that); the resolved view a run
|
|
85
|
+
// or `vx show` sees — plugin stages applied — is `loadResolvedProjects`
|
|
86
|
+
// below. Workspace's `listProjects` re-exports as `listProjectMetas` (the
|
|
87
|
+
// bare name once belonged to a metrics query).
|
|
88
|
+
export { findWorkspaceRoot } from './workspace/index.js'
|
|
93
89
|
export {
|
|
94
90
|
loadWorkspace,
|
|
95
91
|
loadProjectConfig,
|
|
96
92
|
listProjects as listProjectMetas,
|
|
97
93
|
} from './workspace/index.js'
|
|
98
94
|
export type { ProjectMeta } from './workspace/index.js'
|
|
95
|
+
// The package graph as a run sees it (workspace deps by manifest), for a tool
|
|
96
|
+
// that needs a project's transitive closure the way `vx run` computes it.
|
|
97
|
+
export { buildPackageGraph } from './workspace/index.js'
|
|
98
|
+
// The run path's RESOLVED view — plugin `config` and `project` stages
|
|
99
|
+
// applied, cached evaluations served — for a reader outside the CLI (the
|
|
100
|
+
// MCP server's `listTasks`, an embedder's task catalog). What `vx show`
|
|
101
|
+
// prints.
|
|
102
|
+
export { loadResolvedProjects } from './orchestrator/index.js'
|
|
103
|
+
export type { ProjectEntry } from './workspace/index.js'
|
|
104
|
+
// The migration seam: how a generated config is planned, rendered, guarded
|
|
105
|
+
// and written. `vx init` (package.json scripts) uses it in core;
|
|
106
|
+
// `@vzn/vx-migrate` (Turbo, Nx) and any other adoption tool use it from here.
|
|
107
|
+
export {
|
|
108
|
+
applyMigration,
|
|
109
|
+
PERSISTENT_TASK_NAMES,
|
|
110
|
+
PERSISTENT_TODO,
|
|
111
|
+
quoteTsLiteral,
|
|
112
|
+
} from './workspace/index.js'
|
|
113
|
+
export type {
|
|
114
|
+
ApplyMigrationArgs,
|
|
115
|
+
MigrationFormat,
|
|
116
|
+
GeneratedProject,
|
|
117
|
+
GeneratedTask,
|
|
118
|
+
MigrationPlan,
|
|
119
|
+
RawExpr,
|
|
120
|
+
} from './workspace/index.js'
|
|
99
121
|
|
|
100
122
|
// Plugin API — the run-level extension points. Behavior capabilities
|
|
101
|
-
// (
|
|
123
|
+
// (executor / cache) change WHAT/HOW work runs; the observe-only `telemetry`
|
|
102
124
|
// capability is the canonical data-export path and cannot change behavior.
|
|
103
125
|
// A plugin is declared in vx.workspace.ts via defineWorkspace({ plugins }).
|
|
104
126
|
// See docs/design/observability-architecture-2026-06.md.
|
|
127
|
+
// The claimant's shell for a lockfile plugin (`@vzn/vx-lockfile`):
|
|
128
|
+
// the claim, the per-project key, the memo and the `--affected` diff around
|
|
129
|
+
// a parser; `reachDigests` is the Merkle-over-components digest both use.
|
|
130
|
+
export {
|
|
131
|
+
lockfileClaim,
|
|
132
|
+
reachDigests,
|
|
133
|
+
type LockfileClaimHooks,
|
|
134
|
+
type LockfileClaimOptions,
|
|
135
|
+
type ReachGraph,
|
|
136
|
+
} from './orchestrator/index.js'
|
|
105
137
|
export type {
|
|
106
138
|
VxPlugin,
|
|
107
|
-
|
|
108
|
-
|
|
139
|
+
PluginHooks,
|
|
140
|
+
PluginOrigin,
|
|
109
141
|
CacheContext,
|
|
110
|
-
|
|
142
|
+
ExecutorContext,
|
|
143
|
+
CommandContext,
|
|
144
|
+
FingerprintChange,
|
|
145
|
+
FingerprintClaim,
|
|
146
|
+
FingerprintContext,
|
|
147
|
+
GraphHookContext,
|
|
148
|
+
KeyHookContext,
|
|
149
|
+
PluginCommand,
|
|
150
|
+
ProjectHookContext,
|
|
151
|
+
ScheduleHookContext,
|
|
152
|
+
AdmitContext,
|
|
153
|
+
WorkspaceHookContext,
|
|
111
154
|
PluginSetupContext,
|
|
112
155
|
} from './orchestrator/index.js'
|
|
156
|
+
// The per-task execution contract a plugin's `executor` capability returns.
|
|
157
|
+
// (`runCommand` / `runSandboxed`, the local executor's own primitives, left
|
|
158
|
+
// the façade 2026-09-10: no executor plugin built on them — `@vzn/vx-reapi`
|
|
159
|
+
// speaks a wire — and a seam with no consumer is a special case in waiting.)
|
|
160
|
+
export type {
|
|
161
|
+
ExecuteRequest,
|
|
162
|
+
ExecuteResult,
|
|
163
|
+
ExecuteSandbox,
|
|
164
|
+
ResolvedSandboxConfig,
|
|
165
|
+
TaskExecutor,
|
|
166
|
+
TaskPlacement,
|
|
167
|
+
} from './exec/index.js'
|
|
113
168
|
|
|
114
169
|
// Telemetry — THE canonical, versioned data-export contract every exporter
|
|
115
170
|
// (an OTel exporter, an HTTP sink, or any third-party consumer) reads. A sink implements TelemetrySink and is
|
|
116
171
|
// returned from VxPlugin.telemetry(); it receives immutable records and holds
|
|
117
172
|
// no run handle (observe-only by construction).
|
|
118
|
-
|
|
173
|
+
// `isPassStatus` / `isCacheHit` are on the façade for the reason the sweep
|
|
174
|
+
// that added them found: with only the raw `TaskStatus` union exported, every
|
|
175
|
+
// consumer rolls its own Set of status literals — and a Set has no
|
|
176
|
+
// compile-time tripwire when the union gains a member, so it silently answers
|
|
177
|
+
// "no" for the new one. `TASK_STATUSES` is the union at runtime, for a
|
|
178
|
+
// consumer that needs the list rather than the predicate.
|
|
179
|
+
// `escapeMarkdownCell` is on the façade for the same demonstrated need: a
|
|
180
|
+
// plugin that renders a run as a markdown table takes the same unvalidated
|
|
181
|
+
// task names core does, and the cloud job summary shipped without the escape.
|
|
182
|
+
export {
|
|
183
|
+
escapeMarkdownCell,
|
|
184
|
+
TELEMETRY_SCHEMA_VERSION,
|
|
185
|
+
deriveCacheSource,
|
|
186
|
+
isCacheHit,
|
|
187
|
+
isPassStatus,
|
|
188
|
+
TASK_STATUSES,
|
|
189
|
+
} from './orchestrator/index.js'
|
|
190
|
+
// `TaskLogBuffer` is on the façade on the same demonstrated need: EVERY
|
|
191
|
+
// telemetry sink that ships build output has to bound it, and the retention
|
|
192
|
+
// rules (per-task tail, per-run budget, failures never evicted by successes,
|
|
193
|
+
// a hit's bytes belong to the run that executed) are a decision, not an
|
|
194
|
+
// implementation detail. Two sinks rolling their own is how they fork.
|
|
195
|
+
export { LOG_WIRE_VERSION, TaskLogBuffer } from './orchestrator/index.js'
|
|
119
196
|
export type {
|
|
120
197
|
CacheSource,
|
|
121
198
|
RunContextRecord,
|
|
122
199
|
RunSummaryRecord,
|
|
200
|
+
TaskLogBundle,
|
|
201
|
+
TaskLogEntry,
|
|
123
202
|
TaskTelemetry,
|
|
124
203
|
TelemetryContext,
|
|
125
204
|
TelemetryRecord,
|
|
126
205
|
TelemetrySink,
|
|
127
206
|
} from './orchestrator/index.js'
|
|
128
207
|
|
|
129
|
-
// The
|
|
130
|
-
|
|
131
|
-
export {
|
|
132
|
-
optionsToRequest,
|
|
133
|
-
requestToOptions,
|
|
134
|
-
projectNode,
|
|
135
|
-
projectOutcome,
|
|
136
|
-
createWireRenderer,
|
|
137
|
-
} from './orchestrator/index.js'
|
|
138
|
-
export type {
|
|
139
|
-
RunBackend,
|
|
140
|
-
RunRequest,
|
|
141
|
-
RunResult,
|
|
142
|
-
ClientMessage,
|
|
143
|
-
ServerMessage,
|
|
144
|
-
} from './orchestrator/index.js'
|
|
145
|
-
|
|
146
|
-
// Event bus + wire form — adapters (otel-bridge, custom subscribers) ride this.
|
|
147
|
-
export {
|
|
148
|
-
createEventBus,
|
|
149
|
-
wireForwarder,
|
|
150
|
-
toWireEvent,
|
|
151
|
-
createVxSurface,
|
|
152
|
-
} from './orchestrator/index.js'
|
|
153
|
-
export type {
|
|
154
|
-
EventBus,
|
|
155
|
-
RunEvent,
|
|
156
|
-
RunEventSubscriber,
|
|
157
|
-
WireEvent,
|
|
158
|
-
TaskView,
|
|
159
|
-
OutcomeView,
|
|
160
|
-
} from './orchestrator/index.js'
|
|
208
|
+
// The serializable run result and its per-task views (what `run()` returns).
|
|
209
|
+
export type { RunResult, TaskView, OutcomeView } from './orchestrator/index.js'
|
|
161
210
|
|
|
162
|
-
// The
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
encodeForSSE,
|
|
168
|
-
encodeForWS,
|
|
169
|
-
ENVELOPE_ERRORS,
|
|
170
|
-
envelopeToClientMessage,
|
|
171
|
-
envelopeToServerMessage,
|
|
172
|
-
isEnvelope,
|
|
173
|
-
isNotification,
|
|
174
|
-
isRequest,
|
|
175
|
-
makeError,
|
|
176
|
-
makeNotification,
|
|
177
|
-
makeRequest,
|
|
178
|
-
makeResponse,
|
|
179
|
-
serverMessageToEnvelope,
|
|
180
|
-
WIRE_CHANNELS,
|
|
181
|
-
WIRE_PROTOCOL_VERSION,
|
|
182
|
-
} from './orchestrator/index.js'
|
|
183
|
-
export type {
|
|
184
|
-
Envelope,
|
|
185
|
-
ErrorResponse,
|
|
186
|
-
Notification,
|
|
187
|
-
WireRequest,
|
|
188
|
-
WireResponse,
|
|
189
|
-
WireChannel,
|
|
190
|
-
} from './orchestrator/index.js'
|
|
191
|
-
|
|
192
|
-
// Metrics / analytics query layer over cache.db — a service plugin's HTTP
|
|
193
|
-
// surface reads from these (the queries stay in core; a serve exposes them over HTTP).
|
|
194
|
-
export {
|
|
195
|
-
cacheKeyDiff,
|
|
196
|
-
compareRuns,
|
|
197
|
-
explainCacheKeyQuery,
|
|
198
|
-
getBottlenecks,
|
|
199
|
-
getCacheBreakdown,
|
|
200
|
-
getCacheSavings,
|
|
201
|
-
getCacheStatsSql,
|
|
202
|
-
getFlakiestTasks,
|
|
203
|
-
getHistory,
|
|
204
|
-
getHitRateSplit,
|
|
205
|
-
getInvocation,
|
|
206
|
-
getParallelismHistory,
|
|
207
|
-
getPeriodComparison,
|
|
208
|
-
getPrunableEntries,
|
|
209
|
-
getRecentFailures,
|
|
210
|
-
getRegressions,
|
|
211
|
-
getRun,
|
|
212
|
-
getRunHeatmap,
|
|
213
|
-
getRunTrends,
|
|
214
|
-
getStorageGrowth,
|
|
215
|
-
getTaskDetail,
|
|
216
|
-
getTopTimeBurners,
|
|
217
|
-
listCacheEntries,
|
|
218
|
-
listInvocations,
|
|
219
|
-
listProjects,
|
|
220
|
-
listRuns,
|
|
221
|
-
whyDidThisRerunQuery,
|
|
222
|
-
} from './orchestrator/index.js'
|
|
211
|
+
// The one run-history query a reader outside the CLI takes today: `vx why`'s
|
|
212
|
+
// answer, which `@vzn/vx-mcp` serves. The event bus / wire form and the
|
|
213
|
+
// other history readers (`listRuns`, `getRun`, …) left the façade 2026-09-10
|
|
214
|
+
// with no consumer; the telemetry seam above is the canonical export path.
|
|
215
|
+
export { whyDidThisRerunQuery } from './orchestrator/index.js'
|