@uipath/maestro-builder-sdk 6.68.6 → 6.69.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.
@@ -45,7 +45,8 @@ export function externalEventRefusal(owner, mode, event) {
45
45
  return undefined;
46
46
  return (`${owner} is on a "hybrid" object, which answers through an event: pass the event the object reports as ` +
47
47
  `event: { operation, mode } (for example { operation: "TRIGGER_CREATED", mode: "polling" }, ` +
48
- `the "operation" and "eventMode" rows of the case spec result's caseShape.context). Without it the task has nothing to wait on.`);
48
+ `the "operation" and "eventMode" rows of the case spec result's caseShape.context), or prepare the connector so the ` +
49
+ `library records it. Without it the task has nothing to wait on.`);
49
50
  }
50
51
  if (mode === 'sync')
51
52
  return `${owner} is on a "sync" object, which answers inline and has no event; drop event, or fix mode.`;
@@ -79,6 +80,83 @@ export function externalServiceType(kind, mode) {
79
80
  return sync ? CASE_FORMAT_PROFILE.externalAgentSyncServiceType : CASE_FORMAT_PROFILE.externalAgentAsyncServiceType;
80
81
  return sync ? CASE_FORMAT_PROFILE.externalWorkflowSyncServiceType : CASE_FORMAT_PROFILE.externalWorkflowAsyncServiceType;
81
82
  }
83
+ /** Check's and compile's code when an authored `mode` disagrees with the library. */
84
+ export const EXTERNAL_TASK_MODE_MISMATCH = 'EXTERNAL_TASK_MODE_MISMATCH';
85
+ /**
86
+ * The execution facts the connector library records for the object
87
+ * (`operation.executionType` and `operation.executionEvent`, from
88
+ * `registry build-library` / `registry prepare`, #1369). A value the SDK does
89
+ * not know is ignored, as if it were absent.
90
+ */
91
+ export function libraryExecution(operation) {
92
+ const op = (operation ?? {});
93
+ const mode = isExternalExecutionMode(op.executionType) ? op.executionType : undefined;
94
+ const raw = op.executionEvent;
95
+ const event = mode !== undefined && mode !== 'sync' && typeof raw?.operation === 'string' && raw.operation
96
+ && isExternalEventMode(raw.mode)
97
+ ? { operation: raw.operation, mode: raw.mode }
98
+ : undefined;
99
+ return { ...(mode ? { mode } : {}), ...(event ? { event } : {}) };
100
+ }
101
+ /**
102
+ * The mode and event an external task is written with: what the author passed,
103
+ * else what the connector library records for the object. `check` and
104
+ * `serialize` both call this, so they refuse the same tasks with the same text.
105
+ *
106
+ * - `mode` comes from the author or the library. With neither, the task is
107
+ * refused (EXTERNAL_TASK_MODE): a guessed default would compile an `async`
108
+ * object to the sync contract without a word (#1370 was closed for that).
109
+ * - An authored `mode` the library contradicts is refused
110
+ * (EXTERNAL_TASK_MODE_MISMATCH): the plan would run the wrong contract.
111
+ * - `event` comes from the author or, for an `async` / `hybrid` object, the
112
+ * library; an authored one the library contradicts is refused
113
+ * (EXTERNAL_TASK_EVENT), and so is any pair {@link externalEventRefusal} refuses.
114
+ *
115
+ * @param owner - The task as the message names it, e.g. `External Workflow task "Run"`.
116
+ * @param authored - The task's authored `mode` and `event`.
117
+ * @param operation - The resolved library entry's `operation`, or `undefined`
118
+ * when no library is at hand (model-only `check`, which then judges only what
119
+ * was authored and reports no missing mode).
120
+ * @returns The mode and event, or the refusal.
121
+ */
122
+ export function externalExecution(owner, authored, operation) {
123
+ if (authored.mode !== undefined && !isExternalExecutionMode(authored.mode))
124
+ return {
125
+ code: 'EXTERNAL_TASK_MODE',
126
+ message: `${owner} mode must be the connector object's executionType: "sync", "async" or "hybrid".`,
127
+ };
128
+ const library = operation === undefined ? {} : libraryExecution(operation);
129
+ if (authored.mode !== undefined && library.mode !== undefined && authored.mode !== library.mode)
130
+ return {
131
+ code: EXTERNAL_TASK_MODE_MISMATCH,
132
+ message: `${owner} has mode "${authored.mode}", but the connector library records the object's executionType as ` +
133
+ `"${library.mode}"; the plan would run the wrong contract. Drop mode to use the library's, or fix it.`,
134
+ };
135
+ const mode = authored.mode ?? library.mode;
136
+ if (mode === undefined) {
137
+ if (operation === undefined)
138
+ return undefined; // model-only check: the library decides
139
+ return {
140
+ code: 'EXTERNAL_TASK_MODE',
141
+ message: `${owner} has no mode, and the connector library records no executionType for the object. ` +
142
+ `Pass mode: the object's executionType from its case spec result ` +
143
+ `(Data.EssentialConfiguration.essentialConfiguration.executionType), or rebuild the library with registry prepare.`,
144
+ };
145
+ }
146
+ const authoredEvent = authored.event;
147
+ if (authoredEvent !== undefined && library.event !== undefined
148
+ && (authoredEvent?.operation !== library.event.operation || authoredEvent?.mode !== library.event.mode))
149
+ return {
150
+ code: EXTERNAL_TASK_EVENT,
151
+ message: `${owner} has event ${JSON.stringify(authoredEvent)}, but the connector library records the object's event as ` +
152
+ `${JSON.stringify(library.event)}. Drop event to use the library's, or fix it.`,
153
+ };
154
+ const event = authoredEvent ?? (mode !== 'sync' ? library.event : undefined);
155
+ const refused = externalEventRefusal(owner, mode, event);
156
+ if (refused)
157
+ return { code: EXTERNAL_TASK_EVENT, message: refused };
158
+ return { mode, ...(event !== undefined ? { event } : {}) };
159
+ }
82
160
  /** Check's code for {@link externalDescriptorRefusal}. */
