@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,321 @@
1
+ /**
2
+ * core/event-checks — the connector-EVENT rules, shared by every family that can
3
+ * subscribe to one.
4
+ *
5
+ * The activity twin is `core/connector-checks.ts`. These were Flow's, and like
6
+ * those they were never flow-shaped: both functions take the subscription as
7
+ * `unknown` and duck-type `{ connector, event, where, object, filters, version,
8
+ * connection }`, because that is all they need. BPMN's `.eventTrigger()` /
9
+ * `.waitForEvent()` node carries the same facts under its own names, and
10
+ * `bpmn/check.ts` adapts one to the other.
11
+ *
12
+ * What BPMN does NOT carry is `filters` — payload filtering is a Flow surface —
13
+ * so the `EVENT_FILTER_*` family simply does not fire there. The rules that do
14
+ * are the ones about the SUBSCRIPTION: whether it resolves at all, whether a
15
+ * generic event named its object, and whether its scope names parameters the
16
+ * operation actually declares. `skill-bpmn-outlook-waitfor-email` in
17
+ * `flow-v2-run-8` failed on exactly that last question.
18
+ */
19
+ import { isLookupToken, resolvedValue, unresolvedLookupMessage, } from './lookups.js';
20
+ import { prepareCommand } from './cli-spelling.js';
21
+ import { EVENT_FILTER_OPERATORS, readEventFilter, eventFilterProblem } from './event-filters.js';
22
+ import { EVENT_BYOA_UNVERIFIED, byoaUnverified } from './byoa.js';
23
+ /**
24
+ * Ask the library what event parameters a subscription's operation declares.
25
+ * Returns `undefined` when the subscription is too broken to look up (its own
26
+ * diagnostics say so) or when no resolver was supplied — both mean "unknown", and
27
+ * unknown fails open.
28
+ */
29
+ export function eventParamsFor(subscription, opts) {
30
+ if (!opts.eventParams)
31
+ return undefined;
32
+ const sub = subscription;
33
+ if (typeof sub?.connector !== 'string' || typeof sub?.event !== 'string')
34
+ return undefined;
35
+ if (sub.connector === '' || sub.event === '')
36
+ return undefined;
37
+ // A GENERIC event's schema is per OBJECT once the library carries baked
38
+ // per-object variants, so the object is part of the subscription's identity
39
+ // here. Without it the lookup asks for a node type that resolves to no
40
+ // single variant and answers "unknown", which reads as EVENT_UNPREPARED on
41
+ // an event the library actually knows.
42
+ const object = typeof sub.object === 'string' && sub.object !== ''
43
+ ? sub.object : undefined;
44
+ return opts.eventParams(sub.connector, sub.event, object);
45
+ }
46
+ /**
47
+ * The event-side resolution rules — the trigger twin of `checkConnectorLookups`,
48
+ * plus three rules with no activity counterpart:
49
+ *
50
+ * - `EVENT_UNKNOWN` — an ERROR: a library is loaded and it cannot resolve the
51
+ * subscription, so `compile` will refuse it. The message is the resolver's
52
+ * own (`eventResolveError`), the same text compile throws. Checked first; when
53
+ * it fires, EVENT_UNPREPARED and the filter rules below are skipped for this
54
+ * subscription, since there is no entry for them to read.
55
+ * - `EVENT_UNPREPARED` — the event RESOLVES but the library knows nothing
56
+ * about its parameters (or no `eventResolveError` resolver was supplied):
57
+ * the fallback case for a connector the shipped library was not generated
58
+ * against. A warning, because compile still succeeds. Prints the one
59
+ * command that makes the event checkable.
60
+ * - `EVENT_BYOA_UNVERIFIED` — an ERROR: the event only fires on a BYOA
61
+ * (bring-your-own-app) connection, and the bound connection id has no record
62
+ * of `prepare` having checked it (`core/byoa.ts` has the rule and when it
63
+ * stays silent). A standard connection deploys and the trigger never fires;
64
+ * the usual way in is binding the connection by preparing one of the
65
+ * connector's ACTIONS, which checks nothing about BYOA. `compile` throws the
66
+ * same text.
67
+ * - `EVENT_FILTER_UNKNOWN_FIELD` — a filter leaf naming a field outside the
68
+ * prepared vocabulary is an ERROR, not a warning, because the platform's own
69
+ * configure DROPS an unmatched leaf and reports Success: the deployed
70
+ * trigger then fires on events the filter should exclude, silently. Matching
71
+ * is case-sensitive, so the near-miss hint checks case first. When the
72
+ * vocabulary is unknown or known-empty this stays quiet — fail open.
73
+ *
74
+ * Unresolved `where` lookups follow the activity rule exactly: a WARNING here
75
+ * (compile repeats it as the hard failure), with the recorded answer looked up
76
+ * under the event-form node type — the form `prepare` stores.
77
+ */
78
+ export function checkEventResolution(subscription, where, kind, opts, error, warn, eventParams) {
79
+ const sub = subscription;
80
+ if (!sub || typeof sub.connector !== 'string' || typeof sub.event !== 'string'
81
+ || sub.connector === '' || sub.event === '')
82
+ return;
83
+ // Captured because the guard above narrows `sub.connector` / `sub.event` only
84
+ // in THIS scope: a property narrowing does not survive into the `filters`
85
+ // callback below, where they are `unknown` again. The old messages hid that
86
+ // by interpolating them into template literals, which accept anything.
87
+ const connectorKey = sub.connector;
88
+ const eventName = sub.event;
89
+ // Exactly what `serialize` passes to `resolveEvent` — `sub.version` and
90
+ // `sub.object` as the author wrote them — so the verdict is compile's.
91
+ const unresolved = opts.eventResolveError?.(kind, {
92
+ connector: connectorKey,
93
+ event: eventName,
94
+ ...(typeof sub.version === 'string' ? { version: sub.version } : {}),
95
+ ...(typeof sub.object === 'string' ? { object: sub.object } : {}),
96
+ });
97
+ if (unresolved !== undefined) {
98
+ error('EVENT_UNKNOWN', `${where}: ${unresolved}`);
99
+ return;
100
+ }
101
+ const object = typeof sub.object === 'string' && sub.object !== '' ? sub.object : undefined;
102
+ const byoa = opts.eventFacts && byoaUnverified(where, { connector: connectorKey, event: eventName, ...(object ? { object } : {}), connection: sub.connection }, opts.eventFacts(connectorKey, eventName, object), opts.byoa, opts.bindings);
103
+ if (byoa)
104
+ error(EVENT_BYOA_UNVERIFIED, byoa);
105
+ if (eventParams?.objectUnprepared && object) {
106
+ // The generic template answers for every object, so resolution SUCCEEDS
107
+ // and nothing downstream notices that the object named here has no
108
+ // schema: no unprepared warning, and a filter vocabulary that fails open.
109
+ // That is the silent case this whole surface exists to remove, one level
110
+ // up from the activity it was first found on.
111
+ warn('OBJECT_UNPREPARED', `${where}: object \`${object}\` has no prepared schema for `
112
+ + `\`${sub.connector}\` \`${sub.event}\`, and this connector's objects come from `
113
+ + `the connection, so the library cannot carry one. Materialize it:\n`
114
+ + ` ${prepareCommand(connectorKey, eventName)} --object ${object}\n`
115
+ + `Until then its event parameters and filterable fields are unknown and `
116
+ + `nothing here can check them.`);
117
+ }
118
+ if (opts.eventParams && eventParams === undefined) {
119
+ warn('EVENT_UNPREPARED', `${where}: the library knows nothing about connector event \`${sub.connector}\` \`${sub.event}\` — `
120
+ + `its parameters and filterable fields cannot be verified offline. Prepare it against a live connection:\n`
121
+ + ` ${prepareCommand(connectorKey, eventName)}\n`
122
+ + `and re-run check: the prepared overlay makes this event checkable.`);
123
+ }
124
+ const resolutions = opts.resolutions ?? {};
125
+ const scope = sub.where && typeof sub.where === 'object'
126
+ ? Object.entries(sub.where) : [];
127
+ for (const [key, value] of scope) {
128
+ if (!isLookupToken(value))
129
+ continue;
130
+ if (resolvedValue(value, resolutions) !== undefined)
131
+ continue;
132
+ warn('LOOKUP_UNRESOLVED', `${where}: \`where.${key}\` ` + unresolvedLookupMessage(value)
133
+ + `\nCompile will fail until this is recorded.`);
134
+ }
135
+ const names = opts.eventFilterFields?.(sub.connector, sub.event, object);
136
+ if (names && names.length > 0 && Array.isArray(sub.filters)) {
137
+ const known = new Set(names);
138
+ sub.filters.forEach((leaf, i) => {
139
+ const { read } = readEventFilter(leaf);
140
+ if (!read || known.has(read.field))
141
+ return;
142
+ const caseSlip = names.find((n) => n.toLowerCase() === read.field.toLowerCase());
143
+ // A case slip is a mistake whatever the vocabulary's completeness — the
144
+ // platform matches case-sensitively, so `Subject` is dropped even where
145
+ // `subject` exists. An unrecognized name is only certainly wrong when
146
+ // the vocabulary is COMPLETE; where required selectors decide the field
147
+ // set and nothing can supply them, refusing a legal field would be the
148
+ // worse error, so it warns.
149
+ const selectors = eventParams?.selectors ?? [];
150
+ // A hybrid connector's custom filter fields are left for `prepare` BY
151
+ // DESIGN, so the baked list is a strict subset of the tenant's and a
152
+ // name outside it may be perfectly real. Erroring there tells the author
153
+ // the platform will drop a field it will actually honour, and the only
154
+ // way out is deleting a correct filter — "not in my snapshot" mistaken
155
+ // for "wrong", which is the trap CUSTOM_FIELDS_UNPREPARED was narrowed
156
+ // around for activities. Same shape here, and the same remedy: name the
157
+ // prepare that closes the gap.
158
+ if (eventParams?.customFieldsUnprepared && !caseSlip) {
159
+ warn('CUSTOM_FIELDS_UNPREPARED', `${where}: filters[${i}] names \`${read.field}\`, which is not in the `
160
+ + `tenant-agnostic vocabulary for \`${sub.connector}\` \`${sub.event}\`. This `
161
+ + `connector discovers CUSTOM FIELDS per connection, so the baked list is a `
162
+ + `subset by design and this name may be a real field of yours. Prepare the `
163
+ + `event against your connection to check it for real:\n`
164
+ + ` ${prepareCommand(connectorKey, eventName)}`
165
+ + `${object ? ` --object ${object}` : ''}\n`
166
+ + `Baked fields: ${names.slice(0, 12).join(', ')}${names.length > 12 ? ', …' : ''}.`);
167
+ return;
168
+ }
169
+ // Same shape as the branch above, for the gap preparing cannot close.
170
+ // `customFieldsUnprepared` says "run prepare and this resolves";
171
+ // `filtersPartial` says "no call reaches these fields at all", so the
172
+ // remedy is the designer, not another command.
173
+ //
174
+ // Yields to the selector branch below, which reaches the same WARNING by
175
+ // a more specific route: where `project` / `issuetype` select the schema
176
+ // it can name them, and "these two values decide the field set" is more
177
+ // use to an author than "this connector reads its schema from the
178
+ // connection". Both are true of Jira; the named one is the better message.
179
+ if (eventParams?.filtersPartial && !caseSlip && selectors.length === 0) {
180
+ warn('EVENT_FILTER_UNKNOWN_FIELD', `${where}: filters[${i}] names \`${read.field}\`, which is not in the known `
181
+ + `filter vocabulary for \`${sub.connector}\` \`${sub.event}\`. This connector `
182
+ + `discovers custom fields per connection, and the trigger metadata call takes no `
183
+ + `parent values (\`uip is triggers describe\` has no \`-f\`), so the list below is a `
184
+ + `subset NO prepare can complete — this name may be a real field of yours. `
185
+ + `Verify it against the trigger in the designer before deploying.\n`
186
+ + `Known fields: ${names.slice(0, 12).join(', ')}${names.length > 12 ? ', …' : ''}.`);
187
+ return;
188
+ }
189
+ const baseline = selectors.length > 0 && !caseSlip;
190
+ (baseline ? warn : error)('EVENT_FILTER_UNKNOWN_FIELD', `${where}: filters[${i}] names \`${read.field}\`, which is not a filterable field of `
191
+ + `\`${sub.connector}\` \`${sub.event}\`. The platform DROPS an unmatched leaf and the deployed `
192
+ + `trigger fires on events this filter should exclude. `
193
+ + (caseSlip
194
+ ? `Did you mean \`${caseSlip}\`? Matching is case-sensitive.`
195
+ : `Filterable fields: ${names.slice(0, 12).join(', ')}${names.length > 12 ? ', …' : ''}.`)
196
+ + (baseline
197
+ ? ` This is a WARNING rather than an error because \`${selectors.join('`, `')}\` `
198
+ + `select this event's schema and no discovery call accepts their values, so the `
199
+ + `list above is a baseline the real vocabulary extends. Verify the name against `
200
+ + `the trigger in the designer before deploying.`
201
+ : ''));
202
+ });
203
+ }
204
+ }
205
+ export function checkEventSubscription(subscription, where, at, warn, eventParams) {
206
+ const sub = subscription;
207
+ if (!sub || typeof sub !== 'object') {
208
+ at('EVENT_NO_SUBSCRIPTION', `${where} has no event subscription. Pass one: `
209
+ + `{ connector: 'uipath-microsoft-outlook365', event: 'email-received', where: { parentFolderId: '<id>' } }.`);
210
+ return;
211
+ }
212
+ if (typeof sub.connector !== 'string' || sub.connector === '') {
213
+ at('EVENT_NO_CONNECTOR', `${where} has no \`connector\` key (e.g. 'uipath-microsoft-outlook365').`);
214
+ }
215
+ if (typeof sub.event !== 'string' || sub.event === '') {
216
+ at('EVENT_NO_EVENT', `${where} has no \`event\` name (e.g. 'email-received').`);
217
+ }
218
+ const scope = sub.where;
219
+ const scopeEntries = scope && typeof scope === 'object'
220
+ ? Object.entries(scope)
221
+ .filter(([, v]) => v !== undefined && v !== null && String(v) !== '')
222
+ : [];
223
+ const scopeKeys = scopeEntries.map(([k]) => k);
224
+ // The object an author put in `where` by mistake — `objectName` is what the
225
+ // platform's own `node configure --detail` calls it, so it is the natural slip.
226
+ const scopeObject = scopeEntries.find(([k]) => /^(objectName|entityName|object)$/i.test(k))?.[1];
227
+ const object = typeof sub.object === 'string' && sub.object !== '' ? sub.object : undefined;
228
+ const say = warn ?? at;
229
+ // The OBJECT half of the contract. A GENERIC event (`record-created` on Data
230
+ // Fabric, Salesforce, ServiceNow, Jira, …) fires for ONE object of the connection
231
+ // and its library entry names none — so `object` is REQUIRED, and its absence is
232
+ // an error: the node would carry `objectName: ""`, validate, and be refused by
233
+ // the platform's dispatcher (or watch nothing). A CURATED event has its object
234
+ // built in, so an `object` there names a choice that does not exist.
235
+ if (eventParams?.generic) {
236
+ if (object === undefined) {
237
+ at('EVENT_GENERIC_NO_OBJECT', `${where} subscribes to a GENERIC event: \`${String(sub.connector)}\` \`${String(sub.event)}\` fires for ONE `
238
+ + `object of the connection (an entity, a table, a custom object) and the library carries no default. `
239
+ + `Name it with \`object\`.`
240
+ + (scopeObject !== undefined
241
+ ? ` \`where: { ${scopeKeys.join(', ')} }\` is not it — \`where\` holds event PARAMETERS, and this `
242
+ + `operation takes none; the value you put there is the object.`
243
+ : ` \`uip is triggers objects ${String(sub.connector)} <EVENT> --connection-id <id>\` lists the objects `
244
+ + `the bound connection exposes.`), `object: '${scopeObject !== undefined ? String(scopeObject) : '<Object>'}'`);
245
+ }
246
+ }
247
+ else if (object !== undefined && eventParams?.known) {
248
+ at('EVENT_OBJECT_NOT_GENERIC', `${where} passes \`object: '${object}'\`, but \`${String(sub.connector)}\` \`${String(sub.event)}\` is a `
249
+ + `CURATED event whose object is built in`
250
+ + (eventParams.object ? ` (\`${eventParams.object}\`)` : '')
251
+ + `. \`object\` names the object of a GENERIC event only (\`record-created\` and its kin); drop it here.`, undefined);
252
+ }
253
+ // The operation declares no event parameters: an empty `where` is the ONLY
254
+ // correct authoring, and a non-empty one names fields the operation has not got.
255
+ if (eventParams?.known && eventParams.names.length === 0) {
256
+ if (scopeKeys.length > 0) {
257
+ say('EVENT_SCOPE_NOT_TAKEN', `${where} passes \`where: { ${scopeKeys.join(', ')} }\`, but this connector operation takes NO event `
258
+ + `parameters — its library entry declares \`inputSchema.fields: []\`, and \`uip is triggers describe\` `
259
+ + `reports \`EventParameters: []\`. The subscription is scoped by the CONNECTION alone, so these keys `
260
+ + `are carried into \`detail.eventParameters\` and matched against nothing. `
261
+ + (eventParams.generic && scopeObject !== undefined
262
+ ? `The object this GENERIC event watches goes in \`object\`, not \`where\`.`
263
+ : `Filter on the PAYLOAD instead if you meant to narrow it.`), eventParams.generic && scopeObject !== undefined
264
+ ? `object: '${String(scopeObject)}'`
265
+ : `where: {}`);
266
+ }
267
+ }
268
+ else if (scopeKeys.length === 0 && !eventParams?.generic) {
269
+ // Whether omitting the scope is a mistake is not knowable here — the platform
270
+ // accepts an unscoped subscription (it listens to everything the connection can
271
+ // see) and 12 deployed Outlook flows that do exactly that scored 1.0. Name the
272
+ // fields the operation actually has and let the author decide.
273
+ const fields = eventParams?.known && eventParams.names.length > 0
274
+ ? `Its event parameters are: ${eventParams.names.map((n) => `\`${n}\``).join(', ')}`
275
+ : "\`where\` carries the connector's event parameters — for Outlook email-received that is the mail folder";
276
+ say('EVENT_NO_SCOPE', `${where} has no \`where\`, so its subscription is UNSCOPED: it listens to everything the bound `
277
+ + `connection can see, not the one thing you named. That deploys and runs — it is broader than you `
278
+ + `probably meant, which is why this is a warning and not an error. ${fields}. `
279
+ + `The names and their ids come from the bound connection: `
280
+ + `\`uip is triggers describe <connector> <EVENT> <object> --connection-id <id>\` lists them, and `
281
+ + `\`uip is resources run list <connector> MailFolder --connection-id <id>\` resolves a folder id. `
282
+ + `If the operation takes no event parameters at all — HTTP Webhook is one — an empty \`where\` `
283
+ + `is correct, and with a library on hand this warning does not fire.`, eventParams?.known && eventParams.names.length > 0
284
+ ? `where: { ${eventParams.names[0]}: '<id>' }`
285
+ : `where: { parentFolderId: '<mail folder id>' }`);
286
+ }
287
+ const filters = sub.filters;
288
+ if (filters !== undefined) {
289
+ if (!Array.isArray(filters)) {
290
+ at('EVENT_FILTERS_NOT_ARRAY', `${where}'s \`filters\` must be an ARRAY of { field, <operator>: value } — filters are positional and `
291
+ + `carry author-supplied field names, so an object map cannot express them.`, `filters: [{ field: 'subject', contains: 'Report' }]`);
292
+ }
293
+ else {
294
+ const operators = Object.keys(EVENT_FILTER_OPERATORS).join(', ');
295
+ filters.forEach((f, i) => {
296
+ const leaf = f;
297
+ if (!leaf || typeof leaf.field !== 'string' || leaf.field === '') {
298
+ at('EVENT_FILTER_NO_FIELD', `${where}'s filters[${i}] has no \`field\` — name a field on the event payload, e.g. 'subject'.`);
299
+ }
300
+ const { keys, read } = readEventFilter(leaf);
301
+ if (keys.length === 0) {
302
+ at('EVENT_FILTER_NO_VALUE', `${where}'s filters[${i}] has no operator. Write exactly one of ${operators} with its value — `
303
+ + `\`contains\` is a case-sensitive substring test, the same one Integration Service applies `
304
+ + `server-side; \`lessThan\`/\`greaterThan\` order a number or an ISO date.`, `{ field: '${typeof leaf?.field === 'string' ? leaf.field : 'subject'}', contains: '<text>' }`);
305
+ }
306
+ else if (keys.length > 1) {
307
+ at('EVENT_FILTER_MANY_OPERATORS', `${where}'s filters[${i}] carries ${keys.length} operators (${keys.join(', ')}); a condition has exactly `
308
+ + `one. Write two conditions instead — they are ANDed.`);
309
+ }
310
+ else if (!read) {
311
+ at('EVENT_FILTER_NO_VALUE', `${where}'s filters[${i}] gives \`${keys[0]}\` a value that is not a string, number or boolean.`);
312
+ }
313
+ else {
314
+ const problem = eventFilterProblem(read);
315
+ if (problem)
316
+ at('EVENT_FILTER_BAD_VALUE', `${where}'s filters[${i}] on \`${read.field}\`: ${problem}`);
317
+ }
318
+ });
319
+ }
320
+ }
321
+ }
package/dist/decompile.js CHANGED
@@ -38,7 +38,7 @@ import { DEFAULT_TRIGGER_ID } from './core/expr.js';
38
38
  import { parseIxpProjectType } from './core/actions.js';
