@uipath/maestro-builder-sdk 6.14.5 → 6.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/api-index.md +71 -71
  2. package/dist/api-members.md +324 -323
  3. package/dist/bpmn/bpmn-sdk.d.ts +16 -0
  4. package/dist/bpmn/bpmn-sdk.js +2 -0
  5. package/dist/bpmn/check-cli.js +22 -5
  6. package/dist/bpmn/check.d.ts +15 -1
  7. package/dist/bpmn/check.js +69 -2
  8. package/dist/bpmn/compile-cli.js +24 -14
  9. package/dist/bpmn/connector.js +29 -14
  10. package/dist/bpmn/intsvc.js +5 -2
  11. package/dist/bpmn/serialize.d.ts +2 -0
  12. package/dist/bpmn/serialize.js +1 -1
  13. package/dist/case/compile-cli.js +6 -9
  14. package/dist/case/serialize.js +1 -1
  15. package/dist/check-cli.js +6 -35
  16. package/dist/check.d.ts +16 -148
  17. package/dist/check.js +33 -709
  18. package/dist/cli/commands/bpmn.js +15 -0
  19. package/dist/cli-library.d.ts +6 -25
  20. package/dist/cli-library.js +6 -73
  21. package/dist/core/cli-library.d.ts +47 -0
  22. package/dist/core/cli-library.js +150 -0
  23. package/dist/core/connector-checks.d.ts +226 -0
  24. package/dist/core/connector-checks.js +518 -0
  25. package/dist/core/connector-inputs.d.ts +80 -0
  26. package/dist/core/connector-inputs.js +453 -0
  27. package/dist/core/event-checks.d.ts +137 -0
  28. package/dist/core/event-checks.js +321 -0
  29. package/dist/decompile.js +1 -1
  30. package/dist/registry/generators/markdown/sections/example.d.ts +7 -1
  31. package/dist/registry/generators/markdown/sections/example.js +70 -5
  32. package/dist/registry/generators/markdown/sections/lookups.js +29 -8
  33. package/dist/registry/prepare/messages.js +6 -1
  34. package/dist/registry/prepare/source-import.d.ts +3 -3
  35. package/dist/registry/prepare/source-import.js +15 -4
  36. package/dist/registry/prepare/source-labels.js +31 -4
  37. package/dist/serialize.d.ts +11 -11
  38. package/dist/serialize.js +27 -420
  39. package/package.json +2 -2
  40. /package/dist/{event-filters.d.ts → core/event-filters.d.ts} +0 -0
  41. /package/dist/{event-filters.js → core/event-filters.js} +0 -0
