@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.
@@ -1,16 +1,9 @@
1
- import { businessEventProblems } from './business-event-shape.js';
2
- import { isUnboundInArg, unboundInArgMessage } from './in-arg-binding.js';
3
- import { ruleShapeProblems } from './rule-options.js';
4
1
  import { types } from '../core/expr.js';
5
- import { CASE_FORMAT_PROFILE, CASE_SCHEMA_VERSIONS, authoredCaseVersion, caseVersionMismatch } from './format-profile.js';
2
+ import { CASE_FORMAT_PROFILE, CASE_SCHEMA_VERSIONS, authoredCaseVersion } from './format-profile.js';
6
3
  import { schemaVersionRefusal } from '../schema-version.js';
7
- import { caseJsonPreservation, caseJsonPreservationErrors, preservedBindings } from './preserve.js';
8
- import { EQUALS_EXPR_DIALECT, checkBindingLabels, checkConnectorContract, checkConnectorLookups, checkConnectorSchema, } from '../core/connector-checks.js';
9
- import { checkEventResolution } from '../core/event-checks.js';
10
- import { EXTERNAL_TASK_DESCRIPTOR_CONTRACT, EXTERNAL_TASK_EVENT, externalEventRefusal, isExternalExecutionMode } from './external.js';
11
- import { isResourceLabelRefusal, resolveResourceRef } from './resource-identity.js';
12
- import { taskShapeProblems } from './task-shape.js';
4
+ import { EQUALS_EXPR_DIALECT, checkConnectorLookups, checkConnectorSchema, } from '../core/connector-checks.js';
13
5
  import { isConnectorPlaceholder } from './connector-placeholder.js';
6
+ import { serialize } from './serialize.js';
14
7
  const TYPE_NAMES = new Set(Object.values(types));
15
8
  // audit-case-coverage: rule-slot table start
16
9
  // This runtime table must equal the committed matrix's rejected cells. The audit
@@ -26,17 +19,6 @@ const ILLEGAL = [];
26
19
  */
27
20
  export function check(built, opts = {}) {
28
21
  const diags = [];
29
- // The same registry `serialize` resolves against, so the two agree on which
30
- // labels a decompiled source's preserved binding rows answer.
31
- const bindings = preservedBindings(opts.bindings, caseJsonPreservation(built));
32
- if (bindings !== opts.bindings)
33
- opts = { ...opts, bindings };
34
- for (const message of caseJsonPreservationErrors(caseJsonPreservation(built)))
35
- diags.push({
36
- level: 'error',
37
- code: 'PRESERVATION_COLLISION',
38
- message: `Invalid Case JSON preservation payload: ${message}.`,
39
- });
40
22
  if (!built.id)
41
23
  diags.push({ level: 'error', code: 'NO_ID', message: 'casePlan() requires an id.' });
42
24
  const requestedVersion = authoredCaseVersion(built);
@@ -46,10 +28,7 @@ export function check(built, opts = {}) {
46
28
  code: 'VERSION_DEPRECATED',
47
29
  message: `.version(${JSON.stringify(requestedVersion)}) is deprecated: the Case JSON schema version is declared with .schemaVersion(), which accepts only versions this build can write. This SDK writes ${CASE_FORMAT_PROFILE.writeVersion}.`,
48
30
  });
49
- // `serialize` refuses it too (`caseVersionMismatch`, one rule, one message).
50
- const mismatch = caseVersionMismatch(built);
51
- if (mismatch)
52
- diags.push({ level: 'error', code: 'VERSION_MISMATCH', message: mismatch });
31
+ // A version this build cannot write is the serializer's VERSION_MISMATCH.
53
32
  }
54
33
  // The DECLARED grammar version (`.schemaVersion()`), as opposed to the
55
34
  // deprecated `.version()` above. It is already refused at the call, so this
@@ -97,55 +76,10 @@ export function check(built, opts = {}) {
97
76
  where,
98
77
  });
99
78
  }
