@multiplier-labs/stepflow 0.3.3 → 0.3.6

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 (84) hide show
  1. package/README.md +9 -0
  2. package/dist/{chunk-GYPNJBDP.js → chunk-D5HW7VEG.js} +269 -48
  3. package/dist/chunk-D5HW7VEG.js.map +1 -0
  4. package/dist/{chunk-KHTUWLVZ.js → chunk-FMPJBK2N.js} +55 -10
  5. package/dist/chunk-FMPJBK2N.js.map +1 -0
  6. package/dist/{chunk-ML35PIHX.js → chunk-JPXPB7U5.js} +52 -2
  7. package/dist/chunk-JPXPB7U5.js.map +1 -0
  8. package/dist/{chunk-4VOKMGP6.js → chunk-JUPZEBR3.js} +519 -522
  9. package/dist/chunk-JUPZEBR3.js.map +1 -0
  10. package/dist/events/index.d.ts +58 -3
  11. package/dist/events/index.js +1 -1
  12. package/dist/{index-Dzr437iW.d.ts → index-Cd0IZ40L.d.ts} +50 -11
  13. package/dist/index.d.ts +2 -12
  14. package/dist/index.js +764 -485
  15. package/dist/index.js.map +1 -1
  16. package/dist/scheduler/index.d.ts +1 -1
  17. package/dist/scheduler/index.js +2 -2
  18. package/dist/storage/index.d.ts +313 -47
  19. package/dist/storage/index.js +4 -2
  20. package/package.json +27 -13
  21. package/.codekin/reports/comments/2026-04-04_comment-assessment.md +0 -179
  22. package/.codekin/reports/complexity/2026-04-03_complexity-report.md +0 -152
  23. package/.codekin/reports/dependencies/2026-04-04_dependency-health.md +0 -94
  24. package/.codekin/reports/docs-audit/2026-04-04_docs-audit.md +0 -10
  25. package/.codekin/reports/security/2026-04-03_security-audit.md +0 -208
  26. package/.github/CODEOWNERS +0 -15
  27. package/.github/dependabot.yml +0 -23
  28. package/.github/workflows/ci.yml +0 -38
  29. package/.github/workflows/publish.yml +0 -36
  30. package/.github/workflows/security.yml +0 -29
  31. package/CHANGELOG.md +0 -34
  32. package/SECURITY.md +0 -40
  33. package/dist/chunk-4VOKMGP6.js.map +0 -1
  34. package/dist/chunk-GYPNJBDP.js.map +0 -1
  35. package/dist/chunk-KHTUWLVZ.js.map +0 -1
  36. package/dist/chunk-ML35PIHX.js.map +0 -1
  37. package/docs/github-packages-setup.md +0 -97
  38. package/docs/stepflow-api-reference.md +0 -1661
  39. package/src/core/engine.test.ts +0 -1365
  40. package/src/core/engine.ts +0 -692
  41. package/src/core/index.ts +0 -7
  42. package/src/core/orchestrator.ts +0 -571
  43. package/src/core/types.ts +0 -220
  44. package/src/events/index.ts +0 -8
  45. package/src/events/memory.test.ts +0 -107
  46. package/src/events/memory.ts +0 -92
  47. package/src/events/socketio.test.ts +0 -378
  48. package/src/events/socketio.ts +0 -262
  49. package/src/events/types.ts +0 -118
  50. package/src/events/webhook.test.ts +0 -657
  51. package/src/events/webhook.ts +0 -564
  52. package/src/index.ts +0 -270
  53. package/src/planning/index.ts +0 -55
  54. package/src/planning/planner.ts +0 -576
  55. package/src/planning/planning.test.ts +0 -727
  56. package/src/planning/registry.test.ts +0 -398
  57. package/src/planning/registry.ts +0 -365
  58. package/src/planning/types.ts +0 -566
  59. package/src/scheduler/cron.test.ts +0 -471
  60. package/src/scheduler/cron.ts +0 -414
  61. package/src/scheduler/index.ts +0 -8
  62. package/src/scheduler/postgres-persistence.ts +0 -518
  63. package/src/scheduler/sqlite-persistence.test.ts +0 -235
  64. package/src/scheduler/sqlite-persistence.ts +0 -261
  65. package/src/scheduler/types.ts +0 -73
  66. package/src/storage/index.ts +0 -27
  67. package/src/storage/memory.test.ts +0 -349
  68. package/src/storage/memory.ts +0 -227
  69. package/src/storage/postgres.ts +0 -1354
  70. package/src/storage/sqlite.test.ts +0 -466
  71. package/src/storage/sqlite.ts +0 -627
  72. package/src/storage/types.ts +0 -343
  73. package/src/utils/errors.test.ts +0 -150
  74. package/src/utils/errors.ts +0 -161
  75. package/src/utils/id.ts +0 -21
  76. package/src/utils/index.ts +0 -8
  77. package/src/utils/logger.test.ts +0 -121
  78. package/src/utils/logger.ts +0 -96
  79. package/src/utils/postgres-deps.ts +0 -63
  80. package/src/utils/retry.test.ts +0 -177
  81. package/src/utils/retry.ts +0 -121
  82. package/tsconfig.json +0 -21
  83. package/tsup.config.ts +0 -14
  84. package/vitest.config.ts +0 -9
