runsheet 0.5.0 → 0.6.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.
package/dist/index.d.ts CHANGED
@@ -1,6 +1,26 @@
1
- import { ParserSchema, Result, Success, Failure } from 'composable-functions';
2
- export { Failure, Result, Success } from 'composable-functions';
3
-
1
+ /**
2
+ * A schema that can parse/validate unknown data.
3
+ *
4
+ * This is the structural interface that Zod schemas (and other schema
5
+ * libraries) satisfy. runsheet does not depend on any specific schema
6
+ * library — any object with a `safeParse` method works.
7
+ *
8
+ * @typeParam T - The validated output type.
9
+ */
10
+ type StepSchema<T> = {
11
+ safeParse(data: unknown): {
12
+ success: true;
13
+ data: T;
14
+ } | {
15
+ success: false;
16
+ error: {
17
+ issues: readonly {
18
+ path: readonly (string | number)[];
19
+ message: string;
20
+ }[];
21
+ };
22
+ };
23
+ };
4
24
  /**
5
25
  * The type-erased context shape used at runtime by the pipeline engine.
6
26
  *
@@ -34,18 +54,18 @@ type Step = {
34
54
  /** Unique name identifying this step in metadata and rollback reports. */
35
55
  readonly name: string;
36
56
  /** Optional schema that validates the accumulated context before `run`. */
37
- readonly requires: ParserSchema<StepContext> | undefined;
57
+ readonly requires: StepSchema<StepContext> | undefined;
38
58
  /** Optional schema that validates the step's output after `run`. */
39
- readonly provides: ParserSchema<StepOutput> | undefined;
59
+ readonly provides: StepSchema<StepOutput> | undefined;
40
60
  /**
41
61
  * Execute the step. Receives the accumulated context and returns a
42
- * `Result` — either `{ success: true, data }` or
43
- * `{ success: false, errors }`.
62
+ * {@link StepResult} — either a success with `data` or a failure with
63
+ * `error`, `failedStep`, and `rollback`.
44
64
  *
45
65
  * Step authors never call this directly; the pipeline engine calls it
46
- * after validating `requires` and wrapping with middleware.
66
+ * after wrapping with middleware.
47
67
  */
48
- readonly run: (ctx: Readonly<StepContext>) => Promise<Result<StepOutput>>;
68
+ readonly run: (ctx: Readonly<StepContext>) => Promise<StepResult<StepOutput>>;
49
69
  /**
50
70
  * Optional rollback handler, called when a later step fails.
51
71
  *
@@ -62,6 +82,10 @@ type Step = {
62
82
  * Phantom type brands for compile-time tracking of step I/O types.
63
83
  * These symbols never exist at runtime — they only guide TypeScript's
64
84
  * type checker through the builder's progressive type narrowing.
85
+ *
86
+ * `RequiresBrand` uses a function type for contravariance: a step that
87
+ * requires `StepContext` (anything) is usable where a narrower context
88
+ * is available. `ProvidesBrand` is covariant (a plain value brand).
65
89
  */
66
90
  declare const RequiresBrand: unique symbol;
67
91
  declare const ProvidesBrand: unique symbol;
@@ -76,13 +100,17 @@ declare const ProvidesBrand: unique symbol;
76
100
  * schemas or generics. When assigned to `Step` (e.g., in a pipeline's
77
101
  * step array), the intersection collapses to the erased signatures.
78
102
  *
103
+ * The typed properties appear BEFORE the `Step` intersection so that
104
+ * TypeScript's overload resolution picks the concrete signatures first
105
+ * when calling `run()` directly on a `TypedStep`.
106
+ *
79
107
  * @typeParam Requires - The context shape this step reads from.
80
108
  * @typeParam Provides - The output shape this step produces.
81
109
  *
82
110
  * @example
83
111
  * ```ts
84
112
  * // Hover over `step.run` to see:
85
- * // (ctx: Readonly<{ amount: number }>) => Promise<Result<{ chargeId: string }>>
113
+ * // (ctx: Readonly<{ amount: number }>) => Promise<StepResult<{ chargeId: string }>>
86
114
  * const step = defineStep({
87
115
  * name: 'charge',
88
116
  * requires: z.object({ amount: z.number() }),
@@ -91,15 +119,15 @@ declare const ProvidesBrand: unique symbol;
91
119
  * });
92
120
  * ```
93
121
  */
94
- type TypedStep<Requires extends StepContext = StepContext, Provides extends StepContext = StepContext> = Step & {
95
- readonly [RequiresBrand]: Requires;
122
+ type TypedStep<Requires extends StepContext = StepContext, Provides extends StepContext = StepContext> = {
123
+ readonly [RequiresBrand]: (ctx: Requires) => void;
96
124
  readonly [ProvidesBrand]: Provides;
97
125
  /** Optional schema that validates the accumulated context before `run`. */
98
- readonly requires: ParserSchema<Requires> | undefined;
126
+ readonly requires: StepSchema<Requires> | undefined;
99
127
  /** Optional schema that validates the step's output after `run`. */
100
- readonly provides: ParserSchema<Provides> | undefined;
128
+ readonly provides: StepSchema<Provides> | undefined;
101
129
  /** Execute the step with concrete input/output types. */
102
- readonly run: (ctx: Readonly<Requires>) => Promise<Result<Provides>>;
130
+ readonly run: (ctx: Readonly<Requires>) => Promise<StepResult<Provides>>;
103
131
  /**
104
132
  * Optional rollback handler, called when a later step fails.
105
133
  *
@@ -107,13 +135,30 @@ type TypedStep<Requires extends StepContext = StepContext, Provides extends Step
107
135
  * @param output - The frozen output this step produced.
108
136
  */
109
137
  readonly rollback: ((ctx: Readonly<Requires>, output: Readonly<Provides>) => Promise<void>) | undefined;
110
- };
138
+ } & Step;
111
139
  /** Convert a union type to an intersection type. */
112
140
  type UnionToIntersection<U> = [U] extends [never] ? unknown : (U extends unknown ? (x: U) => void : never) extends (x: infer I) => void ? I : never;
