@vzn/vx 0.0.638 → 0.0.639
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/docs/caching.md +44 -15
- package/docs/cli.md +168 -345
- package/docs/execution.md +18 -43
- package/docs/features.md +7 -10
- package/docs/patterns.md +5 -7
- package/docs/schema.md +47 -49
- package/package.json +5 -5
- package/schemas/last.json +7 -4
- package/skills/vx/SKILL.md +6 -4
- package/src/cache/cache.ts +40 -10
- package/src/cache/git-inputs.ts +6 -0
- package/src/cache/index.ts +2 -2
- package/src/cache/layer.ts +25 -11
- package/src/cache/layered-cache.ts +108 -315
- package/src/cache/plugin-cache.ts +2 -10
- package/src/cache/remote-lane.ts +383 -0
- package/src/cache/run-history.ts +24 -8
- package/src/cache/run-store.ts +4 -4
- package/src/cache/schema.ts +6 -4
- package/src/cache/scoped-cache.ts +7 -0
- package/src/cli/completions.ts +2 -2
- package/src/cli/foreign-flags.ts +10 -22
- package/src/cli/help.ts +6 -31
- package/src/cli/info.ts +2 -1
- package/src/cli/last.ts +6 -3
- package/src/cli/plan-format.ts +5 -58
- package/src/cli/plugin-commands.ts +2 -2
- package/src/cli/run.ts +23 -185
- package/src/cli/show.ts +6 -0
- package/src/cli/upgrade.ts +123 -39
- package/src/cli/watch-cycle.ts +2 -1
- package/src/cli/watch.ts +29 -14
- package/src/cli/why.ts +4 -4
- package/src/config.ts +0 -6
- package/src/exec/executor.ts +1 -1
- package/src/exec/sandbox-runtime.ts +1 -1
- package/src/exec/sandbox-violations.ts +3 -1
- package/src/graph/index.ts +9 -1
- package/src/graph/priorities.ts +1 -1
- package/src/graph/scheduler.ts +4 -16
- package/src/graph/task-graph.ts +87 -17
- package/src/graph/task-status.ts +95 -0
- package/src/{orchestrator → history}/failure-mode.ts +1 -1
- package/src/{orchestrator → history}/history.ts +1 -1
- package/src/history/index.ts +48 -0
- package/src/{orchestrator → history}/metrics.ts +7 -20
- package/src/{orchestrator → history}/miss-reason.ts +1 -1
- package/src/index.ts +10 -28
- package/src/orchestrator/doctor.ts +1 -1
- package/src/orchestrator/execute-args.ts +80 -0
- package/src/orchestrator/execute-task.ts +21 -386
- package/src/orchestrator/fingerprint-watch.ts +2 -2
- package/src/orchestrator/index.ts +1 -73
- package/src/orchestrator/miss-save.ts +33 -3
- package/src/orchestrator/options.ts +1 -36
- package/src/orchestrator/output-host.ts +4 -3
- package/src/orchestrator/persistent-task.ts +321 -0
- package/src/orchestrator/persistent.ts +130 -3
- package/src/orchestrator/placement.ts +3 -3
- package/src/orchestrator/plan.ts +18 -5
- package/src/orchestrator/plugin-host.ts +67 -47
- package/src/orchestrator/plugin.ts +6 -6
- package/src/orchestrator/prepare.ts +28 -29
- package/src/orchestrator/remote-prefetch.ts +14 -1
- package/src/orchestrator/run-artifacts.ts +5 -177
- package/src/orchestrator/run-context.ts +55 -0
- package/src/orchestrator/run-failures.ts +15 -3
- package/src/orchestrator/run-records.ts +35 -3
- package/src/orchestrator/run-report.ts +81 -179
- package/src/orchestrator/run-settings.ts +1 -16
- package/src/orchestrator/run-stop.ts +153 -0
- package/src/orchestrator/run.ts +260 -601
- package/src/orchestrator/sandbox-fix.ts +0 -16
- package/src/orchestrator/signals.ts +14 -6
- package/src/orchestrator/stable-keys.ts +107 -3
- package/src/orchestrator/steps.ts +50 -0
- package/src/orchestrator/task-hash.ts +3 -16
- package/src/orchestrator/task-log-buffer.ts +1 -1
- package/src/orchestrator/task-process.ts +94 -0
- package/src/orchestrator/telemetry-host.ts +1 -1
- package/src/orchestrator/telemetry.ts +5 -82
- package/src/{orchestrator → report}/events.ts +41 -8
- package/src/{orchestrator → report}/forecast.ts +35 -2
- package/src/{orchestrator → report}/framed-output.ts +39 -33
- package/src/report/index.ts +36 -0
- package/src/{orchestrator → report}/logger.ts +158 -20
- package/src/{orchestrator → report}/status-line.ts +84 -7
- package/src/{orchestrator → report}/summary.ts +126 -35
- package/src/report/tally.ts +83 -0
- package/src/settings.ts +1 -22
- package/src/util/colors.ts +44 -4
- package/src/util/git-ignored.ts +143 -0
- package/src/util/index.ts +19 -4
- package/src/util/report.ts +13 -5
- package/src/util/settle.ts +14 -39
- package/src/util/tail.ts +1 -1
- package/src/util/ui.ts +17 -6
- package/schemas/summary.json +0 -133
- package/src/cache/chained-cache.ts +0 -280
- package/src/orchestrator/tally.ts +0 -112
- /package/src/{orchestrator → report}/path-links.ts +0 -0
- /package/src/{orchestrator → report}/plain-output.ts +0 -0
- /package/src/{orchestrator → report}/terminal-signals.ts +0 -0
|
@@ -1,280 +0,0 @@
|
|
|
1
|
-
// Several declared cache layers, consulted in declaration order. Lookup
|
|
2
|
-
// walks the layers until one answers; a save reaches every layer; the FIRST
|
|
3
|
-
// layer owns the run index (history, stats, prune) so a run is recorded
|
|
4
|
-
// once. Restore goes to the layer that produced the hit — remembered per
|
|
5
|
-
// hash — because an entry's artifact lives wherever it was found.
|
|
6
|
-
//
|
|
7
|
-
// A layer that throws in a lookup (a batch row load or a has-many included)
|
|
8
|
-
// is a miss there and the walk goes on; a throwing drain is passed; one
|
|
9
|
-
// that throws in a save is skipped and the rest still save. Unisolated, a
|
|
10
|
-
// plugin layer's throw ended the walk before the local floor under it: its
|
|
11
|
-
// `get` failed the task, its `save` kept the entry from every later layer
|
|
12
|
-
// (item 1020). A save that fails in EVERY layer still throws, so the
|
|
13
|
-
// caller's own "cache save failed" line stays the one word on it.
|
|
14
|
-
|
|
15
|
-
import { statSync } from 'node:fs'
|
|
16
|
-
import type {
|
|
17
|
-
Cache,
|
|
18
|
-
CacheEntry,
|
|
19
|
-
CacheGetContext,
|
|
20
|
-
CacheKeyInput,
|
|
21
|
-
CacheLayer,
|
|
22
|
-
CacheStats,
|
|
23
|
-
CacheStatsOptions,
|
|
24
|
-
IngestMeta,
|
|
25
|
-
InvocationRecord,
|
|
26
|
-
OutputDirRow,
|
|
27
|
-
OutputFileRow,
|
|
28
|
-
PruneOptions,
|
|
29
|
-
PruneResult,
|
|
30
|
-
RunRecord,
|
|
31
|
-
} from './cache.js'
|
|
32
|
-
|
|
33
|
-
/** Told of a layer's failure the chain went past: its index, the method, the error. */
|
|
34
|
-
export type LayerErrorReport = (layer: number, method: string, err: unknown) => void
|
|
35
|
-
|
|
36
|
-
export class ChainedCache implements CacheLayer {
|
|
37
|
-
readonly hasRemote: boolean
|
|
38
|
-
private readonly hitLayer = new Map<string, CacheLayer>()
|
|
39
|
-
|
|
40
|
-
constructor(
|
|
41
|
-
readonly layers: readonly CacheLayer[],
|
|
42
|
-
private readonly onLayerError: LayerErrorReport = () => undefined,
|
|
43
|
-
) {
|
|
44
|
-
if (layers.length < 2) throw new Error('ChainedCache needs at least two layers')
|
|
45
|
-
this.hasRemote = layers.some((l) => l.hasRemote === true)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** `layer`'s answer, or `fallback` once its failure is reported. */
|
|
49
|
-
private async ask<T>(layer: CacheLayer, method: string, call: () => Promise<T>, fallback: T) {
|
|
50
|
-
try {
|
|
51
|
-
return await call()
|
|
52
|
-
} catch (err) {
|
|
53
|
-
this.onLayerError(this.layers.indexOf(layer), method, err)
|
|
54
|
-
return fallback
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
get local(): Cache | undefined {
|
|
59
|
-
return this.layers[0]!.local
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
private owner(hash: string): CacheLayer {
|
|
63
|
-
return this.hitLayer.get(hash) ?? this.layers[0]!
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
key(input: CacheKeyInput): Promise<string> {
|
|
67
|
-
return this.layers[0]!.key(input)
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
async get(hash: string, ctx?: CacheGetContext): Promise<CacheEntry | null> {
|
|
71
|
-
// The layer that pulled this hash in a prefetch answers first. Layers
|
|
72
|
-
// may share one local store, and an earlier layer found the pulled copy
|
|
73
|
-
// there and called it a local hit: a second remote's hit was counted as
|
|
74
|
-
// saved locally, in the outcome, the summary and telemetry (item 889).
|
|
75
|
-
const owner = this.hitLayer.get(hash)
|
|
76
|
-
if (owner !== undefined) {
|
|
77
|
-
const entry = await this.ask(owner, 'get', () => owner.get(hash, ctx), null)
|
|
78
|
-
if (entry !== null) return entry
|
|
79
|
-
}
|
|
80
|
-
for (const layer of this.layers) {
|
|
81
|
-
const entry = await this.ask(layer, 'get', () => layer.get(hash, ctx), null)
|
|
82
|
-
if (entry !== null) {
|
|
83
|
-
this.hitLayer.set(hash, layer)
|
|
84
|
-
return entry
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
return null
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
async has(hash: string): Promise<'local' | 'remote' | null> {
|
|
91
|
-
for (const layer of this.layers) {
|
|
92
|
-
const where = await this.ask(layer, 'has', () => layer.has(hash), null)
|
|
93
|
-
if (where !== null) {
|
|
94
|
-
this.hitLayer.set(hash, layer)
|
|
95
|
-
return where
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
return null
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
async prefetch(hash: string, ctx?: CacheGetContext): Promise<boolean> {
|
|
102
|
-
for (const layer of this.layers) {
|
|
103
|
-
if (await this.ask(layer, 'prefetch', () => layer.prefetch(hash, ctx), false)) {
|
|
104
|
-
this.hitLayer.set(hash, layer)
|
|
105
|
-
return true
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
return false
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
localHasMany(hashes: readonly string[]): Promise<Set<string>> {
|
|
112
|
-
return this.layers[0]!.localHasMany?.(hashes) ?? Promise.resolve(new Set())
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
async remoteHasMany(hashes: readonly string[]): Promise<Set<string> | null> {
|
|
116
|
-
// The caller (remote-prefetch) treats a non-null answer as authoritative
|
|
117
|
-
// for the WHOLE chain: complement = absent, broadcast. That is sound only
|
|
118
|
-
// if EVERY remote layer answered — a partial union would poison a layer
|
|
119
|
-
// that cannot batch with another layer's negatives, and its later lazy
|
|
120
|
-
// get() would skip a real remote hit. So: each answering layer gets its
|
|
121
|
-
// OWN complement marked here (its own truth — this also spares it the
|
|
122
|
-
// per-hash GETs for hashes only a sibling holds), and the merged answer
|
|
123
|
-
// is returned only when no remote layer was left unanswered.
|
|
124
|
-
let out: Set<string> | null = null
|
|
125
|
-
let complete = true
|
|
126
|
-
for (const layer of this.layers) {
|
|
127
|
-
if (layer.hasRemote !== true) continue
|
|
128
|
-
const found = await this.ask(
|
|
129
|
-
layer,
|
|
130
|
-
'remoteHasMany',
|
|
131
|
-
// The shape check and the mark sit inside the ask: a layer that
|
|
132
|
-
// answers a non-Set or throws marking gave no answer.
|
|
133
|
-
async () => {
|
|
134
|
-
const got: unknown = (await layer.remoteHasMany?.(hashes)) ?? null
|
|
135
|
-
if (got === null) return null
|
|
136
|
-
if (!(got instanceof Set)) throw new TypeError('remoteHasMany answered a non-Set')
|
|
137
|
-
const set = got as Set<string>
|
|
138
|
-
layer.markRemoteAbsent?.(hashes.filter((h) => !set.has(h)))
|
|
139
|
-
return set
|
|
140
|
-
},
|
|
141
|
-
null,
|
|
142
|
-
)
|
|
143
|
-
if (found === null) {
|
|
144
|
-
complete = false
|
|
145
|
-
continue
|
|
146
|
-
}
|
|
147
|
-
out ??= new Set()
|
|
148
|
-
for (const h of found) out.add(h)
|
|
149
|
-
}
|
|
150
|
-
return complete ? out : null
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
markRemoteAbsent(hashes: Iterable<string>): void {
|
|
154
|
-
const list = [...hashes]
|
|
155
|
-
for (const layer of this.layers) layer.markRemoteAbsent?.(list)
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
async drainUploads(): Promise<void> {
|
|
159
|
-
await Promise.all(
|
|
160
|
-
this.layers.map((l) =>
|
|
161
|
-
this.ask(l, 'drainUploads', async () => l.drainUploads?.(), undefined),
|
|
162
|
-
),
|
|
163
|
-
)
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
loadOutputFilesBatch(hashes: readonly string[]): Map<string, OutputFileRow[]> {
|
|
167
|
-
const out = new Map<string, OutputFileRow[]>()
|
|
168
|
-
for (const [i, layer] of this.layers.entries()) {
|
|
169
|
-
let rows: Map<string, OutputFileRow[]>
|
|
170
|
-
try {
|
|
171
|
-
rows = layer.loadOutputFilesBatch(hashes)
|
|
172
|
-
} catch (err) {
|
|
173
|
-
this.onLayerError(i, 'loadOutputFilesBatch', err)
|
|
174
|
-
continue
|
|
175
|
-
}
|
|
176
|
-
for (const [h, r] of rows) if (!out.has(h)) out.set(h, r)
|
|
177
|
-
}
|
|
178
|
-
return out
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
isOutputsCurrent(projectDir: string, expected: readonly OutputFileRow[]): Promise<boolean> {
|
|
182
|
-
return this.layers[0]!.isOutputsCurrent(projectDir, expected)
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// The directory short-circuit lives with the layer that owns the local
|
|
186
|
-
// rows — the first, like the file check.
|
|
187
|
-
recordOutputDirs(
|
|
188
|
-
hash: string,
|
|
189
|
-
projectDir: string,
|
|
190
|
-
prefixes: readonly string[],
|
|
191
|
-
holds?: (files: readonly string[]) => boolean,
|
|
192
|
-
): Promise<void> {
|
|
193
|
-
return (
|
|
194
|
-
this.layers[0]!.recordOutputDirs?.(hash, projectDir, prefixes, holds) ?? Promise.resolve()
|
|
195
|
-
)
|
|
196
|
-
}
|
|
197
|
-
recordOutputStamps(hash: string, projectDir: string, workspaceRoot: string): void {
|
|
198
|
-
this.layers[0]!.recordOutputStamps?.(hash, projectDir, workspaceRoot)
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
loadOutputDirsBatch(hashes: readonly string[]): Map<string, OutputDirRow[]> {
|
|
202
|
-
return this.layers[0]!.loadOutputDirsBatch?.(hashes) ?? new Map()
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
outputDirsCurrent(projectDir: string, rows: readonly OutputDirRow[]): Promise<boolean> {
|
|
206
|
-
return this.layers[0]!.outputDirsCurrent?.(projectDir, rows) ?? Promise.resolve(false)
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
restoreOutputs(hash: string, projectDir: string, workspaceRoot?: string): Promise<void> {
|
|
210
|
-
return this.owner(hash).restoreOutputs(hash, projectDir, workspaceRoot)
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
async save(args: Parameters<CacheLayer['save']>[0]): Promise<void> {
|
|
214
|
-
// Layers wrapping the SAME local handle (two remote plugins over
|
|
215
|
-
// ctx.localCache) would each pack + write the identical artifact; the
|
|
216
|
-
// first write is the only one that matters, so later layers get
|
|
217
|
-
// `skipLocalWrite` and go straight to their remote upload (which reads
|
|
218
|
-
// the artifact the first layer just wrote — same handle, same path).
|
|
219
|
-
const seenLocals = new Set<NonNullable<CacheLayer['local']>>()
|
|
220
|
-
const failures: Array<[layer: number, err: unknown]> = []
|
|
221
|
-
for (const [i, layer] of this.layers.entries()) {
|
|
222
|
-
const local = layer.local
|
|
223
|
-
const skip = local !== undefined && seenLocals.has(local)
|
|
224
|
-
try {
|
|
225
|
-
await layer.save(skip ? { ...args, skipLocalWrite: true } : args)
|
|
226
|
-
} catch (err) {
|
|
227
|
-
failures.push([i, err])
|
|
228
|
-
continue
|
|
229
|
-
}
|
|
230
|
-
if (local !== undefined) seenLocals.add(local)
|
|
231
|
-
}
|
|
232
|
-
if (failures.length === this.layers.length) throw failures[0]![1]
|
|
233
|
-
for (const [i, err] of failures) this.onLayerError(i, 'save', err)
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
ingest(hash: string, body: Blob | Response, meta: IngestMeta): Promise<void> {
|
|
237
|
-
return this.layers[0]!.ingest(hash, body, meta)
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
recordRunBundle(bundle: { runs: readonly RunRecord[]; invocation: InvocationRecord }): void {
|
|
241
|
-
this.layers[0]!.recordRunBundle(bundle)
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
stats(opts?: CacheStatsOptions): CacheStats {
|
|
245
|
-
return this.layers[0]!.stats(opts)
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
hashFile(filePath: string): Promise<string> {
|
|
249
|
-
return this.layers[0]!.hashFile(filePath)
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
outputsPath(hash: string): string {
|
|
253
|
-
return this.owner(hash).outputsPath(hash)
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/** A layer that cannot say keeps a file at `outputsPath`, or nothing. */
|
|
257
|
-
artifactSize(hash: string): number | undefined {
|
|
258
|
-
const owner = this.owner(hash)
|
|
259
|
-
return owner.artifactSize !== undefined
|
|
260
|
-
? owner.artifactSize(hash)
|
|
261
|
-
: statSync(owner.outputsPath(hash), { throwIfNoEntry: false })?.size
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
prune(options: PruneOptions): Promise<PruneResult> {
|
|
265
|
-
return this.layers[0]!.prune(options)
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
close(): void {
|
|
269
|
-
// Every layer closes even if an earlier one throws; the first error wins.
|
|
270
|
-
let failure: unknown
|
|
271
|
-
for (const layer of this.layers) {
|
|
272
|
-
try {
|
|
273
|
-
layer.close()
|
|
274
|
-
} catch (err) {
|
|
275
|
-
failure ??= err
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
if (failure !== undefined) throw failure
|
|
279
|
-
}
|
|
280
|
-
}
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
// Shared outcome-tally helper. `summary.ts` (end-of-run terminal block),
|
|
2
|
-
// `run-artifacts.ts` (`--summarize` JSON) and `run-report.ts`
|
|
3
|
-
// (`--report=markdown`) all count the same numbers; centralising the bucket
|
|
4
|
-
// rules keeps the three surfaces from disagreeing about one run.
|
|
5
|
-
|
|
6
|
-
import { isGroupTask, type TaskOutcome } from '../graph/index.js'
|
|
7
|
-
|
|
8
|
-
export interface Tally {
|
|
9
|
-
successful: number
|
|
10
|
-
failed: number
|
|
11
|
-
skipped: number
|
|
12
|
-
cachedLocal: number
|
|
13
|
-
/** Hits that materialized files from the local artifact. */
|
|
14
|
-
restoredLocal: number
|
|
15
|
-
/** Hits that materialized files pulled from the remote layer. */
|
|
16
|
-
restoredRemote: number
|
|
17
|
-
/** Cache hits whose tree already matched — nothing was written. */
|
|
18
|
-
upToDate: number
|
|
19
|
-
cachedRemote: number
|
|
20
|
-
/**
|
|
21
|
-
* Killed by a shutdown signal. No work, so it joins no outcome bucket and
|
|
22
|
-
* not `total` — but it is counted, because it makes the run red: a red run
|
|
23
|
-
* whose every counted task succeeded is undiagnosable without this.
|
|
24
|
-
*/
|
|
25
|
-
aborted: number
|
|
26
|
-
/** Tasks counted toward `total`. Group tasks are excluded by default. */
|
|
27
|
-
total: number
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* The minimum an outcome must expose to be bucketed. The in-process
|
|
32
|
-
* `TaskOutcome` and the serializable `OutcomeView` both satisfy it; they
|
|
33
|
-
* differ only in how they carry group-ness (a live node ref vs. a projected
|
|
34
|
-
* boolean), so each entry point supplies that and the RULE lives in one
|
|
35
|
-
* place. Declared structurally here rather than importing `OutcomeView`,
|
|
36
|
-
* which would close a module cycle (events → summary → tally).
|
|
37
|
-
*/
|
|
38
|
-
export interface TallyItem {
|
|
39
|
-
status: TaskOutcome['status']
|
|
40
|
-
restored?: boolean
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function emptyTally(): Tally {
|
|
44
|
-
return {
|
|
45
|
-
successful: 0,
|
|
46
|
-
failed: 0,
|
|
47
|
-
skipped: 0,
|
|
48
|
-
cachedLocal: 0,
|
|
49
|
-
restoredLocal: 0,
|
|
50
|
-
restoredRemote: 0,
|
|
51
|
-
upToDate: 0,
|
|
52
|
-
cachedRemote: 0,
|
|
53
|
-
aborted: 0,
|
|
54
|
-
total: 0,
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Fold ONE outcome into the tally — the single owner of the bucket rules.
|
|
60
|
-
*
|
|
61
|
-
* Group tasks (no `exec`) aren't real work; including them inflates the
|
|
62
|
-
* per-run "N total" count in ways that mislead ("3 of 4 cached" when one was
|
|
63
|
-
* a group that ran nothing).
|
|
64
|
-
*
|
|
65
|
-
* Status bucket rules:
|
|
66
|
-
* `success` → +successful
|
|
67
|
-
* `cache-hit` → +successful, +cachedLocal
|
|
68
|
-
* `cache-hit-remote` → +successful, +cachedRemote
|
|
69
|
-
* `failed` → +failed
|
|
70
|
-
* `skipped` → +skipped
|
|
71
|
-
* `aborted` → +aborted only (no bucket, no total)
|
|
72
|
-
*/
|
|
73
|
-
function fold(t: Tally, o: TallyItem, isGroup: boolean): void {
|
|
74
|
-
if (isGroup) return
|
|
75
|
-
if (o.status === 'aborted') {
|
|
76
|
-
t.aborted++
|
|
77
|
-
return
|
|
78
|
-
}
|
|
79
|
-
t.total++
|
|
80
|
-
if (o.status === 'success') t.successful++
|
|
81
|
-
else if (o.status === 'cache-hit') {
|
|
82
|
-
t.successful++
|
|
83
|
-
t.cachedLocal++
|
|
84
|
-
if (o.restored === true) t.restoredLocal++
|
|
85
|
-
else t.upToDate++
|
|
86
|
-
} else if (o.status === 'cache-hit-remote') {
|
|
87
|
-
t.successful++
|
|
88
|
-
t.cachedRemote++
|
|
89
|
-
if (o.restored === true) t.restoredRemote++
|
|
90
|
-
else t.upToDate++
|
|
91
|
-
} else if (o.status === 'failed') t.failed++
|
|
92
|
-
else if (o.status === 'skipped') t.skipped++
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/** Walk live outcomes and return a `Tally`. */
|
|
96
|
-
export function tallyOutcomes(outcomes: readonly TaskOutcome[]): Tally {
|
|
97
|
-
const t = emptyTally()
|
|
98
|
-
for (const o of outcomes) fold(t, o, isGroupTask(o.node))
|
|
99
|
-
return t
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* The same tally over the serializable projection (`OutcomeView`), which
|
|
104
|
-
* carries group-ness as a flag because it has no node back-ref. An absent
|
|
105
|
-
* flag reads as "not a group" — the only outcomes without one predate the
|
|
106
|
-
* field and none of them are groups.
|
|
107
|
-
*/
|
|
108
|
-
export function tallyViews(views: readonly (TallyItem & { isGroup?: boolean })[]): Tally {
|
|
109
|
-
const t = emptyTally()
|
|
110
|
-
for (const v of views) fold(t, v, v.isGroup === true)
|
|
111
|
-
return t
|
|
112
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|