@calmo/task-runner 3.4.0 → 3.4.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 (87) hide show
  1. package/.github/dependabot.yml +7 -7
  2. package/.github/workflows/ci.yml +4 -4
  3. package/.jules/backlog_maniac.md +1 -0
  4. package/.jules/nexus.md +1 -0
  5. package/.jules/sentinel.md +1 -0
  6. package/.releaserc.json +2 -7
  7. package/AGENTS.md +8 -2
  8. package/CHANGELOG.md +178 -174
  9. package/README.md +23 -23
  10. package/coverage/coverage-final.json +8 -8
  11. package/coverage/index.html +7 -7
  12. package/coverage/lcov-report/index.html +7 -7
  13. package/coverage/lcov-report/src/EventBus.ts.html +27 -21
  14. package/coverage/lcov-report/src/TaskGraphValidationError.ts.html +12 -3
  15. package/coverage/lcov-report/src/TaskGraphValidator.ts.html +152 -137
  16. package/coverage/lcov-report/src/TaskRunner.ts.html +48 -45
  17. package/coverage/lcov-report/src/TaskRunnerBuilder.ts.html +29 -5
  18. package/coverage/lcov-report/src/TaskRunnerExecutionConfig.ts.html +1 -1
  19. package/coverage/lcov-report/src/TaskStateManager.ts.html +1 -1
  20. package/coverage/lcov-report/src/WorkflowExecutor.ts.html +21 -12
  21. package/coverage/lcov-report/src/contracts/RunnerEvents.ts.html +1 -1
  22. package/coverage/lcov-report/src/contracts/index.html +1 -1
  23. package/coverage/lcov-report/src/index.html +8 -8
  24. package/coverage/lcov-report/src/strategies/DryRunExecutionStrategy.ts.html +4 -4
  25. package/coverage/lcov-report/src/strategies/RetryingExecutionStrategy.ts.html +29 -11
  26. package/coverage/lcov-report/src/strategies/StandardExecutionStrategy.ts.html +5 -5
  27. package/coverage/lcov-report/src/strategies/index.html +1 -1
  28. package/coverage/lcov.info +266 -262
  29. package/coverage/src/EventBus.ts.html +27 -21
  30. package/coverage/src/TaskGraphValidationError.ts.html +12 -3
  31. package/coverage/src/TaskGraphValidator.ts.html +152 -137
  32. package/coverage/src/TaskRunner.ts.html +48 -45
  33. package/coverage/src/TaskRunnerBuilder.ts.html +29 -5
  34. package/coverage/src/TaskRunnerExecutionConfig.ts.html +1 -1
  35. package/coverage/src/TaskStateManager.ts.html +1 -1
  36. package/coverage/src/WorkflowExecutor.ts.html +21 -12
  37. package/coverage/src/contracts/RunnerEvents.ts.html +1 -1
  38. package/coverage/src/contracts/index.html +1 -1
  39. package/coverage/src/index.html +8 -8
  40. package/coverage/src/strategies/DryRunExecutionStrategy.ts.html +4 -4
  41. package/coverage/src/strategies/RetryingExecutionStrategy.ts.html +29 -11
  42. package/coverage/src/strategies/StandardExecutionStrategy.ts.html +5 -5
  43. package/coverage/src/strategies/index.html +1 -1
  44. package/dist/EventBus.js +13 -11
  45. package/dist/EventBus.js.map +1 -1
  46. package/dist/TaskGraphValidationError.js.map +1 -1
  47. package/dist/TaskGraphValidator.js +9 -9
  48. package/dist/TaskGraphValidator.js.map +1 -1
  49. package/dist/TaskRunner.js.map +1 -1
  50. package/dist/TaskRunnerBuilder.js.map +1 -1
  51. package/dist/WorkflowExecutor.js +2 -1
  52. package/dist/WorkflowExecutor.js.map +1 -1
  53. package/dist/strategies/RetryingExecutionStrategy.js +3 -1
  54. package/dist/strategies/RetryingExecutionStrategy.js.map +1 -1
  55. package/dist/strategies/StandardExecutionStrategy.js +1 -1
  56. package/dist/strategies/StandardExecutionStrategy.js.map +1 -1
  57. package/openspec/AGENTS.md +81 -15
  58. package/openspec/changes/archive/2026-01-18-add-concurrency-control/proposal.md +7 -4
  59. package/openspec/changes/archive/2026-01-18-add-concurrency-control/tasks.md +1 -0
  60. package/openspec/changes/archive/2026-01-18-add-external-task-cancellation/proposal.md +4 -1
  61. package/openspec/changes/archive/2026-01-18-add-external-task-cancellation/tasks.md +2 -1
  62. package/openspec/changes/archive/2026-01-18-add-integration-tests/proposal.md +3 -0
  63. package/openspec/changes/archive/2026-01-18-add-integration-tests/tasks.md +1 -0
  64. package/openspec/changes/archive/2026-01-18-add-task-retry-policy/proposal.md +3 -0
  65. package/openspec/changes/archive/2026-01-18-add-task-retry-policy/tasks.md +1 -0
  66. package/openspec/changes/archive/2026-01-18-add-workflow-preview/proposal.md +3 -0
  67. package/openspec/changes/archive/2026-01-18-add-workflow-preview/tasks.md +1 -0
  68. package/openspec/changes/archive/2026-01-18-refactor-core-architecture/proposal.md +3 -0
  69. package/openspec/changes/archive/2026-01-18-refactor-core-architecture/tasks.md +1 -0
  70. package/openspec/changes/feat-per-task-timeout/proposal.md +11 -6
  71. package/openspec/changes/feat-per-task-timeout/tasks.md +1 -1
  72. package/openspec/project.md +21 -15
  73. package/package.json +1 -1
  74. package/src/EventBus.ts +18 -16
  75. package/src/TaskGraph.ts +8 -8
  76. package/src/TaskGraphValidationError.ts +4 -1
  77. package/src/TaskGraphValidator.ts +148 -143
  78. package/src/TaskRunner.ts +42 -41
  79. package/src/TaskRunnerBuilder.ts +11 -3
  80. package/src/WorkflowExecutor.ts +13 -10
  81. package/src/contracts/ITaskGraphValidator.ts +12 -12
  82. package/src/contracts/ValidationError.ts +6 -6
  83. package/src/contracts/ValidationResult.ts +4 -4
  84. package/src/strategies/DryRunExecutionStrategy.ts +3 -3
  85. package/src/strategies/RetryingExecutionStrategy.ts +15 -9
  86. package/src/strategies/StandardExecutionStrategy.ts +4 -4
  87. package/test-report.xml +108 -108
