@auto-engineer/pipeline 1.67.0 → 1.69.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 (70) 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 +47 -0
  5. package/dist/src/engine/workflow-processor.d.ts +3 -0
  6. package/dist/src/engine/workflow-processor.d.ts.map +1 -1
  7. package/dist/src/engine/workflow-processor.js +50 -7
  8. package/dist/src/engine/workflow-processor.js.map +1 -1
  9. package/dist/src/index.d.ts +0 -2
  10. package/dist/src/index.d.ts.map +1 -1
  11. package/dist/src/index.js +0 -2
  12. package/dist/src/index.js.map +1 -1
  13. package/dist/src/server/phased-bridge.d.ts +13 -0
  14. package/dist/src/server/phased-bridge.d.ts.map +1 -0
  15. package/dist/src/server/phased-bridge.js +103 -0
  16. package/dist/src/server/phased-bridge.js.map +1 -0
  17. package/dist/src/server/pipeline-server.d.ts +2 -2
  18. package/dist/src/server/pipeline-server.d.ts.map +1 -1
  19. package/dist/src/server/pipeline-server.js +18 -38
  20. package/dist/src/server/pipeline-server.js.map +1 -1
  21. package/dist/src/server/v2-runtime-bridge.d.ts +21 -0
  22. package/dist/src/server/v2-runtime-bridge.d.ts.map +1 -0
  23. package/dist/src/server/v2-runtime-bridge.js +182 -0
  24. package/dist/src/server/v2-runtime-bridge.js.map +1 -0
  25. package/dist/src/store/pipeline-event-store.d.ts.map +1 -1
  26. package/dist/src/store/pipeline-event-store.js +0 -30
  27. package/dist/src/store/pipeline-event-store.js.map +1 -1
  28. package/dist/src/store/pipeline-read-model.d.ts +0 -15
  29. package/dist/src/store/pipeline-read-model.d.ts.map +1 -1
  30. package/dist/src/store/pipeline-read-model.js +0 -49
  31. package/dist/src/store/pipeline-read-model.js.map +1 -1
  32. package/dist/tsconfig.tsbuildinfo +1 -1
  33. package/ketchup-plan.md +10 -12
  34. package/package.json +3 -3
  35. package/src/engine/workflow-processor.specs.ts +101 -0
  36. package/src/engine/workflow-processor.ts +54 -8
  37. package/src/index.ts +0 -2
  38. package/src/server/phased-bridge.specs.ts +272 -0
  39. package/src/server/phased-bridge.ts +130 -0
  40. package/src/server/pipeline-server.ts +20 -41
  41. package/src/server/v2-runtime-bridge.specs.ts +347 -0
  42. package/src/server/v2-runtime-bridge.ts +246 -0
  43. package/src/store/pipeline-event-store.specs.ts +0 -137
  44. package/src/store/pipeline-event-store.ts +0 -35
  45. package/src/store/pipeline-read-model.specs.ts +0 -567
  46. package/src/store/pipeline-read-model.ts +0 -71
  47. package/dist/src/projections/phased-execution-projection.d.ts +0 -77
  48. package/dist/src/projections/phased-execution-projection.d.ts.map +0 -1
  49. package/dist/src/projections/phased-execution-projection.js +0 -54
  50. package/dist/src/projections/phased-execution-projection.js.map +0 -1
  51. package/dist/src/projections/settled-instance-projection.d.ts +0 -67
  52. package/dist/src/projections/settled-instance-projection.d.ts.map +0 -1
  53. package/dist/src/projections/settled-instance-projection.js +0 -66
  54. package/dist/src/projections/settled-instance-projection.js.map +0 -1
  55. package/dist/src/runtime/phased-executor.d.ts +0 -34
  56. package/dist/src/runtime/phased-executor.d.ts.map +0 -1
  57. package/dist/src/runtime/phased-executor.js +0 -172
  58. package/dist/src/runtime/phased-executor.js.map +0 -1
  59. package/dist/src/runtime/settled-tracker.d.ts +0 -44
  60. package/dist/src/runtime/settled-tracker.d.ts.map +0 -1
  61. package/dist/src/runtime/settled-tracker.js +0 -170
  62. package/dist/src/runtime/settled-tracker.js.map +0 -1
  63. package/src/projections/phased-execution-projection.specs.ts +0 -202
  64. package/src/projections/phased-execution-projection.ts +0 -146
  65. package/src/projections/settled-instance-projection.specs.ts +0 -296
  66. package/src/projections/settled-instance-projection.ts +0 -160
  67. package/src/runtime/phased-executor.specs.ts +0 -680
  68. package/src/runtime/phased-executor.ts +0 -230
  69. package/src/runtime/settled-tracker.specs.ts +0 -1044
  70. package/src/runtime/settled-tracker.ts +0 -235
