@vzn/vx 0.0.15 → 0.0.18

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 (111) hide show
  1. package/README.md +91 -130
  2. package/index.ts +10 -0
  3. package/launcher.mjs +10 -11
  4. package/package.json +19 -5
  5. package/plugins/local-cache/index.ts +2 -0
  6. package/plugins/local-executor/index.ts +2 -0
  7. package/plugins/schedule-history/index.ts +2 -0
  8. package/src/bin.ts +2 -2
  9. package/src/cache/archive.ts +557 -0
  10. package/src/cache/cache.ts +929 -250
  11. package/src/cache/cas-backend.ts +20 -2
  12. package/src/cache/chained-cache.ts +202 -0
  13. package/src/cache/digest.ts +0 -15
  14. package/src/cache/index.ts +13 -3
  15. package/src/cache/inputs.ts +445 -41
  16. package/src/cache/layered-cache.ts +129 -30
  17. package/src/cache/tar-stream.ts +358 -0
  18. package/src/cli/cache.ts +36 -10
  19. package/src/cli/help.ts +167 -128
  20. package/src/cli/index.ts +74 -43
  21. package/src/cli/info.ts +30 -0
  22. package/src/cli/last.ts +140 -0
  23. package/src/cli/migrate-emit.ts +21 -0
  24. package/src/cli/migrate-nx.ts +23 -0
  25. package/src/cli/migrate-scripts.ts +127 -0
  26. package/src/cli/migrate-turbo.ts +43 -6
  27. package/src/cli/migrate.ts +119 -33
  28. package/src/cli/plan-format.ts +58 -3
  29. package/src/cli/plugin-commands.ts +96 -0
  30. package/src/cli/prune.ts +291 -0
  31. package/src/cli/run.ts +272 -80
  32. package/src/cli/upgrade.ts +7 -6
  33. package/src/cli/watch.ts +259 -61
  34. package/src/cli/why.ts +193 -0
  35. package/src/config.ts +40 -23
  36. package/src/exec/env.ts +7 -1
  37. package/src/exec/executor.ts +239 -0
  38. package/src/exec/index.ts +19 -1
  39. package/src/exec/runner.ts +116 -29
  40. package/src/exec/sandbox-runtime.ts +194 -24
  41. package/src/graph/index.ts +3 -1
  42. package/src/graph/scheduler.ts +92 -18
  43. package/src/graph/task-graph.ts +220 -6
  44. package/src/index.ts +82 -84
  45. package/src/orchestrator/deferred-outputs.ts +144 -0
  46. package/src/orchestrator/download-policy.ts +171 -0
  47. package/src/orchestrator/events.ts +30 -0
  48. package/src/orchestrator/execute-task.ts +328 -94
  49. package/src/orchestrator/failure-mode.ts +61 -0
  50. package/src/orchestrator/framed-output.ts +87 -35
  51. package/src/orchestrator/history.ts +78 -31
  52. package/src/orchestrator/index.ts +43 -90
  53. package/src/orchestrator/local-shortcircuit.ts +22 -0
  54. package/src/orchestrator/logger.ts +196 -14
  55. package/src/orchestrator/metrics.ts +57 -1376
  56. package/src/orchestrator/missing-plugin.ts +9 -0
  57. package/src/orchestrator/options.ts +22 -4
  58. package/src/orchestrator/plan.ts +144 -1
  59. package/src/orchestrator/plugin-host.ts +199 -119
  60. package/src/orchestrator/plugin.ts +144 -57
  61. package/src/orchestrator/prepare.ts +220 -69
  62. package/src/orchestrator/remote-prefetch.ts +27 -8
  63. package/src/orchestrator/resources.ts +20 -34
  64. package/src/orchestrator/run-artifacts.ts +41 -16
  65. package/src/orchestrator/run-context.ts +167 -29
  66. package/src/orchestrator/run-report.ts +73 -46
  67. package/src/orchestrator/run.ts +590 -122
  68. package/src/orchestrator/stable-keys.ts +74 -33
  69. package/src/orchestrator/status-line.ts +47 -8
  70. package/src/orchestrator/summary.ts +22 -1
  71. package/src/orchestrator/tally.ts +78 -35
  72. package/src/orchestrator/task-hash.ts +105 -11
  73. package/src/orchestrator/task-log-buffer.ts +291 -0
  74. package/src/orchestrator/telemetry-host.ts +24 -6
  75. package/src/orchestrator/telemetry.ts +181 -21
  76. package/src/orchestrator/upstream.ts +72 -31
  77. package/src/orchestrator/verify.ts +12 -2
  78. package/src/plugins/local-cache/index.ts +11 -0
  79. package/src/plugins/local-executor/index.ts +45 -0
  80. package/src/plugins/schedule-history/index.ts +109 -0
  81. package/src/util/edit-distance.ts +21 -0
  82. package/src/util/errors.ts +15 -1
  83. package/src/util/index.ts +7 -2
  84. package/src/util/num.ts +37 -0
  85. package/src/util/paths.ts +43 -0
  86. package/src/util/settle.ts +48 -0
  87. package/src/util/size.ts +5 -1
  88. package/src/util/tail.ts +66 -0
  89. package/src/util/timing.ts +63 -0
  90. package/src/workspace/affected.ts +142 -15
  91. package/src/workspace/config-cache.ts +304 -0
  92. package/src/workspace/config-eval.ts +210 -0
  93. package/src/workspace/config-imports.ts +161 -0
  94. package/src/workspace/filter.ts +22 -4
  95. package/src/workspace/fingerprint.ts +16 -1
  96. package/src/workspace/index.ts +15 -3
  97. package/src/workspace/lockfile.ts +13 -2
  98. package/src/workspace/nested-dirs.ts +21 -7
  99. package/src/workspace/package-graph.ts +23 -20
  100. package/src/workspace/project-loader.ts +503 -59
  101. package/src/workspace/workspace.ts +213 -72
  102. package/src/cache/tar.ts +0 -343
  103. package/src/cli/backend.ts +0 -23
  104. package/src/cli/mcp-rpc.ts +0 -292
  105. package/src/cli/mcp.ts +0 -92
  106. package/src/orchestrator/devframe-surface.ts +0 -74
  107. package/src/orchestrator/predict.ts +0 -106
  108. package/src/orchestrator/protocol.ts +0 -156
  109. package/src/orchestrator/run-state.ts +0 -145
  110. package/src/orchestrator/wire-render.ts +0 -92
  111. package/src/orchestrator/wire.ts +0 -212
