@calmo/task-runner 1.2.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/.gemini/commands/openspec/apply.toml +21 -0
  2. package/.gemini/commands/openspec/archive.toml +25 -0
  3. package/.gemini/commands/openspec/proposal.toml +26 -0
  4. package/.jules/sentinel.md +4 -0
  5. package/AGENTS.md +21 -0
  6. package/CHANGELOG.md +21 -0
  7. package/GEMINI.md +10 -0
  8. package/coverage/coverage-final.json +4 -4
  9. package/coverage/index.html +9 -9
  10. package/coverage/lcov-report/index.html +9 -9
  11. package/coverage/lcov-report/src/EventBus.ts.html +4 -4
  12. package/coverage/lcov-report/src/TaskGraphValidator.ts.html +165 -54
  13. package/coverage/lcov-report/src/TaskRunner.ts.html +7 -109
  14. package/coverage/lcov-report/src/WorkflowExecutor.ts.html +169 -106
  15. package/coverage/lcov-report/src/contracts/RunnerEvents.ts.html +1 -1
  16. package/coverage/lcov-report/src/contracts/index.html +1 -1
  17. package/coverage/lcov-report/src/index.html +13 -13
  18. package/coverage/lcov.info +165 -167
  19. package/coverage/src/EventBus.ts.html +4 -4
  20. package/coverage/src/TaskGraphValidator.ts.html +165 -54
  21. package/coverage/src/TaskRunner.ts.html +7 -109
  22. package/coverage/src/WorkflowExecutor.ts.html +169 -106
  23. package/coverage/src/contracts/RunnerEvents.ts.html +1 -1
  24. package/coverage/src/contracts/index.html +1 -1
  25. package/coverage/src/index.html +13 -13
  26. package/dist/TaskGraphValidator.d.ts +6 -0
  27. package/dist/TaskGraphValidator.js +48 -16
  28. package/dist/TaskGraphValidator.js.map +1 -1
  29. package/dist/TaskRunner.js +1 -32
  30. package/dist/TaskRunner.js.map +1 -1
  31. package/dist/WorkflowExecutor.d.ts +16 -0
  32. package/dist/WorkflowExecutor.js +67 -54
  33. package/dist/WorkflowExecutor.js.map +1 -1
  34. package/dist/contracts/ITaskGraphValidator.d.ts +6 -0
  35. package/openspec/AGENTS.md +456 -0
  36. package/openspec/changes/add-external-task-cancellation/proposal.md +14 -0
  37. package/openspec/changes/add-external-task-cancellation/tasks.md +10 -0
  38. package/openspec/project.md +31 -0
  39. package/package.json +1 -1
  40. package/src/TaskGraphValidator.ts +56 -19
  41. package/src/TaskRunner.ts +1 -35
  42. package/src/WorkflowExecutor.ts +84 -63
  43. package/src/contracts/ITaskGraphValidator.ts +7 -0
  44. package/test-report.xml +51 -39
@@ -39,7 +39,7 @@
39
39
  <div class='fl pad1y space-right2'>
40
40
  <span class="strong">100% </span>
41
41
  <span class="quiet">Functions</span>
42
- <span class='fraction'>9/9</span>
42
+ <span class='fraction'>11/11</span>
43
43
  </div>
44
44
 
45
45
 
@@ -168,7 +168,28 @@
168
168
  <a name='L103'></a><a href='#L103'>103</a>
169
169
  <a name='L104'></a><a href='#L104'>104</a>
170
170
  <a name='L105'></a><a href='#L105'>105</a>
171
- <a name='L106'></a><a href='#L106'>106</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
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">&nbsp;</span>
172
193
  <span class="cline-any cline-neutral">&nbsp;</span>
173
194
  <span class="cline-any cline-neutral">&nbsp;</span>
174
195
  <span class="cline-any cline-neutral">&nbsp;</span>
@@ -177,15 +198,15 @@
177
198
  <span class="cline-any cline-neutral">&nbsp;</span>
178
199
  <span class="cline-any cline-neutral">&nbsp;</span>
