@uipath/maestro-builder-sdk 6.66.6 → 6.66.8

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.
Files changed (41) hide show
  1. package/dist/bindings-v2.d.ts +15 -1
  2. package/dist/bindings-v2.js +29 -6
  3. package/dist/bpmn/check.js +16 -113
  4. package/dist/bpmn/serialize.d.ts +41 -0
  5. package/dist/bpmn/serialize.js +103 -1
  6. package/dist/bpmn/typed-node.d.ts +12 -0
  7. package/dist/bpmn/typed-node.js +24 -0
  8. package/dist/case/check.js +5 -40
  9. package/dist/case/serialize.js +6 -0
  10. package/dist/case/task-shape.d.ts +31 -0
  11. package/dist/case/task-shape.js +48 -0
  12. package/dist/check.js +7 -43
  13. package/dist/core/agent-sidecar.d.ts +13 -3
  14. package/dist/core/agent-sidecar.js +10 -0
  15. package/dist/core/compile-refusal.d.ts +25 -0
  16. package/dist/core/compile-refusal.js +19 -0
  17. package/dist/core/process-resource.d.ts +9 -0
  18. package/dist/core/process-resource.js +4 -0
  19. package/dist/core/refusals.d.ts +47 -44
  20. package/dist/core/refusals.js +47 -44
  21. package/dist/nodes/agent/flow/check.js +14 -42
  22. package/dist/nodes/agent/flow/inline-agent.js +62 -1
  23. package/dist/nodes/agent/flow/spec.d.ts +1 -1
  24. package/dist/nodes/connector/flow/node-type.js +5 -3
  25. package/dist/nodes/data-fabric/flow/check.js +2 -4
  26. package/dist/nodes/data-fabric/flow/shared.d.ts +9 -4
  27. package/dist/nodes/data-fabric/flow/shared.js +14 -4
  28. package/dist/nodes/document/flow/ixp.js +20 -9
  29. package/dist/nodes/hitl/flow/check.js +29 -110
  30. package/dist/nodes/hitl/flow/node-type.js +170 -3
  31. package/dist/nodes/process/flow/check.js +15 -80
  32. package/dist/nodes/process/flow/node-type.js +4 -0
  33. package/dist/nodes/process/flow/refusals.d.ts +8 -0
  34. package/dist/nodes/process/flow/refusals.js +92 -0
  35. package/dist/nodes/queue/flow/check.d.ts +25 -0
  36. package/dist/nodes/queue/flow/check.js +48 -18
  37. package/dist/nodes/queue/flow/node-type.js +13 -1
  38. package/dist/nodes/transform/flow/check.js +31 -43
  39. package/dist/nodes/transform/flow/node-type.js +35 -9
  40. package/dist/serialize.js +172 -20
  41. package/package.json +2 -2
