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
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { loadFailure, loadSuccess } from "./loadOutcome.js";
|
|
2
|
+
import { toRouteEdges, toRouteLoopEntries } from "./pipelineRoute.js";
|
|
3
|
+
function isPlainObject(value) {
|
|
4
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5
|
+
}
|
|
6
|
+
function namedSchemaRef(schema) {
|
|
7
|
+
if (!isPlainObject(schema))
|
|
8
|
+
return undefined;
|
|
9
|
+
const keys = Object.keys(schema);
|
|
10
|
+
if (keys.length !== 1 || keys[0] !== "$ref")
|
|
11
|
+
return undefined;
|
|
12
|
+
const ref = schema.$ref;
|
|
13
|
+
if (typeof ref !== "string" || !ref.startsWith("#/schemas/"))
|
|
14
|
+
return undefined;
|
|
15
|
+
const name = ref.slice("#/schemas/".length);
|
|
16
|
+
if (!name || name.includes("/"))
|
|
17
|
+
return undefined;
|
|
18
|
+
return ref;
|
|
19
|
+
}
|
|
20
|
+
function inspectOutput(schema) {
|
|
21
|
+
const empty = {
|
|
22
|
+
namedRefArrays: [],
|
|
23
|
+
nonRefArrayFields: [],
|
|
24
|
+
isRootArray: false,
|
|
25
|
+
rootArrayRef: undefined,
|
|
26
|
+
};
|
|
27
|
+
if (!isPlainObject(schema))
|
|
28
|
+
return empty;
|
|
29
|
+
if (schema.type === "array") {
|
|
30
|
+
const ref = namedSchemaRef(schema.items);
|
|
31
|
+
return {
|
|
32
|
+
namedRefArrays: [],
|
|
33
|
+
nonRefArrayFields: ref === undefined ? ["<root>"] : [],
|
|
34
|
+
isRootArray: true,
|
|
35
|
+
rootArrayRef: ref,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
if (schema.type !== "object" || !isPlainObject(schema.properties))
|
|
39
|
+
return empty;
|
|
40
|
+
const namedRefArrays = [];
|
|
41
|
+
const nonRefArrayFields = [];
|
|
42
|
+
for (const [field, prop] of Object.entries(schema.properties)) {
|
|
43
|
+
if (!isPlainObject(prop) || prop.type !== "array")
|
|
44
|
+
continue;
|
|
45
|
+
const ref = namedSchemaRef(prop.items);
|
|
46
|
+
if (ref !== undefined)
|
|
47
|
+
namedRefArrays.push({ field, ref });
|
|
48
|
+
else
|
|
49
|
+
nonRefArrayFields.push(field);
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
namedRefArrays,
|
|
53
|
+
nonRefArrayFields,
|
|
54
|
+
isRootArray: false,
|
|
55
|
+
rootArrayRef: undefined,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function inboundSources(refs, targetId) {
|
|
59
|
+
return refs.filter((ref) => toRouteEdges(ref.route).some((edge) => edge.to === targetId));
|
|
60
|
+
}
|
|
61
|
+
function isSealedThreeStagePath(refs, stageById, refById, emitter, childId) {
|
|
62
|
+
const edges = toRouteEdges(emitter.route);
|
|
63
|
+
if (edges.length !== 1)
|
|
64
|
+
return false;
|
|
65
|
+
const childEdge = edges[0];
|
|
66
|
+
if (childEdge.to !== childId || childEdge.if !== undefined)
|
|
67
|
+
return false;
|
|
68
|
+
const childStage = stageById.get(childId);
|
|
69
|
+
const childRef = refById.get(childId);
|
|
70
|
+
if (!childStage || !childRef)
|
|
71
|
+
return false;
|
|
72
|
+
if (inboundSources(refs, childId).length !== 1)
|
|
73
|
+
return false;
|
|
74
|
+
const childEdges = toRouteEdges(childRef.route);
|
|
75
|
+
if (childEdges.length !== 1 || childEdges[0].if !== undefined)
|
|
76
|
+
return false;
|
|
77
|
+
const joinId = childEdges[0].to;
|
|
78
|
+
if (!stageById.has(joinId))
|
|
79
|
+
return false;
|
|
80
|
+
if (inboundSources(refs, joinId).length !== 1)
|
|
81
|
+
return false;
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
function dagError(pipelineId, message, stageId) {
|
|
85
|
+
return {
|
|
86
|
+
code: "pipeline.dag_error",
|
|
87
|
+
message: `Pipeline ${pipelineId}: ${message}`,
|
|
88
|
+
category: "pipeline",
|
|
89
|
+
pipelineId,
|
|
90
|
+
...(stageId !== undefined ? { stageId } : {}),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function fail(pipelineId, message, stageId) {
|
|
94
|
+
return loadFailure([dagError(pipelineId, message, stageId)]);
|
|
95
|
+
}
|
|
96
|
+
function childOutputIsCloneArray(schema) {
|
|
97
|
+
const inspected = inspectOutput(schema);
|
|
98
|
+
return inspected.namedRefArrays.length > 0 || inspected.rootArrayRef !== undefined;
|
|
99
|
+
}
|
|
100
|
+
export function detectCloneChains(stages, refs, pipelineId) {
|
|
101
|
+
const stageById = new Map(stages.map((stage) => [stage.id, stage]));
|
|
102
|
+
const refById = new Map(refs.map((ref) => [ref.id, ref]));
|
|
103
|
+
const chains = [];
|
|
104
|
+
for (const emitter of refs) {
|
|
105
|
+
const emitterStage = stageById.get(emitter.id);
|
|
106
|
+
if (!emitterStage)
|
|
107
|
+
continue;
|
|
108
|
+
const edges = toRouteEdges(emitter.route);
|
|
109
|
+
const output = inspectOutput(emitterStage.payload_schema);
|
|
110
|
+
if (output.namedRefArrays.length >= 2 && edges.length > 0) {
|
|
111
|
+
return fail(pipelineId, `stage "${emitter.id}": Clone Chain emitter output must have exactly one named $ref Clone Array`, emitter.id);
|
|
112
|
+
}
|
|
113
|
+
if (output.isRootArray && edges.length > 0) {
|
|
114
|
+
const looksLikeChain = edges.some((edge) => {
|
|
115
|
+
const childStage = stageById.get(edge.to);
|
|
116
|
+
if (!childStage)
|
|
117
|
+
return false;
|
|
118
|
+
if (namedSchemaRef(childStage.clone_input_schema) === undefined)
|
|
119
|
+
return false;
|
|
120
|
+
return isSealedThreeStagePath(refs, stageById, refById, emitter, edge.to);
|
|
121
|
+
});
|
|
122
|
+
if (looksLikeChain) {
|
|
123
|
+
return fail(pipelineId, `stage "${emitter.id}": Clone Chain emitter output is a root-level array, which is not a Clone Array`, emitter.id);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (output.namedRefArrays.length === 0 &&
|
|
127
|
+
output.nonRefArrayFields.length > 0 &&
|
|
128
|
+
edges.length > 0) {
|
|
129
|
+
for (const edge of edges) {
|
|
130
|
+
const childStage = stageById.get(edge.to);
|
|
131
|
+
const childRef = refById.get(edge.to);
|
|
132
|
+
if (!childStage || !childRef)
|
|
133
|
+
continue;
|
|
134
|
+
if (namedSchemaRef(childStage.clone_input_schema) === undefined)
|
|
135
|
+
continue;
|
|
136
|
+
if (toRouteEdges(childRef.route).length === 0)
|
|
137
|
+
continue;
|
|
138
|
+
return fail(pipelineId, `stage "${emitter.id}": Clone Chain emitter Clone Array items must be a named $ref`, emitter.id);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (output.namedRefArrays.length !== 1 || edges.length === 0)
|
|
142
|
+
continue;
|
|
143
|
+
const array = output.namedRefArrays[0];
|
|
144
|
+
const childTakesNamedRef = edges.some((edge) => {
|
|
145
|
+
const childStage = stageById.get(edge.to);
|
|
146
|
+
return (childStage !== undefined &&
|
|
147
|
+
namedSchemaRef(childStage.clone_input_schema) !== undefined);
|
|
148
|
+
});
|
|
149
|
+
const hasClonePolicy = emitter.clone_cap !== undefined || emitter.clone_mode !== undefined;
|
|
150
|
+
const sealedLookalike = edges.some((edge) => isSealedThreeStagePath(refs, stageById, refById, emitter, edge.to));
|
|
151
|
+
if (!childTakesNamedRef && !hasClonePolicy && !sealedLookalike) {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
const matchingChildIds = edges
|
|
155
|
+
.filter((edge) => {
|
|
156
|
+
const childStage = stageById.get(edge.to);
|
|
157
|
+
return (childStage !== undefined &&
|
|
158
|
+
namedSchemaRef(childStage.clone_input_schema) === array.ref);
|
|
159
|
+
})
|
|
160
|
+
.map((edge) => edge.to);
|
|
161
|
+
const uniqueMatching = [...new Set(matchingChildIds)];
|
|
162
|
+
if (edges.length !== 1) {
|
|
163
|
+
if (uniqueMatching.length >= 2) {
|
|
164
|
+
return fail(pipelineId, `stage "${emitter.id}": Clone Chain emitter must have exactly one clone child`, emitter.id);
|
|
165
|
+
}
|
|
166
|
+
return fail(pipelineId, `stage "${emitter.id}": Clone Chain emitter must have exactly one forward Route (the clone child)`, emitter.id);
|
|
167
|
+
}
|
|
168
|
+
const childEdge = edges[0];
|
|
169
|
+
const cloneChildId = childEdge.to;
|
|
170
|
+
if (childEdge.if !== undefined) {
|
|
171
|
+
return fail(pipelineId, `stage "${emitter.id}": Clone Chain inbound edge to the clone child must not have if`, emitter.id);
|
|
172
|
+
}
|
|
173
|
+
const childStage = stageById.get(cloneChildId);
|
|
174
|
+
const childRef = refById.get(cloneChildId);
|
|
175
|
+
if (!childStage || !childRef) {
|
|
176
|
+
return fail(pipelineId, `stage "${emitter.id}": Clone Chain emitter must have exactly one forward Route (the clone child)`, emitter.id);
|
|
177
|
+
}
|
|
178
|
+
if (namedSchemaRef(childStage.clone_input_schema) !== array.ref) {
|
|
179
|
+
return fail(pipelineId, `stage "${cloneChildId}": Clone Chain clone child input must be exactly the named $ref of the emitter Clone Array`, cloneChildId);
|
|
180
|
+
}
|
|
181
|
+
if (inboundSources(refs, cloneChildId).length !== 1) {
|
|
182
|
+
return fail(pipelineId, `stage "${cloneChildId}": Clone Chain clone child must have exactly one parent (the emitter)`, cloneChildId);
|
|
183
|
+
}
|
|
184
|
+
const childEdges = toRouteEdges(childRef.route);
|
|
185
|
+
if (childEdges.length !== 1) {
|
|
186
|
+
return fail(pipelineId, `stage "${cloneChildId}": Clone Chain clone child must have exactly one forward Route (the Join)`, cloneChildId);
|
|
187
|
+
}
|
|
188
|
+
if (childEdges[0].if !== undefined) {
|
|
189
|
+
return fail(pipelineId, `stage "${cloneChildId}": Clone Chain inbound edge to the Join must not have if`, cloneChildId);
|
|
190
|
+
}
|
|
191
|
+
const joinId = childEdges[0].to;
|
|
192
|
+
if (!stageById.has(joinId)) {
|
|
193
|
+
return fail(pipelineId, `stage "${cloneChildId}": Clone Chain clone child must have exactly one forward Route (the Join)`, cloneChildId);
|
|
194
|
+
}
|
|
195
|
+
if (inboundSources(refs, joinId).length !== 1) {
|
|
196
|
+
return fail(pipelineId, `stage "${joinId}": Clone Chain Join must have exactly one parent (the clone child)`, joinId);
|
|
197
|
+
}
|
|
198
|
+
if (childOutputIsCloneArray(childStage.payload_schema)) {
|
|
199
|
+
return fail(pipelineId, `stage "${cloneChildId}": Clone Chain clone child output must not be a Clone Array`, cloneChildId);
|
|
200
|
+
}
|
|
201
|
+
chains.push({
|
|
202
|
+
emitterId: emitter.id,
|
|
203
|
+
cloneChildId,
|
|
204
|
+
joinId,
|
|
205
|
+
arrayField: array.field,
|
|
206
|
+
ref: array.ref,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
const emitterIds = new Set();
|
|
210
|
+
const cloneChildIds = new Set();
|
|
211
|
+
const joinIds = new Set();
|
|
212
|
+
for (const chain of chains) {
|
|
213
|
+
if (emitterIds.has(chain.emitterId)) {
|
|
214
|
+
return fail(pipelineId, `stage "${chain.emitterId}": Clone Chain emitter is shared with another Clone Chain`, chain.emitterId);
|
|
215
|
+
}
|
|
216
|
+
if (cloneChildIds.has(chain.cloneChildId)) {
|
|
217
|
+
return fail(pipelineId, `stage "${chain.cloneChildId}": Clone Chain clone child is shared with another Clone Chain`, chain.cloneChildId);
|
|
218
|
+
}
|
|
219
|
+
if (joinIds.has(chain.joinId)) {
|
|
220
|
+
return fail(pipelineId, `stage "${chain.joinId}": Clone Chain Join is shared with another Clone Chain`, chain.joinId);
|
|
221
|
+
}
|
|
222
|
+
emitterIds.add(chain.emitterId);
|
|
223
|
+
cloneChildIds.add(chain.cloneChildId);
|
|
224
|
+
joinIds.add(chain.joinId);
|
|
225
|
+
}
|
|
226
|
+
return loadSuccess(chains);
|
|
227
|
+
}
|
|
228
|
+
function validateCloneChainLoops(refs, chains, pipelineId) {
|
|
229
|
+
if (chains.length === 0)
|
|
230
|
+
return loadSuccess(undefined);
|
|
231
|
+
const emitterIds = new Set(chains.map((chain) => chain.emitterId));
|
|
232
|
+
const cloneChildIds = new Set(chains.map((chain) => chain.cloneChildId));
|
|
233
|
+
const forbiddenTargets = new Map();
|
|
234
|
+
for (const chain of chains) {
|
|
235
|
+
forbiddenTargets.set(chain.emitterId, "emitter");
|
|
236
|
+
forbiddenTargets.set(chain.cloneChildId, "clone child");
|
|
237
|
+
forbiddenTargets.set(chain.joinId, "Join");
|
|
238
|
+
}
|
|
239
|
+
for (const ref of refs) {
|
|
240
|
+
const loops = toRouteLoopEntries(ref.route);
|
|
241
|
+
if (loops.length === 0)
|
|
242
|
+
continue;
|
|
243
|
+
if (emitterIds.has(ref.id)) {
|
|
244
|
+
return loadFailure([
|
|
245
|
+
dagError(pipelineId, `stage "${ref.id}": Clone Chain emitter cannot originate a Loop`, ref.id),
|
|
246
|
+
]);
|
|
247
|
+
}
|
|
248
|
+
if (cloneChildIds.has(ref.id)) {
|
|
249
|
+
return loadFailure([
|
|
250
|
+
dagError(pipelineId, `stage "${ref.id}": Clone Chain clone child cannot originate a Loop`, ref.id),
|
|
251
|
+
]);
|
|
252
|
+
}
|
|
253
|
+
for (const loop of loops) {
|
|
254
|
+
const role = forbiddenTargets.get(loop.to);
|
|
255
|
+
if (role === undefined)
|
|
256
|
+
continue;
|
|
257
|
+
return loadFailure([
|
|
258
|
+
dagError(pipelineId, `stage "${ref.id}": Loop target "${loop.to}" is a Clone Chain ${role}`, ref.id),
|
|
259
|
+
]);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return loadSuccess(undefined);
|
|
263
|
+
}
|
|
264
|
+
function applyCloneCapToSchema(schema, field, cap) {
|
|
265
|
+
if (!isPlainObject(schema) || !isPlainObject(schema.properties))
|
|
266
|
+
return;
|
|
267
|
+
const prop = schema.properties[field];
|
|
268
|
+
if (!isPlainObject(prop))
|
|
269
|
+
return;
|
|
270
|
+
prop.minItems = 1;
|
|
271
|
+
prop.maxItems = cap;
|
|
272
|
+
}
|
|
273
|
+
export function applyCloneChains(stages, refs, dag, pipelineId) {
|
|
274
|
+
const detected = detectCloneChains(stages, refs, pipelineId);
|
|
275
|
+
if (!detected.ok)
|
|
276
|
+
return detected;
|
|
277
|
+
const chains = detected.value;
|
|
278
|
+
const emitterIds = new Set(chains.map((chain) => chain.emitterId));
|
|
279
|
+
const refById = new Map(refs.map((ref) => [ref.id, ref]));
|
|
280
|
+
const nodeById = new Map(dag.nodes.map((node) => [node.id, node]));
|
|
281
|
+
for (const ref of refs) {
|
|
282
|
+
const isEmitter = emitterIds.has(ref.id);
|
|
283
|
+
if (isEmitter) {
|
|
284
|
+
if (ref.clone_cap === undefined || ref.clone_mode === undefined) {
|
|
285
|
+
return loadFailure([
|
|
286
|
+
dagError(pipelineId, `stage "${ref.id}": Clone Chain emitter requires clone_cap and clone_mode`, ref.id),
|
|
287
|
+
]);
|
|
288
|
+
}
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
if (ref.clone_cap !== undefined) {
|
|
292
|
+
return loadFailure([
|
|
293
|
+
dagError(pipelineId, `stage "${ref.id}": "clone_cap" is no longer supported — use a Clone Chain instead`, ref.id),
|
|
294
|
+
]);
|
|
295
|
+
}
|
|
296
|
+
if (ref.clone_mode !== undefined) {
|
|
297
|
+
return loadFailure([
|
|
298
|
+
dagError(pipelineId, `stage "${ref.id}": "clone_mode" is only valid on a Clone Chain emitter`, ref.id),
|
|
299
|
+
]);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
const loopsOutcome = validateCloneChainLoops(refs, chains, pipelineId);
|
|
303
|
+
if (!loopsOutcome.ok)
|
|
304
|
+
return loopsOutcome;
|
|
305
|
+
const stageById = new Map(stages.map((stage) => [stage.id, stage]));
|
|
306
|
+
for (const chain of chains) {
|
|
307
|
+
const ref = refById.get(chain.emitterId);
|
|
308
|
+
const cap = ref.clone_cap;
|
|
309
|
+
const mode = ref.clone_mode;
|
|
310
|
+
const node = nodeById.get(chain.emitterId);
|
|
311
|
+
if (node) {
|
|
312
|
+
node.clone_cap = cap;
|
|
313
|
+
node.clone_mode = mode;
|
|
314
|
+
node.clone_array_field = chain.arrayField;
|
|
315
|
+
}
|
|
316
|
+
const emitterStage = stageById.get(chain.emitterId);
|
|
317
|
+
if (emitterStage?.payload_schema !== undefined) {
|
|
318
|
+
applyCloneCapToSchema(emitterStage.payload_schema, chain.arrayField, cap);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return loadSuccess(undefined);
|
|
322
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { access, mkdir, readdir, unlink, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { parsePipelineNeeds } from "./pipelineNeeds.js";
|
|
3
|
+
import { invertPredecessorEdgesToRoute, invertRouteToPredecessorEdges, parsePipelineNeeds, toPipelineNeeds, } from "./pipelineNeeds.js";
|
|
4
4
|
import { STAGE_ID_PATTERN } from "./createStage.js";
|
|
5
5
|
import { inferIdFromUsesPath, normalizePipelineStageEntries, toWiringRefs, } from "./normalizePipelineStageEntry.js";
|
|
6
6
|
import { parseModelField } from "./modelField.js";
|
|
@@ -29,10 +29,12 @@ export function normalizeCreatePipelineStages(stages) {
|
|
|
29
29
|
uses: stage.uses ?? (stage.inline ? undefined : defaultUsesPath(stage.id)),
|
|
30
30
|
}));
|
|
31
31
|
}
|
|
32
|
-
function stageRefToRaw(ref) {
|
|
32
|
+
function stageRefToRaw(ref, routeInfo) {
|
|
33
33
|
const raw = { id: ref.id };
|
|
34
|
-
if (
|
|
35
|
-
raw.
|
|
34
|
+
if (routeInfo?.entry)
|
|
35
|
+
raw.entry = true;
|
|
36
|
+
if (routeInfo?.route && routeInfo.route.length > 0)
|
|
37
|
+
raw.route = routeInfo.route;
|
|
36
38
|
if (ref.uses !== undefined)
|
|
37
39
|
raw.uses = ref.uses;
|
|
38
40
|
if (ref.inline) {
|
|
@@ -47,16 +49,20 @@ function stageRefToRaw(ref) {
|
|
|
47
49
|
return raw;
|
|
48
50
|
}
|
|
49
51
|
function toAuthoringRawEntries(stages, declaringPath) {
|
|
52
|
+
const routeById = invertPredecessorEdgesToRoute(stages);
|
|
50
53
|
return stages.map((stage) => ({
|
|
51
|
-
raw: stageRefToRaw(stage),
|
|
54
|
+
raw: stageRefToRaw(stage, routeById.get(stage.id)),
|
|
52
55
|
declaringPath,
|
|
53
56
|
}));
|
|
54
57
|
}
|
|
55
58
|
function normalizedToCreateRefs(entries) {
|
|
59
|
+
const inbound = invertRouteToPredecessorEdges(entries);
|
|
56
60
|
return entries.map((entry) => {
|
|
57
61
|
const ref = { id: entry.id };
|
|
58
|
-
|
|
59
|
-
|
|
62
|
+
const edges = inbound.get(entry.id);
|
|
63
|
+
const needs = edges !== undefined ? toPipelineNeeds(edges) : undefined;
|
|
64
|
+
if (needs !== undefined)
|
|
65
|
+
ref.needs = needs;
|
|
60
66
|
if (entry.body.kind === "uses") {
|
|
61
67
|
ref.uses = entry.body.path;
|
|
62
68
|
}
|
|
@@ -236,21 +242,86 @@ export function parseCreatePipelineBody(body) {
|
|
|
236
242
|
stages: refs,
|
|
237
243
|
};
|
|
238
244
|
}
|
|
239
|
-
function
|
|
240
|
-
if (typeof
|
|
241
|
-
|
|
245
|
+
function formatRouteIfValue(value) {
|
|
246
|
+
if (typeof value === "string")
|
|
247
|
+
return JSON.stringify(value);
|
|
248
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
249
|
+
return String(value);
|
|
250
|
+
if (value === null)
|
|
251
|
+
return "null";
|
|
252
|
+
if (Array.isArray(value)) {
|
|
253
|
+
return `[${value.map((item) => formatRouteIfValue(item)).join(", ")}]`;
|
|
254
|
+
}
|
|
255
|
+
return JSON.stringify(value);
|
|
256
|
+
}
|
|
257
|
+
function appendRouteIfYamlListItem(lines, indent, predicate) {
|
|
258
|
+
if ("all" in predicate) {
|
|
259
|
+
lines.push(`${indent}- all:`);
|
|
260
|
+
for (const child of predicate.all) {
|
|
261
|
+
appendRouteIfYamlListItem(lines, `${indent} `, child);
|
|
262
|
+
}
|
|
242
263
|
return;
|
|
243
264
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
lines
|
|
248
|
-
|
|
265
|
+
if ("any" in predicate) {
|
|
266
|
+
lines.push(`${indent}- any:`);
|
|
267
|
+
for (const child of predicate.any) {
|
|
268
|
+
appendRouteIfYamlListItem(lines, `${indent} `, child);
|
|
269
|
+
}
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if ("not" in predicate) {
|
|
273
|
+
lines.push(`${indent}- not:`);
|
|
274
|
+
appendRouteIfYaml(lines, `${indent} `, predicate.not);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
lines.push(`${indent}- field: ${formatInlineYamlScalar(predicate.field)}`);
|
|
278
|
+
lines.push(`${indent} op: ${predicate.op}`);
|
|
279
|
+
lines.push(`${indent} value: ${formatRouteIfValue(predicate.value)}`);
|
|
280
|
+
}
|
|
281
|
+
function appendRouteIfYaml(lines, indent, predicate) {
|
|
282
|
+
if ("all" in predicate) {
|
|
283
|
+
lines.push(`${indent}all:`);
|
|
284
|
+
for (const child of predicate.all) {
|
|
285
|
+
appendRouteIfYamlListItem(lines, `${indent} `, child);
|
|
286
|
+
}
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if ("any" in predicate) {
|
|
290
|
+
lines.push(`${indent}any:`);
|
|
291
|
+
for (const child of predicate.any) {
|
|
292
|
+
appendRouteIfYamlListItem(lines, `${indent} `, child);
|
|
293
|
+
}
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
if ("not" in predicate) {
|
|
297
|
+
lines.push(`${indent}not:`);
|
|
298
|
+
appendRouteIfYaml(lines, `${indent} `, predicate.not);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
lines.push(`${indent}field: ${formatInlineYamlScalar(predicate.field)}`);
|
|
302
|
+
lines.push(`${indent}op: ${predicate.op}`);
|
|
303
|
+
lines.push(`${indent}value: ${formatRouteIfValue(predicate.value)}`);
|
|
304
|
+
}
|
|
305
|
+
function appendRouteYaml(lines, routeInfo) {
|
|
306
|
+
if (routeInfo.entry) {
|
|
307
|
+
lines.push(" entry: true");
|
|
308
|
+
}
|
|
309
|
+
if (!routeInfo.route || routeInfo.route.length === 0) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
lines.push(" route:");
|
|
313
|
+
for (const item of routeInfo.route) {
|
|
314
|
+
const on = item.on === undefined ? ["succeeded"] : Array.isArray(item.on) ? item.on : [item.on];
|
|
315
|
+
lines.push(` - to: ${item.to}`);
|
|
316
|
+
if (!(on.length === 1 && on[0] === "succeeded")) {
|
|
317
|
+
lines.push(" on:");
|
|
318
|
+
for (const state of on) {
|
|
319
|
+
lines.push(` - ${state}`);
|
|
320
|
+
}
|
|
249
321
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
lines.push(` - ${state}`);
|
|
322
|
+
if (item.if !== undefined) {
|
|
323
|
+
lines.push(" if:");
|
|
324
|
+
appendRouteIfYaml(lines, " ", item.if);
|
|
254
325
|
}
|
|
255
326
|
}
|
|
256
327
|
}
|
|
@@ -266,6 +337,7 @@ function formatInlineYamlScalar(value) {
|
|
|
266
337
|
return value;
|
|
267
338
|
}
|
|
268
339
|
export function pipelineConfigToYaml(pipeline, options) {
|
|
340
|
+
const routeById = invertPredecessorEdgesToRoute(pipeline.stages);
|
|
269
341
|
const lines = [`id: ${pipeline.id}`, "stages:"];
|
|
270
342
|
for (const stage of pipeline.stages) {
|
|
271
343
|
if (options.format === "linear" && !stage.needs && !stage.inline && stage.uses) {
|
|
@@ -274,8 +346,9 @@ export function pipelineConfigToYaml(pipeline, options) {
|
|
|
274
346
|
continue;
|
|
275
347
|
}
|
|
276
348
|
lines.push(` - id: ${stage.id}`);
|
|
277
|
-
|
|
278
|
-
|
|
349
|
+
const routeInfo = routeById.get(stage.id);
|
|
350
|
+
if (routeInfo) {
|
|
351
|
+
appendRouteYaml(lines, routeInfo);
|
|
279
352
|
}
|
|
280
353
|
if (stage.inline) {
|
|
281
354
|
if (stage.inline.gate_kinds !== undefined) {
|
|
@@ -301,6 +374,13 @@ export function pipelineConfigToYaml(pipeline, options) {
|
|
|
301
374
|
if (stage.inline.model !== undefined && stage.inline.model.length > 0) {
|
|
302
375
|
lines.push(` model: ${formatInlineYamlScalar(stage.inline.model)}`);
|
|
303
376
|
}
|
|
377
|
+
lines.push(" io:");
|
|
378
|
+
lines.push(" input:");
|
|
379
|
+
lines.push(" schema:");
|
|
380
|
+
lines.push(" type: object");
|
|
381
|
+
lines.push(" output:");
|
|
382
|
+
lines.push(" schema:");
|
|
383
|
+
lines.push(" type: object");
|
|
304
384
|
}
|
|
305
385
|
else if (stage.uses) {
|
|
306
386
|
lines.push(` uses: ${stage.uses}`);
|
|
@@ -122,6 +122,13 @@ export function stageConfigToYaml(stage) {
|
|
|
122
122
|
if (stage.model !== undefined && stage.model.length > 0) {
|
|
123
123
|
lines.push(`model: ${formatInlineYamlScalar(stage.model)}`);
|
|
124
124
|
}
|
|
125
|
+
lines.push("io:");
|
|
126
|
+
lines.push(" input:");
|
|
127
|
+
lines.push(" schema:");
|
|
128
|
+
lines.push(" type: object");
|
|
129
|
+
lines.push(" output:");
|
|
130
|
+
lines.push(" schema:");
|
|
131
|
+
lines.push(" type: object");
|
|
125
132
|
lines.push("");
|
|
126
133
|
return lines.join("\n");
|
|
127
134
|
}
|
|
@@ -6,8 +6,10 @@ export declare function resolvePipelinePath(pipelinePath: string, cwd: string):
|
|
|
6
6
|
export declare function loadPipelineOutcome(nameOrPath: string, options?: {
|
|
7
7
|
cwd?: string;
|
|
8
8
|
projectRoot?: string;
|
|
9
|
+
requireIo?: boolean;
|
|
9
10
|
}): Promise<LoadOutcome<LoadedPipeline>>;
|
|
10
11
|
export declare function loadPipeline(nameOrPath: string, options?: {
|
|
11
12
|
cwd?: string;
|
|
12
13
|
projectRoot?: string;
|
|
14
|
+
requireIo?: boolean;
|
|
13
15
|
}): Promise<LoadedPipeline>;
|
|
@@ -10,6 +10,8 @@ import { predecessorEdges } from "./pipelineNeeds.js";
|
|
|
10
10
|
import { resolvePipelineDagFromRefs } from "./resolvePipelineDag.js";
|
|
11
11
|
import { recoveryRequiresCompletionIssue } from "./parseCompletionContract.js";
|
|
12
12
|
import { validateCompletionContractForStage } from "./validateCompletionContract.js";
|
|
13
|
+
import { applyCloneChains } from "./cloneChain.js";
|
|
14
|
+
import { collectRouteIfSchemaIssues, collectRouteIfIllegalCombos, collectRouteAllGatedWarnings, } from "./routeIf.js";
|
|
13
15
|
function schemaCompileIssue(stageId, field, err, pipelineId) {
|
|
14
16
|
const message = err instanceof Error ? err.message : String(err);
|
|
15
17
|
if (err instanceof UnresolvedSchemaRefError) {
|
|
@@ -59,26 +61,23 @@ function checkSequentialIoCompatibility(stages, dag, pipelineId, schemas) {
|
|
|
59
61
|
const node = nodeById.get(child.id);
|
|
60
62
|
if (!node)
|
|
61
63
|
continue;
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
pipelineId,
|
|
80
|
-
},
|
|
81
|
-
]);
|
|
64
|
+
for (const parentEdge of predecessorEdges(node)) {
|
|
65
|
+
const parentNode = nodeById.get(parentEdge.id);
|
|
66
|
+
if (parentNode?.clone_array_field !== undefined)
|
|
67
|
+
continue;
|
|
68
|
+
const parent = stageById.get(parentEdge.id);
|
|
69
|
+
if (!parent?.payload_schema || child.clone_input_schema === undefined)
|
|
70
|
+
continue;
|
|
71
|
+
if (!isPayloadSchemaSubset(child.clone_input_schema, parent.payload_schema, options)) {
|
|
72
|
+
return loadFailure([
|
|
73
|
+
{
|
|
74
|
+
code: "pipeline.io_incompatible",
|
|
75
|
+
message: `Pipeline ${pipelineId}: stage "${child.id}" io.input is not a structural subset of "${parent.id}" io.output`,
|
|
76
|
+
category: "pipeline",
|
|
77
|
+
pipelineId,
|
|
78
|
+
},
|
|
79
|
+
]);
|
|
80
|
+
}
|
|
82
81
|
}
|
|
83
82
|
}
|
|
84
83
|
return loadSuccess(undefined);
|
|
@@ -93,7 +92,7 @@ export async function resolvePipelinePath(pipelinePath, cwd) {
|
|
|
93
92
|
throw new Error(`Pipeline file not found: ${resolved}. Pass a filesystem path to --pipeline (e.g. pipelines/hello.pipeline.yaml), not a bare pipeline id.`);
|
|
94
93
|
}
|
|
95
94
|
}
|
|
96
|
-
async function loadPipelineFromPath(pipelinePath, cwd, projectRoot = cwd) {
|
|
95
|
+
async function loadPipelineFromPath(pipelinePath, cwd, projectRoot = cwd, requireIo = true) {
|
|
97
96
|
const normalizedPipelinePath = path.normalize(path.resolve(pipelinePath));
|
|
98
97
|
const mergeOutcome = await mergePipelineStages(normalizedPipelinePath);
|
|
99
98
|
if (!mergeOutcome.ok) {
|
|
@@ -147,6 +146,7 @@ async function loadPipelineFromPath(pipelinePath, cwd, projectRoot = cwd) {
|
|
|
147
146
|
entryId: entry.id,
|
|
148
147
|
declaringPath: entry.declaringPath,
|
|
149
148
|
deferSchemaRefs: true,
|
|
149
|
+
requireIo,
|
|
150
150
|
});
|
|
151
151
|
if (!inlineOutcome.ok) {
|
|
152
152
|
return loadFailure(inlineOutcome.issues);
|
|
@@ -157,6 +157,7 @@ async function loadPipelineFromPath(pipelinePath, cwd, projectRoot = cwd) {
|
|
|
157
157
|
}
|
|
158
158
|
const stageOutcome = await loadStageOutcome(entry.body.absolutePath, {
|
|
159
159
|
deferSchemaRefs: true,
|
|
160
|
+
requireIo,
|
|
160
161
|
});
|
|
161
162
|
if (!stageOutcome.ok) {
|
|
162
163
|
if (stageOutcome.issues.some((issue) => issue.code === "catalog.mixed_yaml_dialect")) {
|
|
@@ -206,9 +207,20 @@ async function loadPipelineFromPath(pipelinePath, cwd, projectRoot = cwd) {
|
|
|
206
207
|
stages.push(stage);
|
|
207
208
|
stageSources[stageId] = { kind: "file", path: entry.body.absolutePath };
|
|
208
209
|
}
|
|
210
|
+
const cloneChainOutcome = applyCloneChains(stages, wiringRefs, dag, pipelineId);
|
|
211
|
+
if (!cloneChainOutcome.ok)
|
|
212
|
+
return cloneChainOutcome;
|
|
209
213
|
const schemaOutcome = attachPipelineSchemas(stages, pipelineSchemas, pipelineId);
|
|
210
214
|
if (!schemaOutcome.ok)
|
|
211
215
|
return schemaOutcome;
|
|
216
|
+
const routeIfIssues = [
|
|
217
|
+
...collectRouteIfSchemaIssues(stages, wiringRefs, pipelineId),
|
|
218
|
+
...collectRouteIfIllegalCombos(wiringRefs, pipelineId),
|
|
219
|
+
];
|
|
220
|
+
if (routeIfIssues.length > 0) {
|
|
221
|
+
return loadFailure(routeIfIssues);
|
|
222
|
+
}
|
|
223
|
+
warnings.push(...collectRouteAllGatedWarnings(wiringRefs, pipelineId));
|
|
212
224
|
const ioOutcome = checkSequentialIoCompatibility(stages, dag, pipelineId, pipelineSchemas);
|
|
213
225
|
if (!ioOutcome.ok)
|
|
214
226
|
return ioOutcome;
|
|
@@ -283,7 +295,7 @@ export async function loadPipelineOutcome(nameOrPath, options = {}) {
|
|
|
283
295
|
},
|
|
284
296
|
]);
|
|
285
297
|
}
|
|
286
|
-
return loadPipelineFromPath(pipelinePath, cwd, projectRoot);
|
|
298
|
+
return loadPipelineFromPath(pipelinePath, cwd, projectRoot, options.requireIo !== false);
|
|
287
299
|
}
|
|
288
300
|
export async function loadPipeline(nameOrPath, options = {}) {
|
|
289
301
|
const outcome = await loadPipelineOutcome(nameOrPath, options);
|
|
@@ -6,11 +6,13 @@ export declare function afterCompletionForStage(stage: StageConfig): CompletionC
|
|
|
6
6
|
export declare function parseStageMcp(raw: unknown, label: string, stageId?: string): LoadOutcome<string[] | undefined>;
|
|
7
7
|
export type LoadStageOptions = {
|
|
8
8
|
deferSchemaRefs?: boolean;
|
|
9
|
+
requireIo?: boolean;
|
|
9
10
|
};
|
|
10
11
|
export declare function loadStageFromObjectOutcome(raw: Record<string, unknown>, ctx: {
|
|
11
12
|
entryId: string;
|
|
12
13
|
declaringPath: string;
|
|
13
14
|
deferSchemaRefs?: boolean;
|
|
15
|
+
requireIo?: boolean;
|
|
14
16
|
}): LoadOutcome<StageConfig>;
|
|
15
17
|
export declare function loadStageOutcome(filePath: string, options?: LoadStageOptions): Promise<LoadOutcome<StageConfig>>;
|
|
16
18
|
export declare function loadStage(filePath: string): Promise<StageConfig>;
|