stageflow 0.13.0 → 0.16.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 (113) hide show
  1. package/dist/agent/activity.d.ts +3 -0
  2. package/dist/agent/claudeAdapter.js +84 -7
  3. package/dist/agent/claudeSession.d.ts +3 -0
  4. package/dist/agent/piAdapter.d.ts +2 -0
  5. package/dist/agent/piAdapter.js +34 -7
  6. package/dist/agent/port.d.ts +5 -2
  7. package/dist/cli/initTemplates.d.ts +1 -1
  8. package/dist/cli/initTemplates.js +1 -1
  9. package/dist/cli/migrateYamlCommand.d.ts +18 -0
  10. package/dist/cli/migrateYamlCommand.js +180 -0
  11. package/dist/cli/runOutput.js +58 -26
  12. package/dist/cli/validateCommand.d.ts +1 -0
  13. package/dist/cli/validateCommand.js +2 -0
  14. package/dist/cli.js +15 -1
  15. package/dist/config/browseCatalog.js +1 -1
  16. package/dist/config/createPipeline.d.ts +1 -1
  17. package/dist/config/createPipeline.js +23 -11
  18. package/dist/config/createStage.d.ts +1 -1
  19. package/dist/config/createStage.js +10 -5
  20. package/dist/config/legacyYaml.d.ts +28 -0
  21. package/dist/config/legacyYaml.js +98 -0
  22. package/dist/config/loadOutcome.d.ts +2 -1
  23. package/dist/config/loadOutcome.js +4 -1
  24. package/dist/config/loadPipeline.d.ts +2 -0
  25. package/dist/config/loadPipeline.js +150 -8
  26. package/dist/config/loadStage.d.ts +8 -1
  27. package/dist/config/loadStage.js +138 -29
  28. package/dist/config/loadStageflowManifest.js +10 -0
  29. package/dist/config/loadTask.js +14 -0
  30. package/dist/config/materializeStageModels.d.ts +7 -0
  31. package/dist/config/materializeStageModels.js +35 -0
  32. package/dist/config/mergePipelineIncludes.d.ts +5 -1
  33. package/dist/config/mergePipelineIncludes.js +82 -4
  34. package/dist/config/migrateYaml.d.ts +19 -0
  35. package/dist/config/migrateYaml.js +627 -0
  36. package/dist/config/modelField.d.ts +13 -0
  37. package/dist/config/modelField.js +16 -0
  38. package/dist/config/normalizePipelineStageEntry.js +41 -2
  39. package/dist/config/parseCompletionContract.d.ts +5 -2
  40. package/dist/config/parseCompletionContract.js +16 -3
  41. package/dist/config/parsePreEmitChecks.js +4 -0
  42. package/dist/config/pipelineNeeds.js +3 -3
  43. package/dist/config/pipelineStageKeys.d.ts +11 -0
  44. package/dist/config/pipelineStageKeys.js +14 -0
  45. package/dist/config/printTargetYaml.d.ts +28 -0
  46. package/dist/config/printTargetYaml.js +243 -0
  47. package/dist/config/resolveModel.d.ts +20 -0
  48. package/dist/config/resolveModel.js +35 -0
  49. package/dist/config/validateCatalog.d.ts +5 -1
  50. package/dist/config/validateCatalog.js +36 -6
  51. package/dist/config/validateCompletionContract.js +5 -0
  52. package/dist/config/yamlDialect.d.ts +29 -0
  53. package/dist/config/yamlDialect.js +327 -0
  54. package/dist/envelope/payloadSchema.d.ts +37 -5
  55. package/dist/envelope/payloadSchema.js +272 -22
  56. package/dist/mcp/catalogTools.js +1 -0
  57. package/dist/package-meta.d.ts +1 -1
  58. package/dist/package-meta.js +1 -1
  59. package/dist/runstore/pipelineDagSnapshot.js +2 -0
  60. package/dist/runstore/port.d.ts +11 -0
  61. package/dist/runstore/runProjection.js +9 -0
  62. package/dist/runstore/sqlite/SqliteRunStore.js +27 -4
  63. package/dist/runstore/sqlite/schema.d.ts +1 -1
  64. package/dist/runstore/sqlite/schema.js +2 -0
  65. package/dist/runstore/stageExecution.d.ts +3 -0
  66. package/dist/runstore/stageExecution.js +6 -2
  67. package/dist/runstore/stageSnapshot.js +11 -0
  68. package/dist/runtime/completionCheckRunner.d.ts +3 -2
  69. package/dist/runtime/completionCheckRunner.js +3 -2
  70. package/dist/runtime/envelopeRouting.d.ts +2 -2
  71. package/dist/runtime/pipelineRunner.d.ts +3 -0
  72. package/dist/runtime/pipelineRunner.js +23 -3
  73. package/dist/runtime/runManager.js +3 -2
  74. package/dist/runtime/stageAttemptBootstrap.d.ts +2 -2
  75. package/dist/runtime/stageRunner.d.ts +2 -2
  76. package/dist/runtime/stageRunner.js +2 -2
  77. package/dist/runtime/taskInput.d.ts +7 -0
  78. package/dist/runtime/taskInput.js +60 -0
  79. package/dist/runtime/verifiedStageExecution.js +3 -0
  80. package/dist/types/completion.d.ts +6 -0
  81. package/dist/types/pipeline.d.ts +21 -2
  82. package/dist/types/preEmitCheck.d.ts +4 -6
  83. package/dist/types/stage.d.ts +22 -3
  84. package/dist/types/stageflowManifest.d.ts +2 -0
  85. package/dist/types/task.d.ts +1 -0
  86. package/dist/types/usage.d.ts +16 -0
  87. package/dist/types/usage.js +24 -0
  88. package/dist/ui/assets/{index-uVHiae8s.css → index-D2Wt8hwH.css} +1 -1
  89. package/dist/ui/assets/index-XWJovnp7.js +118 -0
  90. package/dist/ui/index.html +2 -2
  91. package/package.json +1 -1
  92. package/skills/stageflow-author/SKILL.md +7 -7
  93. package/skills/stageflow-author/assets/examples/branch-decision/hotfix.yaml +9 -1
  94. package/skills/stageflow-author/assets/examples/branch-decision/release-gate.pipeline.yaml +6 -2
  95. package/skills/stageflow-author/assets/examples/branch-decision/run-tests.yaml +9 -1
  96. package/skills/stageflow-author/assets/examples/branch-decision/ship.yaml +16 -1
  97. package/skills/stageflow-author/assets/examples/linear-review/draft.yaml +12 -2
  98. package/skills/stageflow-author/assets/examples/linear-review/publish.yaml +16 -1
  99. package/skills/stageflow-author/assets/examples/linear-review/review-loop.pipeline.yaml +11 -2
  100. package/skills/stageflow-author/assets/examples/linear-review/review.yaml +19 -1
  101. package/skills/stageflow-author/assets/examples/non-sdlc-digest/gather.yaml +9 -1
  102. package/skills/stageflow-author/assets/examples/non-sdlc-digest/research-digest.pipeline.yaml +6 -2
  103. package/skills/stageflow-author/assets/examples/non-sdlc-digest/send.yaml +16 -1
  104. package/skills/stageflow-author/assets/examples/non-sdlc-digest/summarize.yaml +9 -1
  105. package/skills/stageflow-author/references/catalog-mapping.md +30 -28
  106. package/skills/stageflow-author/references/catalog-write-conventions.md +2 -2
  107. package/skills/stageflow-author/references/stage-prompt-template.md +5 -5
  108. package/skills/stageflow-session-capture/SKILL.md +1 -1
  109. package/skills/stageflow-session-capture/assets/example-pipeline/example.pipeline.yaml +2 -1
  110. package/skills/stageflow-session-capture/assets/example-pipeline/implement.yaml +13 -2
  111. package/skills/stageflow-session-capture/assets/example-pipeline/research.yaml +18 -2
  112. package/skills/stageflow-session-capture/references/catalog-authoring.md +2 -2
  113. package/dist/ui/assets/index-Yzh42vhf.js +0 -118