179
200
  <span class="cline-any cline-neutral">&nbsp;</span>
180
- <span class="cline-any cline-yes">15x</span>
201
+ <span class="cline-any cline-yes">18x</span>
181
202
  <span class="cline-any cline-neutral">&nbsp;</span>
182
203
  <span class="cline-any cline-neutral">&nbsp;</span>
183
204
  <span class="cline-any cline-neutral">&nbsp;</span>
184
205
  <span class="cline-any cline-neutral">&nbsp;</span>
185
206
  <span class="cline-any cline-neutral">&nbsp;</span>
186
207
  <span class="cline-any cline-neutral">&nbsp;</span>
187
- <span class="cline-any cline-yes">15x</span>
188
- <span class="cline-any cline-yes">15x</span>
208
+ <span class="cline-any cline-yes">18x</span>
209
+ <span class="cline-any cline-yes">18x</span>
189
210
  <span class="cline-any cline-neutral">&nbsp;</span>
190
211
  <span class="cline-any cline-neutral">&nbsp;</span>
191
212
  <span class="cline-any cline-neutral">&nbsp;</span>
@@ -194,83 +215,104 @@
194
215
  <span class="cline-any cline-neutral">&nbsp;</span>
195
216
  <span class="cline-any cline-neutral">&nbsp;</span>
196
217
  <span class="cline-any cline-neutral">&nbsp;</span>
197
- <span class="cline-any cline-yes">15x</span>
218
+ <span class="cline-any cline-yes">18x</span>
198
219
  <span class="cline-any cline-neutral">&nbsp;</span>
199
- <span class="cline-any cline-yes">15x</span>
200
- <span class="cline-any cline-yes">15x</span>
220
+ <span class="cline-any cline-yes">18x</span>
221
+ <span class="cline-any cline-yes">18x</span>
201
222
  <span class="cline-any cline-neutral">&nbsp;</span>
202
223
  <span class="cline-any cline-neutral">&nbsp;</span>
203
- <span class="cline-any cline-yes">15x</span>
204
- <span class="cline-any cline-yes">35x</span>
205
- <span class="cline-any cline-yes">98x</span>
224
+ <span class="cline-any cline-yes">18x</span>
206
225
  <span class="cline-any cline-neutral">&nbsp;</span>
226
+ <span class="cline-any cline-yes">18x</span>
227
+ <span class="cline-any cline-neutral">&nbsp;</span>
228
+ <span class="cline-any cline-yes">25x</span>
229
+ <span class="cline-any cline-neutral">&nbsp;</span>
230
+ <span class="cline-any cline-yes">25x</span>
231
+ <span class="cline-any cline-neutral">&nbsp;</span>
232
+ <span class="cline-any cline-neutral">&nbsp;</span>
233
+ <span class="cline-any cline-yes">18x</span>
234
+ <span class="cline-any cline-yes">18x</span>
207
235
  <span class="cline-any cline-neutral">&nbsp;</span>
208
236
  <span class="cline-any cline-neutral">&nbsp;</span>
209
- <span class="cline-any cline-yes">35x</span>
210
- <span class="cline-any cline-yes">49x</span>
211
- <span class="cline-any cline-yes">49x</span>
212
- <span class="cline-any cline-yes">39x</span>
213
237
  <span class="cline-any cline-neutral">&nbsp;</span>
214
- <span class="cline-any cline-yes">49x</span>
215
- <span class="cline-any cline-yes">6x</span>
216
238
  <span class="cline-any cline-neutral">&nbsp;</span>
217
239
  <span class="cline-any cline-neutral">&nbsp;</span>
218
240
  <span class="cline-any cline-neutral">&nbsp;</span>
219
- <span class="cline-any cline-yes">6x</span>
220
- <span class="cline-any cline-yes">6x</span>
221
241
  <span class="cline-any cline-neutral">&nbsp;</span>
222
242
  <span class="cline-any cline-neutral">&nbsp;</span>
223
243
  <span class="cline-any cline-neutral">&nbsp;</span>
224
244
  <span class="cline-any cline-neutral">&nbsp;</span>
