@uipath/maestro-builder-sdk 6.68.7 → 6.69.1

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,80 +1,48 @@
1
1
  /**
2
- * case/check — fast static validation of a BuiltCase before serialization.
2
+ * case/check — fast validation of a BuiltCase.
3
3
  *
4
4
  * Surfaces the common `uip maestro case validate` failures early (task without
5
5
  * an entry rule, case/stage without a completion rule, an unsatisfiable
6
- * `required-tasks-completed`, unresolved stage/task references, duplicate stage
7
- * labels) so authoring gets instant feedback. The authoritative gate is still
8
- * `uip maestro case validate` / `@uipath/case-schema` on the emitted file.
6
+ * `required-tasks-completed`) so authoring gets instant feedback, and runs the
7
+ * serializer in collect mode (`serializerRefusals`), so it also reports every
8
+ * refusal `compile` would raise (unresolved stage/task references, duplicate
9
+ * stage labels, a connector input the operation does not declare), each from
10
+ * its one implementation and placed on its task, stage or trigger
11
+ * (docs/CHECK_COMPILE_CONTRACT_DESIGN.md §3.5, phase 8). The authoritative gate
12
+ * is still `uip maestro case validate` / `@uipath/case-schema` on the emitted file.
9
13
  */
10
14
  import type { BuiltCase } from './case-sdk.js';
11
15
  import { type ConnectorCheckOpts } from '../core/connector-checks.js';
12
- import type { EventSubscription } from '../core/actions.js';
13
- import { type EventCheckOpts } from '../core/event-checks.js';
16
+ import type { Library } from '../core/library.js';
14
17
  export interface Diagnostic {
15
18
  level: 'error' | 'warning';
16
19
  code: string;
17
20
  message: string;
21
+ /** What the finding is about: a task's name, a stage's label, a trigger's label. */
18
22
  where?: string;
19
23
  suggestion?: string;
24
+ /** The id of the node `where` names, for a refusal the serializer placed on one (`serializerRefusals`). */
25
+ nodeId?: string;
20
26
  }
21
27
  /**
22
28
  * The connector-library facts Case `check` reads, from the same `Library`
23
29
  * instance `compile` serializes against (`caseCheckOpts` in `./cli-library.ts`).
24
30
  *
25
- * Deliberately NARROWER than Flow's bag. An error here must be a refusal Case
26
- * `compile` actually makes. Case `serialize` resolves the connector operation and
27
- * the event, and validates a connector task's inputs and resolves its lookups
28
- * through the shared `buildConnectorInputs` (`connectorInputs`, `lookups`,
29
- * `resolutions`), and refuses a connection/folder label bindings.json cannot
30
- * turn into a resource key (`bindings`, `bindingLabelRefusal`), and refuses an
31
- * event on an unverified BYOA connection (`eventFacts`, `byoa`).
31
+ * `check` runs the serializer in collect mode (`serializerRefusals`), with
32
+ * `library` and `bindings`, so every refusal `compile` makes (an operation or
33
+ * event the library cannot resolve, a connector input the operation does not
34
+ * declare, an unrecorded lookup, a binding label bindings.json cannot answer,
35
+ * an unverified BYOA connection, an external task's descriptor contract) is
36
+ * reported from its one implementation. The other hooks feed the warnings only
37
+ * `check` gives: the schema-readiness warnings and the lookup rules of
38
+ * `checkConnectorLookups`.
32
39
  *
33
- * Every hook fails open: without a library, `check` stays a pure function of the
34
- * built model.
40
+ * Every hook fails open: without a library, `check` judges a connector task up
41
+ * to the library, as `compile` would.
35
42
  */
