@uipath/maestro-tool 1.202.2 → 1.203.0-preview.180
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/dist/THIRD-PARTY-NOTICES.md +83 -290
- package/dist/{authoring-818595dq.js → authoring-at68vr5g.js} +7 -5
- package/dist/{authoring-6r3wx3fa.js → authoring-sh284a0x.js} +3 -3
- package/dist/{authoring-61hqf5em.js → authoring-szfy0rh1.js} +5 -5
- package/dist/{binding-service-643e69bb.js → binding-service-8a05gy4k.js} +37 -29
- package/dist/{bpmn-action-type-validator-kx36sqhe.js → bpmn-action-type-validator-8kvyvvcf.js} +2 -2
- package/dist/bpmn-binding-resolver-5scq28mc.js +145 -0
- package/dist/{bpmn-branch-y1m9f10n.js → bpmn-branch-t12a7jvf.js} +50 -44
- package/dist/bpmn-format-service-ay6jd9he.js +69 -0
- package/dist/{bpmn-refresh-service-ew545y06.js → bpmn-refresh-service-ak1ctkwg.js} +4 -4
- package/dist/{bpmn-update-metadata-service-jhahhjab.js → bpmn-update-metadata-service-hqe4edze.js} +6 -5
- package/dist/{bpmn-validate-service-6z2dj8ac.js → bpmn-validate-service-48sfy3a2.js} +3 -3
- package/dist/{case-api-4mstfkzf.js → case-api-e3fmq685.js} +6 -6
- package/dist/case-bindings-service-nswc6v0v.js +240 -0
- package/dist/case-debug-studio-web-service-58gtqznh.js +135 -0
- package/dist/case-format-service-6s5e1ckk.js +84 -0
- package/dist/{case-pack-service-v7wtb0qt.js → case-pack-service-sg435he1.js} +62 -11
- package/dist/{case-validate-service-bnpaf0zt.js → case-validate-service-7s69shsa.js} +1424 -42
- package/dist/{connector-enrichment-service-scrqnke3.js → connector-enrichment-service-9rqr3esj.js} +27 -26
- package/dist/{connector-lookup-service-hnk08t3m.js → connector-lookup-service-pencasp9.js} +5 -5
- package/dist/{connector-service-581bhxaj.js → connector-service-k4cgcv2b.js} +224 -56
- package/dist/{connector-task-spec-y2tcthz5.js → connector-task-spec-13ex0x0m.js} +27 -26
- package/dist/{conversion-FQTRFHRX-03xh3xbp.js → conversion-FQTRFHRX-0y9br7gw.js} +6 -5
- package/dist/de-FGHEJRCI-gn4cf5gh.js +2697 -0
- package/dist/de-MUJPA4EB-dpg7t2rw.js +173 -0
- package/dist/{debug-conversation-trigger-nj04k7s2.js → debug-conversation-trigger-wgzjaasj.js} +17 -14
- package/dist/{debug-http-client-c4teyma3.js → debug-http-client-7dvbwp96.js} +2 -2
- package/dist/{debug-service-m5jdyz3q.js → debug-service-8cwxhgww.js} +3 -3
- package/dist/{edge-service-vv2yfsn4.js → edge-service-szpsaezp.js} +17 -14
- package/dist/{index-yqfqxh2r.js → embedded-file-reader-0sjt4nkj.js} +96 -16641
- package/dist/{en-TBNO2WUV-rfe87pgh.js → en-BWGKA5OE-1qkvb9rn.js} +2 -2
- package/dist/en-FBPVNKFN-dqt573xn.js +138 -0
- package/dist/en-TBNO2WUV-9nan43e5.js +1936 -0
- package/dist/{enrichment-service-a65qt7y1.js → enrichment-service-7n1tf75p.js} +4 -4
- package/dist/es-7SE7HX2X-8yh1aj2y.js +173 -0
- package/dist/es-KR3ZSNMP-0cszm2n3.js +2697 -0
- package/dist/es-MX-2MYFZU7X-3rek4r73.js +173 -0
- package/dist/es-MX-XXZJGC4R-b4fbwe9d.js +2697 -0
- package/dist/first-party-service-srxaftkn.js +27 -0
- package/dist/{flow-api-vqvvs0q2.js → flow-api-4vkpbmt3.js} +7 -7
- package/dist/{flow-debug-service-vd7bdyhg.js → flow-debug-service-9tad5ckh.js} +66 -41
- package/dist/{flow-eval-node-target-mve3v55h.js → flow-eval-node-target-g222rh2e.js} +22 -15
- package/dist/flow-eval-run-api-hashjgq1.js +13 -0
- package/dist/{flow-eval-run-service-rt9kg4wx.js → flow-eval-run-service-btghb02r.js} +109 -224
- package/dist/{flow-eval-service-ededh59n.js → flow-eval-service-ma5psrsd.js} +346 -302
- package/dist/{flow-init-service-4ve9tr0t.js → flow-init-service-662zhfd3.js} +22 -18
- package/dist/{flow-io-2dd3sx38.js → flow-io-3cqz0svg.js} +16 -13
- package/dist/{flow-migrate-service-nbe6et60.js → flow-migrate-service-ehx7nmsr.js} +18 -14
- package/dist/{flow-pack-service-b2n877d8.js → flow-pack-service-a3w3489r.js} +43 -35
- package/dist/flow-validate-service-02e21w4d.js +3555 -0
- package/dist/fr-MDFKNZ4C-h4a122k9.js +173 -0
- package/dist/fr-Y6NPVODW-q5ef3htr.js +2697 -0
- package/dist/{governance-fetcher-at165287.js → governance-fetcher-7vxmap1e.js} +5 -4
- package/dist/{headless-85f2cys0.js → headless-627q5my5.js} +42909 -1756
- package/dist/{hitl-service-xn1bt80z.js → hitl-service-fn94tk6z.js} +16 -13
- package/dist/{index-hfsfzg4k.js → index-3arytfxn.js} +6 -4
- package/dist/{index-d1yqqk6q.js → index-55cym5j7.js} +6 -4
- package/dist/{index-0j6xww0f.js → index-9f94yk28.js} +3 -3
- package/dist/{index-1kgxzh8x.js → index-fz4ttsde.js} +14 -2
- package/dist/index-gvwc29bb.js +16742 -0
- package/dist/{index-mkgrn150.js → index-j1azsfr5.js} +36 -33
- package/dist/index-kmp9fmhb.js +25 -0
- package/dist/{index-938ee96p.js → index-rtg24r4x.js} +1 -1
- package/dist/index-tzccx5ym.js +2961 -0
- package/dist/{index-acd6bpqc.js → index-ymsd3pyx.js} +12 -15
- package/dist/index.js +3 -3
- package/dist/{input-reader-1wda1a2v.js → input-reader-7pzkcw6t.js} +1 -1
- package/dist/{input-reader-svafwk7x.js → input-reader-j8rvde4z.js} +1 -1
- package/dist/ja-Q5PXKJ2V-tzf2rvz2.js +2697 -0
- package/dist/ja-UTQEAP34-8e0q6swp.js +173 -0
- package/dist/known-types-8dhhqk35.js +19 -0
- package/dist/ko-AXCHW2OK-askct7f6.js +173 -0
- package/dist/ko-AYBDSRZG-am0h0m2r.js +2697 -0
- package/dist/{maestro-automate-entry-gwntzkvj.js → maestro-automate-entry-jehpbg37.js} +2 -2
- package/dist/{maestro-debug-service-dvh7qbx8.js → maestro-debug-service-xhtszkrc.js} +10 -10
- package/dist/{maestro-pack-service-fam5zdm3.js → maestro-pack-service-fkkc5g3v.js} +12 -6
- package/dist/{node-ks3095fb.js → node-p4bde6qy.js} +2344 -376
- package/dist/node-service-frvz67y3.js +83 -0
- package/dist/{node-sync-service-zwvm30qg.js → node-sync-service-wa2t7c65.js} +14 -14
- package/dist/{packager-tool-me5q0naa.js → packager-tool-095jzn8v.js} +16 -6
- package/dist/{packager-tool-y5f2qhfe.js → packager-tool-19030vpg.js} +32 -2
- package/dist/{packager-tool-gtqkcj4x.js → packager-tool-2c1rbj85.js} +310 -13
- package/dist/packager-tool-2q1mffef.js +261 -0
- package/dist/{flow-validate-service-0hxf5080.js → packager-tool-2xrng5ff.js} +1956 -3065
- package/dist/{packager-tool-8dgxmjzx.js → packager-tool-30ky51ca.js} +6 -3
- package/dist/{packager-tool-d5mjdpbf.js → packager-tool-4r35ye9r.js} +5 -4
- package/dist/packager-tool-53w8skv5.js +262 -0
- package/dist/{packager-tool-db4jm1rz.js → packager-tool-56gcg9de.js} +1 -1
- package/dist/{packager-tool-29eg86w6.js → packager-tool-6rsegsby.js} +6 -7
- package/dist/packager-tool-7msx23gd.js +13 -0
- package/dist/packager-tool-7q6z6npd.js +1937 -0
- package/dist/packager-tool-8qp60s20.js +12 -0
- package/dist/{packager-tool-s1r7krkp.js → packager-tool-9pccpz3r.js} +1 -1
- package/dist/{packager-tool-m351tkkr.js → packager-tool-c95fhhys.js} +30 -8
- package/dist/{packager-tool-kg5sxneg.js → packager-tool-cmzzh4f8.js} +72 -73
- package/dist/{packager-tool-ffvmys2r.js → packager-tool-cr0z8cbj.js} +5 -5
- package/dist/{packager-tool-8fkt71n4.js → packager-tool-cs1ec1da.js} +4 -3
- package/dist/{packager-tool-xatnrtwp.js → packager-tool-d2813z49.js} +9 -9
- package/dist/packager-tool-d88rqwk8.js +135 -0
- package/dist/{packager-tool-pm3qh1hy.js → packager-tool-dh5nppv4.js} +1 -1
- package/dist/packager-tool-dqxz5v18.js +486 -0
- package/dist/{packager-tool-r87gxwqa.js → packager-tool-e87tzg4y.js} +2 -2
- package/dist/{packager-tool-w9fd7vzs.js → packager-tool-f33sg3vr.js} +9 -9
- package/dist/{packager-tool-cj2ka9jr.js → packager-tool-fer9mdsq.js} +2 -2
- package/dist/{packager-tool-70eh7rpg.js → packager-tool-fm67whdy.js} +23 -5
- package/dist/packager-tool-fq7wtkd3.js +21012 -0
- package/dist/packager-tool-fvm2dddf.js +6297 -0
- package/dist/packager-tool-gdtpsdn7.js +342 -0
- package/dist/{packager-tool-ejyeka5x.js → packager-tool-gpwec9sp.js} +2 -2
- package/dist/{packager-tool-pap09zfh.js → packager-tool-h8x54cvx.js} +1 -1
- package/dist/{packager-tool-9vh3df76.js → packager-tool-hdmhxdch.js} +7 -7
- package/dist/packager-tool-hnyecbk6.js +88 -0
- package/dist/{packager-tool-mtbak8t4.js → packager-tool-j00gcgbs.js} +2 -2
- package/dist/packager-tool-j78fwwhq.js +16 -0
- package/dist/packager-tool-jysvpt8p.js +11 -0
- package/dist/{packager-tool-jfv7t0c8.js → packager-tool-kb554twx.js} +94 -94
- package/dist/{packager-tool-k4x5yn3p.js → packager-tool-khm7zsm8.js} +4 -1
- package/dist/packager-tool-kj0qc4f4.js +269 -0
- package/dist/{packager-tool-12636p2y.js → packager-tool-mcjjj1hk.js} +4 -4
- package/dist/{packager-tool-bftjhmqz.js → packager-tool-mfrdmqmt.js} +124 -2
- package/dist/{packager-tool-qgh5yaeh.js → packager-tool-mj89jhvb.js} +1 -1
- package/dist/{packager-tool-ve35117x.js → packager-tool-mjz4hma1.js} +5791 -176
- package/dist/packager-tool-paq9c70w.js +1332 -0
- package/dist/{packager-tool-d8v984ah.js → packager-tool-q2gwvnf5.js} +15 -56
- package/dist/{packager-tool-nmznynbj.js → packager-tool-qff6ang6.js} +1 -1
- package/dist/{packager-tool-j79gbsee.js → packager-tool-rg5pkkf0.js} +6 -6
- package/dist/{packager-tool-7309ymg1.js → packager-tool-s1466723.js} +10 -13
- package/dist/{packager-tool-54b4v7wp.js → packager-tool-te6rrpsz.js} +205 -195
- package/dist/packager-tool-vcjq9xkc.js +1895 -0
- package/dist/{packager-tool-w20r6n9v.js → packager-tool-vck7ct1e.js} +2877 -2221
- package/dist/{packager-tool-e8td7b01.js → packager-tool-vdc8r21p.js} +10328 -16610
- package/dist/{packager-tool-nxw5yzs7.js → packager-tool-vqcnvaae.js} +1 -1
- package/dist/{packager-tool-7rz102fw.js → packager-tool-wd01egnx.js} +1 -1
- package/dist/{packager-tool-gxqd24n0.js → packager-tool-wger1vt9.js} +7 -7
- package/dist/{packager-tool-a6yfnjgx.js → packager-tool-wynrdnqj.js} +1 -1
- package/dist/packager-tool-x0eamrk4.js +104577 -0
- package/dist/packager-tool-x92czn5x.js +208 -0
- package/dist/{packager-tool-am67sn1c.js → packager-tool-xtkgeccn.js} +1 -1
- package/dist/packager-tool-y59jhapb.js +10369 -0
- package/dist/{packager-tool-317z6dc1.js → packager-tool-ydg78w9z.js} +23 -23
- package/dist/{packager-tool-rr8hveb9.js → packager-tool-ygrrb1zh.js} +1 -1
- package/dist/{packager-tool-f0kd5kq4.js → packager-tool-yrsrzz1a.js} +127 -12
- package/dist/{packager-tool-d9dpx9wn.js → packager-tool-z185rq2a.js} +1 -1
- package/dist/{packager-tool-79gws1zn.js → packager-tool-z6nkdv48.js} +270 -24
- package/dist/{packager-tool-802qm7jy.js → packager-tool-zzxt99e4.js} +3 -3
- package/dist/packager-tool.js +75 -218
- package/dist/{project-artifacts-9z7atbk7.js → project-artifacts-h5cydk2n.js} +1 -1
- package/dist/{project-validator-0rkrfmj0.js → project-validator-ek97s27p.js} +1 -1
- package/dist/pt-3DIO5QWG-59b3f4bc.js +173 -0
- package/dist/pt-BR-TKHNHMET-dn6zxk1d.js +2697 -0
- package/dist/pt-BR-Y6K6XHXK-mp3hwhw1.js +173 -0
- package/dist/pt-ROXU257Y-yd80gx6s.js +2697 -0
- package/dist/{resourcesSyncService-3pajvveg.js → resourcesSyncService-7s92g1hm.js} +28 -27
- package/dist/ro-AVAWPKOO-42m6am1w.js +2697 -0
- package/dist/ro-BWR3ONRR-vzx3hy8a.js +173 -0
- package/dist/ru-GSCT4ARZ-jrxwev63.js +10 -0
- package/dist/ru-WBLQ5B2U-s4em88p0.js +10 -0
- package/dist/sdd-lookup-7h7j2h6v.js +48 -0
- package/dist/{serialization-BXU6AKAN-dk0sqx89.js → serialization-BXU6AKAN-0ghhrsja.js} +6 -5
- package/dist/{shared-utils-twgpb9ax.js → shared-utils-6z7gnnc8.js} +1 -1
- package/dist/{strict-case-rules-zxnq80zm.js → strict-case-rules-byym8cvc.js} +141 -109
- package/dist/strict-sdd-audit-v156c9e3.js +2520 -0
- package/dist/tidy-service-zyy7wxc0.js +39 -0
- package/dist/{tool-pxfafj3m.js → tool-dejjyv8m.js} +213 -51
- package/dist/{tool-td30zmyt.js → tool-v0r5qzcc.js} +106 -146
- package/dist/tool.js +3 -3
- package/dist/tr-5IIHSFW5-hw4cewew.js +173 -0
- package/dist/tr-JMIP7ZSL-fb67ft9w.js +2696 -0
- package/dist/{trigger-enrichment-service-t3wcd12r.js → trigger-enrichment-service-ak1nrz5t.js} +30 -29
- package/dist/{variable-service-2q705dsx.js → variable-service-t9gsp8xs.js} +16 -13
- package/dist/{wait-for-flow-job-sx0ht8sb.js → wait-for-flow-job-1s8ka07s.js} +7 -7
- package/dist/{xml-949787yf.js → xml-3rzcgm9z.js} +1359 -659
- package/dist/zh-CN-IFFXSEJS-tke4mzty.js +2697 -0
- package/dist/zh-CN-R2KU4U5L-tc1hat59.js +173 -0
- package/dist/zh-TW-GNKMELBF-j674515r.js +173 -0
- package/dist/zh-TW-IYBNYAMG-wcan0qpb.js +2697 -0
- package/package.json +3 -2
- package/dist/bpmn-format-service-r67afbny.js +0 -477
- package/dist/case-debug-studio-web-service-kj1797m8.js +0 -336
- package/dist/known-types-eq32py2n.js +0 -20
- package/dist/node-service-teagjd9n.js +0 -75
- package/dist/packager-tool-15bstxb6.js +0 -4192
- package/dist/packager-tool-1zpm7wts.js +0 -7217
- package/dist/packager-tool-7tjxtpfp.js +0 -106
- package/dist/packager-tool-ckhyvcn5.js +0 -58
- package/dist/packager-tool-dz506qqq.js +0 -2120
- package/dist/packager-tool-hpc2j0zw.js +0 -27
- package/dist/packager-tool-jjpc0yka.js +0 -304
- package/dist/packager-tool-xtv236p7.js +0 -73
- package/dist/strict-sdd-audit-zc0n5vj6.js +0 -1024
- package/dist/tidy-service-hc9gex0v.js +0 -36
- /package/dist/{localization-xecwjpe2.js → localization-y790z9vm.js} +0 -0
|
@@ -0,0 +1,3555 @@
|
|
|
1
|
+
import {
|
|
2
|
+
UI_ONLY_NODE_TYPES
|
|
3
|
+
} from "./packager-tool-2c1rbj85.js";
|
|
4
|
+
import {
|
|
5
|
+
checkForwardMigration
|
|
6
|
+
} from "./packager-tool-j00gcgbs.js";
|
|
7
|
+
import {
|
|
8
|
+
connectorNodeValidator,
|
|
9
|
+
crossNodeBindingValidator,
|
|
10
|
+
isConnectorTriggerType,
|
|
11
|
+
isConnectorWaitForEventType,
|
|
12
|
+
isEventDrivenConnectorType,
|
|
13
|
+
readEssentialConfiguration
|
|
14
|
+
} from "./packager-tool-2q1mffef.js";
|
|
15
|
+
import {
|
|
16
|
+
ERROR_SOURCE_PORT,
|
|
17
|
+
deriveHandleCustomizationSourceOverride,
|
|
18
|
+
deriveOutcomeHandleIds,
|
|
19
|
+
expandHandleIds,
|
|
20
|
+
isHandleVisible,
|
|
21
|
+
isTemplatedHandleId,
|
|
22
|
+
parseVisibleInputKey,
|
|
23
|
+
portMatchesTemplate
|
|
24
|
+
} from "./packager-tool-19030vpg.js";
|
|
25
|
+
import {
|
|
26
|
+
collectNodeBindingResourceKeys,
|
|
27
|
+
isProcessNode,
|
|
28
|
+
resolveProcessTarget
|
|
29
|
+
} from "./packager-tool-q2gwvnf5.js";
|
|
30
|
+
import {
|
|
31
|
+
isRecord
|
|
32
|
+
} from "./packager-tool-mfrdmqmt.js";
|
|
33
|
+
import {
|
|
34
|
+
findNodeDefinition
|
|
35
|
+
} from "./packager-tool-7msx23gd.js";
|
|
36
|
+
import {
|
|
37
|
+
loadRegistry,
|
|
38
|
+
parse
|
|
39
|
+
} from "./packager-tool-2xrng5ff.js";
|
|
40
|
+
import"./packager-tool-dqxz5v18.js";
|
|
41
|
+
import"./packager-tool-d2813z49.js";
|
|
42
|
+
import"./packager-tool-9pccpz3r.js";
|
|
43
|
+
import {
|
|
44
|
+
resolveProjectFlowFile
|
|
45
|
+
} from "./packager-tool-zzxt99e4.js";
|
|
46
|
+
import"./packager-tool-cr0z8cbj.js";
|
|
47
|
+
import"./packager-tool-ygrrb1zh.js";
|
|
48
|
+
import"./packager-tool-eypvc2kv.js";
|
|
49
|
+
import"./packager-tool-ydg78w9z.js";
|
|
50
|
+
import"./packager-tool-8qp60s20.js";
|
|
51
|
+
import"./packager-tool-fq7wtkd3.js";
|
|
52
|
+
import"./packager-tool-dh5nppv4.js";
|
|
53
|
+
import {
|
|
54
|
+
ManifestClient,
|
|
55
|
+
buildErrorHandle
|
|
56
|
+
} from "./packager-tool-vck7ct1e.js";
|
|
57
|
+
import"./packager-tool-paq9c70w.js";
|
|
58
|
+
import"./packager-tool-4r35ye9r.js";
|
|
59
|
+
import {
|
|
60
|
+
getLoginStatusAsync
|
|
61
|
+
} from "./packager-tool-yrsrzz1a.js";
|
|
62
|
+
import"./packager-tool-0gctz400.js";
|
|
63
|
+
import"./packager-tool-qa9gftnj.js";
|
|
64
|
+
import"./packager-tool-yc4kw997.js";
|
|
65
|
+
import"./packager-tool-095jzn8v.js";
|
|
66
|
+
import"./packager-tool-mj89jhvb.js";
|
|
67
|
+
import"./packager-tool-s1466723.js";
|
|
68
|
+
import {
|
|
69
|
+
findNearestParentUipxFile,
|
|
70
|
+
readSolutionManifest
|
|
71
|
+
} from "./packager-tool-wynrdnqj.js";
|
|
72
|
+
import"./packager-tool-kb554twx.js";
|
|
73
|
+
import"./packager-tool-h8x54cvx.js";
|
|
74
|
+
import {
|
|
75
|
+
canonicalAgentInputPath,
|
|
76
|
+
collectAgentInputLeaves,
|
|
77
|
+
createFlowRefResolver,
|
|
78
|
+
resolveWorkflowRefs
|
|
79
|
+
} from "./packager-tool-te6rrpsz.js";
|
|
80
|
+
import"./packager-tool-6rsegsby.js";
|
|
81
|
+
import"./packager-tool-wd01egnx.js";
|
|
82
|
+
import"./packager-tool-2513jrxr.js";
|
|
83
|
+
import"./packager-tool-khm7zsm8.js";
|
|
84
|
+
import"./packager-tool-vcjq9xkc.js";
|
|
85
|
+
import"./packager-tool-b803sdfp.js";
|
|
86
|
+
import"./packager-tool-nkrmjr3a.js";
|
|
87
|
+
import"./packager-tool-rs6jm14d.js";
|
|
88
|
+
import"./packager-tool-hk1gxnth.js";
|
|
89
|
+
import"./packager-tool-7q6z6npd.js";
|
|
90
|
+
import"./packager-tool-sc961w0q.js";
|
|
91
|
+
import"./packager-tool-gpwec9sp.js";
|
|
92
|
+
import"./packager-tool-vqcnvaae.js";
|
|
93
|
+
import"./packager-tool-gbgfwx8f.js";
|
|
94
|
+
import"./packager-tool-d88rqwk8.js";
|
|
95
|
+
import"./packager-tool-jysvpt8p.js";
|
|
96
|
+
import {
|
|
97
|
+
AGENT_NODE_TYPES,
|
|
98
|
+
DECISION_NODE_TYPE,
|
|
99
|
+
SYNTHETIC_GATEWAY_OUTPUT_KEYS,
|
|
100
|
+
cliRules,
|
|
101
|
+
conditionExpressionRule,
|
|
102
|
+
decodeLiteral,
|
|
103
|
+
getJsExpressionBody,
|
|
104
|
+
isBlockingSeverity,
|
|
105
|
+
isExpressionValue,
|
|
106
|
+
isStartEventNode,
|
|
107
|
+
isVoiceAgentNodeType,
|
|
108
|
+
parseWorkflowJson,
|
|
109
|
+
runRules,
|
|
110
|
+
toSerializedNodes
|
|
111
|
+
} from "./packager-tool-fvm2dddf.js";
|
|
112
|
+
import {
|
|
113
|
+
registerFlowSchemaI18n
|
|
114
|
+
} from "./packager-tool-p79yh9ww.js";
|
|
115
|
+
import {
|
|
116
|
+
instance
|
|
117
|
+
} from "./packager-tool-vdc8r21p.js";
|
|
118
|
+
import"./packager-tool-z83pga0z.js";
|
|
119
|
+
import"./packager-tool-6ejqwbw9.js";
|
|
120
|
+
import"./packager-tool-mcaf4ewf.js";
|
|
121
|
+
import {
|
|
122
|
+
OutputFormatter,
|
|
123
|
+
RESULTS,
|
|
124
|
+
catchError,
|
|
125
|
+
extractErrorMessageSync,
|
|
126
|
+
logger
|
|
127
|
+
} from "./packager-tool-z6nkdv48.js";
|
|
128
|
+
import {
|
|
129
|
+
getFileSystem
|
|
130
|
+
} from "./packager-tool-0v3s8r01.js";
|
|
131
|
+
import"./packager-tool-4mna26cc.js";
|
|
132
|
+
import"./packager-tool-c23zrhj8.js";
|
|
133
|
+
import {
|
|
134
|
+
__require
|
|
135
|
+
} from "./packager-tool-1yh8g550.js";
|
|
136
|
+
|
|
137
|
+
// ../flow-tool/src/services/connector-binding-validator.ts
|
|
138
|
+
var STATIC_CONNECTOR_KEYWORDS = [
|
|
139
|
+
"slack",
|
|
140
|
+
"gmail",
|
|
141
|
+
"outlook",
|
|
142
|
+
"office365",
|
|
143
|
+
"exchange",
|
|
144
|
+
"jira",
|
|
145
|
+
"confluence",
|
|
146
|
+
"salesforce",
|
|
147
|
+
"sfdc",
|
|
148
|
+
"hubspot",
|
|
149
|
+
"servicenow",
|
|
150
|
+
"zendesk",
|
|
151
|
+
"intercom",
|
|
152
|
+
"notion",
|
|
153
|
+
"asana",
|
|
154
|
+
"trello",
|
|
155
|
+
"monday",
|
|
156
|
+
"github",
|
|
157
|
+
"gitlab",
|
|
158
|
+
"bitbucket",
|
|
159
|
+
"discord",
|
|
160
|
+
"teams",
|
|
161
|
+
"sharepoint",
|
|
162
|
+
"onedrive",
|
|
163
|
+
"dropbox",
|
|
164
|
+
"box",
|
|
165
|
+
"googledrive",
|
|
166
|
+
"docusign",
|
|
167
|
+
"adobesign",
|
|
168
|
+
"mailchimp",
|
|
169
|
+
"sendgrid",
|
|
170
|
+
"twilio",
|
|
171
|
+
"stripe",
|
|
172
|
+
"shopify",
|
|
173
|
+
"quickbooks",
|
|
174
|
+
"xero",
|
|
175
|
+
"workday",
|
|
176
|
+
"successfactors",
|
|
177
|
+
"bamboohr",
|
|
178
|
+
"sap",
|
|
179
|
+
"netsuite",
|
|
180
|
+
"oracle",
|
|
181
|
+
"marketo",
|
|
182
|
+
"pardot",
|
|
183
|
+
"segment",
|
|
184
|
+
"snowflake",
|
|
185
|
+
"bigquery",
|
|
186
|
+
"redshift",
|
|
187
|
+
"databricks",
|
|
188
|
+
"tableau",
|
|
189
|
+
"powerbi"
|
|
190
|
+
];
|
|
191
|
+
var ACTION_VERBS = [
|
|
192
|
+
"send",
|
|
193
|
+
"post",
|
|
194
|
+
"create",
|
|
195
|
+
"update",
|
|
196
|
+
"delete",
|
|
197
|
+
"get",
|
|
198
|
+
"fetch",
|
|
199
|
+
"list",
|
|
200
|
+
"read",
|
|
201
|
+
"write",
|
|
202
|
+
"upload",
|
|
203
|
+
"download",
|
|
204
|
+
"find",
|
|
205
|
+
"add",
|
|
206
|
+
"remove",
|
|
207
|
+
"query",
|
|
208
|
+
"search",
|
|
209
|
+
"notify",
|
|
210
|
+
"publish",
|
|
211
|
+
"subscribe",
|
|
212
|
+
"trigger",
|
|
213
|
+
"watch",
|
|
214
|
+
"share",
|
|
215
|
+
"invite",
|
|
216
|
+
"schedule",
|
|
217
|
+
"assign",
|
|
218
|
+
"close",
|
|
219
|
+
"open",
|
|
220
|
+
"reply",
|
|
221
|
+
"forward",
|
|
222
|
+
"archive"
|
|
223
|
+
];
|
|
224
|
+
var MANAGED_HTTP_NODE_TYPE = "core.action.http.v2";
|
|
225
|
+
async function loadConnectorCatalog() {
|
|
226
|
+
const keywords = new Set(STATIC_CONNECTOR_KEYWORDS);
|
|
227
|
+
const activityTokenBags = [];
|
|
228
|
+
const [err, registry] = await catchError(loadRegistry());
|
|
229
|
+
if (err) {
|
|
230
|
+
logger.debug(`Connector-binding validator: could not load IS catalog cache (${err.message}); falling back to static keyword list.`);
|
|
231
|
+
return { keywords, activityTokenBags };
|
|
232
|
+
}
|
|
233
|
+
if (!registry) {
|
|
234
|
+
return { keywords, activityTokenBags };
|
|
235
|
+
}
|
|
236
|
+
for (const connector of registry.discovered.connectors) {
|
|
237
|
+
addConnectorKeywords(connector, keywords);
|
|
238
|
+
for (const activity of connector.activities) {
|
|
239
|
+
const tokens = tokenize(activity.displayName);
|
|
240
|
+
if (tokens.size > 0) {
|
|
241
|
+
activityTokenBags.push({
|
|
242
|
+
connectorKey: connector.key,
|
|
243
|
+
connectorName: connector.name,
|
|
244
|
+
tokens
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return { keywords, activityTokenBags };
|
|
250
|
+
}
|
|
251
|
+
function addConnectorKeywords(connector, out) {
|
|
252
|
+
const keyParts = connector.key.toLowerCase().split("-").filter((p) => p && p !== "uipath");
|
|
253
|
+
for (const part of keyParts)
|
|
254
|
+
out.add(part);
|
|
255
|
+
for (const t of tokenize(connector.name))
|
|
256
|
+
out.add(t);
|
|
257
|
+
}
|
|
258
|
+
function tokenize(label) {
|
|
259
|
+
if (!label)
|
|
260
|
+
return new Set;
|
|
261
|
+
const tokens = label.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 1);
|
|
262
|
+
return new Set(tokens);
|
|
263
|
+
}
|
|
264
|
+
function readNodeLabel(node) {
|
|
265
|
+
const fromDisplay = node.display?.label;
|
|
266
|
+
if (typeof fromDisplay === "string" && fromDisplay.length > 0) {
|
|
267
|
+
return fromDisplay;
|
|
268
|
+
}
|
|
269
|
+
const fromName = node.name;
|
|
270
|
+
if (typeof fromName === "string" && fromName.length > 0) {
|
|
271
|
+
return fromName;
|
|
272
|
+
}
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
function hasConnectorBinding(node) {
|
|
276
|
+
const detail = node.inputs?.detail;
|
|
277
|
+
if (!detail || typeof detail !== "object" || Array.isArray(detail)) {
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
const d = detail;
|
|
281
|
+
const authentication = d.authentication;
|
|
282
|
+
if (authentication === "connector") {
|
|
283
|
+
const connectionId = d.connectionId;
|
|
284
|
+
const targetConnector = d.targetConnector;
|
|
285
|
+
return typeof connectionId === "string" && connectionId.length > 0 && connectionId !== "ImplicitConnection" && typeof targetConnector === "string" && targetConnector.length > 0;
|
|
286
|
+
}
|
|
287
|
+
if (authentication === "manual") {
|
|
288
|
+
const url = d.url;
|
|
289
|
+
return typeof url === "string" && url.length > 0;
|
|
290
|
+
}
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
function classifyLabel(label, catalog) {
|
|
294
|
+
const tokens = tokenize(label);
|
|
295
|
+
if (tokens.size === 0)
|
|
296
|
+
return null;
|
|
297
|
+
for (const bag of catalog.activityTokenBags) {
|
|
298
|
+
let overlap = 0;
|
|
299
|
+
for (const t of bag.tokens) {
|
|
300
|
+
if (tokens.has(t))
|
|
301
|
+
overlap += 1;
|
|
302
|
+
}
|
|
303
|
+
if (overlap >= 2) {
|
|
304
|
+
const keyword = pickConnectorKeyword(tokens, catalog.keywords);
|
|
305
|
+
return {
|
|
306
|
+
connectorKeyword: keyword ?? bag.connectorKey,
|
|
307
|
+
matchedActivity: {
|
|
308
|
+
connectorKey: bag.connectorKey,
|
|
309
|
+
connectorName: bag.connectorName
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
const matchedKeyword = pickConnectorKeyword(tokens, catalog.keywords);
|
|
315
|
+
if (!matchedKeyword)
|
|
316
|
+
return null;
|
|
317
|
+
const hasVerb = [...tokens].some((t) => ACTION_VERBS.includes(t));
|
|
318
|
+
if (!hasVerb)
|
|
319
|
+
return null;
|
|
320
|
+
return { connectorKeyword: matchedKeyword };
|
|
321
|
+
}
|
|
322
|
+
function pickConnectorKeyword(tokens, keywords) {
|
|
323
|
+
for (const t of tokens) {
|
|
324
|
+
if (keywords.has(t))
|
|
325
|
+
return t;
|
|
326
|
+
}
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
function buildDiagnosticMessage(label, classification, strict) {
|
|
330
|
+
const connectorHint = classification.matchedActivity ? `looks like a "${classification.matchedActivity.connectorName}" connector activity` : `mentions the "${classification.connectorKeyword}" connector keyword`;
|
|
331
|
+
const strictNote = strict ? " (--strict-bindings promoted this to an error.)" : " Pass `--strict-bindings` to fail validation on this.";
|
|
332
|
+
const connectorDetail = `{"authentication":"connector",` + `"targetConnector":"${classification.connectorKeyword}",` + `"connectionId":"<uuid>","folderKey":"<uuid>","method":"GET","url":"<path>"}`;
|
|
333
|
+
const manualDetail = `{"authentication":"manual","method":"GET","url":"https://…"}`;
|
|
334
|
+
return `Node "${label}" ${connectorHint} but uses the generic Managed HTTP type \`${MANAGED_HTTP_NODE_TYPE}\` ` + "with no connection binding. The runtime will issue a raw HTTP call to an undefined endpoint. " + `Either bind a real Integration Service connection with \`uip maestro flow node configure <file> <node-id> --detail '${connectorDetail}'\`, ` + `or configure manual-mode HTTP with \`--detail '${manualDetail}'\`. ` + "If this node is intentionally a raw HTTP call, rename it so the label doesn't " + `imply a connector binding.${strictNote}`;
|
|
335
|
+
}
|
|
336
|
+
var MCP_CONNECTOR_KEY = "uipath-uipath-mcp";
|
|
337
|
+
function readDetail(node) {
|
|
338
|
+
const detail = node.inputs?.detail;
|
|
339
|
+
if (!detail || typeof detail !== "object" || Array.isArray(detail)) {
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
return detail;
|
|
343
|
+
}
|
|
344
|
+
var MCP_RUN_TOOL_SEGMENT = /run[-_]?a?[-_]?tool/;
|
|
345
|
+
var MCP_RUN_TOOL_OBJECT = "mcp_request";
|
|
346
|
+
function isMcpRunToolNode(node) {
|
|
347
|
+
const type = typeof node.type === "string" ? node.type : "";
|
|
348
|
+
const detail = readDetail(node);
|
|
349
|
+
const connector = typeof detail?.connector === "string" ? detail.connector : "";
|
|
350
|
+
if (connector !== MCP_CONNECTOR_KEY && !type.includes(MCP_CONNECTOR_KEY)) {
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
const endpoint = typeof detail?.endpoint === "string" ? detail.endpoint : "";
|
|
354
|
+
const objectName = detail ? readEssentialConfiguration(detail)?.objectName : undefined;
|
|
355
|
+
return MCP_RUN_TOOL_SEGMENT.test(type) || endpoint.includes(MCP_RUN_TOOL_OBJECT) || objectName === MCP_RUN_TOOL_OBJECT;
|
|
356
|
+
}
|
|
357
|
+
function definitionRequiresToolName(definitions, node) {
|
|
358
|
+
const def = findNodeDefinition(definitions, node);
|
|
359
|
+
const input = def?.inputDefinition;
|
|
360
|
+
const detail = input?.properties?.detail;
|
|
361
|
+
const body = detail?.properties?.bodyParameters;
|
|
362
|
+
return Array.isArray(body?.required) && body.required.includes("toolName");
|
|
363
|
+
}
|
|
364
|
+
function readToolName(node) {
|
|
365
|
+
const detail = readDetail(node);
|
|
366
|
+
const body = detail?.bodyParameters;
|
|
367
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
return body.toolName;
|
|
371
|
+
}
|
|
372
|
+
function hasSelectedTool(node) {
|
|
373
|
+
const toolName = readToolName(node);
|
|
374
|
+
return typeof toolName === "string" && toolName.trim().length > 0;
|
|
375
|
+
}
|
|
376
|
+
function hasMcpConnectionBinding(node) {
|
|
377
|
+
const detail = readDetail(node);
|
|
378
|
+
for (const key of ["connectionId", "connectionResourceId"]) {
|
|
379
|
+
const value = detail?.[key];
|
|
380
|
+
if (typeof value === "string" && value.trim().length > 0)
|
|
381
|
+
return true;
|
|
382
|
+
}
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
function validateConnectorBindings(workflow, catalog, options = {}) {
|
|
386
|
+
const issues = [];
|
|
387
|
+
const severity = options.strict ? "error" : "warning";
|
|
388
|
+
const checkNode = (node, scope) => {
|
|
389
|
+
if (isMcpRunToolNode(node)) {
|
|
390
|
+
const toolName = readToolName(node);
|
|
391
|
+
const schemaCatchesIt = (typeof toolName !== "string" || toolName === "") && definitionRequiresToolName(workflow.definitions, node);
|
|
392
|
+
if (hasMcpConnectionBinding(node) && !hasSelectedTool(node) && !schemaCatchesIt) {
|
|
393
|
+
issues.push({
|
|
394
|
+
path: `${scope}nodes[${node.id}].inputs.detail.bodyParameters.toolName`,
|
|
395
|
+
severity,
|
|
396
|
+
message: `MCP "Run a Tool" node "${readNodeLabel(node) ?? node.id}" has no selected tool — ` + "`inputs.detail.bodyParameters.toolName` is empty or missing. Studio blocks publish " + 'with "Available Tools is required" and the runtime fails with ' + "`AGENT_RUNTIME.HTTP_ERROR: Not Found`. Select a tool for this node in Studio, or " + "set `inputs.detail.bodyParameters.toolName` to the tool's name." + (options.strict === true ? "" : " Pass `--strict-bindings` to fail validation on this.")
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
if (node.type !== MANAGED_HTTP_NODE_TYPE)
|
|
402
|
+
return;
|
|
403
|
+
if (hasConnectorBinding(node))
|
|
404
|
+
return;
|
|
405
|
+
const label = readNodeLabel(node);
|
|
406
|
+
if (!label)
|
|
407
|
+
return;
|
|
408
|
+
const classification = classifyLabel(label, catalog);
|
|
409
|
+
if (!classification)
|
|
410
|
+
return;
|
|
411
|
+
issues.push({
|
|
412
|
+
path: `${scope}nodes[${node.id}].inputs.detail`,
|
|
413
|
+
severity,
|
|
414
|
+
message: buildDiagnosticMessage(label, classification, options.strict === true)
|
|
415
|
+
});
|
|
416
|
+
};
|
|
417
|
+
for (const node of workflow.nodes) {
|
|
418
|
+
checkNode(node, "");
|
|
419
|
+
}
|
|
420
|
+
for (const [subflowId, sub] of Object.entries(workflow.subflows ?? {})) {
|
|
421
|
+
for (const node of sub.nodes ?? []) {
|
|
422
|
+
checkNode(node, `subflows[${subflowId}].`);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return issues;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// ../flow-tool/src/services/expression-validator.ts
|
|
429
|
+
var catalogRegistered = false;
|
|
430
|
+
async function ensureCatalogRegistered() {
|
|
431
|
+
if (catalogRegistered || !instance.isInitialized)
|
|
432
|
+
return;
|
|
433
|
+
try {
|
|
434
|
+
const { registerFlowExpressionModelI18n } = await import("./localization-y790z9vm.js");
|
|
435
|
+
registerFlowExpressionModelI18n(instance);
|
|
436
|
+
} catch (error) {
|
|
437
|
+
logger.warn(`Expression type checking localization unavailable, falling back to English: ${extractErrorMessageSync(error)}`);
|
|
438
|
+
}
|
|
439
|
+
catalogRegistered = true;
|
|
440
|
+
}
|
|
441
|
+
var headlessModule;
|
|
442
|
+
function loadHeadless() {
|
|
443
|
+
headlessModule ??= import("./headless-627q5my5.js").then((mod) => {
|
|
444
|
+
logger.info(`Expression type checking: @uipath/flow-expression-model (TypeScript ${mod.HEADLESS_TYPESCRIPT_VERSION})`);
|
|
445
|
+
return mod;
|
|
446
|
+
}).catch((error) => {
|
|
447
|
+
logger.warn(`Expression type checking unavailable, skipping: ${extractErrorMessageSync(error)}`);
|
|
448
|
+
return null;
|
|
449
|
+
});
|
|
450
|
+
return headlessModule;
|
|
451
|
+
}
|
|
452
|
+
function findingPath(finding) {
|
|
453
|
+
if (!finding.nodeId)
|
|
454
|
+
return finding.fieldPath ?? "(workflow)";
|
|
455
|
+
if (!finding.fieldPath)
|
|
456
|
+
return `nodes[${finding.nodeId}]`;
|
|
457
|
+
return `nodes[${finding.nodeId}].${finding.fieldPath}`;
|
|
458
|
+
}
|
|
459
|
+
async function validateExpressions(graph, pathPrefix, strict = false) {
|
|
460
|
+
await ensureCatalogRegistered();
|
|
461
|
+
const headless = await loadHeadless();
|
|
462
|
+
if (!headless)
|
|
463
|
+
return [];
|
|
464
|
+
const [error, findings] = await catchError(headless.checkWorkflowExpressionsHeadless(graph));
|
|
465
|
+
if (error || !findings) {
|
|
466
|
+
logger.warn(`Expression type checking failed, skipping: ${extractErrorMessageSync(error)}`);
|
|
467
|
+
return [];
|
|
468
|
+
}
|
|
469
|
+
return findings.map((finding) => ({
|
|
470
|
+
path: `${pathPrefix}${findingPath(finding)}`,
|
|
471
|
+
message: `[${finding.code}] ${finding.message}`,
|
|
472
|
+
severity: strict ? "error" : "warning"
|
|
473
|
+
}));
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// ../flow-tool/src/services/node-validators/activity-type-id-validator.ts
|
|
477
|
+
var activityTypeIdValidator = {
|
|
478
|
+
id: "activity-type-id",
|
|
479
|
+
appliesTo: (node) => {
|
|
480
|
+
const t = node.type;
|
|
481
|
+
if (!t)
|
|
482
|
+
return false;
|
|
483
|
+
return t === "core.action.http.v2" || t.startsWith("uipath.connector.");
|
|
484
|
+
},
|
|
485
|
+
validate: (node) => {
|
|
486
|
+
const rawDetail = node.inputs?.detail;
|
|
487
|
+
if (!rawDetail || typeof rawDetail !== "object" || Array.isArray(rawDetail)) {
|
|
488
|
+
return [];
|
|
489
|
+
}
|
|
490
|
+
const detail = rawDetail;
|
|
491
|
+
if (Object.keys(detail).length === 0)
|
|
492
|
+
return [];
|
|
493
|
+
const activityTypeId = detail.uiPathActivityTypeId;
|
|
494
|
+
if (typeof activityTypeId === "string" && activityTypeId.length > 0) {
|
|
495
|
+
return [];
|
|
496
|
+
}
|
|
497
|
+
const nodeKind = node.type === "core.action.http.v2" ? "Managed HTTP node" : isConnectorTriggerType(node.type) ? "Connector trigger node" : isConnectorWaitForEventType(node.type) ? "Connector wait-for-event node" : "Connector activity node";
|
|
498
|
+
const quotedType = shellSingleQuote(node.type);
|
|
499
|
+
const quotedId = shellSingleQuote(node.id);
|
|
500
|
+
return [
|
|
501
|
+
{
|
|
502
|
+
path: `nodes[${node.id}].inputs.detail.uiPathActivityTypeId`,
|
|
503
|
+
severity: "error",
|
|
504
|
+
message: `${nodeKind} (\`${node.type}\`): \`inputs.detail.uiPathActivityTypeId\` is missing — Studio Web crashes when opening this flow. ` + `Fix: refresh the embedded definition without re-IDing the node — \`uip maestro flow registry get ${quotedType} --output json\`, replace the matching entry in \`definitions[]\` with the returned node object (preserve \`typeVersion\` exactly as returned), ` + `then \`uip maestro flow node configure <file> ${quotedId} --detail <json>\` to re-populate \`inputs.detail.uiPathActivityTypeId\` from the fresh manifest.`
|
|
505
|
+
}
|
|
506
|
+
];
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
function shellSingleQuote(value) {
|
|
510
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// ../flow-tool/src/services/node-validators/ceql-query-validator.ts
|
|
514
|
+
var CEQL_SYMBOL_OPERATORS = ["=", "!=", "<", "<=", ">", ">="];
|
|
515
|
+
var CEQL_WORD_OPERATORS = [
|
|
516
|
+
"In",
|
|
517
|
+
"Not In",
|
|
518
|
+
"Like",
|
|
519
|
+
"Not Like",
|
|
520
|
+
"Contains",
|
|
521
|
+
"Not Contains",
|
|
522
|
+
"Starts With",
|
|
523
|
+
"Not Starts With",
|
|
524
|
+
"Ends With",
|
|
525
|
+
"Not Ends With",
|
|
526
|
+
"Is Null",
|
|
527
|
+
"Is Not Null"
|
|
528
|
+
];
|
|
529
|
+
var CEQL_OPERATOR_ALIASES = {
|
|
530
|
+
eq: "=",
|
|
531
|
+
ne: "!=",
|
|
532
|
+
gt: ">",
|
|
533
|
+
ge: ">=",
|
|
534
|
+
lt: "<",
|
|
535
|
+
le: "<="
|
|
536
|
+
};
|
|
537
|
+
var byLengthDescending = (a, b) => b.length - a.length;
|
|
538
|
+
var escapeRegexToken = (token) => token.replace(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
|
|
539
|
+
var toWordOperatorPattern = (token) => token.split(/\s+/).map(escapeRegexToken).join(String.raw`\s+`);
|
|
540
|
+
var CEQL_SYMBOL_OPERATOR_ALTERNATION = [...CEQL_SYMBOL_OPERATORS].sort(byLengthDescending).map(escapeRegexToken).join("|");
|
|
541
|
+
var CEQL_WORD_OPERATOR_ALTERNATION = [...CEQL_WORD_OPERATORS].sort(byLengthDescending).map(toWordOperatorPattern).join("|");
|
|
542
|
+
var CEQL_OPERATOR = String.raw`(?:${CEQL_SYMBOL_OPERATOR_ALTERNATION}|(?:${CEQL_WORD_OPERATOR_ALTERNATION})\b)`;
|
|
543
|
+
var CEQL_OPERATOR_ALIAS_ALTERNATION = Object.keys(CEQL_OPERATOR_ALIASES).map(escapeRegexToken).join("|");
|
|
544
|
+
var CEQL_FIELD_OPERATOR = String.raw`(?:${CEQL_OPERATOR}|(?:${CEQL_OPERATOR_ALIAS_ALTERNATION})\b)`;
|
|
545
|
+
var CEQL_FILTER_PARAMETERS = new Set(["where", "queryExpression"]);
|
|
546
|
+
var CEQL_REFERENCE = 'See uipath-maestro-flow skill — plugins/connector/impl.md, "Hand-authored CEQL strings".';
|
|
547
|
+
var HOLE = "\x00";
|
|
548
|
+
var JS_PREFIX = "=js:";
|
|
549
|
+
function collectFragments(root, fragments) {
|
|
550
|
+
const pending = [root];
|
|
551
|
+
while (pending.length > 0) {
|
|
552
|
+
const item = pending.pop();
|
|
553
|
+
if (!("type" in item)) {
|
|
554
|
+
fragments.push(item);
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
switch (item.type) {
|
|
558
|
+
case "Literal":
|
|
559
|
+
fragments.push(typeof item.value === "string" ? { kind: "text", text: item.value } : { kind: "hole" });
|
|
560
|
+
continue;
|
|
561
|
+
case "BinaryExpression":
|
|
562
|
+
if (item.operator !== "+") {
|
|
563
|
+
fragments.push({ kind: "hole" });
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
pending.push(item.right, item.left);
|
|
567
|
+
continue;
|
|
568
|
+
case "TemplateLiteral":
|
|
569
|
+
for (let i = item.quasis.length - 1;i >= 0; i--) {
|
|
570
|
+
if (i < item.expressions.length)
|
|
571
|
+
pending.push({ kind: "hole" });
|
|
572
|
+
pending.push({
|
|
573
|
+
kind: "text",
|
|
574
|
+
text: item.quasis[i].value.cooked ?? ""
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
continue;
|
|
578
|
+
default:
|
|
579
|
+
fragments.push({ kind: "hole" });
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
function isStringConcatShape(node) {
|
|
584
|
+
if (node.type === "TemplateLiteral")
|
|
585
|
+
return true;
|
|
586
|
+
if (node.type === "Literal")
|
|
587
|
+
return typeof node.value === "string";
|
|
588
|
+
return node.type === "BinaryExpression" && node.operator === "+";
|
|
589
|
+
}
|
|
590
|
+
function lexCeqlFilterValue(value) {
|
|
591
|
+
const trimmed = value.trim();
|
|
592
|
+
if (!trimmed.startsWith(JS_PREFIX)) {
|
|
593
|
+
if (trimmed.startsWith("="))
|
|
594
|
+
return;
|
|
595
|
+
return [{ kind: "text", text: value }];
|
|
596
|
+
}
|
|
597
|
+
let program;
|
|
598
|
+
try {
|
|
599
|
+
program = parse(`(${trimmed.slice(JS_PREFIX.length)}
|
|
600
|
+
)`, {
|
|
601
|
+
ecmaVersion: 2020,
|
|
602
|
+
sourceType: "script"
|
|
603
|
+
});
|
|
604
|
+
} catch {
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
const statement = program.body[0];
|
|
608
|
+
if (statement?.type !== "ExpressionStatement")
|
|
609
|
+
return;
|
|
610
|
+
if (!isStringConcatShape(statement.expression))
|
|
611
|
+
return;
|
|
612
|
+
const fragments = [];
|
|
613
|
+
collectFragments(statement.expression, fragments);
|
|
614
|
+
return fragments;
|
|
615
|
+
}
|
|
616
|
+
function buildSkeleton(fragments) {
|
|
617
|
+
let text = "";
|
|
618
|
+
const holeAt = [];
|
|
619
|
+
for (const fragment of fragments) {
|
|
620
|
+
if (fragment.kind === "hole") {
|
|
621
|
+
text += HOLE;
|
|
622
|
+
holeAt.push(true);
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
625
|
+
text += fragment.text;
|
|
626
|
+
for (let i = 0;i < fragment.text.length; i++)
|
|
627
|
+
holeAt.push(false);
|
|
628
|
+
}
|
|
629
|
+
return { text, holeAt };
|
|
630
|
+
}
|
|
631
|
+
function singleQuoteOpenBefore(skeleton) {
|
|
632
|
+
const openBefore = new Array(skeleton.length);
|
|
633
|
+
let open = false;
|
|
634
|
+
for (let i = 0;i < skeleton.length; i++) {
|
|
635
|
+
openBefore[i] = open;
|
|
636
|
+
if (skeleton[i] === "'")
|
|
637
|
+
open = !open;
|
|
638
|
+
}
|
|
639
|
+
return openBefore;
|
|
640
|
+
}
|
|
641
|
+
function renderSkeletonSlice(skeleton, start, length) {
|
|
642
|
+
let rendered = "";
|
|
643
|
+
for (let i = start;i < start + length; i++) {
|
|
644
|
+
rendered += skeleton.holeAt[i] ? "<expression>" : skeleton.text[i];
|
|
645
|
+
}
|
|
646
|
+
return rendered;
|
|
647
|
+
}
|
|
648
|
+
var CEQL_QUOTED_FIELD_RE = new RegExp(String.raw`(?:^|\bAND\b|\bOR\b|\()\s*'([^']*)'\s*(?=${CEQL_FIELD_OPERATOR})`, "gi");
|
|
649
|
+
var CEQL_DOUBLE_QUOTED_VALUE_RE = new RegExp(String.raw`(${CEQL_OPERATOR})\s*"`, "gi");
|
|
650
|
+
var CEQL_OPERATOR_ALIAS_RE = new RegExp(String.raw`[A-Za-z0-9_.)\]']\s+(${CEQL_OPERATOR_ALIAS_ALTERNATION})(?!\s*(?:${CEQL_OPERATOR}))(?=\s)`, "gi");
|
|
651
|
+
function findQuotedFields(skeleton, openBefore) {
|
|
652
|
+
const matches = [];
|
|
653
|
+
for (const match of skeleton.matchAll(CEQL_QUOTED_FIELD_RE)) {
|
|
654
|
+
const openIndex = match.index + match[0].indexOf("'");
|
|
655
|
+
if (openBefore[openIndex])
|
|
656
|
+
continue;
|
|
657
|
+
matches.push({ name: match[1], openIndex });
|
|
658
|
+
}
|
|
659
|
+
return matches;
|
|
660
|
+
}
|
|
661
|
+
function unquoteFields(value, fields) {
|
|
662
|
+
const quoteIndexes = fields.flatMap((field) => [
|
|
663
|
+
field.openIndex,
|
|
664
|
+
field.openIndex + field.name.length + 1
|
|
665
|
+
]).sort((a, b) => b - a);
|
|
666
|
+
let fixed = value;
|
|
667
|
+
for (const index of quoteIndexes) {
|
|
668
|
+
fixed = fixed.slice(0, index) + fixed.slice(index + 1);
|
|
669
|
+
}
|
|
670
|
+
if (!fixed.trim().startsWith(JS_PREFIX) && fixed.includes("${")) {
|
|
671
|
+
const escaped = fixed.replace(/\\/g, "\\\\").replace(/`/g, "\\`");
|
|
672
|
+
return `=js:\`${escaped}\``;
|
|
673
|
+
}
|
|
674
|
+
return fixed;
|
|
675
|
+
}
|
|
676
|
+
function quotedFieldMessage(token, fixedValue) {
|
|
677
|
+
const fix = fixedValue ? `Write "${fixedValue}" instead.` : "Remove the quotes around the field name.";
|
|
678
|
+
return `Quoted token '${token}' where CEQL expects a bare field name — ` + "Integration Service rejects this with [102003] Expected a field name " + `expression but got 'StringValue'. ${fix} ${CEQL_REFERENCE}`;
|
|
679
|
+
}
|
|
680
|
+
function doubleQuotedValueMessage(operator) {
|
|
681
|
+
return `Double-quoted value after '${operator}' — CEQL quotes a value with ` + "single quotes. Integration Service rejects this with [102003] " + `"Unsupported value expression 'Column' on field ...". Write the ` + `value in single quotes. ${CEQL_REFERENCE}`;
|
|
682
|
+
}
|
|
683
|
+
function operatorAliasMessage(alias) {
|
|
684
|
+
const symbol = CEQL_OPERATOR_ALIASES[alias.toLowerCase()];
|
|
685
|
+
return `Operator '${alias}' is not CEQL — write '${symbol}'. Integration ` + "Service rejects this with [102003] Integration Services bad request. " + CEQL_REFERENCE;
|
|
686
|
+
}
|
|
687
|
+
function findCeqlFilterIssues(value) {
|
|
688
|
+
const fragments = lexCeqlFilterValue(value);
|
|
689
|
+
if (!fragments)
|
|
690
|
+
return [];
|
|
691
|
+
const skeleton = buildSkeleton(fragments);
|
|
692
|
+
const openBefore = singleQuoteOpenBefore(skeleton.text);
|
|
693
|
+
const issues = [];
|
|
694
|
+
const quotedFields = findQuotedFields(skeleton.text, openBefore);
|
|
695
|
+
if (quotedFields.length > 0) {
|
|
696
|
+
const fixedValue = value.trim().startsWith(JS_PREFIX) ? undefined : unquoteFields(value, quotedFields);
|
|
697
|
+
for (const field of quotedFields) {
|
|
698
|
+
const token = renderSkeletonSlice(skeleton, field.openIndex + 1, field.name.length);
|
|
699
|
+
issues.push({
|
|
700
|
+
rule: "quoted-field",
|
|
701
|
+
token,
|
|
702
|
+
fixedValue,
|
|
703
|
+
message: quotedFieldMessage(token, fixedValue)
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
for (const match of skeleton.text.matchAll(CEQL_DOUBLE_QUOTED_VALUE_RE)) {
|
|
708
|
+
const quoteIndex = match.index + match[0].length - 1;
|
|
709
|
+
if (openBefore[quoteIndex])
|
|
710
|
+
continue;
|
|
711
|
+
const operator = match[1].replace(/\s+/g, " ");
|
|
712
|
+
issues.push({
|
|
713
|
+
rule: "double-quoted-value",
|
|
714
|
+
token: operator,
|
|
715
|
+
message: doubleQuotedValueMessage(operator)
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
for (const match of skeleton.text.matchAll(CEQL_OPERATOR_ALIAS_RE)) {
|
|
719
|
+
const aliasIndex = match.index + match[0].length - match[1].length;
|
|
720
|
+
if (openBefore[aliasIndex])
|
|
721
|
+
continue;
|
|
722
|
+
issues.push({
|
|
723
|
+
rule: "operator-alias",
|
|
724
|
+
token: match[1],
|
|
725
|
+
message: operatorAliasMessage(match[1])
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
return issues;
|
|
729
|
+
}
|
|
730
|
+
function scanContainerForCeqlFilterIssues(obj, path, severity, issues) {
|
|
731
|
+
if (typeof obj === "string") {
|
|
732
|
+
for (const issue of findCeqlFilterIssues(obj)) {
|
|
733
|
+
issues.push({ path, severity, message: issue.message });
|
|
734
|
+
}
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
if (Array.isArray(obj)) {
|
|
738
|
+
for (let i = 0;i < obj.length; i++) {
|
|
739
|
+
scanContainerForCeqlFilterIssues(obj[i], `${path}[${i}]`, severity, issues);
|
|
740
|
+
}
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
if (obj && typeof obj === "object") {
|
|
744
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
745
|
+
scanContainerForCeqlFilterIssues(value, `${path}.${key}`, severity, issues);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
var ceqlQueryValidator = {
|
|
750
|
+
id: "ceql-query",
|
|
751
|
+
appliesTo: (node) => {
|
|
752
|
+
const t = node.type;
|
|
753
|
+
if (!t)
|
|
754
|
+
return false;
|
|
755
|
+
if (isEventDrivenConnectorType(t))
|
|
756
|
+
return false;
|
|
757
|
+
return t.startsWith("uipath.connector.");
|
|
758
|
+
},
|
|
759
|
+
validate: (node, context) => {
|
|
760
|
+
const rawDetail = node.inputs?.detail;
|
|
761
|
+
if (!rawDetail || typeof rawDetail !== "object" || Array.isArray(rawDetail)) {
|
|
762
|
+
return [];
|
|
763
|
+
}
|
|
764
|
+
const queryParameters = rawDetail.queryParameters;
|
|
765
|
+
if (!queryParameters || typeof queryParameters !== "object" || Array.isArray(queryParameters)) {
|
|
766
|
+
return [];
|
|
767
|
+
}
|
|
768
|
+
const severity = context.strictBindings ? "error" : "warning";
|
|
769
|
+
const issues = [];
|
|
770
|
+
for (const [key, value] of Object.entries(queryParameters)) {
|
|
771
|
+
if (!CEQL_FILTER_PARAMETERS.has(key))
|
|
772
|
+
continue;
|
|
773
|
+
scanContainerForCeqlFilterIssues(value, `nodes[${node.id}].inputs.detail.queryParameters.${key}`, severity, issues);
|
|
774
|
+
}
|
|
775
|
+
return issues;
|
|
776
|
+
}
|
|
777
|
+
};
|
|
778
|
+
|
|
779
|
+
// ../flow-tool/src/services/node-validators/expression-prefix-validator.ts
|
|
780
|
+
var expressionPrefixValidator = {
|
|
781
|
+
id: "expression-prefix",
|
|
782
|
+
appliesTo: (node) => {
|
|
783
|
+
const t = node.type;
|
|
784
|
+
if (!t)
|
|
785
|
+
return false;
|
|
786
|
+
if (isEventDrivenConnectorType(t))
|
|
787
|
+
return false;
|
|
788
|
+
return t.startsWith("uipath.connector.") || t === "core.action.http" || t === "core.action.http.v2" || t === "core.action.script";
|
|
789
|
+
},
|
|
790
|
+
validate: (node) => {
|
|
791
|
+
const issues = [];
|
|
792
|
+
if (node.type === "core.action.script") {
|
|
793
|
+
scanScriptSource(node.id, node.inputs, issues);
|
|
794
|
+
return issues;
|
|
795
|
+
}
|
|
796
|
+
const rawDetail = node.inputs?.detail;
|
|
797
|
+
if (!rawDetail || typeof rawDetail !== "object" || Array.isArray(rawDetail)) {
|
|
798
|
+
return [];
|
|
799
|
+
}
|
|
800
|
+
const detail = rawDetail;
|
|
801
|
+
for (const containerKey of [
|
|
802
|
+
"bodyParameters",
|
|
803
|
+
"queryParameters",
|
|
804
|
+
"pathParameters"
|
|
805
|
+
]) {
|
|
806
|
+
const container = detail[containerKey];
|
|
807
|
+
if (!container || typeof container !== "object" || Array.isArray(container)) {
|
|
808
|
+
continue;
|
|
809
|
+
}
|
|
810
|
+
scanForUnprefixedExpressions(container, `nodes[${node.id}].inputs.detail.${containerKey}`, issues);
|
|
811
|
+
}
|
|
812
|
+
return issues;
|
|
813
|
+
}
|
|
814
|
+
};
|
|
815
|
+
function scanScriptSource(nodeId, inputs, issues) {
|
|
816
|
+
if (!inputs)
|
|
817
|
+
return;
|
|
818
|
+
const source = inputs.source;
|
|
819
|
+
if (typeof source !== "string")
|
|
820
|
+
return;
|
|
821
|
+
const trimmed = source.trimStart();
|
|
822
|
+
if (!trimmed.startsWith("=js:"))
|
|
823
|
+
return;
|
|
824
|
+
issues.push({
|
|
825
|
+
path: `nodes[${nodeId}].inputs.source`,
|
|
826
|
+
severity: "error",
|
|
827
|
+
message: 'Script node `inputs.source` begins with `"=js:"` — that is the ' + "property-level expression-mode marker for connector / HTTP / " + "End-output / subflow-input fields, not JavaScript syntax. The " + "Script node's `source` field is the function body; runtime treats " + 'this value as literal source code, so e.g. `"=js:vars.X.Y"` ' + "compiles into a string-reversed gibberish output. Replace with a " + 'JavaScript expression that returns the value, e.g. `"return ' + '$vars.X.Y"` or a multi-statement function body. See ' + "uipath-maestro-flow skill — script-node reference (MST-9265)."
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
var BARE_DOLLAR_REF_RE = /^\$(vars|metadata|self)\b/;
|
|
831
|
+
var INVENTED_NODES_REF_RE = /^nodes\.[A-Za-z_$][\w$]*\.output\b/;
|
|
832
|
+
function scanForUnprefixedExpressions(obj, path, issues) {
|
|
833
|
+
if (typeof obj === "string") {
|
|
834
|
+
const trimmed = obj.trimStart();
|
|
835
|
+
if (trimmed.startsWith("="))
|
|
836
|
+
return;
|
|
837
|
+
if (BARE_DOLLAR_REF_RE.test(trimmed)) {
|
|
838
|
+
issues.push({
|
|
839
|
+
path,
|
|
840
|
+
severity: "error",
|
|
841
|
+
message: `Value "${obj}" references a runtime variable but is missing the "=js:" prefix. Use "=js:${obj}" so the runtime evaluates it; without the prefix, the value ships as a literal string and the activity input is bound to "${obj.replace(/^\$vars\b/, "vars").replace(/^\$metadata\b/, "metadata").replace(/^\$self\b/, "self")}". See uipath-maestro-flow skill — node-output-wiring.md (MST-9107).`
|
|
842
|
+
});
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
if (INVENTED_NODES_REF_RE.test(trimmed)) {
|
|
846
|
+
const fixed = trimmed.replace(/^nodes\./, "=js:$vars.");
|
|
847
|
+
issues.push({
|
|
848
|
+
path,
|
|
849
|
+
severity: "error",
|
|
850
|
+
message: `Value "${obj}" looks like an invented "nodes.<id>.output" reference. There is no such syntax — variable references use "$vars". Write "${fixed}" instead. See uipath-maestro-flow skill — node-output-wiring.md (MST-9107).`
|
|
851
|
+
});
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
if (Array.isArray(obj)) {
|
|
857
|
+
for (let i = 0;i < obj.length; i++) {
|
|
858
|
+
scanForUnprefixedExpressions(obj[i], `${path}[${i}]`, issues);
|
|
859
|
+
}
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
if (obj && typeof obj === "object") {
|
|
863
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
864
|
+
scanForUnprefixedExpressions(value, `${path}.${key}`, issues);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// ../flow-tool/src/services/node-validators/ixp-node-validator.ts
|
|
870
|
+
var FORBIDDEN_INPUT_FIELDS = [
|
|
871
|
+
"digitizationMode",
|
|
872
|
+
"documentTaxonomy",
|
|
873
|
+
"attachmentId",
|
|
874
|
+
"fileName",
|
|
875
|
+
"mimeType"
|
|
876
|
+
];
|
|
877
|
+
var FILE_REF_PATTERN = /^=js:\$vars\.[A-Za-z_$]/;
|
|
878
|
+
var ixpNodeValidator = {
|
|
879
|
+
id: "ixp-node",
|
|
880
|
+
appliesTo: (node) => typeof node.type === "string" && node.type.startsWith("uipath.ixp."),
|
|
881
|
+
validate: (node, _context) => {
|
|
882
|
+
const issues = [];
|
|
883
|
+
const inputs = node.inputs ?? {};
|
|
884
|
+
const outputs = node.outputs ?? {};
|
|
885
|
+
const base = `nodes[${node.id}]`;
|
|
886
|
+
const model = inputs.model;
|
|
887
|
+
const isNonEmptyString = (v) => typeof v === "string" && v.length > 0;
|
|
888
|
+
if (typeof model !== "object" || model === null || Array.isArray(model) || !isNonEmptyString(model.modelName) || !isNonEmptyString(model.folderKey)) {
|
|
889
|
+
issues.push({
|
|
890
|
+
path: `${base}.inputs.model`,
|
|
891
|
+
severity: "error",
|
|
892
|
+
message: "inputs.model must be an object with non-empty string " + "`modelName` and `folderKey`. Studio Web's property " + "panel destructures these and crashes when either is " + "missing or not a string."
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
const fileRef = inputs.fileRef;
|
|
896
|
+
if (typeof fileRef !== "string" || !FILE_REF_PATTERN.test(fileRef)) {
|
|
897
|
+
issues.push({
|
|
898
|
+
path: `${base}.inputs.fileRef`,
|
|
899
|
+
severity: "error",
|
|
900
|
+
message: "inputs.fileRef must be a `=js:$vars.…` expression. " + "The canonical form for an upstream node output is " + "`=js:$vars.<upstream>.output.<field>`. Without the " + "`=js:` prefix the BPMN serializer treats the value " + "as a literal string at runtime."
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
for (const port of ["output", "error"]) {
|
|
904
|
+
if (!(port in outputs)) {
|
|
905
|
+
issues.push({
|
|
906
|
+
path: `${base}.outputs.${port}`,
|
|
907
|
+
severity: "error",
|
|
908
|
+
message: `outputs.${port} must be present on the instance. ` + `Without it, downstream $vars.${node.id}.${port} ` + "references will not resolve at runtime."
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
const topLevelModel = node.model;
|
|
913
|
+
if (topLevelModel != null) {
|
|
914
|
+
issues.push({
|
|
915
|
+
path: `${base}.model`,
|
|
916
|
+
severity: "error",
|
|
917
|
+
message: "Top-level `model` block is forbidden on IxP node " + "instances. The manifest's slim model " + "({ type, serviceType }) lives only in `definitions[]`."
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
const legacy = FORBIDDEN_INPUT_FIELDS.filter((k) => (k in inputs));
|
|
921
|
+
if (legacy.length > 0) {
|
|
922
|
+
issues.push({
|
|
923
|
+
path: `${base}.inputs`,
|
|
924
|
+
severity: "error",
|
|
925
|
+
message: "Forbidden legacy fields in inputs: " + `${legacy.join(", ")}. These keys were removed from ` + "the current standalone IxP schema and are not " + "returned by `registry get`."
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
return issues;
|
|
929
|
+
}
|
|
930
|
+
};
|
|
931
|
+
|
|
932
|
+
// ../flow-tool/src/services/node-validators/js-expression-syntax-validator.ts
|
|
933
|
+
var jsExpressionSyntaxValidator = {
|
|
934
|
+
id: "js-expression-syntax",
|
|
935
|
+
appliesTo: (node) => {
|
|
936
|
+
const t = node.type;
|
|
937
|
+
if (!t)
|
|
938
|
+
return false;
|
|
939
|
+
if (isEventDrivenConnectorType(t))
|
|
940
|
+
return false;
|
|
941
|
+
return t.startsWith("uipath.connector.") || t === "core.action.http" || t === "core.action.http.v2" || t === "core.action.script";
|
|
942
|
+
},
|
|
943
|
+
validate: (node, context) => {
|
|
944
|
+
const issues = [];
|
|
945
|
+
const severity = context.strictBindings ? "error" : "warning";
|
|
946
|
+
if (node.type === "core.action.script") {
|
|
947
|
+
scanScriptBody(node.id, node.inputs, severity, issues);
|
|
948
|
+
return issues;
|
|
949
|
+
}
|
|
950
|
+
const rawDetail = node.inputs?.detail;
|
|
951
|
+
if (!rawDetail || typeof rawDetail !== "object" || Array.isArray(rawDetail)) {
|
|
952
|
+
return [];
|
|
953
|
+
}
|
|
954
|
+
const detail = rawDetail;
|
|
955
|
+
for (const containerKey of [
|
|
956
|
+
"bodyParameters",
|
|
957
|
+
"queryParameters",
|
|
958
|
+
"pathParameters",
|
|
959
|
+
"filterVariables"
|
|
960
|
+
]) {
|
|
961
|
+
const container = detail[containerKey];
|
|
962
|
+
if (!container || typeof container !== "object" || Array.isArray(container)) {
|
|
963
|
+
continue;
|
|
964
|
+
}
|
|
965
|
+
scanForInvalidJsExpressions(container, `nodes[${node.id}].inputs.detail.${containerKey}`, severity, issues);
|
|
966
|
+
}
|
|
967
|
+
return issues;
|
|
968
|
+
}
|
|
969
|
+
};
|
|
970
|
+
function scanScriptBody(nodeId, inputs, severity, issues) {
|
|
971
|
+
if (!inputs)
|
|
972
|
+
return;
|
|
973
|
+
const rawScript = inputs.script;
|
|
974
|
+
const script = typeof rawScript === "string" ? rawScript : isExpressionValue(rawScript) && rawScript.type === "literal" ? decodeLiteral(rawScript) : undefined;
|
|
975
|
+
if (typeof script !== "string")
|
|
976
|
+
return;
|
|
977
|
+
if (script.trim() === "")
|
|
978
|
+
return;
|
|
979
|
+
const error = syntaxErrorFrom(() => parse(script, {
|
|
980
|
+
ecmaVersion: 2020,
|
|
981
|
+
sourceType: "script",
|
|
982
|
+
allowReturnOutsideFunction: true
|
|
983
|
+
}));
|
|
984
|
+
if (!error)
|
|
985
|
+
return;
|
|
986
|
+
issues.push({
|
|
987
|
+
path: `nodes[${nodeId}].inputs.script`,
|
|
988
|
+
severity,
|
|
989
|
+
message: `Script body has a JavaScript syntax error: ${error.message}.`
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
function scanForInvalidJsExpressions(obj, path, severity, issues) {
|
|
993
|
+
const expression = getJsExpressionBody(typeof obj === "string" ? obj.trimStart() : obj);
|
|
994
|
+
if (expression !== undefined) {
|
|
995
|
+
reportInvalidExpression(expression, path, severity, issues);
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
if (isExpressionValue(obj))
|
|
999
|
+
return;
|
|
1000
|
+
if (typeof obj === "string")
|
|
1001
|
+
return;
|
|
1002
|
+
if (Array.isArray(obj)) {
|
|
1003
|
+
for (let i = 0;i < obj.length; i++) {
|
|
1004
|
+
scanForInvalidJsExpressions(obj[i], `${path}[${i}]`, severity, issues);
|
|
1005
|
+
}
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
if (obj && typeof obj === "object") {
|
|
1009
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
1010
|
+
scanForInvalidJsExpressions(value, `${path}.${key}`, severity, issues);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
function reportInvalidExpression(expression, path, severity, issues) {
|
|
1015
|
+
if (expression.trim() === "")
|
|
1016
|
+
return;
|
|
1017
|
+
const error = syntaxErrorFrom(() => parse(`(${expression}
|
|
1018
|
+
)`, {
|
|
1019
|
+
ecmaVersion: 2020,
|
|
1020
|
+
sourceType: "script"
|
|
1021
|
+
}));
|
|
1022
|
+
if (!error)
|
|
1023
|
+
return;
|
|
1024
|
+
issues.push({
|
|
1025
|
+
path,
|
|
1026
|
+
severity,
|
|
1027
|
+
message: `Expression at this field is not valid JavaScript: ${error.message}. Fix the syntax — validate cannot otherwise catch JavaScript expression errors; they surface only at flow debug.`
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
function syntaxErrorFrom(parseSource) {
|
|
1031
|
+
try {
|
|
1032
|
+
parseSource();
|
|
1033
|
+
return;
|
|
1034
|
+
} catch (err) {
|
|
1035
|
+
return err instanceof Error ? err : new Error(String(err));
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// ../flow-tool/src/services/node-validators/model-source-validator.ts
|
|
1040
|
+
var modelSourceValidator = {
|
|
1041
|
+
id: "model-source",
|
|
1042
|
+
appliesTo: () => true,
|
|
1043
|
+
validate: (node, context) => {
|
|
1044
|
+
const definition = context.definitions.find((d) => d.nodeType === node.type);
|
|
1045
|
+
if (!definition)
|
|
1046
|
+
return [];
|
|
1047
|
+
const requiresModelSource = readModelSourceFlag(definition);
|
|
1048
|
+
if (!requiresModelSource)
|
|
1049
|
+
return [];
|
|
1050
|
+
const legacySource = readLegacyModelSource(node);
|
|
1051
|
+
if (legacySource !== undefined) {
|
|
1052
|
+
return [
|
|
1053
|
+
{
|
|
1054
|
+
path: `nodes[${node.id}].model.source`,
|
|
1055
|
+
severity: "error",
|
|
1056
|
+
message: `Node type "${node.type}" carries the deprecated ` + `"model.source" UUID on the node instance. ` + `flow-core 0.2.50+ requires the canvas-generated ` + `UUID at "inputs.source"; remove "model.source" ` + `from the node and place the value under ` + `"inputs": { "source": "<uuid>" } instead. ` + `See uipath-maestro-flow skill — ` + `inline-agent/impl.md (MST-9265).`
|
|
1057
|
+
}
|
|
1058
|
+
];
|
|
1059
|
+
}
|
|
1060
|
+
const inputsSource = node.inputs?.source;
|
|
1061
|
+
if (typeof inputsSource === "string" && inputsSource.length > 0) {
|
|
1062
|
+
return [];
|
|
1063
|
+
}
|
|
1064
|
+
return [
|
|
1065
|
+
{
|
|
1066
|
+
path: `nodes[${node.id}].inputs.source`,
|
|
1067
|
+
severity: "error",
|
|
1068
|
+
message: `Node type "${node.type}" requires a stable UUID at ` + `"inputs.source" — the agent's ProjectId for inline ` + `agent nodes, or the resource id from the parent inline ` + `agent's "resources[]" for agent-tool resource nodes. ` + `Set "inputs": { "source": "<uuid>" } on the node ` + `(use the ProjectId returned by ` + `"uip agent init --inline-in-flow" for inline agents). ` + `See uipath-maestro-flow skill — inline-agent/impl.md ` + `(MST-9265).`
|
|
1069
|
+
}
|
|
1070
|
+
];
|
|
1071
|
+
}
|
|
1072
|
+
};
|
|
1073
|
+
function readModelSourceFlag(definition) {
|
|
1074
|
+
const model = definition.model;
|
|
1075
|
+
if (!model || typeof model !== "object" || Array.isArray(model)) {
|
|
1076
|
+
return false;
|
|
1077
|
+
}
|
|
1078
|
+
const source = model.source;
|
|
1079
|
+
return source === true;
|
|
1080
|
+
}
|
|
1081
|
+
function readLegacyModelSource(node) {
|
|
1082
|
+
const model = node.model;
|
|
1083
|
+
if (!model || typeof model !== "object" || Array.isArray(model)) {
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
const source = model.source;
|
|
1087
|
+
if (typeof source === "string" && source.length > 0) {
|
|
1088
|
+
return source;
|
|
1089
|
+
}
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// ../flow-tool/src/services/node-validators/index.ts
|
|
1094
|
+
var defaultNodeValidators = [
|
|
1095
|
+
activityTypeIdValidator,
|
|
1096
|
+
ceqlQueryValidator,
|
|
1097
|
+
connectorNodeValidator,
|
|
1098
|
+
crossNodeBindingValidator,
|
|
1099
|
+
expressionPrefixValidator,
|
|
1100
|
+
jsExpressionSyntaxValidator,
|
|
1101
|
+
ixpNodeValidator,
|
|
1102
|
+
modelSourceValidator
|
|
1103
|
+
];
|
|
1104
|
+
|
|
1105
|
+
// ../flow-tool/src/services/vars-reference-rule.ts
|
|
1106
|
+
var VARS_REF_RE = /(?<![\w$])\$vars((?:\s*\??\.\s*[A-Za-z_$][\w$]*|\s*(?:\?\.\s*)?\[\s*(?:'[^']*'|"[^"]*")\s*\])+)/g;
|
|
1107
|
+
var VARS_STEP_RE = /(\??\.)\s*([A-Za-z_$][\w$]*)|(?:(\?\.)\s*)?\[\s*(?:'([^']*)'|"([^"]*)")\s*\]/g;
|
|
1108
|
+
var JS_EXPRESSION_PREFIX = "=js:";
|
|
1109
|
+
var ARRAY_MEMBERS = new Set([
|
|
1110
|
+
"length",
|
|
1111
|
+
"at",
|
|
1112
|
+
"concat",
|
|
1113
|
+
"copyWithin",
|
|
1114
|
+
"entries",
|
|
1115
|
+
"every",
|
|
1116
|
+
"fill",
|
|
1117
|
+
"filter",
|
|
1118
|
+
"find",
|
|
1119
|
+
"findIndex",
|
|
1120
|
+
"findLast",
|
|
1121
|
+
"findLastIndex",
|
|
1122
|
+
"flat",
|
|
1123
|
+
"flatMap",
|
|
1124
|
+
"forEach",
|
|
1125
|
+
"includes",
|
|
1126
|
+
"indexOf",
|
|
1127
|
+
"join",
|
|
1128
|
+
"keys",
|
|
1129
|
+
"lastIndexOf",
|
|
1130
|
+
"map",
|
|
1131
|
+
"pop",
|
|
1132
|
+
"push",
|
|
1133
|
+
"reduce",
|
|
1134
|
+
"reduceRight",
|
|
1135
|
+
"reverse",
|
|
1136
|
+
"shift",
|
|
1137
|
+
"slice",
|
|
1138
|
+
"some",
|
|
1139
|
+
"sort",
|
|
1140
|
+
"splice",
|
|
1141
|
+
"toLocaleString",
|
|
1142
|
+
"toReversed",
|
|
1143
|
+
"toSorted",
|
|
1144
|
+
"toSpliced",
|
|
1145
|
+
"toString",
|
|
1146
|
+
"unshift",
|
|
1147
|
+
"values",
|
|
1148
|
+
"with"
|
|
1149
|
+
]);
|
|
1150
|
+
var LOOP_NODE_TYPE = "core.logic.loop";
|
|
1151
|
+
var LOOP_BACK_TARGET_PORT = "loopBack";
|
|
1152
|
+
var SCRIPT_NODE_TYPE = "core.action.script";
|
|
1153
|
+
var JS_EXPRESSION_TYPE = "jsExpression";
|
|
1154
|
+
var DECISION_UNPOPULATED_OUTPUTS = new Set(SYNTHETIC_GATEWAY_OUTPUT_KEYS);
|
|
1155
|
+
function undefinedSafe(ref, k) {
|
|
1156
|
+
return ref.guarded.has(k + 1);
|
|
1157
|
+
}
|
|
1158
|
+
function inRanges(ranges, index) {
|
|
1159
|
+
for (const [start, end] of ranges) {
|
|
1160
|
+
if (index < start)
|
|
1161
|
+
return false;
|
|
1162
|
+
if (index < end)
|
|
1163
|
+
return true;
|
|
1164
|
+
}
|
|
1165
|
+
return false;
|
|
1166
|
+
}
|
|
1167
|
+
var CONTROL_FLOW_HEADS = new Set(["for", "if", "while", "with"]);
|
|
1168
|
+
var REGEX_PRECEDING_KEYWORDS = new Set([
|
|
1169
|
+
"await",
|
|
1170
|
+
"case",
|
|
1171
|
+
"delete",
|
|
1172
|
+
"do",
|
|
1173
|
+
"else",
|
|
1174
|
+
"in",
|
|
1175
|
+
"instanceof",
|
|
1176
|
+
"new",
|
|
1177
|
+
"of",
|
|
1178
|
+
"return",
|
|
1179
|
+
"throw",
|
|
1180
|
+
"typeof",
|
|
1181
|
+
"void",
|
|
1182
|
+
"yield"
|
|
1183
|
+
]);
|
|
1184
|
+
function scanJsText(source) {
|
|
1185
|
+
const n = source.length;
|
|
1186
|
+
let out = "";
|
|
1187
|
+
let i = 0;
|
|
1188
|
+
const proseRanges = [];
|
|
1189
|
+
const pushProse = (start) => {
|
|
1190
|
+
if (out.length > start)
|
|
1191
|
+
proseRanges.push([start, out.length]);
|
|
1192
|
+
};
|
|
1193
|
+
const copyQuoted = (quote) => {
|
|
1194
|
+
const start = out.length;
|
|
1195
|
+
let j = i + 1;
|
|
1196
|
+
while (j < n && source[j] !== quote) {
|
|
1197
|
+
if (source[j] === "\\")
|
|
1198
|
+
j++;
|
|
1199
|
+
else if (source[j] === `
|
|
1200
|
+
`)
|
|
1201
|
+
break;
|
|
1202
|
+
j++;
|
|
1203
|
+
}
|
|
1204
|
+
out += source.slice(i, Math.min(j + 1, n));
|
|
1205
|
+
pushProse(start);
|
|
1206
|
+
i = j + 1;
|
|
1207
|
+
};
|
|
1208
|
+
const proseStartAt = (k) => {
|
|
1209
|
+
for (let r = proseRanges.length - 1;r >= 0; r--) {
|
|
1210
|
+
const [start, end] = proseRanges[r];
|
|
1211
|
+
if (k >= end)
|
|
1212
|
+
return;
|
|
1213
|
+
if (k >= start)
|
|
1214
|
+
return start;
|
|
1215
|
+
}
|
|
1216
|
+
return;
|
|
1217
|
+
};
|
|
1218
|
+
const prevSignificant = (from) => {
|
|
1219
|
+
let k = from;
|
|
1220
|
+
while (k >= 0) {
|
|
1221
|
+
const proseStart = proseStartAt(k);
|
|
1222
|
+
if (proseStart !== undefined) {
|
|
1223
|
+
k = proseStart - 1;
|
|
1224
|
+
continue;
|
|
1225
|
+
}
|
|
1226
|
+
if (!/\s/.test(out[k]))
|
|
1227
|
+
return k;
|
|
1228
|
+
k--;
|
|
1229
|
+
}
|
|
1230
|
+
return -1;
|
|
1231
|
+
};
|
|
1232
|
+
const standaloneKeywordEndingAt = (end) => {
|
|
1233
|
+
if (end < 0 || !/[\w$]/.test(out[end]))
|
|
1234
|
+
return;
|
|
1235
|
+
let s = end;
|
|
1236
|
+
while (s >= 0 && /[\w$]/.test(out[s]))
|
|
1237
|
+
s--;
|
|
1238
|
+
let p = s;
|
|
1239
|
+
while (p >= 0 && /\s/.test(out[p]))
|
|
1240
|
+
p--;
|
|
1241
|
+
if (p >= 0 && out[p] === ".")
|
|
1242
|
+
return;
|
|
1243
|
+
return out.slice(s + 1, end + 1);
|
|
1244
|
+
};
|
|
1245
|
+
const closesControlFlowHead = (closeIdx) => {
|
|
1246
|
+
let depth = 0;
|
|
1247
|
+
let k = closeIdx;
|
|
1248
|
+
while (k >= 0) {
|
|
1249
|
+
const proseStart = proseStartAt(k);
|
|
1250
|
+
if (proseStart !== undefined) {
|
|
1251
|
+
k = proseStart - 1;
|
|
1252
|
+
continue;
|
|
1253
|
+
}
|
|
1254
|
+
const ch = out[k];
|
|
1255
|
+
if (ch === ")")
|
|
1256
|
+
depth++;
|
|
1257
|
+
else if (ch === "(") {
|
|
1258
|
+
depth--;
|
|
1259
|
+
if (depth === 0)
|
|
1260
|
+
break;
|
|
1261
|
+
}
|
|
1262
|
+
k--;
|
|
1263
|
+
}
|
|
1264
|
+
if (k < 0)
|
|
1265
|
+
return false;
|
|
1266
|
+
const word = standaloneKeywordEndingAt(prevSignificant(k - 1));
|
|
1267
|
+
return word !== undefined && CONTROL_FLOW_HEADS.has(word);
|
|
1268
|
+
};
|
|
1269
|
+
const regexStartsHere = () => {
|
|
1270
|
+
let k = out.length - 1;
|
|
1271
|
+
while (k >= 0 && /\s/.test(out[k]))
|
|
1272
|
+
k--;
|
|
1273
|
+
if (k < 0)
|
|
1274
|
+
return true;
|
|
1275
|
+
if (proseStartAt(k) !== undefined)
|
|
1276
|
+
return false;
|
|
1277
|
+
const prev = out[k];
|
|
1278
|
+
if (prev === ")")
|
|
1279
|
+
return closesControlFlowHead(k);
|
|
1280
|
+
if (prev === "]")
|
|
1281
|
+
return false;
|
|
1282
|
+
if (/[\w$]/.test(prev)) {
|
|
1283
|
+
const word = standaloneKeywordEndingAt(k);
|
|
1284
|
+
return word !== undefined && REGEX_PRECEDING_KEYWORDS.has(word);
|
|
1285
|
+
}
|
|
1286
|
+
return true;
|
|
1287
|
+
};
|
|
1288
|
+
const copyRegex = () => {
|
|
1289
|
+
const start = out.length;
|
|
1290
|
+
let j = i + 1;
|
|
1291
|
+
let inClass = false;
|
|
1292
|
+
while (j < n) {
|
|
1293
|
+
const ch = source[j];
|
|
1294
|
+
if (ch === "\\") {
|
|
1295
|
+
j += 2;
|
|
1296
|
+
continue;
|
|
1297
|
+
}
|
|
1298
|
+
if (ch === `
|
|
1299
|
+
`)
|
|
1300
|
+
break;
|
|
1301
|
+
if (ch === "[")
|
|
1302
|
+
inClass = true;
|
|
1303
|
+
else if (ch === "]")
|
|
1304
|
+
inClass = false;
|
|
1305
|
+
else if (ch === "/" && !inClass)
|
|
1306
|
+
break;
|
|
1307
|
+
j++;
|
|
1308
|
+
}
|
|
1309
|
+
let end = Math.min(j + 1, n);
|
|
1310
|
+
while (end < n && /[a-z]/i.test(source[end]))
|
|
1311
|
+
end++;
|
|
1312
|
+
out += source.slice(i, end);
|
|
1313
|
+
pushProse(start);
|
|
1314
|
+
i = end;
|
|
1315
|
+
};
|
|
1316
|
+
const copyCode = (opts) => {
|
|
1317
|
+
let depth = 0;
|
|
1318
|
+
while (i < n) {
|
|
1319
|
+
const c = source[i];
|
|
1320
|
+
const next = source[i + 1];
|
|
1321
|
+
if (c === "/" && next === "/") {
|
|
1322
|
+
if (opts.stripComments) {
|
|
1323
|
+
while (i < n && source[i] !== `
|
|
1324
|
+
`)
|
|
1325
|
+
i++;
|
|
1326
|
+
continue;
|
|
1327
|
+
}
|
|
1328
|
+
const start = out.length;
|
|
1329
|
+
while (i < n && source[i] !== `
|
|
1330
|
+
` && source[i] !== "}") {
|
|
1331
|
+
out += source[i];
|
|
1332
|
+
i++;
|
|
1333
|
+
}
|
|
1334
|
+
pushProse(start);
|
|
1335
|
+
continue;
|
|
1336
|
+
}
|
|
1337
|
+
if (c === "/" && next === "*") {
|
|
1338
|
+
const end = source.indexOf("*/", i + 2);
|
|
1339
|
+
const stop = end === -1 ? n : end + 2;
|
|
1340
|
+
if (opts.stripComments) {
|
|
1341
|
+
out += " ";
|
|
1342
|
+
} else {
|
|
1343
|
+
const start = out.length;
|
|
1344
|
+
out += source.slice(i, stop);
|
|
1345
|
+
pushProse(start);
|
|
1346
|
+
}
|
|
1347
|
+
i = stop;
|
|
1348
|
+
continue;
|
|
1349
|
+
}
|
|
1350
|
+
if (c === "'" || c === '"') {
|
|
1351
|
+
copyQuoted(c);
|
|
1352
|
+
continue;
|
|
1353
|
+
}
|
|
1354
|
+
if (c === "`") {
|
|
1355
|
+
copyTemplate();
|
|
1356
|
+
continue;
|
|
1357
|
+
}
|
|
1358
|
+
if (c === "/" && regexStartsHere()) {
|
|
1359
|
+
copyRegex();
|
|
1360
|
+
continue;
|
|
1361
|
+
}
|
|
1362
|
+
if (opts.stopAtBrace) {
|
|
1363
|
+
if (c === "{")
|
|
1364
|
+
depth++;
|
|
1365
|
+
else if (c === "}") {
|
|
1366
|
+
if (depth === 0)
|
|
1367
|
+
return;
|
|
1368
|
+
depth--;
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
out += c;
|
|
1372
|
+
i++;
|
|
1373
|
+
}
|
|
1374
|
+
};
|
|
1375
|
+
const copyTemplate = () => {
|
|
1376
|
+
let start = out.length;
|
|
1377
|
+
out += source[i];
|
|
1378
|
+
i++;
|
|
1379
|
+
while (i < n) {
|
|
1380
|
+
const c = source[i];
|
|
1381
|
+
if (c === "\\") {
|
|
1382
|
+
out += source.slice(i, i + 2);
|
|
1383
|
+
i += 2;
|
|
1384
|
+
continue;
|
|
1385
|
+
}
|
|
1386
|
+
if (c === "`") {
|
|
1387
|
+
out += c;
|
|
1388
|
+
i++;
|
|
1389
|
+
pushProse(start);
|
|
1390
|
+
return;
|
|
1391
|
+
}
|
|
1392
|
+
if (c === "$" && source[i + 1] === "{") {
|
|
1393
|
+
pushProse(start);
|
|
1394
|
+
out += "${";
|
|
1395
|
+
i += 2;
|
|
1396
|
+
copyCode({ stopAtBrace: true, stripComments: false });
|
|
1397
|
+
if (i < n) {
|
|
1398
|
+
out += source[i];
|
|
1399
|
+
i++;
|
|
1400
|
+
}
|
|
1401
|
+
start = out.length;
|
|
1402
|
+
continue;
|
|
1403
|
+
}
|
|
1404
|
+
out += c;
|
|
1405
|
+
i++;
|
|
1406
|
+
}
|
|
1407
|
+
pushProse(start);
|
|
1408
|
+
};
|
|
1409
|
+
copyCode({ stopAtBrace: false, stripComments: true });
|
|
1410
|
+
return { text: out, proseRanges };
|
|
1411
|
+
}
|
|
1412
|
+
function isExpressionObject(v) {
|
|
1413
|
+
return typeof v === "object" && v !== null && !Array.isArray(v) && typeof v.expression === "string" && typeof v.type === "string";
|
|
1414
|
+
}
|
|
1415
|
+
function isWorkflowVarsRoot(text, index) {
|
|
1416
|
+
if (index === 0 || text[index - 1] !== ".")
|
|
1417
|
+
return true;
|
|
1418
|
+
return index >= 3 && text.slice(index - 3, index) === "...";
|
|
1419
|
+
}
|
|
1420
|
+
function parseRefs(text, proseRanges = []) {
|
|
1421
|
+
const refs = [];
|
|
1422
|
+
for (const m of text.matchAll(VARS_REF_RE)) {
|
|
1423
|
+
if (!isWorkflowVarsRoot(text, m.index))
|
|
1424
|
+
continue;
|
|
1425
|
+
const chain = m[1];
|
|
1426
|
+
const segments = [];
|
|
1427
|
+
const guarded = new Set;
|
|
1428
|
+
for (const step of chain.matchAll(VARS_STEP_RE)) {
|
|
1429
|
+
if (step[1] === "?." || step[3] === "?.") {
|
|
1430
|
+
guarded.add(segments.length);
|
|
1431
|
+
}
|
|
1432
|
+
const key = step[2] ?? step[4] ?? step[5] ?? "";
|
|
1433
|
+
for (const part of key.split(".")) {
|
|
1434
|
+
if (part)
|
|
1435
|
+
segments.push(part);
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
if (segments.length > 0) {
|
|
1439
|
+
refs.push({
|
|
1440
|
+
segments,
|
|
1441
|
+
text: `$vars${chain.replace(/\s+/g, "")}`,
|
|
1442
|
+
guarded,
|
|
1443
|
+
inProse: inRanges(proseRanges, m.index)
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
return refs;
|
|
1448
|
+
}
|
|
1449
|
+
function scanNodeFields(node, updates) {
|
|
1450
|
+
const out = [];
|
|
1451
|
+
const walk = (val, path, underAgentBinding) => {
|
|
1452
|
+
const isScriptBody = node.type === SCRIPT_NODE_TYPE && path === "inputs.script";
|
|
1453
|
+
if (typeof val === "string") {
|
|
1454
|
+
if (!val.includes("$vars"))
|
|
1455
|
+
return;
|
|
1456
|
+
out.push({
|
|
1457
|
+
fieldPath: path,
|
|
1458
|
+
isAgentBinding: underAgentBinding,
|
|
1459
|
+
isJs: isScriptBody || val.startsWith(JS_EXPRESSION_PREFIX),
|
|
1460
|
+
text: val
|
|
1461
|
+
});
|
|
1462
|
+
return;
|
|
1463
|
+
}
|
|
1464
|
+
if (isExpressionObject(val)) {
|
|
1465
|
+
if (!val.expression.includes("$vars"))
|
|
1466
|
+
return;
|
|
1467
|
+
out.push({
|
|
1468
|
+
fieldPath: `${path}.expression`,
|
|
1469
|
+
isAgentBinding: underAgentBinding,
|
|
1470
|
+
isJs: isScriptBody || val.type === JS_EXPRESSION_TYPE,
|
|
1471
|
+
text: val.expression
|
|
1472
|
+
});
|
|
1473
|
+
return;
|
|
1474
|
+
}
|
|
1475
|
+
if (Array.isArray(val)) {
|
|
1476
|
+
for (const [i, v] of val.entries()) {
|
|
1477
|
+
walk(v, `${path}[${i}]`, underAgentBinding);
|
|
1478
|
+
}
|
|
1479
|
+
return;
|
|
1480
|
+
}
|
|
1481
|
+
if (val && typeof val === "object") {
|
|
1482
|
+
for (const [k, v] of Object.entries(val)) {
|
|
1483
|
+
if (path === "inputs.detail" && k === "configuration")
|
|
1484
|
+
continue;
|
|
1485
|
+
walk(v, `${path}.${k}`, underAgentBinding || path === "inputs" && k === "agentInputVariables");
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
};
|
|
1489
|
+
if (node.inputs)
|
|
1490
|
+
walk(node.inputs, "inputs", false);
|
|
1491
|
+
if (node.outputs)
|
|
1492
|
+
walk(node.outputs, "outputs", false);
|
|
1493
|
+
if (Array.isArray(updates)) {
|
|
1494
|
+
for (const [i, u] of updates.entries()) {
|
|
1495
|
+
const expr = u?.expression;
|
|
1496
|
+
if (typeof expr === "string" || isExpressionObject(expr)) {
|
|
1497
|
+
walk(expr, `variableUpdates[${i}]`, false);
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
return out;
|
|
1502
|
+
}
|
|
1503
|
+
function collectVarsRefs(nodes, updatesByNode = {}) {
|
|
1504
|
+
const out = [];
|
|
1505
|
+
for (const node of nodes) {
|
|
1506
|
+
for (const field of scanNodeFields(node, updatesByNode[node.id])) {
|
|
1507
|
+
const scanned = field.isJs ? scanJsText(field.text) : { text: field.text, proseRanges: [] };
|
|
1508
|
+
const text = scanned.text;
|
|
1509
|
+
const bareValue = text.replace(/^=(js:)?/, "").replace(/\s+/g, "");
|
|
1510
|
+
for (const ref of parseRefs(text, scanned.proseRanges)) {
|
|
1511
|
+
out.push({
|
|
1512
|
+
nodeId: node.id,
|
|
1513
|
+
fieldPath: field.fieldPath,
|
|
1514
|
+
isAgentBinding: field.isAgentBinding,
|
|
1515
|
+
embedded: bareValue !== ref.text,
|
|
1516
|
+
ref
|
|
1517
|
+
});
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
return out;
|
|
1522
|
+
}
|
|
1523
|
+
function buildAncestors(flow) {
|
|
1524
|
+
const preds = new Map;
|
|
1525
|
+
for (const e of flow.edges) {
|
|
1526
|
+
const set = preds.get(e.targetNodeId) ?? new Set;
|
|
1527
|
+
set.add(e.sourceNodeId);
|
|
1528
|
+
preds.set(e.targetNodeId, set);
|
|
1529
|
+
}
|
|
1530
|
+
const result = new Map;
|
|
1531
|
+
for (const n of flow.nodes) {
|
|
1532
|
+
const seen = new Set;
|
|
1533
|
+
const stack = [...preds.get(n.id) ?? []];
|
|
1534
|
+
while (stack.length > 0) {
|
|
1535
|
+
const x = stack.pop();
|
|
1536
|
+
if (seen.has(x))
|
|
1537
|
+
continue;
|
|
1538
|
+
seen.add(x);
|
|
1539
|
+
for (const p of preds.get(x) ?? [])
|
|
1540
|
+
stack.push(p);
|
|
1541
|
+
}
|
|
1542
|
+
result.set(n.id, seen);
|
|
1543
|
+
}
|
|
1544
|
+
return result;
|
|
1545
|
+
}
|
|
1546
|
+
function buildMustRun(flow, modelTypeOf) {
|
|
1547
|
+
const nodesById = new Map(flow.nodes.map((n) => [n.id, n]));
|
|
1548
|
+
const incoming = new Map;
|
|
1549
|
+
for (const e of flow.edges) {
|
|
1550
|
+
if (e.targetPort === LOOP_BACK_TARGET_PORT && nodesById.get(e.targetNodeId)?.type === LOOP_NODE_TYPE) {
|
|
1551
|
+
continue;
|
|
1552
|
+
}
|
|
1553
|
+
const list = incoming.get(e.targetNodeId) ?? [];
|
|
1554
|
+
list.push(e.sourceNodeId);
|
|
1555
|
+
incoming.set(e.targetNodeId, list);
|
|
1556
|
+
}
|
|
1557
|
+
const memo = new Map;
|
|
1558
|
+
const visiting = new Set;
|
|
1559
|
+
const mustRun = (id) => {
|
|
1560
|
+
const hit = memo.get(id);
|
|
1561
|
+
if (hit)
|
|
1562
|
+
return hit;
|
|
1563
|
+
if (visiting.has(id))
|
|
1564
|
+
return new Set;
|
|
1565
|
+
visiting.add(id);
|
|
1566
|
+
const ins = incoming.get(id) ?? [];
|
|
1567
|
+
let acc = new Set;
|
|
1568
|
+
if (ins.length > 0) {
|
|
1569
|
+
const sets = ins.map((s) => new Set([...mustRun(s), s]));
|
|
1570
|
+
const node = nodesById.get(id);
|
|
1571
|
+
const isJoin = node !== undefined && (node.type === "core.logic.merge" || modelTypeOf(node) === "bpmn:ParallelGateway");
|
|
1572
|
+
if (isJoin) {
|
|
1573
|
+
for (const s of sets)
|
|
1574
|
+
for (const x of s)
|
|
1575
|
+
acc.add(x);
|
|
1576
|
+
} else {
|
|
1577
|
+
acc = new Set(sets[0]);
|
|
1578
|
+
for (const s of sets.slice(1)) {
|
|
1579
|
+
for (const x of [...acc])
|
|
1580
|
+
if (!s.has(x))
|
|
1581
|
+
acc.delete(x);
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
acc.add(id);
|
|
1586
|
+
visiting.delete(id);
|
|
1587
|
+
memo.set(id, acc);
|
|
1588
|
+
return acc;
|
|
1589
|
+
};
|
|
1590
|
+
return mustRun;
|
|
1591
|
+
}
|
|
1592
|
+
function declaredOutputs(node, definition) {
|
|
1593
|
+
const names = new Set;
|
|
1594
|
+
if (node.outputs && typeof node.outputs === "object") {
|
|
1595
|
+
for (const k of Object.keys(node.outputs))
|
|
1596
|
+
names.add(k);
|
|
1597
|
+
}
|
|
1598
|
+
const outDef = definition?.outputDefinition;
|
|
1599
|
+
if (outDef && typeof outDef === "object") {
|
|
1600
|
+
for (const k of Object.keys(outDef))
|
|
1601
|
+
names.add(k);
|
|
1602
|
+
}
|
|
1603
|
+
if (node.type === "core.subflow")
|
|
1604
|
+
names.add("output");
|
|
1605
|
+
const isListOutput = node.type.startsWith("uipath.connector.") && connectorOperation(node) === "list";
|
|
1606
|
+
const declaredType = (name) => {
|
|
1607
|
+
const entry = outDef?.[name];
|
|
1608
|
+
const t = entry?.type;
|
|
1609
|
+
return typeof t === "string" ? t : undefined;
|
|
1610
|
+
};
|
|
1611
|
+
const typeOf = (name) => name === "output" && isListOutput ? "array" : declaredType(name);
|
|
1612
|
+
const whyArray = (name) => declaredType(name) === "array" ? 'declared type "array"' : 'a list operation — inputMetadata.operation is "list", the response is the bare array of records';
|
|
1613
|
+
return { names, typeOf, whyArray };
|
|
1614
|
+
}
|
|
1615
|
+
function connectorOperation(node) {
|
|
1616
|
+
const detail = node.inputs?.detail;
|
|
1617
|
+
const op = detail?.inputMetadata?.operation;
|
|
1618
|
+
return typeof op === "string" ? op.toLowerCase() : undefined;
|
|
1619
|
+
}
|
|
1620
|
+
function boundedEditDistance(a, b, max) {
|
|
1621
|
+
if (Math.abs(a.length - b.length) > max)
|
|
1622
|
+
return max + 1;
|
|
1623
|
+
let prev = Array.from({ length: b.length + 1 }, (_, j) => j);
|
|
1624
|
+
for (let i = 1;i <= a.length; i++) {
|
|
1625
|
+
const cur = [i];
|
|
1626
|
+
let rowMin = i;
|
|
1627
|
+
for (let j = 1;j <= b.length; j++) {
|
|
1628
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
1629
|
+
cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + cost);
|
|
1630
|
+
rowMin = Math.min(rowMin, cur[j]);
|
|
1631
|
+
}
|
|
1632
|
+
if (rowMin > max)
|
|
1633
|
+
return max + 1;
|
|
1634
|
+
prev = cur;
|
|
1635
|
+
}
|
|
1636
|
+
return prev[b.length];
|
|
1637
|
+
}
|
|
1638
|
+
var HINT_MAX_EDITS = 2;
|
|
1639
|
+
var HINT_MIN_PREFIX = 3;
|
|
1640
|
+
function nearestNodeId(root, ids) {
|
|
1641
|
+
const lower = root.toLowerCase();
|
|
1642
|
+
let best;
|
|
1643
|
+
let bestScore = Number.POSITIVE_INFINITY;
|
|
1644
|
+
for (const id of ids) {
|
|
1645
|
+
const l = id.toLowerCase();
|
|
1646
|
+
if (l === lower)
|
|
1647
|
+
return id;
|
|
1648
|
+
if (lower.length < HINT_MIN_PREFIX)
|
|
1649
|
+
continue;
|
|
1650
|
+
let score = boundedEditDistance(lower, l, HINT_MAX_EDITS);
|
|
1651
|
+
if (score > HINT_MAX_EDITS) {
|
|
1652
|
+
const [short, long] = lower.length <= l.length ? [lower, l] : [l, lower];
|
|
1653
|
+
const isPrefix = short.length >= HINT_MIN_PREFIX && short.length * 2 >= long.length && long.startsWith(short);
|
|
1654
|
+
if (!isPrefix)
|
|
1655
|
+
continue;
|
|
1656
|
+
score = HINT_MAX_EDITS + (long.length - short.length);
|
|
1657
|
+
}
|
|
1658
|
+
if (score < bestScore) {
|
|
1659
|
+
best = id;
|
|
1660
|
+
bestScore = score;
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
return best;
|
|
1664
|
+
}
|
|
1665
|
+
function labelOf(node) {
|
|
1666
|
+
return node.display?.label ?? node.id;
|
|
1667
|
+
}
|
|
1668
|
+
function arrayMemberIssue(args) {
|
|
1669
|
+
const { names, typeOf, whyArray } = declaredOutputs(args.producer, args.definition);
|
|
1670
|
+
if (!names.has(args.second) || typeOf(args.second) !== "array") {
|
|
1671
|
+
return;
|
|
1672
|
+
}
|
|
1673
|
+
if (/^\d+$/.test(args.third) || ARRAY_MEMBERS.has(args.third)) {
|
|
1674
|
+
return;
|
|
1675
|
+
}
|
|
1676
|
+
const { root, second, third } = args;
|
|
1677
|
+
return {
|
|
1678
|
+
path: args.path,
|
|
1679
|
+
severity: args.undefinedSafe || args.embedded ? "warning" : args.severity,
|
|
1680
|
+
message: `"${args.reader}" reads "${args.refText}", but "$vars.${root}.${second}" is an array (${whyArray(second)}), so ".${third}" is undefined. ` + `Iterate it (a Loop's collection is "$vars.${root}.${second}" itself), index it ("[0].${third}"), or use an Array method.`
|
|
1681
|
+
};
|
|
1682
|
+
}
|
|
1683
|
+
function decisionUnpopulatedOutputIssue(args) {
|
|
1684
|
+
if (args.producer.type !== DECISION_NODE_TYPE || !DECISION_UNPOPULATED_OUTPUTS.has(args.second)) {
|
|
1685
|
+
return;
|
|
1686
|
+
}
|
|
1687
|
+
const { root, second } = args;
|
|
1688
|
+
return {
|
|
1689
|
+
path: args.path,
|
|
1690
|
+
severity: "error",
|
|
1691
|
+
message: `"${args.reader}" reads "${args.refText}", but a Decision never populates its "${second}" output at runtime — the packed flow emits the gateway with no output mapping, so "$vars.${root}.${second}" is always null (a branch or value chosen from it silently takes the wrong path). ` + `Route logic from the Decision's own output branches instead, or compute the value in an upstream node and read that.`
|
|
1692
|
+
};
|
|
1693
|
+
}
|
|
1694
|
+
function validateVarsReferences(flow, options = {}) {
|
|
1695
|
+
const issues = [];
|
|
1696
|
+
const nodesById = new Map(flow.nodes.map((n) => [n.id, n]));
|
|
1697
|
+
if (nodesById.size === 0)
|
|
1698
|
+
return issues;
|
|
1699
|
+
const knownRoots = new Set(options.extraKnownRoots ?? []);
|
|
1700
|
+
const variables = flow.variables;
|
|
1701
|
+
for (const g of variables?.globals ?? []) {
|
|
1702
|
+
if (typeof g?.id === "string")
|
|
1703
|
+
knownRoots.add(g.id);
|
|
1704
|
+
}
|
|
1705
|
+
const updatesByNode = variables?.variableUpdates ?? {};
|
|
1706
|
+
const defByKey = new Map;
|
|
1707
|
+
const defByType = new Map;
|
|
1708
|
+
for (const d of flow.definitions) {
|
|
1709
|
+
defByKey.set(`${d.nodeType}:${d.version}`, d);
|
|
1710
|
+
if (!defByType.has(d.nodeType))
|
|
1711
|
+
defByType.set(d.nodeType, d);
|
|
1712
|
+
}
|
|
1713
|
+
const definitionOf = (n) => (n.typeVersion ? defByKey.get(`${n.type}:${n.typeVersion}`) : undefined) ?? defByType.get(n.type);
|
|
1714
|
+
const modelTypeOf = (n) => typeof n.model?.type === "string" ? n.model.type : definitionOf(n)?.model?.type;
|
|
1715
|
+
const ancestors = buildAncestors(flow);
|
|
1716
|
+
const mustRun = buildMustRun(flow, modelTypeOf);
|
|
1717
|
+
const reported = new Set;
|
|
1718
|
+
for (const occurrence of collectVarsRefs(flow.nodes, updatesByNode)) {
|
|
1719
|
+
const node = nodesById.get(occurrence.nodeId);
|
|
1720
|
+
if (!node)
|
|
1721
|
+
continue;
|
|
1722
|
+
const { ref, embedded } = occurrence;
|
|
1723
|
+
const reader = labelOf(node);
|
|
1724
|
+
const path = `nodes[${node.id}].${occurrence.fieldPath}`;
|
|
1725
|
+
const severity = occurrence.fieldPath.startsWith("inputs") ? "error" : "warning";
|
|
1726
|
+
const [root, second, third] = ref.segments;
|
|
1727
|
+
const key = `${node.id}|${occurrence.fieldPath}|${ref.segments.join(".")}`;
|
|
1728
|
+
if (reported.has(key))
|
|
1729
|
+
continue;
|
|
1730
|
+
reported.add(key);
|
|
1731
|
+
if (root === node.id)
|
|
1732
|
+
continue;
|
|
1733
|
+
if (knownRoots.has(root))
|
|
1734
|
+
continue;
|
|
1735
|
+
const producer = nodesById.get(root);
|
|
1736
|
+
const anc = ancestors.get(node.id) ?? new Set;
|
|
1737
|
+
if (occurrence.isAgentBinding) {
|
|
1738
|
+
if (producer && anc.size > 0 && anc.has(root)) {
|
|
1739
|
+
if (!mustRun(node.id).has(root)) {
|
|
1740
|
+
issues.push({
|
|
1741
|
+
path,
|
|
1742
|
+
severity: undefinedSafe(ref, 0) ? "warning" : severity,
|
|
1743
|
+
message: `Agent "${reader}" input binds "${ref.text}", but "${root}" runs only on some of the paths that reach "${node.id}" (it sits behind a Decision or Switch branch the agent does not depend on). ` + "On the other paths the input is undefined and the agent invocation faults ([400300] Error evaluating expression in activity inputs). " + "Bind a node that runs on every path to the agent, or compute the value on each branch before they rejoin."
|
|
1744
|
+
});
|
|
1745
|
+
} else if (second !== undefined) {
|
|
1746
|
+
const decisionIssue = decisionUnpopulatedOutputIssue({
|
|
1747
|
+
producer,
|
|
1748
|
+
root,
|
|
1749
|
+
second,
|
|
1750
|
+
reader,
|
|
1751
|
+
refText: ref.text,
|
|
1752
|
+
path
|
|
1753
|
+
});
|
|
1754
|
+
if (decisionIssue) {
|
|
1755
|
+
issues.push(decisionIssue);
|
|
1756
|
+
} else if (third !== undefined) {
|
|
1757
|
+
const issue = arrayMemberIssue({
|
|
1758
|
+
producer,
|
|
1759
|
+
definition: definitionOf(producer),
|
|
1760
|
+
root,
|
|
1761
|
+
second,
|
|
1762
|
+
third,
|
|
1763
|
+
reader,
|
|
1764
|
+
refText: ref.text,
|
|
1765
|
+
path,
|
|
1766
|
+
undefinedSafe: undefinedSafe(ref, 2),
|
|
1767
|
+
embedded,
|
|
1768
|
+
severity
|
|
1769
|
+
});
|
|
1770
|
+
if (issue)
|
|
1771
|
+
issues.push(issue);
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
continue;
|
|
1776
|
+
}
|
|
1777
|
+
if (!producer) {
|
|
1778
|
+
const hint = nearestNodeId(root, nodesById.keys());
|
|
1779
|
+
issues.push({
|
|
1780
|
+
path,
|
|
1781
|
+
severity: undefinedSafe(ref, 0) ? "warning" : severity,
|
|
1782
|
+
message: `"${reader}" reads "${ref.text}", but no node or workflow variable named "${root}" exists in this flow` + (hint ? ` (did you mean node "${hint}"?)` : "") + ". At runtime the reference resolves to nothing — a Script reading it faults ([300501]), a Decision or Switch faults ([400302]), a Transform or connector input silently gets an empty value. Fix the id or declare the variable under variables.globals."
|
|
1783
|
+
});
|
|
1784
|
+
continue;
|
|
1785
|
+
}
|
|
1786
|
+
if (anc.size > 0 && !anc.has(root)) {
|
|
1787
|
+
const runsAfter = (ancestors.get(root) ?? new Set).has(node.id);
|
|
1788
|
+
const why = runsAfter ? `"${root}" runs AFTER "${node.id}" (it is downstream of it)` : `"${root}" is not on any path that reaches "${node.id}" (another branch, or not connected)`;
|
|
1789
|
+
issues.push({
|
|
1790
|
+
path,
|
|
1791
|
+
severity: undefinedSafe(ref, 0) ? "warning" : severity,
|
|
1792
|
+
message: `"${reader}" reads "${ref.text}", but ${why}, so "${root}" has not produced a value when this expression is evaluated ` + "(Decision/Switch: [400302]; Script: [300501]; agent or connector input: [400300]/empty value). " + `Reference a node that runs earlier on this path, or move "${node.id}" after "${root}".`
|
|
1793
|
+
});
|
|
1794
|
+
continue;
|
|
1795
|
+
}
|
|
1796
|
+
if (second !== undefined) {
|
|
1797
|
+
const decisionIssue = decisionUnpopulatedOutputIssue({
|
|
1798
|
+
producer,
|
|
1799
|
+
root,
|
|
1800
|
+
second,
|
|
1801
|
+
reader,
|
|
1802
|
+
refText: ref.text,
|
|
1803
|
+
path
|
|
1804
|
+
});
|
|
1805
|
+
if (decisionIssue) {
|
|
1806
|
+
issues.push(decisionIssue);
|
|
1807
|
+
continue;
|
|
1808
|
+
}
|
|
1809
|
+
const { names } = declaredOutputs(producer, definitionOf(producer));
|
|
1810
|
+
const declaresOutputs = [...names].some((n) => n !== "error");
|
|
1811
|
+
if (declaresOutputs && !names.has(second)) {
|
|
1812
|
+
const available = [...names].sort((a, b) => a.localeCompare(b)).join(", ");
|
|
1813
|
+
issues.push({
|
|
1814
|
+
path,
|
|
1815
|
+
severity: undefinedSafe(ref, 1) ? "warning" : severity,
|
|
1816
|
+
message: `"${reader}" reads "${ref.text}", but node "${root}" has no output named "${second}" — its outputs are: ${available}. ` + `Read "$vars.${root}.<one of those>" (e.g. a Decision exposes "$vars.${root}.matchedCaseId", not ".output.matchedCaseId").`
|
|
1817
|
+
});
|
|
1818
|
+
continue;
|
|
1819
|
+
}
|
|
1820
|
+
if (third !== undefined) {
|
|
1821
|
+
const issue = arrayMemberIssue({
|
|
1822
|
+
producer,
|
|
1823
|
+
definition: definitionOf(producer),
|
|
1824
|
+
root,
|
|
1825
|
+
second,
|
|
1826
|
+
third,
|
|
1827
|
+
reader,
|
|
1828
|
+
refText: ref.text,
|
|
1829
|
+
path,
|
|
1830
|
+
undefinedSafe: undefinedSafe(ref, 2),
|
|
1831
|
+
embedded,
|
|
1832
|
+
severity
|
|
1833
|
+
});
|
|
1834
|
+
if (issue)
|
|
1835
|
+
issues.push(issue);
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
return issues;
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
// ../flow-tool/src/services/flow-validate-service.ts
|
|
1843
|
+
if (!instance.isInitialized) {
|
|
1844
|
+
instance.init({
|
|
1845
|
+
lng: "en",
|
|
1846
|
+
fallbackLng: "en",
|
|
1847
|
+
defaultNS: "validation",
|
|
1848
|
+
resources: {},
|
|
1849
|
+
interpolation: { escapeValue: false }
|
|
1850
|
+
});
|
|
1851
|
+
}
|
|
1852
|
+
registerFlowSchemaI18n(instance);
|
|
1853
|
+
function extractPromptVariables(content) {
|
|
1854
|
+
const vars = new Set;
|
|
1855
|
+
const re = /\{\{([^}]*)\}\}/g;
|
|
1856
|
+
let m = re.exec(content);
|
|
1857
|
+
while (m !== null) {
|
|
1858
|
+
vars.add(m[1]);
|
|
1859
|
+
m = re.exec(content);
|
|
1860
|
+
}
|
|
1861
|
+
return [...vars];
|
|
1862
|
+
}
|
|
1863
|
+
var VARS_NODE_OUTPUT_RE = /^=?\$vars\.([A-Za-z0-9_$]+)\.output\.([A-Za-z0-9_$]+)/;
|
|
1864
|
+
function getProducerOutputFields(node) {
|
|
1865
|
+
if (AGENT_NODE_TYPES.includes(node.type)) {
|
|
1866
|
+
const vars = node.inputs?.agentOutputVariables;
|
|
1867
|
+
if (!Array.isArray(vars) || vars.length === 0)
|
|
1868
|
+
return;
|
|
1869
|
+
const fields = new Set;
|
|
1870
|
+
for (const v of vars) {
|
|
1871
|
+
const id = v?.id;
|
|
1872
|
+
if (typeof id === "string")
|
|
1873
|
+
fields.add(id);
|
|
1874
|
+
}
|
|
1875
|
+
return fields.size > 0 ? fields : undefined;
|
|
1876
|
+
}
|
|
1877
|
+
const output = node.outputs?.output;
|
|
1878
|
+
const props = output?.properties;
|
|
1879
|
+
if (props && typeof props === "object") {
|
|
1880
|
+
return new Set(Object.keys(props));
|
|
1881
|
+
}
|
|
1882
|
+
return;
|
|
1883
|
+
}
|
|
1884
|
+
function decodeSchemaKeyNodeRef(key) {
|
|
1885
|
+
const segments = key.split("__");
|
|
1886
|
+
if (segments.length < 3)
|
|
1887
|
+
return;
|
|
1888
|
+
if (segments[0] === "metadata" || segments[1] !== "output")
|
|
1889
|
+
return;
|
|
1890
|
+
if (segments[0].length === 0)
|
|
1891
|
+
return;
|
|
1892
|
+
return { nodeId: segments[0], field: segments[2] || undefined };
|
|
1893
|
+
}
|
|
1894
|
+
function collectAgentInputVarIds(raw) {
|
|
1895
|
+
const ids = new Set;
|
|
1896
|
+
if (!Array.isArray(raw))
|
|
1897
|
+
return ids;
|
|
1898
|
+
for (const entry of raw) {
|
|
1899
|
+
if (!entry || typeof entry !== "object")
|
|
1900
|
+
continue;
|
|
1901
|
+
const e = entry;
|
|
1902
|
+
if (typeof e.id === "string" && e.id.length > 0)
|
|
1903
|
+
ids.add(e.id);
|
|
1904
|
+
}
|
|
1905
|
+
return ids;
|
|
1906
|
+
}
|
|
1907
|
+
function readAgentSourceId(node) {
|
|
1908
|
+
const fromInputs = node.inputs?.source;
|
|
1909
|
+
if (typeof fromInputs === "string" && fromInputs.length > 0) {
|
|
1910
|
+
return fromInputs;
|
|
1911
|
+
}
|
|
1912
|
+
return;
|
|
1913
|
+
}
|
|
1914
|
+
function formatHydrationHint(diag, flowDir) {
|
|
1915
|
+
if (diag.sourceLocation === "missing") {
|
|
1916
|
+
return "inline-agent source key not set on the node. Add `inputs.source = <projectId-uuid>` (canonical post-flow-core-0.2.50 location — the legacy `model.source` location is deprecated and rejected by validate) and ensure the colocated `<projectId>/agent.json` carries non-empty `messages[].content` for both system and user roles.";
|
|
1917
|
+
}
|
|
1918
|
+
const sourceId = diag.sourceId ?? "<projectId>";
|
|
1919
|
+
const expectedAgentJson = diag.agentJsonPath ?? `${flowDir}/${sourceId}/agent.json`;
|
|
1920
|
+
if (!diag.agentJsonExists) {
|
|
1921
|
+
const dirPart = diag.agentDirExists ? "directory exists but" : "directory not found and";
|
|
1922
|
+
return `agent.json not found at \`${expectedAgentJson}\` (${dirPart} agent.json file is missing). Re-run \`uip agent init "${flowDir}" --inline-in-flow\` and verify the returned ProjectId matches \`${diag.sourceLocation} = ${sourceId}\`.`;
|
|
1923
|
+
}
|
|
1924
|
+
if (!diag.messagesPresent) {
|
|
1925
|
+
return `\`${expectedAgentJson}\` parsed but has no \`messages\` array. Restore the scaffold shape: \`messages: [{ role: "system", content: "..." }, { role: "user", content: "..." }]\`.`;
|
|
1926
|
+
}
|
|
1927
|
+
if (!diag.systemPromptPopulated && !diag.userPromptPopulated) {
|
|
1928
|
+
return `\`${expectedAgentJson}\` has \`messages[]\` but both \`system\` and \`user\` content strings are empty (\`uip agent init --inline-in-flow\` scaffolds them empty by design). Populate \`messages[0].content\` (system) and \`messages[1].content\` (user) and rebuild \`contentTokens\` to match.`;
|
|
1929
|
+
}
|
|
1930
|
+
if (!diag.systemPromptPopulated) {
|
|
1931
|
+
return `\`${expectedAgentJson}\` is missing or has empty \`messages[].content\` for role=\`system\`. Populate \`messages[0].content\`.`;
|
|
1932
|
+
}
|
|
1933
|
+
if (!diag.userPromptPopulated) {
|
|
1934
|
+
return `\`${expectedAgentJson}\` is missing or has empty \`messages[].content\` for role=\`user\`. Populate \`messages[1].content\`.`;
|
|
1935
|
+
}
|
|
1936
|
+
return `inline-agent hydration ran but the validator still flagged \`${diag.sourceLocation}\`-resolved prompts as empty. This is unexpected — file an issue with the .flow and agent.json contents.`;
|
|
1937
|
+
}
|
|
1938
|
+
function collectNodeExpressionStrings(nodes) {
|
|
1939
|
+
const expressions = [];
|
|
1940
|
+
const collect = (val) => {
|
|
1941
|
+
if (typeof val === "string") {
|
|
1942
|
+
expressions.push(val);
|
|
1943
|
+
} else if (Array.isArray(val)) {
|
|
1944
|
+
for (const v of val)
|
|
1945
|
+
collect(v);
|
|
1946
|
+
} else if (val && typeof val === "object") {
|
|
1947
|
+
for (const v of Object.values(val))
|
|
1948
|
+
collect(v);
|
|
1949
|
+
}
|
|
1950
|
+
};
|
|
1951
|
+
for (const n of nodes) {
|
|
1952
|
+
collect(n.inputs);
|
|
1953
|
+
collect(n.outputs);
|
|
1954
|
+
}
|
|
1955
|
+
return expressions;
|
|
1956
|
+
}
|
|
1957
|
+
function extractSupportsErrorHandling(content) {
|
|
1958
|
+
const map = new Map;
|
|
1959
|
+
let raw;
|
|
1960
|
+
try {
|
|
1961
|
+
raw = JSON.parse(content);
|
|
1962
|
+
} catch {
|
|
1963
|
+
return map;
|
|
1964
|
+
}
|
|
1965
|
+
if (!raw || typeof raw !== "object")
|
|
1966
|
+
return map;
|
|
1967
|
+
const defs = raw.definitions;
|
|
1968
|
+
if (!Array.isArray(defs))
|
|
1969
|
+
return map;
|
|
1970
|
+
for (const def of defs) {
|
|
1971
|
+
if (!def || typeof def !== "object")
|
|
1972
|
+
continue;
|
|
1973
|
+
const d = def;
|
|
1974
|
+
if (typeof d.nodeType === "string" && typeof d.version === "string" && d.supportsErrorHandling === true) {
|
|
1975
|
+
map.set(`${d.nodeType}:${d.version}`, true);
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
return map;
|
|
1979
|
+
}
|
|
1980
|
+
function normalizeManifestVersion(version) {
|
|
1981
|
+
const raw = version ?? "1.0.0";
|
|
1982
|
+
const parts = raw.split(".");
|
|
1983
|
+
if (!parts.every((p) => /^\d+$/.test(p))) {
|
|
1984
|
+
return raw;
|
|
1985
|
+
}
|
|
1986
|
+
while (parts.length > 2 && parts[parts.length - 1] === "0") {
|
|
1987
|
+
parts.pop();
|
|
1988
|
+
}
|
|
1989
|
+
return parts.join(".");
|
|
1990
|
+
}
|
|
1991
|
+
function manifestVersionMatchesNode(nodeVersion, manifestVersion) {
|
|
1992
|
+
return normalizeManifestVersion(nodeVersion) === normalizeManifestVersion(manifestVersion);
|
|
1993
|
+
}
|
|
1994
|
+
function formatHandleList(concrete, templates) {
|
|
1995
|
+
const ports = [
|
|
1996
|
+
...concrete.keys(),
|
|
1997
|
+
...templates.map((t) => t.templateId.replace("{item.id}", "{id}"))
|
|
1998
|
+
];
|
|
1999
|
+
return ports.length > 0 ? ports.join(", ") : "(none)";
|
|
2000
|
+
}
|
|
2001
|
+
function validationErrorToIssue(error) {
|
|
2002
|
+
let path;
|
|
2003
|
+
if (error.nodeId) {
|
|
2004
|
+
path = error.fieldPath ? `nodes[${error.nodeId}].${error.fieldPath}` : `nodes[${error.nodeId}]`;
|
|
2005
|
+
} else {
|
|
2006
|
+
path = error.fieldPath ?? "(workflow)";
|
|
2007
|
+
}
|
|
2008
|
+
return {
|
|
2009
|
+
path,
|
|
2010
|
+
message: `[${error.code}] ${error.message}`,
|
|
2011
|
+
severity: isBlockingSeverity(error.severity) ? "error" : "warning"
|
|
2012
|
+
};
|
|
2013
|
+
}
|
|
2014
|
+
function buildValidationContext(flow) {
|
|
2015
|
+
const nodes = toSerializedNodes(flow.nodes);
|
|
2016
|
+
const edges = flow.edges.map((e) => ({
|
|
2017
|
+
id: e.id,
|
|
2018
|
+
source: e.sourceNodeId,
|
|
2019
|
+
sourceHandle: e.sourcePort ?? null,
|
|
2020
|
+
target: e.targetNodeId,
|
|
2021
|
+
targetHandle: e.targetPort ?? null
|
|
2022
|
+
}));
|
|
2023
|
+
const manifests = new Map;
|
|
2024
|
+
for (const def of flow.definitions) {
|
|
2025
|
+
manifests.set(`${def.nodeType}:${def.version}`, {
|
|
2026
|
+
nodeType: def.nodeType,
|
|
2027
|
+
version: def.version,
|
|
2028
|
+
category: def.category,
|
|
2029
|
+
display: def.display,
|
|
2030
|
+
handleConfiguration: def.handleConfiguration,
|
|
2031
|
+
inputDefinition: def.inputDefinition,
|
|
2032
|
+
outputDefinition: def.outputDefinition,
|
|
2033
|
+
tags: def.tags
|
|
2034
|
+
});
|
|
2035
|
+
}
|
|
2036
|
+
const outputVariableIds = [];
|
|
2037
|
+
const variables = flow.variables;
|
|
2038
|
+
if (variables?.globals) {
|
|
2039
|
+
for (const v of variables.globals) {
|
|
2040
|
+
if (v.direction === "out" || v.direction === "inout") {
|
|
2041
|
+
outputVariableIds.push(v.id);
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
return {
|
|
2046
|
+
nodes,
|
|
2047
|
+
edges,
|
|
2048
|
+
manifests,
|
|
2049
|
+
outputVariableIds
|
|
2050
|
+
};
|
|
2051
|
+
}
|
|
2052
|
+
function splitSchemaParseError(parseError) {
|
|
2053
|
+
const header = "Schema validation failed:";
|
|
2054
|
+
if (!parseError.startsWith(header)) {
|
|
2055
|
+
return null;
|
|
2056
|
+
}
|
|
2057
|
+
const body = parseError.slice(header.length);
|
|
2058
|
+
const lines = body.split(`
|
|
2059
|
+
`);
|
|
2060
|
+
const issues = [];
|
|
2061
|
+
for (const rawLine of lines) {
|
|
2062
|
+
const match = /^\s*-\s+(.+?):\s(.*)$/.exec(rawLine);
|
|
2063
|
+
if (!match)
|
|
2064
|
+
continue;
|
|
2065
|
+
const [, path, message] = match;
|
|
2066
|
+
if (!path || !message)
|
|
2067
|
+
continue;
|
|
2068
|
+
issues.push({ path, message });
|
|
2069
|
+
}
|
|
2070
|
+
return issues.length > 0 ? issues : null;
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
class FlowValidateService {
|
|
2074
|
+
fs;
|
|
2075
|
+
extraRules;
|
|
2076
|
+
nodeValidators;
|
|
2077
|
+
governanceEnforcements;
|
|
2078
|
+
availableModelNames;
|
|
2079
|
+
validateCurrentManifests;
|
|
2080
|
+
currentManifestOverrides;
|
|
2081
|
+
strictBindings;
|
|
2082
|
+
strictExpressions;
|
|
2083
|
+
connectorCatalogOverride;
|
|
2084
|
+
currentManifestCache;
|
|
2085
|
+
connectorCatalogCache;
|
|
2086
|
+
constructor(options = {}) {
|
|
2087
|
+
this.fs = getFileSystem();
|
|
2088
|
+
this.extraRules = options.extraRules ?? [];
|
|
2089
|
+
this.nodeValidators = [
|
|
2090
|
+
...defaultNodeValidators,
|
|
2091
|
+
...options.extraNodeValidators ?? []
|
|
2092
|
+
];
|
|
2093
|
+
this.governanceEnforcements = options.governanceEnforcements;
|
|
2094
|
+
this.availableModelNames = options.availableModelNames;
|
|
2095
|
+
this.validateCurrentManifests = options.validateCurrentManifests === true || options.currentManifests !== undefined;
|
|
2096
|
+
this.currentManifestOverrides = options.currentManifests;
|
|
2097
|
+
this.strictBindings = options.strictBindings === true;
|
|
2098
|
+
this.strictExpressions = options.strictExpressions === true;
|
|
2099
|
+
this.connectorCatalogOverride = options.connectorCatalog;
|
|
2100
|
+
}
|
|
2101
|
+
async execute(flowFilePath) {
|
|
2102
|
+
const [error, result] = await catchError(this.validateFile(flowFilePath));
|
|
2103
|
+
if (error) {
|
|
2104
|
+
OutputFormatter.error({
|
|
2105
|
+
Result: RESULTS.Failure,
|
|
2106
|
+
Message: "Flow validation failed",
|
|
2107
|
+
Instructions: error.message
|
|
2108
|
+
});
|
|
2109
|
+
if (error.stack) {
|
|
2110
|
+
logger.error("Stack trace:");
|
|
2111
|
+
logger.error(error.stack);
|
|
2112
|
+
}
|
|
2113
|
+
return 1;
|
|
2114
|
+
}
|
|
2115
|
+
if (!result.success) {
|
|
2116
|
+
OutputFormatter.error({
|
|
2117
|
+
Result: RESULTS.Failure,
|
|
2118
|
+
Message: `Validation failed for ${result.filePath}`,
|
|
2119
|
+
Instructions: `Found ${result.issues.filter((i) => i.severity === "error").length} error(s):
|
|
2120
|
+
${result.issues.map((i) => ` - [${i.severity}] [${i.path}] ${i.message}`).join(`
|
|
2121
|
+
`)}`
|
|
2122
|
+
});
|
|
2123
|
+
return 1;
|
|
2124
|
+
}
|
|
2125
|
+
const warnings = result.issues.filter((i) => i.severity === "warning");
|
|
2126
|
+
OutputFormatter.success({
|
|
2127
|
+
Result: RESULTS.Success,
|
|
2128
|
+
Code: "FlowValidate",
|
|
2129
|
+
Data: {
|
|
2130
|
+
File: result.filePath,
|
|
2131
|
+
Status: "Valid",
|
|
2132
|
+
...warnings.length > 0 && {
|
|
2133
|
+
Warnings: `${warnings.length} warning(s):
|
|
2134
|
+
${warnings.map((i) => ` - [${i.path}] ${i.message}`).join(`
|
|
2135
|
+
`)}`
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
});
|
|
2139
|
+
return 0;
|
|
2140
|
+
}
|
|
2141
|
+
async loadCurrentManifestMap() {
|
|
2142
|
+
this.currentManifestCache ??= this.loadCurrentManifestMapUncached();
|
|
2143
|
+
return this.currentManifestCache;
|
|
2144
|
+
}
|
|
2145
|
+
async loadCurrentManifestMapUncached() {
|
|
2146
|
+
const manifests = this.currentManifestOverrides;
|
|
2147
|
+
if (manifests) {
|
|
2148
|
+
return new Map(manifests.map((m) => [m.nodeType.toLowerCase(), m]));
|
|
2149
|
+
}
|
|
2150
|
+
const [loginError, loginStatus] = await catchError(getLoginStatusAsync());
|
|
2151
|
+
if (loginError) {
|
|
2152
|
+
logger.warn(`Could not read login status for current manifest validation; using OOTB manifests: ${loginError.message}`);
|
|
2153
|
+
}
|
|
2154
|
+
const client = new ManifestClient({
|
|
2155
|
+
baseUrl: loginStatus?.baseUrl || "localhost"
|
|
2156
|
+
});
|
|
2157
|
+
const auth = loginStatus?.loginStatus === "Logged in" && loginStatus.accessToken && loginStatus.tenantId && loginStatus.organizationId ? {
|
|
2158
|
+
accessToken: loginStatus.accessToken,
|
|
2159
|
+
tenantId: loginStatus.tenantId,
|
|
2160
|
+
organizationId: loginStatus.organizationId
|
|
2161
|
+
} : undefined;
|
|
2162
|
+
const [error, manifest] = await catchError(client.getManifest({
|
|
2163
|
+
...auth && { auth },
|
|
2164
|
+
inlineAgentNodes: true
|
|
2165
|
+
}));
|
|
2166
|
+
if (error || !manifest?.nodes) {
|
|
2167
|
+
logger.warn(`Could not load current OOTB node manifests; skipping stale-handle validation: ${error?.message ?? "manifest response did not include nodes"}`);
|
|
2168
|
+
return new Map;
|
|
2169
|
+
}
|
|
2170
|
+
return new Map(manifest.nodes.map((m) => [
|
|
2171
|
+
m.nodeType.toLowerCase(),
|
|
2172
|
+
m
|
|
2173
|
+
]));
|
|
2174
|
+
}
|
|
2175
|
+
async validateFile(flowFilePath) {
|
|
2176
|
+
let absolutePath = this.fs.path.resolve(flowFilePath);
|
|
2177
|
+
if (!await this.fs.exists(absolutePath)) {
|
|
2178
|
+
throw new Error(`File not found: ${absolutePath}`);
|
|
2179
|
+
}
|
|
2180
|
+
const stats = await this.fs.stat(absolutePath);
|
|
2181
|
+
if (stats?.isDirectory()) {
|
|
2182
|
+
absolutePath = await resolveProjectFlowFile(absolutePath);
|
|
2183
|
+
}
|
|
2184
|
+
if (!absolutePath.endsWith(".flow")) {
|
|
2185
|
+
throw new Error(`Expected a .flow file but got: ${this.fs.path.basename(absolutePath)}`);
|
|
2186
|
+
}
|
|
2187
|
+
logger.info(`
|
|
2188
|
+
Validating: ${absolutePath}
|
|
2189
|
+
`);
|
|
2190
|
+
const content = await this.fs.readFile(absolutePath, "utf-8");
|
|
2191
|
+
if (!content) {
|
|
2192
|
+
throw new Error(`Could not read file: ${absolutePath}`);
|
|
2193
|
+
}
|
|
2194
|
+
const issues = [];
|
|
2195
|
+
const parsed = parseWorkflowJson(content);
|
|
2196
|
+
const { workflow, parseError, parseIssues } = parsed;
|
|
2197
|
+
if (parseError || !workflow) {
|
|
2198
|
+
const recoveredIssues = parseIssues && parseIssues.length > 0 ? parseIssues : parseError ? splitSchemaParseError(parseError) : null;
|
|
2199
|
+
const fallback = [
|
|
2200
|
+
{
|
|
2201
|
+
path: "(root)",
|
|
2202
|
+
message: parseError ?? "File is not valid JSON",
|
|
2203
|
+
severity: "error"
|
|
2204
|
+
}
|
|
2205
|
+
];
|
|
2206
|
+
return {
|
|
2207
|
+
success: false,
|
|
2208
|
+
filePath: absolutePath,
|
|
2209
|
+
issues: recoveredIssues && recoveredIssues.length > 0 ? recoveredIssues.map((pi) => ({
|
|
2210
|
+
path: pi.path,
|
|
2211
|
+
message: pi.message,
|
|
2212
|
+
severity: "error"
|
|
2213
|
+
})) : fallback
|
|
2214
|
+
};
|
|
2215
|
+
}
|
|
2216
|
+
const rawDefErrorHandling = extractSupportsErrorHandling(content);
|
|
2217
|
+
const currentManifests = this.validateCurrentManifests ? await this.loadCurrentManifestMap() : new Map;
|
|
2218
|
+
const flowDir = this.fs.path.dirname(absolutePath);
|
|
2219
|
+
const hydrationDiagnoses = await this.resolveAgentPrompts(workflow.nodes, flowDir);
|
|
2220
|
+
issues.push(...await this.validateForwardMigration(content, absolutePath));
|
|
2221
|
+
issues.push(...this.validateGraph(workflow, "", rawDefErrorHandling, currentManifests));
|
|
2222
|
+
issues.push(...this.validateGlobalVariableUniqueness(workflow, ""));
|
|
2223
|
+
issues.push(...this.validateGlobalTriggerBindings(workflow, ""));
|
|
2224
|
+
issues.push(...this.validateTriggerScopedBareVarReads(workflow, ""));
|
|
2225
|
+
issues.push(...this.validateUnboundInputGlobals(workflow, ""));
|
|
2226
|
+
issues.push(...this.validateResourceBindings(workflow));
|
|
2227
|
+
issues.push(...this.validateConnectionIdReuse(workflow));
|
|
2228
|
+
issues.push(...this.validateDuplicateProcessTargets(workflow));
|
|
2229
|
+
issues.push(...await this.validateConnectorBindingShapes(workflow));
|
|
2230
|
+
issues.push(...this.validateAgentInputBindings(workflow, hydrationDiagnoses, ""));
|
|
2231
|
+
issues.push(...this.validateAgentPromptInputRefs(workflow, hydrationDiagnoses, ""));
|
|
2232
|
+
issues.push(...this.validateAgentInputScopeRefs(workflow, hydrationDiagnoses, ""));
|
|
2233
|
+
issues.push(...this.validateVoiceAgentSettings(workflow, hydrationDiagnoses, ""));
|
|
2234
|
+
issues.push(...await this.validateSolutionRegistrationAndLayout(absolutePath));
|
|
2235
|
+
issues.push(...await validateExpressions(workflow, "", this.strictExpressions));
|
|
2236
|
+
const subflows = workflow.subflows ?? {};
|
|
2237
|
+
for (const [subflowId, entry] of Object.entries(subflows)) {
|
|
2238
|
+
const subNodes = entry.nodes;
|
|
2239
|
+
const subDiagnoses = await this.resolveAgentPrompts(subNodes, flowDir);
|
|
2240
|
+
for (const [k, v] of subDiagnoses)
|
|
2241
|
+
hydrationDiagnoses.set(k, v);
|
|
2242
|
+
const subGraph = {
|
|
2243
|
+
nodes: subNodes,
|
|
2244
|
+
edges: entry.edges,
|
|
2245
|
+
definitions: workflow.definitions,
|
|
2246
|
+
variables: entry.variables
|
|
2247
|
+
};
|
|
2248
|
+
const prefix = `subflows[${subflowId}].`;
|
|
2249
|
+
issues.push(...this.validateGraph(subGraph, prefix, rawDefErrorHandling, currentManifests, (workflow.variables?.globals ?? []).map((g) => g.id).filter((id) => typeof id === "string")));
|
|
2250
|
+
issues.push(...this.validateGlobalVariableUniqueness(subGraph, prefix));
|
|
2251
|
+
issues.push(...this.validateGlobalTriggerBindings(subGraph, prefix));
|
|
2252
|
+
issues.push(...this.validateTriggerScopedBareVarReads(subGraph, prefix));
|
|
2253
|
+
issues.push(...this.validateUnboundInputGlobals(subGraph, prefix));
|
|
2254
|
+
issues.push(...this.validateAgentInputBindings(subGraph, hydrationDiagnoses, prefix));
|
|
2255
|
+
issues.push(...this.validateAgentPromptInputRefs(subGraph, hydrationDiagnoses, prefix));
|
|
2256
|
+
issues.push(...this.validateAgentInputScopeRefs(subGraph, hydrationDiagnoses, prefix));
|
|
2257
|
+
issues.push(...this.rejectSubflowVoiceAgents(subGraph, prefix));
|
|
2258
|
+
issues.push(...await validateExpressions({
|
|
2259
|
+
...subGraph,
|
|
2260
|
+
subflows
|
|
2261
|
+
}, prefix, this.strictExpressions));
|
|
2262
|
+
}
|
|
2263
|
+
this.enrichInlineAgentPromptIssues(issues, hydrationDiagnoses, flowDir);
|
|
2264
|
+
return {
|
|
2265
|
+
success: issues.filter((i) => i.severity === "error").length === 0,
|
|
2266
|
+
filePath: absolutePath,
|
|
2267
|
+
issues
|
|
2268
|
+
};
|
|
2269
|
+
}
|
|
2270
|
+
async validateForwardMigration(content, absolutePath) {
|
|
2271
|
+
let raw;
|
|
2272
|
+
try {
|
|
2273
|
+
raw = JSON.parse(content);
|
|
2274
|
+
} catch {
|
|
2275
|
+
return [];
|
|
2276
|
+
}
|
|
2277
|
+
if (!raw || typeof raw !== "object")
|
|
2278
|
+
return [];
|
|
2279
|
+
let resolved;
|
|
2280
|
+
try {
|
|
2281
|
+
({ resolved } = await resolveWorkflowRefs(raw, createFlowRefResolver(this.fs), absolutePath));
|
|
2282
|
+
} catch {
|
|
2283
|
+
return [];
|
|
2284
|
+
}
|
|
2285
|
+
const check = checkForwardMigration(resolved);
|
|
2286
|
+
if (check.ok)
|
|
2287
|
+
return [];
|
|
2288
|
+
return [
|
|
2289
|
+
{
|
|
2290
|
+
path: "version",
|
|
2291
|
+
message: `[MIGRATION] ${check.message}`,
|
|
2292
|
+
severity: "error"
|
|
2293
|
+
}
|
|
2294
|
+
];
|
|
2295
|
+
}
|
|
2296
|
+
async resolveAgentPrompts(nodes, flowDir) {
|
|
2297
|
+
const diagnoses = new Map;
|
|
2298
|
+
for (const node of nodes) {
|
|
2299
|
+
if (!AGENT_NODE_TYPES.includes(node.type))
|
|
2300
|
+
continue;
|
|
2301
|
+
const source = readAgentSourceId(node);
|
|
2302
|
+
if (!source) {
|
|
2303
|
+
diagnoses.set(node.id, {
|
|
2304
|
+
sourceLocation: "missing",
|
|
2305
|
+
agentDirExists: false,
|
|
2306
|
+
agentJsonExists: false,
|
|
2307
|
+
messagesPresent: false,
|
|
2308
|
+
systemPromptPopulated: false,
|
|
2309
|
+
userPromptPopulated: false
|
|
2310
|
+
});
|
|
2311
|
+
continue;
|
|
2312
|
+
}
|
|
2313
|
+
const sourceLocation = "inputs.source";
|
|
2314
|
+
if (/[/\\.]/.test(source)) {
|
|
2315
|
+
diagnoses.set(node.id, {
|
|
2316
|
+
sourceLocation,
|
|
2317
|
+
sourceId: source,
|
|
2318
|
+
agentDirExists: false,
|
|
2319
|
+
agentJsonExists: false,
|
|
2320
|
+
messagesPresent: false,
|
|
2321
|
+
systemPromptPopulated: false,
|
|
2322
|
+
userPromptPopulated: false
|
|
2323
|
+
});
|
|
2324
|
+
continue;
|
|
2325
|
+
}
|
|
2326
|
+
if (!node.inputs)
|
|
2327
|
+
node.inputs = {};
|
|
2328
|
+
const inputs = node.inputs;
|
|
2329
|
+
const diag = {
|
|
2330
|
+
sourceLocation,
|
|
2331
|
+
sourceId: source,
|
|
2332
|
+
agentDirExists: false,
|
|
2333
|
+
agentJsonExists: false,
|
|
2334
|
+
messagesPresent: false,
|
|
2335
|
+
systemPromptPopulated: false,
|
|
2336
|
+
userPromptPopulated: false
|
|
2337
|
+
};
|
|
2338
|
+
diagnoses.set(node.id, diag);
|
|
2339
|
+
const agentDir = this.fs.path.join(flowDir, source);
|
|
2340
|
+
diag.agentDirExists = await this.fs.exists(agentDir);
|
|
2341
|
+
const agentJsonPath = this.fs.path.join(agentDir, "agent.json");
|
|
2342
|
+
diag.agentJsonPath = agentJsonPath;
|
|
2343
|
+
diag.agentJsonExists = await this.fs.exists(agentJsonPath);
|
|
2344
|
+
if (!diag.agentJsonExists)
|
|
2345
|
+
continue;
|
|
2346
|
+
const [readErr, raw] = await catchError(this.fs.readFile(agentJsonPath, "utf-8"));
|
|
2347
|
+
if (readErr || !raw) {
|
|
2348
|
+
diag.agentJsonUnparseable = true;
|
|
2349
|
+
continue;
|
|
2350
|
+
}
|
|
2351
|
+
const [parseErr, agent] = await catchError(Promise.resolve(raw).then((s) => JSON.parse(s)));
|
|
2352
|
+
if (parseErr || !agent) {
|
|
2353
|
+
diag.agentJsonUnparseable = true;
|
|
2354
|
+
continue;
|
|
2355
|
+
}
|
|
2356
|
+
const settings = agent.settings;
|
|
2357
|
+
if (isVoiceAgentNodeType(node.type)) {
|
|
2358
|
+
const voiceSettings = isRecord(settings) ? settings.voice : undefined;
|
|
2359
|
+
diag.voiceSettingsPresent = isRecord(voiceSettings);
|
|
2360
|
+
if (diag.voiceSettingsPresent && inputs.voice === undefined) {
|
|
2361
|
+
inputs.voice = voiceSettings;
|
|
2362
|
+
}
|
|
2363
|
+
diag.isConversationalAgent = isRecord(agent.metadata) && agent.metadata.isConversational === true;
|
|
2364
|
+
}
|
|
2365
|
+
const messages = agent.messages;
|
|
2366
|
+
if (!Array.isArray(messages))
|
|
2367
|
+
continue;
|
|
2368
|
+
diag.messagesPresent = true;
|
|
2369
|
+
const systemVars = new Set;
|
|
2370
|
+
const userVars = new Set;
|
|
2371
|
+
for (const msg of messages) {
|
|
2372
|
+
if (!msg || typeof msg !== "object")
|
|
2373
|
+
continue;
|
|
2374
|
+
const m = msg;
|
|
2375
|
+
if (typeof m.content === "string") {
|
|
2376
|
+
const target = m.role === "system" ? systemVars : userVars;
|
|
2377
|
+
for (const v of extractPromptVariables(m.content)) {
|
|
2378
|
+
target.add(v);
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
if (m.role === "system" && typeof m.content === "string") {
|
|
2382
|
+
inputs.systemPrompt ??= m.content;
|
|
2383
|
+
if (m.content.length > 0)
|
|
2384
|
+
diag.systemPromptPopulated = true;
|
|
2385
|
+
}
|
|
2386
|
+
if (m.role === "user" && typeof m.content === "string") {
|
|
2387
|
+
inputs.userPrompt ??= m.content;
|
|
2388
|
+
if (m.content.length > 0)
|
|
2389
|
+
diag.userPromptPopulated = true;
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
const byField = {};
|
|
2393
|
+
if (systemVars.size > 0)
|
|
2394
|
+
byField.systemPrompt = [...systemVars];
|
|
2395
|
+
if (userVars.size > 0)
|
|
2396
|
+
byField.userPrompt = [...userVars];
|
|
2397
|
+
if (Object.keys(byField).length > 0) {
|
|
2398
|
+
diag.promptVariablesByField = byField;
|
|
2399
|
+
}
|
|
2400
|
+
if (settings && typeof settings === "object") {
|
|
2401
|
+
const s = settings;
|
|
2402
|
+
if (typeof s.temperature === "number" && inputs.temperature === undefined) {
|
|
2403
|
+
inputs.temperature = s.temperature;
|
|
2404
|
+
}
|
|
2405
|
+
if (typeof s.maxTokens === "number" && inputs.maxTokenPerResponse === undefined) {
|
|
2406
|
+
inputs.maxTokenPerResponse = s.maxTokens;
|
|
2407
|
+
}
|
|
2408
|
+
if (typeof s.model === "string" && inputs.model === undefined) {
|
|
2409
|
+
inputs.model = s.model;
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
const inputSchema = agent.inputSchema;
|
|
2413
|
+
if (inputSchema && typeof inputSchema === "object") {
|
|
2414
|
+
const props = inputSchema.properties;
|
|
2415
|
+
if (props && typeof props === "object") {
|
|
2416
|
+
diag.declaredInputKeys = Object.keys(props);
|
|
2417
|
+
const leaves = collectAgentInputLeaves(inputSchema);
|
|
2418
|
+
diag.declaredInputLeaves = leaves;
|
|
2419
|
+
diag.declaredInputPaths = new Set([
|
|
2420
|
+
...leaves.map((leaf) => leaf.path),
|
|
2421
|
+
...diag.declaredInputKeys.map(canonicalAgentInputPath)
|
|
2422
|
+
]);
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
return diagnoses;
|
|
2427
|
+
}
|
|
2428
|
+
validateAgentInputBindings(graph, diagnoses, pathPrefix) {
|
|
2429
|
+
const issues = [];
|
|
2430
|
+
for (const node of graph.nodes) {
|
|
2431
|
+
if (!AGENT_NODE_TYPES.includes(node.type))
|
|
2432
|
+
continue;
|
|
2433
|
+
const diag = diagnoses.get(node.id);
|
|
2434
|
+
const declared = diag?.declaredInputKeys ?? [];
|
|
2435
|
+
if (declared.length > 0)
|
|
2436
|
+
continue;
|
|
2437
|
+
if (!diag?.messagesPresent)
|
|
2438
|
+
continue;
|
|
2439
|
+
const provided = collectAgentInputVarIds(node.inputs?.agentInputVariables);
|
|
2440
|
+
if (provided.size > 0) {
|
|
2441
|
+
issues.push(this.buildIgnoredAgentInputsWarning(node, provided, pathPrefix));
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
return issues;
|
|
2445
|
+
}
|
|
2446
|
+
buildIgnoredAgentInputsWarning(node, provided, pathPrefix) {
|
|
2447
|
+
const label = node.display?.label ?? node.id;
|
|
2448
|
+
const many = provided.size > 1;
|
|
2449
|
+
const quoted = [...provided].map((p) => `"${p}"`).join(", ");
|
|
2450
|
+
return {
|
|
2451
|
+
path: `${pathPrefix}nodes[${node.id}].inputs.agentInputVariables`,
|
|
2452
|
+
message: `Agent "${label}" declares no inputs in its agent.json \`inputSchema\`, but the flow node binds ${provided.size} input${many ? "s" : ""} (${quoted}) under \`inputs.agentInputVariables\`. Because the agent.json \`inputSchema\` declares no inputs, at runtime ${many ? "these bindings are" : "this binding is"} silently dropped — the agent runs without the flow's inputs. Add the expected input${many ? "s" : ""} under \`inputSchema.properties\` in agent.json (then \`uip agent refresh --inline-in-flow\`), or remove the unused binding${many ? "s" : ""}.`,
|
|
2453
|
+
severity: "warning"
|
|
2454
|
+
};
|
|
2455
|
+
}
|
|
2456
|
+
validateAgentPromptInputRefs(graph, diagnoses, pathPrefix) {
|
|
2457
|
+
const issues = [];
|
|
2458
|
+
for (const node of graph.nodes) {
|
|
2459
|
+
if (!AGENT_NODE_TYPES.includes(node.type))
|
|
2460
|
+
continue;
|
|
2461
|
+
const diag = diagnoses.get(node.id);
|
|
2462
|
+
if (!diag?.messagesPresent)
|
|
2463
|
+
continue;
|
|
2464
|
+
const byField = diag.promptVariablesByField;
|
|
2465
|
+
if (!byField)
|
|
2466
|
+
continue;
|
|
2467
|
+
const declared = new Set(diag.declaredInputKeys ?? []);
|
|
2468
|
+
const declaredPaths = diag.declaredInputPaths ?? new Set;
|
|
2469
|
+
const label = node.display?.label ?? node.id;
|
|
2470
|
+
for (const [field, vars] of Object.entries(byField)) {
|
|
2471
|
+
issues.push(...this.buildPromptInputRefIssues(node, field, vars ?? [], declared, declaredPaths, label, pathPrefix));
|
|
2472
|
+
}
|
|
2473
|
+
}
|
|
2474
|
+
return issues;
|
|
2475
|
+
}
|
|
2476
|
+
rejectSubflowVoiceAgents(graph, pathPrefix) {
|
|
2477
|
+
const issues = [];
|
|
2478
|
+
for (const node of graph.nodes) {
|
|
2479
|
+
if (!isVoiceAgentNodeType(node.type))
|
|
2480
|
+
continue;
|
|
2481
|
+
const label = node.display?.label ?? node.id;
|
|
2482
|
+
issues.push({
|
|
2483
|
+
path: `${pathPrefix}nodes[${node.id}]`,
|
|
2484
|
+
message: `Voice agent "${label}": voice agent nodes are not supported inside subflows — move it to the top-level flow.`,
|
|
2485
|
+
severity: "error"
|
|
2486
|
+
});
|
|
2487
|
+
}
|
|
2488
|
+
return issues;
|
|
2489
|
+
}
|
|
2490
|
+
validateVoiceAgentSettings(graph, diagnoses, pathPrefix) {
|
|
2491
|
+
const issues = [];
|
|
2492
|
+
for (const node of graph.nodes) {
|
|
2493
|
+
if (!isVoiceAgentNodeType(node.type))
|
|
2494
|
+
continue;
|
|
2495
|
+
const diag = diagnoses.get(node.id);
|
|
2496
|
+
if (!diag || diag.sourceLocation === "missing")
|
|
2497
|
+
continue;
|
|
2498
|
+
const label = node.display?.label ?? node.id;
|
|
2499
|
+
const agentJsonPath = diag.agentJsonPath ?? `${diag.sourceId}/agent.json`;
|
|
2500
|
+
if (!diag.agentJsonExists) {
|
|
2501
|
+
issues.push({
|
|
2502
|
+
path: `${pathPrefix}nodes[${node.id}].inputs.source`,
|
|
2503
|
+
message: `Voice agent "${label}": agent.json not found at \`${agentJsonPath}\`. A voice agent node needs its backing agent directory inside the flow project — create it with \`uip agent init "<flowDir>" --inline-in-flow --conversational\` (then add \`settings.voice\` to its agent.json) or fix \`inputs.source\`.`,
|
|
2504
|
+
severity: "error"
|
|
2505
|
+
});
|
|
2506
|
+
continue;
|
|
2507
|
+
}
|
|
2508
|
+
if (diag.agentJsonUnparseable) {
|
|
2509
|
+
issues.push({
|
|
2510
|
+
path: `${pathPrefix}nodes[${node.id}].inputs.source`,
|
|
2511
|
+
message: `Voice agent "${label}": \`${agentJsonPath}\` exists but is empty or not valid JSON. Fix the file — check for merge-conflict markers or a truncated write.`,
|
|
2512
|
+
severity: "error"
|
|
2513
|
+
});
|
|
2514
|
+
continue;
|
|
2515
|
+
}
|
|
2516
|
+
if (diag.voiceSettingsPresent === false) {
|
|
2517
|
+
issues.push({
|
|
2518
|
+
path: `${pathPrefix}nodes[${node.id}].inputs.source`,
|
|
2519
|
+
message: `Voice agent "${label}": \`${agentJsonPath}\` has no \`settings.voice\`. Add the voice settings object (model, maxTokens, temperature, persona).`,
|
|
2520
|
+
severity: "error"
|
|
2521
|
+
});
|
|
2522
|
+
}
|
|
2523
|
+
if (diag.isConversationalAgent === false) {
|
|
2524
|
+
issues.push({
|
|
2525
|
+
path: `${pathPrefix}nodes[${node.id}].inputs.source`,
|
|
2526
|
+
message: `Voice agent "${label}": \`${agentJsonPath}\` is not a conversational agent. Voice agents run on the conversational engine — set \`metadata.isConversational: true\` and \`settings.engine: "conversational-v1"\`.`,
|
|
2527
|
+
severity: "error"
|
|
2528
|
+
});
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
return issues;
|
|
2532
|
+
}
|
|
2533
|
+
buildPromptInputRefIssues(node, field, vars, declared, declaredPaths, label, pathPrefix) {
|
|
2534
|
+
const issues = [];
|
|
2535
|
+
const malformed = [];
|
|
2536
|
+
const undeclared = [];
|
|
2537
|
+
for (const raw of vars) {
|
|
2538
|
+
const key = this.canonicalInputKey(raw);
|
|
2539
|
+
if (key === undefined) {
|
|
2540
|
+
malformed.push(raw);
|
|
2541
|
+
} else if (!declared.has(key) && !declaredPaths.has(canonicalAgentInputPath(key))) {
|
|
2542
|
+
undeclared.push(key);
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
const path = `${pathPrefix}nodes[${node.id}].inputs.${field}`;
|
|
2546
|
+
if (malformed.length > 0) {
|
|
2547
|
+
const many = malformed.length > 1;
|
|
2548
|
+
const quoted = malformed.map((r) => `{{${r}}}`).join(", ");
|
|
2549
|
+
issues.push({
|
|
2550
|
+
path,
|
|
2551
|
+
message: `Agent "${label}"'s prompt has placeholder${many ? "s" : ""} ${quoted} that ${many ? "do" : "does"} not use the \`input.\` namespace. The runtime binds a placeholder only in the canonical \`{{input.<key>}}\` form, so ${many ? "these stay" : "this stays"} literal text at runtime even when the value is delivered. Rewrite ${many ? "them" : "it"} as \`{{input.<key>}}\` (and declare \`<key>\` under \`inputSchema.properties\`).`,
|
|
2552
|
+
severity: "error"
|
|
2553
|
+
});
|
|
2554
|
+
}
|
|
2555
|
+
if (undeclared.length > 0) {
|
|
2556
|
+
const many = undeclared.length > 1;
|
|
2557
|
+
const placeholders = undeclared.map((u) => `{{input.${u}}}`).join(", ");
|
|
2558
|
+
const quoted = undeclared.map((u) => `"${u}"`).join(", ");
|
|
2559
|
+
const schemaState = declared.size === 0 ? "the agent.json declares no `inputSchema.properties`" : `${many ? "they are" : "it is"} absent from the agent.json \`inputSchema.properties\` (which declares ${[...declared].map((d) => `"${d}"`).join(", ")})`;
|
|
2560
|
+
issues.push({
|
|
2561
|
+
path,
|
|
2562
|
+
message: `Agent "${label}"'s prompt reads ${placeholders}, but ${schemaState}. The runtime only substitutes placeholders declared in \`inputSchema.properties\`, so at runtime ${many ? "these placeholders stay" : "this placeholder stays"} as ${many ? "literal tokens" : "a literal token"} — the agent receives the raw \`{{input.<key>}}\` text instead of the delivered value. Declare ${many ? "these inputs" : "this input"} (${quoted}) under \`inputSchema.properties\` in agent.json (e.g. \`uip agent refresh --inline-in-flow\`), or remove the placeholder${many ? "s" : ""} from the prompt.`,
|
|
2563
|
+
severity: "error"
|
|
2564
|
+
});
|
|
2565
|
+
}
|
|
2566
|
+
return issues;
|
|
2567
|
+
}
|
|
2568
|
+
validateAgentInputScopeRefs(graph, diagnoses, pathPrefix) {
|
|
2569
|
+
const agentNodes = graph.nodes.filter((n) => AGENT_NODE_TYPES.includes(n.type));
|
|
2570
|
+
if (agentNodes.length === 0)
|
|
2571
|
+
return [];
|
|
2572
|
+
const nodesById = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
2573
|
+
const nodeIds = new Set(nodesById.keys());
|
|
2574
|
+
const globalIds = new Set((graph.variables?.globals ?? []).map((g) => g.id));
|
|
2575
|
+
const incoming = new Map;
|
|
2576
|
+
for (const edge of graph.edges) {
|
|
2577
|
+
const sources = incoming.get(edge.targetNodeId);
|
|
2578
|
+
if (sources)
|
|
2579
|
+
sources.push(edge.sourceNodeId);
|
|
2580
|
+
else
|
|
2581
|
+
incoming.set(edge.targetNodeId, [edge.sourceNodeId]);
|
|
2582
|
+
}
|
|
2583
|
+
const issues = [];
|
|
2584
|
+
for (const node of agentNodes) {
|
|
2585
|
+
const { byNode: refs, byField } = this.collectAgentVarRefs(node, diagnoses.get(node.id), globalIds);
|
|
2586
|
+
if (refs.size === 0)
|
|
2587
|
+
continue;
|
|
2588
|
+
const wired = incoming.has(node.id);
|
|
2589
|
+
const upstream = wired ? this.collectUpstreamNodeIds(incoming, node.id) : new Set;
|
|
2590
|
+
const missing = [];
|
|
2591
|
+
const notUpstream = [];
|
|
2592
|
+
for (const [refId, origin] of refs) {
|
|
2593
|
+
if (!nodeIds.has(refId))
|
|
2594
|
+
missing.push(origin);
|
|
2595
|
+
else if (wired && !upstream.has(refId))
|
|
2596
|
+
notUpstream.push(origin);
|
|
2597
|
+
}
|
|
2598
|
+
const label = node.display?.label ?? node.id;
|
|
2599
|
+
const path = `${pathPrefix}nodes[${node.id}].inputs.agentInputVariables`;
|
|
2600
|
+
const scope = pathPrefix ? "subflow" : "flow";
|
|
2601
|
+
if (missing.length > 0) {
|
|
2602
|
+
issues.push({
|
|
2603
|
+
path,
|
|
2604
|
+
message: `Agent "${label}" reads ${missing.join(", ")}, but no such node exists in this ${scope}. Studio Web rejects the flow on open with "that variable is not available in this scope", and at runtime the input resolves to nothing. Point the reference at an existing node id (then re-run \`uip agent refresh --inline-in-flow\` so agent.json and the node agree), or drop the input.`,
|
|
2605
|
+
severity: "error"
|
|
2606
|
+
});
|
|
2607
|
+
}
|
|
2608
|
+
if (notUpstream.length > 0) {
|
|
2609
|
+
issues.push({
|
|
2610
|
+
path,
|
|
2611
|
+
message: `Agent "${label}" reads ${notUpstream.join(", ")}, but no edge path leads from that node to "${node.id}", so it has not run when the agent does and its output is not written yet ([400300] Error evaluating expression in activity inputs). Studio Web reports the same variable as not available in this scope. Wire the node upstream of the agent, or bind an input the agent can see.`,
|
|
2612
|
+
severity: "error"
|
|
2613
|
+
});
|
|
2614
|
+
}
|
|
2615
|
+
const invalidFields = [];
|
|
2616
|
+
for (const ref of byField) {
|
|
2617
|
+
if (!nodeIds.has(ref.nodeId))
|
|
2618
|
+
continue;
|
|
2619
|
+
const producer = nodesById.get(ref.nodeId);
|
|
2620
|
+
if (!producer)
|
|
2621
|
+
continue;
|
|
2622
|
+
const outputFields = getProducerOutputFields(producer);
|
|
2623
|
+
if (!outputFields)
|
|
2624
|
+
continue;
|
|
2625
|
+
if (outputFields.has(ref.field))
|
|
2626
|
+
continue;
|
|
2627
|
+
const available = [...outputFields].join(", ");
|
|
2628
|
+
invalidFields.push(`${ref.origin} — node "${ref.nodeId}" has no output named "${ref.field}"` + (available ? ` (available: ${available})` : ""));
|
|
2629
|
+
}
|
|
2630
|
+
if (invalidFields.length > 0) {
|
|
2631
|
+
issues.push({
|
|
2632
|
+
path,
|
|
2633
|
+
message: `Agent "${label}" reads ${invalidFields.join("; ")}. Fix the binding to use an output the producer node declares, or add the missing output to the producer.`,
|
|
2634
|
+
severity: "error"
|
|
2635
|
+
});
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
return issues;
|
|
2639
|
+
}
|
|
2640
|
+
collectAgentVarRefs(node, diag, globalIds) {
|
|
2641
|
+
const byNode = new Map;
|
|
2642
|
+
const fieldSeen = new Set;
|
|
2643
|
+
const byField = [];
|
|
2644
|
+
const add = (nodeId, field, origin) => {
|
|
2645
|
+
if (globalIds.has(nodeId))
|
|
2646
|
+
return;
|
|
2647
|
+
if (!byNode.has(nodeId))
|
|
2648
|
+
byNode.set(nodeId, origin);
|
|
2649
|
+
if (field) {
|
|
2650
|
+
const fk = `${nodeId}\x00${field}`;
|
|
2651
|
+
if (!fieldSeen.has(fk)) {
|
|
2652
|
+
fieldSeen.add(fk);
|
|
2653
|
+
byField.push({ nodeId, field, origin });
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
};
|
|
2657
|
+
for (const leaf of diag?.declaredInputLeaves ?? []) {
|
|
2658
|
+
const ref = decodeSchemaKeyNodeRef(leaf.flatKey);
|
|
2659
|
+
if (ref) {
|
|
2660
|
+
add(ref.nodeId, ref.field, `\`$vars.${leaf.path}\` (agent.json input "${leaf.flatKey}")`);
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
2663
|
+
const bound = node.inputs?.agentInputVariables;
|
|
2664
|
+
if (Array.isArray(bound)) {
|
|
2665
|
+
for (const entry of bound) {
|
|
2666
|
+
if (!entry || typeof entry !== "object")
|
|
2667
|
+
continue;
|
|
2668
|
+
const binding = entry.binding;
|
|
2669
|
+
if (typeof binding !== "string")
|
|
2670
|
+
continue;
|
|
2671
|
+
const match = VARS_NODE_OUTPUT_RE.exec(binding);
|
|
2672
|
+
if (!match)
|
|
2673
|
+
continue;
|
|
2674
|
+
add(match[1], match[2], `\`${binding.replace(/^=/, "")}\` (agentInputVariables binding)`);
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
return { byNode, byField };
|
|
2678
|
+
}
|
|
2679
|
+
collectUpstreamNodeIds(incoming, targetId) {
|
|
2680
|
+
const upstream = new Set;
|
|
2681
|
+
const queue = [targetId];
|
|
2682
|
+
while (queue.length > 0) {
|
|
2683
|
+
const current = queue.pop();
|
|
2684
|
+
for (const source of incoming.get(current) ?? []) {
|
|
2685
|
+
if (upstream.has(source))
|
|
2686
|
+
continue;
|
|
2687
|
+
upstream.add(source);
|
|
2688
|
+
queue.push(source);
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
return upstream;
|
|
2692
|
+
}
|
|
2693
|
+
canonicalInputKey(rawVar) {
|
|
2694
|
+
if (!rawVar.startsWith("input."))
|
|
2695
|
+
return;
|
|
2696
|
+
const key = rawVar.slice("input.".length);
|
|
2697
|
+
if (key.length === 0 || key !== key.trim())
|
|
2698
|
+
return;
|
|
2699
|
+
return key;
|
|
2700
|
+
}
|
|
2701
|
+
enrichInlineAgentPromptIssues(issues, diagnoses, flowDir) {
|
|
2702
|
+
if (diagnoses.size === 0)
|
|
2703
|
+
return;
|
|
2704
|
+
const promptFieldRe = /\bnodes\[([^\]]+)\]\.inputs\.(systemPrompt|userPrompt)/;
|
|
2705
|
+
for (const issue of issues) {
|
|
2706
|
+
if (!issue.message.includes("REQUIRED_FIELD"))
|
|
2707
|
+
continue;
|
|
2708
|
+
const m = promptFieldRe.exec(issue.path);
|
|
2709
|
+
if (!m)
|
|
2710
|
+
continue;
|
|
2711
|
+
const nodeId = m[1];
|
|
2712
|
+
const diag = diagnoses.get(nodeId);
|
|
2713
|
+
if (!diag)
|
|
2714
|
+
continue;
|
|
2715
|
+
const hint = formatHydrationHint(diag, flowDir);
|
|
2716
|
+
issue.message = `${issue.message}
|
|
2717
|
+
Hint: ${hint}`;
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2720
|
+
validateGraph(graph, pathPrefix, rawDefErrorHandling = new Map, currentManifests = new Map, parentVariableIds = []) {
|
|
2721
|
+
const issues = [];
|
|
2722
|
+
issues.push(...this.validateEdgeReferences(graph));
|
|
2723
|
+
issues.push(...this.validateNodeTypeDefinitions(graph));
|
|
2724
|
+
issues.push(...this.validateUniqueIds(graph), ...this.validateExclusiveBranchJoins(graph), ...validateVarsReferences(graph, {
|
|
2725
|
+
extraKnownRoots: parentVariableIds
|
|
2726
|
+
}));
|
|
2727
|
+
issues.push(...this.validateEdgeHandles(graph, rawDefErrorHandling));
|
|
2728
|
+
issues.push(...this.validateEdgeHandlesAgainstCurrentManifests(graph, currentManifests));
|
|
2729
|
+
const ctx = buildValidationContext(graph);
|
|
2730
|
+
if (this.governanceEnforcements) {
|
|
2731
|
+
ctx.governanceEnforcements = this.governanceEnforcements;
|
|
2732
|
+
}
|
|
2733
|
+
if (this.availableModelNames) {
|
|
2734
|
+
ctx.availableModelNames = this.availableModelNames;
|
|
2735
|
+
}
|
|
2736
|
+
const dropped = new Set(["expression-ref"]);
|
|
2737
|
+
if (pathPrefix)
|
|
2738
|
+
dropped.add("trigger-required");
|
|
2739
|
+
const rules = cliRules.filter((r) => !dropped.has(r.id));
|
|
2740
|
+
const ruleErrors = runRules([
|
|
2741
|
+
...rules,
|
|
2742
|
+
conditionExpressionRule,
|
|
2743
|
+
...this.extraRules
|
|
2744
|
+
], ctx);
|
|
2745
|
+
issues.push(...ruleErrors.map(validationErrorToIssue));
|
|
2746
|
+
issues.push(...this.runNodeValidators(graph));
|
|
2747
|
+
return pathPrefix ? issues.map((i) => ({ ...i, path: `${pathPrefix}${i.path}` })) : issues;
|
|
2748
|
+
}
|
|
2749
|
+
validateExclusiveBranchJoins(flow) {
|
|
2750
|
+
const graph = indexJoinGraph(flow);
|
|
2751
|
+
const issues = [];
|
|
2752
|
+
for (const node of flow.nodes) {
|
|
2753
|
+
const finding = findUnsatisfiableJoin(node, graph);
|
|
2754
|
+
if (finding) {
|
|
2755
|
+
issues.push(describeUnsatisfiableJoin(node, finding, graph));
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2758
|
+
return issues;
|
|
2759
|
+
}
|
|
2760
|
+
validateEdgeReferences(flow) {
|
|
2761
|
+
const issues = [];
|
|
2762
|
+
const nodeIds = new Set(flow.nodes.map((n) => n.id));
|
|
2763
|
+
for (const edge of flow.edges) {
|
|
2764
|
+
if (!nodeIds.has(edge.sourceNodeId)) {
|
|
2765
|
+
issues.push({
|
|
2766
|
+
path: `edges[${edge.id}].sourceNodeId`,
|
|
2767
|
+
message: `Edge references non-existent source node: "${edge.sourceNodeId}"`,
|
|
2768
|
+
severity: "error"
|
|
2769
|
+
});
|
|
2770
|
+
}
|
|
2771
|
+
if (!nodeIds.has(edge.targetNodeId)) {
|
|
2772
|
+
issues.push({
|
|
2773
|
+
path: `edges[${edge.id}].targetNodeId`,
|
|
2774
|
+
message: `Edge references non-existent target node: "${edge.targetNodeId}"`,
|
|
2775
|
+
severity: "error"
|
|
2776
|
+
});
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
return issues;
|
|
2780
|
+
}
|
|
2781
|
+
validateNodeTypeDefinitions(flow) {
|
|
2782
|
+
const issues = [];
|
|
2783
|
+
const definedTypes = new Set(flow.definitions.map((d) => `${d.nodeType}:${d.version}`));
|
|
2784
|
+
const definedVersionsByType = new Map;
|
|
2785
|
+
for (const d of flow.definitions) {
|
|
2786
|
+
const versions = definedVersionsByType.get(d.nodeType) ?? [];
|
|
2787
|
+
versions.push(d.version);
|
|
2788
|
+
definedVersionsByType.set(d.nodeType, versions);
|
|
2789
|
+
}
|
|
2790
|
+
for (const node of flow.nodes) {
|
|
2791
|
+
if (UI_ONLY_NODE_TYPES.has(node.type)) {
|
|
2792
|
+
continue;
|
|
2793
|
+
}
|
|
2794
|
+
const key = `${node.type}:${node.typeVersion}`;
|
|
2795
|
+
if (!definedTypes.has(key)) {
|
|
2796
|
+
const sameTypeVersions = definedVersionsByType.get(node.type);
|
|
2797
|
+
const hint = sameTypeVersions?.length ? `
|
|
2798
|
+
Hint: \`definitions[]\` has \`${node.type}\` at version(s) ${sameTypeVersions.map((v) => `"${v}"`).join(", ")} but the node uses \`typeVersion: "${node.typeVersion}"\`. Align one side — \`typeVersion\` must match the definition's \`version\` exactly (e.g., \`"1.0"\` ≠ \`"1.0.0"\`).` : `
|
|
2799
|
+
Hint: run \`uip maestro flow registry get ${node.type} --output json\` and append the returned node object to \`definitions[]\` (depending on plugin version, the object is at \`Data.Node\` or the top level). Set \`typeVersion\` to the returned \`version\` exactly — registry may emit \`"1.0"\` while older flows used \`"1.0.0"\`. To scaffold a new node with its definition auto-injected, run \`uip maestro flow node add <file> ${node.type}\` instead.`;
|
|
2800
|
+
issues.push({
|
|
2801
|
+
path: `nodes[${node.id}].type`,
|
|
2802
|
+
message: `Node type "${key}" has no matching definition.${hint}`,
|
|
2803
|
+
severity: "error"
|
|
2804
|
+
});
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2807
|
+
return issues;
|
|
2808
|
+
}
|
|
2809
|
+
validateUniqueIds(flow) {
|
|
2810
|
+
const issues = [];
|
|
2811
|
+
const nodeIds = new Map;
|
|
2812
|
+
for (const node of flow.nodes) {
|
|
2813
|
+
nodeIds.set(node.id, (nodeIds.get(node.id) ?? 0) + 1);
|
|
2814
|
+
}
|
|
2815
|
+
for (const [id, count] of nodeIds) {
|
|
2816
|
+
if (count > 1) {
|
|
2817
|
+
issues.push({
|
|
2818
|
+
path: `nodes`,
|
|
2819
|
+
message: `Duplicate node ID: "${id}" (appears ${count} times)`,
|
|
2820
|
+
severity: "error"
|
|
2821
|
+
});
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2824
|
+
const edgeIds = new Map;
|
|
2825
|
+
for (const edge of flow.edges) {
|
|
2826
|
+
edgeIds.set(edge.id, (edgeIds.get(edge.id) ?? 0) + 1);
|
|
2827
|
+
}
|
|
2828
|
+
for (const [id, count] of edgeIds) {
|
|
2829
|
+
if (count > 1) {
|
|
2830
|
+
issues.push({
|
|
2831
|
+
path: `edges`,
|
|
2832
|
+
message: `Duplicate edge ID: "${id}" (appears ${count} times)`,
|
|
2833
|
+
severity: "error"
|
|
2834
|
+
});
|
|
2835
|
+
}
|
|
2836
|
+
}
|
|
2837
|
+
return issues;
|
|
2838
|
+
}
|
|
2839
|
+
validateGlobalVariableUniqueness(flow, pathPrefix) {
|
|
2840
|
+
const issues = [];
|
|
2841
|
+
const globals = flow.variables?.globals ?? [];
|
|
2842
|
+
const varCounts = new Map;
|
|
2843
|
+
for (const v of globals) {
|
|
2844
|
+
if (!v.id)
|
|
2845
|
+
continue;
|
|
2846
|
+
varCounts.set(v.id, (varCounts.get(v.id) ?? 0) + 1);
|
|
2847
|
+
}
|
|
2848
|
+
for (const [id, count] of varCounts) {
|
|
2849
|
+
if (count > 1) {
|
|
2850
|
+
issues.push({
|
|
2851
|
+
path: `${pathPrefix}variables.globals`,
|
|
2852
|
+
message: `Duplicate variable ID: "${id}" (appears ${count} times)`,
|
|
2853
|
+
severity: "error"
|
|
2854
|
+
});
|
|
2855
|
+
}
|
|
2856
|
+
}
|
|
2857
|
+
return issues;
|
|
2858
|
+
}
|
|
2859
|
+
validateGlobalTriggerBindings(flow, pathPrefix) {
|
|
2860
|
+
const issues = [];
|
|
2861
|
+
const nodes = flow.nodes ?? [];
|
|
2862
|
+
const globals = flow.variables?.globals ?? [];
|
|
2863
|
+
const inGlobals = new Map;
|
|
2864
|
+
for (const g of globals) {
|
|
2865
|
+
if (g.id && g.direction === "in") {
|
|
2866
|
+
inGlobals.set(g.id, g.triggerNodeId);
|
|
2867
|
+
}
|
|
2868
|
+
}
|
|
2869
|
+
if (inGlobals.size === 0)
|
|
2870
|
+
return issues;
|
|
2871
|
+
const triggerNodeIds = new Set(nodes.filter((n) => typeof n.type === "string" && n.type.startsWith("core.trigger.")).map((n) => n.id));
|
|
2872
|
+
if (triggerNodeIds.size === 0)
|
|
2873
|
+
return issues;
|
|
2874
|
+
const expressions = collectNodeExpressionStrings(nodes);
|
|
2875
|
+
const refRe = /\$vars\.([A-Za-z0-9_$]+)\.output\.([A-Za-z0-9_$]+)/g;
|
|
2876
|
+
const reported = new Set;
|
|
2877
|
+
for (const expr of expressions) {
|
|
2878
|
+
for (const match of expr.matchAll(refRe)) {
|
|
2879
|
+
const refNode = match[1];
|
|
2880
|
+
const refField = match[2];
|
|
2881
|
+
if (!triggerNodeIds.has(refNode))
|
|
2882
|
+
continue;
|
|
2883
|
+
if (!inGlobals.has(refField))
|
|
2884
|
+
continue;
|
|
2885
|
+
const boundTo = inGlobals.get(refField);
|
|
2886
|
+
if (boundTo === refNode)
|
|
2887
|
+
continue;
|
|
2888
|
+
const key = `${refField}->${refNode}`;
|
|
2889
|
+
if (reported.has(key))
|
|
2890
|
+
continue;
|
|
2891
|
+
reported.add(key);
|
|
2892
|
+
const detail = boundTo ? `its "triggerNodeId" is "${boundTo}", not "${refNode}"` : `it has no "triggerNodeId"`;
|
|
2893
|
+
issues.push({
|
|
2894
|
+
path: `${pathPrefix}variables.globals[${refField}].triggerNodeId`,
|
|
2895
|
+
message: `Input variable "${refField}" is read via \`$vars.${refNode}.output.${refField}\`, but ${detail}. At runtime the trigger "${refNode}" output will not contain "${refField}", so the value resolves to null (a downstream node may fault, e.g. "Cannot read property '…' of null"). Set "triggerNodeId": "${refNode}" on the "${refField}" global.`,
|
|
2896
|
+
severity: "warning"
|
|
2897
|
+
});
|
|
2898
|
+
}
|
|
2899
|
+
}
|
|
2900
|
+
return issues;
|
|
2901
|
+
}
|
|
2902
|
+
validateTriggerScopedBareVarReads(flow, pathPrefix) {
|
|
2903
|
+
const issues = [];
|
|
2904
|
+
const nodes = flow.nodes ?? [];
|
|
2905
|
+
const globals = flow.variables?.globals ?? [];
|
|
2906
|
+
const triggerNodeIds = new Set(nodes.filter((n) => isStartEventNode({
|
|
2907
|
+
id: n.id,
|
|
2908
|
+
type: n.type,
|
|
2909
|
+
typeVersion: n.typeVersion ?? "1.0"
|
|
2910
|
+
})).map((n) => n.id));
|
|
2911
|
+
if (triggerNodeIds.size === 0)
|
|
2912
|
+
return issues;
|
|
2913
|
+
const boundInGlobals = new Map;
|
|
2914
|
+
for (const g of globals) {
|
|
2915
|
+
if (g.id && g.direction === "in" && g.triggerNodeId && triggerNodeIds.has(g.triggerNodeId)) {
|
|
2916
|
+
boundInGlobals.set(g.id, g.triggerNodeId);
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2919
|
+
if (boundInGlobals.size === 0)
|
|
2920
|
+
return issues;
|
|
2921
|
+
const nodeIds = new Set(nodes.map((n) => n.id));
|
|
2922
|
+
const updatesByNode = flow.variables?.variableUpdates ?? {};
|
|
2923
|
+
const reported = new Set;
|
|
2924
|
+
for (const { ref } of collectVarsRefs(nodes, updatesByNode)) {
|
|
2925
|
+
if (ref.inProse)
|
|
2926
|
+
continue;
|
|
2927
|
+
const root = ref.segments[0];
|
|
2928
|
+
if (nodeIds.has(root))
|
|
2929
|
+
continue;
|
|
2930
|
+
const trigger = boundInGlobals.get(root);
|
|
2931
|
+
if (!trigger)
|
|
2932
|
+
continue;
|
|
2933
|
+
if (reported.has(root))
|
|
2934
|
+
continue;
|
|
2935
|
+
reported.add(root);
|
|
2936
|
+
issues.push({
|
|
2937
|
+
path: `${pathPrefix}variables.globals[${root}]`,
|
|
2938
|
+
message: `Input variable "${root}" is trigger-scoped (its "triggerNodeId" is "${trigger}"), but it is read as \`${ref.text}\`. At runtime that value is surfaced only under the trigger output, so the read resolves to null (it may look fine with a manual trigger, which also emits a process-level input, then fault or use a wrong value under a connector or scheduled trigger). Read it as \`$vars.${trigger}.output.${root}\`.`,
|
|
2939
|
+
severity: "error"
|
|
2940
|
+
});
|
|
2941
|
+
}
|
|
2942
|
+
return issues;
|
|
2943
|
+
}
|
|
2944
|
+
validateUnboundInputGlobals(flow, pathPrefix) {
|
|
2945
|
+
const issues = [];
|
|
2946
|
+
const nodes = flow.nodes ?? [];
|
|
2947
|
+
const inGlobals = (flow.variables?.globals ?? []).filter((g) => g.id && g.direction === "in");
|
|
2948
|
+
if (inGlobals.length === 0)
|
|
2949
|
+
return issues;
|
|
2950
|
+
const triggerNodes = nodes.filter((n) => isStartEventNode({
|
|
2951
|
+
id: n.id,
|
|
2952
|
+
type: n.type,
|
|
2953
|
+
typeVersion: n.typeVersion ?? "1.0"
|
|
2954
|
+
}));
|
|
2955
|
+
if (triggerNodes.length === 0)
|
|
2956
|
+
return issues;
|
|
2957
|
+
const definitions = flow.definitions ?? [];
|
|
2958
|
+
const manifestInputIds = (node) => {
|
|
2959
|
+
const def = definitions.find((d) => d.nodeType === node.type && d.version === node.typeVersion) ?? definitions.find((d) => d.nodeType === node.type);
|
|
2960
|
+
const props = def?.outputDefinition?.output?.schema?.properties;
|
|
2961
|
+
return props && typeof props === "object" ? Object.keys(props) : [];
|
|
2962
|
+
};
|
|
2963
|
+
const manifestInputs = new Set;
|
|
2964
|
+
const globalsDrivenTriggers = [];
|
|
2965
|
+
for (const t of triggerNodes) {
|
|
2966
|
+
const ids = manifestInputIds(t);
|
|
2967
|
+
if (ids.length > 0) {
|
|
2968
|
+
for (const id of ids)
|
|
2969
|
+
manifestInputs.add(id);
|
|
2970
|
+
} else {
|
|
2971
|
+
globalsDrivenTriggers.push(t.id);
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2974
|
+
if (globalsDrivenTriggers.length === 0)
|
|
2975
|
+
return issues;
|
|
2976
|
+
const triggerIds = new Set(triggerNodes.map((n) => n.id));
|
|
2977
|
+
const readViaTrigger = new Set;
|
|
2978
|
+
const refRe = /\$vars\.([A-Za-z0-9_$]+)\.output\.([A-Za-z0-9_$]+)/g;
|
|
2979
|
+
for (const expr of collectNodeExpressionStrings(nodes)) {
|
|
2980
|
+
for (const match of expr.matchAll(refRe)) {
|
|
2981
|
+
if (triggerIds.has(match[1]))
|
|
2982
|
+
readViaTrigger.add(match[2]);
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
const soleTrigger = globalsDrivenTriggers.length === 1 ? globalsDrivenTriggers[0] : undefined;
|
|
2986
|
+
for (const g of inGlobals) {
|
|
2987
|
+
if (g.triggerNodeId && triggerIds.has(g.triggerNodeId))
|
|
2988
|
+
continue;
|
|
2989
|
+
if (readViaTrigger.has(g.id))
|
|
2990
|
+
continue;
|
|
2991
|
+
if (manifestInputs.has(g.id))
|
|
2992
|
+
continue;
|
|
2993
|
+
const detail = g.triggerNodeId ? `its "triggerNodeId" is "${g.triggerNodeId}", which is not a trigger node in this flow` : `it has no "triggerNodeId"`;
|
|
2994
|
+
const fix = soleTrigger ? `Set "triggerNodeId": "${soleTrigger}" on the "${g.id}" global.` : `Set "triggerNodeId" on the "${g.id}" global to the trigger that supplies it (${globalsDrivenTriggers.join(", ")}).`;
|
|
2995
|
+
issues.push({
|
|
2996
|
+
path: `${pathPrefix}variables.globals[${g.id}].triggerNodeId`,
|
|
2997
|
+
message: `Input variable "${g.id}" is omitted from the packed entry point's \`input.properties\` because ${detail}. A deployed process or trigger then has no way to pass "${g.id}" — while \`flow validate\`, \`flow format\`, and \`flow debug --inputs\` all stay green. ${fix}`,
|
|
2998
|
+
severity: "warning"
|
|
2999
|
+
});
|
|
3000
|
+
}
|
|
3001
|
+
return issues;
|
|
3002
|
+
}
|
|
3003
|
+
validateResourceBindings(workflow) {
|
|
3004
|
+
const issues = [];
|
|
3005
|
+
const bindings = workflow.bindings ?? [];
|
|
3006
|
+
const declaredKeys = new Set(bindings.map((b) => b.resourceKey));
|
|
3007
|
+
const definitions = workflow.definitions;
|
|
3008
|
+
const subflows = workflow.subflows ?? {};
|
|
3009
|
+
const allNodes = [
|
|
3010
|
+
...workflow.nodes.map((n) => ({
|
|
3011
|
+
id: n.id,
|
|
3012
|
+
type: n.type,
|
|
3013
|
+
typeVersion: n.typeVersion,
|
|
3014
|
+
inputs: n.inputs,
|
|
3015
|
+
scope: ""
|
|
3016
|
+
})),
|
|
3017
|
+
...Object.entries(subflows).flatMap(([subflowId, sub]) => (sub.nodes ?? []).map((n) => ({
|
|
3018
|
+
id: n.id,
|
|
3019
|
+
type: n.type,
|
|
3020
|
+
typeVersion: n.typeVersion,
|
|
3021
|
+
inputs: n.inputs,
|
|
3022
|
+
scope: `subflows[${subflowId}].`
|
|
3023
|
+
})))
|
|
3024
|
+
];
|
|
3025
|
+
for (const node of allNodes) {
|
|
3026
|
+
if (!isProcessNode({ nodeType: node.type }))
|
|
3027
|
+
continue;
|
|
3028
|
+
const needed = collectNodeBindingResourceKeys(node, bindings, definitions);
|
|
3029
|
+
for (const key of needed) {
|
|
3030
|
+
if (declaredKeys.has(key))
|
|
3031
|
+
continue;
|
|
3032
|
+
issues.push({
|
|
3033
|
+
path: `${node.scope}nodes[${node.id}].bindings`,
|
|
3034
|
+
message: `Resource node "${node.id}" (type ${node.type}) references binding key "${key}" that is not declared in workflow.bindings. Add the binding entry; otherwise the runtime fails as "Failure in the Orchestrator Job".`,
|
|
3035
|
+
severity: "error"
|
|
3036
|
+
});
|
|
3037
|
+
}
|
|
3038
|
+
}
|
|
3039
|
+
return issues;
|
|
3040
|
+
}
|
|
3041
|
+
validateConnectionIdReuse(workflow) {
|
|
3042
|
+
const issues = [];
|
|
3043
|
+
const subflows = workflow.subflows ?? {};
|
|
3044
|
+
const buckets = new Map;
|
|
3045
|
+
function recordNode(node, scope) {
|
|
3046
|
+
const detail = node.inputs?.detail;
|
|
3047
|
+
const connId = detail?.connectionResourceId;
|
|
3048
|
+
if (typeof connId !== "string" || connId.length === 0)
|
|
3049
|
+
return;
|
|
3050
|
+
let list = buckets.get(connId);
|
|
3051
|
+
if (!list) {
|
|
3052
|
+
list = [];
|
|
3053
|
+
buckets.set(connId, list);
|
|
3054
|
+
}
|
|
3055
|
+
list.push({ nodeId: node.id, scope });
|
|
3056
|
+
}
|
|
3057
|
+
for (const node of workflow.nodes) {
|
|
3058
|
+
recordNode(node, "");
|
|
3059
|
+
}
|
|
3060
|
+
for (const [subflowId, sub] of Object.entries(subflows)) {
|
|
3061
|
+
for (const node of sub.nodes ?? []) {
|
|
3062
|
+
recordNode(node, `subflows[${subflowId}].`);
|
|
3063
|
+
}
|
|
3064
|
+
}
|
|
3065
|
+
for (const [connId, occurrences] of buckets) {
|
|
3066
|
+
if (occurrences.length < 2)
|
|
3067
|
+
continue;
|
|
3068
|
+
const nodeList = occurrences.map((o) => `${o.scope}nodes[${o.nodeId}]`).join(", ");
|
|
3069
|
+
const first = occurrences[0];
|
|
3070
|
+
issues.push({
|
|
3071
|
+
path: `${first.scope}nodes[${first.nodeId}].inputs.detail.connectionResourceId`,
|
|
3072
|
+
severity: "warning",
|
|
3073
|
+
message: `Connection "${connId}" is referenced by ${occurrences.length} nodes (${nodeList}). Shared connections cause cross-node state leakage at runtime. If this is intentional (e.g. two connectors on the same SaaS account), ignore this warning; otherwise create a separate connection per node.`
|
|
3074
|
+
});
|
|
3075
|
+
}
|
|
3076
|
+
return issues;
|
|
3077
|
+
}
|
|
3078
|
+
validateDuplicateProcessTargets(workflow) {
|
|
3079
|
+
const issues = [];
|
|
3080
|
+
const subflows = workflow.subflows ?? {};
|
|
3081
|
+
const definitions = workflow.definitions;
|
|
3082
|
+
const buckets = new Map;
|
|
3083
|
+
const recordNode = (node, scope) => {
|
|
3084
|
+
const target = resolveProcessTarget(node, definitions);
|
|
3085
|
+
if (!target)
|
|
3086
|
+
return;
|
|
3087
|
+
const bucketKey = `${target.subType}\x00${target.key}`;
|
|
3088
|
+
let list = buckets.get(bucketKey);
|
|
3089
|
+
if (!list) {
|
|
3090
|
+
list = [];
|
|
3091
|
+
buckets.set(bucketKey, list);
|
|
3092
|
+
}
|
|
3093
|
+
list.push({
|
|
3094
|
+
nodeId: node.id,
|
|
3095
|
+
label: node.display?.label ?? "",
|
|
3096
|
+
scope,
|
|
3097
|
+
targetKey: target.key
|
|
3098
|
+
});
|
|
3099
|
+
};
|
|
3100
|
+
for (const node of workflow.nodes) {
|
|
3101
|
+
recordNode(node, "");
|
|
3102
|
+
}
|
|
3103
|
+
for (const [subflowId, sub] of Object.entries(subflows)) {
|
|
3104
|
+
for (const node of sub.nodes ?? []) {
|
|
3105
|
+
recordNode(node, `subflows[${subflowId}].`);
|
|
3106
|
+
}
|
|
3107
|
+
}
|
|
3108
|
+
for (const occurrences of buckets.values()) {
|
|
3109
|
+
if (occurrences.length < 2)
|
|
3110
|
+
continue;
|
|
3111
|
+
const distinctLabels = new Set(occurrences.map((o) => o.label));
|
|
3112
|
+
if (distinctLabels.size < 2)
|
|
3113
|
+
continue;
|
|
3114
|
+
const nodeList = occurrences.map((o) => `${o.scope}nodes[${o.nodeId}] ("${o.label}")`).join(", ");
|
|
3115
|
+
const first = occurrences[0];
|
|
3116
|
+
issues.push({
|
|
3117
|
+
path: `${first.scope}nodes[${first.nodeId}]`,
|
|
3118
|
+
severity: "warning",
|
|
3119
|
+
message: `Process target "${first.targetKey}" is invoked by ${occurrences.length} differently-named nodes (${nodeList}). Two differently-named nodes resolving to the same process/function usually means a node was duplicated without re-pointing its target — verify each node calls the intended process. If this reuse is intentional, ignore this warning.`
|
|
3120
|
+
});
|
|
3121
|
+
}
|
|
3122
|
+
return issues;
|
|
3123
|
+
}
|
|
3124
|
+
async validateConnectorBindingShapes(workflow) {
|
|
3125
|
+
const catalog = await this.loadConnectorCatalogCached();
|
|
3126
|
+
return validateConnectorBindings(workflow, catalog, {
|
|
3127
|
+
strict: this.strictBindings
|
|
3128
|
+
});
|
|
3129
|
+
}
|
|
3130
|
+
async loadConnectorCatalogCached() {
|
|
3131
|
+
if (this.connectorCatalogOverride) {
|
|
3132
|
+
return this.connectorCatalogOverride;
|
|
3133
|
+
}
|
|
3134
|
+
this.connectorCatalogCache ??= loadConnectorCatalog();
|
|
3135
|
+
return this.connectorCatalogCache;
|
|
3136
|
+
}
|
|
3137
|
+
async validateSolutionRegistrationAndLayout(absolutePath) {
|
|
3138
|
+
const issues = [];
|
|
3139
|
+
const flowDir = this.fs.path.dirname(absolutePath);
|
|
3140
|
+
const discovery = await findNearestParentUipxFile(this.fs, absolutePath);
|
|
3141
|
+
if (discovery.status !== "found") {
|
|
3142
|
+
return issues;
|
|
3143
|
+
}
|
|
3144
|
+
const [readErr, manifest] = await readSolutionManifest(this.fs, discovery.solutionFile);
|
|
3145
|
+
if (readErr || !manifest) {
|
|
3146
|
+
return issues;
|
|
3147
|
+
}
|
|
3148
|
+
const solutionDir = this.fs.path.dirname(discovery.solutionFile);
|
|
3149
|
+
const flowDirReal = await this.canonicalizePath(flowDir);
|
|
3150
|
+
let isRegistered = false;
|
|
3151
|
+
for (const p of manifest.projects) {
|
|
3152
|
+
const projectDir = this.fs.path.resolve(solutionDir, this.fs.path.dirname(p.ProjectRelativePath));
|
|
3153
|
+
const projectDirReal = await this.canonicalizePath(projectDir);
|
|
3154
|
+
if (projectDirReal === flowDirReal) {
|
|
3155
|
+
isRegistered = true;
|
|
3156
|
+
break;
|
|
3157
|
+
}
|
|
3158
|
+
}
|
|
3159
|
+
if (!isRegistered) {
|
|
3160
|
+
issues.push({
|
|
3161
|
+
path: "(workflow)",
|
|
3162
|
+
severity: "warning",
|
|
3163
|
+
message: `.flow file at ${absolutePath} is not registered as a project of solution ${discovery.solutionFile}. Run \`uip solution projects add "${flowDir}"\` so Studio Web and \`flow debug\` can discover it.`
|
|
3164
|
+
});
|
|
3165
|
+
}
|
|
3166
|
+
const flowBase = this.fs.path.basename(absolutePath, ".flow");
|
|
3167
|
+
const dirBase = this.fs.path.basename(flowDir);
|
|
3168
|
+
if (flowBase !== dirBase) {
|
|
3169
|
+
issues.push({
|
|
3170
|
+
path: "(workflow)",
|
|
3171
|
+
severity: "warning",
|
|
3172
|
+
message: `.flow file at ${absolutePath} does not match canonical layout. Expected the .flow basename ("${flowBase}") to match its parent directory name ("${dirBase}"). The canonical layout is <solution>/<projectName>/<projectName>.flow.`
|
|
3173
|
+
});
|
|
3174
|
+
}
|
|
3175
|
+
return issues;
|
|
3176
|
+
}
|
|
3177
|
+
async canonicalizePath(p) {
|
|
3178
|
+
const [err, real] = await catchError(this.fs.realpath(p));
|
|
3179
|
+
return err || !real ? p : real;
|
|
3180
|
+
}
|
|
3181
|
+
validateEdgeHandlesAgainstCurrentManifests(flow, currentManifests) {
|
|
3182
|
+
const issues = [];
|
|
3183
|
+
if (currentManifests.size === 0)
|
|
3184
|
+
return issues;
|
|
3185
|
+
const nodeById = new Map(flow.nodes.map((n) => [n.id, n]));
|
|
3186
|
+
const handlesByNode = new Map;
|
|
3187
|
+
for (const node of flow.nodes) {
|
|
3188
|
+
const currentManifest = currentManifests.get(node.type.toLowerCase());
|
|
3189
|
+
if (!currentManifest)
|
|
3190
|
+
continue;
|
|
3191
|
+
if (!manifestVersionMatchesNode(node.typeVersion, currentManifest.version)) {
|
|
3192
|
+
continue;
|
|
3193
|
+
}
|
|
3194
|
+
const entry = {
|
|
3195
|
+
source: new Map,
|
|
3196
|
+
target: new Map,
|
|
3197
|
+
sourceTemplates: [],
|
|
3198
|
+
targetTemplates: [],
|
|
3199
|
+
sourceDeclared: false
|
|
3200
|
+
};
|
|
3201
|
+
const handleConfiguration = currentManifest.handleConfiguration ?? [];
|
|
3202
|
+
for (const group of handleConfiguration) {
|
|
3203
|
+
for (const handle of group.handles ?? []) {
|
|
3204
|
+
if (handle.type !== "source" && handle.type !== "target") {
|
|
3205
|
+
continue;
|
|
3206
|
+
}
|
|
3207
|
+
const concreteTarget = handle.type === "source" ? entry.source : entry.target;
|
|
3208
|
+
const templateTarget = handle.type === "source" ? entry.sourceTemplates : entry.targetTemplates;
|
|
3209
|
+
for (const id of expandHandleIds(handle, node)) {
|
|
3210
|
+
if (isTemplatedHandleId(id)) {
|
|
3211
|
+
templateTarget.push({
|
|
3212
|
+
templateId: id,
|
|
3213
|
+
position: group.position
|
|
3214
|
+
});
|
|
3215
|
+
} else {
|
|
3216
|
+
concreteTarget.set(id, {
|
|
3217
|
+
position: group.position
|
|
3218
|
+
});
|
|
3219
|
+
}
|
|
3220
|
+
}
|
|
3221
|
+
}
|
|
3222
|
+
}
|
|
3223
|
+
if (currentManifest.supportsErrorHandling === true && !entry.source.has("error")) {
|
|
3224
|
+
entry.source.set("error", {});
|
|
3225
|
+
}
|
|
3226
|
+
const sourceSetDeclared = [...entry.source.values()].some((info) => info.position !== undefined) || entry.sourceTemplates.length > 0;
|
|
3227
|
+
entry.sourceDeclared = sourceSetDeclared;
|
|
3228
|
+
const customization = deriveHandleCustomizationSourceOverride(node);
|
|
3229
|
+
if (customization && sourceSetDeclared) {
|
|
3230
|
+
const overriddenPositions = new Set(customization.positions);
|
|
3231
|
+
for (const [handleId, info] of [...entry.source]) {
|
|
3232
|
+
if (info.position !== undefined && overriddenPositions.has(info.position)) {
|
|
3233
|
+
entry.source.delete(handleId);
|
|
3234
|
+
}
|
|
3235
|
+
}
|
|
3236
|
+
entry.sourceTemplates = entry.sourceTemplates.filter((template) => template.position === undefined || !overriddenPositions.has(template.position));
|
|
3237
|
+
for (const handleId of customization.sourceHandleIds) {
|
|
3238
|
+
entry.source.set(handleId, {});
|
|
3239
|
+
}
|
|
3240
|
+
}
|
|
3241
|
+
handlesByNode.set(node.id, entry);
|
|
3242
|
+
}
|
|
3243
|
+
const resolveHandle = (nodeHandles, side, port) => {
|
|
3244
|
+
if (!nodeHandles)
|
|
3245
|
+
return true;
|
|
3246
|
+
const concrete = nodeHandles[side];
|
|
3247
|
+
if (concrete.has(port))
|
|
3248
|
+
return true;
|
|
3249
|
+
const templates = side === "source" ? nodeHandles.sourceTemplates : nodeHandles.targetTemplates;
|
|
3250
|
+
return templates.some((t) => portMatchesTemplate(port, t.templateId));
|
|
3251
|
+
};
|
|
3252
|
+
const hasAnyHandles = (nodeHandles, side) => {
|
|
3253
|
+
if (!nodeHandles)
|
|
3254
|
+
return false;
|
|
3255
|
+
if (side === "source")
|
|
3256
|
+
return nodeHandles.sourceDeclared;
|
|
3257
|
+
return nodeHandles.target.size + nodeHandles.targetTemplates.length > 0;
|
|
3258
|
+
};
|
|
3259
|
+
const availablePorts = (nodeHandles, side) => nodeHandles ? formatHandleList(nodeHandles[side], side === "source" ? nodeHandles.sourceTemplates : nodeHandles.targetTemplates) : "(none)";
|
|
3260
|
+
for (const edge of flow.edges) {
|
|
3261
|
+
const sourceNode = nodeById.get(edge.sourceNodeId);
|
|
3262
|
+
const sourceHandles = handlesByNode.get(edge.sourceNodeId);
|
|
3263
|
+
if (sourceNode && hasAnyHandles(sourceHandles, "source") && !resolveHandle(sourceHandles, "source", edge.sourcePort)) {
|
|
3264
|
+
issues.push({
|
|
3265
|
+
path: `edges[${edge.id}].sourcePort`,
|
|
3266
|
+
message: `Edge references source handle "${edge.sourcePort}" on node "${edge.sourceNodeId}", but the current manifest for "${sourceNode.type}" does not declare that handle. The .flow definition appears stale; refresh the node definition from the registry and rewire to one of: ${availablePorts(sourceHandles, "source")}`,
|
|
3267
|
+
severity: "error"
|
|
3268
|
+
});
|
|
3269
|
+
}
|
|
3270
|
+
const targetNode = nodeById.get(edge.targetNodeId);
|
|
3271
|
+
const targetHandles = handlesByNode.get(edge.targetNodeId);
|
|
3272
|
+
if (targetNode && hasAnyHandles(targetHandles, "target") && !resolveHandle(targetHandles, "target", edge.targetPort)) {
|
|
3273
|
+
issues.push({
|
|
3274
|
+
path: `edges[${edge.id}].targetPort`,
|
|
3275
|
+
message: `Edge references target handle "${edge.targetPort}" on node "${edge.targetNodeId}", but the current manifest for "${targetNode.type}" does not declare that handle. The .flow definition appears stale; refresh the node definition from the registry and rewire to one of: ${availablePorts(targetHandles, "target")}`,
|
|
3276
|
+
severity: "error"
|
|
3277
|
+
});
|
|
3278
|
+
}
|
|
3279
|
+
}
|
|
3280
|
+
return issues;
|
|
3281
|
+
}
|
|
3282
|
+
validateEdgeHandles(flow, rawDefErrorHandling = new Map) {
|
|
3283
|
+
const issues = [];
|
|
3284
|
+
const defByKey = new Map;
|
|
3285
|
+
for (const def of flow.definitions) {
|
|
3286
|
+
const key = `${def.nodeType}:${def.version}`;
|
|
3287
|
+
defByKey.set(key, {
|
|
3288
|
+
handleConfiguration: def.handleConfiguration ?? [],
|
|
3289
|
+
supportsErrorHandling: rawDefErrorHandling.get(key) === true
|
|
3290
|
+
});
|
|
3291
|
+
}
|
|
3292
|
+
const nodeById = new Map(flow.nodes.map((node) => [node.id, node]));
|
|
3293
|
+
const handlesByNode = new Map;
|
|
3294
|
+
const handleCustomizedNodes = new Map;
|
|
3295
|
+
for (const node of flow.nodes) {
|
|
3296
|
+
const version = node.typeVersion ?? "1.0.0";
|
|
3297
|
+
const defEntry = defByKey.get(`${node.type}:${version}`);
|
|
3298
|
+
if (!defEntry)
|
|
3299
|
+
continue;
|
|
3300
|
+
const entry = {
|
|
3301
|
+
source: new Map,
|
|
3302
|
+
target: new Map,
|
|
3303
|
+
sourceTemplates: [],
|
|
3304
|
+
targetTemplates: [],
|
|
3305
|
+
sourceDeclared: false
|
|
3306
|
+
};
|
|
3307
|
+
for (const group of defEntry.handleConfiguration) {
|
|
3308
|
+
for (const handle of group.handles ?? []) {
|
|
3309
|
+
const info = {
|
|
3310
|
+
maxConnections: handle.constraints?.maxConnections,
|
|
3311
|
+
visible: handle.visible,
|
|
3312
|
+
position: group.position
|
|
3313
|
+
};
|
|
3314
|
+
const expandedIds = expandHandleIds(handle, node);
|
|
3315
|
+
if (handle.type !== "source" && handle.type !== "target") {
|
|
3316
|
+
continue;
|
|
3317
|
+
}
|
|
3318
|
+
const concreteTarget = handle.type === "source" ? entry.source : entry.target;
|
|
3319
|
+
const templateTarget = handle.type === "source" ? entry.sourceTemplates : entry.targetTemplates;
|
|
3320
|
+
for (const id of expandedIds) {
|
|
3321
|
+
if (isTemplatedHandleId(id)) {
|
|
3322
|
+
templateTarget.push({ templateId: id, info });
|
|
3323
|
+
} else {
|
|
3324
|
+
concreteTarget.set(id, info);
|
|
3325
|
+
}
|
|
3326
|
+
}
|
|
3327
|
+
}
|
|
3328
|
+
}
|
|
3329
|
+
if (defEntry.supportsErrorHandling && !entry.source.has("error")) {
|
|
3330
|
+
entry.source.set("error", {
|
|
3331
|
+
visible: buildErrorHandle().visible
|
|
3332
|
+
});
|
|
3333
|
+
}
|
|
3334
|
+
const sourceSetDeclared = [...entry.source.values()].some((info) => info.position !== undefined) || entry.sourceTemplates.length > 0;
|
|
3335
|
+
entry.sourceDeclared = sourceSetDeclared;
|
|
3336
|
+
const customization = deriveHandleCustomizationSourceOverride(node);
|
|
3337
|
+
if (sourceSetDeclared && !customization) {
|
|
3338
|
+
for (const outcomeHandleId of deriveOutcomeHandleIds(node)) {
|
|
3339
|
+
if (!entry.source.has(outcomeHandleId)) {
|
|
3340
|
+
entry.source.set(outcomeHandleId, {});
|
|
3341
|
+
}
|
|
3342
|
+
}
|
|
3343
|
+
}
|
|
3344
|
+
if (customization && sourceSetDeclared) {
|
|
3345
|
+
const overriddenPositions = new Set(customization.positions);
|
|
3346
|
+
for (const [handleId, info] of [...entry.source]) {
|
|
3347
|
+
if (info.position !== undefined && overriddenPositions.has(info.position)) {
|
|
3348
|
+
entry.source.delete(handleId);
|
|
3349
|
+
}
|
|
3350
|
+
}
|
|
3351
|
+
entry.sourceTemplates = entry.sourceTemplates.filter((template) => template.info.position === undefined || !overriddenPositions.has(template.info.position));
|
|
3352
|
+
for (const handleId of customization.sourceHandleIds) {
|
|
3353
|
+
entry.source.set(handleId, {});
|
|
3354
|
+
}
|
|
3355
|
+
if (!customization.sourceHandleIds.includes("completed")) {
|
|
3356
|
+
handleCustomizedNodes.set(node.id, customization.sourceHandleIds);
|
|
3357
|
+
}
|
|
3358
|
+
}
|
|
3359
|
+
handlesByNode.set(node.id, entry);
|
|
3360
|
+
}
|
|
3361
|
+
const sourceCounts = new Map;
|
|
3362
|
+
const targetCounts = new Map;
|
|
3363
|
+
const bump = (counts, nodeId, handleId) => {
|
|
3364
|
+
let inner = counts.get(nodeId);
|
|
3365
|
+
if (!inner) {
|
|
3366
|
+
inner = new Map;
|
|
3367
|
+
counts.set(nodeId, inner);
|
|
3368
|
+
}
|
|
3369
|
+
inner.set(handleId, (inner.get(handleId) ?? 0) + 1);
|
|
3370
|
+
};
|
|
3371
|
+
const resolveHandle = (nodeHandles, side, port) => {
|
|
3372
|
+
if (!nodeHandles)
|
|
3373
|
+
return { matched: false };
|
|
3374
|
+
const concrete = nodeHandles[side];
|
|
3375
|
+
if (concrete.has(port)) {
|
|
3376
|
+
return { matched: true, info: concrete.get(port) };
|
|
3377
|
+
}
|
|
3378
|
+
const templates = side === "source" ? nodeHandles.sourceTemplates : nodeHandles.targetTemplates;
|
|
3379
|
+
const matchedTemplate = templates.find((t) => portMatchesTemplate(port, t.templateId));
|
|
3380
|
+
if (matchedTemplate) {
|
|
3381
|
+
return { matched: true, info: matchedTemplate.info };
|
|
3382
|
+
}
|
|
3383
|
+
return { matched: false };
|
|
3384
|
+
};
|
|
3385
|
+
const hasAnyHandles = (nodeHandles, side) => {
|
|
3386
|
+
if (!nodeHandles)
|
|
3387
|
+
return false;
|
|
3388
|
+
if (side === "source")
|
|
3389
|
+
return nodeHandles.sourceDeclared;
|
|
3390
|
+
return nodeHandles.target.size + nodeHandles.targetTemplates.length > 0;
|
|
3391
|
+
};
|
|
3392
|
+
for (const edge of flow.edges) {
|
|
3393
|
+
const sourceHandles = handlesByNode.get(edge.sourceNodeId);
|
|
3394
|
+
if (hasAnyHandles(sourceHandles, "source")) {
|
|
3395
|
+
const resolved = resolveHandle(sourceHandles, "source", edge.sourcePort);
|
|
3396
|
+
if (!resolved.matched) {
|
|
3397
|
+
const customizedHandles = handleCustomizedNodes.get(edge.sourceNodeId);
|
|
3398
|
+
const message = edge.sourcePort === "completed" && customizedHandles ? `Edge from node "${edge.sourceNodeId}" uses the bare "completed" source handle, but this node's handles are customized to ${customizedHandles.join(", ")}, which does not include "completed".${customizedHandles.length > 1 ? ' A "completed" edge collapses every outcome onto one unconditional path and renders no edge on the canvas.' : ""} Rewire the edge to one of those handles.` : `Edge references undeclared source handle "${edge.sourcePort}" on node "${edge.sourceNodeId}"`;
|
|
3399
|
+
issues.push({
|
|
3400
|
+
path: `edges[${edge.id}].sourcePort`,
|
|
3401
|
+
message,
|
|
3402
|
+
severity: "error"
|
|
3403
|
+
});
|
|
3404
|
+
} else {
|
|
3405
|
+
bump(sourceCounts, edge.sourceNodeId, edge.sourcePort);
|
|
3406
|
+
const sourceNode = nodeById.get(edge.sourceNodeId);
|
|
3407
|
+
const visible = resolved.info?.visible;
|
|
3408
|
+
if (!isHandleVisible({ visible }, sourceNode ?? {})) {
|
|
3409
|
+
const visibleInputKey = parseVisibleInputKey(visible);
|
|
3410
|
+
const detail = visibleInputKey !== null ? `inputs.${visibleInputKey} is not true. The "${edge.sourcePort}" handle is only available when inputs.${visibleInputKey} is true. Run "uip maestro flow format" or set inputs.${visibleInputKey} to true on the source node.` : `the "${edge.sourcePort}" handle is hidden (visible: false) on this node, so the edge can never render.`;
|
|
3411
|
+
issues.push({
|
|
3412
|
+
path: visibleInputKey !== null ? `nodes[${edge.sourceNodeId}].inputs.${visibleInputKey}` : `edges[${edge.id}].sourcePort`,
|
|
3413
|
+
message: `Node "${edge.sourceNodeId}" has an outgoing sourcePort: "${edge.sourcePort}" edge, but ${detail}`,
|
|
3414
|
+
severity: "error"
|
|
3415
|
+
});
|
|
3416
|
+
} else if (visible === undefined && edge.sourcePort === ERROR_SOURCE_PORT && sourceNode?.inputs?.errorHandlingEnabled !== true) {
|
|
3417
|
+
issues.push({
|
|
3418
|
+
path: `nodes[${edge.sourceNodeId}].inputs.errorHandlingEnabled`,
|
|
3419
|
+
message: `Node "${edge.sourceNodeId}" has an outgoing sourcePort: "error" edge, but inputs.errorHandlingEnabled is not true. Run "uip maestro flow format" or set inputs.errorHandlingEnabled to true on the source node.`,
|
|
3420
|
+
severity: "error"
|
|
3421
|
+
});
|
|
3422
|
+
}
|
|
3423
|
+
}
|
|
3424
|
+
}
|
|
3425
|
+
const targetHandles = handlesByNode.get(edge.targetNodeId);
|
|
3426
|
+
if (hasAnyHandles(targetHandles, "target")) {
|
|
3427
|
+
const resolved = resolveHandle(targetHandles, "target", edge.targetPort);
|
|
3428
|
+
if (!resolved.matched) {
|
|
3429
|
+
issues.push({
|
|
3430
|
+
path: `edges[${edge.id}].targetPort`,
|
|
3431
|
+
message: `Edge references undeclared target handle "${edge.targetPort}" on node "${edge.targetNodeId}"`,
|
|
3432
|
+
severity: "error"
|
|
3433
|
+
});
|
|
3434
|
+
} else {
|
|
3435
|
+
bump(targetCounts, edge.targetNodeId, edge.targetPort);
|
|
3436
|
+
}
|
|
3437
|
+
}
|
|
3438
|
+
}
|
|
3439
|
+
const checkMax = (counts, side) => {
|
|
3440
|
+
for (const [nodeId, perHandle] of counts) {
|
|
3441
|
+
for (const [handleId, count] of perHandle) {
|
|
3442
|
+
const nodeHandles = handlesByNode.get(nodeId);
|
|
3443
|
+
let info = nodeHandles?.[side].get(handleId);
|
|
3444
|
+
if (!info && nodeHandles) {
|
|
3445
|
+
const templates = side === "source" ? nodeHandles.sourceTemplates : nodeHandles.targetTemplates;
|
|
3446
|
+
info = templates.find((t) => portMatchesTemplate(handleId, t.templateId))?.info;
|
|
3447
|
+
}
|
|
3448
|
+
const max = info?.maxConnections;
|
|
3449
|
+
if (max !== undefined && count > max) {
|
|
3450
|
+
issues.push({
|
|
3451
|
+
path: `nodes[${nodeId}].handles[${handleId}]`,
|
|
3452
|
+
message: `[MAX_CONNECTIONS_EXCEEDED] Handle "${handleId}" on node "${nodeId}" has ${count} ${side} connection(s), max is ${max}`,
|
|
3453
|
+
severity: "error"
|
|
3454
|
+
});
|
|
3455
|
+
}
|
|
3456
|
+
}
|
|
3457
|
+
}
|
|
3458
|
+
};
|
|
3459
|
+
checkMax(sourceCounts, "source");
|
|
3460
|
+
checkMax(targetCounts, "target");
|
|
3461
|
+
return issues;
|
|
3462
|
+
}
|
|
3463
|
+
runNodeValidators(flow) {
|
|
3464
|
+
const issues = [];
|
|
3465
|
+
const context = {
|
|
3466
|
+
definitions: flow.definitions,
|
|
3467
|
+
nodeIds: new Set(flow.nodes.map((n) => n.id)),
|
|
3468
|
+
strictBindings: this.strictBindings
|
|
3469
|
+
};
|
|
3470
|
+
for (const node of flow.nodes) {
|
|
3471
|
+
for (const validator of this.nodeValidators) {
|
|
3472
|
+
if (!validator.appliesTo(node))
|
|
3473
|
+
continue;
|
|
3474
|
+
issues.push(...validator.validate(node, context));
|
|
3475
|
+
}
|
|
3476
|
+
}
|
|
3477
|
+
return issues;
|
|
3478
|
+
}
|
|
3479
|
+
}
|
|
3480
|
+
function indexJoinGraph(flow) {
|
|
3481
|
+
const modelTypeByNodeType = new Map;
|
|
3482
|
+
for (const def of flow.definitions) {
|
|
3483
|
+
if (typeof def.model?.type === "string") {
|
|
3484
|
+
modelTypeByNodeType.set(def.nodeType, def.model.type);
|
|
3485
|
+
}
|
|
3486
|
+
}
|
|
3487
|
+
const incomingByTarget = new Map;
|
|
3488
|
+
for (const edge of flow.edges) {
|
|
3489
|
+
const list = incomingByTarget.get(edge.targetNodeId) ?? [];
|
|
3490
|
+
list.push(edge);
|
|
3491
|
+
incomingByTarget.set(edge.targetNodeId, list);
|
|
3492
|
+
}
|
|
3493
|
+
return {
|
|
3494
|
+
nodesById: new Map(flow.nodes.map((n) => [n.id, n])),
|
|
3495
|
+
incomingByTarget,
|
|
3496
|
+
modelTypeOf: (node) => typeof node.model?.type === "string" ? node.model.type : modelTypeByNodeType.get(node.type)
|
|
3497
|
+
};
|
|
3498
|
+
}
|
|
3499
|
+
function isParallelJoin(node, graph) {
|
|
3500
|
+
return node.type === "core.logic.merge" || graph.modelTypeOf(node) === "bpmn:ParallelGateway";
|
|
3501
|
+
}
|
|
3502
|
+
function isExclusiveSplit(node, graph) {
|
|
3503
|
+
return node.type === "core.logic.decision" || node.type === "core.logic.switch" || graph.modelTypeOf(node) === "bpmn:ExclusiveGateway";
|
|
3504
|
+
}
|
|
3505
|
+
function walkToExclusiveSplit(edge, graph) {
|
|
3506
|
+
let current = edge;
|
|
3507
|
+
const seen = new Set;
|
|
3508
|
+
while (current !== undefined && !seen.has(current.sourceNodeId)) {
|
|
3509
|
+
const source = graph.nodesById.get(current.sourceNodeId);
|
|
3510
|
+
if (!source)
|
|
3511
|
+
return;
|
|
3512
|
+
seen.add(source.id);
|
|
3513
|
+
if (isExclusiveSplit(source, graph)) {
|
|
3514
|
+
return { splitId: source.id, port: current.sourcePort };
|
|
3515
|
+
}
|
|
3516
|
+
const upstream = graph.incomingByTarget.get(source.id) ?? [];
|
|
3517
|
+
current = upstream.length === 1 ? upstream[0] : undefined;
|
|
3518
|
+
}
|
|
3519
|
+
return;
|
|
3520
|
+
}
|
|
3521
|
+
function findUnsatisfiableJoin(node, graph) {
|
|
3522
|
+
if (!isParallelJoin(node, graph))
|
|
3523
|
+
return;
|
|
3524
|
+
const incoming = graph.incomingByTarget.get(node.id) ?? [];
|
|
3525
|
+
if (incoming.length < 2)
|
|
3526
|
+
return;
|
|
3527
|
+
const origins = incoming.map((edge) => walkToExclusiveSplit(edge, graph));
|
|
3528
|
+
if (origins.includes(undefined))
|
|
3529
|
+
return;
|
|
3530
|
+
const resolved = origins;
|
|
3531
|
+
const splitIds = new Set(resolved.map((o) => o.splitId));
|
|
3532
|
+
if (splitIds.size !== 1)
|
|
3533
|
+
return;
|
|
3534
|
+
const ports = [...new Set(resolved.map((o) => o.port))].sort((a, b) => a.localeCompare(b));
|
|
3535
|
+
if (ports.length < 2)
|
|
3536
|
+
return;
|
|
3537
|
+
return { splitId: resolved[0].splitId, ports };
|
|
3538
|
+
}
|
|
3539
|
+
function describeUnsatisfiableJoin(node, finding, graph) {
|
|
3540
|
+
const split = graph.nodesById.get(finding.splitId);
|
|
3541
|
+
const splitLabel = split?.display?.label ?? finding.splitId;
|
|
3542
|
+
const joinLabel = node.display?.label ?? node.id;
|
|
3543
|
+
const splitKind = split?.type === "core.logic.switch" ? "Switch" : "Decision";
|
|
3544
|
+
const portList = finding.ports.map((p) => `"${p}"`).join(", ");
|
|
3545
|
+
return {
|
|
3546
|
+
path: `nodes[${node.id}]`,
|
|
3547
|
+
message: `Merge "${joinLabel}" can never complete: every one of its incoming paths comes from a ` + `different branch of the same ${splitKind} "${splitLabel}" (ports ${portList}). ` + `A Merge is a parallel join that waits for ALL incoming paths, but a ${splitKind} takes exactly one branch, ` + `so the run would stop at "${joinLabel}" and never reach an End node. ` + `Connect each branch directly to the node that should run next (a node accepts several incoming edges), ` + `and use Merge only to rejoin branches that run in parallel.`,
|
|
3548
|
+
severity: "error"
|
|
3549
|
+
};
|
|
3550
|
+
}
|
|
3551
|
+
export {
|
|
3552
|
+
FlowValidateService
|
|
3553
|
+
};
|
|
3554
|
+
|
|
3555
|
+
//# debugId=31F34A853235544A64756E2164756E21
|