@scenario-labs/sdk 3.1.0 → 3.2.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 (129) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/client.d.mts +2 -2
  3. package/client.d.mts.map +1 -1
  4. package/client.d.ts +2 -2
  5. package/client.d.ts.map +1 -1
  6. package/client.js.map +1 -1
  7. package/client.mjs.map +1 -1
  8. package/lib/workflow_converter.d.mts +6 -0
  9. package/lib/workflow_converter.d.mts.map +1 -1
  10. package/lib/workflow_converter.d.ts +6 -0
  11. package/lib/workflow_converter.d.ts.map +1 -1
  12. package/lib/workflow_converter.js +99 -12
  13. package/lib/workflow_converter.js.map +1 -1
  14. package/lib/workflow_converter.mjs +99 -12
  15. package/lib/workflow_converter.mjs.map +1 -1
  16. package/lib/workflow_validator.d.mts +3 -1
  17. package/lib/workflow_validator.d.mts.map +1 -1
  18. package/lib/workflow_validator.d.ts +3 -1
  19. package/lib/workflow_validator.d.ts.map +1 -1
  20. package/lib/workflow_validator.js +41 -1
  21. package/lib/workflow_validator.js.map +1 -1
  22. package/lib/workflow_validator.mjs +41 -1
  23. package/lib/workflow_validator.mjs.map +1 -1
  24. package/package.json +1 -1
  25. package/resources/assets/assets.d.mts +182 -99
  26. package/resources/assets/assets.d.mts.map +1 -1
  27. package/resources/assets/assets.d.ts +182 -99
  28. package/resources/assets/assets.d.ts.map +1 -1
  29. package/resources/assets/assets.js.map +1 -1
  30. package/resources/assets/assets.mjs.map +1 -1
  31. package/resources/assets/download.d.mts +15 -0
  32. package/resources/assets/download.d.mts.map +1 -1
  33. package/resources/assets/download.d.ts +15 -0
  34. package/resources/assets/download.d.ts.map +1 -1
  35. package/resources/assets/download.js +2 -2
  36. package/resources/assets/download.js.map +1 -1
  37. package/resources/assets/download.mjs +2 -2
  38. package/resources/assets/download.mjs.map +1 -1
  39. package/resources/generate.d.mts +379 -221
  40. package/resources/generate.d.mts.map +1 -1
  41. package/resources/generate.d.ts +379 -221
  42. package/resources/generate.d.ts.map +1 -1
  43. package/resources/generate.js +2 -6
  44. package/resources/generate.js.map +1 -1
  45. package/resources/generate.mjs +2 -6
  46. package/resources/generate.mjs.map +1 -1
  47. package/resources/index.d.mts +1 -1
  48. package/resources/index.d.mts.map +1 -1
  49. package/resources/index.d.ts +1 -1
  50. package/resources/index.d.ts.map +1 -1
  51. package/resources/index.js.map +1 -1
  52. package/resources/index.mjs.map +1 -1
  53. package/resources/jobs.d.mts +77 -46
  54. package/resources/jobs.d.mts.map +1 -1
  55. package/resources/jobs.d.ts +77 -46
  56. package/resources/jobs.d.ts.map +1 -1
  57. package/resources/jobs.js +2 -6
  58. package/resources/jobs.js.map +1 -1
  59. package/resources/jobs.mjs +2 -6
  60. package/resources/jobs.mjs.map +1 -1
  61. package/resources/models/description.d.mts +18 -0
  62. package/resources/models/description.d.mts.map +1 -1
  63. package/resources/models/description.d.ts +18 -0
  64. package/resources/models/description.d.ts.map +1 -1
  65. package/resources/models/examples.d.mts +80 -46
  66. package/resources/models/examples.d.mts.map +1 -1
  67. package/resources/models/examples.d.ts +80 -46
  68. package/resources/models/examples.d.ts.map +1 -1
  69. package/resources/models/models.d.mts +131 -35
  70. package/resources/models/models.d.mts.map +1 -1
  71. package/resources/models/models.d.ts +131 -35
  72. package/resources/models/models.d.ts.map +1 -1
  73. package/resources/models/models.js +6 -2
  74. package/resources/models/models.js.map +1 -1
  75. package/resources/models/models.mjs +6 -2
  76. package/resources/models/models.mjs.map +1 -1
  77. package/resources/models/train.d.mts +78 -38
  78. package/resources/models/train.d.mts.map +1 -1
  79. package/resources/models/train.d.ts +78 -38
  80. package/resources/models/train.d.ts.map +1 -1
  81. package/resources/models/train.js +2 -2
  82. package/resources/models/train.js.map +1 -1
  83. package/resources/models/train.mjs +2 -2
  84. package/resources/models/train.mjs.map +1 -1
  85. package/resources/search.d.mts +168 -5
  86. package/resources/search.d.mts.map +1 -1
  87. package/resources/search.d.ts +168 -5
  88. package/resources/search.d.ts.map +1 -1
  89. package/resources/search.js +15 -2
  90. package/resources/search.js.map +1 -1
  91. package/resources/search.mjs +15 -2
  92. package/resources/search.mjs.map +1 -1
  93. package/resources/uploads.d.mts +13 -3
  94. package/resources/uploads.d.mts.map +1 -1
  95. package/resources/uploads.d.ts +13 -3
  96. package/resources/uploads.d.ts.map +1 -1
  97. package/resources/uploads.js +2 -2
  98. package/resources/uploads.js.map +1 -1
  99. package/resources/uploads.mjs +2 -2
  100. package/resources/uploads.mjs.map +1 -1
  101. package/resources/usages.d.mts +34 -27
  102. package/resources/usages.d.mts.map +1 -1
  103. package/resources/usages.d.ts +34 -27
  104. package/resources/usages.d.ts.map +1 -1
  105. package/resources/workflows.d.mts +283 -130
  106. package/resources/workflows.d.mts.map +1 -1
  107. package/resources/workflows.d.ts +283 -130
  108. package/resources/workflows.d.ts.map +1 -1
  109. package/src/client.ts +6 -0
  110. package/src/lib/workflow_converter.ts +126 -12
  111. package/src/lib/workflow_validator.ts +68 -1
  112. package/src/resources/assets/assets.ts +574 -99
  113. package/src/resources/assets/download.ts +19 -2
  114. package/src/resources/generate.ts +495 -236
  115. package/src/resources/index.ts +3 -0
  116. package/src/resources/jobs.ts +79 -53
  117. package/src/resources/models/description.ts +116 -0
  118. package/src/resources/models/examples.ts +178 -46
  119. package/src/resources/models/models.ts +138 -37
  120. package/src/resources/models/train.ts +82 -42
  121. package/src/resources/search.ts +218 -7
  122. package/src/resources/uploads.ts +16 -5
  123. package/src/resources/usages.ts +40 -27
  124. package/src/resources/workflows.ts +283 -130
  125. package/src/version.ts +1 -1
  126. package/version.d.mts +1 -1
  127. package/version.d.ts +1 -1
  128. package/version.js +1 -1
  129. package/version.mjs +1 -1
