@uipath/maestro-builder-sdk 6.15.4 → 6.15.6

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.
@@ -43,9 +43,18 @@ export function dataServiceWire(entity, operation) {
43
43
  ...optionalRow('queryExpression', 'query', operation.where),
44
44
  scalarRow('start', 'query', operation.start ?? 0),
45
45
  scalarRow('limit', 'query', operation.limit ?? 100),
46
- ...optionalRow('sortBy', 'query', operation.sortBy),
47
46
  ...optionalRow('isAscending', 'query', operation.ascending),
48
47
  ...optionalRow('expansionLevel', 'query', operation.expansionLevel),
48
+ // The sort FIELD rides in the body as `_sortFieldName` — the name the curated
49
+ // operation's `RequestFields` declare (`uip is resources describe
50
+ // uipath-uipath-dataservice QueryEntityRecordsCurated --operation Create`), and
51
+ // the only place the connector reads it: as a query parameter it is ignored and
52
+ // the query comes back in default order. This used to write `sortBy` as a query
53
+ // row; on flow-v2-run-12 the Query node so built validated and failed
54
+ // `no Query node sorted by priority DESC` (datafabric-e2e-contract-intake-pipeline
55
+ // 1.00 → 0.45) while the v1 arm wrote `{"_sortFieldName":"priority"}` by hand.
56
+ // `isAscending` stays a query parameter, which is where the connector reads it.
57
+ ...(operation.sortBy !== undefined ? [bodyRow({ _sortFieldName: operation.sortBy })] : []),
49
58
  ],
50
59
  };
51
60
  case 'delete':
@@ -56,6 +56,7 @@ import BpmnModdle from 'bpmn-moddle';
56
56
  import { createRequire } from 'node:module';
57
57
  import { registryType, rowDeclaration } from './typed-node.js';
58
58
  import { errorSchema, errorSchemaWithResponse } from './bpmn-sdk.js';
59
+ import { SCRIPT_ARGS_ENVELOPE, defaultScriptInputSchema } from './serialize.js';
59
60
  import { BPMN_SCHEMA_VERSIONS } from './format-profile.js';
60
61
  import { preserveSchemaVersion } from '../schema-version.js';
61
62
  const require = createRequire(import.meta.url);
