@intentius/chant 0.124.23 → 0.124.25

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 (210) hide show
  1. package/dist/ci-jobs.d.ts +41 -1
  2. package/dist/ci-jobs.d.ts.map +1 -1
  3. package/dist/ci-jobs.js +48 -1
  4. package/dist/cli/commands/build.d.ts.map +1 -1
  5. package/dist/cli/commands/build.js +14 -0
  6. package/dist/cli/handlers/approve.d.ts.map +1 -1
  7. package/dist/cli/handlers/approve.js +9 -0
  8. package/dist/cli/handlers/emulator.d.ts +13 -2
  9. package/dist/cli/handlers/emulator.d.ts.map +1 -1
  10. package/dist/cli/handlers/emulator.js +63 -3
  11. package/dist/cli/handlers/promote.d.ts.map +1 -1
  12. package/dist/cli/handlers/promote.js +7 -1
  13. package/dist/cli/handlers/run-wave.d.ts.map +1 -1
  14. package/dist/cli/handlers/run-wave.js +5 -4
  15. package/dist/cli/handlers/run.d.ts +4 -3
  16. package/dist/cli/handlers/run.d.ts.map +1 -1
  17. package/dist/cli/handlers/run.js +73 -9
  18. package/dist/cli/main.d.ts.map +1 -1
  19. package/dist/cli/main.js +38 -11
  20. package/dist/cli/mcp/workspace-tools.d.ts +3 -3
  21. package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
  22. package/dist/cli/mcp/workspace-tools.js +27 -4
  23. package/dist/cli/parse-args.d.ts.map +1 -1
  24. package/dist/cli/parse-args.js +31 -0
  25. package/dist/cli/registry.d.ts +12 -0
  26. package/dist/cli/registry.d.ts.map +1 -1
  27. package/dist/components/cli-support.d.ts +45 -6
  28. package/dist/components/cli-support.d.ts.map +1 -1
  29. package/dist/components/cli-support.js +97 -22
  30. package/dist/components/driver.d.ts.map +1 -1
  31. package/dist/components/driver.js +25 -10
  32. package/dist/effect-receipt.d.ts +13 -0
  33. package/dist/effect-receipt.d.ts.map +1 -1
  34. package/dist/effect-receipt.js +19 -0
  35. package/dist/graph-detail.d.ts.map +1 -1
  36. package/dist/graph-detail.js +24 -2
  37. package/dist/lexicon-module.d.ts +7 -1
  38. package/dist/lexicon-module.d.ts.map +1 -1
  39. package/dist/lexicon-module.js +7 -2
  40. package/dist/lifecycle/git.d.ts +11 -0
  41. package/dist/lifecycle/git.d.ts.map +1 -1
  42. package/dist/lifecycle/git.js +62 -6
  43. package/dist/lifecycle/no-record.d.ts +59 -0
  44. package/dist/lifecycle/no-record.d.ts.map +1 -0
  45. package/dist/lifecycle/no-record.js +88 -0
  46. package/dist/lifecycle/receipt-plan.d.ts.map +1 -1
  47. package/dist/lifecycle/receipt-plan.js +4 -2
  48. package/dist/op/activities/shell.d.ts.map +1 -1
  49. package/dist/op/activities/shell.js +30 -9
  50. package/dist/op/emulator-lifecycle.d.ts +23 -0
  51. package/dist/op/emulator-lifecycle.d.ts.map +1 -1
  52. package/dist/op/emulator-lifecycle.js +39 -2
  53. package/dist/op/gate.d.ts.map +1 -1
  54. package/dist/op/gate.js +3 -0
  55. package/dist/op/index.d.ts +1 -1
  56. package/dist/op/index.d.ts.map +1 -1
  57. package/dist/op/index.js +1 -1
  58. package/dist/op/local-executor.d.ts +10 -0
  59. package/dist/op/local-executor.d.ts.map +1 -1
  60. package/dist/op/local-executor.js +71 -7
  61. package/dist/op/op-waves-notify.d.ts +2 -0
  62. package/dist/op/op-waves-notify.d.ts.map +1 -1
  63. package/dist/op/op-waves-notify.js +9 -3
  64. package/dist/op/op-waves-run.d.ts +8 -2
  65. package/dist/op/op-waves-run.d.ts.map +1 -1
  66. package/dist/op/op-waves-run.js +36 -11
  67. package/dist/op/op-waves.d.ts +85 -1
  68. package/dist/op/op-waves.d.ts.map +1 -1
  69. package/dist/op/op-waves.js +153 -1
  70. package/dist/op/receipt-store.d.ts +5 -0
  71. package/dist/op/receipt-store.d.ts.map +1 -1
  72. package/dist/op/receipt-store.js +4 -3
  73. package/dist/op/runtimes/local.d.ts.map +1 -1
  74. package/dist/op/runtimes/local.js +6 -0
  75. package/dist/op/step-console.d.ts +65 -0
  76. package/dist/op/step-console.d.ts.map +1 -0
  77. package/dist/op/step-console.js +140 -0
  78. package/dist/op/step-output-ref.d.ts +26 -18
  79. package/dist/op/step-output-ref.d.ts.map +1 -1
  80. package/dist/op/step-output-ref.js +86 -63
  81. package/dist/point-decision.d.ts.map +1 -1
  82. package/dist/point-decision.js +3 -0
  83. package/dist/wave-runtime.mjs +239 -239
  84. package/dist/workspace/agent-cli.d.ts.map +1 -1
  85. package/dist/workspace/agent-cli.js +3 -0
  86. package/dist/workspace/compose-graph.d.ts +22 -3
  87. package/dist/workspace/compose-graph.d.ts.map +1 -1
  88. package/dist/workspace/compose-graph.js +18 -5
  89. package/dist/workspace/conformance/__writer_fixture__/answers/answer.schema.json +16 -1
  90. package/dist/workspace/conformance/writer.js +1 -1
  91. package/dist/workspace/decide.d.ts +51 -9
  92. package/dist/workspace/decide.d.ts.map +1 -1
  93. package/dist/workspace/decide.js +125 -14
  94. package/dist/workspace/graph.schema.json +68 -1
  95. package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
  96. package/dist/workspace/lineage-upgrade.js +20 -11
  97. package/dist/workspace/nested-graph.d.ts +1 -1
  98. package/dist/workspace/nested-graph.d.ts.map +1 -1
  99. package/dist/workspace/nested-graph.js +5 -1
  100. package/dist/workspace/point-answer.schema.json +16 -1
  101. package/dist/workspace/points-cli.d.ts +5 -3
  102. package/dist/workspace/points-cli.d.ts.map +1 -1
  103. package/dist/workspace/points-cli.js +16 -7
  104. package/dist/workspace/points-write.schema.json +39 -4
  105. package/dist/workspace/points.schema.json +42 -3
  106. package/dist/workspace/reason-codes.d.ts +2 -1
  107. package/dist/workspace/reason-codes.d.ts.map +1 -1
  108. package/dist/workspace/reason-codes.js +2 -1
  109. package/dist/workspace/records-cli.d.ts +12 -2
  110. package/dist/workspace/records-cli.d.ts.map +1 -1
  111. package/dist/workspace/records-cli.js +27 -7
  112. package/dist/workspace/records-write.d.ts.map +1 -1
  113. package/dist/workspace/records-write.js +2 -3
  114. package/dist/workspace/records.d.ts +10 -4
  115. package/dist/workspace/records.d.ts.map +1 -1
  116. package/dist/workspace/records.js +16 -3
  117. package/dist/workspace/records.schema.json +2 -1
  118. package/dist/workspace/runs.d.ts.map +1 -1
  119. package/dist/workspace/runs.js +4 -2
  120. package/dist/workspace/trust/verify-cli.d.ts.map +1 -1
  121. package/dist/workspace/trust/verify-cli.js +2 -1
  122. package/dist/workspace/trust/verify.d.ts +22 -0
  123. package/dist/workspace/trust/verify.d.ts.map +1 -1
  124. package/dist/workspace/trust/verify.js +94 -14
  125. package/dist/workspace/write-scope.d.ts +3 -1
  126. package/dist/workspace/write-scope.d.ts.map +1 -1
  127. package/dist/workspace/write-scope.js +11 -4
  128. package/package.json +1 -1
  129. package/src/ci-jobs.ts +58 -1
  130. package/src/cli/commands/build.ts +18 -0
  131. package/src/cli/compiled-cli.e2e.test.ts +45 -1
  132. package/src/cli/handlers/approve.ts +9 -0
  133. package/src/cli/handlers/emulator.test.ts +61 -1
  134. package/src/cli/handlers/emulator.ts +65 -4
  135. package/src/cli/handlers/promote.test.ts +7 -0
  136. package/src/cli/handlers/promote.ts +7 -1
  137. package/src/cli/handlers/run-no-record.e2e.test.ts +87 -0
  138. package/src/cli/handlers/run-wave.ts +5 -4
  139. package/src/cli/handlers/run.test.ts +126 -0
  140. package/src/cli/handlers/run.ts +69 -9
  141. package/src/cli/main.test.ts +34 -0
  142. package/src/cli/main.ts +38 -11
  143. package/src/cli/mcp/workspace-tools.e2e.test.ts +1 -1
  144. package/src/cli/mcp/workspace-tools.ts +27 -4
  145. package/src/cli/parse-args.ts +21 -0
  146. package/src/cli/registry.ts +12 -0
  147. package/src/components/cli-support.test.ts +75 -0
  148. package/src/components/cli-support.ts +130 -25
  149. package/src/components/driver.test.ts +28 -0
  150. package/src/components/driver.ts +23 -10
  151. package/src/components/generate-disabled.test.ts +56 -0
  152. package/src/effect-receipt.ts +22 -0
  153. package/src/graph-ci-edges.test.ts +50 -0
  154. package/src/graph-detail.ts +21 -2
  155. package/src/lexicon-module.ts +9 -2
  156. package/src/lifecycle/git.ts +61 -6
  157. package/src/lifecycle/no-record.test.ts +175 -0
  158. package/src/lifecycle/no-record.ts +108 -0
  159. package/src/lifecycle/receipt-plan.test.ts +9 -0
  160. package/src/lifecycle/receipt-plan.ts +4 -1
  161. package/src/op/activities/shell.ts +28 -6
  162. package/src/op/effect-step.test.ts +95 -1
  163. package/src/op/emulator-lifecycle.test.ts +24 -1
  164. package/src/op/emulator-lifecycle.ts +58 -2
  165. package/src/op/gate.ts +3 -0
  166. package/src/op/index.ts +1 -1
  167. package/src/op/local-executor.test.ts +65 -0
  168. package/src/op/local-executor.ts +88 -8
  169. package/src/op/op-waves-jobs.test.ts +213 -0
  170. package/src/op/op-waves-notify.ts +11 -5
  171. package/src/op/op-waves-run.ts +42 -11
  172. package/src/op/op-waves.ts +202 -2
  173. package/src/op/receipt-store.ts +13 -3
  174. package/src/op/runtimes/local.ts +6 -0
  175. package/src/op/step-console.test.ts +150 -0
  176. package/src/op/step-console.ts +155 -0
  177. package/src/op/step-output-ref.test.ts +68 -24
  178. package/src/op/step-output-ref.ts +85 -65
  179. package/src/point-decision-bundle.test.ts +1 -0
  180. package/src/point-decision.ts +3 -0
  181. package/src/workspace/agent-cli.ts +2 -0
  182. package/src/workspace/compose-graph.test.ts +55 -0
  183. package/src/workspace/compose-graph.ts +28 -6
  184. package/src/workspace/conformance/__writer_fixture__/answers/answer.schema.json +16 -1
  185. package/src/workspace/conformance/writer.ts +1 -1
  186. package/src/workspace/decide.test.ts +87 -3
  187. package/src/workspace/decide.ts +142 -17
  188. package/src/workspace/graph-contract.test.ts +26 -1
  189. package/src/workspace/graph.schema.json +68 -1
  190. package/src/workspace/lineage-upgrade.test.ts +21 -0
  191. package/src/workspace/lineage-upgrade.ts +19 -11
  192. package/src/workspace/nested-graph.test.ts +14 -0
  193. package/src/workspace/nested-graph.ts +7 -2
  194. package/src/workspace/point-answer.schema.json +16 -1
  195. package/src/workspace/points-cli.ts +16 -8
  196. package/src/workspace/points-write.schema.json +39 -4
  197. package/src/workspace/points.schema.json +42 -3
  198. package/src/workspace/reason-codes.ts +2 -1
  199. package/src/workspace/record-assets.test.ts +14 -0
  200. package/src/workspace/records-cli.ts +28 -7
  201. package/src/workspace/records-write-commands.e2e.test.ts +32 -0
  202. package/src/workspace/records-write.ts +2 -3
  203. package/src/workspace/records.schema.json +2 -1
  204. package/src/workspace/records.ts +17 -4
  205. package/src/workspace/runs.ts +4 -2
  206. package/src/workspace/trust/trust.test.ts +105 -0
  207. package/src/workspace/trust/verify-cli.ts +2 -1
  208. package/src/workspace/trust/verify.ts +106 -13
  209. package/src/workspace/write-scope.test.ts +18 -0
  210. package/src/workspace/write-scope.ts +16 -5
