@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
package/src/history/metrics.ts
CHANGED
|
@@ -44,6 +44,12 @@ export interface RunSummaryRow {
|
|
|
44
44
|
hash: string
|
|
45
45
|
cpuMs: number | null
|
|
46
46
|
peakRssBytes: number | null
|
|
47
|
+
/** The most cores one sampling interval burned (v43); null when nothing sampled. */
|
|
48
|
+
peakCores: number | null
|
|
49
|
+
/** The mean resident memory over the samples, in bytes (v43); null when nothing sampled. */
|
|
50
|
+
avgRssBytes: number | null
|
|
51
|
+
/** Where an executor ran it (`small#1`, v44); null = here. */
|
|
52
|
+
placement: string | null
|
|
47
53
|
// High-precision spans relative to run start. Decimal strings on the wire
|
|
48
54
|
// (bigints aren't JSON-safe); use BigInt or Number on the client.
|
|
49
55
|
wallclockStartNs: string | null
|
|
@@ -57,6 +63,8 @@ export interface RunSummaryRow {
|
|
|
57
63
|
forwarded: boolean
|
|
58
64
|
/** Each remote read or write of its key that failed, `upload: <message>`, `; `-joined (v40). */
|
|
59
65
|
remoteError: string | null
|
|
66
|
+
/** The attempts a retried task took (>1, v23); null when it ran once. */
|
|
67
|
+
attempts: number | null
|
|
60
68
|
}
|
|
61
69
|
|
|
62
70
|
export interface ListRunsArgs {
|
|
@@ -114,10 +122,11 @@ export function listRuns(db: Database, args: ListRunsArgs = {}): RunSummaryRow[]
|
|
|
114
122
|
duration_ms AS durationMs, started_at AS startedAt, ended_at AS endedAt,
|
|
115
123
|
cache_hit AS cacheHit, restored, cached, hash,
|
|
116
124
|
cpu_ms AS cpuMs, peak_rss_bytes AS peakRssBytes,
|
|
125
|
+
peak_cores AS peakCores, avg_rss_bytes AS avgRssBytes, placement,
|
|
117
126
|
wallclock_start_ns AS wallclockStartNs, wallclock_end_ns AS wallclockEndNs,
|
|
118
127
|
blocked_by AS blockedBy, timed_out AS timedOut,
|
|
119
128
|
sandbox_violations AS sandboxViolations, not_ready AS notReady,
|
|
120
|
-
forward_args IS NOT NULL AS forwarded, remote_error AS remoteError
|
|
129
|
+
forward_args IS NOT NULL AS forwarded, remote_error AS remoteError, attempts
|
|
121
130
|
FROM runs ${clause} ORDER BY id DESC LIMIT ?`,
|
|
122
131
|
)
|
|
123
132
|
.all(...params, limit) as RawRow[]
|
|
@@ -285,7 +294,9 @@ export interface RunDetail {
|
|
|
285
294
|
}
|
|
286
295
|
|
|
287
296
|
export function getRun(db: Database, runId: string): RunDetail | null {
|
|
288
|
-
|
|
297
|
+
// In the order the run recorded them, as it printed them: `listRuns` is
|
|
298
|
+
// newest first, and the replay listed a dependency below its dependant.
|
|
299
|
+
const tasks = listRuns(db, { runId, limit: 100_000 }).reverse()
|
|
289
300
|
if (tasks.length === 0) return null
|
|
290
301
|
const startedAt = Math.min(...tasks.map((t) => t.startedAt))
|
|
291
302
|
const endedAt = Math.max(...tasks.map((t) => t.endedAt))
|
|
@@ -378,6 +389,18 @@ export interface WhyDidThisRerun {
|
|
|
378
389
|
note: string
|
|
379
390
|
}
|
|
380
391
|
|
|
392
|
+
/**
|
|
393
|
+
* Failed tasks in a run that wrote the cache: a passing task beside one
|
|
394
|
+
* ran past a failed dependency (--continue) and was never saved.
|
|
395
|
+
*/
|
|
396
|
+
function failedWithWrites(db: Database, id: string | null): number {
|
|
397
|
+
if (id === null) return 0
|
|
398
|
+
const row = db
|
|
399
|
+
.query('SELECT cache_policy AS p, failed_count AS f FROM invocations WHERE run_id = ?')
|
|
400
|
+
.get(id) as { p: string; f: number } | null
|
|
401
|
+
return row !== null && writesCache(row.p) ? row.f : 0
|
|
402
|
+
}
|
|
403
|
+
|
|
381
404
|
/**
|
|
382
405
|
* The endings an UNCHANGED cache key can have. A re-execution is told apart
|
|
383
406
|
* by the evidence the index holds: every one used to blame `--no-cache` /
|
|
@@ -389,7 +412,7 @@ function unchangedKeyNote(
|
|
|
389
412
|
db: Database,
|
|
390
413
|
runId: string,
|
|
391
414
|
this_: { hash: string; status: string; cacheHit: number | null; startedAt: number },
|
|
392
|
-
prev: { status: string; cacheHit: number | null; runId: string | null },
|
|
415
|
+
prev: { status: string; cacheHit: number | null; runId: string | null; blockedBy: string | null },
|
|
393
416
|
): string {
|
|
394
417
|
if (this_.cacheHit === null) {
|
|
395
418
|
return 'cache key unchanged — this run recorded no cache outcome, so whether it re-ran is unknown'
|
|
@@ -403,8 +426,12 @@ function unchangedKeyNote(
|
|
|
403
426
|
p: string
|
|
404
427
|
} | null
|
|
405
428
|
)?.p
|
|
429
|
+
// `--no-cache` reads and writes nothing, `--force` writes: each named
|
|
430
|
+
// apart, or a `--no-cache` run was told it had passed `--force`.
|
|
406
431
|
if (policy !== undefined && !readsCache(policy)) {
|
|
407
|
-
return
|
|
432
|
+
return writesCache(policy)
|
|
433
|
+
? 'cache key unchanged — re-executed because this run did not read the cache (--force, or a --cache-policy without read)'
|
|
434
|
+
: 'cache key unchanged — re-executed because this run did not use the cache (--no-cache, or a --cache-policy without read or write)'
|
|
408
435
|
}
|
|
409
436
|
// `get` answers null, not undefined, for no row.
|
|
410
437
|
const createdAt = (
|
|
@@ -424,17 +451,9 @@ function unchangedKeyNote(
|
|
|
424
451
|
prev.status === 'success' &&
|
|
425
452
|
prev.cacheHit === 0
|
|
426
453
|
) {
|
|
427
|
-
const
|
|
428
|
-
if (id === null) return 0
|
|
429
|
-
const row = db
|
|
430
|
-
.query('SELECT cache_policy AS p, failed_count AS f FROM invocations WHERE run_id = ?')
|
|
431
|
-
.get(id) as { p: string; f: number } | null
|
|
432
|
-
const writes = row !== null && writesCache(row.p)
|
|
433
|
-
return writes ? (row?.f ?? 0) : 0
|
|
434
|
-
}
|
|
435
|
-
const before = failedWithWrites(prev.runId)
|
|
454
|
+
const before = failedWithWrites(db, prev.runId)
|
|
436
455
|
if (before > 0) {
|
|
437
|
-
const now = failedWithWrites(runId)
|
|
456
|
+
const now = failedWithWrites(db, runId)
|
|
438
457
|
if (createdAt === undefined && now > 0) {
|
|
439
458
|
return `cache key unchanged — neither run saved it: each ran beside a failed task (${now} failed in this one), and a task run past a failed dependency (--continue) is never cached`
|
|
440
459
|
}
|
|
@@ -466,6 +485,18 @@ function unchangedKeyNote(
|
|
|
466
485
|
if (prevPolicy !== undefined && !writesCache(prevPolicy)) {
|
|
467
486
|
return 'cache key unchanged — the previous run on this key did not write the cache (--no-cache, or a --cache-policy without write), so there was nothing to hit'
|
|
468
487
|
}
|
|
488
|
+
// A task that passed behind a dependency its run never recorded: the
|
|
489
|
+
// one `--exclude-dependencies` left out, keyed but not run, so nothing
|
|
490
|
+
// built on it was saved (excluded-keys.ts).
|
|
491
|
+
if (
|
|
492
|
+
prev.blockedBy !== null &&
|
|
493
|
+
prev.runId !== null &&
|
|
494
|
+
db
|
|
495
|
+
.query('SELECT 1 FROM runs WHERE run_id = ? AND project = ? AND task = ?')
|
|
496
|
+
.get(prev.runId, ...splitTaskId(prev.blockedBy)) === null
|
|
497
|
+
) {
|
|
498
|
+
return `cache key unchanged — the previous run on this key built on a dependency it left out (${prev.blockedBy}, --exclude-dependencies), so it saved nothing and there was nothing to hit`
|
|
499
|
+
}
|
|
469
500
|
return 'cache key unchanged — the previous run on this key executed but no entry for it is in the cache (its save failed, or it was pruned since), so there was nothing to hit'
|
|
470
501
|
}
|
|
471
502
|
return 'cache key unchanged — re-executed on the same key though this run read the cache; vx cannot name the cause'
|
|
@@ -486,11 +517,19 @@ export function latestRunId(db: Database, taskId: string): string | null {
|
|
|
486
517
|
return row?.runId ?? null
|
|
487
518
|
}
|
|
488
519
|
|
|
520
|
+
/** Why a key that folds a task with no `inputs` never hits, and the fix. */
|
|
521
|
+
function keylessNote(taskId: string, keylessBy: string): string {
|
|
522
|
+
return keylessBy === taskId
|
|
523
|
+
? 'never cached: this task declares no `inputs`, so each run derives a new key; declare its `inputs`'
|
|
524
|
+
: `never cached: its key includes ${keylessBy}, which declares no \`inputs\`, so each run derives a new key; declare inputs on ${keylessBy}`
|
|
525
|
+
}
|
|
526
|
+
|
|
489
527
|
export function whyDidThisRerun(db: Database, runId: string, taskId: string): WhyDidThisRerun {
|
|
490
528
|
const [project, task] = splitTaskId(taskId)
|
|
491
529
|
const this_ = db
|
|
492
530
|
.query(
|
|
493
|
-
`SELECT id, hash, status, cache_hit AS cacheHit, restored, cached, started_at AS startedAt
|
|
531
|
+
`SELECT id, hash, status, cache_hit AS cacheHit, restored, cached, started_at AS startedAt,
|
|
532
|
+
keyless_by AS keylessBy
|
|
494
533
|
FROM runs WHERE run_id = ? AND project = ? AND task = ?`,
|
|
495
534
|
)
|
|
496
535
|
.get(runId, project, task) as
|
|
@@ -502,6 +541,7 @@ export function whyDidThisRerun(db: Database, runId: string, taskId: string): Wh
|
|
|
502
541
|
restored: number | null
|
|
503
542
|
cached: number | null
|
|
504
543
|
startedAt: number
|
|
544
|
+
keylessBy: string | null
|
|
505
545
|
}
|
|
506
546
|
| undefined
|
|
507
547
|
if (!this_) {
|
|
@@ -519,7 +559,7 @@ export function whyDidThisRerun(db: Database, runId: string, taskId: string): Wh
|
|
|
519
559
|
const prev = db
|
|
520
560
|
.query(
|
|
521
561
|
`SELECT hash, status, cache_hit AS cacheHit, restored, started_at AS startedAt,
|
|
522
|
-
run_id AS runId
|
|
562
|
+
run_id AS runId, blocked_by AS blockedBy
|
|
523
563
|
FROM runs WHERE project = ? AND task = ? AND id < ? AND ${KEYED_RUNS_SQL}
|
|
524
564
|
ORDER BY id DESC LIMIT 1`,
|
|
525
565
|
)
|
|
@@ -531,6 +571,7 @@ export function whyDidThisRerun(db: Database, runId: string, taskId: string): Wh
|
|
|
531
571
|
restored: number | null
|
|
532
572
|
startedAt: number
|
|
533
573
|
runId: string | null
|
|
574
|
+
blockedBy: string | null
|
|
534
575
|
}
|
|
535
576
|
| undefined
|
|
536
577
|
// …and this run must have one too, or there is nothing to compare.
|
|
@@ -561,19 +602,27 @@ export function whyDidThisRerun(db: Database, runId: string, taskId: string): Wh
|
|
|
561
602
|
? 'this task recorded no cache key (skipped, or a persistent task) — nothing to compare'
|
|
562
603
|
: this_.cached === 0
|
|
563
604
|
? // Not a cache decision at all: the task runs every time by design,
|
|
564
|
-
// and its key is derived only so dependents can fold it.
|
|
565
|
-
'
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
:
|
|
605
|
+
// and its key is derived only so dependents can fold it. A keyless
|
|
606
|
+
// one's is new each run, so every dependent misses: "folded by
|
|
607
|
+
// dependents" read as if they could hit.
|
|
608
|
+
this_.keylessBy === null
|
|
609
|
+
? 'this task declares no `cache` block — it runs on every invocation; its key is folded by dependents only'
|
|
610
|
+
: this_.keylessBy === taskId
|
|
611
|
+
? 'this task declares no `cache` block — it runs on every invocation; it declares no `inputs` either, so each run derives a new key and every task that depends on it misses; declare its `inputs`'
|
|
612
|
+
: `this task declares no \`cache\` block — it runs on every invocation; its key includes ${this_.keylessBy}, which declares no \`inputs\`, so each run derives a new key and every task that depends on it misses; declare inputs on ${this_.keylessBy}`
|
|
613
|
+
: this_.keylessBy !== null
|
|
614
|
+
? keylessNote(taskId, this_.keylessBy)
|
|
615
|
+
: prev && prev.hash !== this_.hash
|
|
616
|
+
? 'cache key changed between the previous run and this one (inputs differ)'
|
|
617
|
+
: prev
|
|
618
|
+
? // An unchanged key has two very different endings, and calling
|
|
619
|
+
// both a "re-run" answered the question wrong: a cache HIT did not
|
|
620
|
+
// re-run at all, so blaming `--no-cache` named a cause that cannot
|
|
621
|
+
// have applied. Only a run that EXECUTED on an unchanged key is
|
|
622
|
+
// the case this verb exists to explain. A row with no recorded
|
|
623
|
+
// cacheHit (older rows) is neither — say that, do not guess.
|
|
624
|
+
unchangedKeyNote(db, runId, this_, prev)
|
|
625
|
+
: 'no prior run for this (project, task)',
|
|
577
626
|
}
|
|
578
627
|
}
|
|
579
628
|
|
|
@@ -617,7 +666,7 @@ export function loadEntryInputs(db: Database, entryHash: string): TaskInputCompo
|
|
|
617
666
|
}
|
|
618
667
|
|
|
619
668
|
/**
|
|
620
|
-
* The rule `vx why`
|
|
669
|
+
* The rule `vx why` names a moved key by: join
|
|
621
670
|
* two keys' components over `(kind, name)`. A component in both with a
|
|
622
671
|
* different hash is `changed`, one only in `after` is `added`, one only in
|
|
623
672
|
* `before` is `removed`, and an equal one is counted. Entries are ordered by
|
|
@@ -659,8 +708,7 @@ export function diffKeyComponents(
|
|
|
659
708
|
}
|
|
660
709
|
}
|
|
661
710
|
// Code-unit order, not `localeCompare`: collation follows the machine's
|
|
662
|
-
// locale,
|
|
663
|
-
// so the CLI and the page could list one diff in two orders. Code units
|
|
711
|
+
// locale, so two machines could list one diff in two orders. Code units
|
|
664
712
|
// are also SQLite's BINARY order, the `entry_inputs` scan's own.
|
|
665
713
|
const byCodeUnit = (a: string, b: string): number => (a < b ? -1 : a > b ? 1 : 0)
|
|
666
714
|
entries.sort((x, y) => byCodeUnit(x.kind, y.kind) || byCodeUnit(x.name, y.name))
|
|
@@ -688,10 +736,17 @@ export function cacheKeyDiff(db: Database, runId: string, taskId: string): Cache
|
|
|
688
736
|
const [project, task] = splitTaskId(taskId)
|
|
689
737
|
const this_ = db
|
|
690
738
|
.query(
|
|
691
|
-
'SELECT id, hash, cached, status, started_at AS startedAt FROM runs WHERE run_id = ? AND project = ? AND task = ?',
|
|
739
|
+
'SELECT id, hash, cached, status, started_at AS startedAt, keyless_by AS keylessBy FROM runs WHERE run_id = ? AND project = ? AND task = ?',
|
|
692
740
|
)
|
|
693
741
|
.get(runId, project, task) as
|
|
694
|
-
| {
|
|
742
|
+
| {
|
|
743
|
+
id: number
|
|
744
|
+
hash: string
|
|
745
|
+
cached: number | null
|
|
746
|
+
status: string
|
|
747
|
+
startedAt: number
|
|
748
|
+
keylessBy: string | null
|
|
749
|
+
}
|
|
695
750
|
| undefined
|
|
696
751
|
if (!this_) {
|
|
697
752
|
return {
|
|
@@ -720,6 +775,18 @@ export function cacheKeyDiff(db: Database, runId: string, taskId: string): Cache
|
|
|
720
775
|
}
|
|
721
776
|
}
|
|
722
777
|
|
|
778
|
+
if (this_.keylessBy !== null) {
|
|
779
|
+
return {
|
|
780
|
+
runId,
|
|
781
|
+
taskId,
|
|
782
|
+
found: true,
|
|
783
|
+
previousRunId: null,
|
|
784
|
+
entries: [],
|
|
785
|
+
unchangedCount: 0,
|
|
786
|
+
note: `${this_.keylessBy} declares no \`inputs\`, so no fingerprints are kept to diff`,
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
723
790
|
const prev = db
|
|
724
791
|
.query(
|
|
725
792
|
`SELECT run_id AS runId, hash, status FROM runs
|
|
@@ -760,7 +827,9 @@ export function cacheKeyDiff(db: Database, runId: string, taskId: string): Cache
|
|
|
760
827
|
// A failed previous run kept no fingerprints; the last run that saved an
|
|
761
828
|
// entry still names what moved since then (the edit an agent made to fix
|
|
762
829
|
// the failure, or the one that broke it).
|
|
763
|
-
|
|
830
|
+
// So did one that passed beside a failed task (--continue).
|
|
831
|
+
const tainted = isPassStatus(prev.status) && failedWithWrites(db, prev.runId) > 0
|
|
832
|
+
if (cur.length > 0 && old.length === 0 && (!isPassStatus(prev.status) || tainted)) {
|
|
764
833
|
const saved = db
|
|
765
834
|
.query(
|
|
766
835
|
`SELECT run_id AS runId, hash FROM runs
|
|
@@ -771,7 +840,7 @@ export function cacheKeyDiff(db: Database, runId: string, taskId: string): Cache
|
|
|
771
840
|
.get(project, task, this_.id) as { runId: string | null; hash: string } | undefined
|
|
772
841
|
if (saved) {
|
|
773
842
|
const { entries, unchangedCount } = diffKeyComponents(loadEntryInputs(db, saved.hash), cur)
|
|
774
|
-
const since = `run ${saved.runId}, the last that saved an entry (the previous run ended ${prev.status})`
|
|
843
|
+
const since = `run ${saved.runId}, the last that saved an entry (${tainted ? 'the previous run ran beside a failed task, so it was not saved' : `the previous run ended ${prev.status}`})`
|
|
775
844
|
return {
|
|
776
845
|
runId,
|
|
777
846
|
taskId,
|
|
@@ -798,7 +867,9 @@ export function cacheKeyDiff(db: Database, runId: string, taskId: string): Cache
|
|
|
798
867
|
? `this run ended ${this_.status}`
|
|
799
868
|
: old.length === 0 && !isPassStatus(prev.status)
|
|
800
869
|
? `the previous run ended ${prev.status}`
|
|
801
|
-
:
|
|
870
|
+
: old.length === 0 && tainted
|
|
871
|
+
? 'the previous run ran beside a failed task'
|
|
872
|
+
: null
|
|
802
873
|
return {
|
|
803
874
|
runId,
|
|
804
875
|
taskId,
|
|
@@ -809,8 +880,8 @@ export function cacheKeyDiff(db: Database, runId: string, taskId: string): Cache
|
|
|
809
880
|
note:
|
|
810
881
|
this_.cached === 0
|
|
811
882
|
? // The verdict already says the task is uncached; this line says
|
|
812
|
-
//
|
|
813
|
-
'no `cache` block
|
|
883
|
+
// why no input can be named.
|
|
884
|
+
'with no `cache` block it saves no entry, so no input fingerprints are kept to say which of its `inputs` moved its key; with no `inputs`, every run gives it a new key'
|
|
814
885
|
: unsaved !== null
|
|
815
886
|
? `cache key changed but input fingerprints are unavailable — ${unsaved} and saved no entry; only the key change is known`
|
|
816
887
|
: 'cache key changed but input fingerprints are unavailable — the entry was pruned (or the row predates the `cached` column); only the key change is known',
|
|
@@ -833,6 +904,18 @@ export function cacheKeyDiff(db: Database, runId: string, taskId: string): Cache
|
|
|
833
904
|
}
|
|
834
905
|
}
|
|
835
906
|
|
|
907
|
+
/**
|
|
908
|
+
* Joins a group and a task under it in an `upstream` row's name: a group
|
|
909
|
+
* records no run, so its dependant's key names its members through it.
|
|
910
|
+
*/
|
|
911
|
+
export const GROUP_VIA = ' \u2190 '
|
|
912
|
+
|
|
913
|
+
/** The task an `upstream` row names: the last one, past any group it came through. */
|
|
914
|
+
export function upstreamTask(name: string): string {
|
|
915
|
+
const i = name.lastIndexOf(GROUP_VIA)
|
|
916
|
+
return i < 0 ? name : name.slice(i + GROUP_VIA.length)
|
|
917
|
+
}
|
|
918
|
+
|
|
836
919
|
export interface RootCause {
|
|
837
920
|
/** From the asked task down to the one whose own inputs moved. */
|
|
838
921
|
chain: string[]
|
|
@@ -842,23 +925,37 @@ export interface RootCause {
|
|
|
842
925
|
/**
|
|
843
926
|
* The tasks under `taskId`'s moved upstreams whose OWN key components
|
|
844
927
|
* moved in the same run, each with the chain that carried it up. A task
|
|
845
|
-
* visited once is not walked again (a diamond names its root once).
|
|
928
|
+
* visited once is not walked again (a diamond names its root once). A
|
|
929
|
+
* group records no run, so `groups` (group id → the tasks it gathers)
|
|
930
|
+
* carries the walk through it.
|
|
846
931
|
*/
|
|
847
932
|
export function rootCauses(
|
|
848
933
|
db: Database,
|
|
849
934
|
runId: string,
|
|
850
935
|
taskId: string,
|
|
851
936
|
entries: CacheKeyDiff['entries'],
|
|
937
|
+
groups: ReadonlyMap<string, readonly string[]> = new Map(),
|
|
852
938
|
): RootCause[] {
|
|
853
939
|
const roots: RootCause[] = []
|
|
854
940
|
const seen = new Set([taskId])
|
|
855
941
|
const walk = (chain: string[], moved: CacheKeyDiff['entries']): void => {
|
|
856
942
|
for (const e of moved) {
|
|
857
|
-
if (e.kind !== 'upstream'
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
943
|
+
if (e.kind !== 'upstream') continue
|
|
944
|
+
const task = upstreamTask(e.name)
|
|
945
|
+
if (seen.has(task)) continue
|
|
946
|
+
seen.add(task)
|
|
947
|
+
const next = [...chain, ...e.name.split(GROUP_VIA)]
|
|
948
|
+
const d = cacheKeyDiff(db, runId, task)
|
|
949
|
+
// A group: its members carry the walk, named in the key through it
|
|
950
|
+
// (`group ← task`) or, in a row that names the group alone, by the graph.
|
|
951
|
+
if (!d.found) {
|
|
952
|
+
const members = groups.get(task) ?? []
|
|
953
|
+
walk(
|
|
954
|
+
next,
|
|
955
|
+
members.map((name) => ({ ...e, name })),
|
|
956
|
+
)
|
|
957
|
+
continue
|
|
958
|
+
}
|
|
862
959
|
const own = d.entries.filter((x) => x.kind !== 'upstream')
|
|
863
960
|
if (own.length > 0) roots.push({ chain: next, entries: own })
|
|
864
961
|
walk(next, d.entries)
|
package/src/index.ts
CHANGED
|
@@ -16,13 +16,18 @@ export { UserError, isUserError } from './util/index.js'
|
|
|
16
16
|
export { PLUGIN_HOOKS, type PluginHook } from './config.js'
|
|
17
17
|
// The machine as this process may use it — cores and memory capped by the
|
|
18
18
|
// cgroup a container runs under. Core's default worker count reads the
|
|
19
|
-
// first; a policy that budgets memory (`@vzn/vx-
|
|
19
|
+
// first; a policy that budgets memory (`@vzn/vx-history`) reads
|
|
20
20
|
// the second, so the cgroup walk has one implementation.
|
|
21
21
|
export { machineMemoryBytes, machineParallelism } from './util/index.js'
|
|
22
22
|
// How core reads a task glob — its spelling, and which characters are
|
|
23
23
|
// wildcards (`[` `]` and an escape are) — for a plugin that turns declared
|
|
24
24
|
// outputs into something else (`@vzn/vx-reapi`'s literal `output_paths`).
|
|
25
|
-
export {
|
|
25
|
+
export {
|
|
26
|
+
isLiteralPattern,
|
|
27
|
+
normalizeGlob,
|
|
28
|
+
outputDirPrefixes,
|
|
29
|
+
restorableOutput,
|
|
30
|
+
} from './util/index.js'
|
|
26
31
|
|
|
27
32
|
// Schema types and helpers (used by user vx.config files and presets).
|
|
28
33
|
export type {
|
|
@@ -43,10 +48,13 @@ export type {
|
|
|
43
48
|
SandboxConfig,
|
|
44
49
|
SandboxGrants,
|
|
45
50
|
SandboxDenials,
|
|
51
|
+
Platform,
|
|
46
52
|
} from './config.js'
|
|
47
|
-
export { defineProject, defineWorkspace } from './config.js'
|
|
53
|
+
export { defineProject, defineWorkspace, PLATFORMS } from './config.js'
|
|
48
54
|
export { definePlugin, refuseUnknownOptions } from './orchestrator/index.js'
|
|
49
55
|
export { branchCacheScope } from './workspace/index.js'
|
|
56
|
+
// A plugin reading the base branch's history: a shallow clone fetches it.
|
|
57
|
+
export { mergeBaseWith } from './workspace/index.js'
|
|
50
58
|
|
|
51
59
|
// Programmatic engine API: run / plan / prepare (docs/cli.md § Programmatic
|
|
52
60
|
// API). The graph primitives, the cache-key hashing seam and the git / host
|
|
@@ -55,7 +63,7 @@ export { branchCacheScope } from './workspace/index.js'
|
|
|
55
63
|
// consumer is re-added when one appears, shaped by its use.
|
|
56
64
|
export { run, planRun, prepareRun } from './orchestrator/index.js'
|
|
57
65
|
// The per-task duration history a `schedule` plugin learns from
|
|
58
|
-
// (`@vzn/vx-
|
|
66
|
+
// (`@vzn/vx-history` does; a plugin package reaches core only
|
|
59
67
|
// through this façade, which is what put these here).
|
|
60
68
|
export { currentBranch } from './orchestrator/index.js'
|
|
61
69
|
export { LocalHistoryProvider } from './history/index.js'
|
|
@@ -75,7 +83,7 @@ export type { Logger, OutputView } from './report/index.js'
|
|
|
75
83
|
// with only `taskId()` to JOIN an id and nothing exported to SPLIT one,
|
|
76
84
|
// consumers roll their own `split('#', 2)` and drift from the graph, which
|
|
77
85
|
// splits on the FIRST '#'.
|
|
78
|
-
export { outputsOverlap, splitTaskId } from './graph/index.js'
|
|
86
|
+
export { criticalPath, outputsOverlap, splitTaskId, taskDefinitions } from './graph/index.js'
|
|
79
87
|
export type { TaskNode, TaskOutcome, TaskStatus } from './graph/index.js'
|
|
80
88
|
|
|
81
89
|
// Cache classes + the layer interface (the `cache` capability's currency).
|
|
@@ -84,14 +92,16 @@ export type {
|
|
|
84
92
|
CacheLayer,
|
|
85
93
|
PluginCache,
|
|
86
94
|
ArtifactInfo,
|
|
95
|
+
LentAccess,
|
|
87
96
|
RemoteCacheLayer,
|
|
88
97
|
RemoteOpenContext,
|
|
89
98
|
RunRecord,
|
|
90
99
|
InvocationRecord,
|
|
91
100
|
} from './cache/index.js'
|
|
92
|
-
// A plugin's keychain (`ctx.secret` on `setup` and a remote's `open`),
|
|
93
|
-
//
|
|
94
|
-
|
|
101
|
+
// A plugin's keychain (`ctx.secret` on `setup` and a remote's `open`), the
|
|
102
|
+
// guard that tells a missing secret from a fault, and the names whose values
|
|
103
|
+
// vx masks, for a plugin that must not send one where it is shown.
|
|
104
|
+
export { isSecretUnavailable, secretNamed } from './util/index.js'
|
|
95
105
|
export type {
|
|
96
106
|
PluginSecrets,
|
|
97
107
|
SecretRequest,
|
|
@@ -110,7 +120,7 @@ export { loadProjectConfig } from './workspace/index.js'
|
|
|
110
120
|
export type { ProjectMeta } from './workspace/index.js'
|
|
111
121
|
// The run path's RESOLVED view — plugin `config` and `project` stages
|
|
112
122
|
// applied — for a reader outside the CLI
|
|
113
|
-
// (`@vzn/vx-
|
|
123
|
+
// (`@vzn/vx-history`, an embedder's task catalog). What `vx show`
|
|
114
124
|
// prints.
|
|
115
125
|
export { loadResolvedProjects } from './orchestrator/index.js'
|
|
116
126
|
export type { ProjectEntry } from './workspace/index.js'
|
|
@@ -121,6 +131,7 @@ export type { ProjectEntry } from './workspace/index.js'
|
|
|
121
131
|
export {
|
|
122
132
|
applyMigration,
|
|
123
133
|
foldScriptHooks,
|
|
134
|
+
isStarterConfig,
|
|
124
135
|
PERSISTENT_TODO,
|
|
125
136
|
pruneOrphanPersistentNotes,
|
|
126
137
|
quoteTsLiteral,
|
|
@@ -163,6 +174,10 @@ export type {
|
|
|
163
174
|
ExecutorContext,
|
|
164
175
|
OutputContext,
|
|
165
176
|
CommandContext,
|
|
177
|
+
ArtifactRun,
|
|
178
|
+
ArtifactTask,
|
|
179
|
+
CacheFacts,
|
|
180
|
+
TaskArtifacts,
|
|
166
181
|
FingerprintChange,
|
|
167
182
|
FingerprintClaim,
|
|
168
183
|
FingerprintContext,
|
|
@@ -190,18 +205,35 @@ export type { RunStartInfo } from './report/index.js'
|
|
|
190
205
|
// speaks a wire — and a seam with no consumer is a special case in waiting.)
|
|
191
206
|
// A remote executor gives a task back to the local floor by rejecting with
|
|
192
207
|
// this (B-100): core runs it here and says why once.
|
|
193
|
-
export { executorFallback } from './exec/index.js'
|
|
208
|
+
export { executorFallback, hostPlatform, sampleTrees, type TreeUsage } from './exec/index.js'
|
|
194
209
|
// How the local executor puts the args after `--` on a command, for an
|
|
195
210
|
// executor that runs `ExecuteRequest.command` itself: `@vzn/vx-reapi` and
|
|
196
211
|
// the `vx init --plugin executor` example each joined their own, and a
|
|
197
212
|
// trailing comment or a template literal's closing newline lost the args
|
|
198
213
|
// there while the local floor kept them (X-112).
|
|
199
214
|
export { withForwardArgs } from './exec/index.js'
|
|
215
|
+
// The `npm_execpath` core gives a local task: a `@vzn/vx-dte` worker gives
|
|
216
|
+
// its tasks the same, its own machine's, not what launched it.
|
|
217
|
+
export { packageManagerPath } from './exec/index.js'
|
|
218
|
+
// A task's output both streams in the order written, bounded as core's
|
|
219
|
+
// capture is: what a worker saves through `TaskArtifacts` (`@vzn/vx-dte`).
|
|
220
|
+
export { BoundedCapture, type CapturedChunk } from './exec/index.js'
|
|
221
|
+
// Core's local executor, sandbox included, for a plugin that runs tasks
|
|
222
|
+
// itself: `@vzn/vx-dte`'s orchestrator and its workers.
|
|
223
|
+
export { localFloor, type LocalFloor } from './orchestrator/index.js'
|
|
224
|
+
// A task's sandbox request built where a worker runs it: the paths and the
|
|
225
|
+
// walls are that machine's (`@vzn/vx-dte`, an orchestrator of another OS).
|
|
226
|
+
// Its sweep takes back each placeholder the task never wrote, as core does.
|
|
227
|
+
export { sandboxRequestFor, sweepPlaceholders, type SandboxRequest } from './orchestrator/index.js'
|
|
228
|
+
// A runtime probe answered where a `runtime` executor runs the task.
|
|
229
|
+
export { answerRuntime } from './cache/index.js'
|
|
200
230
|
export type {
|
|
201
231
|
ExecuteRequest,
|
|
202
232
|
ExecuteResult,
|
|
203
233
|
ExecuteSandbox,
|
|
234
|
+
MachineReport,
|
|
204
235
|
ResolvedSandboxConfig,
|
|
236
|
+
ServerSpec,
|
|
205
237
|
TaskExecutor,
|
|
206
238
|
TaskPlacement,
|
|
207
239
|
} from './exec/index.js'
|
|
@@ -212,12 +212,16 @@ export function admitTasks(
|
|
|
212
212
|
upstream,
|
|
213
213
|
nestedProjectDirs: nestedDirsByProject.get(node.projectName) ?? [],
|
|
214
214
|
})
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
215
|
+
// Join a sibling already computing this exact task: wait, then
|
|
216
|
+
// executeTask cache-hits on the artifact it just saved. A sibling that
|
|
217
|
+
// saved nothing (it failed) leaves this task to run, so the joiners
|
|
218
|
+
// released together take the barrier in turn: each returning straight
|
|
219
|
+
// to executeTask ran the task side by side in one directory.
|
|
220
|
+
let joined = false
|
|
221
|
+
for (let existing = inflight.get(hash); existing !== undefined;) {
|
|
222
|
+
joined = true
|
|
219
223
|
await existing.catch(() => {})
|
|
220
|
-
|
|
224
|
+
existing = inflight.get(hash)
|
|
221
225
|
}
|
|
222
226
|
// Become the executor: register a barrier siblings await. get→set has
|
|
223
227
|
// no await between, so registration is atomic — at most one executor
|
|
@@ -229,7 +233,7 @@ export function admitTasks(
|
|
|
229
233
|
release = resolve
|
|
230
234
|
}),
|
|
231
235
|
)
|
|
232
|
-
const execArgs = buildExecuteArgs(node, upstream)
|
|
236
|
+
const execArgs = buildExecuteArgs(node, upstream, !joined)
|
|
233
237
|
try {
|
|
234
238
|
// `return await`, deliberately: the finally must run after the task
|
|
235
239
|
// settles, not when its promise is handed back.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// `--affected` per task (
|
|
1
|
+
// `--affected` per task (2026-10-04). The diff seeds the tasks it
|
|
2
2
|
// reaches; a requested task runs when its `dependsOn` closure holds one.
|
|
3
3
|
// So a change reaches another project only along a task edge: `web#test`
|
|
4
4
|
// behind `^build` runs when `ui#build` (or what it depends on) is reached,
|
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
import { applyGraphStage } from './graph-stage.js'
|
|
27
27
|
import { hasHook } from './plugin-host.js'
|
|
28
28
|
import type { VxPlugin } from './plugin.js'
|
|
29
|
-
import { isDefaultBuild } from '
|
|
29
|
+
import { isDefaultBuild } from '../graph/index.js'
|
|
30
30
|
|
|
31
31
|
/** Every file in this set re-keys each of its project's tasks, whatever their inputs. */
|
|
32
32
|
const KEYED_BY_EVERY_TASK: ReadonlySet<string> = new Set(PROJECT_CONFIG_FILENAMES)
|
|
@@ -50,8 +50,10 @@ function wholeProjects(
|
|
|
50
50
|
const keyed = Object.values(projects.get(name)?.config.tasks ?? {}).filter(
|
|
51
51
|
(t) => t.exec !== undefined && t.inputs !== undefined,
|
|
52
52
|
)
|
|
53
|
+
const links = changes.dirLinksIn?.get(name)
|
|
53
54
|
const unclaimed = (rel: string): boolean =>
|
|
54
|
-
KEYED_BY_EVERY_TASK.has(rel) ||
|
|
55
|
+
KEYED_BY_EVERY_TASK.has(rel) ||
|
|
56
|
+
(!byKeys && !keyed.some((t) => declaresInput(t, rel, null, links?.has(rel))))
|
|
55
57
|
const rel = rels.find(unclaimed)
|
|
56
58
|
if (rel !== undefined) whole.set(name, rel)
|
|
57
59
|
}
|
|
@@ -118,7 +120,7 @@ export function affectedRoots(
|
|
|
118
120
|
// it passes through: a holder's own node is already in `deps`.
|
|
119
121
|
const passesChanged = (n: TaskNode): boolean => {
|
|
120
122
|
for (const raw of n.config.dependsOn ?? []) {
|
|
121
|
-
const spec = parseDependencySpec(raw)
|
|
123
|
+
const spec = parseDependencySpec(raw, 'dependsOn')
|
|
122
124
|
if (spec.kind !== 'deps' || spec.negated) continue
|
|
123
125
|
const re = isTaskPattern(spec.task) ? compileTaskPattern(spec.task) : null
|
|
124
126
|
const holds = (name: string): boolean => {
|
|
@@ -147,7 +149,9 @@ export function affectedRoots(
|
|
|
147
149
|
// Asked of every node, not only the changed projects' (the
|
|
148
150
|
// `workspaceFiles` owners): a `graph` hook may have given the glob.
|
|
149
151
|
if (keyed) {
|
|
150
|
-
const file = changes.changed.find((rel) =>
|
|
152
|
+
const file = changes.changed.find((rel) =>
|
|
153
|
+
declaresInput(task, null, rel, changes.dirLinks?.has(rel)),
|
|
154
|
+
)
|
|
151
155
|
const dir =
|
|
152
156
|
file === undefined
|
|
153
157
|
? (changes.nested ?? []).find((d) => workspaceFilesReachInto(task, d))
|
|
@@ -171,7 +175,8 @@ export function affectedRoots(
|
|
|
171
175
|
seeds?.set(n.id, inProject(n.projectName, whole.get(n.projectName)))
|
|
172
176
|
return true
|
|
173
177
|
}
|
|
174
|
-
const
|
|
178
|
+
const links = changes.dirLinksIn?.get(n.projectName)
|
|
179
|
+
const rel = rels.find((r) => declaresInput(task, r, null, links?.has(r)))
|
|
175
180
|
if (rel === undefined) return false
|
|
176
181
|
if (seeds !== null) seeds.set(n.id, { ...inProject(n.projectName, rel), kind: 'input' })
|
|
177
182
|
return true
|
|
@@ -156,6 +156,8 @@ export class DeferredOutputs {
|
|
|
156
156
|
await this.args.cache.save({
|
|
157
157
|
hash: entry.hash,
|
|
158
158
|
projectDir: producer.projectDir,
|
|
159
|
+
// As saveMiss: a link is bounded by the workspace, not the project.
|
|
160
|
+
workspaceRoot: this.args.workspaceRoot,
|
|
159
161
|
outputFiles,
|
|
160
162
|
...(emptyDirs.length > 0 ? { outputDirs: emptyDirs } : {}),
|
|
161
163
|
entry: entry.entry,
|