@uipath/maestro-builder-sdk 6.47.3 → 6.48.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.
@@ -1,7 +1,7 @@
1
1
  /**
2
- * The document factories, `documentClassify()` and `dynamicExtract()`. Moved
3
- * from core/actions.ts in Phase 4 of docs/NODE_TYPE_ARCHITECTURE_DESIGN.md; the
4
- * package entry re-exports them.
2
+ * The document factories, `documentClassify()`, `dynamicExtract()` and
3
+ * `ixpExtract()`. Moved from core/actions.ts in Phase 4 of
4
+ * docs/NODE_TYPE_ARCHITECTURE_DESIGN.md; the package entry re-exports them.
5
5
  */
6
6
  import { builtinActionSpec } from '../../../core/actions.js';
7
7
  /**
@@ -28,3 +28,60 @@ export function documentClassify(inputs) {
28
28
  export function dynamicExtract(inputs) {
29
29
  return builtinActionSpec({ kind: 'dynamicExtract', inputs });
30
30
  }
31
+ /**
32
+ * Extract fields from a document with a published IxP project — the platform's
33
+ * **Extract** node (`uipath.ixp.*`, service contract `IXP.Extraction`).
34
+ *
35
+ * @remarks
36
+ * **The result's shape is three levels deep and PascalCase.** A field lands at
37
+ * `out('<step>', 'ExtractionResult.ResultsDocument.Fields')` — an array of
38
+ * `{ FieldId, FieldName, FieldType, IsMissing, Values[], Confidence }`, looked
39
+ * up by `FieldName`. `Fields[]` is under `ResultsDocument`, not directly under
40
+ * `ExtractionResult`, and there is no `result` wrapper: the platform's
41
+ * serializer strips it before the value reaches the variable.
42
+ *
43
+ * **A local run cannot extract anything** — extraction needs a real document on
44
+ * the IxP service. An offline run returns the baked
45
+ * fixture shape, and only a platform run reaches the model. So a green local
46
+ * rung proves the wiring and the definition, never that a document was read.
47
+ *
48
+ * **`fileRef` is emitted as a PLAIN `=js:` string, not the expression envelope
49
+ * the rest of the file uses** — the one place this family diverges from the
50
+ * format's general spelling, and the contract this node owes the platform. The
51
+ * product's own `ixp-node` validator tests `typeof fileRef !== 'string'` and
52
+ * refuses the object form (*"inputs.fileRef must be a `=js:$vars.…`
53
+ * expression"*), the node's registry manifest declares
54
+ * `{type:'string', minLength:1}`, and so do all 150 v1-authored instances in the
55
+ * deployed corpus that carry the field — at file versions 1.6, 1.8 and 1.9 alike,
56
+ * with zero envelopes among them. The RUNTIME
57
+ * accepts both, so only `uip maestro flow validate` sees a wrong one; the
58
+ * emitter's exemption lives in `migrate.ts` (`UNWRAPPED_INPUTS`).
59
+ * See the IxP node reference (`website/reference/ixp.md`) §3.
60
+ *
61
+ * @example
62
+ * **Extract an invoice total and read it out of the nested result**
63
+ * ```ts
64
+ * flow('InvoiceIntake')
65
+ * .input({ invoiceFile: types.file })
66
+ * .output({ total: types.string })
67
+ * .step('extract', ixpExtract({
68
+ * project: 'uipath.ixp.invoiceixp-cef0d447-ixp.ff973488-5d89-8035-86ac-f980b4be6deb-c4359cde-55f0-4f0e-9322-c6cdce74ab4c',
69
+ * modelName: 'invoiceixp-cef0d447-ixp',
70
+ * name: 'InvoiceIXP',
71
+ * folderName: 'Shared',
72
+ * fileRef: out('start', 'invoiceFile'),
73
+ * }))
74
+ * .step('readTotal', script({ code: `
75
+ * const fields = $vars.extract.output.ExtractionResult.ResultsDocument.Fields || [];
76
+ * return { total: fields.find(f => f.FieldName === 'invoiceTotal')?.Values?.[0] };
77
+ * ` }))
78
+ * .return({ total: out('readTotal', 'total') })
79
+ * ```
80
+ *
81
+ * @param inputs - The published IxP `project` and `modelName`, and the document
82
+ * to read (`fileRef`).
83
+ * @returns An action spec to pass to `.step(name, …)`.
84
+ */
85
+ export function ixpExtract(inputs) {
86
+ return builtinActionSpec({ kind: 'ixpExtract', inputs });
87
+ }
@@ -1,3 +1,3 @@
1
- /** The document node family, as flow-node-registry.ts lists it: Document Classify and Dynamic Extract. */
1
+ /** The document node family, as flow-node-registry.ts lists it: Document Classify, Dynamic Extract and an IxP project's extractor. */
2
2
  import type { FlowNodeFamily } from '../../../flow-node-type.js';
3
3
  export declare const documentFlowFamily: FlowNodeFamily;
