deepline 0.3.258 → 0.3.260
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/dist/bundling-sources/sdk/src/api-error.ts +45 -0
- package/dist/bundling-sources/sdk/src/client.ts +98 -459
- package/dist/bundling-sources/sdk/src/http.ts +1 -34
- package/dist/bundling-sources/sdk/src/index.ts +8 -0
- package/dist/bundling-sources/sdk/src/plays/csv-data-input.ts +139 -0
- package/dist/bundling-sources/sdk/src/plays/run-request.ts +855 -0
- package/dist/bundling-sources/sdk/src/release.ts +1 -1
- package/dist/bundling-sources/sdk/src/runs/overview.ts +45 -2
- package/dist/bundling-sources/sdk/src/types.ts +5 -0
- package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +8 -0
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +42 -10
- package/dist/bundling-sources/shared_libs/play-runtime/launch-identity.ts +3 -0
- package/dist/bundling-sources/shared_libs/play-runtime/launch-source-policy.ts +23 -0
- package/dist/bundling-sources/shared_libs/play-runtime/live-state-contract.ts +3 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-admission-outcome.ts +23 -5
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/projection/gateway-adapter.ts +1 -1
- package/dist/bundling-sources/shared_libs/play-runtime/run-execution-state.ts +845 -0
- package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +106 -6
- package/dist/bundling-sources/shared_libs/play-runtime/run-observation.ts +440 -0
- package/dist/bundling-sources/shared_libs/play-runtime/run-snapshot-stream.ts +35 -2
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +66 -7
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-crash-details.ts +95 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +13 -2
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +0 -29
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-sheet-row-transition.ts +47 -2
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backend.ts +6 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +475 -36
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-progress.ts +78 -11
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +550 -42
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/run-ledger-outbox-recovery.ts +181 -8
- package/dist/bundling-sources/shared_libs/play-runtime/step-lifecycle-tracker.ts +0 -18
- package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +80 -7
- package/dist/bundling-sources/shared_libs/play-runtime/tool-http-errors.ts +37 -2
- package/dist/bundling-sources/shared_libs/plays/bundling/index.ts +148 -18
- package/dist/bundling-sources/shared_libs/plays/cascade-falloff.ts +86 -0
- package/dist/bundling-sources/shared_libs/plays/docflow-unreached-reason.ts +611 -0
- package/dist/bundling-sources/shared_libs/plays/docflow.ts +21 -0
- package/dist/bundling-sources/shared_libs/plays/play-exports.ts +28 -5
- package/dist/bundling-sources/shared_libs/plays/settings-contract.ts +92 -10
- package/dist/bundling-sources/shared_libs/plays/static-pipeline.ts +5 -0
- package/dist/bundling-sources/shared_libs/plays/trigger-config-contract.ts +222 -0
- package/dist/bundling-sources/shared_libs/plays/trigger-config-route.ts +15 -0
- package/dist/cli/index.js +4957 -771
- package/dist/cli/index.mjs +4957 -771
- package/dist/client-B5XbKrrs.d.mts +6564 -0
- package/dist/client-C9Y49yya.d.ts +6564 -0
- package/dist/{compiler-manifest-C4mdRJOP.d.mts → compiler-manifest-CUuh8C3b.d.mts} +14 -1
- package/dist/{compiler-manifest-C4mdRJOP.d.ts → compiler-manifest-CUuh8C3b.d.ts} +14 -1
- package/dist/index.d.mts +7 -6275
- package/dist/index.d.ts +7 -6275
- package/dist/index.js +360 -326
- package/dist/index.mjs +360 -326
- package/dist/install-integrity.json +22 -2
- package/dist/plays/bundle-play-file.d.mts +2 -2
- package/dist/plays/bundle-play-file.d.ts +2 -2
- package/dist/plays/bundle-play-file.mjs +117 -18
- package/dist/plays/csv-data-input.d.mts +37 -0
- package/dist/plays/csv-data-input.d.ts +37 -0
- package/dist/plays/csv-data-input.js +3135 -0
- package/dist/plays/csv-data-input.mjs +3108 -0
- package/dist/plays/run-request.d.mts +6 -0
- package/dist/plays/run-request.d.ts +6 -0
- package/dist/plays/run-request.js +664 -0
- package/dist/plays/run-request.mjs +614 -0
- package/dist/release.d.mts +1 -1
- package/dist/release.d.ts +1 -1
- package/dist/release.js +1 -1
- package/dist/release.mjs +1 -1
- package/package.json +12 -1
|
@@ -0,0 +1,845 @@
|
|
|
1
|
+
import {
|
|
2
|
+
decodePlayRunPublicStatus,
|
|
3
|
+
type PlayRunPublicStatus,
|
|
4
|
+
} from './run-lifecycle-policy';
|
|
5
|
+
import { docflowUnreachedVerdict } from '../plays/docflow-unreached-reason';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Run Execution State: the ONE typed model of what a Play Run and each of its
|
|
9
|
+
* steps did, and the ONE set of words for it.
|
|
10
|
+
*
|
|
11
|
+
* Every surface that says what a run did reads it from here: the run read
|
|
12
|
+
* path (`GET /api/v2/runs/:id` steps), `deepline runs get`, the dashboard's
|
|
13
|
+
* canvas pills, run rail and record pane. Before this, each surface derived
|
|
14
|
+
* the same concept on its own, and they disagreed: a step the run stopped
|
|
15
|
+
* mid-map said "Failed 1,241 of 50,000 rows" (no row had failed — the run had
|
|
16
|
+
* stopped), the CLI printed the same step as `running` on a failed run, and
|
|
17
|
+
* the CLI and the record pane formatted one billing read two ways.
|
|
18
|
+
*
|
|
19
|
+
* The rules this module enforces by type:
|
|
20
|
+
* - "failed" about rows only ever counts rows that failed. A step the run
|
|
21
|
+
* ended while it was in flight is `stopped`, never `failed`.
|
|
22
|
+
* - A step is never `running` once its run is terminal.
|
|
23
|
+
* - Row counts are run-scoped and monotonic (`mergeRowCounts`), and
|
|
24
|
+
* `total`, when known, is never below `completed + failed`.
|
|
25
|
+
* - Every switch over a state is exhaustive (`assertNeverState`).
|
|
26
|
+
*
|
|
27
|
+
* Pure and dependency-free (bundled into the CLI): no I/O, no clocks.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/** Exhaustiveness guard: a new state kind is a compile error at every switch. */
|
|
31
|
+
export function assertNeverState(value: never, what: string): never {
|
|
32
|
+
throw new Error(`Unhandled ${what}: ${JSON.stringify(value)}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Row counts
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Row outcomes of one scope (a step's table, or a whole run). Scope is always
|
|
41
|
+
* the run: a chunk or a sheet page never feeds these.
|
|
42
|
+
*/
|
|
43
|
+
export type RowCounts = {
|
|
44
|
+
/** Rows that finished successfully. */
|
|
45
|
+
readonly completed: number;
|
|
46
|
+
/** Rows that FAILED — never rows that did not run. */
|
|
47
|
+
readonly failed: number;
|
|
48
|
+
/** Rows the scope was handed, when known. Never below completed + failed. */
|
|
49
|
+
readonly total: number | null;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
function count(value: unknown): number | null {
|
|
53
|
+
return typeof value === 'number' && Number.isFinite(value)
|
|
54
|
+
? Math.max(0, Math.trunc(value))
|
|
55
|
+
: null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Build counts from whatever fields a producer reported. `completed` is the
|
|
60
|
+
* ledger's successes-only counter (`PlayRunLedgerStepProgress.completed`);
|
|
61
|
+
* `completedRows` stands in only when `completed` is absent. When a total is
|
|
62
|
+
* known, failed rows are at most the rows that did not succeed. Null when no
|
|
63
|
+
* field carries a number.
|
|
64
|
+
*/
|
|
65
|
+
export function rowCountsFrom(
|
|
66
|
+
input:
|
|
67
|
+
| {
|
|
68
|
+
completed?: unknown;
|
|
69
|
+
completedRows?: unknown;
|
|
70
|
+
failed?: unknown;
|
|
71
|
+
total?: unknown;
|
|
72
|
+
}
|
|
73
|
+
| null
|
|
74
|
+
| undefined,
|
|
75
|
+
): RowCounts | null {
|
|
76
|
+
if (!input) return null;
|
|
77
|
+
const completed = count(input.completed) ?? count(input.completedRows);
|
|
78
|
+
const failed = count(input.failed);
|
|
79
|
+
const total = count(input.total);
|
|
80
|
+
if (completed === null && failed === null && total === null) return null;
|
|
81
|
+
const c = completed ?? 0;
|
|
82
|
+
const known = total !== null && total > 0 ? Math.max(total, c) : null;
|
|
83
|
+
// A row is counted once. A producer that kept a row's earlier failure after
|
|
84
|
+
// it later succeeded (max-merged `failed` across attempts) reports more
|
|
85
|
+
// settled rows than it was handed; the successes are the recount.
|
|
86
|
+
const f = known === null ? (failed ?? 0) : Math.min(failed ?? 0, known - c);
|
|
87
|
+
return { completed: c, failed: f, total: known };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Monotonic merge of two reads of the same run-scoped counts: neither
|
|
92
|
+
* completed nor failed ever goes down, and a later total replaces an earlier
|
|
93
|
+
* one (the map reconciles its estimate at finalize) but never drops below the
|
|
94
|
+
* settled rows.
|
|
95
|
+
*/
|
|
96
|
+
export function mergeRowCounts(
|
|
97
|
+
held: RowCounts | null,
|
|
98
|
+
incoming: RowCounts | null,
|
|
99
|
+
): RowCounts | null {
|
|
100
|
+
if (!held) return incoming;
|
|
101
|
+
if (!incoming) return held;
|
|
102
|
+
const completed = Math.max(held.completed, incoming.completed);
|
|
103
|
+
const failed = Math.max(held.failed, incoming.failed);
|
|
104
|
+
const total = incoming.total ?? held.total;
|
|
105
|
+
return {
|
|
106
|
+
completed,
|
|
107
|
+
failed,
|
|
108
|
+
total: total === null ? null : Math.max(total, completed + failed),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
// Run execution state
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
export type RunStopCause = 'run_failed' | 'run_cancelled';
|
|
117
|
+
|
|
118
|
+
export type RunExecutionState =
|
|
119
|
+
| { readonly kind: 'queued' }
|
|
120
|
+
| { readonly kind: 'running' }
|
|
121
|
+
| { readonly kind: 'waiting'; readonly overdue: boolean }
|
|
122
|
+
/** Completed; `rows.failed > 0` means some rows failed, each in its row. */
|
|
123
|
+
| { readonly kind: 'completed'; readonly rows: RowCounts | null }
|
|
124
|
+
| {
|
|
125
|
+
readonly kind: 'failed';
|
|
126
|
+
readonly rows: RowCounts | null;
|
|
127
|
+
readonly error: string | null;
|
|
128
|
+
}
|
|
129
|
+
| { readonly kind: 'cancelled'; readonly rows: RowCounts | null }
|
|
130
|
+
/** The stored status is unreadable: no claim either way. */
|
|
131
|
+
| { readonly kind: 'unknown' };
|
|
132
|
+
|
|
133
|
+
export type RunExecutionKind = RunExecutionState['kind'];
|
|
134
|
+
|
|
135
|
+
/** Decode any stored/wire run status into the public lifecycle, or null. */
|
|
136
|
+
export function decodeRunStatus(status: unknown): PlayRunPublicStatus | null {
|
|
137
|
+
return decodePlayRunPublicStatus(status);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function isTerminalRunExecution(state: RunExecutionState): boolean {
|
|
141
|
+
switch (state.kind) {
|
|
142
|
+
case 'completed':
|
|
143
|
+
case 'failed':
|
|
144
|
+
case 'cancelled':
|
|
145
|
+
return true;
|
|
146
|
+
case 'queued':
|
|
147
|
+
case 'running':
|
|
148
|
+
case 'waiting':
|
|
149
|
+
case 'unknown':
|
|
150
|
+
return false;
|
|
151
|
+
default:
|
|
152
|
+
return assertNeverState(state, 'run execution state');
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function deriveRunExecutionState(input: {
|
|
157
|
+
status: unknown;
|
|
158
|
+
/** Terminal row outcomes, when the run reports them. */
|
|
159
|
+
rows?: RowCounts | null;
|
|
160
|
+
error?: string | null;
|
|
161
|
+
/** A waiting run's deadline, and the clock it is judged against. */
|
|
162
|
+
waitDeadlineAt?: number | null;
|
|
163
|
+
now?: number;
|
|
164
|
+
}): RunExecutionState {
|
|
165
|
+
const status = decodeRunStatus(input.status);
|
|
166
|
+
const rows = input.rows ?? null;
|
|
167
|
+
switch (status) {
|
|
168
|
+
case 'queued':
|
|
169
|
+
return { kind: 'queued' };
|
|
170
|
+
case 'running':
|
|
171
|
+
return { kind: 'running' };
|
|
172
|
+
case 'waiting':
|
|
173
|
+
return {
|
|
174
|
+
kind: 'waiting',
|
|
175
|
+
overdue:
|
|
176
|
+
typeof input.waitDeadlineAt === 'number' &&
|
|
177
|
+
typeof input.now === 'number' &&
|
|
178
|
+
input.waitDeadlineAt < input.now,
|
|
179
|
+
};
|
|
180
|
+
case 'completed':
|
|
181
|
+
return { kind: 'completed', rows };
|
|
182
|
+
case 'failed':
|
|
183
|
+
return {
|
|
184
|
+
kind: 'failed',
|
|
185
|
+
rows,
|
|
186
|
+
error: input.error?.trim() ? input.error.trim() : null,
|
|
187
|
+
};
|
|
188
|
+
case 'cancelled':
|
|
189
|
+
return { kind: 'cancelled', rows };
|
|
190
|
+
case null:
|
|
191
|
+
return { kind: 'unknown' };
|
|
192
|
+
default:
|
|
193
|
+
return assertNeverState(status, 'run status');
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export type RunExecutionLabel = {
|
|
198
|
+
/** The glyph/pill vocabulary every run surface shares. */
|
|
199
|
+
readonly tone:
|
|
200
|
+
| 'queued'
|
|
201
|
+
| 'running'
|
|
202
|
+
| 'waiting'
|
|
203
|
+
| 'waiting_overdue'
|
|
204
|
+
| 'completed'
|
|
205
|
+
| 'partial'
|
|
206
|
+
| 'failed'
|
|
207
|
+
| 'cancelled'
|
|
208
|
+
| 'unknown';
|
|
209
|
+
/** "Completed", "Completed, some rows failed", "Failed", ... */
|
|
210
|
+
readonly word: string;
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
export function runExecutionLabel(state: RunExecutionState): RunExecutionLabel {
|
|
214
|
+
switch (state.kind) {
|
|
215
|
+
case 'queued':
|
|
216
|
+
return { tone: 'queued', word: 'Queued' };
|
|
217
|
+
case 'running':
|
|
218
|
+
return { tone: 'running', word: 'Running' };
|
|
219
|
+
case 'waiting':
|
|
220
|
+
return state.overdue
|
|
221
|
+
? { tone: 'waiting_overdue', word: 'Waiting past its deadline' }
|
|
222
|
+
: { tone: 'waiting', word: 'Waiting' };
|
|
223
|
+
case 'completed':
|
|
224
|
+
return (state.rows?.failed ?? 0) > 0
|
|
225
|
+
? { tone: 'partial', word: 'Completed, some rows failed' }
|
|
226
|
+
: { tone: 'completed', word: 'Completed' };
|
|
227
|
+
case 'failed':
|
|
228
|
+
return { tone: 'failed', word: 'Failed' };
|
|
229
|
+
case 'cancelled':
|
|
230
|
+
return { tone: 'cancelled', word: 'Cancelled' };
|
|
231
|
+
case 'unknown':
|
|
232
|
+
return { tone: 'unknown', word: 'Unknown' };
|
|
233
|
+
default:
|
|
234
|
+
return assertNeverState(state, 'run execution state');
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** The same words keyed by tone, for surfaces that hold only a tone. */
|
|
239
|
+
export const RUN_EXECUTION_TONE_WORD: Record<
|
|
240
|
+
RunExecutionLabel['tone'],
|
|
241
|
+
string
|
|
242
|
+
> = {
|
|
243
|
+
queued: 'Queued',
|
|
244
|
+
running: 'Running',
|
|
245
|
+
waiting: 'Waiting',
|
|
246
|
+
waiting_overdue: 'Waiting past its deadline',
|
|
247
|
+
completed: 'Completed',
|
|
248
|
+
partial: 'Completed, some rows failed',
|
|
249
|
+
failed: 'Failed',
|
|
250
|
+
cancelled: 'Cancelled',
|
|
251
|
+
unknown: 'Unknown',
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
// ---------------------------------------------------------------------------
|
|
255
|
+
// Node (step) execution state
|
|
256
|
+
// ---------------------------------------------------------------------------
|
|
257
|
+
|
|
258
|
+
/** The node statuses the Run Snapshot reports (`PlayRunStreamNodeState`). */
|
|
259
|
+
export type ReportedNodeStatus =
|
|
260
|
+
| 'idle'
|
|
261
|
+
| 'running'
|
|
262
|
+
| 'completed'
|
|
263
|
+
| 'failed'
|
|
264
|
+
| 'skipped';
|
|
265
|
+
|
|
266
|
+
export type NodeExecutionState =
|
|
267
|
+
/** The run is live and has not reached this step. */
|
|
268
|
+
| { readonly kind: 'queued' }
|
|
269
|
+
| {
|
|
270
|
+
readonly kind: 'running';
|
|
271
|
+
readonly rows: RowCounts | null;
|
|
272
|
+
readonly startedAt: number | null;
|
|
273
|
+
}
|
|
274
|
+
/** Finished. `rows.failed > 0`: some rows failed, each isolated to its row. */
|
|
275
|
+
| { readonly kind: 'completed'; readonly rows: RowCounts | null }
|
|
276
|
+
/** The step itself failed (it carries its own error). */
|
|
277
|
+
| {
|
|
278
|
+
readonly kind: 'failed';
|
|
279
|
+
readonly rows: RowCounts | null;
|
|
280
|
+
readonly error: string | null;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* The run ended while this step was in flight. Its rows did not fail; the
|
|
284
|
+
* ones short of `total` never ran.
|
|
285
|
+
*/
|
|
286
|
+
| {
|
|
287
|
+
readonly kind: 'stopped';
|
|
288
|
+
readonly rows: RowCounts | null;
|
|
289
|
+
readonly cause: RunStopCause;
|
|
290
|
+
}
|
|
291
|
+
/** The runtime decided this step does not run. */
|
|
292
|
+
| { readonly kind: 'skipped' }
|
|
293
|
+
/** The run ended with this step reported idle: it never started. */
|
|
294
|
+
| {
|
|
295
|
+
readonly kind: 'not_reached';
|
|
296
|
+
readonly cause: RunStopCause | 'run_completed';
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* No claim either way: the run reported no state for this node and it is
|
|
300
|
+
* not live, or its status is unreadable.
|
|
301
|
+
*/
|
|
302
|
+
| { readonly kind: 'unknown' };
|
|
303
|
+
|
|
304
|
+
export type NodeExecutionKind = NodeExecutionState['kind'];
|
|
305
|
+
|
|
306
|
+
/** What a node reported, as the Run Snapshot / step package carries it. */
|
|
307
|
+
export type ReportedNodeExecution = {
|
|
308
|
+
status: ReportedNodeStatus | string;
|
|
309
|
+
error?: string | null;
|
|
310
|
+
/** Set by the ledger when the run's terminal event settled the step. */
|
|
311
|
+
stoppedBy?: RunStopCause | null;
|
|
312
|
+
startedAt?: number | null;
|
|
313
|
+
progress?: {
|
|
314
|
+
completed?: unknown;
|
|
315
|
+
completedRows?: unknown;
|
|
316
|
+
failed?: unknown;
|
|
317
|
+
total?: unknown;
|
|
318
|
+
} | null;
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
function stopCauseForRun(run: PlayRunPublicStatus | null): RunStopCause | null {
|
|
322
|
+
if (run === 'failed') return 'run_failed';
|
|
323
|
+
if (run === 'cancelled') return 'run_cancelled';
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* The one derivation of a step's state from what it reported and its run's
|
|
329
|
+
* status. `rows` overrides the reported progress when the caller holds a more
|
|
330
|
+
* specific run-scoped count for this step (a loop member's own column).
|
|
331
|
+
*/
|
|
332
|
+
export function deriveNodeExecutionState(input: {
|
|
333
|
+
reported: ReportedNodeExecution | null | undefined;
|
|
334
|
+
runStatus: unknown;
|
|
335
|
+
rows?: RowCounts | null;
|
|
336
|
+
}): NodeExecutionState {
|
|
337
|
+
const run = decodeRunStatus(input.runStatus);
|
|
338
|
+
const runTerminal =
|
|
339
|
+
run === 'completed' || run === 'failed' || run === 'cancelled';
|
|
340
|
+
const reported = input.reported ?? null;
|
|
341
|
+
const rows =
|
|
342
|
+
input.rows !== undefined
|
|
343
|
+
? input.rows
|
|
344
|
+
: rowCountsFrom(reported?.progress ?? null);
|
|
345
|
+
const status = reported?.status ?? 'idle';
|
|
346
|
+
const runStop = stopCauseForRun(run);
|
|
347
|
+
|
|
348
|
+
switch (status) {
|
|
349
|
+
case 'running': {
|
|
350
|
+
if (!runTerminal) {
|
|
351
|
+
return {
|
|
352
|
+
kind: 'running',
|
|
353
|
+
rows,
|
|
354
|
+
startedAt:
|
|
355
|
+
typeof reported?.startedAt === 'number' ? reported.startedAt : null,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
// The run ended under a step still reporting running. A completed run
|
|
359
|
+
// finished it (the ledger heals a missing final bump the same way).
|
|
360
|
+
return runStop
|
|
361
|
+
? { kind: 'stopped', rows, cause: runStop }
|
|
362
|
+
: { kind: 'completed', rows };
|
|
363
|
+
}
|
|
364
|
+
case 'completed':
|
|
365
|
+
return { kind: 'completed', rows };
|
|
366
|
+
case 'skipped':
|
|
367
|
+
return { kind: 'skipped' };
|
|
368
|
+
case 'failed': {
|
|
369
|
+
if (reported?.stoppedBy) {
|
|
370
|
+
return { kind: 'stopped', rows, cause: reported.stoppedBy };
|
|
371
|
+
}
|
|
372
|
+
// A stop interrupts the step it lands on; the runtime records that step
|
|
373
|
+
// `failed`. It did not fail — the run was cancelled.
|
|
374
|
+
if (run === 'cancelled') {
|
|
375
|
+
return { kind: 'stopped', rows, cause: 'run_cancelled' };
|
|
376
|
+
}
|
|
377
|
+
// `stoppedBy` is projected once, by the Run Snapshot
|
|
378
|
+
// (run-snapshot-stream.ts `stepStopCause`), including for snapshots
|
|
379
|
+
// written before the ledger recorded it. Without it the step failed.
|
|
380
|
+
return {
|
|
381
|
+
kind: 'failed',
|
|
382
|
+
rows,
|
|
383
|
+
error: reported?.error?.trim() ? reported.error.trim() : null,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
default: {
|
|
387
|
+
// idle / absent / unreadable.
|
|
388
|
+
if (run === 'queued' || run === 'running' || run === 'waiting') {
|
|
389
|
+
return { kind: 'queued' };
|
|
390
|
+
}
|
|
391
|
+
// Only a node the run REPORTED idle is known not to have run. A node
|
|
392
|
+
// the run reported nothing for says nothing either way.
|
|
393
|
+
if (runTerminal && status === 'idle' && reported) {
|
|
394
|
+
return { kind: 'not_reached', cause: runStop ?? 'run_completed' };
|
|
395
|
+
}
|
|
396
|
+
return { kind: 'unknown' };
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** Where a node state sits in its lifecycle; a node never steps back. */
|
|
402
|
+
export function nodeExecutionRank(state: NodeExecutionState): number {
|
|
403
|
+
switch (state.kind) {
|
|
404
|
+
case 'unknown':
|
|
405
|
+
return 0;
|
|
406
|
+
case 'queued':
|
|
407
|
+
return 1;
|
|
408
|
+
case 'running':
|
|
409
|
+
return 2;
|
|
410
|
+
case 'completed':
|
|
411
|
+
case 'failed':
|
|
412
|
+
case 'stopped':
|
|
413
|
+
case 'skipped':
|
|
414
|
+
case 'not_reached':
|
|
415
|
+
return 3;
|
|
416
|
+
default:
|
|
417
|
+
return assertNeverState(state, 'node execution state');
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function stateRows(state: NodeExecutionState): RowCounts | null {
|
|
422
|
+
switch (state.kind) {
|
|
423
|
+
case 'running':
|
|
424
|
+
case 'completed':
|
|
425
|
+
case 'failed':
|
|
426
|
+
case 'stopped':
|
|
427
|
+
return state.rows;
|
|
428
|
+
case 'queued':
|
|
429
|
+
case 'skipped':
|
|
430
|
+
case 'not_reached':
|
|
431
|
+
case 'unknown':
|
|
432
|
+
return null;
|
|
433
|
+
default:
|
|
434
|
+
return assertNeverState(state, 'node execution state');
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Monotonic merge of two reads of one node within one run: the state never
|
|
440
|
+
* steps back a rank, and its row counts never decrease.
|
|
441
|
+
*/
|
|
442
|
+
export function mergeNodeExecutionState(
|
|
443
|
+
held: NodeExecutionState | null | undefined,
|
|
444
|
+
incoming: NodeExecutionState,
|
|
445
|
+
): NodeExecutionState {
|
|
446
|
+
if (!held) return incoming;
|
|
447
|
+
const winner =
|
|
448
|
+
nodeExecutionRank(incoming) >= nodeExecutionRank(held) ? incoming : held;
|
|
449
|
+
const rows = mergeRowCounts(stateRows(held), stateRows(incoming));
|
|
450
|
+
switch (winner.kind) {
|
|
451
|
+
case 'running':
|
|
452
|
+
case 'completed':
|
|
453
|
+
case 'failed':
|
|
454
|
+
case 'stopped':
|
|
455
|
+
return rows === winner.rows ? winner : { ...winner, rows };
|
|
456
|
+
case 'queued':
|
|
457
|
+
case 'skipped':
|
|
458
|
+
case 'not_reached':
|
|
459
|
+
case 'unknown':
|
|
460
|
+
return winner;
|
|
461
|
+
default:
|
|
462
|
+
return assertNeverState(winner, 'node execution state');
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/** The pill vocabulary every step surface shares (UI badge variant, CLI word). */
|
|
467
|
+
export type NodeExecutionTone =
|
|
468
|
+
| 'queued'
|
|
469
|
+
| 'running'
|
|
470
|
+
| 'completed'
|
|
471
|
+
/** Completed, some rows failed. */
|
|
472
|
+
| 'partial'
|
|
473
|
+
| 'failed'
|
|
474
|
+
| 'stopped'
|
|
475
|
+
| 'cancelled'
|
|
476
|
+
| 'skipped'
|
|
477
|
+
| 'unknown';
|
|
478
|
+
|
|
479
|
+
export type NodeExecutionLabel = {
|
|
480
|
+
readonly tone: NodeExecutionTone;
|
|
481
|
+
/** "Running", "Completed", "Failed", "Stopped", "Cancelled", ... */
|
|
482
|
+
readonly word: string;
|
|
483
|
+
/** "1,241/50,000", "50,000 rows", "49,000/50,000 ok", "1,241 of 50,000 done". */
|
|
484
|
+
readonly count: string | null;
|
|
485
|
+
/** Rows that failed (> 0 only when rows did). */
|
|
486
|
+
readonly failedRows: number;
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
function n(value: number): string {
|
|
490
|
+
return value.toLocaleString('en-US');
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function rowsWord(value: number): string {
|
|
494
|
+
return `${n(value)} ${value === 1 ? 'row' : 'rows'}`;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/** "{completed}/{total} ok" when rows failed; null otherwise. */
|
|
498
|
+
function okFraction(rows: RowCounts): string | null {
|
|
499
|
+
if (rows.failed <= 0) return null;
|
|
500
|
+
return `${n(rows.completed)}/${n(rows.total ?? rows.completed + rows.failed)} ok`;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function settledCount(rows: RowCounts | null): string | null {
|
|
504
|
+
if (!rows) return null;
|
|
505
|
+
const all = rows.total ?? rows.completed + rows.failed;
|
|
506
|
+
return all > 0 ? rowsWord(all) : null;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/** A step that ended short of its total says how far it got. */
|
|
510
|
+
function partialCount(rows: RowCounts | null): string | null {
|
|
511
|
+
if (!rows) return null;
|
|
512
|
+
const ok = okFraction(rows);
|
|
513
|
+
if (ok) return ok;
|
|
514
|
+
if (rows.total !== null && rows.completed < rows.total) {
|
|
515
|
+
return `${n(rows.completed)} of ${n(rows.total)} done`;
|
|
516
|
+
}
|
|
517
|
+
return settledCount(rows);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
export function nodeExecutionLabel(
|
|
521
|
+
state: NodeExecutionState,
|
|
522
|
+
): NodeExecutionLabel {
|
|
523
|
+
const failedRows = stateRows(state)?.failed ?? 0;
|
|
524
|
+
switch (state.kind) {
|
|
525
|
+
case 'queued':
|
|
526
|
+
return { tone: 'queued', word: 'Queued', count: null, failedRows };
|
|
527
|
+
case 'running': {
|
|
528
|
+
const rows = state.rows;
|
|
529
|
+
// Progress counts every settled row; the failed ones are named after.
|
|
530
|
+
const settled = rows ? rows.completed + rows.failed : 0;
|
|
531
|
+
const live = rows
|
|
532
|
+
? rows.total !== null
|
|
533
|
+
? `${n(settled)}/${n(rows.total)}`
|
|
534
|
+
: settled > 0
|
|
535
|
+
? rowsWord(settled)
|
|
536
|
+
: null
|
|
537
|
+
: null;
|
|
538
|
+
return { tone: 'running', word: 'Running', count: live, failedRows };
|
|
539
|
+
}
|
|
540
|
+
case 'completed':
|
|
541
|
+
return state.rows && state.rows.failed > 0
|
|
542
|
+
? {
|
|
543
|
+
tone: 'partial',
|
|
544
|
+
word: 'Completed',
|
|
545
|
+
count: okFraction(state.rows),
|
|
546
|
+
failedRows,
|
|
547
|
+
}
|
|
548
|
+
: {
|
|
549
|
+
tone: 'completed',
|
|
550
|
+
word: 'Completed',
|
|
551
|
+
count: settledCount(state.rows),
|
|
552
|
+
failedRows,
|
|
553
|
+
};
|
|
554
|
+
case 'failed':
|
|
555
|
+
return {
|
|
556
|
+
tone: 'failed',
|
|
557
|
+
word: 'Failed',
|
|
558
|
+
count: partialCount(state.rows),
|
|
559
|
+
failedRows,
|
|
560
|
+
};
|
|
561
|
+
case 'stopped':
|
|
562
|
+
return state.cause === 'run_cancelled'
|
|
563
|
+
? {
|
|
564
|
+
tone: 'cancelled',
|
|
565
|
+
word: 'Cancelled',
|
|
566
|
+
count: partialCount(state.rows),
|
|
567
|
+
failedRows,
|
|
568
|
+
}
|
|
569
|
+
: {
|
|
570
|
+
tone: 'stopped',
|
|
571
|
+
word: 'Stopped',
|
|
572
|
+
count: partialCount(state.rows),
|
|
573
|
+
failedRows,
|
|
574
|
+
};
|
|
575
|
+
case 'skipped':
|
|
576
|
+
return { tone: 'skipped', word: 'Skipped', count: null, failedRows };
|
|
577
|
+
case 'not_reached':
|
|
578
|
+
return { tone: 'skipped', word: 'Not reached', count: null, failedRows };
|
|
579
|
+
case 'unknown':
|
|
580
|
+
return { tone: 'unknown', word: 'Unknown', count: null, failedRows };
|
|
581
|
+
default:
|
|
582
|
+
return assertNeverState(state, 'node execution state');
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* The label as one line of text — `deepline runs get`'s step line, and what
|
|
588
|
+
* the dashboard pill reads aloud. "Stopped · 1,241 of 50,000 done",
|
|
589
|
+
* "Completed · 49,000/50,000 ok · 1,000 rows failed", "Running · 4/10 · 1 row
|
|
590
|
+
* failed".
|
|
591
|
+
*/
|
|
592
|
+
export function formatNodeExecutionLabel(label: NodeExecutionLabel): string {
|
|
593
|
+
const facts = formatNodeExecutionFacts(label);
|
|
594
|
+
return facts ? `${label.word} · ${facts}` : label.word;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/** The label's facts without its word: "1,241 of 50,000 done · 2 rows failed". */
|
|
598
|
+
export function formatNodeExecutionFacts(label: NodeExecutionLabel): string {
|
|
599
|
+
return [
|
|
600
|
+
label.count,
|
|
601
|
+
label.failedRows > 0 ? `${rowsWord(label.failedRows)} failed` : null,
|
|
602
|
+
]
|
|
603
|
+
.filter((part): part is string => Boolean(part))
|
|
604
|
+
.join(' · ');
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
export function formatNodeExecutionState(state: NodeExecutionState): string {
|
|
608
|
+
return formatNodeExecutionLabel(nodeExecutionLabel(state));
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// ---------------------------------------------------------------------------
|
|
612
|
+
// Run cost
|
|
613
|
+
// ---------------------------------------------------------------------------
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* A run's credits as every surface states them, from ONE source: the run
|
|
617
|
+
* billing read (`GET /api/v2/runs/billing`, Convex `runBillingSettlement`)
|
|
618
|
+
* plus whether the run is still live.
|
|
619
|
+
*/
|
|
620
|
+
export type RunCostState =
|
|
621
|
+
/** The billing read failed: say so, never a number. */
|
|
622
|
+
| { readonly kind: 'unavailable'; readonly reason: string }
|
|
623
|
+
/** Nothing billed, and nothing can still bill. */
|
|
624
|
+
| { readonly kind: 'none' }
|
|
625
|
+
/** Nothing posted yet, but a charge reservation is still open. */
|
|
626
|
+
| { readonly kind: 'pending' }
|
|
627
|
+
/** Credits posted so far; the total may still change. */
|
|
628
|
+
| { readonly kind: 'settling'; readonly credits: number }
|
|
629
|
+
/** The final total. */
|
|
630
|
+
| { readonly kind: 'settled'; readonly credits: number };
|
|
631
|
+
|
|
632
|
+
export function deriveRunCostState(input: {
|
|
633
|
+
/** Posted credits from the billing read; `undefined` when none recorded. */
|
|
634
|
+
credits: number | null | undefined;
|
|
635
|
+
/**
|
|
636
|
+
* Unsettled charge reservations from the billing read: `true`/`false`, or
|
|
637
|
+
* `null` when the read could not say (then a shown amount is not final).
|
|
638
|
+
*/
|
|
639
|
+
settling: boolean | null;
|
|
640
|
+
/** The run is still live, so its total is a running total. */
|
|
641
|
+
live: boolean;
|
|
642
|
+
}): RunCostState {
|
|
643
|
+
const credits =
|
|
644
|
+
typeof input.credits === 'number' &&
|
|
645
|
+
Number.isFinite(input.credits) &&
|
|
646
|
+
input.credits > 0
|
|
647
|
+
? input.credits
|
|
648
|
+
: null;
|
|
649
|
+
const mayChange = input.live || input.settling !== false;
|
|
650
|
+
if (credits === null) {
|
|
651
|
+
// Only an unsettled reservation says a charge is coming. A live run that
|
|
652
|
+
// has billed nothing and holds none has, so far, cost nothing.
|
|
653
|
+
return input.settling === true ? { kind: 'pending' } : { kind: 'none' };
|
|
654
|
+
}
|
|
655
|
+
return mayChange
|
|
656
|
+
? { kind: 'settling', credits }
|
|
657
|
+
: { kind: 'settled', credits };
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/** "0.06", "<0.01", "12", "1.2k" — one amount format for every surface. */
|
|
661
|
+
export function formatCreditAmount(credits: number): string {
|
|
662
|
+
if (credits < 0.01) return '<0.01';
|
|
663
|
+
if (credits >= 1000) {
|
|
664
|
+
return `${(credits / 1000).toLocaleString('en-US', { maximumFractionDigits: 1 })}k`;
|
|
665
|
+
}
|
|
666
|
+
return credits.toLocaleString('en-US', {
|
|
667
|
+
minimumFractionDigits: credits < 10 ? 2 : 0,
|
|
668
|
+
maximumFractionDigits: credits < 10 ? 2 : 0,
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function creditsWord(credits: number): string {
|
|
673
|
+
const amount = formatCreditAmount(credits);
|
|
674
|
+
return `${amount} ${amount === '1.00' ? 'credit' : 'credits'}`;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* The cost as one line: `deepline runs get`'s `credits:` line and the run
|
|
679
|
+
* record's header. "0.06 credits", "0.06 credits · settling", "settling",
|
|
680
|
+
* "none", "unavailable (…)".
|
|
681
|
+
*/
|
|
682
|
+
export function formatRunCostState(state: RunCostState): string {
|
|
683
|
+
switch (state.kind) {
|
|
684
|
+
case 'unavailable':
|
|
685
|
+
return `unavailable (${state.reason})`;
|
|
686
|
+
case 'none':
|
|
687
|
+
return 'none';
|
|
688
|
+
case 'pending':
|
|
689
|
+
return 'settling';
|
|
690
|
+
case 'settling':
|
|
691
|
+
return `${creditsWord(state.credits)} · settling`;
|
|
692
|
+
case 'settled':
|
|
693
|
+
return creditsWord(state.credits);
|
|
694
|
+
default:
|
|
695
|
+
return assertNeverState(state, 'run cost state');
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* The same line without the unit, for a surface that draws the credit mark
|
|
701
|
+
* beside it (the run record's header): "0.06", "0.06 · settling", "settling".
|
|
702
|
+
*/
|
|
703
|
+
export function formatRunCostAmount(state: RunCostState): string {
|
|
704
|
+
switch (state.kind) {
|
|
705
|
+
case 'unavailable':
|
|
706
|
+
case 'none':
|
|
707
|
+
case 'pending':
|
|
708
|
+
return formatRunCostState(state);
|
|
709
|
+
case 'settling':
|
|
710
|
+
return `${formatCreditAmount(state.credits)} · settling`;
|
|
711
|
+
case 'settled':
|
|
712
|
+
return formatCreditAmount(state.credits);
|
|
713
|
+
default:
|
|
714
|
+
return assertNeverState(state, 'run cost state');
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/** The runs gutter's few characters: "0.06", "0.06…", "…", "" for none. */
|
|
719
|
+
export function formatRunCostCompact(state: RunCostState): string {
|
|
720
|
+
switch (state.kind) {
|
|
721
|
+
case 'unavailable':
|
|
722
|
+
case 'none':
|
|
723
|
+
return '';
|
|
724
|
+
case 'pending':
|
|
725
|
+
return '…';
|
|
726
|
+
case 'settling':
|
|
727
|
+
return `${formatCreditAmount(state.credits)}…`;
|
|
728
|
+
case 'settled':
|
|
729
|
+
return formatCreditAmount(state.credits);
|
|
730
|
+
default:
|
|
731
|
+
return assertNeverState(state, 'run cost state');
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// ---------------------------------------------------------------------------
|
|
736
|
+
// Wire decoding
|
|
737
|
+
// ---------------------------------------------------------------------------
|
|
738
|
+
|
|
739
|
+
function isObject(value: unknown): value is Record<string, unknown> {
|
|
740
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function decodeRows(value: unknown): RowCounts | null | undefined {
|
|
744
|
+
if (value === null) return null;
|
|
745
|
+
if (!isObject(value)) return undefined;
|
|
746
|
+
const rows = rowCountsFrom({
|
|
747
|
+
completed: value.completed,
|
|
748
|
+
failed: value.failed,
|
|
749
|
+
total: value.total,
|
|
750
|
+
});
|
|
751
|
+
return rows ?? undefined;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function decodeStopCause(value: unknown): RunStopCause | null {
|
|
755
|
+
return value === 'run_failed' || value === 'run_cancelled' ? value : null;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Decode a step's `execution` from a run read (`steps[].execution`). Null for
|
|
760
|
+
* anything that is not a well-formed state — an older server sends none, and
|
|
761
|
+
* the caller then derives one from the step's reported fields.
|
|
762
|
+
*/
|
|
763
|
+
export function decodeNodeExecutionState(
|
|
764
|
+
value: unknown,
|
|
765
|
+
): NodeExecutionState | null {
|
|
766
|
+
if (!isObject(value)) return null;
|
|
767
|
+
const rows = decodeRows(value.rows);
|
|
768
|
+
switch (value.kind) {
|
|
769
|
+
case 'queued':
|
|
770
|
+
case 'skipped':
|
|
771
|
+
case 'unknown':
|
|
772
|
+
return { kind: value.kind };
|
|
773
|
+
case 'running':
|
|
774
|
+
return rows === undefined
|
|
775
|
+
? null
|
|
776
|
+
: {
|
|
777
|
+
kind: 'running',
|
|
778
|
+
rows,
|
|
779
|
+
startedAt:
|
|
780
|
+
typeof value.startedAt === 'number' ? value.startedAt : null,
|
|
781
|
+
};
|
|
782
|
+
case 'completed':
|
|
783
|
+
return rows === undefined ? null : { kind: 'completed', rows };
|
|
784
|
+
case 'failed':
|
|
785
|
+
return rows === undefined
|
|
786
|
+
? null
|
|
787
|
+
: {
|
|
788
|
+
kind: 'failed',
|
|
789
|
+
rows,
|
|
790
|
+
error: typeof value.error === 'string' ? value.error : null,
|
|
791
|
+
};
|
|
792
|
+
case 'stopped': {
|
|
793
|
+
const cause = decodeStopCause(value.cause);
|
|
794
|
+
return rows === undefined || !cause
|
|
795
|
+
? null
|
|
796
|
+
: { kind: 'stopped', rows, cause };
|
|
797
|
+
}
|
|
798
|
+
case 'not_reached': {
|
|
799
|
+
const cause =
|
|
800
|
+
value.cause === 'run_completed'
|
|
801
|
+
? 'run_completed'
|
|
802
|
+
: decodeStopCause(value.cause);
|
|
803
|
+
return cause ? { kind: 'not_reached', cause } : null;
|
|
804
|
+
}
|
|
805
|
+
default:
|
|
806
|
+
return null;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// ---------------------------------------------------------------------------
|
|
811
|
+
// A step the run carries no state for
|
|
812
|
+
// ---------------------------------------------------------------------------
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* What a step the run reported NO state for is, from the graph walk that says
|
|
816
|
+
* why (`deriveDocflowUnreachedReason`) and the one rule that turns it into a
|
|
817
|
+
* verdict (`docflowUnreachedVerdict`, both in
|
|
818
|
+
* ../plays/docflow-unreached-reason). The canvas pill and `deepline
|
|
819
|
+
* runs get` give the same answer. `null` makes no claim.
|
|
820
|
+
*/
|
|
821
|
+
export function nodeExecutionForUnreachedReason(input: {
|
|
822
|
+
reason: string;
|
|
823
|
+
runStatus: unknown;
|
|
824
|
+
}): NodeExecutionState | null {
|
|
825
|
+
const run = decodeRunStatus(input.runStatus);
|
|
826
|
+
const verdict = docflowUnreachedVerdict({
|
|
827
|
+
reason: input.reason,
|
|
828
|
+
runTerminal: run === 'completed' || run === 'failed' || run === 'cancelled',
|
|
829
|
+
});
|
|
830
|
+
switch (verdict) {
|
|
831
|
+
case null:
|
|
832
|
+
return null;
|
|
833
|
+
case 'skipped':
|
|
834
|
+
return { kind: 'skipped' };
|
|
835
|
+
case 'unknown':
|
|
836
|
+
return { kind: 'unknown' };
|
|
837
|
+
case 'not_reached':
|
|
838
|
+
return {
|
|
839
|
+
kind: 'not_reached',
|
|
840
|
+
cause: stopCauseForRun(run) ?? 'run_completed',
|
|
841
|
+
};
|
|
842
|
+
default:
|
|
843
|
+
return assertNeverState(verdict, 'unreached verdict');
|
|
844
|
+
}
|
|
845
|
+
}
|