100
- const stageLabels = new Set();
101
- const taskNames = new Set();
102
- let selectNextStageCount = 0;
103
- for (const stage of built.stages) {
79
+ // A repeated stage label or task name, and the select-next-stage exit rules,
80
+ // are the serializer's (DUP_STAGE, DUP_TASK, SELECT_NEXT_STAGE_*).
81
+ for (const stage of built.stages)
104
82
  checkName('Stage label', stage.label, stage.label);
105
- if (stageLabels.has(stage.label))
106
- diags.push({ level: 'error', code: 'DUP_STAGE', message: `Duplicate stage label "${stage.label}".`, where: stage.label });
107
- stageLabels.add(stage.label);
108
- for (const exit of stage.exit) {
109
- if (!exit.selectNextStage)
110
- continue;
111
- selectNextStageCount += 1;
112
- if (exit.type !== 'wait-for-user')
113
- diags.push({
114
- level: 'error',
115
- code: 'SELECT_NEXT_STAGE_EXIT_TYPE',
116
- message: `Stage "${stage.label}" configures selectNextStage on an exit that is not type "wait-for-user".`,
117
- where: stage.label,
118
- });
119
- if (!exit.selectNextStage.objectName.trim())
120
- diags.push({
121
- level: 'error',
122
- code: 'SELECT_NEXT_STAGE_OBJECT_NAME',
123
- message: `Stage "${stage.label}" selectNextStage.objectName must name a Data Fabric entity.`,
124
- where: stage.label,
125
- });
126
- // Its connector's labels resolve through the same `bindingRef` as a
127
- // connector task's, so they get the same binding-label rules.
128
- const connector = exit.selectNextStage.connector;
129
- if (connector)
130
- checkBindingLabels('select-next-stage connector', connector, opts, (code, message) => diags.push({ level: 'error', code, message, where: stage.label }), { key: connector.connectorKey });
131
- }
132
- for (const lane of stage.lanes)
133
- for (const t of lane) {
134
- // Task displayNames must be unique across the case: the platform rejects
135
- // duplicates (CRITICAL), and a bare selected-tasks-completed reference
136
- // would silently mis-resolve to the first task of that name.
137
- if (taskNames.has(t.name))
138
- diags.push({
139
- level: 'error',
140
- code: 'DUP_TASK',
141
- message: `Duplicate task displayName "${t.name}" — task names must be unique across the case.`,
142
- where: `${stage.label}/${t.name}`,
143
- });
144
- taskNames.add(t.name);
145
- // Also register the qualified `Stage/Task` key so a qualified reference resolves.
146
- taskNames.add(`${stage.label}/${t.name}`);
147
- }
148
- }
149
83
  // Condition `displayName`s must be unique across the whole case — `uip maestro
150
84
  // case validate` rejects a repeat as CASE_MGMT_RULE_NAME_DUPLICATE, at error
151
85
  // severity. The namespace spans stage entry, stage exit and task entry
@@ -209,12 +143,6 @@ export function check(built, opts = {}) {
209
143
  for (const c of task.entry)
210
144
  checkConditionName(c.displayName, `task "${task.name}" entry`);
211
145
  }
212
- if (selectNextStageCount > 1)
213
- diags.push({
214
- level: 'error',
215
- code: 'SELECT_NEXT_STAGE_MULTIPLE',
216
- message: 'A case can configure selectNextStage on at most one exit because the wire contract has one case-level connector.',
217
- });
218
146
  if (built.stages.length === 0)
219
147
  diags.push({ level: 'error', code: 'NO_STAGES', message: 'Case has no stages.' });
220
148
  const hasCompletion = built.caseExit.some((c) => c.marksCaseComplete !== false);
@@ -224,35 +152,11 @@ export function check(built, opts = {}) {
224
152
  code: 'NO_CASE_COMPLETION',
225
153
  message: 'Case has no completion rule — add .completeWhen(rule(\'required-stages-completed\')).',
226
154
  });
