@vzn/vx 0.0.621 → 0.0.623

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.621",
3
+ "version": "0.0.623",
4
4
  "description": "The fastest 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.621",
25
- "@vzn/vx-linux-arm64": "0.0.621",
26
- "@vzn/vx-darwin-x64": "0.0.621",
27
- "@vzn/vx-darwin-arm64": "0.0.621"
24
+ "@vzn/vx-linux-x64": "0.0.623",
25
+ "@vzn/vx-linux-arm64": "0.0.623",
26
+ "@vzn/vx-darwin-x64": "0.0.623",
27
+ "@vzn/vx-darwin-arm64": "0.0.623"
28
28
  },
29
29
  "dependencies": {
30
30
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
package/src/cli/why.ts CHANGED
@@ -9,7 +9,9 @@ import path from 'node:path'
9
9
  import { Cache } from '../cache/index.js'
10
10
  import { flagHint, formatValue, seeHelp } from './help.js'
11
11
  import { splitTaskId } from '../graph/index.js'
12
+ import type { Database } from 'bun:sqlite'
12
13
  import {
14
+ type CacheKeyDiff,
13
15
  cacheKeyDiff,
14
16
  explainCacheKeyQuery as explainCacheKey,
15
17
  latestRunId,
@@ -232,9 +234,13 @@ export async function whyCmd(args: readonly string[]): Promise<number> {
232
234
  )
233
235
  }
234
236
  const diff = cacheKeyDiff(db, runId, taskId)
237
+ // An upstream change is never the cause, only its carrier: follow
238
+ // each moved dependency in this run down to the tasks whose own
239
+ // inputs moved, so one `vx why` names the file, not the next task.
240
+ const roots = rootCauses(db, runId, taskId, diff.entries)
235
241
 
236
242
  if (parsed.format === 'json') {
237
- process.stdout.write(`${JSON.stringify({ taskId, runId, why, diff })}\n`)
243
+ process.stdout.write(`${JSON.stringify({ taskId, runId, why, diff, roots })}\n`)
238
244
  return 0
239
245
  }
240
246
 
@@ -293,7 +299,22 @@ export async function whyCmd(args: readonly string[]): Promise<number> {
293
299
  ` ${e.change.padEnd(7)} ${e.kind.padEnd(kindW)} ${printable(e.name)} ${beforeAfter}`.trimEnd(),
294
300
  )
295
301
  }
296
- const kinds = [...new Set(diff.entries.map((e) => e.kind))].filter((k) => k in WHAT_TO_DO)
302
+ if (roots.length > 0) {
303
+ lines.push('', ' root cause:')
304
+ for (const r of roots.slice(0, ROOTS_SHOWN)) {
305
+ const what = r.entries.map((e) =>
306
+ e.name === e.kind ? e.kind : `${e.kind} ${printable(e.name)}`,
307
+ )
308
+ lines.push(
309
+ ` ${r.chain.join(' \u2190 ')} \u2190 ${what.slice(0, 3).join(', ')}${what.length > 3 ? ` and ${what.length - 3} more` : ''}`,
310
+ )
311
+ }
312
+ if (roots.length > ROOTS_SHOWN) lines.push(` and ${roots.length - ROOTS_SHOWN} more`)
313
+ }
314
+ const rootKinds = roots.flatMap((r) => r.entries.map((e) => e.kind))
315
+ const kinds = [...new Set([...diff.entries.map((e) => e.kind), ...rootKinds])].filter(
316
+ (k) => k in WHAT_TO_DO && !(k === 'upstream' && roots.length > 0),
317
+ )
297
318
  if (kinds.length > 0) {
298
319
  lines.push('', ' what to do:')
299
320
  // An upstream row names the task to ask next: the command, not a
@@ -321,3 +342,40 @@ export async function whyCmd(args: readonly string[]): Promise<number> {
321
342
  cache.close()
322
343
  }
323
344
  }