113
- /** Extract the Requires type from a step. Returns `StepContext` for untyped (erased) steps. */
114
- type ExtractRequires<T extends Step> = T extends TypedStep<infer R, StepContext> ? R : StepContext;
115
- /** Extract the Provides type from a step. Returns `object` for untyped (erased) steps. */
116
- type ExtractProvides<T extends Step> = T extends TypedStep<StepContext, infer P> ? P : object;
141
+ /**
142
+ * Extract the Requires type from a step via its phantom brand.
143
+ * Returns `StepContext` for untyped (erased) steps.
144
+ *
145
+ * Matches the contravariant function brand directly — avoids full
146
+ * structural matching on `TypedStep` which would fail due to
147
+ * `run` parameter contravariance in conditional types.
148
+ */
149
+ type ExtractRequires<T extends Step> = T extends {
150
+ readonly [RequiresBrand]: (ctx: infer R) => void;
151
+ } ? R : StepContext;
152
+ /**
153
+ * Extract the Provides type from a step via its phantom brand.
154
+ * Returns `object` for untyped (erased) steps.
155
+ *
156
+ * Matches the covariant value brand directly — avoids full structural
157
+ * matching which would fail for steps with non-trivial Requires types.
158
+ */
159
+ type ExtractProvides<T extends Step> = T extends {
160
+ readonly [ProvidesBrand]: infer P;
161
+ } ? P : object;
117
162
  /**
118
163
  * Retry policy for a step's `run` function.
119
164
  *
@@ -160,23 +205,23 @@ type StepConfig<Requires extends StepContext, Provides extends StepContext> = {
160
205
  /**
161
206
  * Optional Zod (or Standard Schema compatible) schema that validates
162
207
  * the accumulated context before `run` is called. When provided, a
163
- * schema validation failure produces a `Result` error — the step's
208
+ * schema validation failure produces a `StepResult` error — the step's
164
209
  * `run` function is never invoked.
165
210
  */
166
- requires?: ParserSchema<Requires>;
211
+ requires?: StepSchema<Requires>;
167
212
  /**
168
213
  * Optional Zod (or Standard Schema compatible) schema that validates
169
214
  * the step's output after `run` returns. When provided, a schema
170
- * validation failure produces a `Result` error even though `run`
215
+ * validation failure produces a `StepResult` error even though `run`
171
216
  * succeeded.
172
217
  */
173
- provides?: ParserSchema<Provides>;
218
+ provides?: StepSchema<Provides>;
174
219
  /**
175
220
  * The step implementation. Receives the accumulated context (frozen)
176
221
  * and returns the step's output. Can be sync or async.
177
222
  *
178
223
  * To signal failure, throw an error. The pipeline catches it and
179
- * produces a `Result` failure — do not return failure objects.
224
+ * produces a `StepResult` failure — do not return failure objects.
180
225
  *
181
226
  * @param ctx - The frozen accumulated context up to this point.
182
227
  * @returns The step's output, which is merged into the accumulated context.
@@ -219,7 +264,7 @@ type RollbackFailure = {
219
264
  readonly error: Error;
220
265
  };
221
266
  /**
222
- * Summary of rollback execution after a pipeline failure.
267
+ * Summary of rollback execution after a step or pipeline failure.
223
268
  *
224
269
  * Rollback is best-effort: every completed step's rollback handler is
225
270
  * attempted in reverse order, regardless of whether earlier handlers
@@ -232,69 +277,153 @@ type RollbackReport = {
232
277
  readonly failed: readonly RollbackFailure[];
233
278
  };
234
279
  /**
235
- * Metadata about a pipeline execution, present on both success and failure
280
+ * Metadata about a step execution, present on both success and failure
236
281
  * results. Useful for logging, debugging, and observability.
237
282
  */