227
- // Business events: `metadata.businessEvents` names, matched exactly by an
228
- // `api-event` rule's `businessEventType`. `uip maestro case validate` rejects a
229
- // rule naming none (BUSINESS_EVENT_NOT_SELECTED) or an undeclared one
230
- // (BUSINESS_EVENT_NOT_FOUND); both are reported here, at the rule.
231
- const businessEvents = new Set((built.businessEvents ?? []).map((event) => event.name));
232
- for (const problem of businessEventProblems(built.businessEvents ?? []))
233
- diags.push({ level: 'error', code: problem.code, message: problem.message });
234
- const refEvent = (r, where) => {
235
- if (r.rule !== 'api-event')
236
- return;
237
- if (!r.businessEvent)
238
- diags.push({
239
- level: 'error',
240
- code: 'API_EVENT_NO_BUSINESS_EVENT',
241
- message: 'Rule "api-event" names no business event, so it listens for nothing — pass { businessEvent: \'<name>\' } with a name declared by .businessEvent(\'<name>\').',
242
- where,
243
- });
244
- else if (!businessEvents.has(r.businessEvent))
245
- diags.push({
246
- level: 'error',
247
- code: 'UNKNOWN_BUSINESS_EVENT',
248
- message: `Rule "api-event" listens for business event "${r.businessEvent}", which the case does not declare — add .businessEvent(${JSON.stringify(r.businessEvent)}) to the case.`,
249
- where,
250
- });
251
- };
155
+ // The business events' shapes, a rule's shape, an `api-event` rule's event,
156
+ // and a rule's stage and task references are the serializer's
157
+ // (`businessEventProblems`, `ruleShapeProblems`, API_EVENT_NO_BUSINESS_EVENT,
158
+ // UNKNOWN_BUSINESS_EVENT, UNKNOWN_STAGE_REF, UNKNOWN_TASK_REF).
252
159
  const refPlacement = (r, slot, where) => {
253
- for (const problem of ruleShapeProblems(r))
254
- diags.push({ level: 'error', code: problem.code, message: problem.message, where });
255
- refEvent(r, where);
256
160
  const bad = ILLEGAL.find((x) => x.rule === r.rule && x.slot === slot);
257
161
  if (bad)
258
162
  diags.push({
@@ -263,28 +167,16 @@ export function check(built, opts = {}) {
263
167
  });
264
168
  };
265
169
  const refStage = (r, where) => {
266
- if ((r.rule === 'selected-stage-completed' || r.rule === 'selected-stage-exited')) {
267
- if (!r.stage || !stageLabels.has(r.stage))
268
- diags.push({ level: 'error', code: 'UNKNOWN_STAGE_REF', message: `Rule "${r.rule}" references unknown stage "${r.stage}".`, where });
269
- }
270
- if (r.rule === 'selected-tasks-completed') {
271
- if (!r.tasks?.length && !r.expression)
272
- diags.push({
273
- level: 'warning',
274
- code: 'RULE_TASKS_REQUIRED',
275
- message: 'Rule "selected-tasks-completed" has neither selected tasks nor an expression gate — pass { tasks: [...] } or use when(expression).',
276
- where,
277
- });
278
- for (const n of r.tasks ?? [])
279
- if (!taskNames.has(n))
280
- diags.push({ level: 'error', code: 'UNKNOWN_TASK_REF', message: `Rule "selected-tasks-completed" references unknown task "${n}".`, where });
281
- }
170
+ if (r.rule === 'selected-tasks-completed' && !r.tasks?.length && !r.expression)
171
+ diags.push({
172
+ level: 'warning',
173
+ code: 'RULE_TASKS_REQUIRED',
174
+ message: 'Rule "selected-tasks-completed" has neither selected tasks nor an expression gate — pass { tasks: [...] } or use when(expression).',
175
+ where,
176
+ });
282
177
  };
283
- // An In-arg with no `{ from }` binds to the primary trigger; when the case has
284
- // none, `serialize` refuses it with the same code (in-arg-binding.ts).
285
- for (const vd of built.vars)
286
- if (isUnboundInArg(vd, built.triggers))
287
- diags.push({ level: 'error', code: 'CASE_IN_ARG_UNBOUND', message: unboundInArgMessage(vd.name, built.triggers), where: vd.name });
178
+ // An In-arg with no `{ from }` and no primary trigger to bind to is the
179
+ // serializer's CASE_IN_ARG_UNBOUND (in-arg-binding.ts), reported per arg.
288
180
  // ── `=js:vars.<name>` resolvability — REMOVED, superseded by #257 ───────────
289
181
  // This file briefly carried a VAR_NOT_REFERENCEABLE error asserting that only a
290
182
  // trigger-bound In-arg (`.input(shape, { from })`) could be read as `=js:vars.x`,
@@ -303,7 +195,7 @@ export function check(built, opts = {}) {
303
195
  // positive that blocks emit (this gate runs before serialization), so it must be
304
196
  // re-derived from the serializer rather than from remembered probe results.
305
197
  for (const stage of built.stages) {
306
- checkStage(stage, diags, refStage, refPlacement, opts.bindings);
198
+ checkStage(stage, diags, refStage, refPlacement);
307
199
  stage.slaRules.forEach((s, i) => checkSla(s, `stage "${stage.label}" SLA #${i + 1}`, diags));
308
200
  }
309
201
  built.slaRules.forEach((s, i) => checkSla(s, `case SLA #${i + 1}`, diags));
@@ -314,93 +206,88 @@ export function check(built, opts = {}) {
314
206
  refPlacement(r, 'case-exit', 'case-exit');
315
207
  }
316
208
  checkLibraryConnectors(built, opts, diags);
209
+ diags.push(...serializerRefusals(built, opts, diags));
317
210
  return diags;
318
211
  }
319
212
  /**
320
- * The connector-library rules (#1014): every place Case `serialize` resolves a
321
- * connector against the library, checked with the same call and the same text.
213
+ * Every refusal `compile` would raise for `built`: the serializer, run in
214
+ * collect mode (docs/CHECK_COMPILE_CONTRACT_DESIGN.md §3.5, phase 8), so each
215
+ * rule it enforces is reported from its one implementation, placed on the
216
+ * task, stage or trigger it is about (`where`, `nodeId`).
322
217
  *
323
- * - a connector task and an External Agent / External Workflow task resolve
324
- * their operation (`library.resolve`): CONNECTOR_NOT_IN_LIBRARY,
325
- * CONNECTOR_OBJECT_REQUIRED, CONNECTOR_OBJECT_NOT_GENERIC; then their inputs
326
- * (`buildConnectorInputs`): CONNECTOR_INPUT, LOOKUP_UNRESOLVED;
327
- * - an external task's resolved descriptor (`externalDescriptorRefusal`):
328
- * EXTERNAL_TASK_DESCRIPTOR_CONTRACT;
329
- * - an event trigger, a wait-for-connector task and a wait-for-connector rule
330
- * resolve their subscription (`resolveCaseSubscription`): EVENT_UNKNOWN,
331
- * EVENT_GENERIC_NO_OBJECT.
332
- *
333
- * A connector task also gets the schema-readiness warnings, because the
334
- * `connectors-local` overlay `registry prepare` writes is what Case compile
335
- * resolves against too.
218
+ * LIBRARY_REQUIRED is not reported: without a library `check` fails open, as
219
+ * its library rules always have. A throw that is no refusal comes back as
220
+ * COMPILE_FAILED, on its node, which is what `compile` says; on a node `check`
221
+ * already refuses it is that refusal's consequence and is not repeated. An
222
+ * `internal SDK error:` is passed on, unless `check` or the serializer already
223
+ * found an error, which the author has to fix first.
224
+ */
225
+ function serializerRefusals(built, opts, found) {
226
+ const collected = [];
227
+ try {
228
+ serialize(built, {
229
+ ...(opts.library ? { library: opts.library } : {}),
230
+ ...(opts.bindings ? { bindings: opts.bindings } : {}),
231
+ collect: (r) => collected.push(r),
232
+ });
233
+ }
234
+ catch (error) {
235
+ if (!found.some((d) => d.level === 'error') && !collected.some((r) => r.code !== 'LIBRARY_REQUIRED'))
236
+ throw error;
237
+ }
238
+ const out = [];
239
+ const seen = new Set();
240
+ for (const r of collected) {
241
+ if (r.code === 'LIBRARY_REQUIRED')
242
+ continue;
243
+ // Only a crash on a node is the consequence of a finding there; a scope-level
244
+ // one (no `where`) is reported even beside a case-level error such as NO_ID.
245
+ if (r.code === 'COMPILE_FAILED' && r.where !== undefined && found.some((d) => d.level === 'error' && d.where === r.where))
246
+ continue;
247
+ // One finding per node: the same refusal twice there is one rule broken once.
248
+ const key = `${r.code}\n${r.where ?? ''}\n${r.message}`;
249
+ if (seen.has(key))
250
+ continue;
251
+ seen.add(key);
252
+ out.push({
253
+ level: 'error', code: r.code, message: r.message,
254
+ ...(r.where !== undefined ? { where: r.where } : {}),
255
+ ...(r.suggestion !== undefined ? { suggestion: r.suggestion } : {}),
256
+ ...(r.nodeId !== undefined ? { nodeId: r.nodeId } : {}),
257
+ });
258
+ }
259
+ return out;
260
+ }
261
+ /**
262
+ * The connector-library warnings only `check` gives (#1014): a connector task's
263
+ * schema-readiness warnings, because the `connectors-local` overlay `registry
264
+ * prepare` writes is what Case compile resolves against too, and the lookup
265
+ * rules of `checkConnectorLookups` (a pasted id, a match key the field does not
266
+ * declare). Everything compile refuses (the operation, its inputs, an
267
+ * unrecorded lookup, the binding labels, the event subscriptions, an external
268
+ * task's descriptor) is the serializer's (`serializerRefusals`).
336
269
  */
337
270
  function checkLibraryConnectors(built, opts, diags) {
338
- const reporter = (_where, at) => (d) => {
271
+ const reporter = (at) => (d) => {
339
272
  diags.push({ level: d.level, code: d.code, message: d.message, where: at, ...(d.suggestion ? { suggestion: d.suggestion } : {}) });
340
273
  };
341
- const subscription = (kind, sub, where, at) => {
342
- // The call `resolvedSubscriptionBag` makes (`resolveCaseSubscription`), so
343
- // check refuses exactly what compile refuses, with the same text.
344
- const refused = opts.subscriptionError?.(kind, sub);
345
- if (refused !== undefined)
346
- diags.push({ level: 'error', code: refused.code, message: `${where}: ${refused.message}`, where: at });
347
- checkBindingLabels(where, sub, opts, (code, message) => diags.push({ level: 'error', code, message, where: at }), { key: sub.connector, action: sub.event });
348
- // From the shared event rules, the two Case compile enforces: a `where` lookup
349
- // nobody recorded (LOOKUP_UNRESOLVED, a warning) and an unverified BYOA
350
- // connection (EVENT_BYOA_UNVERIFIED). Only their hooks are passed, so nothing
351
- // else in them fires.
352
- checkEventResolution(sub, where, kind, {
353
- resolutions: opts.resolutions ?? {},
354
- ...(opts.eventFacts ? { eventFacts: opts.eventFacts } : {}),
355
- ...(opts.byoa ? { byoa: opts.byoa } : {}),
356
- ...(opts.bindings ? { bindings: opts.bindings } : {}),
357
- }, (code, message) => diags.push({ level: 'error', code, message, where: at }), (code, message) => diags.push({ level: 'warning', code, message, where: at }));
358
- };
359
- const rules = (conditions, where) => {
360
- for (const c of conditions)
361
- for (const g of c.rules)
362
- for (const r of g)
363
- if (r.rule === 'wait-for-connector' && isSubscription(r.connector))
364
- subscription('event', r.connector, `wait-for-connector rule in ${where}`, where);
365
- };
366
- built.triggers.forEach((t, i) => {
367
- if (t.kind !== 'event' || !t.eventSubscription)
368
- return;
369
- const name = t.name ?? `Trigger ${i + 1}`;
370
- subscription('trigger', t.eventSubscription, `Event trigger "${name}"`, name);
371
- });
372
274
  for (const stage of built.stages) {
373
- rules(stage.entry, `stage "${stage.label}" entry`);
374
- rules(stage.exit, `stage "${stage.label}" exit`);
375
275
  for (const t of stage.lanes.flat()) {
376
- rules(t.entry, `task "${t.name}" entry`);
377
276
  // A placeholder (`.unresolved('connector')`) names no operation to resolve.
378
277
  if (t.kind === 'connector' && t.connector && !isConnectorPlaceholder(t)) {
379
278
  const where = `Connector task "${t.name}"`;
380
279
  const spec = t.connector;
381
- checkConnectorLookups(where, spec, opts, EQUALS_EXPR_DIALECT, reporter(where, t.name));
382
- checkConnectorSchema(where, spec, opts, EQUALS_EXPR_DIALECT, reporter(where, t.name));
383
- checkConnectorContract(where, spec, opts, reporter(where, t.name));
280
+ // An unrecorded lookup is the serializer's LOOKUP_UNRESOLVED.
281
+ checkConnectorLookups(where, spec, opts, EQUALS_EXPR_DIALECT, reporter(t.name), false);
282
+ checkConnectorSchema(where, spec, opts, EQUALS_EXPR_DIALECT, reporter(t.name));
384
283
  }
385
284
  if ((t.kind === 'external-agent' || t.kind === 'external-workflow')
386
285
  && t.external?.connector?.key?.trim() && t.external.connector.action?.trim()) {
387
286
  const where = `${t.kind === 'external-agent' ? 'External Agent' : 'External Workflow'} task "${t.name}"`;
388
- const spec = t.external.connector;
389
- checkConnectorLookups(where, spec, opts, EQUALS_EXPR_DIALECT, reporter(where, t.name));
390
- checkConnectorContract(where, spec, opts, reporter(where, t.name));
391
- const refused = opts.externalDescriptorError?.(t.kind, spec);
392
- if (refused !== undefined)
393
- diags.push({ level: 'error', code: EXTERNAL_TASK_DESCRIPTOR_CONTRACT, message: `${where}: ${refused}`, where: t.name });
287
+ checkConnectorLookups(where, t.external.connector, opts, EQUALS_EXPR_DIALECT, reporter(t.name), false);
394
288
  }
395
- if (t.kind === 'wait-for-connector' && isSubscription(t.waitConnector))
396
- subscription('event', t.waitConnector, `Wait-for-connector task "${t.name}"`, t.name);
397
289
  }
398
290
  }
399
- rules(built.caseExit, 'case exit');
400
- }
401
- /** A resolved subscription, not a placeholder; the test `serialize`'s `isResolvedSubscription` makes. */
402
- function isSubscription(spec) {
403
- return !!spec && typeof spec === 'object' && 'connector' in spec && 'event' in spec;
404
291
  }
405
292
  /**
406
293
  * SLA guardrails surfaced early: zero at-risk percentages that the engine
@@ -441,7 +328,7 @@ function isNilOrEmpty(value) {
441
328
  return Object.keys(value).length === 0;
442
329
  return false;
443
330
  }
444
- function checkStage(stage, diags, refStage, refPlacement, bindings) {
331
+ function checkStage(stage, diags, refStage, refPlacement) {
445
332
  const where = `stage "${stage.label}"`;
446
333
  if (stage.exception && stage.required !== false)
447
334
  diags.push({
@@ -511,9 +398,9 @@ function checkStage(stage, diags, refStage, refPlacement, bindings) {
511
398
  });
512
399
  for (const t of tasks) {
513
400
  // A timer with no or several values, an action field key compile does not
514
- // write: serialize refuses the same shapes with the same text (task-shape.ts).
515
- for (const problem of taskShapeProblems(t))
516
- diags.push({ level: 'error', code: problem.code, message: problem.message, where: t.name });
401
+ // write, a `=bindings.<label>` resource name bindings.json cannot answer, and
402
+ // an external task's mode, connection and folder are the serializer's
403
+ // (`taskShapeProblems`, `resolveResourceRef`, EXTERNAL_TASK_*).
517
404
  const invalidBinding = [...(t.inputs ?? []), ...(t.outputs ?? [])]
518
405
  .find((binding) => !TYPE_NAMES.has(binding.type));
519
406
  if (invalidBinding)
@@ -523,16 +410,6 @@ function checkStage(stage, diags, refStage, refPlacement, bindings) {
523
410
  message: `Task "${t.name}" binding "${'var' in invalidBinding ? invalidBinding.var : invalidBinding.name}" has unsupported type ${JSON.stringify(invalidBinding.type)}. Use a type name such as "string" or "jsonSchema"; jsonSchema(...) is only for declaring a structured case variable.`,
524
411
  where: t.name,
525
412
  });
526
- // The builder binds a resource task itself: it mints the name/folderPath
527
- // binding pair from the literal name and folder. A `=bindings.<label>` name
528
- // or folder is read through bindings.json first, the way a connector's
529
- // `connection:` label is; one bindings.json cannot answer is refused with the
530
- // same text serialize throws (`resolveResourceRef`).
531
- if (t.ref) {
532
- const resolved = resolveResourceRef(t.ref, bindings, `Task "${t.name}"`);
533
- if (isResourceLabelRefusal(resolved))
534
- diags.push({ level: 'error', code: resolved.code, message: resolved.message, where: t.name });
535
- }
536
413
  // A custom row (`{ assign }`) is evaluated in array order, after the rows
537
414
  // before it on the same task: it cannot read itself or a later row's
538
415
  // variable (it would see the old value). `uip maestro case validate
@@ -621,41 +498,13 @@ function checkStage(stage, diags, refStage, refPlacement, bindings) {
621
498
  message: `${family} task "${t.name}" has no configuration; empty data degrades to a runtime no-op placeholder.`,
622
499
  where: t.name,
623
500
  });
624
- else {
625
- if (!isExternalExecutionMode(t.external.mode))
626
- diags.push({
627
- level: 'error',
628
- code: 'EXTERNAL_TASK_MODE',
629
- message: `${family} task "${t.name}" mode must be the connector object's executionType: "sync", "async" or "hybrid".`,
630
- where: t.name,
631
- });
632
- else {
633
- const refused = externalEventRefusal(`${family} task "${t.name}"`, t.external.mode, t.external.event);
634
- if (refused)
635
- diags.push({ level: 'error', code: EXTERNAL_TASK_EVENT, message: refused, where: t.name });
636
- }
637
- if (!t.external.connector?.key?.trim() || !t.external.connector?.action?.trim())
638
- diags.push({
639
- level: 'error',
640
- code: 'EXTERNAL_TASK_DESCRIPTOR',
641
- message: `${family} task "${t.name}" requires a generated Integration Service descriptor.`,
642
- where: t.name,
643
- });
644
- if (!t.external.connector?.connection?.trim())
645
- diags.push({
646
- level: 'error',
647
- code: 'EXTERNAL_TASK_CONNECTION',
648
- message: `${family} task "${t.name}" requires a non-empty connection binding name.`,
649
- where: t.name,
650
- });
651
- if (!t.external.connector?.folder?.trim())
652
- diags.push({
653
- level: 'error',
654
- code: 'EXTERNAL_TASK_FOLDER',
655
- message: `${family} task "${t.name}" requires a non-empty folder binding name.`,
656
- where: t.name,
657
- });
658
- }
501
+ else if (!t.external.connector?.key?.trim() || !t.external.connector?.action?.trim())
502
+ diags.push({
503
+ level: 'error',
504
+ code: 'EXTERNAL_TASK_DESCRIPTOR',
505
+ message: `${family} task "${t.name}" requires a generated Integration Service descriptor.`,
506
+ where: t.name,
507
+ });
659
508
  }
660
509
  if (stage.exception && t.runOnce !== undefined)
661
510
  diags.push({
@@ -4,13 +4,10 @@ import type { CaseCheckOpts } from './check.js';
4
4
  /**
5
5
  * Case's check options from a library the CLI already opened, or `{}` without one.
6
6
  *
7
- * Only the hooks for refusals Case `compile` makes (see {@link CaseCheckOpts}):
8
- * the operation and event resolution (the event through `resolveCaseSubscription`,
9
- * compile's own call), a connector task's field contract and lookups (the
10
- * `=`-expression validator Case compile runs), and the schema-readiness warnings.
11
- * With `bindings`, the binding-label rules (BINDING_SELF_NAME, BINDING_UNDECLARED),
12
- * which Case `serialize` enforces through the same `bindingLabelRefusal`, and the
13
- * BYOA facts and ledger, for the refusal `resolveCaseSubscription` makes.
7
+ * The library and bindings themselves, for the serializer `check` runs in
8
+ * collect mode (it reports every refusal compile makes, from compile's own
9
+ * code), plus the hooks for the warnings only `check` gives: a connector task's
10
+ * lookup rules and the schema-readiness warnings.
14
11
  *
15
12
  * @param library - The library `openCliLibrary` returned, or `undefined`.
16
13
  * @param bindings - The project's bindings.json, as compile reads it.
@@ -8,18 +8,13 @@
8
8
  * both `case check` and `case compile`'s check pass call.
9
9
  */
10
10
  import { connectorCheckOpts } from '../core/cli-library.js';
11
- import { externalDescriptorRefusal } from './external.js';
12
- import { EVENT_GENERIC_NO_OBJECT, GenericEventNoObjectError, resolveCaseSubscription } from './subscription.js';
13
11
  /**
14
12
  * Case's check options from a library the CLI already opened, or `{}` without one.
15
13
  *
16
- * Only the hooks for refusals Case `compile` makes (see {@link CaseCheckOpts}):
17
- * the operation and event resolution (the event through `resolveCaseSubscription`,
18
- * compile's own call), a connector task's field contract and lookups (the
19
- * `=`-expression validator Case compile runs), and the schema-readiness warnings.
20
- * With `bindings`, the binding-label rules (BINDING_SELF_NAME, BINDING_UNDECLARED),
21
- * which Case `serialize` enforces through the same `bindingLabelRefusal`, and the
22
- * BYOA facts and ledger, for the refusal `resolveCaseSubscription` makes.
14
+ * The library and bindings themselves, for the serializer `check` runs in
15
+ * collect mode (it reports every refusal compile makes, from compile's own
16
+ * code), plus the hooks for the warnings only `check` gives: a connector task's
17
+ * lookup rules and the schema-readiness warnings.
23
18
  *
24
19
  * @param library - The library `openCliLibrary` returned, or `undefined`.
25
20
  * @param bindings - The project's bindings.json, as compile reads it.
@@ -30,35 +25,10 @@ export function caseCheckOpts(library, bindings) {
30
25
  return bindings ? { bindings } : {};
31
26
  const shared = connectorCheckOpts(library, bindings);
32
27
  return {
28
+ library,
33
29
  ...(bindings ? { bindings } : {}),
34
- connectorResolveError: shared.connectorResolveError,
35
- connectorInputs: shared.connectorInputs,
36
30
  lookups: shared.lookups,
37
31
  resolutions: shared.resolutions,
38
- eventFacts: shared.eventFacts,
39
- byoa: shared.byoa,
40
32
  schemaReadiness: shared.schemaReadiness,
41
- externalDescriptorError: (kind, d) => {
42
- let rc;
43
- try {
44
- rc = library.resolve(d.key, d.action, d.version, d.object);
45
- }
46
- catch {
47
- return undefined; // connectorResolveError's to report
48
- }
49
- return externalDescriptorRefusal(kind, rc);
50
- },
51
- subscriptionError: (kind, sub) => {
52
- try {
53
- resolveCaseSubscription(library, kind, sub);
54
- return undefined;
55
- }
56
- catch (error) {
57
- return {
58
- code: error instanceof GenericEventNoObjectError ? EVENT_GENERIC_NO_OBJECT : 'EVENT_UNKNOWN',
59
- message: error instanceof Error ? error.message : String(error),
60
- };
61
- }
62
- },
63
33
  };
64
34
  }
@@ -2,7 +2,7 @@ import { commentText } from '../core/comment-text.js';
2
2
  import { implicitInArgTrigger } from './in-arg-binding.js';
3
3
  import { tsLiteral, tsNumber } from '../core/ts-literal.js';
4
4
  import { CASE_FORMAT_PROFILE, CASE_SCHEMA_VERSIONS } from './format-profile.js';
5
- import { EXTERNAL_TASK_CONTEXT, EXTERNAL_TASK_EVENT_CONTEXT, externalDescriptorRefusal, externalEventRefusal, externalServiceType, isExternalExecutionMode } from './external.js';
5
+ import { EXTERNAL_TASK_CONTEXT, EXTERNAL_TASK_EVENT_CONTEXT, externalDescriptorRefusal, externalEventRefusal, externalServiceType, isExternalExecutionMode, libraryExecution } from './external.js';
6
6
  import { preserveSchemaVersion } from '../schema-version.js';
7
7
  import { ownEntry } from '../core/own-entry.js';
8
8
  import { isUnrestorableSlotId, taskFingerprint } from './preserve.js';
@@ -676,8 +676,8 @@ function externalCall(task, refs, library) {
676
676
  const serviceType = externalServiceType(kind, mode);
677
677
  if (data.serviceType !== serviceType)
678
678
  throw new Error(`${owner} has serviceType "${String(data.serviceType ?? '')}", but its executionType "${mode}" saves as "${serviceType}".`);
679
- // The event the object answers with, as saved: the same rule compile applies,
680
- // so a lifted task always compiles back (a hybrid task without one does not).
679
+ // The event the object answers with, as saved, judged by compile's event rule
680
+ // here, and against the library's record below (`externalExecution` in compile).
681
681
  const event = eventRows.length ? { operation: stringValue('operation'), mode: stringValue('eventMode') } : undefined;
682
682
  // A saved plan has no `event` option to drop, so a sync task with event rows
683
683
  // gets its own text naming the rows; every other case is compile's rule.
@@ -699,6 +699,19 @@ function externalCall(task, refs, library) {
699
699
  const refused = externalDescriptorRefusal(kind, resolved);
700
700
  if (refused)
701
701
  throw new Error(`${owner}: ${refused}`);
702
+ // Compile refuses a mode or event the library contradicts (#1369), so a task
703
+ // saved against a different record would lift to source that does not compile
704
+ // back: refuse it here, by name, before the author edits anything.
705
+ const recorded = libraryExecution(resolved.entry?.operation);
706
+ if (recorded.mode !== undefined && recorded.mode !== mode)
707
+ throw new Error(`${owner} is saved with executionType "${mode}", but the connector library records the object's ` +
708
+ `executionType as "${recorded.mode}", so the lifted source would not compile back. Re-save the task in Studio Web, ` +
709
+ `or rebuild the library (registry prepare) if the object changed.`);
710
+ if (event !== undefined && recorded.event !== undefined
711
+ && (event.operation !== recorded.event.operation || event.mode !== recorded.event.mode))
712
+ throw new Error(`${owner} is saved with event ${JSON.stringify(event)}, but the connector library records the object's ` +
713
+ `event as ${JSON.stringify(recorded.event)}, so the lifted source would not compile back. Re-save the task in Studio Web, ` +
714
+ `or rebuild the library (registry prepare) if the object changed.`);
702
715
  if (!Array.isArray(data.inputs))
703
716
  throw new Error(`${owner} inputs must be an array.`);
704
717
  const inputs = connectorInputs(data.inputs, resolved, owner);
@@ -58,6 +58,52 @@ export declare function isExternalExecutionMode(value: unknown): value is Extern
58
58
  * @returns The service type.
59
59
  */
60
60
  export declare function externalServiceType(kind: 'external-agent' | 'external-workflow', mode: ExternalExecutionMode): string;
61
+ /** Check's and compile's code when an authored `mode` disagrees with the library. */
62
+ export declare const EXTERNAL_TASK_MODE_MISMATCH = "EXTERNAL_TASK_MODE_MISMATCH";
63
+ /** A judged execution: the task's mode and event, or why it cannot be written. */
64
+ export type ExternalExecution = {
65
+ mode: ExternalExecutionMode;
66
+ event?: ExternalEventPair;
67
+ code?: undefined;
68
+ } | {
69
+ code: string;
70
+ message: string;
71
+ };
72
+ /**
73
+ * The execution facts the connector library records for the object
74
+ * (`operation.executionType` and `operation.executionEvent`, from
75
+ * `registry build-library` / `registry prepare`, #1369). A value the SDK does
76
+ * not know is ignored, as if it were absent.
77
+ */
78
+ export declare function libraryExecution(operation: unknown): {
79
+ mode?: ExternalExecutionMode;
80
+ event?: ExternalEventPair;
81
+ };
82
+ /**
83
+ * The mode and event an external task is written with: what the author passed,
84
+ * else what the connector library records for the object. `check` and
85
+ * `serialize` both call this, so they refuse the same tasks with the same text.
86
+ *
87
+ * - `mode` comes from the author or the library. With neither, the task is
88
+ * refused (EXTERNAL_TASK_MODE): a guessed default would compile an `async`
89
+ * object to the sync contract without a word (#1370 was closed for that).
90
+ * - An authored `mode` the library contradicts is refused
91
+ * (EXTERNAL_TASK_MODE_MISMATCH): the plan would run the wrong contract.
92
+ * - `event` comes from the author or, for an `async` / `hybrid` object, the
93
+ * library; an authored one the library contradicts is refused
94
+ * (EXTERNAL_TASK_EVENT), and so is any pair {@link externalEventRefusal} refuses.
95
+ *
96
+ * @param owner - The task as the message names it, e.g. `External Workflow task "Run"`.
97
+ * @param authored - The task's authored `mode` and `event`.
98
+ * @param operation - The resolved library entry's `operation`, or `undefined`
99
+ * when no library is at hand (model-only `check`, which then judges only what
100
+ * was authored and reports no missing mode).
101
+ * @returns The mode and event, or the refusal.
102
+ */
103
+ export declare function externalExecution(owner: string, authored: {
104
+ mode?: unknown;
105
+ event?: unknown;
106
+ }, operation: unknown): ExternalExecution | undefined;
61
107
  /** Check's code for {@link externalDescriptorRefusal}. */
62
108
  export declare const EXTERNAL_TASK_DESCRIPTOR_CONTRACT = "EXTERNAL_TASK_DESCRIPTOR_CONTRACT";
63
109
  /**