@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.
- package/dist/api-index.md +115 -115
- package/dist/api-members.md +351 -351
- package/dist/case/compile-cli.js +58 -2
- package/dist/check.js +1 -392
- package/dist/core/actions.d.ts +0 -304
- package/dist/core/actions.js +0 -54
- package/dist/core/form-field-type.d.ts +16 -0
- package/dist/core/form-field-type.js +33 -0
- package/dist/core/hitl-routing.d.ts +17 -3
- package/dist/core/stable-id.d.ts +9 -0
- package/dist/core/stable-id.js +16 -0
- package/dist/core/step-ports.d.ts +25 -1
- package/dist/core/step-ports.js +6 -47
- package/dist/core/typed-raw-node.js +0 -8
- package/dist/decompile.js +7 -191
- package/dist/flow-node-registry.js +2 -1
- package/dist/flow-node-type.d.ts +45 -1
- package/dist/flow-sdk.d.ts +1 -0
- package/dist/flow-sdk.js +1 -0
- package/dist/flow-step-exits.js +1 -1
- package/dist/nodes/hitl/flow/check.d.ts +38 -0
- package/dist/nodes/hitl/flow/check.js +357 -0
- package/dist/nodes/hitl/flow/factories.d.ts +58 -0
- package/dist/nodes/hitl/flow/factories.js +59 -0
- package/dist/nodes/hitl/flow/family.d.ts +3 -0
- package/dist/nodes/hitl/flow/family.js +8 -0
- package/dist/nodes/hitl/flow/index.d.ts +3 -0
- package/dist/nodes/hitl/flow/index.js +3 -0
- package/dist/nodes/hitl/flow/node-type.d.ts +7 -0
- package/dist/nodes/hitl/flow/node-type.js +601 -0
- package/dist/nodes/hitl/flow/spec.d.ts +265 -0
- package/dist/nodes/hitl/flow/spec.js +1 -0
- package/dist/serialize.d.ts +2 -4
- package/dist/serialize.js +35 -344
- package/package.json +2 -2
package/dist/case/compile-cli.js
CHANGED
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
* Output defaults to the plan file the project in the working directory ships
|
|
11
11
|
* (`caseplan.case` for a project `uip maestro case init` scaffolded), else
|
|
12
12
|
* `caseplan.json`. An `-o` that names a different plan file than the project
|
|
13
|
-
* ships is refused (CASE_OUTPUT_NOT_SHIPPED)
|
|
13
|
+
* ships is refused (CASE_OUTPUT_NOT_SHIPPED), and so is a plan file written into
|
|
14
|
+
* a solution directory above its Case project (CASE_OUTPUT_OUTSIDE_PROJECT).
|
|
14
15
|
*/
|
|
15
16
|
import { reportDiagnostics, reportResult, thrownDiagnostic } from '../cli-result.js';
|
|
16
17
|
import { writeJsonFile } from '../core/json-file.js';
|
|
@@ -67,7 +68,7 @@ export async function run(argv) {
|
|
|
67
68
|
const out = flagValue(argv, '-o') ?? shippedCasePlanName('.') ?? 'caseplan.json';
|
|
68
69
|
const shadow = shadowedOutputDiagnostic(out);
|
|
69
70
|
if (shadow) {
|
|
70
|
-
console.error(` ✗ [${shadow.code}] ${shadow.message}
|
|
71
|
+
console.error(` ✗ [${shadow.code}] ${shadow.message} Fix: ${shadow.suggestion}.`);
|
|
71
72
|
reportDiagnostics([...diags, shadow]);
|
|
72
73
|
console.error('compile: 1 error(s); not emitting.');
|
|
73
74
|
return 1;
|
|
@@ -96,6 +97,9 @@ export async function run(argv) {
|
|
|
96
97
|
* `uip maestro case init` scaffolds `caseplan.case` since uipath-cli#5004.
|
|
97
98
|
*/
|
|
98
99
|
const CASE_PLAN_FILE_NAMES = ['caseplan.case', 'caseplan.json', 'case.stage.json', 'default.stage.json'];
|
|
100
|
+
/** The file that makes a directory a project, and the `ProjectType` a Case project declares in it. */
|
|
101
|
+
const PROJECT_FILE = 'project.uiproj';
|
|
102
|
+
const CASE_PROJECT_TYPE = 'CaseManagement';
|
|
99
103
|
/**
|
|
100
104
|
* The plan file the project in `dir` ships, or undefined when `dir` is not a
|
|
101
105
|
* Case project. Decided as the packager decides: the first plan file present in
|
|
@@ -126,6 +130,11 @@ function shadowedOutputDiagnostic(outPath) {
|
|
|
126
130
|
if (!CASE_PLAN_FILE_NAMES.includes(name.toLowerCase()))
|
|
127
131
|
return undefined;
|
|
128
132
|
const dir = dirname(outPath);
|
|
133
|
+
// First: a plan file already in a solution directory would otherwise make it
|
|
134
|
+
// look like a project that ships that file.
|
|
135
|
+
const outside = outsideProjectDiagnostic(outPath);
|
|
136
|
+
if (outside)
|
|
137
|
+
return outside;
|
|
129
138
|
const shipped = shippedCasePlanName(dir);
|
|
130
139
|
if (!shipped || shipped === name)
|
|
131
140
|
return undefined;
|
|
@@ -138,6 +147,53 @@ function shadowedOutputDiagnostic(outPath) {
|
|
|
138
147
|
suggestion: `compile with -o ${target}`,
|
|
139
148
|
};
|
|
140
149
|
}
|
|
150
|
+
/**
|
|
151
|
+
* A plan file in a non-project dir that holds Case projects (a solution dir) is
|
|
152
|
+
* packed by none. A directory that is itself a project, or holds no Case
|
|
153
|
+
* project, is not checked.
|
|
154
|
+
*/
|
|
155
|
+
function outsideProjectDiagnostic(outPath) {
|
|
156
|
+
const dir = dirname(outPath);
|
|
157
|
+
if (existsSync(join(dir, PROJECT_FILE)))
|
|
158
|
+
return undefined;
|
|
159
|
+
const projects = childCaseProjects(dir);
|
|
160
|
+
if (projects.length === 0)
|
|
161
|
+
return undefined;
|
|
162
|
+
const targets = projects.map((p) => join(dir, p, shippedCasePlanName(join(dir, p)) ?? CASE_PLAN_FILE_NAMES[0]));
|
|
163
|
+
return {
|
|
164
|
+
level: 'error',
|
|
165
|
+
code: 'CASE_OUTPUT_OUTSIDE_PROJECT',
|
|
166
|
+
message: `${outPath} is in a solution directory, not a Case project; no project packs it. ` +
|
|
167
|
+
`The Case project${projects.length > 1 ? 's here are' : ' here is'} ${projects.map((p) => join(dir, p)).join(', ')}.`,
|
|
168
|
+
suggestion: projects.length === 1 ? `compile with -o ${targets[0]}` : `compile with -o one of: ${targets.join(', ')}`,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Subdirectories of `dir` (or links to one) whose `project.uiproj` declares a
|
|
173
|
+
* CaseManagement project.
|
|
174
|
+
*/
|
|
175
|
+
function childCaseProjects(dir) {
|
|
176
|
+
let entries;
|
|
177
|
+
try {
|
|
178
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
return [];
|
|
182
|
+
}
|
|
183
|
+
return entries
|
|
184
|
+
// A link counts: the read below fails for a link to a file.
|
|
185
|
+
.filter((e) => e.isDirectory() || e.isSymbolicLink())
|
|
186
|
+
.map((e) => e.name)
|
|
187
|
+
.filter((name) => {
|
|
188
|
+
try {
|
|
189
|
+
return JSON.parse(readFileSync(join(dir, name, PROJECT_FILE), 'utf8'))?.ProjectType === CASE_PROJECT_TYPE;
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
})
|
|
195
|
+
.sort();
|
|
196
|
+
}
|
|
141
197
|
/** The case file `package-descriptor.json` in `dir` ships, if it names one. */
|
|
142
198
|
function describedCaseFileName(dir) {
|
|
143
199
|
const descriptorPath = join(dir, 'package-descriptor.json');
|
package/dist/check.js
CHANGED
|
@@ -10,7 +10,7 @@ import { FlowNode } from './core/node-classes.js';
|
|
|
10
10
|
import { planContributionBindings } from './core/contribution-bindings.js';
|
|
11
11
|
import { definitionHasErrorPort, errorPortRefusal } from './core/error-port.js';
|
|
12
12
|
import { actionVersion, bundledActionNodeType, bundledContainerDefinition, bundledDefinition, perCallDefinition, resolveContributionDefinition } from './serialize.js';
|
|
13
|
-
import {
|
|
13
|
+
import { FORM_FIELD_TYPES, formFieldTypeHint, formFieldTypeWhy } from './core/form-field-type.js';
|
|
14
14
|
import { AGENTIC_PROCESS_FORMS } from './core/agentic-process-forms.js';
|
|
15
15
|
import { checkEventResolution, checkEventSubscription, eventParamsFor, } from './core/event-checks.js';
|
|
16
16
|
import { checkCookedEscapes } from './flow-expr-check.js';
|
|
@@ -18,7 +18,6 @@ import { FLOW_EXPR_DIALECT, checkBindingLabels, checkBindingPresence, checkConne
|
|
|
18
18
|
import { prepareCommand } from './core/cli-spelling.js';
|
|
19
19
|
import { commentText } from './core/comment-text.js';
|
|
20
20
|
import { nearestName } from './core/edit-distance.js';
|
|
21
|
-
import { isAnswerField } from './core/hitl-answers.js';
|
|
22
21
|
import { hitlDerivesOutcomeHandles, hitlExitModel, hitlRoutesPerOutcome, outcomeIdOf } from './core/hitl-routing.js';
|
|
23
22
|
import { hasExits } from './core/step-ports.js';
|
|
24
23
|
import { stopsAtEnd } from './flow-step-tree.js';
|
|
@@ -175,10 +174,6 @@ function checkFlow(built, ancestors, opts = {}) {
|
|
|
175
174
|
// readable result (see TERMINATE_HAS_NO_OUTPUT below) — and it stops the RUN,
|
|
176
175
|
// so anything after it is unreachable (TERMINATE_UNREACHABLE_AFTER).
|
|
177
176
|
const terminateNames = new Set();
|
|
178
|
-
// Human-task step name → its form. The node's output is an object keyed by the
|
|
179
|
-
// form's ANSWER fields (plus `Action`), so a read of it is checked against the
|
|
180
|
-
// form the author wrote rather than waved through (see HITL_READ_* below).
|
|
181
|
-
const hitlForms = new Map();
|
|
182
177
|
// Agentic-process step name → its call, for the form-field rules below. Its
|
|
183
178
|
// output READS go through OUTPUT_READ_RULES like every `returns` family.
|
|
184
179
|
const agenticProcesses = new Map();
|
|
@@ -423,8 +418,6 @@ function checkFlow(built, ancestors, opts = {}) {
|
|
|
423
418
|
})));
|
|
424
419
|
}
|
|
425
420
|
if (s.spec.kind === 'hitl') {
|
|
426
|
-
checkHitl(s.name, s.spec.inputs, diags);
|
|
427
|
-
hitlForms.set(s.name, s.spec.inputs);
|
|
428
421
|
const outcomes = s.spec.inputs?.outcomes ?? [];
|
|
429
422
|
// `action: 'End'` ends the PROCESS at the node, so a task whose every
|
|
430
423
|
// outcome ends it has nothing downstream to distinguish — one exit is
|
|
@@ -1131,15 +1124,6 @@ function checkFlow(built, ancestors, opts = {}) {
|
|
|
1131
1124
|
checkSubflowRead(name, field, rest, child, step, diags);
|
|
1132
1125
|
continue;
|
|
1133
1126
|
}
|
|
1134
|
-
// A human task's output is an object keyed by the form's ANSWER fields,
|
|
1135
|
-
// plus `Action` (which outcome was pressed). A read of anything else is
|
|
1136
|
-
// undefined at run time, and `.status` is the worst of them — it resolves
|
|
1137
|
-
// deployed, so the mistake only shows up in production.
|
|
1138
|
-
const form = hitlForms.get(name);
|
|
1139
|
-
if (form) {
|
|
1140
|
-
checkHitlRead(name, field, rest, form, step, diags);
|
|
1141
|
-
continue;
|
|
1142
|
-
}
|
|
1143
1127
|
// Every family that declares its output's fields — the author's `returns`, or
|
|
1144
1128
|
// a shape the SDK fixes — has its read rule in OUTPUT_READ_RULES, keyed by
|
|
1145
1129
|
// kind. A read outside the declared set is undefined on every rung and on
|
|
@@ -3820,39 +3804,6 @@ function endDiag(join, h) {
|
|
|
3820
3804
|
* reference to hide — which is what the checks below enforce for the cases
|
|
3821
3805
|
* where types have been stripped at run time.
|
|
3822
3806
|
*/
|
|
3823
|
-
/**
|
|
3824
|
-
* Why a form-field type is refused: a product spelling of a type the SDK
|
|
3825
|
-
* models (`"string"` is the platform's own name for text), or a type
|
|
3826
|
-
* the SDK does not model at all. `unknownWhy` is the caller's reason for the
|
|
3827
|
-
* second case; the default is a human task's.
|
|
3828
|
-
*/
|
|
3829
|
-
function formFieldTypeWhy(type, unknownWhy = `which the platform does not recognize. Older runtimes read it as text, newer ones reject `
|
|
3830
|
-
+ `the form`) {
|
|
3831
|
-
const alias = sdkFormFieldType(type);
|
|
3832
|
-
return alias !== undefined
|
|
3833
|
-
? `which is the platform's spelling of the SDK's '${alias}' — the SDK writes '${alias}'`
|
|
3834
|
-
: unknownWhy;
|
|
3835
|
-
}
|
|
3836
|
-
/** Every FormFieldType, once; `Record<FormFieldType, …>` makes tsc refuse a missing or extra one. */
|
|
3837
|
-
const FORM_FIELD_TYPE_SET = { text: true, number: true, boolean: true, 'date-time': true };
|
|
3838
|
-
/** The form-field types the SDK models. */
|
|
3839
|
-
const FORM_FIELD_TYPES = Object.keys(FORM_FIELD_TYPE_SET);
|
|
3840
|
-
/** The `suggestion` for an unknown form-field type: the nearest valid one, else `type: 'text'`. */
|
|
3841
|
-
function formFieldTypeHint(type) {
|
|
3842
|
-
const alias = sdkFormFieldType(type);
|
|
3843
|
-
if (alias !== undefined)
|
|
3844
|
-
return `type: '${alias}'`;
|
|
3845
|
-
const nearest = nearestName(String(type), FORM_FIELD_TYPES);
|
|
3846
|
-
return nearest !== undefined ? `type: '${nearest}'` : `type: 'text'`;
|
|
3847
|
-
}
|
|
3848
|
-
/** Names the human task's own result already uses — an answer cannot claim one. */
|
|
3849
|
-
const RESERVED_ANSWER_IDS = new Set(['output', 'status', 'Action']);
|
|
3850
|
-
/**
|
|
3851
|
-
* The human-task node types `variant` can name. `document-validation` resolves
|
|
3852
|
-
* from the BUNDLED workbench manifest (the tenant registry does not serve the
|
|
3853
|
-
* family — same availability note as do-while).
|
|
3854
|
-
*/
|
|
3855
|
-
const HITL_VARIANTS = ['quick-form', 'action-app', 'document-validation'];
|
|
3856
3807
|
/**
|
|
3857
3808
|
* Why a base or quick-form task continues every outcome to the next step, as a
|
|
3858
3809
|
* clause for a diagnostic. `pinned` is the step's `{ version }`.
|
|
@@ -3964,338 +3915,6 @@ function reportUnreachableHitlOutcomes(choices, exprs, diags) {
|
|
|
3964
3915
|
});
|
|
3965
3916
|
}
|
|
3966
3917
|
}
|
|
3967
|
-
/**
|
|
3968
|
-
* A human task's form.
|
|
3969
|
-
*
|
|
3970
|
-
* Everything here is a mistake no later rung can see, which is the test for
|
|
3971
|
-
* earning a place in `check` rather than in the docs: the flow compiles, deploys,
|
|
3972
|
-
* and shows a person a form with something quietly missing from it. These catch it
|
|
3973
|
-
* at rung 1, in the author's own vocabulary.
|
|
3974
|
-
*/
|
|
3975
|
-
function checkHitl(step, inputs, diags) {
|
|
3976
|
-
const at = (code, message, suggestion) => {
|
|
3977
|
-
diags.push({ level: 'error', code, message, step, ...(suggestion ? { suggestion } : {}) });
|
|
3978
|
-
};
|
|
3979
|
-
// Which of the three human-task node types this step emits. The two suffixed
|
|
3980
|
-
// variants differ in exactly one authoring-visible way — who OWNS the form —
|
|
3981
|
-
// and that is what the next four rules are about.
|
|
3982
|
-
const variant = inputs?.variant;
|
|
3983
|
-
if (variant !== undefined && variant !== 'quick-form' && variant !== 'action-app'
|
|
3984
|
-
&& variant !== 'document-validation') {
|
|
3985
|
-
const nearest = nearestName(String(variant), HITL_VARIANTS);
|
|
3986
|
-
at('HITL_VARIANT_UNKNOWN', `Human task "${step}" names the variant ${JSON.stringify(variant)}, which is not a human-task `
|
|
3987
|
-
+ `node type that resolves on this tenant. Valid: ${HITL_VARIANTS.join(', ')} — or omit it for `
|
|
3988
|
-
+ `the default node type (uipath.human-in-the-loop).`, nearest !== undefined ? `variant: '${nearest}'` : `Omit \`variant\`.`);
|
|
3989
|
-
return;
|
|
3990
|
-
}
|
|
3991
|
-
const isApp = variant === 'action-app';
|
|
3992
|
-
const isDocval = variant === 'document-validation';
|
|
3993
|
-
// The error output is the BASE node's 1.2 definition, which no variant has.
|
|
3994
|
-
// Per-outcome exits are not a version question: quick-form draws one handle
|
|
3995
|
-
// per outcome like the base node, so it may ask for them; the two app-backed
|
|
3996
|
-
// variants read their outcomes from the app and leave on `completed`.
|
|
3997
|
-
if (variant !== undefined
|
|
3998
|
-
&& (inputs?.exposeError === true || (inputs?.outcomePorts === true && !hitlDerivesOutcomeHandles(variant)))) {
|
|
3999
|
-
at('HITL_OUTCOME_PORTS_VARIANT', inputs?.exposeError === true
|
|
4000
|
-
? `Human task "${step}" asks for exposeError, but its variant is "${variant}" — the error output `
|
|
4001
|
-
+ `exists only on the base node type's 1.2 definition.`
|
|
4002
|
-
: `Human task "${step}" asks for outcomePorts, but its variant is "${variant}", which reads its `
|
|
4003
|
-
+ `outcomes from its app and leaves on its single 'completed' exit.`, inputs?.exposeError === true
|
|
4004
|
-
? 'Omit `variant`, or drop exposeError.'
|
|
4005
|
-
: `Route on out('${step}', 'Action') downstream.`);
|
|
4006
|
-
return;
|
|
4007
|
-
}
|
|
4008
|
-
if (inputs?.document !== undefined && !isDocval) {
|
|
4009
|
-
at('HITL_DOCUMENT_WITHOUT_VARIANT', `Human task "${step}" names a document to validate, but its variant is `
|
|
4010
|
-
+ `${variant === undefined ? 'the default node type' : `"${variant}"`} — only the validation `
|
|
4011
|
-
+ `station reads it, so it would be emitted and ignored.`, `variant: 'document-validation'`);
|
|
4012
|
-
return;
|
|
4013
|
-
}
|
|
4014
|
-
if (isDocval) {
|
|
4015
|
-
if (Array.isArray(inputs?.fields) && inputs.fields.length > 0) {
|
|
4016
|
-
at('HITL_DOCVAL_WITH_FIELDS', `Human task "${step}" is a Document Validation task but also declares ${inputs.fields.length} `
|
|
4017
|
-
+ `form field(s). The validation station IS the form — these are dropped.`, `Delete \`fields\`.`);
|
|
4018
|
-
return;
|
|
4019
|
-
}
|
|
4020
|
-
const doc = inputs?.document;
|
|
4021
|
-
if (!doc || typeof doc !== 'object') {
|
|
4022
|
-
at('HITL_DOCVAL_NO_DOCUMENT', `Human task "${step}" uses variant 'document-validation' but names no document — the station `
|
|
4023
|
-
+ `has nothing to validate.`, `document: { extractionResult: out('<extractStep>', 'ExtractionResult') }`);
|
|
4024
|
-
return;
|
|
4025
|
-
}
|
|
4026
|
-
const er = doc.extractionResult;
|
|
4027
|
-
const erEmpty = er === undefined || er === null || (typeof er === 'string' && !er.trim());
|
|
4028
|
-
if (erEmpty) {
|
|
4029
|
-
at('DOCVAL_NO_EXTRACTION', `Human task "${step}" gives the validation station no extractionResult. Bind the `
|
|
4030
|
-
+ `ExtractionResult output of the upstream extract step — the platform's own validator `
|
|
4031
|
-
+ `rejects the node without it.`, `extractionResult: out('<extractStep>', 'ExtractionResult')`);
|
|
4032
|
-
}
|
|
4033
|
-
if (doc.render === 'custom') {
|
|
4034
|
-
const tx = doc.taxonomy;
|
|
4035
|
-
if (tx === undefined || tx === null || (typeof tx === 'string' && !tx.trim())) {
|
|
4036
|
-
at('DOCVAL_TAXONOMY_REQUIRED', `Human task "${step}" renders validation in a custom app (render: 'custom') but names no `
|
|
4037
|
-
+ `taxonomy — the platform requires one for the app's Validation Control.`, `taxonomy: out('<classifyStep>', 'taxonomy') // or the taxonomy id string`);
|
|
4038
|
-
}
|
|
4039
|
-
}
|
|
4040
|
-
else if (doc.render !== undefined && doc.render !== 'standard') {
|
|
4041
|
-
at('DOCVAL_RENDER_UNKNOWN', `Human task "${step}" gives the validation render type ${JSON.stringify(doc.render)}. Valid: `
|
|
4042
|
-
+ `'standard' (the native station, the default) or 'custom' (an app with a Validation Control).`, `render: 'standard'`);
|
|
4043
|
-
}
|
|
4044
|
-
// No inline form — skip the field rules; outcomes still route, fall through.
|
|
4045
|
-
}
|
|
4046
|
-
if (!isApp && !(isDocval && inputs?.document?.render === 'custom') && inputs?.app !== undefined) {
|
|
4047
|
-
at('HITL_APP_WITHOUT_VARIANT', `Human task "${step}" names a deployed app, but its variant is `
|
|
4048
|
-
+ `${variant === undefined ? 'the default node type' : `"${variant}"`}, whose form lives in the `
|
|
4049
|
-
+ `flow. The app would be emitted and then ignored — the reviewer still gets the inline form.`, `variant: 'action-app'`);
|
|
4050
|
-
return;
|
|
4051
|
-
}
|
|
4052
|
-
if (isApp) {
|
|
4053
|
-
// The app owns the form, so `fields` is not merely unnecessary here — it is a
|
|
4054
|
-
// statement about a form this node does not have, and the platform drops it.
|
|
4055
|
-
if (Array.isArray(inputs?.fields) && inputs.fields.length > 0) {
|
|
4056
|
-
at('HITL_APP_WITH_FIELDS', `Human task "${step}" is backed by an Action App but also declares ${inputs.fields.length} form `
|
|
4057
|
-
+ `field(s). The form belongs to the DEPLOYED APP, not to the flow, so these are dropped and `
|
|
4058
|
-
+ `the reviewer sees the app's own form instead.`, `Delete \`fields\`; map values into the app with \`app.inputs\` instead.`);
|
|
4059
|
-
return;
|
|
4060
|
-
}
|
|
4061
|
-
const app = inputs?.app;
|
|
4062
|
-
if (!app || typeof app !== 'object') {
|
|
4063
|
-
at('HITL_APP_MISSING', `Human task "${step}" uses variant 'action-app', which takes its form from a deployed Action `
|
|
4064
|
-
+ `App — but names no app, so nothing tells the platform which form to show.`, `app: { name: 'Expense Review', key: '<guid>', folderPath: 'Shared' }`);
|
|
4065
|
-
return;
|
|
4066
|
-
}
|
|
4067
|
-
for (const k of ['name', 'key', 'folderPath']) {
|
|
4068
|
-
if (typeof app[k] !== 'string' || !app[k].trim()) {
|
|
4069
|
-
at('HITL_APP_INCOMPLETE', `Human task "${step}" names an Action App with no ${k}. An app is resolved by its key plus `
|
|
4070
|
-
+ `the folder it is deployed in, with the name shown to the reviewer — all three are needed `
|
|
4071
|
-
+ `for the platform to find it.`, `app: { name: 'Expense Review', key: '<guid>', folderPath: 'Shared' }`);
|
|
4072
|
-
return;
|
|
4073
|
-
}
|
|
4074
|
-
}
|
|
4075
|
-
// Outcomes still matter for an app task: the flow's own routing and its
|
|
4076
|
-
// `Action`/`status` outputs are derived from them, so fall through to the
|
|
4077
|
-
// outcome rules below rather than returning here.
|
|
4078
|
-
}
|
|
4079
|
-
// An app task legitimately has no form fields of its own — the rules below then
|
|
4080
|
-
// walk an empty list and only the outcome rules bite, which is what we want.
|
|
4081
|
-
const fields = isApp || isDocval ? (inputs?.fields ?? []) : inputs?.fields;
|
|
4082
|
-
if (!Array.isArray(fields)) {
|
|
4083
|
-
at('HITL_NO_FIELDS', `Human task "${step}" has no fields array. Give it the form: what the reviewer reads `
|
|
4084
|
-
+ `(direction 'input') and what they fill in (direction 'output').`, `fields: [{ id: 'amount', type: 'number', direction: 'input', value: input('amount') }]`);
|
|
4085
|
-
return;
|
|
4086
|
-
}
|
|
4087
|
-
const seen = new Set();
|
|
4088
|
-
let answers = 0;
|
|
4089
|
-
fields.forEach((f, i) => {
|
|
4090
|
-
const where = `fields[${i}]`;
|
|
4091
|
-
const id = typeof f?.id === 'string' ? f.id.trim() : '';
|
|
4092
|
-
if (!id) {
|
|
4093
|
-
at('HITL_FIELD_NO_ID', `Human task "${step}" has a form field with no id (${where}). The id is the key the `
|
|
4094
|
-
+ `reviewer's answer comes back under, so a field without one is dropped.`, `id: 'amount'`);
|
|
4095
|
-
return;
|
|
4096
|
-
}
|
|
4097
|
-
if (seen.has(id)) {
|
|
4098
|
-
at('HITL_DUP_FIELD', `Human task "${step}" declares the form field "${id}" twice. The answer object is keyed `
|
|
4099
|
-
+ `by field id, so the second silently replaces the first.`, `Rename one of them.`);
|
|
4100
|
-
return;
|
|
4101
|
-
}
|
|
4102
|
-
seen.add(id);
|
|
4103
|
-
if (RESERVED_ANSWER_IDS.has(id)) {
|
|
4104
|
-
at('HITL_FIELD_RESERVED_ID', `Human task "${step}" has a form field named "${id}", which the task's own result already `
|
|
4105
|
-
+ `uses (its output object carries "Action", and the node publishes "output" and "status"). `
|
|
4106
|
-
+ `The platform drops a field that collides with those.`, `Rename it, e.g. id: '${id}Value'.`);
|
|
4107
|
-
return;
|
|
4108
|
-
}
|
|
4109
|
-
if (!FORM_FIELD_TYPES.includes(String(f?.type))) {
|
|
4110
|
-
at('HITL_FIELD_TYPE_UNKNOWN', `Human task "${step}" gives the form field "${id}" the type ${JSON.stringify(f?.type)}, `
|
|
4111
|
-
+ `${formFieldTypeWhy(f?.type)}. Valid: ${FORM_FIELD_TYPES.join(', ')}.`, formFieldTypeHint(f?.type));
|
|
4112
|
-
}
|
|
4113
|
-
if (isAnswerField(f))
|
|
4114
|
-
answers++; // an `output`, or an `inOut` whose reviewed value comes back
|
|
4115
|
-
if (f?.direction === 'input' || f?.direction === 'inOut') {
|
|
4116
|
-
const value = f.value;
|
|
4117
|
-
if (value === undefined || value === null) {
|
|
4118
|
-
at('HITL_FIELD_NO_VALUE', `Human task "${step}" shows the form field "${id}" to the reviewer but gives it no value, `
|
|
4119
|
-
+ `so the field renders empty — the flow says "show them this" and shows them nothing.`, `value: input('<flowInput>') // or out('<step>', '<field>')`);
|
|
4120
|
-
}
|
|
4121
|
-
else if (!(value instanceof Expr)) {
|
|
4122
|
-
at('HITL_FIELD_VALUE_NOT_REF', `Human task "${step}" gives the form field "${id}" a raw `
|
|
4123
|
-
+ `${Array.isArray(value) ? 'array' : typeof value} as its value. A shown field is filled `
|
|
4124
|
-
+ `from a variable PATH, not a constant — the platform reads the string as a lookup, so a `
|
|
4125
|
-
+ `literal resolves to nothing on the form.`, `value: input('<flowInput>')`);
|
|
4126
|
-
}
|
|
4127
|
-
else if (value.literal || !/^\$vars\.[A-Za-z0-9_]+(\.[A-Za-z0-9_]+)*$/.test(value.js.trim())) {
|
|
4128
|
-
// Same trap as a transform's `collection`: a computed expression resolves
|
|
4129
|
-
// locally and to nothing once deployed, where it is used as a path.
|
|
4130
|
-
at('HITL_FIELD_VALUE_NOT_PATH', `Human task "${step}" fills the form field "${id}" from the expression `
|
|
4131
|
-
+ `"${value.literal ? String(value.value) : value.js.trim().slice(0, 60)}". A field binding is `
|
|
4132
|
-
+ `a variable PATH, so a computed expression resolves to nothing on the deployed form. `
|
|
4133
|
-
+ `Compute it in a script step first and point the field at that step's output.`, `value: out('<scriptStep>')`);
|
|
4134
|
-
}
|
|
4135
|
-
}
|
|
4136
|
-
else if (f?.direction !== 'output') {
|
|
4137
|
-
at('HITL_FIELD_DIRECTION', `Human task "${step}" gives the form field "${id}" the direction `
|
|
4138
|
-
+ `${JSON.stringify(f?.direction)}. A field is shown to the `
|
|
4139
|
-
+ `reviewer ('input'), filled in by them ('output'), or pre-filled and editable ('inOut'); `
|
|
4140
|
-
+ `anything else appears in the form schema and is neither displayed nor read back.`, `direction: 'output'`);
|
|
4141
|
-
}
|
|
4142
|
-
});
|
|
4143
|
-
const outcomes = inputs?.outcomes;
|
|
4144
|
-
if (!Array.isArray(outcomes) || outcomes.length === 0) {
|
|
4145
|
-
at('HITL_NO_OUTCOMES', `Human task "${step}" declares no outcomes, so the reviewer has no button to complete the `
|
|
4146
|
-
+ `task with and the flow waits forever.`, `outcomes: ['Approve', 'Reject']`);
|
|
4147
|
-
return;
|
|
4148
|
-
}
|
|
4149
|
-
const names = new Set();
|
|
4150
|
-
const ids = new Map();
|
|
4151
|
-
outcomes.forEach((o, i) => {
|
|
4152
|
-
const name = (typeof o === 'string' ? o : o?.name)?.trim();
|
|
4153
|
-
// Two outcomes on one id would share one `outcome-<id>` port, so the
|
|
4154
|
-
// second's edge could never be told apart. An explicit `id` can collide
|
|
4155
|
-
// with another outcome's derived slug.
|
|
4156
|
-
const id = outcomeIdOf(o, i);
|
|
4157
|
-
const explicitId = typeof o === 'object' && o !== null ? o.id : undefined;
|
|
4158
|
-
if (explicitId !== undefined && (typeof explicitId !== 'string' || explicitId === '')) {
|
|
4159
|
-
at('HITL_OUTCOME_BAD_ID', `Human task "${step}" outcome ${JSON.stringify(name ?? i)} has an id that is not a non-empty string. `
|
|
4160
|
-
+ `Leave \`id\` out to use the slug of the name.`, `{ name: ${JSON.stringify(name ?? 'Approve')} }`);
|
|
4161
|
-
}
|
|
4162
|
-
else if (ids.has(id) && ids.get(id) !== String(name ?? '')) {
|
|
4163
|
-
// Same name, same id: HITL_DUP_OUTCOME below reports it once, as a name clash.
|
|
4164
|
-
at('HITL_DUP_OUTCOME_ID', `Human task "${step}" gives outcomes "${ids.get(id)}" and "${name}" the same id "${id}", so both would `
|
|
4165
|
-
+ `leave from one 'outcome-${id}' port. Give one of them a different \`id\`.`);
|
|
4166
|
-
}
|
|
4167
|
-
else {
|
|
4168
|
-
ids.set(id, String(name ?? ''));
|
|
4169
|
-
}
|
|
4170
|
-
if (!name) {
|
|
4171
|
-
at('HITL_OUTCOME_NO_NAME', `Human task "${step}" has an outcome with no name (outcomes[${i}]). The result carries the `
|
|
4172
|
-
+ `outcome's NAME — it is what "$vars.${step}.output.Action" holds and what the platform's `
|
|
4173
|
-
+ `own routing conditions compare against — so a nameless one can never be matched.`, `outcomes: ['Approve', 'Reject']`);
|
|
4174
|
-
return;
|
|
4175
|
-
}
|
|
4176
|
-
if (names.has(name)) {
|
|
4177
|
-
at('HITL_DUP_OUTCOME', `Human task "${step}" declares the outcome "${name}" twice. Only the name reaches the flow, `
|
|
4178
|
-
+ `so two outcomes sharing one are indistinguishable downstream.`, `Rename one of them.`);
|
|
4179
|
-
return;
|
|
4180
|
-
}
|
|
4181
|
-
names.add(name);
|
|
4182
|
-
const action = typeof o === 'string' ? undefined : o?.action;
|
|
4183
|
-
if (action !== undefined && action !== 'Continue' && action !== 'End') {
|
|
4184
|
-
at('HITL_OUTCOME_ACTION', `Human task "${step}" gives the outcome "${name}" the action ${JSON.stringify(action)}. Only `
|
|
4185
|
-
+ `the exact string 'End' means "end the process here"; anything else — including 'end' — is `
|
|
4186
|
-
+ `read as 'Continue'.`, `action: 'End'`);
|
|
4187
|
-
}
|
|
4188
|
-
});
|
|
4189
|
-
// Delivery: the channel ids and assignee types are closed platform
|
|
4190
|
-
// vocabularies; a typo silently falls back to tenant defaults at run time.
|
|
4191
|
-
const HITL_CHANNELS = ['Slack', 'teams', 'Email', 'ActionCenter'];
|
|
4192
|
-
const HITL_ASSIGNEE_TYPES = ['user', 'group', 'staticEmail', 'staticGroupName', 'workload', 'roundRobin', 'custom'];
|
|
4193
|
-
const rec = inputs?.recipient;
|
|
4194
|
-
if (rec !== undefined) {
|
|
4195
|
-
for (const ch of rec.channels ?? []) {
|
|
4196
|
-
if (!HITL_CHANNELS.includes(String(ch))) {
|
|
4197
|
-
const nearest = nearestName(String(ch), HITL_CHANNELS);
|
|
4198
|
-
at('HITL_CHANNEL_UNKNOWN', `Human task "${step}" delivers over the channel ${JSON.stringify(ch)}, which the platform `
|
|
4199
|
-
+ `does not recognize — valid: ${HITL_CHANNELS.join(', ')} (Microsoft Teams really is the `
|
|
4200
|
-
+ `lowercase 'teams').`, nearest !== undefined ? `channels: ['${nearest}']` : undefined);
|
|
4201
|
-
}
|
|
4202
|
-
}
|
|
4203
|
-
const at2 = rec.assignee?.type;
|
|
4204
|
-
if (!HITL_ASSIGNEE_TYPES.includes(String(at2))) {
|
|
4205
|
-
const nearest = nearestName(String(at2), HITL_ASSIGNEE_TYPES);
|
|
4206
|
-
at('HITL_RECIPIENT_TYPE', `Human task "${step}" assigns its recipient with the type ${JSON.stringify(at2)}. Valid: `
|
|
4207
|
-
+ `${HITL_ASSIGNEE_TYPES.join(', ')}.`, nearest !== undefined ? `assignee: { type: '${nearest}' }` : undefined);
|
|
4208
|
-
}
|
|
4209
|
-
else if (at2 === 'custom' && !(typeof rec.assignee?.value === 'string' && rec.assignee.value.trim())) {
|
|
4210
|
-
diags.push({
|
|
4211
|
-
level: 'warning', code: 'HITL_RECIPIENT_VALUE', step,
|
|
4212
|
-
message: `Human task "${step}" uses a custom assignment with no value — the engine gets an `
|
|
4213
|
-
+ `empty custom-assignees expression and falls back to the tenant default.`,
|
|
4214
|
-
suggestion: `assignee: { type: 'custom', value: '<expression>' }`,
|
|
4215
|
-
});
|
|
4216
|
-
}
|
|
4217
|
-
}
|
|
4218
|
-
// A form with nothing to fill in and one outcome is a notification, not a task
|
|
4219
|
-
// — but it is a legitimate one (an acknowledgement step), so this is not an
|
|
4220
|
-
// error. Two or more outcomes with no answer fields is the common approval
|
|
4221
|
-
// shape and equally fine. Nothing to report either way.
|
|
4222
|
-
void answers;
|
|
4223
|
-
}
|
|
4224
|
-
/**
|
|
4225
|
-
* A read of a human task's output: `out('<step>', '<field>')`.
|
|
4226
|
-
*
|
|
4227
|
-
* The node publishes an object keyed by the form's ANSWER fields plus `Action`.
|
|
4228
|
-
* Two mistakes are silent at run time and both are caught here:
|
|
4229
|
-
*
|
|
4230
|
-
* - reading a field the form does not answer (a shown field, or a typo) —
|
|
4231
|
-
* `undefined`, offline and deployed;
|
|
4232
|
-
* - reading `$vars.<step>.status`, the platform's own projection of the chosen
|
|
4233
|
-
* outcome. It resolves *deployed*, so the flow works in production and reads
|
|
4234
|
-
* `undefined` on this SDK's offline rungs — a divergence, which is why the read
|
|
4235
|
-
* is redirected rather than merely flagged. (A runner that derives declared
|
|
4236
|
-
* output variables resolves `status` offline as well; the surface still points
|
|
4237
|
-
* at one spelling so a flow reads the same everywhere.)
|
|
4238
|
-
*/
|
|
4239
|
-
function checkHitlRead(name, field, rest, form, step, diags) {
|
|
4240
|
-
if (field === undefined) {
|
|
4241
|
-
// `out('review')` — the whole answer object. Legitimate (a script may want
|
|
4242
|
-
// all of it), same as a subflow's bare read being legitimate.
|
|
4243
|
-
return;
|
|
4244
|
-
}
|
|
4245
|
-
if (field === 'status') {
|
|
4246
|
-
diags.push({
|
|
4247
|
-
level: 'error',
|
|
4248
|
-
code: 'HITL_READ_STATUS',
|
|
4249
|
-
message: `"$vars.${name}.status" (read in "${step}") is the platform's own projection of the chosen `
|
|
4250
|
-
+ `outcome, not part of the answer object. Read the outcome from the output instead, which is `
|
|
4251
|
-
+ `the spelling flow-v1's routing conditions use and the one every rung here resolves.`,
|
|
4252
|
-
step,
|
|
4253
|
-
suggestion: `out('${name}', 'Action')`,
|
|
4254
|
-
});
|
|
4255
|
-
return;
|
|
4256
|
-
}
|
|
4257
|
-
if (field !== 'output') {
|
|
4258
|
-
diags.push({
|
|
4259
|
-
level: 'error',
|
|
4260
|
-
code: 'HITL_READ_NOT_OUTPUT',
|
|
4261
|
-
message: `Human task "${name}" publishes its result under "output", but "$vars.${name}.${field}" `
|
|
4262
|
-
+ `(read in "${step}") asks for "${field}", which is undefined at run time.`,
|
|
4263
|
-
step,
|
|
4264
|
-
suggestion: `out('${name}', '<fieldId>')`,
|
|
4265
|
-
});
|
|
4266
|
-
return;
|
|
4267
|
-
}
|
|
4268
|
-
// `out(name, 'x')` renders as `$vars.<name>.output.x`, so the answer field is
|
|
4269
|
-
// the first segment of `rest` — which arrives with its leading dot
|
|
4270
|
-
// (`.approved`), the same shape `checkSubflowRead` receives.
|
|
4271
|
-
const answer = (rest ?? '').replace(/^\./, '').split('.')[0];
|
|
4272
|
-
if (!answer)
|
|
4273
|
-
return; // `$vars.<name>.output` — the whole object, fine.
|
|
4274
|
-
if (answer === 'Action')
|
|
4275
|
-
return; // which outcome was pressed.
|
|
4276
|
-
const answers = (form?.fields ?? [])
|
|
4277
|
-
.filter((f) => isAnswerField(f) && typeof f?.id === 'string')
|
|
4278
|
-
.map((f) => f.id);
|
|
4279
|
-
if (answers.includes(answer))
|
|
4280
|
-
return;
|
|
4281
|
-
const shown = (form?.fields ?? [])
|
|
4282
|
-
.filter((f) => f?.direction === 'input' && typeof f?.id === 'string')
|
|
4283
|
-
.map((f) => f.id);
|
|
4284
|
-
const nearest = nearestName(answer, [...answers, 'Action']);
|
|
4285
|
-
diags.push({
|
|
4286
|
-
level: 'error',
|
|
4287
|
-
code: 'HITL_READ_UNKNOWN_FIELD',
|
|
4288
|
-
message: `Human task "${name}" has no answer "${answer}" (read as "$vars.${name}.output.${answer}" in `
|
|
4289
|
-
+ `"${step}"). Its result carries only the fields the reviewer FILLS IN or EDITS`
|
|
4290
|
-
+ `${answers.length > 0 ? ` (${answers.join(', ')})` : ' (it declares none)'} plus "Action". `
|
|
4291
|
-
+ (shown.includes(answer)
|
|
4292
|
-
? `"${answer}" is a field the reviewer only reads, so it is not part of the answer — read the `
|
|
4293
|
-
+ `value you bound to it instead.`
|
|
4294
|
-
: 'Nothing else resolves at run time.'),
|
|
4295
|
-
step,
|
|
4296
|
-
...(nearest !== undefined ? { suggestion: `out('${name}', '${nearest}')` } : {}),
|
|
4297
|
-
});
|
|
4298
|
-
}
|
|
4299
3918
|
/**
|
|
4300
3919
|
* Flag a case value whose type cannot match the discriminant's declared type.
|
|
4301
3920
|
*
|
|
@@ -4490,16 +4109,6 @@ function collectActionExprs(s, out, errorSource) {
|
|
|
4490
4109
|
if (val && typeof val === 'object')
|
|
4491
4110
|
return Object.values(val).forEach(pushDeep);
|
|
4492
4111
|
};
|
|
4493
|
-
if (s.spec.kind === 'hitl') {
|
|
4494
|
-
// A human task's only expressions are the values its shown fields display.
|
|
4495
|
-
// Without this a typo'd reference inside a form field would reach the
|
|
4496
|
-
// deployed task as an unresolvable path and show the reviewer nothing.
|
|
4497
|
-
for (const f of s.spec.inputs?.fields ?? []) {
|
|
4498
|
-
if (f?.direction === 'input')
|
|
4499
|
-
push(f.value);
|
|
4500
|
-
}
|
|
4501
|
-
return;
|
|
4502
|
-
}
|
|
4503
4112
|
if (s.spec.kind === 'subflow') {
|
|
4504
4113
|
// The values passed INTO the child are parent-scope expressions, so they
|
|
4505
4114
|
// resolve against the parent's declared names like any other reference.
|