@auto-engineer/pipeline 0.0.1 → 0.15.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 (201) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +26 -0
  3. package/README.md +279 -0
  4. package/dist/src/builder/define.d.ts +6 -2
  5. package/dist/src/builder/define.d.ts.map +1 -1
  6. package/dist/src/builder/define.js +17 -7
  7. package/dist/src/builder/define.js.map +1 -1
  8. package/dist/src/builder/define.specs.js +3 -3
  9. package/dist/src/builder/define.specs.js.map +1 -1
  10. package/dist/src/core/descriptors.d.ts +6 -2
  11. package/dist/src/core/descriptors.d.ts.map +1 -1
  12. package/dist/src/graph/filter-graph.d.ts +3 -0
  13. package/dist/src/graph/filter-graph.d.ts.map +1 -0
  14. package/dist/src/graph/filter-graph.js +80 -0
  15. package/dist/src/graph/filter-graph.js.map +1 -0
  16. package/dist/src/graph/filter-graph.specs.d.ts +2 -0
  17. package/dist/src/graph/filter-graph.specs.d.ts.map +1 -0
  18. package/dist/src/graph/filter-graph.specs.js +204 -0
  19. package/dist/src/graph/filter-graph.specs.js.map +1 -0
  20. package/dist/src/graph/types.d.ts +8 -0
  21. package/dist/src/graph/types.d.ts.map +1 -1
  22. package/dist/src/index.d.ts +1 -0
  23. package/dist/src/index.d.ts.map +1 -1
  24. package/dist/src/index.js.map +1 -1
  25. package/dist/src/projections/await-tracker-projection.d.ts +31 -0
  26. package/dist/src/projections/await-tracker-projection.d.ts.map +1 -0
  27. package/dist/src/projections/await-tracker-projection.js +35 -0
  28. package/dist/src/projections/await-tracker-projection.js.map +1 -0
  29. package/dist/src/projections/index.d.ts +4 -0
  30. package/dist/src/projections/index.d.ts.map +1 -0
  31. package/dist/src/projections/index.js +4 -0
  32. package/dist/src/projections/index.js.map +1 -0
  33. package/dist/src/projections/item-status-projection.d.ts +22 -0
  34. package/dist/src/projections/item-status-projection.d.ts.map +1 -0
  35. package/dist/src/projections/item-status-projection.js +11 -0
  36. package/dist/src/projections/item-status-projection.js.map +1 -0
  37. package/dist/src/projections/item-status-projection.specs.d.ts +2 -0
  38. package/dist/src/projections/item-status-projection.specs.d.ts.map +1 -0
  39. package/dist/src/projections/item-status-projection.specs.js +119 -0
  40. package/dist/src/projections/item-status-projection.specs.js.map +1 -0
  41. package/dist/src/projections/latest-run-projection.d.ts +15 -0
  42. package/dist/src/projections/latest-run-projection.d.ts.map +1 -0
  43. package/dist/src/projections/latest-run-projection.js +7 -0
  44. package/dist/src/projections/latest-run-projection.js.map +1 -0
  45. package/dist/src/projections/latest-run-projection.specs.d.ts +2 -0
  46. package/dist/src/projections/latest-run-projection.specs.d.ts.map +1 -0
  47. package/dist/src/projections/latest-run-projection.specs.js +33 -0
  48. package/dist/src/projections/latest-run-projection.specs.js.map +1 -0
  49. package/dist/src/projections/message-log-projection.d.ts +51 -0
  50. package/dist/src/projections/message-log-projection.d.ts.map +1 -0
  51. package/dist/src/projections/message-log-projection.js +51 -0
  52. package/dist/src/projections/message-log-projection.js.map +1 -0
  53. package/dist/src/projections/message-log-projection.specs.d.ts +2 -0
  54. package/dist/src/projections/message-log-projection.specs.d.ts.map +1 -0
  55. package/dist/src/projections/message-log-projection.specs.js +101 -0
  56. package/dist/src/projections/message-log-projection.specs.js.map +1 -0
  57. package/dist/src/projections/node-status-projection.d.ts +23 -0
  58. package/dist/src/projections/node-status-projection.d.ts.map +1 -0
  59. package/dist/src/projections/node-status-projection.js +10 -0
  60. package/dist/src/projections/node-status-projection.js.map +1 -0
  61. package/dist/src/projections/node-status-projection.specs.d.ts +2 -0
  62. package/dist/src/projections/node-status-projection.specs.d.ts.map +1 -0
  63. package/dist/src/projections/node-status-projection.specs.js +116 -0
  64. package/dist/src/projections/node-status-projection.specs.js.map +1 -0
  65. package/dist/src/projections/phased-execution-projection.d.ts +77 -0
  66. package/dist/src/projections/phased-execution-projection.d.ts.map +1 -0
  67. package/dist/src/projections/phased-execution-projection.js +54 -0
  68. package/dist/src/projections/phased-execution-projection.js.map +1 -0
  69. package/dist/src/projections/phased-execution-projection.specs.d.ts +2 -0
  70. package/dist/src/projections/phased-execution-projection.specs.d.ts.map +1 -0
  71. package/dist/src/projections/phased-execution-projection.specs.js +171 -0
  72. package/dist/src/projections/phased-execution-projection.specs.js.map +1 -0
  73. package/dist/src/projections/settled-instance-projection.d.ts +67 -0
  74. package/dist/src/projections/settled-instance-projection.d.ts.map +1 -0
  75. package/dist/src/projections/settled-instance-projection.js +66 -0
  76. package/dist/src/projections/settled-instance-projection.js.map +1 -0
  77. package/dist/src/projections/settled-instance-projection.specs.d.ts +2 -0
  78. package/dist/src/projections/settled-instance-projection.specs.d.ts.map +1 -0
  79. package/dist/src/projections/settled-instance-projection.specs.js +217 -0
  80. package/dist/src/projections/settled-instance-projection.specs.js.map +1 -0
  81. package/dist/src/projections/stats-projection.d.ts +9 -0
  82. package/dist/src/projections/stats-projection.d.ts.map +1 -0
  83. package/dist/src/projections/stats-projection.js +16 -0
  84. package/dist/src/projections/stats-projection.js.map +1 -0
  85. package/dist/src/projections/stats-projection.specs.d.ts +2 -0
  86. package/dist/src/projections/stats-projection.specs.d.ts.map +1 -0
  87. package/dist/src/projections/stats-projection.specs.js +91 -0
  88. package/dist/src/projections/stats-projection.specs.js.map +1 -0
  89. package/dist/src/runtime/await-tracker.d.ts +17 -7
  90. package/dist/src/runtime/await-tracker.d.ts.map +1 -1
  91. package/dist/src/runtime/await-tracker.js +32 -29
  92. package/dist/src/runtime/await-tracker.js.map +1 -1
  93. package/dist/src/runtime/await-tracker.specs.js +56 -38
  94. package/dist/src/runtime/await-tracker.specs.js.map +1 -1
  95. package/dist/src/runtime/context.d.ts +1 -1
  96. package/dist/src/runtime/context.d.ts.map +1 -1
  97. package/dist/src/runtime/event-command-map.d.ts +3 -3
  98. package/dist/src/runtime/event-command-map.d.ts.map +1 -1
  99. package/dist/src/runtime/event-command-map.js +6 -2
  100. package/dist/src/runtime/event-command-map.js.map +1 -1
  101. package/dist/src/runtime/phased-executor.d.ts +15 -9
  102. package/dist/src/runtime/phased-executor.d.ts.map +1 -1
  103. package/dist/src/runtime/phased-executor.js +126 -104
  104. package/dist/src/runtime/phased-executor.js.map +1 -1
  105. package/dist/src/runtime/phased-executor.specs.js +243 -81
  106. package/dist/src/runtime/phased-executor.specs.js.map +1 -1
  107. package/dist/src/runtime/pipeline-runtime.d.ts.map +1 -1
  108. package/dist/src/runtime/pipeline-runtime.js +2 -2
  109. package/dist/src/runtime/pipeline-runtime.js.map +1 -1
  110. package/dist/src/runtime/pipeline-runtime.specs.js +35 -0
  111. package/dist/src/runtime/pipeline-runtime.specs.js.map +1 -1
  112. package/dist/src/runtime/settled-tracker.d.ts +12 -9
  113. package/dist/src/runtime/settled-tracker.d.ts.map +1 -1
  114. package/dist/src/runtime/settled-tracker.js +92 -77
  115. package/dist/src/runtime/settled-tracker.js.map +1 -1
  116. package/dist/src/runtime/settled-tracker.specs.js +568 -118
  117. package/dist/src/runtime/settled-tracker.specs.js.map +1 -1
  118. package/dist/src/server/pipeline-server.d.ts +31 -9
  119. package/dist/src/server/pipeline-server.d.ts.map +1 -1
  120. package/dist/src/server/pipeline-server.e2e.specs.js +2 -10
  121. package/dist/src/server/pipeline-server.e2e.specs.js.map +1 -1
  122. package/dist/src/server/pipeline-server.js +418 -134
  123. package/dist/src/server/pipeline-server.js.map +1 -1
  124. package/dist/src/server/pipeline-server.specs.js +777 -32
  125. package/dist/src/server/pipeline-server.specs.js.map +1 -1
  126. package/dist/src/server/sse-manager.specs.js +55 -35
  127. package/dist/src/server/sse-manager.specs.js.map +1 -1
  128. package/dist/src/store/index.d.ts +3 -0
  129. package/dist/src/store/index.d.ts.map +1 -0
  130. package/dist/src/store/index.js +3 -0
  131. package/dist/src/store/index.js.map +1 -0
  132. package/dist/src/store/pipeline-event-store.d.ts +10 -0
  133. package/dist/src/store/pipeline-event-store.d.ts.map +1 -0
  134. package/dist/src/store/pipeline-event-store.js +112 -0
  135. package/dist/src/store/pipeline-event-store.js.map +1 -0
  136. package/dist/src/store/pipeline-event-store.specs.d.ts +2 -0
  137. package/dist/src/store/pipeline-event-store.specs.d.ts.map +1 -0
  138. package/dist/src/store/pipeline-event-store.specs.js +287 -0
  139. package/dist/src/store/pipeline-event-store.specs.js.map +1 -0
  140. package/dist/src/store/pipeline-read-model.d.ts +49 -0
  141. package/dist/src/store/pipeline-read-model.d.ts.map +1 -0
  142. package/dist/src/store/pipeline-read-model.js +157 -0
  143. package/dist/src/store/pipeline-read-model.js.map +1 -0
  144. package/dist/src/store/pipeline-read-model.specs.d.ts +2 -0
  145. package/dist/src/store/pipeline-read-model.specs.d.ts.map +1 -0
  146. package/dist/src/store/pipeline-read-model.specs.js +830 -0
  147. package/dist/src/store/pipeline-read-model.specs.js.map +1 -0
  148. package/dist/src/testing/fixtures/kanban-full.pipeline.js +2 -2
  149. package/dist/src/testing/fixtures/kanban-full.pipeline.js.map +1 -1
  150. package/dist/src/testing/fixtures/kanban.pipeline.js +2 -2
  151. package/dist/src/testing/fixtures/kanban.pipeline.js.map +1 -1
  152. package/dist/tsconfig.tsbuildinfo +1 -1
  153. package/ketchup-plan.md +960 -0
  154. package/package.json +7 -3
  155. package/src/builder/define.specs.ts +3 -3
  156. package/src/builder/define.ts +24 -11
  157. package/src/core/descriptors.ts +7 -2
  158. package/src/graph/filter-graph.specs.ts +241 -0
  159. package/src/graph/filter-graph.ts +111 -0
  160. package/src/graph/types.ts +10 -0
  161. package/src/index.ts +1 -2
  162. package/src/projections/await-tracker-projection.ts +68 -0
  163. package/src/projections/index.ts +11 -0
  164. package/src/projections/item-status-projection.specs.ts +130 -0
  165. package/src/projections/item-status-projection.ts +32 -0
  166. package/src/projections/latest-run-projection.specs.ts +38 -0
  167. package/src/projections/latest-run-projection.ts +20 -0
  168. package/src/projections/message-log-projection.specs.ts +118 -0
  169. package/src/projections/message-log-projection.ts +113 -0
  170. package/src/projections/node-status-projection.specs.ts +127 -0
  171. package/src/projections/node-status-projection.ts +33 -0
  172. package/src/projections/phased-execution-projection.specs.ts +202 -0
  173. package/src/projections/phased-execution-projection.ts +146 -0
  174. package/src/projections/settled-instance-projection.specs.ts +249 -0
  175. package/src/projections/settled-instance-projection.ts +160 -0
  176. package/src/projections/stats-projection.specs.ts +105 -0
  177. package/src/projections/stats-projection.ts +26 -0
  178. package/src/runtime/await-tracker.specs.ts +57 -34
  179. package/src/runtime/await-tracker.ts +43 -31
  180. package/src/runtime/context.ts +1 -1
  181. package/src/runtime/event-command-map.ts +11 -4
  182. package/src/runtime/phased-executor.specs.ts +357 -81
  183. package/src/runtime/phased-executor.ts +142 -126
  184. package/src/runtime/pipeline-runtime.specs.ts +42 -0
  185. package/src/runtime/pipeline-runtime.ts +6 -4
  186. package/src/runtime/settled-tracker.specs.ts +716 -120
  187. package/src/runtime/settled-tracker.ts +104 -98
  188. package/src/server/pipeline-server.e2e.specs.ts +10 -16
  189. package/src/server/pipeline-server.specs.ts +964 -49
  190. package/src/server/pipeline-server.ts +522 -156
  191. package/src/server/sse-manager.specs.ts +67 -36
  192. package/src/store/index.ts +2 -0
  193. package/src/store/pipeline-event-store.specs.ts +309 -0
  194. package/src/store/pipeline-event-store.ts +156 -0
  195. package/src/store/pipeline-read-model.specs.ts +967 -0
  196. package/src/store/pipeline-read-model.ts +223 -0
  197. package/src/testing/fixtures/kanban-full.pipeline.ts +2 -2
  198. package/src/testing/fixtures/kanban.pipeline.ts +2 -2
  199. package/claude.md +0 -160
  200. package/docs/testing-analysis.md +0 -395
  201. package/pomodoro-plan.md +0 -651