36
- export type CaseCheckOpts = Pick<ConnectorCheckOpts, 'connectorResolveError' | 'connectorInputs' | 'lookups' | 'resolutions' | 'schemaReadiness' | 'bindings'> & Pick<EventCheckOpts, 'eventFacts' | 'byoa'> & {
37
- /**
38
- * What Case `compile` would refuse an event subscription with, or `undefined`
39
- * when it resolves: `EVENT_UNKNOWN` (the event is not in the library, or
40
- * refuses the `object`) or `EVENT_GENERIC_NO_OBJECT` (a GENERIC event with no
41
- * `object`). Built on `resolveCaseSubscription`, the call compile makes.
42
- */
43
- /**
44
- * What Case `compile` would refuse an External Agent / External Workflow
45
- * task's resolved descriptor with (`externalDescriptorRefusal`), or
46
- * `undefined`: also `undefined` when the descriptor does not resolve, which
47
- * `connectorResolveError` reports.
48
- */
49
- externalDescriptorError?: (kind: 'external-agent' | 'external-workflow', descriptor: {
50
- key: string;
51
- action: string;
52
- version?: string;
53
- object?: string;
54
- }) => string | undefined;
55
- /**
56
- * What Case `compile` would refuse an External Agent / External Workflow
57
- * task's mode and event with, judged against what the library records for
58
- * the object (`externalExecution`): EXTERNAL_TASK_MODE (no mode from the
59
- * author or the library), EXTERNAL_TASK_MODE_MISMATCH, EXTERNAL_TASK_EVENT.
60
- * `undefined` when they are fine, or the descriptor does not resolve.
61
- */
62
- externalExecutionError?: (owner: string, descriptor: {
63
- key: string;
64
- action: string;
65
- version?: string;
66
- object?: string;
67
- }, authored: {
68
- mode?: unknown;
69
- event?: unknown;
70
- }) => {
71
- code: string;
72
- message: string;
73
- } | undefined;
74
- subscriptionError?: (kind: 'trigger' | 'event', subscription: EventSubscription) => {
75
- code: string;
76
- message: string;
77
- } | undefined;
43
+ export type CaseCheckOpts = Pick<ConnectorCheckOpts, 'lookups' | 'resolutions' | 'schemaReadiness' | 'bindings'> & {
44
+ /** The library `compile` serializes against, for the serializer `check` runs. */
45
+ library?: Library;
78
46
  };
79
47
  /**
80
48
  * Validate a built case.
@@ -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, externalExecution } 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, opts.externalExecutionError !== undefined);
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,98 +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; and its mode and event against what the
329
- * library records for the object (`externalExecution`): EXTERNAL_TASK_MODE,
330
- * EXTERNAL_TASK_MODE_MISMATCH, EXTERNAL_TASK_EVENT;
331
- * - an event trigger, a wait-for-connector task and a wait-for-connector rule
332
- * resolve their subscription (`resolveCaseSubscription`): EVENT_UNKNOWN,
333
- * EVENT_GENERIC_NO_OBJECT.
334
- *
335
- * A connector task also gets the schema-readiness warnings, because the
336
- * `connectors-local` overlay `registry prepare` writes is what Case compile
337
- * 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`).
338
269
  */
339
270
  function checkLibraryConnectors(built, opts, diags) {
340
- const reporter = (_where, at) => (d) => {
271
+ const reporter = (at) => (d) => {
341
272
  diags.push({ level: d.level, code: d.code, message: d.message, where: at, ...(d.suggestion ? { suggestion: d.suggestion } : {}) });
342
273
  };
343
- const subscription = (kind, sub, where, at) => {
344
- // The call `resolvedSubscriptionBag` makes (`resolveCaseSubscription`), so
345
- // check refuses exactly what compile refuses, with the same text.
346
- const refused = opts.subscriptionError?.(kind, sub);
347
- if (refused !== undefined)
348
- diags.push({ level: 'error', code: refused.code, message: `${where}: ${refused.message}`, where: at });
349
- checkBindingLabels(where, sub, opts, (code, message) => diags.push({ level: 'error', code, message, where: at }), { key: sub.connector, action: sub.event });
350
- // From the shared event rules, the two Case compile enforces: a `where` lookup
351
- // nobody recorded (LOOKUP_UNRESOLVED, a warning) and an unverified BYOA
352
- // connection (EVENT_BYOA_UNVERIFIED). Only their hooks are passed, so nothing
353
- // else in them fires.
354
- checkEventResolution(sub, where, kind, {
355
- resolutions: opts.resolutions ?? {},
356
- ...(opts.eventFacts ? { eventFacts: opts.eventFacts } : {}),
357
- ...(opts.byoa ? { byoa: opts.byoa } : {}),
358
- ...(opts.bindings ? { bindings: opts.bindings } : {}),
359
- }, (code, message) => diags.push({ level: 'error', code, message, where: at }), (code, message) => diags.push({ level: 'warning', code, message, where: at }));
360
- };
361
- const rules = (conditions, where) => {
362
- for (const c of conditions)
363
- for (const g of c.rules)
364
- for (const r of g)
365
- if (r.rule === 'wait-for-connector' && isSubscription(r.connector))
366
- subscription('event', r.connector, `wait-for-connector rule in ${where}`, where);
367
- };
368
- built.triggers.forEach((t, i) => {
369
- if (t.kind !== 'event' || !t.eventSubscription)
370
- return;
371
- const name = t.name ?? `Trigger ${i + 1}`;
372
- subscription('trigger', t.eventSubscription, `Event trigger "${name}"`, name);
373
- });
374
274
  for (const stage of built.stages) {
375
- rules(stage.entry, `stage "${stage.label}" entry`);
376
- rules(stage.exit, `stage "${stage.label}" exit`);
377
275
  for (const t of stage.lanes.flat()) {
378
- rules(t.entry, `task "${t.name}" entry`);
379
276
  // A placeholder (`.unresolved('connector')`) names no operation to resolve.
380
277
  if (t.kind === 'connector' && t.connector && !isConnectorPlaceholder(t)) {
381
278
  const where = `Connector task "${t.name}"`;
382
279
  const spec = t.connector;
383
- checkConnectorLookups(where, spec, opts, EQUALS_EXPR_DIALECT, reporter(where, t.name));
384
- checkConnectorSchema(where, spec, opts, EQUALS_EXPR_DIALECT, reporter(where, t.name));
385
- 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));
386
283
  }
387
284
  if ((t.kind === 'external-agent' || t.kind === 'external-workflow')
388
285
  && t.external?.connector?.key?.trim() && t.external.connector.action?.trim()) {
389
286
  const where = `${t.kind === 'external-agent' ? 'External Agent' : 'External Workflow'} task "${t.name}"`;
390
- const spec = t.external.connector;
391
- checkConnectorLookups(where, spec, opts, EQUALS_EXPR_DIALECT, reporter(where, t.name));
392
- checkConnectorContract(where, spec, opts, reporter(where, t.name));
393
- const refused = opts.externalDescriptorError?.(t.kind, spec);
394
- if (refused !== undefined)
395
- diags.push({ level: 'error', code: EXTERNAL_TASK_DESCRIPTOR_CONTRACT, message: `${where}: ${refused}`, where: t.name });
396
- const execution = opts.externalExecutionError?.(where, spec, t.external);
397
- if (execution !== undefined)
398
- diags.push({ level: 'error', code: execution.code, message: execution.message, where: t.name });
287
+ checkConnectorLookups(where, t.external.connector, opts, EQUALS_EXPR_DIALECT, reporter(t.name), false);
399
288
  }
400
- if (t.kind === 'wait-for-connector' && isSubscription(t.waitConnector))
401
- subscription('event', t.waitConnector, `Wait-for-connector task "${t.name}"`, t.name);
402
289
  }
403
290
  }
404
- rules(built.caseExit, 'case exit');
405
- }
406
- /** A resolved subscription, not a placeholder; the test `serialize`'s `isResolvedSubscription` makes. */
407
- function isSubscription(spec) {
408
- return !!spec && typeof spec === 'object' && 'connector' in spec && 'event' in spec;
409
291
  }
