@auto-engineer/job-graph-processor 1.12.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 (116) hide show
  1. package/.turbo/turbo-build.log +5 -0
  2. package/.turbo/turbo-test.log +14 -0
  3. package/.turbo/turbo-type-check.log +4 -0
  4. package/CHANGELOG.md +12 -0
  5. package/LICENSE +10 -0
  6. package/README.md +408 -0
  7. package/dist/src/apply-policy.d.ts +3 -0
  8. package/dist/src/apply-policy.d.ts.map +1 -0
  9. package/dist/src/apply-policy.js +24 -0
  10. package/dist/src/apply-policy.js.map +1 -0
  11. package/dist/src/apply-policy.specs.d.ts +2 -0
  12. package/dist/src/apply-policy.specs.d.ts.map +1 -0
  13. package/dist/src/apply-policy.specs.js +75 -0
  14. package/dist/src/apply-policy.specs.js.map +1 -0
  15. package/dist/src/commands/process-job-graph.d.ts +31 -0
  16. package/dist/src/commands/process-job-graph.d.ts.map +1 -0
  17. package/dist/src/commands/process-job-graph.js +64 -0
  18. package/dist/src/commands/process-job-graph.js.map +1 -0
  19. package/dist/src/commands/process-job-graph.specs.d.ts +2 -0
  20. package/dist/src/commands/process-job-graph.specs.d.ts.map +1 -0
  21. package/dist/src/commands/process-job-graph.specs.js +73 -0
  22. package/dist/src/commands/process-job-graph.specs.js.map +1 -0
  23. package/dist/src/evolve.d.ts +70 -0
  24. package/dist/src/evolve.d.ts.map +1 -0
  25. package/dist/src/evolve.js +82 -0
  26. package/dist/src/evolve.js.map +1 -0
  27. package/dist/src/evolve.specs.d.ts +2 -0
  28. package/dist/src/evolve.specs.d.ts.map +1 -0
  29. package/dist/src/evolve.specs.js +209 -0
  30. package/dist/src/evolve.specs.js.map +1 -0
  31. package/dist/src/graph-processor.d.ts +22 -0
  32. package/dist/src/graph-processor.d.ts.map +1 -0
  33. package/dist/src/graph-processor.js +82 -0
  34. package/dist/src/graph-processor.js.map +1 -0
  35. package/dist/src/graph-processor.specs.d.ts +2 -0
  36. package/dist/src/graph-processor.specs.d.ts.map +1 -0
  37. package/dist/src/graph-processor.specs.js +286 -0
  38. package/dist/src/graph-processor.specs.js.map +1 -0
  39. package/dist/src/graph-validator.d.ts +19 -0
  40. package/dist/src/graph-validator.d.ts.map +1 -0
  41. package/dist/src/graph-validator.js +65 -0
  42. package/dist/src/graph-validator.js.map +1 -0
  43. package/dist/src/graph-validator.specs.d.ts +2 -0
  44. package/dist/src/graph-validator.specs.d.ts.map +1 -0
  45. package/dist/src/graph-validator.specs.js +86 -0
  46. package/dist/src/graph-validator.specs.js.map +1 -0
  47. package/dist/src/handle-job-event.d.ts +16 -0
  48. package/dist/src/handle-job-event.d.ts.map +1 -0
  49. package/dist/src/handle-job-event.js +46 -0
  50. package/dist/src/handle-job-event.js.map +1 -0
  51. package/dist/src/handle-job-event.specs.d.ts +2 -0
  52. package/dist/src/handle-job-event.specs.d.ts.map +1 -0
  53. package/dist/src/handle-job-event.specs.js +136 -0
  54. package/dist/src/handle-job-event.specs.js.map +1 -0
  55. package/dist/src/index.d.ts +41 -0
  56. package/dist/src/index.d.ts.map +1 -0
  57. package/dist/src/index.js +11 -0
  58. package/dist/src/index.js.map +1 -0
  59. package/dist/src/integration.specs.d.ts +2 -0
  60. package/dist/src/integration.specs.d.ts.map +1 -0
  61. package/dist/src/integration.specs.js +225 -0
  62. package/dist/src/integration.specs.js.map +1 -0
  63. package/dist/src/process-graph.d.ts +14 -0
  64. package/dist/src/process-graph.d.ts.map +1 -0
  65. package/dist/src/process-graph.js +26 -0
  66. package/dist/src/process-graph.js.map +1 -0
  67. package/dist/src/process-graph.specs.d.ts +2 -0
  68. package/dist/src/process-graph.specs.d.ts.map +1 -0
  69. package/dist/src/process-graph.specs.js +41 -0
  70. package/dist/src/process-graph.specs.js.map +1 -0
  71. package/dist/src/process-job-graph.e2e.specs.d.ts +2 -0
  72. package/dist/src/process-job-graph.e2e.specs.d.ts.map +1 -0
  73. package/dist/src/process-job-graph.e2e.specs.js +81 -0
  74. package/dist/src/process-job-graph.e2e.specs.js.map +1 -0
  75. package/dist/src/retry-manager.d.ts +10 -0
  76. package/dist/src/retry-manager.d.ts.map +1 -0
  77. package/dist/src/retry-manager.js +17 -0
  78. package/dist/src/retry-manager.js.map +1 -0
  79. package/dist/src/retry-manager.specs.d.ts +2 -0
  80. package/dist/src/retry-manager.specs.d.ts.map +1 -0
  81. package/dist/src/retry-manager.specs.js +55 -0
  82. package/dist/src/retry-manager.specs.js.map +1 -0
  83. package/dist/src/timeout-manager.d.ts +7 -0
  84. package/dist/src/timeout-manager.d.ts.map +1 -0
  85. package/dist/src/timeout-manager.js +25 -0
  86. package/dist/src/timeout-manager.js.map +1 -0
  87. package/dist/src/timeout-manager.specs.d.ts +2 -0
  88. package/dist/src/timeout-manager.specs.d.ts.map +1 -0
  89. package/dist/src/timeout-manager.specs.js +44 -0
  90. package/dist/src/timeout-manager.specs.js.map +1 -0
  91. package/dist/tsconfig.tsbuildinfo +1 -0
  92. package/ketchup-plan.md +31 -0
  93. package/package.json +25 -0
  94. package/src/apply-policy.specs.ts +85 -0
  95. package/src/apply-policy.ts +27 -0
  96. package/src/commands/process-job-graph.specs.ts +93 -0
  97. package/src/commands/process-job-graph.ts +80 -0
  98. package/src/evolve.specs.ts +235 -0
  99. package/src/evolve.ts +121 -0
  100. package/src/graph-processor.specs.ts +331 -0
  101. package/src/graph-processor.ts +121 -0
  102. package/src/graph-validator.specs.ts +105 -0
  103. package/src/graph-validator.ts +94 -0
  104. package/src/handle-job-event.specs.ts +154 -0
  105. package/src/handle-job-event.ts +59 -0
  106. package/src/index.ts +17 -0
  107. package/src/integration.specs.ts +249 -0
  108. package/src/process-graph.specs.ts +44 -0
  109. package/src/process-graph.ts +42 -0
  110. package/src/process-job-graph.e2e.specs.ts +121 -0
  111. package/src/retry-manager.specs.ts +66 -0
  112. package/src/retry-manager.ts +29 -0
  113. package/src/timeout-manager.specs.ts +55 -0
  114. package/src/timeout-manager.ts +34 -0
  115. package/tsconfig.json +9 -0
  116. package/vitest.config.ts +22 -0
