@vzn/vx 0.0.636 → 0.0.638
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 +39 -11
- package/docs/caching.md +246 -213
- package/docs/cli.md +791 -705
- package/docs/execution.md +68 -76
- package/docs/features.md +47 -42
- package/docs/patterns.md +26 -25
- package/docs/schema.md +392 -205
- package/docs/security.md +76 -39
- package/launcher.sh +43 -0
- package/package.json +5 -5
- package/schemas/cache.json +41 -19
- package/schemas/info.json +23 -11
- package/schemas/last.json +48 -2
- package/schemas/lock.json +1 -1
- package/schemas/plan.json +4 -4
- package/schemas/show.json +5 -3
- package/schemas/why.json +15 -1
- package/skills/vx/SKILL.md +28 -13
- package/src/bin.ts +30 -9
- package/src/cache/archive.ts +11 -0
- package/src/cache/cache.ts +124 -53
- package/src/cache/chained-cache.ts +4 -0
- package/src/cache/git-inputs.ts +277 -63
- package/src/cache/index.ts +15 -1
- package/src/cache/inputs.ts +102 -36
- package/src/cache/key-fold.ts +8 -7
- package/src/cache/layer.ts +15 -18
- package/src/cache/layered-cache.ts +20 -102
- package/src/cache/plugin-cache.ts +248 -0
- package/src/cache/policy.ts +32 -27
- package/src/cache/run-history.ts +104 -14
- package/src/cache/run-store.ts +77 -0
- package/src/cache/schema.ts +58 -31
- package/src/cache/scoped-cache.ts +237 -0
- package/src/cli/cache.ts +173 -34
- package/src/cli/completions.ts +11 -7
- package/src/cli/core-alias.ts +1 -1
- package/src/cli/foreign-flags.ts +13 -12
- package/src/cli/help.ts +110 -70
- package/src/cli/index.ts +67 -24
- package/src/cli/info.ts +43 -27
- package/src/cli/init.ts +115 -407
- package/src/cli/last.ts +157 -9
- package/src/cli/lock.ts +356 -122
- package/src/cli/plan-format.ts +7 -5
- package/src/cli/plugin-commands.ts +81 -14
- package/src/cli/plugin-templates.ts +11 -11
- package/src/cli/run.ts +116 -160
- package/src/cli/select.ts +50 -47
- package/src/cli/show.ts +83 -59
- package/src/cli/task-verb.ts +6 -13
- package/src/cli/upgrade.ts +415 -149
- package/src/cli/watch-cycle.ts +9 -2
- package/src/cli/watch-filter.ts +39 -61
- package/src/cli/watch-fs.ts +29 -16
- package/src/cli/watch-judge.ts +18 -4
- package/src/cli/watch-set.ts +16 -42
- package/src/cli/watch.ts +227 -230
- package/src/cli/why.ts +38 -18
- package/src/cli/workspace-config.ts +33 -70
- package/src/config.ts +169 -70
- package/src/exec/env.ts +1 -31
- package/src/exec/executor.ts +1 -5
- package/src/exec/kill-tree.ts +2 -1
- package/src/exec/sandbox-runtime.ts +12 -1
- package/src/exec/sandbox-violations.ts +9 -1
- package/src/graph/dependency-spec.ts +1 -1
- package/src/graph/index.ts +1 -0
- package/src/graph/priorities.ts +1 -1
- package/src/graph/scheduler.ts +27 -7
- package/src/graph/task-graph.ts +76 -24
- package/src/handoff.ts +169 -0
- package/src/index.ts +35 -58
- package/src/orchestrator/affected-tasks.ts +15 -8
- package/src/orchestrator/command-windows.ts +44 -0
- package/src/orchestrator/deferred-outputs.ts +12 -10
- package/src/orchestrator/doctor.ts +98 -110
- package/src/orchestrator/download-policy.ts +3 -3
- package/src/orchestrator/events.ts +19 -3
- package/src/orchestrator/excluded-keys.ts +0 -2
- package/src/orchestrator/execute-task.ts +147 -69
- package/src/orchestrator/failure-mode.ts +19 -10
- package/src/orchestrator/fingerprint-watch.ts +18 -15
- package/src/orchestrator/forecast.ts +10 -2
- package/src/orchestrator/framed-output.ts +9 -5
- package/src/orchestrator/graph-stage.ts +5 -5
- package/src/orchestrator/history.ts +126 -30
- package/src/orchestrator/hit-restore.ts +15 -3
- package/src/orchestrator/index.ts +20 -5
- package/src/orchestrator/local-shortcircuit.ts +2 -1
- package/src/orchestrator/lockfile-claim.ts +79 -20
- package/src/orchestrator/logger.ts +80 -41
- package/src/orchestrator/metrics.ts +78 -9
- package/src/orchestrator/miss-save.ts +3 -0
- package/src/orchestrator/options.ts +42 -29
- package/src/orchestrator/output-host.ts +72 -0
- package/src/orchestrator/plan.ts +6 -8
- package/src/orchestrator/plugin-host.ts +100 -66
- package/src/orchestrator/plugin.ts +173 -67
- package/src/orchestrator/prepare.ts +163 -78
- package/src/orchestrator/projects.ts +105 -99
- package/src/orchestrator/remote-prefetch.ts +11 -6
- package/src/orchestrator/run-context.ts +25 -6
- package/src/orchestrator/run-failures.ts +17 -8
- package/src/orchestrator/run-lock.ts +1 -1
- package/src/orchestrator/run-records.ts +1 -1
- package/src/orchestrator/run-report.ts +9 -9
- package/src/orchestrator/run-settings.ts +48 -0
- package/src/orchestrator/run.ts +244 -119
- package/src/orchestrator/sandbox-fix.ts +39 -20
- package/src/orchestrator/sandbox-request.ts +84 -13
- package/src/orchestrator/signals.ts +1 -1
- package/src/orchestrator/stable-keys.ts +13 -11
- package/src/orchestrator/status-line.ts +49 -7
- package/src/orchestrator/summary.ts +115 -106
- package/src/orchestrator/task-hash.ts +12 -83
- package/src/orchestrator/telemetry.ts +4 -4
- package/src/settings.ts +428 -0
- package/src/{orchestrator → util}/colors.ts +5 -0
- package/src/util/config-frame.ts +31 -2
- package/src/util/errors.ts +33 -1
- package/src/util/essential-env.ts +31 -0
- package/src/util/hide-env.ts +107 -0
- package/src/util/index.ts +16 -3
- package/src/util/paths.ts +10 -0
- package/src/util/refusal-notice.ts +111 -0
- package/src/util/report.ts +31 -0
- package/src/util/settle.ts +49 -30
- package/src/util/size.ts +23 -4
- package/src/util/timing.ts +3 -2
- package/src/util/ui.ts +186 -0
- package/src/util/ulid.ts +1 -1
- package/src/util/verbs.ts +0 -15
- package/src/workspace/affected.ts +84 -232
- package/src/workspace/config-eval.ts +15 -20
- package/src/workspace/config-imports.ts +78 -3
- package/src/workspace/config-schema.ts +209 -268
- package/src/workspace/dependency-rules.ts +65 -0
- package/src/workspace/filter.ts +2 -2
- package/src/workspace/fingerprint.ts +11 -121
- package/src/workspace/index.ts +20 -27
- package/src/workspace/load-reads.ts +4 -4
- package/src/workspace/lockfile.ts +96 -140
- package/src/workspace/migration.ts +307 -215
- package/src/workspace/package-graph.ts +30 -216
- package/src/workspace/project-loader.ts +80 -285
- package/src/workspace/repo-id.ts +2 -1
- package/src/workspace/workspace.ts +363 -831
- package/src/cache/config-evals.ts +0 -130
- package/src/workspace/config-cache.ts +0 -661
- package/src/workspace/migrate-scripts.ts +0 -1121
|
@@ -4,56 +4,31 @@
|
|
|
4
4
|
// aren't real work and would inflate "N total" misleadingly.
|
|
5
5
|
|
|
6
6
|
import type { TaskOutcome } from '../graph/index.js'
|
|
7
|
-
import { paint, type ColorSupport } from './colors.js'
|
|
8
7
|
import { tallyOutcomes } from './tally.js'
|
|
9
8
|
import { isCacheHit } from './telemetry.js'
|
|
10
9
|
import { outcomeLabel, projectOutcome, ranNoCache } from './events.js'
|
|
11
10
|
import { isGroupTask } from '../graph/index.js'
|
|
12
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
ERROR,
|
|
13
|
+
formatElapsed,
|
|
14
|
+
gradientRule,
|
|
15
|
+
paint,
|
|
16
|
+
SUCCESS,
|
|
17
|
+
WARN,
|
|
18
|
+
type ColorSupport,
|
|
19
|
+
} from '../util/index.js'
|
|
13
20
|
|
|
14
21
|
const NO_COLOR: ColorSupport = { enabled: false }
|
|
15
22
|
|
|
16
|
-
const SUCCESS = '#22c55e'
|
|
17
|
-
const WARN = '#eab308'
|
|
18
23
|
// Cache provenance hues (owner): local light blue, remote dark blue —
|
|
19
24
|
// yellow is skipped's color on both meters.
|
|
20
25
|
const LOCAL = '#38bdf8' // sky-400
|
|
21
26
|
const REMOTE = '#2563eb' // blue-600
|
|
22
|
-
const ERROR = '#ef4444'
|
|
23
27
|
const ABORTED = '#f97316' // orange-500: red's neighbour, not a failure
|
|
28
|
+
const NOT_RUN = '#a855f7' // purple-500: stopped before it began, apart from every other state
|
|
24
29
|
const ACCENT = '#06b6d4' // cyan-500: the live region's running word
|
|
25
30
|
|
|
26
31
|
const BAR_WIDTH = 50
|
|
27
|
-
// Brand gradient for the rule: identity violet → pink (the project /
|
|
28
|
-
// task hues), faded across the dash run. Plain dashes when colors
|
|
29
|
-
// are off.
|
|
30
|
-
const GRADIENT_FROM = [0xa7, 0x8b, 0xfa] as const
|
|
31
|
-
const GRADIENT_TO = [0xf4, 0x72, 0xb6] as const
|
|
32
|
-
// Rule ends flush with the bars: 2 indent + 8 label + 2 gap + 50 cells.
|
|
33
|
-
const RULE_DASHES = 57
|
|
34
|
-
|
|
35
|
-
/** Total visible rule width: `\u2500 <mark> ` + dashes. Shared by summary + header. */
|
|
36
|
-
function gradientRule(colors: ColorSupport, mark = 'vx'): string {
|
|
37
|
-
const head = `\u2500 `
|
|
38
|
-
const dashes = '\u2500'.repeat(Math.max(8, RULE_DASHES + 2 - mark.length))
|
|
39
|
-
if (!colors.enabled) return `${head}${mark} ${dashes}`
|
|
40
|
-
const chunks = 8
|
|
41
|
-
const per = Math.ceil(RULE_DASHES / chunks)
|
|
42
|
-
let out = ''
|
|
43
|
-
for (let i = 0; i < chunks; i++) {
|
|
44
|
-
const seg = dashes.slice(i * per, (i + 1) * per)
|
|
45
|
-
if (seg.length === 0) break
|
|
46
|
-
const f = i / (chunks - 1)
|
|
47
|
-
const hex = `#${GRADIENT_FROM.map((c, j) =>
|
|
48
|
-
Math.round(c + (GRADIENT_TO[j]! - c) * f)
|
|
49
|
-
.toString(16)
|
|
50
|
-
.padStart(2, '0'),
|
|
51
|
-
).join('')}`
|
|
52
|
-
out += paint(hex, seg, colors)
|
|
53
|
-
}
|
|
54
|
-
return `${paint(`#${GRADIENT_FROM.map((c) => c.toString(16).padStart(2, '0')).join('')}`, head, colors)}${paint('', mark, colors, { bold: true })} ${out}`
|
|
55
|
-
}
|
|
56
|
-
|
|
57
32
|
/**
|
|
58
33
|
* Stacked state meter: one ▰ run per bucket, proportional cells via
|
|
59
34
|
* largest-remainder, every non-zero bucket guaranteed at least one
|
|
@@ -110,6 +85,10 @@ export interface SummaryStats {
|
|
|
110
85
|
upToDate: number
|
|
111
86
|
restoredLocal: number
|
|
112
87
|
restoredRemote: number
|
|
88
|
+
/** Ran here and saved to the cache: the legend's `↑`. */
|
|
89
|
+
saved?: number
|
|
90
|
+
/** Ran here with a `cache` block, succeeded, and saved nothing. */
|
|
91
|
+
notSaved?: number
|
|
113
92
|
/** Tasks that executed (success + failed) with a `cache` block — the misses. */
|
|
114
93
|
miss: number
|
|
115
94
|
/** Executed with no `cache` block: never consulted the cache, not a miss. */
|
|
@@ -137,11 +116,26 @@ export interface SummaryStats {
|
|
|
137
116
|
*/
|
|
138
117
|
export interface RunContext {
|
|
139
118
|
version: string
|
|
140
|
-
/** Projects covered by the graph — the "
|
|
119
|
+
/** Projects covered by the graph — the "affected" half of the bar. */
|
|
141
120
|
packageCount: number
|
|
142
121
|
/** Worker-pool size for this run; shown on the `info` row. */
|
|
143
122
|
concurrency?: number
|
|
144
123
|
remoteCacheEnabled: boolean
|
|
124
|
+
/**
|
|
125
|
+
* The cache layers the run used, so the legend reads `local 1= 2↓ 4↑ ·
|
|
126
|
+
* remote 3↓ 4↑`. Absent: the plain `N miss` legend.
|
|
127
|
+
*/
|
|
128
|
+
cache?: {
|
|
129
|
+
/** The run's policy reads or writes the local layer. */
|
|
130
|
+
local: boolean
|
|
131
|
+
localWrite: boolean
|
|
132
|
+
/** A remote layer exists and the policy reads or writes it. */
|
|
133
|
+
remote: boolean
|
|
134
|
+
/** The run's uploads, filled as each lands (a live ref). */
|
|
135
|
+
uploads: { count: number; failed: number }
|
|
136
|
+
/** Tasks whose outputs are still in the remote store. */
|
|
137
|
+
notDownloaded: () => number
|
|
138
|
+
}
|
|
145
139
|
/** Total projects discovery found — the bar's denominator. */
|
|
146
140
|
workspaceProjectCount?: number
|
|
147
141
|
}
|
|
@@ -164,25 +158,25 @@ export function formatSummarySection(
|
|
|
164
158
|
const row = (label: string, value: string): string => ` ${dim(label.padEnd(8))} ${value}`
|
|
165
159
|
const join = (parts: string[]): string => parts.join(` ${dim('\u00b7')} `)
|
|
166
160
|
|
|
167
|
-
// Tasks meter + numbers, fixed order: failed · success · skipped
|
|
168
|
-
// then a dim total
|
|
161
|
+
// Tasks meter + numbers, fixed order: failed · success · skipped ·
|
|
162
|
+
// aborted · not run, then a dim total, last on its line. Every task in
|
|
163
|
+
// the run counts, each state its own segment (M, 2026-10-14).
|
|
164
|
+
const runTotal = stats.total + aborted + notRun
|
|
169
165
|
const taskParts: string[] = []
|
|
170
166
|
if (stats.failed > 0)
|
|
171
167
|
taskParts.push(paint(ERROR, `${stats.failed} failed`, colors, { bold: true }))
|
|
172
168
|
if (stats.successful > 0) taskParts.push(paint(SUCCESS, `${stats.successful} success`, colors))
|
|
173
169
|
if (stats.skipped > 0) taskParts.push(paint(WARN, `${stats.skipped} skipped`, colors))
|
|
174
|
-
if (
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const uncounted: string[] = []
|
|
178
|
-
if (aborted > 0) uncounted.push(paint(ABORTED, `${aborted} aborted`, colors))
|
|
179
|
-
if (notRun > 0) uncounted.push(dim(`${notRun} not run`))
|
|
180
|
-
if (uncounted.length > 0) taskParts.push(`${dim('not counted:')} ${uncounted.join(dim(', '))}`)
|
|
170
|
+
if (aborted > 0) taskParts.push(paint(ABORTED, `${aborted} aborted`, colors))
|
|
171
|
+
if (notRun > 0) taskParts.push(paint(NOT_RUN, `${notRun} not run`, colors))
|
|
172
|
+
if (runTotal > 0) taskParts.push(dim(`${runTotal} total`))
|
|
181
173
|
const taskBar = segmentBar(
|
|
182
174
|
[
|
|
183
175
|
{ n: stats.failed, color: ERROR },
|
|
184
176
|
{ n: stats.successful, color: SUCCESS },
|
|
185
177
|
{ n: stats.skipped, color: WARN },
|
|
178
|
+
{ n: aborted, color: ABORTED },
|
|
179
|
+
{ n: notRun, color: NOT_RUN },
|
|
186
180
|
remainder,
|
|
187
181
|
],
|
|
188
182
|
colors,
|
|
@@ -211,7 +205,7 @@ export function formatSummarySection(
|
|
|
211
205
|
paint('', '▱'.repeat(BAR_WIDTH - cells), colors, { dim: true })
|
|
212
206
|
lines.push(
|
|
213
207
|
row('projects', bar),
|
|
214
|
-
legend([paint(WARN, `${context.packageCount}
|
|
208
|
+
legend([paint(WARN, `${context.packageCount} affected`, colors), dim(`${wsTotal} total`)]),
|
|
215
209
|
)
|
|
216
210
|
}
|
|
217
211
|
}
|
|
@@ -233,8 +227,8 @@ export function formatSummarySection(
|
|
|
233
227
|
if (taskParts.length > 0) lines.push(legend(taskParts))
|
|
234
228
|
}
|
|
235
229
|
|
|
236
|
-
// Cache meter + numbers
|
|
237
|
-
// remote.
|
|
230
|
+
// Cache meter + numbers: per layer when the run context names its
|
|
231
|
+
// layers, else miss · no-cache · up-to-date · local · remote.
|
|
238
232
|
if (miss + noCache + hits + left + stats.skipped > 0) {
|
|
239
233
|
const cacheBar = segmentBar(
|
|
240
234
|
[
|
|
@@ -249,34 +243,80 @@ export function formatSummarySection(
|
|
|
249
243
|
colors,
|
|
250
244
|
)
|
|
251
245
|
const cacheParts: string[] = []
|
|
252
|
-
|
|
253
|
-
if (
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
246
|
+
const layers = context?.cache
|
|
247
|
+
if (layers !== undefined) {
|
|
248
|
+
// Per layer (M, 2026-10-15): `●` fresh (outputs already on disk, from
|
|
249
|
+
// either layer) and `○` no cache, both local; `↓` restored from a
|
|
250
|
+
// layer, `↑` saved to it.
|
|
251
|
+
const n = (count: number, glyph: string): string[] => (count > 0 ? [`${count}${glyph}`] : [])
|
|
252
|
+
const local = [
|
|
253
|
+
...(layers.local ? n(stats.upToDate, '\u25cf') : []),
|
|
254
|
+
...n(noCache, '\u25cb'),
|
|
255
|
+
...(layers.local ? n(stats.restoredLocal, '\u2193') : []),
|
|
256
|
+
...(layers.localWrite ? n(stats.saved ?? 0, '\u2191') : []),
|
|
257
|
+
]
|
|
258
|
+
if (local.length > 0) cacheParts.push(paint(LOCAL, `local ${local.join(' ')}`, colors))
|
|
259
|
+
const remote = [...n(stats.restoredRemote, '\u2193'), ...n(layers.uploads.count, '\u2191')]
|
|
260
|
+
if (layers.remote && remote.length > 0)
|
|
261
|
+
cacheParts.push(paint(REMOTE, `remote ${remote.join(' ')}`, colors))
|
|
262
|
+
if (layers.uploads.failed > 0)
|
|
263
|
+
cacheParts.push(paint(ERROR, `${layers.uploads.failed} \u2191 failed`, colors))
|
|
264
|
+
if ((stats.notSaved ?? 0) > 0)
|
|
265
|
+
cacheParts.push(paint(WARN, `${stats.notSaved} not saved`, colors))
|
|
266
|
+
const notDownloaded = layers.notDownloaded()
|
|
267
|
+
if (notDownloaded > 0) cacheParts.push(paint(WARN, `${notDownloaded} not downloaded`, colors))
|
|
268
|
+
} else {
|
|
269
|
+
if (miss > 0) cacheParts.push(paint(ERROR, `${miss} miss`, colors))
|
|
270
|
+
if (noCache > 0) cacheParts.push(paint('', `${noCache} no-cache`, colors, { dim: true }))
|
|
271
|
+
if (stats.upToDate > 0)
|
|
272
|
+
cacheParts.push(paint(SUCCESS, `${stats.upToDate} up-to-date`, colors))
|
|
273
|
+
if (stats.restoredLocal > 0)
|
|
274
|
+
cacheParts.push(paint(LOCAL, `${stats.restoredLocal} local`, colors))
|
|
275
|
+
if (stats.restoredRemote > 0)
|
|
276
|
+
cacheParts.push(paint(REMOTE, `${stats.restoredRemote} remote`, colors))
|
|
277
|
+
}
|
|
278
|
+
// Skipped tasks ride the bar, yellow like the tasks meter, so the two
|
|
279
|
+
// bars fill alike; their count is the tasks legend's alone (the footer
|
|
280
|
+
// never says a fact twice, M, 2026-10-14).
|
|
264
281
|
lines.push(row('cache', cacheBar))
|
|
265
282
|
if (cacheParts.length > 0) lines.push(legend(cacheParts))
|
|
266
283
|
}
|
|
267
284
|
|
|
268
285
|
// Cache-miss duration spread (owner: hits' restore times polluted
|
|
269
286
|
// it — only tasks that actually executed count). Dim: context.
|
|
270
|
-
|
|
271
|
-
|
|
287
|
+
// A value already on the row is not said again: one executed task read
|
|
288
|
+
// as its own max, avg and min (M, 2026-10-14). Tasks that all took the
|
|
289
|
+
// same time have no spread to show.
|
|
290
|
+
const total = formatElapsed(totalMs)
|
|
291
|
+
const said = new Set([total])
|
|
292
|
+
const spreadParts: string[] = []
|
|
293
|
+
if (
|
|
294
|
+
stats.spread !== null &&
|
|
295
|
+
formatElapsed(stats.spread.maxMs) !== formatElapsed(stats.spread.minMs)
|
|
296
|
+
) {
|
|
272
297
|
const { maxMs, minMs, sumMs, count } = stats.spread
|
|
273
|
-
|
|
298
|
+
for (const [label, ms] of [
|
|
299
|
+
['max', maxMs],
|
|
300
|
+
['avg', sumMs / count],
|
|
301
|
+
['min', minMs],
|
|
302
|
+
] as const) {
|
|
303
|
+
const v = formatElapsed(ms)
|
|
304
|
+
if (said.has(v)) continue
|
|
305
|
+
said.add(v)
|
|
306
|
+
spreadParts.push(`${label} ${v}`)
|
|
307
|
+
}
|
|
274
308
|
}
|
|
309
|
+
const timeParts = [total]
|
|
310
|
+
// What the cache paid back, beside what the run cost: the stored exec
|
|
311
|
+
// time of every hit, never the restore this run spent on it.
|
|
312
|
+
if ((stats.savedMs ?? 0) >= 1)
|
|
313
|
+
timeParts.push(paint(SUCCESS, `${formatElapsed(stats.savedMs!)} saved`, colors))
|
|
275
314
|
// The live run's forecast leads the spread: the one number on the row
|
|
276
315
|
// that is about the future.
|
|
277
|
-
if (stats.etaMs !== undefined)
|
|
278
|
-
|
|
279
|
-
|
|
316
|
+
if (stats.etaMs !== undefined)
|
|
317
|
+
timeParts.push(paint(ACCENT, `${formatEta(stats.etaMs)} left`, colors))
|
|
318
|
+
for (const p of spreadParts) timeParts.push(dim(p))
|
|
319
|
+
const timeRow = row('time', join(timeParts))
|
|
280
320
|
// Run-shape footer (final summary only): worker pool + cache mode,
|
|
281
321
|
// grouped with time under a blank line below the meters.
|
|
282
322
|
if (context !== undefined) {
|
|
@@ -294,12 +334,9 @@ export function formatSummarySection(
|
|
|
294
334
|
`admit held ${stats.held.count} task${stats.held.count === 1 ? '' : 's'}, ${formatElapsed(stats.held.sumMs)} in all`,
|
|
295
335
|
)
|
|
296
336
|
}
|
|
297
|
-
lines.push('', row('info', join(info)),
|
|
298
|
-
// The run in one line, last, where the eye lands: what a Turbo user
|
|
299
|
-
// reads first in its own summary (tasks, cached, time), in vx's words.
|
|
300
|
-
if (stats.total > 0) lines.push(row('result', resultLine(stats, hits, totalMs, colors)))
|
|
337
|
+
lines.push('', row('info', join(info)), timeRow)
|
|
301
338
|
} else {
|
|
302
|
-
lines.push('',
|
|
339
|
+
lines.push('', timeRow)
|
|
303
340
|
}
|
|
304
341
|
return lines
|
|
305
342
|
}
|
|
@@ -315,41 +352,6 @@ function formatEta(ms: number): string {
|
|
|
315
352
|
return `~${Math.floor(s / 60)}m ${String(s % 60).padStart(2, '0')}s`
|
|
316
353
|
}
|
|
317
354
|
|
|
318
|
-
/**
|
|
319
|
-
* `42 tasks · 38 cached (90%) · 3.2s`, `2 tasks · all cached · 23ms`,
|
|
320
|
-
* with the failures first when there are any. Cached is every hit (up to
|
|
321
|
-
* date, restored locally or remotely) over every task that consulted a
|
|
322
|
-
* cache: a skipped or not-yet-run task never did.
|
|
323
|
-
*/
|
|
324
|
-
function resultLine(
|
|
325
|
-
stats: SummaryStats,
|
|
326
|
-
hits: number,
|
|
327
|
-
totalMs: number,
|
|
328
|
-
colors: ColorSupport,
|
|
329
|
-
): string {
|
|
330
|
-
const parts = [`${stats.total} task${stats.total === 1 ? '' : 's'}`]
|
|
331
|
-
if (stats.failed > 0) parts.push(paint(ERROR, `${stats.failed} failed`, colors, { bold: true }))
|
|
332
|
-
// A task with no `cache` block could never hit: it is counted apart, not
|
|
333
|
-
// as a miss, or `vx run dev` would read "0 cached (0%)". A skipped task
|
|
334
|
-
// never asked either, and counted as a miss it put "0 cached (0%)" on a
|
|
335
|
-
// run in which no task has a cache.
|
|
336
|
-
const noCache = stats.noCache ?? 0
|
|
337
|
-
const cacheable = stats.total - noCache - stats.skipped - (stats.left ?? 0)
|
|
338
|
-
if (cacheable > 0)
|
|
339
|
-
parts.push(
|
|
340
|
-
hits === cacheable
|
|
341
|
-
? paint(SUCCESS, 'all cached', colors, { bold: true })
|
|
342
|
-
: `${hits} cached (${Math.floor((hits / cacheable) * 100)}%)`,
|
|
343
|
-
)
|
|
344
|
-
if (noCache > 0) parts.push(paint('', `${noCache} no-cache`, colors, { dim: true }))
|
|
345
|
-
// What the cache paid back, beside what the run cost: the stored exec
|
|
346
|
-
// time of every hit, never the restore this run spent on it.
|
|
347
|
-
if ((stats.savedMs ?? 0) >= 1)
|
|
348
|
-
parts.push(paint(SUCCESS, `${formatElapsed(stats.savedMs!)} saved`, colors))
|
|
349
|
-
parts.push(formatElapsed(totalMs))
|
|
350
|
-
return parts.join(` ${paint('', '\u00b7', colors, { dim: true })} `)
|
|
351
|
-
}
|
|
352
|
-
|
|
353
355
|
/**
|
|
354
356
|
* The `--verbosity 1` per-task table (`RunOptions.summaryTable`). Groups are
|
|
355
357
|
* left out as the footer and `--report` leave them: a group ran no command.
|
|
@@ -380,6 +382,11 @@ export function formatOutcomeTable(outcomes: readonly TaskOutcome[]): string[] {
|
|
|
380
382
|
|
|
381
383
|
// Local byte formatter — the orchestrator can't import cli/format.ts
|
|
382
384
|
// (module boundary), and the footer only needs one compact form.
|
|
385
|
+
/** Ran here with a `cache` block, succeeded, and its result was not saved. */
|
|
386
|
+
export function ranUnsaved(o: TaskOutcome): boolean {
|
|
387
|
+
return o.status === 'success' && !ranNoCache(o) && o.saved !== true && o.where === undefined
|
|
388
|
+
}
|
|
389
|
+
|
|
383
390
|
export function formatRunSummary(
|
|
384
391
|
outcomes: readonly TaskOutcome[],
|
|
385
392
|
totalMs: number,
|
|
@@ -418,6 +425,8 @@ export function formatRunSummary(
|
|
|
418
425
|
upToDate: t.upToDate,
|
|
419
426
|
restoredLocal: t.restoredLocal,
|
|
420
427
|
restoredRemote: t.restoredRemote,
|
|
428
|
+
saved: outcomes.filter((o) => o.saved === true && !isGroupTask(o.node)).length,
|
|
429
|
+
notSaved: outcomes.filter((o) => !isGroupTask(o.node) && ranUnsaved(o)).length,
|
|
421
430
|
miss: t.total - t.skipped - (t.cachedLocal + t.cachedRemote) - noCache,
|
|
422
431
|
noCache,
|
|
423
432
|
savedMs,
|
|
@@ -21,12 +21,8 @@ import { isUserError, relPosix, secretMask, span, UserError, xxh3hex } from '../
|
|
|
21
21
|
import { expandGroupUpstream, filterUpstreamHashes } from './upstream.js'
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
|
-
* Per-run memoization caches for
|
|
25
|
-
*
|
|
26
|
-
* - `packageJson`: keyed by absolute projectDir. Every task in a
|
|
27
|
-
* project re-reads the same `package.json`; without this, a
|
|
28
|
-
* monorepo with N projects × M tasks per project does N×M reads
|
|
29
|
-
* of the same bytes.
|
|
24
|
+
* Per-run memoization caches for what doesn't change across tasks within
|
|
25
|
+
* one run:
|
|
30
26
|
* - `projectFiles`: keyed by the project and its input DECLARATION.
|
|
31
27
|
* Tasks of one project that declare the same inputs and outputs
|
|
32
28
|
* resolve the same file list; see `ProjectFilesCache`.
|
|
@@ -35,11 +31,10 @@ import { expandGroupUpstream, filterUpstreamHashes } from './upstream.js'
|
|
|
35
31
|
* JSON.stringify + xxh3 of it is deterministic. WeakMap so
|
|
36
32
|
* entries free when the orchestrator is done.
|
|
37
33
|
*
|
|
38
|
-
*
|
|
39
|
-
* fresh when
|
|
34
|
+
* The whole memo is optional — the helpers fall back to computing
|
|
35
|
+
* fresh when it is missing.
|
|
40
36
|
*/
|
|
41
37
|
export interface HashCache {
|
|
42
|
-
packageJson: Map<string, Promise<string>>
|
|
43
38
|
taskConfig: WeakMap<TaskConfig, string>
|
|
44
39
|
runtime: Map<string, Promise<string>>
|
|
45
40
|
workspaceRuntime: Map<string, Promise<string>>
|
|
@@ -49,7 +44,6 @@ export interface HashCache {
|
|
|
49
44
|
|
|
50
45
|
export function createHashCache(): HashCache {
|
|
51
46
|
return {
|
|
52
|
-
packageJson: new Map(),
|
|
53
47
|
taskConfig: new WeakMap(),
|
|
54
48
|
runtime: new Map(),
|
|
55
49
|
workspaceRuntime: new Map(),
|
|
@@ -76,7 +70,6 @@ export interface ComputeHashArgs {
|
|
|
76
70
|
node: TaskNode
|
|
77
71
|
upstream: TaskOutcome[]
|
|
78
72
|
workspaceRoot: string
|
|
79
|
-
workspaceFingerprint: string
|
|
80
73
|
cache: CacheLayer
|
|
81
74
|
forwardArgs?: readonly string[] | undefined
|
|
82
75
|
nestedProjectDirs: string[]
|
|
@@ -100,8 +93,7 @@ export interface ComputeHashArgs {
|
|
|
100
93
|
|
|
101
94
|
/**
|
|
102
95
|
* Resolve every input the cache key depends on (file content, env
|
|
103
|
-
* values, upstream hashes, config bytes,
|
|
104
|
-
* forwardArgs) and ask the cache layer to combine them. Used by both
|
|
96
|
+
* values, upstream hashes, config bytes, forwardArgs) and ask the cache layer to combine them. Used by both
|
|
105
97
|
* `executeTask` for the real run and `plan()` for `--dry-run` /
|
|
106
98
|
* `--graph` previews.
|
|
107
99
|
*
|
|
@@ -161,22 +153,12 @@ export async function describeTaskInputs(args: ComputeHashArgs): Promise<{
|
|
|
161
153
|
? []
|
|
162
154
|
: [{ path: relPosix(input.workspaceRoot, f), digest: digests[i]! }],
|
|
163
155
|
)
|
|
164
|
-
// The package.json digest is a per-run memo that may predate this
|
|
165
|
-
// describe, so it is dated from the enumeration, the earliest a run
|
|
166
|
-
// learns anything.
|
|
167
156
|
const indexedAt = args.gitFilesCache?.enumeratedAtMs ?? 0
|
|
168
157
|
const facts: InputFact[] = sorted.map((f, i) => ({
|
|
169
158
|
path: f,
|
|
170
159
|
digest: digests[i]!,
|
|
171
160
|
since: input.fileHashes?.has(f) === true ? indexedAt : hashedAt,
|
|
172
161
|
}))
|
|
173
|
-
if (input.projectPackageJsonHash !== '') {
|
|
174
|
-
facts.push({
|
|
175
|
-
path: path.join(args.node.projectDir, 'package.json'),
|
|
176
|
-
digest: input.projectPackageJsonHash,
|
|
177
|
-
since: indexedAt,
|
|
178
|
-
})
|
|
179
|
-
}
|
|
180
162
|
// Groups expanded: this list describes the INPUT ROOT, not the key, and a
|
|
181
163
|
// group produces nothing of its own (see `CacheKeyInput.upstreamGraft`).
|
|
182
164
|
// Sorted by hash so the list — and any action digest derived from it — is
|
|
@@ -220,9 +202,7 @@ export async function describeTaskInputs(args: ComputeHashArgs): Promise<{
|
|
|
220
202
|
get upstream() {
|
|
221
203
|
return (upstream ??= listUpstream())
|
|
222
204
|
},
|
|
223
|
-
packageJsonDigest: input.projectPackageJsonHash,
|
|
224
205
|
configDigest: input.taskConfigHash,
|
|
225
|
-
workspaceFingerprint: input.workspaceFingerprint,
|
|
226
206
|
},
|
|
227
207
|
}
|
|
228
208
|
}
|
|
@@ -238,15 +218,17 @@ export async function describeTaskInputs(args: ComputeHashArgs): Promise<{
|
|
|
238
218
|
* input changed and changed BACK while the command ran matched its digest
|
|
239
219
|
* again, and the entry saved the other content's output under the key (item
|
|
240
220
|
* 1015). A write before it — an upstream's — is judged by content, as before.
|
|
241
|
-
* `
|
|
242
|
-
*
|
|
243
|
-
*
|
|
221
|
+
* `byBytes` (`inputStability.strategy: 'mutable'`) judges a write during
|
|
222
|
+
* the command by content too, when it answers true for that file and its
|
|
223
|
+
* ctime: the task says the same bytes are its own identical rewrite
|
|
224
|
+
* (tsdown's package.json), not an edit undone, unless another command that
|
|
225
|
+
* may write the file ran until then (command-windows.ts).
|
|
244
226
|
*/
|
|
245
227
|
export async function movedInput(
|
|
246
228
|
facts: readonly InputFact[],
|
|
247
229
|
cache: CacheLayer,
|
|
248
230
|
commandFrom?: number,
|
|
249
|
-
|
|
231
|
+
byBytes?: (file: string, ctimeMs: number) => boolean,
|
|
250
232
|
): Promise<string | undefined> {
|
|
251
233
|
const suspects: InputFact[] = []
|
|
252
234
|
for (const f of facts) {
|
|
@@ -264,7 +246,7 @@ export async function movedInput(
|
|
|
264
246
|
// an even one, FAT's tick), one during the command included
|
|
265
247
|
// (`racyWindowMs`).
|
|
266
248
|
if (commandFrom !== undefined && ctimeMs >= commandFrom - racyWindowMs(ctimeMs, 0)) {
|
|
267
|
-
if (
|
|
249
|
+
if (byBytes?.(f.path, ctimeMs) !== true) return f.path
|
|
268
250
|
suspects.push(f)
|
|
269
251
|
continue
|
|
270
252
|
}
|
|
@@ -344,12 +326,6 @@ async function resolveKeyInput(
|
|
|
344
326
|
}
|
|
345
327
|
}
|
|
346
328
|
const taskConfigHash = hashTaskConfig(cfg, args.hashCache)
|
|
347
|
-
const projectPackageJsonHash = await hashProjectPackageJson(
|
|
348
|
-
args.node.projectDir,
|
|
349
|
-
args.cache,
|
|
350
|
-
args.hashCache,
|
|
351
|
-
fileHashes,
|
|
352
|
-
)
|
|
353
329
|
|
|
354
330
|
// Args after `--` reach a requested command; the default build (a keyed
|
|
355
331
|
// group) runs none, and folded, they moved every key that folds its own.
|
|
@@ -359,7 +335,6 @@ async function resolveKeyInput(
|
|
|
359
335
|
const input: CacheKeyInput = {
|
|
360
336
|
taskId: args.node.id,
|
|
361
337
|
taskConfigHash,
|
|
362
|
-
projectPackageJsonHash,
|
|
363
338
|
envValues: resolved.envValues,
|
|
364
339
|
runtimeValues: resolved.runtimeValues,
|
|
365
340
|
workspaceRuntimeValues: resolved.workspaceRuntimeValues,
|
|
@@ -368,7 +343,6 @@ async function resolveKeyInput(
|
|
|
368
343
|
upstreamHashes,
|
|
369
344
|
upstreamIds,
|
|
370
345
|
upstreamGraft,
|
|
371
|
-
workspaceFingerprint: args.workspaceFingerprint,
|
|
372
346
|
forwardArgs: effectiveForwardArgs,
|
|
373
347
|
...(fileHashes !== undefined ? { fileHashes } : {}),
|
|
374
348
|
...(args.node.keyParts !== undefined ? { pluginParts: args.node.keyParts } : {}),
|
|
@@ -462,48 +436,3 @@ export function computeGroupHash(upstream: TaskOutcome[]): string {
|
|
|
462
436
|
.join('|')
|
|
463
437
|
return xxh3hex(`group|${ids}`)
|
|
464
438
|
}
|
|
465
|
-
|
|
466
|
-
/**
|
|
467
|
-
* Hash the project's `package.json` bytes. Folded into every task's
|
|
468
|
-
* cache key in that project so dep changes (devDependencies, scripts,
|
|
469
|
-
* version bumps) invalidate the project's tasks even when
|
|
470
|
-
* `cache.inputs.files` is narrow and doesn't cover the file.
|
|
471
|
-
*
|
|
472
|
-
* Matches Turbo and Nx's "implicit dependencies" behavior. Returns
|
|
473
|
-
* '' for the edge case of a project without a package.json (impossible
|
|
474
|
-
* in practice — workspace discovery requires one).
|
|
475
|
-
*
|
|
476
|
-
* Two optimizations: routed through `Cache.hashFile` so the mtime+size
|
|
477
|
-
* fast path applies (an unchanged pkg.json takes a stat + SQLite SELECT
|
|
478
|
-
* instead of a full file read). And memoized within-run per projectDir
|
|
479
|
-
* so a monorepo with 100 projects × 3 tasks each does 100 lookups, not
|
|
480
|
-
* 300.
|
|
481
|
-
*/
|
|
482
|
-
async function hashProjectPackageJson(
|
|
483
|
-
projectDir: string,
|
|
484
|
-
cache: CacheLayer,
|
|
485
|
-
hashCache?: HashCache,
|
|
486
|
-
fileHashes?: ReadonlyMap<string, string>,
|
|
487
|
-
): Promise<string> {
|
|
488
|
-
const cached = hashCache?.packageJson.get(projectDir)
|
|
489
|
-
if (cached !== undefined) return cached
|
|
490
|
-
const promise = doHashProjectPackageJson(projectDir, cache, fileHashes)
|
|
491
|
-
hashCache?.packageJson.set(projectDir, promise)
|
|
492
|
-
return promise
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
async function doHashProjectPackageJson(
|
|
496
|
-
projectDir: string,
|
|
497
|
-
cache: CacheLayer,
|
|
498
|
-
fileHashes?: ReadonlyMap<string, string>,
|
|
499
|
-
): Promise<string> {
|
|
500
|
-
const filePath = path.join(projectDir, 'package.json')
|
|
501
|
-
// Clean tracked package.json: its index OID is already in hand —
|
|
502
|
-
// no exists probe, no stat, no SQLite.
|
|
503
|
-
const oid = fileHashes?.get(filePath)
|
|
504
|
-
if (oid !== undefined) return oid
|
|
505
|
-
if (!(await Bun.file(filePath).exists())) return ''
|
|
506
|
-
// Route through the cache layer's mtime+size fast path so the
|
|
507
|
-
// typical re-run sees a stat + SQLite lookup instead of a file read.
|
|
508
|
-
return cache.hashFile(filePath)
|
|
509
|
-
}
|
|
@@ -117,7 +117,7 @@ export interface RunContextRecord {
|
|
|
117
117
|
/** The invocation command line (process.argv-derived), what follows `--` counted, not quoted. */
|
|
118
118
|
command: string
|
|
119
119
|
requestedTasks: readonly string[]
|
|
120
|
-
/**
|
|
120
|
+
/** The `--cache-policy` spec, e.g. `'local:rw,remote:r'`. */
|
|
121
121
|
cachePolicy: string
|
|
122
122
|
concurrency: number
|
|
123
123
|
flow: 'focused' | 'broad' | null
|
|
@@ -191,11 +191,11 @@ export interface TaskTelemetry {
|
|
|
191
191
|
/** Executor-reported placement (a worker id) — absent for local runs.
|
|
192
192
|
* Additive-optional, no schema bump (the `attempts` precedent). */
|
|
193
193
|
where?: string
|
|
194
|
-
/** `'deferred'` when the outputs stayed remote (`--download=none`).
|
|
194
|
+
/** `'deferred'` when the outputs stayed remote (`--cache-download=none`).
|
|
195
195
|
* Additive-optional, same no-bump precedent as `where`. */
|
|
196
196
|
outputs?: 'deferred'
|
|
197
197
|
/** Total attempts when the task RETRIED (>1) — set only when `exec.retries`
|
|
198
|
-
* / `--
|
|
198
|
+
* / `--retries` produced more than one attempt. A retried-then-passed task is
|
|
199
199
|
* flaky by definition; this is the telemetry-side flaky signal. */
|
|
200
200
|
attempts?: number
|
|
201
201
|
/**
|
|
@@ -340,7 +340,7 @@ export interface RunSummaryRecord {
|
|
|
340
340
|
exitOk: boolean
|
|
341
341
|
tasks: readonly TaskTelemetry[]
|
|
342
342
|
/**
|
|
343
|
-
* The run's stages as `
|
|
343
|
+
* The run's stages as `VX_DEBUG_TIMING` marks them (startup, load configs,
|
|
344
344
|
* classify + probe, run graph, …), each a wall window in epoch ms. The
|
|
345
345
|
* stages ahead of the run lock end before `startedAt`. Additive.
|
|
346
346
|
*/
|