tubeless 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (224) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +106 -0
  3. package/dist/abort.d.ts +12 -0
  4. package/dist/abort.d.ts.map +1 -0
  5. package/dist/abort.js +49 -0
  6. package/dist/abort.js.map +1 -0
  7. package/dist/batch.d.ts +22 -0
  8. package/dist/batch.d.ts.map +1 -0
  9. package/dist/batch.js +73 -0
  10. package/dist/batch.js.map +1 -0
  11. package/dist/checkpoint.d.ts +33 -0
  12. package/dist/checkpoint.d.ts.map +1 -0
  13. package/dist/checkpoint.js +71 -0
  14. package/dist/checkpoint.js.map +1 -0
  15. package/dist/child-execution.d.ts +30 -0
  16. package/dist/child-execution.d.ts.map +1 -0
  17. package/dist/child-execution.js +270 -0
  18. package/dist/child-execution.js.map +1 -0
  19. package/dist/cli-command.d.ts +29 -0
  20. package/dist/cli-command.d.ts.map +1 -0
  21. package/dist/cli-command.js +314 -0
  22. package/dist/cli-command.js.map +1 -0
  23. package/dist/cli-parser.d.ts +28 -0
  24. package/dist/cli-parser.d.ts.map +1 -0
  25. package/dist/cli-parser.js +557 -0
  26. package/dist/cli-parser.js.map +1 -0
  27. package/dist/cli-pipeline-command.d.ts +84 -0
  28. package/dist/cli-pipeline-command.d.ts.map +1 -0
  29. package/dist/cli-pipeline-command.js +185 -0
  30. package/dist/cli-pipeline-command.js.map +1 -0
  31. package/dist/cli-types.d.ts +220 -0
  32. package/dist/cli-types.d.ts.map +1 -0
  33. package/dist/cli-types.js +19 -0
  34. package/dist/cli-types.js.map +1 -0
  35. package/dist/cli.d.ts +6 -0
  36. package/dist/cli.d.ts.map +1 -0
  37. package/dist/cli.js +6 -0
  38. package/dist/cli.js.map +1 -0
  39. package/dist/env.d.ts +2 -0
  40. package/dist/env.d.ts.map +1 -0
  41. package/dist/env.js +8 -0
  42. package/dist/env.js.map +1 -0
  43. package/dist/file-utils.d.ts +4 -0
  44. package/dist/file-utils.d.ts.map +1 -0
  45. package/dist/file-utils.js +35 -0
  46. package/dist/file-utils.js.map +1 -0
  47. package/dist/interactive-reporter.d.ts +38 -0
  48. package/dist/interactive-reporter.d.ts.map +1 -0
  49. package/dist/interactive-reporter.js +308 -0
  50. package/dist/interactive-reporter.js.map +1 -0
  51. package/dist/lifecycle.d.ts +17 -0
  52. package/dist/lifecycle.d.ts.map +1 -0
  53. package/dist/lifecycle.js +77 -0
  54. package/dist/lifecycle.js.map +1 -0
  55. package/dist/live-ticker.d.ts +21 -0
  56. package/dist/live-ticker.d.ts.map +1 -0
  57. package/dist/live-ticker.js +377 -0
  58. package/dist/live-ticker.js.map +1 -0
  59. package/dist/mapped-child-progress.d.ts +109 -0
  60. package/dist/mapped-child-progress.d.ts.map +1 -0
  61. package/dist/mapped-child-progress.js +133 -0
  62. package/dist/mapped-child-progress.js.map +1 -0
  63. package/dist/node.d.ts +5 -0
  64. package/dist/node.d.ts.map +1 -0
  65. package/dist/node.js +5 -0
  66. package/dist/node.js.map +1 -0
  67. package/dist/paths.d.ts +9 -0
  68. package/dist/paths.d.ts.map +1 -0
  69. package/dist/paths.js +19 -0
  70. package/dist/paths.js.map +1 -0
  71. package/dist/pipeline-command-marker.d.ts +5 -0
  72. package/dist/pipeline-command-marker.d.ts.map +1 -0
  73. package/dist/pipeline-command-marker.js +21 -0
  74. package/dist/pipeline-command-marker.js.map +1 -0
  75. package/dist/pipeline-diagnostics.d.ts +4 -0
  76. package/dist/pipeline-diagnostics.d.ts.map +1 -0
  77. package/dist/pipeline-diagnostics.js +17 -0
  78. package/dist/pipeline-diagnostics.js.map +1 -0
  79. package/dist/pipeline-module.d.ts +62 -0
  80. package/dist/pipeline-module.d.ts.map +1 -0
  81. package/dist/pipeline-module.js +157 -0
  82. package/dist/pipeline-module.js.map +1 -0
  83. package/dist/pipeline.d.ts +606 -0
  84. package/dist/pipeline.d.ts.map +1 -0
  85. package/dist/pipeline.js +1444 -0
  86. package/dist/pipeline.js.map +1 -0
  87. package/dist/progress.d.ts +4 -0
  88. package/dist/progress.d.ts.map +1 -0
  89. package/dist/progress.js +8 -0
  90. package/dist/progress.js.map +1 -0
  91. package/dist/prompt-select.d.ts +81 -0
  92. package/dist/prompt-select.d.ts.map +1 -0
  93. package/dist/prompt-select.js +197 -0
  94. package/dist/prompt-select.js.map +1 -0
  95. package/dist/rate-limit.d.ts +7 -0
  96. package/dist/rate-limit.d.ts.map +1 -0
  97. package/dist/rate-limit.js +23 -0
  98. package/dist/rate-limit.js.map +1 -0
  99. package/dist/render.d.ts +21 -0
  100. package/dist/render.d.ts.map +1 -0
  101. package/dist/render.js +60 -0
  102. package/dist/render.js.map +1 -0
  103. package/dist/reporter-entry.d.ts +3 -0
  104. package/dist/reporter-entry.d.ts.map +1 -0
  105. package/dist/reporter-entry.js +3 -0
  106. package/dist/reporter-entry.js.map +1 -0
  107. package/dist/reporter.d.ts +50 -0
  108. package/dist/reporter.d.ts.map +1 -0
  109. package/dist/reporter.js +167 -0
  110. package/dist/reporter.js.map +1 -0
  111. package/dist/retry.d.ts +28 -0
  112. package/dist/retry.d.ts.map +1 -0
  113. package/dist/retry.js +59 -0
  114. package/dist/retry.js.map +1 -0
  115. package/dist/run-store-sqlite.d.ts +12 -0
  116. package/dist/run-store-sqlite.d.ts.map +1 -0
  117. package/dist/run-store-sqlite.js +187 -0
  118. package/dist/run-store-sqlite.js.map +1 -0
  119. package/dist/run-store-ui-http.d.ts +5 -0
  120. package/dist/run-store-ui-http.d.ts.map +1 -0
  121. package/dist/run-store-ui-http.js +19 -0
  122. package/dist/run-store-ui-http.js.map +1 -0
  123. package/dist/run-store-ui-page.d.ts +2 -0
  124. package/dist/run-store-ui-page.d.ts.map +1 -0
  125. package/dist/run-store-ui-page.js +759 -0
  126. package/dist/run-store-ui-page.js.map +1 -0
  127. package/dist/run-store-ui-protocol.d.ts +28 -0
  128. package/dist/run-store-ui-protocol.d.ts.map +1 -0
  129. package/dist/run-store-ui-protocol.js +84 -0
  130. package/dist/run-store-ui-protocol.js.map +1 -0
  131. package/dist/run-store-ui-state.d.ts +21 -0
  132. package/dist/run-store-ui-state.d.ts.map +1 -0
  133. package/dist/run-store-ui-state.js +65 -0
  134. package/dist/run-store-ui-state.js.map +1 -0
  135. package/dist/run-store-ui.d.ts +25 -0
  136. package/dist/run-store-ui.d.ts.map +1 -0
  137. package/dist/run-store-ui.js +217 -0
  138. package/dist/run-store-ui.js.map +1 -0
  139. package/dist/run-store.d.ts +109 -0
  140. package/dist/run-store.d.ts.map +1 -0
  141. package/dist/run-store.js +345 -0
  142. package/dist/run-store.js.map +1 -0
  143. package/dist/testing.d.ts +40 -0
  144. package/dist/testing.d.ts.map +1 -0
  145. package/dist/testing.js +83 -0
  146. package/dist/testing.js.map +1 -0
  147. package/dist/tracing-internal.d.ts +21 -0
  148. package/dist/tracing-internal.d.ts.map +1 -0
  149. package/dist/tracing-internal.js +206 -0
  150. package/dist/tracing-internal.js.map +1 -0
  151. package/dist/tracing-json.d.ts +12 -0
  152. package/dist/tracing-json.d.ts.map +1 -0
  153. package/dist/tracing-json.js +8 -0
  154. package/dist/tracing-json.js.map +1 -0
  155. package/dist/tracing-otel.d.ts +36 -0
  156. package/dist/tracing-otel.d.ts.map +1 -0
  157. package/dist/tracing-otel.js +60 -0
  158. package/dist/tracing-otel.js.map +1 -0
  159. package/dist/tracing.d.ts +48 -0
  160. package/dist/tracing.d.ts.map +1 -0
  161. package/dist/tracing.js +2 -0
  162. package/dist/tracing.js.map +1 -0
  163. package/dist/workbench-bin.d.ts +3 -0
  164. package/dist/workbench-bin.d.ts.map +1 -0
  165. package/dist/workbench-bin.js +8 -0
  166. package/dist/workbench-bin.js.map +1 -0
  167. package/dist/workbench-graph.d.ts +3 -0
  168. package/dist/workbench-graph.d.ts.map +1 -0
  169. package/dist/workbench-graph.js +64 -0
  170. package/dist/workbench-graph.js.map +1 -0
  171. package/dist/workbench-inspect.d.ts +3 -0
  172. package/dist/workbench-inspect.d.ts.map +1 -0
  173. package/dist/workbench-inspect.js +65 -0
  174. package/dist/workbench-inspect.js.map +1 -0
  175. package/dist/workbench-plan.d.ts +3 -0
  176. package/dist/workbench-plan.d.ts.map +1 -0
  177. package/dist/workbench-plan.js +72 -0
  178. package/dist/workbench-plan.js.map +1 -0
  179. package/dist/workbench-run.d.ts +13 -0
  180. package/dist/workbench-run.d.ts.map +1 -0
  181. package/dist/workbench-run.js +115 -0
  182. package/dist/workbench-run.js.map +1 -0
  183. package/dist/workbench-shared.d.ts +67 -0
  184. package/dist/workbench-shared.d.ts.map +1 -0
  185. package/dist/workbench-shared.js +163 -0
  186. package/dist/workbench-shared.js.map +1 -0
  187. package/dist/workbench-studio.d.ts +26 -0
  188. package/dist/workbench-studio.d.ts.map +1 -0
  189. package/dist/workbench-studio.js +57 -0
  190. package/dist/workbench-studio.js.map +1 -0
  191. package/dist/workbench-ui.d.ts +3 -0
  192. package/dist/workbench-ui.d.ts.map +1 -0
  193. package/dist/workbench-ui.js +262 -0
  194. package/dist/workbench-ui.js.map +1 -0
  195. package/dist/workbench.d.ts +5 -0
  196. package/dist/workbench.d.ts.map +1 -0
  197. package/dist/workbench.js +43 -0
  198. package/dist/workbench.js.map +1 -0
  199. package/docs/README.md +31 -0
  200. package/docs/agent-guide.md +168 -0
  201. package/docs/api-reference.md +269 -0
  202. package/docs/api-report.json +298 -0
  203. package/docs/child-pipeline-composition.md +185 -0
  204. package/docs/cli.md +118 -0
  205. package/docs/concepts.md +502 -0
  206. package/docs/getting-started.md +162 -0
  207. package/docs/llms.txt +62 -0
  208. package/docs/recipes.md +51 -0
  209. package/docs/studio.md +86 -0
  210. package/examples/best-effort.ts +42 -0
  211. package/examples/cancellation-and-testing.ts +45 -0
  212. package/examples/child-pipeline.ts +51 -0
  213. package/examples/cli-job.ts +86 -0
  214. package/examples/conditional-step.ts +41 -0
  215. package/examples/fan-out-progress.ts +53 -0
  216. package/examples/local-observability.ts +80 -0
  217. package/examples/publish-with-gates.ts +56 -0
  218. package/examples/rendering.ts +34 -0
  219. package/examples/resumable-enrichment.ts +75 -0
  220. package/examples/tracing.ts +35 -0
  221. package/examples/tsconfig.json +15 -0
  222. package/examples/typed-import.ts +54 -0
  223. package/examples/validated-boundaries.ts +75 -0
  224. package/package.json +156 -0
