@f-o-t/rules-engine 2.0.2 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +168 -0
- package/LICENSE.md +16 -4
- package/README.md +106 -23
- package/__tests__/builder.test.ts +363 -0
- package/__tests__/cache.test.ts +130 -0
- package/__tests__/config.test.ts +35 -0
- package/__tests__/engine.test.ts +1213 -0
- package/__tests__/evaluate.test.ts +339 -0
- package/__tests__/exports.test.ts +30 -0
- package/__tests__/filter-sort.test.ts +303 -0
- package/__tests__/integration.test.ts +419 -0
- package/__tests__/money-integration.test.ts +149 -0
- package/__tests__/validation.test.ts +862 -0
- package/biome.json +39 -0
- package/docs/MIGRATION-v3.md +118 -0
- package/fot.config.ts +5 -0
- package/package.json +31 -67
- package/src/analyzer/analysis.ts +401 -0
- package/src/builder/conditions.ts +321 -0
- package/src/builder/rule.ts +192 -0
- package/src/cache/cache.ts +135 -0
- package/src/cache/noop.ts +20 -0
- package/src/core/evaluate.ts +185 -0
- package/src/core/filter.ts +85 -0
- package/src/core/group.ts +103 -0
- package/src/core/sort.ts +90 -0
- package/src/engine/engine.ts +462 -0
- package/src/engine/hooks.ts +235 -0
- package/src/engine/state.ts +322 -0
- package/src/index.ts +303 -0
- package/src/optimizer/index-builder.ts +381 -0
- package/src/serialization/serializer.ts +408 -0
- package/src/simulation/simulator.ts +359 -0
- package/src/types/config.ts +184 -0
- package/src/types/consequence.ts +38 -0
- package/src/types/evaluation.ts +87 -0
- package/src/types/rule.ts +112 -0
- package/src/types/state.ts +116 -0
- package/src/utils/conditions.ts +108 -0
- package/src/utils/hash.ts +30 -0
- package/src/utils/id.ts +6 -0
- package/src/utils/time.ts +42 -0
- package/src/validation/conflicts.ts +440 -0
- package/src/validation/integrity.ts +473 -0
- package/src/validation/schema.ts +386 -0
- package/src/versioning/version-store.ts +337 -0
- package/tsconfig.json +29 -0
- package/dist/index.cjs +0 -3088
- package/dist/index.d.cts +0 -1173
- package/dist/index.d.ts +0 -1173
- package/dist/index.js +0 -3072
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Condition,
|
|
3
|
+
type ConditionGroup,
|
|
4
|
+
isConditionGroup,
|
|
5
|
+
} from "@f-o-t/condition-evaluator";
|
|
6
|
+
import type {
|
|
7
|
+
ConsequenceDefinitions,
|
|
8
|
+
DefaultConsequences,
|
|
9
|
+
} from "../types/consequence";
|
|
10
|
+
import type { Rule, RuleSet } from "../types/rule";
|
|
11
|
+
|
|
12
|
+
export type IntegrityIssue = {
|
|
13
|
+
readonly code: string;
|
|
14
|
+
readonly message: string;
|
|
15
|
+
readonly severity: "error" | "warning" | "info";
|
|
16
|
+
readonly path?: string;
|
|
17
|
+
readonly ruleId?: string;
|
|
18
|
+
readonly details?: Readonly<Record<string, unknown>>;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type IntegrityCheckResult = {
|
|
22
|
+
readonly valid: boolean;
|
|
23
|
+
readonly issues: ReadonlyArray<IntegrityIssue>;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type IntegrityCheckOptions = {
|
|
27
|
+
readonly checkCircularReferences?: boolean;
|
|
28
|
+
readonly checkOrphanedRuleSets?: boolean;
|
|
29
|
+
readonly checkMissingReferences?: boolean;
|
|
30
|
+
readonly checkFieldConsistency?: boolean;
|
|
31
|
+
readonly requiredFields?: ReadonlyArray<string>;
|
|
32
|
+
readonly allowedCategories?: ReadonlyArray<string>;
|
|
33
|
+
readonly allowedTags?: ReadonlyArray<string>;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const DEFAULT_OPTIONS: IntegrityCheckOptions = {
|
|
37
|
+
checkCircularReferences: true,
|
|
38
|
+
checkOrphanedRuleSets: true,
|
|
39
|
+
checkMissingReferences: true,
|
|
40
|
+
checkFieldConsistency: true,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const createIssue = (
|
|
44
|
+
code: string,
|
|
45
|
+
message: string,
|
|
46
|
+
severity: "error" | "warning" | "info",
|
|
47
|
+
details?: {
|
|
48
|
+
path?: string;
|
|
49
|
+
ruleId?: string;
|
|
50
|
+
extra?: Record<string, unknown>;
|
|
51
|
+
},
|
|
52
|
+
): IntegrityIssue => ({
|
|
53
|
+
code,
|
|
54
|
+
message,
|
|
55
|
+
severity,
|
|
56
|
+
path: details?.path,
|
|
57
|
+
ruleId: details?.ruleId,
|
|
58
|
+
details: details?.extra,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const collectAllFields = (condition: ConditionGroup): Set<string> => {
|
|
62
|
+
const fields = new Set<string>();
|
|
63
|
+
|
|
64
|
+
const traverse = (c: Condition | ConditionGroup) => {
|
|
65
|
+
if (isConditionGroup(c)) {
|
|
66
|
+
for (const child of c.conditions) {
|
|
67
|
+
traverse(child as Condition | ConditionGroup);
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
fields.add(c.field);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
traverse(condition);
|
|
75
|
+
return fields;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const checkDuplicateConditionIds = (
|
|
79
|
+
condition: ConditionGroup,
|
|
80
|
+
ruleId: string,
|
|
81
|
+
): ReadonlyArray<IntegrityIssue> => {
|
|
82
|
+
const issues: IntegrityIssue[] = [];
|
|
83
|
+
const seenIds = new Map<string, number>();
|
|
84
|
+
|
|
85
|
+
const traverse = (c: Condition | ConditionGroup, path: string) => {
|
|
86
|
+
const id = c.id;
|
|
87
|
+
const count = seenIds.get(id) ?? 0;
|
|
88
|
+
seenIds.set(id, count + 1);
|
|
89
|
+
|
|
90
|
+
if (count > 0) {
|
|
91
|
+
issues.push(
|
|
92
|
+
createIssue(
|
|
93
|
+
"DUPLICATE_CONDITION_ID",
|
|
94
|
+
`Duplicate condition ID "${id}" found within rule`,
|
|
95
|
+
"error",
|
|
96
|
+
{ path, ruleId, extra: { conditionId: id } },
|
|
97
|
+
),
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (isConditionGroup(c)) {
|
|
102
|
+
c.conditions.forEach((child, i) => {
|
|
103
|
+
traverse(child as Condition | ConditionGroup, `${path}[${i}]`);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
traverse(condition, "conditions");
|
|
109
|
+
return issues;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const checkRuleIntegrity = <
|
|
113
|
+
TContext = unknown,
|
|
114
|
+
TConsequences extends ConsequenceDefinitions = DefaultConsequences,
|
|
115
|
+
>(
|
|
116
|
+
rule: Rule<TContext, TConsequences>,
|
|
117
|
+
options: IntegrityCheckOptions,
|
|
118
|
+
): ReadonlyArray<IntegrityIssue> => {
|
|
119
|
+
const issues: IntegrityIssue[] = [];
|
|
120
|
+
|
|
121
|
+
issues.push(...checkDuplicateConditionIds(rule.conditions, rule.id));
|
|
122
|
+
|
|
123
|
+
if (rule.priority < 0) {
|
|
124
|
+
issues.push(
|
|
125
|
+
createIssue(
|
|
126
|
+
"NEGATIVE_PRIORITY",
|
|
127
|
+
`Rule "${rule.name}" has negative priority: ${rule.priority}`,
|
|
128
|
+
"warning",
|
|
129
|
+
{ ruleId: rule.id, extra: { priority: rule.priority } },
|
|
130
|
+
),
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (rule.priority > 1000) {
|
|
135
|
+
issues.push(
|
|
136
|
+
createIssue(
|
|
137
|
+
"EXTREME_PRIORITY",
|
|
138
|
+
`Rule "${rule.name}" has very high priority: ${rule.priority}`,
|
|
139
|
+
"info",
|
|
140
|
+
{ ruleId: rule.id, extra: { priority: rule.priority } },
|
|
141
|
+
),
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (rule.consequences.length === 0) {
|
|
146
|
+
issues.push(
|
|
147
|
+
createIssue(
|
|
148
|
+
"NO_CONSEQUENCES",
|
|
149
|
+
`Rule "${rule.name}" has no consequences defined`,
|
|
150
|
+
"warning",
|
|
151
|
+
{ ruleId: rule.id },
|
|
152
|
+
),
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (options.allowedCategories && rule.category) {
|
|
157
|
+
if (!options.allowedCategories.includes(rule.category)) {
|
|
158
|
+
issues.push(
|
|
159
|
+
createIssue(
|
|
160
|
+
"INVALID_CATEGORY",
|
|
161
|
+
`Rule "${rule.name}" has invalid category: ${rule.category}`,
|
|
162
|
+
"error",
|
|
163
|
+
{
|
|
164
|
+
ruleId: rule.id,
|
|
165
|
+
extra: {
|
|
166
|
+
category: rule.category,
|
|
167
|
+
allowedCategories: [...options.allowedCategories],
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
),
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (options.allowedTags) {
|
|
176
|
+
const invalidTags = rule.tags.filter(
|
|
177
|
+
(t) => !options.allowedTags?.includes(t),
|
|
178
|
+
);
|
|
179
|
+
if (invalidTags.length > 0) {
|
|
180
|
+
issues.push(
|
|
181
|
+
createIssue(
|
|
182
|
+
"INVALID_TAGS",
|
|
183
|
+
`Rule "${rule.name}" has invalid tags: ${invalidTags.join(", ")}`,
|
|
184
|
+
"warning",
|
|
185
|
+
{
|
|
186
|
+
ruleId: rule.id,
|
|
187
|
+
extra: {
|
|
188
|
+
invalidTags,
|
|
189
|
+
allowedTags: [...options.allowedTags],
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
),
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (options.requiredFields) {
|
|
198
|
+
const ruleFields = collectAllFields(rule.conditions);
|
|
199
|
+
const missingFields = options.requiredFields.filter(
|
|
200
|
+
(f) => !ruleFields.has(f),
|
|
201
|
+
);
|
|
202
|
+
if (missingFields.length > 0) {
|
|
203
|
+
issues.push(
|
|
204
|
+
createIssue(
|
|
205
|
+
"MISSING_REQUIRED_FIELDS",
|
|
206
|
+
`Rule "${rule.name}" is missing required fields: ${missingFields.join(", ")}`,
|
|
207
|
+
"warning",
|
|
208
|
+
{ ruleId: rule.id, extra: { missingFields } },
|
|
209
|
+
),
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return issues;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const checkRuleSetIntegrity = <
|
|
218
|
+
TContext = unknown,
|
|
219
|
+
TConsequences extends ConsequenceDefinitions = DefaultConsequences,
|
|
220
|
+
>(
|
|
221
|
+
ruleSet: RuleSet,
|
|
222
|
+
rules: ReadonlyArray<Rule<TContext, TConsequences>>,
|
|
223
|
+
): ReadonlyArray<IntegrityIssue> => {
|
|
224
|
+
const issues: IntegrityIssue[] = [];
|
|
225
|
+
const ruleIds = new Set(rules.map((r) => r.id));
|
|
226
|
+
|
|
227
|
+
for (const ruleId of ruleSet.ruleIds) {
|
|
228
|
+
if (!ruleIds.has(ruleId)) {
|
|
229
|
+
issues.push(
|
|
230
|
+
createIssue(
|
|
231
|
+
"MISSING_RULE_REFERENCE",
|
|
232
|
+
`RuleSet "${ruleSet.name}" references non-existent rule: ${ruleId}`,
|
|
233
|
+
"error",
|
|
234
|
+
{ extra: { ruleSetId: ruleSet.id, missingRuleId: ruleId } },
|
|
235
|
+
),
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (ruleSet.ruleIds.length === 0) {
|
|
241
|
+
issues.push(
|
|
242
|
+
createIssue(
|
|
243
|
+
"EMPTY_RULESET",
|
|
244
|
+
`RuleSet "${ruleSet.name}" contains no rules`,
|
|
245
|
+
"warning",
|
|
246
|
+
{ extra: { ruleSetId: ruleSet.id } },
|
|
247
|
+
),
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const duplicateIds = ruleSet.ruleIds.filter(
|
|
252
|
+
(id, i) => ruleSet.ruleIds.indexOf(id) !== i,
|
|
253
|
+
);
|
|
254
|
+
if (duplicateIds.length > 0) {
|
|
255
|
+
issues.push(
|
|
256
|
+
createIssue(
|
|
257
|
+
"DUPLICATE_RULESET_ENTRIES",
|
|
258
|
+
`RuleSet "${ruleSet.name}" contains duplicate rule references: ${[...new Set(duplicateIds)].join(", ")}`,
|
|
259
|
+
"warning",
|
|
260
|
+
{ extra: { ruleSetId: ruleSet.id, duplicateIds } },
|
|
261
|
+
),
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return issues;
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const checkFieldConsistency = <
|
|
269
|
+
TContext = unknown,
|
|
270
|
+
TConsequences extends ConsequenceDefinitions = DefaultConsequences,
|
|
271
|
+
>(
|
|
272
|
+
rules: ReadonlyArray<Rule<TContext, TConsequences>>,
|
|
273
|
+
): ReadonlyArray<IntegrityIssue> => {
|
|
274
|
+
const issues: IntegrityIssue[] = [];
|
|
275
|
+
const fieldTypes = new Map<
|
|
276
|
+
string,
|
|
277
|
+
{ type: string; ruleId: string; ruleName: string }[]
|
|
278
|
+
>();
|
|
279
|
+
|
|
280
|
+
for (const rule of rules) {
|
|
281
|
+
const traverse = (c: Condition | ConditionGroup) => {
|
|
282
|
+
if (isConditionGroup(c)) {
|
|
283
|
+
for (const child of c.conditions) {
|
|
284
|
+
traverse(child as Condition | ConditionGroup);
|
|
285
|
+
}
|
|
286
|
+
} else {
|
|
287
|
+
const existing = fieldTypes.get(c.field) ?? [];
|
|
288
|
+
existing.push({
|
|
289
|
+
type: c.type,
|
|
290
|
+
ruleId: rule.id,
|
|
291
|
+
ruleName: rule.name,
|
|
292
|
+
});
|
|
293
|
+
fieldTypes.set(c.field, existing);
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
traverse(rule.conditions);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
for (const [field, types] of fieldTypes) {
|
|
301
|
+
const uniqueTypes = [...new Set(types.map((t) => t.type))];
|
|
302
|
+
if (uniqueTypes.length > 1) {
|
|
303
|
+
issues.push(
|
|
304
|
+
createIssue(
|
|
305
|
+
"INCONSISTENT_FIELD_TYPE",
|
|
306
|
+
`Field "${field}" is used with different types: ${uniqueTypes.join(", ")}`,
|
|
307
|
+
"warning",
|
|
308
|
+
{
|
|
309
|
+
extra: {
|
|
310
|
+
field,
|
|
311
|
+
types: uniqueTypes,
|
|
312
|
+
rules: types.map((t) => ({
|
|
313
|
+
id: t.ruleId,
|
|
314
|
+
name: t.ruleName,
|
|
315
|
+
type: t.type,
|
|
316
|
+
})),
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
),
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return issues;
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
export const checkIntegrity = <
|
|
328
|
+
TContext = unknown,
|
|
329
|
+
TConsequences extends ConsequenceDefinitions = DefaultConsequences,
|
|
330
|
+
>(
|
|
331
|
+
rules: ReadonlyArray<Rule<TContext, TConsequences>>,
|
|
332
|
+
ruleSets: ReadonlyArray<RuleSet> = [],
|
|
333
|
+
options: IntegrityCheckOptions = {},
|
|
334
|
+
): IntegrityCheckResult => {
|
|
335
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
336
|
+
const issues: IntegrityIssue[] = [];
|
|
337
|
+
|
|
338
|
+
for (const rule of rules) {
|
|
339
|
+
issues.push(...checkRuleIntegrity(rule, opts));
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
for (const ruleSet of ruleSets) {
|
|
343
|
+
issues.push(...checkRuleSetIntegrity(ruleSet, rules));
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (opts.checkFieldConsistency) {
|
|
347
|
+
issues.push(...checkFieldConsistency(rules));
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return {
|
|
351
|
+
valid: issues.filter((i) => i.severity === "error").length === 0,
|
|
352
|
+
issues,
|
|
353
|
+
};
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
export const checkRuleFieldCoverage = <
|
|
357
|
+
TContext = unknown,
|
|
358
|
+
TConsequences extends ConsequenceDefinitions = DefaultConsequences,
|
|
359
|
+
>(
|
|
360
|
+
rules: ReadonlyArray<Rule<TContext, TConsequences>>,
|
|
361
|
+
expectedFields: ReadonlyArray<string>,
|
|
362
|
+
): {
|
|
363
|
+
coveredFields: ReadonlyArray<string>;
|
|
364
|
+
uncoveredFields: ReadonlyArray<string>;
|
|
365
|
+
extraFields: ReadonlyArray<string>;
|
|
366
|
+
coveragePercentage: number;
|
|
367
|
+
} => {
|
|
368
|
+
const allFields = new Set<string>();
|
|
369
|
+
|
|
370
|
+
for (const rule of rules) {
|
|
371
|
+
const ruleFields = collectAllFields(rule.conditions);
|
|
372
|
+
for (const field of ruleFields) {
|
|
373
|
+
allFields.add(field);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const expectedSet = new Set(expectedFields);
|
|
378
|
+
const coveredFields = [...expectedFields].filter((f) => allFields.has(f));
|
|
379
|
+
const uncoveredFields = [...expectedFields].filter((f) => !allFields.has(f));
|
|
380
|
+
const extraFields = [...allFields].filter((f) => !expectedSet.has(f));
|
|
381
|
+
|
|
382
|
+
return {
|
|
383
|
+
coveredFields,
|
|
384
|
+
uncoveredFields,
|
|
385
|
+
extraFields,
|
|
386
|
+
coveragePercentage:
|
|
387
|
+
expectedFields.length > 0
|
|
388
|
+
? (coveredFields.length / expectedFields.length) * 100
|
|
389
|
+
: 100,
|
|
390
|
+
};
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
export const getUsedFields = <
|
|
394
|
+
TContext = unknown,
|
|
395
|
+
TConsequences extends ConsequenceDefinitions = DefaultConsequences,
|
|
396
|
+
>(
|
|
397
|
+
rules: ReadonlyArray<Rule<TContext, TConsequences>>,
|
|
398
|
+
): ReadonlyArray<string> => {
|
|
399
|
+
const fields = new Set<string>();
|
|
400
|
+
|
|
401
|
+
for (const rule of rules) {
|
|
402
|
+
const ruleFields = collectAllFields(rule.conditions);
|
|
403
|
+
for (const field of ruleFields) {
|
|
404
|
+
fields.add(field);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return [...fields].sort();
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
export const getUsedOperators = <
|
|
412
|
+
TContext = unknown,
|
|
413
|
+
TConsequences extends ConsequenceDefinitions = DefaultConsequences,
|
|
414
|
+
>(
|
|
415
|
+
rules: ReadonlyArray<Rule<TContext, TConsequences>>,
|
|
416
|
+
): ReadonlyArray<{ field: string; operator: string; type: string }> => {
|
|
417
|
+
const operators: Array<{ field: string; operator: string; type: string }> =
|
|
418
|
+
[];
|
|
419
|
+
const seen = new Set<string>();
|
|
420
|
+
|
|
421
|
+
for (const rule of rules) {
|
|
422
|
+
const traverse = (c: Condition | ConditionGroup) => {
|
|
423
|
+
if (isConditionGroup(c)) {
|
|
424
|
+
for (const child of c.conditions) {
|
|
425
|
+
traverse(child as Condition | ConditionGroup);
|
|
426
|
+
}
|
|
427
|
+
} else {
|
|
428
|
+
const key = `${c.field}:${c.operator}:${c.type}`;
|
|
429
|
+
if (!seen.has(key)) {
|
|
430
|
+
seen.add(key);
|
|
431
|
+
operators.push({
|
|
432
|
+
field: c.field,
|
|
433
|
+
operator: c.operator,
|
|
434
|
+
type: c.type,
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
traverse(rule.conditions);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return operators;
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
export const formatIntegrityResult = (result: IntegrityCheckResult): string => {
|
|
447
|
+
if (result.valid && result.issues.length === 0) {
|
|
448
|
+
return "Integrity check passed - no issues found";
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const lines: string[] = [
|
|
452
|
+
result.valid
|
|
453
|
+
? `Integrity check passed with ${result.issues.length} warning(s)`
|
|
454
|
+
: `Integrity check failed with ${result.issues.filter((i) => i.severity === "error").length} error(s)`,
|
|
455
|
+
];
|
|
456
|
+
|
|
457
|
+
const grouped = {
|
|
458
|
+
error: result.issues.filter((i) => i.severity === "error"),
|
|
459
|
+
warning: result.issues.filter((i) => i.severity === "warning"),
|
|
460
|
+
info: result.issues.filter((i) => i.severity === "info"),
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
for (const [severity, issues] of Object.entries(grouped)) {
|
|
464
|
+
if (issues.length > 0) {
|
|
465
|
+
lines.push(`\n${severity.toUpperCase()}S (${issues.length}):`);
|
|
466
|
+
for (const issue of issues) {
|
|
467
|
+
lines.push(` - [${issue.code}] ${issue.message}`);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return lines.join("\n");
|
|
473
|
+
};
|