@@ -1,9 +1,11 @@
1
1
  # Change: Add Comprehensive Integration Tests
2
2
 
3
3
  ## Why
4
+
4
5
  The current test suite relies heavily on unit tests and mocks. To ensure robust behavior in real-world scenarios, we need comprehensive integration tests that execute full task graphs without mocks, validating complex configurations and interactions.
5
6
 
6
7
  ## What Changes
8
+
7
9
  - Create a dedicated `tests/integration-tests/` directory.
8
10
  - Implement 10-20 integration test scenarios covering:
9
11
  - Linear and branching dependencies.
@@ -14,5 +16,6 @@ The current test suite relies heavily on unit tests and mocks. To ensure robust
14
16
  - Error recovery (if retry policy is implemented, otherwise standard error states).
15
17
 
16
18
  ## Impact
19
+
17
20
  - Affected specs: `task-runner` (no functional changes to the runtime, but validates existing specs)
18
21
  - Affected code: `tests/integration-tests/*`
@@ -1,4 +1,5 @@
1
1
  ## Implementation
2
+
2
3
  - [x] 1.1 Setup `tests/integration-tests/` directory and test runner config if needed.
3
4
  - [x] 1.2 Implement Scenario 1: Basic linear workflow (A -> B -> C) success.
4
5
  - [x] 1.3 Implement Scenario 2: Branching workflow (A -> [B, C] -> D) success.
@@ -1,9 +1,11 @@
1
1
  # Change: Add Task Retry Policy
2
2
 
3
3
  ## Why
4
+
4
5
  Tasks currently run once and fail immediately if they throw an error or return a failure status. Network glitches or transient issues can therefore cause an entire workflow to fail unnecessarily.
