@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
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
// The workspace doctor's facts — what `vx info` prints and `vx mcp`'s
|
|
2
|
+
// `getWorkspaceInfo` returns: versions, the git status cache, the
|
|
3
|
+
// workspace's shape, the plugins and the seams each fills, what a run
|
|
4
|
+
// will use (workers, memory) and where those come from, the cache's
|
|
5
|
+
// versions and state, the flaky tasks, the lock. One collector, so the
|
|
6
|
+
// verb and the agent tool cannot disagree; the CLI renders, this gathers.
|
|
7
|
+
|
|
8
|
+
import os from 'node:os'
|
|
9
|
+
import path from 'node:path'
|
|
10
|
+
import { Cache, CACHE_VERSION, noteSchemaReset, SCHEMA_VERSION } from '../cache/index.js'
|
|
11
|
+
import { PLUGIN_HOOKS } from '../config.js'
|
|
12
|
+
import {
|
|
13
|
+
cgroupCpuQuota,
|
|
14
|
+
cgroupMemoryLimitBytes,
|
|
15
|
+
machineMemoryBytes,
|
|
16
|
+
machineParallelism,
|
|
17
|
+
} from '../util/index.js'
|
|
18
|
+
import { VERSION } from '../version.js'
|
|
19
|
+
import {
|
|
20
|
+
buildPackageGraph,
|
|
21
|
+
computeWorkspaceFingerprint,
|
|
22
|
+
findWorkspaceRoot,
|
|
23
|
+
listProjects,
|
|
24
|
+
loadProjectConfig,
|
|
25
|
+
loadWorkspace,
|
|
26
|
+
lockfilePath,
|
|
27
|
+
type ProjectMeta,
|
|
28
|
+
resolveCacheDir,
|
|
29
|
+
} from '../workspace/index.js'
|
|
30
|
+
import { flakyTasks, type FlakyTask } from './failure-mode.js'
|
|
31
|
+
import type { VxPlugin } from './plugin.js'
|
|
32
|
+
import { loadProjects, loadWorkspacePlugins } from './projects.js'
|
|
33
|
+
|
|
34
|
+
const warnToStderr = (message: string): void => {
|
|
35
|
+
process.stderr.write(`${message}\n`)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The doctor's facts, typed: what `--format json` prints and the pretty rows render. */
|
|
39
|
+
export interface InfoFacts {
|
|
40
|
+
vx: string
|
|
41
|
+
bun: string
|
|
42
|
+
git: string | null
|
|
43
|
+
/** null when git could not answer. */
|
|
44
|
+
gitStatusCache: { fsmonitor: boolean; untrackedCache: boolean } | null
|
|
45
|
+
workspaceRoot: string
|
|
46
|
+
projects: number
|
|
47
|
+
tasks: number
|
|
48
|
+
plugins: Array<{ name: string; seams: string[] }>
|
|
49
|
+
/**
|
|
50
|
+
* The worker count a run defaults to and where it comes from: the
|
|
51
|
+
* workspace's `concurrency`, else the cores this process may use — the
|
|
52
|
+
* CPU count capped by a cgroup quota (`cpuQuota`, in cores, null when
|
|
53
|
+
* none binds).
|
|
54
|
+
*/
|
|
55
|
+
workers: {
|
|
56
|
+
count: number
|
|
57
|
+
source: 'workspace' | 'cgroup' | 'cores'
|
|
58
|
+
cores: number
|
|
59
|
+
cpuQuota: number | null
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* What a memory-packing policy budgets: the machine's total capped by
|
|
63
|
+
* the cgroup limit (`cgroupLimitBytes`, null when none binds).
|
|
64
|
+
*/
|
|
65
|
+
memory: { usableBytes: number; totalBytes: number; cgroupLimitBytes: number | null }
|
|
66
|
+
cacheDir: string
|
|
67
|
+
cacheVersion: string
|
|
68
|
+
schemaVersion: string
|
|
69
|
+
cacheEntries: number
|
|
70
|
+
cacheBytes: number
|
|
71
|
+
orphans: { artifacts: number; bytes: number }
|
|
72
|
+
runs24h: number
|
|
73
|
+
hits24h: number
|
|
74
|
+
/** Tasks the retained history shows both passing and failing on unchanged inputs. */
|
|
75
|
+
flakyTasks: FlakyTask[]
|
|
76
|
+
lockfile: boolean
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface CollectInfoOptions {
|
|
80
|
+
/** `--cache-dir`: report on the cache a run with the same flag uses. */
|
|
81
|
+
readonly cacheDir?: string
|
|
82
|
+
/** Where a warning (a schema reset, a plugin's note) goes. Default: stderr. */
|
|
83
|
+
readonly warn?: (message: string) => void
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function collectInfo(cwd: string, opts: CollectInfoOptions = {}): Promise<InfoFacts> {
|
|
87
|
+
const warn = opts.warn ?? warnToStderr
|
|
88
|
+
const root = await findWorkspaceRoot(cwd)
|
|
89
|
+
const metas = await listProjects(await loadWorkspace(root))
|
|
90
|
+
const { workspaceConfig, plugins } = await loadWorkspacePlugins(root, warn)
|
|
91
|
+
const cacheDir =
|
|
92
|
+
opts.cacheDir === undefined
|
|
93
|
+
? resolveCacheDir(root, workspaceConfig)
|
|
94
|
+
: path.resolve(cwd, opts.cacheDir)
|
|
95
|
+
const cache = new Cache(cacheDir)
|
|
96
|
+
noteSchemaReset(cache, warn)
|
|
97
|
+
let stats
|
|
98
|
+
let orphans
|
|
99
|
+
let flaky: FlakyTask[]
|
|
100
|
+
let taskCount = 0
|
|
101
|
+
try {
|
|
102
|
+
stats = cache.stats()
|
|
103
|
+
orphans = await cache.orphanStats()
|
|
104
|
+
flaky = flakyTasks(cache.dbHandle())
|
|
105
|
+
// The run path's load — a plugin's `project` stage counts — so the
|
|
106
|
+
// doctor's task count is the number a run would see. A broken config
|
|
107
|
+
// must not take the doctor down with it: the count then falls back to
|
|
108
|
+
// the configs that do load, one by one, the broken ones as zero.
|
|
109
|
+
try {
|
|
110
|
+
const loaded = await loadProjects({
|
|
111
|
+
workspaceRoot: root,
|
|
112
|
+
cacheDir,
|
|
113
|
+
plugins,
|
|
114
|
+
projectMetas: metas,
|
|
115
|
+
packageGraph: buildPackageGraph([...metas]),
|
|
116
|
+
seeds: 'all',
|
|
117
|
+
closure: false,
|
|
118
|
+
lock: null,
|
|
119
|
+
evalCache: { store: cache, workspaceFingerprint: await computeWorkspaceFingerprint(root) },
|
|
120
|
+
warn,
|
|
121
|
+
})
|
|
122
|
+
for (const p of loaded.projects.values()) {
|
|
123
|
+
taskCount += Object.keys(p.config.tasks ?? {}).length
|
|
124
|
+
}
|
|
125
|
+
} catch {
|
|
126
|
+
taskCount = await countLoadableTasks(metas)
|
|
127
|
+
}
|
|
128
|
+
} finally {
|
|
129
|
+
cache.close()
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const lockPresent = await Bun.file(lockfilePath(root)).exists()
|
|
133
|
+
return {
|
|
134
|
+
vx: VERSION,
|
|
135
|
+
bun: Bun.version,
|
|
136
|
+
git: gitVersion(),
|
|
137
|
+
// The one `git status` walk per run is the warm path's critical path on
|
|
138
|
+
// a large tree (~55 ms at 1000 projects, measured 2026-09-02). git's
|
|
139
|
+
// own caches make it near-free after the first run, and they are OFF by
|
|
140
|
+
// default — say so, since nothing else in a run would.
|
|
141
|
+
gitStatusCache: gitStatusCache(root),
|
|
142
|
+
workspaceRoot: root,
|
|
143
|
+
projects: metas.length,
|
|
144
|
+
tasks: taskCount,
|
|
145
|
+
// Which plugins loaded and which seams each fills, in pipeline order —
|
|
146
|
+
// the answer to "why did this task run there / cache there / not at
|
|
147
|
+
// all" before reading any config. A declined seam still costs nothing;
|
|
148
|
+
// this names the declarations, not what a run consulted.
|
|
149
|
+
plugins: plugins.map((p) => ({ name: p.name, seams: filledSeams(p) })),
|
|
150
|
+
workers: workersFact(workspaceConfig?.concurrency),
|
|
151
|
+
memory: memoryFact(),
|
|
152
|
+
cacheDir,
|
|
153
|
+
// The two versions a bug report needs and the reset notice names: the
|
|
154
|
+
// key prefix (a bump orphans every entry) and the index schema (a
|
|
155
|
+
// mismatch drops every table).
|
|
156
|
+
cacheVersion: CACHE_VERSION,
|
|
157
|
+
schemaVersion: SCHEMA_VERSION,
|
|
158
|
+
cacheEntries: stats.entryCount,
|
|
159
|
+
cacheBytes: stats.totalBytes,
|
|
160
|
+
// The index is authoritative, so a row-less artifact is bytes nothing
|
|
161
|
+
// will ever hit — and only `vx cache prune` reclaims them (after an
|
|
162
|
+
// upgrade's schema reset, most often).
|
|
163
|
+
orphans: { artifacts: orphans.orphans, bytes: orphans.orphanBytes },
|
|
164
|
+
runs24h: stats.runCountLast24h,
|
|
165
|
+
hits24h: stats.hitCountLast24h,
|
|
166
|
+
// Same inputs, both outcomes — the history's definition of flaky, over
|
|
167
|
+
// the 30 days it keeps. A run names its own findings in its footer;
|
|
168
|
+
// this is the workspace's standing list.
|
|
169
|
+
flakyTasks: flaky,
|
|
170
|
+
lockfile: lockPresent,
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** The seams a plugin can fill, in pipeline order: the one hook list, less the lifecycle end. */
|
|
175
|
+
const SEAMS = PLUGIN_HOOKS.filter((h) => h !== 'teardown')
|
|
176
|
+
|
|
177
|
+
function filledSeams(p: VxPlugin): string[] {
|
|
178
|
+
return SEAMS.filter((s) => p[s as keyof VxPlugin] !== undefined)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Inside a container the CPU count and `os.totalmem()` are the HOST's; the
|
|
183
|
+
* cgroup is what the kernel enforces, and a run that used the host's
|
|
184
|
+
* numbers would be the OOM killer's. The doctor says which one a run
|
|
185
|
+
* reads, because nothing else would.
|
|
186
|
+
*/
|
|
187
|
+
function workersFact(workspaceConcurrency: number | undefined): InfoFacts['workers'] {
|
|
188
|
+
const cores = Math.max(1, navigator.hardwareConcurrency)
|
|
189
|
+
const quota = process.platform === 'linux' ? (cgroupCpuQuota() ?? null) : null
|
|
190
|
+
if (workspaceConcurrency !== undefined) {
|
|
191
|
+
return { count: workspaceConcurrency, source: 'workspace', cores, cpuQuota: quota }
|
|
192
|
+
}
|
|
193
|
+
const machine = machineParallelism()
|
|
194
|
+
return { count: machine, source: machine < cores ? 'cgroup' : 'cores', cores, cpuQuota: quota }
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function memoryFact(): InfoFacts['memory'] {
|
|
198
|
+
const totalBytes = os.totalmem()
|
|
199
|
+
const limit = process.platform === 'linux' ? (cgroupMemoryLimitBytes() ?? null) : null
|
|
200
|
+
return { usableBytes: machineMemoryBytes(), totalBytes, cgroupLimitBytes: limit }
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Whether git's fsmonitor / untracked cache are on. Reported as a fact,
|
|
205
|
+
* not a remedy: interleaved A/B at 1000 projects measured neither moving
|
|
206
|
+
* the warm run (STATUS, waves 5 and the 2026-09-03 refutations) — the
|
|
207
|
+
* status walk's cost is git's own, and vx already overlaps it.
|
|
208
|
+
*/
|
|
209
|
+
function gitStatusCache(root: string): InfoFacts['gitStatusCache'] {
|
|
210
|
+
try {
|
|
211
|
+
const p = Bun.spawnSync({
|
|
212
|
+
cmd: ['git', 'config', '--get-regexp', '^core\\.(fsmonitor|untrackedcache)$'],
|
|
213
|
+
cwd: root,
|
|
214
|
+
stdout: 'pipe',
|
|
215
|
+
stderr: 'pipe',
|
|
216
|
+
})
|
|
217
|
+
// exit 1 is git's "no key matched": both off, still an answer.
|
|
218
|
+
if (p.exitCode !== 0 && p.exitCode !== 1) return null
|
|
219
|
+
const out = p.exitCode === 0 ? new TextDecoder().decode(p.stdout) : ''
|
|
220
|
+
const on = (key: string): boolean =>
|
|
221
|
+
new RegExp(`^core\\.${key} (true|1|yes|on)$`, 'im').test(out)
|
|
222
|
+
return { fsmonitor: on('fsmonitor'), untrackedCache: on('untrackedcache') }
|
|
223
|
+
} catch {
|
|
224
|
+
return null
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function gitVersion(): string | null {
|
|
229
|
+
try {
|
|
230
|
+
const p = Bun.spawnSync({ cmd: ['git', '--version'], stdout: 'pipe', stderr: 'pipe' })
|
|
231
|
+
if (p.exitCode !== 0) return null
|
|
232
|
+
return new TextDecoder()
|
|
233
|
+
.decode(p.stdout)
|
|
234
|
+
.trim()
|
|
235
|
+
.replace(/^git version /, '')
|
|
236
|
+
} catch {
|
|
237
|
+
return null
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function countLoadableTasks(metas: readonly ProjectMeta[]): Promise<number> {
|
|
242
|
+
let count = 0
|
|
243
|
+
await Promise.all(
|
|
244
|
+
metas.map(async (meta) => {
|
|
245
|
+
if (meta.configPath === null) return
|
|
246
|
+
try {
|
|
247
|
+
const config = await loadProjectConfig(meta.configPath)
|
|
248
|
+
count += Object.keys(config.tasks ?? {}).length
|
|
249
|
+
} catch {
|
|
250
|
+
// counted as zero
|
|
251
|
+
}
|
|
252
|
+
}),
|
|
253
|
+
)
|
|
254
|
+
return count
|
|
255
|
+
}
|
|
@@ -59,7 +59,7 @@ export function createEventBus(): EventBus {
|
|
|
59
59
|
// depends on it), so emit must never reorder or defer.
|
|
60
60
|
//
|
|
61
61
|
// A throwing subscriber must NEVER break the run — the whole point
|
|
62
|
-
// of the bus is that a surface (
|
|
62
|
+
// of the bus is that a surface (an embedder's renderer, a future
|
|
63
63
|
// TUI) is isolated from execution. We swallow per-subscriber so one
|
|
64
64
|
// bad surface can't crash the user's build (mirrors the deleted
|
|
65
65
|
// Observer's makeSafeObserver contract). The terminal renderer is
|
|
@@ -138,9 +138,9 @@ export function terminalSubscriber(sink: Logger): RunEventSubscriber {
|
|
|
138
138
|
|
|
139
139
|
/**
|
|
140
140
|
* A bus subscriber that projects each event to its `WireEvent` and hands
|
|
141
|
-
* it to `send` — the shared run→consumer forwarding path
|
|
142
|
-
*
|
|
143
|
-
*
|
|
141
|
+
* it to `send` — the shared run→consumer forwarding path for whatever an
|
|
142
|
+
* embedder builds on it (an NDJSON stream, an enveloped protocol; core
|
|
143
|
+
* ships neither). Group-task start/complete events are dropped (no command,
|
|
144
144
|
* pure scheduling noise). `run()` emits run:end twice (normal + finally)
|
|
145
145
|
* plus trailing summary status lines; we forward the run once and then go
|
|
146
146
|
* quiet, so a consumer sees exactly one terminal frame per run. `send`
|
|
@@ -151,8 +151,8 @@ export function wireForwarder(send: (event: WireEvent) => void): RunEventSubscri
|
|
|
151
151
|
// run() emits run:end TWICE (normal + finally), with the summary footer
|
|
152
152
|
// (run:status lines) emitted in BETWEEN. Drop the duplicate run:end, but
|
|
153
153
|
// keep forwarding everything else — including those post-run:end status
|
|
154
|
-
// lines, so a consumer that renders them
|
|
155
|
-
//
|
|
154
|
+
// lines, so a consumer that renders them gets the footer, and one that
|
|
155
|
+
// ignores run:status is unaffected.
|
|
156
156
|
let endForwarded = false
|
|
157
157
|
// Which tasks the consumer has been told about. A skipped task never
|
|
158
158
|
// reaches the scheduler's onStart, so its completion arrives with no
|
|
@@ -181,7 +181,7 @@ export function wireForwarder(send: (event: WireEvent) => void): RunEventSubscri
|
|
|
181
181
|
}
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
-
// --- Serializable projections (the off-thread
|
|
184
|
+
// --- Serializable projections (the off-thread wire form) ---------------
|
|
185
185
|
//
|
|
186
186
|
// The in-process bus carries live refs; crossing a worker postMessage or
|
|
187
187
|
// an RPC needs a JSON/structured-clone-safe form. Two concrete blockers
|
|
@@ -220,12 +220,19 @@ export interface OutcomeView {
|
|
|
220
220
|
* exactly how it drifted from the terminal and `--summarize`.
|
|
221
221
|
*/
|
|
222
222
|
isGroup?: boolean
|
|
223
|
+
/** Executed with no `cache` block: never consulted the cache, not a miss. */
|
|
224
|
+
noCache?: boolean
|
|
223
225
|
/** Cache hits only: the exec time the entry was stored with — the work
|
|
224
226
|
* this hit skipped, as opposed to `durationMs`, the restore it cost. */
|
|
225
227
|
storedDurationMs?: number
|
|
228
|
+
/** Cache hits only: what the producing execution used (see `TaskOutcome`). */
|
|
229
|
+
storedCpuMs?: number
|
|
230
|
+
storedPeakRssBytes?: number
|
|
226
231
|
hash?: string
|
|
227
232
|
cpuMs?: number
|
|
228
233
|
peakRssBytes?: number
|
|
234
|
+
/** How long an `admit` policy held the task with a worker free (see `TaskOutcome`). */
|
|
235
|
+
admissionHeldMs?: number
|
|
229
236
|
restored?: boolean
|
|
230
237
|
sandboxViolations?: number
|
|
231
238
|
sandboxViolationLines?: string[]
|
|
@@ -234,6 +241,29 @@ export interface OutcomeView {
|
|
|
234
241
|
wallclockEndNs?: string
|
|
235
242
|
}
|
|
236
243
|
|
|
244
|
+
/**
|
|
245
|
+
* The one outcome vocabulary every renderer prints — the framed block
|
|
246
|
+
* header and footer, the focused one-liner, the hash-only audit line,
|
|
247
|
+
* the verbose summary table: `success` / `restored-local` /
|
|
248
|
+
* `restored-remote` / `up-to-date` / `failed` / `skipped`. A hit that
|
|
249
|
+
* materialized nothing is up-to-date whichever layer answered.
|
|
250
|
+
*/
|
|
251
|
+
export function outcomeWord(o: Pick<OutcomeView, 'status' | 'restored'>): string {
|
|
252
|
+
switch (o.status) {
|
|
253
|
+
case 'cache-hit':
|
|
254
|
+
return o.restored === false ? 'up-to-date' : 'restored-local'
|
|
255
|
+
case 'cache-hit-remote':
|
|
256
|
+
return o.restored === false ? 'up-to-date' : 'restored-remote'
|
|
257
|
+
default:
|
|
258
|
+
return o.status
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** `outcomeWord` with the exit code a failure carries: `failed (exit 9)`. */
|
|
263
|
+
export function outcomeLabel(o: Pick<OutcomeView, 'status' | 'restored' | 'exitCode'>): string {
|
|
264
|
+
return o.status === 'failed' ? `failed (exit ${o.exitCode})` : outcomeWord(o)
|
|
265
|
+
}
|
|
266
|
+
|
|
237
267
|
export function projectNode(node: TaskNode): TaskView {
|
|
238
268
|
const view: TaskView = {
|
|
239
269
|
id: node.id,
|
|
@@ -256,10 +286,14 @@ export function projectOutcome(outcome: TaskOutcome): OutcomeView {
|
|
|
256
286
|
durationMs: outcome.durationMs,
|
|
257
287
|
}
|
|
258
288
|
if (isGroupTask(outcome.node)) view.isGroup = true
|
|
289
|
+
else if (outcome.node.config.cache === undefined) view.noCache = true
|
|
259
290
|
if (outcome.storedDurationMs !== undefined) view.storedDurationMs = outcome.storedDurationMs
|
|
291
|
+
if (outcome.storedCpuMs !== undefined) view.storedCpuMs = outcome.storedCpuMs
|
|
292
|
+
if (outcome.storedPeakRssBytes !== undefined) view.storedPeakRssBytes = outcome.storedPeakRssBytes
|
|
260
293
|
if (outcome.hash !== undefined) view.hash = outcome.hash
|
|
261
294
|
if (outcome.cpuMs !== undefined) view.cpuMs = outcome.cpuMs
|
|
262
295
|
if (outcome.peakRssBytes !== undefined) view.peakRssBytes = outcome.peakRssBytes
|
|
296
|
+
if (outcome.admissionHeldMs !== undefined) view.admissionHeldMs = outcome.admissionHeldMs
|
|
263
297
|
if (outcome.restored !== undefined) view.restored = outcome.restored
|
|
264
298
|
if (outcome.sandboxViolations !== undefined) view.sandboxViolations = outcome.sandboxViolations
|
|
265
299
|
if (outcome.sandboxViolationLines !== undefined)
|
|
@@ -272,7 +306,7 @@ export function projectOutcome(outcome: TaskOutcome): OutcomeView {
|
|
|
272
306
|
|
|
273
307
|
/**
|
|
274
308
|
* The serializable form of a RunEvent — JSON / structured-clone safe, the
|
|
275
|
-
* wire contract a non-local consumer (
|
|
309
|
+
* wire contract a non-local consumer (an embedder's client) reads.
|
|
276
310
|
* `task:start` carries the full `TaskView` so a consumer can rebuild a
|
|
277
311
|
* node-shaped object incrementally (no upfront table needed); later
|
|
278
312
|
* task events reference the id, which the consumer already holds.
|