@vzn/vx 0.0.645 → 0.0.648
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 -50
- package/launcher.cjs +9 -2
- package/launcher.sh +2 -2
- package/package.json +5 -5
- package/schemas/cache.json +5 -5
- package/schemas/error.json +1 -1
- package/schemas/info.json +16 -2
- package/schemas/last.json +127 -5
- package/schemas/plan.json +28 -2
- package/schemas/show.json +10 -1
- package/skills/vx/SKILL.md +19 -3
- package/src/bin.ts +54 -13
- package/src/cache/archive.ts +324 -23
- package/src/cache/cache.ts +330 -65
- package/src/cache/git-inputs.ts +274 -97
- package/src/cache/index.ts +7 -2
- package/src/cache/inputs.ts +515 -83
- package/src/cache/key-fold.ts +15 -6
- package/src/cache/layer.ts +74 -11
- package/src/cache/layered-cache.ts +154 -59
- package/src/cache/linked-inputs.ts +1 -1
- package/src/cache/output-index.ts +44 -14
- package/src/cache/plugin-cache.ts +49 -23
- package/src/cache/remote-hits.ts +72 -0
- package/src/cache/remote-lane.ts +100 -15
- package/src/cache/run-history.ts +77 -23
- package/src/cache/run-store.ts +11 -10
- package/src/cache/schema.ts +42 -6
- package/src/cache/tar-stream.ts +5 -0
- package/src/cache/umask.ts +1 -2
- package/src/cli/cache.ts +58 -27
- package/src/cli/completions.ts +12 -5
- package/src/cli/core-alias.ts +5 -2
- package/src/cli/docs.ts +42 -16
- package/src/cli/help.ts +62 -25
- package/src/cli/index.ts +79 -45
- package/src/cli/info.ts +12 -6
- package/src/cli/init.ts +222 -34
- package/src/cli/last.ts +443 -47
- package/src/cli/lock.ts +197 -52
- package/src/cli/plan-format.ts +91 -49
- package/src/cli/plugin-commands.ts +160 -11
- package/src/cli/plugin-templates.ts +13 -13
- package/src/cli/registry.ts +209 -0
- package/src/cli/run.ts +111 -41
- package/src/cli/select.ts +148 -49
- package/src/cli/show.ts +59 -16
- package/src/cli/task-verb.ts +16 -6
- package/src/cli/upgrade.ts +155 -54
- package/src/cli/watch-cycle.ts +10 -5
- package/src/cli/watch-filter.ts +71 -3
- package/src/cli/watch-fs.ts +22 -6
- package/src/cli/watch-judge.ts +48 -6
- package/src/cli/watch.ts +163 -42
- package/src/cli/why.ts +211 -73
- package/src/cli/workspace-config.ts +54 -15
- package/src/config.ts +13 -1
- package/src/exec/env.ts +7 -2
- package/src/exec/executor.ts +262 -15
- package/src/exec/index.ts +8 -2
- package/src/exec/kill-tree.ts +15 -0
- package/src/exec/local-executor.ts +1 -1
- package/src/exec/proc-sample.ts +70 -6
- package/src/exec/runner.ts +256 -53
- package/src/exec/sandbox-binds.ts +7 -3
- package/src/exec/sandbox-paths.ts +1 -1
- package/src/exec/sandbox-runtime.ts +222 -28
- package/src/exec/sandbox-violations.ts +52 -20
- package/src/graph/critical-path.ts +79 -0
- package/src/graph/default-build.ts +19 -0
- package/src/graph/definition.ts +38 -0
- package/src/graph/folds.ts +38 -5
- package/src/graph/index.ts +4 -1
- package/src/graph/priorities.ts +74 -8
- package/src/graph/scheduler.ts +326 -65
- package/src/graph/task-graph.ts +228 -35
- package/src/handoff.ts +97 -9
- package/src/history/failure-mode.ts +6 -3
- package/src/history/history.ts +268 -50
- package/src/history/index.ts +2 -0
- package/src/history/metrics.ts +142 -45
- package/src/index.ts +42 -10
- package/src/orchestrator/admission.ts +10 -6
- package/src/orchestrator/affected-tasks.ts +11 -6
- package/src/orchestrator/deferred-outputs.ts +2 -0
- package/src/orchestrator/doctor.ts +134 -42
- package/src/orchestrator/execute-args.ts +6 -0
- package/src/orchestrator/execute-task.ts +155 -42
- package/src/orchestrator/fingerprint-watch.ts +1 -1
- package/src/orchestrator/graph-memo.ts +140 -0
- package/src/orchestrator/graph-stage.ts +18 -1
- package/src/orchestrator/held-servers.ts +86 -0
- package/src/orchestrator/hit-restore.ts +227 -122
- package/src/orchestrator/index.ts +20 -3
- package/src/orchestrator/local-floor.ts +198 -0
- package/src/orchestrator/local-shortcircuit.ts +51 -8
- package/src/orchestrator/lockfile-claim.ts +96 -16
- package/src/orchestrator/miss-save.ts +52 -12
- package/src/orchestrator/options.ts +8 -6
- package/src/orchestrator/output-host.ts +43 -14
- package/src/orchestrator/output-log.ts +90 -10
- package/src/orchestrator/persistent-task.ts +97 -28
- package/src/orchestrator/persistent.ts +16 -4
- package/src/orchestrator/placement.ts +257 -14
- package/src/orchestrator/plan.ts +64 -7
- package/src/orchestrator/plugin-host.ts +230 -40
- package/src/orchestrator/plugin.ts +150 -37
- package/src/orchestrator/prepare.ts +178 -32
- package/src/orchestrator/projects.ts +65 -40
- package/src/orchestrator/remote-prefetch.ts +119 -25
- package/src/orchestrator/run-artifacts.ts +95 -32
- package/src/orchestrator/run-context.ts +39 -41
- package/src/orchestrator/run-failures.ts +65 -23
- package/src/orchestrator/run-id.ts +6 -3
- package/src/orchestrator/run-keys.ts +300 -0
- package/src/orchestrator/run-live.ts +294 -0
- package/src/orchestrator/run-lock.ts +13 -7
- package/src/orchestrator/run-records.ts +35 -23
- package/src/orchestrator/run-report.ts +3 -3
- package/src/orchestrator/run-steps.ts +58 -22
- package/src/orchestrator/run-stop.ts +22 -5
- package/src/orchestrator/run.ts +409 -79
- package/src/orchestrator/sandbox-fix.ts +7 -5
- package/src/orchestrator/sandbox-request.ts +78 -22
- package/src/orchestrator/shell-verdict.ts +5 -5
- package/src/orchestrator/signals.ts +26 -1
- package/src/orchestrator/stable-keys.ts +92 -8
- package/src/orchestrator/steps.ts +2 -2
- package/src/orchestrator/task-artifacts.ts +206 -0
- package/src/orchestrator/task-hash.ts +89 -14
- package/src/orchestrator/task-log-buffer.ts +72 -36
- package/src/orchestrator/task-process.ts +22 -0
- package/src/orchestrator/telemetry-host.ts +24 -3
- package/src/orchestrator/telemetry.ts +113 -38
- package/src/orchestrator/upstream.ts +15 -0
- package/src/report/events.ts +32 -13
- package/src/report/forecast.ts +36 -35
- package/src/report/framed-output.ts +173 -46
- package/src/report/index.ts +1 -0
- package/src/report/logger.ts +217 -186
- package/src/report/path-links.ts +12 -4
- package/src/report/plain-output.ts +13 -5
- package/src/report/status-line.ts +94 -46
- package/src/report/summary.ts +174 -38
- package/src/report/terminal-signals.ts +13 -2
- package/src/settings.ts +14 -6
- package/src/util/bun-version.ts +4 -1
- package/src/util/colors.ts +20 -3
- package/src/util/config-frame.ts +41 -4
- package/src/{graph → util}/dependency-spec.ts +22 -13
- package/src/util/edit-distance.ts +13 -1
- package/src/util/env-flag.ts +9 -0
- package/src/util/errors.ts +59 -3
- package/src/util/essential-env.ts +57 -0
- package/src/util/git-ignored.ts +1 -1
- package/src/util/hash.ts +35 -0
- package/src/util/hide-env.ts +45 -5
- package/src/util/index.ts +22 -5
- package/src/util/num.ts +8 -0
- package/src/util/paths.ts +121 -21
- package/src/util/report.ts +6 -1
- package/src/util/secret-mask.ts +62 -1
- package/src/util/secrets.ts +7 -2
- package/src/util/size.ts +11 -3
- package/src/util/tail.ts +6 -1
- package/src/util/timing.ts +13 -1
- package/src/util/ui.ts +65 -4
- package/src/util/which.ts +1 -1
- package/src/util/write-file.ts +67 -0
- package/src/workspace/affected.ts +710 -107
- package/src/workspace/bracket-literal.ts +84 -0
- package/src/workspace/canonical-dir.ts +37 -0
- package/src/workspace/config-bytecode.ts +282 -0
- package/src/workspace/config-eval.ts +29 -8
- package/src/workspace/config-imports.ts +199 -58
- package/src/workspace/config-schema.ts +242 -39
- package/src/workspace/filter.ts +37 -8
- package/src/workspace/index.ts +11 -2
- package/src/workspace/json-data.ts +3 -4
- package/src/workspace/lockfile.ts +57 -8
- package/src/workspace/migration.ts +72 -49
- package/src/workspace/package-graph.ts +5 -4
- package/src/workspace/project-loader.ts +314 -47
- package/src/workspace/repo-id.ts +1 -1
- package/src/workspace/workspace.ts +176 -33
- package/src/cli/foreign-flags.ts +0 -727
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
1
2
|
import type { ExecConfig, TaskConfig, CacheConfig, InputStability } from '../config.js'
|
|
2
3
|
import {
|
|
3
4
|
ArtifactVanishedError,
|
|
@@ -43,7 +44,7 @@ import { isGroupTask, RestoreDemoted, type TaskNode, type TaskOutcome } from '..
|
|
|
43
44
|
import {
|
|
44
45
|
killGraceMs,
|
|
45
46
|
MASKED,
|
|
46
|
-
|
|
47
|
+
maskedStreams,
|
|
47
48
|
outputMatcher,
|
|
48
49
|
printable,
|
|
49
50
|
relPosix,
|
|
@@ -67,15 +68,22 @@ import {
|
|
|
67
68
|
type WithheldLink,
|
|
68
69
|
withheldLinkLine,
|
|
69
70
|
} from './sandbox-request.js'
|
|
70
|
-
import {
|
|
71
|
+
import {
|
|
72
|
+
bracketLiteralHint,
|
|
73
|
+
markUnsaved,
|
|
74
|
+
saveMiss,
|
|
75
|
+
type OutputDirSnapshot,
|
|
76
|
+
type SaveFacts,
|
|
77
|
+
} from './miss-save.js'
|
|
71
78
|
import type { FingerprintWatch } from './fingerprint-watch.js'
|
|
72
|
-
import { restoreHit } from './hit-restore.js'
|
|
79
|
+
import { currentHit, restoreHit } from './hit-restore.js'
|
|
73
80
|
import type { MissExplainer } from '../history/index.js'
|
|
74
81
|
import { shellVerdict } from './shell-verdict.js'
|
|
75
82
|
import { sandboxFix } from './sandbox-fix.js'
|
|
76
83
|
// The hit path's entry stays importable from here (tests).
|
|
77
84
|
export { restoreHit, type RestoreHitArgs } from './hit-restore.js'
|
|
78
85
|
import type { DeferredOutputs } from './deferred-outputs.js'
|
|
86
|
+
import type { TreeLoad } from './telemetry.js'
|
|
79
87
|
import { type Logger, warnOf } from '../report/index.js'
|
|
80
88
|
import {
|
|
81
89
|
computeGroupHash,
|
|
@@ -94,6 +102,7 @@ import {
|
|
|
94
102
|
forgetUndeclaredWrites,
|
|
95
103
|
isAborted,
|
|
96
104
|
taskBinDirs,
|
|
105
|
+
serverSpec,
|
|
97
106
|
taskEnv,
|
|
98
107
|
taskTimeoutMs,
|
|
99
108
|
} from './task-process.js'
|
|
@@ -109,6 +118,8 @@ export interface ExecuteArgs {
|
|
|
109
118
|
log: Logger
|
|
110
119
|
/** The executor this task was PLACED on (run.ts, before scheduling). */
|
|
111
120
|
executor: TaskExecutor
|
|
121
|
+
/** Told the task executes, so the executor's demand counts it again (`ExecutorDemand.executing`). */
|
|
122
|
+
executing?: ((id: string) => void) | undefined
|
|
112
123
|
/**
|
|
113
124
|
* `exec.remote: 'only'` with no remote executor to take it: succeed
|
|
114
125
|
* WITHOUT executing, cleaning, restoring or saving anything on this
|
|
@@ -161,7 +172,7 @@ export interface ExecuteArgs {
|
|
|
161
172
|
*/
|
|
162
173
|
liveChildren?: Set<ReturnType<typeof Bun.spawn>>
|
|
163
174
|
/** Sample a spawned task's process tree (`TelemetrySource.track`); absent when nobody samples. */
|
|
164
|
-
track?: (taskId: string, pid: number) => () =>
|
|
175
|
+
track?: (taskId: string, pid: number) => () => TreeLoad | undefined
|
|
165
176
|
/** Name what a missed key changed (`createMissExplainer`); absent when nobody listens. */
|
|
166
177
|
explainMiss?: MissExplainer
|
|
167
178
|
/**
|
|
@@ -270,6 +281,12 @@ export interface ExecuteArgs {
|
|
|
270
281
|
noDependants?: true
|
|
271
282
|
/** The task holds vx's terminal (`terminalHolders`): its stdio is vx's own. */
|
|
272
283
|
terminal?: true
|
|
284
|
+
/** A persistent task every dependant of which starts it where it runs: never spawned here. */
|
|
285
|
+
carried?: true
|
|
286
|
+
/** The persistent tasks its `servers` executor starts where it runs it. */
|
|
287
|
+
servers?: readonly TaskNode[]
|
|
288
|
+
/** Each of `servers`' nested project dirs, in its order. */
|
|
289
|
+
serversNested?: readonly (readonly string[])[]
|
|
273
290
|
/** When each command of the run ran and where it may write (command-windows.ts). */
|
|
274
291
|
commandWindows?: CommandWindows
|
|
275
292
|
}
|
|
@@ -300,6 +317,28 @@ export async function executeTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
300
317
|
return executeCachedTask(args)
|
|
301
318
|
}
|
|
302
319
|
|
|
320
|
+
/**
|
|
321
|
+
* A probed hit `executeTask` would restore, settled now when its tree is
|
|
322
|
+
* already current (`currentHit`): the thunk replays its output and returns
|
|
323
|
+
* the outcome. Undefined dispatches it as usual. A dispatch, a slot and the
|
|
324
|
+
* promises around it were ~10 µs of each of a warm run's hits.
|
|
325
|
+
*/
|
|
326
|
+
export function settleProbedHit(args: ExecuteArgs): (() => TaskOutcome) | undefined {
|
|
327
|
+
const probed = args.preProbed
|
|
328
|
+
if (probed?.hit == null || !readsCache(args) || args.remoteOnlyNoop === true) return undefined
|
|
329
|
+
if (isGroupTask(args.node) || args.node.config.exec?.persistent !== undefined) return undefined
|
|
330
|
+
// A moved fingerprint is said, and demotes the hit, on the dispatch.
|
|
331
|
+
if (args.fingerprintWatch?.moved() !== undefined) return undefined
|
|
332
|
+
const restore = {
|
|
333
|
+
args,
|
|
334
|
+
hash: probed.hash,
|
|
335
|
+
hit: probed.hit,
|
|
336
|
+
cacheOpStart: performance.now(),
|
|
337
|
+
taskStartNs: process.hrtime.bigint() - args.runStartHrTimeNs,
|
|
338
|
+
}
|
|
339
|
+
return currentHit(restore)
|
|
340
|
+
}
|
|
341
|
+
|
|
303
342
|
/** Whether this task reads the cache: a cached, not remote-only task with a read axis on. */
|
|
304
343
|
function readsCache(args: ExecuteArgs): boolean {
|
|
305
344
|
const policy = args.cachePolicy ?? FULL_CACHE_POLICY
|
|
@@ -358,7 +397,7 @@ async function restoreOrMiss(
|
|
|
358
397
|
/**
|
|
359
398
|
* What a write to an input during the command means, each field highest
|
|
360
399
|
* first: the env var (`layers.run`), the task's `cache.inputStability`,
|
|
361
|
-
* the workspace's, then `'immutable'` and `'warn'` (
|
|
400
|
+
* the workspace's, then `'immutable'` and `'warn'` (2026-10-12), so
|
|
362
401
|
* by default any such write withholds the save, on CI too, since a
|
|
363
402
|
* concurrent task can change an input and put it back.
|
|
364
403
|
*/
|
|
@@ -498,8 +537,12 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
498
537
|
filterUpstreamHashes(present, cfg.inputs?.tasks, node.projectName, node.id).map(([id]) => id),
|
|
499
538
|
)
|
|
500
539
|
const foldedTasks = expandGroupUpstream(present.filter((u) => folded.has(u.node.id)))
|
|
501
|
-
const
|
|
502
|
-
|
|
540
|
+
const keylessBy = isKeyless(cfg)
|
|
541
|
+
? node.id
|
|
542
|
+
: foldedTasks
|
|
543
|
+
.map((u) => u.keylessBy ?? (isKeyless(u.node.config) ? u.node.id : undefined))
|
|
544
|
+
.find((id) => id !== undefined)
|
|
545
|
+
const keyless = keylessBy !== undefined
|
|
503
546
|
const reads = policy.localRead || policy.remoteRead
|
|
504
547
|
const willRead = !remoteOnly && cfgCacheable && !keyless && reads
|
|
505
548
|
const willWrite = !remoteOnly && cfgCacheable && (policy.localWrite || policy.remoteWrite)
|
|
@@ -539,7 +582,8 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
539
582
|
// The local executor keeps its own timer from the spawn (it signals the
|
|
540
583
|
// process group), so its request carries no clock: armed here, it also
|
|
541
584
|
// timed the sandbox's wrap, and a wrap slower than the timeout aborted
|
|
542
|
-
// the request before the spawn — a task reported killed, never run.
|
|
585
|
+
// the request before the spawn — a task reported killed, never run. So
|
|
586
|
+
// does an executor that declares `timeouts`.
|
|
543
587
|
let timeoutTimer: ReturnType<typeof setTimeout> | undefined
|
|
544
588
|
let timeoutFired = false
|
|
545
589
|
// The attempt's listener on the run's stop signal, taken off once the
|
|
@@ -560,7 +604,7 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
560
604
|
}
|
|
561
605
|
clearTimeout(timeoutTimer)
|
|
562
606
|
timeoutFired = false
|
|
563
|
-
if (effectiveTimeout !== undefined && !local) {
|
|
607
|
+
if (effectiveTimeout !== undefined && !local && args.executor.timeouts !== true) {
|
|
564
608
|
timeoutTimer = setTimeout(() => {
|
|
565
609
|
timeoutFired = true
|
|
566
610
|
stop.abort(new Error(`timed out after ${effectiveTimeout}ms`))
|
|
@@ -588,7 +632,7 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
588
632
|
const why = timeoutFired ? `timeout (${effectiveTimeout}ms)` : 'stop'
|
|
589
633
|
log.taskStderr(
|
|
590
634
|
node,
|
|
591
|
-
|
|
635
|
+
`${midLine ? '\n' : ''}vx: ${executorLabel(args.executor)} did not return within ${graceMs}ms of the ${why}; abandoned\n`,
|
|
592
636
|
)
|
|
593
637
|
const reason: unknown = signal.reason
|
|
594
638
|
resolve({
|
|
@@ -692,8 +736,8 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
692
736
|
let wsStampedBefore: ReadonlyMap<string, OutputStamp> | undefined
|
|
693
737
|
// `cache.outputs.clean: false` leaves the outputs to the first attempt's
|
|
694
738
|
// command, and the save stores whatever they hold after it, a leftover
|
|
695
|
-
// included: the user opted out of the clean-tree guarantee
|
|
696
|
-
// 2026-10-15). A retry still starts clean. An additive task never cleans
|
|
739
|
+
// included: the user opted out of the clean-tree guarantee
|
|
740
|
+
// (2026-10-15). A retry still starts clean. An additive task never cleans
|
|
697
741
|
// by glob anyway.
|
|
698
742
|
const keepOutputs = !additive && (cacheCfg?.outputs.clean ?? args.cleanOutputs ?? true) === false
|
|
699
743
|
|
|
@@ -787,11 +831,13 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
787
831
|
for (const c of captured)
|
|
788
832
|
if (c.kind === 'env' && named.includes(c.name) && !secretNamed(c.name))
|
|
789
833
|
c.hash = MASKED + c.hash
|
|
834
|
+
// The values of secret-named variables, masked in what the task prints
|
|
835
|
+
// and what the cache keeps of it (L-11); null when there are none.
|
|
836
|
+
const secrets = secretMask([process.env, env, step.env?.define], step.env?.secret)
|
|
790
837
|
// A runtime probe's row is named by its command, which a TS config may
|
|
791
838
|
// build from `process.env`: the run history stored it and `vx why`
|
|
792
839
|
// printed it (L-11).
|
|
793
|
-
const nameSecrets =
|
|
794
|
-
captured.length > 0 ? secretMask([process.env, env, step.env?.define], step.env?.secret) : null
|
|
840
|
+
const nameSecrets = captured.length > 0 ? secrets : null
|
|
795
841
|
if (nameSecrets !== null) {
|
|
796
842
|
// Two probes that differ only by a secret mask to one name, and a row
|
|
797
843
|
// is stored once per name: numbered, the second's change still shows.
|
|
@@ -819,7 +865,11 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
819
865
|
if (inputs !== undefined && declaredInputs.length > 0 && inputs.files.length === 0) {
|
|
820
866
|
warnOf(log)(
|
|
821
867
|
`[vx] ${node.id}: inputs matched no files (${declaredInputs.join(', ')}) — ` +
|
|
822
|
-
`the key will not change when this project's source does
|
|
868
|
+
`the key will not change when this project's source does` +
|
|
869
|
+
bracketLiteralHint([
|
|
870
|
+
...(cfg.inputs?.files ?? []).map((g) => [node.projectDir, g] as const),
|
|
871
|
+
...(cfg.inputs?.workspaceFiles ?? []).map((g) => [args.workspaceRoot, g] as const),
|
|
872
|
+
]),
|
|
823
873
|
)
|
|
824
874
|
}
|
|
825
875
|
|
|
@@ -892,6 +942,9 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
892
942
|
const cleanedWsRels = await cleanWorkspaceOutputs(wsCleanArgs)
|
|
893
943
|
args.gitFilesCache?.markWorkspaceOutputsChanged(args.workspaceRoot, cleanedWsRels)
|
|
894
944
|
}
|
|
945
|
+
// What the attempt before this one left, put back if this one is
|
|
946
|
+
// withdrawn: it stands, with its output and its refusals.
|
|
947
|
+
const standing = { output, violations, flushMasked }
|
|
895
948
|
violations = []
|
|
896
949
|
const endReq = span('miss: build request')
|
|
897
950
|
const req = await buildRequest(isLocalExecutor(args.executor))
|
|
@@ -900,6 +953,9 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
900
953
|
// cleans or builds its request, after the loop's own check: the retry
|
|
901
954
|
// is withdrawn and the attempt before it stands.
|
|
902
955
|
if (attempt > 1 && args.failFast?.aborted === true) {
|
|
956
|
+
output = standing.output
|
|
957
|
+
violations = standing.violations
|
|
958
|
+
flushMasked = standing.flushMasked
|
|
903
959
|
clearTimeout(timeoutTimer)
|
|
904
960
|
unlistenStop?.()
|
|
905
961
|
unlistenStop = undefined
|
|
@@ -921,6 +977,7 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
921
977
|
node.projectDir,
|
|
922
978
|
commandWriteReach(node, args.workspaceRoot),
|
|
923
979
|
) ?? (() => {})
|
|
980
|
+
args.executing?.(node.id)
|
|
924
981
|
try {
|
|
925
982
|
running = Promise.resolve(args.executor.execute(req))
|
|
926
983
|
} catch (thrown) {
|
|
@@ -940,8 +997,12 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
940
997
|
const why = secrets?.mask(raw.message) ?? raw.message
|
|
941
998
|
if (remoteOnly) {
|
|
942
999
|
raw = new UserError(`${why}, and remote: 'only' keeps it off this machine`)
|
|
1000
|
+
} else if (args.executor.exclusive === true) {
|
|
1001
|
+
raw = new UserError(`${why}, and ${args.executor.name} runs no task on this machine`)
|
|
943
1002
|
} else {
|
|
944
|
-
warnOf(log)(
|
|
1003
|
+
warnOf(log)(
|
|
1004
|
+
`[vx] ${node.id}: ${executorLabel(args.executor)} gave it back: ${why} — running it here`,
|
|
1005
|
+
)
|
|
945
1006
|
const local = await localExecutor().execute(await buildRequest(true))
|
|
946
1007
|
assertExecuteResult('local', node.id, local)
|
|
947
1008
|
return local
|
|
@@ -964,9 +1025,15 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
964
1025
|
unlistenStop?.()
|
|
965
1026
|
unlistenStop = undefined
|
|
966
1027
|
flushMasked()
|
|
967
|
-
untrack?.()
|
|
1028
|
+
load = untrack?.() ?? load
|
|
968
1029
|
untrack = undefined
|
|
969
1030
|
})
|
|
1031
|
+
.catch((thrown: unknown) => {
|
|
1032
|
+
// The scheduler prints the throw's line into the task's stream,
|
|
1033
|
+
// after the masked tail the finally above flushed.
|
|
1034
|
+
if (midLine) log.taskStderr(node, '\n')
|
|
1035
|
+
throw thrown
|
|
1036
|
+
})
|
|
970
1037
|
endExec()
|
|
971
1038
|
if (secrets !== null)
|
|
972
1039
|
res = {
|
|
@@ -1006,7 +1073,7 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1006
1073
|
// before the strace pass shipped, false since, and the branch below is
|
|
1007
1074
|
// live on Linux.) Violations surface via `TaskOutcome.sandboxViolationLines`.
|
|
1008
1075
|
let code = res.exitCode
|
|
1009
|
-
// ANY violation fails the task (
|
|
1076
|
+
// ANY violation fails the task (2026-09-05). A sandboxed task
|
|
1010
1077
|
// declares what it touches; a denial means it touched something else,
|
|
1011
1078
|
// and an artifact built while reading — or failing to read — something
|
|
1012
1079
|
// the cache key never folded is not a safe thing to replay. A task that
|
|
@@ -1022,7 +1089,10 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1022
1089
|
// 137 (item 1063).
|
|
1023
1090
|
if (res.timedOut) {
|
|
1024
1091
|
const how = res.signal === 'SIGKILL' ? 'SIGKILL after the SIGTERM grace' : 'SIGTERM'
|
|
1025
|
-
log.taskStderr(
|
|
1092
|
+
log.taskStderr(
|
|
1093
|
+
node,
|
|
1094
|
+
`${midLine ? '\n' : ''}[vx] timed out after ${effectiveTimeout}ms — killed (${how})\n`,
|
|
1095
|
+
)
|
|
1026
1096
|
// Force a non-zero classification even if the child TRAPPED SIGTERM and
|
|
1027
1097
|
// still exited 0 (`trap 'exit 0' TERM`, a common graceful-shutdown
|
|
1028
1098
|
// pattern). Without this a timed-out task is classified `success` and its
|
|
@@ -1037,7 +1107,15 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1037
1107
|
// about what sent it (259). One frame line names the rule. A spawn that
|
|
1038
1108
|
// threw ran no shell: its 127 is vx's, and the line would send the
|
|
1039
1109
|
// reader after a command that exists (A-41). A run's stop killed it
|
|
1040
|
-
// itself: the SIGKILL past the grace read as the OOM killer's.
|
|
1110
|
+
// itself: the SIGKILL past the grace read as the OOM killer's. A
|
|
1111
|
+
// SIGINT/SIGTERM death waits a turn for a pending handler first, as the
|
|
1112
|
+
// shutdown check below does: a Ctrl-C reaches the child and vx together.
|
|
1113
|
+
if (
|
|
1114
|
+
!res.timedOut &&
|
|
1115
|
+
(res.signal === 'SIGINT' || res.signal === 'SIGTERM') &&
|
|
1116
|
+
args.stopSignal?.aborted !== true
|
|
1117
|
+
)
|
|
1118
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
1041
1119
|
if (!res.timedOut && res.spawnFailed !== true && args.stopSignal?.aborted !== true) {
|
|
1042
1120
|
const verdict = shellVerdict({
|
|
1043
1121
|
code,
|
|
@@ -1049,7 +1127,8 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1049
1127
|
})
|
|
1050
1128
|
// The line quotes the command's first word, which a config may have
|
|
1051
1129
|
// built from a secret: masked as the task's own output is (L-37).
|
|
1052
|
-
if (verdict !== undefined)
|
|
1130
|
+
if (verdict !== undefined)
|
|
1131
|
+
log.taskStderr(node, `${midLine ? '\n' : ''}${secrets?.mask(verdict) ?? verdict}\n`)
|
|
1053
1132
|
}
|
|
1054
1133
|
return { result: res, exitCode: code }
|
|
1055
1134
|
}
|
|
@@ -1065,7 +1144,8 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1065
1144
|
await args.deferred.materializeFor(node)
|
|
1066
1145
|
} catch (err) {
|
|
1067
1146
|
const msg = err instanceof Error ? err.message : String(err)
|
|
1068
|
-
|
|
1147
|
+
// Nothing ran: the note is the frame's first line.
|
|
1148
|
+
log.taskStderr(node, `[vx] ${msg}\n`)
|
|
1069
1149
|
return {
|
|
1070
1150
|
node,
|
|
1071
1151
|
status: 'failed',
|
|
@@ -1078,17 +1158,24 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1078
1158
|
}
|
|
1079
1159
|
}
|
|
1080
1160
|
|
|
1081
|
-
// The values of secret-named variables, masked in what the task prints
|
|
1082
|
-
// and what the cache keeps of it (L-11); null when there are none.
|
|
1083
|
-
const secrets = secretMask([process.env, env, step.env?.define], step.env?.secret)
|
|
1084
1161
|
let flushMasked = (): void => {}
|
|
1162
|
+
// The attempt's output, both streams, last ended mid-line: vx's own line
|
|
1163
|
+
// (a retry, a timeout, the shell's verdict, an abandoned or thrown
|
|
1164
|
+
// executor) starts its own, and only then: a newline always before it
|
|
1165
|
+
// printed a blank line under the OUTPUT rule or the task's last line. Glued, a frame read
|
|
1166
|
+
// `partialvx: retrying …`.
|
|
1167
|
+
let midLine = false
|
|
1085
1168
|
// The attempt's output as the logger got it (masked): the entry's when
|
|
1086
1169
|
// the task saves, else only what a failure reports (`failedOutput`).
|
|
1087
1170
|
let output: BoundedCapture | undefined
|
|
1088
|
-
|
|
1171
|
+
// Masked again whole: the live stream lets a held piece go after a pause.
|
|
1172
|
+
const storeMask = secrets ?? undefined
|
|
1173
|
+
const storedOutput = (): string => encodeOutputLog(output?.chunks() ?? [], storeMask)
|
|
1089
1174
|
const failedAttempts: { endedAt: number; exitCode: number; timedOut?: true }[] = []
|
|
1090
1175
|
// The sampling of the attempt's process tree, stopped once it settles.
|
|
1091
|
-
let untrack: (() =>
|
|
1176
|
+
let untrack: (() => TreeLoad | undefined) | undefined
|
|
1177
|
+
// What the last attempt's samples saw, for the run history.
|
|
1178
|
+
let load: TreeLoad | undefined
|
|
1092
1179
|
// The entry's command is shown by `vx why` and sent with the entry to a
|
|
1093
1180
|
// remote cache: a value a config interpolated stays out of both.
|
|
1094
1181
|
const storedCommand = secrets?.mask(step.command) ?? step.command
|
|
@@ -1160,6 +1247,7 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1160
1247
|
hash,
|
|
1161
1248
|
wallclockStartNs,
|
|
1162
1249
|
wallclockEndNs: process.hrtime.bigint() - args.runStartHrTimeNs,
|
|
1250
|
+
...(result.where !== undefined ? { where: result.where } : {}),
|
|
1163
1251
|
}
|
|
1164
1252
|
}
|
|
1165
1253
|
|
|
@@ -1171,7 +1259,7 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1171
1259
|
})
|
|
1172
1260
|
log.taskStderr(
|
|
1173
1261
|
node,
|
|
1174
|
-
|
|
1262
|
+
`${midLine ? '\n' : ''}vx: retrying ${node.id} (attempt ${attempt + 1}/${maxAttempts}) after ${result.timedOut === true ? 'a timeout' : `exit ${effectiveExitCode}`}\n`,
|
|
1175
1263
|
)
|
|
1176
1264
|
}
|
|
1177
1265
|
|
|
@@ -1181,20 +1269,19 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1181
1269
|
? new BoundedCapture()
|
|
1182
1270
|
: new BoundedCapture(FAILED_OUTPUT_HEAD_CHARS, FAILED_OUTPUT_TAIL_CHARS)
|
|
1183
1271
|
output = kept
|
|
1272
|
+
midLine = false
|
|
1184
1273
|
const toOut = (t: string): void => {
|
|
1185
1274
|
kept.push(t)
|
|
1275
|
+
if (t.length > 0) midLine = !t.endsWith('\n')
|
|
1186
1276
|
log.taskStdout(node, t)
|
|
1187
1277
|
}
|
|
1188
1278
|
const toErr = (t: string): void => {
|
|
1189
1279
|
kept.push(t, true)
|
|
1280
|
+
if (t.length > 0) midLine = !t.endsWith('\n')
|
|
1190
1281
|
log.taskStderr(node, t)
|
|
1191
1282
|
}
|
|
1192
|
-
const
|
|
1193
|
-
|
|
1194
|
-
flushMasked = () => {
|
|
1195
|
-
out?.end()
|
|
1196
|
-
err?.end()
|
|
1197
|
-
}
|
|
1283
|
+
const masked = secrets && maskedStreams(secrets, toOut, toErr)
|
|
1284
|
+
flushMasked = () => masked?.end()
|
|
1198
1285
|
const base: ExecuteRequest = {
|
|
1199
1286
|
taskId: node.id,
|
|
1200
1287
|
workspaceRoot: args.workspaceRoot,
|
|
@@ -1204,8 +1291,8 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1204
1291
|
env,
|
|
1205
1292
|
envDefine: step.env?.define ?? {},
|
|
1206
1293
|
capture,
|
|
1207
|
-
onStdout:
|
|
1208
|
-
onStderr:
|
|
1294
|
+
onStdout: masked ? masked.out : toOut,
|
|
1295
|
+
onStderr: masked ? masked.err : toErr,
|
|
1209
1296
|
...(args.liveChildren !== undefined ? { liveChildren: args.liveChildren } : {}),
|
|
1210
1297
|
...(args.track !== undefined
|
|
1211
1298
|
? {
|
|
@@ -1218,19 +1305,29 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1218
1305
|
...(effectiveTimeout !== undefined ? { timeoutMs: effectiveTimeout } : {}),
|
|
1219
1306
|
...(inputs !== undefined ? { inputs } : {}),
|
|
1220
1307
|
...(cfgCacheable ? { cacheKey: hash } : {}),
|
|
1308
|
+
...(cfgCacheable && !remoteOnly && !willSave ? { unsaved: true } : {}),
|
|
1221
1309
|
...(remoteOnly ? { remoteOnly: true } : {}),
|
|
1310
|
+
...(node.platform !== undefined ? { platform: node.platform } : {}),
|
|
1222
1311
|
...(deferralRequested ? { download: 'deferred' as const } : {}),
|
|
1223
1312
|
// `--force`/`--no-cache` reach a remote executor's private record
|
|
1224
1313
|
// through this flag — the policy gates above only cover vx's OWN cache.
|
|
1225
1314
|
...(cfgCacheable && !(policy.localRead || policy.remoteRead) ? { refresh: true } : {}),
|
|
1226
1315
|
outputs: { files: outputs, workspaceFiles: wsOutputs },
|
|
1227
1316
|
...(args.terminal === true ? { terminal: true as const } : {}),
|
|
1317
|
+
...(args.servers === undefined
|
|
1318
|
+
? {}
|
|
1319
|
+
: {
|
|
1320
|
+
servers: args.servers.map((s, i) =>
|
|
1321
|
+
serverSpec(s, args.workspaceRoot, args.serversNested?.[i] ?? []),
|
|
1322
|
+
),
|
|
1323
|
+
}),
|
|
1228
1324
|
}
|
|
1229
1325
|
// The signal last: it arms a plugin's `exec.timeout`, and the sandbox's arming
|
|
1230
1326
|
// below is vx's work, not the task's. Armed first, a 60 ms timeout
|
|
1231
1327
|
// expired before the spawn and failed `echo` as timed out, unrun.
|
|
1232
1328
|
if (!userSandbox) return { ...base, signal: requestSignal(local) }
|
|
1233
|
-
|
|
1329
|
+
// A remote executor enforces it where it runs the task.
|
|
1330
|
+
if (args.executor.remote !== true) await args.armSandbox?.()
|
|
1234
1331
|
const sb = await sandboxRequestFor(
|
|
1235
1332
|
node,
|
|
1236
1333
|
step.sandbox!,
|
|
@@ -1250,7 +1347,11 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1250
1347
|
let movedFails: string | undefined
|
|
1251
1348
|
let own: Awaited<ReturnType<typeof ownOutputs>>
|
|
1252
1349
|
let saved = false
|
|
1253
|
-
if (
|
|
1350
|
+
if (
|
|
1351
|
+
effectiveExitCode === 0 &&
|
|
1352
|
+
willSave &&
|
|
1353
|
+
(deferralRequested || result.outputs?.kind === 'deferred')
|
|
1354
|
+
) {
|
|
1254
1355
|
// The outputs never landed here: no artifact, no rows. A partial local
|
|
1255
1356
|
// record (a row with no artifact) is exactly the corrupt-entry shape
|
|
1256
1357
|
// `restoreOutputs` refuses, so writing none is the only clean answer.
|
|
@@ -1376,6 +1477,12 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1376
1477
|
*/
|
|
1377
1478
|
async function movedSinceKey(): Promise<string | null | undefined> {
|
|
1378
1479
|
if (described!.hash !== hash) return null
|
|
1480
|
+
// Changed where an executor ran it: this disk never shows it.
|
|
1481
|
+
if (result.changed !== undefined && result.changed.length > 0) {
|
|
1482
|
+
const changed = new Set(result.changed.map((f) => path.join(args.workspaceRoot, f)))
|
|
1483
|
+
const there = keyedInputs(described!.facts).find((f) => changed.has(f.path))
|
|
1484
|
+
if (there !== undefined) return there.path
|
|
1485
|
+
}
|
|
1379
1486
|
return (
|
|
1380
1487
|
(await movedInput(
|
|
1381
1488
|
keyedInputs(described!.facts),
|
|
@@ -1463,10 +1570,10 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1463
1570
|
: {}),
|
|
1464
1571
|
...(inputChanges !== undefined ? { inputChanges } : {}),
|
|
1465
1572
|
...(unkeyed ? { unkeyed: true as const } : {}),
|
|
1466
|
-
...(
|
|
1573
|
+
...(keylessBy !== undefined ? { keylessBy } : {}),
|
|
1467
1574
|
...(saved ? { saved: true as const } : {}),
|
|
1468
1575
|
...(effectiveExitCode !== 0 && output !== undefined
|
|
1469
|
-
? { failedOutput: failedOutputLog(output, willSave || args.keepRun !== undefined) }
|
|
1576
|
+
? { failedOutput: failedOutputLog(output, willSave || args.keepRun !== undefined, storeMask) }
|
|
1470
1577
|
: {}),
|
|
1471
1578
|
...(args.keepRun !== undefined &&
|
|
1472
1579
|
(effectiveExitCode !== 0 ? args.keepRun.failures : !willSave) &&
|
|
@@ -1477,10 +1584,16 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1477
1584
|
...(movedFails !== undefined ? { inputMoved: movedFails } : {}),
|
|
1478
1585
|
...(result.cpuMs !== undefined ? { cpuMs: result.cpuMs } : {}),
|
|
1479
1586
|
...(result.peakRssBytes !== undefined ? { peakRssBytes: result.peakRssBytes } : {}),
|
|
1587
|
+
...(load !== undefined ? { peakCores: load.peakCores, avgRssBytes: load.avgRssBytes } : {}),
|
|
1480
1588
|
...(result.where !== undefined ? { where: result.where } : {}),
|
|
1481
|
-
...(
|
|
1482
|
-
? {
|
|
1589
|
+
...(result.where !== undefined && result.machine !== undefined
|
|
1590
|
+
? {
|
|
1591
|
+
whereCores: result.machine.cores,
|
|
1592
|
+
whereMemoryBytes: result.machine.memoryBytes,
|
|
1593
|
+
...(result.machine.cpuShare === undefined ? {} : { cpuShare: result.machine.cpuShare }),
|
|
1594
|
+
}
|
|
1483
1595
|
: {}),
|
|
1596
|
+
...(result.outputs?.kind === 'deferred' ? { outputs: 'deferred' as const } : {}),
|
|
1484
1597
|
wallclockStartNs,
|
|
1485
1598
|
wallclockEndNs,
|
|
1486
1599
|
...(finalViolations.length > 0
|
|
@@ -85,7 +85,7 @@ export class FingerprintWatch {
|
|
|
85
85
|
this.said = true
|
|
86
86
|
warnOf(log)(
|
|
87
87
|
`[vx] a task rewrote \`${this.movedFiles.join('`, `')}\` during the run, which a plugin ` +
|
|
88
|
-
`
|
|
88
|
+
`puts into every key: nothing keyed before it is restored or saved from here on`,
|
|
89
89
|
)
|
|
90
90
|
}
|
|
91
91
|
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// The task graph memo: the graph a run built, read back whole when nothing
|
|
2
|
+
// it is built from has moved. Building 1,206 tasks' graph was 4.5 ms of a
|
|
3
|
+
// warm run (2026-10-23), nearly all of it code a short process runs cold;
|
|
4
|
+
// reading it back is half that.
|
|
5
|
+
//
|
|
6
|
+
// A run's configs come from the lock and nowhere else when no plugin fills
|
|
7
|
+
// the `project` stage, so the token names them by the lock's bytes instead
|
|
8
|
+
// of serializing them (3 ms at 1,206 tasks): the bytes, the projects
|
|
9
|
+
// discovered and loaded, their package edges, the requested tasks, the
|
|
10
|
+
// rules, the root and the code are everything `buildTaskGraph` reads. A
|
|
11
|
+
// scoped load hands undeclared `^name`s back instead of refusing them, so
|
|
12
|
+
// it is never remembered.
|
|
13
|
+
|
|
14
|
+
import type { FoldMemo } from '../cache/index.js'
|
|
15
|
+
import { buildTaskGraph, declaredTask, type TaskNode } from '../graph/index.js'
|
|
16
|
+
import { xxh3hex } from '../util/index.js'
|
|
17
|
+
import type { ProjectMeta } from '../workspace/index.js'
|
|
18
|
+
import { codeIdentity } from './run-keys.js'
|
|
19
|
+
|
|
20
|
+
type BuildGraphOptions = Parameters<typeof buildTaskGraph>[0]
|
|
21
|
+
|
|
22
|
+
/** Moves every remembered graph: bump it when what a token stands for changes. */
|
|
23
|
+
const GRAPH_MEMO_VERSION = 'graph-memo-v1'
|
|
24
|
+
|
|
25
|
+
/** What names a run's configs: the lock they were read from and the projects discovered. */
|
|
26
|
+
export interface GraphMemo {
|
|
27
|
+
folds: FoldMemo
|
|
28
|
+
lock: string
|
|
29
|
+
metas: readonly ProjectMeta[]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A node as the memo holds it: its project and task, requested, its deps
|
|
34
|
+
* and the tasks it adds outputs to as positions in the list (ids in full
|
|
35
|
+
* were 123 KB for 1,206 tasks, and parsing them back cost what the walk
|
|
36
|
+
* saved), and the globs that add to its own outputs.
|
|
37
|
+
*/
|
|
38
|
+
type Row = [
|
|
39
|
+
project: string,
|
|
40
|
+
task: string,
|
|
41
|
+
requested: 0 | 1,
|
|
42
|
+
deps: number[],
|
|
43
|
+
adds?: number[] | null,
|
|
44
|
+
added?: string[] | null,
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* `buildTaskGraph`, answered from the memo when it built this graph before,
|
|
49
|
+
* and the token that names the graph (undefined when not remembered).
|
|
50
|
+
*/
|
|
51
|
+
export function memoGraph(
|
|
52
|
+
options: BuildGraphOptions,
|
|
53
|
+
memo: GraphMemo | undefined,
|
|
54
|
+
): { nodes: Map<string, TaskNode>; token: string | undefined } {
|
|
55
|
+
if (memo === undefined || options.undeclaredDeps !== undefined) {
|
|
56
|
+
return { nodes: buildTaskGraph(options), token: undefined }
|
|
57
|
+
}
|
|
58
|
+
const { projects, packageGraph } = options
|
|
59
|
+
const parts: string[] = [
|
|
60
|
+
GRAPH_MEMO_VERSION,
|
|
61
|
+
codeIdentity(),
|
|
62
|
+
memo.lock,
|
|
63
|
+
options.workspaceRoot ?? '',
|
|
64
|
+
JSON.stringify(options.rules ?? null),
|
|
65
|
+
String(packageGraph.edgeSource),
|
|
66
|
+
JSON.stringify(options.requested),
|
|
67
|
+
]
|
|
68
|
+
for (const m of memo.metas) {
|
|
69
|
+
parts.push(
|
|
70
|
+
m.name,
|
|
71
|
+
m.dir,
|
|
72
|
+
m.configPath ?? '\u0001',
|
|
73
|
+
packageGraph.directDeps(m.name).join('\u0001'),
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
for (const name of projects.keys()) parts.push(name)
|
|
77
|
+
const id = `\0graph\0${xxh3hex(JSON.stringify(options.requested))}`
|
|
78
|
+
const token = xxh3hex(parts.join('\0'))
|
|
79
|
+
const row = memo.folds.get(id, token)
|
|
80
|
+
if (row !== undefined) {
|
|
81
|
+
const nodes = rebuilt(row, projects)
|
|
82
|
+
if (nodes !== undefined) return { nodes, token }
|
|
83
|
+
}
|
|
84
|
+
const nodes = buildTaskGraph(options)
|
|
85
|
+
memo.folds.set(id, token, JSON.stringify(rowsOf(nodes)))
|
|
86
|
+
return { nodes, token }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function rowsOf(nodes: Map<string, TaskNode>): Row[] {
|
|
90
|
+
const at = new Map<string, number>()
|
|
91
|
+
for (const id of nodes.keys()) at.set(id, at.size)
|
|
92
|
+
const pos = (ids: readonly string[]): number[] => ids.map((d) => at.get(d)!)
|
|
93
|
+
return [...nodes.values()].map((n) => {
|
|
94
|
+
const row: Row = [n.projectName, n.taskName, n.requested ? 1 : 0, pos(n.deps)]
|
|
95
|
+
if (n.addsToOutputsOf !== undefined || n.outputsAddedToBy !== undefined)
|
|
96
|
+
row.push(
|
|
97
|
+
n.addsToOutputsOf === undefined ? null : pos(n.addsToOutputsOf),
|
|
98
|
+
n.outputsAddedToBy ?? null,
|
|
99
|
+
)
|
|
100
|
+
return row
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function rebuilt(
|
|
105
|
+
row: string,
|
|
106
|
+
projects: BuildGraphOptions['projects'],
|
|
107
|
+
): Map<string, TaskNode> | undefined {
|
|
108
|
+
let rows: Row[]
|
|
109
|
+
try {
|
|
110
|
+
rows = JSON.parse(row)
|
|
111
|
+
} catch {
|
|
112
|
+
return undefined
|
|
113
|
+
}
|
|
114
|
+
const list: TaskNode[] = []
|
|
115
|
+
for (const [projectName, taskName, requested] of rows) {
|
|
116
|
+
const project = projects.get(projectName)
|
|
117
|
+
const config = declaredTask(project?.config, taskName)
|
|
118
|
+
if (project === undefined || config === undefined) return undefined
|
|
119
|
+
list.push({
|
|
120
|
+
id: `${projectName}#${taskName}`,
|
|
121
|
+
projectName,
|
|
122
|
+
projectDir: project.dir,
|
|
123
|
+
taskName,
|
|
124
|
+
config,
|
|
125
|
+
deps: [],
|
|
126
|
+
requested: requested === 1,
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
const ids = (at: readonly number[]): string[] => at.map((i) => list[i]!.id)
|
|
130
|
+
const nodes = new Map<string, TaskNode>()
|
|
131
|
+
for (let i = 0; i < rows.length; i++) {
|
|
132
|
+
const [, , , deps, adds, added] = rows[i]!
|
|
133
|
+
const node = list[i]!
|
|
134
|
+
node.deps = ids(deps)
|
|
135
|
+
if (adds != null) node.addsToOutputsOf = ids(adds)
|
|
136
|
+
if (added != null) node.outputsAddedToBy = added
|
|
137
|
+
nodes.set(node.id, node)
|
|
138
|
+
}
|
|
139
|
+
return nodes
|
|
140
|
+
}
|
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
// CLI's `--affected` task selection (`vx show <task> --affected`), so both
|
|
3
3
|
// select over the graph the hooks leave.
|
|
4
4
|
|
|
5
|
+
import path from 'node:path'
|
|
5
6
|
import type { GraphRules, TaskNode } from '../graph/index.js'
|
|
7
|
+
import { relPosix, UserError } from '../util/index.js'
|
|
6
8
|
import { type ProjectMeta, validateProjectConfig } from '../workspace/index.js'
|
|
7
9
|
import { applyGraphHooks } from './plugin-host.js'
|
|
8
10
|
import type { VxPlugin } from './plugin.js'
|
|
9
|
-
import { isDefaultBuild } from '
|
|
11
|
+
import { isDefaultBuild } from '../graph/index.js'
|
|
10
12
|
|
|
11
13
|
export async function applyGraphStage(
|
|
12
14
|
plugins: readonly VxPlugin[],
|
|
@@ -23,6 +25,10 @@ export async function applyGraphStage(
|
|
|
23
25
|
// edit is held to what the loader accepts from a user: a misspelled
|
|
24
26
|
// `exec` field ran as an empty command and failed with no reason.
|
|
25
27
|
const configPaths = new Map(args.projectMetas.map((m) => [m.name, m.configPath]))
|
|
28
|
+
// A node under a directory no project owns, or another project's, ran,
|
|
29
|
+
// keyed and cleaned its outputs there: past the hard project boundary.
|
|
30
|
+
const dirs = new Map(args.projectMetas.map((m) => [m.name, path.resolve(m.dir)]))
|
|
31
|
+
const shown = (dir: string): string => relPosix(args.workspaceRoot, dir) || '.'
|
|
26
32
|
await applyGraphHooks(
|
|
27
33
|
plugins,
|
|
28
34
|
nodes,
|
|
@@ -34,6 +40,17 @@ export async function applyGraphStage(
|
|
|
34
40
|
},
|
|
35
41
|
(plugin) => {
|
|
36
42
|
for (const n of nodes.values()) {
|
|
43
|
+
const dir = dirs.get(n.projectName)
|
|
44
|
+
if (dir === undefined) {
|
|
45
|
+
throw new UserError(
|
|
46
|
+
`plugin '${plugin.name}' failed in graph: ${n.id}'s project ${n.projectName} is not a project in this workspace`,
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
if (path.resolve(n.projectDir) !== dir) {
|
|
50
|
+
throw new UserError(
|
|
51
|
+
`plugin '${plugin.name}' failed in graph: ${n.id}'s projectDir is ${shown(n.projectDir)}, not project ${n.projectName}'s directory ${shown(dir)}`,
|
|
52
|
+
)
|
|
53
|
+
}
|
|
37
54
|
if (isDefaultBuild(n.config)) continue
|
|
38
55
|
const own = configPaths.get(n.projectName) ?? null
|
|
39
56
|
validateProjectConfig(
|