5
6
 
6
7
  ## What Changes
8
+
7
9
  - Add `TaskRetryConfig` interface to define retry behavior (attempts, delay, backoff).
8
10
  - Update `TaskStep` interface to include optional `retry: TaskRetryConfig`.
9
11
  - Implement `RetryingExecutionStrategy` which decorates any `IExecutionStrategy`.
@@ -12,5 +14,6 @@ Tasks currently run once and fail immediately if they throw an error or return a
12
14
  - It waits and re-executes the step if applicable.
13
15
 
14
16
  ## Impact
17
+
15
18
  - **New Components**: `RetryingExecutionStrategy`, `TaskRetryConfig`
16
19
  - **Affected Components**: `TaskStep` (interface update)
@@ -1,4 +1,5 @@
1
1
  ## Implementation
2
+
2
3
  - [x] 1.1 Create `TaskRetryConfig` interface with `attempts`, `delay`, and `backoff`.
3
4
  - [x] 1.2 Update `TaskStep` interface to include optional `retry: TaskRetryConfig`.
4
5
  - [x] 1.3 Update execution logic to catch task failures.
@@ -1,13 +1,16 @@
1
1
  # Change: Add Workflow Preview
2
2
 
3
3
  ## Why
4
+
4
5
  It can be difficult to understand the execution flow of complex dependency graphs just by looking at the code. Users also currently cannot easily verify the execution plan without running the side effects, which carries risk.
5
6
 
6
7
  ## What Changes
8
+
7
9
  - Add a `DryRunExecutionStrategy` which implements `IExecutionStrategy`. This allows `WorkflowExecutor` to simulate execution without side effects.
8
10
  - Add a standalone utility `generateMermaidGraph(steps: TaskStep[])` to generate a Mermaid.js diagram of the dependency graph.
9
11
  - Expose these features via the main `TaskRunner` facade if applicable, or as separate utilities.
10
12
 
11
13
  ## Impact
14
+
12
15
  - **New Components**: `DryRunExecutionStrategy`, `generateMermaidGraph`
13
16
  - **Affected Components**: `WorkflowExecutor` (indirectly, via strategy injection)
@@ -1,4 +1,5 @@
1
1
  ## Implementation
2
+
2
3
  - [x] 1.1 Update `TaskRunnerExecutionConfig` to include an optional `dryRun: boolean` property.
3
4
  - [x] 1.2 Implement `dryRun` logic in `WorkflowExecutor` (traverse graph, validate order, skip `step.run()`, return `skipped` or `success` pseudo-status).
4
5
  - [x] 1.3 Implement `getMermaidGraph(steps: TaskStep[])` method (can be static or instance method on `TaskRunner`).
@@ -1,14 +1,17 @@
1
1
  # Change: Refactor Core Architecture
2
2
 
3
3
  ## Why
4
+
4
5
  When multiple developers work on the project, conflicts arise due to tight coupling and poor separation of concerns. Large classes like `WorkflowExecutor` are taking on too many responsibilities, and there is duplicated logic around graph traversal and state management.
5
6
 
6
7
  ## What Changes
8
+
7
9
  - Decouple `WorkflowExecutor` from `EventBus` (pass a listener interface or use a mediating controller).
8
10
  - Extract `TaskExecutionStrategy` to allow pluggable execution modes (e.g., standard, dry-run, debug).
9
11
  - Centralize state management for task results and context, moving it out of the executor loop.
10
12
  - Standardize error handling and logging (QoL improvements).
11
13
 
12
14
  ## Impact
15
+
13
16
  - Affected specs: `task-runner` (no behavior change, but structural refactor)
14
17
  - Affected code: `src/WorkflowExecutor.ts`, `src/TaskRunner.ts`, new files for extracted logic.
@@ -1,4 +1,5 @@
1
1
  ## Implementation
2
+
2
3
  - [x] 1.1 Extract `TaskStateManager` to handle `TaskResult` storage, updates, and context mutations.
3
4
  - [x] 1.2 Define `IExecutionStrategy` interface for running tasks (Strategy Pattern).
4
5
  - [x] 1.3 Refactor `WorkflowExecutor` to use `TaskStateManager` and `IExecutionStrategy`.