@@ -0,0 +1,502 @@
1
+ # Core concepts
2
+
3
+ ## Definition model
4
+
5
+ A pipeline is an ordered declaration of typed steps plus a finalizer.
6
+ `definePipeline` immediately rejects invalid static graphs (duplicate/reserved
7
+ IDs, missing or contradictory dependencies, and cycles). Planning validates
8
+ option-dependent step selection before execution begins.
9
+
10
+ Literal duplicate step IDs are also rejected by TypeScript at the
11
+ `definePipeline` call. Runtime definition validation remains the backstop for
12
+ widened arrays and dynamically assembled definitions.
13
+
14
+ Use one `createSteps<TOptions>()` factory per pipeline. A step object is both its
15
+ definition and its typed dependency token.
16
+
17
+ The ID is the stable machine identity used for dependencies, output keys,
18
+ selection, and tracing. An optional `name` overrides how reporters and printed
19
+ plans display the step without changing that identity.
20
+
21
+ ## Dependency choices
22
+
23
+ | Field | Supplies data | Blocks after failure | Typical use |
24
+ | -------------------- | ------------- | -------------------- | ----------------------------------- |
25
+ | `dependsOn` | Required | Yes | Transformation needs upstream value |
26
+ | `optionalDependsOn` | When present | No | Fallback or partial rerun |
27
+ | `skipAfterFailureOf` | No | Yes | Publish/write safety gate |
28
+
29
+ Prefer required dependencies unless partial execution is an intentional part of
30
+ the workflow. A failure gate blocks after either `failed` or `cancelled`; both
31
+ mean the guarded prerequisite did not safely complete.
32
+
33
+ Shorter aliases such as `needs`, `uses`, and `gatedBy` were evaluated and not
34
+ adopted. `uses` does not communicate optionality, and aliases would leave two
35
+ vocabularies for the same graph. The explicit fields above remain canonical.
36
+
37
+ ## Three kinds of non-execution
38
+
39
+ - **Structural skip:** a dependency, dry-run rule, filter, abort, or fail-fast
40
+ decision prevents execution. Dependents that require the step do not run.
41
+ - **Policy skip:** a `step.skippable` predicate deliberately decides that work
42
+ is unnecessary. It may publish a value and unlock dependents.
43
+ - **Failure:** the step ran and threw. Fail-fast stops new work by default;
44
+ `continueOnError` lets independent work proceed.
45
+
46
+ `continueOnError` changes scheduling, not success: the structured run result
47
+ remains unsuccessful and `runOrThrow` still throws. Inspect `run()` when a
48
+ best-effort final value is useful despite recorded failures.
49
+
50
+ Any `step.skippable` step has output type `T | undefined`, even if current skip
51
+ paths publish a value. Dependents must handle the absence explicitly.
52
+
53
+ ## Selection and finalization
54
+
55
+ Pipeline definitions declare supported downstream goals with step references:
56
+
57
+ ```ts
58
+ definePipeline({
59
+ id: "publish",
60
+ steps: [build, validate, publish],
61
+ targets: [publish],
62
+ finalize: requireOutputs([publish], ({ publish }) => publish),
63
+ });
64
+ ```
65
+
66
+ At call sites, `targets` accepts only those declared literal IDs. The planner
67
+ recursively selects each target's required inputs and failure gates, while
68
+ leaving optional-only inputs out. `pipeline.targetIds` and CLI `--target`
69
+ discovery expose the same declared set. Internal steps are not public goals;
70
+ `stepIds` remains an exact filter for partial reruns and other advanced
71
+ workflows and does not add prerequisites. Empty selections, unknown,
72
+ undeclared, or duplicate IDs, and a run that supplies both fields fail during
73
+ planning.
74
+
75
+ Every planned step includes `selectionReasons`, a stable discriminated union
76
+ that explains inclusion or omission without requiring callers to reconstruct
77
+ the graph. A target plan can report a direct `target`, a
78
+ `required-dependency`, a `failure-gate`, an excluded `optional-only` input, or
79
+ an `outside-target-closure` omission. Exact filters use `exact` and
80
+ `not-selected`; unfiltered plans use `all`.
81
+
82
+ ```ts
83
+ const plan = pipeline.plan({ targets: ["publish"] });
84
+ const load = plan.steps.find((step) => step.id === "load");
85
+
86
+ // A shared prerequisite can explain every path that selected it.
87
+ load?.selectionReasons;
88
+ // [{ kind: "required-dependency", dependentId: "build", targetId: "publish" }]
89
+ ```
90
+
91
+ `selected` remains the convenient boolean and `skipReason` describes execution
92
+ disposition such as `filtered`, `dry-run`, or `unmet-dependency`.
93
+
94
+ Render a plan at the presentation boundary instead of duplicating its selection
95
+ reason switch in each command or tool:
96
+
97
+ ```ts
98
+ import { renderPipelinePlan } from "tubeless/render";
99
+
100
+ const plan = pipeline.plan({ targets: ["publish"] });
101
+ const terminalText = renderPipelinePlan(plan);
102
+ const machineJson = renderPipelinePlan(plan, { format: "json" });
103
+ ```
104
+
105
+ Human output explains target and dependency provenance by default; pass
106
+ `{ explain: false }` for a compact disposition-only view. JSON output preserves
107
+ the original structured plan fields for machine consumers.
108
+
109
+ ## Step statuses and structured errors
110
+
111
+ Steps have one canonical, enforced lifecycle:
112
+
113
+ ```text
114
+ planned ──┬──> running ──┬──> complete
115
+ │ ├──> skipped
116
+ │ ├──> cancelled
117
+ │ └──> failed
118
+ ├──> skipped
119
+ └──> cancelled
120
+ ```
121
+
122
+ The runtime derives focused hooks from this state machine. Use
123
+ `onStepStart`, `onStepProgress`, `onStepComplete`, `onStepSkip`,
124
+ `onStepCancel`, or `onStepFail` for ordinary pipeline integration; each
125
+ callback receives metadata narrowed to that case. `onStepPlan` is available
126
+ for plan-aware tooling. `onStepStatus` is an additive catch-all for consumers
127
+ that need the complete discriminated lifecycle, such as renderers and event
128
+ stores. Registering it does not replace the focused callbacks.
129
+
130
+ A `running` status may be published repeatedly as progress changes. Every
131
+ selected step ends in exactly one terminal report; cancellation is distinct
132
+ from failure, including selected steps that were cancelled before they started.
133
+
134
+ ```ts
135
+ const hooks: PipelineHooks = {
136
+ onStepProgress({ progress, step }) {
137
+ renderProgress(step.id, progress);
138
+ },
139
+ onStepSkip({ reason, step }) {
140
+ explainSkip(step.id, reason);
141
+ },
142
+ onStepFail({ error, step }) {
143
+ reportError(step.id, error);
144
+ },
145
+ onStepCancel({ error, step }) {
146
+ reportCancellation(step.id, error);
147
+ },
148
+ };
149
+ ```
150
+
151
+ Every `PipelineError` has package-owned `code`, `phase`, and `kind` fields.
152
+ Branch on those fields rather than message text. `phase` locates the lifecycle
153
+ boundary (`definition`, `planning`, `execution`, or `finalization`); `kind`
154
+ distinguishes definition, selection, step, dependency, cancellation, child, and
155
+ finalization problems. If a thrown error supplied its own machine code, it is
156
+ retained separately as `sourceCode`. Native `Error.cause` chains are copied into
157
+ a bounded, cycle-safe `PipelineErrorCause` tree containing only message, name,
158
+ source code, and nested cause fields, so reports and trace events stay JSON-safe.
159
+
160
+ ```ts
161
+ const result = await pipeline.run(options);
162
+ const first = result.errors[0];
163
+
164
+ if (first?.code === "TUBELESS_STEP_FAILED" && first.sourceCode === "ENOENT") {
165
+ // A step failed because its underlying operation could not find a file.
166
+ }
167
+ ```
168
+
169
+ `runOrThrow` raises `PipelineExecutionError` with the full structured result on
170
+ `result` and the original thrown value on native `cause`. Its default message
171
+ identifies the pipeline, phase, package code, step, and deepest normalized cause.
172
+ `PipelineDefinitionError` uses the same diagnostic summary for every rejected
173
+ definition issue. `renderPipelineError` from `tubeless/render` exposes that
174
+ same diagnostic formatting directly and can emit the structured error as JSON.
175
+
176
+ `PipelineStepReport` is the terminal-state union. Successful reports carry
177
+ timing, skipped reports carry `reason` plus optional `message` and
178
+ `dependencyId`, and failed/cancelled reports carry a structured `error`.
179
+ Hooks and tracing consume the same terminal objects rather than reconstructing
180
+ status from separate callbacks.
181
+
182
+ ## Versioned run records
183
+
184
+ `run()` returns one versioned `PipelineRun<TResult>`. Every execution has a
185
+ `runId`, terminal `status`, start and finish timestamps, structured errors, and
186
+ one terminal report per planned step. Pass `context.runId` and
187
+ `context.parentRunId` when an external orchestrator owns correlation; otherwise
188
+ core generates an opaque run ID. The same IDs are available in step contexts
189
+ and optional trace exports.
190
+
191
+ An actual step execution receives one `attemptId`. It appears on the
192
+ `PipelineStepContext`, its terminal `PipelineStepReport`, and trace lifecycle
193
+ records. Executed reports also carry start and finish timestamps. Structural
194
+ skips and steps cancelled before starting have neither an attempt ID nor a start
195
+ timestamp because their handlers never ran. `context.reportAttempt()` describes
196
+ retry activity within that execution; it does not create another persisted
197
+ attempt model.
198
+
199
+ The terminal record deliberately does not copy log or progress streams. Use the
200
+ injected logger and hooks for live observation, or tracing when durable event
201
+ export is required. Errors and cancellation remain represented directly in the
202
+ run and terminal step reports. The run currently has `version: 1`, also exported
203
+ as `RUN_MODEL_VERSION`; persist that field and branch on it before decoding a
204
+ stored run. Durations are derived by subtracting the relevant timestamps.
205
+
206
+ ```ts
207
+ const result = await pipeline.run(options, {
208
+ ...defaultPipelineContext(),
209
+ runId: externalJobId,
210
+ });
211
+
212
+ result.runId;
213
+ result.status;
214
+ result.steps.find((step) => step.attemptId)?.attemptId;
215
+ result.finishedAtMs - result.startedAtMs;
216
+ ```
217
+
218
+ ### Local event store and studio
219
+
220
+ The local studio is a composition of existing boundaries, not part of pipeline
221
+ execution:
222
+
223
+ ```text
224
+ pipeline definition → dependency-free executor → trace exporter
225
+ ↘ append-only SQLite
226
+ ↘ local studio
227
+ ↘ injected command launcher
228
+ ```
229
+
230
+ `openSqlitePipelineRunStore()` implements `PipelineTraceExporter` and appends
231
+ every lifecycle record to one versioned SQLite event table. Database triggers
232
+ reject updates and deletes. Current run state, history, step attempts, progress,
233
+ logs, errors, and observed definition graphs are projections of that immutable
234
+ stream; they are not competing executor models.
235
+
236
+ The SQLite adapter also exposes an explicit `clearHistory()` maintenance reset.
237
+ It deletes the complete event history and compacts the database while restoring
238
+ the append-only triggers in the same transaction; individual event updates and
239
+ deletes remain forbidden. The workbench injects this capability into its
240
+ loopback studio with a destructive confirmation and refuses to clear while it
241
+ knows a browser-launched run is still live. Persisted runs left active by an
242
+ interrupted process can still be cleared after confirmation. A directly embedded
243
+ studio stays history-immutable unless its caller explicitly injects the same
244
+ capability and may report its own known live writers through `isBusy()`.
245
+
246
+ Trace-enabled contexts route calls made through `context.log` into correlated
247
+ `pipeline.log` events while still forwarding them to the injected logger.
248
+ Definition metadata is attached to `step.planned` records, so the studio can
249
+ render observed graphs without importing application pipeline modules. The
250
+ first planned step from a newer run replaces that pipeline ID's observed targets
251
+ and steps, so a changed definition does not retain older structure while a run
252
+ that fails validation before planning does not erase the last usable graph.
253
+
254
+ The main `tubeless` entrypoint never imports SQLite or the UI. The optional
255
+ `tubeless/run-store/sqlite` adapter selects the runtime-provided SQLite
256
+ implementation, and `tubeless/run-store/ui` is a separate HTTP projection.
257
+ It is read-only unless the caller injects a `PipelineRunStudioLauncher`.
258
+ Likewise, ordinary `tubeless run` remains unchanged; pass `--store` to record a run
259
+ and invoke `tubeless ui` only when a browser view is useful.
260
+
261
+ Observed definitions are never treated as executable registrations: an event
262
+ stream does not contain a trusted module path or a command's domain contract.
263
+ `tubeless ui --command ./path/to/command.ts` explicitly loads only marked
264
+ `definePipelineCommand` exports and passes bounded structured form values
265
+ through their validation, option mapping, and execution path. No shell or argv
266
+ round trip is involved. Launch-enabled workbench servers are restricted to
267
+ loopback, while the UI HTTP module stays execution-agnostic through its injected
268
+ capability. Each command exposes an immutable, JSON-safe descriptor derived
269
+ from the same effective schema used by its parser, including built-in flags.
270
+ The studio uses that descriptor for checkboxes, numeric inputs, constrained
271
+ selects, paths, and repeatable values.
272
+ Planning is a separate read-only capability: **Preview plan** passes the current
273
+ dry-run and step/target values to the real pipeline planner and expands the
274
+ result beneath the launch form. Domain parameters remain visible for the
275
+ eventual run but are not interpreted by the planner, no run is recorded, and
276
+ previewing is optional. Opaque `fromPipeline` and `forEachPipeline` steps carry
277
+ `nestedPipeline` metadata so the preview can distinguish ordinary work from a
278
+ single child pipeline or runtime fan-out without flattening child selection.
279
+
280
+ Studio snapshots page through the store once and then fetch only events after
281
+ the last observed sequence. Those pages go through `createPipelineRunProjector`
282
+ from `tubeless/run-store`; a refresh with no newer ids returns the cached
283
+ snapshot instead of re-folding history. Store-local event ids are monotonic and
284
+ may start at `0`. Duplicate or out-of-order ids are ignored. Use
285
+ `projectPipelineRunStore` when the caller already holds a complete event list
286
+ and does not need incremental refresh. History therefore continues past an
287
+ adapter's per-query safety cap without reloading the entire database on every
288
+ refresh.
289
+
290
+ For a reusable catalog, `definePipelineStudio` declares versioned command-module
291
+ references in one dependency-free manifest. Run `tubeless ui ./tubeless.studio.ts` to
292
+ load it. Module paths and the optional execution `cwd` resolve from the manifest
293
+ instead of the caller's shell directory; duplicate or malformed declarations
294
+ fail before the server listens. Presentation-name overrides never change run or
295
+ pipeline identity.
296
+
297
+ Cancellation is classified from an actual abort error or propagated child
298
+ cancellation, not merely from the signal's current state. An unrelated failure
299
+ that races with `abort()` remains a step or finalization failure.
300
+
301
+ Finalizer inputs remain partial because dry-run policy, exact filtering, and
302
+ best-effort execution can omit outputs. Wrap a normal finalizer with
303
+ `requireOutputs([stepA, stepB], callback)` to declare which output slots make a
304
+ valid result. The callback receives those values as required properties, and
305
+ the run fails finalization with the missing step IDs if any slot is absent. A
306
+ successfully published `undefined` still counts as an output; structural
307
+ absence is determined by whether the slot was published.
308
+
309
+ When a pipeline uses `requireOutputs`, every declared target is checked during
310
+ definition construction. Its required-input and failure-gate closure must
311
+ contain all required finalizer steps, so an advertised target cannot complete
312
+ its work and then fail only because it intentionally omitted the pipeline's
313
+ result. Plain finalizers remain appropriate when selected goals intentionally
314
+ produce a partial domain result.
315
+
316
+ ### Why targets are declared
317
+
318
+ Three contracts were evaluated after dependency-aware selection shipped:
319
+
320
+ - expose every step as a target and keep the global finalizer;
321
+ - add a separate selected-execution runner returning raw output slots; or
322
+ - declare the pipeline's meaningful public goals and retain one finalized
323
+ domain result.
324
+
325
+ Declared targets were selected because they preserve the existing
326
+ `run`/`runOrThrow` result contract, add only one short definition field, keep
327
+ steps as implementation details, and let definition validation prove
328
+ `requireOutputs` compatibility. A second runner would create competing
329
+ execution/result semantics, while goal-specific finalizers add ceremony that
330
+ current production pipelines do not need. Exact `stepIds` remains the explicit
331
+ escape hatch for low-level partial work.
332
+
333
+ ## Dry runs
334
+
335
+ Every step has one normal `run` handler and an optional dry-run policy:
336
+
337
+ - Omit `dryRun` when the normal handler is safe and useful during a dry run.
338
+ - Set `dryRun: "skip"` for filesystem writes, database mutation, publication,
339
+ email, and other external side effects. The step is reported as structurally
340
+ skipped and its required dependents do not run.
341
+ - Provide a `dryRun(inputs, context)` handler to substitute a side-effect-free
342
+ preview. It must return the same output type as `run`, so dependents can use
343
+ the preview normally.
344
+
345
+ ```ts
346
+ const publish = step("publish", {
347
+ dependsOn: [build],
348
+ dryRun: ({ build }) => ({ id: `preview:${build.id}` }),
349
+ run: ({ build }) => publishArtifact(build),
350
+ });
351
+ ```
352
+
353
+ Dry run is not a rollback mechanism. Read-only validation and resolution steps
354
+ may run normally so the preview remains useful.
355
+
356
+ ## Validated boundaries
357
+
358
+ `tubeless` implements the small Standard Schema V1 structural contract and
359
+ does not import a validation library. Pass a domain-options schema to
360
+ `createSteps(schema)`, an `outputSchema` to an ordinary step, or a
361
+ `resultSchema` to `definePipeline`:
362
+
363
+ ```ts
364
+ const step = createSteps(optionsSchema);
365
+
366
+ const parse = step("parse", {
367
+ outputSchema: parsedRowsSchema,
368
+ run: (_inputs, context) => readRows(context.options.source),
369
+ });
370
+
371
+ const pipeline = definePipeline({
372
+ id: "validated-import",
373
+ steps: [parse],
374
+ resultSchema,
375
+ finalize: requireOutputs([parse], ({ parse }) => summarize(parse)),
376
+ });
377
+ ```
378
+
379
+ Schema input and output types are inferred independently. Callers pass the
380
+ options schema's input type, steps read its validated output type, a step's
381
+ `run` and dry-run handler return its schema input type, dependents receive its
382
+ schema output type, and `runOrThrow` returns the result schema's output type.
383
+ Built-in run controls are separate from domain-options validation, so strict
384
+ object schemas do not need to know about `dryRun`, `targets`, or other executor
385
+ policy. Callers pass those controls alongside domain fields; the executor removes
386
+ the reserved control keys before validation while preserving class methods,
387
+ getters, inherited and non-enumerable properties, and symbol keys. Steps receive
388
+ the validated output object directly; the executor neither flattens it nor
389
+ overlays control fields onto it.
390
+
391
+ Options are validated once after structural planning and before any step
392
+ starts. Step values are validated before publication, including values from a
393
+ custom dry-run handler or policy skip. Finalized values are validated before a
394
+ run is marked successful. Sync and async Standard Schema validators are both
395
+ supported during `run`; `plan()` stays synchronous and does not invoke schemas.
396
+
397
+ Failures retain normalized issue messages and paths on `PipelineError.issues`.
398
+ Use the stable codes `TUBELESS_OPTIONS_VALIDATION_FAILED`,
399
+ `TUBELESS_STEP_OUTPUT_VALIDATION_FAILED`, and
400
+ `TUBELESS_FINAL_RESULT_VALIDATION_FAILED` to distinguish boundaries. A step-output
401
+ failure is reported on that step; final-result failure uses
402
+ `__finalize__`. Pipeline definitions cannot mix steps from different
403
+ options-schema factory scopes.
404
+
405
+ ## Child pipelines
406
+
407
+ Use `step.fromPipeline` for one child run and `step.forEachPipeline` when runtime
408
+ items each need the same child. Children are opaque to the parent's plan and
409
+ hooks; their activity is summarized as progress on the parent step. See
410
+ [child-pipeline composition](./child-pipeline-composition.md) for propagation and
411
+ selection boundaries.
412
+
413
+ ## Mermaid diagrams
414
+
415
+ Call `pipeline.toMermaid()` to generate a static flowchart without run options
416
+ or execution. Nodes use `name ?? id`; generated internal node identifiers keep
417
+ arbitrary user-facing text out of Mermaid syntax. Required dependencies render
418
+ as solid arrows, while optional inputs and failure gates render as labeled
419
+ dotted arrows. Use `direction` to change layout and `includeDescriptions` to add
420
+ operational descriptions to node labels.
421
+
422
+ ## Module workbench
423
+
424
+ Command-by-command usage lives in [the CLI](./cli.md). The optional local UI is
425
+ documented in [the studio](./studio.md).
426
+
427
+ `tubeless inspect` and `tubeless graph` load a pipeline module without executing
428
+ steps or requiring domain options. `tubeless plan` accepts either a pipeline or a
429
+ marked `definePipelineCommand` and prefers the command when both are exported.
430
+ `tubeless run` deliberately accepts only a command created by
431
+ `definePipelineCommand`, because that export carries the application-owned
432
+ parser, validation, option mapping, reporter, and result summary needed for
433
+ safe execution. A raw pipeline is not executable through the workbench.
434
+ Inspect still requires a pipeline export.
435
+
436
+ `definePipelineCommand` defaults to a same-name mapping when its validated flag
437
+ values structurally satisfy the pipeline's domain options. Command-only values
438
+ such as `resume`, `step`, and `target` are removed before execution;
439
+ bridge-owned selection and failure controls are applied separately. If flags
440
+ do not satisfy required domain options, `mapOptions` remains required at
441
+ compile time. Keep explicit mapping for renamed fields, file loading, adaptive
442
+ defaults, prompts, or other derived values.
443
+
444
+ The returned command's `descriptor` is the non-terminal view of that contract:
445
+ name, description, and immutable parameter metadata for flags, types, defaults,
446
+ choices, numeric constraints, repeatability, paths, and positional support. It
447
+ is presentation-neutral; consumers still submit argv through `parse` or `run`
448
+ so one validation and execution path remains authoritative.
449
+ `command.plan()` always exposes structural planning from `dryRun`, `step`, and
450
+ `target` input only. It deliberately skips domain parsing, option mapping,
451
+ execution, and persistence. Use `command.plan()` or `tubeless plan`; do not
452
+ simulate planning with `--plan`.
453
+
454
+ Keep the workbench's module-selection arguments before the `--` boundary and
455
+ the command's application arguments after it:
456
+
457
+ ```sh
458
+ tubeless run --export PublishCommand ./scripts/publish.ts -- --source input.json --target publish
459
+ ```
460
+
461
+ The workbench forwards SIGINT through the command context and classifies exits
462
+ as validation, planning, execution, or cancellation without parsing diagnostic
463
+ messages.
464
+
465
+ ## Runtime context
466
+
467
+ Use the step context instead of global facilities:
468
+
469
+ - `context.options` for validated domain options and `context.dryRun` for the active mode.
470
+ - `context.log` so interactive reporters can preserve output.
471
+ - `context.signal` for cooperative cancellation.
472
+ - `context.sleep` for cancellation-aware, testable delays.
473
+ - `context.reportProgress` and `context.reportAttempt` for observability.
474
+ - `context.cwd` for caller-controlled path resolution.
475
+
476
+ Callers may inject the logger, clock, sleep function, signal, hooks, and tracing.
477
+ This makes a pipeline embeddable and deterministic under test.
478
+
479
+ ## Deterministic testing
480
+
481
+ `createPipelineTestRuntime` from `tubeless/testing` packages the ordinary
482
+ runtime injection points into a framework-neutral harness. Its default sleep
483
+ advances a monotonic clock immediately, its logger stays silent while capturing
484
+ calls, and its canonical status hook records lifecycle events and latest
485
+ progress. The harness owns an `AbortController` and delegates `run`,
486
+ `runOrThrow`, and `plan` to the real pipeline, preserving option and result
487
+ inference without implementing alternate execution semantics.
488
+
489
+ Create one runtime per test. Customize `sleep` when a test needs to pause,
490
+ interleave, or advance time differently; call `test.abort()` to exercise the
491
+ normal cancellation path. Assertions remain application- or framework-owned.
492
+
493
+ ## Stable boundaries
494
+
495
+ - Keep step IDs stable, use `name` only for a friendlier display label, and keep
496
+ descriptions operationally useful.
497
+ - Generate documentation from `pipeline.toMermaid()` instead of hand-maintaining
498
+ a second copy of the graph.
499
+ - Return small domain results; inspect `run()` reports for execution metadata.
500
+ - Put remote API mechanics in retry/rate-limit helpers, not in the executor.
501
+ - Put CLI parsing at the script edge with `definePipelineCommand`.
502
+ - Keep application-owned telemetry SDKs outside the dependency-free core.
@@ -0,0 +1,162 @@
1
+ # Getting started
2
+
3
+ ## Install
4
+
5
+ Library consumers install the package and import it from Node.js 22 or later:
6
+
7
+ ```sh
8
+ npm install tubeless
9
+ ```
10
+
11
+ The same package works with `pnpm add tubeless`, `yarn add tubeless`, and
12
+ `bun add tubeless`. The README quick start is a complete first program.
13
+
14
+ The `tubeless` CLI is separate: it requires Bun 1.3.14 or later.
15
+
16
+ ```sh
17
+ bunx tubeless --help
18
+ ```
19
+
20
+ `npx tubeless` is not supported. Command-by-command usage is in
21
+ [the CLI](./cli.md).
22
+
23
+ ## Runtime support
24
+
25
+ Library imports (`tubeless` and every subpath) are ESM-only and require
26
+ Node.js 22 or later. They are dependency-free compiled JavaScript.
27
+
28
+ The `tubeless` CLI requires Bun 1.3.14 or later. Its shebang is
29
+ `#!/usr/bin/env bun`, and Bun loads TypeScript pipeline modules directly.
30
+ `npx tubeless` is not a supported way to run the binary.
31
+
32
+ Supported operating systems are Linux and macOS. Windows is untested.
33
+
34
+ ## 1. Define domain options
35
+
36
+ Describe only domain input. Built-in controls are added automatically to the
37
+ typed run object, while `plan(controls)` accepts controls alone.
38
+
39
+ ```ts
40
+ interface ImportOptions {
41
+ lines: readonly string[];
42
+ }
43
+
44
+ const lines = [" First row ", "", "Second row"];
45
+ ```
46
+
47
+ ## 2. Create typed steps
48
+
49
+ Call `createSteps` once for the pipeline. Declare dependencies with step object
50
+ references; their output types become the dependent step's input types.
51
+
52
+ ```ts
53
+ import { createSteps } from "tubeless";
54
+
55
+ const step = createSteps<ImportOptions>();
56
+
57
+ const load = step("load", {
58
+ description: "Read source rows",
59
+ run: (_inputs, context) => context.options.lines,
60
+ });
61
+
62
+ const normalize = step("normalize", {
63
+ dependsOn: [load],
64
+ description: "Normalize non-empty rows",
65
+ run: ({ load: rows }) => rows.map((row) => row.trim().toLowerCase()).filter(Boolean),
66
+ });
67
+ ```
68
+
69
+ IDs should remain stable: plans, reports, hooks, tracing, and CLI selection all
70
+ use them.
71
+
72
+ ## 3. Define the result
73
+
74
+ ```ts
75
+ import { definePipeline, requireOutputs } from "tubeless";
76
+
77
+ const ImportPipeline = definePipeline({
78
+ id: "import",
79
+ steps: [load, normalize],
80
+ targets: [normalize],
81
+ finalize: requireOutputs([normalize], ({ normalize }) => ({
82
+ count: normalize.length,
83
+ rows: normalize,
84
+ })),
85
+ });
86
+ ```
87
+
88
+ `finalize` receives the outputs that completed. `requireOutputs` declares which
89
+ ones a valid result needs, makes them non-optional in the callback, and reports
90
+ a finalization error when a dry run, exact filter, or failure leaves one absent.
91
+ Use a plain finalizer when partial output is itself a valid result.
92
+
93
+ `targets` on the definition declares the pipeline's supported downstream goals.
94
+ Only those literal IDs are accepted by target selection and shown by pipeline
95
+ commands. When `requireOutputs` is used, definition construction rejects a
96
+ declared target whose dependency closure cannot produce the required result.
97
+
98
+ ## 4. Visualize the static graph
99
+
100
+ `toMermaid` returns dependency-free Mermaid flowchart source without planning or
101
+ running the pipeline. It uses `name` when present and otherwise displays the
102
+ stable step ID.
103
+
104
+ ```ts
105
+ const diagram = ImportPipeline.toMermaid({ direction: "LR" });
106
+ ```
107
+
108
+ Required data dependencies use solid arrows. Optional inputs and failure gates
109
+ use labeled dotted arrows. Set `includeDescriptions: true` when the extra node
110
+ text is useful. The same source is available from `tubeless graph`; see
111
+ [the CLI](./cli.md).
112
+
113
+ ## 5. Pick an execution method
114
+
115
+ ```ts
116
+ const value = await ImportPipeline.runOrThrow({ lines });
117
+ const report = await ImportPipeline.run({ lines, continueOnError: true });
118
+ const plan = ImportPipeline.plan({ dryRun: true });
119
+ const normalizePlan = ImportPipeline.plan({ targets: ["normalize"] });
120
+ ```
121
+
122
+ - `runOrThrow` returns the finalized value only when the run succeeds and throws
123
+ `PipelineExecutionError` for every unsuccessful run. `continueOnError` may let
124
+ independent steps finish, but it never makes their failures successful.
125
+ - `run` always returns the structured pipeline result.
126
+ - `plan` describes executor selection without requiring or validating domain options.
127
+ - `targets` selects declared downstream goals plus their required inputs and
128
+ failure gates. `stepIds` is an exact low-level filter and cannot be combined
129
+ with `targets`. See [core concepts](./concepts.md) for selection provenance.
130
+
131
+ Library callers invoke the pipeline directly. Scripts should use
132
+ `definePipelineCommand` so help, `--target`, `--step`, dry run, and
133
+ cancellation stay generated instead of hand-parsed. See [the CLI](./cli.md)
134
+ and [`cli-job.ts`](../examples/cli-job.ts).
135
+
136
+ ## 6. Test with deterministic runtime plumbing
137
+
138
+ ```ts
139
+ import { createPipelineTestRuntime } from "tubeless/testing";
140
+
141
+ const test = createPipelineTestRuntime({ cwd: "/workspace" });
142
+ const result = await test.run(ImportPipeline, { lines });
143
+
144
+ test.clock.now();
145
+ test.logs;
146
+ test.statuses;
147
+ test.latestProgress;
148
+ ```
149
+
150
+ The default sleep advances the test clock immediately. The harness also owns
151
+ an abort controller and provides typed `runOrThrow` and `plan` helpers. It has
152
+ no test-framework dependency; use ordinary assertions against its structured
153
+ captures and pipeline results.
154
+
155
+ ## Next
156
+
157
+ Continue with the [recipe index](./recipes.md), or read
158
+ [core concepts](./concepts.md) before implementing failure-sensitive writes.
159
+
160
+ For untrusted boundary values, use the Standard Schema support in
161
+ [`validated-boundaries.ts`](../examples/validated-boundaries.ts). Persist and
162
+ inspect local runs with [the studio](./studio.md).