@multiplier-labs/stepflow 0.3.1 → 0.3.3

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/.codekin/reports/comments/2026-04-04_comment-assessment.md +179 -0
  2. package/.codekin/reports/complexity/2026-04-03_complexity-report.md +152 -0
  3. package/.codekin/reports/dependencies/2026-04-04_dependency-health.md +94 -0
  4. package/.codekin/reports/docs-audit/2026-04-04_docs-audit.md +10 -0
  5. package/.codekin/reports/security/2026-04-03_security-audit.md +208 -0
  6. package/.github/CODEOWNERS +15 -0
  7. package/.github/dependabot.yml +23 -0
  8. package/.github/workflows/ci.yml +38 -0
  9. package/.github/workflows/publish.yml +11 -5
  10. package/.github/workflows/security.yml +29 -0
  11. package/CHANGELOG.md +34 -0
  12. package/README.md +24 -0
  13. package/SECURITY.md +40 -0
  14. package/dist/{chunk-FQCT3I2A.js → chunk-4VOKMGP6.js} +23 -42
  15. package/dist/chunk-4VOKMGP6.js.map +1 -0
  16. package/dist/{chunk-XCF4PLN7.js → chunk-GYPNJBDP.js} +12 -16
  17. package/dist/chunk-GYPNJBDP.js.map +1 -0
  18. package/dist/{chunk-U5QGN3RT.js → chunk-KHTUWLVZ.js} +14 -47
  19. package/dist/chunk-KHTUWLVZ.js.map +1 -0
  20. package/dist/{chunk-GXY66V7Q.js → chunk-ML35PIHX.js} +1 -1
  21. package/dist/chunk-ML35PIHX.js.map +1 -0
  22. package/dist/events/index.d.ts +4 -4
  23. package/dist/events/index.js +1 -1
  24. package/dist/{index-CPfGsmrp.d.ts → index-Dzr437iW.d.ts} +6 -6
  25. package/dist/index.d.ts +14 -12
  26. package/dist/index.js +40 -82
  27. package/dist/index.js.map +1 -1
  28. package/dist/scheduler/index.d.ts +4 -4
  29. package/dist/scheduler/index.js +2 -2
  30. package/dist/storage/index.d.ts +13 -13
  31. package/dist/storage/index.js +2 -2
  32. package/dist/{types-BlwFEGzj.d.ts → types-DSEjiLMj.d.ts} +16 -15
  33. package/dist/{types-DzkEqnQJ.d.ts → types-DZUdWmCc.d.ts} +2 -2
  34. package/dist/{types-BVShq7zT.d.ts → types-K5Gjk3H_.d.ts} +4 -4
  35. package/docs/stepflow-api-reference.md +2 -3
  36. package/package.json +7 -6
  37. package/src/core/engine.test.ts +466 -522
  38. package/src/core/engine.ts +42 -67
  39. package/src/core/index.ts +3 -3
  40. package/src/core/orchestrator.ts +64 -83
  41. package/src/core/types.ts +7 -29
  42. package/src/events/index.ts +4 -4
  43. package/src/events/memory.test.ts +39 -41
  44. package/src/events/memory.ts +7 -10
  45. package/src/events/socketio.test.ts +112 -162
  46. package/src/events/socketio.ts +20 -27
  47. package/src/events/types.ts +16 -19
  48. package/src/events/webhook.test.ts +197 -235
  49. package/src/events/webhook.ts +44 -71
  50. package/src/index.ts +19 -19
  51. package/src/planning/index.ts +6 -3
  52. package/src/planning/planner.ts +66 -91
  53. package/src/planning/planning.test.ts +303 -366
  54. package/src/planning/registry.test.ts +204 -251
  55. package/src/planning/registry.ts +39 -43
  56. package/src/planning/types.ts +23 -27
  57. package/src/scheduler/cron.test.ts +141 -150
  58. package/src/scheduler/cron.ts +32 -67
  59. package/src/scheduler/index.ts +4 -14
  60. package/src/scheduler/postgres-persistence.ts +71 -99
  61. package/src/scheduler/sqlite-persistence.test.ts +96 -96
  62. package/src/scheduler/sqlite-persistence.ts +12 -19
  63. package/src/scheduler/types.ts +4 -9
  64. package/src/storage/index.ts +5 -5
  65. package/src/storage/memory.test.ts +133 -183
  66. package/src/storage/memory.ts +21 -42
  67. package/src/storage/postgres.ts +197 -393
  68. package/src/storage/sqlite.test.ts +154 -281
  69. package/src/storage/sqlite.ts +50 -85
  70. package/src/storage/types.ts +33 -58
  71. package/src/utils/errors.test.ts +78 -80
  72. package/src/utils/errors.ts +27 -57
  73. package/src/utils/id.ts +3 -3
  74. package/src/utils/index.ts +4 -4
  75. package/src/utils/logger.test.ts +52 -58
  76. package/src/utils/logger.ts +4 -4
  77. package/src/utils/postgres-deps.ts +4 -4
  78. package/src/utils/retry.test.ts +47 -67
  79. package/src/utils/retry.ts +9 -9
  80. package/dist/chunk-FQCT3I2A.js.map +0 -1
  81. package/dist/chunk-GXY66V7Q.js.map +0 -1
  82. package/dist/chunk-U5QGN3RT.js.map +0 -1
  83. package/dist/chunk-XCF4PLN7.js.map +0 -1
  84. package/src/scheduler/postgres-persistence.test.ts +0 -906
