stageflow 0.15.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/activity.d.ts +3 -0
- package/dist/agent/claudeAdapter.js +84 -7
- package/dist/agent/claudeSession.d.ts +3 -0
- 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.d.ts +2 -0
- package/dist/agent/piAdapter.js +30 -19
- package/dist/agent/port.d.ts +3 -0
- package/dist/cli/runOutput.js +32 -8
- 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/port.d.ts +10 -0
- package/dist/runstore/runProjection.js +9 -0
- package/dist/runstore/sqlite/SqliteRunStore.js +27 -4
- package/dist/runstore/sqlite/schema.d.ts +1 -1
- package/dist/runstore/sqlite/schema.js +2 -0
- package/dist/runstore/stageExecution.d.ts +3 -0
- package/dist/runstore/stageExecution.js +6 -2
- package/dist/runstore/stageInstanceId.d.ts +1 -0
- package/dist/runstore/stageInstanceId.js +9 -0
- package/dist/runstore/stageSnapshot.js +11 -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/stageRunner.js +2 -2
- 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/types/usage.d.ts +16 -0
- package/dist/types/usage.js +24 -0
- package/dist/ui/assets/{index-uVHiae8s.css → index-DsgVjLUS.css} +1 -1
- package/dist/ui/assets/index-DwKGaQvQ.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-GtyiWiZg.js +0 -118
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { resolvePayloadSchemaFieldPath } from "../envelope/payloadSchema.js";
|
|
2
|
+
const ALLOWED_LEAF_KEYS = new Set(["field", "op", "value"]);
|
|
3
|
+
const ALLOWED_ALL_KEYS = new Set(["all"]);
|
|
4
|
+
const ALLOWED_ANY_KEYS = new Set(["any"]);
|
|
5
|
+
const ALLOWED_NOT_KEYS = new Set(["not"]);
|
|
6
|
+
const ROUTE_IF_OPS = new Set([
|
|
7
|
+
"eq",
|
|
8
|
+
"ne",
|
|
9
|
+
"gt",
|
|
10
|
+
"gte",
|
|
11
|
+
"lt",
|
|
12
|
+
"lte",
|
|
13
|
+
"in",
|
|
14
|
+
"not_in",
|
|
15
|
+
]);
|
|
16
|
+
const EQ_NE_TYPES = new Set(["string", "number", "integer", "boolean"]);
|
|
17
|
+
const NUMERIC_TYPES = new Set(["number", "integer"]);
|
|
18
|
+
const NUMERIC_OPS = new Set(["gt", "gte", "lt", "lte"]);
|
|
19
|
+
const MEMBERSHIP_OPS = new Set(["in", "not_in"]);
|
|
20
|
+
function isPlainObject(value) {
|
|
21
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
22
|
+
}
|
|
23
|
+
function hasOwn(object, key) {
|
|
24
|
+
return Object.prototype.hasOwnProperty.call(object, key);
|
|
25
|
+
}
|
|
26
|
+
function isRouteIfOp(value) {
|
|
27
|
+
return typeof value === "string" && ROUTE_IF_OPS.has(value);
|
|
28
|
+
}
|
|
29
|
+
function failParse(prefix, message) {
|
|
30
|
+
return {
|
|
31
|
+
ok: false,
|
|
32
|
+
message: `${prefix}${message}`,
|
|
33
|
+
code: "pipeline.route_if_invalid",
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function unknownKeyIssue(raw, allowed, prefix) {
|
|
37
|
+
for (const key of Object.keys(raw)) {
|
|
38
|
+
if (!allowed.has(key)) {
|
|
39
|
+
return failParse(prefix, `: unknown key "${key}"`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
function parseRouteIfLeaf(raw, prefix) {
|
|
45
|
+
const extra = unknownKeyIssue(raw, ALLOWED_LEAF_KEYS, prefix);
|
|
46
|
+
if (extra)
|
|
47
|
+
return extra;
|
|
48
|
+
if (typeof raw.field !== "string" || !raw.field) {
|
|
49
|
+
return failParse(prefix, ": field must be a non-empty string");
|
|
50
|
+
}
|
|
51
|
+
if (!isRouteIfOp(raw.op)) {
|
|
52
|
+
return failParse(prefix, `: unknown op "${String(raw.op)}"`);
|
|
53
|
+
}
|
|
54
|
+
if (!hasOwn(raw, "value")) {
|
|
55
|
+
return failParse(prefix, ": value is required");
|
|
56
|
+
}
|
|
57
|
+
if (MEMBERSHIP_OPS.has(raw.op)) {
|
|
58
|
+
if (!Array.isArray(raw.value) || raw.value.length === 0) {
|
|
59
|
+
return failParse(prefix, `: ${raw.op} value must be a non-empty list`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
ok: true,
|
|
64
|
+
value: {
|
|
65
|
+
field: raw.field,
|
|
66
|
+
op: raw.op,
|
|
67
|
+
value: raw.value,
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function parseRouteIfNode(raw, prefix) {
|
|
72
|
+
if (!isPlainObject(raw)) {
|
|
73
|
+
return failParse(prefix, " must be a predicate object");
|
|
74
|
+
}
|
|
75
|
+
const hasAll = hasOwn(raw, "all");
|
|
76
|
+
const hasAny = hasOwn(raw, "any");
|
|
77
|
+
const hasNot = hasOwn(raw, "not");
|
|
78
|
+
const hasField = hasOwn(raw, "field");
|
|
79
|
+
const hasOp = hasOwn(raw, "op");
|
|
80
|
+
const hasValue = hasOwn(raw, "value");
|
|
81
|
+
const compositionCount = Number(hasAll) + Number(hasAny) + Number(hasNot);
|
|
82
|
+
const hasLeafKeys = hasField || hasOp || hasValue;
|
|
83
|
+
if (compositionCount > 1 || (compositionCount === 1 && hasLeafKeys)) {
|
|
84
|
+
return failParse(prefix, " must be exactly one of { field, op, value }, { all }, { any }, or { not }");
|
|
85
|
+
}
|
|
86
|
+
if (hasAll) {
|
|
87
|
+
const extra = unknownKeyIssue(raw, ALLOWED_ALL_KEYS, prefix);
|
|
88
|
+
if (extra)
|
|
89
|
+
return extra;
|
|
90
|
+
return parseRouteIfList(raw.all, prefix, "all");
|
|
91
|
+
}
|
|
92
|
+
if (hasAny) {
|
|
93
|
+
const extra = unknownKeyIssue(raw, ALLOWED_ANY_KEYS, prefix);
|
|
94
|
+
if (extra)
|
|
95
|
+
return extra;
|
|
96
|
+
return parseRouteIfList(raw.any, prefix, "any");
|
|
97
|
+
}
|
|
98
|
+
if (hasNot) {
|
|
99
|
+
const extra = unknownKeyIssue(raw, ALLOWED_NOT_KEYS, prefix);
|
|
100
|
+
if (extra)
|
|
101
|
+
return extra;
|
|
102
|
+
if (!isPlainObject(raw.not)) {
|
|
103
|
+
return failParse(prefix, ": not must be a predicate object");
|
|
104
|
+
}
|
|
105
|
+
const inner = parseRouteIfNode(raw.not, prefix);
|
|
106
|
+
if (!inner.ok)
|
|
107
|
+
return inner;
|
|
108
|
+
return { ok: true, value: { not: inner.value } };
|
|
109
|
+
}
|
|
110
|
+
return parseRouteIfLeaf(raw, prefix);
|
|
111
|
+
}
|
|
112
|
+
function parseRouteIfList(raw, prefix, kind) {
|
|
113
|
+
if (!Array.isArray(raw) || raw.length === 0) {
|
|
114
|
+
return failParse(prefix, `: ${kind} must be a non-empty list`);
|
|
115
|
+
}
|
|
116
|
+
const children = [];
|
|
117
|
+
for (const item of raw) {
|
|
118
|
+
const parsed = parseRouteIfNode(item, prefix);
|
|
119
|
+
if (!parsed.ok)
|
|
120
|
+
return parsed;
|
|
121
|
+
children.push(parsed.value);
|
|
122
|
+
}
|
|
123
|
+
return { ok: true, value: kind === "all" ? { all: children } : { any: children } };
|
|
124
|
+
}
|
|
125
|
+
export function parseRouteIf(raw, stageId, targetId) {
|
|
126
|
+
return parseRouteIfNode(raw, `stage "${stageId}": route "${targetId}" if`);
|
|
127
|
+
}
|
|
128
|
+
function valueMatchesScalarType(value, typeName) {
|
|
129
|
+
if (typeName === "string")
|
|
130
|
+
return typeof value === "string";
|
|
131
|
+
if (typeName === "boolean")
|
|
132
|
+
return typeof value === "boolean";
|
|
133
|
+
if (typeName === "number")
|
|
134
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
135
|
+
if (typeName === "integer")
|
|
136
|
+
return typeof value === "number" && Number.isInteger(value);
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
function isRouteIfLeaf(predicate) {
|
|
140
|
+
return "op" in predicate && predicate.op !== undefined && typeof predicate.field === "string";
|
|
141
|
+
}
|
|
142
|
+
function routeIfLeafSchemaIssue(predicate, payloadSchema, prefix) {
|
|
143
|
+
const path = resolvePayloadSchemaFieldPath(payloadSchema, predicate.field);
|
|
144
|
+
if (!path.ok) {
|
|
145
|
+
if (path.issue === "array_index") {
|
|
146
|
+
return `${prefix}: field "${predicate.field}" array index paths are not allowed`;
|
|
147
|
+
}
|
|
148
|
+
if (path.issue === "optional") {
|
|
149
|
+
return `${prefix}: field "${predicate.field}" must be required on io.output.schema`;
|
|
150
|
+
}
|
|
151
|
+
return `${prefix}: field "${predicate.field}" is not a property of io.output.schema`;
|
|
152
|
+
}
|
|
153
|
+
const fieldType = path.type;
|
|
154
|
+
if (NUMERIC_OPS.has(predicate.op)) {
|
|
155
|
+
if (!NUMERIC_TYPES.has(fieldType)) {
|
|
156
|
+
return `${prefix}: op ${predicate.op} is only valid for number or integer fields`;
|
|
157
|
+
}
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
if (!EQ_NE_TYPES.has(fieldType)) {
|
|
161
|
+
return `${prefix}: op ${predicate.op} is only valid for string, number, integer, or boolean fields`;
|
|
162
|
+
}
|
|
163
|
+
if (MEMBERSHIP_OPS.has(predicate.op)) {
|
|
164
|
+
if (!Array.isArray(predicate.value) || predicate.value.length === 0) {
|
|
165
|
+
return `${prefix}: ${predicate.op} value must be a non-empty list`;
|
|
166
|
+
}
|
|
167
|
+
if (!predicate.value.every((item) => valueMatchesScalarType(item, fieldType))) {
|
|
168
|
+
return `${prefix}: ${predicate.op} list items must be the same scalar type as field "${predicate.field}"`;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
function routeIfFieldSchemaIssue(predicate, payloadSchema, stageId, targetId, pipelineId) {
|
|
174
|
+
const prefix = `Pipeline ${pipelineId}: stage "${stageId}": route "${targetId}" if`;
|
|
175
|
+
if ("all" in predicate && predicate.all !== undefined) {
|
|
176
|
+
for (const child of predicate.all) {
|
|
177
|
+
const issue = routeIfFieldSchemaIssue(child, payloadSchema, stageId, targetId, pipelineId);
|
|
178
|
+
if (issue !== undefined)
|
|
179
|
+
return issue;
|
|
180
|
+
}
|
|
181
|
+
return undefined;
|
|
182
|
+
}
|
|
183
|
+
if ("any" in predicate && predicate.any !== undefined) {
|
|
184
|
+
for (const child of predicate.any) {
|
|
185
|
+
const issue = routeIfFieldSchemaIssue(child, payloadSchema, stageId, targetId, pipelineId);
|
|
186
|
+
if (issue !== undefined)
|
|
187
|
+
return issue;
|
|
188
|
+
}
|
|
189
|
+
return undefined;
|
|
190
|
+
}
|
|
191
|
+
if ("not" in predicate && predicate.not !== undefined) {
|
|
192
|
+
return routeIfFieldSchemaIssue(predicate.not, payloadSchema, stageId, targetId, pipelineId);
|
|
193
|
+
}
|
|
194
|
+
if (!isRouteIfLeaf(predicate))
|
|
195
|
+
return `${prefix}: invalid predicate`;
|
|
196
|
+
return routeIfLeafSchemaIssue(predicate, payloadSchema, prefix);
|
|
197
|
+
}
|
|
198
|
+
function isForwardRouteEntry(entry) {
|
|
199
|
+
return !("type" in entry) || entry.type === undefined;
|
|
200
|
+
}
|
|
201
|
+
export function collectRouteIfIllegalCombos(_refs, _pipelineId) {
|
|
202
|
+
return [];
|
|
203
|
+
}
|
|
204
|
+
export function collectRouteIfSchemaIssues(stages, refs, pipelineId) {
|
|
205
|
+
const stageById = new Map(stages.map((stage) => [stage.id, stage]));
|
|
206
|
+
const issues = [];
|
|
207
|
+
for (const ref of refs) {
|
|
208
|
+
if (ref.route === undefined)
|
|
209
|
+
continue;
|
|
210
|
+
const source = stageById.get(ref.id);
|
|
211
|
+
for (const entry of ref.route) {
|
|
212
|
+
if (!("if" in entry) || entry.if === undefined)
|
|
213
|
+
continue;
|
|
214
|
+
const message = routeIfFieldSchemaIssue(entry.if, source?.payload_schema, ref.id, entry.to, pipelineId);
|
|
215
|
+
if (message === undefined)
|
|
216
|
+
continue;
|
|
217
|
+
issues.push({
|
|
218
|
+
code: "pipeline.route_if_invalid",
|
|
219
|
+
message,
|
|
220
|
+
category: "pipeline",
|
|
221
|
+
pipelineId,
|
|
222
|
+
stageId: ref.id,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return issues;
|
|
227
|
+
}
|
|
228
|
+
export function collectRouteAllGatedWarnings(refs, pipelineId) {
|
|
229
|
+
const issues = [];
|
|
230
|
+
for (const ref of refs) {
|
|
231
|
+
if (ref.route === undefined)
|
|
232
|
+
continue;
|
|
233
|
+
const forward = ref.route.filter((entry) => !("type" in entry) || entry.type === undefined);
|
|
234
|
+
if (forward.length === 0)
|
|
235
|
+
continue;
|
|
236
|
+
if (!forward.every((entry) => entry.if !== undefined))
|
|
237
|
+
continue;
|
|
238
|
+
issues.push({
|
|
239
|
+
code: "pipeline.route_all_gated",
|
|
240
|
+
message: `Pipeline ${pipelineId}: stage "${ref.id}": every forward route entry has if`,
|
|
241
|
+
category: "pipeline",
|
|
242
|
+
pipelineId,
|
|
243
|
+
stageId: ref.id,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
return issues;
|
|
247
|
+
}
|
|
@@ -2,7 +2,7 @@ import type { LoadedPipeline } from "../types/pipeline.js";
|
|
|
2
2
|
import type { LoadIssue } from "./loadOutcome.js";
|
|
3
3
|
export type ValidationSeverity = "error" | "warning";
|
|
4
4
|
export type ValidationCategory = "pipeline" | "stage" | "catalog" | "task";
|
|
5
|
-
export type ValidationFindingCode = "pipeline.invalid_shape" | "pipeline.dag_error" | "pipeline.missing_stage" | "pipeline.load_error" | "pipeline.string_stage_ref" | "pipeline.stage_uses_inline_conflict" | "pipeline.stage_missing_body" | "pipeline.stage_id_mismatch" | "pipeline.invalid_completion" | "pipeline.invalid_recovery" | "pipeline.include_cycle" | "pipeline.include_invalid" | "pipeline.include_duplicate_stage" | "pipeline.invalid_agent" | "pipeline.invalid_model" | "pipeline.invalid_verify" | "pipeline.io_incompatible" | "pipeline.model_applies" | "stage.invalid_shape" | "stage.invalid_model" | "stage.missing_model" | "stage.invalid_payload_schema" | "stage.invalid_clone_input_schema" | "stage.unresolved_schema_ref" | "stage.invalid_clone_actions" | "stage.invalid_gate_kinds" | "stage.invalid_pre_emit_checks" | "stage.invalid_timeout_ms" | "stage.invalid_skill" | "stage.invalid_mcp" | "stage.invalid_agent" | "stage.invalid_io" | "stage.load_error" | "stage.id_filename_mismatch" | "task.invalid_shape" | "task.load_error" | "task.entry_input_unmet" | "catalog.duplicate_pipeline_id" | "catalog.manifest_missing" | "catalog.manifest_invalid" | "catalog.empty_catalog" | "catalog.manifest_load_error" | "catalog.invalid_mcp" | "catalog.mixed_yaml_dialect" | "catalog.legacy_yaml";
|
|
5
|
+
export type ValidationFindingCode = "pipeline.invalid_shape" | "pipeline.dag_error" | "pipeline.missing_stage" | "pipeline.load_error" | "pipeline.string_stage_ref" | "pipeline.stage_uses_inline_conflict" | "pipeline.stage_missing_body" | "pipeline.stage_id_mismatch" | "pipeline.invalid_completion" | "pipeline.invalid_recovery" | "pipeline.include_cycle" | "pipeline.include_invalid" | "pipeline.include_duplicate_stage" | "pipeline.invalid_agent" | "pipeline.invalid_model" | "pipeline.invalid_verify" | "pipeline.io_incompatible" | "pipeline.model_applies" | "pipeline.route_if_invalid" | "pipeline.route_all_gated" | "stage.invalid_shape" | "stage.invalid_model" | "stage.missing_model" | "stage.invalid_payload_schema" | "stage.invalid_clone_input_schema" | "stage.unresolved_schema_ref" | "stage.invalid_clone_actions" | "stage.invalid_gate_kinds" | "stage.invalid_pre_emit_checks" | "stage.invalid_timeout_ms" | "stage.invalid_skill" | "stage.invalid_mcp" | "stage.invalid_agent" | "stage.invalid_io" | "stage.load_error" | "stage.id_filename_mismatch" | "task.invalid_shape" | "task.load_error" | "task.entry_input_unmet" | "catalog.duplicate_pipeline_id" | "catalog.manifest_missing" | "catalog.manifest_invalid" | "catalog.empty_catalog" | "catalog.manifest_load_error" | "catalog.invalid_mcp" | "catalog.mixed_yaml_dialect" | "catalog.legacy_yaml";
|
|
6
6
|
export type ValidationFinding = {
|
|
7
7
|
severity: ValidationSeverity;
|
|
8
8
|
code: ValidationFindingCode | string;
|
|
@@ -69,7 +69,7 @@ function baseFinding(fields, severity) {
|
|
|
69
69
|
...(fields.stageId !== undefined ? { stageId: fields.stageId } : {}),
|
|
70
70
|
};
|
|
71
71
|
}
|
|
72
|
-
function findingPipelineError(cwd, absPath, message, code, pipelineId) {
|
|
72
|
+
function findingPipelineError(cwd, absPath, message, code, pipelineId, stageId) {
|
|
73
73
|
return baseFinding({
|
|
74
74
|
cwd,
|
|
75
75
|
absPath,
|
|
@@ -77,6 +77,7 @@ function findingPipelineError(cwd, absPath, message, code, pipelineId) {
|
|
|
77
77
|
code,
|
|
78
78
|
category: "pipeline",
|
|
79
79
|
pipelineId,
|
|
80
|
+
stageId,
|
|
80
81
|
}, "error");
|
|
81
82
|
}
|
|
82
83
|
function findingStageError(cwd, absPath, message, code, stageId) {
|
|
@@ -110,7 +111,7 @@ function findingCatalog(cwd, absPath, message, code, severity) {
|
|
|
110
111
|
export function findingsFromLoadIssues(cwd, absPath, issues) {
|
|
111
112
|
return issues.flatMap((issue) => {
|
|
112
113
|
if (issue.category === "pipeline") {
|
|
113
|
-
if (issue.code === "pipeline.model_applies") {
|
|
114
|
+
if (issue.code === "pipeline.model_applies" || issue.code === "pipeline.route_all_gated") {
|
|
114
115
|
return [
|
|
115
116
|
baseFinding({
|
|
116
117
|
cwd,
|
|
@@ -119,11 +120,12 @@ export function findingsFromLoadIssues(cwd, absPath, issues) {
|
|
|
119
120
|
code: issue.code,
|
|
120
121
|
category: "pipeline",
|
|
121
122
|
pipelineId: issue.pipelineId,
|
|
123
|
+
stageId: issue.stageId,
|
|
122
124
|
}, "warning"),
|
|
123
125
|
];
|
|
124
126
|
}
|
|
125
127
|
return [
|
|
126
|
-
findingPipelineError(cwd, absPath, issue.message, issue.code, issue.pipelineId),
|
|
128
|
+
findingPipelineError(cwd, absPath, issue.message, issue.code, issue.pipelineId, issue.stageId),
|
|
127
129
|
];
|
|
128
130
|
}
|
|
129
131
|
if (issue.category === "stage") {
|
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
import type { CompletionContract, RecoveryPolicy } from "../types/completion.js";
|
|
2
|
+
import type { CompiledStageEmitBody } from "../types/stage.js";
|
|
2
3
|
import { type LoadIssue, type LoadOutcome } from "./loadOutcome.js";
|
|
3
4
|
export { LEGACY_CONTRACT_KEYS, LEGACY_KEY_REPLACEMENTS, dialectWarningForDocument, formatLegacyReplacements, legacyYamlIssue, presentLegacyKeys, } from "./legacyYaml.js";
|
|
4
5
|
/** Target authoring keys. New catalog contracts belong here, not on the IR field names. */
|
|
5
6
|
export declare const TARGET_CONTRACT_KEYS: Set<string>;
|
|
6
7
|
/** Target wiring on a stage *file* is forbidden (`on_verify_fail`). `completion`/`recovery` are IR/legacy keys. */
|
|
7
|
-
export declare const STAGE_FILE_WIRING_KEYS: readonly ["needs", "on_verify_fail", "completion", "recovery", "fork", "
|
|
8
|
+
export declare const STAGE_FILE_WIRING_KEYS: readonly ["needs", "on_verify_fail", "completion", "recovery", "fork", "route", "entry", "route_select", "allow_none", "clone_cap", "clone_mode"];
|
|
8
9
|
export type YamlDialect = "legacy" | "target" | "invalid" | "neutral";
|
|
9
|
-
/** Target YAML compiled onto IR
|
|
10
|
-
export type CompiledTargetContract = {
|
|
11
|
-
payload_schema?: unknown;
|
|
12
|
-
clone_input_schema?: unknown;
|
|
13
|
-
pre_emit_raw?: unknown[];
|
|
10
|
+
/** Target YAML compiled onto typed emit/after/recovery IR fields (see file comment). */
|
|
11
|
+
export type CompiledTargetContract = CompiledStageEmitBody & {
|
|
14
12
|
completion?: CompletionContract;
|
|
15
13
|
recovery?: RecoveryPolicy;
|
|
16
14
|
};
|
|
@@ -24,6 +22,5 @@ export declare function compileTargetContract(raw: Record<string, unknown>, ctx:
|
|
|
24
22
|
label: string;
|
|
25
23
|
category: "pipeline" | "stage";
|
|
26
24
|
deferSchemaRefs?: boolean;
|
|
25
|
+
requireIo?: boolean;
|
|
27
26
|
}): LoadOutcome<CompiledTargetContract>;
|
|
28
|
-
/** Strip target YAML keys and stamp IR `payload_schema` / `clone_input_schema` / `pre_emit_checks`. */
|
|
29
|
-
export declare function applyCompiledBody(raw: Record<string, unknown>, compiled: CompiledTargetContract): Record<string, unknown>;
|
|
@@ -22,6 +22,7 @@ import path from "node:path";
|
|
|
22
22
|
import { compilePayloadSchema, UnresolvedSchemaRefError, } from "../envelope/payloadSchema.js";
|
|
23
23
|
import { loadFailure, loadSuccess } from "./loadOutcome.js";
|
|
24
24
|
import { parseExecutionPolicy } from "./parseCompletionContract.js";
|
|
25
|
+
import { parsePreEmitChecks } from "./parsePreEmitChecks.js";
|
|
25
26
|
import { LEGACY_CONTRACT_KEYS } from "./legacyYaml.js";
|
|
26
27
|
export { LEGACY_CONTRACT_KEYS, LEGACY_KEY_REPLACEMENTS, dialectWarningForDocument, formatLegacyReplacements, legacyYamlIssue, presentLegacyKeys, } from "./legacyYaml.js";
|
|
27
28
|
/** Target authoring keys. New catalog contracts belong here, not on the IR field names. */
|
|
@@ -33,7 +34,12 @@ export const STAGE_FILE_WIRING_KEYS = [
|
|
|
33
34
|
"completion",
|
|
34
35
|
"recovery",
|
|
35
36
|
"fork",
|
|
36
|
-
"
|
|
37
|
+
"route",
|
|
38
|
+
"entry",
|
|
39
|
+
"route_select",
|
|
40
|
+
"allow_none",
|
|
41
|
+
"clone_cap",
|
|
42
|
+
"clone_mode",
|
|
37
43
|
];
|
|
38
44
|
function isPlainObject(value) {
|
|
39
45
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -97,9 +103,12 @@ function invalidVerify(stageId, message, category) {
|
|
|
97
103
|
},
|
|
98
104
|
]);
|
|
99
105
|
}
|
|
100
|
-
function parseIo(raw, label, stageId, deferSchemaRefs) {
|
|
101
|
-
if (raw === undefined)
|
|
102
|
-
|
|
106
|
+
function parseIo(raw, label, stageId, deferSchemaRefs, requireIo) {
|
|
107
|
+
if (raw === undefined) {
|
|
108
|
+
if (!requireIo)
|
|
109
|
+
return loadSuccess({});
|
|
110
|
+
return invalidIo(label, "io is required", stageId);
|
|
111
|
+
}
|
|
103
112
|
if (!isPlainObject(raw)) {
|
|
104
113
|
return invalidIo(label, "io must be an object", stageId);
|
|
105
114
|
}
|
|
@@ -108,14 +117,18 @@ function parseIo(raw, label, stageId, deferSchemaRefs) {
|
|
|
108
117
|
return invalidIo(label, `io: unknown key "${key}"`, stageId);
|
|
109
118
|
}
|
|
110
119
|
}
|
|
120
|
+
if (raw.input === undefined) {
|
|
121
|
+
return invalidIo(label, "io.input.schema is required", stageId);
|
|
122
|
+
}
|
|
123
|
+
if (raw.output === undefined) {
|
|
124
|
+
return invalidIo(label, "io.output.schema is required", stageId);
|
|
125
|
+
}
|
|
111
126
|
const result = {};
|
|
112
127
|
for (const [field, target] of [
|
|
113
128
|
["output", "payload_schema"],
|
|
114
129
|
["input", "clone_input_schema"],
|
|
115
130
|
]) {
|
|
116
131
|
const side = raw[field];
|
|
117
|
-
if (side === undefined)
|
|
118
|
-
continue;
|
|
119
132
|
if (!isPlainObject(side)) {
|
|
120
133
|
return invalidIo(label, `io.${field} must be an object`, stageId);
|
|
121
134
|
}
|
|
@@ -124,8 +137,9 @@ function parseIo(raw, label, stageId, deferSchemaRefs) {
|
|
|
124
137
|
return invalidIo(label, `io.${field}: unknown key "${key}"`, stageId);
|
|
125
138
|
}
|
|
126
139
|
}
|
|
127
|
-
if (side.schema === undefined)
|
|
128
|
-
|
|
140
|
+
if (side.schema === undefined) {
|
|
141
|
+
return invalidIo(label, `io.${field}.schema is required`, stageId);
|
|
142
|
+
}
|
|
129
143
|
if (side.schema === null || typeof side.schema !== "object" || Array.isArray(side.schema)) {
|
|
130
144
|
return invalidIo(label, `io.${field}.schema must be an object`, stageId);
|
|
131
145
|
}
|
|
@@ -158,6 +172,12 @@ function parseIo(raw, label, stageId, deferSchemaRefs) {
|
|
|
158
172
|
else
|
|
159
173
|
result.clone_input_schema = side.schema;
|
|
160
174
|
}
|
|
175
|
+
if (result.payload_schema === undefined) {
|
|
176
|
+
return invalidIo(label, "io.output.schema is required", stageId);
|
|
177
|
+
}
|
|
178
|
+
if (result.clone_input_schema === undefined) {
|
|
179
|
+
return invalidIo(label, "io.input.schema is required", stageId);
|
|
180
|
+
}
|
|
161
181
|
return loadSuccess(result);
|
|
162
182
|
}
|
|
163
183
|
const EMIT_LEGAL_TYPES = new Set(["gate", "artifact"]);
|
|
@@ -283,7 +303,7 @@ function parseVerifyList(raw, stageId, category) {
|
|
|
283
303
|
}
|
|
284
304
|
/** Compile target YAML (`io` / `verify` / `on_verify_fail`) onto IR fields. */
|
|
285
305
|
export function compileTargetContract(raw, ctx) {
|
|
286
|
-
const ioOutcome = parseIo(raw.io, ctx.label, ctx.stageId, ctx.deferSchemaRefs === true);
|
|
306
|
+
const ioOutcome = parseIo(raw.io, ctx.label, ctx.stageId, ctx.deferSchemaRefs === true, ctx.requireIo !== false);
|
|
287
307
|
if (!ioOutcome.ok)
|
|
288
308
|
return ioOutcome;
|
|
289
309
|
const verifyOutcome = parseVerifyList(raw.verify, ctx.stageId, ctx.category);
|
|
@@ -295,11 +315,16 @@ export function compileTargetContract(raw, ctx) {
|
|
|
295
315
|
}, ctx.stageId, { requireCompletionForRecovery: false });
|
|
296
316
|
if (!policyOutcome.ok)
|
|
297
317
|
return policyOutcome;
|
|
318
|
+
let pre_emit_checks;
|
|
319
|
+
if (verifyOutcome.value.pre_emit_raw !== undefined) {
|
|
320
|
+
const preEmitOutcome = parsePreEmitChecks(verifyOutcome.value.pre_emit_raw, ctx.label);
|
|
321
|
+
if (!preEmitOutcome.ok)
|
|
322
|
+
return preEmitOutcome;
|
|
323
|
+
pre_emit_checks = preEmitOutcome.value;
|
|
324
|
+
}
|
|
298
325
|
return loadSuccess({
|
|
299
326
|
...ioOutcome.value,
|
|
300
|
-
...(
|
|
301
|
-
? { pre_emit_raw: verifyOutcome.value.pre_emit_raw }
|
|
302
|
-
: {}),
|
|
327
|
+
...(pre_emit_checks !== undefined ? { pre_emit_checks } : {}),
|
|
303
328
|
...(policyOutcome.value.completion !== undefined
|
|
304
329
|
? { completion: policyOutcome.value.completion }
|
|
305
330
|
: {}),
|
|
@@ -308,20 +333,3 @@ export function compileTargetContract(raw, ctx) {
|
|
|
308
333
|
: {}),
|
|
309
334
|
});
|
|
310
335
|
}
|
|
311
|
-
/** Strip target YAML keys and stamp IR `payload_schema` / `clone_input_schema` / `pre_emit_checks`. */
|
|
312
|
-
export function applyCompiledBody(raw, compiled) {
|
|
313
|
-
const next = {};
|
|
314
|
-
for (const [key, value] of Object.entries(raw)) {
|
|
315
|
-
if (key === "io" || key === "verify" || key === "on_verify_fail")
|
|
316
|
-
continue;
|
|
317
|
-
next[key] = value;
|
|
318
|
-
}
|
|
319
|
-
if (compiled.payload_schema !== undefined)
|
|
320
|
-
next.payload_schema = compiled.payload_schema;
|
|
321
|
-
if (compiled.clone_input_schema !== undefined) {
|
|
322
|
-
next.clone_input_schema = compiled.clone_input_schema;
|
|
323
|
-
}
|
|
324
|
-
if (compiled.pre_emit_raw !== undefined)
|
|
325
|
-
next.pre_emit_checks = compiled.pre_emit_raw;
|
|
326
|
-
return next;
|
|
327
|
-
}
|
package/dist/envelope/check.d.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
1
|
import { type StageEnvelope } from "../types/envelope.js";
|
|
2
|
-
import type { CloneForkItem } from "../types/forkChoice.js";
|
|
3
|
-
export declare function parseCloneForks(raw: unknown): CloneForkItem[];
|
|
4
2
|
export declare function assertRequiredEnvelope(value: unknown, pathPrefix?: string): StageEnvelope;
|
|
5
3
|
export declare function isAdvancingEnvelope(envelope: StageEnvelope): boolean;
|
package/dist/envelope/check.js
CHANGED
|
@@ -8,62 +8,6 @@ function qualifyEnvelopeMessage(pathPrefix, message) {
|
|
|
8
8
|
}
|
|
9
9
|
return `${pathPrefix}.${message}`;
|
|
10
10
|
}
|
|
11
|
-
function parseCloneForkItem(raw, index) {
|
|
12
|
-
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
13
|
-
throw new EnvelopeError(`clone_forks[${index}] must be an object`);
|
|
14
|
-
}
|
|
15
|
-
const record = raw;
|
|
16
|
-
if (typeof record.successor_id !== "string") {
|
|
17
|
-
throw new EnvelopeError(`clone_forks[${index}].successor_id must be a string`);
|
|
18
|
-
}
|
|
19
|
-
const action = record.action;
|
|
20
|
-
if (action !== "skip" && action !== "once" && action !== "fanout") {
|
|
21
|
-
throw new EnvelopeError(`clone_forks[${index}].action must be "skip", "once", or "fanout"`);
|
|
22
|
-
}
|
|
23
|
-
const item = {
|
|
24
|
-
successor_id: record.successor_id,
|
|
25
|
-
action,
|
|
26
|
-
};
|
|
27
|
-
if (record.envelope !== undefined) {
|
|
28
|
-
const envelope = assertRequiredEnvelope(record.envelope, `clone_forks[${index}].envelope`);
|
|
29
|
-
if (envelope.feedback_loop !== undefined) {
|
|
30
|
-
throw new EnvelopeError(`clone_forks[${index}].envelope.feedback_loop is not allowed in clone assignment envelopes`);
|
|
31
|
-
}
|
|
32
|
-
item.envelope = envelope;
|
|
33
|
-
}
|
|
34
|
-
if (record.mode !== undefined) {
|
|
35
|
-
if (record.mode !== "parallel" && record.mode !== "sequential") {
|
|
36
|
-
throw new EnvelopeError(`clone_forks[${index}].mode must be "parallel" or "sequential"`);
|
|
37
|
-
}
|
|
38
|
-
item.mode = record.mode;
|
|
39
|
-
}
|
|
40
|
-
if (record.clones !== undefined) {
|
|
41
|
-
if (!Array.isArray(record.clones)) {
|
|
42
|
-
throw new EnvelopeError(`clone_forks[${index}].clones must be an array`);
|
|
43
|
-
}
|
|
44
|
-
item.clones = record.clones.map((clone, cloneIndex) => {
|
|
45
|
-
if (clone === null || typeof clone !== "object" || Array.isArray(clone)) {
|
|
46
|
-
throw new EnvelopeError(`clone_forks[${index}].clones[${cloneIndex}] must be an object`);
|
|
47
|
-
}
|
|
48
|
-
const cloneRecord = clone;
|
|
49
|
-
if (cloneRecord.envelope === undefined) {
|
|
50
|
-
throw new EnvelopeError(`clone_forks[${index}].clones[${cloneIndex}].envelope is required`);
|
|
51
|
-
}
|
|
52
|
-
const envelope = assertRequiredEnvelope(cloneRecord.envelope, `clone_forks[${index}].clones[${cloneIndex}].envelope`);
|
|
53
|
-
if (envelope.feedback_loop !== undefined) {
|
|
54
|
-
throw new EnvelopeError(`clone_forks[${index}].clones[${cloneIndex}].envelope.feedback_loop is not allowed in clone assignment envelopes`);
|
|
55
|
-
}
|
|
56
|
-
return { envelope };
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
return item;
|
|
60
|
-
}
|
|
61
|
-
export function parseCloneForks(raw) {
|
|
62
|
-
if (!Array.isArray(raw)) {
|
|
63
|
-
throw new EnvelopeError("clone_forks must be an array");
|
|
64
|
-
}
|
|
65
|
-
return raw.map((item, index) => parseCloneForkItem(item, index));
|
|
66
|
-
}
|
|
67
11
|
export function assertRequiredEnvelope(value, pathPrefix = "") {
|
|
68
12
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
69
13
|
throw new EnvelopeError(qualifyEnvelopeMessage(pathPrefix, "envelope must be an object"));
|
|
@@ -98,7 +42,7 @@ export function assertRequiredEnvelope(value, pathPrefix = "") {
|
|
|
98
42
|
envelope.fork_choice = record.fork_choice;
|
|
99
43
|
}
|
|
100
44
|
if (record.clone_forks !== undefined) {
|
|
101
|
-
|
|
45
|
+
throw new EnvelopeError(qualifyEnvelopeMessage(pathPrefix, '"clone_forks" is no longer supported — use a Clone Chain instead'));
|
|
102
46
|
}
|
|
103
47
|
if (record.feedback_loop !== undefined) {
|
|
104
48
|
if (record.feedback_loop === null ||
|
|
@@ -22,9 +22,6 @@ export function assertFeedbackLoopAction(envelope, context) {
|
|
|
22
22
|
if (envelope.fork_choice !== undefined) {
|
|
23
23
|
throw new EnvelopeError("feedback_loop send_back cannot be combined with fork_choice");
|
|
24
24
|
}
|
|
25
|
-
if (envelope.clone_forks !== undefined) {
|
|
26
|
-
throw new EnvelopeError("feedback_loop send_back cannot be combined with clone_forks");
|
|
27
|
-
}
|
|
28
25
|
if (action.target !== context.target) {
|
|
29
26
|
throw new EnvelopeError(`feedback_loop.target "${action.target}" is not an allowed feedback-loop target`);
|
|
30
27
|
}
|
|
@@ -7,7 +7,7 @@ function assertForkChoiceLegality(choices, forkEmitContext) {
|
|
|
7
7
|
const { immediateSuccessorIds, forkShape } = forkEmitContext;
|
|
8
8
|
for (const id of choices) {
|
|
9
9
|
if (!immediateSuccessorIds.includes(id)) {
|
|
10
|
-
throw new EnvelopeError(`fork_choice contains non-immediate successor: ${id}
|
|
10
|
+
throw new EnvelopeError(`fork_choice contains non-immediate successor: ${id}`);
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
13
|
if (choices.length === 0) {
|
|
@@ -39,6 +39,15 @@ export declare function compilePayloadSchema(raw: unknown, options?: CompilePayl
|
|
|
39
39
|
*/
|
|
40
40
|
export declare function isPayloadSchemaSubset(consumer: unknown, producer: unknown, options?: CompilePayloadSchemaOptions): boolean;
|
|
41
41
|
export declare function expandPayloadSchemaRefs(raw: unknown, options?: CompilePayloadSchemaOptions): unknown;
|
|
42
|
+
export type PayloadSchemaFieldPathIssue = "empty_segment" | "array_index" | "missing" | "optional" | "untyped";
|
|
43
|
+
export type PayloadSchemaFieldPathResult = {
|
|
44
|
+
ok: true;
|
|
45
|
+
type: string;
|
|
46
|
+
} | {
|
|
47
|
+
ok: false;
|
|
48
|
+
issue: PayloadSchemaFieldPathIssue;
|
|
49
|
+
};
|
|
50
|
+
export declare function resolvePayloadSchemaFieldPath(schema: unknown, field: string, options?: CompilePayloadSchemaOptions): PayloadSchemaFieldPathResult;
|
|
42
51
|
export declare function payloadInstanceMismatch(instance: unknown, payloadSchema: unknown, options?: CompilePayloadSchemaOptions): string | undefined;
|
|
43
52
|
export declare function assertEnvelopePayload(envelope: StageEnvelope, payloadSchema: unknown | undefined, options?: CompilePayloadSchemaOptions): void;
|
|
44
53
|
/**
|
|
@@ -46,3 +55,8 @@ export declare function assertEnvelopePayload(envelope: StageEnvelope, payloadSc
|
|
|
46
55
|
* `clone_input_schema` (YAML: `io.input.schema`).
|
|
47
56
|
*/
|
|
48
57
|
export declare function assertCloneAssignmentPayload(envelope: StageEnvelope, cloneInputSchema: unknown, successorId: string, itemPath?: string, options?: CompilePayloadSchemaOptions): void;
|
|
58
|
+
/**
|
|
59
|
+
* Validate a predecessor success payload against the child's IR
|
|
60
|
+
* `clone_input_schema` (YAML: `io.input.schema`) on a normal pipeline edge.
|
|
61
|
+
*/
|
|
62
|
+
export declare function assertPriorInputPayload(envelope: StageEnvelope, cloneInputSchema: unknown, childId: string, options?: CompilePayloadSchemaOptions): void;
|