@@ -0,0 +1,246 @@
1
+ import type { Command, Event } from '@auto-engineer/message-bus';
2
+ import type { SettledHandlerDescriptor } from '../core/descriptors.js';
3
+ import type { SettledInput, SettledOutput, SettledState } from '../engine/workflows/settled-workflow.js';
4
+ import { decide, evolve, initialState } from '../engine/workflows/settled-workflow.js';
5
+
6
+ type NodeStatus = 'idle' | 'running' | 'success' | 'error';
7
+
8
+ export interface SettledStats {
9
+ status: NodeStatus;
10
+ pendingCount: number;
11
+ endedCount: number;
12
+ }
13
+
14
+ interface V2RuntimeBridgeOptions {
15
+ onDispatch: (commandType: string, data: unknown, correlationId: string) => void;
16
+ }
17
+
18
+ interface RegisteredSettled {
19
+ templateId: string;
20
+ descriptor: SettledHandlerDescriptor;
21
+ commandTypes: readonly string[];
22
+ maxRetries: number;
23
+ }
24
+
25
+ type SettledEvent = SettledInput | SettledOutput;
26
+
27
+ function rebuildState(events: SettledEvent[], maxRetries: number): SettledState {
28
+ let state: SettledState = { ...initialState(), maxRetries };
29
+ for (const event of events) {
30
+ state = evolve(state, event);
31
+ }
32
+ return state;
33
+ }
34
+
35
+ function processInput(input: SettledInput, history: SettledEvent[], maxRetries: number): SettledOutput[] {
36
+ history.push(input);
37
+ const state = rebuildState(history, maxRetries);
38
+ const result = decide(input, state);
39
+ const outputs = Array.isArray(result) ? result : [result];
40
+ for (const output of outputs) {
41
+ history.push(output);
42
+ }
43
+ return outputs;
44
+ }
45
+
46
+ export function createV2RuntimeBridge(options: V2RuntimeBridgeOptions) {
47
+ const registrations = new Map<string, RegisteredSettled>();
48
+ const commandToTemplateIds = new Map<string, Set<string>>();
49
+ const eventBuffer = new Map<string, Event[]>();
50
+ const settledCounts = new Map<string, number>();
51
+ const keyedHistories = new Map<string, SettledEvent[]>();
52
+
53
+ function bufferKey(templateId: string, correlationId: string, commandType: string): string {
54
+ return `${templateId}-${correlationId}-${commandType}`;
55
+ }
56
+
57
+ function compositeKey(templateId: string, correlationId: string): string {
58
+ return `${templateId}-${correlationId}`;
59
+ }
60
+
61
+ function isValidId(id: string | undefined): id is string {
62
+ return id !== undefined && id !== null && id !== '';
63
+ }
64
+
65
+ function ensureBuffer(templateId: string, correlationId: string, commandType: string): Event[] {
66
+ const key = bufferKey(templateId, correlationId, commandType);
67
+ let events = eventBuffer.get(key);
68
+ if (!events) {
69
+ events = [];
70
+ eventBuffer.set(key, events);
71
+ }
72
+ return events;
73
+ }
74
+
75
+ function collectBufferedEvents(
76
+ templateId: string,
77
+ correlationId: string,
78
+ commandTypes: readonly string[],
79
+ ): Record<string, Event[]> {
80
+ const result: Record<string, Event[]> = {};
81
+ for (const ct of commandTypes) {
82
+ result[ct] = ensureBuffer(templateId, correlationId, ct);
83
+ }
84
+ return result;
85
+ }
86
+
87
+ function clearBuffer(templateId: string, correlationId: string, commandTypes: readonly string[]): void {
88
+ for (const ct of commandTypes) {
89
+ eventBuffer.delete(bufferKey(templateId, correlationId, ct));
90
+ }
91
+ }
92
+
93
+ function getHistory(templateId: string, correlationId: string): SettledEvent[] {
94
+ const key = compositeKey(templateId, correlationId);
95
+ let history = keyedHistories.get(key);
96
+ if (!history) {
97
+ history = [];
98
+ keyedHistories.set(key, history);
99
+ }
100
+ return history;
101
+ }
102
+
103
+ function resetHistory(templateId: string, correlationId: string): void {
104
+ keyedHistories.delete(compositeKey(templateId, correlationId));
105
+ }
106
+
107
+ function handleOutputs(outputs: SettledOutput[], registration: RegisteredSettled, correlationId: string): void {
108
+ for (const output of outputs) {
109
+ if (output.type === 'AllSettled') {
110
+ const events = collectBufferedEvents(registration.templateId, correlationId, registration.commandTypes);
111
+
112
+ const send = (commandType: string, data: unknown) => {
113
+ options.onDispatch(commandType, data, correlationId);
114
+ };
115
+
116
+ const handlerResult = registration.descriptor.handler(events, send);
117
+ const persist =
118
+ handlerResult !== null &&
119
+ handlerResult !== undefined &&
120
+ typeof handlerResult === 'object' &&
121
+ 'persist' in handlerResult &&
122
+ handlerResult.persist === true;
123
+
124
+ const countKey = compositeKey(registration.templateId, correlationId);
125
+ settledCounts.set(countKey, (settledCounts.get(countKey) ?? 0) + 1);
126
+
127
+ if (persist) {
128
+ resetHistory(registration.templateId, correlationId);
129
+ clearBuffer(registration.templateId, correlationId, registration.commandTypes);
130
+ }
131
+ }
132
+
133
+ if (output.type === 'RetryCommands') {
134
+ for (const ct of output.data.commandTypes) {
135
+ options.onDispatch(ct, {}, correlationId);
136
+ eventBuffer.delete(bufferKey(registration.templateId, correlationId, ct));
137
+ }
138
+ }
139
+ }
140
+ }
141
+
142
+ return {
143
+ registerSettled(descriptor: SettledHandlerDescriptor, config?: { maxRetries?: number }): void {
144
+ const commandTypes = descriptor.commandTypes;
145
+ const templateId = `template-${commandTypes.join(',')}`;
146
+
147
+ const registration: RegisteredSettled = {
148
+ templateId,
149
+ descriptor,
150
+ commandTypes,
151
+ maxRetries: config?.maxRetries ?? 3,
152
+ };
153
+
154
+ registrations.set(templateId, registration);
155
+
156
+ for (const ct of commandTypes) {
157
+ const existing = commandToTemplateIds.get(ct) ?? new Set<string>();
158
+ existing.add(templateId);
159
+ commandToTemplateIds.set(ct, existing);
160
+ }
161
+ },
162
+
163
+ onCommandStarted(command: Command): void {
164
+ const { type: commandType, correlationId, requestId } = command;
165
+
166
+ if (!isValidId(correlationId) || !isValidId(requestId)) {
167
+ return;
168
+ }
169
+
170
+ const templateIds = commandToTemplateIds.get(commandType);
171
+ if (!templateIds) {
172
+ return;
173
+ }
174
+
175
+ for (const templateId of templateIds) {
176
+ const registration = registrations.get(templateId);
177
+ if (registration) {
178
+ const history = getHistory(templateId, correlationId);
179
+ const input: SettledInput = {
180
+ type: 'StartSettled',
181
+ data: { correlationId, commandTypes: [...registration.commandTypes] },
182
+ };
183
+ processInput(input, history, registration.maxRetries);
184
+ }
185
+ }
186
+ },
187
+
188
+ onEventReceived(event: Event, sourceCommandType: string, result: 'success' | 'failure' = 'success'): void {
189
+ const correlationId = event.correlationId;
190
+
191
+ if (!isValidId(correlationId)) {
192
+ return;
193
+ }
194
+
195
+ const templateIds = commandToTemplateIds.get(sourceCommandType);
196
+ if (!templateIds) {
197
+ return;
198
+ }
199
+
200
+ for (const templateId of templateIds) {
201
+ const registration = registrations.get(templateId)!;
202
+ const existing = ensureBuffer(templateId, correlationId, sourceCommandType);
203
+ existing.push(event);
204
+
205
+ const history = getHistory(templateId, correlationId);
206
+ const input: SettledInput = {
207
+ type: 'CommandCompleted',
208
+ data: {
209
+ commandType: sourceCommandType,
210
+ result,
211
+ event: { ...event.data },
212
+ },
213
+ };
214
+ const outputs = processInput(input, history, registration.maxRetries);
215
+
216
+ handleOutputs(outputs, registration, correlationId);
217
+ }
218
+ },
219
+
220
+ getSettledStats(correlationId: string, templateId: string): SettledStats {
221
+ const registration = registrations.get(templateId);
222
+ if (!registration) {
223
+ return { status: 'idle', pendingCount: 0, endedCount: 0 };
224
+ }
225
+
226
+ const history = getHistory(templateId, correlationId);
227
+ const state = rebuildState(history, registration.maxRetries);
228
+ const countKey = compositeKey(templateId, correlationId);
229
+ const endedCount = settledCounts.get(countKey) ?? 0;
230
+
231
+ if (state.status === 'idle') {
232
+ return { status: 'idle', pendingCount: 0, endedCount: 0 };
233
+ }
234
+
235
+ if (state.status === 'waiting') {
236
+ return { status: 'running', pendingCount: 1, endedCount };
237
+ }
238
+
239
+ const hasFailure = Object.values(state.completions).some((c) => c.result === 'failure');
240
+ if (hasFailure) {
241
+ return { status: 'error', pendingCount: 0, endedCount };
242
+ }
243
+ return { status: 'success', pendingCount: 0, endedCount };
244
+ },
245
+ };
246
+ }
@@ -1,8 +1,6 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
  import type { ItemStatusDocument } from '../projections/item-status-projection';
