@vzn/vx 0.0.256 → 0.0.258

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vzn/vx",
3
- "version": "0.0.256",
3
+ "version": "0.0.258",
4
4
  "description": "A fast, correct task runner and build cache for JavaScript monorepos.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -17,10 +17,10 @@
17
17
  "node": ">=18"
18
18
  },
19
19
  "optionalDependencies": {
20
- "@vzn/vx-linux-x64": "0.0.256",
21
- "@vzn/vx-linux-arm64": "0.0.256",
22
- "@vzn/vx-darwin-x64": "0.0.256",
23
- "@vzn/vx-darwin-arm64": "0.0.256"
20
+ "@vzn/vx-linux-x64": "0.0.258",
21
+ "@vzn/vx-linux-arm64": "0.0.258",
22
+ "@vzn/vx-darwin-x64": "0.0.258",
23
+ "@vzn/vx-darwin-arm64": "0.0.258"
24
24
  },
25
25
  "dependencies": {
26
26
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
@@ -100,8 +100,9 @@ import { CACHE_VERSION, foldKey } from './key-fold.js'
100
100
  /**
101
101
  * An artifact or temp file without an `entries` row is reaped by
102
102
  * `prune()` only once it is this old. A save renames the artifact into
103
- * place and commits its row in the same tick; a crashed save's temp is
104
- * stale long before this. Generous on purpose — a false orphan costs a
103
+ * place inside its `BEGIN IMMEDIATE` row transaction (A-3), so a live
104
+ * artifact without a row is one whose commit failed and was taken back
105
+ * out; a crashed save's temp is stale long before this. Generous on purpose — a false orphan costs a
105
106
  * re-run, a leaked temp costs disk.
106
107
  */
107
108
  const ORPHAN_GRACE_MS = 60 * 60 * 1000
@@ -87,6 +87,15 @@ import type { CacheKeyInput } from './layer.js'
87
87
  // never folded and saved what it built. The fix withholds that link unless
88
88
  // the key answers for the package, but an entry saved before it still hits
89
89
  // after the sibling changes (probed: the old output replayed).
90
+ // v32: an artifact saved before non-UTF-8 names were refused lacks that
91
+ // output under an unchanged key (item 739).
92
+ // v33: an entry saved after an input changed under a running task sits
93
+ // under the key the fixed code derives once the tree is back (item 743).
94
+ // v34: the same for an in-run lockfile rewrite (item 750).
95
+ // v35: the sidecar records the key and ingest refuses a mismatch; a v34
96
+ // artifact records none (item 943).
97
+ // v36: each artifact ends in a `.vx-sum` CRC-32 of its entries, checked on
98
+ // scan and restore; a v35 artifact has none (L-19).
90
99
  export const CACHE_VERSION = 'vx-cache-v36'
91
100
 
92
101
  /**
@@ -498,8 +498,8 @@ export class ArtifactVanishedError extends Error {
498
498
  }
499
499
 
500
500
  /**
501
- * The shape every cache implementation honors. `Cache` (the local v10
502
- * implementation) and `LayeredCache` both `implements` this so the
501
+ * The shape every cache implementation honors. `Cache` (the local store)
502
+ * and `LayeredCache` both `implements` this so the
503
503
  * orchestrator's `executeTask` can take either without a discriminated
504
504
  * union and we get a compile-time guarantee the surfaces stay congruent.
505
505
  */
@@ -561,7 +561,8 @@ export interface CacheLayer {
561
561
  * Lightweight existence probe. `'local'` / `'remote'` names the layer
562
562
  * that holds the artifact; `null` is a miss. NEVER moves bytes: no
563
563
  * artifact read, no remote download, no local ingest, no accessed_at
564
- * bump (the LayeredCache's remote side is an HTTP HEAD). Planning
564
+ * bump (on the remote side the layer decides the call: an HTTP HEAD, or
565
+ * REAPI's `getActionResult` + `findMissingBlobs`). Planning
565
566
  * (`--dry` / `--graph`) predicts hits with this instead of `get` so a
566
567
  * dry run can't pull N artifacts over the network. Remote errors
567
568
  * degrade to `null` — an existence probe never fails anything.
@@ -396,8 +396,8 @@ export class LayeredCache implements CacheLayer {
396
396
  const remoteBody = remoteResult as { body: Blob | Response; durationMs?: number }
397
397
 
398
398
  // Ingest the remote body into local using the caller-supplied
399
- // taskId/command plus the remote-reported durationMs. The remote
400
- // layer carries durationMs as an HTTP header (x-artifact-duration);
399
+ // taskId/command plus the remote-reported durationMs, which `get`
400
+ // returns beside the body (the layer decides where it reads it from);
401
401
  // taskId + command come from the orchestrator's TaskNode in scope.
402
402
  // Without `ctx`, we can't populate a meaningful entries row, so
403
403
  // ingest with placeholders — caller-side typing nudges everyone
@@ -9,9 +9,11 @@ import { UserError } from '../util/index.js'
9
9
  * toggled on its own so `--force` (re-execute but still refresh the
10
10
  * cache) is distinct from `--no-cache` (disable everything).
11
11
  *
12
- * Only the task-artifact get/save path is gated. `recordRun`, `stats`,
13
- * `prune`, key derivation, and prefetch-ingest are never affected — they
14
- * are bookkeeping/analytics that a run policy has no business disabling.
12
+ * The task-artifact get/save path is gated, and the local axes also gate
13
+ * the local store's config-evaluation reads and writes and its file-hash
14
+ * writes (`cache.ts`). `recordRun`, `stats`, `prune`, key derivation, and
15
+ * prefetch-ingest are never affected — they are bookkeeping/analytics
16
+ * that a run policy has no business disabling.
15
17
  */
16
18
  export interface CachePolicy {
17
19
  localRead: boolean
@@ -176,8 +176,9 @@ export interface RunOptions {
176
176
  */
177
177
  signal?: AbortSignal
178
178
  /**
179
- * Hand the requested persistent tasks still running back on
180
- * `RunSummary.persistent` instead of stopping them when the graph ends.
179
+ * Hand the requested persistent tasks still running, and the persistent
180
+ * tasks they depend on (C-46), back on `RunSummary.persistent` instead
181
+ * of stopping them when the graph ends.
181
182
  * The caller owns them from then on and stops them with its `stop()`.
182
183
  * The watch loop sets this: a dev server stays up while watch idles and
183
184
  * is replaced only when the next cycle starts.
@@ -4,7 +4,7 @@
4
4
  //
5
5
  // No TASK execution happens: no task spawn, no cleanOutputs, no
6
6
  // restoreOutputs. Cache probing uses the byte-free `cache.has()`
7
- // existence probe (a remote layer answers with an HTTP HEAD — no
7
+ // existence probe (a remote layer answers it its own way — no
8
8
  // artifact download, no local ingest), so the plan is read-only — with
9
9
  // one deliberate exception: `cache.inputs.runtime` / `workspaceRuntime`
10
10
  // probe commands DO run, because predicting a task's key requires
@@ -28,12 +28,10 @@ import type { TelemetryContext, TelemetrySink } from './telemetry.js'
28
28
  * executed. Registered explicitly in vx.workspace.ts via
29
29
  * defineWorkspace({ plugins: [...] }). No auto-discovery.
30
30
  *
31
- * The old observe-only `Plugin` (`{ name, setup(ctx) }`) is a subset of
32
- * this shape: a plugin with only `setup` installs and runs exactly as
33
- * before via `installPlugins`. The capabilities are consulted by
34
- * `plugin-host.ts`; core's own executor and cache are plugins too
35
- * (src/plugins/), declared by the workspace — there is no fallback
36
- * outside the list.
31
+ * Made by `definePlugin(import.meta, hooks)` only: the loader refuses a
32
+ * plain object. The capabilities are consulted by `plugin-host.ts`. Core
33
+ * names no plugin; the local executor and the local cache are the floor
34
+ * under the list, taking what every plugin declines.
37
35
  */
38
36
  export interface VxPlugin {
39
37
  /**
@@ -47,7 +45,7 @@ export interface VxPlugin {
47
45
  // --- PIPELINE stages (shape the run before it executes — opt-in) ----------
48
46
  //
49
47
  // Each receives the object core is about to use and edits it IN PLACE;
50
- // core re-validates after the last plugin, so a plugin cannot produce what
48
+ // core re-validates after each plugin, so a plugin cannot produce what
51
49
  // the loader would refuse from a user. Declaration order is the order.
52
50
  // Whatever a stage changes reaches the cache key by construction: the key
53
51
  // hashes the task config AFTER `project` ran. See
@@ -10,9 +10,10 @@
10
10
  * about the identity of the same task: a lookup either found nothing or, worse,
11
11
  * answered with a different task's history.
12
12
  *
13
- * A `#`-containing task name is legal and pinned elsewhere in the suite, so
14
- * this is reachable rather than theoretical. The cache's run history read
15
- * the same rule from a private copy until item 646; one rule, one place.
13
+ * A config's task name may not hold `#` (`taskNameProblem`), but an id also
14
+ * arrives from run history and plugins, so the first-`#` rule is the one
15
+ * the graph uses. The cache's run history read the same rule from a
16
+ * private copy until item 646; one rule, one place.
16
17
  */
17
18
  export function splitTaskId(id: string): [project: string, task: string] {
18
19
  const i = id.indexOf('#')
@@ -594,10 +594,15 @@ export async function defaultAffectedBase(workspaceRoot: string): Promise<string
594
594
  'ignore',
595
595
  )
596
596
  const out = new TextDecoder().decode(probe.stdout).trim()
597
- if (probe.exitCode === 0 && out.length > 0) return out
597
+ // An origin/HEAD naming a branch the remote deleted (a pruned fetch keeps
598
+ // the symref) is no base either: it failed as `git ref "origin/master" did
599
+ // not resolve`, a ref nobody typed, where an unset one falls back.
600
+ if (probe.exitCode === 0 && out.length > 0 && revParse(workspaceRoot, out) !== undefined) {
601
+ return out
602
+ }
598
603
  if (revParse(workspaceRoot, 'HEAD~1') === undefined) {
599
604
  throw new UserError(
600
- '--affected has no base here: origin/HEAD is not set and HEAD has no parent to compare ' +
605
+ '--affected has no base here: origin/HEAD is not set (or names a branch that is gone) and HEAD has no parent to compare ' +
601
606
  'with — a shallow clone? Fetch history (actions/checkout: fetch-depth: 0) or name the ' +
602
607
  'base: --affected=origin/main',
603
608
  )
@@ -993,8 +993,10 @@ function assertFilterNamesDeclaredDeps(
993
993
  if (raw.startsWith('!')) continue
994
994
  const f = specForm(raw)
995
995
  if (f.task.length === 0 || f.task.includes('*') || named(f)) continue
996
+ // `where` opens with the config's path; the message names it once.
997
+ const task = where.slice(where.lastIndexOf(': tasks.') + 2)
996
998
  throw new UserError(
997
- `${where}.cache.inputs.tasks: "${raw}" names no task in ${where}.dependsOn ` +
999
+ `${where}.cache.inputs.tasks: "${raw}" names no task in ${task}.dependsOn ` +
998
1000
  `(${declared.length === 0 ? 'none declared' : dependsOn!.map((d) => `'${d}'`).join(', ')}) — ` +
999
1001
  `it would match nothing and fold no upstream hash, decoupling the task from its ` +
1000
1002
  `dependencies. \`name\` is this project's task, \`^name\` its dependencies', ` +