tubeless 0.1.0

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.
Files changed (224) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +106 -0
  3. package/dist/abort.d.ts +12 -0
  4. package/dist/abort.d.ts.map +1 -0
  5. package/dist/abort.js +49 -0
  6. package/dist/abort.js.map +1 -0
  7. package/dist/batch.d.ts +22 -0
  8. package/dist/batch.d.ts.map +1 -0
  9. package/dist/batch.js +73 -0
  10. package/dist/batch.js.map +1 -0
  11. package/dist/checkpoint.d.ts +33 -0
  12. package/dist/checkpoint.d.ts.map +1 -0
  13. package/dist/checkpoint.js +71 -0
  14. package/dist/checkpoint.js.map +1 -0
  15. package/dist/child-execution.d.ts +30 -0
  16. package/dist/child-execution.d.ts.map +1 -0
  17. package/dist/child-execution.js +270 -0
  18. package/dist/child-execution.js.map +1 -0
  19. package/dist/cli-command.d.ts +29 -0
  20. package/dist/cli-command.d.ts.map +1 -0
  21. package/dist/cli-command.js +314 -0
  22. package/dist/cli-command.js.map +1 -0
  23. package/dist/cli-parser.d.ts +28 -0
  24. package/dist/cli-parser.d.ts.map +1 -0
  25. package/dist/cli-parser.js +557 -0
  26. package/dist/cli-parser.js.map +1 -0
  27. package/dist/cli-pipeline-command.d.ts +84 -0
  28. package/dist/cli-pipeline-command.d.ts.map +1 -0
  29. package/dist/cli-pipeline-command.js +185 -0
  30. package/dist/cli-pipeline-command.js.map +1 -0
  31. package/dist/cli-types.d.ts +220 -0
  32. package/dist/cli-types.d.ts.map +1 -0
  33. package/dist/cli-types.js +19 -0
  34. package/dist/cli-types.js.map +1 -0
  35. package/dist/cli.d.ts +6 -0
  36. package/dist/cli.d.ts.map +1 -0
  37. package/dist/cli.js +6 -0
  38. package/dist/cli.js.map +1 -0
  39. package/dist/env.d.ts +2 -0
  40. package/dist/env.d.ts.map +1 -0
  41. package/dist/env.js +8 -0
  42. package/dist/env.js.map +1 -0
  43. package/dist/file-utils.d.ts +4 -0
  44. package/dist/file-utils.d.ts.map +1 -0
  45. package/dist/file-utils.js +35 -0
  46. package/dist/file-utils.js.map +1 -0
  47. package/dist/interactive-reporter.d.ts +38 -0
  48. package/dist/interactive-reporter.d.ts.map +1 -0
  49. package/dist/interactive-reporter.js +308 -0
  50. package/dist/interactive-reporter.js.map +1 -0
  51. package/dist/lifecycle.d.ts +17 -0
  52. package/dist/lifecycle.d.ts.map +1 -0
  53. package/dist/lifecycle.js +77 -0
  54. package/dist/lifecycle.js.map +1 -0
  55. package/dist/live-ticker.d.ts +21 -0
  56. package/dist/live-ticker.d.ts.map +1 -0
  57. package/dist/live-ticker.js +377 -0
  58. package/dist/live-ticker.js.map +1 -0
  59. package/dist/mapped-child-progress.d.ts +109 -0
  60. package/dist/mapped-child-progress.d.ts.map +1 -0
  61. package/dist/mapped-child-progress.js +133 -0
  62. package/dist/mapped-child-progress.js.map +1 -0
  63. package/dist/node.d.ts +5 -0
  64. package/dist/node.d.ts.map +1 -0
  65. package/dist/node.js +5 -0
  66. package/dist/node.js.map +1 -0
  67. package/dist/paths.d.ts +9 -0
  68. package/dist/paths.d.ts.map +1 -0
  69. package/dist/paths.js +19 -0
  70. package/dist/paths.js.map +1 -0
  71. package/dist/pipeline-command-marker.d.ts +5 -0
  72. package/dist/pipeline-command-marker.d.ts.map +1 -0
  73. package/dist/pipeline-command-marker.js +21 -0
  74. package/dist/pipeline-command-marker.js.map +1 -0
  75. package/dist/pipeline-diagnostics.d.ts +4 -0
  76. package/dist/pipeline-diagnostics.d.ts.map +1 -0
  77. package/dist/pipeline-diagnostics.js +17 -0
  78. package/dist/pipeline-diagnostics.js.map +1 -0
  79. package/dist/pipeline-module.d.ts +62 -0
  80. package/dist/pipeline-module.d.ts.map +1 -0
  81. package/dist/pipeline-module.js +157 -0
  82. package/dist/pipeline-module.js.map +1 -0
  83. package/dist/pipeline.d.ts +606 -0
  84. package/dist/pipeline.d.ts.map +1 -0
  85. package/dist/pipeline.js +1444 -0
  86. package/dist/pipeline.js.map +1 -0
  87. package/dist/progress.d.ts +4 -0
  88. package/dist/progress.d.ts.map +1 -0
  89. package/dist/progress.js +8 -0
  90. package/dist/progress.js.map +1 -0
  91. package/dist/prompt-select.d.ts +81 -0
  92. package/dist/prompt-select.d.ts.map +1 -0
  93. package/dist/prompt-select.js +197 -0
  94. package/dist/prompt-select.js.map +1 -0
  95. package/dist/rate-limit.d.ts +7 -0
  96. package/dist/rate-limit.d.ts.map +1 -0
  97. package/dist/rate-limit.js +23 -0
  98. package/dist/rate-limit.js.map +1 -0
  99. package/dist/render.d.ts +21 -0
  100. package/dist/render.d.ts.map +1 -0
  101. package/dist/render.js +60 -0
  102. package/dist/render.js.map +1 -0
  103. package/dist/reporter-entry.d.ts +3 -0
  104. package/dist/reporter-entry.d.ts.map +1 -0
  105. package/dist/reporter-entry.js +3 -0
  106. package/dist/reporter-entry.js.map +1 -0
  107. package/dist/reporter.d.ts +50 -0
  108. package/dist/reporter.d.ts.map +1 -0
  109. package/dist/reporter.js +167 -0
  110. package/dist/reporter.js.map +1 -0
  111. package/dist/retry.d.ts +28 -0
  112. package/dist/retry.d.ts.map +1 -0
  113. package/dist/retry.js +59 -0
  114. package/dist/retry.js.map +1 -0
  115. package/dist/run-store-sqlite.d.ts +12 -0
  116. package/dist/run-store-sqlite.d.ts.map +1 -0
  117. package/dist/run-store-sqlite.js +187 -0
  118. package/dist/run-store-sqlite.js.map +1 -0
  119. package/dist/run-store-ui-http.d.ts +5 -0
  120. package/dist/run-store-ui-http.d.ts.map +1 -0
  121. package/dist/run-store-ui-http.js +19 -0
  122. package/dist/run-store-ui-http.js.map +1 -0
  123. package/dist/run-store-ui-page.d.ts +2 -0
  124. package/dist/run-store-ui-page.d.ts.map +1 -0
  125. package/dist/run-store-ui-page.js +759 -0
  126. package/dist/run-store-ui-page.js.map +1 -0
  127. package/dist/run-store-ui-protocol.d.ts +28 -0
  128. package/dist/run-store-ui-protocol.d.ts.map +1 -0
  129. package/dist/run-store-ui-protocol.js +84 -0
  130. package/dist/run-store-ui-protocol.js.map +1 -0
  131. package/dist/run-store-ui-state.d.ts +21 -0
  132. package/dist/run-store-ui-state.d.ts.map +1 -0
  133. package/dist/run-store-ui-state.js +65 -0
  134. package/dist/run-store-ui-state.js.map +1 -0
  135. package/dist/run-store-ui.d.ts +25 -0
  136. package/dist/run-store-ui.d.ts.map +1 -0
  137. package/dist/run-store-ui.js +217 -0
  138. package/dist/run-store-ui.js.map +1 -0
  139. package/dist/run-store.d.ts +109 -0
  140. package/dist/run-store.d.ts.map +1 -0
  141. package/dist/run-store.js +345 -0
  142. package/dist/run-store.js.map +1 -0
  143. package/dist/testing.d.ts +40 -0
  144. package/dist/testing.d.ts.map +1 -0
  145. package/dist/testing.js +83 -0
  146. package/dist/testing.js.map +1 -0
  147. package/dist/tracing-internal.d.ts +21 -0
  148. package/dist/tracing-internal.d.ts.map +1 -0
  149. package/dist/tracing-internal.js +206 -0
  150. package/dist/tracing-internal.js.map +1 -0
  151. package/dist/tracing-json.d.ts +12 -0
  152. package/dist/tracing-json.d.ts.map +1 -0
  153. package/dist/tracing-json.js +8 -0
  154. package/dist/tracing-json.js.map +1 -0
  155. package/dist/tracing-otel.d.ts +36 -0
  156. package/dist/tracing-otel.d.ts.map +1 -0
  157. package/dist/tracing-otel.js +60 -0
  158. package/dist/tracing-otel.js.map +1 -0
  159. package/dist/tracing.d.ts +48 -0
  160. package/dist/tracing.d.ts.map +1 -0
  161. package/dist/tracing.js +2 -0
  162. package/dist/tracing.js.map +1 -0
  163. package/dist/workbench-bin.d.ts +3 -0
  164. package/dist/workbench-bin.d.ts.map +1 -0
  165. package/dist/workbench-bin.js +8 -0
  166. package/dist/workbench-bin.js.map +1 -0
  167. package/dist/workbench-graph.d.ts +3 -0
  168. package/dist/workbench-graph.d.ts.map +1 -0
  169. package/dist/workbench-graph.js +64 -0
  170. package/dist/workbench-graph.js.map +1 -0
  171. package/dist/workbench-inspect.d.ts +3 -0
  172. package/dist/workbench-inspect.d.ts.map +1 -0
  173. package/dist/workbench-inspect.js +65 -0
  174. package/dist/workbench-inspect.js.map +1 -0
  175. package/dist/workbench-plan.d.ts +3 -0
  176. package/dist/workbench-plan.d.ts.map +1 -0
  177. package/dist/workbench-plan.js +72 -0
  178. package/dist/workbench-plan.js.map +1 -0
  179. package/dist/workbench-run.d.ts +13 -0
  180. package/dist/workbench-run.d.ts.map +1 -0
  181. package/dist/workbench-run.js +115 -0
  182. package/dist/workbench-run.js.map +1 -0
  183. package/dist/workbench-shared.d.ts +67 -0
  184. package/dist/workbench-shared.d.ts.map +1 -0
  185. package/dist/workbench-shared.js +163 -0
  186. package/dist/workbench-shared.js.map +1 -0
  187. package/dist/workbench-studio.d.ts +26 -0
  188. package/dist/workbench-studio.d.ts.map +1 -0
  189. package/dist/workbench-studio.js +57 -0
  190. package/dist/workbench-studio.js.map +1 -0
  191. package/dist/workbench-ui.d.ts +3 -0
  192. package/dist/workbench-ui.d.ts.map +1 -0
  193. package/dist/workbench-ui.js +262 -0
  194. package/dist/workbench-ui.js.map +1 -0
  195. package/dist/workbench.d.ts +5 -0
  196. package/dist/workbench.d.ts.map +1 -0
  197. package/dist/workbench.js +43 -0
  198. package/dist/workbench.js.map +1 -0
  199. package/docs/README.md +31 -0
  200. package/docs/agent-guide.md +168 -0
  201. package/docs/api-reference.md +269 -0
  202. package/docs/api-report.json +298 -0
  203. package/docs/child-pipeline-composition.md +185 -0
  204. package/docs/cli.md +118 -0
  205. package/docs/concepts.md +502 -0
  206. package/docs/getting-started.md +162 -0
  207. package/docs/llms.txt +62 -0
  208. package/docs/recipes.md +51 -0
  209. package/docs/studio.md +86 -0
  210. package/examples/best-effort.ts +42 -0
  211. package/examples/cancellation-and-testing.ts +45 -0
  212. package/examples/child-pipeline.ts +51 -0
  213. package/examples/cli-job.ts +86 -0
  214. package/examples/conditional-step.ts +41 -0
  215. package/examples/fan-out-progress.ts +53 -0
  216. package/examples/local-observability.ts +80 -0
  217. package/examples/publish-with-gates.ts +56 -0
  218. package/examples/rendering.ts +34 -0
  219. package/examples/resumable-enrichment.ts +75 -0
  220. package/examples/tracing.ts +35 -0
  221. package/examples/tsconfig.json +15 -0
  222. package/examples/typed-import.ts +54 -0
  223. package/examples/validated-boundaries.ts +75 -0
  224. package/package.json +156 -0