@@ -0,0 +1,518 @@
1
+ /**
2
+ * core/connector-checks — the check rules whose facts live in the connector
3
+ * library, shared by every family that authors a connector activity.
4
+ *
5
+ * These were Flow's, and they are the reason Flow's `check` can say "run THIS
6
+ * prepare" in the loop the author is already in. BPMN had none of them: its
7
+ * `check` took no options at all, so a BPMN connector node reached `compile`
8
+ * with an unresolved lookup, an unprepared object or a self-naming binding and
9
+ * learned about it from `uip maestro bpmn validate`, a tenant, or a faulted run.
10
+ * (`flow-v2-run-8`: `skill-bpmn-e2e-customer-escalation-triage` packed
11
+ * `bindings_v2.json` carrying the literal strings `jiraConnection` and
12
+ * `slackConnection` where connection ids belong.)
13
+ *
14
+ * Nothing here knows what a step or a task is. A caller supplies:
15
+ *
16
+ * - `where` — how the message opens (`Connector step "post"`,
17
+ * `Connector task "post"`), so each family keeps its own vocabulary;
18
+ * - `report` — where the diagnostic goes, so each family adds its own locator
19
+ * field (Flow's `step`, BPMN's `where`);
20
+ * - `dialect` — how an authored value reads, because Flow writes expressions as
21
+ * `Expr` objects over `$vars` and BPMN writes them as `=`-prefixed strings
22
+ * over `vars`. Without this a BPMN `'=vars.channelId'` would read as a pasted
23
+ * literal id and warn about itself.
24
+ */
25
+ import { isLookupToken, resolvedValue, } from './lookups.js';
26
+ import { Expr } from './expr.js';
27
+ import { extractRefs } from './expr-check.js';
28
+ import { prepareCommand, resolveFlagArg } from './cli-spelling.js';
29
+ import { bindingSelfNameMessage, bindingUndeclaredMessage, connectionConnectorConflictMessage, undeclaredBinding, } from './binding-messages.js';
30
+ /**
31
+ * Flow: an input is an {@link Expr} object. A non-literal one is runtime; a
32
+ * literal one (`lit(…)`) carries the constant it was built from.
33
+ */
34
+ export const FLOW_EXPR_DIALECT = {
35
+ namespace: '$vars',
36
+ isOpaque: (value) => value instanceof Expr,
37
+ descriptors: true,
38
+ classify(value) {
39
+ if (value instanceof Expr) {
40
+ return value.literal ? { kind: 'literal', value: value.value } : { kind: 'runtime', js: value.js };
41
+ }
42
+ if (value !== null && typeof value === 'object')
43
+ return { kind: 'container' };
44
+ return { kind: 'literal', value };
45
+ },
46
+ };
47
+ /**
48
+ * BPMN and Case: an expression is a string that starts with `=`.
49
+ *
50
+ * `=js:…` is the same thing with an evaluator prefix; both reduce to the source
51
+ * `extractRefs` reads. A string that does NOT start with `=` is a constant, which
52
+ * is exactly what `LOOKUP_LITERAL_ID` is about.
53
+ */
54
+ export const EQUALS_EXPR_DIALECT = {
55
+ namespace: 'vars',
56
+ isOpaque: (value) => typeof value === 'string' && value.startsWith('='),
57
+ descriptors: false,
58
+ classify(value) {
59
+ if (typeof value === 'string' && value.startsWith('=')) {
60
+ return { kind: 'runtime', js: value.slice(1).replace(/^js:/, '') };
61
+ }
62
+ if (value !== null && typeof value === 'object')
63
+ return { kind: 'container' };
64
+ return { kind: 'literal', value };
65
+ },
66
+ };
67
+ /**
68
+ * The lookup rules: a pasted id, and an unresolved token.
69
+ *
70
+ * These are the strongest discoverability route in the design, because they
71
+ * need no prior knowledge and arrive in the loop the author is already in. An
72
+ * author who pastes `WT9RC72KU` into a lookup field is not being careless, they
73
+ * are being uninformed — nothing anywhere told them the field was resolvable —
74
+ * and the fix is to tell them at the point of the mistake.
75
+ *
76
+ * Both are WARNINGS, not errors. A literal id is legal and sometimes correct
77
+ * (a value that genuinely does not move), and the unresolved case is already a
78
+ * hard failure at compile, where the same message is repeated. Making it an
79
+ * error here would stop a `check` that is otherwise clean from telling the
80
+ * author about everything ELSE that is wrong, which is the whole point of
81
+ * running the cheap pass first.
82
+ */
83
+ export function checkConnectorLookups(where, spec, opts, dialect, report) {
84
+ const specs = opts.lookups?.(spec.key, spec.action, spec.version, spec.object);
85
+ const resolutions = opts.resolutions ?? {};
86
+ // One LOOKUP_LITERAL_ID per field: a multiselect of three pasted ids is one
87
+ // mistake with one remedy, not three.
88
+ const literalWarned = new Set();
89
+ // ...and ONE diagnostic per NODE for both families, not one per field.
90
+ //
91
+ // The remedy is identical for every field of a node — the same `registry
92
+ // prepare`, which takes every `--resolve` in one invocation, exactly as the
93
+ // operation's own reference page prints it. Emitting it per field repeated a
94
+ // five-line block with two full commands for each one: a Slack send with a
95
+ // channel and a `send_as` produced ten such blocks across a run, and the
96
+ // `integration_service_boundary` eval card went from 70 turns to 86 and from
97
+ // 0/3 to 3/3 `max_turns_exhausted` when BPMN started reporting them at all.
98
+ const unresolved = [];
99
+ const literals = [];
100
+ const visit = (value, path) => {
101
+ if (isLookupToken(value)) {
102
+ // A match key the field does not declare is certainly wrong, and the
103
+ // spec that says so is already in hand. `fields.reporter.id` is searchable
104
+ // by `displayName | accountId`; `.by('name', …)` used to pass here as an
105
+ // ordinary unresolved lookup, and the prepare command this warning prints
106
+ // would then be REFUSED by the resolver — the mistake surviving both the
107
+ // check and its own suggested fix. `spec.by` is the field's own
108
+ // declaration, so it is complete by construction and may hard-error.
109
+ const named = path.join('.');
110
+ const declared = specs?.[named];
111
+ const by = value.__lookup.by;
112
+ if (declared && typeof by === 'string' && !declared.by.includes(by)) {
113
+ const { target, call } = lookupPhrases(named, declared, spec, dialect.descriptors);
114
+ report({
115
+ level: 'error',
116
+ code: 'LOOKUP_UNKNOWN_MATCH_KEY',
117
+ message: `${where}: \`${named}\` has no match key \`${by}\`. It resolves `
118
+ + `against \`${target}\`, which is searchable by `
119
+ + `${declared.by.map((b) => `\`${b}\``).join(' or ')}. Resolving on `
120
+ + `\`${by}\` is refused, so recording it would fail too:\n`
121
+ + ` ${call}`,
122
+ });
123
+ return;
124
+ }
125
+ if (resolvedValue(value, resolutions) === undefined)
126
+ unresolved.push(value);
127
+ return;
128
+ }
129
+ const reading = dialect.classify(value);
130
+ if (reading.kind === 'container') {
131
+ if (Array.isArray(value)) {
132
+ // An element is named by the WIRE name the lookup table is keyed on —
133
+ // `users[*]`, `contacts[*].id` — not by its index. `users.0` matched no
134
+ // spec, so a pasted id, a token with a bad match key, or a runtime value
135
+ // inside a multiselect passed every rule below unseen. A name that already
136
+ // is the array's (a flat `'users[*]'` key, or a field typed as an array)
137
+ // stays as it is.
138
+ const last = path[path.length - 1] ?? '';
139
+ const element = last.endsWith('[*]') || specs?.[path.join('.')]
140
+ ? path
141
+ : [...path.slice(0, -1), `${last}[*]`];
142
+ value.forEach((v) => visit(v, element));
143
+ return;
144
+ }
145
+ for (const [k, v] of Object.entries(value)) {
146
+ visit(v, [...path, k]);
147
+ }
148
+ return;
149
+ }
150
+ if (!specs)
151
+ return;
152
+ const field = path.join('.');
153
+ const lookup = specs[field];
154
+ if (!lookup)
155
+ return;
156
+ if (reading.kind === 'runtime') {
157
+ const d = runtimeLookupDiagnostic(where, spec, field, lookup, reading.js, dialect);
158
+ if (d)
159
+ report(d);
160
+ return;
161
+ }
162
+ // A pasted id, whether bare or wrapped in `lit()`.
163
+ const literal = reading.value;
164
+ if (typeof literal !== 'string')
165
+ return;
166
+ if (literalWarned.has(field))
167
+ return;
168
+ literalWarned.add(field);
169
+ literals.push({ field, lookup });
170
+ };
171
+ for (const [name, value] of Object.entries(spec.inputs ?? {}))
172
+ visit(value, [name]);
173
+ if (unresolved.length)
174
+ report(unresolvedDiagnostic(where, spec, unresolved));
175
+ if (literals.length)
176
+ report(literalIdDiagnostic(where, spec, literals, dialect.descriptors));
177
+ }
178
+ /** `--resolve a:by=<v> \` … — every field of one node, in one pasteable command. */
179
+ function resolveBlock(spec, args) {
180
+ const head = ` ${prepareCommand(spec.key, spec.action)}`;
181
+ if (args.length === 1)
182
+ return `${head} \\\n --resolve ${args[0]}`;
183
+ return [head + ' \\', ...args.map((a, i) => ` --resolve ${a}${i < args.length - 1 ? ' \\' : ''}`)].join('\n');
184
+ }
185
+ /** Every unresolved token on one node, with the single prepare that records them all. */
186
+ function unresolvedDiagnostic(where, spec, tokens) {
187
+ const named = tokens.map((t) => `\`${t.__lookup.field}\` (${t.__lookup.by}=${JSON.stringify(t.__lookup.value)})`);
188
+ const args = tokens.map((t) => resolveFlagArg(t.__lookup.field, t.__lookup.by, String(t.__lookup.value)));
189
+ return {
190
+ level: 'warning',
191
+ code: 'LOOKUP_UNRESOLVED',
192
+ message: `${where}: ${named.length === 1 ? 'input' : 'inputs'} ${named.join(', ')} `
193
+ + `${named.length === 1 ? 'is an' : 'are'} unresolved lookup${named.length === 1 ? '' : 's'}. `
194
+ + `Compile does not make network calls, so the value has to be resolved first:\n`
195
+ + resolveBlock(spec, args)
196
+ + `\nCompile will fail until ${named.length === 1 ? 'this is' : 'these are'} recorded. Running it now, `
197
+ + `before compiling, is the cheap order: this pass has already found everything else that is wrong.`,
198
+ };
199
+ }
200
+ /**
201
+ * Every pasted lookup value on one node.
202
+ *
203
+ * The JUSTIFICATION here is the one the operation's reference page settled on
204
+ * (#706) and this message had not caught up with. It used to assert the value
205
+ * is an opaque, connection-specific id — false for 320 of the library's 2,563
206
+ * lookup rows, and false for a field it fires on constantly: Slack's `send_as`
207
+ * sends the word `bot`, which is the same word on every connection. Nothing in
208
+ * the metadata says which case a field is in, so the id claim is stated as the
209
+ * CONDITION it always was, and the value each field sends is named so the
210
+ * reader can tell which case theirs is.
211
+ */
212
+ function literalIdDiagnostic(where, spec, rows, descriptors) {
213
+ const sends = rows.map(({ field, lookup }) => `\`${field}\` sends \`${lookup.objectName ?? lookup.path}.${lookup.value}\``);
214
+ const calls = rows.map(({ field, lookup }) => ` ${lookupPhrases(field, lookup, spec, descriptors).call}`);
215
+ const args = rows.map(({ field, lookup }) => resolveFlagArg(field, lookup.by[0] ?? 'id', '<value>'));
216
+ return {
217
+ level: 'warning',
218
+ code: 'LOOKUP_LITERAL_ID',
219
+ message: `${where}: ${sends.join(', ')} — the value the connector defines, not the label `
220
+ + `you know it by. A literal is CORRECT where that value is a fixed vocabulary `
221
+ + `(\`bot\`, a country code), and where you have no connection to resolve against: `
222
+ + `resolving is a live call, so a draft authored offline writes the literal and says so. `
223
+ + `It is wrong only where the value is an opaque id, which is also connection-specific — `
224
+ + `a pasted one resolves to nobody, or to somebody else, against a different connection. `
225
+ + `If this is that case, resolve ${rows.length === 1 ? 'it' : 'them'} late:\n`
226
+ + calls.join('\n')
227
+ + `\nand record ${rows.length === 1 ? 'the value' : 'the values'} once, against a live connection:\n`
228
+ + resolveBlock(spec, args),
229
+ };
230
+ }
231
+ /**
232
+ * The schema rules: an unprepared object, and unprepared custom fields.
233
+ *
234
+ * Same reasoning as the lookup rules above — the author needs no prior
235
+ * knowledge, and the diagnostic arrives in the loop they are already in, with
236
+ * the one command that discharges it. Both are WARNINGS: compile accepts a
237
+ * connection-scoped object (stamped `objectNameUnverified`) and passes the
238
+ * inputs through, so `check` is the one place the gap is visible BEFORE a
239
+ * process dispatches against a schema nothing validated.
240
+ *
241
+ * The custom-fields rule is deliberately narrower than "unprepared op": a step
242
+ * that uses only baked fields owes nothing, and warning on it anyway teaches
243
+ * the author that the warning is noise. It fires only for inputs OUTSIDE the
244
+ * tenant-agnostic snapshot — the moment hand-guessing actually starts.
245
+ */
246
+ export function checkConnectorSchema(where, spec, opts, dialect, report) {
247
+ const readiness = opts.schemaReadiness?.(spec.key, spec.action, spec.version, spec.object);
248
+ if (!readiness || readiness.prepared)
249
+ return;
250
+ const prepare = prepareCommand(spec.key, spec.action);
251
+ if (readiness.kind === 'object') {
252
+ report({
253
+ level: 'warning',
254
+ code: 'OBJECT_UNPREPARED',
255
+ message: `${where}: object "${spec.object}" has no locally materialized schema — `
256
+ + `this operation resolves its object against the connection, so its fields cannot be `
257
+ + `validated here and compile will emit it unverified. Prepare it once:\n`
258
+ + ` ${prepare} --object ${spec.object}\n`
259
+ + `The name is matched against the connected catalog (exact, then case-insensitive on `
260
+ + `name and display name); several matches fail with the candidates to choose from, and `
261
+ + `the prepared descriptor is the import to switch to.`,
262
+ ...(readiness.canonical ? { suggestion: readiness.canonical } : {}),
263
+ });
264
+ return;
265
+ }
266
+ if (readiness.kind === 'customFields') {
267
+ // Match by full dotted path, the same descent `checkConnectorLookups`
268
+ // uses above — a top-level comparison would let `contact:
269
+ // { cf_custom: … }` slide because `contact` is a baked head, and a nested
270
+ // hand-guessed custom field is precisely the moment this warning is for.
271
+ // Wire-array markers are stripped from the baked names (`users[*]` is
272
+ // authored as `users`).
273
+ const baked = (readiness.bakedFields ?? []).map((name) => name.replace(/\[\*\]/g, ''));
274
+ const unknown = [];
275
+ const visit = (value, path) => {
276
+ const dotted = path.join('.');
277
+ // A baked field, or deeper than one — the name is in the vocabulary;
278
+ // whether the VALUE fits is compile's business, not this warning's.
279
+ if (baked.some((b) => b === dotted || dotted.startsWith(b + '.')))
280
+ return;
281
+ // The head of baked fields (`contact` when `contact.email` is baked):
282
+ // descend to find the precise gap underneath it. Array elements keep
283
+ // their parent's path — the baked names index arrays as `items[*].sku`,
284
+ // normalized above to `items.sku` — so a guessed field inside a list
285
+ // element (`lineItems: [{ cf_custom }]`) warns exactly like one under
286
+ // an object head.
287
+ if (baked.some((b) => b.startsWith(dotted + '.'))) {
288
+ if (Array.isArray(value)) {
289
+ for (const element of value)
290
+ visit(element, path);
291
+ }
292
+ else if (!isLookupToken(value) && dialect.classify(value).kind === 'container') {
293
+ for (const [k, v] of Object.entries(value)) {
294
+ visit(v, [...path, k]);
295
+ }
296
+ }
297
+ return;
298
+ }
299
+ // Neither a baked name nor on the way to one — this IS the gap,
300
+ // reported once at its head.
301
+ unknown.push(dotted);
302
+ };
303
+ for (const [name, value] of Object.entries(spec.inputs ?? {}))
304
+ visit(value, [name]);
305
+ // Array elements share their parent's path, so one guessed field across
306
+ // many list rows is one gap, not one per row.
307
+ const distinct = [...new Set(unknown)];
308
+ if (distinct.length === 0)
309
+ return;
310
+ report({
311
+ level: 'warning',
312
+ code: 'CUSTOM_FIELDS_UNPREPARED',
313
+ message: `${where}: ${distinct.map((n) => `\`${n}\``).join(', ')} `
314
+ + `${distinct.length === 1 ? 'is' : 'are'} not in the tenant-agnostic snapshot, and this `
315
+ + `operation resolves its real field set — custom fields included — from a live `
316
+ + `connection. Do not hand-guess the names; materialize the schema once:\n`
317
+ + ` ${prepare}\n`
318
+ + `then author against the prepared descriptor (its field names and doc comments are `
319
+ + `the vocabulary) and re-run check.`,
320
+ });
321
+ }
322
+ }
323
+ /**
324
+ * The field contract and the binding names `compile` enforces, reported here first.
325
+ *
326
+ * Both rules run only with the facts in hand (a library for the fields, a
327
+ * bindings.json for the names); without them `check` stays a pure function of the
328
+ * built model and says nothing, as before.
329
+ */
330
+ export function checkConnectorContract(where, spec, opts, report,
331
+ /** The activity's own id, for {@link connectionConnectorConflicts}; omit to record nothing. */
332
+ id) {
333
+ const inputErr = opts.connectorInputs?.({ key: spec.key, action: spec.action, version: spec.version, object: spec.object }, spec.inputs ?? {});
334
+ if (inputErr) {
335
+ report({ level: 'error', code: 'CONNECTOR_INPUT', message: `${where}: ${inputErr}` });
336
+ }
337
+ checkBindingLabels(where, spec, opts, (code, message) => report({ level: 'error', code, message }), { key: spec.key, action: spec.action }, id === undefined ? undefined : { id, connector: spec.key });
338
+ }
339
+ /**
340
+ * The binding-label rules `serialize`'s `resolveBinding` enforces, for one
341
+ * `{ connection, folder }` pair — reported at every site that resolves one
342
+ * (connector step, event trigger and `.entryPoint` onEvent, waitForEvent, managed
343
+ * HTTP), with the sentence compile throws. Without this at the event and HTTP
344
+ * sites `check` printed `✓ no issues` for a source `compile` then refused.
345
+ *
346
+ * Silent without a bindings.json, and for a label that resolves to anything
347
+ * other than its own name (stub values are compile's CONNECTION_STUB warning).
348
+ */
349
+ export function checkBindingLabels(where, labels, opts, report, target,
350
+ /** The activity, and the connector its connection must belong to, for {@link connectionConnectorConflicts}. */
351
+ use) {
352
+ const label = labels?.connection;
353
+ if (use?.connector && typeof label === 'string' && label !== '') {
354
+ connectionUsage?.push({ id: use.id, label, connector: use.connector });
355
+ }
356
+ const bindings = opts.bindings;
357
+ if (!bindings || !labels)
358
+ return;
359
+ for (const attribute of ['connection', 'folder']) {
360
+ const name = labels[attribute];
361
+ if (typeof name !== 'string')
362
+ continue;
363
+ const resolved = bindings.resolve(name);
364
+ if (resolved === name) {
365
+ report('BINDING_SELF_NAME', bindingSelfNameMessage(where, name, attribute, target));
366
+ }
367
+ else if (resolved === undefined) {
368
+ const declared = undeclaredBinding(bindings, name);
369
+ if (declared) {
370
+ report('BINDING_UNDECLARED', bindingUndeclaredMessage(where, name, attribute, declared, target));
371
+ }
372
+ }
373
+ }
374
+ }
375
+ /**
376
+ * The two phrases every lookup diagnostic needs: what the field sends, and how
377
+ * to resolve it.
378
+ *
379
+ * The `call` names BOTH authoring forms, and names which one the alias belongs
380
+ * to. Printing `lookup(<Descriptor>, 'entityName').byDisplayName('<value>')`
381
+ * alone — the old text — was a remedy with an unstated precondition: aliases
382
+ * are generated onto the descriptor, and `<Descriptor>` is a placeholder the
383
+ * reader has to fill from somewhere. An author holding the connector key and
384
+ * action instead (the form the library's Generic CRUD half is authored in, and
385
+ * the majority of its lookup fields) substitutes those for the placeholder,
386
+ * keeps the alias, and gets `lookup(...).byDisplayName is not a function`. Six
387
+ * flow-eval tasks did precisely that, each losing a check-edit-check round to
388
+ * a message that had told them to.
389
+ */
390
+ export function lookupPhrases(field, lookup, spec, descriptors = true) {
391
+ const target = `${lookup.objectName ?? lookup.path}.${lookup.value}`;
392
+ const by = lookup.by[0] ?? 'id';
393
+ const alias = Object.keys(lookup.aliases)[0];
394
+ // The key/action form first: it needs no import, so it is the one line that
395
+ // is paste-ready from the message alone.
396
+ const generic = `lookup('${spec.key}', '${spec.action}', '${field}').by('${by}', '<value>')`;
397
+ const call = alias && descriptors
398
+ ? `${generic}\n or \`lookup(D, '${field}').${alias}('<value>')\`, where D is the `
399
+ + `descriptor \`${prepareCommand(spec.key, spec.action)}\` generates — the aliases exist only there`
400
+ : generic;
401
+ return { target, call };
402
+ }
403
+ /**
404
+ * `senderEmail`, `emailAddress`, `e_mail`, `userPrincipalName` — an address. Applied
405
+ * to the LEAF an expression reads, and to the lookup's own `value` / `by` names,
406
+ * because whether an address is wrong depends on what the field sends: SendGrid's
407
+ * `from` sends `from_email`, PagerDuty's `From` sends `email` — there an e-mail IS
408
+ * the id. `emailId` is left to the id rule.
409
+ */
410
+ const EMAIL_LEAF = /e[-_]?mail|principal/i;
411
+ /**
412
+ * `id`, `accountId`, `channelKey`, `record_id`, `CHANNEL_KEY` — the author is
413
+ * already naming an id. Case-sensitive on purpose: `valid`, `paid` and `monkey`
414
+ * end in the same letters and are not ids.
415
+ */
416
+ const ID_LEAF = /(?:^|[a-z0-9])(?:Id|ID|Key|KEY)$|(?:^|_)(?:id|key|ID|KEY|Id|Key)$/;
417
+ /**
418
+ * The lookup rule for a RUNTIME value.
419
+ *
420
+ * `check` cannot evaluate `$vars.start.output.senderEmail`, but it does know what
421
+ * the field it lands in must carry — `reporter.accountId` — because the descriptor
422
+ * says so. That is the fact the author of the 09-01 escalation flow never saw: the
423
+ * generated type read `id: ConnectorLookupValue<string>`, the field was required,
424
+ * the only person-shaped input in scope was an e-mail, and nothing between the
425
+ * source and the tenant said an address is not an account id. Jira did, at run
426
+ * time, as `[102003] Integration Services bad request`.
427
+ *
428
+ * Three tiers, decided by the LEAF the expression reads, because that name is
429
+ * the only signal available before the process runs:
430
+ * - id-shaped (`…Id`, `…Key`, or the lookup's own `value`): silence. The author
431
+ * has said it is an id; a mistaken id is `LOOKUP_LITERAL_ID`'s move-between-
432
+ * connections problem, not this one.
433
+ * - e-mail-shaped, into a lookup that neither SENDS an e-mail (`value`) nor
434
+ * SEARCHES by one (`by`): an ERROR. Jira's reporter (`accountId`, by
435
+ * displayName/accountId) is the measured case — the provider rejects the
436
+ * address, and stopping here is cheaper than a faulted run. Into a lookup
437
+ * whose `value` is itself an e-mail (SendGrid `from` → `from_email`, PagerDuty
438
+ * `From` → `email`, 19 fields in the library): silence — the address is the
439
+ * id. Into one that only searches by e-mail (ActiveCampaign `contact` → `id`,
440
+ * by `email`, 139 fields): a WARNING — the provider indexes people by address,
441
+ * so some accept it in the id slot and some do not; the reader decides.
442
+ * - anything else (a name, a whole output object, a concatenation): a WARNING
443
+ * that states the identity, so the reader can decide.
444
+ */
445
+ function runtimeLookupDiagnostic(where, spec, field, lookup, js, dialect) {
446
+ const refs = extractRefs(js, dialect.namespace);
447
+ const single = refs.length === 1 && js.trim() === refs[0].raw ? refs[0] : undefined;
448
+ const leaf = single ? (single.path.length ? single.path[single.path.length - 1] : single.root) : undefined;
449
+ // The lookup's value may be dotted (`properties.email`): compare against its last segment too.
450
+ const valueLeaf = lookup.value.split('.').pop() ?? lookup.value;
451
+ const valueIsEmail = EMAIL_LEAF.test(lookup.value);
452
+ if (leaf && (ID_LEAF.test(leaf)
453
+ || leaf.toLowerCase() === lookup.value.toLowerCase()
454
+ || leaf.toLowerCase() === valueLeaf.toLowerCase()
455
+ || (valueIsEmail && EMAIL_LEAF.test(leaf))))
456
+ return undefined;
457
+ const { target, call } = lookupPhrases(field, lookup, spec, dialect.descriptors);
458
+ const searchesByEmail = lookup.by.some((b) => EMAIL_LEAF.test(b));
459
+ const isEmail = !!leaf && EMAIL_LEAF.test(leaf) && !valueIsEmail && !searchesByEmail;
460
+ const message = `${where}: \`${field}\` is a lookup field bound to the runtime expression `
461
+ + `\`${js}\`. At run time it must carry a \`${target}\` — the id the connector stores — `
462
+ + `not a name or an address: the connector rejects anything else when the flow runs, and `
463
+ + `neither compile nor validate can tell. Resolve the value you know instead:\n`
464
+ + ` ${call}\n`
465
+ + `then record it once with \`${prepareCommand(spec.key, spec.action)} `
466
+ + `--resolve ${resolveFlagArg(field, lookup.by[0] ?? 'id', '<value>')}\`; or read the id from a step whose `
467
+ + `output returned one (\`${lookup.value}\`).`
468
+ + (isEmail
469
+ ? `\n\`${single.raw}\` reads \`${leaf}\`, an e-mail address, not a \`${target}\`.`
470
+ : '');
471
+ return { level: isEmail ? 'error' : 'warning', code: 'LOOKUP_RUNTIME_VALUE', message };
472
+ }
473
+ /**
474
+ * The connection uses {@link checkBindingLabels} saw during the current check —
475
+ * the model and every child model, which share one `bindings[]`. Module state
476
+ * rather than a `ConnectorCheckOpts` field because a check is synchronous and
477
+ * this is not an input; {@link collectConnectionUses} saves and restores it, so
478
+ * a nested check cannot mix two models.
479
+ */
480
+ let connectionUsage;
481
+ /** Run `check`, and hand back the connection uses its binding-label rules saw. */
482
+ export function collectConnectionUses(check) {
483
+ const outer = connectionUsage;
484
+ connectionUsage = [];
485
+ try {
486
+ check();
487
+ return connectionUsage;
488
+ }
489
+ finally {
490
+ connectionUsage = outer;
491
+ }
492
+ }
493
+ /**
494
+ * CONNECTION_CONNECTOR_CONFLICT — the rule `serialize` enforces on the emitted
495
+ * document (`connectionConnectors`, bindings-v2.ts), reported here with the same
496
+ * sentence so `check` does not print `✓ no issues` for a source `compile` then
497
+ * refuses. Labels are grouped by what they resolve to, so two labels bound to
498
+ * one connection id are one connection, as they are in the artifact.
499
+ */
500
+ export function connectionConnectorConflicts(uses, opts) {
501
+ const byConnection = new Map();
502
+ for (const use of uses) {
503
+ const key = opts.bindings?.resolve(use.label) ?? use.label;
504
+ const byConnector = byConnection.get(key) ?? new Map();
505
+ byConnection.set(key, byConnector);
506
+ byConnector.set(use.connector, [...(byConnector.get(use.connector) ?? []), use.id]);
507
+ }
508
+ const out = [];
509
+ for (const [key, byConnector] of byConnection) {
510
+ if (byConnector.size < 2)
511
+ continue;
512
+ out.push({
513
+ id: [...byConnector.values()][1][0],
514
+ message: connectionConnectorConflictMessage(key, byConnector),
515
+ });
516
+ }
517
+ return out;
518
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * core/connector-inputs — a connector operation's authored inputs, validated
3
+ * against its declared field list and ROUTED to the wire slots the platform reads.
4
+ *
5
+ * Shared by every family that authors a connector activity. It was Flow's, inside
6
+ * `serialize.ts`, and BPMN had a fourteen-line substitute: an input the library
7
+ * did not declare went silently to the body, a missing required field was never
8
+ * reported, and a `lookup()` token was refused outright because nothing here
9
+ * read `connectors-local/resolutions.json`. That last one is why
10
+ * `skill-bpmn-paginated-reference-lookup` could resolve a Slack channel id and
11
+ * then emit a process that did not carry it.
12
+ *
13
+ * Two things differ between families and are injected rather than assumed:
14
+ *
15
+ * - `dialect` — which values are opaque (a Flow `Expr`, a BPMN `=`-expression).
16
+ * An opaque value is not type-checked against the field's declared type,
17
+ * because nothing here can evaluate it.
18
+ * - `render` — how an authored value becomes a wire value. Flow runs `renderDeep`
19
+ * to lower `Expr` objects into `$vars.…` source; BPMN's values are already
20
+ * plain, so it passes them through.
21
+ *
22
+ * The ROUTING is the library's business either way: a field's location is a
23
+ * property of the operation, not of the family emitting it.
24
+ */
25
+ import { type LookupResolutions, type LookupSpec } from './lookups.js';
26
+ import type { MultipartPart, ResolvedConnector } from './library.js';
27
+ import type { ExprDialect } from './connector-checks.js';
28
+ /** How one family's values reach the wire. */
29
+ export interface ConnectorInputOpts {
30
+ /** Which values are opaque, and so exempt from the declared-type check. */
31
+ dialect: ExprDialect;
32
+ /** An authored value → its wire value. Identity when the family needs no lowering. */
33
+ render?: (value: unknown) => unknown;
34
+ /** Everything `registry prepare --resolve` has recorded in the local overlay. */
35
+ resolutions?: LookupResolutions;
36
+ /** The operation's generated lookup table, keyed by field — for the missing-required hint. */
37
+ lookups?: Readonly<Record<string, LookupSpec>>;
38
+ /** `ignore` skips an unresolved lookup instead of refusing it — what `check` wants. */
39
+ unresolvedLookups?: 'error' | 'ignore';
40
+ }
41
+ /** Flow's dialect, re-exported so `serialize.ts` need not reach past this module. */
42
+ export { FLOW_EXPR_DIALECT } from './connector-checks.js';
43
+ /**
44
+ * What `compile` would refuse these connector inputs with, or `undefined` when it
45
+ * would accept them — the SAME code path `buildConnectorInputs` runs at emit time,
46
+ * so `check` and `compile` cannot disagree about a missing required field, an
47
+ * unknown or mistyped input, or a container part passed as a value. Unresolved
48
+ * lookups are the one thing skipped: `check` already reports them as
49
+ * LOOKUP_UNRESOLVED with the resolution recipe.
50
+ */
51
+ export declare function connectorInputErrors(rc: ResolvedConnector, inputs: Record<string, unknown>, resolutions?: LookupResolutions, lookups?: Readonly<Record<string, LookupSpec>>, wire?: Pick<ConnectorInputOpts, 'dialect' | 'render'>): string | undefined;
52
+ export declare function buildConnectorInputs(rc: any, inputs: Record<string, unknown>, opts: ConnectorInputOpts): ConnectorSlots;
53
+ /**
54
+ * `inputMetadata.multipart.bodyFieldName` names the CONTAINER part — the one the
55
+ * runtime composes from `bodyParameters`. Measured on the designer's own nodes in
56
+ * the eval archive: `body` ×413, `RagRequest` ×1 (GenAI `content-generation`, whose
57
+ * string part is named that), and `body` on 91 of 93 file-only operations. A
58
+ * mis-named container is fatal at dispatch — hardcoding `body` for GenAI made
59
+ * Integration Service answer `400 The request body is empty` (verified live
60
+ * 2026-09-02) — so it is the string part's own name, with `body` only as the
61
+ * default for an operation that has no string part.
62
+ */
63
+ export declare function multipartBodyFieldName(parts: MultipartPart[]): string;
64
+ /** The `inputs.detail` slots a connector's inputs are routed to, by field location. */
65
+ interface ConnectorSlots {
66
+ bodyParameters?: Record<string, unknown>;
67
+ pathParameters?: Record<string, unknown>;
68
+ queryParameters?: Record<string, unknown>;
69
+ multipartParameters?: Array<{
70
+ name: string;
71
+ dataType: string;
72
+ value?: unknown;
73
+ }>;
74
+ inputMetadata?: {
75
+ type: 'multipart';
76
+ multipart: {
77
+ bodyFieldName: string;
78
+ };
79
+ };
80
+ }