3
3
  import type { NodeStatusDocument } from '../projections/node-status-projection';
4
- import type { PhasedExecutionDocument } from '../projections/phased-execution-projection';
5
- import type { SettledInstanceDocument } from '../projections/settled-instance-projection';
6
4
  import { createPipelineEventStore } from './pipeline-event-store';
7
5
 
8
6
  describe('PipelineEventStore', () => {
@@ -219,139 +217,4 @@ describe('PipelineEventStore', () => {
219
217
  }
220
218
  });
221
219
  });
222
-
223
- describe('settled instance projection', () => {
224
- it('should project SettledInstanceCreated events', async () => {
225
- const { eventStore, database, close } = createPipelineEventStore();
226
- try {
227
- await eventStore.appendToStream('settled-c1', [
228
- {
229
- type: 'SettledInstanceCreated',
230
- data: {
231
- templateId: 'template-CmdA,CmdB',
232
- correlationId: 'c1',
233
- commandTypes: ['CmdA', 'CmdB'],
234
- },
235
- },
236
- ]);
237
-
238
- const collection = database.collection<SettledInstanceDocument & { _id: string }>('SettledInstance');
239
- const instances = await collection.find();
240
-
241
- expect(instances.length).toBe(1);
242
- expect(instances[0]?.status).toBe('active');
243
- expect(instances[0]?.commandTrackers).toHaveLength(2);
244
- } finally {
245
- await close();
246
- }
247
- });
248
-
249
- it('should update settled instance through lifecycle events', async () => {
250
- const { eventStore, database, close } = createPipelineEventStore();
251
- try {
252
- await eventStore.appendToStream('settled-c1', [
253
- {
254
- type: 'SettledInstanceCreated',
255
- data: {
256
- templateId: 'template-CmdA',
257
- correlationId: 'c1',
258
- commandTypes: ['CmdA'],
259
- },
260
- },
261
- {
262
- type: 'SettledCommandStarted',
263
- data: {
264
- templateId: 'template-CmdA',
265
- correlationId: 'c1',
266
- commandType: 'CmdA',
267
- },
268
- },
269
- ]);
270
-
271
- const collection = database.collection<SettledInstanceDocument & { _id: string }>('SettledInstance');
272
- const instances = await collection.find();
273
-
274
- expect(instances.length).toBe(1);
275
- expect(instances[0]?.commandTrackers[0]?.hasStarted).toBe(true);
276
- expect(instances[0]?.commandTrackers[0]?.hasCompleted).toBe(false);
277
- } finally {
278
- await close();
279
- }
280
- });
281
- });
282
-
283
- describe('phased execution projection', () => {
284
- it('should project PhasedExecutionStarted events', async () => {
285
- const { eventStore, database, close } = createPipelineEventStore();
286
- try {
287
- await eventStore.appendToStream('phased-c1', [
288
- {
289
- type: 'PhasedExecutionStarted',
290
- data: {
291
- executionId: 'exec-1',
292
- correlationId: 'c1',
293
- handlerId: 'handler-1',
294
- triggerEvent: { type: 'TestEvent', correlationId: 'c1', data: {} },
295
- items: [{ key: 'a', phase: 'prepare', dispatched: false, completed: false }],
296
- phases: ['prepare', 'execute'],
297
- },
298
- },
299
- ]);
300
-
301
- const collection = database.collection<PhasedExecutionDocument & { _id: string }>('PhasedExecution');
302
- const executions = await collection.find();
303
-
304
- expect(executions.length).toBe(1);
305
- expect(executions[0]?.status).toBe('active');
306
- expect(executions[0]?.items).toHaveLength(1);
307
- } finally {
308
- await close();
309
- }
310
- });
311
-
312
- it('should update phased execution through lifecycle events', async () => {
313
- const { eventStore, database, close } = createPipelineEventStore();
314
- try {
315
- await eventStore.appendToStream('phased-c1', [
316
- {
317
- type: 'PhasedExecutionStarted',
318
- data: {
319
- executionId: 'exec-1',
320
- correlationId: 'c1',
321
- handlerId: 'handler-1',
322
- triggerEvent: { type: 'TestEvent', correlationId: 'c1', data: {} },
323
- items: [{ key: 'a', phase: 'prepare', dispatched: false, completed: false }],
324
- phases: ['prepare'],
325
- },
326
- },
327
- {
328
- type: 'PhasedItemDispatched',
329
- data: { executionId: 'exec-1', itemKey: 'a', phase: 'prepare' },
330
- },
331
- {
332
- type: 'PhasedItemCompleted',
333
- data: {
334
- executionId: 'exec-1',
335
- itemKey: 'a',
336
- resultEvent: { type: 'ItemDone', correlationId: 'c1', data: {} },
337
- },
338
- },
339
- {
340
- type: 'PhasedExecutionCompleted',
341
- data: { executionId: 'exec-1', success: true, results: ['a'] },
342
- },
343
- ]);
344
-
345
- const collection = database.collection<PhasedExecutionDocument & { _id: string }>('PhasedExecution');
346
- const executions = await collection.find();
347
-
348
- expect(executions.length).toBe(1);
349
- expect(executions[0]?.status).toBe('completed');
350
- expect(executions[0]?.items[0]?.dispatched).toBe(true);
351
- expect(executions[0]?.items[0]?.completed).toBe(true);
352
- } finally {
353
- await close();
354
- }
355
- });
356
- });
357
220
  });
