@workglow/task-graph 0.2.13 → 0.2.15

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 (89) hide show
  1. package/dist/__tests__/public-exports.test.d.ts +7 -0
  2. package/dist/__tests__/public-exports.test.d.ts.map +1 -0
  3. package/dist/browser.js +1068 -404
  4. package/dist/browser.js.map +30 -20
  5. package/dist/bun.js +1063 -399
  6. package/dist/bun.js.map +30 -20
  7. package/dist/common.d.ts +4 -0
  8. package/dist/common.d.ts.map +1 -1
  9. package/dist/node.js +1063 -399
  10. package/dist/node.js.map +30 -20
  11. package/dist/task/EntitlementEnforcer.d.ts +37 -4
  12. package/dist/task/EntitlementEnforcer.d.ts.map +1 -1
  13. package/dist/task/EntitlementPolicy.d.ts +10 -0
  14. package/dist/task/EntitlementPolicy.d.ts.map +1 -1
  15. package/dist/task/FallbackTask.d.ts +2 -1
  16. package/dist/task/FallbackTask.d.ts.map +1 -1
  17. package/dist/task/GraphAsTask.d.ts +8 -0
  18. package/dist/task/GraphAsTask.d.ts.map +1 -1
  19. package/dist/task/IteratorTask.d.ts +26 -7
  20. package/dist/task/IteratorTask.d.ts.map +1 -1
  21. package/dist/task/IteratorTaskRunner.d.ts +1 -1
  22. package/dist/task/IteratorTaskRunner.d.ts.map +1 -1
  23. package/dist/task/MapTask.d.ts +47 -3
  24. package/dist/task/MapTask.d.ts.map +1 -1
  25. package/dist/task/ReduceTask.d.ts +10 -3
  26. package/dist/task/ReduceTask.d.ts.map +1 -1
  27. package/dist/task/Task.d.ts +4 -1
  28. package/dist/task/Task.d.ts.map +1 -1
  29. package/dist/task/TaskJSON.d.ts +10 -1
  30. package/dist/task/TaskJSON.d.ts.map +1 -1
  31. package/dist/task/WhileTask.d.ts +20 -7
  32. package/dist/task/WhileTask.d.ts.map +1 -1
  33. package/dist/task/__tests__/DataflowJson.transforms.test.d.ts +7 -0
  34. package/dist/task/__tests__/DataflowJson.transforms.test.d.ts.map +1 -0
  35. package/dist/task-graph/ConditionalBuilder.d.ts +49 -0
  36. package/dist/task-graph/ConditionalBuilder.d.ts.map +1 -0
  37. package/dist/task-graph/Conversions.d.ts.map +1 -1
  38. package/dist/task-graph/Dataflow.d.ts +41 -1
  39. package/dist/task-graph/Dataflow.d.ts.map +1 -1
  40. package/dist/task-graph/GraphSchemaUtils.d.ts +2 -2
  41. package/dist/task-graph/GraphSchemaUtils.d.ts.map +1 -1
  42. package/dist/task-graph/GraphToWorkflowCode.d.ts.map +1 -1
  43. package/dist/task-graph/TaskGraph.d.ts +6 -0
  44. package/dist/task-graph/TaskGraph.d.ts.map +1 -1
  45. package/dist/task-graph/TaskGraphRunner.d.ts.map +1 -1
  46. package/dist/task-graph/TransformRegistry.d.ts +14 -0
  47. package/dist/task-graph/TransformRegistry.d.ts.map +1 -0
  48. package/dist/task-graph/TransformTypes.d.ts +51 -0
  49. package/dist/task-graph/TransformTypes.d.ts.map +1 -0
  50. package/dist/task-graph/Workflow.d.ts +31 -3
  51. package/dist/task-graph/Workflow.d.ts.map +1 -1
  52. package/dist/task-graph/__tests__/Dataflow.streaming.test.d.ts +7 -0
  53. package/dist/task-graph/__tests__/Dataflow.streaming.test.d.ts.map +1 -0
  54. package/dist/task-graph/__tests__/Dataflow.transforms.test.d.ts +7 -0
  55. package/dist/task-graph/__tests__/Dataflow.transforms.test.d.ts.map +1 -0
  56. package/dist/task-graph/__tests__/TaskGraphRunner.transforms.test.d.ts +7 -0
  57. package/dist/task-graph/__tests__/TaskGraphRunner.transforms.test.d.ts.map +1 -0
  58. package/dist/task-graph/__tests__/TransformRegistry.test.d.ts +6 -0
  59. package/dist/task-graph/__tests__/TransformRegistry.test.d.ts.map +1 -0
  60. package/dist/task-graph/__tests__/transforms/coalesce.test.d.ts +6 -0
  61. package/dist/task-graph/__tests__/transforms/coalesce.test.d.ts.map +1 -0
  62. package/dist/task-graph/__tests__/transforms/date-conversions.test.d.ts +6 -0
  63. package/dist/task-graph/__tests__/transforms/date-conversions.test.d.ts.map +1 -0
  64. package/dist/task-graph/__tests__/transforms/index-access.test.d.ts +6 -0
  65. package/dist/task-graph/__tests__/transforms/index-access.test.d.ts.map +1 -0
  66. package/dist/task-graph/__tests__/transforms/pick.test.d.ts +6 -0
  67. package/dist/task-graph/__tests__/transforms/pick.test.d.ts.map +1 -0
  68. package/dist/task-graph/__tests__/transforms/scalar-conversions.test.d.ts +6 -0
  69. package/dist/task-graph/__tests__/transforms/scalar-conversions.test.d.ts.map +1 -0
  70. package/dist/task-graph/__tests__/transforms/string-casts.test.d.ts +6 -0
  71. package/dist/task-graph/__tests__/transforms/string-casts.test.d.ts.map +1 -0
  72. package/dist/task-graph/autoConnect.d.ts +39 -0
  73. package/dist/task-graph/autoConnect.d.ts.map +1 -0
  74. package/dist/task-graph/transforms/coalesce.d.ts +11 -0
  75. package/dist/task-graph/transforms/coalesce.d.ts.map +1 -0
  76. package/dist/task-graph/transforms/date-conversions.d.ts +12 -0
  77. package/dist/task-graph/transforms/date-conversions.d.ts.map +1 -0
  78. package/dist/task-graph/transforms/index-access.d.ts +11 -0
  79. package/dist/task-graph/transforms/index-access.d.ts.map +1 -0
  80. package/dist/task-graph/transforms/index.d.ts +18 -0
  81. package/dist/task-graph/transforms/index.d.ts.map +1 -0
  82. package/dist/task-graph/transforms/pick.d.ts +11 -0
  83. package/dist/task-graph/transforms/pick.d.ts.map +1 -0
  84. package/dist/task-graph/transforms/scalar-conversions.d.ts +10 -0
  85. package/dist/task-graph/transforms/scalar-conversions.d.ts.map +1 -0
  86. package/dist/task-graph/transforms/string-casts.d.ts +18 -0
  87. package/dist/task-graph/transforms/string-casts.d.ts.map +1 -0
  88. package/package.json +7 -7
  89. package/src/EXECUTION_MODEL.md +6 -0