@@ -110,6 +110,18 @@ function findingCatalog(cwd, absPath, message, code, severity) {
110
110
  export function findingsFromLoadIssues(cwd, absPath, issues) {
111
111
  return issues.flatMap((issue) => {
112
112
  if (issue.category === "pipeline") {
113
+ if (issue.code === "pipeline.model_applies") {
114
+ return [
115
+ baseFinding({
116
+ cwd,
117
+ absPath,
118
+ message: issue.message,
119
+ code: issue.code,
120
+ category: "pipeline",
121
+ pipelineId: issue.pipelineId,
122
+ }, "warning"),
123
+ ];
124
+ }
113
125
  return [
114
126
  findingPipelineError(cwd, absPath, issue.message, issue.code, issue.pipelineId),
115
127
  ];
@@ -126,7 +138,9 @@ export function findingsFromLoadIssues(cwd, absPath, issues) {
126
138
  }
127
139
  if (issue.category === "catalog") {
128
140
  const code = issue.code;
129
- const severity = code === "catalog.manifest_missing" || code === "catalog.empty_catalog"
141
+ const severity = code === "catalog.manifest_missing" ||
142
+ code === "catalog.empty_catalog" ||
143
+ code === "catalog.legacy_yaml"
130
144
  ? "warning"
131
145
  : "error";
132
146
  return [findingCatalog(cwd, absPath, issue.message, code, severity)];
@@ -223,6 +237,7 @@ async function findingsForStageMcpCatalog(cwd, loaded) {
223
237
  }
224
238
  async function runPipelineValidation(nameOrPath, options) {
225
239
  const { cwd, validateStages } = options;
240
+ const projectRoot = options.projectRoot ?? cwd;
226
241
  let pipelinePath;
227
242
  try {
228
243
  pipelinePath = await resolvePipelinePath(nameOrPath, cwd);
@@ -240,17 +255,21 @@ async function runPipelineValidation(nameOrPath, options) {
240
255
  ]),
241
256
  };
242
257
  }
243
- const outcome = await loadPipelineOutcome(pipelinePath, { cwd });
258
+ const outcome = await loadPipelineOutcome(pipelinePath, { cwd, projectRoot });
244
259
  const findings = [];
245
260
  if (!outcome.ok) {
246
261
  findings.push(...findingsFromLoadIssues(cwd, pipelinePath, outcome.issues));
247
262
  return { ok: false, findings };
248
263
  }
264
+ if (outcome.issues) {
265
+ findings.push(...findingsFromLoadIssues(cwd, pipelinePath, outcome.issues));
266
+ }
249
267
  findings.push(...(await findingsForStageMcpCatalog(cwd, outcome.value)));
250
268
  if (validateStages && outcome.value.stageSources) {
251
269
  for (const source of Object.values(outcome.value.stageSources)) {
252
270
  if (source.kind === "file") {
253
- findings.push(...(await validateStageFile(cwd, source.path)));
271
+ const extra = await validateStageFile(cwd, source.path);
272
+ findings.push(...extra.filter((finding) => finding.code !== "stage.unresolved_schema_ref"));
254
273
  }
255
274
  }
256
275
  }
@@ -261,17 +280,27 @@ async function runPipelineValidation(nameOrPath, options) {
261
280
  }
262
281
  export async function validatePipeline(nameOrPath, options = {}) {
263
282
  const cwd = options.cwd ?? process.cwd();
283
+ const projectRoot = options.projectRoot ?? cwd;
264
284
  const validateStages = options.validateStages ?? true;
265
285
  const strict = options.strict ?? false;
266
- const core = await runPipelineValidation(nameOrPath, { cwd, validateStages });
286
+ const core = await runPipelineValidation(nameOrPath, {
287
+ cwd,
288
+ projectRoot,
289
+ validateStages,
290
+ });
267
291
  return buildValidationResult("pipeline", core.findings, strict);
268
292
  }
269
293
  export async function loadPipelineValidated(nameOrPath, options = {}) {
270
294
  const cwd = options.cwd ?? process.cwd();
295
+ const projectRoot = options.projectRoot ?? cwd;
271
296
  const validateStages = options.validateStages ?? true;
272
- const core = await runPipelineValidation(nameOrPath, { cwd, validateStages });
297
+ const core = await runPipelineValidation(nameOrPath, {
298
+ cwd,
299
+ projectRoot,
300
+ validateStages,
301
+ });
273
302
  if (core.ok) {
274
- return { ok: true, loaded: core.loaded };
303
+ return { ok: true, loaded: core.loaded, findings: core.findings };
275
304
  }
276
305
  return { ok: false, findings: core.findings };
277
306
  }
@@ -396,6 +425,7 @@ export async function validateCatalog(options) {
396
425
  if (options.scope === "pipeline") {
397
426
  return validatePipeline(options.pipeline, {
398
427
  cwd,
428
+ projectRoot: options.projectRoot ?? cwd,
399
429
  validateStages: true,
400
430
  strict,
401
431
  });
@@ -30,6 +30,11 @@ function isRequiredStringArrayField(schema, field) {
30
30
  items.type === "string");
31
31
  }
32
32
  export function validateCompletionContractForStage(stage, completion) {
33
+ for (const check of stage.pre_emit_checks ?? []) {
34
+ if (check.type === "gate" && !stage.gate_kinds?.includes(check.kind)) {
35
+ return failure(stage.id, `gate check "${check.id}" requires gate_kinds to include "${check.kind}"`);
36
+ }
37
+ }
33
38
  if (!completion)
34
39
  return loadSuccess(undefined);
35
40
  for (const check of completion.checks) {
@@ -0,0 +1,29 @@
1
+ import type { CompletionContract, RecoveryPolicy } from "../types/completion.js";
2
+ import { type LoadIssue, type LoadOutcome } from "./loadOutcome.js";
3
+ export { LEGACY_CONTRACT_KEYS, LEGACY_KEY_REPLACEMENTS, dialectWarningForDocument, formatLegacyReplacements, legacyYamlIssue, presentLegacyKeys, } from "./legacyYaml.js";
4
+ /** Target authoring keys. New catalog contracts belong here, not on the IR field names. */
5
+ export declare const TARGET_CONTRACT_KEYS: Set<string>;
6
+ /** 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 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[];
14
+ completion?: CompletionContract;
15
+ recovery?: RecoveryPolicy;
16
+ };
17
+ export declare function dialectFromKeys(keys: Iterable<string>): YamlDialect;
18
+ export declare function collectDocumentKeys(raw: Record<string, unknown>): Set<string>;
19
+ export declare function classifyYamlDocument(raw: Record<string, unknown>): YamlDialect;
20
+ export declare function mixedDialectIssue(message?: string): LoadIssue;
21
+ /** Compile target YAML (`io` / `verify` / `on_verify_fail`) onto IR fields. */
22
+ export declare function compileTargetContract(raw: Record<string, unknown>, ctx: {
23
+ stageId: string;
24
+ label: string;
25
+ category: "pipeline" | "stage";
26
+ deferSchemaRefs?: boolean;
27
+ }): 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>;
@@ -0,0 +1,327 @@
1
+ /**
2
+ * Target YAML dialect compiler.
3
+ *
4
+ * Catalog files (and new catalog fields) use `io`, `verify`, and `on_verify_fail`.
5
+ * Runtime, DAG snapshots, emit, and VSE still speak the IR names this module
6
+ * writes. Dual-read of old YAML keys (`payload_schema`, `pre_emit_checks`,
7
+ * `completion`, `recovery`) is `legacyYaml.ts` — not this compiler.
8
+ *
9
+ * YAML → IR
10
+ * io.output.schema → StageConfig.payload_schema
11
+ * io.input.schema → StageConfig.clone_input_schema
12
+ * verify when includes emit → StageConfig.pre_emit_checks
13
+ * verify when includes after → DAG node.completion
14
+ * on_verify_fail → DAG node.recovery
15
+ *
16
+ * `type: payload_schema` on a verify item is a check kind, not the legacy
17
+ * authoring key. Adding a catalog contract: extend TARGET_CONTRACT_KEYS and
18
+ * compileTargetContract (and printTargetYaml for migrate). Do not add a new
19
+ * LEGACY_CONTRACT_KEYS entry.
20
+ */
21
+ import path from "node:path";
22
+ import { compilePayloadSchema, UnresolvedSchemaRefError, } from "../envelope/payloadSchema.js";
23
+ import { loadFailure, loadSuccess } from "./loadOutcome.js";
24
+ import { parseExecutionPolicy } from "./parseCompletionContract.js";
25
+ import { LEGACY_CONTRACT_KEYS } from "./legacyYaml.js";
26
+ export { LEGACY_CONTRACT_KEYS, LEGACY_KEY_REPLACEMENTS, dialectWarningForDocument, formatLegacyReplacements, legacyYamlIssue, presentLegacyKeys, } from "./legacyYaml.js";
27
+ /** Target authoring keys. New catalog contracts belong here, not on the IR field names. */
28
+ export const TARGET_CONTRACT_KEYS = new Set(["io", "verify", "on_verify_fail"]);
29
+ /** Target wiring on a stage *file* is forbidden (`on_verify_fail`). `completion`/`recovery` are IR/legacy keys. */
30
+ export const STAGE_FILE_WIRING_KEYS = [
31
+ "needs",
32
+ "on_verify_fail",
33
+ "completion",
34
+ "recovery",
35
+ "fork",
36
+ "clonable",
37
+ ];
38
+ function isPlainObject(value) {
39
+ return typeof value === "object" && value !== null && !Array.isArray(value);
40
+ }
41
+ export function dialectFromKeys(keys) {
42
+ let legacy = false;
43
+ let target = false;
44
+ for (const key of keys) {
45
+ if (LEGACY_CONTRACT_KEYS.has(key))
46
+ legacy = true;
47
+ if (TARGET_CONTRACT_KEYS.has(key))
48
+ target = true;
49
+ }
50
+ if (legacy && target)
51
+ return "invalid";
52
+ if (legacy)
53
+ return "legacy";
54
+ if (target)
55
+ return "target";
56
+ return "neutral";
57
+ }
58
+ export function collectDocumentKeys(raw) {
59
+ const keys = new Set(Object.keys(raw));
60
+ if (Array.isArray(raw.stages)) {
61
+ for (const entry of raw.stages) {
62
+ if (isPlainObject(entry)) {
63
+ for (const key of Object.keys(entry))
64
+ keys.add(key);
65
+ }
66
+ }
67
+ }
68
+ return keys;
69
+ }
70
+ export function classifyYamlDocument(raw) {
71
+ return dialectFromKeys(collectDocumentKeys(raw));
72
+ }
73
+ export function mixedDialectIssue(message) {
74
+ return {
75
+ code: "catalog.mixed_yaml_dialect",
76
+ message: message ??
77
+ "YAML mixes legacy and target contract keys; use one dialect per file",
78
+ category: "catalog",
79
+ };
80
+ }
81
+ function invalidIo(label, message, stageId) {
82
+ return loadFailure([
83
+ {
84
+ code: "stage.invalid_io",
85
+ message: `Invalid stage ${label}: ${message}`,
86
+ category: "stage",
87
+ stageId,
88
+ },
89
+ ]);
90
+ }
91
+ function invalidVerify(stageId, message, category) {
92
+ return loadFailure([
93
+ {
94
+ code: "pipeline.invalid_verify",
95
+ message: `Stage "${stageId}" verify: ${message}`,
96
+ category,
97
+ },
98
+ ]);
99
+ }
100
+ function parseIo(raw, label, stageId, deferSchemaRefs) {
101
+ if (raw === undefined)
102
+ return loadSuccess({});
103
+ if (!isPlainObject(raw)) {
104
+ return invalidIo(label, "io must be an object", stageId);
105
+ }
106
+ for (const key of Object.keys(raw)) {
107
+ if (key !== "input" && key !== "output") {
108
+ return invalidIo(label, `io: unknown key "${key}"`, stageId);
109
+ }
110
+ }
111
+ const result = {};
112
+ for (const [field, target] of [
113
+ ["output", "payload_schema"],
114
+ ["input", "clone_input_schema"],
115
+ ]) {
116
+ const side = raw[field];
117
+ if (side === undefined)
118
+ continue;
119
+ if (!isPlainObject(side)) {
120
+ return invalidIo(label, `io.${field} must be an object`, stageId);
121
+ }
122
+ for (const key of Object.keys(side)) {
123
+ if (key !== "schema") {
124
+ return invalidIo(label, `io.${field}: unknown key "${key}"`, stageId);
125
+ }
126
+ }
127
+ if (side.schema === undefined)
128
+ continue;
129
+ if (side.schema === null || typeof side.schema !== "object" || Array.isArray(side.schema)) {
130
+ return invalidIo(label, `io.${field}.schema must be an object`, stageId);
131
+ }
132
+ try {
133
+ compilePayloadSchema(side.schema);
134
+ }
135
+ catch (err) {
136
+ if (err instanceof UnresolvedSchemaRefError) {
137
+ if (deferSchemaRefs) {
138
+ if (target === "payload_schema")
139
+ result.payload_schema = side.schema;
140
+ else
141
+ result.clone_input_schema = side.schema;
142
+ continue;
143
+ }
144
+ return loadFailure([
145
+ {
146
+ code: "stage.unresolved_schema_ref",
147
+ message: `Invalid stage ${label}: ${err.message}`,
148
+ category: "stage",
149
+ stageId,
150
+ },
151
+ ]);
152
+ }
153
+ const message = err instanceof Error ? err.message : String(err);
154
+ return invalidIo(label, `invalid io.${field}.schema: ${message}`, stageId);
155
+ }
156
+ if (target === "payload_schema")
157
+ result.payload_schema = side.schema;
158
+ else
159
+ result.clone_input_schema = side.schema;
160
+ }
161
+ return loadSuccess(result);
162
+ }
163
+ const EMIT_LEGAL_TYPES = new Set(["gate", "artifact"]);
164
+ function parseVerifyWhen(item, type, stageId, index, category) {
165
+ if (item.when === undefined) {
166
+ if (type === "artifact") {
167
+ return invalidVerify(stageId, `[${index}] type artifact requires when`, category);
168
+ }
169
+ if (type === "gate")
170
+ return loadSuccess(["emit"]);
171
+ return loadSuccess(["after"]);
172
+ }
173
+ if (!Array.isArray(item.when) || item.when.length === 0) {
174
+ return invalidVerify(stageId, `[${index}].when must be a non-empty array of emit and/or after`, category);
175
+ }
176
+ const phases = [];
177
+ const seen = new Set();
178
+ for (const value of item.when) {
179
+ if (value !== "emit" && value !== "after") {
180
+ return invalidVerify(stageId, `[${index}].when must be a non-empty unique subset of emit, after`, category);
181
+ }
182
+ if (seen.has(value)) {
183
+ return invalidVerify(stageId, `[${index}].when must not contain duplicates`, category);
184
+ }
185
+ seen.add(value);
186
+ phases.push(value);
187
+ }
188
+ if (phases.includes("emit") && !EMIT_LEGAL_TYPES.has(type)) {
189
+ return invalidVerify(stageId, `[${index}] type "${type}" cannot use when: emit`, category);
190
+ }
191
+ return loadSuccess(phases);
192
+ }
193
+ function withoutWhen(item) {
194
+ const next = {};
195
+ for (const [key, value] of Object.entries(item)) {
196
+ if (key !== "when")
197
+ next[key] = value;
198
+ }
199
+ return next;
200
+ }
201
+ function emitArtifactRaw(item, stageId, index, category) {
202
+ const basenameSource = typeof item.basename === "string" && item.basename.trim()
203
+ ? item.basename.trim()
204
+ : typeof item.path === "string" && item.path.trim()
205
+ ? path.basename(item.path.trim())
206
+ : "";
207
+ if (!basenameSource) {
208
+ return invalidVerify(stageId, `[${index}] emit artifact requires path or basename`, category);
209
+ }
210
+ return loadSuccess({
211
+ id: item.id,
212
+ type: "artifact_declared",
213
+ basename: basenameSource,
214
+ });
215
+ }
216
+ function afterArtifactRaw(item, stageId, index, category) {
217
+ const artifactPath = typeof item.path === "string" && item.path.trim()
218
+ ? item.path.trim()
219
+ : typeof item.basename === "string" && item.basename.trim()
220
+ ? item.basename.trim()
221
+ : "";
222
+ if (!artifactPath) {
223
+ return invalidVerify(stageId, `[${index}] after artifact requires path or basename`, category);
224
+ }
225
+ if (item.nonempty !== undefined && typeof item.nonempty !== "boolean") {
226
+ return invalidVerify(stageId, `[${index}].nonempty must be a boolean`, category);
227
+ }
228
+ return loadSuccess({
229
+ id: item.id,
230
+ type: "artifact",
231
+ path: artifactPath,
232
+ ...(typeof item.nonempty === "boolean" ? { nonempty: item.nonempty } : {}),
233
+ });
234
+ }
235
+ function parseVerifyList(raw, stageId, category) {
236
+ if (raw === undefined)
237
+ return loadSuccess({});
238
+ if (!Array.isArray(raw) || raw.length === 0) {
239
+ return invalidVerify(stageId, "must be a non-empty array", category);
240
+ }
241
+ const preEmit = [];
242
+ const afterChecks = [];
243
+ for (let index = 0; index < raw.length; index++) {
244
+ const item = raw[index];
245
+ if (!isPlainObject(item)) {
246
+ return invalidVerify(stageId, `[${index}] must be an object`, category);
247
+ }
248
+ if (typeof item.type !== "string" || item.type.trim() === "") {
249
+ return invalidVerify(stageId, `[${index}].type must be a string`, category);
250
+ }
251
+ const type = item.type.trim();
252
+ const whenOutcome = parseVerifyWhen(item, type, stageId, index, category);
253
+ if (!whenOutcome.ok)
254
+ return whenOutcome;
255
+ const stripped = withoutWhen(item);
256
+ if (whenOutcome.value.includes("emit")) {
257
+ if (type === "artifact") {
258
+ const emitRaw = emitArtifactRaw(item, stageId, index, category);
259
+ if (!emitRaw.ok)
260
+ return emitRaw;
261
+ preEmit.push(emitRaw.value);
262
+ }
263
+ else {
264
+ preEmit.push(stripped);
265
+ }
266
+ }
267
+ if (whenOutcome.value.includes("after")) {
268
+ if (type === "artifact") {
269
+ const afterRaw = afterArtifactRaw(item, stageId, index, category);
270
+ if (!afterRaw.ok)
271
+ return afterRaw;
272
+ afterChecks.push(afterRaw.value);
273
+ }
274
+ else {
275
+ afterChecks.push(stripped);
276
+ }
277
+ }
278
+ }
279
+ return loadSuccess({
280
+ ...(preEmit.length > 0 ? { pre_emit_raw: preEmit } : {}),
281
+ ...(afterChecks.length > 0 ? { completionRaw: { mode: "all", checks: afterChecks } } : {}),
282
+ });
283
+ }
284
+ /** Compile target YAML (`io` / `verify` / `on_verify_fail`) onto IR fields. */
285
+ export function compileTargetContract(raw, ctx) {
286
+ const ioOutcome = parseIo(raw.io, ctx.label, ctx.stageId, ctx.deferSchemaRefs === true);
287
+ if (!ioOutcome.ok)
288
+ return ioOutcome;
289
+ const verifyOutcome = parseVerifyList(raw.verify, ctx.stageId, ctx.category);
290
+ if (!verifyOutcome.ok)
291
+ return verifyOutcome;
292
+ const policyOutcome = parseExecutionPolicy({
293
+ completion: verifyOutcome.value.completionRaw,
294
+ recovery: raw.on_verify_fail,
295
+ }, ctx.stageId, { requireCompletionForRecovery: false });
296
+ if (!policyOutcome.ok)
297
+ return policyOutcome;
298
+ return loadSuccess({
299
+ ...ioOutcome.value,
300
+ ...(verifyOutcome.value.pre_emit_raw !== undefined
301
+ ? { pre_emit_raw: verifyOutcome.value.pre_emit_raw }
302
+ : {}),
303
+ ...(policyOutcome.value.completion !== undefined
304
+ ? { completion: policyOutcome.value.completion }
305
+ : {}),
306
+ ...(policyOutcome.value.recovery !== undefined
307
+ ? { recovery: policyOutcome.value.recovery }
308
+ : {}),
309
+ });
310
+ }
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,16 +1,48 @@
1
1
  import type { TSchema } from "typebox/type";
2
2
  import { type StageEnvelope } from "../types/envelope.js";
3
3
  export type CompiledPayloadSchema = TSchema;
4
+ export type PayloadSchemaMap = Record<string, unknown>;
5
+ export type CompilePayloadSchemaOptions = {
6
+ schemas?: PayloadSchemaMap;
7
+ };
8
+ export declare class UnresolvedSchemaRefError extends Error {
9
+ readonly ref: string;
10
+ constructor(path: string, ref: string);
11
+ }
4
12
  /**
5
- * Compile a JSON Schema subset used for stage payload_schema.
13
+ * Compile a JSON Schema subset for IR `payload_schema` / `clone_input_schema`
14
+ * (YAML: `io.output.schema` / `io.input.schema`).
6
15
  * Supported: type object/string/number/integer/boolean/array,
7
16
  * properties, required, items, additionalProperties (boolean),
8
17
  * minItems, enum, minimum, maximum. String nodes also accept
9
18
  * pattern (a JS RegExp validated with the unicode flag), minLength,
10
19
  * and maxLength (non-negative integers). Nested nodes accept
11
20
  * nullable (boolean), compiling to a union of that type with null;
12
- * the root object cannot be nullable. Unknown keywords are ignored.
21
+ * the root object cannot be nullable. `$ref` is resolved against
22
+ * `options.schemas` using JSON Pointer `#/schemas/<name>`. Unknown
23
+ * keywords other than `$ref` are ignored.
13
24
  */
14
- export declare function compilePayloadSchema(raw: unknown): CompiledPayloadSchema;
15
- export declare function assertEnvelopePayload(envelope: StageEnvelope, payloadSchema: unknown | undefined): void;
16
- export declare function assertCloneAssignmentPayload(envelope: StageEnvelope, cloneInputSchema: unknown, successorId: string, itemPath?: string): void;
25
+ export declare function compilePayloadSchema(raw: unknown, options?: CompilePayloadSchemaOptions): CompiledPayloadSchema;
26
+ /**
27
+ * After `$ref` resolve, true when every success payload that satisfies the
28
+ * producer schema also satisfies the consumer schema (producer output
29
+ * assignable to consumer input). Extra producer fields are allowed unless
30
+ * the consumer sets `additionalProperties: false`, which also requires the
31
+ * producer to close extras. A number consumer accepts an integer producer;
32
+ * the reverse is incompatible. Producer enums must be subsets of consumer
33
+ * enums; a consumer enum with an unconstrained producer is incompatible.
34
+ * String `pattern` values must be identical when the consumer sets one.
35
+ * Producer `minimum`/`maximum`, `minLength`/`maxLength`, and array `minItems`
36
+ * must fit inside the consumer bounds (a consumer bound with an omitted
37
+ * producer bound is incompatible). A nullable producer is incompatible unless
38
+ * the consumer is also nullable.
39
+ */
40
+ export declare function isPayloadSchemaSubset(consumer: unknown, producer: unknown, options?: CompilePayloadSchemaOptions): boolean;
41
+ export declare function expandPayloadSchemaRefs(raw: unknown, options?: CompilePayloadSchemaOptions): unknown;
42
+ export declare function payloadInstanceMismatch(instance: unknown, payloadSchema: unknown, options?: CompilePayloadSchemaOptions): string | undefined;
43
+ export declare function assertEnvelopePayload(envelope: StageEnvelope, payloadSchema: unknown | undefined, options?: CompilePayloadSchemaOptions): void;
44
+ /**
45
+ * Validate clone/fan-out assignment against the successor's IR
46
+ * `clone_input_schema` (YAML: `io.input.schema`).
47
+ */
48
+ export declare function assertCloneAssignmentPayload(envelope: StageEnvelope, cloneInputSchema: unknown, successorId: string, itemPath?: string, options?: CompilePayloadSchemaOptions): void;