@@ -0,0 +1,8 @@
1
+ import { type ProcessKind } from '../../../core/process-resource.js';
2
+ /**
3
+ * Throw the first condition of `inputs` the serializer would write wrong, as a
4
+ * `CompileRefusal` on the step. For instance `rpaWorkflow({ name, folderPath })`
5
+ * with no `key` throws `RPA_NO_KEY` instead of writing node type
6
+ * `uipath.core.rpa-workflow.undefined`.
7
+ */
8
+ export declare function refuseLossyProcess(kind: ProcessKind, inputs: unknown, step: string): void;
@@ -0,0 +1,92 @@
1
+ /**
2
+ * What the process family's serializer refuses, because it cannot write the step
3
+ * the author wrote (#1518, docs/CHECK_COMPILE_CONTRACT_DESIGN.md §3.1). Since
4
+ * #1497 `compile` does not run `check`, so a rule whose condition the emitter
5
+ * silently loses has to stop the emitter itself. `check` still reports each one:
6
+ * it runs the serializer in collect mode, which places the refusal on the step.
7
+ *
8
+ * - `<PREFIX>_NO_KEY`: the node type is `<prefix><key>`, so an omitted key
9
+ * wrote `uipath.core.rpa-workflow.undefined` and a blank one the bare prefix.
10
+ * - `<PREFIX>_LOCAL_HAS_FOLDER`: an in-solution resource is written with
11
+ * folder `""` (`processResourceLocation`), so the author's folderPath is dropped.
12
+ * - `AGENTIC_RESERVED_INPUT`: `fireAndForget` is the design-time switch; the
13
+ * definition retypes it boolean and decompile deletes it as an argument.
14
+ */
15
+ import { CompileRefusal } from '../../../core/compile-refusal.js';
16
+ import { ownEntry } from '../../../core/own-entry.js';
17
+ import { blank, renamedFieldMessage } from '../../../core/process-resource.js';
18
+ /** Per kind: the codes, the label messages use, and the retired name of `key` (if any). */
19
+ const KINDS = {
20
+ rpaWorkflow: {
21
+ noKeyCode: 'RPA_NO_KEY', localCode: 'RPA_LOCAL_HAS_FOLDER', label: 'RPA', oldField: 'process', localLabel: 'RPA step',
22
+ noKey: (step) => `RPA step "${step}" has no \`key\`. That is the published process's release key (a GUID), and `
23
+ + `it becomes part of the node's type — without it there is no process to start. Find it with `
24
+ + `\`uip or processes list --all-folders --process-type Process\` (the row's \`Key\`).`,
25
+ noKeySuggestion: `key: '486edc26-0658-4ac1-92c9-1ef953927151'`,
26
+ },
27
+ apiWorkflow: {
28
+ noKeyCode: 'APIWF_NO_KEY', label: 'API workflow', oldField: 'workflow',
29
+ noKey: (step) => `API workflow step "${step}" has no \`key\`. That is the published workflow's key (a GUID), and `
30
+ + `it becomes part of the node's type — without it there is no workflow to invoke. Find it with `
31
+ + `\`uip or processes list --all-folders\` (an API workflow's ProcessKey reads "<name>.api.<name>").`,
32
+ noKeySuggestion: `key: 'ce857908-ee1d-4392-b552-38bcea0be29c'`,
33
+ },
34
+ publishedFunction: {
35
+ noKeyCode: 'FUNCTION_NO_KEY', localCode: 'FUNCTION_LOCAL_HAS_FOLDER', label: 'Function', localLabel: 'function step',
36
+ noKey: (step) => `Function step "${step}" has no \`key\`. That is the published function's key (a GUID), and it becomes `
37
+ + `part of the node's type — without it there is no function to invoke. Find it with `
38
+ + `\`uip maestro flow registry search "uipath.core.function"\`.`,
39
+ noKeySuggestion: `key: '7059bdb5-fdd7-4e13-9d7b-1748aaeb129d'`,
40
+ },
41
+ agenticProcess: {
42
+ noKeyCode: 'AGENTIC_NO_KEY', localCode: 'AGENTIC_LOCAL_HAS_FOLDER', label: 'Agentic process', oldField: 'process', localLabel: 'agentic process step',
43
+ noKey: (step) => `Agentic process step "${step}" has no \`key\`. That is the published process's key (a GUID), `
44
+ + `and it becomes part of the node's type — without it there is no process to start. Find it with `
45
+ + `\`uip or processes list --all-folders --limit 100\` (an agentic process's ProcessKey reads `
46
+ + `"<name>.agentic.<name>"; the listing paginates at 50, so pass a limit or scope it to a folder).`,
47
+ noKeySuggestion: `key: '4fc450ab-89be-4462-8fc8-21ac4c1d6fb9'`,
48
+ },
49
+ agent: {
50
+ noKeyCode: 'AGENT_NO_KEY', label: 'Agent', oldField: 'agent',
51
+ noKey: (step) => `Agent step "${step}" has no \`key\`. It is a GUID and becomes part of the node's `
52
+ + `type — without it there is no agent to start. For a published agent, find the Orchestrator `
53
+ + `release with \`uip or processes list --all-folders --limit 200\` (take the row's \`Key\`) or `
54
+ + `\`uip maestro flow registry search uipath.core.agent\`. For an in-solution agent, read `
55
+ + `\`resource.key\` from \`resources/solution_folder/process/agent/<project>.json\`.`,
56
+ noKeySuggestion: `key: '6a47dc5b-5fd4-461f-b97d-6c1817abcaaa'`,
57
+ },
58
+ };
59
+ /**
60
+ * Throw the first condition of `inputs` the serializer would write wrong, as a
61
+ * `CompileRefusal` on the step. For instance `rpaWorkflow({ name, folderPath })`
62
+ * with no `key` throws `RPA_NO_KEY` instead of writing node type
63
+ * `uipath.core.rpa-workflow.undefined`.
64
+ */
65
+ export function refuseLossyProcess(kind, inputs, step) {
66
+ const k = ownEntry(KINDS, kind);
67
+ const s = (inputs ?? {});
68
+ // Every blank form is refused: omitted writes `<prefix>undefined`, and `''`
69
+ // or whitespace writes the bare prefix (or one ending in spaces).
70
+ if (blank(s.key)) {
71
+ const old = k.oldField !== undefined ? s[k.oldField] : undefined;
72
+ if (k.oldField !== undefined && !blank(old)) {
73
+ throw new CompileRefusal(k.noKeyCode, renamedFieldMessage(k.label, step, k.oldField), `key: ${JSON.stringify(old)}`);
74
+ }
75
+ throw new CompileRefusal(k.noKeyCode, k.noKey(step), k.noKeySuggestion);
76
+ }
77
+ if (k.localCode !== undefined && k.localLabel !== undefined && s.location === 'in-solution' && !blank(s.folderPath)) {
78
+ throw new CompileRefusal(k.localCode, `In-solution ${k.localLabel} "${step}" also sets \`folderPath\` ${JSON.stringify(s.folderPath)}. An `
79
+ + `in-solution sibling has no folder until the solution is deployed — the platform's own manifest `
80
+ + `writes \`""\` — so the value would be dropped. Remove it, or drop \`location: 'in-solution'\` to `
81
+ + `call the published copy.`);
82
+ }
83
+ const args = s.inputs;
84
+ if (kind === 'agenticProcess' && args !== null && typeof args === 'object'
85
+ && Object.prototype.hasOwnProperty.call(args, 'fireAndForget')) {
86
+ throw new CompileRefusal('AGENTIC_RESERVED_INPUT', `Agentic process step "${step}" passes an input called \`fireAndForget\`. That name is RESERVED: it is `
87
+ + `the platform's design-time switch for starting the process without waiting, and its serializer `
88
+ + `removes the key from the node's inputs before emitting — so a process argument of that name never `
89
+ + `arrives. Locally it would appear to work, because this engine passes the whole envelope through. `
90
+ + `Rename the argument, or set \`completion: 'fire-and-forget'\` to start the process without waiting.`);
91
+ }
92
+ }
@@ -20,6 +20,31 @@ export declare function itemDataIsString(def: unknown): boolean;
20
20
  * serializer's refusal, so the two say the same thing.
