@vzn/vx 0.0.154 → 0.0.156

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.154",
3
+ "version": "0.0.156",
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.154",
21
- "@vzn/vx-linux-arm64": "0.0.154",
22
- "@vzn/vx-darwin-x64": "0.0.154",
23
- "@vzn/vx-darwin-arm64": "0.0.154"
20
+ "@vzn/vx-linux-x64": "0.0.156",
21
+ "@vzn/vx-linux-arm64": "0.0.156",
22
+ "@vzn/vx-darwin-x64": "0.0.156",
23
+ "@vzn/vx-darwin-arm64": "0.0.156"
24
24
  },
25
25
  "dependencies": {
26
26
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
@@ -28,11 +28,14 @@ export function formatPlanText(plan: RunPlan): string {
28
28
  let remote = 0
29
29
  let miss = 0
30
30
  let nocache = 0
31
+ let noop = 0
31
32
  for (const t of real) {
32
- const sym = symbolFor(t.cacheStatus)
33
- const desc = describe(t.cacheStatus)
33
+ // `@noop`: the run succeeds it without running, whatever its key says.
34
+ const skipped = t.executor === 'noop'
35
+ const sym = skipped ? '∅' : symbolFor(t.cacheStatus)
36
+ const desc = skipped ? 'noop — would not run' : describe(t.cacheStatus)
34
37
  const shortHash = t.hash ? t.hash.slice(0, 8) : ''
35
- const executes = t.cacheStatus === 'miss' || t.cacheStatus === 'no-cache'
38
+ const executes = !skipped && (t.cacheStatus === 'miss' || t.cacheStatus === 'no-cache')
36
39
  const eta = executes && t.p50Ms !== undefined ? ` ~${formatDuration(t.p50Ms)}` : ''
37
40
  // Placement, by EXECUTOR NAME rather than a local/remote word: the
38
41
  // summary line below already spends "local" and "remote" on the cache
@@ -48,7 +51,8 @@ export function formatPlanText(plan: RunPlan): string {
48
51
  if (taskDesc) {
49
52
  lines.push(` ${' '.repeat(idWidth)} ${taskDesc}`)
50
53
  }
51
- if (t.cacheStatus === 'hit-local') local++
54
+ if (skipped) noop++
55
+ else if (t.cacheStatus === 'hit-local') local++
52
56
  else if (t.cacheStatus === 'hit-remote') remote++
53
57
  else if (t.cacheStatus === 'miss') miss++
54
58
  else if (t.cacheStatus === 'no-cache') nocache++
@@ -62,6 +66,7 @@ export function formatPlanText(plan: RunPlan): string {
62
66
  if (hits > 0) summary.push(`${hits} cache hit${hits === 1 ? '' : 's'} (${hitParts.join(', ')})`)
63
67
  if (miss > 0) summary.push(`${miss} would run`)
64
68
  if (nocache > 0) summary.push(`${nocache} no-cache`)
69
+ if (noop > 0) summary.push(`${noop} noop`)
65
70
  lines.push('')
66
71
  lines.push(summary.join(', ') + '.')
67
72
 
@@ -52,7 +52,7 @@ export function isIgnoredWatchPath(rel: string): boolean {
52
52
  export function makeWatchIgnore(
53
53
  cacheDir: string,
54
54
  outputs: ReadonlyMap<string, readonly string[]> = new Map(),
55
- inputs: ReadonlyMap<string, readonly string[]> = new Map(),
55
+ inputs: ReadonlyMap<string, ReadonlyArray<readonly string[]>> = new Map(),
56
56
  ): (base: string, filename: string) => boolean {
57
57
  const cacheAbs = path.resolve(cacheDir)
58
58
  // A task's own outputs are not edits: without this every cycle that
@@ -82,23 +82,37 @@ export function makeWatchIgnore(
82
82
  // A path some task takes as an INPUT is never an output to ignore, even
83
83
  // when another task declares it one: an in-place formatter declaring
84
84
  // `src/**` hid every `src` edit from a `build` watched beside it, and no
85
- // cycle ran (item 946). Negations are not consulted — they only narrow,
86
- // and a path they would exclude costs one cache-hit cycle.
87
- const read = [...inputs].map(
88
- ([dir, globs]) =>
89
- [
90
- path.resolve(dir),
91
- asTrees(globs.filter((g) => !g.startsWith('!'))).map((g) => taskGlob(g)),
92
- ] as const,
85
+ // cycle ran (item 946). Each task's inputs are judged on their own, less
86
+ // their `!` entries (its own outputs among them): a turbo() task reading
87
+ // `**/*` took its own `dist/` write for an edit, and every save ran one
88
+ // more "up-to-date" cycle (2026-09-28).
89
+ const read = [...inputs].flatMap(([dir, tasks]) =>
90
+ tasks.map(
91
+ (globs) =>
92
+ [
93
+ path.resolve(dir),
94
+ asTrees(globs.filter((g) => !g.startsWith('!'))).map((g) => taskGlob(g)),
95
+ asTrees(globs.filter((g) => g.startsWith('!')).map((g) => g.slice(1))).map((g) =>
96
+ taskGlob(g),
97
+ ),
98
+ // The directory an own output lives in is the task's too (see below).
99
+ globs
100
+ .filter((g) => g.startsWith('!'))
101
+ .map((g) => outputContainer(g.slice(1)))
102
+ .filter((c) => c !== ''),
103
+ ] as const,
104
+ ),
93
105
  )
94
106
  const isInput = (abs: string): boolean =>
95
- read.some(([dir, globs]) => {
107
+ read.some(([dir, globs, not, containers]) => {
96
108
  if (!abs.startsWith(dir + path.sep)) return false
97
109
  const rel = abs
98
110
  .slice(dir.length + 1)
99
111
  .split(path.sep)
100
112
  .join('/')
101
- return globs.some((g) => g.match(rel))
113
+ if (!globs.some((g) => g.match(rel))) return false
114
+ if (not.some((g) => g.match(rel))) return false
115
+ return !containers.some((c) => c === rel || c.startsWith(`${rel}/`))
102
116
  })
103
117
  return (base, filename) => {
104
118
  if (isIgnoredWatchPath(filename)) return true
@@ -56,8 +56,12 @@ export interface ConfigSweep {
56
56
  workspaceWide: boolean
57
57
  workspaceInputs: string[]
58
58
  outputs: Map<string, string[]>
59
- /** Declared input globs per directory they are relative to, the counterweight to `outputs`. */
60
- inputs: Map<string, string[]>
59
+ /**
60
+ * Each task's input globs per directory they are relative to, the
61
+ * counterweight to `outputs`; the task's own outputs ride along as `!`
62
+ * entries, as its key leaves them out.
63
+ */
64
+ inputs: Map<string, string[][]>
61
65
  /** Projects with a task that has a command and no cache: it reads what it likes, git-ignored files included. */
62
66
  uncached: Set<string>
63
67
  /** Files the project configs import by relative path (item 949). */
@@ -86,7 +90,19 @@ export async function sweepConfigs(
86
90
  load: CliLoadOptions = {},
87
91
  ): Promise<ConfigSweep> {
88
92
  const outputs = new Map<string, string[]>()
89
- const inputs = new Map<string, string[]>()
93
+ const inputs = new Map<string, string[][]>()
94
+ // One task's inputs, less what it writes itself: the key leaves a task's
95
+ // own outputs out of its inputs, so a turbo() task reading `**/*` does not
96
+ // read the `dist/` it builds, and its write is no edit to watch.
97
+ const addInputs = (
98
+ dir: string,
99
+ files: readonly string[] | undefined,
100
+ own: readonly string[] | undefined,
101
+ ): void => {
102
+ if (files === undefined || files.length === 0) return
103
+ const minus = (own ?? []).filter((o) => !o.startsWith('!')).map((o) => `!${o}`)
104
+ inputs.set(dir, [...(inputs.get(dir) ?? []), [...files, ...minus]])
105
+ }
90
106
  const addTo = (
91
107
  into: Map<string, string[]>,
92
108
  dir: string,
@@ -104,8 +120,12 @@ export async function sweepConfigs(
104
120
  if (task.cache === undefined && task.exec !== undefined && task.exec.persistent === undefined)
105
121
  uncached.add(dir)
106
122
  for (const g of task.cache?.inputs?.workspaceFiles ?? []) workspaceInputs.add(g)
107
- addTo(inputs, dir, task.cache?.inputs?.files)
108
- addTo(inputs, workspaceRoot, task.cache?.inputs?.workspaceFiles)
123
+ addInputs(dir, task.cache?.inputs?.files, task.cache?.outputs?.files)
124
+ addInputs(
125
+ workspaceRoot,
126
+ task.cache?.inputs?.workspaceFiles,
127
+ task.cache?.outputs?.workspaceFiles,
128
+ )
109
129
  add(dir, task.cache?.outputs?.files)
110
130
  add(workspaceRoot, task.cache?.outputs?.workspaceFiles)
111
131
  }
package/src/cli/watch.ts CHANGED
@@ -272,7 +272,7 @@ interface WatchLoopArgs {
272
272
  /** Declared output globs per directory they are relative to (project dir, or the root for `workspaceFiles`). */
273
273
  outputs: ReadonlyMap<string, readonly string[]>
274
274
  /** Declared input globs per directory: never ignored as another task's output. */
275
- inputs: ReadonlyMap<string, readonly string[]>
275
+ inputs: ReadonlyMap<string, ReadonlyArray<readonly string[]>>
276
276
  /** Projects whose uncached task may read a git-ignored file: such a path there is still an edit. */
277
277
  uncached: ReadonlySet<string>
278
278
  /** Files the configs import: an edit to one outside the watched projects is a cycle that re-reads them. */
@@ -299,7 +299,7 @@ interface Rediscovered {
299
299
  workspaceWide: boolean
300
300
  workspaceInputs: readonly string[]
301
301
  outputs: ReadonlyMap<string, readonly string[]>
302
- inputs: ReadonlyMap<string, readonly string[]>
302
+ inputs: ReadonlyMap<string, ReadonlyArray<readonly string[]>>
303
303
  uncached: ReadonlySet<string>
304
304
  configImports: readonly string[]
305
305
  workspaceConfigImports: readonly string[]
@@ -20,6 +20,7 @@
20
20
  // that implements neither still prefetches, per-hash.
21
21
 
22
22
  import type { TaskNode } from '../graph/index.js'
23
+ import { maskedCommand } from '../util/index.js'
23
24
  import type { CacheLayer, GitFilesCache } from '../cache/index.js'
24
25
  import type { HashCache } from './task-hash.js'
25
26
  import { deriveStableKeys } from './stable-keys.js'
@@ -114,7 +115,9 @@ export function getContext(node: TaskNode, command: string) {
114
115
  const outputs = node.config.cache?.outputs
115
116
  return {
116
117
  taskId: node.id,
117
- command,
118
+ // Stored in the entry row a remote hit inserts: masked as a save's is,
119
+ // or the token a config inlined sat in cache.db (L-27).
120
+ command: maskedCommand(command, node.config.exec?.env),
118
121
  outputs: { files: outputs?.files ?? [], workspaceFiles: outputs?.workspaceFiles ?? [] },
119
122
  }
120
123
  }
@@ -1,9 +1,10 @@
1
1
  // What a finished run leaves behind: the `runs` rows, the `invocations`
2
2
  // header row, and — only when a telemetry sink is active — the per-task
3
3
  // telemetry mirror. One pass over the outcome list builds all three, so
4
- // `invocations.task_count`, the terminal's "N total" and the number of
5
- // telemetry tasks are the same count by construction (`tallyOutcomes`
6
- // applies the same group/aborted filter).
4
+ // `invocations.task_count` and the number of telemetry tasks agree by
5
+ // construction; the terminal's "N total" agrees because `tallyOutcomes`
6
+ // applies the same group/aborted filter, a second copy of the rule that
7
+ // tests/run-records.test.ts holds to this one.
7
8
 
8
9
  import type { InvocationRecord, RunRecord } from '../cache/index.js'
9
10
  import { isGroupTask, type TaskOutcome } from '../graph/index.js'