@uipath/maestro-builder-sdk 6.66.6 → 6.66.8

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/bindings-v2.d.ts +15 -1
  2. package/dist/bindings-v2.js +29 -6
  3. package/dist/bpmn/check.js +16 -113
  4. package/dist/bpmn/serialize.d.ts +41 -0
  5. package/dist/bpmn/serialize.js +103 -1
  6. package/dist/bpmn/typed-node.d.ts +12 -0
  7. package/dist/bpmn/typed-node.js +24 -0
  8. package/dist/case/check.js +5 -40
  9. package/dist/case/serialize.js +6 -0
  10. package/dist/case/task-shape.d.ts +31 -0
  11. package/dist/case/task-shape.js +48 -0
  12. package/dist/check.js +7 -43
  13. package/dist/core/agent-sidecar.d.ts +13 -3
  14. package/dist/core/agent-sidecar.js +10 -0
  15. package/dist/core/compile-refusal.d.ts +25 -0
  16. package/dist/core/compile-refusal.js +19 -0
  17. package/dist/core/process-resource.d.ts +9 -0
  18. package/dist/core/process-resource.js +4 -0
  19. package/dist/core/refusals.d.ts +47 -44
  20. package/dist/core/refusals.js +47 -44
  21. package/dist/nodes/agent/flow/check.js +14 -42
  22. package/dist/nodes/agent/flow/inline-agent.js +62 -1
  23. package/dist/nodes/agent/flow/spec.d.ts +1 -1
  24. package/dist/nodes/connector/flow/node-type.js +5 -3
  25. package/dist/nodes/data-fabric/flow/check.js +2 -4
  26. package/dist/nodes/data-fabric/flow/shared.d.ts +9 -4
  27. package/dist/nodes/data-fabric/flow/shared.js +14 -4
  28. package/dist/nodes/document/flow/ixp.js +20 -9
  29. package/dist/nodes/hitl/flow/check.js +29 -110
  30. package/dist/nodes/hitl/flow/node-type.js +170 -3
  31. package/dist/nodes/process/flow/check.js +15 -80
  32. package/dist/nodes/process/flow/node-type.js +4 -0
  33. package/dist/nodes/process/flow/refusals.d.ts +8 -0
  34. package/dist/nodes/process/flow/refusals.js +92 -0
  35. package/dist/nodes/queue/flow/check.d.ts +25 -0
  36. package/dist/nodes/queue/flow/check.js +48 -18
  37. package/dist/nodes/queue/flow/node-type.js +13 -1
  38. package/dist/nodes/transform/flow/check.js +31 -43
  39. package/dist/nodes/transform/flow/node-type.js +35 -9
  40. package/dist/serialize.js +172 -20
  41. package/package.json +2 -2
@@ -1,3 +1,4 @@
1
+ import type { ConnectionUse } from './core/compile-refusal.js';
1
2
  /** One declared solution resource. */
