stageflow 0.16.0 → 0.18.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 (96) hide show
  1. package/README.md +2 -4
  2. package/dist/agent/claudeTools.d.ts +1 -4
  3. package/dist/agent/claudeTools.js +1 -6
  4. package/dist/agent/fakeAgent.js +4 -5
  5. package/dist/agent/piAdapter.js +1 -14
  6. package/dist/config/cloneChain.d.ts +12 -0
  7. package/dist/config/cloneChain.js +322 -0
  8. package/dist/config/createPipeline.js +101 -21
  9. package/dist/config/createStage.js +7 -0
  10. package/dist/config/loadOutcome.d.ts +1 -0
  11. package/dist/config/loadPipeline.d.ts +2 -0
  12. package/dist/config/loadPipeline.js +34 -22
  13. package/dist/config/loadStage.d.ts +2 -0
  14. package/dist/config/loadStage.js +135 -125
  15. package/dist/config/migrateYaml.js +14 -3
  16. package/dist/config/normalizePipelineStageEntry.d.ts +5 -9
  17. package/dist/config/normalizePipelineStageEntry.js +133 -60
  18. package/dist/config/parsePreEmitChecks.js +2 -2
  19. package/dist/config/pipelineNeeds.d.ts +15 -2
  20. package/dist/config/pipelineNeeds.js +86 -4
  21. package/dist/config/pipelineRoute.d.ts +46 -0
  22. package/dist/config/pipelineRoute.js +215 -0
  23. package/dist/config/pipelineStageKeys.d.ts +2 -0
  24. package/dist/config/pipelineStageKeys.js +15 -4
  25. package/dist/config/printTargetYaml.js +18 -12
  26. package/dist/config/resolveForkEmitContext.d.ts +1 -1
  27. package/dist/config/resolveForkEmitContext.js +3 -26
  28. package/dist/config/resolvePipelineDag.d.ts +1 -3
  29. package/dist/config/resolvePipelineDag.js +143 -186
  30. package/dist/config/routeIf.d.ts +24 -0
  31. package/dist/config/routeIf.js +247 -0
  32. package/dist/config/validateCatalog.d.ts +1 -1
  33. package/dist/config/validateCatalog.js +5 -3
  34. package/dist/config/yamlDialect.d.ts +5 -8
  35. package/dist/config/yamlDialect.js +37 -29
  36. package/dist/envelope/check.d.ts +0 -2
  37. package/dist/envelope/check.js +1 -57
  38. package/dist/envelope/feedbackLoop.js +0 -3
  39. package/dist/envelope/forkChoice.js +1 -1
  40. package/dist/envelope/payloadSchema.d.ts +14 -0
  41. package/dist/envelope/payloadSchema.js +87 -2
  42. package/dist/mcp/catalogTools.js +1 -2
  43. package/dist/package-meta.d.ts +1 -1
  44. package/dist/package-meta.js +1 -1
  45. package/dist/projection/projectRun.d.ts +0 -2
  46. package/dist/projection/projectRun.js +0 -3
  47. package/dist/prompt/priorEnvelope.js +1 -1
  48. package/dist/runstore/stageInstanceId.d.ts +1 -0
  49. package/dist/runstore/stageInstanceId.js +9 -0
  50. package/dist/runtime/cloneSchedule.d.ts +16 -33
  51. package/dist/runtime/cloneSchedule.js +155 -236
  52. package/dist/runtime/envelopeRouting.js +43 -94
  53. package/dist/runtime/forkGeneration.d.ts +2 -2
  54. package/dist/runtime/forkGeneration.js +29 -15
  55. package/dist/runtime/joinReadiness.d.ts +16 -0
  56. package/dist/runtime/joinReadiness.js +156 -0
  57. package/dist/runtime/pipelineScheduler.d.ts +0 -3
  58. package/dist/runtime/pipelineScheduler.js +81 -65
  59. package/dist/runtime/routeIfEval.d.ts +3 -0
  60. package/dist/runtime/routeIfEval.js +94 -0
  61. package/dist/runtime/runRetryCoordinator.js +0 -4
  62. package/dist/runtime/stageAttemptBootstrap.js +60 -11
  63. package/dist/runtime/taskInput.js +1 -13
  64. package/dist/tools/emitStageEnvelope.d.ts +2 -3
  65. package/dist/tools/emitStageEnvelope.js +1 -65
  66. package/dist/types/envelope.d.ts +0 -2
  67. package/dist/types/forkChoice.d.ts +0 -16
  68. package/dist/types/pipeline.d.ts +86 -27
  69. package/dist/types/stage.d.ts +11 -7
  70. package/dist/ui/assets/{index-D2Wt8hwH.css → index-DsgVjLUS.css} +1 -1
  71. package/dist/ui/assets/{index-XWJovnp7.js → index-DwKGaQvQ.js} +13 -12
  72. package/dist/ui/index.html +2 -2
  73. package/package.json +2 -1
  74. package/skills/stageflow-author/SKILL.md +4 -4
  75. package/skills/stageflow-author/assets/examples/branch-decision/hotfix.yaml +10 -2
  76. package/skills/stageflow-author/assets/examples/branch-decision/release-gate.pipeline.yaml +12 -4
  77. package/skills/stageflow-author/assets/examples/branch-decision/run-tests.yaml +6 -4
  78. package/skills/stageflow-author/assets/examples/branch-decision/ship.yaml +9 -1
  79. package/skills/stageflow-author/assets/examples/linear-review/draft.yaml +3 -0
  80. package/skills/stageflow-author/assets/examples/linear-review/publish.yaml +8 -0
  81. package/skills/stageflow-author/assets/examples/linear-review/review-loop.pipeline.yaml +5 -2
  82. package/skills/stageflow-author/assets/examples/linear-review/review.yaml +8 -0
  83. package/skills/stageflow-author/assets/examples/non-sdlc-digest/gather.yaml +3 -0
  84. package/skills/stageflow-author/assets/examples/non-sdlc-digest/research-digest.pipeline.yaml +5 -2
  85. package/skills/stageflow-author/assets/examples/non-sdlc-digest/send.yaml +8 -0
  86. package/skills/stageflow-author/assets/examples/non-sdlc-digest/summarize.yaml +8 -0
  87. package/skills/stageflow-author/references/catalog-mapping.md +159 -50
  88. package/skills/stageflow-author/references/catalog-write-conventions.md +3 -3
  89. package/skills/stageflow-author/references/stage-prompt-template.md +57 -17
  90. package/skills/stageflow-session-capture/SKILL.md +2 -2
  91. package/skills/stageflow-session-capture/assets/example-pipeline/example.pipeline.yaml +3 -1
  92. package/skills/stageflow-session-capture/assets/example-pipeline/implement.yaml +15 -0
  93. package/skills/stageflow-session-capture/assets/example-pipeline/research.yaml +3 -0
  94. package/skills/stageflow-session-capture/references/catalog-authoring.md +4 -2
  95. package/dist/envelope/cloneForks.d.ts +0 -2
  96. package/dist/envelope/cloneForks.js +0 -77
