@intentius/chant 0.70.0 → 0.71.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 (85) hide show
  1. package/dist/cli/handlers/fan-out.d.ts +45 -0
  2. package/dist/cli/handlers/fan-out.d.ts.map +1 -0
  3. package/dist/cli/handlers/lifecycle.d.ts +13 -0
  4. package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
  5. package/dist/cli/handlers/operator.d.ts.map +1 -1
  6. package/dist/cli/handlers/run.d.ts +35 -0
  7. package/dist/cli/handlers/run.d.ts.map +1 -1
  8. package/dist/cli/main.d.ts.map +1 -1
  9. package/dist/cli/registry.d.ts +23 -0
  10. package/dist/cli/registry.d.ts.map +1 -1
  11. package/dist/components/deploy-units.d.ts +12 -2
  12. package/dist/components/deploy-units.d.ts.map +1 -1
  13. package/dist/components/fan-out-output.d.ts +70 -0
  14. package/dist/components/fan-out-output.d.ts.map +1 -0
  15. package/dist/components/fan-out-run.d.ts +80 -0
  16. package/dist/components/fan-out-run.d.ts.map +1 -0
  17. package/dist/components/fan-out-support.d.ts +65 -0
  18. package/dist/components/fan-out-support.d.ts.map +1 -0
  19. package/dist/components/fan-out.d.ts +194 -0
  20. package/dist/components/fan-out.d.ts.map +1 -0
  21. package/dist/components/index.d.ts +4 -0
  22. package/dist/components/index.d.ts.map +1 -1
  23. package/dist/discovery/fold-import.d.ts +96 -1
  24. package/dist/discovery/fold-import.d.ts.map +1 -1
  25. package/dist/discovery/index.d.ts.map +1 -1
  26. package/dist/fold/subset.d.ts +22 -0
  27. package/dist/fold/subset.d.ts.map +1 -1
  28. package/dist/index.d.ts +2 -0
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/lifecycle/affected.d.ts +26 -0
  31. package/dist/lifecycle/affected.d.ts.map +1 -1
  32. package/dist/op/activities/activity-contracts.d.ts +16 -1
  33. package/dist/op/activities/activity-contracts.d.ts.map +1 -1
  34. package/dist/op/activities/index.d.ts +1 -1
  35. package/dist/op/activities/index.d.ts.map +1 -1
  36. package/dist/op/activities/shell.d.ts +31 -2
  37. package/dist/op/activities/shell.d.ts.map +1 -1
  38. package/dist/op/activity-contract.d.ts +1 -1
  39. package/dist/op/activity-contract.d.ts.map +1 -1
  40. package/dist/op/activity-profiles.d.ts +19 -0
  41. package/dist/op/activity-profiles.d.ts.map +1 -1
  42. package/dist/op/builders.d.ts +21 -3
  43. package/dist/op/builders.d.ts.map +1 -1
  44. package/dist/op/gate-name.d.ts +10 -0
  45. package/dist/op/gate-name.d.ts.map +1 -1
  46. package/dist/op/step-output-ref.d.ts +25 -8
  47. package/dist/op/step-output-ref.d.ts.map +1 -1
  48. package/package.json +1 -1
  49. package/src/cli/handlers/fan-out.test.ts +394 -0
  50. package/src/cli/handlers/fan-out.ts +336 -0
  51. package/src/cli/handlers/lifecycle.test.ts +74 -1
  52. package/src/cli/handlers/lifecycle.ts +29 -1
  53. package/src/cli/handlers/operator.test.ts +22 -0
  54. package/src/cli/handlers/operator.ts +11 -3
  55. package/src/cli/handlers/run.ts +7 -1
  56. package/src/cli/main.ts +25 -3
  57. package/src/cli/registry.ts +23 -0
  58. package/src/components/deploy-units.ts +14 -4
  59. package/src/components/fan-out-output.test.ts +216 -0
  60. package/src/components/fan-out-output.ts +162 -0
  61. package/src/components/fan-out-run.test.ts +194 -0
  62. package/src/components/fan-out-run.ts +221 -0
  63. package/src/components/fan-out-support.test.ts +125 -0
  64. package/src/components/fan-out-support.ts +95 -0
  65. package/src/components/fan-out.test.ts +284 -0
  66. package/src/components/fan-out.ts +421 -0
  67. package/src/components/index.ts +33 -0
  68. package/src/discovery/fold-import.ts +195 -5
  69. package/src/discovery/index.ts +13 -5
  70. package/src/fold/subset-public-export.test.ts +31 -0
  71. package/src/fold/subset.ts +23 -0
  72. package/src/index.ts +18 -0
  73. package/src/lifecycle/affected.test.ts +118 -0
  74. package/src/lifecycle/affected.ts +118 -14
  75. package/src/op/activities/activity-contracts.ts +17 -1
  76. package/src/op/activities/index.ts +1 -1
  77. package/src/op/activities/shell.test.ts +156 -0
  78. package/src/op/activities/shell.ts +84 -9
  79. package/src/op/activity-profiles.test.ts +16 -2
  80. package/src/op/activity-profiles.ts +18 -0
  81. package/src/op/builders.ts +22 -4
  82. package/src/op/gate-name.ts +11 -0
  83. package/src/op/op-ir.test.ts +4 -1
  84. package/src/op/op.test.ts +7 -2
  85. package/src/op/step-output-ref.ts +29 -8
