@auto-engineer/pipeline 1.65.0 → 1.67.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-test.log +6 -6
  3. package/.turbo/turbo-type-check.log +1 -1
  4. package/CHANGELOG.md +135 -0
  5. package/dist/src/builder/define-v2.d.ts +101 -0
  6. package/dist/src/builder/define-v2.d.ts.map +1 -0
  7. package/dist/src/builder/define-v2.js +209 -0
  8. package/dist/src/builder/define-v2.js.map +1 -0
  9. package/dist/src/engine/command-dispatcher.d.ts +31 -0
  10. package/dist/src/engine/command-dispatcher.d.ts.map +1 -0
  11. package/dist/src/engine/command-dispatcher.js +26 -0
  12. package/dist/src/engine/command-dispatcher.js.map +1 -0
  13. package/dist/src/engine/event-router.d.ts +21 -0
  14. package/dist/src/engine/event-router.d.ts.map +1 -0
  15. package/dist/src/engine/event-router.js +22 -0
  16. package/dist/src/engine/event-router.js.map +1 -0
  17. package/dist/src/engine/index.d.ts +15 -0
  18. package/dist/src/engine/index.d.ts.map +1 -0
  19. package/dist/src/engine/index.js +15 -0
  20. package/dist/src/engine/index.js.map +1 -0
  21. package/dist/src/engine/pipeline-engine.d.ts +37 -0
  22. package/dist/src/engine/pipeline-engine.d.ts.map +1 -0
  23. package/dist/src/engine/pipeline-engine.js +53 -0
  24. package/dist/src/engine/pipeline-engine.js.map +1 -0
  25. package/dist/src/engine/projections/item-status.d.ts +9 -0
  26. package/dist/src/engine/projections/item-status.d.ts.map +1 -0
  27. package/dist/src/engine/projections/item-status.js +9 -0
  28. package/dist/src/engine/projections/item-status.js.map +1 -0
  29. package/dist/src/engine/projections/latest-run.d.ts +9 -0
  30. package/dist/src/engine/projections/latest-run.d.ts.map +1 -0
  31. package/dist/src/engine/projections/latest-run.js +9 -0
  32. package/dist/src/engine/projections/latest-run.js.map +1 -0
  33. package/dist/src/engine/projections/message-log.d.ts +9 -0
  34. package/dist/src/engine/projections/message-log.d.ts.map +1 -0
  35. package/dist/src/engine/projections/message-log.js +10 -0
  36. package/dist/src/engine/projections/message-log.js.map +1 -0
  37. package/dist/src/engine/projections/node-status.d.ts +9 -0
  38. package/dist/src/engine/projections/node-status.d.ts.map +1 -0
  39. package/dist/src/engine/projections/node-status.js +9 -0
  40. package/dist/src/engine/projections/node-status.js.map +1 -0
  41. package/dist/src/engine/projections/stats.d.ts +9 -0
  42. package/dist/src/engine/projections/stats.d.ts.map +1 -0
  43. package/dist/src/engine/projections/stats.js +9 -0
  44. package/dist/src/engine/projections/stats.js.map +1 -0
  45. package/dist/src/engine/sqlite-consumer.d.ts +11 -0
  46. package/dist/src/engine/sqlite-consumer.d.ts.map +1 -0
  47. package/dist/src/engine/sqlite-consumer.js +27 -0
  48. package/dist/src/engine/sqlite-consumer.js.map +1 -0
  49. package/dist/src/engine/sqlite-store.d.ts +10 -0
  50. package/dist/src/engine/sqlite-store.d.ts.map +1 -0
  51. package/dist/src/engine/sqlite-store.js +14 -0
  52. package/dist/src/engine/sqlite-store.js.map +1 -0
  53. package/dist/src/engine/workflow-processor.d.ts +20 -0
  54. package/dist/src/engine/workflow-processor.d.ts.map +1 -0
  55. package/dist/src/engine/workflow-processor.js +36 -0
  56. package/dist/src/engine/workflow-processor.js.map +1 -0
  57. package/dist/src/engine/workflows/await-workflow.d.ts +33 -0
  58. package/dist/src/engine/workflows/await-workflow.d.ts.map +1 -0
  59. package/dist/src/engine/workflows/await-workflow.js +45 -0
  60. package/dist/src/engine/workflows/await-workflow.js.map +1 -0
  61. package/dist/src/engine/workflows/phased-workflow.d.ts +64 -0
  62. package/dist/src/engine/workflows/phased-workflow.d.ts.map +1 -0
  63. package/dist/src/engine/workflows/phased-workflow.js +103 -0
  64. package/dist/src/engine/workflows/phased-workflow.js.map +1 -0
  65. package/dist/src/engine/workflows/settled-workflow.d.ts +62 -0
  66. package/dist/src/engine/workflows/settled-workflow.d.ts.map +1 -0
  67. package/dist/src/engine/workflows/settled-workflow.js +92 -0
  68. package/dist/src/engine/workflows/settled-workflow.js.map +1 -0
  69. package/dist/src/graph/types.d.ts +1 -1
  70. package/dist/src/graph/types.d.ts.map +1 -1
  71. package/dist/src/index.d.ts +2 -0
  72. package/dist/src/index.d.ts.map +1 -1
  73. package/dist/src/index.js +2 -0
  74. package/dist/src/index.js.map +1 -1
  75. package/dist/src/server/pipeline-server-v2.d.ts +48 -0
  76. package/dist/src/server/pipeline-server-v2.d.ts.map +1 -0
  77. package/dist/src/server/pipeline-server-v2.js +61 -0
  78. package/dist/src/server/pipeline-server-v2.js.map +1 -0
  79. package/dist/src/server/pipeline-server.d.ts +5 -1
  80. package/dist/src/server/pipeline-server.d.ts.map +1 -1
  81. package/dist/src/server/pipeline-server.js +71 -10
  82. package/dist/src/server/pipeline-server.js.map +1 -1
  83. package/dist/tsconfig.tsbuildinfo +1 -1
  84. package/ketchup-plan.md +13 -0
  85. package/package.json +3 -3
  86. package/src/builder/define-v2.specs.ts +236 -0
  87. package/src/builder/define-v2.ts +351 -0
  88. package/src/engine/command-dispatcher.specs.ts +62 -0
  89. package/src/engine/command-dispatcher.ts +46 -0
  90. package/src/engine/event-router.specs.ts +75 -0
  91. package/src/engine/event-router.ts +36 -0
  92. package/src/engine/index.ts +39 -0
  93. package/src/engine/pipeline-engine-e2e.specs.ts +776 -0
  94. package/src/engine/pipeline-engine.integration.specs.ts +126 -0
  95. package/src/engine/pipeline-engine.specs.ts +70 -0
  96. package/src/engine/pipeline-engine.ts +82 -0
  97. package/src/engine/projections/item-status.ts +11 -0
  98. package/src/engine/projections/latest-run.ts +10 -0
  99. package/src/engine/projections/message-log.ts +11 -0
  100. package/src/engine/projections/node-status.ts +10 -0
  101. package/src/engine/projections/projections.specs.ts +176 -0
  102. package/src/engine/projections/stats.ts +10 -0
  103. package/src/engine/sqlite-consumer.specs.ts +42 -0
  104. package/src/engine/sqlite-consumer.ts +34 -0
  105. package/src/engine/sqlite-store.specs.ts +46 -0
  106. package/src/engine/sqlite-store.ts +21 -0
  107. package/src/engine/workflow-processor.specs.ts +37 -0
  108. package/src/engine/workflow-processor.ts +57 -0
  109. package/src/engine/workflows/await-workflow.specs.ts +104 -0
  110. package/src/engine/workflows/await-workflow.ts +66 -0
  111. package/src/engine/workflows/phased-workflow.specs.ts +383 -0
  112. package/src/engine/workflows/phased-workflow.ts +153 -0
  113. package/src/engine/workflows/settled-workflow.specs.ts +364 -0
  114. package/src/engine/workflows/settled-workflow.ts +139 -0
  115. package/src/graph/types.ts +1 -1
  116. package/src/index.ts +2 -0
  117. package/src/server/pipeline-server-v2.specs.ts +91 -0
  118. package/src/server/pipeline-server-v2.ts +70 -0
  119. package/src/server/pipeline-server.specs.ts +327 -134
  120. package/src/server/pipeline-server.ts +77 -11