@@ -1,25 +1,30 @@
1
1
  # Feature: Per-Task Timeout
2
2
 
3
3
  ## 🎯 User Story
4
+
4
5
  "As a developer, I want to define a maximum execution time for specific tasks so that a single hung task (e.g., a stalled network request) fails fast without blocking the rest of the independent tasks or waiting for the global workflow timeout."
5
6
 
6
7
  ## ❓ Why
8
+
7
9
  Currently, the `TaskRunner` allows a **global** timeout for the entire `execute()` call. However, this is insufficient for granular control:
10
+
8
11
  1. **Varying Latency**: Some tasks are expected to be fast (local validation), others slow (data fetching). A global timeout of 30s is too loose for the fast ones.
9
12
  2. **Boilerplate**: Developers currently have to manually implement `setTimeout`, `Promise.race`, and `AbortController` logic inside every `run()` method to handle timeouts properly.
10
13
  3. **Resilience**: A single "zombie" task can hold up the entire pipeline until the global timeout kills everything. Per-task timeouts allow failing that specific task (and skipping its dependents) while letting other independent tasks continue.
11
14
 
12
15
  ## 🛠️ What Changes
16
+
13
17
  1. **Interface Update**: Update `TaskStep<T>` to accept an optional `timeout` property (in milliseconds).
14
18
  2. **Execution Strategy**: Update `StandardExecutionStrategy` to:
