@auto-engineer/pipeline 0.0.1

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 (270) hide show
  1. package/.turbo/turbo-build.log +5 -0
  2. package/LICENSE +10 -0
  3. package/claude.md +160 -0
  4. package/dist/src/builder/define.d.ts +90 -0
  5. package/dist/src/builder/define.d.ts.map +1 -0
  6. package/dist/src/builder/define.js +425 -0
  7. package/dist/src/builder/define.js.map +1 -0
  8. package/dist/src/builder/define.specs.d.ts +2 -0
  9. package/dist/src/builder/define.specs.d.ts.map +1 -0
  10. package/dist/src/builder/define.specs.js +435 -0
  11. package/dist/src/builder/define.specs.js.map +1 -0
  12. package/dist/src/config/pipeline-config.d.ts +13 -0
  13. package/dist/src/config/pipeline-config.d.ts.map +1 -0
  14. package/dist/src/config/pipeline-config.js +15 -0
  15. package/dist/src/config/pipeline-config.js.map +1 -0
  16. package/dist/src/core/descriptors.d.ts +84 -0
  17. package/dist/src/core/descriptors.d.ts.map +1 -0
  18. package/dist/src/core/descriptors.js +2 -0
  19. package/dist/src/core/descriptors.js.map +1 -0
  20. package/dist/src/core/descriptors.specs.d.ts +2 -0
  21. package/dist/src/core/descriptors.specs.d.ts.map +1 -0
  22. package/dist/src/core/descriptors.specs.js +24 -0
  23. package/dist/src/core/descriptors.specs.js.map +1 -0
  24. package/dist/src/core/types.d.ts +10 -0
  25. package/dist/src/core/types.d.ts.map +1 -0
  26. package/dist/src/core/types.js +4 -0
  27. package/dist/src/core/types.js.map +1 -0
  28. package/dist/src/core/types.specs.d.ts +2 -0
  29. package/dist/src/core/types.specs.d.ts.map +1 -0
  30. package/dist/src/core/types.specs.js +40 -0
  31. package/dist/src/core/types.specs.js.map +1 -0
  32. package/dist/src/graph/types.d.ts +17 -0
  33. package/dist/src/graph/types.d.ts.map +1 -0
  34. package/dist/src/graph/types.js +2 -0
  35. package/dist/src/graph/types.js.map +1 -0
  36. package/dist/src/graph/types.specs.d.ts +2 -0
  37. package/dist/src/graph/types.specs.d.ts.map +1 -0
  38. package/dist/src/graph/types.specs.js +148 -0
  39. package/dist/src/graph/types.specs.js.map +1 -0
  40. package/dist/src/index.d.ts +20 -0
  41. package/dist/src/index.d.ts.map +1 -0
  42. package/dist/src/index.js +12 -0
  43. package/dist/src/index.js.map +1 -0
  44. package/dist/src/logging/event-logger.d.ts +21 -0
  45. package/dist/src/logging/event-logger.d.ts.map +1 -0
  46. package/dist/src/logging/event-logger.js +31 -0
  47. package/dist/src/logging/event-logger.js.map +1 -0
  48. package/dist/src/logging/event-logger.specs.d.ts +2 -0
  49. package/dist/src/logging/event-logger.specs.d.ts.map +1 -0
  50. package/dist/src/logging/event-logger.specs.js +81 -0
  51. package/dist/src/logging/event-logger.specs.js.map +1 -0
  52. package/dist/src/plugins/handler-adapter.d.ts +5 -0
  53. package/dist/src/plugins/handler-adapter.d.ts.map +1 -0
  54. package/dist/src/plugins/handler-adapter.js +17 -0
  55. package/dist/src/plugins/handler-adapter.js.map +1 -0
  56. package/dist/src/plugins/handler-adapter.specs.d.ts +2 -0
  57. package/dist/src/plugins/handler-adapter.specs.d.ts.map +1 -0
  58. package/dist/src/plugins/handler-adapter.specs.js +129 -0
  59. package/dist/src/plugins/handler-adapter.specs.js.map +1 -0
  60. package/dist/src/plugins/plugin-loader.d.ts +25 -0
  61. package/dist/src/plugins/plugin-loader.d.ts.map +1 -0
  62. package/dist/src/plugins/plugin-loader.js +150 -0
  63. package/dist/src/plugins/plugin-loader.js.map +1 -0
  64. package/dist/src/plugins/plugin-loader.specs.d.ts +2 -0
  65. package/dist/src/plugins/plugin-loader.specs.d.ts.map +1 -0
  66. package/dist/src/plugins/plugin-loader.specs.js +246 -0
  67. package/dist/src/plugins/plugin-loader.specs.js.map +1 -0
  68. package/dist/src/runtime/await-tracker.d.ts +10 -0
  69. package/dist/src/runtime/await-tracker.d.ts.map +1 -0
  70. package/dist/src/runtime/await-tracker.js +42 -0
  71. package/dist/src/runtime/await-tracker.js.map +1 -0
  72. package/dist/src/runtime/await-tracker.specs.d.ts +2 -0
  73. package/dist/src/runtime/await-tracker.specs.d.ts.map +1 -0
  74. package/dist/src/runtime/await-tracker.specs.js +46 -0
  75. package/dist/src/runtime/await-tracker.specs.js.map +1 -0
  76. package/dist/src/runtime/context.d.ts +12 -0
  77. package/dist/src/runtime/context.d.ts.map +1 -0
  78. package/dist/src/runtime/context.js +2 -0
  79. package/dist/src/runtime/context.js.map +1 -0
  80. package/dist/src/runtime/context.specs.d.ts +2 -0
  81. package/dist/src/runtime/context.specs.d.ts.map +1 -0
  82. package/dist/src/runtime/context.specs.js +26 -0
  83. package/dist/src/runtime/context.specs.js.map +1 -0
  84. package/dist/src/runtime/event-command-map.d.ts +15 -0
  85. package/dist/src/runtime/event-command-map.d.ts.map +1 -0
  86. package/dist/src/runtime/event-command-map.js +26 -0
  87. package/dist/src/runtime/event-command-map.js.map +1 -0
  88. package/dist/src/runtime/event-command-map.specs.d.ts +2 -0
  89. package/dist/src/runtime/event-command-map.specs.d.ts.map +1 -0
  90. package/dist/src/runtime/event-command-map.specs.js +108 -0
  91. package/dist/src/runtime/event-command-map.specs.js.map +1 -0
  92. package/dist/src/runtime/phased-executor.d.ts +29 -0
  93. package/dist/src/runtime/phased-executor.d.ts.map +1 -0
  94. package/dist/src/runtime/phased-executor.js +164 -0
  95. package/dist/src/runtime/phased-executor.js.map +1 -0
  96. package/dist/src/runtime/phased-executor.specs.d.ts +2 -0
  97. package/dist/src/runtime/phased-executor.specs.d.ts.map +1 -0
  98. package/dist/src/runtime/phased-executor.specs.js +256 -0
  99. package/dist/src/runtime/phased-executor.specs.js.map +1 -0
  100. package/dist/src/runtime/pipeline-runtime.d.ts +17 -0
  101. package/dist/src/runtime/pipeline-runtime.d.ts.map +1 -0
  102. package/dist/src/runtime/pipeline-runtime.js +87 -0
  103. package/dist/src/runtime/pipeline-runtime.js.map +1 -0
  104. package/dist/src/runtime/pipeline-runtime.specs.d.ts +2 -0
  105. package/dist/src/runtime/pipeline-runtime.specs.d.ts.map +1 -0
  106. package/dist/src/runtime/pipeline-runtime.specs.js +192 -0
  107. package/dist/src/runtime/pipeline-runtime.specs.js.map +1 -0
  108. package/dist/src/runtime/settled-tracker.d.ts +42 -0
  109. package/dist/src/runtime/settled-tracker.d.ts.map +1 -0
  110. package/dist/src/runtime/settled-tracker.js +161 -0
  111. package/dist/src/runtime/settled-tracker.js.map +1 -0
  112. package/dist/src/runtime/settled-tracker.specs.d.ts +2 -0
  113. package/dist/src/runtime/settled-tracker.specs.d.ts.map +1 -0
  114. package/dist/src/runtime/settled-tracker.specs.js +361 -0
  115. package/dist/src/runtime/settled-tracker.specs.js.map +1 -0
  116. package/dist/src/server/full-orchestration.e2e.specs.d.ts +2 -0
  117. package/dist/src/server/full-orchestration.e2e.specs.d.ts.map +1 -0
  118. package/dist/src/server/full-orchestration.e2e.specs.js +561 -0
  119. package/dist/src/server/full-orchestration.e2e.specs.js.map +1 -0
  120. package/dist/src/server/pipeline-server.d.ts +59 -0
  121. package/dist/src/server/pipeline-server.d.ts.map +1 -0
  122. package/dist/src/server/pipeline-server.e2e.specs.d.ts +2 -0
  123. package/dist/src/server/pipeline-server.e2e.specs.d.ts.map +1 -0
  124. package/dist/src/server/pipeline-server.e2e.specs.js +381 -0
  125. package/dist/src/server/pipeline-server.e2e.specs.js.map +1 -0
  126. package/dist/src/server/pipeline-server.js +527 -0
  127. package/dist/src/server/pipeline-server.js.map +1 -0
  128. package/dist/src/server/pipeline-server.specs.d.ts +2 -0
  129. package/dist/src/server/pipeline-server.specs.d.ts.map +1 -0
  130. package/dist/src/server/pipeline-server.specs.js +662 -0
  131. package/dist/src/server/pipeline-server.specs.js.map +1 -0
  132. package/dist/src/server/sse-manager.d.ts +12 -0
  133. package/dist/src/server/sse-manager.d.ts.map +1 -0
  134. package/dist/src/server/sse-manager.js +63 -0
  135. package/dist/src/server/sse-manager.js.map +1 -0
  136. package/dist/src/server/sse-manager.specs.d.ts +2 -0
  137. package/dist/src/server/sse-manager.specs.d.ts.map +1 -0
  138. package/dist/src/server/sse-manager.specs.js +158 -0
  139. package/dist/src/server/sse-manager.specs.js.map +1 -0
  140. package/dist/src/testing/event-capture.d.ts +14 -0
  141. package/dist/src/testing/event-capture.d.ts.map +1 -0
  142. package/dist/src/testing/event-capture.js +55 -0
  143. package/dist/src/testing/event-capture.js.map +1 -0
  144. package/dist/src/testing/event-capture.specs.d.ts +2 -0
  145. package/dist/src/testing/event-capture.specs.d.ts.map +1 -0
  146. package/dist/src/testing/event-capture.specs.js +114 -0
  147. package/dist/src/testing/event-capture.specs.js.map +1 -0
  148. package/dist/src/testing/fixtures/kanban-full.pipeline.d.ts +7 -0
  149. package/dist/src/testing/fixtures/kanban-full.pipeline.d.ts.map +1 -0
  150. package/dist/src/testing/fixtures/kanban-full.pipeline.js +168 -0
  151. package/dist/src/testing/fixtures/kanban-full.pipeline.js.map +1 -0
  152. package/dist/src/testing/fixtures/kanban-full.pipeline.specs.d.ts +2 -0
  153. package/dist/src/testing/fixtures/kanban-full.pipeline.specs.d.ts.map +1 -0
  154. package/dist/src/testing/fixtures/kanban-full.pipeline.specs.js +263 -0
  155. package/dist/src/testing/fixtures/kanban-full.pipeline.specs.js.map +1 -0
  156. package/dist/src/testing/fixtures/kanban-todo.config.d.ts +3 -0
  157. package/dist/src/testing/fixtures/kanban-todo.config.d.ts.map +1 -0
  158. package/dist/src/testing/fixtures/kanban-todo.config.js +19 -0
  159. package/dist/src/testing/fixtures/kanban-todo.config.js.map +1 -0
  160. package/dist/src/testing/fixtures/kanban.pipeline.d.ts +5 -0
  161. package/dist/src/testing/fixtures/kanban.pipeline.d.ts.map +1 -0
  162. package/dist/src/testing/fixtures/kanban.pipeline.js +76 -0
  163. package/dist/src/testing/fixtures/kanban.pipeline.js.map +1 -0
  164. package/dist/src/testing/fixtures/kanban.pipeline.specs.d.ts +2 -0
  165. package/dist/src/testing/fixtures/kanban.pipeline.specs.d.ts.map +1 -0
  166. package/dist/src/testing/fixtures/kanban.pipeline.specs.js +29 -0
  167. package/dist/src/testing/fixtures/kanban.pipeline.specs.js.map +1 -0
  168. package/dist/src/testing/kanban-todo.e2e.specs.d.ts +2 -0
  169. package/dist/src/testing/kanban-todo.e2e.specs.d.ts.map +1 -0
  170. package/dist/src/testing/kanban-todo.e2e.specs.js +160 -0
  171. package/dist/src/testing/kanban-todo.e2e.specs.js.map +1 -0
  172. package/dist/src/testing/mock-handlers.d.ts +21 -0
  173. package/dist/src/testing/mock-handlers.d.ts.map +1 -0
  174. package/dist/src/testing/mock-handlers.js +34 -0
  175. package/dist/src/testing/mock-handlers.js.map +1 -0
  176. package/dist/src/testing/mock-handlers.specs.d.ts +2 -0
  177. package/dist/src/testing/mock-handlers.specs.d.ts.map +1 -0
  178. package/dist/src/testing/mock-handlers.specs.js +193 -0
  179. package/dist/src/testing/mock-handlers.specs.js.map +1 -0
  180. package/dist/src/testing/real-execution.e2e.specs.d.ts +2 -0
  181. package/dist/src/testing/real-execution.e2e.specs.d.ts.map +1 -0
  182. package/dist/src/testing/real-execution.e2e.specs.js +140 -0
  183. package/dist/src/testing/real-execution.e2e.specs.js.map +1 -0
  184. package/dist/src/testing/real-plugin.e2e.specs.d.ts +2 -0
  185. package/dist/src/testing/real-plugin.e2e.specs.d.ts.map +1 -0
  186. package/dist/src/testing/real-plugin.e2e.specs.js +65 -0
  187. package/dist/src/testing/real-plugin.e2e.specs.js.map +1 -0
  188. package/dist/src/testing/server-startup.e2e.specs.d.ts +2 -0
  189. package/dist/src/testing/server-startup.e2e.specs.d.ts.map +1 -0
  190. package/dist/src/testing/server-startup.e2e.specs.js +104 -0
  191. package/dist/src/testing/server-startup.e2e.specs.js.map +1 -0
  192. package/dist/src/testing/snapshot-compare.d.ts +18 -0
  193. package/dist/src/testing/snapshot-compare.d.ts.map +1 -0
  194. package/dist/src/testing/snapshot-compare.js +86 -0
  195. package/dist/src/testing/snapshot-compare.js.map +1 -0
  196. package/dist/src/testing/snapshot-compare.specs.d.ts +2 -0
  197. package/dist/src/testing/snapshot-compare.specs.d.ts.map +1 -0
  198. package/dist/src/testing/snapshot-compare.specs.js +112 -0
  199. package/dist/src/testing/snapshot-compare.specs.js.map +1 -0
  200. package/dist/src/testing/snapshot-sanitize.d.ts +8 -0
  201. package/dist/src/testing/snapshot-sanitize.d.ts.map +1 -0
  202. package/dist/src/testing/snapshot-sanitize.js +10 -0
  203. package/dist/src/testing/snapshot-sanitize.js.map +1 -0
  204. package/dist/src/testing/snapshot-sanitize.specs.d.ts +2 -0
  205. package/dist/src/testing/snapshot-sanitize.specs.d.ts.map +1 -0
  206. package/dist/src/testing/snapshot-sanitize.specs.js +104 -0
  207. package/dist/src/testing/snapshot-sanitize.specs.js.map +1 -0
  208. package/dist/tsconfig.tsbuildinfo +1 -0
  209. package/docs/testing-analysis.md +395 -0
  210. package/package.json +31 -0
  211. package/pipeline-api-new.md +1078 -0
  212. package/pomodoro-plan.md +651 -0
  213. package/scripts/run-kanban-e2e.ts +219 -0
  214. package/scripts/start-server.ts +64 -0
  215. package/snapshots/e2e-run-2025-12-22T15-52-03.json +613 -0
  216. package/snapshots/e2e-run-2025-12-22T16-51-30.json +699 -0
  217. package/src/builder/define.specs.ts +531 -0
  218. package/src/builder/define.ts +700 -0
  219. package/src/config/pipeline-config.ts +32 -0
  220. package/src/core/descriptors.specs.ts +28 -0
  221. package/src/core/descriptors.ts +99 -0
  222. package/src/core/types.specs.ts +44 -0
  223. package/src/core/types.ts +16 -0
  224. package/src/graph/types.specs.ts +176 -0
  225. package/src/graph/types.ts +19 -0
  226. package/src/index.ts +54 -0
  227. package/src/logging/event-logger.specs.ts +100 -0
  228. package/src/logging/event-logger.ts +50 -0
  229. package/src/plugins/handler-adapter.specs.ts +164 -0
  230. package/src/plugins/handler-adapter.ts +21 -0
  231. package/src/plugins/plugin-loader.specs.ts +295 -0
  232. package/src/plugins/plugin-loader.ts +202 -0
  233. package/src/runtime/await-tracker.specs.ts +52 -0
  234. package/src/runtime/await-tracker.ts +50 -0
  235. package/src/runtime/context.specs.ts +28 -0
  236. package/src/runtime/context.ts +13 -0
  237. package/src/runtime/event-command-map.specs.ts +136 -0
  238. package/src/runtime/event-command-map.ts +38 -0
  239. package/src/runtime/phased-executor.specs.ts +358 -0
  240. package/src/runtime/phased-executor.ts +224 -0
  241. package/src/runtime/pipeline-runtime.specs.ts +214 -0
  242. package/src/runtime/pipeline-runtime.ts +119 -0
  243. package/src/runtime/settled-tracker.specs.ts +448 -0
  244. package/src/runtime/settled-tracker.ts +237 -0
  245. package/src/server/full-orchestration.e2e.specs.ts +672 -0
  246. package/src/server/pipeline-server.e2e.specs.ts +505 -0
  247. package/src/server/pipeline-server.specs.ts +761 -0
  248. package/src/server/pipeline-server.ts +656 -0
  249. package/src/server/sse-manager.specs.ts +208 -0
  250. package/src/server/sse-manager.ts +79 -0
  251. package/src/testing/event-capture.specs.ts +143 -0
  252. package/src/testing/event-capture.ts +65 -0
  253. package/src/testing/fixtures/kanban-full.pipeline.specs.ts +337 -0
  254. package/src/testing/fixtures/kanban-full.pipeline.ts +225 -0
  255. package/src/testing/fixtures/kanban-todo.config.ts +19 -0
  256. package/src/testing/fixtures/kanban.pipeline.specs.ts +33 -0
  257. package/src/testing/fixtures/kanban.pipeline.ts +124 -0
  258. package/src/testing/kanban-todo.e2e.specs.ts +209 -0
  259. package/src/testing/mock-handlers.specs.ts +229 -0
  260. package/src/testing/mock-handlers.ts +58 -0
  261. package/src/testing/real-execution.e2e.specs.ts +193 -0
  262. package/src/testing/real-plugin.e2e.specs.ts +94 -0
  263. package/src/testing/server-startup.e2e.specs.ts +162 -0
  264. package/src/testing/snapshot-compare.specs.ts +136 -0
  265. package/src/testing/snapshot-compare.ts +106 -0
  266. package/src/testing/snapshot-sanitize.specs.ts +131 -0
  267. package/src/testing/snapshot-sanitize.ts +17 -0
  268. package/tsconfig.json +11 -0
  269. package/tsconfig.test.json +9 -0
  270. package/vitest.config.ts +29 -0