@@ -1,17 +1,17 @@
1
1
  /**
2
2
  * Normalize a pipeline `stages:` entry. Target YAML (`io` / `verify` /
3
- * `on_verify_fail`) compiles onto IR `completion` / `recovery` / body
4
- * `payload_schema`. Legacy YAML keys are dual-read via legacyYaml.ts.
3
+ * `on_verify_fail`) compiles onto IR `completion` / `recovery`. Emit/schema
4
+ * fields stay on the YAML-shaped inline body until `parseStageFields`.
5
+ * Legacy YAML keys are dual-read via legacyYaml.ts.
5
6
  */
6
7
  import path from "node:path";
7
8
  import { loadFailure, loadSuccess } from "./loadOutcome.js";
8
9
  import { parseStageMcp } from "./loadStage.js";
9
10
  import { legacyAuthoringRejected, presentLegacyKeys } from "./legacyYaml.js";
10
- import { applyCompiledBody, compileTargetContract, dialectFromKeys, mixedDialectIssue, } from "./yamlDialect.js";
11
+ import { compileTargetContract, dialectFromKeys, mixedDialectIssue, } from "./yamlDialect.js";
11
12
  import { BODY_KEYS, isAllowedPipelineStageEntryKey, } from "./pipelineStageKeys.js";