@@ -0,0 +1,153 @@
1
+ export type PhasedItem = {
2
+ key: string;
3
+ phase: string;
4
+ status: 'pending' | 'dispatched' | 'completed' | 'failed';
5
+ };
6
+
7
+ export type PhasedInput =
8
+ | {
9
+ type: 'StartPhased';
10
+ data: {
11
+ correlationId: string;
12
+ items: Array<{ key: string; phase: string }>;
13
+ phases: string[];
14
+ stopOnFailure: boolean;
15
+ };
16
+ }
17
+ | { type: 'ItemCompleted'; data: { itemKey: string; result: Record<string, unknown> } }
18
+ | { type: 'ItemFailed'; data: { itemKey: string; error: Record<string, unknown> } };
19
+
20
+ export type PhasedOutput =
21
+ | { type: 'DispatchItem'; kind: 'Command'; data: { itemKey: string; phase: string } }
22
+ | { type: 'PhasedCompleted'; data: { completedItems: string[] } }
23
+ | { type: 'PhasedFailed'; data: { failedItems: string[]; completedItems: string[] } };
24
+
25
+ export type PhasedState = {
26
+ status: 'idle' | 'running' | 'completed' | 'failed';
27
+ items: PhasedItem[];
28
+ phases: string[];
29
+ currentPhaseIndex: number;
30
+ stopOnFailure: boolean;
31
+ };
32
+
33
+ export function initialState(): PhasedState {
34
+ return {
35
+ status: 'idle',
36
+ items: [],
37
+ phases: [],
38
+ currentPhaseIndex: 0,
39
+ stopOnFailure: false,
40
+ };
41
+ }
42
+
43
+ export function decide(input: PhasedInput, state: PhasedState): PhasedOutput | PhasedOutput[] {
44
+ switch (input.type) {
45
+ case 'StartPhased': {
46
+ const currentPhase = state.phases[state.currentPhaseIndex];
47
+ return state.items
48
+ .filter((item) => item.phase === currentPhase)
49
+ .map((item) => ({
50
+ type: 'DispatchItem' as const,
51
+ kind: 'Command' as const,
52
+ data: { itemKey: item.key, phase: item.phase },
53
+ }));
54
+ }
55
+ case 'ItemCompleted': {
56
+ const currentPhase = state.phases[state.currentPhaseIndex];
57
+ const currentPhaseItems = state.items.filter((item) => item.phase === currentPhase);
58
+ const allCompleted = currentPhaseItems.every((item) => item.status === 'completed');
59
+ if (allCompleted) {
60
+ const allItems = state.items;
61
+ const allDone = allItems.every((item) => item.status === 'completed');
62
+ if (allDone) {
63
+ return {
64
+ type: 'PhasedCompleted',
65
+ data: { completedItems: allItems.map((item) => item.key) },
66
+ };
67
+ }
68
+ const nextPhaseIndex = state.currentPhaseIndex + 1;
69
+ if (nextPhaseIndex < state.phases.length) {
70
+ const nextPhase = state.phases[nextPhaseIndex];
71
+ return state.items
72
+ .filter((item) => item.phase === nextPhase)
73
+ .map((item) => ({
74
+ type: 'DispatchItem' as const,
75
+ kind: 'Command' as const,
76
+ data: { itemKey: item.key, phase: item.phase },
77
+ }));
78
+ }
79
+ }
80
+ return [];
81
+ }
82
+ case 'ItemFailed': {
83
+ if (state.stopOnFailure) {
84
+ const failedItems = state.items.filter((item) => item.status === 'failed').map((item) => item.key);
85
+ const completedItems = state.items.filter((item) => item.status === 'completed').map((item) => item.key);
86
+ return {
87
+ type: 'PhasedFailed',
88
+ data: { failedItems, completedItems },
89
+ };
90
+ }
91
+ return [];
92
+ }
93
+ default:
94
+ return [];
95
+ }
96
+ }
97
+
98
+ export function evolve(state: PhasedState, event: PhasedInput | PhasedOutput): PhasedState {
99
+ switch (event.type) {
100
+ case 'StartPhased':
101
+ return {
102
+ status: 'running',
103
+ items: event.data.items.map((item) => ({ ...item, status: 'pending' as const })),
104
+ phases: event.data.phases,
105
+ currentPhaseIndex: 0,
106
+ stopOnFailure: event.data.stopOnFailure,
107
+ };
108
+
109
+ case 'DispatchItem': {
110
+ const phaseIndex = state.phases.indexOf(event.data.phase);
111
+ return {
112
+ ...state,
113
+ items: state.items.map((item) =>
114
+ item.key === event.data.itemKey ? { ...item, status: 'dispatched' as const } : item,
115
+ ),
116
+ currentPhaseIndex: phaseIndex > state.currentPhaseIndex ? phaseIndex : state.currentPhaseIndex,
117
+ };
118
+ }
119
+
120
+ case 'ItemCompleted':
121
+ return {
122
+ ...state,
123
+ items: state.items.map((item) =>
124
+ item.key === event.data.itemKey ? { ...item, status: 'completed' as const } : item,
125
+ ),
126
+ };
127
+
128
+ case 'ItemFailed':
129
+ return {
130
+ ...state,
131
+ items: state.items.map((item) =>
132
+ item.key === event.data.itemKey ? { ...item, status: 'failed' as const } : item,
133
+ ),
134
+ };
135
+
136
+ case 'PhasedCompleted':
137
+ return { ...state, status: 'completed' };
138
+
139
+ case 'PhasedFailed':
140
+ return { ...state, status: 'failed' };
141
+
142
+ default:
143
+ return state;
144
+ }
145
+ }
146
+
147
+ export function createPhasedWorkflow(): {
148
+ decide: typeof decide;
149
+ evolve: typeof evolve;
150
+ initialState: typeof initialState;
151
+ } {
152
+ return { decide, evolve, initialState };
153
+ }
@@ -0,0 +1,364 @@
1
+ import type { SettledInput, SettledState } from './settled-workflow';
2
+ import { createSettledWorkflow, decide, evolve, initialState } from './settled-workflow';
3
+
4
+ describe('settled workflow', () => {
5
+ describe('initialState', () => {
6
+ it('returns idle state with empty tracking', () => {
7
+ expect(initialState()).toEqual({
8
+ status: 'idle',
9
+ commandTypes: [],
10
+ completions: {},
11
+ retryCount: 0,
12
+ maxRetries: 3,
13
+ });
14
+ });
15
+ });
16
+
17
+ describe('evolve', () => {
18
+ it('transitions from idle to waiting on StartSettled', () => {
19
+ const state = initialState();
20
+ const event = {
21
+ type: 'StartSettled' as const,
22
+ data: { correlationId: 'corr-1', commandTypes: ['CheckTests', 'CheckTypes'] },
23
+ };
24
+
25
+ const result = evolve(state, event);
26
+
27
+ expect(result).toEqual({
28
+ status: 'waiting',
29
+ commandTypes: ['CheckTests', 'CheckTypes'],
30
+ completions: {},
31
+ retryCount: 0,
32
+ maxRetries: 3,
33
+ });
34
+ });
35
+
36
+ it('records command completion', () => {
37
+ const state = {
38
+ status: 'waiting' as const,
39
+ commandTypes: ['CheckTests', 'CheckTypes'],
40
+ completions: {},
41
+ retryCount: 0,
42
+ maxRetries: 3,
43
+ };
44
+ const event = {
45
+ type: 'CommandCompleted' as const,
46
+ data: {
47
+ commandType: 'CheckTests',
48
+ result: 'success' as const,
49
+ event: { passed: true },
50
+ },
51
+ };
52
+
53
+ const result = evolve(state, event);
54
+
55
+ expect(result).toEqual({
56
+ status: 'waiting',
57
+ commandTypes: ['CheckTests', 'CheckTypes'],
58
+ completions: {
59
+ CheckTests: { result: 'success', event: { passed: true } },
60
+ },
61
+ retryCount: 0,
62
+ maxRetries: 3,
63
+ });
64
+ });
65
+
66
+ it('preserves existing completions when recording another', () => {
67
+ const state = {
68
+ status: 'waiting' as const,
69
+ commandTypes: ['CheckTests', 'CheckTypes', 'CheckLint'],
70
+ completions: {
71
+ CheckTests: { result: 'success' as const, event: { passed: true } },
72
+ },
73
+ retryCount: 0,
74
+ maxRetries: 3,
75
+ };
76
+ const event = {
77
+ type: 'CommandCompleted' as const,
78
+ data: {
79
+ commandType: 'CheckTypes',
80
+ result: 'failure' as const,
81
+ event: { errors: 3 },
82
+ },
83
+ };
84
+
85
+ const result = evolve(state, event);
86
+
87
+ expect(result).toEqual({
88
+ status: 'waiting',
89
+ commandTypes: ['CheckTests', 'CheckTypes', 'CheckLint'],
90
+ completions: {
91
+ CheckTests: { result: 'success', event: { passed: true } },
92
+ CheckTypes: { result: 'failure', event: { errors: 3 } },
93
+ },
94
+ retryCount: 0,
95
+ maxRetries: 3,
96
+ });
97
+ });
98
+
99
+ it('does not mutate the original state', () => {
100
+ const state = {
101
+ status: 'waiting' as const,
102
+ commandTypes: ['CheckTests', 'CheckTypes'],
103
+ completions: {},
104
+ retryCount: 0,
105
+ maxRetries: 3,
106
+ };
107
+ const event = {
108
+ type: 'CommandCompleted' as const,
109
+ data: {
110
+ commandType: 'CheckTests',
111
+ result: 'success' as const,
112
+ event: { passed: true },
113
+ },
114
+ };
115
+
116
+ evolve(state, event);
117
+
118
+ expect(state.completions).toEqual({});
119
+ });
120
+
121
+ it('returns state unchanged for unknown event types', () => {
122
+ const state = initialState();
123
+ const event = { type: 'UnknownEvent' as string, data: {} };
124
+
125
+ const result = evolve(state, event as any);
126
+
127
+ expect(result).toEqual(state);
128
+ });
129
+
130
+ it('handles output events by returning state unchanged', () => {
131
+ const state = {
132
+ status: 'waiting' as const,
133
+ commandTypes: ['CheckTests'],
134
+ completions: {
135
+ CheckTests: { result: 'success' as const, event: { passed: true } },
136
+ },
137
+ retryCount: 0,
138
+ maxRetries: 3,
139
+ };
140
+ const event = {
141
+ type: 'AllSettled' as const,
142
+ data: {
143
+ results: {
144
+ CheckTests: { result: 'success' as const, event: { passed: true } },
145
+ },
146
+ },
147
+ };
148
+
149
+ const result = evolve(state, event);
150
+
151
+ expect(result).toEqual({
152
+ status: 'done',
153
+ commandTypes: ['CheckTests'],
154
+ completions: {
155
+ CheckTests: { result: 'success', event: { passed: true } },
156
+ },
157
+ retryCount: 0,
158
+ maxRetries: 3,
159
+ });
160
+ });
161
+
162
+ it('transitions to done on SettledFailed', () => {
163
+ const state = {
164
+ status: 'waiting' as const,
165
+ commandTypes: ['CheckTests'],
166
+ completions: {
167
+ CheckTests: { result: 'failure' as const, event: { errors: 1 } },
168
+ },
169
+ retryCount: 0,
170
+ maxRetries: 3,
171
+ };
172
+ const event = {
173
+ type: 'SettledFailed' as const,
174
+ data: {
175
+ results: {
176
+ CheckTests: { result: 'failure' as const, event: { errors: 1 } },
177
+ },
178
+ failures: ['CheckTests'],
179
+ },
180
+ };
181
+
182
+ const result = evolve(state, event);
183
+
184
+ expect(result).toEqual({
185
+ status: 'done',
186
+ commandTypes: ['CheckTests'],
187
+ completions: {
188
+ CheckTests: { result: 'failure', event: { errors: 1 } },
189
+ },
190
+ retryCount: 0,
191
+ maxRetries: 3,
192
+ });
193
+ });
194
+
195
+ it('increments retryCount on RetryCommands', () => {
196
+ const state = {
197
+ status: 'waiting' as const,
198
+ commandTypes: ['CheckTests', 'CheckTypes'],
199
+ completions: {
200
+ CheckTests: { result: 'failure' as const, event: { errors: 1 } },
201
+ CheckTypes: { result: 'success' as const, event: {} },
202
+ },
203
+ retryCount: 0,
204
+ maxRetries: 3,
205
+ };
206
+ const event = {
207
+ type: 'RetryCommands' as const,
208
+ kind: 'Command' as const,
209
+ data: { commandTypes: ['CheckTests'] },
210
+ };
211
+
212
+ const result = evolve(state, event);
213
+
214
+ expect(result).toEqual({
215
+ status: 'waiting',
216
+ commandTypes: ['CheckTests', 'CheckTypes'],
217
+ completions: {
218
+ CheckTypes: { result: 'success', event: {} },
219
+ },
220
+ retryCount: 1,
221
+ maxRetries: 3,
222
+ });
223
+ });
224
+ });
225
+
226
+ describe('decide', () => {
227
+ it('returns empty array when not all commands completed', () => {
228
+ const state: SettledState = {
229
+ status: 'waiting',
230
+ commandTypes: ['CheckTests', 'CheckTypes', 'CheckLint'],
231
+ completions: {
232
+ CheckTests: { result: 'success', event: { duration: 100 } },
233
+ },
234
+ retryCount: 0,
235
+ maxRetries: 3,
236
+ };
237
+ const input: SettledInput = {
238
+ type: 'CommandCompleted',
239
+ data: { commandType: 'CheckTests', result: 'success', event: { duration: 100 } },
240
+ };
241
+ const result = decide(input, state);
242
+ expect(result).toEqual([]);
243
+ });
244
+
245
+ it('returns empty array when status is idle', () => {
246
+ const state = initialState();
247
+ const input: SettledInput = {
248
+ type: 'StartSettled',
249
+ data: { correlationId: 'c1', commandTypes: ['A'] },
250
+ };
251
+ const result = decide(input, state);
252
+ expect(result).toEqual([]);
253
+ });
254
+
255
+ it('returns AllSettled when all commands completed successfully', () => {
256
+ const completions = {
257
+ CheckTests: { result: 'success' as const, event: { duration: 100 } },
258
+ CheckTypes: { result: 'success' as const, event: { duration: 50 } },
259
+ CheckLint: { result: 'success' as const, event: { duration: 30 } },
260
+ };
261
+ const state: SettledState = {
262
+ status: 'waiting',
263
+ commandTypes: ['CheckTests', 'CheckTypes', 'CheckLint'],
264
+ completions,
265
+ retryCount: 0,
266
+ maxRetries: 3,
267
+ };
268
+ const input: SettledInput = {
269
+ type: 'CommandCompleted',
270
+ data: { commandType: 'CheckLint', result: 'success', event: { duration: 30 } },
271
+ };
272
+ const result = decide(input, state);
273
+ expect(result).toEqual({
274
+ type: 'AllSettled',
275
+ data: { results: completions },
276
+ });
277
+ });
278
+
279
+ it('returns RetryCommands when failures exist and retries available', () => {
280
+ const completions = {
281
+ CheckTests: { result: 'failure' as const, event: { error: 'test failed' } },
282
+ CheckTypes: { result: 'success' as const, event: { duration: 50 } },
283
+ CheckLint: { result: 'success' as const, event: { duration: 30 } },
284
+ };
285
+ const state: SettledState = {
286
+ status: 'waiting',
287
+ commandTypes: ['CheckTests', 'CheckTypes', 'CheckLint'],
288
+ completions,
289
+ retryCount: 0,
290
+ maxRetries: 3,
291
+ };
292
+ const input: SettledInput = {
293
+ type: 'CommandCompleted',
294
+ data: { commandType: 'CheckLint', result: 'success', event: { duration: 30 } },
295
+ };
296
+ const result = decide(input, state);
297
+ expect(result).toEqual({
298
+ type: 'RetryCommands',
299
+ kind: 'Command',
300
+ data: { commandTypes: ['CheckTests'] },
301
+ });
302
+ });
303
+
304
+ it('returns SettledFailed when failures exist and no retries left', () => {
305
+ const completions = {
306
+ CheckTests: { result: 'failure' as const, event: { error: 'test failed' } },
307
+ CheckTypes: { result: 'success' as const, event: { duration: 50 } },
308
+ CheckLint: { result: 'success' as const, event: { duration: 30 } },
309
+ };
310
+ const state: SettledState = {
311
+ status: 'waiting',
312
+ commandTypes: ['CheckTests', 'CheckTypes', 'CheckLint'],
313
+ completions,
314
+ retryCount: 3,
315
+ maxRetries: 3,
316
+ };
317
+ const input: SettledInput = {
318
+ type: 'CommandCompleted',
319
+ data: { commandType: 'CheckLint', result: 'success', event: { duration: 30 } },
320
+ };
321
+ const result = decide(input, state);
322
+ expect(result).toEqual({
323
+ type: 'SettledFailed',
324
+ data: {
325
+ results: completions,
326
+ failures: ['CheckTests'],
327
+ },
328
+ });
329
+ });
330
+ });
331
+
332
+ describe('createSettledWorkflow', () => {
333
+ it('factory produces workflow with configured command types and max retries', () => {
334
+ const workflow = createSettledWorkflow({
335
+ commandTypes: ['A', 'B'],
336
+ maxRetries: 5,
337
+ });
338
+ const state = workflow.initialState();
339
+ expect(state).toEqual({
340
+ status: 'idle',
341
+ commandTypes: [],
342
+ completions: {},
343
+ retryCount: 0,
344
+ maxRetries: 5,
345
+ });
346
+ expect(workflow.decide).toBe(decide);
347
+ expect(workflow.evolve).toBe(evolve);
348
+ });
349
+
350
+ it('defaults maxRetries to 3 when not provided', () => {
351
+ const workflow = createSettledWorkflow({
352
+ commandTypes: ['X'],
353
+ });
354
+ const state = workflow.initialState();
355
+ expect(state).toEqual({
356
+ status: 'idle',
357
+ commandTypes: [],
358
+ completions: {},
359
+ retryCount: 0,
360
+ maxRetries: 3,
361
+ });
362
+ });
363
+ });
364
+ });
@@ -0,0 +1,139 @@
1
+ export type SettledInput =
2
+ | { type: 'StartSettled'; data: { correlationId: string; commandTypes: string[] } }
3
+ | {
4
+ type: 'CommandCompleted';
5
+ data: { commandType: string; result: 'success' | 'failure'; event: Record<string, unknown> };
6
+ };
7
+
8
+ export type SettledOutput =
9
+ | {
10
+ type: 'AllSettled';
11
+ data: { results: Record<string, { result: 'success' | 'failure'; event: Record<string, unknown> }> };
12
+ }
13
+ | {
14
+ type: 'SettledFailed';
15
+ data: {
16
+ results: Record<string, { result: 'success' | 'failure'; event: Record<string, unknown> }>;
17
+ failures: string[];
18
+ };
19
+ }
20
+ | { type: 'RetryCommands'; kind: 'Command'; data: { commandTypes: string[] } };
21
+
22
+ export type SettledState = {
23
+ status: 'idle' | 'waiting' | 'done';
24
+ commandTypes: string[];
25
+ completions: Record<string, { result: 'success' | 'failure'; event: Record<string, unknown> }>;
26
+ retryCount: number;
27
+ maxRetries: number;
28
+ };
29
+
30
+ type SettledEvent = SettledInput | SettledOutput;
31
+
32
+ export function initialState(): SettledState {
33
+ return {
34
+ status: 'idle',
35
+ commandTypes: [],
36
+ completions: {},
37
+ retryCount: 0,
38
+ maxRetries: 3,
39
+ };
40
+ }
41
+
42
+ export function evolve(state: SettledState, event: SettledEvent): SettledState {
43
+ switch (event.type) {
44
+ case 'StartSettled':
45
+ return {
46
+ ...state,
47
+ status: 'waiting',
48
+ commandTypes: event.data.commandTypes,
49
+ };
50
+
51
+ case 'CommandCompleted':
52
+ return {
53
+ ...state,
54
+ completions: {
55
+ ...state.completions,
56
+ [event.data.commandType]: {
57
+ result: event.data.result,
58
+ event: event.data.event,
59
+ },
60
+ },
61
+ };
62
+
63
+ case 'AllSettled':
64
+ return { ...state, status: 'done' };
65
+
66
+ case 'SettledFailed':
67
+ return { ...state, status: 'done' };
68
+
69
+ case 'RetryCommands': {
70
+ const retried = new Set(event.data.commandTypes);
71
+ const completions = { ...state.completions };
72
+ for (const ct of retried) {
73
+ delete completions[ct];
74
+ }
75
+ return {
76
+ ...state,
77
+ completions,
78
+ retryCount: state.retryCount + 1,
79
+ };
80
+ }
81
+
82
+ default:
83
+ return state;
84
+ }
85
+ }
86
+
87
+ export function decide(input: SettledInput, state: SettledState): SettledOutput | SettledOutput[] {
88
+ if (state.status !== 'waiting') {
89
+ return [];
90
+ }
91
+
92
+ const allCompleted = state.commandTypes.every((ct) => ct in state.completions);
93
+ if (!allCompleted) {
94
+ return [];
95
+ }
96
+
97
+ const failures = state.commandTypes.filter((ct) => state.completions[ct].result === 'failure');
98
+
99
+ if (failures.length === 0) {
100
+ return {
101
+ type: 'AllSettled',
102
+ data: { results: state.completions },
103
+ };
104
+ }
105
+
106
+ if (state.retryCount < state.maxRetries) {
107
+ return {
108
+ type: 'RetryCommands',
109
+ kind: 'Command',
110
+ data: { commandTypes: failures },
111
+ };
112
+ }
113
+
114
+ return {
115
+ type: 'SettledFailed',
116
+ data: {
117
+ results: state.completions,
118
+ failures,
119
+ },
120
+ };
121
+ }
122
+
123
+ export function createSettledWorkflow(config: { commandTypes: string[]; maxRetries?: number }): {
124
+ decide: typeof decide;
125
+ evolve: typeof evolve;
126
+ initialState: () => SettledState;
127
+ } {
128
+ return {
129
+ decide,
130
+ evolve,
131
+ initialState: () => ({
132
+ status: 'idle',
133
+ commandTypes: [],
134
+ completions: {},
135
+ retryCount: 0,
136
+ maxRetries: config.maxRetries ?? 3,
137
+ }),
138
+ };
139
+ }
@@ -1,4 +1,4 @@
1
- export type NodeType = 'event' | 'command' | 'settled';
1
+ export type NodeType = 'event' | 'command' | 'settled' | 'phased' | 'await';
2
2
 
3
3
  export type NodeStatus = 'idle' | 'running' | 'success' | 'error';
4
4
 
package/src/index.ts CHANGED
@@ -17,6 +17,7 @@ export type {
17
17
  TriggerBuilder,
18
18
  } from './builder/define';
19
19
  export { define } from './builder/define';
20
+ export { defineV2, type PipelineV2, toGraph as toGraphV2 } from './builder/define-v2';
20
21
  export type {
21
22
  AcceptsDescriptor,
22
23
  CustomHandlerDescriptor,
@@ -33,6 +34,7 @@ export type {
33
34
  } from './core/descriptors';
34
35
  export type { Command, CommandDispatch, Event, HandlerFailedEvent } from './core/types';
35
36
  export { dispatch } from './core/types';
37
+ export * from './engine/index';
36
38
  export type { GraphEdge, GraphIR, GraphNode, NodeType } from './graph/types';
37
39
  export type { EventLoggerOptions, LogEntry } from './logging/event-logger';
38
40
  export { EventLogger } from './logging/event-logger';