225
- <span class="cline-any cline-yes">35x</span>
226
- <span class="cline-any cline-yes">98x</span>
227
245
  <span class="cline-any cline-yes">43x</span>
228
- <span class="cline-any cline-yes">98x</span>
229
- <span class="cline-any cline-yes">29x</span>
230
246
  <span class="cline-any cline-neutral">&nbsp;</span>
247
+ <span class="cline-any cline-yes">43x</span>
248
+ <span class="cline-any cline-neutral">&nbsp;</span>
249
+ <span class="cline-any cline-yes">43x</span>
250
+ <span class="cline-any cline-yes">31x</span>
251
+ <span class="cline-any cline-yes">31x</span>
231
252
  <span class="cline-any cline-neutral">&nbsp;</span>
253
+ <span class="cline-any cline-yes">31x</span>
232
254
  <span class="cline-any cline-neutral">&nbsp;</span>
233
255
  <span class="cline-any cline-neutral">&nbsp;</span>
234
- <span class="cline-any cline-yes">35x</span>
235
- <span class="cline-any cline-yes">26x</span>
236
- <span class="cline-any cline-yes">26x</span>
237
256
  <span class="cline-any cline-neutral">&nbsp;</span>
238
- <span class="cline-any cline-yes">26x</span>
239
- <span class="cline-any cline-yes">26x</span>
240
- <span class="cline-any cline-yes">26x</span>
241
- <span class="cline-any cline-yes">24x</span>
242
257
  <span class="cline-any cline-neutral">&nbsp;</span>
243
- <span class="cline-any cline-yes">2x</span>
244
258
  <span class="cline-any cline-neutral">&nbsp;</span>
245
259
  <span class="cline-any cline-neutral">&nbsp;</span>
246
260
  <span class="cline-any cline-neutral">&nbsp;</span>
261
+ <span class="cline-any cline-yes">43x</span>
262
+ <span class="cline-any cline-yes">114x</span>
263
+ <span class="cline-any cline-neutral">&nbsp;</span>
264
+ <span class="cline-any cline-neutral">&nbsp;</span>
265
+ <span class="cline-any cline-yes">43x</span>
266
+ <span class="cline-any cline-yes">57x</span>
267
+ <span class="cline-any cline-yes">57x</span>
268
+ <span class="cline-any cline-yes">43x</span>
247
269
  <span class="cline-any cline-neutral">&nbsp;</span>
248
- <span class="cline-any cline-yes">26x</span>
249
- <span class="cline-any cline-yes">26x</span>
250
- <span class="cline-any cline-yes">26x</span>
251
270
  <span class="cline-any cline-neutral">&nbsp;</span>
271
+ <span class="cline-any cline-yes">57x</span>
272
+ <span class="cline-any cline-yes">7x</span>
252
273
  <span class="cline-any cline-neutral">&nbsp;</span>
253
274
  <span class="cline-any cline-neutral">&nbsp;</span>
254
275
  <span class="cline-any cline-neutral">&nbsp;</span>
255
- <span class="cline-any cline-yes">26x</span>
256
- <span class="cline-any cline-yes">26x</span>
276
+ <span class="cline-any cline-yes">7x</span>
277
+ <span class="cline-any cline-yes">7x</span>
257
278
  <span class="cline-any cline-neutral">&nbsp;</span>
258
- <span class="cline-any cline-yes">26x</span>
259
279
  <span class="cline-any cline-neutral">&nbsp;</span>
260
280
  <span class="cline-any cline-neutral">&nbsp;</span>
261
281
  <span class="cline-any cline-neutral">&nbsp;</span>
262
282
  <span class="cline-any cline-neutral">&nbsp;</span>
263
- <span class="cline-any cline-yes">15x</span>
264
283
  <span class="cline-any cline-neutral">&nbsp;</span>
265
- <span class="cline-any cline-yes">15x</span>
266
284
  <span class="cline-any cline-neutral">&nbsp;</span>
267
- <span class="cline-any cline-yes">20x</span>
268
285
  <span class="cline-any cline-neutral">&nbsp;</span>
