@db-lyon/flowkit 0.13.0 → 0.14.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.
@@ -1,458 +1,568 @@
1
- # API reference
2
-
3
- Complete reference for all public exports from `@db-lyon/flowkit`.
4
-
5
- ## Config
6
-
7
- *Import from `@db-lyon/flowkit` or `@db-lyon/flowkit/config`*
8
-
9
- ### `loadConfig(options)`
10
-
11
- Load, layer, and validate YAML configuration files.
12
-
13
- ```typescript
14
- function loadConfig<T extends z.ZodType>(
15
- options: LoadConfigOptions<T>,
16
- ): LoadedConfig<z.infer<T>>
17
- ```
18
-
19
- **`LoadConfigOptions<T>`**
20
-
21
- | Field | Type | Required | Description |
22
- |-------|------|----------|-------------|
23
- | `filename` | `string` | yes | Primary config filename (e.g., `'app.yml'`) |
24
- | `schema` | `z.ZodType` | yes | Zod schema applied after merging all layers |
25
- | `defaults` | `unknown` | no | Built-in defaults merged under the project file |
26
- | `env` | `string` | no | Environment name — loads `{base}.{env}.{ext}` overlay |
27
- | `envVar` | `string` | no | Env var to read environment name from when `env` is not passed |
28
- | `configDir` | `string` | no | Directory to search (default: `process.cwd()`) |
29
-
30
- **`LoadedConfig<T>`**
31
-
32
- | Field | Type | Description |
33
- |-------|------|-------------|
34
- | `config` | `T` | The validated, merged configuration object |
35
- | `configDir` | `string` | The directory the config was loaded from |
36
-
37
- ---
38
-
39
- ### `findConfigFile(filename, startDir?)`
40
-
41
- Walk up parent directories looking for a file by name.
42
-
43
- ```typescript
44
- function findConfigFile(filename: string, startDir?: string): string
45
- ```
46
-
47
- Returns the absolute path. Throws if not found.
48
-
49
- ---
50
-
51
- ### `loadRawYaml(filePath)`
52
-
53
- Parse a YAML file and return the raw result (no schema validation).
54
-
55
- ```typescript
56
- function loadRawYaml(filePath: string): unknown
57
- ```
58
-
59
- ---
60
-
61
- ### `deepMerge(base, override)`
62
-
63
- Recursively merge two values. Objects merge key-by-key, arrays replace (unless `__merge: 'append'`), scalars override, `null` nullifies, `undefined` is a no-op.
64
-
65
- ```typescript
66
- function deepMerge(base: unknown, override: unknown): unknown
67
- ```
68
-
69
- ---
70
-
71
- ### Zod schemas
72
-
73
- | Schema | Validates |
74
- |--------|-----------|
75
- | `TaskOptionsSchema` | `Record<string, unknown>` |
76
- | `TaskDefinitionSchema` | Task definition object |
77
- | `FlowStepSchema` | Single flow step (task xor flow) |
78
- | `FlowDefinitionSchema` | Flow with description and steps |
79
- | `EngineConfigSchema` | Top-level config with `tasks` and `flows` |
80
-
81
- ---
82
-
83
- ### Config types
84
-
85
- ```typescript
86
- type TaskOptions = Record<string, unknown>;
87
-
88
- type TaskDefinition = {
89
- class_path: string;
90
- description?: string;
91
- group?: string;
92
- options: TaskOptions; // defaults to {}
93
- };
94
-
95
- type FlowStep = {
96
- task?: string;
97
- flow?: string;
98
- options?: TaskOptions;
99
- };
100
-
101
- type FlowDefinition = {
102
- description: string;
103
- steps: Record<string, FlowStep>;
104
- };
105
-
106
- type EngineConfig = {
107
- tasks: Record<string, TaskDefinition>;
108
- flows: Record<string, FlowDefinition>;
109
- };
110
- ```
111
-
112
- ---
113
-
114
- ## Task
115
-
116
- *Import from `@db-lyon/flowkit` or `@db-lyon/flowkit/task`*
117
-
118
- ### `BaseTask<TOpts>`
119
-
120
- Abstract base class for all tasks.
121
-
122
- ```typescript
123
- abstract class BaseTask<TOpts = Record<string, unknown>> {
124
- protected ctx: TaskContext;
125
- protected options: TOpts;
126
- protected logger: Logger;
127
-
128
- constructor(ctx: TaskContext, options: TOpts);
129
-
130
- abstract get taskName(): string;
131
- abstract execute(): Promise<TaskResult>;
132
- protected validate(): void;
133
- async run(): Promise<TaskResult>;
134
- }
135
- ```
136
-
137
- | Method | Description |
138
- |--------|-------------|
139
- | `taskName` | (getter) Human-readable name for logging |
140
- | `execute()` | Perform the task's work. Return a `TaskResult`. |
141
- | `validate()` | Optional. Called before `execute()`. Throw to abort. |
142
- | `run()` | Lifecycle wrapper: validate → execute → catch errors → add duration. Called by the flow runner. |
143
-
144
- ---
145
-
146
- ### `TaskContext`
147
-
148
- ```typescript
149
- interface TaskContext {
150
- logger?: Logger;
151
- [key: string]: unknown;
152
- }
153
- ```
154
-
155
- Shared context passed to every task. Add any properties you need (database connections, API clients, etc.).
156
-
157
- ---
158
-
159
- ### `TaskResult`
160
-
161
- ```typescript
162
- interface TaskResult {
163
- success: boolean;
164
- data?: Record<string, unknown>;
165
- error?: Error;
166
- duration?: number; // milliseconds, set by run()
167
- }
168
- ```
169
-
170
- ---
171
-
172
- ### `ShellTask`
173
-
174
- Built-in task that executes shell commands through the platform shell with
175
- streamed stdout and stderr.
176
-
177
- ```typescript
178
- class ShellTask extends BaseTask<ShellTaskOptions> {
179
- get taskName(): string; // "shell:{command}"
180
- protected validate(): void; // requires command
181
- async execute(): Promise<TaskResult>;
182
- }
183
- ```
184
-
185
- **`ShellTaskOptions`**
186
-
187
- | Field | Type | Default | Description |
188
- |-------|------|---------|-------------|
189
- | `command` | `string` | (required) | Shell command to execute |
190
- | `cwd` | `string` | `undefined` | Working directory |
191
- | `timeout` | `number` | `300000` | Timeout in milliseconds (5 min) |
192
- | `signal` | `AbortSignal` | `undefined` | Cancels this individual programmatic invocation; not representable in YAML |
193
-
194
- **Success result:** `data.output` contains trimmed stdout.
195
-
196
- **Failure result:** `data.exitCode`, `data.stderr`, `data.stdout`.
197
-
198
- When `signal` is already aborted, no shell process is launched and the task
199
- returns the normal failed result with `Shell command cancelled`. An abort during
200
- execution requests termination of the spawned shell process. On Windows,
201
- Flowkit makes a best-effort `taskkill /T /F` request for that invocation's
202
- shell PID and falls back to direct shell termination only if that request
203
- fails or the three-second terminal deadline expires. On POSIX,
204
- signal-bearing invocations run in a dedicated process group. Flowkit requests
205
- `SIGTERM`, allows 250ms for cooperative cleanup, and then escalates the group
206
- to `SIGKILL`. Neither approach guarantees termination of descendants that have
207
- escaped the managed process tree or group; callers must not treat the returned
208
- result as proof of complete descendant termination.
209
-
210
- For signal-bearing invocations, Flowkit waits for the spawned shell to close
211
- after requesting termination. If the operating system does not report closure
212
- within one second on POSIX or three seconds on Windows, it returns the
213
- cancellation or timeout result with the output captured so far; that bounded
214
- fallback requests force termination and releases Flowkit's Node handles for
215
- the root shell and any Windows `taskkill` helper, but is not confirmation that
216
- either process, or any escaped descendant, has
217
- exited. POSIX
218
- signal-bearing invocations are in a separate process group, so terminal Ctrl+C
219
- does not reach them automatically; cancel through the supplied `AbortSignal`.
220
-
221
- Trailing stdout and stderr fragments are captured once. This corrects the
222
- duplicate final-partial-line output in earlier releases.
223
-
224
- ---
225
-
226
- ### `TaskRegistry`
227
-
228
- Registry that maps names and class paths to task constructors.
229
-
230
- ```typescript
231
- class TaskRegistry {
232
- register(name: string, ctor: TaskConstructor): this;
233
- registerClassPath(classPath: string, ctor: TaskConstructor): this;
234
- registerAll(entries: Record<string, TaskConstructor>): this;
235
- registerClassPaths(entries: Record<string, TaskConstructor>): this;
236
- async resolve(classPathOrName: string): Promise<TaskConstructor>;
237
- async create(
238
- classPathOrName: string,
239
- ctx: TaskContext,
240
- options: Record<string, unknown>,
241
- ): Promise<BaseTask>;
242
- listRegistered(): string[];
243
- }
244
- ```
245
-
246
- | Method | Description |
247
- |--------|-------------|
248
- | `register(name, ctor)` | Register by short name |
249
- | `registerClassPath(path, ctor)` | Register by dotted class path |
250
- | `registerAll(entries)` | Bulk register by short name |
251
- | `registerClassPaths(entries)` | Bulk register by class path |
252
- | `resolve(nameOrPath)` | Look up a constructor. Falls back to dynamic filesystem import. |
253
- | `create(nameOrPath, ctx, opts)` | Resolve + instantiate in one call |
254
- | `listRegistered()` | Return all registered names and class paths |
255
-
256
- **`TaskConstructor`**
257
-
258
- ```typescript
259
- type TaskConstructor = new (
260
- ctx: TaskContext,
261
- options: Record<string, unknown>,
262
- ) => BaseTask;
263
- ```
264
-
265
- ---
266
-
267
- ## Flow
268
-
269
- *Import from `@db-lyon/flowkit` or `@db-lyon/flowkit/flow`*
270
-
271
- ### `FlowRunner`
272
-
273
- Orchestration engine that executes flows.
274
-
275
- ```typescript
276
- class FlowRunner {
277
- constructor(config: FlowRunnerConfig);
278
- async run(options: FlowRunOptions): Promise<FlowRunResult>;
279
- resolveExecutionPlan(
280
- flow: FlowDefinition,
281
- skipSet: Set<string>,
282
- ): PlanStep[];
283
- }
284
- ```
285
-
286
- ---
287
-
288
- ### `FlowRunnerConfig`
289
-
290
- ```typescript
291
- interface FlowRunnerConfig {
292
- tasks: Record<string, TaskDefinition>;
293
- flows: Record<string, FlowDefinition>;
294
- registry: TaskRegistry;
295
- context: TaskContext;
296
- hooks?: FlowRunnerHooks;
297
- logger?: Logger;
298
- }
299
- ```
300
-
301
- ---
302
-
303
- ### `FlowRunOptions`
304
-
305
- ```typescript
306
- interface FlowRunOptions {
307
- flowName: string; // name of the flow to execute
308
- skip?: string[]; // task names or step numbers to skip
309
- plan?: boolean; // return plan without executing
310
- }
311
- ```
312
-
313
- ---
314
-
315
- ### `FlowRunResult`
316
-
317
- ```typescript
318
- interface FlowRunResult {
319
- success: boolean;
320
- steps: FlowStepResult[];
321
- duration: number; // total milliseconds
322
- error?: Error; // first error that caused failure
323
- }
324
- ```
325
-
326
- ---
327
-
328
- ### `FlowStepResult`
329
-
330
- ```typescript
331
- interface FlowStepResult {
332
- stepNumber: number;
333
- type: 'task' | 'flow';
334
- name: string;
335
- result?: TaskResult;
336
- skipped: boolean;
337
- duration: number; // milliseconds
338
- }
339
- ```
340
-
341
- ---
342
-
343
- ### `PlanStep`
344
-
345
- Represents a step in the execution plan (returned by plan mode or passed to hooks).
346
-
347
- ```typescript
348
- interface PlanStep {
349
- stepNumber: number;
350
- type: 'task' | 'flow';
351
- name: string;
352
- skipped: boolean;
353
- options?: Record<string, unknown>;
354
- }
355
- ```
356
-
357
- ---
358
-
359
- ### `FlowRunnerHooks`
360
-
361
- ```typescript
362
- interface FlowRunnerHooks {
363
- beforeRun?(flowName: string, plan: PlanStep[]): Promise<void>;
364
- afterRun?(result: FlowRunResult): Promise<void>;
365
- beforeStep?(step: PlanStep): Promise<void>;
366
- afterStep?(step: PlanStep, result: FlowStepResult): Promise<void>;
367
- onStepError?(
368
- step: PlanStep,
369
- error: Error,
370
- completed: FlowStepResult[],
371
- ): Promise<void>;
372
- }
373
- ```
374
-
375
- | Hook | Fires | Scope |
376
- |------|-------|-------|
377
- | `beforeRun` | Once before execution starts | Top-level flow only |
378
- | `afterRun` | Once after execution completes | Top-level flow only |
379
- | `beforeStep` | Before each step executes | All steps (including nested) |
380
- | `afterStep` | After each step completes | All steps (including nested) |
381
- | `onStepError` | When a step fails | All steps (including nested) |
382
-
383
- ---
384
-
385
- ## DAG
386
-
387
- *Import from `@db-lyon/flowkit` or `@db-lyon/flowkit/dag`*
388
-
389
- ### `topologicalSort(nodes)`
390
-
391
- Sort a directed acyclic graph in dependency order (dependencies first).
392
-
393
- ```typescript
394
- function topologicalSort<T>(nodes: DagNode<T>[]): DagNode<T>[]
395
- ```
396
-
397
- Throws `CircularDependencyError` if the graph has cycles. Throws `MissingDependencyError` if a node references a dependency that doesn't exist.
398
-
399
- ---
400
-
401
- ### `DagNode<T>`
402
-
403
- ```typescript
404
- interface DagNode<T = unknown> {
405
- id: string;
406
- dependencies: string[];
407
- data: T;
408
- }
409
- ```
410
-
411
- ---
412
-
413
- ### `CircularDependencyError`
414
-
415
- ```typescript
416
- class CircularDependencyError extends Error {
417
- cycle: string[]; // e.g., ['a', 'b', 'c', 'a']
418
- }
419
- ```
420
-
421
- ---
422
-
423
- ### `MissingDependencyError`
424
-
425
- ```typescript
426
- class MissingDependencyError extends Error {
427
- nodeId: string; // the node that has the bad dependency
428
- missingDep: string; // the dependency that doesn't exist
429
- }
430
- ```
431
-
432
- ---
433
-
434
- ## Logger
435
-
436
- *Import from `@db-lyon/flowkit`*
437
-
438
- ### `Logger` interface
439
-
440
- ```typescript
441
- interface Logger {
442
- debug(...args: unknown[]): void;
443
- info(...args: unknown[]): void;
444
- warn(...args: unknown[]): void;
445
- error(...args: unknown[]): void;
446
- child(bindings: Record<string, unknown>): Logger;
447
- }
448
- ```
449
-
450
- Compatible with pino, winston, and similar structured loggers.
451
-
452
- ### `noopLogger`
453
-
454
- A silent logger that discards all output. Used as the default when no logger is provided.
455
-
456
- ```typescript
457
- const noopLogger: Logger;
458
- ```
1
+ # API reference
2
+
3
+ Complete reference for all public exports from `@db-lyon/flowkit`.
4
+
5
+ ## Config
6
+
7
+ *Import from `@db-lyon/flowkit` or `@db-lyon/flowkit/config`*
8
+
9
+ ### `loadConfig(options)`
10
+
11
+ Load, layer, and validate YAML configuration files.
12
+
13
+ ```typescript
14
+ function loadConfig<T extends z.ZodType>(
15
+ options: LoadConfigOptions<T>,
16
+ ): LoadedConfig<z.infer<T>>
17
+ ```
18
+
19
+ **`LoadConfigOptions<T>`**
20
+
21
+ | Field | Type | Required | Description |
22
+ |-------|------|----------|-------------|
23
+ | `filename` | `string` | yes | Primary config filename (e.g., `'app.yml'`) |
24
+ | `schema` | `z.ZodType` | yes | Zod schema applied after merging all layers |
25
+ | `defaults` | `unknown` | no | Built-in defaults merged under the project file |
26
+ | `env` | `string` | no | Environment name — loads `{base}.{env}.{ext}` overlay |
27
+ | `envVar` | `string` | no | Env var to read environment name from when `env` is not passed |
28
+ | `configDir` | `string` | no | Directory to search (default: `process.cwd()`) |
29
+
30
+ **`LoadedConfig<T>`**
31
+
32
+ | Field | Type | Description |
33
+ |-------|------|-------------|
34
+ | `config` | `T` | The validated, merged configuration object |
35
+ | `configDir` | `string` | The directory the config was loaded from |
36
+
37
+ ---
38
+
39
+ ### `findConfigFile(filename, startDir?)`
40
+
41
+ Walk up parent directories looking for a file by name.
42
+
43
+ ```typescript
44
+ function findConfigFile(filename: string, startDir?: string): string
45
+ ```
46
+
47
+ Returns the absolute path. Throws if not found.
48
+
49
+ ---
50
+
51
+ ### `loadRawYaml(filePath)`
52
+
53
+ Parse a YAML file and return the raw result (no schema validation).
54
+
55
+ ```typescript
56
+ function loadRawYaml(filePath: string): unknown
57
+ ```
58
+
59
+ ---
60
+
61
+ ### `deepMerge(base, override)`
62
+
63
+ Recursively merge two values. Objects merge key-by-key, arrays replace (unless `__merge: 'append'`), scalars override, `null` nullifies, `undefined` is a no-op.
64
+
65
+ ```typescript
66
+ function deepMerge(base: unknown, override: unknown): unknown
67
+ ```
68
+
69
+ ---
70
+
71
+ ### Zod schemas
72
+
73
+ | Schema | Validates |
74
+ |--------|-----------|
75
+ | `TaskOptionsSchema` | `Record<string, unknown>` |
76
+ | `TaskDefinitionSchema` | Task definition object |
77
+ | `FlowStepSchema` | Single flow step (task xor flow) |
78
+ | `FlowDefinitionSchema` | Flow with description and steps |
79
+ | `EngineConfigSchema` | Top-level config with `tasks` and `flows` |
80
+
81
+ ---
82
+
83
+ ### Config types
84
+
85
+ ```typescript
86
+ type TaskOptions = Record<string, unknown>;
87
+
88
+ type TaskDefinition = {
89
+ class_path: string;
90
+ description?: string;
91
+ group?: string;
92
+ options: TaskOptions; // defaults to {}
93
+ };
94
+
95
+ type FlowStep = {
96
+ task?: string;
97
+ flow?: string;
98
+ options?: TaskOptions;
99
+ };
100
+
101
+ type FlowDefinition = {
102
+ description: string;
103
+ steps: Record<string, FlowStep>;
104
+ };
105
+
106
+ type EngineConfig = {
107
+ tasks: Record<string, TaskDefinition>;
108
+ flows: Record<string, FlowDefinition>;
109
+ };
110
+ ```
111
+
112
+ ---
113
+
114
+ ## Task
115
+
116
+ *Import from `@db-lyon/flowkit` or `@db-lyon/flowkit/task`*
117
+
118
+ ### `BaseTask<TOpts>`
119
+
120
+ Abstract base class for all tasks.
121
+
122
+ ```typescript
123
+ abstract class BaseTask<TOpts = Record<string, unknown>> {
124
+ protected ctx: TaskContext;
125
+ protected options: TOpts;
126
+ protected logger: Logger;
127
+
128
+ constructor(ctx: TaskContext, options: TOpts);
129
+
130
+ abstract get taskName(): string;
131
+ abstract execute(): Promise<TaskResult>;
132
+ protected validate(): void;
133
+ async run(): Promise<TaskResult>;
134
+ }
135
+ ```
136
+
137
+ | Method | Description |
138
+ |--------|-------------|
139
+ | `taskName` | (getter) Human-readable name for logging |
140
+ | `execute()` | Perform the task's work. Return a `TaskResult`. |
141
+ | `validate()` | Optional. Called before `execute()`. Throw to abort. |
142
+ | `run()` | Lifecycle wrapper: validate → execute → catch errors → add duration. Called by the flow runner. |
143
+
144
+ ---
145
+
146
+ ### `TaskContext`
147
+
148
+ ```typescript
149
+ interface TaskContext {
150
+ logger?: Logger;
151
+ [key: string]: unknown;
152
+ }
153
+ ```
154
+
155
+ Shared context passed to every task. Add any properties you need (database connections, API clients, etc.).
156
+
157
+ ---
158
+
159
+ ### `TaskResult`
160
+
161
+ ```typescript
162
+ interface TaskResult {
163
+ success: boolean;
164
+ data?: Record<string, unknown>;
165
+ error?: Error;
166
+ duration?: number; // milliseconds, set by run()
167
+ }
168
+ ```
169
+
170
+ ---
171
+
172
+ ### `ShellTask`
173
+
174
+ Built-in task that executes shell commands through the platform shell with
175
+ streamed stdout and stderr.
176
+
177
+ ```typescript
178
+ class ShellTask extends BaseTask<ShellTaskOptions> {
179
+ get taskName(): string; // "shell:{command}"
180
+ protected validate(): void; // requires command
181
+ async execute(): Promise<TaskResult>;
182
+ }
183
+ ```
184
+
185
+ **`ShellTaskOptions`**
186
+
187
+ | Field | Type | Default | Description |
188
+ |-------|------|---------|-------------|
189
+ | `command` | `string` | (required) | Shell command to execute |
190
+ | `cwd` | `string` | `undefined` | Working directory |
191
+ | `timeout` | `number` | `300000` | Timeout in milliseconds (5 min) |
192
+ | `signal` | `AbortSignal` | `undefined` | Cancels this individual programmatic invocation; not representable in YAML |
193
+
194
+ **Success result:** `data.output` contains trimmed stdout.
195
+
196
+ **Failure result:** `data.exitCode`, `data.stderr`, `data.stdout`.
197
+
198
+ When `signal` is already aborted, no shell process is launched and the task
199
+ returns the normal failed result with `Shell command cancelled`. An abort during
200
+ execution requests termination of the spawned shell process. On Windows,
201
+ Flowkit makes a best-effort `taskkill /T /F` request for that invocation's
202
+ shell PID and falls back to direct shell termination only if that request
203
+ fails or the three-second terminal deadline expires. On POSIX,
204
+ signal-bearing invocations run in a dedicated process group. Flowkit requests
205
+ `SIGTERM`, allows 250ms for cooperative cleanup, and then escalates the group
206
+ to `SIGKILL`. Neither approach guarantees termination of descendants that have
207
+ escaped the managed process tree or group; callers must not treat the returned
208
+ result as proof of complete descendant termination.
209
+
210
+ For signal-bearing invocations, Flowkit waits for the spawned shell to close
211
+ after requesting termination. If the operating system does not report closure
212
+ within one second on POSIX or three seconds on Windows, it returns the
213
+ cancellation or timeout result with the output captured so far; that bounded
214
+ fallback requests force termination and releases Flowkit's Node handles for
215
+ the root shell and any Windows `taskkill` helper, but is not confirmation that
216
+ either process, or any escaped descendant, has
217
+ exited. POSIX
218
+ signal-bearing invocations are in a separate process group, so terminal Ctrl+C
219
+ does not reach them automatically; cancel through the supplied `AbortSignal`.
220
+
221
+ Trailing stdout and stderr fragments are captured once. This corrects the
222
+ duplicate final-partial-line output in earlier releases.
223
+
224
+ ---
225
+
226
+ ### `TaskRegistry`
227
+
228
+ Registry that maps names and class paths to task constructors.
229
+
230
+ ```typescript
231
+ class TaskRegistry {
232
+ register(name: string, ctor: TaskConstructor): this;
233
+ registerClassPath(classPath: string, ctor: TaskConstructor): this;
234
+ registerAll(entries: Record<string, TaskConstructor>): this;
235
+ registerClassPaths(entries: Record<string, TaskConstructor>): this;
236
+ async resolve(classPathOrName: string): Promise<TaskConstructor>;
237
+ async create(
238
+ classPathOrName: string,
239
+ ctx: TaskContext,
240
+ options: Record<string, unknown>,
241
+ ): Promise<BaseTask>;
242
+ listRegistered(): string[];
243
+ }
244
+ ```
245
+
246
+ | Method | Description |
247
+ |--------|-------------|
248
+ | `register(name, ctor)` | Register by short name |
249
+ | `registerClassPath(path, ctor)` | Register by dotted class path |
250
+ | `registerAll(entries)` | Bulk register by short name |
251
+ | `registerClassPaths(entries)` | Bulk register by class path |
252
+ | `resolve(nameOrPath)` | Look up a constructor. Falls back to dynamic filesystem import. |
253
+ | `create(nameOrPath, ctx, opts)` | Resolve + instantiate in one call |
254
+ | `listRegistered()` | Return all registered names and class paths |
255
+
256
+ **`TaskConstructor`**
257
+
258
+ ```typescript
259
+ type TaskConstructor = new (
260
+ ctx: TaskContext,
261
+ options: Record<string, unknown>,
262
+ ) => BaseTask;
263
+ ```
264
+
265
+ ---
266
+
267
+ ## Flow
268
+
269
+ *Import from `@db-lyon/flowkit` or `@db-lyon/flowkit/flow`*
270
+
271
+ ### `FlowRunner`
272
+
273
+ Orchestration engine that executes flows.
274
+
275
+ ```typescript
276
+ class FlowRunner {
277
+ constructor(config: FlowRunnerConfig);
278
+ async run(options: FlowRunOptions): Promise<FlowRunResult>;
279
+ resolveExecutionPlan(
280
+ flow: FlowDefinition,
281
+ skipSet: Set<string>,
282
+ ): PlanStep[];
283
+ }
284
+ ```
285
+
286
+ ---
287
+
288
+ ### `FlowRunnerConfig`
289
+
290
+ ```typescript
291
+ interface FlowRunnerConfig {
292
+ tasks: Record<string, TaskDefinition>;
293
+ flows: Record<string, FlowDefinition>;
294
+ registry: TaskRegistry;
295
+ context: TaskContext;
296
+ hooks?: FlowRunnerHooks;
297
+ logger?: Logger;
298
+ }
299
+ ```
300
+
301
+ ---
302
+
303
+ ### `FlowRunOptions`
304
+
305
+ ```typescript
306
+ interface FlowRunOptions {
307
+ flowName: string; // name of the flow to execute
308
+ skip?: string[]; // task names or step numbers to skip
309
+ plan?: boolean; // return plan without executing
310
+ }
311
+ ```
312
+
313
+ ---
314
+
315
+ ### `FlowRunResult`
316
+
317
+ ```typescript
318
+ interface FlowRunResult {
319
+ success: boolean;
320
+ steps: FlowStepResult[];
321
+ duration: number; // total milliseconds
322
+ error?: Error; // first error that caused failure
323
+ }
324
+ ```
325
+
326
+ ---
327
+
328
+ ### `FlowStepResult`
329
+
330
+ ```typescript
331
+ interface FlowStepResult {
332
+ stepNumber: number;
333
+ type: 'task' | 'flow';
334
+ name: string;
335
+ result?: TaskResult;
336
+ skipped: boolean;
337
+ duration: number; // milliseconds
338
+ }
339
+ ```
340
+
341
+ ---
342
+
343
+ ### `PlanStep`
344
+
345
+ Represents a step in the execution plan (returned by plan mode or passed to hooks).
346
+
347
+ ```typescript
348
+ interface PlanStep {
349
+ stepNumber: number;
350
+ type: 'task' | 'flow';
351
+ name: string;
352
+ skipped: boolean;
353
+ options?: Record<string, unknown>;
354
+ }
355
+ ```
356
+
357
+ ---
358
+
359
+ ### `FlowRunnerHooks`
360
+
361
+ ```typescript
362
+ interface FlowRunnerHooks {
363
+ beforeRun?(flowName: string, plan: PlanStep[]): Promise<void>;
364
+ afterRun?(result: FlowRunResult): Promise<void>;
365
+ beforeStep?(step: PlanStep): Promise<void>;
366
+ afterStep?(step: PlanStep, result: FlowStepResult): Promise<void>;
367
+ onStepError?(
368
+ step: PlanStep,
369
+ error: Error,
370
+ completed: FlowStepResult[],
371
+ ): Promise<void>;
372
+ }
373
+ ```
374
+
375
+ | Hook | Fires | Scope |
376
+ |------|-------|-------|
377
+ | `beforeRun` | Once before execution starts | Top-level flow only |
378
+ | `afterRun` | Once after execution completes | Top-level flow only |
379
+ | `beforeStep` | Before each step executes | All steps (including nested) |
380
+ | `afterStep` | After each step completes | All steps (including nested) |
381
+ | `onStepError` | When a step fails | All steps (including nested) |
382
+
383
+ ---
384
+
385
+ ## DAG
386
+
387
+ *Import from `@db-lyon/flowkit` or `@db-lyon/flowkit/dag`*
388
+
389
+ ### `topologicalSort(nodes)`
390
+
391
+ Sort a directed acyclic graph in dependency order (dependencies first).
392
+
393
+ ```typescript
394
+ function topologicalSort<T>(nodes: DagNode<T>[]): DagNode<T>[]
395
+ ```
396
+
397
+ Throws `CircularDependencyError` if the graph has cycles. Throws `MissingDependencyError` if a node references a dependency that doesn't exist.
398
+
399
+ ---
400
+
401
+ ### `DagNode<T>`
402
+
403
+ ```typescript
404
+ interface DagNode<T = unknown> {
405
+ id: string;
406
+ dependencies: string[];
407
+ data: T;
408
+ }
409
+ ```
410
+
411
+ ---
412
+
413
+ ### `CircularDependencyError`
414
+
415
+ ```typescript
416
+ class CircularDependencyError extends Error {
417
+ cycle: string[]; // e.g., ['a', 'b', 'c', 'a']
418
+ }
419
+ ```
420
+
421
+ ---
422
+
423
+ ### `MissingDependencyError`
424
+
425
+ ```typescript
426
+ class MissingDependencyError extends Error {
427
+ nodeId: string; // the node that has the bad dependency
428
+ missingDep: string; // the dependency that doesn't exist
429
+ }
430
+ ```
431
+
432
+ ---
433
+
434
+ ## Guard
435
+
436
+ *Import from `@db-lyon/flowkit` or `@db-lyon/flowkit/guard`*
437
+
438
+ A before/after pipeline around one host operation. See [guards.md](guards.md) for the guide.
439
+
440
+ ### `Guard<Ctx, TResult>`
441
+
442
+ ```typescript
443
+ interface Guard<Ctx extends GuardContext = GuardContext, TResult = unknown> {
444
+ readonly name: string;
445
+ readonly order?: number; // lower runs first, default 0
446
+ appliesTo?(ctx: Ctx): boolean | Promise<boolean>; // default: always
447
+ before?(ctx: Ctx): Promise<void>; // throw to DENY
448
+ after?(ctx: Ctx, result: TResult): Promise<TResult | void>; // return to replace
449
+ }
450
+ ```
451
+
452
+ ---
453
+
454
+ ### `GuardContext`
455
+
456
+ The minimum a host context must provide. Extend it with whatever the operation carries.
457
+
458
+ ```typescript
459
+ interface GuardContext {
460
+ meta: Map<string, unknown>; // scratch space shared across guards for one operation
461
+ }
462
+
463
+ function guardContextBase(): GuardContext
464
+ ```
465
+
466
+ ---
467
+
468
+ ### `lazy(ctx, key, compute)`
469
+
470
+ Wrap a computation so it runs at most once per operation, cached into `ctx.meta` under `key`.
471
+
472
+ ```typescript
473
+ function lazy<T>(ctx: GuardContext, key: string, compute: () => T): () => T
474
+ ```
475
+
476
+ ---
477
+
478
+ ### `GuardRegistry<Ctx, TResult>`
479
+
480
+ Ordered set of guards. Sorted on registration by `order`, then by name.
481
+
482
+ ```typescript
483
+ class GuardRegistry<Ctx extends GuardContext = GuardContext, TResult = unknown> {
484
+ register(guard: Guard<Ctx, TResult>): this;
485
+ registerAll(guards: Iterable<Guard<Ctx, TResult>>): this;
486
+ list(): readonly Guard<Ctx, TResult>[];
487
+ names(): string[];
488
+ get size(): number;
489
+ }
490
+ ```
491
+
492
+ ---
493
+
494
+ ### `runGuarded(ctx, registry, invoke)`
495
+
496
+ Run one operation through the pipeline: `before` in order, `invoke`, `after` in reverse.
497
+
498
+ ```typescript
499
+ function runGuarded<Ctx extends GuardContext, TResult>(
500
+ ctx: Ctx,
501
+ registry: GuardRegistry<Ctx, TResult>,
502
+ invoke: () => Promise<TResult>,
503
+ ): Promise<TResult>
504
+ ```
505
+
506
+ Applicability resolves once, up front. A `before` throw denies the operation and propagates unchanged. With an empty registry this is exactly `invoke()`.
507
+
508
+ ---
509
+
510
+ ### `discoverTaskGuards(registry, options)`
511
+
512
+ Build a `Guard` for every `guard.<name>.<before|after><Scope?>` task in a `TaskRegistry`.
513
+
514
+ ```typescript
515
+ function discoverTaskGuards<Ctx extends GuardContext, TResult = unknown>(
516
+ registry: TaskRegistry,
517
+ options: DiscoverTaskGuardsOptions<Ctx, TResult>,
518
+ ): Guard<Ctx, TResult>[]
519
+
520
+ interface DiscoverTaskGuardsOptions<Ctx extends GuardContext, TResult = unknown> {
521
+ scopes?: Record<string, (ctx: Ctx) => boolean | Promise<boolean>>;
522
+ contextFor(ctx: Ctx): TaskContext;
523
+ optionsFor(ctx: Ctx, result?: TResult): Record<string, unknown>;
524
+ onDeny?(info: GuardTaskFailure<Ctx>): Error;
525
+ onError?(info: GuardTaskFailure<Ctx>): Error;
526
+ onAfterFailure?(info: GuardTaskFailure<Ctx>): void;
527
+ logger?: Logger;
528
+ }
529
+
530
+ interface GuardTaskFailure<Ctx extends GuardContext> {
531
+ readonly guard: string; // 'p4' for guard.p4.beforeWrite
532
+ readonly phase: string; // 'beforeWrite'
533
+ readonly taskName: string; // 'guard.p4.beforeWrite'
534
+ readonly ctx: Ctx;
535
+ readonly reason: string;
536
+ readonly cause?: Error;
537
+ }
538
+ ```
539
+
540
+ Throws at discovery if a task names a scope the host did not register.
541
+
542
+ ---
543
+
544
+ ## Logger
545
+
546
+ *Import from `@db-lyon/flowkit`*
547
+
548
+ ### `Logger` interface
549
+
550
+ ```typescript
551
+ interface Logger {
552
+ debug(...args: unknown[]): void;
553
+ info(...args: unknown[]): void;
554
+ warn(...args: unknown[]): void;
555
+ error(...args: unknown[]): void;
556
+ child(bindings: Record<string, unknown>): Logger;
557
+ }
558
+ ```
559
+
560
+ Compatible with pino, winston, and similar structured loggers.
561
+
562
+ ### `noopLogger`
563
+
564
+ A silent logger that discards all output. Used as the default when no logger is provided.
565
+
566
+ ```typescript
567
+ const noopLogger: Logger;
568
+ ```