@@ -0,0 +1,1444 @@
1
+ import { abortableSleep, isAbortError, throwIfAborted as throwIfSignalAborted } from "./abort.js";
2
+ import { createPipelineLifecycleObserver } from "./lifecycle.js";
3
+ import { createMappedChildRunner, createSingleChildRunner, PipelineChildError, } from "./child-execution.js";
4
+ import { formatPipelineError } from "./pipeline-diagnostics.js";
5
+ import { createPipelineTraceEmitter } from "./tracing-internal.js";
6
+ /** Current persisted run-record schema version. */
7
+ export const RUN_MODEL_VERSION = 1;
8
+ export const PIPELINE_FINALIZE_STEP_ID = "__finalize__";
9
+ const fallbackNow = () => Date.now();
10
+ let nextRunSequence = 0;
11
+ /** Create a dependency-free opaque ID suitable for a pipeline run record. */
12
+ export function createRunId(pipelineId) {
13
+ nextRunSequence += 1;
14
+ const random = globalThis.crypto?.randomUUID?.();
15
+ return random
16
+ ? `${pipelineId}:${random}`
17
+ : `${pipelineId}:${Date.now().toString(36)}:${nextRunSequence.toString(36)}`;
18
+ }
19
+ function terminalRunStatus(errors) {
20
+ if (errors.length === 0)
21
+ return "completed";
22
+ return errors.length > 0 && errors.every(({ kind }) => kind === "cancellation")
23
+ ? "cancelled"
24
+ : "failed";
25
+ }
26
+ const fallbackSleep = (durationMs, signal) => abortableSleep(durationMs, signal, "Pipeline sleep");
27
+ const PIPELINE_LOGGER_BASE = Symbol("pipelineLoggerBase");
28
+ function basePipelineLogger(log) {
29
+ // SAFETY: `TracedPipelineLogger` only adds the optional symbol-keyed base
30
+ // property; reading it off any `PipelineLogger` is safe because the property
31
+ // is absent unless a tracing wrapper installed it.
32
+ return log[PIPELINE_LOGGER_BASE] ?? log;
33
+ }
34
+ export function defaultPipelineContext() {
35
+ return { cwd: process.cwd(), log: console, now: fallbackNow, sleep: fallbackSleep };
36
+ }
37
+ const originalPipelineErrors = new WeakMap();
38
+ function defaultExecutionErrorMessage(result) {
39
+ const firstError = result.errors[0];
40
+ const disposition = result.errors.length > 0 && result.errors.every(({ kind }) => kind === "cancellation")
41
+ ? "cancelled"
42
+ : "failed";
43
+ return firstError
44
+ ? `Pipeline ${result.pipelineId} ${disposition}: ${formatPipelineError(firstError)}`
45
+ : `Pipeline ${result.pipelineId} ${disposition}`;
46
+ }
47
+ function firstOriginalPipelineError(errors) {
48
+ for (const error of errors) {
49
+ if (originalPipelineErrors.has(error))
50
+ return originalPipelineErrors.get(error);
51
+ }
52
+ return undefined;
53
+ }
54
+ export class PipelineExecutionError extends Error {
55
+ result;
56
+ constructor(result, message = defaultExecutionErrorMessage(result)) {
57
+ const cause = firstOriginalPipelineError(result.errors);
58
+ super(message, cause === undefined ? undefined : { cause });
59
+ this.result = result;
60
+ this.name = "PipelineExecutionError";
61
+ }
62
+ }
63
+ /** Programmer error raised immediately when a pipeline graph is invalid. */
64
+ export class PipelineDefinitionError extends Error {
65
+ pipelineId;
66
+ errors;
67
+ constructor(pipelineId, errors) {
68
+ super(errors.length > 0
69
+ ? `Invalid pipeline ${pipelineId}: ${errors.map((error) => formatPipelineError(error)).join("; ")}`
70
+ : `Invalid pipeline ${pipelineId}`);
71
+ this.pipelineId = pipelineId;
72
+ this.errors = errors;
73
+ this.name = "PipelineDefinitionError";
74
+ }
75
+ }
76
+ class PipelineBoundaryValidationError extends Error {
77
+ issues;
78
+ constructor(message, issues, cause) {
79
+ super(message, cause === undefined ? undefined : { cause });
80
+ this.issues = issues;
81
+ this.name = "PipelineBoundaryValidationError";
82
+ }
83
+ }
84
+ function normalizeValidationPath(path) {
85
+ if (!path || path.length === 0)
86
+ return undefined;
87
+ return path.map((segment) => {
88
+ const key = typeof segment === "object" && segment !== null ? segment.key : segment;
89
+ return typeof key === "number" || typeof key === "string" ? key : String(key);
90
+ });
91
+ }
92
+ function renderValidationIssue(issue) {
93
+ const location = issue.path?.map(String).join(".");
94
+ return location ? `${location}: ${issue.message}` : issue.message;
95
+ }
96
+ async function validateStandardSchema(schema, value, boundary) {
97
+ let result;
98
+ try {
99
+ result = await schema["~standard"].validate(value);
100
+ }
101
+ catch (error) {
102
+ throw new PipelineBoundaryValidationError(`${boundary} schema (${schema["~standard"].vendor}) threw while validating`, [], error);
103
+ }
104
+ if (result.issues) {
105
+ const issues = result.issues.map((issue) => {
106
+ const path = normalizeValidationPath(issue.path);
107
+ const normalized = { message: issue.message };
108
+ if (path)
109
+ normalized.path = path;
110
+ return normalized;
111
+ });
112
+ throw new PipelineBoundaryValidationError(`${boundary} validation failed: ${issues.map(renderValidationIssue).join("; ")}`, issues);
113
+ }
114
+ return result.value;
115
+ }
116
+ const PIPELINE_RUN_CONTROL_KEYS = ["continueOnError", "dryRun", "stepIds", "targets"];
117
+ function isPipelineRunControlKey(property) {
118
+ return (typeof property === "string" &&
119
+ // SAFETY: `includes` membership over the literal tuple guarantees the cast
120
+ // target is exactly one of the declared run-control keys.
121
+ PIPELINE_RUN_CONTROL_KEYS.includes(property));
122
+ }
123
+ function createDomainOptionsTarget(options) {
124
+ const descriptors = Object.fromEntries(Reflect.ownKeys(options)
125
+ .filter((property) => !isPipelineRunControlKey(property))
126
+ .map((property) => {
127
+ const descriptor = Reflect.getOwnPropertyDescriptor(options, property);
128
+ return [property, { ...descriptor, configurable: true }];
129
+ }));
130
+ // SAFETY: `Object.create` returns the same prototype shape as `options` with
131
+ // only its own enumerable descriptors re-applied, so it remains assignable
132
+ // to the caller's `object` contract.
133
+ return Object.create(Object.getPrototypeOf(options), descriptors);
134
+ }
135
+ function createDomainOptionsView(options) {
136
+ const target = createDomainOptionsTarget(options);
137
+ const boundMethods = new WeakMap();
138
+ // SAFETY: the Proxy wraps the domain-options target and forwards to the
139
+ // original options, so its surface is assignable to `TOptions`.
140
+ return new Proxy(target, {
141
+ get(_target, property) {
142
+ if (isPipelineRunControlKey(property))
143
+ return undefined;
144
+ // SAFETY: `property` is a non-control key, so indexing the options object
145
+ // with it reads a genuine domain property; the cast is the only way to
146
+ // express dynamic-key access on a generic options type.
147
+ const value = options[property];
148
+ if (typeof value !== "function")
149
+ return value;
150
+ const cached = boundMethods.get(value);
151
+ if (cached)
152
+ return cached;
153
+ const bound = value.bind(options);
154
+ boundMethods.set(value, bound);
155
+ return bound;
156
+ },
157
+ has(_target, property) {
158
+ return isPipelineRunControlKey(property) ? false : Reflect.has(options, property);
159
+ },
160
+ set(_target, property, value) {
161
+ if (isPipelineRunControlKey(property))
162
+ return true;
163
+ return Reflect.set(options, property, value, options);
164
+ },
165
+ // SAFETY: the Proxy target is the domain-options object, so it is
166
+ // assignable to the caller's `TOptions` contract.
167
+ });
168
+ }
169
+ function splitPipelineRunOptions(options) {
170
+ const controls = {};
171
+ let hasControls = false;
172
+ for (const key of PIPELINE_RUN_CONTROL_KEYS) {
173
+ if (!Object.prototype.hasOwnProperty.call(options, key))
174
+ continue;
175
+ hasControls = true;
176
+ Object.assign(controls, { [key]: options[key] });
177
+ }
178
+ return {
179
+ controls,
180
+ domainOptions: hasControls ? createDomainOptionsView(options) : options,
181
+ };
182
+ }
183
+ const STEP_OPTIONS_SCHEMA = Symbol("tubeless.stepOptionsSchema");
184
+ const STEP_NESTED_PIPELINE = Symbol("tubeless.stepNestedPipeline");
185
+ export function createSteps(optionsSchema) {
186
+ return createStepFactory(optionsSchema);
187
+ }
188
+ function createStepFactory(optionsSchema) {
189
+ // Runtime construction is deliberately simple; the public factory owns the
190
+ // distinction between ordinary and explicitly skippable constructors.
191
+ const buildStep = (id, definition) => {
192
+ // SAFETY: `definition` carries the step's own fields; spreading it onto the
193
+ // id-bearing shell yields a value whose shape matches `AnyStep<TOptions>`.
194
+ const built = { id, ...definition };
195
+ if (optionsSchema) {
196
+ Object.defineProperty(built, STEP_OPTIONS_SCHEMA, { value: optionsSchema });
197
+ }
198
+ return built;
199
+ };
200
+ // SAFETY: the closure's parameter and return shapes match the
201
+ // `fromPipeline` constructor signature it is assigned to.
202
+ const fromPipeline = ((id, config) => {
203
+ const definition = {
204
+ [STEP_NESTED_PIPELINE]: {
205
+ mode: "single",
206
+ pipelineId: config.pipeline.id,
207
+ stepIds: config.pipeline.stepIds,
208
+ },
209
+ dependsOn: config.dependsOn,
210
+ optionalDependsOn: config.optionalDependsOn,
211
+ skipAfterFailureOf: config.skipAfterFailureOf,
212
+ name: config.name,
213
+ description: config.description,
214
+ dryRun: config.dryRun,
215
+ // SAFETY: the config carries `pipeline` and `mapResult`; the runner
216
+ // derives `mapOptions` from the same fields, so the config is a valid
217
+ // single-child execution config at runtime.
218
+ run: createSingleChildRunner(config, {
219
+ createExecutionError: (result, message) => new PipelineExecutionError(result, message),
220
+ isCancellation: (error, childContext) => isPipelineCancellation(error, childContext),
221
+ }),
222
+ };
223
+ if ("skip" in config && config.skip !== undefined) {
224
+ const skip = config.skip;
225
+ // SAFETY: the skip predicate is invoked with the step's own inputs and
226
+ // returns a step-skip decision; the casts only align the generic
227
+ // predicate signature with the concrete step context.
228
+ return buildStep(id, {
229
+ ...definition,
230
+ skip: (inputs, context) => skip(inputs, context),
231
+ });
232
+ }
233
+ return buildStep(id, definition);
234
+ });
235
+ // SAFETY: the closure's parameter and return shapes match the
236
+ // `forEachPipeline` constructor signature it is assigned to.
237
+ const forEachPipeline = ((id, config) => buildStep(id, {
238
+ [STEP_NESTED_PIPELINE]: {
239
+ mode: "for-each",
240
+ pipelineId: config.pipeline.id,
241
+ stepIds: config.pipeline.stepIds,
242
+ },
243
+ dependsOn: config.dependsOn,
244
+ optionalDependsOn: config.optionalDependsOn,
245
+ skipAfterFailureOf: config.skipAfterFailureOf,
246
+ name: config.name,
247
+ description: config.description,
248
+ dryRun: config.dryRun,
249
+ // SAFETY: the config carries `pipeline`, `items`, `key`, and `mapResult`;
250
+ // the runner derives the rest from those fields, so the config is a valid
251
+ // mapped-child execution config at runtime.
252
+ run: createMappedChildRunner(config, {
253
+ createExecutionError: (result, message) => new PipelineExecutionError(result, message),
254
+ isCancellation: (error, childContext) => isPipelineCancellation(error, childContext),
255
+ }),
256
+ }));
257
+ // SAFETY: `buildStep` is a callable; attaching the constructor properties
258
+ // yields exactly the `StepFactory` surface (call + skippable + fromPipeline +
259
+ // forEachPipeline), so the single assertion is sound.
260
+ // SAFETY: `buildStep` is a callable; attaching the constructor properties
261
+ // yields exactly the `StepFactory` surface (call + skippable + fromPipeline +
262
+ // forEachPipeline), so the single assertion is sound.
263
+ const factory = Object.assign(buildStep, {
264
+ skippable: buildStep,
265
+ fromPipeline,
266
+ forEachPipeline,
267
+ });
268
+ // SAFETY: `fromPipeline` is the same callable as `factory.fromPipeline`; it
269
+ // carries the skippable variant, so the assignment is sound.
270
+ factory.fromPipeline.skippable = fromPipeline;
271
+ return factory;
272
+ }
273
+ const REQUIRED_FINALIZER_OUTPUTS = Symbol("tubeless.requiredFinalizerOutputs");
274
+ /**
275
+ * Build a finalizer that only runs when every listed step published an output.
276
+ * Presence is checked by output slot, so a successfully published `undefined`
277
+ * remains distinct from a structural skip or failure.
278
+ */
279
+ export function requireOutputs(requiredSteps, finalize) {
280
+ const uniqueRequiredSteps = [...new Set(requiredSteps)];
281
+ const requiredFinalizer = (outputs, context) => {
282
+ const missingStepIds = uniqueRequiredSteps
283
+ .filter((step) => !Object.prototype.hasOwnProperty.call(outputs, step.id))
284
+ .map((step) => step.id);
285
+ if (missingStepIds.length > 0) {
286
+ throw new Error(`Required pipeline outputs missing: ${missingStepIds.join(", ")}`);
287
+ }
288
+ // SAFETY: every required step id was verified present above, so the partial
289
+ // outputs contain all required keys and are a complete required-outputs map.
290
+ return finalize(outputs, context);
291
+ };
292
+ Object.defineProperty(requiredFinalizer, REQUIRED_FINALIZER_OUTPUTS, {
293
+ value: uniqueRequiredSteps,
294
+ });
295
+ return requiredFinalizer;
296
+ }
297
+ function duplicateValues(values) {
298
+ const seen = new Set();
299
+ const duplicates = new Set();
300
+ for (const value of values) {
301
+ if (seen.has(value)) {
302
+ duplicates.add(value);
303
+ }
304
+ seen.add(value);
305
+ }
306
+ return [...duplicates].sort((left, right) => left.localeCompare(right));
307
+ }
308
+ function pipelineDiagnostic(code, phase, kind, message, fields = {}) {
309
+ return { code, kind, message, phase, ...fields };
310
+ }
311
+ const MAX_PIPELINE_CAUSE_DEPTH = 8;
312
+ function readErrorField(value, field) {
313
+ try {
314
+ // SAFETY: any object may be probed for an optional string-keyed field; a
315
+ // getter that throws is caught below, so the cast only enables the lookup.
316
+ return value[field];
317
+ }
318
+ catch {
319
+ return undefined;
320
+ }
321
+ }
322
+ function safeErrorMessage(value) {
323
+ try {
324
+ return String(value);
325
+ }
326
+ catch {
327
+ return "Unknown thrown value";
328
+ }
329
+ }
330
+ function normalizePipelineCause(value, seen, depth = 0) {
331
+ if (depth >= MAX_PIPELINE_CAUSE_DEPTH) {
332
+ return { message: "Cause chain truncated" };
333
+ }
334
+ if (typeof value !== "object" || value === null) {
335
+ return { message: safeErrorMessage(value) };
336
+ }
337
+ if (seen.has(value)) {
338
+ return { message: "Circular cause" };
339
+ }
340
+ seen.add(value);
341
+ const message = readErrorField(value, "message");
342
+ const name = readErrorField(value, "name");
343
+ const sourceCode = readErrorField(value, "code");
344
+ const nested = readErrorField(value, "cause");
345
+ const cause = {
346
+ message: typeof message === "string" ? message : safeErrorMessage(value),
347
+ };
348
+ if (typeof name === "string")
349
+ cause.name = name;
350
+ if (typeof sourceCode === "string")
351
+ cause.sourceCode = sourceCode;
352
+ if (nested !== undefined)
353
+ cause.cause = normalizePipelineCause(nested, seen, depth + 1);
354
+ return cause;
355
+ }
356
+ function normalizedNestedCause(error) {
357
+ if (typeof error !== "object" || error === null)
358
+ return undefined;
359
+ const cause = readErrorField(error, "cause");
360
+ if (cause === undefined)
361
+ return undefined;
362
+ const seen = new WeakSet();
363
+ seen.add(error);
364
+ return normalizePipelineCause(cause, seen);
365
+ }
366
+ function toPipelineError(error, classification) {
367
+ const sourceCode = typeof error === "object" && error !== null ? readErrorField(error, "code") : undefined;
368
+ const cause = normalizedNestedCause(error);
369
+ const pipelineError = {
370
+ ...classification,
371
+ message: error instanceof Error ? error.message : safeErrorMessage(error),
372
+ };
373
+ if (error instanceof Error && error.stack)
374
+ pipelineError.stack = error.stack;
375
+ if (typeof sourceCode === "string")
376
+ pipelineError.sourceCode = sourceCode;
377
+ if (cause)
378
+ pipelineError.cause = cause;
379
+ if (error instanceof PipelineBoundaryValidationError)
380
+ pipelineError.issues = error.issues;
381
+ originalPipelineErrors.set(pipelineError, error);
382
+ return pipelineError;
383
+ }
384
+ function isPipelineCancellation(error, runtime) {
385
+ if (isAbortError(error))
386
+ return true;
387
+ if (runtime.signal?.aborted === true && error === runtime.signal.reason)
388
+ return true;
389
+ if (error instanceof PipelineChildError)
390
+ return error.cancelled;
391
+ return error instanceof PipelineExecutionError && isCancelledResult(error.result);
392
+ }
393
+ function isCancelledResult(result) {
394
+ return result.status === "cancelled";
395
+ }
396
+ function errorRunResult(runtime, pipelineId, dryRun, startedAt, errors, identity) {
397
+ const finishedAtMs = runtime.now();
398
+ const result = {
399
+ pipelineId,
400
+ dryRun,
401
+ errors,
402
+ finalized: false,
403
+ finishedAtMs,
404
+ runId: identity.runId,
405
+ startedAtMs: startedAt,
406
+ status: terminalRunStatus(errors),
407
+ steps: [],
408
+ version: RUN_MODEL_VERSION,
409
+ };
410
+ if (identity.parentRunId)
411
+ result.parentRunId = identity.parentRunId;
412
+ return result;
413
+ }
414
+ function resolvePipelineRuntime(context = {}) {
415
+ const defaults = defaultPipelineContext();
416
+ return {
417
+ ...defaults,
418
+ ...context,
419
+ cwd: context.cwd ?? defaults.cwd,
420
+ log: context.log ?? defaults.log,
421
+ now: context.now ?? defaults.now ?? fallbackNow,
422
+ sleep: context.sleep ?? defaults.sleep ?? fallbackSleep,
423
+ };
424
+ }
425
+ function throwIfAborted(runtime) {
426
+ throwIfSignalAborted(runtime.signal, "Pipeline run");
427
+ }
428
+ function recordSkip(step, stepId, reports, reportsByStepId, reason, message, dependencyId, attemptId, startedAtMs, finishedAtMs) {
429
+ const report = {
430
+ id: stepId,
431
+ name: step.name,
432
+ description: step.description,
433
+ finishedAtMs,
434
+ reason,
435
+ status: "skipped",
436
+ };
437
+ if (attemptId)
438
+ report.attemptId = attemptId;
439
+ if (startedAtMs !== undefined)
440
+ report.startedAtMs = startedAtMs;
441
+ if (dependencyId)
442
+ report.dependencyId = dependencyId;
443
+ if (message)
444
+ report.message = message;
445
+ reports.push(report);
446
+ reportsByStepId.set(stepId, report);
447
+ return report;
448
+ }
449
+ function normalizeStepSkipDecision(decision) {
450
+ if (decision == null || decision === false)
451
+ return null;
452
+ if (typeof decision === "string") {
453
+ const reason = decision.trim();
454
+ return reason.length > 0 ? { reason } : null;
455
+ }
456
+ if (typeof decision === "object" && typeof decision.reason === "string") {
457
+ const reason = decision.reason.trim();
458
+ if (reason.length === 0)
459
+ return null;
460
+ return { reason, value: decision.value };
461
+ }
462
+ return null;
463
+ }
464
+ /** Required dependencies are met when completed, or intentionally policy-skipped. */
465
+ function isRequiredDependencyMet(report) {
466
+ if (!report)
467
+ return false;
468
+ if (report.status === "complete")
469
+ return true;
470
+ return report.status === "skipped" && report.reason === "policy";
471
+ }
472
+ /**
473
+ * Single skip ladder for plan, live execution, and fail-fast/cancel remainder.
474
+ * Policy skips stay outside: they run only after a step is scheduled.
475
+ *
476
+ * When `stoppedAfter` is set, live failed/cancelled deps and fail-fast remainder
477
+ * skips do not reclassify the step as unmet-dependency — those become fail-fast
478
+ * or cancelled so the whole remainder keeps today's abort attribution. Structural
479
+ * skip reports (filtered / unmet-dependency / dry-run / failed-dependency) still
480
+ * win so plan-time chains survive remainder recording.
481
+ */
482
+ function decideStepDisposition(input) {
483
+ const { dryRun, planned, reportsByStepId, step, stoppedAfter } = input;
484
+ if (!planned.selected) {
485
+ return { kind: "skip", reason: "filtered" };
486
+ }
487
+ if (!stoppedAfter) {
488
+ const unsuccessfulAncestor = (step.skipAfterFailureOf ?? []).find((dep) => {
489
+ const status = reportsByStepId.get(dep.id)?.status;
490
+ return status === "failed" || status === "cancelled";
491
+ });
492
+ if (unsuccessfulAncestor) {
493
+ return {
494
+ kind: "skip",
495
+ reason: "failed-dependency",
496
+ dependencyId: unsuccessfulAncestor.id,
497
+ };
498
+ }
499
+ }
500
+ const unmetDependency = (step.dependsOn ?? []).find((dep) => {
501
+ const report = reportsByStepId.get(dep.id);
502
+ if (stoppedAfter) {
503
+ // Only structural skips propagate under abort remainder. Failed/cancelled
504
+ // deps and fail-fast skips fall through so the step is fail-fast/cancelled.
505
+ return (report?.status === "skipped" && report.reason !== "policy" && report.reason !== "fail-fast");
506
+ }
507
+ return !isRequiredDependencyMet(report);
508
+ });
509
+ if (unmetDependency) {
510
+ return {
511
+ kind: "skip",
512
+ reason: "unmet-dependency",
513
+ dependencyId: unmetDependency.id,
514
+ };
515
+ }
516
+ if (dryRun && skipsInDryRun(step)) {
517
+ return { kind: "skip", reason: "dry-run" };
518
+ }
519
+ if (stoppedAfter) {
520
+ if (stoppedAfter.cancelled) {
521
+ return { kind: "cancelled" };
522
+ }
523
+ const failedStepId = stoppedAfter.stepId;
524
+ const disposition = {
525
+ kind: "skip",
526
+ reason: "fail-fast",
527
+ message: failedStepId
528
+ ? `Not run because fail-fast stopped after ${failedStepId} failed.`
529
+ : "Not run because the pipeline was aborted before this step started.",
530
+ };
531
+ if (failedStepId)
532
+ disposition.dependencyId = failedStepId;
533
+ return disposition;
534
+ }
535
+ return { kind: "run" };
536
+ }
537
+ /**
538
+ * Deduplicated by object identity: the same dependency commonly appears in more than one
539
+ * of these arrays (e.g. a step that's both `optionalDependsOn` and `skipAfterFailureOf` the
540
+ * same upstream step). Without dedup, `topologicalSort`'s in-degree count would no longer
541
+ * equal the number of distinct prerequisites — harmless there since the inflated count and
542
+ * the duplicated `dependentsOf` entries cancel out, but confusing to read and a latent trap
543
+ * for future callers of this helper that assume it returns distinct edges.
544
+ */
545
+ function stepEdges(step) {
546
+ return [
547
+ ...new Set([
548
+ ...(step.dependsOn ?? []),
549
+ ...(step.optionalDependsOn ?? []),
550
+ ...(step.skipAfterFailureOf ?? []),
551
+ ]),
552
+ ];
553
+ }
554
+ /** Required data and failure-gate closure for dependency-aware targets. */
555
+ function targetClosure(targets) {
556
+ const selected = new Set();
557
+ const include = (step) => {
558
+ if (selected.has(step))
559
+ return;
560
+ selected.add(step);
561
+ for (const prerequisite of [...(step.dependsOn ?? []), ...(step.skipAfterFailureOf ?? [])]) {
562
+ include(prerequisite);
563
+ }
564
+ };
565
+ for (const target of targets)
566
+ include(target);
567
+ return selected;
568
+ }
569
+ /**
570
+ * Orders `steps` so every dependency runs before its dependents, using each step's
571
+ * original array position to break ties. For an array that's already in valid dependency
572
+ * order (true of every hand-ordered pipeline today) this provably reproduces that same
573
+ * order — each step becomes eligible (in-degree 0) at the same point it would've been
574
+ * reached by a plain declaration-order walk. What it adds is tolerance for a `steps` array
575
+ * that ISN'T already ordered, and a real error instead of silent misbehavior on a cycle.
576
+ * Returns null if the graph has a cycle (shouldn't happen given `const`-reference
577
+ * construction, but the executor should never spin or drop steps if it somehow does).
578
+ */
579
+ function topologicalSort(steps) {
580
+ const indexById = new Map(steps.map((step, index) => [step.id, index]));
581
+ const remainingInDegree = new Map(steps.map((step) => [step.id, stepEdges(step).length]));
582
+ const dependentsOf = new Map();
583
+ for (const step of steps) {
584
+ for (const dep of stepEdges(step)) {
585
+ const dependents = dependentsOf.get(dep.id) ?? [];
586
+ dependents.push(step.id);
587
+ dependentsOf.set(dep.id, dependents);
588
+ }
589
+ }
590
+ const ready = steps.filter((step) => remainingInDegree.get(step.id) === 0).map((step) => step.id);
591
+ const orderedIds = [];
592
+ while (ready.length > 0) {
593
+ ready.sort((left, right) => (indexById.get(left) ?? 0) - (indexById.get(right) ?? 0));
594
+ const nextId = ready.shift();
595
+ if (nextId === undefined) {
596
+ break;
597
+ }
598
+ orderedIds.push(nextId);
599
+ for (const dependentId of dependentsOf.get(nextId) ?? []) {
600
+ const remaining = (remainingInDegree.get(dependentId) ?? 0) - 1;
601
+ remainingInDegree.set(dependentId, remaining);
602
+ if (remaining === 0) {
603
+ ready.push(dependentId);
604
+ }
605
+ }
606
+ }
607
+ if (orderedIds.length !== steps.length) {
608
+ return null;
609
+ }
610
+ const stepsById = new Map(steps.map((step) => [step.id, step]));
611
+ // SAFETY: every id in `orderedIds` was produced by walking the same `steps`
612
+ // array, so the map lookup always finds the matching `AnyStep<TOptions>`.
613
+ return orderedIds.map((id) => stepsById.get(id));
614
+ }
615
+ function escapeMermaidLabel(value) {
616
+ return value
617
+ .replace(/\r\n?|\n/g, " ")
618
+ .replace(/[\u0000-\u001f\u007f-\u009f]/g, " ")
619
+ .replace(/#/g, "#35;")
620
+ .replace(/&/g, "#38;")
621
+ .replace(/"/g, "#quot;")
622
+ .replace(/</g, "#60;")
623
+ .replace(/>/g, "#62;");
624
+ }
625
+ function renderPipelineMermaid(steps, options) {
626
+ const direction = options.direction ?? "TD";
627
+ if (!["BT", "LR", "RL", "TB", "TD"].includes(direction)) {
628
+ throw new Error(`Invalid Mermaid flowchart direction: ${direction}`);
629
+ }
630
+ const nodeIdByStep = new Map(steps.map((step, index) => [step, `step${index}`]));
631
+ const lines = [`flowchart ${direction}`];
632
+ for (const step of steps) {
633
+ const nodeId = nodeIdByStep.get(step);
634
+ const displayName = step.name ?? step.id;
635
+ const label = options.includeDescriptions && step.description
636
+ ? `${displayName} — ${step.description}`
637
+ : displayName;
638
+ lines.push(` ${nodeId}["${escapeMermaidLabel(label)}"]`);
639
+ }
640
+ const edgeLines = [];
641
+ for (const step of steps) {
642
+ const targetId = nodeIdByStep.get(step);
643
+ const required = new Set(step.dependsOn ?? []);
644
+ const optional = new Set(step.optionalDependsOn ?? []);
645
+ const failureGates = new Set(step.skipAfterFailureOf ?? []);
646
+ for (const dependency of stepEdges(step)) {
647
+ const sourceId = nodeIdByStep.get(dependency);
648
+ if (required.has(dependency)) {
649
+ edgeLines.push(` ${sourceId} --> ${targetId}`);
650
+ continue;
651
+ }
652
+ if (optional.has(dependency) && failureGates.has(dependency)) {
653
+ edgeLines.push(` ${sourceId} -. optional input + failure gate .-> ${targetId}`);
654
+ continue;
655
+ }
656
+ if (optional.has(dependency)) {
657
+ edgeLines.push(` ${sourceId} -. optional input .-> ${targetId}`);
658
+ continue;
659
+ }
660
+ edgeLines.push(` ${sourceId} -. failure gate .-> ${targetId}`);
661
+ }
662
+ }
663
+ if (edgeLines.length > 0) {
664
+ lines.push("", ...edgeLines);
665
+ }
666
+ return `${lines.join("\n")}\n`;
667
+ }
668
+ function stepToPlanStep(step, selected, skipReason, selectionReasons = [{ kind: "all" }]) {
669
+ const planStep = {
670
+ dependencies: (step.dependsOn ?? []).map((dep) => dep.id),
671
+ description: step.description,
672
+ dryRun: step.dryRun === "skip" ? "skip" : step.dryRun !== undefined ? "custom" : "run",
673
+ id: step.id,
674
+ name: step.name,
675
+ optionalDependencies: (step.optionalDependsOn ?? []).map((dep) => dep.id),
676
+ runtimeSkipPossible: step.skip !== undefined,
677
+ selected,
678
+ selectionReasons,
679
+ skipAfterFailureOf: (step.skipAfterFailureOf ?? []).map((dep) => dep.id),
680
+ skipReason,
681
+ };
682
+ if (step[STEP_NESTED_PIPELINE]) {
683
+ planStep.nestedPipeline = {
684
+ ...step[STEP_NESTED_PIPELINE],
685
+ stepIds: [...step[STEP_NESTED_PIPELINE].stepIds],
686
+ };
687
+ }
688
+ return planStep;
689
+ }
690
+ function addSelectionReason(reasonsByStepId, stepId, reason) {
691
+ const reasons = reasonsByStepId.get(stepId) ?? [];
692
+ const reasonKey = JSON.stringify(reason);
693
+ if (!reasons.some((candidate) => JSON.stringify(candidate) === reasonKey)) {
694
+ reasons.push(reason);
695
+ reasonsByStepId.set(stepId, reasons);
696
+ }
697
+ }
698
+ function targetSelectionReasons(targets) {
699
+ const reasonsByStepId = new Map();
700
+ for (const target of targets) {
701
+ addSelectionReason(reasonsByStepId, target.id, { kind: "target", targetId: target.id });
702
+ const expanded = new Set();
703
+ const includePrerequisites = (step) => {
704
+ if (expanded.has(step))
705
+ return;
706
+ expanded.add(step);
707
+ for (const dependency of step.dependsOn ?? []) {
708
+ addSelectionReason(reasonsByStepId, dependency.id, {
709
+ dependentId: step.id,
710
+ kind: "required-dependency",
711
+ targetId: target.id,
712
+ });
713
+ includePrerequisites(dependency);
714
+ }
715
+ for (const dependency of step.skipAfterFailureOf ?? []) {
716
+ addSelectionReason(reasonsByStepId, dependency.id, {
717
+ dependentId: step.id,
718
+ kind: "failure-gate",
719
+ targetId: target.id,
720
+ });
721
+ includePrerequisites(dependency);
722
+ }
723
+ for (const dependency of step.optionalDependsOn ?? []) {
724
+ addSelectionReason(reasonsByStepId, dependency.id, {
725
+ dependentId: step.id,
726
+ kind: "optional-only",
727
+ targetId: target.id,
728
+ });
729
+ }
730
+ };
731
+ includePrerequisites(target);
732
+ }
733
+ return reasonsByStepId;
734
+ }
735
+ function selectsStep(reason) {
736
+ return !["not-selected", "optional-only", "outside-target-closure"].includes(reason.kind);
737
+ }
738
+ function skipsInDryRun(step) {
739
+ return step.dryRun === "skip";
740
+ }
741
+ function validatePipelineDefinition(definition) {
742
+ const steps = definition.steps;
743
+ const errors = [];
744
+ if (definition.id.trim().length === 0) {
745
+ errors.push(pipelineDiagnostic("TUBELESS_DEFINITION_PIPELINE_ID_BLANK", "definition", "definition", "Pipeline id must not be blank"));
746
+ }
747
+ const stepIds = steps.map((step) => step.id);
748
+ const duplicateStepIds = duplicateValues(stepIds);
749
+ if (duplicateStepIds.length > 0) {
750
+ errors.push(pipelineDiagnostic("TUBELESS_DEFINITION_STEP_IDS_DUPLICATE", "definition", "definition", `Pipeline ${definition.id} has duplicate step ids: ${duplicateStepIds.join(", ")}`));
751
+ }
752
+ const optionsSchemas = new Set(steps.map((step) => step[STEP_OPTIONS_SCHEMA]));
753
+ if (optionsSchemas.size > 1 && [...optionsSchemas].some(Boolean)) {
754
+ errors.push(pipelineDiagnostic("TUBELESS_DEFINITION_OPTIONS_SCHEMA_CONFLICT", "definition", "definition", `Pipeline ${definition.id} mixes steps from different options-schema scopes`));
755
+ }
756
+ for (const step of steps) {
757
+ if (step.id.trim().length === 0) {
758
+ errors.push(pipelineDiagnostic("TUBELESS_DEFINITION_STEP_ID_BLANK", "definition", "definition", `Pipeline ${definition.id} has a step with a blank id`, { stepId: step.id }));
759
+ }
760
+ if (step.id === PIPELINE_FINALIZE_STEP_ID) {
761
+ errors.push(pipelineDiagnostic("TUBELESS_DEFINITION_STEP_ID_RESERVED", "definition", "definition", `Pipeline ${definition.id} uses reserved step id ${PIPELINE_FINALIZE_STEP_ID}`, { stepId: step.id }));
762
+ }
763
+ if (step.name !== undefined && step.name.trim().length === 0) {
764
+ errors.push(pipelineDiagnostic("TUBELESS_DEFINITION_STEP_NAME_BLANK", "definition", "definition", `Pipeline ${definition.id} step ${step.id} has a blank display name`, { stepId: step.id }));
765
+ }
766
+ const dependencyGroups = [
767
+ ["dependsOn", step.dependsOn ?? []],
768
+ ["optionalDependsOn", step.optionalDependsOn ?? []],
769
+ ["skipAfterFailureOf", step.skipAfterFailureOf ?? []],
770
+ ];
771
+ for (const [field, dependencies] of dependencyGroups) {
772
+ const duplicates = duplicateValues(dependencies.map((dependency) => dependency.id));
773
+ if (duplicates.length > 0) {
774
+ errors.push(pipelineDiagnostic("TUBELESS_DEFINITION_DEPENDENCY_DUPLICATE", "definition", "definition", `Pipeline ${definition.id} step ${step.id} repeats ${field}: ${duplicates.join(", ")}`, { stepId: step.id }));
775
+ }
776
+ if (dependencies.includes(step)) {
777
+ errors.push(pipelineDiagnostic("TUBELESS_DEFINITION_DEPENDENCY_SELF_REFERENCE", "definition", "definition", `Pipeline ${definition.id} step ${step.id} cannot reference itself in ${field}`, { stepId: step.id }));
778
+ }
779
+ }
780
+ const required = new Set(step.dependsOn ?? []);
781
+ const contradictory = (step.optionalDependsOn ?? []).filter((dependency) => required.has(dependency));
782
+ if (contradictory.length > 0) {
783
+ errors.push(pipelineDiagnostic("TUBELESS_DEFINITION_DEPENDENCY_CONTRADICTORY", "definition", "definition", `Pipeline ${definition.id} step ${step.id} declares dependencies as both required and optional: ${contradictory.map(({ id }) => id).join(", ")}`, { stepId: step.id }));
784
+ }
785
+ }
786
+ // SAFETY: `steps` is `TSteps extends readonly AnyStep[]`; the cast restores
787
+ // the `TOptions` generic that the tuple erased, without changing the values.
788
+ const knownSteps = new Set(steps);
789
+ // SAFETY: targets are a subset of `TSteps[number]`, each an `AnyStep<TOptions>`.
790
+ const declaredTargets = (definition.targets ?? []);
791
+ const duplicateTargetIds = duplicateValues(declaredTargets.map((target) => target.id));
792
+ if (duplicateTargetIds.length > 0) {
793
+ errors.push(pipelineDiagnostic("TUBELESS_DEFINITION_TARGETS_DUPLICATE", "definition", "definition", `Pipeline ${definition.id} declares duplicate targets: ${duplicateTargetIds.join(", ")}`));
794
+ }
795
+ const missingTargets = declaredTargets.filter((target) => !knownSteps.has(target));
796
+ if (missingTargets.length > 0) {
797
+ errors.push(pipelineDiagnostic("TUBELESS_DEFINITION_TARGET_NOT_IN_STEPS", "definition", "definition", `Pipeline ${definition.id} declares target step(s) not included in its steps list: ${missingTargets.map(({ id }) => id).join(", ")}`));
798
+ }
799
+ const missingReferences = [];
800
+ // SAFETY: `steps` is `TSteps extends readonly AnyStep[]`; the cast restores the
801
+ // `TOptions` generic that the tuple erased, without changing the values.
802
+ for (const step of steps) {
803
+ for (const dependency of stepEdges(step)) {
804
+ if (!knownSteps.has(dependency)) {
805
+ missingReferences.push(`${step.id} -> ${dependency.id}`);
806
+ }
807
+ }
808
+ }
809
+ if (missingReferences.length > 0) {
810
+ errors.push(pipelineDiagnostic("TUBELESS_DEFINITION_DEPENDENCY_NOT_IN_STEPS", "definition", "definition", `Pipeline ${definition.id} references step(s) not included in its steps list: ${missingReferences.join(", ")}`));
811
+ }
812
+ // SAFETY: `requireOutputs` stamps the required step ids onto the finalizer
813
+ // function under `REQUIRED_FINALIZER_OUTPUTS`; the intersection only widens
814
+ // the function type to expose that optional property.
815
+ const requiredFinalizerSteps = definition.finalize[REQUIRED_FINALIZER_OUTPUTS];
816
+ const missingFinalizerSteps = (requiredFinalizerSteps ?? []).filter((step) => !knownSteps.has(step));
817
+ if (missingFinalizerSteps.length > 0) {
818
+ errors.push(pipelineDiagnostic("TUBELESS_DEFINITION_FINALIZER_STEP_NOT_IN_STEPS", "definition", "definition", `Pipeline ${definition.id} requires finalizer output from step(s) not included in its steps list: ${missingFinalizerSteps.map(({ id }) => id).join(", ")}`));
819
+ }
820
+ if (requiredFinalizerSteps &&
821
+ missingTargets.length === 0 &&
822
+ missingReferences.length === 0 &&
823
+ missingFinalizerSteps.length === 0) {
824
+ for (const target of declaredTargets) {
825
+ const selected = targetClosure([target]);
826
+ const missingRequiredSteps = requiredFinalizerSteps.filter((step) => !selected.has(step));
827
+ if (missingRequiredSteps.length > 0) {
828
+ errors.push(pipelineDiagnostic("TUBELESS_DEFINITION_TARGET_FINALIZER_MISMATCH", "definition", "definition", `Pipeline ${definition.id} target ${target.id} cannot satisfy required finalizer output(s): ${missingRequiredSteps.map(({ id }) => id).join(", ")}`, { stepId: target.id }));
829
+ }
830
+ }
831
+ }
832
+ // SAFETY: `steps` is `TSteps extends readonly AnyStep[]`; the cast restores the
833
+ // `TOptions` generic that the tuple erased, without changing the values.
834
+ if (errors.length === 0 && !topologicalSort(steps)) {
835
+ errors.push(pipelineDiagnostic("TUBELESS_DEFINITION_DEPENDENCY_CYCLE", "definition", "definition", `Pipeline ${definition.id} has a dependency cycle`));
836
+ }
837
+ return errors;
838
+ }
839
+ function buildPipelinePlan(definition, controls) {
840
+ const dryRun = controls.dryRun === true;
841
+ const steps = definition.steps;
842
+ const stepIds = steps.map((step) => step.id);
843
+ const errors = validatePipelineDefinition(definition);
844
+ const knownStepIds = new Set(stepIds);
845
+ const declaredTargetIds = new Set((definition.targets ?? []).map((target) => target.id));
846
+ const requestedStepIds = controls.stepIds ?? [];
847
+ const requestedTargets = controls.targets ?? [];
848
+ if (controls.stepIds !== undefined && controls.targets !== undefined) {
849
+ errors.push(pipelineDiagnostic("TUBELESS_PLANNING_SELECTION_CONFLICT", "planning", "selection", `Pipeline ${definition.id} cannot combine exact stepIds filtering with dependency-aware targets`));
850
+ }
851
+ if (controls.stepIds !== undefined && controls.stepIds.length === 0) {
852
+ errors.push(pipelineDiagnostic("TUBELESS_PLANNING_STEP_SELECTION_EMPTY", "planning", "selection", `Pipeline ${definition.id} received an empty stepIds array; omit stepIds to run every step, or pass at least one step id`));
853
+ }
854
+ if (controls.targets !== undefined && controls.targets.length === 0) {
855
+ errors.push(pipelineDiagnostic("TUBELESS_PLANNING_TARGET_SELECTION_EMPTY", "planning", "selection", `Pipeline ${definition.id} received an empty targets array; omit targets to run every step, or pass at least one target step id`));
856
+ }
857
+ const duplicateRequestedStepIds = duplicateValues(requestedStepIds);
858
+ if (duplicateRequestedStepIds.length > 0) {
859
+ errors.push(pipelineDiagnostic("TUBELESS_PLANNING_STEP_SELECTION_DUPLICATE", "planning", "selection", `Pipeline ${definition.id} requested duplicate step ids: ${duplicateRequestedStepIds.join(", ")}`));
860
+ }
861
+ const unknownRequestedStepIds = requestedStepIds.filter((stepId) => !knownStepIds.has(stepId));
862
+ if (unknownRequestedStepIds.length > 0) {
863
+ errors.push(pipelineDiagnostic("TUBELESS_PLANNING_STEP_UNKNOWN", "planning", "selection", `Pipeline ${definition.id} requested unknown step ids: ${unknownRequestedStepIds.join(", ")}`));
864
+ }
865
+ const duplicateTargets = duplicateValues(requestedTargets);
866
+ if (duplicateTargets.length > 0) {
867
+ errors.push(pipelineDiagnostic("TUBELESS_PLANNING_TARGET_SELECTION_DUPLICATE", "planning", "selection", `Pipeline ${definition.id} requested duplicate targets: ${duplicateTargets.join(", ")}`));
868
+ }
869
+ const unknownTargets = requestedTargets.filter((stepId) => !knownStepIds.has(stepId));
870
+ if (unknownTargets.length > 0) {
871
+ errors.push(pipelineDiagnostic("TUBELESS_PLANNING_TARGET_UNKNOWN", "planning", "selection", `Pipeline ${definition.id} requested unknown targets: ${unknownTargets.join(", ")}`));
872
+ }
873
+ const undeclaredTargets = requestedTargets.filter((stepId) => knownStepIds.has(stepId) && !declaredTargetIds.has(stepId));
874
+ if (undeclaredTargets.length > 0) {
875
+ errors.push(pipelineDiagnostic("TUBELESS_PLANNING_TARGET_UNDECLARED", "planning", "selection", `Pipeline ${definition.id} requested undeclared targets: ${undeclaredTargets.join(", ")}`));
876
+ }
877
+ const orderedSteps = errors.length === 0 ? topologicalSort(steps) : null;
878
+ if (errors.length > 0) {
879
+ return {
880
+ dryRun,
881
+ errors,
882
+ ok: false,
883
+ pipelineId: definition.id,
884
+ steps: [],
885
+ };
886
+ }
887
+ let selectionReasonsByStepId;
888
+ if (controls.stepIds !== undefined) {
889
+ const selectedIds = new Set(controls.stepIds);
890
+ selectionReasonsByStepId = new Map(steps.map((step) => [
891
+ step.id,
892
+ selectedIds.has(step.id)
893
+ ? [{ kind: "exact" }]
894
+ : [{ kind: "not-selected" }],
895
+ ]));
896
+ }
897
+ else if (controls.targets !== undefined) {
898
+ const stepsById = new Map(steps.map((step) => [step.id, step]));
899
+ const selectedTargets = controls.targets.map((target) => stepsById.get(target));
900
+ selectionReasonsByStepId = targetSelectionReasons(selectedTargets);
901
+ for (const step of steps) {
902
+ const reasons = selectionReasonsByStepId.get(step.id) ?? [];
903
+ const selected = reasons.some(selectsStep);
904
+ selectionReasonsByStepId.set(step.id, selected
905
+ ? reasons.filter(selectsStep)
906
+ : reasons.length > 0
907
+ ? reasons
908
+ : [{ kind: "outside-target-closure" }]);
909
+ }
910
+ }
911
+ else {
912
+ selectionReasonsByStepId = new Map(steps.map((step) => [step.id, [{ kind: "all" }]]));
913
+ }
914
+ const planSteps = [];
915
+ // Accumulated planned skip/complete reports so later steps see the same
916
+ // unmet-dependency chain as today's skipReason walk (not one empty map).
917
+ const plannedReportsByStepId = new Map();
918
+ for (const step of orderedSteps ?? steps) {
919
+ const selectionReasons = selectionReasonsByStepId.get(step.id);
920
+ const selected = selectionReasons.some(selectsStep);
921
+ const planned = stepToPlanStep(step, selected, undefined, selectionReasons);
922
+ const disposition = decideStepDisposition({
923
+ dryRun,
924
+ planned,
925
+ reportsByStepId: plannedReportsByStepId,
926
+ step,
927
+ });
928
+ const skipReason = disposition.kind === "skip" ? disposition.reason : undefined;
929
+ const planStep = stepToPlanStep(step, selected, skipReason, selectionReasons);
930
+ planSteps.push(planStep);
931
+ if (disposition.kind === "skip") {
932
+ const report = {
933
+ id: step.id,
934
+ name: step.name,
935
+ description: step.description,
936
+ finishedAtMs: 0,
937
+ reason: disposition.reason,
938
+ status: "skipped",
939
+ };
940
+ if (disposition.dependencyId)
941
+ report.dependencyId = disposition.dependencyId;
942
+ if (disposition.message)
943
+ report.message = disposition.message;
944
+ plannedReportsByStepId.set(step.id, report);
945
+ }
946
+ else {
947
+ // Synthetic complete so isRequiredDependencyMet treats will-run deps as met.
948
+ plannedReportsByStepId.set(step.id, {
949
+ attemptId: "",
950
+ id: step.id,
951
+ name: step.name,
952
+ description: step.description,
953
+ finishedAtMs: 0,
954
+ startedAtMs: 0,
955
+ status: "complete",
956
+ });
957
+ }
958
+ }
959
+ return {
960
+ dryRun,
961
+ errors,
962
+ ok: errors.length === 0,
963
+ pipelineId: definition.id,
964
+ steps: planSteps,
965
+ };
966
+ }
967
+ function planStepById(plan) {
968
+ return new Map(plan.steps.map((step) => [step.id, step]));
969
+ }
970
+ export function definePipeline(definition) {
971
+ const definitionErrors = validatePipelineDefinition(definition);
972
+ if (definitionErrors.length > 0) {
973
+ throw new PipelineDefinitionError(definition.id, definitionErrors);
974
+ }
975
+ // SAFETY: each step id is a string key of `TSteps`, so the frozen id list is
976
+ // exactly the declared `TStepId` union.
977
+ const stepIds = Object.freeze(definition.steps.map((step) => step.id));
978
+ // SAFETY: each target id is a string key of `TTargets`, so the frozen list is
979
+ // exactly the declared `TTargetId` union.
980
+ const targetIds = Object.freeze((definition.targets ?? []).map((step) => step.id));
981
+ const optionsSchema = definition.steps[0]?.[STEP_OPTIONS_SCHEMA];
982
+ // SAFETY: every step in `definition.steps` is an `AnyStep<TOptions>`; the
983
+ // cast only restores the generic type parameter that the tuple lost.
984
+ const orderedDefinitionSteps = topologicalSort(definition.steps);
985
+ function plan(controls = {}) {
986
+ return buildPipelinePlan(definition, controls);
987
+ }
988
+ function toMermaid(options = {}) {
989
+ return renderPipelineMermaid(orderedDefinitionSteps, options);
990
+ }
991
+ async function run(options, context = defaultPipelineContext()) {
992
+ const { controls, domainOptions } = splitPipelineRunOptions(options);
993
+ // SAFETY: `domainOptions` is the user-supplied options object; if a schema
994
+ // is present it is re-validated below before assignment to `pipelineOptions`.
995
+ let pipelineOptions = domainOptions;
996
+ const runtime = resolvePipelineRuntime(context);
997
+ const startedAt = runtime.now();
998
+ const runId = runtime.runId ?? createRunId(definition.id);
999
+ const parentRunId = runtime.parentRunId;
1000
+ const identity = { runId };
1001
+ if (parentRunId)
1002
+ identity.parentRunId = parentRunId;
1003
+ const dryRun = controls.dryRun === true;
1004
+ const trace = createPipelineTraceEmitter(definition.id, runtime.tracing, basePipelineLogger(runtime.log), {
1005
+ ...identity,
1006
+ itemKey: runtime.tracing?.itemKey,
1007
+ }, runtime.now);
1008
+ const lifecycle = createPipelineLifecycleObserver(definition.id, runtime, trace);
1009
+ const tracedLogger = (stepId, attemptId) => {
1010
+ if (!trace)
1011
+ return runtime.log;
1012
+ const base = basePipelineLogger(runtime.log);
1013
+ const log = {
1014
+ error: (message, ...params) => {
1015
+ lifecycle.log("error", message, params, stepId, attemptId);
1016
+ base.error(message, ...params);
1017
+ },
1018
+ log: (message, ...params) => {
1019
+ lifecycle.log("log", message, params, stepId, attemptId);
1020
+ base.log(message, ...params);
1021
+ },
1022
+ warn: (message, ...params) => {
1023
+ lifecycle.log("warn", message, params, stepId, attemptId);
1024
+ base.warn(message, ...params);
1025
+ },
1026
+ };
1027
+ log[PIPELINE_LOGGER_BASE] = base;
1028
+ return log;
1029
+ };
1030
+ let nextAttemptSequence = 0;
1031
+ const beginAttempt = (stepId, startedAtMs = runtime.now()) => ({
1032
+ attemptId: `${runId}:attempt:${(nextAttemptSequence += 1).toString(36)}`,
1033
+ startedAtMs,
1034
+ stepId,
1035
+ });
1036
+ const currentStepStatuses = new Map();
1037
+ const publishStepStatus = (event, traceStatus = true) => {
1038
+ const previous = currentStepStatuses.get(event.step.id);
1039
+ const valid = (previous === undefined && event.status === "planned") ||
1040
+ (previous === "planned" &&
1041
+ (event.status === "running" ||
1042
+ event.status === "skipped" ||
1043
+ event.status === "cancelled")) ||
1044
+ (previous === "running" &&
1045
+ (event.status === "running" ||
1046
+ event.status === "complete" ||
1047
+ event.status === "skipped" ||
1048
+ event.status === "cancelled" ||
1049
+ event.status === "failed"));
1050
+ if (!valid) {
1051
+ throw new Error(`Invalid step status transition for ${event.step.id}: ${previous ?? "unobserved"} -> ${event.status}`);
1052
+ }
1053
+ currentStepStatuses.set(event.step.id, event.status);
1054
+ lifecycle.stepStatus(event, traceStatus);
1055
+ };
1056
+ const publishStepReport = (step, report) => {
1057
+ switch (report.status) {
1058
+ case "cancelled":
1059
+ case "failed":
1060
+ case "skipped":
1061
+ case "complete":
1062
+ publishStepStatus({ ...report, pipelineId: definition.id, step });
1063
+ }
1064
+ };
1065
+ const runPlan = plan(controls);
1066
+ const plannedSteps = planStepById(runPlan);
1067
+ lifecycle.pipelineStart(runPlan, targetIds);
1068
+ if (!runPlan.ok) {
1069
+ const result = errorRunResult(runtime, definition.id, dryRun, startedAt, runPlan.errors, identity);
1070
+ lifecycle.pipelineComplete(result);
1071
+ await lifecycle.flush();
1072
+ return result;
1073
+ }
1074
+ if (optionsSchema) {
1075
+ try {
1076
+ const validated = await validateStandardSchema(optionsSchema, domainOptions, `Pipeline ${definition.id} options`);
1077
+ if (typeof validated !== "object" || validated === null || Array.isArray(validated)) {
1078
+ throw new PipelineBoundaryValidationError(`Pipeline ${definition.id} options schema returned a non-object value`, [{ message: "Expected the validated options value to be an object" }]);
1079
+ }
1080
+ // SAFETY: the schema validated the value against `TOptions` and the
1081
+ // object-shape check above passed, so the value is a `TOptions`.
1082
+ pipelineOptions = validated;
1083
+ }
1084
+ catch (error) {
1085
+ const pipelineError = toPipelineError(error, {
1086
+ code: "TUBELESS_OPTIONS_VALIDATION_FAILED",
1087
+ kind: "validation",
1088
+ phase: "execution",
1089
+ });
1090
+ const result = errorRunResult(runtime, definition.id, dryRun, startedAt, [pipelineError], identity);
1091
+ lifecycle.pipelineComplete(result);
1092
+ await lifecycle.flush();
1093
+ return result;
1094
+ }
1095
+ }
1096
+ for (const step of runPlan.steps) {
1097
+ publishStepStatus({ pipelineId: definition.id, status: "planned", step });
1098
+ }
1099
+ const outputs = new Map();
1100
+ const reports = [];
1101
+ const errors = [];
1102
+ const reportsByStepId = new Map();
1103
+ let completed = true;
1104
+ const executionContext = {
1105
+ ...runtime,
1106
+ dryRun,
1107
+ log: tracedLogger(),
1108
+ options: pipelineOptions,
1109
+ runId,
1110
+ trace: trace?.context,
1111
+ };
1112
+ if (parentRunId)
1113
+ executionContext.parentRunId = parentRunId;
1114
+ const stepsById = new Map(definition.steps.map((step) => [step.id, step]));
1115
+ // `runPlan.steps` is already topologically ordered. Rehydrate from the original step
1116
+ // objects so callers still get the typed `run` functions rather than plan metadata.
1117
+ // SAFETY: every planned step id originates from `definition.steps`, so the
1118
+ // map lookup always finds the matching `AnyStep<TOptions>`.
1119
+ const orderedSteps = runPlan.steps.map((step) => stepsById.get(step.id));
1120
+ const recordRemainingStates = (fromIndex, failedStepId, cancellationError) => {
1121
+ const stoppedAfter = {
1122
+ stepId: failedStepId ?? "",
1123
+ cancelled: cancellationError !== undefined,
1124
+ };
1125
+ for (let index = fromIndex; index < orderedSteps.length; index++) {
1126
+ const step = orderedSteps[index];
1127
+ const plannedStep = plannedSteps.get(step.id) ?? stepToPlanStep(step, true);
1128
+ const disposition = decideStepDisposition({
1129
+ dryRun,
1130
+ planned: plannedStep,
1131
+ reportsByStepId,
1132
+ step,
1133
+ stoppedAfter,
1134
+ });
1135
+ if (disposition.kind === "cancelled" && cancellationError) {
1136
+ const report = {
1137
+ id: step.id,
1138
+ name: step.name,
1139
+ description: step.description,
1140
+ error: { ...cancellationError, stepId: step.id },
1141
+ finishedAtMs: runtime.now(),
1142
+ status: "cancelled",
1143
+ };
1144
+ reports.push(report);
1145
+ reportsByStepId.set(step.id, report);
1146
+ publishStepReport(plannedStep, report);
1147
+ continue;
1148
+ }
1149
+ const skip = disposition.kind === "skip"
1150
+ ? disposition
1151
+ : {
1152
+ reason: "fail-fast",
1153
+ dependencyId: failedStepId,
1154
+ message: failedStepId
1155
+ ? `Not run because fail-fast stopped after ${failedStepId} failed.`
1156
+ : "Not run because the pipeline was aborted before this step started.",
1157
+ };
1158
+ const report = recordSkip(step, step.id, reports, reportsByStepId, skip.reason, skip.message, skip.dependencyId, undefined, undefined, runtime.now());
1159
+ publishStepReport(plannedStep, report);
1160
+ }
1161
+ };
1162
+ for (const [stepIndex, step] of orderedSteps.entries()) {
1163
+ const stepId = step.id;
1164
+ const plannedStep = plannedSteps.get(stepId) ?? stepToPlanStep(step, true);
1165
+ try {
1166
+ throwIfAborted(runtime);
1167
+ }
1168
+ catch (error) {
1169
+ const pipelineError = toPipelineError(error, {
1170
+ code: "TUBELESS_RUN_CANCELLED",
1171
+ kind: "cancellation",
1172
+ phase: "execution",
1173
+ stepId,
1174
+ });
1175
+ errors.push(pipelineError);
1176
+ completed = false;
1177
+ recordRemainingStates(stepIndex, undefined, pipelineError);
1178
+ break;
1179
+ }
1180
+ const disposition = decideStepDisposition({
1181
+ dryRun,
1182
+ planned: plannedStep,
1183
+ reportsByStepId,
1184
+ step,
1185
+ });
1186
+ if (disposition.kind === "skip") {
1187
+ const report = recordSkip(step, stepId, reports, reportsByStepId, disposition.reason, disposition.message, disposition.dependencyId, undefined, undefined, runtime.now());
1188
+ publishStepReport(plannedStep, report);
1189
+ continue;
1190
+ }
1191
+ const inputs = {};
1192
+ for (const dep of step.dependsOn ?? []) {
1193
+ inputs[dep.id] = outputs.get(dep.id);
1194
+ }
1195
+ for (const dep of step.optionalDependsOn ?? []) {
1196
+ if (outputs.has(dep.id)) {
1197
+ inputs[dep.id] = outputs.get(dep.id);
1198
+ }
1199
+ }
1200
+ if (typeof step.skip === "function") {
1201
+ const skipDecision = normalizeStepSkipDecision(await step.skip(inputs, { ...executionContext, log: tracedLogger(stepId) }));
1202
+ if (skipDecision) {
1203
+ let skippedOutput = skipDecision.value;
1204
+ let policyAttemptId;
1205
+ let policyStartedAtMs;
1206
+ let policyFinishedAtMs;
1207
+ if (step.outputSchema) {
1208
+ const validationStartedAt = runtime.now();
1209
+ const attempt = beginAttempt(stepId, validationStartedAt);
1210
+ policyAttemptId = attempt.attemptId;
1211
+ policyStartedAtMs = attempt.startedAtMs;
1212
+ publishStepStatus({
1213
+ attemptId: attempt.attemptId,
1214
+ pipelineId: definition.id,
1215
+ status: "running",
1216
+ step: plannedStep,
1217
+ });
1218
+ try {
1219
+ skippedOutput = await validateStandardSchema(step.outputSchema, skippedOutput, `Pipeline ${definition.id} step ${stepId} output`);
1220
+ }
1221
+ catch (error) {
1222
+ const pipelineError = toPipelineError(error, {
1223
+ code: "TUBELESS_STEP_OUTPUT_VALIDATION_FAILED",
1224
+ kind: "validation",
1225
+ phase: "execution",
1226
+ stepId,
1227
+ });
1228
+ errors.push(pipelineError);
1229
+ const finishedAtMs = runtime.now();
1230
+ const report = {
1231
+ attemptId: attempt.attemptId,
1232
+ id: stepId,
1233
+ name: step.name,
1234
+ description: step.description,
1235
+ error: pipelineError,
1236
+ finishedAtMs,
1237
+ startedAtMs: attempt.startedAtMs,
1238
+ status: "failed",
1239
+ };
1240
+ reports.push(report);
1241
+ reportsByStepId.set(stepId, report);
1242
+ publishStepReport(plannedStep, report);
1243
+ completed = false;
1244
+ if (!controls.continueOnError) {
1245
+ recordRemainingStates(stepIndex + 1, stepId);
1246
+ break;
1247
+ }
1248
+ continue;
1249
+ }
1250
+ policyFinishedAtMs = runtime.now();
1251
+ }
1252
+ // Always publish an output slot so optionalDependsOn can observe the skip.
1253
+ outputs.set(stepId, skippedOutput);
1254
+ const report = recordSkip(step, stepId, reports, reportsByStepId, "policy", skipDecision.reason, undefined, policyAttemptId, policyStartedAtMs, policyFinishedAtMs ?? runtime.now());
1255
+ publishStepReport(plannedStep, report);
1256
+ continue;
1257
+ }
1258
+ }
1259
+ const stepStartedAt = runtime.now();
1260
+ const attempt = beginAttempt(stepId, stepStartedAt);
1261
+ publishStepStatus({
1262
+ attemptId: attempt.attemptId,
1263
+ pipelineId: definition.id,
1264
+ status: "running",
1265
+ step: plannedStep,
1266
+ });
1267
+ try {
1268
+ let acceptsProgress = true;
1269
+ const publishProgress = (progress) => {
1270
+ if (!acceptsProgress)
1271
+ return;
1272
+ publishStepStatus({
1273
+ attemptId: attempt.attemptId,
1274
+ pipelineId: definition.id,
1275
+ progress,
1276
+ status: "running",
1277
+ step: plannedStep,
1278
+ });
1279
+ };
1280
+ const stepContext = {
1281
+ ...executionContext,
1282
+ attemptId: attempt.attemptId,
1283
+ log: tracedLogger(stepId, attempt.attemptId),
1284
+ reportAttempt: (attempt, attributes) => {
1285
+ lifecycle.reportAttempt(stepId, attempt, attributes, stepContext.attemptId);
1286
+ },
1287
+ reportProgress: (progress) => {
1288
+ publishProgress(progress);
1289
+ },
1290
+ };
1291
+ let output;
1292
+ try {
1293
+ const runStep = dryRun && typeof step.dryRun === "function" ? step.dryRun : step.run;
1294
+ output = await runStep(inputs, stepContext);
1295
+ if (step.outputSchema) {
1296
+ output = await validateStandardSchema(step.outputSchema, output, `Pipeline ${definition.id} step ${stepId} output`);
1297
+ }
1298
+ }
1299
+ finally {
1300
+ acceptsProgress = false;
1301
+ }
1302
+ outputs.set(stepId, output);
1303
+ const finishedAtMs = runtime.now();
1304
+ const report = {
1305
+ attemptId: attempt.attemptId,
1306
+ id: stepId,
1307
+ name: step.name,
1308
+ description: step.description,
1309
+ finishedAtMs,
1310
+ startedAtMs: attempt.startedAtMs,
1311
+ status: "complete",
1312
+ };
1313
+ reports.push(report);
1314
+ reportsByStepId.set(stepId, report);
1315
+ publishStepReport(plannedStep, report);
1316
+ }
1317
+ catch (error) {
1318
+ const cancelled = isPipelineCancellation(error, runtime);
1319
+ const childFailure = error instanceof PipelineExecutionError || error instanceof PipelineChildError;
1320
+ const validationFailure = error instanceof PipelineBoundaryValidationError;
1321
+ const pipelineError = toPipelineError(error, {
1322
+ code: cancelled
1323
+ ? "TUBELESS_RUN_CANCELLED"
1324
+ : validationFailure
1325
+ ? "TUBELESS_STEP_OUTPUT_VALIDATION_FAILED"
1326
+ : childFailure
1327
+ ? "TUBELESS_CHILD_FAILED"
1328
+ : "TUBELESS_STEP_FAILED",
1329
+ kind: cancelled
1330
+ ? "cancellation"
1331
+ : validationFailure
1332
+ ? "validation"
1333
+ : childFailure
1334
+ ? "child"
1335
+ : "step",
1336
+ phase: "execution",
1337
+ stepId,
1338
+ });
1339
+ errors.push(pipelineError);
1340
+ const finishedAtMs = runtime.now();
1341
+ const report = cancelled
1342
+ ? {
1343
+ attemptId: attempt.attemptId,
1344
+ id: stepId,
1345
+ name: step.name,
1346
+ description: step.description,
1347
+ error: pipelineError,
1348
+ finishedAtMs,
1349
+ startedAtMs: attempt.startedAtMs,
1350
+ status: "cancelled",
1351
+ }
1352
+ : {
1353
+ attemptId: attempt.attemptId,
1354
+ id: stepId,
1355
+ name: step.name,
1356
+ description: step.description,
1357
+ error: pipelineError,
1358
+ finishedAtMs,
1359
+ startedAtMs: attempt.startedAtMs,
1360
+ status: "failed",
1361
+ };
1362
+ reports.push(report);
1363
+ reportsByStepId.set(stepId, report);
1364
+ publishStepReport(plannedStep, report);
1365
+ completed = false;
1366
+ if (!controls.continueOnError) {
1367
+ recordRemainingStates(stepIndex + 1, stepId, report.status === "cancelled" ? pipelineError : undefined);
1368
+ break;
1369
+ }
1370
+ }
1371
+ }
1372
+ let value;
1373
+ let finalized = false;
1374
+ if (completed || controls.continueOnError) {
1375
+ const finalizeStartedAt = runtime.now();
1376
+ lifecycle.finalizeStart();
1377
+ try {
1378
+ throwIfAborted(runtime);
1379
+ // SAFETY: `outputs` maps step ids to their produced values, which is
1380
+ // exactly the shape `Partial<PipelineOutputs<TSteps>>` describes.
1381
+ const finalOutputs = Object.fromEntries(outputs);
1382
+ const finalizedValue = await definition.finalize(finalOutputs, {
1383
+ ...executionContext,
1384
+ log: tracedLogger(PIPELINE_FINALIZE_STEP_ID),
1385
+ });
1386
+ // SAFETY: with a result schema the value is validated against
1387
+ // `TPipelineResult`; without one the finalizer's declared return type
1388
+ // is `TPipelineResult`, so the cast only restores that type.
1389
+ value = definition.resultSchema
1390
+ ? (await validateStandardSchema(definition.resultSchema, finalizedValue, `Pipeline ${definition.id} final result`))
1391
+ : finalizedValue;
1392
+ finalized = true;
1393
+ lifecycle.finalizeComplete(runtime.now() - finalizeStartedAt, value);
1394
+ }
1395
+ catch (error) {
1396
+ const cancelled = isPipelineCancellation(error, runtime);
1397
+ const validationFailure = error instanceof PipelineBoundaryValidationError;
1398
+ const pipelineError = toPipelineError(error, {
1399
+ code: cancelled
1400
+ ? "TUBELESS_FINALIZATION_CANCELLED"
1401
+ : validationFailure
1402
+ ? "TUBELESS_FINAL_RESULT_VALIDATION_FAILED"
1403
+ : "TUBELESS_FINALIZATION_FAILED",
1404
+ kind: cancelled ? "cancellation" : validationFailure ? "validation" : "finalization",
1405
+ phase: "finalization",
1406
+ stepId: PIPELINE_FINALIZE_STEP_ID,
1407
+ });
1408
+ errors.push(pipelineError);
1409
+ lifecycle.finalizeError(pipelineError, runtime.now() - finalizeStartedAt);
1410
+ completed = false;
1411
+ }
1412
+ }
1413
+ const finishedAtMs = runtime.now();
1414
+ const result = {
1415
+ pipelineId: definition.id,
1416
+ dryRun,
1417
+ errors,
1418
+ finalized,
1419
+ finishedAtMs,
1420
+ runId,
1421
+ startedAtMs: startedAt,
1422
+ status: terminalRunStatus(errors),
1423
+ steps: reports,
1424
+ value,
1425
+ version: RUN_MODEL_VERSION,
1426
+ };
1427
+ if (parentRunId)
1428
+ result.parentRunId = parentRunId;
1429
+ lifecycle.pipelineComplete(result);
1430
+ await lifecycle.flush();
1431
+ return result;
1432
+ }
1433
+ async function runOrThrow(options, context = defaultPipelineContext()) {
1434
+ const result = await run(options, context);
1435
+ if (result.status !== "completed") {
1436
+ throw new PipelineExecutionError(result);
1437
+ }
1438
+ // SAFETY: a completed run always carries its `TPipelineResult` value.
1439
+ return result.value;
1440
+ }
1441
+ return { id: definition.id, stepIds, targetIds, plan, run, runOrThrow, toMermaid };
1442
+ }
1443
+ export { formatMappedChildProgressMessage, mappedChildProgressDetails, mappedChildProgressUnits, toMappedChildStepProgress, } from "./mapped-child-progress.js";
1444
+ //# sourceMappingURL=pipeline.js.map