410
292
  /**
411
293
  * SLA guardrails surfaced early: zero at-risk percentages that the engine
@@ -446,9 +328,7 @@ function isNilOrEmpty(value) {
446
328
  return Object.keys(value).length === 0;
447
329
  return false;
448
330
  }
449
- function checkStage(stage, diags, refStage, refPlacement, bindings,
450
- /** A library judges external tasks' mode and event (`externalExecutionError`). */
451
- externalJudge) {
331
+ function checkStage(stage, diags, refStage, refPlacement) {
452
332
  const where = `stage "${stage.label}"`;
453
333
  if (stage.exception && stage.required !== false)
454
334
  diags.push({
@@ -518,9 +398,9 @@ externalJudge) {
518
398
  });
519
399
  for (const t of tasks) {
520
400
  // A timer with no or several values, an action field key compile does not
521
- // write: serialize refuses the same shapes with the same text (task-shape.ts).
522
- for (const problem of taskShapeProblems(t))
523
- 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_*).
524
404
  const invalidBinding = [...(t.inputs ?? []), ...(t.outputs ?? [])]
525
405
  .find((binding) => !TYPE_NAMES.has(binding.type));
526
406
  if (invalidBinding)
@@ -530,16 +410,6 @@ externalJudge) {
530
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.`,
531
411
  where: t.name,
532
412
  });
533
- // The builder binds a resource task itself: it mints the name/folderPath
534
- // binding pair from the literal name and folder. A `=bindings.<label>` name
535
- // or folder is read through bindings.json first, the way a connector's
536
- // `connection:` label is; one bindings.json cannot answer is refused with the
537
- // same text serialize throws (`resolveResourceRef`).
538
- if (t.ref) {
539
- const resolved = resolveResourceRef(t.ref, bindings, `Task "${t.name}"`);
540
- if (isResourceLabelRefusal(resolved))
541
- diags.push({ level: 'error', code: resolved.code, message: resolved.message, where: t.name });
542
- }
543
413
  // A custom row (`{ assign }`) is evaluated in array order, after the rows
544
414
  // before it on the same task: it cannot read itself or a later row's
545
415
  // variable (it would see the old value). `uip maestro case validate
@@ -628,39 +498,13 @@ externalJudge) {
628
498
  message: `${family} task "${t.name}" has no configuration; empty data degrades to a runtime no-op placeholder.`,
629
499
  where: t.name,
630
500
  });
