@vzn/vx 0.0.18 → 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.
Files changed (121) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +99 -65
  3. package/package.json +10 -20
  4. package/src/bin.ts +12 -2
  5. package/src/cache/archive.ts +120 -18
  6. package/src/cache/cache.ts +313 -1324
  7. package/src/cache/chained-cache.ts +0 -8
  8. package/src/cache/config-evals.ts +89 -0
  9. package/src/cache/file-hashes.ts +262 -0
  10. package/src/cache/git-inputs.ts +637 -0
  11. package/src/cache/index.ts +16 -7
  12. package/src/cache/inputs.ts +246 -733
  13. package/src/cache/layer.ts +604 -0
  14. package/src/cache/layered-cache.ts +12 -17
  15. package/src/cache/output-index.ts +224 -0
  16. package/src/cache/policy.ts +84 -0
  17. package/src/cache/run-history.ts +147 -0
  18. package/src/cache/zstd.ts +160 -0
  19. package/src/cli/cache.ts +39 -11
  20. package/src/cli/completions.ts +104 -0
  21. package/src/cli/core-alias.ts +25 -0
  22. package/src/cli/help.ts +64 -36
  23. package/src/cli/index.ts +54 -15
  24. package/src/cli/info.ts +120 -91
  25. package/src/cli/init.ts +75 -0
  26. package/src/cli/last.ts +38 -6
  27. package/src/cli/lock.ts +2 -1
  28. package/src/cli/plan-format.ts +14 -3
  29. package/src/cli/plugin-commands.ts +15 -8
  30. package/src/cli/run.ts +84 -302
  31. package/src/cli/select.ts +332 -0
  32. package/src/cli/show.ts +144 -69
  33. package/src/cli/upgrade.ts +2 -1
  34. package/src/cli/watch.ts +647 -97
  35. package/src/cli/why.ts +44 -14
  36. package/src/cli/workspace-config.ts +126 -0
  37. package/src/config.ts +156 -136
  38. package/src/exec/env.ts +11 -0
  39. package/src/exec/executor.ts +11 -26
  40. package/src/exec/index.ts +6 -2
  41. package/src/{plugins/local-executor/index.ts → exec/local-executor.ts} +12 -16
  42. package/src/exec/runner.ts +57 -18
  43. package/src/exec/sandbox-binds.ts +182 -0
  44. package/src/exec/sandbox-paths.ts +50 -0
  45. package/src/exec/sandbox-runtime.ts +582 -370
  46. package/src/exec/sandbox-violations.ts +280 -0
  47. package/src/graph/index.ts +1 -9
  48. package/src/graph/scheduler.ts +158 -203
  49. package/src/graph/task-graph.ts +2 -2
  50. package/src/index.ts +97 -102
  51. package/src/orchestrator/admission.ts +137 -0
  52. package/src/orchestrator/deferred-outputs.ts +3 -0
  53. package/src/orchestrator/doctor.ts +255 -0
  54. package/src/orchestrator/events.ts +42 -8
  55. package/src/orchestrator/execute-task.ts +145 -554
  56. package/src/orchestrator/failure-mode.ts +169 -28
  57. package/src/orchestrator/framed-output.ts +41 -36
  58. package/src/orchestrator/history.ts +118 -98
  59. package/src/orchestrator/hit-restore.ts +221 -0
  60. package/src/orchestrator/index.ts +35 -4
  61. package/src/orchestrator/lockfile-claim.ts +281 -0
  62. package/src/orchestrator/logger.ts +13 -22
  63. package/src/orchestrator/metrics.ts +53 -20
  64. package/src/orchestrator/miss-save.ts +169 -0
  65. package/src/orchestrator/options.ts +34 -31
  66. package/src/orchestrator/persistent.ts +79 -0
  67. package/src/orchestrator/placement.ts +182 -0
  68. package/src/orchestrator/plugin-host.ts +195 -38
  69. package/src/orchestrator/plugin.ts +162 -9
  70. package/src/orchestrator/prepare.ts +77 -132
  71. package/src/orchestrator/projects.ts +259 -0
  72. package/src/orchestrator/remote-prefetch.ts +1 -1
  73. package/src/orchestrator/run-artifacts.ts +25 -3
  74. package/src/orchestrator/run-context.ts +1 -1
  75. package/src/orchestrator/run-records.ts +149 -0
  76. package/src/orchestrator/run-report.ts +2 -2
  77. package/src/orchestrator/run.ts +246 -597
  78. package/src/orchestrator/sandbox-request.ts +243 -0
  79. package/src/orchestrator/save-lane.ts +47 -0
  80. package/src/orchestrator/signals.ts +111 -0
  81. package/src/orchestrator/stable-keys.ts +60 -3
  82. package/src/orchestrator/status-line.ts +2 -1
  83. package/src/orchestrator/summary.ts +68 -25
  84. package/src/orchestrator/task-hash.ts +20 -18
  85. package/src/orchestrator/task-log-buffer.ts +8 -11
  86. package/src/orchestrator/telemetry-host.ts +11 -1
  87. package/src/orchestrator/telemetry.ts +3 -17
  88. package/src/util/cgroup.ts +160 -0
  89. package/src/util/edit-distance.ts +49 -0
  90. package/src/util/index.ts +11 -3
  91. package/src/util/paths.ts +23 -2
  92. package/src/util/settle.ts +19 -0
  93. package/src/util/verbs.ts +39 -0
  94. package/src/workspace/affected.ts +111 -11
  95. package/src/workspace/config-cache.ts +11 -2
  96. package/src/workspace/config-eval.ts +11 -4
  97. package/src/workspace/config-imports.ts +5 -4
  98. package/src/workspace/config-schema.ts +837 -0
  99. package/src/workspace/filter.ts +43 -6
  100. package/src/workspace/fingerprint.ts +37 -3
  101. package/src/workspace/index.ts +30 -3
  102. package/src/workspace/lockfile.ts +12 -7
  103. package/src/{cli → workspace}/migrate-scripts.ts +26 -19
  104. package/src/{cli/migrate.ts → workspace/migration.ts} +133 -124
  105. package/src/workspace/package-graph.ts +98 -28
  106. package/src/workspace/project-loader.ts +76 -746
  107. package/src/workspace/workspace.ts +98 -13
  108. package/plugins/local-cache/index.ts +0 -2
  109. package/plugins/local-executor/index.ts +0 -2
  110. package/plugins/schedule-history/index.ts +0 -2
  111. package/src/cache/cas-backend.ts +0 -115
  112. package/src/cache/digest.ts +0 -32
  113. package/src/cli/migrate-emit.ts +0 -21
  114. package/src/cli/migrate-nx.ts +0 -473
  115. package/src/cli/migrate-turbo.ts +0 -401
  116. package/src/cli/prune.ts +0 -291
  117. package/src/orchestrator/missing-plugin.ts +0 -9
  118. package/src/orchestrator/resources.ts +0 -45
  119. package/src/orchestrator/verify.ts +0 -174
  120. package/src/plugins/local-cache/index.ts +0 -11
  121. package/src/plugins/schedule-history/index.ts +0 -109