@@ -1,398 +0,0 @@
1
- import { describe, it, expect, beforeEach } from 'vitest';
2
- import { MemoryStepHandlerRegistry, MemoryRecipeRegistry, createRegistry } from './registry';
3
- import type { Recipe, RegisteredStepHandler } from './types';
4
-
5
- describe('MemoryStepHandlerRegistry', () => {
6
- let registry: MemoryStepHandlerRegistry;
7
-
8
- beforeEach(() => {
9
- registry = new MemoryStepHandlerRegistry();
10
- });
11
-
12
- it('should register and retrieve a handler', () => {
13
- const handler: RegisteredStepHandler = {
14
- id: 'handler.a',
15
- description: 'Handler A',
16
- handler: async () => 'result',
17
- };
18
-
19
- registry.register(handler);
20
-
21
- expect(registry.get('handler.a')).toBeDefined();
22
- expect(registry.get('handler.a')?.id).toBe('handler.a');
23
- expect(registry.has('handler.a')).toBe(true);
24
- });
25
-
26
- it('should throw on duplicate registration', () => {
27
- registry.register({ id: 'h1', handler: async () => {} });
28
-
29
- expect(() => registry.register({ id: 'h1', handler: async () => {} }))
30
- .toThrow("Step handler 'h1' is already registered");
31
- });
32
-
33
- it('should return undefined for non-existent handler', () => {
34
- expect(registry.get('nonexistent')).toBeUndefined();
35
- expect(registry.has('nonexistent')).toBe(false);
36
- });
37
-
38
- it('should list all handlers', () => {
39
- registry.register({ id: 'h1', handler: async () => {} });
40
- registry.register({ id: 'h2', handler: async () => {} });
41
-
42
- const list = registry.list();
43
- expect(list).toHaveLength(2);
44
- expect(list.map(h => h.id).sort()).toEqual(['h1', 'h2']);
45
- });
46
-
47
- it('should list handlers by tag', () => {
48
- registry.register({ id: 'h1', handler: async () => {}, tags: ['ai', 'text'] });
49
- registry.register({ id: 'h2', handler: async () => {}, tags: ['ai'] });
50
- registry.register({ id: 'h3', handler: async () => {}, tags: ['io'] });
51
-
52
- const aiHandlers = registry.listByTag('ai');
53
- expect(aiHandlers).toHaveLength(2);
54
- expect(aiHandlers.map(h => h.id).sort()).toEqual(['h1', 'h2']);
55
-
56
- expect(registry.listByTag('nonexistent')).toEqual([]);
57
- });
58
-
59
- it('should resolve handler reference to handler function', () => {
60
- const fn = async () => 'resolved';
61
- registry.register({ id: 'h1', handler: fn });
62
-
63
- expect(registry.resolve('h1')).toBe(fn);
64
- expect(registry.resolve('nonexistent')).toBeUndefined();
65
- });
66
-
67
- it('should clear all handlers and tag indexes', () => {
68
- registry.register({ id: 'h1', handler: async () => {}, tags: ['t1'] });
69
- registry.clear();
70
-
71
- expect(registry.list()).toHaveLength(0);
72
- expect(registry.listByTag('t1')).toEqual([]);
73
- });
74
- });
75
-
76
- describe('MemoryRecipeRegistry', () => {
77
- let registry: MemoryRecipeRegistry;
78
-
79
- const makeRecipe = (overrides: Partial<Recipe> = {}): Recipe => ({
80
- id: 'recipe-1',
81
- name: 'Recipe 1',
82
- workflowKind: 'test.workflow',
83
- variant: 'default',
84
- steps: [{ key: 's1', name: 'S1', handlerRef: 'h1' }],
85
- ...overrides,
86
- });
87
-
88
- beforeEach(() => {
89
- registry = new MemoryRecipeRegistry();
90
- });
91
-
92
- describe('register / get / has', () => {
93
- it('should register and retrieve a recipe', () => {
94
- const recipe = makeRecipe();
95
- registry.register(recipe);
96
-
97
- expect(registry.get('recipe-1')).toEqual(recipe);
98
- expect(registry.has('recipe-1')).toBe(true);
99
- });
100
-
101
- it('should throw on duplicate recipe id', () => {
102
- registry.register(makeRecipe());
103
-
104
- expect(() => registry.register(makeRecipe()))
105
- .toThrow("Recipe 'recipe-1' is already registered");
106
- });
107
-
108
- it('should throw on duplicate kind:variant', () => {
109
- registry.register(makeRecipe({ id: 'r1' }));
110
-
111
- expect(() => registry.register(makeRecipe({ id: 'r2' })))
112
- .toThrow("Recipe variant 'default' for 'test.workflow' is already registered");
113
- });
114
-
115
- it('should return undefined for non-existent recipe', () => {
116
- expect(registry.get('nonexistent')).toBeUndefined();
117
- expect(registry.has('nonexistent')).toBe(false);
118
- });
119
- });
120
-
121
- describe('registerAll', () => {
122
- it('should register multiple recipes', () => {
123
- registry.registerAll([
124
- makeRecipe({ id: 'r1', variant: 'fast' }),
125
- makeRecipe({ id: 'r2', variant: 'slow' }),
126
- ]);
127
-
128
- expect(registry.list()).toHaveLength(2);
129
- });
130
- });
131
-
132
- describe('getByKind', () => {
133
- it('should return recipes matching a workflow kind', () => {
134
- registry.register(makeRecipe({ id: 'r1', variant: 'v1' }));
135
- registry.register(makeRecipe({ id: 'r2', variant: 'v2' }));
136
- registry.register(makeRecipe({ id: 'r3', workflowKind: 'other', variant: 'default' }));
137
-
138
- const results = registry.getByKind('test.workflow');
139
- expect(results).toHaveLength(2);
140
- });
141
-
142
- it('should return empty array for unknown kind', () => {
143
- expect(registry.getByKind('unknown')).toEqual([]);
144
- });
145
- });
146
-
147
- describe('getVariant', () => {
148
- it('should return recipe by kind and variant', () => {
149
- registry.register(makeRecipe({ id: 'r1', variant: 'fast' }));
150
- registry.register(makeRecipe({ id: 'r2', workflowKind: 'other', variant: 'default' }));
151
-
152
- expect(registry.getVariant('test.workflow', 'fast')?.id).toBe('r1');
153
- expect(registry.getVariant('test.workflow', 'nonexistent')).toBeUndefined();
154
- });
155
- });
156
-
157
- describe('getDefault', () => {
158
- it('should return the "default" variant', () => {
159
- registry.register(makeRecipe({ id: 'r1', variant: 'default' }));
160
- registry.register(makeRecipe({ id: 'r2', variant: 'fast' }));
161
-
162
- expect(registry.getDefault('test.workflow')?.id).toBe('r1');
163
- });
164
-
165
- it('should fall back to lowest priority recipe', () => {
166
- registry.register(makeRecipe({ id: 'r1', variant: 'fast', priority: 10 }));
167
- registry.register(makeRecipe({ id: 'r2', variant: 'slow', priority: 1 }));
168
-
169
- expect(registry.getDefault('test.workflow')?.id).toBe('r2');
170
- });
171
-
172
- it('should return undefined for unknown kind', () => {
173
- expect(registry.getDefault('unknown')).toBeUndefined();
174
- });
175
-
176
- it('should handle recipes without explicit priority', () => {
177
- registry.register(makeRecipe({ id: 'r1', variant: 'v1' }));
178
-
179
- expect(registry.getDefault('test.workflow')?.id).toBe('r1');
180
- });
181
- });
182
-
183
- describe('listVariants', () => {
184
- it('should list all variants for a kind', () => {
185
- registry.register(makeRecipe({ id: 'r1', variant: 'fast' }));
186
- registry.register(makeRecipe({ id: 'r2', variant: 'slow' }));
187
-
188
- expect(registry.listVariants('test.workflow').sort()).toEqual(['fast', 'slow']);
189
- });
190
- });
191
-
192
- describe('query', () => {
193
- beforeEach(() => {
194
- registry.register(makeRecipe({ id: 'r1', variant: 'fast', tags: ['ai'] }));
195
- registry.register(makeRecipe({ id: 'r2', variant: 'slow', tags: ['legacy'], workflowKind: 'other' }));
196
- registry.register(makeRecipe({
197
- id: 'r3',
198
- variant: 'conditional',
199
- workflowKind: 'other',
200
- conditions: [{ field: 'size', operator: 'gt', value: 100 }],
201
- }));
202
- });
203
-
204
- it('should filter by workflowKind', () => {
205
- const results = registry.query({ workflowKind: 'other' });
206
- expect(results).toHaveLength(2);
207
- });
208
-
209
- it('should filter by variant', () => {
210
- const results = registry.query({ variant: 'fast' });
211
- expect(results).toHaveLength(1);
212
- expect(results[0].id).toBe('r1');
213
- });
214
-
215
- it('should filter by tags', () => {
216
- const results = registry.query({ tags: ['ai'] });
217
- expect(results).toHaveLength(1);
218
- expect(results[0].id).toBe('r1');
219
- });
220
-
221
- it('should filter by matchConditions', () => {
222
- const results = registry.query({ matchConditions: { size: 200 } });
223
- // r1 has no conditions (passes), r2 has no conditions (passes), r3 has size > 100 (passes)
224
- expect(results).toHaveLength(3);
225
- });
226
-
227
- it('should exclude recipes where conditions fail', () => {
228
- const results = registry.query({ matchConditions: { size: 50 } });
229
- // r3 has size > 100 which fails for 50
230
- expect(results).toHaveLength(2);
231
- expect(results.find(r => r.id === 'r3')).toBeUndefined();
232
- });
233
- });
234
-
235
- describe('condition evaluation', () => {
236
- const registerConditional = (field: string, operator: string, value?: unknown) => {
237
- const id = `cond-${Math.random()}`;
238
- registry.register(makeRecipe({
239
- id,
240
- variant: id,
241
- conditions: [{ field, operator: operator as any, value }],
242
- }));
243
- return id;
244
- };
245
-
246
- it('should evaluate eq/neq', () => {
247
- const eqId = registerConditional('x', 'eq', 5);
248
- const neqId = registerConditional('x', 'neq', 5);
249
-
250
- let results = registry.query({ matchConditions: { x: 5 } });
251
- expect(results.find(r => r.id === eqId)).toBeDefined();
252
-
253
- results = registry.query({ matchConditions: { x: 3 } });
254
- expect(results.find(r => r.id === eqId)).toBeUndefined();
255
- expect(results.find(r => r.id === neqId)).toBeDefined();
256
- });
257
-
258
- it('should evaluate gt/gte/lt/lte', () => {
259
- const gtId = registerConditional('n', 'gt', 10);
260
- const gteId = registerConditional('n', 'gte', 10);
261
- const ltId = registerConditional('n', 'lt', 10);
262
- const lteId = registerConditional('n', 'lte', 10);
263
-
264
- let results = registry.query({ matchConditions: { n: 15 } });
265
- expect(results.find(r => r.id === gtId)).toBeDefined();
266
- expect(results.find(r => r.id === gteId)).toBeDefined();
267
- expect(results.find(r => r.id === ltId)).toBeUndefined();
268
-
269
- results = registry.query({ matchConditions: { n: 10 } });
270
- expect(results.find(r => r.id === gtId)).toBeUndefined();
271
- expect(results.find(r => r.id === gteId)).toBeDefined();
272
- expect(results.find(r => r.id === lteId)).toBeDefined();
273
-
274
- results = registry.query({ matchConditions: { n: 5 } });
275
- expect(results.find(r => r.id === ltId)).toBeDefined();
276
- expect(results.find(r => r.id === lteId)).toBeDefined();
277
- });
278
-
279
- it('should return false for gt/lt with non-number values', () => {
280
- const gtId = registerConditional('x', 'gt', 10);
281
-
282
- const results = registry.query({ matchConditions: { x: 'not a number' } });
283
- expect(results.find(r => r.id === gtId)).toBeUndefined();
284
- });
285
-
286
- it('should evaluate contains for strings', () => {
287
- const id = registerConditional('text', 'contains', 'hello');
288
-
289
- let results = registry.query({ matchConditions: { text: 'say hello world' } });
290
- expect(results.find(r => r.id === id)).toBeDefined();
291
-
292
- results = registry.query({ matchConditions: { text: 'no match' } });
293
- expect(results.find(r => r.id === id)).toBeUndefined();
294
- });
295
-
296
- it('should evaluate contains for arrays', () => {
297
- const id = registerConditional('tags', 'contains', 'important');
298
-
299
- const results = registry.query({ matchConditions: { tags: ['important', 'other'] } });
300
- expect(results.find(r => r.id === id)).toBeDefined();
301
- });
302
-
303
- it('should return false for contains with non-string/non-array', () => {
304
- const id = registerConditional('x', 'contains', 'test');
305
-
306
- const results = registry.query({ matchConditions: { x: 42 } });
307
- expect(results.find(r => r.id === id)).toBeUndefined();
308
- });
309
-
310
- it('should evaluate matches (regex)', () => {
311
- const id = registerConditional('name', 'matches', '^test-\\d+$');
312
-
313
- let results = registry.query({ matchConditions: { name: 'test-123' } });
314
- expect(results.find(r => r.id === id)).toBeDefined();
315
-
316
- results = registry.query({ matchConditions: { name: 'not-matching' } });
317
- expect(results.find(r => r.id === id)).toBeUndefined();
318
- });
319
-
320
- it('should return false for matches with invalid regex', () => {
321
- const id = registerConditional('x', 'matches', '[invalid');
322
-
323
- const results = registry.query({ matchConditions: { x: 'anything' } });
324
- expect(results.find(r => r.id === id)).toBeUndefined();
325
- });
326
-
327
- it('should return false for matches with non-string field', () => {
328
- const id = registerConditional('x', 'matches', '.*');
329
-
330
- const results = registry.query({ matchConditions: { x: 42 } });
331
- expect(results.find(r => r.id === id)).toBeUndefined();
332
- });
333
-
334
- it('should evaluate exists/notExists', () => {
335
- const existsId = registerConditional('field', 'exists');
336
- const notExistsId = registerConditional('field', 'notExists');
337
-
338
- let results = registry.query({ matchConditions: { field: 'present' } });
339
- expect(results.find(r => r.id === existsId)).toBeDefined();
340
- expect(results.find(r => r.id === notExistsId)).toBeUndefined();
341
-
342
- results = registry.query({ matchConditions: { field: '' } });
343
- expect(results.find(r => r.id === existsId)).toBeUndefined();
344
- expect(results.find(r => r.id === notExistsId)).toBeDefined();
345
-
346
- results = registry.query({ matchConditions: { field: null } });
347
- expect(results.find(r => r.id === existsId)).toBeUndefined();
348
- expect(results.find(r => r.id === notExistsId)).toBeDefined();
349
-
350
- results = registry.query({ matchConditions: {} });
351
- expect(results.find(r => r.id === existsId)).toBeUndefined();
352
- expect(results.find(r => r.id === notExistsId)).toBeDefined();
353
- });
354
-
355
- it('should handle nested field paths', () => {
356
- const id = registerConditional('a.b.c', 'eq', 42);
357
-
358
- let results = registry.query({ matchConditions: { a: { b: { c: 42 } } } });
359
- expect(results.find(r => r.id === id)).toBeDefined();
360
-
361
- results = registry.query({ matchConditions: { a: { b: { c: 99 } } } });
362
- expect(results.find(r => r.id === id)).toBeUndefined();
363
- });
364
-
365
- it('should return undefined for missing nested path segments', () => {
366
- const id = registerConditional('a.b.c', 'exists');
367
-
368
- const results = registry.query({ matchConditions: { a: 'not-object' } });
369
- expect(results.find(r => r.id === id)).toBeUndefined();
370
- });
371
-
372
- it('should return false for unknown operator', () => {
373
- const id = registerConditional('x', 'unknownOp' as any, 1);
374
-
375
- const results = registry.query({ matchConditions: { x: 1 } });
376
- expect(results.find(r => r.id === id)).toBeUndefined();
377
- });
378
- });
379
-
380
- describe('clear', () => {
381
- it('should clear all registries', () => {
382
- registry.register(makeRecipe({ id: 'r1', variant: 'v1', tags: ['t1'] }));
383
- registry.clear();
384
-
385
- expect(registry.list()).toHaveLength(0);
386
- expect(registry.getByKind('test.workflow')).toEqual([]);
387
- expect(registry.listVariants('test.workflow')).toEqual([]);
388
- });
389
- });
390
- });
391
-
392
- describe('createRegistry', () => {
393
- it('should return a combined registry', () => {
394
- const reg = createRegistry();
395
- expect(reg.recipes).toBeInstanceOf(MemoryRecipeRegistry);
396
- expect(reg.handlers).toBeInstanceOf(MemoryStepHandlerRegistry);
397
- });
398
- });