269
- <span class="cline-any cline-yes">20x</span>
286
+ <span class="cline-any cline-yes">43x</span>
287
+ <span class="cline-any cline-yes">114x</span>
288
+ <span class="cline-any cline-neutral">&nbsp;</span>
289
+ <span class="cline-any cline-yes">50x</span>
290
+ <span class="cline-any cline-yes">114x</span>
291
+ <span class="cline-any cline-yes">32x</span>
292
+ <span class="cline-any cline-neutral">&nbsp;</span>
293
+ <span class="cline-any cline-neutral">&nbsp;</span>
270
294
  <span class="cline-any cline-neutral">&nbsp;</span>
271
295
  <span class="cline-any cline-neutral">&nbsp;</span>
272
- <span class="cline-any cline-yes">15x</span>
273
- <span class="cline-any cline-yes">15x</span>
296
+ <span class="cline-any cline-neutral">&nbsp;</span>
297
+ <span class="cline-any cline-neutral">&nbsp;</span>
298
+ <span class="cline-any cline-neutral">&nbsp;</span>
299
+ <span class="cline-any cline-neutral">&nbsp;</span>
300
+ <span class="cline-any cline-yes">31x</span>
301
+ <span class="cline-any cline-yes">31x</span>
302
+ <span class="cline-any cline-neutral">&nbsp;</span>
303
+ <span class="cline-any cline-yes">31x</span>
304
+ <span class="cline-any cline-yes">31x</span>
305
+ <span class="cline-any cline-yes">29x</span>
306
+ <span class="cline-any cline-neutral">&nbsp;</span>
307
+ <span class="cline-any cline-yes">2x</span>
308
+ <span class="cline-any cline-neutral">&nbsp;</span>
309
+ <span class="cline-any cline-neutral">&nbsp;</span>
310
+ <span class="cline-any cline-neutral">&nbsp;</span>
311
+ <span class="cline-any cline-neutral">&nbsp;</span>
312
+ <span class="cline-any cline-yes">31x</span>
313
+ <span class="cline-any cline-yes">31x</span>
314
+ <span class="cline-any cline-yes">31x</span>
315
+ <span class="cline-any cline-neutral">&nbsp;</span>
274
316
  <span class="cline-any cline-neutral">&nbsp;</span>
275
317
  <span class="cline-any cline-neutral">&nbsp;</span>
276
318
  <span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">import { TaskStep } from "./TaskStep.js";