83
161
  export const EXTERNAL_TASK_DESCRIPTOR_CONTRACT = 'EXTERNAL_TASK_DESCRIPTOR_CONTRACT';
84
162
  /**
@@ -1,13 +1,59 @@
1
1
  import { Library } from '../core/library.js';
2
- import { Bindings } from '../core/bindings.js';
2
+ import { type BindingsView } from '../core/binding-messages.js';
3
3
  import { type CaseJsonPreservation } from './preserve.js';
4
4
  import type { BuiltCase } from './case-sdk.js';
5
+ /** A refusal a collect-mode serialize reported instead of throwing (see {@link CaseSerializeOpts.collect}). */
6
+ export interface CaseCollectedRefusal {
7
+ code: string;
8
+ /** The sentence an ordinary compile refuses with. */
9
+ message: string;
10
+ /** What to write instead, when the refusal knows ({@link CompileRefusal.suggestion}). */
11
+ suggestion?: string;
12
+ /**
13
+ * What the refusal is about, as `check` names it: a task's name, a stage's
14
+ * label, a trigger's label. Absent for a scope-level refusal.
15
+ */
16
+ where?: string;
17
+ /** The id of the node `where` names (the task, stage or trigger node). */
18
+ nodeId?: string;
19
+ }
5
20
  /** Optional wiring for connector tasks — the library that resolves a connector
6
21
  * op, and the bindings that map its symbolic connection/folder to resources. */
7
22
  export interface CaseSerializeOpts {
8
23
  library?: Library;
9
- bindings?: Bindings;
24
+ /** The project's bindings.json (`Bindings`), or any view that answers the label rules. */
25
+ bindings?: BindingsView;
26
+ /**
27
+ * Collect mode, for `check` (docs/CHECK_COMPILE_CONTRACT_DESIGN.md §3.5, phase 8).
28
+ *
29
+ * When given, a {@link CompileRefusal} that escapes one node's emission is
30
+ * reported here instead of thrown, and emission goes on, so every later node
31
+ * is judged too. The nodes with their own catch are a trigger, a task, and
32
+ * each stage entry condition, exit condition and SLA (reported on its stage).
33
+ * A refused trigger or task is replaced by an opaque stand-in: its id, type
34
+ * and name, and `data: { standIn: true, spec: <the task as authored> }`, with
35
+ * nothing it had added to the root bindings. A throw that is no refusal is
36
+ * reported the same way, as COMPILE_FAILED, which is what `compile` reports.
37
+ * A refusal that names no one node (two stages with one label, a case exit
38
+ * rule, a variable default, a layout or preservation entry) is scope-level:
39
+ * it is reported with no `where`, and collection stops there, so `serialize`
40
+ * returns `undefined`. An `internal SDK error:` throw is never caught. The
41
+ * output is for nothing to read: no caller writes it.
42
+ *
43
+ * For instance, a case whose connector task "Notify" names an input the
44
+ * operation does not declare, and whose stage "Review" exits to a stage that
45
+ * does not exist, reports both, where an ordinary compile stops at the first.
46
+ */
47
+ collect?: (refusal: CaseCollectedRefusal) => void;
10
48
  }
49
+ /**
50
+ * Serialize `built` to a caseplan. With {@link CaseSerializeOpts.collect}, the
51
+ * refusals are reported instead of thrown, and the result is `undefined` when
52
+ * a scope-level refusal stopped collection.
53
+ */
54
+ export declare function serialize(built: BuiltCase, opts: CaseSerializeOpts & {
55
+ collect: NonNullable<CaseSerializeOpts['collect']>;
56
+ }): Record<string, unknown> | undefined;
11
57
  export declare function serialize(built: BuiltCase, opts?: CaseSerializeOpts): Record<string, unknown>;
12
58
  export declare const CASE_NODE_LAYOUT_FIELDS: readonly ["position", "style", "measured", "width", "height", "zIndex"];
13
59
  /** How `applyPreservation` treats a field the target already has. */
@@ -19,7 +19,7 @@ import { bindingLabelRefusal } from '../core/binding-messages.js';
19
19
  import { resolveEventWhere } from '../core/lookups.js';
20
20
  import { resolveCaseSubscription } from './subscription.js';
21
21
  import { dedupeConditionNames } from './rule-names.js';
22
- import { EXTERNAL_TASK_DESCRIPTOR_CONTRACT, EXTERNAL_TASK_EVENT, externalDescriptorRefusal, externalEventRefusal, externalServiceType, isExternalExecutionMode, } from './external.js';
22
+ import { EXTERNAL_TASK_DESCRIPTOR_CONTRACT, externalDescriptorRefusal, externalExecution, externalServiceType, } from './external.js';
23
23
  import { CASE_FORMAT_PROFILE, CASE_SCHEMA_VERSIONS, caseVersionMismatch } from './format-profile.js';