@@ -0,0 +1,6 @@
1
+ /**
2
+ * @license Copyright 2025 Steven Roussey <sroussey@gmail.com>
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+ export {};
6
+ //# sourceMappingURL=string-casts.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"string-casts.test.d.ts","sourceRoot":"","sources":["../../../../src/task-graph/__tests__/transforms/string-casts.test.ts"],"names":[],"mappings":"AAAA;;;GAGG"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Steven Roussey <sroussey@gmail.com>
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import type { ITask } from "../task/ITask";
7
+ import type { TaskGraph } from "./TaskGraph";
8
+ /**
9
+ * Auto-connects two tasks based on their schemas.
10
+ * Uses multiple matching strategies:
11
+ * 1. Match by type AND port name (highest priority)
12
+ * 2. Match by specific type only (format, $id) for unmatched ports
13
+ * 3. Look back through earlier tasks for unmatched required inputs
14
+ *
15
+ * @param graph - The task graph to add dataflows to
16
+ * @param sourceTask - The source task to connect from
17
+ * @param targetTask - The target task to connect to
18
+ * @param options - Optional configuration for the auto-connect operation
19
+ * @returns Result containing matches made, any errors, and unmatched required inputs
20
+ */
21
+ export declare function autoConnect(graph: TaskGraph, sourceTask: ITask, targetTask: ITask, options?: {
22
+ /** Keys of inputs that are already provided and don't need connection */
23
+ readonly providedInputKeys?: Set<string>;
24
+ /** Keys of inputs that are already connected via dataflow (e.g., from rename) and must not be re-matched */
25
+ readonly connectedInputKeys?: Set<string>;
26
+ /** Earlier tasks to search for unmatched required inputs (in reverse chronological order) */
27
+ readonly earlierTasks?: readonly ITask[];
28
+ /**
29
+ * When true, skip `graph.addDataflow(...)` side effects and return matches
30
+ * only. Used by callers (e.g. the builder's proximity auto-connect) that
31
+ * need to know what *would* be connected without mutating the graph.
32
+ */
33
+ readonly dryRun?: boolean;
34
+ }): {
35
+ readonly matches: Map<string, string>;
36
+ readonly error?: string;
37
+ readonly unmatchedRequired: readonly string[];
38
+ };
39
+ //# sourceMappingURL=autoConnect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"autoConnect.d.ts","sourceRoot":"","sources":["../../src/task-graph/autoConnect.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAI3C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C;;;;;;;;;;;;GAYG;AACH,wBAAgB,WAAW,CACzB,KAAK,EAAE,SAAS,EAChB,UAAU,EAAE,KAAK,EACjB,UAAU,EAAE,KAAK,EACjB,OAAO,CAAC,EAAE;IACR,yEAAyE;IACzE,QAAQ,CAAC,iBAAiB,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACzC,4GAA4G;IAC5G,QAAQ,CAAC,kBAAkB,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC1C,6FAA6F;IAC7F,QAAQ,CAAC,YAAY,CAAC,EAAE,SAAS,KAAK,EAAE,CAAC;IACzC;;;;OAIG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;CAC3B,GACA;IACD,QAAQ,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,iBAAiB,EAAE,SAAS,MAAM,EAAE,CAAC;CAC/C,CA0aA"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @license Copyright 2025 Steven Roussey <sroussey@gmail.com>
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+ import type { ITransformDef } from "../TransformTypes";
6
+ interface CoalesceParams {
7
+ readonly defaultValue: unknown;
8
+ }
9
+ export declare const coalesceTransform: ITransformDef<CoalesceParams>;
10
+ export {};
11
+ //# sourceMappingURL=coalesce.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"coalesce.d.ts","sourceRoot":"","sources":["../../../src/task-graph/transforms/coalesce.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAEvD,UAAU,cAAc;IACtB,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;CAChC;AAaD,eAAO,MAAM,iBAAiB,EAAE,aAAa,CAAC,cAAc,CAW3D,CAAC"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @license Copyright 2025 Steven Roussey <sroussey@gmail.com>
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+ import type { ITransformDef } from "../TransformTypes";
6
+ interface UnixUnit {
7
+ readonly unit: "s" | "ms";
8
+ }
9
+ export declare const unixToIsoDateTransform: ITransformDef<UnixUnit>;
10
+ export declare const isoDateToUnixTransform: ITransformDef<{}>;
11
+ export {};
12
+ //# sourceMappingURL=date-conversions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"date-conversions.d.ts","sourceRoot":"","sources":["../../../src/task-graph/transforms/date-conversions.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAEvD,UAAU,QAAQ;IAChB,QAAQ,CAAC,IAAI,EAAE,GAAG,GAAG,IAAI,CAAC;CAC3B;AAUD,eAAO,MAAM,sBAAsB,EAAE,aAAa,CAAC,QAAQ,CAwB1D,CAAC;AAEF,eAAO,MAAM,sBAAsB,EAAE,aAAa,CAAC,EAAE,CAapD,CAAC"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @license Copyright 2025 Steven Roussey <sroussey@gmail.com>
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+ import type { ITransformDef } from "../TransformTypes";
6
+ interface IndexParams {
7
+ readonly index: number;
8
+ }
9
+ export declare const indexTransform: ITransformDef<IndexParams>;
10
+ export {};
11
+ //# sourceMappingURL=index-access.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index-access.d.ts","sourceRoot":"","sources":["../../../src/task-graph/transforms/index-access.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAEvD,UAAU,WAAW;IACnB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAQD,eAAO,MAAM,cAAc,EAAE,aAAa,CAAC,WAAW,CA8BrD,CAAC"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * @license Copyright 2025 Steven Roussey <sroussey@gmail.com>
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+ import { pickTransform } from "./pick";
6
+ import { indexTransform } from "./index-access";
7
+ import { coalesceTransform } from "./coalesce";
8
+ import { uppercaseTransform, lowercaseTransform, truncateTransform, substringTransform } from "./string-casts";
9
+ import { unixToIsoDateTransform, isoDateToUnixTransform } from "./date-conversions";
10
+ import { numberToStringTransform, toBooleanTransform, stringifyTransform, parseJsonTransform } from "./scalar-conversions";
11
+ export { pickTransform, indexTransform, coalesceTransform, uppercaseTransform, lowercaseTransform, truncateTransform, substringTransform, unixToIsoDateTransform, isoDateToUnixTransform, numberToStringTransform, toBooleanTransform, stringifyTransform, parseJsonTransform, };
12
+ /**
13
+ * Registers all MVP built-in transforms. Separate from registerBaseTasks so
14
+ * consumers can opt in independently (tests may want transforms without
15
+ * task registration and vice versa).
16
+ */
17
+ export declare function registerBuiltInTransforms(): void;
18
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/task-graph/transforms/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAC;AACvC,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,EACL,kBAAkB,EAClB,kBAAkB,EAClB,iBAAiB,EACjB,kBAAkB,EACnB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,sBAAsB,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AACpF,OAAO,EACL,uBAAuB,EACvB,kBAAkB,EAClB,kBAAkB,EAClB,kBAAkB,EACnB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EACL,aAAa,EACb,cAAc,EACd,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,EAClB,iBAAiB,EACjB,kBAAkB,EAClB,sBAAsB,EACtB,sBAAsB,EACtB,uBAAuB,EACvB,kBAAkB,EAClB,kBAAkB,EAClB,kBAAkB,GACnB,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,yBAAyB,IAAI,IAAI,CAiBhD"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @license Copyright 2025 Steven Roussey <sroussey@gmail.com>
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+ import type { ITransformDef } from "../TransformTypes";
6
+ interface PickParams {
7
+ readonly path: string;
8
+ }
9
+ export declare const pickTransform: ITransformDef<PickParams>;
10
+ export {};
11
+ //# sourceMappingURL=pick.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pick.d.ts","sourceRoot":"","sources":["../../../src/task-graph/transforms/pick.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAEvD,UAAU,UAAU;IAClB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AA0BD,eAAO,MAAM,aAAa,EAAE,aAAa,CAAC,UAAU,CAiCnD,CAAC"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @license Copyright 2025 Steven Roussey <sroussey@gmail.com>
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+ import type { ITransformDef } from "../TransformTypes";
6
+ export declare const numberToStringTransform: ITransformDef<{}>;
7
+ export declare const toBooleanTransform: ITransformDef<{}>;
8
+ export declare const stringifyTransform: ITransformDef<{}>;
9
+ export declare const parseJsonTransform: ITransformDef<{}>;
10
+ //# sourceMappingURL=scalar-conversions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scalar-conversions.d.ts","sourceRoot":"","sources":["../../../src/task-graph/transforms/scalar-conversions.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAKvD,eAAO,MAAM,uBAAuB,EAAE,aAAa,CAAC,EAAE,CAerD,CAAC;AAEF,eAAO,MAAM,kBAAkB,EAAE,aAAa,CAAC,EAAE,CAYhD,CAAC;AAEF,eAAO,MAAM,kBAAkB,EAAE,aAAa,CAAC,EAAE,CAWhD,CAAC;AAEF,eAAO,MAAM,kBAAkB,EAAE,aAAa,CAAC,EAAE,CAOhD,CAAC"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * @license Copyright 2025 Steven Roussey <sroussey@gmail.com>
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+ import type { ITransformDef } from "../TransformTypes";
6
+ export declare const uppercaseTransform: ITransformDef<{}>;
7
+ export declare const lowercaseTransform: ITransformDef<{}>;
8
+ interface TruncateParams {
9
+ readonly max: number;
10
+ }
11
+ export declare const truncateTransform: ITransformDef<TruncateParams>;
12
+ interface SubstringParams {
13
+ readonly start: number;
14
+ readonly end: number | undefined;
15
+ }
16
+ export declare const substringTransform: ITransformDef<SubstringParams>;
17
+ export {};
18
+ //# sourceMappingURL=string-casts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"string-casts.d.ts","sourceRoot":"","sources":["../../../src/task-graph/transforms/string-casts.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAIvD,eAAO,MAAM,kBAAkB,EAAE,aAAa,CAAC,EAAE,CAOhD,CAAC;AAEF,eAAO,MAAM,kBAAkB,EAAE,aAAa,CAAC,EAAE,CAOhD,CAAC;AAEF,UAAU,cAAc;IACtB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED,eAAO,MAAM,iBAAiB,EAAE,aAAa,CAAC,cAAc,CAW3D,CAAC;AAEF,UAAU,eAAe;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAC;CAClC;AAED,eAAO,MAAM,kBAAkB,EAAE,aAAa,CAAC,eAAe,CAc7D,CAAC"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@workglow/task-graph",
3
3
  "type": "module",