15
- - Create a local timeout timer for the task.
16
- - Create a combined `AbortSignal` (merging the workflow's signal and the local timeout).
17
- - Race the task execution against the timer.
18
- - Return a specific failure result if the timeout wins.
19
+ - Create a local timeout timer for the task.
20
+ - Create a combined `AbortSignal` (merging the workflow's signal and the local timeout).
21
+ - Race the task execution against the timer.
22
+ - Return a specific failure result if the timeout wins.
19
23
 
20
24
  ## ✅ Acceptance Criteria
25
+
21
26
  - [ ] A task with `timeout: 100` must fail if the `run` method takes > 100ms.
22
27
  - [ ] The error message for a timed-out task should clearly state "Task timed out after 100ms".
23
28
  - [ ] The `AbortSignal` passed to the task's `run` method must be triggered when the timeout occurs.
24
- - [ ] If the Global Workflow is cancelled *before* the task times out, the task should receive the cancellation signal immediately.
25
- - [ ] A task completing *before* the timeout should clear the timer to prevent open handles.
29
+ - [ ] If the Global Workflow is cancelled _before_ the task times out, the task should receive the cancellation signal immediately.
30
+ - [ ] A task completing _before_ the timeout should clear the timer to prevent open handles.
@@ -11,7 +11,7 @@
11
11
  - If yes, create an `AbortController`.
12
12
  - Set a `setTimeout` to trigger the controller.
13
13
  - Use `Promise.race` (or simply pass the new signal and wait) to handle the timeout.
14
- - **Crucial**: Ensure the new signal respects the *parent* `signal` (if global cancel happens, local signal must also abort).
14
+ - **Crucial**: Ensure the new signal respects the _parent_ `signal` (if global cancel happens, local signal must also abort).
15
15
  - **Crucial**: Clean up the timer (`clearTimeout`) in a `finally` block.
16
16
 
17
17
  - [ ] **Task 3: Unit Tests**
@@ -1,32 +1,38 @@
1
1
  # Project: Task Runner
2
2
 
3
3
  ## Overview
4
+
4
5
  The 'task-runner' project is a TypeScript-based utility designed to manage and execute tasks. It incorporates features such as task cancellation, pre-execution validation, and concurrency control, providing a robust framework for workflow automation.
5
6
 
6
7
  ## Tech Stack
8
+
7
9
  - **Languages:** TypeScript 5.9.3
8
10
  - **Testing:** Vitest 4.0.17
9
11
  - **Core APIs:** AbortSignal/AbortController (Standard Web APIs for cancellation)
10
12
  - **Package Manager:** pnpm
11
13
 
12
14
  ## Architecture
15
+
13
16
  The project follows a modular architecture with distinct components for managing different aspects of task execution:
14
- - `EventBus.ts`: Handles event propagation within the system.
15
- - `TaskGraph.ts`: Represents the structure and dependencies of tasks.
16
- - `TaskGraphValidator.ts`: Ensures the validity of task graphs before execution.
17
- - `TaskRunner.ts`: Orchestrates the execution of tasks.
18
- - `WorkflowExecutor.ts`: Manages the overall workflow.
19
- - `contracts/`: Defines interfaces and types for various components, promoting loose coupling and clear API boundaries.
17
+
18
+ - `EventBus.ts`: Handles event propagation within the system.
19
+ - `TaskGraph.ts`: Represents the structure and dependencies of tasks.
20
+ - `TaskGraphValidator.ts`: Ensures the validity of task graphs before execution.
21
+ - `TaskRunner.ts`: Orchestrates the execution of tasks.
22
+ - `WorkflowExecutor.ts`: Manages the overall workflow.
23
+ - `contracts/`: Defines interfaces and types for various components, promoting loose coupling and clear API boundaries.
20
24
 
21
25
  ## Conventions
22
- - **Coding Style:** Adheres to standard TypeScript conventions, enforced by ESLint and Prettier.
23
- - **Commit Messages:** Follows conventional commits enforced by Commitlint.
24
- - **Git Hooks:** Utilizes Husky for pre-commit and commit-msg hooks.
25
- - **Testing:** Uses Vitest for unit and integration testing.
26
- - **Atomic Commits:** When working on complex multi-task features, commit after each distinct task, ensuring build, lint, and test success to establish safe rollback points.
26
+
27
+ - **Coding Style:** Adheres to standard TypeScript conventions, enforced by ESLint and Prettier.
28
+ - **Commit Messages:** Follows conventional commits enforced by Commitlint.
29
+ - **Git Hooks:** Utilizes Husky for pre-commit and commit-msg hooks.
30
+ - **Testing:** Uses Vitest for unit and integration testing.
31
+ - **Atomic Commits:** When working on complex multi-task features, commit after each distinct task, ensuring build, lint, and test success to establish safe rollback points.
27
32
 
28
33
  ## Build/Test/Run Commands
29
- - **Install Dependencies:** `pnpm install`
30
- - **Build Project:** `pnpm build`
31
- - **Run Tests:** `pnpm test`
32
- - **Lint Code:** `pnpm lint`
34
+
35
+ - **Install Dependencies:** `pnpm install`
36
+ - **Build Project:** `pnpm build`
37
+ - **Run Tests:** `pnpm test`
38
+ - **Lint Code:** `pnpm lint`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@calmo/task-runner",
3
- "version": "3.4.0",
3
+ "version": "3.4.1",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
package/src/EventBus.ts CHANGED
@@ -61,23 +61,25 @@ export class EventBus<TContext> {
61
61
  | undefined;
62
62
  if (listeners) {
63
63
  for (const listener of listeners) {
64
- try {
65
- const result = listener(data);
66
- if (result instanceof Promise) {
67
- result.catch((error) => {
68
- console.error(
69
- `Error in event listener for ${String(event)}:`,
70
- error
71
- );
72
- });
64
+ Promise.resolve().then(() => {
65
+ try {
66
+ const result = listener(data);
67
+ if (result instanceof Promise) {
68
+ result.catch((error) => {
69
+ console.error(
70
+ `Error in event listener for ${String(event)}:`,
71
+ error
72
+ );
73
+ });
74
+ }
75
+ } catch (error) {
76
+ // Prevent listener errors from bubbling up
77
+ console.error(
78
+ `Error in event listener for ${String(event)}:`,
79
+ error
80
+ );
73
81
  }
74
- } catch (error) {
75
- // Prevent listener errors from bubbling up
76
- console.error(
77
- `Error in event listener for ${String(event)}:`,
78
- error
79
- );
80
- }
82
+ });
81
83
  }
82
84
  }
83
85
  }
package/src/TaskGraph.ts CHANGED
@@ -2,18 +2,18 @@
2
2
  * Represents a single task in the task graph.
3
3
  */
4
4
  export interface Task {
5
- /** Unique identifier for the task. */
6
- id: string;
7
- /** An array of task IDs that this task directly depends on. */
8
- dependencies: string[];
9
- /** Allows for any other properties specific to the task's payload or configuration. */
10
- [key: string]: unknown;
5
+ /** Unique identifier for the task. */
6
+ id: string;
7
+ /** An array of task IDs that this task directly depends on. */
8
+ dependencies: string[];
9
+ /** Allows for any other properties specific to the task's payload or configuration. */
10
+ [key: string]: unknown;
11
11
  }
12
12
 
13
13
  /**
14
14
  * Represents the entire collection of tasks and their interdependencies.
15
15
  */
16
16
  export interface TaskGraph {
17
- /** An array of tasks that make up the graph. */
18
- tasks: Task[];
17
+ /** An array of tasks that make up the graph. */
18
+ tasks: Task[];
19
19
  }
@@ -5,7 +5,10 @@ import { ValidationResult } from "./contracts/ValidationResult.js";
5
5
  * Contains the validation result with detailed error information.
6
6
  */
7
7
  export class TaskGraphValidationError extends Error {
8
- constructor(public result: ValidationResult, message: string) {
8
+ constructor(
9
+ public result: ValidationResult,
10
+ message: string
11
+ ) {
9
12
  super(message);
10
13
  this.name = "TaskGraphValidationError";
11
14
  }
@@ -4,160 +4,165 @@ import { ValidationError } from "./contracts/ValidationError.js";
4
4
  import { TaskGraph } from "./TaskGraph.js";
5
5
 
6
6
  export class TaskGraphValidator implements ITaskGraphValidator {
7
- /**
8
- * Validates a given task graph for structural integrity.
9
- * Checks for:
10
- * 1. Duplicate task IDs.
11
- * 2. Missing dependencies (tasks that depend on non-existent IDs).
12
- * 3. Circular dependencies (cycles in the graph).
13
- *
14
- * @param taskGraph The task graph to validate.
15
- * @returns A ValidationResult object indicating the outcome of the validation.
16
- */
17
- validate(taskGraph: TaskGraph): ValidationResult {
18
- const errors: ValidationError[] = [];
19
-
20
- // 1. Check for duplicate tasks
21
- const taskIds = new Set<string>();
22
- for (const task of taskGraph.tasks) {
23
- if (taskIds.has(task.id)) {
24
- errors.push({
25
- type: "duplicate_task",
26
- message: `Duplicate task detected with ID: ${task.id}`,
27
- details: { taskId: task.id }
28
- });
29
- } else {
30
- taskIds.add(task.id);
31
- }
32
- }
7
+ /**
8
+ * Validates a given task graph for structural integrity.
9
+ * Checks for:
10
+ * 1. Duplicate task IDs.
11
+ * 2. Missing dependencies (tasks that depend on non-existent IDs).
12
+ * 3. Circular dependencies (cycles in the graph).
13
+ *
14
+ * @param taskGraph The task graph to validate.
15
+ * @returns A ValidationResult object indicating the outcome of the validation.
16
+ */
17
+ validate(taskGraph: TaskGraph): ValidationResult {
18
+ const errors: ValidationError[] = [];
19
+
20
+ // 1. Check for duplicate tasks
21
+ const taskIds = new Set<string>();
22
+ for (const task of taskGraph.tasks) {
23
+ if (taskIds.has(task.id)) {
24
+ errors.push({
25
+ type: "duplicate_task",
26
+ message: `Duplicate task detected with ID: ${task.id}`,
27
+ details: { taskId: task.id },
28
+ });
29
+ } else {
30
+ taskIds.add(task.id);
31
+ }
32
+ }
33
33
 
34
- // 2. Check for missing dependencies
35
- for (const task of taskGraph.tasks) {
36
- for (const dependenceId of task.dependencies) {
37
- if (!taskIds.has(dependenceId)) {
38
- errors.push({
39
- type: "missing_dependency",
40
- message: `Task '${task.id}' depends on missing task '${dependenceId}'`,
41
- details: { taskId: task.id, missingDependencyId: dependenceId }
42
- });
43
- }
44
- }
34
+ // 2. Check for missing dependencies
35
+ for (const task of taskGraph.tasks) {
36
+ for (const dependenceId of task.dependencies) {
37
+ if (!taskIds.has(dependenceId)) {
38
+ errors.push({
39
+ type: "missing_dependency",
40
+ message: `Task '${task.id}' depends on missing task '${dependenceId}'`,
41
+ details: { taskId: task.id, missingDependencyId: dependenceId },
42
+ });
45
43
  }
44
+ }
45
+ }
46
46
 
47
- // 3. Check for cycles
48
- // Only run cycle detection if there are no missing dependencies, otherwise we might chase non-existent nodes.
49
- const hasMissingDependencies = errors.some(e => e.type === "missing_dependency");
47
+ // 3. Check for cycles
48
+ // Only run cycle detection if there are no missing dependencies, otherwise we might chase non-existent nodes.
49
+ const hasMissingDependencies = errors.some(
50
+ (e) => e.type === "missing_dependency"
51
+ );
52
+
53
+ if (hasMissingDependencies) {
54
+ return {
55
+ isValid: errors.length === 0,
56
+ errors,
57
+ };
58
+ }
50
59
 
51
- if (hasMissingDependencies) {
52
- return {
53
- isValid: errors.length === 0,
54
- errors
55
- };
56
- }
60
+ // Build adjacency list
61
+ const adjacencyList = new Map<string, string[]>();
62
+ for (const task of taskGraph.tasks) {
63
+ adjacencyList.set(task.id, task.dependencies);
64
+ }
57
65
 
58
- // Build adjacency list
59
- const adjacencyList = new Map<string, string[]>();
60
- for (const task of taskGraph.tasks) {
61
- adjacencyList.set(task.id, task.dependencies);
62
- }
66
+ const visited = new Set<string>();
67
+ const recursionStack = new Set<string>();
68
+
69
+ for (const task of taskGraph.tasks) {
70
+ if (visited.has(task.id)) {
71
+ continue;
72
+ }
73
+
74
+ const path: string[] = [];
75
+ if (
76
+ this.detectCycle(task.id, path, visited, recursionStack, adjacencyList)
77
+ ) {
78
+ // Extract the actual cycle from the path
79
+ // The path might look like A -> B -> C -> B (if we started at A and found cycle B-C-B)
80
+ const cycleStart = path[path.length - 1];
81
+ const cycleStartIndex = path.indexOf(cycleStart);
82
+ const cyclePath = path.slice(cycleStartIndex);
83
+
84
+ errors.push({
85
+ type: "cycle",
86
+ message: `Cycle detected: ${cyclePath.join(" -> ")}`,
87
+ details: { cyclePath },
88
+ });
89
+ // Break after first cycle found to avoid spamming similar errors
90
+ break;
91
+ }
92
+ }
63
93
 
64
- const visited = new Set<string>();
65
- const recursionStack = new Set<string>();
66
-
67
- for (const task of taskGraph.tasks) {
68
- if (visited.has(task.id)) {
69
- continue;
70
- }
71
-
72
- const path: string[] = [];
73
- if (this.detectCycle(task.id, path, visited, recursionStack, adjacencyList)) {
74
- // Extract the actual cycle from the path
75
- // The path might look like A -> B -> C -> B (if we started at A and found cycle B-C-B)
76
- const cycleStart = path[path.length - 1];
77
- const cycleStartIndex = path.indexOf(cycleStart);
78
- const cyclePath = path.slice(cycleStartIndex);
79
-
80
- errors.push({
81
- type: "cycle",
82
- message: `Cycle detected: ${cyclePath.join(" -> ")}`,
83
- details: { cyclePath }
84
- });
85
- // Break after first cycle found to avoid spamming similar errors
86
- break;
87
- }
94
+ return {
95
+ isValid: errors.length === 0,
96
+ errors,
97
+ };
98
+ }
99
+
100
+ /**
101
+ * Creates a human-readable error message from a validation result.
102
+ * @param result The validation result containing errors.
103
+ * @returns A formatted error string.
104
+ */
105
+ createErrorMessage(result: ValidationResult): string {
106
+ const errorDetails = result.errors.map((e) => e.message);
107
+ return `Task graph validation failed: ${errorDetails.join("; ")}`;
108
+ }
109
+
110
+ private detectCycle(
111
+ startTaskId: string,
112
+ path: string[],
113
+ visited: Set<string>,
114
+ recursionStack: Set<string>,
115
+ adjacencyList: Map<string, string[]>
116
+ ): boolean {
117
+ // Use an explicit stack to avoid maximum call stack size exceeded errors
118
+ const stack: { taskId: string; index: number; dependencies: string[] }[] =
119
+ [];
120
+
121
+ visited.add(startTaskId);
122
+ recursionStack.add(startTaskId);
123
+ path.push(startTaskId);
124
+
125
+ stack.push({
126
+ taskId: startTaskId,
127
+ index: 0,
128
+ /* v8 ignore next */
129
+ dependencies: adjacencyList.get(startTaskId) ?? [],
130
+ });
131
+
132
+ while (stack.length > 0) {
133
+ const frame = stack[stack.length - 1];
134
+ const { taskId, dependencies } = frame;
135
+
136
+ if (frame.index < dependencies.length) {
137
+ const dependenceId = dependencies[frame.index];
138
+ frame.index++;
139
+
140
+ if (recursionStack.has(dependenceId)) {
141
+ // Cycle detected
142
+ path.push(dependenceId);
143
+ return true;
88
144
  }
89
145
 
90
- return {
91
- isValid: errors.length === 0,
92
- errors
93
- };
94
- }
95
-
96
- /**
97
- * Creates a human-readable error message from a validation result.
98
- * @param result The validation result containing errors.
99
- * @returns A formatted error string.
100
- */
101
- createErrorMessage(result: ValidationResult): string {
102
- const errorDetails = result.errors.map(e => e.message);
103
- return `Task graph validation failed: ${errorDetails.join("; ")}`;
104
- }
146
+ if (!visited.has(dependenceId)) {
147
+ visited.add(dependenceId);
148
+ recursionStack.add(dependenceId);
149
+ path.push(dependenceId);
105
150
 
106
- private detectCycle(
107
- startTaskId: string,
108
- path: string[],
109
- visited: Set<string>,
110
- recursionStack: Set<string>,
111
- adjacencyList: Map<string, string[]>
112
- ): boolean {
113
- // Use an explicit stack to avoid maximum call stack size exceeded errors
114
- const stack: { taskId: string; index: number; dependencies: string[] }[] = [];
115
-
116
- visited.add(startTaskId);
117
- recursionStack.add(startTaskId);
118
- path.push(startTaskId);
119
-
120
- stack.push({
121
- taskId: startTaskId,
151
+ stack.push({
152
+ taskId: dependenceId,
122
153
  index: 0,
123
154
  /* v8 ignore next */
124
- dependencies: adjacencyList.get(startTaskId) ?? []
125
- });
126
-
127
- while (stack.length > 0) {
128
- const frame = stack[stack.length - 1];
129
- const { taskId, dependencies } = frame;
130
-
131
- if (frame.index < dependencies.length) {
132
- const dependenceId = dependencies[frame.index];
133
- frame.index++;
134
-
135
- if (recursionStack.has(dependenceId)) {
136
- // Cycle detected
137
- path.push(dependenceId);
138
- return true;
139
- }
140
-
141
- if (!visited.has(dependenceId)) {
142
- visited.add(dependenceId);
143
- recursionStack.add(dependenceId);
144
- path.push(dependenceId);
145
-
146
- stack.push({
147
- taskId: dependenceId,
148
- index: 0,
149
- /* v8 ignore next */
150
- dependencies: adjacencyList.get(dependenceId) ?? []
151
- });
152
- }
153
- } else {
154
- // Finished all dependencies for this node
155
- recursionStack.delete(taskId);
156
- path.pop();
157
- stack.pop();
158
- }
155
+ dependencies: adjacencyList.get(dependenceId) ?? [],
156
+ });
159
157
  }
160
-
161
- return false;
158
+ } else {
159
+ // Finished all dependencies for this node
160
+ recursionStack.delete(taskId);
161
+ path.pop();
162
+ stack.pop();
163
+ }
162
164
  }
165
+
166
+ return false;
167
+ }
163
168
  }