@barnum/barnum 0.0.0-main-ef6df91f → 0.0.0-main-e8b82cff

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/src/ast.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import type { JSONSchema7 } from "json-schema";
2
+
1
3
  // ---------------------------------------------------------------------------
2
4
  // Serializable Types — mirror the Rust AST in barnum_ast
3
5
  // ---------------------------------------------------------------------------
@@ -8,9 +10,10 @@ export type Action =
8
10
  | ForEachAction
9
11
  | AllAction
10
12
  | BranchAction
11
- | StepAction
12
- | HandleAction
13
- | PerformAction;
13
+ | ResumeHandleAction
14
+ | ResumePerformAction
15
+ | RestartHandleAction
16
+ | RestartPerformAction;
14
17
 
15
18
  export interface InvokeAction {
16
19
  kind: "Invoke";
@@ -38,25 +41,30 @@ export interface BranchAction {
38
41
  cases: Record<string, Action>;
39
42
  }
40
43
 
41
- export interface StepAction {
42
- kind: "Step";
43
- step: StepRef;
44
+ export interface ResumeHandleAction {
45
+ kind: "ResumeHandle";
46
+ resume_handler_id: ResumeHandlerId;
47
+ body: Action;
48
+ handler: Action;
49
+ }
50
+
51
+ export interface ResumePerformAction {
52
+ kind: "ResumePerform";
53
+ resume_handler_id: ResumeHandlerId;
44
54
  }
45
55
 
46
- export interface HandleAction {
47
- kind: "Handle";
48
- effect_id: number;
56
+ export interface RestartHandleAction {
57
+ kind: "RestartHandle";
58
+ restart_handler_id: RestartHandlerId;
49
59
  body: Action;
50
60
  handler: Action;
51
61
  }
52
62
 
53
- export interface PerformAction {
54
- kind: "Perform";
55
- effect_id: number;
63
+ export interface RestartPerformAction {
64
+ kind: "RestartPerform";
65
+ restart_handler_id: RestartHandlerId;
56
66
  }
57
67
 
58
- export type StepRef = { kind: "Named"; name: string } | { kind: "Root" };
59
-
60
68
  // ---------------------------------------------------------------------------
61
69
  // HandlerKind
62
70
  // ---------------------------------------------------------------------------
@@ -67,6 +75,8 @@ export interface TypeScriptHandler {
67
75
  kind: "TypeScript";
68
76
  module: string;
69
77
  func: string;
78
+ input_schema?: JSONSchema7;
79
+ output_schema?: JSONSchema7;
70
80
  }
71
81
 
72
82
  export interface BuiltinHandler {
@@ -86,32 +96,6 @@ export type BuiltinKind =
86
96
  | { kind: "Pick"; value: string[] }
87
97
  | { kind: "CollectSome" };
88
98
 
89
- // ---------------------------------------------------------------------------
90
- // WorkflowAction — loosened input constraint for workflow entry points
91
- // ---------------------------------------------------------------------------
92
-
93
- /**
94
- * A TypedAction suitable as a workflow entry point. Workflows start with
95
- * no input data, so the action must not require specific input.
96
- *
97
- * Uses `__in?: void` to accept both:
98
- * - `TypedAction<any, Out>` — combinators that ignore input (constant, sleep)
99
- * - `TypedAction<never, Out>` — handlers that genuinely take no params
100
- *
101
- * Rejects `TypedAction<{ artifact: string }, Out>` etc. because
102
- * `{ artifact: string }` is not assignable to `void`.
103
- *
104
- * Only `__in` is checked (no `__phantom_in`) — the contravariant phantom
105
- * field would accept anything due to `void`'s permissiveness, so omitting
106
- * it is harmless and avoids deep method signature comparison.
107
- */
108
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
109
- export type WorkflowAction<Out = any> = Action & {
110
- __in?: void;
111
- __phantom_out?: () => Out;
112
- __phantom_out_check?: (output: Out) => void;
113
- };
114
-
115
99
  /**
116
100
  * When TIn is `never` (handler ignores input), produce `any` so the
117
101
  * combinator/pipe can sit in any pipeline position.
@@ -122,10 +106,8 @@ export type PipeIn<T> = [T] extends [never] ? any : T;
122
106
  // Config
123
107
  // ---------------------------------------------------------------------------
124
108
 
125
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
126
- export interface Config<Out = any> {
127
- workflow: WorkflowAction<Out>;
128
- steps?: Record<string, Action>;
109
+ export interface Config {
110
+ workflow: Action;
129
111
  }
130
112
 
131
113
  // ---------------------------------------------------------------------------
@@ -133,14 +115,16 @@ export interface Config<Out = any> {
133
115
  // ---------------------------------------------------------------------------
134
116
 
135
117
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
136
- type UnionToIntersection<TUnion> = (TUnion extends any ? (x: TUnion) => void : never) extends (
137
- x: infer TIntersection,
138
- ) => void
118
+ type UnionToIntersection<TUnion> = (
119
+ TUnion extends any ? (x: TUnion) => void : never
120
+ ) extends (x: infer TIntersection) => void
139
121
  ? TIntersection
140
122
  : never;
141
123
 
142
124
  /** Merge a tuple of objects into a single intersection type. */
143
- export type MergeTuple<TTuple> = TTuple extends unknown[] ? UnionToIntersection<TTuple[number]> : never;
125
+ export type MergeTuple<TTuple> = TTuple extends unknown[]
126
+ ? UnionToIntersection<TTuple[number]>
127
+ : never;
144
128
 
145
129
  // ---------------------------------------------------------------------------
146
130
  // Phantom Types — type-safe input/output tracking
@@ -163,8 +147,6 @@ export type MergeTuple<TTuple> = TTuple extends unknown[] ? UnionToIntersection<
163
147
  * (the contravariant __phantom_in makes never the most permissive input,
164
148
  * so the covariant __in is needed for the entry point check).
165
149
  *
166
- * Refs: tracks step reference names through combinators for compile-time
167
- * validation in registerSteps (see ValidateStepRefs)
168
150
  */
169
151
  export type TypedAction<
170
152
  In = unknown,
@@ -181,16 +163,34 @@ export type TypedAction<
181
163
  next: Pipeable<Out, TNext, TRefs2>,
182
164
  ): TypedAction<In, TNext, Refs | TRefs2>;
183
165
  /** Apply an action to each element of an array output. `a.forEach(b)` ≡ `a.then(forEach(b))`. */
184
- forEach<TIn, TElement, TNext, TRefs extends string, TRefs2 extends string = never>(
166
+ forEach<
167
+ TIn,
168
+ TElement,
169
+ TNext,
170
+ TRefs extends string,
171
+ TRefs2 extends string = never,
172
+ >(
185
173
  this: TypedAction<TIn, TElement[], TRefs>,
186
174
  action: Pipeable<TElement, TNext, TRefs2>,
187
175
  ): TypedAction<TIn, TNext[], TRefs | TRefs2>;
188
176
  /** Dispatch on a tagged union output. Auto-unwraps `value` before each case handler. */
189
- branch<TCases extends { [K in BranchKeys<Out>]: CaseHandler<BranchPayload<Out, K>, unknown, string> }>(
177
+ branch<
178
+ TCases extends {
179
+ [K in BranchKeys<Out>]: CaseHandler<
180
+ BranchPayload<Out, K>,
181
+ unknown,
182
+ string
183
+ >;
184
+ },
185
+ >(
190
186
  cases: [BranchKeys<Out>] extends [never] ? never : TCases,
191
- ): TypedAction<In, ExtractOutput<TCases[keyof TCases & string]>, Refs | ExtractRefs<TCases[keyof TCases & string]>>;
187
+ ): TypedAction<In, ExtractOutput<TCases[keyof TCases & string]>, Refs>;
192
188
  /** Flatten a nested array output. `a.flatten()` ≡ `pipe(a, flatten())`. */
193
- flatten(): TypedAction<In, Out extends (infer TElement)[][] ? TElement[] : Out, Refs>;
189
+ flatten(): TypedAction<
190
+ In,
191
+ Out extends (infer TElement)[][] ? TElement[] : Out,
192
+ Refs
193
+ >;
194
194
  /** Discard output. `a.drop()` ≡ `pipe(a, drop)`. */
195
195
  drop(): TypedAction<In, never, Refs>;
196
196
  /** Wrap output as a tagged union member. Requires full variant map TDef so __def is carried. */
@@ -198,7 +198,9 @@ export type TypedAction<
198
198
  kind: TKind,
199
199
  ): TypedAction<In, TaggedUnion<TDef>, Refs>;
200
200
  /** Extract a field from the output object. `a.get("name")` ≡ `pipe(a, extractField("name"))`. */
201
- get<TField extends keyof Out & string>(field: TField): TypedAction<In, Out[TField], Refs>;
201
+ get<TField extends keyof Out & string>(
202
+ field: TField,
203
+ ): TypedAction<In, Out[TField], Refs>;
202
204
  /**
203
205
  * Run this sub-pipeline, then merge its output back into the original input.
204
206
  * `pipe(extractField("x"), transform).augment()` takes `In`, runs the
@@ -303,7 +305,11 @@ export type Pipeable<
303
305
  * TypedAction is assignable to CaseHandler because CaseHandler only
304
306
  * requires a subset of TypedAction's phantom fields.
305
307
  */
306
- type CaseHandler<TIn = unknown, TOut = unknown, TRefs extends string = never> = Action & {
308
+ type CaseHandler<
309
+ TIn = unknown,
310
+ TOut = unknown,
311
+ TRefs extends string = never,
312
+ > = Action & {
307
313
  __phantom_in?: (input: TIn) => void;
308
314
  __phantom_out?: () => TOut;
309
315
  __refs?: { _brand: TRefs };
@@ -320,8 +326,21 @@ type CaseHandler<TIn = unknown, TOut = unknown, TRefs extends string = never> =
320
326
  * (`keyof ExtractDef<Out>` and `ExtractDef<Out>[K]`) instead of
321
327
  * conditional types (`KindOf<Out>` and `Extract<Out, { kind: K }>`).
322
328
  */
329
+ // 0 extends 1 & T detects `any` — preserve as-is to avoid collapsing.
330
+ type VoidToNull<T> = 0 extends 1 & T
331
+ ? T
332
+ : [T] extends [never]
333
+ ? never
334
+ : [T] extends [void]
335
+ ? null
336
+ : T;
337
+
323
338
  export type TaggedUnion<TDef extends Record<string, unknown>> = {
324
- [K in keyof TDef & string]: { kind: K; value: TDef[K]; __def?: TDef };
339
+ [K in keyof TDef & string]: {
340
+ kind: K;
341
+ value: VoidToNull<TDef[K]>;
342
+ __def?: TDef;
343
+ };
325
344
  }[keyof TDef & string];
326
345
 
327
346
  /** Extract the variant map definition from a tagged union's phantom __def. */
@@ -352,50 +371,65 @@ type UnwrapVariant<T> = T extends { value: infer V } ? V : T;
352
371
  * output carries __def. Falls back to KindOf (conditional type) for
353
372
  * outputs without __def.
354
373
  */
355
- type BranchKeys<Out> =
356
- [ExtractDef<Out>] extends [never] ? KindOf<Out> : keyof ExtractDef<Out> & string;
374
+ type BranchKeys<Out> = [ExtractDef<Out>] extends [never]
375
+ ? KindOf<Out>
376
+ : keyof ExtractDef<Out> & string;
357
377
 
358
378
  /**
359
379
  * Branch case payload: prefer ExtractDef[K] (simple indexing) when available.
360
380
  * Falls back to UnwrapVariant<Extract<Out, { kind: K }>> for outputs without __def.
361
381
  */
362
- type BranchPayload<Out, K extends string> =
363
- [ExtractDef<Out>] extends [never]
364
- ? UnwrapVariant<Extract<Out, { kind: K }>>
365
- : K extends keyof ExtractDef<Out> ? ExtractDef<Out>[K] : never;
366
-
382
+ type BranchPayload<Out, K extends string> = [ExtractDef<Out>] extends [never]
383
+ ? UnwrapVariant<Extract<Out, { kind: K }>>
384
+ : K extends keyof ExtractDef<Out>
385
+ ? VoidToNull<ExtractDef<Out>[K]>
386
+ : never;
367
387
 
368
388
  // ---------------------------------------------------------------------------
369
389
  // typedAction — attach .then() and .forEach() as non-enumerable methods
370
390
  // ---------------------------------------------------------------------------
371
391
 
372
392
  // Shared implementations (one closure, not per-instance)
373
- function thenMethod<TIn, TOut, TRefs extends string, TNext, TRefs2 extends string>(
393
+ function thenMethod<
394
+ TIn,
395
+ TOut,
396
+ TRefs extends string,
397
+ TNext,
398
+ TRefs2 extends string,
399
+ >(
374
400
  this: TypedAction<TIn, TOut, TRefs>,
375
401
  next: Pipeable<TOut, TNext, TRefs2>,
376
402
  ): TypedAction<TIn, TNext, TRefs | TRefs2> {
377
403
  return typedAction({ kind: "Chain", first: this, rest: next as Action });
378
404
  }
379
405
 
380
- function forEachMethod(
381
- this: TypedAction,
382
- action: Action,
383
- ): TypedAction {
384
- return typedAction({ kind: "Chain", first: this, rest: { kind: "ForEach", action } });
406
+ function forEachMethod(this: TypedAction, action: Action): TypedAction {
407
+ return typedAction({
408
+ kind: "Chain",
409
+ first: this,
410
+ rest: { kind: "ForEach", action },
411
+ });
385
412
  }
386
413
 
387
414
  function branchMethod(
388
415
  this: TypedAction,
389
416
  cases: Record<string, Action>,
390
417
  ): TypedAction {
391
- return typedAction({ kind: "Chain", first: this, rest: { kind: "Branch", cases: unwrapBranchCases(cases) } });
418
+ return typedAction({
419
+ kind: "Chain",
420
+ first: this,
421
+ rest: { kind: "Branch", cases: unwrapBranchCases(cases) },
422
+ });
392
423
  }
393
424
 
394
425
  function flattenMethod(this: TypedAction): TypedAction {
395
426
  return typedAction({
396
427
  kind: "Chain",
397
428
  first: this,
398
- rest: { kind: "Invoke", handler: { kind: "Builtin", builtin: { kind: "Flatten" } } },
429
+ rest: {
430
+ kind: "Invoke",
431
+ handler: { kind: "Builtin", builtin: { kind: "Flatten" } },
432
+ },
399
433
  });
400
434
  }
401
435
 
@@ -403,7 +437,10 @@ function dropMethod(this: TypedAction): TypedAction {
403
437
  return typedAction({
404
438
  kind: "Chain",
405
439
  first: this,
406
- rest: { kind: "Invoke", handler: { kind: "Builtin", builtin: { kind: "Drop" } } },
440
+ rest: {
441
+ kind: "Invoke",
442
+ handler: { kind: "Builtin", builtin: { kind: "Drop" } },
443
+ },
407
444
  });
408
445
  }
409
446
 
@@ -411,7 +448,10 @@ function tagMethod(this: TypedAction, kind: string): TypedAction {
411
448
  return typedAction({
412
449
  kind: "Chain",
413
450
  first: this,
414
- rest: { kind: "Invoke", handler: { kind: "Builtin", builtin: { kind: "Tag", value: kind } } },
451
+ rest: {
452
+ kind: "Invoke",
453
+ handler: { kind: "Builtin", builtin: { kind: "Tag", value: kind } },
454
+ },
415
455
  });
416
456
  }
417
457
 
@@ -419,7 +459,13 @@ function getMethod(this: TypedAction, field: string): TypedAction {
419
459
  return typedAction({
420
460
  kind: "Chain",
421
461
  first: this,
422
- rest: { kind: "Invoke", handler: { kind: "Builtin", builtin: { kind: "ExtractField", value: field } } },
462
+ rest: {
463
+ kind: "Invoke",
464
+ handler: {
465
+ kind: "Builtin",
466
+ builtin: { kind: "ExtractField", value: field },
467
+ },
468
+ },
423
469
  });
424
470
  }
425
471
 
@@ -433,10 +479,16 @@ function augmentMethod(this: TypedAction): TypedAction {
433
479
  kind: "All",
434
480
  actions: [
435
481
  this as Action,
436
- { kind: "Invoke", handler: { kind: "Builtin", builtin: { kind: "Identity" } } },
482
+ {
483
+ kind: "Invoke",
484
+ handler: { kind: "Builtin", builtin: { kind: "Identity" } },
485
+ },
437
486
  ],
438
487
  },
439
- rest: { kind: "Invoke", handler: { kind: "Builtin", builtin: { kind: "Merge" } } },
488
+ rest: {
489
+ kind: "Invoke",
490
+ handler: { kind: "Builtin", builtin: { kind: "Merge" } },
491
+ },
440
492
  });
441
493
  }
442
494
 
@@ -444,7 +496,10 @@ function mergeMethod(this: TypedAction): TypedAction {
444
496
  return typedAction({
445
497
  kind: "Chain",
446
498
  first: this,
447
- rest: { kind: "Invoke", handler: { kind: "Builtin", builtin: { kind: "Merge" } } },
499
+ rest: {
500
+ kind: "Invoke",
501
+ handler: { kind: "Builtin", builtin: { kind: "Merge" } },
502
+ },
448
503
  });
449
504
  }
450
505
 
@@ -452,7 +507,10 @@ function pickMethod(this: TypedAction, ...keys: string[]): TypedAction {
452
507
  return typedAction({
453
508
  kind: "Chain",
454
509
  first: this,
455
- rest: { kind: "Invoke", handler: { kind: "Builtin", builtin: { kind: "Pick", value: keys } } },
510
+ rest: {
511
+ kind: "Invoke",
512
+ handler: { kind: "Builtin", builtin: { kind: "Pick", value: keys } },
513
+ },
456
514
  });
457
515
  }
458
516
 
@@ -466,9 +524,22 @@ function mapOptionMethod(this: TypedAction, action: Action): TypedAction {
466
524
  first: this,
467
525
  rest: {
468
526
  kind: "Branch",
469
- cases: unwrapBranchCases({
470
- Some: { kind: "Chain", first: action, rest: { kind: "Invoke", handler: { kind: "Builtin", builtin: { kind: "Tag", value: "Some" } } } },
471
- None: { kind: "Invoke", handler: { kind: "Builtin", builtin: { kind: "Tag", value: "None" } } },
527
+ cases: unwrapBranchCases({
528
+ Some: {
529
+ kind: "Chain",
530
+ first: action,
531
+ rest: {
532
+ kind: "Invoke",
533
+ handler: {
534
+ kind: "Builtin",
535
+ builtin: { kind: "Tag", value: "Some" },
536
+ },
537
+ },
538
+ },
539
+ None: {
540
+ kind: "Invoke",
541
+ handler: { kind: "Builtin", builtin: { kind: "Tag", value: "None" } },
542
+ },
472
543
  }),
473
544
  },
474
545
  });
@@ -481,9 +552,22 @@ function mapErrMethod(this: TypedAction, action: Action): TypedAction {
481
552
  first: this,
482
553
  rest: {
483
554
  kind: "Branch",
484
- cases: unwrapBranchCases({
485
- Ok: { kind: "Invoke", handler: { kind: "Builtin", builtin: { kind: "Tag", value: "Ok" } } },
486
- Err: { kind: "Chain", first: action, rest: { kind: "Invoke", handler: { kind: "Builtin", builtin: { kind: "Tag", value: "Err" } } } },
555
+ cases: unwrapBranchCases({
556
+ Ok: {
557
+ kind: "Invoke",
558
+ handler: { kind: "Builtin", builtin: { kind: "Tag", value: "Ok" } },
559
+ },
560
+ Err: {
561
+ kind: "Chain",
562
+ first: action,
563
+ rest: {
564
+ kind: "Invoke",
565
+ handler: {
566
+ kind: "Builtin",
567
+ builtin: { kind: "Tag", value: "Err" },
568
+ },
569
+ },
570
+ },
487
571
  }),
488
572
  },
489
573
  });
@@ -496,8 +580,11 @@ function unwrapOrMethod(this: TypedAction, defaultAction: Action): TypedAction {
496
580
  first: this,
497
581
  rest: {
498
582
  kind: "Branch",
499
- cases: unwrapBranchCases({
500
- Ok: { kind: "Invoke", handler: { kind: "Builtin", builtin: { kind: "Identity" } } },
583
+ cases: unwrapBranchCases({
584
+ Ok: {
585
+ kind: "Invoke",
586
+ handler: { kind: "Builtin", builtin: { kind: "Identity" } },
587
+ },
501
588
  Err: defaultAction,
502
589
  }),
503
590
  },
@@ -508,9 +595,11 @@ function unwrapOrMethod(this: TypedAction, defaultAction: Action): TypedAction {
508
595
  * Attach `.then()` and `.forEach()` methods to a plain Action object.
509
596
  * Methods are non-enumerable: invisible to JSON.stringify and toEqual.
510
597
  */
511
- export function typedAction<In = unknown, Out = unknown, Refs extends string = never>(
512
- action: Action,
513
- ): TypedAction<In, Out, Refs> {
598
+ export function typedAction<
599
+ In = unknown,
600
+ Out = unknown,
601
+ Refs extends string = never,
602
+ >(action: Action): TypedAction<In, Out, Refs> {
514
603
  if (!("then" in action)) {
515
604
  Object.defineProperties(action, {
516
605
  then: { value: thenMethod, configurable: true },
@@ -542,140 +631,20 @@ export function typedAction<In = unknown, Out = unknown, Refs extends string = n
542
631
  * avoid the `TypedAction<any, any, any>` constraint which fails for In=never
543
632
  * due to __phantom_in contravariance.
544
633
  */
545
- export type ExtractInput<T> =
546
- T extends { __phantom_in?: (input: infer In) => void } ? In : never;
634
+ export type ExtractInput<T> = T extends {
635
+ __phantom_in?: (input: infer In) => void;
636
+ }
637
+ ? In
638
+ : never;
547
639
 
548
640
  /**
549
641
  * Extract the output type from a TypedAction.
550
642
  *
551
643
  * Uses direct phantom field extraction to avoid constraint issues.
552
644
  */
553
- export type ExtractOutput<T> =
554
- T extends { __phantom_out?: () => infer Out } ? Out : never;
555
-
556
- /**
557
- * Extract step reference names tracked in a TypedAction's Refs parameter.
558
- *
559
- * Uses direct __refs extraction (not full TypedAction matching) to avoid
560
- * variance issues with contravariant __phantom_in when In = never.
561
- */
562
- export type ExtractRefs<T> =
563
- T extends { __refs?: { _brand: infer R } }
564
- ? R extends string
565
- ? R
566
- : never
567
- : never;
568
-
569
- /**
570
- * Validates that all step references in R resolve to known step names
571
- * within the current batch only (keyof R). Previously registered steps
572
- * should be accessed via the callback's `steps` parameter, not stepRef.
573
- *
574
- * When valid: resolves to {} (transparent intersection).
575
- * When invalid: resolves to a type with __error that causes a compile error.
576
- */
577
- export type ValidateStepRefs<
578
- R extends Record<string, Action>,
579
- > = [ExtractRefs<R[keyof R]>] extends [keyof R]
580
- ? // eslint-disable-next-line @typescript-eslint/no-empty-object-type
581
- {}
582
- : {
583
- __error: `Step reference to undefined step: ${Exclude<ExtractRefs<R[keyof R]>, keyof R> & string}`;
584
- };
585
-
586
- // ---------------------------------------------------------------------------
587
- // Step Reference Tracking (Refs type parameter)
588
- // ---------------------------------------------------------------------------
589
- //
590
- // ## Overview
591
- //
592
- // Named steps can reference each other via `stepRef("B")`. These references
593
- // are validated at compile time: if you write `stepRef("Bt")` when only "A"
594
- // and "B" exist, TypeScript rejects it. This works through a third type
595
- // parameter on TypedAction called `Refs`.
596
- //
597
- // ## How it works
598
- //
599
- // 1. `stepRef<N extends string>(name: N)` returns `TypedAction<any, any, N>`.
600
- // The literal string "B" is captured in the Refs position.
601
- //
602
- // 2. Every combinator propagates Refs via union. For example, pipe's 2-arg
603
- // overload is:
604
- //
605
- // pipe<T1, T2, T3, R1 extends string, R2 extends string>(
606
- // a1: TypedAction<T1, T2, R1>,
607
- // a2: TypedAction<T2, T3, R2>,
608
- // ): TypedAction<T1, T3, R1 | R2>
609
- //
610
- // So `pipe(check(), stepRef("B"))` has Refs = never | "B" = "B".
611
- // And `pipe(stepRef("A"), stepRef("B"))` has Refs = "A" | "B".
612
- //
613
- // 3. `registerSteps` uses `ValidateStepRefs` to extract all Refs from the
614
- // registered step values and check they're a subset of the current batch
615
- // keys only (keyof R). Previously registered steps are accessed via the
616
- // typed `steps` parameter in the callback form, not via stepRef:
617
- //
618
- // registerSteps<R extends Record<string, Action>>(
619
- // stepsOrBuild:
620
- // | (R & ValidateStepRefs<R>)
621
- // | ((ctx: { steps: StripRefs<TSteps>; stepRef: ... })
622
- // => R & ValidateStepRefs<R>),
623
- // )
624
- //
625
- // When valid, ValidateStepRefs resolves to {} (transparent intersection).
626
- // When invalid, it resolves to { __error: "Step reference to undefined
627
- // step: Bt" }, which makes the argument incompatible and produces a
628
- // readable compile error.
629
- //
630
- // stepRef is not exported — it's only available as a parameter in the
631
- // registerSteps callback. This ensures step references are always
632
- // validated within a batch context.
633
- //
634
- // 4. `StripRefs` removes Refs from step types before they're passed to the
635
- // workflow callback, since refs have already been validated by
636
- // registerSteps and shouldn't propagate into the workflow's return type.
637
- //
638
- // ## Why Refs is boxed: `__refs?: { _brand: Refs }`
639
- //
640
- // The Refs phantom field uses a boxing wrapper `{ _brand: Refs }` instead of
641
- // a bare `__refs?: Refs`. This is necessary because of how TypeScript infers
642
- // generic type parameters from optional properties on discriminated unions.
643
- //
644
- // When Refs = never (the common case — most actions don't use stepRef), the
645
- // field `__refs?: never` collapses to `undefined` at the type level. When
646
- // pipe's overload tries to infer `R1 extends string` from this field, TS
647
- // sees `undefined`, can't find a valid inference for `R1`, and falls back to
648
- // the constraint bound `string`.
649
- //
650
- // This means `pipe(check(), recur())` — two actions with no step refs —
651
- // would infer Refs = string | string = string. Then ValidateStepRefs sees
652
- // Refs = string and rejects it because `string` doesn't extend the step
653
- // name literals.
654
- //
655
- // **Important**: this only happens with the real Action type (a discriminated
656
- // union of 8 variants). With a simple `{ kind: string }` Action type, TS
657
- // infers correctly. The union distribution changes how TS resolves optional
658
- // fields during inference.
659
- //
660
- // The fix: `__refs?: { _brand: Refs }`. When Refs = never, the field is
661
- // `__refs?: { _brand: never }`. The wrapper `{ _brand: never }` is a
662
- // distinct structural type (not just `undefined`), so TS can match
663
- // `{ _brand: R1 }` against `{ _brand: never }` and correctly infer
664
- // R1 = never.
665
- //
666
- // ## Why ExtractInput/ExtractOutput/ExtractRefs use structural extraction
667
- //
668
- // All three Extract* utilities match on individual phantom fields rather than
669
- // on the full TypedAction type. The constraint `TypedAction<any, any, any>`
670
- // fails for actions with In = never because `(input: never) => void` is not
671
- // assignable to `(input: any) => void` (function params are contravariant).
672
- // Structural extraction avoids this entirely.
673
- //
674
- // ExtractRefs uses a two-step conditional (`infer R` then `R extends string`)
675
- // rather than `infer R extends string` because the latter falls back to the
676
- // constraint bound `string` when R = never.
677
- //
678
- // ---------------------------------------------------------------------------
645
+ export type ExtractOutput<T> = T extends { __phantom_out?: () => infer Out }
646
+ ? Out
647
+ : never;
679
648
 
680
649
  // ---------------------------------------------------------------------------
681
650
  // Combinators
@@ -686,7 +655,11 @@ export { chain } from "./chain.js";
686
655
  export { all } from "./all.js";
687
656
  export { bind, bindInput, type VarRef, type InferVarRefs } from "./bind.js";
688
657
  export { resetEffectIdCounter } from "./effect-id.js";
689
- import { allocateEffectId } from "./effect-id.js";
658
+ import {
659
+ allocateRestartHandlerId,
660
+ type RestartHandlerId,
661
+ type ResumeHandlerId,
662
+ } from "./effect-id.js";
690
663
  export { tryCatch } from "./try-catch.js";
691
664
  export { race, sleep, withTimeout } from "./race.js";
692
665
 
@@ -702,12 +675,20 @@ export function forEach<In, Out, R extends string = never>(
702
675
  * extracts `value` before passing to the handler. Case handlers receive
703
676
  * the payload directly, not the full `{ kind, value }` variant.
704
677
  */
705
- function unwrapBranchCases(cases: Record<string, Action>): Record<string, Action> {
678
+ function unwrapBranchCases(
679
+ cases: Record<string, Action>,
680
+ ): Record<string, Action> {
706
681
  const unwrapped: Record<string, Action> = {};
707
682
  for (const key of Object.keys(cases)) {
708
683
  unwrapped[key] = {
709
684
  kind: "Chain",
710
- first: { kind: "Invoke", handler: { kind: "Builtin", builtin: { kind: "ExtractField", value: "value" } } },
685
+ first: {
686
+ kind: "Invoke",
687
+ handler: {
688
+ kind: "Builtin",
689
+ builtin: { kind: "ExtractField", value: "value" },
690
+ },
691
+ },
711
692
  rest: cases[key],
712
693
  };
713
694
  }
@@ -723,8 +704,8 @@ function unwrapBranchCases(cases: Record<string, Action>): Record<string, Action
723
704
  * Example: `BranchInput<{ Yes: TypedAction<number, ...>, No: TypedAction<string, ...> }>`
724
705
  * = `{ kind: "Yes"; value: number } | { kind: "No"; value: string }`
725
706
  *
726
- * When a case handler uses `any` as input (e.g. stepRef), the wrapping
727
- * produces `{ kind: K; value: any }`, which is the correct escape hatch.
707
+ * When a case handler uses `any` as input, the wrapping produces
708
+ * `{ kind: K; value: any }`, which is the correct escape hatch.
728
709
  */
729
710
  export type BranchInput<TCases> = {
730
711
  [K in keyof TCases & string]: { kind: K; value: ExtractInput<TCases[K]> };
@@ -735,8 +716,7 @@ export function branch<TCases extends Record<string, Action>>(
735
716
  cases: TCases,
736
717
  ): TypedAction<
737
718
  BranchInput<TCases>,
738
- ExtractOutput<TCases[keyof TCases & string]>,
739
- ExtractRefs<TCases[keyof TCases & string]>
719
+ ExtractOutput<TCases[keyof TCases & string]>
740
720
  > {
741
721
  return typedAction({ kind: "Branch", cases: unwrapBranchCases(cases) });
742
722
  }
@@ -746,7 +726,9 @@ type LoopResultDef<TContinue, TBreak> = {
746
726
  Break: TBreak;
747
727
  };
748
728
 
749
- export type LoopResult<TContinue, TBreak> = TaggedUnion<LoopResultDef<TContinue, TBreak>>;
729
+ export type LoopResult<TContinue, TBreak> = TaggedUnion<
730
+ LoopResultDef<TContinue, TBreak>
731
+ >;
750
732
 
751
733
  // ---------------------------------------------------------------------------
752
734
  // Shared AST constants for control flow compilation
@@ -754,12 +736,7 @@ export type LoopResult<TContinue, TBreak> = TaggedUnion<LoopResultDef<TContinue,
754
736
 
755
737
  const EXTRACT_PAYLOAD: Action = {
756
738
  kind: "Invoke",
757
- handler: { kind: "Builtin", builtin: { kind: "ExtractField", value: "payload" } },
758
- };
759
-
760
- const TAG_RESTART_BODY: Action = {
761
- kind: "Invoke",
762
- handler: { kind: "Builtin", builtin: { kind: "Tag", value: "RestartBody" } },
739
+ handler: { kind: "Builtin", builtin: { kind: "ExtractIndex", value: 0 } },
763
740
  };
764
741
 
765
742
  const TAG_CONTINUE: Action = {
@@ -767,23 +744,16 @@ const TAG_CONTINUE: Action = {
767
744
  handler: { kind: "Builtin", builtin: { kind: "Tag", value: "Continue" } },
768
745
  };
769
746
 
770
- const TAG_BREAK: Action = {
747
+ export const TAG_BREAK: Action = {
771
748
  kind: "Invoke",
772
749
  handler: { kind: "Builtin", builtin: { kind: "Tag", value: "Break" } },
773
750
  };
774
751
 
775
- const IDENTITY: Action = {
752
+ export const IDENTITY: Action = {
776
753
  kind: "Invoke",
777
754
  handler: { kind: "Builtin", builtin: { kind: "Identity" } },
778
755
  };
779
756
 
780
- /** Handler: extract payload, tag as RestartBody. */
781
- const RESTART_BODY_HANDLER: Action = {
782
- kind: "Chain",
783
- first: EXTRACT_PAYLOAD,
784
- rest: TAG_RESTART_BODY,
785
- };
786
-
787
757
  // ---------------------------------------------------------------------------
788
758
  // recur — restart body primitive
789
759
  // ---------------------------------------------------------------------------
@@ -795,25 +765,25 @@ const RESTART_BODY_HANDLER: Action = {
795
765
  * If the body completes normally → output is TOut.
796
766
  * If restart fires → body re-executes with the restarted value.
797
767
  *
798
- * Compiled form: Handle(effectId, body, RestartBodyHandler)
768
+ * Compiled form: `RestartHandle(id, ExtractIndex(0), body)`
799
769
  */
800
770
  export function recur<TIn = never, TOut = any, TRefs extends string = never>(
801
771
  bodyFn: (restart: TypedAction<TIn, never>) => Pipeable<TIn, TOut, TRefs>,
802
772
  ): TypedAction<PipeIn<TIn>, TOut, TRefs> {
803
- const effectId = allocateEffectId();
773
+ const restartHandlerId = allocateRestartHandlerId();
804
774
 
805
775
  const restartAction = typedAction<TIn, never>({
806
- kind: "Perform",
807
- effect_id: effectId,
776
+ kind: "RestartPerform",
777
+ restart_handler_id: restartHandlerId,
808
778
  });
809
779
 
810
780
  const body = bodyFn(restartAction) as Action;
811
781
 
812
782
  return typedAction({
813
- kind: "Handle",
814
- effect_id: effectId,
783
+ kind: "RestartHandle",
784
+ restart_handler_id: restartHandlerId,
815
785
  body,
816
- handler: RESTART_BODY_HANDLER,
786
+ handler: EXTRACT_PAYLOAD,
817
787
  });
818
788
  }
819
789
 
@@ -833,20 +803,29 @@ export function recur<TIn = never, TOut = any, TRefs extends string = never>(
833
803
  * a Branch. earlyReturn tags with Break and performs — the handler restarts
834
804
  * the body, Branch takes the Break path, and the value exits.
835
805
  */
836
- export function earlyReturn<TEarlyReturn = never, TIn = any, TOut = any, TRefs extends string = never>(
837
- bodyFn: (earlyReturn: TypedAction<TEarlyReturn, never>) => Pipeable<TIn, TOut, TRefs>,
806
+ export function earlyReturn<
807
+ TEarlyReturn = never,
808
+ TIn = any,
809
+ TOut = any,
810
+ TRefs extends string = never,
811
+ >(
812
+ bodyFn: (
813
+ earlyReturn: TypedAction<TEarlyReturn, never>,
814
+ ) => Pipeable<TIn, TOut, TRefs>,
838
815
  ): TypedAction<TIn, TEarlyReturn | TOut, TRefs> {
839
- const effectId = allocateEffectId();
816
+ const restartHandlerId = allocateRestartHandlerId();
840
817
 
841
818
  const earlyReturnAction = typedAction<TEarlyReturn, never>({
842
819
  kind: "Chain",
843
820
  first: TAG_BREAK,
844
- rest: { kind: "Perform", effect_id: effectId },
821
+ rest: { kind: "RestartPerform", restart_handler_id: restartHandlerId },
845
822
  });
846
823
 
847
824
  const body = bodyFn(earlyReturnAction) as Action;
848
825
 
849
- return typedAction(buildLoopAction(effectId, body));
826
+ return typedAction(
827
+ buildRestartBranchAction(restartHandlerId, body, IDENTITY),
828
+ );
850
829
  }
851
830
 
852
831
  // ---------------------------------------------------------------------------
@@ -854,27 +833,33 @@ export function earlyReturn<TEarlyReturn = never, TIn = any, TOut = any, TRefs e
854
833
  // ---------------------------------------------------------------------------
855
834
 
856
835
  /**
857
- * Build the restart+branch compiled form used by earlyReturn and loop:
858
- * Chain(Tag("Continue"), Handle(effectId, Branch({ Continue: body, Break: identity() }), RestartBodyHandler))
836
+ * Build the restart+branch compiled form:
837
+ * `Chain(Tag("Continue"), RestartHandle(id, ExtractIndex(0), Branch({ Continue: continueArm, Break: breakArm })))`
859
838
  *
860
- * Input is tagged Continue so the Branch enters the body on first execution.
861
- * Continue tag → restart → re-enters body. Break tag → restart → exits via Branch.
839
+ * Input is tagged Continue so the Branch enters the continueArm on first execution.
840
+ * Continue tag → restart → re-enters continueArm. Break tag → restart → runs breakArm, exits `RestartHandle`.
841
+ *
842
+ * Used by earlyReturn, loop, tryCatch, and race.
862
843
  */
863
- function buildLoopAction(effectId: number, body: Action): Action {
844
+ export function buildRestartBranchAction(
845
+ restartHandlerId: RestartHandlerId,
846
+ continueArm: Action,
847
+ breakArm: Action,
848
+ ): Action {
864
849
  return {
865
850
  kind: "Chain",
866
851
  first: TAG_CONTINUE,
867
852
  rest: {
868
- kind: "Handle",
869
- effect_id: effectId,
853
+ kind: "RestartHandle",
854
+ restart_handler_id: restartHandlerId,
870
855
  body: {
871
856
  kind: "Branch",
872
857
  cases: unwrapBranchCases({
873
- Continue: body,
874
- Break: IDENTITY,
858
+ Continue: continueArm,
859
+ Break: breakArm,
875
860
  }),
876
861
  },
877
- handler: RESTART_BODY_HANDLER,
862
+ handler: EXTRACT_PAYLOAD,
878
863
  },
879
864
  };
880
865
  }
@@ -886,17 +871,20 @@ function buildLoopAction(effectId: number, body: Action): Action {
886
871
  *
887
872
  * Both are TypedAction values (not functions), consistent with throwError in tryCatch.
888
873
  *
889
- * Compiles to Handle/Perform/Branch — same effect substrate as tryCatch and earlyReturn.
874
+ * Compiles to `RestartHandle`/`RestartPerform`/Branch — same effect substrate as tryCatch and earlyReturn.
890
875
  */
891
876
  export function loop<TBreak = never, TIn = never, TRefs extends string = never>(
892
877
  bodyFn: (
893
878
  recur: TypedAction<TIn, never>,
894
- done: TypedAction<TBreak, never>,
879
+ done: TypedAction<VoidToNull<TBreak>, never>,
895
880
  ) => Pipeable<TIn, never, TRefs>,
896
- ): TypedAction<PipeIn<TIn>, TBreak, TRefs> {
897
- const effectId = allocateEffectId();
881
+ ): TypedAction<PipeIn<TIn>, VoidToNull<TBreak>, TRefs> {
882
+ const restartHandlerId = allocateRestartHandlerId();
898
883
 
899
- const perform: Action = { kind: "Perform", effect_id: effectId };
884
+ const perform: Action = {
885
+ kind: "RestartPerform",
886
+ restart_handler_id: restartHandlerId,
887
+ };
900
888
 
901
889
  const recurAction = typedAction<TIn, never>({
902
890
  kind: "Chain",
@@ -904,7 +892,7 @@ export function loop<TBreak = never, TIn = never, TRefs extends string = never>(
904
892
  rest: perform,
905
893
  });
906
894
 
907
- const doneAction = typedAction<TBreak, never>({
895
+ const doneAction = typedAction<VoidToNull<TBreak>, never>({
908
896
  kind: "Chain",
909
897
  first: TAG_BREAK,
910
898
  rest: perform,
@@ -912,196 +900,16 @@ export function loop<TBreak = never, TIn = never, TRefs extends string = never>(
912
900
 
913
901
  const body = bodyFn(recurAction, doneAction) as Action;
914
902
 
915
- return typedAction(buildLoopAction(effectId, body));
916
- }
917
-
918
- /**
919
- * Create a typed step reference. The name is tracked at the type level
920
- * via the Refs parameter so registerSteps can validate all references resolve.
921
- *
922
- * Not exported — only available as a parameter in registerSteps callbacks.
923
- * This ensures step references are always validated within a batch context.
924
- *
925
- * **Warning: no input/output type safety.** stepRef returns
926
- * `TypedAction<any, any, N>` — it validates the reference *name* at compile
927
- * time, but provides no type checking on input or output. The referenced
928
- * step's types are unknown at the call site (mutual recursion means the
929
- * step may not be fully defined yet). Prefer `steps.X` from the callback
930
- * parameter when referencing previously registered steps, as that preserves
931
- * full input/output types.
932
- */
933
- function stepRef<N extends string>(name: N): TypedAction<any, any, N> {
934
- return typedAction({
935
- kind: "Step",
936
- step: { kind: "Named", name },
937
- });
903
+ return typedAction(
904
+ buildRestartBranchAction(restartHandlerId, body, IDENTITY),
905
+ );
938
906
  }
939
907
 
940
908
  // ---------------------------------------------------------------------------
941
909
  // Config builders
942
910
  // ---------------------------------------------------------------------------
943
911
 
944
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
945
- type AnyAction = TypedAction<any, any, any>;
946
-
947
- /**
948
- * Strip the Refs parameter from registered step types. Refs are a compile-time
949
- * mechanism for validating step references in registerSteps — once validated,
950
- * they shouldn't propagate into the workflow callback's return type.
951
- */
952
- type StripRefs<TSteps> = {
953
- [K in keyof TSteps]: TypedAction<ExtractInput<TSteps[K]>, ExtractOutput<TSteps[K]>>;
954
- };
955
-
956
- /** Simple config with no named steps. */
957
- export function config<Out>(workflow: WorkflowAction<Out>): Config<Out> {
912
+ /** Simple config factory. */
913
+ export function config(workflow: Action): Config {
958
914
  return { workflow };
959
915
  }
960
-
961
- /** Builder for configs with type-safe named steps. */
962
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
963
- export class ConfigBuilder<TSteps extends Record<string, AnyAction> = {}> {
964
- private readonly _steps: Record<string, Action>;
965
-
966
- constructor(steps: Record<string, Action> = {}) {
967
- this._steps = steps;
968
- }
969
-
970
- /**
971
- * Register named steps. Two forms:
972
- *
973
- * **Object form** — for steps with no cross-references:
974
- *
975
- * ```ts
976
- * .registerSteps({
977
- * Setup: setup(),
978
- * Migrate: pipe(listFiles(), forEach(migrate())),
979
- * })
980
- * ```
981
- *
982
- * **Callback form** — for mutual recursion (via `stepRef`) and/or
983
- * referencing previously registered steps (via `steps`):
984
- *
985
- * ```ts
986
- * .registerSteps({ Setup: setup() })
987
- * .registerSteps(({ steps, stepRef }) => ({
988
- * Pipeline: pipe(steps.Setup, process(), stepRef("FixCycle")),
989
- * FixCycle: loop(pipe(check(), stepRef("Pipeline"))),
990
- * }))
991
- * ```
992
- *
993
- * `stepRef` only validates against current-batch keys. Previously
994
- * registered steps must be accessed via `steps`.
995
- */
996
- registerSteps<R extends Record<string, Action>>(
997
- steps: R & ValidateStepRefs<R>,
998
- ): ConfigBuilder<TSteps & R>;
999
- registerSteps<R extends Record<string, Action>>(
1000
- build: (ctx: {
1001
- steps: StripRefs<TSteps>;
1002
- stepRef: <N extends string>(name: N) => TypedAction<any, any, N>;
1003
- }) => R,
1004
- ): [ExtractRefs<R[keyof R]>] extends [keyof R]
1005
- ? ConfigBuilder<TSteps & R>
1006
- : ValidateStepRefs<R>;
1007
- registerSteps<R extends Record<string, Action>>(
1008
- stepsOrBuild:
1009
- | (R & ValidateStepRefs<R>)
1010
- | ((ctx: {
1011
- steps: StripRefs<TSteps>;
1012
- stepRef: <N extends string>(name: N) => TypedAction<any, any, N>;
1013
- }) => R),
1014
- ): ConfigBuilder<TSteps & R> {
1015
- const resolved =
1016
- typeof stepsOrBuild === "function"
1017
- ? stepsOrBuild({ steps: this._buildStepRefs() as StripRefs<TSteps>, stepRef })
1018
- : stepsOrBuild;
1019
- return new ConfigBuilder({
1020
- ...this._steps,
1021
- ...resolved,
1022
- }) as ConfigBuilder<TSteps & R>;
1023
- }
1024
-
1025
- /** Build typed step reference objects for previously registered steps. */
1026
- private _buildStepRefs(): Record<string, Action> {
1027
- const refs: Record<string, Action> = {};
1028
- for (const name of Object.keys(this._steps)) {
1029
- refs[name] = typedAction({ kind: "Step", step: { kind: "Named", name } });
1030
- }
1031
- return refs;
1032
- }
1033
-
1034
- /**
1035
- * Define the workflow entry point.
1036
- *
1037
- * @param build - receives `{ steps, self }`.
1038
- * `self` is `TypedAction<never, never>` — a jump to the workflow
1039
- * root. Input `never` because it doesn't consume pipeline data.
1040
- * Output `never` because the execution path restarts (and `never`
1041
- * is eliminated from unions, so branches with `self` don't pollute
1042
- * the output type).
1043
- *
1044
- * Use `pipe(drop, self)` to place `self` in a branch case.
1045
- *
1046
- * Note: ideally `self` would be `TypedAction<never, Out>` so it
1047
- * carries the workflow's output type, but TypeScript can't infer
1048
- * a generic from a callback's return and use it in the same
1049
- * callback's parameter — Out falls back to `unknown`.
1050
- */
1051
- workflow<Out>(
1052
- build: (ctx: {
1053
- steps: StripRefs<TSteps>;
1054
- self: TypedAction<never, never>;
1055
- }) => WorkflowAction<Out>,
1056
- ): RunnableConfig<Out> {
1057
- const stepRefs: Record<string, Action> = {};
1058
- for (const name of Object.keys(this._steps)) {
1059
- stepRefs[name] = typedAction({ kind: "Step", step: { kind: "Named", name } });
1060
- }
1061
- const self = typedAction<never, never>({
1062
- kind: "Step",
1063
- step: { kind: "Root" },
1064
- });
1065
- const workflowAction = build({ steps: stepRefs as StripRefs<TSteps>, self });
1066
- const steps = Object.keys(this._steps).length > 0 ? this._steps : undefined;
1067
- return new RunnableConfig(workflowAction, steps);
1068
- }
1069
- }
1070
-
1071
- /**
1072
- * A workflow config with a `.run()` method for execution.
1073
- *
1074
- * Serializes to the same JSON shape as `Config` via `toJSON()`, so it
1075
- * works with `JSON.stringify` and round-trip tests.
1076
- */
1077
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1078
- export class RunnableConfig<Out = any> {
1079
- readonly workflow: WorkflowAction<Out>;
1080
- readonly steps?: Record<string, Action>;
1081
-
1082
- constructor(workflow: WorkflowAction<Out>, steps?: Record<string, Action>) {
1083
- this.workflow = workflow;
1084
- this.steps = steps;
1085
- }
1086
-
1087
- /** Run this workflow to completion. Prints result to stdout. */
1088
- async run(): Promise<void> {
1089
- // Dynamic import to avoid pulling in Node.js APIs at module load time
1090
- // (keeps ast.ts importable in non-Node environments for type checking).
1091
- const { run } = await import("./run.js");
1092
- await run(this.toJSON());
1093
- }
1094
-
1095
- /** Serialize to the same shape as Config. */
1096
- toJSON(): Config<Out> {
1097
- const result: Config<Out> = { workflow: this.workflow };
1098
- if (this.steps) {
1099
- result.steps = this.steps;
1100
- }
1101
- return result;
1102
- }
1103
- }
1104
-
1105
- export function workflowBuilder(): ConfigBuilder {
1106
- return new ConfigBuilder();
1107
- }