4
- "version": "0.2.13",
4
+ "version": "0.2.15",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "https://github.com/workglow-dev/workglow.git",
@@ -51,9 +51,9 @@
51
51
  "access": "public"
52
52
  },
53
53
  "peerDependencies": {
54
- "@workglow/job-queue": "0.2.13",
55
- "@workglow/storage": "0.2.13",
56
- "@workglow/util": "0.2.13"
54
+ "@workglow/job-queue": "0.2.15",
55
+ "@workglow/storage": "0.2.15",
56
+ "@workglow/util": "0.2.15"
57
57
  },
58
58
  "peerDependenciesMeta": {
59
59
  "@workglow/job-queue": {
@@ -67,8 +67,8 @@
67
67
  }
68
68
  },
69
69
  "devDependencies": {
70
- "@workglow/job-queue": "0.2.13",
71
- "@workglow/storage": "0.2.13",
72
- "@workglow/util": "0.2.13"
70
+ "@workglow/job-queue": "0.2.15",
71
+ "@workglow/storage": "0.2.15",
72
+ "@workglow/util": "0.2.15"
73
73
  }
74
74
  }
@@ -288,6 +288,12 @@ This ensures:
288
288
 
289
289
  ## Key Invariants
290
290
 
291
+ ### 0. Cycle Guarantees
292
+
293
+ - `TaskGraph` is a `DirectedAcyclicGraph`. The underlying `TaskGraphDAG` extends `DirectedAcyclicGraph` from `@workglow/util/graph`.
294
+ - `TaskGraph.addDataflow` throws `CycleError` **synchronously** whenever the new edge would close a cycle. Detection runs inside `DirectedAcyclicGraph.addEdge` via `wouldAddingEdgeCreateCycle`, so no graph can ever reach a cyclic state — cycles are rejected at the construction call, not at run time.
295
+ - Loop tasks (`WhileTask`, `IteratorTask`, `MapTask`, `ReduceTask`) achieve repetition by re-running an internally-acyclic subgraph once per iteration, never by adding back-edges. Each subgraph is its own `TaskGraph` and inherits the same invariant. `GraphAsTask.validateAcyclic()` re-asserts the invariant when the subgraph is finalized, so any direct `_dag` manipulation is caught before execution.
296
+
291
297
  ### 1. COMPLETED Tasks Are Immutable
292
298
 
293
299
  Once a task's `run()` completes and status becomes `COMPLETED`: