@vzn/vx 0.0.18 → 0.0.21
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/LICENSE +1 -1
- package/README.md +99 -65
- package/package.json +10 -20
- package/src/bin.ts +12 -2
- package/src/cache/archive.ts +120 -18
- package/src/cache/cache.ts +313 -1324
- package/src/cache/chained-cache.ts +0 -8
- package/src/cache/config-evals.ts +89 -0
- package/src/cache/file-hashes.ts +262 -0
- package/src/cache/git-inputs.ts +637 -0
- package/src/cache/index.ts +16 -7
- package/src/cache/inputs.ts +246 -733
- package/src/cache/layer.ts +604 -0
- package/src/cache/layered-cache.ts +12 -17
- package/src/cache/output-index.ts +224 -0
- package/src/cache/policy.ts +84 -0
- package/src/cache/run-history.ts +147 -0
- package/src/cache/zstd.ts +160 -0
- package/src/cli/cache.ts +39 -11
- package/src/cli/completions.ts +104 -0
- package/src/cli/core-alias.ts +25 -0
- package/src/cli/help.ts +64 -36
- package/src/cli/index.ts +54 -15
- package/src/cli/info.ts +120 -91
- package/src/cli/init.ts +75 -0
- package/src/cli/last.ts +38 -6
- package/src/cli/lock.ts +2 -1
- package/src/cli/plan-format.ts +14 -3
- package/src/cli/plugin-commands.ts +15 -8
- package/src/cli/run.ts +84 -302
- package/src/cli/select.ts +332 -0
- package/src/cli/show.ts +144 -69
- package/src/cli/upgrade.ts +2 -1
- package/src/cli/watch.ts +647 -97
- package/src/cli/why.ts +44 -14
- package/src/cli/workspace-config.ts +126 -0
- package/src/config.ts +156 -136
- package/src/exec/env.ts +11 -0
- package/src/exec/executor.ts +11 -26
- package/src/exec/index.ts +6 -2
- package/src/{plugins/local-executor/index.ts → exec/local-executor.ts} +12 -16
- package/src/exec/runner.ts +57 -18
- package/src/exec/sandbox-binds.ts +182 -0
- package/src/exec/sandbox-paths.ts +50 -0
- package/src/exec/sandbox-runtime.ts +582 -370
- package/src/exec/sandbox-violations.ts +280 -0
- package/src/graph/index.ts +1 -9
- package/src/graph/scheduler.ts +158 -203
- package/src/graph/task-graph.ts +2 -2
- package/src/index.ts +97 -102
- package/src/orchestrator/admission.ts +137 -0
- package/src/orchestrator/deferred-outputs.ts +3 -0
- package/src/orchestrator/doctor.ts +255 -0
- package/src/orchestrator/events.ts +42 -8
- package/src/orchestrator/execute-task.ts +145 -554
- package/src/orchestrator/failure-mode.ts +169 -28
- package/src/orchestrator/framed-output.ts +41 -36
- package/src/orchestrator/history.ts +118 -98
- package/src/orchestrator/hit-restore.ts +221 -0
- package/src/orchestrator/index.ts +35 -4
- package/src/orchestrator/lockfile-claim.ts +281 -0
- package/src/orchestrator/logger.ts +13 -22
- package/src/orchestrator/metrics.ts +53 -20
- package/src/orchestrator/miss-save.ts +169 -0
- package/src/orchestrator/options.ts +34 -31
- package/src/orchestrator/persistent.ts +79 -0
- package/src/orchestrator/placement.ts +182 -0
- package/src/orchestrator/plugin-host.ts +195 -38
- package/src/orchestrator/plugin.ts +162 -9
- package/src/orchestrator/prepare.ts +77 -132
- package/src/orchestrator/projects.ts +259 -0
- package/src/orchestrator/remote-prefetch.ts +1 -1
- package/src/orchestrator/run-artifacts.ts +25 -3
- package/src/orchestrator/run-context.ts +1 -1
- package/src/orchestrator/run-records.ts +149 -0
- package/src/orchestrator/run-report.ts +2 -2
- package/src/orchestrator/run.ts +246 -597
- package/src/orchestrator/sandbox-request.ts +243 -0
- package/src/orchestrator/save-lane.ts +47 -0
- package/src/orchestrator/signals.ts +111 -0
- package/src/orchestrator/stable-keys.ts +60 -3
- package/src/orchestrator/status-line.ts +2 -1
- package/src/orchestrator/summary.ts +68 -25
- package/src/orchestrator/task-hash.ts +20 -18
- package/src/orchestrator/task-log-buffer.ts +8 -11
- package/src/orchestrator/telemetry-host.ts +11 -1
- package/src/orchestrator/telemetry.ts +3 -17
- package/src/util/cgroup.ts +160 -0
- package/src/util/edit-distance.ts +49 -0
- package/src/util/index.ts +11 -3
- package/src/util/paths.ts +23 -2
- package/src/util/settle.ts +19 -0
- package/src/util/verbs.ts +39 -0
- package/src/workspace/affected.ts +111 -11
- package/src/workspace/config-cache.ts +11 -2
- package/src/workspace/config-eval.ts +11 -4
- package/src/workspace/config-imports.ts +5 -4
- package/src/workspace/config-schema.ts +837 -0
- package/src/workspace/filter.ts +43 -6
- package/src/workspace/fingerprint.ts +37 -3
- package/src/workspace/index.ts +30 -3
- package/src/workspace/lockfile.ts +12 -7
- package/src/{cli → workspace}/migrate-scripts.ts +26 -19
- package/src/{cli/migrate.ts → workspace/migration.ts} +133 -124
- package/src/workspace/package-graph.ts +98 -28
- package/src/workspace/project-loader.ts +76 -746
- package/src/workspace/workspace.ts +98 -13
- package/plugins/local-cache/index.ts +0 -2
- package/plugins/local-executor/index.ts +0 -2
- package/plugins/schedule-history/index.ts +0 -2
- package/src/cache/cas-backend.ts +0 -115
- package/src/cache/digest.ts +0 -32
- package/src/cli/migrate-emit.ts +0 -21
- package/src/cli/migrate-nx.ts +0 -473
- package/src/cli/migrate-turbo.ts +0 -401
- package/src/cli/prune.ts +0 -291
- package/src/orchestrator/missing-plugin.ts +0 -9
- package/src/orchestrator/resources.ts +0 -45
- package/src/orchestrator/verify.ts +0 -174
- package/src/plugins/local-cache/index.ts +0 -11
- package/src/plugins/schedule-history/index.ts +0 -109
package/src/index.ts
CHANGED
|
@@ -3,20 +3,33 @@
|
|
|
3
3
|
// This is the stable cross-package contract — any plugin or integration
|
|
4
4
|
// package imports everything it needs from here via the bare
|
|
5
5
|
// `'@vzn/vx'` specifier (never a deep `src/...` path). The surface is pinned
|
|
6
|
-
// by tests/package-boundaries.test.ts; a widening updates that snapshot
|
|
7
|
-
// deliberately.
|
|
6
|
+
// by tests/package-boundaries.unsafe.test.ts; a widening updates that snapshot
|
|
7
|
+
// deliberately. The boundary law: docs/architecture.md § Repository shape.
|
|
8
8
|
|
|
9
9
|
export { VERSION } from './version.js'
|
|
10
10
|
|
|
11
11
|
// Clean error type — user-input failures print a message without a stack.
|
|
12
|
-
// `clampInt`
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
//
|
|
19
|
-
|
|
12
|
+
// `clampInt` rides along for the same reason the status predicates do:
|
|
13
|
+
// without it on the façade an integration package writes its own, and that
|
|
14
|
+
// had already happened. A bounds helper whose floor is load-bearing (a
|
|
15
|
+
// fractional SQL LIMIT is a datatype mismatch, not a smaller page) should
|
|
16
|
+
// have one implementation.
|
|
17
|
+
export { clampInt, UserError, isUserError } from './util/index.js'
|
|
18
|
+
// "Did you mean": the hint core's own verbs give for a near-miss name, for a
|
|
19
|
+
// plugin verb to give the same one.
|
|
20
|
+
export { nearMatches } from './util/index.js'
|
|
21
|
+
// Every hook a plugin may fill, in pipeline order — the one list the
|
|
22
|
+
// loader, the host and `vx info` read; exported so a doc pin outside core
|
|
23
|
+
// (the site's guides) holds its tables to the same list.
|
|
24
|
+
export { PLUGIN_HOOKS, type PluginHook } from './config.js'
|
|
25
|
+
// The workspace doctor's facts (`vx info`), for a host that answers the
|
|
26
|
+
// same question over another surface — `@vzn/vx-mcp`'s `getWorkspaceInfo`.
|
|
27
|
+
export { collectInfo, type CollectInfoOptions, type InfoFacts } from './orchestrator/index.js'
|
|
28
|
+
// The machine as this process may use it — cores and memory capped by the
|
|
29
|
+
// cgroup a container runs under. Core's default worker count reads the
|
|
30
|
+
// first; a policy that budgets memory (`@vzn/vx-schedule-history`) reads
|
|
31
|
+
// the second, so the cgroup walk has one implementation.
|
|
32
|
+
export { machineMemoryBytes, machineParallelism } from './util/index.js'
|
|
20
33
|
|
|
21
34
|
// Schema types and helpers (used by user vx.config files and presets).
|
|
22
35
|
export type {
|
|
@@ -28,36 +41,26 @@ export type {
|
|
|
28
41
|
CacheConfig,
|
|
29
42
|
CacheInputs,
|
|
30
43
|
CacheOutputs,
|
|
31
|
-
ResourcesConfig,
|
|
32
44
|
SandboxConfig,
|
|
33
|
-
|
|
45
|
+
SandboxGrants,
|
|
46
|
+
SandboxDenials,
|
|
34
47
|
} from './config.js'
|
|
35
48
|
export { defineProject, defineWorkspace } from './config.js'
|
|
49
|
+
export { definePlugin } from './orchestrator/index.js'
|
|
36
50
|
|
|
37
|
-
// Programmatic engine API
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
// the
|
|
41
|
-
//
|
|
42
|
-
// give agents + the submitter identity before/without a telemetry run.
|
|
51
|
+
// Programmatic engine API: run / plan / prepare (docs/cli.md § Programmatic
|
|
52
|
+
// API). The graph primitives, the cache-key hashing seam and the git / host
|
|
53
|
+
// context capture used to sit beside these for a distributed submitter that
|
|
54
|
+
// left the repo (2026-08); they went with it (2026-09-10) — a seam with no
|
|
55
|
+
// consumer is re-added when one appears, shaped by its use.
|
|
43
56
|
export { run, planRun, prepareRun } from './orchestrator/index.js'
|
|
44
|
-
// The per-task duration history a `schedule` plugin learns from
|
|
45
|
-
//
|
|
46
|
-
// this façade, which is what put these here).
|
|
47
|
-
export {
|
|
57
|
+
// The per-task duration history a `schedule` plugin learns from
|
|
58
|
+
// (`@vzn/vx-schedule-history` does; a plugin package reaches core only
|
|
59
|
+
// through this façade, which is what put these here).
|
|
60
|
+
export { LocalHistoryProvider } from './orchestrator/index.js'
|
|
48
61
|
export type { HistoryProvider, HistoryTable, TaskHistory } from './orchestrator/index.js'
|
|
49
62
|
export type { PreparedRun } from './orchestrator/index.js'
|
|
50
|
-
export { computeTaskHash, createHashCache, deriveStableKeys } from './orchestrator/index.js'
|
|
51
|
-
export type { DeriveStableKeysArgs, HashCache, StableKey } from './orchestrator/index.js'
|
|
52
|
-
export {
|
|
53
|
-
captureDefaultBranch,
|
|
54
|
-
captureGitContext,
|
|
55
|
-
captureHostContext,
|
|
56
|
-
captureWorkspaceIdentity,
|
|
57
|
-
detectCi,
|
|
58
|
-
} from './orchestrator/index.js'
|
|
59
63
|
export type { CiContext, GitContext, HostContext, WorkspaceIdentity } from './orchestrator/index.js'
|
|
60
|
-
export { FULL_CACHE_POLICY, parseCachePolicy } from './orchestrator/index.js'
|
|
61
64
|
export type {
|
|
62
65
|
CachePolicy,
|
|
63
66
|
Logger,
|
|
@@ -65,84 +68,95 @@ export type {
|
|
|
65
68
|
RunOptions,
|
|
66
69
|
RunSummary,
|
|
67
70
|
} from './orchestrator/index.js'
|
|
68
|
-
export { defaultLogger, resolveOutputView } from './orchestrator/index.js'
|
|
69
71
|
// `splitTaskId` is on the façade because the alternative is what happened:
|
|
70
72
|
// with only `taskId()` to JOIN an id and nothing exported to SPLIT one,
|
|
71
73
|
// consumers roll their own `split('#', 2)` and drift from the graph, which
|
|
72
74
|
// splits on the FIRST '#'.
|
|
73
|
-
export {
|
|
74
|
-
|
|
75
|
-
expandRequested,
|
|
76
|
-
isGroupTask,
|
|
77
|
-
markSurfacedDeps,
|
|
78
|
-
splitTaskId,
|
|
79
|
-
} from './graph/index.js'
|
|
80
|
-
export type {
|
|
81
|
-
OutputFingerprint,
|
|
82
|
-
TaskNode,
|
|
83
|
-
TaskOutcome,
|
|
84
|
-
TaskStatus,
|
|
85
|
-
VerifyVerdict,
|
|
86
|
-
} from './graph/index.js'
|
|
87
|
-
// The one output-tree diff implementation (verify verdicts + a serve's
|
|
88
|
-
// cross-machine fingerprint diff both name rels through it).
|
|
89
|
-
export { diffOutputTrees } from './orchestrator/index.js'
|
|
75
|
+
export { splitTaskId } from './graph/index.js'
|
|
76
|
+
export type { TaskNode, TaskOutcome, TaskStatus } from './graph/index.js'
|
|
90
77
|
|
|
91
|
-
// Cache classes + the layer interface (the `cache` capability's currency)
|
|
92
|
-
|
|
93
|
-
// (cas-backend.ts / digest.ts) stays module-internal until it has a
|
|
94
|
-
// consumer — no speculative public API. `cleanOutputs` is public for the
|
|
95
|
-
// distributed submitter's targeted output materialization (wipe declared
|
|
96
|
-
// outputs, then `restoreOutputs` the artifact — never a naive re-run).
|
|
97
|
-
export {
|
|
98
|
-
Cache,
|
|
99
|
-
LayeredCache,
|
|
100
|
-
GitFilesCache,
|
|
101
|
-
cleanOutputs,
|
|
102
|
-
resolveInputs,
|
|
103
|
-
resolveOutputs,
|
|
104
|
-
} from './cache/index.js'
|
|
78
|
+
// Cache classes + the layer interface (the `cache` capability's currency).
|
|
79
|
+
export { Cache, LayeredCache } from './cache/index.js'
|
|
105
80
|
export type { CacheLayer, RemoteCacheLayer, RunRecord, InvocationRecord } from './cache/index.js'
|
|
106
81
|
|
|
107
82
|
// Workspace discovery + the project/config catalog surface — an
|
|
108
|
-
// out-of-process service/CLI needs these. `
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
export { findWorkspaceRoot, loadWorkspaceConfig, resolveCacheDir } from './workspace/index.js'
|
|
115
|
-
export { readLockfile, LOCKFILE_NAME } from './workspace/index.js'
|
|
116
|
-
export type { Lockfile, LockfileEntry } from './workspace/index.js'
|
|
83
|
+
// out-of-process service/CLI needs these. `loadProjectConfig` is the RAW
|
|
84
|
+
// per-file load (`vx lock` freezes exactly that); the resolved view a run
|
|
85
|
+
// or `vx show` sees — plugin stages applied — is `loadResolvedProjects`
|
|
86
|
+
// below. Workspace's `listProjects` re-exports as `listProjectMetas` (the
|
|
87
|
+
// bare name once belonged to a metrics query).
|
|
88
|
+
export { findWorkspaceRoot } from './workspace/index.js'
|
|
117
89
|
export {
|
|
118
90
|
loadWorkspace,
|
|
119
91
|
loadProjectConfig,
|
|
120
92
|
listProjects as listProjectMetas,
|
|
121
93
|
} from './workspace/index.js'
|
|
122
94
|
export type { ProjectMeta } from './workspace/index.js'
|
|
95
|
+
// The package graph as a run sees it (workspace deps by manifest), for a tool
|
|
96
|
+
// that needs a project's transitive closure the way `vx run` computes it.
|
|
97
|
+
export { buildPackageGraph } from './workspace/index.js'
|
|
98
|
+
// The run path's RESOLVED view — plugin `config` and `project` stages
|
|
99
|
+
// applied, cached evaluations served — for a reader outside the CLI (the
|
|
100
|
+
// MCP server's `listTasks`, an embedder's task catalog). What `vx show`
|
|
101
|
+
// prints.
|
|
102
|
+
export { loadResolvedProjects } from './orchestrator/index.js'
|
|
103
|
+
export type { ProjectEntry } from './workspace/index.js'
|
|
104
|
+
// The migration seam: how a generated config is planned, rendered, guarded
|
|
105
|
+
// and written. `vx init` (package.json scripts) uses it in core;
|
|
106
|
+
// `@vzn/vx-migrate` (Turbo, Nx) and any other adoption tool use it from here.
|
|
107
|
+
export {
|
|
108
|
+
applyMigration,
|
|
109
|
+
PERSISTENT_TASK_NAMES,
|
|
110
|
+
PERSISTENT_TODO,
|
|
111
|
+
quoteTsLiteral,
|
|
112
|
+
} from './workspace/index.js'
|
|
113
|
+
export type {
|
|
114
|
+
ApplyMigrationArgs,
|
|
115
|
+
MigrationFormat,
|
|
116
|
+
GeneratedProject,
|
|
117
|
+
GeneratedTask,
|
|
118
|
+
MigrationPlan,
|
|
119
|
+
RawExpr,
|
|
120
|
+
} from './workspace/index.js'
|
|
123
121
|
|
|
124
122
|
// Plugin API — the run-level extension points. Behavior capabilities
|
|
125
123
|
// (executor / cache) change WHAT/HOW work runs; the observe-only `telemetry`
|
|
126
124
|
// capability is the canonical data-export path and cannot change behavior.
|
|
127
125
|
// A plugin is declared in vx.workspace.ts via defineWorkspace({ plugins }).
|
|
128
126
|
// See docs/design/observability-architecture-2026-06.md.
|
|
127
|
+
// The claimant's shell for a lockfile plugin (`@vzn/vx-lockfile`):
|
|
128
|
+
// the claim, the per-project key, the memo and the `--affected` diff around
|
|
129
|
+
// a parser; `reachDigests` is the Merkle-over-components digest both use.
|
|
130
|
+
export {
|
|
131
|
+
lockfileClaim,
|
|
132
|
+
reachDigests,
|
|
133
|
+
type LockfileClaimHooks,
|
|
134
|
+
type LockfileClaimOptions,
|
|
135
|
+
type ReachGraph,
|
|
136
|
+
} from './orchestrator/index.js'
|
|
129
137
|
export type {
|
|
130
138
|
VxPlugin,
|
|
139
|
+
PluginHooks,
|
|
140
|
+
PluginOrigin,
|
|
131
141
|
CacheContext,
|
|
132
142
|
ExecutorContext,
|
|
133
143
|
CommandContext,
|
|
144
|
+
FingerprintChange,
|
|
145
|
+
FingerprintClaim,
|
|
146
|
+
FingerprintContext,
|
|
134
147
|
GraphHookContext,
|
|
135
148
|
KeyHookContext,
|
|
136
149
|
PluginCommand,
|
|
137
150
|
ProjectHookContext,
|
|
138
151
|
ScheduleHookContext,
|
|
152
|
+
AdmitContext,
|
|
139
153
|
WorkspaceHookContext,
|
|
140
154
|
PluginSetupContext,
|
|
141
155
|
} from './orchestrator/index.js'
|
|
142
|
-
// Process primitives — what an executor plugin builds on. `@vzn/vx/plugins/local-executor`
|
|
143
|
-
// is the reference implementation and imports exactly these.
|
|
144
|
-
export { runCommand, runSandboxed } from './exec/index.js'
|
|
145
156
|
// The per-task execution contract a plugin's `executor` capability returns.
|
|
157
|
+
// (`runCommand` / `runSandboxed`, the local executor's own primitives, left
|
|
158
|
+
// the façade 2026-09-10: no executor plugin built on them — `@vzn/vx-reapi`
|
|
159
|
+
// speaks a wire — and a seam with no consumer is a special case in waiting.)
|
|
146
160
|
export type {
|
|
147
161
|
ExecuteRequest,
|
|
148
162
|
ExecuteResult,
|
|
@@ -166,7 +180,6 @@ export type {
|
|
|
166
180
|
// plugin that renders a run as a markdown table takes the same unvalidated
|
|
167
181
|
// task names core does, and the cloud job summary shipped without the escape.
|
|
168
182
|
export {
|
|
169
|
-
assembleRunSummary,
|
|
170
183
|
escapeMarkdownCell,
|
|
171
184
|
TELEMETRY_SCHEMA_VERSION,
|
|
172
185
|
deriveCacheSource,
|
|
@@ -192,29 +205,11 @@ export type {
|
|
|
192
205
|
TelemetrySink,
|
|
193
206
|
} from './orchestrator/index.js'
|
|
194
207
|
|
|
195
|
-
// The serializable
|
|
196
|
-
export {
|
|
197
|
-
export type { RunResult } from './orchestrator/index.js'
|
|
198
|
-
|
|
199
|
-
// Event bus + wire form — adapters (otel-bridge, custom subscribers) ride this.
|
|
200
|
-
export { createEventBus, wireForwarder, toWireEvent } from './orchestrator/index.js'
|
|
201
|
-
export type {
|
|
202
|
-
EventBus,
|
|
203
|
-
RunEvent,
|
|
204
|
-
RunEventSubscriber,
|
|
205
|
-
WireEvent,
|
|
206
|
-
TaskView,
|
|
207
|
-
OutcomeView,
|
|
208
|
-
} from './orchestrator/index.js'
|
|
208
|
+
// The serializable run result and its per-task views (what `run()` returns).
|
|
209
|
+
export type { RunResult, TaskView, OutcomeView } from './orchestrator/index.js'
|
|
209
210
|
|
|
210
|
-
//
|
|
211
|
-
//
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
getInvocation,
|
|
216
|
-
getRun,
|
|
217
|
-
listInvocations,
|
|
218
|
-
listRuns,
|
|
219
|
-
whyDidThisRerunQuery,
|
|
220
|
-
} from './orchestrator/index.js'
|
|
211
|
+
// The one run-history query a reader outside the CLI takes today: `vx why`'s
|
|
212
|
+
// answer, which `@vzn/vx-mcp` serves. The event bus / wire form and the
|
|
213
|
+
// other history readers (`listRuns`, `getRun`, …) left the façade 2026-09-10
|
|
214
|
+
// with no consumer; the telemetry seam above is the canonical export path.
|
|
215
|
+
export { whyDidThisRerunQuery } from './orchestrator/index.js'
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// What stands between the scheduler's `execute` callback and executeTask:
|
|
2
|
+
// two rules that decide whether a ready task runs NOW and whether its
|
|
3
|
+
// result may be SAVED. Neither changes what the task is.
|
|
4
|
+
//
|
|
5
|
+
// - In-flight dedup: an embedder running concurrent delegated runs in one
|
|
6
|
+
// process (a daemon built on the façade; core ships none) supplies an
|
|
7
|
+
// `inflight` registry; a task whose key
|
|
8
|
+
// a sibling is already computing waits for it and cache-hits on what it
|
|
9
|
+
// saved. A stateless `vx run` passes none and takes the untouched path.
|
|
10
|
+
// - Continue-taint: under `continueMode: 'always'` a task runs although an
|
|
11
|
+
// upstream failed. Its key is the healthy one (pure-input hashing) but
|
|
12
|
+
// its bytes are not, so the taint is tracked here and propagates through
|
|
13
|
+
// every success built on it — or a grand-dependent would cache the same
|
|
14
|
+
// partial tree one hop later. Only that mode ever executes a task behind
|
|
15
|
+
// a failure; the other modes skip it, so a default run carries no check.
|
|
16
|
+
//
|
|
17
|
+
// Split from `run.ts` on 2026-09-10 (pure motion).
|
|
18
|
+
|
|
19
|
+
import type { CachePolicy } from '../cache/index.js'
|
|
20
|
+
import { isGroupTask, type TaskNode, type TaskOutcome } from '../graph/index.js'
|
|
21
|
+
import { executeTask, type ExecuteArgs } from './execute-task.js'
|
|
22
|
+
import type { ShortCircuit } from './local-shortcircuit.js'
|
|
23
|
+
import { computeTaskHash, type ComputeHashArgs } from './task-hash.js'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Records which tasks ran behind a failure and answers, per task, whether
|
|
27
|
+
* this one does. Disabled (always `false`, nothing recorded) unless the
|
|
28
|
+
* run's `continueMode` is `'always'`.
|
|
29
|
+
*/
|
|
30
|
+
export function taintTracker(
|
|
31
|
+
enabled: boolean,
|
|
32
|
+
): (node: TaskNode, upstream: TaskOutcome[]) => boolean {
|
|
33
|
+
if (!enabled) return () => false
|
|
34
|
+
const tainted = new Set<string>()
|
|
35
|
+
return (node, upstream) => {
|
|
36
|
+
const taint = upstream.some(
|
|
37
|
+
// A restore-tier task may run before its deps and see holes here;
|
|
38
|
+
// it never saves anyway (a hit restores), so a hole is not taint.
|
|
39
|
+
(u) =>
|
|
40
|
+
u !== undefined &&
|
|
41
|
+
(u.status === 'failed' ||
|
|
42
|
+
u.status === 'aborted' ||
|
|
43
|
+
u.status === 'skipped' ||
|
|
44
|
+
tainted.has(u.node.id)),
|
|
45
|
+
)
|
|
46
|
+
if (taint) tainted.add(node.id)
|
|
47
|
+
return taint
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface AdmissionArgs {
|
|
52
|
+
/** The shared registry, or undefined for a stateless run (no dedup). */
|
|
53
|
+
inflight: Map<string, Promise<void>> | undefined
|
|
54
|
+
policy: CachePolicy
|
|
55
|
+
shortCircuit: ShortCircuit
|
|
56
|
+
/** Everything `computeTaskHash` needs besides the node and its upstream. */
|
|
57
|
+
hashArgs: Omit<ComputeHashArgs, 'node' | 'upstream' | 'nestedProjectDirs'> & {
|
|
58
|
+
nestedDirsByProject: ReadonlyMap<string, string[]>
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The run's execute-args builder. `reuseProbe: false` drops the up-front
|
|
62
|
+
* probe: a task joining a sibling must let executeTask probe fresh,
|
|
63
|
+
* because the probe predates the sibling's save.
|
|
64
|
+
*/
|
|
65
|
+
buildExecuteArgs: (node: TaskNode, upstream: TaskOutcome[], reuseProbe?: boolean) => ExecuteArgs
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The scheduler's `execute` callback: dedup when it can help, else executeTask. */
|
|
69
|
+
export function admitTasks(
|
|
70
|
+
args: AdmissionArgs,
|
|
71
|
+
): (node: TaskNode, upstream: TaskOutcome[]) => Promise<TaskOutcome> {
|
|
72
|
+
const { inflight, policy, shortCircuit, hashArgs, buildExecuteArgs } = args
|
|
73
|
+
return async (node, upstream) => {
|
|
74
|
+
// Dedup only helps when the sibling will WRITE the artifact and this
|
|
75
|
+
// task can READ it back — i.e. both axes effectively on.
|
|
76
|
+
const canRead = policy.localRead || policy.remoteRead
|
|
77
|
+
const canWrite = policy.localWrite || policy.remoteWrite
|
|
78
|
+
const cacheable =
|
|
79
|
+
!isGroupTask(node) &&
|
|
80
|
+
node.config.exec?.persistent === undefined &&
|
|
81
|
+
node.config.cache !== undefined &&
|
|
82
|
+
canRead &&
|
|
83
|
+
canWrite
|
|
84
|
+
// A restore-tier task (confirmed local hit, may run before its deps)
|
|
85
|
+
// needs no dedup — it's a restore, not an executor, and its live
|
|
86
|
+
// `upstream` is incomplete, so the dedup hash recompute would be
|
|
87
|
+
// wrong. Route it straight to executeTask, which reuses the up-front
|
|
88
|
+
// probe.
|
|
89
|
+
const restorable = shortCircuit.restoreTier.has(node.id)
|
|
90
|
+
if (inflight === undefined || !cacheable || restorable) {
|
|
91
|
+
return executeTask(buildExecuteArgs(node, upstream))
|
|
92
|
+
}
|
|
93
|
+
const { nestedDirsByProject, ...rest } = hashArgs
|
|
94
|
+
const hash = await computeTaskHash({
|
|
95
|
+
...rest,
|
|
96
|
+
node,
|
|
97
|
+
upstream,
|
|
98
|
+
nestedProjectDirs: nestedDirsByProject.get(node.projectName) ?? [],
|
|
99
|
+
})
|
|
100
|
+
const existing = inflight.get(hash)
|
|
101
|
+
if (existing !== undefined) {
|
|
102
|
+
// Join a sibling already computing this exact task: wait, then
|
|
103
|
+
// executeTask cache-hits on the artifact it just saved.
|
|
104
|
+
await existing.catch(() => {})
|
|
105
|
+
return executeTask(buildExecuteArgs(node, upstream, false))
|
|
106
|
+
}
|
|
107
|
+
// Become the executor: register a barrier siblings await. get→set has
|
|
108
|
+
// no await between, so registration is atomic — at most one executor
|
|
109
|
+
// per hash. Released on every exit (success / failure / throw).
|
|
110
|
+
let release!: () => void
|
|
111
|
+
inflight.set(
|
|
112
|
+
hash,
|
|
113
|
+
new Promise<void>((resolve) => {
|
|
114
|
+
release = resolve
|
|
115
|
+
}),
|
|
116
|
+
)
|
|
117
|
+
const execArgs = buildExecuteArgs(node, upstream)
|
|
118
|
+
try {
|
|
119
|
+
// `return await`, deliberately: the finally must run after the task
|
|
120
|
+
// settles, not when its promise is handed back.
|
|
121
|
+
return await executeTask(execArgs)
|
|
122
|
+
} finally {
|
|
123
|
+
// The barrier lifts when the ENTRY is there, not when the task is:
|
|
124
|
+
// the save runs off the slot (save-lane.ts), and a sibling released
|
|
125
|
+
// before it landed would probe a miss and run the task again. The
|
|
126
|
+
// executor's own return is not held — only the joiners wait.
|
|
127
|
+
const landed = execArgs.deferredSaves?.get(node.id)
|
|
128
|
+
const lift = (): void => {
|
|
129
|
+
execArgs.deferredSaves?.delete(node.id)
|
|
130
|
+
inflight.delete(hash)
|
|
131
|
+
release()
|
|
132
|
+
}
|
|
133
|
+
if (landed !== undefined) void landed.then(lift, lift)
|
|
134
|
+
else lift()
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -101,6 +101,9 @@ export class DeferredOutputs {
|
|
|
101
101
|
private async run(taskId: string): Promise<void> {
|
|
102
102
|
const entry = this.entries.get(taskId)!
|
|
103
103
|
const producer = this.args.nodes.get(taskId)!
|
|
104
|
+
// Project outputs only: a task declaring `outputs.workspaceFiles` is
|
|
105
|
+
// never deferred (`deferralEligibility` forces it eager), so nothing
|
|
106
|
+
// workspace-anchored can reach this path.
|
|
104
107
|
const outputs = producer.config.cache?.outputs.files ?? []
|
|
105
108
|
const nestedProjectDirs = this.args.nestedDirsByProject.get(producer.projectName) ?? []
|
|
106
109
|
const cleanArgs = { projectDir: producer.projectDir, outputs, nestedProjectDirs }
|