@@ -1,7 +1,8 @@
1
+ import { IXP_DIAGNOSTICS, ixpExtractNodeType } from './ixp.js';
1
2
  import { documentClassifyNodeType, DOCUMENT_DIAGNOSTICS, dynamicExtractNodeType } from './node-type.js';
2
3
  export const documentFlowFamily = {
3
4
  id: 'document',
4
5
  title: 'Document understanding',
5
- types: [documentClassifyNodeType, dynamicExtractNodeType],
6
- diagnostics: DOCUMENT_DIAGNOSTICS,
6
+ types: [documentClassifyNodeType, dynamicExtractNodeType, ixpExtractNodeType],
7
+ diagnostics: { ...DOCUMENT_DIAGNOSTICS, ...IXP_DIAGNOSTICS },
7
8
  };
@@ -0,0 +1,34 @@
1
+ import type { DiagnosticCatalog } from '../../../core/nodes/family.js';
2
+ import type { FlowVersionedNodeType } from '../../../flow-node-type.js';
3
+ import { type IxpExtractInputs } from './spec.js';
4
+ export type IxpExtractSpec = {
5
+ kind: 'ixpExtract';
6
+ inputs: IxpExtractInputs;
7
+ };
8
+ export declare const IXP_DIAGNOSTICS: DiagnosticCatalog;
9
+ /**
10
+ * Build the definition for one published IxP project.
11
+ *
12
+ * The per-project half is derived, not asked for twice: the node type's tail
13
+ * packs the project VERSION id (the binding's `resourceKey`, and the model
14
+ * blob's `id`) and the FOLDER key, so the author passes the type once and those
15
+ * two come out of it.
16
+ *
17
+ * `inputDefaults` matters more here than in the other per-instance families,
18
+ * because the platform's own instance IS its defaults: measured on today's 1.0
19
+ * nightly artifact, `node.inputs` equals `inputDefaults` verbatim with exactly
20
+ * two edits — `fileRef` authored, and `model.modelName` forced non-empty (the
21
+ * registry returns `null` for published/OOB deployments, and both `flow
22
+ * validate` and Studio Web's property panel refuse that). So the emitter writes
23
+ * the defaults and the node reads them back, rather than the two drifting.
24
+ */
25
+ export declare function ixpExtractDef(spec: {
26
+ project: string;
27
+ modelName: string;
28
+ name: string;
29
+ folderName: string;
30
+ folderPath?: string;
31
+ versionTag?: string;
32
+ description?: string;
33
+ }): any;
34
+ export declare const ixpExtractNodeType: FlowVersionedNodeType<IxpExtractSpec>;
@@ -0,0 +1,389 @@
1
+ /**
2
+ * An IxP project's extractor — `ixpExtract()`, a node type per published
3
+ * project: `uipath.ixp.<projectSlug>.<projectVersionId>-<folderKey>`. The type
4
+ * owns every name under `uipath.ixp.` whose tail is that pair of uuids
5
+ * (`typeNameMatches`), which keeps Dynamic Extract's exact
6
+ * `uipath.ixp.extract-document-builder` and the two older, unresolvable tail
7
+ * spellings out of it. Per-tenant: the definition is one bundled template plus
8
+ * the project's identity. Moved from serialize.ts / check.ts / decompile.ts in
9
+ * Phase 4 of docs/NODE_TYPE_ARCHITECTURE_DESIGN.md; serialized output, printed
10
+ * source, codes and messages unchanged.
11
+ */
12
+ import ixpDefinition from '../../../core/ixp-definition.json' with { type: 'json' };
13
+ import { definitionHasErrorPort } from '../../../core/error-port.js';
14
+ import { Expr } from '../../../core/expr.js';
15
+ import { jsonClone } from '../../../core/json-clone.js';
16
+ import { parseIxpProjectType } from './spec.js';
17
+ const IXP_PREFIX = 'uipath.ixp.';
18
+ export const IXP_DIAGNOSTICS = {
19
+ IXP_FORBIDDEN_INPUT: { severity: 'error', summary: 'an IxP step setting an input the current node does not declare' },
20
+ IXP_NO_FILE: { severity: 'error', summary: 'an IxP step with no fileRef' },
21
+ IXP_NO_FOLDER: { severity: 'error', summary: 'an IxP step with no folderName' },
22
+ IXP_NO_MODEL_NAME: { severity: 'error', summary: 'an IxP step with no modelName' },
23
+ IXP_NO_NAME: { severity: 'error', summary: 'an IxP step with no name' },
24
+ IXP_NO_PROJECT: { severity: 'error', summary: 'an IxP step with no project' },
25
+ IXP_PROJECT_NOT_NODE_TYPE: { severity: 'error', summary: 'an IxP project that is not a uipath.ixp.* node type' },
26
+ IXP_PROJECT_TAIL_UNPARSEABLE: { severity: 'error', summary: "an IxP project whose tail is not the version id and folder key" },
27
+ };
28
+ /**
29
+ * The parts every `uipath.ixp.*` definition shares, bundled verbatim from
30
+ * `uip maestro flow registry get` (2026-07-31, after `registry pull --force`).
31
+ *
32
+ * Unlike rpa's and api-workflow's, an extractor's definition is MOSTLY generic:
33
+ * three projects' definitions (`invoiceixp`, `idp-benchmark---invoices`,
34
+ * `invoices-billing`) are byte-identical in `category`, `tags`, `sortOrder`,
35
+ * `version`, `supportsErrorHandling`, `handleConfiguration`, `form`, `debug`,
36
+ * `inputDefinition` and the 18.7 KB `outputDefinition` — and differ in exactly
37
+ * five per-project places (`nodeType`, `description`, `display.label`,
38
+ * `model.bindings`, `inputDefaults`), all of which `ixpExtractDef` builds from
39
+ * the author's call. So the family costs one bundled template, not 13.
40
+ *
41
+ * **The one thing to know before re-recording it:** `registry get` answers from
42
+ * a LOCAL CACHE (`~/.uipath/nodes/index.json`). Without
43
+ * `uip maestro flow registry pull --force` first you get whenever you last
44
+ * synced — which is how a stale copy of this file would claim `required:
45
+ * ["fileRef"]` (the pre-pull answer) instead of the three the platform's
46
+ * `ixp-node` validator actually enforces.
47
+ */
48
+ const IXP_DEF = ixpDefinition;
49
+ /**
50
+ * Build the definition for one published IxP project.
51
+ *
52
+ * The per-project half is derived, not asked for twice: the node type's tail
53
+ * packs the project VERSION id (the binding's `resourceKey`, and the model
54
+ * blob's `id`) and the FOLDER key, so the author passes the type once and those
55
+ * two come out of it.
56
+ *
57
+ * `inputDefaults` matters more here than in the other per-instance families,
58
+ * because the platform's own instance IS its defaults: measured on today's 1.0
59
+ * nightly artifact, `node.inputs` equals `inputDefaults` verbatim with exactly
60
+ * two edits — `fileRef` authored, and `model.modelName` forced non-empty (the
61
+ * registry returns `null` for published/OOB deployments, and both `flow
62
+ * validate` and Studio Web's property panel refuse that). So the emitter writes
63
+ * the defaults and the node reads them back, rather than the two drifting.
64
+ */
65
+ export function ixpExtractDef(spec) {
66
+ const parts = parseIxpProjectType(spec.project);
67
+ if (!parts) {
68
+ throw new Error(`ixpExtract: "${spec.project}" is not a resolvable extractor node type. It must be `
69
+ + `uipath.ixp.<slug>.<projectVersionId>-<folderKey> — copy it from `
70
+ + `\`uip maestro flow registry pull --force && uip maestro flow registry search -f "type:startsWith=uipath.ixp"\`.`);
71
+ }
72
+ const { versionId, folderKey } = parts;
73
+ const model = {
74
+ id: versionId,
75
+ // Authoring rule #1 in v1's own IxP reference: non-empty or the `ixp-node`
76
+ // validator fails the flow and the designer's panel crashes destructuring it.
77
+ modelName: spec.modelName,
78
+ modelDisplayName: spec.name,
79
+ folderKey,
80
+ folderName: spec.folderName,
81
+ // The folder's ancestor-key chain for a nested folder; the folder key alone
82
+ // for one published straight into a root folder, which is what the platform
83
+ // writes there.
84
+ folderPath: spec.folderPath ?? folderKey,
85
+ description: '',
86
+ };
87
+ return {
88
+ nodeType: spec.project,
89
+ version: IXP_DEF.version,
90
+ category: IXP_DEF.category,
91
+ tags: jsonClone(IXP_DEF.tags),
92
+ // The platform's own definition describes an extractor by its FOLDER, and
93
+ // the designer prints exactly this in the node's subtitle.
94
+ description: `(${spec.folderName})`,
95
+ sortOrder: IXP_DEF.sortOrder,
96
+ supportsErrorHandling: IXP_DEF.supportsErrorHandling,
97
+ display: {
98
+ canvasLabel: IXP_DEF.canvasLabel,
99
+ label: spec.name,
100
+ description: `(${spec.folderName})`,
101
+ icon: IXP_DEF.icon,
102
+ },
103
+ handleConfiguration: jsonClone(IXP_DEF.handleConfiguration),
104
+ model: {
105
+ ...jsonClone(IXP_DEF.model),
106
+ bindings: {
107
+ ...jsonClone(IXP_DEF.bindingsFrame),
108
+ resourceKey: versionId,
109
+ name: spec.name,
110
+ values: [
111
+ { name: 'name', propertyAttribute: 'name', default: spec.name },
112
+ { name: 'folderPath', propertyAttribute: 'folderPath', default: spec.folderName },
113
+ ],
114
+ },
115
+ },
116
+ form: jsonClone(IXP_DEF.form),
117
+ // A `description` the author asked for is DECLARED in the same write. The
118
+ // registry's own copy carries `description` in `inputDefaults` and not in
119
+ // `inputDefinition.properties` — lesson 42's gap, live on this family — so
120
+ // emitting the input without this would flag our own output as setting an
121
+ // undeclared input, which
122
+ // is what v1's deployed artifacts do (measured: 1 warning on today's green
123
+ // nightly file). The default is to emit no `description` at all, which is
124
+ // why this is conditional and the definition otherwise stays byte-identical
125
+ // to the platform's.
126
+ inputDefinition: spec.description === undefined
127
+ ? jsonClone(IXP_DEF.inputDefinition)
128
+ : {
129
+ ...jsonClone(IXP_DEF.inputDefinition),
130
+ properties: { ...jsonClone(IXP_DEF.inputDefinition.properties), description: { type: 'string' } },
131
+ },
132
+ inputDefaults: {
133
+ model,
134
+ modelName: spec.modelName,
135
+ ...(spec.description === undefined ? {} : { description: spec.description }),
136
+ // `modelName` and `projectName` are ONE value in two fields: all three
137
+ // measured definitions agree, and the designer shows it in two disabled
138
+ // boxes (the IXP-model and Folder rows of the `ixp-model` form section).
139
+ projectName: spec.modelName,
140
+ versionTag: spec.versionTag ?? '',
141
+ folderKey,
142
+ folderName: spec.folderName,
143
+ fileRef: '',
144
+ pageRange: '',
145
+ },
146
+ outputDefinition: jsonClone(IXP_DEF.outputDefinition),
147
+ debug: jsonClone(IXP_DEF.debug),
148
+ };
149
+ }
150
+ /**
151
+ * The `ixpDeployment` binding pair for one IxP project — the THIRD resource kind
152
+ * this SDK emits, after `process` and `queue`.
153
+ *
154
+ * Same shape and same one-pair-per-resource rule as
155
+ * {@link processResourceBindings}: keyed by `resource + resourceKey +
156
+ * propertyAttribute`, so two steps extracting with the same project share one
157
+ * pair rather than shadowing each other. `resourceKey` is the project VERSION
158
+ * id, which is what the definition's `model.bindings.resourceKey` names.
159
+ *
160
+ * Measured on 48 of the 151 deployed IxP artifacts (and 3 of today's 4 green
161
+ * ones): exactly these two entries, `name` defaulting to the deployment's
162
+ * display name and `folderPath` to its folder — note `folderPath` here holds the
163
+ * folder NAME, not the key chain that `inputs.model.folderPath` carries.
164
+ */
165
+ function ixpDeploymentBindings(spec, existing) {
166
+ const found = (attr) => [...existing.values()].find((b) => b.resource === 'ixpDeployment' && b.resourceKey === spec.versionId && b.propertyAttribute === attr)?.id;
167
+ const already = { name: found('name'), folderPath: found('folderPath') };
168
+ if (already.name && already.folderPath) {
169
+ return { ids: { name: already.name, folderPath: already.folderPath }, entries: [] };
170
+ }
171
+ const slug = spec.name.replace(/[^A-Za-z0-9]/g, '') || 'Ixp';
172
+ let base = `b${slug.charAt(0).toUpperCase()}${slug.slice(1)}`;
173
+ for (let n = 2; existing.has(`${base}Name`) || existing.has(`${base}FolderPath`); n++) {
174
+ base = `b${slug.charAt(0).toUpperCase()}${slug.slice(1)}${n}`;
175
+ }
176
+ const ids = { name: `${base}Name`, folderPath: `${base}FolderPath` };
177
+ return { ids, entries: [
178
+ {
179
+ id: ids.name,
180
+ name: 'name',
181
+ type: 'string',
182
+ resource: 'ixpDeployment',
183
+ resourceKey: spec.versionId,
184
+ default: spec.name,
185
+ propertyAttribute: 'name',
186
+ },
187
+ {
188
+ id: ids.folderPath,
189
+ name: 'folderPath',
190
+ type: 'string',
191
+ resource: 'ixpDeployment',
192
+ resourceKey: spec.versionId,
193
+ default: spec.folderName,
194
+ propertyAttribute: 'folderPath',
195
+ },
196
+ ] };
197
+ }
198
+ /**
199
+ * The five forbidden inputs on an IxP node. They belonged to a prior schema and
200
+ * v1's own authoring reference calls them "the most common training-data-recall
201
+ * mistake" — the current node declares none of them, so each is an input the
202
+ * definition does not have.
203
+ *
204
+ * They are not reachable through `ixpExtract()`'s typed surface, which is the
205
+ * point of having one; this check exists for the JS caller and for a
206
+ * hand-written spec object, where TypeScript is not in the way.
207
+ */
208
+ const IXP_FORBIDDEN_INPUTS = ['digitizationMode', 'documentTaxonomy', 'attachmentId', 'fileName', 'mimeType'];
209
+ /**
210
+ * `ixpExtract(...)`-specific checks.
211
+ *
212
+ * All four identity fields are load-bearing on the tenant and none of them can
213
+ * be derived, so a missing one is an error rather than a warning: the node type
214
+ * is per-project, and the definition's own `inputDefinition` marks `modelName`
215
+ * and `folderKey` required (`minLength: 1`) beside `fileRef` — which is why
216
+ * `flow validate`'s `ixp-node` validator rejects a node with an empty one.
217
+ */
218
+ function checkIxpExtract(inputs, ctx) {
219
+ const step = ctx.step;
220
+ const at = (code, message, suggestion) => ctx.report(code, message, suggestion);
221
+ const blank = (v) => v === undefined || v === null || (typeof v === 'string' && v.trim() === '');
222
+ const project = String(inputs?.project ?? '').trim();
223
+ if (blank(project)) {
224
+ at('IXP_NO_PROJECT', `IxP step "${step}" has no \`project\`. That is the extractor's whole node type, and it carries the `
225
+ + `project version id and folder key the platform resolves the deployment by. Copy it from the registry `
226
+ + `— never construct it: three different spellings of its tail exist in deployed flows.`, `project: 'uipath.ixp.<slug>.<projectVersionId>-<folderKey>' // uip maestro flow registry pull --force && uip maestro flow registry search -f "type:startsWith=uipath.ixp"`);
227
+ }
228
+ else if (!project.startsWith('uipath.ixp.')) {
229
+ at('IXP_PROJECT_NOT_NODE_TYPE', `IxP step "${step}"'s \`project\` is ${JSON.stringify(inputs.project)}, which is not a \`uipath.ixp.*\` node `
230
+ + `type. This field takes the type verbatim from \`registry search\`, not the project's name or id — the `
231
+ + `name goes in \`name\` and the slug in \`modelName\`.`);
232
+ }
233
+ else if (parseIxpProjectType(project) === undefined) {
234
+ at('IXP_PROJECT_TAIL_UNPARSEABLE', `IxP step "${step}"'s \`project\` ends in ${JSON.stringify(project.slice(project.lastIndexOf('.') + 1))}, which is `
235
+ + `not the \`<projectVersionId>-<folderKey>\` pair of uuids the current registry serves. Two older spellings `
236
+ + `exist in the corpus — a sanitized folder path, and the literal string "undefined" — and neither resolves `
237
+ + `today. Re-read the type from \`registry search\` after \`registry pull --force\`.`);
238
+ }
239
+ if (blank(inputs?.modelName)) {
240
+ at('IXP_NO_MODEL_NAME', `IxP step "${step}" has no \`modelName\`. The definition marks it required (\`minLength: 1\`) and the `
241
+ + `platform's \`ixp-node\` validator refuses an empty one — it is the project's own name as the tenant `
242
+ + `spells it, which the node type's slug has lower-cased and de-punctuated beyond recovery.`, `modelName: 'invoiceixp-cef0d447-ixp' // registry get → inputDefaults.modelName`);
243
+ }
244
+ if (blank(inputs?.name)) {
245
+ at('IXP_NO_NAME', `IxP step "${step}" has no \`name\`. It is the deployment's display name — the \`ixpDeployment\` binding's `
246
+ + `\`name\`, which is how the deployed flow finds the project.`, `name: 'InvoiceIXP' // registry get → inputDefaults.model.modelDisplayName`);
247
+ }
248
+ if (blank(inputs?.folderName)) {
249
+ at('IXP_NO_FOLDER', `IxP step "${step}" has no \`folderName\`. IxP deployments are folder-scoped, and this is the binding's `
250
+ + `\`folderPath\` default — the folder the runtime looks in.`, `folderName: 'Shared' // registry get → inputDefaults.folderName`);
251
+ }
252
+ const fileRef = inputs?.fileRef;
253
+ if (fileRef === undefined || fileRef === null
254
+ || (typeof fileRef === 'string' && fileRef.trim() === '')) {
255
+ at('IXP_NO_FILE', `IxP step "${step}" has no \`fileRef\`. It is the node's only required INPUT (\`inputDefinition.required\`) `
256
+ + `and it takes a whole Flow Attachment object — normally a \`file\`-typed flow input.`, `fileRef: out('start', 'invoiceFile') // .input({ invoiceFile: types.file })`);
257
+ }
258
+ for (const f of IXP_FORBIDDEN_INPUTS) {
259
+ if (inputs?.[f] !== undefined) {
260
+ at('IXP_FORBIDDEN_INPUT', `IxP step "${step}" sets \`${f}\`, which the current node does not declare — it belonged to a prior `
261
+ + `schema. ${f === 'documentTaxonomy' ? 'The taxonomy travels in the resolved `model` blob.'
262
+ : f === 'digitizationMode' ? 'The serializer defaults it to `fileUpload` internally.'
263
+ : 'It is derived from `fileRef` upstream.'}`);
264
+ }
265
+ }
266
+ }
267
+ /**
268
+ * The extractor's node: the definition's `inputDefaults` with the file the
269
+ * author named, and the fixed output pair.
270
+ */
271
+ function serializeIxpExtract(s, ctx) {
272
+ const def = ixpExtractDef(s); // throws on an unparseable project
273
+ const { versionId } = parseIxpProjectType(s.project); // ixpExtractDef threw if it could not parse
274
+ // The bindings first: two steps on the same project share ONE pair, and the
275
+ // definition's `model.bindings.resourceKey` names the key they hang off.
276
+ const { entries } = ixpDeploymentBindings({ name: s.name, folderName: s.folderName, versionId }, ctx.bindings);
277
+ for (const b of entries)
278
+ ctx.bind(b);
279
+ return {
280
+ definition: { source: 'snapshot', nodeType: def.nodeType, version: def.version, manifest: def },
281
+ // No node-level `model` — v1's authoring reference makes this rule #15 for
282
+ // this family ("Studio Web–authored .flow files never carry one"), and 151
283
+ // of 151 deployed instances agree. Same reason the rpa/pattern families
284
+ // delete theirs.
285
+ model: 'omit',
286
+ // Every extractor's definition declares `success` (+ `error`, gated on
287
+ // `errorHandlingEnabled`) on the right — so the forward edge is `success`,
288
+ // as it is in all 151 deployed IxP instances.
289
+ port: 'success',
290
+ // The instance IS the definition's defaults, with the file the author named.
291
+ // Every field the designer writes, in the platform's own order.
292
+ inputs: {
293
+ ...jsonClone(def.inputDefaults),
294
+ fileRef: ctx.render(s.fileRef),
295
+ ...(s.pageRange === undefined ? {} : { pageRange: s.pageRange }),
296
+ },
297
+ // The instance's `outputs` is a FIXED four-field pair, not a copy of the
298
+ // definition's `outputDefinition` — which would drag the 18.7 KB extraction
299
+ // schema onto the node. v1's authoring reference is explicit ("copying
300
+ // outputDefinition verbatim also validates, but drags in a schema blob the
301
+ // runtime does not need"), and today's green nightly artifact carries
302
+ // exactly this. Both halves are REQUIRED: `flow validate`'s `ixp-node`
303
+ // validator fails a node missing either one.
304
+ outputs: {
305
+ output: { name: 'output', type: 'object', source: '=this', var: 'output' },
306
+ error: {
307
+ type: 'object',
308
+ description: 'Error information if the node fails',
309
+ source: '=Error',
310
+ var: 'error',
311
+ },
312
+ },
313
+ replaceOutputs: true,
314
+ };
315
+ }
316
+ function liftIxpExtract(node, ctx) {
317
+ const n = node;
318
+ const i = (n.inputs ?? {});
319
+ const plain = (k) => {
320
+ const v = ctx.sourceText(i[k]);
321
+ return v === undefined || v === '' ? undefined : v;
322
+ };
323
+ const fr = ctx.sourceText(i.fileRef) ?? '';
324
+ // The DEPLOYMENT's display name is nested in `inputs.model`, not at the top
325
+ // of `inputs` — the two are different values (the model block's
326
+ // `modelDisplayName` is 'InvoiceIXP'; `modelName` is the slug), so reading
327
+ // the wrong one recovers the slug as the name.
328
+ const model = (i.model ?? {});
329
+ const nested = (k) => typeof model[k] === 'string' && model[k] !== '' ? model[k] : undefined;
330
+ const lifted = {
331
+ project: n.type,
332
+ // `modelName`/`projectName` are one value in two fields; either recovers it.
333
+ modelName: plain('modelName') ?? plain('projectName') ?? nested('modelName') ?? '',
334
+ name: nested('modelDisplayName') ?? plain('modelName') ?? '',
335
+ folderName: plain('folderName') ?? nested('folderName') ?? '',
336
+ fileRef: fr.startsWith('=js:') ? new Expr(fr.slice(4)) : fr,
337
+ pageRange: plain('pageRange'),
338
+ versionTag: plain('versionTag'),
339
+ description: plain('description') ?? nested('description'),
340
+ };
341
+ return { kind: 'ixpExtract', inputs: lifted };
342
+ }
343
+ function printIxpExtract(spec, ctx) {
344
+ const l = spec.inputs;
345
+ const optional = (v) => (v === undefined ? undefined : ctx.string(v));
346
+ return {
347
+ factory: 'ixpExtract',
348
+ options: [
349
+ ['project', ctx.string(l.project)],
350
+ ['modelName', ctx.string(l.modelName)],
351
+ ['name', ctx.string(l.name)],
352
+ ['folderName', ctx.string(l.folderName)],
353
+ ['fileRef', ctx.value(l.fileRef)],
354
+ ['pageRange', optional(l.pageRange)],
355
+ ['versionTag', optional(l.versionTag)],
356
+ ['description', optional(l.description)],
357
+ ],
358
+ };
359
+ }
360
+ const ixpExtract10 = {
361
+ kind: 'ixpExtract', typeName: `${IXP_PREFIX}*`, version: IXP_DEF.version, factory: 'ixpExtract',
362
+ definition: 'per-tenant',
363
+ // The definition this SDK builds is the one serialize emits; asking it keeps
364
+ // the two in step (#1040). A project the builder cannot parse has no
365
+ // definition to ask, and the family's own rules report it (IXP_PROJECT_*).
366
+ errorPort: (spec) => {
367
+ try {
368
+ return definitionHasErrorPort(ixpExtractDef(spec.inputs));
369
+ }
370
+ catch {
371
+ return true;
372
+ }
373
+ },
374
+ serialize: (spec, ctx) => serializeIxpExtract(spec.inputs, ctx),
375
+ deserialize: (node, ctx) => liftIxpExtract(node, ctx),
376
+ print: (spec, ctx) => printIxpExtract(spec, ctx),
377
+ check: (spec, ctx) => checkIxpExtract(spec.inputs, ctx),
378
+ // check collected no expressions for an IxP step before the port; unchanged by it.
379
+ expressions: () => [],
380
+ output: () => ({ kind: 'opaque' }),
381
+ };
382
+ export const ixpExtractNodeType = {
383
+ kind: 'ixpExtract', typeName: `${IXP_PREFIX}*`, typeNamePrefix: IXP_PREFIX, factory: 'ixpExtract',
384
+ // An extractor is a tail of two uuids; Dynamic Extract's
385
+ // `uipath.ixp.extract-document-builder` and the older tails are not.
386
+ typeNameMatches: (typeName) => parseIxpProjectType(typeName) !== undefined,
387
+ defaultVersion: ixpExtract10.version,
388
+ versions: { [ixpExtract10.version]: ixpExtract10 },
389
+ };
@@ -5,9 +5,7 @@
5
5
  * docs/NODE_TYPE_ARCHITECTURE_DESIGN.md; serialized output, printed source,
