@uipath/maestro-builder-sdk 6.24.0 → 6.25.1

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.
@@ -0,0 +1,601 @@
1
+ /**
2
+ * The four human-task node types: the base `uipath.human-in-the-loop` (1.0, 1.1,
3
+ * 1.2) and its `.quick-form` (1.0, 1.1), `.coded-action-app` (1.0, 1.1) and
4
+ * `.document-validation` (1.0) variants. They share the `hitl` kind; `variant`
5
+ * picks one (`matches`), and the base node's inputs pick its version (`select`).
6
+ * Moved from serialize.ts / check.ts / decompile.ts in Phase 4 of
7
+ * docs/NODE_TYPE_ARCHITECTURE_DESIGN.md; serialized output, printed source,
8
+ * codes and messages unchanged.
9
+ *
10
+ * How a task LEAVES its node is `core/hitl-routing.ts`'s `hitlExitModel`, which
11
+ * the pipeline's flow-level routing rules read too: its tails here, its exits
12
+ * for `.stepSwitch`, and the edge rule (`hitlExitViolation`) serialize holds
13
+ * every edge out of an outcome-handle node to.
14
+ */
15
+ import { Expr, toExpr } from '../../../core/expr.js';
16
+ import { FORM_FIELD_JSON_TYPE, sdkFormFieldType } from '../../../core/form-field-type.js';
17
+ import { isAnswerField } from '../../../core/hitl-answers.js';
18
+ import { OUTCOME_HANDLE_NODE_TYPES, hitlExitModel, outcomeIdOf, outcomeSlug } from '../../../core/hitl-routing.js';
19
+ import { jsonClone } from '../../../core/json-file.js';
20
+ import { ownEntry } from '../../../core/own-entry.js';
21
+ import { schemaId } from '../../../core/stable-id.js';
22
+ import { wireExprBody } from '../../../core/wire-expr.js';
23
+ import { checkHitl, checkHitlRead } from './check.js';
24
+ const BASE = 'uipath.human-in-the-loop';
25
+ const QUICK_FORM = 'uipath.human-in-the-loop.quick-form';
26
+ const ACTION_APP = 'uipath.human-in-the-loop.coded-action-app';
27
+ const DOC_VALIDATION = 'uipath.human-in-the-loop.document-validation';
28
+ const VARIANTS = ['quick-form', 'action-app', 'document-validation'];
29
+ /**
30
+ * A human task's inputs, and the outputs its form implies.
31
+ *
32
+ * Three things the author does not state and the platform needs:
33
+ *
34
+ * 1. **`type: 'quick'`** — the node type carries both an inline quick form and an
35
+ * App-backed task, selected by this field. flow-v1's serializer defaults an
36
+ * absent value to `quick`, but a reader cannot rely on that default, so the
37
+ * serializer writes it (the same "the encoding is the serializer's job" call as
38
+ * the scheduled trigger's `timerPreset`/`timerValue` split). **Only on the base
39
+ * node type**: on either suffixed variant the node TYPE is the choice, so no
40
+ * `inputs.type` is written — 165 of the 175 deployed `.quick-form` instances
41
+ * omit it, neither variant's definition declares it, and flow-v1's serializer
42
+ * DERIVES its `hitlType` from the node type rather than reading the field
43
+ * (`hitl-serialization.ts:140-146`).
44
+ * 2. **`recipient`** — who gets the task. Emitted from the definition's own
45
+ * `inputDefaults`: Email + Action Center, assigned to a group. That is what
46
+ * 203 of 233 deployed, debug-green v1 human tasks carry, unchanged.
47
+ * 3. **`schema.schemaId`** — the stable id the backend caches the form under
48
+ * (229 of those 233 flows have one). Derived from the flow and node ids so the
49
+ * emitted `.flow` is byte-stable across builds.
50
+ *
51
+ * And the outputs, which the DEFINITION cannot declare: a quick form's result keys
52
+ * are its own answer fields, so `uipath.human-in-the-loop@1.0` says only
53
+ * `output: { type: 'object' }` and the instance carries the shape — `properties`
54
+ * per answer field plus `Action` enumerated over the outcome names, and `status`
55
+ * (the platform's `=result.Action` projection) enumerated the same way. This is
56
+ * exactly what deployed v1 flows carry, and it is what lets `gen-tests` synthesize
57
+ * a replay fixture a downstream read can actually use.
58
+ */
59
+ function hitlInputs(inputs, nodeType, ctx) {
60
+ const isApp = nodeType === ACTION_APP;
61
+ const fields = (inputs?.fields ?? []).map((f) => {
62
+ const base = {
63
+ id: f.id,
64
+ label: f.label ?? f.id,
65
+ type: f.type,
66
+ direction: f.direction,
67
+ };
68
+ if (f.direction === 'input') {
69
+ // The platform reads a field's `binding` as a variable PATH (`vars.…`),
70
+ // not as an expression — so the Expr's `$vars.x` form is rewritten here
71
+ // rather than asked of the author.
72
+ base.binding = bindingPath(f.value, ctx);
73
+ }
74
+ else if (f.direction === 'inOut') {
75
+ // Pre-filled AND editable: `binding` is the canonical pre-fill (the data
76
+ // contract flow-v1's serializer reads), and the WRITE target is the field
77
+ // id itself — deliberately decoupled from the binding, so no `variable`.
78
+ base.binding = bindingPath(f.value, ctx);
79
+ if (f.required)
80
+ base.required = true;
81
+ }
82
+ else {
83
+ // The answer's landing variable. The platform falls back to the field id
84
+ // when this is absent; naming it keeps the emitted flow explicit and
85
+ // matches what the designer writes.
86
+ base.variable = `vars.${f.id}`;
87
+ if (f.required)
88
+ base.required = true;
89
+ }
90
+ return base;
91
+ });
92
+ const outcomes = (inputs?.outcomes ?? []).map((o, i) => {
93
+ const name = typeof o === 'string' ? o : o?.name;
94
+ const action = typeof o === 'string' ? 'Continue' : (o?.action ?? 'Continue');
95
+ return { id: outcomeIdOf(o, i), name, type: 'string', isPrimary: i === 0, action };
96
+ });
97
+ const properties = {};
98
+ for (const f of inputs?.fields ?? []) {
99
+ // inOut fields land in the answers too — their reviewed value flows back.
100
+ if (!isAnswerField(f))
101
+ continue;
102
+ properties[f.id] = { ...ownEntry(FORM_FIELD_JSON_TYPE, String(f.type)) };
103
+ }
104
+ const names = outcomes.map((o) => o.name).filter((n) => typeof n === 'string');
105
+ if (names.length > 0) {
106
+ properties.Action = { type: 'string', enum: names, default: names[0] };
107
+ }
108
+ // The app reference and its input map, for the `action-app` variant only. The
109
+ // keys are flow-v1's own app-picker names (`name`/`key`/`folderPath`, plus the
110
+ // `appSystemName` that stays null until an app's first deployment); the
111
+ // bindings map the app's OWN parameter names to `=`-prefixed expressions,
112
+ // which is the form v1's serializer turns into `HitlTaskArguments`.
113
+ const app = inputs?.app;
114
+ const appInputBindings = {};
115
+ for (const [name, value] of Object.entries(app?.inputs ?? {})) {
116
+ appInputBindings[name] = appBindingExpr(value, ctx);
117
+ }
118
+ // The author's recipient when given; the definition's own default otherwise
119
+ // (unchanged — that default is what 203 of 233 deployed v1 tasks carry).
120
+ // `1.0`: every human-task type's first bundled definition, which is the one
121
+ // the pipeline read this default from before the port (`defByType`).
122
+ const defaultRecipient = jsonClone(ctx.definition(nodeType, '1.0')?.inputDefaults?.recipient ?? {});
123
+ const r = inputs?.recipient;
124
+ const recipient = r
125
+ ? {
126
+ channels: r.channels ?? (defaultRecipient.channels ?? []),
127
+ // Preserve the default's shape: quick-form's default carries an empty
128
+ // `connections` map, the others none — keep whichever, unless the
129
+ // author supplies their own.
130
+ ...(r.connections !== undefined
131
+ ? { connections: r.connections }
132
+ : 'connections' in defaultRecipient ? { connections: defaultRecipient.connections } : {}),
133
+ assignee: {
134
+ type: r.assignee.type,
135
+ ...(r.assignee.value !== undefined ? { value: r.assignee.value } : {}),
136
+ ...(r.assignee.displayName !== undefined ? { displayName: r.assignee.displayName } : {}),
137
+ },
138
+ }
139
+ : defaultRecipient;
140
+ // Document Validation carries the station's own inputs instead of a form; the
141
+ // expressions ride as `=js:` strings, EV-wrapped by the profile migration like
142
+ // any other input (the family is not in UNWRAPPED_INPUTS).
143
+ // The station's expression fields ride as PLAIN `$vars.…` strings — the
144
+ // property panel's own placeholder shape, and what the platform validator
145
+ // reads (it string-checks them directly; an ExpressionValue envelope fails
146
+ // with "extractionResult?.trim is not a function").
147
+ const docPath = (v) => {
148
+ const e = toExpr(v);
149
+ return e.literal ? String(e.value) : ctx.renameRefs(e.js);
150
+ };
151
+ const doc = inputs?.document;
152
+ const docvalInputs = nodeType === DOC_VALIDATION && doc
153
+ ? {
154
+ validationRenderType: doc.render ?? 'standard',
155
+ extractionResult: docPath(doc.extractionResult),
156
+ ...(doc.storageBucket !== undefined ? { storageBucket: doc.storageBucket } : {}),
157
+ ...(doc.documentId !== undefined ? { documentId: doc.documentId } : {}),
158
+ ...(doc.taxonomy !== undefined ? { taxonomy: docPath(doc.taxonomy) } : {}),
159
+ }
160
+ : {};
161
+ // 1.2's error output, verbatim from the bundled definition — the instance
162
+ // outputs replace the definition's (see the caller), so the slot is re-added
163
+ // here when the author asked for it.
164
+ const errorOutput = inputs?.exposeError
165
+ ? (ctx.definition(BASE, '1.2')?.outputDefinition)?.error
166
+ : undefined;
167
+ return {
168
+ inputs: {
169
+ // The base node type alone needs the discriminator — see the doc block.
170
+ ...(nodeType === BASE ? { type: 'quick' } : {}),
171
+ title: inputs?.title ?? ctx.step,
172
+ priority: inputs?.priority ?? 'Low',
173
+ ...(inputs?.labels !== undefined ? { labels: inputs.labels } : {}),
174
+ recipient,
175
+ ...docvalInputs,
176
+ ...((isApp || (nodeType === DOC_VALIDATION && doc?.render === 'custom')) && app
177
+ ? {
178
+ app: {
179
+ displayName: app.name,
180
+ name: app.name,
181
+ key: app.key,
182
+ folderPath: app.folderPath,
183
+ ...(app.appSystemName !== undefined ? { appSystemName: app.appSystemName } : {}),
184
+ },
185
+ ...(Object.keys(appInputBindings).length > 0 ? { appInputBindings } : {}),
186
+ }
187
+ : {}),
188
+ // The app variant still carries a `schema` — with NO fields (the app owns
189
+ // the form) but WITH the outcomes, because the flow's own routing and its
190
+ // `Action`/`status` outputs are derived from them. Both of flow-v1's
191
+ // app-task node references write exactly this shape.
192
+ schema: { schemaId: schemaId(ctx.flowId, ctx.nodeId), fields, outcomes },
193
+ },
194
+ outputs: {
195
+ output: {
196
+ type: 'object',
197
+ description: 'Task result data',
198
+ source: '=result',
199
+ var: 'output',
200
+ ...(Object.keys(properties).length > 0 ? { properties } : {}),
201
+ },
202
+ status: {
203
+ type: 'string',
204
+ description: 'Task completion status',
205
+ source: '=result.Action',
206
+ var: 'status',
207
+ ...(names.length > 0 ? { enum: names, default: names[0] } : {}),
208
+ },
209
+ ...(errorOutput !== undefined ? { error: jsonClone(errorOutput) } : {}),
210
+ },
211
+ };
212
+ }
213
+ /**
214
+ * One `appInputBindings` value: the expression sent to a deployed app's input.
215
+ *
216
+ * flow-v1 writes these `=`-prefixed and WITHOUT `js:` (`"=vars.<node>.output.<f>"`
217
+ * — `hitl-node-apptask.md`), and its serializer normalizes each to `=js:<body>`
218
+ * when it builds `HitlTaskArguments`. So the flow-level form is the bare
219
+ * `=`-prefixed path, which is what our own `$vars.` expressions become once the
220
+ * prefix is dropped. A plain string passes through as written.
221
+ */
222
+ function appBindingExpr(value, ctx) {
223
+ if (!(value instanceof Expr))
224
+ return String(value ?? '');
225
+ const js = ctx.renameRefs(value.js).trim();
226
+ const path = js.startsWith('$vars.') ? js.slice('$vars.'.length) : js;
227
+ return path.startsWith('=') ? path : `=${path.startsWith('vars.') ? path : `vars.${path}`}`;
228
+ }
229
+ /**
230
+ * A shown field's `binding`: the platform resolves it as a path against the
231
+ * workflow's variables (`vars.<flowInput>`, `vars.<node>.output.<field>`), so the
232
+ * `$vars.` prefix our expressions carry is dropped. A computed expression cannot
233
+ * be a path — `check` rejects one (HITL_FIELD_VALUE_NOT_PATH) before we get here.
234
+ */
235
+ function bindingPath(value, ctx) {
236
+ if (!(value instanceof Expr))
237
+ return String(value ?? '');
238
+ const js = ctx.renameRefs(value.js).trim();
239
+ const path = js.startsWith('$vars.') ? js.slice('$vars.'.length) : js;
240
+ // `vars.`-prefixed, which is what 526 of 528 bound fields in deployed v1 flows
241
+ // carry. flow-v1's serializer does prefix a bare identifier itself
242
+ // (`hitl-serialization.ts`), but only for `HitlTaskArguments` — the
243
+ // `__hitlInputBindings` the designer reads back stores the binding verbatim.
244
+ return path.startsWith('vars.') ? path : `vars.${path}`;
245
+ }
246
+ /**
247
+ * The exits `.stepSwitch` routes over, or why the task has none — read off the
248
+ * same model the task's tails come from.
249
+ */
250
+ function hitlExits({ inputs }, pinnedVersion) {
251
+ const model = hitlExitModel(inputs, pinnedVersion);
252
+ if (model.shape === 'placeholder') {
253
+ // WHY it has no outcome exits decides what the author can do about it, so
254
+ // the caller gets the specific one rather than "ports are off".
255
+ if ((inputs?.outcomes ?? []).length === 0) {
256
+ return { reason: 'it declares no outcomes', suggestion: `outcomes: ['Approve', 'Reject']` };
257
+ }
258
+ return {
259
+ reason: `the '${inputs?.variant}' variant reads its outcomes from its app and leaves on its single 'completed' exit`,
260
+ suggestion: `Use .step() and switch on out('<step>', 'Action').`,
261
+ };
262
+ }
263
+ // A quick-form task and a pinned one continue every outcome under `.step()`,
264
+ // but their outcome handles are real, so `.stepSwitch` routes them. The
265
+ // other fan-out reasons leave nothing to route.
266
+ if (model.shape === 'every-outcome' && model.why !== 'quick-form' && model.why !== 'pinned') {
267
+ if (model.why === 'outcomePorts: false') {
268
+ return {
269
+ reason: 'outcomePorts: false continues every outcome to the next step',
270
+ suggestion: 'Drop `outcomePorts: false`.',
271
+ };
272
+ }
273
+ if (model.why === 'one outcome') {
274
+ return {
275
+ reason: 'a single outcome has a single exit, so there is nothing to route',
276
+ suggestion: 'Use .step() — the next step follows it unconditionally.',
277
+ };
278
+ }
279
+ return {
280
+ reason: 'every outcome ends the process, so no exit distinguishes them',
281
+ suggestion: 'Use .step().',
282
+ };
283
+ }
284
+ return model.exits.map((e) => ({
285
+ value: e.value,
286
+ port: e.port,
287
+ ...(e.endsProcess ? { endsProcess: true } : {}),
288
+ }));
289
+ }
290
+ /** The outcome ids in a human task's `inputs.schema.outcomes`. */
291
+ function outcomeIdsOfNode(node) {
292
+ const outcomes = (node.inputs?.schema?.outcomes ?? []);
293
+ return Array.isArray(outcomes)
294
+ ? outcomes.map((o) => o?.id).filter((id) => typeof id === 'string' && id !== '')
295
+ : [];
296
+ }
297
+ function liftHitl(node, typeName, ctx) {
298
+ const n = node;
299
+ const i = n.inputs ?? {};
300
+ const schema = (i.schema ?? {});
301
+ const plain = (v) => {
302
+ const raw = ctx.sourceText(v);
303
+ return raw === undefined || raw === '' ? undefined : raw;
304
+ };
305
+ const variant = typeName === QUICK_FORM ? 'quick-form'
306
+ : typeName === ACTION_APP ? 'action-app'
307
+ : typeName === DOC_VALIDATION ? 'document-validation'
308
+ : undefined;
309
+ const expr = (js) => ctx.expr(`=js:${js}`);
310
+ // A field row's binding is a variable PATH (`vars.x`), not an expression —
311
+ // serialize rewrote the author's `$vars.x` on the way out, so put it back.
312
+ const bindingExpr = (v) => {
313
+ const raw = plain(v);
314
+ if (raw === undefined)
315
+ return undefined;
316
+ const js = wireExprBody(raw);
317
+ return expr(js.startsWith('vars.') ? `$${js}` : js);
318
+ };
319
+ const rows = (Array.isArray(schema.fields) ? schema.fields : []);
320
+ const fields = rows.map((f) => {
321
+ const id = String(f.id ?? '');
322
+ const direction = String(f.direction ?? 'output');
323
+ const value = direction === 'input' || direction === 'inOut' ? bindingExpr(f.binding) : undefined;
324
+ return {
325
+ id,
326
+ // serialize defaults a row's label to its id.
327
+ ...(typeof f.label === 'string' && f.label !== id ? { label: f.label } : {}),
328
+ // The product's own spelling (`"string"`, `"datetime"`, …) → the SDK's (#1143).
329
+ type: sdkFormFieldType(f.type ?? 'text') ?? String(f.type),
330
+ direction,
331
+ ...(value !== undefined ? { value } : {}),
332
+ ...(f.required === true ? { required: true } : {}),
333
+ };
334
+ });
335
+ const outcomeRows = (Array.isArray(schema.outcomes) ? schema.outcomes : []);
336
+ const outcomes = outcomeRows.map((o, idx) => {
337
+ const name = String(o.name ?? '');
338
+ // The file's id comes back only when serialize would not derive it from the
339
+ // name: a designer-drawn task's ids (`outcome-1778015958049`) are not slugs,
340
+ // and merge keeps the ORIGINAL node, so recompiled edges must use its ids
341
+ // (#1130). An SDK-written task's ids are slugs and stay implicit.
342
+ const id = typeof o.id === 'string' && o.id !== '' && o.id !== outcomeSlug(name, idx) ? o.id : undefined;
343
+ // A bare string is the shorthand; `action: 'End'` or a kept id needs the object.
344
+ return o.action === 'End' || id !== undefined
345
+ ? { name, ...(o.action === 'End' ? { action: 'End' } : {}), ...(id !== undefined ? { id } : {}) }
346
+ : name;
347
+ });
348
+ // Delivery: only an override comes back. The definition's own default is what
349
+ // serialize emits when the author wrote nothing.
350
+ const def = ctx.definition();
351
+ const defaultRecipient = JSON.stringify(def?.inputDefaults?.recipient ?? null);
352
+ const rec = i.recipient;
353
+ let recipient;
354
+ if (rec !== undefined && JSON.stringify(rec) !== defaultRecipient) {
355
+ const assignee = (rec.assignee ?? {});
356
+ recipient = {
357
+ ...(Array.isArray(rec.channels) ? { channels: rec.channels } : {}),
358
+ assignee: {
359
+ type: String(assignee.type ?? 'user'),
360
+ ...(typeof assignee.value === 'string' ? { value: assignee.value } : {}),
361
+ ...(typeof assignee.displayName === 'string' ? { displayName: assignee.displayName } : {}),
362
+ },
363
+ };
364
+ }
365
+ // The deployed app that owns the form (action-app, and a custom-render
366
+ // validation station). `app.inputs` is a VALUE map, keyed by the app's own
367
+ // parameter names, that lowers to a SIBLING key, `appInputBindings`, whose
368
+ // values are `=vars.…` paths (or literals). Not a type map, and not inside
369
+ // `app`: both are easy to assume and both round-trip wrong.
370
+ const app = i.app;
371
+ const bindings = (i.appInputBindings ?? {});
372
+ const appInputs = Object.keys(bindings).length === 0 ? undefined : Object.fromEntries(Object.entries(bindings).map(([k, v]) => {
373
+ const raw = String(v ?? '');
374
+ return [k, raw.startsWith('=') ? expr(wireExprBody(raw)) : raw];
375
+ }));
376
+ const liftedApp = app === undefined ? undefined : {
377
+ // `displayName` is serialize's echo of `name`; only `name` is authored.
378
+ name: String(app.name ?? ''),
379
+ key: String(app.key ?? ''),
380
+ ...(typeof app.folderPath === 'string' && app.folderPath !== '' ? { folderPath: app.folderPath } : {}),
381
+ ...(app.appSystemName === null ? { appSystemName: null }
382
+ : typeof app.appSystemName === 'string' ? { appSystemName: app.appSystemName } : {}),
383
+ ...(appInputs !== undefined ? { inputs: appInputs } : {}),
384
+ };
385
+ // The validation station's own inputs ride as PLAIN `$vars.…` paths. Anything
386
+ // but a `$vars.` reference stays the wire text verbatim.
387
+ const stationExpr = (v) => {
388
+ const raw = plain(v);
389
+ if (raw === undefined)
390
+ return undefined;
391
+ const js = wireExprBody(raw);
392
+ return js.startsWith('$vars.') ? expr(js) : raw;
393
+ };
394
+ const render = plain(i.validationRenderType);
395
+ const document = typeName !== DOC_VALIDATION ? undefined : {
396
+ extractionResult: stationExpr(i.extractionResult) ?? '',
397
+ ...(plain(i.storageBucket) !== undefined ? { storageBucket: plain(i.storageBucket) } : {}),
398
+ ...(plain(i.documentId) !== undefined ? { documentId: plain(i.documentId) } : {}),
399
+ // 'standard' is the definition's default.
400
+ ...(render !== undefined && render !== 'standard' ? { render } : {}),
401
+ ...(stationExpr(i.taxonomy) !== undefined ? { taxonomy: stationExpr(i.taxonomy) } : {}),
402
+ };
403
+ // On the base node the VERSION says it: 1.1 routes per outcome, 1.2 adds the
404
+ // error output (and implies the routing). Everywhere else — quick-form, a
405
+ // base task on 1.0 — the EDGES say it: outcomes that lead to different places
406
+ // are routed per outcome (`outcomePorts: true`, which a pinned 1.0 keeps at
407
+ // 1.0), and outcomes that all lead to one step are a fan-out, which the
408
+ // task's default recompiles to.
409
+ const version = String(n.typeVersion ?? '');
410
+ const versionRoutes = typeName === BASE && (version === '1.1' || version === '1.2');
411
+ const outPorts = ctx.outPorts();
412
+ const derivesHandles = OUTCOME_HANDLE_NODE_TYPES.has(typeName);
413
+ const outcomePorts = (typeName === BASE && version === '1.1')
414
+ || (derivesHandles && !versionRoutes && !ctx.convergedFanOut()
415
+ && outPorts.some((p) => p.startsWith('outcome-')));
416
+ // A lone `completed` edge on a task with several outcomes is the shape the
417
+ // designer stopped drawing: before flow-workbench #3512 (09-08) it was
418
+ // unconditioned, so every outcome continued down it; since then Studio Web and
419
+ // the converter remap it to the primary outcome only. Recompiling writes one
420
+ // `outcome-<id>` edge per outcome to the same step — the first meaning — so
421
+ // this is the one place decompile→compile is deliberately not exact. Say so.
422
+ const legacyCompleted = derivesHandles && outcomeIdsOfNode(n).length > 1 && outPorts.includes('completed');
423
+ // Both a converged fan-out and a legacy lone `completed` mean "every outcome
424
+ // continues to the next step". Say so in the source with `outcomePorts: false`
425
+ // rather than leaving it to the `{ version }` pin, so the intent survives an
426
+ // author dropping the pin.
427
+ const fanOut = !outcomePorts && derivesHandles && !versionRoutes && outcomeIdsOfNode(n).length > 1
428
+ && (legacyCompleted || ctx.convergedFanOut());
429
+ if (legacyCompleted) {
430
+ ctx.warn(`human task ${JSON.stringify(n.id)} leaves on the legacy 'completed' handle, which Studio Web no longer draws `
431
+ + `once a task has outcomes (it reads the edge as the primary outcome only). Recompiling emits one `
432
+ + `outcome-<id> edge per outcome to the same step, so every outcome continues — what the edge meant `
433
+ + `before flow-workbench #3512.`);
434
+ }
435
+ const title = plain(i.title);
436
+ const priority = plain(i.priority);
437
+ const lifted = {
438
+ ...(variant !== undefined ? { variant } : {}),
439
+ // serialize defaults the title to the step's own name.
440
+ ...(title !== undefined && title !== n.id ? { title } : {}),
441
+ ...(priority !== undefined && priority !== 'Low' ? { priority } : {}),
442
+ ...(plain(i.labels) !== undefined ? { labels: plain(i.labels) } : {}),
443
+ ...(recipient !== undefined ? { recipient } : {}),
444
+ ...(liftedApp !== undefined ? { app: liftedApp } : {}),
445
+ ...(document !== undefined ? { document } : {}),
446
+ // An app task and a validation station own no inline form; an empty array
447
+ // there is serialize's own, not the author's.
448
+ ...(fields.length || (variant !== 'action-app' && variant !== 'document-validation') ? { fields } : {}),
449
+ outcomes,
450
+ ...(outcomePorts ? { outcomePorts: true } : fanOut ? { outcomePorts: false } : {}),
451
+ ...(version === '1.2' ? { exposeError: true } : {}),
452
+ };
453
+ return { kind: 'hitl', inputs: lifted };
454
+ }
455
+ /** `{ k: v, … }` over the entries that are set, or '' when none is — the decompiler's `opts`. */
456
+ function obj(entries, ctx) {
457
+ const parts = entries.filter(([, v]) => v !== undefined).map(([k, v]) => `${ctx.key(k)}: ${v}`);
458
+ return parts.length ? `{ ${parts.join(', ')} }` : '';
459
+ }
460
+ function printHitl({ inputs }, ctx) {
461
+ const h = inputs;
462
+ const val = (v) => (v instanceof Expr ? ctx.value(v) : ctx.string(v));
463
+ const recipient = h.recipient === undefined ? undefined : obj([
464
+ ['channels', h.recipient.channels ? `[${h.recipient.channels.map((c) => ctx.string(c)).join(', ')}]` : undefined],
465
+ ['assignee', obj([
466
+ ['type', ctx.string(h.recipient.assignee.type)],
467
+ ['value', h.recipient.assignee.value !== undefined ? ctx.string(h.recipient.assignee.value) : undefined],
468
+ ['displayName', h.recipient.assignee.displayName !== undefined ? ctx.string(h.recipient.assignee.displayName) : undefined],
469
+ ], ctx)],
470
+ ], ctx);
471
+ const app = h.app === undefined ? undefined : obj([
472
+ ['name', ctx.string(h.app.name)],
473
+ ['key', ctx.string(h.app.key)],
474
+ ['folderPath', h.app.folderPath !== undefined ? ctx.string(h.app.folderPath) : undefined],
475
+ ['appSystemName', h.app.appSystemName === null ? 'null'
476
+ : h.app.appSystemName !== undefined ? ctx.string(h.app.appSystemName) : undefined],
477
+ ['inputs', h.app.inputs !== undefined
478
+ ? obj(Object.entries(h.app.inputs).map(([k, v]) => [k, val(v)]), ctx) : undefined],
479
+ ], ctx);
480
+ const document = h.document === undefined ? undefined : obj([
481
+ ['extractionResult', val(h.document.extractionResult)],
482
+ ['storageBucket', h.document.storageBucket !== undefined ? ctx.string(h.document.storageBucket) : undefined],
483
+ ['documentId', h.document.documentId !== undefined ? ctx.string(h.document.documentId) : undefined],
484
+ ['render', h.document.render !== undefined ? ctx.string(h.document.render) : undefined],
485
+ ['taxonomy', h.document.taxonomy !== undefined ? val(h.document.taxonomy) : undefined],
486
+ ], ctx);
487
+ const fields = h.fields?.map((f) => obj([
488
+ ['id', ctx.string(f.id)],
489
+ ['label', f.label !== undefined ? ctx.string(f.label) : undefined],
490
+ ['type', ctx.string(f.type)],
491
+ ['direction', ctx.string(f.direction)],
492
+ ['value', f.value !== undefined ? ctx.value(f.value) : undefined],
493
+ ['required', f.required ? 'true' : undefined],
494
+ ], ctx));
495
+ const outcomes = h.outcomes.map((o) => (typeof o === 'string' ? ctx.string(o) : obj([
496
+ ['name', ctx.string(o.name)],
497
+ ['action', o.action !== undefined ? ctx.string(o.action) : undefined],
498
+ ['id', o.id !== undefined ? ctx.string(o.id) : undefined],
499
+ ], ctx)));
500
+ return {
501
+ factory: 'hitl',
502
+ options: [
503
+ ['variant', h.variant !== undefined ? ctx.string(h.variant) : undefined],
504
+ ['title', h.title !== undefined ? ctx.string(h.title) : undefined],
505
+ ['priority', h.priority !== undefined ? ctx.string(h.priority) : undefined],
506
+ ['labels', h.labels !== undefined ? ctx.string(h.labels) : undefined],
507
+ ['recipient', recipient],
508
+ ['app', app],
509
+ ['document', document],
510
+ ['fields', fields !== undefined ? `[${fields.join(', ')}]` : undefined],
511
+ ['outcomes', `[${outcomes.join(', ')}]`],
512
+ ['outcomePorts', h.outcomePorts === true ? 'true' : h.outcomePorts === false ? 'false' : undefined],
513
+ ['exposeError', h.exposeError ? 'true' : undefined],
514
+ ],
515
+ };
516
+ }
517
+ function hitlAt(typeName, version, factory) {
518
+ return {
519
+ kind: 'hitl', typeName, version, factory: 'hitl', definition: 'bundled',
520
+ rawNode: { factory },
521
+ serialize: ({ inputs }, ctx) => {
522
+ // How the task leaves its node — `hitlExitModel` (core/hitl-routing.ts)
523
+ // has the product rule and what `.step()` means after each shape. An
524
+ // explicit pin wins over the inferred 1.1 (`select`) — `flow validate`
525
+ // accepts `outcome-<id>` edges on base@1.0 — but not over `exposeError`,
526
+ // whose output only 1.2 declares.
527
+ const pinned = ctx.pinnedVersion;
528
+ const exitModel = hitlExitModel(inputs, pinned);
529
+ if (typeName !== BASE && inputs?.exposeError === true) {
530
+ throw new Error(`"${ctx.step}": exposeError needs the base human task's 1.2 definition, which adds the `
531
+ + `error output — the '${inputs?.variant}' variant has no such version. Drop the variant or exposeError.`);
532
+ }
533
+ if (inputs?.outcomePorts === true && !OUTCOME_HANDLE_NODE_TYPES.has(typeName)) {
534
+ throw new Error(`"${ctx.step}": outcomePorts: true, but the '${inputs?.variant}' variant reads its outcomes `
535
+ + `from its app and leaves on its single 'completed' exit. Route on out('${ctx.step}', 'Action') downstream.`);
536
+ }
537
+ if (inputs?.exposeError === true && pinned !== undefined && pinned !== '1.2') {
538
+ throw new Error(`"${ctx.step}": exposeError needs the task's 1.2 definition, but { version: '${pinned}' } pins it. `
539
+ + `Drop the pin or match it.`);
540
+ }
541
+ const { inputs: nodeInputs, outputs } = hitlInputs(inputs, typeName, ctx);
542
+ // The definition's `outputDefinition` gives the two output SLOTS; the form
543
+ // gives their shape (see hitlInputs), so the instance's outputs replace it.
544
+ const contribution = { inputs: nodeInputs, outputs, replaceOutputs: true };
545
+ if (exitModel.shape === 'per-outcome') {
546
+ // The PRIMARY (first) outcome continues the main path; the author routes
547
+ // the rest with .stepToList('outcome-<id>', …) or as .stepSwitch arms.
548
+ return { ...contribution, port: exitModel.exits[0]?.port ?? `outcome-${outcomeSlug(undefined, 0)}` };
549
+ }
550
+ if (exitModel.shape === 'every-outcome') {
551
+ // Fan-out: every outcome continues to the next step, each on its own
552
+ // handle. End outcomes are marked, and `emit` ends them.
553
+ return { ...contribution, fanOut: exitModel.exits.map((e) => ({ port: e.port, ...(e.endsProcess ? { endsRun: true } : {}) })) };
554
+ }
555
+ // Every human-task definition declares one static source handle,
556
+ // `completed`: the real exit on an app-backed task and on one with no outcomes.
557
+ return { ...contribution, port: 'completed' };
558
+ },
559
+ deserialize: (node, ctx) => liftHitl(node, typeName, ctx),
560
+ print: printHitl,
561
+ check: ({ inputs }, ctx) => checkHitl(inputs, ctx),
562
+ // A human task's only expressions are the values its shown fields display.
563
+ // Without this a typo'd reference inside a form field would reach the
564
+ // deployed task as an unresolvable path and show the reviewer nothing.
565
+ expressions: ({ inputs }) => (inputs?.fields ?? [])
566
+ .filter((f) => f?.direction === 'input')
567
+ .map((f) => ({ value: f.value })),
568
+ output: ({ inputs }) => ({ kind: 'reader', read: (read, report) => checkHitlRead(read, inputs, report) }),
569
+ exits: hitlExits,
570
+ // The base node and quick-form draw one `outcome-<id>` handle per outcome,
571
+ // whatever the version; their definitions declare only `completed`.
572
+ derivesPort: ({ inputs }, port) => OUTCOME_HANDLE_NODE_TYPES.has(typeName)
573
+ && (inputs?.outcomes ?? []).some((o, i) => port === `outcome-${outcomeIdOf(o, i)}`),
574
+ };
575
+ }
576
+ function hitlType(typeName, versions, factory, variant) {
577
+ return {
578
+ kind: 'hitl', typeName, factory: 'hitl', defaultVersion: '1.0',
579
+ versions: Object.fromEntries(versions.map((v) => [v, hitlAt(typeName, v, factory)])),
580
+ // The base node is the one an absent or UNKNOWN variant resolves to, as it
581
+ // always has: `check` reports the unknown one (HITL_VARIANT_UNKNOWN).
582
+ matches: ({ inputs }) => (variant === undefined
583
+ ? !VARIANTS.includes(inputs?.variant)
584
+ : inputs?.variant === variant),
585
+ // Only the BASE node has definition versions to select: `exposeError` picks
586
+ // 1.2 (which adds the `error` output), and a task that routes per outcome
587
+ // picks 1.1. A variant routes per outcome on the one definition it has.
588
+ // `select` runs only when the step is unpinned (a pin wins upstream), so it
589
+ // asks the routing model with no pin, while `exits` and `serialize` get it.
590
+ ...(variant === undefined ? {
591
+ select: ({ inputs }) => (inputs?.exposeError ? '1.2'
592
+ : hitlExitModel(inputs, undefined).shape === 'per-outcome' ? '1.1' : '1.0'),
593
+ } : {}),
594
+ };
595
+ }
596
+ export const hitlNodeTypes = [
597
+ hitlType(BASE, ['1.0', '1.1', '1.2'], 'hitl()', undefined),
598
+ hitlType(QUICK_FORM, ['1.0', '1.1'], "hitl({ variant: 'quick-form' })", 'quick-form'),
599
+ hitlType(ACTION_APP, ['1.0', '1.1'], "hitl({ variant: 'action-app' })", 'action-app'),
600
+ hitlType(DOC_VALIDATION, ['1.0'], "hitl({ variant: 'document-validation' })", 'document-validation'),
601
+ ];