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.
- package/README.md +2 -4
- package/dist/agent/claudeTools.d.ts +1 -4
- package/dist/agent/claudeTools.js +1 -6
- package/dist/agent/fakeAgent.js +4 -5
- package/dist/agent/piAdapter.js +1 -14
- package/dist/config/cloneChain.d.ts +12 -0
- package/dist/config/cloneChain.js +322 -0
- package/dist/config/createPipeline.js +101 -21
- package/dist/config/createStage.js +7 -0
- package/dist/config/loadOutcome.d.ts +1 -0
- package/dist/config/loadPipeline.d.ts +2 -0
- package/dist/config/loadPipeline.js +34 -22
- package/dist/config/loadStage.d.ts +2 -0
- package/dist/config/loadStage.js +135 -125
- package/dist/config/migrateYaml.js +14 -3
- package/dist/config/normalizePipelineStageEntry.d.ts +5 -9
- package/dist/config/normalizePipelineStageEntry.js +133 -60
- package/dist/config/parsePreEmitChecks.js +2 -2
- package/dist/config/pipelineNeeds.d.ts +15 -2
- package/dist/config/pipelineNeeds.js +86 -4
- package/dist/config/pipelineRoute.d.ts +46 -0
- package/dist/config/pipelineRoute.js +215 -0
- package/dist/config/pipelineStageKeys.d.ts +2 -0
- package/dist/config/pipelineStageKeys.js +15 -4
- package/dist/config/printTargetYaml.js +18 -12
- package/dist/config/resolveForkEmitContext.d.ts +1 -1
- package/dist/config/resolveForkEmitContext.js +3 -26
- package/dist/config/resolvePipelineDag.d.ts +1 -3
- package/dist/config/resolvePipelineDag.js +143 -186
- package/dist/config/routeIf.d.ts +24 -0
- package/dist/config/routeIf.js +247 -0
- package/dist/config/validateCatalog.d.ts +1 -1
- package/dist/config/validateCatalog.js +5 -3
- package/dist/config/yamlDialect.d.ts +5 -8
- package/dist/config/yamlDialect.js +37 -29
- package/dist/envelope/check.d.ts +0 -2
- package/dist/envelope/check.js +1 -57
- package/dist/envelope/feedbackLoop.js +0 -3
- package/dist/envelope/forkChoice.js +1 -1
- package/dist/envelope/payloadSchema.d.ts +14 -0
- package/dist/envelope/payloadSchema.js +87 -2
- package/dist/mcp/catalogTools.js +1 -2
- package/dist/package-meta.d.ts +1 -1
- package/dist/package-meta.js +1 -1
- package/dist/projection/projectRun.d.ts +0 -2
- package/dist/projection/projectRun.js +0 -3
- package/dist/prompt/priorEnvelope.js +1 -1
- package/dist/runstore/stageInstanceId.d.ts +1 -0
- package/dist/runstore/stageInstanceId.js +9 -0
- package/dist/runtime/cloneSchedule.d.ts +16 -33
- package/dist/runtime/cloneSchedule.js +155 -236
- package/dist/runtime/envelopeRouting.js +43 -94
- package/dist/runtime/forkGeneration.d.ts +2 -2
- package/dist/runtime/forkGeneration.js +29 -15
- package/dist/runtime/joinReadiness.d.ts +16 -0
- package/dist/runtime/joinReadiness.js +156 -0
- package/dist/runtime/pipelineScheduler.d.ts +0 -3
- package/dist/runtime/pipelineScheduler.js +81 -65
- package/dist/runtime/routeIfEval.d.ts +3 -0
- package/dist/runtime/routeIfEval.js +94 -0
- package/dist/runtime/runRetryCoordinator.js +0 -4
- package/dist/runtime/stageAttemptBootstrap.js +60 -11
- package/dist/runtime/taskInput.js +1 -13
- package/dist/tools/emitStageEnvelope.d.ts +2 -3
- package/dist/tools/emitStageEnvelope.js +1 -65
- package/dist/types/envelope.d.ts +0 -2
- package/dist/types/forkChoice.d.ts +0 -16
- package/dist/types/pipeline.d.ts +86 -27
- package/dist/types/stage.d.ts +11 -7
- package/dist/ui/assets/{index-D2Wt8hwH.css → index-DsgVjLUS.css} +1 -1
- package/dist/ui/assets/{index-XWJovnp7.js → index-DwKGaQvQ.js} +13 -12
- package/dist/ui/index.html +2 -2
- package/package.json +2 -1
- package/skills/stageflow-author/SKILL.md +4 -4
- package/skills/stageflow-author/assets/examples/branch-decision/hotfix.yaml +10 -2
- package/skills/stageflow-author/assets/examples/branch-decision/release-gate.pipeline.yaml +12 -4
- package/skills/stageflow-author/assets/examples/branch-decision/run-tests.yaml +6 -4
- package/skills/stageflow-author/assets/examples/branch-decision/ship.yaml +9 -1
- package/skills/stageflow-author/assets/examples/linear-review/draft.yaml +3 -0
- package/skills/stageflow-author/assets/examples/linear-review/publish.yaml +8 -0
- package/skills/stageflow-author/assets/examples/linear-review/review-loop.pipeline.yaml +5 -2
- package/skills/stageflow-author/assets/examples/linear-review/review.yaml +8 -0
- package/skills/stageflow-author/assets/examples/non-sdlc-digest/gather.yaml +3 -0
- package/skills/stageflow-author/assets/examples/non-sdlc-digest/research-digest.pipeline.yaml +5 -2
- package/skills/stageflow-author/assets/examples/non-sdlc-digest/send.yaml +8 -0
- package/skills/stageflow-author/assets/examples/non-sdlc-digest/summarize.yaml +8 -0
- package/skills/stageflow-author/references/catalog-mapping.md +159 -50
- package/skills/stageflow-author/references/catalog-write-conventions.md +3 -3
- package/skills/stageflow-author/references/stage-prompt-template.md +57 -17
- package/skills/stageflow-session-capture/SKILL.md +2 -2
- package/skills/stageflow-session-capture/assets/example-pipeline/example.pipeline.yaml +3 -1
- package/skills/stageflow-session-capture/assets/example-pipeline/implement.yaml +15 -0
- package/skills/stageflow-session-capture/assets/example-pipeline/research.yaml +3 -0
- package/skills/stageflow-session-capture/references/catalog-authoring.md +4 -2
- package/dist/envelope/cloneForks.d.ts +0 -2
- 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
|
|
4
|
-
*
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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}): ${
|
|
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
|
-
|
|
304
|
+
route = parsedRoute.value;
|
|
223
305
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
{
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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:
|
|
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).
|
|
3
|
-
*
|
|
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,
|
|
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:
|
|
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) =>
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
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[];
|