6
6
  * codes and messages unchanged.
7
7
  *
8
- * Not here: `ixpExtract()` (`uipath.ixp.<project type>`), whose type name comes
9
- * from the model rather than being one fixed name, which the contract does not
10
- * express yet.
8
+ * `ixpExtract()` (`uipath.ixp.<slug>.<versionId>-<folderKey>`) is in `ixp.ts`.
11
9
  */
12
10
  import { toExpr } from '../../../core/expr.js';
13
11
  import { jsonClone } from '../../../core/json-clone.js';
@@ -1,11 +1,13 @@
1
1
  /**
2
2
  * The document family's authoring types — Document Classify
3
- * (`uipath.document.classify`) and Dynamic Extract
4
- * (`uipath.ixp.extract-document-builder`) — and their kinds' entries in
3
+ * (`uipath.document.classify`), Dynamic Extract
4
+ * (`uipath.ixp.extract-document-builder`) and an IxP project's extractor
5
+ * (`uipath.ixp.<slug>.<versionId>-<folderKey>`) — and their kinds' entries in
5
6
  * `FlowActionSpecs`. Moved from core/actions.ts in Phase 4 of
6
7
  * docs/NODE_TYPE_ARCHITECTURE_DESIGN.md.
7
8
  */
8
9
  import type { Expr } from '../../../core/expr.js';