@@ -216,6 +216,8 @@ export function assertOpWavesSpec(spec) {
216
216
  }
217
217
  if (spec.neverOverMcp !== undefined && typeof spec.neverOverMcp !== "boolean")
218
218
  throw new Error(`${where}: neverOverMcp must be true or false.`);
219
+ if (spec.runner !== undefined)
220
+ assertOpWaveRunner(where, "runner", spec.runner);
219
221
  if (spec.notify !== undefined) {
220
222
  const problem = notifyConfigProblem(spec.notify, "notify");
221
223
  if (problem)
@@ -241,6 +243,12 @@ export function assertOpWavesSpec(spec) {
241
243
  if (problems.length > 0)
242
244
  throw new Error(`${where}: wave "${wave.name}": ${problems.join("; ")}.`);
243
245
  }
246
+ if (wave.runner !== undefined)
247
+ assertOpWaveRunner(where, `wave "${wave.name}" runner`, wave.runner);
248
+ if (wave.idTokens !== undefined)
249
+ assertOpWaveIdTokens(where, `wave "${wave.name}" idTokens`, wave.idTokens);
250
+ if (wave.jobs !== undefined)
251
+ assertOpWaveJobGroups(where, wave);
244
252
  if (wave.discover !== undefined && typeof wave.discover !== "boolean")
245
253
  throw new Error(`${where}: wave "${wave.name}" has discover ${JSON.stringify(wave.discover)}; use true or false.`);
246
254
  if (wave.rest !== undefined && typeof wave.rest !== "boolean")
@@ -282,7 +290,127 @@ export function assertOpWavesSpec(spec) {
282
290
  if (shares > wave.runs.length) {
283
291
  throw new Error(`${where}: wave "${wave.name}" has ${shares} shares for ${wave.runs.length} runs, so a share would apply nothing.`);
284
292
  }
293
+ if (wave.jobs !== undefined) {
294
+ const problem = opWaveGroupsProblem(wave, wave.runs.map((r) => r.target));
295
+ if (problem)
296
+ throw new Error(`${where}: ${problem}.`);
297
+ }
298
+ }
299
+ }
300
+ const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
301
+ /** Refuse a `runner` no forge could read (#3914). */
302
+ function assertOpWaveRunner(where, field, runner) {
303
+ const labels = (value) => Array.isArray(value) && value.length > 0 && value.every((l) => typeof l === "string" && l.trim() !== "");
304
+ if (typeof runner === "string" && runner.trim() !== "")
305
+ return;
306
+ if (labels(runner))
307
+ return;
308
+ if (runner !== null && typeof runner === "object" && !Array.isArray(runner)) {
309
+ const { group, labels: l, ...rest } = runner;
310
+ const extra = Object.keys(rest)[0];
311
+ if (extra !== undefined)
312
+ throw new Error(`${where}: ${field}.${extra} is not a setting; a runner group is { "group": "<name>", "labels"?: [...] }.`);
313
+ if (typeof group === "string" && group.trim() !== "" && (l === undefined || labels(l)))
314
+ return;
315
+ }
316
+ throw new Error(`${where}: ${field} must be a runner label, a non-empty list of labels, or { "group": "<name>", "labels"?: [...] } (GitHub only).`);
317
+ }
318
+ /** Refuse `idTokens` that are not `{ <variable>: <audience> }` (#3914). */
319
+ function assertOpWaveIdTokens(where, field, idTokens) {
320
+ if (idTokens === null || typeof idTokens !== "object" || Array.isArray(idTokens) || Object.keys(idTokens).length === 0) {
321
+ throw new Error(`${where}: ${field} must be { "<variable>": "<audience>" }, with at least one token.`);
285
322
  }
323
+ for (const [name, aud] of Object.entries(idTokens)) {
324
+ if (!ENV_NAME.test(name))
325
+ throw new Error(`${where}: ${field} names "${name}", which is not a variable name (letters, digits and "_", not starting with a digit).`);
326
+ if (typeof aud !== "string" || aud.trim() === "")
327
+ throw new Error(`${where}: ${field}.${name} must be an audience, a non-empty string.`);
328
+ }
329
+ }
330
+ /** Refuse a wave's `jobs` no pipeline could render (#3914). Which runs each group holds is {@link opWaveGroupsProblem}'s. */
331
+ function assertOpWaveJobGroups(where, wave) {
332
+ const at = `wave "${wave.name}"`;
333
+ if (!Array.isArray(wave.jobs) || wave.jobs.length === 0)
334
+ throw new Error(`${where}: ${at} jobs must be a non-empty list of job groups.`);
335
+ if ((wave.shares ?? 1) > 1)
336
+ throw new Error(`${where}: ${at} has jobs and shares; split each job group with its own shares.`);
337
+ if (wave.rest)
338
+ throw new Error(`${where}: ${at} has jobs and rest; the waves after it run in its one job, so its apply cannot be split into groups.`);
339
+ const names = new Set();
340
+ wave.jobs.forEach((group, i) => {
341
+ if (group === null || typeof group !== "object")
342
+ throw new Error(`${where}: ${at} jobs[${i}] must be { "name", "runs", ... }.`);
343
+ const field = `${at} jobs[${i}]${typeof group.name === "string" ? ` (${group.name})` : ""}`;
344
+ if (typeof group.name !== "string" || !NAME.test(group.name))
345
+ throw new Error(`${where}: ${field} must be named with lowercase letters, digits, "-" and "_".`);
346
+ if (group.name === "decide" || /^share-\d+$/.test(group.name))
347
+ throw new Error(`${where}: ${field} is named like the wave's own jobs (decide, share-<i>); name it another way.`);
348
+ if (names.has(group.name))
349
+ throw new Error(`${where}: ${at} names job group "${group.name}" twice.`);
350
+ names.add(group.name);
351
+ const known = new Set(["name", "runs", "shares", "runner", "environment", "variables", "setup", "idTokens"]);
352
+ const extra = Object.keys(group).find((key) => !known.has(key));
353
+ if (extra !== undefined)
354
+ throw new Error(`${where}: ${field}.${extra} is not a setting; a job group takes ${[...known].join(", ")}.`);
355
+ if (!Array.isArray(group.runs) || group.runs.length === 0 || !group.runs.every((r) => typeof r === "string" && r.trim() !== "")) {
356
+ throw new Error(`${where}: ${field} runs must be a non-empty list of targets or patterns.`);
357
+ }
358
+ const shares = group.shares ?? 1;
359
+ if (!Number.isInteger(shares) || shares < 1)
360
+ throw new Error(`${where}: ${field} has shares ${group.shares}; use a whole number from 1.`);
361
+ if (group.runner !== undefined)
362
+ assertOpWaveRunner(where, `${field} runner`, group.runner);
363
+ if (group.idTokens !== undefined)
364
+ assertOpWaveIdTokens(where, `${field} idTokens`, group.idTokens);
365
+ if (group.variables !== undefined && (group.variables === null || typeof group.variables !== "object" || Array.isArray(group.variables) || !Object.values(group.variables).every((v) => typeof v === "string"))) {
366
+ throw new Error(`${where}: ${field} variables must be { "<name>": "<value>" }.`);
367
+ }
368
+ if (group.setup !== undefined && !Array.isArray(group.setup))
369
+ throw new Error(`${where}: ${field} setup must be a list of steps.`);
370
+ if (group.environment !== undefined && (group.environment === null || typeof group.environment !== "object" || typeof group.environment.name !== "string")) {
371
+ throw new Error(`${where}: ${field} environment must be { "name", "url"? }.`);
372
+ }
373
+ });
374
+ }
375
+ /**
376
+ * True when `target` matches a job group's `runs` entry (#3914): the target
377
+ * itself, or a pattern where `*` matches within one `/` segment and `**`
378
+ * across segments.
379
+ */
380
+ export function opWaveRunMatches(pattern, target) {
381
+ if (!pattern.includes("*"))
382
+ return pattern === target;
383
+ let re = "";
384
+ for (let i = 0; i < pattern.length; i++) {
385
+ const c = pattern[i];
386
+ if (c === "*" && pattern[i + 1] === "*") {
387
+ re += ".*";
388
+ i++;
389
+ }
390
+ else if (c === "*")
391
+ re += "[^/]*";
392
+ else
393
+ re += c.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
394
+ }
395
+ return new RegExp(`^${re}$`).test(target);
396
+ }
397
+ /** The runs of `runs` that job group `group` holds (#3914). */
398
+ export function opWaveGroupRuns(group, runs) {
399
+ return runs.filter((run) => group.runs.some((pattern) => opWaveRunMatches(pattern, run.target)));
400
+ }
401
+ /**
402
+ * What is wrong with how a wave's job groups hold `targets` (#3914): a run in
403
+ * two groups, or in none. Undefined when each run is in exactly one.
404
+ */
405
+ export function opWaveGroupsProblem(wave, targets) {
406
+ for (const target of targets) {
407
+ const holders = (wave.jobs ?? []).filter((group) => group.runs.some((pattern) => opWaveRunMatches(pattern, target))).map((g) => `"${g.name}"`);
408
+ if (holders.length > 1)
409
+ return `wave "${wave.name}" jobs: run "${target}" is in job groups ${holders.join(" and ")}; each run is in exactly one`;
410
+ if (holders.length === 0)
411
+ return `wave "${wave.name}" jobs: run "${target}" is in no job group; each run is in exactly one`;
412
+ }
413
+ return undefined;
286
414
  }
