runsheet 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/llms.txt CHANGED
@@ -7,17 +7,22 @@ focused steps with explicit inputs and outputs, then compose them into pipelines
7
7
  that handle context passing, rollback on failure, and schema validation at every
8
8
  boundary.
9
9
 
10
- Built on composable-functions for Result semantics. Zod is supported for runtime
11
- schema validation but is optional — TypeScript generics alone provide
10
+ Uses its own `StepResult` type for result semantics. Zod is supported for
11
+ runtime schema validation but is optional — TypeScript generics alone provide
12
12
  compile-time safety.
13
13
 
14
14
  ## Core concepts
15
15
 
16
+ - Args persist and outputs accumulate. Initial arguments flow through the entire
17
+ pipeline, each step's output merges into the context, and every step sees the
18
+ full picture of everything before it.
16
19
  - Steps declare `requires` (input from context) and `provides` (output added to
17
- context). Context accumulates immutably across steps.
20
+ context).
18
21
  - Pipelines run steps sequentially. On failure, rollback handlers execute in
19
22
  reverse order with snapshot-based context.
20
- - Results use `{ success, data, errors }` — never throws.
23
+ - Results use `StepResult<T>` a discriminated union: `StepSuccess<T>`
24
+ (`success`, `data`, `meta`) or `StepFailure` (`success`, `error`, `meta`,
25
+ `failedStep`, `rollback`). Never throws.
21
26
 
22
27
  ## Quick reference
23
28
 
@@ -114,25 +119,25 @@ RetryPolicy type:
114
119
  - backoff?: 'linear' | 'exponential' (default 'linear')
115
120
  - retryIf?: (errors: Error[]) => boolean — return false to stop retrying
116
121
 
117
- ### buildPipeline
122
+ ### pipeline
118
123
 
119
124
  Build a pipeline from an array of steps. The result type is inferred from the
120
125
  steps.
121
126
 
122
127
  ```typescript
123
- import { buildPipeline } from 'runsheet';
128
+ import { pipeline } from 'runsheet';
124
129
 
125
- const pipeline = buildPipeline({
130
+ const p = pipeline({
126
131
  name: 'checkout',
127
132
  steps: [fetchUser, chargePayment, sendEmail],
128
133
  });
129
134
 
130
- const result = await pipeline.run({ userId: '123', amount: 50 });
135
+ const result = await p.run({ userId: '123', amount: 50 });
131
136
 
132
137
  if (result.success) {
133
138
  result.data.chargeId; // string — fully typed
134
139
  } else {
135
- result.errors; // what went wrong
140
+ result.error; // what went wrong (single Error)
136
141
  result.rollback; // { completed: string[], failed: RollbackFailure[] }
137
142
  }
138
143
  ```