10
+ /** What `documentClassify()` takes: the document, and how to split and classify it. */
9
11
  export interface DocumentClassifyInputs {
10
12
  /** The document to classify — a file reference (`input('file')`, `out('fetch', 'fileRef')`, …). */
11
13
  fileRef: string | Expr;
@@ -16,6 +18,7 @@ export interface DocumentClassifyInputs {
16
18
  /** Model configuration overrides; omit for the platform defaults (gemini_2_5_flash). */
17
19
  modelConfig?: Record<string, unknown>;
18
20
  }
21
+ /** What `dynamicExtract()` takes: the document and the fields to extract from it. */
19
22
  export interface DynamicExtractInputs {
20
23
  /** The document to extract from — a file reference. */
21
24
  fileRef: string | Expr;
@@ -51,6 +54,99 @@ export interface DynamicExtractInputs {
51
54
  /** Model configuration overrides; omit for the platform defaults. */
52
55
  modelConfig?: Record<string, unknown>;
53
56
  }
57
+ /**
58
+ * Split an extractor's node type into the two GUIDs its tail packs.
59
+ *
60
+ * `uipath.ixp.<projectSlug>.<projectVersionId>-<folderKey>` — the version id is
61
+ * the `ixpDeployment` binding's `resourceKey` and the node's `model.id`; the
62
+ * folder key is `inputs.folderKey`. Returns `undefined` for a tail that is not
63
+ * exactly two uuids joined by `-`, which is how the two older spellings (a
64
+ * sanitized folder path, and the literal string `undefined`) are refused rather
65
+ * than mis-split.
66
+ *
67
+ * @internal Public for compatibility; not an authoring factory.
68
+ */
69
+ export declare function parseIxpProjectType(nodeType: string): {
70
+ slug: string;
71
+ versionId: string;
72
+ folderKey: string;
73
+ } | undefined;
74
+ /** What `ixpExtract()` takes: the IxP project's extractor, its identity on the tenant, and the document. */
75
+ export interface IxpExtractInputs {
76
+ /**
77
+ * The extractor's node type, verbatim from
78
+ * `uip maestro flow registry search -f "type:startsWith=uipath.ixp"` — e.g.
79
+ * `'uipath.ixp.invoiceixp-cef0d447-ixp.ff973488-5d89-8035-86ac-f980b4be6deb-c4359cde-55f0-4f0e-9322-c6cdce74ab4c'`.
80
+ *
81
+ * **Run `uip maestro flow registry pull --force` first.** `registry search`
82
+ * and `registry get` answer from a local cache (`~/.uipath/nodes/`), so
83
+ * without a pull you are reading whenever you last synced.
84
+ *
85
+ * Two GUIDs are packed in that tail — the project **version** id and the
86
+ * **folder** key — and both are read back out of it here, which is why
87
+ * nothing else has to be passed twice.
88
+ */
89
+ project: string;
90
+ /**
91
+ * The project's own name, as the tenant spells it — the slug, e.g.
92
+ * `'idp-benchmark---invoices-c735405a-ixp'`. It is
93
+ * `inputDefaults.modelName` in `registry get`, and the node carries it as
94
+ * both `modelName` and `projectName` (one value, two fields — the designer
95
+ * shows it in two disabled boxes).
96
+ *
97
+ * It cannot be recovered from {@link IxpExtractInputs.project}: the node type's slug is
98
+ * lower-cased and de-punctuated (`---` collapses to `-`), which does not
99
+ * invert.
100
+ */
101
+ modelName: string;
102
+ /**
103
+ * The deployment's display name, e.g. `'InvoiceIXP'` —
104
+ * `inputDefaults.model.modelDisplayName`. It becomes the node's
105
+ * `model.modelDisplayName` and the `ixpDeployment` binding's `name`.
106
+ */
107
+ name: string;
108
+ /**
109
+ * The folder the project is published in, e.g. `'Shared'` or
110
+ * `'Shared/uipath-maestro-flow/BillingDispute'` —
111
+ * `inputDefaults.folderName`. It becomes the binding's `folderPath` default
112
+ * and the definition's own `description`.
113
+ */
114
+ folderName: string;
115
+ /**
116
+ * The document to extract from — a whole **Flow Attachment** object, the same
117
+ * shape and the same two ways of referencing it as
118
+ * {@link SummarizeInputs.attachment}:
119
+ *
120
+ * ```ts
121
+ * flow('InvoiceIntake')
122
+ * .input({ invoiceFile: types.file })
123
+ * .step('extract', ixpExtract({ …, fileRef: out('start', 'invoiceFile') }))
124
+ * ```
125
+ *
126
+ * It is the node's only authored input, and the definition marks it
127
+ * `required` (with `modelName` and `folderKey`, which this factory fills in).
128
+ */
129
+ fileRef: Expr;
130
+ /** Pages to process, e.g. `'1-5'`. Empty (the default) means the whole document. */
131
+ pageRange?: string;
132
+ /** Pin a published version tag. Empty (the default) means the latest. */
133
+ versionTag?: string;
134
+ /**
135
+ * The folder's key CHAIN, when the project lives in a nested folder —
136
+ * `inputDefaults.model.folderPath`, a dot-joined path of ancestor folder keys
137
+ * (`<root>.<parent>.<folder>`). Defaults to the folder key alone, which is
138
+ * what the platform writes for a project published straight into `Shared`.
139
+ */
140
+ folderPath?: string;
141
+ /**
142
+ * A free-text note on the node. Off by default, deliberately: the definition
143
+ * does not declare it (so setting it draws a "sets an input its type does not
144
+ * declare" warning unless the emitted definition is widened in the same
145
+ * write, which this does), and it is empty in 114 of the 117 deployed nodes
146
+ * that carry it at all.
147
+ */
148
+ description?: string;
149
+ }
54
150
  declare module '../../../core/actions.js' {
55
151
  interface FlowActionSpecs {
56
152
  /** The spec {@link documentClassify} builds. */
@@ -61,5 +157,9 @@ declare module '../../../core/actions.js' {
61
157
  dynamicExtract: {
62
158
  inputs: DynamicExtractInputs;
63
159
  };
160
+ /** The spec {@link ixpExtract} builds. */
161
+ ixpExtract: {
162
+ inputs: IxpExtractInputs;
163
+ };
64
164
  }
65
165
  }