stageflow 0.16.0 → 0.19.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/activity.d.ts +4 -0
- package/dist/agent/claudeAdapter.js +38 -0
- package/dist/agent/claudeTools.d.ts +1 -4
- package/dist/agent/claudeTools.js +1 -6
- package/dist/agent/fakeAgent.js +7 -6
- package/dist/agent/piAdapter.js +34 -15
- package/dist/agent/port.d.ts +1 -1
- package/dist/agent/stageTimeout.d.ts +9 -0
- package/dist/agent/stageTimeout.js +24 -0
- package/dist/cli/graphCommand.d.ts +15 -0
- package/dist/cli/graphCommand.js +79 -0
- package/dist/cli/graphRender.d.ts +2 -0
- package/dist/cli/graphRender.js +85 -0
- package/dist/cli/runsCommand.d.ts +1 -1
- package/dist/cli/runsCommand.js +37 -0
- package/dist/cli.js +12 -0
- 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/mcp/controlTools.js +17 -0
- 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/port.js +2 -0
- 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 +6 -2
- package/dist/runtime/forkGeneration.js +45 -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 +103 -65
- package/dist/runtime/reopenSkipped.d.ts +13 -0
- package/dist/runtime/reopenSkipped.js +56 -0
- package/dist/runtime/resumeTimedOut.d.ts +33 -0
- package/dist/runtime/resumeTimedOut.js +165 -0
- package/dist/runtime/routeIfEval.d.ts +3 -0
- package/dist/runtime/routeIfEval.js +94 -0
- package/dist/runtime/runManager.d.ts +1 -0
- package/dist/runtime/runManager.js +76 -0
- package/dist/runtime/runRetryCoordinator.js +0 -4
- package/dist/runtime/stageAttemptBootstrap.js +60 -11
- package/dist/runtime/stageRecovery.js +2 -2
- package/dist/runtime/stageRunner.js +6 -2
- package/dist/runtime/taskInput.js +1 -13
- package/dist/server/http.js +18 -0
- package/dist/tools/askOperator.d.ts +1 -0
- package/dist/tools/askOperator.js +14 -11
- package/dist/tools/emitStageEnvelope.d.ts +3 -3
- package/dist/tools/emitStageEnvelope.js +4 -66
- package/dist/tools/prepareToolArguments.d.ts +4 -0
- package/dist/tools/prepareToolArguments.js +79 -0
- 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-CSsDued1.css} +1 -1
- package/dist/ui/assets/index-DMVdl1qB.js +119 -0
- 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
- package/dist/ui/assets/index-XWJovnp7.js +0 -118
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { predecessorEdges } from "../config/pipelineNeeds.js";
|
|
2
2
|
import { appendCloneInstances, instancesOfDefinition, } from "../runstore/pipelineDagSnapshot.js";
|
|
3
|
-
import { definitionIdForInstance } from "../runstore/stageInstanceId.js";
|
|
3
|
+
import { cloneInstanceOrdinal, definitionIdForInstance, } from "../runstore/stageInstanceId.js";
|
|
4
4
|
import { collectDownstreamStageIds } from "./dagTraversal.js";
|
|
5
|
-
import { activeCohortFromCloneIds, filterJoinInputs, } from "./forkGeneration.js";
|
|
6
5
|
function asDagSnapshot(dag) {
|
|
7
6
|
const snapshot = dag;
|
|
8
7
|
if (Array.isArray(snapshot.stage_ids))
|
|
@@ -22,53 +21,34 @@ function definitionInstances(dag, catalogId) {
|
|
|
22
21
|
export function isCloneInstance(dag, stageId) {
|
|
23
22
|
return definitionIdForInstance(asDagSnapshot(dag), stageId) !== stageId;
|
|
24
23
|
}
|
|
25
|
-
function
|
|
24
|
+
function emitterCloneMode(dag, stageId) {
|
|
26
25
|
if (!isCloneInstance(dag, stageId))
|
|
27
|
-
return
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
item.successor_id === defId &&
|
|
34
|
-
definitionInstances(dag, defId).includes(stageId)) {
|
|
35
|
-
return true;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
return false;
|
|
26
|
+
return undefined;
|
|
27
|
+
const node = dag.nodes.find((n) => n.id === stageId);
|
|
28
|
+
const emitterId = typeof node?.needs === "string" && node.needs ? node.needs : undefined;
|
|
29
|
+
if (emitterId === undefined)
|
|
30
|
+
return undefined;
|
|
31
|
+
return dag.nodes.find((n) => n.id === emitterId)?.clone_mode;
|
|
40
32
|
}
|
|
41
|
-
function
|
|
42
|
-
|
|
43
|
-
for (const item of envelope.clone_forks ?? []) {
|
|
44
|
-
if (item.action === "fanout" &&
|
|
45
|
-
item.mode === "sequential" &&
|
|
46
|
-
item.successor_id === successorId) {
|
|
47
|
-
return true;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
return false;
|
|
33
|
+
function isParallelCloneInstance(dag, stageId, _completedEnvelopes) {
|
|
34
|
+
return emitterCloneMode(dag, stageId) === "parallel";
|
|
52
35
|
}
|
|
53
36
|
function isSequentialCloneInstance(dag, stageId, completedEnvelopes) {
|
|
54
|
-
|
|
55
|
-
return false;
|
|
56
|
-
const defId = definitionIdForInstance(asDagSnapshot(dag), stageId);
|
|
57
|
-
if (!isSequentialFanoutSuccessor(completedEnvelopes, defId))
|
|
58
|
-
return false;
|
|
59
|
-
return definitionInstances(dag, defId).includes(stageId);
|
|
37
|
+
return sequentialInstanceList(dag, stageId, completedEnvelopes) !== undefined;
|
|
60
38
|
}
|
|
61
|
-
function sequentialInstanceList(dag, stageId,
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
39
|
+
function sequentialInstanceList(dag, stageId, _completedEnvelopes) {
|
|
40
|
+
if (!isCloneInstance(dag, stageId))
|
|
41
|
+
return undefined;
|
|
42
|
+
const snapshot = asDagSnapshot(dag);
|
|
43
|
+
const defId = definitionIdForInstance(snapshot, stageId);
|
|
44
|
+
const node = dag.nodes.find((n) => n.id === stageId);
|
|
45
|
+
const emitterId = typeof node?.needs === "string" && node.needs ? node.needs : undefined;
|
|
46
|
+
if (emitterId === undefined)
|
|
47
|
+
return undefined;
|
|
48
|
+
const emitter = dag.nodes.find((n) => n.id === emitterId);
|
|
49
|
+
if (emitter?.clone_mode !== "sequential")
|
|
50
|
+
return undefined;
|
|
51
|
+
return definitionInstances(dag, defId);
|
|
72
52
|
}
|
|
73
53
|
function sequentialPreviousUnsatisfied(dag, stageId, states, completedEnvelopes) {
|
|
74
54
|
const instances = sequentialInstanceList(dag, stageId, completedEnvelopes);
|
|
@@ -91,27 +71,21 @@ export function sequentialLaterCloneIds(dag, stageId, completedEnvelopes) {
|
|
|
91
71
|
function parentNeedEdge(node, parentId, parentDefId) {
|
|
92
72
|
return predecessorEdges(node).find((edge) => edge.id === parentId || edge.id === parentDefId);
|
|
93
73
|
}
|
|
94
|
-
function edgeInstancesReady(dag, edge, states) {
|
|
95
|
-
const instances = definitionInstances(dag, edge.id);
|
|
96
|
-
if (instances.length === 0)
|
|
97
|
-
return false;
|
|
98
|
-
const hasSucceededSibling = instances.some((id) => states.get(id) === "succeeded");
|
|
99
|
-
return instances.every((id) => {
|
|
100
|
-
const state = states.get(id);
|
|
101
|
-
if (!isNeedTerminalState(state))
|
|
102
|
-
return false;
|
|
103
|
-
if (edge.on.includes(state))
|
|
104
|
-
return true;
|
|
105
|
-
return state === "skipped" && hasSucceededSibling;
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
74
|
function shouldSkipForObservedNeed(dag, node, parentId, parentDefId, observed) {
|
|
75
|
+
// A multi-parent join is never skip-cascaded from one parent. Once every
|
|
76
|
+
// parent is terminal: run if any succeeded (skipped siblings do not
|
|
77
|
+
// block) unless every parent succeeded and an inbound `if` missed;
|
|
78
|
+
// stay pending if any failed; force-skip when every parent is skipped
|
|
79
|
+
// or every parent succeeded with an inbound miss (`pickStalledJoinSkips`).
|
|
80
|
+
// A single-parent node keeps eager skip-cascade below.
|
|
81
|
+
if (predecessorEdges(node).length > 1)
|
|
82
|
+
return false;
|
|
109
83
|
const edge = parentNeedEdge(node, parentId, parentDefId);
|
|
110
84
|
if (!edge)
|
|
111
85
|
return false;
|
|
112
86
|
if (edge.on.includes(observed))
|
|
113
87
|
return false;
|
|
114
|
-
if (observed === "skipped" &&
|
|
88
|
+
if ((observed === "skipped" || observed === "failed") &&
|
|
115
89
|
predecessorEdges(node).length === 1 &&
|
|
116
90
|
typeof node.needs === "string" &&
|
|
117
91
|
isCloneInstance(dag, parentId)) {
|
|
@@ -164,41 +138,7 @@ function joinAndDownstreamIds(dag, cloneStageId) {
|
|
|
164
138
|
}
|
|
165
139
|
return ids;
|
|
166
140
|
}
|
|
167
|
-
|
|
168
|
-
* A fresh clone_forks fanout targeting a catalog id that already has
|
|
169
|
-
* instances is only safe when every existing instance was put back to
|
|
170
|
-
* `pending` by the current retry cascade (`retryDownstreamIds`) — that means
|
|
171
|
-
* an ancestor was legitimately retried and this parent re-emitted the same
|
|
172
|
-
* fanout for reactivation. Instances that are still active/terminal, or
|
|
173
|
-
* merely `pending` without having gone through a retry reset (e.g. an
|
|
174
|
-
* envelope processed twice in one live run), are rejected as before.
|
|
175
|
-
*
|
|
176
|
-
* Feedback-loop replay is different: the prior cohort was superseded
|
|
177
|
-
* (`skipped`) and a new generation must mint fresh `~N` ids, so
|
|
178
|
-
* `forceFreshCloneIds` bypasses the conflict.
|
|
179
|
-
*/
|
|
180
|
-
export function cloneFanoutConflict(dag, envelope, states, retryDownstreamIds, options) {
|
|
181
|
-
if (options?.forceFreshCloneIds === true)
|
|
182
|
-
return undefined;
|
|
183
|
-
const snapshot = asDagSnapshot(dag);
|
|
184
|
-
for (const item of envelope.clone_forks ?? []) {
|
|
185
|
-
if (item.action !== "fanout")
|
|
186
|
-
continue;
|
|
187
|
-
const existing = definitionInstances(dag, item.successor_id).filter((id) => id !== item.successor_id);
|
|
188
|
-
if (existing.length > 0) {
|
|
189
|
-
const reactivatable = existing.every((id) => states.get(id) === "pending" && retryDownstreamIds.has(id));
|
|
190
|
-
if (!reactivatable) {
|
|
191
|
-
return `clone fan-out for "${item.successor_id}" is already instanced`;
|
|
192
|
-
}
|
|
193
|
-
continue;
|
|
194
|
-
}
|
|
195
|
-
if (!snapshot.stage_ids.includes(item.successor_id)) {
|
|
196
|
-
return `clone fan-out for "${item.successor_id}" is already instanced`;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
return undefined;
|
|
200
|
-
}
|
|
201
|
-
export function nextFreeCloneSuffix(snapshot, catalogId) {
|
|
141
|
+
function nextFreeCloneSuffix(snapshot, catalogId) {
|
|
202
142
|
const prefix = `${catalogId}~`;
|
|
203
143
|
let max = 0;
|
|
204
144
|
for (const id of snapshot.stage_ids) {
|
|
@@ -210,15 +150,127 @@ export function nextFreeCloneSuffix(snapshot, catalogId) {
|
|
|
210
150
|
}
|
|
211
151
|
return max + 1;
|
|
212
152
|
}
|
|
213
|
-
|
|
153
|
+
function catalogCloneChildId(dag, emitterId) {
|
|
154
|
+
const catalogChildId = (dag.childrenOf[emitterId] ?? []).find((id) => {
|
|
155
|
+
const child = dag.nodes.find((n) => n.id === id);
|
|
156
|
+
return child !== undefined && (child.definition_id ?? child.id) === child.id;
|
|
157
|
+
});
|
|
158
|
+
if (catalogChildId !== undefined)
|
|
159
|
+
return catalogChildId;
|
|
160
|
+
const instanceChild = (dag.childrenOf[emitterId] ?? []).find((id) => {
|
|
161
|
+
const child = dag.nodes.find((n) => n.id === id);
|
|
162
|
+
return child?.definition_id !== undefined && child.definition_id !== child.id;
|
|
163
|
+
});
|
|
164
|
+
return instanceChild
|
|
165
|
+
? dag.nodes.find((n) => n.id === instanceChild)?.definition_id
|
|
166
|
+
: undefined;
|
|
167
|
+
}
|
|
168
|
+
export function mint(dag, emitterId, envelope) {
|
|
169
|
+
const emitter = dag.nodes.find((n) => n.id === emitterId);
|
|
170
|
+
const field = emitter?.clone_array_field;
|
|
171
|
+
if (field === undefined)
|
|
172
|
+
return { kind: "none" };
|
|
173
|
+
const catalogChildId = catalogCloneChildId(dag, emitterId);
|
|
174
|
+
if (catalogChildId === undefined)
|
|
175
|
+
return { kind: "none" };
|
|
176
|
+
const arr = envelope.payload?.[field];
|
|
177
|
+
if (!Array.isArray(arr) || arr.length < 1) {
|
|
178
|
+
return {
|
|
179
|
+
kind: "error",
|
|
180
|
+
reason: `Clone Array "${field}" must contain at least one item`,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
const snapshot = asDagSnapshot(dag);
|
|
184
|
+
const { snapshot: next, instanceIds } = appendCloneInstances(snapshot, {
|
|
185
|
+
catalogId: catalogChildId,
|
|
186
|
+
predecessorId: emitterId,
|
|
187
|
+
count: arr.length,
|
|
188
|
+
startAt: nextFreeCloneSuffix(snapshot, catalogChildId),
|
|
189
|
+
});
|
|
190
|
+
return {
|
|
191
|
+
kind: "minted",
|
|
192
|
+
dag: next,
|
|
193
|
+
catalogChildId,
|
|
194
|
+
instanceIds,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
function cloneAssignmentIndex(dag, stageId, definitionId, arrayLength) {
|
|
198
|
+
const ordinal = cloneInstanceOrdinal(stageId, definitionId);
|
|
199
|
+
if (ordinal === undefined)
|
|
200
|
+
return undefined;
|
|
201
|
+
const snapshot = dag;
|
|
202
|
+
if (!Array.isArray(snapshot.stage_ids) || arrayLength < 1) {
|
|
203
|
+
return ordinal - 1;
|
|
204
|
+
}
|
|
205
|
+
const siblings = instancesOfDefinition(snapshot, definitionId)
|
|
206
|
+
.map((id) => ({ id, ordinal: cloneInstanceOrdinal(id, definitionId) }))
|
|
207
|
+
.filter((row) => row.ordinal !== undefined)
|
|
208
|
+
.sort((a, b) => a.ordinal - b.ordinal);
|
|
209
|
+
const cohort = siblings.slice(-arrayLength);
|
|
210
|
+
const index = cohort.findIndex((row) => row.id === stageId);
|
|
211
|
+
return index === -1 ? undefined : index;
|
|
212
|
+
}
|
|
213
|
+
export function assignment(dag, stageId, definitionId, completedEnvelopes) {
|
|
214
|
+
const ordinal = cloneInstanceOrdinal(stageId, definitionId);
|
|
215
|
+
if (ordinal === undefined)
|
|
216
|
+
return undefined;
|
|
217
|
+
const node = dag.nodes.find((n) => n.id === stageId);
|
|
218
|
+
if (!node)
|
|
219
|
+
return undefined;
|
|
220
|
+
const emitterId = typeof node.needs === "string" && node.needs
|
|
221
|
+
? node.needs
|
|
222
|
+
: predecessorEdges(node)[0]?.id;
|
|
223
|
+
if (emitterId === undefined)
|
|
224
|
+
return undefined;
|
|
225
|
+
const emitter = dag.nodes.find((n) => n.id === emitterId);
|
|
226
|
+
const field = emitter?.clone_array_field;
|
|
227
|
+
if (field === undefined)
|
|
228
|
+
return undefined;
|
|
229
|
+
const parent = completedEnvelopes.get(emitterId);
|
|
230
|
+
if (parent === undefined || parent.status !== "success") {
|
|
231
|
+
return {
|
|
232
|
+
ok: false,
|
|
233
|
+
reason: `missing envelope for Clone Chain emitter "${emitterId}"`,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
const arr = parent.payload?.[field];
|
|
237
|
+
if (!Array.isArray(arr)) {
|
|
238
|
+
return {
|
|
239
|
+
ok: false,
|
|
240
|
+
reason: `missing Clone Array element ${ordinal} on "${emitterId}"`,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
const index = cloneAssignmentIndex(dag, stageId, definitionId, arr.length);
|
|
244
|
+
if (index === undefined || index >= arr.length) {
|
|
245
|
+
return {
|
|
246
|
+
ok: false,
|
|
247
|
+
reason: `missing Clone Array element ${ordinal} on "${emitterId}"`,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
const element = arr[index];
|
|
251
|
+
if (element === null || typeof element !== "object" || Array.isArray(element)) {
|
|
252
|
+
return {
|
|
253
|
+
ok: false,
|
|
254
|
+
reason: `Clone Array element ${index + 1} is not an object`,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
return {
|
|
258
|
+
ok: true,
|
|
259
|
+
prior: {
|
|
260
|
+
status: "success",
|
|
261
|
+
summary: parent.summary,
|
|
262
|
+
artifacts: [],
|
|
263
|
+
payload: structuredClone(element),
|
|
264
|
+
},
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
export function protectedClonableChildIds(dag, predecessorId, _envelope) {
|
|
214
268
|
const ids = new Set();
|
|
269
|
+
const predecessor = dag.nodes.find((n) => n.id === predecessorId);
|
|
270
|
+
if (predecessor?.clone_array_field === undefined)
|
|
271
|
+
return ids;
|
|
215
272
|
for (const childId of dag.childrenOf[predecessorId] ?? []) {
|
|
216
|
-
|
|
217
|
-
if (child?.clonable === true)
|
|
218
|
-
ids.add(childId);
|
|
219
|
-
}
|
|
220
|
-
for (const item of envelope.clone_forks ?? []) {
|
|
221
|
-
ids.add(item.successor_id);
|
|
273
|
+
ids.add(childId);
|
|
222
274
|
}
|
|
223
275
|
return ids;
|
|
224
276
|
}
|
|
@@ -276,142 +328,9 @@ export function cloneFailFastSkipIds(dag, stageId, completedEnvelopes) {
|
|
|
276
328
|
ids.push(...joinAndDownstreamIds(dag, stageId));
|
|
277
329
|
return ids;
|
|
278
330
|
}
|
|
279
|
-
export function cloneScheduleAllowsRun(dag, stageId, states, completedEnvelopes
|
|
280
|
-
const node = dag.nodes.find((n) => n.id === stageId);
|
|
281
|
-
if (!node)
|
|
282
|
-
return false;
|
|
331
|
+
export function cloneScheduleAllowsRun(dag, stageId, states, completedEnvelopes) {
|
|
283
332
|
if (definitionInstances(dag, stageId).some((id) => id !== stageId)) {
|
|
284
333
|
return false;
|
|
285
334
|
}
|
|
286
|
-
const edges = predecessorEdges(node);
|
|
287
|
-
if (edges.length === 0) {
|
|
288
|
-
return !sequentialPreviousUnsatisfied(dag, stageId, states, completedEnvelopes);
|
|
289
|
-
}
|
|
290
|
-
if (edges.length >= 2) {
|
|
291
|
-
for (const edge of edges) {
|
|
292
|
-
if (!edgeInstancesReady(dag, edge, states))
|
|
293
|
-
return false;
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
else {
|
|
297
|
-
const parentNeedId = typeof node.needs === "string" && node.needs ? node.needs : edges[0].id;
|
|
298
|
-
const allInstances = definitionInstances(dag, parentNeedId);
|
|
299
|
-
const cohort = options?.activeCohortForNeeds?.(parentNeedId) ??
|
|
300
|
-
activeCohortFromCloneIds(options?.activeCloneIdsForNeeds?.(parentNeedId));
|
|
301
|
-
if (cohort.kind === "awaiting_mint") {
|
|
302
|
-
return false;
|
|
303
|
-
}
|
|
304
|
-
const instances = filterJoinInputs(allInstances, cohort);
|
|
305
|
-
if (instances.length <= 1) {
|
|
306
|
-
const parentId = instances[0] ?? parentNeedId;
|
|
307
|
-
if (states.get(parentId) !== "succeeded")
|
|
308
|
-
return false;
|
|
309
|
-
}
|
|
310
|
-
else if (cohort.kind === "active") {
|
|
311
|
-
if (!instances.every((id) => states.get(id) === "succeeded")) {
|
|
312
|
-
return false;
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
else if (!instances.every((id) => states.get(id) === "succeeded" || states.get(id) === "skipped")) {
|
|
316
|
-
return false;
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
335
|
return !sequentialPreviousUnsatisfied(dag, stageId, states, completedEnvelopes);
|
|
320
336
|
}
|
|
321
|
-
export function applyCloneForksToSchedule(dag, predecessorId, envelope, states, options) {
|
|
322
|
-
let current = asDagSnapshot(dag);
|
|
323
|
-
const skippedIds = [];
|
|
324
|
-
const mintedBySuccessor = new Map();
|
|
325
|
-
const forceFresh = options?.forceFreshCloneIds === true;
|
|
326
|
-
const skipPending = (stageId) => {
|
|
327
|
-
if (states.get(stageId) === "pending") {
|
|
328
|
-
states.set(stageId, "skipped");
|
|
329
|
-
skippedIds.push(stageId);
|
|
330
|
-
}
|
|
331
|
-
};
|
|
332
|
-
for (const item of envelope.clone_forks ?? []) {
|
|
333
|
-
if (item.action === "skip") {
|
|
334
|
-
skipPending(item.successor_id);
|
|
335
|
-
skipRejectedNeedDependents(current, item.successor_id, states, "skipped", skipPending);
|
|
336
|
-
continue;
|
|
337
|
-
}
|
|
338
|
-
if (item.action !== "fanout")
|
|
339
|
-
continue;
|
|
340
|
-
const existing = definitionInstances(current, item.successor_id).filter((id) => id !== item.successor_id);
|
|
341
|
-
if (forceFresh) {
|
|
342
|
-
const { snapshot, instanceIds } = appendCloneInstances(current, {
|
|
343
|
-
catalogId: item.successor_id,
|
|
344
|
-
predecessorId,
|
|
345
|
-
count: item.clones.length,
|
|
346
|
-
startAt: nextFreeCloneSuffix(current, item.successor_id),
|
|
347
|
-
});
|
|
348
|
-
current = snapshot;
|
|
349
|
-
states.delete(item.successor_id);
|
|
350
|
-
for (const id of instanceIds) {
|
|
351
|
-
states.set(id, "pending");
|
|
352
|
-
}
|
|
353
|
-
mintedBySuccessor.set(item.successor_id, instanceIds);
|
|
354
|
-
continue;
|
|
355
|
-
}
|
|
356
|
-
if (existing.length > 0) {
|
|
357
|
-
// Reactivating instances a retry cascade reset to `pending` (see
|
|
358
|
-
// cloneFanoutConflict), which only allows this fanout through when
|
|
359
|
-
// *every* existing instance is pending. Anything short of that —
|
|
360
|
-
// a normal resume re-apply of an already-processed fanout, or only
|
|
361
|
-
// one sibling out of a cohort being retried directly — must stay a
|
|
362
|
-
// no-op here: growing/shrinking the cohort in those cases would mint
|
|
363
|
-
// or skip instances no retry actually asked for.
|
|
364
|
-
const allPending = existing.every((id) => states.get(id) === "pending");
|
|
365
|
-
if (!allPending)
|
|
366
|
-
continue;
|
|
367
|
-
const desired = item.clones.length;
|
|
368
|
-
for (const id of existing.slice(desired)) {
|
|
369
|
-
skipPending(id);
|
|
370
|
-
skipRejectedNeedDependents(current, id, states, "skipped", skipPending);
|
|
371
|
-
}
|
|
372
|
-
if (desired > existing.length) {
|
|
373
|
-
const { snapshot, instanceIds } = appendCloneInstances(current, {
|
|
374
|
-
catalogId: item.successor_id,
|
|
375
|
-
predecessorId,
|
|
376
|
-
count: desired - existing.length,
|
|
377
|
-
startAt: nextFreeCloneSuffix(current, item.successor_id),
|
|
378
|
-
});
|
|
379
|
-
current = snapshot;
|
|
380
|
-
for (const id of instanceIds) {
|
|
381
|
-
states.set(id, "pending");
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
continue;
|
|
385
|
-
}
|
|
386
|
-
const { snapshot, instanceIds } = appendCloneInstances(current, {
|
|
387
|
-
catalogId: item.successor_id,
|
|
388
|
-
predecessorId,
|
|
389
|
-
count: item.clones.length,
|
|
390
|
-
});
|
|
391
|
-
current = snapshot;
|
|
392
|
-
states.delete(item.successor_id);
|
|
393
|
-
for (const id of instanceIds) {
|
|
394
|
-
states.set(id, "pending");
|
|
395
|
-
}
|
|
396
|
-
mintedBySuccessor.set(item.successor_id, instanceIds);
|
|
397
|
-
}
|
|
398
|
-
return {
|
|
399
|
-
dag: current,
|
|
400
|
-
skippedIds,
|
|
401
|
-
...(mintedBySuccessor.size > 0 ? { mintedBySuccessor } : {}),
|
|
402
|
-
};
|
|
403
|
-
}
|
|
404
|
-
export function applyCloneForksFromEnvelopes(dag, states, completedEnvelopes) {
|
|
405
|
-
let current = asDagSnapshot(dag);
|
|
406
|
-
const skippedIds = [];
|
|
407
|
-
const predecessors = [...current.nodes];
|
|
408
|
-
for (const node of predecessors) {
|
|
409
|
-
const envelope = completedEnvelopes.get(node.id);
|
|
410
|
-
if (!envelope?.clone_forks?.length)
|
|
411
|
-
continue;
|
|
412
|
-
const applied = applyCloneForksToSchedule(current, node.id, envelope, states);
|
|
413
|
-
current = applied.dag;
|
|
414
|
-
skippedIds.push(...applied.skippedIds);
|
|
415
|
-
}
|
|
416
|
-
return { dag: current, skippedIds };
|
|
417
|
-
}
|
|
@@ -1,14 +1,7 @@
|
|
|
1
1
|
import { predecessorEdges } from "../config/pipelineNeeds.js";
|
|
2
|
-
import { instancesOfDefinition, } from "../runstore/pipelineDagSnapshot.js";
|
|
3
2
|
import { definitionIdForInstance } from "../runstore/stageInstanceId.js";
|
|
4
3
|
import { activeCohortFromCloneIds, filterJoinInputs, selectActiveInput, } from "./forkGeneration.js";
|
|
5
|
-
|
|
6
|
-
const snapshot = dag;
|
|
7
|
-
if (Array.isArray(snapshot.stage_ids)) {
|
|
8
|
-
return instancesOfDefinition(snapshot, catalogId);
|
|
9
|
-
}
|
|
10
|
-
return dag.nodes.some((n) => n.id === catalogId) ? [catalogId] : [];
|
|
11
|
-
}
|
|
4
|
+
import { expandJoinParent } from "./joinReadiness.js";
|
|
12
5
|
async function resolveActiveCohortForNeeds(dag, needsId, store, runId, activeCloneIds, scheduleOverride) {
|
|
13
6
|
if (activeCloneIds !== undefined) {
|
|
14
7
|
return activeCohortFromCloneIds(activeCloneIds);
|
|
@@ -59,29 +52,6 @@ export function buildStageConfigById(loaded) {
|
|
|
59
52
|
function dagNode(dag, stageId) {
|
|
60
53
|
return dag.nodes.find((node) => node.id === stageId);
|
|
61
54
|
}
|
|
62
|
-
const SYNTHETIC_SKIPPED_TERMINAL = {
|
|
63
|
-
status: "skipped",
|
|
64
|
-
summary: "stage was skipped",
|
|
65
|
-
artifacts: [],
|
|
66
|
-
};
|
|
67
|
-
function mintedCloneInstances(dag, parentId) {
|
|
68
|
-
return definitionInstances(dag, parentId).filter((id) => id !== parentId);
|
|
69
|
-
}
|
|
70
|
-
function isClonableParent(dag, parentId) {
|
|
71
|
-
const node = dagNode(dag, parentId);
|
|
72
|
-
return node?.clonable === true || mintedCloneInstances(dag, parentId).length > 0;
|
|
73
|
-
}
|
|
74
|
-
function failureReasonFromSnapshot(snap) {
|
|
75
|
-
let reason = "stage failed";
|
|
76
|
-
if (snap === undefined)
|
|
77
|
-
return reason;
|
|
78
|
-
for (const ev of snap.events) {
|
|
79
|
-
if (ev.event === "failed" && typeof ev.reason === "string" && ev.reason) {
|
|
80
|
-
reason = ev.reason;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
return reason;
|
|
84
|
-
}
|
|
85
55
|
async function loadStageSnapshots(options) {
|
|
86
56
|
if (options.stages !== undefined)
|
|
87
57
|
return options.stages;
|
|
@@ -107,24 +77,10 @@ async function readCachedEnvelope(options, snapshots, stageId) {
|
|
|
107
77
|
}
|
|
108
78
|
return undefined;
|
|
109
79
|
}
|
|
110
|
-
async function
|
|
80
|
+
async function successTerminalForPersistedStage(options, snapshots, stageId) {
|
|
111
81
|
const snap = snapshots.get(stageId);
|
|
112
|
-
if (snap?.status === "skipped") {
|
|
113
|
-
return { ok: true,
|
|
114
|
-
}
|
|
115
|
-
if (snap?.status === "failed") {
|
|
116
|
-
const envelope = await readCachedEnvelope(options, snapshots, stageId);
|
|
117
|
-
if (envelope?.status === "failure") {
|
|
118
|
-
return { ok: true, value: envelope };
|
|
119
|
-
}
|
|
120
|
-
return {
|
|
121
|
-
ok: true,
|
|
122
|
-
value: {
|
|
123
|
-
status: "failure",
|
|
124
|
-
summary: failureReasonFromSnapshot(snap),
|
|
125
|
-
artifacts: [],
|
|
126
|
-
},
|
|
127
|
-
};
|
|
82
|
+
if (snap?.status === "skipped" || snap?.status === "failed") {
|
|
83
|
+
return { ok: true, omit: true };
|
|
128
84
|
}
|
|
129
85
|
if (snap?.status === "succeeded") {
|
|
130
86
|
const envelope = await readCachedEnvelope(options, snapshots, stageId);
|
|
@@ -134,8 +90,18 @@ async function terminalForPersistedStage(options, snapshots, stageId) {
|
|
|
134
90
|
reason: `missing envelope for upstream stage "${stageId}"`,
|
|
135
91
|
};
|
|
136
92
|
}
|
|
93
|
+
if (envelope.status !== "success") {
|
|
94
|
+
return { ok: true, omit: true };
|
|
95
|
+
}
|
|
137
96
|
return { ok: true, value: envelope };
|
|
138
97
|
}
|
|
98
|
+
const fromMap = options.completedEnvelopes.get(stageId);
|
|
99
|
+
if (fromMap?.status === "success") {
|
|
100
|
+
return { ok: true, value: structuredClone(fromMap) };
|
|
101
|
+
}
|
|
102
|
+
if (fromMap !== undefined) {
|
|
103
|
+
return { ok: true, omit: true };
|
|
104
|
+
}
|
|
139
105
|
return {
|
|
140
106
|
ok: false,
|
|
141
107
|
reason: `missing envelope for upstream stage "${stageId}"`,
|
|
@@ -146,32 +112,27 @@ async function resolveGenericJoinPriors(options, node) {
|
|
|
146
112
|
const priorEnvelopesByStage = {};
|
|
147
113
|
for (const edge of predecessorEdges(node)) {
|
|
148
114
|
const parentId = edge.id;
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
priorEnvelopesByStage[parentId] = list;
|
|
160
|
-
continue;
|
|
115
|
+
const expanded = expandJoinParent(options.dag, parentId);
|
|
116
|
+
if (expanded.length > 1 || expanded[0] !== parentId) {
|
|
117
|
+
const list = [];
|
|
118
|
+
for (const id of expanded) {
|
|
119
|
+
const terminal = await successTerminalForPersistedStage(options, snapshots, id);
|
|
120
|
+
if (!terminal.ok)
|
|
121
|
+
return terminal;
|
|
122
|
+
if ("omit" in terminal)
|
|
123
|
+
continue;
|
|
124
|
+
list.push(terminal.value);
|
|
161
125
|
}
|
|
162
|
-
if (
|
|
163
|
-
priorEnvelopesByStage[parentId] =
|
|
164
|
-
continue;
|
|
126
|
+
if (list.length > 0) {
|
|
127
|
+
priorEnvelopesByStage[parentId] = list;
|
|
165
128
|
}
|
|
166
|
-
const once = await terminalForPersistedStage(options, snapshots, parentId);
|
|
167
|
-
if (!once.ok)
|
|
168
|
-
return once;
|
|
169
|
-
priorEnvelopesByStage[parentId] = [once.value];
|
|
170
129
|
continue;
|
|
171
130
|
}
|
|
172
|
-
const terminal = await
|
|
131
|
+
const terminal = await successTerminalForPersistedStage(options, snapshots, parentId);
|
|
173
132
|
if (!terminal.ok)
|
|
174
133
|
return terminal;
|
|
134
|
+
if ("omit" in terminal)
|
|
135
|
+
continue;
|
|
175
136
|
priorEnvelopesByStage[parentId] = terminal.value;
|
|
176
137
|
}
|
|
177
138
|
return { ok: true, prior: null, priorEnvelopesByStage };
|
|
@@ -192,21 +153,30 @@ export async function resolvePriorEnvelope(options) {
|
|
|
192
153
|
return resolveGenericJoinPriors(options, node);
|
|
193
154
|
}
|
|
194
155
|
const parentId = typeof node.needs === "string" && node.needs ? node.needs : edges[0].id;
|
|
195
|
-
const
|
|
196
|
-
if (
|
|
156
|
+
const expanded = expandJoinParent(options.dag, parentId);
|
|
157
|
+
if (expanded.length > 1 || expanded[0] !== parentId) {
|
|
197
158
|
const cohort = await resolveActiveCohortForNeeds(options.dag, parentId, options.store, options.runId, options.activeCloneIds, options.scheduleOverride);
|
|
198
|
-
const joinInstances = filterJoinInputs(
|
|
159
|
+
const joinInstances = filterJoinInputs(expanded, cohort);
|
|
160
|
+
const snapshots = new Map((await loadStageSnapshots(options)).map((snap) => [snap.stage_id, snap]));
|
|
199
161
|
const joinPriors = [];
|
|
200
162
|
for (const id of joinInstances) {
|
|
163
|
+
const snap = snapshots.get(id);
|
|
164
|
+
if (snap?.status === "skipped" || snap?.status === "failed") {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
201
167
|
const fromMap = options.completedEnvelopes.get(id);
|
|
202
168
|
if (fromMap !== undefined) {
|
|
203
|
-
|
|
169
|
+
if (fromMap.status === "success") {
|
|
170
|
+
joinPriors.push(structuredClone(fromMap));
|
|
171
|
+
}
|
|
204
172
|
continue;
|
|
205
173
|
}
|
|
206
174
|
if (options.store !== undefined && options.runId !== undefined) {
|
|
207
175
|
try {
|
|
208
176
|
const envelope = await options.store.readEnvelope(options.runId, id);
|
|
209
|
-
|
|
177
|
+
if (envelope.status === "success") {
|
|
178
|
+
joinPriors.push(structuredClone(envelope));
|
|
179
|
+
}
|
|
210
180
|
continue;
|
|
211
181
|
}
|
|
212
182
|
catch {
|
|
@@ -242,26 +212,5 @@ export async function resolvePriorEnvelope(options) {
|
|
|
242
212
|
reason: `missing envelope for upstream stage "${parentId}"`,
|
|
243
213
|
};
|
|
244
214
|
}
|
|
245
|
-
for (const item of parent.clone_forks ?? []) {
|
|
246
|
-
if (item.action === "once" && item.successor_id === options.stageId) {
|
|
247
|
-
return { ok: true, prior: structuredClone(item.envelope) };
|
|
248
|
-
}
|
|
249
|
-
if (item.action === "fanout") {
|
|
250
|
-
const allInstanceIds = definitionInstances(options.dag, item.successor_id);
|
|
251
|
-
const cohort = await resolveActiveCohortForNeeds(options.dag, item.successor_id, options.store, options.runId, options.activeCloneIds, options.scheduleOverride);
|
|
252
|
-
const instanceIds = filterJoinInputs(allInstanceIds, cohort);
|
|
253
|
-
const index = instanceIds.indexOf(options.stageId);
|
|
254
|
-
if (index >= 0) {
|
|
255
|
-
const clone = item.clones[index];
|
|
256
|
-
if (clone === undefined) {
|
|
257
|
-
return {
|
|
258
|
-
ok: false,
|
|
259
|
-
reason: `missing clone envelope for instance "${options.stageId}"`,
|
|
260
|
-
};
|
|
261
|
-
}
|
|
262
|
-
return { ok: true, prior: structuredClone(clone.envelope) };
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
215
|
return { ok: true, prior: structuredClone(parent) };
|
|
267
216
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { ForkGenerationRecord, RunStore } from "../runstore/port.js";
|
|
2
2
|
import type { ResolvedPipelineDag } from "../types/pipeline.js";
|
|
3
|
-
/** Persistent route stages that fan out to
|
|
3
|
+
/** Persistent route stages that fan out to Clone Chain children (or already have instances). */
|
|
4
4
|
export declare function routeRelatedForkParentIds(dag: ResolvedPipelineDag, routeStageIds: readonly string[]): string[];
|
|
5
|
-
/** Clone instance ids currently under
|
|
5
|
+
/** Clone instance ids currently under Clone Chain children of a fork parent. */
|
|
6
6
|
export declare function cloneInstanceIdsForForkParent(dag: ResolvedPipelineDag, forkParentStageId: string): string[];
|
|
7
7
|
export declare function backfillActiveGenerationIfNeeded(store: RunStore, runId: string, options: {
|
|
8
8
|
dag: ResolvedPipelineDag;
|
|
@@ -63,6 +63,10 @@ export declare function selectActiveInput(options: {
|
|
|
63
63
|
needsStageId: string;
|
|
64
64
|
scheduleOverride?: ReadonlyMap<string, ReadonlySet<string>>;
|
|
65
65
|
}): Promise<ActiveCohort>;
|
|
66
|
+
export declare function forkCohortMapsFromStore(store: Pick<RunStore, "listForkGenerations">, runId: string): Promise<{
|
|
67
|
+
supersededCloneIds: Set<string>;
|
|
68
|
+
cohortByForkParent: Map<string, Set<string>>;
|
|
69
|
+
}>;
|
|
66
70
|
export declare function filterJoinInputs(allInstanceIds: readonly string[], cohort: ActiveCohort): string[];
|
|
67
71
|
export declare function filterJoinInstanceIds(allInstanceIds: readonly string[], activeCloneIds: Set<string> | null): string[];
|
|
68
72
|
/**
|