21
21
  */
22
22
  export declare function queueItemExprMessage(step: string, typeName: string, version: string, field: string): string;
23
+ /**
24
+ * Whether `item` is something other than an object of fields: an array, a
25
+ * primitive, or a whole-item expression (an `Expr` is an object, but its own
26
+ * fields are not the item's). An omitted or null item is the empty item.
27
+ */
28
+ export declare function itemIsNotObject(item: unknown): boolean;
29
+ /**
30
+ * Why `serialize` refuses an `item` that is not an object (QUEUE_ITEM_NOT_OBJECT).
31
+ * Writing it anyway would rewrite it through `Object.entries`: `['INV-1']` would
32
+ * become `{ "0": "INV-1" }`, a string one field per character, a number `{}`,
33
+ * and a whole-item expression its own `js`/`literal` fields.
34
+ */
35
+ export declare function queueItemNotObjectMessage(step: string, item: unknown): {
36
+ message: string;
37
+ suggestion: string;
38
+ };
39
+ /**
40
+ * Why `serialize` refuses `returns` on a fire-and-forget enqueue
41
+ * (QUEUE_RETURNS_WITHOUT_WAIT). That node publishes the item's own record, so
42
+ * the `.flow` has no place for `returns` and writing it would drop them.
43
+ */
44
+ export declare function queueReturnsWithoutWaitMessage(step: string): {
45
+ message: string;
46
+ suggestion: string;
47
+ };
23
48
  /**
24
49
  * Check a `queueItem(...)` call.
25
50
  *
@@ -14,7 +14,7 @@ export const QUEUE_DIAGNOSTICS = {
14
14
  QUEUE_ITEM_EXPR_UNSUPPORTED: { severity: 'error', summary: 'an expression in an item stored as one JSON string' },
15
15
  QUEUE_BAD_PRIORITY: { severity: 'error', summary: 'a priority the node type does not offer' },
16
16
  QUEUE_BAD_DATE: { severity: 'error', summary: 'a date Orchestrator cannot read' },
17
- QUEUE_RETURNS_WITHOUT_WAIT: { severity: 'warning', summary: 'returns without wait: true' },
17
+ QUEUE_RETURNS_WITHOUT_WAIT: { severity: 'error', summary: 'returns without wait: true' },
18
18
  QUEUE_READ_NOT_OUTPUT: { severity: 'error', summary: 'a read outside the step\'s output' },
19
19
  QUEUE_READ_WITHOUT_RETURNS: { severity: 'error', summary: 'a field read off a wait result with no returns' },
20
20
  QUEUE_READ_UNKNOWN_FIELD: { severity: 'error', summary: 'a read of a field the step does not publish' },
@@ -40,6 +40,50 @@ export function queueItemExprMessage(step, typeName, version, field) {
40
40
  + `cannot be an expression there. Omit the { version } to emit the SDK's default version, which `
41
41
  + `stores the item as an object of values and expressions.`;
42
42
  }
43
+ /**
44
+ * Whether `item` is something other than an object of fields: an array, a
45
+ * primitive, or a whole-item expression (an `Expr` is an object, but its own
46
+ * fields are not the item's). An omitted or null item is the empty item.
47
+ */
48
+ export function itemIsNotObject(item) {
49
+ if (item === undefined || item === null)
50
+ return false;
51
+ return item instanceof Expr || typeof item !== 'object' || Array.isArray(item);
52
+ }
53
+ function itemKind(item) {
54
+ if (item instanceof Expr)
55
+ return 'an expression';
56
+ return Array.isArray(item) ? 'an array' : typeof item;
57
+ }
58
+ /**
59
+ * Why `serialize` refuses an `item` that is not an object (QUEUE_ITEM_NOT_OBJECT).
60
+ * Writing it anyway would rewrite it through `Object.entries`: `['INV-1']` would
61
+ * become `{ "0": "INV-1" }`, a string one field per character, a number `{}`,
62
+ * and a whole-item expression its own `js`/`literal` fields.
63
+ */
64
+ export function queueItemNotObjectMessage(step, item) {
65
+ return {
66
+ message: `Queue step "${step}"'s \`item\` is ${itemKind(item)}. It `
67
+ + `must be an object of field names to primitive values — that object becomes the queue `
68
+ + `item's \`SpecificContent\`.`,
69
+ suggestion: `item: { InvoiceId: 'INV-1', Amount: 120 }`,
70
+ };
71
+ }
72
+ /**
73
+ * Why `serialize` refuses `returns` on a fire-and-forget enqueue
74
+ * (QUEUE_RETURNS_WITHOUT_WAIT). That node publishes the item's own record, so
75
+ * the `.flow` has no place for `returns` and writing it would drop them.
76
+ */
77
+ export function queueReturnsWithoutWaitMessage(step) {
78
+ return {
79
+ message: `Queue step "${step}" declares \`returns\` but does not \`wait\`, so nothing produces those `
80
+ + `fields and the \`.flow\` would drop them: a fire-and-forget enqueue publishes the queue ITEM's own `
81
+ + `Orchestrator record (\`Key\`, \`Status\`, \`Reference\`, \`SpecificContent\`, …), whose fields are `
82
+ + `already declared for you. \`returns\` describes what a CONSUMER wrote into a processed item's `
83
+ + `result, which needs \`wait: true\`.`,
84
+ suggestion: `wait: true`,
85
+ };
86
+ }
43
87
  /**
44
88
  * Check a `queueItem(...)` call.
45
89
  *
@@ -102,12 +146,8 @@ export function checkQueueItem(inputs, ctx) {
102
146
  + `— a bare item with a reference is a real pattern — but if the work needs a payload, this `
103
147
  + `is where it goes.`, `item: { InvoiceId: input('invoiceRef') }`);
104
148
  }
105
- else if (typeof item !== 'object' || Array.isArray(item)) {
106
- at('QUEUE_ITEM_NOT_OBJECT', `Queue step "${step}"'s \`item\` is ${Array.isArray(item) ? 'an array' : typeof item}. It `
107
- + `must be an object of field names to primitive values — that object becomes the queue `
108
- + `item's \`SpecificContent\`.`, `item: { InvoiceId: 'INV-1', Amount: 120 }`);
109
- }
110
- else {
149
+ else if (!itemIsNotObject(item)) {
150
+ // (A non-object item is the serializer's QUEUE_ITEM_NOT_OBJECT.)
111
151
  const itemIsString = itemDataIsString(ctx.definition());
112
152
  for (const [k, v] of Object.entries(item)) {
113
153
  // An expression where the pinned version stores one JSON string is the
@@ -140,17 +180,7 @@ export function checkQueueItem(inputs, ctx) {
140
180
  + `Orchestrator can read. It takes ISO-8601 — a full timestamp with a zone.`, `${field}: '2026-08-05T17:00:00Z'`);
141
181
  }
142
182
  }
143
- // `returns` describes what a CONSUMER produced, which only exists if the step
144
- // waited for one. On a fire-and-forget enqueue it is a misunderstanding worth
145
- // naming: the output is the queue item's own record, and the serializer already
146
- // declares its fields.
147
- if (!inputs?.wait && inputs?.returns && Object.keys(inputs.returns).length) {
148
- warn('QUEUE_RETURNS_WITHOUT_WAIT', `Queue step "${step}" declares \`returns\` but does not \`wait\`, so nothing produces those `
149
- + `fields: a fire-and-forget enqueue publishes the queue ITEM's own Orchestrator record `
150
- + `(\`Key\`, \`Status\`, \`Reference\`, \`SpecificContent\`, …), whose fields are already `
151
- + `declared for you. \`returns\` describes what a CONSUMER wrote into a processed item's `
152
- + `result, which needs \`wait: true\`.`, `wait: true`);
153
- }
183
+ // `returns` without `wait` is the serializer's QUEUE_RETURNS_WITHOUT_WAIT.
154
184
  }
155
185
  /** The three priorities `core.action.queue.create[-and-wait]`'s form offers. */
