@calmo/task-runner 1.1.0 → 1.2.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/.github/workflows/ci.yml +7 -0
- package/CHANGELOG.md +42 -0
- package/GEMINI.md +5 -1
- package/README.md +20 -0
- package/coverage/TaskGraphValidator.ts.html +463 -0
- package/coverage/TaskRunner.ts.html +163 -52
- package/coverage/coverage-final.json +2 -1
- package/coverage/index.html +22 -7
- package/coverage/lcov-report/TaskGraphValidator.ts.html +463 -0
- package/coverage/lcov-report/TaskRunner.ts.html +796 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +131 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov.info +199 -0
- package/dist/TaskGraph.d.ts +18 -0
- package/dist/TaskGraph.js +2 -0
- package/dist/TaskGraph.js.map +1 -0
- package/dist/TaskGraphValidator.d.ts +17 -0
- package/dist/TaskGraphValidator.js +103 -0
- package/dist/TaskGraphValidator.js.map +1 -0
- package/dist/TaskRunner.d.ts +1 -0
- package/dist/TaskRunner.js +44 -9
- package/dist/TaskRunner.js.map +1 -1
- package/dist/contracts/ITaskGraphValidator.d.ts +13 -0
- package/dist/contracts/ITaskGraphValidator.js +2 -0
- package/dist/contracts/ITaskGraphValidator.js.map +1 -0
- package/dist/contracts/ValidationError.d.ts +11 -0
- package/dist/contracts/ValidationError.js +2 -0
- package/dist/contracts/ValidationError.js.map +1 -0
- package/dist/contracts/ValidationResult.d.ts +10 -0
- package/dist/contracts/ValidationResult.js +2 -0
- package/dist/contracts/ValidationResult.js.map +1 -0
- package/package.json +1 -1
- package/sonar-project.properties +22 -0
- package/src/TaskGraph.ts +19 -0
- package/src/TaskGraphValidator.ts +126 -0
- package/src/TaskRunner.ts +50 -13
- package/src/contracts/ITaskGraphValidator.ts +14 -0
- package/src/contracts/ValidationError.ts +11 -0
- package/src/contracts/ValidationResult.ts +11 -0
- package/test-report.xml +87 -0
package/.github/workflows/ci.yml
CHANGED
|
@@ -13,6 +13,8 @@ jobs:
|
|
|
13
13
|
steps:
|
|
14
14
|
- name: Checkout repository
|
|
15
15
|
uses: actions/checkout@v6
|
|
16
|
+
with:
|
|
17
|
+
fetch-depth: 0
|
|
16
18
|
|
|
17
19
|
- name: Install pnpm
|
|
18
20
|
uses: pnpm/action-setup@v4
|
|
@@ -37,6 +39,11 @@ jobs:
|
|
|
37
39
|
- name: Test
|
|
38
40
|
run: pnpm test
|
|
39
41
|
|
|
42
|
+
- name: SonarQube Scan
|
|
43
|
+
uses: SonarSource/sonarqube-scan-action@v6
|
|
44
|
+
env:
|
|
45
|
+
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
|
46
|
+
|
|
40
47
|
- name: Upload coverage reports to Codecov
|
|
41
48
|
uses: codecov/codecov-action@v5
|
|
42
49
|
with:
|
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,45 @@
|
|
|
1
|
+
## 1.2.0 (2026-01-18)
|
|
2
|
+
|
|
3
|
+
* Merge branch 'main' into 004-pre-execution-validation-14850747355617832302 ([cc01409](https://github.com/thalesraymond/task-runner/commit/cc01409))
|
|
4
|
+
* Merge branch 'main' into 004-pre-execution-validation-14850747355617832302 ([0a31f84](https://github.com/thalesraymond/task-runner/commit/0a31f84))
|
|
5
|
+
* Merge pull request #25 from thalesraymond/004-pre-execution-validation-14850747355617832302 ([8c022d1](https://github.com/thalesraymond/task-runner/commit/8c022d1)), closes [#25](https://github.com/thalesraymond/task-runner/issues/25)
|
|
6
|
+
* refactor: address PR comments, extract TaskGraph ([f2d506f](https://github.com/thalesraymond/task-runner/commit/f2d506f))
|
|
7
|
+
* refactor: address PR review feedback ([38e7878](https://github.com/thalesraymond/task-runner/commit/38e7878))
|
|
8
|
+
* refactor: split validation contracts into individual files ([c9caea0](https://github.com/thalesraymond/task-runner/commit/c9caea0))
|
|
9
|
+
* feat: pre-validation of task graph for cycles and missing deps ([4680c1b](https://github.com/thalesraymond/task-runner/commit/4680c1b)), closes [#004](https://github.com/thalesraymond/task-runner/issues/004)
|
|
10
|
+
|
|
11
|
+
## <small>1.1.1 (2026-01-18)</small>
|
|
12
|
+
|
|
13
|
+
* Merge branch 'main' into chore/configure-sonar-quality-gate-6593988281334866666 ([fe5c88d](https://github.com/thalesraymond/task-runner/commit/fe5c88d))
|
|
14
|
+
* Merge pull request #23 from thalesraymond/004-pre-execution-validation ([bf641c5](https://github.com/thalesraymond/task-runner/commit/bf641c5)), closes [#23](https://github.com/thalesraymond/task-runner/issues/23)
|
|
15
|
+
* Merge pull request #26 from thalesraymond/002-task-cancellation ([30fafaa](https://github.com/thalesraymond/task-runner/commit/30fafaa)), closes [#26](https://github.com/thalesraymond/task-runner/issues/26)
|
|
16
|
+
* Merge pull request #28 from thalesraymond/chore/configure-sonar-quality-gate-6593988281334866666 ([0b7373f](https://github.com/thalesraymond/task-runner/commit/0b7373f)), closes [#28](https://github.com/thalesraymond/task-runner/issues/28)
|
|
17
|
+
* Merge pull request #29 from thalesraymond/update-docs-and-constitution-15453351708375382519 ([84b6293](https://github.com/thalesraymond/task-runner/commit/84b6293)), closes [#29](https://github.com/thalesraymond/task-runner/issues/29)
|
|
18
|
+
* Update GEMINI.md ([64c5660](https://github.com/thalesraymond/task-runner/commit/64c5660))
|
|
19
|
+
* Update GEMINI.md ([e28abbd](https://github.com/thalesraymond/task-runner/commit/e28abbd))
|
|
20
|
+
* Update specs/002-task-cancellation/quickstart.md ([c52ca0d](https://github.com/thalesraymond/task-runner/commit/c52ca0d))
|
|
21
|
+
* Update specs/002-task-cancellation/quickstart.md ([153981a](https://github.com/thalesraymond/task-runner/commit/153981a))
|
|
22
|
+
* Update specs/004-pre-execution-validation/checklists/requirements.md ([e1cd90f](https://github.com/thalesraymond/task-runner/commit/e1cd90f))
|
|
23
|
+
* Update specs/004-pre-execution-validation/checklists/requirements.md ([03869b8](https://github.com/thalesraymond/task-runner/commit/03869b8))
|
|
24
|
+
* Update specs/004-pre-execution-validation/contracts/api.ts ([b217a10](https://github.com/thalesraymond/task-runner/commit/b217a10))
|
|
25
|
+
* Update specs/004-pre-execution-validation/contracts/api.ts ([503309e](https://github.com/thalesraymond/task-runner/commit/503309e))
|
|
26
|
+
* Update specs/004-pre-execution-validation/contracts/api.ts ([6394f0d](https://github.com/thalesraymond/task-runner/commit/6394f0d))
|
|
27
|
+
* Update specs/004-pre-execution-validation/quickstart.md ([4756fbc](https://github.com/thalesraymond/task-runner/commit/4756fbc))
|
|
28
|
+
* Update specs/004-pre-execution-validation/quickstart.md ([9233e8c](https://github.com/thalesraymond/task-runner/commit/9233e8c))
|
|
29
|
+
* Update specs/004-pre-execution-validation/quickstart.md ([1d639d2](https://github.com/thalesraymond/task-runner/commit/1d639d2))
|
|
30
|
+
* Update specs/004-pre-execution-validation/spec.md ([74875de](https://github.com/thalesraymond/task-runner/commit/74875de))
|
|
31
|
+
* Update specs/004-pre-execution-validation/tasks.md ([93ee4a7](https://github.com/thalesraymond/task-runner/commit/93ee4a7))
|
|
32
|
+
* fix: address PR comments on sonar config ([6eab0b5](https://github.com/thalesraymond/task-runner/commit/6eab0b5))
|
|
33
|
+
* fix: address PR comments on sonar config ([156eea0](https://github.com/thalesraymond/task-runner/commit/156eea0))
|
|
34
|
+
* docs: ✏️ fix gemini code review discussion ([a788e47](https://github.com/thalesraymond/task-runner/commit/a788e47))
|
|
35
|
+
* docs: ✏️ fix gemini inconsistent pointed by gemini ([ddc39b7](https://github.com/thalesraymond/task-runner/commit/ddc39b7))
|
|
36
|
+
* docs: ✏️ fix inconsistences pointed in CR ([95acba7](https://github.com/thalesraymond/task-runner/commit/95acba7))
|
|
37
|
+
* docs: ✏️ signal abort spec ([57f1cbf](https://github.com/thalesraymond/task-runner/commit/57f1cbf))
|
|
38
|
+
* docs: ✏️ spec for abort signal ([90340b8](https://github.com/thalesraymond/task-runner/commit/90340b8))
|
|
39
|
+
* docs: ✏️ specs for tree pre validation ([e13f056](https://github.com/thalesraymond/task-runner/commit/e13f056))
|
|
40
|
+
* docs: update constitution and README with latest features ([7b601fc](https://github.com/thalesraymond/task-runner/commit/7b601fc))
|
|
41
|
+
* chore: configure SonarCloud quality gate and coverage ([e08912b](https://github.com/thalesraymond/task-runner/commit/e08912b))
|
|
42
|
+
|
|
1
43
|
## 1.1.0 (2026-01-18)
|
|
2
44
|
|
|
3
45
|
* Merge branch 'main' into feat/task-runner-observer-pattern-15088823676344229252 ([d38d6f8](https://github.com/thalesraymond/task-runner/commit/d38d6f8))
|
package/GEMINI.md
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
Auto-generated from all feature plans. Last updated: 2026-01-17
|
|
4
4
|
|
|
5
5
|
## Active Technologies
|
|
6
|
+
- TypeScript 5.9.3 + vitest 4.0.17 (for testing) (004-pre-execution-validation)
|
|
7
|
+
- TypeScript 5.9.3 + vitest 4.0.17, AbortSignal/AbortController (standard Web APIs) (002-task-cancellation)
|
|
8
|
+
- N/A (in-memory context object) (002-task-cancellation)
|
|
6
9
|
|
|
7
10
|
- TypeScript 5.9.3 + vitest 4.0.17 (003-refactor-file-structure)
|
|
8
11
|
|
|
@@ -24,10 +27,11 @@ tests/
|
|
|
24
27
|
: Follow standard conventions
|
|
25
28
|
|
|
26
29
|
## Recent Changes
|
|
30
|
+
- 002-task-cancellation: Added TypeScript 5.9.3 + vitest 4.0.17, AbortSignal/AbortController (standard Web APIs)
|
|
31
|
+
- 004-pre-execution-validation: Added TypeScript 5.9.3 + vitest 4.0.17 (for testing)
|
|
27
32
|
|
|
28
33
|
- 003-refactor-file-structure: Added TypeScript 5.9.3 + vitest 4.0.17
|
|
29
34
|
|
|
30
|
-
- 001-generic-task-runner: Added
|
|
31
35
|
|
|
32
36
|
<!-- MANUAL ADDITIONS START -->
|
|
33
37
|
<!-- MANUAL ADDITIONS END -->
|
package/README.md
CHANGED
|
@@ -9,6 +9,22 @@ A lightweight, type-safe, and domain-agnostic task orchestration engine. It reso
|
|
|
9
9
|
- **Parallel Execution**: Automatically identifies and runs independent steps concurrently.
|
|
10
10
|
- **Dependency Management**: Enforces execution order based on dependencies.
|
|
11
11
|
- **Error Handling & Skipping**: robustly handles failures and automatically skips dependent steps.
|
|
12
|
+
- **Event System**: Subscribe to lifecycle events (`workflowStart`, `taskStart`, `taskEnd`, etc.) for logging or monitoring.
|
|
13
|
+
- **Runtime Validation**: Automatically detects circular dependencies and missing dependencies before execution loops.
|
|
14
|
+
|
|
15
|
+
## Event System
|
|
16
|
+
|
|
17
|
+
The `TaskRunner` implements an Observer Pattern, allowing you to subscribe to various lifecycle events.
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
runner.on("taskStart", ({ step }) => {
|
|
21
|
+
console.log(`Starting step: ${step.name}`);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
runner.on("taskEnd", ({ step, result }) => {
|
|
25
|
+
console.log(`Step ${step.name} finished with status: ${result.status}`);
|
|
26
|
+
});
|
|
27
|
+
```
|
|
12
28
|
|
|
13
29
|
## Usage Example
|
|
14
30
|
|
|
@@ -77,6 +93,10 @@ async function main() {
|
|
|
77
93
|
main();
|
|
78
94
|
```
|
|
79
95
|
|
|
96
|
+
## Skip Propagation
|
|
97
|
+
|
|
98
|
+
If a task fails or is skipped, the `TaskRunner` automatically marks all subsequent tasks that depend on it as `skipped`. This ensures that your pipeline doesn't attempt to run steps with missing prerequisites, saving resources and preventing cascading errors.
|
|
99
|
+
|
|
80
100
|
## Context Hydration
|
|
81
101
|
|
|
82
102
|
One nice thing to do is to avoid optional parameters and excessive use of ```!``` operator, with task dependencies we can chain our steps and context usages to make sure steps are executed only when pre requisites are met.
|
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
|
|
2
|
+
<!doctype html>
|
|
3
|
+
<html lang="en">
|
|
4
|
+
|
|
5
|
+
<head>
|
|
6
|
+
<title>Code coverage report for TaskGraphValidator.ts</title>
|
|
7
|
+
<meta charset="utf-8" />
|
|
8
|
+
<link rel="stylesheet" href="prettify.css" />
|
|
9
|
+
<link rel="stylesheet" href="base.css" />
|
|
10
|
+
<link rel="shortcut icon" type="image/x-icon" href="favicon.png" />
|
|
11
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
12
|
+
<style type='text/css'>
|
|
13
|
+
.coverage-summary .sorter {
|
|
14
|
+
background-image: url(sort-arrow-sprite.png);
|
|
15
|
+
}
|
|
16
|
+
</style>
|
|
17
|
+
</head>
|
|
18
|
+
|
|
19
|
+
<body>
|
|
20
|
+
<div class='wrapper'>
|
|
21
|
+
<div class='pad1'>
|
|
22
|
+
<h1><a href="index.html">All files</a> TaskGraphValidator.ts</h1>
|
|
23
|
+
<div class='clearfix'>
|
|
24
|
+
|
|
25
|
+
<div class='fl pad1y space-right2'>
|
|
26
|
+
<span class="strong">100% </span>
|
|
27
|
+
<span class="quiet">Statements</span>
|
|
28
|
+
<span class='fraction'>43/43</span>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
<div class='fl pad1y space-right2'>
|
|
33
|
+
<span class="strong">100% </span>
|
|
34
|
+
<span class="quiet">Branches</span>
|
|
35
|
+
<span class='fraction'>16/16</span>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
<div class='fl pad1y space-right2'>
|
|
40
|
+
<span class="strong">100% </span>
|
|
41
|
+
<span class="quiet">Functions</span>
|
|
42
|
+
<span class='fraction'>3/3</span>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
<div class='fl pad1y space-right2'>
|
|
47
|
+
<span class="strong">100% </span>
|
|
48
|
+
<span class="quiet">Lines</span>
|
|
49
|
+
<span class='fraction'>42/42</span>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
</div>
|
|
54
|
+
<p class="quiet">
|
|
55
|
+
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
|
|
56
|
+
</p>
|
|
57
|
+
<template id="filterTemplate">
|
|
58
|
+
<div class="quiet">
|
|
59
|
+
Filter:
|
|
60
|
+
<input type="search" id="fileSearch">
|
|
61
|
+
</div>
|
|
62
|
+
</template>
|
|
63
|
+
</div>
|
|
64
|
+
<div class='status-line high'></div>
|
|
65
|
+
<pre><table class="coverage">
|
|
66
|
+
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
|
|
67
|
+
<a name='L2'></a><a href='#L2'>2</a>
|
|
68
|
+
<a name='L3'></a><a href='#L3'>3</a>
|
|
69
|
+
<a name='L4'></a><a href='#L4'>4</a>
|
|
70
|
+
<a name='L5'></a><a href='#L5'>5</a>
|
|
71
|
+
<a name='L6'></a><a href='#L6'>6</a>
|
|
72
|
+
<a name='L7'></a><a href='#L7'>7</a>
|
|
73
|
+
<a name='L8'></a><a href='#L8'>8</a>
|
|
74
|
+
<a name='L9'></a><a href='#L9'>9</a>
|
|
75
|
+
<a name='L10'></a><a href='#L10'>10</a>
|
|
76
|
+
<a name='L11'></a><a href='#L11'>11</a>
|
|
77
|
+
<a name='L12'></a><a href='#L12'>12</a>
|
|
78
|
+
<a name='L13'></a><a href='#L13'>13</a>
|
|
79
|
+
<a name='L14'></a><a href='#L14'>14</a>
|
|
80
|
+
<a name='L15'></a><a href='#L15'>15</a>
|
|
81
|
+
<a name='L16'></a><a href='#L16'>16</a>
|
|
82
|
+
<a name='L17'></a><a href='#L17'>17</a>
|
|
83
|
+
<a name='L18'></a><a href='#L18'>18</a>
|
|
84
|
+
<a name='L19'></a><a href='#L19'>19</a>
|
|
85
|
+
<a name='L20'></a><a href='#L20'>20</a>
|
|
86
|
+
<a name='L21'></a><a href='#L21'>21</a>
|
|
87
|
+
<a name='L22'></a><a href='#L22'>22</a>
|
|
88
|
+
<a name='L23'></a><a href='#L23'>23</a>
|
|
89
|
+
<a name='L24'></a><a href='#L24'>24</a>
|
|
90
|
+
<a name='L25'></a><a href='#L25'>25</a>
|
|
91
|
+
<a name='L26'></a><a href='#L26'>26</a>
|
|
92
|
+
<a name='L27'></a><a href='#L27'>27</a>
|
|
93
|
+
<a name='L28'></a><a href='#L28'>28</a>
|
|
94
|
+
<a name='L29'></a><a href='#L29'>29</a>
|
|
95
|
+
<a name='L30'></a><a href='#L30'>30</a>
|
|
96
|
+
<a name='L31'></a><a href='#L31'>31</a>
|
|
97
|
+
<a name='L32'></a><a href='#L32'>32</a>
|
|
98
|
+
<a name='L33'></a><a href='#L33'>33</a>
|
|
99
|
+
<a name='L34'></a><a href='#L34'>34</a>
|
|
100
|
+
<a name='L35'></a><a href='#L35'>35</a>
|
|
101
|
+
<a name='L36'></a><a href='#L36'>36</a>
|
|
102
|
+
<a name='L37'></a><a href='#L37'>37</a>
|
|
103
|
+
<a name='L38'></a><a href='#L38'>38</a>
|
|
104
|
+
<a name='L39'></a><a href='#L39'>39</a>
|
|
105
|
+
<a name='L40'></a><a href='#L40'>40</a>
|
|
106
|
+
<a name='L41'></a><a href='#L41'>41</a>
|
|
107
|
+
<a name='L42'></a><a href='#L42'>42</a>
|
|
108
|
+
<a name='L43'></a><a href='#L43'>43</a>
|
|
109
|
+
<a name='L44'></a><a href='#L44'>44</a>
|
|
110
|
+
<a name='L45'></a><a href='#L45'>45</a>
|
|
111
|
+
<a name='L46'></a><a href='#L46'>46</a>
|
|
112
|
+
<a name='L47'></a><a href='#L47'>47</a>
|
|
113
|
+
<a name='L48'></a><a href='#L48'>48</a>
|
|
114
|
+
<a name='L49'></a><a href='#L49'>49</a>
|
|
115
|
+
<a name='L50'></a><a href='#L50'>50</a>
|
|
116
|
+
<a name='L51'></a><a href='#L51'>51</a>
|
|
117
|
+
<a name='L52'></a><a href='#L52'>52</a>
|
|
118
|
+
<a name='L53'></a><a href='#L53'>53</a>
|
|
119
|
+
<a name='L54'></a><a href='#L54'>54</a>
|
|
120
|
+
<a name='L55'></a><a href='#L55'>55</a>
|
|
121
|
+
<a name='L56'></a><a href='#L56'>56</a>
|
|
122
|
+
<a name='L57'></a><a href='#L57'>57</a>
|
|
123
|
+
<a name='L58'></a><a href='#L58'>58</a>
|
|
124
|
+
<a name='L59'></a><a href='#L59'>59</a>
|
|
125
|
+
<a name='L60'></a><a href='#L60'>60</a>
|
|
126
|
+
<a name='L61'></a><a href='#L61'>61</a>
|
|
127
|
+
<a name='L62'></a><a href='#L62'>62</a>
|
|
128
|
+
<a name='L63'></a><a href='#L63'>63</a>
|
|
129
|
+
<a name='L64'></a><a href='#L64'>64</a>
|
|
130
|
+
<a name='L65'></a><a href='#L65'>65</a>
|
|
131
|
+
<a name='L66'></a><a href='#L66'>66</a>
|
|
132
|
+
<a name='L67'></a><a href='#L67'>67</a>
|
|
133
|
+
<a name='L68'></a><a href='#L68'>68</a>
|
|
134
|
+
<a name='L69'></a><a href='#L69'>69</a>
|
|
135
|
+
<a name='L70'></a><a href='#L70'>70</a>
|
|
136
|
+
<a name='L71'></a><a href='#L71'>71</a>
|
|
137
|
+
<a name='L72'></a><a href='#L72'>72</a>
|
|
138
|
+
<a name='L73'></a><a href='#L73'>73</a>
|
|
139
|
+
<a name='L74'></a><a href='#L74'>74</a>
|
|
140
|
+
<a name='L75'></a><a href='#L75'>75</a>
|
|
141
|
+
<a name='L76'></a><a href='#L76'>76</a>
|
|
142
|
+
<a name='L77'></a><a href='#L77'>77</a>
|
|
143
|
+
<a name='L78'></a><a href='#L78'>78</a>
|
|
144
|
+
<a name='L79'></a><a href='#L79'>79</a>
|
|
145
|
+
<a name='L80'></a><a href='#L80'>80</a>
|
|
146
|
+
<a name='L81'></a><a href='#L81'>81</a>
|
|
147
|
+
<a name='L82'></a><a href='#L82'>82</a>
|
|
148
|
+
<a name='L83'></a><a href='#L83'>83</a>
|
|
149
|
+
<a name='L84'></a><a href='#L84'>84</a>
|
|
150
|
+
<a name='L85'></a><a href='#L85'>85</a>
|
|
151
|
+
<a name='L86'></a><a href='#L86'>86</a>
|
|
152
|
+
<a name='L87'></a><a href='#L87'>87</a>
|
|
153
|
+
<a name='L88'></a><a href='#L88'>88</a>
|
|
154
|
+
<a name='L89'></a><a href='#L89'>89</a>
|
|
155
|
+
<a name='L90'></a><a href='#L90'>90</a>
|
|
156
|
+
<a name='L91'></a><a href='#L91'>91</a>
|
|
157
|
+
<a name='L92'></a><a href='#L92'>92</a>
|
|
158
|
+
<a name='L93'></a><a href='#L93'>93</a>
|
|
159
|
+
<a name='L94'></a><a href='#L94'>94</a>
|
|
160
|
+
<a name='L95'></a><a href='#L95'>95</a>
|
|
161
|
+
<a name='L96'></a><a href='#L96'>96</a>
|
|
162
|
+
<a name='L97'></a><a href='#L97'>97</a>
|
|
163
|
+
<a name='L98'></a><a href='#L98'>98</a>
|
|
164
|
+
<a name='L99'></a><a href='#L99'>99</a>
|
|
165
|
+
<a name='L100'></a><a href='#L100'>100</a>
|
|
166
|
+
<a name='L101'></a><a href='#L101'>101</a>
|
|
167
|
+
<a name='L102'></a><a href='#L102'>102</a>
|
|
168
|
+
<a name='L103'></a><a href='#L103'>103</a>
|
|
169
|
+
<a name='L104'></a><a href='#L104'>104</a>
|
|
170
|
+
<a name='L105'></a><a href='#L105'>105</a>
|
|
171
|
+
<a name='L106'></a><a href='#L106'>106</a>
|
|
172
|
+
<a name='L107'></a><a href='#L107'>107</a>
|
|
173
|
+
<a name='L108'></a><a href='#L108'>108</a>
|
|
174
|
+
<a name='L109'></a><a href='#L109'>109</a>
|
|
175
|
+
<a name='L110'></a><a href='#L110'>110</a>
|
|
176
|
+
<a name='L111'></a><a href='#L111'>111</a>
|
|
177
|
+
<a name='L112'></a><a href='#L112'>112</a>
|
|
178
|
+
<a name='L113'></a><a href='#L113'>113</a>
|
|
179
|
+
<a name='L114'></a><a href='#L114'>114</a>
|
|
180
|
+
<a name='L115'></a><a href='#L115'>115</a>
|
|
181
|
+
<a name='L116'></a><a href='#L116'>116</a>
|
|
182
|
+
<a name='L117'></a><a href='#L117'>117</a>
|
|
183
|
+
<a name='L118'></a><a href='#L118'>118</a>
|
|
184
|
+
<a name='L119'></a><a href='#L119'>119</a>
|
|
185
|
+
<a name='L120'></a><a href='#L120'>120</a>
|
|
186
|
+
<a name='L121'></a><a href='#L121'>121</a>
|
|
187
|
+
<a name='L122'></a><a href='#L122'>122</a>
|
|
188
|
+
<a name='L123'></a><a href='#L123'>123</a>
|
|
189
|
+
<a name='L124'></a><a href='#L124'>124</a>
|
|
190
|
+
<a name='L125'></a><a href='#L125'>125</a>
|
|
191
|
+
<a name='L126'></a><a href='#L126'>126</a>
|
|
192
|
+
<a name='L127'></a><a href='#L127'>127</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral"> </span>
|
|
193
|
+
<span class="cline-any cline-neutral"> </span>
|
|
194
|
+
<span class="cline-any cline-neutral"> </span>
|
|
195
|
+
<span class="cline-any cline-neutral"> </span>
|
|
196
|
+
<span class="cline-any cline-neutral"> </span>
|
|
197
|
+
<span class="cline-any cline-neutral"> </span>
|
|
198
|
+
<span class="cline-any cline-neutral"> </span>
|
|
199
|
+
<span class="cline-any cline-neutral"> </span>
|
|
200
|
+
<span class="cline-any cline-neutral"> </span>
|
|
201
|
+
<span class="cline-any cline-neutral"> </span>
|
|
202
|
+
<span class="cline-any cline-neutral"> </span>
|
|
203
|
+
<span class="cline-any cline-neutral"> </span>
|
|
204
|
+
<span class="cline-any cline-neutral"> </span>
|
|
205
|
+
<span class="cline-any cline-neutral"> </span>
|
|
206
|
+
<span class="cline-any cline-neutral"> </span>
|
|
207
|
+
<span class="cline-any cline-neutral"> </span>
|
|
208
|
+
<span class="cline-any cline-neutral"> </span>
|
|
209
|
+
<span class="cline-any cline-yes">29x</span>
|
|
210
|
+
<span class="cline-any cline-neutral"> </span>
|
|
211
|
+
<span class="cline-any cline-neutral"> </span>
|
|
212
|
+
<span class="cline-any cline-yes">29x</span>
|
|
213
|
+
<span class="cline-any cline-yes">29x</span>
|
|
214
|
+
<span class="cline-any cline-yes">61x</span>
|
|
215
|
+
<span class="cline-any cline-yes">3x</span>
|
|
216
|
+
<span class="cline-any cline-neutral"> </span>
|
|
217
|
+
<span class="cline-any cline-neutral"> </span>
|
|
218
|
+
<span class="cline-any cline-neutral"> </span>
|
|
219
|
+
<span class="cline-any cline-neutral"> </span>
|
|
220
|
+
<span class="cline-any cline-neutral"> </span>
|
|
221
|
+
<span class="cline-any cline-yes">58x</span>
|
|
222
|
+
<span class="cline-any cline-neutral"> </span>
|
|
223
|
+
<span class="cline-any cline-neutral"> </span>
|
|
224
|
+
<span class="cline-any cline-neutral"> </span>
|
|
225
|
+
<span class="cline-any cline-neutral"> </span>
|
|
226
|
+
<span class="cline-any cline-yes">29x</span>
|
|
227
|
+
<span class="cline-any cline-yes">61x</span>
|
|
228
|
+
<span class="cline-any cline-yes">41x</span>
|
|
229
|
+
<span class="cline-any cline-yes">5x</span>
|
|
230
|
+
<span class="cline-any cline-neutral"> </span>
|
|
231
|
+
<span class="cline-any cline-neutral"> </span>
|
|
232
|
+
<span class="cline-any cline-neutral"> </span>
|
|
233
|
+
<span class="cline-any cline-neutral"> </span>
|
|
234
|
+
<span class="cline-any cline-neutral"> </span>
|
|
235
|
+
<span class="cline-any cline-neutral"> </span>
|
|
236
|
+
<span class="cline-any cline-neutral"> </span>
|
|
237
|
+
<span class="cline-any cline-neutral"> </span>
|
|
238
|
+
<span class="cline-any cline-neutral"> </span>
|
|
239
|
+
<span class="cline-any cline-neutral"> </span>
|
|
240
|
+
<span class="cline-any cline-yes">29x</span>
|
|
241
|
+
<span class="cline-any cline-neutral"> </span>
|
|
242
|
+
<span class="cline-any cline-yes">29x</span>
|
|
243
|
+
<span class="cline-any cline-yes">4x</span>
|
|
244
|
+
<span class="cline-any cline-neutral"> </span>
|
|
245
|
+
<span class="cline-any cline-neutral"> </span>
|
|
246
|
+
<span class="cline-any cline-neutral"> </span>
|
|
247
|
+
<span class="cline-any cline-neutral"> </span>
|
|
248
|
+
<span class="cline-any cline-neutral"> </span>
|
|
249
|
+
<span class="cline-any cline-neutral"> </span>
|
|
250
|
+
<span class="cline-any cline-yes">25x</span>
|
|
251
|
+
<span class="cline-any cline-yes">25x</span>
|
|
252
|
+
<span class="cline-any cline-yes">54x</span>
|
|
253
|
+
<span class="cline-any cline-neutral"> </span>
|
|
254
|
+
<span class="cline-any cline-neutral"> </span>
|
|
255
|
+
<span class="cline-any cline-yes">25x</span>
|
|
256
|
+
<span class="cline-any cline-yes">25x</span>
|
|
257
|
+
<span class="cline-any cline-neutral"> </span>
|
|
258
|
+
<span class="cline-any cline-yes">25x</span>
|
|
259
|
+
<span class="cline-any cline-yes">49x</span>
|
|
260
|
+
<span class="cline-any cline-yes">3x</span>
|
|
261
|
+
<span class="cline-any cline-neutral"> </span>
|
|
262
|
+
<span class="cline-any cline-neutral"> </span>
|
|
263
|
+
<span class="cline-any cline-yes">46x</span>
|
|
264
|
+
<span class="cline-any cline-yes">46x</span>
|
|
265
|
+
<span class="cline-any cline-neutral"> </span>
|
|
266
|
+
<span class="cline-any cline-neutral"> </span>
|
|
267
|
+
<span class="cline-any cline-yes">4x</span>
|
|
268
|
+
<span class="cline-any cline-yes">4x</span>
|
|
269
|
+
<span class="cline-any cline-yes">4x</span>
|
|
270
|
+
<span class="cline-any cline-neutral"> </span>
|
|
271
|
+
<span class="cline-any cline-yes">4x</span>
|
|
272
|
+
<span class="cline-any cline-neutral"> </span>
|
|
273
|
+
<span class="cline-any cline-neutral"> </span>
|
|
274
|
+
<span class="cline-any cline-neutral"> </span>
|
|
275
|
+
<span class="cline-any cline-neutral"> </span>
|
|
276
|
+
<span class="cline-any cline-neutral"> </span>
|
|
277
|
+
<span class="cline-any cline-yes">4x</span>
|
|
278
|
+
<span class="cline-any cline-neutral"> </span>
|
|
279
|
+
<span class="cline-any cline-neutral"> </span>
|
|
280
|
+
<span class="cline-any cline-neutral"> </span>
|
|
281
|
+
<span class="cline-any cline-yes">25x</span>
|
|
282
|
+
<span class="cline-any cline-neutral"> </span>
|
|
283
|
+
<span class="cline-any cline-neutral"> </span>
|
|
284
|
+
<span class="cline-any cline-neutral"> </span>
|
|
285
|
+
<span class="cline-any cline-neutral"> </span>
|
|
286
|
+
<span class="cline-any cline-neutral"> </span>
|
|
287
|
+
<span class="cline-any cline-neutral"> </span>
|
|
288
|
+
<span class="cline-any cline-neutral"> </span>
|
|
289
|
+
<span class="cline-any cline-neutral"> </span>
|
|
290
|
+
<span class="cline-any cline-neutral"> </span>
|
|
291
|
+
<span class="cline-any cline-neutral"> </span>
|
|
292
|
+
<span class="cline-any cline-neutral"> </span>
|
|
293
|
+
<span class="cline-any cline-neutral"> </span>
|
|
294
|
+
<span class="cline-any cline-yes">51x</span>
|
|
295
|
+
<span class="cline-any cline-yes">51x</span>
|
|
296
|
+
<span class="cline-any cline-yes">51x</span>
|
|
297
|
+
<span class="cline-any cline-neutral"> </span>
|
|
298
|
+
<span class="cline-any cline-yes">51x</span>
|
|
299
|
+
<span class="cline-any cline-yes">51x</span>
|
|
300
|
+
<span class="cline-any cline-yes">33x</span>
|
|
301
|
+
<span class="cline-any cline-neutral"> </span>
|
|
302
|
+
<span class="cline-any cline-neutral"> </span>
|
|
303
|
+
<span class="cline-any cline-neutral"> </span>
|
|
304
|
+
<span class="cline-any cline-yes">5x</span>
|
|
305
|
+
<span class="cline-any cline-yes">28x</span>
|
|
306
|
+
<span class="cline-any cline-neutral"> </span>
|
|
307
|
+
<span class="cline-any cline-neutral"> </span>
|
|
308
|
+
<span class="cline-any cline-yes">4x</span>
|
|
309
|
+
<span class="cline-any cline-yes">4x</span>
|
|
310
|
+
<span class="cline-any cline-neutral"> </span>
|
|
311
|
+
<span class="cline-any cline-neutral"> </span>
|
|
312
|
+
<span class="cline-any cline-neutral"> </span>
|
|
313
|
+
<span class="cline-any cline-yes">42x</span>
|
|
314
|
+
<span class="cline-any cline-yes">42x</span>
|
|
315
|
+
<span class="cline-any cline-yes">42x</span>
|
|
316
|
+
<span class="cline-any cline-neutral"> </span>
|
|
317
|
+
<span class="cline-any cline-neutral"> </span>
|
|
318
|
+
<span class="cline-any cline-neutral"> </span></td><td class="text"><pre class="prettyprint lang-js">import { ITaskGraphValidator } from "./contracts/ITaskGraphValidator.js";
|
|
319
|
+
import { ValidationResult } from "./contracts/ValidationResult.js";
|
|
320
|
+
import { ValidationError } from "./contracts/ValidationError.js";
|
|
321
|
+
import { TaskGraph } from "./TaskGraph.js";
|
|
322
|
+
|
|
323
|
+
export class TaskGraphValidator implements ITaskGraphValidator {
|
|
324
|
+
/**
|
|
325
|
+
* Validates a given task graph for structural integrity.
|
|
326
|
+
* Checks for:
|
|
327
|
+
* 1. Duplicate task IDs.
|
|
328
|
+
* 2. Missing dependencies (tasks that depend on non-existent IDs).
|
|
329
|
+
* 3. Circular dependencies (cycles in the graph).
|
|
330
|
+
*
|
|
331
|
+
* @param taskGraph The task graph to validate.
|
|
332
|
+
* @returns A ValidationResult object indicating the outcome of the validation.
|
|
333
|
+
*/
|
|
334
|
+
validate(taskGraph: TaskGraph): ValidationResult {
|
|
335
|
+
const errors: ValidationError[] = [];
|
|
336
|
+
|
|
337
|
+
// 1. Check for duplicate tasks
|
|
338
|
+
const taskIds = new Set<string>();
|
|
339
|
+
for (const task of taskGraph.tasks) {
|
|
340
|
+
if (taskIds.has(task.id)) {
|
|
341
|
+
errors.push({
|
|
342
|
+
type: "duplicate_task",
|
|
343
|
+
message: `Duplicate task detected with ID: ${task.id}`,
|
|
344
|
+
details: { taskId: task.id }
|
|
345
|
+
});
|
|
346
|
+
} else {
|
|
347
|
+
taskIds.add(task.id);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// 2. Check for missing dependencies
|
|
352
|
+
for (const task of taskGraph.tasks) {
|
|
353
|
+
for (const dependenceId of task.dependencies) {
|
|
354
|
+
if (!taskIds.has(dependenceId)) {
|
|
355
|
+
errors.push({
|
|
356
|
+
type: "missing_dependency",
|
|
357
|
+
message: `Task '${task.id}' depends on missing task '${dependenceId}'`,
|
|
358
|
+
details: { taskId: task.id, missingDependencyId: dependenceId }
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// 3. Check for cycles
|
|
365
|
+
// Only run cycle detection if there are no missing dependencies, otherwise we might chase non-existent nodes.
|
|
366
|
+
const hasMissingDependencies = errors.some(e => e.type === "missing_dependency");
|
|
367
|
+
|
|
368
|
+
if (hasMissingDependencies) {
|
|
369
|
+
return {
|
|
370
|
+
isValid: errors.length === 0,
|
|
371
|
+
errors
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Build adjacency list
|
|
376
|
+
const adjacencyList = new Map<string, string[]>();
|
|
377
|
+
for (const task of taskGraph.tasks) {
|
|
378
|
+
adjacencyList.set(task.id, task.dependencies);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const visited = new Set<string>();
|
|
382
|
+
const recursionStack = new Set<string>();
|
|
383
|
+
|
|
384
|
+
for (const task of taskGraph.tasks) {
|
|
385
|
+
if (visited.has(task.id)) {
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const path: string[] = [];
|
|
390
|
+
if (this.detectCycle(task.id, path, visited, recursionStack, adjacencyList)) {
|
|
391
|
+
// Extract the actual cycle from the path
|
|
392
|
+
// The path might look like A -> B -> C -> B (if we started at A and found cycle B-C-B)
|
|
393
|
+
const cycleStart = path[path.length - 1];
|
|
394
|
+
const cycleStartIndex = path.indexOf(cycleStart);
|
|
395
|
+
const cyclePath = path.slice(cycleStartIndex);
|
|
396
|
+
|
|
397
|
+
errors.push({
|
|
398
|
+
type: "cycle",
|
|
399
|
+
message: `Cycle detected: ${cyclePath.join(" -> ")}`,
|
|
400
|
+
details: { cyclePath }
|
|
401
|
+
});
|
|
402
|
+
// Break after first cycle found to avoid spamming similar errors
|
|
403
|
+
break;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return {
|
|
408
|
+
isValid: errors.length === 0,
|
|
409
|
+
errors
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
private detectCycle(
|
|
414
|
+
taskId: string,
|
|
415
|
+
path: string[],
|
|
416
|
+
visited: Set<string>,
|
|
417
|
+
recursionStack: Set<string>,
|
|
418
|
+
adjacencyList: Map<string, string[]>
|
|
419
|
+
): boolean {
|
|
420
|
+
visited.add(taskId);
|
|
421
|
+
recursionStack.add(taskId);
|
|
422
|
+
path.push(taskId);
|
|
423
|
+
|
|
424
|
+
const dependencies = adjacencyList.get(taskId)!;
|
|
425
|
+
for (const dependenceId of dependencies) {
|
|
426
|
+
if (
|
|
427
|
+
!visited.has(dependenceId) &&
|
|
428
|
+
this.detectCycle(dependenceId, path, visited, recursionStack, adjacencyList)
|
|
429
|
+
) {
|
|
430
|
+
return true;
|
|
431
|
+
} else if (recursionStack.has(dependenceId)) {
|
|
432
|
+
// Cycle detected
|
|
433
|
+
// Add the dependency to complete the visual cycle
|
|
434
|
+
path.push(dependenceId);
|
|
435
|
+
return true;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
recursionStack.delete(taskId);
|
|
440
|
+
path.pop();
|
|
441
|
+
return false;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
</pre></td></tr></table></pre>
|
|
445
|
+
|
|
446
|
+
<div class='push'></div><!-- for sticky footer -->
|
|
447
|
+
</div><!-- /wrapper -->
|
|
448
|
+
<div class='footer quiet pad2 space-top1 center small'>
|
|
449
|
+
Code coverage generated by
|
|
450
|
+
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
|
|
451
|
+
at 2026-01-18T03:56:21.880Z
|
|
452
|
+
</div>
|
|
453
|
+
<script src="prettify.js"></script>
|
|
454
|
+
<script>
|
|
455
|
+
window.onload = function () {
|
|
456
|
+
prettyPrint();
|
|
457
|
+
};
|
|
458
|
+
</script>
|
|
459
|
+
<script src="sorter.js"></script>
|
|
460
|
+
<script src="block-navigation.js"></script>
|
|
461
|
+
</body>
|
|
462
|
+
</html>
|
|
463
|
+
|