@vzn/vx 0.0.16 → 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 +135 -124
- package/index.ts +10 -0
- package/launcher.mjs +10 -11
- package/package.json +11 -7
- package/src/bin.ts +14 -4
- package/src/cache/archive.ts +659 -0
- package/src/cache/cache.ts +769 -1101
- package/src/cache/chained-cache.ts +194 -0
- 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 +24 -5
- package/src/cache/inputs.ts +373 -456
- package/src/cache/layer.ts +604 -0
- package/src/cache/layered-cache.ts +96 -43
- 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/tar-stream.ts +358 -0
- package/src/cache/zstd.ts +160 -0
- package/src/cli/cache.ts +75 -21
- package/src/cli/completions.ts +104 -0
- package/src/cli/core-alias.ts +25 -0
- package/src/cli/help.ts +195 -128
- package/src/cli/index.ts +116 -46
- package/src/cli/info.ts +122 -63
- package/src/cli/init.ts +75 -0
- package/src/cli/last.ts +172 -0
- package/src/cli/lock.ts +2 -1
- package/src/cli/plan-format.ts +72 -6
- package/src/cli/plugin-commands.ts +103 -0
- package/src/cli/run.ts +245 -271
- package/src/cli/select.ts +332 -0
- package/src/cli/show.ts +144 -69
- package/src/cli/upgrade.ts +9 -7
- package/src/cli/watch.ts +858 -110
- package/src/cli/why.ts +223 -0
- package/src/cli/workspace-config.ts +126 -0
- package/src/config.ts +187 -150
- package/src/exec/env.ts +18 -1
- package/src/exec/executor.ts +224 -0
- package/src/exec/index.ts +24 -2
- package/src/exec/local-executor.ts +41 -0
- package/src/exec/runner.ts +166 -40
- package/src/exec/sandbox-binds.ts +182 -0
- package/src/exec/sandbox-paths.ts +50 -0
- package/src/exec/sandbox-runtime.ts +642 -260
- package/src/exec/sandbox-violations.ts +280 -0
- package/src/graph/index.ts +4 -10
- package/src/graph/scheduler.ts +232 -203
- package/src/graph/task-graph.ts +222 -8
- package/src/index.ts +146 -153
- package/src/orchestrator/admission.ts +137 -0
- package/src/orchestrator/deferred-outputs.ts +147 -0
- package/src/orchestrator/doctor.ts +255 -0
- package/src/orchestrator/download-policy.ts +171 -0
- package/src/orchestrator/events.ts +72 -8
- package/src/orchestrator/execute-task.ts +374 -549
- package/src/orchestrator/failure-mode.ts +202 -0
- package/src/orchestrator/framed-output.ts +126 -69
- package/src/orchestrator/history.ts +126 -59
- package/src/orchestrator/hit-restore.ts +221 -0
- package/src/orchestrator/index.ts +77 -93
- package/src/orchestrator/local-shortcircuit.ts +22 -0
- package/src/orchestrator/lockfile-claim.ts +281 -0
- package/src/orchestrator/logger.ts +208 -35
- package/src/orchestrator/metrics.ts +102 -1388
- package/src/orchestrator/miss-save.ts +169 -0
- package/src/orchestrator/options.ts +55 -34
- package/src/orchestrator/persistent.ts +79 -0
- package/src/orchestrator/placement.ts +182 -0
- package/src/orchestrator/plan.ts +144 -1
- package/src/orchestrator/plugin-host.ts +357 -120
- package/src/orchestrator/plugin.ts +298 -58
- package/src/orchestrator/prepare.ts +202 -106
- package/src/orchestrator/projects.ts +259 -0
- package/src/orchestrator/remote-prefetch.ts +12 -8
- package/src/orchestrator/run-artifacts.ts +63 -16
- package/src/orchestrator/run-context.ts +167 -29
- package/src/orchestrator/run-records.ts +149 -0
- package/src/orchestrator/run-report.ts +74 -47
- package/src/orchestrator/run.ts +534 -417
- 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 +129 -31
- package/src/orchestrator/status-line.ts +49 -9
- package/src/orchestrator/summary.ts +90 -26
- package/src/orchestrator/tally.ts +78 -35
- package/src/orchestrator/task-hash.ts +111 -15
- package/src/orchestrator/task-log-buffer.ts +288 -0
- package/src/orchestrator/telemetry-host.ts +34 -6
- package/src/orchestrator/telemetry.ts +182 -36
- package/src/orchestrator/upstream.ts +72 -31
- package/src/util/cgroup.ts +160 -0
- package/src/util/edit-distance.ts +70 -0
- package/src/util/errors.ts +15 -1
- package/src/util/index.ts +15 -2
- package/src/util/num.ts +37 -0
- package/src/util/paths.ts +64 -0
- package/src/util/settle.ts +67 -0
- package/src/util/size.ts +5 -1
- package/src/util/tail.ts +66 -0
- package/src/util/timing.ts +63 -0
- package/src/util/verbs.ts +39 -0
- package/src/workspace/affected.ts +250 -23
- package/src/workspace/config-cache.ts +313 -0
- package/src/workspace/config-eval.ts +217 -0
- package/src/workspace/config-imports.ts +162 -0
- package/src/workspace/config-schema.ts +837 -0
- package/src/workspace/filter.ts +65 -10
- package/src/workspace/fingerprint.ts +53 -4
- package/src/workspace/index.ts +42 -3
- package/src/workspace/lockfile.ts +25 -9
- package/src/workspace/migrate-scripts.ts +134 -0
- package/src/workspace/migration.ts +314 -0
- package/src/workspace/nested-dirs.ts +21 -7
- package/src/workspace/package-graph.ts +114 -41
- package/src/workspace/project-loader.ts +224 -450
- package/src/workspace/workspace.ts +298 -72
- package/src/cache/cas-backend.ts +0 -97
- package/src/cache/digest.ts +0 -47
- package/src/cache/tar.ts +0 -343
- package/src/cli/backend.ts +0 -23
- package/src/cli/mcp-rpc.ts +0 -292
- package/src/cli/mcp.ts +0 -92
- package/src/cli/migrate-nx.ts +0 -450
- package/src/cli/migrate-turbo.ts +0 -364
- package/src/cli/migrate.ts +0 -219
- package/src/orchestrator/devframe-surface.ts +0 -74
- package/src/orchestrator/predict.ts +0 -106
- package/src/orchestrator/protocol.ts +0 -156
- package/src/orchestrator/resources.ts +0 -59
- package/src/orchestrator/run-state.ts +0 -145
- package/src/orchestrator/verify.ts +0 -164
- package/src/orchestrator/wire-render.ts +0 -92
- package/src/orchestrator/wire.ts +0 -212
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// `--download` policy: which tasks may leave their outputs in the remote
|
|
2
|
+
// store instead of landing them on this machine, and which must not.
|
|
3
|
+
//
|
|
4
|
+
// See docs/design/download-policy-cas-cache-2026-08.md. Two decisions live
|
|
5
|
+
// here, both made ONCE per run at plan time (never per attempt), so
|
|
6
|
+
// `--dry` can show them and the scheduler never re-derives them.
|
|
7
|
+
|
|
8
|
+
import { isGroupTask, type TaskNode } from '../graph/index.js'
|
|
9
|
+
import { staticPrefix } from '../util/index.js'
|
|
10
|
+
|
|
11
|
+
/** Where a task's outputs go. `never` is `exec.remote: 'only'`. */
|
|
12
|
+
export type DownloadMode = 'eager' | 'deferred' | 'never'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Deferral eligibility: the tasks whose outputs NO key in this run could
|
|
16
|
+
* observe on disk. Returns the INELIGIBLE ids mapped to the reason, so a
|
|
17
|
+
* downgrade can name itself in `--dry`.
|
|
18
|
+
*
|
|
19
|
+
* The channel this closes is narrow and specific. A dependent's key folds
|
|
20
|
+
* an upstream's KEY, never its output content (pure-input transitive
|
|
21
|
+
* hashing), so deferral cannot move a dependent's key that way. The one
|
|
22
|
+
* real channel is a task whose `cache.inputs` globs can MATCH a producer's
|
|
23
|
+
* declared outputs on disk: then its key differs by whether the bytes
|
|
24
|
+
* arrived.
|
|
25
|
+
*
|
|
26
|
+
* DELIBERATELY TIGHTER than "any task sharing the producer's project"
|
|
27
|
+
* (the shape the design doc first sketched): that rule fires whenever a
|
|
28
|
+
* sibling reads the project at all, which is every ordinary workspace —
|
|
29
|
+
* `test` reads `src/**` while `build` writes `dist/**` — and would leave
|
|
30
|
+
* `--download=none` with nothing to defer. Comparing the globs' static
|
|
31
|
+
* prefixes answers the question actually being asked, and stays
|
|
32
|
+
* conservative in every direction that matters: a leading wildcard yields
|
|
33
|
+
* `.` and reaches everything, a cacheable task with no declared `files`
|
|
34
|
+
* is treated as reading its whole project, and `workspaceFiles` on either
|
|
35
|
+
* side ignores project boundaries — all three force ineligible.
|
|
36
|
+
*/
|
|
37
|
+
export function deferralEligibility(nodes: Map<string, TaskNode>): Map<string, string> {
|
|
38
|
+
const ineligible = new Map<string, string>()
|
|
39
|
+
|
|
40
|
+
const readersByProject = new Map<string, { taskId: string; prefixes: string[] }[]>()
|
|
41
|
+
let workspaceReader: string | undefined
|
|
42
|
+
let runtimeReader: string | undefined
|
|
43
|
+
for (const n of nodes.values()) {
|
|
44
|
+
const cache = n.config.cache
|
|
45
|
+
if (cache === undefined) continue
|
|
46
|
+
if ((cache.inputs?.workspaceFiles?.length ?? 0) > 0) workspaceReader ??= n.id
|
|
47
|
+
// A `runtime` input is a SHELL COMMAND whose reads are unknowable —
|
|
48
|
+
// the same reason vx refuses to infer inputs by tracing. It can `cat` a
|
|
49
|
+
// producer's output (or path-escape its project to do it), and its
|
|
50
|
+
// stdout is folded into the key, so its answer would differ by whether
|
|
51
|
+
// the bytes were fetched. Deferral makes that sharper than it already
|
|
52
|
+
// was: it deliberately skips the output clean, so a stale prior build
|
|
53
|
+
// is exactly what such a command would sample. Nothing defers in a run
|
|
54
|
+
// that declares one — worse than nothing would be a key that moves with
|
|
55
|
+
// a transfer flag.
|
|
56
|
+
if (
|
|
57
|
+
(cache.inputs?.runtime?.length ?? 0) > 0 ||
|
|
58
|
+
(cache.inputs?.workspaceRuntime?.length ?? 0) > 0
|
|
59
|
+
) {
|
|
60
|
+
runtimeReader ??= n.id
|
|
61
|
+
}
|
|
62
|
+
const files = cache.inputs?.files
|
|
63
|
+
const prefixes = files === undefined || files.length === 0 ? ['.'] : files.map(staticPrefix)
|
|
64
|
+
const list = readersByProject.get(n.projectName)
|
|
65
|
+
if (list) list.push({ taskId: n.id, prefixes })
|
|
66
|
+
else readersByProject.set(n.projectName, [{ taskId: n.id, prefixes }])
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
for (const n of nodes.values()) {
|
|
70
|
+
const outputs = n.config.cache?.outputs
|
|
71
|
+
if ((outputs?.workspaceFiles?.length ?? 0) > 0) {
|
|
72
|
+
ineligible.set(
|
|
73
|
+
n.id,
|
|
74
|
+
'declares cache.outputs.workspaceFiles — root-anchored outputs can land where any project-relative input reads',
|
|
75
|
+
)
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
const files = outputs?.files ?? []
|
|
79
|
+
if (files.length === 0) continue
|
|
80
|
+
if (runtimeReader !== undefined) {
|
|
81
|
+
ineligible.set(
|
|
82
|
+
n.id,
|
|
83
|
+
`${runtimeReader} declares a cache.inputs.runtime command, whose reads cannot be bounded`,
|
|
84
|
+
)
|
|
85
|
+
continue
|
|
86
|
+
}
|
|
87
|
+
if (workspaceReader !== undefined) {
|
|
88
|
+
ineligible.set(
|
|
89
|
+
n.id,
|
|
90
|
+
`${workspaceReader} declares cache.inputs.workspaceFiles, which can read any project`,
|
|
91
|
+
)
|
|
92
|
+
continue
|
|
93
|
+
}
|
|
94
|
+
const outPrefixes = files.map(staticPrefix)
|
|
95
|
+
// Project boundaries are hard, so only a SAME-project reader can reach a
|
|
96
|
+
// project-relative output. A task never observes its own outputs — they
|
|
97
|
+
// are excluded from its own key by construction.
|
|
98
|
+
for (const reader of readersByProject.get(n.projectName) ?? []) {
|
|
99
|
+
if (reader.taskId === n.id) continue
|
|
100
|
+
const clash = reader.prefixes.find((ip) => outPrefixes.some((op) => prefixesOverlap(ip, op)))
|
|
101
|
+
if (clash !== undefined) {
|
|
102
|
+
ineligible.set(
|
|
103
|
+
n.id,
|
|
104
|
+
`${reader.taskId} reads ${clash === '.' ? 'the whole project' : clash} with cache.inputs.files`,
|
|
105
|
+
)
|
|
106
|
+
break
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return ineligible
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Could two glob prefixes, anchored at the same dir, name overlapping paths? */
|
|
114
|
+
function prefixesOverlap(a: string, b: string): boolean {
|
|
115
|
+
if (a === '.' || b === '.' || a === '' || b === '' || a === '/' || b === '/') return true
|
|
116
|
+
if (a === b) return true
|
|
117
|
+
return a.startsWith(`${b}/`) || b.startsWith(`${a}/`)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The effective per-task download mode. `all` (the default) reproduces
|
|
122
|
+
* today's behaviour exactly: every task eager.
|
|
123
|
+
*/
|
|
124
|
+
export function resolveDownloadModes(args: {
|
|
125
|
+
nodes: Map<string, TaskNode>
|
|
126
|
+
policy: 'all' | 'toplevel' | 'none'
|
|
127
|
+
/** Task ids placed on a LOCAL executor — they write in place. */
|
|
128
|
+
localPlaced: ReadonlySet<string>
|
|
129
|
+
/** Task ids with `exec.remote: 'only'`. */
|
|
130
|
+
remoteOnly: ReadonlySet<string>
|
|
131
|
+
}): { modeOf: Map<string, DownloadMode>; downgrades: Map<string, string> } {
|
|
132
|
+
const modeOf = new Map<string, DownloadMode>()
|
|
133
|
+
const downgrades = new Map<string, string>()
|
|
134
|
+
const ineligible = args.policy === 'all' ? new Map() : deferralEligibility(args.nodes)
|
|
135
|
+
|
|
136
|
+
for (const n of args.nodes.values()) {
|
|
137
|
+
if (isGroupTask(n)) continue
|
|
138
|
+
if (args.remoteOnly.has(n.id)) {
|
|
139
|
+
modeOf.set(n.id, 'never')
|
|
140
|
+
continue
|
|
141
|
+
}
|
|
142
|
+
if (args.policy === 'all' || args.localPlaced.has(n.id)) {
|
|
143
|
+
modeOf.set(n.id, 'eager')
|
|
144
|
+
continue
|
|
145
|
+
}
|
|
146
|
+
// `toplevel` = the outputs you ASKED for come home; intermediates stay
|
|
147
|
+
// remote. Decided here rather than by materialising at run end: eager
|
|
148
|
+
// materialisation rides each task's own completion, overlapped with the
|
|
149
|
+
// rest of the run, where a run-end batch would serialise every download
|
|
150
|
+
// after the last task finishes.
|
|
151
|
+
//
|
|
152
|
+
// SURFACED counts as asked-for. A requested GROUP has no outputs of its
|
|
153
|
+
// own — `markSurfacedDeps` marks the real tasks it chains — so keying on
|
|
154
|
+
// `requested` alone made `vx run ci --download=toplevel` (ci being a
|
|
155
|
+
// group) bring home NOTHING, which is the one thing the mode exists to
|
|
156
|
+
// avoid. run.ts already pairs the two flags for the same reason when it
|
|
157
|
+
// decides which persistent children to keep alive.
|
|
158
|
+
if (args.policy === 'toplevel' && (n.requested === true || n.surfaced === true)) {
|
|
159
|
+
modeOf.set(n.id, 'eager')
|
|
160
|
+
continue
|
|
161
|
+
}
|
|
162
|
+
const reason = ineligible.get(n.id)
|
|
163
|
+
if (reason !== undefined) {
|
|
164
|
+
modeOf.set(n.id, 'eager')
|
|
165
|
+
downgrades.set(n.id, reason)
|
|
166
|
+
continue
|
|
167
|
+
}
|
|
168
|
+
modeOf.set(n.id, 'deferred')
|
|
169
|
+
}
|
|
170
|
+
return { modeOf, downgrades }
|
|
171
|
+
}
|
|
@@ -18,6 +18,11 @@ export interface RunStartInfo {
|
|
|
18
18
|
requestedCount?: number
|
|
19
19
|
/** Run banner context so a live footer can match the final summary. */
|
|
20
20
|
context?: RunContext
|
|
21
|
+
/** The run's canonical start (epoch ms) — the SAME value the end-of-run
|
|
22
|
+
* summary reports as `startedAt`. A telemetry sink needs it during the run
|
|
23
|
+
* (e.g. to derive per-task started_at consistently with the batch summary
|
|
24
|
+
* for idempotent incremental ingest). */
|
|
25
|
+
startedAtMs?: number
|
|
21
26
|
}
|
|
22
27
|
|
|
23
28
|
/**
|
|
@@ -54,7 +59,7 @@ export function createEventBus(): EventBus {
|
|
|
54
59
|
// depends on it), so emit must never reorder or defer.
|
|
55
60
|
//
|
|
56
61
|
// A throwing subscriber must NEVER break the run — the whole point
|
|
57
|
-
// of the bus is that a surface (
|
|
62
|
+
// of the bus is that a surface (an embedder's renderer, a future
|
|
58
63
|
// TUI) is isolated from execution. We swallow per-subscriber so one
|
|
59
64
|
// bad surface can't crash the user's build (mirrors the deleted
|
|
60
65
|
// Observer's makeSafeObserver contract). The terminal renderer is
|
|
@@ -133,9 +138,9 @@ export function terminalSubscriber(sink: Logger): RunEventSubscriber {
|
|
|
133
138
|
|
|
134
139
|
/**
|
|
135
140
|
* A bus subscriber that projects each event to its `WireEvent` and hands
|
|
136
|
-
* it to `send` — the shared run→consumer forwarding path
|
|
137
|
-
*
|
|
138
|
-
*
|
|
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,
|
|
139
144
|
* pure scheduling noise). `run()` emits run:end twice (normal + finally)
|
|
140
145
|
* plus trailing summary status lines; we forward the run once and then go
|
|
141
146
|
* quiet, so a consumer sees exactly one terminal frame per run. `send`
|
|
@@ -146,9 +151,17 @@ export function wireForwarder(send: (event: WireEvent) => void): RunEventSubscri
|
|
|
146
151
|
// run() emits run:end TWICE (normal + finally), with the summary footer
|
|
147
152
|
// (run:status lines) emitted in BETWEEN. Drop the duplicate run:end, but
|
|
148
153
|
// keep forwarding everything else — including those post-run:end status
|
|
149
|
-
// lines, so a consumer that renders them
|
|
150
|
-
//
|
|
154
|
+
// lines, so a consumer that renders them gets the footer, and one that
|
|
155
|
+
// ignores run:status is unaffected.
|
|
151
156
|
let endForwarded = false
|
|
157
|
+
// Which tasks the consumer has been told about. A skipped task never
|
|
158
|
+
// reaches the scheduler's onStart, so its completion arrives with no
|
|
159
|
+
// preceding task:start — and `createWireRenderer` resolves a completion's
|
|
160
|
+
// node from the start it recorded, so it would drop the task entirely
|
|
161
|
+
// while the forwarded footer still counted it. Synthesize the start here,
|
|
162
|
+
// where the live TaskNode is in hand, so the projection stays full
|
|
163
|
+
// fidelity (real requested/surfaced/command) instead of a stand-in.
|
|
164
|
+
const started = new Set<string>()
|
|
152
165
|
return (event) => {
|
|
153
166
|
if (event.kind === 'run:end') {
|
|
154
167
|
if (endForwarded) return
|
|
@@ -158,12 +171,17 @@ export function wireForwarder(send: (event: WireEvent) => void): RunEventSubscri
|
|
|
158
171
|
isGroupTask(event.node)
|
|
159
172
|
) {
|
|
160
173
|
return
|
|
174
|
+
} else if (event.kind === 'task:start') {
|
|
175
|
+
started.add(event.node.id)
|
|
176
|
+
} else if (event.kind === 'task:complete' && !started.has(event.node.id)) {
|
|
177
|
+
started.add(event.node.id)
|
|
178
|
+
send({ kind: 'task:start', task: projectNode(event.node) })
|
|
161
179
|
}
|
|
162
180
|
send(toWireEvent(event))
|
|
163
181
|
}
|
|
164
182
|
}
|
|
165
183
|
|
|
166
|
-
// --- Serializable projections (the off-thread
|
|
184
|
+
// --- Serializable projections (the off-thread wire form) ---------------
|
|
167
185
|
//
|
|
168
186
|
// The in-process bus carries live refs; crossing a worker postMessage or
|
|
169
187
|
// an RPC needs a JSON/structured-clone-safe form. Two concrete blockers
|
|
@@ -195,9 +213,26 @@ export interface OutcomeView {
|
|
|
195
213
|
status: TaskOutcome['status']
|
|
196
214
|
exitCode: number
|
|
197
215
|
durationMs: number
|
|
216
|
+
/**
|
|
217
|
+
* Group node (no `exec`) — organizational, not work. `TaskView` carries
|
|
218
|
+
* this too, but a consumer that only holds outcomes (the `--report`
|
|
219
|
+
* writer) has no node to ask, and counting groups as executed tasks is
|
|
220
|
+
* exactly how it drifted from the terminal and `--summarize`.
|
|
221
|
+
*/
|
|
222
|
+
isGroup?: boolean
|
|
223
|
+
/** Executed with no `cache` block: never consulted the cache, not a miss. */
|
|
224
|
+
noCache?: boolean
|
|
225
|
+
/** Cache hits only: the exec time the entry was stored with — the work
|
|
226
|
+
* this hit skipped, as opposed to `durationMs`, the restore it cost. */
|
|
227
|
+
storedDurationMs?: number
|
|
228
|
+
/** Cache hits only: what the producing execution used (see `TaskOutcome`). */
|
|
229
|
+
storedCpuMs?: number
|
|
230
|
+
storedPeakRssBytes?: number
|
|
198
231
|
hash?: string
|
|
199
232
|
cpuMs?: number
|
|
200
233
|
peakRssBytes?: number
|
|
234
|
+
/** How long an `admit` policy held the task with a worker free (see `TaskOutcome`). */
|
|
235
|
+
admissionHeldMs?: number
|
|
201
236
|
restored?: boolean
|
|
202
237
|
sandboxViolations?: number
|
|
203
238
|
sandboxViolationLines?: string[]
|
|
@@ -206,6 +241,29 @@ export interface OutcomeView {
|
|
|
206
241
|
wallclockEndNs?: string
|
|
207
242
|
}
|
|
208
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
|
+
|
|
209
267
|
export function projectNode(node: TaskNode): TaskView {
|
|
210
268
|
const view: TaskView = {
|
|
211
269
|
id: node.id,
|
|
@@ -227,9 +285,15 @@ export function projectOutcome(outcome: TaskOutcome): OutcomeView {
|
|
|
227
285
|
exitCode: outcome.exitCode,
|
|
228
286
|
durationMs: outcome.durationMs,
|
|
229
287
|
}
|
|
288
|
+
if (isGroupTask(outcome.node)) view.isGroup = true
|
|
289
|
+
else if (outcome.node.config.cache === undefined) view.noCache = true
|
|
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
|
|
230
293
|
if (outcome.hash !== undefined) view.hash = outcome.hash
|
|
231
294
|
if (outcome.cpuMs !== undefined) view.cpuMs = outcome.cpuMs
|
|
232
295
|
if (outcome.peakRssBytes !== undefined) view.peakRssBytes = outcome.peakRssBytes
|
|
296
|
+
if (outcome.admissionHeldMs !== undefined) view.admissionHeldMs = outcome.admissionHeldMs
|
|
233
297
|
if (outcome.restored !== undefined) view.restored = outcome.restored
|
|
234
298
|
if (outcome.sandboxViolations !== undefined) view.sandboxViolations = outcome.sandboxViolations
|
|
235
299
|
if (outcome.sandboxViolationLines !== undefined)
|
|
@@ -242,7 +306,7 @@ export function projectOutcome(outcome: TaskOutcome): OutcomeView {
|
|
|
242
306
|
|
|
243
307
|
/**
|
|
244
308
|
* The serializable form of a RunEvent — JSON / structured-clone safe, the
|
|
245
|
-
* wire contract a non-local consumer (
|
|
309
|
+
* wire contract a non-local consumer (an embedder's client) reads.
|
|
246
310
|
* `task:start` carries the full `TaskView` so a consumer can rebuild a
|
|
247
311
|
* node-shaped object incrementally (no upfront table needed); later
|
|
248
312
|
* task events reference the id, which the consumer already holds.
|