@@ -1,9 +0,0 @@
1
- /** Shown by every "no <capability> plugin declared" error. */
2
- export const MISSING_PLUGIN_HINT = `vx runs nothing it was not told to. Declare the plugins in vx.workspace.ts:
3
-
4
- import { defineWorkspace } from '@vzn/vx'
5
- import { localExecutorPlugin } from '@vzn/vx/plugins/local-executor'
6
- import { localCachePlugin } from '@vzn/vx/plugins/local-cache'
7
- export default defineWorkspace({ plugins: [localExecutorPlugin(), localCachePlugin()] })
8
-
9
- Put a remote executor or cache plugin BEFORE the local one to prefer it.`
@@ -1,45 +0,0 @@
1
- // Resolve `exec.resources` declarations into the absolute per-task costs the
2
- // scheduler's 2-D admission gate packs against. Pure; the loader has already
3
- // validated every form (this module trusts internal code), so an unparseable
4
- // value can't reach it from a real config.
5
- //
6
- // The declared units are CPU CORES and MEGABYTES — the same numbers on this
7
- // machine and on a remote worker, which is why they are also what
8
- // `TaskPlacement.resources` hands an executor verbatim. Percent forms were
9
- // removed on 2026-08-30: a percentage names a fraction of THIS run's budget,
10
- // and an executor placing the task on someone else's machine has no way to
11
- // mean anything by it.
12
-
13
- import type { ResourceCost, TaskNode } from '../graph/index.js'
14
-
15
- const BYTES_PER_MB = 1024 * 1024
16
-
17
- /** `cpus` → CPU units. Absent → 0 = reserve nothing. */
18
- export function resolveCpu(v: number | undefined): number {
19
- return v ?? 0
20
- }
21
-
22
- /** `memory` (megabytes) → bytes, which is what the budget axis counts. */
23
- export function resolveMem(v: number | undefined): number {
24
- return v === undefined ? 0 : v * BYTES_PER_MB
25
- }
26
-
27
- /**
28
- * Resolve the whole graph's declared reservations, OMITTING zero-cost
29
- * tasks: an empty map means "no reservations declared", the single gate
30
- * the scheduler (and run.ts's option threading) keys off to keep every
31
- * current run byte-identical.
32
- */
33
- export function resolveResourceCosts(
34
- nodes: ReadonlyMap<string, TaskNode>,
35
- ): Map<string, ResourceCost> {
36
- const out = new Map<string, ResourceCost>()
37
- for (const node of nodes.values()) {
38
- const declared = node.config.exec?.resources
39
- if (declared === undefined) continue
40
- const cpu = resolveCpu(declared.cpus)
41
- const mem = resolveMem(declared.memory)
42
- if (cpu > 0 || mem > 0) out.set(node.id, { cpu, mem })
43
- }
44
- return out
45
- }
@@ -1,174 +0,0 @@
1
- // Cache-correctness verification (Phase 1: determinism). Pure helpers for the
2
- // `vx run --verify` hook in execute-task.ts: content-fingerprint a task's
3
- // output tree (mtime-independent) and diff two runs' trees, so a task whose
4
- // outputs are NOT a pure function of its declared inputs — a non-hermetic task
5
- // whose cache entry would replay arbitrary past bytes — is caught and named.
6
- // See docs/design/cache-correctness-2026-07.md.
7
-
8
- import path from 'node:path'
9
- import type { OutputFingerprint, TaskOutcome, VerifyVerdict } from '../graph/index.js'
10
- import { xxh3hex } from '../util/index.js'
11
-
12
- /** An output file to fingerprint: absolute path + a stable, run-independent
13
- * key (project-relative, or `workspace-outputs/<rel-to-root>` for workspace
14
- * outputs — matching the cache artifact's namespace so the two never collide). */
15
- export interface OutputRef {
16
- abs: string
17
- key: string
18
- }
19
-
20
- /** Build the fingerprint key set from a task's resolved output paths. */
21
- export function outputRefs(
22
- projectDir: string,
23
- outputFiles: readonly string[],
24
- workspaceRoot: string,
25
- workspaceOutputFiles: readonly string[],
26
- ): OutputRef[] {
27
- const rel = (from: string, abs: string): string =>
28
- path.relative(from, abs).split(path.sep).join('/')
29
- return [
30
- ...outputFiles.map((abs) => ({ abs, key: rel(projectDir, abs) })),
31
- ...workspaceOutputFiles.map((abs) => ({
32
- abs,
33
- key: `workspace-outputs/${rel(workspaceRoot, abs)}`,
34
- })),
35
- ]
36
- }
37
-
38
- /** Content-fingerprint an output tree: key → xxh3 of the file's BYTES, read
39
- * unconditionally from disk. Deliberately NOT `Cache.hashFile`: its
40
- * mtime+size memo would return attempt 1's digest for a re-run output with
41
- * equal size and mtime (e.g. a build that normalizes mtimes — standard
42
- * reproducible-build practice), silently proving a divergent task
43
- * deterministic. fp1/fp2 are only ever compared to each other, so any
44
- * content hash works — and the proof must not trust a cache. */
45
- export async function hashOutputTree(refs: readonly OutputRef[]): Promise<Map<string, string>> {
46
- const out = new Map<string, string>()
47
- for (const r of refs) {
48
- out.set(r.key, xxh3hex(new Uint8Array(await Bun.file(r.abs).arrayBuffer())))
49
- }
50
- return out
51
- }
52
-
53
- /** Per-file map cap on a shipped fingerprint (~40 KB at the cap). The tree
54
- * digest always folds ALL entries, so detection never depends on the map. */
55
- const FP_MAX_FILES = 500
56
-
57
- /** Roll an output-tree fingerprint map into the shippable `OutputFingerprint`
58
- * payload (Phase 4, cross-machine diff): a tree digest over ALL sorted
59
- * entries folded `key\0hash\n` (\0 boundaries so parts can't alias), plus
60
- * the first-`cap` pairs — deterministic truncation, so two machines'
61
- * truncated maps cover the same key subset and stay diffable. */
62
- export function foldFingerprint(fp: Map<string, string>, cap = FP_MAX_FILES): OutputFingerprint {
63
- const keys = [...fp.keys()].sort()
64
- let folded = ''
65
- for (const k of keys) folded += `${k}\0${fp.get(k)!}\n`
66
- return {
67
- tree: xxh3hex(folded),
68
- fileCount: keys.length,
69
- files: keys.slice(0, cap).map((k) => [k, fp.get(k)!] as const),
70
- ...(keys.length > cap ? { truncated: true } : {}),
71
- }
72
- }
73
-
74
- /** The output keys whose content OID differs between two fingerprints
75
- * (added, removed, or changed), sorted. */
76
- export function diffOutputTrees(a: Map<string, string>, b: Map<string, string>): string[] {
77
- const changed: string[] = []
78
- for (const k of new Set([...a.keys(), ...b.keys()])) {
79
- if (a.get(k) !== b.get(k)) changed.push(k)
80
- }
81
- return changed.sort()
82
- }
83
-
84
- /** Turn a determinism diff into a verdict, honoring the `--verify-allow`
85
- * escape hatch (an allowed divergence is reported but never fails the run). */
86
- export function classifyDeterminism(changed: readonly string[], allowed: boolean): VerifyVerdict {
87
- if (changed.length === 0) return { kind: 'proven-deterministic' }
88
- return allowed
89
- ? { kind: 'allowed-nondeterministic', changed }
90
- : { kind: 'nondeterministic', changed }
91
- }
92
-
93
- /** Extract the workspace-relative paths a task read outside its declared inputs
94
- * (Phase 2, `--verify=inputs`). A strace-derived violation line carries the
95
- * offending absolute path in trailing brackets (`… [/abs/path]`); pull it out,
96
- * make it workspace-relative, dedup, and sort. Lines without a bracketed path
97
- * (e.g. macOS syscall lines) fall back to the raw line so nothing is lost. */
98
- export function undeclaredInputPaths(
99
- violations: readonly { line: string }[],
100
- workspaceRoot: string,
101
- ): string[] {
102
- const rels = new Set<string>()
103
- for (const v of violations) {
104
- // Two wire shapes: Linux strace lines carry `[path]`; macOS
105
- // sandbox-exec lines end in a bare absolute path
106
- // (`node(123) deny(1) file-read-data /abs/path`). Fall back to the
107
- // raw line only when neither parses — a path the user can act on
108
- // beats a log line every time.
109
- const m = /\[([^\]]+)\]\s*$/.exec(v.line) ?? /\s(\/\S+)\s*$/.exec(v.line)
110
- const raw = m ? m[1]! : v.line
111
- rels.add(
112
- path.isAbsolute(raw) ? path.relative(workspaceRoot, raw).split(path.sep).join('/') : raw,
113
- )
114
- }
115
- return [...rels].sort()
116
- }
117
-
118
- /** The end-of-run `--verify` summary section: a counts line plus a
119
- * failures block naming each non-hermetic task and its diverging outputs.
120
- * Empty when no task carried a verdict. */
121
- export function formatVerifySection(outcomes: readonly TaskOutcome[]): string[] {
122
- const verdicts = outcomes.filter((o) => o.verify !== undefined)
123
- if (verdicts.length === 0) return []
124
- let proven = 0
125
- let bad = 0
126
- let na = 0
127
- let notVerified = 0
128
- for (const o of verdicts) {
129
- switch (o.verify!.kind) {
130
- case 'proven-deterministic':
131
- case 'proven-complete':
132
- proven++
133
- break
134
- case 'nondeterministic':
135
- case 'rerun-failed':
136
- case 'undeclared-inputs':
137
- bad++
138
- break
139
- case 'allowed-nondeterministic':
140
- case 'no-outputs':
141
- na++
142
- break
143
- case 'not-verified':
144
- notVerified++
145
- break
146
- case 'unverifiable-remote-only':
147
- na++
148
- break
149
- }
150
- }
151
- const lines = [
152
- '',
153
- ` Verify: ${proven} proven · ${bad} unsafe · ${na} n/a · ${notVerified} not-verified`,
154
- ]
155
- for (const o of outcomes) {
156
- const v = o.verify
157
- if (v === undefined) continue
158
- if (v.kind === 'nondeterministic') {
159
- lines.push(` ✗ ${o.node.id} — nondeterministic`)
160
- lines.push(` changed: ${v.changed.join(', ')}`)
161
- } else if (v.kind === 'undeclared-inputs') {
162
- lines.push(` ✗ ${o.node.id} — read undeclared inputs`)
163
- if (v.paths.length > 0) lines.push(` ${v.paths.join(', ')}`)
164
- lines.push(' add them to cache.inputs.files / workspaceFiles')
165
- } else if (v.kind === 'rerun-failed') {
166
- lines.push(` ✗ ${o.node.id} — verify re-run failed (exit ${v.exitCode})`)
167
- } else if (v.kind === 'unverifiable-remote-only') {
168
- lines.push(` ⚠ ${o.node.id} — remote-only: no local execution to prove (unverified)`)
169
- } else if (v.kind === 'allowed-nondeterministic') {
170
- lines.push(` ⚠ ${o.node.id} — nondeterministic (allowed)`)
171
- }
172
- }
173
- return lines
174
- }
@@ -1,11 +0,0 @@
1
- // Core's cache as a plugin: the `.vx/cache` handle the host opened (the run
2
- // index + the local artifact store). Declared like any other cache layer;
3
- // put a remote layer BEFORE it to look there first.
4
-
5
- import type { VxPlugin } from '@vzn/vx'
6
-
7
- export const LOCAL_CACHE_PLUGIN = 'vx/local-cache'
8
-
9
- export function localCachePlugin(): VxPlugin {
10
- return { name: LOCAL_CACHE_PLUGIN, cache: (ctx) => ctx.localCache }
11
- }
@@ -1,109 +0,0 @@
1
- // History-based scheduling as a plugin: order ready tasks by their expected
2
- // REMAINING critical-path duration (own p50 + the longest chain of
3
- // dependents), learned from the local run history. This was core's opt-in
4
- // `predictive` mode until 2026-09-02; it is the reference `schedule` plugin
5
- // now — declared like any other, and paying its history read only in
6
- // workspaces that ask for it.
7
- //
8
- // Cache hits are NOT modelled as zero-cost: predicting cache state needs
9
- // the key and a probe, which the scheduler handles at run time (a confirmed
10
- // hit is backfill, never a critical-path task). The estimate here is "what
11
- // if everything ran", which is exactly the case where order matters.
12
-
13
- import { LocalHistoryProvider, type HistoryTable, type TaskNode, type VxPlugin } from '@vzn/vx'
14
-
15
- const SCHEDULE_HISTORY_PLUGIN = 'vx/schedule-history'
16
-
17
- /** Default duration when neither task history nor a workspace median exists. */
18
- const DEFAULT_DURATION_MS = 1000
19
-
20
- export interface ScheduleHistoryOptions {
21
- /** How many recent runs per task to learn from. Default 20. */
22
- readonly window?: number
23
- }
24
-
25
- export function scheduleHistoryPlugin(options: ScheduleHistoryOptions = {}): VxPlugin {
26
- return {
27
- name: SCHEDULE_HISTORY_PLUGIN,
28
- async schedule(nodes, ctx) {
29
- const provider =
30
- options.window === undefined
31
- ? new LocalHistoryProvider(ctx.localCache.dbHandle())
32
- : new LocalHistoryProvider(ctx.localCache.dbHandle(), options.window)
33
- let table: HistoryTable
34
- try {
35
- table = await provider.loadFor([...nodes.keys()])
36
- } catch (err) {
37
- // Failing open: a broken history read costs the ordering, never the run.
38
- ctx.warn(
39
- `[vx] schedule-history: falling back to the baseline order: ${err instanceof Error ? err.message : String(err)}`,
40
- )
41
- return undefined
42
- }
43
- return criticalPathPriorities([...nodes.values()], table)
44
- },
45
- }
46
- }
47
-
48
- /**
49
- * For each node, the expected remaining critical-path duration: its own p50
50
- * plus the maximum over its dependents. A node with no history takes the
51
- * workspace median; an empty history takes a flat default.
52
- */
53
- export function criticalPathPriorities(
54
- nodes: readonly TaskNode[],
55
- history: HistoryTable,
56
- ): ReadonlyMap<string, number> {
57
- const p50s: number[] = []
58
- for (const h of history.values()) {
59
- if (h.p50DurationMs !== undefined) p50s.push(h.p50DurationMs)
60
- }
61
- p50s.sort((a, b) => a - b)
62
- const workspaceMedian =
63
- p50s.length > 0
64
- ? (p50s[Math.floor(p50s.length / 2)] ?? DEFAULT_DURATION_MS)
65
- : DEFAULT_DURATION_MS
66
-
67
- const dependentsOf = new Map<string, string[]>()
68
- for (const n of nodes) {
69
- for (const upstreamId of n.deps) {
70
- const list = dependentsOf.get(upstreamId)
71
- if (list) list.push(n.id)
72
- else dependentsOf.set(upstreamId, [n.id])
73
- }
74
- }
75
- const ownDuration = (n: TaskNode): number => history.get(n.id)?.p50DurationMs ?? workspaceMedian
76
-
77
- // Reverse-topological pass: a node's value is final once every dependent's
78
- // is, so start from the sinks (no dependents) and release each upstream
79
- // when its last dependent has been scored.
80
- const memo = new Map<string, number>()
81
- const nodeById = new Map<string, TaskNode>(nodes.map((n) => [n.id, n]))
82
- const pending = new Map<string, number>()
83
- const queue: TaskNode[] = []
84
- for (const n of nodes) {
85
- const count = dependentsOf.get(n.id)?.length ?? 0
86
- pending.set(n.id, count)
87
- if (count === 0) queue.push(n)
88
- }
89
- let head = 0
90
- while (head < queue.length) {
91
- const n = queue[head++]!
92
- let downstream = 0
93
- for (const dep of dependentsOf.get(n.id) ?? []) {
94
- const d = memo.get(dep) ?? 0
95
- if (d > downstream) downstream = d
96
- }
97
- memo.set(n.id, ownDuration(n) + downstream)
98
- for (const up of n.deps) {
99
- const left = (pending.get(up) ?? 0) - 1
100
- pending.set(up, left)
101
- if (left === 0) {
102
- const upNode = nodeById.get(up)
103
- if (upNode) queue.push(upNode)
104
- }
105
- }
106
- }
107
- for (const n of nodes) if (!memo.has(n.id)) memo.set(n.id, ownDuration(n))
108
- return memo
109
- }