39
39
  import { readFlowGlobals } from './flow-variables.js';
40
40
  import { OUTCOME_HANDLE_NODE_TYPES } from './core/hitl-routing.js';
41
- import { eventFilterFromTreeLeaf } from './event-filters.js';
41
+ import { eventFilterFromTreeLeaf } from './core/event-filters.js';
42
42
  /**
43
43
  * The trigger id of the flow currently being decompiled, so a reference through a
44
44
  * RENAMED trigger is still recognized as `input('x')` rather than falling back to
@@ -10,7 +10,13 @@ export declare function nestInputs(pairs: ReadonlyArray<readonly [unknown, strin
10
10
  * helper; an enum shows its FIRST DECLARED CHOICE, typed to the declaration
11
11
  * where it can be; anything else shows `input('<name>')`.
12
12
  */
13
- export declare function exampleValue(f: Dict, symbol: string | undefined, invariant: ReadonlySet<string>, connectorKey?: string, action?: string): string;
13
+ export declare function exampleValue(f: Dict, symbol: string | undefined, invariant: ReadonlySet<string>, connectorKey?: string, action?: string,
14
+ /**
15
+ * BPMN spelling: `.connector()` there takes the key and action as STRINGS —
16
+ * there is no generated descriptor to import — and a process reads a variable
17
+ * as the `=`-expression `=vars.<name>` rather than through `input()`.
18
+ */
19
+ bpmnForm?: boolean): string;
14
20
  /** A compilable `connector(...)` call for this operation; events go to {@link eventExampleLines}. */
