@uipath/maestro-builder-sdk 6.18.9 → 6.19.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 (111) hide show
  1. package/dist/api-index.md +185 -184
  2. package/dist/api-members.md +504 -473
  3. package/dist/bpmn/uipath-moddle.v1.json +5 -0
  4. package/dist/case/decompile-cli.js +2 -8
  5. package/dist/check.js +177 -650
  6. package/dist/core/actions.d.ts +119 -760
  7. package/dist/core/actions.js +0 -306
  8. package/dist/core/error-port.d.ts +6 -0
  9. package/dist/core/error-port.js +9 -0
  10. package/dist/core/node-classes.d.ts +4 -63
  11. package/dist/core/node-classes.js +0 -112
  12. package/dist/core/nodes/family.d.ts +36 -0
  13. package/dist/core/nodes/family.js +1 -0
  14. package/dist/core/nodes/node-type.d.ts +136 -0
  15. package/dist/core/nodes/node-type.js +1 -0
  16. package/dist/core/nodes/registry.d.ts +57 -0
  17. package/dist/core/nodes/registry.js +122 -0
  18. package/dist/core/nodes/versioned.d.ts +50 -0
  19. package/dist/core/nodes/versioned.js +46 -0
  20. package/dist/core/typed-raw-node.d.ts +13 -62
  21. package/dist/core/typed-raw-node.js +40 -154
  22. package/dist/decompile-cli.js +2 -9
  23. package/dist/decompile.js +92 -271
  24. package/dist/flow-definitions.d.ts +10 -0
  25. package/dist/flow-definitions.js +35 -0
  26. package/dist/flow-node-registry.d.ts +13 -0
  27. package/dist/flow-node-registry.js +19 -0
  28. package/dist/flow-node-type.d.ts +192 -0
  29. package/dist/flow-node-type.js +1 -0
  30. package/dist/flow-sdk.d.ts +7 -0
  31. package/dist/flow-sdk.js +8 -0
  32. package/dist/nodes/data-fabric/flow/check.d.ts +13 -0
  33. package/dist/nodes/data-fabric/flow/check.js +164 -0
  34. package/dist/nodes/data-fabric/flow/factories.d.ts +113 -0
  35. package/dist/nodes/data-fabric/flow/factories.js +134 -0
  36. package/dist/nodes/data-fabric/flow/family.d.ts +7 -0
  37. package/dist/nodes/data-fabric/flow/family.js +9 -0
  38. package/dist/nodes/data-fabric/flow/index.d.ts +8 -0
  39. package/dist/nodes/data-fabric/flow/index.js +8 -0
  40. package/dist/nodes/data-fabric/flow/raw.d.ts +32 -0
  41. package/dist/nodes/data-fabric/flow/raw.js +100 -0
  42. package/dist/nodes/data-fabric/flow/shared.d.ts +77 -0
  43. package/dist/nodes/data-fabric/flow/shared.js +174 -0
  44. package/dist/nodes/data-fabric/flow/spec.d.ts +238 -0
  45. package/dist/nodes/data-fabric/flow/spec.js +1 -0
  46. package/dist/nodes/data-fabric/flow/verbs.d.ts +29 -0
  47. package/dist/nodes/data-fabric/flow/verbs.js +239 -0
  48. package/dist/nodes/delay/flow/check.d.ts +16 -0
  49. package/dist/nodes/delay/flow/check.js +86 -0
  50. package/dist/nodes/delay/flow/factories.d.ts +28 -0
  51. package/dist/nodes/delay/flow/factories.js +38 -0
  52. package/dist/nodes/delay/flow/family.d.ts +3 -0
  53. package/dist/nodes/delay/flow/family.js +8 -0
  54. package/dist/nodes/delay/flow/index.d.ts +3 -0
  55. package/dist/nodes/delay/flow/index.js +3 -0
  56. package/dist/nodes/delay/flow/node-type.d.ts +13 -0
  57. package/dist/nodes/delay/flow/node-type.js +71 -0
  58. package/dist/nodes/delay/flow/spec.d.ts +43 -0
  59. package/dist/nodes/delay/flow/spec.js +16 -0
  60. package/dist/nodes/document/flow/factories.d.ts +27 -0
  61. package/dist/nodes/document/flow/factories.js +30 -0
  62. package/dist/nodes/document/flow/family.d.ts +3 -0
  63. package/dist/nodes/document/flow/family.js +7 -0
  64. package/dist/nodes/document/flow/index.d.ts +3 -0
  65. package/dist/nodes/document/flow/index.js +3 -0
  66. package/dist/nodes/document/flow/node-type.d.ts +14 -0
  67. package/dist/nodes/document/flow/node-type.js +150 -0
  68. package/dist/nodes/document/flow/spec.d.ts +65 -0
  69. package/dist/nodes/document/flow/spec.js +1 -0
  70. package/dist/nodes/mock/flow/factories.d.ts +51 -0
  71. package/dist/nodes/mock/flow/factories.js +47 -0
  72. package/dist/nodes/mock/flow/family.d.ts +3 -0
  73. package/dist/nodes/mock/flow/family.js +7 -0
  74. package/dist/nodes/mock/flow/index.d.ts +2 -0
  75. package/dist/nodes/mock/flow/index.js +2 -0
  76. package/dist/nodes/mock/flow/node-type.d.ts +11 -0
  77. package/dist/nodes/mock/flow/node-type.js +51 -0
  78. package/dist/nodes/script/flow/check.d.ts +19 -0
  79. package/dist/nodes/script/flow/check.js +49 -0
  80. package/dist/nodes/script/flow/factories.d.ts +14 -0
  81. package/dist/nodes/script/flow/factories.js +15 -0
  82. package/dist/nodes/script/flow/family.d.ts +3 -0
  83. package/dist/nodes/script/flow/family.js +8 -0
  84. package/dist/nodes/script/flow/index.d.ts +3 -0
  85. package/dist/nodes/script/flow/index.js +3 -0
  86. package/dist/nodes/script/flow/node-type.d.ts +7 -0
  87. package/dist/nodes/script/flow/node-type.js +125 -0
  88. package/dist/nodes/script/flow/spec.d.ts +52 -0
  89. package/dist/nodes/script/flow/spec.js +1 -0
  90. package/dist/nodes/transform/flow/check.d.ts +6 -0
  91. package/dist/nodes/transform/flow/check.js +229 -0
  92. package/dist/nodes/transform/flow/factories.d.ts +52 -0
  93. package/dist/nodes/transform/flow/factories.js +53 -0
  94. package/dist/nodes/transform/flow/family.d.ts +3 -0
  95. package/dist/nodes/transform/flow/family.js +8 -0
  96. package/dist/nodes/transform/flow/index.d.ts +3 -0
  97. package/dist/nodes/transform/flow/index.js +3 -0
  98. package/dist/nodes/transform/flow/node-type.d.ts +7 -0
  99. package/dist/nodes/transform/flow/node-type.js +244 -0
  100. package/dist/nodes/transform/flow/spec.d.ts +116 -0
  101. package/dist/nodes/transform/flow/spec.js +10 -0
  102. package/dist/raw-node-advice.d.ts +16 -0
  103. package/dist/raw-node-advice.js +36 -0
  104. package/dist/raw-node.d.ts +71 -0
  105. package/dist/raw-node.js +116 -0
  106. package/dist/registry/uip/launch.d.ts +11 -1
  107. package/dist/registry/uip/launch.js +50 -43
  108. package/dist/registry/uip/pipeline-front-door.d.ts +2 -0
  109. package/dist/registry/uip/pipeline-front-door.js +88 -0
  110. package/dist/serialize.js +138 -492
  111. package/package.json +2 -2
package/dist/check.js CHANGED
@@ -5,7 +5,7 @@
5
5
  * duplicate step names, references to undeclared variables/steps, empty branch
6
6
  * arms, and missing terminals.
7
7
  */
8
- import { Expr, SCHEDULE_PRESETS, DELAY_PRESETS, VARIANT_OPERATION, parseIxpProjectType, flatNameOf, flatNameSegmentsOf, FLAT_SEP } from './flow-sdk.js';
8
+ import { Expr, SCHEDULE_PRESETS, parseIxpProjectType, flatNameOf, flatNameSegmentsOf, FLAT_SEP } from './flow-sdk.js';
9
9
  import { FlowNode } from './core/node-classes.js';
10
10
  import { planContributionBindings } from './core/contribution-bindings.js';
11
11
  import { definitionHasErrorPort, errorPortRefusal } from './core/error-port.js';
@@ -26,6 +26,9 @@ import { FLOW_SCHEMA_VERSIONS } from './format-profile.js';
26
26
  import { compareSchemaVersions, schemaVersionRefusal } from './schema-version.js';
27
27
  import { planFlowGlobals, planSharedInputs, sharedInputRoots } from './flow-variables.js';
28
28
  import { ownEntry } from './core/own-entry.js';