@@ -654,7 +655,17 @@ function scriptVersionOf(el) {
654
655
  /** A `uipath:mapping` script task. */
655
656
  function emitScriptTask(chain, el, payload, type) {
656
657
  const args = (payload?.input ?? []).find((r) => r.name === 'args');
657
- const inputs = args ? safeJson(args.body) : undefined;
658
+ // The serializer writes the product's envelope — `vars`/`metadata`, and the default
659
+ // inputSchema declaring them — on every script task, so the source need not repeat
660
+ // them: an `args` that is exactly the envelope decompiles to no `inputs` at all, and
661
+ // only what the designer added (`"iterator[0]":"=iterator[0]"`) comes through. A
662
+ // schema that differs from the default (the designer's carries `$schema`) is kept.
663
+ const rawInputs = args ? safeJson(args.body) : undefined;
664
+ const inputs = rawInputs
665
+ ? Object.fromEntries(Object.entries(rawInputs).filter(([k, v]) => !(k in SCRIPT_ARGS_ENVELOPE) || v !== SCRIPT_ARGS_ENVELOPE[k]))
666
+ : undefined;
667
+ const schemaBody = payload?.context?.inputSchema?.body;
668
+ const schemaIsDefault = schemaBody !== undefined && rawInputs !== undefined && JSON.stringify(safeJson(schemaBody)) === JSON.stringify(defaultScriptInputSchema(rawInputs));
658
669
  const outputs = {};
659
670
  for (const row of payload?.output ?? [])
660
671
  outputs[row.var ?? row.name] = row.source;
@@ -680,7 +691,7 @@ function emitScriptTask(chain, el, payload, type) {
680
691
  'type',
681
692
  type !== undefined && type !== 'BPMN.Variables' && type !== 'BPMN.ScriptTask' ? str(type) : undefined,
682
693
  ],
683
- ['inputSchema', payload?.context?.inputSchema?.body !== undefined ? jsonLiteral(payload.context.inputSchema.body) : undefined],
694
+ ['inputSchema', schemaBody !== undefined && !schemaIsDefault ? jsonLiteral(schemaBody) : undefined],
684
695
  ['inputs', record(inputs)],
685
696
  ['outputs', verbatim ? undefined : record(outputs)],
686
697
  ['outputRows', verbatim],
@@ -66,6 +66,14 @@ export declare function intsvcOutputVar(n: EventNode | ExternalNode): string | u
66
66
  * @returns The moddle element.
67
67
  */
68
68
  export declare function createConnectorEvent(ctx: ConnectorCtx, n: EventNode): El;
69
+ /**
70
+ * The `body` of a connector event node: the subscription's filter in the one place the
71
+ * platform reads it. `scope` (the `filter` option — the operation's event parameters, by
72
+ * equality) becomes both the `queryParams` and the leading `(k == 'v')` clauses;
73
+ * `filters` (payload conditions) follow, each in parentheses, all AND-joined. No scope and
74
+ * no filters is `{}`, which is what the designer writes for an unfiltered subscription.
75
+ */
76
+ export declare function eventFilterBody(nodeId: string, scope: Record<string, unknown> | undefined, filters: readonly unknown[] | undefined): Record<string, unknown>;
69
77
  /**
70
78
  * An external agent / workflow invocation.
71
79
  *
@@ -1,5 +1,6 @@
1
1
  import { createTypedNode, typedNodeOutputVar } from './typed-node.js';
2
2
  import { connectorBindingRef } from './connector.js';
3
+ import { eventFilterJmes, eventFilterProblem, readEventFilter } from '../core/event-filters.js';
3
4
  /**
4
5
  * Which extension type an external invocation lowers to.
5
6
  *
@@ -96,8 +97,18 @@ export function createConnectorEvent(ctx, n) {
96
97
  // and watches nothing. A curated entry carries its own and the author passes none.
97
98
  objectName: n.object ?? op.objectName ?? '',
98
99
  };
99
- if (n.filter !== undefined)
100
- inputs.filter = n.filter;
100
+ // The subscription's filter goes in the `body` row, where the platform reads it:
101
+ // `PO.BpmnEngine` `BpmnInputExtensions.GetEventFilters` takes `body.filters.expression`
102
+ // (and `queryParams` registers the scope with the connector). This emitter used to write
103
+ // `<uipath:input name="filter" target="filter">{…}</uipath:input>`, an input no handler
104
+ // reads: on flow-v2-run-12 the wait so scoped validated and matched every mail in the
105
+ // mailbox (outlook-waitfor-email 1.00 → 0.43), while the v1 arm and every Studio Web
106
+ // export write `{"filters":{"expression":"(parentFolderId == '…') && (contains(subject, '…'))"},"queryParams":{"parentFolderId":"…"}}`.
107
+ //
108
+ // The expression is the scope's equality clauses AND-joined with the authored
109
+ // `filters`, each lowered by the designer's own grammar (`core/event-filters.ts`) — the
110
+ // same lowering the Flow serializer does for `onEvent`/`waitForEvent`.
111
+ inputs.body = eventFilterBody(n.id, n.filter, n.filters);
101
112
  if (n.parameters !== undefined)
102
113
  inputs.parameters = n.parameters;
103
114
  return createTypedNode(ctx.m, n.id, type, {
@@ -108,6 +119,36 @@ export function createConnectorEvent(ctx, n) {
108
119
  skipCondition: n.skipCondition,
109
120
  }, (varId) => ctx.varTypeOf?.(varId));
110
121
  }
122
+ /**
123
+ * The `body` of a connector event node: the subscription's filter in the one place the
124
+ * platform reads it. `scope` (the `filter` option — the operation's event parameters, by
125
+ * equality) becomes both the `queryParams` and the leading `(k == 'v')` clauses;
126
+ * `filters` (payload conditions) follow, each in parentheses, all AND-joined. No scope and
127
+ * no filters is `{}`, which is what the designer writes for an unfiltered subscription.
128
+ */
129
+ export function eventFilterBody(nodeId, scope, filters) {
130
+ const scopeEntries = Object.entries(scope ?? {}).filter(([, v]) => v !== undefined && v !== null && String(v) !== '');
131
+ const scopeClauses = scopeEntries.map(([k, v]) => `(${k} == '${String(v).replace(/'/g, "\\'")}')`);
132
+ const conditions = (filters ?? []).map((leaf, i) => {
133
+ const { read } = readEventFilter(leaf);
134
+ if (!read) {
135
+ throw new Error(`connector event "${nodeId}": filters[${i}] must carry a field and exactly ONE operator ` +
136
+ `(contains, startsWith, endsWith, equals, notEquals, lessThan, lessThanOrEqual, greaterThan, ` +
137
+ `greaterThanOrEqual) — got ${JSON.stringify(leaf)}.`);
138
+ }
139
+ const problem = eventFilterProblem(read);
140
+ if (problem)
141
+ throw new Error(`connector event "${nodeId}": filters[${i}] on \`${read.field}\`: ${problem}`);
142
+ return `(${eventFilterJmes(read)})`;
143
+ });
144
+ const expression = [...scopeClauses, ...conditions].join(' && ');
145
+ const body = {};
146
+ if (expression !== '')
147
+ body.filters = { expression };
148
+ if (scopeEntries.length > 0)
149
+ body.queryParams = Object.fromEntries(scopeEntries);
150
+ return body;
151
+ }
111
152
  /**
112
153
  * An external agent / workflow invocation.
113
154
  *
@@ -26,6 +26,20 @@ export declare function serialize(built: BuiltBpmn, opts?: BpmnSerializeOpts): P
26
26
  */
27
27
  export declare function finalizeBpmnXml(xml: string): string;
28
28
  export declare function processId(id: string): string;
29
+ /**
30
+ * The `args` keys every Studio Web script task carries, in the order the designer writes
31
+ * them. The platform runtime injects these — and only these — into the script's scope.
32
+ */
33
+ export declare const SCRIPT_ARGS_ENVELOPE: {
34
+ readonly vars: "=vars";
35
+ readonly metadata: "=metadata";
36
+ };
37
+ /**
38
+ * The `uipath:inputSchema` the product writes for a script task's `args`: `vars` and
39
+ * `metadata` as objects, plus an untyped property per extra key. Exported so the
40
+ * decompiler can recognise it and leave it out of the source.
41
+ */
42
+ export declare function defaultScriptInputSchema(args: Record<string, unknown>): Record<string, unknown>;
29
43
  /**
30
44
  * The variable declarations a scope's output-bearing nodes imply.
31
45
  *
@@ -444,21 +444,50 @@ function buildErrorMappingExt(ctx, rows) {
444
444
  }),
445
445
  });
446
446
  }
447
+ /**
448
+ * The `args` keys every Studio Web script task carries, in the order the designer writes
449
+ * them. The platform runtime injects these — and only these — into the script's scope.
450
+ */
451
+ export const SCRIPT_ARGS_ENVELOPE = { vars: '=vars', metadata: '=metadata' };
452
+ /**
453
+ * The `uipath:inputSchema` the product writes for a script task's `args`: `vars` and
454
+ * `metadata` as objects, plus an untyped property per extra key. Exported so the
455
+ * decompiler can recognise it and leave it out of the source.
456
+ */
457
+ export function defaultScriptInputSchema(args) {
458
+ const properties = {};
459
+ for (const k of Object.keys(args))
460
+ properties[k] = k in SCRIPT_ARGS_ENVELOPE ? { type: 'object' } : {};
461
+ return { type: 'object', properties };
462
+ }
447
463
  function createScriptTask(ctx, n) {
448
464
  const { m } = ctx;
465
+ // The `args` row is the product's fixed envelope — `vars` and `metadata`, always, in
466
+ // that order — with the author's `inputs` after it. The platform runtime injects the
467
+ // ENVELOPE into the script's scope and nothing else: in flow-v2-run-12 every script
468
+ // that read an `inputs` key as a bare identifier faulted on the cloud with
469
+ // `<key> is not defined` (3 of 3 cloud-executed script tasks; `check`, `compile` and
470
+ // `uip maestro bpmn validate` had all passed), while every Studio Web export in
471
+ // `research/bpmn-roundtrip` and every v1-authored process in the same run carries
472
+ // exactly this envelope and reads `vars.<id>`. The author's keys are kept so an import
473
+ // round-trips (`iterator[0]` is how the designer scopes a loop body) and so a key the
474
+ // author knows the runtime honours can still be passed; a bare read of one is what
475
+ // `check` reports as SCRIPT_BARE_INPUT_REF. See docs/BPMN_EVAL_RUN12_FINDINGS.md §4 P0-1.
476
+ const args = { ...SCRIPT_ARGS_ENVELOPE, ...n.inputs };
477
+ const inputSchema = n.inputSchema ?? defaultScriptInputSchema(args);
449
478
  const mapping = m.create('uipath:Mapping', {
450
479
  version: 'v1',
451
480
  type: m.create('uipath:Type', { value: n.type, version: 'v1' }),
452
- // The context holds only the schema here — a script task has no context ROWS.
453
- context: n.inputSchema === undefined
454
- ? undefined
455
- : m.create('uipath:Context', {
456
- inputSchema: m.create('uipath:InputSchema', {
457
- type: 'jsonSchema',
458
- body: typeof n.inputSchema === 'string' ? n.inputSchema : JSON.stringify(n.inputSchema),
459
- }),
481
+ // The context holds only the schema here — a script task has no context ROWS. Real
482
+ // script tasks all carry the schema; the product refuses to run without the envelope
483
+ // it declares, so an absent one is filled in rather than left out.
484
+ context: m.create('uipath:Context', {
485
+ inputSchema: m.create('uipath:InputSchema', {
486
+ type: 'jsonSchema',
487
+ body: typeof inputSchema === 'string' ? inputSchema : JSON.stringify(inputSchema),
460
488
  }),
461
- input: [m.create('uipath:Input', { name: 'args', type: 'json', target: 'bodyField', body: JSON.stringify(n.inputs) })],
489
+ }),
490
+ input: [m.create('uipath:Input', { name: 'args', type: 'json', target: 'bodyField', body: JSON.stringify(args) })],
462
491
  // Spelled-out rows win over derived ones: `outputs` names each row after the
463
492
  // variable it writes, and the product names it after what the script returns
464
493
  // while binding a differently-named variable. See ScriptTaskOpts.outputRows.
@@ -1,5 +1,6 @@
1
1
  import { createRequire } from 'node:module';
2
2
  import { resolveFromRegistry } from './registry-fallback.js';
3
+ import { nestConnectorBody } from '../core/connector-inputs.js';
3
4
  import { types } from '../core/expr.js';
4
5
  const require = createRequire(import.meta.url);
5
6
  const REGISTRY = require('./registry-types.json');
@@ -64,7 +65,20 @@ export function isSnapshotType(type) {
64
65
  * constant for the one type known to need it rather than derived.
65
66
  */
66
67
  export const REGISTRY_GAPS = {
67
- 'Orchestrator.StartAgentJob': { resourceSubType: 'Agent' },
68
+ // `folderKey` — the job-start family's v1 addressing field. The registry declares
69
+ // `folderId`, but the engine's v1 branch reads `FolderKey` (`ExecuteApiWorkflowArgs`:
70
+ // `input.GetValueFromContext<string>(nameof(OrchestratorFolderContext.FolderKey))`), and
71
+ // the v1 skill verified live that `releaseKey` without it faults with `Could not get
72
+ // value for key:FolderKey from context in input`. Emitted only for a node addressed by
73
+ // `releaseKey` — see `OrchestratorOpts.folderKey`.
74
+ 'Orchestrator.StartAgentJob': { resourceSubType: 'Agent', extraContext: ['folderKey'] },
75
+ 'Orchestrator.StartJob': { extraContext: ['folderKey'] },
76
+ 'Orchestrator.ExecuteApiWorkflowAsync': { extraContext: ['folderKey'] },
77
+ 'Orchestrator.StartAgenticProcess': { extraContext: ['folderKey'] },
78
+ 'Orchestrator.StartAgenticProcessAsync': { extraContext: ['folderKey'] },
79
+ 'Orchestrator.StartCaseMgmtProcess': { extraContext: ['folderKey'] },
80
+ 'Orchestrator.StartCaseMgmtProcessAsync': { extraContext: ['folderKey'] },
81
+ 'Orchestrator.BusinessRules': { extraContext: ['folderKey'] },
68
82
  // `notEnforcedInputs` — required InputFields the platform does NOT actually enforce.
69
83
  //
70
84
  // MEASURED, on a real trigger that carries `connection`, `folderKey`, `connectorKey`,
@@ -226,9 +240,19 @@ function contextRows(m, type, spec, supplied, verbatim = false) {
226
240
  return rows;
227
241
  }
228
242
  for (const field of spec.context) {
229
- const value = supplied[field.name] ?? field.default;
243
+ let value = supplied[field.name] ?? field.default;
230
244
  if (value === undefined || value === null)
231
245
  continue;
246
+ // A json field whose REGISTRY default is the text `null` is unrunnable as declared:
247
+ // the platform parses the row to a JSON null and `IntSvcUnifiedHttpRequestArgs`
248
+ // throws `Body cannot be null` on it (incident 102018 — flow-v2-run-12
249
+ // slack-http-fallback, a GET with no body, faulted on exactly this after check,
250
+ // compile and validate had passed). Omitting the row is the same null. So a
251
+ // default of `null` on a json field is written as `{}`, the empty object the handler
252
+ // accepts; an author who wrote `null` themselves still gets it verbatim, and an
253
+ // import's `"null"` text round-trips untouched (see decompile's json-row rules).
254
+ if (field.type === 'json' && !(field.name in supplied) && field.default === 'null')
255
+ value = {};
232
256
  emitted.add(field.name);
233
257
  // A `json` field goes in the CDATA BODY, not a `value` attribute. The product
234
258
  // validator parses that field as JSON and reads the attribute WITHOUT XML
@@ -266,6 +290,28 @@ function contextRows(m, type, spec, supplied, verbatim = false) {
266
290
  }
267
291
  return rows;
268
292
  }
293
+ /**
294
+ * The `target` values a payload row can carry — `BpmnInputTarget` in PO.BpmnEngine
295
+ * (`InputResolutionActivities` routes `Body`/`BodyField`/`Header`/`PathVariable`/`Query`/
296
+ * `PathParameters`/`QueryParameters`/`File`), plus the connector emitter's `multipart`.
297
+ * An undeclared `separateInputs` key that is one of these addresses a slot; any other
298
+ * key is a field — see `inputRows`.
299
+ */
300
+ const PAYLOAD_TARGETS = new Set([
301
+ 'body',
302
+ 'bodyField',
303
+ 'header',
304
+ 'headers',
305
+ 'pathVariable',
306
+ 'pathParameters',
307
+ 'query',
308
+ 'queryParameters',
309
+ 'file',
310
+ 'multipart',
311
+ ]);
312
+ function isPlainRecord(value) {
313
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
314
+ }
269
315
  /**
270
316
  * The payload rows, per the type's `inputPattern`.
271
317
  *
@@ -308,11 +354,37 @@ function inputRows(m, spec, inputs) {
308
354
  // `recordId` as `"1111-…"`, quotes included, where every real export writes
309
355
  // `type="string" value="1111-…"`. A structured value is still a json body; a
310
356
  // scalar is written as the `value` attribute, the way a declared scalar is.
357
+ //
358
+ // An undeclared key is a TARGET, then — `body`, `pathParameters`, `queryParameters`
359
+ // — and only a target. A key that is none of the platform's targets is a request
360
+ // FIELD the author wrote where a slot belongs (`inputs: { script: '…' }` on
361
+ // `Intsvc.ActivityExecution`), and keyed as its own target it reaches a row the
362
+ // platform never reads: on flow-v2-run-12 the JDBC `execute-query-synchronously` node
363
+ // so written went out as `<uipath:input name="script" … target="script"/>` and every
364
+ // one of seven cloud runs faulted `Integration Services bad request`, while the v1 arm's
365
+ // `<uipath:input name="body" target="body">{"script":"…"}</uipath:input>` completed
366
+ // first time. Such keys fold into the type's own payload row (`inputName`/`inputTarget`,
367
+ // `body` for the Integration Service family), nested by their dotted ids, exactly as
368
+ // `.connector()` writes a request. A type with no payload row keeps keying them.
311
369
  const declared = new Map(spec.inputs.map((f) => [f.name, f]));
312
- return keys.map((k) => {
370
+ const fieldKeys = spec.inputName !== null && spec.inputTarget !== null
371
+ ? keys.filter((k) => !declared.has(k) && !PAYLOAD_TARGETS.has(k) && k !== spec.inputName)
372
+ : [];
373
+ const folded = new Set(fieldKeys);
374
+ const rows = keys
375
+ .filter((k) => !folded.has(k))
376
+ .map((k) => {
313
377
  const field = declared.get(k);
314
- return payloadInput(m, k, field?.target ?? k, inputs[k], field?.type ?? inferredInputType(inputs[k]));
378
+ let value = inputs[k];
379
+ if (k === spec.inputName && fieldKeys.length > 0 && isPlainRecord(value)) {
380
+ value = { ...nestConnectorBody(Object.fromEntries(fieldKeys.map((f) => [f, inputs[f]]))), ...value };
381
+ }
382
+ return payloadInput(m, k, field?.target ?? k, value, field?.type ?? inferredInputType(value));
315
383
  });
384
+ if (fieldKeys.length > 0 && !keys.includes(spec.inputName)) {
385
+ rows.push(payloadInput(m, spec.inputName, spec.inputTarget, nestConnectorBody(Object.fromEntries(fieldKeys.map((f) => [f, inputs[f]]))), 'json'));
386
+ }
387
+ return rows;
316
388
  }
317
389
  if (!spec.inputName)
318
390
  return [];
package/dist/check.js CHANGED
@@ -12,11 +12,11 @@ import { checkEventResolution, checkEventSubscription, eventParamsFor, } from '.
12
12
  import { FLOW_EXPR_DIALECT, checkBindingLabels, checkBindingPresence, checkConnectorContract as coreCheckConnectorContract, checkConnectorLookups as coreCheckConnectorLookups, checkConnectorSchema as coreCheckConnectorSchema, collectConnectionUses, connectionConnectorConflicts, } from './core/connector-checks.js';
13
13
  import { prepareCommand } from './core/cli-spelling.js';
14
14
  import { isAnswerField } from './core/hitl-answers.js';
15
- import { globalNameCollisionMessage, globalNameCollisions } from './core/global-names.js';
16
15
  import { hitlDerivesOutcomeHandles, hitlExitModel, hitlRoutesPerOutcome } from './core/hitl-routing.js';
17
16
  import { childStepLists, declaredExits, hasExits, stopsAtEnd } from './core/step-ports.js';
18
17
  import { FLOW_SCHEMA_VERSIONS } from './format-profile.js';
19
18
  import { compareSchemaVersions, schemaVersionRefusal } from './schema-version.js';
19
+ import { planFlowGlobals } from './flow-variables.js';
20
20
  export function check(built, opts = {}) {
21
21
  let diags = [];
22
22
  const uses = collectConnectionUses(() => { diags = checkFlow(built, [], opts); });
@@ -36,12 +36,8 @@ function checkFlow(built, ancestors, opts = {}) {
36
36
  if (!built.id)
37
37
  diags.push({ level: 'error', code: 'NO_ID', message: 'flow() requires an id.' });
38
38
  checkSchemaVersion(built, ancestors, diags);
39
+ checkGlobalNames(built, diags);
39
40
  checkNodeUpdates(built, diags);
40
- // Inputs, outputs and vars are one namespace in the .flow (variables.globals[]);
41
- // `serialize` throws the same sentence (flow-builder-sdk#890).
42
- for (const collision of globalNameCollisions(built.inputs ?? [], built.outputs ?? [], built.vars ?? [])) {
43
- diags.push({ level: 'error', code: 'VARIABLE_NAME_COLLISION', message: globalNameCollisionMessage(collision) });
44
- }
45
41
  const maybeCustomTrigger = built.trigger;
46
42
  if (maybeCustomTrigger !== undefined && !('kind' in maybeCustomTrigger) && maybeCustomTrigger instanceof FlowNode) {
47
43
  // A custom FlowTrigger subclass reports through its own diagnostics() hook;
@@ -945,7 +941,12 @@ function checkFlow(built, ancestors, opts = {}) {
945
941
  }
946
942
  }
947
943
  // Check $vars.<name>[.<field>] references resolve to a declared name or step.
944
+ const inputNames = new Set(built.inputs.map((x) => x.name));
948
945
  for (const { js, step, errorSource } of exprs) {
946
+ // Comments and string-literal text blanked out, offsets preserved: the
947
+ // input-read rule below fires on a VALID name, so it has to tell a read
948
+ // from a mention, the same way `uip maestro flow validate` does.
949
+ const code = codeOnly(js);
949
950
  // `rest` is the remaining dotted tail after `<name>.<field>` — needed for a
950
951
  // subflow read, where the value lives one level deeper
951
952
  // (`$vars.<call>.output.<childOutput>`). Every other rule reads only the
@@ -963,6 +964,36 @@ function checkFlow(built, ancestors, opts = {}) {
963
964
  });
964
965
  continue;
965
966
  }
967
+ // `$vars.<input>` — a flow INPUT read by its bare name.
968
+ //
969
+ // Inputs, outputs and vars share one namespace in `variables.globals[]`,
970
+ // but they are not read the same way. A var or output is read as
971
+ // `$vars.<name>`. An input is not: serialize gives EVERY input global a
972
+ // `triggerNodeId` (the root trigger, an entry point, or a subflow child's
973
+ // own Start), and the runtime surfaces such an input only under that
974
+ // trigger's output — `$vars.<trigger>.output.<name>`, which is exactly what
975
+ // `input('<name>')` lowers to. The bare read resolves only by accident (a
976
+ // manual trigger also emits a process-level input of the same name) and is
977
+ // null under any other trigger. `uip maestro flow validate` refuses it
978
+ // (UV-16316, UiPath/cli#4236); this rule used to treat every declared name
979
+ // as readable bare, so check and compile passed what validate refused
980
+ // (five shipped examples, found with flow-builder-sdk#904).
981
+ //
982
+ // A step with the same name wins, as it does in validate, and so does a
983
+ // mention in a comment or a string literal (template `${…}` is code).
984
+ if (inputNames.has(name) && !stepNames.has(name) && code.startsWith('$vars', m.index ?? 0)) {
985
+ diags.push({
986
+ level: 'error',
987
+ code: 'INPUT_READ_WITHOUT_TRIGGER',
988
+ message: `"$vars.${name}" in "${step}" reads flow input "${name}" by its bare name. A flow input is `
989
+ + `published under the trigger's output, so the runtime value is "$vars.${triggerId}.output.${name}" `
990
+ + `— the bare read is null under any trigger but a manual one, and \`uip maestro flow validate\` `
991
+ + `refuses it. In an expression, write input('${name}'); \`v()\` is for a .var() or an output.`,
992
+ step,
993
+ suggestion: `$vars.${triggerId}.output.${name}`,
994
+ });
995
+ continue;
996
+ }
966
997
  // A BARE `$vars.<step>` — the step itself, not its output.
967
998
  //
968
999
  // **The mechanism is now MEASURED, not inferred.** This comment used to say
@@ -1612,6 +1643,20 @@ const INTERVAL_WORDS = [
1612
1643
  * Without a library (`uip maestro flow check` run with no `--library`) the fields
1613
1644
  * are unknown, so the warning keeps the generic wording.
1614
1645
  */
1646
+ /**
1647
+ * Every declared name must become exactly one global, because `flow validate`
1648
+ * rejects a repeated id (`[variables.globals] Duplicate variable ID`;
1649
+ * VARIABLE_NAME_COLLISION, flow-builder-sdk#890). The planning — including the one collision that is NOT an error, a same-named
1650
+ * `.output()` + `.var()`, which merges into one `out` global carrying the var's
1651
+ * default — lives in `planFlowGlobals`, shared with the serializer, so `check`
1652
+ * and the emitted file cannot disagree about it (flow-builder-sdk#899: `check`
1653
+ * said `Errors: 0`, validate refused the file).
1654
+ */
1655
+ function checkGlobalNames(built, diags) {
1656
+ for (const p of planFlowGlobals({ inputs: built.inputs ?? [], outputs: built.outputs ?? [], vars: built.vars ?? [] }).problems) {
1657
+ diags.push({ level: 'error', code: p.code, message: p.message, ...(p.suggestion ? { suggestion: p.suggestion } : {}) });
1658
+ }
1659
+ }
1615
1660
  /**
1616
1661
  * `{ updates }` writes a declared flow variable when the node completes. The
1617
1662
  * assignable targets are `.var()` variables and flow outputs — assigning to an
@@ -1761,6 +1806,97 @@ function declaredRefName(js, triggerId) {
1761
1806
  const viaTrigger = new RegExp(`^\\$vars\\.${triggerId}\\.output\\.([A-Za-z0-9_]+)$`).exec(js.trim());
1762
1807
  return viaTrigger?.[1];
1763
1808
  }
1809
+ /**
1810
+ * The JavaScript text with `//` and `/* *\/` comments and the text of string and
1811
+ * template literals replaced by spaces — same length, so a match offset in the
1812
+ * original indexes the same character here. A template's `${…}` bodies are code
1813
+ * and are kept (nested templates and strings inside them are blanked in turn).
1814
+ *
1815
+ * This is the read-versus-mention line `uip maestro flow validate` draws in its
1816
+ * `$vars` extraction (`collectVarsRefs`, `inProse`): a rule that fires on a VALID
1817
+ * name must not fire on `"pass $vars.x here"` or a commented-out line. Regex
1818
+ * literals are not recognised; a `$vars` reference inside one is vanishingly rare.
1819
+ */
1820
+ function codeOnly(src) {
1821
+ const out = src.split('');
1822
+ const n = src.length;
1823
+ let i = 0;
1824
+ const blank = (from, to) => {
1825
+ for (let k = from; k < to && k < n; k++)
1826
+ if (out[k] !== '\n')
1827
+ out[k] = ' ';
1828
+ };
1829
+ /** Scan code from `i`; with `stopAtBrace`, return at the `}` closing a `${`. */
1830
+ const code = (stopAtBrace) => {
1831
+ let depth = 0;
1832
+ while (i < n) {
1833
+ const c = src[i];
1834
+ if (c === '/' && src[i + 1] === '/') {
1835
+ const end = src.indexOf('\n', i);
1836
+ const stop = end === -1 ? n : end;
1837
+ blank(i, stop);
1838
+ i = stop;
1839
+ continue;
1840
+ }
1841
+ if (c === '/' && src[i + 1] === '*') {
1842
+ const end = src.indexOf('*/', i + 2);
1843
+ const stop = end === -1 ? n : end + 2;
1844
+ blank(i, stop);
1845
+ i = stop;
1846
+ continue;
1847
+ }
1848
+ if (c === '"' || c === "'") {
1849
+ const start = i++;
1850
+ while (i < n && src[i] !== c && src[i] !== '\n')
1851
+ i += src[i] === '\\' ? 2 : 1;
1852
+ blank(start + 1, i);
1853
+ i++;
1854
+ continue;
1855
+ }
1856
+ if (c === '`') {
1857
+ template();
1858
+ continue;
1859
+ }
1860
+ if (stopAtBrace) {
1861
+ if (c === '{')
1862
+ depth++;
1863
+ else if (c === '}') {
1864
+ if (depth === 0)
1865
+ return;
1866
+ depth--;
1867
+ }
1868
+ }
1869
+ i++;
1870
+ }
1871
+ };
1872
+ const template = () => {
1873
+ let start = ++i;
1874
+ while (i < n) {
1875
+ const c = src[i];
1876
+ if (c === '\\') {
1877
+ i += 2;
1878
+ continue;
1879
+ }
1880
+ if (c === '`') {
1881
+ blank(start, i);
1882
+ i++;
1883
+ return;
1884
+ }
1885
+ if (c === '$' && src[i + 1] === '{') {
1886
+ blank(start, i);
1887
+ i += 2;
1888
+ code(true);
1889
+ i++; // the `}` closing this `${`
1890
+ start = i;
1891
+ continue;
1892
+ }
1893
+ i++;
1894
+ }
1895
+ blank(start, n);
1896
+ };
1897
+ code(false);
1898
+ return out.join('');
1899
+ }
1764
1900
  const GUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1765
1901
  /**
1766
1902
  * The four published-resource factories once each named the release key
@@ -10,6 +10,20 @@
10
10
  * @param fields - JSON-serializable values, e.g. `{ path, nodes, edges }`.
11
11
  */
12
12
  export declare function reportResult(fields: Record<string, unknown>): void;
13
+ /**
14
+ * Print text that IS the verb's result (a pretty-printed object, a captured
15
+ * path), as opposed to a status line such as `compile: wrote X`.
16
+ *
17
+ * Under `--json` this prints nothing: the result is already the JSON object on
18
+ * stdout, and the umbrella routes a tool's stdout to stderr meanwhile, so a
19
+ * plain `console.log` of the result would land on stderr as a second copy.
20
+ * A caller that merges the streams (`2>&1`, an agent's shell tool) then gets
21
+ * two documents and `json.load` fails with `Extra data` (#901). Status lines
22
+ * keep using `console.log`; they are what stderr is for under `--json`.
23
+ *
24
+ * @param text - the result as text; a trailing newline is added.
25
+ */
26
+ export declare function printResult(text: string): void;
13
27
  /**
14
28
  * Report a check's diagnostics as `{ errors, warnings, diagnostics }`, the one
15
29
  * shape every family's check (and compile's check pass) uses.
@@ -18,6 +18,24 @@ function slot() {
18
18
  export function reportResult(fields) {
19
19
  Object.assign(slot().fields, fields);
20
20
  }
21
+ /**
22
+ * Print text that IS the verb's result (a pretty-printed object, a captured
23
+ * path), as opposed to a status line such as `compile: wrote X`.
24
+ *
25
+ * Under `--json` this prints nothing: the result is already the JSON object on
26
+ * stdout, and the umbrella routes a tool's stdout to stderr meanwhile, so a
27
+ * plain `console.log` of the result would land on stderr as a second copy.
28
+ * A caller that merges the streams (`2>&1`, an agent's shell tool) then gets
29
+ * two documents and `json.load` fails with `Extra data` (#901). Status lines
30
+ * keep using `console.log`; they are what stderr is for under `--json`.
31
+ *
32
+ * @param text - the result as text; a trailing newline is added.
33
+ */
34
+ export function printResult(text) {
35
+ if (slot().json)
36
+ return;
37
+ process.stdout.write(`${text}\n`);
38
+ }
21
39
  /**
22
40
  * Report a check's diagnostics as `{ errors, warnings, diagnostics }`, the one
23
41
  * shape every family's check (and compile's check pass) uses.
@@ -78,3 +78,23 @@ interface ConnectorSlots {
78
78
  };
79
79
  };
80
80
  }
81
+ /**
82
+ * Fold a body keyed by the registry's dotted field ids into the nested request
83
+ * object those ids describe: `{"fields.project.key":"CE"}` → `{"fields":{"project":{"key":"CE"}}}`.
84
+ *
85
+ * `buildConnectorInputs` keeps the DOTTED keys on purpose — Flow's `bodyParameters` is a
86
+ * flat map and Flow's runtime (`convertDotNotationToNested`) nests it before the call.
87
+ * The Maestro BPMN runtime does not: `IntSvcSendTaskHandler` sends the `body` input as
88
+ * written, so a dotted key reaches the connector as a literal property name. Measured on
89
+ * flow-v2-run-12: the SDK's Jira `create-issue` body went out flat and Integration
90
+ * Service answered 400 `project: Specify a valid project ID or key`; the v1 arm, which
91
+ * writes the nested form by hand, created the ticket. A BPMN body therefore goes through
92
+ * this before it is serialised.
93
+ *
94
+ * A `[*]` segment names an array. On a leaf (`users[*]`) an array value is kept and a
95
+ * scalar becomes a one-element list, as the product's own conversion does; on an inner
96
+ * segment (`attachments[*].url`) each element of an array value becomes one object, and
97
+ * a scalar one. Later keys win over earlier ones at the same leaf; a nested object the
98
+ * author already wrote under a plain key is merged into, not replaced.
99
+ */
100
+ export declare function nestConnectorBody(flat: Record<string, unknown>): Record<string, unknown>;