24
24
  import { isResourceLabelRefusal, resolveResourceRef, resourceIdentity } from './resource-identity.js';
25
25
  import { ownEntry } from '../core/own-entry.js';
@@ -32,6 +32,7 @@ import { responseJsonSchema } from '../core/connector-response-schema.js';
32
32
  import { ruleShapeProblems } from './rule-options.js';
33
33
  import { CompileRefusal } from '../core/compile-refusal.js';
34
34
  import { implicitInArgBuiltTrigger, unboundInArgMessage } from './in-arg-binding.js';
35
+ import { jsonClone } from '../core/json-clone.js';
35
36
  import { taskShapeProblems } from './task-shape.js';
36
37
  import { connectorPlaceholderData, isConnectorPlaceholder } from './connector-placeholder.js';
37
38
  /** The ids a serialize pass hashed from element ids, by the document it returned. */
@@ -92,6 +93,100 @@ function conditionNamer(built) {
92
93
  };
93
94
  }
94
95
  export function serialize(built, opts = {}) {
96
+ const collect = opts.collect;
97
+ if (!collect)
98
+ return serializeCase(built, opts);
99
+ try {
100
+ return serializeCase(built, opts);
101
+ }
102
+ catch (error) {
103
+ // A node's own refusal was taken by its catch (`collected`), so one that
104
+ // gets here was thrown outside any one node: scope-level, and collection
105
+ // stops. A throw that is no refusal is compile's COMPILE_FAILED; an internal
106
+ // SDK error is passed on.
107
+ const refusal = refusalOf(error);
108
+ collect({ code: refusal.code, message: refusal.message, ...(refusal.suggestion !== undefined ? { suggestion: refusal.suggestion } : {}) });
109
+ return undefined;
110
+ }
111
+ }
112
+ /**
113
+ * What a throw is, in collect mode: a refusal as it is, and any other throw (a
114
+ * `TypeError` on a spec the serializer has no refusal for) as COMPILE_FAILED,
115
+ * which is what `compile` reports for it. An `internal SDK error:` is passed on.
116
+ */
117
+ function refusalOf(error) {
118
+ if (error instanceof CompileRefusal)
119
+ return error;
120
+ if (!(error instanceof Error) || error.message.startsWith('internal SDK error:'))
121
+ throw error;
122
+ return new CompileRefusal('COMPILE_FAILED', error.message);
123
+ }
124
+ /**
125
+ * Emit one node under collect mode's per-node catch. Outside collect mode this
126
+ * is just `emit()`. In it, a refusal escaping `emit` is reported on `where`
127
+ * (node `nodeId`), the root bindings and output ids the node had claimed are
128
+ * undone, and `standIn()` is returned in its place.
129
+ *
130
+ * The variable companions a node registered are kept, and its stand-in
131
+ * registers the rest (`standInCompanions`): they come from the outputs as
132
+ * authored, not from the part that was refused, so a preserved variable row or
133
+ * a read of one does not fail just because its writer was refused.
134
+ *
135
+ * The mark is the collections' sizes, so a node that emits cleanly costs
136
+ * nothing extra: every collection here is only added to, and Maps and Sets
137
+ * iterate in insertion order, so undoing is deleting what came after the mark.
138
+ */
139
+ function collected(resolver, where, nodeId, emit, standIn) {
140
+ const collect = resolver.collect;
141
+ if (!collect)
142
+ return emit();
143
+ const mark = {
144
+ bindingSink: resolver.bindingSink.size,
145
+ offlineBindingIds: resolver.offlineBindingIds.size,
146
+ outputIds: resolver.outputIds.size,
147
+ elementSeededIds: resolver.elementSeededIds.length,
148
+ };
149
+ try {
150
+ return emit();
151
+ }
152
+ catch (error) {
153
+ const refusal = refusalOf(error);
154
+ collect({
155
+ code: refusal.code, message: refusal.message,
156
+ ...(refusal.suggestion !== undefined ? { suggestion: refusal.suggestion } : {}),
157
+ where, nodeId,
158
+ });
159
+ const after = (keys, size) => [...keys].slice(size);
160
+ for (const k of after(resolver.bindingSink.keys(), mark.bindingSink))
161
+ resolver.bindingSink.delete(k);
162
+ for (const k of after(resolver.offlineBindingIds.keys(), mark.offlineBindingIds))
163
+ resolver.offlineBindingIds.delete(k);
164
+ for (const k of after(resolver.outputIds, mark.outputIds))
165
+ resolver.outputIds.delete(k);
166
+ resolver.elementSeededIds.length = mark.elementSeededIds;
167
+ return standIn();
168
+ }
169
+ }
170
+ /** The root companions a stand-in's authored outputs need, the way an emitted node registers them. */
171
+ function standInCompanions(resolver, outputs) {
172
+ for (const o of outputs ?? [])
173
+ if (!resolver.varCompanions.has(o.var))
174
+ resolver.varCompanions.set(o.var, { id: o.var, name: o.var, type: o.type, elementId: 'root', custom: true });
175
+ }
176
+ /**
177
+ * The value an author wrote, as plain JSON, for a stand-in. Deterministic, so
178
+ * two compiles of one unchanged task give equal stand-ins
179
+ * (docs/CHECK_COMPILE_CONTRACT_DESIGN.md D5).
180
+ */
181
+ function authoredJson(value) {
182
+ try {
183
+ return value === undefined ? {} : jsonClone(value);
184
+ }
185
+ catch {
186
+ return {};
187
+ }
188
+ }
189
+ function serializeCase(built, opts) {
95
190
  const versionMismatch = caseVersionMismatch(built);
96
191
  if (versionMismatch)
97
192
  throw new CompileRefusal('VERSION_MISMATCH', versionMismatch);
@@ -119,6 +214,7 @@ export function serialize(built, opts = {}) {
119
214
  businessEvents: new Set((built.businessEvents ?? []).map((event) => event.name)),
120
215
  conditionName: conditionNamer(built),
121
216
  elementSeededIds: [],
217
+ ...(opts.collect ? { collect: opts.collect } : {}),
122
218
  };
123
219
  // Preallocate SLA + escalation ids (before any rule is emitted) so a
124
220
  // `sla-status-change` rule can resolve the `slaId`/`escalationId` it points at.
@@ -259,7 +355,7 @@ export function serialize(built, opts = {}) {
259
355
  // outputs bound (collected during stage emission above). A declared var wins
260
356
  // on a name clash. An own-name output (`id == var`) writes no root entry: like
261
357
  // the Designer, the output row is itself the variable `=vars.<id>` reads.
262
- const variables = emitVariables(built.vars, ids, triggerNodeByRef, triggerNodes, built.triggers);
358
+ const variables = emitVariables(built.vars, ids, triggerNodeByRef, triggerNodes, built.triggers, resolver.collect);
263
359
  const inputOutputs = variables.inputOutputs;
264
360
  const declared = new Set(inputOutputs.map((e) => e.id));
265
361
  for (const [name, companion] of resolver.varCompanions)
@@ -961,7 +1057,7 @@ function reseed(value, ids) {
961
1057
  value[key] = next;
962
1058
  }
963
1059
  }
964
- function emitVariables(vars, ids, triggerNodeByRef, triggerNodes, triggers) {
1060
+ function emitVariables(vars, ids, triggerNodeByRef, triggerNodes, triggers, collect) {
965
1061
  const implicit = implicitInArgBuiltTrigger(triggers);
966
1062
  const inputs = [];
967
1063
  const outputs = [];
@@ -1019,8 +1115,9 @@ function emitVariables(vars, ids, triggerNodeByRef, triggerNodes, triggers) {
1019
1115
  // The Designer writes no other In-arg shape, and `uip maestro case validate
1020
1116
  // --strict` refuses an In-arg without its companion
1021
1117
  // (STRICT_INPUTOUTPUTS_COMPANION). Without `{ from }` the arg binds to the
1022
- // primary trigger (in-arg-binding.ts); when there is none it is refused
1023
- // (`check` reports the same code first).
1118
+ // primary trigger (in-arg-binding.ts); when there is none it is refused.
1119
+ // In collect mode each such arg is reported on its own name and left out,
1120
+ // so `check` reports every one, once, from this rule.
1024
1121
  let node;
1025
1122
  if (vd.sourceTrigger) {
1026
1123
  node = triggerNodeByRef.get(vd.sourceTrigger);
@@ -1029,8 +1126,13 @@ function emitVariables(vars, ids, triggerNodeByRef, triggerNodes, triggers) {
1029
1126
  }
1030
1127
  else if (implicit !== undefined)
1031
1128
  node = triggerNodes[implicit];
1032
- else
1033
- throw new CompileRefusal('CASE_IN_ARG_UNBOUND', unboundInArgMessage(vd.name, triggers));
1129
+ else {
1130
+ const refusal = new CompileRefusal('CASE_IN_ARG_UNBOUND', unboundInArgMessage(vd.name, triggers));
1131
+ if (!collect)
1132
+ throw refusal;
1133
+ collect({ code: refusal.code, message: refusal.message, where: vd.name });
1134
+ continue;
1135
+ }
1034
1136
  entry.elementId = node.id;
1035
1137
  inputs.push(entry);
1036
1138
  inputOutputs.push({ id: vd.name, name: vd.name, type: vd.type, elementId: node.id, ...jsonSchemaFields(vd) });
@@ -1076,55 +1178,73 @@ function emitTriggers(triggers, ids, resolver) {
1076
1178
  const list = triggers.length ? triggers : [{ kind: 'manual' }];
1077
1179
  return list.map((t, i) => {
1078
1180
  const triggerId = i === 0 ? 'trigger_1' : ids.make('trigger_', 6, `trigger:${i}:${t.name ?? t.kind}`);
1079
- const data = {};
1080
- if (i > 0)
1081
- data.parentElement = { id: 'root', type: 'case-management:root' };
1082
- if (CASE_FORMAT_PROFILE.triggerTypeVersion)
1083
- data.typeVersion = CASE_FORMAT_PROFILE.triggerTypeVersion;
1181
+ // Refused in collect mode, it is stood in, and every later node is still judged.
1084
1182
  const label = t.name ?? `Trigger ${i + 1}`;
1085
- if (CASE_FORMAT_PROFILE.triggerLabelField === 'display')
1086
- data.display = { label };
1087
- else
1088
- data.label = label;
1089
- if (t.description)
1090
- data.description = t.description;
1091
- if (t.kind === 'timer') {
1092
- data[CASE_FORMAT_PROFILE.triggerBagField] = {
1093
- timerType: 'timeCycle',
1094
- timeCycle: t.timeCycle,
1095
- serviceType: CASE_FORMAT_PROFILE.timerServiceType,
1183
+ return collected(resolver, label, triggerId, () => emitTrigger(t, i, triggerId, resolver), () => {
1184
+ standInCompanions(resolver, t.eventOutputs);
1185
+ // Labelled as an emitted trigger is, so a `.layout({ triggers })` entry still finds it.
1186
+ return {
1187
+ id: triggerId,
1188
+ [CASE_FORMAT_PROFILE.nodeTypeField]: CASE_FORMAT_PROFILE.triggerNodeType,
1189
+ data: {
1190
+ ...(CASE_FORMAT_PROFILE.triggerLabelField === 'display' ? { display: { label } } : { label }),
1191
+ standIn: true,
1192
+ spec: authoredJson(t),
1193
+ },
1096
1194
  };
1097
- }
1098
- else if (t.kind === 'event') {
1099
- // With no subscription this remains the byte-identical, serviceType-only
1100
- // placeholder. A subscription resolves through the same library/binding
1101
- // machinery as connector tasks and wait-for-connector bags.
1102
- const bag = t.eventSubscription
1103
- ? resolvedSubscriptionBag('trigger', t.eventSubscription, resolver, triggerId, `Event trigger "${label}"`)
1104
- : {
1105
- serviceType: CASE_FORMAT_PROFILE.eventServiceType,
1106
- // A placeholder that names its source object carries it the way
1107
- // `case sdd convert` does: one `context[objectName]` row.
1108
- ...(t.eventObject ? { context: [io('objectName', 'string', { value: t.eventObject })] } : {}),
1109
- };
1110
- if (t.eventOutputs?.length) {
1111
- const outputs = t.eventOutputs.map((o) => {
1112
- const field = o.source.replace(/^=/, '').split('.').pop() || o.source;
1113
- if (!resolver.varCompanions.has(o.var))
1114
- resolver.varCompanions.set(o.var, { id: o.var, name: o.var, type: o.type, elementId: 'root', custom: true });
1115
- return { name: field, var: o.var, source: o.source, type: o.type, value: o.var };
1116
- });
1117
- bag.outputs = [...(bag.outputs ?? []), ...outputs];
1118
- }
1119
- data[CASE_FORMAT_PROFILE.triggerBagField] = bag;
1120
- }
1121
- return {
1122
- id: triggerId,
1123
- [CASE_FORMAT_PROFILE.nodeTypeField]: CASE_FORMAT_PROFILE.triggerNodeType,
1124
- data,
1125
- };
1195
+ });
1126
1196
  });
1127
1197
  }
1198
+ /** One trigger node (see {@link emitTriggers}). */
1199
+ function emitTrigger(t, i, triggerId, resolver) {
1200
+ const data = {};
1201
+ if (i > 0)
1202
+ data.parentElement = { id: 'root', type: 'case-management:root' };
1203
+ if (CASE_FORMAT_PROFILE.triggerTypeVersion)
1204
+ data.typeVersion = CASE_FORMAT_PROFILE.triggerTypeVersion;
1205
+ const label = t.name ?? `Trigger ${i + 1}`;
1206
+ if (CASE_FORMAT_PROFILE.triggerLabelField === 'display')
1207
+ data.display = { label };
1208
+ else
1209
+ data.label = label;
1210
+ if (t.description)
1211
+ data.description = t.description;
1212
+ if (t.kind === 'timer') {
1213
+ data[CASE_FORMAT_PROFILE.triggerBagField] = {
1214
+ timerType: 'timeCycle',
1215
+ timeCycle: t.timeCycle,
1216
+ serviceType: CASE_FORMAT_PROFILE.timerServiceType,
1217
+ };
1218
+ }
1219
+ else if (t.kind === 'event') {
1220
+ // With no subscription this remains the byte-identical, serviceType-only
1221
+ // placeholder. A subscription resolves through the same library/binding
1222
+ // machinery as connector tasks and wait-for-connector bags.
1223
+ const bag = t.eventSubscription
1224
+ ? resolvedSubscriptionBag('trigger', t.eventSubscription, resolver, triggerId, `Event trigger "${label}"`)
1225
+ : {
1226
+ serviceType: CASE_FORMAT_PROFILE.eventServiceType,
1227
+ // A placeholder that names its source object carries it the way
1228
+ // `case sdd convert` does: one `context[objectName]` row.
1229
+ ...(t.eventObject ? { context: [io('objectName', 'string', { value: t.eventObject })] } : {}),
1230
+ };
1231
+ if (t.eventOutputs?.length) {
1232
+ const outputs = t.eventOutputs.map((o) => {
1233
+ const field = o.source.replace(/^=/, '').split('.').pop() || o.source;
1234
+ if (!resolver.varCompanions.has(o.var))
1235
+ resolver.varCompanions.set(o.var, { id: o.var, name: o.var, type: o.type, elementId: 'root', custom: true });
1236
+ return { name: field, var: o.var, source: o.source, type: o.type, value: o.var };
1237
+ });
1238
+ bag.outputs = [...(bag.outputs ?? []), ...outputs];
1239
+ }
1240
+ data[CASE_FORMAT_PROFILE.triggerBagField] = bag;
1241
+ }
1242
+ return {
1243
+ id: triggerId,
1244
+ [CASE_FORMAT_PROFILE.nodeTypeField]: CASE_FORMAT_PROFILE.triggerNodeType,
1245
+ data,
1246
+ };
1247
+ }
1128
1248
  /** A stage's task sets as the serializer writes them — one set per parallel task (`v31TaskSets`). */
1129
1249
  function laneSets(lanes) {
1130
1250
  return v31TaskSets(lanes, (task) => task.entry.some((condition) => condition.rules.some((group) => group.some((r) => r.rule === 'runs-sequentially'))));
@@ -1149,14 +1269,18 @@ function emitStage(stage, lanes, si, stageId, laneTaskIds, ids, resolver) {
1149
1269
  data.description = stage.description;
1150
1270
  if (stage.required !== undefined)
1151
1271
  data.isRequired = stage.required;
1272
+ // Each condition and SLA is judged on its own in collect mode, reported on the
1273
+ // stage; a refused one is left out, and the stage's other parts are still judged.
1274
+ const onStage = (emit) => collected(resolver, stage.label, stageId, emit, () => undefined);
1275
+ const present = (items) => items.filter((item) => item !== undefined);
1152
1276
  if (stage.entry.length || stage.exception) {
1153
- data.entryConditions = stage.entry.map((c, i) => emitEntry(c, `${stage.label}:entry:${i}`, stageId, stage.label, ids, resolver));
1277
+ data.entryConditions = present(stage.entry.map((c, i) => onStage(() => emitEntry(c, `${stage.label}:entry:${i}`, stageId, stage.label, ids, resolver))));
1154
1278
  }
1155
1279
  if (stage.exit.length || stage.exception) {
1156
- data.exitConditions = stage.exit.map((c, i) => emitExit(c, `${stage.label}:exit:${i}`, stageId, stage.label, ids, resolver));
1280
+ data.exitConditions = present(stage.exit.map((c, i) => onStage(() => emitExit(c, `${stage.label}:exit:${i}`, stageId, stage.label, ids, resolver))));
1157
1281
  }
1158
1282
  if (stage.slaRules.length) {
1159
- data.slaRules = stage.slaRules.map((s, i, all) => emitSla(s, i, all.length, ids, resolver));
1283
+ data.slaRules = present(stage.slaRules.map((s, i, all) => onStage(() => emitSla(s, i, all.length, ids, resolver))));
1160
1284
  }
1161
1285
  const nodeType = stage.exception && CASE_FORMAT_PROFILE.exceptionStageField === CASE_FORMAT_PROFILE.nodeTypeField
1162
1286
  ? CASE_FORMAT_PROFILE.exceptionStageValue
@@ -1172,8 +1296,23 @@ function emitStage(stage, lanes, si, stageId, laneTaskIds, ids, resolver) {
1172
1296
  }
1173
1297
  function emitTask(task, stageLabel, taskId, ids, resolver) {
1174
1298
  const type = task.kind === 'connector' ? CASE_FORMAT_PROFILE.connectorActivityNodeType : task.kind;
1175
- const node = { id: taskId, displayName: task.name, [CASE_FORMAT_PROFILE.nodeTypeField]: type };
1176
1299
  const stageId = resolver.stageIdByLabel.get(stageLabel);
1300
+ // Collect mode's per-task catch: a refused task is stood in, and every later
1301
+ // task is still judged.
1302
+ return collected(resolver, task.name, taskId, () => emitTaskNode(task, stageLabel, stageId, taskId, type, ids, resolver), () => {
1303
+ standInCompanions(resolver, task.outputs);
1304
+ return {
1305
+ id: taskId,
1306
+ displayName: task.name,
1307
+ [CASE_FORMAT_PROFILE.nodeTypeField]: type,
1308
+ elementId: `${stageId}-${taskId}`,
1309
+ data: { standIn: true, spec: authoredJson(task) },
1310
+ };
1311
+ });
1312
+ }
1313
+ /** One task node (see {@link emitTask}). */
1314
+ function emitTaskNode(task, stageLabel, stageId, taskId, type, ids, resolver) {
1315
+ const node = { id: taskId, displayName: task.name, [CASE_FORMAT_PROFILE.nodeTypeField]: type };
1177
1316
  node.data = taskData(task, resolver, taskId, stageId, ids);
1178
1317
  // Every task carries `elementId: "<stageId>-<taskId>"`, the identity the
1179
1318
  // converter matches a task's io rows against:
@@ -1380,10 +1519,14 @@ function isResolvedSubscription(spec) {
1380
1519
  /** Resolve a Case event/wait subscription into the designer/runtime bag pinned
1381
1520
  * by the archived v1 artifacts. Placeholder bags never enter this path. */
1382
1521
  function resolvedSubscriptionBag(kind, spec, resolver, elementId, where) {
1522
+ // The binding labels need no library, so they are judged first: a `check`
1523
+ // without one still reports them.
1524
+ refuseBindingLabels(resolver, spec, { where, target: { key: spec.connector, action: spec.event } });
1383
1525
  if (!resolver.library)
1384
1526
  throw new CompileRefusal('LIBRARY_REQUIRED', `${kind === 'trigger' ? 'event trigger' : 'wait-for-connector'} needs the connector library — `
1385
1527
  + `pass { library } to serialize or use uip maestro case compile --library.`);
1386
- const rc = resolveCaseSubscription(resolver.library, kind, spec, { where, bindings: resolver.bindings });
1528
+ const library = resolver.library;
1529
+ const rc = onTask(where, () => resolveCaseSubscription(library, kind, spec, { where, bindings: resolver.bindings }));
1387
1530
  const op = rc.entry.operation ?? {};
1388
1531
  const context = [io('connectorKey', 'string', { value: rc.connectorKey })];
1389
1532
  const target = { key: spec.connector, action: spec.event };
@@ -1399,7 +1542,7 @@ function resolvedSubscriptionBag(kind, spec, resolver, elementId, where) {
1399
1542
  return {
1400
1543
  serviceType: kind === 'trigger' ? CASE_FORMAT_PROFILE.eventServiceType : CASE_FORMAT_PROFILE.waitConnectorServiceType,
1401
1544
  context,
1402
- inputs: [eventInput(resolveEventWhere(spec.where, resolver.library.lookupResolutions, `${kind === 'trigger' ? 'event trigger' : 'wait-for-connector'} "${elementId}"`), elementId)],
1545
+ inputs: [eventInput(resolveEventWhere(spec.where, library.lookupResolutions, where), elementId)],
1403
1546
  outputs: connectorOutputs(rc, elementId, resolver),
1404
1547
  bindings: [],
1405
1548
  };
@@ -1754,9 +1897,11 @@ function businessRuleTaskData(task, resolver, elementId, ids) {
1754
1897
  * referenced as `=bindings.<id>`, with the binding entry collected at case root.
1755
1898
  */
1756
1899
  function connectorTaskData(spec, resolver, elementId, where, executionType, event) {
1900
+ refuseBindingLabels(resolver, spec, { where, target: { key: spec.key, action: spec.action } });
1757
1901
  if (!resolver.library)
1758
1902
  throw new CompileRefusal('LIBRARY_REQUIRED', `connector task needs the connector library — pass { library } to serialize or use uip maestro case compile --library.`);
1759
- const rc = resolver.library.resolve(spec.key, spec.action, spec.version, spec.object);
1903
+ const library = resolver.library;
1904
+ const rc = onTask(where, () => resolveOperation(library, spec));
1760
1905
  const op = rc.entry.operation ?? {};
1761
1906
  const context = [
1762
1907
  io('connectorKey', 'string', { value: rc.connectorKey }),
@@ -1784,11 +1929,11 @@ function connectorTaskData(spec, resolver, elementId, where, executionType, even
1784
1929
  // operation); the SPELLING of the path/query keys is Case's own: the Case
1785
1930
  // designer camelCases them (`send_as` → `sendAs`, see
1786
1931
  // `fixtures/execute-connector-activity.sample.json`), and the body is nested.
1787
- const slots = buildConnectorInputs(rc, spec.inputs ?? {}, {
1932
+ const slots = onTask(where, () => buildConnectorInputs(rc, spec.inputs ?? {}, {
1788
1933
  dialect: EQUALS_EXPR_DIALECT,
1789
- resolutions: resolver.library.lookupResolutions,
1790
- lookups: resolver.library.lookupSpecs(spec.key, spec.action, spec.version, spec.object),
1791
- });
1934
+ resolutions: library.lookupResolutions,
1935
+ lookups: library.lookupSpecs(spec.key, spec.action, spec.version, spec.object),
1936
+ }));
1792
1937
  const camelKeys = (slot) => Object.fromEntries(Object.entries(slot ?? {}).map(([name, value]) => [toCamel(name), value]));
1793
1938
  const pathParameters = camelKeys(slots.pathParameters);
1794
1939
  const queryParameters = camelKeys(slots.queryParameters);
@@ -1822,30 +1967,42 @@ function connectorTaskData(spec, resolver, elementId, where, executionType, even
1822
1967
  * "v2"`, and the sync or async service type that execution type gives.
1823
1968
  */
1824
1969
  function externalTaskData(kind, spec, resolver, elementId, where) {
1970
+ // What needs no library first, so a `check` without one still reports it: the
1971
+ // authored mode and event judged alone (`externalExecution` with no operation
1972
+ // only refuses what is wrong whatever the library says), and the binding names.
1973
+ const authored = externalExecution(where, spec, undefined);
1974
+ if (authored?.code !== undefined)
1975
+ throw new CompileRefusal(authored.code, authored.message);
1976
+ if (!spec.connector.connection?.trim())
1977
+ throw new CompileRefusal('EXTERNAL_TASK_CONNECTION', `${where} requires a non-empty connection binding name.`);
1978
+ if (!spec.connector.folder?.trim())
1979
+ throw new CompileRefusal('EXTERNAL_TASK_FOLDER', `${where} requires a non-empty folder binding name.`);
1980
+ refuseBindingLabels(resolver, spec.connector, { where, target: { key: spec.connector.key, action: spec.connector.action } });
1825
1981
  if (!resolver.library)
1826
1982
  throw new CompileRefusal('LIBRARY_REQUIRED', `${kind} task needs the connector library — pass { library } to serialize or use uip maestro case compile --library.`);
1827
- const rc = resolver.library.resolve(spec.connector.key, spec.connector.action, spec.connector.version, spec.connector.object);
1983
+ const library = resolver.library;
1984
+ const rc = onTask(where, () => resolveOperation(library, spec.connector));
1828
1985
  const refused = externalDescriptorRefusal(kind, rc);
1829
1986
  if (refused)
1830
- throw new CompileRefusal(EXTERNAL_TASK_DESCRIPTOR_CONTRACT, refused);
1831
- if (!isExternalExecutionMode(spec.mode))
1832
- throw new CompileRefusal('EXTERNAL_TASK_MODE', `${kind} task mode must be the connector object's executionType: "sync", "async" or "hybrid".`);
1833
- const eventRefused = externalEventRefusal(where, spec.mode, spec.event);
1834
- if (eventRefused)
1835
- throw new CompileRefusal(EXTERNAL_TASK_EVENT, eventRefused);
1836
- if (!spec.connector.connection || !spec.connector.folder)
1837
- throw new CompileRefusal(spec.connector.connection ? 'EXTERNAL_TASK_FOLDER' : 'EXTERNAL_TASK_CONNECTION', `${kind} task requires non-empty connection and folder binding names.`);
1838
- const data = connectorTaskData(spec.connector, resolver, elementId, where, spec.mode, spec.event);
1987
+ throw new CompileRefusal(EXTERNAL_TASK_DESCRIPTOR_CONTRACT, `${where}: ${refused}`);
1988
+ // The authored mode and event, else the library's (#1369).
1989
+ const execution = externalExecution(where, spec, rc.entry?.operation ?? {});
1990
+ if (!execution)
1991
+ throw new Error(`internal SDK error: ${where} was judged without its library operation.`);
1992
+ if (execution.code !== undefined)
1993
+ throw new CompileRefusal(execution.code, execution.message);
1994
+ const { mode, event } = execution;
1995
+ const data = connectorTaskData(spec.connector, resolver, elementId, where, mode, event);
1839
1996
  const context = data.context;
1840
1997
  const metadataAt = context.findIndex((row) => row.name === 'metadata');
1841
1998
  // The Designer's order: the event pair (when the object answers through one),
1842
1999
  // then the execution type, just before `metadata`.
1843
- context.splice(metadataAt, 0, ...(spec.event ? [
1844
- io('operation', 'string', { value: spec.event.operation }),
1845
- io('eventMode', 'string', { value: spec.event.mode }),
1846
- ] : []), io('executionType', 'string', { value: spec.mode }));
2000
+ context.splice(metadataAt, 0, ...(event ? [
2001
+ io('operation', 'string', { value: event.operation }),
2002
+ io('eventMode', 'string', { value: event.mode }),
2003
+ ] : []), io('executionType', 'string', { value: mode }));
1847
2004
  context.push(io('activityConfigurationVersion', 'string', { value: 'v2' }));
1848
- return { ...data, serviceType: externalServiceType(kind, spec.mode) };
2005
+ return { ...data, serviceType: externalServiceType(kind, mode) };
1849
2006
  }
1850
2007
  /** A context/output name/value entry (the caseplan `InputOutput` shape). */
1851
2008
  function io(name, type, extra) {
@@ -1884,6 +2041,53 @@ function offlineBindingId(resolver, seed) {
1884
2041
  }
1885
2042
  return id;
1886
2043
  }
2044
+ /**
2045
+ * A connector operation from the library. A throw `library.resolve` makes on no
2046
+ * purpose is still a refusal, CONNECTOR_RESOLVE_FAILED, the code `check` has
2047
+ * always given it (#1015).
2048
+ */
2049
+ function resolveOperation(library, spec) {
2050
+ try {
2051
+ return library.resolve(spec.key, spec.action, spec.version, spec.object);
2052
+ }
2053
+ catch (error) {
2054
+ if (error instanceof CompileRefusal || (error instanceof Error && error.message.startsWith('internal SDK error:')))
2055
+ throw error;
2056
+ throw new CompileRefusal('CONNECTOR_RESOLVE_FAILED', error instanceof Error ? error.message : String(error));
2057
+ }
2058
+ }
2059
+ /**
2060
+ * Run `fn`, naming the task or subscription site (`where`) in a refusal it throws. The library's
2061
+ * refusals (the operation, its fields) name the connector, not the task: they
2062
+ * say which task as `check` always has (`Connector task "Post": connector not
2063
+ * in library: …`).
2064
+ */
2065
+ function onTask(where, fn) {
2066
+ try {
2067
+ return fn();
2068
+ }
2069
+ catch (error) {
2070
+ // A refusal that already names the site (a BYOA refusal) is passed on as it is.
2071
+ if (!(error instanceof CompileRefusal) || error.message.startsWith(`${where}: `))
2072
+ throw error;
2073
+ throw new CompileRefusal(error.code, `${where}: ${error.message}`, error.suggestion);
2074
+ }
2075
+ }
2076
+ /**
2077
+ * Refuse a connection or folder label bindings.json cannot answer, before
2078
+ * anything that needs the library: the refusal `bindingRef` and
2079
+ * `folderBindingRef` make, so `check` reports it with no library too.
2080
+ */
2081
+ function refuseBindingLabels(resolver, labels, site) {
2082
+ for (const attribute of ['connection', 'folder']) {
2083
+ const name = labels[attribute];
2084
+ if (!name)
2085
+ continue;
2086
+ const refused = bindingLabelRefusal(resolver.bindings, name, attribute, site.where, site.target);
2087
+ if (refused)
2088
+ throw refused;
2089
+ }
2090
+ }
1887
2091
  /** Resolve a symbolic connection/folder to a root binding entry (collected once),
1888
2092
  * returning its id + resourceKey for the `=bindings.<id>` reference. */
1889
2093
  function bindingRef(resolver, name, resource, propertyAttribute, displayName, site) {