29
+ import { FLOW_NODE_TYPES } from './flow-node-registry.js';
30
+ import { resolveVersion } from './core/nodes/versioned.js';
31
+ import { declaredErrorPortRefusal } from './core/error-port.js';
29
32
  export function check(built, opts = {}) {
30
33
  let diags = [];
31
34
  const uses = collectConnectionUses(() => { diags = checkFlow(built, [], opts); });
@@ -115,6 +118,12 @@ function checkFlow(built, ancestors, opts = {}) {
115
118
  ]);
116
119
  const outputType = new Map(built.outputs.map((x) => [x.name, String(x.type)]));
117
120
  const stepNames = new Set();
121
+ // Steps whose kind is a registered node type (flow-node-registry.ts), by name:
122
+ // their node-local rules, expressions and output contract come from the node
123
+ // type, and their family's rules run over all of them after the walk.
124
+ const registeredSteps = new Map();
125
+ /** Steps of a registered kind whose version did not resolve (NODE_VERSION_UNREGISTERED), so a family rule can stay silent about them. */
126
+ const unresolvedSteps = new Set();
118
127
  /**
119
128
  * Every expression the flow carries, with the step that failed when the
120
129
  * expression sits inside that step's error handler. The failed step's own
@@ -133,8 +142,6 @@ function checkFlow(built, ancestors, opts = {}) {
133
142
  // `core.logic.merge` definition declares no outputs at all (see
134
143
  // PARALLEL_HAS_NO_OUTPUT below).
135
144
  const parallelNames = new Set();
136
- const delayNames = new Set();
137
- const dataFabricReads = new Set();
138
145
  // Per-outcome routing bookkeeping: which hitl steps opted into outcome ports
139
146
  // (and their declared outcome ids), and which ids actually got a route.
140
147
  const hitlOutcomeSteps = new Map();
@@ -151,22 +158,6 @@ function checkFlow(built, ancestors, opts = {}) {
151
158
  // waits until the expressions are collected (see HITL_OUTCOMES_UNREACHABLE
152
159
  // below).
153
160
  const hitlChoiceNoPorts = new Map();
154
- // Data Fabric writes that target a record `{ fromRead: '<step>' }` — update and
155
- // delete share the selector, so they share the check.
156
- const dataFabricRefs = [];
157
- // Delete step names. `core.datafabric.delete` is the one verb of the family
158
- // whose definition declares NO outputDefinition, so `$vars.<delete>.output` is
159
- // undefined at run time (see DATAFABRIC_DELETE_HAS_NO_OUTPUT below).
160
- const dataFabricDeletes = new Set();
161
- // Reads that return a LIST. A multi-record read is refused as a write target
162
- // (see DATAFABRIC_FROMREAD_MULTIPLE below) — the serializer emits a plain
163
- // no-op task for the write, and nothing at any rung reports it.
164
- const dataFabricMultiReads = new Set();
165
- // Placeholder (`mock()`) step names. The platform's `core.logic.mock` declares
166
- // one output whose `source` is the literal `"null"` — it publishes nothing —
167
- // so `$vars.<mock>.output` is null at run time, always (see
168
- // MOCK_HAS_NO_OUTPUT below).
169
- const mockNames = new Set();
170
161
  // Terminate step names. A terminate declares no action output in any registry
171
162
  // version, so `$vars.<terminate>` is undefined at run time. The terminal
172
163
  // flow-output mappings the serializer emits are validation metadata, not a
@@ -260,7 +251,7 @@ function checkFlow(built, ancestors, opts = {}) {
260
251
  declared.add(s.name); // a step's output is referenceable by name
261
252
  }
262
253
  if ((s.kind === 'stepToList' || s.kind === 'stepToRef') && s.port === 'error')
263
- checkOnError(s, prevPortSource, diags);
254
+ checkOnError(s, prevPortSource, diags, prevPortSource && registeredSteps.get(prevPortSource.name));
264
255
  if ((s.kind === 'stepToList' || s.kind === 'stepToRef') && s.port.startsWith('branch-')) {
265
256
  const want = s.port.slice('branch-'.length);
266
257
  const prevSpec = prevAction?.spec;
@@ -363,7 +354,17 @@ function checkFlow(built, ancestors, opts = {}) {
363
354
  if (s.kind === 'action') {
364
355
  prevAction = s;
365
356
  prevPortSource = s;
366
- collectActionExprs(s, exprs, errorSource);
357
+ const registered = registeredKind(s.spec);
358
+ if (registered) {
359
+ const placed = checkRegisteredStep(registered, s, exprs, errorSource, opts, diags, typeOf, triggerId);
360
+ if (placed)
361
+ registeredSteps.set(s.name, placed);
362
+ else
363
+ unresolvedSteps.add(s.name);
364
+ }
365
+ else {
366
+ collectActionExprs(s, exprs, errorSource);
367
+ }
367
368
  if (s.spec.kind === 'connector') {
368
369
  connectorSteps.set(s.name, s.spec);
369
370
  checkConnectorLookups(s.name, s.spec, opts, diags);
@@ -380,8 +381,6 @@ function checkFlow(built, ancestors, opts = {}) {
380
381
  }
381
382
  checkContribution(maybeCustom, s.options?.version, s.name, own.some((d) => d.severity === 'error'), diags);
382
383
  }
383
- if (s.spec.kind === 'transform')
384
- checkTransform(s.name, s.spec.inputs, diags, typeOf, triggerId);
385
384
  if (s.spec.kind === 'http') {
386
385
  checkHttp(s.name, s.spec.inputs, diags);
387
386
  // Only a complete pair: an empty half is HTTP_MANAGED_BINDING_INCOMPLETE, which
@@ -419,14 +418,6 @@ function checkFlow(built, ancestors, opts = {}) {
419
418
  }
420
419
  }
421
420
  }
422
- if (s.spec.kind === 'script')
423
- checkScript(s.name, s.spec.inputs, diags);
424
- if (s.spec.kind === 'delay') {
425
- checkDelay(s.name, s.spec.inputs, diags);
426
- delayNames.add(s.name);
427
- }
428
- if (s.spec.kind === 'mock')
429
- mockNames.add(s.name);
430
421
  // `.stepSwitch` routes its exits as ARMS, so the `.stepToList` bookkeeping
431
422
  // would report every one of them unrouted. STEP_SWITCH_EXIT_UNROUTED is
432
423
  // that step's version of the same question.
@@ -497,71 +488,6 @@ function checkFlow(built, ancestors, opts = {}) {
497
488
  outputReaders.set(stepName, (field, rest, at, d) => rule(stepName, field, rest, inputs, at, d));
498
489
  }
499
490
  }
500
- if (s.spec.kind === 'documentClassify' || s.spec.kind === 'dynamicExtract') {
501
- const fr = s.spec.inputs.fileRef;
502
- const empty = fr === undefined || fr === null || (typeof fr === 'string' && fr.trim() === '');
503
- if (empty) {
504
- diags.push({
505
- level: 'error', code: 'DOC_NO_FILEREF', step: s.name,
506
- message: `"${s.name}" has no fileRef — nothing to ${s.spec.kind === 'documentClassify' ? 'classify' : 'extract from'}. Reference a file input or an upstream attachment.`,
507
- });
508
- }
509
- if (s.spec.kind === 'dynamicExtract' && s.spec.inputs.schema !== undefined
510
- && (typeof s.spec.inputs.schema !== 'object' || Array.isArray(s.spec.inputs.schema))) {
511
- diags.push({
512
- level: 'error', code: 'EXTRACT_SCHEMA_SHAPE', step: s.name,
513
- message: `"${s.name}"'s schema must be an object (the inline extraction schema document).`,
514
- });
515
- }
516
- }
517
- if (s.spec.kind === 'dataFabricRead' || s.spec.kind === 'dataFabricCreate'
518
- || s.spec.kind === 'dataFabricUpdate' || s.spec.kind === 'dataFabricDelete') {
519
- if (!String(s.spec.inputs.entity ?? '').trim()) {
520
- diags.push({
521
- level: 'error', code: 'DATAFABRIC_NO_ENTITY', step: s.name,
522
- message: `"${s.name}" names no Data Fabric entity. Set { entity: '<entity name>' }.`,
523
- });
524
- }
525
- if (s.spec.kind === 'dataFabricRead') {
526
- dataFabricReads.add(s.name);
527
- if (s.spec.inputs.resultMode === 'multiple')
528
- dataFabricMultiReads.add(s.name);
529
- checkDataFabricQuery(s.name, s.spec.inputs, diags);
530
- }
531
- if (s.spec.kind === 'dataFabricCreate') {
532
- // Not the same failure as an empty update: a BLANK value is omitted
533
- // from the insert so the column default applies, so an all-blank
534
- // `fieldValues` serializes to an empty body and runs green having
535
- // inserted nothing. The node's schema rejects that — and the fix is
536
- // a real value, never a placeholder to satisfy the rule.
537
- const written = Object.values(s.spec.inputs.values ?? {})
538
- .filter((v) => (v instanceof Expr ? true : String(v ?? '').trim() !== ''));
539
- if (written.length === 0) {
540
- diags.push({
541
- level: 'error', code: 'DATAFABRIC_EMPTY_VALUES', step: s.name,
542
- message: `"${s.name}" sets no column to a non-blank value, so it would insert nothing and run green. `
543
- + 'The node\'s schema rejects that — give a real column a real value rather than a placeholder.',
544
- });
545
- }
546
- checkDataFabricColumns(s.name, Object.keys(s.spec.inputs.values ?? {}), 'values', diags);
547
- }
548
- if (s.spec.kind === 'dataFabricUpdate') {
549
- if (Object.keys(s.spec.inputs.set ?? {}).length === 0) {
550
- diags.push({
551
- level: 'error', code: 'DATAFABRIC_EMPTY_SET', step: s.name,
552
- message: `"${s.name}" writes no columns — { set } is empty, so the update would no-op green.`,
553
- });
554
- }
555
- checkDataFabricColumns(s.name, Object.keys(s.spec.inputs.set ?? {}), 'set', diags);
556
- }
557
- checkDataFabricScope(s.name, s.spec.inputs, diags);
558
- if (s.spec.kind === 'dataFabricUpdate' || s.spec.kind === 'dataFabricDelete') {
559
- const factory = s.spec.kind === 'dataFabricUpdate' ? 'dataFabricUpdate' : 'dataFabricDelete';
560
- dataFabricRefs.push({ step: s.name, factory, record: s.spec.inputs.record });
561
- if (s.spec.kind === 'dataFabricDelete')
562
- dataFabricDeletes.add(s.name);
563
- }
564
- }
565
491
  if (s.spec.kind === 'ixpExtract')
566
492
  checkIxpExtract(s.name, s.spec.inputs, diags);
567
493
  if (s.spec.kind === 'waitEvent') {
@@ -839,30 +765,7 @@ function checkFlow(built, ancestors, opts = {}) {
839
765
  }
840
766
  }
841
767
  reportUnreachableHitlOutcomes(hitlChoiceNoPorts, exprs, diags);
842
- for (const { step, factory, record } of dataFabricRefs) {
843
- const fromRead = record?.fromRead;
844
- if (typeof fromRead !== 'string')
845
- continue;
846
- if (!dataFabricReads.has(fromRead)) {
847
- diags.push({
848
- level: 'error', code: 'DATAFABRIC_FROMREAD_UNKNOWN', step,
849
- message: `"${step}" targets record: { fromRead: ${JSON.stringify(fromRead)} }, but no dataFabricRead step has that name.`,
850
- suggestion: `${factory}({ …, record: { byId: '<record id>' } })`,
851
- });
852
- continue;
853
- }
854
- // A LIST is not a write target: the serializer refuses it and emits a plain
855
- // no-op task, so the flow validates, runs green, and writes nothing.
856
- if (dataFabricMultiReads.has(fromRead)) {
857
- diags.push({
858
- level: 'error', code: 'DATAFABRIC_FROMREAD_MULTIPLE', step,
859
- message: `"${step}" targets record: { fromRead: ${JSON.stringify(fromRead)} }, but "${fromRead}" is a `
860
- + "resultMode: 'multiple' query. A multi-record read is refused as a write target — the step compiles to "
861
- + 'a no-op and the run goes green having written nothing.',
862
- suggestion: `loop the results and target each record with ${factory}({ …, record: { byId: … } })`,
863
- });
864
- }
865
- }
768
+ checkRegisteredFamilies(registeredSteps, unresolvedSteps, stepNames, diags);
866
769
  // Entry points: unique roots with valid triggers, colliding with nothing.
867
770
  const epIds = new Set();
868
771
  for (const ep of built.entryPoints ?? []) {
@@ -1144,6 +1047,16 @@ function checkFlow(built, ancestors, opts = {}) {
1144
1047
  });
1145
1048
  continue;
1146
1049
  }
1050
+ // A registered node type declares what its output is; `none` reports here,
1051
+ // at the same point as the per-family no-output rules below.
1052
+ const placed = registeredSteps.get(name);
1053
+ if (placed) {
1054
+ const contract = placed.impl.output(placed.spec);
1055
+ if (contract.kind === 'none') {
1056
+ diags.push(catalogued(placed.entry, step, contract.code, contract.message({ node: name, ref: `$vars.${name}${field ? '.' + field : ''}`, reader: step })));
1057
+ continue;
1058
+ }
1059
+ }
1147
1060
  // A switch only routes; it has no output of its own, so any read of it is
1148
1061
  // undefined at run time. Point the author at the arm's step instead.
1149
1062
  if (switchNames.has(name)) {
@@ -1186,62 +1099,6 @@ function checkFlow(built, ancestors, opts = {}) {
1186
1099
  });
1187
1100
  continue;
1188
1101
  }
1189
- // A Data Fabric delete is the one verb of that family whose definition
1190
- // declares no outputDefinition at all — the record is gone, so there is
1191
- // nothing to publish. Read the record on the step that FOUND it instead,
1192
- // which on the `{ fromRead }` form is already in the flow.
1193
- if (dataFabricDeletes.has(name)) {
1194
- diags.push({
1195
- level: 'error',
1196
- code: 'DATAFABRIC_DELETE_HAS_NO_OUTPUT',
1197
- message: `Delete "${name}" produces no output, but "$vars.${name}${field ? '.' + field : ''}" reads one in "${step}". `
1198
- + 'A Data Fabric delete declares no outputs — take the value off the dataFabricRead step that found the record, before the delete.',
1199
- step,
1200
- });
1201
- continue;
1202
- }
1203
- if (delayNames.has(name)) {
1204
- diags.push({
1205
- level: 'error',
1206
- code: 'DELAY_HAS_NO_OUTPUT',
1207
- message: `Delay "${name}" produces no output, but "$vars.${name}${field ? '.' + field : ''}" reads one in "${step}". `
1208
- + 'A delay only waits — read the step that computed the value instead.',
1209
- step,
1210
- });
1211
- continue;
1212
- }
1213
- // A `mock()` is a PLACEHOLDER: `core.logic.mock`'s definition declares one
1214
- // output whose `source` is the literal `"null"`, so the value is null at run
1215
- // time whatever the flow does. Unlike the four rules above, the node DOES
1216
- // declare an output slot — which is exactly why the read is silent: it
1217
- // resolves in the designer, it resolves in `variables.nodes`, and it hands
1218
- // the downstream step a null.
1219
- //
1220
- // WHY THIS RULE LIVES HERE, in the authoring checker rather than in a
1221
- // pass over the compiled artifact (the measurement,
1222
- // convention #191). Reading a placeholder's output is the ARCHIVE's
1223
- // dominant idiom, not a rarity: 55 of the 62 archived mock-carrying
1224
- // artifacts do it — 110 read sites, 87 of them inside a script's body and
1225
- // 19 in a decision's expression — and **39 of those 55 come from runs that
1226
- // scored 1.0**. That is an author writing the downstream code for the real
1227
- // node the placeholder stands in for, and a `.flow`-level rule refusing it
1228
- // would refuse the deployed corpus (lesson 47). An SDK author is in the
1229
- // other position: they wrote `mock()` here, in this file, and the SDK can
1230
- // say at compile time that the read is dead.
1231
- // Measured 2026-08-02 over 4,033 distinct archived artifacts —
1232
- // runs/flow-sdk-cleanup-2026-08-02/step0/mock-output-read-sites.txt.
1233
- if (mockNames.has(name)) {
1234
- diags.push({
1235
- level: 'error',
1236
- code: 'MOCK_HAS_NO_OUTPUT',
1237
- message: `Placeholder "${name}" publishes nothing, but "$vars.${name}${field ? '.' + field : ''}" reads it in "${step}". `
1238
- + 'A mock() is a marker for a node that does not exist yet — its output is null at run time, always. '
1239
- + 'Keep the placeholder while the real node does not exist, and have the reader use a value you supply instead (a flow input, as example/ClaimScaffold.flow.ts does). '
1240
- + 'Replace the placeholder only with the real node, once `registry search` finds it; a script returning fixed values is not the real node.',
1241
- step,
1242
- });
1243
- continue;
1244
- }
1245
1102
  // A subflow hands back an OBJECT keyed by the child flow's declared
1246
1103
  // outputs — even when the child declares exactly one. So the reference
1247
1104
  // needs the output's NAME (`out('call', 'reversed')`), and the name has to
@@ -1985,26 +1842,6 @@ function checkTrigger(spec, diags, opts = {}, entryPointId) {
1985
1842
  + `comes round. Use a non-zero interval.`, `every: 'R/PT1H'`);
1986
1843
  }
1987
1844
  }
1988
- /**
1989
- * An EXACT ISO-8601 duration: weeks/days plus a time part, at least one
1990
- * component. Copied in shape from the delay definition's own
1991
- * `timerValue.pattern`, minus the years and months — see DELAY_CALENDAR below.
1992
- */
1993
- const DURATION_RE = /^P(?!$)(\d+W)?(\d+D)?(T(?=\d)(\d+H)?(\d+M)?(\d+S)?)?$/;
1994
- /** Grammatical ISO-8601 duration, including the calendar-relative parts. */
1995
- const ANY_DURATION_RE = /^P(?!$)(\d+Y)?(\d+M)?(\d+W)?(\d+D)?(T(?=\d)(\d+H)?(\d+M)?(\d+S)?)?$/;
1996
- /** Natural-language guesses an author might reach for, mapped to a duration. */
1997
- const DURATION_WORDS = [
1998
- ['second', 'PT30S'], ['minute', 'PT15M'], ['hour', 'PT1H'],
1999
- ['day', 'P1D'], ['daily', 'P1D'], ['week', 'P1W'], ['weekly', 'P1W'],
2000
- ];
2001
- /**
2002
- * Diagnose a `delay` step's wait.
2003
- *
2004
- * Rung 1 is the only place an author hears about a bad duration in time to fix
2005
- * it cheaply. Anything downstream speaks in terms of `timerPreset`/`timerValue`,
2006
- * the two fields the serializer writes, rather than the one field the author typed.
2007
- */
2008
1845
  /** A release key is a GUID — the shape 45/45 deployed rpa node types carry. */
