@galaxy-tool-util/schema 1.2.0 → 1.6.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/dist/index.d.ts +32 -2
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +31 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/schema/parsed-tool.d.ts +3 -0
  6. package/dist/schema/parsed-tool.d.ts.map +1 -1
  7. package/dist/user-tool-parse/index.d.ts +12 -0
  8. package/dist/user-tool-parse/index.d.ts.map +1 -0
  9. package/dist/user-tool-parse/index.js +12 -0
  10. package/dist/user-tool-parse/index.js.map +1 -0
  11. package/dist/user-tool-parse/inputs.d.ts +13 -0
  12. package/dist/user-tool-parse/inputs.d.ts.map +1 -0
  13. package/dist/user-tool-parse/inputs.js +565 -0
  14. package/dist/user-tool-parse/inputs.js.map +1 -0
  15. package/dist/user-tool-parse/outputs.d.ts +10 -0
  16. package/dist/user-tool-parse/outputs.d.ts.map +1 -0
  17. package/dist/user-tool-parse/outputs.js +231 -0
  18. package/dist/user-tool-parse/outputs.js.map +1 -0
  19. package/dist/user-tool-parse/parse.d.ts +12 -0
  20. package/dist/user-tool-parse/parse.d.ts.map +1 -0
  21. package/dist/user-tool-parse/parse.js +98 -0
  22. package/dist/user-tool-parse/parse.js.map +1 -0
  23. package/dist/user-tool-source/index.d.ts +3 -0
  24. package/dist/user-tool-source/index.d.ts.map +1 -0
  25. package/dist/user-tool-source/index.js +3 -0
  26. package/dist/user-tool-source/index.js.map +1 -0
  27. package/dist/user-tool-source/schema.generated.d.ts +7298 -0
  28. package/dist/user-tool-source/schema.generated.d.ts.map +1 -0
  29. package/dist/user-tool-source/schema.generated.js +8851 -0
  30. package/dist/user-tool-source/schema.generated.js.map +1 -0
  31. package/dist/user-tool-source/semantic.d.ts +14 -0
  32. package/dist/user-tool-source/semantic.d.ts.map +1 -0
  33. package/dist/user-tool-source/semantic.js +137 -0
  34. package/dist/user-tool-source/semantic.js.map +1 -0
  35. package/dist/user-tool-source/validate.d.ts +37 -0
  36. package/dist/user-tool-source/validate.d.ts.map +1 -0
  37. package/dist/user-tool-source/validate.js +54 -0
  38. package/dist/user-tool-source/validate.js.map +1 -0
  39. package/dist/workflow/draft-checks.d.ts +205 -0
  40. package/dist/workflow/draft-checks.d.ts.map +1 -0
  41. package/dist/workflow/draft-checks.js +1151 -0
  42. package/dist/workflow/draft-checks.js.map +1 -0
  43. package/dist/workflow/index.d.ts +4 -2
  44. package/dist/workflow/index.d.ts.map +1 -1
  45. package/dist/workflow/index.js +4 -2
  46. package/dist/workflow/index.js.map +1 -1
  47. package/dist/workflow/normalized/format2.d.ts +1 -0
  48. package/dist/workflow/normalized/format2.d.ts.map +1 -1
  49. package/dist/workflow/normalized/format2.js +15 -2
  50. package/dist/workflow/normalized/format2.js.map +1 -1
  51. package/dist/workflow/normalized/native.d.ts +1 -1
  52. package/dist/workflow/normalized/toNative.js +29 -4
  53. package/dist/workflow/normalized/toNative.js.map +1 -1
  54. package/dist/workflow/promote-draft.d.ts +53 -0
  55. package/dist/workflow/promote-draft.d.ts.map +1 -0
  56. package/dist/workflow/promote-draft.js +133 -0
  57. package/dist/workflow/promote-draft.js.map +1 -0
  58. package/dist/workflow/raw/gxformat2-draft.d.ts +688 -0
  59. package/dist/workflow/raw/gxformat2-draft.d.ts.map +1 -0
  60. package/dist/workflow/raw/gxformat2-draft.effect.d.ts +4607 -0
  61. package/dist/workflow/raw/gxformat2-draft.effect.d.ts.map +1 -0
  62. package/dist/workflow/raw/gxformat2-draft.effect.js +778 -0
  63. package/dist/workflow/raw/gxformat2-draft.effect.js.map +1 -0
  64. package/dist/workflow/raw/gxformat2-draft.js +20 -0
  65. package/dist/workflow/raw/gxformat2-draft.js.map +1 -0
  66. package/dist/workflow/raw/gxformat2.d.ts +237 -13
  67. package/dist/workflow/raw/gxformat2.d.ts.map +1 -1
  68. package/dist/workflow/raw/gxformat2.effect.d.ts +1375 -177
  69. package/dist/workflow/raw/gxformat2.effect.d.ts.map +1 -1
  70. package/dist/workflow/raw/gxformat2.effect.js +355 -108
  71. package/dist/workflow/raw/gxformat2.effect.js.map +1 -1
  72. package/dist/workflow/raw/gxformat2.js +18 -1
  73. package/dist/workflow/raw/gxformat2.js.map +1 -1
  74. package/dist/workflow/raw/index.d.ts +2 -0
  75. package/dist/workflow/raw/index.d.ts.map +1 -1
  76. package/dist/workflow/raw/index.js +1 -0
  77. package/dist/workflow/raw/index.js.map +1 -1
  78. package/dist/workflow/raw/native.d.ts +51 -2
  79. package/dist/workflow/raw/native.d.ts.map +1 -1
  80. package/dist/workflow/raw/native.effect.d.ts +82 -30
  81. package/dist/workflow/raw/native.effect.d.ts.map +1 -1
  82. package/dist/workflow/raw/native.effect.js +67 -18
  83. package/dist/workflow/raw/native.effect.js.map +1 -1
  84. package/dist/workflow/raw/native.js.map +1 -1
  85. package/dist/workflow/report-models.d.ts +102 -0
  86. package/dist/workflow/report-models.d.ts.map +1 -1
  87. package/dist/workflow/report-models.js +98 -0
  88. package/dist/workflow/report-models.js.map +1 -1
  89. package/dist/workflow/semantic-validators.d.ts +18 -0
  90. package/dist/workflow/semantic-validators.d.ts.map +1 -0
  91. package/dist/workflow/semantic-validators.js +137 -0
  92. package/dist/workflow/semantic-validators.js.map +1 -0
  93. package/dist/workflow/validators.d.ts.map +1 -1
  94. package/dist/workflow/validators.js +7 -2
  95. package/dist/workflow/validators.js.map +1 -1
  96. package/package.json +1 -1