@@ -18,10 +18,13 @@
18
18
  // reference impls so downstream work (R2, otel-bridge, distributed-ci)
19
19
  // can rely on the type. Byte-identical behaviour today.
20
20
 
21
- import { rm } from 'node:fs/promises'
21
+ import { mkdir, rename, rm } from 'node:fs/promises'
22
22
  import path from 'node:path'
23
23
  import type { Digest } from './digest.js'
24
24
 
25
+ /** Per-process counter for put() temp names; uniqueness only. */
26
+ let casTmpSeq = 0
27
+
25
28
  export interface CASBackend {
26
29
  /** Write bytes under `digest`. Idempotent — putting the same digest twice is a no-op. */
27
30
  put(digest: Digest, bytes: Uint8Array): Promise<void>
@@ -78,7 +81,22 @@ export class FsCASBackend implements CASBackend {
78
81
  `FsCASBackend.put: sizeBytes mismatch (digest=${digest.sizeBytes}, actual=${bytes.byteLength})`,
79
82
  )
80
83
  }
81
- await Bun.write(this.pathFor(digest), bytes)
84
+ // Temp + rename, the same shape `Cache.writeArtifactAndIndex` and the
85
+ // archive extractor use: two writers of the SAME content-addressed
86
+ // blob are expected (that is what content-addressing invites), and a
87
+ // plain write lets a reader observe a half-written file under a name
88
+ // that promises complete bytes. rename(2) is atomic, so a concurrent
89
+ // reader sees either the old file or the whole new one.
90
+ const final = this.pathFor(digest)
91
+ const tmp = `${final}.tmp-${process.pid.toString(36)}-${(casTmpSeq++).toString(36)}`
92
+ await mkdir(this.rootDir, { recursive: true })
93
+ try {
94
+ await Bun.write(tmp, bytes)
95
+ await rename(tmp, final)
96
+ } catch (err) {
97
+ await rm(tmp, { force: true })
98
+ throw err
99
+ }
82
100
  }