@@ -140,7 +145,7 @@ if (result.success) {
140
145
  Optional argsSchema validates pipeline input:
141
146
 
142
147
  ```typescript
143
- const pipeline = buildPipeline({
148
+ const p = pipeline({
144
149
  name: 'checkout',
145
150
  steps: [fetchUser, chargePayment],
146
151
  argsSchema: z.object({ userId: z.string(), amount: z.number() }),
@@ -151,21 +156,37 @@ Use `strict: true` to detect provides key collisions at build time. Throws a
151
156
  RunsheetError with code 'STRICT_OVERLAP' if two steps provide the same key:
152
157
 
153
158
  ```typescript
154
- const pipeline = buildPipeline({
159
+ const p = pipeline({
155
160
  name: 'checkout',
156
161
  steps: [fetchUser, chargePayment],
157
162
  strict: true,
158
163
  });
159
164
  ```
160
165
 
161
- ### createPipeline (builder API)
166
+ ### Pipeline composition
162
167
 
163
- Fluent builder with progressive type narrowing.
168
+ Pipelines are steps pipeline returns an AggregateStep. A pipeline can be used
169
+ directly as a step in another pipeline's steps array:
164
170
 
165
171
  ```typescript
166
- import { createPipeline } from 'runsheet';
172
+ const inner = pipeline({ name: 'inner', steps: [a, b] });
173
+ const outer = pipeline({ name: 'outer', steps: [inner, c, d] });
174
+ ```
175
+
176
+ If a later outer step fails, the inner pipeline's steps are rolled back.
177
+
178
+ ### Builder API
179
+
180
+ Omit `steps` from the config to get a fluent builder with progressive type
181
+ narrowing.
167
182
 
168
- const pipeline = createPipeline('checkout', z.object({ userId: z.string() }))
183
+ ```typescript
184
+ import { pipeline } from 'runsheet';
185
+
186
+ const checkout = pipeline({
187
+ name: 'checkout',
188
+ argsSchema: z.object({ userId: z.string() }),
189
+ })
169
190
  .step(fetchUser)
170
191
  .step(chargePayment)
171
192
  .step(sendEmail)
@@ -175,7 +196,7 @@ const pipeline = createPipeline('checkout', z.object({ userId: z.string() }))
175
196
  Type-only args (no runtime validation of pipeline input):
176
197
 
177
198
  ```typescript
178
- const pipeline = createPipeline<{ userId: string }>('checkout')
199
+ const checkout = pipeline<{ userId: string }>({ name: 'checkout' })
179
200
  .step(fetchUser)
180
201
  .build();
181
202
  ```
@@ -183,13 +204,17 @@ const pipeline = createPipeline<{ userId: string }>('checkout')
183
204
  Strict mode via the builder:
184
205
 
185
206
  ```typescript
186
- createPipeline('checkout', { strict: true })
207
+ pipeline({ name: 'checkout', strict: true })
187
208
  .step(fetchUser)
188
209
  .step(chargePayment)
189
210
  .build();
190
211
 
191
212
  // Or with a schema:
192
- createPipeline('checkout', z.object({ userId: z.string() }), { strict: true })
213
+ pipeline({
214
+ name: 'checkout',
215
+ argsSchema: z.object({ userId: z.string() }),
216
+ strict: true,
217
+ })
193
218
  .step(fetchUser)
194
219
  .step(chargePayment)
195
220
  .build();
@@ -200,7 +225,7 @@ createPipeline('checkout', z.object({ userId: z.string() }), { strict: true })
200
225
  ```typescript
201
226
  import { parallel } from 'runsheet';
202
227
 
203
- const pipeline = buildPipeline({
228
+ const p = pipeline({
204
229
  name: 'checkout',
205
230
  steps: [
206
231
  validateOrder,
@@ -216,12 +241,137 @@ inner steps are rolled back before the error propagates. Inner steps retain
216
241
  their own requires/provides validation, retry, and timeout. Conditional steps
217
242
  (when()) work inside parallel().
218
243
 
244
+ ### choice (branching)
245
+
246
+ ```typescript
247
+ import { choice } from 'runsheet';
248
+
249
+ const p = pipeline({
250
+ name: 'checkout',
251
+ steps: [
252
+ validateOrder,
253
+ choice(
254
+ [(ctx) => ctx.method === 'card', chargeCard],
255
+ [(ctx) => ctx.method === 'bank', chargeBankTransfer],
256
+ chargeDefault, // default (bare step)
257
+ ),
258
+ sendConfirmation,
259
+ ],
260
+ });
261
+ ```
262
+
263
+ Predicates are evaluated in order — first match wins. A bare step (without a
264
+ tuple) can be passed as the last argument to serve as a default — equivalent to
265
+ `[() => true, step]`. If no predicate matches, the step fails with RunsheetError
266
+ code 'CHOICE_NO_MATCH'. Only the matched branch participates in rollback.
267
+
268
+ ### map (collection iteration)
269
+
270
+ ```typescript
271
+ import { map } from 'runsheet';
272
+
273
+ // Function form — items can be any type
274
+ const p = pipeline({
275
+ name: 'notify',
276
+ steps: [
277
+ map(
278
+ 'emails',
279
+ (ctx) => ctx.users,
280
+ async (user) => {
281
+ await sendEmail(user.email);
282
+ return { email: user.email, sentAt: new Date() };
283
+ },
284
+ ),
285
+ ],
286
+ });
287
+
288
+ // Step form — reuse existing steps, item spread into context
289
+ const p = pipeline({
290
+ name: 'process',
291
+ steps: [map('results', (ctx) => ctx.items, processItem)],
292
+ });
293
+ ```
294
+
295
+ Items run concurrently via Promise.allSettled. Results collected into an array
296
+ under the given key. In step form, each item is spread into the pipeline context
297
+ ({ ...ctx, ...item }) so the step sees both pipeline-level and per-item values.
298
+ On partial failure, succeeded items are rolled back (step form only).
299
+
300
+ ### filter (collection filtering)
301
+
302
+ ```typescript
303
+ import { filter, map } from 'runsheet';
304
+
305
+ const p = pipeline({
306
+ name: 'notify',
307
+ steps: [
308
+ filter(
309
+ 'eligible',
310
+ (ctx) => ctx.users,
311
+ (user) => user.optedIn,
312
+ ),
313
+ map('emails', (ctx) => ctx.eligible, sendEmail),
314
+ ],
315
+ });
316
+ ```
317
+
318
+ Predicates run concurrently via Promise.allSettled. Supports sync or async
319
+ predicates. Items where the predicate returns true are kept; original order is
320
+ preserved. If any predicate throws, the step fails. No rollback (pure
321
+ operation).
322
+
323
+ ### flatMap (collection expansion)
324
+
325
+ ```typescript
326
+ import { flatMap } from 'runsheet';
327
+
328
+ const p = pipeline({
329
+ name: 'process',
330
+ steps: [
331
+ flatMap(
332
+ 'lineItems',
333
+ (ctx) => ctx.orders,
334
+ (order) => order.items,
335
+ ),
336
+ ],
337
+ });
338
+ ```
339
+
340
+ Maps each item to an array, then flattens one level. Callbacks run concurrently
341
+ via Promise.allSettled. Supports sync or async callbacks. If any callback
342
+ throws, the step fails. No rollback (pure operation).
343
+
344
+ ### Dependency injection
345
+
346
+ Pass dependencies as pipeline args — they're available to every step through the
347
+ accumulated context without any step needing to provides them:
348
+
349
+ ```typescript
350
+ const placeOrder = pipeline<{
351
+ orderId: string;
352
+ stripe: Stripe;
353
+ db: Database;
354
+ }>({ name: 'placeOrder' })
355
+ .step(validateOrder)
356
+ .step(chargePayment)
357
+ .build();
358
+
359
+ await placeOrder.run({
360
+ orderId: '123',
361
+ stripe: stripeClient,
362
+ db: dbClient,
363
+ });
364
+ ```
365
+
366
+ Steps declare infrastructure deps in requires like any other context key. For
367
+ testing, swap in mocks at the call site.
368
+
219
369
  ### when (conditional steps)
220
370
 
221
371
  ```typescript
222
372
  import { when } from 'runsheet';
223
373
 
224
- const pipeline = buildPipeline({
374
+ const p = pipeline({
225
375
  name: 'checkout',
226
376
  steps: [
227
377
  fetchUser,
@@ -248,43 +398,41 @@ const timing: StepMiddleware = (step, next) => async (ctx) => {
248
398
  return result;
249
399
  };
250
400
 
251
- const pipeline = buildPipeline({
401
+ const p = pipeline({
252
402
  name: 'checkout',
253
403
  steps: [fetchUser, chargePayment],
254
404
  middleware: [timing],
255
405
  });
256
406
 
257
407
  // Or with the builder:
258
- createPipeline('checkout')
408
+ pipeline({ name: 'checkout' })
259
409
  .use(timing)
260
410
  .step(fetchUser)
261
411
  .step(chargePayment)
262
412
  .build();
263
413
  ```
264
414
 
265
- ### PipelineResult
415
+ ### StepResult
266
416
 
267
- Every pipeline.run() returns a PipelineResult (never throws):
417
+ Every run() returns a StepResult (never throws):
268
418
 
269
419
  ```typescript
270
420
  // Success
271
421
  {
272
422
  success: true,
273
423
  data: { /* accumulated context */ },
274
- errors: [],
275
424
  meta: {
276
- pipeline: 'checkout',
425
+ name: 'checkout',
277
426
  args: { userId: '123' },
278
427
  stepsExecuted: ['fetchUser', 'chargePayment'],
279
- stepsSkipped: [],
280
428
  },
281
429
  }
282
430
 
283
431
  // Failure
284
432
  {
285
433
  success: false,
286
- errors: [Error],
287
- meta: { pipeline, args, stepsExecuted, stepsSkipped },
434
+ error: Error,
435
+ meta: { name, args, stepsExecuted },
288
436
  failedStep: 'chargePayment',
289
437
  rollback: { completed: ['fetchUser'], failed: [] },
290
438
  }
@@ -298,21 +446,28 @@ RunsheetError instances with a discriminated `code` property:
298
446
  - REQUIRES_VALIDATION — step's requires schema failed
299
447
  - PROVIDES_VALIDATION — step's provides schema failed
300
448
  - ARGS_VALIDATION — pipeline argsSchema failed
301
- - PREDICATE — when() predicate threw
449
+ - PREDICATE — when() or choice() predicate threw
302
450
  - TIMEOUT — step exceeded its timeout
303
451
  - RETRY_EXHAUSTED — step failed after all retry attempts
304
452
  - STRICT_OVERLAP — two steps provide the same key (strict mode, build time)
453
+ - CHOICE_NO_MATCH — no branch matched in a choice() step
454
+ - ROLLBACK — one or more rollback handlers failed in a combinator
455
+ - UNKNOWN — a non-Error value was thrown and caught by the pipeline engine
305
456
 
306
457
  Application errors (thrown from step run functions) pass through as-is.
307
458
 
308
459
  ## Key exports
309
460
 
310
- Functions: defineStep, buildPipeline, createPipeline, when, parallel Classes:
311
- RunsheetError Types: Step, TypedStep, StepConfig, RetryPolicy, Pipeline,
312
- PipelineBuilder, PipelineResult, PipelineSuccess, PipelineFailure,
313
- PipelineExecutionMeta, RollbackReport, RollbackFailure, StepMiddleware,
314
- StepInfo, StepExecutor, ConditionalStep, RunsheetErrorCode, Result, Success,
315
- Failure
461
+ Functions: defineStep, pipeline, when, parallel, choice, map, flatMap, filter
462
+ Classes: RunsheetError, RequiresValidationError, ProvidesValidationError,
463
+ ArgsValidationError, PredicateError, TimeoutError, RetryExhaustedError,
464
+ StrictOverlapError, ChoiceNoMatchError, RollbackError, UnknownError Types: Step,
465
+ TypedStep, AggregateStep, StepConfig, StepContext, StepOutput, StepSchema,
466
+ RetryPolicy, StepResult, StepSuccess, StepFailure, StepMeta, AggregateResult,
467
+ AggregateSuccess, AggregateFailure, AggregateMeta, RollbackReport,
468
+ RollbackFailure, PipelineBuilder, PipelineConfig, StepMiddleware, StepInfo,
469
+ StepExecutor, ConditionalStep, RunsheetErrorCode, ExtractRequires,
470
+ ExtractProvides
316
471
 
317
472
  ## Important patterns
318
473
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runsheet",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Type-safe, composable business logic pipelines for TypeScript",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -31,11 +31,11 @@
31
31
  "build": "tsup",
32
32
  "test": "vitest run",
33
33
  "test:watch": "vitest",
34
- "lint": "eslint . && markdownlint-cli2 '**/*.md' 'llms.txt' '#node_modules' '#CLAUDE.md' '#CHANGELOG.md'",
35
- "lint:fix": "eslint --fix . && markdownlint-cli2 --fix '**/*.md' 'llms.txt' '#node_modules' '#CHANGELOG.md'",
34
+ "lint": "./scripts/lint.sh",
35
+ "lint:fix": "./scripts/lint.sh --fix",
36
36
  "format": "prettier --check .",
37
37
  "format:fix": "prettier --write .",
38
- "typecheck": "tsc --noEmit",
38
+ "typecheck": "tsc --noEmit -p tsconfig.check.json",
39
39
  "prepare": "husky",
40
40
  "prepublishOnly": "pnpm run build"
41
41
  },
@@ -81,9 +81,6 @@
81
81
  "engines": {
82
82
  "node": ">=20"
83
83
  },
84
- "dependencies": {
85
- "composable-functions": "^4.0.0"
86
- },
87
84
  "peerDependencies": {
88
85
  "zod": "^3.22.0"
89
86
  },