@boboddy/sdk 0.2.15-alpha → 0.4.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.
@@ -242,10 +242,252 @@ function makeAdvanceCtx() {
242
242
  route: (pipelineKey, inputJson) => inputJson !== undefined ? { outcome: "route", pipelineKey, inputJson } : { outcome: "route", pipelineKey }
243
243
  };
244
244
  }
245
+ // src/definitions/advancement-policies/cohort-advancement-policy.ts
246
+ var cohortAdvancementEventTypeValues = ["continue", "block"];
247
+ function serializeCohortCondition(condition) {
248
+ if (condition._tag === "signal") {
249
+ return {
250
+ fact: typeof condition.signal === "string" ? condition.signal : condition.signal.key,
251
+ operator: condition.operator,
252
+ value: condition.value
253
+ };
254
+ }
255
+ if (condition._tag === "all") {
256
+ return { all: condition.conditions.map(serializeCohortCondition) };
257
+ }
258
+ return { any: condition.conditions.map(serializeCohortCondition) };
259
+ }
260
+ function serializeCohortRule(rule) {
261
+ return {
262
+ conditions: { [rule.mode]: rule.conditions.map(serializeCohortCondition) },
263
+ event: {
264
+ type: rule.outcome,
265
+ ...rule.outcomeJson ? { params: rule.outcomeJson } : {}
266
+ }
267
+ };
268
+ }
269
+ function serializeCohortAdvancementPolicy(policy) {
270
+ if (!policy) {
271
+ return { rules: [], defaultEventType: "continue", defaultEventParamsJson: null };
272
+ }
273
+ return {
274
+ rules: (policy.rules ?? []).map(serializeCohortRule),
275
+ defaultEventType: policy.default,
276
+ defaultEventParamsJson: policy.defaultParamsJson ?? null
277
+ };
278
+ }
279
+ function visitCohortSignalConditions(conditions, visit) {
280
+ for (const c of conditions) {
281
+ if (c._tag === "signal") {
282
+ visit(c);
283
+ } else {
284
+ visitCohortSignalConditions(c.conditions, visit);
285
+ }
286
+ }
287
+ }
288
+ function isSameStepSignalsListDefinition(a, b) {
289
+ return JSON.stringify(a.ops) === JSON.stringify(b.ops) && JSON.stringify(a.reducer) === JSON.stringify(b.reducer);
290
+ }
291
+ function extractInlineStepSignalsListDefinitions(policy) {
292
+ if (!policy?.rules)
293
+ return [];
294
+ const byKey = new Map;
295
+ for (const rule of policy.rules) {
296
+ visitCohortSignalConditions(rule.conditions, (cond) => {
297
+ if (typeof cond.signal === "string")
298
+ return;
299
+ const inline = cond.signal;
300
+ const def = {
301
+ key: inline.key,
302
+ ops: inline.ops,
303
+ reducer: inline.reducer
304
+ };
305
+ const existing = byKey.get(def.key);
306
+ if (existing) {
307
+ if (!isSameStepSignalsListDefinition(existing, def)) {
308
+ throw new Error(`Conflicting inline stepSignalsList definitions for key "${def.key}"`);
309
+ }
310
+ return;
311
+ }
312
+ byKey.set(def.key, def);
313
+ });
314
+ }
315
+ return [...byKey.values()];
316
+ }
317
+ // src/definitions/advancement-policies/cohort-fluent-rules.ts
318
+ var LEAF_BRAND2 = Symbol("boboddy.cohortRule.leaf");
319
+ var GROUP_BRAND2 = Symbol("boboddy.cohortRule.group");
320
+ function createCohortSignalRef(signal2) {
321
+ const leaf = (operator, value) => {
322
+ const condition = {
323
+ _tag: "signal",
324
+ signal: signal2,
325
+ operator,
326
+ value
327
+ };
328
+ return {
329
+ [LEAF_BRAND2]: condition,
330
+ then(outcome, paramsJson) {
331
+ return {
332
+ _tag: "rule",
333
+ mode: "all",
334
+ conditions: [condition],
335
+ outcome,
336
+ ...paramsJson ? { outcomeJson: paramsJson } : {}
337
+ };
338
+ }
339
+ };
340
+ };
341
+ return {
342
+ eq: (v) => leaf("equal", v),
343
+ ne: (v) => leaf("notEqual", v),
344
+ gt: (v) => leaf("greaterThan", v),
345
+ gte: (v) => leaf("greaterThanInclusive", v),
346
+ lt: (v) => leaf("lessThan", v),
347
+ lte: (v) => leaf("lessThanInclusive", v),
348
+ in: (vs) => leaf("in", vs),
349
+ notIn: (vs) => leaf("notIn", vs),
350
+ contains: (v) => leaf("contains", v),
351
+ doesNotContain: (v) => leaf("doesNotContain", v)
352
+ };
353
+ }
354
+ function createCohortLeafFromCondition(condition) {
355
+ return {
356
+ [LEAF_BRAND2]: condition,
357
+ then(outcome, paramsJson) {
358
+ return {
359
+ _tag: "rule",
360
+ mode: "all",
361
+ conditions: [condition],
362
+ outcome,
363
+ ...paramsJson ? { outcomeJson: paramsJson } : {}
364
+ };
365
+ }
366
+ };
367
+ }
368
+ function extractCohortCondition(ref) {
369
+ if (LEAF_BRAND2 in ref)
370
+ return ref[LEAF_BRAND2];
371
+ const group = ref[GROUP_BRAND2];
372
+ return group.mode === "all" ? { _tag: "all", conditions: group.conditions } : { _tag: "any", conditions: group.conditions };
373
+ }
374
+ function createCohortGroup(mode, refs) {
375
+ const conditions = refs.map(extractCohortCondition);
376
+ return {
377
+ [GROUP_BRAND2]: { mode, conditions },
378
+ then(outcome, paramsJson) {
379
+ return {
380
+ _tag: "rule",
381
+ mode,
382
+ conditions,
383
+ outcome,
384
+ ...paramsJson ? { outcomeJson: paramsJson } : {}
385
+ };
386
+ }
387
+ };
388
+ }
389
+ function makeKeyedCohortSignalRef(key) {
390
+ return createCohortSignalRef(key);
391
+ }
392
+ function makeAdvanceEachCtx() {
393
+ return {
394
+ signal: (key) => makeKeyedCohortSignalRef(key),
395
+ stepSignals: new Proxy({}, {
396
+ get(_, key) {
397
+ if (typeof key === "string")
398
+ return makeKeyedCohortSignalRef(key);
399
+ return;
400
+ }
401
+ }),
402
+ all: (...refs) => createCohortGroup("all", refs),
403
+ any: (...refs) => createCohortGroup("any", refs)
404
+ };
405
+ }
406
+ var branchOutcomeValues = [
407
+ "continue",
408
+ "block",
409
+ "error",
410
+ "abandoned"
411
+ ];
412
+ function summarizeTransformOp(op) {
413
+ if (op.op === "filter") {
414
+ return `filter_${op.operator}_${JSON.stringify(op.value)}`;
415
+ }
416
+ if (op.op === "sortBy") {
417
+ return `sortBy_${op.direction}`;
418
+ }
419
+ return "unique";
420
+ }
421
+ function deriveStepSignalsListKey(ops, reducer) {
422
+ const pluck = ops.find((op) => op.op === "pluck");
423
+ const base = `${reducer.op}_${pluck?.signalKey ?? "value"}`;
424
+ const extras = ops.filter((op) => op.op !== "pluck").map(summarizeTransformOp);
425
+ const reducerExtra = reducer.op === "join" ? `sep_${reducer.separator}` : null;
426
+ const suffixParts = [...extras, ...reducerExtra ? [reducerExtra] : []];
427
+ return suffixParts.length > 0 ? `${base}_${suffixParts.join("_")}` : base;
428
+ }
429
+ function createStepSignalsListBuilder(ops) {
430
+ const withOp = (op) => createStepSignalsListBuilder([...ops, op]);
431
+ const reduce = (reducer) => {
432
+ const token = {
433
+ _tag: "step_signals_list",
434
+ key: deriveStepSignalsListKey(ops, reducer),
435
+ ops: [...ops],
436
+ reducer
437
+ };
438
+ return createCohortSignalRef(token);
439
+ };
440
+ return {
441
+ filter: (operator, value) => withOp({ op: "filter", operator, value }),
442
+ sortBy: (direction = "asc") => withOp({ op: "sortBy", direction }),
443
+ unique: () => withOp({ op: "unique" }),
444
+ count: () => reduce({ op: "count" }),
445
+ sum: () => reduce({ op: "sum" }),
446
+ avg: () => reduce({ op: "avg" }),
447
+ min: () => reduce({ op: "min" }),
448
+ max: () => reduce({ op: "max" }),
449
+ booleanAll: () => reduce({ op: "booleanAll" }),
450
+ booleanAny: () => reduce({ op: "booleanAny" }),
451
+ join: (separator = ",") => reduce({ op: "join", separator }),
452
+ first: () => reduce({ op: "first" }),
453
+ last: () => reduce({ op: "last" })
454
+ };
455
+ }
456
+ function makeAdvanceAllCtx() {
457
+ return {
458
+ branchOutcomes: {
459
+ total: () => createCohortSignalRef("branchCount"),
460
+ count: (outcome) => createCohortSignalRef(`${outcome}Count`),
461
+ every: (outcome) => createCohortLeafFromCondition({
462
+ _tag: "signal",
463
+ signal: `${outcome}Count`,
464
+ operator: "equal",
465
+ value: { fact: "branchCount" }
466
+ }),
467
+ some: (outcome) => createCohortLeafFromCondition({
468
+ _tag: "signal",
469
+ signal: `${outcome}Count`,
470
+ operator: "greaterThan",
471
+ value: 0
472
+ })
473
+ },
474
+ stepSignalsList: {
475
+ pluck: (signalKey) => createStepSignalsListBuilder([{ op: "pluck", signalKey }])
476
+ },
477
+ all: (...refs) => createCohortGroup("all", refs),
478
+ any: (...refs) => createCohortGroup("any", refs)
479
+ };
480
+ }
245
481
  export {
482
+ serializeCohortAdvancementPolicy,
246
483
  serializeAdvancementPolicy,
484
+ makeAdvanceEachCtx,
247
485
  makeAdvanceCtx,
486
+ makeAdvanceAllCtx,
487
+ extractInlineStepSignalsListDefinitions,
248
488
  extractInlineComputedSignals,
489
+ cohortAdvancementEventTypeValues,
490
+ branchOutcomeValues,
249
491
  Rule,
250
492
  Computed
251
493
  };