238
- type PipelineExecutionMeta = {
239
- /** The pipeline's name as passed to `buildPipeline` or `createPipeline`. */
240
- readonly pipeline: string;
241
- /** The original arguments passed to `pipeline.run()`. */
283
+ type StepMeta = {
284
+ /** The step's name (or pipeline name for pipeline-steps). */
285
+ readonly name: string;
286
+ /** The original arguments/context passed to `step.run()`. */
242
287
  readonly args: Readonly<StepContext>;
288
+ };
289
+ /**
290
+ * Extended metadata for orchestrator results (pipelines, parallel,
291
+ * choice).
292
+ *
293
+ * Includes orchestration detail — which steps ran — on top of the
294
+ * base {@link StepMeta}. Present on results from `pipeline()`,
295
+ * `parallel()`, and `choice()`.
296
+ */
297
+ type AggregateMeta = StepMeta & {
243
298
  /** Names of steps that executed successfully, in order. */
244
299
  readonly stepsExecuted: readonly string[];
245
- /** Names of conditional steps that were skipped (predicate returned false). */
246
- readonly stepsSkipped: readonly string[];
247
300
  };
248
301
  /**
249
- * A successful pipeline result.
302
+ * A successful step result.
250
303
  *
251
- * Extends composable-functions' `Success<T>` with pipeline execution
252
- * metadata. The `data` property contains the fully accumulated context
253
- * (initial args merged with all step outputs).
304
+ * The `data` property contains the step's output (or the fully
305
+ * accumulated context for pipeline-steps).
254
306
  *
255
- * @typeParam T - The accumulated context type.
307
+ * @typeParam T - The output type.
256
308
  */
257
- type PipelineSuccess<T> = Success<T> & {
258
- /** Pipeline execution metadata. */
259
- readonly meta: PipelineExecutionMeta;
309
+ type StepSuccess<T> = {
310
+ readonly success: true;
311
+ /** The step's output data. */
312
+ readonly data: T;
313
+ /** Step execution metadata. */
314
+ readonly meta: StepMeta;
260
315
  };
261
316
  /**
262
- * A failed pipeline result.
263
- *
264
- * Extends composable-functions' `Failure` with the name of the step that
265
- * failed, a rollback report, and pipeline execution metadata.
317
+ * A failed step result.
266
318
  *
267
- * On failure, rollback handlers for all previously completed steps are
268
- * executed in reverse order before this result is returned.
319
+ * Contains the error that caused the failure, the name of the step
320
+ * that failed, and a rollback report.
269
321
  */
270
- type PipelineFailure = Failure & {
271
- /** Pipeline execution metadata. */
272
- readonly meta: PipelineExecutionMeta;
322
+ type StepFailure = {
323
+ readonly success: false;
324
+ /** The error that caused the failure. Use `AggregateError` when multiple errors occur. */
325
+ readonly error: Error;
326
+ /** Step execution metadata. */
327
+ readonly meta: StepMeta;
273
328
  /** Name of the step that failed. */
274
329
  readonly failedStep: string;
275
330
  /** Report of which rollback handlers succeeded and which threw. */
276
331
  readonly rollback: RollbackReport;
277
332
  };
278
333
  /**
279
- * The result of running a pipeline — either a success or a failure.
334
+ * The result of running a step — either a success or a failure.
280
335
  *
281
336
  * Use the `success` discriminant to narrow:
282
337
  *
283
338
  * ```ts
284
- * const result = await pipeline.run(args);
339
+ * const result = await step.run(ctx);
285
340
  * if (result.success) {
286
- * result.data; // fully typed accumulated context
341
+ * result.data; // the step's output
287
342
  * result.meta; // execution metadata
288
343
  * } else {
289
- * result.errors; // what went wrong
344
+ * result.error; // what went wrong
290
345
  * result.failedStep; // which step failed
291
346
  * result.rollback; // { completed: [...], failed: [...] }
292
347
  * }
293
348
  * ```
294
349
  *
295
- * @typeParam T - The accumulated context type on success.
350
+ * @typeParam T - The output type on success.
351
+ */
352
+ type StepResult<T> = StepSuccess<T> | StepFailure;
353
+ /**
354
+ * A successful orchestrator result.
355
+ *
356
+ * Identical to {@link StepSuccess} but with {@link AggregateMeta}
357
+ * instead of {@link StepMeta}, providing orchestration detail.
358
+ *
359
+ * @typeParam T - The accumulated output type.
360
+ */
361
+ type AggregateSuccess<T> = {
362
+ readonly success: true;
363
+ /** The accumulated output after all inner steps. */
364
+ readonly data: T;
365
+ /** Orchestrator execution metadata including step tracking. */
366
+ readonly meta: AggregateMeta;
367
+ };
368
+ /**
369
+ * A failed orchestrator result.
370
+ *
371
+ * Identical to {@link StepFailure} but with {@link AggregateMeta}
372
+ * instead of {@link StepMeta}, providing orchestration detail.
373
+ */
374
+ type AggregateFailure = {
375
+ readonly success: false;
376
+ /** The error that caused the failure. */
377
+ readonly error: Error;
378
+ /** Orchestrator execution metadata including step tracking. */
379
+ readonly meta: AggregateMeta;
380
+ /** Name of the step that failed. */
381
+ readonly failedStep: string;
382
+ /** Report of which rollback handlers succeeded and which threw. */
383
+ readonly rollback: RollbackReport;
384
+ };
385
+ /**
386
+ * The result of running an orchestrator — extends {@link StepResult}
387
+ * with richer metadata.
388
+ *
389
+ * `AggregateResult<T>` is assignable to `StepResult<T>`, so
390
+ * orchestrators (`pipeline`, `parallel`, `choice`) satisfy the `Step`
391
+ * interface while providing orchestration detail to callers.
392
+ *
393
+ * ```ts
394
+ * const checkout = pipeline({ name: 'checkout', steps: [...] });
395
+ * const result = await checkout.run({ orderId: '123' });
396
+ * if (result.success) {
397
+ * result.meta.stepsExecuted; // string[] — which steps ran
398
+ * } else {
399
+ * result.meta.stepsExecuted; // string[] — which steps ran
400
+ * result.failedStep; // which step failed
401
+ * result.rollback; // { completed, failed }
402
+ * }
403
+ * ```
404
+ *
405
+ * @typeParam T - The accumulated output type on success.
406
+ */
407
+ type AggregateResult<T> = AggregateSuccess<T> | AggregateFailure;
408
+ /**
409
+ * A step that orchestrates other steps and returns rich results.
410
+ *
411
+ * Extends {@link TypedStep} with a narrower `run()` that returns
412
+ * {@link AggregateResult} instead of {@link StepResult}. This is the
413
+ * type returned by `pipeline()`, `parallel()`, and `choice()`.
414
+ *
415
+ * The `run` property uses an explicit overloaded function type:
416
+ * the first overload returns `AggregateResult` (matched when calling
417
+ * directly), the second preserves the erased `Step.run` signature
418
+ * (for `Step` assignability when used in pipeline arrays).
419
+ *
420
+ * @typeParam Requires - The input type.
421
+ * @typeParam Provides - The accumulated output type.
296
422
  */
297
- type PipelineResult<T> = PipelineSuccess<T> | PipelineFailure;
423
+ type AggregateStep<Requires extends StepContext = StepContext, Provides extends StepContext = StepContext> = {
424
+ /** Execute the orchestrator and return an {@link AggregateResult}. */
425
+ readonly run: (ctx: Readonly<Requires>) => Promise<AggregateResult<Provides>>;
426
+ } & TypedStep<Requires, Provides>;
298
427
 
299
428
  /**
300
429
  * Define a pipeline step.
@@ -323,9 +452,8 @@ type PipelineResult<T> = PipelineSuccess<T> | PipelineFailure;
323
452
  *
324
453
  * **Invariants:**
325
454
  * - The returned step object is always frozen (immutable).
326
- * - The `run` function is wrapped with `composable()` from
327
- * composable-functions, which catches thrown errors and produces
328
- * `Result` values. Step authors should throw to signal failure.
455
+ * - The `run` function catches thrown errors and produces
456
+ * `StepResult` values. Step authors should throw to signal failure.
329
457
  * - This is the single type-erasure cast point in the library.
330
458
  *
331
459
  * @typeParam Requires - The context shape this step reads from.
@@ -338,15 +466,12 @@ declare function defineStep<Requires extends StepContext, Provides extends StepC
338
466
  /**
339
467
  * Error codes for errors produced by the runsheet library itself.
340
468
  *
341
- * Use these to distinguish library errors from application errors
342
- * in a pipeline's `errors` array:
469
+ * Use these to distinguish library errors from application errors:
343
470
  *
344
471
  * ```ts
345
472
  * if (!result.success) {
346
- * for (const error of result.errors) {
347
- * if (error instanceof RunsheetError) {
348
- * console.log(error.code); // 'REQUIRES_VALIDATION', etc.
349
- * }
473
+ * if (result.error instanceof RunsheetError) {
474
+ * console.log(result.error.code); // 'REQUIRES_VALIDATION', etc.
350
475
  * }
351
476
  * }
352
477
  * ```
@@ -357,8 +482,8 @@ type RunsheetErrorCode = 'REQUIRES_VALIDATION' | 'PROVIDES_VALIDATION' | 'ARGS_V
357
482
  *
358
483
  * Application errors (thrown by step `run` or `rollback` functions)
359
484
  * are never wrapped in `RunsheetError` — they pass through as-is.
360
- * If you see a `RunsheetError` in a result's `errors` array, the
361
- * library itself produced it.
485
+ * If you see a `RunsheetError` as `result.error`, the library itself
486
+ * produced it.
362
487
  *
363
488
  * Use `instanceof RunsheetError` to distinguish library errors from
364
489
  * application errors. Use `instanceof` on a subclass (e.g.,
@@ -421,7 +546,9 @@ declare class UnknownError extends RunsheetError {
421
546
  }
422
547
  /** One or more rollback handlers failed in a combinator. */
423
548
  declare class RollbackError extends RunsheetError {
424
- constructor(message: string);
549
+ /** The individual errors from each failed rollback handler. */
550
+ readonly causes: readonly Error[];
551
+ constructor(message: string, causes?: readonly Error[]);
425
552
  }
426
553
 
427
554
  /**
@@ -441,10 +568,9 @@ type StepInfo = {
441
568
  /**
442
569
  * A function that executes a step (or the next middleware in the chain).
443
570
  *
444
- * Receives the frozen accumulated context and returns a `Result` — either
445
- * `{ success: true, data }` or `{ success: false, errors }`.
571
+ * Receives the frozen accumulated context and returns a {@link StepResult}.
446
572
  */
447
- type StepExecutor = (ctx: Readonly<StepContext>) => Promise<Result<StepOutput>>;
573
+ type StepExecutor = (ctx: Readonly<StepContext>) => Promise<StepResult<StepOutput>>;
448
574
  /**
449
575
  * Middleware that wraps the entire step lifecycle, including schema
450
576
  * validation.
@@ -455,7 +581,7 @@ type StepExecutor = (ctx: Readonly<StepContext>) => Promise<Result<StepOutput>>;
455
581
  *
456
582
  * - **Observe**: read the context or result for logging/metrics.
457
583
  * - **Transform**: modify the result before returning it.
458
- * - **Short-circuit**: return a `Result` without calling `next`.
584
+ * - **Short-circuit**: return a `StepResult` without calling `next`.
459
585
  *
460
586
  * If a middleware throws, the pipeline catches it and treats it as a
461
587
  * step failure (triggering rollback for previously completed steps).
@@ -476,11 +602,58 @@ type StepExecutor = (ctx: Readonly<StepContext>) => Promise<Result<StepOutput>>;
476
602
  */
477
603
  type StepMiddleware = (step: StepInfo, next: StepExecutor) => StepExecutor;
478
604
 
605
+ /**
606
+ * A fluent pipeline builder that progressively narrows the accumulated
607
+ * context type as steps are added.
608
+ *
609
+ * Each method returns a new, frozen builder — builders are immutable.
610
+ *
611
+ * This means you can safely fork a builder to create variants:
612
+ *
613
+ * ```ts
614
+ * const base = pipeline({ name: 'order' }).step(validate);
615
+ * const withCharge = base.step(charge).build();
616
+ * const withoutCharge = base.build(); // unaffected by the fork
617
+ * ```
618
+ *
619
+ * @typeParam Args - The pipeline's initial input type.
620
+ * @typeParam Ctx - The accumulated context type so far (grows with each `.step()`).
621
+ */
622
+ type PipelineBuilder<Args extends StepContext, Ctx extends StepContext> = {
623
+ /**
624
+ * Add a step to the pipeline.
625
+ *
626
+ * The step's `Requires` type must be satisfied by the current `Ctx`
627
+ * (checked via phantom brands — a step that requires less than `Ctx`
628
+ * is always accepted). The returned builder's `Ctx` expands to
629
+ * include the step's `Provides`.
630
+ *
631
+ * @typeParam S - The step type being added.
632
+ * @param step - A {@link Step} (from `defineStep`, `when`, `pipeline`, etc.).
633
+ * @returns A new builder with the expanded context type.
634
+ */
635
+ readonly step: <S extends Step>(step: S & ([Ctx] extends [ExtractRequires<S>] ? unknown : never)) => PipelineBuilder<Args, Ctx & ExtractProvides<S>>;
636
+ /**
637
+ * Add middleware to the pipeline.
638
+ *
639
+ * Middleware is applied to every step. Multiple `.use()` calls
640
+ * accumulate — earlier middleware is outermost (executes first).
641
+ *
642
+ * @param middleware - One or more {@link StepMiddleware} functions.
643
+ * @returns A new builder with the middleware added.
644
+ */
645
+ readonly use: (...middleware: StepMiddleware[]) => PipelineBuilder<Args, Ctx>;
646
+ /**
647
+ * Build the pipeline. Returns an {@link AggregateStep} — pipelines
648
+ * are steps whose `run()` returns {@link AggregateResult}.
649
+ */
650
+ readonly build: () => AggregateStep<Args, Ctx>;
651
+ };
652
+
479
653
  /**
480
654
  * Internal configuration shape for the pipeline engine.
481
655
  *
482
- * Users typically don't construct this directly — use `buildPipeline()`
483
- * or `createPipeline()` instead.
656
+ * Users typically don't construct this directly — use `pipeline()`.
484
657
  */
485
658
  type PipelineConfig = {
486
659
  /** Pipeline name, used in execution metadata and error messages. */
@@ -490,7 +663,7 @@ type PipelineConfig = {
490
663
  /** Optional middleware applied to every step. First in array = outermost. */
491
664
  readonly middleware?: readonly StepMiddleware[];
492
665
  /** Optional schema that validates the pipeline's input arguments. */
493
- readonly argsSchema?: ParserSchema<StepContext>;
666
+ readonly argsSchema?: StepSchema<StepContext>;
494
667
  /**
495
668
  * When `true`, throws at build time if two or more steps provide the
496
669
  * same key. Only checks steps that have a `provides` schema with an
@@ -500,89 +673,57 @@ type PipelineConfig = {
500
673
  readonly strict?: boolean;
501
674
  };
502
675
  /**
503
- * A built pipeline, ready to execute.
504
- *
505
- * Call `run(args)` to execute the pipeline. The result is a
506
- * {@link PipelineResult} — either a success with the fully accumulated
507
- * context, or a failure with error details and a rollback report.
676
+ * Create a pipeline — either directly from steps, or via the fluent
677
+ * builder API.
508
678
  *
509
- * Pipeline objects are frozen (immutable) and can be called repeatedly.
679
+ * **With steps** returns an {@link AggregateStep} immediately:
510
680
  *
511
- * @typeParam Args - The input type accepted by `run()`.
512
- * @typeParam Ctx - The accumulated output type on success.
513
- */
514
- type Pipeline<Args extends StepContext, Ctx> = {
515
- /** The pipeline's name, as provided at build time. */
516
- readonly name: string;
517
- /**
518
- * Execute the pipeline.
519
- *
520
- * @param args - The initial arguments. Merged into the context before
521
- * the first step runs. Validated against `argsSchema` if one was
522
- * provided.
523
- * @returns A {@link PipelineResult} — discriminate on `success` to
524
- * access `data` (on success) or `errors`/`rollback` (on failure).
525
- */
526
- readonly run: (args: Args) => Promise<PipelineResult<Ctx>>;
527
- };
528
- /**
529
- * Build a pipeline from an array of steps.
530
- *
531
- * The result type is inferred from the steps — `pipeline.run()` returns
532
- * a {@link PipelineResult} whose `data` is the intersection of the
533
- * initial `Args` and all step output types.
534
- *
535
- * @example
536
681
  * ```ts
537
- * const pipeline = buildPipeline({
538
- * name: 'placeOrder',
682
+ * const checkout = pipeline({
683
+ * name: 'checkout',
539
684
  * steps: [validateOrder, chargePayment, sendConfirmation],
540
685
  * middleware: [logging, timing],
541
686
  * argsSchema: z.object({ orderId: z.string() }),
542
687
  * });
543
- *
544
- * const result = await pipeline.run({ orderId: '123' });
545
- * if (result.success) {
546
- * result.data.chargeId; // string — fully typed
547
- * }
548
688
  * ```
549
689
  *
550
- * **Execution semantics:**
551
- * - Steps execute sequentially in array order.
552
- * - Context is frozen (`Object.freeze`) at every step boundary.
553
- * - Conditional steps (wrapped with `when()`) are skipped when their
554
- * predicate returns false — no snapshot, no rollback entry.
555
- * - On step failure, rollback handlers for all previously completed
556
- * steps execute in reverse order (best-effort).
557
- * - Middleware wraps the full step lifecycle including schema validation.
690
+ * **Without steps** — returns a {@link PipelineBuilder} with
691
+ * progressive type narrowing:
558
692
  *
559
- * **Invariants:**
560
- * - The returned pipeline object is frozen (immutable).
561
- * - Errors thrown by steps, predicates, or middleware are caught and
562
- * returned as `PipelineFailure` — `run()` never throws.
563
- *
564
- * @typeParam Args - The pipeline's input type. Inferred from `argsSchema`
565
- * if provided, otherwise defaults to `StepContext`.
566
- * @typeParam S - The step types in the array. Inferred automatically —
567
- * do not specify manually.
568
- * @param config - Pipeline configuration.
569
- * @param config.name - Pipeline name, used in metadata and error messages.
570
- * @param config.steps - Steps to execute in order.
571
- * @param config.middleware - Optional middleware applied to every step.
572
- * First in array = outermost wrapper.
573
- * @param config.argsSchema - Optional schema that validates `args` before
574
- * any steps run. Validation failure produces a `PipelineFailure` with
575
- * `failedStep` set to the pipeline name.
576
- * @returns A frozen {@link Pipeline} whose `run()` method executes the
577
- * steps and returns a {@link PipelineResult}.
578
- */
579
- declare function buildPipeline<Args extends StepContext = StepContext, S extends Step = Step>(config: {
693
+ * ```ts
694
+ * const checkout = pipeline({ name: 'checkout' })
695
+ * .step(validateOrder)
696
+ * .step(chargePayment)
697
+ * .build();
698
+ *
699
+ * // With schema (runtime validation):
700
+ * pipeline({
701
+ * name: 'checkout',
702
+ * argsSchema: z.object({ orderId: z.string() }),
703
+ * }).step(validateOrder).build();
704
+ *
705
+ * // Type-only args (no runtime validation):
706
+ * pipeline<{ orderId: string }>({ name: 'checkout' })
707
+ * .step(validateOrder)
708
+ * .build();
709
+ * ```
710
+ *
711
+ * Pipelines ARE steps they can be used directly in another
712
+ * pipeline's steps array for composition.
713
+ */
714
+ declare function pipeline<Args extends StepContext = StepContext, S extends Step = Step>(config: {
580
715
  readonly name: string;
581
716
  readonly steps: readonly S[];
582
717
  readonly middleware?: readonly StepMiddleware[];
583
- readonly argsSchema?: ParserSchema<Args>;
718
+ readonly argsSchema?: StepSchema<Args>;
719
+ readonly strict?: boolean;
720
+ }): AggregateStep<Args, Args & UnionToIntersection<ExtractProvides<S>>>;
721
+ declare function pipeline<Args extends StepContext = StepContext>(config: {
722
+ readonly name: string;
723
+ readonly middleware?: readonly StepMiddleware[];
724
+ readonly argsSchema?: StepSchema<Args>;
584
725
  readonly strict?: boolean;
585
- }): Pipeline<Args, Args & UnionToIntersection<ExtractProvides<S>>>;
726
+ }): PipelineBuilder<Args, Args>;
586
727
 
587
728
  /**
588
729
  * A step with a conditional predicate attached.
@@ -602,24 +743,9 @@ type ConditionalStep = Step & {
602
743
  * skipped:
603
744
  * - No context snapshot is taken.
604
745
  * - No rollback entry is created.
605
- * - The step name is recorded in `result.meta.stepsSkipped`.
606
- *
607
- * If the predicate throws, the pipeline treats it as a step failure
608
- * and triggers rollback for any previously completed steps.
609
- *
610
- * @example
611
- * ```ts
612
- * const steps = [
613
- * validateOrder,
614
- * when((ctx) => ctx.order.amount > 100, notifyManager),
615
- * sendConfirmation,
616
- * ];
617
- * ```
746
+ * - The step name is not recorded in the pipeline's `meta.stepsExecuted`.
618
747
  *
619
- * @typeParam Requires - Inferred from the step's requires type.
620
- * @typeParam Provides - Inferred from the step's provides type.
621
- * @param predicate - Guard function. Receives the current accumulated
622
- * context (frozen). Return `true` to execute, `false` to skip.
748
+ * @param predicate - Guard function. Return `true` to execute, `false` to skip.
623
749
  * @param step - The step to conditionally execute.
624
750
  * @returns A frozen {@link TypedStep} with the predicate attached.
625
751
  */
@@ -637,8 +763,8 @@ type AsContext<T> = T extends StepContext ? T : StepContext;
637
763
  * steps are rolled back in reverse array order before the failure
638
764
  * propagates.
639
765
  *
640
- * The returned step acts as a single step from the pipeline's
641
- * perspective middleware wraps the group as a whole.
766
+ * Returns an {@link AggregateStep} with orchestration metadata
767
+ * tracking which inner steps executed.
642
768
  *
643
769
  * Inner steps retain their own `requires`/`provides` validation,
644
770
  * `retry`, and `timeout` behavior. Conditional steps (via `when()`)
@@ -646,7 +772,7 @@ type AsContext<T> = T extends StepContext ? T : StepContext;
646
772
  *
647
773
  * @example
648
774
  * ```ts
649
- * const pipeline = buildPipeline({
775
+ * const p = pipeline({
650
776
  * name: 'checkout',
651
777
  * steps: [
652
778
  * validateOrder,
@@ -657,14 +783,14 @@ type AsContext<T> = T extends StepContext ? T : StepContext;
657
783
  * ```
658
784
  *
659
785
  * @param steps - Two or more steps to execute concurrently.
660
- * @returns A frozen {@link TypedStep} whose `Requires` is the
786
+ * @returns A frozen {@link AggregateStep} whose `Requires` is the
661
787
  * intersection of all inner steps' requires, and `Provides` is the
662
788
  * intersection of all inner steps' provides.
663
789
  */
664
- declare function parallel<S extends readonly TypedStep[]>(...steps: [...S]): TypedStep<AsContext<UnionToIntersection<ExtractRequires<S[number]>>>, AsContext<UnionToIntersection<ExtractProvides<S[number]>>>>;
790
+ declare function parallel<S extends readonly Step[]>(...steps: [...S]): AggregateStep<AsContext<UnionToIntersection<ExtractRequires<S[number]>>>, AsContext<UnionToIntersection<ExtractProvides<S[number]>>>>;
665
791
 
666
792
  /** A [predicate, step] tuple used by {@link choice}. */
667
- type BranchTuple = readonly [(ctx: Readonly<StepContext>) => boolean, TypedStep];
793
+ type BranchTuple = readonly [(ctx: Readonly<StepContext>) => boolean, Step];
668
794
  /** Extract the Requires type from a branch tuple's step. */
669
795
  type BranchRequires<T> = T extends readonly [unknown, infer S extends Step] ? ExtractRequires<S> : T extends Step ? ExtractRequires<T> : StepContext;
670
796
  /** Extract the Provides type from a branch tuple's step. */
@@ -679,12 +805,15 @@ type BranchProvides<T> = T extends readonly [unknown, infer S extends Step] ? Ex
679
805
  * A bare step (without a predicate tuple) can be passed as the last argument
680
806
  * to serve as a default branch — it is equivalent to `[() => true, step]`.
681
807
  *
682
- * All branches should provide the same output shape so that subsequent
683
- * steps can rely on a consistent context type.
808
+ * Returns an {@link AggregateStep} with orchestration metadata
809
+ * tracking which branch executed.
810
+ *
811
+ * All branches should provide the same output shape so that
812
+ * subsequent steps can rely on a consistent context type.
684
813
  *
685
814
  * @example
686
815
  * ```ts
687
- * const pipeline = buildPipeline({
816
+ * const p = pipeline({
688
817
  * name: 'payment',
689
818
  * steps: [
690
819
  * validateOrder,
@@ -700,31 +829,29 @@ type BranchProvides<T> = T extends readonly [unknown, infer S extends Step] ? Ex
700
829
  *
701
830
  * @param branches - One or more `[predicate, step]` tuples, optionally
702
831
  * followed by a bare step as the default.
703
- * @returns A frozen {@link TypedStep} that executes the first matching branch.
832
+ * @returns A frozen {@link AggregateStep} that executes the first
833
+ * matching branch.
704
834
  */
705
- declare function choice<B extends readonly BranchTuple[]>(...branches: [...B]): TypedStep<AsContext<UnionToIntersection<BranchRequires<B[number]>>>, AsContext<UnionToIntersection<BranchProvides<B[number]>>>>;
706
- declare function choice<B extends readonly BranchTuple[], D extends TypedStep>(...args: [...B, D]): TypedStep<AsContext<UnionToIntersection<BranchRequires<B[number]> | ExtractRequires<D>>>, AsContext<UnionToIntersection<BranchProvides<B[number]> | ExtractProvides<D>>>>;
835
+ declare function choice<B extends readonly BranchTuple[]>(...branches: [...B]): AggregateStep<AsContext<UnionToIntersection<BranchRequires<B[number]>>>, AsContext<UnionToIntersection<BranchProvides<B[number]>>>>;
836
+ declare function choice<B extends readonly BranchTuple[], D extends Step>(...args: [...B, D]): AggregateStep<AsContext<UnionToIntersection<BranchRequires<B[number]> | ExtractRequires<D>>>, AsContext<UnionToIntersection<BranchProvides<B[number]> | ExtractProvides<D>>>>;
707
837
 
708
838
  /**
709
839
  * Iterate over a collection and run a function or step per item, concurrently.
710
840
  *
711
- * Similar to an AWS Step Functions Map state — extracts a collection from
712
- * the pipeline context, runs the callback for each item via
713
- * `Promise.allSettled`, and collects results into an array under the
714
- * given key.
715
- *
716
841
  * **Function form:** `(item, ctx) => result` — items can be any type.
717
842
  *
718
843
  * **Step form:** each item must be an object whose keys are spread into
719
- * the pipeline context before the step runs (i.e., the step receives
720
- * `{ ...ctx, ...item }`). The step's own `requires`/`provides`
721
- * validation, `retry`, and `timeout` apply per item. On partial failure,
722
- * succeeded items are rolled back (if the step has a rollback handler).
844
+ * the pipeline context before the step runs.
845
+ *
846
+ * The step receives `{ ...ctx, ...item }`. The step's own
847
+ * `requires`/`provides` validation, `retry`, and `timeout` apply
848
+ * per item. On partial failure, succeeded items are rolled back
849
+ * (if the step has a rollback handler).
723
850
  *
724
851
  * @example
725
852
  * ```ts
726
853
  * // Function form
727
- * const pipeline = buildPipeline({
854
+ * const pipeline = pipeline({
728
855
  * name: 'notify',
729
856
  * steps: [
730
857
  * map('emails', (ctx) => ctx.users, async (user) => {
@@ -735,7 +862,7 @@ declare function choice<B extends readonly BranchTuple[], D extends TypedStep>(.
735
862
  * });
736
863
  *
737
864
  * // Step form
738
- * const pipeline = buildPipeline({
865
+ * const pipeline = pipeline({
739
866
  * name: 'process',
740
867
  * steps: [
741
868
  * map('results', (ctx) => ctx.items, processItem),
@@ -748,16 +875,17 @@ declare function choice<B extends readonly BranchTuple[], D extends TypedStep>(.
748
875
  * @param fnOrStep - A per-item function or a step to execute for each item.
749
876
  * @returns A frozen {@link TypedStep} that provides `{ [key]: Result[] }`.
750
877
  */
751
- declare function map<K extends string, Item, Result>(key: K, collection: (ctx: Readonly<StepContext>) => Item[], fn: (item: Item, ctx: Readonly<StepContext>) => Result | Promise<Result>): TypedStep<StepContext, Record<K, Awaited<Result>[]>>;
752
- declare function map<K extends string, S extends TypedStep>(key: K, collection: (ctx: Readonly<StepContext>) => StepContext[], step: S): TypedStep<StepContext, Record<K, ExtractProvides<S>[]>>;
878
+ declare function map<K extends string, Item, Result, Ctx extends StepContext = StepContext>(key: K, collection: (ctx: Readonly<Ctx>) => Item[], fn: (item: Item, ctx: Readonly<Ctx>) => Result | Promise<Result>): TypedStep<Ctx, Record<K, Awaited<Result>[]>>;
879
+ declare function map<K extends string, S extends Step, Ctx extends StepContext = StepContext>(key: K, collection: (ctx: Readonly<Ctx>) => StepContext[], step: S): TypedStep<Ctx, Record<K, ExtractProvides<S>[]>>;
753
880
 
754
881
  /**
755
882
  * Filter a collection from context using a predicate, concurrently.
756
883
  *
757
884
  * Extracts a collection from the pipeline context, evaluates the
758
885
  * predicate for each item via `Promise.allSettled`, and collects
759
- * items that pass into an array under the given key. Original order
760
- * is preserved.
886
+ * items that pass into an array under the given key.
887
+ *
888
+ * Original order is preserved.
761
889
  *
762
890
  * The predicate can be sync or async. If any predicate throws, the
763
891
  * entire step fails — no partial results are returned.
@@ -767,10 +895,14 @@ declare function map<K extends string, S extends TypedStep>(key: K, collection:
767
895
  *
768
896
  * @example
769
897
  * ```ts
770
- * const pipeline = buildPipeline({
898
+ * const pipeline = pipeline({
771
899
  * name: 'notify',
772
900
  * steps: [
773
- * filter('eligible', (ctx) => ctx.users, (user) => user.optedIn),
901
+ * filter(
902
+ * 'eligible',
903
+ * (ctx) => ctx.users,
904
+ * (user) => user.optedIn,
905
+ * ),
774
906
  * map('emails', (ctx) => ctx.eligible, sendEmail),
775
907
  * ],
776
908
  * });
@@ -787,7 +919,7 @@ declare function map<K extends string, S extends TypedStep>(key: K, collection:
787
919
  * @param predicate - A per-item predicate. Return `true` to keep, `false` to discard.
788
920
  * @returns A frozen {@link TypedStep} that provides `{ [key]: Item[] }`.
789
921
  */
790
- declare function filter<K extends string, Item>(key: K, collection: (ctx: Readonly<StepContext>) => Item[], predicate: (item: Item, ctx: Readonly<StepContext>) => boolean | Promise<boolean>): TypedStep<StepContext, Record<K, Item[]>>;
922
+ declare function filter<K extends string, Item, Ctx extends StepContext = StepContext>(key: K, collection: (ctx: Readonly<Ctx>) => Item[], predicate: (item: Item, ctx: Readonly<Ctx>) => boolean | Promise<boolean>): TypedStep<Ctx, Record<K, Item[]>>;
791
923
 
792
924
  /**
793
925
  * Map each item in a collection to an array, then flatten one level.
@@ -804,10 +936,14 @@ declare function filter<K extends string, Item>(key: K, collection: (ctx: Readon
804
936
  * @example
805
937
  * ```ts
806
938
  * // Expand orders into line items
807
- * const pipeline = buildPipeline({
939
+ * const pipeline = pipeline({
808
940
  * name: 'process',
809
941
  * steps: [
810
- * flatMap('lineItems', (ctx) => ctx.orders, (order) => order.items),
942
+ * flatMap(
943
+ * 'lineItems',
944
+ * (ctx) => ctx.orders,
945
+ * (order) => order.items,
946
+ * ),
811
947
  * ],
812
948
  * });
813
949
  *
@@ -823,101 +959,6 @@ declare function filter<K extends string, Item>(key: K, collection: (ctx: Readon
823
959
  * @param fn - A per-item callback that returns an array (or Promise of array).
824
960
  * @returns A frozen {@link TypedStep} that provides `{ [key]: Item[] }`.
825
961
  */
826
- declare function flatMap<K extends string, Item, Result>(key: K, collection: (ctx: Readonly<StepContext>) => Item[], fn: (item: Item, ctx: Readonly<StepContext>) => Result[] | Promise<Result[]>): TypedStep<StepContext, Record<K, Result[]>>;
827
-
828
- /**
829
- * A fluent pipeline builder that progressively narrows the accumulated
830
- * context type as steps are added.
831
- *
832
- * Each method returns a new, frozen builder — builders are immutable.
833
- * This means you can safely fork a builder to create variants:
834
- *
835
- * ```ts
836
- * const base = createPipeline('order').step(validate);
837
- * const withCharge = base.step(charge).build();
838
- * const withoutCharge = base.build(); // unaffected by the fork
839
- * ```
840
- *
841
- * @typeParam Args - The pipeline's initial input type.
842
- * @typeParam Ctx - The accumulated context type so far (grows with each `.step()`).
843
- */
844
- type PipelineBuilder<Args extends StepContext, Ctx extends StepContext> = {
845
- /**
846
- * Add a step to the pipeline.
847
- *
848
- * The step's `Requires` type must be satisfied by the current `Ctx`.
849
- * The returned builder's `Ctx` expands to include the step's `Provides`.
850
- *
851
- * @typeParam Provides - The output type of the step being added.
852
- * @param step - A {@link TypedStep} (from `defineStep` or `when`).
853
- * @returns A new builder with the expanded context type.
854
- */
855
- readonly step: <Provides extends StepContext>(step: TypedStep<Ctx, Provides>) => PipelineBuilder<Args, Ctx & Provides>;
856
- /**
857
- * Add middleware to the pipeline.
858
- *
859
- * Middleware is applied to every step. Multiple `.use()` calls
860
- * accumulate — earlier middleware is outermost (executes first).
861
- *
862
- * @param middleware - One or more {@link StepMiddleware} functions.
863
- * @returns A new builder with the middleware added.
864
- */
865
- readonly use: (...middleware: StepMiddleware[]) => PipelineBuilder<Args, Ctx>;
866
- /**
867
- * Build the pipeline.
868
- *
869
- * @returns A frozen {@link Pipeline} ready to execute with `run()`.
870
- */
871
- readonly build: () => Pipeline<Args, Ctx>;
872
- };
873
- /**
874
- * Start building a pipeline with the fluent builder API.
875
- *
876
- * The builder gives progressive type narrowing — each `.step()` call
877
- * extends the known context type, so TypeScript catches mismatches
878
- * at compile time.
879
- *
880
- * **Type-only args** (no runtime validation):
881
- * ```ts
882
- * createPipeline<{ orderId: string }>('placeOrder')
883
- * .step(validateOrder)
884
- * .step(chargePayment)
885
- * .build();
886
- * ```
887
- *
888
- * **Schema args** (runtime validation + type inference):
889
- * ```ts
890
- * createPipeline('placeOrder', z.object({ orderId: z.string() }))
891
- * .step(validateOrder)
892
- * .step(chargePayment)
893
- * .build();
894
- * ```
895
- *
896
- * **Strict mode** (no schema):
897
- * ```ts
898
- * createPipeline('placeOrder', { strict: true })
899
- * ```
900
- *
901
- * **Schema + strict mode:**
902
- * ```ts
903
- * createPipeline('placeOrder', z.object({ orderId: z.string() }), { strict: true })
904
- * ```
905
- *
906
- * @typeParam Args - The pipeline's input type. Inferred from `argsSchema`
907
- * if provided, otherwise specify via generic parameter.
908
- * @param name - Pipeline name, used in metadata and error messages.
909
- * @param schemaOrOptions - A schema for runtime args validation, or a
910
- * {@link PipelineOptions} object.
911
- * @param options - When the second argument is a schema, pass options here.
912
- * @returns A frozen {@link PipelineBuilder} ready for `.step()`,
913
- * `.use()`, and `.build()`.
914
- */
915
- type PipelineOptions = {
916
- strict?: boolean;
917
- };
918
- declare function createPipeline<Args extends StepContext>(name: string): PipelineBuilder<Args, Args>;
919
- declare function createPipeline<Args extends StepContext>(name: string, argsSchema: ParserSchema<Args>): PipelineBuilder<Args, Args>;
920
- declare function createPipeline<Args extends StepContext>(name: string, options: PipelineOptions): PipelineBuilder<Args, Args>;
921
- declare function createPipeline<Args extends StepContext>(name: string, argsSchema: ParserSchema<Args>, options: PipelineOptions): PipelineBuilder<Args, Args>;
962
+ declare function flatMap<K extends string, Item, Result, Ctx extends StepContext = StepContext>(key: K, collection: (ctx: Readonly<Ctx>) => Item[], fn: (item: Item, ctx: Readonly<Ctx>) => Result[] | Promise<Result[]>): TypedStep<Ctx, Record<K, Result[]>>;
922
963
 
923
- export { ArgsValidationError, ChoiceNoMatchError, type ConditionalStep, type ExtractProvides, type ExtractRequires, type Pipeline, type PipelineBuilder, type PipelineConfig, type PipelineExecutionMeta, type PipelineFailure, type PipelineResult, type PipelineSuccess, PredicateError, ProvidesValidationError, RequiresValidationError, RetryExhaustedError, type RetryPolicy, RollbackError, type RollbackFailure, type RollbackReport, RunsheetError, type RunsheetErrorCode, type Step, type StepConfig, type StepContext, type StepExecutor, type StepInfo, type StepMiddleware, type StepOutput, StrictOverlapError, TimeoutError, type TypedStep, UnknownError, buildPipeline, choice, createPipeline, defineStep, filter, flatMap, map, parallel, when };
964
+ export { type AggregateFailure, type AggregateMeta, type AggregateResult, type AggregateStep, type AggregateSuccess, ArgsValidationError, ChoiceNoMatchError, type ConditionalStep, type ExtractProvides, type ExtractRequires, type PipelineBuilder, type PipelineConfig, PredicateError, ProvidesValidationError, RequiresValidationError, RetryExhaustedError, type RetryPolicy, RollbackError, type RollbackFailure, type RollbackReport, RunsheetError, type RunsheetErrorCode, type Step, type StepConfig, type StepContext, type StepExecutor, type StepFailure, type StepInfo, type StepMeta, type StepMiddleware, type StepOutput, type StepResult, type StepSchema, type StepSuccess, StrictOverlapError, TimeoutError, type TypedStep, UnknownError, choice, defineStep, filter, flatMap, map, parallel, pipeline, when };