631
- else {
632
- // The authored mode and event alone. With a library at hand, the
633
- // library pass judges them against the object instead (and reports a
634
- // missing mode), so they are judged here only without one.
635
- const libraryJudges = externalJudge
636
- && !!t.external.connector?.key?.trim() && !!t.external.connector?.action?.trim();
637
- if (!libraryJudges) {
638
- const judged = externalExecution(`${family} task "${t.name}"`, t.external, undefined);
639
- if (judged?.code !== undefined)
640
- diags.push({ level: 'error', code: judged.code, message: judged.message, where: t.name });
641
- }
642
- if (!t.external.connector?.key?.trim() || !t.external.connector?.action?.trim())
643
- diags.push({
644
- level: 'error',
645
- code: 'EXTERNAL_TASK_DESCRIPTOR',
646
- message: `${family} task "${t.name}" requires a generated Integration Service descriptor.`,
647
- where: t.name,
648
- });
649
- if (!t.external.connector?.connection?.trim())
650
- diags.push({
651
- level: 'error',
652
- code: 'EXTERNAL_TASK_CONNECTION',
653
- message: `${family} task "${t.name}" requires a non-empty connection binding name.`,
654
- where: t.name,
655
- });
656
- if (!t.external.connector?.folder?.trim())
657
- diags.push({
658
- level: 'error',
659
- code: 'EXTERNAL_TASK_FOLDER',
660
- message: `${family} task "${t.name}" requires a non-empty folder binding name.`,
661
- where: t.name,
662
- });
663
- }
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
+ });
664
508
  }
665
509
  if (stage.exception && t.runOnce !== undefined)
666
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.