@vzn/vx 0.0.374 → 0.0.376

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 CHANGED
@@ -35,7 +35,7 @@ Same graph, commands and concurrency: [how it is measured](https://vznjs.github.
35
35
  ## Why it is faster
36
36
 
37
37
  - **Nothing boots per task:** no daemon, no Node process; vx spawns your command.
38
- - **No file reads to hash:** on a clean tree, keys come from git's index.
38
+ - **No source-file reads to hash:** on a clean tree, keys come from git's index; each project's `package.json` and the lockfile are still read.
39
39
  - **One pass:** one git walk per run; each stat and lookup happens once.
40
40
 
41
41
  ## Install
@@ -44,7 +44,7 @@ Same graph, commands and concurrency: [how it is measured](https://vznjs.github.
44
44
  npm install -D @vzn/vx # or: pnpm add -D -w · yarn add -D (-W on Yarn 1) · bun add -d
45
45
  ```
46
46
 
47
- A prebuilt binary for Linux and macOS (x64, arm64); no Bun needed. The
47
+ A prebuilt binary for Linux (glibc, not Alpine's musl) and macOS (x64, arm64); no Bun needed. The
48
48
  [release](https://github.com/vznjs/vx/releases) binary needs no Node either.
49
49
  Windows: use WSL.
50
50
 
@@ -103,9 +103,9 @@ vx config. [Migration guide](https://vznjs.github.io/vx/guides/migrate/).
103
103
  - **A cache you can trust:** TypeScript configs are evaluated before
104
104
  hashing, so imports and computed values are in the key. Outputs are
105
105
  wiped before every run and restore: no stale file survives.
106
- - **Sandboxed tasks:** opt in per task; an undeclared read fails the
107
- build instead of poisoning the cache.
108
- - **Clean exits:** Ctrl-C reaps every child.
106
+ - **Sandboxed tasks:** opt in per task; a workspace file the task did
107
+ not declare is out of its reach, so it cannot poison the cache.
108
+ - **Clean exits:** Ctrl-C reaps each task's process group.
109
109
  - **Plugins at every stage:** remote cache, remote execution, telemetry,
110
110
  CLI verbs. Core ships none and needs none.
111
111
  - **Free:** MIT. No paywall, no cloud, no account.
@@ -116,7 +116,7 @@ vx config. [Migration guide](https://vznjs.github.io/vx/guides/migrate/).
116
116
  | ------------------------- | ----------------------------------------- | ------------------------------ | ---------------- |
117
117
  | Config | TypeScript, evaluated into the cache key | JSON (static) | JSON (static) |
118
118
  | Output ownership | **Strict**: wiped before exec and restore | Additive (stale files survive) | Additive |
119
- | Clean-tree hashing | **Zero reads** (git index OIDs) | git OIDs | re-hash / daemon |
119
+ | Clean-tree hashing | **No source reads** (git index OIDs) | git OIDs | re-hash / daemon |
120
120
  | Daemon required for speed | **No** | No (`turbo run` has none) | Yes |
121
121
  | Per-task sandbox | **Yes**: kernel-level, opt-in | No | No |
122
122
  | Plugin API | **Yes**: every pipeline stage | No | Yes (TS-tied) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vzn/vx",
3
- "version": "0.0.374",
3
+ "version": "0.0.376",
4
4
  "description": "A fast, correct task runner and build cache for JavaScript monorepos.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -21,10 +21,10 @@
21
21
  "node": ">=18"
22
22
  },
23
23
  "optionalDependencies": {
24
- "@vzn/vx-linux-x64": "0.0.374",
25
- "@vzn/vx-linux-arm64": "0.0.374",
26
- "@vzn/vx-darwin-x64": "0.0.374",
27
- "@vzn/vx-darwin-arm64": "0.0.374"
24
+ "@vzn/vx-linux-x64": "0.0.376",
25
+ "@vzn/vx-linux-arm64": "0.0.376",
26
+ "@vzn/vx-darwin-x64": "0.0.376",
27
+ "@vzn/vx-darwin-arm64": "0.0.376"
28
28
  },
29
29
  "dependencies": {
30
30
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
@@ -10,6 +10,7 @@ export {
10
10
  } from './dependency-spec.js'
11
11
  export {
12
12
  type ContinueMode,
13
+ deadServerBehind,
13
14
  RestoreDemoted,
14
15
  runGraph,
15
16
  type TaskOutcome,
@@ -338,6 +338,26 @@ class ReadyHeap {
338
338
  }
339
339
  }
340
340
 
341
+ /**
342
+ * The dead server a dependency stands for: itself, or one a group reaches.
343
+ * A group is a name for its deps, and it finished the moment the server
344
+ * was ready, so a task behind it ran against the dead one.
345
+ */
346
+ export function deadServerBehind(
347
+ nodes: ReadonlyMap<string, TaskNode>,
348
+ serverDied: (id: string) => boolean,
349
+ id: string,
350
+ ): string | undefined {
351
+ if (serverDied(id)) return id
352
+ const n = nodes.get(id)
353
+ if (n === undefined || !isGroupTask(n)) return undefined
354
+ for (const d of n.deps) {
355
+ const dead = deadServerBehind(nodes, serverDied, d)
356
+ if (dead !== undefined) return dead
357
+ }
358
+ return undefined
359
+ }
360
+
341
361
  /**
342
362
  * Run the task graph. Independent tasks run in parallel up to `concurrency`.
343
363
  * If a task fails, its dependents are marked `skipped` but unrelated tasks
@@ -572,19 +592,8 @@ export async function runGraph(options: ScheduleOptions): Promise<Map<string, Ta
572
592
  // could fail anyway.
573
593
  const aborted = (): boolean => options.signal?.aborted === true
574
594
  const serverDied = options.serverDied ?? ((): boolean => false)
575
- // The dead server a dependency stands for: itself, or one a group
576
- // reaches. A group is a name for its deps, and it finished the moment
577
- // the server was ready, so a task behind it ran against the dead one.
578
- const deadServerVia = (id: string): string | undefined => {
579
- if (serverDied(id)) return id
580
- const n = nodes.get(id)
581
- if (n === undefined || !isGroupTask(n)) return undefined
582
- for (const d of n.deps) {
583
- const dead = deadServerVia(d)
584
- if (dead !== undefined) return dead
585
- }
586
- return undefined
587
- }
595
+ const deadServerVia = (id: string): string | undefined =>
596
+ deadServerBehind(nodes, serverDied, id)
588
597
  const servers =
589
598
  options.serverDied === undefined
590
599
  ? []
@@ -22,6 +22,7 @@ import { DeferredOutputs } from './deferred-outputs.js'
22
22
  import { resolveDownloadModes } from './download-policy.js'
23
23
  import type { TaskExecutor } from '../exec/index.js'
24
24
  import {
25
+ deadServerBehind,
25
26
  isGroupTask,
26
27
  markSurfacedDeps,
27
28
  runGraph,
@@ -873,12 +874,28 @@ async function runOnBus(
873
874
  `[vx] --exclude-dependencies: ${excluded.unsaved} cached task(s) build on a skipped dependency; what they build is not saved`,
874
875
  )
875
876
  }
876
- const taint = taintTracker(options.continueMode === 'always', excluded.seeds, nodes)
877
+ // A server that ended on its own is failed (the end of the run says
878
+ // so); what has not yet started hears it before it dispatches (C-88).
879
+ const serverDied = (id: string): boolean => {
880
+ const child = persistentRegistry.get(id)
881
+ return child !== undefined && hasEnded(child) && child.exitCode !== 0
882
+ }
883
+ // Under `always` a task that starts while a server it depends on is
884
+ // dead runs on a failure the outcomes do not hold yet (the server's
885
+ // says `success` until the run ends), so it is seeded here, at its
886
+ // dispatch, and the tracker carries it to what is built on it.
887
+ const taintSeeds = new Set(excluded.seeds)
888
+ const taint = taintTracker(options.continueMode === 'always', taintSeeds, nodes)
877
889
  const dependedOn = new Set<string>()
878
890
  for (const n of nodes.values()) for (const d of n.deps) dependedOn.add(d)
879
891
 
880
892
  const buildExecuteArgs = (node: TaskNode, upstream: TaskOutcome[], reuseProbe = true) => {
881
893
  const probe = reuseProbe ? shortCircuit.preProbed.get(node.id) : undefined
894
+ if (
895
+ options.continueMode === 'always' &&
896
+ node.deps.some((d) => deadServerBehind(nodes, serverDied, d) !== undefined)
897
+ )
898
+ taintSeeds.add(node.id)
882
899
  const tainted = taint.judge(node, upstream)
883
900
  return {
884
901
  node,
@@ -940,12 +957,7 @@ async function runOnBus(
940
957
  ...(hasPooledExecutor(executors) ? { poolOf: poolOfPlacement(placements) } : {}),
941
958
  ...(admit !== undefined ? { admit } : {}),
942
959
  ...(options.continueMode !== undefined ? { continueMode: options.continueMode } : {}),
943
- // A server that ended on its own is failed (the end of the run says
944
- // so); what has not yet started hears it before it dispatches (C-88).
945
- serverDied: (id) => {
946
- const child = persistentRegistry.get(id)
947
- return child !== undefined && hasEnded(child) && child.exitCode !== 0
948
- },
960
+ serverDied,
949
961
  signal: stopRun.signal,
950
962
  onStart: (node) => {
951
963
  log.taskStart?.(node)