2009
1846
  /**
2010
1847
  * The DECLARED name a single reference resolves to, or undefined.
@@ -3870,64 +3707,6 @@ function checkQueueItem(step, inputs, diags) {
3870
3707
  }
3871
3708
  /** The three priorities `core.action.queue.create[-and-wait]`'s form offers. */
3872
3709
  const QUEUE_PRIORITIES = ['Low', 'Normal', 'High'];
3873
- function checkDelay(step, inputs, diags) {
3874
- const at = (code, message, suggestion) => {
3875
- diags.push({ level: 'error', code, message, step, ...(suggestion ? { suggestion } : {}) });
3876
- };
3877
- const untilRaw = inputs?.until;
3878
- if (typeof untilRaw === 'string' && untilRaw.trim() !== '') {
3879
- // The absolute-date arm: an ISO-8601 date-time, e.g. 2026-09-01T09:00:00Z.
3880
- const until = untilRaw.trim();
3881
- if (!/^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:\d{2})?)?$/.test(until)) {
3882
- at('DELAY_UNTIL_NOT_ISO', `Delay "${step}"'s until ${JSON.stringify(untilRaw)} is not an ISO-8601 date-time. `
3883
- + `Use e.g. '2026-09-01T09:00:00Z' (a timezone-less value is read in the tenant's zone).`, `until: '2026-09-01T09:00:00Z'`);
3884
- }
3885
- return;
3886
- }
3887
- const raw = inputs?.duration;
3888
- if (raw === undefined || raw === null || (typeof raw === 'string' && raw.trim() === '')) {
3889
- at('DELAY_NO_DURATION', `Delay "${step}" has no \`duration\`, so it carries no wait at all and the flow runs straight `
3890
- + `past it. Give it an ISO-8601 duration — ${DELAY_PRESETS.slice(0, 8).join(', ')} … are the `
3891
- + `designer's presets, and any other exact duration works too.`, `duration: 'PT15M'`);
3892
- return;
3893
- }
3894
- const d = String(raw).trim();
3895
- if (!ANY_DURATION_RE.test(d)) {
3896
- at('DELAY_NOT_ISO', `Delay "${step}"'s duration ${JSON.stringify(raw)} is not an ISO-8601 duration. The format is `
3897
- + `"P" plus a date part and/or "T" plus a time part — PT30S, PT15M, PT2H, P1D, P1W. Note the `
3898
- + `"T": P1M is a MONTH, PT1M is a minute.`, nearestDuration(d));
3899
- return;
3900
- }
3901
- if (!/[1-9]/.test(d)) {
3902
- at('DELAY_ZERO', `Delay "${step}"'s duration ${JSON.stringify(d)} is zero-length, so the step waits for no time `
3903
- + `at all. Use a non-zero duration, or drop the step.`, `duration: 'PT15M'`);
3904
- return;
3905
- }
3906
- // Grammatical, and the platform runs it — but a month is not a fixed number
3907
- // of milliseconds, so NO local rung can run the flow at all.
3908
- // Rejected here rather than approximated: guessing 30 days would
3909
- // put a silent disagreement with the deployed engine back into the pipeline.
3910
- if (!DURATION_RE.test(d)) {
3911
- at('DELAY_CALENDAR', `Delay "${step}"'s duration ${JSON.stringify(d)} is calendar-relative: how long a month or a `
3912
- + `year is depends on when it starts. The platform resolves that at run time, but the local `
3913
- + `timer takes a fixed millisecond count, so this flow could not be replayed or run offline.`, `duration: 'P30D'`);
3914
- }
3915
- }
3916
- /** Best guess at what the author meant, for a `DELAY_NOT_ISO` suggestion. */
3917
- function nearestDuration(d) {
3918
- // The likeliest slip: a time-part duration written without its "T".
3919
- if (/^P\d+[HMS]$/.test(d) && ANY_DURATION_RE.test(`PT${d.slice(1)}`))
3920
- return `duration: 'PT${d.slice(1)}'`;
3921
- const lower = d.toLowerCase();
3922
- const n = /(\d+)/.exec(lower)?.[1];
3923
- const word = DURATION_WORDS.find(([w]) => lower.includes(w));
3924
- if (!word)
3925
- return undefined;
3926
- if (!n)
3927
- return `duration: '${word[1]}'`;
3928
- const unit = word[1].replace(/\d+/, '');
3929
- return `duration: '${unit.startsWith('PT') ? `PT${n}${unit.slice(2)}` : `P${n}${unit.slice(1)}`}'`;
3930
- }
3931
3710
  /** Best guess at what the author meant, for a `SCHEDULE_NOT_ISO` suggestion. */
