tubeless 0.2.6 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +3 -3
  2. package/dist/core/pipeline-steps.d.ts +61 -77
  3. package/dist/core/pipeline-steps.js +25 -44
  4. package/dist/core/pipeline-steps.js.map +1 -1
  5. package/dist/project/project-compiler.js +1 -1
  6. package/dist/project/project-compiler.js.map +1 -1
  7. package/docs/agent-guide.md +9 -7
  8. package/docs/api-reference.md +4 -4
  9. package/docs/api-report.json +4 -4
  10. package/docs/child-pipeline-composition.md +24 -19
  11. package/docs/concepts.md +13 -11
  12. package/docs/getting-started.md +3 -3
  13. package/docs/recipes.md +5 -5
  14. package/docs/remote-step-composition.md +4 -3
  15. package/examples/best-effort.ts +1 -1
  16. package/examples/cancellation-and-testing.ts +1 -1
  17. package/examples/catalog/pipelines/enrich.ts +3 -3
  18. package/examples/catalog/pipelines/import.ts +2 -2
  19. package/examples/catalog/pipelines/normalize.ts +1 -1
  20. package/examples/catalog/pipelines/publish.ts +1 -1
  21. package/examples/catalog/tubeless.project.ts +2 -0
  22. package/examples/child-pipeline.ts +4 -4
  23. package/examples/cli-job.ts +1 -1
  24. package/examples/conditional-step.ts +2 -2
  25. package/examples/fan-out-progress.ts +3 -3
  26. package/examples/host-embedding.ts +1 -1
  27. package/examples/live-tui.ts +1 -1
  28. package/examples/node-artifacts.ts +1 -1
  29. package/examples/peloton.ts +9 -6
  30. package/examples/publish-with-gates.ts +1 -1
  31. package/examples/remote-steps.ts +2 -2
  32. package/examples/resumable-enrichment.ts +1 -1
  33. package/examples/tracing.ts +1 -1
  34. package/examples/typed-import.ts +1 -1
  35. package/examples/validated-boundaries.ts +1 -1
  36. package/package.json +1 -1
  37. package/skills/tubeless/SKILL.md +6 -4
  38. package/skills/tubeless-make-pipeline/SKILL.md +5 -4
@@ -69,7 +69,7 @@
69
69
  "StepFactory",
70
70
  "StepSkipDecision"
71
71
  ],
72
- "sha256": "abb2f1fca6d2e21eda1bc1cf72921fee3110f474660834202d223f805300c8d9",
72
+ "sha256": "84b22a259773f3dcfeb91a3bae96ccef79e4279664946faa25d0219778ef6fd2",
73
73
  "specifier": "tubeless"
74
74
  },
75
75
  {
@@ -109,7 +109,7 @@
109
109
  "ReporterTerminalCapabilities",
110
110
  "RunReporterConfig"
111
111
  ],
112
- "sha256": "996eeba686bb2a3124c21f30e4c5b5d36873c1a30cbba89ef2b9629206feef48",
112
+ "sha256": "fe37491a52df175b666ebffc497f8afea9ad94aa2bb97841b5e7f22e1b71a04c",
113
113
  "specifier": "tubeless/cli"
114
114
  },
115
115
  {
@@ -171,7 +171,7 @@
171
171
  "PipelineProjectManifestInput",
172
172
  "validatePipelineDocument"
173
173
  ],
174
- "sha256": "464f5d94a3533c4050c32d902badafa4fabfdec176152bb36bb738cf52ce6e81",
174
+ "sha256": "316af8da2ed6a5708004ed3d52fb2311a81e19789796bdc926a5902764472a72",
175
175
  "specifier": "tubeless/project"
176
176
  },
177
177
  {
@@ -185,7 +185,7 @@
185
185
  "PipelineTestRuntimeOptions",
186
186
  "PipelineTestSleep"
187
187
  ],
188
- "sha256": "9510932b8aec600ecf2bcfdb1a188257dad48a3108087c974689c6e6ec3ad5c9",
188
+ "sha256": "6eb6f55eaa2bb1b75a1085dafd5e136dc5d76566dfe405a60b40942a5e40b958",
189
189
  "specifier": "tubeless/testing"
190
190
  },