156
186
  const QUEUE_PRIORITIES = ['Low', 'Normal', 'High'];
@@ -1,7 +1,7 @@
1
1
  import { Expr } from '../../../core/expr.js';
2
2
  import { jsonClone } from '../../../core/json-clone.js';
3
3
  import { isRecord } from '../../../core/node-envelope.js';
4
- import { checkQueueItem, itemDataIsString, queueItemExprMessage } from './check.js';
4
+ import { checkQueueItem, itemDataIsString, itemIsNotObject, queueItemExprMessage, queueItemNotObjectMessage, queueReturnsWithoutWaitMessage, } from './check.js';
5
5
  import { QUEUE_ITEM_RECORD_SCHEMA } from './record.js';
6
6
  import { CompileRefusal } from '../../../core/compile-refusal.js';
7
7
  const CREATE = 'core.action.queue.create';
@@ -247,6 +247,18 @@ function queueAt(typeName, wait) {
247
247
  kind: 'queueItem', typeName, version: '1.2', factory: 'queueItem', definition: 'per-tenant', errorPort: true,
248
248
  rawNode: { factory: wait ? 'queueItem({ wait: true })' : 'queueItem()' },
249
249
  serialize: ({ inputs: s }, ctx) => {
250
+ // Both would be written lossily, so they are refused here, on the step:
251
+ // a non-object item (a whole-item Expr included) would be rewritten
252
+ // through `Object.entries`, and a
253
+ // fire-and-forget node carries the item record schema, not `returns`.
254
+ if (itemIsNotObject(s.item)) {
255
+ const { message, suggestion } = queueItemNotObjectMessage(ctx.step, s.item);
256
+ throw new CompileRefusal('QUEUE_ITEM_NOT_OBJECT', message, suggestion);
257
+ }
258
+ if (!s.wait && s.returns && Object.keys(s.returns).length) {
259
+ const { message, suggestion } = queueReturnsWithoutWaitMessage(ctx.step);
260
+ throw new CompileRefusal('QUEUE_RETURNS_WITHOUT_WAIT', message, suggestion);
261
+ }
250
262
  // The bindings first: two steps on the same queue share one pair, and the
251
263
  // node's `model.context` has to name the ids that actually exist.
252
264
  const { ids, entries } = queueItemBindings(s, ctx.bindings);
@@ -23,7 +23,7 @@ export const TRANSFORM_DIAGNOSTICS = {
23
23
  TRANSFORM_NO_COLLECTION: { severity: 'error', summary: 'no collection' },
24
24
  TRANSFORM_NO_GROUP_FIELD: { severity: 'error', summary: 'a groupBy with no field' },
25
25
  TRANSFORM_NO_OPERATIONS: { severity: 'error', summary: 'no operations' },
26
- TRANSFORM_UNKNOWN_OP: { severity: 'error', summary: 'an operation type the runtime skips' },
26
+ TRANSFORM_UNKNOWN_OP: { severity: 'error', summary: 'an operation type compile cannot write' },
27
27
  TRANSFORM_VARIANT_ONE_OP: { severity: 'error', summary: 'a variant with more or fewer than one operation' },
28
28
  TRANSFORM_VARIANT_OP_MISMATCH: { severity: 'error', summary: 'a variant whose operation does not match it' },
29
29
  TRANSFORM_VARIANT_UNKNOWN: { severity: 'error', summary: 'a variant no runtime resolves' },
@@ -32,44 +32,35 @@ export function checkTransform(inputs, ctx) {
32
32
  const step = ctx.step;
33
33
  const at = (code, message, suggestion) => ctx.report(code, message, suggestion || undefined);
34
34
  // ── collection must be a bare variable path ──
35
+ // A missing, raw-value or literal collection has no path form at all: the
36
+ // serializer refuses it (TRANSFORM_NO_COLLECTION, TRANSFORM_COLLECTION_NOT_REF,
37
+ // TRANSFORM_COLLECTION_LITERAL in node-type.ts `collectionPath`).
35
38
  const collection = inputs?.collection;
36
- if (collection === undefined || collection === null) {
37
- at('TRANSFORM_NO_COLLECTION', `Transform "${step}" has no collection. Point it at the variable or step output holding the array.`, `collection: v('catalog')`);
38
- }
39
- else if (!(collection instanceof Expr)) {
40
- // Types are stripped at run time, so `collection: [{…}]` or
41
- // `collection: 'catalog'` reaches here as a raw value.
42
- at('TRANSFORM_COLLECTION_NOT_REF', `Transform "${step}" was given a ${Array.isArray(collection) ? 'raw array' : `raw ${typeof collection}`} as its `
43
- + `collection. That field is a variable PATH — the runtime turns the string into a lookup, so a value there `
44
- + `resolves to nothing and the transform silently sees an empty collection.`, typeof collection === 'string' ? `collection: v('${collection}')` : `collection: v('<varName>')`);
45
- }
46
- else if (collection.literal) {
47
- at('TRANSFORM_COLLECTION_LITERAL', `Transform "${step}" has a literal collection. Inline data cannot be a path — keep the array in a variable `
48
- + `default (\`.var('items', types.array, [...])\`) and reference it, or read an upstream step's output.`, `collection: v('items')`);
49
- }
50
- else if (!/^\$vars\.[A-Za-z0-9_]+(\.[A-Za-z0-9_]+)*$/.test(collection.js.trim())) {
51
- // A computed expression (js`…`, tmpl`…`, `.concat(…)`) is the trap that
52
- // survives every local rung: a local run resolves it to a value, so
53
- // it runs correctly there and resolves to NOTHING once deployed, where the
54
- // string is used as a lookup path.
55
- at('TRANSFORM_COLLECTION_NOT_PATH', `Transform "${step}"'s collection must be a plain reference, but it is the expression `
56
- + `"${collection.js.trim().slice(0, 60)}". The deployed runtime uses that string as a lookup PATH, so a `
57
- + `computed expression resolves to nothing there even though it runs locally. Compute it in a script step `
58
- + `first, then point the transform at that step's output.`, `collection: out('<scriptStep>')`);
59
- }
60
- else {
61
- // A collection that is not an array is PASSED THROUGH untouched — no error,
62
- // no operations applied (faithful to v1). The result is a transform that
63
- // looks like it ran and changed nothing.
64
- // Only a DECLARED scalar can be judged here: a step output's type is not
65
- // known offline, so this stays quiet about those rather than guessing.
66
- const declared = ctx.declaredRef(collection.js);
67
- const refName = declared?.name;
68
- const declaredType = declared?.type;
69
- if (declaredType && ['string', 'number', 'boolean'].includes(declaredType)) {
70
- at('TRANSFORM_COLLECTION_SCALAR', `Transform "${step}"'s collection is "${refName}", declared as ${declaredType}. A transform runs its `
71
- + `operations over an ARRAY; anything else is passed through untouched — no error, nothing applied — so `
72
- + `the step would look like it ran and change nothing.`, `Declare it as types.array, or point the collection at the array field: collection: out('<step>', '<field>')`);
39
+ if (collection instanceof Expr && !collection.literal) {
40
+ if (!/^\$vars\.[A-Za-z0-9_]+(\.[A-Za-z0-9_]+)*$/.test(collection.js.trim())) {
41
+ // A computed expression (js`…`, tmpl`…`, `.concat(…)`) is the trap that
42
+ // survives every local rung: a local run resolves it to a value, so
43
+ // it runs correctly there and resolves to NOTHING once deployed, where the
44
+ // string is used as a lookup path.
45
+ at('TRANSFORM_COLLECTION_NOT_PATH', `Transform "${step}"'s collection must be a plain reference, but it is the expression `
46
+ + `"${collection.js.trim().slice(0, 60)}". The deployed runtime uses that string as a lookup PATH, so a `
47
+ + `computed expression resolves to nothing there even though it runs locally. Compute it in a script step `
48
+ + `first, then point the transform at that step's output.`, `collection: out('<scriptStep>')`);
49
+ }
50
+ else {
51
+ // A collection that is not an array is PASSED THROUGH untouched — no error,
52
+ // no operations applied (faithful to v1). The result is a transform that
53
+ // looks like it ran and changed nothing.
54
+ // Only a DECLARED scalar can be judged here: a step output's type is not
55
+ // known offline, so this stays quiet about those rather than guessing.
56
+ const declared = ctx.declaredRef(collection.js);
57
+ const refName = declared?.name;
58
+ const declaredType = declared?.type;
59
+ if (declaredType && ['string', 'number', 'boolean'].includes(declaredType)) {
60
+ at('TRANSFORM_COLLECTION_SCALAR', `Transform "${step}"'s collection is "${refName}", declared as ${declaredType}. A transform runs its `
61
+ + `operations over an ARRAY; anything else is passed through untouched — no error, nothing applied — so `
62
+ + `the step would look like it ran and change nothing.`, `Declare it as types.array, or point the collection at the array field: collection: out('<step>', '<field>')`);
63
+ }
73
64
  }
74
65
  }
75
66
  // ── the operation chain ──
@@ -82,9 +73,6 @@ export function checkTransform(inputs, ctx) {
82
73
  checkVariant(step, inputs, ops, at);
83
74
  ops.forEach((op, i) => {
84
75
  const where = `operations[${i}]`;
85
- // Read the discriminant before the narrowing below exhausts the union — at
86
- // run time it can be anything (an author typo survives type erasure).
87
- const kind = op?.type;
88
76
  if (op?.type === 'filter') {
89
77
  if (!Array.isArray(op.filters) || op.filters.length === 0) {
90
78
  at('TRANSFORM_EMPTY_FILTER', `Transform "${step}" ${where} is a filter with no rules — the runtime passes every item through it.`);
@@ -145,8 +133,8 @@ export function checkTransform(inputs, ctx) {
145
133
  }
146
134
  return;
147
135
  }
148
- at('TRANSFORM_UNKNOWN_OP', `Transform "${step}" ${where} has type ${JSON.stringify(kind)}. The runtime skips an operation it does `
149
- + `not recognize, so the collection would pass through unchanged. Use 'filter', 'map', or 'groupBy'.`);
136
+ // Any other type is the serializer's TRANSFORM_UNKNOWN_OP (node-type.ts
137
+ // `transformInputs`): it would otherwise be written as a groupBy.
150
138
  });
151
139
  }
152
140
  /**
@@ -59,6 +59,12 @@ function transformInputs(inputs, ctx) {
59
59
  },
60
60
  };
61
61
  }
62
+ // Anything that is not a groupBy would be written as one below, which the
63
+ // runtime would then run as a real grouping (#1518). Refused instead.
64
+ if (op?.type !== 'groupBy') {
65
+ throw new CompileRefusal('TRANSFORM_UNKNOWN_OP', `Transform "${ctx.step}" operations[${i}] has type ${JSON.stringify(op?.type)}; `
66
+ + `compile writes only filter, map or groupBy. Use one of them.`);
67
+ }
62
68
  return {
63
69
  id,
64
70
  type: 'groupBy',
@@ -74,20 +80,40 @@ function transformInputs(inputs, ctx) {
74
80
  },
75
81
  };
76
82
  });
77
- return { collection: ctx.renameRefs(collectionPath(inputs.collection)), operations };
83
+ return { collection: ctx.renameRefs(collectionPath(inputs.collection, ctx.step)), operations };
78
84
  }
79
85
  /**
80
86
  * The `collection` string. A bare reference (`v('x')` / `out('s','p')`) is
81
87
  * already the path the runtime wants, so it is emitted verbatim. Anything else
82
- * cannot be expressed as a path — `check` rejects it (TRANSFORM_COLLECTION_*)
83
- * before serialization, so reaching here with one means `compile` was called
84
- * without a check; emit the raw text so the failure stays visible downstream
85
- * rather than becoming a silently empty collection.
88
+ * has no faithful path form, so it is refused here rather than written (#1518):
89
+ * a missing one would be the string "undefined", a raw array "[object Object]",
90
+ * a raw string `items` reads back as the expression js`items`, and `lit([1,2])`
91
+ * would be "[1,2]", the same text as js`[1,2]`. `compile` does not run `check`
92
+ * (#1497), so this is the only place that stops them. A computed expression
93
+ * (TRANSFORM_COLLECTION_NOT_PATH) is written as its own text, so it stays
94
+ * `check`'s.
86
95
  */
87
- function collectionPath(collection) {
88
- if (collection instanceof Expr)
89
- return collection.literal ? JSON.stringify(collection.value) : collection.js;
90
- return String(collection);
96
+ function collectionPath(collection, step) {
97
+ if (collection === undefined || collection === null) {
98
+ throw new CompileRefusal('TRANSFORM_NO_COLLECTION', `Transform "${step}" has no collection. Point it at the variable or step output holding the array.`, `collection: v('catalog')`);
99
+ }
100
+ if (!(collection instanceof Expr)) {
101
+ // Types are stripped at run time, so `collection: [{…}]` or
102
+ // `collection: 'catalog'` reaches here as a raw value.
103
+ throw new CompileRefusal('TRANSFORM_COLLECTION_NOT_REF', `Transform "${step}" was given a ${Array.isArray(collection) ? 'raw array' : `raw ${typeof collection}`} as its `
104
+ + `collection. That field is a variable PATH — the runtime turns the string into a lookup, so a value there `
105
+ + `resolves to nothing and the transform silently sees an empty collection.`, typeof collection === 'string' ? `collection: v('${collection}')` : `collection: v('<varName>')`);
106
+ }
107
+ // A decompiled canvas Transform with no collection prints as `lit([])`
108
+ // (`printTransform`), so recompiling it stops here too: this message names
109
+ // the fix for that file as well as for authored inline data.
110
+ if (collection.literal) {
111
+ throw new CompileRefusal('TRANSFORM_COLLECTION_LITERAL', `Transform "${step}" has a literal collection, and that field is a variable PATH, so inline data has no `
112
+ + `form there. A Transform decompiled with no collection prints as \`lit([])\` and lands here too. Bind `
113
+ + `\`collection\` to a variable or step output, e.g. \`v('<var>')\`; for inline data, keep the array in a `
114
+ + `variable default (\`.var('items', types.array, [...])\`) and reference it.`, `collection: v('<var>')`);
115
+ }
116
+ return collection.js;
91
117
  }
92
118
  /**
93
119
  * A transform node publishes a COLLECTION — always.