3932
3711
  function nearestInterval(every) {
3933
3712
  // By far the likeliest slip: a correct duration missing the "R/" repeat marker.
@@ -4249,70 +4028,6 @@ const RETRY_COUNT_SOFT_MAX = 5;
4249
4028
  * not a shorter timeout, it is a value the request node fails to configure with,
4250
4029
  * and nothing downstream of `check` would say so in the author's own vocabulary.
4251
4030
  */
4252
- /** The type vocabulary a script's `returns` may name — what the wire format accepts. */
4253
- const SCRIPT_RETURN_TYPES = new Set(['string', 'number', 'boolean', 'object', 'array']);
4254
- /**
4255
- * A script step's `returns` — the author overriding what the body looks like.
4256
- *
4257
- * TypeScript already refuses a bad value at the call site, so this is the fence
4258
- * for everything that does NOT go through the type checker: a `.flow.ts` loaded
4259
- * as plain JS, a generated flow, a value read from config. Both mistakes below
4260
- * reach the emitted file silently and are then the designer's problem instead of
4261
- * the author's, which is precisely the failure this whole feature exists to undo.
4262
- */
4263
- function checkScript(step, inputs, diags) {
4264
- const returns = inputs?.returns;
4265
- if (returns === undefined)
4266
- return;
4267
- if (typeof returns === 'string') {
4268
- if (!SCRIPT_RETURN_TYPES.has(returns)) {
4269
- diags.push({
4270
- level: 'error',
4271
- code: 'SCRIPT_RETURNS_UNKNOWN_TYPE',
4272
- message: `Script step "${step}" declares \`returns: ${JSON.stringify(returns)}\`, which is not a type the `
4273
- + `wire format knows. Use one of ${[...SCRIPT_RETURN_TYPES].map((t) => `"${t}"`).join(', ')}, or a `
4274
- + `field map like \`{ total: "number" }\` to name an object's fields.`,
4275
- step,
4276
- });
4277
- }
4278
- return;
4279
- }
4280
- if (typeof returns !== 'object' || returns === null || Array.isArray(returns)) {
4281
- diags.push({
4282
- level: 'error',
4283
- code: 'SCRIPT_RETURNS_UNKNOWN_TYPE',
4284
- message: `Script step "${step}"'s \`returns\` is ${JSON.stringify(returns)}. It takes either a type name or a `
4285
- + `map of field names to types (\`{ total: "number" }\`).`,
4286
- step,
4287
- });
4288
- return;
4289
- }
4290
- const fields = returns;
4291
- // Same call as `APIWF_EMPTY_RETURNS` one surface over: an empty map declares
4292
- // nothing, emits nothing, and reads exactly like omitting the key — so it is
4293
- // almost always a half-finished edit rather than a statement.
4294
- if (Object.keys(fields).length === 0) {
4295
- diags.push({
4296
- level: 'warning',
4297
- code: 'SCRIPT_EMPTY_RETURNS',
4298
- message: `Script step "${step}" declares \`returns: {}\`, which names no fields and emits nothing — the same `
4299
- + `as omitting it. Name the fields, or drop the key and let the body speak for itself.`,
4300
- step,
4301
- });
4302
- return;
4303
- }
4304
- for (const [name, type] of Object.entries(fields)) {
4305
- if (typeof type !== 'string' || !SCRIPT_RETURN_TYPES.has(type)) {
4306
- diags.push({
4307
- level: 'error',
4308
- code: 'SCRIPT_RETURNS_UNKNOWN_TYPE',
4309
- message: `Script step "${step}" declares field "${name}" as ${JSON.stringify(type)}, which is not a type the `
4310
- + `wire format knows. Use one of ${[...SCRIPT_RETURN_TYPES].map((t) => `"${t}"`).join(', ')}.`,
4311
- step,
4312
- });
4313
- }
4314
- }
4315
- }
4316
4031
  function checkHttp(step, inputs, diags) {
4317
4032
  // `core.action.http` is retired on the platform side: after `registry pull
4318
4033
  // --force` the tenant serves only `core.action.http.v2` (2.3), and a
@@ -4907,211 +4622,6 @@ function checkHitlRead(name, field, rest, form, step, diags) {
4907
4622
  ...(nearest !== undefined ? { suggestion: `out('${name}', '${nearest}')` } : {}),
4908
4623
  });
4909
4624
  }
