stageflow 0.16.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/README.md +2 -4
  2. package/dist/agent/claudeTools.d.ts +1 -4
  3. package/dist/agent/claudeTools.js +1 -6
  4. package/dist/agent/fakeAgent.js +4 -5
  5. package/dist/agent/piAdapter.js +1 -14
  6. package/dist/config/cloneChain.d.ts +12 -0
  7. package/dist/config/cloneChain.js +322 -0
  8. package/dist/config/createPipeline.js +101 -21
  9. package/dist/config/createStage.js +7 -0
  10. package/dist/config/loadOutcome.d.ts +1 -0
  11. package/dist/config/loadPipeline.d.ts +2 -0
  12. package/dist/config/loadPipeline.js +34 -22
  13. package/dist/config/loadStage.d.ts +2 -0
  14. package/dist/config/loadStage.js +135 -125
  15. package/dist/config/migrateYaml.js +14 -3
  16. package/dist/config/normalizePipelineStageEntry.d.ts +5 -9
  17. package/dist/config/normalizePipelineStageEntry.js +133 -60
  18. package/dist/config/parsePreEmitChecks.js +2 -2
  19. package/dist/config/pipelineNeeds.d.ts +15 -2
  20. package/dist/config/pipelineNeeds.js +86 -4
  21. package/dist/config/pipelineRoute.d.ts +46 -0
  22. package/dist/config/pipelineRoute.js +215 -0
  23. package/dist/config/pipelineStageKeys.d.ts +2 -0
  24. package/dist/config/pipelineStageKeys.js +15 -4
  25. package/dist/config/printTargetYaml.js +18 -12
  26. package/dist/config/resolveForkEmitContext.d.ts +1 -1
  27. package/dist/config/resolveForkEmitContext.js +3 -26
  28. package/dist/config/resolvePipelineDag.d.ts +1 -3
  29. package/dist/config/resolvePipelineDag.js +143 -186
  30. package/dist/config/routeIf.d.ts +24 -0
  31. package/dist/config/routeIf.js +247 -0
  32. package/dist/config/validateCatalog.d.ts +1 -1
  33. package/dist/config/validateCatalog.js +5 -3
  34. package/dist/config/yamlDialect.d.ts +5 -8
  35. package/dist/config/yamlDialect.js +37 -29
  36. package/dist/envelope/check.d.ts +0 -2
  37. package/dist/envelope/check.js +1 -57
  38. package/dist/envelope/feedbackLoop.js +0 -3
  39. package/dist/envelope/forkChoice.js +1 -1
  40. package/dist/envelope/payloadSchema.d.ts +14 -0
  41. package/dist/envelope/payloadSchema.js +87 -2
  42. package/dist/mcp/catalogTools.js +1 -2
  43. package/dist/package-meta.d.ts +1 -1
  44. package/dist/package-meta.js +1 -1
  45. package/dist/projection/projectRun.d.ts +0 -2
  46. package/dist/projection/projectRun.js +0 -3
  47. package/dist/prompt/priorEnvelope.js +1 -1
  48. package/dist/runstore/stageInstanceId.d.ts +1 -0
  49. package/dist/runstore/stageInstanceId.js +9 -0
  50. package/dist/runtime/cloneSchedule.d.ts +16 -33
  51. package/dist/runtime/cloneSchedule.js +155 -236
  52. package/dist/runtime/envelopeRouting.js +43 -94
  53. package/dist/runtime/forkGeneration.d.ts +2 -2
  54. package/dist/runtime/forkGeneration.js +29 -15
  55. package/dist/runtime/joinReadiness.d.ts +16 -0
  56. package/dist/runtime/joinReadiness.js +156 -0
  57. package/dist/runtime/pipelineScheduler.d.ts +0 -3
  58. package/dist/runtime/pipelineScheduler.js +81 -65
  59. package/dist/runtime/routeIfEval.d.ts +3 -0
  60. package/dist/runtime/routeIfEval.js +94 -0
  61. package/dist/runtime/runRetryCoordinator.js +0 -4
  62. package/dist/runtime/stageAttemptBootstrap.js +60 -11
  63. package/dist/runtime/taskInput.js +1 -13
  64. package/dist/tools/emitStageEnvelope.d.ts +2 -3
  65. package/dist/tools/emitStageEnvelope.js +1 -65
  66. package/dist/types/envelope.d.ts +0 -2
  67. package/dist/types/forkChoice.d.ts +0 -16
  68. package/dist/types/pipeline.d.ts +86 -27
  69. package/dist/types/stage.d.ts +11 -7
  70. package/dist/ui/assets/{index-D2Wt8hwH.css → index-DsgVjLUS.css} +1 -1
  71. package/dist/ui/assets/{index-XWJovnp7.js → index-DwKGaQvQ.js} +13 -12
  72. package/dist/ui/index.html +2 -2
  73. package/package.json +2 -1
  74. package/skills/stageflow-author/SKILL.md +4 -4
  75. package/skills/stageflow-author/assets/examples/branch-decision/hotfix.yaml +10 -2
  76. package/skills/stageflow-author/assets/examples/branch-decision/release-gate.pipeline.yaml +12 -4
  77. package/skills/stageflow-author/assets/examples/branch-decision/run-tests.yaml +6 -4
  78. package/skills/stageflow-author/assets/examples/branch-decision/ship.yaml +9 -1
  79. package/skills/stageflow-author/assets/examples/linear-review/draft.yaml +3 -0
  80. package/skills/stageflow-author/assets/examples/linear-review/publish.yaml +8 -0
  81. package/skills/stageflow-author/assets/examples/linear-review/review-loop.pipeline.yaml +5 -2
  82. package/skills/stageflow-author/assets/examples/linear-review/review.yaml +8 -0
  83. package/skills/stageflow-author/assets/examples/non-sdlc-digest/gather.yaml +3 -0
  84. package/skills/stageflow-author/assets/examples/non-sdlc-digest/research-digest.pipeline.yaml +5 -2
  85. package/skills/stageflow-author/assets/examples/non-sdlc-digest/send.yaml +8 -0
  86. package/skills/stageflow-author/assets/examples/non-sdlc-digest/summarize.yaml +8 -0
  87. package/skills/stageflow-author/references/catalog-mapping.md +159 -50
  88. package/skills/stageflow-author/references/catalog-write-conventions.md +3 -3
  89. package/skills/stageflow-author/references/stage-prompt-template.md +57 -17
  90. package/skills/stageflow-session-capture/SKILL.md +2 -2
  91. package/skills/stageflow-session-capture/assets/example-pipeline/example.pipeline.yaml +3 -1
  92. package/skills/stageflow-session-capture/assets/example-pipeline/implement.yaml +15 -0
  93. package/skills/stageflow-session-capture/assets/example-pipeline/research.yaml +3 -0
  94. package/skills/stageflow-session-capture/references/catalog-authoring.md +4 -2
  95. package/dist/envelope/cloneForks.d.ts +0 -2
  96. package/dist/envelope/cloneForks.js +0 -77
@@ -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", "clonable"];
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 field names (see file comment). */
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
- "clonable",
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
- return loadSuccess({});
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
- continue;
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
- ...(verifyOutcome.value.pre_emit_raw !== undefined
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
- }
@@ -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;
@@ -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
- envelope.clone_forks = parseCloneForks(record.clone_forks);
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}; clonable successors belong in clone_forks`);
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;