12
13
  import { parseExecutionPolicy } from "./parseCompletionContract.js";
13
- import { parsePipelineNeeds } from "./pipelineNeeds.js";
14
- import { parseFeedbackLoopConfig } from "./resolvePipelineDag.js";
14
+ import { parsePipelineRoute } from "./pipelineRoute.js";
15
15
  function isPlainObject(value) {
16
16
  return typeof value === "object" && value !== null && !Array.isArray(value);
17
17
  }
@@ -149,7 +149,106 @@ export function normalizePipelineStageEntries(rawEntries, ctx) {
149
149
  },
150
150
  ]);
151
151
  }
152
- let compiledBody;
152
+ if (raw.needs !== undefined) {
153
+ return loadFailure([
154
+ {
155
+ code: "pipeline.dag_error",
156
+ message: `Pipeline ${ctx.pipelineId} (${ctx.path}): stage "${id}": "needs" is no longer supported — declare the wiring on the source stage's "route" instead`,
157
+ category: "pipeline",
158
+ pipelineId: ctx.pipelineId,
159
+ },
160
+ ]);
161
+ }
162
+ if (raw.fork !== undefined) {
163
+ return loadFailure([
164
+ {
165
+ code: "pipeline.dag_error",
166
+ message: `Pipeline ${ctx.pipelineId} (${ctx.path}): stage "${id}": "fork" is no longer supported — use "route" instead; listed route targets always run`,
167
+ category: "pipeline",
168
+ pipelineId: ctx.pipelineId,
169
+ },
170
+ ]);
171
+ }
172
+ if (raw.feedback_loop !== undefined) {
173
+ return loadFailure([
174
+ {
175
+ code: "pipeline.dag_error",
176
+ message: `Pipeline ${ctx.pipelineId} (${ctx.path}): stage "${id}": "feedback_loop" is no longer supported — use a "type: loop" entry inside "route" instead`,
177
+ category: "pipeline",
178
+ pipelineId: ctx.pipelineId,
179
+ },
180
+ ]);
181
+ }
182
+ if (raw.route_select !== undefined) {
183
+ return loadFailure([
184
+ {
185
+ code: "pipeline.dag_error",
186
+ message: `Pipeline ${ctx.pipelineId} (${ctx.path}): stage "${id}": "route_select" is no longer supported — listed route targets always run`,
187
+ category: "pipeline",
188
+ pipelineId: ctx.pipelineId,
189
+ },
190
+ ]);
191
+ }
192
+ if (raw.allow_none !== undefined) {
193
+ return loadFailure([
194
+ {
195
+ code: "pipeline.dag_error",
196
+ message: `Pipeline ${ctx.pipelineId} (${ctx.path}): stage "${id}": "allow_none" is no longer supported — listed route targets always run`,
197
+ category: "pipeline",
198
+ pipelineId: ctx.pipelineId,
199
+ },
200
+ ]);
201
+ }
202
+ if (raw.clonable !== undefined) {
203
+ return loadFailure([
204
+ {
205
+ code: "pipeline.dag_error",
206
+ message: `Pipeline ${ctx.pipelineId} (${ctx.path}): stage "${id}": "clonable" is no longer supported — use a Clone Chain instead`,
207
+ category: "pipeline",
208
+ pipelineId: ctx.pipelineId,
209
+ },
210
+ ]);
211
+ }
212
+ let cloneCap;
213
+ if (raw.clone_cap !== undefined) {
214
+ if (typeof raw.clone_cap !== "number" ||
215
+ !Number.isInteger(raw.clone_cap) ||
216
+ raw.clone_cap < 1) {
217
+ return loadFailure([
218
+ {
219
+ code: "pipeline.dag_error",
220
+ message: `Pipeline ${ctx.pipelineId} (${ctx.path}): stage "${id}": clone_cap must be an integer >= 1`,
221
+ category: "pipeline",
222
+ pipelineId: ctx.pipelineId,
223
+ },
224
+ ]);
225
+ }
226
+ cloneCap = raw.clone_cap;
227
+ }
228
+ let cloneMode;
229
+ if (raw.clone_mode !== undefined) {
230
+ if (raw.clone_mode !== "parallel" && raw.clone_mode !== "sequential") {
231
+ return loadFailure([
232
+ {
233
+ code: "pipeline.dag_error",
234
+ message: `Pipeline ${ctx.pipelineId} (${ctx.path}): stage "${id}": clone_mode must be "parallel" or "sequential"`,
235
+ category: "pipeline",
236
+ pipelineId: ctx.pipelineId,
237
+ },
238
+ ]);
239
+ }
240
+ cloneMode = raw.clone_mode;
241
+ }
242
+ if (raw.clone_actions !== undefined) {
243
+ return loadFailure([
244
+ {
245
+ code: "pipeline.dag_error",
246
+ message: `Pipeline ${ctx.pipelineId} (${ctx.path}): stage "${id}": "clone_actions" is no longer supported — use a Clone Chain instead`,
247
+ category: "pipeline",
248
+ pipelineId: ctx.pipelineId,
249
+ },
250
+ ]);
251
+ }
153
252
  let policyOutcome;