4910
- function checkTransform(step, inputs, diags, typeOf = new Map(), triggerId = 'start') {
4911
- const at = (code, message, suggestion) => {
4912
- diags.push({ level: 'error', code, message, step, ...(suggestion ? { suggestion } : {}) });
4913
- };
4914
- // ── collection must be a bare variable path ──
4915
- const collection = inputs?.collection;
4916
- if (collection === undefined || collection === null) {
4917
- at('TRANSFORM_NO_COLLECTION', `Transform "${step}" has no collection. Point it at the variable or step output holding the array.`, `collection: v('catalog')`);
4918
- }
4919
- else if (!(collection instanceof Expr)) {
4920
- // Types are stripped at run time, so `collection: [{…}]` or
4921
- // `collection: 'catalog'` reaches here as a raw value.
4922
- at('TRANSFORM_COLLECTION_NOT_REF', `Transform "${step}" was given a ${Array.isArray(collection) ? 'raw array' : `raw ${typeof collection}`} as its `
4923
- + `collection. That field is a variable PATH — the runtime turns the string into a lookup, so a value there `
4924
- + `resolves to nothing and the transform silently sees an empty collection.`, typeof collection === 'string' ? `collection: v('${collection}')` : `collection: v('<varName>')`);
4925
- }
4926
- else if (collection.literal) {
4927
- at('TRANSFORM_COLLECTION_LITERAL', `Transform "${step}" has a literal collection. Inline data cannot be a path — keep the array in a variable `
4928
- + `default (\`.var('items', types.array, [...])\`) and reference it, or read an upstream step's output.`, `collection: v('items')`);
4929
- }
4930
- else if (!/^\$vars\.[A-Za-z0-9_]+(\.[A-Za-z0-9_]+)*$/.test(collection.js.trim())) {
4931
- // A computed expression (js`…`, tmpl`…`, `.concat(…)`) is the trap that
4932
- // survives every local rung: a local run resolves it to a value, so
4933
- // it runs correctly there and resolves to NOTHING once deployed, where the
4934
- // string is used as a lookup path.
4935
- at('TRANSFORM_COLLECTION_NOT_PATH', `Transform "${step}"'s collection must be a plain reference, but it is the expression `
4936
- + `"${collection.js.trim().slice(0, 60)}". The deployed runtime uses that string as a lookup PATH, so a `
4937
- + `computed expression resolves to nothing there even though it runs locally. Compute it in a script step `
4938
- + `first, then point the transform at that step's output.`, `collection: out('<scriptStep>')`);
4939
- }
4940
- else {
4941
- // A collection that is not an array is PASSED THROUGH untouched — no error,
4942
- // no operations applied (faithful to v1). The result is a transform that
4943
- // looks like it ran and changed nothing.
4944
- // Only a DECLARED scalar can be judged here: a step output's type is not
4945
- // known offline, so this stays quiet about those rather than guessing.
4946
- const refName = declaredRefName(collection.js, triggerId);
4947
- const declaredType = refName ? typeOf.get(refName) : undefined;
4948
- if (declaredType && ['string', 'number', 'boolean'].includes(declaredType)) {
4949
- at('TRANSFORM_COLLECTION_SCALAR', `Transform "${step}"'s collection is "${refName}", declared as ${declaredType}. A transform runs its `
4950
- + `operations over an ARRAY; anything else is passed through untouched — no error, nothing applied — so `
4951
- + `the step would look like it ran and change nothing.`, `Declare it as types.array, or point the collection at the array field: collection: out('<step>', '<field>')`);
4952
- }
4953
- }
4954
- // ── the operation chain ──
4955
- const ops = inputs?.operations;
4956
- if (!Array.isArray(ops) || ops.length === 0) {
4957
- at('TRANSFORM_NO_OPERATIONS', `Transform "${step}" has no operations, so it passes its collection straight through. Add at least one `
4958
- + `filter / map / groupBy operation, or drop the node.`, `operations: [{ type: 'filter', filters: [{ field: 'status', condition: 'equals', value: 'open' }] }]`);
4959
- return;
4960
- }
4961
- checkVariant(step, inputs, ops, at);
4962
- ops.forEach((op, i) => {
4963
- const where = `operations[${i}]`;
4964
- // Read the discriminant before the narrowing below exhausts the union — at
4965
- // run time it can be anything (an author typo survives type erasure).
4966
- const kind = op?.type;
4967
- if (op?.type === 'filter') {
4968
- if (!Array.isArray(op.filters) || op.filters.length === 0) {
4969
- at('TRANSFORM_EMPTY_FILTER', `Transform "${step}" ${where} is a filter with no rules — the runtime passes every item through it.`);
4970
- return;
4971
- }
4972
- for (const [j, f] of op.filters.entries()) {
4973
- if (!f?.field && f?.field !== '')
4974
- at('TRANSFORM_FILTER_NO_FIELD', `Transform "${step}" ${where}.filters[${j}] has no field, so the rule is dropped and the filter `
4975
- + `matches everything.`);
4976
- checkVocab(step, `${where}.filters[${j}].condition`, f?.condition, FILTER_CONDITIONS, 'filter condition', 'passes every item through the rule', diags);
4977
- if (typeof f?.value === 'string' && LOOKS_LIKE_EXPRESSION.test(f.value.trim()))
4978
- at('TRANSFORM_FILTER_VALUE_EXPR', `Transform "${step}" ${where}.filters[${j}] compares against ${JSON.stringify(f.value)}. Filter values `
4979
- + `are LITERAL only — the runtime evaluates nothing here, so every item would be compared against that `
4980
- + `text and the filter would return nothing. Use a literal, or do the comparison in a script step.`, `value: 500`);
4981
- // `contains` is TWO operators wearing one name — the runtime does
4982
- // `Array.includes` when the FIELD holds an array and `String.includes`
4983
- // otherwise, faithful to v1. Which
4984
- // one runs is decided by the data, so nothing static can pick for you —
4985
- // but an ARRAY on the compare side is wrong under both readings
4986
- // (`['a','b']` is stringified to `a,b` for the substring test, and
4987
- // `includes` looks for the array itself as one element), and it is the
4988
- // shape an author reaches for when they mean "any of these".
4989
- if ((f?.condition === 'contains' || f?.condition === 'not_contains') && Array.isArray(f?.value))
4990
- at('TRANSFORM_CONTAINS_ARRAY_VALUE', `Transform "${step}" ${where}.filters[${j}] uses "${f.condition}" against an ARRAY. "contains" compares `
4991
- + `ONE value: if the item's field holds an array it tests membership of that value, otherwise it tests `
4992
- + `for that value as a substring — either way an array on the right matches nothing, silently. For `
4993
- + `"any of these", use one rule per value with the filter's "or" operation.`, `condition: 'contains', value: ${JSON.stringify(f.value[0] ?? 'x')}`);
4994
- }
4995
- return;
4996
- }
4997
- if (op?.type === 'map') {
4998
- if (!Array.isArray(op.mappings) || op.mappings.length === 0) {
4999
- at('TRANSFORM_EMPTY_MAP', `Transform "${step}" ${where} is a map with no mappings — with keepOriginalFields off that turns every `
5000
- + `item into {}.`);
5001
- return;
5002
- }
5003
- for (const [j, m] of op.mappings.entries()) {
5004
- if (!m?.field)
5005
- at('TRANSFORM_MAP_NO_FIELD', `Transform "${step}" ${where}.mappings[${j}] has no field, so nothing is carried into the output.`);
5006
- // Omitted is fine — it defaults to `copy`.
5007
- if (m?.transformation !== undefined)
5008
- checkVocab(step, `${where}.mappings[${j}].transformation`, m.transformation, TRANSFORMATIONS, 'transformation', 'copies the value unchanged', diags);
5009
- }
5010
- return;
5011
- }
5012
- if (op?.type === 'groupBy') {
5013
- if (!op.groupByField)
5014
- at('TRANSFORM_NO_GROUP_FIELD', `Transform "${step}" ${where} is a groupBy with no groupByField, so every item lands in one anonymous `
5015
- + `group.`);
5016
- for (const [j, a] of (op.aggregations ?? []).entries()) {
5017
- if (!a?.alias?.trim())
5018
- at('TRANSFORM_AGG_NO_ALIAS', `Transform "${step}" ${where}.aggregations[${j}] has no alias. The runtime drops an unaliased `
5019
- + `aggregation, so its result would simply be absent from the output.`);
5020
- if (a?.operation !== 'count' && !a?.field?.trim())
5021
- at('TRANSFORM_AGG_NO_FIELD', `Transform "${step}" ${where}.aggregations[${j}] is "${a?.operation}" with no field. Only "count" `
5022
- + `works without one; the runtime drops the rest, so the aggregate would be missing from the output.`);
5023
- checkVocab(step, `${where}.aggregations[${j}].operation`, a?.operation, AGGREGATION_OPERATIONS, 'aggregation operation', 'yields null for that aggregate', diags);
5024
- }
5025
- return;
5026
- }
5027
- at('TRANSFORM_UNKNOWN_OP', `Transform "${step}" ${where} has type ${JSON.stringify(kind)}. The runtime skips an operation it does `
5028
- + `not recognize, so the collection would pass through unchanged. Use 'filter', 'map', or 'groupBy'.`);
5029
- });
5030
- }
5031
- /**
5032
- * Diagnose the `variant` discriminator on a transform step.
5033
- *
5034
- * A variant emits a single-purpose node type (`core.action.transform.filter`,
5035
- * `.map`, `.group-by`) instead of the generic one. The platform runs all four
5036
- * node types through the SAME script generators and dispatches on the
5037
- * OPERATION's type, never the node's — so a mismatch here does not fault and
5038
- * does not change the answer. It produces a node that lies: a `.filter` node
5039
- * that actually maps, whose operation the designer's filter editor cannot even
5040
- * show. That is exactly the class this SDK catches at rung 1, because no rung
5041
- * below it ever will.
5042
- */
5043
- function checkVariant(step, inputs, ops, at) {
5044
- const variant = inputs?.variant;
5045
- if (variant === undefined || variant === null)
5046
- return; // generic — chains freely
5047
- const variants = Object.keys(VARIANT_OPERATION);
5048
- if (typeof variant !== 'string' || !variants.includes(variant)) {
5049
- at('TRANSFORM_VARIANT_UNKNOWN', `Transform "${step}" has variant ${JSON.stringify(variant)}, which is not a Transform node type. `
5050
- + `The emitted node would be "core.action.transform.${String(variant)}", which no runtime resolves. `
5051
- + `Valid: ${variants.join(', ')} — or omit \`variant\` for the generic chaining Transform.`, nearestIn(variant, variants));
5052
- return;
5053
- }
5054
- const expected = VARIANT_OPERATION[variant];
5055
- if (ops.length !== 1) {
5056
- at('TRANSFORM_VARIANT_ONE_OP', `Transform "${step}" is the "${variant}" variant but carries ${ops.length} operations. A variant node runs `
5057
- + `exactly one — the designer's ${variant} editor shows a single ${expected} operation and nothing else. `
5058
- + `Drop \`variant\` to chain them in one generic Transform, or split them across one node per step.`);
5059
- return;
5060
- }
5061
- const got = ops[0]?.type;
5062
- if (got !== expected) {
5063
- at('TRANSFORM_VARIANT_OP_MISMATCH', `Transform "${step}" is the "${variant}" variant but its operation is ${JSON.stringify(got)}, not `
5064
- + `"${expected}". The node type does not drive the runtime — the OPERATION does — so this would emit a `
5065
- + `node labelled "${variant}" that actually does ${JSON.stringify(got)}, and the designer's ${variant} `
5066
- + `editor could not show it. Match them, or drop \`variant\` for the generic Transform.`, typeof got === 'string' && Object.values(VARIANT_OPERATION).includes(got)
5067
- ? `variant: '${variants.find((v) => VARIANT_OPERATION[v] === got)}'`
5068
- : `type: '${expected}'`);
5069
- }
5070
- }
5071
- /** Nearest member of a closed vocabulary, for a "did you mean" suggestion. */
5072
- function nearestIn(value, vocab) {
5073
- const v = typeof value === 'string' ? value.toLowerCase() : '';
5074
- return vocab.find((c) => c.replace(/[-_]/g, '') === v.replace(/[^a-z]/g, ''))
5075
- ?? vocab.find((c) => c.startsWith(v) || (v !== '' && v.startsWith(c)));
5076
- }
5077
- /** A string that is an expression the transform runtime will NOT evaluate. */
5078
- const LOOKS_LIKE_EXPRESSION = /^(=js:|\$vars\.|\{\s*\$vars\.)/;
5079
- // The three closed vocabularies a transform operation carries. TypeScript types
5080
- // them, but an authored `.flow.ts` runs with types ERASED — so `condition:
5081
- // 'greater'` reaches the runtime, whose generated script falls through on a name
5082
- // it does not know and is silent about it:
5083
- // condition → the rule passes every item
5084
- // transformation → the value is copied unchanged
5085
- // aggregation → the aggregate is null
5086
- // Green at every rung, wrong answer. Checked here so the author hears it at
5087
- // rung 1 — a flow that never went through this SDK gets no such warning.
5088
- const FILTER_CONDITIONS = [
5089
- 'equals', 'not_equals',
5090
- 'greater_than', 'less_than', 'greater_equal', 'less_equal',
5091
- 'contains', 'not_contains',
5092
- 'starts_with', 'not_starts_with',
5093
- 'ends_with', 'not_ends_with',
5094
- 'matches_regex',
5095
- 'in', 'not_in',
5096
- 'is_null', 'is_not_null',
5097
- 'is_empty', 'is_not_empty',
5098
- ];
5099
- const TRANSFORMATIONS = ['copy', 'uppercase', 'lowercase', 'trim'];
5100
- const AGGREGATION_OPERATIONS = ['count', 'sum', 'average', 'min', 'max', 'first', 'last', 'collect'];
5101
- /** Flag a value that is not a member of a closed vocabulary, suggesting the nearest. */
5102
- function checkVocab(step, at, value, vocab, what, effect, diags) {
5103
- if (typeof value === 'string' && vocab.includes(value))
5104
- return;
5105
- const near = nearestIn(value, vocab);
5106
- diags.push({
5107
- level: 'error',
5108
- code: 'TRANSFORM_BAD_VOCAB',
5109
- message: `Transform "${step}" ${at} is ${JSON.stringify(value)}, which is not a valid ${what}. The runtime does not `
5110
- + `recognize it and ${effect}, so the result would be silently wrong. Valid: ${vocab.join(', ')}.`,
5111
- step,
5112
- ...(near ? { suggestion: near } : {}),
5113
- });
5114
- }
5115
4625
  /**
5116
4626
  * Flag a case value whose type cannot match the discriminant's declared type.
5117
4627
  *
@@ -5174,100 +4684,116 @@ const LOOP_FIELDS = new Set(['currentItem', 'currentIteration', 'collection', 'o
5174
4684
  * before the 2.4 default (#868); its own fields are not audited here.
5175
4685
  */
5176
4686
  const LOOP_FIELDS_1_0_0 = new Set(['currentItem', 'currentIndex', 'collection', 'output', 'error']);
5177
- /**
5178
- * Columns Data Fabric assigns and never accepts a write for.
5179
- *
5180
- * The canvas hides them from both write editors, but the serializer has no
5181
- * guard: a hand-authored `Id` row satisfies the node's own "at least one field"
5182
- * rule, ships in the body, and is rejected — and the rejection is only logged,
5183
- * so the row comes back unchanged and the run goes green.
5184
- */
5185
- const DATAFABRIC_SYSTEM_COLUMNS = new Set(['Id', 'CreateTime', 'CreatedBy', 'UpdateTime', 'UpdatedBy']);
5186
- function checkDataFabricColumns(step, columns, option, diags) {
5187
- const system = columns.filter((c) => DATAFABRIC_SYSTEM_COLUMNS.has(c));
5188
- if (system.length) {
5189
- diags.push({
5190
- level: 'error', code: 'DATAFABRIC_SYSTEM_COLUMN', step,
5191
- message: `"${step}" writes ${system.map((c) => JSON.stringify(c)).join(', ')} in { ${option} }. `
5192
- + 'Data Fabric assigns its system columns (Id, CreateTime, CreatedBy, UpdateTime, UpdatedBy) and rejects a '
5193
- + 'write to them — and it only LOGS the rejection, so the row comes back unchanged and the run goes green.',
5194
- });
5195
- }
4687
+ /** The registered node type for a spec's kind, or undefined (a custom FlowAction has no kind). */
4688
+ function registeredKind(spec) {
4689
+ const kind = spec?.kind;
4690
+ return typeof kind === 'string' ? FLOW_NODE_TYPES.byKind(kind, spec) : undefined;
5196
4691
  }
5197
4692
  /**
5198
- * A folder-scoped entity is portable only with its `Entity` bindings.
4693
+ * Check one step whose kind is registered: its version, its node-local rules
4694
+ * (reported through the family's catalog), and the expressions it carries.
4695
+ * Returns its placement, or undefined when the version cannot be resolved — the
4696
+ * step is then reported and not checked further.
5199
4697
  *
5200
- * `_folderKey` on its own still serializes — it falls back to a source-org
5201
- * literal and logs a warning — so the flow looks fine until it is deployed to
5202
- * another org. Refusing the half-authored pair here is the whole point: the
5203
- * alternative is a defect that only appears at deploy time, in a different org,
5204
- * to somebody else.
4698
+ * Its bindings are not checked here: unlike a contribution (`checkContribution`),
4699
+ * a registered type's bindings exist only once it serializes. Closing every
4700
+ * `<bindings.NAME>` its definition references is the node type's own contract,
4701
+ * which serialize enforces (#942) and the type's tests pin; an author cannot
4702
+ * cause the gap, so there is no author diagnostic to report.
5205
4703
  */
5206
- function checkDataFabricScope(step, inputs, diags) {
5207
- if (inputs.folderKey !== undefined && inputs.resourceKey === undefined) {
5208
- diags.push({
5209
- level: 'error', code: 'DATAFABRIC_FOLDER_NO_RESOURCE_KEY', step,
5210
- message: `"${step}" sets { folderKey } without { resourceKey }. A folder-scoped entity needs both — with only `
5211
- + 'the folder key the entity serializes as a source-org literal, which validates, runs in this org, and '
5212
- + 'breaks on deploy to another one.',
5213
- suggestion: 'resolve the key with `uip solution resources list --kind Entity`, or drop { folderKey } and keep '
5214
- + 'the entity tenant-scoped',
5215
- });
4704
+ function checkRegisteredStep(entry, s, exprs, errorSource, opts, diags, typeOf, triggerId) {
4705
+ const spec = s.spec;
4706
+ /** The step's node-local rules and expressions, as `impl` defines them. */
4707
+ const runRules = (impl) => {
4708
+ const ctx = {
4709
+ step: s.name,
4710
+ version: impl.version,
4711
+ report: (code, message, suggestion) => diags.push(catalogued(entry, s.name, code, message, suggestion)),
4712
+ declaredRef: (js) => {
4713
+ const name = declaredRefName(js, triggerId);
4714
+ return name ? { name, type: typeOf.get(name) } : undefined;
4715
+ },
4716
+ options: opts,
4717
+ };
4718
+ impl.check?.(spec, ctx);
4719
+ for (const site of impl.expressions(spec)) {
4720
+ if ('code' in site) {
4721
+ // A script body: checked for its `$vars.` refs although it is a string. No
4722
+ // errorSource, as for `script()` (collectActionExprs).
4723
+ exprs.push({ js: site.code, step: s.name });
4724
+ }
4725
+ else {
4726
+ pushDeepExprs(site.value, s.name, errorSource, exprs);
4727
+ }
4728
+ }
4729
+ return ctx;
4730
+ };
4731
+ let impl;
4732
+ try {
4733
+ impl = resolveVersion(entry.type, spec, s.options?.version, `step "${s.name}"`);
5216
4734
  }
5217
- if (inputs.resourceKey !== undefined && inputs.folderKey === undefined) {
5218
- diags.push({
5219
- level: 'error', code: 'DATAFABRIC_RESOURCE_KEY_NO_FOLDER', step,
5220
- message: `"${step}" sets { resourceKey } without { folderKey }. The pair exists to scope a FOLDER entity; a `
5221
- + 'tenant-scoped entity needs neither and serializes as a dotted literal.',
5222
- });
4735
+ catch (e) {
4736
+ // A select() that picks an unregistered version is the node type's bug, not the author's pin.
4737
+ if (isInternalSdkError(e))
4738
+ throw e;
4739
+ diags.push({ level: 'error', code: 'NODE_VERSION_UNREGISTERED', step: s.name, message: e.message });
4740
+ // Still report the step's other mistakes in this same pass, with the version it
4741
+ // would get unpinned (select(), else the default) — as decompile lifts an
4742
+ // unregistered version — so fixing the pin is not followed by a second round.
4743
+ // It stays unplaced: the pin is wrong, so no family rule may rely on it.
4744
+ runRules(resolveVersion(entry.type, spec, undefined, `step "${s.name}"`));
4745
+ return undefined;
5223
4746
  }
4747
+ const ctx = runRules(impl);
4748
+ return { entry, impl, spec, placement: { step: s.name, kind: impl.kind, spec, version: impl.version }, ctx };
5224
4749
  }
5225
- /**
5226
- * The query options, none of which the node's `inputDefinition` declares — they
5227
- * are `_`-prefixed extras, so `uip maestro flow validate` checks none of them
5228
- * and every mistake here is silent at run time.
5229
- */
5230
- function checkDataFabricQuery(step, inputs, diags) {
5231
- const listOnly = ['limit', 'skip', 'sort', 'fields'].filter((k) => inputs[k] !== undefined);
5232
- if (listOnly.length && inputs.resultMode !== 'multiple') {
5233
- diags.push({
5234
- level: 'error', code: 'DATAFABRIC_QUERY_OPTS_WITHOUT_MULTIPLE', step,
5235
- message: `"${step}" sets { ${listOnly.join(', ')} }, which only a list query uses, but its resultMode is `
5236
- + `${inputs.resultMode ? JSON.stringify(inputs.resultMode) : 'unset (a single-record read)'}.`,
5237
- suggestion: "resultMode: 'multiple'",
5238
- });
4750
+ /** Every non-literal Expr reachable in `value`, as expressions to check. */
4751
+ function pushDeepExprs(value, step, errorSource, out) {
4752
+ if (value instanceof Expr) {
4753
+ if (!value.literal)
4754
+ out.push({ js: value.js, step, errorSource });
5239
4755
  }
5240
- // The emitted query ALWAYS carries a limit — omitted it resolves to 100 — and
5241
- // 1000 is a hard ceiling that TRUNCATES rather than erroring above it.
5242
- if (inputs.limit !== undefined && (!Number.isInteger(inputs.limit) || inputs.limit < 1 || inputs.limit > 1000)) {
5243
- diags.push({
5244
- level: 'error', code: 'DATAFABRIC_LIMIT_RANGE', step,
5245
- message: `"${step}": limit must be a whole number between 1 and 1000 (got ${inputs.limit}). 1000 is a hard `
5246
- + 'ceiling — a larger value truncates silently, so page with { skip } instead.',
5247
- });
4756
+ else if (Array.isArray(value)) {
4757
+ for (const v of value)
4758
+ pushDeepExprs(v, step, errorSource, out);
5248
4759
  }
5249
- // `in` is honoured only on a list query; on a single read the row compiles
5250
- // away, and a read whose filters compiled away returns EVERY record.
5251
- if (inputs.resultMode !== 'multiple' && (inputs.filters ?? []).some((f) => f.operator === 'in')) {
5252
- diags.push({
5253
- level: 'error', code: 'DATAFABRIC_IN_REQUIRES_MULTIPLE', step,
5254
- message: `"${step}" filters with the 'in' operator, which is honoured only when resultMode is 'multiple'. `
5255
- + 'On a single-record read that row compiles away — and a read whose filters compiled away matches every record.',
5256
- });
4760
+ else if (value && typeof value === 'object') {
4761
+ for (const v of Object.values(value))
4762
+ pushDeepExprs(v, step, errorSource, out);
5257
4763
  }
5258
- // A filter expression switched on and left blank is refused by the serializer,
5259
- // and it refuses the WHOLE query rather than dropping the row, which strands
5260
- // every downstream reference to this step.
5261
- for (const f of inputs.filters ?? []) {
5262
- const blank = f.value instanceof Expr ? f.value.js.trim() === '' : String(f.value ?? '').trim() === '';
5263
- if (blank) {
5264
- diags.push({
5265
- level: 'error', code: 'DATAFABRIC_FILTER_BLANK_VALUE', step,
5266
- message: `"${step}" has a filter on ${JSON.stringify(f.field)} with a blank value. The serializer refuses the `
5267
- + 'ENTIRE query rather than dropping the row, so the step emits nothing and every downstream $vars read of '
5268
- + 'it breaks.',
5269
- });
5270
- }
4764
+ }
4765
+ /** Whether `e` is one of the SDK's own `internal SDK error:` throws, which `check` never turns into a diagnostic. */
4766
+ function isInternalSdkError(e) {
4767
+ return e instanceof Error && e.message.startsWith('internal SDK error:');
4768
+ }
4769
+ /** A diagnostic a registered node type reports, at its family catalog's severity. */
4770
+ function catalogued(entry, step, code, message, suggestion) {
4771
+ const known = Object.hasOwn(entry.family.diagnostics, code) ? entry.family.diagnostics[code] : undefined;
4772
+ if (!known) {
4773
+ throw new Error(`internal SDK error: node family "${entry.family.id}" reports ${code}, which its diagnostics catalog does not list.`);
4774
+ }
4775
+ // Key order as the per-family rules always wrote it — `check --json` prints it.
4776
+ return { level: known.severity, code, step, message, ...(suggestion !== undefined ? { suggestion } : {}) };
4777
+ }
4778
+ /** Run each registered family's own rules over all of its placements in this flow. */
4779
+ function checkRegisteredFamilies(steps, unresolved, stepNames, diags) {
4780
+ const byFamily = new Map();
4781
+ for (const placed of steps.values()) {
4782
+ const list = byFamily.get(placed.entry.family.id) ?? [];
4783
+ list.push(placed);
4784
+ byFamily.set(placed.entry.family.id, list);
4785
+ }
4786
+ for (const placed of byFamily.values()) {
4787
+ const family = placed[0].entry.family;
4788
+ if (!family.check)
4789
+ continue;
4790
+ const ctx = {
4791
+ report: (step, code, message, suggestion) => diags.push(catalogued(placed[0].entry, step, code, message, suggestion)),
4792
+ hasStep: (step) => stepNames.has(step),
4793
+ placementOf: (step) => steps.get(step)?.placement,
4794
+ versionUnresolved: (step) => unresolved.has(step),
4795
+ };
4796
+ family.check(placed.map((p) => p.placement), ctx);
5271
4797
  }
5272
4798
  }
5273
4799
  function collectActionExprs(s, out, errorSource) {
@@ -5294,20 +4820,6 @@ function collectActionExprs(s, out, errorSource) {
5294
4820
  push(val);
5295
4821
  return;
5296
4822
  }
5297
- if (s.spec.kind === 'script') {
5298
- // A script body is a plain STRING, not an Expr — so it never went through
5299
- // this check, and a bad `$vars.…` ref inside one used to surface only rungs
5300
- // later, as a type error about generated code (flow-builder-sdk#20). The body
5301
- // isn't parsed; the `$vars.<name>` refs it contains are checked like any other.
5302
- out.push({ js: s.spec.inputs.code, step: s.name });
5303
- return;
5304
- }
5305
- if (s.spec.kind === 'transform') {
5306
- // The only expression a transform carries is its collection; everything
5307
- // else is a literal or a field name (see checkTransform).
5308
- push(s.spec.inputs.collection);
5309
- return;
5310
- }
5311
4823
  if (s.spec.kind === 'hitl') {
5312
4824
  // A human task's only expressions are the values its shown fields display.
5313
4825
  // Without this a typo'd reference inside a form field would reach the
@@ -5326,30 +4838,6 @@ function collectActionExprs(s, out, errorSource) {
5326
4838
  push(val);
5327
4839
  return;
5328
4840
  }
5329
- if (s.spec.kind === 'dataFabricRead' || s.spec.kind === 'dataFabricCreate'
5330
- || s.spec.kind === 'dataFabricUpdate' || s.spec.kind === 'dataFabricDelete') {
5331
- // Every value a Data Fabric step carries is authored against flow scope: a
5332
- // filter's comparison value, the record id, the columns written. The family
5333
- // was missing here entirely, so a `$vars.` typo in any of them travelled to
5334
- // the platform unchecked — and the delete verb's no-output rule below has
5335
- // nothing to fire on until these are collected.
5336
- if (s.spec.kind === 'dataFabricRead') {
5337
- for (const f of s.spec.inputs.filters ?? [])
5338
- push(f.value);
5339
- return;
5340
- }
5341
- if (s.spec.kind === 'dataFabricCreate') {
5342
- for (const val of Object.values(s.spec.inputs.values ?? {}))
5343
- push(val);
5344
- return;
5345
- }
5346
- push(s.spec.inputs.record?.byId);
5347
- if (s.spec.kind === 'dataFabricUpdate') {
5348
- for (const val of Object.values(s.spec.inputs.set ?? {}))
5349
- push(val);
5350
- }
5351
- return;
5352
- }
5353
4841
  if (s.spec.kind === 'connector') {
5354
4842
  // Same gap on the connector surface: `messageToSend: tmpl`…${v('typo')}``
5355
4843
  // reached the runtime unchecked.
@@ -5376,6 +4864,17 @@ function collectActionExprs(s, out, errorSource) {
5376
4864
  const PER_TENANT_ERROR_PORT_KINDS = new Set([
5377
4865
  'connector', 'rpaWorkflow', 'apiWorkflow', 'agenticProcess', 'agent', 'ixpExtract',
5378
4866
  ]);
4867
+ /**
4868
+ * The ONERROR_NO_ERROR_PORT sentence for a typed step — a built-in family or a
4869
+ * registered node type — whose node type has no error port. One spelling for
4870
+ * both, so porting a family onto the registry changes no message.
4871
+ */
4872
+ function typedNoErrorPortMessage(stepName, kind, def) {
4873
+ const nodeType = def ? ` (${def.nodeType}@${def.version})` : '';
4874
+ return `Step "${stepName}" is a ${kind} step${nodeType}, and that node type has no error port — its definition does not `
4875
+ + `set supportsErrorHandling, so the edge would leave a handle the node does not have. Handle the failure `
4876
+ + `downstream instead (a delay cannot fail; a wait resumes or does not).`;
4877
+ }
5379
4878
  /** The five fields the platform's error envelope declares — no more, no less. */
5380
4879
  // The five the definitions declare, plus the two every probed runtime envelope
5381
4880
  // carried anyway (`response`, `element`) — see ErrorEnvelopeField in core/expr.ts.
@@ -5385,7 +4884,9 @@ const ERROR_ENVELOPE_FIELDS = ['code', 'message', 'detail', 'category', 'status'
5385
4884
  * mistakes at build time (no previous step, two handlers on one node, an empty
5386
4885
  * handler); these are the ones only a checker can see.
5387
4886
  */
5388
- function checkOnError(_handler, step, diags) {
4887
+ function checkOnError(_handler, step, diags,
4888
+ /** The step's registry record, when its kind is registered and its version resolved. */
4889
+ placed) {
5389
4890
  // The builder refuses a handler with no eligible step before it, so a missing
5390
4891
  // source here means a hand-built `BuiltFlow`; there is nothing to say about it.
5391
4892
  if (!step)
@@ -5439,6 +4940,35 @@ function checkOnError(_handler, step, diags) {
5439
4940
  return;
5440
4941
  }
5441
4942
  const spec = step.spec;
4943
+ // A registered node type: a bundled one asks its definition at the version it
4944
+ // will be emitted at; a per-tenant one answers for itself (its errorPort).
4945
+ if (registeredKind(spec)) {
4946
+ // No record: the version did not resolve, which NODE_VERSION_UNREGISTERED
4947
+ // already reports where the step is checked.
4948
+ if (!placed)
4949
+ return;
4950
+ const { impl } = placed;
4951
+ if (impl.definition === 'per-tenant') {
4952
+ const port = typeof impl.errorPort === 'function' ? impl.errorPort(spec) : impl.errorPort === true;
4953
+ if (!port) {
4954
+ diags.push({
4955
+ level: 'error', code: 'ONERROR_NO_ERROR_PORT', step: step.name,
4956
+ message: declaredErrorPortRefusal(step.name, impl.typeName, impl.version),
4957
+ });
4958
+ return;
4959
+ }
4960
+ }
4961
+ else {
4962
+ const def = bundledDefinition(impl.typeName, impl.version);
4963
+ if (def !== undefined && !definitionHasErrorPort(def)) {
4964
+ diags.push({ level: 'error', code: 'ONERROR_NO_ERROR_PORT', message: typedNoErrorPortMessage(step.name, impl.kind, def), step: step.name });
4965
+ return;
4966
+ }
4967
+ }
4968
+ // The port exists; the node type may still refuse the handler (plain HTTP's rule, once ported).
4969
+ impl.checkErrorHandler?.(placed.spec, placed.ctx);
4970
+ return;
4971
+ }
5442
4972
  // A typed family with a bundled definition: ask the definition serialize will
5443
4973
  // emit, at the version it will emit (#1011). For instance `voiceAgent()`
5444
4974
  // defaults to uipath.agent.voice@1.0 (no error port) and `{ version: '1.1' }`
@@ -5451,13 +4981,10 @@ function checkOnError(_handler, step, diags) {
5451
4981
  ? bundledDef !== undefined && !definitionHasErrorPort(bundledDef)
5452
4982
  : !PER_TENANT_ERROR_PORT_KINDS.has(spec.kind);
5453
4983
  if (noPort) {
5454
- const nodeType = bundledDef ? ` (${bundledDef.nodeType}@${bundledDef.version})` : '';
5455
4984
  diags.push({
5456
4985
  level: 'error',
5457
4986
  code: 'ONERROR_NO_ERROR_PORT',
5458
- message: `Step "${step.name}" is a ${spec.kind} step${nodeType}, and that node type has no error port — its definition does not `
5459
- + `set supportsErrorHandling, so the edge would leave a handle the node does not have. Handle the failure `
5460
- + `downstream instead (a delay cannot fail; a wait resumes or does not).`,
4987
+ message: typedNoErrorPortMessage(step.name, spec.kind, bundledDef),
5461
4988
  step: step.name,
5462
4989
  });
5463
4990
  return;