@uipath/case-tool 1.202.0 → 1.203.0-preview.160
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/THIRD-PARTY-NOTICES.md +172 -3
- package/dist/{authoring-e757p9dq.js → authoring-kngaggwm.js} +42 -4
- package/dist/{case-api-42s4mbpn.js → case-api-v6cjyrfw.js} +8 -8
- package/dist/case-bindings-service-1wxfghzd.js +240 -0
- package/dist/case-debug-studio-web-service-0624c07c.js +133 -0
- package/dist/case-format-service-q345a2fd.js +84 -0
- package/dist/{case-pack-service-8exkecnd.js → case-pack-service-29rza2q2.js} +61 -11
- package/dist/{case-validate-service-xvxkr026.js → case-validate-service-e06varbt.js} +1422 -40
- package/dist/{connector-enrichment-service-xq9xtz95.js → connector-enrichment-service-2pes4b1b.js} +20 -20
- package/dist/{connector-lookup-service-bk1wscyr.js → connector-lookup-service-2bkybhkf.js} +7 -7
- package/dist/{connector-task-spec-0g34na74.js → connector-task-spec-q59xmzm0.js} +24 -24
- package/dist/{de-7XQXX2DK-tqdy5n2b.js → de-FGHEJRCI-rv513mfh.js} +153 -9
- package/dist/{de-5KVP66U6-1bnf4zt5.js → de-MUJPA4EB-dpg7t2rw.js} +15 -3
- package/dist/{debug-service-f6t4ntmg.js → debug-service-8x087xxt.js} +2 -2
- package/dist/{index-j3xb87ag.js → embedded-file-reader-0sjt4nkj.js} +96 -16641
- package/dist/{en-5ZGC5VQ3-h581y30k.js → en-BWGKA5OE-1qkvb9rn.js} +140 -2
- package/dist/{en-X3S6IZ5J-nhkg6pwn.js → en-FBPVNKFN-dqt573xn.js} +29 -3
- package/dist/{enrichment-service-k1zhkzgr.js → enrichment-service-7vbxcwge.js} +7 -7
- package/dist/{es-VFPNVMP5-bx2c337j.js → es-7SE7HX2X-8yh1aj2y.js} +15 -3
- package/dist/{es-ZGCPPOME-8gazspjj.js → es-KR3ZSNMP-84zphx0x.js} +153 -9
- package/dist/{es-MX-XAVLOQW3-q00km2y7.js → es-MX-2MYFZU7X-3rek4r73.js} +15 -3
- package/dist/{es-MX-U2VN3SZW-anynsmd8.js → es-MX-XXZJGC4R-2mr8jtgm.js} +153 -9
- package/dist/first-party-service-srxaftkn.js +27 -0
- package/dist/{fr-E5MDO4EG-c3mkty2t.js → fr-MDFKNZ4C-h4a122k9.js} +15 -3
- package/dist/{fr-T3G5GZCT-y3f7xb1q.js → fr-Y6NPVODW-8vr1ja4b.js} +153 -9
- package/dist/{index-f0rf4m8p.js → index-8yv94z7r.js} +6 -4
- package/dist/{index-kwxe1vep.js → index-fz4ttsde.js} +16 -4
- package/dist/index-jxj2jz45.js +16742 -0
- package/dist/index-kmp9fmhb.js +25 -0
- package/dist/index.js +10 -10
- package/dist/{input-reader-vgsdjgst.js → input-reader-pnh948bq.js} +1 -1
- package/dist/{ja-BI5K3JDZ-77zat4z2.js → ja-Q5PXKJ2V-p4msxzjd.js} +153 -9
- package/dist/{ja-DPO5PPLO-mysfmznx.js → ja-UTQEAP34-8e0q6swp.js} +15 -3
- package/dist/{known-types-qryebm95.js → known-types-mn8xn36b.js} +4 -4
- package/dist/{ko-ITOPNIYI-hmxd2asd.js → ko-AXCHW2OK-askct7f6.js} +15 -3
- package/dist/{ko-JOBF5V7N-4bv0mjje.js → ko-AYBDSRZG-41mdmsg1.js} +153 -9
- package/dist/{node-strategy-0267x5e5.js → node-strategy-gc4sg6a2.js} +4 -4
- package/dist/{node-sync-service-ac8rzgnj.js → node-sync-service-9d0dqz4z.js} +12 -14
- package/dist/{packager-tool-5yw9xatr.js → packager-tool-052kvydg.js} +1 -1
- package/dist/{packager-tool-3tfbb641.js → packager-tool-32jxda3r.js} +213 -51
- package/dist/packager-tool-53w8skv5.js +262 -0
- package/dist/{packager-tool-w0209wf4.js → packager-tool-61hxhgeg.js} +1 -1
- package/dist/{packager-tool-58q3tvh7.js → packager-tool-7q6z6npd.js} +166 -28
- package/dist/packager-tool-83qnybab.js +1564 -0
- package/dist/{packager-tool-eh50gcdk.js → packager-tool-8q7vzh65.js} +5 -4
- package/dist/{packager-tool-8rf8qda5.js → packager-tool-adcaxdhs.js} +15 -5
- package/dist/{packager-tool-933n6t8x.js → packager-tool-af2s79jb.js} +6 -6
- package/dist/{packager-tool-sk2b33km.js → packager-tool-b6368khs.js} +10 -9
- package/dist/{packager-tool-d73376xs.js → packager-tool-cg2j4rma.js} +8 -8
- package/dist/packager-tool-d4sjk46c.js +88 -0
- package/dist/{packager-tool-a42jnpy5.js → packager-tool-d88rqwk8.js} +29 -3
- package/dist/packager-tool-dd8bq6az.js +8462 -0
- package/dist/packager-tool-gdtpsdn7.js +342 -0
- package/dist/{packager-tool-v60h8pvv.js → packager-tool-hrdan5ms.js} +1 -1
- package/dist/{packager-tool-bmn50g7v.js → packager-tool-hz1mz8fs.js} +1 -1
- package/dist/{packager-tool-v0vdet53.js → packager-tool-jf4hmztd.js} +10 -13
- package/dist/{packager-tool-c23zrhj8.js → packager-tool-jysvpt8p.js} +2 -2
- package/dist/{packager-tool-jhdf8gt1.js → packager-tool-kkxhc0fp.js} +13554 -8844
- package/dist/{packager-tool-4knkebkr.js → packager-tool-kpcjnxd1.js} +269 -23
- package/dist/{packager-tool-nx0j1294.js → packager-tool-rhkk0pta.js} +31 -30
- package/dist/{packager-tool-d128hs55.js → packager-tool-wefb1zw3.js} +1 -1
- package/dist/{packager-tool-3kbkkv2a.js → packager-tool-x4vxexvk.js} +11 -11
- package/dist/{packager-tool-dqs5qy9s.js → packager-tool-yrsrzz1a.js} +134 -19
- package/dist/packager-tool.js +5 -1501
- package/dist/{project-artifacts-43c2ynd9.js → project-artifacts-8snev67g.js} +1 -1
- package/dist/{pt-YI4VE4YQ-wdkn2dcf.js → pt-3DIO5QWG-59b3f4bc.js} +15 -3
- package/dist/{pt-BR-7DYX2YPT-fqxqzqtf.js → pt-BR-TKHNHMET-sn91xyq0.js} +153 -9
- package/dist/{pt-BR-BXURWX4W-bwfcwhk1.js → pt-BR-Y6K6XHXK-mp3hwhw1.js} +15 -3
- package/dist/{pt-HTXGGX6Y-97t21dty.js → pt-ROXU257Y-2f6bxev4.js} +153 -9
- package/dist/{resourcesSyncService-7ampdrez.js → resourcesSyncService-5bq0j9wf.js} +20 -20
- package/dist/{ro-KTNQ3MNU-y08n1x5g.js → ro-AVAWPKOO-b89pw79r.js} +153 -9
- package/dist/{ro-RKXZUG7Z-nndrrsq7.js → ro-BWR3ONRR-vzx3hy8a.js} +15 -3
- package/dist/ru-WBLQ5B2U-s4em88p0.js +10 -0
- package/dist/sdd-lookup-x08j9g9m.js +48 -0
- package/dist/{strict-case-rules-303vjvas.js → strict-case-rules-x6t0vb40.js} +141 -109
- package/dist/strict-sdd-audit-8e2fh9jg.js +2520 -0
- package/dist/tool.js +9 -9
- package/dist/{tr-AA6O2WSR-86ba4bdf.js → tr-5IIHSFW5-hw4cewew.js} +15 -3
- package/dist/{tr-XHSSPNK7-cmqyc4wd.js → tr-JMIP7ZSL-c39b6pe4.js} +153 -9
- package/dist/{trigger-enrichment-service-cg99dk1f.js → trigger-enrichment-service-kkh3197w.js} +24 -24
- package/dist/{zh-CN-2EHQT23P-mr63c4xz.js → zh-CN-IFFXSEJS-9wm9gmrv.js} +153 -9
- package/dist/{zh-CN-R2KLHXQV-agqr45ds.js → zh-CN-R2KU4U5L-tc1hat59.js} +15 -3
- package/dist/{zh-TW-RJGCQANM-w0x3qc1p.js → zh-TW-GNKMELBF-j674515r.js} +15 -3
- package/dist/{zh-TW-MGZCE5BT-c4ekmrz9.js → zh-TW-IYBNYAMG-8727c7v2.js} +153 -9
- package/package.json +3 -2
- package/dist/case-debug-studio-web-service-npcw81ay.js +0 -335
- package/dist/packager-tool-134mvmcm.js +0 -3040
- package/dist/packager-tool-ty1t2byf.js +0 -27
- package/dist/ru-WBLQ5B2U-8r3c7v6d.js +0 -10
- package/dist/strict-sdd-audit-qdmc9e1y.js +0 -1024
|
@@ -0,0 +1,2520 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isAnyTriggerNode,
|
|
3
|
+
isStageNode
|
|
4
|
+
} from "./packager-tool-sv0d6wb6.js";
|
|
5
|
+
import {
|
|
6
|
+
catchError
|
|
7
|
+
} from "./packager-tool-kpcjnxd1.js";
|
|
8
|
+
import"./packager-tool-0v3s8r01.js";
|
|
9
|
+
import"./packager-tool-1yh8g550.js";
|
|
10
|
+
|
|
11
|
+
// src/services/strict-sdd-audit.ts
|
|
12
|
+
var isDigit = (ch) => ch >= "0" && ch <= "9";
|
|
13
|
+
var isLowerLetter = (ch) => ch >= "a" && ch <= "z";
|
|
14
|
+
var isUpperLetter = (ch) => ch >= "A" && ch <= "Z";
|
|
15
|
+
var isLetter = (ch) => isLowerLetter(ch) || isUpperLetter(ch);
|
|
16
|
+
var isWordChar = (ch) => isLetter(ch) || isDigit(ch) || ch === "_";
|
|
17
|
+
var isSlugChar = (ch) => isDigit(ch) || isLowerLetter(ch) || ch === "-" || ch === "_";
|
|
18
|
+
var readHeading = (line) => {
|
|
19
|
+
let level = 0;
|
|
20
|
+
while (level < line.length && line[level] === "#")
|
|
21
|
+
level += 1;
|
|
22
|
+
if (level === 0 || level > 6)
|
|
23
|
+
return;
|
|
24
|
+
const rest = line.slice(level);
|
|
25
|
+
if (!rest.startsWith(" ") && !rest.startsWith("\t"))
|
|
26
|
+
return;
|
|
27
|
+
return { level, title: rest.trim() };
|
|
28
|
+
};
|
|
29
|
+
var firstIndexOfEither = (text, a, b) => {
|
|
30
|
+
const first = text.indexOf(a);
|
|
31
|
+
const second = text.indexOf(b);
|
|
32
|
+
if (first === -1)
|
|
33
|
+
return second;
|
|
34
|
+
if (second === -1)
|
|
35
|
+
return first;
|
|
36
|
+
return Math.min(first, second);
|
|
37
|
+
};
|
|
38
|
+
var readStageHeading = (title) => {
|
|
39
|
+
const words = title.trim().split(" ").filter((w) => w !== "");
|
|
40
|
+
const at = words.findIndex((w) => w.toLowerCase().startsWith("stage"));
|
|
41
|
+
if (at === -1 || at > 1)
|
|
42
|
+
return;
|
|
43
|
+
let rest = words.slice(at).join(" ");
|
|
44
|
+
rest = rest.slice("stage".length);
|
|
45
|
+
const separator = firstIndexOfEither(rest, ":", ".");
|
|
46
|
+
if (separator === -1)
|
|
47
|
+
return;
|
|
48
|
+
const between = rest.slice(0, separator).trim();
|
|
49
|
+
if (between !== "" && ![...between].every(isDigit))
|
|
50
|
+
return;
|
|
51
|
+
const name = rest.slice(separator + 1).trim();
|
|
52
|
+
return name === "" ? undefined : name;
|
|
53
|
+
};
|
|
54
|
+
var readTaskHeading = (title) => {
|
|
55
|
+
const text = title.trim();
|
|
56
|
+
if (!text.toLowerCase().startsWith("task "))
|
|
57
|
+
return;
|
|
58
|
+
let rest = text.slice("task ".length).trimStart();
|
|
59
|
+
if (rest.length > 0 && rest[0] >= "A" && rest[0] <= "Z")
|
|
60
|
+
rest = rest.slice(1);
|
|
61
|
+
let at = 0;
|
|
62
|
+
while (at < rest.length && (isDigit(rest[at]) || rest[at] === "."))
|
|
63
|
+
at += 1;
|
|
64
|
+
if (at === 0)
|
|
65
|
+
return;
|
|
66
|
+
const marker = rest.slice(0, at);
|
|
67
|
+
let tail = rest.slice(at);
|
|
68
|
+
if (marker.endsWith("."))
|
|
69
|
+
tail = `.${tail}`;
|
|
70
|
+
tail = tail.trimStart();
|
|
71
|
+
if (!tail.startsWith(":") && !tail.startsWith("."))
|
|
72
|
+
return;
|
|
73
|
+
const name = tail.slice(1).trim();
|
|
74
|
+
return name === "" ? undefined : name;
|
|
75
|
+
};
|
|
76
|
+
var isSeparatorCell = (cell) => {
|
|
77
|
+
let body = cell;
|
|
78
|
+
if (body.startsWith(":"))
|
|
79
|
+
body = body.slice(1);
|
|
80
|
+
if (body.endsWith(":"))
|
|
81
|
+
body = body.slice(0, -1);
|
|
82
|
+
return body.length >= 2 && [...body].every((ch) => ch === "-");
|
|
83
|
+
};
|
|
84
|
+
var hasUnresolvedMarker = (text) => text.toLowerCase().includes("<unresolved");
|
|
85
|
+
var withoutSectionNumber = (key) => {
|
|
86
|
+
const words = key.split(" ");
|
|
87
|
+
let at = 0;
|
|
88
|
+
if (words[at] === "section")
|
|
89
|
+
at += 1;
|
|
90
|
+
const looksNumbered = (word) => {
|
|
91
|
+
if (!word)
|
|
92
|
+
return false;
|
|
93
|
+
let i = 0;
|
|
94
|
+
if (isLowerLetter(word[i]))
|
|
95
|
+
i += 1;
|
|
96
|
+
const start = i;
|
|
97
|
+
while (i < word.length && isDigit(word[i]))
|
|
98
|
+
i += 1;
|
|
99
|
+
if (i === start)
|
|
100
|
+
return false;
|
|
101
|
+
if (i < word.length && isLowerLetter(word[i]))
|
|
102
|
+
i += 1;
|
|
103
|
+
return i === word.length;
|
|
104
|
+
};
|
|
105
|
+
if (!looksNumbered(words[at]))
|
|
106
|
+
return key;
|
|
107
|
+
at += 1;
|
|
108
|
+
while (looksNumbered(words[at]))
|
|
109
|
+
at += 1;
|
|
110
|
+
return words.slice(at).join(" ");
|
|
111
|
+
};
|
|
112
|
+
var TOPIC_WORDS = ["conditions", "variables", "triggers", "sla"];
|
|
113
|
+
var withoutTrailingSlug = (text) => {
|
|
114
|
+
const trimmed = text.trimEnd();
|
|
115
|
+
if (!trimmed.endsWith(")"))
|
|
116
|
+
return text;
|
|
117
|
+
const open = trimmed.lastIndexOf("(");
|
|
118
|
+
if (open === -1)
|
|
119
|
+
return text;
|
|
120
|
+
let inner = trimmed.slice(open + 1, -1).trim();
|
|
121
|
+
if (inner.startsWith("`") && inner.endsWith("`") && inner.length >= 2) {
|
|
122
|
+
inner = inner.slice(1, -1);
|
|
123
|
+
}
|
|
124
|
+
if (inner === "" || ![...inner].every(isSlugChar))
|
|
125
|
+
return text;
|
|
126
|
+
return trimmed.slice(0, open).trimEnd();
|
|
127
|
+
};
|
|
128
|
+
var PLACEHOLDER_CELLS = ["", "-", "—", "–", "n/a", "na", "none", "tbd"];
|
|
129
|
+
var DEFAULT_UNFILLED = new Set(["-", "—", "–", "n/a", "na", "tbd"]);
|
|
130
|
+
var CASE_EXIT_HEADINGS = [
|
|
131
|
+
"Case Exit Conditions",
|
|
132
|
+
"Case Completion Conditions"
|
|
133
|
+
];
|
|
134
|
+
var CASE_TRIGGER_HEADINGS = ["Case Triggers", "Triggers"];
|
|
135
|
+
var CASE_SLA_HEADINGS = ["Case-Level SLA Escalation Rules"];
|
|
136
|
+
var CASE_METADATA_HEADINGS = ["Case Metadata"];
|
|
137
|
+
var CASE_SLA_PROPERTY = "Case-Level SLA";
|
|
138
|
+
var CASE_VARIABLE_HEADINGS = ["Case Variables"];
|
|
139
|
+
var STAGE_ENTRY_HEADINGS = ["Stage Entry Conditions"];
|
|
140
|
+
var STAGE_EXIT_HEADINGS = [
|
|
141
|
+
"Stage Exit Conditions",
|
|
142
|
+
"Stage Completion Conditions"
|
|
143
|
+
];
|
|
144
|
+
var STAGE_SLA_HEADINGS = ["Stage SLA"];
|
|
145
|
+
var STAGE_CONDITIONAL_SLA_HEADINGS = [
|
|
146
|
+
"Stage Conditional SLA Rules",
|
|
147
|
+
"Stage Variable SLA Rules"
|
|
148
|
+
];
|
|
149
|
+
var CASE_CONDITIONAL_SLA_HEADINGS = [
|
|
150
|
+
"Variable SLA Rules",
|
|
151
|
+
"Case Conditional SLA Rules",
|
|
152
|
+
"Case-Level Conditional SLA Rules",
|
|
153
|
+
"Case Variable SLA Rules"
|
|
154
|
+
];
|
|
155
|
+
var STAGE_TASK_HEADINGS = ["Tasks"];
|
|
156
|
+
var VARIABLE_GROUPS = ["inputs", "outputs", "inputOutputs"];
|
|
157
|
+
var VARIABLE_PLACEMENT = new Map([
|
|
158
|
+
["in", ["inputs", "inputOutputs"]],
|
|
159
|
+
["out", ["outputs", "inputOutputs"]],
|
|
160
|
+
["variable", ["inputOutputs"]]
|
|
161
|
+
]);
|
|
162
|
+
var DEFAULT_GROUP = new Map([
|
|
163
|
+
["in", "inputs"],
|
|
164
|
+
["out", "inputOutputs"],
|
|
165
|
+
["variable", "inputOutputs"]
|
|
166
|
+
]);
|
|
167
|
+
var QUOTE_CHARS = new Set(["`", '"', "“", "”", "‘", "’", " "]);
|
|
168
|
+
var trimQuotes = (text) => {
|
|
169
|
+
let start = 0;
|
|
170
|
+
let end = text.length;
|
|
171
|
+
while (start < end && QUOTE_CHARS.has(text[start]))
|
|
172
|
+
start += 1;
|
|
173
|
+
while (end > start && QUOTE_CHARS.has(text[end - 1]))
|
|
174
|
+
end -= 1;
|
|
175
|
+
return text.slice(start, end);
|
|
176
|
+
};
|
|
177
|
+
var normAnnotationless = (value) => {
|
|
178
|
+
const text = String(value ?? "").trim();
|
|
179
|
+
if (!text.endsWith(")"))
|
|
180
|
+
return norm(text);
|
|
181
|
+
const open = text.lastIndexOf("(");
|
|
182
|
+
return open === -1 ? norm(text) : norm(text.slice(0, open));
|
|
183
|
+
};
|
|
184
|
+
var norm = (value) => {
|
|
185
|
+
const text = trimQuotes(withoutTrailingSlug(String(value ?? "").trim()));
|
|
186
|
+
const words = [];
|
|
187
|
+
let word = "";
|
|
188
|
+
for (const ch of text.toLowerCase()) {
|
|
189
|
+
if (isLowerLetter(ch) || isDigit(ch))
|
|
190
|
+
word += ch;
|
|
191
|
+
else if (word !== "") {
|
|
192
|
+
words.push(word);
|
|
193
|
+
word = "";
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (word !== "")
|
|
197
|
+
words.push(word);
|
|
198
|
+
return words.join(" ");
|
|
199
|
+
};
|
|
200
|
+
var headingKey = (title) => withoutSectionNumber(norm(title));
|
|
201
|
+
var headingIs = (title, accepted) => accepted.some((a) => headingKey(a) === headingKey(title));
|
|
202
|
+
var topicOf = (title) => {
|
|
203
|
+
const key = headingKey(title);
|
|
204
|
+
const words = key.split(" ");
|
|
205
|
+
if (words.some((w) => w === "task" || w === "tasks"))
|
|
206
|
+
return "tasks";
|
|
207
|
+
const last = words.at(-1);
|
|
208
|
+
return TOPIC_WORDS.find((topic) => topic === last);
|
|
209
|
+
};
|
|
210
|
+
var isBlank = (cell) => PLACEHOLDER_CELLS.map(norm).includes(norm(cell));
|
|
211
|
+
var sectionBlocks = (text) => {
|
|
212
|
+
const heads = [];
|
|
213
|
+
let offset = 0;
|
|
214
|
+
for (const line of text.split(`
|
|
215
|
+
`)) {
|
|
216
|
+
const heading = readHeading(line);
|
|
217
|
+
if (heading) {
|
|
218
|
+
heads.push({
|
|
219
|
+
level: heading.level,
|
|
220
|
+
title: heading.title,
|
|
221
|
+
start: offset,
|
|
222
|
+
at: offset + line.length
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
offset += line.length + 1;
|
|
226
|
+
}
|
|
227
|
+
return heads.map((head, index) => {
|
|
228
|
+
let end = text.length;
|
|
229
|
+
for (const later of heads.slice(index + 1)) {
|
|
230
|
+
if (later.level <= head.level) {
|
|
231
|
+
end = later.start;
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
level: head.level,
|
|
237
|
+
title: head.title,
|
|
238
|
+
body: text.slice(head.at, end)
|
|
239
|
+
};
|
|
240
|
+
});
|
|
241
|
+
};
|
|
242
|
+
var splitRowCells = (inner) => {
|
|
243
|
+
const cells = [];
|
|
244
|
+
let current = "";
|
|
245
|
+
let fenced = false;
|
|
246
|
+
for (let at = 0;at < inner.length; at += 1) {
|
|
247
|
+
const ch = inner[at];
|
|
248
|
+
if (ch === "\\" && inner[at + 1] === "|") {
|
|
249
|
+
current += "|";
|
|
250
|
+
at += 1;
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
if (ch === "`") {
|
|
254
|
+
fenced = !fenced;
|
|
255
|
+
current += ch;
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
if (ch === "|" && !fenced) {
|
|
259
|
+
cells.push(current.trim());
|
|
260
|
+
current = "";
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
current += ch;
|
|
264
|
+
}
|
|
265
|
+
cells.push(current.trim());
|
|
266
|
+
return cells;
|
|
267
|
+
};
|
|
268
|
+
var firstTable = (block) => {
|
|
269
|
+
let header = [];
|
|
270
|
+
const rows = [];
|
|
271
|
+
for (const line of block.split(`
|
|
272
|
+
`)) {
|
|
273
|
+
const stripped = line.trim();
|
|
274
|
+
if (!stripped.startsWith("|")) {
|
|
275
|
+
if (header.length > 0 && rows.length > 0)
|
|
276
|
+
break;
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
let inner = stripped;
|
|
280
|
+
if (inner.startsWith("|"))
|
|
281
|
+
inner = inner.slice(1);
|
|
282
|
+
if (inner.endsWith("|"))
|
|
283
|
+
inner = inner.slice(0, -1);
|
|
284
|
+
const cells = splitRowCells(inner);
|
|
285
|
+
if (cells.length > 0 && cells.filter((c) => c !== "").every(isSeparatorCell)) {
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
if (header.length === 0)
|
|
289
|
+
header = cells;
|
|
290
|
+
else
|
|
291
|
+
rows.push(cells);
|
|
292
|
+
}
|
|
293
|
+
return { header, rows };
|
|
294
|
+
};
|
|
295
|
+
var columnIndex = (header, ...names) => {
|
|
296
|
+
const wanted = new Set(names.map(norm));
|
|
297
|
+
const found = header.findIndex((c) => wanted.has(norm(c)));
|
|
298
|
+
return found === -1 ? undefined : found;
|
|
299
|
+
};
|
|
300
|
+
var cellAt = (row, index) => index !== undefined && index < row.length ? row[index] : "";
|
|
301
|
+
var INPUT_LABELS = ["Inputs", "Input Schema"];
|
|
302
|
+
var OUTPUT_LABELS = ["Outputs", "Output Schema"];
|
|
303
|
+
var afterAnyLabel = (block, labels) => afterBoldLabel(block, ...labels);
|
|
304
|
+
var unwrapCodeSpan = (value) => {
|
|
305
|
+
const text = value.trim();
|
|
306
|
+
let fence = 0;
|
|
307
|
+
while (fence < text.length && text[fence] === "`")
|
|
308
|
+
fence += 1;
|
|
309
|
+
if (fence === 0 || text.length < fence * 2)
|
|
310
|
+
return text;
|
|
311
|
+
if (text.slice(text.length - fence) !== "`".repeat(fence))
|
|
312
|
+
return text;
|
|
313
|
+
return text.slice(fence, text.length - fence).trim();
|
|
314
|
+
};
|
|
315
|
+
var stripTicks = (value) => {
|
|
316
|
+
const span = /^(`+)([\s\S]*?)\1/.exec(value.trim());
|
|
317
|
+
return (span ? span[2] : value).trim();
|
|
318
|
+
};
|
|
319
|
+
var field = (block, label) => {
|
|
320
|
+
const wanted = label.toLowerCase();
|
|
321
|
+
for (const line of block.split(`
|
|
322
|
+
`)) {
|
|
323
|
+
const text = line.trim();
|
|
324
|
+
if (!text.startsWith("**"))
|
|
325
|
+
continue;
|
|
326
|
+
const close = text.indexOf("**", 2);
|
|
327
|
+
if (close === -1)
|
|
328
|
+
continue;
|
|
329
|
+
let name = text.slice(2, close).trim().toLowerCase();
|
|
330
|
+
if (name.endsWith(":"))
|
|
331
|
+
name = name.slice(0, -1);
|
|
332
|
+
if (name !== wanted)
|
|
333
|
+
continue;
|
|
334
|
+
return text.slice(close + 2).trim();
|
|
335
|
+
}
|
|
336
|
+
return;
|
|
337
|
+
};
|
|
338
|
+
var declaresProse = (value) => value?.trim().startsWith("<") === false && !isBlank(value);
|
|
339
|
+
var firstProseLabel = (block, ...labels) => labels.find((label) => declaresProse(field(block, label)));
|
|
340
|
+
var noNameColumnNote = (title, wanted, header) => `section "${title}": the table has rows but no ${wanted.join("/")} ` + `column (header: ${header.join(" | ") || "<none>"}) — every row is ` + "skipped, so the section contributes nothing to audit";
|
|
341
|
+
var capitalize = (text) => text.charAt(0).toUpperCase() + text.slice(1);
|
|
342
|
+
var noCategoryColumnNote = (title, header) => `section "${title}": the table has rows but no Category column ` + `(header: ${header.join(" | ") || "<none>"}) — no row can be checked ` + "for `variables` group placement, so a formal argument missing half its " + "pair passes";
|
|
343
|
+
var unrecognizedHeadingNote = (title, level) => `heading "${title}" (level ${level}) reads like an audited section but ` + "matches no accepted spelling — rename it to one of the canonical " + "headings, or the checks it feeds are skipped";
|
|
344
|
+
var entryConditionBlocks = (block) => {
|
|
345
|
+
const blocks = [];
|
|
346
|
+
for (const sub of sectionBlocks(block)) {
|
|
347
|
+
if (norm(sub.title).startsWith(norm("Entry Condition"))) {
|
|
348
|
+
blocks.push(sub.body);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
const lines = block.split(`
|
|
352
|
+
`);
|
|
353
|
+
const isInlineLabel = (line) => line.trim().startsWith("**") && norm(line.replaceAll("*", "")).startsWith(norm("Entry Condition"));
|
|
354
|
+
const at = lines.findIndex(isInlineLabel);
|
|
355
|
+
if (at === -1)
|
|
356
|
+
return blocks;
|
|
357
|
+
const tail = [];
|
|
358
|
+
for (const line of lines.slice(at + 1)) {
|
|
359
|
+
const text = line.trim();
|
|
360
|
+
if (text.startsWith("**") || readHeading(text))
|
|
361
|
+
break;
|
|
362
|
+
tail.push(line);
|
|
363
|
+
}
|
|
364
|
+
blocks.push(tail.join(`
|
|
365
|
+
`));
|
|
366
|
+
return blocks;
|
|
367
|
+
};
|
|
368
|
+
var detailEntryRows = (block) => entryConditionBlocks(block).reduce((most, text) => Math.max(most, firstTable(text).rows.length), 0);
|
|
369
|
+
var afterBoldLabel = (block, ...labels) => {
|
|
370
|
+
const hits = [];
|
|
371
|
+
for (const label of labels) {
|
|
372
|
+
const opener = `**${label}`;
|
|
373
|
+
let at = block.indexOf(opener);
|
|
374
|
+
while (at !== -1) {
|
|
375
|
+
const from = at + opener.length;
|
|
376
|
+
if (!isWordChar(block.charAt(from)))
|
|
377
|
+
hits.push({ at, from });
|
|
378
|
+
at = block.indexOf(opener, from);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
hits.sort((left, right) => left.at - right.at);
|
|
382
|
+
return hits.map(({ from }, index) => block.slice(from, hits[index + 1]?.at ?? block.length));
|
|
383
|
+
};
|
|
384
|
+
var isIdentifier = (text) => {
|
|
385
|
+
const head = text.charAt(0);
|
|
386
|
+
if (!isLetter(head) && head !== "_")
|
|
387
|
+
return false;
|
|
388
|
+
for (const ch of text.slice(1)) {
|
|
389
|
+
if (!isWordChar(ch))
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
return true;
|
|
393
|
+
};
|
|
394
|
+
var detailInputRows = (block) => {
|
|
395
|
+
const rowsOut = [];
|
|
396
|
+
for (const sub of afterAnyLabel(block, INPUT_LABELS)) {
|
|
397
|
+
const { header, rows } = firstTable(sub);
|
|
398
|
+
const nameAt = columnIndex(header, "Name", "Input", "Field");
|
|
399
|
+
const valueAt = columnIndex(header, "Binding / Value", "Binding", "Value");
|
|
400
|
+
if (nameAt === undefined || valueAt === undefined)
|
|
401
|
+
continue;
|
|
402
|
+
for (const row of rows) {
|
|
403
|
+
const name = stripTicks(cellAt(row, nameAt).trim());
|
|
404
|
+
const raw = stripTicks(cellAt(row, valueAt).trim());
|
|
405
|
+
if (!name || isBlank(name) || isBlank(raw))
|
|
406
|
+
continue;
|
|
407
|
+
rowsOut.push({ name, raw });
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return rowsOut;
|
|
411
|
+
};
|
|
412
|
+
var isBinding = (raw) => raw.startsWith("=") || raw.startsWith("<-");
|
|
413
|
+
var EMPTY_LITERAL = /^""\s*\(empty literal\)$/i;
|
|
414
|
+
var detailLiteralInputs = (block) => detailInputRows(block).filter(({ raw }) => !isBinding(raw)).map(({ name, raw }) => ({
|
|
415
|
+
name,
|
|
416
|
+
value: EMPTY_LITERAL.test(raw) ? "" : raw
|
|
417
|
+
}));
|
|
418
|
+
var detailBoundInputs = (block) => detailInputRows(block).filter(({ raw }) => isBinding(raw)).map(({ name, raw }) => ({ name, binding: raw }));
|
|
419
|
+
var detailExpressionInputs = (block) => {
|
|
420
|
+
const inputs = [];
|
|
421
|
+
for (const sub of afterAnyLabel(block, INPUT_LABELS)) {
|
|
422
|
+
const { header, rows } = firstTable(sub);
|
|
423
|
+
const nameAt = columnIndex(header, "Name", "Input", "Field");
|
|
424
|
+
const valueAt = columnIndex(header, "Binding / Value", "Binding", "Value");
|
|
425
|
+
if (nameAt === undefined || valueAt === undefined)
|
|
426
|
+
continue;
|
|
427
|
+
for (const row of rows) {
|
|
428
|
+
const name = stripTicks(cellAt(row, nameAt).trim());
|
|
429
|
+
const raw = unwrapCodeSpan(cellAt(row, valueAt));
|
|
430
|
+
if (!name || isBlank(name) || isBlank(raw))
|
|
431
|
+
continue;
|
|
432
|
+
if (!raw.startsWith("="))
|
|
433
|
+
continue;
|
|
434
|
+
inputs.push({ name, expression: raw });
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return inputs;
|
|
438
|
+
};
|
|
439
|
+
var PRODUCER_ARROW = "<-";
|
|
440
|
+
var XREF_OPEN = "$xref(";
|
|
441
|
+
var SEGMENT_QUOTES = new Set(['"', "'", "`", "“", "”"]);
|
|
442
|
+
var CLOSING_QUOTE = new Map([["“", "”"]]);
|
|
443
|
+
var closesQuote = (ch, quote) => ch === (CLOSING_QUOTE.get(quote) ?? quote);
|
|
444
|
+
var dottedSegments = (text) => {
|
|
445
|
+
const segments = [];
|
|
446
|
+
let current = "";
|
|
447
|
+
let quote;
|
|
448
|
+
for (const ch of text) {
|
|
449
|
+
if (quote !== undefined) {
|
|
450
|
+
if (closesQuote(ch, quote))
|
|
451
|
+
quote = undefined;
|
|
452
|
+
else
|
|
453
|
+
current += ch;
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
if (SEGMENT_QUOTES.has(ch)) {
|
|
457
|
+
quote = ch;
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
if (ch === ".") {
|
|
461
|
+
segments.push(current.trim());
|
|
462
|
+
current = "";
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
current += ch;
|
|
466
|
+
}
|
|
467
|
+
segments.push(current.trim());
|
|
468
|
+
return segments.filter((segment) => segment !== "");
|
|
469
|
+
};
|
|
470
|
+
var callCloseIndex = (text, from) => {
|
|
471
|
+
let quote;
|
|
472
|
+
for (let at = from;at < text.length; at += 1) {
|
|
473
|
+
const ch = text[at];
|
|
474
|
+
if (quote !== undefined) {
|
|
475
|
+
if (closesQuote(ch, quote))
|
|
476
|
+
quote = undefined;
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
if (SEGMENT_QUOTES.has(ch)) {
|
|
480
|
+
quote = ch;
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
if (ch === ")")
|
|
484
|
+
return at;
|
|
485
|
+
}
|
|
486
|
+
return -1;
|
|
487
|
+
};
|
|
488
|
+
var splitCallArguments = (text) => {
|
|
489
|
+
const parts = [];
|
|
490
|
+
let current = "";
|
|
491
|
+
let quote;
|
|
492
|
+
for (const ch of text) {
|
|
493
|
+
if (quote !== undefined) {
|
|
494
|
+
current += ch;
|
|
495
|
+
if (closesQuote(ch, quote))
|
|
496
|
+
quote = undefined;
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
if (SEGMENT_QUOTES.has(ch)) {
|
|
500
|
+
quote = ch;
|
|
501
|
+
current += ch;
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
if (ch === ",") {
|
|
505
|
+
parts.push(current);
|
|
506
|
+
current = "";
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
current += ch;
|
|
510
|
+
}
|
|
511
|
+
parts.push(current);
|
|
512
|
+
return parts;
|
|
513
|
+
};
|
|
514
|
+
var xrefArguments = (text) => {
|
|
515
|
+
const open = text.indexOf(XREF_OPEN);
|
|
516
|
+
if (open === -1)
|
|
517
|
+
return [];
|
|
518
|
+
const close = callCloseIndex(text, open);
|
|
519
|
+
if (close === -1)
|
|
520
|
+
return [];
|
|
521
|
+
return splitCallArguments(text.slice(open + XREF_OPEN.length, close)).map((part) => {
|
|
522
|
+
let value = part.trim();
|
|
523
|
+
while (value.length > 0 && SEGMENT_QUOTES.has(value[0])) {
|
|
524
|
+
value = value.slice(1);
|
|
525
|
+
}
|
|
526
|
+
while (value.length > 0 && SEGMENT_QUOTES.has(value.at(-1) ?? "")) {
|
|
527
|
+
value = value.slice(0, -1);
|
|
528
|
+
}
|
|
529
|
+
return value.trim();
|
|
530
|
+
}).filter((part) => part !== "");
|
|
531
|
+
};
|
|
532
|
+
var producerSegments = (raw) => {
|
|
533
|
+
if (raw.includes(XREF_OPEN))
|
|
534
|
+
return xrefArguments(raw);
|
|
535
|
+
if (raw.startsWith(PRODUCER_ARROW)) {
|
|
536
|
+
return dottedSegments(raw.slice(PRODUCER_ARROW.length).trim());
|
|
537
|
+
}
|
|
538
|
+
return [];
|
|
539
|
+
};
|
|
540
|
+
var readProducer = (raw) => {
|
|
541
|
+
const segments = producerSegments(raw);
|
|
542
|
+
if (segments.length !== 3)
|
|
543
|
+
return;
|
|
544
|
+
const [stage, task, output] = segments;
|
|
545
|
+
return stage && task && output ? { stage, task, output } : undefined;
|
|
546
|
+
};
|
|
547
|
+
var xrefProducers = (text) => {
|
|
548
|
+
const found = [];
|
|
549
|
+
const seen = new Set;
|
|
550
|
+
let cursor = 0;
|
|
551
|
+
while (cursor < text.length) {
|
|
552
|
+
const at = text.indexOf(XREF_OPEN, cursor);
|
|
553
|
+
if (at === -1)
|
|
554
|
+
break;
|
|
555
|
+
const close = callCloseIndex(text, at);
|
|
556
|
+
if (close === -1)
|
|
557
|
+
break;
|
|
558
|
+
const args = xrefArguments(text.slice(at, close + 1));
|
|
559
|
+
if (args.length === 3) {
|
|
560
|
+
const key = `${norm(args[0])}|${norm(args[1])}|${norm(args[2])}`;
|
|
561
|
+
if (!seen.has(key)) {
|
|
562
|
+
seen.add(key);
|
|
563
|
+
found.push({ stage: args[0], task: args[1], output: args[2] });
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
cursor = close + 1;
|
|
567
|
+
}
|
|
568
|
+
return found;
|
|
569
|
+
};
|
|
570
|
+
var detailProducerInputs = (block) => {
|
|
571
|
+
const inputs = [];
|
|
572
|
+
for (const sub of afterAnyLabel(block, INPUT_LABELS)) {
|
|
573
|
+
const { header, rows } = firstTable(sub);
|
|
574
|
+
const nameAt = columnIndex(header, "Name", "Input", "Field");
|
|
575
|
+
const valueAt = columnIndex(header, "Binding / Value", "Binding", "Value");
|
|
576
|
+
if (nameAt === undefined || valueAt === undefined)
|
|
577
|
+
continue;
|
|
578
|
+
for (const row of rows) {
|
|
579
|
+
const name = stripTicks(cellAt(row, nameAt).trim());
|
|
580
|
+
const raw = unwrapCodeSpan(cellAt(row, valueAt));
|
|
581
|
+
if (!name || isBlank(name) || isBlank(raw))
|
|
582
|
+
continue;
|
|
583
|
+
const producer = readProducer(raw);
|
|
584
|
+
if (producer !== undefined) {
|
|
585
|
+
inputs.push({
|
|
586
|
+
name,
|
|
587
|
+
producer,
|
|
588
|
+
direct: raw.trimStart().startsWith(PRODUCER_ARROW)
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
return inputs;
|
|
594
|
+
};
|
|
595
|
+
var detailOutputRows = (block) => {
|
|
596
|
+
const names = [];
|
|
597
|
+
for (const sub of afterAnyLabel(block, OUTPUT_LABELS)) {
|
|
598
|
+
const { header, rows } = firstTable(sub);
|
|
599
|
+
const nameAt = columnIndex(header, "Field", "Name", "Output");
|
|
600
|
+
if (nameAt === undefined)
|
|
601
|
+
continue;
|
|
602
|
+
for (const row of rows) {
|
|
603
|
+
const name = stripTicks(cellAt(row, nameAt).trim());
|
|
604
|
+
if (!name || isBlank(name))
|
|
605
|
+
continue;
|
|
606
|
+
if (!names.includes(name))
|
|
607
|
+
names.push(name);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
return names;
|
|
611
|
+
};
|
|
612
|
+
var SLA_STATUS_AT_RISK = new Set(["at risk", "atrisk", "at risk warning"]);
|
|
613
|
+
var SLA_STATUS_BREACH = new Set(["breached", "breach"]);
|
|
614
|
+
var slaEscalationsOf = (block) => {
|
|
615
|
+
const { header, rows } = firstTable(block);
|
|
616
|
+
const atRiskAt = columnIndex(header, "At-Risk Action", "At Risk Action");
|
|
617
|
+
const breachAt = columnIndex(header, "Breach Action", "Breached Action");
|
|
618
|
+
const declaredColumn = (at) => at !== undefined && rows.some((row) => {
|
|
619
|
+
const cell = stripTicks(cellAt(row, at).trim());
|
|
620
|
+
return cell !== "" && !isBlank(cell);
|
|
621
|
+
});
|
|
622
|
+
let atRisk = declaredColumn(atRiskAt);
|
|
623
|
+
let breach = declaredColumn(breachAt);
|
|
624
|
+
const statusAt = columnIndex(header, "SLA Status", "Status");
|
|
625
|
+
const actionAt = columnIndex(header, "Action", "Escalation Action", "Response");
|
|
626
|
+
if (statusAt === undefined || actionAt === undefined) {
|
|
627
|
+
return { atRisk, breach };
|
|
628
|
+
}
|
|
629
|
+
for (const row of rows) {
|
|
630
|
+
const action = stripTicks(cellAt(row, actionAt).trim());
|
|
631
|
+
if (action === "" || isBlank(action))
|
|
632
|
+
continue;
|
|
633
|
+
const status = norm(stripTicks(cellAt(row, statusAt)));
|
|
634
|
+
if (SLA_STATUS_AT_RISK.has(status))
|
|
635
|
+
atRisk = true;
|
|
636
|
+
else if (SLA_STATUS_BREACH.has(status))
|
|
637
|
+
breach = true;
|
|
638
|
+
}
|
|
639
|
+
return { atRisk, breach };
|
|
640
|
+
};
|
|
641
|
+
var detailExtractRows = (block) => {
|
|
642
|
+
const rows = [];
|
|
643
|
+
for (const sub of afterAnyLabel(block, OUTPUT_LABELS)) {
|
|
644
|
+
const { header, rows: tableRows } = firstTable(sub);
|
|
645
|
+
const fieldAt = columnIndex(header, "Field", "Name", "Output");
|
|
646
|
+
const bindingAt = columnIndex(header, "Binding / Value", "Binding", "Value");
|
|
647
|
+
if (fieldAt === undefined || bindingAt === undefined)
|
|
648
|
+
continue;
|
|
649
|
+
for (const row of tableRows) {
|
|
650
|
+
const field2 = stripTicks(cellAt(row, fieldAt).trim());
|
|
651
|
+
const binding = unwrapCodeSpan(cellAt(row, bindingAt));
|
|
652
|
+
if (!field2 || isBlank(field2) || !binding.startsWith("->"))
|
|
653
|
+
continue;
|
|
654
|
+
const variable = binding.slice(2).trim();
|
|
655
|
+
if (!variable || isBlank(variable))
|
|
656
|
+
continue;
|
|
657
|
+
rows.push({ field: field2, variable });
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
return rows;
|
|
661
|
+
};
|
|
662
|
+
var detailCustomRows = (block) => {
|
|
663
|
+
const rows = [];
|
|
664
|
+
for (const sub of afterAnyLabel(block, OUTPUT_LABELS)) {
|
|
665
|
+
const { header, rows: tableRows } = firstTable(sub);
|
|
666
|
+
const bindingAt = columnIndex(header, "Binding / Value", "Binding", "Value");
|
|
667
|
+
if (bindingAt === undefined)
|
|
668
|
+
continue;
|
|
669
|
+
for (const row of tableRows) {
|
|
670
|
+
const binding = unwrapCodeSpan(cellAt(row, bindingAt));
|
|
671
|
+
const at = binding.indexOf("=");
|
|
672
|
+
if (at <= 0)
|
|
673
|
+
continue;
|
|
674
|
+
const variable = binding.slice(0, at).trim();
|
|
675
|
+
const expression = binding.slice(at + 1).trim();
|
|
676
|
+
if (!isIdentifier(variable))
|
|
677
|
+
continue;
|
|
678
|
+
if (!expression || isBlank(expression))
|
|
679
|
+
continue;
|
|
680
|
+
rows.push({ variable, expression });
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
return rows;
|
|
684
|
+
};
|
|
685
|
+
var timerFields = (block) => {
|
|
686
|
+
const declared = stripTicks(field(block, "Timer") ?? "");
|
|
687
|
+
if (!declared || isBlank(declared))
|
|
688
|
+
return {};
|
|
689
|
+
if (TIMER_VALUE_FIELDS.has(norm(declared).replaceAll(" ", ""))) {
|
|
690
|
+
const value2 = stripTicks(field(block, "Value") ?? "");
|
|
691
|
+
return {
|
|
692
|
+
timerType: declared,
|
|
693
|
+
...value2 && !isBlank(value2) && { timerValue: value2 }
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
const configured = stripTicks(field(block, "Timer Configuration") ?? "");
|
|
697
|
+
const asType = TIMER_CONFIGURATION_TYPES.get(norm(configured));
|
|
698
|
+
if (asType === undefined)
|
|
699
|
+
return {};
|
|
700
|
+
const value = stripTicks(field(block, "Value") ?? "");
|
|
701
|
+
return {
|
|
702
|
+
timerType: asType,
|
|
703
|
+
...value && !isBlank(value) && { timerValue: value }
|
|
704
|
+
};
|
|
705
|
+
};
|
|
706
|
+
var readTaskDetail = (block) => {
|
|
707
|
+
const identity = [
|
|
708
|
+
field(block, "Resource Identity"),
|
|
709
|
+
field(block, "Folder Path"),
|
|
710
|
+
field(block, "Connection ID"),
|
|
711
|
+
field(block, "Activity Type ID")
|
|
712
|
+
].filter((value) => !!value && !isBlank(value));
|
|
713
|
+
return {
|
|
714
|
+
type: stripTicks(field(block, "Type") ?? ""),
|
|
715
|
+
resolvedResource: field(block, "Resolved Resource"),
|
|
716
|
+
identityResolved: identity.length > 0 && !identity.some(hasUnresolvedMarker),
|
|
717
|
+
folder: field(block, "Deployment Folder") ?? field(block, "Folder Path"),
|
|
718
|
+
proseLabel: firstProseLabel(block, "Description", "Design Rationale"),
|
|
719
|
+
entryRows: detailEntryRows(block),
|
|
720
|
+
entryConditions: entryConditionBlocks(block).flatMap(conditionRows),
|
|
721
|
+
expressionInputs: detailExpressionInputs(block),
|
|
722
|
+
producerInputs: detailProducerInputs(block),
|
|
723
|
+
outputRows: detailOutputRows(block),
|
|
724
|
+
literalInputs: detailLiteralInputs(block),
|
|
725
|
+
boundInputs: detailBoundInputs(block),
|
|
726
|
+
extractRows: detailExtractRows(block),
|
|
727
|
+
customRows: detailCustomRows(block),
|
|
728
|
+
...timerFields(block)
|
|
729
|
+
};
|
|
730
|
+
};
|
|
731
|
+
var yesNoCell = (cell) => {
|
|
732
|
+
const value = norm(stripTicks(cell.trim()));
|
|
733
|
+
if (value === "yes" || value === "true")
|
|
734
|
+
return true;
|
|
735
|
+
if (value === "no" || value === "false")
|
|
736
|
+
return false;
|
|
737
|
+
return;
|
|
738
|
+
};
|
|
739
|
+
var conditionRows = (block) => {
|
|
740
|
+
const { header, rows } = firstTable(block);
|
|
741
|
+
const nameAt = columnIndex(header, "Display Name", "Name");
|
|
742
|
+
const ifAt = columnIndex(header, "IF", "Condition");
|
|
743
|
+
if (nameAt === undefined)
|
|
744
|
+
return [];
|
|
745
|
+
const whenAt = columnIndex(header, "WHEN", "When", "Rule");
|
|
746
|
+
const exitAt = columnIndex(header, "Exit Type");
|
|
747
|
+
const interruptAt = columnIndex(header, "Interrupting");
|
|
748
|
+
const marksAt = columnIndex(header, "Marks Stage Complete", "Marks");
|
|
749
|
+
const declared = (row, at) => {
|
|
750
|
+
if (at === undefined)
|
|
751
|
+
return "";
|
|
752
|
+
const text = unwrapCodeSpan(cellAt(row, at));
|
|
753
|
+
return isBlank(text) ? "" : text;
|
|
754
|
+
};
|
|
755
|
+
const conditions = [];
|
|
756
|
+
for (const row of rows) {
|
|
757
|
+
const displayName = unwrapCodeSpan(cellAt(row, nameAt));
|
|
758
|
+
if (!displayName || isBlank(displayName))
|
|
759
|
+
continue;
|
|
760
|
+
const raw = ifAt === undefined ? "" : unwrapCodeSpan(cellAt(row, ifAt));
|
|
761
|
+
const when = declared(row, whenAt);
|
|
762
|
+
const exitType = declared(row, exitAt);
|
|
763
|
+
conditions.push({
|
|
764
|
+
displayName,
|
|
765
|
+
ifExpression: raw !== "" && !isBlank(raw) ? raw : undefined,
|
|
766
|
+
...ifAt !== undefined && (raw === "" || isBlank(raw)) && { ungated: true },
|
|
767
|
+
...when !== "" && { when },
|
|
768
|
+
...exitType !== "" && { exitType },
|
|
769
|
+
...interruptAt !== undefined && {
|
|
770
|
+
interrupting: yesNoCell(cellAt(row, interruptAt))
|
|
771
|
+
},
|
|
772
|
+
...marksAt !== undefined && {
|
|
773
|
+
marksComplete: yesNoCell(cellAt(row, marksAt))
|
|
774
|
+
}
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
return conditions;
|
|
778
|
+
};
|
|
779
|
+
var readTaskRows = (title, block, notes) => {
|
|
780
|
+
const { header, rows } = firstTable(block);
|
|
781
|
+
const wanted = ["Task Name", "Task", "Name"];
|
|
782
|
+
const nameAt = columnIndex(header, ...wanted);
|
|
783
|
+
if (rows.length > 0 && nameAt === undefined) {
|
|
784
|
+
notes.push(noNameColumnNote(title, wanted, header));
|
|
785
|
+
}
|
|
786
|
+
const typeAt = columnIndex(header, "Type", "Task Type");
|
|
787
|
+
const requiredAt = columnIndex(header, "Required");
|
|
788
|
+
const runOnceAt = columnIndex(header, "Run Only Once", "Run Once");
|
|
789
|
+
const tasks = [];
|
|
790
|
+
for (const row of rows) {
|
|
791
|
+
const taskName = cellAt(row, nameAt);
|
|
792
|
+
if (!taskName || isBlank(taskName))
|
|
793
|
+
continue;
|
|
794
|
+
tasks.push({
|
|
795
|
+
name: stripTicks(taskName.trim()),
|
|
796
|
+
type: stripTicks(cellAt(row, typeAt).trim()),
|
|
797
|
+
required: yesNoCell(cellAt(row, requiredAt)),
|
|
798
|
+
runOnlyOnce: yesNoCell(cellAt(row, runOnceAt)),
|
|
799
|
+
expressionInputs: [],
|
|
800
|
+
producerInputs: [],
|
|
801
|
+
outputRows: [],
|
|
802
|
+
literalInputs: [],
|
|
803
|
+
boundInputs: [],
|
|
804
|
+
entryConditions: [],
|
|
805
|
+
extractRows: [],
|
|
806
|
+
customRows: [],
|
|
807
|
+
identityResolved: false,
|
|
808
|
+
entryRows: 0
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
return tasks;
|
|
812
|
+
};
|
|
813
|
+
var applyStageSection = (stage, title, block, notes) => {
|
|
814
|
+
if (headingIs(title, STAGE_ENTRY_HEADINGS)) {
|
|
815
|
+
stage.entryRows = firstTable(block).rows.length;
|
|
816
|
+
stage.entryConditions = conditionRows(block);
|
|
817
|
+
} else if (headingIs(title, STAGE_EXIT_HEADINGS)) {
|
|
818
|
+
stage.exitRows = firstTable(block).rows.length;
|
|
819
|
+
stage.exitConditions = conditionRows(block);
|
|
820
|
+
} else if (headingIs(title, STAGE_CONDITIONAL_SLA_HEADINGS)) {
|
|
821
|
+
stage.conditionalSlas = conditionalSlaRows(block);
|
|
822
|
+
} else if (headingIs(title, STAGE_SLA_HEADINGS)) {
|
|
823
|
+
stage.hasSla = firstTable(block).rows.length > 0;
|
|
824
|
+
stage.slaEscalations = slaEscalationsOf(block);
|
|
825
|
+
} else if (headingIs(title, STAGE_TASK_HEADINGS)) {
|
|
826
|
+
stage.tasks.push(...readTaskRows(title, block, notes));
|
|
827
|
+
} else {
|
|
828
|
+
return false;
|
|
829
|
+
}
|
|
830
|
+
return true;
|
|
831
|
+
};
|
|
832
|
+
var conditionalSlaRows = (block) => {
|
|
833
|
+
const { header, rows } = firstTable(block);
|
|
834
|
+
const expressionAt = columnIndex(header, "Expression", "Condition");
|
|
835
|
+
const slaAt = columnIndex(header, "SLA", "Duration");
|
|
836
|
+
const unitAt = columnIndex(header, "Unit");
|
|
837
|
+
if (expressionAt === undefined || slaAt === undefined)
|
|
838
|
+
return [];
|
|
839
|
+
const found = [];
|
|
840
|
+
const nameAt = columnIndex(header, "Display Name", "Title");
|
|
841
|
+
for (const row of rows) {
|
|
842
|
+
const expression = (row[expressionAt] ?? "").trim();
|
|
843
|
+
if (expression === "" || expression.startsWith("<"))
|
|
844
|
+
continue;
|
|
845
|
+
const count = (row[slaAt] ?? "").trim();
|
|
846
|
+
const unit = unitAt === undefined ? "" : (row[unitAt] ?? "").trim();
|
|
847
|
+
const displayName = nameAt === undefined ? undefined : (row[nameAt] ?? "").trim();
|
|
848
|
+
found.push({
|
|
849
|
+
expression,
|
|
850
|
+
duration: unit === "" ? count : `${count} ${unit}`,
|
|
851
|
+
...displayName !== undefined && displayName !== "" && { displayName }
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
return found;
|
|
855
|
+
};
|
|
856
|
+
var parseStage = (name, body, notes) => {
|
|
857
|
+
const stage = {
|
|
858
|
+
conditionalSlas: [],
|
|
859
|
+
name: name.trim(),
|
|
860
|
+
tasks: [],
|
|
861
|
+
entryConditions: [],
|
|
862
|
+
exitConditions: [],
|
|
863
|
+
entryRows: 0,
|
|
864
|
+
exitRows: 0,
|
|
865
|
+
hasSla: false,
|
|
866
|
+
slaEscalations: { atRisk: false, breach: false }
|
|
867
|
+
};
|
|
868
|
+
const details = new Map;
|
|
869
|
+
const annotated = [];
|
|
870
|
+
const stageNotes = [];
|
|
871
|
+
const lookalikes = [];
|
|
872
|
+
for (const { title, body: block, level } of sectionBlocks(body)) {
|
|
873
|
+
if (level === 4) {
|
|
874
|
+
if (applyStageSection(stage, title, block, stageNotes))
|
|
875
|
+
continue;
|
|
876
|
+
const topic = topicOf(title);
|
|
877
|
+
if (topic === "conditions" || topic === "sla" || topic === "tasks") {
|
|
878
|
+
lookalikes.push([title, topic]);
|
|
879
|
+
}
|
|
880
|
+
continue;
|
|
881
|
+
}
|
|
882
|
+
if (level !== 5)
|
|
883
|
+
continue;
|
|
884
|
+
const detailName = readTaskHeading(title);
|
|
885
|
+
if (detailName !== undefined) {
|
|
886
|
+
const detail = readTaskDetail(block);
|
|
887
|
+
details.set(norm(detailName), detail);
|
|
888
|
+
annotated.push({
|
|
889
|
+
loose: normAnnotationless(detailName),
|
|
890
|
+
detail
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
for (const task of stage.tasks) {
|
|
895
|
+
let detail = details.get(norm(task.name));
|
|
896
|
+
if (detail === undefined) {
|
|
897
|
+
const loose = normAnnotationless(task.name);
|
|
898
|
+
const hits = annotated.filter((entry) => entry.loose === loose);
|
|
899
|
+
if (hits.length === 1)
|
|
900
|
+
detail = hits[0].detail;
|
|
901
|
+
}
|
|
902
|
+
if (!detail)
|
|
903
|
+
continue;
|
|
904
|
+
task.expressionInputs = detail.expressionInputs;
|
|
905
|
+
task.producerInputs = detail.producerInputs;
|
|
906
|
+
task.outputRows = detail.outputRows;
|
|
907
|
+
task.literalInputs = detail.literalInputs;
|
|
908
|
+
task.boundInputs = detail.boundInputs;
|
|
909
|
+
task.extractRows = detail.extractRows;
|
|
910
|
+
task.customRows = detail.customRows;
|
|
911
|
+
task.resolvedResource = detail.resolvedResource;
|
|
912
|
+
task.identityResolved = detail.identityResolved;
|
|
913
|
+
task.folder = detail.folder;
|
|
914
|
+
task.proseLabel = detail.proseLabel;
|
|
915
|
+
task.entryRows = detail.entryRows;
|
|
916
|
+
task.entryConditions = detail.entryConditions;
|
|
917
|
+
task.timerType = detail.timerType;
|
|
918
|
+
task.timerValue = detail.timerValue;
|
|
919
|
+
if (!task.type && detail.type)
|
|
920
|
+
task.type = detail.type;
|
|
921
|
+
}
|
|
922
|
+
const empty = {
|
|
923
|
+
conditions: !(stage.entryRows && stage.exitRows),
|
|
924
|
+
sla: !stage.hasSla,
|
|
925
|
+
tasks: stage.tasks.length === 0
|
|
926
|
+
};
|
|
927
|
+
for (const [title, topic] of lookalikes) {
|
|
928
|
+
if (empty[topic])
|
|
929
|
+
stageNotes.push(unrecognizedHeadingNote(title, 4));
|
|
930
|
+
}
|
|
931
|
+
if (details.size > 0 && stage.tasks.length === 0 && stageNotes.length === 0) {
|
|
932
|
+
stageNotes.push(`${details.size} \`Task <n>.<m>:\` detail section(s) but no task ` + "row parsed — the `#### Tasks` table is missing or has no Task " + "Name column, so none of its tasks are audited");
|
|
933
|
+
}
|
|
934
|
+
notes.push(...stageNotes.map((note) => `stage "${stage.name}": ${note}`));
|
|
935
|
+
return stage;
|
|
936
|
+
};
|
|
937
|
+
var applyCaseSection = (sdd, title, body) => {
|
|
938
|
+
if (headingIs(title, CASE_EXIT_HEADINGS)) {
|
|
939
|
+
sdd.caseExitRows = firstTable(body).rows.length;
|
|
940
|
+
sdd.caseExitConditions = conditionRows(body);
|
|
941
|
+
} else if (headingIs(title, CASE_TRIGGER_HEADINGS)) {
|
|
942
|
+
sdd.triggers = firstTable(body).rows.length;
|
|
943
|
+
} else if (headingIs(title, CASE_CONDITIONAL_SLA_HEADINGS)) {
|
|
944
|
+
sdd.caseConditionalSlas = conditionalSlaRows(body);
|
|
945
|
+
} else if (headingIs(title, CASE_SLA_HEADINGS)) {
|
|
946
|
+
sdd.caseSla = firstTable(body).rows.length > 0;
|
|
947
|
+
sdd.caseSlaEscalations = slaEscalationsOf(body);
|
|
948
|
+
} else if (headingIs(title, CASE_METADATA_HEADINGS)) {
|
|
949
|
+
const { header, rows } = firstTable(body);
|
|
950
|
+
const propertyAt = columnIndex(header, "Property", "Field", "Name");
|
|
951
|
+
const valueAt = columnIndex(header, "Value");
|
|
952
|
+
if (propertyAt !== undefined && valueAt !== undefined) {
|
|
953
|
+
for (const row of rows) {
|
|
954
|
+
if (norm(cellAt(row, propertyAt)) !== norm(CASE_SLA_PROPERTY)) {
|
|
955
|
+
continue;
|
|
956
|
+
}
|
|
957
|
+
const value = stripTicks(cellAt(row, valueAt));
|
|
958
|
+
if (!isBlank(value))
|
|
959
|
+
sdd.caseSlaDuration = value;
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
} else if (headingIs(title, CASE_VARIABLE_HEADINGS)) {
|
|
963
|
+
sdd.sawVariableSection = true;
|
|
964
|
+
const { header, rows } = firstTable(body);
|
|
965
|
+
const wanted = ["Name", "Variable", "Variable Name"];
|
|
966
|
+
const nameAt = columnIndex(header, ...wanted);
|
|
967
|
+
const categoryAt = columnIndex(header, "Category");
|
|
968
|
+
const defaultAt = columnIndex(header, "Default");
|
|
969
|
+
if (rows.length > 0 && nameAt === undefined) {
|
|
970
|
+
sdd.parseNotes.push(noNameColumnNote(title, wanted, header));
|
|
971
|
+
} else if (rows.length > 0 && categoryAt === undefined) {
|
|
972
|
+
sdd.parseNotes.push(noCategoryColumnNote(title, header));
|
|
973
|
+
}
|
|
974
|
+
for (const row of rows) {
|
|
975
|
+
const name = cellAt(row, nameAt);
|
|
976
|
+
if (name && !isBlank(name)) {
|
|
977
|
+
sdd.variables.push(stripTicks(name.trim()));
|
|
978
|
+
const category = norm(cellAt(row, categoryAt));
|
|
979
|
+
if (VARIABLE_PLACEMENT.has(category)) {
|
|
980
|
+
sdd.variableCategories.set(norm(name), category);
|
|
981
|
+
}
|
|
982
|
+
const cell = cellAt(row, defaultAt).trim();
|
|
983
|
+
const declared = trimQuotes(stripTicks(cell));
|
|
984
|
+
if (cell !== "" && !DEFAULT_UNFILLED.has(declared.toLowerCase()) && !DEFAULT_UNFILLED.has(cell.toLowerCase())) {
|
|
985
|
+
sdd.variableDefaults.set(norm(name), declared);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
} else {
|
|
990
|
+
return false;
|
|
991
|
+
}
|
|
992
|
+
return true;
|
|
993
|
+
};
|
|
994
|
+
var parseSdd = (text) => {
|
|
995
|
+
const sdd = {
|
|
996
|
+
stages: [],
|
|
997
|
+
caseExitConditions: [],
|
|
998
|
+
caseExitRows: 0,
|
|
999
|
+
triggers: 0,
|
|
1000
|
+
variables: [],
|
|
1001
|
+
sawVariableSection: false,
|
|
1002
|
+
variableCategories: new Map,
|
|
1003
|
+
variableDefaults: new Map,
|
|
1004
|
+
caseSla: false,
|
|
1005
|
+
caseSlaEscalations: { atRisk: false, breach: false },
|
|
1006
|
+
caseConditionalSlas: [],
|
|
1007
|
+
xrefOutputs: xrefProducers(text),
|
|
1008
|
+
parseNotes: []
|
|
1009
|
+
};
|
|
1010
|
+
const lookalikes = [];
|
|
1011
|
+
for (const { level, title, body } of sectionBlocks(text)) {
|
|
1012
|
+
if (level !== 3)
|
|
1013
|
+
continue;
|
|
1014
|
+
if (applyCaseSection(sdd, title, body))
|
|
1015
|
+
continue;
|
|
1016
|
+
const stageName = readStageHeading(title);
|
|
1017
|
+
if (stageName !== undefined) {
|
|
1018
|
+
sdd.stages.push(parseStage(stageName, body, sdd.parseNotes));
|
|
1019
|
+
continue;
|
|
1020
|
+
}
|
|
1021
|
+
const topic = topicOf(title);
|
|
1022
|
+
if (topic === "conditions" || topic === "variables" || topic === "triggers" || topic === "sla") {
|
|
1023
|
+
lookalikes.push([title, topic]);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
const empty = {
|
|
1027
|
+
conditions: !sdd.caseExitRows,
|
|
1028
|
+
variables: sdd.variables.length === 0,
|
|
1029
|
+
triggers: !sdd.triggers,
|
|
1030
|
+
sla: !sdd.caseSla
|
|
1031
|
+
};
|
|
1032
|
+
for (const [title, topic] of lookalikes) {
|
|
1033
|
+
if (empty[topic])
|
|
1034
|
+
sdd.parseNotes.push(unrecognizedHeadingNote(title, 3));
|
|
1035
|
+
}
|
|
1036
|
+
return sdd;
|
|
1037
|
+
};
|
|
1038
|
+
var asRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : undefined;
|
|
1039
|
+
var asArray = (value) => Array.isArray(value) ? value : [];
|
|
1040
|
+
var addressedByVar = (output) => output.custom === true;
|
|
1041
|
+
var bindingId = (expression) => {
|
|
1042
|
+
if (typeof expression !== "string")
|
|
1043
|
+
return;
|
|
1044
|
+
const text = expression.trim();
|
|
1045
|
+
const prefix = "=bindings.";
|
|
1046
|
+
if (!text.startsWith(prefix))
|
|
1047
|
+
return;
|
|
1048
|
+
const id = text.slice(prefix.length);
|
|
1049
|
+
const isWordChar2 = (ch) => isDigit(ch) || ch === "_" || isLowerLetter(ch.toLowerCase());
|
|
1050
|
+
return id !== "" && [...id].every(isWordChar2) ? id : undefined;
|
|
1051
|
+
};
|
|
1052
|
+
var bindingDefault = (bindings, expression) => {
|
|
1053
|
+
const id = bindingId(expression);
|
|
1054
|
+
if (id === undefined)
|
|
1055
|
+
return;
|
|
1056
|
+
const binding = bindings.find((b) => b.id === id);
|
|
1057
|
+
const value = binding?.default;
|
|
1058
|
+
return typeof value === "string" ? value : undefined;
|
|
1059
|
+
};
|
|
1060
|
+
var readPlanTasks = (data, bindings) => {
|
|
1061
|
+
const tasks = [];
|
|
1062
|
+
for (const lane of asArray(data.tasks)) {
|
|
1063
|
+
for (const task of asArray(lane)) {
|
|
1064
|
+
const record = asRecord(task);
|
|
1065
|
+
if (!record)
|
|
1066
|
+
continue;
|
|
1067
|
+
const taskData = asRecord(record.data) ?? {};
|
|
1068
|
+
const name = (typeof record.displayName === "string" ? record.displayName : undefined) ?? (typeof record.label === "string" ? record.label : undefined) ?? bindingDefault(bindings, taskData.name);
|
|
1069
|
+
tasks.push({
|
|
1070
|
+
name,
|
|
1071
|
+
id: typeof record.id === "string" ? record.id : undefined,
|
|
1072
|
+
type: typeof record.type === "string" ? record.type : undefined,
|
|
1073
|
+
isRequired: record.isRequired,
|
|
1074
|
+
shouldRunOnlyOnce: record.shouldRunOnlyOnce,
|
|
1075
|
+
description: record.description,
|
|
1076
|
+
entry: asArray(record.entryConditions),
|
|
1077
|
+
data: taskData
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
return tasks;
|
|
1082
|
+
};
|
|
1083
|
+
var readPlanVariables = (doc) => {
|
|
1084
|
+
const variablesDoc = asRecord(doc.variables) ?? {};
|
|
1085
|
+
const variables = [];
|
|
1086
|
+
const variableGroups = {};
|
|
1087
|
+
const variableEntries = {};
|
|
1088
|
+
for (const group of VARIABLE_GROUPS) {
|
|
1089
|
+
const names = [];
|
|
1090
|
+
const entries = new Map;
|
|
1091
|
+
for (const entry of asArray(variablesDoc[group])) {
|
|
1092
|
+
const record = asRecord(entry);
|
|
1093
|
+
if (record === undefined)
|
|
1094
|
+
continue;
|
|
1095
|
+
const name = record.name;
|
|
1096
|
+
if (typeof name !== "string")
|
|
1097
|
+
continue;
|
|
1098
|
+
names.push(name);
|
|
1099
|
+
if (!entries.has(norm(name)))
|
|
1100
|
+
entries.set(norm(name), record);
|
|
1101
|
+
}
|
|
1102
|
+
variableGroups[group] = new Set(names.map(norm));
|
|
1103
|
+
variableEntries[group] = entries;
|
|
1104
|
+
variables.push(...names);
|
|
1105
|
+
}
|
|
1106
|
+
return { variables, variableGroups, variableEntries };
|
|
1107
|
+
};
|
|
1108
|
+
var parseCaseplan = (doc) => {
|
|
1109
|
+
const bindings = asArray(doc.bindings).map(asRecord).filter((b) => b !== undefined);
|
|
1110
|
+
const metadata = asRecord(doc.metadata) ?? {};
|
|
1111
|
+
const plan = {
|
|
1112
|
+
stages: [],
|
|
1113
|
+
triggers: 0,
|
|
1114
|
+
...readPlanVariables(doc),
|
|
1115
|
+
bindings,
|
|
1116
|
+
caseExitConditions: asArray(metadata.caseExitRules),
|
|
1117
|
+
caseSla: asArray(metadata.slaRules).length > 0,
|
|
1118
|
+
caseSlaRules: asArray(metadata.slaRules)
|
|
1119
|
+
};
|
|
1120
|
+
for (const node of asArray(doc.nodes)) {
|
|
1121
|
+
const record = asRecord(node);
|
|
1122
|
+
if (!record)
|
|
1123
|
+
continue;
|
|
1124
|
+
const typed = record;
|
|
1125
|
+
if (isAnyTriggerNode(typed)) {
|
|
1126
|
+
plan.triggers += 1;
|
|
1127
|
+
continue;
|
|
1128
|
+
}
|
|
1129
|
+
if (!isStageNode(typed))
|
|
1130
|
+
continue;
|
|
1131
|
+
const data = asRecord(record.data) ?? {};
|
|
1132
|
+
plan.stages.push({
|
|
1133
|
+
name: (typeof data.label === "string" ? data.label : undefined) ?? (typeof data.displayName === "string" ? data.displayName : undefined),
|
|
1134
|
+
id: typeof record.id === "string" ? record.id : undefined,
|
|
1135
|
+
tasks: readPlanTasks(data, bindings),
|
|
1136
|
+
entry: asArray(data.entryConditions),
|
|
1137
|
+
exit: asArray(data.exitConditions),
|
|
1138
|
+
sla: asArray(data.slaRules)
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
return plan;
|
|
1142
|
+
};
|
|
1143
|
+
var resolveTasks = (sddTasks, planTasks) => {
|
|
1144
|
+
const bound = sddTasks.map(() => {
|
|
1145
|
+
return;
|
|
1146
|
+
});
|
|
1147
|
+
const taken = new Set;
|
|
1148
|
+
const free = (predicate) => planTasks.map((t, i) => ({ t, i })).filter(({ t, i }) => !taken.has(i) && predicate(t)).map(({ i }) => i);
|
|
1149
|
+
const bind = (row, index) => {
|
|
1150
|
+
bound[row] = index;
|
|
1151
|
+
taken.add(index);
|
|
1152
|
+
};
|
|
1153
|
+
sddTasks.forEach((task, row) => {
|
|
1154
|
+
const hits = free((t) => (t.name ?? "") === task.name);
|
|
1155
|
+
if (hits.length > 0)
|
|
1156
|
+
bind(row, hits[0]);
|
|
1157
|
+
});
|
|
1158
|
+
const ambiguous = new Set;
|
|
1159
|
+
sddTasks.forEach((task, row) => {
|
|
1160
|
+
if (bound[row] !== undefined)
|
|
1161
|
+
return;
|
|
1162
|
+
const key = norm(task.name);
|
|
1163
|
+
const hits = free((t) => norm(t.name) === key);
|
|
1164
|
+
if (hits.length === 1)
|
|
1165
|
+
bind(row, hits[0]);
|
|
1166
|
+
else if (hits.length > 1)
|
|
1167
|
+
ambiguous.add(row);
|
|
1168
|
+
});
|
|
1169
|
+
sddTasks.forEach((task, row) => {
|
|
1170
|
+
if (bound[row] !== undefined || ambiguous.has(row))
|
|
1171
|
+
return;
|
|
1172
|
+
const loose = normAnnotationless(task.name);
|
|
1173
|
+
const hits = free((t) => normAnnotationless(t.name) === loose);
|
|
1174
|
+
if (hits.length === 1)
|
|
1175
|
+
bind(row, hits[0]);
|
|
1176
|
+
});
|
|
1177
|
+
sddTasks.forEach((task, row) => {
|
|
1178
|
+
if (bound[row] !== undefined || ambiguous.has(row))
|
|
1179
|
+
return;
|
|
1180
|
+
const key = `${norm(task.name)} `;
|
|
1181
|
+
const hits = free((t) => norm(t.name).startsWith(key));
|
|
1182
|
+
if (hits.length === 1)
|
|
1183
|
+
bind(row, hits[0]);
|
|
1184
|
+
});
|
|
1185
|
+
return bound;
|
|
1186
|
+
};
|
|
1187
|
+
var ruleTotal = (conditions) => {
|
|
1188
|
+
let total = 0;
|
|
1189
|
+
for (const condition of conditions) {
|
|
1190
|
+
for (const group of asArray(asRecord(condition)?.rules)) {
|
|
1191
|
+
total += asArray(group).length;
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
return total;
|
|
1195
|
+
};
|
|
1196
|
+
var error = (code, path, message) => ({ code, path, message, severity: "error" });
|
|
1197
|
+
var warning = (code, path, message) => ({ code, path, message, severity: "warning" });
|
|
1198
|
+
var coverageFinding = (path, label, kind, rows, conditions) => {
|
|
1199
|
+
if (!rows)
|
|
1200
|
+
return [];
|
|
1201
|
+
const total = ruleTotal(conditions);
|
|
1202
|
+
if (total >= rows)
|
|
1203
|
+
return [];
|
|
1204
|
+
return [
|
|
1205
|
+
error("STRICT_SDD_CONDITION_MISSING", path, total === 0 ? `${label}: the SDD declares ${rows} ${kind} condition row(s), the caseplan has no ${kind} rules.` : `${label}: the SDD declares ${rows} ${kind} condition row(s), the caseplan keeps ${total} ${kind} rule(s) — rows may group into fewer conditions, never into fewer rules.`)
|
|
1206
|
+
];
|
|
1207
|
+
};
|
|
1208
|
+
var OPEN_QUOTES = new Set(['"', "“", "‘", "'"]);
|
|
1209
|
+
var CLOSE_QUOTES = new Set(['"', "”", "’", "'"]);
|
|
1210
|
+
var quotedArgs = (argumentList) => {
|
|
1211
|
+
const args = [];
|
|
1212
|
+
let at = 0;
|
|
1213
|
+
while (at < argumentList.length) {
|
|
1214
|
+
if (!OPEN_QUOTES.has(argumentList[at])) {
|
|
1215
|
+
at += 1;
|
|
1216
|
+
continue;
|
|
1217
|
+
}
|
|
1218
|
+
const from = at + 1;
|
|
1219
|
+
let end = from;
|
|
1220
|
+
while (end < argumentList.length && !CLOSE_QUOTES.has(argumentList[end])) {
|
|
1221
|
+
end += 1;
|
|
1222
|
+
}
|
|
1223
|
+
if (end >= argumentList.length)
|
|
1224
|
+
break;
|
|
1225
|
+
if (end > from)
|
|
1226
|
+
args.push(argumentList.slice(from, end));
|
|
1227
|
+
at = end + 1;
|
|
1228
|
+
}
|
|
1229
|
+
return args;
|
|
1230
|
+
};
|
|
1231
|
+
var findSlaCalls = (text) => {
|
|
1232
|
+
const calls = [];
|
|
1233
|
+
const name = "sla-status-change";
|
|
1234
|
+
text.split(`
|
|
1235
|
+
`).forEach((line, index) => {
|
|
1236
|
+
const lower = line.toLowerCase();
|
|
1237
|
+
let found = lower.indexOf(name);
|
|
1238
|
+
while (found !== -1) {
|
|
1239
|
+
const after = found + name.length;
|
|
1240
|
+
found = lower.indexOf(name, after);
|
|
1241
|
+
let open = after;
|
|
1242
|
+
while (open < line.length && (line[open] === " " || line[open] === "\t")) {
|
|
1243
|
+
open += 1;
|
|
1244
|
+
}
|
|
1245
|
+
if (!line.startsWith("(", open))
|
|
1246
|
+
continue;
|
|
1247
|
+
const close = callCloseIndex(line, open);
|
|
1248
|
+
if (close === -1)
|
|
1249
|
+
continue;
|
|
1250
|
+
const args = quotedArgs(line.slice(open + 1, close));
|
|
1251
|
+
if (args.length > 0)
|
|
1252
|
+
calls.push({ line: index + 1, args });
|
|
1253
|
+
}
|
|
1254
|
+
});
|
|
1255
|
+
return calls;
|
|
1256
|
+
};
|
|
1257
|
+
var slaReferenceFindings = (text, source) => {
|
|
1258
|
+
const findings = [];
|
|
1259
|
+
for (const call of findSlaCalls(text)) {
|
|
1260
|
+
const at = `${source}:${call.line}`;
|
|
1261
|
+
if (call.args.length !== 2 && call.args.length !== 3) {
|
|
1262
|
+
findings.push(error("STRICT_SDD_SLA_REFERENCE_ARITY", at, `sla-status-change needs 2 quoted args (breach) or 3 (at-risk); got ${call.args.length}.`));
|
|
1263
|
+
}
|
|
1264
|
+
if (call.args[0].trim().toLowerCase() === "case") {
|
|
1265
|
+
findings.push(error("STRICT_SDD_SLA_REFERENCE_TARGET", at, 'sla-status-change target "Case" — the case-level target is the literal "root".'));
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
return findings;
|
|
1269
|
+
};
|
|
1270
|
+
var folderFindings = (binding, declared, path, label) => {
|
|
1271
|
+
const actual = binding.default;
|
|
1272
|
+
if (typeof actual !== "string" || isBlank(actual))
|
|
1273
|
+
return [];
|
|
1274
|
+
if (comparableFolder(actual) === comparableFolder(declared))
|
|
1275
|
+
return [];
|
|
1276
|
+
const kind = GUID.test(actual.trim()) ? "that is a folder key, not a folder path — the pair resolves by path, so a key resolves nowhere off the machine that wrote it" : "that is a different folder, so the resource resolves in the wrong scope or not at all";
|
|
1277
|
+
return [
|
|
1278
|
+
error("STRICT_SDD_BINDING_FOLDER_MISMATCH", path, `${label}: binding "${String(binding.id)}" defaults its folderPath to "${actual}", and the SDD declares "${declared}" for this task — ${kind}. Set the binding's default from the SDD row.`)
|
|
1279
|
+
];
|
|
1280
|
+
};
|
|
1281
|
+
var GUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1282
|
+
var trimSlashes = (text) => {
|
|
1283
|
+
let start = 0;
|
|
1284
|
+
let end = text.length;
|
|
1285
|
+
while (start < end && text[start] === "/") {
|
|
1286
|
+
start += 1;
|
|
1287
|
+
}
|
|
1288
|
+
while (end > start && text[end - 1] === "/") {
|
|
1289
|
+
end -= 1;
|
|
1290
|
+
}
|
|
1291
|
+
return text.slice(start, end);
|
|
1292
|
+
};
|
|
1293
|
+
var comparableFolder = (text) => trimSlashes(text.trim()).toLowerCase();
|
|
1294
|
+
var bindingFindings = (plan, task, path, label, declaredFolder) => {
|
|
1295
|
+
const findings = [];
|
|
1296
|
+
for (const attribute of ["name", "folderPath"]) {
|
|
1297
|
+
const id = bindingId(task.data[attribute]);
|
|
1298
|
+
if (id === undefined)
|
|
1299
|
+
continue;
|
|
1300
|
+
const binding = plan.bindings.find((b) => b.id === id);
|
|
1301
|
+
if (!binding)
|
|
1302
|
+
continue;
|
|
1303
|
+
if (attribute === "folderPath" && declaredFolder !== undefined) {
|
|
1304
|
+
findings.push(...folderFindings(binding, declaredFolder, path, label));
|
|
1305
|
+
}
|
|
1306
|
+
const key = binding.resourceKey;
|
|
1307
|
+
if (typeof key === "string" && key !== "" && !key.split("/").at(-1)?.includes(".")) {
|
|
1308
|
+
findings.push(error("STRICT_SDD_BINDING_RESOURCE_KEY", path, `${label}: binding "${String(binding.id)}" has a bare resourceKey "${key}" — it needs the \`.<entry name>\` leaf suffix.`));
|
|
1309
|
+
}
|
|
1310
|
+
if (task.type === "api-workflow" && !binding.resourceSubType) {
|
|
1311
|
+
findings.push(error("STRICT_SDD_BINDING_RESOURCE_KEY", path, `${label}: api-workflow binding "${String(binding.id)}" is missing \`resourceSubType: "Api"\`.`));
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
return findings;
|
|
1315
|
+
};
|
|
1316
|
+
var parsesAsJson = (text) => catchError(() => JSON.parse(text))[0] === undefined;
|
|
1317
|
+
var sameLiteral = (declared, actual) => {
|
|
1318
|
+
if (declared === actual)
|
|
1319
|
+
return true;
|
|
1320
|
+
const parse = (text) => catchError(() => JSON.parse(text))[1];
|
|
1321
|
+
const canonical = (value) => {
|
|
1322
|
+
if (Array.isArray(value)) {
|
|
1323
|
+
return `[${value.map(canonical).join(",")}]`;
|
|
1324
|
+
}
|
|
1325
|
+
const record = asRecord(value);
|
|
1326
|
+
if (record) {
|
|
1327
|
+
return `{${Object.keys(record).sort((a2, b2) => a2.localeCompare(b2)).map((key) => `${JSON.stringify(key)}:${canonical(record[key])}`).join(",")}}`;
|
|
1328
|
+
}
|
|
1329
|
+
return JSON.stringify(value) ?? "null";
|
|
1330
|
+
};
|
|
1331
|
+
const a = parse(declared);
|
|
1332
|
+
const b = parse(actual);
|
|
1333
|
+
if (a === undefined && b === undefined) {
|
|
1334
|
+
return trimQuotes(declared) === trimQuotes(actual);
|
|
1335
|
+
}
|
|
1336
|
+
if (a === undefined || b === undefined) {
|
|
1337
|
+
const parsed = a ?? b;
|
|
1338
|
+
const structural = Array.isArray(parsed) || asRecord(parsed) !== undefined;
|
|
1339
|
+
return structural ? false : trimQuotes(declared) === trimQuotes(actual);
|
|
1340
|
+
}
|
|
1341
|
+
if (typeof a === "string" || typeof b === "string") {
|
|
1342
|
+
const left = typeof a === "string" ? a : declared;
|
|
1343
|
+
const right = typeof b === "string" ? b : actual;
|
|
1344
|
+
if (left === right)
|
|
1345
|
+
return true;
|
|
1346
|
+
}
|
|
1347
|
+
return canonical(a) === canonical(b);
|
|
1348
|
+
};
|
|
1349
|
+
var WHITESPACE = new Set([" ", "\t", `
|
|
1350
|
+
`, "\r"]);
|
|
1351
|
+
var withoutWhitespace = (text) => {
|
|
1352
|
+
let out = "";
|
|
1353
|
+
for (const ch of text) {
|
|
1354
|
+
if (!WHITESPACE.has(ch))
|
|
1355
|
+
out += ch;
|
|
1356
|
+
}
|
|
1357
|
+
return out;
|
|
1358
|
+
};
|
|
1359
|
+
var VARS_PREFIX = "vars.";
|
|
1360
|
+
var XREF_CALL_PREFIX = "$xref(";
|
|
1361
|
+
var VARIABLE_PLACEHOLDER = `${VARS_PREFIX}@`;
|
|
1362
|
+
var isReferenceChar = (ch) => ch >= "a" && ch <= "z" || ch >= "0" && ch <= "9" || ch === "_";
|
|
1363
|
+
var withCollapsedReferences = (text) => {
|
|
1364
|
+
let out = "";
|
|
1365
|
+
let index = 0;
|
|
1366
|
+
while (index < text.length) {
|
|
1367
|
+
if (!text.startsWith(VARS_PREFIX, index)) {
|
|
1368
|
+
out += text[index];
|
|
1369
|
+
index += 1;
|
|
1370
|
+
continue;
|
|
1371
|
+
}
|
|
1372
|
+
out += VARIABLE_PLACEHOLDER;
|
|
1373
|
+
index += VARS_PREFIX.length;
|
|
1374
|
+
if (text.startsWith(XREF_CALL_PREFIX, index)) {
|
|
1375
|
+
const close = callCloseIndex(text, index);
|
|
1376
|
+
index = close === -1 ? text.length : close + 1;
|
|
1377
|
+
continue;
|
|
1378
|
+
}
|
|
1379
|
+
while (index < text.length && isReferenceChar(text[index])) {
|
|
1380
|
+
index += 1;
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
return out;
|
|
1384
|
+
};
|
|
1385
|
+
var withoutJsPrefix = (text) => text.startsWith("=js:") ? `=${text.slice("=js:".length)}` : text;
|
|
1386
|
+
var withoutWrappingParens = (text) => {
|
|
1387
|
+
let current = text;
|
|
1388
|
+
for (;; ) {
|
|
1389
|
+
if (!current.startsWith("(") || !current.endsWith(")"))
|
|
1390
|
+
return current;
|
|
1391
|
+
let depth = 0;
|
|
1392
|
+
for (let index = 0;index < current.length; index += 1) {
|
|
1393
|
+
const ch = current[index];
|
|
1394
|
+
if (ch === "(")
|
|
1395
|
+
depth += 1;
|
|
1396
|
+
else if (ch === ")")
|
|
1397
|
+
depth -= 1;
|
|
1398
|
+
if (depth === 0 && index < current.length - 1)
|
|
1399
|
+
return current;
|
|
1400
|
+
}
|
|
1401
|
+
current = current.slice(1, -1);
|
|
1402
|
+
}
|
|
1403
|
+
};
|
|
1404
|
+
var normalizeExpression = (text) => {
|
|
1405
|
+
const bare = withoutWhitespace(withoutJsPrefix(text.trim())).replaceAll("'", '"').toLowerCase();
|
|
1406
|
+
const body = bare.startsWith("=") ? bare.slice(1) : bare;
|
|
1407
|
+
const unwrapped = withoutWrappingParens(body);
|
|
1408
|
+
return withCollapsedReferences(bare.startsWith("=") ? `=${unwrapped}` : unwrapped);
|
|
1409
|
+
};
|
|
1410
|
+
var wiringFindings = (task, found, path, label) => {
|
|
1411
|
+
const findings = [];
|
|
1412
|
+
const inputs = asArray(found.data.inputs).map(asRecord).filter((i) => i !== undefined);
|
|
1413
|
+
for (const declared of task.expressionInputs) {
|
|
1414
|
+
const input = inputs.find((i) => norm(i.name) === norm(declared.name));
|
|
1415
|
+
if (!input)
|
|
1416
|
+
continue;
|
|
1417
|
+
const actual = input.value;
|
|
1418
|
+
if (typeof actual !== "string" || actual === "")
|
|
1419
|
+
continue;
|
|
1420
|
+
if (!actual.startsWith("="))
|
|
1421
|
+
continue;
|
|
1422
|
+
if (normalizeExpression(actual) === normalizeExpression(declared.expression)) {
|
|
1423
|
+
continue;
|
|
1424
|
+
}
|
|
1425
|
+
findings.push(error("STRICT_SDD_INPUT_FORM_MISMATCH", `${path}.data.inputs[${declared.name}]`, `${label}: input "${declared.name}" is wired differently from the SDD's Inputs row for it. Re-read that row: a dropped conversion (a bare reference where the SDD wraps it) passes the wrong type and nothing else reports it.`));
|
|
1426
|
+
}
|
|
1427
|
+
return findings;
|
|
1428
|
+
};
|
|
1429
|
+
var referenceTokensIn = (text) => {
|
|
1430
|
+
const prefix = "vars.";
|
|
1431
|
+
const tokens = [];
|
|
1432
|
+
let index = 0;
|
|
1433
|
+
while (index < text.length) {
|
|
1434
|
+
if (!text.startsWith(prefix, index)) {
|
|
1435
|
+
index += 1;
|
|
1436
|
+
continue;
|
|
1437
|
+
}
|
|
1438
|
+
index += prefix.length;
|
|
1439
|
+
let token = "";
|
|
1440
|
+
while (index < text.length && isReferenceToken(text[index])) {
|
|
1441
|
+
token += text[index];
|
|
1442
|
+
index += 1;
|
|
1443
|
+
}
|
|
1444
|
+
if (token !== "")
|
|
1445
|
+
tokens.push(token);
|
|
1446
|
+
}
|
|
1447
|
+
return tokens;
|
|
1448
|
+
};
|
|
1449
|
+
var isReferenceToken = (ch) => ch >= "a" && ch <= "z" || ch >= "A" && ch <= "Z" || ch >= "0" && ch <= "9" || ch === "_";
|
|
1450
|
+
var planOutputFor = (plan, producer) => {
|
|
1451
|
+
for (const stage of plan.stages) {
|
|
1452
|
+
if (norm(stage.name) !== norm(producer.stage))
|
|
1453
|
+
continue;
|
|
1454
|
+
for (const planTask of stage.tasks) {
|
|
1455
|
+
if (norm(planTask.name) !== norm(producer.task))
|
|
1456
|
+
continue;
|
|
1457
|
+
for (const entry of asArray(planTask.data.outputs)) {
|
|
1458
|
+
const output = asRecord(entry);
|
|
1459
|
+
if (output === undefined)
|
|
1460
|
+
continue;
|
|
1461
|
+
for (const label of [output.name, output.var]) {
|
|
1462
|
+
if (typeof label === "string" && norm(label) === norm(producer.output)) {
|
|
1463
|
+
return output;
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
return;
|
|
1470
|
+
};
|
|
1471
|
+
var planHasTask = (plan, producer) => plan.stages.some((stage) => norm(stage.name) === norm(producer.stage) && stage.tasks.some((planTask) => norm(planTask.name) === norm(producer.task)));
|
|
1472
|
+
var xrefOutputFindings = (sdd, plan) => {
|
|
1473
|
+
const findings = [];
|
|
1474
|
+
for (const producer of sdd.xrefOutputs) {
|
|
1475
|
+
if (!planHasTask(plan, producer))
|
|
1476
|
+
continue;
|
|
1477
|
+
const output = planOutputFor(plan, producer);
|
|
1478
|
+
const path = `nodes[${producer.stage}].data.tasks[${producer.task}].data.outputs`;
|
|
1479
|
+
if (output === undefined) {
|
|
1480
|
+
findings.push(error("STRICT_SDD_OUTPUT_ROW_MISSING", path, `${producer.task}: the SDD references "${producer.output}" as an output of this task, and the task exposes no output with that name. Every reference to it reads nothing.`));
|
|
1481
|
+
continue;
|
|
1482
|
+
}
|
|
1483
|
+
if (addressedByVar(output))
|
|
1484
|
+
continue;
|
|
1485
|
+
const id = output.id;
|
|
1486
|
+
if (typeof id !== "string" || id.trim() === "") {
|
|
1487
|
+
findings.push(error("STRICT_SDD_OUTPUT_ROW_MISSING", `${path}[${producer.output}].id`, `${producer.task}: output "${producer.output}" is referenced by the SDD and carries no id. Nothing can address it, so the references resolve to nothing.`));
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
return findings;
|
|
1491
|
+
};
|
|
1492
|
+
var outputIds = (plan) => {
|
|
1493
|
+
const ids = new Set;
|
|
1494
|
+
for (const stage of plan.stages) {
|
|
1495
|
+
for (const planTask of stage.tasks) {
|
|
1496
|
+
for (const entry of asArray(planTask.data.outputs)) {
|
|
1497
|
+
const id = asRecord(entry)?.id;
|
|
1498
|
+
if (typeof id === "string" && id !== "")
|
|
1499
|
+
ids.add(id);
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
return ids;
|
|
1504
|
+
};
|
|
1505
|
+
var producerIndex = (plan) => {
|
|
1506
|
+
const index = new Map;
|
|
1507
|
+
const ids = outputIds(plan);
|
|
1508
|
+
for (const stage of plan.stages) {
|
|
1509
|
+
for (const planTask of stage.tasks) {
|
|
1510
|
+
for (const entry of asArray(planTask.data.outputs)) {
|
|
1511
|
+
const output = asRecord(entry);
|
|
1512
|
+
if (output === undefined)
|
|
1513
|
+
continue;
|
|
1514
|
+
const own = output.id;
|
|
1515
|
+
const tokens = new Set;
|
|
1516
|
+
for (const attribute of ["id", "name", "var"]) {
|
|
1517
|
+
const value = output[attribute];
|
|
1518
|
+
if (typeof value !== "string" || value === "")
|
|
1519
|
+
continue;
|
|
1520
|
+
if (attribute !== "id" && value !== own && ids.has(value)) {
|
|
1521
|
+
continue;
|
|
1522
|
+
}
|
|
1523
|
+
tokens.add(value);
|
|
1524
|
+
}
|
|
1525
|
+
if (tokens.size === 0)
|
|
1526
|
+
continue;
|
|
1527
|
+
for (const label of [output.name, output.var]) {
|
|
1528
|
+
if (typeof label !== "string" || label === "")
|
|
1529
|
+
continue;
|
|
1530
|
+
const key = `${norm(stage.name)}|${norm(planTask.name)}|${norm(label)}`;
|
|
1531
|
+
const bucket = index.get(key);
|
|
1532
|
+
if (bucket === undefined)
|
|
1533
|
+
index.set(key, [tokens]);
|
|
1534
|
+
else if (!bucket.includes(tokens))
|
|
1535
|
+
bucket.push(tokens);
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
return index;
|
|
1541
|
+
};
|
|
1542
|
+
var AUDIT_CONNECTOR_TASK_TYPES = new Set([
|
|
1543
|
+
"execute-connector-activity",
|
|
1544
|
+
"wait-for-connector"
|
|
1545
|
+
]);
|
|
1546
|
+
var CONNECTOR_ENVELOPE_INPUTS = new Set([
|
|
1547
|
+
"pathParameters",
|
|
1548
|
+
"queryParameters",
|
|
1549
|
+
"body"
|
|
1550
|
+
]);
|
|
1551
|
+
var resolveEnvelopeKey = (body, name) => {
|
|
1552
|
+
const named = (record, part) => Object.entries(record).find(([key]) => norm(key) === norm(part));
|
|
1553
|
+
const direct = named(body, name);
|
|
1554
|
+
if (direct !== undefined)
|
|
1555
|
+
return { value: direct[1] };
|
|
1556
|
+
const parts = name.split(".").filter((part) => part !== "");
|
|
1557
|
+
if (parts.length < 2)
|
|
1558
|
+
return;
|
|
1559
|
+
let current = body;
|
|
1560
|
+
for (const part of parts) {
|
|
1561
|
+
const record = asRecord(current);
|
|
1562
|
+
if (record === undefined)
|
|
1563
|
+
return;
|
|
1564
|
+
const hit = named(record, part);
|
|
1565
|
+
if (hit === undefined)
|
|
1566
|
+
return;
|
|
1567
|
+
current = hit[1];
|
|
1568
|
+
}
|
|
1569
|
+
return { value: current };
|
|
1570
|
+
};
|
|
1571
|
+
var carriedInput = (found, name) => {
|
|
1572
|
+
const absent = { present: false, value: undefined, valueless: false };
|
|
1573
|
+
const inputs = asArray(found.data.inputs).map(asRecord).filter((i) => i !== undefined);
|
|
1574
|
+
const direct = inputs.find((i) => norm(i.name) === norm(name));
|
|
1575
|
+
if (direct !== undefined) {
|
|
1576
|
+
return {
|
|
1577
|
+
present: true,
|
|
1578
|
+
value: direct.value,
|
|
1579
|
+
valueless: !("value" in direct)
|
|
1580
|
+
};
|
|
1581
|
+
}
|
|
1582
|
+
if (typeof found.type !== "string" || !AUDIT_CONNECTOR_TASK_TYPES.has(found.type)) {
|
|
1583
|
+
return absent;
|
|
1584
|
+
}
|
|
1585
|
+
for (const entry of inputs) {
|
|
1586
|
+
const entryName = entry.name;
|
|
1587
|
+
if (typeof entryName !== "string" || !CONNECTOR_ENVELOPE_INPUTS.has(entryName)) {
|
|
1588
|
+
continue;
|
|
1589
|
+
}
|
|
1590
|
+
const body = asRecord(entry.body);
|
|
1591
|
+
if (body === undefined)
|
|
1592
|
+
continue;
|
|
1593
|
+
const carried = resolveEnvelopeKey(body, name);
|
|
1594
|
+
if (carried !== undefined) {
|
|
1595
|
+
return { present: true, value: carried.value, valueless: false };
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
return absent;
|
|
1599
|
+
};
|
|
1600
|
+
var declaredInputNames = (task) => {
|
|
1601
|
+
const names = [];
|
|
1602
|
+
const add = (name) => {
|
|
1603
|
+
if (!names.some((existing) => norm(existing) === norm(name))) {
|
|
1604
|
+
names.push(name);
|
|
1605
|
+
}
|
|
1606
|
+
};
|
|
1607
|
+
for (const entry of task.literalInputs)
|
|
1608
|
+
add(entry.name);
|
|
1609
|
+
for (const entry of task.expressionInputs)
|
|
1610
|
+
add(entry.name);
|
|
1611
|
+
for (const entry of task.producerInputs)
|
|
1612
|
+
add(entry.name);
|
|
1613
|
+
return names;
|
|
1614
|
+
};
|
|
1615
|
+
var boundInputNames = (task) => task.boundInputs.map((entry) => entry.name);
|
|
1616
|
+
var emptyLiteralInputNames = (task) => task.literalInputs.filter((entry) => isBlank(entry.value)).map((entry) => entry.name);
|
|
1617
|
+
var envelopeFindings = (task, found, path, label) => {
|
|
1618
|
+
const findings = [];
|
|
1619
|
+
const columns = [
|
|
1620
|
+
{
|
|
1621
|
+
declared: task.required,
|
|
1622
|
+
actual: found.isRequired,
|
|
1623
|
+
column: "Required",
|
|
1624
|
+
field: "isRequired"
|
|
1625
|
+
},
|
|
1626
|
+
{
|
|
1627
|
+
declared: task.runOnlyOnce,
|
|
1628
|
+
actual: found.shouldRunOnlyOnce,
|
|
1629
|
+
column: "Run Only Once",
|
|
1630
|
+
field: "shouldRunOnlyOnce"
|
|
1631
|
+
}
|
|
1632
|
+
];
|
|
1633
|
+
for (const { declared, actual, column, field: field2 } of columns) {
|
|
1634
|
+
if (declared === undefined)
|
|
1635
|
+
continue;
|
|
1636
|
+
if (typeof actual === "boolean" && actual === declared)
|
|
1637
|
+
continue;
|
|
1638
|
+
findings.push(error("STRICT_SDD_TASK_ENVELOPE_MISMATCH", `${path}.${field2}`, `${label}: the SDD Tasks table declares a "${column}" value for this task and its \`${field2}\` ${typeof actual === "boolean" ? "disagrees" : "is absent"}. Set it from that column.`));
|
|
1639
|
+
}
|
|
1640
|
+
return findings;
|
|
1641
|
+
};
|
|
1642
|
+
var emptyInputFindings = (task, found, path, label) => {
|
|
1643
|
+
const declared = declaredInputNames(task);
|
|
1644
|
+
if (declared.length === 0)
|
|
1645
|
+
return [];
|
|
1646
|
+
if (Object.keys(found.data).length === 0)
|
|
1647
|
+
return [];
|
|
1648
|
+
const bound = boundInputNames(task);
|
|
1649
|
+
const emptyLiteral = emptyLiteralInputNames(task);
|
|
1650
|
+
const findings = [];
|
|
1651
|
+
const names = (list, name) => list.some((entry) => norm(entry) === norm(name));
|
|
1652
|
+
for (const name of declared) {
|
|
1653
|
+
const carried = carriedInput(found, name);
|
|
1654
|
+
const value = carried.value;
|
|
1655
|
+
const missing = !carried.present;
|
|
1656
|
+
if (!missing && !carried.valueless && (typeof value !== "string" || value.trim() !== "")) {
|
|
1657
|
+
continue;
|
|
1658
|
+
}
|
|
1659
|
+
if (!missing && (names(bound, name) || names(emptyLiteral, name))) {
|
|
1660
|
+
continue;
|
|
1661
|
+
}
|
|
1662
|
+
const declares = names(emptyLiteral, name) ? "the empty literal" : "a value";
|
|
1663
|
+
findings.push(error("STRICT_SDD_INPUT_EMPTY", `${path}.data.inputs[${name}]`, `${label}: the SDD Inputs row for "${name}" declares ${declares}, and the caseplan input ${missing ? "is absent" : carried.valueless ? "carries no `value` at all" : "is empty"}. Nothing reaches the task for it, and none of the value comparisons can run against an input that carries nothing. ${missing ? "The row exists, so the input has to exist too — add it, carrying that row's value." : carried.valueless ? "Its value has to live under `value` — data under `body` is the connector envelope, which does not apply to this task type." : "Re-read that row and set the input from it."}`));
|
|
1664
|
+
}
|
|
1665
|
+
return findings;
|
|
1666
|
+
};
|
|
1667
|
+
var comparableExpression = (text) => withoutWhitespace(text).replaceAll("'", '"');
|
|
1668
|
+
var resolveXrefExpression = (expression, index) => {
|
|
1669
|
+
const marker = `${VARS_PREFIX}${XREF_OPEN}`;
|
|
1670
|
+
const tokens = new Set;
|
|
1671
|
+
let expected = "";
|
|
1672
|
+
let cursor = 0;
|
|
1673
|
+
while (cursor < expression.length) {
|
|
1674
|
+
const at = expression.indexOf(marker, cursor);
|
|
1675
|
+
if (at === -1) {
|
|
1676
|
+
expected += expression.slice(cursor);
|
|
1677
|
+
break;
|
|
1678
|
+
}
|
|
1679
|
+
expected += expression.slice(cursor, at);
|
|
1680
|
+
const close = callCloseIndex(expression, at);
|
|
1681
|
+
if (close === -1)
|
|
1682
|
+
return;
|
|
1683
|
+
const call = expression.slice(at + VARS_PREFIX.length, close + 1);
|
|
1684
|
+
const args = xrefArguments(call);
|
|
1685
|
+
if (args.length !== 3)
|
|
1686
|
+
return;
|
|
1687
|
+
const candidates = index.get(`${norm(args[0])}|${norm(args[1])}|${norm(args[2])}`);
|
|
1688
|
+
if (candidates === undefined || candidates.length !== 1) {
|
|
1689
|
+
return;
|
|
1690
|
+
}
|
|
1691
|
+
for (const token of candidates[0])
|
|
1692
|
+
tokens.add(token);
|
|
1693
|
+
expected += VARIABLE_PLACEHOLDER;
|
|
1694
|
+
cursor = close + 1;
|
|
1695
|
+
}
|
|
1696
|
+
return { expected, tokens };
|
|
1697
|
+
};
|
|
1698
|
+
var ruleCarryingExpression = (condition) => {
|
|
1699
|
+
for (const lane of asArray(asRecord(condition)?.rules)) {
|
|
1700
|
+
for (const entry of asArray(lane)) {
|
|
1701
|
+
const rule = asRecord(entry);
|
|
1702
|
+
for (const key of ["conditionExpression", "expression"]) {
|
|
1703
|
+
const value = rule?.[key];
|
|
1704
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
1705
|
+
return rule;
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
return;
|
|
1711
|
+
};
|
|
1712
|
+
var conditionExpressionOf = (condition) => {
|
|
1713
|
+
for (const lane of asArray(asRecord(condition)?.rules)) {
|
|
1714
|
+
for (const entry of asArray(lane)) {
|
|
1715
|
+
const rule = asRecord(entry);
|
|
1716
|
+
for (const key of ["conditionExpression", "expression"]) {
|
|
1717
|
+
const value = rule?.[key];
|
|
1718
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
1719
|
+
return value;
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
return;
|
|
1725
|
+
};
|
|
1726
|
+
var predicateFinding = (row, condition, path, label) => {
|
|
1727
|
+
if (row.when === undefined)
|
|
1728
|
+
return [];
|
|
1729
|
+
const { rule: declared } = whenRule(row.when);
|
|
1730
|
+
if (!declared || isBlank(declared))
|
|
1731
|
+
return [];
|
|
1732
|
+
const carrier = ruleCarryingExpression(condition);
|
|
1733
|
+
const actual = carrier?.rule;
|
|
1734
|
+
if (typeof actual !== "string" || actual === "")
|
|
1735
|
+
return [];
|
|
1736
|
+
if (norm(actual) === norm(declared))
|
|
1737
|
+
return [];
|
|
1738
|
+
return [
|
|
1739
|
+
error("STRICT_SDD_CONDITION_RULE_MISMATCH", `${path}[${row.displayName}].rule`, `${label}: the SDD row "${row.displayName}" declares WHEN \`${declared}\`, and the rule carrying that row's gate uses \`${actual}\`. The gate itself is right, so every expression check passes; a different predicate fires it at a different moment, which builds a transition the SDD never described.`)
|
|
1740
|
+
];
|
|
1741
|
+
};
|
|
1742
|
+
var conditionExpressionFindings = (declared, planConditions, plan, path, label) => {
|
|
1743
|
+
const withMarkers = declared.filter((row) => row.ifExpression?.includes(XREF_OPEN) === true);
|
|
1744
|
+
if (withMarkers.length === 0)
|
|
1745
|
+
return [];
|
|
1746
|
+
const findings = [];
|
|
1747
|
+
const index = producerIndex(plan);
|
|
1748
|
+
for (const row of withMarkers) {
|
|
1749
|
+
const resolved = resolveXrefExpression(row.ifExpression ?? "", index);
|
|
1750
|
+
if (resolved === undefined)
|
|
1751
|
+
continue;
|
|
1752
|
+
const expected = comparableExpression(resolved.expected);
|
|
1753
|
+
const comparable = (actual2) => {
|
|
1754
|
+
let normalised = actual2;
|
|
1755
|
+
for (const token of resolved.tokens) {
|
|
1756
|
+
normalised = normalised.replaceAll(`${VARS_PREFIX}${token}`, VARIABLE_PLACEHOLDER);
|
|
1757
|
+
}
|
|
1758
|
+
return comparableExpression(normalised);
|
|
1759
|
+
};
|
|
1760
|
+
const match = planConditions.find((condition) => norm(asRecord(condition)?.displayName) === norm(row.displayName));
|
|
1761
|
+
const actual = match === undefined ? undefined : conditionExpressionOf(match);
|
|
1762
|
+
if (actual !== undefined) {
|
|
1763
|
+
if (comparable(actual) === expected) {
|
|
1764
|
+
findings.push(...predicateFinding(row, match, path, label));
|
|
1765
|
+
continue;
|
|
1766
|
+
}
|
|
1767
|
+
findings.push(error("STRICT_SDD_CONDITION_EXPRESSION_MISMATCH", `${path}[${row.displayName}].conditionExpression`, `${label}: the gate on "${row.displayName}" does not carry the expression its SDD row gives. SDD row: ${row.ifExpression} — which resolves to: ${[...resolved.tokens].join(" | ")} — caseplan: ${actual}. If the caseplan names something the row's reference does not resolve to, the gate reads a different value and opens on the wrong condition; that is not a formatting difference. A rewritten but equivalent form is also reported, because the row is preserved verbatim.`));
|
|
1768
|
+
continue;
|
|
1769
|
+
}
|
|
1770
|
+
if (planConditions.some((condition) => {
|
|
1771
|
+
const carried = conditionExpressionOf(condition);
|
|
1772
|
+
return carried !== undefined && comparable(carried) === expected;
|
|
1773
|
+
})) {
|
|
1774
|
+
continue;
|
|
1775
|
+
}
|
|
1776
|
+
findings.push(error("STRICT_SDD_CONDITION_MISSING", `${path}[${row.displayName}]`, `${label}: no ${label.startsWith("case") ? "case exit" : "condition"} rule carries the route the SDD row "${row.displayName}" describes — its IF expression appears on no rule, under any Display Name. A generic rule with no expression in its slot keeps the row count whole and drops the route, so the count checks pass. Re-read that row and add the gate it describes.`));
|
|
1777
|
+
}
|
|
1778
|
+
return findings;
|
|
1779
|
+
};
|
|
1780
|
+
var expressionShapeFindings = (task, found, plan, path, label) => {
|
|
1781
|
+
const withMarkers = task.expressionInputs.filter((entry) => entry.expression.includes(XREF_OPEN));
|
|
1782
|
+
if (withMarkers.length === 0)
|
|
1783
|
+
return [];
|
|
1784
|
+
const findings = [];
|
|
1785
|
+
const index = producerIndex(plan);
|
|
1786
|
+
const inputs = asArray(found.data.inputs).map(asRecord).filter((i) => i !== undefined);
|
|
1787
|
+
for (const declared of withMarkers) {
|
|
1788
|
+
const resolved = resolveXrefExpression(declared.expression, index);
|
|
1789
|
+
if (resolved === undefined)
|
|
1790
|
+
continue;
|
|
1791
|
+
const input = inputs.find((i) => norm(i.name) === norm(declared.name));
|
|
1792
|
+
const actual = input?.value;
|
|
1793
|
+
if (typeof actual !== "string" || actual.trim() === "")
|
|
1794
|
+
continue;
|
|
1795
|
+
let normalisedActual = actual;
|
|
1796
|
+
for (const token of resolved.tokens) {
|
|
1797
|
+
normalisedActual = normalisedActual.replaceAll(`${VARS_PREFIX}${token}`, VARIABLE_PLACEHOLDER);
|
|
1798
|
+
}
|
|
1799
|
+
if (comparableExpression(normalisedActual) === comparableExpression(resolved.expected)) {
|
|
1800
|
+
continue;
|
|
1801
|
+
}
|
|
1802
|
+
findings.push(warning("STRICT_SDD_EXPRESSION_MISMATCH", `${path}.data.inputs[${declared.name}]`, `${label}: input "${declared.name}" does not carry the expression its SDD Inputs row gives, once that row's reference is resolved. The row's value is preserved exactly, so a rewritten but equivalent form counts as a difference. Re-read that row.`));
|
|
1803
|
+
}
|
|
1804
|
+
return findings;
|
|
1805
|
+
};
|
|
1806
|
+
var producerFindings = (task, found, plan, path, label) => {
|
|
1807
|
+
if (task.producerInputs.length === 0)
|
|
1808
|
+
return [];
|
|
1809
|
+
const findings = [];
|
|
1810
|
+
const index = producerIndex(plan);
|
|
1811
|
+
const inputs = asArray(found.data.inputs).map(asRecord).filter((i) => i !== undefined);
|
|
1812
|
+
for (const declared of task.producerInputs) {
|
|
1813
|
+
const { stage, task: producerTask, output } = declared.producer;
|
|
1814
|
+
const candidates = index.get(`${norm(stage)}|${norm(producerTask)}|${norm(output)}`);
|
|
1815
|
+
if (candidates === undefined || candidates.length !== 1)
|
|
1816
|
+
continue;
|
|
1817
|
+
const input = inputs.find((i) => norm(i.name) === norm(declared.name));
|
|
1818
|
+
if (input === undefined)
|
|
1819
|
+
continue;
|
|
1820
|
+
const value = input.value;
|
|
1821
|
+
if (typeof value !== "string" || value.trim() === "")
|
|
1822
|
+
continue;
|
|
1823
|
+
const accepted = candidates[0];
|
|
1824
|
+
const tokens = referenceTokensIn(value);
|
|
1825
|
+
if (tokens.some((token) => accepted.has(token))) {
|
|
1826
|
+
if (!declared.direct)
|
|
1827
|
+
continue;
|
|
1828
|
+
if (!value.trimStart().startsWith("=js:"))
|
|
1829
|
+
continue;
|
|
1830
|
+
findings.push(error("STRICT_SDD_INPUT_FORM_MISMATCH", `${path}.data.inputs[${declared.name}]`, `${label}: input "${declared.name}" reads the output its SDD row names, but wraps it in a JS expression. The row is a direct binding (\`<- "${stage}"."${producerTask}".${output}\`), so the plan form is the bare reference — \`=vars.<id>\` — not \`=js:\`. Passing the value through an expression is a different runtime path from binding it.`));
|
|
1831
|
+
continue;
|
|
1832
|
+
}
|
|
1833
|
+
findings.push(error("STRICT_SDD_PRODUCER_MISMATCH", `${path}.data.inputs[${declared.name}]`, `${label}: input "${declared.name}" is not wired to the output its SDD row names — "${output}" on task "${producerTask}" in "${stage}". It reads something else, so the task runs on data the SDD did not intend. Re-read that Inputs row and point the input at that output.`));
|
|
1834
|
+
}
|
|
1835
|
+
return findings;
|
|
1836
|
+
};
|
|
1837
|
+
var outputRowFindings = (task, found, path, label) => {
|
|
1838
|
+
const findings = [];
|
|
1839
|
+
const outputs = asArray(found.data.outputs).map(asRecord).filter((o) => o !== undefined);
|
|
1840
|
+
for (const declared of task.outputRows) {
|
|
1841
|
+
const match = outputs.find((o) => [o.name, o.var, o.source].some((candidate) => {
|
|
1842
|
+
if (typeof candidate !== "string" || candidate === "") {
|
|
1843
|
+
return false;
|
|
1844
|
+
}
|
|
1845
|
+
const bare = candidate.startsWith("=") ? candidate.slice(1) : candidate;
|
|
1846
|
+
return norm(bare) === norm(declared) || norm(bare) === norm(declared.split(".").at(-1) ?? declared);
|
|
1847
|
+
}));
|
|
1848
|
+
if (match === undefined) {
|
|
1849
|
+
findings.push(error("STRICT_SDD_OUTPUT_ROW_MISSING", `${path}.data.outputs`, `${label}: the SDD Outputs table declares "${declared}", and no output on the task carries it as its name, \`var\` or \`source\`. Nothing downstream can bind to it, so every reference to it is dead.`));
|
|
1850
|
+
continue;
|
|
1851
|
+
}
|
|
1852
|
+
if (addressedByVar(match))
|
|
1853
|
+
continue;
|
|
1854
|
+
const id = match.id;
|
|
1855
|
+
if (typeof id !== "string" || id.trim() === "") {
|
|
1856
|
+
findings.push(error("STRICT_SDD_OUTPUT_ROW_MISSING", `${path}.data.outputs[${declared}].id`, `${label}: output "${declared}" carries no id. An output without one cannot be referenced, so the SDD row it satisfies is unfulfilled.`));
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
return findings;
|
|
1860
|
+
};
|
|
1861
|
+
var escalationFindings = (declared, rules, path, label) => {
|
|
1862
|
+
if (!declared.atRisk && !declared.breach)
|
|
1863
|
+
return [];
|
|
1864
|
+
if (rules.length === 0)
|
|
1865
|
+
return [];
|
|
1866
|
+
const escalations = rules.map(asRecord).flatMap((rule) => asArray(rule?.escalationRule)).map(asRecord).filter((e) => e !== undefined);
|
|
1867
|
+
const isKind = (escalation, kind) => {
|
|
1868
|
+
const type = asRecord(escalation.triggerInfo)?.type;
|
|
1869
|
+
if (typeof type !== "string")
|
|
1870
|
+
return false;
|
|
1871
|
+
const lowered = type.toLowerCase();
|
|
1872
|
+
return kind === "at-risk" ? lowered.includes("at-risk") || lowered.includes("atrisk") : lowered.includes("breach");
|
|
1873
|
+
};
|
|
1874
|
+
const reaches = (escalation) => asArray(asRecord(escalation.action)?.recipients).length > 0;
|
|
1875
|
+
const findings = [];
|
|
1876
|
+
const report = (kind, consequence) => {
|
|
1877
|
+
const matching = escalations.filter((e) => isKind(e, kind));
|
|
1878
|
+
const noun = kind === "at-risk" ? "an At-Risk Action" : "a Breach Action";
|
|
1879
|
+
if (matching.length === 0) {
|
|
1880
|
+
findings.push(error("STRICT_SDD_SLA_ESCALATION_MISSING", path, `${label}: the SDD SLA table declares ${noun}, the SLA rules carry no ${kind} escalation. ${consequence}`));
|
|
1881
|
+
return;
|
|
1882
|
+
}
|
|
1883
|
+
if (!matching.some(reaches)) {
|
|
1884
|
+
findings.push(error("STRICT_SDD_SLA_ESCALATION_MISSING", path, `${label}: the SDD SLA table declares ${noun} and the ${kind} escalation exists, but it carries no recipient. ${consequence} Add the recipient the SDD names.`));
|
|
1885
|
+
}
|
|
1886
|
+
};
|
|
1887
|
+
if (declared.atRisk) {
|
|
1888
|
+
report("at-risk", "The duration is enforced and nobody is notified before it elapses.");
|
|
1889
|
+
}
|
|
1890
|
+
if (declared.breach) {
|
|
1891
|
+
report("breach", "The SLA can elapse with nobody told.");
|
|
1892
|
+
}
|
|
1893
|
+
return findings;
|
|
1894
|
+
};
|
|
1895
|
+
var whenRule = (when) => {
|
|
1896
|
+
const open = when.indexOf("(");
|
|
1897
|
+
if (open === -1)
|
|
1898
|
+
return { rule: when.trim(), selectors: [] };
|
|
1899
|
+
const close = when.lastIndexOf(")");
|
|
1900
|
+
return {
|
|
1901
|
+
rule: when.slice(0, open).trim(),
|
|
1902
|
+
selectors: close > open ? quotedArgs(when.slice(open + 1, close)) : []
|
|
1903
|
+
};
|
|
1904
|
+
};
|
|
1905
|
+
var sameLabel = (a, b) => norm(a) === norm(b) || normAnnotationless(a) === normAnnotationless(b);
|
|
1906
|
+
var leadingSegment = (name) => {
|
|
1907
|
+
for (const dash of [" - ", " – ", " — "]) {
|
|
1908
|
+
const at = name.indexOf(dash);
|
|
1909
|
+
if (at > 0)
|
|
1910
|
+
return name.slice(0, at);
|
|
1911
|
+
}
|
|
1912
|
+
return name;
|
|
1913
|
+
};
|
|
1914
|
+
var selectorIds = (rule) => {
|
|
1915
|
+
const ids = [];
|
|
1916
|
+
for (const [key, value] of Object.entries(rule ?? {})) {
|
|
1917
|
+
if (!isTargetKey(key))
|
|
1918
|
+
continue;
|
|
1919
|
+
if (typeof value === "string" && value.trim() !== "")
|
|
1920
|
+
ids.push(value);
|
|
1921
|
+
if (Array.isArray(value)) {
|
|
1922
|
+
for (const entry of value) {
|
|
1923
|
+
if (typeof entry === "string" && entry.trim() !== "") {
|
|
1924
|
+
ids.push(entry);
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
return ids;
|
|
1930
|
+
};
|
|
1931
|
+
var isTargetKey = (key) => key.startsWith("selected") || key === "slaId";
|
|
1932
|
+
var hasSelector = (rule) => {
|
|
1933
|
+
for (const [key, value] of Object.entries(rule ?? {})) {
|
|
1934
|
+
if (!isTargetKey(key))
|
|
1935
|
+
continue;
|
|
1936
|
+
if (Array.isArray(value) && value.length > 0)
|
|
1937
|
+
return true;
|
|
1938
|
+
if (typeof value === "string" && value.trim() !== "")
|
|
1939
|
+
return true;
|
|
1940
|
+
}
|
|
1941
|
+
return false;
|
|
1942
|
+
};
|
|
1943
|
+
var undeclaredGateFinding = (row, planRules, path, label, resolve) => {
|
|
1944
|
+
if (row.ungated !== true || row.when === undefined)
|
|
1945
|
+
return;
|
|
1946
|
+
const { rule, selectors } = whenRule(row.when);
|
|
1947
|
+
if (!rule || isBlank(rule))
|
|
1948
|
+
return;
|
|
1949
|
+
const belongsToRow = (candidate) => {
|
|
1950
|
+
if (selectors.length === 0)
|
|
1951
|
+
return !hasSelector(candidate);
|
|
1952
|
+
if (!hasSelector(candidate))
|
|
1953
|
+
return false;
|
|
1954
|
+
if (resolve === undefined)
|
|
1955
|
+
return false;
|
|
1956
|
+
const pointsAt = selectorIds(candidate).map((id) => resolve.stage(id) ?? resolve.task(id)).filter((name) => name !== undefined);
|
|
1957
|
+
if (pointsAt.length === 0)
|
|
1958
|
+
return false;
|
|
1959
|
+
return selectors.some((want) => {
|
|
1960
|
+
const denotes = resolve.label(want);
|
|
1961
|
+
return pointsAt.some((got) => sameLabel(got, want) || denotes !== undefined && got === denotes);
|
|
1962
|
+
});
|
|
1963
|
+
};
|
|
1964
|
+
const gated = planRules.filter((candidate) => {
|
|
1965
|
+
if (norm(candidate?.rule) !== norm(rule))
|
|
1966
|
+
return false;
|
|
1967
|
+
if (!belongsToRow(candidate))
|
|
1968
|
+
return false;
|
|
1969
|
+
const expression = candidate?.conditionExpression;
|
|
1970
|
+
return typeof expression === "string" && !isBlank(expression);
|
|
1971
|
+
});
|
|
1972
|
+
if (gated.length === 0)
|
|
1973
|
+
return;
|
|
1974
|
+
const expressions = [
|
|
1975
|
+
...new Set(gated.map((candidate) => String(candidate?.conditionExpression)).filter((expression) => expression !== ""))
|
|
1976
|
+
];
|
|
1977
|
+
const on = selectors.length > 0 ? ` on "${selectors.join('", "')}"` : "";
|
|
1978
|
+
return error("STRICT_SDD_CONDITION_EXPRESSION_UNDECLARED", `${path}[${row.displayName}].conditionExpression`, `${label}: the SDD row "${row.displayName}" declares WHEN \`${rule}\`${on} with no IF gate, and the caseplan's \`${rule}\` carries \`${expressions.join("`, `")}\`. A gate the SDD does not declare narrows the transition to one branch, so it fires where the row says it should fire every time. Remove the expression, or add the IF cell that justifies it.`);
|
|
1979
|
+
};
|
|
1980
|
+
var whenCellFinding = (row, planRules, path, label, resolve) => {
|
|
1981
|
+
if (row.when === undefined)
|
|
1982
|
+
return;
|
|
1983
|
+
const { rule, selectors: wanted } = whenRule(row.when);
|
|
1984
|
+
if (!rule || isBlank(rule))
|
|
1985
|
+
return;
|
|
1986
|
+
const matching = planRules.filter((r) => norm(r?.rule) === norm(rule));
|
|
1987
|
+
if (matching.length === 0) {
|
|
1988
|
+
return error("STRICT_SDD_CONDITION_RULE_MISMATCH", `${path}[${row.displayName}].rule`, `${label}: the SDD row "${row.displayName}" declares WHEN \`${rule}\`, no rule on it uses that predicate. A different predicate starts the task at a different moment. Re-read that row's WHEN cell.`);
|
|
1989
|
+
}
|
|
1990
|
+
if (wanted.length === 0)
|
|
1991
|
+
return;
|
|
1992
|
+
if (!matching.some(hasSelector)) {
|
|
1993
|
+
return error("STRICT_SDD_CONDITION_SELECTOR_MISSING", `${path}[${row.displayName}].rule`, `${label}: the SDD row "${row.displayName}" declares WHEN \`${rule}\` on "${wanted.join('", "')}", and the caseplan's \`${rule}\` carries no target. The predicate then refers to nothing, so it never becomes true and the gate never opens. Set the rule's \`selected*\` field to the target the row names.`);
|
|
1994
|
+
}
|
|
1995
|
+
if (resolve === undefined)
|
|
1996
|
+
return;
|
|
1997
|
+
const pointsAt = matching.flatMap((candidate) => selectorIds(candidate)).map((id) => resolve.stage(id) ?? resolve.task(id)).filter((name) => name !== undefined);
|
|
1998
|
+
if (pointsAt.length === 0)
|
|
1999
|
+
return;
|
|
2000
|
+
const missed = wanted.filter((want) => {
|
|
2001
|
+
const denotes = resolve.label(want);
|
|
2002
|
+
return !pointsAt.some((got) => sameLabel(got, want) || denotes !== undefined && got === denotes);
|
|
2003
|
+
});
|
|
2004
|
+
if (missed.length === 0)
|
|
2005
|
+
return;
|
|
2006
|
+
return error("STRICT_SDD_CONDITION_SELECTOR_MISMATCH", `${path}[${row.displayName}].rule`, `${label}: the SDD row "${row.displayName}" declares WHEN \`${rule}\` on "${missed.join('", "')}", and the caseplan's \`${rule}\` points at "${pointsAt.join('", "')}". A selector that resolves to the wrong target builds a transition the SDD never asked for and leaves the declared one absent, which no count or predicate check can see.`);
|
|
2007
|
+
};
|
|
2008
|
+
var conditionRuleFindings = (declared, planConditions, path, label, resolve) => {
|
|
2009
|
+
if (declared.length === 0)
|
|
2010
|
+
return [];
|
|
2011
|
+
const planRules = planConditions.flatMap((condition) => asArray(asRecord(condition)?.rules).flatMap((group) => asArray(group).map(asRecord)));
|
|
2012
|
+
if (planRules.length === 0)
|
|
2013
|
+
return [];
|
|
2014
|
+
return declared.flatMap((row) => [
|
|
2015
|
+
whenCellFinding(row, planRules, path, label, resolve),
|
|
2016
|
+
undeclaredGateFinding(row, planRules, path, label, resolve)
|
|
2017
|
+
]).filter((finding) => finding !== undefined);
|
|
2018
|
+
};
|
|
2019
|
+
var CONDITION_FLAGS = [
|
|
2020
|
+
["interrupting", "isInterrupting", "Interrupting"],
|
|
2021
|
+
["marksComplete", "marksStageComplete", "Marks Stage Complete"]
|
|
2022
|
+
];
|
|
2023
|
+
var conditionShapeFindings = (declared, planConditions, path, label) => {
|
|
2024
|
+
const findings = [];
|
|
2025
|
+
for (const row of declared) {
|
|
2026
|
+
const match = planConditions.map(asRecord).find((c) => norm(c?.displayName) === norm(row.displayName));
|
|
2027
|
+
if (match === undefined)
|
|
2028
|
+
continue;
|
|
2029
|
+
if (row.exitType !== undefined) {
|
|
2030
|
+
const has = typeof match.type === "string" && match.type !== "";
|
|
2031
|
+
if (!has || norm(match.type) !== norm(row.exitType)) {
|
|
2032
|
+
findings.push(error("STRICT_SDD_CONDITION_EXIT_TYPE_MISMATCH", `${path}[${row.displayName}].type`, `${label}: the SDD row "${row.displayName}" declares Exit Type "${row.exitType}", the condition has ${has ? `"${String(match.type)}"` : "no `type` at all"}. The exit type decides where the case goes when the gate opens, so this ${has ? "sends it somewhere else" : "leaves it undecided — check for an invented key like `exitType` holding the value `type` should carry"}.`));
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
for (const [cell, field2, title] of CONDITION_FLAGS) {
|
|
2036
|
+
const want = row[cell];
|
|
2037
|
+
const got = typeof match[field2] === "boolean" ? match[field2] : false;
|
|
2038
|
+
if (want === undefined)
|
|
2039
|
+
continue;
|
|
2040
|
+
if (want === got)
|
|
2041
|
+
continue;
|
|
2042
|
+
findings.push(error("STRICT_SDD_CONDITION_FLAG_MISMATCH", `${path}[${row.displayName}].${field2}`, `${label}: the SDD row "${row.displayName}" declares ${title}=${want ? "Yes" : "No"}, the condition has ${typeof match[field2] === "boolean" ? String(got) : "no such field, which the runtime reads as false"}.`));
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
return findings;
|
|
2046
|
+
};
|
|
2047
|
+
var TIMER_CONFIGURATION_TYPES = new Map([
|
|
2048
|
+
["duration", "timeDuration"],
|
|
2049
|
+
["date", "timeDate"],
|
|
2050
|
+
["cycle", "timeCycle"]
|
|
2051
|
+
]);
|
|
2052
|
+
var TIMER_VALUE_FIELDS = new Map([
|
|
2053
|
+
["timeduration", "timeDuration"],
|
|
2054
|
+
["timedate", "timeDate"],
|
|
2055
|
+
["timecycle", "timeCycle"]
|
|
2056
|
+
]);
|
|
2057
|
+
var timerFindings = (task, found, path, label) => {
|
|
2058
|
+
if (task.timerType === undefined)
|
|
2059
|
+
return [];
|
|
2060
|
+
const findings = [];
|
|
2061
|
+
const carriedType = found.data.timerType;
|
|
2062
|
+
if (typeof carriedType === "string" && carriedType.trim() !== "") {
|
|
2063
|
+
if (norm(carriedType) !== norm(task.timerType)) {
|
|
2064
|
+
findings.push(error("STRICT_SDD_TIMER_TYPE_MISMATCH", `${path}.data.timerType`, `${label}: the SDD declares timer type "${task.timerType}", the caseplan has "${carriedType}". Re-read the **Timer:** line for this task.`));
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
if (task.timerValue === undefined)
|
|
2068
|
+
return findings;
|
|
2069
|
+
const key = TIMER_VALUE_FIELDS.get(norm(task.timerType).replaceAll(" ", ""));
|
|
2070
|
+
const carried = key === undefined ? undefined : found.data[key];
|
|
2071
|
+
if (typeof carried !== "string" || carried.trim() === "")
|
|
2072
|
+
return findings;
|
|
2073
|
+
if (norm(carried) === norm(task.timerValue))
|
|
2074
|
+
return findings;
|
|
2075
|
+
findings.push(error("STRICT_SDD_TIMER_VALUE_MISMATCH", `${path}.data.${key}`, `${label}: the timer runs for "${carried}" where the SDD's **Value:** line declares "${task.timerValue}". A timer copied from a sibling task waits the wrong length of time and nothing downstream can tell. Re-read the **Value:** line for this task.`));
|
|
2076
|
+
return findings;
|
|
2077
|
+
};
|
|
2078
|
+
var literalDifference = (actual, declared) => {
|
|
2079
|
+
if (actual === "")
|
|
2080
|
+
return "carries an empty value";
|
|
2081
|
+
if (actual.includes("`")) {
|
|
2082
|
+
return "carries a value with a fence character (`) in it, so it is not the literal the SDD declares";
|
|
2083
|
+
}
|
|
2084
|
+
if (!parsesAsJson(actual) && parsesAsJson(declared)) {
|
|
2085
|
+
return "does not match the literal the SDD declares, which is JSON where this value is not";
|
|
2086
|
+
}
|
|
2087
|
+
return "does not match the literal the SDD declares";
|
|
2088
|
+
};
|
|
2089
|
+
var missingLiteralFields = (declared, actual) => {
|
|
2090
|
+
const [declaredError, declaredValue] = catchError(() => JSON.parse(declared));
|
|
2091
|
+
const [actualError, actualValue] = catchError(() => JSON.parse(actual));
|
|
2092
|
+
if (declaredError || actualError)
|
|
2093
|
+
return;
|
|
2094
|
+
const declaredRecord = asRecord(declaredValue);
|
|
2095
|
+
const actualRecord = asRecord(actualValue);
|
|
2096
|
+
if (declaredRecord === undefined || actualRecord === undefined) {
|
|
2097
|
+
return;
|
|
2098
|
+
}
|
|
2099
|
+
const carried = new Set(Object.keys(actualRecord));
|
|
2100
|
+
const declaredKeys = Object.keys(declaredRecord);
|
|
2101
|
+
const missing = declaredKeys.filter((key) => !carried.has(key));
|
|
2102
|
+
if (missing.length === 0)
|
|
2103
|
+
return;
|
|
2104
|
+
return {
|
|
2105
|
+
declaredCount: declaredKeys.length,
|
|
2106
|
+
actualCount: declaredKeys.length - missing.length,
|
|
2107
|
+
firstMissing: missing[0]
|
|
2108
|
+
};
|
|
2109
|
+
};
|
|
2110
|
+
var literalFindings = (task, found, path, label) => {
|
|
2111
|
+
const findings = [];
|
|
2112
|
+
for (const declared of task.literalInputs) {
|
|
2113
|
+
const carried = carriedInput(found, declared.name);
|
|
2114
|
+
if (!carried.present)
|
|
2115
|
+
continue;
|
|
2116
|
+
const actual = carried.value;
|
|
2117
|
+
if (typeof actual !== "string")
|
|
2118
|
+
continue;
|
|
2119
|
+
if (actual.startsWith("=")) {
|
|
2120
|
+
findings.push(error("STRICT_SDD_LITERAL_MISMATCH", `${path}.data.inputs[${declared.name}]`, `${label}: input "${declared.name}" carries an expression where the SDD's Inputs row declares a literal value. Re-read that row and set the literal it gives.`));
|
|
2121
|
+
continue;
|
|
2122
|
+
}
|
|
2123
|
+
if (sameLiteral(declared.value, actual))
|
|
2124
|
+
continue;
|
|
2125
|
+
const truncated = missingLiteralFields(declared.value, actual);
|
|
2126
|
+
if (truncated !== undefined) {
|
|
2127
|
+
findings.push(error("STRICT_SDD_LITERAL_MISMATCH", `${path}.data.inputs[${declared.name}]`, `${label}: input "${declared.name}" carries ${truncated.actualCount} of the ${truncated.declaredCount} fields its SDD Inputs row declares — "${truncated.firstMissing}" is the first one absent. The row is the verbatim contract, so the task runs on data the SDD does not describe. Re-read that row and carry every field.`));
|
|
2128
|
+
continue;
|
|
2129
|
+
}
|
|
2130
|
+
const difference = literalDifference(actual, declared.value);
|
|
2131
|
+
findings.push(warning("STRICT_SDD_LITERAL_MISMATCH", `${path}.data.inputs[${declared.name}]`, `${label}: input "${declared.name}" ${difference}. Re-read the Inputs row for "${declared.name}" in the SDD and set the value from it. A literal that arrives truncated or rewritten runs with the wrong data.`));
|
|
2132
|
+
}
|
|
2133
|
+
return findings;
|
|
2134
|
+
};
|
|
2135
|
+
var PAYLOAD_SLOTS = ["body", "queryParameters", "pathParameters"];
|
|
2136
|
+
var usesSlotInputs = (found) => {
|
|
2137
|
+
const names = asArray(found.data.inputs).map(asRecord).map((i) => norm(i?.name));
|
|
2138
|
+
if (found.type === "wait-for-connector") {
|
|
2139
|
+
return names.length === 1 && names[0] === norm("body");
|
|
2140
|
+
}
|
|
2141
|
+
return names.includes(norm("pathParameters")) || names.includes(norm("queryParameters"));
|
|
2142
|
+
};
|
|
2143
|
+
var carriesPayload = (inputs) => PAYLOAD_SLOTS.some((slot) => {
|
|
2144
|
+
const input = inputs.find((i) => norm(i.name) === norm(slot));
|
|
2145
|
+
if (typeof input?.body === "string")
|
|
2146
|
+
return input.body !== "";
|
|
2147
|
+
return Object.keys(asRecord(input?.body) ?? {}).length > 0;
|
|
2148
|
+
}) || inputs.some((i) => (i.type === "file" || i.target === "file") && typeof i.value === "string" && i.value !== "");
|
|
2149
|
+
var boundInputFindings = (task, found, path, label) => {
|
|
2150
|
+
if (task.boundInputs.length === 0)
|
|
2151
|
+
return [];
|
|
2152
|
+
const inputs = asArray(found.data.inputs).map(asRecord).filter((i) => i !== undefined);
|
|
2153
|
+
const findings = [];
|
|
2154
|
+
const isConnector = AUDIT_CONNECTOR_TASK_TYPES.has(found.type ?? "") && usesSlotInputs(found);
|
|
2155
|
+
let connectorRows = 0;
|
|
2156
|
+
for (const declared of task.boundInputs) {
|
|
2157
|
+
const input = inputs.find((i) => norm(i.name) === norm(declared.name));
|
|
2158
|
+
if (input) {
|
|
2159
|
+
const valueless = !("value" in input);
|
|
2160
|
+
if (!valueless && input.value !== "")
|
|
2161
|
+
continue;
|
|
2162
|
+
findings.push(error("STRICT_SDD_INPUT_UNBOUND", `${path}.data.inputs[${declared.name}]`, `${label}: input "${declared.name}" ${valueless ? "carries no `value` at all" : "carries an empty value"}. The SDD binds it to \`${declared.binding}\`, so the build has to resolve that reference into the slot. ${valueless ? "The runtime reads `value`, so data under any other key — `body` is the usual one — reaches it as nothing." : "Empty, the field reaches the runtime missing and the task faults on its first use."}`));
|
|
2163
|
+
continue;
|
|
2164
|
+
}
|
|
2165
|
+
if (!isConnector)
|
|
2166
|
+
continue;
|
|
2167
|
+
connectorRows += 1;
|
|
2168
|
+
}
|
|
2169
|
+
if (connectorRows > 0 && !carriesPayload(inputs)) {
|
|
2170
|
+
findings.push(error("STRICT_SDD_INPUT_UNBOUND", `${path}.data.inputs[body].body`, `${label}: the SDD binds ${connectorRows} input ${connectorRows === 1 ? "row" : "rows"} and the connector task carries no spliced payload: nothing in \`body\`, \`queryParameters\` or \`pathParameters\`, and no file parameter with a value. \`case spec\` returns that payload under \`Body\`, which lower-cases to \`body\` on the way in. Written to \`value\` instead it is dropped at packaging, and the activity runs with none of its fields.`));
|
|
2171
|
+
}
|
|
2172
|
+
return findings;
|
|
2173
|
+
};
|
|
2174
|
+
var extractFindings = (task, found, path, label) => {
|
|
2175
|
+
if (task.extractRows.length === 0)
|
|
2176
|
+
return [];
|
|
2177
|
+
const outputs = asArray(found.data.outputs).map(asRecord).filter((o) => o !== undefined);
|
|
2178
|
+
const findings = [];
|
|
2179
|
+
for (const declared of task.extractRows) {
|
|
2180
|
+
const wanted = `=${declared.field}`;
|
|
2181
|
+
const present = outputs.some((o) => o.source === wanted && o.var === declared.variable);
|
|
2182
|
+
if (present)
|
|
2183
|
+
continue;
|
|
2184
|
+
const carriesSource = outputs.filter((o) => o.source === wanted);
|
|
2185
|
+
findings.push(error("STRICT_SDD_EXTRACT_MISSING", `${path}.data.outputs`, `${label}: no output writes "${declared.variable}". The SDD ` + `extracts \`${declared.field} -> ${declared.variable}\`, so ` + `the task needs an output with source "${wanted}" and var ` + `"${declared.variable}". ` + (carriesSource.length === 0 ? "The task carries no output with that source at all, " + "so the row was never written and anything reading " + "the variable sees null." : `The task does carry an output with that source, ` + `${carriesSource.map((o) => {
|
|
2186
|
+
const named = (key, value) => typeof value === "string" && value.trim() !== "" ? `\`${key}\` "${value}"` : `no \`${key}\``;
|
|
2187
|
+
return `${named("var", o.var)} and ${named("id", o.id)}`;
|
|
2188
|
+
}).join(", ")}. The row is written and points at a ` + `variable the SDD never declared, so the repair is ` + `that \`var\`, not a second output. A collision ` + `suffix belongs on the \`id\`, which is what can ` + `clash; the \`var\` names the one case variable every ` + `such task writes.`)));
|
|
2189
|
+
}
|
|
2190
|
+
return findings;
|
|
2191
|
+
};
|
|
2192
|
+
var customOutputFindings = (task, found, path, label) => {
|
|
2193
|
+
if (task.customRows.length === 0)
|
|
2194
|
+
return [];
|
|
2195
|
+
const outputs = asArray(found.data.outputs).map(asRecord).filter((o) => o !== undefined);
|
|
2196
|
+
const findings = [];
|
|
2197
|
+
for (const declared of task.customRows) {
|
|
2198
|
+
const present = outputs.some((o) => o.custom === true && o.var === declared.variable);
|
|
2199
|
+
if (present)
|
|
2200
|
+
continue;
|
|
2201
|
+
findings.push(error("STRICT_SDD_CUSTOM_OUTPUT_MISSING", `${path}.data.outputs`, `${label}: nothing sets "${declared.variable}". The SDD assigns it \`${declared.expression}\`, so the task needs an output with \`custom: true\` and var "${declared.variable}". Without it no element in the packed BPMN writes the variable and it keeps its default.`));
|
|
2202
|
+
}
|
|
2203
|
+
return findings;
|
|
2204
|
+
};
|
|
2205
|
+
var gatedSlaFindings = (rules, path, label) => {
|
|
2206
|
+
if (rules.length === 0)
|
|
2207
|
+
return [];
|
|
2208
|
+
const expressions = rules.map((rule) => {
|
|
2209
|
+
const expression = asRecord(rule)?.expression;
|
|
2210
|
+
return typeof expression === "string" ? expression.trim() : "";
|
|
2211
|
+
});
|
|
2212
|
+
const isDefault = (expression) => expression === "" || norm(expression) === norm("=js:true");
|
|
2213
|
+
if (expressions.some(isDefault))
|
|
2214
|
+
return [];
|
|
2215
|
+
return [
|
|
2216
|
+
warning("STRICT_SDD_SLA_GATED_NEEDS_DEFAULT", path, `${label}: every SLA rule is gated by an expression and none is the \`=js:true\` default, so no SLA applies when no gate matches. Add a trailing unconditional rule if that is not intended.`)
|
|
2217
|
+
];
|
|
2218
|
+
};
|
|
2219
|
+
var compareTask = (plan, task, found, stagePath, label, selectors) => {
|
|
2220
|
+
const issues = [];
|
|
2221
|
+
const taskPath = `${stagePath}.data.tasks[${found.name ?? task.name}]`;
|
|
2222
|
+
if (task.type && found.type && norm(task.type) !== norm(found.type)) {
|
|
2223
|
+
issues.push(error("STRICT_SDD_TASK_TYPE_MISMATCH", `${taskPath}.type`, `${label}: the SDD declares type "${task.type}", the caseplan has "${found.type}".`));
|
|
2224
|
+
}
|
|
2225
|
+
if (task.proseLabel !== undefined) {
|
|
2226
|
+
const description = typeof found.description === "string" ? found.description : "";
|
|
2227
|
+
if (description.trim() === "") {
|
|
2228
|
+
issues.push(warning("STRICT_SDD_TASK_DESCRIPTION_MISSING", `${taskPath}.description`, `${label}: the SDD writes a \`**${task.proseLabel}:**\` line for this task and the caseplan task carries no \`description\`. Studio Web shows that field on the task's General tab, so dropping it is how the reasoning stops reaching whoever opens the case. Copy that line into \`description\` word for word. This is a warning because the case still runs without it, so a \`Valid\` result does not mean the prose arrived.`));
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
issues.push(...coverageFinding(`${taskPath}.entryConditions`, label, "entry", task.entryRows, found.entry));
|
|
2232
|
+
if (found.entry.length === 0) {
|
|
2233
|
+
issues.push(error("STRICT_SDD_ENTRY_RULE_EMPTY", `${taskPath}.entryConditions`, `${label}: no entryConditions in the caseplan. The default profile only warns, but a task with no entry rule hangs \`case debug\` indefinitely.`));
|
|
2234
|
+
} else if (ruleTotal(found.entry) === 0) {
|
|
2235
|
+
issues.push(error("STRICT_SDD_ENTRY_RULE_EMPTY", `${taskPath}.entryConditions`, `${label}: entryConditions carry no rules.`));
|
|
2236
|
+
}
|
|
2237
|
+
const isPlaceholder = Object.keys(found.data).length === 0;
|
|
2238
|
+
if (isPlaceholder && task.identityResolved) {
|
|
2239
|
+
const resolved = task.resolvedResource ? ` to "${task.resolvedResource}"` : "";
|
|
2240
|
+
issues.push(error("STRICT_SDD_PLACEHOLDER_RESOLVED", `${taskPath}.data`, `${label}: the caseplan has an empty \`data: {}\` (placeholder) but the SDD resolved the resource${resolved}.`));
|
|
2241
|
+
} else if (isPlaceholder) {
|
|
2242
|
+
issues.push(warning("STRICT_SDD_PLACEHOLDER_TASK", `${taskPath}.data`, `${label}: placeholder task (empty \`data\`) — the SDD leaves its resource unresolved.`));
|
|
2243
|
+
}
|
|
2244
|
+
issues.push(...conditionRuleFindings(task.entryConditions, found.entry, `${taskPath}.entryConditions`, label, selectors), ...envelopeFindings(task, found, taskPath, label));
|
|
2245
|
+
if (isPlaceholder)
|
|
2246
|
+
return issues;
|
|
2247
|
+
issues.push(...bindingFindings(plan, found, taskPath, label, task.folder), ...literalFindings(task, found, taskPath, label), ...boundInputFindings(task, found, taskPath, label), ...timerFindings(task, found, taskPath, label), ...wiringFindings(task, found, taskPath, label), ...producerFindings(task, found, plan, taskPath, label), ...emptyInputFindings(task, found, taskPath, label), ...expressionShapeFindings(task, found, plan, taskPath, label), ...outputRowFindings(task, found, taskPath, label), ...extractFindings(task, found, taskPath, label), ...customOutputFindings(task, found, taskPath, label));
|
|
2248
|
+
return issues;
|
|
2249
|
+
};
|
|
2250
|
+
var variablePlacementFindings = (sdd, plan, name, key) => {
|
|
2251
|
+
const category = sdd.variableCategories.get(key);
|
|
2252
|
+
const required = VARIABLE_PLACEMENT.get(category ?? "");
|
|
2253
|
+
if (category === undefined || required === undefined)
|
|
2254
|
+
return [];
|
|
2255
|
+
const issues = [];
|
|
2256
|
+
for (const group of required) {
|
|
2257
|
+
if (plan.variableGroups[group].has(key))
|
|
2258
|
+
continue;
|
|
2259
|
+
const found = VARIABLE_GROUPS.filter((g) => plan.variableGroups[g].has(key));
|
|
2260
|
+
issues.push(error("STRICT_SDD_VARIABLE_GROUP_MISSING", `variables.${group}[${name}]`, `Variable "${name}": the SDD Category=${capitalize(category)} needs an entry in caseplan variables.${group}[], found only in ${found.join(", ")}. \`validate\` accepts half the pair and the argument then drops out of entry-points.json.`));
|
|
2261
|
+
}
|
|
2262
|
+
return issues;
|
|
2263
|
+
};
|
|
2264
|
+
var variableDefaultFindings = (sdd, plan, name, key) => {
|
|
2265
|
+
const declared = sdd.variableDefaults.get(key);
|
|
2266
|
+
const category = sdd.variableCategories.get(key);
|
|
2267
|
+
const group = DEFAULT_GROUP.get(category ?? "");
|
|
2268
|
+
if (declared === undefined || group === undefined)
|
|
2269
|
+
return [];
|
|
2270
|
+
const entry = plan.variableEntries[group].get(key);
|
|
2271
|
+
if (entry === undefined)
|
|
2272
|
+
return [];
|
|
2273
|
+
if (!("default" in entry)) {
|
|
2274
|
+
return [
|
|
2275
|
+
error("STRICT_SDD_VARIABLE_DEFAULT_MISSING", `variables.${group}[${name}].default`, `Variable "${name}": the SDD Default column gives it "${declared}" and caseplan variables.${group}[] carries no \`default\`. Packaging writes no attribute for it, so the slot starts with no value and a route that reads it before anything writes it sees undefined.`)
|
|
2276
|
+
];
|
|
2277
|
+
}
|
|
2278
|
+
const written = entry.default;
|
|
2279
|
+
const asText = typeof written === "string" ? written : JSON.stringify(written);
|
|
2280
|
+
if (asText === declared)
|
|
2281
|
+
return [];
|
|
2282
|
+
return [
|
|
2283
|
+
error("STRICT_SDD_VARIABLE_DEFAULT_MISMATCH", `variables.${group}[${name}].default`, `Variable "${name}": the SDD Default column gives it "${declared}" and caseplan variables.${group}[] carries "${asText}". The slot starts at the written value, so every route that reads it before a task writes it reads the wrong one.`)
|
|
2284
|
+
];
|
|
2285
|
+
};
|
|
2286
|
+
var compareCaseLevel = (sdd, plan) => {
|
|
2287
|
+
const issues = [
|
|
2288
|
+
...coverageFinding("metadata.caseExitRules", "case", "exit", sdd.caseExitRows, plan.caseExitConditions),
|
|
2289
|
+
...gatedSlaFindings(plan.caseSlaRules, "metadata.slaRules", "case"),
|
|
2290
|
+
...conditionalSlaFindings(sdd.caseConditionalSlas, plan.caseSlaRules, "metadata.slaRules", "case")
|
|
2291
|
+
];
|
|
2292
|
+
if (sdd.caseSla && !plan.caseSla) {
|
|
2293
|
+
issues.push(error("STRICT_SDD_SLA_MISSING", "metadata.slaRules", "case: the SDD declares Case-Level SLA Escalation Rules, the caseplan metadata.slaRules is empty."));
|
|
2294
|
+
}
|
|
2295
|
+
issues.push(...escalationFindings(sdd.caseSlaEscalations, plan.caseSlaRules, "metadata.slaRules", "case"), ...xrefOutputFindings(sdd, plan), ...conditionExpressionFindings(sdd.caseExitConditions, plan.caseExitConditions, plan, "metadata.caseExitRules", "case"));
|
|
2296
|
+
issues.push(...slaDurationFindings(sdd, plan));
|
|
2297
|
+
if (sdd.caseSla && plan.caseSlaRules.length > 1) {
|
|
2298
|
+
issues.push(error("STRICT_SDD_SLA_EXTRA", "metadata.slaRules", `case: the SDD declares one case-level SLA and ${plan.caseSlaRules.length} SLA rules are present. Its escalations are the rows of that one table, so splitting them across rules leaves each rule with part of the SLA and the runtime applies whichever rule it reaches — the other escalations are lost. Carry one rule with every escalation the table declares.`));
|
|
2299
|
+
}
|
|
2300
|
+
if (sdd.triggers && plan.triggers < sdd.triggers) {
|
|
2301
|
+
issues.push(error("STRICT_SDD_TRIGGER_MISSING", "nodes", plan.triggers === 0 ? `case: the SDD declares ${sdd.triggers} trigger(s), the caseplan has no trigger node.` : `case: the SDD declares ${sdd.triggers} trigger(s), the caseplan has ${plan.triggers}.`));
|
|
2302
|
+
}
|
|
2303
|
+
if (sdd.triggers && plan.triggers > sdd.triggers) {
|
|
2304
|
+
issues.push(error("STRICT_SDD_TRIGGER_EXTRA", "nodes", `case: the SDD declares ${sdd.triggers} trigger(s), the caseplan has ${plan.triggers}. A trigger the SDD does not declare starts cases nobody asked for; remove it or add the row that justifies it.`));
|
|
2305
|
+
}
|
|
2306
|
+
const planVariables = new Set(plan.variables.map(norm));
|
|
2307
|
+
for (const name of sdd.variables) {
|
|
2308
|
+
const key = norm(name);
|
|
2309
|
+
if (!planVariables.has(key)) {
|
|
2310
|
+
issues.push(error("STRICT_SDD_VARIABLE_MISSING", `variables[${name}]`, `Variable "${name}" is declared in the SDD Case Variables table and absent from the caseplan.`));
|
|
2311
|
+
continue;
|
|
2312
|
+
}
|
|
2313
|
+
issues.push(...variablePlacementFindings(sdd, plan, name, key), ...variableDefaultFindings(sdd, plan, name, key));
|
|
2314
|
+
}
|
|
2315
|
+
if (sdd.sawVariableSection) {
|
|
2316
|
+
const declared = new Set(sdd.variables.map(norm));
|
|
2317
|
+
for (const stage of sdd.stages) {
|
|
2318
|
+
for (const task of stage.tasks) {
|
|
2319
|
+
for (const row of task.extractRows)
|
|
2320
|
+
declared.add(norm(row.variable));
|
|
2321
|
+
for (const row of task.customRows)
|
|
2322
|
+
declared.add(norm(row.variable));
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
const reported = new Set;
|
|
2326
|
+
for (const name of plan.variables) {
|
|
2327
|
+
const key = norm(name);
|
|
2328
|
+
if (declared.has(key) || reported.has(key))
|
|
2329
|
+
continue;
|
|
2330
|
+
reported.add(key);
|
|
2331
|
+
issues.push(error("STRICT_SDD_VARIABLE_EXTRA", `variables[${name}]`, `Variable "${name}" is in the caseplan and absent from the SDD Case Variables table. A case variable the SDD does not declare moves a value from task scope to case scope — it outlives the task, is visible to every stage, and nothing in the document says it should exist. Remove it and read the value where it is produced, or add the row that justifies it.`));
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
return issues;
|
|
2335
|
+
};
|
|
2336
|
+
var SLA_UNIT_CODES = new Map([
|
|
2337
|
+
["min", "min"],
|
|
2338
|
+
["mins", "min"],
|
|
2339
|
+
["minute", "min"],
|
|
2340
|
+
["minutes", "min"],
|
|
2341
|
+
["h", "h"],
|
|
2342
|
+
["hr", "h"],
|
|
2343
|
+
["hrs", "h"],
|
|
2344
|
+
["hour", "h"],
|
|
2345
|
+
["hours", "h"],
|
|
2346
|
+
["d", "d"],
|
|
2347
|
+
["day", "d"],
|
|
2348
|
+
["days", "d"],
|
|
2349
|
+
["w", "w"],
|
|
2350
|
+
["week", "w"],
|
|
2351
|
+
["weeks", "w"],
|
|
2352
|
+
["m", "m"],
|
|
2353
|
+
["mo", "m"],
|
|
2354
|
+
["month", "m"],
|
|
2355
|
+
["months", "m"]
|
|
2356
|
+
]);
|
|
2357
|
+
var AMBIGUOUS_SDD_UNITS = new Map([
|
|
2358
|
+
["m", ["min", "m"]]
|
|
2359
|
+
]);
|
|
2360
|
+
var acceptedUnitCodes = (cell) => {
|
|
2361
|
+
const key = cell.trim().toLowerCase();
|
|
2362
|
+
const ambiguous = AMBIGUOUS_SDD_UNITS.get(key);
|
|
2363
|
+
if (ambiguous !== undefined)
|
|
2364
|
+
return ambiguous;
|
|
2365
|
+
const exact = SLA_UNIT_CODES.get(key);
|
|
2366
|
+
return exact === undefined ? [] : [exact];
|
|
2367
|
+
};
|
|
2368
|
+
var parseDurationCell = (text) => {
|
|
2369
|
+
const match = /^(\d+)\s*([a-z]+)$/i.exec(text.trim());
|
|
2370
|
+
if (!match)
|
|
2371
|
+
return;
|
|
2372
|
+
const count = Number(match[1]);
|
|
2373
|
+
if (!Number.isFinite(count))
|
|
2374
|
+
return;
|
|
2375
|
+
const units = acceptedUnitCodes(match[2]);
|
|
2376
|
+
return units.length === 0 ? undefined : { count, units };
|
|
2377
|
+
};
|
|
2378
|
+
var conditionalSlaFindings = (declared, carried, path, label) => {
|
|
2379
|
+
if (declared.length === 0)
|
|
2380
|
+
return [];
|
|
2381
|
+
const findings = [];
|
|
2382
|
+
const rules = carried.map((entry) => asRecord(entry)).filter((entry) => entry !== undefined);
|
|
2383
|
+
for (const row of declared) {
|
|
2384
|
+
const wanted = comparableExpression(row.expression);
|
|
2385
|
+
const match = rules.find((rule) => typeof rule.expression === "string" && comparableExpression(rule.expression) === wanted);
|
|
2386
|
+
const title = row.displayName === undefined ? `\`${row.expression}\`` : `"${row.displayName}"`;
|
|
2387
|
+
if (match === undefined) {
|
|
2388
|
+
findings.push(error("STRICT_SDD_SLA_CONDITIONAL_MISSING", path, `${label}: the SDD declares a conditional SLA rule ${title} and no rule on this target carries that expression. The override is what applies the stricter duration when the condition holds, so without it every case takes the default and nothing reports a gap. Add the rule ahead of the trailing \`=js:true\` default.`));
|
|
2389
|
+
continue;
|
|
2390
|
+
}
|
|
2391
|
+
const want = parseDurationCell(row.duration);
|
|
2392
|
+
if (want === undefined)
|
|
2393
|
+
continue;
|
|
2394
|
+
const count = match.count;
|
|
2395
|
+
const unit = match.unit;
|
|
2396
|
+
const carriedUnit = typeof unit === "string" ? SLA_UNIT_CODES.get(unit.toLowerCase()) : undefined;
|
|
2397
|
+
if (count === want.count && carriedUnit !== undefined && want.units.includes(carriedUnit)) {
|
|
2398
|
+
continue;
|
|
2399
|
+
}
|
|
2400
|
+
findings.push(error("STRICT_SDD_SLA_CONDITIONAL_MISSING", path, `${label}: the conditional SLA rule ${title} carries ${typeof count === "number" && carriedUnit !== undefined ? `count ${String(count)} unit "${String(unit)}"` : "no usable `count`/`unit`"} where the SDD declares "${row.duration}". The duration is the whole point of the override, so a different one makes the condition decide nothing.`));
|
|
2401
|
+
}
|
|
2402
|
+
return findings;
|
|
2403
|
+
};
|
|
2404
|
+
var slaDurationFindings = (sdd, plan) => {
|
|
2405
|
+
if (sdd.caseSlaDuration === undefined)
|
|
2406
|
+
return [];
|
|
2407
|
+
const declared = parseDurationCell(sdd.caseSlaDuration);
|
|
2408
|
+
if (declared === undefined)
|
|
2409
|
+
return [];
|
|
2410
|
+
const findings = [];
|
|
2411
|
+
plan.caseSlaRules.forEach((entry, index) => {
|
|
2412
|
+
const rule = asRecord(entry);
|
|
2413
|
+
if (rule === undefined)
|
|
2414
|
+
return;
|
|
2415
|
+
const label = typeof rule.displayName === "string" && rule.displayName !== "" ? rule.displayName : String(index);
|
|
2416
|
+
const count = rule.count;
|
|
2417
|
+
const unit = rule.unit;
|
|
2418
|
+
const has = typeof count === "number" && Number.isFinite(count) && typeof unit === "string" && unit !== "";
|
|
2419
|
+
const carried = has ? SLA_UNIT_CODES.get(String(unit).toLowerCase()) : undefined;
|
|
2420
|
+
if (has && count === declared.count && carried !== undefined && declared.units.includes(carried)) {
|
|
2421
|
+
return;
|
|
2422
|
+
}
|
|
2423
|
+
findings.push(error("STRICT_SDD_SLA_DURATION_MISMATCH", `metadata.slaRules[${label}]`, `case: the SDD declares a case-level SLA of "${sdd.caseSlaDuration}", and the rule carries ${has ? `count ${String(count)} unit "${String(unit)}"` : "no `count`/`unit` at all"}. The duration is what makes the SLA elapse, so ${has ? "a different one moves every escalation with it" : "without it the SLA never elapses and no escalation ever fires — check for the value sitting in an invented key such as `duration`"}.`));
|
|
2424
|
+
});
|
|
2425
|
+
return findings;
|
|
2426
|
+
};
|
|
2427
|
+
var compareStage = (plan, stage, planStage, selectors) => {
|
|
2428
|
+
const stagePath = `nodes[${planStage.name ?? stage.name}]`;
|
|
2429
|
+
const stageLabel = `stage "${stage.name}"`;
|
|
2430
|
+
const issues = [
|
|
2431
|
+
...coverageFinding(`${stagePath}.data.entryConditions`, stageLabel, "entry", stage.entryRows, planStage.entry),
|
|
2432
|
+
...coverageFinding(`${stagePath}.data.exitConditions`, stageLabel, "exit", stage.exitRows, planStage.exit),
|
|
2433
|
+
...gatedSlaFindings(planStage.sla, `${stagePath}.data.slaRules`, stageLabel),
|
|
2434
|
+
...conditionalSlaFindings(stage.conditionalSlas, planStage.sla, `${stagePath}.data.slaRules`, stageLabel)
|
|
2435
|
+
];
|
|
2436
|
+
if (stage.hasSla && planStage.sla.length === 0) {
|
|
2437
|
+
issues.push(error("STRICT_SDD_SLA_MISSING", `${stagePath}.data.slaRules`, `${stageLabel}: the SDD declares a Stage SLA, the caseplan has no slaRules.`));
|
|
2438
|
+
}
|
|
2439
|
+
if (stage.hasSla && planStage.sla.length > 1) {
|
|
2440
|
+
issues.push(error("STRICT_SDD_SLA_EXTRA", `${stagePath}.data.slaRules`, `${stageLabel}: the SDD declares one Stage SLA and ${planStage.sla.length} SLA rules are present. Its escalations belong to that one SLA, so splitting them across rules leaves each with part of it and the runtime applies whichever rule it reaches — the other escalations are lost. Carry one rule with every escalation the table declares.`));
|
|
2441
|
+
}
|
|
2442
|
+
issues.push(...conditionRuleFindings(stage.entryConditions, planStage.entry, `${stagePath}.data.entryConditions`, stageLabel, selectors), ...conditionRuleFindings(stage.exitConditions, planStage.exit, `${stagePath}.data.exitConditions`, stageLabel, selectors), ...conditionShapeFindings(stage.entryConditions, planStage.entry, `${stagePath}.data.entryConditions`, stageLabel), ...conditionShapeFindings(stage.exitConditions, planStage.exit, `${stagePath}.data.exitConditions`, stageLabel), ...conditionExpressionFindings(stage.entryConditions, planStage.entry, plan, `${stagePath}.data.entryConditions`, stageLabel), ...conditionExpressionFindings(stage.exitConditions, planStage.exit, plan, `${stagePath}.data.exitConditions`, stageLabel), ...escalationFindings(stage.slaEscalations, planStage.sla, `${stagePath}.data.slaRules`, stageLabel));
|
|
2443
|
+
const bound = resolveTasks(stage.tasks, planStage.tasks);
|
|
2444
|
+
stage.tasks.forEach((task, row) => {
|
|
2445
|
+
const taskIndex = bound[row];
|
|
2446
|
+
const label = `${stageLabel} task "${task.name}"`;
|
|
2447
|
+
if (taskIndex === undefined) {
|
|
2448
|
+
issues.push(error("STRICT_SDD_TASK_MISSING", `${stagePath}.data.tasks[${task.name}]`, `${label} is declared in the SDD and absent from the caseplan.`));
|
|
2449
|
+
return;
|
|
2450
|
+
}
|
|
2451
|
+
issues.push(...compareTask(plan, task, planStage.tasks[taskIndex], stagePath, label, selectors));
|
|
2452
|
+
});
|
|
2453
|
+
planStage.tasks.forEach((extra, index) => {
|
|
2454
|
+
if (bound.includes(index))
|
|
2455
|
+
return;
|
|
2456
|
+
issues.push(warning("STRICT_SDD_TASK_EXTRA", `${stagePath}.data.tasks[${extra.name ?? index}]`, `${stageLabel} task "${extra.name ?? index}" is in the caseplan with no matching SDD task row.`));
|
|
2457
|
+
});
|
|
2458
|
+
return issues;
|
|
2459
|
+
};
|
|
2460
|
+
var compare = (sdd, plan) => {
|
|
2461
|
+
const issues = [];
|
|
2462
|
+
const stageBound = new Set;
|
|
2463
|
+
const pairs = [];
|
|
2464
|
+
for (const stage of sdd.stages) {
|
|
2465
|
+
const key = norm(stage.name);
|
|
2466
|
+
let index = plan.stages.findIndex((candidate, at) => !stageBound.has(at) && norm(candidate.name) === key);
|
|
2467
|
+
if (index === -1) {
|
|
2468
|
+
const loose = normAnnotationless(stage.name);
|
|
2469
|
+
const hits = plan.stages.map((candidate, at) => ({ candidate, at })).filter(({ candidate, at }) => !stageBound.has(at) && normAnnotationless(candidate.name) === loose);
|
|
2470
|
+
if (hits.length === 1)
|
|
2471
|
+
index = hits[0].at;
|
|
2472
|
+
}
|
|
2473
|
+
if (index === -1) {
|
|
2474
|
+
issues.push(error("STRICT_SDD_STAGE_MISSING", `nodes[${stage.name}]`, `Stage "${stage.name}" is declared in the SDD and absent from the caseplan.`));
|
|
2475
|
+
continue;
|
|
2476
|
+
}
|
|
2477
|
+
stageBound.add(index);
|
|
2478
|
+
pairs.push({ stage, at: index });
|
|
2479
|
+
}
|
|
2480
|
+
const sddStageOfPlanId = new Map;
|
|
2481
|
+
for (const { stage, at } of pairs) {
|
|
2482
|
+
const id = plan.stages[at].id;
|
|
2483
|
+
if (id !== undefined && id !== "")
|
|
2484
|
+
sddStageOfPlanId.set(id, stage.name);
|
|
2485
|
+
}
|
|
2486
|
+
const selectors = {
|
|
2487
|
+
stage: (id) => sddStageOfPlanId.get(id),
|
|
2488
|
+
task: (id) => plan.stages.flatMap((s) => s.tasks).find((t) => t.id === id && t.name !== undefined)?.name,
|
|
2489
|
+
label: (text) => {
|
|
2490
|
+
const hits = sdd.stages.filter((candidate) => sameLabel(candidate.name, text) || norm(leadingSegment(candidate.name)) === norm(text));
|
|
2491
|
+
return hits.length === 1 ? hits[0].name : undefined;
|
|
2492
|
+
}
|
|
2493
|
+
};
|
|
2494
|
+
for (const { stage, at } of pairs) {
|
|
2495
|
+
issues.push(...compareStage(plan, stage, plan.stages[at], selectors));
|
|
2496
|
+
}
|
|
2497
|
+
plan.stages.forEach((extra, index) => {
|
|
2498
|
+
if (stageBound.has(index))
|
|
2499
|
+
return;
|
|
2500
|
+
issues.push(warning("STRICT_SDD_STAGE_EXTRA", `nodes[${extra.name ?? index}]`, `Stage "${extra.name ?? index}" is in the caseplan with no matching SDD stage.`));
|
|
2501
|
+
});
|
|
2502
|
+
issues.push(...compareCaseLevel(sdd, plan));
|
|
2503
|
+
return issues;
|
|
2504
|
+
};
|
|
2505
|
+
var auditCaseplanAgainstSdd = (parsed, sddText, sddName) => {
|
|
2506
|
+
const sdd = parseSdd(sddText);
|
|
2507
|
+
const issues = sdd.parseNotes.map((note) => warning("STRICT_SDD_PARSE_NOTE", sddName, note));
|
|
2508
|
+
if (sdd.stages.length === 0) {
|
|
2509
|
+
issues.push(error("STRICT_SDD_PARSE_EMPTY", sddName, `No stages parsed from "${sddName}", so every completeness check would pass without checking anything. Stage headings must read \`### Stage <n>: <name>\`.`));
|
|
2510
|
+
return issues;
|
|
2511
|
+
}
|
|
2512
|
+
issues.push(...compare(sdd, parseCaseplan(parsed)));
|
|
2513
|
+
issues.push(...slaReferenceFindings(sddText, sddName));
|
|
2514
|
+
return issues;
|
|
2515
|
+
};
|
|
2516
|
+
export {
|
|
2517
|
+
auditCaseplanAgainstSdd
|
|
2518
|
+
};
|
|
2519
|
+
|
|
2520
|
+
//# debugId=06CF0AAF35CEE5DC64756E2164756E21
|