@@ -0,0 +1,224 @@
1
+ import type { Event } from '@auto-engineer/message-bus';
2
+ import type { ForEachPhasedDescriptor } from '../core/descriptors';
3
+
4
+ interface ItemTracker {
5
+ key: string;
6
+ phase: string;
7
+ dispatched: boolean;
8
+ completed: boolean;
9
+ }
10
+
11
+ interface PhasedSession {
12
+ correlationId: string;
13
+ handler: ForEachPhasedDescriptor;
14
+ triggerEvent: Event;
15
+ items: Map<string, ItemTracker>;
16
+ phases: readonly string[];
17
+ currentPhaseIndex: number;
18
+ pendingInPhase: Set<string>;
19
+ failedItems: Map<string, unknown>;
20
+ }
21
+
22
+ interface PhasedExecutorOptions {
23
+ onDispatch: (commandType: string, data: unknown, correlationId: string) => void;
24
+ onComplete: (event: Event, correlationId: string) => void;
25
+ }
26
+
27
+ export class PhasedExecutor {
28
+ private sessions = new Map<string, PhasedSession>();
29
+ private keyToSession = new Map<string, string>();
30
+ private readonly onDispatch: (commandType: string, data: unknown, correlationId: string) => void;
31
+ private readonly onComplete: (event: Event, correlationId: string) => void;
32
+
33
+ constructor(options: PhasedExecutorOptions) {
34
+ this.onDispatch = options.onDispatch;
35
+ this.onComplete = options.onComplete;
36
+ }
37
+
38
+ startPhased(handler: ForEachPhasedDescriptor, event: Event, correlationId: string): void {
39
+ const items = handler.itemsSelector(event);
40
+ const itemTrackers = new Map<string, ItemTracker>();
41
+
42
+ for (const item of items) {
43
+ const key = handler.completion.itemKey({ type: event.type, data: item as Record<string, unknown> });
44
+ const phase = handler.classifier(item);
45
+ itemTrackers.set(key, {
46
+ key,
47
+ phase,
48
+ dispatched: false,
49
+ completed: false,
50
+ });
51
+ }
52
+
53
+ const session: PhasedSession = {
54
+ correlationId,
55
+ handler,
56
+ triggerEvent: event,
57
+ items: itemTrackers,
58
+ phases: handler.phases,
59
+ currentPhaseIndex: 0,
60
+ pendingInPhase: new Set(),
61
+ failedItems: new Map(),
62
+ };
63
+
64
+ const sessionId = this.generateSessionId(correlationId, handler);
65
+ this.sessions.set(sessionId, session);
66
+
67
+ for (const key of itemTrackers.keys()) {
68
+ this.keyToSession.set(this.keyWithCorrelation(key, correlationId), sessionId);
69
+ }
70
+
71
+ this.dispatchCurrentPhase(sessionId);
72
+ }
73
+
74
+ onEventReceived(event: Event, itemKey: string): void {
75
+ const correlationId = event.correlationId;
76
+ if (correlationId === undefined || correlationId === '') return;
77
+
78
+ const lookupKey = this.keyWithCorrelation(itemKey, correlationId);
79
+ const sessionId = this.keyToSession.get(lookupKey);
80
+ if (sessionId === undefined) return;
81
+
82
+ const session = this.sessions.get(sessionId)!;
83
+ const tracker = session.items.get(itemKey);
84
+ if (tracker === undefined || tracker.completed) return;
85
+
86
+ tracker.completed = true;
87
+ session.pendingInPhase.delete(itemKey);
88
+
89
+ if (session.handler.stopOnFailure && this.isFailureEvent(event, session.handler)) {
90
+ session.failedItems.set(itemKey, event);
91
+ this.handleFailure(sessionId, session);
92
+ return;
93
+ }
94
+
95
+ if (session.pendingInPhase.size === 0) {
96
+ this.advanceToNextPhase(sessionId, session);
97
+ }
98
+ }
99
+
100
+ getActiveSessionCount(): number {
101
+ return this.sessions.size;
102
+ }
103
+
104
+ isPhaseComplete(correlationId: string, phase: string): boolean {
105
+ for (const session of this.sessions.values()) {
106
+ if (session.correlationId !== correlationId) continue;
107
+
108
+ const phaseIndex = session.phases.indexOf(phase);
109
+ if (phaseIndex === -1) continue;
110
+
111
+ if (session.currentPhaseIndex > phaseIndex) {
112
+ return true;
113
+ }
114
+
115
+ if (session.currentPhaseIndex === phaseIndex) {
116
+ return session.pendingInPhase.size === 0;
117
+ }
118
+
119
+ return false;
120
+ }
121
+ return false;
122
+ }
123
+
124
+ private dispatchCurrentPhase(sessionId: string): void {
125
+ const session = this.sessions.get(sessionId)!;
126
+
127
+ if (session.currentPhaseIndex >= session.phases.length) {
128
+ this.completeSession(sessionId, session, true);
129
+ return;
130
+ }
131
+
132
+ const currentPhase = session.phases[session.currentPhaseIndex];
133
+ const itemsToDispatch: ItemTracker[] = [];
134
+
135
+ for (const tracker of session.items.values()) {
136
+ if (tracker.phase === currentPhase && !tracker.dispatched) {
137
+ itemsToDispatch.push(tracker);
138
+ }
139
+ }
140
+
141
+ if (itemsToDispatch.length === 0) {
142
+ this.advanceToNextPhase(sessionId, session);
143
+ return;
144
+ }
145
+
146
+ for (const tracker of itemsToDispatch) {
147
+ tracker.dispatched = true;
148
+ session.pendingInPhase.add(tracker.key);
149
+
150
+ const item = this.findItemByKey(session, tracker.key);
151
+ if (item !== undefined) {
152
+ const command = session.handler.emitFactory(item, tracker.phase, session.triggerEvent);
153
+ this.onDispatch(command.commandType, command.data, session.correlationId);
154
+ }
155
+ }
156
+ }
157
+
158
+ private advanceToNextPhase(sessionId: string, session: PhasedSession): void {
159
+ session.currentPhaseIndex++;
160
+ this.dispatchCurrentPhase(sessionId);
161
+ }
162
+
163
+ private handleFailure(sessionId: string, session: PhasedSession): void {
164
+ this.completeSession(sessionId, session, false);
165
+ }
166
+
167
+ private completeSession(sessionId: string, session: PhasedSession, success: boolean): void {
168
+ const eventType = success ? session.handler.completion.successEvent : session.handler.completion.failureEvent;
169
+
170
+ const results = this.collectResults(session);
171
+ const eventData = success
172
+ ? { results, itemCount: session.items.size }
173
+ : { failures: Array.from(session.failedItems.entries()).map(([k, v]) => ({ key: k, error: v })) };
174
+
175
+ const completionEvent: Event = {
176
+ type: eventType,
177
+ correlationId: session.correlationId,
178
+ data: eventData,
179
+ };
180
+
181
+ this.onComplete(completionEvent, session.correlationId);
182
+ this.cleanupSession(sessionId, session);
183
+ }
184
+
185
+ private collectResults(session: PhasedSession): string[] {
186
+ const completed: string[] = [];
187
+ for (const tracker of session.items.values()) {
188
+ if (tracker.completed) {
189
+ completed.push(tracker.key);
190
+ }
191
+ }
192
+ return completed;
193
+ }
194
+
195
+ private cleanupSession(sessionId: string, session: PhasedSession): void {
196
+ for (const key of session.items.keys()) {
197
+ this.keyToSession.delete(this.keyWithCorrelation(key, session.correlationId));
198
+ }
199
+ this.sessions.delete(sessionId);
200
+ }
201
+
202
+ private findItemByKey(session: PhasedSession, key: string): unknown {
203
+ const items = session.handler.itemsSelector(session.triggerEvent);
204
+ return items.find((item) => {
205
+ const itemKey = session.handler.completion.itemKey({
206
+ type: session.triggerEvent.type,
207
+ data: item as Record<string, unknown>,
208
+ });
209
+ return itemKey === key;
210
+ });
211
+ }
212
+
213
+ private isFailureEvent(event: Event, handler: ForEachPhasedDescriptor): boolean {
214
+ return event.type === handler.completion.failureEvent;
215
+ }
216
+
217
+ private generateSessionId(correlationId: string, handler: ForEachPhasedDescriptor): string {
218
+ return `phased-${correlationId}-${handler.eventType}`;
219
+ }
220
+
221
+ private keyWithCorrelation(key: string, correlationId: string): string {
222
+ return `${correlationId}::${key}`;
223
+ }
224
+ }
@@ -0,0 +1,214 @@
1
+ import { define } from '../builder/define';
2
+ import { PipelineRuntime } from './pipeline-runtime';
3
+
4
+ describe('PipelineRuntime', () => {
5
+ it('should create PipelineRuntime', () => {
6
+ const pipeline = define('test').on('Start').emit('Cmd', {}).build();
7
+ const runtime = new PipelineRuntime(pipeline.descriptor);
8
+ expect(runtime.descriptor.name).toBe('test');
9
+ });
10
+
11
+ it('should index handlers by event type', () => {
12
+ const pipeline = define('test').on('EventA').emit('CmdA', {}).on('EventB').emit('CmdB', {}).build();
13
+ const runtime = new PipelineRuntime(pipeline.descriptor);
14
+ expect(runtime.getHandlersForEvent('EventA')).toHaveLength(1);
15
+ expect(runtime.getHandlersForEvent('EventB')).toHaveLength(1);
16
+ expect(runtime.getHandlersForEvent('NonExistent')).toHaveLength(0);
17
+ });
18
+
19
+ it('should return multiple handlers for same event', () => {
20
+ const pipeline = define('test').on('Start').emit('CmdA', {}).on('Start').emit('CmdB', {}).build();
21
+ const runtime = new PipelineRuntime(pipeline.descriptor);
22
+ expect(runtime.getHandlersForEvent('Start')).toHaveLength(2);
23
+ });
24
+
25
+ it('should filter by predicate', () => {
26
+ type MyEvent = { type: string; data: { ok: boolean } };
27
+ const pipeline = define('test')
28
+ .on('Event')
29
+ .when((e: MyEvent) => e.data.ok)
30
+ .emit('A', {})
31
+ .on('Event')
32
+ .emit('B', {})
33
+ .build();
34
+ const runtime = new PipelineRuntime(pipeline.descriptor);
35
+ const matchingFalse = runtime.getMatchingHandlers({ type: 'Event', data: { ok: false } });
36
+ const matchingTrue = runtime.getMatchingHandlers({ type: 'Event', data: { ok: true } });
37
+ expect(matchingFalse).toHaveLength(1);
38
+ expect(matchingTrue).toHaveLength(2);
39
+ });
40
+
41
+ it('should execute emit handler', async () => {
42
+ const sent: string[] = [];
43
+ const ctx = {
44
+ sendCommand: async (type: string) => {
45
+ sent.push(type);
46
+ },
47
+ emit: async () => {},
48
+ correlationId: 'test',
49
+ };
50
+ const pipeline = define('test').on('Start').emit('Process', {}).build();
51
+ const runtime = new PipelineRuntime(pipeline.descriptor);
52
+ await runtime.handleEvent({ type: 'Start', data: {} }, ctx);
53
+ expect(sent).toContain('Process');
54
+ });
55
+
56
+ it('should resolve data factory', async () => {
57
+ const sent: Array<{ type: string; data: unknown }> = [];
58
+ const ctx = {
59
+ sendCommand: async (type: string, data: unknown) => {
60
+ sent.push({ type, data });
61
+ },
62
+ emit: async () => {},
63
+ correlationId: 'test',
64
+ };
65
+ type InEvent = { type: string; data: { id: string } };
66
+ const pipeline = define('test')
67
+ .on('In')
68
+ .emit('Out', (e: InEvent) => ({ x: e.data.id }))
69
+ .build();
70
+ const runtime = new PipelineRuntime(pipeline.descriptor);
71
+ await runtime.handleEvent({ type: 'In', data: { id: '1' } }, ctx);
72
+ expect(sent[0].data).toEqual({ x: '1' });
73
+ });
74
+
75
+ it('should execute custom handler', async () => {
76
+ let called = false;
77
+ const pipeline = define('test')
78
+ .on('E')
79
+ .handle(async () => {
80
+ called = true;
81
+ })
82
+ .build();
83
+ const runtime = new PipelineRuntime(pipeline.descriptor);
84
+ await runtime.handleEvent(
85
+ { type: 'E', data: {} },
86
+ { emit: async () => {}, sendCommand: async () => {}, correlationId: '' },
87
+ );
88
+ expect(called).toBe(true);
89
+ });
90
+
91
+ it('should dispatch run-await commands with static array', async () => {
92
+ const sent: string[] = [];
93
+ const ctx = {
94
+ sendCommand: async (type: string) => {
95
+ sent.push(type);
96
+ },
97
+ emit: async () => {},
98
+ correlationId: 'test',
99
+ };
100
+ const pipeline = define('test')
101
+ .on('Start')
102
+ .run([
103
+ { commandType: 'A', data: {} },
104
+ { commandType: 'B', data: {} },
105
+ ])
106
+ .awaitAll('key', () => '')
107
+ .build();
108
+ const runtime = new PipelineRuntime(pipeline.descriptor);
109
+ await runtime.handleEvent({ type: 'Start', data: {} }, ctx);
110
+ expect(sent).toEqual(['A', 'B']);
111
+ });
112
+
113
+ it('should dispatch run-await commands with factory', async () => {
114
+ const sent: Array<{ type: string; data: unknown }> = [];
115
+ const ctx = {
116
+ sendCommand: async (type: string, data: unknown) => {
117
+ sent.push({ type, data });
118
+ },
119
+ emit: async () => {},
120
+ correlationId: 'test',
121
+ };
122
+ type ItemsEvent = { type: string; data: { items: Array<{ id: string }> } };
123
+ const pipeline = define('test')
124
+ .on('Items')
125
+ .run((e: ItemsEvent) => e.data.items.map((item) => ({ commandType: 'Process', data: { itemId: item.id } })))
126
+ .awaitAll('byItem', () => '')
127
+ .build();
128
+ const runtime = new PipelineRuntime(pipeline.descriptor);
129
+ await runtime.handleEvent({ type: 'Items', data: { items: [{ id: '1' }, { id: '2' }] } }, ctx);
130
+ expect(sent).toHaveLength(2);
131
+ expect(sent[0].data).toEqual({ itemId: '1' });
132
+ expect(sent[1].data).toEqual({ itemId: '2' });
133
+ });
134
+
135
+ it('should dispatch run-await commands with data factory per command', async () => {
136
+ const sent: Array<{ type: string; data: unknown }> = [];
137
+ const ctx = {
138
+ sendCommand: async (type: string, data: unknown) => {
139
+ sent.push({ type, data });
140
+ },
141
+ emit: async () => {},
142
+ correlationId: 'test',
143
+ };
144
+ type MyEvent = { type: string; data: { x: number } };
145
+ const pipeline = define('test')
146
+ .on('Start')
147
+ .run((e: MyEvent) => [{ commandType: 'Cmd', data: { val: e.data.x * 2 } }])
148
+ .awaitAll('key', () => '')
149
+ .build();
150
+ const runtime = new PipelineRuntime(pipeline.descriptor);
151
+ await runtime.handleEvent({ type: 'Start', data: { x: 5 } }, ctx);
152
+ expect(sent[0].data).toEqual({ val: 10 });
153
+ });
154
+
155
+ it('should resolve data factory in static run-await commands', async () => {
156
+ const sent: Array<{ type: string; data: unknown }> = [];
157
+ const ctx = {
158
+ sendCommand: async (type: string, data: unknown) => {
159
+ sent.push({ type, data });
160
+ },
161
+ emit: async () => {},
162
+ correlationId: 'test',
163
+ };
164
+ const dataFactory = (e: { data: { v?: number } }) => ({ result: (e.data.v ?? 0) * 3 });
165
+ const runtime = new PipelineRuntime({
166
+ name: 'test',
167
+ keys: new Map(),
168
+ handlers: [
169
+ {
170
+ type: 'run-await',
171
+ eventType: 'Start',
172
+ commands: [{ commandType: 'Cmd', data: dataFactory }],
173
+ awaitConfig: { keyName: 'k', key: () => '' },
174
+ },
175
+ ],
176
+ });
177
+ await runtime.handleEvent({ type: 'Start', data: { v: 7 } }, ctx);
178
+ expect(sent[0].data).toEqual({ result: 21 });
179
+ });
180
+
181
+ it('should process items in phase order for foreach-phased', async () => {
182
+ const sent: string[] = [];
183
+ const ctx = {
184
+ sendCommand: async (_: string, data: unknown) => {
185
+ sent.push((data as { id: string }).id);
186
+ },
187
+ emit: async () => {},
188
+ correlationId: 'test',
189
+ };
190
+ type Item = { id: string; p: 'high' | 'low' };
191
+ const pipeline = define('test')
192
+ .on('Items')
193
+ .forEach((e: { data: { items: Item[] } }) => e.data.items)
194
+ .groupInto(['high', 'low'], (i: Item) => i.p)
195
+ .process('Cmd', (i: Item) => ({ id: i.id }))
196
+ .onComplete({ success: 'Done', failure: 'Fail', itemKey: () => '' })
197
+ .build();
198
+ const runtime = new PipelineRuntime(pipeline.descriptor);
199
+ await runtime.handleEvent(
200
+ {
201
+ type: 'Items',
202
+ data: {
203
+ items: [
204
+ { id: '1', p: 'low' },
205
+ { id: '2', p: 'high' },
206
+ { id: '3', p: 'high' },
207
+ ],
208
+ },
209
+ },
210
+ ctx,
211
+ );
212
+ expect(sent).toEqual(['2', '3', '1']);
213
+ });
214
+ });
@@ -0,0 +1,119 @@
1
+ import type { Event } from '@auto-engineer/message-bus';
2
+ import type {
3
+ CustomHandlerDescriptor,
4
+ EmitHandlerDescriptor,
5
+ EventHandlerDescriptor,
6
+ ForEachPhasedDescriptor,
7
+ PipelineDescriptor,
8
+ RunAwaitHandlerDescriptor,
9
+ } from '../core/descriptors';
10
+ import type { PipelineContext } from './context';
11
+
12
+ export class PipelineRuntime {
13
+ private readonly handlerIndex: Map<string, EventHandlerDescriptor[]>;
14
+
15
+ constructor(public readonly descriptor: PipelineDescriptor) {
16
+ this.handlerIndex = this.buildHandlerIndex();
17
+ }
18
+
19
+ getHandlersForEvent(eventType: string): EventHandlerDescriptor[] {
20
+ return this.handlerIndex.get(eventType) ?? [];
21
+ }
22
+
23
+ getMatchingHandlers(event: Event): EventHandlerDescriptor[] {
24
+ const handlers = this.getHandlersForEvent(event.type);
25
+ return handlers.filter((handler) => {
26
+ if (!handler.predicate) return true;
27
+ return handler.predicate(event);
28
+ });
29
+ }
30
+
31
+ async handleEvent(event: Event, ctx: PipelineContext): Promise<void> {
32
+ const handlers = this.getMatchingHandlers(event);
33
+ for (const handler of handlers) {
34
+ switch (handler.type) {
35
+ case 'emit':
36
+ await this.executeEmitHandler(handler, event, ctx);
37
+ break;
38
+ case 'custom':
39
+ await this.executeCustomHandler(handler, event, ctx);
40
+ break;
41
+ case 'run-await':
42
+ await this.executeRunAwaitHandler(handler, event, ctx);
43
+ break;
44
+ case 'foreach-phased':
45
+ if (ctx.startPhased !== undefined) {
46
+ ctx.startPhased(handler, event);
47
+ } else {
48
+ await this.executeForEachPhasedHandler(handler, event, ctx);
49
+ }
50
+ break;
51
+ }
52
+ }
53
+ }
54
+
55
+ private async executeEmitHandler(handler: EmitHandlerDescriptor, event: Event, ctx: PipelineContext): Promise<void> {
56
+ for (const command of handler.commands) {
57
+ const data = typeof command.data === 'function' ? command.data(event) : command.data;
58
+ await ctx.sendCommand(command.commandType, data);
59
+ }
60
+ }
61
+
62
+ private async executeCustomHandler(
63
+ handler: CustomHandlerDescriptor,
64
+ event: Event,
65
+ ctx: PipelineContext,
66
+ ): Promise<void> {
67
+ await handler.handler(event, ctx);
68
+ }
69
+
70
+ private async executeRunAwaitHandler(
71
+ handler: RunAwaitHandlerDescriptor,
72
+ event: Event,
73
+ ctx: PipelineContext,
74
+ ): Promise<void> {
75
+ const commands = typeof handler.commands === 'function' ? handler.commands(event) : handler.commands;
76
+ for (const command of commands) {
77
+ const data = typeof command.data === 'function' ? command.data(event) : command.data;
78
+ await ctx.sendCommand(command.commandType, data);
79
+ }
80
+ }
81
+
82
+ private async executeForEachPhasedHandler(
83
+ handler: ForEachPhasedDescriptor,
84
+ event: Event,
85
+ ctx: PipelineContext,
86
+ ): Promise<void> {
87
+ const items = handler.itemsSelector(event);
88
+ const phaseGroups: Record<string, unknown[]> = {};
89
+
90
+ for (const phase of handler.phases) {
91
+ phaseGroups[phase] = [];
92
+ }
93
+
94
+ for (const item of items) {
95
+ const phase = handler.classifier(item);
96
+ phaseGroups[phase]?.push(item);
97
+ }
98
+
99
+ for (const phase of handler.phases) {
100
+ for (const item of phaseGroups[phase]) {
101
+ const command = handler.emitFactory(item, phase, event);
102
+ await ctx.sendCommand(command.commandType, command.data);
103
+ }
104
+ }
105
+ }
106
+
107
+ private buildHandlerIndex(): Map<string, EventHandlerDescriptor[]> {
108
+ const index = new Map<string, EventHandlerDescriptor[]>();
109
+ for (const handler of this.descriptor.handlers) {
110
+ if (handler.type === 'settled') {
111
+ continue;
112
+ }
113
+ const existing = index.get(handler.eventType) ?? [];
114
+ existing.push(handler);
115
+ index.set(handler.eventType, existing);
116
+ }
117
+ return index;
118
+ }
119
+ }