package/package.json CHANGED
@@ -4,13 +4,17 @@
4
4
  "main": "./dist/src/index.js",
5
5
  "types": "./dist/src/index.d.ts",
6
6
  "dependencies": {
7
+ "@event-driven-io/emmett": "^0.38.2",
8
+ "@event-driven-io/emmett-sqlite": "^0.38.5",
9
+ "chokidar": "^3.6.0",
7
10
  "cors": "^2.8.5",
8
11
  "dotenv": "^16.4.5",
9
12
  "express": "^4.18.0",
10
13
  "get-port": "^7.1.0",
14
+ "jose": "^5.9.6",
11
15
  "nanoid": "^5.0.0",
12
- "@auto-engineer/message-store": "0.13.3",
13
- "@auto-engineer/message-bus": "0.13.3"
16
+ "@auto-engineer/file-store": "0.15.0",
17
+ "@auto-engineer/message-bus": "0.15.0"
14
18
  },
15
19
  "devDependencies": {
16
20
  "@types/cors": "^2.8.17",
@@ -19,7 +23,7 @@
19
23
  "publishConfig": {
20
24
  "access": "public"
21
25
  },
22
- "version": "0.0.1",
26
+ "version": "0.15.0",
23
27
  "scripts": {
24
28
  "build": "tsc && tsx ../../scripts/fix-esm-imports.ts",
25
29
  "test": "vitest run --reporter=dot",
@@ -148,7 +148,7 @@ describe('settled()', () => {
148
148
  const graph = pipeline.toGraph();
149
149
  const settledNode = graph.nodes.find((n) => n.id.startsWith('settled:'));
150
150
  expect(settledNode).toBeDefined();
151
- expect(settledNode?.label).toBe('settled(CheckTests, CheckTypes)');
151
+ expect(settledNode?.label).toBe('Settled');
152
152
  });
153
153
 
154
154
  it('should accept options-first dispatch with dispatches array', () => {
@@ -426,8 +426,8 @@ describe('forEach() and groupInto() - Phased Execution', () => {
426
426
  .build();
427
427
 
428
428
  const handler = pipeline.descriptor.handlers[0] as ForEachPhasedDescriptor;
429
- expect(handler.completion.successEvent).toBe('AllItemsProcessed');
430
- expect(handler.completion.failureEvent).toBe('ProcessingFailed');
429
+ expect(handler.completion.successEvent.name).toBe('AllItemsProcessed');
430
+ expect(handler.completion.failureEvent.name).toBe('ProcessingFailed');
431
431
  });
432
432
 
433
433
  it('should chain on() from PhasedTerminal', () => {
@@ -73,9 +73,11 @@ export interface PhasedBuilder<T> {
73
73
  process(commandType: string, dataFactory: (item: T) => Record<string, unknown>): PhasedChain<T>;
74
74
  }
75
75
 
76
+ export type CompletionEventConfig = string | { name: string; displayName: string };
77
+
76
78
  export interface CompletionConfig {
77
- success: string;
78
- failure: string;
79
+ success: CompletionEventConfig;
80
+ failure: CompletionEventConfig;
79
81
  itemKey: (event: Event) => string;
80
82
  }
81
83
 
@@ -132,6 +134,13 @@ export interface HandleChain {
132
134
  build(): Pipeline;
133
135
  }
134
136
 
137
+ function normalizeCompletionEvent(config: CompletionEventConfig): { name: string; displayName?: string } {
138
+ if (typeof config === 'string') {
139
+ return { name: config };
140
+ }
141
+ return config;
142
+ }
143
+
135
144
  class PipelineBuilderImpl implements PipelineBuilder {
136
145
  private readonly name: string;
137
146
  private versionValue?: string;
@@ -225,10 +234,14 @@ function processForEachPhasedHandler(ctx: GraphBuilderContext, handler: ForEachP
225
234
  const sampleCmd = handler.emitFactory({}, '', { type: '', data: {} });
226
235
  addNode(ctx, `cmd:${sampleCmd.commandType}`, 'command', sampleCmd.commandType);
227
236
  ctx.edges.push({ from: `evt:${handler.eventType}`, to: `cmd:${sampleCmd.commandType}` });
228
- addNode(ctx, `evt:${handler.completion.successEvent}`, 'event', handler.completion.successEvent);
229
- addNode(ctx, `evt:${handler.completion.failureEvent}`, 'event', handler.completion.failureEvent);
230
- ctx.edges.push({ from: `cmd:${sampleCmd.commandType}`, to: `evt:${handler.completion.successEvent}` });
231
- ctx.edges.push({ from: `cmd:${sampleCmd.commandType}`, to: `evt:${handler.completion.failureEvent}` });
237
+ const successEvent = handler.completion.successEvent;
238
+ const failureEvent = handler.completion.failureEvent;
239
+ const successLabel = successEvent.displayName ?? successEvent.name;
240
+ const failureLabel = failureEvent.displayName ?? failureEvent.name;
241
+ addNode(ctx, `evt:${successEvent.name}`, 'event', successLabel);
242
+ addNode(ctx, `evt:${failureEvent.name}`, 'event', failureLabel);
243
+ ctx.edges.push({ from: `cmd:${sampleCmd.commandType}`, to: `evt:${successEvent.name}` });
244
+ ctx.edges.push({ from: `cmd:${sampleCmd.commandType}`, to: `evt:${failureEvent.name}` });
232
245
  }
233
246
 
234
247
  function processCustomHandler(ctx: GraphBuilderContext, handler: CustomHandlerDescriptor): void {
@@ -243,7 +256,7 @@ function processCustomHandler(ctx: GraphBuilderContext, handler: CustomHandlerDe
243
256
 
244
257
  function processSettledHandler(ctx: GraphBuilderContext, handler: SettledHandlerDescriptor): void {
245
258
  const settledNodeId = `settled:${handler.commandTypes.join(',')}`;
246
- addNode(ctx, settledNodeId, 'settled', `settled(${handler.commandTypes.join(', ')})`);
259
+ addNode(ctx, settledNodeId, 'settled', 'Settled');
247
260
 
248
261
  for (const commandType of handler.commandTypes) {
249
262
  addNode(ctx, `cmd:${commandType}`, 'command', commandType);
@@ -571,8 +584,8 @@ class PhasedBuilderImpl<T> implements PhasedBuilder<T> {
571
584
  class PhasedChainImpl<T> implements PhasedChain<T> {
572
585
  private stopOnFailureFlag = false;
573
586
  private completionConfig?: {
574
- successEvent: string;
575
- failureEvent: string;
587
+ successEvent: { name: string; displayName?: string };
588
+ failureEvent: { name: string; displayName?: string };
576
589
  itemKey: KeyExtractor;
577
590
  };
578
591
 
@@ -594,8 +607,8 @@ class PhasedChainImpl<T> implements PhasedChain<T> {
594
607
 
595
608
  onComplete(config: CompletionConfig): PhasedTerminal {
596
609
  this.completionConfig = {
597
- successEvent: config.success,
598
- failureEvent: config.failure,
610
+ successEvent: normalizeCompletionEvent(config.success),
611
+ failureEvent: normalizeCompletionEvent(config.failure),
599
612
  itemKey: config.itemKey as KeyExtractor,
600
613
  };
601
614
  return new PhasedTerminalImpl(this, this.parent);
@@ -44,6 +44,11 @@ export interface RunAwaitHandlerDescriptor {
44
44
  onFailure?: GatherEventConfig<FailureContext>;
45
45
  }
46
46
 
47
+ export type CompletionEventDescriptor = {
48
+ name: string;
49
+ displayName?: string;
50
+ };
51
+
47
52
  export interface ForEachPhasedDescriptor {
48
53
  type: 'foreach-phased';
49
54
  eventType: string;
@@ -54,8 +59,8 @@ export interface ForEachPhasedDescriptor {
54
59
  stopOnFailure: boolean;
55
60
  emitFactory: (item: unknown, phase: string, event: Event) => CommandDispatch;
56
61
  completion: {
57
- successEvent: string;
58
- failureEvent: string;
62
+ successEvent: CompletionEventDescriptor;
63
+ failureEvent: CompletionEventDescriptor;
59
64
  itemKey: KeyExtractor;
60
65
  };
61
66
  }
@@ -0,0 +1,241 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { filterGraph } from './filter-graph';
4
+ import type { GraphIR } from './types';
5
+
6
+ describe('filterGraph', () => {
7
+ describe('P1: filter nodes by type', () => {
8
+ it('should remove nodes of excluded type', () => {
9
+ const graph: GraphIR = {
10
+ nodes: [
11
+ { id: 'evt:Start', type: 'event', label: 'Start' },
12
+ { id: 'cmd:Process', type: 'command', label: 'Process' },
13
+ ],
14
+ edges: [{ from: 'evt:Start', to: 'cmd:Process' }],
15
+ };
16
+
17
+ const result = filterGraph(graph, { excludeTypes: ['event'], maintainEdges: false });
18
+
19
+ expect(result).toEqual({
20
+ nodes: [{ id: 'cmd:Process', type: 'command', label: 'Process' }],
21
+ edges: [],
22
+ });
23
+ });
24
+ });
25
+
26
+ describe('P2: dangling edge removal', () => {
27
+ it('should remove edges referencing filtered nodes in a chain', () => {
28
+ const graph: GraphIR = {
29
+ nodes: [
30
+ { id: 'evt:A', type: 'event', label: 'A' },
31
+ { id: 'cmd:B', type: 'command', label: 'B' },
32
+ { id: 'evt:C', type: 'event', label: 'C' },
33
+ ],
34
+ edges: [
35
+ { from: 'evt:A', to: 'cmd:B' },
36
+ { from: 'cmd:B', to: 'evt:C' },
37
+ ],
38
+ };
39
+
40
+ const result = filterGraph(graph, { excludeTypes: ['event'], maintainEdges: false });
41
+
42
+ expect(result).toEqual({
43
+ nodes: [{ id: 'cmd:B', type: 'command', label: 'B' }],
44
+ edges: [],
45
+ });
46
+ });
47
+ });
48
+
49
+ describe('P3: single-hop edge maintenance', () => {
50
+ it('should reconnect edges through single filtered node', () => {
51
+ const graph: GraphIR = {
52
+ nodes: [
53
+ { id: 'evt:A', type: 'event', label: 'A' },
54
+ { id: 'cmd:B', type: 'command', label: 'B' },
55
+ { id: 'evt:C', type: 'event', label: 'C' },
56
+ ],
57
+ edges: [
58
+ { from: 'evt:A', to: 'cmd:B' },
59
+ { from: 'cmd:B', to: 'evt:C' },
60
+ ],
61
+ };
62
+
63
+ const result = filterGraph(graph, { excludeTypes: ['command'], maintainEdges: true });
64
+
65
+ expect(result).toEqual({
66
+ nodes: [
67
+ { id: 'evt:A', type: 'event', label: 'A' },
68
+ { id: 'evt:C', type: 'event', label: 'C' },
69
+ ],
70
+ edges: [{ from: 'evt:A', to: 'evt:C' }],
71
+ });
72
+ });
73
+ });
74
+
75
+ describe('P4: multi-hop edge maintenance', () => {
76
+ it('should reconnect edges through multiple consecutive filtered nodes', () => {
77
+ const graph: GraphIR = {
78
+ nodes: [
79
+ { id: 'evt:A', type: 'event', label: 'A' },
80
+ { id: 'cmd:B1', type: 'command', label: 'B1' },
81
+ { id: 'cmd:B2', type: 'command', label: 'B2' },
82
+ { id: 'evt:C', type: 'event', label: 'C' },
83
+ ],
84
+ edges: [
85
+ { from: 'evt:A', to: 'cmd:B1' },
86
+ { from: 'cmd:B1', to: 'cmd:B2' },
87
+ { from: 'cmd:B2', to: 'evt:C' },
88
+ ],
89
+ };
90
+
91
+ const result = filterGraph(graph, { excludeTypes: ['command'], maintainEdges: true });
92
+
93
+ expect(result).toEqual({
94
+ nodes: [
95
+ { id: 'evt:A', type: 'event', label: 'A' },
96
+ { id: 'evt:C', type: 'event', label: 'C' },
97
+ ],
98
+ edges: [{ from: 'evt:A', to: 'evt:C' }],
99
+ });
100
+ });
101
+ });
102
+
103
+ describe('P5: self-loop preservation', () => {
104
+ it('should preserve self-loops created by edge reconnection and mark as backLink', () => {
105
+ const graph: GraphIR = {
106
+ nodes: [
107
+ { id: 'cmd:A', type: 'command', label: 'A' },
108
+ { id: 'evt:B', type: 'event', label: 'B' },
109
+ ],
110
+ edges: [
111
+ { from: 'cmd:A', to: 'evt:B' },
112
+ { from: 'evt:B', to: 'cmd:A' },
113
+ ],
114
+ };
115
+
116
+ const result = filterGraph(graph, { excludeTypes: ['event'], maintainEdges: true });
117
+
118
+ expect(result).toEqual({
119
+ nodes: [{ id: 'cmd:A', type: 'command', label: 'A' }],
120
+ edges: [{ from: 'cmd:A', to: 'cmd:A', backLink: true }],
121
+ });
122
+ });
123
+ });
124
+
125
+ describe('P6: preserve edge properties', () => {
126
+ it('should preserve edge labels through reconnection', () => {
127
+ const graph: GraphIR = {
128
+ nodes: [
129
+ { id: 'evt:A', type: 'event', label: 'A' },
130
+ { id: 'cmd:B', type: 'command', label: 'B' },
131
+ { id: 'evt:C', type: 'event', label: 'C' },
132
+ ],
133
+ edges: [
134
+ { from: 'evt:A', to: 'cmd:B', label: 'triggers' },
135
+ { from: 'cmd:B', to: 'evt:C' },
136
+ ],
137
+ };
138
+
139
+ const result = filterGraph(graph, { excludeTypes: ['command'], maintainEdges: true });
140
+
141
+ expect(result.edges[0]).toEqual({ from: 'evt:A', to: 'evt:C', label: 'triggers' });
142
+ });
143
+
144
+ it('should preserve backLink property through reconnection', () => {
145
+ const graph: GraphIR = {
146
+ nodes: [
147
+ { id: 'evt:A', type: 'event', label: 'A' },
148
+ { id: 'cmd:B', type: 'command', label: 'B' },
149
+ { id: 'evt:C', type: 'event', label: 'C' },
150
+ ],
151
+ edges: [
152
+ { from: 'evt:A', to: 'cmd:B', backLink: true },
153
+ { from: 'cmd:B', to: 'evt:C' },
154
+ ],
155
+ };
156
+
157
+ const result = filterGraph(graph, { excludeTypes: ['command'], maintainEdges: true });
158
+
159
+ expect(result.edges[0]).toEqual({ from: 'evt:A', to: 'evt:C', backLink: true });
160
+ });
161
+ });
162
+
163
+ describe('P7: multiple excluded types', () => {
164
+ it('should filter multiple node types', () => {
165
+ const graph: GraphIR = {
166
+ nodes: [
167
+ { id: 'evt:A', type: 'event', label: 'A' },
168
+ { id: 'cmd:B', type: 'command', label: 'B' },
169
+ { id: 'settled:C', type: 'settled', label: 'C' },
170
+ ],
171
+ edges: [
172
+ { from: 'evt:A', to: 'cmd:B' },
173
+ { from: 'cmd:B', to: 'settled:C' },
174
+ ],
175
+ };
176
+
177
+ const result = filterGraph(graph, { excludeTypes: ['event', 'settled'], maintainEdges: true });
178
+
179
+ expect(result).toEqual({
180
+ nodes: [{ id: 'cmd:B', type: 'command', label: 'B' }],
181
+ edges: [],
182
+ });
183
+ });
184
+ });
185
+
186
+ describe('P8: edge deduplication', () => {
187
+ it('should deduplicate edges when multiple paths merge', () => {
188
+ const graph: GraphIR = {
189
+ nodes: [
190
+ { id: 'evt:A', type: 'event', label: 'A' },
191
+ { id: 'cmd:B1', type: 'command', label: 'B1' },
192
+ { id: 'cmd:B2', type: 'command', label: 'B2' },
193
+ { id: 'evt:C', type: 'event', label: 'C' },
194
+ ],
195
+ edges: [
196
+ { from: 'evt:A', to: 'cmd:B1' },
197
+ { from: 'evt:A', to: 'cmd:B2' },
198
+ { from: 'cmd:B1', to: 'evt:C' },
199
+ { from: 'cmd:B2', to: 'evt:C' },
200
+ ],
201
+ };
202
+
203
+ const result = filterGraph(graph, { excludeTypes: ['command'], maintainEdges: true });
204
+
205
+ expect(result.edges).toHaveLength(1);
206
+ expect(result.edges[0]).toEqual({ from: 'evt:A', to: 'evt:C' });
207
+ });
208
+ });
209
+
210
+ describe('P9: edge cases', () => {
211
+ it('should return empty graph when all nodes filtered', () => {
212
+ const graph: GraphIR = {
213
+ nodes: [{ id: 'evt:A', type: 'event', label: 'A' }],
214
+ edges: [],
215
+ };
216
+
217
+ const result = filterGraph(graph, { excludeTypes: ['event'], maintainEdges: true });
218
+
219
+ expect(result).toEqual({ nodes: [], edges: [] });
220
+ });
221
+
222
+ it('should return unchanged graph when no types excluded', () => {
223
+ const graph: GraphIR = {
224
+ nodes: [{ id: 'evt:A', type: 'event', label: 'A' }],
225
+ edges: [],
226
+ };
227
+
228
+ const result = filterGraph(graph, { excludeTypes: [], maintainEdges: false });
229
+
230
+ expect(result).toEqual(graph);
231
+ });
232
+
233
+ it('should handle empty graph input', () => {
234
+ const graph: GraphIR = { nodes: [], edges: [] };
235
+
236
+ const result = filterGraph(graph, { excludeTypes: ['event'], maintainEdges: true });
237
+
238
+ expect(result).toEqual({ nodes: [], edges: [] });
239
+ });
240
+ });
241
+ });
@@ -0,0 +1,111 @@
1
+ import type { FilterOptions, GraphEdge, GraphIR, NodeType } from './types';
2
+
3
+ export function filterGraph(graph: GraphIR, options: FilterOptions): GraphIR {
4
+ const excludeSet = new Set<NodeType>(options.excludeTypes);
5
+ const remainingNodes = graph.nodes.filter((node) => !excludeSet.has(node.type));
6
+ const removedNodeIds = new Set(graph.nodes.filter((n) => excludeSet.has(n.type)).map((n) => n.id));
7
+
8
+ if (!options.maintainEdges) {
9
+ return filterEdgesSimple(graph, remainingNodes, removedNodeIds);
10
+ }
11
+
12
+ return reconnectEdges(graph, remainingNodes, removedNodeIds);
13
+ }
14
+
15
+ function filterEdgesSimple(graph: GraphIR, remainingNodes: GraphIR['nodes'], removedNodeIds: Set<string>): GraphIR {
16
+ const remainingEdges = graph.edges.filter((edge) => !removedNodeIds.has(edge.from) && !removedNodeIds.has(edge.to));
17
+ return { nodes: remainingNodes, edges: remainingEdges };
18
+ }
19
+
20
+ function reconnectEdges(graph: GraphIR, remainingNodes: GraphIR['nodes'], removedNodeIds: Set<string>): GraphIR {
21
+ const outgoingEdges = buildOutgoingEdgesMap(graph.edges);
22
+ const reconnectedEdges: GraphEdge[] = [];
23
+ const seenEdges = new Set<string>();
24
+
25
+ for (const edge of graph.edges) {
26
+ if (!removedNodeIds.has(edge.from)) {
27
+ processEdge(edge, removedNodeIds, outgoingEdges, reconnectedEdges, seenEdges);
28
+ }
29
+ }
30
+
31
+ return { nodes: remainingNodes, edges: reconnectedEdges };
32
+ }
33
+
34
+ function buildOutgoingEdgesMap(edges: GraphEdge[]): Map<string, GraphEdge[]> {
35
+ const outgoingEdges = new Map<string, GraphEdge[]>();
36
+ for (const edge of edges) {
37
+ const existing = outgoingEdges.get(edge.from) ?? [];
38
+ existing.push(edge);
39
+ outgoingEdges.set(edge.from, existing);
40
+ }
41
+ return outgoingEdges;
42
+ }
43
+
44
+ function processEdge(
45
+ edge: GraphEdge,
46
+ removedNodeIds: Set<string>,
47
+ outgoingEdges: Map<string, GraphEdge[]>,
48
+ reconnectedEdges: GraphEdge[],
49
+ seenEdges: Set<string>,
50
+ ): void {
51
+ if (!removedNodeIds.has(edge.to)) {
52
+ addEdgeIfNew(edge, reconnectedEdges, seenEdges);
53
+ return;
54
+ }
55
+
56
+ const targets = findFinalTargets(edge.to, removedNodeIds, outgoingEdges);
57
+ for (const target of targets) {
58
+ const isSelfLoop = target === edge.from;
59
+ addReconnectedEdge(edge, target, reconnectedEdges, seenEdges, isSelfLoop);
60
+ }
61
+ }
62
+
63
+ function addEdgeIfNew(edge: GraphEdge, reconnectedEdges: GraphEdge[], seenEdges: Set<string>): void {
64
+ const key = `${edge.from}->${edge.to}`;
65
+ if (!seenEdges.has(key)) {
66
+ seenEdges.add(key);
67
+ reconnectedEdges.push(edge);
68
+ }
69
+ }
70
+
71
+ function addReconnectedEdge(
72
+ sourceEdge: GraphEdge,
73
+ target: string,
74
+ reconnectedEdges: GraphEdge[],
75
+ seenEdges: Set<string>,
76
+ isSelfLoop: boolean,
77
+ ): void {
78
+ const key = `${sourceEdge.from}->${target}`;
79
+ if (seenEdges.has(key)) {
80
+ return;
81
+ }
82
+ seenEdges.add(key);
83
+
84
+ const newEdge: GraphEdge = { from: sourceEdge.from, to: target };
85
+ if (sourceEdge.label !== undefined) {
86
+ newEdge.label = sourceEdge.label;
87
+ }
88
+ if (isSelfLoop || sourceEdge.backLink === true) {
89
+ newEdge.backLink = true;
90
+ }
91
+ reconnectedEdges.push(newEdge);
92
+ }
93
+
94
+ function findFinalTargets(
95
+ nodeId: string,
96
+ removedNodeIds: Set<string>,
97
+ outgoingEdges: Map<string, GraphEdge[]>,
98
+ ): string[] {
99
+ const edges = outgoingEdges.get(nodeId) ?? [];
100
+ const targets: string[] = [];
101
+
102
+ for (const edge of edges) {
103
+ if (removedNodeIds.has(edge.to)) {
104
+ targets.push(...findFinalTargets(edge.to, removedNodeIds, outgoingEdges));
105
+ } else {
106
+ targets.push(edge.to);
107
+ }
108
+ }
109
+
110
+ return targets;
111
+ }
@@ -1,9 +1,14 @@
1
1
  export type NodeType = 'event' | 'command' | 'settled';
2
2
 
3
+ export type NodeStatus = 'idle' | 'running' | 'success' | 'error';
4
+
3
5
  export interface GraphNode {
4
6
  id: string;
5
7
  type: NodeType;
6
8
  label: string;
9
+ status?: NodeStatus;
10
+ pendingCount?: number;
11
+ endedCount?: number;
7
12
  }
8
13
 
9
14
  export interface GraphEdge {
@@ -17,3 +22,8 @@ export interface GraphIR {
17
22
  nodes: GraphNode[];
18
23
  edges: GraphEdge[];
19
24
  }
25
+
26
+ export interface FilterOptions {
27
+ excludeTypes: NodeType[];
28
+ maintainEdges: boolean;
29
+ }
package/src/index.ts CHANGED
@@ -30,11 +30,10 @@ export type {
30
30
  } from './core/descriptors';
31
31
  export type { Command, CommandDispatch, Event } from './core/types';
32
32
  export { dispatch } from './core/types';
33
-
34
33
  export type { GraphEdge, GraphIR, GraphNode, NodeType } from './graph/types';
35
34
  export type { EventLoggerOptions, LogEntry } from './logging/event-logger';
36
35
  export { EventLogger } from './logging/event-logger';
37
-
36
+ export type { AwaitEvent, AwaitTrackerDocument } from './projections/await-tracker-projection';
38
37
  export { AwaitTracker } from './runtime/await-tracker';
39
38
  export type { PipelineContext, RuntimeConfig } from './runtime/context';
40
39
  export { EventCommandMapper } from './runtime/event-command-map';
@@ -0,0 +1,68 @@
1
+ export interface AwaitTrackerDocument {
2
+ [key: string]: unknown;
3
+ correlationId: string;
4
+ pendingKeys: string[];
5
+ results: Record<string, unknown>;
6
+ status: 'pending' | 'completed';
7
+ }
8
+
9
+ export interface AwaitStartedEvent {
10
+ type: 'AwaitStarted';
11
+ data: {
12
+ correlationId: string;
13
+ keys: string[];
14
+ };
15
+ }
16
+
17
+ export interface AwaitItemCompletedEvent {
18
+ type: 'AwaitItemCompleted';
19
+ data: {
20
+ correlationId: string;
21
+ key: string;
22
+ result: unknown;
23
+ };
24
+ }
25
+
26
+ export interface AwaitCompletedEvent {
27
+ type: 'AwaitCompleted';
28
+ data: {
29
+ correlationId: string;
30
+ };
31
+ }
32
+
33
+ export type AwaitEvent = AwaitStartedEvent | AwaitItemCompletedEvent | AwaitCompletedEvent;
34
+
35
+ export function evolve(document: AwaitTrackerDocument | null, event: AwaitEvent): AwaitTrackerDocument {
36
+ switch (event.type) {
37
+ case 'AwaitStarted': {
38
+ const { correlationId, keys } = event.data;
39
+ return {
40
+ correlationId,
41
+ pendingKeys: [...keys],
42
+ results: {},
43
+ status: 'pending',
44
+ };
45
+ }
46
+ case 'AwaitItemCompleted': {
47
+ if (document === null) {
48
+ throw new Error('Cannot apply AwaitItemCompleted to null document');
49
+ }
50
+ const { key, result } = event.data;
51
+ const newPendingKeys = document.pendingKeys.filter((k) => k !== key);
52
+ return {
53
+ ...document,
54
+ pendingKeys: newPendingKeys,
55
+ results: { ...document.results, [key]: result },
56
+ };
57
+ }
58
+ case 'AwaitCompleted': {
59
+ if (document === null) {
60
+ throw new Error('Cannot apply AwaitCompleted to null document');
61
+ }
62
+ return {
63
+ ...document,
64
+ status: 'completed',
65
+ };
66
+ }
67
+ }
68
+ }
@@ -0,0 +1,11 @@
1
+ export {
2
+ evolve as evolveItemStatus,
3
+ type ItemStatusChangedEvent,
4
+ type ItemStatusDocument,
5
+ } from './item-status-projection';
6
+ export { evolve as evolveLatestRun, type LatestRunDocument } from './latest-run-projection';
7
+ export {
8
+ evolve as evolveNodeStatus,
9
+ type NodeStatusChangedEvent,
10
+ type NodeStatusDocument,
11
+ } from './node-status-projection';