@@ -0,0 +1,1151 @@
1
+ /**
2
+ * Pure logic for Galaxy draft workflows (`class: GalaxyWorkflowDraft`).
3
+ *
4
+ * Substrate for the `gxwf draft-validate`, `gxwf draft-next-step`, and
5
+ * `gxwf draft-extract` CLI commands. No I/O.
6
+ *
7
+ * The sentinel constants mirror `gxformat2/draft.py`. Drift is enforced by
8
+ * `make check-sync-draft-sentinel` against the JSON snapshot in
9
+ * `schema-sources/v19_09/draft_constants.json`.
10
+ */
11
+ import { Either, ParseResult, Schema } from "effect";
12
+ import { GalaxyWorkflowDraftSchema } from "./raw/gxformat2-draft.effect.js";
13
+ export const TODO_SENTINEL_PATTERN = /^TODO(_[a-zA-Z0-9_]+)?$/;
14
+ // Heuristic for TODO-shaped strings — flags malformed sentinels (TODO-foo,
15
+ // TODOfoo, TODO_) that the canonical pattern misses. Mixed-case suffixes
16
+ // (TODO_Trimmed, TODO_TRIMMED) are valid sentinels.
17
+ // Anchored: only TODO followed by `_`, `-`, or end-of-string counts; this
18
+ // avoids false positives on unrelated identifiers that happen to start with
19
+ // TODO (e.g. TODOLIST, TODONE).
20
+ const TODO_LIKE = /^TODO([_-]|$)/;
21
+ export const PLAN_FIELDS = ["_plan_state", "_plan_context", "_plan_in", "_plan_out"];
22
+ export const DRAFT_CLASS = "GalaxyWorkflowDraft";
23
+ export function isTodoSentinel(value) {
24
+ return typeof value === "string" && TODO_SENTINEL_PATTERN.test(value);
25
+ }
26
+ export function isDraftWorkflow(doc) {
27
+ return isRecord(doc) && doc.class === DRAFT_CLASS;
28
+ }
29
+ /**
30
+ * Walk a draft workflow once, returning every TODO sentinel and `_plan_*`
31
+ * field with its step path. Non-draft documents return `{ isDraft: false }`
32
+ * with empty arrays.
33
+ *
34
+ * Step paths are `[outerLabel, ..., innerStepLabel]`. Subworkflows recurse
35
+ * only when the inner `run:` block also carries `class: GalaxyWorkflowDraft`.
36
+ *
37
+ * Note: this survey is step-focused — it intentionally does NOT report
38
+ * top-level `_plan_*` fields on a draft workflow root. Those are flagged
39
+ * by `validateDraft` (a rules-focused walker); `detectDraft` exists to
40
+ * drive `nextDraftStep` / `extractConcreteSubset`, which only act on
41
+ * per-step state.
42
+ */
43
+ export function detectDraft(workflow) {
44
+ const todos = [];
45
+ const planFields = [];
46
+ if (!isRecord(workflow) || workflow.class !== DRAFT_CLASS) {
47
+ return { isDraft: false, todos, planFields };
48
+ }
49
+ walkDraftSteps(workflow, [], todos, planFields);
50
+ return { isDraft: true, todos, planFields };
51
+ }
52
+ function walkDraftSteps(workflow, prefix, todos, planFields) {
53
+ for (const [label, step] of iterateSteps(workflow.steps)) {
54
+ if (!isRecord(step))
55
+ continue;
56
+ const path = [...prefix, label];
57
+ collectStepTodos(step, path, todos);
58
+ collectStepPlanFields(step, path, planFields);
59
+ const inner = step.run;
60
+ if (isDraftWorkflow(inner)) {
61
+ walkDraftSteps(inner, path, todos, planFields);
62
+ }
63
+ }
64
+ // Workflow-level outputs: outputSource may contain a TODO port reference.
65
+ // For inner draft subworkflows (recursive call), `prefix` is the path to
66
+ // reach this workflow (i.e. the outer step's path); inner outputs collect
67
+ // at that prefix. For the outermost call `prefix` is [], so outer outputs
68
+ // collect at path [].
69
+ for (const [label, output] of iterateOutputs(workflow.outputs)) {
70
+ const ref = readOutputSource(output);
71
+ if (ref == null)
72
+ continue;
73
+ const [, port] = splitSourceRef(ref);
74
+ if (port != null && isTodoSentinel(port)) {
75
+ todos.push({
76
+ path: prefix,
77
+ location: { kind: "output_source", output_label: label, port },
78
+ sentinel: port,
79
+ });
80
+ }
81
+ }
82
+ }
83
+ function collectStepTodos(step, path, todos) {
84
+ if (isTodoSentinel(step.tool_id)) {
85
+ todos.push({ path, location: { kind: "tool_id" }, sentinel: step.tool_id });
86
+ }
87
+ if (isTodoSentinel(step.tool_version)) {
88
+ todos.push({
89
+ path,
90
+ location: { kind: "tool_version" },
91
+ sentinel: step.tool_version,
92
+ });
93
+ }
94
+ // Use the shared iterator so dict-form (`in: { TODO_x: ... }`) and list-form
95
+ // (`in: [{ id: TODO_x, source: ... }]`) draft inputs are covered uniformly.
96
+ for (const [key] of iterateStepInputEntries(step.in)) {
97
+ if (isTodoSentinel(key)) {
98
+ todos.push({ path, location: { kind: "in_key", key }, sentinel: key });
99
+ }
100
+ }
101
+ for (const id of iterateStepOutIds(step.out)) {
102
+ if (isTodoSentinel(id)) {
103
+ todos.push({ path, location: { kind: "out_id", id }, sentinel: id });
104
+ }
105
+ }
106
+ }
107
+ function collectStepPlanFields(step, path, planFields) {
108
+ for (const field of PLAN_FIELDS) {
109
+ const value = step[field];
110
+ if (typeof value === "string" && value.length > 0) {
111
+ planFields.push({ path, field, value });
112
+ }
113
+ }
114
+ }
115
+ function* iterateSteps(steps) {
116
+ if (Array.isArray(steps)) {
117
+ for (const step of steps) {
118
+ if (isRecord(step)) {
119
+ const label = typeof step.label === "string" ? step.label : typeof step.id === "string" ? step.id : "";
120
+ yield [label, step];
121
+ }
122
+ }
123
+ }
124
+ else if (isRecord(steps)) {
125
+ for (const [key, value] of Object.entries(steps)) {
126
+ yield [key, value];
127
+ }
128
+ }
129
+ }
130
+ function* iterateOutputs(outputs) {
131
+ if (Array.isArray(outputs)) {
132
+ for (const out of outputs) {
133
+ if (isRecord(out)) {
134
+ const label = typeof out.id === "string" ? out.id : "";
135
+ yield [label, out];
136
+ }
137
+ }
138
+ }
139
+ else if (isRecord(outputs)) {
140
+ for (const [key, value] of Object.entries(outputs)) {
141
+ yield [key, value];
142
+ }
143
+ }
144
+ }
145
+ function readOutputSource(output) {
146
+ if (typeof output === "string")
147
+ return output;
148
+ if (!isRecord(output))
149
+ return null;
150
+ const src = output.outputSource ?? output.source;
151
+ return typeof src === "string" ? src : null;
152
+ }
153
+ function splitSourceRef(ref) {
154
+ const slash = ref.indexOf("/");
155
+ if (slash < 0)
156
+ return [ref, null];
157
+ return [ref.slice(0, slash), ref.slice(slash + 1)];
158
+ }
159
+ function* iterateStepOutIds(out) {
160
+ if (Array.isArray(out)) {
161
+ for (const entry of out) {
162
+ if (typeof entry === "string") {
163
+ yield entry;
164
+ }
165
+ else if (isRecord(entry) && typeof entry.id === "string") {
166
+ yield entry.id;
167
+ }
168
+ }
169
+ }
170
+ else if (isRecord(out)) {
171
+ for (const key of Object.keys(out)) {
172
+ yield key;
173
+ }
174
+ }
175
+ }
176
+ function isRecord(value) {
177
+ return typeof value === "object" && value !== null && !Array.isArray(value);
178
+ }
179
+ /**
180
+ * Validate a draft Galaxy workflow. Collects all diagnostics — does not throw.
181
+ *
182
+ * structureErrors Effect Schema decode failures.
183
+ * topologyErrors Concrete-topology must hold even in drafts: workflow
184
+ * input labels, output labels, step labels, and step types
185
+ * cannot be TODO sentinels. Step/port edge refs must
186
+ * resolve to a declared step + declared port (TODO_*
187
+ * ports count if declared in the step's `out:`).
188
+ * semanticErrors TODO-shaped strings (start with `TODO`) that don't
189
+ * match the canonical sentinel form (e.g. `TODO-foo`,
190
+ * `TODOfoo`, `TODO_` trailing).
191
+ * warnings Bare `TODO` in port position; top-level `_plan_*`.
192
+ *
193
+ * The returned `survey` is the same shape detectDraft() returns and is
194
+ * usable by downstream nextDraftStep / extractConcreteSubset callers.
195
+ */
196
+ export function validateDraft(workflow) {
197
+ const structureErrors = [];
198
+ const topologyErrors = [];
199
+ const semanticErrors = [];
200
+ const warnings = [];
201
+ if (!isRecord(workflow) || workflow.class !== DRAFT_CLASS) {
202
+ structureErrors.push({
203
+ path: [],
204
+ message: `not a draft workflow (class must be "${DRAFT_CLASS}")`,
205
+ });
206
+ return {
207
+ ok: false,
208
+ structureErrors,
209
+ topologyErrors,
210
+ semanticErrors,
211
+ warnings,
212
+ survey: { isDraft: false, todos: [], planFields: [] },
213
+ };
214
+ }
215
+ const decode = Schema.decodeUnknownEither(GalaxyWorkflowDraftSchema, {
216
+ onExcessProperty: "ignore",
217
+ });
218
+ const decodeResult = decode(workflow);
219
+ if (Either.isLeft(decodeResult)) {
220
+ structureErrors.push({
221
+ path: [],
222
+ message: ParseResult.TreeFormatter.formatErrorSync(decodeResult.left),
223
+ });
224
+ }
225
+ const survey = detectDraft(workflow);
226
+ walkDraftValidation(workflow, [], topologyErrors, semanticErrors, warnings);
227
+ for (const todo of survey.todos) {
228
+ if (todo.sentinel === "TODO" &&
229
+ todo.location.kind !== "tool_id" &&
230
+ todo.location.kind !== "tool_version") {
231
+ warnings.push({
232
+ path: todo.path,
233
+ message: `bare \`TODO\` in port position (location: ${formatTodoLocation(todo.location)}); prefer \`TODO_<hint>\``,
234
+ });
235
+ }
236
+ }
237
+ const ok = structureErrors.length === 0 && topologyErrors.length === 0 && semanticErrors.length === 0;
238
+ return { ok, structureErrors, topologyErrors, semanticErrors, warnings, survey };
239
+ }
240
+ function walkDraftValidation(workflow, prefix, topologyErrors, semanticErrors, warnings) {
241
+ // Plan fields belong on individual steps, never at a draft root — applies
242
+ // to the outer document AND any nested draft subworkflow root reached via
243
+ // recursion below.
244
+ for (const field of PLAN_FIELDS) {
245
+ const value = workflow[field];
246
+ if (typeof value === "string" && value.length > 0) {
247
+ warnings.push({
248
+ path: prefix,
249
+ message: `top-level \`${field}\` is not part of the draft contract; planning fields belong on individual steps`,
250
+ });
251
+ }
252
+ }
253
+ // Workflow inputs: labels and types must be concrete.
254
+ const inputLabels = new Set();
255
+ for (const [label, input] of iterateMapOrArrayLabeled(workflow.inputs)) {
256
+ inputLabels.add(label);
257
+ if (isTodoSentinel(label)) {
258
+ topologyErrors.push({
259
+ path: prefix,
260
+ message: `workflow input label cannot be a TODO sentinel: "${label}"`,
261
+ });
262
+ }
263
+ else {
264
+ checkTodoLike(label, prefix, `workflow input label "${label}"`, semanticErrors);
265
+ }
266
+ if (isRecord(input) && isTodoSentinel(input.type)) {
267
+ topologyErrors.push({
268
+ path: prefix,
269
+ message: `workflow input "${label}" type cannot be a TODO sentinel`,
270
+ });
271
+ }
272
+ }
273
+ // Workflow outputs: labels must be concrete; outputSource resolves.
274
+ const stepIndex = buildStepIndex(workflow.steps);
275
+ for (const [label, output] of iterateMapOrArrayLabeled(workflow.outputs)) {
276
+ if (isTodoSentinel(label)) {
277
+ topologyErrors.push({
278
+ path: prefix,
279
+ message: `workflow output label cannot be a TODO sentinel: "${label}"`,
280
+ });
281
+ }
282
+ else {
283
+ checkTodoLike(label, prefix, `workflow output label "${label}"`, semanticErrors);
284
+ }
285
+ const ref = readOutputSource(output);
286
+ if (ref != null) {
287
+ checkEdgeRef(ref, prefix, `workflow output "${label}"`, stepIndex, inputLabels, topologyErrors, semanticErrors);
288
+ }
289
+ }
290
+ // Steps: labels, types, edge refs, sentinel-form on every TODO-shaped string.
291
+ for (const [label, step] of iterateSteps(workflow.steps)) {
292
+ if (!isRecord(step))
293
+ continue;
294
+ if (isTodoSentinel(label)) {
295
+ topologyErrors.push({
296
+ path: prefix,
297
+ message: `step label cannot be a TODO sentinel: "${label}"`,
298
+ });
299
+ }
300
+ else {
301
+ checkTodoLike(label, prefix, `step label "${label}"`, semanticErrors);
302
+ }
303
+ const stepPath = [...prefix, label];
304
+ if (isTodoSentinel(step.type)) {
305
+ topologyErrors.push({
306
+ path: stepPath,
307
+ message: `step type cannot be a TODO sentinel`,
308
+ });
309
+ }
310
+ // Sentinel form on TODO-shaped strings the schema would otherwise pass through.
311
+ if (typeof step.tool_id === "string") {
312
+ checkTodoLike(step.tool_id, stepPath, `tool_id "${step.tool_id}"`, semanticErrors);
313
+ }
314
+ if (typeof step.tool_version === "string") {
315
+ checkTodoLike(step.tool_version, stepPath, `tool_version "${step.tool_version}"`, semanticErrors);
316
+ }
317
+ if (isRecord(step.in)) {
318
+ for (const key of Object.keys(step.in)) {
319
+ checkTodoLike(key, stepPath, `in: key "${key}"`, semanticErrors);
320
+ }
321
+ }
322
+ for (const id of iterateStepOutIds(step.out)) {
323
+ checkTodoLike(id, stepPath, `out: id "${id}"`, semanticErrors);
324
+ }
325
+ // Step input edges.
326
+ for (const [inKey, inValue] of iterateStepInputEntries(step.in)) {
327
+ for (const ref of extractSourceRefs(inValue)) {
328
+ checkEdgeRef(ref, stepPath, `step input "${inKey}"`, stepIndex, inputLabels, topologyErrors, semanticErrors);
329
+ }
330
+ }
331
+ // _plan_* on a fully-resolved tool step is a contract violation: planning
332
+ // context must be cleaned up before the step is treated as ready. Non-tool
333
+ // steps (subworkflow / pause / pick_value) are allowed in v1 — the locked
334
+ // decision keeps modeling simple until usage shows we need to tighten.
335
+ if (isToolStep(step) && !stepHasAnyTodo(step)) {
336
+ const planFieldsPresent = PLAN_FIELDS.filter((f) => typeof step[f] === "string" && step[f].length > 0);
337
+ if (planFieldsPresent.length > 0) {
338
+ semanticErrors.push({
339
+ path: stepPath,
340
+ message: `tool step has no TODO sentinels but still carries planning fields (${planFieldsPresent.join(", ")}); strip planning state before treating the step as resolved`,
341
+ });
342
+ }
343
+ }
344
+ // Recurse into draft subworkflows.
345
+ if (isRecord(step.run) && step.run.class === DRAFT_CLASS) {
346
+ walkDraftValidation(step.run, stepPath, topologyErrors, semanticErrors, warnings);
347
+ }
348
+ }
349
+ }
350
+ function stepHasAnyTodo(step) {
351
+ if (isTodoSentinel(step.tool_id))
352
+ return true;
353
+ if (isTodoSentinel(step.tool_version))
354
+ return true;
355
+ for (const [key] of iterateStepInputEntries(step.in)) {
356
+ if (isTodoSentinel(key))
357
+ return true;
358
+ }
359
+ for (const id of iterateStepOutIds(step.out)) {
360
+ if (isTodoSentinel(id))
361
+ return true;
362
+ }
363
+ return false;
364
+ }
365
+ function isToolStep(step) {
366
+ return step.type == null || step.type === "tool";
367
+ }
368
+ function checkTodoLike(value, path, context, semanticErrors) {
369
+ if (TODO_LIKE.test(value) && !TODO_SENTINEL_PATTERN.test(value)) {
370
+ semanticErrors.push({
371
+ path,
372
+ message: `${context} is TODO-shaped but malformed (must match ${TODO_SENTINEL_PATTERN.source})`,
373
+ });
374
+ }
375
+ }
376
+ function buildStepIndex(steps) {
377
+ const index = new Map();
378
+ for (const [label, step] of iterateSteps(steps)) {
379
+ if (!isRecord(step))
380
+ continue;
381
+ const outPorts = new Set(iterateStepOutIds(step.out));
382
+ index.set(label, { outPorts });
383
+ }
384
+ return index;
385
+ }
386
+ function checkEdgeRef(ref, path, context, stepIndex, inputLabels, topologyErrors, semanticErrors) {
387
+ checkTodoLike(ref, path, `${context} source "${ref}"`, semanticErrors);
388
+ const slash = ref.indexOf("/");
389
+ if (slash < 0) {
390
+ if (!inputLabels.has(ref)) {
391
+ topologyErrors.push({
392
+ path,
393
+ message: `${context} source "${ref}" does not match any declared workflow input`,
394
+ });
395
+ }
396
+ return;
397
+ }
398
+ const stepLabel = ref.slice(0, slash);
399
+ const port = ref.slice(slash + 1);
400
+ const entry = stepIndex.get(stepLabel);
401
+ if (entry == null) {
402
+ topologyErrors.push({
403
+ path,
404
+ message: `${context} source "${ref}" references unknown step "${stepLabel}"`,
405
+ });
406
+ return;
407
+ }
408
+ if (!entry.outPorts.has(port)) {
409
+ topologyErrors.push({
410
+ path,
411
+ message: `${context} source "${ref}" references unknown port "${port}" on step "${stepLabel}"`,
412
+ });
413
+ }
414
+ }
415
+ function* iterateStepInputEntries(input) {
416
+ if (Array.isArray(input)) {
417
+ for (const entry of input) {
418
+ if (isRecord(entry) && typeof entry.id === "string") {
419
+ yield [entry.id, entry];
420
+ }
421
+ }
422
+ }
423
+ else if (isRecord(input)) {
424
+ for (const [key, value] of Object.entries(input)) {
425
+ yield [key, value];
426
+ }
427
+ }
428
+ }
429
+ function extractSourceRefs(value) {
430
+ if (typeof value === "string")
431
+ return [value];
432
+ if (Array.isArray(value)) {
433
+ return value.flatMap(extractSourceRefs);
434
+ }
435
+ if (isRecord(value)) {
436
+ if (value.source != null)
437
+ return extractSourceRefs(value.source);
438
+ // Step inputs without a source: are not an edge (e.g. `default:` only entries).
439
+ return [];
440
+ }
441
+ return [];
442
+ }
443
+ function iterateMapOrArrayLabeled(value) {
444
+ return iterateOutputs(value);
445
+ }
446
+ function formatTodoLocation(loc) {
447
+ switch (loc.kind) {
448
+ case "tool_id":
449
+ case "tool_version":
450
+ return loc.kind;
451
+ case "in_key":
452
+ return `in.${loc.key}`;
453
+ case "out_id":
454
+ return `out.${loc.id}`;
455
+ case "output_source":
456
+ return `outputs.${loc.output_label} (port ${loc.port})`;
457
+ }
458
+ }
459
+ /**
460
+ * Pick the next step a downstream agent should work on. Pure function;
461
+ * idempotent — same input → same output, byte-for-byte.
462
+ *
463
+ * Walks steps in topological order (steps with no draft-step source deps
464
+ * first), tie-breaking by alphabetical step label. The first step that
465
+ * carries any TODO sentinel or `_plan_*` field is returned with a
466
+ * prompt-shaped `work[]` array. Subworkflows (draft inner `run:`) are
467
+ * descended only after their outer step is itself fully concrete.
468
+ *
469
+ * Returns `{ draft: false }` when the workflow has no remaining work
470
+ * (this includes non-draft documents — there is nothing to do).
471
+ */
472
+ export function nextDraftStep(workflow) {
473
+ if (!isRecord(workflow) || workflow.class !== DRAFT_CLASS) {
474
+ return { draft: false };
475
+ }
476
+ return nextDraftStepIn(workflow, []);
477
+ }
478
+ function nextDraftStepIn(workflow, prefix) {
479
+ const ordered = topoOrderedSteps(workflow.steps);
480
+ const outputRefs = collectOutputRefs(workflow.outputs);
481
+ for (const [label, step] of ordered) {
482
+ if (!isRecord(step))
483
+ continue;
484
+ const stepPath = [...prefix, label];
485
+ const work = stepWorkItems(step, label, outputRefs);
486
+ if (work.length > 0) {
487
+ return { draft: true, step: stepPath, work };
488
+ }
489
+ // Step is fully concrete + carries no _plan_*. If it's a draft
490
+ // subworkflow with work inside, drill in.
491
+ if (isRecord(step.run) && step.run.class === DRAFT_CLASS) {
492
+ const inner = nextDraftStepIn(step.run, stepPath);
493
+ if (inner.draft)
494
+ return inner;
495
+ }
496
+ }
497
+ return { draft: false };
498
+ }
499
+ /**
500
+ * Topological order of steps in this workflow. Tie-break by label
501
+ * alphabetically. Steps that depend on a missing or non-step source
502
+ * (e.g. workflow input refs) are level 0.
503
+ *
504
+ * Returns `[label, step]` tuples preserving the iteration shape that
505
+ * iterateSteps would have yielded.
506
+ */
507
+ function topoOrderedSteps(steps) {
508
+ const entries = [];
509
+ const stepLabels = new Set();
510
+ for (const entry of iterateSteps(steps)) {
511
+ entries.push(entry);
512
+ stepLabels.add(entry[0]);
513
+ }
514
+ // step label -> in-degree count (only counting deps on other declared steps)
515
+ const inDegree = new Map();
516
+ const deps = new Map();
517
+ for (const [label, step] of entries) {
518
+ if (!isRecord(step)) {
519
+ inDegree.set(label, 0);
520
+ deps.set(label, new Set());
521
+ continue;
522
+ }
523
+ const seen = new Set();
524
+ for (const [, inValue] of iterateStepInputEntries(step.in)) {
525
+ for (const ref of extractSourceRefs(inValue)) {
526
+ const slash = ref.indexOf("/");
527
+ if (slash < 0)
528
+ continue;
529
+ const depLabel = ref.slice(0, slash);
530
+ if (depLabel !== label && stepLabels.has(depLabel)) {
531
+ seen.add(depLabel);
532
+ }
533
+ }
534
+ }
535
+ inDegree.set(label, seen.size);
536
+ deps.set(label, seen);
537
+ }
538
+ // Stable: process levels by alphabetical label order.
539
+ const ordered = [];
540
+ const byLabel = new Map(entries);
541
+ const remaining = new Set(entries.map(([l]) => l));
542
+ while (remaining.size > 0) {
543
+ const ready = [...remaining].filter((l) => (inDegree.get(l) ?? 0) === 0).sort();
544
+ if (ready.length === 0) {
545
+ // Cycle (or unresolvable refs). Drain the rest in alphabetical order
546
+ // so the function stays total — validateDraft is responsible for
547
+ // surfacing cycle/dangling-edge errors separately.
548
+ for (const label of [...remaining].sort()) {
549
+ ordered.push([label, byLabel.get(label)]);
550
+ }
551
+ break;
552
+ }
553
+ for (const label of ready) {
554
+ ordered.push([label, byLabel.get(label)]);
555
+ remaining.delete(label);
556
+ for (const [other, otherDeps] of deps) {
557
+ if (otherDeps.has(label)) {
558
+ otherDeps.delete(label);
559
+ inDegree.set(other, (inDegree.get(other) ?? 0) - 1);
560
+ }
561
+ }
562
+ }
563
+ }
564
+ return ordered;
565
+ }
566
+ function collectOutputRefs(outputs) {
567
+ const byStepPort = new Map();
568
+ for (const [label, output] of iterateOutputs(outputs)) {
569
+ const ref = readOutputSource(output);
570
+ if (ref == null)
571
+ continue;
572
+ const [stepLabel, port] = splitSourceRef(ref);
573
+ if (port == null)
574
+ continue;
575
+ let perPort = byStepPort.get(stepLabel);
576
+ if (perPort == null) {
577
+ perPort = new Map();
578
+ byStepPort.set(stepLabel, perPort);
579
+ }
580
+ let set = perPort.get(port);
581
+ if (set == null) {
582
+ set = new Set();
583
+ perPort.set(port, set);
584
+ }
585
+ set.add(label);
586
+ }
587
+ return { byStepPort };
588
+ }
589
+ function stepWorkItems(step, stepLabel, outputRefs) {
590
+ const work = [];
591
+ if (isTodoSentinel(step.tool_id)) {
592
+ work.push("TODO[tool_id]: pick a Galaxy Tool Shed wrapper for this step");
593
+ }
594
+ if (isTodoSentinel(step.tool_version)) {
595
+ work.push("TODO[tool_version]: pick the wrapper version");
596
+ }
597
+ if (isRecord(step.in)) {
598
+ for (const key of Object.keys(step.in)) {
599
+ if (!isTodoSentinel(key))
600
+ continue;
601
+ const hint = sentinelHint(key);
602
+ const hintFragment = hint != null ? ` (semantic hint: '${hint}')` : "";
603
+ work.push(`TODO[in.${key}]: assign the real wrapper input port name${hintFragment}`);
604
+ }
605
+ }
606
+ const stepOutputRefs = outputRefs.byStepPort.get(stepLabel);
607
+ for (const id of iterateStepOutIds(step.out)) {
608
+ if (!isTodoSentinel(id))
609
+ continue;
610
+ const hint = sentinelHint(id);
611
+ const refs = stepOutputRefs?.get(id);
612
+ const parts = [];
613
+ if (hint != null)
614
+ parts.push(`semantic hint: '${hint}'`);
615
+ if (refs != null && refs.size > 0) {
616
+ const labels = [...refs].sort();
617
+ const refStr = labels.map((l) => `'${l}'`).join(", ");
618
+ const word = labels.length === 1 ? "output" : "outputs";
619
+ parts.push(`referenced by workflow ${word} ${refStr}`);
620
+ }
621
+ const hintFragment = parts.length > 0 ? ` (${parts.join("; ")})` : "";
622
+ work.push(`TODO[out.${id}]: assign the real wrapper output port name${hintFragment}`);
623
+ }
624
+ for (const field of PLAN_FIELDS) {
625
+ const value = step[field];
626
+ if (typeof value === "string" && value.length > 0) {
627
+ work.push(`${field}: ${value.trim()}`);
628
+ }
629
+ }
630
+ return work;
631
+ }
632
+ function sentinelHint(sentinel) {
633
+ if (sentinel === "TODO")
634
+ return null;
635
+ return sentinel.slice("TODO_".length);
636
+ }
637
+ /**
638
+ * Trim a draft workflow down to its concrete subset: drop any step that
639
+ * carries a TODO sentinel or `_plan_*` field, then cascade-drop steps whose
640
+ * `in:` becomes dead, then drop workflow outputs whose source went away.
641
+ * Multi-source inputs are rewritten in place to the surviving ref subset.
642
+ *
643
+ * Pure + idempotent. The result always carries `class: GalaxyWorkflowDraft`
644
+ * — promotion to concrete is a CLI-layer decision (E).
645
+ *
646
+ * Non-draft inputs pass through unchanged with empty drop / rewrite arrays.
647
+ */
648
+ export function extractConcreteSubset(workflow) {
649
+ if (!isRecord(workflow) || workflow.class !== DRAFT_CLASS) {
650
+ return {
651
+ workflow,
652
+ dropped_steps: [],
653
+ dropped_outputs: [],
654
+ rewritten_step_inputs: [],
655
+ };
656
+ }
657
+ const result = extractLevel(workflow, []);
658
+ return {
659
+ workflow: result.workflow,
660
+ dropped_steps: result.droppedSteps.map(({ path, reason }) => ({ path, reason })),
661
+ dropped_outputs: result.droppedOutputs,
662
+ rewritten_step_inputs: result.rewrittenStepInputs,
663
+ };
664
+ }
665
+ function extractLevel(workflow, prefix) {
666
+ // Collect step entries in original iteration order. Same shape as
667
+ // iterateSteps so we keep dict-form / list-form distinguishable later.
668
+ const stepEntries = [];
669
+ for (const [label, step] of iterateSteps(workflow.steps)) {
670
+ if (isRecord(step))
671
+ stepEntries.push([label, step]);
672
+ }
673
+ const stepLabels = new Set(stepEntries.map(([l]) => l));
674
+ // Round 0: directly-drafty steps.
675
+ const drops = new Map();
676
+ for (const [label, step] of stepEntries) {
677
+ const reason = directDropReason(step);
678
+ if (reason != null) {
679
+ drops.set(label, { path: [...prefix, label], reason, round: 0 });
680
+ }
681
+ }
682
+ // Recurse into surviving subworkflow steps (inline draft `run:` only).
683
+ // String-form `run:` and concrete (`class: GalaxyWorkflow`) `run:` are
684
+ // opaque — no descent.
685
+ const innerResults = new Map();
686
+ for (const [label, step] of stepEntries) {
687
+ if (drops.has(label))
688
+ continue;
689
+ const innerWf = step.run;
690
+ if (isDraftWorkflow(innerWf)) {
691
+ const sub = extractLevel(innerWf, [...prefix, label]);
692
+ innerResults.set(label, sub);
693
+ }
694
+ }
695
+ // Cascade rounds.
696
+ let round = 1;
697
+ while (true) {
698
+ const livePorts = computeLivePorts(stepEntries, drops, innerResults);
699
+ let changed = false;
700
+ for (const [label, step] of stepEntries) {
701
+ if (drops.has(label))
702
+ continue;
703
+ const cascade = checkStepCascade(step, prefix, drops, livePorts, stepLabels);
704
+ if (cascade != null) {
705
+ drops.set(label, {
706
+ path: [...prefix, label],
707
+ reason: { kind: "cascade", depends_on: cascade.depsOnDropped },
708
+ round,
709
+ });
710
+ changed = true;
711
+ }
712
+ }
713
+ if (!changed)
714
+ break;
715
+ round++;
716
+ }
717
+ // Final live ports after all drops settled.
718
+ const livePorts = computeLivePorts(stepEntries, drops, innerResults);
719
+ // Compute rewrites for surviving steps.
720
+ const rewrittenStepInputs = [];
721
+ for (const [label, step] of stepEntries) {
722
+ if (drops.has(label))
723
+ continue;
724
+ for (const rewrite of computeInputRewrites(step, [...prefix, label], drops, livePorts)) {
725
+ rewrittenStepInputs.push(rewrite);
726
+ }
727
+ }
728
+ // Build the trimmed workflow dict; collect this level's output drops.
729
+ const trimmed = trimWorkflow(workflow, stepEntries, drops, innerResults, livePorts, prefix);
730
+ // Sort this level's step drops: by round, then alphabetical step-path.
731
+ const levelDrops = [...drops.values()];
732
+ levelDrops.sort((a, b) => {
733
+ if (a.round !== b.round)
734
+ return a.round - b.round;
735
+ return pathKey(a.path).localeCompare(pathKey(b.path));
736
+ });
737
+ // Concatenate inner-level results in source iteration order. Each inner
738
+ // level is already sorted by its own recursive call; we don't re-sort
739
+ // across levels (cascade rounds are not comparable across nesting).
740
+ const droppedSteps = [...levelDrops];
741
+ const droppedOutputs = [...trimmed.droppedOutputs];
742
+ for (const [label, inner] of innerResults) {
743
+ if (drops.has(label))
744
+ continue;
745
+ droppedSteps.push(...inner.droppedSteps);
746
+ droppedOutputs.push(...inner.droppedOutputs);
747
+ rewrittenStepInputs.push(...inner.rewrittenStepInputs);
748
+ }
749
+ return {
750
+ workflow: trimmed.workflow,
751
+ droppedSteps,
752
+ droppedOutputs,
753
+ rewrittenStepInputs,
754
+ livePorts,
755
+ };
756
+ }
757
+ /** TODO sentinels first, then plan fields; mirrors the planning ordering. */
758
+ function directDropReason(step) {
759
+ const locations = [];
760
+ if (isTodoSentinel(step.tool_id))
761
+ locations.push({ kind: "tool_id" });
762
+ if (isTodoSentinel(step.tool_version))
763
+ locations.push({ kind: "tool_version" });
764
+ for (const [key] of iterateStepInputEntries(step.in)) {
765
+ if (isTodoSentinel(key))
766
+ locations.push({ kind: "in_key", key });
767
+ }
768
+ for (const id of iterateStepOutIds(step.out)) {
769
+ if (isTodoSentinel(id))
770
+ locations.push({ kind: "out_id", id });
771
+ }
772
+ if (locations.length > 0)
773
+ return { kind: "step_has_todo", locations };
774
+ const fields = [];
775
+ for (const field of PLAN_FIELDS) {
776
+ const value = step[field];
777
+ if (typeof value === "string" && value.length > 0)
778
+ fields.push(field);
779
+ }
780
+ if (fields.length > 0)
781
+ return { kind: "step_has_plan_field", fields };
782
+ return null;
783
+ }
784
+ /**
785
+ * For each surviving step, compute its currently-live output ports:
786
+ * - Non-subworkflow steps: all declared `out:` ids.
787
+ * - Inline-draft subworkflow steps: keys of the recursed inner workflow's
788
+ * surviving outputs (so outer refs that referenced now-dropped inner
789
+ * outputs evaluate as dead).
790
+ * - String-form `run:` / concrete-class `run:`: opaque; all declared
791
+ * `out:` ids are live.
792
+ */
793
+ function computeLivePorts(stepEntries, drops, innerResults) {
794
+ const livePorts = new Map();
795
+ for (const [label, step] of stepEntries) {
796
+ if (drops.has(label)) {
797
+ livePorts.set(label, new Set());
798
+ continue;
799
+ }
800
+ const inner = innerResults.get(label);
801
+ if (inner != null) {
802
+ const surviving = new Set();
803
+ for (const [outLabel] of iterateOutputs(inner.workflow.outputs)) {
804
+ surviving.add(outLabel);
805
+ }
806
+ livePorts.set(label, surviving);
807
+ }
808
+ else {
809
+ livePorts.set(label, new Set(iterateStepOutIds(step.out)));
810
+ }
811
+ }
812
+ return livePorts;
813
+ }
814
+ function checkStepCascade(step, prefix, drops, livePorts, stepLabels) {
815
+ const depsOnDropped = new Map();
816
+ let cascade = false;
817
+ for (const [, inValue] of iterateStepInputEntries(step.in)) {
818
+ const refs = extractSourceRefs(inValue);
819
+ if (refs.length === 0)
820
+ continue;
821
+ let anyAlive = false;
822
+ const localDeps = [];
823
+ for (const ref of refs) {
824
+ const [refStep, refPort] = splitSourceRef(ref);
825
+ if (refPort == null) {
826
+ // Workflow input ref — always alive (workflow inputs are preserved verbatim).
827
+ anyAlive = true;
828
+ continue;
829
+ }
830
+ if (!stepLabels.has(refStep)) {
831
+ // Dangling ref — validateDraft owns the diagnostic; treat as alive
832
+ // here so extract doesn't double-fail on the same issue.
833
+ anyAlive = true;
834
+ continue;
835
+ }
836
+ const drop = drops.get(refStep);
837
+ if (drop != null) {
838
+ localDeps.push([refStep, drop.path]);
839
+ continue;
840
+ }
841
+ const ports = livePorts.get(refStep);
842
+ if (ports == null || !ports.has(refPort)) {
843
+ // Step survives but the port doesn't (inner-workflow shrink).
844
+ localDeps.push([refStep, [...prefix, refStep]]);
845
+ continue;
846
+ }
847
+ anyAlive = true;
848
+ }
849
+ if (!anyAlive) {
850
+ if (inputHasDefault(inValue))
851
+ continue; // default fallback — not dead
852
+ cascade = true;
853
+ for (const [key, path] of localDeps) {
854
+ if (!depsOnDropped.has(key))
855
+ depsOnDropped.set(key, path);
856
+ }
857
+ }
858
+ }
859
+ if (!cascade)
860
+ return null;
861
+ const paths = [...depsOnDropped.values()].sort((a, b) => pathKey(a).localeCompare(pathKey(b)));
862
+ return { depsOnDropped: paths };
863
+ }
864
+ function inputHasDefault(value) {
865
+ if (isRecord(value) && "default" in value)
866
+ return true;
867
+ return false;
868
+ }
869
+ function readInputShape(value) {
870
+ if (typeof value === "string") {
871
+ return { refs: [value], hasDefault: false, carrier: { kind: "string" } };
872
+ }
873
+ if (Array.isArray(value)) {
874
+ // Bare list of refs / sub-objects: `in: { x: ["a/out", "b/out"] }` or
875
+ // `in: { x: [{ source: "a/out" }, "b/out"] }`. No defaults at this level.
876
+ const refs = [];
877
+ for (const entry of value) {
878
+ if (typeof entry === "string")
879
+ refs.push(entry);
880
+ else if (isRecord(entry) && typeof entry.source === "string")
881
+ refs.push(entry.source);
882
+ else if (isRecord(entry) && Array.isArray(entry.source)) {
883
+ for (const s of entry.source)
884
+ if (typeof s === "string")
885
+ refs.push(s);
886
+ }
887
+ }
888
+ return { refs, hasDefault: false, carrier: { kind: "list_of_refs" } };
889
+ }
890
+ if (isRecord(value)) {
891
+ const hasDefault = "default" in value;
892
+ const src = value.source;
893
+ if (typeof src === "string") {
894
+ return {
895
+ refs: [src],
896
+ hasDefault,
897
+ carrier: { kind: "object_source_string", original: value },
898
+ };
899
+ }
900
+ if (Array.isArray(src)) {
901
+ const refs = src.filter((s) => typeof s === "string");
902
+ return {
903
+ refs,
904
+ hasDefault,
905
+ carrier: { kind: "object_source_list", original: value },
906
+ };
907
+ }
908
+ return { refs: [], hasDefault, carrier: { kind: "object_no_source", original: value } };
909
+ }
910
+ return null;
911
+ }
912
+ function computeInputRewrites(step, stepPath, drops, livePorts) {
913
+ const out = [];
914
+ for (const [inKey, value] of iterateStepInputEntries(step.in)) {
915
+ const shape = readInputShape(value);
916
+ if (shape == null || shape.refs.length === 0)
917
+ continue;
918
+ const removed = [];
919
+ const surviving = [];
920
+ for (const ref of shape.refs) {
921
+ if (refIsDead(ref, drops, livePorts))
922
+ removed.push(ref);
923
+ else
924
+ surviving.push(ref);
925
+ }
926
+ if (removed.length === 0)
927
+ continue;
928
+ out.push({ path: stepPath, in_key: inKey, removed_refs: removed, surviving_refs: surviving });
929
+ }
930
+ return out;
931
+ }
932
+ function refIsDead(ref, drops, livePorts) {
933
+ const [refStep, refPort] = splitSourceRef(ref);
934
+ if (refPort == null)
935
+ return false;
936
+ if (drops.has(refStep))
937
+ return true;
938
+ const ports = livePorts.get(refStep);
939
+ // Unknown step → defer to validateDraft; treat as live here.
940
+ if (ports == null)
941
+ return false;
942
+ return !ports.has(refPort);
943
+ }
944
+ function trimWorkflow(workflow, stepEntries, drops, innerResults, livePorts, prefix) {
945
+ // Iterate steps in source order; replace dropped + inner-shrunk inline; preserve list/dict shape.
946
+ const trimmedSteps = trimStepsContainer(workflow.steps, drops, innerResults, livePorts);
947
+ const trimmedOutputsResult = trimOutputsContainer(workflow.outputs, drops, livePorts, prefix);
948
+ // Build the result dict — preserve key iteration order from the source.
949
+ const trimmed = {};
950
+ for (const [k, v] of Object.entries(workflow)) {
951
+ if (k === "steps")
952
+ trimmed[k] = trimmedSteps;
953
+ else if (k === "outputs")
954
+ trimmed[k] = trimmedOutputsResult.outputs;
955
+ else
956
+ trimmed[k] = v;
957
+ }
958
+ // If the source had no `outputs:` key originally, don't synthesize one.
959
+ // (Object.entries above handles that — the key only appears if it existed.)
960
+ // Always carry the draft class — the caller may flip later.
961
+ trimmed.class = DRAFT_CLASS;
962
+ return { workflow: trimmed, droppedOutputs: trimmedOutputsResult.droppedOutputs };
963
+ }
964
+ function trimStepsContainer(steps, drops, innerResults, livePorts) {
965
+ if (Array.isArray(steps)) {
966
+ const out = [];
967
+ for (const entry of steps) {
968
+ if (!isRecord(entry))
969
+ continue;
970
+ const label = typeof entry.label === "string"
971
+ ? entry.label
972
+ : typeof entry.id === "string"
973
+ ? entry.id
974
+ : "";
975
+ if (drops.has(label))
976
+ continue;
977
+ out.push(trimStep(entry, label, drops, innerResults, livePorts));
978
+ }
979
+ return out;
980
+ }
981
+ if (isRecord(steps)) {
982
+ const out = {};
983
+ for (const [label, step] of Object.entries(steps)) {
984
+ if (drops.has(label))
985
+ continue;
986
+ if (!isRecord(step)) {
987
+ out[label] = step;
988
+ continue;
989
+ }
990
+ out[label] = trimStep(step, label, drops, innerResults, livePorts);
991
+ }
992
+ return out;
993
+ }
994
+ return steps;
995
+ }
996
+ function trimStep(step, label, drops, innerResults, livePorts) {
997
+ const trimmed = {};
998
+ for (const [k, v] of Object.entries(step)) {
999
+ if (k === "in") {
1000
+ trimmed[k] = rewriteStepIn(v, drops, livePorts);
1001
+ }
1002
+ else if (k === "run" && innerResults.has(label)) {
1003
+ trimmed[k] = innerResults.get(label).workflow;
1004
+ }
1005
+ else {
1006
+ trimmed[k] = v;
1007
+ }
1008
+ }
1009
+ return trimmed;
1010
+ }
1011
+ function rewriteStepIn(input, drops, livePorts) {
1012
+ if (Array.isArray(input)) {
1013
+ return input.map((entry) => {
1014
+ if (!isRecord(entry))
1015
+ return entry;
1016
+ return rewriteStepInputEntry(entry, drops, livePorts);
1017
+ });
1018
+ }
1019
+ if (isRecord(input)) {
1020
+ const out = {};
1021
+ for (const [key, value] of Object.entries(input)) {
1022
+ out[key] = rewriteStepInputValue(value, drops, livePorts);
1023
+ }
1024
+ return out;
1025
+ }
1026
+ return input;
1027
+ }
1028
+ function rewriteStepInputEntry(entry, drops, livePorts) {
1029
+ const out = {};
1030
+ for (const [k, v] of Object.entries(entry)) {
1031
+ if (k === "source") {
1032
+ const rewritten = rewriteSourceField(v, drops, livePorts);
1033
+ if (rewritten === DROP_SOURCE_KEY)
1034
+ continue;
1035
+ out[k] = rewritten;
1036
+ }
1037
+ else {
1038
+ out[k] = v;
1039
+ }
1040
+ }
1041
+ return out;
1042
+ }
1043
+ function rewriteStepInputValue(value, drops, livePorts) {
1044
+ const shape = readInputShape(value);
1045
+ if (shape == null)
1046
+ return value;
1047
+ if (shape.refs.length === 0)
1048
+ return value;
1049
+ const surviving = shape.refs.filter((r) => !refIsDead(r, drops, livePorts));
1050
+ if (surviving.length === shape.refs.length)
1051
+ return value;
1052
+ switch (shape.carrier.kind) {
1053
+ case "string": {
1054
+ // Single-ref string form. If it survives, value unchanged.
1055
+ // If dead and dict-style with default isn't applicable (string form has no default),
1056
+ // the consuming step would have cascaded — we shouldn't be here.
1057
+ // If we are here with 0 survivors and no default, return value untouched
1058
+ // (defensive — should be unreachable).
1059
+ return surviving.length > 0 ? surviving[0] : value;
1060
+ }
1061
+ case "object_source_string":
1062
+ case "object_source_list": {
1063
+ const original = shape.carrier.original;
1064
+ if (surviving.length === 0) {
1065
+ // default-fallback: drop source key, keep default + the rest.
1066
+ const out = {};
1067
+ for (const [k, v] of Object.entries(original)) {
1068
+ if (k !== "source")
1069
+ out[k] = v;
1070
+ }
1071
+ return out;
1072
+ }
1073
+ const out = {};
1074
+ for (const [k, v] of Object.entries(original)) {
1075
+ if (k === "source")
1076
+ out[k] = surviving.length === 1 ? surviving[0] : surviving;
1077
+ else
1078
+ out[k] = v;
1079
+ }
1080
+ return out;
1081
+ }
1082
+ case "list_of_refs": {
1083
+ if (surviving.length === 0)
1084
+ return value; // unreachable; would've cascaded
1085
+ if (surviving.length === 1)
1086
+ return surviving[0];
1087
+ return surviving;
1088
+ }
1089
+ case "object_no_source":
1090
+ return value;
1091
+ }
1092
+ }
1093
+ const DROP_SOURCE_KEY = Symbol("drop");
1094
+ function rewriteSourceField(source, drops, livePorts) {
1095
+ if (typeof source === "string") {
1096
+ return refIsDead(source, drops, livePorts) ? DROP_SOURCE_KEY : source;
1097
+ }
1098
+ if (Array.isArray(source)) {
1099
+ const surviving = source.filter((r) => typeof r === "string" && !refIsDead(r, drops, livePorts));
1100
+ if (surviving.length === 0)
1101
+ return DROP_SOURCE_KEY;
1102
+ if (surviving.length === 1)
1103
+ return surviving[0];
1104
+ return surviving;
1105
+ }
1106
+ return source;
1107
+ }
1108
+ function trimOutputsContainer(outputs, drops, livePorts, prefix) {
1109
+ const dropped = [];
1110
+ const keep = (label, value) => {
1111
+ const ref = readOutputSource(value);
1112
+ if (ref == null)
1113
+ return true;
1114
+ if (!refIsDead(ref, drops, livePorts))
1115
+ return true;
1116
+ const [refStep] = splitSourceRef(ref);
1117
+ const drop = drops.get(refStep);
1118
+ const reason = drop != null
1119
+ ? { kind: "cascade", depends_on: [drop.path] }
1120
+ : { kind: "cascade", depends_on: [[...prefix, refStep]] };
1121
+ dropped.push({ path: prefix, label, reason });
1122
+ return false;
1123
+ };
1124
+ let trimmed;
1125
+ if (Array.isArray(outputs)) {
1126
+ const out = [];
1127
+ for (const entry of outputs) {
1128
+ const label = isRecord(entry) && typeof entry.id === "string" ? entry.id : "";
1129
+ if (keep(label, entry))
1130
+ out.push(entry);
1131
+ }
1132
+ trimmed = out;
1133
+ }
1134
+ else if (isRecord(outputs)) {
1135
+ const out = {};
1136
+ for (const [label, value] of Object.entries(outputs)) {
1137
+ if (keep(label, value))
1138
+ out[label] = value;
1139
+ }
1140
+ trimmed = out;
1141
+ }
1142
+ else {
1143
+ trimmed = outputs;
1144
+ }
1145
+ dropped.sort((a, b) => a.label.localeCompare(b.label));
1146
+ return { outputs: trimmed, droppedOutputs: dropped };
1147
+ }
1148
+ function pathKey(path) {
1149
+ return path.join("/");
1150
+ }
1151
+ //# sourceMappingURL=draft-checks.js.map