83
101
 
84
102
  async get(digest: Digest): Promise<Uint8Array | null> {
@@ -0,0 +1,202 @@
1
+ // Several declared cache layers, consulted in declaration order. Lookup
2
+ // walks the layers until one answers; a save reaches every layer; the FIRST
3
+ // layer owns the run index (history, stats, prune) so a run is recorded
4
+ // once. Restore goes to the layer that produced the hit — remembered per
5
+ // hash — because an entry's artifact lives wherever it was found.
6
+
7
+ import type {
8
+ Cache,
9
+ CacheEntry,
10
+ CacheGetContext,
11
+ CacheKeyInput,
12
+ CacheLayer,
13
+ CacheStats,
14
+ CacheStatsOptions,
15
+ IngestMeta,
16
+ InvocationRecord,
17
+ OutputDirRow,
18
+ OutputFileRow,
19
+ PruneOptions,
20
+ PruneResult,
21
+ RunRecord,
22
+ } from './cache.js'
23
+
24
+ export class ChainedCache implements CacheLayer {
25
+ readonly hasRemote: boolean
26
+ private readonly hitLayer = new Map<string, CacheLayer>()
27
+
28
+ constructor(readonly layers: readonly CacheLayer[]) {
29
+ if (layers.length < 2) throw new Error('ChainedCache needs at least two layers')
30
+ this.hasRemote = layers.some((l) => l.hasRemote === true)
31
+ }
32
+
33
+ get local(): Cache | undefined {
34
+ return this.layers[0]!.local
35
+ }
36
+
37
+ private owner(hash: string): CacheLayer {
38
+ return this.hitLayer.get(hash) ?? this.layers[0]!
39
+ }
40
+
41
+ key(input: CacheKeyInput): Promise<string> {
42
+ return this.layers[0]!.key(input)
43
+ }
44
+
45
+ async get(hash: string, ctx?: CacheGetContext): Promise<CacheEntry | null> {
46
+ for (const layer of this.layers) {
47
+ const entry = await layer.get(hash, ctx)
48
+ if (entry !== null) {
49
+ this.hitLayer.set(hash, layer)
50
+ return entry
51
+ }
52
+ }
53
+ return null
54
+ }
55
+
56
+ async has(hash: string): Promise<'local' | 'remote' | null> {
57
+ for (const layer of this.layers) {
58
+ const where = await layer.has(hash)
59
+ if (where !== null) {
60
+ this.hitLayer.set(hash, layer)
61
+ return where
62
+ }
63
+ }
64
+ return null
65
+ }
66
+
67
+ async prefetch(hash: string, ctx?: CacheGetContext): Promise<boolean> {
68
+ for (const layer of this.layers) {
69
+ if (await layer.prefetch(hash, ctx)) {
70
+ this.hitLayer.set(hash, layer)
71
+ return true
72
+ }
73
+ }
74
+ return false
75
+ }
76
+
77
+ async remoteHasMany(hashes: readonly string[]): Promise<Set<string> | null> {
78
+ // The caller (remote-prefetch) treats a non-null answer as authoritative
79
+ // for the WHOLE chain: complement = absent, broadcast. That is sound only
80
+ // if EVERY remote layer answered — a partial union would poison a layer
81
+ // that cannot batch with another layer's negatives, and its later lazy
82
+ // get() would skip a real remote hit. So: each answering layer gets its
83
+ // OWN complement marked here (its own truth — this also spares it the
84
+ // per-hash GETs for hashes only a sibling holds), and the merged answer
85
+ // is returned only when no remote layer was left unanswered.
86
+ let out: Set<string> | null = null
87
+ let complete = true
88
+ for (const layer of this.layers) {
89
+ if (layer.hasRemote !== true) continue
90
+ const found = (await layer.remoteHasMany?.(hashes)) ?? null
91
+ if (found === null) {
92
+ complete = false
93
+ continue
94
+ }
95
+ layer.markRemoteAbsent?.(hashes.filter((h) => !found.has(h)))
96
+ out ??= new Set()
97
+ for (const h of found) out.add(h)
98
+ }
99
+ return complete ? out : null
100
+ }
101
+
102
+ markRemoteAbsent(hashes: Iterable<string>): void {
103
+ const list = [...hashes]
104
+ for (const layer of this.layers) layer.markRemoteAbsent?.(list)
105
+ }
106
+
107
+ async drainUploads(): Promise<void> {
108
+ await Promise.all(this.layers.map((l) => l.drainUploads?.()))
109
+ }
110
+
111
+ loadOutputFilesBatch(hashes: readonly string[]): Map<string, OutputFileRow[]> {
112
+ const out = new Map<string, OutputFileRow[]>()
113
+ for (const layer of this.layers) {
114
+ for (const [h, rows] of layer.loadOutputFilesBatch(hashes)) {
115
+ if (!out.has(h)) out.set(h, rows)
116
+ }
117
+ }
118
+ return out
119
+ }
120
+
121
+ isOutputsCurrent(projectDir: string, expected: readonly OutputFileRow[]): Promise<boolean> {
122
+ return this.layers[0]!.isOutputsCurrent(projectDir, expected)
123
+ }
124
+
125
+ // The directory short-circuit lives with the layer that owns the local
126
+ // rows — the first, like the file check.
127
+ recordOutputDirs(hash: string, projectDir: string, prefixes: readonly string[]): Promise<void> {
128
+ return this.layers[0]!.recordOutputDirs?.(hash, projectDir, prefixes) ?? Promise.resolve()
129
+ }
130
+
131
+ loadOutputDirsBatch(hashes: readonly string[]): Map<string, OutputDirRow[]> {
132
+ return this.layers[0]!.loadOutputDirsBatch?.(hashes) ?? new Map()
133
+ }
134
+
135
+ outputDirsCurrent(projectDir: string, rows: readonly OutputDirRow[]): Promise<boolean> {
136
+ return this.layers[0]!.outputDirsCurrent?.(projectDir, rows) ?? Promise.resolve(false)
137
+ }
138
+
139
+ restoreOutputs(hash: string, projectDir: string, workspaceRoot?: string): Promise<void> {
140
+ return this.owner(hash).restoreOutputs(hash, projectDir, workspaceRoot)
141
+ }
142
+
143
+ async save(args: Parameters<CacheLayer['save']>[0]): Promise<void> {
144
+ // Layers wrapping the SAME local handle (two remote plugins over
145
+ // ctx.localCache) would each pack + write the identical artifact; the
146
+ // first write is the only one that matters, so later layers get
147
+ // `skipLocalWrite` and go straight to their remote upload (which reads
148
+ // the artifact the first layer just wrote — same handle, same path).
149
+ const seenLocals = new Set<NonNullable<CacheLayer['local']>>()
150
+ for (const layer of this.layers) {
151
+ const local = layer.local
152
+ const skip = local !== undefined && seenLocals.has(local)
153
+ await layer.save(skip ? { ...args, skipLocalWrite: true } : args)
154
+ if (local !== undefined) seenLocals.add(local)
155
+ }
156
+ }
157
+
158
+ ingest(hash: string, compressed: Uint8Array, meta: IngestMeta): Promise<void> {
159
+ return this.layers[0]!.ingest(hash, compressed, meta)
160
+ }
161
+
162
+ recordRun(run: RunRecord): void {
163
+ this.layers[0]!.recordRun(run)
164
+ }
165
+
166
+ recordRuns(runs: readonly RunRecord[]): void {
167
+ this.layers[0]!.recordRuns(runs)
168
+ }
169
+
170
+ recordRunBundle(bundle: { runs: readonly RunRecord[]; invocation: InvocationRecord }): void {
171
+ this.layers[0]!.recordRunBundle(bundle)
172
+ }
173
+
174
+ stats(opts?: CacheStatsOptions): CacheStats {
175
+ return this.layers[0]!.stats(opts)
176
+ }
177
+
178
+ hashFile(filePath: string): Promise<string> {
179
+ return this.layers[0]!.hashFile(filePath)
180
+ }
181
+
182
+ outputsPath(hash: string): string {
183
+ return this.owner(hash).outputsPath(hash)
184
+ }
185
+
186
+ prune(options: PruneOptions): Promise<PruneResult> {
187
+ return this.layers[0]!.prune(options)
188
+ }
189
+
190
+ close(): void {
191
+ // Every layer closes even if an earlier one throws; the first error wins.
192
+ let failure: unknown
193
+ for (const layer of this.layers) {
194
+ try {
195
+ layer.close()
196
+ } catch (err) {
197
+ failure ??= err
198
+ }
199
+ }
200
+ if (failure !== undefined) throw failure
201
+ }
202
+ }
@@ -30,18 +30,3 @@ export function makeDigest(hash: string, sizeBytes: number): Digest {
30
30
  }
31
31
  return { hash, sizeBytes }
32
32
  }