@@ -0,0 +1,5 @@
1
+
2
+ > @auto-engineer/job-graph-processor@1.12.1 build /home/runner/work/auto-engineer/auto-engineer/packages/job-graph-processor
3
+ > tsc && tsx ../../scripts/fix-esm-imports.ts
4
+
5
+ Fixed ESM imports in dist/
@@ -0,0 +1,14 @@
1
+
2
+ > @auto-engineer/job-graph-processor@1.12.1 test /home/runner/work/auto-engineer/auto-engineer/packages/job-graph-processor
3
+ > vitest run --reporter=dot
4
+
5
+
6
+  RUN  v3.2.4 /home/runner/work/auto-engineer/auto-engineer/packages/job-graph-processor
7
+
8
+ ·········································································
9
+
10
+  Test Files  11 passed (11)
11
+  Tests  73 passed (73)
12
+  Start at  12:30:06
13
+  Duration  5.76s (transform 1.36s, setup 0ms, collect 3.19s, tests 1.41s, environment 2ms, prepare 4.31s)
14
+
@@ -0,0 +1,4 @@
1
+
2
+ > @auto-engineer/job-graph-processor@1.12.1 type-check /home/runner/work/auto-engineer/auto-engineer/packages/job-graph-processor
3
+ > tsc --noEmit
4
+
package/CHANGELOG.md ADDED
@@ -0,0 +1,12 @@
1
+ # @auto-engineer/job-graph-processor
2
+
3
+ ## 1.12.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [`6557224`](https://github.com/BeOnAuto/auto-engineer/commit/6557224ec6f51c704855e5058931e81ab1de1544) Thanks [@osamanar](https://github.com/osamanar)! - - Updated project dependency lock file to ensure consistent package installations
8
+
9
+ - [`cd5f56b`](https://github.com/BeOnAuto/auto-engineer/commit/cd5f56b01951cd27392c51a384706a8c2a7401c5) Thanks [@osamanar](https://github.com/osamanar)! - - Removed unused packages to keep the project lean and reduce maintenance overhead
10
+
11
+ - Updated dependencies [[`6557224`](https://github.com/BeOnAuto/auto-engineer/commit/6557224ec6f51c704855e5058931e81ab1de1544), [`cd5f56b`](https://github.com/BeOnAuto/auto-engineer/commit/cd5f56b01951cd27392c51a384706a8c2a7401c5)]:
12
+ - @auto-engineer/message-bus@1.12.1
package/LICENSE ADDED
@@ -0,0 +1,10 @@
1
+ Elastic License 2.0
2
+
3
+ Copyright 2024 Sam Hatoum
4
+
5
+ This software and associated documentation files (the "Software") are licensed under the Elastic License 2.0 (the "License"). You may not use this file except in compliance with the License.
6
+
7
+ You may obtain a copy of the License at:
8
+ https://www.elastic.co/licensing/elastic-license
9
+
10
+ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
package/README.md ADDED
@@ -0,0 +1,408 @@
1
+ # @auto-engineer/job-graph-processor
2
+
3
+ DAG-based job orchestration with dependency tracking, parallel dispatch, and configurable failure policies.
4
+
5
+ ---
6
+
7
+ ## Purpose
8
+
9
+ Without `@auto-engineer/job-graph-processor`, you would have to manually track job dependencies, determine which jobs are ready to run, handle failures across dependency chains, and wire up event correlation to know when downstream work completes.
10
+
11
+ This package implements job graph processing as a pure event-sourced state machine. State is computed by folding events through an `evolve` function -- no mutable state, no side effects in the core logic. The `createGraphProcessor` factory wires the pure logic to a `MessageBus` for correlation-based event routing, so any domain event type can complete or fail a job as long as it carries the correct correlation ID.
12
+
13
+ The package exports a `COMMANDS` array, so `PipelineServer` discovers it via `PluginLoader` and exposes `ProcessJobGraph` as a dispatchable command.
14
+
15
+ ## Key Concepts
16
+
17
+ - **Event-sourced state machine**: Graph state is derived by replaying events through a pure `evolve` reducer. No mutable state.
18
+ - **Correlation-based routing**: Jobs are tracked via `graph:<graphId>:<jobId>` correlation IDs. Downstream handlers don't need to know about the job graph.
19
+ - **Failure policies**: Three strategies (`halt`, `skip-dependents`, `continue`) control what happens when a job fails.
20
+ - **Maximum parallelism**: All jobs whose dependencies are satisfied dispatch simultaneously.
21
+ - **Target handlers required**: Each job's `target` field names a command handler that must also be registered as a plugin. The graph processor dispatches jobs by subscribing to correlated events on the messageBus, but the actual job execution depends on those target handlers existing and emitting events with the inherited correlation ID. Without matching handlers, the graph will dispatch but jobs will never complete.
22
+
23
+ ---
24
+
25
+ ## Installation
26
+
27
+ ```bash
28
+ pnpm add @auto-engineer/job-graph-processor
29
+ ```
30
+
31
+ ## Quick Start
32
+
33
+ ### 1. Register the Plugin
34
+
35
+ ```typescript
36
+ // auto.config.ts
37
+ export const plugins = [
38
+ '@auto-engineer/job-graph-processor',
39
+ '@auto-engineer/server-checks', // target handlers for your jobs
40
+ ];
41
+ ```
42
+
43
+ ### 2. Send a Command
44
+
45
+ ```bash
46
+ curl -X POST http://localhost:3000/command \
47
+ -H 'Content-Type: application/json' \
48
+ -d '{
49
+ "type": "ProcessJobGraph",
50
+ "data": {
51
+ "graphId": "deploy-1",
52
+ "jobs": [
53
+ { "id": "build", "dependsOn": [], "target": "RunBuild", "payload": { "src": "./app" } },
54
+ { "id": "test", "dependsOn": ["build"], "target": "RunTests", "payload": {} },
55
+ { "id": "deploy", "dependsOn": ["test"], "target": "Deploy", "payload": {} }
56
+ ],
57
+ "failurePolicy": "halt"
58
+ }
59
+ }'
60
+ ```
61
+
62
+ The server returns an ack. The handler creates a `createGraphProcessor(messageBus)`, validates the graph, and dispatches `build` immediately (no dependencies). When the `RunBuild` handler emits an event with `correlationId: 'graph:deploy-1:build'`, the processor dispatches `test`, and so on until `graph.completed` is published to the messageBus.
63
+
64
+ Each job's `target` must match a registered command handler name. If `RunBuild` is not registered as a plugin, the build job dispatches but never receives a completion event, so the graph stalls.
65
+
66
+ ---
67
+
68
+ ## How-to Guides
69
+
70
+ ### Run via CLI
71
+
72
+ ```bash
73
+ auto process:job-graph --graphId=g1 --jobs='[{"id":"a","dependsOn":[],"target":"build","payload":{}}]' --failurePolicy=halt
74
+ ```
75
+
76
+ ### Run Programmatically
77
+
78
+ ```typescript
79
+ import { COMMANDS } from '@auto-engineer/job-graph-processor';
80
+
81
+ const handler = COMMANDS[0];
82
+ const result = await handler.handle(
83
+ {
84
+ type: 'ProcessJobGraph',
85
+ data: {
86
+ graphId: 'g1',
87
+ jobs: [{ id: 'a', dependsOn: [], target: 'build', payload: {} }],
88
+ failurePolicy: 'halt',
89
+ },
90
+ },
91
+ { messageBus },
92
+ );
93
+ ```
94
+
95
+ The second argument is the pipeline context. The handler extracts `messageBus` from it. Without a messageBus, the handler returns `graph.failed` with reason `messageBus not available in context`.
96
+
97
+ ### Handle Errors
98
+
99
+ ```typescript
100
+ if (result.type === 'graph.failed') {
101
+ console.error(result.data.reason);
102
+ }
103
+ ```
104
+
105
+ ### Listen for Graph Completion
106
+
107
+ ```typescript
108
+ bus.subscribeToEvent('graph.completed', {
109
+ name: 'onComplete',
110
+ handle: (event) => console.log('Graph done:', event.data.graphId),
111
+ });
112
+ ```
113
+
114
+ ### Complete a Job via Correlated Events
115
+
116
+ Downstream handlers emit their normal domain events. The graph processor picks them up via correlation ID:
117
+
118
+ ```typescript
119
+ await bus.publishEvent({
120
+ type: 'BuildCompleted',
121
+ data: { output: 'ok' },
122
+ correlationId: 'graph:deploy-pipeline:build',
123
+ });
124
+ ```
125
+
126
+ Any event without an `error` field in its data is treated as success. Any event with an `error` field is treated as failure.
127
+
128
+ ### Handle Failures with Policies
129
+
130
+ ```typescript
131
+ processor.submit({
132
+ type: 'ProcessGraph',
133
+ data: {
134
+ graphId: 'g1',
135
+ jobs: [
136
+ { id: 'a', dependsOn: [], target: 'build', payload: {} },
137
+ { id: 'b', dependsOn: ['a'], target: 'test', payload: {} },
138
+ { id: 'c', dependsOn: [], target: 'lint', payload: {} },
139
+ ],
140
+ failurePolicy: 'skip-dependents',
141
+ },
142
+ });
143
+ ```
144
+
145
+ | Policy | When `a` Fails |
146
+ | ----------------- | -------------------------------------------------------- |
147
+ | `halt` | `b` and `c` are skipped. Graph completes immediately. |
148
+ | `skip-dependents` | `b` is skipped (depends on `a`). `c` continues. |
149
+ | `continue` | `b` is dispatched anyway. Failure is treated as resolved. |
150
+
151
+ ### Use the Pure State Machine Directly
152
+
153
+ ```typescript
154
+ import { evolve, getReadyJobs, initialState, isGraphComplete } from '@auto-engineer/job-graph-processor';
155
+
156
+ let state = evolve(initialState(), {
157
+ type: 'GraphSubmitted',
158
+ data: {
159
+ graphId: 'g1',
160
+ jobs: [
161
+ { id: 'a', dependsOn: [], target: 'build', payload: {} },
162
+ { id: 'b', dependsOn: ['a'], target: 'test', payload: {} },
163
+ ],
164
+ failurePolicy: 'halt',
165
+ },
166
+ });
167
+
168
+ getReadyJobs(state); // ['a']
169
+ isGraphComplete(state); // false
170
+
171
+ state = evolve(state, { type: 'JobDispatched', data: { jobId: 'a', target: 'build', correlationId: 'graph:g1:a' } });
172
+ state = evolve(state, { type: 'JobSucceeded', data: { jobId: 'a', result: { output: 'ok' } } });
173
+
174
+ getReadyJobs(state); // ['b']
175
+ ```
176
+
177
+ ### Add Per-Job Timeouts
178
+
179
+ ```typescript
180
+ import { createTimeoutManager } from '@auto-engineer/job-graph-processor';
181
+
182
+ const timeouts = createTimeoutManager((jobId) => {
183
+ // Handle timeout -- emit JobTimedOut event
184
+ });
185
+
186
+ timeouts.start('build', 30000);
187
+ timeouts.clear('build'); // Cancel if job completes in time
188
+ ```
189
+
190
+ ### Add Retry with Exponential Backoff
191
+
192
+ ```typescript
193
+ import { createRetryManager } from '@auto-engineer/job-graph-processor';
194
+
195
+ const retries = createRetryManager((jobId, attempt) => {
196
+ // Re-dispatch the job
197
+ });
198
+
199
+ const config = { maxRetries: 3, backoffMs: 100, maxBackoffMs: 5000 };
200
+ const exhausted = retries.recordFailure('build', config);
201
+ // false: will retry after 100ms
202
+ // Subsequent failures: 200ms, 400ms, then returns true (exhausted)
203
+ ```
204
+
205
+ ---
206
+
207
+ ## API Reference
208
+
209
+ ### Exports
210
+
211
+ ```typescript
212
+ import {
213
+ COMMANDS,
214
+ createGraphProcessor,
215
+ evolve,
216
+ initialState,
217
+ getReadyJobs,
218
+ getTransitiveDependents,
219
+ isGraphComplete,
220
+ applyPolicy,
221
+ validateGraph,
222
+ classifyJobEvent,
223
+ handleJobEvent,
224
+ isJobFailure,
225
+ parseCorrelationId,
226
+ handleProcessGraph,
227
+ createTimeoutManager,
228
+ createRetryManager,
229
+ } from '@auto-engineer/job-graph-processor';
230
+
231
+ import type {
232
+ GraphState,
233
+ JobStatus,
234
+ FailurePolicy,
235
+ JobGraphEvent,
236
+ Job,
237
+ RetryConfig,
238
+ RetryManager,
239
+ TimeoutManager,
240
+ } from '@auto-engineer/job-graph-processor';
241
+ ```
242
+
243
+ ### Commands
244
+
245
+ | Command | CLI Alias | Description |
246
+ | ----------------- | ------------------ | -------------------------------------------------------------------------- |
247
+ | `ProcessJobGraph` | `process:job-graph`| Process a DAG of jobs with dependency tracking and failure policies |
248
+
249
+ #### Command Fields
250
+
251
+ | Field | Type | Required | Description |
252
+ | --------------- | --------------- | -------- | ------------------------------------------------------------------- |
253
+ | `graphId` | `string` | yes | Unique identifier for the graph |
254
+ | `jobs` | `Job[]` | yes | Array of jobs with dependencies. Each job's `target` must match a registered command handler |
255
+ | `failurePolicy` | `FailurePolicy` | yes | How to handle failures: `halt`, `skip-dependents`, or `continue` |
256
+
257
+ #### Events Produced
258
+
259
+ | Event | When |
260
+ | ------------------ | --------------------------------------- |
261
+ | `graph.dispatching`| Graph validated, ready jobs dispatched |
262
+ | `graph.failed` | Validation error or missing messageBus |
263
+ | `graph.completed` | All jobs reached terminal status (published to messageBus, not returned from handler) |
264
+
265
+ ### Functions
266
+
267
+ #### `createGraphProcessor(messageBus): { submit }`
268
+
269
+ Stateful processor that wires the pure state machine to a MessageBus. Tracks active graphs, subscribes to correlation events, dispatches ready jobs, and publishes `graph.completed`.
270
+
271
+ #### `evolve(state, event): GraphState`
272
+
273
+ Pure reducer. Applies a `JobGraphEvent` to a `GraphState` and returns a new state.
274
+
275
+ #### `initialState(): GraphState`
276
+
277
+ Returns `{ status: 'pending' }`.
278
+
279
+ #### `getReadyJobs(state): string[]`
280
+
281
+ Returns job IDs whose dependencies are all resolved (respects `continue` policy).
282
+
283
+ #### `isGraphComplete(state): boolean`
284
+
285
+ Returns `true` when every job has reached a terminal status.
286
+
287
+ #### `getTransitiveDependents(state, jobId): string[]`
288
+
289
+ BFS traversal returning all downstream dependents of a job.
290
+
291
+ #### `validateGraph(jobs): { valid: true } | { valid: false, error: string }`
292
+
293
+ Validates DAG structure: unique IDs, valid dependency references, no self-loops, no cycles.
294
+
295
+ #### `applyPolicy(state, failedJobId): JobGraphEvent[]`
296
+
297
+ Returns `JobSkipped` events based on the graph's failure policy.
298
+
299
+ #### `classifyJobEvent(event): JobGraphEvent | null`
300
+
301
+ Maps a domain event (with correlation ID) to a typed `JobSucceeded` or `JobFailed` event.
302
+
303
+ #### `handleJobEvent(state, event): { events, readyJobs, graphComplete } | null`
304
+
305
+ Full orchestration: classifies event, evolves state, applies failure policy, returns results.
306
+
307
+ #### `parseCorrelationId(id): { graphId, jobId } | null`
308
+
309
+ Extracts graph and job identifiers from a `graph:<graphId>:<jobId>` string.
310
+
311
+ #### `isJobFailure(event): boolean`
312
+
313
+ Returns `true` if the event's data contains an `error` field.
314
+
315
+ #### `createTimeoutManager(onTimeout): TimeoutManager`
316
+
317
+ Per-job timeout tracking with `start`, `clear`, and `clearAll`.
318
+
319
+ #### `createRetryManager(onRetry): RetryManager`
320
+
321
+ Exponential backoff retry with `recordFailure(jobId, config)` returning `true` when exhausted.
322
+
323
+ ### Interfaces
324
+
325
+ #### `Job`
326
+
327
+ ```typescript
328
+ interface Job {
329
+ id: string;
330
+ dependsOn: readonly string[];
331
+ target: string;
332
+ payload: unknown;
333
+ timeoutMs?: number;
334
+ retries?: number;
335
+ backoffMs?: number;
336
+ maxBackoffMs?: number;
337
+ }
338
+ ```
339
+
340
+ #### `FailurePolicy`
341
+
342
+ ```typescript
343
+ type FailurePolicy = 'halt' | 'skip-dependents' | 'continue';
344
+ ```
345
+
346
+ #### `JobGraphEvent`
347
+
348
+ ```typescript
349
+ type JobGraphEvent =
350
+ | { type: 'GraphSubmitted'; data: { graphId: string; jobs: readonly Job[]; failurePolicy: FailurePolicy } }
351
+ | { type: 'JobDispatched'; data: { jobId: string; target: string; correlationId: string } }
352
+ | { type: 'JobSucceeded'; data: { jobId: string; result?: unknown } }
353
+ | { type: 'JobFailed'; data: { jobId: string; error: string } }
354
+ | { type: 'JobSkipped'; data: { jobId: string; reason: string } }
355
+ | { type: 'JobTimedOut'; data: { jobId: string; timeoutMs: number } };
356
+ ```
357
+
358
+ ---
359
+
360
+ ## Architecture
361
+
362
+ ```
363
+ src/
364
+ ├── index.ts Barrel exports + COMMANDS array
365
+ ├── commands/
366
+ │ └── process-job-graph.ts Pipeline command handler (ProcessJobGraph)
367
+ ├── evolve.ts Pure event-sourced state machine
368
+ ├── graph-validator.ts DAG validation (cycles, duplicates, refs)
369
+ ├── handle-job-event.ts Domain event classification and orchestration
370
+ ├── apply-policy.ts Failure policy engine (halt/skip-dependents/continue)
371
+ ├── graph-processor.ts Stateful MessageBus coordinator
372
+ ├── process-graph.ts Standalone command handler
373
+ ├── timeout-manager.ts Per-job setTimeout wrapper
374
+ └── retry-manager.ts Exponential backoff retry
375
+ ```
376
+
377
+ ```mermaid
378
+ flowchart LR
379
+ Submit[submit command] --> Validate[validateGraph]
380
+ Validate --> Evolve[evolve: GraphSubmitted]
381
+ Evolve --> Ready[getReadyJobs]
382
+ Ready --> Dispatch[dispatch via MessageBus]
383
+ Dispatch --> Correlate[onCorrelationPrefix]
384
+ Correlate --> Classify[classifyJobEvent]
385
+ Classify --> EvolveAgain[evolve: JobSucceeded/Failed]
386
+ EvolveAgain --> Policy{failure?}
387
+ Policy -->|yes| Apply[applyPolicy]
388
+ Policy -->|no| ReadyAgain[getReadyJobs]
389
+ Apply --> ReadyAgain
390
+ ReadyAgain --> Complete{all terminal?}
391
+ Complete -->|yes| Done[graph.completed]
392
+ Complete -->|no| Dispatch
393
+ ```
394
+
395
+ ### Dependencies
396
+
397
+ **Monorepo:**
398
+
399
+ | Package | Usage |
400
+ | ---------------------------- | ---------------------------------- |
401
+ | `@auto-engineer/message-bus` | Correlation subscriptions, pub/sub |
402
+
403
+ **External:**
404
+
405
+ | Package | Usage |
406
+ | ------------------------- | ---------------------- |
407
+ | `@event-driven-io/emmett` | Event type import only |
408
+ | `nanoid` | Unique ID generation |
@@ -0,0 +1,3 @@
1
+ import type { GraphState, JobGraphEvent } from './evolve';
2
+ export declare function applyPolicy(state: GraphState, failedJobId: string): JobGraphEvent[];
3
+ //# sourceMappingURL=apply-policy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apply-policy.d.ts","sourceRoot":"","sources":["../../src/apply-policy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAG1D,wBAAgB,WAAW,CAAC,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,GAAG,aAAa,EAAE,CAuBnF"}
@@ -0,0 +1,24 @@
1
+ import { getTransitiveDependents } from './evolve.js';
2
+ export function applyPolicy(state, failedJobId) {
3
+ if (state.status !== 'processing')
4
+ return [];
5
+ switch (state.failurePolicy) {
6
+ case 'continue':
7
+ return [];
8
+ case 'skip-dependents':
9
+ return getTransitiveDependents(state, failedJobId).map((id) => ({
10
+ type: 'JobSkipped',
11
+ data: { jobId: id, reason: 'dependency failed' },
12
+ }));
13
+ case 'halt': {
14
+ const events = [];
15
+ for (const [id, job] of state.jobs) {
16
+ if (job.status === 'pending') {
17
+ events.push({ type: 'JobSkipped', data: { jobId: id, reason: 'halt policy' } });
18
+ }
19
+ }
20
+ return events;
21
+ }
22
+ }
23
+ }
24
+ //# sourceMappingURL=apply-policy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apply-policy.js","sourceRoot":"","sources":["../../src/apply-policy.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,uBAAuB,EAAE,MAAM,UAAU,CAAC;AAEnD,MAAM,UAAU,WAAW,CAAC,KAAiB,EAAE,WAAmB;IAChE,IAAI,KAAK,CAAC,MAAM,KAAK,YAAY;QAAE,OAAO,EAAE,CAAC;IAE7C,QAAQ,KAAK,CAAC,aAAa,EAAE,CAAC;QAC5B,KAAK,UAAU;YACb,OAAO,EAAE,CAAC;QACZ,KAAK,iBAAiB;YACpB,OAAO,uBAAuB,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC,GAAG,CACpD,CAAC,EAAE,EAAiB,EAAE,CAAC,CAAC;gBACtB,IAAI,EAAE,YAAY;gBAClB,IAAI,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,MAAM,EAAE,mBAAmB,EAAE;aACjD,CAAC,CACH,CAAC;QACJ,KAAK,MAAM,CAAC,CAAC,CAAC;YACZ,MAAM,MAAM,GAAoB,EAAE,CAAC;YACnC,KAAK,MAAM,CAAC,EAAE,EAAE,GAAG,CAAC,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC;gBACnC,IAAI,GAAG,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;oBAC7B,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,MAAM,EAAE,aAAa,EAAE,EAAE,CAAC,CAAC;gBAClF,CAAC;YACH,CAAC;YACD,OAAO,MAAM,CAAC;QAChB,CAAC;IACH,CAAC;AACH,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=apply-policy.specs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apply-policy.specs.d.ts","sourceRoot":"","sources":["../../src/apply-policy.specs.ts"],"names":[],"mappings":""}
@@ -0,0 +1,75 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { applyPolicy } from './apply-policy.js';
3
+ import { evolve, initialState } from './evolve.js';
4
+ describe('applyPolicy', () => {
5
+ it('returns empty array before graph submission', () => {
6
+ const events = applyPolicy(initialState(), 'a');
7
+ expect(events).toEqual([]);
8
+ });
9
+ it('halt policy skips all pending jobs when a job fails', () => {
10
+ let state = evolve(initialState(), {
11
+ type: 'GraphSubmitted',
12
+ data: {
13
+ graphId: 'g1',
14
+ jobs: [
15
+ { id: 'a', dependsOn: [], target: 'build', payload: {} },
16
+ { id: 'b', dependsOn: ['a'], target: 'test', payload: {} },
17
+ { id: 'c', dependsOn: ['a'], target: 'lint', payload: {} },
18
+ ],
19
+ failurePolicy: 'halt',
20
+ },
21
+ });
22
+ state = evolve(state, {
23
+ type: 'JobDispatched',
24
+ data: { jobId: 'a', target: 'build', correlationId: 'graph:g1:a' },
25
+ });
26
+ state = evolve(state, { type: 'JobFailed', data: { jobId: 'a', error: 'build error' } });
27
+ const events = applyPolicy(state, 'a');
28
+ expect(events).toEqual([
29
+ { type: 'JobSkipped', data: { jobId: 'b', reason: 'halt policy' } },
30
+ { type: 'JobSkipped', data: { jobId: 'c', reason: 'halt policy' } },
31
+ ]);
32
+ });
33
+ it('skip-dependents policy skips only transitive dependents of failed job', () => {
34
+ let state = evolve(initialState(), {
35
+ type: 'GraphSubmitted',
36
+ data: {
37
+ graphId: 'g1',
38
+ jobs: [
39
+ { id: 'a', dependsOn: [], target: 'build', payload: {} },
40
+ { id: 'b', dependsOn: ['a'], target: 'test', payload: {} },
41
+ { id: 'c', dependsOn: [], target: 'lint', payload: {} },
42
+ ],
43
+ failurePolicy: 'skip-dependents',
44
+ },
45
+ });
46
+ state = evolve(state, {
47
+ type: 'JobDispatched',
48
+ data: { jobId: 'a', target: 'build', correlationId: 'graph:g1:a' },
49
+ });
50
+ state = evolve(state, { type: 'JobFailed', data: { jobId: 'a', error: 'build error' } });
51
+ const events = applyPolicy(state, 'a');
52
+ expect(events).toEqual([{ type: 'JobSkipped', data: { jobId: 'b', reason: 'dependency failed' } }]);
53
+ });
54
+ it('continue policy returns no skip events', () => {
55
+ let state = evolve(initialState(), {
56
+ type: 'GraphSubmitted',
57
+ data: {
58
+ graphId: 'g1',
59
+ jobs: [
60
+ { id: 'a', dependsOn: [], target: 'build', payload: {} },
61
+ { id: 'b', dependsOn: ['a'], target: 'test', payload: {} },
62
+ ],
63
+ failurePolicy: 'continue',
64
+ },
65
+ });
66
+ state = evolve(state, {
67
+ type: 'JobDispatched',
68
+ data: { jobId: 'a', target: 'build', correlationId: 'graph:g1:a' },
69
+ });
70
+ state = evolve(state, { type: 'JobFailed', data: { jobId: 'a', error: 'build error' } });
71
+ const events = applyPolicy(state, 'a');
72
+ expect(events).toEqual([]);
73
+ });
74
+ });
75
+ //# sourceMappingURL=apply-policy.specs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apply-policy.specs.js","sourceRoot":"","sources":["../../src/apply-policy.specs.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ import type { Command, Event } from '@auto-engineer/message-bus';
2
+ export declare const commandHandler: {
3
+ name: string;
4
+ displayName: string;
5
+ alias: string;
6
+ description: string;
7
+ category: string;
8
+ icon: string;
9
+ fields: {
10
+ graphId: {
11
+ description: string;
12
+ required: boolean;
13
+ };
14
+ jobs: {
15
+ description: string;
16
+ required: boolean;
17
+ };
18
+ failurePolicy: {
19
+ description: string;
20
+ required: boolean;
21
+ };
22
+ };
23
+ examples: string[];
24
+ events: {
25
+ name: string;
26
+ displayName: string;
27
+ }[];
28
+ handle: (command: Command, context?: unknown) => Promise<Event | Event[]>;
29
+ };
30
+ export default commandHandler;
31
+ //# sourceMappingURL=process-job-graph.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"process-job-graph.d.ts","sourceRoot":"","sources":["../../../src/commands/process-job-graph.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,KAAK,EAAc,MAAM,4BAA4B,CAAC;AAkC7E,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;sBA6BD,OAAO,YAAY,OAAO,KAAG,OAAO,CAAC,KAAK,GAAG,KAAK,EAAE,CAAC;CAc9E,CAAC;AAEF,eAAe,cAAc,CAAC"}