@f-o-t/rules-engine 1.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/LICENSE.md +9 -0
- package/README.md +513 -0
- package/dist/index.cjs +3020 -0
- package/dist/index.d.cts +1312 -0
- package/dist/index.d.ts +1312 -0
- package/dist/index.js +2999 -0
- package/package.json +69 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,3020 @@
|
|
|
1
|
+
var import_node_module = require("node:module");
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __moduleCache = /* @__PURE__ */ new WeakMap;
|
|
7
|
+
var __toCommonJS = (from) => {
|
|
8
|
+
var entry = __moduleCache.get(from), desc;
|
|
9
|
+
if (entry)
|
|
10
|
+
return entry;
|
|
11
|
+
entry = __defProp({}, "__esModule", { value: true });
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function")
|
|
13
|
+
__getOwnPropNames(from).map((key) => !__hasOwnProp.call(entry, key) && __defProp(entry, key, {
|
|
14
|
+
get: () => from[key],
|
|
15
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
16
|
+
}));
|
|
17
|
+
__moduleCache.set(from, entry);
|
|
18
|
+
return entry;
|
|
19
|
+
};
|
|
20
|
+
var __export = (target, all) => {
|
|
21
|
+
for (var name in all)
|
|
22
|
+
__defProp(target, name, {
|
|
23
|
+
get: all[name],
|
|
24
|
+
enumerable: true,
|
|
25
|
+
configurable: true,
|
|
26
|
+
set: (newValue) => all[name] = () => newValue
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var exports_src = {};
|
|
32
|
+
__export(exports_src, {
|
|
33
|
+
withTimeout: () => withTimeout,
|
|
34
|
+
whatIf: () => whatIf,
|
|
35
|
+
validateRules: () => validateRules,
|
|
36
|
+
validateRuleSet: () => validateRuleSet,
|
|
37
|
+
validateRule: () => validateRule,
|
|
38
|
+
validateConditions: () => validateConditions,
|
|
39
|
+
updateRule: () => updateRule,
|
|
40
|
+
tap: () => tap,
|
|
41
|
+
str: () => str,
|
|
42
|
+
sortRules: () => sortRules,
|
|
43
|
+
sortByUpdatedAt: () => sortByUpdatedAt,
|
|
44
|
+
sortByPriority: () => sortByPriority,
|
|
45
|
+
sortByName: () => sortByName,
|
|
46
|
+
sortByCreatedAt: () => sortByCreatedAt,
|
|
47
|
+
simulateSingleRule: () => simulateSingleRule,
|
|
48
|
+
simulate: () => simulate,
|
|
49
|
+
serializeRuleSet: () => serializeRuleSet,
|
|
50
|
+
serializeRule: () => serializeRule,
|
|
51
|
+
safeParseRule: () => safeParseRule,
|
|
52
|
+
rule: () => rule,
|
|
53
|
+
rollbackToVersion: () => rollbackToVersion,
|
|
54
|
+
resetBuilderIds: () => resetBuilderIds,
|
|
55
|
+
removeRuleSet: () => removeRuleSet,
|
|
56
|
+
removeRule: () => removeRule,
|
|
57
|
+
pruneOldVersions: () => pruneOldVersions,
|
|
58
|
+
pipe: () => pipe,
|
|
59
|
+
parseRule: () => parseRule,
|
|
60
|
+
or: () => or,
|
|
61
|
+
num: () => num,
|
|
62
|
+
mergeRuleSets: () => mergeRuleSets,
|
|
63
|
+
measureTimeAsync: () => measureTimeAsync,
|
|
64
|
+
measureTime: () => measureTime,
|
|
65
|
+
importRules: () => importRules,
|
|
66
|
+
importFromJson: () => importFromJson,
|
|
67
|
+
identity: () => identity,
|
|
68
|
+
hashRules: () => hashRules,
|
|
69
|
+
hashContext: () => hashContext,
|
|
70
|
+
hasErrors: () => hasErrors,
|
|
71
|
+
hasConflicts: () => hasConflicts,
|
|
72
|
+
groupRules: () => groupRules,
|
|
73
|
+
groupByPriority: () => groupByPriority,
|
|
74
|
+
groupByEnabled: () => groupByEnabled,
|
|
75
|
+
groupByCustom: () => groupByCustom,
|
|
76
|
+
groupByCategory: () => groupByCategory,
|
|
77
|
+
getVersionsByDateRange: () => getVersionsByDateRange,
|
|
78
|
+
getVersionsByChangeType: () => getVersionsByChangeType,
|
|
79
|
+
getVersion: () => getVersion,
|
|
80
|
+
getUsedOperators: () => getUsedOperators,
|
|
81
|
+
getUsedFields: () => getUsedFields,
|
|
82
|
+
getRulesInSet: () => getRulesInSet,
|
|
83
|
+
getRulesByTags: () => getRulesByTags,
|
|
84
|
+
getRulesByTag: () => getRulesByTag,
|
|
85
|
+
getRulesByPriorityRange: () => getRulesByPriorityRange,
|
|
86
|
+
getRulesByPriority: () => getRulesByPriority,
|
|
87
|
+
getRulesByFields: () => getRulesByFields,
|
|
88
|
+
getRulesByField: () => getRulesByField,
|
|
89
|
+
getRulesByCategory: () => getRulesByCategory,
|
|
90
|
+
getRules: () => getRules,
|
|
91
|
+
getRuleSets: () => getRuleSets,
|
|
92
|
+
getRuleSet: () => getRuleSet,
|
|
93
|
+
getRuleById: () => getRuleById,
|
|
94
|
+
getRule: () => getRule,
|
|
95
|
+
getLatestVersion: () => getLatestVersion,
|
|
96
|
+
getIndexStats: () => getIndexStats,
|
|
97
|
+
getHistory: () => getHistory,
|
|
98
|
+
getConflictsByType: () => getConflictsByType,
|
|
99
|
+
getConflictsBySeverity: () => getConflictsBySeverity,
|
|
100
|
+
getAllVersions: () => getAllVersions,
|
|
101
|
+
generateId: () => generateId,
|
|
102
|
+
formatVersionHistory: () => formatVersionHistory,
|
|
103
|
+
formatSimulationResult: () => formatSimulationResult,
|
|
104
|
+
formatRuleSetAnalysis: () => formatRuleSetAnalysis,
|
|
105
|
+
formatIntegrityResult: () => formatIntegrityResult,
|
|
106
|
+
formatConflicts: () => formatConflicts,
|
|
107
|
+
findRulesAffectedByContextChange: () => findRulesAffectedByContextChange,
|
|
108
|
+
findMostComplexRules: () => findMostComplexRules,
|
|
109
|
+
findLeastUsedFields: () => findLeastUsedFields,
|
|
110
|
+
filterRulesForContext: () => filterRulesForContext,
|
|
111
|
+
filterRules: () => filterRules,
|
|
112
|
+
filterByTags: () => filterByTags,
|
|
113
|
+
filterByIds: () => filterByIds,
|
|
114
|
+
filterByEnabled: () => filterByEnabled,
|
|
115
|
+
filterByCategory: () => filterByCategory,
|
|
116
|
+
exportToJson: () => exportToJson,
|
|
117
|
+
exportRules: () => exportRules,
|
|
118
|
+
evaluateRules: () => evaluateRules,
|
|
119
|
+
evaluateRule: () => evaluateRule,
|
|
120
|
+
enableRule: () => enableRule,
|
|
121
|
+
disableRule: () => disableRule,
|
|
122
|
+
diffRuleSets: () => diffRuleSets,
|
|
123
|
+
detectConflicts: () => detectConflicts,
|
|
124
|
+
deserializeRuleSet: () => deserializeRuleSet,
|
|
125
|
+
deserializeRule: () => deserializeRule,
|
|
126
|
+
delay: () => delay,
|
|
127
|
+
date: () => date,
|
|
128
|
+
createVersionStore: () => createVersionStore,
|
|
129
|
+
createRuleValidator: () => createRuleValidator,
|
|
130
|
+
createRule: () => createRule,
|
|
131
|
+
createNoopCache: () => createNoopCache,
|
|
132
|
+
createInitialState: () => createInitialState,
|
|
133
|
+
createInitialRuleStats: () => createInitialRuleStats,
|
|
134
|
+
createInitialOptimizerState: () => createInitialOptimizerState,
|
|
135
|
+
createEngine: () => createEngine,
|
|
136
|
+
createCache: () => createCache,
|
|
137
|
+
conditions: () => conditions,
|
|
138
|
+
compose: () => compose,
|
|
139
|
+
compareVersions: () => compareVersions,
|
|
140
|
+
cloneState: () => cloneState,
|
|
141
|
+
cloneRule: () => cloneRule,
|
|
142
|
+
clearRules: () => clearRules,
|
|
143
|
+
checkRuleFieldCoverage: () => checkRuleFieldCoverage,
|
|
144
|
+
checkIntegrity: () => checkIntegrity,
|
|
145
|
+
buildIndex: () => buildIndex,
|
|
146
|
+
bool: () => bool,
|
|
147
|
+
batchSimulate: () => batchSimulate,
|
|
148
|
+
arr: () => arr,
|
|
149
|
+
any: () => any,
|
|
150
|
+
and: () => and,
|
|
151
|
+
analyzeRuleSet: () => analyzeRuleSet,
|
|
152
|
+
analyzeRuleComplexity: () => analyzeRuleComplexity,
|
|
153
|
+
analyzeOptimizations: () => analyzeOptimizations,
|
|
154
|
+
analyzeOperatorUsage: () => analyzeOperatorUsage,
|
|
155
|
+
analyzeFieldUsage: () => analyzeFieldUsage,
|
|
156
|
+
analyzeConsequenceUsage: () => analyzeConsequenceUsage,
|
|
157
|
+
always: () => always,
|
|
158
|
+
all: () => all,
|
|
159
|
+
addVersion: () => addVersion,
|
|
160
|
+
addRules: () => addRules,
|
|
161
|
+
addRuleSet: () => addRuleSet,
|
|
162
|
+
addRule: () => addRule,
|
|
163
|
+
RuleSetSchema: () => RuleSetSchema,
|
|
164
|
+
RuleSchema: () => RuleSchema,
|
|
165
|
+
DEFAULT_VERSIONING_CONFIG: () => DEFAULT_VERSIONING_CONFIG,
|
|
166
|
+
DEFAULT_VALIDATION_CONFIG: () => DEFAULT_VALIDATION_CONFIG,
|
|
167
|
+
DEFAULT_ENGINE_CONFIG: () => DEFAULT_ENGINE_CONFIG,
|
|
168
|
+
DEFAULT_CACHE_CONFIG: () => DEFAULT_CACHE_CONFIG
|
|
169
|
+
});
|
|
170
|
+
module.exports = __toCommonJS(exports_src);
|
|
171
|
+
|
|
172
|
+
// src/analyzer/analysis.ts
|
|
173
|
+
var import_condition_evaluator = require("@f-o-t/condition-evaluator");
|
|
174
|
+
var countConditions = (condition) => {
|
|
175
|
+
if (import_condition_evaluator.isConditionGroup(condition)) {
|
|
176
|
+
return condition.conditions.reduce((sum, c) => sum + countConditions(c), 0);
|
|
177
|
+
}
|
|
178
|
+
return 1;
|
|
179
|
+
};
|
|
180
|
+
var calculateMaxDepth = (condition, currentDepth = 1) => {
|
|
181
|
+
if (import_condition_evaluator.isConditionGroup(condition)) {
|
|
182
|
+
if (condition.conditions.length === 0)
|
|
183
|
+
return currentDepth;
|
|
184
|
+
return Math.max(...condition.conditions.map((c) => calculateMaxDepth(c, currentDepth + 1)));
|
|
185
|
+
}
|
|
186
|
+
return currentDepth;
|
|
187
|
+
};
|
|
188
|
+
var countGroups = (condition) => {
|
|
189
|
+
if (import_condition_evaluator.isConditionGroup(condition)) {
|
|
190
|
+
return 1 + condition.conditions.reduce((sum, c) => sum + countGroups(c), 0);
|
|
191
|
+
}
|
|
192
|
+
return 0;
|
|
193
|
+
};
|
|
194
|
+
var collectFields = (condition) => {
|
|
195
|
+
const fields = new Set;
|
|
196
|
+
const traverse = (c) => {
|
|
197
|
+
if (import_condition_evaluator.isConditionGroup(c)) {
|
|
198
|
+
for (const child of c.conditions) {
|
|
199
|
+
traverse(child);
|
|
200
|
+
}
|
|
201
|
+
} else {
|
|
202
|
+
fields.add(c.field);
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
traverse(condition);
|
|
206
|
+
return fields;
|
|
207
|
+
};
|
|
208
|
+
var collectOperators = (condition) => {
|
|
209
|
+
const operators = new Set;
|
|
210
|
+
const traverse = (c) => {
|
|
211
|
+
if (import_condition_evaluator.isConditionGroup(c)) {
|
|
212
|
+
operators.add(c.operator);
|
|
213
|
+
for (const child of c.conditions) {
|
|
214
|
+
traverse(child);
|
|
215
|
+
}
|
|
216
|
+
} else {
|
|
217
|
+
operators.add(c.operator);
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
traverse(condition);
|
|
221
|
+
return operators;
|
|
222
|
+
};
|
|
223
|
+
var calculateComplexityScore = (totalConditions, maxDepth, groupCount, uniqueFields) => {
|
|
224
|
+
return totalConditions * 1 + maxDepth * 2 + groupCount * 1.5 + uniqueFields * 0.5;
|
|
225
|
+
};
|
|
226
|
+
var analyzeRuleComplexity = (rule) => {
|
|
227
|
+
const totalConditions = countConditions(rule.conditions);
|
|
228
|
+
const maxDepth = calculateMaxDepth(rule.conditions);
|
|
229
|
+
const groupCount = countGroups(rule.conditions);
|
|
230
|
+
const uniqueFields = collectFields(rule.conditions).size;
|
|
231
|
+
const uniqueOperators = collectOperators(rule.conditions).size;
|
|
232
|
+
const consequenceCount = rule.consequences.length;
|
|
233
|
+
const complexityScore = calculateComplexityScore(totalConditions, maxDepth, groupCount, uniqueFields);
|
|
234
|
+
return {
|
|
235
|
+
ruleId: rule.id,
|
|
236
|
+
ruleName: rule.name,
|
|
237
|
+
totalConditions,
|
|
238
|
+
maxDepth,
|
|
239
|
+
groupCount,
|
|
240
|
+
uniqueFields,
|
|
241
|
+
uniqueOperators,
|
|
242
|
+
consequenceCount,
|
|
243
|
+
complexityScore
|
|
244
|
+
};
|
|
245
|
+
};
|
|
246
|
+
var analyzeRuleSet = (rules) => {
|
|
247
|
+
const complexities = rules.map(analyzeRuleComplexity);
|
|
248
|
+
const allFields = new Set;
|
|
249
|
+
const allOperators = new Set;
|
|
250
|
+
const allConsequenceTypes = new Set;
|
|
251
|
+
const allCategories = new Set;
|
|
252
|
+
const allTags = new Set;
|
|
253
|
+
let totalConditions = 0;
|
|
254
|
+
let totalConsequences = 0;
|
|
255
|
+
let minPriority = Number.POSITIVE_INFINITY;
|
|
256
|
+
let maxPriority = Number.NEGATIVE_INFINITY;
|
|
257
|
+
let enabledCount = 0;
|
|
258
|
+
for (const rule of rules) {
|
|
259
|
+
totalConditions += countConditions(rule.conditions);
|
|
260
|
+
totalConsequences += rule.consequences.length;
|
|
261
|
+
if (rule.priority < minPriority)
|
|
262
|
+
minPriority = rule.priority;
|
|
263
|
+
if (rule.priority > maxPriority)
|
|
264
|
+
maxPriority = rule.priority;
|
|
265
|
+
if (rule.enabled)
|
|
266
|
+
enabledCount++;
|
|
267
|
+
for (const field of collectFields(rule.conditions)) {
|
|
268
|
+
allFields.add(field);
|
|
269
|
+
}
|
|
270
|
+
for (const operator of collectOperators(rule.conditions)) {
|
|
271
|
+
allOperators.add(operator);
|
|
272
|
+
}
|
|
273
|
+
for (const consequence of rule.consequences) {
|
|
274
|
+
allConsequenceTypes.add(consequence.type);
|
|
275
|
+
}
|
|
276
|
+
if (rule.category)
|
|
277
|
+
allCategories.add(rule.category);
|
|
278
|
+
for (const tag of rule.tags)
|
|
279
|
+
allTags.add(tag);
|
|
280
|
+
}
|
|
281
|
+
const averageComplexity = complexities.length > 0 ? complexities.reduce((sum, c) => sum + c.complexityScore, 0) / complexities.length : 0;
|
|
282
|
+
const complexityDistribution = {
|
|
283
|
+
low: complexities.filter((c) => c.complexityScore < 5).length,
|
|
284
|
+
medium: complexities.filter((c) => c.complexityScore >= 5 && c.complexityScore < 15).length,
|
|
285
|
+
high: complexities.filter((c) => c.complexityScore >= 15).length
|
|
286
|
+
};
|
|
287
|
+
return {
|
|
288
|
+
ruleCount: rules.length,
|
|
289
|
+
enabledCount,
|
|
290
|
+
disabledCount: rules.length - enabledCount,
|
|
291
|
+
totalConditions,
|
|
292
|
+
totalConsequences,
|
|
293
|
+
uniqueFields: [...allFields].sort(),
|
|
294
|
+
uniqueOperators: [...allOperators].sort(),
|
|
295
|
+
uniqueConsequenceTypes: [...allConsequenceTypes].sort(),
|
|
296
|
+
uniqueCategories: [...allCategories].sort(),
|
|
297
|
+
uniqueTags: [...allTags].sort(),
|
|
298
|
+
priorityRange: {
|
|
299
|
+
min: minPriority === Number.POSITIVE_INFINITY ? 0 : minPriority,
|
|
300
|
+
max: maxPriority === Number.NEGATIVE_INFINITY ? 0 : maxPriority
|
|
301
|
+
},
|
|
302
|
+
averageComplexity,
|
|
303
|
+
complexityDistribution,
|
|
304
|
+
ruleComplexities: complexities
|
|
305
|
+
};
|
|
306
|
+
};
|
|
307
|
+
var analyzeFieldUsage = (rules) => {
|
|
308
|
+
const fieldMap = new Map;
|
|
309
|
+
for (const rule of rules) {
|
|
310
|
+
const traverse = (c) => {
|
|
311
|
+
if (import_condition_evaluator.isConditionGroup(c)) {
|
|
312
|
+
for (const child of c.conditions) {
|
|
313
|
+
traverse(child);
|
|
314
|
+
}
|
|
315
|
+
} else {
|
|
316
|
+
const existing = fieldMap.get(c.field) ?? {
|
|
317
|
+
types: new Set,
|
|
318
|
+
operators: new Set,
|
|
319
|
+
rules: []
|
|
320
|
+
};
|
|
321
|
+
existing.types.add(c.type);
|
|
322
|
+
existing.operators.add(c.operator);
|
|
323
|
+
if (!existing.rules.some((r) => r.id === rule.id)) {
|
|
324
|
+
existing.rules.push({ id: rule.id, name: rule.name });
|
|
325
|
+
}
|
|
326
|
+
fieldMap.set(c.field, existing);
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
traverse(rule.conditions);
|
|
330
|
+
}
|
|
331
|
+
return [...fieldMap.entries()].map(([field, data]) => ({
|
|
332
|
+
field,
|
|
333
|
+
count: data.rules.length,
|
|
334
|
+
types: [...data.types].sort(),
|
|
335
|
+
operators: [...data.operators].sort(),
|
|
336
|
+
rules: data.rules
|
|
337
|
+
})).sort((a, b) => b.count - a.count);
|
|
338
|
+
};
|
|
339
|
+
var analyzeOperatorUsage = (rules) => {
|
|
340
|
+
const operatorMap = new Map;
|
|
341
|
+
for (const rule of rules) {
|
|
342
|
+
const traverse = (c) => {
|
|
343
|
+
if (import_condition_evaluator.isConditionGroup(c)) {
|
|
344
|
+
for (const child of c.conditions) {
|
|
345
|
+
traverse(child);
|
|
346
|
+
}
|
|
347
|
+
} else {
|
|
348
|
+
const key = `${c.type}:${c.operator}`;
|
|
349
|
+
const existing = operatorMap.get(key) ?? {
|
|
350
|
+
type: c.type,
|
|
351
|
+
rules: []
|
|
352
|
+
};
|
|
353
|
+
if (!existing.rules.some((r) => r.id === rule.id)) {
|
|
354
|
+
existing.rules.push({ id: rule.id, name: rule.name });
|
|
355
|
+
}
|
|
356
|
+
operatorMap.set(key, existing);
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
traverse(rule.conditions);
|
|
360
|
+
}
|
|
361
|
+
return [...operatorMap.entries()].map(([key, data]) => ({
|
|
362
|
+
operator: key.split(":")[1],
|
|
363
|
+
type: data.type,
|
|
364
|
+
count: data.rules.length,
|
|
365
|
+
rules: data.rules
|
|
366
|
+
})).sort((a, b) => b.count - a.count);
|
|
367
|
+
};
|
|
368
|
+
var analyzeConsequenceUsage = (rules) => {
|
|
369
|
+
const consequenceMap = new Map;
|
|
370
|
+
for (const rule of rules) {
|
|
371
|
+
for (const consequence of rule.consequences) {
|
|
372
|
+
const type = consequence.type;
|
|
373
|
+
const existing = consequenceMap.get(type) ?? [];
|
|
374
|
+
if (!existing.some((r) => r.id === rule.id)) {
|
|
375
|
+
existing.push({ id: rule.id, name: rule.name });
|
|
376
|
+
}
|
|
377
|
+
consequenceMap.set(type, existing);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return [...consequenceMap.entries()].map(([type, rules2]) => ({
|
|
381
|
+
type,
|
|
382
|
+
count: rules2.length,
|
|
383
|
+
rules: rules2
|
|
384
|
+
})).sort((a, b) => b.count - a.count);
|
|
385
|
+
};
|
|
386
|
+
var findMostComplexRules = (rules, limit = 10) => {
|
|
387
|
+
return rules.map(analyzeRuleComplexity).sort((a, b) => b.complexityScore - a.complexityScore).slice(0, limit);
|
|
388
|
+
};
|
|
389
|
+
var findLeastUsedFields = (rules, limit = 10) => {
|
|
390
|
+
return [...analyzeFieldUsage(rules)].sort((a, b) => a.count - b.count).slice(0, limit);
|
|
391
|
+
};
|
|
392
|
+
var formatRuleSetAnalysis = (analysis) => {
|
|
393
|
+
const lines = [
|
|
394
|
+
"=== Rule Set Analysis ===",
|
|
395
|
+
"",
|
|
396
|
+
`Rules: ${analysis.ruleCount} (${analysis.enabledCount} enabled, ${analysis.disabledCount} disabled)`,
|
|
397
|
+
`Total Conditions: ${analysis.totalConditions}`,
|
|
398
|
+
`Total Consequences: ${analysis.totalConsequences}`,
|
|
399
|
+
"",
|
|
400
|
+
`Unique Fields: ${analysis.uniqueFields.length}`,
|
|
401
|
+
` ${analysis.uniqueFields.join(", ") || "(none)"}`,
|
|
402
|
+
"",
|
|
403
|
+
`Unique Operators: ${analysis.uniqueOperators.length}`,
|
|
404
|
+
` ${analysis.uniqueOperators.join(", ") || "(none)"}`,
|
|
405
|
+
"",
|
|
406
|
+
`Consequence Types: ${analysis.uniqueConsequenceTypes.length}`,
|
|
407
|
+
` ${analysis.uniqueConsequenceTypes.join(", ") || "(none)"}`,
|
|
408
|
+
"",
|
|
409
|
+
`Categories: ${analysis.uniqueCategories.length}`,
|
|
410
|
+
` ${analysis.uniqueCategories.join(", ") || "(none)"}`,
|
|
411
|
+
"",
|
|
412
|
+
`Tags: ${analysis.uniqueTags.length}`,
|
|
413
|
+
` ${analysis.uniqueTags.join(", ") || "(none)"}`,
|
|
414
|
+
"",
|
|
415
|
+
`Priority Range: ${analysis.priorityRange.min} - ${analysis.priorityRange.max}`,
|
|
416
|
+
"",
|
|
417
|
+
`Average Complexity: ${analysis.averageComplexity.toFixed(2)}`,
|
|
418
|
+
`Complexity Distribution:`,
|
|
419
|
+
` Low (< 5): ${analysis.complexityDistribution.low}`,
|
|
420
|
+
` Medium (5-15): ${analysis.complexityDistribution.medium}`,
|
|
421
|
+
` High (> 15): ${analysis.complexityDistribution.high}`
|
|
422
|
+
];
|
|
423
|
+
return lines.join(`
|
|
424
|
+
`);
|
|
425
|
+
};
|
|
426
|
+
// src/builder/conditions.ts
|
|
427
|
+
var conditionIdCounter = 0;
|
|
428
|
+
var groupIdCounter = 0;
|
|
429
|
+
var generateConditionId = () => `cond-${++conditionIdCounter}`;
|
|
430
|
+
var generateGroupId = () => `group-${++groupIdCounter}`;
|
|
431
|
+
var resetBuilderIds = () => {
|
|
432
|
+
conditionIdCounter = 0;
|
|
433
|
+
groupIdCounter = 0;
|
|
434
|
+
};
|
|
435
|
+
var createConditionBuilder = (state = { conditions: [] }, operator = "AND") => {
|
|
436
|
+
const addCondition = (condition) => {
|
|
437
|
+
return createConditionBuilder({ conditions: [...state.conditions, condition] }, operator);
|
|
438
|
+
};
|
|
439
|
+
return {
|
|
440
|
+
number: (field, op, value) => addCondition({
|
|
441
|
+
id: generateConditionId(),
|
|
442
|
+
type: "number",
|
|
443
|
+
field,
|
|
444
|
+
operator: op,
|
|
445
|
+
value
|
|
446
|
+
}),
|
|
447
|
+
string: (field, op, value) => addCondition({
|
|
448
|
+
id: generateConditionId(),
|
|
449
|
+
type: "string",
|
|
450
|
+
field,
|
|
451
|
+
operator: op,
|
|
452
|
+
value
|
|
453
|
+
}),
|
|
454
|
+
boolean: (field, op, value) => addCondition({
|
|
455
|
+
id: generateConditionId(),
|
|
456
|
+
type: "boolean",
|
|
457
|
+
field,
|
|
458
|
+
operator: op,
|
|
459
|
+
value
|
|
460
|
+
}),
|
|
461
|
+
date: (field, op, value) => {
|
|
462
|
+
const normalizedValue = value instanceof Date ? value.toISOString() : Array.isArray(value) ? value.map((v) => v instanceof Date ? v.toISOString() : v) : value;
|
|
463
|
+
return addCondition({
|
|
464
|
+
id: generateConditionId(),
|
|
465
|
+
type: "date",
|
|
466
|
+
field,
|
|
467
|
+
operator: op,
|
|
468
|
+
value: normalizedValue
|
|
469
|
+
});
|
|
470
|
+
},
|
|
471
|
+
array: (field, op, value) => addCondition({
|
|
472
|
+
id: generateConditionId(),
|
|
473
|
+
type: "array",
|
|
474
|
+
field,
|
|
475
|
+
operator: op,
|
|
476
|
+
value
|
|
477
|
+
}),
|
|
478
|
+
ref: (field, op, valueRef) => addCondition({
|
|
479
|
+
id: generateConditionId(),
|
|
480
|
+
type: "number",
|
|
481
|
+
field,
|
|
482
|
+
operator: op,
|
|
483
|
+
valueRef
|
|
484
|
+
}),
|
|
485
|
+
and: (builderFn) => {
|
|
486
|
+
const nestedBuilder = createConditionBuilder({ conditions: [] }, "AND");
|
|
487
|
+
const result = builderFn(nestedBuilder);
|
|
488
|
+
return addCondition(result.build());
|
|
489
|
+
},
|
|
490
|
+
or: (builderFn) => {
|
|
491
|
+
const nestedBuilder = createConditionBuilder({ conditions: [] }, "OR");
|
|
492
|
+
const result = builderFn(nestedBuilder);
|
|
493
|
+
return addCondition(result.build());
|
|
494
|
+
},
|
|
495
|
+
raw: (condition) => addCondition(condition),
|
|
496
|
+
build: () => ({
|
|
497
|
+
id: generateGroupId(),
|
|
498
|
+
operator,
|
|
499
|
+
conditions: state.conditions
|
|
500
|
+
}),
|
|
501
|
+
getConditions: () => state.conditions
|
|
502
|
+
};
|
|
503
|
+
};
|
|
504
|
+
var conditions = () => createConditionBuilder({ conditions: [] }, "AND");
|
|
505
|
+
var and = (builderFn) => {
|
|
506
|
+
const builder = createConditionBuilder({ conditions: [] }, "AND");
|
|
507
|
+
return builderFn(builder).build();
|
|
508
|
+
};
|
|
509
|
+
var or = (builderFn) => {
|
|
510
|
+
const builder = createConditionBuilder({ conditions: [] }, "OR");
|
|
511
|
+
return builderFn(builder).build();
|
|
512
|
+
};
|
|
513
|
+
var all = (...items) => ({
|
|
514
|
+
id: generateGroupId(),
|
|
515
|
+
operator: "AND",
|
|
516
|
+
conditions: items
|
|
517
|
+
});
|
|
518
|
+
var any = (...items) => ({
|
|
519
|
+
id: generateGroupId(),
|
|
520
|
+
operator: "OR",
|
|
521
|
+
conditions: items
|
|
522
|
+
});
|
|
523
|
+
var num = (field, operator, value) => ({
|
|
524
|
+
id: generateConditionId(),
|
|
525
|
+
type: "number",
|
|
526
|
+
field,
|
|
527
|
+
operator,
|
|
528
|
+
value
|
|
529
|
+
});
|
|
530
|
+
var str = (field, operator, value) => ({
|
|
531
|
+
id: generateConditionId(),
|
|
532
|
+
type: "string",
|
|
533
|
+
field,
|
|
534
|
+
operator,
|
|
535
|
+
value
|
|
536
|
+
});
|
|
537
|
+
var bool = (field, operator, value) => ({
|
|
538
|
+
id: generateConditionId(),
|
|
539
|
+
type: "boolean",
|
|
540
|
+
field,
|
|
541
|
+
operator,
|
|
542
|
+
value
|
|
543
|
+
});
|
|
544
|
+
var date = (field, operator, value) => {
|
|
545
|
+
const normalizedValue = value instanceof Date ? value.toISOString() : Array.isArray(value) ? value.map((v) => v instanceof Date ? v.toISOString() : v) : value;
|
|
546
|
+
return {
|
|
547
|
+
id: generateConditionId(),
|
|
548
|
+
type: "date",
|
|
549
|
+
field,
|
|
550
|
+
operator,
|
|
551
|
+
value: normalizedValue
|
|
552
|
+
};
|
|
553
|
+
};
|
|
554
|
+
var arr = (field, operator, value) => ({
|
|
555
|
+
id: generateConditionId(),
|
|
556
|
+
type: "array",
|
|
557
|
+
field,
|
|
558
|
+
operator,
|
|
559
|
+
value
|
|
560
|
+
});
|
|
561
|
+
// src/builder/rule.ts
|
|
562
|
+
var createRuleBuilder = (state = {
|
|
563
|
+
consequences: [],
|
|
564
|
+
priority: 0,
|
|
565
|
+
enabled: true,
|
|
566
|
+
stopOnMatch: false,
|
|
567
|
+
tags: []
|
|
568
|
+
}) => {
|
|
569
|
+
const update = (updates) => {
|
|
570
|
+
return createRuleBuilder({ ...state, ...updates });
|
|
571
|
+
};
|
|
572
|
+
return {
|
|
573
|
+
id: (id) => update({ id }),
|
|
574
|
+
named: (name) => update({ name }),
|
|
575
|
+
describedAs: (description) => update({ description }),
|
|
576
|
+
when: (conditionsOrBuilder) => {
|
|
577
|
+
if (typeof conditionsOrBuilder === "function") {
|
|
578
|
+
const builder = conditions();
|
|
579
|
+
const result = conditionsOrBuilder(builder);
|
|
580
|
+
return update({ conditions: result.build() });
|
|
581
|
+
}
|
|
582
|
+
return update({ conditions: conditionsOrBuilder });
|
|
583
|
+
},
|
|
584
|
+
then: (type, payload) => {
|
|
585
|
+
return update({
|
|
586
|
+
consequences: [
|
|
587
|
+
...state.consequences,
|
|
588
|
+
{ type, payload }
|
|
589
|
+
]
|
|
590
|
+
});
|
|
591
|
+
},
|
|
592
|
+
withPriority: (priority) => update({ priority }),
|
|
593
|
+
enabled: (enabled = true) => update({ enabled }),
|
|
594
|
+
disabled: () => update({ enabled: false }),
|
|
595
|
+
stopOnMatch: (stop = true) => update({ stopOnMatch: stop }),
|
|
596
|
+
tagged: (...tags) => update({ tags: [...state.tags, ...tags] }),
|
|
597
|
+
inCategory: (category) => update({ category }),
|
|
598
|
+
withMetadata: (metadata) => update({ metadata: { ...state.metadata, ...metadata } }),
|
|
599
|
+
build: () => {
|
|
600
|
+
if (!state.name) {
|
|
601
|
+
throw new Error("Rule must have a name");
|
|
602
|
+
}
|
|
603
|
+
if (!state.conditions) {
|
|
604
|
+
throw new Error("Rule must have conditions");
|
|
605
|
+
}
|
|
606
|
+
if (state.consequences.length === 0) {
|
|
607
|
+
throw new Error("Rule must have at least one consequence");
|
|
608
|
+
}
|
|
609
|
+
return {
|
|
610
|
+
id: state.id,
|
|
611
|
+
name: state.name,
|
|
612
|
+
description: state.description,
|
|
613
|
+
conditions: state.conditions,
|
|
614
|
+
consequences: state.consequences,
|
|
615
|
+
priority: state.priority,
|
|
616
|
+
enabled: state.enabled,
|
|
617
|
+
stopOnMatch: state.stopOnMatch,
|
|
618
|
+
tags: state.tags,
|
|
619
|
+
category: state.category,
|
|
620
|
+
metadata: state.metadata
|
|
621
|
+
};
|
|
622
|
+
},
|
|
623
|
+
getState: () => state
|
|
624
|
+
};
|
|
625
|
+
};
|
|
626
|
+
var rule = () => {
|
|
627
|
+
return createRuleBuilder();
|
|
628
|
+
};
|
|
629
|
+
var createRule = (builderFn) => {
|
|
630
|
+
const builder = createRuleBuilder();
|
|
631
|
+
return builderFn(builder).build();
|
|
632
|
+
};
|
|
633
|
+
// src/cache/cache.ts
|
|
634
|
+
var createCache = (options) => {
|
|
635
|
+
const entries = new Map;
|
|
636
|
+
let hits = 0;
|
|
637
|
+
let misses = 0;
|
|
638
|
+
let evictions = 0;
|
|
639
|
+
const isExpired = (entry) => {
|
|
640
|
+
return Date.now() > entry.expiresAt;
|
|
641
|
+
};
|
|
642
|
+
const evictExpired = () => {
|
|
643
|
+
const now = Date.now();
|
|
644
|
+
for (const [key, entry] of entries) {
|
|
645
|
+
if (now > entry.expiresAt) {
|
|
646
|
+
entries.delete(key);
|
|
647
|
+
evictions++;
|
|
648
|
+
options.onEvict?.(key, entry.value);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
const evictOldest = () => {
|
|
653
|
+
if (entries.size === 0)
|
|
654
|
+
return;
|
|
655
|
+
let oldestKey;
|
|
656
|
+
let oldestTime = Number.POSITIVE_INFINITY;
|
|
657
|
+
for (const [key, entry] of entries) {
|
|
658
|
+
if (entry.createdAt < oldestTime) {
|
|
659
|
+
oldestTime = entry.createdAt;
|
|
660
|
+
oldestKey = key;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
if (oldestKey) {
|
|
664
|
+
const entry = entries.get(oldestKey);
|
|
665
|
+
entries.delete(oldestKey);
|
|
666
|
+
evictions++;
|
|
667
|
+
if (entry) {
|
|
668
|
+
options.onEvict?.(oldestKey, entry.value);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
};
|
|
672
|
+
const get = (key) => {
|
|
673
|
+
const entry = entries.get(key);
|
|
674
|
+
if (!entry) {
|
|
675
|
+
misses++;
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
if (isExpired(entry)) {
|
|
679
|
+
entries.delete(key);
|
|
680
|
+
evictions++;
|
|
681
|
+
options.onEvict?.(key, entry.value);
|
|
682
|
+
misses++;
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
hits++;
|
|
686
|
+
return entry.value;
|
|
687
|
+
};
|
|
688
|
+
const set = (key, value) => {
|
|
689
|
+
evictExpired();
|
|
690
|
+
while (entries.size >= options.maxSize) {
|
|
691
|
+
evictOldest();
|
|
692
|
+
}
|
|
693
|
+
const now = Date.now();
|
|
694
|
+
entries.set(key, {
|
|
695
|
+
value,
|
|
696
|
+
expiresAt: now + options.ttl,
|
|
697
|
+
createdAt: now
|
|
698
|
+
});
|
|
699
|
+
};
|
|
700
|
+
const has = (key) => {
|
|
701
|
+
const entry = entries.get(key);
|
|
702
|
+
if (!entry)
|
|
703
|
+
return false;
|
|
704
|
+
if (isExpired(entry)) {
|
|
705
|
+
entries.delete(key);
|
|
706
|
+
evictions++;
|
|
707
|
+
options.onEvict?.(key, entry.value);
|
|
708
|
+
return false;
|
|
709
|
+
}
|
|
710
|
+
return true;
|
|
711
|
+
};
|
|
712
|
+
const deleteEntry = (key) => {
|
|
713
|
+
return entries.delete(key);
|
|
714
|
+
};
|
|
715
|
+
const clear = () => {
|
|
716
|
+
entries.clear();
|
|
717
|
+
};
|
|
718
|
+
const getStats = () => {
|
|
719
|
+
const totalRequests = hits + misses;
|
|
720
|
+
return {
|
|
721
|
+
size: entries.size,
|
|
722
|
+
maxSize: options.maxSize,
|
|
723
|
+
hits,
|
|
724
|
+
misses,
|
|
725
|
+
hitRate: totalRequests > 0 ? hits / totalRequests : 0,
|
|
726
|
+
evictions
|
|
727
|
+
};
|
|
728
|
+
};
|
|
729
|
+
return {
|
|
730
|
+
get,
|
|
731
|
+
set,
|
|
732
|
+
has,
|
|
733
|
+
delete: deleteEntry,
|
|
734
|
+
clear,
|
|
735
|
+
getStats
|
|
736
|
+
};
|
|
737
|
+
};
|
|
738
|
+
// src/cache/noop.ts
|
|
739
|
+
var createNoopCache = () => {
|
|
740
|
+
return {
|
|
741
|
+
get: () => {
|
|
742
|
+
return;
|
|
743
|
+
},
|
|
744
|
+
set: () => {},
|
|
745
|
+
has: () => false,
|
|
746
|
+
delete: () => false,
|
|
747
|
+
clear: () => {},
|
|
748
|
+
getStats: () => ({
|
|
749
|
+
size: 0,
|
|
750
|
+
maxSize: 0,
|
|
751
|
+
hits: 0,
|
|
752
|
+
misses: 0,
|
|
753
|
+
hitRate: 0,
|
|
754
|
+
evictions: 0
|
|
755
|
+
})
|
|
756
|
+
};
|
|
757
|
+
};
|
|
758
|
+
// src/core/evaluate.ts
|
|
759
|
+
var import_condition_evaluator2 = require("@f-o-t/condition-evaluator");
|
|
760
|
+
|
|
761
|
+
// src/utils/time.ts
|
|
762
|
+
var measureTime = (fn) => {
|
|
763
|
+
const start = performance.now();
|
|
764
|
+
const result = fn();
|
|
765
|
+
const durationMs = performance.now() - start;
|
|
766
|
+
return { result, durationMs };
|
|
767
|
+
};
|
|
768
|
+
var measureTimeAsync = async (fn) => {
|
|
769
|
+
const start = performance.now();
|
|
770
|
+
const result = await fn();
|
|
771
|
+
const durationMs = performance.now() - start;
|
|
772
|
+
return { result, durationMs };
|
|
773
|
+
};
|
|
774
|
+
var withTimeout = (promise, timeoutMs, errorMessage = "Operation timed out") => {
|
|
775
|
+
return new Promise((resolve, reject) => {
|
|
776
|
+
const timer = setTimeout(() => {
|
|
777
|
+
reject(new Error(errorMessage));
|
|
778
|
+
}, timeoutMs);
|
|
779
|
+
promise.then((result) => {
|
|
780
|
+
clearTimeout(timer);
|
|
781
|
+
resolve(result);
|
|
782
|
+
}).catch((error) => {
|
|
783
|
+
clearTimeout(timer);
|
|
784
|
+
reject(error);
|
|
785
|
+
});
|
|
786
|
+
});
|
|
787
|
+
};
|
|
788
|
+
var delay = (ms) => {
|
|
789
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
790
|
+
};
|
|
791
|
+
|
|
792
|
+
// src/core/evaluate.ts
|
|
793
|
+
var evaluateRule = (rule2, context, options = {}) => {
|
|
794
|
+
if (options.skipDisabled && !rule2.enabled) {
|
|
795
|
+
return {
|
|
796
|
+
ruleId: rule2.id,
|
|
797
|
+
ruleName: rule2.name,
|
|
798
|
+
matched: false,
|
|
799
|
+
conditionResult: createEmptyGroupResult(rule2.conditions.id),
|
|
800
|
+
consequences: [],
|
|
801
|
+
evaluationTimeMs: 0,
|
|
802
|
+
skipped: true,
|
|
803
|
+
skipReason: "Rule is disabled"
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
const { result: conditionResult, durationMs } = measureTime(() => {
|
|
807
|
+
try {
|
|
808
|
+
const evalContext = {
|
|
809
|
+
data: context.data,
|
|
810
|
+
metadata: context.metadata
|
|
811
|
+
};
|
|
812
|
+
return import_condition_evaluator2.evaluateConditionGroup(rule2.conditions, evalContext);
|
|
813
|
+
} catch (error) {
|
|
814
|
+
return {
|
|
815
|
+
error,
|
|
816
|
+
result: createEmptyGroupResult(rule2.conditions.id)
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
});
|
|
820
|
+
if ("error" in conditionResult) {
|
|
821
|
+
return {
|
|
822
|
+
ruleId: rule2.id,
|
|
823
|
+
ruleName: rule2.name,
|
|
824
|
+
matched: false,
|
|
825
|
+
conditionResult: conditionResult.result,
|
|
826
|
+
consequences: [],
|
|
827
|
+
evaluationTimeMs: durationMs,
|
|
828
|
+
skipped: false,
|
|
829
|
+
error: conditionResult.error instanceof Error ? conditionResult.error : new Error(String(conditionResult.error))
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
const matched = conditionResult.passed;
|
|
833
|
+
const consequences = matched ? rule2.consequences.map((consequence) => ({
|
|
834
|
+
type: consequence.type,
|
|
835
|
+
payload: consequence.payload,
|
|
836
|
+
ruleId: rule2.id,
|
|
837
|
+
ruleName: rule2.name,
|
|
838
|
+
priority: rule2.priority
|
|
839
|
+
})) : [];
|
|
840
|
+
return {
|
|
841
|
+
ruleId: rule2.id,
|
|
842
|
+
ruleName: rule2.name,
|
|
843
|
+
matched,
|
|
844
|
+
conditionResult,
|
|
845
|
+
consequences,
|
|
846
|
+
evaluationTimeMs: durationMs,
|
|
847
|
+
skipped: false
|
|
848
|
+
};
|
|
849
|
+
};
|
|
850
|
+
var createEmptyGroupResult = (groupId) => ({
|
|
851
|
+
groupId,
|
|
852
|
+
operator: "AND",
|
|
853
|
+
passed: false,
|
|
854
|
+
results: []
|
|
855
|
+
});
|
|
856
|
+
var evaluateRules = (rules, context, options = {}) => {
|
|
857
|
+
const config = {
|
|
858
|
+
conflictResolution: options.config?.conflictResolution ?? "priority",
|
|
859
|
+
continueOnError: options.config?.continueOnError ?? true,
|
|
860
|
+
collectAllConsequences: options.config?.collectAllConsequences ?? true
|
|
861
|
+
};
|
|
862
|
+
const results = [];
|
|
863
|
+
const matchedRules = [];
|
|
864
|
+
const consequences = [];
|
|
865
|
+
let stoppedEarly = false;
|
|
866
|
+
let stoppedByRuleId;
|
|
867
|
+
for (const rule2 of rules) {
|
|
868
|
+
const result = evaluateRule(rule2, context, { skipDisabled: true });
|
|
869
|
+
results.push(result);
|
|
870
|
+
options.onRuleEvaluated?.(result);
|
|
871
|
+
if (result.error && !config.continueOnError) {
|
|
872
|
+
break;
|
|
873
|
+
}
|
|
874
|
+
if (result.matched) {
|
|
875
|
+
matchedRules.push(rule2);
|
|
876
|
+
consequences.push(...result.consequences);
|
|
877
|
+
if (rule2.stopOnMatch) {
|
|
878
|
+
stoppedEarly = true;
|
|
879
|
+
stoppedByRuleId = rule2.id;
|
|
880
|
+
break;
|
|
881
|
+
}
|
|
882
|
+
if (config.conflictResolution === "first-match") {
|
|
883
|
+
stoppedEarly = true;
|
|
884
|
+
stoppedByRuleId = rule2.id;
|
|
885
|
+
break;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
return {
|
|
890
|
+
results,
|
|
891
|
+
matchedRules,
|
|
892
|
+
consequences,
|
|
893
|
+
stoppedEarly,
|
|
894
|
+
stoppedByRuleId
|
|
895
|
+
};
|
|
896
|
+
};
|
|
897
|
+
// src/core/filter.ts
|
|
898
|
+
var filterRules = (filters) => {
|
|
899
|
+
return (rules) => {
|
|
900
|
+
return rules.filter((rule2) => {
|
|
901
|
+
if (filters.enabled !== undefined && rule2.enabled !== filters.enabled) {
|
|
902
|
+
return false;
|
|
903
|
+
}
|
|
904
|
+
if (filters.tags && filters.tags.length > 0) {
|
|
905
|
+
const hasMatchingTag = filters.tags.some((tag) => rule2.tags.includes(tag));
|
|
906
|
+
if (!hasMatchingTag) {
|
|
907
|
+
return false;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
if (filters.category !== undefined && rule2.category !== filters.category) {
|
|
911
|
+
return false;
|
|
912
|
+
}
|
|
913
|
+
if (filters.ids && filters.ids.length > 0) {
|
|
914
|
+
if (!filters.ids.includes(rule2.id)) {
|
|
915
|
+
return false;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
return true;
|
|
919
|
+
});
|
|
920
|
+
};
|
|
921
|
+
};
|
|
922
|
+
var filterByEnabled = (enabled) => {
|
|
923
|
+
return filterRules({ enabled });
|
|
924
|
+
};
|
|
925
|
+
var filterByTags = (tags) => {
|
|
926
|
+
return filterRules({ tags });
|
|
927
|
+
};
|
|
928
|
+
var filterByCategory = (category) => {
|
|
929
|
+
return filterRules({ category });
|
|
930
|
+
};
|
|
931
|
+
var filterByIds = (ids) => {
|
|
932
|
+
return filterRules({ ids });
|
|
933
|
+
};
|
|
934
|
+
// src/core/group.ts
|
|
935
|
+
var groupRules = (field) => {
|
|
936
|
+
return (rules) => {
|
|
937
|
+
const groups = new Map;
|
|
938
|
+
for (const rule2 of rules) {
|
|
939
|
+
let key;
|
|
940
|
+
switch (field) {
|
|
941
|
+
case "category":
|
|
942
|
+
key = rule2.category ?? "uncategorized";
|
|
943
|
+
break;
|
|
944
|
+
case "priority":
|
|
945
|
+
key = rule2.priority;
|
|
946
|
+
break;
|
|
947
|
+
case "enabled":
|
|
948
|
+
key = rule2.enabled;
|
|
949
|
+
break;
|
|
950
|
+
}
|
|
951
|
+
const existing = groups.get(key);
|
|
952
|
+
if (existing) {
|
|
953
|
+
existing.push(rule2);
|
|
954
|
+
} else {
|
|
955
|
+
groups.set(key, [rule2]);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
return groups;
|
|
959
|
+
};
|
|
960
|
+
};
|
|
961
|
+
var groupByCategory = () => {
|
|
962
|
+
return groupRules("category");
|
|
963
|
+
};
|
|
964
|
+
var groupByPriority = () => {
|
|
965
|
+
return groupRules("priority");
|
|
966
|
+
};
|
|
967
|
+
var groupByEnabled = () => {
|
|
968
|
+
return groupRules("enabled");
|
|
969
|
+
};
|
|
970
|
+
var groupByCustom = (keyFn) => {
|
|
971
|
+
return (rules) => {
|
|
972
|
+
const groups = new Map;
|
|
973
|
+
for (const rule2 of rules) {
|
|
974
|
+
const key = keyFn(rule2);
|
|
975
|
+
const existing = groups.get(key);
|
|
976
|
+
if (existing) {
|
|
977
|
+
existing.push(rule2);
|
|
978
|
+
} else {
|
|
979
|
+
groups.set(key, [rule2]);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
return groups;
|
|
983
|
+
};
|
|
984
|
+
};
|
|
985
|
+
// src/core/sort.ts
|
|
986
|
+
var sortRules = (options) => {
|
|
987
|
+
const normalizedOptions = typeof options === "string" ? { field: options, direction: "desc" } : options;
|
|
988
|
+
const { field, direction = "desc" } = normalizedOptions;
|
|
989
|
+
return (rules) => {
|
|
990
|
+
const sorted = [...rules].sort((a, b) => {
|
|
991
|
+
let comparison = 0;
|
|
992
|
+
switch (field) {
|
|
993
|
+
case "priority":
|
|
994
|
+
comparison = a.priority - b.priority;
|
|
995
|
+
break;
|
|
996
|
+
case "name":
|
|
997
|
+
comparison = a.name.localeCompare(b.name);
|
|
998
|
+
break;
|
|
999
|
+
case "createdAt":
|
|
1000
|
+
comparison = a.createdAt.getTime() - b.createdAt.getTime();
|
|
1001
|
+
break;
|
|
1002
|
+
case "updatedAt":
|
|
1003
|
+
comparison = a.updatedAt.getTime() - b.updatedAt.getTime();
|
|
1004
|
+
break;
|
|
1005
|
+
}
|
|
1006
|
+
return direction === "desc" ? -comparison : comparison;
|
|
1007
|
+
});
|
|
1008
|
+
return sorted;
|
|
1009
|
+
};
|
|
1010
|
+
};
|
|
1011
|
+
var sortByPriority = (direction = "desc") => {
|
|
1012
|
+
return sortRules({ field: "priority", direction });
|
|
1013
|
+
};
|
|
1014
|
+
var sortByName = (direction = "asc") => {
|
|
1015
|
+
return sortRules({ field: "name", direction });
|
|
1016
|
+
};
|
|
1017
|
+
var sortByCreatedAt = (direction = "desc") => {
|
|
1018
|
+
return sortRules({ field: "createdAt", direction });
|
|
1019
|
+
};
|
|
1020
|
+
var sortByUpdatedAt = (direction = "desc") => {
|
|
1021
|
+
return sortRules({ field: "updatedAt", direction });
|
|
1022
|
+
};
|
|
1023
|
+
// src/types/config.ts
|
|
1024
|
+
var DEFAULT_CACHE_CONFIG = {
|
|
1025
|
+
enabled: true,
|
|
1026
|
+
ttl: 60000,
|
|
1027
|
+
maxSize: 1000
|
|
1028
|
+
};
|
|
1029
|
+
var DEFAULT_VALIDATION_CONFIG = {
|
|
1030
|
+
enabled: true,
|
|
1031
|
+
strict: false
|
|
1032
|
+
};
|
|
1033
|
+
var DEFAULT_VERSIONING_CONFIG = {
|
|
1034
|
+
enabled: false,
|
|
1035
|
+
maxVersions: 10
|
|
1036
|
+
};
|
|
1037
|
+
var DEFAULT_ENGINE_CONFIG = {
|
|
1038
|
+
conflictResolution: "priority",
|
|
1039
|
+
cache: DEFAULT_CACHE_CONFIG,
|
|
1040
|
+
validation: DEFAULT_VALIDATION_CONFIG,
|
|
1041
|
+
versioning: DEFAULT_VERSIONING_CONFIG,
|
|
1042
|
+
logLevel: "warn",
|
|
1043
|
+
continueOnError: true,
|
|
1044
|
+
slowRuleThresholdMs: 10
|
|
1045
|
+
};
|
|
1046
|
+
|
|
1047
|
+
// src/types/state.ts
|
|
1048
|
+
var createInitialState = () => ({
|
|
1049
|
+
rules: new Map,
|
|
1050
|
+
ruleSets: new Map,
|
|
1051
|
+
ruleOrder: []
|
|
1052
|
+
});
|
|
1053
|
+
var createInitialOptimizerState = () => ({
|
|
1054
|
+
ruleStats: new Map,
|
|
1055
|
+
lastOptimized: undefined
|
|
1056
|
+
});
|
|
1057
|
+
var createInitialRuleStats = () => ({
|
|
1058
|
+
evaluations: 0,
|
|
1059
|
+
matches: 0,
|
|
1060
|
+
errors: 0,
|
|
1061
|
+
totalTimeMs: 0,
|
|
1062
|
+
avgTimeMs: 0,
|
|
1063
|
+
lastEvaluated: undefined
|
|
1064
|
+
});
|
|
1065
|
+
|
|
1066
|
+
// src/utils/hash.ts
|
|
1067
|
+
var hashContext = (context) => {
|
|
1068
|
+
const str2 = JSON.stringify(context, (_, value) => {
|
|
1069
|
+
if (value instanceof Date) {
|
|
1070
|
+
return value.toISOString();
|
|
1071
|
+
}
|
|
1072
|
+
if (value instanceof Map) {
|
|
1073
|
+
return Object.fromEntries(value);
|
|
1074
|
+
}
|
|
1075
|
+
if (value instanceof Set) {
|
|
1076
|
+
return Array.from(value);
|
|
1077
|
+
}
|
|
1078
|
+
return value;
|
|
1079
|
+
});
|
|
1080
|
+
if (typeof Bun !== "undefined" && Bun.hash) {
|
|
1081
|
+
return Bun.hash(str2).toString(16);
|
|
1082
|
+
}
|
|
1083
|
+
let hash = 0;
|
|
1084
|
+
for (let i = 0;i < str2.length; i++) {
|
|
1085
|
+
const char = str2.charCodeAt(i);
|
|
1086
|
+
hash = (hash << 5) - hash + char;
|
|
1087
|
+
hash = hash & hash;
|
|
1088
|
+
}
|
|
1089
|
+
return Math.abs(hash).toString(16);
|
|
1090
|
+
};
|
|
1091
|
+
var hashRules = (ruleIds) => {
|
|
1092
|
+
return hashContext(ruleIds.slice().sort());
|
|
1093
|
+
};
|
|
1094
|
+
|
|
1095
|
+
// src/utils/id.ts
|
|
1096
|
+
var generateId = () => {
|
|
1097
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
1098
|
+
return crypto.randomUUID();
|
|
1099
|
+
}
|
|
1100
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).substring(2, 11)}`;
|
|
1101
|
+
};
|
|
1102
|
+
|
|
1103
|
+
// src/engine/hooks.ts
|
|
1104
|
+
var executeBeforeEvaluation = async (hooks, context, rules) => {
|
|
1105
|
+
if (!hooks.beforeEvaluation)
|
|
1106
|
+
return;
|
|
1107
|
+
try {
|
|
1108
|
+
await hooks.beforeEvaluation(context, rules);
|
|
1109
|
+
} catch {}
|
|
1110
|
+
};
|
|
1111
|
+
var executeAfterEvaluation = async (hooks, result) => {
|
|
1112
|
+
if (!hooks.afterEvaluation)
|
|
1113
|
+
return;
|
|
1114
|
+
try {
|
|
1115
|
+
await hooks.afterEvaluation(result);
|
|
1116
|
+
} catch {}
|
|
1117
|
+
};
|
|
1118
|
+
var executeBeforeRuleEvaluation = async (hooks, rule2, context) => {
|
|
1119
|
+
if (!hooks.beforeRuleEvaluation)
|
|
1120
|
+
return;
|
|
1121
|
+
try {
|
|
1122
|
+
await hooks.beforeRuleEvaluation(rule2, context);
|
|
1123
|
+
} catch {}
|
|
1124
|
+
};
|
|
1125
|
+
var executeAfterRuleEvaluation = async (hooks, rule2, result) => {
|
|
1126
|
+
if (!hooks.afterRuleEvaluation)
|
|
1127
|
+
return;
|
|
1128
|
+
try {
|
|
1129
|
+
await hooks.afterRuleEvaluation(rule2, result);
|
|
1130
|
+
} catch {}
|
|
1131
|
+
};
|
|
1132
|
+
var executeOnRuleMatch = async (hooks, rule2, context) => {
|
|
1133
|
+
if (!hooks.onRuleMatch)
|
|
1134
|
+
return;
|
|
1135
|
+
try {
|
|
1136
|
+
await hooks.onRuleMatch(rule2, context);
|
|
1137
|
+
} catch {}
|
|
1138
|
+
};
|
|
1139
|
+
var executeOnRuleSkip = async (hooks, rule2, reason) => {
|
|
1140
|
+
if (!hooks.onRuleSkip)
|
|
1141
|
+
return;
|
|
1142
|
+
try {
|
|
1143
|
+
await hooks.onRuleSkip(rule2, reason);
|
|
1144
|
+
} catch {}
|
|
1145
|
+
};
|
|
1146
|
+
var executeOnRuleError = async (hooks, rule2, error) => {
|
|
1147
|
+
if (!hooks.onRuleError)
|
|
1148
|
+
return;
|
|
1149
|
+
try {
|
|
1150
|
+
await hooks.onRuleError(rule2, error);
|
|
1151
|
+
} catch {}
|
|
1152
|
+
};
|
|
1153
|
+
var executeOnConsequenceCollected = async (hooks, rule2, consequence) => {
|
|
1154
|
+
if (!hooks.onConsequenceCollected)
|
|
1155
|
+
return;
|
|
1156
|
+
try {
|
|
1157
|
+
await hooks.onConsequenceCollected(rule2, consequence);
|
|
1158
|
+
} catch {}
|
|
1159
|
+
};
|
|
1160
|
+
var executeOnCacheHit = async (hooks, key, result) => {
|
|
1161
|
+
if (!hooks.onCacheHit)
|
|
1162
|
+
return;
|
|
1163
|
+
try {
|
|
1164
|
+
await hooks.onCacheHit(key, result);
|
|
1165
|
+
} catch {}
|
|
1166
|
+
};
|
|
1167
|
+
var executeOnCacheMiss = async (hooks, key) => {
|
|
1168
|
+
if (!hooks.onCacheMiss)
|
|
1169
|
+
return;
|
|
1170
|
+
try {
|
|
1171
|
+
await hooks.onCacheMiss(key);
|
|
1172
|
+
} catch {}
|
|
1173
|
+
};
|
|
1174
|
+
var executeOnSlowRule = async (hooks, rule2, timeMs, threshold) => {
|
|
1175
|
+
if (!hooks.onSlowRule)
|
|
1176
|
+
return;
|
|
1177
|
+
try {
|
|
1178
|
+
await hooks.onSlowRule(rule2, timeMs, threshold);
|
|
1179
|
+
} catch {}
|
|
1180
|
+
};
|
|
1181
|
+
|
|
1182
|
+
// src/engine/state.ts
|
|
1183
|
+
var addRule = (state, input) => {
|
|
1184
|
+
const now = new Date;
|
|
1185
|
+
const rule2 = {
|
|
1186
|
+
id: input.id ?? generateId(),
|
|
1187
|
+
name: input.name,
|
|
1188
|
+
description: input.description,
|
|
1189
|
+
conditions: input.conditions,
|
|
1190
|
+
consequences: input.consequences.map((c) => ({
|
|
1191
|
+
type: c.type,
|
|
1192
|
+
payload: c.payload
|
|
1193
|
+
})),
|
|
1194
|
+
priority: input.priority ?? 0,
|
|
1195
|
+
enabled: input.enabled ?? true,
|
|
1196
|
+
stopOnMatch: input.stopOnMatch ?? false,
|
|
1197
|
+
tags: input.tags ?? [],
|
|
1198
|
+
category: input.category,
|
|
1199
|
+
metadata: input.metadata,
|
|
1200
|
+
createdAt: now,
|
|
1201
|
+
updatedAt: now
|
|
1202
|
+
};
|
|
1203
|
+
state.rules.set(rule2.id, rule2);
|
|
1204
|
+
if (!state.ruleOrder.includes(rule2.id)) {
|
|
1205
|
+
state.ruleOrder.push(rule2.id);
|
|
1206
|
+
sortRuleOrder(state);
|
|
1207
|
+
}
|
|
1208
|
+
return rule2;
|
|
1209
|
+
};
|
|
1210
|
+
var addRules = (state, inputs) => {
|
|
1211
|
+
return inputs.map((input) => addRule(state, input));
|
|
1212
|
+
};
|
|
1213
|
+
var removeRule = (state, ruleId) => {
|
|
1214
|
+
const deleted = state.rules.delete(ruleId);
|
|
1215
|
+
if (deleted) {
|
|
1216
|
+
const index = state.ruleOrder.indexOf(ruleId);
|
|
1217
|
+
if (index !== -1) {
|
|
1218
|
+
state.ruleOrder.splice(index, 1);
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
return deleted;
|
|
1222
|
+
};
|
|
1223
|
+
var updateRule = (state, ruleId, updates) => {
|
|
1224
|
+
const existing = state.rules.get(ruleId);
|
|
1225
|
+
if (!existing)
|
|
1226
|
+
return;
|
|
1227
|
+
const updated = {
|
|
1228
|
+
...existing,
|
|
1229
|
+
...updates.name !== undefined && { name: updates.name },
|
|
1230
|
+
...updates.description !== undefined && {
|
|
1231
|
+
description: updates.description
|
|
1232
|
+
},
|
|
1233
|
+
...updates.conditions !== undefined && {
|
|
1234
|
+
conditions: updates.conditions
|
|
1235
|
+
},
|
|
1236
|
+
...updates.consequences !== undefined && {
|
|
1237
|
+
consequences: updates.consequences.map((c) => ({
|
|
1238
|
+
type: c.type,
|
|
1239
|
+
payload: c.payload
|
|
1240
|
+
}))
|
|
1241
|
+
},
|
|
1242
|
+
...updates.priority !== undefined && { priority: updates.priority },
|
|
1243
|
+
...updates.enabled !== undefined && { enabled: updates.enabled },
|
|
1244
|
+
...updates.stopOnMatch !== undefined && {
|
|
1245
|
+
stopOnMatch: updates.stopOnMatch
|
|
1246
|
+
},
|
|
1247
|
+
...updates.tags !== undefined && { tags: updates.tags },
|
|
1248
|
+
...updates.category !== undefined && { category: updates.category },
|
|
1249
|
+
...updates.metadata !== undefined && { metadata: updates.metadata },
|
|
1250
|
+
updatedAt: new Date
|
|
1251
|
+
};
|
|
1252
|
+
state.rules.set(ruleId, updated);
|
|
1253
|
+
if (updates.priority !== undefined) {
|
|
1254
|
+
sortRuleOrder(state);
|
|
1255
|
+
}
|
|
1256
|
+
return updated;
|
|
1257
|
+
};
|
|
1258
|
+
var getRule = (state, ruleId) => {
|
|
1259
|
+
return state.rules.get(ruleId);
|
|
1260
|
+
};
|
|
1261
|
+
var getRules = (state, filters) => {
|
|
1262
|
+
const rules = [];
|
|
1263
|
+
for (const id of state.ruleOrder) {
|
|
1264
|
+
const rule2 = state.rules.get(id);
|
|
1265
|
+
if (!rule2)
|
|
1266
|
+
continue;
|
|
1267
|
+
if (filters) {
|
|
1268
|
+
if (filters.enabled !== undefined && rule2.enabled !== filters.enabled) {
|
|
1269
|
+
continue;
|
|
1270
|
+
}
|
|
1271
|
+
if (filters.tags && filters.tags.length > 0) {
|
|
1272
|
+
const hasTag = filters.tags.some((tag) => rule2.tags.includes(tag));
|
|
1273
|
+
if (!hasTag)
|
|
1274
|
+
continue;
|
|
1275
|
+
}
|
|
1276
|
+
if (filters.category !== undefined && rule2.category !== filters.category) {
|
|
1277
|
+
continue;
|
|
1278
|
+
}
|
|
1279
|
+
if (filters.ids && filters.ids.length > 0 && !filters.ids.includes(rule2.id)) {
|
|
1280
|
+
continue;
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
rules.push(rule2);
|
|
1284
|
+
}
|
|
1285
|
+
return rules;
|
|
1286
|
+
};
|
|
1287
|
+
var enableRule = (state, ruleId) => {
|
|
1288
|
+
const rule2 = state.rules.get(ruleId);
|
|
1289
|
+
if (!rule2)
|
|
1290
|
+
return false;
|
|
1291
|
+
state.rules.set(ruleId, { ...rule2, enabled: true, updatedAt: new Date });
|
|
1292
|
+
return true;
|
|
1293
|
+
};
|
|
1294
|
+
var disableRule = (state, ruleId) => {
|
|
1295
|
+
const rule2 = state.rules.get(ruleId);
|
|
1296
|
+
if (!rule2)
|
|
1297
|
+
return false;
|
|
1298
|
+
state.rules.set(ruleId, { ...rule2, enabled: false, updatedAt: new Date });
|
|
1299
|
+
return true;
|
|
1300
|
+
};
|
|
1301
|
+
var clearRules = (state) => {
|
|
1302
|
+
state.rules.clear();
|
|
1303
|
+
state.ruleOrder.length = 0;
|
|
1304
|
+
};
|
|
1305
|
+
var addRuleSet = (state, input) => {
|
|
1306
|
+
const ruleSet = {
|
|
1307
|
+
id: input.id ?? generateId(),
|
|
1308
|
+
name: input.name,
|
|
1309
|
+
description: input.description,
|
|
1310
|
+
ruleIds: input.ruleIds,
|
|
1311
|
+
enabled: input.enabled ?? true,
|
|
1312
|
+
metadata: input.metadata
|
|
1313
|
+
};
|
|
1314
|
+
state.ruleSets.set(ruleSet.id, ruleSet);
|
|
1315
|
+
return ruleSet;
|
|
1316
|
+
};
|
|
1317
|
+
var getRuleSet = (state, ruleSetId) => {
|
|
1318
|
+
return state.ruleSets.get(ruleSetId);
|
|
1319
|
+
};
|
|
1320
|
+
var getRuleSets = (state) => {
|
|
1321
|
+
return Array.from(state.ruleSets.values());
|
|
1322
|
+
};
|
|
1323
|
+
var removeRuleSet = (state, ruleSetId) => {
|
|
1324
|
+
return state.ruleSets.delete(ruleSetId);
|
|
1325
|
+
};
|
|
1326
|
+
var getRulesInSet = (state, ruleSetId) => {
|
|
1327
|
+
const ruleSet = state.ruleSets.get(ruleSetId);
|
|
1328
|
+
if (!ruleSet || !ruleSet.enabled)
|
|
1329
|
+
return [];
|
|
1330
|
+
const rules = [];
|
|
1331
|
+
for (const ruleId of ruleSet.ruleIds) {
|
|
1332
|
+
const rule2 = state.rules.get(ruleId);
|
|
1333
|
+
if (rule2) {
|
|
1334
|
+
rules.push(rule2);
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
return rules;
|
|
1338
|
+
};
|
|
1339
|
+
var sortRuleOrder = (state) => {
|
|
1340
|
+
state.ruleOrder.sort((a, b) => {
|
|
1341
|
+
const ruleA = state.rules.get(a);
|
|
1342
|
+
const ruleB = state.rules.get(b);
|
|
1343
|
+
if (!ruleA || !ruleB)
|
|
1344
|
+
return 0;
|
|
1345
|
+
return ruleB.priority - ruleA.priority;
|
|
1346
|
+
});
|
|
1347
|
+
};
|
|
1348
|
+
var cloneState = (state) => {
|
|
1349
|
+
const newState = createInitialState();
|
|
1350
|
+
for (const [id, rule2] of state.rules) {
|
|
1351
|
+
newState.rules.set(id, { ...rule2 });
|
|
1352
|
+
}
|
|
1353
|
+
for (const [id, ruleSet] of state.ruleSets) {
|
|
1354
|
+
newState.ruleSets.set(id, { ...ruleSet, ruleIds: [...ruleSet.ruleIds] });
|
|
1355
|
+
}
|
|
1356
|
+
newState.ruleOrder.push(...state.ruleOrder);
|
|
1357
|
+
return newState;
|
|
1358
|
+
};
|
|
1359
|
+
|
|
1360
|
+
// src/engine/engine.ts
|
|
1361
|
+
var resolveConfig = (config) => {
|
|
1362
|
+
return {
|
|
1363
|
+
consequences: config.consequences,
|
|
1364
|
+
conflictResolution: config.conflictResolution ?? DEFAULT_ENGINE_CONFIG.conflictResolution,
|
|
1365
|
+
cache: {
|
|
1366
|
+
...DEFAULT_CACHE_CONFIG,
|
|
1367
|
+
...config.cache
|
|
1368
|
+
},
|
|
1369
|
+
validation: {
|
|
1370
|
+
...DEFAULT_VALIDATION_CONFIG,
|
|
1371
|
+
...config.validation
|
|
1372
|
+
},
|
|
1373
|
+
versioning: {
|
|
1374
|
+
...DEFAULT_VERSIONING_CONFIG,
|
|
1375
|
+
...config.versioning
|
|
1376
|
+
},
|
|
1377
|
+
hooks: config.hooks ?? {},
|
|
1378
|
+
logLevel: config.logLevel ?? DEFAULT_ENGINE_CONFIG.logLevel,
|
|
1379
|
+
logger: config.logger ?? console,
|
|
1380
|
+
continueOnError: config.continueOnError ?? DEFAULT_ENGINE_CONFIG.continueOnError,
|
|
1381
|
+
slowRuleThresholdMs: config.slowRuleThresholdMs ?? DEFAULT_ENGINE_CONFIG.slowRuleThresholdMs
|
|
1382
|
+
};
|
|
1383
|
+
};
|
|
1384
|
+
var createEngine = (config = {}) => {
|
|
1385
|
+
const resolvedConfig = resolveConfig(config);
|
|
1386
|
+
const state = createInitialState();
|
|
1387
|
+
const cache = resolvedConfig.cache.enabled ? createCache({
|
|
1388
|
+
ttl: resolvedConfig.cache.ttl,
|
|
1389
|
+
maxSize: resolvedConfig.cache.maxSize
|
|
1390
|
+
}) : createNoopCache();
|
|
1391
|
+
let totalEvaluations = 0;
|
|
1392
|
+
let totalMatches = 0;
|
|
1393
|
+
let totalErrors = 0;
|
|
1394
|
+
let cacheHits = 0;
|
|
1395
|
+
let cacheMisses = 0;
|
|
1396
|
+
let totalEvaluationTime = 0;
|
|
1397
|
+
const evaluate = async (contextData, options = {}) => {
|
|
1398
|
+
const context = {
|
|
1399
|
+
data: contextData,
|
|
1400
|
+
timestamp: new Date,
|
|
1401
|
+
correlationId: generateId()
|
|
1402
|
+
};
|
|
1403
|
+
let rulesToEvaluate = getRules(state, {
|
|
1404
|
+
enabled: options.skipDisabled !== false ? true : undefined,
|
|
1405
|
+
tags: options.tags,
|
|
1406
|
+
category: options.category
|
|
1407
|
+
});
|
|
1408
|
+
if (options.ruleSetId) {
|
|
1409
|
+
const ruleSetRules = getRulesInSet(state, options.ruleSetId);
|
|
1410
|
+
const ruleSetIds = new Set(ruleSetRules.map((r) => r.id));
|
|
1411
|
+
rulesToEvaluate = rulesToEvaluate.filter((r) => ruleSetIds.has(r.id));
|
|
1412
|
+
}
|
|
1413
|
+
rulesToEvaluate = [
|
|
1414
|
+
...sortRules({
|
|
1415
|
+
field: "priority",
|
|
1416
|
+
direction: "desc"
|
|
1417
|
+
})(rulesToEvaluate)
|
|
1418
|
+
];
|
|
1419
|
+
if (options.maxRules && options.maxRules > 0) {
|
|
1420
|
+
rulesToEvaluate = rulesToEvaluate.slice(0, options.maxRules);
|
|
1421
|
+
}
|
|
1422
|
+
const cacheKey = !options.bypassCache ? `${hashContext(contextData)}:${hashRules(rulesToEvaluate.map((r) => r.id))}` : null;
|
|
1423
|
+
if (cacheKey && cache.has(cacheKey)) {
|
|
1424
|
+
const cached = cache.get(cacheKey);
|
|
1425
|
+
if (cached) {
|
|
1426
|
+
cacheHits++;
|
|
1427
|
+
await executeOnCacheHit(resolvedConfig.hooks, cacheKey, cached);
|
|
1428
|
+
return { ...cached, cacheHit: true };
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
if (cacheKey) {
|
|
1432
|
+
cacheMisses++;
|
|
1433
|
+
await executeOnCacheMiss(resolvedConfig.hooks, cacheKey);
|
|
1434
|
+
}
|
|
1435
|
+
await executeBeforeEvaluation(resolvedConfig.hooks, context, rulesToEvaluate);
|
|
1436
|
+
const { result: evaluationResult, durationMs } = measureTime(() => {
|
|
1437
|
+
const results = [];
|
|
1438
|
+
for (const rule2 of rulesToEvaluate) {
|
|
1439
|
+
const result = evaluateRule(rule2, context, { skipDisabled: true });
|
|
1440
|
+
results.push({ rule: rule2, result });
|
|
1441
|
+
}
|
|
1442
|
+
return results;
|
|
1443
|
+
});
|
|
1444
|
+
const ruleResults = [];
|
|
1445
|
+
const matchedRules = [];
|
|
1446
|
+
const consequences = [];
|
|
1447
|
+
let stoppedEarly = false;
|
|
1448
|
+
let stoppedByRuleId;
|
|
1449
|
+
let rulesErrored = 0;
|
|
1450
|
+
const conflictResolution = options.conflictResolution ?? resolvedConfig.conflictResolution;
|
|
1451
|
+
for (const { rule: rule2, result } of evaluationResult) {
|
|
1452
|
+
await executeBeforeRuleEvaluation(resolvedConfig.hooks, rule2, context);
|
|
1453
|
+
ruleResults.push(result);
|
|
1454
|
+
if (result.error) {
|
|
1455
|
+
rulesErrored++;
|
|
1456
|
+
await executeOnRuleError(resolvedConfig.hooks, rule2, result.error);
|
|
1457
|
+
if (!resolvedConfig.continueOnError) {
|
|
1458
|
+
break;
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
if (result.skipped) {
|
|
1462
|
+
await executeOnRuleSkip(resolvedConfig.hooks, rule2, result.skipReason ?? "Unknown");
|
|
1463
|
+
}
|
|
1464
|
+
if (result.evaluationTimeMs > resolvedConfig.slowRuleThresholdMs) {
|
|
1465
|
+
await executeOnSlowRule(resolvedConfig.hooks, rule2, result.evaluationTimeMs, resolvedConfig.slowRuleThresholdMs);
|
|
1466
|
+
}
|
|
1467
|
+
await executeAfterRuleEvaluation(resolvedConfig.hooks, rule2, result);
|
|
1468
|
+
if (result.matched) {
|
|
1469
|
+
matchedRules.push(rule2);
|
|
1470
|
+
for (const consequence of result.consequences) {
|
|
1471
|
+
consequences.push(consequence);
|
|
1472
|
+
await executeOnConsequenceCollected(resolvedConfig.hooks, rule2, consequence);
|
|
1473
|
+
}
|
|
1474
|
+
await executeOnRuleMatch(resolvedConfig.hooks, rule2, context);
|
|
1475
|
+
if (rule2.stopOnMatch) {
|
|
1476
|
+
stoppedEarly = true;
|
|
1477
|
+
stoppedByRuleId = rule2.id;
|
|
1478
|
+
break;
|
|
1479
|
+
}
|
|
1480
|
+
if (conflictResolution === "first-match") {
|
|
1481
|
+
stoppedEarly = true;
|
|
1482
|
+
stoppedByRuleId = rule2.id;
|
|
1483
|
+
break;
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
totalEvaluations++;
|
|
1488
|
+
totalMatches += matchedRules.length;
|
|
1489
|
+
totalErrors += rulesErrored;
|
|
1490
|
+
totalEvaluationTime += durationMs;
|
|
1491
|
+
const executionResult = {
|
|
1492
|
+
context,
|
|
1493
|
+
results: ruleResults,
|
|
1494
|
+
matchedRules,
|
|
1495
|
+
consequences,
|
|
1496
|
+
totalRulesEvaluated: ruleResults.length,
|
|
1497
|
+
totalRulesMatched: matchedRules.length,
|
|
1498
|
+
totalRulesSkipped: ruleResults.filter((r) => r.skipped).length,
|
|
1499
|
+
totalRulesErrored: rulesErrored,
|
|
1500
|
+
executionTimeMs: durationMs,
|
|
1501
|
+
stoppedEarly,
|
|
1502
|
+
stoppedByRuleId,
|
|
1503
|
+
cacheHit: false
|
|
1504
|
+
};
|
|
1505
|
+
if (cacheKey) {
|
|
1506
|
+
cache.set(cacheKey, executionResult);
|
|
1507
|
+
}
|
|
1508
|
+
await executeAfterEvaluation(resolvedConfig.hooks, executionResult);
|
|
1509
|
+
return executionResult;
|
|
1510
|
+
};
|
|
1511
|
+
return {
|
|
1512
|
+
addRule: (input) => addRule(state, input),
|
|
1513
|
+
addRules: (inputs) => addRules(state, inputs),
|
|
1514
|
+
removeRule: (ruleId) => {
|
|
1515
|
+
const result = removeRule(state, ruleId);
|
|
1516
|
+
if (result)
|
|
1517
|
+
cache.clear();
|
|
1518
|
+
return result;
|
|
1519
|
+
},
|
|
1520
|
+
updateRule: (ruleId, updates) => {
|
|
1521
|
+
const result = updateRule(state, ruleId, updates);
|
|
1522
|
+
if (result)
|
|
1523
|
+
cache.clear();
|
|
1524
|
+
return result;
|
|
1525
|
+
},
|
|
1526
|
+
getRule: (ruleId) => getRule(state, ruleId),
|
|
1527
|
+
getRules: (filters) => getRules(state, filters),
|
|
1528
|
+
enableRule: (ruleId) => {
|
|
1529
|
+
const result = enableRule(state, ruleId);
|
|
1530
|
+
if (result)
|
|
1531
|
+
cache.clear();
|
|
1532
|
+
return result;
|
|
1533
|
+
},
|
|
1534
|
+
disableRule: (ruleId) => {
|
|
1535
|
+
const result = disableRule(state, ruleId);
|
|
1536
|
+
if (result)
|
|
1537
|
+
cache.clear();
|
|
1538
|
+
return result;
|
|
1539
|
+
},
|
|
1540
|
+
clearRules: () => {
|
|
1541
|
+
clearRules(state);
|
|
1542
|
+
cache.clear();
|
|
1543
|
+
},
|
|
1544
|
+
addRuleSet: (input) => addRuleSet(state, input),
|
|
1545
|
+
getRuleSet: (ruleSetId) => getRuleSet(state, ruleSetId),
|
|
1546
|
+
getRuleSets: () => getRuleSets(state),
|
|
1547
|
+
removeRuleSet: (ruleSetId) => removeRuleSet(state, ruleSetId),
|
|
1548
|
+
evaluate,
|
|
1549
|
+
clearCache: () => cache.clear(),
|
|
1550
|
+
getCacheStats: () => cache.getStats(),
|
|
1551
|
+
getState: () => ({
|
|
1552
|
+
rules: state.rules,
|
|
1553
|
+
ruleSets: state.ruleSets,
|
|
1554
|
+
ruleOrder: state.ruleOrder
|
|
1555
|
+
}),
|
|
1556
|
+
getStats: () => {
|
|
1557
|
+
const enabledRules = Array.from(state.rules.values()).filter((r) => r.enabled).length;
|
|
1558
|
+
const cacheStats = cache.getStats();
|
|
1559
|
+
return {
|
|
1560
|
+
totalRules: state.rules.size,
|
|
1561
|
+
enabledRules,
|
|
1562
|
+
disabledRules: state.rules.size - enabledRules,
|
|
1563
|
+
totalRuleSets: state.ruleSets.size,
|
|
1564
|
+
totalEvaluations,
|
|
1565
|
+
totalMatches,
|
|
1566
|
+
totalErrors,
|
|
1567
|
+
avgEvaluationTimeMs: totalEvaluations > 0 ? totalEvaluationTime / totalEvaluations : 0,
|
|
1568
|
+
cacheHits,
|
|
1569
|
+
cacheMisses,
|
|
1570
|
+
cacheHitRate: cacheHits + cacheMisses > 0 ? cacheHits / (cacheHits + cacheMisses) : 0
|
|
1571
|
+
};
|
|
1572
|
+
}
|
|
1573
|
+
};
|
|
1574
|
+
};
|
|
1575
|
+
// src/optimizer/index-builder.ts
|
|
1576
|
+
var import_condition_evaluator3 = require("@f-o-t/condition-evaluator");
|
|
1577
|
+
var DEFAULT_OPTIONS = {
|
|
1578
|
+
indexByField: true,
|
|
1579
|
+
indexByTag: true,
|
|
1580
|
+
indexByCategory: true,
|
|
1581
|
+
indexByPriority: true
|
|
1582
|
+
};
|
|
1583
|
+
var collectFields2 = (condition) => {
|
|
1584
|
+
const fields = new Set;
|
|
1585
|
+
const traverse = (c) => {
|
|
1586
|
+
if (import_condition_evaluator3.isConditionGroup(c)) {
|
|
1587
|
+
for (const child of c.conditions) {
|
|
1588
|
+
traverse(child);
|
|
1589
|
+
}
|
|
1590
|
+
} else {
|
|
1591
|
+
fields.add(c.field);
|
|
1592
|
+
}
|
|
1593
|
+
};
|
|
1594
|
+
traverse(condition);
|
|
1595
|
+
return fields;
|
|
1596
|
+
};
|
|
1597
|
+
var buildIndex = (rules, options = {}) => {
|
|
1598
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
1599
|
+
const byId = new Map;
|
|
1600
|
+
const byField = new Map;
|
|
1601
|
+
const byTag = new Map;
|
|
1602
|
+
const byCategory = new Map;
|
|
1603
|
+
const byPriority = new Map;
|
|
1604
|
+
for (const rule2 of rules) {
|
|
1605
|
+
byId.set(rule2.id, rule2);
|
|
1606
|
+
if (opts.indexByField) {
|
|
1607
|
+
const fields = collectFields2(rule2.conditions);
|
|
1608
|
+
for (const field of fields) {
|
|
1609
|
+
const existing = byField.get(field) ?? [];
|
|
1610
|
+
existing.push(rule2);
|
|
1611
|
+
byField.set(field, existing);
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
if (opts.indexByTag) {
|
|
1615
|
+
for (const tag of rule2.tags) {
|
|
1616
|
+
const existing = byTag.get(tag) ?? [];
|
|
1617
|
+
existing.push(rule2);
|
|
1618
|
+
byTag.set(tag, existing);
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
if (opts.indexByCategory && rule2.category) {
|
|
1622
|
+
const existing = byCategory.get(rule2.category) ?? [];
|
|
1623
|
+
existing.push(rule2);
|
|
1624
|
+
byCategory.set(rule2.category, existing);
|
|
1625
|
+
}
|
|
1626
|
+
if (opts.indexByPriority) {
|
|
1627
|
+
const existing = byPriority.get(rule2.priority) ?? [];
|
|
1628
|
+
existing.push(rule2);
|
|
1629
|
+
byPriority.set(rule2.priority, existing);
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
const sortedByPriority = [...rules].sort((a, b) => b.priority - a.priority);
|
|
1633
|
+
return {
|
|
1634
|
+
byField,
|
|
1635
|
+
byTag,
|
|
1636
|
+
byCategory,
|
|
1637
|
+
byPriority,
|
|
1638
|
+
byId,
|
|
1639
|
+
sortedByPriority
|
|
1640
|
+
};
|
|
1641
|
+
};
|
|
1642
|
+
var getRulesByField = (index, field) => {
|
|
1643
|
+
return index.byField.get(field) ?? [];
|
|
1644
|
+
};
|
|
1645
|
+
var getRulesByFields = (index, fields, mode = "any") => {
|
|
1646
|
+
if (fields.length === 0)
|
|
1647
|
+
return [];
|
|
1648
|
+
const ruleSets = fields.map((f) => new Set(getRulesByField(index, f)));
|
|
1649
|
+
if (mode === "any") {
|
|
1650
|
+
const combined = new Set;
|
|
1651
|
+
for (const ruleSet of ruleSets) {
|
|
1652
|
+
for (const rule2 of ruleSet) {
|
|
1653
|
+
combined.add(rule2);
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
return [...combined];
|
|
1657
|
+
}
|
|
1658
|
+
const [first, ...rest] = ruleSets;
|
|
1659
|
+
if (!first)
|
|
1660
|
+
return [];
|
|
1661
|
+
return [...first].filter((rule2) => rest.every((set) => set.has(rule2)));
|
|
1662
|
+
};
|
|
1663
|
+
var getRulesByTag = (index, tag) => {
|
|
1664
|
+
return index.byTag.get(tag) ?? [];
|
|
1665
|
+
};
|
|
1666
|
+
var getRulesByTags = (index, tags, mode = "any") => {
|
|
1667
|
+
if (tags.length === 0)
|
|
1668
|
+
return [];
|
|
1669
|
+
const ruleSets = tags.map((t) => new Set(getRulesByTag(index, t)));
|
|
1670
|
+
if (mode === "any") {
|
|
1671
|
+
const combined = new Set;
|
|
1672
|
+
for (const ruleSet of ruleSets) {
|
|
1673
|
+
for (const rule2 of ruleSet) {
|
|
1674
|
+
combined.add(rule2);
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
return [...combined];
|
|
1678
|
+
}
|
|
1679
|
+
const [first, ...rest] = ruleSets;
|
|
1680
|
+
if (!first)
|
|
1681
|
+
return [];
|
|
1682
|
+
return [...first].filter((rule2) => rest.every((set) => set.has(rule2)));
|
|
1683
|
+
};
|
|
1684
|
+
var getRulesByCategory = (index, category) => {
|
|
1685
|
+
return index.byCategory.get(category) ?? [];
|
|
1686
|
+
};
|
|
1687
|
+
var getRulesByPriority = (index, priority) => {
|
|
1688
|
+
return index.byPriority.get(priority) ?? [];
|
|
1689
|
+
};
|
|
1690
|
+
var getRulesByPriorityRange = (index, minPriority, maxPriority) => {
|
|
1691
|
+
return index.sortedByPriority.filter((rule2) => rule2.priority >= minPriority && rule2.priority <= maxPriority);
|
|
1692
|
+
};
|
|
1693
|
+
var getRuleById = (index, id) => {
|
|
1694
|
+
return index.byId.get(id);
|
|
1695
|
+
};
|
|
1696
|
+
var filterRulesForContext = (index, contextFields) => {
|
|
1697
|
+
const relevantRules = new Set;
|
|
1698
|
+
for (const field of contextFields) {
|
|
1699
|
+
const rules = index.byField.get(field);
|
|
1700
|
+
if (rules) {
|
|
1701
|
+
for (const rule2 of rules) {
|
|
1702
|
+
relevantRules.add(rule2);
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
return [...relevantRules].sort((a, b) => b.priority - a.priority);
|
|
1707
|
+
};
|
|
1708
|
+
var analyzeOptimizations = (rules) => {
|
|
1709
|
+
const suggestions = [];
|
|
1710
|
+
const fieldUsage = new Map;
|
|
1711
|
+
for (const rule2 of rules) {
|
|
1712
|
+
const fields = collectFields2(rule2.conditions);
|
|
1713
|
+
for (const field of fields) {
|
|
1714
|
+
fieldUsage.set(field, (fieldUsage.get(field) ?? 0) + 1);
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
const frequentFields = [...fieldUsage.entries()].filter(([, count]) => count > rules.length * 0.5).map(([field]) => field);
|
|
1718
|
+
if (frequentFields.length > 0) {
|
|
1719
|
+
suggestions.push({
|
|
1720
|
+
type: "INDEX",
|
|
1721
|
+
severity: "medium",
|
|
1722
|
+
message: `Consider creating indexes for frequently used fields: ${frequentFields.join(", ")}`,
|
|
1723
|
+
details: {
|
|
1724
|
+
fields: frequentFields,
|
|
1725
|
+
usagePercentage: frequentFields.map((f) => ({
|
|
1726
|
+
field: f,
|
|
1727
|
+
percentage: (fieldUsage.get(f) ?? 0) / rules.length * 100
|
|
1728
|
+
}))
|
|
1729
|
+
}
|
|
1730
|
+
});
|
|
1731
|
+
}
|
|
1732
|
+
const priorityGroups = new Map;
|
|
1733
|
+
for (const rule2 of rules) {
|
|
1734
|
+
const existing = priorityGroups.get(rule2.priority) ?? [];
|
|
1735
|
+
existing.push(rule2);
|
|
1736
|
+
priorityGroups.set(rule2.priority, existing);
|
|
1737
|
+
}
|
|
1738
|
+
const largePriorityGroups = [...priorityGroups.entries()].filter(([, group]) => group.length > 5);
|
|
1739
|
+
if (largePriorityGroups.length > 0) {
|
|
1740
|
+
for (const [priority, group] of largePriorityGroups) {
|
|
1741
|
+
suggestions.push({
|
|
1742
|
+
type: "REORDER",
|
|
1743
|
+
severity: "low",
|
|
1744
|
+
message: `${group.length} rules share priority ${priority}. Consider differentiating priorities for more predictable execution order.`,
|
|
1745
|
+
ruleIds: group.map((r) => r.id),
|
|
1746
|
+
details: { priority, count: group.length }
|
|
1747
|
+
});
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
const disabledRules = rules.filter((r) => !r.enabled);
|
|
1751
|
+
if (disabledRules.length > rules.length * 0.3) {
|
|
1752
|
+
suggestions.push({
|
|
1753
|
+
type: "SIMPLIFY",
|
|
1754
|
+
severity: "low",
|
|
1755
|
+
message: `${disabledRules.length} rules (${(disabledRules.length / rules.length * 100).toFixed(1)}%) are disabled. Consider removing unused rules.`,
|
|
1756
|
+
ruleIds: disabledRules.map((r) => r.id),
|
|
1757
|
+
details: {
|
|
1758
|
+
disabledCount: disabledRules.length,
|
|
1759
|
+
totalCount: rules.length
|
|
1760
|
+
}
|
|
1761
|
+
});
|
|
1762
|
+
}
|
|
1763
|
+
if (rules.length > 100) {
|
|
1764
|
+
suggestions.push({
|
|
1765
|
+
type: "CACHE",
|
|
1766
|
+
severity: "high",
|
|
1767
|
+
message: `Rule set contains ${rules.length} rules. Enable caching for better performance.`,
|
|
1768
|
+
details: { ruleCount: rules.length }
|
|
1769
|
+
});
|
|
1770
|
+
}
|
|
1771
|
+
return suggestions;
|
|
1772
|
+
};
|
|
1773
|
+
var getIndexStats = (index) => {
|
|
1774
|
+
const totalRules = index.byId.size;
|
|
1775
|
+
const fieldRuleCounts = [...index.byField.values()].map((r) => r.length);
|
|
1776
|
+
const tagRuleCounts = [...index.byTag.values()].map((r) => r.length);
|
|
1777
|
+
return {
|
|
1778
|
+
totalRules,
|
|
1779
|
+
uniqueFields: index.byField.size,
|
|
1780
|
+
uniqueTags: index.byTag.size,
|
|
1781
|
+
uniqueCategories: index.byCategory.size,
|
|
1782
|
+
uniquePriorities: index.byPriority.size,
|
|
1783
|
+
averageRulesPerField: fieldRuleCounts.length > 0 ? fieldRuleCounts.reduce((a, b) => a + b, 0) / fieldRuleCounts.length : 0,
|
|
1784
|
+
averageRulesPerTag: tagRuleCounts.length > 0 ? tagRuleCounts.reduce((a, b) => a + b, 0) / tagRuleCounts.length : 0
|
|
1785
|
+
};
|
|
1786
|
+
};
|
|
1787
|
+
// src/serialization/serializer.ts
|
|
1788
|
+
var EXPORT_VERSION = "1.0.0";
|
|
1789
|
+
var serializeRule = (rule2) => ({
|
|
1790
|
+
id: rule2.id,
|
|
1791
|
+
name: rule2.name,
|
|
1792
|
+
description: rule2.description,
|
|
1793
|
+
conditions: rule2.conditions,
|
|
1794
|
+
consequences: rule2.consequences.map((c) => ({
|
|
1795
|
+
type: c.type,
|
|
1796
|
+
payload: c.payload
|
|
1797
|
+
})),
|
|
1798
|
+
priority: rule2.priority,
|
|
1799
|
+
enabled: rule2.enabled,
|
|
1800
|
+
stopOnMatch: rule2.stopOnMatch,
|
|
1801
|
+
tags: rule2.tags,
|
|
1802
|
+
category: rule2.category,
|
|
1803
|
+
metadata: rule2.metadata,
|
|
1804
|
+
createdAt: rule2.createdAt.toISOString(),
|
|
1805
|
+
updatedAt: rule2.updatedAt.toISOString()
|
|
1806
|
+
});
|
|
1807
|
+
var deserializeRule = (serialized, options = {}) => {
|
|
1808
|
+
const id = options.generateNewIds ? `${options.idPrefix ?? ""}${generateId()}` : serialized.id;
|
|
1809
|
+
const now = new Date;
|
|
1810
|
+
return {
|
|
1811
|
+
id,
|
|
1812
|
+
name: serialized.name,
|
|
1813
|
+
description: serialized.description,
|
|
1814
|
+
conditions: serialized.conditions,
|
|
1815
|
+
consequences: serialized.consequences,
|
|
1816
|
+
priority: serialized.priority,
|
|
1817
|
+
enabled: serialized.enabled,
|
|
1818
|
+
stopOnMatch: serialized.stopOnMatch,
|
|
1819
|
+
tags: serialized.tags,
|
|
1820
|
+
category: serialized.category,
|
|
1821
|
+
metadata: serialized.metadata,
|
|
1822
|
+
createdAt: options.preserveDates ? new Date(serialized.createdAt) : now,
|
|
1823
|
+
updatedAt: options.preserveDates ? new Date(serialized.updatedAt) : now
|
|
1824
|
+
};
|
|
1825
|
+
};
|
|
1826
|
+
var serializeRuleSet = (ruleSet) => ({
|
|
1827
|
+
id: ruleSet.id,
|
|
1828
|
+
name: ruleSet.name,
|
|
1829
|
+
description: ruleSet.description,
|
|
1830
|
+
ruleIds: ruleSet.ruleIds,
|
|
1831
|
+
enabled: ruleSet.enabled,
|
|
1832
|
+
metadata: ruleSet.metadata
|
|
1833
|
+
});
|
|
1834
|
+
var deserializeRuleSet = (serialized, idMapping, options = {}) => {
|
|
1835
|
+
const id = options.generateNewIds ? `${options.idPrefix ?? ""}${generateId()}` : serialized.id;
|
|
1836
|
+
const ruleIds = serialized.ruleIds.map((oldId) => idMapping.get(oldId) ?? oldId);
|
|
1837
|
+
return {
|
|
1838
|
+
id,
|
|
1839
|
+
name: serialized.name,
|
|
1840
|
+
description: serialized.description,
|
|
1841
|
+
ruleIds,
|
|
1842
|
+
enabled: serialized.enabled,
|
|
1843
|
+
metadata: serialized.metadata
|
|
1844
|
+
};
|
|
1845
|
+
};
|
|
1846
|
+
var exportRules = (rules, ruleSets, metadata) => ({
|
|
1847
|
+
version: EXPORT_VERSION,
|
|
1848
|
+
exportedAt: new Date().toISOString(),
|
|
1849
|
+
rules: rules.map(serializeRule),
|
|
1850
|
+
ruleSets: ruleSets?.map(serializeRuleSet),
|
|
1851
|
+
metadata
|
|
1852
|
+
});
|
|
1853
|
+
var exportToJson = (rules, ruleSets, metadata) => {
|
|
1854
|
+
const exportData = exportRules(rules, ruleSets, metadata);
|
|
1855
|
+
return JSON.stringify(exportData, null, 2);
|
|
1856
|
+
};
|
|
1857
|
+
var importRules = (data, options = {}) => {
|
|
1858
|
+
const rules = [];
|
|
1859
|
+
const ruleSets = [];
|
|
1860
|
+
const errors = [];
|
|
1861
|
+
const idMapping = new Map;
|
|
1862
|
+
for (let i = 0;i < data.rules.length; i++) {
|
|
1863
|
+
try {
|
|
1864
|
+
const serialized = data.rules[i];
|
|
1865
|
+
const rule2 = deserializeRule(serialized, options);
|
|
1866
|
+
idMapping.set(serialized.id, rule2.id);
|
|
1867
|
+
rules.push(rule2);
|
|
1868
|
+
} catch (error) {
|
|
1869
|
+
errors.push({
|
|
1870
|
+
index: i,
|
|
1871
|
+
type: "rule",
|
|
1872
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1873
|
+
});
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
if (data.ruleSets) {
|
|
1877
|
+
for (let i = 0;i < data.ruleSets.length; i++) {
|
|
1878
|
+
try {
|
|
1879
|
+
const serialized = data.ruleSets[i];
|
|
1880
|
+
const ruleSet = deserializeRuleSet(serialized, idMapping, options);
|
|
1881
|
+
ruleSets.push(ruleSet);
|
|
1882
|
+
} catch (error) {
|
|
1883
|
+
errors.push({
|
|
1884
|
+
index: i,
|
|
1885
|
+
type: "ruleSet",
|
|
1886
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1887
|
+
});
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
return {
|
|
1892
|
+
success: errors.length === 0,
|
|
1893
|
+
rules,
|
|
1894
|
+
ruleSets,
|
|
1895
|
+
errors,
|
|
1896
|
+
idMapping
|
|
1897
|
+
};
|
|
1898
|
+
};
|
|
1899
|
+
var importFromJson = (json, options = {}) => {
|
|
1900
|
+
try {
|
|
1901
|
+
const data = JSON.parse(json);
|
|
1902
|
+
return importRules(data, options);
|
|
1903
|
+
} catch (error) {
|
|
1904
|
+
return {
|
|
1905
|
+
success: false,
|
|
1906
|
+
rules: [],
|
|
1907
|
+
ruleSets: [],
|
|
1908
|
+
errors: [
|
|
1909
|
+
{
|
|
1910
|
+
index: -1,
|
|
1911
|
+
type: "rule",
|
|
1912
|
+
message: `Invalid JSON: ${error instanceof Error ? error.message : String(error)}`
|
|
1913
|
+
}
|
|
1914
|
+
],
|
|
1915
|
+
idMapping: new Map
|
|
1916
|
+
};
|
|
1917
|
+
}
|
|
1918
|
+
};
|
|
1919
|
+
var cloneRule = (rule2, newId, newName) => {
|
|
1920
|
+
const now = new Date;
|
|
1921
|
+
return {
|
|
1922
|
+
...rule2,
|
|
1923
|
+
id: newId ?? generateId(),
|
|
1924
|
+
name: newName ?? `${rule2.name} (Copy)`,
|
|
1925
|
+
createdAt: now,
|
|
1926
|
+
updatedAt: now
|
|
1927
|
+
};
|
|
1928
|
+
};
|
|
1929
|
+
var mergeRuleSets = (baseRules, incomingRules, strategy = "replace") => {
|
|
1930
|
+
const ruleMap = new Map;
|
|
1931
|
+
for (const rule2 of baseRules) {
|
|
1932
|
+
ruleMap.set(rule2.id, rule2);
|
|
1933
|
+
}
|
|
1934
|
+
for (const rule2 of incomingRules) {
|
|
1935
|
+
const existing = ruleMap.get(rule2.id);
|
|
1936
|
+
if (!existing) {
|
|
1937
|
+
ruleMap.set(rule2.id, rule2);
|
|
1938
|
+
} else {
|
|
1939
|
+
switch (strategy) {
|
|
1940
|
+
case "replace":
|
|
1941
|
+
ruleMap.set(rule2.id, rule2);
|
|
1942
|
+
break;
|
|
1943
|
+
case "skip":
|
|
1944
|
+
break;
|
|
1945
|
+
case "merge": {
|
|
1946
|
+
const merged = {
|
|
1947
|
+
...existing,
|
|
1948
|
+
...rule2,
|
|
1949
|
+
tags: [...new Set([...existing.tags, ...rule2.tags])],
|
|
1950
|
+
metadata: { ...existing.metadata, ...rule2.metadata },
|
|
1951
|
+
updatedAt: new Date
|
|
1952
|
+
};
|
|
1953
|
+
ruleMap.set(rule2.id, merged);
|
|
1954
|
+
break;
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
return [...ruleMap.values()];
|
|
1960
|
+
};
|
|
1961
|
+
var diffRuleSets = (oldRules, newRules) => {
|
|
1962
|
+
const oldMap = new Map(oldRules.map((r) => [r.id, r]));
|
|
1963
|
+
const newMap = new Map(newRules.map((r) => [r.id, r]));
|
|
1964
|
+
const added = [];
|
|
1965
|
+
const removed = [];
|
|
1966
|
+
const modified = [];
|
|
1967
|
+
const unchanged = [];
|
|
1968
|
+
for (const [id, newRule] of newMap) {
|
|
1969
|
+
const oldRule = oldMap.get(id);
|
|
1970
|
+
if (!oldRule) {
|
|
1971
|
+
added.push(newRule);
|
|
1972
|
+
} else if (JSON.stringify({ ...oldRule, updatedAt: null, createdAt: null }) !== JSON.stringify({ ...newRule, updatedAt: null, createdAt: null })) {
|
|
1973
|
+
modified.push({ old: oldRule, new: newRule });
|
|
1974
|
+
} else {
|
|
1975
|
+
unchanged.push(newRule);
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
for (const [id, oldRule] of oldMap) {
|
|
1979
|
+
if (!newMap.has(id)) {
|
|
1980
|
+
removed.push(oldRule);
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
return { added, removed, modified, unchanged };
|
|
1984
|
+
};
|
|
1985
|
+
// src/simulation/simulator.ts
|
|
1986
|
+
var toEvaluationContext = (simContext) => ({
|
|
1987
|
+
data: simContext.data,
|
|
1988
|
+
timestamp: new Date,
|
|
1989
|
+
metadata: simContext.metadata
|
|
1990
|
+
});
|
|
1991
|
+
var simulate = (rules, context) => {
|
|
1992
|
+
const startTime = performance.now();
|
|
1993
|
+
const enabledRules = rules.filter((r) => r.enabled);
|
|
1994
|
+
const evalContext = toEvaluationContext(context);
|
|
1995
|
+
const results = evaluateRules(enabledRules, evalContext);
|
|
1996
|
+
const matchedRules = results.results.filter((r) => r.matched);
|
|
1997
|
+
const unmatchedRules = [];
|
|
1998
|
+
for (const result of results.results) {
|
|
1999
|
+
if (!result.matched) {
|
|
2000
|
+
const reason = result.skipped ? result.skipReason ?? "Skipped" : result.error ? `Error: ${result.error.message}` : "Conditions not met";
|
|
2001
|
+
unmatchedRules.push({
|
|
2002
|
+
ruleId: result.ruleId,
|
|
2003
|
+
ruleName: result.ruleName,
|
|
2004
|
+
reason
|
|
2005
|
+
});
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
const consequences = [];
|
|
2009
|
+
for (const match of matchedRules) {
|
|
2010
|
+
for (const consequence of match.consequences) {
|
|
2011
|
+
consequences.push({
|
|
2012
|
+
type: consequence.type,
|
|
2013
|
+
payload: consequence.payload,
|
|
2014
|
+
ruleId: consequence.ruleId,
|
|
2015
|
+
ruleName: consequence.ruleName ?? match.ruleName
|
|
2016
|
+
});
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
const executionTimeMs = performance.now() - startTime;
|
|
2020
|
+
return {
|
|
2021
|
+
context,
|
|
2022
|
+
matchedRules,
|
|
2023
|
+
unmatchedRules,
|
|
2024
|
+
consequences,
|
|
2025
|
+
executionTimeMs
|
|
2026
|
+
};
|
|
2027
|
+
};
|
|
2028
|
+
var simulateSingleRule = (rule2, context) => {
|
|
2029
|
+
const evalContext = toEvaluationContext(context);
|
|
2030
|
+
const result = evaluateRule(rule2, evalContext);
|
|
2031
|
+
return {
|
|
2032
|
+
matched: result.matched,
|
|
2033
|
+
conditionResult: result.conditionResult,
|
|
2034
|
+
consequences: result.matched ? rule2.consequences.map((c) => ({ type: c.type, payload: c.payload })) : []
|
|
2035
|
+
};
|
|
2036
|
+
};
|
|
2037
|
+
var whatIf = (originalRules, modifiedRules, context) => {
|
|
2038
|
+
const original = simulate(originalRules, context);
|
|
2039
|
+
const modified = simulate(modifiedRules, context);
|
|
2040
|
+
const originalMatchIds = new Set(original.matchedRules.map((r) => r.ruleId));
|
|
2041
|
+
const modifiedMatchIds = new Set(modified.matchedRules.map((r) => r.ruleId));
|
|
2042
|
+
const newMatches = [...modifiedMatchIds].filter((id) => !originalMatchIds.has(id));
|
|
2043
|
+
const lostMatches = [...originalMatchIds].filter((id) => !modifiedMatchIds.has(id));
|
|
2044
|
+
const consequenceChanges = [];
|
|
2045
|
+
const originalConsequenceKeys = new Set(original.consequences.map((c) => `${c.ruleId}:${c.type}`));
|
|
2046
|
+
const modifiedConsequenceKeys = new Set(modified.consequences.map((c) => `${c.ruleId}:${c.type}`));
|
|
2047
|
+
for (const key of modifiedConsequenceKeys) {
|
|
2048
|
+
if (!originalConsequenceKeys.has(key)) {
|
|
2049
|
+
const [ruleId, consequenceType] = key.split(":");
|
|
2050
|
+
consequenceChanges.push({
|
|
2051
|
+
type: "added",
|
|
2052
|
+
consequenceType,
|
|
2053
|
+
ruleId
|
|
2054
|
+
});
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
for (const key of originalConsequenceKeys) {
|
|
2058
|
+
if (!modifiedConsequenceKeys.has(key)) {
|
|
2059
|
+
const [ruleId, consequenceType] = key.split(":");
|
|
2060
|
+
consequenceChanges.push({
|
|
2061
|
+
type: "removed",
|
|
2062
|
+
consequenceType,
|
|
2063
|
+
ruleId
|
|
2064
|
+
});
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
return {
|
|
2068
|
+
original,
|
|
2069
|
+
modified,
|
|
2070
|
+
differences: {
|
|
2071
|
+
newMatches,
|
|
2072
|
+
lostMatches,
|
|
2073
|
+
consequenceChanges
|
|
2074
|
+
}
|
|
2075
|
+
};
|
|
2076
|
+
};
|
|
2077
|
+
var batchSimulate = (rules, contexts) => {
|
|
2078
|
+
const results = contexts.map((context) => simulate(rules, context));
|
|
2079
|
+
const ruleMatchFrequency = new Map;
|
|
2080
|
+
const consequenceFrequency = new Map;
|
|
2081
|
+
let totalMatchedRules = 0;
|
|
2082
|
+
let totalExecutionTime = 0;
|
|
2083
|
+
for (const result of results) {
|
|
2084
|
+
totalMatchedRules += result.matchedRules.length;
|
|
2085
|
+
totalExecutionTime += result.executionTimeMs;
|
|
2086
|
+
for (const match of result.matchedRules) {
|
|
2087
|
+
const count = ruleMatchFrequency.get(match.ruleId) ?? 0;
|
|
2088
|
+
ruleMatchFrequency.set(match.ruleId, count + 1);
|
|
2089
|
+
}
|
|
2090
|
+
for (const consequence of result.consequences) {
|
|
2091
|
+
const key = consequence.type;
|
|
2092
|
+
const count = consequenceFrequency.get(key) ?? 0;
|
|
2093
|
+
consequenceFrequency.set(key, count + 1);
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
return {
|
|
2097
|
+
results,
|
|
2098
|
+
summary: {
|
|
2099
|
+
totalContexts: contexts.length,
|
|
2100
|
+
averageMatchedRules: contexts.length > 0 ? totalMatchedRules / contexts.length : 0,
|
|
2101
|
+
ruleMatchFrequency,
|
|
2102
|
+
consequenceFrequency,
|
|
2103
|
+
averageExecutionTimeMs: contexts.length > 0 ? totalExecutionTime / contexts.length : 0
|
|
2104
|
+
}
|
|
2105
|
+
};
|
|
2106
|
+
};
|
|
2107
|
+
var findRulesAffectedByContextChange = (rules, originalContext, modifiedContext) => {
|
|
2108
|
+
const originalResult = simulate(rules, originalContext);
|
|
2109
|
+
const modifiedResult = simulate(rules, modifiedContext);
|
|
2110
|
+
const originalMatchIds = new Set(originalResult.matchedRules.map((r) => r.ruleId));
|
|
2111
|
+
const modifiedMatchIds = new Set(modifiedResult.matchedRules.map((r) => r.ruleId));
|
|
2112
|
+
const becameTrue = [];
|
|
2113
|
+
const becameFalse = [];
|
|
2114
|
+
const unchanged = [];
|
|
2115
|
+
for (const rule2 of rules) {
|
|
2116
|
+
const wasMatched = originalMatchIds.has(rule2.id);
|
|
2117
|
+
const isMatched = modifiedMatchIds.has(rule2.id);
|
|
2118
|
+
if (!wasMatched && isMatched) {
|
|
2119
|
+
becameTrue.push(rule2);
|
|
2120
|
+
} else if (wasMatched && !isMatched) {
|
|
2121
|
+
becameFalse.push(rule2);
|
|
2122
|
+
} else {
|
|
2123
|
+
unchanged.push(rule2);
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
return { becameTrue, becameFalse, unchanged };
|
|
2127
|
+
};
|
|
2128
|
+
var formatSimulationResult = (result) => {
|
|
2129
|
+
const lines = [
|
|
2130
|
+
"=== Simulation Result ===",
|
|
2131
|
+
"",
|
|
2132
|
+
`Execution Time: ${result.executionTimeMs.toFixed(2)}ms`,
|
|
2133
|
+
"",
|
|
2134
|
+
`Matched Rules (${result.matchedRules.length}):`
|
|
2135
|
+
];
|
|
2136
|
+
for (const match of result.matchedRules) {
|
|
2137
|
+
lines.push(` - ${match.ruleName} (${match.ruleId})`);
|
|
2138
|
+
}
|
|
2139
|
+
lines.push("");
|
|
2140
|
+
lines.push(`Unmatched Rules (${result.unmatchedRules.length}):`);
|
|
2141
|
+
for (const unmatched of result.unmatchedRules) {
|
|
2142
|
+
lines.push(` - ${unmatched.ruleName}: ${unmatched.reason}`);
|
|
2143
|
+
}
|
|
2144
|
+
lines.push("");
|
|
2145
|
+
lines.push(`Consequences (${result.consequences.length}):`);
|
|
2146
|
+
for (const consequence of result.consequences) {
|
|
2147
|
+
lines.push(` - ${consequence.type} from "${consequence.ruleName}"`);
|
|
2148
|
+
}
|
|
2149
|
+
return lines.join(`
|
|
2150
|
+
`);
|
|
2151
|
+
};
|
|
2152
|
+
// src/types/rule.ts
|
|
2153
|
+
var import_zod = require("zod");
|
|
2154
|
+
var RuleSchema = import_zod.z.object({
|
|
2155
|
+
id: import_zod.z.string().min(1),
|
|
2156
|
+
name: import_zod.z.string().min(1),
|
|
2157
|
+
description: import_zod.z.string().optional(),
|
|
2158
|
+
conditions: import_zod.z.custom((val) => {
|
|
2159
|
+
return typeof val === "object" && val !== null && "id" in val && "operator" in val;
|
|
2160
|
+
}, "Invalid condition group"),
|
|
2161
|
+
consequences: import_zod.z.array(import_zod.z.object({
|
|
2162
|
+
type: import_zod.z.string(),
|
|
2163
|
+
payload: import_zod.z.unknown()
|
|
2164
|
+
})),
|
|
2165
|
+
priority: import_zod.z.number().int().default(0),
|
|
2166
|
+
enabled: import_zod.z.boolean().default(true),
|
|
2167
|
+
stopOnMatch: import_zod.z.boolean().default(false),
|
|
2168
|
+
tags: import_zod.z.array(import_zod.z.string()).default([]),
|
|
2169
|
+
category: import_zod.z.string().optional(),
|
|
2170
|
+
metadata: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).optional(),
|
|
2171
|
+
createdAt: import_zod.z.date().default(() => new Date),
|
|
2172
|
+
updatedAt: import_zod.z.date().default(() => new Date)
|
|
2173
|
+
});
|
|
2174
|
+
var RuleSetSchema = import_zod.z.object({
|
|
2175
|
+
id: import_zod.z.string().min(1),
|
|
2176
|
+
name: import_zod.z.string().min(1),
|
|
2177
|
+
description: import_zod.z.string().optional(),
|
|
2178
|
+
ruleIds: import_zod.z.array(import_zod.z.string()),
|
|
2179
|
+
enabled: import_zod.z.boolean().default(true),
|
|
2180
|
+
metadata: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).optional()
|
|
2181
|
+
});
|
|
2182
|
+
// src/utils/pipe.ts
|
|
2183
|
+
function pipe(...fns) {
|
|
2184
|
+
return (value) => fns.reduce((acc, fn) => fn(acc), value);
|
|
2185
|
+
}
|
|
2186
|
+
function compose(...fns) {
|
|
2187
|
+
return (value) => fns.reduceRight((acc, fn) => fn(acc), value);
|
|
2188
|
+
}
|
|
2189
|
+
var identity = (value) => value;
|
|
2190
|
+
var always = (value) => () => value;
|
|
2191
|
+
var tap = (fn) => (value) => {
|
|
2192
|
+
fn(value);
|
|
2193
|
+
return value;
|
|
2194
|
+
};
|
|
2195
|
+
// src/validation/conflicts.ts
|
|
2196
|
+
var import_condition_evaluator4 = require("@f-o-t/condition-evaluator");
|
|
2197
|
+
var DEFAULT_OPTIONS2 = {
|
|
2198
|
+
checkDuplicateIds: true,
|
|
2199
|
+
checkDuplicateConditions: true,
|
|
2200
|
+
checkOverlappingConditions: true,
|
|
2201
|
+
checkPriorityCollisions: true,
|
|
2202
|
+
checkUnreachableRules: true
|
|
2203
|
+
};
|
|
2204
|
+
var collectConditionFields = (condition) => {
|
|
2205
|
+
const fields = new Set;
|
|
2206
|
+
if (import_condition_evaluator4.isConditionGroup(condition)) {
|
|
2207
|
+
for (const child of condition.conditions) {
|
|
2208
|
+
const childFields = collectConditionFields(child);
|
|
2209
|
+
for (const field of childFields) {
|
|
2210
|
+
fields.add(field);
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
} else {
|
|
2214
|
+
fields.add(condition.field);
|
|
2215
|
+
}
|
|
2216
|
+
return fields;
|
|
2217
|
+
};
|
|
2218
|
+
var hashConditionGroup = (condition) => {
|
|
2219
|
+
const serialize = (c) => {
|
|
2220
|
+
if (import_condition_evaluator4.isConditionGroup(c)) {
|
|
2221
|
+
const sortedConditions = [...c.conditions].map((child) => serialize(child)).sort();
|
|
2222
|
+
return `GROUP:${c.operator}:[${sortedConditions.join(",")}]`;
|
|
2223
|
+
}
|
|
2224
|
+
return `COND:${c.type}:${c.field}:${c.operator}:${JSON.stringify(c.value)}`;
|
|
2225
|
+
};
|
|
2226
|
+
return serialize(condition);
|
|
2227
|
+
};
|
|
2228
|
+
var getConditionOperatorValues = (condition, field) => {
|
|
2229
|
+
const results = [];
|
|
2230
|
+
const traverse = (c) => {
|
|
2231
|
+
if (import_condition_evaluator4.isConditionGroup(c)) {
|
|
2232
|
+
for (const child of c.conditions) {
|
|
2233
|
+
traverse(child);
|
|
2234
|
+
}
|
|
2235
|
+
} else if (c.field === field) {
|
|
2236
|
+
results.push({ operator: c.operator, value: c.value });
|
|
2237
|
+
}
|
|
2238
|
+
};
|
|
2239
|
+
traverse(condition);
|
|
2240
|
+
return results;
|
|
2241
|
+
};
|
|
2242
|
+
var areConditionsOverlapping = (conditions1, conditions2) => {
|
|
2243
|
+
const fields1 = collectConditionFields(conditions1);
|
|
2244
|
+
const fields2 = collectConditionFields(conditions2);
|
|
2245
|
+
const commonFields = new Set([...fields1].filter((f) => fields2.has(f)));
|
|
2246
|
+
if (commonFields.size === 0)
|
|
2247
|
+
return false;
|
|
2248
|
+
for (const field of commonFields) {
|
|
2249
|
+
const ops1 = getConditionOperatorValues(conditions1, field);
|
|
2250
|
+
const ops2 = getConditionOperatorValues(conditions2, field);
|
|
2251
|
+
for (const op1 of ops1) {
|
|
2252
|
+
for (const op2 of ops2) {
|
|
2253
|
+
if (op1.operator === op2.operator && op1.value === op2.value) {
|
|
2254
|
+
return true;
|
|
2255
|
+
}
|
|
2256
|
+
if (op1.operator === "gt" && op2.operator === "lt" || op1.operator === "lt" && op2.operator === "gt" || op1.operator === "gte" && op2.operator === "lte" || op1.operator === "lte" && op2.operator === "gte") {
|
|
2257
|
+
const val1 = op1.value;
|
|
2258
|
+
const val2 = op2.value;
|
|
2259
|
+
if (typeof val1 === "number" && typeof val2 === "number") {
|
|
2260
|
+
if (op1.operator === "gt" && op2.operator === "lt") {
|
|
2261
|
+
if (val1 < val2)
|
|
2262
|
+
return true;
|
|
2263
|
+
}
|
|
2264
|
+
if (op1.operator === "lt" && op2.operator === "gt") {
|
|
2265
|
+
if (val1 > val2)
|
|
2266
|
+
return true;
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
return false;
|
|
2274
|
+
};
|
|
2275
|
+
var findDuplicateIds = (rules) => {
|
|
2276
|
+
const conflicts = [];
|
|
2277
|
+
const idMap = new Map;
|
|
2278
|
+
for (const rule2 of rules) {
|
|
2279
|
+
const existing = idMap.get(rule2.id) ?? [];
|
|
2280
|
+
existing.push(rule2);
|
|
2281
|
+
idMap.set(rule2.id, existing);
|
|
2282
|
+
}
|
|
2283
|
+
for (const [id, duplicates] of idMap) {
|
|
2284
|
+
if (duplicates.length > 1) {
|
|
2285
|
+
conflicts.push({
|
|
2286
|
+
type: "DUPLICATE_ID",
|
|
2287
|
+
severity: "error",
|
|
2288
|
+
message: `Multiple rules share the same ID: ${id}`,
|
|
2289
|
+
ruleIds: duplicates.map((r) => r.id),
|
|
2290
|
+
rules: duplicates,
|
|
2291
|
+
details: { duplicateId: id, count: duplicates.length }
|
|
2292
|
+
});
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
return conflicts;
|
|
2296
|
+
};
|
|
2297
|
+
var findDuplicateConditions = (rules) => {
|
|
2298
|
+
const conflicts = [];
|
|
2299
|
+
const hashMap = new Map;
|
|
2300
|
+
for (const rule2 of rules) {
|
|
2301
|
+
const hash = hashConditionGroup(rule2.conditions);
|
|
2302
|
+
const existing = hashMap.get(hash) ?? [];
|
|
2303
|
+
existing.push(rule2);
|
|
2304
|
+
hashMap.set(hash, existing);
|
|
2305
|
+
}
|
|
2306
|
+
for (const [hash, duplicates] of hashMap) {
|
|
2307
|
+
if (duplicates.length > 1) {
|
|
2308
|
+
conflicts.push({
|
|
2309
|
+
type: "DUPLICATE_CONDITIONS",
|
|
2310
|
+
severity: "warning",
|
|
2311
|
+
message: `Multiple rules have identical conditions: ${duplicates.map((r) => r.name).join(", ")}`,
|
|
2312
|
+
ruleIds: duplicates.map((r) => r.id),
|
|
2313
|
+
rules: duplicates,
|
|
2314
|
+
details: { conditionHash: hash, count: duplicates.length }
|
|
2315
|
+
});
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
return conflicts;
|
|
2319
|
+
};
|
|
2320
|
+
var findOverlappingConditions = (rules) => {
|
|
2321
|
+
const conflicts = [];
|
|
2322
|
+
const checked = new Set;
|
|
2323
|
+
for (let i = 0;i < rules.length; i++) {
|
|
2324
|
+
for (let j = i + 1;j < rules.length; j++) {
|
|
2325
|
+
const rule1 = rules[i];
|
|
2326
|
+
const rule2 = rules[j];
|
|
2327
|
+
const key = [rule1.id, rule2.id].sort().join(":");
|
|
2328
|
+
if (checked.has(key))
|
|
2329
|
+
continue;
|
|
2330
|
+
checked.add(key);
|
|
2331
|
+
if (areConditionsOverlapping(rule1.conditions, rule2.conditions)) {
|
|
2332
|
+
conflicts.push({
|
|
2333
|
+
type: "OVERLAPPING_CONDITIONS",
|
|
2334
|
+
severity: "info",
|
|
2335
|
+
message: `Rules "${rule1.name}" and "${rule2.name}" have overlapping conditions`,
|
|
2336
|
+
ruleIds: [rule1.id, rule2.id],
|
|
2337
|
+
rules: [rule1, rule2],
|
|
2338
|
+
details: {
|
|
2339
|
+
rule1Fields: [...collectConditionFields(rule1.conditions)],
|
|
2340
|
+
rule2Fields: [...collectConditionFields(rule2.conditions)]
|
|
2341
|
+
}
|
|
2342
|
+
});
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
return conflicts;
|
|
2347
|
+
};
|
|
2348
|
+
var findPriorityCollisions = (rules) => {
|
|
2349
|
+
const conflicts = [];
|
|
2350
|
+
const priorityMap = new Map;
|
|
2351
|
+
for (const rule2 of rules) {
|
|
2352
|
+
const existing = priorityMap.get(rule2.priority) ?? [];
|
|
2353
|
+
existing.push(rule2);
|
|
2354
|
+
priorityMap.set(rule2.priority, existing);
|
|
2355
|
+
}
|
|
2356
|
+
for (const [priority, rulesWithPriority] of priorityMap) {
|
|
2357
|
+
if (rulesWithPriority.length > 1) {
|
|
2358
|
+
const overlappingPairs = new Set;
|
|
2359
|
+
for (let i = 0;i < rulesWithPriority.length; i++) {
|
|
2360
|
+
for (let j = i + 1;j < rulesWithPriority.length; j++) {
|
|
2361
|
+
const r1 = rulesWithPriority[i];
|
|
2362
|
+
const r2 = rulesWithPriority[j];
|
|
2363
|
+
if (areConditionsOverlapping(r1.conditions, r2.conditions)) {
|
|
2364
|
+
overlappingPairs.add(r1.id);
|
|
2365
|
+
overlappingPairs.add(r2.id);
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
if (overlappingPairs.size > 1) {
|
|
2370
|
+
const overlapping = rulesWithPriority.filter((r) => overlappingPairs.has(r.id));
|
|
2371
|
+
conflicts.push({
|
|
2372
|
+
type: "PRIORITY_COLLISION",
|
|
2373
|
+
severity: "warning",
|
|
2374
|
+
message: `Multiple overlapping rules share priority ${priority}: ${overlapping.map((r) => r.name).join(", ")}`,
|
|
2375
|
+
ruleIds: overlapping.map((r) => r.id),
|
|
2376
|
+
rules: overlapping,
|
|
2377
|
+
details: { priority, count: overlapping.length }
|
|
2378
|
+
});
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
return conflicts;
|
|
2383
|
+
};
|
|
2384
|
+
var findUnreachableRules = (rules) => {
|
|
2385
|
+
const conflicts = [];
|
|
2386
|
+
const sortedRules = [...rules].sort((a, b) => b.priority - a.priority);
|
|
2387
|
+
for (let i = 0;i < sortedRules.length; i++) {
|
|
2388
|
+
const rule2 = sortedRules[i];
|
|
2389
|
+
if (!rule2.enabled)
|
|
2390
|
+
continue;
|
|
2391
|
+
for (let j = 0;j < i; j++) {
|
|
2392
|
+
const higherPriorityRule = sortedRules[j];
|
|
2393
|
+
if (!higherPriorityRule.enabled || !higherPriorityRule.stopOnMatch) {
|
|
2394
|
+
continue;
|
|
2395
|
+
}
|
|
2396
|
+
if (hashConditionGroup(rule2.conditions) === hashConditionGroup(higherPriorityRule.conditions)) {
|
|
2397
|
+
conflicts.push({
|
|
2398
|
+
type: "UNREACHABLE_RULE",
|
|
2399
|
+
severity: "warning",
|
|
2400
|
+
message: `Rule "${rule2.name}" may be unreachable because rule "${higherPriorityRule.name}" has higher priority and stops on match`,
|
|
2401
|
+
ruleIds: [rule2.id, higherPriorityRule.id],
|
|
2402
|
+
rules: [rule2, higherPriorityRule],
|
|
2403
|
+
details: {
|
|
2404
|
+
unreachableRule: rule2.id,
|
|
2405
|
+
blockingRule: higherPriorityRule.id,
|
|
2406
|
+
priorityDifference: higherPriorityRule.priority - rule2.priority
|
|
2407
|
+
}
|
|
2408
|
+
});
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
return conflicts;
|
|
2413
|
+
};
|
|
2414
|
+
var detectConflicts = (rules, options = {}) => {
|
|
2415
|
+
const opts = { ...DEFAULT_OPTIONS2, ...options };
|
|
2416
|
+
const conflicts = [];
|
|
2417
|
+
if (opts.checkDuplicateIds) {
|
|
2418
|
+
conflicts.push(...findDuplicateIds(rules));
|
|
2419
|
+
}
|
|
2420
|
+
if (opts.checkDuplicateConditions) {
|
|
2421
|
+
conflicts.push(...findDuplicateConditions(rules));
|
|
2422
|
+
}
|
|
2423
|
+
if (opts.checkOverlappingConditions) {
|
|
2424
|
+
conflicts.push(...findOverlappingConditions(rules));
|
|
2425
|
+
}
|
|
2426
|
+
if (opts.checkPriorityCollisions) {
|
|
2427
|
+
conflicts.push(...findPriorityCollisions(rules));
|
|
2428
|
+
}
|
|
2429
|
+
if (opts.checkUnreachableRules) {
|
|
2430
|
+
conflicts.push(...findUnreachableRules(rules));
|
|
2431
|
+
}
|
|
2432
|
+
return conflicts;
|
|
2433
|
+
};
|
|
2434
|
+
var hasConflicts = (rules, options = {}) => {
|
|
2435
|
+
return detectConflicts(rules, options).length > 0;
|
|
2436
|
+
};
|
|
2437
|
+
var hasErrors = (rules, options = {}) => {
|
|
2438
|
+
return detectConflicts(rules, options).some((c) => c.severity === "error");
|
|
2439
|
+
};
|
|
2440
|
+
var getConflictsByType = (conflicts, type) => {
|
|
2441
|
+
return conflicts.filter((c) => c.type === type);
|
|
2442
|
+
};
|
|
2443
|
+
var getConflictsBySeverity = (conflicts, severity) => {
|
|
2444
|
+
return conflicts.filter((c) => c.severity === severity);
|
|
2445
|
+
};
|
|
2446
|
+
var formatConflicts = (conflicts) => {
|
|
2447
|
+
if (conflicts.length === 0) {
|
|
2448
|
+
return "No conflicts detected";
|
|
2449
|
+
}
|
|
2450
|
+
const lines = [`Found ${conflicts.length} conflict(s):`];
|
|
2451
|
+
for (const conflict of conflicts) {
|
|
2452
|
+
const severityIcon = conflict.severity === "error" ? "[ERROR]" : conflict.severity === "warning" ? "[WARN]" : "[INFO]";
|
|
2453
|
+
lines.push(` ${severityIcon} ${conflict.type}: ${conflict.message}`);
|
|
2454
|
+
}
|
|
2455
|
+
return lines.join(`
|
|
2456
|
+
`);
|
|
2457
|
+
};
|
|
2458
|
+
// src/validation/integrity.ts
|
|
2459
|
+
var import_condition_evaluator5 = require("@f-o-t/condition-evaluator");
|
|
2460
|
+
var DEFAULT_OPTIONS3 = {
|
|
2461
|
+
checkCircularReferences: true,
|
|
2462
|
+
checkOrphanedRuleSets: true,
|
|
2463
|
+
checkMissingReferences: true,
|
|
2464
|
+
checkFieldConsistency: true
|
|
2465
|
+
};
|
|
2466
|
+
var createIssue = (code, message, severity, details) => ({
|
|
2467
|
+
code,
|
|
2468
|
+
message,
|
|
2469
|
+
severity,
|
|
2470
|
+
path: details?.path,
|
|
2471
|
+
ruleId: details?.ruleId,
|
|
2472
|
+
details: details?.extra
|
|
2473
|
+
});
|
|
2474
|
+
var collectAllFields = (condition) => {
|
|
2475
|
+
const fields = new Set;
|
|
2476
|
+
const traverse = (c) => {
|
|
2477
|
+
if (import_condition_evaluator5.isConditionGroup(c)) {
|
|
2478
|
+
for (const child of c.conditions) {
|
|
2479
|
+
traverse(child);
|
|
2480
|
+
}
|
|
2481
|
+
} else {
|
|
2482
|
+
fields.add(c.field);
|
|
2483
|
+
}
|
|
2484
|
+
};
|
|
2485
|
+
traverse(condition);
|
|
2486
|
+
return fields;
|
|
2487
|
+
};
|
|
2488
|
+
var checkDuplicateConditionIds = (condition, ruleId) => {
|
|
2489
|
+
const issues = [];
|
|
2490
|
+
const seenIds = new Map;
|
|
2491
|
+
const traverse = (c, path) => {
|
|
2492
|
+
const id = c.id;
|
|
2493
|
+
const count = seenIds.get(id) ?? 0;
|
|
2494
|
+
seenIds.set(id, count + 1);
|
|
2495
|
+
if (count > 0) {
|
|
2496
|
+
issues.push(createIssue("DUPLICATE_CONDITION_ID", `Duplicate condition ID "${id}" found within rule`, "error", { path, ruleId, extra: { conditionId: id } }));
|
|
2497
|
+
}
|
|
2498
|
+
if (import_condition_evaluator5.isConditionGroup(c)) {
|
|
2499
|
+
c.conditions.forEach((child, i) => {
|
|
2500
|
+
traverse(child, `${path}[${i}]`);
|
|
2501
|
+
});
|
|
2502
|
+
}
|
|
2503
|
+
};
|
|
2504
|
+
traverse(condition, "conditions");
|
|
2505
|
+
return issues;
|
|
2506
|
+
};
|
|
2507
|
+
var checkRuleIntegrity = (rule2, options) => {
|
|
2508
|
+
const issues = [];
|
|
2509
|
+
issues.push(...checkDuplicateConditionIds(rule2.conditions, rule2.id));
|
|
2510
|
+
if (rule2.priority < 0) {
|
|
2511
|
+
issues.push(createIssue("NEGATIVE_PRIORITY", `Rule "${rule2.name}" has negative priority: ${rule2.priority}`, "warning", { ruleId: rule2.id, extra: { priority: rule2.priority } }));
|
|
2512
|
+
}
|
|
2513
|
+
if (rule2.priority > 1000) {
|
|
2514
|
+
issues.push(createIssue("EXTREME_PRIORITY", `Rule "${rule2.name}" has very high priority: ${rule2.priority}`, "info", { ruleId: rule2.id, extra: { priority: rule2.priority } }));
|
|
2515
|
+
}
|
|
2516
|
+
if (rule2.consequences.length === 0) {
|
|
2517
|
+
issues.push(createIssue("NO_CONSEQUENCES", `Rule "${rule2.name}" has no consequences defined`, "warning", { ruleId: rule2.id }));
|
|
2518
|
+
}
|
|
2519
|
+
if (options.allowedCategories && rule2.category) {
|
|
2520
|
+
if (!options.allowedCategories.includes(rule2.category)) {
|
|
2521
|
+
issues.push(createIssue("INVALID_CATEGORY", `Rule "${rule2.name}" has invalid category: ${rule2.category}`, "error", {
|
|
2522
|
+
ruleId: rule2.id,
|
|
2523
|
+
extra: {
|
|
2524
|
+
category: rule2.category,
|
|
2525
|
+
allowedCategories: [...options.allowedCategories]
|
|
2526
|
+
}
|
|
2527
|
+
}));
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
if (options.allowedTags) {
|
|
2531
|
+
const invalidTags = rule2.tags.filter((t) => !options.allowedTags.includes(t));
|
|
2532
|
+
if (invalidTags.length > 0) {
|
|
2533
|
+
issues.push(createIssue("INVALID_TAGS", `Rule "${rule2.name}" has invalid tags: ${invalidTags.join(", ")}`, "warning", {
|
|
2534
|
+
ruleId: rule2.id,
|
|
2535
|
+
extra: {
|
|
2536
|
+
invalidTags,
|
|
2537
|
+
allowedTags: [...options.allowedTags]
|
|
2538
|
+
}
|
|
2539
|
+
}));
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
if (options.requiredFields) {
|
|
2543
|
+
const ruleFields = collectAllFields(rule2.conditions);
|
|
2544
|
+
const missingFields = options.requiredFields.filter((f) => !ruleFields.has(f));
|
|
2545
|
+
if (missingFields.length > 0) {
|
|
2546
|
+
issues.push(createIssue("MISSING_REQUIRED_FIELDS", `Rule "${rule2.name}" is missing required fields: ${missingFields.join(", ")}`, "warning", { ruleId: rule2.id, extra: { missingFields } }));
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
return issues;
|
|
2550
|
+
};
|
|
2551
|
+
var checkRuleSetIntegrity = (ruleSet, rules) => {
|
|
2552
|
+
const issues = [];
|
|
2553
|
+
const ruleIds = new Set(rules.map((r) => r.id));
|
|
2554
|
+
for (const ruleId of ruleSet.ruleIds) {
|
|
2555
|
+
if (!ruleIds.has(ruleId)) {
|
|
2556
|
+
issues.push(createIssue("MISSING_RULE_REFERENCE", `RuleSet "${ruleSet.name}" references non-existent rule: ${ruleId}`, "error", { extra: { ruleSetId: ruleSet.id, missingRuleId: ruleId } }));
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
if (ruleSet.ruleIds.length === 0) {
|
|
2560
|
+
issues.push(createIssue("EMPTY_RULESET", `RuleSet "${ruleSet.name}" contains no rules`, "warning", { extra: { ruleSetId: ruleSet.id } }));
|
|
2561
|
+
}
|
|
2562
|
+
const duplicateIds = ruleSet.ruleIds.filter((id, i) => ruleSet.ruleIds.indexOf(id) !== i);
|
|
2563
|
+
if (duplicateIds.length > 0) {
|
|
2564
|
+
issues.push(createIssue("DUPLICATE_RULESET_ENTRIES", `RuleSet "${ruleSet.name}" contains duplicate rule references: ${[...new Set(duplicateIds)].join(", ")}`, "warning", { extra: { ruleSetId: ruleSet.id, duplicateIds } }));
|
|
2565
|
+
}
|
|
2566
|
+
return issues;
|
|
2567
|
+
};
|
|
2568
|
+
var checkFieldConsistency = (rules) => {
|
|
2569
|
+
const issues = [];
|
|
2570
|
+
const fieldTypes = new Map;
|
|
2571
|
+
for (const rule2 of rules) {
|
|
2572
|
+
const traverse = (c) => {
|
|
2573
|
+
if (import_condition_evaluator5.isConditionGroup(c)) {
|
|
2574
|
+
for (const child of c.conditions) {
|
|
2575
|
+
traverse(child);
|
|
2576
|
+
}
|
|
2577
|
+
} else {
|
|
2578
|
+
const existing = fieldTypes.get(c.field) ?? [];
|
|
2579
|
+
existing.push({
|
|
2580
|
+
type: c.type,
|
|
2581
|
+
ruleId: rule2.id,
|
|
2582
|
+
ruleName: rule2.name
|
|
2583
|
+
});
|
|
2584
|
+
fieldTypes.set(c.field, existing);
|
|
2585
|
+
}
|
|
2586
|
+
};
|
|
2587
|
+
traverse(rule2.conditions);
|
|
2588
|
+
}
|
|
2589
|
+
for (const [field, types] of fieldTypes) {
|
|
2590
|
+
const uniqueTypes = [...new Set(types.map((t) => t.type))];
|
|
2591
|
+
if (uniqueTypes.length > 1) {
|
|
2592
|
+
issues.push(createIssue("INCONSISTENT_FIELD_TYPE", `Field "${field}" is used with different types: ${uniqueTypes.join(", ")}`, "warning", {
|
|
2593
|
+
extra: {
|
|
2594
|
+
field,
|
|
2595
|
+
types: uniqueTypes,
|
|
2596
|
+
rules: types.map((t) => ({
|
|
2597
|
+
id: t.ruleId,
|
|
2598
|
+
name: t.ruleName,
|
|
2599
|
+
type: t.type
|
|
2600
|
+
}))
|
|
2601
|
+
}
|
|
2602
|
+
}));
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
return issues;
|
|
2606
|
+
};
|
|
2607
|
+
var checkIntegrity = (rules, ruleSets = [], options = {}) => {
|
|
2608
|
+
const opts = { ...DEFAULT_OPTIONS3, ...options };
|
|
2609
|
+
const issues = [];
|
|
2610
|
+
for (const rule2 of rules) {
|
|
2611
|
+
issues.push(...checkRuleIntegrity(rule2, opts));
|
|
2612
|
+
}
|
|
2613
|
+
for (const ruleSet of ruleSets) {
|
|
2614
|
+
issues.push(...checkRuleSetIntegrity(ruleSet, rules));
|
|
2615
|
+
}
|
|
2616
|
+
if (opts.checkFieldConsistency) {
|
|
2617
|
+
issues.push(...checkFieldConsistency(rules));
|
|
2618
|
+
}
|
|
2619
|
+
return {
|
|
2620
|
+
valid: issues.filter((i) => i.severity === "error").length === 0,
|
|
2621
|
+
issues
|
|
2622
|
+
};
|
|
2623
|
+
};
|
|
2624
|
+
var checkRuleFieldCoverage = (rules, expectedFields) => {
|
|
2625
|
+
const allFields = new Set;
|
|
2626
|
+
for (const rule2 of rules) {
|
|
2627
|
+
const ruleFields = collectAllFields(rule2.conditions);
|
|
2628
|
+
for (const field of ruleFields) {
|
|
2629
|
+
allFields.add(field);
|
|
2630
|
+
}
|
|
2631
|
+
}
|
|
2632
|
+
const expectedSet = new Set(expectedFields);
|
|
2633
|
+
const coveredFields = [...expectedFields].filter((f) => allFields.has(f));
|
|
2634
|
+
const uncoveredFields = [...expectedFields].filter((f) => !allFields.has(f));
|
|
2635
|
+
const extraFields = [...allFields].filter((f) => !expectedSet.has(f));
|
|
2636
|
+
return {
|
|
2637
|
+
coveredFields,
|
|
2638
|
+
uncoveredFields,
|
|
2639
|
+
extraFields,
|
|
2640
|
+
coveragePercentage: expectedFields.length > 0 ? coveredFields.length / expectedFields.length * 100 : 100
|
|
2641
|
+
};
|
|
2642
|
+
};
|
|
2643
|
+
var getUsedFields = (rules) => {
|
|
2644
|
+
const fields = new Set;
|
|
2645
|
+
for (const rule2 of rules) {
|
|
2646
|
+
const ruleFields = collectAllFields(rule2.conditions);
|
|
2647
|
+
for (const field of ruleFields) {
|
|
2648
|
+
fields.add(field);
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
return [...fields].sort();
|
|
2652
|
+
};
|
|
2653
|
+
var getUsedOperators = (rules) => {
|
|
2654
|
+
const operators = [];
|
|
2655
|
+
const seen = new Set;
|
|
2656
|
+
for (const rule2 of rules) {
|
|
2657
|
+
const traverse = (c) => {
|
|
2658
|
+
if (import_condition_evaluator5.isConditionGroup(c)) {
|
|
2659
|
+
for (const child of c.conditions) {
|
|
2660
|
+
traverse(child);
|
|
2661
|
+
}
|
|
2662
|
+
} else {
|
|
2663
|
+
const key = `${c.field}:${c.operator}:${c.type}`;
|
|
2664
|
+
if (!seen.has(key)) {
|
|
2665
|
+
seen.add(key);
|
|
2666
|
+
operators.push({
|
|
2667
|
+
field: c.field,
|
|
2668
|
+
operator: c.operator,
|
|
2669
|
+
type: c.type
|
|
2670
|
+
});
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
};
|
|
2674
|
+
traverse(rule2.conditions);
|
|
2675
|
+
}
|
|
2676
|
+
return operators;
|
|
2677
|
+
};
|
|
2678
|
+
var formatIntegrityResult = (result) => {
|
|
2679
|
+
if (result.valid && result.issues.length === 0) {
|
|
2680
|
+
return "Integrity check passed - no issues found";
|
|
2681
|
+
}
|
|
2682
|
+
const lines = [
|
|
2683
|
+
result.valid ? `Integrity check passed with ${result.issues.length} warning(s)` : `Integrity check failed with ${result.issues.filter((i) => i.severity === "error").length} error(s)`
|
|
2684
|
+
];
|
|
2685
|
+
const grouped = {
|
|
2686
|
+
error: result.issues.filter((i) => i.severity === "error"),
|
|
2687
|
+
warning: result.issues.filter((i) => i.severity === "warning"),
|
|
2688
|
+
info: result.issues.filter((i) => i.severity === "info")
|
|
2689
|
+
};
|
|
2690
|
+
for (const [severity, issues] of Object.entries(grouped)) {
|
|
2691
|
+
if (issues.length > 0) {
|
|
2692
|
+
lines.push(`
|
|
2693
|
+
${severity.toUpperCase()}S (${issues.length}):`);
|
|
2694
|
+
for (const issue of issues) {
|
|
2695
|
+
lines.push(` - [${issue.code}] ${issue.message}`);
|
|
2696
|
+
}
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
2699
|
+
return lines.join(`
|
|
2700
|
+
`);
|
|
2701
|
+
};
|
|
2702
|
+
// src/validation/schema.ts
|
|
2703
|
+
var import_condition_evaluator6 = require("@f-o-t/condition-evaluator");
|
|
2704
|
+
var DEFAULT_OPTIONS4 = {
|
|
2705
|
+
validateConditions: true,
|
|
2706
|
+
validateConsequences: true,
|
|
2707
|
+
strictMode: false
|
|
2708
|
+
};
|
|
2709
|
+
var createError = (path, message, code) => ({
|
|
2710
|
+
path,
|
|
2711
|
+
message,
|
|
2712
|
+
code
|
|
2713
|
+
});
|
|
2714
|
+
var validResult = () => ({
|
|
2715
|
+
valid: true,
|
|
2716
|
+
errors: []
|
|
2717
|
+
});
|
|
2718
|
+
var invalidResult = (errors) => ({
|
|
2719
|
+
valid: false,
|
|
2720
|
+
errors
|
|
2721
|
+
});
|
|
2722
|
+
var validateConditionStructure = (condition, path) => {
|
|
2723
|
+
const errors = [];
|
|
2724
|
+
if (import_condition_evaluator6.isConditionGroup(condition)) {
|
|
2725
|
+
if (!condition.id || typeof condition.id !== "string") {
|
|
2726
|
+
errors.push(createError(`${path}.id`, "Condition group must have a string id", "MISSING_GROUP_ID"));
|
|
2727
|
+
}
|
|
2728
|
+
if (!["AND", "OR"].includes(condition.operator)) {
|
|
2729
|
+
errors.push(createError(`${path}.operator`, `Invalid operator: ${condition.operator}. Must be "AND" or "OR"`, "INVALID_GROUP_OPERATOR"));
|
|
2730
|
+
}
|
|
2731
|
+
if (!Array.isArray(condition.conditions)) {
|
|
2732
|
+
errors.push(createError(`${path}.conditions`, "Condition group must have a conditions array", "MISSING_CONDITIONS_ARRAY"));
|
|
2733
|
+
} else if (condition.conditions.length === 0) {
|
|
2734
|
+
errors.push(createError(`${path}.conditions`, "Condition group must have at least one condition", "EMPTY_CONDITIONS_ARRAY"));
|
|
2735
|
+
} else {
|
|
2736
|
+
for (let i = 0;i < condition.conditions.length; i++) {
|
|
2737
|
+
const nestedErrors = validateConditionStructure(condition.conditions[i], `${path}.conditions[${i}]`);
|
|
2738
|
+
errors.push(...nestedErrors);
|
|
2739
|
+
}
|
|
2740
|
+
}
|
|
2741
|
+
} else {
|
|
2742
|
+
if (!condition.id || typeof condition.id !== "string") {
|
|
2743
|
+
errors.push(createError(`${path}.id`, "Condition must have a string id", "MISSING_CONDITION_ID"));
|
|
2744
|
+
}
|
|
2745
|
+
if (!condition.type || typeof condition.type !== "string") {
|
|
2746
|
+
errors.push(createError(`${path}.type`, "Condition must have a type", "MISSING_CONDITION_TYPE"));
|
|
2747
|
+
}
|
|
2748
|
+
if (!condition.field || typeof condition.field !== "string") {
|
|
2749
|
+
errors.push(createError(`${path}.field`, "Condition must have a field", "MISSING_CONDITION_FIELD"));
|
|
2750
|
+
}
|
|
2751
|
+
if (!condition.operator || typeof condition.operator !== "string") {
|
|
2752
|
+
errors.push(createError(`${path}.operator`, "Condition must have an operator", "MISSING_CONDITION_OPERATOR"));
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
return errors;
|
|
2756
|
+
};
|
|
2757
|
+
var validateConsequenceStructure = (consequences, consequenceSchemas, strictMode = false) => {
|
|
2758
|
+
const errors = [];
|
|
2759
|
+
for (let i = 0;i < consequences.length; i++) {
|
|
2760
|
+
const consequence = consequences[i];
|
|
2761
|
+
const path = `consequences[${i}]`;
|
|
2762
|
+
if (!consequence.type || typeof consequence.type !== "string") {
|
|
2763
|
+
errors.push(createError(`${path}.type`, "Consequence must have a type", "MISSING_CONSEQUENCE_TYPE"));
|
|
2764
|
+
continue;
|
|
2765
|
+
}
|
|
2766
|
+
if (strictMode && consequenceSchemas) {
|
|
2767
|
+
const schema = consequenceSchemas[consequence.type];
|
|
2768
|
+
if (!schema) {
|
|
2769
|
+
errors.push(createError(`${path}.type`, `Unknown consequence type: ${consequence.type}`, "UNKNOWN_CONSEQUENCE_TYPE"));
|
|
2770
|
+
} else {
|
|
2771
|
+
const result = schema.safeParse(consequence.payload);
|
|
2772
|
+
if (!result.success) {
|
|
2773
|
+
for (const issue of result.error.issues) {
|
|
2774
|
+
errors.push(createError(`${path}.payload.${issue.path.join(".")}`, issue.message, "INVALID_CONSEQUENCE_PAYLOAD"));
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2780
|
+
return errors;
|
|
2781
|
+
};
|
|
2782
|
+
var validateRule = (rule2, options = {}) => {
|
|
2783
|
+
const opts = { ...DEFAULT_OPTIONS4, ...options };
|
|
2784
|
+
const errors = [];
|
|
2785
|
+
const schemaResult = RuleSchema.safeParse(rule2);
|
|
2786
|
+
if (!schemaResult.success) {
|
|
2787
|
+
for (const issue of schemaResult.error.issues) {
|
|
2788
|
+
errors.push(createError(issue.path.join("."), issue.message, "SCHEMA_VALIDATION_FAILED"));
|
|
2789
|
+
}
|
|
2790
|
+
return invalidResult(errors);
|
|
2791
|
+
}
|
|
2792
|
+
const validRule = schemaResult.data;
|
|
2793
|
+
if (opts.validateConditions) {
|
|
2794
|
+
const conditionErrors = validateConditionStructure(validRule.conditions, "conditions");
|
|
2795
|
+
errors.push(...conditionErrors);
|
|
2796
|
+
}
|
|
2797
|
+
if (opts.validateConsequences) {
|
|
2798
|
+
const consequenceErrors = validateConsequenceStructure(validRule.consequences, opts.consequenceSchemas, opts.strictMode);
|
|
2799
|
+
errors.push(...consequenceErrors);
|
|
2800
|
+
}
|
|
2801
|
+
return errors.length > 0 ? invalidResult(errors) : validResult();
|
|
2802
|
+
};
|
|
2803
|
+
var validateRules = (rules, options = {}) => {
|
|
2804
|
+
const errors = [];
|
|
2805
|
+
for (let i = 0;i < rules.length; i++) {
|
|
2806
|
+
const result = validateRule(rules[i], options);
|
|
2807
|
+
if (!result.valid) {
|
|
2808
|
+
for (const error of result.errors) {
|
|
2809
|
+
errors.push(createError(`rules[${i}].${error.path}`, error.message, error.code));
|
|
2810
|
+
}
|
|
2811
|
+
}
|
|
2812
|
+
}
|
|
2813
|
+
return errors.length > 0 ? invalidResult(errors) : validResult();
|
|
2814
|
+
};
|
|
2815
|
+
var validateRuleSet = (ruleSet) => {
|
|
2816
|
+
const errors = [];
|
|
2817
|
+
const schemaResult = RuleSetSchema.safeParse(ruleSet);
|
|
2818
|
+
if (!schemaResult.success) {
|
|
2819
|
+
for (const issue of schemaResult.error.issues) {
|
|
2820
|
+
errors.push(createError(issue.path.join("."), issue.message, "SCHEMA_VALIDATION_FAILED"));
|
|
2821
|
+
}
|
|
2822
|
+
return invalidResult(errors);
|
|
2823
|
+
}
|
|
2824
|
+
return validResult();
|
|
2825
|
+
};
|
|
2826
|
+
var validateConditions = (conditions2) => {
|
|
2827
|
+
const errors = validateConditionStructure(conditions2, "conditions");
|
|
2828
|
+
return errors.length > 0 ? invalidResult(errors) : validResult();
|
|
2829
|
+
};
|
|
2830
|
+
var parseRule = (rule2, options = {}) => {
|
|
2831
|
+
const result = validateRule(rule2, options);
|
|
2832
|
+
if (!result.valid) {
|
|
2833
|
+
throw new Error(`Invalid rule: ${result.errors.map((e) => e.message).join(", ")}`);
|
|
2834
|
+
}
|
|
2835
|
+
return rule2;
|
|
2836
|
+
};
|
|
2837
|
+
var safeParseRule = (rule2, options = {}) => {
|
|
2838
|
+
const result = validateRule(rule2, options);
|
|
2839
|
+
if (!result.valid) {
|
|
2840
|
+
return { success: false, errors: result.errors };
|
|
2841
|
+
}
|
|
2842
|
+
return { success: true, data: rule2 };
|
|
2843
|
+
};
|
|
2844
|
+
var createRuleValidator = (consequenceSchemas, defaultOptions = {}) => {
|
|
2845
|
+
return {
|
|
2846
|
+
validate: (rule2, options = {}) => validateRule(rule2, {
|
|
2847
|
+
...defaultOptions,
|
|
2848
|
+
...options,
|
|
2849
|
+
consequenceSchemas
|
|
2850
|
+
}),
|
|
2851
|
+
parse: (rule2, options = {}) => parseRule(rule2, {
|
|
2852
|
+
...defaultOptions,
|
|
2853
|
+
...options,
|
|
2854
|
+
consequenceSchemas
|
|
2855
|
+
}),
|
|
2856
|
+
safeParse: (rule2, options = {}) => safeParseRule(rule2, {
|
|
2857
|
+
...defaultOptions,
|
|
2858
|
+
...options,
|
|
2859
|
+
consequenceSchemas
|
|
2860
|
+
})
|
|
2861
|
+
};
|
|
2862
|
+
};
|
|
2863
|
+
// src/versioning/version-store.ts
|
|
2864
|
+
var createVersionStore = () => ({
|
|
2865
|
+
histories: new Map
|
|
2866
|
+
});
|
|
2867
|
+
var addVersion = (store, rule2, changeType, options = {}) => {
|
|
2868
|
+
const histories = new Map(store.histories);
|
|
2869
|
+
const existingHistory = histories.get(rule2.id);
|
|
2870
|
+
const currentVersion = existingHistory ? existingHistory.currentVersion + 1 : 1;
|
|
2871
|
+
const version = {
|
|
2872
|
+
versionId: generateId(),
|
|
2873
|
+
ruleId: rule2.id,
|
|
2874
|
+
version: currentVersion,
|
|
2875
|
+
rule: { ...rule2 },
|
|
2876
|
+
createdAt: new Date,
|
|
2877
|
+
createdBy: options.createdBy,
|
|
2878
|
+
comment: options.comment,
|
|
2879
|
+
changeType
|
|
2880
|
+
};
|
|
2881
|
+
const newHistory = {
|
|
2882
|
+
ruleId: rule2.id,
|
|
2883
|
+
currentVersion,
|
|
2884
|
+
versions: existingHistory ? [...existingHistory.versions, version] : [version]
|
|
2885
|
+
};
|
|
2886
|
+
histories.set(rule2.id, newHistory);
|
|
2887
|
+
return { histories };
|
|
2888
|
+
};
|
|
2889
|
+
var getHistory = (store, ruleId) => {
|
|
2890
|
+
return store.histories.get(ruleId);
|
|
2891
|
+
};
|
|
2892
|
+
var getVersion = (store, ruleId, version) => {
|
|
2893
|
+
const history = store.histories.get(ruleId);
|
|
2894
|
+
if (!history)
|
|
2895
|
+
return;
|
|
2896
|
+
return history.versions.find((v) => v.version === version);
|
|
2897
|
+
};
|
|
2898
|
+
var getLatestVersion = (store, ruleId) => {
|
|
2899
|
+
const history = store.histories.get(ruleId);
|
|
2900
|
+
if (!history || history.versions.length === 0)
|
|
2901
|
+
return;
|
|
2902
|
+
return history.versions[history.versions.length - 1];
|
|
2903
|
+
};
|
|
2904
|
+
var getAllVersions = (store, ruleId) => {
|
|
2905
|
+
const history = store.histories.get(ruleId);
|
|
2906
|
+
return history?.versions ?? [];
|
|
2907
|
+
};
|
|
2908
|
+
var rollbackToVersion = (store, ruleId, targetVersion, options = {}) => {
|
|
2909
|
+
const targetVersionRecord = getVersion(store, ruleId, targetVersion);
|
|
2910
|
+
if (!targetVersionRecord) {
|
|
2911
|
+
return { store, rule: undefined };
|
|
2912
|
+
}
|
|
2913
|
+
const rolledBackRule = {
|
|
2914
|
+
...targetVersionRecord.rule,
|
|
2915
|
+
updatedAt: new Date
|
|
2916
|
+
};
|
|
2917
|
+
const newStore = addVersion(store, rolledBackRule, "update", {
|
|
2918
|
+
createdBy: options.createdBy,
|
|
2919
|
+
comment: options.comment ?? `Rollback to version ${targetVersion}`
|
|
2920
|
+
});
|
|
2921
|
+
return { store: newStore, rule: rolledBackRule };
|
|
2922
|
+
};
|
|
2923
|
+
var compareVersions = (store, ruleId, version1, version2) => {
|
|
2924
|
+
const v1 = getVersion(store, ruleId, version1);
|
|
2925
|
+
const v2 = getVersion(store, ruleId, version2);
|
|
2926
|
+
if (!v1 || !v2) {
|
|
2927
|
+
return null;
|
|
2928
|
+
}
|
|
2929
|
+
const differences = [];
|
|
2930
|
+
const compareFields = [
|
|
2931
|
+
"name",
|
|
2932
|
+
"description",
|
|
2933
|
+
"priority",
|
|
2934
|
+
"enabled",
|
|
2935
|
+
"stopOnMatch",
|
|
2936
|
+
"category"
|
|
2937
|
+
];
|
|
2938
|
+
for (const field of compareFields) {
|
|
2939
|
+
const oldValue = v1.rule[field];
|
|
2940
|
+
const newValue = v2.rule[field];
|
|
2941
|
+
if (JSON.stringify(oldValue) !== JSON.stringify(newValue)) {
|
|
2942
|
+
differences.push({ field, oldValue, newValue });
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
if (JSON.stringify(v1.rule.conditions) !== JSON.stringify(v2.rule.conditions)) {
|
|
2946
|
+
differences.push({
|
|
2947
|
+
field: "conditions",
|
|
2948
|
+
oldValue: v1.rule.conditions,
|
|
2949
|
+
newValue: v2.rule.conditions
|
|
2950
|
+
});
|
|
2951
|
+
}
|
|
2952
|
+
if (JSON.stringify(v1.rule.consequences) !== JSON.stringify(v2.rule.consequences)) {
|
|
2953
|
+
differences.push({
|
|
2954
|
+
field: "consequences",
|
|
2955
|
+
oldValue: v1.rule.consequences,
|
|
2956
|
+
newValue: v2.rule.consequences
|
|
2957
|
+
});
|
|
2958
|
+
}
|
|
2959
|
+
if (JSON.stringify(v1.rule.tags) !== JSON.stringify(v2.rule.tags)) {
|
|
2960
|
+
differences.push({
|
|
2961
|
+
field: "tags",
|
|
2962
|
+
oldValue: v1.rule.tags,
|
|
2963
|
+
newValue: v2.rule.tags
|
|
2964
|
+
});
|
|
2965
|
+
}
|
|
2966
|
+
return { version1: v1, version2: v2, differences };
|
|
2967
|
+
};
|
|
2968
|
+
var getVersionsByDateRange = (store, startDate, endDate) => {
|
|
2969
|
+
const versions = [];
|
|
2970
|
+
for (const history of store.histories.values()) {
|
|
2971
|
+
for (const version of history.versions) {
|
|
2972
|
+
if (version.createdAt >= startDate && version.createdAt <= endDate) {
|
|
2973
|
+
versions.push(version);
|
|
2974
|
+
}
|
|
2975
|
+
}
|
|
2976
|
+
}
|
|
2977
|
+
return versions.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
|
|
2978
|
+
};
|
|
2979
|
+
var getVersionsByChangeType = (store, changeType) => {
|
|
2980
|
+
const versions = [];
|
|
2981
|
+
for (const history of store.histories.values()) {
|
|
2982
|
+
for (const version of history.versions) {
|
|
2983
|
+
if (version.changeType === changeType) {
|
|
2984
|
+
versions.push(version);
|
|
2985
|
+
}
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2988
|
+
return versions;
|
|
2989
|
+
};
|
|
2990
|
+
var pruneOldVersions = (store, keepCount) => {
|
|
2991
|
+
const histories = new Map;
|
|
2992
|
+
for (const [ruleId, history] of store.histories) {
|
|
2993
|
+
const prunedVersions = history.versions.length > keepCount ? history.versions.slice(-keepCount) : history.versions;
|
|
2994
|
+
histories.set(ruleId, {
|
|
2995
|
+
ruleId,
|
|
2996
|
+
currentVersion: history.currentVersion,
|
|
2997
|
+
versions: prunedVersions
|
|
2998
|
+
});
|
|
2999
|
+
}
|
|
3000
|
+
return { histories };
|
|
3001
|
+
};
|
|
3002
|
+
var formatVersionHistory = (history) => {
|
|
3003
|
+
const lines = [
|
|
3004
|
+
`=== Version History for Rule: ${history.ruleId} ===`,
|
|
3005
|
+
`Current Version: ${history.currentVersion}`,
|
|
3006
|
+
`Total Versions: ${history.versions.length}`,
|
|
3007
|
+
""
|
|
3008
|
+
];
|
|
3009
|
+
for (const version of history.versions) {
|
|
3010
|
+
lines.push(`v${version.version} - ${version.changeType} (${version.createdAt.toISOString()})`);
|
|
3011
|
+
if (version.createdBy) {
|
|
3012
|
+
lines.push(` By: ${version.createdBy}`);
|
|
3013
|
+
}
|
|
3014
|
+
if (version.comment) {
|
|
3015
|
+
lines.push(` Comment: ${version.comment}`);
|
|
3016
|
+
}
|
|
3017
|
+
}
|
|
3018
|
+
return lines.join(`
|
|
3019
|
+
`);
|
|
3020
|
+
};
|