33
-
34
- export function digestEqual(a: Digest, b: Digest): boolean {
35
- return a.hash === b.hash && a.sizeBytes === b.sizeBytes
36
- }
37
-
38
- /** Stable wire format for logging / RPC: `hash/sizeBytes`. */
39
- export function digestString(d: Digest): string {
40
- return `${d.hash}/${d.sizeBytes}`
41
- }
42
-
43
- export function parseDigest(s: string): Digest {
44
- const slash = s.indexOf('/')
45
- if (slash === -1) throw new Error(`invalid digest string: ${JSON.stringify(s)}`)
46
- return makeDigest(s.slice(0, slash), Number(s.slice(slash + 1)))
47
- }
@@ -5,24 +5,34 @@
5
5
  export {
6
6
  Cache,
7
7
  type CacheEntry,
8
+ type CacheKeyInput,
8
9
  type CacheLayer,
9
10
  type CachePolicy,
11
+ EXECUTED_RUNS_SQL,
10
12
  FULL_CACHE_POLICY,
11
13
  type InvocationRecord,
14
+ KEYED_RUNS_SQL,
12
15
  parseCachePolicy,
13
16
  type RunRecord,
14
17
  type TaskInputRow,
15
18
  WORKSPACE_OUTPUT_PREFIX,
19
+ type OutputDirRow,
20
+ OUTPUT_DIRS_CAP,
21
+ OUTPUT_DIRS_RACY_MS,
16
22
  } from './cache.js'
17
- export { type CASBackend, FsCASBackend, MemoryCASBackend } from './cas-backend.js'
18
- export { type Digest, digestEqual, digestString, makeDigest, parseDigest } from './digest.js'
23
+ export { type CASBackend, FsCASBackend } from './cas-backend.js'
24
+ export { type Digest } from './digest.js'
19
25
  export {
20
26
  cleanOutputs,
21
27
  cleanWorkspaceOutputs,
22
28
  GitFilesCache,
23
- populateGitFilesCache,
29
+ applyGitEnumeration,
30
+ gitPathspecs,
31
+ startGitEnumeration,
32
+ type GitEnumeration,
24
33
  resolveInputs,
25
34
  resolveOutputs,
26
35
  resolveWorkspaceOutputs,
27
36
  } from './inputs.js'
28
37
  export { LayeredCache, type RemoteCacheLayer } from './layered-cache.js'
38
+ export { ChainedCache } from './chained-cache.js'