@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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +26 -0
- package/README.md +279 -0
- package/dist/src/builder/define.d.ts +6 -2
- package/dist/src/builder/define.d.ts.map +1 -1
- package/dist/src/builder/define.js +17 -7
- package/dist/src/builder/define.js.map +1 -1
- package/dist/src/builder/define.specs.js +3 -3
- package/dist/src/builder/define.specs.js.map +1 -1
- package/dist/src/core/descriptors.d.ts +6 -2
- package/dist/src/core/descriptors.d.ts.map +1 -1
- package/dist/src/graph/filter-graph.d.ts +3 -0
- package/dist/src/graph/filter-graph.d.ts.map +1 -0
- package/dist/src/graph/filter-graph.js +80 -0
- package/dist/src/graph/filter-graph.js.map +1 -0
- package/dist/src/graph/filter-graph.specs.d.ts +2 -0
- package/dist/src/graph/filter-graph.specs.d.ts.map +1 -0
- package/dist/src/graph/filter-graph.specs.js +204 -0
- package/dist/src/graph/filter-graph.specs.js.map +1 -0
- package/dist/src/graph/types.d.ts +8 -0
- package/dist/src/graph/types.d.ts.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/projections/await-tracker-projection.d.ts +31 -0
- package/dist/src/projections/await-tracker-projection.d.ts.map +1 -0
- package/dist/src/projections/await-tracker-projection.js +35 -0
- package/dist/src/projections/await-tracker-projection.js.map +1 -0
- package/dist/src/projections/index.d.ts +4 -0
- package/dist/src/projections/index.d.ts.map +1 -0
- package/dist/src/projections/index.js +4 -0
- package/dist/src/projections/index.js.map +1 -0
- package/dist/src/projections/item-status-projection.d.ts +22 -0
- package/dist/src/projections/item-status-projection.d.ts.map +1 -0
- package/dist/src/projections/item-status-projection.js +11 -0
- package/dist/src/projections/item-status-projection.js.map +1 -0
- package/dist/src/projections/item-status-projection.specs.d.ts +2 -0
- package/dist/src/projections/item-status-projection.specs.d.ts.map +1 -0
- package/dist/src/projections/item-status-projection.specs.js +119 -0
- package/dist/src/projections/item-status-projection.specs.js.map +1 -0
- package/dist/src/projections/latest-run-projection.d.ts +15 -0
- package/dist/src/projections/latest-run-projection.d.ts.map +1 -0
- package/dist/src/projections/latest-run-projection.js +7 -0
- package/dist/src/projections/latest-run-projection.js.map +1 -0
- package/dist/src/projections/latest-run-projection.specs.d.ts +2 -0
- package/dist/src/projections/latest-run-projection.specs.d.ts.map +1 -0
- package/dist/src/projections/latest-run-projection.specs.js +33 -0
- package/dist/src/projections/latest-run-projection.specs.js.map +1 -0
- package/dist/src/projections/message-log-projection.d.ts +51 -0
- package/dist/src/projections/message-log-projection.d.ts.map +1 -0
- package/dist/src/projections/message-log-projection.js +51 -0
- package/dist/src/projections/message-log-projection.js.map +1 -0
- package/dist/src/projections/message-log-projection.specs.d.ts +2 -0
- package/dist/src/projections/message-log-projection.specs.d.ts.map +1 -0
- package/dist/src/projections/message-log-projection.specs.js +101 -0
- package/dist/src/projections/message-log-projection.specs.js.map +1 -0
- package/dist/src/projections/node-status-projection.d.ts +23 -0
- package/dist/src/projections/node-status-projection.d.ts.map +1 -0
- package/dist/src/projections/node-status-projection.js +10 -0
- package/dist/src/projections/node-status-projection.js.map +1 -0
- package/dist/src/projections/node-status-projection.specs.d.ts +2 -0
- package/dist/src/projections/node-status-projection.specs.d.ts.map +1 -0
- package/dist/src/projections/node-status-projection.specs.js +116 -0
- package/dist/src/projections/node-status-projection.specs.js.map +1 -0
- package/dist/src/projections/phased-execution-projection.d.ts +77 -0
- package/dist/src/projections/phased-execution-projection.d.ts.map +1 -0
- package/dist/src/projections/phased-execution-projection.js +54 -0
- package/dist/src/projections/phased-execution-projection.js.map +1 -0
- package/dist/src/projections/phased-execution-projection.specs.d.ts +2 -0
- package/dist/src/projections/phased-execution-projection.specs.d.ts.map +1 -0
- package/dist/src/projections/phased-execution-projection.specs.js +171 -0
- package/dist/src/projections/phased-execution-projection.specs.js.map +1 -0
- package/dist/src/projections/settled-instance-projection.d.ts +67 -0
- package/dist/src/projections/settled-instance-projection.d.ts.map +1 -0
- package/dist/src/projections/settled-instance-projection.js +66 -0
- package/dist/src/projections/settled-instance-projection.js.map +1 -0
- package/dist/src/projections/settled-instance-projection.specs.d.ts +2 -0
- package/dist/src/projections/settled-instance-projection.specs.d.ts.map +1 -0
- package/dist/src/projections/settled-instance-projection.specs.js +217 -0
- package/dist/src/projections/settled-instance-projection.specs.js.map +1 -0
- package/dist/src/projections/stats-projection.d.ts +9 -0
- package/dist/src/projections/stats-projection.d.ts.map +1 -0
- package/dist/src/projections/stats-projection.js +16 -0
- package/dist/src/projections/stats-projection.js.map +1 -0
- package/dist/src/projections/stats-projection.specs.d.ts +2 -0
- package/dist/src/projections/stats-projection.specs.d.ts.map +1 -0
- package/dist/src/projections/stats-projection.specs.js +91 -0
- package/dist/src/projections/stats-projection.specs.js.map +1 -0
- package/dist/src/runtime/await-tracker.d.ts +17 -7
- package/dist/src/runtime/await-tracker.d.ts.map +1 -1
- package/dist/src/runtime/await-tracker.js +32 -29
- package/dist/src/runtime/await-tracker.js.map +1 -1
- package/dist/src/runtime/await-tracker.specs.js +56 -38
- package/dist/src/runtime/await-tracker.specs.js.map +1 -1
- package/dist/src/runtime/context.d.ts +1 -1
- package/dist/src/runtime/context.d.ts.map +1 -1
- package/dist/src/runtime/event-command-map.d.ts +3 -3
- package/dist/src/runtime/event-command-map.d.ts.map +1 -1
- package/dist/src/runtime/event-command-map.js +6 -2
- package/dist/src/runtime/event-command-map.js.map +1 -1
- package/dist/src/runtime/phased-executor.d.ts +15 -9
- package/dist/src/runtime/phased-executor.d.ts.map +1 -1
- package/dist/src/runtime/phased-executor.js +126 -104
- package/dist/src/runtime/phased-executor.js.map +1 -1
- package/dist/src/runtime/phased-executor.specs.js +243 -81
- package/dist/src/runtime/phased-executor.specs.js.map +1 -1
- package/dist/src/runtime/pipeline-runtime.d.ts.map +1 -1
- package/dist/src/runtime/pipeline-runtime.js +2 -2
- package/dist/src/runtime/pipeline-runtime.js.map +1 -1
- package/dist/src/runtime/pipeline-runtime.specs.js +35 -0
- package/dist/src/runtime/pipeline-runtime.specs.js.map +1 -1
- package/dist/src/runtime/settled-tracker.d.ts +12 -9
- package/dist/src/runtime/settled-tracker.d.ts.map +1 -1
- package/dist/src/runtime/settled-tracker.js +92 -77
- package/dist/src/runtime/settled-tracker.js.map +1 -1
- package/dist/src/runtime/settled-tracker.specs.js +568 -118
- package/dist/src/runtime/settled-tracker.specs.js.map +1 -1
- package/dist/src/server/pipeline-server.d.ts +31 -9
- package/dist/src/server/pipeline-server.d.ts.map +1 -1
- package/dist/src/server/pipeline-server.e2e.specs.js +2 -10
- package/dist/src/server/pipeline-server.e2e.specs.js.map +1 -1
- package/dist/src/server/pipeline-server.js +418 -134
- package/dist/src/server/pipeline-server.js.map +1 -1
- package/dist/src/server/pipeline-server.specs.js +777 -32
- package/dist/src/server/pipeline-server.specs.js.map +1 -1
- package/dist/src/server/sse-manager.specs.js +55 -35
- package/dist/src/server/sse-manager.specs.js.map +1 -1
- package/dist/src/store/index.d.ts +3 -0
- package/dist/src/store/index.d.ts.map +1 -0
- package/dist/src/store/index.js +3 -0
- package/dist/src/store/index.js.map +1 -0
- package/dist/src/store/pipeline-event-store.d.ts +10 -0
- package/dist/src/store/pipeline-event-store.d.ts.map +1 -0
- package/dist/src/store/pipeline-event-store.js +112 -0
- package/dist/src/store/pipeline-event-store.js.map +1 -0
- package/dist/src/store/pipeline-event-store.specs.d.ts +2 -0
- package/dist/src/store/pipeline-event-store.specs.d.ts.map +1 -0
- package/dist/src/store/pipeline-event-store.specs.js +287 -0
- package/dist/src/store/pipeline-event-store.specs.js.map +1 -0
- package/dist/src/store/pipeline-read-model.d.ts +49 -0
- package/dist/src/store/pipeline-read-model.d.ts.map +1 -0
- package/dist/src/store/pipeline-read-model.js +157 -0
- package/dist/src/store/pipeline-read-model.js.map +1 -0
- package/dist/src/store/pipeline-read-model.specs.d.ts +2 -0
- package/dist/src/store/pipeline-read-model.specs.d.ts.map +1 -0
- package/dist/src/store/pipeline-read-model.specs.js +830 -0
- package/dist/src/store/pipeline-read-model.specs.js.map +1 -0
- package/dist/src/testing/fixtures/kanban-full.pipeline.js +2 -2
- package/dist/src/testing/fixtures/kanban-full.pipeline.js.map +1 -1
- package/dist/src/testing/fixtures/kanban.pipeline.js +2 -2
- package/dist/src/testing/fixtures/kanban.pipeline.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/ketchup-plan.md +960 -0
- package/package.json +7 -3
- package/src/builder/define.specs.ts +3 -3
- package/src/builder/define.ts +24 -11
- package/src/core/descriptors.ts +7 -2
- package/src/graph/filter-graph.specs.ts +241 -0
- package/src/graph/filter-graph.ts +111 -0
- package/src/graph/types.ts +10 -0
- package/src/index.ts +1 -2
- package/src/projections/await-tracker-projection.ts +68 -0
- package/src/projections/index.ts +11 -0
- package/src/projections/item-status-projection.specs.ts +130 -0
- package/src/projections/item-status-projection.ts +32 -0
- package/src/projections/latest-run-projection.specs.ts +38 -0
- package/src/projections/latest-run-projection.ts +20 -0
- package/src/projections/message-log-projection.specs.ts +118 -0
- package/src/projections/message-log-projection.ts +113 -0
- package/src/projections/node-status-projection.specs.ts +127 -0
- package/src/projections/node-status-projection.ts +33 -0
- package/src/projections/phased-execution-projection.specs.ts +202 -0
- package/src/projections/phased-execution-projection.ts +146 -0
- package/src/projections/settled-instance-projection.specs.ts +249 -0
- package/src/projections/settled-instance-projection.ts +160 -0
- package/src/projections/stats-projection.specs.ts +105 -0
- package/src/projections/stats-projection.ts +26 -0
- package/src/runtime/await-tracker.specs.ts +57 -34
- package/src/runtime/await-tracker.ts +43 -31
- package/src/runtime/context.ts +1 -1
- package/src/runtime/event-command-map.ts +11 -4
- package/src/runtime/phased-executor.specs.ts +357 -81
- package/src/runtime/phased-executor.ts +142 -126
- package/src/runtime/pipeline-runtime.specs.ts +42 -0
- package/src/runtime/pipeline-runtime.ts +6 -4
- package/src/runtime/settled-tracker.specs.ts +716 -120
- package/src/runtime/settled-tracker.ts +104 -98
- package/src/server/pipeline-server.e2e.specs.ts +10 -16
- package/src/server/pipeline-server.specs.ts +964 -49
- package/src/server/pipeline-server.ts +522 -156
- package/src/server/sse-manager.specs.ts +67 -36
- package/src/store/index.ts +2 -0
- package/src/store/pipeline-event-store.specs.ts +309 -0
- package/src/store/pipeline-event-store.ts +156 -0
- package/src/store/pipeline-read-model.specs.ts +967 -0
- package/src/store/pipeline-read-model.ts +223 -0
- package/src/testing/fixtures/kanban-full.pipeline.ts +2 -2
- package/src/testing/fixtures/kanban.pipeline.ts +2 -2
- package/claude.md +0 -160
- package/docs/testing-analysis.md +0 -395
- 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/
|
|
13
|
-
"@auto-engineer/message-bus": "0.
|
|
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
|
|
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('
|
|
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', () => {
|
package/src/builder/define.ts
CHANGED
|
@@ -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:
|
|
78
|
-
failure:
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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',
|
|
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);
|
package/src/core/descriptors.ts
CHANGED
|
@@ -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:
|
|
58
|
-
failureEvent:
|
|
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
|
+
}
|
package/src/graph/types.ts
CHANGED
|
@@ -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';
|