@uipath/maestro-builder-sdk 6.65.0 → 6.66.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.
@@ -23,6 +23,7 @@
23
23
  */
24
24
  import { type ExprDiagnostic } from '../core/expr-check.js';
25
25
  import type { BuiltBpmn } from './bpmn-sdk.js';
26
+ import { CompileRefusal } from '../core/compile-refusal.js';
26
27
  /** A diagnostic plus the element it was found in. */
27
28
  export interface BpmnLocatedDiagnostic extends ExprDiagnostic {
28
29
  where: string;
@@ -35,7 +36,7 @@ export declare function checkBpmnExpressions(built: BuiltBpmn): BpmnLocatedDiagn
35
36
  * `FlowBuildError`: `message` lists every problem; `diagnostics` carries them
36
37
  * structured.
37
38
  */
38
- export declare class BpmnBuildError extends Error {
39
+ export declare class BpmnBuildError extends CompileRefusal {
39
40
  readonly diagnostics: BpmnLocatedDiagnostic[];
40
41
  constructor(built: BuiltBpmn, diagnostics: BpmnLocatedDiagnostic[]);
41
42
  }
@@ -23,6 +23,7 @@
23
23
  */
24
24
  import { checkExpression } from '../core/expr-check.js';
25
25
  import { nodeDeclaredVars } from './declared-vars.js';
26
+ import { CompileRefusal } from '../core/compile-refusal.js';
26
27
  const NOUN = 'variable';
27
28
  const NAMESPACE = 'vars';
28
29
  const BINDING_NOUN = 'binding';
@@ -213,12 +214,13 @@ function connectorBindingNames(nodes) {
213
214
  * `FlowBuildError`: `message` lists every problem; `diagnostics` carries them
214
215
  * structured.
215
216
  */
216
- export class BpmnBuildError extends Error {
217
+ export class BpmnBuildError extends CompileRefusal {
217
218
  diagnostics;
218
219
  constructor(built, diagnostics) {
219
220
  const label = built.name || built.id || 'process';
220
221
  const lines = diagnostics.map((d) => ` ✗ [${d.code}] in ${d.where}: ${d.message}${d.suggestion ? ` (did you mean "${d.suggestion}"?)` : ''}`);
221
- super(`Cannot build BPMN process "${label}" — ${diagnostics.length} expression problem(s):\n${lines.join('\n')}`);
222
+ // The first problem's code; `diagnostics` carries every one.
223
+ super(diagnostics[0]?.code ?? 'EXPR_UNDECLARED_REF', `Cannot build BPMN process "${label}" — ${diagnostics.length} expression problem(s):\n${lines.join('\n')}`);
222
224
  this.name = 'BpmnBuildError';
223
225
  this.diagnostics = diagnostics;
224
226
  }
@@ -5,6 +5,7 @@ import { schemaVersionRefusal } from '../schema-version.js';
5
5
  import { bindingRequiredFields, REGISTRY_GAPS, registryType } from './typed-node.js';
6
6
  import { resolveResourceKeyPattern } from './resource-key-pattern.js';
7
7
  import { quickHumanTaskRows } from './human-task-form.js';
8
+ import { CompileRefusal } from '../core/compile-refusal.js';
8
9
  import { DATA_SERVICE_ACTIVITY_TYPE, DATA_SERVICE_CONNECTOR_KEY, dataServiceWire } from './data-service.js';
9
10
  import { MANAGED_HTTP_CONNECTION_NAME, MANAGED_HTTP_CONNECTOR_KEY, MANAGED_HTTP_FOLDER_NAME, MANAGED_HTTP_TYPE, managedHttpContext, managedHttpInputs, managedHttpOutputs, } from './managed-http.js';
10
11
  import { ownEntry } from '../core/own-entry.js';
@@ -14,6 +15,10 @@ export { connectorOutputVars, nodeDeclaredVars, typedNodeDeclaredVars } from './
14
15
  function normTimer(t) {
15
16
  return typeof t === 'string' ? { duration: t } : t;
16
17
  }
18
+ /** Every {@link PlainTaskElement}, for the run-time check in `.plainTask()`. */
19
+ const PLAIN_TASK_ELEMENTS = new Set([
20
+ 'bpmn:task', 'bpmn:userTask', 'bpmn:serviceTask', 'bpmn:sendTask', 'bpmn:manualTask', 'bpmn:businessRuleTask', 'bpmn:receiveTask',
21
+ ]);
17
22
  /** Bookkeeping the nesting sugar shares across every scope of one process. */
18
23
  class SugarRegistry {
19
24
  /** Flow ids the builder wrote by implication rather than from an explicit `.sequenceFlow()`. */
@@ -215,7 +220,7 @@ function toErrorDef(defs, error, where) {
215
220
  // ERROR_END_EVENT_MISSING_EXCEPTION on an end event) after a whole toolchain cycle.
216
221
  // `error: true` is the way to say "any error"; a name is the way to say which.
217
222
  if (spec.name.trim() === '') {
218
- throw new Error(`${where}: the error name is empty, so the declared \`bpmn:error\` has nothing to match on. ` +
223
+ throw new CompileRefusal('BPMN_ERROR_NAME_EMPTY', `${where}: the error name is empty, so the declared \`bpmn:error\` has nothing to match on. ` +
219
224
  'Pass `error: true` to catch any error, or a name — `error: "PaymentFailed"`, or ' +
220
225
  '`error: { name: "PaymentFailed", code: "PAYMENT_FAILED" }`.');
221
226
  }
@@ -335,10 +340,10 @@ class ScopeBuilder {
335
340
  */
336
341
  flowMode(mode) {
337
342
  if (this._isArm) {
338
- throw new Error(`.flowMode('${mode}') is not available inside an arm or handler body — those always chain in order.`);
343
+ throw new CompileRefusal('BPMN_FLOW_MODE_IN_BODY', `.flowMode('${mode}') is not available inside an arm or handler body — those always chain in order.`);
339
344
  }
340
345
  if (this._nodes.length > 0) {
341
- throw new Error(`.flowMode('${mode}') must come before the scope's first element; "${this._nodes[0].id}" is already placed.`);
346
+ throw new CompileRefusal('BPMN_FLOW_MODE_AFTER_ELEMENT', `.flowMode('${mode}') must come before the scope's first element; "${this._nodes[0].id}" is already placed.`);
342
347
  }
343
348
  this._mode = mode;
344
349
  this._scopeMode = mode;
@@ -360,7 +365,7 @@ class ScopeBuilder {
360
365
  // with no output row (the fire-and-forget Async starts) would accept it and emit
361
366
  // nothing, so the author reads a variable nothing ever writes.
362
367
  if (node.kind === 'typed' && node.outputVar !== undefined && !node.outputRows && !registryType(node.type, node.spec).output) {
363
- throw new Error(`"${node.id}" (${node.type}) publishes no output row, so outputVar "${node.outputVar}" would bind nothing. ` +
368
+ throw new CompileRefusal('BPMN_OUTPUT_VAR_NO_OUTPUT_ROW', `"${node.id}" (${node.type}) publishes no output row, so outputVar "${node.outputVar}" would bind nothing. ` +
364
369
  (node.type.endsWith('Async')
365
370
  ? 'The Async start returns before the job does: drop outputVar, or use the waiting form (async: false) to read the response.'
366
371
  : 'Drop outputVar, or spell the rows with outputRows.'));
@@ -381,15 +386,15 @@ class ScopeBuilder {
381
386
  */
382
387
  place(node, viaSugar = false) {
383
388
  if (this._closedBy !== undefined) {
384
- throw new Error(`"${node.id}" follows ${this._closedBy}, which already left this path — nothing can come after it.`);
389
+ throw new CompileRefusal('BPMN_PATH_ALREADY_LEFT', `"${node.id}" follows ${this._closedBy}, which already left this path — nothing can come after it.`);
385
390
  }
386
391
  if (this._mode === 'sequence') {
387
392
  if (isStartLike(node)) {
388
393
  if (this._isArm) {
389
- throw new Error(`startEvent "${node.id}": a body has no start event of its own — it begins where the enclosing element hands over to it.`);
394
+ throw new CompileRefusal('BPMN_START_IN_BODY', `startEvent "${node.id}": a body has no start event of its own — it begins where the enclosing element hands over to it.`);
390
395
  }
391
396
  if (this._synthesizedStart !== undefined) {
392
- throw new Error(`startEvent "${node.id}": an event sub-process has exactly one start event, and this one's is ` +
397
+ throw new CompileRefusal('BPMN_START_ALREADY_SYNTHESIZED', `startEvent "${node.id}": an event sub-process has exactly one start event, and this one's is ` +
393
398
  `synthesized as "${this._synthesizedStart}" — write what follows it instead.`);
394
399
  }
395
400
  // Explicit wins here as everywhere: a plain exit whose element already has an
@@ -399,7 +404,7 @@ class ScopeBuilder {
399
404
  // (a branch with nowhere to go), blocks a new chain.
400
405
  const dangling = this._open.filter((e) => e.condition !== undefined || e.defaultOf !== undefined || !this.hasExplicitOutgoing(e.source));
401
406
  if (dangling.length > 0) {
402
- throw new Error(`startEvent "${node.id}" begins a new chain while "${dangling.map((e) => e.source).join('", "')}" is still ` +
407
+ throw new CompileRefusal('BPMN_START_WHILE_PATH_OPEN', `startEvent "${node.id}" begins a new chain while "${dangling.map((e) => e.source).join('", "')}" is still ` +
403
408
  `open — end the current path with an end event before starting another.`);
404
409
  }
405
410
  }
@@ -408,7 +413,7 @@ class ScopeBuilder {
408
413
  // wired INTO, and its outgoing flows are the author's to write — and whatever
409
414
  // follows it is checked at close for an explicit incoming edge.
410
415
  if (GATEWAY_NODE_KINDS.has(node.kind) && !viaSugar && this._isArm) {
411
- throw new Error(`${node.kind} "${node.id}": inside a body, branch with .choose() / .fork() / .race() — ` +
416
+ throw new CompileRefusal('BPMN_GATEWAY_IN_BODY', `${node.kind} "${node.id}": inside a body, branch with .choose() / .fork() / .race() — ` +
412
417
  `a bare gateway's outgoing flows cannot be implied.`);
413
418
  }
414
419
  }
@@ -416,7 +421,7 @@ class ScopeBuilder {
416
421
  if (target) {
417
422
  if (this._mode === 'sequence' && this._started && this._open.length === 0) {
418
423
  if (this._isArm) {
419
- throw new Error(`"${node.id}" is unreachable: the path ended before it (an end event, or every arm of a branch), so nothing leads to it.`);
424
+ throw new CompileRefusal('BPMN_UNREACHABLE_IN_BODY', `"${node.id}" is unreachable: the path ended before it (an end event, or every arm of a branch), so nothing leads to it.`);
420
425
  }
421
426
  // Placed with nothing implied into it. Legal only if an explicit flow leads
422
427
  // here — which may be written later — so it is checked when the scope closes.
@@ -520,7 +525,7 @@ class ScopeBuilder {
520
525
  const clash = here.list.find((o) => o.catchAll === caught.catchAll && o.ref === caught.ref);
521
526
  if (clash !== undefined) {
522
527
  const where = here.key !== undefined ? `on "${here.key}"` : 'in this scope';
523
- throw new Error(`"${n.id}" catches the same error as "${clash.id}" ${where} ` +
528
+ throw new CompileRefusal('BPMN_DUP_ERROR_CATCH', `"${n.id}" catches the same error as "${clash.id}" ${where} ` +
524
529
  `(${caught.catchAll ? 'both catch ANY error' : `both catch "${caught.ref}"`}), so one of the two can ` +
525
530
  'never run. Catch a specific error in each, or keep the one catch-all.');
526
531
  }
@@ -530,14 +535,14 @@ class ScopeBuilder {
530
535
  }
531
536
  for (const { id, after } of this._unattached) {
532
537
  if (!this._flows.some((f) => f.target === id)) {
533
- throw new Error(`"${id}" has no incoming flow. In a sequence scope it was placed after "${after}", which does not ` +
538
+ throw new CompileRefusal('BPMN_SEQUENCE_NO_INCOMING', `"${id}" has no incoming flow. In a sequence scope it was placed after "${after}", which does not ` +
534
539
  `continue to it (a bare gateway's outgoing flows are never implied; an end event ends the path). ` +
535
540
  `Wire it with .sequenceFlow(…, "${id}"), or place it after an element that continues.`);
536
541
  }
537
542
  }
538
543
  for (const { id, reason } of this._storage.mustClose) {
539
544
  if (!this._flows.some((f) => f.source === id)) {
540
- throw new Error(`${reason} ends at "${id}", which has no outgoing flow. A non-interrupting handler runs beside a token ` +
545
+ throw new CompileRefusal('BPMN_HANDLER_PATH_NOT_CLOSED', `${reason} ends at "${id}", which has no outgoing flow. A non-interrupting handler runs beside a token ` +
541
546
  `that will reach the next statement anyway, so its path cannot rejoin there: end it with an end event, ` +
542
547
  `leave it with .goto(), or wire it with .sequenceFlow("${id}", …).`);
543
548
  }
@@ -546,11 +551,11 @@ class ScopeBuilder {
546
551
  for (const exit of this._open) {
547
552
  const who = exit.origin ?? what;
548
553
  if (exit.condition !== undefined || exit.defaultOf !== undefined) {
549
- throw new Error(`${who} has nowhere to go: nothing follows the branch in ${what}. Add the element that comes ` +
554
+ throw new CompileRefusal('BPMN_BRANCH_NOWHERE_TO_GO', `${who} has nowhere to go: nothing follows the branch in ${what}. Add the element that comes ` +
550
555
  `next, or end every arm inside its own body.`);
551
556
  }
552
557
  if (!this._flows.some((f) => f.source === exit.source)) {
553
- throw new Error(`${who} ends at "${exit.source}", which has no outgoing flow, and nothing follows it in ${what} ` +
558
+ throw new CompileRefusal('BPMN_PATH_NOT_CONTINUED', `${who} ends at "${exit.source}", which has no outgoing flow, and nothing follows it in ${what} ` +
554
559
  `to continue it. Add the element that comes next, end the path with an end event, or wire it with ` +
555
560
  `.sequenceFlow("${exit.source}", …).`);
556
561
  }
@@ -588,10 +593,10 @@ class ScopeBuilder {
588
593
  const { opts } = spec;
589
594
  const id = opts.id ?? `${activityId}_${suffix}`;
590
595
  if (this._nodes.some((n) => n.id === id)) {
591
- throw new Error(`"${activityId}" already has a boundary event "${id}"; a second .${suffix}() on one activity needs its own \`id\`.`);
596
+ throw new CompileRefusal('BPMN_DUP_BOUNDARY_ID', `"${activityId}" already has a boundary event "${id}"; a second .${suffix}() on one activity needs its own \`id\`.`);
592
597
  }
593
598
  if (opts.errorVar !== undefined && opts.payload !== undefined) {
594
- throw new Error(`.${suffix}() on "${activityId}": pass \`errorVar\` or \`payload\`, not both — both write the event's output row.`);
599
+ throw new CompileRefusal('BPMN_ERROR_VAR_AND_PAYLOAD', `.${suffix}() on "${activityId}": pass \`errorVar\` or \`payload\`, not both — both write the event's output row.`);
595
600
  }
596
601
  // Two handlers on one activity catching the SAME error: whichever the runtime picks,
597
602
  // the other is dead, and the platform rejects the pair outright
@@ -607,7 +612,7 @@ class ScopeBuilder {
607
612
  return other !== undefined && other.catchAll === caught.catchAll && other.ref === caught.ref;
608
613
  });
609
614
  if (clash !== undefined) {
610
- throw new Error(`.${suffix}() on "${activityId}" catches the same error as "${clash.id}" ` +
615
+ throw new CompileRefusal('BPMN_DUP_ERROR_CATCH', `.${suffix}() on "${activityId}" catches the same error as "${clash.id}" ` +
611
616
  `(${caught.catchAll ? 'both catch ANY error' : `both catch "${caught.ref}"`}), so one of the two can never ` +
612
617
  'run. Catch a specific error in each handler, or keep the one catch-all.');
613
618
  }
@@ -661,7 +666,7 @@ class ScopeBuilder {
661
666
  if (mustWait) {
662
667
  const first = ab._first;
663
668
  if (!first || !waitsForEvent(first)) {
664
- throw new Error(`${what}: arm ${i + 1} must begin with what it waits for — an intermediateCatchEvent (message or timer), ` +
669
+ throw new CompileRefusal('BPMN_RACE_ARM_NOT_WAIT', `${what}: arm ${i + 1} must begin with what it waits for — an intermediateCatchEvent (message or timer), ` +
665
670
  `a receiveMessage, or a waitForEvent — but begins with ${first ? `${first.kind} "${first.id}"` : 'nothing'}.`);
666
671
  }
667
672
  }
@@ -669,7 +674,7 @@ class ScopeBuilder {
669
674
  });
670
675
  const empties = exits.filter((e) => e.source === split.id);
671
676
  if (empties.length > 1) {
672
- throw new Error(`${what} has ${empties.length} arms with no body; at most one arm may be empty.`);
677
+ throw new CompileRefusal('BPMN_BRANCH_EMPTY_ARMS', `${what} has ${empties.length} arms with no body; at most one arm may be empty.`);
673
678
  }
674
679
  if (exits.length >= 2) {
675
680
  // Only when more than one arm actually reaches the continuation: a single
@@ -705,15 +710,15 @@ class ScopeBuilder {
705
710
  */
706
711
  startEvent(id, opts = {}) {
707
712
  if (opts.trigger !== undefined && opts.timer === undefined) {
708
- throw new Error(`startEvent "${id}": \`trigger\` needs \`timer\` — the schedule lives in the ` +
713
+ throw new CompileRefusal('BPMN_START_TRIGGER_NO_TIMER', `startEvent "${id}": \`trigger\` needs \`timer\` — the schedule lives in the ` +
709
714
  `bpmn:timerEventDefinition, not in the trigger extension, so a trigger without one emits an ` +
710
715
  `empty definition and fires never. \`uip maestro bpmn validate\` reports that as Valid.`);
711
716
  }
712
717
  if (opts.trigger !== undefined && opts.payload !== undefined) {
713
- throw new Error(`startEvent "${id}": \`trigger\` and \`payload\` both write the event's extension element; pass one.`);
718
+ throw new CompileRefusal('BPMN_START_TRIGGER_AND_PAYLOAD', `startEvent "${id}": \`trigger\` and \`payload\` both write the event's extension element; pass one.`);
714
719
  }
715
720
  if (opts.interrupting === false && opts.error !== undefined) {
716
- throw new Error(`startEvent "${id}": an error start is always interrupting — the throw has already ended the scope it came from.`);
721
+ throw new CompileRefusal('BPMN_ERROR_START_NOT_INTERRUPTING', `startEvent "${id}": an error start is always interrupting — the throw has already ended the scope it came from.`);
717
722
  }
718
723
  return this.push({
719
724
  kind: 'startEvent',
@@ -793,7 +798,7 @@ class ScopeBuilder {
793
798
  else if (opts.message !== undefined)
794
799
  event = { type: 'message', messageRef: this._defs.messageRef(opts.message) };
795
800
  else
796
- throw new Error(`boundaryEvent "${id}" needs one of { message, timer, error }.`);
801
+ throw new CompileRefusal('BPMN_BOUNDARY_NO_EVENT', `boundaryEvent "${id}" needs one of { message, timer, error }.`);
797
802
  return this.push({
798
803
  kind: 'boundaryEvent',
799
804
  id,
@@ -930,11 +935,16 @@ class ScopeBuilder {
930
935
  * ```
931
936
  */
932
937
  plainTask(id, opts = {}, body) {
938
+ const element = opts.element ?? 'bpmn:task';
939
+ // `element` is only type-restricted, and a `.bpmn.ts` loads without a type check.
940
+ if (!PLAIN_TASK_ELEMENTS.has(element)) {
941
+ throw new CompileRefusal('BPMN_PLAIN_TASK_ELEMENT_INVALID', `.plainTask('${id}'): element "${element}" is not a plain task element. Use one of ${[...PLAIN_TASK_ELEMENTS].join(', ')}.`);
942
+ }
933
943
  return this.pushActivity({
934
944
  kind: 'plainTask',
935
945
  id,
936
946
  name: opts.name,
937
- element: opts.element ?? 'bpmn:task',
947
+ element,
938
948
  tags: opts.tags,
939
949
  retry: opts.retry,
940
950
  errorMapping: opts.errorMapping,
@@ -952,7 +962,7 @@ class ScopeBuilder {
952
962
  ? makeConnector(a, b, c, bpmnOpts)
953
963
  : makeConnector(a, b, bpmnOpts);
954
964
  if (spec.kind !== 'connector')
955
- throw new Error('connector(): unexpected spec kind');
965
+ throw new Error('internal SDK error: connector(): unexpected spec kind');
956
966
  return this.pushActivity({
957
967
  kind: 'connector',
958
968
  id,
@@ -1013,7 +1023,7 @@ class ScopeBuilder {
1013
1023
  if (opts.connection !== undefined)
1014
1024
  return this.connectorHttp(id, opts, body);
1015
1025
  if (opts.folder !== undefined || opts.connectorKey !== undefined) {
1016
- throw new Error(`http "${id}": \`folder\` and \`connectorKey\` belong to connector mode, which \`connection\` selects — ` +
1026
+ throw new CompileRefusal('BPMN_HTTP_CONNECTOR_FIELDS_NO_CONNECTION', `http "${id}": \`folder\` and \`connectorKey\` belong to connector mode, which \`connection\` selects — ` +
1017
1027
  `pass the connection's binding name too, or drop them for a connectionless request.`);
1018
1028
  }
1019
1029
  return this.pushActivity({
@@ -1052,7 +1062,7 @@ class ScopeBuilder {
1052
1062
  const codeVar = `${id}_code`;
1053
1063
  const headersVar = `${id}_headers`;
1054
1064
  if (opts.outputVar === codeVar || opts.outputVar === headersVar) {
1055
- throw new Error(`http "${id}": outputVar "${opts.outputVar}" is the variable connector mode writes the status code or headers to. ` +
1065
+ throw new CompileRefusal('BPMN_HTTP_OUTPUT_VAR_RESERVED', `http "${id}": outputVar "${opts.outputVar}" is the variable connector mode writes the status code or headers to. ` +
1056
1066
  `Pick another name, or drop outputVar for the default "${id}_response".`);
1057
1067
  }
1058
1068
  const connection = this.dataServiceBinding(id, 'connection', opts.connection, 'ConnectionId', MANAGED_HTTP_CONNECTION_NAME);
@@ -1235,7 +1245,7 @@ class ScopeBuilder {
1235
1245
  decl.resourceKey = keyed.key;
1236
1246
  else if (decl.resourceKey !== keyed.key) {
1237
1247
  const resource = field.bindingInfo?.resource ?? 'process';
1238
- throw new Error(`${type} "${id}": its ${resource} key "${keyed.key}" is read through =bindings.${decl.id}, whose resource key is already ` +
1248
+ throw new CompileRefusal('BPMN_BINDING_RESOURCE_KEY_CONFLICT', `${type} "${id}": its ${resource} key "${keyed.key}" is read through =bindings.${decl.id}, whose resource key is already ` +
1239
1249
  `"${decl.resourceKey}" (another node's pair, or the binding's own), so the pair would name two resources. ` +
1240
1250
  `A ${resource} binding names one: declare one per node, e.g. ` +
1241
1251
  `\`.binding('${decl.id}_${id}', { resource: '${decl.resource}', propertyAttribute: '${decl.propertyAttribute}', value: '${decl.default}' })\`.`);
@@ -1266,7 +1276,7 @@ class ScopeBuilder {
1266
1276
  const { field: blockedField, ref } = resolved.blocked;
1267
1277
  const refId = ref.slice('=bindings.'.length);
1268
1278
  const resource = field.bindingInfo?.resource ?? 'process';
1269
- throw new Error(`${type} "${id}": ${blockedField} reads ${ref}, which no .binding() declares with a value before this node, ` +
1279
+ throw new CompileRefusal('BPMN_BINDING_READ_BEFORE_DECLARED', `${type} "${id}": ${blockedField} reads ${ref}, which no .binding() declares with a value before this node, ` +
1270
1280
  `so the ${resource} key \`${pattern}\` it is paired under cannot be built. Declare ` +
1271
1281
  `\`.binding('${refId}', { resource: '${resource}', propertyAttribute: '${blockedField}', value: '…' })\` first, or pass the value itself.`);
1272
1282
  }
@@ -1463,7 +1473,7 @@ class ScopeBuilder {
1463
1473
  */
1464
1474
  businessRule(id, opts, body) {
1465
1475
  if (opts.releaseKey !== undefined || opts.folderKey !== undefined) {
1466
- throw new Error(`businessRule('${id}'): a business rule is addressed by name and folder (version v3 binds the ` +
1476
+ throw new CompileRefusal('BPMN_BUSINESS_RULE_KEY_FIELDS', `businessRule('${id}'): a business rule is addressed by name and folder (version v3 binds the ` +
1467
1477
  'BusinessRule resource by `name` + `folderPath`); drop `releaseKey` / `folderKey`.');
1468
1478
  }
1469
1479
  // The default output variable is pinned rather than derived. Derived, it would come
@@ -1608,7 +1618,7 @@ class ScopeBuilder {
1608
1618
  const hasApp = opts.app !== undefined;
1609
1619
  const hasForm = opts.form !== undefined;
1610
1620
  if (hasApp === hasForm) {
1611
- throw new Error(`humanTask "${id}": pass exactly one of app (a deployed Action App) or form ({ fields, outcomes }, an inline form that needs no app).`);
1621
+ throw new CompileRefusal('BPMN_HUMAN_TASK_APP_XOR_FORM', `humanTask "${id}": pass exactly one of app (a deployed Action App) or form ({ fields, outcomes }, an inline form that needs no app).`);
1612
1622
  }
1613
1623
  if (hasForm) {
1614
1624
  const o = opts;
@@ -1616,7 +1626,7 @@ class ScopeBuilder {
1616
1626
  // outcomes (`actions`) and what the human sees (`input`).
1617
1627
  const appOnly = ['appVersion', 'actions', 'key', 'input'].filter((k) => opts[k] !== undefined);
1618
1628
  if (appOnly.length) {
1619
- throw new Error(`humanTask "${id}": ${appOnly.join(', ')} belong to an app-backed task. On an inline form, put the outcomes in form.outcomes and what the human sees in form.fields.`);
1629
+ throw new CompileRefusal('BPMN_HUMAN_TASK_APP_FIELDS_ON_FORM', `humanTask "${id}": ${appOnly.join(', ')} belong to an app-backed task. On an inline form, put the outcomes in form.outcomes and what the human sees in form.fields.`);
1620
1630
  }
1621
1631
  const rows = quickHumanTaskRows(id, this._sugar.processId, o);
1622
1632
  return this.pushActivity({
@@ -2046,16 +2056,16 @@ class ScopeBuilder {
2046
2056
  eventSubProcess(id, opts, fn) {
2047
2057
  const given = [opts.error !== undefined, opts.message !== undefined, opts.timer !== undefined].filter(Boolean).length;
2048
2058
  if (given !== 1) {
2049
- throw new Error(`eventSubProcess "${id}" needs exactly one of { error, message, timer } — that is what starts it.`);
2059
+ throw new CompileRefusal('BPMN_EVENT_SUBPROCESS_START_COUNT', `eventSubProcess "${id}" needs exactly one of { error, message, timer } — that is what starts it.`);
2050
2060
  }
2051
2061
  if (opts.error !== undefined && opts.interrupting === false) {
2052
- throw new Error(`eventSubProcess "${id}": an error start is always interrupting — the throw has already ended the scope it came from.`);
2062
+ throw new CompileRefusal('BPMN_ERROR_START_NOT_INTERRUPTING', `eventSubProcess "${id}": an error start is always interrupting — the throw has already ended the scope it came from.`);
2053
2063
  }
2054
2064
  if (opts.errorVar !== undefined && opts.errorVar !== false && opts.error === undefined) {
2055
- throw new Error(`eventSubProcess "${id}": \`errorVar\` captures an error, so it needs \`error\`.`);
2065
+ throw new CompileRefusal('BPMN_ERROR_VAR_WITHOUT_ERROR', `eventSubProcess "${id}": \`errorVar\` captures an error, so it needs \`error\`.`);
2056
2066
  }
2057
2067
  if (typeof opts.errorVar === 'string' && opts.startPayload !== undefined) {
2058
- throw new Error(`eventSubProcess "${id}": pass \`errorVar\` or \`startPayload\`, not both — both write the start event's output row.`);
2068
+ throw new CompileRefusal('BPMN_ERROR_VAR_AND_PAYLOAD', `eventSubProcess "${id}": pass \`errorVar\` or \`startPayload\`, not both — both write the start event's output row.`);
2059
2069
  }
2060
2070
  // The capture row is on by default for an error start: without a declared `Error`
2061
2071
  // the platform validator rejects every `vars.Error` read in the net. An explicit
@@ -2078,7 +2088,7 @@ class ScopeBuilder {
2078
2088
  return other !== undefined && other.catchAll === (mineRef === undefined) && other.ref === mineRef;
2079
2089
  });
2080
2090
  if (clash !== undefined) {
2081
- throw new Error(`eventSubProcess "${id}" catches the same error as "${clash.id}" in this scope ` +
2091
+ throw new CompileRefusal('BPMN_DUP_ERROR_CATCH', `eventSubProcess "${id}" catches the same error as "${clash.id}" in this scope ` +
2082
2092
  `(${mineRef === undefined ? 'both catch ANY error' : `both catch "${mineRef}"`}), so one of the ` +
2083
2093
  'two can never run. Catch a specific error in each net, or keep the one catch-all.');
2084
2094
  }
@@ -2151,13 +2161,13 @@ class ScopeBuilder {
2151
2161
  */
2152
2162
  choose(id, arms, opts = {}) {
2153
2163
  if (!arms.length)
2154
- throw new Error(`choose "${id}" has no arms.`);
2164
+ throw new CompileRefusal('BPMN_BRANCH_NO_ARMS', `choose "${id}" has no arms.`);
2155
2165
  if (arms.filter((a) => a.otherwise).length > 1) {
2156
- throw new Error(`choose "${id}" has more than one \`otherwise\` arm; at most one.`);
2166
+ throw new CompileRefusal('BPMN_CHOOSE_MANY_OTHERWISE', `choose "${id}" has more than one \`otherwise\` arm; at most one.`);
2157
2167
  }
2158
2168
  for (const a of arms) {
2159
2169
  if (a.when === undefined && !a.otherwise)
2160
- throw new Error(`choose "${id}": every arm needs \`when\` or \`otherwise: true\`.`);
2170
+ throw new CompileRefusal('BPMN_CHOOSE_ARM_NO_WHEN', `choose "${id}": every arm needs \`when\` or \`otherwise: true\`.`);
2161
2171
  }
2162
2172
  const kind = opts.inclusive ? 'inclusiveGateway' : 'exclusiveGateway';
2163
2173
  const split = { kind, id, name: opts.name };
@@ -2199,7 +2209,7 @@ class ScopeBuilder {
2199
2209
  */
2200
2210
  fork(id, arms, opts = {}) {
2201
2211
  if (!arms.length)
2202
- throw new Error(`fork "${id}" has no arms.`);
2212
+ throw new CompileRefusal('BPMN_BRANCH_NO_ARMS', `fork "${id}" has no arms.`);
2203
2213
  return this.branch({ kind: 'parallelGateway', id, name: opts.name }, 'parallelGateway', arms.map((body, i) => ({ entry: { source: id, origin: `arm ${i + 1} of fork "${id}"` }, body })), `fork "${id}"`, false, opts.join);
2204
2214
  }
2205
2215
  /**
@@ -2234,7 +2244,7 @@ class ScopeBuilder {
2234
2244
  */
2235
2245
  race(id, arms, opts = {}) {
2236
2246
  if (!arms.length)
2237
- throw new Error(`race "${id}" has no arms.`);
2247
+ throw new CompileRefusal('BPMN_BRANCH_NO_ARMS', `race "${id}" has no arms.`);
2238
2248
  return this.branch({ kind: 'eventBasedGateway', id, name: opts.name }, 'exclusiveGateway', arms.map((body, i) => ({ entry: { source: id, origin: `arm ${i + 1} of race "${id}"` }, body })), `race "${id}"`, true, opts.join);
2239
2249
  }
2240
2250
  // ── Flows ──
@@ -2487,11 +2497,11 @@ export class SubProcessBuilder extends ScopeBuilder {
2487
2497
  continue;
2488
2498
  const hasDefinition = n.event !== undefined || n.trigger !== undefined;
2489
2499
  if (opts.triggeredByEvent === true && !hasDefinition) {
2490
- throw new Error(`sub-process "${id}" is \`triggeredByEvent\`, so its start event "${n.id}" needs the event that starts it — ` +
2500
+ throw new CompileRefusal('BPMN_SUBPROCESS_START_NO_EVENT', `sub-process "${id}" is \`triggeredByEvent\`, so its start event "${n.id}" needs the event that starts it — ` +
2491
2501
  'an `error`, `message` or `timer`. `.eventSubProcess(id, { error: true }, fn)` writes that start for you.');
2492
2502
  }
2493
2503
  if (opts.triggeredByEvent !== true && hasDefinition) {
2494
- throw new Error(`sub-process "${id}" is entered by an incoming flow, so its start event "${n.id}" must not carry an event ` +
2504
+ throw new CompileRefusal('BPMN_SUBPROCESS_START_HAS_EVENT', `sub-process "${id}" is entered by an incoming flow, so its start event "${n.id}" must not carry an event ` +
2495
2505
  'definition. Drop it, or make this an event sub-process with `.eventSubProcess()`.');
2496
2506
  }
2497
2507
  }
@@ -2576,7 +2586,7 @@ export class PathBuilder extends ScopeBuilder {
2576
2586
  */
2577
2587
  goto(target) {
2578
2588
  if (this._closedBy !== undefined)
2579
- throw new Error(`.goto("${target}") follows ${this._closedBy}; an arm leaves once.`);
2589
+ throw new CompileRefusal('BPMN_PATH_ALREADY_LEFT', `.goto("${target}") follows ${this._closedBy}; an arm leaves once.`);
2580
2590
  for (const exit of this._open)
2581
2591
  this.writeImpliedFlow(exit, target);
2582
2592
  this._open = [];
@@ -2753,7 +2763,7 @@ export class BpmnBuilder extends ScopeBuilder {
2753
2763
  schemaVersion(version) {
2754
2764
  const refusal = schemaVersionRefusal(BPMN_SCHEMA_VERSIONS, version);
2755
2765
  if (refusal)
2756
- throw new Error(refusal);
2766
+ throw new CompileRefusal('SCHEMA_VERSION_UNSUPPORTED', refusal);
2757
2767
  this._metadata = { ...this._metadata, migrationVersion: version };
2758
2768
  return this;
2759
2769
  }
@@ -1,10 +1,11 @@
1
1
  import { connectorOutputVars } from './declared-vars.js';
2
2
  import { EQUALS_EXPR_DIALECT } from '../core/connector-checks.js';
3
3
  import { buildConnectorInputs, nestConnectorBody, transportHttpMethod } from '../core/connector-inputs.js';
4
+ import { CompileRefusal } from '../core/compile-refusal.js';
4
5
  export function createConnectorTask(ctx, n) {
5
6
  const { m } = ctx;
6
7
  if (!ctx.library)
7
- throw new Error(`connector task "${n.id}" needs the connector library — pass { library } to serialize or use uip maestro bpmn compile --library.`);
8
+ throw new CompileRefusal('LIBRARY_REQUIRED', `connector task "${n.id}" needs the connector library — pass { library } to serialize or use uip maestro bpmn compile --library.`);
8
9
  const rc = ctx.library.resolve(n.key, n.action, n.version, n.object);
9
10
  const outVars = connectorOutputVars(n);
10
11
  const op = (rc.entry.operation ?? {});
@@ -1,6 +1,7 @@
1
1
  import { FORM_FIELD_TYPES, formFieldJsonSchema, formFieldOptionsProblem, formFieldOptionsWhy, formFieldSettingProblems, formFieldSettingsWire, formFieldTypeHint, liftFormFieldSettings, validFormFieldTypesText, wireFormFieldType, } from '../core/form-field-type.js';
2
2
  import { outcomeIdOf } from '../core/hitl-routing.js';
3
3
  import { schemaId as stableSchemaId } from '../core/stable-id.js';
4
+ import { CompileRefusal } from '../core/compile-refusal.js';
4
5
  /** The `uipath:type` version both the converter and Studio Web write for `Actions.HITL`. */
5
6
  const QUICK_TYPE_VERSION = 'v2';
6
7
  /** The response row the app variant derives from the registry; the quick form keeps it. */
@@ -56,44 +57,44 @@ function validate(id, input) {
56
57
  const { form } = input;
57
58
  const where = `humanTask "${id}"`;
58
59
  if (!Array.isArray(form.fields))
59
- throw new Error(`${where}: form.fields must be an array.`);
60
+ throw new CompileRefusal('BPMN_FORM_FIELDS_NOT_ARRAY', `${where}: form.fields must be an array.`);
60
61
  if (!Array.isArray(form.outcomes) || form.outcomes.length === 0) {
61
- throw new Error(`${where}: the form needs at least one outcome, e.g. outcomes: ['Approve', 'Reject'].`);
62
+ throw new CompileRefusal('BPMN_FORM_NO_OUTCOMES', `${where}: the form needs at least one outcome, e.g. outcomes: ['Approve', 'Reject'].`);
62
63
  }
63
64
  const ids = new Set();
64
65
  for (const f of form.fields) {
65
66
  if (typeof f.id !== 'string' || f.id === '')
66
- throw new Error(`${where}: every form field needs an id.`);
67
+ throw new CompileRefusal('BPMN_FORM_FIELD_NO_ID', `${where}: every form field needs an id.`);
67
68
  if (ids.has(f.id))
68
- throw new Error(`${where}: form field id "${f.id}" is used twice.`);
69
+ throw new CompileRefusal('BPMN_FORM_DUP_FIELD', `${where}: form field id "${f.id}" is used twice.`);
69
70
  ids.add(f.id);
70
71
  if (f.id === OUTCOME_KEY) {
71
- throw new Error(`${where}: form field "${OUTCOME_KEY}" collides with the response's own "${OUTCOME_KEY}" — the outcome picked. Rename it, e.g. id: '${OUTCOME_KEY}Value'.`);
72
+ throw new CompileRefusal('BPMN_FORM_FIELD_OUTCOME_COLLISION', `${where}: form field "${OUTCOME_KEY}" collides with the response's own "${OUTCOME_KEY}" — the outcome picked. Rename it, e.g. id: '${OUTCOME_KEY}Value'.`);
72
73
  }
73
74
  if (!IDENTIFIER.test(f.id)) {
74
- throw new Error(`${where}: form field "${f.id}" (${f.direction}) is not a name the runtime can read back as =${f.id}. ` +
75
+ throw new CompileRefusal('BPMN_FORM_FIELD_ID_NOT_IDENTIFIER', `${where}: form field "${f.id}" (${f.direction}) is not a name the runtime can read back as =${f.id}. ` +
75
76
  `Use an identifier (letters, digits, _ or $), e.g. id: '${f.id.replace(/\W+(\w)?/g, (_m, c) => (c ?? '').toUpperCase())}'.`);
76
77
  }
77
78
  if (RESERVED_WORDS.has(f.id)) {
78
- throw new Error(`${where}: form field "${f.id}" is a reserved word, which the runtime cannot bind as a name — it would lose every answer and the outcome. Rename it, e.g. id: '${f.id}Value'.`);
79
+ throw new CompileRefusal('BPMN_FORM_FIELD_ID_RESERVED', `${where}: form field "${f.id}" is a reserved word, which the runtime cannot bind as a name — it would lose every answer and the outcome. Rename it, e.g. id: '${f.id}Value'.`);
79
80
  }
80
81
  // The SDK's own spellings only. The product also resolves platform aliases such as
81
82
  // `System.String`, but those are not `FormFieldType`s: accepting one here would let a
82
83
  // decompile lift write source that does not type-check, so such an import stays verbatim.
83
84
  if (!FORM_FIELD_TYPES.includes(String(f.type))) {
84
- throw new Error(`${where}: form field "${f.id}" has type "${String(f.type)}". Valid: ${validFormFieldTypesText()}. ${formFieldTypeHint(f.type)}`.trimEnd());
85
+ throw new CompileRefusal('BPMN_FORM_FIELD_TYPE_UNKNOWN', `${where}: form field "${f.id}" has type "${String(f.type)}". Valid: ${validFormFieldTypesText()}. ${formFieldTypeHint(f.type)}`.trimEnd());
85
86
  }
86
87
  // The checks Flow's form checker reports as errors: a picker the reviewer cannot
87
88
  // pick from, and a setting the platform's schema refuses. (Options on a field that
88
89
  // renders no picker is a warning there, and is not refused here.)
89
90
  if (formFieldOptionsProblem(f) === 'missing') {
90
- throw new Error(`${where}: form field "${f.id}" ${formFieldOptionsWhy('missing', f.type)}. Add options: [{ id: 'o1', value: 'a', label: 'A' }, …].`);
91
+ throw new CompileRefusal('BPMN_FORM_FIELD_NO_OPTIONS', `${where}: form field "${f.id}" ${formFieldOptionsWhy('missing', f.type)}. Add options: [{ id: 'o1', value: 'a', label: 'A' }, …].`);
91
92
  }
92
93
  const settings = formFieldSettingProblems(f);
93
94
  if (settings.length)
94
- throw new Error(`${where}: form field "${f.id}" sets ${settings.join('; ')}.`);
95
+ throw new CompileRefusal('BPMN_FORM_FIELD_SETTING_INVALID', `${where}: form field "${f.id}" sets ${settings.join('; ')}.`);
95
96
  if (isShown(f) && (typeof f.value !== 'string' || !f.value.startsWith('='))) {
96
- throw new Error(`${where}: form field "${f.id}" (${f.direction}) needs a value — a =-expression such as '=vars.${f.id}' — or the form shows it empty.`);
97
+ throw new CompileRefusal('BPMN_FORM_FIELD_NO_VALUE', `${where}: form field "${f.id}" (${f.direction}) needs a value — a =-expression such as '=vars.${f.id}' — or the form shows it empty.`);
97
98
  }
98
99
  }
99
100
  // One variable, one writer. Two answers landing in one variable, or an `outputs`
@@ -105,7 +106,7 @@ function validate(id, input) {
105
106
  const claim = (variable, writer) => {
106
107
  const first = writers.get(variable);
107
108
  if (first !== undefined) {
108
- throw new Error(`${where}: ${first} and ${writer} both write vars.${variable}, so one would overwrite the other. Give one a different variable.`);
109
+ throw new CompileRefusal('BPMN_FORM_DUP_VARIABLE_WRITER', `${where}: ${first} and ${writer} both write vars.${variable}, so one would overwrite the other. Give one a different variable.`);
109
110
  }
110
111
  writers.set(variable, writer);
111
112
  };
@@ -120,9 +121,9 @@ function validate(id, input) {
120
121
  form.outcomes.forEach((o, i) => {
121
122
  const name = typeof o === 'string' ? o : o?.name;
122
123
  if (typeof name !== 'string' || name.trim() === '')
123
- throw new Error(`${where}: every outcome needs a name.`);
124
+ throw new CompileRefusal('BPMN_FORM_OUTCOME_NO_NAME', `${where}: every outcome needs a name.`);
124
125
  if (names.has(name))
125
- throw new Error(`${where}: outcome "${name}" is listed twice.`);
126
+ throw new CompileRefusal('BPMN_FORM_DUP_OUTCOME', `${where}: outcome "${name}" is listed twice.`);
126
127
  names.add(name);
127
128
  // The schema's outcome ids must be unique too, and two names can reach one: an
128
129
  // explicit `id` repeated, or `Needs review` and `Needs-review`, which both derive
@@ -130,7 +131,7 @@ function validate(id, input) {
130
131
  const outcomeId = outcomeIdOf(o, i);
131
132
  const clash = outcomeIds.get(outcomeId);
132
133
  if (clash !== undefined) {
133
- throw new Error(`${where}: outcomes "${clash}" and "${name}" both have the id "${outcomeId}". Give one an explicit, different id: { name: '${name}', id: '…' }.`);
134
+ throw new CompileRefusal('BPMN_FORM_DUP_OUTCOME_ID', `${where}: outcomes "${clash}" and "${name}" both have the id "${outcomeId}". Give one an explicit, different id: { name: '${name}', id: '…' }.`);
134
135
  }
135
136
  outcomeIds.set(outcomeId, name);
136
137
  });