287
415
  function assertArgv(where, field, argv) {
288
416
  if (!Array.isArray(argv) || argv.length === 0 || !argv.every((part) => typeof part === "string") || argv[0] === "") {
@@ -381,7 +509,7 @@ export function opWaveJobs(spec, specFile) {
381
509
  const command = ["chant", "run", "wave", "--spec", specFile, "--wave", String(k), ...(spec.base ? ["--base", spec.base] : [])];
382
510
  const prefix = `wave-${k}-${wave.name}`;
383
511
  const shares = wave.shares ?? 1;
384
- if (shares === 1) {
512
+ if (shares === 1 && wave.jobs === undefined) {
385
513
  jobs.push({ jobName: prefix, wave: k, waveName: wave.name, kind: "wave", needs: previous, command });
386
514
  previous = [prefix];
387
515
  return;
@@ -390,6 +518,26 @@ export function opWaveJobs(spec, specFile) {
390
518
  const decide = `${prefix}-decide`;
391
519
  jobs.push({ jobName: decide, wave: k, waveName: wave.name, kind: "decide", needs: previous, command: [...command, "--decide"], record });
392
520
  const shareJobs = [];
521
+ if (wave.jobs !== undefined) {
522
+ // #3914: a job per group, or per share of a group, each applying its runs under the deciding job's decision.
523
+ for (const group of wave.jobs) {
524
+ const groupShares = group.shares ?? 1;
525
+ const groupCommand = [...command, "--group", group.name];
526
+ if (groupShares === 1) {
527
+ const jobName = `${prefix}-${group.name}`;
528
+ shareJobs.push(jobName);
529
+ jobs.push({ jobName, wave: k, waveName: wave.name, kind: "share", group: group.name, needs: [decide], command: groupCommand, record });
530
+ continue;
531
+ }
532
+ for (let share = 1; share <= groupShares; share++) {
533
+ const jobName = `${prefix}-${group.name}-share-${share}`;
534
+ shareJobs.push(jobName);
535
+ jobs.push({ jobName, wave: k, waveName: wave.name, kind: "share", group: group.name, share, needs: [decide], command: [...groupCommand, "--share", String(share)], record });
536
+ }
537
+ }
538
+ previous = shareJobs;
539
+ return;
540
+ }
393
541
  for (let share = 1; share <= shares; share++) {
394
542
  const jobName = `${prefix}-share-${share}`;
395
543
  shareJobs.push(jobName);
@@ -399,6 +547,10 @@ export function opWaveJobs(spec, specFile) {
399
547
  });
400
548
  return jobs;
401
549
  }
550
+ /** The job group a job applies (#3914), or undefined for a wave's own jobs. */
551
+ export function opWaveGroupOf(spec, job) {
552
+ return job.group === undefined ? undefined : spec.waves[job.wave - 1]?.jobs?.find((g) => g.name === job.group);
553
+ }
402
554
  /** The wave a job belongs to. */
403
555
  export function opWaveOf(spec, job) {
404
556
  return spec.waves[job.wave - 1];
@@ -71,6 +71,11 @@ export interface ReceiptCheckInput {
71
71
  export declare function receiptCheckInput(receipt: EffectReceiptDeclaration): ReceiptCheckInput;
72
72
  /** Args of the `receiptRead` activity, as the effect step's codegen emits them. */
73
73
  export interface ReceiptReadArgs extends ReceiptCheckInput {
74
+ /**
75
+ * The digest of the step outputs the effect's nested steps read (#3948),
76
+ * when they read any. Folded into the expectation by {@link withStepOutputs}.
77
+ */
78
+ outputs?: string;
74
79
  }
75
80
  /** Result of the `receiptRead` activity. `current` is `null` (not `undefined`)
76
81
  * for an absent receipt so the value survives JSON transport. */
@@ -1 +1 @@
1
- {"version":3,"file":"receipt-store.d.ts","sourceRoot":"","sources":["../../src/op/receipt-store.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"receipt-store.d.ts","sourceRoot":"","sources":["../../src/op/receipt-store.ts"],"names":[],"mappings":"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"}
@@ -26,7 +26,7 @@
26
26
  * {@link ReceiptActivityOptions.resolveExpectation} hook — never by hashing
27
27
  * placeholders.
28
28
  */
29
- import { EXISTENCE_EXPECTATION, isEffectReceipt, receiptExpectation, referenceInputPaths, } from "../effect-receipt.js";
29
+ import { EXISTENCE_EXPECTATION, isEffectReceipt, receiptExpectation, referenceInputPaths, declaredExpectation, withStepOutputs, } from "../effect-receipt.js";
30
30
  /**
31
31
  * Snapshot a typed receipt declaration into check-input data: the
32
32
  * {@link EffectReceiptRef} plus the synthesis-time expectation when the
@@ -67,7 +67,8 @@ export function receiptActivities(store, opts) {
67
67
  };
68
68
  return {
69
69
  async receiptRead(args) {
70
- const expectation = await expectationOf(args);
70
+ const declared = await expectationOf(args);
71
+ const expectation = args.outputs !== undefined ? withStepOutputs(declared, args.outputs) : declared;
71
72
  const current = await store.read(args.receipt);
72
73
  return { current: current ?? null, expectation, applied: current === expectation };
73
74
  },
@@ -84,7 +85,7 @@ export function receiptActivities(store, opts) {
84
85
  if (current === undefined) {
85
86
  findings.push({ receipt: input.receipt.name, effect: input.receipt.effect, kind: "absent", expected });
86
87
  }
87
- else if (current !== expected) {
88
+ else if (declaredExpectation(current) !== expected) {
88
89
  findings.push({ receipt: input.receipt.name, effect: input.receipt.effect, kind: "differs", expected, current });
89
90
  }
90
91
  }
@@ -1 +1 @@
1
- {"version":3,"file":"local.d.ts","sourceRoot":"","sources":["../../../src/op/runtimes/local.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAUH,OAAO,EAML,KAAK,iBAAiB,EACvB,MAAM,YAAY,CAAC;AA0CpB;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,GAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,iBAAiB,CAsJ3F"}
1
+ {"version":3,"file":"local.d.ts","sourceRoot":"","sources":["../../../src/op/runtimes/local.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAWH,OAAO,EAML,KAAK,iBAAiB,EACvB,MAAM,YAAY,CAAC;AA0CpB;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,GAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,iBAAiB,CA2J3F"}
@@ -21,6 +21,7 @@ import { lexiconNames } from "../../lexicon-module.js";
21
21
  import { loadChantConfig } from "../../config.js";
22
22
  import { loadActivities, loadProfiles } from "../activity-registry.js";
23
23
  import { runOpLocally, OpRunFailure } from "../local-executor.js";
24
+ import { stepHeartbeatMs } from "../step-console.js";
24
25
  import { runComponents } from "../../components/cli-support.js";
25
26
  import { discoverOps } from "../discover.js";
26
27
  import { readRunLedger, runEnvOf, DEFAULT_RUN_ENV } from "../../lifecycle/run-ledger.js";
@@ -142,6 +143,11 @@ export function createLocalOpRuntime(opts = {}) {
142
143
  onLedgerError: (err) => process.stderr.write(`warning: run "${runId}" of "${op.name}" finished, but its record could not be ` +
143
144
  `appended to the run ledger: ${err instanceof Error ? err.message : String(err)}\n`),
144
145
  ...(startOpts.progress ? { onRecord: startOpts.progress } : {}),
146
+ // #3956: a running step's output, and a still-running line while
147
+ // it is quiet, on stderr as they happen; stdout stays the
148
+ // `--json` record's and `--progress-json`'s.
149
+ stepOutput: (line) => process.stderr.write(`${line}\n`),
150
+ stepHeartbeatMs: stepHeartbeatMs(process.env, (message) => process.stderr.write(`warning: ${message}\n`)),
145
151
  ...(startOpts.work ? { work: startOpts.work } : {}),
146
152
  onWorkLeaseWarning: (message) => process.stderr.write(`warning: "${op.name}" could not renew its work lease, retrying at the next beat: ${message}\n`),
147
153
  });
@@ -0,0 +1,65 @@
1
+ /**
2
+ * A running step's console (#3956): its output as it arrives, and a
3
+ * "still running" line while it is quiet.
4
+ *
5
+ * The run's own render (`./local-output.ts`) is printed once the run
6
+ * settles, so before this a 15-minute step printed nothing for 15 minutes and
7
+ * a watchdog reading the output killed a healthy run. When the caller hands
8
+ * the executor a writer (`RunOpOptions.stepOutput`, which the local runtime
9
+ * passes, writing to stderr), each activity step runs with a console:
10
+ *
11
+ * - an activity that runs a process (`shellCmd`) passes each line the process
12
+ * writes, stdout and stderr alike, through {@link stepOutputLine}, and it is
13
+ * printed at once as `[<phase>] <step> | <line>`;
14
+ * - a step that prints nothing for `heartbeatMs` (an activity that only
15
+ * returns a value, or a command that is quiet) gets
16
+ * `[<phase>] <step>: still running after <n>s` each time that much passes.
17
+ *
18
+ * `<step>` is the step's id when it has one, else its call as the run's
19
+ * render names it, `fn(arg=value, ...)`, cut to a readable length. Without a
20
+ * writer there is no console, and `shellCmd` prints a command's stderr when it
21
+ * ends, as it did before.
22
+ */
23
+ /** How long a step may be quiet before the runner says it is still running. */
24
+ export declare const DEFAULT_STEP_HEARTBEAT_MS = 60000;
25
+ /** The environment variable that sets the interval: a duration ("30s", "5m"), or `0` / `off`. */
26
+ export declare const STEP_HEARTBEAT_ENV = "CHANT_STEP_HEARTBEAT";
27
+ /** Where a run's step lines go, and how often a quiet step says it is still running. */
28
+ export interface RunConsole {
29
+ write: (line: string) => void;
30
+ heartbeatMs?: number;
31
+ }
32
+ /** Run `fn` as a run whose steps print through `run`. Without one, steps get no console. */
33
+ export declare function withRunConsole<T>(run: RunConsole | undefined, fn: () => Promise<T>): Promise<T>;
34
+ /** The name a step's lines carry: its id, else `fn(args)` on one line, cut to {@link MAX_LABEL}. */
35
+ export declare function stepLabel(fn: string, args: Record<string, unknown>, id?: string): string;
36
+ /**
37
+ * Run `fn` with a console for one step, when the run has one
38
+ * ({@link withRunConsole}). The heartbeat timer is stopped when `fn`
39
+ * settles; it never holds the process open on its own.
40
+ */
41
+ export declare function withStepConsole<T>(opts: {
42
+ phase: string;
43
+ label: string;
44
+ }, fn: () => Promise<T>): Promise<T>;
45
+ /** Whether the calling activity runs under a step console, so its output is printed as it arrives. */
46
+ export declare function hasStepConsole(): boolean;
47
+ /** Print one line of the calling step's output. A no-op outside a step console. */
48
+ export declare function stepOutputLine(text: string): void;
49
+ /**
50
+ * A sink for a stream's chunks that prints whole lines to the calling step's
51
+ * console as they complete; `end()` prints what is left. The console is the
52
+ * one current when the splitter is made, since a stream's `data` events need
53
+ * not run in the step's async context.
54
+ */
55
+ export declare function stepLineSplitter(): {
56
+ push(chunk: string | Buffer): void;
57
+ end(): void;
58
+ };
59
+ /**
60
+ * The heartbeat interval `CHANT_STEP_HEARTBEAT` sets, or the default. `0` and
61
+ * `off` turn it off. A value that is not a duration falls back to the default
62
+ * and says so through `warn`.
63
+ */
64
+ export declare function stepHeartbeatMs(env?: NodeJS.ProcessEnv, warn?: (message: string) => void): number;
65
+ //# sourceMappingURL=step-console.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"step-console.d.ts","sourceRoot":"","sources":["../../src/op/step-console.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAKH,+EAA+E;AAC/E,eAAO,MAAM,yBAAyB,QAAS,CAAC;AAEhD,iGAAiG;AACjG,eAAO,MAAM,kBAAkB,yBAAyB,CAAC;AASzD,wFAAwF;AACxF,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9B,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAKD,4FAA4F;AAC5F,wBAAgB,cAAc,CAAC,CAAC,EAAE,GAAG,EAAE,UAAU,GAAG,SAAS,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAE/F;AAQD,oGAAoG;AACpG,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAIxF;AAED;;;;GAIG;AACH,wBAAsB,eAAe,CAAC,CAAC,EAAE,IAAI,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CA6BjH;AAED,sGAAsG;AACtG,wBAAgB,cAAc,IAAI,OAAO,CAExC;AAED,mFAAmF;AACnF,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAEjD;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,IAAI;IAAE,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAAC,GAAG,IAAI,IAAI,CAAA;CAAE,CAmBtF;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,EAAE,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GAAG,MAAM,CAU9G"}
@@ -0,0 +1,140 @@
1
+ /**
2
+ * A running step's console (#3956): its output as it arrives, and a
3
+ * "still running" line while it is quiet.
4
+ *
5
+ * The run's own render (`./local-output.ts`) is printed once the run
6
+ * settles, so before this a 15-minute step printed nothing for 15 minutes and
7
+ * a watchdog reading the output killed a healthy run. When the caller hands
8
+ * the executor a writer (`RunOpOptions.stepOutput`, which the local runtime
9
+ * passes, writing to stderr), each activity step runs with a console:
10
+ *
11
+ * - an activity that runs a process (`shellCmd`) passes each line the process
12
+ * writes, stdout and stderr alike, through {@link stepOutputLine}, and it is
13
+ * printed at once as `[<phase>] <step> | <line>`;
14
+ * - a step that prints nothing for `heartbeatMs` (an activity that only
15
+ * returns a value, or a command that is quiet) gets
16
+ * `[<phase>] <step>: still running after <n>s` each time that much passes.
17
+ *
18
+ * `<step>` is the step's id when it has one, else its call as the run's
19
+ * render names it, `fn(arg=value, ...)`, cut to a readable length. Without a
20
+ * writer there is no console, and `shellCmd` prints a command's stderr when it
21
+ * ends, as it did before.
22
+ */
23
+ import { AsyncLocalStorage } from "node:async_hooks";
24
+ import { parseDuration } from "./duration.js";
25
+ /** How long a step may be quiet before the runner says it is still running. */
26
+ export const DEFAULT_STEP_HEARTBEAT_MS = 60000;
27
+ /** The environment variable that sets the interval: a duration ("30s", "5m"), or `0` / `off`. */
28
+ export const STEP_HEARTBEAT_ENV = "CHANT_STEP_HEARTBEAT";
29
+ /** The longest step label a line carries before it is cut. */
30
+ const MAX_LABEL = 60;
31
+ const store = new AsyncLocalStorage();
32
+ const runStore = new AsyncLocalStorage();
33
+ /** Run `fn` as a run whose steps print through `run`. Without one, steps get no console. */
34
+ export function withRunConsole(run, fn) {
35
+ return run ? runStore.run(run, fn) : fn();
36
+ }
37
+ function formatArgs(args) {
38
+ return Object.entries(args)
39
+ .map(([k, v]) => `${k}=${typeof v === "string" ? v : JSON.stringify(v)}`)
40
+ .join(", ");
41
+ }
42
+ /** The name a step's lines carry: its id, else `fn(args)` on one line, cut to {@link MAX_LABEL}. */
43
+ export function stepLabel(fn, args, id) {
44
+ if (id)
45
+ return id;
46
+ const call = `${fn}(${formatArgs(args)})`.replace(/\s+/g, " ");
47
+ return call.length > MAX_LABEL ? `${call.slice(0, MAX_LABEL - 4)}...)` : call;
48
+ }
49
+ /**
50
+ * Run `fn` with a console for one step, when the run has one
51
+ * ({@link withRunConsole}). The heartbeat timer is stopped when `fn`
52
+ * settles; it never holds the process open on its own.
53
+ */
54
+ export async function withStepConsole(opts, fn) {
55
+ const run = runStore.getStore();
56
+ if (!run)
57
+ return fn();
58
+ const { write } = run;
59
+ const prefix = `[${opts.phase}] ${opts.label}`;
60
+ const started = Date.now();
61
+ let spoke = false;
62
+ const sink = {
63
+ line(text) {
64
+ spoke = true;
65
+ write(`${prefix} | ${text}`);
66
+ },
67
+ };
68
+ const every = run.heartbeatMs ?? DEFAULT_STEP_HEARTBEAT_MS;
69
+ const timer = every > 0
70
+ ? setInterval(() => {
71
+ if (spoke) {
72
+ spoke = false;
73
+ return;
74
+ }
75
+ write(`${prefix}: still running after ${Math.round((Date.now() - started) / 1000)}s`);
76
+ }, every)
77
+ : undefined;
78
+ timer?.unref();
79
+ try {
80
+ return await store.run(sink, fn);
81
+ }
82
+ finally {
83
+ if (timer)
84
+ clearInterval(timer);
85
+ }
86
+ }
87
+ /** Whether the calling activity runs under a step console, so its output is printed as it arrives. */
88
+ export function hasStepConsole() {
89
+ return store.getStore() !== undefined;
90
+ }
91
+ /** Print one line of the calling step's output. A no-op outside a step console. */
92
+ export function stepOutputLine(text) {
93
+ store.getStore()?.line(text);
94
+ }
95
+ /**
96
+ * A sink for a stream's chunks that prints whole lines to the calling step's
97
+ * console as they complete; `end()` prints what is left. The console is the
98
+ * one current when the splitter is made, since a stream's `data` events need
99
+ * not run in the step's async context.
100
+ */
101
+ export function stepLineSplitter() {
102
+ const sink = store.getStore();
103
+ let pending = "";
104
+ const emit = (line) => sink?.line(line.endsWith("\r") ? line.slice(0, -1) : line);
105
+ return {
106
+ push(chunk) {
107
+ pending += typeof chunk === "string" ? chunk : chunk.toString("utf8");
108
+ let at = pending.indexOf("\n");
109
+ while (at >= 0) {
110
+ emit(pending.slice(0, at));
111
+ pending = pending.slice(at + 1);
112
+ at = pending.indexOf("\n");
113
+ }
114
+ },
115
+ end() {
116
+ if (pending !== "")
117
+ emit(pending);
118
+ pending = "";
119
+ },
120
+ };
121
+ }
122
+ /**
123
+ * The heartbeat interval `CHANT_STEP_HEARTBEAT` sets, or the default. `0` and
124
+ * `off` turn it off. A value that is not a duration falls back to the default
125
+ * and says so through `warn`.
126
+ */
127
+ export function stepHeartbeatMs(env = process.env, warn) {
128
+ const raw = env[STEP_HEARTBEAT_ENV]?.trim();
129
+ if (!raw)
130
+ return DEFAULT_STEP_HEARTBEAT_MS;
131
+ if (raw === "0" || raw.toLowerCase() === "off")
132
+ return 0;
133
+ try {
134
+ return parseDuration(raw);
135
+ }
136
+ catch {
137
+ warn?.(`${STEP_HEARTBEAT_ENV}="${raw}" is not a duration (such as 30s or 5m) or off; using ${DEFAULT_STEP_HEARTBEAT_MS / 1000}s`);
138
+ return DEFAULT_STEP_HEARTBEAT_MS;
139
+ }
140
+ }
@@ -23,15 +23,20 @@
23
23
  * Scope, kept deliberately tight (see the issue's "Open questions"):
24
24
  * - same-Op only — no cross-Op plumbing (that needs a durable place to put
25
25
  * the value, which #1290 explicitly defers).
26
- * - main `phases` only — a step inside `onFailure` or nested inside an
27
- * `EffectStep` can neither produce nor consume a step-output reference.
28
- * `onFailure` compensation runs only when something upstream already
29
- * failed, so a main-phase step's captured result isn't reliably
30
- * available there; an effect step's nested steps run only when the
31
- * receipt comparison mismatches, so a value it produces isn't reliably
32
- * available to anything outside it either. Both are conditional
33
- * execution paths, which is exactly the class of thing #1290 keeps out
34
- * of an Op's inert-data model.
26
+ * - producers are the top-level activity steps of the main `phases`. A step
27
+ * in `onFailure` or nested inside an `EffectStep` produces nothing another
28
+ * step may reference: an effect's nested steps run only when the receipt
29
+ * comparison mismatches, so a value one of them returns isn't reliably
30
+ * there for anything else (#3948 decision A7, "for now").
31
+ * - consumers are those producers, plus (#3948) every activity step in
32
+ * `onFailure` and every activity step nested inside an effect. An
33
+ * `onFailure` step may reference any main-phase producer. Compensation
34
+ * runs after a failure, so its producer may never have completed; the
35
+ * executor then skips the compensation step and records why (decision
36
+ * A6), rather than calling it with an `undefined` in place of the value.
37
+ * A step nested inside an effect may reference a producer that runs
38
+ * before the effect, and the executor folds the resolved values into the
39
+ * effect's expectation, so a changed value re-fires the effect.
35
40
  * - producer must run to completion before the consumer starts: an earlier
36
41
  * phase, or an earlier step within the same non-parallel phase. Two
37
42
  * steps in the same parallel phase run concurrently (`Promise.all`) with
@@ -176,22 +181,25 @@ export declare function collectStepOutputRefs(value: unknown): StepOutputRef[];
176
181
  * a public export a caller can invoke directly, bypassing that lint pass —
177
182
  * this is its own defense-in-depth.
178
183
  *
179
- * Flags:
180
- * - a reference authored inside `onFailure`, or inside an `EffectStep`'s
181
- * nested steps — out of scope by design (see the module doc); flagged
182
- * explicitly here rather than left to fall through as "unknown producer",
184
+ * Consumers are the main-phase activity steps, the steps nested inside an
185
+ * effect, and every `onFailure` activity step (#3948). Flags:
186
+ * - a reference to a step nested inside an effect step, from anywhere
187
+ * (#3948 decision A7) — flagged explicitly here rather than left to fall
188
+ * through as "unknown producer",
183
189
  * - a reference to an unregistered step id (no step in scope declares it),
184
190
  * - a duplicate step id (ambiguous producer),
185
191
  * - a reference to a step that does not precede the referencing step
186
192
  * (a later phase, a later step in the same phase, itself, or a step in
187
- * the same parallel phase).
193
+ * the same parallel phase). A step nested inside an effect takes the
194
+ * effect's position, so it may reference only a step that runs before
195
+ * the effect. An `onFailure` step comes after every main phase.
188
196
  */
189
197
  export declare function validateStepOutputRefScope(config: Pick<OpConfig, "name" | "phases" | "onFailure"> & Partial<Pick<OpConfig, "workLease">>): ActivityContractIssue[];
190
198
  /**
191
- * Validate every step-output reference in an Op's main phases against a
192
- * contract map and the Op's own step ordering. Scope: `config.phases` only
193
- * (never `onFailure`, never a step nested inside an `EffectStep` — see the
194
- * module doc). Layers contract-based checks on top of {@link
199
+ * Validate every step-output reference in an Op against a contract map and
200
+ * the Op's own step ordering, over the consumers {@link
201
+ * validateStepOutputRefScope} covers (see the module doc). Layers
202
+ * contract-based checks on top of {@link
195
203
  * validateStepOutputRefScope}'s scope/ordering checks.
196
204
  *
197
205
  * Flags everything {@link validateStepOutputRefScope} does, plus:
@@ -1 +1 @@
1
- {"version":3,"file":"step-output-ref.d.ts","sourceRoot":"","sources":["../../src/op/step-output-ref.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqFG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAmB,MAAM,SAAS,CAAC;AAEvE,OAAO,EAAqD,KAAK,gBAAgB,EAAE,KAAK,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAE3I,QAAA,MAAM,qBAAqB,eAAuC,CAAC;AAEnE;;;;;;GAMG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,CAAC,qBAAqB,CAAC,EAAE,IAAI,CAAC;IACvC,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAC;IACjC,+CAA+C;IAC/C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;;;;;GAYG;AACH,KAAK,SAAS,CAAC,CAAC,IACZ,aAAa,GACb,CAAC,CAAC,SAAS,QAAQ,GAAG,CAAC,GACrB,CAAC,SAAS,SAAS,CAAC,MAAM,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,SAAS,OAAO,EAAE,GAAG,SAAS,CAAC,CAAC,CAAC,EAAE,GAAG,SAAS,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,GACjG,CAAC,SAAS,MAAM,GAAG;KAAG,CAAC,IAAI,MAAM,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CAAE,GACtD,CAAC,CAAC,CAAC;AAET;;;;;;;;;GASG;AACH,MAAM,MAAM,YAAY,CAAC,CAAC,IAAI;KAAG,CAAC,IAAI,MAAM,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CAAE,CAAC;AAElE,8FAA8F;AAC9F,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,aAAa,CAEtE;AAqBD;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC,YAAY,EAAE,IAAI,CAAC,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,aAAa,CAQhG;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAU1E;AAED,8GAA8G;AAC9G,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG,aAAa,EAAE,CAKrE;AAmED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,QAAQ,GAAG,WAAW,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC,GAC7F,qBAAqB,EAAE,CAwEzB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,QAAQ,GAAG,WAAW,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC,EAC9F,SAAS,EAAE,WAAW,CAAC,MAAM,EAAE,gBAAgB,CAAC,GAC/C,qBAAqB,EAAE,CA4EzB"}
1
+ {"version":3,"file":"step-output-ref.d.ts","sourceRoot":"","sources":["../../src/op/step-output-ref.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0FG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAmB,MAAM,SAAS,CAAC;AAEvE,OAAO,EAAqD,KAAK,gBAAgB,EAAE,KAAK,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAE3I,QAAA,MAAM,qBAAqB,eAAuC,CAAC;AAEnE;;;;;;GAMG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,CAAC,qBAAqB,CAAC,EAAE,IAAI,CAAC;IACvC,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAC;IACjC,+CAA+C;IAC/C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;;;;;GAYG;AACH,KAAK,SAAS,CAAC,CAAC,IACZ,aAAa,GACb,CAAC,CAAC,SAAS,QAAQ,GAAG,CAAC,GACrB,CAAC,SAAS,SAAS,CAAC,MAAM,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,SAAS,OAAO,EAAE,GAAG,SAAS,CAAC,CAAC,CAAC,EAAE,GAAG,SAAS,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,GACjG,CAAC,SAAS,MAAM,GAAG;KAAG,CAAC,IAAI,MAAM,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CAAE,GACtD,CAAC,CAAC,CAAC;AAET;;;;;;;;;GASG;AACH,MAAM,MAAM,YAAY,CAAC,CAAC,IAAI;KAAG,CAAC,IAAI,MAAM,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CAAE,CAAC;AAElE,8FAA8F;AAC9F,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,aAAa,CAEtE;AAqBD;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC,YAAY,EAAE,IAAI,CAAC,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,aAAa,CAQhG;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAU1E;AAED,8GAA8G;AAC9G,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG,aAAa,EAAE,CAKrE;AAoGD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,QAAQ,GAAG,WAAW,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC,GAC7F,qBAAqB,EAAE,CAqDzB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,QAAQ,GAAG,WAAW,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC,EAC9F,SAAS,EAAE,WAAW,CAAC,MAAM,EAAE,gBAAgB,CAAC,GAC/C,qBAAqB,EAAE,CA0EzB"}