191
191
  {
@@ -3,10 +3,10 @@
3
3
  Use a child pipeline when part of a workflow is useful on its own and should
4
4
  also run inside a larger pipeline. Tubeless provides two step builders:
5
5
 
6
- | Builder | Use it to | Step output |
7
- | ---------------------- | ----------------------------------------------- | ---------------------------------------- |
8
- | `step.fromPipeline` | Run one child pipeline | The child's final result |
9
- | `step.forEachPipeline` | Run the same child pipeline for a list of items | An array of child results in input order |
6
+ | Builder | Use it to | Step output |
7
+ | ----------------- | ----------------------------------------------- | ---------------------------------------- |
8
+ | `fromPipeline` | Run one child pipeline | The child's final result |
9
+ | `forEachPipeline` | Run the same child pipeline for a list of items | An array of child results in input order |
10
10
 
11
11
  Both builders infer the child options and result types. They forward the
12
12
  parent's runtime context and report child progress through the parent step.
@@ -20,7 +20,9 @@ parent step's dependency outputs and context, and returns the inputs the child
20
20
  needs. Add `mapResult` if the parent needs a different result shape.
21
21
 
22
22
  ```ts
23
- const seedIndexStage = step.fromPipeline("seed-index", {
23
+ const { fromPipeline } = createSteps<ParentOptions>();
24
+
25
+ const seedIndexStage = fromPipeline("seed-index", {
24
26
  pipeline: IndexSeedPipeline,
25
27
  dependsOn: [seedCatalogStage],
26
28
  description: "Seed a precomputed search index",
@@ -38,7 +40,9 @@ Use `items` to return the list to process, `key` for stable item IDs, and
38
40
  `concurrency` to limit simultaneous child runs:
39
41
 
40
42
  ```ts
41
- const processShards = step.forEachPipeline("process-shards", {
43
+ const { forEachPipeline } = createSteps<ParentOptions>();
44
+
45
+ const processShards = forEachPipeline("process-shards", {
42
46
  pipeline: ShardPipeline,
43
47
  dependsOn: [resolveShards],
44
48
  items: ({ "resolve-shards": shards }) => shards,
@@ -53,10 +57,12 @@ const processShards = step.forEachPipeline("process-shards", {
53
57
  });
54
58
  ```
55
59
 
56
- To skip the whole step when there are no items, use the skippable builder:
60
+ To skip the whole step when there are no items, add `skip` to its definition:
57
61
 
58
62
  ```ts
59
- const processShards = step.forEachPipeline.skippable("process-shards", {
63
+ const { forEachPipeline } = createSteps<ParentOptions>();
64
+
65
+ const processShards = forEachPipeline("process-shards", {
60
66
  pipeline: ShardPipeline,
61
67
  dependsOn: [resolveShards],
62
68
  skip: ({ "resolve-shards": shards }) =>
@@ -80,17 +86,16 @@ Keys must be stable and unique; duplicate keys fail before any child starts.
80
86
  All running children finish or cancel before the parent step returns a failure.
81
87
  The parent therefore cannot finalize while those children are still running.
82
88
 
83
- Use `.skippable` when the entire child step may be intentionally omitted.
84
- Its `skip` callback returns `false` to proceed, a non-empty reason string to
85
- skip, or `{ reason, value }` to skip with an output. A skip without a value
86
- publishes `undefined`. Policy skips allow required dependents to run, so those
87
- dependents must handle `undefined` explicitly.
88
-
89
- The ordinary builders reject `skip` and keep non-optional output types.
90
- `fromPipeline.skippable` returns `T | undefined`, and
91
- `forEachPipeline.skippable` returns `readonly T[] | undefined`. For fan-out,
92
- the skip value is the complete result array. Skipping does not call `items`,
93
- run children, or apply `mapResult`.
89
+ Add `skip` to a `fromPipeline` or `forEachPipeline` definition when the entire
90
+ child step may be intentionally omitted. The callback returns `false` to proceed,
91
+ a non-empty reason string to skip, or `{ reason, value }` to skip with an output.
92
+ A skip without a value publishes `undefined`. Policy skips allow required
93
+ dependents to run, so those dependents must handle `undefined` explicitly.
94
+
95
+ Without `skip`, `fromPipeline` returns `T` and `forEachPipeline` returns
96
+ `readonly T[]`. Adding `skip` widens those types to `T | undefined` and
97
+ `readonly T[] | undefined`. For fan-out, the skip value is the complete result
98
+ array. Skipping does not call `items`, run children, or apply `mapResult`.
94
99
 
95
100
  ## Planning and execution controls
96
101
 
package/docs/concepts.md CHANGED
@@ -6,8 +6,9 @@ A pipeline describes a series of steps and the values passed between them.
6
6
  Each step has an ID, a function to run, and any dependencies. A finalizer
7
7
  combines step outputs into the value returned to the caller.
8
8
 
9
- Create steps with one `createSteps<TOptions>()` builder per pipeline. Pass a
10
- step object to another step's `dependsOn` array to require its output;
9
+ Create steps with one `createSteps<TOptions>()` factory per pipeline. Take its
10
+ `step` constructor, then pass a step object to another step's
11
+ `dependsOn` array to require its output;
11
12
  TypeScript then infers that input's type.
12
13
 
13
14
  `definePipeline` checks the graph immediately. Duplicate or reserved IDs,
@@ -45,15 +46,16 @@ or fail while running. These cases have different effects on dependent steps.
45
46
  | Outcome | What happened | What dependents receive |
46
47
  | --------------- | ----------------------------------------------------------------------------------------- | ---------------------------------------------------- |
47
48
  | Structural skip | A filter, dry-run rule, missing dependency, abort, or earlier failure prevented execution | No output; required dependents cannot run |
48
- | Policy skip | A `step.skippable` predicate decided that work was unnecessary | The supplied skip value, or `undefined` |
49
+ | Policy skip | A step's `skip` predicate decided that work was unnecessary | The supplied skip value, or `undefined` |
49
50
  | Failure | The step's work failed | No successful output; required dependents cannot run |
50
51
 
51
52
  Fail-fast is the default: a failure stops new work. Set `continueOnError` to
52
53
  allow independent steps to proceed. The run still reports failure, and
53
54
  `runOrThrow` still throws. Use `run` to inspect any partial result.
54
55
 
55
- A `step.skippable` output is always typed as `T | undefined`, even if all current
56
- skip paths return a value. Dependent steps must handle that possible absence.
56
+ Adding `skip` to a step definition makes it skippable. Its output is always
57
+ typed as `T | undefined`, even if all current skip paths return a value.
58
+ Dependent steps must handle that possible absence.
57
59
 
58
60
  ## Execution controls
59
61
 
@@ -302,7 +304,7 @@ Schema V1. Tubeless accepts these schemas without importing a validation library
302
304
  `resultSchema` to `definePipeline`:
303
305
 
304
306
  ```ts
305
- const step = createSteps(optionsSchema);
307
+ const { step } = createSteps(optionsSchema);
306
308
 
307
309
  const parse = step("parse", {
308
310
  outputSchema: parsedRowsSchema,
@@ -349,18 +351,18 @@ options-schema factory scopes.
349
351
 
350
352
  ## Child pipelines
351
353
 
352
- Use `step.fromPipeline` to run one reusable child pipeline. Use
353
- `step.forEachPipeline` to run that child for each item in a list. The parent
354
+ Use `fromPipeline` to run one reusable child pipeline. Use `forEachPipeline` to
355
+ run that child for each item in a list. The parent
354
356
  plan contains one wrapper step; child activity appears as progress beneath it.
355
357
  Read [child-pipeline composition](./child-pipeline-composition.md) for option
356
358
  mapping, selection, failures, and progress controls.
357
359
 
358
- Use `step.forEachPipeline.skippable` when the entire batch may be intentionally
359
- omitted. Its output can be `undefined`, which dependent code must handle.
360
+ Add `skip` to either child-step definition when the entire child step may be
361
+ intentionally omitted. Its output can be `undefined`, which dependent code must handle.
360
362
 
361
363
  ## Remote steps
362
364
 
363
- Use `step.fromRemote` to call a service or execution engine from one step.
365
+ Use `fromRemote` to call a service or execution engine from one step.
364
366
  The parent pipeline still runs locally. The step's `remote.engine` and optional
365
367
  `remote.target` describe the destination for display and inspection.
366
368
 
@@ -51,15 +51,15 @@ const lines = [" First row ", "", "Second row"];
51
51
 
52
52
  ## 2. Create typed steps
53
53
 
54
- Call `createSteps` once to create a step builder for these options. The `load`
55
- step returns the input lines. The `normalize` step declares `dependsOn: [load]`,
54
+ Call `createSteps` once and take its `step` constructor for these options. The
55
+ `load` step returns the input lines. The `normalize` step declares `dependsOn: [load]`,
56
56
  so it receives the output of `load` as a typed input. TypeScript infers that
57
57
  `rows` is a read-only array of strings.
58
58
 
59
59
  ```ts
60
60
  import { createSteps } from "tubeless";
61
61
 
62
- const step = createSteps<ImportOptions>();
62
+ const { step } = createSteps<ImportOptions>();
63
63
 
64
64
  const load = step("load", {
65
65
  description: "Read source rows",
package/docs/recipes.md CHANGED
@@ -15,12 +15,12 @@ one of these recipes.
15
15
  | Validate options, outputs, and results | [`validated-boundaries.ts`](../examples/validated-boundaries.ts) | Standard Schema, `outputSchema`, `resultSchema` |
16
16
  | Inspect, plan, or graph a pipeline or command | [`typed-import.ts`](../examples/typed-import.ts) | `tubeless inspect`, `tubeless plan`, `tubeless graph`, `toMermaid` |
17
17
  | Safe write/publish preview | [`publish-with-gates.ts`](../examples/publish-with-gates.ts) | `dryRun`, `optionalDependsOn`, `skipAfterFailureOf` |
18
- | Deliberately omit unnecessary work | [`conditional-step.ts`](../examples/conditional-step.ts) | `step.skippable`, valued skip, skip-aware output typing |
18
+ | Deliberately omit unnecessary work | [`conditional-step.ts`](../examples/conditional-step.ts) | `step` with `skip`, valued skip, skip-aware output typing |
19
19
  | Preserve independent work after failure | [`best-effort.ts`](../examples/best-effort.ts) | `continueOnError`, structured `run` result |
20
20
  | Compose one reusable workflow | [`child-pipeline.ts`](../examples/child-pipeline.ts) | `fromPipeline`, `mapOptions`, resolved async `mapResult` |
21
21
  | Call a real HTTP service | [`remote-steps.ts`](../examples/remote-steps.ts) | `fromRemote`, fetch cancellation, validated HTTP output |
22
22
  | Host a pipeline in a durable engine | [`host-embedding.ts`](../examples/host-embedding.ts) | `runOrThrow`, pass `correlationId` / `parentRunId` |
23
- | Fan out over runtime items | [`fan-out-progress.ts`](../examples/fan-out-progress.ts) | `forEachPipeline.skippable`, stable keys, concurrency, progress |
23
+ | Fan out over runtime items | [`fan-out-progress.ts`](../examples/fan-out-progress.ts) | `forEachPipeline` with `skip`, stable keys, concurrency, progress |
24
24
  | Inspect keyed fan-out failures | [`fan-out-progress.ts`](../examples/fan-out-progress.ts) | `error.fanOut`, bounded diagnostics, caller-directed reruns |
25
25
  | Show determinate progress | [`fan-out-progress.ts`](../examples/fan-out-progress.ts) | `reportProgress`, bounded live CLI rows, complete final trees |
26
26
  | Watch the live TTY reporter | [`live-tui.ts`](../examples/live-tui.ts) | named steps, nested `details`; persist with `--store` |
@@ -68,12 +68,12 @@ pipeline does not require credentials.
68
68
  1. Use an ordinary step for one unit of domain work.
69
69
  2. Set `dryRun: "skip"` or provide a side-effect-free `dryRun` handler before
70
70
  exposing side-effecting work through CLI dry-run support.
71
- 3. Use `step.skippable` only for a successful policy decision, never to hide an error.
71
+ 3. Add `skip` only for a successful policy decision, never to hide an error.
72
72
  4. Use a child pipeline when the child has value independently; use a normal
73
73
  helper function when it does not.
74
74
  5. Use `forEachPipeline` when every item needs child-pipeline lifecycle and
75
- reporting. Opt into `forEachPipeline.skippable` when policy may omit the
76
- whole fan-out and a skip should appear in reports. Use `runConcurrent`
75
+ reporting. Add `skip` to `forEachPipeline` when policy may omit the whole
76
+ fan-out and a skip should appear in reports. Use `runConcurrent`
77
77
  for lightweight worker functions that should
78
78
  throw on the first failure. Use `runConcurrentSettled` when the caller needs
79
79
  completed results plus that failure without throwing.
@@ -1,6 +1,6 @@
1
1
  # Remote-step composition
2
2
 
3
- Use `step.fromRemote` when a pipeline step needs to call a service or execution
3
+ Use `fromRemote` when a pipeline step needs to call a service or execution
4
4
  engine outside the current process. The step sends input through an adapter,
5
5
  waits for the result, and validates it before dependent steps run.
6
6
 
@@ -10,8 +10,9 @@ The required fields are `adapter`, `mapInput`, and `outputSchema`:
10
10
  - `mapInput` builds the request from local step inputs and context.
11
11
  - `outputSchema` checks the returned value and provides its type to dependents.
12
12
 
13
- Use `fromRemote.skippable` when the request may be intentionally omitted. As
14
- with other skippable steps, dependent code must handle a possible `undefined`.
13
+ Add `skip` to the `fromRemote` definition when the request may be intentionally
14
+ omitted. As with other skippable steps, dependent code must handle a possible
15
+ `undefined`.
15
16
  See the [HTTP example](../examples/remote-steps.ts) for a complete implementation.
16
17
 
17
18
  ## Choose where the pipeline runs
@@ -4,7 +4,7 @@ interface AuditOptions {
4
4
  failLinks?: boolean;
5
5
  }
6
6
 
7
- const step = createSteps<AuditOptions>();
7
+ const { step } = createSteps<AuditOptions>();
8
8
 
9
9
  const checkLinks = step("check-links", {
10
10
  description: "Validate links independently",
@@ -5,7 +5,7 @@ interface PollOptions {
5
5
  attempts: number;
6
6
  }
7
7
 
8
- const step = createSteps<PollOptions>();
8
+ const { step } = createSteps<PollOptions>();
9
9
 
10
10
  const poll = step("poll", {
11
11
  description: "Poll cooperatively with an injected, cancellation-aware sleep",
@@ -43,7 +43,7 @@ interface EnrichOptions {
43
43
  lines: readonly string[];
44
44
  }
45
45
 
46
- const step = createSteps<EnrichOptions>();
46
+ const { step, fromRemote } = createSteps<EnrichOptions>();
47
47
 
48
48
  const parseRows = step("parse-rows", {
49
49
  description: "Trim and drop blank rows from caller input.",
@@ -66,7 +66,7 @@ const enrichAdapter: RemoteStepAdapter<
66
66
  },
67
67
  };
68
68
 
69
- const enrichRows = step.fromRemote("enrich-rows", {
69
+ const enrichRows = fromRemote("enrich-rows", {
70
70
  dependsOn: [parseRows],
71
71
  description: "Rehearse a remote enrich during dry-run.",
72
72
  adapter: enrichAdapter,
@@ -84,7 +84,7 @@ const chargeAdapter: RemoteStepAdapter<EnrichOptions, { orderId: string }, Charg
84
84
  invoke: async (payload) => ({ charged: true, orderId: payload.orderId }),
85
85
  };
86
86
 
87
- const chargeOrder = step.fromRemote("charge-order", {
87
+ const chargeOrder = fromRemote("charge-order", {
88
88
  dependsOn: [enrichRows],
89
89
  description: "Skip remote charge during dry-run.",
90
90
  adapter: chargeAdapter,
@@ -5,14 +5,14 @@ interface ImportOptions {
5
5
  lines: readonly string[];
6
6
  }
7
7
 
8
- const step = createSteps<ImportOptions>();
8
+ const { step, forEachPipeline } = createSteps<ImportOptions>();
9
9
 
10
10
  const loadRows = step("load-rows", {
11
11
  description: "Read raw input records from the caller.",
12
12
  run: (_inputs, context) => context.options.lines,
13
13
  });
14
14
 
15
- const normalizedImport = step.forEachPipeline.skippable("normalized-import", {
15
+ const normalizedImport = forEachPipeline("normalized-import", {
16
16
  dependsOn: [loadRows],
17
17
  description: "Normalize rows through mapped child pipelines when input is present.",
18
18
  pipeline: NormalizePipeline,
@@ -4,7 +4,7 @@ interface NormalizeOptions {
4
4
  rows: readonly string[];
5
5
  }
6
6
 
7
- const step = createSteps<NormalizeOptions>();
7
+ const { step } = createSteps<NormalizeOptions>();
8
8
 
9
9
  const normalizeRows = step("normalize-rows", {
10
10
  description: "Trim, lowercase, and drop blank rows.",
@@ -9,7 +9,7 @@ interface Artifact {
9
9
  size: number;
10
10
  }
11
11
 
12
- const step = createSteps<PublishOptions>();
12
+ const { step } = createSteps<PublishOptions>();
13
13
 
14
14
  const buildArtifact = step("build-artifact", {
15
15
  description: "Build an artifact that can be validated and published.",
@@ -7,6 +7,8 @@ import { definePipelineProject } from "tubeless/project";
7
7
  // execution. Command argv flags stay --step/--target, while mapOptions and hooks
8
8
  // receive stepIds and targets.
9
9
  // When converting existing code, use the tubeless-make-pipeline agent skill.
10
+ // Pipeline files destructure the constructors they need from createSteps.
11
+ // Adding skip to any definition marks that step as intentionally omittable.
10
12
  // Adapt this layout to the consumer; retain its established IDs and caller contracts.
11
13
  // For caller-directed fan-out reruns, see ../fan-out-progress.ts: inspect
12
14
  // error.fanOut and check omitted entries and truncated keys before selecting inputs.
@@ -4,7 +4,7 @@ interface NormalizeOptions {
4
4
  rows: readonly string[];
5
5
  }
6
6
 
7
- const normalizeStep = createSteps<NormalizeOptions>();
7
+ const { step: normalizeStep } = createSteps<NormalizeOptions>();
8
8
 
9
9
  const normalizeRows = normalizeStep("normalize-rows", {
10
10
  run: (_inputs, context) =>
@@ -22,9 +22,9 @@ interface ImportOptions {
22
22
  lines: readonly string[];
23
23
  }
24
24
 
25
- const importStep = createSteps<ImportOptions>();
25
+ const { fromPipeline } = createSteps<ImportOptions>();
26
26
 
27
- const normalizedImport = importStep.fromPipeline("normalized-import", {
27
+ const normalizedImport = fromPipeline("normalized-import", {
28
28
  pipeline: NormalizePipeline,
29
29
  mapOptions: (_inputs, context) => ({ rows: context.options.lines }),
30
30
  mapResult: (rows) => ({ count: rows.length, rows }),
@@ -44,7 +44,7 @@ export async function runChildPipelineExample() {
44
44
 
45
45
  // oxlint-disable-next-line no-constant-condition -- typecheck-only compile probe
46
46
  if (false) {
47
- importStep.fromPipeline("invalid-child-selection", {
47
+ fromPipeline("invalid-child-selection", {
48
48
  pipeline: NormalizePipeline,
49
49
  // @ts-expect-error Child run options are checked against its declared target IDs.
50
50
  mapOptions: () => ({ rows: [], targets: ["normalise-rows"] }),
@@ -7,7 +7,7 @@ interface ImportOptions {
7
7
  limit?: number;
8
8
  }
9
9
 
10
- const step = createSteps<ImportOptions>();
10
+ const { step } = createSteps<ImportOptions>();
11
11
 
12
12
  const loadRows = step("load-rows", {
13
13
  description: "Read raw input records from the caller.",
@@ -5,9 +5,9 @@ interface CacheOptions {
5
5
  refresh: boolean;
6
6
  }
7
7
 
8
- const step = createSteps<CacheOptions>();
8
+ const { step } = createSteps<CacheOptions>();
9
9
 
10
- const resolveValue = step.skippable("resolve-value", {
10
+ const resolveValue = step("resolve-value", {
11
11
  description: "Reuse a cached value unless a refresh is required",
12
12
  skip: (_inputs, context) =>
13
13
  !context.options.refresh && context.options.cachedValue !== undefined
@@ -5,7 +5,7 @@ interface ShardOptions {
5
5
  shardId: string;
6
6
  }
7
7
 
8
- const shardStep = createSteps<ShardOptions>();
8
+ const { step: shardStep } = createSteps<ShardOptions>();
9
9
 
10
10
  const processRecords = shardStep("process-records", {
11
11
  name: "Process records",
@@ -46,9 +46,9 @@ interface FanOutOptions {
46
46
  shards: readonly { id: string; records: readonly string[] }[];
47
47
  }
48
48
 
49
- const fanOutStep = createSteps<FanOutOptions>();
49
+ const { forEachPipeline } = createSteps<FanOutOptions>();
50
50
 
51
- const processShards = fanOutStep.forEachPipeline.skippable("process-shards", {
51
+ const processShards = forEachPipeline("process-shards", {
52
52
  pipeline: ShardPipeline,
53
53
  description: "Process shards with bounded concurrency and stable identities",
54
54
  skip: (_inputs, context) =>
@@ -1,6 +1,6 @@
1
1
  import { createSteps, definePipeline, requireOutputs } from "tubeless";
2
2
 
3
- const step = createSteps<{ lines: readonly string[] }>();
3
+ const { step } = createSteps<{ lines: readonly string[] }>();
4
4
  const normalize = step("normalize", {
5
5
  description: "Normalize rows within one host-owned invocation",
6
6
  run: (_inputs, context) => context.options.lines.map((line) => line.trim()),
@@ -11,7 +11,7 @@ const SOURCES = ["west", "east", "north", "south"] as const;
11
11
  const ARTIFACTS = ["index.json", "manifest.json", "checksums.txt"] as const;
12
12
  const DEFAULT_DELAY_MS = 450;
13
13
 
14
- const step = createSteps<LiveTuiOptions>();
14
+ const { step } = createSteps<LiveTuiOptions>();
15
15
 
16
16
  function sequentialStatus(index: number, current: number): DetailStatus {
17
17
  if (index < current) return "completed";
@@ -2,7 +2,7 @@ import { createSteps, definePipeline } from "tubeless";
2
2
  import { definePaths, writeJson } from "tubeless/node";
3
3
 
4
4
  const paths = definePaths({ artifact: "build/artifacts/rows.json" });
5
- const step = createSteps<{ rows: readonly string[] }>();
5
+ const { step } = createSteps<{ rows: readonly string[] }>();
6
6
 
7
7
  const normalize = step("normalize", {
8
8
  description: "Normalize the caller's rows before writing an artifact.",
@@ -39,13 +39,16 @@ const START_LIST_FILES = ["start-list.json", "timing.log", "weather.txt"] as con
39
39
 
40
40
  type DetailStatus = "completed" | "pending" | "running";
41
41
 
42
- const normalizeStep = createSteps<{ delay: number; riders: readonly PelotonRider[] }>();
43
- const inspectStep = createSteps<{
42
+ const { step: normalizeStep } = createSteps<{
43
+ delay: number;
44
+ riders: readonly PelotonRider[];
45
+ }>();
46
+ const { step: inspectStep } = createSteps<{
44
47
  delay: number;
45
48
  kit: readonly string[];
46
49
  riderId: string;
47
50
  }>();
48
- const step = createSteps<PelotonOptions>();
51
+ const { step, fromPipeline, forEachPipeline } = createSteps<PelotonOptions>();
49
52
 
50
53
  function sequentialStatus(index: number, current: number): DetailStatus {
51
54
  if (index < current) return "completed";
@@ -176,7 +179,7 @@ const discoverPeloton = step("discover-peloton", {
176
179
  },
177
180
  });
178
181
 
179
- const resolveWeather = step.skippable("resolve-weather", {
182
+ const resolveWeather = step("resolve-weather", {
180
183
  name: "Resolve Weather",
181
184
  description: "Reuse a posted forecast unless the race asks for a refresh.",
182
185
  skip: (_inputs, context) =>
@@ -192,7 +195,7 @@ const resolveWeather = step.skippable("resolve-weather", {
192
195
  },
193
196
  });
194
197
 
195
- const normalizedBikes = step.fromPipeline("normalize-bikes", {
198
+ const normalizedBikes = fromPipeline("normalize-bikes", {
196
199
  name: "Normalize Bikes",
197
200
  dependsOn: [discoverPeloton],
198
201
  description: "Normalize kit lists through the independently useful child pipeline.",
@@ -203,7 +206,7 @@ const normalizedBikes = step.fromPipeline("normalize-bikes", {
203
206
  }),
204
207
  });
205
208
 
206
- const inspectBikes = step.forEachPipeline("inspect-bikes", {
209
+ const inspectBikes = forEachPipeline("inspect-bikes", {
207
210
  name: "Inspect Bikes",
208
211
  dependsOn: [normalizedBikes],
209
212
  description: "Fan out bike inspections with bounded concurrency and stable rider keys.",
@@ -9,7 +9,7 @@ interface Artifact {
9
9
  size: number;
10
10
  }
11
11
 
12
- const step = createSteps<PublishOptions>();
12
+ const { step } = createSteps<PublishOptions>();
13
13
 
14
14
  const buildArtifact = step("build-artifact", {
15
15
  description: "Build an artifact that can be validated and published.",
@@ -75,13 +75,13 @@ const enrichAdapter: RemoteStepAdapter<RemoteStepsOptions, RemotePayload, unknow
75
75
  },
76
76
  };
77
77
 
78
- const step = createSteps<RemoteStepsOptions>();
78
+ const { step, fromRemote } = createSteps<RemoteStepsOptions>();
79
79
  const parse = step("parse", {
80
80
  description: "Trim input rows before sending them to the remote service",
81
81
  run: (_inputs, context) =>
82
82
  context.options.lines.map((line) => line.trim()).filter((line) => line.length > 0),
83
83
  });
84
- const enrich = step.fromRemote("enrich", {
84
+ const enrich = fromRemote("enrich", {
85
85
  description: "Validate the HTTP service result before local consumption",
86
86
  dependsOn: [parse],
87
87
  adapter: enrichAdapter,
@@ -15,7 +15,7 @@ interface EnrichedItem {
15
15
  summary: string;
16
16
  }
17
17
 
18
- const step = createSteps<EnrichmentOptions>();
18
+ const { step } = createSteps<EnrichmentOptions>();
19
19
 
20
20
  const loadPendingItems = step("load-pending-items", {
21
21
  description: "Skip work that was already checkpointed by a previous run.",
@@ -99,7 +99,7 @@ interface TracingExampleOptions {
99
99
  rows: readonly string[];
100
100
  }
101
101
 
102
- const step = createSteps<TracingExampleOptions>();
102
+ const { step } = createSteps<TracingExampleOptions>();
103
103
  const normalize = step("normalize", {
104
104
  run: async (_inputs, context) =>
105
105
  withRetry(
@@ -4,7 +4,7 @@ interface ImportOptions {
4
4
  lines: readonly string[];
5
5
  }
6
6
 
7
- const step = createSteps<ImportOptions>();
7
+ const { step } = createSteps<ImportOptions>();
8
8
 
9
9
  const loadRows = step("load-rows", {
10
10
  description: "Read raw input records from the caller.",
@@ -42,7 +42,7 @@ const resultSchema = standardSchema<
42
42
  return { value: { ...result, validated: true } };
43
43
  });
44
44
 
45
- const step = createSteps(optionsSchema);
45
+ const { step } = createSteps(optionsSchema);
46
46
 
47
47
  const load = step("load", {
48
48
  outputSchema: rowsSchema,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tubeless",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "description": "Typed pipeline primitives for composable data-processing workflows.",
5
5
  "author": {
6
6
  "name": "Chet Mancini",
@@ -25,16 +25,18 @@ the installed declarations before using them; do not silently upgrade Tubeless.
25
25
 
26
26
  ## Authoring decisions
27
27
 
28
- - Use `createSteps<TDomainOptions>()` per pipeline. Domain options contain
29
- business inputs; pass built-in controls separately to `run(options, controls?)`.
28
+ - Use `createSteps<TDomainOptions>()` per pipeline and destructure every
29
+ constructor it needs: `step`, `fromPipeline`, `fromRemote`, and/or
30
+ `forEachPipeline`. Domain options contain business inputs; pass built-in
31
+ controls separately to `run(options, controls?)`.
30
32
  - Give steps stable kebab-case IDs and descriptions of their domain work.
31
33
  `name` is an optional display label. Return values from steps and consume
32
34
  inferred dependency outputs instead of sharing mutable state.
33
35
  - Use `dependsOn` for required outputs, `optionalDependsOn` for expected
34
36
  absence, and `skipAfterFailureOf` for a failure gate without a required value.
35
37
  Keep publication dependent on successful validation.
36
- - Use `step.skippable` for an intentional successful omission. Handle its
37
- `T | undefined` output explicitly; do not turn exceptions into skips.
38
+ - Add `skip` to a step definition for an intentional successful omission. Handle
39
+ its `T | undefined` output explicitly; do not turn exceptions into skips.
38
40
  - Set `dryRun: "skip"` on writes and other external side effects, or supply a
39
41
  side-effect-free typed preview handler. Unmarked steps still run in dry runs.
40
42
  - Use `requireOutputs` when the final result requires specific outputs. A plain
@@ -57,9 +57,10 @@ has a coherent output and a reason to appear in a plan or failure report.
57
57
 
58
58
  ## Keep the graph typed
59
59
 
60
- Use one `createSteps<TDomainOptions>()` builder and one `definePipeline` per
61
- pipeline. Import from `tubeless` and its public subpaths. Keep business inputs
62
- in domain options; built-in run controls belong in the separate controls argument.
60
+ Use one `createSteps<TDomainOptions>()` factory and one `definePipeline` per
61
+ pipeline. Define ordinary steps with the returned `step` constructor. Import from `tubeless` and
62
+ its public subpaths. Keep business inputs in domain options; built-in run
63
+ controls belong in the separate controls argument.
63
64
 
64
65
  Give each step a stable literal kebab-case ID and an operational description.
65
66
  Let `dependsOn: [step]` infer input types from returned outputs. Preserve
@@ -88,7 +89,7 @@ a missing required output to an empty array, `false`, or a fake success value.
88
89
  write to make dry runs pass.
89
90
  - Keep validation required for publication. Test this even with
90
91
  `continueOnError` if the caller enables best-effort execution.
91
- - Use `step.skippable` only for a successful policy decision. Preserve thrown
92
+ - Add `skip` only for a successful policy decision. Preserve thrown
92
93
  failures and their causes; branch on structured Tubeless error fields.
93
94
  Map pipeline errors at the caller boundary if existing callers require a
94
95
  specific error contract.