345
+
346
+ const ROOTS_SHOWN = 5
347
+
348
+ interface RootCause {
349
+ /** From the asked task down to the one whose own inputs moved. */
350
+ chain: string[]
351
+ entries: CacheKeyDiff['entries']
352
+ }
353
+
354
+ /**
355
+ * The tasks under `taskId`'s moved upstreams whose OWN key components
356
+ * moved in the same run, each with the chain that carried it up. A task
357
+ * visited once is not walked again (a diamond names its root once).
358
+ */
359
+ function rootCauses(
360
+ db: Database,
361
+ runId: string,
362
+ taskId: string,
363
+ entries: CacheKeyDiff['entries'],
364
+ ): RootCause[] {
365
+ const roots: RootCause[] = []
366
+ const seen = new Set([taskId])
367
+ const walk = (chain: string[], moved: CacheKeyDiff['entries']): void => {
368
+ for (const e of moved) {
369
+ if (e.kind !== 'upstream' || seen.has(e.name)) continue
370
+ seen.add(e.name)
371
+ const next = [...chain, e.name]
372
+ const d = cacheKeyDiff(db, runId, e.name)
373
+ if (!d.found) continue
374
+ const own = d.entries.filter((x) => x.kind !== 'upstream')
375
+ if (own.length > 0) roots.push({ chain: next, entries: own })
376
+ walk(next, d.entries)
377
+ }
378
+ }
379
+ walk([taskId], entries)
380
+ return roots
381
+ }
@@ -283,6 +283,7 @@ export function defaultLogger(
283
283
  let restoredLocal = 0
284
284
  let restoredRemote = 0
285
285
  let skippedCount = 0
286
+ let savedMs = 0
286
287
  // Cache-miss duration spread, accumulated incrementally (the same
287
288
  // numbers the final summary computes from the outcome list).
288
289
  let spreadMax = 0
@@ -369,6 +370,7 @@ export function defaultLogger(
369
370
  restoredRemote,
370
371
  miss: succeeded + failed - noCache,
371
372
  noCache,
373
+ savedMs,
372
374
  left: total - done,
373
375
  spread:
374
376
  spreadCount > 0
@@ -698,10 +700,12 @@ export function defaultLogger(
698
700
  case 'cache-hit':
699
701
  if (outcome.restored === false) upToDate++
700
702
  else restoredLocal++
703
+ savedMs += outcome.storedDurationMs ?? 0
701
704
  break
702
705
  case 'cache-hit-remote':
703
706
  if (outcome.restored === false) upToDate++
704
707
  else restoredRemote++
708
+ savedMs += outcome.storedDurationMs ?? 0
705
709
  break
706
710
  case 'skipped':
707
711
  skippedCount++
@@ -6,6 +6,7 @@
6
6
  import type { TaskOutcome } from '../graph/index.js'
7
7
  import { paint, type ColorSupport } from './colors.js'
8
8
  import { tallyOutcomes } from './tally.js'
9
+ import { isCacheHit } from './telemetry.js'
9
10
  import { outcomeLabel, projectOutcome, ranNoCache } from './events.js'
10
11
  import { isGroupTask } from '../graph/index.js'
11
12
  import { formatElapsed } from '../util/index.js'
@@ -119,6 +120,8 @@ export interface SummaryStats {
119
120
  spread: { maxMs: number; minMs: number; sumMs: number; count: number } | null
120
121
  /** Tasks an `admit` policy held with a worker free, and the waits summed. Absent with no hold. */
121
122
  held?: { count: number; sumMs: number }
123
+ /** The hits' stored exec times summed: the work the cache skipped. */
124
+ savedMs?: number
122
125
  /** A run of groups only: their names, so `0 tasks` says why. */
123
126
  emptyGroups?: readonly string[]
124
127
  }
@@ -320,6 +323,10 @@ function resultLine(
320
323
  : `${hits} cached (${Math.floor((hits / cacheable) * 100)}%)`,
321
324
  )
322
325
  if (noCache > 0) parts.push(paint('', `${noCache} no-cache`, colors, { dim: true }))
326
+ // What the cache paid back, beside what the run cost: the stored exec
327
+ // time of every hit, never the restore this run spent on it.
328
+ if ((stats.savedMs ?? 0) >= 1)
329
+ parts.push(paint(SUCCESS, `${formatElapsed(stats.savedMs!)} saved`, colors))
323
330
  parts.push(formatElapsed(totalMs))
324
331
  return parts.join(` ${paint('', '\u00b7', colors, { dim: true })} `)
325
332
  }
@@ -373,6 +380,9 @@ export function formatRunSummary(
373
380
  const durations = outcomes
374
381
  .filter((o) => (o.status === 'success' || o.status === 'failed') && !isGroupTask(o.node))
375
382
  .map((o) => o.durationMs)
383
+ let savedMs = 0
384
+ for (const o of outcomes)
385
+ if (isCacheHit(o.status) && !isGroupTask(o.node)) savedMs += o.storedDurationMs ?? 0
376
386
  const heldOutcomes = outcomes.filter((o) => o.admissionHeldMs !== undefined)
377
387
  const notRun = outcomes.filter((o) => neverStarted(o) && !isGroupTask(o.node)).length
378
388
  const emptyGroups = outcomes.every((o) => isGroupTask(o.node))
@@ -391,6 +401,7 @@ export function formatRunSummary(
391
401
  restoredRemote: t.restoredRemote,
392
402
  miss: t.total - t.skipped - (t.cachedLocal + t.cachedRemote) - noCache,
393
403
  noCache,
404
+ savedMs,
394
405
  spread:
395
406
  durations.length > 0
396
407
  ? {