@uipath/maestro-builder-sdk 6.24.0 → 6.25.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/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 { sdkFormFieldType } from './core/form-field-type.js';
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.