@@ -1,12 +1,35 @@
1
1
  import { z, type ZodType } from "zod";
2
2
  import { type AdditionalStepInputBinding, type TypedStepDefinitionSpec } from "../steps/define-step";
3
- import { type AnyBinding, type LiteralBinding, type StepOutputBinding, type StepSignalBinding, type WorkItemBinding } from "./define-pipeline";
3
+ import { type AnyBinding, type FanOutItemBinding, type LiteralBinding, type SignalsListBinding, type StepOutputBinding, type StepSignalBinding, type WorkItemBinding } from "./define-pipeline";
4
4
  import { type InputAccessor } from "./input-accessor";
5
5
  export type AnyTypedStep = TypedStepDefinitionSpec<any, any, any, any>;
6
- export type StepConfig = {
7
- timeout?: number | null;
8
- };
9
6
  type ElementOf<T extends ReadonlyArray<unknown>> = T extends ReadonlyArray<infer U> ? U : never;
7
+ export type LastStep<T extends ReadonlyArray<AnyTypedStep>> = T extends readonly [...AnyTypedStep[], infer L] ? L extends AnyTypedStep ? L : never : never;
8
+ export type LastSignalKeys<T extends ReadonlyArray<AnyTypedStep>> = LastStep<T> extends AnyTypedStep ? LastStep<T>["__signalKeys"] : never;
9
+ /**
10
+ * The per-branch `item` type a fan-out's `over` key resolves to (issue
11
+ * #167): `never` unless `K` names a signal on the most recent ordinary step
12
+ * (`LastStep<TSteps>`) whose resolved TS type is itself an array — in which
13
+ * case this is that array's element type. A number-typed signal (count-only
14
+ * mode) resolves to `never`, which is how `FanOutInputCtx` below decides
15
+ * whether `item` exists on the ctx type at all.
16
+ */
17
+ export type FanOutItemType<TSteps extends ReadonlyArray<AnyTypedStep>, K extends string> = K extends keyof LastStep<TSteps>["__signalTypeMap"] ? LastStep<TSteps>["__signalTypeMap"][K] extends ReadonlyArray<infer Item> ? Item : never : never;
18
+ /**
19
+ * A fan-out's own `input` mapper ctx: everything `StepInputCtx` already
20
+ * offers, plus `item` when `FanOutItemType` resolves to something other
21
+ * than `never`. `item`'s exposed type is intersected with `FanOutItemBinding`
22
+ * (the same "phantom binding" trick `WithWorkItemFields`/`InputAccessor`
23
+ * use elsewhere in this file) so it reads as the real per-item TS type
24
+ * (e.g. `string`) to callers while still structurally satisfying `AnyBinding`
25
+ * when assigned straight into a `FanOutInputMapping` field — at runtime it
26
+ * is always the single `{ source: "fan_out_item" }` binding object,
27
+ * regardless of `Item`'s shape.
28
+ */
29
+ export type FanOutInputCtx<TInput extends ZodType, TSteps extends ReadonlyArray<AnyTypedStep>, TFanOuts extends ReadonlyArray<AnyTypedStep>, K extends string> = StepInputCtx<TInput, TSteps, TFanOuts> & (FanOutItemType<TSteps, K> extends never ? unknown : {
30
+ item: FanOutItemBinding & FanOutItemType<TSteps, K>;
31
+ });
32
+ export type IsAny<T> = 0 extends 1 & T ? true : false;
10
33
  export type WorkItemAccessor = {
11
34
  readonly title: WorkItemBinding;
12
35
  readonly description: WorkItemBinding;
@@ -21,20 +44,28 @@ export type WithWorkItemFields<T> = {
21
44
  workItemDescription: string | null;
22
45
  workItemComments: PinnedWorkItemComment[];
23
46
  } & T;
24
- type RequiredInputKeys<T extends object> = {
47
+ export type RequiredInputKeys<T extends object> = {
25
48
  [K in keyof T & string]-?: undefined extends T[K] ? never : K;
26
49
  }[keyof T & string];
27
- type OptionalInputKeys<T extends object> = {
50
+ export type OptionalInputKeys<T extends object> = {
28
51
  [K in keyof T & string]-?: undefined extends T[K] ? K : never;
29
52
  }[keyof T & string];
30
53
  type Prettify<T> = {
31
54
  [K in keyof T]: T[K];
32
55
  } & {};
33
- export type StepInputCtx<TInput extends ZodType, TSteps extends ReadonlyArray<AnyTypedStep>> = {
56
+ export type StepInputCtx<TInput extends ZodType, TSteps extends ReadonlyArray<AnyTypedStep>, TFanOuts extends ReadonlyArray<AnyTypedStep> = []> = {
34
57
  input: InputAccessor<Prettify<WithWorkItemFields<TInput["_output"]>>>;
35
58
  signal: <S extends ElementOf<TSteps>>(step: S, key: S["__signalKeys"]) => StepSignalBinding;
36
59
  output: (step: ElementOf<TSteps>) => StepOutputBinding;
37
60
  literal: (value: unknown) => LiteralBinding;
61
+ /**
62
+ * Reaches a fan-out's whole cohort — every terminal branch's own signals
63
+ * + output, resolved server-side — from a later, non-adjacent step's
64
+ * input mapper (issue #167). `fanOutStep` is constrained to a fan-out
65
+ * step already seen earlier in this pipeline (`.fanOutStep(fanOutStep, ...)`),
66
+ * the same way `signal`/`output` are constrained to `TSteps`.
67
+ */
68
+ signalsList: (fanOutStep: ElementOf<TFanOuts>) => SignalsListBinding;
38
69
  };
39
70
  type ReservedPipelineInputKeys = "workItemTitle" | "workItemDescription" | "workItemComments";
40
71
  export type NoReservedKeys<T extends ZodType> = T extends {
@@ -66,7 +97,7 @@ export type PipelineMeta<TInput extends ZodType = z.ZodUnknown> = {
66
97
  };
67
98
  };
68
99
  export declare const WORK_ITEM_ACCESSOR: WorkItemAccessor;
69
- export declare function makeStepInputCtx<TInput extends ZodType>(inputSchema: TInput): StepInputCtx<TInput, ReadonlyArray<AnyTypedStep>>;
100
+ export declare function makeStepInputCtx<TInput extends ZodType>(inputSchema: TInput): StepInputCtx<TInput, ReadonlyArray<AnyTypedStep>, ReadonlyArray<AnyTypedStep>>;
70
101
  export declare function literal(value: unknown): LiteralBinding;
71
102
  export declare function normalizeInputMapping(mapping: Record<string, AnyBinding | undefined> | undefined): Record<string, AnyBinding> | undefined;
72
103
  export declare function resolveAdditionalStepInputBindings(label: "additionalStepInput", definition: PipelineMeta["additionalStepInput"] | undefined): Record<string, AnyBinding>;
@@ -1,74 +1,80 @@
1
1
  import { z, type ZodType } from "zod";
2
2
  import { type AdvanceCtx, type AdvanceResult } from "../advancement-policies/fluent-rules";
3
- import { type AnyBinding, type PipelineDefinitionSpec, type PipelineStepConfig } from "./define-pipeline";
4
- import { type AnyTypedStep, type PipelineMeta, type StepConfig, type StepInputCtx } from "./builder-helpers";
5
- export type { AnyTypedStep, PipelineMeta, StepConfig, StepInputCtx, WorkItemAccessor, } from "./builder-helpers";
6
- type LastStep<T extends ReadonlyArray<AnyTypedStep>> = T extends readonly [
7
- ...AnyTypedStep[],
8
- infer L
9
- ] ? L extends AnyTypedStep ? L : never : never;
10
- type LastSignalKeys<T extends ReadonlyArray<AnyTypedStep>> = LastStep<T> extends AnyTypedStep ? LastStep<T>["__signalKeys"] : never;
11
- type LastSignalTypeMap<T extends ReadonlyArray<AnyTypedStep>> = LastStep<T> extends AnyTypedStep ? LastStep<T>["__signalTypeMap"] : Record<string, unknown>;
12
- type IsAny<T> = 0 extends 1 & T ? true : false;
13
- type RequiredInputKeys<T extends object> = {
14
- [K in keyof T & string]-?: undefined extends T[K] ? never : K;
15
- }[keyof T & string];
16
- type OptionalInputKeys<T extends object> = {
17
- [K in keyof T & string]-?: undefined extends T[K] ? K : never;
18
- }[keyof T & string];
3
+ import { type AnyBinding, type PipelineDefinitionSpec, type PipelineNodeConfig } from "./define-pipeline";
4
+ import { type AnyTypedStep, type IsAny, type LastSignalKeys, type OptionalInputKeys, type PipelineMeta, type RequiredInputKeys, type StepInputCtx } from "./builder-helpers";
5
+ import { type FanOutStepConfig } from "./fan-out-builder";
6
+ export type { AnyTypedStep, PipelineMeta, StepInputCtx, WorkItemAccessor, } from "./builder-helpers";
7
+ export { type FanOutStepConfig, type FanOutInputMapping, } from "./fan-out-builder";
19
8
  type StepInputMapping<S extends AnyTypedStep> = IsAny<S["__inputType"]> extends true ? Partial<Record<string, AnyBinding>> : S["__inputType"] extends object ? {
20
9
  [K in RequiredInputKeys<S["__inputType"]>]: AnyBinding;
21
10
  } & {
22
11
  [K in OptionalInputKeys<S["__inputType"]>]?: AnyBinding;
23
12
  } : Partial<Record<string, AnyBinding>>;
24
13
  /**
25
- * Returned by `.step()`. Requires `.advance()` before the pipeline can
26
- * continue. Also accepts `.timeout()` before `.advance()`.
14
+ * `.step()`'s single options argument. Deliberately a single generic type
15
+ * (not a set of overload signatures split on `S["__hasAdditionalInput"]`):
16
+ * with overloads, a mistake inside `options.input`'s return value fails
17
+ * every overload, and TS reports "no overload matches" against the whole
18
+ * call rather than pointing at the specific missing/wrong property inside
19
+ * `input`'s return type. A single signature lets TS check `options`
20
+ * structurally in one pass and localize the error correctly.
27
21
  */
28
- export declare class PipelineStepAdvancementBuilder<TInput extends ZodType, TSteps extends ReadonlyArray<AnyTypedStep>> {
29
- protected readonly inputSchema: TInput;
30
- protected readonly meta: Omit<PipelineMeta<TInput>, "additionalPipelineInput" | "additionalStepInput">;
31
- protected readonly steps: PipelineStepConfig[];
32
- protected readonly pipelineInputBindings: Record<string, AnyBinding>;
33
- protected readonly pipelineStepInputBindings: Record<string, AnyBinding>;
34
- readonly __steps: TSteps;
35
- constructor(inputSchema: TInput, meta: Omit<PipelineMeta<TInput>, "additionalPipelineInput" | "additionalStepInput">, steps: PipelineStepConfig[], pipelineInputBindings?: Record<string, AnyBinding>, pipelineStepInputBindings?: Record<string, AnyBinding>);
36
- advance(callback: (ctx: AdvanceCtx<LastSignalKeys<TSteps>, LastSignalTypeMap<TSteps>>) => AdvanceResult<LastSignalKeys<TSteps>>): PipelineStepBuilder<TInput, TSteps>;
37
- }
22
+ type StepOptions<TInput extends ZodType, TSteps extends ReadonlyArray<AnyTypedStep>, TFanOuts extends ReadonlyArray<AnyTypedStep>, S extends AnyTypedStep> = (S extends {
23
+ __hasAdditionalInput: false;
24
+ } ? {
25
+ input?: (ctx: StepInputCtx<TInput, TSteps, TFanOuts>) => Partial<Record<string, AnyBinding>>;
26
+ } : {
27
+ input: (ctx: StepInputCtx<TInput, TSteps, TFanOuts>) => StepInputMapping<S>;
28
+ }) & {
29
+ advance: (ctx: AdvanceCtx<S["__signalKeys"], S["__signalTypeMap"]>) => AdvanceResult<S["__signalKeys"]>;
30
+ timeout?: number | null;
31
+ };
38
32
  /**
39
- * Returned by `.advance()`. Provides `.step()` to chain additional steps,
40
- * `.timeout()` to set timeout after advancing, and `.build()` to finalize.
33
+ * Returned by `.step()`/`pipeline()`. Provides `.step()` to chain the next
34
+ * step, `.fanOutStep()` to begin a fan-out+cohort-gate pair (issue #167),
35
+ * and `.build()` to finalize. `.step()` requires an `advance` callback in
36
+ * its options — deciding how the pipeline continues past this step — as
37
+ * part of the same call that declares the step's input, rather than as a
38
+ * separate chained method on an intermediate builder class.
41
39
  */
42
- export declare class PipelineStepBuilder<TInput extends ZodType, TSteps extends ReadonlyArray<AnyTypedStep>> {
40
+ export declare class PipelineStepBuilder<TInput extends ZodType, TSteps extends ReadonlyArray<AnyTypedStep>, TFanOuts extends ReadonlyArray<AnyTypedStep> = []> {
43
41
  protected readonly inputSchema: TInput;
44
42
  protected readonly meta: Omit<PipelineMeta<TInput>, "additionalPipelineInput" | "additionalStepInput">;
45
- protected readonly steps: PipelineStepConfig[];
43
+ protected readonly nodes: PipelineNodeConfig[];
46
44
  protected readonly pipelineInputBindings: Record<string, AnyBinding>;
47
45
  protected readonly pipelineStepInputBindings: Record<string, AnyBinding>;
48
46
  readonly __steps: TSteps;
49
- constructor(inputSchema: TInput, meta: Omit<PipelineMeta<TInput>, "additionalPipelineInput" | "additionalStepInput">, steps: PipelineStepConfig[], pipelineInputBindings?: Record<string, AnyBinding>, pipelineStepInputBindings?: Record<string, AnyBinding>);
50
- step<S extends AnyTypedStep & {
51
- __hasAdditionalInput: false;
52
- }>(step: S, mapper?: (ctx: StepInputCtx<TInput, TSteps>) => Partial<Record<string, AnyBinding>>, configFn?: (config: StepConfig) => void): PipelineStepAdvancementBuilder<TInput, [...TSteps, S]>;
53
- step<S extends AnyTypedStep>(step: S, mapper: (ctx: StepInputCtx<TInput, TSteps>) => StepInputMapping<S>, configFn?: (config: StepConfig) => void): PipelineStepAdvancementBuilder<TInput, [...TSteps, S]>;
47
+ readonly __fanOuts: TFanOuts;
48
+ constructor(inputSchema: TInput, meta: Omit<PipelineMeta<TInput>, "additionalPipelineInput" | "additionalStepInput">, nodes: PipelineNodeConfig[], pipelineInputBindings?: Record<string, AnyBinding>, pipelineStepInputBindings?: Record<string, AnyBinding>);
49
+ step<S extends AnyTypedStep>(step: S, options: StepOptions<TInput, TSteps, TFanOuts, S>): PipelineStepBuilder<TInput, [...TSteps, S], TFanOuts>;
50
+ /**
51
+ * Begins a fan-out+cohort-gate pair (issue #167): `step` is the template
52
+ * every branch executes, with its branch count (and, when `over` names
53
+ * an array-typed signal, each branch's own typed `item`) resolved at
54
+ * runtime from `config.over` (a signal on the step immediately
55
+ * preceding this fan-out). `config` requires both `advance` (each
56
+ * branch's own continue/block decision) and `advanceAll` (the
57
+ * whole-cohort decision — a pure gate, not a step; nothing besides the
58
+ * fan-out+gate pair itself is appended to the pipeline's node sequence)
59
+ * up front, mirroring how `.step()` requires `advance` in its own
60
+ * options rather than as a separate chained call.
61
+ */
62
+ fanOutStep<S extends AnyTypedStep, K extends LastSignalKeys<TSteps>>(step: S, config: FanOutStepConfig<TInput, TSteps, TFanOuts, S, K>): PipelineStepBuilder<TInput, TSteps, [...TFanOuts, S]>;
54
63
  build(): PipelineDefinitionSpec;
55
64
  }
56
65
  /**
57
66
  * Entry-point builder returned by `pipeline()`. Only exposes `.step()` —
58
- * call that to receive a `PipelineStepAdvancementBuilder` which requires
59
- * `.advance()` before the pipeline can proceed.
67
+ * call that to receive a `PipelineStepBuilder`, which chains further
68
+ * `.step()`/`.fanOutStep()` calls or finalizes with `.build()`.
60
69
  */
61
70
  export declare class PipelineBuilder<TInput extends ZodType> {
62
71
  private readonly inputSchema;
63
72
  private readonly meta;
64
- private readonly steps;
73
+ private readonly nodes;
65
74
  private readonly pipelineInputBindings;
66
75
  private readonly pipelineStepInputBindings;
67
76
  constructor(meta: PipelineMeta<TInput>);
68
- step<S extends AnyTypedStep & {
69
- __hasAdditionalInput: false;
70
- }>(step: S, mapper?: (ctx: StepInputCtx<TInput, []>) => Partial<Record<string, AnyBinding>>, configFn?: (config: StepConfig) => void): PipelineStepAdvancementBuilder<TInput, [S]>;
71
- step<S extends AnyTypedStep>(step: S, mapper: (ctx: StepInputCtx<TInput, []>) => StepInputMapping<S>, configFn?: (config: StepConfig) => void): PipelineStepAdvancementBuilder<TInput, [S]>;
77
+ step<S extends AnyTypedStep>(step: S, options: StepOptions<TInput, [], [], S>): PipelineStepBuilder<TInput, [S]>;
72
78
  }
73
79
  export { literal } from "./builder-helpers";
74
80
  export declare function pipeline<TInput extends ZodType = z.ZodUnknown>(meta: PipelineMeta<TInput>): PipelineBuilder<TInput>;
@@ -0,0 +1,19 @@
1
+ import type { DependencyEdgeSpec, NodeDefinitionSpec } from "./define-pipeline";
2
+ /**
3
+ * Orders `nodeDefinitions` by walking `dependencyEdges` from the single root
4
+ * to the single leaf. Returns `null` (does not throw) on any structural
5
+ * problem: multiple roots, a node with more than one outgoing edge, a node
6
+ * with more than one incoming edge, a cycle, or a disconnected node.
7
+ */
8
+ export declare function tryOrderChainNodeDefinitions(nodeDefinitions: readonly NodeDefinitionSpec[], dependencyEdges: readonly DependencyEdgeSpec[]): NodeDefinitionSpec[] | null;
9
+ /**
10
+ * Builds one dependency edge between each consecutive pair of `orderedNodes`,
11
+ * in the order given. Used to synthesize a chain's edges from an already-known
12
+ * author order (e.g. declaration order in `.step()` calls).
13
+ */
14
+ export declare function buildChainDependencyEdges(orderedNodes: readonly Pick<NodeDefinitionSpec, "nodeKey">[]): DependencyEdgeSpec[];
15
+ /**
16
+ * `tryOrderChainNodeDefinitions`, but throws a descriptive error instead of
17
+ * returning `null` when the graph isn't a single valid chain.
18
+ */
19
+ export declare function orderChainNodeDefinitions(nodeDefinitions: readonly NodeDefinitionSpec[], dependencyEdges: readonly DependencyEdgeSpec[]): NodeDefinitionSpec[];
@@ -185,10 +185,10 @@ type SerializedAssignmentRule = {
185
185
  export type SerializedDefaultPipelineAssignment = {
186
186
  /**
187
187
  * Pipeline key for the primary assign pipeline; resolved to a
188
- * `linearPipelineDefinitionId` by the push layer. Null when `default`
188
+ * `pipelineDefinitionId` by the push layer. Null when `default`
189
189
  * is `skip()` and no rule assigns a pipeline (push will reject this).
190
190
  */
191
- linearPipelineDefinitionKey: string;
191
+ pipelineDefinitionKey: string;
192
192
  rulesJson: {
193
193
  rules: SerializedAssignmentRule[];
194
194
  };
@@ -199,7 +199,7 @@ export type SerializedDefaultPipelineAssignment = {
199
199
  /**
200
200
  * Serialize a `DefaultPipelineAssignmentSpec` to the wire format.
201
201
  *
202
- * `linearPipelineDefinitionKey` is the key of the primary assign pipeline —
202
+ * `pipelineDefinitionKey` is the key of the primary assign pipeline —
203
203
  * taken from `default` if it's `assign(...)`, otherwise from the first rule
204
204
  * that assigns a pipeline. The push layer rejects specs with no assign outcome.
205
205
  */
@@ -1,6 +1,7 @@
1
1
  import { type ZodType } from "zod";
2
2
  import type { StepDefinitionSpec, TypedStepDefinitionSpec } from "../steps/define-step";
3
3
  import { type AdvancementPolicy, type SerializedAdvancementPolicy, type SerializedComputedSignalDefinition } from "../advancement-policies/define-advancement-policy";
4
+ import { type CohortAdvancementPolicy, type SerializedCohortAdvancementPolicy, type SerializedStepSignalsListDefinition } from "../advancement-policies/cohort-advancement-policy";
4
5
  export type { AdvancementPolicy, PipelineStepComputedSignalType, } from "../advancement-policies/define-advancement-policy";
5
6
  export { Computed, Rule, } from "../advancement-policies/define-advancement-policy";
6
7
  type AnyTypedStep = TypedStepDefinitionSpec<any, any, any, any>;
@@ -25,7 +26,31 @@ export type LiteralBinding = {
25
26
  source: "literal";
26
27
  value: unknown;
27
28
  };
28
- export type AnyBinding = PipelineInputBinding | WorkItemBinding | StepSignalBinding | StepOutputBinding | LiteralBinding;
29
+ /**
30
+ * `ctx.signalsList(fanOutStep)`'s binding (issue #167): reaches a fan-out's
31
+ * whole cohort — every terminal branch's own signals + output — from a
32
+ * later, non-adjacent step's input mapper. Resolved server-side against
33
+ * `ResolvedNodeInputContext.cohorts[stepKey]` (an array of
34
+ * `{ branchIndex, signals, outputJson }`, sorted by `branchIndex`).
35
+ */
36
+ export type SignalsListBinding = {
37
+ source: "signals_list";
38
+ fanOutStep: AnyTypedStep;
39
+ };
40
+ /**
41
+ * `.fanOutStep(step, config)`'s own `input` ctx's `item` binding (issue
42
+ * #167): resolves server-side, per branch, to that branch's own item value
43
+ * — the element of the array `config.over` names, when `over` resolves to
44
+ * an array (count-only mode has no `item` to bind, both at the type level
45
+ * — see `FanOutItemType` — and at the wire level, since no `fanOut` node
46
+ * config would carry an `item` binding for it). Carries no extra fields:
47
+ * the branch index alone (implicit in which branch is executing) is enough
48
+ * to resolve the right element server-side.
49
+ */
50
+ export type FanOutItemBinding = {
51
+ source: "fan_out_item";
52
+ };
53
+ export type AnyBinding = PipelineInputBinding | WorkItemBinding | StepSignalBinding | StepOutputBinding | LiteralBinding | SignalsListBinding | FanOutItemBinding;
29
54
  export type PipelineStepConfig<TStep extends AnyTypedStep = AnyTypedStep> = {
30
55
  step: TStep;
31
56
  /** Maps each step input field to an input source. Extra keys are ignored at runtime. */
@@ -43,7 +68,50 @@ export type PipelineStepConfig<TStep extends AnyTypedStep = AnyTypedStep> = {
43
68
  */
44
69
  advancement?: AdvancementPolicy<TStep["__signalKeys"]>;
45
70
  };
46
- type SerializedBinding = {
71
+ /**
72
+ * `.fanOutStep(step, config)`'s node config (issue #167): a `fanOut` node
73
+ * whose `stepDefinitionId`/`stepDefinitionVersion` template is `fanOutStep`
74
+ * — the template every branch executes — and whose `advanceEach` policy
75
+ * each branch's own result is evaluated against. Pushes exactly one
76
+ * `fanOut` node onto the pipeline's node sequence (paired with exactly one
77
+ * `PipelineCohortGateNodeConfig` immediately after it — see `.advanceAll()`
78
+ * — never more than the fan-out+gate pair itself).
79
+ *
80
+ * Named `*StepConfig` (rather than a bare `PipelineFanOutConfig`) to reserve
81
+ * room for a future sibling `PipelineFanOutSubPipelineConfig` — a fan-out
82
+ * whose template is a whole sub-pipeline rather than a single step. That
83
+ * sibling is out of scope for issue #167 and is not implemented here.
84
+ */
85
+ export type PipelineFanOutStepConfig<TStep extends AnyTypedStep = AnyTypedStep> = {
86
+ nodeType: "fanOut";
87
+ fanOutStep: TStep;
88
+ overSignalKey: string;
89
+ input?: Partial<{
90
+ [K in keyof NonNullable<TStep["__inputType"]> & string]: AnyBinding;
91
+ }>;
92
+ timeout?: number | null;
93
+ advanceEach?: CohortAdvancementPolicy;
94
+ };
95
+ /**
96
+ * `.advanceAll(callback)`'s node config (issue #167): the pure decision
97
+ * gate that aggregates a fan-out's cohort back together — no work of its
98
+ * own, so no `step`/`input`/`timeout`. `nodeKey` is derived by the builder
99
+ * (`${fanOutStep.key}__cohortGate`), not user-supplied.
100
+ */
101
+ export type PipelineCohortGateNodeConfig = {
102
+ nodeType: "cohortGate";
103
+ nodeKey: string;
104
+ advanceAll?: CohortAdvancementPolicy;
105
+ stepSignalsListDefinitions?: SerializedStepSignalsListDefinition[];
106
+ };
107
+ /**
108
+ * A single entry in a pipeline's declaration-order node sequence: an
109
+ * ordinary step, or one half of a fan-out+cohort-gate pair. Discriminated
110
+ * by the presence/value of `nodeType` (absent means a plain step) rather
111
+ * than a `kind` field, so `PipelineStepConfig` itself needs no change.
112
+ */
113
+ export type PipelineNodeConfig = PipelineStepConfig | PipelineFanOutStepConfig | PipelineCohortGateNodeConfig;
114
+ export type SerializedBinding = {
47
115
  source: "pipeline_input";
48
116
  path: string;
49
117
  } | {
@@ -59,6 +127,39 @@ type SerializedBinding = {
59
127
  } | {
60
128
  source: "literal";
61
129
  value: unknown;
130
+ } | {
131
+ source: "signals_list";
132
+ stepKey: string;
133
+ } | {
134
+ source: "fan_out_item";
135
+ };
136
+ export type NodeDefinitionKind = "step" | "fanOut" | "cohortGate";
137
+ export type NodeDefinitionSpec = {
138
+ /** Unique within the pipeline; equals `stepKey` for a step/fanOut node, or the builder-derived gate key for a cohortGate node. */
139
+ nodeKey: string;
140
+ kind: NodeDefinitionKind;
141
+ /** `step`/`fanOut` only — the step template's key/version. Absent on a `cohortGate` node (it produces no work of its own). */
142
+ stepKey?: string;
143
+ stepName?: string;
144
+ stepDescription?: string | null;
145
+ inputBindingsJson?: Record<string, SerializedBinding>;
146
+ timeoutSeconds?: number | null;
147
+ /** `step` only. */
148
+ advancementPolicyDefinition?: SerializedAdvancementPolicy;
149
+ /** `step` only. */
150
+ computedSignalDefinitions?: SerializedComputedSignalDefinition[];
151
+ /** `fanOut` only — the signal its branch cardinality is resolved from. */
152
+ overSignalKey?: string;
153
+ /** `fanOut` only — each branch's own continue/block decision. */
154
+ advanceEachPolicyDefinition?: SerializedCohortAdvancementPolicy;
155
+ /** `cohortGate` only — the whole cohort's continue/block decision. */
156
+ advanceAllPolicyDefinition?: SerializedCohortAdvancementPolicy;
157
+ /** `cohortGate` only — every `ctx.stepSignalsList`-derived value `advanceAll`'s rules may reference. */
158
+ stepSignalsListDefinitions?: SerializedStepSignalsListDefinition[];
159
+ };
160
+ export type DependencyEdgeSpec = {
161
+ fromNodeKey: string;
162
+ toNodeKey: string;
62
163
  };
63
164
  export type PipelineDefinitionSpec = {
64
165
  key: string;
@@ -67,16 +168,8 @@ export type PipelineDefinitionSpec = {
67
168
  version: number;
68
169
  status: "draft" | "active" | "archived";
69
170
  inputSchemaJson?: Record<string, unknown> | null;
70
- steps: Array<{
71
- stepKey: string;
72
- stepName: string;
73
- stepDescription: string | null;
74
- position: number;
75
- inputBindingsJson: Record<string, SerializedBinding>;
76
- timeoutSeconds: number | null;
77
- advancementPolicyDefinition: SerializedAdvancementPolicy;
78
- computedSignalDefinitions: SerializedComputedSignalDefinition[];
79
- }>;
171
+ nodeDefinitions: NodeDefinitionSpec[];
172
+ dependencyEdges: DependencyEdgeSpec[];
80
173
  /** Step specs referenced by this pipeline. Used by the push command to auto-push steps that aren't explicitly exported. */
81
174
  _stepDefinitions?: StepDefinitionSpec[];
82
175
  };
@@ -87,7 +180,7 @@ export type DefinePipelineInput = {
87
180
  version?: number;
88
181
  status?: "draft" | "active";
89
182
  input?: ZodType | null;
90
- steps: ReadonlyArray<PipelineStepConfig>;
183
+ nodes: ReadonlyArray<PipelineNodeConfig>;
91
184
  pipelineInputBindings?: Record<string, AnyBinding>;
92
185
  };
93
186
  export declare function buildPipelineSpec(config: DefinePipelineInput): PipelineDefinitionSpec;