@@ -304,78 +346,99 @@ export class WorkflowExecutor&lt;TContext&gt; {
304
346
  const results = new Map&lt;string, TaskResult&gt;();
305
347
  const executingPromises = new Set&lt;Promise&lt;void&gt;&gt;();
306
348
  &nbsp;
307
- // Helper to process pending steps and launch ready ones
308
- const processPendingSteps = () =&gt; {
309
- const pendingSteps = steps.filter(
310
- (step) =&gt; !results.has(step.name) &amp;&amp; !this.running.has(step.name)
311
- );
349
+ // Initial pass
350
+ this.processQueue(steps, results, executingPromises);
312
351
  &nbsp;
313
- // 1. Identify and mark skipped tasks
314
- for (const step of pendingSteps) {
315
- const deps = step.dependencies ?? [];
316
- const failedDep = deps.find(
317
- (dep) =&gt; results.has(dep) &amp;&amp; results.get(dep)?.status !== "success"
318
- );
319
- if (failedDep) {
320
- const result: TaskResult = {
321
- status: "skipped",
322
- message: `Skipped due to failed dependency: ${failedDep}`,
323
- };
324
- results.set(step.name, result);
325
- this.eventBus.emit("taskSkipped", { step, result });
326
- }
327
- }
352
+ while (results.size &lt; steps.length &amp;&amp; executingPromises.size &gt; 0) {
353
+ // Wait for the next task to finish
354
+ await Promise.race(executingPromises);
355
+ // After a task finishes, check for new work
356
+ this.processQueue(steps, results, executingPromises);
357
+ }
358
+ &nbsp;
359
+ this.eventBus.emit("workflowEnd", { context: this.context, results });
360
+ return results;
361
+ }
362
+ &nbsp;
363
+ /**
364
+ * Logic to identify tasks that can be started or must be skipped.
365
+ */
366
+ private processQueue(
367
+ steps: TaskStep&lt;TContext&gt;[],
368
+ results: Map&lt;string, TaskResult&gt;,
369
+ executingPromises: Set&lt;Promise&lt;void&gt;&gt;
370
+ ): void {
371
+ this.handleSkippedTasks(steps, results);
372
+ &nbsp;
373
+ const readySteps = this.getReadySteps(steps, results);
328
374
  &nbsp;
329
- // Re-filter pending steps as some might have been skipped above
330
- const readySteps = steps.filter((step) =&gt; {
331
- if (results.has(step.name) || this.running.has(step.name)) return false;
332
- const deps = step.dependencies ?? [];
333
- return deps.every(
334
- (dep) =&gt; results.has(dep) &amp;&amp; results.get(dep)?.status === "success"
335
- );
375
+ for (const step of readySteps) {
376
+ const taskPromise = this.runStep(step, results).then(() =&gt; {
377
+ executingPromises.delete(taskPromise);
336
378
  });
379
+ executingPromises.add(taskPromise);
380
+ }
381
+ }
337
382
  &nbsp;
338
- // 2. Launch ready tasks
339
- for (const step of readySteps) {
340
- this.running.add(step.name);
341
- this.eventBus.emit("taskStart", { step });
383
+ /**
384
+ * Identifies steps that cannot run because a dependency failed.
385
+ */
386
+ private handleSkippedTasks(steps: TaskStep&lt;TContext&gt;[], results: Map&lt;string, TaskResult&gt;): void {
387
+ const pendingSteps = steps.filter(
388
+ (step) =&gt; !results.has(step.name) &amp;&amp; !this.running.has(step.name)
389
+ );
342
390
  &nbsp;
343
- const taskPromise = (async () =&gt; {
344
- try {
345
- const result = await step.run(this.context);
346
- results.set(step.name, result);
347
- } catch (e) {
348
- results.set(step.name, {
349
- status: "failure",
350
- error: e instanceof Error ? e.message : String(e),
351
- });
352
- } finally {
353
- this.running.delete(step.name);
354
- const result = results.get(step.name)!;
355
- this.eventBus.emit("taskEnd", { step, result });
356
- }
357
- })();
391
+ for (const step of pendingSteps) {
392
+ const deps = step.dependencies ?? [];
393
+ const failedDep = deps.find(
394
+ (dep) =&gt; results.has(dep) &amp;&amp; results.get(dep)?.status !== "success"
395
+ );
358
396
  &nbsp;
359
- // Wrap the task promise to ensure we can track it in the Set
360
- const trackedPromise = taskPromise.then(() =&gt; {
361
- executingPromises.delete(trackedPromise);
362
- });
363
- executingPromises.add(trackedPromise);
397
+ if (failedDep) {
398
+ const result: TaskResult = {
399
+ status: "skipped",
400
+ message: `Skipped due to failed dependency: ${failedDep}`,
401
+ };
402
+ results.set(step.name, result);
403
+ this.eventBus.emit("taskSkipped", { step, result });
364
404
  }
365
- };
405
+ }
406
+ }
366
407
  &nbsp;
367
- // Initial check to start independent tasks
368
- processPendingSteps();
408
+ /**
409
+ * Returns steps where all dependencies have finished successfully.
410
+ */
411
+ private getReadySteps(steps: TaskStep&lt;TContext&gt;[], results: Map&lt;string, TaskResult&gt;): TaskStep&lt;TContext&gt;[] {
412
+ return steps.filter((step) =&gt; {
413
+ if (results.has(step.name) || this.running.has(step.name)) return false;
369
414
  &nbsp;
370
- while (results.size &lt; steps.length &amp;&amp; executingPromises.size &gt; 0) {
371
- // Wait for the next task to finish
372
- await Promise.race(executingPromises);
373
- // After a task finishes, check for new work
374
- processPendingSteps();
375
- }
415
+ const deps = step.dependencies ?? [];
416
+ return deps.every(
417
+ (dep) =&gt; results.has(dep) &amp;&amp; results.get(dep)?.status === "success"
418
+ );
419
+ });
420
+ }
376
421
  &nbsp;
377
- this.eventBus.emit("workflowEnd", { context: this.context, results });
378
- return results;
422
+ /**
423
+ * Handles the lifecycle of a single task execution.
424
+ */
425
+ private async runStep(step: TaskStep&lt;TContext&gt;, results: Map&lt;string, TaskResult&gt;): Promise&lt;void&gt; {
426
+ this.running.add(step.name);
427
+ this.eventBus.emit("taskStart", { step });
428
+ &nbsp;
429
+ try {
430
+ const result = await step.run(this.context);
431
+ results.set(step.name, result);
432
+ } catch (e) {
433
+ results.set(step.name, {
434
+ status: "failure",
435
+ error: e instanceof Error ? e.message : String(e),
436
+ });
437
+ } finally {
438
+ this.running.delete(step.name);
439
+ const result = results.get(step.name)!;
440
+ this.eventBus.emit("taskEnd", { step, result });
441
+ }
379
442
  }
380
443
  }
381
444
  &nbsp;</pre></td></tr></table></pre>
@@ -385,7 +448,7 @@ export class WorkflowExecutor&lt;TContext&gt; {
385
448
  <div class='footer quiet pad2 space-top1 center small'>
386
449
  Code coverage generated by
387
450
  <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
388
- at 2026-01-18T05:44:30.758Z
451
+ at 2026-01-18T15:13:46.741Z
389
452
  </div>
390
453
  <script src="../prettify.js"></script>
391
454
  <script>
@@ -202,7 +202,7 @@ export type ListenerMap&lt;TContext&gt; = {
202
202
  <div class='footer quiet pad2 space-top1 center small'>
203
203
  Code coverage generated by
204
204
  <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
205
- at 2026-01-18T05:44:30.758Z
205
+ at 2026-01-18T15:13:46.741Z
206
206
  </div>
207
207
  <script src="../../prettify.js"></script>
208
208
  <script>
@@ -101,7 +101,7 @@
101
101
  <div class='footer quiet pad2 space-top1 center small'>
102
102
  Code coverage generated by
103
103
  <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
104
- at 2026-01-18T05:44:30.758Z
104
+ at 2026-01-18T15:13:46.741Z
105
105
  </div>
106
106
  <script src="../../prettify.js"></script>
107
107
  <script>
@@ -25,28 +25,28 @@
25
25
  <div class='fl pad1y space-right2'>
26
26
  <span class="strong">100% </span>
27
27
  <span class="quiet">Statements</span>
28
- <span class='fraction'>131/131</span>
28
+ <span class='fraction'>126/126</span>
29
29
  </div>
30
30
 
31
31
 
32
32
  <div class='fl pad1y space-right2'>
33
33
  <span class="strong">100% </span>
34
34
  <span class="quiet">Branches</span>
35
- <span class='fraction'>51/51</span>
35
+ <span class='fraction'>48/48</span>
36
36
  </div>
37
37
 
38
38
 
39
39
  <div class='fl pad1y space-right2'>
40
40
  <span class="strong">100% </span>
41
41
  <span class="quiet">Functions</span>
42
- <span class='fraction'>22/22</span>
42
+ <span class='fraction'>25/25</span>
43
43
  </div>
44
44
 
45
45
 
46
46
  <div class='fl pad1y space-right2'>
47
47
  <span class="strong">100% </span>
48
48
  <span class="quiet">Lines</span>
49
- <span class='fraction'>128/128</span>
49
+ <span class='fraction'>123/123</span>
50
50
  </div>
51
51
 
52
52
 
@@ -99,13 +99,13 @@
99
99
  <div class="chart"><div class="cover-fill cover-full" style="width: 100%"></div><div class="cover-empty" style="width: 0%"></div></div>
100
100
  </td>
101
101
  <td data-value="100" class="pct high">100%</td>
102
- <td data-value="43" class="abs high">43/43</td>
102
+ <td data-value="56" class="abs high">56/56</td>
103
103
  <td data-value="100" class="pct high">100%</td>
104
104
  <td data-value="16" class="abs high">16/16</td>
105
105
  <td data-value="100" class="pct high">100%</td>
106
- <td data-value="3" class="abs high">3/3</td>
106
+ <td data-value="5" class="abs high">5/5</td>
107
107
  <td data-value="100" class="pct high">100%</td>
108
- <td data-value="42" class="abs high">42/42</td>
108
+ <td data-value="54" class="abs high">54/54</td>
109
109
  </tr>
110
110
 
111
111
  <tr>
@@ -114,13 +114,13 @@
114
114
  <div class="chart"><div class="cover-fill cover-full" style="width: 100%"></div><div class="cover-empty" style="width: 0%"></div></div>
115
115
  </td>
116
116
  <td data-value="100" class="pct high">100%</td>
117
- <td data-value="30" class="abs high">30/30</td>
117
+ <td data-value="12" class="abs high">12/12</td>
118
118
  <td data-value="100" class="pct high">100%</td>
119
- <td data-value="7" class="abs high">7/7</td>
119
+ <td data-value="4" class="abs high">4/4</td>
120
120
  <td data-value="100" class="pct high">100%</td>
121
- <td data-value="6" class="abs high">6/6</td>
121
+ <td data-value="5" class="abs high">5/5</td>
122
122
  <td data-value="100" class="pct high">100%</td>
123
- <td data-value="29" class="abs high">29/29</td>
123
+ <td data-value="12" class="abs high">12/12</td>
124
124
  </tr>
125
125
 
126
126
  <tr>
@@ -133,7 +133,7 @@
133
133
  <td data-value="100" class="pct high">100%</td>
134
134
  <td data-value="20" class="abs high">20/20</td>
135
135
  <td data-value="100" class="pct high">100%</td>
136
- <td data-value="9" class="abs high">9/9</td>
136
+ <td data-value="11" class="abs high">11/11</td>
137
137
  <td data-value="100" class="pct high">100%</td>
138
138
  <td data-value="42" class="abs high">42/42</td>
139
139
  </tr>
@@ -146,7 +146,7 @@
146
146
  <div class='footer quiet pad2 space-top1 center small'>
147
147
  Code coverage generated by
148
148
  <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
149
- at 2026-01-18T05:44:30.758Z
149
+ at 2026-01-18T15:13:46.741Z
150
150
  </div>
151
151
  <script src="../prettify.js"></script>
152
152
  <script>
@@ -13,5 +13,11 @@ export declare class TaskGraphValidator implements ITaskGraphValidator {
13
13
  * @returns A ValidationResult object indicating the outcome of the validation.
14
14
  */
15
15
  validate(taskGraph: TaskGraph): ValidationResult;
16
+ /**
17
+ * Creates a human-readable error message from a validation result.
18
+ * @param result The validation result containing errors.
19
+ * @returns A formatted error string.
20
+ */
21
+ createErrorMessage(result: ValidationResult): string;
16
22
  private detectCycle;
17
23
  }
@@ -78,25 +78,57 @@ export class TaskGraphValidator {
78
78
  errors
79
79
  };
80
80
  }
81
- detectCycle(taskId, path, visited, recursionStack, adjacencyList) {
82
- visited.add(taskId);
83
- recursionStack.add(taskId);
84
- path.push(taskId);
85
- const dependencies = adjacencyList.get(taskId);
86
- for (const dependenceId of dependencies) {
87
- if (!visited.has(dependenceId) &&
88
- this.detectCycle(dependenceId, path, visited, recursionStack, adjacencyList)) {
89
- return true;
81
+ /**
82
+ * Creates a human-readable error message from a validation result.
83
+ * @param result The validation result containing errors.
84
+ * @returns A formatted error string.
85
+ */
86
+ createErrorMessage(result) {
87
+ const errorDetails = result.errors.map(e => e.message);
88
+ return `Task graph validation failed: ${errorDetails.join("; ")}`;
89
+ }
90
+ detectCycle(startTaskId, path, visited, recursionStack, adjacencyList) {
91
+ // Use an explicit stack to avoid maximum call stack size exceeded errors
92
+ const stack = [];
93
+ visited.add(startTaskId);
94
+ recursionStack.add(startTaskId);
95
+ path.push(startTaskId);
96
+ stack.push({
97
+ taskId: startTaskId,
98
+ index: 0,
99
+ /* v8 ignore next */
100
+ dependencies: adjacencyList.get(startTaskId) ?? []
101
+ });
102
+ while (stack.length > 0) {
103
+ const frame = stack[stack.length - 1];
104
+ const { taskId, dependencies } = frame;
105
+ if (frame.index < dependencies.length) {
106
+ const dependenceId = dependencies[frame.index];
107
+ frame.index++;
108
+ if (recursionStack.has(dependenceId)) {
109
+ // Cycle detected
110
+ path.push(dependenceId);
111
+ return true;
112
+ }
113
+ if (!visited.has(dependenceId)) {
114
+ visited.add(dependenceId);
115
+ recursionStack.add(dependenceId);
116
+ path.push(dependenceId);
117
+ stack.push({
118
+ taskId: dependenceId,
119
+ index: 0,
120
+ /* v8 ignore next */
121
+ dependencies: adjacencyList.get(dependenceId) ?? []
122
+ });
123
+ }
90
124
  }
91
- else if (recursionStack.has(dependenceId)) {
92
- // Cycle detected
93
- // Add the dependency to complete the visual cycle
94
- path.push(dependenceId);
95
- return true;
125
+ else {
126
+ // Finished all dependencies for this node
127
+ recursionStack.delete(taskId);
128
+ path.pop();
129
+ stack.pop();
96
130
  }
97
131
  }
98
- recursionStack.delete(taskId);
99
- path.pop();
100
132
  return false;
101
133
  }
102
134
  }
@@ -1 +1 @@
1
- {"version":3,"file":"TaskGraphValidator.js","sourceRoot":"","sources":["../src/TaskGraphValidator.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"TaskGraphValidator.js","sourceRoot":"","sources":["../src/TaskGraphValidator.ts"],"names":[],"mappings":"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"}
@@ -49,38 +49,7 @@ export class TaskRunner {
49
49
  };
50
50
  const validationResult = this.validator.validate(taskGraph);
51
51
  if (!validationResult.isValid) {
52
- // Construct error message compatible with legacy tests
53
- const affectedTasks = new Set();
54
- const errorDetails = [];
55
- for (const error of validationResult.errors) {
56
- errorDetails.push(error.message);
57
- switch (error.type) {
58
- case "cycle": {
59
- // details is { cyclePath: string[] }
60
- const path = error.details.cyclePath;
61
- // The last element duplicates the first in the path representation, so valid unique tasks are slice(0, -1) or just all as Set handles uniq
62
- path.forEach((t) => affectedTasks.add(t));
63
- break;
64
- }
65
- case "missing_dependency": {
66
- // details is { taskId: string, missingDependencyId: string }
67
- const d = error.details;
68
- affectedTasks.add(d.taskId);
69
- break;
70
- }
71
- case "duplicate_task": {
72
- const d = error.details;
73
- affectedTasks.add(d.taskId);
74
- break;
75
- }
76
- }
77
- }
78
- // Legacy error format: "Circular dependency or missing dependency detected. Unable to run tasks: A, B"
79
- const taskList = Array.from(affectedTasks).join(", ");
80
- const legacyMessage = `Circular dependency or missing dependency detected. Unable to run tasks: ${taskList}`;
81
- const detailedMessage = `Task graph validation failed: ${errorDetails.join("; ")}`;
82
- // Combine them to satisfy both legacy tests (checking for legacy message) and new requirements (clear details)
83
- throw new Error(`${legacyMessage} | ${detailedMessage}`);
52
+ throw new Error(this.validator.createErrorMessage(validationResult));
84
53
  }
85
54
  const executor = new WorkflowExecutor(this.context, this.eventBus);
86
55
  return executor.execute(steps);