@@ -1,13 +1,9 @@
1
- import { describe, it, expect, beforeEach } from "vitest";
2
- import {
3
- MemoryRecipeRegistry,
4
- MemoryStepHandlerRegistry,
5
- createRegistry,
6
- } from "./registry";
7
- import { RuleBasedPlanner } from "./planner";
8
- import type { Recipe, RecipeCondition, PlanningContext } from "./types";
9
-
10
- describe("MemoryRecipeRegistry", () => {
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { MemoryRecipeRegistry, MemoryStepHandlerRegistry, createRegistry } from './registry';
3
+ import { RuleBasedPlanner } from './planner';
4
+ import type { Recipe, RecipeCondition, PlanningContext } from './types';
5
+
6
+ describe('MemoryRecipeRegistry', () => {
11
7
  let registry: MemoryRecipeRegistry;
12
8
 
13
9
  beforeEach(() => {
@@ -15,225 +11,195 @@ describe("MemoryRecipeRegistry", () => {
15
11
  });
16
12
 
17
13
  const createTestRecipe = (overrides: Partial<Recipe> = {}): Recipe => ({
18
- id: "test.recipe",
19
- name: "Test Recipe",
20
- workflowKind: "test.workflow",
21
- variant: "default",
22
- steps: [{ key: "step1", name: "Step 1", handlerRef: "handlers.step1" }],
14
+ id: 'test.recipe',
15
+ name: 'Test Recipe',
16
+ workflowKind: 'test.workflow',
17
+ variant: 'default',
18
+ steps: [
19
+ { key: 'step1', name: 'Step 1', handlerRef: 'handlers.step1' },
20
+ ],
23
21
  ...overrides,
24
22
  });
25
23
 
26
- describe("register", () => {
27
- it("should register a recipe", () => {
24
+ describe('register', () => {
25
+ it('should register a recipe', () => {
28
26
  const recipe = createTestRecipe();
29
27
  registry.register(recipe);
30
28
 
31
- expect(registry.has("test.recipe")).toBe(true);
32
- expect(registry.get("test.recipe")).toEqual(recipe);
29
+ expect(registry.has('test.recipe')).toBe(true);
30
+ expect(registry.get('test.recipe')).toEqual(recipe);
33
31
  });
34
32
 
35
- it("should throw if recipe already registered", () => {
33
+ it('should throw if recipe already registered', () => {
36
34
  const recipe = createTestRecipe();
37
35
  registry.register(recipe);
38
36
 
39
37
  expect(() => registry.register(recipe)).toThrow(
40
- "Recipe 'test.recipe' is already registered",
38
+ "Recipe 'test.recipe' is already registered"
41
39
  );
42
40
  });
43
41
 
44
- it("should throw if variant already exists for workflow kind", () => {
45
- const recipe1 = createTestRecipe({ id: "recipe1" });
46
- const recipe2 = createTestRecipe({ id: "recipe2" });
42
+ it('should throw if variant already exists for workflow kind', () => {
43
+ const recipe1 = createTestRecipe({ id: 'recipe1' });
44
+ const recipe2 = createTestRecipe({ id: 'recipe2' });
47
45
 
48
46
  registry.register(recipe1);
49
47
 
50
48
  expect(() => registry.register(recipe2)).toThrow(
51
- "Recipe variant 'default' for 'test.workflow' is already registered",
49
+ "Recipe variant 'default' for 'test.workflow' is already registered"
52
50
  );
53
51
  });
54
52
  });
55
53
 
56
- describe("getByKind", () => {
57
- it("should return all recipes for a workflow kind", () => {
58
- registry.register(
59
- createTestRecipe({ id: "recipe1", variant: "default" }),
60
- );
61
- registry.register(createTestRecipe({ id: "recipe2", variant: "fast" }));
62
- registry.register(
63
- createTestRecipe({
64
- id: "recipe3",
65
- workflowKind: "other.workflow",
66
- variant: "default",
67
- }),
68
- );
54
+ describe('getByKind', () => {
55
+ it('should return all recipes for a workflow kind', () => {
56
+ registry.register(createTestRecipe({ id: 'recipe1', variant: 'default' }));
57
+ registry.register(createTestRecipe({ id: 'recipe2', variant: 'fast' }));
58
+ registry.register(createTestRecipe({
59
+ id: 'recipe3',
60
+ workflowKind: 'other.workflow',
61
+ variant: 'default',
62
+ }));
69
63
 
70
- const recipes = registry.getByKind("test.workflow");
64
+ const recipes = registry.getByKind('test.workflow');
71
65
  expect(recipes).toHaveLength(2);
72
- expect(recipes.map((r) => r.id)).toContain("recipe1");
73
- expect(recipes.map((r) => r.id)).toContain("recipe2");
66
+ expect(recipes.map(r => r.id)).toContain('recipe1');
67
+ expect(recipes.map(r => r.id)).toContain('recipe2');
74
68
  });
75
69
 
76
- it("should return empty array if no recipes found", () => {
77
- expect(registry.getByKind("nonexistent")).toEqual([]);
70
+ it('should return empty array if no recipes found', () => {
71
+ expect(registry.getByKind('nonexistent')).toEqual([]);
78
72
  });
79
73
  });
80
74
 
81
- describe("getVariant", () => {
82
- it("should return specific variant", () => {
83
- registry.register(
84
- createTestRecipe({ id: "recipe1", variant: "default" }),
85
- );
86
- registry.register(createTestRecipe({ id: "recipe2", variant: "fast" }));
75
+ describe('getVariant', () => {
76
+ it('should return specific variant', () => {
77
+ registry.register(createTestRecipe({ id: 'recipe1', variant: 'default' }));
78
+ registry.register(createTestRecipe({ id: 'recipe2', variant: 'fast' }));
87
79
 
88
- const recipe = registry.getVariant("test.workflow", "fast");
89
- expect(recipe?.id).toBe("recipe2");
80
+ const recipe = registry.getVariant('test.workflow', 'fast');
81
+ expect(recipe?.id).toBe('recipe2');
90
82
  });
91
83
 
92
- it("should return undefined if variant not found", () => {
84
+ it('should return undefined if variant not found', () => {
93
85
  registry.register(createTestRecipe());
94
86
 
95
- expect(
96
- registry.getVariant("test.workflow", "nonexistent"),
97
- ).toBeUndefined();
87
+ expect(registry.getVariant('test.workflow', 'nonexistent')).toBeUndefined();
98
88
  });
99
89
  });
100
90
 
101
- describe("getDefault", () => {
102
- it("should return default variant if exists", () => {
103
- registry.register(
104
- createTestRecipe({ id: "recipe1", variant: "default" }),
105
- );
106
- registry.register(createTestRecipe({ id: "recipe2", variant: "fast" }));
91
+ describe('getDefault', () => {
92
+ it('should return default variant if exists', () => {
93
+ registry.register(createTestRecipe({ id: 'recipe1', variant: 'default' }));
94
+ registry.register(createTestRecipe({ id: 'recipe2', variant: 'fast' }));
107
95
 
108
- const recipe = registry.getDefault("test.workflow");
109
- expect(recipe?.variant).toBe("default");
96
+ const recipe = registry.getDefault('test.workflow');
97
+ expect(recipe?.variant).toBe('default');
110
98
  });
111
99
 
112
- it("should return lowest priority recipe if no default variant", () => {
113
- registry.register(
114
- createTestRecipe({ id: "recipe1", variant: "fast", priority: 10 }),
115
- );
116
- registry.register(
117
- createTestRecipe({ id: "recipe2", variant: "slow", priority: 5 }),
118
- );
100
+ it('should return lowest priority recipe if no default variant', () => {
101
+ registry.register(createTestRecipe({ id: 'recipe1', variant: 'fast', priority: 10 }));
102
+ registry.register(createTestRecipe({ id: 'recipe2', variant: 'slow', priority: 5 }));
119
103
 
120
- const recipe = registry.getDefault("test.workflow");
121
- expect(recipe?.id).toBe("recipe2");
104
+ const recipe = registry.getDefault('test.workflow');
105
+ expect(recipe?.id).toBe('recipe2');
122
106
  });
123
107
  });
124
108
 
125
- describe("listVariants", () => {
126
- it("should list all variants for a workflow kind", () => {
127
- registry.register(
128
- createTestRecipe({ id: "recipe1", variant: "default" }),
129
- );
130
- registry.register(createTestRecipe({ id: "recipe2", variant: "fast" }));
131
- registry.register(
132
- createTestRecipe({ id: "recipe3", variant: "thorough" }),
133
- );
109
+ describe('listVariants', () => {
110
+ it('should list all variants for a workflow kind', () => {
111
+ registry.register(createTestRecipe({ id: 'recipe1', variant: 'default' }));
112
+ registry.register(createTestRecipe({ id: 'recipe2', variant: 'fast' }));
113
+ registry.register(createTestRecipe({ id: 'recipe3', variant: 'thorough' }));
134
114
 
135
- const variants = registry.listVariants("test.workflow");
115
+ const variants = registry.listVariants('test.workflow');
136
116
  expect(variants).toHaveLength(3);
137
- expect(variants).toContain("default");
138
- expect(variants).toContain("fast");
139
- expect(variants).toContain("thorough");
117
+ expect(variants).toContain('default');
118
+ expect(variants).toContain('fast');
119
+ expect(variants).toContain('thorough');
140
120
  });
141
121
  });
142
122
 
143
- describe("query", () => {
144
- it("should filter by workflow kind", () => {
145
- registry.register(createTestRecipe({ id: "recipe1", variant: "v1" }));
146
- registry.register(
147
- createTestRecipe({
148
- id: "recipe2",
149
- workflowKind: "other",
150
- variant: "v1",
151
- }),
152
- );
123
+ describe('query', () => {
124
+ it('should filter by workflow kind', () => {
125
+ registry.register(createTestRecipe({ id: 'recipe1', variant: 'v1' }));
126
+ registry.register(createTestRecipe({
127
+ id: 'recipe2',
128
+ workflowKind: 'other',
129
+ variant: 'v1',
130
+ }));
153
131
 
154
- const results = registry.query({ workflowKind: "test.workflow" });
132
+ const results = registry.query({ workflowKind: 'test.workflow' });
155
133
  expect(results).toHaveLength(1);
156
- expect(results[0].id).toBe("recipe1");
134
+ expect(results[0].id).toBe('recipe1');
157
135
  });
158
136
 
159
- it("should filter by tags", () => {
160
- registry.register(
161
- createTestRecipe({
162
- id: "recipe1",
163
- variant: "v1",
164
- tags: ["fast", "simple"],
165
- }),
166
- );
167
- registry.register(
168
- createTestRecipe({
169
- id: "recipe2",
170
- variant: "v2",
171
- tags: ["slow", "complex"],
172
- }),
173
- );
137
+ it('should filter by tags', () => {
138
+ registry.register(createTestRecipe({ id: 'recipe1', variant: 'v1', tags: ['fast', 'simple'] }));
139
+ registry.register(createTestRecipe({ id: 'recipe2', variant: 'v2', tags: ['slow', 'complex'] }));
174
140
 
175
- const results = registry.query({ tags: ["fast"] });
141
+ const results = registry.query({ tags: ['fast'] });
176
142
  expect(results).toHaveLength(1);
177
- expect(results[0].id).toBe("recipe1");
143
+ expect(results[0].id).toBe('recipe1');
178
144
  });
179
145
  });
180
146
  });
181
147
 
182
- describe("MemoryStepHandlerRegistry", () => {
148
+ describe('MemoryStepHandlerRegistry', () => {
183
149
  let registry: MemoryStepHandlerRegistry;
184
150
 
185
151
  beforeEach(() => {
186
152
  registry = new MemoryStepHandlerRegistry();
187
153
  });
188
154
 
189
- it("should register and retrieve handlers", () => {
155
+ it('should register and retrieve handlers', () => {
190
156
  const handler = {
191
- id: "test.handler",
192
- description: "Test handler",
193
- handler: async () => ({ result: "test" }),
157
+ id: 'test.handler',
158
+ description: 'Test handler',
159
+ handler: async () => ({ result: 'test' }),
194
160
  };
195
161
 
196
162
  registry.register(handler);
197
163
 
198
- expect(registry.has("test.handler")).toBe(true);
199
- expect(registry.get("test.handler")).toEqual(handler);
164
+ expect(registry.has('test.handler')).toBe(true);
165
+ expect(registry.get('test.handler')).toEqual(handler);
200
166
  });
201
167
 
202
- it("should throw if handler already registered", () => {
168
+ it('should throw if handler already registered', () => {
203
169
  const handler = {
204
- id: "test.handler",
170
+ id: 'test.handler',
205
171
  handler: async () => ({}),
206
172
  };
207
173
 
208
174
  registry.register(handler);
209
175
 
210
176
  expect(() => registry.register(handler)).toThrow(
211
- "Step handler 'test.handler' is already registered",
177
+ "Step handler 'test.handler' is already registered"
212
178
  );
213
179
  });
214
180
 
215
- it("should list handlers by tag", () => {
181
+ it('should list handlers by tag', () => {
216
182
  registry.register({
217
- id: "handler1",
183
+ id: 'handler1',
218
184
  handler: async () => ({}),
219
- tags: ["llm", "text"],
185
+ tags: ['llm', 'text'],
220
186
  });
221
187
  registry.register({
222
- id: "handler2",
188
+ id: 'handler2',
223
189
  handler: async () => ({}),
224
- tags: ["data", "text"],
190
+ tags: ['data', 'text'],
225
191
  });
226
192
 
227
- const llmHandlers = registry.listByTag("llm");
193
+ const llmHandlers = registry.listByTag('llm');
228
194
  expect(llmHandlers).toHaveLength(1);
229
- expect(llmHandlers[0].id).toBe("handler1");
195
+ expect(llmHandlers[0].id).toBe('handler1');
230
196
 
231
- const textHandlers = registry.listByTag("text");
197
+ const textHandlers = registry.listByTag('text');
232
198
  expect(textHandlers).toHaveLength(2);
233
199
  });
234
200
  });
235
201
 
236
- describe("RuleBasedPlanner", () => {
202
+ describe('RuleBasedPlanner', () => {
237
203
  let registry: MemoryRecipeRegistry;
238
204
  let planner: RuleBasedPlanner;
239
205
 
@@ -243,156 +209,132 @@ describe("RuleBasedPlanner", () => {
243
209
  });
244
210
 
245
211
  const createTestRecipe = (overrides: Partial<Recipe> = {}): Recipe => ({
246
- id: "test.recipe",
247
- name: "Test Recipe",
248
- workflowKind: "test.workflow",
249
- variant: "default",
212
+ id: 'test.recipe',
213
+ name: 'Test Recipe',
214
+ workflowKind: 'test.workflow',
215
+ variant: 'default',
250
216
  steps: [
251
- { key: "step1", name: "Step 1", handlerRef: "handlers.step1" },
252
- { key: "step2", name: "Step 2", handlerRef: "handlers.step2" },
217
+ { key: 'step1', name: 'Step 1', handlerRef: 'handlers.step1' },
218
+ { key: 'step2', name: 'Step 2', handlerRef: 'handlers.step2' },
253
219
  ],
254
220
  ...overrides,
255
221
  });
256
222
 
257
- describe("selectRecipe", () => {
258
- it("should select recipe with matching conditions", async () => {
223
+ describe('selectRecipe', () => {
224
+ it('should select recipe with matching conditions', async () => {
259
225
  const conditions: RecipeCondition[] = [
260
- { field: "priority", operator: "eq", value: "speed" },
226
+ { field: 'priority', operator: 'eq', value: 'speed' },
261
227
  ];
262
228
 
263
- registry.register(
264
- createTestRecipe({ id: "default", variant: "default" }),
265
- );
266
- registry.register(
267
- createTestRecipe({
268
- id: "fast",
269
- variant: "fast",
270
- conditions,
271
- priority: 10,
272
- }),
273
- );
229
+ registry.register(createTestRecipe({ id: 'default', variant: 'default' }));
230
+ registry.register(createTestRecipe({
231
+ id: 'fast',
232
+ variant: 'fast',
233
+ conditions,
234
+ priority: 10,
235
+ }));
274
236
 
275
- const result = await planner.selectRecipe("test.workflow", {
276
- priority: "speed",
277
- });
237
+ const result = await planner.selectRecipe('test.workflow', { priority: 'speed' });
278
238
 
279
- expect(result.recipe.id).toBe("fast");
239
+ expect(result.recipe.id).toBe('fast');
280
240
  expect(result.score).toBeGreaterThan(0);
281
241
  });
282
242
 
283
- it("should fall back to default if no conditions match", async () => {
284
- registry.register(
285
- createTestRecipe({ id: "default", variant: "default" }),
286
- );
287
- registry.register(
288
- createTestRecipe({
289
- id: "fast",
290
- variant: "fast",
291
- conditions: [{ field: "priority", operator: "eq", value: "speed" }],
292
- }),
293
- );
243
+ it('should fall back to default if no conditions match', async () => {
244
+ registry.register(createTestRecipe({ id: 'default', variant: 'default' }));
245
+ registry.register(createTestRecipe({
246
+ id: 'fast',
247
+ variant: 'fast',
248
+ conditions: [{ field: 'priority', operator: 'eq', value: 'speed' }],
249
+ }));
294
250
 
295
- const result = await planner.selectRecipe("test.workflow", {
296
- priority: "quality",
297
- });
251
+ const result = await planner.selectRecipe('test.workflow', { priority: 'quality' });
298
252
 
299
- expect(result.recipe.id).toBe("default");
253
+ expect(result.recipe.id).toBe('default');
300
254
  });
301
255
 
302
- it("should use forced recipe from hints", async () => {
303
- registry.register(
304
- createTestRecipe({ id: "default", variant: "default" }),
305
- );
306
- registry.register(
307
- createTestRecipe({ id: "specific", variant: "specific" }),
308
- );
256
+ it('should use forced recipe from hints', async () => {
257
+ registry.register(createTestRecipe({ id: 'default', variant: 'default' }));
258
+ registry.register(createTestRecipe({ id: 'specific', variant: 'specific' }));
309
259
 
310
260
  const context: PlanningContext = {
311
- hints: { forceRecipeId: "specific" },
261
+ hints: { forceRecipeId: 'specific' },
312
262
  };
313
263
 
314
- const result = await planner.selectRecipe("test.workflow", {}, context);
264
+ const result = await planner.selectRecipe('test.workflow', {}, context);
315
265
 
316
- expect(result.recipe.id).toBe("specific");
266
+ expect(result.recipe.id).toBe('specific');
317
267
  expect(result.score).toBe(100);
318
268
  });
319
269
 
320
- it("should use preferred variant from hints", async () => {
321
- registry.register(
322
- createTestRecipe({ id: "default", variant: "default" }),
323
- );
324
- registry.register(createTestRecipe({ id: "fast", variant: "fast" }));
270
+ it('should use preferred variant from hints', async () => {
271
+ registry.register(createTestRecipe({ id: 'default', variant: 'default' }));
272
+ registry.register(createTestRecipe({ id: 'fast', variant: 'fast' }));
325
273
 
326
274
  const context: PlanningContext = {
327
- hints: { preferredVariant: "fast" },
275
+ hints: { preferredVariant: 'fast' },
328
276
  };
329
277
 
330
- const result = await planner.selectRecipe("test.workflow", {}, context);
278
+ const result = await planner.selectRecipe('test.workflow', {}, context);
331
279
 
332
- expect(result.recipe.id).toBe("fast");
280
+ expect(result.recipe.id).toBe('fast');
333
281
  });
334
282
 
335
- it("should throw if no recipes found", async () => {
336
- await expect(planner.selectRecipe("nonexistent", {})).rejects.toThrow(
337
- "No recipes found for workflow kind: nonexistent",
338
- );
283
+ it('should throw if no recipes found', async () => {
284
+ await expect(
285
+ planner.selectRecipe('nonexistent', {})
286
+ ).rejects.toThrow('No recipes found for workflow kind: nonexistent');
339
287
  });
340
288
  });
341
289
 
342
- describe("generatePlan", () => {
343
- it("should generate a plan from a recipe", async () => {
290
+ describe('generatePlan', () => {
291
+ it('should generate a plan from a recipe', async () => {
344
292
  const recipe = createTestRecipe();
345
293
 
346
294
  const plan = await planner.generatePlan(recipe, {});
347
295
 
348
- expect(plan.recipeId).toBe("test.recipe");
349
- expect(plan.variant).toBe("default");
296
+ expect(plan.recipeId).toBe('test.recipe');
297
+ expect(plan.variant).toBe('default');
350
298
  expect(plan.steps).toHaveLength(2);
351
- expect(plan.steps[0].key).toBe("step1");
352
- expect(plan.steps[1].key).toBe("step2");
299
+ expect(plan.steps[0].key).toBe('step1');
300
+ expect(plan.steps[1].key).toBe('step2');
353
301
  });
354
302
 
355
- it("should apply skip hints", async () => {
303
+ it('should apply skip hints', async () => {
356
304
  const recipe = createTestRecipe();
357
305
  const context: PlanningContext = {
358
- hints: { skipSteps: ["step1"] },
306
+ hints: { skipSteps: ['step1'] },
359
307
  };
360
308
 
361
309
  const plan = await planner.generatePlan(recipe, {}, context);
362
310
 
363
311
  expect(plan.steps).toHaveLength(1);
364
- expect(plan.steps[0].key).toBe("step2");
312
+ expect(plan.steps[0].key).toBe('step2');
365
313
  expect(plan.modifications).toContainEqual(
366
- expect.objectContaining({ type: "remove_step", stepKey: "step1" }),
314
+ expect.objectContaining({ type: 'remove_step', stepKey: 'step1' })
367
315
  );
368
316
  });
369
317
 
370
- it("should apply additional config from hints", async () => {
318
+ it('should apply additional config from hints', async () => {
371
319
  const recipe = createTestRecipe();
372
320
  const context: PlanningContext = {
373
- hints: { additionalConfig: { depth: "comprehensive" } },
321
+ hints: { additionalConfig: { depth: 'comprehensive' } },
374
322
  };
375
323
 
376
324
  const plan = await planner.generatePlan(recipe, {}, context);
377
325
 
378
- expect(plan.steps[0].config).toEqual({ depth: "comprehensive" });
379
- expect(plan.steps[1].config).toEqual({ depth: "comprehensive" });
326
+ expect(plan.steps[0].config).toEqual({ depth: 'comprehensive' });
327
+ expect(plan.steps[1].config).toEqual({ depth: 'comprehensive' });
380
328
  });
381
329
 
382
- it("should apply speed constraints", async () => {
330
+ it('should apply speed constraints', async () => {
383
331
  const recipe = createTestRecipe({
384
332
  steps: [
385
- {
386
- key: "step1",
387
- name: "Step 1",
388
- handlerRef: "h1",
389
- timeout: 60000,
390
- maxRetries: 5,
391
- },
333
+ { key: 'step1', name: 'Step 1', handlerRef: 'h1', timeout: 60000, maxRetries: 5 },
392
334
  ],
393
335
  });
394
336
  const context: PlanningContext = {
395
- constraints: { priority: "speed" },
337
+ constraints: { priority: 'speed' },
396
338
  };
397
339
 
398
340
  const plan = await planner.generatePlan(recipe, {}, context);
@@ -401,7 +343,7 @@ describe("RuleBasedPlanner", () => {
401
343
  expect(plan.steps[0].maxRetries).toBeLessThanOrEqual(1);
402
344
  });
403
345
 
404
- it("should apply duration constraints", async () => {
346
+ it('should apply duration constraints', async () => {
405
347
  const recipe = createTestRecipe();
406
348
  const context: PlanningContext = {
407
349
  constraints: { maxDuration: 10000 },
@@ -414,15 +356,15 @@ describe("RuleBasedPlanner", () => {
414
356
  expect(plan.steps[1].timeout).toBeLessThanOrEqual(5000);
415
357
  });
416
358
 
417
- it("should apply cost constraints (maxRetries=0)", async () => {
359
+ it('should apply cost constraints (maxRetries=0)', async () => {
418
360
  const recipe = createTestRecipe({
419
361
  steps: [
420
- { key: "step1", name: "Step 1", handlerRef: "h1", maxRetries: 5 },
421
- { key: "step2", name: "Step 2", handlerRef: "h2", maxRetries: 3 },
362
+ { key: 'step1', name: 'Step 1', handlerRef: 'h1', maxRetries: 5 },
363
+ { key: 'step2', name: 'Step 2', handlerRef: 'h2', maxRetries: 3 },
422
364
  ],
423
365
  });
424
366
  const context: PlanningContext = {
425
- constraints: { priority: "cost" },
367
+ constraints: { priority: 'cost' },
426
368
  };
427
369
 
428
370
  const plan = await planner.generatePlan(recipe, {}, context);
@@ -431,19 +373,19 @@ describe("RuleBasedPlanner", () => {
431
373
  expect(plan.steps[1].maxRetries).toBe(0);
432
374
  expect(plan.modifications).toContainEqual(
433
375
  expect.objectContaining({
434
- type: "set_default",
435
- value: { priority: "cost" },
436
- }),
376
+ type: 'set_default',
377
+ value: { priority: 'cost' },
378
+ })
437
379
  );
438
380
  });
439
381
 
440
- it("should apply maxDuration to per-step timeouts evenly", async () => {
382
+ it('should apply maxDuration to per-step timeouts evenly', async () => {
441
383
  const recipe = createTestRecipe({
442
384
  steps: [
443
- { key: "step1", name: "Step 1", handlerRef: "h1" },
444
- { key: "step2", name: "Step 2", handlerRef: "h2" },
445
- { key: "step3", name: "Step 3", handlerRef: "h3" },
446
- { key: "step4", name: "Step 4", handlerRef: "h4" },
385
+ { key: 'step1', name: 'Step 1', handlerRef: 'h1' },
386
+ { key: 'step2', name: 'Step 2', handlerRef: 'h2' },
387
+ { key: 'step3', name: 'Step 3', handlerRef: 'h3' },
388
+ { key: 'step4', name: 'Step 4', handlerRef: 'h4' },
447
389
  ],
448
390
  });
449
391
  const context: PlanningContext = {
@@ -458,17 +400,17 @@ describe("RuleBasedPlanner", () => {
458
400
  }
459
401
  expect(plan.modifications).toContainEqual(
460
402
  expect.objectContaining({
461
- type: "set_default",
403
+ type: 'set_default',
462
404
  value: { maxDuration: 20000 },
463
- }),
405
+ })
464
406
  );
465
407
  });
466
408
 
467
- it("should cap existing step timeout to per-step budget when maxDuration is set", async () => {
409
+ it('should cap existing step timeout to per-step budget when maxDuration is set', async () => {
468
410
  const recipe = createTestRecipe({
469
411
  steps: [
470
- { key: "step1", name: "Step 1", handlerRef: "h1", timeout: 30000 },
471
- { key: "step2", name: "Step 2", handlerRef: "h2", timeout: 2000 },
412
+ { key: 'step1', name: 'Step 1', handlerRef: 'h1', timeout: 30000 },
413
+ { key: 'step2', name: 'Step 2', handlerRef: 'h2', timeout: 2000 },
472
414
  ],
473
415
  });
474
416
  const context: PlanningContext = {
@@ -483,22 +425,22 @@ describe("RuleBasedPlanner", () => {
483
425
  });
484
426
  });
485
427
 
486
- describe("plan", () => {
487
- it("should select recipe and generate plan in one call", async () => {
428
+ describe('plan', () => {
429
+ it('should select recipe and generate plan in one call', async () => {
488
430
  registry.register(createTestRecipe());
489
431
 
490
- const plan = await planner.plan("test.workflow", {});
432
+ const plan = await planner.plan('test.workflow', {});
491
433
 
492
- expect(plan.recipeId).toBe("test.recipe");
434
+ expect(plan.recipeId).toBe('test.recipe');
493
435
  expect(plan.steps).toHaveLength(2);
494
- expect(plan.reasoning).toContain("test.recipe");
436
+ expect(plan.reasoning).toContain('test.recipe');
495
437
  });
496
438
  });
497
439
 
498
- describe("validatePlan", () => {
499
- it("should validate a correct plan", async () => {
440
+ describe('validatePlan', () => {
441
+ it('should validate a correct plan', async () => {
500
442
  registry.register(createTestRecipe());
501
- const plan = await planner.plan("test.workflow", {});
443
+ const plan = await planner.plan('test.workflow', {});
502
444
 
503
445
  const result = planner.validatePlan(plan);
504
446
 
@@ -506,11 +448,11 @@ describe("RuleBasedPlanner", () => {
506
448
  expect(result.errors).toHaveLength(0);
507
449
  });
508
450
 
509
- it("should detect missing steps", () => {
451
+ it('should detect missing steps', () => {
510
452
  const result = planner.validatePlan({
511
- id: "plan-1",
512
- recipeId: "test",
513
- variant: "default",
453
+ id: 'plan-1',
454
+ recipeId: 'test',
455
+ variant: 'default',
514
456
  modifications: [],
515
457
  steps: [],
516
458
  defaults: {},
@@ -518,37 +460,32 @@ describe("RuleBasedPlanner", () => {
518
460
  });
519
461
 
520
462
  expect(result.valid).toBe(false);
521
- expect(result.errors).toContain("Plan has no steps");
463
+ expect(result.errors).toContain('Plan has no steps');
522
464
  });
523
465
 
524
- it("should detect duplicate step keys", () => {
466
+ it('should detect duplicate step keys', () => {
525
467
  const result = planner.validatePlan({
526
- id: "plan-1",
527
- recipeId: "test",
528
- variant: "default",
468
+ id: 'plan-1',
469
+ recipeId: 'test',
470
+ variant: 'default',
529
471
  modifications: [],
530
472
  steps: [
531
- { key: "step1", name: "Step 1", handlerRef: "h1", config: {} },
532
- {
533
- key: "step1",
534
- name: "Step 1 Duplicate",
535
- handlerRef: "h2",
536
- config: {},
537
- },
473
+ { key: 'step1', name: 'Step 1', handlerRef: 'h1', config: {} },
474
+ { key: 'step1', name: 'Step 1 Duplicate', handlerRef: 'h2', config: {} },
538
475
  ],
539
476
  defaults: {},
540
477
  createdAt: new Date(),
541
478
  });
542
479
 
543
480
  expect(result.valid).toBe(false);
544
- expect(result.errors).toContain("Duplicate step key: step1");
481
+ expect(result.errors).toContain('Duplicate step key: step1');
545
482
  });
546
483
  });
547
484
 
548
- describe("estimateResources", () => {
549
- it("should estimate resources for a plan", async () => {
485
+ describe('estimateResources', () => {
486
+ it('should estimate resources for a plan', async () => {
550
487
  registry.register(createTestRecipe());
551
- const plan = await planner.plan("test.workflow", {});
488
+ const plan = await planner.plan('test.workflow', {});
552
489
 
553
490
  const estimate = planner.estimateResources(plan);
554
491
 
@@ -559,8 +496,8 @@ describe("RuleBasedPlanner", () => {
559
496
  });
560
497
  });
561
498
 
562
- describe("createRegistry", () => {
563
- it("should create a combined registry", () => {
499
+ describe('createRegistry', () => {
500
+ it('should create a combined registry', () => {
564
501
  const { recipes, handlers } = createRegistry();
565
502
 
566
503
  expect(recipes).toBeInstanceOf(MemoryRecipeRegistry);
@@ -568,7 +505,7 @@ describe("createRegistry", () => {
568
505
  });
569
506
  });
570
507
 
571
- describe("Condition Evaluation", () => {
508
+ describe('Condition Evaluation', () => {
572
509
  let registry: MemoryRecipeRegistry;
573
510
  let planner: RuleBasedPlanner;
574
511
 
@@ -580,211 +517,211 @@ describe("Condition Evaluation", () => {
580
517
  const testConditions = async (
581
518
  conditions: RecipeCondition[],
582
519
  input: Record<string, unknown>,
583
- shouldMatch: boolean,
520
+ shouldMatch: boolean
584
521
  ) => {
585
522
  registry.clear();
586
523
  registry.register({
587
- id: "conditional",
588
- name: "Conditional Recipe",
589
- workflowKind: "test.workflow",
590
- variant: "conditional",
591
- steps: [{ key: "s1", name: "S1", handlerRef: "h1" }],
524
+ id: 'conditional',
525
+ name: 'Conditional Recipe',
526
+ workflowKind: 'test.workflow',
527
+ variant: 'conditional',
528
+ steps: [{ key: 's1', name: 'S1', handlerRef: 'h1' }],
592
529
  conditions,
593
530
  priority: 10,
594
531
  });
595
532
  registry.register({
596
- id: "default",
597
- name: "Default Recipe",
598
- workflowKind: "test.workflow",
599
- variant: "default",
600
- steps: [{ key: "s1", name: "S1", handlerRef: "h1" }],
533
+ id: 'default',
534
+ name: 'Default Recipe',
535
+ workflowKind: 'test.workflow',
536
+ variant: 'default',
537
+ steps: [{ key: 's1', name: 'S1', handlerRef: 'h1' }],
601
538
  });
602
539
 
603
- const result = await planner.selectRecipe("test.workflow", input);
540
+ const result = await planner.selectRecipe('test.workflow', input);
604
541
  if (shouldMatch) {
605
- expect(result.recipe.id).toBe("conditional");
542
+ expect(result.recipe.id).toBe('conditional');
606
543
  } else {
607
- expect(result.recipe.id).toBe("default");
544
+ expect(result.recipe.id).toBe('default');
608
545
  }
609
546
  };
610
547
 
611
- it("should evaluate eq operator", async () => {
548
+ it('should evaluate eq operator', async () => {
612
549
  await testConditions(
613
- [{ field: "type", operator: "eq", value: "fast" }],
614
- { type: "fast" },
615
- true,
550
+ [{ field: 'type', operator: 'eq', value: 'fast' }],
551
+ { type: 'fast' },
552
+ true
616
553
  );
617
554
  await testConditions(
618
- [{ field: "type", operator: "eq", value: "fast" }],
619
- { type: "slow" },
620
- false,
555
+ [{ field: 'type', operator: 'eq', value: 'fast' }],
556
+ { type: 'slow' },
557
+ false
621
558
  );
622
559
  });
623
560
 
624
- it("should evaluate neq operator", async () => {
561
+ it('should evaluate neq operator', async () => {
625
562
  await testConditions(
626
- [{ field: "type", operator: "neq", value: "slow" }],
627
- { type: "fast" },
628
- true,
563
+ [{ field: 'type', operator: 'neq', value: 'slow' }],
564
+ { type: 'fast' },
565
+ true
629
566
  );
630
567
  await testConditions(
631
- [{ field: "type", operator: "neq", value: "slow" }],
632
- { type: "slow" },
633
- false,
568
+ [{ field: 'type', operator: 'neq', value: 'slow' }],
569
+ { type: 'slow' },
570
+ false
634
571
  );
635
572
  });
636
573
 
637
- it("should evaluate gt operator", async () => {
574
+ it('should evaluate gt operator', async () => {
638
575
  await testConditions(
639
- [{ field: "count", operator: "gt", value: 10 }],
576
+ [{ field: 'count', operator: 'gt', value: 10 }],
640
577
  { count: 15 },
641
- true,
578
+ true
642
579
  );
643
580
  await testConditions(
644
- [{ field: "count", operator: "gt", value: 10 }],
581
+ [{ field: 'count', operator: 'gt', value: 10 }],
645
582
  { count: 5 },
646
- false,
583
+ false
647
584
  );
648
585
  });
649
586
 
650
- it("should evaluate gte operator", async () => {
587
+ it('should evaluate gte operator', async () => {
651
588
  await testConditions(
652
- [{ field: "count", operator: "gte", value: 10 }],
589
+ [{ field: 'count', operator: 'gte', value: 10 }],
653
590
  { count: 10 },
654
- true,
591
+ true
655
592
  );
656
593
  await testConditions(
657
- [{ field: "count", operator: "gte", value: 10 }],
594
+ [{ field: 'count', operator: 'gte', value: 10 }],
658
595
  { count: 9 },
659
- false,
596
+ false
660
597
  );
661
598
  });
662
599
 
663
- it("should evaluate lt operator", async () => {
600
+ it('should evaluate lt operator', async () => {
664
601
  await testConditions(
665
- [{ field: "count", operator: "lt", value: 10 }],
602
+ [{ field: 'count', operator: 'lt', value: 10 }],
666
603
  { count: 5 },
667
- true,
604
+ true
668
605
  );
669
606
  await testConditions(
670
- [{ field: "count", operator: "lt", value: 10 }],
607
+ [{ field: 'count', operator: 'lt', value: 10 }],
671
608
  { count: 15 },
672
- false,
609
+ false
673
610
  );
674
611
  });
675
612
 
676
- it("should evaluate lte operator", async () => {
613
+ it('should evaluate lte operator', async () => {
677
614
  await testConditions(
678
- [{ field: "count", operator: "lte", value: 10 }],
615
+ [{ field: 'count', operator: 'lte', value: 10 }],
679
616
  { count: 10 },
680
- true,
617
+ true
681
618
  );
682
619
  await testConditions(
683
- [{ field: "count", operator: "lte", value: 10 }],
620
+ [{ field: 'count', operator: 'lte', value: 10 }],
684
621
  { count: 11 },
685
- false,
622
+ false
686
623
  );
687
624
  });
688
625
 
689
- it("should evaluate contains operator for strings", async () => {
626
+ it('should evaluate contains operator for strings', async () => {
690
627
  await testConditions(
691
- [{ field: "name", operator: "contains", value: "test" }],
692
- { name: "my-test-file" },
693
- true,
628
+ [{ field: 'name', operator: 'contains', value: 'test' }],
629
+ { name: 'my-test-file' },
630
+ true
694
631
  );
695
632
  await testConditions(
696
- [{ field: "name", operator: "contains", value: "test" }],
697
- { name: "my-file" },
698
- false,
633
+ [{ field: 'name', operator: 'contains', value: 'test' }],
634
+ { name: 'my-file' },
635
+ false
699
636
  );
700
637
  });
701
638
 
702
- it("should evaluate contains operator for arrays", async () => {
639
+ it('should evaluate contains operator for arrays', async () => {
703
640
  await testConditions(
704
- [{ field: "tags", operator: "contains", value: "urgent" }],
705
- { tags: ["urgent", "review"] },
706
- true,
641
+ [{ field: 'tags', operator: 'contains', value: 'urgent' }],
642
+ { tags: ['urgent', 'review'] },
643
+ true
707
644
  );
708
645
  await testConditions(
709
- [{ field: "tags", operator: "contains", value: "urgent" }],
710
- { tags: ["normal"] },
711
- false,
646
+ [{ field: 'tags', operator: 'contains', value: 'urgent' }],
647
+ { tags: ['normal'] },
648
+ false
712
649
  );
713
650
  });
714
651
 
715
- it("should evaluate matches operator", async () => {
652
+ it('should evaluate matches operator', async () => {
716
653
  await testConditions(
717
- [{ field: "email", operator: "matches", value: "^test@" }],
718
- { email: "test@example.com" },
719
- true,
654
+ [{ field: 'email', operator: 'matches', value: '^test@' }],
655
+ { email: 'test@example.com' },
656
+ true
720
657
  );
721
658
  await testConditions(
722
- [{ field: "email", operator: "matches", value: "^test@" }],
723
- { email: "user@example.com" },
724
- false,
659
+ [{ field: 'email', operator: 'matches', value: '^test@' }],
660
+ { email: 'user@example.com' },
661
+ false
725
662
  );
726
663
  });
727
664
 
728
- it("should evaluate exists operator", async () => {
665
+ it('should evaluate exists operator', async () => {
729
666
  await testConditions(
730
- [{ field: "optional", operator: "exists" }],
731
- { optional: "value" },
732
- true,
667
+ [{ field: 'optional', operator: 'exists' }],
668
+ { optional: 'value' },
669
+ true
733
670
  );
734
671
  await testConditions(
735
- [{ field: "optional", operator: "exists" }],
736
- { other: "value" },
737
- false,
672
+ [{ field: 'optional', operator: 'exists' }],
673
+ { other: 'value' },
674
+ false
738
675
  );
739
676
  await testConditions(
740
- [{ field: "optional", operator: "exists" }],
741
- { optional: "" },
742
- false,
677
+ [{ field: 'optional', operator: 'exists' }],
678
+ { optional: '' },
679
+ false
743
680
  );
744
681
  });
745
682
 
746
- it("should evaluate notExists operator", async () => {
683
+ it('should evaluate notExists operator', async () => {
747
684
  await testConditions(
748
- [{ field: "optional", operator: "notExists" }],
749
- { other: "value" },
750
- true,
685
+ [{ field: 'optional', operator: 'notExists' }],
686
+ { other: 'value' },
687
+ true
751
688
  );
752
689
  await testConditions(
753
- [{ field: "optional", operator: "notExists" }],
754
- { optional: "value" },
755
- false,
690
+ [{ field: 'optional', operator: 'notExists' }],
691
+ { optional: 'value' },
692
+ false
756
693
  );
757
694
  });
758
695
 
759
- it("should evaluate nested fields", async () => {
696
+ it('should evaluate nested fields', async () => {
760
697
  await testConditions(
761
- [{ field: "config.settings.enabled", operator: "eq", value: true }],
698
+ [{ field: 'config.settings.enabled', operator: 'eq', value: true }],
762
699
  { config: { settings: { enabled: true } } },
763
- true,
700
+ true
764
701
  );
765
702
  await testConditions(
766
- [{ field: "config.settings.enabled", operator: "eq", value: true }],
703
+ [{ field: 'config.settings.enabled', operator: 'eq', value: true }],
767
704
  { config: { settings: { enabled: false } } },
768
- false,
705
+ false
769
706
  );
770
707
  });
771
708
 
772
- it("should require all conditions to match", async () => {
709
+ it('should require all conditions to match', async () => {
773
710
  await testConditions(
774
711
  [
775
- { field: "type", operator: "eq", value: "fast" },
776
- { field: "count", operator: "gt", value: 5 },
712
+ { field: 'type', operator: 'eq', value: 'fast' },
713
+ { field: 'count', operator: 'gt', value: 5 },
777
714
  ],
778
- { type: "fast", count: 10 },
779
- true,
715
+ { type: 'fast', count: 10 },
716
+ true
780
717
  );
781
718
  await testConditions(
782
719
  [
783
- { field: "type", operator: "eq", value: "fast" },
784
- { field: "count", operator: "gt", value: 5 },
720
+ { field: 'type', operator: 'eq', value: 'fast' },
721
+ { field: 'count', operator: 'gt', value: 5 },
785
722
  ],
786
- { type: "fast", count: 3 },
787
- false,
723
+ { type: 'fast', count: 3 },
724
+ false
788
725
  );
789
726
  });
790
727
  });