154
253
  if (dialect === "target") {
155
254
  const compiled = compileTargetContract(raw, {
@@ -157,6 +256,7 @@ export function normalizePipelineStageEntries(rawEntries, ctx) {
157
256
  label: `entry at index ${index} in ${declaringPath}`,
158
257
  category: "pipeline",
159
258
  deferSchemaRefs: true,
259
+ requireIo: !uses,
160
260
  });
161
261
  if (!compiled.ok)
162
262
  return compiled;
@@ -168,9 +268,6 @@ export function normalizePipelineStageEntries(rawEntries, ctx) {
168
268
  ? { recovery: compiled.value.recovery }
169
269
  : {}),
170
270
  });
171
- if (!uses) {
172
- compiledBody = applyCompiledBody(extractBodyRaw(raw), compiled.value);
173
- }
174
271
  }
175
272
  else {
176
273
  const rejected = legacyAuthoringRejected(dialect, `stage "${id}" in ${declaringPath}`, presentLegacyKeys(Object.keys(raw)));
@@ -180,23 +277,6 @@ export function normalizePipelineStageEntries(rawEntries, ctx) {
180
277
  }
181
278
  if (!policyOutcome.ok)
182
279
  return policyOutcome;
183
- let needs;
184
- let feedbackLoop;
185
- if (raw.feedback_loop !== undefined) {
186
- try {
187
- feedbackLoop = parseFeedbackLoopConfig(raw.feedback_loop, id, ctx);
188
- }
189
- catch (err) {
190
- return loadFailure([
191
- {
192
- code: "pipeline.dag_error",
193
- message: err instanceof Error ? err.message : String(err),
194
- category: "pipeline",
195
- pipelineId: ctx.pipelineId,
196
- },
197
- ]);
198
- }
199
- }
200
280
  if (raw.replay_safe !== undefined && typeof raw.replay_safe !== "boolean") {
201
281
  return loadFailure([
202
282
  {
@@ -207,64 +287,60 @@ export function normalizePipelineStageEntries(rawEntries, ctx) {
207
287
  },
208
288
  ]);
209
289
  }
210
- if (raw.needs !== undefined) {
211
- const parsedNeeds = parsePipelineNeeds(raw.needs, id);
212
- if (!parsedNeeds.ok) {
290
+ let route;
291
+ if (raw.route !== undefined) {
292
+ const parsedRoute = parsePipelineRoute(raw.route, id);
293
+ if (!parsedRoute.ok) {
213
294
  return loadFailure([
214
295
  {
215
- code: "pipeline.dag_error",
216
- message: `Pipeline ${ctx.pipelineId} (${ctx.path}): ${parsedNeeds.message}`,
296
+ code: parsedRoute.code ?? "pipeline.dag_error",
297
+ message: `Pipeline ${ctx.pipelineId} (${ctx.path}): ${parsedRoute.message}`,
217
298
  category: "pipeline",
218
299
  pipelineId: ctx.pipelineId,
300
+ ...(parsedRoute.code === "pipeline.route_if_invalid" ? { stageId: id } : {}),
219
301
  },
220
302
  ]);
221
303
  }
222
- needs = parsedNeeds.value;
304
+ route = parsedRoute.value;
223
305
  }
224
- let forkValue;
225
- if (raw.fork !== undefined) {
226
- if (!isPlainObject(raw.fork)) {
227
- return loadFailure([
228
- {
229
- code: "pipeline.dag_error",
230
- message: `Pipeline ${ctx.pipelineId} (${ctx.path}): stage "${id}": fork must be an object`,
231
- category: "pipeline",
232
- pipelineId: ctx.pipelineId,
233
- },
234
- ]);
235
- }
236
- forkValue = raw.fork;
306
+ if (raw.entry !== undefined && typeof raw.entry !== "boolean") {
307
+ return loadFailure([
308
+ {
309
+ code: "pipeline.dag_error",
310
+ message: `Pipeline ${ctx.pipelineId} (${ctx.path}): stage "${id}": entry must be a boolean`,
311
+ category: "pipeline",
312
+ pipelineId: ctx.pipelineId,
313
+ },
314
+ ]);
237
315
  }
316
+ const entryFlag = raw.entry;
238
317
  let body;
239
318
  if (uses) {
240
319
  const absolutePath = path.resolve(path.dirname(declaringPath), uses);
241
320
  body = { kind: "uses", path: uses, absolutePath };
242
321
  }
243
322
  else {
244
- body = { kind: "inline", raw: compiledBody ?? extractBodyRaw(raw) };
323
+ body = { kind: "inline", raw: extractBodyRaw(raw) };
245
324
  }
246
325
  const entry = {
247
326
  id,
248
327
  declaringPath,
249
328
  body,
250
- ...(needs !== undefined ? { needs } : {}),
251
- ...(forkValue !== undefined ? { fork: forkValue } : {}),
252
- ...(raw.clonable !== undefined ? { clonable: raw.clonable } : {}),
253
- ...(raw.clone_cap !== undefined ? { clone_cap: raw.clone_cap } : {}),
254
329
  ...(policyOutcome.value.completion !== undefined
255
330
  ? { completion: policyOutcome.value.completion }
256
331
  : {}),
257
332
  ...(policyOutcome.value.recovery !== undefined
258
333
  ? { recovery: policyOutcome.value.recovery }
259
334
  : {}),
260
- ...(feedbackLoop !== undefined
261
- ? { feedback_loop: feedbackLoop }
262
- : {}),
263
335
  ...(raw.replay_safe !== undefined
264
336
  ? { replay_safe: raw.replay_safe }
265
337
  : {}),
338
+ ...(route !== undefined ? { route } : {}),
339
+ ...(entryFlag !== undefined ? { entry: entryFlag } : {}),
266
340
  ...(skill !== undefined ? { skill } : {}),
267
341
  ...(mcp !== undefined ? { mcp } : {}),
342
+ ...(cloneCap !== undefined ? { clone_cap: cloneCap } : {}),
343
+ ...(cloneMode !== undefined ? { clone_mode: cloneMode } : {}),
268
344
  };
269
345
  const priorPath = normalized.find((e) => e.id === id)?.declaringPath;
270
346
  if (priorPath) {
@@ -284,15 +360,12 @@ export function normalizePipelineStageEntries(rawEntries, ctx) {
284
360
  export function toWiringRefs(entries) {
285
361
  return entries.map((entry) => ({
286
362
  id: entry.id,
287
- ...(entry.needs !== undefined ? { needs: entry.needs } : {}),
288
- ...(entry.fork !== undefined ? { fork: entry.fork } : {}),
289
- ...(entry.clonable !== undefined ? { clonable: entry.clonable } : {}),
290
- ...(entry.clone_cap !== undefined ? { clone_cap: entry.clone_cap } : {}),
291
363
  ...(entry.completion !== undefined ? { completion: entry.completion } : {}),
292
364
  ...(entry.recovery !== undefined ? { recovery: entry.recovery } : {}),
293
- ...(entry.feedback_loop !== undefined
294
- ? { feedback_loop: entry.feedback_loop }
295
- : {}),
296
365
  ...(entry.replay_safe !== undefined ? { replay_safe: entry.replay_safe } : {}),
366
+ ...(entry.route !== undefined ? { route: entry.route } : {}),
367
+ ...(entry.entry !== undefined ? { entry: entry.entry } : {}),
368
+ ...(entry.clone_cap !== undefined ? { clone_cap: entry.clone_cap } : {}),
369
+ ...(entry.clone_mode !== undefined ? { clone_mode: entry.clone_mode } : {}),
297
370
  }));
298
371
  }
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Parse IR `pre_emit_checks` (emit-phase). YAML `verify` emit items are compiled
3
- * onto this list in yamlDialect.ts before this parser runs.
2
+ * Parse IR `pre_emit_checks` (emit-phase). Target `verify` emit items are mapped
3
+ * in yamlDialect.ts then parsed here. Legacy YAML still authors this key.
4
4
  */
5
5
  import { STAGE_GATE_KINDS } from "../types/stage.js";
6
6
  import { loadFailure, loadSuccess } from "./loadOutcome.js";
@@ -1,4 +1,4 @@
1
- import type { NeedTerminalState, PipelineNeedEdge, PipelineNeeds, PipelineStageRef, ResolvedPipelineStageNode } from "../types/pipeline.js";
1
+ import type { NeedTerminalState, PipelineNeedEdge, PipelineNeedItem, PipelineNeeds, PipelineRoute, PipelineRouteForwardEntry, ResolvedPipelineStageNode } from "../types/pipeline.js";
2
2
  export declare const NEED_TERMINAL_STATES: readonly NeedTerminalState[];
3
3
  export declare function isNeedTerminalState(value: unknown): value is NeedTerminalState;
4
4
  export declare function parsePipelineNeeds(raw: unknown, stageId: string): {
@@ -8,7 +8,20 @@ export declare function parsePipelineNeeds(raw: unknown, stageId: string): {
8
8
  ok: false;
9
9
  message: string;
10
10
  };
11
- export declare function toNeedEdges(needs: PipelineStageRef["needs"]): PipelineNeedEdge[];
11
+ export declare function toNeedEdges(needs: PipelineNeeds | PipelineNeedItem[] | undefined): PipelineNeedEdge[];
12
+ export declare function toPipelineNeeds(edges: PipelineNeedEdge[]): PipelineNeeds;
13
+ export declare function invertRouteToPredecessorEdges(sources: ReadonlyArray<{
14
+ id: string;
15
+ route?: PipelineRoute;
16
+ }>): Map<string, PipelineNeedEdge[]>;
17
+ export type OutboundRouteInversion = {
18
+ route?: PipelineRouteForwardEntry[];
19
+ entry?: boolean;
20
+ };
21
+ export declare function invertPredecessorEdgesToRoute(stages: ReadonlyArray<{
22
+ id: string;
23
+ needs?: PipelineNeeds;
24
+ }>): Map<string, OutboundRouteInversion>;
12
25
  export declare function predecessorEdges(node: Pick<ResolvedPipelineStageNode, "needs" | "needsEdges">): PipelineNeedEdge[];
13
26
  export declare function hydrateResolvedNeeds(node: {
14
27
  needs?: unknown;
@@ -1,3 +1,4 @@
1
+ import { parseRouteIf } from "./routeIf.js";
1
2
  export const NEED_TERMINAL_STATES = [
2
3
  "succeeded",
3
4
  "failed",
@@ -110,9 +111,81 @@ export function toNeedEdges(needs) {
110
111
  if (typeof needs === "string") {
111
112
  return needs ? [{ id: needs, on: ["succeeded"] }] : [];
112
113
  }
113
- return needs.map((item) => typeof item === "string"
114
- ? { id: item, on: ["succeeded"] }
115
- : { id: item.id, on: item.on && item.on.length > 0 ? item.on : ["succeeded"] });
114
+ return needs.map((item) => {
115
+ if (typeof item === "string") {
116
+ return { id: item, on: ["succeeded"] };
117
+ }
118
+ const edge = {
119
+ id: item.id,
120
+ on: item.on && item.on.length > 0 ? item.on : ["succeeded"],
121
+ };
122
+ if ("if" in item && item.if !== undefined) {
123
+ edge.if = item.if;
124
+ }
125
+ return edge;
126
+ });
127
+ }
128
+ export function toPipelineNeeds(edges) {
129
+ const only = edges[0];
130
+ if (edges.length === 1 &&
131
+ only !== undefined &&
132
+ only.on.length === 1 &&
133
+ only.on[0] === "succeeded" &&
134
+ only.if === undefined) {
135
+ return only.id;
136
+ }
137
+ return edges;
138
+ }
139
+ function isForwardRouteEntry(entry) {
140
+ return !("type" in entry) || entry.type === undefined;
141
+ }
142
+ export function invertRouteToPredecessorEdges(sources) {
143
+ const inbound = new Map();
144
+ for (const source of sources) {
145
+ if (source.route === undefined)
146
+ continue;
147
+ for (const item of source.route) {
148
+ if (!isForwardRouteEntry(item))
149
+ continue;
150
+ const on = item.on === undefined
151
+ ? ["succeeded"]
152
+ : Array.isArray(item.on)
153
+ ? item.on
154
+ : [item.on];
155
+ const list = inbound.get(item.to) ?? [];
156
+ const edge = { id: source.id, on };
157
+ if (item.if !== undefined)
158
+ edge.if = item.if;
159
+ list.push(edge);
160
+ inbound.set(item.to, list);
161
+ }
162
+ }
163
+ return inbound;
164
+ }
165
+ export function invertPredecessorEdgesToRoute(stages) {
166
+ const result = new Map();
167
+ if (!stages.some((stage) => stage.needs !== undefined)) {
168
+ return result;
169
+ }
170
+ const outbound = new Map();
171
+ for (const stage of stages) {
172
+ if (stage.needs === undefined) {
173
+ result.set(stage.id, { entry: true });
174
+ continue;
175
+ }
176
+ for (const parent of toNeedEdges(stage.needs)) {
177
+ const list = outbound.get(parent.id) ?? [];
178
+ const entry = { to: stage.id, on: parent.on };
179
+ if (parent.if !== undefined)
180
+ entry.if = parent.if;
181
+ list.push(entry);
182
+ outbound.set(parent.id, list);
183
+ }
184
+ }
185
+ for (const [id, route] of outbound) {
186
+ result.set(id, { ...(result.get(id) ?? {}), route });
187
+ }
188
+ return result;
116
189
  }
117
190
  export function predecessorEdges(node) {
118
191
  if (node.needsEdges !== undefined)
@@ -131,7 +204,16 @@ export function hydrateResolvedNeeds(node) {
131
204
  const on = Array.isArray(item.on)
132
205
  ? item.on.filter((state) => typeof state === "string" && NEED_TERMINAL_STATE_SET.has(state))
133
206
  : ["succeeded"];
134
- edges.push({ id: item.id, on: on.length > 0 ? on : ["succeeded"] });
207
+ const edge = {
208
+ id: item.id,
209
+ on: on.length > 0 ? on : ["succeeded"],
210
+ };
211
+ if (item.if !== undefined) {
212
+ const parsedIf = parseRouteIf(item.if, item.id, item.id);
213
+ if (parsedIf.ok)
214
+ edge.if = parsedIf.value;
215
+ }
216
+ edges.push(edge);
135
217
  }
136
218
  if (typeof node.needs === "string") {
137
219
  return { needs: node.needs, needsEdges: edges };
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Parsing/normalization for the additive `route` stage field
3
+ * (docs/yaml-catalog.md). Mirrors the shape of
4
+ * pipelineNeeds.ts: `parsePipelineRoute` validates raw YAML/JSON input at
5
+ * parse time, `toRouteEdges`/`toRouteLoopEntries` normalize an already-typed
6
+ * `PipelineRoute` (used by the `resolvePipelineDagFromRefs` seam, which may
7
+ * receive un-validated refs built directly in TypeScript/tests).
8
+ *
9
+ * Ticket 01 handled forward entries (`{ to, on? }`) only. Ticket 03 adds the
10
+ * loop entry shape (`{ type: "loop", to, max_replays, on_max_replays,
11
+ * replay_session }`) — same replay-policy fields `FeedbackLoopConfig`
12
+ * carries today, `target` renamed to `to`. Loop entries never produce a
13
+ * forward edge: `toRouteEdges` skips them, `toRouteLoopEntries` extracts
14
+ * them for the resolver's replay validation/synthesis.
15
+ */
16
+ import type { PipelineRoute, PipelineRouteEdge, PipelineRouteEntry, PipelineRouteLoopEntry } from "../types/pipeline.js";
17
+ export declare function isLoopRouteEntry(entry: PipelineRouteEntry): entry is PipelineRouteLoopEntry;
18
+ /**
19
+ * Parse+validate a raw `route:` field for one stage. Each item is either a
20
+ * forward entry (`{ to, on? }`) or a loop entry (`{ type: "loop", to,
21
+ * max_replays, on_max_replays, replay_session }`); entries are never bare
22
+ * strings (route entries are always structured objects, per the spec).
23
+ */
24
+ export type ParsePipelineRouteFailure = {
25
+ ok: false;
26
+ message: string;
27
+ code?: "pipeline.route_if_invalid";
28
+ };
29
+ export declare function parsePipelineRoute(raw: unknown, stageId: string): {
30
+ ok: true;
31
+ value: PipelineRouteEntry[];
32
+ } | ParsePipelineRouteFailure;
33
+ /**
34
+ * Normalize an already-typed `route` value (as found on a `PipelineStageRef`,
35
+ * which may be hand-built in TypeScript and not have gone through
36
+ * `parsePipelineRoute`) into forward route edges with `on` always populated.
37
+ * Loop entries are skipped — they contribute no forward edge.
38
+ */
39
+ export declare function toRouteEdges(route: PipelineRoute | undefined): PipelineRouteEdge[];
40
+ /**
41
+ * Extract the loop entries from an already-typed `route` value (mirrors
42
+ * `toRouteEdges`, the forward-entry counterpart). Used by the resolver to
43
+ * validate replay policy and synthesize the legacy `feedback_loop` shape on
44
+ * the resolved DAG node.
45
+ */
46
+ export declare function toRouteLoopEntries(route: PipelineRoute | undefined): PipelineRouteLoopEntry[];