15
21
  export declare function exampleLines(j: Dict, fields: readonly Dict[], symbol: string | undefined, invariant: ReadonlySet<string>, v1def: Dict | undefined): string[];
16
22
  /**
@@ -93,7 +93,13 @@ export function nestInputs(pairs) {
93
93
  * helper; an enum shows its FIRST DECLARED CHOICE, typed to the declaration
94
94
  * where it can be; anything else shows `input('<name>')`.
95
95
  */
96
- export function exampleValue(f, symbol, invariant, connectorKey = "", action = "") {
96
+ export function exampleValue(f, symbol, invariant, connectorKey = "", action = "",
97
+ /**
98
+ * BPMN spelling: `.connector()` there takes the key and action as STRINGS —
99
+ * there is no generated descriptor to import — and a process reads a variable
100
+ * as the `=`-expression `=vars.<name>` rather than through `input()`.
101
+ */
102
+ bpmnForm = false) {
97
103
  const [name] = authoringField(f);
98
104
  // The lookup table is keyed by the WIRE name (`addLabelIds[*]`), which is
99
105
  // what `--resolve` and `buildConnectorInputs` both match on.
@@ -104,7 +110,7 @@ export function exampleValue(f, symbol, invariant, connectorKey = "", action = "
104
110
  const names = strList(reference.lookupNames);
105
111
  const aliases = byAliases(names);
106
112
  const first = names.length ? names[0] : "id";
107
- if (symbol) {
113
+ if (symbol && !bpmnForm) {
108
114
  if (aliases.size)
109
115
  return `lookup(${symbol}, '${wire}').${firstValue(aliases)}('…')`;
110
116
  return `lookup(${symbol}, '${wire}').by('${first}', '…')`;
@@ -131,7 +137,64 @@ export function exampleValue(f, symbol, invariant, connectorKey = "", action = "
131
137
  return pyJsonString(choice);
132
138
  }
133
139
  }
134
- return `input('${leafName(name)}')`;
140
+ return bpmnForm ? `'=vars.${leafName(name)}'` : `input('${leafName(name)}')`;
141
+ }
142
+ /**
143
+ * `### BPMN` — the same operation as a `.connector()` call on the BPMN builder.
144
+ *
145
+ * The Flow block above cannot be pasted into a `.bpmn.ts`: BPMN leads with the
146
+ * ELEMENT ID, never takes a generated descriptor (so `lookup()` is always the
147
+ * string form), and reads a variable as `=vars.x`. An agent handed this page by
148
+ * `registry search --limit 5` and told to author BPMN was being handed Flow.
149
+ */
150
+ function bpmnExampleLines(required, invariant, connectorKey, action, step, runtime, operation) {
151
+ const pairs = required.map((f) => {
152
+ const [name, , wireName] = authoringField(f);
153
+ return [wireName ? f.name : name, exampleValue(f, undefined, invariant, connectorKey, action, true)];
154
+ });
155
+ const out = ["### BPMN", "", "```ts"];
156
+ out.push(`import { bpmn } from '${PACKAGE_NAME}/bpmn';`);
157
+ if (pairs.some(([, v]) => v.includes("lookup(")))
158
+ out.push(`import { lookup } from '${PACKAGE_NAME}';`);
159
+ out.push("");
160
+ out.push(`.connector('${step}', '${connectorKey}', '${action}', {`);
161
+ if (pairs.length)
162
+ out.push(...nestInputs(pairs));
163
+ const opts = [];
164
+ if (pyTruthy(runtime.requiresConnection))
165
+ opts.push(`connection: '${afterLast(connectorKey, "-")}'`);
166
+ if (pyTruthy(runtime.requiresFolderKey))
167
+ opts.push("folder: 'shared'");
168
+ if (operation.activityType === "Generic" || operation.activityType === "GenericExpanded") {
169
+ opts.push(`object: '${pyStr(pyOr(operation.objectName, "<object>"))}'`);
170
+ }
171
+ out.push("}" + (opts.length ? `, { ${opts.join(", ")} }` : "") + ")");
172
+ out.push("```");
173
+ out.push("");
174
+ return out;
175
+ }
176
+ /**
177
+ * `### BPMN` for a connector EVENT: `.eventTrigger()` starts a process,
178
+ * `.waitForEvent()` pauses one. The subscription scope is `filter` and its
179
+ * parameters are FLAT there, where Flow's `where` nests them.
180
+ */
181
+ function bpmnEventExampleLines(params, generic, start, connectorKey, action, step) {
182
+ const method = start ? "eventTrigger" : "waitForEvent";
183
+ const opts = [`connection: '${afterLast(connectorKey, "-")}'`, "folder: 'shared'"];
184
+ if (generic)
185
+ opts.push("object: '<Object>'");
186
+ if (params.length)
187
+ opts.push(`filter: { ${params.map((p) => `${IDENT.test(p) ? p : pyJsonString(p)}: '<${p}>'`).join(", ")} }`);
188
+ return [
189
+ "### BPMN",
190
+ "",
191
+ "```ts",
192
+ `import { bpmn } from '${PACKAGE_NAME}/bpmn';`,
193
+ "",
194
+ `.${method}('${step}', '${connectorKey}', '${action}', { ${opts.join(", ")} })`,
195
+ "```",
196
+ "",
197
+ ];
135
198
  }
136
199
  /** A compilable `connector(...)` call for this operation; events go to {@link eventExampleLines}. */
137
200
  export function exampleLines(j, fields, symbol, invariant, v1def) {
@@ -162,7 +225,7 @@ export function exampleLines(j, fields, symbol, invariant, v1def) {
162
225
  const schemaDynamic = pyTruthy(operation.objectFromDiscovery) ||
163
226
  pyTruthy(dictOf(operation.schemaAction).parents) ||
164
227
  pyTruthy(runtime.requiresConnectionForSchema);
165
- const out = ["## Example", ""];
228
+ const out = ["## Example", "", "### Flow", ""];
166
229
  const pairs = required.map((f) => {
167
230
  const [name, , wireName] = authoringField(f);
168
231
  return [wireName ? f.name : name, exampleValue(f, symbol, invariant, connectorKey, action)];
@@ -224,6 +287,7 @@ export function exampleLines(j, fields, symbol, invariant, v1def) {
224
287
  }
225
288
  out.push("");
226
289
  }
290
+ out.push(...bpmnExampleLines(required, invariant, connectorKey, action, step, runtime, operation));
227
291
  return out;
228
292
  }
229
293
  /**
@@ -243,7 +307,7 @@ export function eventExampleLines(j, symbol, connectorKey, action, step, v1def)
243
307
  .filter((f) => typeof f.name === "string" && !pyTruthy(f.hidden))
244
308
  .map((f) => f.name);
245
309
  const factory = start ? "onEvent" : "waitForEvent";
246
- const out = ["## Example", "", "```ts"];
310
+ const out = ["## Example", "", "### Flow", "", "```ts"];
247
311
  out.push(`import { ${factory} } from '${PACKAGE_NAME}';`);
248
312
  if (symbol)
249
313
  out.push(`import { ${symbol} } from './connectors/${connectorKey}.ts';`);
@@ -291,5 +355,6 @@ export function eventExampleLines(j, symbol, connectorKey, action, step, v1def)
291
355
  "--connection-id <id>` lists the objects the bound connection exposes.");
292
356
  out.push("");
293
357
  }
358
+ out.push(...bpmnEventExampleLines(params, generic, start, connectorKey, action, step));
294
359
  return out;
295
360
  }
@@ -11,7 +11,7 @@ import { sortedStrings } from "../../../compat/compare.js";
11
11
  import { pyStr, splitOnce } from "../../../compat/pystr.js";
12
12
  import { classify, referenceOf } from "../../../lookup-specs.js";
13
13
  import { byAliases } from "../../../naming.js";
14
- import { resolveFlagArg } from "../../../../core/cli-spelling.js";
14
+ import { prepareCommand, resolveFlagArg } from "../../../../core/cli-spelling.js";
15
15
  import { authoringField, referenceCollection, referenceQuery } from "../common.js";
16
16
  import { firstValue, IDENT, pyOr, pyTruthy, strList } from "../pyvalue.js";
17
17
  /** The `resources run list` call that shows what an identity-lookup field accepts. */
@@ -94,15 +94,36 @@ export function lookupLines(connectorKey, action, fields, symbol, invariant) {
94
94
  // array too: the flat key compiles to the same list the nested
95
95
  // `recipients: { listIds: [...] }` of `## Example` does.
96
96
  const [authoringName, , multiselectWire] = authoringField(firstField);
97
- if (multiselectWire) {
98
- out.push(`${authoringName}: [${call}${sample}]`);
99
- }
100
- else {
97
+ const line = (c, sa) => {
98
+ if (multiselectWire)
99
+ return `${authoringName}: [${c}${sa}]`;
101
100
  const key = IDENT.test(firstName) ? firstName : pyJsonString(firstName);
102
- out.push(firstName.endsWith("[*]") ? `${key}: [${call}${sample}]` : `${key}: ${call}${sample}`);
103
- }
101
+ return firstName.endsWith("[*]") ? `${key}: [${c}${sa}]` : `${key}: ${c}${sa}`;
102
+ };
103
+ out.push(line(call, sample));
104
104
  out.push("```");
105
105
  out.push("");
106
+ // A SECOND fence, not a second line in the first: that one is an object
107
+ // literal and `## Example`'s gate compiles it, so two keys there is a
108
+ // syntax error before it is anything else.
109
+ //
110
+ // The descriptor form above is Flow's — `lookup(Descriptor, …)` needs a
111
+ // generated `connectors/<key>.ts` module, and the aliases (`.byName`) exist
112
+ // only on it. BPMN's `.connector()` takes the key and action as strings and
113
+ // imports no descriptor, so a page that printed only the descriptor form
114
+ // handed a BPMN author a call that does not resolve, with the generic
115
+ // `.by()` it needs mentioned two paragraphs further down, after the table.
116
+ if (symbol) {
117
+ const stringCall = `lookup('${connectorKey}', '${action}', '${firstName}')`;
118
+ const generic = firstNames.length ? `.by('${firstNames[0]}', '…')` : ".by('id', '…')";
119
+ out.push("In BPMN there is no descriptor to import, so the key and the action " +
120
+ "are strings and only the generic `.by()` exists:");
121
+ out.push("");
122
+ out.push("```ts");
123
+ out.push(line(stringCall, generic));
124
+ out.push("```");
125
+ out.push("");
126
+ }
106
127
  out.push("| field | helpers | matches on | sends |");
107
128
  out.push("| --- | --- | --- | --- |");
108
129
  for (const [f, reference] of resolvable) {
@@ -128,7 +149,7 @@ export function lookupLines(connectorKey, action, fields, symbol, invariant) {
128
149
  out.push("");
129
150
  // ONE command carrying every --resolve, and no `--connection-id`.
130
151
  out.push("```bash");
131
- out.push(`npx flow-sdk registry prepare ${connectorKey} --action ${action} \\`);
152
+ out.push(`${prepareCommand(connectorKey, action)} \\`);
132
153
  // The WIRE name (`users[*]`), which is what prepare's lookup table is keyed
133
154
  // on — the authoring name (`users`) is refused as RESOLVE_UNKNOWN_FIELD.
134
155
  const resolves = resolvable.map(([f, reference]) => {
@@ -220,7 +220,12 @@ export function unresolvedOperationMessage(input) {
220
220
  else {
221
221
  lines.push("", `\`registry search ${key}\` lists no action for ${key} either.`);
222
222
  }
223
- lines.push("", "`registry get` and `registry search` answer from the CLI's local copy of the catalog.", "If it looks stale, refresh it first: `uip maestro flow registry pull --force`.");
223
+ lines.push("", "`registry get` and `registry search` answer from the CLI's local copy of the catalog.", "If it looks stale, refresh it first: `uip maestro flow registry pull --force`.",
224
+ // Named `flow` on purpose, whatever family you are authoring: this is the
225
+ // catalog `prepare` itself reads, and `uip maestro bpmn registry pull` fills
226
+ // a DIFFERENT one (the BPMN extension types). A BPMN author who "corrects"
227
+ // the command here refreshes the wrong cache and sees no change.
228
+ "That is the catalog this command reads, in every family — `uip maestro bpmn registry`", "is the BPMN extension-type catalog, a different cache, and pulling it changes nothing here.");
224
229
  return lines.join("\n");
225
230
  }
226
231
  /**
@@ -4,7 +4,7 @@
4
4
  */
5
5
  export declare const IMPORT_PATTERN: RegExp;
6
6
  /**
7
- * Where a `.flow.ts` will not be: generated output and installed packages.
7
+ * Where an authored source will not be: generated output and installed packages.
8
8
  * Matched by name at any depth, so `.flow-sdk/connectors/` is skipped too while
9
9
  * a source kept in `.flow-sdk/` itself is found.
10
10
  */
@@ -22,10 +22,10 @@ export declare function isRelativeSpecifier(specifier: string): boolean;
22
22
  * A bare package specifier never does — it is not our overlay.
23
23
  */
24
24
  export declare function resolvesTo(specifier: string, source: string, target: string): boolean;
25
- /** Every `*.flow.ts` file under `root`, sorted, skipping the directories a source is never in. */
25
+ /** Every authored source file under `root`, sorted, skipping the directories a source is never in. */
26
26
  export declare function flowSources(root: string): string[];
27
27
  /**
28
- * `_sources_importing`: every `*.flow.ts` under `root` whose import resolves
28
+ * `_sources_importing`: every authored source under `root` whose import resolves
29
29
  * to `baked`. Selection is by CONTENT rather than "the single flow in this
30
30
  * directory": a workspace with three flows, one of which uses this connector,
31
31
  * has one correct answer and this finds it.
@@ -31,7 +31,7 @@ import { repointedLine, severalSourcesLine, sourceNotAFileLine } from "./message
31
31
  */
32
32
  export const IMPORT_PATTERN = /(^\s*import\s[^;'"]*?\sfrom\s*)(['"])([^'"]+)(\2)/gm;
33
33
  /**
34
- * Where a `.flow.ts` will not be: generated output and installed packages.
34
+ * Where an authored source will not be: generated output and installed packages.
35
35
  * Matched by name at any depth, so `.flow-sdk/connectors/` is skipped too while
36
36
  * a source kept in `.flow-sdk/` itself is found.
37
37
  */
@@ -75,7 +75,18 @@ function comparePaths(a, b) {
75
75
  }
76
76
  return a.length - b.length;
77
77
  }
78
- /** Every `*.flow.ts` file under `root`, sorted, skipping the directories a source is never in. */
78
+ /**
79
+ * The authored-source suffixes `prepare` reads.
80
+ *
81
+ * `.bpmn.ts` is here because a BPMN `.connector()` binds a connection and a
82
+ * folder exactly as a Flow step does, and `prepare` reuses the author's existing
83
+ * label names rather than inventing new ones (`source-labels.ts`). It will never
84
+ * match in `sourcesImporting` — BPMN takes its connector as strings and imports
85
+ * no descriptor — which costs one `readFileSync` per BPMN source and keeps the
86
+ * two callers on one walk.
87
+ */
88
+ const SOURCE_SUFFIXES = ['.flow.ts', '.bpmn.ts'];
89
+ /** Every authored source file under `root`, sorted, skipping the directories a source is never in. */
79
90
  export function flowSources(root) {
80
91
  const found = [];
81
92
  const walk = (dir, parts) => {
@@ -91,7 +102,7 @@ export function flowSources(root) {
91
102
  if (!SOURCE_SKIP_DIRS.has(child.name))
92
103
  walk(join(dir, child.name), [...parts, child.name]);
93
104
  }
94
- else if (child.isFile() && child.name.endsWith(".flow.ts")) {
105
+ else if (child.isFile() && SOURCE_SUFFIXES.some((suffix) => child.name.endsWith(suffix))) {
95
106
  found.push([...parts, child.name]);
96
107
  }
97
108
  }
@@ -101,7 +112,7 @@ export function flowSources(root) {
101
112
  return found.map((parts) => join(root, ...parts));
102
113
  }
103
114
  /**
104
- * `_sources_importing`: every `*.flow.ts` under `root` whose import resolves
115
+ * `_sources_importing`: every authored source under `root` whose import resolves
105
116
  * to `baked`. Selection is by CONTENT rather than "the single flow in this
106
117
  * directory": a workspace with three flows, one of which uses this connector,
107
118
  * has one correct answer and this finds it.