@@ -17,10 +17,6 @@ import type { MessageLogDocument, MessageLogEvent } from '../projections/message
17
17
  import { evolve as evolveMessageLog } from '../projections/message-log-projection';
18
18
  import type { NodeStatusChangedEvent, NodeStatusDocument } from '../projections/node-status-projection';
19
19
  import { evolve as evolveNodeStatus } from '../projections/node-status-projection';
20
- import type { PhasedExecutionDocument, PhasedExecutionEvent } from '../projections/phased-execution-projection';
21
- import { evolve as evolvePhasedExecution } from '../projections/phased-execution-projection';
22
- import type { SettledEvent, SettledInstanceDocument } from '../projections/settled-instance-projection';
23
- import { evolve as evolveSettledInstance } from '../projections/settled-instance-projection';
24
20
  import type { StatsDocument } from '../projections/stats-projection';
25
21
  import { evolve as evolveStats } from '../projections/stats-projection';
26
22
  import { PipelineReadModel } from './pipeline-read-model';
@@ -80,35 +76,6 @@ function createProjections() {
80
76
  evolve: (document: StatsDocument | null, event: MessageLogEvent) => evolveStats(document, event),
81
77
  });
82
78
 
83
- const settledInstanceProjection = inMemorySingleStreamProjection<SettledInstanceDocument, SettledEvent>({
84
- collectionName: 'SettledInstance',
85
- canHandle: [
86
- 'SettledInstanceCreated',
87
- 'SettledCommandStarted',
88
- 'SettledEventReceived',
89
- 'SettledHandlerFired',
90
- 'SettledInstanceReset',
91
- 'SettledInstanceCleaned',
92
- ],
93
- getDocumentId: (event) => `${event.data.templateId}-${event.data.correlationId}`,
94
- evolve: (document: SettledInstanceDocument | null, event: SettledEvent) => evolveSettledInstance(document, event),
95
- });
96
-
97
- const phasedExecutionProjection = inMemorySingleStreamProjection<PhasedExecutionDocument, PhasedExecutionEvent>({
98
- collectionName: 'PhasedExecution',
99
- canHandle: [
100
- 'PhasedExecutionStarted',
101
- 'PhasedItemDispatched',
102
- 'PhasedItemCompleted',
103
- 'PhasedItemFailed',
104
- 'PhasedPhaseAdvanced',
105
- 'PhasedExecutionCompleted',
106
- ],
107
- getDocumentId: (event) => event.data.executionId,
108
- evolve: (document: PhasedExecutionDocument | null, event: PhasedExecutionEvent) =>
109
- evolvePhasedExecution(document, event),
110
- });
111
-
112
79
  const awaitTrackerProjection = inMemorySingleStreamProjection<AwaitTrackerDocument, AwaitEvent>({
113
80
  collectionName: 'AwaitTracker',
114
81
  canHandle: ['AwaitStarted', 'AwaitItemCompleted', 'AwaitCompleted'],
@@ -122,8 +89,6 @@ function createProjections() {
122
89
  latestRunProjection,
123
90
  messageLogProjection,
124
91
  statsProjection,
125
- settledInstanceProjection,
126
- phasedExecutionProjection,
127
92
  awaitTrackerProjection,
128
93
  ] as Parameters<typeof inlineProjections<InMemoryReadEventMetadata>>[0]);
129
94
  }