@uipath/case-tool 1.201.0 → 1.202.0-preview.136
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{authoring-aaaqwck3.js → authoring-cvjhkfxp.js} +74 -48
- package/dist/{browser-strategy-rkcvc6tn.js → browser-strategy-6atc7v8s.js} +3 -3
- package/dist/case-api-bwcxsbxe.js +381 -0
- package/dist/case-debug-studio-web-service-qcb328bt.js +335 -0
- package/dist/{case-pack-service-bxas5bxz.js → case-pack-service-txby2znw.js} +11 -18
- package/dist/case-validate-service-0q8krj2a.js +2114 -0
- package/dist/connector-enrichment-service-agqxam5y.js +60 -0
- package/dist/connector-lookup-service-ycgk77n0.js +22 -0
- package/dist/connector-task-spec-kdx5bv87.js +1250 -0
- package/dist/{de-5KVP66U6-t37qr521.js → de-5KVP66U6-1bnf4zt5.js} +54 -54
- package/dist/{de-5HU6GFWV-zheyw6d2.js → de-7XQXX2DK-tqdy5n2b.js} +857 -845
- package/dist/debug-service-dxx5pfev.js +57 -0
- package/dist/{en-MABQK6C4-nkzq2j8t.js → en-5ZGC5VQ3-h581y30k.js} +897 -897
- package/dist/{en-4KXU7N7B-24v330b5.js → en-X3S6IZ5J-nhkg6pwn.js} +55 -61
- package/dist/enrichment-service-ebz1f4ds.js +421 -0
- package/dist/{es-MX-POESXYP2-9538722k.js → es-MX-U2VN3SZW-anynsmd8.js} +857 -845
- package/dist/{es-MX-XAVLOQW3-47a405kf.js → es-MX-XAVLOQW3-q00km2y7.js} +54 -54
- package/dist/{es-VFPNVMP5-6sftsdxe.js → es-VFPNVMP5-bx2c337j.js} +54 -54
- package/dist/{es-LUTYN7U7-ye5zn4sn.js → es-ZGCPPOME-8gazspjj.js} +857 -845
- package/dist/{fr-E5MDO4EG-f3x1jeqw.js → fr-E5MDO4EG-c3mkty2t.js} +54 -54
- package/dist/{fr-LJIK56LW-xf3xf2kh.js → fr-T3G5GZCT-y3f7xb1q.js} +857 -845
- package/dist/{index-cncq089f.js → index-7s201ka0.js} +163 -80
- package/dist/{index-hsadteg4.js → index-h8cph72k.js} +4 -4
- package/dist/index-k4xz16db.js +63712 -0
- package/dist/{index-p50kx6jv.js → index-k5tbd184.js} +276 -276
- package/dist/index-kwxe1vep.js +143 -0
- package/dist/{index-9f8q2ege.js → index-p3vk57z7.js} +27 -27
- package/dist/index-qyf1dee9.js +630 -0
- package/dist/index.js +16 -17
- package/dist/{index.umd.min-drpc57vn.js → index.umd.min-fqefd9kn.js} +3 -3
- package/dist/input-reader-xb9vecxv.js +11 -0
- package/dist/{ja-FGEGLBJ3-w9byjhze.js → ja-BI5K3JDZ-77zat4z2.js} +857 -845
- package/dist/{ja-DPO5PPLO-fmbg7zt2.js → ja-DPO5PPLO-mysfmznx.js} +54 -54
- package/dist/{js-yaml-cggj6w7q.js → js-yaml-87cdx6bm.js} +27 -19
- package/dist/known-types-qryebm95.js +15 -0
- package/dist/{ko-ITOPNIYI-aqy82jk8.js → ko-ITOPNIYI-hmxd2asd.js} +54 -54
- package/dist/{ko-TUAK3G3A-s25w0ckg.js → ko-JOBF5V7N-4bv0mjje.js} +857 -845
- package/dist/{node-strategy-8gzp67xs.js → node-strategy-0267x5e5.js} +130 -14
- package/dist/node-sync-service-9a9ksj9s.js +38 -0
- package/dist/{packager-tool-9qecd4wb.js → packager-tool-0gctz400.js} +5 -2
- package/dist/packager-tool-0mj06yen.js +65 -0
- package/dist/{packager-tool-1cb0d5e0.js → packager-tool-0v3s8r01.js} +2 -2
- package/dist/packager-tool-134mvmcm.js +3040 -0
- package/dist/{packager-tool-wckvcay0.js → packager-tool-1yh8g550.js} +9 -7
- package/dist/{packager-tool-zqa5k9f7.js → packager-tool-37zvccmt.js} +990 -2287
- package/dist/packager-tool-3ze5vt52.js +54 -0
- package/dist/{packager-tool-w3h8xrnv.js → packager-tool-56j6keqr.js} +71 -3
- package/dist/{packager-tool-3wygxefb.js → packager-tool-58q3tvh7.js} +10 -10
- package/dist/{packager-tool-zx48pq4v.js → packager-tool-5ax2ywcg.js} +1997 -1174
- package/dist/{packager-tool-we1phz71.js → packager-tool-885mmrvr.js} +31 -3
- package/dist/packager-tool-9scn4b15.js +852 -0
- package/dist/{packager-tool-p9j43p8h.js → packager-tool-a42jnpy5.js} +3 -9
- package/dist/packager-tool-ajvse9r3.js +356 -0
- package/dist/{packager-tool-jysvpt8p.js → packager-tool-c23zrhj8.js} +2 -2
- package/dist/packager-tool-dqs5qy9s.js +2327 -0
- package/dist/packager-tool-ee5dhff3.js +6717 -0
- package/dist/{packager-tool-wvqz81mx.js → packager-tool-gtzs471w.js} +3 -2
- package/dist/packager-tool-hrr0efqc.js +546 -0
- package/dist/packager-tool-j9rfdrdm.js +1125 -0
- package/dist/{packager-tool-gq6952nc.js → packager-tool-jhdf8gt1.js} +5054 -17851
- package/dist/packager-tool-mp59jtpm.js +6865 -0
- package/dist/packager-tool-njzcfve1.js +77 -0
- package/dist/packager-tool-p7jgrm5r.js +15 -0
- package/dist/packager-tool-r84ng21s.js +57 -0
- package/dist/packager-tool-rqvr63x5.js +1217 -0
- package/dist/packager-tool-s12392rd.js +80 -0
- package/dist/packager-tool-sv0d6wb6.js +173 -0
- package/dist/packager-tool-ty1t2byf.js +27 -0
- package/dist/packager-tool-vepqrsse.js +23 -0
- package/dist/packager-tool-w5ek334f.js +4892 -0
- package/dist/{packager-tool-5arsyj36.js → packager-tool-yc4kw997.js} +4 -2
- package/dist/packager-tool.js +91 -5
- package/dist/project-artifacts-vhanbtw5.js +14 -0
- package/dist/{pt-BR-7XGDXRDJ-espnkgrj.js → pt-BR-7DYX2YPT-fqxqzqtf.js} +857 -845
- package/dist/{pt-BR-BXURWX4W-xzyndmrx.js → pt-BR-BXURWX4W-bwfcwhk1.js} +54 -54
- package/dist/{pt-TMVOOITK-myha14p9.js → pt-HTXGGX6Y-97t21dty.js} +857 -845
- package/dist/{pt-YI4VE4YQ-521681zf.js → pt-YI4VE4YQ-wdkn2dcf.js} +54 -54
- package/dist/resourcesSyncService-mrmmwaqr.js +683 -0
- package/dist/{ro-2VALF3S5-cdz5afkr.js → ro-KTNQ3MNU-y08n1x5g.js} +857 -845
- package/dist/{ro-RKXZUG7Z-pdztdafr.js → ro-RKXZUG7Z-nndrrsq7.js} +54 -54
- package/dist/{ru-GSCT4ARZ-qj8dm4dp.js → ru-GSCT4ARZ-1sa9wbr8.js} +1 -1
- package/dist/ru-WBLQ5B2U-8r3c7v6d.js +10 -0
- package/dist/strict-case-rules-303vjvas.js +377 -0
- package/dist/strict-sdd-audit-qdmc9e1y.js +1024 -0
- package/dist/tool.js +17 -18
- package/dist/{tr-AA6O2WSR-bj9n80k8.js → tr-AA6O2WSR-86ba4bdf.js} +54 -54
- package/dist/{tr-RIFCEXFH-hgkb4g8y.js → tr-XHSSPNK7-cmqyc4wd.js} +857 -845
- package/dist/trigger-enrichment-service-zdx4x92j.js +340 -0
- package/dist/{zh-CN-IP3FMXWM-1cfarhcp.js → zh-CN-2EHQT23P-mr63c4xz.js} +857 -845
- package/dist/{zh-CN-R2KLHXQV-x6gnk2n0.js → zh-CN-R2KLHXQV-agqr45ds.js} +54 -54
- package/dist/{zh-TW-AGIDXIPF-n6r6w7vy.js → zh-TW-MGZCE5BT-c4ekmrz9.js} +857 -845
- package/dist/{zh-TW-RJGCQANM-tt8j5wv2.js → zh-TW-RJGCQANM-w0x3qc1p.js} +54 -54
- package/package.json +2 -2
- package/dist/index-8bsngk9y.js +0 -247140
- package/dist/packager-tool-axcdppp9.js +0 -13607
- package/dist/ru-WBLQ5B2U-8xd4gpv2.js +0 -10
|
@@ -0,0 +1,2114 @@
|
|
|
1
|
+
import {
|
|
2
|
+
contextEntry,
|
|
3
|
+
missingContextKeys,
|
|
4
|
+
readActivityTypeId
|
|
5
|
+
} from "./packager-tool-ty1t2byf.js";
|
|
6
|
+
import {
|
|
7
|
+
validateIntSvcNode
|
|
8
|
+
} from "./packager-tool-mp59jtpm.js";
|
|
9
|
+
import {
|
|
10
|
+
loadCaseSchemaAsync
|
|
11
|
+
} from "./packager-tool-p7jgrm5r.js";
|
|
12
|
+
import {
|
|
13
|
+
VALID_CASE_COMPLETION_RULE_TYPES,
|
|
14
|
+
VALID_CASE_EXIT_RULE_TYPES,
|
|
15
|
+
VALID_STAGE_COMPLETION_RULE_TYPES,
|
|
16
|
+
VALID_STAGE_ENTRY_RULE_TYPES,
|
|
17
|
+
VALID_STAGE_EXIT_RULE_TYPES,
|
|
18
|
+
VALID_TASK_ENTRY_RULE_TYPES
|
|
19
|
+
} from "./packager-tool-sv0d6wb6.js";
|
|
20
|
+
import"./packager-tool-dqs5qy9s.js";
|
|
21
|
+
import"./packager-tool-qa9gftnj.js";
|
|
22
|
+
import {
|
|
23
|
+
catchError,
|
|
24
|
+
logger
|
|
25
|
+
} from "./packager-tool-5ax2ywcg.js";
|
|
26
|
+
import {
|
|
27
|
+
getFileSystem
|
|
28
|
+
} from "./packager-tool-0v3s8r01.js";
|
|
29
|
+
import"./packager-tool-0gctz400.js";
|
|
30
|
+
import"./packager-tool-yc4kw997.js";
|
|
31
|
+
import {
|
|
32
|
+
__require
|
|
33
|
+
} from "./packager-tool-1yh8g550.js";
|
|
34
|
+
|
|
35
|
+
// src/services/case-validators/connector-validator.ts
|
|
36
|
+
var CONNECTOR_TASK_TYPES = new Set([
|
|
37
|
+
"execute-connector-activity",
|
|
38
|
+
"wait-for-connector"
|
|
39
|
+
]);
|
|
40
|
+
var connectorTaskValidator = {
|
|
41
|
+
id: "uipath-connector",
|
|
42
|
+
appliesTo: (task) => CONNECTOR_TASK_TYPES.has(task.type),
|
|
43
|
+
validate: (task, ctx) => {
|
|
44
|
+
const data = task.data;
|
|
45
|
+
if (!data)
|
|
46
|
+
return [];
|
|
47
|
+
const context = data.context ?? [];
|
|
48
|
+
const inputs = data.inputs ?? [];
|
|
49
|
+
const isTrigger = task.type === "wait-for-connector";
|
|
50
|
+
const connectorKey = readContextValue(context, "connectorKey");
|
|
51
|
+
const objectName = readContextValue(context, "objectName");
|
|
52
|
+
const configurationStr = readMetadataConfiguration(context);
|
|
53
|
+
const httpMethod = readContextValue(context, "method");
|
|
54
|
+
const eventType = readContextValue(context, "eventType");
|
|
55
|
+
if (!connectorKey || !objectName || !configurationStr)
|
|
56
|
+
return [];
|
|
57
|
+
if (isTrigger ? !eventType : !httpMethod)
|
|
58
|
+
return [];
|
|
59
|
+
const bindings = ctx.schema.bindings ?? [];
|
|
60
|
+
const connectionId = resolveBindingRef(readContextValue(context, "connection"), bindings) ?? readContextValue(context, "resourceKey");
|
|
61
|
+
const folderKey = resolveBindingRef(readContextValue(context, "folderKey"), bindings);
|
|
62
|
+
const validationContext = {
|
|
63
|
+
nodeId: task.id,
|
|
64
|
+
nodeKind: isTrigger ? "trigger" : "activity",
|
|
65
|
+
connectionId,
|
|
66
|
+
folderKey,
|
|
67
|
+
objectName,
|
|
68
|
+
httpMethod: isTrigger ? undefined : httpMethod,
|
|
69
|
+
endpoint: readContextValue(context, "path"),
|
|
70
|
+
eventMode: readContextValue(context, "eventMode"),
|
|
71
|
+
eventType,
|
|
72
|
+
bodyParameters: readInputBody(inputs, "body"),
|
|
73
|
+
queryParameters: readInputBody(inputs, "queryParameters"),
|
|
74
|
+
pathParameters: readInputBody(inputs, "pathParameters"),
|
|
75
|
+
eventParameters: isTrigger ? readTriggerEventParameters(inputs) : undefined,
|
|
76
|
+
configuration: configurationStr
|
|
77
|
+
};
|
|
78
|
+
const path = `nodes[${ctx.stageId}].data.tasks[${task.id}].data`;
|
|
79
|
+
const issues = validateIntSvcNode(validationContext).map((issue) => ({
|
|
80
|
+
code: extractDapCode(issue.title) ?? "CONNECTOR_TASK_INVALID",
|
|
81
|
+
path,
|
|
82
|
+
message: issue.title,
|
|
83
|
+
severity: issue.type === "Error" ? "error" : "warning"
|
|
84
|
+
}));
|
|
85
|
+
issues.push(...validateCeqlFilterShape(task.id, ctx.stageId, inputs, configurationStr));
|
|
86
|
+
return issues;
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
function readContextValue(context, name) {
|
|
90
|
+
const entry = context.find((c) => c.name === name);
|
|
91
|
+
if (typeof entry?.value !== "string")
|
|
92
|
+
return;
|
|
93
|
+
return entry.value;
|
|
94
|
+
}
|
|
95
|
+
var BINDING_REF_PATTERN = /^=bindings\.([A-Za-z0-9_-]+)$/;
|
|
96
|
+
function resolveBindingRef(rawValue, bindings) {
|
|
97
|
+
if (typeof rawValue !== "string")
|
|
98
|
+
return;
|
|
99
|
+
const match = BINDING_REF_PATTERN.exec(rawValue);
|
|
100
|
+
if (!match)
|
|
101
|
+
return rawValue;
|
|
102
|
+
const binding = bindings.find((b) => b.id === match[1]);
|
|
103
|
+
return typeof binding?.default === "string" ? binding.default : undefined;
|
|
104
|
+
}
|
|
105
|
+
function readMetadataConfiguration(context) {
|
|
106
|
+
const meta = context.find((c) => c.name === "metadata");
|
|
107
|
+
const body = meta?.body;
|
|
108
|
+
const apc = body?.activityPropertyConfiguration;
|
|
109
|
+
const config = apc?.configuration;
|
|
110
|
+
return typeof config === "string" ? config : undefined;
|
|
111
|
+
}
|
|
112
|
+
function readInputBody(inputs, name) {
|
|
113
|
+
const entry = inputs.find((i) => i.name === name);
|
|
114
|
+
const body = entry?.body;
|
|
115
|
+
if (body && typeof body === "object" && !Array.isArray(body)) {
|
|
116
|
+
return body;
|
|
117
|
+
}
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
function readTriggerEventParameters(inputs) {
|
|
121
|
+
const body = readInputBody(inputs, "body");
|
|
122
|
+
const parameters = body?.parameters;
|
|
123
|
+
if (parameters && typeof parameters === "object" && !Array.isArray(parameters)) {
|
|
124
|
+
return parameters;
|
|
125
|
+
}
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
function extractDapCode(title) {
|
|
129
|
+
const m = title.match(/^\[(DAP-[A-Z]{2}-\d{4})\]/);
|
|
130
|
+
return m?.[1];
|
|
131
|
+
}
|
|
132
|
+
function validateCeqlFilterShape(taskId, stageId, inputs, configurationStr) {
|
|
133
|
+
const issues = [];
|
|
134
|
+
const queryParameters = readInputBody(inputs, "queryParameters");
|
|
135
|
+
if (queryParameters && Object.hasOwn(queryParameters, "$where")) {
|
|
136
|
+
issues.push({
|
|
137
|
+
code: "CONNECTOR_TASK_CEQL_DOLLAR_WHERE",
|
|
138
|
+
path: `nodes[${stageId}].data.tasks[${taskId}].data.inputs[name="queryParameters"].body["$where"]`,
|
|
139
|
+
severity: "error",
|
|
140
|
+
message: 'Connector task uses OData-style `"$where"` (with leading `$`) ' + "in queryParameters.body. CEQL uses bare `where` — rename the " + 'key from `"$where"` to `"where"`. The leading `$` is the OData ' + "system-query-option prefix and is not recognized by " + "uipath.connector.* nodes."
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
const queryWhereAuthored = queryParameters !== undefined && isNonEmptyString(queryParameters.where);
|
|
144
|
+
const savedTrees = readSavedFilterTrees(configurationStr);
|
|
145
|
+
const savedWhereAuthored = savedTrees !== undefined && isNonEmptyObject(savedTrees.where);
|
|
146
|
+
if (queryWhereAuthored && !savedWhereAuthored) {
|
|
147
|
+
issues.push({
|
|
148
|
+
code: "CONNECTOR_TASK_CEQL_MISSING_FILTER_TREE",
|
|
149
|
+
path: `nodes[${stageId}].data.tasks[${taskId}].data.context[name="metadata"].body.activityPropertyConfiguration.configuration.savedFilterTrees.where`,
|
|
150
|
+
severity: "error",
|
|
151
|
+
message: "Connector task has `queryParameters.where` set but no " + "matching `savedFilterTrees.where` AST inside the " + "essentialConfiguration. CEQL filters must be authored on " + "both sides — the runtime evaluates the expression, Studio " + "Web needs the AST to render the filter UI."
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
if (savedWhereAuthored && !queryWhereAuthored) {
|
|
155
|
+
issues.push({
|
|
156
|
+
code: "CONNECTOR_TASK_CEQL_MISSING_FILTER_EXPRESSION",
|
|
157
|
+
path: `nodes[${stageId}].data.tasks[${taskId}].data.inputs[name="queryParameters"].body.where`,
|
|
158
|
+
severity: "error",
|
|
159
|
+
message: "Connector task has `savedFilterTrees.where` AST but no " + "matching `queryParameters.where` expression. CEQL filters " + "must be authored on both sides — the runtime evaluates the " + "expression, not the AST."
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
return issues;
|
|
163
|
+
}
|
|
164
|
+
function readSavedFilterTrees(configurationStr) {
|
|
165
|
+
const jsonStr = configurationStr.replace(/^=jsonString:/, "");
|
|
166
|
+
const [, parsed] = catchError(() => JSON.parse(jsonStr));
|
|
167
|
+
if (!parsed)
|
|
168
|
+
return;
|
|
169
|
+
const ec = parsed.essentialConfiguration;
|
|
170
|
+
const trees = ec?.savedFilterTrees;
|
|
171
|
+
if (trees && typeof trees === "object" && !Array.isArray(trees)) {
|
|
172
|
+
return trees;
|
|
173
|
+
}
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
function isNonEmptyString(value) {
|
|
177
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
178
|
+
}
|
|
179
|
+
function isNonEmptyObject(value) {
|
|
180
|
+
return !!value && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length > 0;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// src/services/case-validators/task-hygiene-validator.ts
|
|
184
|
+
var taskElementIdValidator = {
|
|
185
|
+
id: "uipath-task-element-id",
|
|
186
|
+
appliesTo: (task) => {
|
|
187
|
+
if (task.elementId !== undefined)
|
|
188
|
+
return false;
|
|
189
|
+
const data = task.data;
|
|
190
|
+
const hasIoArray = (key) => Array.isArray(data?.[key]) && data[key].length > 0;
|
|
191
|
+
return hasIoArray("inputs") || hasIoArray("outputs");
|
|
192
|
+
},
|
|
193
|
+
validate: (task, ctx) => {
|
|
194
|
+
const name = task.displayName ?? task.id ?? "task";
|
|
195
|
+
return [
|
|
196
|
+
{
|
|
197
|
+
code: "TASK_MISSING_ELEMENT_ID",
|
|
198
|
+
path: `nodes[${ctx.stageId}].data.tasks[${task.id ?? "?"}]`,
|
|
199
|
+
message: `Task "${name}" carries inputs or outputs but has no "elementId". Set it to "${ctx.stageId}-${task.id ?? "<taskId>"}" so those variables resolve to this task.`,
|
|
200
|
+
severity: "warning"
|
|
201
|
+
}
|
|
202
|
+
];
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
var unconfiguredTaskValidator = {
|
|
206
|
+
id: "uipath-unconfigured-task",
|
|
207
|
+
appliesTo: (task) => task.type !== "wait-for-timer" && Object.keys(task.data ?? {}).length === 0,
|
|
208
|
+
validate: (task, ctx) => {
|
|
209
|
+
const name = task.displayName ?? task.id ?? "task";
|
|
210
|
+
return [
|
|
211
|
+
{
|
|
212
|
+
code: "TASK_NOT_CONFIGURED",
|
|
213
|
+
path: `nodes[${ctx.stageId}].data.tasks[${task.id ?? "?"}].data`,
|
|
214
|
+
message: `Task "${name}" (type "${task.type ?? "unknown"}") has an empty "data" block, so no other check can run on it. Configure the task or remove it.`,
|
|
215
|
+
severity: "warning"
|
|
216
|
+
}
|
|
217
|
+
];
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
// src/services/case-validators/timer-validator.ts
|
|
222
|
+
var TIMER_TYPES = ["timeDuration", "timeDate", "timeCycle"];
|
|
223
|
+
var VALUE_FIELD = {
|
|
224
|
+
timeDuration: "timeDuration",
|
|
225
|
+
timeDate: "timeDate",
|
|
226
|
+
timeCycle: "timeCycle"
|
|
227
|
+
};
|
|
228
|
+
function issue(stageId, taskId, code, message) {
|
|
229
|
+
return {
|
|
230
|
+
code,
|
|
231
|
+
path: `nodes[${stageId}].data.tasks[${taskId ?? "?"}].data`,
|
|
232
|
+
message,
|
|
233
|
+
severity: "error"
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
var timerTaskValidator = {
|
|
237
|
+
id: "uipath-wait-for-timer",
|
|
238
|
+
appliesTo: (task) => task.type === "wait-for-timer",
|
|
239
|
+
validate: (task, ctx) => {
|
|
240
|
+
const name = task.displayName ?? task.id ?? "wait-for-timer task";
|
|
241
|
+
const data = task.data ?? {};
|
|
242
|
+
if (Object.keys(data).length === 0) {
|
|
243
|
+
return [
|
|
244
|
+
issue(ctx.stageId, task.id, "TIMER_TASK_NOT_CONFIGURED", `Timer task "${name}" has an empty "data" block, so it is not configured and no other check runs on it. Set "timerType" to one of ${TIMER_TYPES.join(", ")} and provide the matching value field.`)
|
|
245
|
+
];
|
|
246
|
+
}
|
|
247
|
+
const timerType = data.timerType;
|
|
248
|
+
if (timerType === undefined) {
|
|
249
|
+
const message = typeof data.timer === "string" ? `Timer task "${name}" sets "timer" instead of "timerType". Rename the field to "timerType" (one of ${TIMER_TYPES.join(", ")}) — nothing reads "timer", so the timer stays unconfigured.` : `Timer task "${name}" has no "timerType". Set it to one of ${TIMER_TYPES.join(", ")} and provide the matching value field.`;
|
|
250
|
+
return [
|
|
251
|
+
issue(ctx.stageId, task.id, "TIMER_TASK_MISSING_TIMER_TYPE", message)
|
|
252
|
+
];
|
|
253
|
+
}
|
|
254
|
+
if (typeof timerType !== "string" || !TIMER_TYPES.includes(timerType)) {
|
|
255
|
+
return [
|
|
256
|
+
issue(ctx.stageId, task.id, "TIMER_TASK_INVALID_TIMER_TYPE", `Timer task "${name}" has an invalid "timerType" (${JSON.stringify(timerType)}). Valid values: ${TIMER_TYPES.join(", ")}.`)
|
|
257
|
+
];
|
|
258
|
+
}
|
|
259
|
+
const field = VALUE_FIELD[timerType];
|
|
260
|
+
const value = data[field];
|
|
261
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
262
|
+
return [
|
|
263
|
+
issue(ctx.stageId, task.id, "TIMER_TASK_MISSING_VALUE", `Timer task "${name}" declares "timerType": "${timerType}" but has no "${field}" value.`)
|
|
264
|
+
];
|
|
265
|
+
}
|
|
266
|
+
return [];
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
// src/services/case-validators/strict-task-validators.ts
|
|
270
|
+
var CONNECTOR_TASK_TYPES2 = new Set([
|
|
271
|
+
"execute-connector-activity",
|
|
272
|
+
"wait-for-connector"
|
|
273
|
+
]);
|
|
274
|
+
var SERVICE_TYPE_BY_TASK = {
|
|
275
|
+
"execute-connector-activity": "Intsvc.ActivityExecution",
|
|
276
|
+
"wait-for-connector": "Intsvc.WaitForEvent"
|
|
277
|
+
};
|
|
278
|
+
var asRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : undefined;
|
|
279
|
+
var asArray = (value) => Array.isArray(value) ? value : [];
|
|
280
|
+
var taskData = (task) => asRecord(task.data);
|
|
281
|
+
var taskLabel = (task) => task.displayName ?? task.id ?? "task";
|
|
282
|
+
var taskPath = (stageId, task) => `nodes[${stageId}].data.tasks[${task.id ?? "?"}]`;
|
|
283
|
+
var strictConnectorContextValidator = {
|
|
284
|
+
id: "strict-connector-context",
|
|
285
|
+
appliesTo: (task) => CONNECTOR_TASK_TYPES2.has(task.type ?? ""),
|
|
286
|
+
validate: (task, ctx) => {
|
|
287
|
+
const issues = [];
|
|
288
|
+
const context = taskData(task)?.context;
|
|
289
|
+
const missing = missingContextKeys(context);
|
|
290
|
+
if (missing.length > 0) {
|
|
291
|
+
issues.push({
|
|
292
|
+
code: "STRICT_CONNECTOR_CONTEXT_INCOMPLETE",
|
|
293
|
+
path: `${taskPath(ctx.stageId, task)}.data.context`,
|
|
294
|
+
severity: "error",
|
|
295
|
+
message: `Connector task "${taskLabel(task)}" is missing context ` + `entries: ${missing.join(", ")}. The default validator ` + "skips a task in this state as mid-authoring, so nothing " + "else reports it — under --strict an incomplete context " + "is a defect."
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
if (contextEntry(context, "metadata") !== undefined) {
|
|
299
|
+
if (readActivityTypeId(context) === undefined) {
|
|
300
|
+
issues.push({
|
|
301
|
+
code: "STRICT_CONNECTOR_ACTIVITY_TYPE_ID",
|
|
302
|
+
path: `${taskPath(ctx.stageId, task)}.data.context[metadata].body.activityPropertyConfiguration.UiPathActivityTypeId`,
|
|
303
|
+
severity: "error",
|
|
304
|
+
message: `Connector task "${taskLabel(task)}" has a metadata ` + "configuration with no UiPathActivityTypeId. The " + "connection id alone does not tell the runtime which " + "activity to run, so the task deploys and never binds."
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
const expectedServiceType = SERVICE_TYPE_BY_TASK[task.type ?? ""];
|
|
309
|
+
const serviceType = taskData(task)?.serviceType;
|
|
310
|
+
if (expectedServiceType !== undefined && serviceType !== undefined) {
|
|
311
|
+
if (serviceType !== expectedServiceType) {
|
|
312
|
+
issues.push({
|
|
313
|
+
code: "STRICT_CONNECTOR_SERVICE_TYPE",
|
|
314
|
+
path: `${taskPath(ctx.stageId, task)}.data.serviceType`,
|
|
315
|
+
severity: "error",
|
|
316
|
+
message: `Connector task "${taskLabel(task)}" declares ` + `serviceType "${String(serviceType)}"; a ` + `"${task.type}" task must declare ` + `"${expectedServiceType}".`
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
} else if (expectedServiceType !== undefined) {
|
|
320
|
+
issues.push({
|
|
321
|
+
code: "STRICT_CONNECTOR_SERVICE_TYPE",
|
|
322
|
+
path: `${taskPath(ctx.stageId, task)}.data.serviceType`,
|
|
323
|
+
severity: "error",
|
|
324
|
+
message: `Connector task "${taskLabel(task)}" declares no ` + `serviceType; a "${task.type}" task must declare ` + `"${expectedServiceType}".`
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
return issues;
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
var strictOutputShapeValidator = {
|
|
331
|
+
id: "strict-output-shape",
|
|
332
|
+
appliesTo: (task) => asArray(taskData(task)?.outputs).length > 0,
|
|
333
|
+
validate: (task, ctx) => {
|
|
334
|
+
const issues = [];
|
|
335
|
+
const base = `${taskPath(ctx.stageId, task)}.data.outputs`;
|
|
336
|
+
asArray(taskData(task)?.outputs).forEach((entry, index) => {
|
|
337
|
+
const output = asRecord(entry);
|
|
338
|
+
if (!output)
|
|
339
|
+
return;
|
|
340
|
+
const label = typeof output.name === "string" ? output.name : `#${index}`;
|
|
341
|
+
const path = `${base}[${label}]`;
|
|
342
|
+
const report = (code, message) => void issues.push({
|
|
343
|
+
code,
|
|
344
|
+
path,
|
|
345
|
+
severity: "error",
|
|
346
|
+
message
|
|
347
|
+
});
|
|
348
|
+
if (output.custom === true) {
|
|
349
|
+
const problems = [];
|
|
350
|
+
if (!("target" in output) || output.target !== "")
|
|
351
|
+
problems.push('"target" must be present and empty');
|
|
352
|
+
if (!("body" in output) || output.body !== "")
|
|
353
|
+
problems.push('"body" must be present and empty');
|
|
354
|
+
if (output.elementId !== "root")
|
|
355
|
+
problems.push('"elementId" must be "root"');
|
|
356
|
+
if ("id" in output)
|
|
357
|
+
problems.push('"id" must be absent');
|
|
358
|
+
if ("originalVar" in output)
|
|
359
|
+
problems.push('"originalVar" must be absent');
|
|
360
|
+
if (problems.length > 0) {
|
|
361
|
+
report("STRICT_CUSTOM_OUTPUT_SHAPE", `Custom output "${label}" is malformed: ` + `${problems.join("; ")}. The converter reads these ` + "keys positionally, so a dropped or renamed one " + "writes the value nowhere.");
|
|
362
|
+
}
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
return issues;
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
var strictInputReferenceValidator = {
|
|
370
|
+
id: "strict-input-reference",
|
|
371
|
+
appliesTo: (task) => asArray(taskData(task)?.inputs).length > 0,
|
|
372
|
+
validate: (task, ctx) => {
|
|
373
|
+
const issues = [];
|
|
374
|
+
const base = `${taskPath(ctx.stageId, task)}.data.inputs`;
|
|
375
|
+
asArray(taskData(task)?.inputs).forEach((entry, index) => {
|
|
376
|
+
const input = asRecord(entry);
|
|
377
|
+
if (!input)
|
|
378
|
+
return;
|
|
379
|
+
const label = typeof input.name === "string" ? input.name : `#${index}`;
|
|
380
|
+
const value = input.value;
|
|
381
|
+
if (typeof value !== "string")
|
|
382
|
+
return;
|
|
383
|
+
if (value === "") {
|
|
384
|
+
issues.push({
|
|
385
|
+
code: "STRICT_INPUT_UNBOUND",
|
|
386
|
+
path: `${base}[${label}]`,
|
|
387
|
+
severity: "warning",
|
|
388
|
+
message: `Input "${label}" on task "${taskLabel(task)}" has no ` + "value. If the task is finished, this input never " + "receives data."
|
|
389
|
+
});
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
if (!value.startsWith("="))
|
|
393
|
+
return;
|
|
394
|
+
if (value.startsWith("=js:") || value.startsWith("=metadata") || value.startsWith("=bindings.")) {
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
if (/^=vars\.[A-Za-z0-9_]+$/.test(value))
|
|
398
|
+
return;
|
|
399
|
+
issues.push({
|
|
400
|
+
code: "STRICT_INPUT_REF_FORM",
|
|
401
|
+
path: `${base}[${label}]`,
|
|
402
|
+
severity: "error",
|
|
403
|
+
message: `Input "${label}" references "${value}". A variable ` + 'reference must be "=vars.<name>" — a bare "=<id>" is ' + "not resolved by the converter."
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
return issues;
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
var strictTaskValidators = [
|
|
410
|
+
strictConnectorContextValidator,
|
|
411
|
+
strictOutputShapeValidator,
|
|
412
|
+
strictInputReferenceValidator
|
|
413
|
+
];
|
|
414
|
+
|
|
415
|
+
// src/services/case-validators/index.ts
|
|
416
|
+
var defaultTaskValidators = [
|
|
417
|
+
connectorTaskValidator,
|
|
418
|
+
timerTaskValidator,
|
|
419
|
+
taskElementIdValidator,
|
|
420
|
+
unconfiguredTaskValidator
|
|
421
|
+
];
|
|
422
|
+
|
|
423
|
+
// src/services/case-validate-service.ts
|
|
424
|
+
var mapCanvasSeverity = (severity) => {
|
|
425
|
+
const normalized = (severity ?? "error").toUpperCase();
|
|
426
|
+
return normalized === "CRITICAL" || normalized === "ERROR" ? "error" : "warning";
|
|
427
|
+
};
|
|
428
|
+
var checksTaskContent = (profile) => profile === "full" || profile === "strict";
|
|
429
|
+
var SKELETON_V2_SKIPPED_CANVAS_CODES = new Set([
|
|
430
|
+
"CASE_MGMT_STAGE_TASKS_MISSING",
|
|
431
|
+
"CASE_MGMT_STAGE_TASK_SELECTION_MISSING",
|
|
432
|
+
"CASE_MGMT_STAGE_TASK_ENTRY_CONDITION_MISSING",
|
|
433
|
+
"CASE_MGMT_STAGE_TASK_NAME_DUPLICATE",
|
|
434
|
+
"CASE_MGMT_STAGE_TASK_NAME_INVALID",
|
|
435
|
+
"CASE_MGMT_TASK_OUTPUT_VARIABLE_MULTIPLE",
|
|
436
|
+
"CASE_MGMT_TASK_OUTPUT_VARIABLE_REQUIRED",
|
|
437
|
+
"CASE_MGMT_STAGE_NO_TASKS_RUNNING_ON_RE_ENTRY",
|
|
438
|
+
"TASK_TIMER_OUT_OF_RANGE",
|
|
439
|
+
"CASE_MGMT_ROOT_CASE_CONDITION_RULE_SELECTED_TASK_IDS_MISSING",
|
|
440
|
+
"CASE_MGMT_ROOT_CASE_CONDITION_RULE_SELECTED_TASK_MISSING",
|
|
441
|
+
"CASE_MGMT_ROOT_CASE_CONDITION_RULE_NO_REQUIRED_TASK"
|
|
442
|
+
]);
|
|
443
|
+
var isTaskScopedCanvasIssue = (issue2) => SKELETON_V2_SKIPPED_CANVAS_CODES.has(issue2.code) || issue2.additionalErrorInfo?.taskId !== undefined;
|
|
444
|
+
function parseSemverMajor(version) {
|
|
445
|
+
if (!version)
|
|
446
|
+
return null;
|
|
447
|
+
const match = /^(\d+)\.(\d+)\.(\d+)/.exec(version);
|
|
448
|
+
if (!match)
|
|
449
|
+
return null;
|
|
450
|
+
return Number.parseInt(match[1], 10);
|
|
451
|
+
}
|
|
452
|
+
function slaToHours(sla) {
|
|
453
|
+
if (!sla?.count || sla.count <= 0)
|
|
454
|
+
return 0;
|
|
455
|
+
const multipliers = {
|
|
456
|
+
h: 1,
|
|
457
|
+
d: 24,
|
|
458
|
+
w: 168,
|
|
459
|
+
m: 5040
|
|
460
|
+
};
|
|
461
|
+
return sla.count * (multipliers[sla.unit ?? "h"] ?? 1);
|
|
462
|
+
}
|
|
463
|
+
function rulesMatch(a, b) {
|
|
464
|
+
if (a.length !== b.length)
|
|
465
|
+
return false;
|
|
466
|
+
return a.every((andGroupA, i) => {
|
|
467
|
+
const andGroupB = b[i];
|
|
468
|
+
if (!andGroupB || andGroupA.length !== andGroupB.length)
|
|
469
|
+
return false;
|
|
470
|
+
return andGroupA.every((ruleA, j) => {
|
|
471
|
+
const ruleB = andGroupB[j];
|
|
472
|
+
if (!ruleB || ruleA.rule !== ruleB.rule)
|
|
473
|
+
return false;
|
|
474
|
+
switch (ruleA.rule) {
|
|
475
|
+
case "wait-for-connector":
|
|
476
|
+
return ruleA.conditionExpression === ruleB.conditionExpression && JSON.stringify(ruleA.uipath) === JSON.stringify(ruleB.uipath);
|
|
477
|
+
case "case-entered":
|
|
478
|
+
return ruleA.conditionExpression === ruleB.conditionExpression;
|
|
479
|
+
case "current-stage-entered":
|
|
480
|
+
return ruleA.conditionExpression === ruleB.conditionExpression;
|
|
481
|
+
case "adhoc":
|
|
482
|
+
return ruleA.conditionExpression === ruleB.conditionExpression;
|
|
483
|
+
case "selected-stage-completed":
|
|
484
|
+
return ruleA.selectedStageId === ruleB.selectedStageId && ruleA.conditionExpression === ruleB.conditionExpression;
|
|
485
|
+
case "selected-stage-exited":
|
|
486
|
+
return ruleA.selectedStageId === ruleB.selectedStageId && ruleA.conditionExpression === ruleB.conditionExpression;
|
|
487
|
+
case "selected-tasks-completed": {
|
|
488
|
+
const aIds = ruleA.selectedTasksIds ?? [];
|
|
489
|
+
const bIds = ruleB.selectedTasksIds ?? [];
|
|
490
|
+
return aIds.length === bIds.length && aIds.every((id) => bIds.includes(id)) && ruleA.conditionExpression === ruleB.conditionExpression;
|
|
491
|
+
}
|
|
492
|
+
case "required-stages-completed":
|
|
493
|
+
return ruleA.conditionExpression === ruleB.conditionExpression;
|
|
494
|
+
case "required-tasks-completed":
|
|
495
|
+
return ruleA.conditionExpression === ruleB.conditionExpression;
|
|
496
|
+
case "user-selected-stage":
|
|
497
|
+
return ruleA.conditionExpression === ruleB.conditionExpression;
|
|
498
|
+
case "runs-sequentially":
|
|
499
|
+
return ruleA.conditionExpression === ruleB.conditionExpression;
|
|
500
|
+
default:
|
|
501
|
+
return false;
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
function stageLabel(node) {
|
|
507
|
+
return node.data.label ?? node.id;
|
|
508
|
+
}
|
|
509
|
+
function triggerLabel(node) {
|
|
510
|
+
return node.data.label ?? node.id;
|
|
511
|
+
}
|
|
512
|
+
function edgeLabel(edge) {
|
|
513
|
+
return edge.data.label ?? edge.id;
|
|
514
|
+
}
|
|
515
|
+
function flatTasks(stage) {
|
|
516
|
+
return (stage.data.tasks ?? []).flat();
|
|
517
|
+
}
|
|
518
|
+
function findRequiredFieldError(fields) {
|
|
519
|
+
for (const field of fields ?? []) {
|
|
520
|
+
if (field.required && (field.value === undefined || field.value === null || field.value === "")) {
|
|
521
|
+
return field.name;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return null;
|
|
525
|
+
}
|
|
526
|
+
function findRequiredFieldErrorInUiPath(uipath) {
|
|
527
|
+
return findRequiredFieldError(uipath?.context) ?? findRequiredFieldError(uipath?.inputs) ?? findRequiredFieldError(uipath?.outputs) ?? null;
|
|
528
|
+
}
|
|
529
|
+
function flatConditionRules(condition) {
|
|
530
|
+
return (condition.rules ?? []).flat();
|
|
531
|
+
}
|
|
532
|
+
function conditionLabel(condition) {
|
|
533
|
+
return condition.displayName ?? condition.id ?? "(unnamed)";
|
|
534
|
+
}
|
|
535
|
+
function hasCaseManagerStageWithTasks(schema) {
|
|
536
|
+
return schema.nodes.some((n) => {
|
|
537
|
+
if (n.type !== "case-management:Stage" && n.type !== "case-management:ExceptionStage") {
|
|
538
|
+
return false;
|
|
539
|
+
}
|
|
540
|
+
const data = n.data;
|
|
541
|
+
if (data.isCaseManagerStage !== true)
|
|
542
|
+
return false;
|
|
543
|
+
return (data.tasks ?? []).some((group) => Array.isArray(group) && group.length > 0);
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
function findFirstStageAfterTrigger(schema) {
|
|
547
|
+
const triggerIds = new Set(schema.nodes.filter((n) => n.type === "case-management:Trigger").map((n) => n.id));
|
|
548
|
+
return schema.edges.filter((e) => triggerIds.has(e.source)).map((e) => e.target)[0];
|
|
549
|
+
}
|
|
550
|
+
var VALID_RULE_TYPES = new Set([
|
|
551
|
+
"wait-for-connector",
|
|
552
|
+
"case-entered",
|
|
553
|
+
"selected-stage-completed",
|
|
554
|
+
"selected-stage-exited",
|
|
555
|
+
"selected-tasks-completed",
|
|
556
|
+
"current-stage-entered",
|
|
557
|
+
"adhoc",
|
|
558
|
+
"required-stages-completed",
|
|
559
|
+
"required-tasks-completed",
|
|
560
|
+
"user-selected-stage",
|
|
561
|
+
"runs-sequentially",
|
|
562
|
+
"stage-complete",
|
|
563
|
+
"condition",
|
|
564
|
+
"timer"
|
|
565
|
+
]);
|
|
566
|
+
var VALID_TASK_TYPES = new Set([
|
|
567
|
+
"process",
|
|
568
|
+
"action",
|
|
569
|
+
"agent",
|
|
570
|
+
"api-workflow",
|
|
571
|
+
"rpa",
|
|
572
|
+
"external-agent",
|
|
573
|
+
"wait-for-timer",
|
|
574
|
+
"wait-for-connector",
|
|
575
|
+
"execute-connector-activity",
|
|
576
|
+
"case-management"
|
|
577
|
+
]);
|
|
578
|
+
var VALID_NODE_TYPES = new Set([
|
|
579
|
+
"case-management:Trigger",
|
|
580
|
+
"case-management:Stage",
|
|
581
|
+
"case-management:ExceptionStage",
|
|
582
|
+
"case-management:StickyNote"
|
|
583
|
+
]);
|
|
584
|
+
var VALID_EDGE_TYPES = new Set([
|
|
585
|
+
"case-management:TriggerEdge",
|
|
586
|
+
"case-management:Edge"
|
|
587
|
+
]);
|
|
588
|
+
var VALID_EXIT_CONDITION_TYPES = new Set([
|
|
589
|
+
"exit-only",
|
|
590
|
+
"wait-for-user",
|
|
591
|
+
"return-to-origin"
|
|
592
|
+
]);
|
|
593
|
+
var VALID_SLA_UNITS = new Set(["min", "h", "d", "w", "m"]);
|
|
594
|
+
var VALID_ESCALATION_TRIGGER_TYPES = new Set([
|
|
595
|
+
"at-risk",
|
|
596
|
+
"sla-breached"
|
|
597
|
+
]);
|
|
598
|
+
var VALID_TRIGGER_SERVICE_TYPES = new Set([
|
|
599
|
+
"None",
|
|
600
|
+
"Intsvc.EventTrigger",
|
|
601
|
+
"Intsvc.TimerTrigger"
|
|
602
|
+
]);
|
|
603
|
+
|
|
604
|
+
class CaseValidateService {
|
|
605
|
+
fs;
|
|
606
|
+
taskValidators;
|
|
607
|
+
constructor(options = {}) {
|
|
608
|
+
this.fs = getFileSystem();
|
|
609
|
+
this.taskValidators = [
|
|
610
|
+
...defaultTaskValidators,
|
|
611
|
+
...options.extraTaskValidators ?? []
|
|
612
|
+
];
|
|
613
|
+
}
|
|
614
|
+
async validateFile(filePath, options = {}) {
|
|
615
|
+
const profile = options.profile ?? "full";
|
|
616
|
+
const absolutePath = this.fs.path.resolve(filePath);
|
|
617
|
+
if (!await this.fs.exists(absolutePath)) {
|
|
618
|
+
throw new Error(`File not found: ${absolutePath}`);
|
|
619
|
+
}
|
|
620
|
+
const stats = await this.fs.stat(absolutePath);
|
|
621
|
+
if (stats?.isDirectory()) {
|
|
622
|
+
throw new Error(`Expected a file but got a directory: ${absolutePath}`);
|
|
623
|
+
}
|
|
624
|
+
logger.info(`
|
|
625
|
+
Validating: ${absolutePath}
|
|
626
|
+
`);
|
|
627
|
+
const content = await this.fs.readFile(absolutePath, "utf-8");
|
|
628
|
+
if (!content) {
|
|
629
|
+
throw new Error(`File is empty: ${absolutePath}`);
|
|
630
|
+
}
|
|
631
|
+
const [parseError, schema] = catchError(() => JSON.parse(content));
|
|
632
|
+
if (parseError) {
|
|
633
|
+
return {
|
|
634
|
+
success: false,
|
|
635
|
+
filePath: absolutePath,
|
|
636
|
+
issues: [
|
|
637
|
+
{
|
|
638
|
+
code: "INVALID_JSON",
|
|
639
|
+
path: "(root)",
|
|
640
|
+
message: "File is not valid JSON",
|
|
641
|
+
severity: "error"
|
|
642
|
+
}
|
|
643
|
+
]
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
const rawSchema = schema;
|
|
647
|
+
if (rawSchema.root && !rawSchema.metadata) {
|
|
648
|
+
return {
|
|
649
|
+
success: false,
|
|
650
|
+
filePath: absolutePath,
|
|
651
|
+
issues: [
|
|
652
|
+
{
|
|
653
|
+
code: "INVALID_SCHEMA_VERSION",
|
|
654
|
+
path: "(root)",
|
|
655
|
+
message: "Detected a V19-shaped schema (top-level `root`). V20 flattens the top level and moves case-specific fields under `metadata` — please migrate the file to V20.",
|
|
656
|
+
severity: "error"
|
|
657
|
+
}
|
|
658
|
+
]
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
if (!schema.nodes || !schema.edges) {
|
|
662
|
+
return {
|
|
663
|
+
success: false,
|
|
664
|
+
filePath: absolutePath,
|
|
665
|
+
issues: [
|
|
666
|
+
{
|
|
667
|
+
code: "INVALID_SCHEMA",
|
|
668
|
+
path: "(root)",
|
|
669
|
+
message: "Missing required top-level fields: nodes, edges",
|
|
670
|
+
severity: "error"
|
|
671
|
+
}
|
|
672
|
+
]
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
const issues = schema.metadata?.allowAdhocOptionalStageTasks ? [
|
|
676
|
+
...this.validateMetadata(schema, profile),
|
|
677
|
+
...this.validateEnums(schema, profile),
|
|
678
|
+
...this.validateTriggers(schema, profile),
|
|
679
|
+
...this.validateAdhocNodes(schema, profile),
|
|
680
|
+
...checksTaskContent(profile) ? this.validateOutputVariableUniqueness(schema) : [],
|
|
681
|
+
...checksTaskContent(profile) ? this.runTaskValidators(schema, profile) : []
|
|
682
|
+
] : [
|
|
683
|
+
...this.validateMetadata(schema, profile),
|
|
684
|
+
...this.validateEnums(schema, profile),
|
|
685
|
+
...this.validateStructure(schema),
|
|
686
|
+
...this.validateTriggers(schema, profile),
|
|
687
|
+
...this.validatePrimaryStages(schema, profile),
|
|
688
|
+
...this.validateSecondaryStages(schema, profile),
|
|
689
|
+
...this.validateEdges(schema, profile),
|
|
690
|
+
...profile !== "skeleton" ? this.validateOverallCase(schema) : [],
|
|
691
|
+
...checksTaskContent(profile) ? this.validateOutputVariableUniqueness(schema) : [],
|
|
692
|
+
...checksTaskContent(profile) ? this.runTaskValidators(schema, profile) : []
|
|
693
|
+
];
|
|
694
|
+
return {
|
|
695
|
+
success: issues.filter((i) => i.severity === "error").length === 0,
|
|
696
|
+
filePath: absolutePath,
|
|
697
|
+
issues
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
runSupplementalRules(parsed, profile) {
|
|
701
|
+
const issues = [];
|
|
702
|
+
const name = parsed.name;
|
|
703
|
+
if (typeof name !== "string" || name.trim() === "") {
|
|
704
|
+
issues.push({
|
|
705
|
+
code: "CASE_MGMT_ROOT_NAME_MISSING",
|
|
706
|
+
path: "name",
|
|
707
|
+
severity: "error",
|
|
708
|
+
message: "Case name is missing or blank. Deployment keys the case " + "definition by name, so a whitespace-only name yields an " + "unaddressable case."
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
const nodes = Array.isArray(parsed.nodes) ? parsed.nodes : [];
|
|
712
|
+
const seen = new Set;
|
|
713
|
+
const reported = new Set;
|
|
714
|
+
for (const node of nodes) {
|
|
715
|
+
const id = node.id;
|
|
716
|
+
if (typeof id !== "string" || id === "")
|
|
717
|
+
continue;
|
|
718
|
+
if (seen.has(id) && !reported.has(id)) {
|
|
719
|
+
reported.add(id);
|
|
720
|
+
issues.push({
|
|
721
|
+
code: "CASE_MGMT_NODE_ID_DUPLICATE",
|
|
722
|
+
path: `nodes[${id}]`,
|
|
723
|
+
severity: "error",
|
|
724
|
+
message: `More than one node uses the id "${id}". Node ids are ` + "the reference target for edges, rules and task " + "bindings, so a duplicate makes every reference to it " + "ambiguous at deploy time."
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
seen.add(id);
|
|
728
|
+
}
|
|
729
|
+
if (profile !== "skeleton") {
|
|
730
|
+
issues.push(...this.checkEntryRuleSlots(nodes, {
|
|
731
|
+
includeTaskSlots: checksTaskContent(profile)
|
|
732
|
+
}));
|
|
733
|
+
}
|
|
734
|
+
return issues;
|
|
735
|
+
}
|
|
736
|
+
checkEntryRuleSlots(nodes, options) {
|
|
737
|
+
const issues = [];
|
|
738
|
+
const knownSomewhere = new Set([
|
|
739
|
+
...VALID_TASK_ENTRY_RULE_TYPES,
|
|
740
|
+
...VALID_STAGE_ENTRY_RULE_TYPES,
|
|
741
|
+
...VALID_STAGE_EXIT_RULE_TYPES,
|
|
742
|
+
...VALID_STAGE_COMPLETION_RULE_TYPES,
|
|
743
|
+
...VALID_CASE_EXIT_RULE_TYPES,
|
|
744
|
+
...VALID_CASE_COMPLETION_RULE_TYPES
|
|
745
|
+
]);
|
|
746
|
+
const check = (conditions, allowed, slotLabel, path) => {
|
|
747
|
+
if (!Array.isArray(conditions))
|
|
748
|
+
return;
|
|
749
|
+
for (const condition of conditions) {
|
|
750
|
+
const groups = condition?.rules;
|
|
751
|
+
if (!Array.isArray(groups))
|
|
752
|
+
continue;
|
|
753
|
+
for (const group of groups) {
|
|
754
|
+
if (!Array.isArray(group))
|
|
755
|
+
continue;
|
|
756
|
+
for (const entry of group) {
|
|
757
|
+
const rule = entry?.rule;
|
|
758
|
+
if (typeof rule !== "string")
|
|
759
|
+
continue;
|
|
760
|
+
if (allowed.includes(rule))
|
|
761
|
+
continue;
|
|
762
|
+
if (!knownSomewhere.has(rule))
|
|
763
|
+
continue;
|
|
764
|
+
issues.push({
|
|
765
|
+
code: "CASE_MGMT_RULE_SLOT_MISMATCH",
|
|
766
|
+
path,
|
|
767
|
+
severity: "error",
|
|
768
|
+
message: `Rule "${rule}" does not belong in a ${slotLabel}. ` + `Allowed there: ${allowed.join(", ")}. The rule is ` + "valid in a different slot, so the plan deploys and " + "the scheduler evaluates it against the wrong " + "context — the condition does not mean what the " + "author wrote."
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
};
|
|
774
|
+
for (const node of nodes) {
|
|
775
|
+
const data = node.data ?? {};
|
|
776
|
+
const nodeId = typeof node.id === "string" ? node.id : "(node)";
|
|
777
|
+
check(data.entryConditions, VALID_STAGE_ENTRY_RULE_TYPES, "stage entry condition", `nodes[${nodeId}].data.entryConditions`);
|
|
778
|
+
const lanes = options.includeTaskSlots && Array.isArray(data.tasks) ? data.tasks : [];
|
|
779
|
+
for (const lane of lanes) {
|
|
780
|
+
if (!Array.isArray(lane))
|
|
781
|
+
continue;
|
|
782
|
+
for (const task of lane) {
|
|
783
|
+
const taskId = typeof task?.id === "string" ? task.id : "(task)";
|
|
784
|
+
check(task?.entryConditions, VALID_TASK_ENTRY_RULE_TYPES, "task entry condition", `nodes[${nodeId}].data.tasks[${taskId}].entryConditions`);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
return issues;
|
|
789
|
+
}
|
|
790
|
+
async validateFileUsingSDK(filePath, options = {}) {
|
|
791
|
+
const profile = options.profile ?? "full";
|
|
792
|
+
const absolutePath = this.fs.path.resolve(filePath);
|
|
793
|
+
if (!await this.fs.exists(absolutePath)) {
|
|
794
|
+
throw new Error(`File not found: ${absolutePath}`);
|
|
795
|
+
}
|
|
796
|
+
const stats = await this.fs.stat(absolutePath);
|
|
797
|
+
if (stats?.isDirectory()) {
|
|
798
|
+
throw new Error(`Expected a file but got a directory: ${absolutePath}`);
|
|
799
|
+
}
|
|
800
|
+
logger.info(`
|
|
801
|
+
Validating: ${absolutePath}
|
|
802
|
+
`);
|
|
803
|
+
const content = await this.fs.readFile(absolutePath, "utf-8");
|
|
804
|
+
if (!content) {
|
|
805
|
+
throw new Error(`File is empty: ${absolutePath}`);
|
|
806
|
+
}
|
|
807
|
+
const [parseError, parsed] = catchError(() => JSON.parse(content));
|
|
808
|
+
if (parseError) {
|
|
809
|
+
return {
|
|
810
|
+
success: false,
|
|
811
|
+
filePath: absolutePath,
|
|
812
|
+
issues: [
|
|
813
|
+
{
|
|
814
|
+
code: "INVALID_JSON",
|
|
815
|
+
path: "(root)",
|
|
816
|
+
message: "File is not valid JSON",
|
|
817
|
+
severity: "error"
|
|
818
|
+
}
|
|
819
|
+
]
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
const issues = [];
|
|
823
|
+
const { validateCaseCanvasJson, validateCaseDiskJsonSchema } = await loadCaseSchemaAsync();
|
|
824
|
+
const [diskCallError, diskResultRaw] = catchError(() => validateCaseDiskJsonSchema(parsed));
|
|
825
|
+
if (diskCallError) {
|
|
826
|
+
issues.push({
|
|
827
|
+
code: "CASE_MGMT_DISK_SCHEMA_VALIDATION_FAILED",
|
|
828
|
+
path: "(root)",
|
|
829
|
+
message: diskCallError.message,
|
|
830
|
+
severity: "error"
|
|
831
|
+
});
|
|
832
|
+
return {
|
|
833
|
+
success: false,
|
|
834
|
+
filePath: absolutePath,
|
|
835
|
+
issues
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
const diskResult = diskResultRaw;
|
|
839
|
+
for (const issue2 of diskResult.issues ?? []) {
|
|
840
|
+
issues.push({
|
|
841
|
+
code: "CASE_MGMT_DISK_SCHEMA_INVALID",
|
|
842
|
+
path: issue2.path && issue2.path !== "" ? issue2.path : "(root)",
|
|
843
|
+
message: issue2.message,
|
|
844
|
+
severity: "error"
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
if (diskResult.migrationError) {
|
|
848
|
+
issues.push({
|
|
849
|
+
code: "CASE_MGMT_MIGRATION_FAILED",
|
|
850
|
+
path: "(root)",
|
|
851
|
+
message: diskResult.migrationError,
|
|
852
|
+
severity: "error"
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
if (diskResult.valid && profile !== "skeleton") {
|
|
856
|
+
const [canvasCallError, canvasResultRaw] = catchError(() => validateCaseCanvasJson(parsed));
|
|
857
|
+
if (canvasCallError) {
|
|
858
|
+
issues.push({
|
|
859
|
+
code: "CASE_MGMT_CANVAS_VALIDATION_FAILED",
|
|
860
|
+
path: "(root)",
|
|
861
|
+
message: canvasCallError.message,
|
|
862
|
+
severity: "error"
|
|
863
|
+
});
|
|
864
|
+
} else {
|
|
865
|
+
const canvasIssues = canvasResultRaw ?? [];
|
|
866
|
+
for (const ce of canvasIssues) {
|
|
867
|
+
if (profile === "skeleton-v2" && isTaskScopedCanvasIssue(ce)) {
|
|
868
|
+
continue;
|
|
869
|
+
}
|
|
870
|
+
issues.push({
|
|
871
|
+
code: ce.code,
|
|
872
|
+
path: ce.elementId ? `nodes[${ce.elementId}]` : "(root)",
|
|
873
|
+
message: ce.message,
|
|
874
|
+
severity: mapCanvasSeverity(ce.severity)
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
if (diskResult.valid && checksTaskContent(profile)) {
|
|
880
|
+
issues.push(...this.runTaskValidators(parsed, profile));
|
|
881
|
+
}
|
|
882
|
+
if (diskResult.valid) {
|
|
883
|
+
issues.push(...this.runSupplementalRules(parsed, profile));
|
|
884
|
+
}
|
|
885
|
+
if (diskResult.valid && profile === "strict") {
|
|
886
|
+
const { runStrictCaseRules } = await import("./strict-case-rules-303vjvas.js");
|
|
887
|
+
issues.push(...runStrictCaseRules(parsed));
|
|
888
|
+
if (options.sddPath !== undefined) {
|
|
889
|
+
const sddPath = this.fs.path.resolve(options.sddPath);
|
|
890
|
+
if (await this.fs.exists(sddPath)) {
|
|
891
|
+
const sddText = await this.fs.readFile(sddPath, "utf-8") ?? "";
|
|
892
|
+
const { auditCaseplanAgainstSdd } = await import("./strict-sdd-audit-qdmc9e1y.js");
|
|
893
|
+
issues.push(...auditCaseplanAgainstSdd(parsed, sddText, this.fs.path.basename(sddPath)));
|
|
894
|
+
} else {
|
|
895
|
+
issues.push({
|
|
896
|
+
code: "STRICT_SDD_FILE_NOT_FOUND",
|
|
897
|
+
path: options.sddPath,
|
|
898
|
+
severity: "error",
|
|
899
|
+
message: `SDD not found at "${sddPath}". --sdd audits the caseplan against the SDD that specifies it, so there is nothing to audit against.`
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
return {
|
|
905
|
+
success: issues.filter((i) => i.severity === "error").length === 0,
|
|
906
|
+
filePath: absolutePath,
|
|
907
|
+
issues
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
validateMetadata(schema, profile) {
|
|
911
|
+
const issues = [];
|
|
912
|
+
const metadata = schema.metadata ?? {};
|
|
913
|
+
const rawSchema = schema;
|
|
914
|
+
const rawMetadata = metadata;
|
|
915
|
+
if (!schema.name || schema.name.trim() === "") {
|
|
916
|
+
issues.push({
|
|
917
|
+
code: "CASE_MGMT_ROOT_NAME_MISSING",
|
|
918
|
+
path: "name",
|
|
919
|
+
message: "Case definition name is missing",
|
|
920
|
+
severity: "error"
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
const versionMajor = parseSemverMajor(schema.version);
|
|
924
|
+
if (versionMajor === null) {
|
|
925
|
+
issues.push({
|
|
926
|
+
code: "CASE_MGMT_INVALID_VERSION",
|
|
927
|
+
path: "version",
|
|
928
|
+
message: `Schema version "${String(schema.version ?? "(missing)")}" is not a valid semver — expected "<major>.<minor>.<patch>" with major >= 20`,
|
|
929
|
+
severity: "error"
|
|
930
|
+
});
|
|
931
|
+
} else if (versionMajor < 20) {
|
|
932
|
+
issues.push({
|
|
933
|
+
code: "CASE_MGMT_INVALID_VERSION",
|
|
934
|
+
path: "version",
|
|
935
|
+
message: `Schema version "${String(schema.version)}" predates V20 — this validator requires major version >= 20`,
|
|
936
|
+
severity: "error"
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
if (!schema.id || !/^case-[A-Za-z0-9]{10}$/.test(schema.id)) {
|
|
940
|
+
issues.push({
|
|
941
|
+
code: "CASE_MGMT_INVALID_ID",
|
|
942
|
+
path: "id",
|
|
943
|
+
message: `Case id "${String(schema.id ?? "(missing)")}" does not match the V20 format "case-<10 alphanumeric chars>"`,
|
|
944
|
+
severity: "error"
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
if (!metadata.caseIdentifier || metadata.caseIdentifier.trim() === "") {
|
|
948
|
+
issues.push({
|
|
949
|
+
code: "CASE_MGMT_ROOT_CASE_ID_MISSING",
|
|
950
|
+
path: "metadata.caseIdentifier",
|
|
951
|
+
message: "Case identifier is missing",
|
|
952
|
+
severity: "error"
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
if (rawSchema.caseExitConditions || rawSchema.caseExitRules) {
|
|
956
|
+
issues.push({
|
|
957
|
+
code: "CASE_MGMT_CASE_EXIT_CONDITIONS_MISPLACED",
|
|
958
|
+
path: rawSchema.caseExitRules ? "caseExitRules" : "caseExitConditions",
|
|
959
|
+
message: rawSchema.caseExitRules ? "caseExitRules is at the top level of the JSON file but should be inside metadata (metadata.caseExitRules)" : "caseExitConditions is at the top level and was renamed to caseExitRules in V20 — move it under metadata.caseExitRules",
|
|
960
|
+
severity: "error"
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
if (rawMetadata.caseExitConditions && !metadata.caseExitRules?.length) {
|
|
964
|
+
issues.push({
|
|
965
|
+
code: "CASE_MGMT_CASE_EXIT_CONDITIONS_MISPLACED",
|
|
966
|
+
path: "metadata.caseExitConditions",
|
|
967
|
+
message: "metadata.caseExitConditions was renamed to metadata.caseExitRules in V20",
|
|
968
|
+
severity: "error"
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
if (profile === "skeleton")
|
|
972
|
+
return issues;
|
|
973
|
+
const rootSlaRules = metadata.slaRules ?? [];
|
|
974
|
+
const stageHoursTotal = schema.nodes.filter((n) => n.type === "case-management:Stage").reduce((sum, s) => sum + slaToHours(s.data.sla), 0);
|
|
975
|
+
for (let i = 0;i < rootSlaRules.length; i++) {
|
|
976
|
+
const rule = rootSlaRules[i];
|
|
977
|
+
const hasEscalations = rule.escalationRule != null && rule.escalationRule.length > 0;
|
|
978
|
+
const hasValue = rule.count != null && rule.count > 0;
|
|
979
|
+
if (hasEscalations && !hasValue) {
|
|
980
|
+
issues.push({
|
|
981
|
+
code: "CASE_MGMT_ROOT_NO_SLA_ESCALATIONS",
|
|
982
|
+
path: `metadata.slaRules[${i}]`,
|
|
983
|
+
message: "Root SLA rule has escalation rules but no SLA value is set",
|
|
984
|
+
severity: "warning"
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
if (hasEscalations) {
|
|
988
|
+
const missingRecipients = (rule.escalationRule ?? []).some((e) => !e.action?.recipients || e.action.recipients.length === 0);
|
|
989
|
+
if (missingRecipients) {
|
|
990
|
+
issues.push({
|
|
991
|
+
code: "CASE_MGMT_ROOT_ESCALATION_RECIPIENTS_MISSING",
|
|
992
|
+
path: `metadata.slaRules[${i}].escalationRule`,
|
|
993
|
+
message: "Root SLA rule has an escalation rule with no recipients",
|
|
994
|
+
severity: "warning"
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
const ruleHours = slaToHours(rule);
|
|
999
|
+
if (ruleHours > 0 && stageHoursTotal > 0 && ruleHours < stageHoursTotal) {
|
|
1000
|
+
issues.push({
|
|
1001
|
+
code: "CASE_MGMT_ROOT_SLA_LESS_THAN_NODES",
|
|
1002
|
+
path: `metadata.slaRules[${i}]`,
|
|
1003
|
+
message: `Root SLA rule (${ruleHours}h) is less than the sum of all stage SLAs (${stageHoursTotal}h)`,
|
|
1004
|
+
severity: "warning"
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
const caseExitRules = metadata.caseExitRules ?? [];
|
|
1009
|
+
const completeConditions = caseExitRules.filter((c) => c.marksCaseComplete);
|
|
1010
|
+
if (completeConditions.length === 0 && metadata.allowAdhocOptionalStageTasks !== true) {
|
|
1011
|
+
issues.push({
|
|
1012
|
+
code: "CASE_MGMT_ROOT_CASE_EXIT_CONDITIONS_MISSING",
|
|
1013
|
+
path: "metadata.caseExitRules",
|
|
1014
|
+
message: "The case has no exit condition that marks the case as complete",
|
|
1015
|
+
severity: "error"
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
for (const condition of caseExitRules) {
|
|
1019
|
+
if (!condition.rules || condition.rules.length === 0) {
|
|
1020
|
+
issues.push({
|
|
1021
|
+
code: "CASE_MGMT_CASE_EXIT_CONDITION_NO_RULES",
|
|
1022
|
+
path: "metadata.caseExitRules",
|
|
1023
|
+
message: "A case exit condition has no rules",
|
|
1024
|
+
severity: "error"
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
issues.push(...this.validateConditionsBattery({
|
|
1029
|
+
conditions: caseExitRules,
|
|
1030
|
+
schema,
|
|
1031
|
+
currentStage: undefined,
|
|
1032
|
+
path: "metadata.caseExitRules",
|
|
1033
|
+
elementLabel: "Case",
|
|
1034
|
+
duplicateCode: "CASE_MGMT_ROOT_CASE_CONDITION_DUPLICATE",
|
|
1035
|
+
profile
|
|
1036
|
+
}));
|
|
1037
|
+
return issues;
|
|
1038
|
+
}
|
|
1039
|
+
validateEnums(schema, profile) {
|
|
1040
|
+
const issues = [];
|
|
1041
|
+
for (const node of schema.nodes) {
|
|
1042
|
+
if (!VALID_NODE_TYPES.has(node.type)) {
|
|
1043
|
+
issues.push({
|
|
1044
|
+
code: "INVALID_ENUM_VALUE",
|
|
1045
|
+
path: `nodes[${node.id}].type`,
|
|
1046
|
+
message: `Node "${node.id}" has invalid type "${node.type}". Expected one of: ${[...VALID_NODE_TYPES].join(", ")}`,
|
|
1047
|
+
severity: "error"
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
for (const edge of schema.edges) {
|
|
1052
|
+
if (!VALID_EDGE_TYPES.has(edge.type)) {
|
|
1053
|
+
issues.push({
|
|
1054
|
+
code: "INVALID_ENUM_VALUE",
|
|
1055
|
+
path: `edges[${edge.id}].type`,
|
|
1056
|
+
message: `Edge "${edge.id}" has invalid type "${edge.type}". Expected one of: ${[...VALID_EDGE_TYPES].join(", ")}`,
|
|
1057
|
+
severity: "error"
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
const triggers = schema.nodes.filter((n) => n.type === "case-management:Trigger");
|
|
1062
|
+
for (const trigger of triggers) {
|
|
1063
|
+
const serviceType = trigger.data.uipath?.serviceType;
|
|
1064
|
+
if (serviceType != null && !VALID_TRIGGER_SERVICE_TYPES.has(serviceType)) {
|
|
1065
|
+
issues.push({
|
|
1066
|
+
code: "INVALID_ENUM_VALUE",
|
|
1067
|
+
path: `nodes[${trigger.id}].data.uipath.serviceType`,
|
|
1068
|
+
message: `Trigger "${triggerLabel(trigger)}" has invalid serviceType "${serviceType}". Expected one of: ${[...VALID_TRIGGER_SERVICE_TYPES].join(", ")}`,
|
|
1069
|
+
severity: "error"
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
const allStages = schema.nodes.filter((n) => n.type === "case-management:Stage" || n.type === "case-management:ExceptionStage");
|
|
1074
|
+
for (const stage of allStages) {
|
|
1075
|
+
const name = stageLabel(stage);
|
|
1076
|
+
const path = `nodes[${stage.id}]`;
|
|
1077
|
+
for (const task of flatTasks(stage)) {
|
|
1078
|
+
if (!VALID_TASK_TYPES.has(task.type)) {
|
|
1079
|
+
issues.push({
|
|
1080
|
+
code: "INVALID_ENUM_VALUE",
|
|
1081
|
+
path,
|
|
1082
|
+
message: `Stage "${name}" has a task with invalid type "${task.type}". Expected one of: ${[...VALID_TASK_TYPES].join(", ")}`,
|
|
1083
|
+
severity: "error"
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
if (profile === "skeleton")
|
|
1088
|
+
continue;
|
|
1089
|
+
const slaUnit = stage.data.sla?.unit;
|
|
1090
|
+
if (slaUnit != null && !VALID_SLA_UNITS.has(slaUnit)) {
|
|
1091
|
+
issues.push({
|
|
1092
|
+
code: "INVALID_ENUM_VALUE",
|
|
1093
|
+
path: `${path}.data.sla.unit`,
|
|
1094
|
+
message: `Stage "${name}" has invalid SLA unit "${slaUnit}". Expected one of: ${[...VALID_SLA_UNITS].join(", ")}`,
|
|
1095
|
+
severity: "error"
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
issues.push(...this.validateEscalationTriggerTypes(stage.data.sla?.escalationRule, path, `Stage "${name}"`));
|
|
1099
|
+
for (const ec of stage.data.exitConditions ?? []) {
|
|
1100
|
+
if (ec.type != null && !VALID_EXIT_CONDITION_TYPES.has(ec.type)) {
|
|
1101
|
+
issues.push({
|
|
1102
|
+
code: "INVALID_ENUM_VALUE",
|
|
1103
|
+
path: `${path}.exitConditions`,
|
|
1104
|
+
message: `Stage "${name}" has an exit condition with invalid type "${ec.type}". Expected one of: ${[...VALID_EXIT_CONDITION_TYPES].join(", ")}`,
|
|
1105
|
+
severity: "error"
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
for (const ec of stage.data.exitConditions ?? []) {
|
|
1110
|
+
issues.push(...this.validateRuleTypes(ec.rules, `${path}.exitConditions`, `Stage "${name}" exit condition`));
|
|
1111
|
+
}
|
|
1112
|
+
for (const ec of stage.data.entryConditions ?? []) {
|
|
1113
|
+
issues.push(...this.validateRuleTypes(ec.rules, `${path}.entryConditions`, `Stage "${name}" entry condition`));
|
|
1114
|
+
}
|
|
1115
|
+
for (const task of flatTasks(stage)) {
|
|
1116
|
+
for (const ec of task.entryConditions ?? []) {
|
|
1117
|
+
issues.push(...this.validateRuleTypes(ec.rules, path, `Stage "${name}" task entry condition`));
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
if (profile === "skeleton")
|
|
1122
|
+
return issues;
|
|
1123
|
+
for (const slaRule of schema.metadata?.slaRules ?? []) {
|
|
1124
|
+
if (slaRule.unit != null && !VALID_SLA_UNITS.has(slaRule.unit)) {
|
|
1125
|
+
issues.push({
|
|
1126
|
+
code: "INVALID_ENUM_VALUE",
|
|
1127
|
+
path: "metadata.slaRules",
|
|
1128
|
+
message: `Root SLA rule has invalid unit "${slaRule.unit}". Expected one of: ${[...VALID_SLA_UNITS].join(", ")}`,
|
|
1129
|
+
severity: "error"
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
issues.push(...this.validateEscalationTriggerTypes(slaRule.escalationRule, "metadata.slaRules", "Root SLA rule"));
|
|
1133
|
+
}
|
|
1134
|
+
for (const cer of schema.metadata?.caseExitRules ?? []) {
|
|
1135
|
+
issues.push(...this.validateRuleTypes(cer.rules, "metadata.caseExitRules", "Case exit condition"));
|
|
1136
|
+
}
|
|
1137
|
+
for (const edge of schema.edges) {
|
|
1138
|
+
if (edge.type !== "case-management:Edge")
|
|
1139
|
+
continue;
|
|
1140
|
+
const caseEdge = edge;
|
|
1141
|
+
issues.push(...this.validateRuleTypes(caseEdge.data.rules, `edges[${edge.id}]`, `Edge "${edgeLabel(edge)}"`));
|
|
1142
|
+
}
|
|
1143
|
+
return issues;
|
|
1144
|
+
}
|
|
1145
|
+
validateRuleTypes(rules, path, context) {
|
|
1146
|
+
const issues = [];
|
|
1147
|
+
for (const andGroup of rules ?? []) {
|
|
1148
|
+
for (const rule of andGroup) {
|
|
1149
|
+
if (!VALID_RULE_TYPES.has(rule.rule)) {
|
|
1150
|
+
issues.push({
|
|
1151
|
+
code: "INVALID_ENUM_VALUE",
|
|
1152
|
+
path,
|
|
1153
|
+
message: `${context} has a rule with invalid type "${rule.rule}". Expected one of: ${[...VALID_RULE_TYPES].join(", ")}`,
|
|
1154
|
+
severity: "error"
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
return issues;
|
|
1160
|
+
}
|
|
1161
|
+
validateEscalationTriggerTypes(escalationRules, path, context) {
|
|
1162
|
+
const issues = [];
|
|
1163
|
+
for (const esc of escalationRules ?? []) {
|
|
1164
|
+
const triggerType = esc.triggerInfo?.type;
|
|
1165
|
+
if (triggerType != null && !VALID_ESCALATION_TRIGGER_TYPES.has(triggerType)) {
|
|
1166
|
+
issues.push({
|
|
1167
|
+
code: "INVALID_ENUM_VALUE",
|
|
1168
|
+
path,
|
|
1169
|
+
message: `${context} has an escalation with invalid triggerInfo.type "${triggerType}". Expected one of: ${[...VALID_ESCALATION_TRIGGER_TYPES].join(", ")}`,
|
|
1170
|
+
severity: "error"
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
return issues;
|
|
1175
|
+
}
|
|
1176
|
+
validateRuleContents(rule, condition, currentStage, schema, path, elementLabel, profile) {
|
|
1177
|
+
const issues = [];
|
|
1178
|
+
const condName = conditionLabel(condition);
|
|
1179
|
+
switch (rule.rule) {
|
|
1180
|
+
case "selected-stage-completed":
|
|
1181
|
+
case "selected-stage-exited": {
|
|
1182
|
+
if (!rule.selectedStageId) {
|
|
1183
|
+
issues.push({
|
|
1184
|
+
code: "CASE_MGMT_ROOT_CASE_CONDITION_RULE_SELECTED_STAGE_ID_MISSING",
|
|
1185
|
+
path,
|
|
1186
|
+
message: `${elementLabel} condition "${condName}" has a "${rule.rule}" rule with no selectedStageId`,
|
|
1187
|
+
severity: "error"
|
|
1188
|
+
});
|
|
1189
|
+
break;
|
|
1190
|
+
}
|
|
1191
|
+
const target = schema.nodes.find((n) => n.id === rule.selectedStageId);
|
|
1192
|
+
if (!target || target.type !== "case-management:Stage" && target.type !== "case-management:ExceptionStage") {
|
|
1193
|
+
issues.push({
|
|
1194
|
+
code: "CASE_MGMT_ROOT_CASE_CONDITION_RULE_SELECTED_STAGE_MISSING",
|
|
1195
|
+
path,
|
|
1196
|
+
message: `${elementLabel} condition "${condName}" references a non-existent stage "${rule.selectedStageId}"`,
|
|
1197
|
+
severity: "error"
|
|
1198
|
+
});
|
|
1199
|
+
break;
|
|
1200
|
+
}
|
|
1201
|
+
const targetStage = target;
|
|
1202
|
+
const targetName = stageLabel(targetStage);
|
|
1203
|
+
const expectedMarksComplete = rule.rule === "selected-stage-completed";
|
|
1204
|
+
const matchingExit = (targetStage.data.exitConditions ?? []).find((c) => (c.marksStageComplete ?? false) === expectedMarksComplete);
|
|
1205
|
+
if (!matchingExit) {
|
|
1206
|
+
issues.push({
|
|
1207
|
+
code: expectedMarksComplete ? "CASE_MGMT_ROOT_CASE_COMPLETE_CONDITION_RULE_SELECTED_STAGE_ID_NOT_FOUND" : "CASE_MGMT_ROOT_CASE_EXIT_CONDITION_RULE_SELECTED_STAGE_ID_NOT_FOUND",
|
|
1208
|
+
path,
|
|
1209
|
+
message: expectedMarksComplete ? `${elementLabel} condition "${condName}" references stage "${targetName}", which has no exit condition that marks the stage complete` : `${elementLabel} condition "${condName}" references stage "${targetName}", which has no non-completing exit condition`,
|
|
1210
|
+
severity: "error"
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
break;
|
|
1214
|
+
}
|
|
1215
|
+
case "selected-tasks-completed": {
|
|
1216
|
+
if (profile !== "full")
|
|
1217
|
+
break;
|
|
1218
|
+
if (!rule.selectedTasksIds || rule.selectedTasksIds.length === 0) {
|
|
1219
|
+
issues.push({
|
|
1220
|
+
code: "CASE_MGMT_ROOT_CASE_CONDITION_RULE_SELECTED_TASK_IDS_MISSING",
|
|
1221
|
+
path,
|
|
1222
|
+
message: `${elementLabel} condition "${condName}" has a "selected-tasks-completed" rule with no selectedTasksIds`,
|
|
1223
|
+
severity: "error"
|
|
1224
|
+
});
|
|
1225
|
+
break;
|
|
1226
|
+
}
|
|
1227
|
+
if (currentStage) {
|
|
1228
|
+
const allTaskIds = new Set(flatTasks(currentStage).map((t) => t.id).filter((id) => Boolean(id)));
|
|
1229
|
+
const missing = rule.selectedTasksIds.find((id) => !allTaskIds.has(id));
|
|
1230
|
+
if (missing) {
|
|
1231
|
+
issues.push({
|
|
1232
|
+
code: "CASE_MGMT_ROOT_CASE_CONDITION_RULE_SELECTED_TASK_MISSING",
|
|
1233
|
+
path,
|
|
1234
|
+
message: `${elementLabel} condition "${condName}" references task "${missing}" which is not in this stage`,
|
|
1235
|
+
severity: "error"
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
break;
|
|
1240
|
+
}
|
|
1241
|
+
case "required-stages-completed": {
|
|
1242
|
+
const allPrimaryStages = schema.nodes.filter((n) => n.type === "case-management:Stage" && n.data.isCaseManagerStage !== true);
|
|
1243
|
+
if (allPrimaryStages.length > 0) {
|
|
1244
|
+
const hasRequired = allPrimaryStages.some((s) => s.data.isRequired === true);
|
|
1245
|
+
if (!hasRequired) {
|
|
1246
|
+
issues.push({
|
|
1247
|
+
code: "CASE_MGMT_ROOT_CASE_CONDITION_RULE_NO_REQUIRED_STAGE",
|
|
1248
|
+
path,
|
|
1249
|
+
message: `${elementLabel} condition "${condName}" has a "required-stages-completed" rule but no primary stage is marked isRequired`,
|
|
1250
|
+
severity: "error"
|
|
1251
|
+
});
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
break;
|
|
1255
|
+
}
|
|
1256
|
+
case "required-tasks-completed": {
|
|
1257
|
+
if (profile !== "full")
|
|
1258
|
+
break;
|
|
1259
|
+
if (currentStage) {
|
|
1260
|
+
const tasks = flatTasks(currentStage);
|
|
1261
|
+
if (tasks.length > 0) {
|
|
1262
|
+
const hasRequired = tasks.some((t) => t.isRequired === true);
|
|
1263
|
+
if (!hasRequired) {
|
|
1264
|
+
issues.push({
|
|
1265
|
+
code: "CASE_MGMT_ROOT_CASE_CONDITION_RULE_NO_REQUIRED_TASK",
|
|
1266
|
+
path,
|
|
1267
|
+
message: `${elementLabel} condition "${condName}" has a "required-tasks-completed" rule but no task in this stage is marked isRequired`,
|
|
1268
|
+
severity: "error"
|
|
1269
|
+
});
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
break;
|
|
1274
|
+
}
|
|
1275
|
+
case "user-selected-stage": {
|
|
1276
|
+
const otherStages = schema.nodes.filter((n) => (n.type === "case-management:Stage" || n.type === "case-management:ExceptionStage") && n.id !== currentStage?.id);
|
|
1277
|
+
const hasWaitForUser = otherStages.some((s) => s.data.exitConditions?.some((c) => c.type === "wait-for-user"));
|
|
1278
|
+
if (!hasWaitForUser) {
|
|
1279
|
+
issues.push({
|
|
1280
|
+
code: "CASE_MGMT_ROOT_CASE_CONDITION_RULE_USER_SELECTED_STAGE_MISSING",
|
|
1281
|
+
path,
|
|
1282
|
+
message: `${elementLabel} condition "${condName}" uses "user-selected-stage" but no other stage has a wait-for-user exit condition`,
|
|
1283
|
+
severity: "error"
|
|
1284
|
+
});
|
|
1285
|
+
}
|
|
1286
|
+
break;
|
|
1287
|
+
}
|
|
1288
|
+
default:
|
|
1289
|
+
break;
|
|
1290
|
+
}
|
|
1291
|
+
return issues;
|
|
1292
|
+
}
|
|
1293
|
+
validateConditionDuplicates(conditions, duplicateCode, path, duplicateMessage) {
|
|
1294
|
+
const issues = [];
|
|
1295
|
+
for (let i = 0;i < conditions.length; i++) {
|
|
1296
|
+
for (let j = i + 1;j < conditions.length; j++) {
|
|
1297
|
+
const a = conditions[i];
|
|
1298
|
+
const b = conditions[j];
|
|
1299
|
+
if (a.rules && b.rules && rulesMatch(a.rules, b.rules)) {
|
|
1300
|
+
issues.push({
|
|
1301
|
+
code: duplicateCode,
|
|
1302
|
+
path,
|
|
1303
|
+
message: duplicateMessage(a, b),
|
|
1304
|
+
severity: "error"
|
|
1305
|
+
});
|
|
1306
|
+
break;
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
return issues;
|
|
1311
|
+
}
|
|
1312
|
+
validateConditionTooSimilar(conditions, currentStage, schema, path, elementLabel, profile) {
|
|
1313
|
+
const issues = [];
|
|
1314
|
+
const requiredStageIds = schema.nodes.filter((n) => n.type === "case-management:Stage" && n.data.isRequired === true && n.data.isCaseManagerStage !== true).map((n) => n.id);
|
|
1315
|
+
const requiredTaskIds = currentStage ? flatTasks(currentStage).filter((t) => t.isRequired === true).map((t) => t.id ?? "").filter((id) => id !== "") : [];
|
|
1316
|
+
const sameSet = (a, b) => a.length === b.length && a.every((id) => b.includes(id));
|
|
1317
|
+
const checked = [];
|
|
1318
|
+
for (const condition of conditions) {
|
|
1319
|
+
const firstRule = condition.rules?.[0]?.[0] ?? null;
|
|
1320
|
+
if (!firstRule) {
|
|
1321
|
+
checked.push(condition);
|
|
1322
|
+
continue;
|
|
1323
|
+
}
|
|
1324
|
+
for (const other of checked) {
|
|
1325
|
+
const otherFirst = other.rules?.[0]?.[0] ?? null;
|
|
1326
|
+
if (!otherFirst)
|
|
1327
|
+
continue;
|
|
1328
|
+
if (firstRule.conditionExpression !== otherFirst.conditionExpression)
|
|
1329
|
+
continue;
|
|
1330
|
+
let overlap = false;
|
|
1331
|
+
if (firstRule.rule === "required-stages-completed" && otherFirst.rule === "selected-stage-completed" || firstRule.rule === "selected-stage-completed" && otherFirst.rule === "required-stages-completed") {
|
|
1332
|
+
const ruleStages = firstRule.rule === "required-stages-completed" ? requiredStageIds : [firstRule.selectedStageId ?? ""];
|
|
1333
|
+
const otherStages = otherFirst.rule === "required-stages-completed" ? requiredStageIds : [otherFirst.selectedStageId ?? ""];
|
|
1334
|
+
overlap = sameSet(ruleStages, otherStages);
|
|
1335
|
+
} else if (profile === "full" && (firstRule.rule === "required-tasks-completed" && otherFirst.rule === "selected-tasks-completed" || firstRule.rule === "selected-tasks-completed" && otherFirst.rule === "required-tasks-completed")) {
|
|
1336
|
+
const ruleTasks = firstRule.rule === "required-tasks-completed" ? requiredTaskIds : firstRule.selectedTasksIds ?? [];
|
|
1337
|
+
const otherTasks = otherFirst.rule === "required-tasks-completed" ? requiredTaskIds : otherFirst.selectedTasksIds ?? [];
|
|
1338
|
+
overlap = sameSet(ruleTasks, otherTasks);
|
|
1339
|
+
}
|
|
1340
|
+
if (overlap) {
|
|
1341
|
+
issues.push({
|
|
1342
|
+
code: "CASE_MGMT_ROOT_CASE_CONDITION_TOO_SIMILAR",
|
|
1343
|
+
path,
|
|
1344
|
+
message: `${elementLabel} condition "${conditionLabel(condition)}" overlaps with condition "${conditionLabel(other)}" (different rule types but the same target set)`,
|
|
1345
|
+
severity: "error"
|
|
1346
|
+
});
|
|
1347
|
+
break;
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
checked.push(condition);
|
|
1351
|
+
}
|
|
1352
|
+
return issues;
|
|
1353
|
+
}
|
|
1354
|
+
validateConditionsBattery(opts) {
|
|
1355
|
+
const issues = [];
|
|
1356
|
+
for (const condition of opts.conditions) {
|
|
1357
|
+
const condName = conditionLabel(condition);
|
|
1358
|
+
for (const rule of flatConditionRules(condition)) {
|
|
1359
|
+
if (!rule.rule) {
|
|
1360
|
+
issues.push({
|
|
1361
|
+
code: "CASE_MGMT_ROOT_CASE_CONDITION_RULE_MISSING",
|
|
1362
|
+
path: opts.path,
|
|
1363
|
+
message: `${opts.elementLabel} condition "${condName}" has a rule object with no rule type set`,
|
|
1364
|
+
severity: "error"
|
|
1365
|
+
});
|
|
1366
|
+
continue;
|
|
1367
|
+
}
|
|
1368
|
+
issues.push(...this.validateRuleContents(rule, condition, opts.currentStage, opts.schema, opts.path, opts.elementLabel, opts.profile));
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
issues.push(...this.validateConditionDuplicates(opts.conditions, opts.duplicateCode, opts.path, (a, b) => `${opts.elementLabel} has duplicate conditions "${conditionLabel(a)}" and "${conditionLabel(b)}"`));
|
|
1372
|
+
issues.push(...this.validateConditionTooSimilar(opts.conditions, opts.currentStage, opts.schema, opts.path, opts.elementLabel, opts.profile));
|
|
1373
|
+
return issues;
|
|
1374
|
+
}
|
|
1375
|
+
validateTaskEntryConditions(stage, schema) {
|
|
1376
|
+
const issues = [];
|
|
1377
|
+
const path = `nodes[${stage.id}]`;
|
|
1378
|
+
const stageName = stageLabel(stage);
|
|
1379
|
+
const data = stage.data;
|
|
1380
|
+
let isCaseManagerStage = data.isCaseManagerStage;
|
|
1381
|
+
if (isCaseManagerStage == null) {
|
|
1382
|
+
isCaseManagerStage = findFirstStageAfterTrigger(schema) === stage.id;
|
|
1383
|
+
}
|
|
1384
|
+
if (isCaseManagerStage && schema.metadata?.allowAdhocOptionalStageTasks === true) {
|
|
1385
|
+
return issues;
|
|
1386
|
+
}
|
|
1387
|
+
const tasks = flatTasks(stage);
|
|
1388
|
+
for (const task of tasks) {
|
|
1389
|
+
const taskName = task.displayName ?? task.id ?? "(unnamed)";
|
|
1390
|
+
if (!task.data || Object.keys(task.data).length === 0) {
|
|
1391
|
+
continue;
|
|
1392
|
+
}
|
|
1393
|
+
if (!task.entryConditions?.length) {
|
|
1394
|
+
issues.push({
|
|
1395
|
+
code: "CASE_MGMT_STAGE_TASK_ENTRY_CONDITION_MISSING",
|
|
1396
|
+
path,
|
|
1397
|
+
message: `Stage "${stageName}" task "${taskName}" has no entry condition (it would never run)`,
|
|
1398
|
+
severity: "error"
|
|
1399
|
+
});
|
|
1400
|
+
continue;
|
|
1401
|
+
}
|
|
1402
|
+
issues.push(...this.validateConditionsBattery({
|
|
1403
|
+
conditions: task.entryConditions,
|
|
1404
|
+
schema,
|
|
1405
|
+
currentStage: stage,
|
|
1406
|
+
path,
|
|
1407
|
+
elementLabel: `Stage "${stageName}" task "${taskName}"`,
|
|
1408
|
+
duplicateCode: "CASE_MGMT_TASK_ENTRY_CONDITION_DUPLICATE",
|
|
1409
|
+
profile: "full"
|
|
1410
|
+
}));
|
|
1411
|
+
}
|
|
1412
|
+
return issues;
|
|
1413
|
+
}
|
|
1414
|
+
validateStructure(schema) {
|
|
1415
|
+
const issues = [];
|
|
1416
|
+
const hasTrigger = schema.nodes.some((n) => n.type === "case-management:Trigger");
|
|
1417
|
+
if (!hasTrigger) {
|
|
1418
|
+
issues.push({
|
|
1419
|
+
code: "CASE_MGMT_NO_TRIGGER_NODE",
|
|
1420
|
+
path: "nodes",
|
|
1421
|
+
message: "The case definition has no trigger node",
|
|
1422
|
+
severity: "error"
|
|
1423
|
+
});
|
|
1424
|
+
}
|
|
1425
|
+
const hasStage = schema.nodes.some((n) => n.type === "case-management:Stage");
|
|
1426
|
+
if (!hasStage) {
|
|
1427
|
+
issues.push({
|
|
1428
|
+
code: "CASE_MGMT_NO_STAGE_NODE",
|
|
1429
|
+
path: "nodes",
|
|
1430
|
+
message: "The case definition has no stage nodes",
|
|
1431
|
+
severity: "error"
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
1434
|
+
return issues;
|
|
1435
|
+
}
|
|
1436
|
+
validateTriggers(schema, profile) {
|
|
1437
|
+
const issues = [];
|
|
1438
|
+
const triggers = schema.nodes.filter((n) => n.type === "case-management:Trigger");
|
|
1439
|
+
const triggerTargets = new Set;
|
|
1440
|
+
for (const trigger of triggers) {
|
|
1441
|
+
const name = triggerLabel(trigger);
|
|
1442
|
+
const outgoing = schema.edges.filter((e) => e.source === trigger.id);
|
|
1443
|
+
if (profile !== "skeleton") {
|
|
1444
|
+
const missingTriggerField = findRequiredFieldErrorInUiPath(trigger.data.uipath);
|
|
1445
|
+
if (missingTriggerField) {
|
|
1446
|
+
issues.push({
|
|
1447
|
+
code: "EMPTY_REQUIRED_FIELD",
|
|
1448
|
+
path: `nodes[${trigger.id}]`,
|
|
1449
|
+
message: `Trigger "${name}" has required field "${missingTriggerField}" that is empty`,
|
|
1450
|
+
severity: "error"
|
|
1451
|
+
});
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
if (outgoing.length === 0) {
|
|
1455
|
+
issues.push({
|
|
1456
|
+
code: "CASE_MGMT_TRIGGER_NO_EDGES",
|
|
1457
|
+
path: `nodes[${trigger.id}]`,
|
|
1458
|
+
message: `Trigger "${name}" has no outgoing edges`,
|
|
1459
|
+
severity: "error"
|
|
1460
|
+
});
|
|
1461
|
+
} else {
|
|
1462
|
+
for (const edge of outgoing) {
|
|
1463
|
+
triggerTargets.add(edge.target);
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
if (triggerTargets.size > 1) {
|
|
1468
|
+
const canonicalTarget = [...triggerTargets][0];
|
|
1469
|
+
for (const trigger of triggers) {
|
|
1470
|
+
const name = triggerLabel(trigger);
|
|
1471
|
+
const outgoing = schema.edges.filter((e) => e.source === trigger.id);
|
|
1472
|
+
for (const edge of outgoing) {
|
|
1473
|
+
if (edge.target === canonicalTarget)
|
|
1474
|
+
continue;
|
|
1475
|
+
issues.push({
|
|
1476
|
+
code: "CASE_MGMT_TRIGGER_EDGE_TO_WRONG_STAGE",
|
|
1477
|
+
path: `nodes[${trigger.id}]`,
|
|
1478
|
+
message: `Trigger "${name}" does not point to the same first stage as other triggers`,
|
|
1479
|
+
severity: "error"
|
|
1480
|
+
});
|
|
1481
|
+
break;
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
return issues;
|
|
1486
|
+
}
|
|
1487
|
+
validateStageShared(stage, labelCounts, profile) {
|
|
1488
|
+
const issues = [];
|
|
1489
|
+
const path = `nodes[${stage.id}]`;
|
|
1490
|
+
const name = stageLabel(stage);
|
|
1491
|
+
if (!stage.data.label || stage.data.label.trim() === "") {
|
|
1492
|
+
issues.push({
|
|
1493
|
+
code: "CASE_MGMT_STAGE_NAME_MISSING",
|
|
1494
|
+
path,
|
|
1495
|
+
message: "Stage name is missing",
|
|
1496
|
+
severity: "error"
|
|
1497
|
+
});
|
|
1498
|
+
} else if ((labelCounts.get(stage.data.label.trim()) ?? 0) > 1) {
|
|
1499
|
+
issues.push({
|
|
1500
|
+
code: "CASE_MGMT_STAGE_NAME_NOT_UNIQUE",
|
|
1501
|
+
path,
|
|
1502
|
+
message: `Stage name "${name}" is not unique`,
|
|
1503
|
+
severity: "error"
|
|
1504
|
+
});
|
|
1505
|
+
}
|
|
1506
|
+
if (profile === "skeleton")
|
|
1507
|
+
return issues;
|
|
1508
|
+
const raw = stage;
|
|
1509
|
+
if (raw.entryConditions && !stage.data.entryConditions?.length) {
|
|
1510
|
+
issues.push({
|
|
1511
|
+
code: "CASE_MGMT_STAGE_ENTRY_CONDITIONS_MISPLACED",
|
|
1512
|
+
path,
|
|
1513
|
+
message: `Stage "${name}" has entryConditions on the node instead of inside data (should be data.entryConditions)`,
|
|
1514
|
+
severity: "error"
|
|
1515
|
+
});
|
|
1516
|
+
}
|
|
1517
|
+
if (raw.exitConditions && !stage.data.exitConditions?.length) {
|
|
1518
|
+
issues.push({
|
|
1519
|
+
code: "CASE_MGMT_STAGE_EXIT_CONDITIONS_MISPLACED",
|
|
1520
|
+
path,
|
|
1521
|
+
message: `Stage "${name}" has exitConditions on the node instead of inside data (should be data.exitConditions)`,
|
|
1522
|
+
severity: "error"
|
|
1523
|
+
});
|
|
1524
|
+
}
|
|
1525
|
+
const allTasks = profile === "full" ? flatTasks(stage) : [];
|
|
1526
|
+
if (profile === "full" && allTasks.length === 0) {
|
|
1527
|
+
issues.push({
|
|
1528
|
+
code: "CASE_MGMT_STAGE_TASKS_MISSING",
|
|
1529
|
+
path,
|
|
1530
|
+
message: `Stage "${name}" has no tasks`,
|
|
1531
|
+
severity: "warning"
|
|
1532
|
+
});
|
|
1533
|
+
} else {
|
|
1534
|
+
for (const task of allTasks) {
|
|
1535
|
+
if (!task.data || Object.keys(task.data).length === 0) {
|
|
1536
|
+
issues.push({
|
|
1537
|
+
code: "CASE_MGMT_STAGE_TASK_SELECTION_MISSING",
|
|
1538
|
+
path,
|
|
1539
|
+
message: `Stage "${name}" has a task with no configuration`,
|
|
1540
|
+
severity: "warning"
|
|
1541
|
+
});
|
|
1542
|
+
} else {
|
|
1543
|
+
const taskData2 = task.data;
|
|
1544
|
+
const missingField = findRequiredFieldErrorInUiPath(taskData2);
|
|
1545
|
+
if (missingField) {
|
|
1546
|
+
issues.push({
|
|
1547
|
+
code: "EMPTY_REQUIRED_FIELD",
|
|
1548
|
+
path,
|
|
1549
|
+
message: `Stage "${name}" has a task with required field "${missingField}" that is empty`,
|
|
1550
|
+
severity: "error"
|
|
1551
|
+
});
|
|
1552
|
+
} else if (task.type === "action" && !task.data.taskTitle) {
|
|
1553
|
+
issues.push({
|
|
1554
|
+
code: "EMPTY_REQUIRED_FIELD",
|
|
1555
|
+
path,
|
|
1556
|
+
message: `Stage "${name}" has an action task with no title`,
|
|
1557
|
+
severity: "error"
|
|
1558
|
+
});
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
for (const entryCondition of task.entryConditions ?? []) {
|
|
1562
|
+
if (!entryCondition.rules || entryCondition.rules.length === 0) {
|
|
1563
|
+
issues.push({
|
|
1564
|
+
code: "CASE_MGMT_ROOT_CASE_CONDITION_RULE_MISSING",
|
|
1565
|
+
path,
|
|
1566
|
+
message: `Stage "${name}" has a task with an entry condition with no rules`,
|
|
1567
|
+
severity: "error"
|
|
1568
|
+
});
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
const taskDataRaw = task.data;
|
|
1572
|
+
const taskDisplayName = task.displayName ?? task.id ?? "(unnamed)";
|
|
1573
|
+
if (taskDataRaw?.entryConditions && !task.entryConditions?.length) {
|
|
1574
|
+
issues.push({
|
|
1575
|
+
code: "CASE_MGMT_TASK_ENTRY_CONDITIONS_MISPLACED",
|
|
1576
|
+
path,
|
|
1577
|
+
message: `Stage "${name}" task "${taskDisplayName}" has entryConditions inside data (should be on the task object, not data.entryConditions)`,
|
|
1578
|
+
severity: "error"
|
|
1579
|
+
});
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
const sla = stage.data.sla;
|
|
1584
|
+
if (sla) {
|
|
1585
|
+
const hasEscalations = sla.escalationRule != null && sla.escalationRule.length > 0;
|
|
1586
|
+
const hasValue = sla.count != null && sla.count > 0;
|
|
1587
|
+
if (hasEscalations && !hasValue) {
|
|
1588
|
+
issues.push({
|
|
1589
|
+
code: "CASE_MGMT_STAGE_NO_SLA_ESCALATIONS",
|
|
1590
|
+
path,
|
|
1591
|
+
message: `Stage "${name}" has SLA escalation rules but no SLA value is set`,
|
|
1592
|
+
severity: "warning"
|
|
1593
|
+
});
|
|
1594
|
+
}
|
|
1595
|
+
if (hasEscalations) {
|
|
1596
|
+
const missingRecipients = (sla.escalationRule ?? []).some((e) => !e.action?.recipients || e.action.recipients.length === 0);
|
|
1597
|
+
if (missingRecipients) {
|
|
1598
|
+
issues.push({
|
|
1599
|
+
code: "CASE_MGMT_STAGE_ESCALATION_RECIPIENTS_MISSING",
|
|
1600
|
+
path,
|
|
1601
|
+
message: `Stage "${name}" has an escalation rule with no recipients`,
|
|
1602
|
+
severity: "warning"
|
|
1603
|
+
});
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
return issues;
|
|
1608
|
+
}
|
|
1609
|
+
detectReEntryStages(schema) {
|
|
1610
|
+
const stages = schema.nodes.filter((n) => n.type === "case-management:Stage");
|
|
1611
|
+
const stageIds = new Set(stages.map((s) => s.id));
|
|
1612
|
+
const adj = new Map;
|
|
1613
|
+
for (const stage of stages)
|
|
1614
|
+
adj.set(stage.id, []);
|
|
1615
|
+
for (const edge of schema.edges) {
|
|
1616
|
+
if (edge.type === "case-management:Edge" && stageIds.has(edge.source) && stageIds.has(edge.target)) {
|
|
1617
|
+
adj.get(edge.source)?.push(edge.target);
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
const reEntryTargets = new Set;
|
|
1621
|
+
const visited = new Set;
|
|
1622
|
+
const inStack = new Set;
|
|
1623
|
+
for (const start of stages) {
|
|
1624
|
+
if (visited.has(start.id))
|
|
1625
|
+
continue;
|
|
1626
|
+
const stack = [[start.id, 0]];
|
|
1627
|
+
visited.add(start.id);
|
|
1628
|
+
inStack.add(start.id);
|
|
1629
|
+
while (stack.length > 0) {
|
|
1630
|
+
const frame = stack[stack.length - 1];
|
|
1631
|
+
const neighbors = adj.get(frame[0]) ?? [];
|
|
1632
|
+
if (frame[1] >= neighbors.length) {
|
|
1633
|
+
stack.pop();
|
|
1634
|
+
inStack.delete(frame[0]);
|
|
1635
|
+
} else {
|
|
1636
|
+
const neighbor = neighbors[frame[1]++];
|
|
1637
|
+
if (inStack.has(neighbor)) {
|
|
1638
|
+
reEntryTargets.add(neighbor);
|
|
1639
|
+
} else if (!visited.has(neighbor)) {
|
|
1640
|
+
visited.add(neighbor);
|
|
1641
|
+
inStack.add(neighbor);
|
|
1642
|
+
stack.push([neighbor, 0]);
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
return reEntryTargets;
|
|
1648
|
+
}
|
|
1649
|
+
buildLabelCounts(schema) {
|
|
1650
|
+
const counts = new Map;
|
|
1651
|
+
for (const stage of schema.nodes.filter((n) => n.type === "case-management:Stage" || n.type === "case-management:ExceptionStage")) {
|
|
1652
|
+
const label = stage.data.label?.trim() ?? "";
|
|
1653
|
+
if (label)
|
|
1654
|
+
counts.set(label, (counts.get(label) ?? 0) + 1);
|
|
1655
|
+
}
|
|
1656
|
+
return counts;
|
|
1657
|
+
}
|
|
1658
|
+
validatePrimaryStages(schema, profile) {
|
|
1659
|
+
const issues = [];
|
|
1660
|
+
const stages = schema.nodes.filter((n) => n.type === "case-management:Stage");
|
|
1661
|
+
const secondaryStages = schema.nodes.filter((n) => n.type === "case-management:ExceptionStage");
|
|
1662
|
+
const reEntryStages = profile === "full" ? this.detectReEntryStages(schema) : new Set;
|
|
1663
|
+
const labelCounts = this.buildLabelCounts(schema);
|
|
1664
|
+
const suppressMissingErrors = hasCaseManagerStageWithTasks(schema);
|
|
1665
|
+
for (const stage of stages) {
|
|
1666
|
+
const path = `nodes[${stage.id}]`;
|
|
1667
|
+
const name = stageLabel(stage);
|
|
1668
|
+
const entryConditions = stage.data.entryConditions ?? [];
|
|
1669
|
+
const exitConditions = stage.data.exitConditions ?? [];
|
|
1670
|
+
issues.push(...this.validateStageShared(stage, labelCounts, profile));
|
|
1671
|
+
const incomingEdges = schema.edges.filter((e) => e.target === stage.id);
|
|
1672
|
+
if (incomingEdges.length === 0) {
|
|
1673
|
+
const hasSendToStage = secondaryStages.some((ss) => ss.data.exitConditions?.some((ec) => ec.exitToStageId === stage.id));
|
|
1674
|
+
const hasWaitForUser = exitConditions.some((ec) => ec.type === "wait-for-user");
|
|
1675
|
+
if (!hasSendToStage && !hasWaitForUser) {
|
|
1676
|
+
issues.push({
|
|
1677
|
+
code: "CASE_MGMT_STAGE_EDGES_MISSING",
|
|
1678
|
+
path,
|
|
1679
|
+
message: `Stage "${name}" has no incoming edges`,
|
|
1680
|
+
severity: "error"
|
|
1681
|
+
});
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
if (profile === "skeleton")
|
|
1685
|
+
continue;
|
|
1686
|
+
if (entryConditions.length === 0 && !suppressMissingErrors) {
|
|
1687
|
+
issues.push({
|
|
1688
|
+
code: "CASE_MGMT_STAGE_ENTRY_CONDITION_MISSING",
|
|
1689
|
+
path,
|
|
1690
|
+
message: `Stage "${name}" has no entry condition`,
|
|
1691
|
+
severity: "error"
|
|
1692
|
+
});
|
|
1693
|
+
}
|
|
1694
|
+
const hasCompleteExit = exitConditions.some((c) => c.marksStageComplete === true);
|
|
1695
|
+
if (!hasCompleteExit && !suppressMissingErrors) {
|
|
1696
|
+
issues.push({
|
|
1697
|
+
code: "CASE_MGMT_STAGE_EXIT_CONDITION_MISSING",
|
|
1698
|
+
path,
|
|
1699
|
+
message: `Stage "${name}" has no exit condition that marks the stage as complete`,
|
|
1700
|
+
severity: "error"
|
|
1701
|
+
});
|
|
1702
|
+
}
|
|
1703
|
+
const allTasks = flatTasks(stage);
|
|
1704
|
+
if (reEntryStages.has(stage.id)) {
|
|
1705
|
+
const hasReEntryTask = allTasks.some((t) => t.shouldRunOnlyOnce === false || t.shouldRunOnlyOnce === undefined);
|
|
1706
|
+
if (!hasReEntryTask) {
|
|
1707
|
+
issues.push({
|
|
1708
|
+
code: "CASE_MGMT_STAGE_NO_TASKS_RUNNING_ON_RE_ENTRY",
|
|
1709
|
+
path,
|
|
1710
|
+
message: `Stage "${name}" can be re-entered but no task is marked to run on re-entry`,
|
|
1711
|
+
severity: "warning"
|
|
1712
|
+
});
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
for (const condition of entryConditions) {
|
|
1716
|
+
for (const rule of flatConditionRules(condition)) {
|
|
1717
|
+
if ((rule.rule === "selected-stage-completed" || rule.rule === "selected-stage-exited") && rule.selectedStageId === stage.id) {
|
|
1718
|
+
issues.push({
|
|
1719
|
+
code: "CASE_MGMT_STAGE_ENTRY_CONDITION_RULE_SELF_REFERENCE_WARNING",
|
|
1720
|
+
path,
|
|
1721
|
+
message: `Stage "${name}" entry condition "${conditionLabel(condition)}" references its own stage`,
|
|
1722
|
+
severity: "warning"
|
|
1723
|
+
});
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
const hasWaitForUserExit = exitConditions.some((c) => c.type === "wait-for-user");
|
|
1728
|
+
if (hasWaitForUserExit) {
|
|
1729
|
+
const otherStages = schema.nodes.filter((n) => (n.type === "case-management:Stage" || n.type === "case-management:ExceptionStage") && n.id !== stage.id);
|
|
1730
|
+
const hasUserSelectedStageEntry = otherStages.some((s) => (s.data.entryConditions ?? []).some((c) => flatConditionRules(c).some((r) => r.rule === "user-selected-stage")));
|
|
1731
|
+
if (!hasUserSelectedStageEntry) {
|
|
1732
|
+
issues.push({
|
|
1733
|
+
code: "CASE_MGMT_STAGE_EXIT_CONDITION_USER_SELECTED_STAGE_MISSING",
|
|
1734
|
+
path,
|
|
1735
|
+
message: `Stage "${name}" has a "wait-for-user" exit condition but no other stage has a "user-selected-stage" entry rule`,
|
|
1736
|
+
severity: "error"
|
|
1737
|
+
});
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
issues.push(...this.validateConditionsBattery({
|
|
1741
|
+
conditions: entryConditions,
|
|
1742
|
+
schema,
|
|
1743
|
+
currentStage: stage,
|
|
1744
|
+
path,
|
|
1745
|
+
elementLabel: `Stage "${name}"`,
|
|
1746
|
+
duplicateCode: "CASE_MGMT_STAGE_ENTRY_CONDITION_DUPLICATE",
|
|
1747
|
+
profile
|
|
1748
|
+
}));
|
|
1749
|
+
issues.push(...this.validateConditionsBattery({
|
|
1750
|
+
conditions: exitConditions,
|
|
1751
|
+
schema,
|
|
1752
|
+
currentStage: stage,
|
|
1753
|
+
path,
|
|
1754
|
+
elementLabel: `Stage "${name}"`,
|
|
1755
|
+
duplicateCode: "CASE_MGMT_STAGE_EXIT_CONDITION_DUPLICATE",
|
|
1756
|
+
profile
|
|
1757
|
+
}));
|
|
1758
|
+
const outgoingEdges = schema.edges.filter((e) => e.source === stage.id && e.type === "case-management:Edge");
|
|
1759
|
+
for (const condition of exitConditions) {
|
|
1760
|
+
if (!condition.rules)
|
|
1761
|
+
continue;
|
|
1762
|
+
for (const edge of outgoingEdges) {
|
|
1763
|
+
const edgeRules = edge.data.rules;
|
|
1764
|
+
if (edgeRules && rulesMatch(condition.rules, edgeRules)) {
|
|
1765
|
+
issues.push({
|
|
1766
|
+
code: "CASE_MGMT_STAGE_EXIT_CONDITION_DUPLICATE_EDGE",
|
|
1767
|
+
path,
|
|
1768
|
+
message: `Stage "${name}" has an exit condition that duplicates outgoing edge "${edgeLabel(edge)}"`,
|
|
1769
|
+
severity: "error"
|
|
1770
|
+
});
|
|
1771
|
+
break;
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
for (const condition of exitConditions) {
|
|
1776
|
+
if (!condition.rules)
|
|
1777
|
+
continue;
|
|
1778
|
+
for (const ss of secondaryStages) {
|
|
1779
|
+
const ssName = stageLabel(ss);
|
|
1780
|
+
for (const ec of ss.data.entryConditions ?? []) {
|
|
1781
|
+
if (!ec.rules || ec.rules.length === 0)
|
|
1782
|
+
continue;
|
|
1783
|
+
if (rulesMatch(condition.rules, ec.rules)) {
|
|
1784
|
+
issues.push({
|
|
1785
|
+
code: "CASE_MGMT_STAGE_EDGE_DUPLICATE_CONDITION_STAGE",
|
|
1786
|
+
path,
|
|
1787
|
+
message: `Stage "${name}" has an exit condition that duplicates the entry rules of secondary stage "${ssName}"`,
|
|
1788
|
+
severity: "error"
|
|
1789
|
+
});
|
|
1790
|
+
break;
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
for (const condition of exitConditions) {
|
|
1796
|
+
const allRules = (condition.rules ?? []).flat();
|
|
1797
|
+
for (const rule of allRules) {
|
|
1798
|
+
const missingField = findRequiredFieldErrorInUiPath(rule.uipath);
|
|
1799
|
+
if (missingField) {
|
|
1800
|
+
issues.push({
|
|
1801
|
+
code: "EMPTY_REQUIRED_FIELD",
|
|
1802
|
+
path,
|
|
1803
|
+
message: `Stage "${name}" has an exit condition rule with required field "${missingField}" that is empty`,
|
|
1804
|
+
severity: "error"
|
|
1805
|
+
});
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
if (profile === "full") {
|
|
1810
|
+
issues.push(...this.validateTaskEntryConditions(stage, schema));
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
return issues;
|
|
1814
|
+
}
|
|
1815
|
+
validateSecondaryStages(schema, profile) {
|
|
1816
|
+
const issues = [];
|
|
1817
|
+
const secondaryStages = schema.nodes.filter((n) => n.type === "case-management:ExceptionStage");
|
|
1818
|
+
const labelCounts = this.buildLabelCounts(schema);
|
|
1819
|
+
const suppressMissingErrors = hasCaseManagerStageWithTasks(schema);
|
|
1820
|
+
for (const stage of secondaryStages) {
|
|
1821
|
+
const path = `nodes[${stage.id}]`;
|
|
1822
|
+
const name = stageLabel(stage);
|
|
1823
|
+
issues.push(...this.validateStageShared(stage, labelCounts, profile));
|
|
1824
|
+
const hasDirectEdges = schema.edges.some((e) => e.source === stage.id || e.target === stage.id);
|
|
1825
|
+
if (hasDirectEdges) {
|
|
1826
|
+
issues.push({
|
|
1827
|
+
code: "CASE_MGMT_SECONDARY_STAGE_EDGES",
|
|
1828
|
+
path,
|
|
1829
|
+
message: `Secondary stage "${name}" has direct edges (secondary stages use entry/exit conditions instead)`,
|
|
1830
|
+
severity: "error"
|
|
1831
|
+
});
|
|
1832
|
+
}
|
|
1833
|
+
if (profile === "skeleton")
|
|
1834
|
+
continue;
|
|
1835
|
+
const entryConditions = stage.data.entryConditions ?? [];
|
|
1836
|
+
if (entryConditions.length === 0) {
|
|
1837
|
+
if (!suppressMissingErrors) {
|
|
1838
|
+
issues.push({
|
|
1839
|
+
code: "CASE_MGMT_SECONDARY_STAGE_ENTRY_RULES_MISSING",
|
|
1840
|
+
path,
|
|
1841
|
+
message: `Secondary stage "${name}" has no entry conditions`,
|
|
1842
|
+
severity: "warning"
|
|
1843
|
+
});
|
|
1844
|
+
}
|
|
1845
|
+
} else {
|
|
1846
|
+
for (const condition of entryConditions) {
|
|
1847
|
+
if (!condition.rules || condition.rules.length === 0) {
|
|
1848
|
+
issues.push({
|
|
1849
|
+
code: "CASE_MGMT_SECONDARY_STAGE_EXIT_RULES_MISSING",
|
|
1850
|
+
path,
|
|
1851
|
+
message: `Secondary stage "${name}" has an entry condition with no rules`,
|
|
1852
|
+
severity: "error"
|
|
1853
|
+
});
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
const stageRules = entryConditions.flatMap((c) => c.rules ?? []);
|
|
1857
|
+
for (const otherStage of secondaryStages) {
|
|
1858
|
+
if (otherStage.id === stage.id)
|
|
1859
|
+
continue;
|
|
1860
|
+
const otherName = stageLabel(otherStage);
|
|
1861
|
+
const otherRules = (otherStage.data.entryConditions ?? []).flatMap((c) => c.rules ?? []);
|
|
1862
|
+
if (stageRules.length > 0 && otherRules.length > 0 && rulesMatch(stageRules, otherRules)) {
|
|
1863
|
+
issues.push({
|
|
1864
|
+
code: "CASE_MGMT_SECONDARY_STAGE_ENTRY_RULES_DUPLICATE",
|
|
1865
|
+
path,
|
|
1866
|
+
message: `Secondary stage "${name}" has entry rules that duplicate those of "${otherName}"`,
|
|
1867
|
+
severity: "error"
|
|
1868
|
+
});
|
|
1869
|
+
break;
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
for (const condition of entryConditions) {
|
|
1873
|
+
if (!condition.rules || condition.rules.length === 0)
|
|
1874
|
+
continue;
|
|
1875
|
+
for (const edge of schema.edges) {
|
|
1876
|
+
if (edge.type !== "case-management:Edge")
|
|
1877
|
+
continue;
|
|
1878
|
+
const caseEdge = edge;
|
|
1879
|
+
const edgeRules = caseEdge.data.rules;
|
|
1880
|
+
if (edgeRules && rulesMatch(condition.rules, edgeRules)) {
|
|
1881
|
+
issues.push({
|
|
1882
|
+
code: "CASE_MGMT_SECONDARY_STAGE_ENTRY_RULES_DUPLICATE_EDGE",
|
|
1883
|
+
path,
|
|
1884
|
+
message: `Secondary stage "${name}" has an entry condition that duplicates edge "${edgeLabel(edge)}"`,
|
|
1885
|
+
severity: "error"
|
|
1886
|
+
});
|
|
1887
|
+
break;
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
const exitConditions = stage.data.exitConditions ?? [];
|
|
1893
|
+
if (exitConditions.length === 0) {
|
|
1894
|
+
if (!suppressMissingErrors) {
|
|
1895
|
+
issues.push({
|
|
1896
|
+
code: "CASE_MGMT_SECONDARY_STAGE_EXIT_RULES_MISSING",
|
|
1897
|
+
path,
|
|
1898
|
+
message: `Secondary stage "${name}" has no exit conditions`,
|
|
1899
|
+
severity: "error"
|
|
1900
|
+
});
|
|
1901
|
+
}
|
|
1902
|
+
} else {
|
|
1903
|
+
for (const condition of exitConditions) {
|
|
1904
|
+
if (!condition.rules || condition.rules.length === 0) {
|
|
1905
|
+
issues.push({
|
|
1906
|
+
code: "CASE_MGMT_SECONDARY_STAGE_EXIT_RULES_MISSING",
|
|
1907
|
+
path,
|
|
1908
|
+
message: `Secondary stage "${name}" has an exit condition with no rules`,
|
|
1909
|
+
severity: "error"
|
|
1910
|
+
});
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
if (profile === "full") {
|
|
1915
|
+
issues.push(...this.validateTaskEntryConditions(stage, schema));
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
return issues;
|
|
1919
|
+
}
|
|
1920
|
+
validateEdges(schema, profile) {
|
|
1921
|
+
const issues = [];
|
|
1922
|
+
const secondaryStages = schema.nodes.filter((n) => n.type === "case-management:ExceptionStage");
|
|
1923
|
+
const nodeIds = new Set(schema.nodes.map((n) => n.id));
|
|
1924
|
+
const edgesBySource = new Map;
|
|
1925
|
+
for (const edge of schema.edges) {
|
|
1926
|
+
if (!nodeIds.has(edge.source)) {
|
|
1927
|
+
issues.push({
|
|
1928
|
+
code: "EDGE_INVALID_SOURCE",
|
|
1929
|
+
path: `edges[${edge.id}]`,
|
|
1930
|
+
message: `Edge "${edgeLabel(edge)}" source node "${edge.source}" does not exist`,
|
|
1931
|
+
severity: "error"
|
|
1932
|
+
});
|
|
1933
|
+
}
|
|
1934
|
+
if (!nodeIds.has(edge.target)) {
|
|
1935
|
+
issues.push({
|
|
1936
|
+
code: "EDGE_INVALID_TARGET",
|
|
1937
|
+
path: `edges[${edge.id}]`,
|
|
1938
|
+
message: `Edge "${edgeLabel(edge)}" target node "${edge.target}" does not exist`,
|
|
1939
|
+
severity: "error"
|
|
1940
|
+
});
|
|
1941
|
+
}
|
|
1942
|
+
const group = edgesBySource.get(edge.source) ?? [];
|
|
1943
|
+
group.push(edge);
|
|
1944
|
+
edgesBySource.set(edge.source, group);
|
|
1945
|
+
}
|
|
1946
|
+
if (profile === "skeleton")
|
|
1947
|
+
return issues;
|
|
1948
|
+
for (const [, edges] of edgesBySource) {
|
|
1949
|
+
const caseEdges = edges.filter((e) => e.type === "case-management:Edge");
|
|
1950
|
+
for (let i = 0;i < caseEdges.length; i++) {
|
|
1951
|
+
let hasDuplicate = false;
|
|
1952
|
+
for (let j = i + 1;j < caseEdges.length && !hasDuplicate; j++) {
|
|
1953
|
+
const a = caseEdges[i];
|
|
1954
|
+
const b = caseEdges[j];
|
|
1955
|
+
if (a.data.rules && b.data.rules && rulesMatch(a.data.rules, b.data.rules)) {
|
|
1956
|
+
issues.push({
|
|
1957
|
+
code: "CASE_MGMT_STAGE_EDGE_DUPLICATE_CONDITION",
|
|
1958
|
+
path: `edges[${a.id}]`,
|
|
1959
|
+
message: `Two or more edges from the same source node have duplicate conditions`,
|
|
1960
|
+
severity: "error"
|
|
1961
|
+
});
|
|
1962
|
+
hasDuplicate = true;
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
for (const edge of caseEdges) {
|
|
1967
|
+
if (!edge.data.rules)
|
|
1968
|
+
continue;
|
|
1969
|
+
for (const ss of secondaryStages) {
|
|
1970
|
+
const ssName = stageLabel(ss);
|
|
1971
|
+
for (const ec of ss.data.entryConditions ?? []) {
|
|
1972
|
+
if (!ec.rules || ec.rules.length === 0)
|
|
1973
|
+
continue;
|
|
1974
|
+
if (rulesMatch(edge.data.rules, ec.rules)) {
|
|
1975
|
+
issues.push({
|
|
1976
|
+
code: "CASE_MGMT_STAGE_EDGE_DUPLICATE_CONDITION_STAGE",
|
|
1977
|
+
path: `edges[${edge.id}]`,
|
|
1978
|
+
message: `Edge "${edgeLabel(edge)}" has conditions that duplicate the entry rules of secondary stage "${ssName}"`,
|
|
1979
|
+
severity: "error"
|
|
1980
|
+
});
|
|
1981
|
+
break;
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
const allEdgeRules = (edge.data.rules ?? []).flat();
|
|
1986
|
+
for (const rule of allEdgeRules) {
|
|
1987
|
+
const missingRuleField = findRequiredFieldErrorInUiPath(rule.uipath);
|
|
1988
|
+
if (missingRuleField) {
|
|
1989
|
+
issues.push({
|
|
1990
|
+
code: "EMPTY_REQUIRED_FIELD",
|
|
1991
|
+
path: `edges[${edge.id}]`,
|
|
1992
|
+
message: `Edge "${edgeLabel(edge)}" has a rule with required field "${missingRuleField}" that is empty`,
|
|
1993
|
+
severity: "error"
|
|
1994
|
+
});
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
return issues;
|
|
2000
|
+
}
|
|
2001
|
+
validateOverallCase(schema) {
|
|
2002
|
+
const issues = [];
|
|
2003
|
+
const stages = schema.nodes.filter((n) => n.type === "case-management:Stage");
|
|
2004
|
+
if (stages.length === 0)
|
|
2005
|
+
return issues;
|
|
2006
|
+
const allStageNodes = schema.nodes.filter((n) => n.type === "case-management:Stage" || n.type === "case-management:ExceptionStage");
|
|
2007
|
+
const hasExplicitTerminal = allStageNodes.some((s) => s.data.exitConditions?.some((ec) => ec.type === "exit-only")) || (schema.metadata?.caseExitRules ?? []).length > 0;
|
|
2008
|
+
if (hasExplicitTerminal)
|
|
2009
|
+
return issues;
|
|
2010
|
+
const hasImplicitTerminal = stages.every((s) => schema.edges.some((e) => e.target === s.id));
|
|
2011
|
+
if (!hasImplicitTerminal) {
|
|
2012
|
+
issues.push({
|
|
2013
|
+
code: "CASE_MGMT_VALID_TERMINAL_EXIT_MISSING",
|
|
2014
|
+
path: "(overall)",
|
|
2015
|
+
message: "The case definition has no valid terminal exit (not all stages have incoming edges and no explicit terminal exit condition)",
|
|
2016
|
+
severity: "error"
|
|
2017
|
+
});
|
|
2018
|
+
}
|
|
2019
|
+
return issues;
|
|
2020
|
+
}
|
|
2021
|
+
validateAdhocNodes(schema, profile) {
|
|
2022
|
+
const issues = [];
|
|
2023
|
+
const labelCounts = this.buildLabelCounts(schema);
|
|
2024
|
+
const triggerIds = new Set(schema.nodes.filter((n) => n.type === "case-management:Trigger").map((n) => n.id));
|
|
2025
|
+
const masterStageId = schema.edges.filter((e) => triggerIds.has(e.source)).map((e) => e.target)[0];
|
|
2026
|
+
const masterStage = masterStageId ? schema.nodes.find((n) => n.id === masterStageId && (n.type === "case-management:Stage" || n.type === "case-management:ExceptionStage")) : undefined;
|
|
2027
|
+
if (!masterStage) {
|
|
2028
|
+
issues.push({
|
|
2029
|
+
code: "CASE_MGMT_NO_MASTER_ADHOC_STAGE",
|
|
2030
|
+
path: "nodes",
|
|
2031
|
+
message: "The adhoc case definition has no master orchestration stage",
|
|
2032
|
+
severity: "error"
|
|
2033
|
+
});
|
|
2034
|
+
} else {
|
|
2035
|
+
issues.push(...this.validateStageShared(masterStage, labelCounts, profile));
|
|
2036
|
+
if (profile === "full") {
|
|
2037
|
+
issues.push(...this.validateTaskEntryConditions(masterStage, schema));
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
const otherStages = schema.nodes.filter((n) => (n.type === "case-management:Stage" || n.type === "case-management:ExceptionStage") && n.id !== masterStageId);
|
|
2041
|
+
if (otherStages.length === 0) {
|
|
2042
|
+
if (profile !== "skeleton") {
|
|
2043
|
+
issues.push({
|
|
2044
|
+
code: "CASE_MGMT_NO_ADDITIONAL_STAGE_NODES",
|
|
2045
|
+
path: "nodes",
|
|
2046
|
+
message: "The adhoc case definition has no additional stage nodes besides the master stage",
|
|
2047
|
+
severity: "error"
|
|
2048
|
+
});
|
|
2049
|
+
}
|
|
2050
|
+
} else {
|
|
2051
|
+
for (const stage of otherStages) {
|
|
2052
|
+
issues.push(...this.validateStageShared(stage, labelCounts, profile));
|
|
2053
|
+
if (profile === "full") {
|
|
2054
|
+
issues.push(...this.validateTaskEntryConditions(stage, schema));
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
return issues;
|
|
2059
|
+
}
|
|
2060
|
+
validateOutputVariableUniqueness(schema) {
|
|
2061
|
+
const issues = [];
|
|
2062
|
+
const allStages = schema.nodes.filter((n) => n.type === "case-management:Stage" || n.type === "case-management:ExceptionStage");
|
|
2063
|
+
const seen = new Map;
|
|
2064
|
+
for (const stage of allStages) {
|
|
2065
|
+
const name = stageLabel(stage);
|
|
2066
|
+
for (const task of flatTasks(stage)) {
|
|
2067
|
+
const taskName = task.displayName ?? task.id ?? "(unnamed)";
|
|
2068
|
+
const outputs = task.data?.outputs ?? [];
|
|
2069
|
+
for (const output of outputs) {
|
|
2070
|
+
const varId = output.var ?? output.id;
|
|
2071
|
+
if (!varId)
|
|
2072
|
+
continue;
|
|
2073
|
+
const location = `"${name}"/"${taskName}"`;
|
|
2074
|
+
const existing = seen.get(varId);
|
|
2075
|
+
if (existing) {
|
|
2076
|
+
issues.push({
|
|
2077
|
+
code: "CASE_MGMT_DUPLICATE_OUTPUT_VARIABLE",
|
|
2078
|
+
path: `nodes[${stage.id}]`,
|
|
2079
|
+
message: `Output variable "${varId}" in ${location} duplicates the same variable in ${existing}`,
|
|
2080
|
+
severity: "error"
|
|
2081
|
+
});
|
|
2082
|
+
} else {
|
|
2083
|
+
seen.set(varId, location);
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
return issues;
|
|
2089
|
+
}
|
|
2090
|
+
runTaskValidators(schema, profile = "full") {
|
|
2091
|
+
const issues = [];
|
|
2092
|
+
const validators = profile === "strict" ? [...this.taskValidators, ...strictTaskValidators] : this.taskValidators;
|
|
2093
|
+
if (!Array.isArray(schema.nodes))
|
|
2094
|
+
return issues;
|
|
2095
|
+
const stages = schema.nodes.filter((n) => n.type === "case-management:Stage" || n.type === "case-management:ExceptionStage");
|
|
2096
|
+
for (const stage of stages) {
|
|
2097
|
+
const tasks = (stage.data.tasks ?? []).flat();
|
|
2098
|
+
for (const task of tasks) {
|
|
2099
|
+
const ctx = { schema, stageId: stage.id };
|
|
2100
|
+
for (const validator of validators) {
|
|
2101
|
+
if (!validator.appliesTo(task))
|
|
2102
|
+
continue;
|
|
2103
|
+
issues.push(...validator.validate(task, ctx));
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
return issues;
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
export {
|
|
2111
|
+
CaseValidateService
|
|
2112
|
+
};
|
|
2113
|
+
|
|
2114
|
+
//# debugId=73947324AD8898D364756E2164756E21
|