@@ -55,9 +55,11 @@ export type WorkflowEditorNode =
55
55
  data: NodeCommonData & {
56
56
  from?: number | string;
57
57
  count?: number | string;
58
+ mode?: 'default' | 'randomize' | 'choose';
58
59
  isRandomize?: boolean;
59
60
  randomSeed?: string;
60
61
  isSeedLocked?: boolean;
62
+ message?: string;
61
63
  value?: string;
62
64
  };
63
65
  }
@@ -134,6 +136,11 @@ type ModelInputNodeData = NodeCommonData & {
134
136
 
135
137
  const RUNNABLE_TYPES = new Set(['model', 'llm', 'forEach', 'forEachEnd']);
136
138
  const MODEL_CUSTOM_INPUT_HANDLE_SUFFIX = '-source-custom';
139
+ const USER_SELECTION_MIN_SELECT = 1;
140
+ const USER_SELECTION_MAX_SELECT = 100;
141
+ const DEFAULT_SLICE_ASSETS_CHOOSE_MESSAGE = 'Select the items to continue';
142
+
143
+ type SliceAssetsMode = 'default' | 'randomize' | 'choose';
137
144
 
138
145
  export const ASPECT_RATIO_PRESETS = [
139
146
  '21:9',
@@ -176,6 +183,10 @@ export type WorkflowEditorFlowInput = {
176
183
  ref?: WorkflowEditorFlowInputRef | undefined;
177
184
  value?: string | number | string[] | boolean | undefined;
178
185
  items?: WorkflowEditorFlowInput[][] | undefined;
186
+ /** Inclusive minimum for number/array inputs. For user-selection, items the user must pick. */
187
+ min?: number | undefined;
188
+ /** Inclusive maximum for number/array inputs. For user-selection, items the user may pick. */
189
+ max?: number | undefined;
179
190
  };
180
191
 
181
192
  export type WorkflowEditorFlowItem = {
@@ -215,12 +226,17 @@ function nodeIdToFlowId(nodeId: string): string {
215
226
  return nodeId.replace(/\s/g, '');
216
227
  }
217
228
 
229
+ /** CEL identifiers cannot contain hyphens (`loop2-end` parses as subtraction). */
230
+ function toCelIdentifier(name: string): string {
231
+ return name.replaceAll('-', '_');
232
+ }
233
+
218
234
  function escapeCELString(value: string): string {
219
235
  return value.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\n/g, '\\n');
220
236
  }
221
237
 
222
238
  function formatTextAsCELString(text: string): string {
223
- return `'${text.replace(/'/g, "\\'").replace(/\n/g, '\\n')}'`;
239
+ return `'${escapeCELString(text)}'`;
224
240
  }
225
241
 
226
242
  function buildConcatArrayTransform(variables: ConcatVariable[]): string {
@@ -566,13 +582,33 @@ function hasHandleConnection(
566
582
  }
567
583
 
568
584
  function getOutputHandleVarName(targetNodeId: string, handleName: string): string {
569
- return ['output', 'allOutputs'].includes(handleName) ? targetNodeId : `${targetNodeId}_${handleName}`;
585
+ return toCelIdentifier(
586
+ ['output', 'allOutputs'].includes(handleName) ? targetNodeId : `${targetNodeId}_${handleName}`,
587
+ );
570
588
  }
571
589
 
572
590
  function getIsSliceAssetsItemsSourceHandle(nodeId: string, sourceHandle: string | null | undefined): boolean {
573
591
  return sourceHandle === `${nodeId}-source-items` || sourceHandle === `${nodeId}-source-assets`;
574
592
  }
575
593
 
594
+ /**
595
+ * Prompt Builder (transformText) binds upstream vars for CEL. Reuse the same
596
+ * list-producer rules as concat/slice (`groupItems`, `sliceAssets`, `allOutputs`,
597
+ * split-text, LLM "All Results", `isArray` handles): those nodes always emit CEL
598
+ * lists (via concat/slice/randomSample), so Prompt Builder must bind them as
599
+ * `string_array` for indexing. Scalar text (plain text nodes, forEach outputs)
600
+ * stays `string`.
601
+ */
602
+ function getPromptBuilderInputType(
603
+ targetNode: WorkflowEditorNode,
604
+ outHandle: WorkflowEditorHandleOutput | undefined,
605
+ outHandleName: string,
606
+ ): 'string' | 'string_array' {
607
+ return getConcatListSourceDescriptor(targetNode, outHandle, outHandleName).inputType === 'string_array' ?
608
+ 'string_array'
609
+ : 'string';
610
+ }
611
+
576
612
  function getConcatListSourceDescriptor(
577
613
  targetNode: WorkflowEditorNode,
578
614
  outHandle: WorkflowEditorHandleOutput | undefined,
@@ -607,7 +643,7 @@ function getConcatListSourceDescriptor(
607
643
 
608
644
  /**
609
645
  * Builds the concat variables and flow inputs for a list-concatenating node
610
- * (groupItems, sliceAssets in randomize mode) from its incoming edges.
646
+ * (groupItems, sliceAssets in randomize/choose mode) from its incoming edges.
611
647
  * `getVarName` maps a target node id + output handle name to the variable name,
612
648
  * which differs between callers.
613
649
  */
@@ -1568,7 +1604,7 @@ export function convertWorkflowEditorToFlow(
1568
1604
  flow.push({
1569
1605
  id: flowId,
1570
1606
  type: 'transform',
1571
- logic: { transform: `'${escapeCELString(value)}'` },
1607
+ logic: { transform: formatTextAsCELString(value) },
1572
1608
  inputs: flowInputs.length > 0 ? flowInputs : undefined,
1573
1609
  });
1574
1610
 
@@ -1601,7 +1637,7 @@ export function convertWorkflowEditorToFlow(
1601
1637
  flow.push({
1602
1638
  id: flowId,
1603
1639
  type: 'transform',
1604
- logic: { transform: `'${escapeCELString(value)}'` },
1640
+ logic: { transform: formatTextAsCELString(value) },
1605
1641
  inputs: flowInputs.length > 0 ? flowInputs : undefined,
1606
1642
  });
1607
1643
 
@@ -1622,7 +1658,11 @@ export function convertWorkflowEditorToFlow(
1622
1658
  const outHandle = targetNode.data.outputHandles?.find((h) => h.id === edge.targetHandle);
1623
1659
  const outHandleName = outHandle?.name ?? 'output';
1624
1660
  const targetFlowId = nodeIdToFlowIdMap.get(targetNode.id) ?? targetNode.id;
1625
- flowInputs.push({ name: `${targetFlowId}_${outHandleName}`, type: 'string', ref });
1661
+ flowInputs.push({
1662
+ name: `${targetFlowId}_${outHandleName}`,
1663
+ type: getPromptBuilderInputType(targetNode, outHandle, outHandleName),
1664
+ ref,
1665
+ });
1626
1666
  }
1627
1667
  flowInputs.push(...collectConditionalConnectionInputs(incomingEdges, node.id, ctx));
1628
1668
  flow.push({
@@ -1670,10 +1710,12 @@ export function convertWorkflowEditorToFlow(
1670
1710
  const { variables, flowInputs } = collectConcatVarsAndInputs(
1671
1711
  incomingEdges.filter((e) => e.sourceHandle !== conditionalHandleId),
1672
1712
  ctx,
1673
- (targetNodeId, handleName) => `${targetNodeId}_${handleName}`,
1713
+ (targetNodeId, handleName) => toCelIdentifier(`${targetNodeId}_${handleName}`),
1674
1714
  );
1675
1715
 
1676
- const orderedValues = convertValueToList((node.data as { value?: string }).value ?? '');
1716
+ const orderedValues = convertValueToList((node.data as { value?: string }).value ?? '').map(
1717
+ toCelIdentifier,
1718
+ );
1677
1719
  variables.sort((a, b) => orderedValues.indexOf(a.name) - orderedValues.indexOf(b.name));
1678
1720
  flowInputs.sort((a, b) => orderedValues.indexOf(a.name) - orderedValues.indexOf(b.name));
1679
1721
  flowInputs.push(...collectConditionalConnectionInputs(incomingEdges, node.id, ctx));
@@ -1685,16 +1727,19 @@ export function convertWorkflowEditorToFlow(
1685
1727
  });
1686
1728
 
1687
1729
  // ------------------------------------
1688
- // SliceAssets -> slice(list, from, from+count) or randomSample(list, count, seed)
1730
+ // SliceAssets -> slice / randomSample transform, or user-selection in choose mode
1689
1731
  // ------------------------------------
1690
1732
  } else if (node.type === 'sliceAssets') {
1691
1733
  const sliceData = node.data as {
1692
1734
  from?: number | string;
1693
1735
  count?: number | string;
1736
+ mode?: SliceAssetsMode;
1694
1737
  isRandomize?: boolean;
1695
1738
  randomSeed?: string;
1696
1739
  isSeedLocked?: boolean;
1740
+ message?: string;
1697
1741
  };
1742
+ const mode = getSliceAssetsMode(sliceData);
1698
1743
  const conditionalHandleId = getConditionalInputHandleId(node.id);
1699
1744
  const itemsEdges = incomingEdges.filter(
1700
1745
  (e) =>
@@ -1702,7 +1747,58 @@ export function convertWorkflowEditorToFlow(
1702
1747
  getIsSliceAssetsItemsSourceHandle(node.id, e.sourceHandle),
1703
1748
  );
1704
1749
 
1705
- if (sliceData.isRandomize) {
1750
+ if (mode === 'choose') {
1751
+ const connectionInputs = collectConditionalConnectionInputs(incomingEdges, node.id, ctx);
1752
+
1753
+ // No item sources → unsatisfiable selection; mirror default empty fallback.
1754
+ if (itemsEdges.length === 0) {
1755
+ flow.push({
1756
+ id: flowId,
1757
+ type: 'transform',
1758
+ logic: { transform: '[]' },
1759
+ inputs: connectionInputs.length > 0 ? connectionInputs : undefined,
1760
+ });
1761
+ } else {
1762
+ const { variables, flowInputs } = collectConcatVarsAndInputs(
1763
+ itemsEdges,
1764
+ ctx,
1765
+ getOutputHandleVarName,
1766
+ );
1767
+ const count = clampUserSelectionCount(parseInt(String(sliceData.count ?? ''), 10) || 1);
1768
+ const message = sliceData.message?.trim() || DEFAULT_SLICE_ASSETS_CHOOSE_MESSAGE;
1769
+ const arrayType = getUserSelectionArrayType(flowInputs);
1770
+
1771
+ // Single array-typed source can feed user-selection directly; scalars must be wrapped.
1772
+ if (variables.length === 1 && variables[0]?.isArray && flowInputs[0]?.ref) {
1773
+ flow.push({
1774
+ id: flowId,
1775
+ type: 'user-selection',
1776
+ label: message,
1777
+ inputs: [
1778
+ { name: 'items', type: arrayType, ref: flowInputs[0].ref, min: count, max: count },
1779
+ ...connectionInputs,
1780
+ ],
1781
+ });
1782
+ } else {
1783
+ const concatId = `${flowId}_items`;
1784
+ flow.push({
1785
+ id: concatId,
1786
+ type: 'transform',
1787
+ logic: { transform: buildConcatArrayTransform(variables) },
1788
+ inputs: flowInputs.length > 0 ? flowInputs : undefined,
1789
+ });
1790
+ flow.push({
1791
+ id: flowId,
1792
+ type: 'user-selection',
1793
+ label: message,
1794
+ inputs: [
1795
+ { name: 'items', type: arrayType, ref: { node: concatId }, min: count, max: count },
1796
+ ...connectionInputs,
1797
+ ],
1798
+ });
1799
+ }
1800
+ }
1801
+ } else if (mode === 'randomize') {
1706
1802
  const { variables, flowInputs } = collectConcatVarsAndInputs(itemsEdges, ctx, getOutputHandleVarName);
1707
1803
  flowInputs.push(...collectConditionalConnectionInputs(incomingEdges, node.id, ctx));
1708
1804
 
@@ -1737,7 +1833,7 @@ export function convertWorkflowEditorToFlow(
1737
1833
  from < 0 ?
1738
1834
  `slice(${varName}, (${varName}).size() + ${from}, (${varName}).size() + ${from + count})`
1739
1835
  : `slice(${varName}, ${from}, ${from + count})`;
1740
- const flowInputs: WorkflowEditorFlowInput[] = [
1836
+ const sliceFlowInputs: WorkflowEditorFlowInput[] = [
1741
1837
  { name: varName, type: inputType, ref },
1742
1838
  ...collectConditionalConnectionInputs(incomingEdges, node.id, ctx),
1743
1839
  ];
@@ -1745,7 +1841,7 @@ export function convertWorkflowEditorToFlow(
1745
1841
  id: flowId,
1746
1842
  type: 'transform',
1747
1843
  logic: { transform },
1748
- inputs: flowInputs.length > 0 ? flowInputs : undefined,
1844
+ inputs: sliceFlowInputs.length > 0 ? sliceFlowInputs : undefined,
1749
1845
  });
1750
1846
  }
1751
1847
  }
@@ -1991,3 +2087,21 @@ function applyApprovalsToFlow({
1991
2087
  });
1992
2088
  }
1993
2089
  }
2090
+
2091
+ // ---- SliceAssets / user-selection helpers ----
2092
+
2093
+ function getSliceAssetsMode(data: { mode?: SliceAssetsMode; isRandomize?: boolean }): SliceAssetsMode {
2094
+ if (data.mode === 'choose' || data.mode === 'randomize' || data.mode === 'default') {
2095
+ return data.mode;
2096
+ }
2097
+ return data.isRandomize ? 'randomize' : 'default';
2098
+ }
2099
+
2100
+ function clampUserSelectionCount(count: number): number {
2101
+ return Math.min(USER_SELECTION_MAX_SELECT, Math.max(USER_SELECTION_MIN_SELECT, count));
2102
+ }
2103
+
2104
+ function getUserSelectionArrayType(flowInputs: WorkflowEditorFlowInput[]): 'file_array' | 'string_array' {
2105
+ const hasFile = flowInputs.some((input) => input.type === 'file' || input.type === 'file_array');
2106
+ return hasFile || flowInputs.length === 0 ? 'file_array' : 'string_array';
2107
+ }
@@ -8,9 +8,14 @@ const VALID_NODE_TYPES = new Set([
8
8
  'remove-background',
9
9
  'transform',
10
10
  'user-approval',
11
+ 'user-selection',
11
12
  'workflow',
12
13
  ]);
13
14
 
15
+ const USER_SELECTION_MIN_SELECT = 1;
16
+ const USER_SELECTION_MAX_SELECT = 100;
17
+ const USER_SELECTION_INPUT_TYPES = new Set(['file_array', 'string_array']);
18
+
14
19
  type RawRef = {
15
20
  node?: unknown;
16
21
  conditional?: unknown;
@@ -18,8 +23,11 @@ type RawRef = {
18
23
 
19
24
  type RawInput = {
20
25
  name?: unknown;
26
+ type?: unknown;
21
27
  ref?: RawRef;
22
28
  items?: unknown[][];
29
+ min?: unknown;
30
+ max?: unknown;
23
31
  };
24
32
 
25
33
  type RawNode = {
@@ -47,7 +55,9 @@ type RawNode = {
47
55
  * - custom-model → modelId
48
56
  * - workflow → workflowId
49
57
  * - logic → logicType
50
- * - for-each → loopBodyNodeIds (non-empty) OR count (>0)
58
+ * - for-each → loopBodyNodeIds (non-empty) OR count (>0)
59
+ * - user-selection → exactly one file_array/string_array input (optional connection);
60
+ * input.min ≤ input.max
51
61
  * 5. Cross-references all point to node IDs that exist in the flow:
52
62
  * - dependsOn entries
53
63
  * - input ref.node (excluding "workflow" which is a reserved keyword)
@@ -101,6 +111,9 @@ export function validateWorkflowFlow(flow: unknown[]): void {
101
111
  );
102
112
  }
103
113
  }
114
+ if (type === 'user-selection') {
115
+ validateUserSelectionNode(label, node);
116
+ }
104
117
  }
105
118
 
106
119
  // Pass 2 — cross-reference checks (all node IDs are now collected)
@@ -165,3 +178,57 @@ function validateInputRefs(nodeId: string, input: RawInput, nodeIds: Set<string>
165
178
  }
166
179
  }
167
180
  }
181
+
182
+ function validateUserSelectionNode(label: string, node: RawNode): void {
183
+ const inputs = Array.isArray(node.inputs) ? (node.inputs as RawInput[]) : [];
184
+ const selectionInputs = inputs.filter(
185
+ (input) => typeof input.type === 'string' && USER_SELECTION_INPUT_TYPES.has(input.type),
186
+ );
187
+ const otherInputs = inputs.filter(
188
+ (input) => !(typeof input.type === 'string' && USER_SELECTION_INPUT_TYPES.has(input.type)),
189
+ );
190
+
191
+ if (selectionInputs.length !== 1) {
192
+ throw new Error(
193
+ `Node ${label} (user-selection) must have exactly one input of type file_array or string_array`,
194
+ );
195
+ }
196
+ for (const input of otherInputs) {
197
+ if (input.type !== 'connection') {
198
+ throw new Error(
199
+ `Node ${label} (user-selection) only allows an optional connection input besides the selection array, got "${String(
200
+ input.type,
201
+ )}"`,
202
+ );
203
+ }
204
+ }
205
+
206
+ const input = selectionInputs[0]!;
207
+
208
+ const min = input.min === undefined ? USER_SELECTION_MIN_SELECT : input.min;
209
+ const max = input.max === undefined ? USER_SELECTION_MAX_SELECT : input.max;
210
+
211
+ if (
212
+ typeof min !== 'number' ||
213
+ !Number.isInteger(min) ||
214
+ min < USER_SELECTION_MIN_SELECT ||
215
+ min > USER_SELECTION_MAX_SELECT
216
+ ) {
217
+ throw new Error(
218
+ `Node ${label} (user-selection) input.min must be an integer between ${USER_SELECTION_MIN_SELECT} and ${USER_SELECTION_MAX_SELECT}`,
219
+ );
220
+ }
221
+ if (
222
+ typeof max !== 'number' ||
223
+ !Number.isInteger(max) ||
224
+ max < USER_SELECTION_MIN_SELECT ||
225
+ max > USER_SELECTION_MAX_SELECT
226
+ ) {
227
+ throw new Error(
228
+ `Node ${label} (user-selection) input.max must be an integer between ${USER_SELECTION_MIN_SELECT} and ${USER_SELECTION_MAX_SELECT}`,
229
+ );
230
+ }
231
+ if (min > max) {
232
+ throw new Error(`Node ${label} (user-selection) input.min (${min}) must be <= input.max (${max})`);
233
+ }
234
+ }