@uipath/maestro-builder-sdk 6.11.5 → 6.12.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.
- package/dist/api-index.md +83 -56
- package/dist/api-members.md +362 -261
- package/dist/bpmn/bpmn-expr-check.js +8 -1
- package/dist/bpmn/bpmn-sdk.d.ts +1026 -27
- package/dist/bpmn/bpmn-sdk.js +1328 -83
- package/dist/bpmn/check-cli.js +19 -4
- package/dist/bpmn/check.d.ts +1 -1
- package/dist/bpmn/check.js +529 -13
- package/dist/bpmn/connector.d.ts +2 -2
- package/dist/bpmn/connector.js +74 -37
- package/dist/bpmn/data-service.d.ts +124 -0
- package/dist/bpmn/data-service.js +98 -0
- package/dist/bpmn/decompile-cli.js +70 -8
- package/dist/bpmn/decompile.d.ts +71 -0
- package/dist/bpmn/decompile.js +845 -31
- package/dist/bpmn/graph.d.ts +69 -0
- package/dist/bpmn/graph.js +98 -0
- package/dist/bpmn/intsvc.js +2 -2
- package/dist/bpmn/known-methods.d.ts +15 -0
- package/dist/bpmn/known-methods.js +271 -0
- package/dist/bpmn/lift-verify.d.ts +40 -0
- package/dist/bpmn/lift-verify.js +133 -0
- package/dist/bpmn/serialize.d.ts +65 -1
- package/dist/bpmn/serialize.js +169 -22
- package/dist/bpmn/typed-node.d.ts +37 -6
- package/dist/bpmn/typed-node.js +81 -19
- package/dist/check-cli.js +2 -1
- package/dist/check.d.ts +13 -0
- package/dist/check.js +11 -0
- package/dist/cli/commands/bpmn.js +7 -1
- package/dist/cli-library.d.ts +7 -0
- package/dist/cli-library.js +19 -0
- package/dist/compile-cli.js +8 -5
- package/dist/core/byoa.d.ts +69 -0
- package/dist/core/byoa.js +98 -0
- package/dist/core/library.d.ts +23 -0
- package/dist/core/library.js +47 -1
- package/dist/core/lookup-store.d.ts +12 -0
- package/dist/core/lookup-store.js +25 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.js +3 -0
- package/dist/registry/generators/descriptors.d.ts +1 -1
- package/dist/registry/generators/descriptors.js +20 -2
- package/dist/registry/generators/markdown/page.js +3 -1
- package/dist/registry/generators/markdown/sections/event-facts.d.ts +2 -0
- package/dist/registry/generators/markdown/sections/event-facts.js +45 -0
- package/dist/registry/library-build/events.d.ts +32 -3
- package/dist/registry/library-build/events.js +48 -19
- package/dist/registry/prepare/byoa-ledger.d.ts +12 -0
- package/dist/registry/prepare/byoa-ledger.js +26 -0
- package/dist/registry/prepare/event.d.ts +5 -1
- package/dist/registry/prepare/event.js +22 -8
- package/dist/registry/prepare/messages.d.ts +2 -0
- package/dist/registry/prepare/messages.js +4 -0
- package/dist/serialize.js +24 -1
- package/package.json +2 -2
package/dist/bpmn/check.js
CHANGED
|
@@ -1,5 +1,55 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* bpmn/check — fast static validation of a BuiltBpmn before serialization.
|
|
3
|
+
*
|
|
4
|
+
* Surfaces the common Maestro canvas-rule failures early (dangling flow refs,
|
|
5
|
+
* duplicate ids, a gateway branch with no condition, a superfluous gateway, an
|
|
6
|
+
* implicit join, a missing start event, a boundary event attached to nothing, an
|
|
7
|
+
* empty timer) so authoring gets instant feedback. The authoritative gate is
|
|
8
|
+
* still `uip maestro bpmn validate` (`validateBpmnCanvas`) on the emitted
|
|
9
|
+
* `.bpmn`.
|
|
10
|
+
*
|
|
11
|
+
* **An `error` here must mirror a real platform rule.** Being stricter than
|
|
12
|
+
* `validate` is worse than being looser: it refuses processes that deploy and run
|
|
13
|
+
* correctly, and the author has no way to appeal. Where this file is deliberately
|
|
14
|
+
* stricter than the platform — an unconditioned inclusive-gateway branch, an
|
|
15
|
+
* implicit join — the diagnostic is a `warning`, and its comment says which
|
|
16
|
+
* platform rule it corresponds to and how the two differ.
|
|
17
|
+
*/
|
|
18
|
+
import { nodeDeclaredVars } from './bpmn-sdk.js';
|
|
19
|
+
import { folderCompanionKeys, implicitNodeVars, processId } from './serialize.js';
|
|
20
|
+
import { KNOWN_METHOD_SET } from './known-methods.js';
|
|
2
21
|
import { bindingRequiredFields, missingRequiredContext, missingRequiredInputs, registryType, REGISTRY_GAPS, unsatisfiableRequiredInputs, } from './typed-node.js';
|
|
22
|
+
/**
|
|
23
|
+
* Service types the platform expects to name a concrete Orchestrator resource.
|
|
24
|
+
*
|
|
25
|
+
* From the canvas rule `MISSING_RESOURCE`, copied rather than inferred: nothing in the
|
|
26
|
+
* registry marks these, and the rule turns on the service type alone.
|
|
27
|
+
*/
|
|
28
|
+
const SERVICE_TYPES_REQUIRING_RESOURCE = new Set([
|
|
29
|
+
'Orchestrator.StartAgentJob',
|
|
30
|
+
'Orchestrator.StartJob',
|
|
31
|
+
'Orchestrator.CreateQueueItem',
|
|
32
|
+
'Orchestrator.CreateAndWaitForQueueItem',
|
|
33
|
+
'Orchestrator.BulkAddQueueItems',
|
|
34
|
+
'Orchestrator.QueueItemAdded',
|
|
35
|
+
'Orchestrator.ExecuteApiWorkflowAsync',
|
|
36
|
+
'Orchestrator.StartFlowProcess',
|
|
37
|
+
'Orchestrator.StartFlowProcessAsync',
|
|
38
|
+
'Actions.HITL',
|
|
39
|
+
'Orchestrator.StartAgenticProcess',
|
|
40
|
+
'Orchestrator.StartAgenticProcessAsync',
|
|
41
|
+
'Orchestrator.StartCaseMgmtProcess',
|
|
42
|
+
'Orchestrator.StartCaseMgmtProcessAsync',
|
|
43
|
+
]);
|
|
44
|
+
/**
|
|
45
|
+
* A HITL task carrying its form inline needs no Action-app resource — the platform's
|
|
46
|
+
* own carve-out from `MISSING_RESOURCE`, and the reason this rule cannot be written as
|
|
47
|
+
* "these types need a resource".
|
|
48
|
+
*/
|
|
49
|
+
const HITL_QUICK_SCHEMA_CONTEXT = new Set(['_schemaFileId', 'hitlSchemaId', '__hitlFormSchema']);
|
|
50
|
+
/** The bounds the platform puts on a human task's `Timer`, in MINUTES. */
|
|
51
|
+
const MIN_TASK_TIMER_MINUTES = 15;
|
|
52
|
+
const MAX_TASK_TIMER_MINUTES = 90 * 24 * 60;
|
|
3
53
|
const GATEWAY_KINDS = new Set(['exclusiveGateway', 'parallelGateway', 'inclusiveGateway', 'eventBasedGateway']);
|
|
4
54
|
/**
|
|
5
55
|
* What a boundary event may attach to. `connector` belongs here: it serializes to
|
|
@@ -15,8 +65,174 @@ export function check(built) {
|
|
|
15
65
|
checkScope(built.nodes, built.flows, 'process', diags, seenIds, true);
|
|
16
66
|
checkScopedVars(built, diags);
|
|
17
67
|
checkDocumentIds(built, diags);
|
|
68
|
+
checkFolderBindings(built, diags);
|
|
69
|
+
checkEntryPointId(built, diags);
|
|
70
|
+
checkMethodParentheses(built, diags);
|
|
18
71
|
return diags;
|
|
19
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* A known method NAMED but not CALLED — `=vars.s.ToUpper` where `ToUpper()` was meant.
|
|
75
|
+
*
|
|
76
|
+
* A faithful port of the canvas rule `METHOD_MISSING_PARENTHESES`, down to its regex
|
|
77
|
+
* and its vocabulary (`known-methods.ts`, generated from the platform's own list). The
|
|
78
|
+
* platform reports it as an ERROR, so it gates — and the message it gives back names
|
|
79
|
+
* the method and an XML element id, which the author then has to map to the
|
|
80
|
+
* TypeScript that produced it.
|
|
81
|
+
*
|
|
82
|
+
* Reported here rather than refused at `.build()`, unlike the structural collisions in
|
|
83
|
+
* `bpmn-sdk.ts`. Those are two objects the builder is holding; this is a textual
|
|
84
|
+
* heuristic over expression source, and the sites this walks are the SDK's, not the
|
|
85
|
+
* platform's. If the two lists of sites ever diverge, a diagnostic is a message the
|
|
86
|
+
* author can weigh and a refusal is a wall — so the weaker rung is the honest one.
|
|
87
|
+
*/
|
|
88
|
+
function checkMethodParentheses(built, diags) {
|
|
89
|
+
const report = (expr, where) => {
|
|
90
|
+
if (!expr)
|
|
91
|
+
return;
|
|
92
|
+
// The platform's own pattern: a member access whose name is a known method and
|
|
93
|
+
// which is not followed by `(`. Kept character-for-character so the two agree.
|
|
94
|
+
for (const m of expr.matchAll(/\.(\w+)(?!\s*\()/g)) {
|
|
95
|
+
const method = m[1];
|
|
96
|
+
if (!KNOWN_METHOD_SET.has(method))
|
|
97
|
+
continue;
|
|
98
|
+
diags.push({
|
|
99
|
+
level: 'error',
|
|
100
|
+
code: 'METHOD_MISSING_PARENTHESES',
|
|
101
|
+
message: `${where} names method "${method}" without calling it. Write "${method}()" — the platform's ` +
|
|
102
|
+
`expression evaluator refuses a method referenced as a property.`,
|
|
103
|
+
where,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
const deep = (val, where) => {
|
|
108
|
+
if (typeof val === 'string')
|
|
109
|
+
return report(val, where);
|
|
110
|
+
if (Array.isArray(val))
|
|
111
|
+
return val.forEach((v) => deep(v, where));
|
|
112
|
+
if (val && typeof val === 'object')
|
|
113
|
+
for (const v of Object.values(val))
|
|
114
|
+
deep(v, where);
|
|
115
|
+
};
|
|
116
|
+
const walk = (nodes, flows) => {
|
|
117
|
+
for (const f of flows)
|
|
118
|
+
report(f.condition, `flow "${f.id}" condition`);
|
|
119
|
+
for (const n of nodes) {
|
|
120
|
+
const at = `${n.kind} "${n.id}"`;
|
|
121
|
+
for (const row of ('errorMapping' in n ? n.errorMapping : undefined) ?? [])
|
|
122
|
+
report(row.condition, `${at} errorMapping "${row.code}" condition`);
|
|
123
|
+
if ('loop' in n && n.loop) {
|
|
124
|
+
report(n.loop.collection, `${at} loop collection`);
|
|
125
|
+
report(n.loop.completion, `${at} loop completion`);
|
|
126
|
+
}
|
|
127
|
+
if (n.kind === 'scriptTask') {
|
|
128
|
+
// The script BODY is deliberately not walked. `obj.Trim` inside a program is a
|
|
129
|
+
// property access the platform never reads, and reporting it would be the SDK
|
|
130
|
+
// refusing what the platform accepts.
|
|
131
|
+
deep(n.inputs, `${at} input`);
|
|
132
|
+
deep(n.outputs, `${at} output`);
|
|
133
|
+
}
|
|
134
|
+
else if (n.kind === 'task') {
|
|
135
|
+
deep(n.set, `${at} assignment`);
|
|
136
|
+
}
|
|
137
|
+
else if (n.kind === 'connector') {
|
|
138
|
+
deep(n.inputs, `${at} input`);
|
|
139
|
+
}
|
|
140
|
+
else if (n.kind === 'typed') {
|
|
141
|
+
deep(n.context, `${at} context`);
|
|
142
|
+
deep(n.inputs, `${at} input`);
|
|
143
|
+
for (const row of n.contextRows ?? [])
|
|
144
|
+
report(row.value, `${at} context "${row.name}"`);
|
|
145
|
+
for (const row of n.inputRows ?? [])
|
|
146
|
+
report(row.value, `${at} input "${row.name}"`);
|
|
147
|
+
}
|
|
148
|
+
if (n.kind === 'subProcess')
|
|
149
|
+
walk(n.nodes, n.flows);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
walk(built.nodes, built.flows);
|
|
153
|
+
}
|
|
154
|
+
const GUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
155
|
+
/**
|
|
156
|
+
* `.metadata({ entryPointId })` must be a GUID.
|
|
157
|
+
*
|
|
158
|
+
* The product assigns one (`bpmn init` writes it, `decompile` carries it) and
|
|
159
|
+
* `uip maestro bpmn validate` refuses anything else — `Root start event
|
|
160
|
+
* uipath:entryPointId value "WikiPageviews_Start" must be a GUID` — which an author who
|
|
161
|
+
* hand-wrote a readable id only learns at the last rung. In the 2026-09-27 A/B against
|
|
162
|
+
* main one agent spent its whole turn budget there. Reported here, at the first rung.
|
|
163
|
+
*/
|
|
164
|
+
function checkEntryPointId(built, diags) {
|
|
165
|
+
const id = built.metadata?.entryPointId;
|
|
166
|
+
if (id === undefined || GUID.test(id))
|
|
167
|
+
return;
|
|
168
|
+
diags.push({
|
|
169
|
+
level: 'error',
|
|
170
|
+
code: 'ENTRYPOINT_ID_NOT_GUID',
|
|
171
|
+
message: `metadata.entryPointId "${id}" is not a GUID; \`uip maestro bpmn validate\` refuses it. Use the id \`bpmn init\` ` +
|
|
172
|
+
`assigned (decompile the stub it wrote to carry it), or a fresh one from \`crypto.randomUUID()\`.`,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* A `folderKey` binding must be keyed by its CONNECTION, not by the folder.
|
|
177
|
+
*
|
|
178
|
+
* The pair every real export writes is `ConnectionId {resourceKey: C, default: C}` +
|
|
179
|
+
* `folderKey {resourceKey: C, default: F}`: the folder is where the connection is
|
|
180
|
+
* resolved, so its resource key is the connection's. `uip maestro bpmn validate`
|
|
181
|
+
* reports `MISSING_BINDING` for a folder binding keyed by its own folder — which is
|
|
182
|
+
* what `.binding('f', { propertyAttribute: 'folderKey', value: F })` used to write, and
|
|
183
|
+
* what every run-6 agent that reached for Data Fabric or Test Manager shipped. The
|
|
184
|
+
* serializer now pairs a folder with its connection itself (`folderCompanionKeys`); this
|
|
185
|
+
* reports the two cases it cannot fix: an explicit `resourceKey` equal to the folder key,
|
|
186
|
+
* and a folder no node pairs with a connection when more than one connection exists.
|
|
187
|
+
*/
|
|
188
|
+
function checkFolderBindings(built, diags) {
|
|
189
|
+
const folders = built.bindings.filter((b) => String(b.propertyAttribute ?? '').toLowerCase() === 'folderkey');
|
|
190
|
+
if (!folders.length)
|
|
191
|
+
return;
|
|
192
|
+
// `serialize` pairs against its binding SINK — the connections a connector, connector
|
|
193
|
+
// event or external task declares as a side effect of being emitted, which never
|
|
194
|
+
// appear in `built.bindings` because the author named them symbolically. `check` runs
|
|
195
|
+
// before serialization and has no sink, and passing an empty one made it warn about
|
|
196
|
+
// folders the serializer goes on to pair correctly: a `.connector(..., { connection:
|
|
197
|
+
// 'conn', folder: 'myFolder' })` node contributes the only ConnectionId there is, and
|
|
198
|
+
// the one-connection rung inside `folderCompanionKeys` finds it. So the sink is
|
|
199
|
+
// reconstructed from the nodes here, which is the same set serialize will build.
|
|
200
|
+
const sink = new Map();
|
|
201
|
+
const collectConnections = (nodes) => {
|
|
202
|
+
for (const n of nodes) {
|
|
203
|
+
const named = n.kind === 'connector' || n.kind === 'connectorEvent' || n.kind === 'externalTask' ? n.connection : undefined;
|
|
204
|
+
if (named)
|
|
205
|
+
sink.set(named, { id: named, propertyAttribute: 'ConnectionId', default: named });
|
|
206
|
+
if (n.kind === 'subProcess')
|
|
207
|
+
collectConnections(n.nodes);
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
collectConnections(built.nodes);
|
|
211
|
+
const paired = folderCompanionKeys(built, sink);
|
|
212
|
+
for (const b of folders) {
|
|
213
|
+
if (b.resourceKey !== undefined && b.resourceKey === b.default) {
|
|
214
|
+
diags.push({
|
|
215
|
+
level: 'warning',
|
|
216
|
+
code: 'FOLDER_BINDING_KEYED_BY_FOLDER',
|
|
217
|
+
message: `Binding "${b.id}" (folderKey) has resourceKey equal to its own folder key. A folder binding is keyed by the ` +
|
|
218
|
+
`CONNECTION it belongs to (\`uip maestro bpmn validate\` reports MISSING_BINDING otherwise). Drop \`resourceKey\` ` +
|
|
219
|
+
`and the builder pairs it with the connection the node reads, or set it to that connection's key.`,
|
|
220
|
+
where: b.id,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
else if (b.resourceKey === undefined && !paired.has(b.id)) {
|
|
224
|
+
diags.push({
|
|
225
|
+
level: 'warning',
|
|
226
|
+
code: 'FOLDER_BINDING_UNPAIRED',
|
|
227
|
+
message: `Binding "${b.id}" (folderKey) could not be paired with a connection: no node reads it beside a ` +
|
|
228
|
+
`\`=bindings.<connection>\`, and the process does not declare exactly one ConnectionId binding. Its resourceKey ` +
|
|
229
|
+
`will be the folder key, which \`uip maestro bpmn validate\` reports as MISSING_BINDING. Read both bindings from ` +
|
|
230
|
+
`the same node, or set \`resourceKey\` to the connection's key.`,
|
|
231
|
+
where: b.id,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
20
236
|
/**
|
|
21
237
|
* Every `id` in the document must be unique — including across KINDS.
|
|
22
238
|
*
|
|
@@ -33,7 +249,14 @@ export function check(built) {
|
|
|
33
249
|
* is the only gate that can catch it, and it has to be an error.
|
|
34
250
|
*
|
|
35
251
|
* The ids in play are every element and flow, every variable and binding, the
|
|
36
|
-
* synthesized `Message_n`/`Error_n` declarations, and the process itself
|
|
252
|
+
* synthesized `Message_n`/`Error_n` declarations, and the process itself — and the
|
|
253
|
+
* declarations serialize DERIVES from output rows (a connector's `<id>_response`, a
|
|
254
|
+
* script task's `outputs`), which are variables in the document too. Those were the one
|
|
255
|
+
* kind this check could not see: a script output named after the gateway that routes
|
|
256
|
+
* on it (`outputs: { confidence }` beside `.choose('confidence', …)`) passed here and
|
|
257
|
+
* every SDK gate, and `uip maestro bpmn validate` answered with three
|
|
258
|
+
* CROSSING_SUBPROCESS_BOUNDARY errors on the gateway's flows — the validator had
|
|
259
|
+
* resolved the id to the variable. Measured on the nested_review port.
|
|
37
260
|
*/
|
|
38
261
|
function checkDocumentIds(built, diags) {
|
|
39
262
|
const owners = new Map();
|
|
@@ -52,12 +275,16 @@ function checkDocumentIds(built, diags) {
|
|
|
52
275
|
});
|
|
53
276
|
};
|
|
54
277
|
claim(built.id, 'the process');
|
|
278
|
+
const owner = (nodes, id) => nodes.find((n) => nodeDeclaredVars(n).includes(id))?.id ?? '?';
|
|
55
279
|
const walk = (nodes, flows) => {
|
|
56
280
|
for (const n of nodes) {
|
|
57
281
|
claim(n.id, `element "${n.id}"`);
|
|
58
282
|
if (n.kind === 'subProcess') {
|
|
59
283
|
for (const v of n.variables)
|
|
60
284
|
claim(v.id, `variable "${v.id}"`);
|
|
285
|
+
for (const v of implicitNodeVars(n.nodes, n.variables)) {
|
|
286
|
+
claim(v.id, `the variable serialize declares for the output row of "${owner(n.nodes, v.id)}"`);
|
|
287
|
+
}
|
|
61
288
|
walk(n.nodes, n.flows);
|
|
62
289
|
}
|
|
63
290
|
}
|
|
@@ -67,6 +294,9 @@ function checkDocumentIds(built, diags) {
|
|
|
67
294
|
walk(built.nodes, built.flows);
|
|
68
295
|
for (const v of built.variables)
|
|
69
296
|
claim(v.id, `variable "${v.id}"`);
|
|
297
|
+
for (const v of implicitNodeVars(built.nodes, built.variables)) {
|
|
298
|
+
claim(v.id, `the variable serialize declares for the output row of "${owner(built.nodes, v.id)}"`);
|
|
299
|
+
}
|
|
70
300
|
for (const b of built.bindings)
|
|
71
301
|
claim(b.id, `binding "${b.id}"`);
|
|
72
302
|
for (const mm of built.messages)
|
|
@@ -106,7 +336,7 @@ function checkScopedVars(built, diags) {
|
|
|
106
336
|
};
|
|
107
337
|
collect(built.nodes);
|
|
108
338
|
for (const v of allVars) {
|
|
109
|
-
if (v.elementId
|
|
339
|
+
if (typeof v.elementId === 'string' && !ids.has(v.elementId))
|
|
110
340
|
diags.push({
|
|
111
341
|
level: 'error',
|
|
112
342
|
code: 'SCOPED_VAR_UNKNOWN_ELEMENT',
|
|
@@ -124,6 +354,57 @@ function checkScopedVars(built, diags) {
|
|
|
124
354
|
* its own `TIMER_DURATION_WEEK_UNSUPPORTED` rule.
|
|
125
355
|
*/
|
|
126
356
|
const RUNTIME_DURATION = /^P(?:\d+D)?(?:T(?:\d+H)?(?:\d+M)?(?:\d+(?:\.\d+)?S)?)?$/i;
|
|
357
|
+
/**
|
|
358
|
+
* A spec-legal ISO-8601 duration, which is WIDER than the runtime's parser: weeks,
|
|
359
|
+
* months and years are valid here and unparseable there. The platform splits the same
|
|
360
|
+
* way — `TIMER_DURATION_INVALID` is an error against ISO-8601 (it calls luxon's
|
|
361
|
+
* `Duration.fromISO`), and `TIMER_DURATION_WEEK_UNSUPPORTED` is a separate warning — so
|
|
362
|
+
* the two tests are kept apart here for the same reason rather than collapsed.
|
|
363
|
+
*/
|
|
364
|
+
const ISO_DURATION = /^P(?!$)(?:\d+(?:\.\d+)?Y)?(?:\d+(?:\.\d+)?M)?(?:\d+(?:\.\d+)?W)?(?:\d+(?:\.\d+)?D)?(?:T(?!$)(?:\d+(?:\.\d+)?H)?(?:\d+(?:\.\d+)?M)?(?:\d+(?:\.\d+)?S)?)?$/i;
|
|
365
|
+
function isIsoDuration(v) {
|
|
366
|
+
return ISO_DURATION.test(v) && /\d/.test(v);
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* A timer value the author supplied but left blank. The platform's `isTimerValueMissing`
|
|
370
|
+
* is `value == null || value === ''`, so an EMPTY STRING is missing — which is the case
|
|
371
|
+
* `check` used to pass, because it only asked whether all three timer fields were
|
|
372
|
+
* `undefined`. `.intermediateCatchEvent('w', { timer: '' })` reached `validate`.
|
|
373
|
+
*/
|
|
374
|
+
function isBlankTimerValue(v) {
|
|
375
|
+
return v !== undefined && v.trim() === '';
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Whether an expression ASSIGNS rather than evaluates — `=vars.n = 5` where `==` was
|
|
379
|
+
* meant. A faithful port of the platform's `expressionHasAssignment`: string, template
|
|
380
|
+
* and regex literals are blanked first, then a compound assignment operator or a bare
|
|
381
|
+
* `=` that is not part of `==`, `!=`, `<=`, `>=`, `=>` or a compound operator is a hit.
|
|
382
|
+
*
|
|
383
|
+
* Scoped where the platform scopes it — flow conditions and error-mapping conditions —
|
|
384
|
+
* and reported at its severity, WARNING, because the artifact is still accepted. The
|
|
385
|
+
* point is that the author hears it at the rung they iterate on rather than the last one.
|
|
386
|
+
*/
|
|
387
|
+
const ASSIGNMENT_OP = /(\?\?=|&&=|\|\|=|>>>=|<<=|>>=|\*\*=|\+=|-=|\*=|\/=|%=|&=|\|=|\^=|(?<![!=<>+\-*/%.&|^?])=(?![=>]))/;
|
|
388
|
+
function expressionHasAssignment(expr) {
|
|
389
|
+
if (!expr)
|
|
390
|
+
return false;
|
|
391
|
+
const body = expr.startsWith('=') ? expr.slice(1) : expr;
|
|
392
|
+
if (!body.trim())
|
|
393
|
+
return false;
|
|
394
|
+
const blank = (m) => ' '.repeat(m.length);
|
|
395
|
+
const sanitized = body
|
|
396
|
+
.replace(/"(?:[^"\\]|\\.)*"/g, blank)
|
|
397
|
+
.replace(/'(?:[^'\\]|\\.)*'/g, blank)
|
|
398
|
+
.replace(/`(?:[^`\\]|\\.)*`/g, blank)
|
|
399
|
+
// A regex literal can only START where a value can start. Matching any run between
|
|
400
|
+
// two `/` blanked real code whenever one expression divided twice:
|
|
401
|
+
// `=vars.done / vars.total > 0.5 && vars.ratio = vars.x / vars.y` had everything
|
|
402
|
+
// from the first slash to the last blanked, and the stray `=` went unreported. So
|
|
403
|
+
// the opening `/` has to sit in regex-legal position — start of expression, or after
|
|
404
|
+
// an operator or opening bracket — which is never true of a division.
|
|
405
|
+
.replace(/(^|[(,=:[!&|?{};+\-*%^~]\s*)(\/(?:[^/\\\n]|\\.)*\/[gimsuy]*)/g, (_m, pre, lit) => pre + blank(lit));
|
|
406
|
+
return ASSIGNMENT_OP.test(sanitized);
|
|
407
|
+
}
|
|
127
408
|
function isRuntimeDuration(v) {
|
|
128
409
|
// A bare `P`/`PT` matches the shape but carries no components — the same
|
|
129
410
|
// "validates, then no runtime can parse it" trap the timer eval card exists for.
|
|
@@ -181,6 +462,18 @@ function checkActivityFields(n, diags) {
|
|
|
181
462
|
where: n.id,
|
|
182
463
|
});
|
|
183
464
|
}
|
|
465
|
+
// An error-mapping condition is the platform's other ASSIGNMENT_NOT_ALLOWED site.
|
|
466
|
+
const mappings = 'errorMapping' in n ? (n.errorMapping ?? []) : [];
|
|
467
|
+
for (const row of mappings) {
|
|
468
|
+
if (expressionHasAssignment(row.condition))
|
|
469
|
+
diags.push({
|
|
470
|
+
level: 'warning',
|
|
471
|
+
code: 'ASSIGNMENT_NOT_ALLOWED',
|
|
472
|
+
message: `Error-mapping condition on "${n.id}" (code "${row.code}") contains an assignment operator; it must only ` +
|
|
473
|
+
`evaluate. \`=\` assigns and \`==\` compares.`,
|
|
474
|
+
where: n.id,
|
|
475
|
+
});
|
|
476
|
+
}
|
|
184
477
|
const loop = 'loop' in n ? n.loop : undefined;
|
|
185
478
|
if (loop) {
|
|
186
479
|
if (!loop.collection)
|
|
@@ -224,12 +517,66 @@ inEventSubProcess = false) {
|
|
|
224
517
|
// NO_START on a process that has one, refusing an artifact the platform accepts.
|
|
225
518
|
if (nodes.length && !nodes.some(isStartElement))
|
|
226
519
|
diags.push({ level: isTop ? 'error' : 'warning', code: 'NO_START', message: `${where} has no start event.`, where });
|
|
520
|
+
// At most one BLANK start event per scope — one with no event definition and no
|
|
521
|
+
// trigger, the "start this manually" event. The platform counts them per process and
|
|
522
|
+
// per sub-process (`validateSingleBlankStartEvent`) and errors on a second; a
|
|
523
|
+
// `sequence` scope refuses one at build, but an explicit-mode scope did not, so the
|
|
524
|
+
// shape reached `validate`.
|
|
525
|
+
const blankStarts = nodes.filter((n) => n.kind === 'startEvent' && n.event === undefined && n.trigger === undefined);
|
|
526
|
+
if (blankStarts.length > 1)
|
|
527
|
+
diags.push({
|
|
528
|
+
level: 'error',
|
|
529
|
+
code: 'MULTIPLE_BLANK_START_EVENTS',
|
|
530
|
+
message: `${where} has ${blankStarts.length} blank start events (${blankStarts.map((n) => `"${n.id}"`).join(', ')}); ` +
|
|
531
|
+
`a scope may have at most one. Give the others an event definition, or merge the paths.`,
|
|
532
|
+
where: blankStarts[1].id,
|
|
533
|
+
});
|
|
227
534
|
// Flow endpoints must resolve within this scope.
|
|
535
|
+
const byId = new Map(nodes.map((n) => [n.id, n]));
|
|
228
536
|
for (const f of flows) {
|
|
229
537
|
if (!nodeIds.has(f.source))
|
|
230
538
|
diags.push({ level: 'error', code: 'DANGLING_FLOW', message: `Flow "${f.id}" source "${f.source}" is not an element in ${where}.`, where: f.id });
|
|
231
539
|
if (!nodeIds.has(f.target))
|
|
232
540
|
diags.push({ level: 'error', code: 'DANGLING_FLOW', message: `Flow "${f.id}" target "${f.target}" is not an element in ${where}.`, where: f.id });
|
|
541
|
+
// A flow from an element to itself. The platform reports it as INVALID_CONNECTION;
|
|
542
|
+
// an error here, to match DANGLING_FLOW, which is the same class of broken edge.
|
|
543
|
+
if (f.source === f.target)
|
|
544
|
+
diags.push({
|
|
545
|
+
level: 'error',
|
|
546
|
+
code: 'SELF_FLOW',
|
|
547
|
+
message: `Flow "${f.id}" leaves "${f.source}" and arrives at the same element; a loop needs a gateway or \`loop\`.`,
|
|
548
|
+
where: f.id,
|
|
549
|
+
});
|
|
550
|
+
// Nothing flows INTO a start event or OUT of an end event. Both are structural
|
|
551
|
+
// facts of BPMN, both are reachable through an explicit `.sequenceFlow()` — a
|
|
552
|
+
// `sequence` scope refuses them when it places the node, an explicit scope did not —
|
|
553
|
+
// and the platform reports each as INVALID_CONNECTION_TYPE. The rest of its
|
|
554
|
+
// type-compatibility matrix is NOT ported: these two are the cases an author
|
|
555
|
+
// actually writes by mistake.
|
|
556
|
+
const target = byId.get(f.target);
|
|
557
|
+
if (target !== undefined && isStartElement(target))
|
|
558
|
+
diags.push({
|
|
559
|
+
level: 'error',
|
|
560
|
+
code: 'FLOW_INTO_START',
|
|
561
|
+
message: `Flow "${f.id}" arrives at start event "${f.target}"; a start event has no incoming flow.`,
|
|
562
|
+
where: f.id,
|
|
563
|
+
});
|
|
564
|
+
const source = byId.get(f.source);
|
|
565
|
+
if (source !== undefined && source.kind === 'endEvent')
|
|
566
|
+
diags.push({
|
|
567
|
+
level: 'error',
|
|
568
|
+
code: 'FLOW_OUT_OF_END',
|
|
569
|
+
message: `Flow "${f.id}" leaves end event "${f.source}"; an end event ends its path.`,
|
|
570
|
+
where: f.id,
|
|
571
|
+
});
|
|
572
|
+
if (expressionHasAssignment(f.condition))
|
|
573
|
+
diags.push({
|
|
574
|
+
level: 'warning',
|
|
575
|
+
code: 'ASSIGNMENT_NOT_ALLOWED',
|
|
576
|
+
message: `Condition on flow "${f.id}" contains an assignment operator; it must only evaluate. ` +
|
|
577
|
+
`\`=\` assigns and \`==\` compares.`,
|
|
578
|
+
where: f.id,
|
|
579
|
+
});
|
|
233
580
|
}
|
|
234
581
|
const outgoing = new Map();
|
|
235
582
|
const incomingCount = new Map();
|
|
@@ -259,11 +606,55 @@ inEventSubProcess = false) {
|
|
|
259
606
|
for (const o of outs)
|
|
260
607
|
if (o.id !== n.default && o.condition === undefined)
|
|
261
608
|
diags.push({ level, code: 'MISSING_CONDITION_EXPRESSION', message: `Outgoing flow "${o.id}" of gateway "${n.id}" has no condition.`, where: o.id });
|
|
609
|
+
// Every branch conditioned and no default: when no condition holds the token
|
|
610
|
+
// has nowhere to go, and the instance stalls at the gateway. A WARNING, not an
|
|
611
|
+
// error: the platform has no rule for it (`uip maestro bpmn validate` says Valid),
|
|
612
|
+
// and the runtime is what discovers it — as the diagnose evals' "stuck gateway"
|
|
613
|
+
// does. `.choose()` with an `otherwise` arm cannot produce this shape.
|
|
614
|
+
if (n.kind === 'exclusiveGateway' && n.default === undefined && outs.every((o) => o.condition !== undefined))
|
|
615
|
+
diags.push({
|
|
616
|
+
level: 'warning',
|
|
617
|
+
code: 'NO_DEFAULT_FLOW',
|
|
618
|
+
message: `Gateway "${n.id}" has no default flow: when none of its ${outs.length} conditions holds, the instance ` +
|
|
619
|
+
`stalls there. Mark one flow as the default, or add an \`otherwise\` arm.`,
|
|
620
|
+
where: n.id,
|
|
621
|
+
});
|
|
262
622
|
}
|
|
263
623
|
}
|
|
264
|
-
// A gateway with exactly one in and one out is superfluous.
|
|
265
|
-
|
|
266
|
-
|
|
624
|
+
// A gateway with exactly one in and one out is superfluous. An ERROR, and scoped to
|
|
625
|
+
// the three kinds the platform's rule covers: `validateSuperfluousGateway` matches
|
|
626
|
+
// ExclusiveGateway / ParallelGateway / InclusiveGateway at severity ERROR, so a
|
|
627
|
+
// warning here told the author something the last rung would refuse — measured as a
|
|
628
|
+
// `validate`-only row in the conformance ledger even though the code existed.
|
|
629
|
+
// An event-based gateway is outside the platform's rule and stays a warning.
|
|
630
|
+
if (GATEWAY_KINDS.has(n.kind) && ins === 1 && outs.length === 1) {
|
|
631
|
+
const platformRule = n.kind !== 'eventBasedGateway';
|
|
632
|
+
diags.push({
|
|
633
|
+
level: platformRule ? 'error' : 'warning',
|
|
634
|
+
code: 'SUPERFLUOUS_GATEWAY',
|
|
635
|
+
message: `Gateway "${n.id}" has one incoming and one outgoing flow, so it decides nothing; remove it and wire ` +
|
|
636
|
+
`its source straight to its target.`,
|
|
637
|
+
where: n.id,
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
// The only outgoing flow carries a condition, so a false condition strands the token
|
|
641
|
+
// with nowhere to go. The platform's rule is on ANY node — a task with one conditional
|
|
642
|
+
// outgoing flow trips it too, not just a gateway — minus parallel and event-based
|
|
643
|
+
// gateways, and minus the exclusive/inclusive gateways the superfluous rule above
|
|
644
|
+
// already owns (one in, one out), so the two rules do not both fire on one element.
|
|
645
|
+
if (n.kind !== 'parallelGateway' && n.kind !== 'eventBasedGateway' && outs.length === 1) {
|
|
646
|
+
const ownedBySuperfluous = (n.kind === 'exclusiveGateway' || n.kind === 'inclusiveGateway') && ins === 1;
|
|
647
|
+
const only = outs[0];
|
|
648
|
+
const defaultFlowId = 'default' in n ? n.default : undefined;
|
|
649
|
+
if (!ownedBySuperfluous && only.condition !== undefined && only.id !== defaultFlowId)
|
|
650
|
+
diags.push({
|
|
651
|
+
level: 'error',
|
|
652
|
+
code: 'SINGLE_CONDITIONAL_OUTGOING_FLOW',
|
|
653
|
+
message: `The only outgoing flow of "${n.id}" ("${only.id}") carries a condition, so the instance strands there ` +
|
|
654
|
+
`whenever it is false. Drop the condition, mark the flow as the default, or add the other branch.`,
|
|
655
|
+
where: only.id,
|
|
656
|
+
});
|
|
657
|
+
}
|
|
267
658
|
// An activity or event with >1 incoming flow is an implicit join. A WARNING,
|
|
268
659
|
// not an error: the platform's FAKE_JOIN rule only matches the abstract types
|
|
269
660
|
// `bpmn:Activity`/`bpmn:Event`, which never appear on real exported BPMN, so
|
|
@@ -276,6 +667,42 @@ inEventSubProcess = false) {
|
|
|
276
667
|
message: `"${n.id}" has ${ins} incoming flows; the platform accepts this, but joining with a gateway is clearer.`,
|
|
277
668
|
where: n.id,
|
|
278
669
|
});
|
|
670
|
+
// The mirror of FAKE_JOIN: an activity or event with more than one outgoing
|
|
671
|
+
// flow and no condition on any of them is an implicit PARALLEL SPLIT — every
|
|
672
|
+
// branch runs. A warning for the same reason: the platform accepts it.
|
|
673
|
+
//
|
|
674
|
+
// Worth saying because of how easily the nesting sugar produces one by accident.
|
|
675
|
+
// An explicit `.sequenceFlow()` off an element the builder has ALREADY implied a
|
|
676
|
+
// flow out of adds a second edge rather than replacing the first, so
|
|
677
|
+
// `.task('a').endEvent('done').sequenceFlow('a', 'failed')` fans out where the
|
|
678
|
+
// author meant to redirect. Writing the explicit flow BEFORE the next element
|
|
679
|
+
// takes the path off the books instead (see `sequenceFlow`), so the two orders
|
|
680
|
+
// no longer differ silently — this is what reports the one that cannot be caught
|
|
681
|
+
// at the call site.
|
|
682
|
+
const unconditioned = outs.filter((f) => f.condition === undefined);
|
|
683
|
+
if (!GATEWAY_KINDS.has(n.kind) && n.kind !== 'boundaryEvent' && unconditioned.length > 1)
|
|
684
|
+
diags.push({
|
|
685
|
+
level: 'warning',
|
|
686
|
+
code: 'IMPLICIT_SPLIT',
|
|
687
|
+
message: `"${n.id}" has ${unconditioned.length} outgoing flows and no condition on any of them ` +
|
|
688
|
+
`(${unconditioned.map((f) => `"${f.target}"`).join(', ')}), so every one of them runs. ` +
|
|
689
|
+
`If that is meant, split with .fork(); if one was meant to replace another, write the ` +
|
|
690
|
+
`.sequenceFlow() before the element that follows.`,
|
|
691
|
+
where: n.id,
|
|
692
|
+
});
|
|
693
|
+
// An element nothing leaves, that is not an end. The token arriving there has
|
|
694
|
+
// nowhere to go, and the instance sits at that element for ever. A WARNING: the
|
|
695
|
+
// platform has no rule for it (`uip maestro bpmn validate` says Valid), and a
|
|
696
|
+
// process under construction legitimately has loose ends. It is here because
|
|
697
|
+
// `bpmn check --graph` showed one in a shipped example that every other gate had
|
|
698
|
+
// passed — an activity whose only exit was through its error handler.
|
|
699
|
+
if (outs.length === 0 && n.kind !== 'endEvent' && !(n.kind === 'subProcess' && n.triggeredByEvent === true))
|
|
700
|
+
diags.push({
|
|
701
|
+
level: 'warning',
|
|
702
|
+
code: 'DEAD_END',
|
|
703
|
+
message: `"${n.id}" has no outgoing flow, so a token reaching it stops there. End the path with an end event, or wire it on.`,
|
|
704
|
+
where: n.id,
|
|
705
|
+
});
|
|
279
706
|
// Boundary events must attach to an activity in the same scope.
|
|
280
707
|
if (n.kind === 'boundaryEvent') {
|
|
281
708
|
const target = nodes.find((x) => x.id === n.attachedTo);
|
|
@@ -301,8 +728,39 @@ inEventSubProcess = false) {
|
|
|
301
728
|
}
|
|
302
729
|
if (n.kind !== 'subProcess' && 'event' in n && n.event?.type === 'timer') {
|
|
303
730
|
const t = n.event.timer;
|
|
731
|
+
const blank = [t.duration, t.date, t.cycle].find(isBlankTimerValue) !== undefined;
|
|
304
732
|
if (t.duration === undefined && t.date === undefined && t.cycle === undefined)
|
|
305
733
|
diags.push({ level: 'error', code: 'TIMER_MISSING_FIELD', message: `Timer on "${n.id}" needs a duration, date, or cycle.`, where: n.id });
|
|
734
|
+
else if (blank)
|
|
735
|
+
// The field is present and empty, which the platform reports as
|
|
736
|
+
// TIMER_MISSING_REQUIRED_FIELD. Same diagnosis, one rung earlier.
|
|
737
|
+
diags.push({
|
|
738
|
+
level: 'error',
|
|
739
|
+
code: 'TIMER_MISSING_FIELD',
|
|
740
|
+
message: `Timer on "${n.id}" declares a field with an empty value; give it a duration, date, or cycle.`,
|
|
741
|
+
where: n.id,
|
|
742
|
+
});
|
|
743
|
+
// An expression or a binding reference is resolved at run time, so its shape is
|
|
744
|
+
// not ours to judge — the platform skips `=`/`@` values for the same reason.
|
|
745
|
+
const d = t.duration;
|
|
746
|
+
if (d !== undefined && d.trim() !== '' && !d.startsWith('=') && !d.startsWith('@')) {
|
|
747
|
+
if (!isIsoDuration(d))
|
|
748
|
+
diags.push({
|
|
749
|
+
level: 'error',
|
|
750
|
+
code: 'TIMER_DURATION_INVALID',
|
|
751
|
+
message: `Timer duration "${d}" on "${n.id}" is not an ISO-8601 duration, e.g. "PT30S" or "P1DT12H".`,
|
|
752
|
+
where: n.id,
|
|
753
|
+
});
|
|
754
|
+
else if (!isRuntimeDuration(d))
|
|
755
|
+
diags.push({
|
|
756
|
+
level: 'warning',
|
|
757
|
+
code: 'TIMER_DURATION_NOT_RUNTIME_PARSEABLE',
|
|
758
|
+
message: `Timer duration "${d}" on "${n.id}" is valid ISO-8601 but the runtime parses days/hours/minutes/seconds ` +
|
|
759
|
+
`only, so a week, month or year designator is accepted by validate and then fails at run time. ` +
|
|
760
|
+
`Write it in days, e.g. "P7D" for a week.`,
|
|
761
|
+
where: n.id,
|
|
762
|
+
});
|
|
763
|
+
}
|
|
306
764
|
}
|
|
307
765
|
// Retry / loop options on any activity.
|
|
308
766
|
checkActivityFields(n, diags);
|
|
@@ -320,7 +778,7 @@ inEventSubProcess = false) {
|
|
|
320
778
|
const contextView = { ...n.context };
|
|
321
779
|
for (const row of n.contextRows ?? [])
|
|
322
780
|
contextView[row.name] = row.value ?? row.body ?? '';
|
|
323
|
-
for (const field of bindingRequiredFields(n.type)) {
|
|
781
|
+
for (const field of bindingRequiredFields(n.type, n.spec)) {
|
|
324
782
|
const value = contextView[field.name];
|
|
325
783
|
if (typeof value === 'string' && value.startsWith('=bindings.'))
|
|
326
784
|
continue;
|
|
@@ -333,6 +791,59 @@ inEventSubProcess = false) {
|
|
|
333
791
|
where: n.id,
|
|
334
792
|
});
|
|
335
793
|
}
|
|
794
|
+
// A human task's own timer, which the platform bounds to 15 minutes … 90 days.
|
|
795
|
+
// Note the units and the type: this is a NUMBER OF MINUTES on an `actions` node,
|
|
796
|
+
// not an ISO duration and not a boundary timer, and an `=`/`@` value is a runtime
|
|
797
|
+
// expression the rule skips. Warning, as the platform reports it.
|
|
798
|
+
if (n.type === 'actions') {
|
|
799
|
+
const raw = contextView['Timer'];
|
|
800
|
+
const str = raw === undefined || raw === null ? '' : String(raw);
|
|
801
|
+
const minutes = Number(str);
|
|
802
|
+
if (str !== '' && !str.startsWith('=') && !str.startsWith('@') && !Number.isNaN(minutes)) {
|
|
803
|
+
if (minutes < MIN_TASK_TIMER_MINUTES || minutes > MAX_TASK_TIMER_MINUTES)
|
|
804
|
+
diags.push({
|
|
805
|
+
level: 'warning',
|
|
806
|
+
code: 'TASK_TIMER_OUT_OF_RANGE',
|
|
807
|
+
message: `"${n.id}" sets Timer to ${str}, outside the 15 minutes to 90 days the platform accepts. ` +
|
|
808
|
+
`The value is a NUMBER OF MINUTES — "${MIN_TASK_TIMER_MINUTES}" is the shortest, ` +
|
|
809
|
+
`"${MAX_TASK_TIMER_MINUTES}" the longest — not an ISO duration.`,
|
|
810
|
+
where: n.id,
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
// A service task of a type that starts or enqueues something, with nothing
|
|
815
|
+
// naming WHAT. The platform reports `MISSING_RESOURCE` at WARNING, so it does
|
|
816
|
+
// not gate — and a warning at the last rung is the weakest signal there is. It
|
|
817
|
+
// is a warning here too: the platform accepts the process, so an error would be
|
|
818
|
+
// the SDK refusing what deploys.
|
|
819
|
+
if (SERVICE_TYPES_REQUIRING_RESOURCE.has(n.type)) {
|
|
820
|
+
const quickHitl = n.type === 'Actions.HITL' &&
|
|
821
|
+
(contextView['hitlType'] === 'quick' ||
|
|
822
|
+
(contextView['hitlType'] !== 'custom' && Object.keys(contextView).some((k) => HITL_QUICK_SCHEMA_CONTEXT.has(k))));
|
|
823
|
+
// `appId` is a deliberate, narrow LOOSENING of the platform's list, and the one
|
|
824
|
+
// place this rule does not mirror it. The canvas rule reads only
|
|
825
|
+
// `releaseKey`/`name`/`queueName`, so every `Actions.HITL` node — including
|
|
826
|
+
// every one `.humanTask()` itself produces, which writes the app as `appId` —
|
|
827
|
+
// trips it. Warning on a shape the SDK's own wrapper emits, about a field that
|
|
828
|
+
// is not how an Action app is addressed, is noise the author cannot act on.
|
|
829
|
+
// Safe in this direction: the platform reports MISSING_RESOURCE at WARNING, so
|
|
830
|
+
// staying quiet cannot turn an accepted artifact into a rejected one. The
|
|
831
|
+
// reverse — reporting more than the platform — is what this file forbids.
|
|
832
|
+
const resourceFields = n.type === 'Actions.HITL' ? ['releaseKey', 'name', 'queueName', 'appId'] : ['releaseKey', 'name', 'queueName'];
|
|
833
|
+
const bound = resourceFields.some((f) => {
|
|
834
|
+
const v = contextView[f];
|
|
835
|
+
return v !== undefined && v !== null && String(v) !== '';
|
|
836
|
+
});
|
|
837
|
+
if (!quickHitl && !bound)
|
|
838
|
+
diags.push({
|
|
839
|
+
level: 'warning',
|
|
840
|
+
code: 'MISSING_RESOURCE',
|
|
841
|
+
message: `"${n.id}" (${n.type}) names no resource, so nothing says which process or queue it acts on. ` +
|
|
842
|
+
`Set "releaseKey", or "name"/"queueName" — normally as a binding reference, e.g. ` +
|
|
843
|
+
`\`name: '=bindings.MyProcess'\`.`,
|
|
844
|
+
where: n.id,
|
|
845
|
+
});
|
|
846
|
+
}
|
|
336
847
|
// Context reaches a node two ways — the `context` record, or the verbatim
|
|
337
848
|
// `contextRows` an importer uses when a row carries detail the record cannot
|
|
338
849
|
// hold (its own type, a `target`, or the absence of a value). Both are context,
|
|
@@ -344,7 +855,7 @@ inEventSubProcess = false) {
|
|
|
344
855
|
// emits, so whether this fired depended on a detail of the source document that
|
|
345
856
|
// has nothing to do with whether the field is set.
|
|
346
857
|
const contextNames = contextView;
|
|
347
|
-
for (const field of missingRequiredContext(n.type, contextNames)) {
|
|
858
|
+
for (const field of missingRequiredContext(n.type, contextNames, n.spec)) {
|
|
348
859
|
diags.push({
|
|
349
860
|
level: 'error',
|
|
350
861
|
code: 'EMPTY_REQUIRED_FIELD',
|
|
@@ -354,7 +865,12 @@ inEventSubProcess = false) {
|
|
|
354
865
|
}
|
|
355
866
|
// The same rule over the PAYLOAD fields — the platform's RequiredFieldsRule
|
|
356
867
|
// draws on both, so an unset required input is the same EMPTY_REQUIRED_FIELD.
|
|
357
|
-
|
|
868
|
+
// The payload reaches a node the same two ways — `inputs`, or the verbatim
|
|
869
|
+
// `inputRows` — and both satisfy a required field.
|
|
870
|
+
const inputView = { ...n.inputs };
|
|
871
|
+
for (const row of n.inputRows ?? [])
|
|
872
|
+
inputView[row.name] = row.value ?? row.body ?? '';
|
|
873
|
+
for (const field of missingRequiredInputs(n.type, inputView, contextNames, n.spec)) {
|
|
358
874
|
diags.push({
|
|
359
875
|
level: 'error',
|
|
360
876
|
code: 'EMPTY_REQUIRED_FIELD',
|
|
@@ -370,7 +886,7 @@ inEventSubProcess = false) {
|
|
|
370
886
|
code: 'OUTPUT_ROWS_FIXED',
|
|
371
887
|
message: `"${n.id}" (${n.type}) permits only its declared output row, so the extra row(s) ` +
|
|
372
888
|
`${Object.keys(n.outputs ?? {}).map((k) => `"${k}"`).join(', ')} will be refused ` +
|
|
373
|
-
`("output name must be \\"${registryType(n.type).output?.name}\\""). Use outputVar and read the ` +
|
|
889
|
+
`("output name must be \\"${registryType(n.type, n.spec).output?.name}\\""). Use outputVar and read the ` +
|
|
374
890
|
`typed row instead.`,
|
|
375
891
|
where: n.id,
|
|
376
892
|
});
|
|
@@ -379,13 +895,13 @@ inEventSubProcess = false) {
|
|
|
379
895
|
// not error: the node is as correct as the registry allows, so refusing it
|
|
380
896
|
// would block work the platform, not the author, has to fix. Saying nothing
|
|
381
897
|
// is worse — the artifact validates here and is rejected by the CLI.
|
|
382
|
-
const unsatisfiable = unsatisfiableRequiredInputs(n.type);
|
|
898
|
+
const unsatisfiable = unsatisfiableRequiredInputs(n.type, n.spec);
|
|
383
899
|
if (unsatisfiable.length) {
|
|
384
900
|
diags.push({
|
|
385
901
|
level: 'warning',
|
|
386
902
|
code: 'REQUIRED_INPUT_UNSATISFIABLE',
|
|
387
903
|
message: `"${n.id}" (${n.type}) declares required input(s) ${unsatisfiable.map((f) => `"${f}"`).join(', ')}, ` +
|
|
388
|
-
`but its "${registryType(n.type).inputPattern}" pattern merges every input into one row, so the ` +
|
|
904
|
+
`but its "${registryType(n.type, n.spec).inputPattern}" pattern merges every input into one row, so the ` +
|
|
389
905
|
`platform's required-field rule cannot see them. \`uip maestro bpmn validate\` will report ` +
|
|
390
906
|
`EMPTY_REQUIRED_FIELD whatever this node carries — a platform defect, not an authoring error. ` +
|
|
391
907
|
`See docs/BPMN_COVERAGE_DESIGN.md D13.`,
|
|
@@ -417,7 +933,7 @@ function isStartElement(n) {
|
|
|
417
933
|
if (n.kind !== 'typed')
|
|
418
934
|
return false;
|
|
419
935
|
try {
|
|
420
|
-
return registryType(n.type).element === 'bpmn:StartEvent';
|
|
936
|
+
return registryType(n.type, n.spec).element === 'bpmn:StartEvent';
|
|
421
937
|
}
|
|
422
938
|
catch {
|
|
423
939
|
return false;
|