2
3
  export interface BindingsV2Resource {
3
4
  resource: string;
@@ -26,9 +27,22 @@ export interface BindingsV2Document {
26
27
  * connector — are refused with `CONNECTION_CONNECTOR_CONFLICT`: at most one of
27
28
  * them can run, and picking the first silently mislabels the solution resource.
28
29
  *
30
+ * A collect-mode stand-in (`check` with no library) has no `detail`; `standIns`
31
+ * gives the connection it was bound to, by node id, so it is judged here like
32
+ * any node and `check` reports the conflict without a library.
33
+ *
34
+ * The bound-connection filter is not applied to a stand-in because a stand-in
35
+ * has no binding row to find. That does not make `check` stricter for the
36
+ * common case: with a library, a connector node writes its own ConnectionId row
37
+ * keyed by its connection id, a literal id with no bindings.json entry included
38
+ * (`connectionResourceBindings`), so the same id is in `bound` and compile
39
+ * refuses the same conflict. The one input where the two differ is a node whose
40
+ * definition declares no connection binding at all: compile skips it, the
41
+ * stand-in cannot know that without the definition.
42
+ *
29
43
  * @throws BindingRefusal when two nodes on one connection name different connectors.
30
44
  */
31
- export declare function connectionConnectors(flow: unknown): Map<string, string>;
45
+ export declare function connectionConnectors(flow: unknown, standIns?: ReadonlyMap<string, ConnectionUse>): Map<string, string>;
32
46
  /**
33
47
  * The `connection` resources a compiled flow declares — one per distinct
34
48
  * connection, in node order, attributed to the FIRST node that uses it.
@@ -118,9 +118,22 @@ function allNodes(flow) {
118
118
  * connector — are refused with `CONNECTION_CONNECTOR_CONFLICT`: at most one of
119
119
  * them can run, and picking the first silently mislabels the solution resource.
120
120
  *
121
+ * A collect-mode stand-in (`check` with no library) has no `detail`; `standIns`
122
+ * gives the connection it was bound to, by node id, so it is judged here like
123
+ * any node and `check` reports the conflict without a library.
124
+ *
125
+ * The bound-connection filter is not applied to a stand-in because a stand-in
126
+ * has no binding row to find. That does not make `check` stricter for the
127
+ * common case: with a library, a connector node writes its own ConnectionId row
128
+ * keyed by its connection id, a literal id with no bindings.json entry included
129
+ * (`connectionResourceBindings`), so the same id is in `bound` and compile
130
+ * refuses the same conflict. The one input where the two differ is a node whose
131
+ * definition declares no connection binding at all: compile skips it, the
132
+ * stand-in cannot know that without the definition.
133
+ *
121
134
  * @throws BindingRefusal when two nodes on one connection name different connectors.
122
135
  */
123
- export function connectionConnectors(flow) {
136
+ export function connectionConnectors(flow, standIns = new Map()) {
124
137
  const out = new Map();
125
138
  if (!isRecord(flow))
126
139
  return out;
@@ -133,11 +146,21 @@ export function connectionConnectors(flow) {
133
146
  const users = new Map();
134
147
  for (const node of allNodes(flow)) {
135
148
  const detail = nodeDetail(node);
136
- const key = detail && nodeConnectionKey(detail);
137
- if (!detail || !key || !bound.has(key))
138
- continue;
139
- const connector = nodeConnectionConnector(detail);
140
- if (!connector)
149
+ const standIn = typeof node.id === 'string' && isRecord(node.inputs) && node.inputs.standIn === true
150
+ ? standIns.get(node.id) : undefined;
151
+ let key;
152
+ let connector;
153
+ if (standIn) {
154
+ key = standIn.connection;
155
+ connector = standIn.connector;
156
+ }
157
+ else {
158
+ key = detail && nodeConnectionKey(detail);
159
+ if (!detail || !key || !bound.has(key))
160
+ continue;
161
+ connector = nodeConnectionConnector(detail);
162
+ }
163
+ if (!key || !connector)
141
164
  continue;
142
165
  const byConnector = users.get(key) ?? new Map();
143
166
  users.set(key, byConnector);
@@ -1,9 +1,8 @@
1
- import { nodeDeclaredVars } from './declared-vars.js';
2
- import { allNodes, folderCompanionKeys, implicitNodeVars, processId } from './serialize.js';
1
+ import { allNodes, documentIdProblems, folderCompanionKeys, implicitNodeVars, processId } from './serialize.js';
3
2
  import { KNOWN_METHOD_SET } from './known-methods.js';
4
3
  import { EQUALS_EXPR_DIALECT, checkConnectorContract, checkConnectorLookups, checkConnectorSchema, checkBindingLabels, } from '../core/connector-checks.js';
5
4
  import { checkEventResolution, checkEventSubscription, eventParamsFor, } from '../core/event-checks.js';
6
- import { bindingRequiredFields, CASE_RUNTIME_INTERNAL_TYPES, missingRequiredContext, missingRequiredInputs, REGISTRY_GAPS, registryType, } from './typed-node.js';
5
+ import { bindingRequiredFields, CASE_RUNTIME_INTERNAL_TYPES, CONNECTOR_ACTIVITY_TYPES, connectorInputsShadowed, missingRequiredContext, missingRequiredInputs, REGISTRY_GAPS, registryType, } from './typed-node.js';
7
6
  import { ownEntry } from '../core/own-entry.js';
8
7
  import { isManagedHttpV2, managedHttpAbsoluteUrl, managedHttpMissing } from './managed-http.js';
9
8
  /**
@@ -60,13 +59,13 @@ const GATEWAY_KINDS = new Set(['exclusiveGateway', 'parallelGateway', 'inclusive
60
59
  const ACTIVITY_KINDS = new Set(['task', 'scriptTask', 'subProcess', 'connector', 'typed', 'connectorEvent', 'externalTask']);
61
60
  export function check(built, opts = {}) {
62
61
  const diags = [];
63
- if (!built.id)
64
- diags.push({ level: 'error', code: 'NO_ID', message: 'bpmn() requires an id.' });
65
- const seenIds = new Set();
66
- checkScope(built.nodes, built.flows, 'process', diags, seenIds, true);
62
+ // The rule lives in serialize.ts `documentIdProblems`; this check does not serialize.
63
+ for (const p of documentIdProblems(built)) {
64
+ diags.push({ level: 'error', code: p.code, message: p.message, ...(p.where ? { where: p.where } : {}) });
65
+ }
66
+ checkScope(built.nodes, built.flows, 'process', diags, true);
67
67
  checkScopedVars(built, diags);
68
68
  checkVariableOrder(built, diags);
69
- checkDocumentIds(built, diags);
70
69
  checkFolderBindings(built, diags);
71
70
  checkIntsvcActivities(built, diags);
72
71
  checkEntryPointId(built, diags);
@@ -237,8 +236,6 @@ function checkMethodParentheses(built, diags) {
237
236
  };
238
237
  walk(built.nodes, built.flows);
239
238
  }
240
- /** The Integration Service activity types, whose payload rows the platform reads only by `target`. */
241
- const CONNECTOR_ACTIVITY_TYPES = new Set(['Intsvc.ActivityExecution', 'Intsvc.AsyncExecution']);
242
239
  /**
243
240
  * The targets the platform's reader knows (`UiPathExtensionsProvider`): anything else —
244
241
  * an empty string included — falls through its switch to `Body`, and the row becomes the
@@ -256,23 +253,18 @@ const PLATFORM_ROW_TARGETS = new Set(['body', 'bodyField', 'header', 'path', 'qu
256
253
  * while the v1 arm's `<uipath:input name="body" target="body">{"script":"…"}</uipath:input>`
257
254
  * ran. `inputs: { script }` folds a request field into the body row, so the remedy is
258
255
  * the plain record for a body field, or the row's own target when it is a real slot.
259
- * And because an `inputRows` list REPLACES `inputs` at emission (typed-node.ts), a
260
- * node that sets both silently drops every `inputs` field — the same live fault.
256
+ * And because an `inputRows` list REPLACES `inputs` at emission, a node that sets both
257
+ * would drop every `inputs` field — the same live fault — so the serializer refuses it
258
+ * (typed-node.ts `connectorInputsShadowed`).
261
259
  */
262
260
  function checkConnectorRows(built, diags) {
263
261
  for (const n of allNodes(built.nodes)) {
264
262
  if (n.kind !== 'typed' || !CONNECTOR_ACTIVITY_TYPES.has(n.type))
265
263
  continue;
266
- if (n.inputRows && Object.keys(n.inputs ?? {}).length > 0) {
267
- diags.push({
268
- level: 'error',
269
- code: 'CONNECTOR_INPUTS_SHADOWED',
270
- where: n.id,
271
- message: `Connector activity "${n.id}" sets both \`inputs\` (${Object.keys(n.inputs).map((k) => `"${k}"`).join(', ')}) and \`inputRows\`; ` +
272
- `an \`inputRows\` list replaces \`inputs\` entirely at emission, so those fields are never sent. Spell them as rows too ` +
273
- `(a body field as \`{ name: 'body', type: 'json', target: 'body', body: { … } }\`), or drop \`inputRows\` and write every field in \`inputs\`.`,
274
- });
275
- }
264
+ // The rule lives in typed-node.ts `connectorInputsShadowed`.
265
+ const shadowed = connectorInputsShadowed(n.id, n.type, n.inputs, n.inputRows);
266
+ if (shadowed)
267
+ diags.push({ level: 'error', code: 'CONNECTOR_INPUTS_SHADOWED', where: n.id, message: shadowed });
276
268
  for (const row of n.inputRows ?? []) {
277
269
  const target = row.target ?? '';
278
270
  if (target === '') {
@@ -628,85 +620,6 @@ function checkIntsvcActivities(built, diags) {
628
620
  }
629
621
  }
630
622
  }
631
- /**
632
- * Every `id` in the document must be unique — including across KINDS.
633
- *
634
- * Not a style rule. `bpmn-moddle` keeps one document-global id registry, so a
635
- * second use of an id makes its whole element **unparsable**, and it drops it:
636
- *
637
- * unparsable content <bpmn:task> detected … nested error: duplicate ID <after>
638
- *
639
- * `.var('after', …)` next to `.task('after', …)` therefore emits a `.bpmn` whose
640
- * task simply is not there. Both validators still pass it — the canvas one
641
- * downgrades the now-dangling flows to warnings and `uip maestro bpmn validate`
642
- * reports `Valid` — and the runtime then dies with `Unknown element 'after'`, or
643
- * worse, silently does no work if nothing else routes through the hole. So this
644
- * is the only gate that can catch it, and it has to be an error.
645
- *
646
- * The ids in play are every element and flow, every variable and binding, the
647
- * synthesized `Message_n`/`Error_n` declarations, and the process itself — and the
648
- * declarations serialize DERIVES from output rows (a connector's `<id>_response`, a
649
- * script task's `outputs`), which are variables in the document too. Those were the one
650
- * kind this check could not see: a script output named after the gateway that routes
651
- * on it (`outputs: { confidence }` beside `.choose('confidence', …)`) passed here and
652
- * every SDK gate, and `uip maestro bpmn validate` answered with three
653
- * CROSSING_SUBPROCESS_BOUNDARY errors on the gateway's flows — the validator had
654
- * resolved the id to the variable. Measured on the nested_review port.
655
- */
656
- function checkDocumentIds(built, diags) {
657
- const owners = new Map();
658
- const claim = (id, kind) => {
659
- const existing = owners.get(id);
660
- if (existing === undefined) {
661
- owners.set(id, kind);
662
- return;
663
- }
664
- diags.push({
665
- level: 'error',
666
- code: 'DUPLICATE_DOCUMENT_ID',
667
- message: `id "${id}" is used by both ${existing} and ${kind}. Every id in a .bpmn must be unique — ` +
668
- `the XML parser drops the second element that reuses one, so it disappears from the process.`,
669
- where: id,
670
- });
671
- };
672
- claim(built.id, 'the process');
673
- const owner = (nodes, id) => nodes.find((n) => nodeDeclaredVars(n).includes(id))?.id ?? '?';
674
- // A sub-process row's variable counts as declared when serialize's createSubProcess
675
- // sees it so: the root's declarations, implied ones included, and every sub-process's own.
676
- const rootImplied = implicitNodeVars(built.nodes, built.variables);
677
- const declared = [
678
- ...built.variables,
679
- ...rootImplied,
680
- ...allNodes(built.nodes).flatMap((n) => (n.kind === 'subProcess' ? n.variables : [])),
681
- ];
682
- const walk = (nodes, flows) => {
683
- for (const n of nodes) {
684
- claim(n.id, `element "${n.id}"`);
685
- if (n.kind === 'subProcess') {
686
- for (const v of n.variables)
687
- claim(v.id, `variable "${v.id}"`);
688
- for (const v of implicitNodeVars(n.nodes, [...n.variables, ...declared])) {
689
- claim(v.id, `the variable serialize declares for the output row of "${owner(n.nodes, v.id)}"`);
690
- }
691
- walk(n.nodes, n.flows);
692
- }
693
- }
694
- for (const f of flows)
695
- claim(f.id, `flow "${f.id}"`);
696
- };
697
- walk(built.nodes, built.flows);
698
- for (const v of built.variables)
699
- claim(v.id, `variable "${v.id}"`);
700
- for (const v of rootImplied) {
701
- claim(v.id, `the variable serialize declares for the output row of "${owner(built.nodes, v.id)}"`);
702
- }
703
- for (const b of built.bindings)
704
- claim(b.id, `binding "${b.id}"`);
705
- for (const mm of built.messages)
706
- claim(mm.id, `message "${mm.name}"`);
707
- for (const e of built.errors)
708
- claim(e.id, `error "${e.name}"`);
709
- }
710
623
  /**
711
624
  * A pinned declaration order must name the declarations it is ordering.
712
625
  *
@@ -1021,7 +934,7 @@ function checkActivityFields(n, diags) {
1021
934
  });
1022
935
  }
1023
936
  }
1024
- function checkScope(nodes, flows, where, diags, seenIds, isTop,
937
+ function checkScope(nodes, flows, where, diags, isTop,
1025
938
  /**
1026
939
  * Whether the CONTAINER of these nodes is an event sub-process. The process root
1027
940
  * never is, so it defaults false — see ERROR_START_OUTSIDE_EVENT_SUBPROCESS below.
@@ -1030,11 +943,6 @@ inEventSubProcess = false) {
1030
943
  const nodeIds = new Set();
1031
944
  for (const n of nodes)
1032
945
  nodeIds.add(n.id);
1033
- // Duplicate ids (globally across the document).
1034
- for (const n of nodes)
1035
- dupId(n.id, seenIds, diags, where);
1036
- for (const f of flows)
1037
- dupId(f.id, seenIds, diags, where);
1038
946
  // At least one start event per scope that has any nodes.
1039
947
  //
1040
948
  // "Start event" is the ELEMENT, not the builder kind. A registry-backed node can
@@ -1472,7 +1380,7 @@ inEventSubProcess = false) {
1472
1380
  }
1473
1381
  // Recurse into sub-process bodies.
1474
1382
  if (n.kind === 'subProcess')
1475
- checkScope(n.nodes, n.flows, `subProcess "${n.id}"`, diags, seenIds, false, n.triggeredByEvent === true);
1383
+ checkScope(n.nodes, n.flows, `subProcess "${n.id}"`, diags, false, n.triggeredByEvent === true);
1476
1384
  }
1477
1385
  }
1478
1386
  /**
@@ -1500,8 +1408,3 @@ function isStartElement(n) {
1500
1408
  return false;
1501
1409
  }
1502
1410
  }
1503
- function dupId(id, seen, diags, where) {
1504
- if (seen.has(id))
1505
- diags.push({ level: 'error', code: 'DUP_ID', message: `Duplicate element id "${id}" in ${where}.`, where: id });
1506
- seen.add(id);
1507
- }
@@ -24,6 +24,47 @@ export declare function serialize(built: BuiltBpmn, opts?: BpmnSerializeOpts): P
24
24
  * of the trade. Revisit if the graders learn to treat absence as interrupting.
25
25
  */
26
26
  export declare function finalizeBpmnXml(xml: string): string;
27
+ /** One id problem in a process: the code, its message, and the id it is about. */
28
+ export interface DocumentIdProblem {
29
+ code: 'BPMN_NO_ID' | 'DUP_ID' | 'DUPLICATE_DOCUMENT_ID';
30
+ message: string;
31
+ where: string;
32
+ }
33
+ /**
34
+ * Every id the `.bpmn` would get wrong, in document order. `serialize` refuses the
35
+ * first; `check` reports them all. One rule for both, because BPMN `check` does not
36
+ * run the serializer.
37
+ *
38
+ * - `BPMN_NO_ID`: an empty process id. `processId` would write the invented
39
+ * `Process_1`, a process the source does not name (decompile then writes
40
+ * `bpmn("Process_1")`). Flow's and Case's `NO_ID` stay `lint`: their files keep
41
+ * the empty id, or never carry it.
42
+ * - `DUP_ID`: two elements or sequence flows with one id.
43
+ * - `DUPLICATE_DOCUMENT_ID`: any other id used twice: an element and a variable,
44
+ * two variables, a binding, a message, an error, the process.
45
+ *
46
+ * Not style rules. `bpmn-moddle` keeps one document-global id registry, so a
47
+ * second use of an id makes its whole element **unparsable**, and it drops it:
48
+ *
49
+ * unparsable content <bpmn:task> detected … nested error: duplicate ID <after>
50
+ *
51
+ * `.var('after', …)` next to `.task('after', …)` therefore emits a `.bpmn` whose
52
+ * task simply is not there. Both validators still pass it — the canvas one
53
+ * downgrades the now-dangling flows to warnings and `uip maestro bpmn validate`
54
+ * reports `Valid` — and the runtime then dies with `Unknown element 'after'`, or
55
+ * worse, silently does no work if nothing else routes through the hole.
56
+ *
57
+ * The ids in play are every element and flow, every variable and binding, the
58
+ * synthesized `Message_n`/`Error_n` declarations, and the process itself (its id
59
+ * as written, `processId`) — and the declarations this file DERIVES from output
60
+ * rows (a connector's `<id>_response`, a script task's `outputs`), which are
61
+ * variables in the document too. A script output named after the gateway that
62
+ * routes on it (`outputs: { confidence }` beside `.choose('confidence', …)`) once
63
+ * passed every SDK gate, and `uip maestro bpmn validate` answered with three
64
+ * CROSSING_SUBPROCESS_BOUNDARY errors on the gateway's flows — the validator had
65
+ * resolved the id to the variable. Measured on the nested_review port.
66
+ */
67
+ export declare function documentIdProblems(built: BuiltBpmn): DocumentIdProblem[];
27
68
  export declare function processId(id: string): string;
28
69
  /**
29
70
  * The `args` keys every Studio Web script task carries, in the order the designer writes
@@ -18,7 +18,7 @@ import { createRequire } from 'node:module';
18
18
  import { createConnectorTask } from './connector.js';
19
19
  import { createConnectorEvent, createExternalTask, intsvcOutputVar } from './intsvc.js';
20
20
  import { createExtensionPayload, createTypedNode, rowDeclaration, typedNodeOutputVar, verbatimOutputRows } from './typed-node.js';
21
- import { connectorOutputVars, typedNodeDeclaredVars } from './declared-vars.js';
21
+ import { connectorOutputVars, nodeDeclaredVars, typedNodeDeclaredVars } from './declared-vars.js';
22
22
  import { defaultEntryPointId } from './entry-point-id.js';
23
23
  import { parseXmlRanges } from './xml-ranges.js';
24
24
  import { ownEntry } from '../core/own-entry.js';
@@ -53,6 +53,11 @@ function mapType(t) {
53
53
  }
54
54
  }
55
55
  export async function serialize(built, opts = {}) {
56
+ // compile writes what this returns, and since #1497 nothing runs `check` first
57
+ // on every path, so a document this would write wrong is refused here.
58
+ const [problem] = documentIdProblems(built);
59
+ if (problem)
60
+ throw new CompileRefusal(problem.code, problem.message);
56
61
  const m = new BpmnModdle({ uipath: uipathDescriptor });
57
62
  const messageById = new Map();
58
63
  const messages = built.messages.map((mm) => {
@@ -308,6 +313,103 @@ function cdata(name, text) {
308
313
  }
309
314
  return `<![CDATA[${escaped}]]>`;
310
315
  }
316
+ /**
317
+ * Every id the `.bpmn` would get wrong, in document order. `serialize` refuses the
318
+ * first; `check` reports them all. One rule for both, because BPMN `check` does not
319
+ * run the serializer.
320
+ *
321
+ * - `BPMN_NO_ID`: an empty process id. `processId` would write the invented
322
+ * `Process_1`, a process the source does not name (decompile then writes
323
+ * `bpmn("Process_1")`). Flow's and Case's `NO_ID` stay `lint`: their files keep
324
+ * the empty id, or never carry it.
325
+ * - `DUP_ID`: two elements or sequence flows with one id.
326
+ * - `DUPLICATE_DOCUMENT_ID`: any other id used twice: an element and a variable,
327
+ * two variables, a binding, a message, an error, the process.
328
+ *
329
+ * Not style rules. `bpmn-moddle` keeps one document-global id registry, so a
330
+ * second use of an id makes its whole element **unparsable**, and it drops it:
331
+ *
332
+ * unparsable content <bpmn:task> detected … nested error: duplicate ID <after>
333
+ *
334
+ * `.var('after', …)` next to `.task('after', …)` therefore emits a `.bpmn` whose
335
+ * task simply is not there. Both validators still pass it — the canvas one
336
+ * downgrades the now-dangling flows to warnings and `uip maestro bpmn validate`
337
+ * reports `Valid` — and the runtime then dies with `Unknown element 'after'`, or
338
+ * worse, silently does no work if nothing else routes through the hole.
339
+ *
340
+ * The ids in play are every element and flow, every variable and binding, the
341
+ * synthesized `Message_n`/`Error_n` declarations, and the process itself (its id
342
+ * as written, `processId`) — and the declarations this file DERIVES from output
343
+ * rows (a connector's `<id>_response`, a script task's `outputs`), which are
344
+ * variables in the document too. A script output named after the gateway that
345
+ * routes on it (`outputs: { confidence }` beside `.choose('confidence', …)`) once
346
+ * passed every SDK gate, and `uip maestro bpmn validate` answered with three
347
+ * CROSSING_SUBPROCESS_BOUNDARY errors on the gateway's flows — the validator had
348
+ * resolved the id to the variable. Measured on the nested_review port.
349
+ */
350
+ export function documentIdProblems(built) {
351
+ const problems = [];
352
+ if (!built.id)
353
+ problems.push({ code: 'BPMN_NO_ID', message: 'bpmn() requires an id.', where: '' });
354
+ /** id → who holds it, and whether that holder is an element or a flow. */
355
+ const owners = new Map();
356
+ const claim = (id, kind, element = undefined) => {
357
+ const existing = owners.get(id);
358
+ if (existing === undefined) {
359
+ owners.set(id, { kind, element: element !== undefined });
360
+ return;
361
+ }
362
+ if (element && existing.element) {
363
+ problems.push({ code: 'DUP_ID', message: `Duplicate element id "${id}" in ${element.scope}.`, where: id });
364
+ return;
365
+ }
366
+ problems.push({
367
+ code: 'DUPLICATE_DOCUMENT_ID',
368
+ message: `id "${id}" is used by both ${existing.kind} and ${kind}. Every id in a .bpmn must be unique — ` +
369
+ `the XML parser drops the second element that reuses one, so it disappears from the process.`,
370
+ where: id,
371
+ });
372
+ };
373
+ if (built.id)
374
+ claim(processId(built.id), 'the process');
375
+ const owner = (nodes, id) => nodes.find((n) => nodeDeclaredVars(n).includes(id))?.id ?? '?';
376
+ // A sub-process row's variable counts as declared when createSubProcess sees it
377
+ // so: the root's declarations, implied ones included, and every sub-process's own.
378
+ const rootImplied = implicitNodeVars(built.nodes, built.variables);
379
+ const declared = [
380
+ ...built.variables,
381
+ ...rootImplied,
382
+ ...allNodes(built.nodes).flatMap((n) => (n.kind === 'subProcess' ? n.variables : [])),
383
+ ];
384
+ const walk = (nodes, flows, scope) => {
385
+ for (const n of nodes) {
386
+ claim(n.id, `element "${n.id}"`, { scope });
387
+ if (n.kind === 'subProcess') {
388
+ for (const v of n.variables)
389
+ claim(v.id, `variable "${v.id}"`);
390
+ for (const v of implicitNodeVars(n.nodes, [...n.variables, ...declared])) {
391
+ claim(v.id, `the variable serialize declares for the output row of "${owner(n.nodes, v.id)}"`);
392
+ }
393
+ walk(n.nodes, n.flows, `subProcess "${n.id}"`);
394
+ }
395
+ }
396
+ for (const f of flows)
397
+ claim(f.id, `flow "${f.id}"`, { scope });
398
+ };
399
+ walk(built.nodes, built.flows, 'process');
400
+ for (const v of built.variables)
401
+ claim(v.id, `variable "${v.id}"`);
402
+ for (const v of rootImplied) {
403
+ claim(v.id, `the variable serialize declares for the output row of "${owner(built.nodes, v.id)}"`);
404
+ }
405
+ for (const b of built.bindings)
406
+ claim(b.id, `binding "${b.id}"`);
407
+ for (const mm of built.messages)
408
+ claim(mm.id, `message "${mm.name}"`);
409
+ for (const e of built.errors)
410
+ claim(e.id, `error "${e.name}"`);
411
+ return problems;
412
+ }
311
413
  export function processId(id) {
312
414
  return /^[A-Za-z_][\w.-]*$/.test(id) ? id : 'Process_1';
313
415
  }
@@ -328,6 +328,18 @@ export interface TypedOutputRow {
328
328
  */
329
329
  target?: string;
330
330
  }
331
+ /** The Integration Service activity types, whose payload rows the platform reads only by `target`. */
332
+ export declare const CONNECTOR_ACTIVITY_TYPES: ReadonlySet<string>;
333
+ /**
334
+ * CONNECTOR_INPUTS_SHADOWED's message when a connector activity sets both `inputs`
335
+ * and `inputRows`, else `undefined`.
336
+ *
337
+ * An `inputRows` list replaces `inputs` at emission (`createTypedNode`), so writing
338
+ * the node would silently drop every `inputs` field, and the live call then faults
339
+ * on the missing parameter. `createTypedNode` refuses it; `check` reports the same
340
+ * condition from this function, because BPMN `check` does not serialize.
341
+ */
342
+ export declare function connectorInputsShadowed(id: string, type: string, inputs: Record<string, unknown> | undefined, rows: readonly unknown[] | undefined): string | undefined;
331
343
  /** What an author supplies for a typed node, on top of its id. */
332
344
  export interface TypedNodeInput {
333
345
  /** Display name the designer shows. */
@@ -193,6 +193,27 @@ export function rowDeclaration(row) {
193
193
  const wire = type === 'object' || type === 'array' || type === 'any' ? 'json' : type;
194
194
  return { type, wire, name: row.name, schema: row.varSchema };
195
195
  }
196
+ /** The Integration Service activity types, whose payload rows the platform reads only by `target`. */
197
+ export const CONNECTOR_ACTIVITY_TYPES = new Set(['Intsvc.ActivityExecution', 'Intsvc.AsyncExecution']);
198
+ /**
199
+ * CONNECTOR_INPUTS_SHADOWED's message when a connector activity sets both `inputs`
200
+ * and `inputRows`, else `undefined`.
201
+ *
202
+ * An `inputRows` list replaces `inputs` at emission (`createTypedNode`), so writing
203
+ * the node would silently drop every `inputs` field, and the live call then faults
204
+ * on the missing parameter. `createTypedNode` refuses it; `check` reports the same
205
+ * condition from this function, because BPMN `check` does not serialize.
206
+ */
207
+ export function connectorInputsShadowed(id, type, inputs, rows) {
208
+ if (!CONNECTOR_ACTIVITY_TYPES.has(type) || !rows)
209
+ return undefined;
210
+ const keys = Object.keys(inputs ?? {});
211
+ if (keys.length === 0)
212
+ return undefined;
213
+ return (`Connector activity "${id}" sets both \`inputs\` (${keys.map((k) => `"${k}"`).join(', ')}) and \`inputRows\`; ` +
214
+ `an \`inputRows\` list replaces \`inputs\` entirely at emission, so those fields are never sent. Spell them as rows too ` +
215
+ `(a body field as \`{ name: 'body', type: 'json', target: 'body', body: { … } }\`), or drop \`inputRows\` and write every field in \`inputs\`.`);
216
+ }
196
217
  /**
197
218
  * An output name reduced to something an expression can actually read.
198
219
  *
@@ -548,6 +569,9 @@ varType) {
548
569
  // Spelled out by the caller: emit exactly these rows, deriving nothing — see
549
570
  // TypedNodeInput.inputRows. This is what lets a connector's per-parameter rows
550
571
  // (`name="start" type="string" value="0" target="query"`) survive a round trip.
572
+ const shadowed = connectorInputsShadowed(id, type, input.inputs, input.inputRows);
573
+ if (shadowed)
574
+ throw new CompileRefusal('CONNECTOR_INPUTS_SHADOWED', shadowed);
551
575
  const ins = input.inputRows ? verbatimInputRows(m, input.inputRows) : inputRows(m, spec, input.inputs ?? {});
552
576
  if (ins.length)
553
577
  payload.input = ins;
@@ -8,18 +8,8 @@ import { EQUALS_EXPR_DIALECT, checkBindingLabels, checkConnectorContract, checkC
8
8
  import { checkEventResolution } from '../core/event-checks.js';
9
9
  import { EXTERNAL_TASK_DESCRIPTOR_CONTRACT, EXTERNAL_TASK_HYBRID_UNSUPPORTED, externalHybridRefusal, isExternalExecutionMode } from './external.js';
10
10
  import { isResourceLabelRefusal, resolveResourceRef } from './resource-identity.js';
11
+ import { taskShapeProblems } from './task-shape.js';
11
12
  const TYPE_NAMES = new Set(Object.values(types));
12
- /**
13
- * Every key an `ActionField` takes. `compile` writes only these onto the form
14
- * row, so any other key would be dropped without a word (builder-sdk#1422): an
15
- * author who adds the Designer's row keys (`id`, `var`, `source`) sees `check`
16
- * and `compile` pass and the row come out unchanged. The `satisfies` keeps this
17
- * list and the interface in step: a key added to one and not the other fails to
18
- * type-check.
19
- */
20
- const ACTION_FIELD_KEYS = new Set(Object.keys({
21
- name: true, type: true, subType: true, displayName: true, value: true, required: true, options: true,
22
- }));
23
13
  // audit-case-coverage: rule-slot table start
24
14
  // This runtime table must equal the committed matrix's rejected cells. The audit
25
15
  // parses these object literals; the product-pin matrix currently rejects none.
@@ -509,17 +499,10 @@ function checkStage(stage, diags, refStage, refPlacement, bindings) {
509
499
  where: stage.label,
510
500
  });
511
501
  for (const t of tasks) {
512
- if (t.kind === 'wait-for-timer') {
513
- const timerValues = [t.timer?.duration, t.timer?.date, t.timer?.cycle]
514
- .filter((value) => typeof value === 'string' && value.trim().length > 0);
515
- if (timerValues.length !== 1)
516
- diags.push({
517
- level: 'error',
518
- code: 'TIMER_VALUE_REQUIRED',
519
- message: `Timer task "${t.name}" needs exactly one non-empty duration, date, or cycle. For a validating stand-in, use .waitForTimer({ duration: 'PT1M' }).`,
520
- where: t.name,
521
- });
522
- }
502
+ // A timer with no or several values, an action field key compile does not
503
+ // write: serialize refuses the same shapes with the same text (task-shape.ts).
504
+ for (const problem of taskShapeProblems(t))
505
+ diags.push({ level: 'error', code: problem.code, message: problem.message, where: t.name });
523
506
  const invalidBinding = [...(t.inputs ?? []), ...(t.outputs ?? [])]
524
507
  .find((binding) => !TYPE_NAMES.has(binding.type));
525
508
  if (invalidBinding)
@@ -681,24 +664,6 @@ function checkStage(stage, diags, refStage, refPlacement, bindings) {
681
664
  message: `Action field "${emptyRequiredField.name}" in task "${t.name}" is required but has no value. Set its value or remove required: true; required does not mean reviewer-must-fill.`,
682
665
  where: t.name,
683
666
  });
684
- for (const [side, fields] of [['input', t.action?.inputs], ['output', t.action?.outputs]])
685
- for (const field of fields ?? []) {
686
- const unknown = Object.keys(field)
687
- .filter((key) => !ACTION_FIELD_KEYS.has(key) && field[key] !== undefined);
688
- if (!unknown.length)
689
- continue;
690
- diags.push({
691
- level: 'error',
692
- code: 'ACTION_FIELD_UNKNOWN_KEY',
693
- message: `Action ${side} field "${String(field.name)}" in task "${t.name}" has ${unknown.length === 1 ? 'a key' : 'keys'} ` +
694
- `${unknown.map((key) => `"${key}"`).join(', ')} that an action field does not take, so compile would drop ${unknown.length === 1 ? 'it' : 'them'}. ` +
695
- `An action field takes ${[...ACTION_FIELD_KEYS].join(', ')}. ` +
696
- (side === 'output'
697
- ? `To choose the case variable an output writes, set value: '<variable>'; compile writes the row's id, var, source and target from it.`
698
- : `An input's value is the literal or =vars.<name> expression the assignee sees.`),
699
- where: t.name,
700
- });
701
- }
702
667
  if (t.entry.length === 0)
703
668
  diags.push({
704
669
  level: 'error',
@@ -31,6 +31,7 @@ import { readEventFilterLeaf, eventFilterTreeLeaf } from '../core/event-filters.
31
31
  import { responseJsonSchema } from '../core/connector-response-schema.js';
32
32
  import { ruleShapeProblems } from './rule-options.js';
33
33
  import { CompileRefusal } from '../core/compile-refusal.js';
34
+ import { taskShapeProblems } from './task-shape.js';
34
35
  /** The ids a serialize pass hashed from element ids, by the document it returned. */
35
36
  const ELEMENT_SEEDED_IDS = new WeakMap();
36
37
  function elementSeededId(resolver, elementId, suffix) {
@@ -1148,6 +1149,11 @@ function emitTask(task, stageLabel, taskId, ids, resolver) {
1148
1149
  return node;
1149
1150
  }
1150
1151
  function taskData(task, resolver, taskId, stageId, ids) {
1152
+ // A timer with no or several values, or an action field key the row has no
1153
+ // place for, cannot be written as authored: refuse it, as `check` does (#1518).
1154
+ const [shape] = taskShapeProblems(task);
1155
+ if (shape)
1156
+ throw new CompileRefusal(shape.code, shape.message);
1151
1157
  switch (task.kind) {
1152
1158
  case 'process':
1153
1159
  case 'agent':
@@ -0,0 +1,31 @@
1
+ /**
2
+ * The task shapes Case `check` reports and `serialize` refuses, because the
3
+ * caseplan cannot say what the source says (#1518):
4
+ *
5
+ * - `TIMER_VALUE_REQUIRED`: a wait-for-timer task sets exactly one of
6
+ * duration, date or cycle, and that one is non-blank. With two, serialize writes the first and drops
7
+ * the rest (`{ duration: 'PT1M', date: '2027-…' }` came out as
8
+ * `timeDuration: 'PT1M'` alone); with none, it writes `data: {}`, which
9
+ * product validation refuses.
10
+ * - `ACTION_FIELD_UNKNOWN_KEY`: an action form field takes only the
11
+ * `ActionField` keys; serialize writes only those, so any other key
12
+ * (`default: 5`, the Designer's `id`/`var`/`source`) was dropped without a
13
+ * word (#1422).
14
+ *
15
+ * Case `check` does not run the serializer, so both call these functions: one
16
+ * rule, one place, the same code and message from each.
17
+ */
18
+ import type { BuiltTask } from './case-sdk.js';
19
+ /** One problem with a task's shape. */
20
+ export interface TaskShapeProblem {
21
+ code: 'TIMER_VALUE_REQUIRED' | 'ACTION_FIELD_UNKNOWN_KEY';
22
+ message: string;
23
+ }
24
+ /**
25
+ * Every problem with `task`'s shape, in a fixed order: the timer first, then
26
+ * each action input field, then each output field.
27
+ *
28
+ * @param task - The task as the builder recorded it.
29
+ * @returns The problems, empty when the task is well formed.
30
+ */
31
+ export declare function taskShapeProblems(task: BuiltTask): TaskShapeProblem[];