@uipath/maestro-builder-sdk 6.70.6 → 6.70.7

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.
@@ -18,20 +18,79 @@
18
18
  import { checkExpression } from '../core/expr-check.js';
19
19
  import { CompileRefusal } from '../core/compile-refusal.js';
20
20
  import { defaultActionOutputVar } from './action-output-var.js';
21
+ import { lowerTaskOutputRefs, pickByName, pickOutputRow, taskOutputRefs, unresolvedTaskOutputRef } from './task-output-ref.js';
21
22
  const NOUN = 'case variable';
22
23
  const NAMESPACE = 'vars';
23
- function checkExpr(expr, where, roots, out) {
24
+ /**
25
+ * The root a task-output reference stands in for while the rest of the
26
+ * expression is checked: `serialize` lowers the reference to the output's id,
27
+ * which only it knows, so the check reads it as a declared root.
28
+ */
29
+ const TASK_OUTPUT_ROOT = '$taskOutput';
30
+ /**
31
+ * The task kinds whose output rows are all authored in `.outputs()`: a
32
+ * reference to any other name on them names nothing. Every other kind also
33
+ * writes rows of its own (a connector's `response` / `Error`, a business
34
+ * rule's `output`, an action's form fields), so for those `build()` resolves
35
+ * the stage and the task, and `serialize` resolves the output against the rows
36
+ * it wrote.
37
+ */
38
+ const AUTHORED_OUTPUT_KINDS = new Set(['process', 'agent', 'rpa', 'api-workflow', 'case-management', 'flow-process', 'function']);
39
+ /** Why `ref` names no output in `built`, or `undefined` when it may name one. */
40
+ function taskOutputRefProblem(built, ref) {
41
+ const stage = pickByName([...new Set(built.stages.map((s) => s.label))], ref.stage, (label) => [label]);
42
+ if (stage === undefined)
43
+ return `names no stage labelled ${JSON.stringify(ref.stage)}`;
44
+ if (stage === 'ambiguous')
45
+ return 'names a stage label that more than one stage answers to';
46
+ const tasks = built.stages.filter((s) => s.label === stage).flatMap((s) => s.lanes.flat());
47
+ const task = pickByName(tasks, ref.task, (t) => [t.name]);
48
+ if (task === undefined)
49
+ return `names no task ${JSON.stringify(ref.task)} in stage ${JSON.stringify(stage)}`;
50
+ if (task === 'ambiguous')
51
+ return `names a task that more than one task in stage ${JSON.stringify(stage)} answers to`;
52
+ if (!AUTHORED_OUTPUT_KINDS.has(task.kind))
53
+ return undefined;
54
+ // The rows `emitTaskOutput` writes: named after the source's last segment
55
+ // (a custom row after its variable), writing `var`.
56
+ const rows = (task.outputs ?? []).map((o) => ({
57
+ name: o.custom ? o.var : o.source.replace(/^=/, '').split('.').pop() || o.source,
58
+ var: o.var,
59
+ }));
60
+ const row = pickOutputRow(rows, ref.output);
61
+ if (row === undefined) {
62
+ const offered = rows.map((r) => JSON.stringify(r.name));
63
+ return `names no output of task ${JSON.stringify(task.name)}, whose outputs are ${offered.length ? offered.join(', ') : 'none'}`;
64
+ }
65
+ if (row === 'ambiguous')
66
+ return `names more than one output of task ${JSON.stringify(task.name)}; name it by the variable it writes instead`;
67
+ return undefined;
68
+ }
69
+ /** Every task-output reference in `value` that names nothing in `built`. */
70
+ function checkTaskOutputRefs(built, value, where, out) {
71
+ if (typeof value !== 'string')
72
+ return;
73
+ for (const ref of taskOutputRefs(value)) {
74
+ const why = taskOutputRefProblem(built, ref);
75
+ if (why)
76
+ out.push({ level: 'error', code: 'TASK_OUTPUT_REF_UNRESOLVED', message: unresolvedTaskOutputRef(ref, why), where });
77
+ }
78
+ }
79
+ function checkExpr(built, expr, where, roots, out) {
24
80
  if (!expr)
25
81
  return;
26
- for (const d of checkExpression(expr, { roots, noun: NOUN, namespace: NAMESPACE })) {
82
+ checkTaskOutputRefs(built, expr, where, out);
83
+ const lowered = lowerTaskOutputRefs(expr, () => TASK_OUTPUT_ROOT);
84
+ const scope = lowered === expr ? roots : new Set([...roots, TASK_OUTPUT_ROOT]);
85
+ for (const d of checkExpression(lowered, { roots: scope, noun: NOUN, namespace: NAMESPACE })) {
27
86
  out.push({ ...d, where });
28
87
  }
29
88
  }
30
89
  /** Every rule's `=js:` gate across an AND/OR rule matrix. */
31
- function checkRules(rules, where, roots, out) {
90
+ function checkRules(built, rules, where, roots, out) {
32
91
  for (const group of rules ?? [])
33
92
  for (const r of group)
34
- checkExpr(r.expression, where, roots, out);
93
+ checkExpr(built, r.expression, where, roots, out);
35
94
  }
36
95
  /** Every first-level expression problem in a built case, each stamped with its location. */
37
96
  export function checkCaseExpressions(built) {
@@ -58,21 +117,32 @@ export function checkCaseExpressions(built) {
58
117
  for (const o of t.eventOutputs ?? [])
59
118
  roots.add(o.var);
60
119
  const out = [];
61
- built.slaRules.forEach((sla, i) => checkExpr(sla.expression, `case SLA #${i + 1}`, roots, out));
62
- built.caseExit.forEach((ce, i) => checkRules(ce.rules, `case completion #${i + 1}`, roots, out));
120
+ built.slaRules.forEach((sla, i) => checkExpr(built, sla.expression, `case SLA #${i + 1}`, roots, out));
121
+ built.caseExit.forEach((ce, i) => checkRules(built, ce.rules, `case completion #${i + 1}`, roots, out));
63
122
  for (const stage of built.stages) {
64
123
  const at = `stage "${stage.label}"`;
65
124
  for (const ec of stage.entry)
66
- checkRules(ec.rules, `${at} entry`, roots, out);
125
+ checkRules(built, ec.rules, `${at} entry`, roots, out);
67
126
  for (const xc of stage.exit)
68
- checkRules(xc.rules, `${at} exit`, roots, out);
69
- stage.slaRules.forEach((sla, i) => checkExpr(sla.expression, `${at} SLA #${i + 1}`, roots, out));
127
+ checkRules(built, xc.rules, `${at} exit`, roots, out);
128
+ stage.slaRules.forEach((sla, i) => checkExpr(built, sla.expression, `${at} SLA #${i + 1}`, roots, out));
70
129
  for (const lane of stage.lanes) {
71
130
  for (const task of lane) {
72
131
  const tat = `task "${task.name}" in ${at}`;
73
- checkExpr(task.skip, `${tat} skipWhen`, roots, out);
132
+ checkExpr(built, task.skip, `${tat} skipWhen`, roots, out);
133
+ // A value that is not a gate can carry a reference too: a task input, an
134
+ // action form input, a custom output. Only the reference is checked
135
+ // here (these values are not gates, so their other reads are not), so a
136
+ // bad one fails `build()` as it would in a gate, not only at compile.
137
+ for (const inp of task.inputs ?? [])
138
+ checkTaskOutputRefs(built, inp.value, `${tat} input "${inp.name}"`, out);
139
+ for (const f of task.action?.inputs ?? [])
140
+ checkTaskOutputRefs(built, f.value, `${tat} action input "${f.name}"`, out);
141
+ for (const o of task.outputs ?? [])
142
+ if (o.custom)
143
+ checkTaskOutputRefs(built, o.source, `${tat} output "${o.var}"`, out);
74
144
  for (const ec of task.entry)
75
- checkRules(ec.rules, `${tat} entry`, roots, out);
145
+ checkRules(built, ec.rules, `${tat} entry`, roots, out);
76
146
  }
77
147
  }
78
148
  }
@@ -99,6 +99,35 @@ export interface CaseRule extends RuleOpts {
99
99
  * @returns A rule to pass to `entryWhen` / `exitWhen` / `completeWhen`.
100
100
  */
101
101
  export declare function rule(type: CaseRuleType, opts?: RuleOpts): CaseRule;
102
+ /**
103
+ * Read another task's output in place, in a `=js:` expression or a task input:
104
+ * `=vars.<id>` once compiled, where `<id>` is the id the plan gives that output.
105
+ * No case variable is needed, so a plan built from an SDD carries only the
106
+ * variables the SDD declares.
107
+ *
108
+ * It returns the SDD's own `$xref` marker, so an SDD cell that carries one
109
+ * can also be copied as written. The output is named by the
110
+ * result field it reads or by its key in `.outputs()`.
111
+ *
112
+ * @example
113
+ * **Pass one task's result to the next, and gate on it**
114
+ * ```ts
115
+ * casePlan('expense').stage('Stage 1', (s) => s
116
+ * .task('Analyze', (t) => t.agent('Analyzer', { folder: 'Shared' })
117
+ * .outputs({ AgentExpenseRequest: '=AgentExpenseRequest' }))
118
+ * .task('Process', (t) => t.process('Processor', { folder: 'Shared' })
119
+ * .inputs({ request: `=${taskOutput('Stage 1', 'Analyze', 'AgentExpenseRequest')}` })
120
+ * .skipWhen(`=js:${taskOutput('Stage 1', 'Analyze', 'AgentExpenseRequest')}.amount > 1000`)))
121
+ * ```
122
+ * compiles the input to `=vars.AgentExpenseRequest` and the gate to
123
+ * `=js:vars.AgentExpenseRequest?.amount > 1000`.
124
+ *
125
+ * @param stage - The producing task's stage label.
126
+ * @param task - The producing task's name.
127
+ * @param output - The output: the result field it reads (`AgentExpenseRequest`, `response`) or its `.outputs()` key.
128
+ * @returns The reference, to place inside an expression string.
129
+ */
130
+ export declare function taskOutput(stage: string, task: string, output: string): string;
102
131
  /**
103
132
  * A pure expression gate. The receiving condition slot supplies its canonical
104
133
  * event: `case-entered` for stage entry, `selected-tasks-completed` for stage
@@ -1219,9 +1248,21 @@ declare class TaskBuilder {
1219
1248
  * name equals the field — an SDD row `estimatedAge -> estimatedAge` is
1220
1249
  * `.var('estimatedAge', 'number')` plus
1221
1250
  * `.outputs({ estimatedAge: { source: '=estimatedAge', type: 'number' } })`.
1222
- * An output that keeps its field's name with no such declaration is the
1223
- * task's own (bare) output, with no root entry — until a second task writes
1224
- * the same variable, which makes both reassignments.
1251
+ *
1252
+ * An output that keeps its field's name (the key is the source's last
1253
+ * segment: `AgentExpenseRequest: '=AgentExpenseRequest'`, or
1254
+ * `app_id: '=response.app_id'` on a connector task) with no such declaration
1255
+ * is the task's own (bare) output: it stays on the task (`id == var`) and
1256
+ * writes no case variable. Two exceptions: a `.var()` of that name (the row
1257
+ * then feeds it), and a second task with the same key, which makes one shared
1258
+ * case variable that both write (reassigned rows plus a root entry). To keep
1259
+ * a second task's output of the same field on its task, give it the
1260
+ * Designer's suffixed key: `Result2: '=Result'` is its own bare output. This
1261
+ * is the output an SDD lists with no `->` arrow. Read it elsewhere with
1262
+ * {@link taskOutput | taskOutput('<stage>', '<task>', '<output>')} (or the
1263
+ * SDD's `$xref` marker copied as written), which compiles to `vars.<id>`
1264
+ * of that output, so a plan built from an SDD needs no case variable the SDD
1265
+ * does not declare.
1225
1266
  *
1226
1267
  * Pass `{ assign, type }` for a **custom** output: the SDD's `x = expr` row.
1227
1268
  * When the task completes, the case variable is set to `assign` — a literal,
@@ -1,6 +1,7 @@
1
1
  import { connector as makeConnector, subscriptionFrom, } from '../core/actions.js';
2
2
  import { isTriggerDescriptor } from '../core/connectors.js';
3
3
  import { checkCaseExpressions, CaseBuildError } from './case-expr-check.js';
4
+ import { taskOutputMarker } from './task-output-ref.js';
4
5
  import { CASE_FORMAT_PROFILE, CASE_SCHEMA_VERSIONS, recordAuthoredCaseVersion } from './format-profile.js';
5
6
  import { schemaVersionRefusal } from '../schema-version.js';
6
7
  import { CompileRefusal } from '../core/compile-refusal.js';
@@ -19,6 +20,37 @@ export { CaseBuildError } from './case-expr-check.js';
19
20
  export function rule(type, opts = {}) {
20
21
  return { rule: type, ...opts };
21
22
  }
23
+ /**
24
+ * Read another task's output in place, in a `=js:` expression or a task input:
25
+ * `=vars.<id>` once compiled, where `<id>` is the id the plan gives that output.
26
+ * No case variable is needed, so a plan built from an SDD carries only the
27
+ * variables the SDD declares.
28
+ *
29
+ * It returns the SDD's own `$xref` marker, so an SDD cell that carries one
30
+ * can also be copied as written. The output is named by the
31
+ * result field it reads or by its key in `.outputs()`.
32
+ *
33
+ * @example
34
+ * **Pass one task's result to the next, and gate on it**
35
+ * ```ts
36
+ * casePlan('expense').stage('Stage 1', (s) => s
37
+ * .task('Analyze', (t) => t.agent('Analyzer', { folder: 'Shared' })
38
+ * .outputs({ AgentExpenseRequest: '=AgentExpenseRequest' }))
39
+ * .task('Process', (t) => t.process('Processor', { folder: 'Shared' })
40
+ * .inputs({ request: `=${taskOutput('Stage 1', 'Analyze', 'AgentExpenseRequest')}` })
41
+ * .skipWhen(`=js:${taskOutput('Stage 1', 'Analyze', 'AgentExpenseRequest')}.amount > 1000`)))
42
+ * ```
43
+ * compiles the input to `=vars.AgentExpenseRequest` and the gate to
44
+ * `=js:vars.AgentExpenseRequest?.amount > 1000`.
45
+ *
46
+ * @param stage - The producing task's stage label.
47
+ * @param task - The producing task's name.
48
+ * @param output - The output: the result field it reads (`AgentExpenseRequest`, `response`) or its `.outputs()` key.
49
+ * @returns The reference, to place inside an expression string.
50
+ */
51
+ export function taskOutput(stage, task, output) {
52
+ return taskOutputMarker(stage, task, output);
53
+ }
22
54
  /**
23
55
  * Gate a condition slot only on case data without naming its default event.
24
56
  *
@@ -505,9 +537,21 @@ class TaskBuilder {
505
537
  * name equals the field — an SDD row `estimatedAge -> estimatedAge` is
506
538
  * `.var('estimatedAge', 'number')` plus
507
539
  * `.outputs({ estimatedAge: { source: '=estimatedAge', type: 'number' } })`.
508
- * An output that keeps its field's name with no such declaration is the
509
- * task's own (bare) output, with no root entry — until a second task writes
510
- * the same variable, which makes both reassignments.
540
+ *
541
+ * An output that keeps its field's name (the key is the source's last
542
+ * segment: `AgentExpenseRequest: '=AgentExpenseRequest'`, or
543
+ * `app_id: '=response.app_id'` on a connector task) with no such declaration
544
+ * is the task's own (bare) output: it stays on the task (`id == var`) and
545
+ * writes no case variable. Two exceptions: a `.var()` of that name (the row
546
+ * then feeds it), and a second task with the same key, which makes one shared
547
+ * case variable that both write (reassigned rows plus a root entry). To keep
548
+ * a second task's output of the same field on its task, give it the
549
+ * Designer's suffixed key: `Result2: '=Result'` is its own bare output. This
550
+ * is the output an SDD lists with no `->` arrow. Read it elsewhere with
551
+ * {@link taskOutput | taskOutput('<stage>', '<task>', '<output>')} (or the
552
+ * SDD's `$xref` marker copied as written), which compiles to `vars.<id>`
553
+ * of that output, so a plan built from an SDD needs no case variable the SDD
554
+ * does not declare.
511
555
  *
512
556
  * Pass `{ assign, type }` for a **custom** output: the SDD's `x = expr` row.
513
557
  * When the task completes, the case variable is set to `assign` — a literal,
@@ -11,6 +11,7 @@ import { isUnrestorableSlotId, taskFingerprint } from './preserve.js';
11
11
  import { isInSolutionKey, SOLUTION_FOLDER } from './resource-identity.js';
12
12
  import { offlineBindingLabel } from './ids.js';
13
13
  import { connectorPlaceholderKindOf } from './connector-placeholder.js';
14
+ import { raiseTaskOutputReads, referableOutputIds } from './task-output-ref.js';
14
15
  const TASK_KIND_METHOD = {
15
16
  process: 'process',
16
17
  agent: 'agent',
@@ -1599,6 +1600,7 @@ function slugId(plan) {
1599
1600
  export function decompileWithDiagnostics(plan, options = {}) {
1600
1601
  const diagnostics = [];
1601
1602
  refuseUnsupportedAuthoring(plan);
1603
+ plan = raiseTaskOutputRefs(plan);
1602
1604
  const preservation = collectPreservation(plan);
1603
1605
  const refs = buildRefs(plan, options.library, options.bestEffort ? diagnostics : undefined);
1604
1606
  const selectNextStage = selectNextStageByExit(plan, refs);
@@ -1677,6 +1679,35 @@ export function decompileWithDiagnostics(plan, options = {}) {
1677
1679
  const source = `import { ${imports.join(', ')} } from ${tsLiteral(options.importSpecifier ?? './case-sdk.js')};\n\n${diagnosticHeader}${out.toString()}\n`;
1678
1680
  return { source, diagnostics };
1679
1681
  }
1682
+ /**
1683
+ * The plan with each read of a task output that recompiling could not keep,
1684
+ * `vars.<output id>`, written as the task-output reference that names the same
1685
+ * row (`vars.$xref('<stage>','<task>','<output>')`, what `taskOutput()` writes;
1686
+ * builder-sdk#1241). Recompiling lowers it back to `vars.<id>` of that row,
1687
+ * whatever id the row then gets. See `referableOutputIds` for which reads.
1688
+ */
1689
+ function raiseTaskOutputRefs(plan) {
1690
+ const variables = plan.variables ?? {};
1691
+ const roots = new Set();
1692
+ for (const lane of [variables.inputs, variables.outputs, variables.inputOutputs])
1693
+ for (const entry of lane ?? [])
1694
+ for (const name of [entry?.name, entry?.id])
1695
+ if (typeof name === 'string')
1696
+ roots.add(name);
1697
+ const byId = referableOutputIds(plan, roots);
1698
+ if (!byId.size)
1699
+ return plan;
1700
+ const raise = (value) => {
1701
+ if (typeof value === 'string')
1702
+ return raiseTaskOutputReads(value, byId);
1703
+ if (Array.isArray(value))
1704
+ return value.map(raise);
1705
+ if (value && typeof value === 'object')
1706
+ return Object.fromEntries(Object.entries(value).map(([name, field]) => [name, raise(field)]));
1707
+ return value;
1708
+ };
1709
+ return raise(plan);
1710
+ }
1680
1711
  export function decompile(plan, options = {}) {
1681
1712
  return decompileWithDiagnostics(plan, options).source;
1682
1713
  }
@@ -37,6 +37,7 @@ import { implicitInArgBuiltTrigger, unboundInArgMessage } from './in-arg-binding
37
37
  import { jsonClone } from '../core/json-clone.js';
38
38
  import { taskShapeProblems } from './task-shape.js';
39
39
  import { connectorPlaceholderData, isConnectorPlaceholder } from './connector-placeholder.js';
40
+ import { lowerTaskOutputRefs, planTaskOutputs, resolveInPlan, unresolvedTaskOutputRef } from './task-output-ref.js';
40
41
  /** The ids a serialize pass hashed from element ids, by the document it returned. */
41
42
  const ELEMENT_SEEDED_IDS = new WeakMap();
42
43
  function elementSeededId(resolver, elementId, suffix) {
@@ -404,8 +405,46 @@ function serializeCase(built, opts) {
404
405
  // repeats the way the platform's V31/V32.0.1 migrations would, so the file is
405
406
  // already what the platform reads (see rule-names.ts).
406
407
  dedupeConditionNames(plan);
408
+ // Over the finished document too: a task-output reference resolves to the id
409
+ // of the row it names, and the ids are only final here.
410
+ lowerPlanTaskOutputRefs(plan);
407
411
  return plan;
408
412
  }
413
+ /**
414
+ * Lower every `vars.$xref('<stage>','<task>','<output>')` marker (what
415
+ * `taskOutput()` writes, and what an SDD cell carries) in the written plan to
416
+ * `vars.<id>` of the output row it names, as `uip maestro case sdd convert`
417
+ * does (builder-sdk#1241). A marker that names no output is refused: left in
418
+ * place it reads nothing at run time, and the default `validate` profile
419
+ * reports only that the marker survived.
420
+ */
421
+ function lowerPlanTaskOutputRefs(plan) {
422
+ let tasks;
423
+ const lower = (text) => lowerTaskOutputRefs(text, (ref) => {
424
+ tasks ??= planTaskOutputs(plan);
425
+ const resolved = resolveInPlan(tasks, ref);
426
+ if ('why' in resolved)
427
+ throw new CompileRefusal('TASK_OUTPUT_REF_UNRESOLVED', unresolvedTaskOutputRef(ref, resolved.why));
428
+ // A stand-in exists only in collect mode, which writes no plan: the marker is
429
+ // left, and the stood-in task's own refusal is what `check` reports.
430
+ return 'standIn' in resolved ? undefined : resolved.variable;
431
+ });
432
+ const walk = (value) => {
433
+ if (typeof value === 'string')
434
+ return value.includes('$xref') ? lower(value) : value;
435
+ if (Array.isArray(value)) {
436
+ for (let i = 0; i < value.length; i++)
437
+ value[i] = walk(value[i]);
438
+ }
439
+ else if (value && typeof value === 'object') {
440
+ const record = value;
441
+ for (const key of Object.keys(record))
442
+ record[key] = walk(record[key]);
443
+ }
444
+ return value;
445
+ };
446
+ walk(plan);
447
+ }
409
448
  /** The task types whose `data.context[]` carries an Integration Service activity configuration. */
410
449
  const CONNECTOR_CONFIG_TASK_TYPES = new Set([
411
450
  'execute-connector-activity',
@@ -866,6 +905,17 @@ function applyPreservedIdentities(result, preservation) {
866
905
  row.value = wanted;
867
906
  });
868
907
  };
908
+ // Rows `reassignCarried` turned back into reassignments, and the variables
909
+ // they write: each needs its root companion (added below, before the root
910
+ // variable ids are restored).
911
+ const carriedCompanions = new Map();
912
+ const reassignCarried = (row, id) => {
913
+ row.id = id;
914
+ row.originalVar = id;
915
+ row.target = `=${id}`;
916
+ if (!carriedCompanions.has(row.var))
917
+ carriedCompanions.set(row.var, { id: row.var, name: row.var, type: row.type, elementId: 'root', custom: true });
918
+ };
869
919
  const nodes = result.nodes;
870
920
  const stages = nodes.filter((node) => String(node.type).endsWith('Stage'));
871
921
  const bagField = CASE_FORMAT_PROFILE.waitConnectorRuleBagField;
@@ -902,6 +952,19 @@ function applyPreservedIdentities(result, preservation) {
902
952
  // A positional match never takes a row a name match owns.
903
953
  : named.has(outputRows[index]?.name) ? undefined : outputRows[index];
904
954
  const currentId = target?.id;
955
+ // A nested own-name row (`app_id: '=response.app_id'`, written bare since
956
+ // builder-sdk#1241) whose carried id is not its variable is a row an
957
+ // SDK 6.67.1-era plan wrote reassigned, with a root companion that
958
+ // readers spell (`=vars.app_id`). It keeps that shape: the carried slot id
959
+ // goes on the row, `var` stays, and the companion comes back. Decided
960
+ // here, on identities `followRenames` has already paired, so a renamed
961
+ // task or stage keeps it too.
962
+ if (target && typeof row.id === 'string' && isCarriedNestedReassignment(target, row.id) && !heldOutputIds.has(row.id)) {
963
+ reassignCarried(target, row.id);
964
+ heldOutputIds.delete(currentId);
965
+ heldOutputIds.add(row.id);
966
+ return;
967
+ }
905
968
  // An own-name row's id IS its variable's name (`emitTaskOutput`). A
906
969
  // preserved id that differs — an `o<hash>` slot from a plan an older
907
970
  // SDK wrote — would rename the variable out from under every
@@ -957,6 +1020,20 @@ function applyPreservedIdentities(result, preservation) {
957
1020
  takeConditions(metadata.caseExitRules, identities.caseExitRules);
958
1021
  takeSlas(metadata.slaRules, identities.slaRules);
959
1022
  const variables = result.variables;
1023
+ if (carriedCompanions.size) {
1024
+ const inputOutputs = (variables.inputOutputs ??= []);
1025
+ const rootIds = new Set([...(variables.inputs ?? []), ...(variables.outputs ?? []), ...inputOutputs].map((entry) => entry?.id));
1026
+ for (const [name, companion] of carriedCompanions)
1027
+ if (!rootIds.has(name))
1028
+ inputOutputs.push(companion);
1029
+ // Any other row that writes the variable under its own name now aliases the
1030
+ // root entry, the same rule `serialize` applies (`originalVar == id`).
1031
+ for (const stage of stages)
1032
+ for (const task of (stage.data?.tasks ?? []).flat())
1033
+ for (const row of task?.data?.outputs ?? [])
1034
+ if (carriedCompanions.has(row?.var) && row.id === row.var && row.originalVar === undefined)
1035
+ row.originalVar = row.id;
1036
+ }
960
1037
  for (const [lane, rows] of Object.entries(identities.variables ?? {}))
961
1038
  (rows ?? []).forEach((row) => {
962
1039
  if (isUnrestorableSlotId(lane, row))
@@ -1722,13 +1799,16 @@ const OWN_NAME_OUTPUT_ROWS = new WeakSet();
1722
1799
  * One `data.outputs[]` row: `source` field → the `var` case variable. Two shapes,
1723
1800
  * as the Case Designer writes them:
1724
1801
  *
1725
- * - **own name** — the output keeps its field's name (a top-level `source` whose
1726
- * field is the `var`, or the Designer's camelCase of it, either one optionally
1802
+ * - **own name** — the output keeps its field's name (the `var` is the source's
1803
+ * last segment, or the Designer's camelCase of it, either one optionally
1727
1804
  * with a numeric suffix: `=APIOutput1` into `APIOutput1`, `aPIOutput1` or
1728
- * `aPIOutput12`; see `keepsOwnName`). The row's `id` IS the `var`, with no
1729
- * `originalVar`: `{ id: 'APIOutput1', var: 'APIOutput1', target: '=APIOutput1' }`.
1805
+ * `aPIOutput12`, and `=response.app_id` into `app_id`; see `keepsOwnName`).
1806
+ * The row's `id` IS the `var`, with no `originalVar`:
1807
+ * `{ id: 'APIOutput1', var: 'APIOutput1', target: '=APIOutput1' }`. This is
1808
+ * the task-scoped output: it writes no case variable, and a later task reads
1809
+ * it in place, `=vars.<id>`, or by `taskOutput(stage, task, output)`.
1730
1810
  * - **reassigned** — the output feeds a differently named variable (`=age` into
1731
- * `estimatedAge`, or any nested `=Error.Message`). The row keeps its own minted
1811
+ * `estimatedAge`, `=Error.Message` into `errorMsg`). The row keeps its own minted
1732
1812
  * slot `id`, points `var` at the case variable, and repeats the slot in
1733
1813
  * `originalVar`, which is how the Designer recognises a reassignment.
1734
1814
  *
@@ -2277,12 +2357,42 @@ function claimOutputId(resolver, base) {
2277
2357
  resolver.outputIds.add(id);
2278
2358
  return id;
2279
2359
  }
2360
+ /**
2361
+ * Whether `row` is a nested own-name output (a dotted `source` whose last
2362
+ * segment keeps its name, written with `id == var`) and the carried plan gave
2363
+ * it a different id, i.e. wrote it reassigned. See `applyPreservedIdentities`.
2364
+ *
2365
+ * An own-name row has two shapes, and both count: bare (no `originalVar`), and
2366
+ * the alias of a root variable of the same name (`originalVar == id`), which is
2367
+ * how it is written when a `.var()` declares that variable. Example: the
2368
+ * phase5-1 business-rule fixture declares `riskBand` and carries
2369
+ * `{ id: 'oMvpql9VW', var: 'riskBand', originalVar: 'oMvpql9VW' }`; it compiles
2370
+ * as the alias `{ id: 'riskBand', originalVar: 'riskBand' }` and is a carried
2371
+ * reassignment all the same. A row whose `originalVar` names another id is a
2372
+ * reassignment already, and `id == var` rules it out above.
2373
+ */
2374
+ function isCarriedNestedReassignment(row, carriedId) {
2375
+ if (!row || row.custom === true || typeof row.id !== 'string' || row.id !== row.var)
2376
+ return false;
2377
+ if (row.originalVar !== undefined && row.originalVar !== row.id)
2378
+ return false;
2379
+ if (carriedId === row.id || typeof row.source !== 'string')
2380
+ return false;
2381
+ const path = row.source.replace(/^=/, '');
2382
+ return path.includes('.') && keepsOwnName(row.var, path.split('.').pop() ?? '');
2383
+ }
2280
2384
  /** Whether a task output is written in the own-name shape (see `emitTaskOutput`). */
2281
2385
  function isOwnNameOutput(o) {
2282
2386
  if (o.custom)
2283
2387
  return false;
2284
- const path = o.source.replace(/^=/, '');
2285
- return !path.includes('.') && keepsOwnName(o.var, path);
2388
+ // The field is the path's last segment, as the row's `name` is: a connector
2389
+ // task's `app_id: '=response.app_id'` keeps its field's name exactly as
2390
+ // `=APIOutput1` into `APIOutput1` does. `uip maestro case sdd convert` writes
2391
+ // that bare row (`outputFor`: `{ id: 'app_id', var: 'app_id', source:
2392
+ // '=response.app_id' }`), so it stays on the task with no root companion
2393
+ // (builder-sdk#1241).
2394
+ const field = o.source.replace(/^=/, '').split('.').pop() ?? '';
2395
+ return keepsOwnName(o.var, field);
2286
2396
  }
2287
2397
  /**
2288
2398
  * Every variable a task in the plan writes under its own name: own-name task
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Reading another task's output in place: the SDD's `vars.$xref('<stage>',
3
+ * '<task>', '<output>')` marker, and `taskOutput()`, the typed way to write it
4
+ * (builder-sdk#1241).
5
+ *
6
+ * A task's own output is a variable of its own: the Case Designer saves it as
7
+ * `{ name: 'AgentExpenseRequest', id: 'agentExpenseRequest', var:
8
+ * 'agentExpenseRequest' }`, with no root `variables` entry, and a later task
9
+ * reads it as `=vars.agentExpenseRequest`. The id is the plan's, not the
10
+ * author's: it is unique across the plan, so a second task's `Action` output
11
+ * is `action2`. An SDD therefore names an output by where it is produced, and
12
+ * `uip maestro case sdd convert` lowers each marker to `vars.<output id>`
13
+ * (`sdd-convert/xrefs.ts`). Without the lowering here, an author had to copy
14
+ * the value into a case variable the SDD does not declare, and `validate
15
+ * --strict --sdd` refuses each one (`STRICT_SDD_VARIABLE_EXTRA`).
16
+ *
17
+ * The rule lives here so `check` (at `build()`) and `serialize` read a marker
18
+ * the same way. `build()` cannot see the ids, which `serialize` mints, so it
19
+ * resolves the stage and the task, and the output where the task's rows are
20
+ * all authored (a reference-mode task). `serialize` resolves every marker
21
+ * against the rows it wrote and refuses one that names no output.
22
+ */
23
+ /** The marker's three arguments. */
24
+ export interface TaskOutputRef {
25
+ stage: string;
26
+ task: string;
27
+ output: string;
28
+ }
29
+ /** An output row, as far as a reference to it is concerned. */
30
+ export interface OutputRowKey {
31
+ /** The row's `name`: the result field it reads (`AgentExpenseRequest`, `response`). */
32
+ name?: string;
33
+ /** The variable the row writes. */
34
+ var?: string;
35
+ }
36
+ /** Each marker in `text`, in order, with where it sits. */
37
+ export declare function taskOutputRefs(text: string): Array<TaskOutputRef & {
38
+ whole: string;
39
+ index: number;
40
+ }>;
41
+ /**
42
+ * Replace every marker in `text` with what `lower` returns for it. In a `=js:`
43
+ * expression, a marker followed by a property read gets the optional chain
44
+ * (`vars.<id>?.field`); any other value keeps the plain read (`vars.<id>.field`).
45
+ *
46
+ * @param text - The expression.
47
+ * @param lower - The variable a marker reads (`agentExpenseRequest`), or `undefined` to keep the marker.
48
+ */
49
+ export declare function lowerTaskOutputRefs(text: string, lower: (ref: TaskOutputRef) => string | undefined): string;
50
+ /**
51
+ * The one candidate `name` picks out: an exact match first, then a
52
+ * case-insensitive one (an SDD addresses a stage or an output in any case).
53
+ * `undefined` when none does; `'ambiguous'` when more than one does.
54
+ */
55
+ export declare function pickByName<T>(items: readonly T[], name: string, names: (item: T) => Array<string | undefined>): T | 'ambiguous' | undefined;
56
+ /**
57
+ * The row an output reference names among one task's rows: by the field it
58
+ * reads (`name`) or by the variable it writes (`var`).
59
+ */
60
+ export declare function pickOutputRow<R extends OutputRowKey>(rows: readonly R[], output: string): R | 'ambiguous' | undefined;
61
+ /** The refusal message for a marker that resolves to nothing. */
62
+ export declare function unresolvedTaskOutputRef(ref: TaskOutputRef, why: string): string;
63
+ /**
64
+ * The marker for a reference: `vars.$xref('<stage>','<task>','<output>')`,
65
+ * each argument single-quoted with its quotes and backslashes escaped. What
66
+ * `taskOutput()` returns, and what decompile writes back.
67
+ */
68
+ export declare function taskOutputMarker(stage: string, task: string, output: string): string;
69
+ /** One task's output rows in a written plan, under the names a reference uses. */
70
+ export interface PlanTaskOutputs {
71
+ stage: string;
72
+ task: string;
73
+ rows: Array<OutputRowKey & {
74
+ id?: string;
75
+ }>;
76
+ /**
77
+ * A refused task's stand-in, in `check`'s collect mode (`data.standIn`): it
78
+ * wrote no rows, so a reference to it cannot be resolved against them.
79
+ */
80
+ standIn?: boolean;
81
+ }
82
+ /** Every stage task's output rows in a written caseplan, in document order. */
83
+ export declare function planTaskOutputs(plan: unknown): PlanTaskOutputs[];
84
+ /**
85
+ * What a reference reads in a written plan: the variable of the row it names
86
+ * (`vars.<id>`, or a custom row's `var`, which has no id), or why it names none.
87
+ */
88
+ export declare function resolveInPlan(tasks: readonly PlanTaskOutputs[], ref: TaskOutputRef): {
89
+ variable: string;
90
+ } | {
91
+ why: string;
92
+ } | {
93
+ standIn: true;
94
+ };
95
+ /**
96
+ * Turn each `vars.<output id>` read in `text` that only a task-output reference
97
+ * can say back into `vars.$xref(…)`, for decompile. `byId` maps such an id to
98
+ * the reference that names it.
99
+ */
100
+ export declare function raiseTaskOutputReads(text: string, byId: ReadonlyMap<string, TaskOutputRef>): string;
101
+ /**
102
+ * The output ids a decompiled source can only read back by reference: a row
103
+ * whose id is not the variable it writes (a reassigned extract, a Designer row
104
+ * such as `{ id: 'oA1b2C3d4', var: 'AgentExpenseRequest' }`). Recompiling mints
105
+ * that id afresh, and no declaration makes `vars.<id>` readable, so a
106
+ * `vars.<id>` read in the plan goes back to the source as the reference that
107
+ * resolves to the same row. A read of an id that is also a variable (an
108
+ * own-name row, a root variable) is left as written. A row the reference
109
+ * could not name uniquely is left as written too.
110
+ */
111
+ export declare function referableOutputIds(plan: unknown, rootNames: ReadonlySet<string>): Map<string, TaskOutputRef>;