@@ -0,0 +1,421 @@
1
+ /**
2
+ * Fan a change out downstream, in an order derived from the source (#2417).
3
+ *
4
+ * The order was never the missing piece. `resolveComponentGraph` (./driver.ts)
5
+ * already Kahn-layers a component set by `dependsOn` into parallel-safe waves,
6
+ * flattens to a topological order, refuses a cycle by name and refuses a
7
+ * `dependsOn` it has not been given. What it cannot do is order a *subset*:
8
+ * hand it the affected components alone and it throws `UnknownDependencyError`,
9
+ * because a selected component still names a dependency that is not in the set.
10
+ *
11
+ * That is what this module is. Given every component in the project and the
12
+ * ones whose inputs moved, it derives who else has to run, in what order, what
13
+ * has to be seeded because it is deliberately not running, and one digest that
14
+ * identifies the whole derivation so a single gate can be bound to it.
15
+ *
16
+ * ## Why derived rather than declared
17
+ *
18
+ * The competing shape is a registry where you write down that component B
19
+ * depends on component A. That registry is a second statement of a relationship
20
+ * the source already makes, and it is wrong the first time somebody adds a
21
+ * reference without updating it. `dependsOn` is in the component; the graph is
22
+ * a walk over it.
23
+ *
24
+ * ## Three outcomes, never two
25
+ *
26
+ * A component is selected, skipped as unaffected, or **indeterminate** — the
27
+ * same third answer `../lifecycle/affected.ts` already refuses to collapse. A
28
+ * component whose inputs arrive at deploy time cannot be judged from a source
29
+ * diff, so it is reported rather than guessed at in either direction. Fanning
30
+ * out to everything on any change is easy to build, worth nothing, and reads
31
+ * exactly like diligence in a log.
32
+ *
33
+ * This plans; it does not run. Executing the plan is ./driver.ts's job.
34
+ */
35
+
36
+ import { resolveComponentGraph, type DriverComponent } from "./driver";
37
+ import { deployUnits } from "./deploy-units";
38
+ import { computePlanDigest } from "../lifecycle/plan-digest";
39
+
40
+ /** Thrown when `changed` or `indeterminate` names a component the project does not have. */
41
+ export class UnknownComponentError extends Error {
42
+ constructor(
43
+ readonly field: "changed" | "indeterminate",
44
+ readonly component: string,
45
+ known: string[],
46
+ ) {
47
+ super(
48
+ `${field} names "${component}", which is not a component in this project ` +
49
+ `(known: ${known.join(", ") || "none"})`,
50
+ );
51
+ this.name = "UnknownComponentError";
52
+ }
53
+ }
54
+
55
+ /** Why a component is in the plan but not running. */
56
+ export type FanOutSkipReason =
57
+ /** Nothing it depends on moved, and it did not move itself. */
58
+ | "unaffected"
59
+ /** Its inputs arrive at deploy time, so a source diff cannot judge it. */
60
+ | "indeterminate"
61
+ /** It already applied in an earlier attempt at this same fan-out. */
62
+ | "already-applied"
63
+ /** Something it depends on failed, so the value it would read never landed. */
64
+ | "blocked";
65
+
66
+ export interface FanOutSkip {
67
+ component: string;
68
+ reason: FanOutSkipReason;
69
+ /** For `blocked`, the failed component the walk reached this one from. */
70
+ blockedBy?: string;
71
+ }
72
+
73
+ export interface FanOutRequest {
74
+ /** Every component in the project, `dependsOn` intact. The full graph is what makes a subset orderable. */
75
+ components: DriverComponent[];
76
+ /** Components whose own inputs moved. The walk starts here. */
77
+ changed: string[];
78
+ /**
79
+ * Components a source diff cannot judge (`../lifecycle/affected.ts`'s third
80
+ * category). One that the walk reaches anyway is selected like any other
81
+ * dependent — reachability is a fact about the graph, not about whether the
82
+ * component's own inputs could be read. One the walk does not reach is
83
+ * reported, never decided.
84
+ */
85
+ indeterminate?: string[];
86
+ /**
87
+ * Per-component input identity, when the caller has it. Folded into
88
+ * {@link FanOutPlan.digest} so an approval is bound to *what* would be
89
+ * applied and not only to who would run. Absent, the digest still identifies
90
+ * the derivation — the selection, the order and the edges it came from.
91
+ */
92
+ inputDigests?: Record<string, string>;
93
+ }
94
+
95
+ export interface FanOutPlan {
96
+ /** The components to run, every dependency before its dependents. */
97
+ order: string[];
98
+ /**
99
+ * Parallel-safe waves over the selected set. A dependency that is not
100
+ * selected is already satisfied — its outputs are seeded — so it does not
101
+ * hold its dependents back a wave.
102
+ */
103
+ waves: string[][];
104
+ /** Components deliberately not running, with why. Sorted by name. */
105
+ skipped: FanOutSkip[];
106
+ /**
107
+ * Dependencies of selected components that are not themselves selected.
108
+ * Their outputs have to be seeded for a reference to resolve, which is the
109
+ * price of running a subset rather than the whole graph.
110
+ */
111
+ seeds: string[];
112
+ /** Components a source diff could not judge and the walk did not reach. Reported, never decided. */
113
+ indeterminate: string[];
114
+ /** Identity of this derivation, for binding one approval to the whole fan-out (#2300's pattern). */
115
+ digest: string;
116
+ }
117
+
118
+ /** Reverse the `dependsOn` edges: who has to re-run when this one moves. */
119
+ function consumersOf(components: DriverComponent[]): Map<string, string[]> {
120
+ const consumers = new Map<string, string[]>();
121
+ for (const c of components) {
122
+ for (const dep of c.dependsOn ?? []) {
123
+ const existing = consumers.get(dep);
124
+ if (existing) existing.push(c.name);
125
+ else consumers.set(dep, [c.name]);
126
+ }
127
+ }
128
+ return consumers;
129
+ }
130
+
131
+ /**
132
+ * Derive the fan-out for a change.
133
+ *
134
+ * Refuses a cycle and an unknown `dependsOn` before selecting anything, by
135
+ * resolving the **full** graph first — a broken graph is a broken graph whether
136
+ * or not the change happens to touch the broken part, and finding out halfway
137
+ * through a fan-out is worse than finding out before it starts.
138
+ */
139
+ export function planFanOut(request: FanOutRequest): FanOutPlan {
140
+ const { components, changed, indeterminate = [], inputDigests } = request;
141
+
142
+ // Refuses DependencyCycleError / UnknownDependencyError over the whole graph.
143
+ // Its `order` is deliberately not used: `topoSort` walks in declaration
144
+ // order, so two projects with the same graph and a different file layout
145
+ // would derive different-looking fan-outs and digest differently. The waves
146
+ // below are canonical, and this plan's order is their flattening.
147
+ resolveComponentGraph(components);
148
+
149
+ const byName = new Map(components.map((c) => [c.name, c]));
150
+ for (const name of changed) {
151
+ if (!byName.has(name)) throw new UnknownComponentError("changed", name, [...byName.keys()].sort());
152
+ }
153
+ for (const name of indeterminate) {
154
+ if (!byName.has(name)) throw new UnknownComponentError("indeterminate", name, [...byName.keys()].sort());
155
+ }
156
+
157
+ // Everything reachable downstream of a changed component, transitively. A
158
+ // component reached by two paths is added once, which is the diamond case.
159
+ const consumers = consumersOf(components);
160
+ const selected = new Set<string>(changed);
161
+ const queue = [...changed];
162
+ while (queue.length > 0) {
163
+ const node = queue.shift()!;
164
+ for (const consumer of consumers.get(node) ?? []) {
165
+ if (selected.has(consumer)) continue;
166
+ selected.add(consumer);
167
+ queue.push(consumer);
168
+ }
169
+ }
170
+
171
+ // Waves over the selected subgraph only. An unselected dependency is already
172
+ // applied, so it is not a reason for its dependents to wait.
173
+ const remaining = new Set(selected);
174
+ const selectedDeps = new Map(
175
+ [...selected].map((name) => [name, new Set((byName.get(name)!.dependsOn ?? []).filter((d) => selected.has(d)))]),
176
+ );
177
+ const waves: string[][] = [];
178
+ while (remaining.size > 0) {
179
+ const wave = [...remaining]
180
+ .filter((n) => [...selectedDeps.get(n)!].every((d) => !remaining.has(d)))
181
+ .sort();
182
+ // resolveComponentGraph already refused every cycle in the full graph, and
183
+ // a subgraph of an acyclic graph is acyclic, so this cannot stall.
184
+ for (const n of wave) remaining.delete(n);
185
+ waves.push(wave);
186
+ }
187
+
188
+ // Every dependency lands in an earlier wave than its dependents, so the
189
+ // flattening is a topological order, and a sorted one.
190
+ const order = waves.flat();
191
+
192
+ const seeds = [
193
+ ...new Set(order.flatMap((name) => (byName.get(name)!.dependsOn ?? []).filter((d) => !selected.has(d)))),
194
+ ].sort();
195
+
196
+ const unreachedIndeterminate = indeterminate.filter((name) => !selected.has(name)).sort();
197
+ const indeterminateSet = new Set(unreachedIndeterminate);
198
+ const skipped: FanOutSkip[] = [...byName.keys()]
199
+ .filter((name) => !selected.has(name))
200
+ .sort()
201
+ .map((component) => ({
202
+ component,
203
+ reason: indeterminateSet.has(component) ? ("indeterminate" as const) : ("unaffected" as const),
204
+ }));
205
+
206
+ // What the approver is approving: who runs, in what order, off which edges,
207
+ // and — when the caller knows it — what each one would apply. Never the run
208
+ // id or the moment, per ../lifecycle/plan-digest.ts's rules, so re-deriving
209
+ // an unchanged fan-out does not expire an approval.
210
+ const digest = computePlanDigest("component-fan-out", {
211
+ order,
212
+ waves,
213
+ seeds,
214
+ edges: order.map((name) => ({ component: name, dependsOn: [...(byName.get(name)!.dependsOn ?? [])].sort() })),
215
+ ...(inputDigests
216
+ ? { inputs: order.map((name) => ({ component: name, digest: inputDigests[name] ?? null })) }
217
+ : {}),
218
+ });
219
+
220
+ return { order, waves, skipped, seeds, indeterminate: unreachedIndeterminate, digest };
221
+ }
222
+
223
+ // ── Joining the change signal to components ──────────────────────────────────
224
+
225
+ /**
226
+ * `../lifecycle/affected.ts`'s answer, which is about **stacks**.
227
+ *
228
+ * `AffectedResult` itself is not imported: this takes the two fields the join
229
+ * needs, so a caller can also hand in a signal that did not come from a git
230
+ * diff (a CI system's own changed-paths answer, an operator naming a stack by
231
+ * hand) without manufacturing the rest of that shape.
232
+ */
233
+ export interface ChangedUnits {
234
+ /** Stacks whose built artifact moved between base and head. */
235
+ changed: string[];
236
+ /** Stacks a source diff could not judge, because their inputs arrive at deploy time. */
237
+ indeterminate?: string[];
238
+ }
239
+
240
+ /** What {@link componentsForUnits} resolved, ready to hand to {@link planFanOut}. */
241
+ export interface ComponentChangeSignal {
242
+ /** Components deploying at least one changed unit. */
243
+ changed: string[];
244
+ /** Components deploying no changed unit but at least one indeterminate one. */
245
+ indeterminate: string[];
246
+ /**
247
+ * Changed or indeterminate units no component claims. Reported rather than
248
+ * dropped: a stack that moved and belongs to nothing this project deploys is
249
+ * a hole in the fan-out's coverage, and a silent one is the failure mode
250
+ * this whole feature exists to avoid.
251
+ */
252
+ unclaimed: string[];
253
+ }
254
+
255
+ /**
256
+ * Join a stack-level change signal to the components that deploy those stacks.
257
+ *
258
+ * The key is `deployUnits` (./deploy-units.ts), which already answers "what
259
+ * live units does this component's composition target" for `chant components
260
+ * status --live`. Reusing it means a component's claim on a stack is stated
261
+ * once, by its deploy steps, rather than restated in a fan-out config — the
262
+ * same argument this module's doc makes about `dependsOn`.
263
+ *
264
+ * Changed beats indeterminate. A component deploying one stack that certainly
265
+ * moved and another that could not be judged is changed: the certainty already
266
+ * decides it, and reporting it as indeterminate would lose that.
267
+ */
268
+ export function componentsForUnits(
269
+ components: DriverComponent[],
270
+ units: ChangedUnits,
271
+ ): ComponentChangeSignal {
272
+ const changedUnits = new Set(units.changed);
273
+ const indeterminateUnits = new Set(units.indeterminate ?? []);
274
+
275
+ const changed: string[] = [];
276
+ const indeterminate: string[] = [];
277
+ const claimed = new Set<string>();
278
+
279
+ for (const component of components) {
280
+ const names = deployUnits(component.deploy).map((u) => u.unit);
281
+ let touchesChanged = false;
282
+ let touchesIndeterminate = false;
283
+ for (const name of names) {
284
+ if (changedUnits.has(name)) {
285
+ touchesChanged = true;
286
+ claimed.add(name);
287
+ }
288
+ if (indeterminateUnits.has(name)) {
289
+ touchesIndeterminate = true;
290
+ claimed.add(name);
291
+ }
292
+ }
293
+ if (touchesChanged) changed.push(component.name);
294
+ else if (touchesIndeterminate) indeterminate.push(component.name);
295
+ }
296
+
297
+ const unclaimed = [...changedUnits, ...indeterminateUnits].filter((u) => !claimed.has(u));
298
+
299
+ return {
300
+ changed: changed.sort(),
301
+ indeterminate: indeterminate.sort(),
302
+ unclaimed: [...new Set(unclaimed)].sort(),
303
+ };
304
+ }
305
+
306
+ /**
307
+ * Components inside `within` that are transitively downstream of any of
308
+ * `roots`, each mapped to the **root** it was reached from rather than to its
309
+ * immediate parent — so a report names the one thing to fix instead of the
310
+ * nearest consequence of it.
311
+ *
312
+ * `roots` is sorted before the walk, so a component downstream of two separate
313
+ * roots always names the same one. Unsorted, the answer would depend on the
314
+ * order the caller listed them in, which is not a fact about anything.
315
+ *
316
+ * Shared by {@link remainingFanOut}, which needs it over a whole plan, and by
317
+ * the runner in ./fan-out-run.ts, which needs it as failures accumulate.
318
+ */
319
+ export function downstreamWithin(
320
+ components: DriverComponent[],
321
+ within: Iterable<string>,
322
+ roots: readonly string[],
323
+ ): Map<string, string> {
324
+ const inside = new Set(within);
325
+ const consumers = consumersOf(components);
326
+ const rootSet = new Set(roots);
327
+ const reached = new Map<string, string>();
328
+ const queue = [...roots].sort();
329
+ while (queue.length > 0) {
330
+ const node = queue.shift()!;
331
+ const blamed = rootSet.has(node) ? node : reached.get(node)!;
332
+ for (const consumer of consumers.get(node) ?? []) {
333
+ if (!inside.has(consumer) || rootSet.has(consumer) || reached.has(consumer)) continue;
334
+ reached.set(consumer, blamed);
335
+ queue.push(consumer);
336
+ }
337
+ }
338
+ return reached;
339
+ }
340
+
341
+ // ── Finishing a fan-out that stopped ─────────────────────────────────────────
342
+
343
+ export interface FanOutProgress {
344
+ /** Components that reached `ok` in an earlier attempt at this same plan. */
345
+ completed?: string[];
346
+ /** Components that failed. Everything downstream of one is blocked, not failed. */
347
+ failed?: string[];
348
+ }
349
+
350
+ /**
351
+ * Narrow a plan to what still has to run.
352
+ *
353
+ * **The digest does not change.** That is the whole point: an operator approved
354
+ * a fan-out, a component in the middle of it failed, and finishing the work
355
+ * they already approved must not ask them to approve it again. `remainingFanOut`
356
+ * returns the same `digest` the original derivation produced, so the standing
357
+ * resolution still satisfies the gate on the next attempt. Re-deriving with
358
+ * {@link planFanOut} would mint a new identity and invalidate the approval,
359
+ * which is why resume narrows a plan rather than recomputing one.
360
+ *
361
+ * **A component beneath a failure is `blocked`, never `failed`.** Nothing about
362
+ * it failed. It did not run because the value it would have read never landed,
363
+ * and the distinction is what makes the next attempt legible: an operator
364
+ * reading `blocked by "cluster-a"` knows to fix one thing, not fourteen.
365
+ *
366
+ * **Independent branches keep going.** The order was derived from the source,
367
+ * so a branch that shares no edge with the failure is *known* to be independent
368
+ * rather than assumed to be. Stopping it is the conservative-looking choice
369
+ * that throws away the reason for deriving the graph in the first place.
370
+ */
371
+ export function remainingFanOut(
372
+ plan: FanOutPlan,
373
+ components: DriverComponent[],
374
+ progress: FanOutProgress,
375
+ ): FanOutPlan {
376
+ const byName = new Map(components.map((c) => [c.name, c]));
377
+ const planned = new Set(plan.order);
378
+ const completed = new Set((progress.completed ?? []).filter((n) => planned.has(n)));
379
+ const failed = new Set((progress.failed ?? []).filter((n) => planned.has(n)));
380
+
381
+ const blockedBy = downstreamWithin(components, planned, [...failed]);
382
+
383
+ const runnable = new Set(
384
+ plan.order.filter((n) => !completed.has(n) && !failed.has(n) && !blockedBy.has(n)),
385
+ );
386
+
387
+ // Re-layer what is left. A dependency that already applied is satisfied, so
388
+ // it does not hold its dependents back — the same rule the original
389
+ // derivation applies to a dependency outside the selection.
390
+ const remaining = new Set(runnable);
391
+ const deps = new Map(
392
+ [...runnable].map((n) => [n, new Set((byName.get(n)?.dependsOn ?? []).filter((d) => runnable.has(d)))]),
393
+ );
394
+ const waves: string[][] = [];
395
+ while (remaining.size > 0) {
396
+ const wave = [...remaining].filter((n) => [...deps.get(n)!].every((d) => !remaining.has(d))).sort();
397
+ for (const n of wave) remaining.delete(n);
398
+ waves.push(wave);
399
+ }
400
+ const order = waves.flat();
401
+
402
+ // A completed component's outputs have to be seeded for a reference to
403
+ // resolve, exactly like a component that was never selected.
404
+ const seeds = [
405
+ ...new Set([
406
+ ...plan.seeds,
407
+ ...order.flatMap((n) => (byName.get(n)?.dependsOn ?? []).filter((d) => !runnable.has(d))),
408
+ ]),
409
+ ].sort();
410
+
411
+ const carried = plan.skipped.filter((s) => !runnable.has(s.component));
412
+ const skipped: FanOutSkip[] = [
413
+ ...carried,
414
+ ...[...completed].sort().map((component) => ({ component, reason: "already-applied" as const })),
415
+ ...[...blockedBy.entries()]
416
+ .sort(([a], [b]) => a.localeCompare(b))
417
+ .map(([component, by]) => ({ component, reason: "blocked" as const, blockedBy: by })),
418
+ ].sort((a, b) => a.component.localeCompare(b.component));
419
+
420
+ return { order, waves, skipped, seeds, indeterminate: plan.indeterminate, digest: plan.digest };
421
+ }
@@ -100,6 +100,39 @@ export {
100
100
  UnknownDependencyError,
101
101
  DriverRunFailure,
102
102
  } from "./driver";
103
+ export {
104
+ type FanOutRequest,
105
+ type FanOutPlan,
106
+ type FanOutSkip,
107
+ type FanOutSkipReason,
108
+ type ChangedUnits,
109
+ type ComponentChangeSignal,
110
+ type FanOutProgress,
111
+ planFanOut,
112
+ componentsForUnits,
113
+ remainingFanOut,
114
+ downstreamWithin,
115
+ UnknownComponentError,
116
+ } from "./fan-out";
117
+ export {
118
+ type FanOutGate,
119
+ type FanOutRunOptions,
120
+ type FanOutRunResult,
121
+ runFanOut,
122
+ } from "./fan-out-run";
123
+ export {
124
+ type FanOutGateRef,
125
+ type FanOutRenderOptions,
126
+ renderFanOutPlan,
127
+ renderFanOutHuman,
128
+ renderFanOutJson,
129
+ } from "./fan-out-output";
130
+ export {
131
+ type DeriveFanOutOptions,
132
+ type DerivedFanOut,
133
+ deriveFanOut,
134
+ fanOutRegistry,
135
+ } from "./fan-out-support";
103
136
  export {
104
137
  EcsFargateComponent,
105
138
  type EcsFargateComponentConfig,