@f-o-t/rules-engine 2.0.2 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +168 -0
- package/LICENSE.md +16 -4
- package/README.md +106 -23
- package/__tests__/builder.test.ts +363 -0
- package/__tests__/cache.test.ts +130 -0
- package/__tests__/config.test.ts +35 -0
- package/__tests__/engine.test.ts +1213 -0
- package/__tests__/evaluate.test.ts +339 -0
- package/__tests__/exports.test.ts +30 -0
- package/__tests__/filter-sort.test.ts +303 -0
- package/__tests__/integration.test.ts +419 -0
- package/__tests__/money-integration.test.ts +149 -0
- package/__tests__/validation.test.ts +862 -0
- package/biome.json +39 -0
- package/docs/MIGRATION-v3.md +118 -0
- package/fot.config.ts +5 -0
- package/package.json +31 -67
- package/src/analyzer/analysis.ts +401 -0
- package/src/builder/conditions.ts +321 -0
- package/src/builder/rule.ts +192 -0
- package/src/cache/cache.ts +135 -0
- package/src/cache/noop.ts +20 -0
- package/src/core/evaluate.ts +185 -0
- package/src/core/filter.ts +85 -0
- package/src/core/group.ts +103 -0
- package/src/core/sort.ts +90 -0
- package/src/engine/engine.ts +462 -0
- package/src/engine/hooks.ts +235 -0
- package/src/engine/state.ts +322 -0
- package/src/index.ts +303 -0
- package/src/optimizer/index-builder.ts +381 -0
- package/src/serialization/serializer.ts +408 -0
- package/src/simulation/simulator.ts +359 -0
- package/src/types/config.ts +184 -0
- package/src/types/consequence.ts +38 -0
- package/src/types/evaluation.ts +87 -0
- package/src/types/rule.ts +112 -0
- package/src/types/state.ts +116 -0
- package/src/utils/conditions.ts +108 -0
- package/src/utils/hash.ts +30 -0
- package/src/utils/id.ts +6 -0
- package/src/utils/time.ts +42 -0
- package/src/validation/conflicts.ts +440 -0
- package/src/validation/integrity.ts +473 -0
- package/src/validation/schema.ts +386 -0
- package/src/versioning/version-store.ts +337 -0
- package/tsconfig.json +29 -0
- package/dist/index.cjs +0 -3088
- package/dist/index.d.cts +0 -1173
- package/dist/index.d.ts +0 -1173
- package/dist/index.js +0 -3072
package/biome.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
|
3
|
+
"vcs": {
|
|
4
|
+
"enabled": true,
|
|
5
|
+
"clientKind": "git",
|
|
6
|
+
"useIgnoreFile": true,
|
|
7
|
+
"defaultBranch": "main"
|
|
8
|
+
},
|
|
9
|
+
"files": {
|
|
10
|
+
"ignoreUnknown": false,
|
|
11
|
+
"ignore": [
|
|
12
|
+
"node_modules",
|
|
13
|
+
"dist",
|
|
14
|
+
"*.config.ts"
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
"formatter": {
|
|
18
|
+
"enabled": true,
|
|
19
|
+
"indentStyle": "tab",
|
|
20
|
+
"indentWidth": 2,
|
|
21
|
+
"lineWidth": 80
|
|
22
|
+
},
|
|
23
|
+
"organizeImports": {
|
|
24
|
+
"enabled": true
|
|
25
|
+
},
|
|
26
|
+
"linter": {
|
|
27
|
+
"enabled": true,
|
|
28
|
+
"rules": {
|
|
29
|
+
"recommended": true
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"javascript": {
|
|
33
|
+
"formatter": {
|
|
34
|
+
"quoteStyle": "double",
|
|
35
|
+
"semicolons": "always",
|
|
36
|
+
"trailingCommas": "all"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Migration Guide: v2.x → v3.0
|
|
2
|
+
|
|
3
|
+
## Breaking Changes
|
|
4
|
+
|
|
5
|
+
### Engine requires evaluator configuration
|
|
6
|
+
|
|
7
|
+
**What changed:**
|
|
8
|
+
The engine now requires explicit evaluator configuration to enable custom operators.
|
|
9
|
+
|
|
10
|
+
**Before (v2.x):**
|
|
11
|
+
```typescript
|
|
12
|
+
import { createEngine } from "@f-o-t/rules-engine";
|
|
13
|
+
|
|
14
|
+
const engine = createEngine({
|
|
15
|
+
consequences: MyConsequences,
|
|
16
|
+
});
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
**After (v3.x):**
|
|
20
|
+
```typescript
|
|
21
|
+
import { createEngine, createEvaluator } from "@f-o-t/rules-engine";
|
|
22
|
+
|
|
23
|
+
// Option 1: Built-in operators only
|
|
24
|
+
const engine = createEngine({
|
|
25
|
+
consequences: MyConsequences,
|
|
26
|
+
evaluator: createEvaluator(),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Option 2: With custom operators
|
|
30
|
+
import { moneyOperators } from "@f-o-t/money/operators";
|
|
31
|
+
|
|
32
|
+
const engine = createEngine({
|
|
33
|
+
consequences: MyConsequences,
|
|
34
|
+
evaluator: createEvaluator({ operators: moneyOperators }),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Option 3: Convenience (engine creates evaluator)
|
|
38
|
+
const engine = createEngine({
|
|
39
|
+
consequences: MyConsequences,
|
|
40
|
+
operators: moneyOperators,
|
|
41
|
+
});
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Internal API changes
|
|
45
|
+
|
|
46
|
+
If you were using `evaluateRule` or `evaluateRules` directly (not through the engine), these now require an evaluator parameter:
|
|
47
|
+
|
|
48
|
+
**Before:**
|
|
49
|
+
```typescript
|
|
50
|
+
import { evaluateRule } from "@f-o-t/rules-engine";
|
|
51
|
+
|
|
52
|
+
const result = evaluateRule(rule, context);
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**After:**
|
|
56
|
+
```typescript
|
|
57
|
+
import { evaluateRule, createEvaluator } from "@f-o-t/rules-engine";
|
|
58
|
+
|
|
59
|
+
const evaluator = createEvaluator();
|
|
60
|
+
const result = evaluateRule(rule, context, evaluator);
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## New Features
|
|
64
|
+
|
|
65
|
+
### Custom operators in rules
|
|
66
|
+
|
|
67
|
+
You can now use custom operators from any library:
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
import { createEngine, createEvaluator } from "@f-o-t/rules-engine";
|
|
71
|
+
import { moneyOperators } from "@f-o-t/money/operators";
|
|
72
|
+
|
|
73
|
+
const engine = createEngine({
|
|
74
|
+
operators: moneyOperators,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
engine.addRule({
|
|
78
|
+
name: "high-value-transaction",
|
|
79
|
+
conditions: {
|
|
80
|
+
id: "g1",
|
|
81
|
+
operator: "AND",
|
|
82
|
+
conditions: [
|
|
83
|
+
{
|
|
84
|
+
id: "c1",
|
|
85
|
+
type: "custom",
|
|
86
|
+
field: "transactionAmount",
|
|
87
|
+
operator: "money_gt",
|
|
88
|
+
value: { amount: "1000.00", currency: "BRL" },
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
consequences: [
|
|
93
|
+
{ type: "require_approval", payload: { level: "manager" } },
|
|
94
|
+
],
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Compose multiple operator sets
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
import { moneyOperators } from "@f-o-t/money/operators";
|
|
102
|
+
import { dateOperators } from "./my-date-operators";
|
|
103
|
+
|
|
104
|
+
const engine = createEngine({
|
|
105
|
+
operators: {
|
|
106
|
+
...moneyOperators,
|
|
107
|
+
...dateOperators,
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Why this change?
|
|
113
|
+
|
|
114
|
+
This breaking change enables:
|
|
115
|
+
1. **Custom operators** - Use domain-specific operators like `money_gt`, `money_between`
|
|
116
|
+
2. **Type safety** - Better TypeScript support for custom operator types
|
|
117
|
+
3. **Extensibility** - Easier to add new operator types
|
|
118
|
+
4. **Consistency** - Aligns with condition-evaluator's plugin architecture
|
package/fot.config.ts
ADDED
package/package.json
CHANGED
|
@@ -1,69 +1,33 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
"files": [
|
|
34
|
-
"dist"
|
|
35
|
-
],
|
|
36
|
-
"homepage": "https://github.com/F-O-T/montte-nx/blob/master/libraries/rules-engine",
|
|
37
|
-
"license": "MIT",
|
|
38
|
-
"module": "./dist/index.js",
|
|
39
|
-
"name": "@f-o-t/rules-engine",
|
|
40
|
-
"peerDependencies": {
|
|
41
|
-
"typescript": ">=4.5.0"
|
|
42
|
-
},
|
|
43
|
-
"peerDependenciesMeta": {
|
|
44
|
-
"typescript": {
|
|
45
|
-
"optional": true
|
|
46
|
-
}
|
|
47
|
-
},
|
|
48
|
-
"private": false,
|
|
49
|
-
"publishConfig": {
|
|
50
|
-
"access": "public"
|
|
51
|
-
},
|
|
52
|
-
"repository": {
|
|
53
|
-
"type": "git",
|
|
54
|
-
"url": "https://github.com/F-O-T/montte-nx.git"
|
|
55
|
-
},
|
|
56
|
-
"scripts": {
|
|
57
|
-
"build": "bunup",
|
|
58
|
-
"check": "biome check --write .",
|
|
59
|
-
"dev": "bunup --watch",
|
|
60
|
-
"release": "bumpp --commit --push --tag",
|
|
61
|
-
"test": "bun test",
|
|
62
|
-
"test:coverage": "bun test --coverage",
|
|
63
|
-
"test:watch": "bun test --watch",
|
|
64
|
-
"typecheck": "tsc"
|
|
65
|
-
},
|
|
66
|
-
"type": "module",
|
|
67
|
-
"types": "./dist/index.d.ts",
|
|
68
|
-
"version": "2.0.2"
|
|
2
|
+
"name": "@f-o-t/rules-engine",
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "bun x --bun fot build",
|
|
15
|
+
"test": "bun x --bun fot test",
|
|
16
|
+
"lint": "bun x --bun fot lint",
|
|
17
|
+
"format": "bun x --bun fot format",
|
|
18
|
+
"typecheck": "bun x --bun fot typecheck"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@f-o-t/condition-evaluator": "^2.0.2",
|
|
22
|
+
"zod": "^4.3.6"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@f-o-t/cli": "^1.0.0",
|
|
26
|
+
"@f-o-t/config": "^1.0.0"
|
|
27
|
+
},
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/F-O-T/libraries.git",
|
|
31
|
+
"directory": "libraries/rules-engine"
|
|
32
|
+
}
|
|
69
33
|
}
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Condition,
|
|
3
|
+
type ConditionGroup,
|
|
4
|
+
isConditionGroup,
|
|
5
|
+
} from "@f-o-t/condition-evaluator";
|
|
6
|
+
import type {
|
|
7
|
+
ConsequenceDefinitions,
|
|
8
|
+
DefaultConsequences,
|
|
9
|
+
} from "../types/consequence";
|
|
10
|
+
import type { Rule } from "../types/rule";
|
|
11
|
+
import {
|
|
12
|
+
calculateMaxDepth,
|
|
13
|
+
collectConditionFields,
|
|
14
|
+
countConditionGroups,
|
|
15
|
+
countConditions,
|
|
16
|
+
} from "../utils/conditions";
|
|
17
|
+
|
|
18
|
+
export type RuleComplexity = {
|
|
19
|
+
readonly ruleId: string;
|
|
20
|
+
readonly ruleName: string;
|
|
21
|
+
readonly totalConditions: number;
|
|
22
|
+
readonly maxDepth: number;
|
|
23
|
+
readonly groupCount: number;
|
|
24
|
+
readonly uniqueFields: number;
|
|
25
|
+
readonly uniqueOperators: number;
|
|
26
|
+
readonly consequenceCount: number;
|
|
27
|
+
readonly complexityScore: number;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type RuleSetAnalysis = {
|
|
31
|
+
readonly ruleCount: number;
|
|
32
|
+
readonly enabledCount: number;
|
|
33
|
+
readonly disabledCount: number;
|
|
34
|
+
readonly totalConditions: number;
|
|
35
|
+
readonly totalConsequences: number;
|
|
36
|
+
readonly uniqueFields: ReadonlyArray<string>;
|
|
37
|
+
readonly uniqueOperators: ReadonlyArray<string>;
|
|
38
|
+
readonly uniqueConsequenceTypes: ReadonlyArray<string>;
|
|
39
|
+
readonly uniqueCategories: ReadonlyArray<string>;
|
|
40
|
+
readonly uniqueTags: ReadonlyArray<string>;
|
|
41
|
+
readonly priorityRange: { min: number; max: number };
|
|
42
|
+
readonly averageComplexity: number;
|
|
43
|
+
readonly complexityDistribution: {
|
|
44
|
+
readonly low: number;
|
|
45
|
+
readonly medium: number;
|
|
46
|
+
readonly high: number;
|
|
47
|
+
};
|
|
48
|
+
readonly ruleComplexities: ReadonlyArray<RuleComplexity>;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type FieldUsage = {
|
|
52
|
+
readonly field: string;
|
|
53
|
+
readonly count: number;
|
|
54
|
+
readonly types: ReadonlyArray<string>;
|
|
55
|
+
readonly operators: ReadonlyArray<string>;
|
|
56
|
+
readonly rules: ReadonlyArray<{ id: string; name: string }>;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export type OperatorUsage = {
|
|
60
|
+
readonly operator: string;
|
|
61
|
+
readonly type: string;
|
|
62
|
+
readonly count: number;
|
|
63
|
+
readonly rules: ReadonlyArray<{ id: string; name: string }>;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export type ConsequenceUsage = {
|
|
67
|
+
readonly type: string;
|
|
68
|
+
readonly count: number;
|
|
69
|
+
readonly rules: ReadonlyArray<{ id: string; name: string }>;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const collectOperators = (
|
|
73
|
+
condition: Condition | ConditionGroup,
|
|
74
|
+
): Set<string> => {
|
|
75
|
+
const operators = new Set<string>();
|
|
76
|
+
|
|
77
|
+
const traverse = (c: Condition | ConditionGroup) => {
|
|
78
|
+
if (isConditionGroup(c)) {
|
|
79
|
+
operators.add(c.operator);
|
|
80
|
+
for (const child of c.conditions) {
|
|
81
|
+
traverse(child as Condition | ConditionGroup);
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
operators.add(c.operator);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
traverse(condition);
|
|
89
|
+
return operators;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const calculateComplexityScore = (
|
|
93
|
+
totalConditions: number,
|
|
94
|
+
maxDepth: number,
|
|
95
|
+
groupCount: number,
|
|
96
|
+
uniqueFields: number,
|
|
97
|
+
): number => {
|
|
98
|
+
return (
|
|
99
|
+
totalConditions * 1 + maxDepth * 2 + groupCount * 1.5 + uniqueFields * 0.5
|
|
100
|
+
);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export const analyzeRuleComplexity = <
|
|
104
|
+
TContext = unknown,
|
|
105
|
+
TConsequences extends ConsequenceDefinitions = DefaultConsequences,
|
|
106
|
+
>(
|
|
107
|
+
rule: Rule<TContext, TConsequences>,
|
|
108
|
+
): RuleComplexity => {
|
|
109
|
+
const totalConditions = countConditions(rule.conditions);
|
|
110
|
+
const maxDepth = calculateMaxDepth(rule.conditions);
|
|
111
|
+
const groupCount = countConditionGroups(rule.conditions);
|
|
112
|
+
const uniqueFields = collectConditionFields(rule.conditions).size;
|
|
113
|
+
const uniqueOperators = collectOperators(rule.conditions).size;
|
|
114
|
+
const consequenceCount = rule.consequences.length;
|
|
115
|
+
|
|
116
|
+
const complexityScore = calculateComplexityScore(
|
|
117
|
+
totalConditions,
|
|
118
|
+
maxDepth,
|
|
119
|
+
groupCount,
|
|
120
|
+
uniqueFields,
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
ruleId: rule.id,
|
|
125
|
+
ruleName: rule.name,
|
|
126
|
+
totalConditions,
|
|
127
|
+
maxDepth,
|
|
128
|
+
groupCount,
|
|
129
|
+
uniqueFields,
|
|
130
|
+
uniqueOperators,
|
|
131
|
+
consequenceCount,
|
|
132
|
+
complexityScore,
|
|
133
|
+
};
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
export const analyzeRuleSet = <
|
|
137
|
+
TContext = unknown,
|
|
138
|
+
TConsequences extends ConsequenceDefinitions = DefaultConsequences,
|
|
139
|
+
>(
|
|
140
|
+
rules: ReadonlyArray<Rule<TContext, TConsequences>>,
|
|
141
|
+
): RuleSetAnalysis => {
|
|
142
|
+
const complexities = rules.map(analyzeRuleComplexity);
|
|
143
|
+
|
|
144
|
+
const allFields = new Set<string>();
|
|
145
|
+
const allOperators = new Set<string>();
|
|
146
|
+
const allConsequenceTypes = new Set<string>();
|
|
147
|
+
const allCategories = new Set<string>();
|
|
148
|
+
const allTags = new Set<string>();
|
|
149
|
+
|
|
150
|
+
let totalConditions = 0;
|
|
151
|
+
let totalConsequences = 0;
|
|
152
|
+
let minPriority = Number.POSITIVE_INFINITY;
|
|
153
|
+
let maxPriority = Number.NEGATIVE_INFINITY;
|
|
154
|
+
let enabledCount = 0;
|
|
155
|
+
|
|
156
|
+
for (const rule of rules) {
|
|
157
|
+
totalConditions += countConditions(rule.conditions);
|
|
158
|
+
totalConsequences += rule.consequences.length;
|
|
159
|
+
|
|
160
|
+
if (rule.priority < minPriority) minPriority = rule.priority;
|
|
161
|
+
if (rule.priority > maxPriority) maxPriority = rule.priority;
|
|
162
|
+
|
|
163
|
+
if (rule.enabled) enabledCount++;
|
|
164
|
+
|
|
165
|
+
for (const field of collectConditionFields(rule.conditions)) {
|
|
166
|
+
allFields.add(field);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
for (const operator of collectOperators(rule.conditions)) {
|
|
170
|
+
allOperators.add(operator);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
for (const consequence of rule.consequences) {
|
|
174
|
+
allConsequenceTypes.add(consequence.type as string);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (rule.category) allCategories.add(rule.category);
|
|
178
|
+
for (const tag of rule.tags) allTags.add(tag);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const averageComplexity =
|
|
182
|
+
complexities.length > 0
|
|
183
|
+
? complexities.reduce((sum, c) => sum + c.complexityScore, 0) /
|
|
184
|
+
complexities.length
|
|
185
|
+
: 0;
|
|
186
|
+
|
|
187
|
+
const complexityDistribution = {
|
|
188
|
+
low: complexities.filter((c) => c.complexityScore < 5).length,
|
|
189
|
+
medium: complexities.filter(
|
|
190
|
+
(c) => c.complexityScore >= 5 && c.complexityScore < 15,
|
|
191
|
+
).length,
|
|
192
|
+
high: complexities.filter((c) => c.complexityScore >= 15).length,
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
ruleCount: rules.length,
|
|
197
|
+
enabledCount,
|
|
198
|
+
disabledCount: rules.length - enabledCount,
|
|
199
|
+
totalConditions,
|
|
200
|
+
totalConsequences,
|
|
201
|
+
uniqueFields: [...allFields].sort(),
|
|
202
|
+
uniqueOperators: [...allOperators].sort(),
|
|
203
|
+
uniqueConsequenceTypes: [...allConsequenceTypes].sort(),
|
|
204
|
+
uniqueCategories: [...allCategories].sort(),
|
|
205
|
+
uniqueTags: [...allTags].sort(),
|
|
206
|
+
priorityRange: {
|
|
207
|
+
min: minPriority === Number.POSITIVE_INFINITY ? 0 : minPriority,
|
|
208
|
+
max: maxPriority === Number.NEGATIVE_INFINITY ? 0 : maxPriority,
|
|
209
|
+
},
|
|
210
|
+
averageComplexity,
|
|
211
|
+
complexityDistribution,
|
|
212
|
+
ruleComplexities: complexities,
|
|
213
|
+
};
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
export const analyzeFieldUsage = <
|
|
217
|
+
TContext = unknown,
|
|
218
|
+
TConsequences extends ConsequenceDefinitions = DefaultConsequences,
|
|
219
|
+
>(
|
|
220
|
+
rules: ReadonlyArray<Rule<TContext, TConsequences>>,
|
|
221
|
+
): ReadonlyArray<FieldUsage> => {
|
|
222
|
+
const fieldMap = new Map<
|
|
223
|
+
string,
|
|
224
|
+
{
|
|
225
|
+
types: Set<string>;
|
|
226
|
+
operators: Set<string>;
|
|
227
|
+
rules: Array<{ id: string; name: string }>;
|
|
228
|
+
}
|
|
229
|
+
>();
|
|
230
|
+
|
|
231
|
+
for (const rule of rules) {
|
|
232
|
+
const traverse = (c: Condition | ConditionGroup) => {
|
|
233
|
+
if (isConditionGroup(c)) {
|
|
234
|
+
for (const child of c.conditions) {
|
|
235
|
+
traverse(child as Condition | ConditionGroup);
|
|
236
|
+
}
|
|
237
|
+
} else {
|
|
238
|
+
const existing = fieldMap.get(c.field) ?? {
|
|
239
|
+
types: new Set(),
|
|
240
|
+
operators: new Set(),
|
|
241
|
+
rules: [],
|
|
242
|
+
};
|
|
243
|
+
existing.types.add(c.type);
|
|
244
|
+
existing.operators.add(c.operator);
|
|
245
|
+
if (!existing.rules.some((r) => r.id === rule.id)) {
|
|
246
|
+
existing.rules.push({ id: rule.id, name: rule.name });
|
|
247
|
+
}
|
|
248
|
+
fieldMap.set(c.field, existing);
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
traverse(rule.conditions);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return [...fieldMap.entries()]
|
|
256
|
+
.map(([field, data]) => ({
|
|
257
|
+
field,
|
|
258
|
+
count: data.rules.length,
|
|
259
|
+
types: [...data.types].sort(),
|
|
260
|
+
operators: [...data.operators].sort(),
|
|
261
|
+
rules: data.rules,
|
|
262
|
+
}))
|
|
263
|
+
.sort((a, b) => b.count - a.count);
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
export const analyzeOperatorUsage = <
|
|
267
|
+
TContext = unknown,
|
|
268
|
+
TConsequences extends ConsequenceDefinitions = DefaultConsequences,
|
|
269
|
+
>(
|
|
270
|
+
rules: ReadonlyArray<Rule<TContext, TConsequences>>,
|
|
271
|
+
): ReadonlyArray<OperatorUsage> => {
|
|
272
|
+
const operatorMap = new Map<
|
|
273
|
+
string,
|
|
274
|
+
{ type: string; rules: Array<{ id: string; name: string }> }
|
|
275
|
+
>();
|
|
276
|
+
|
|
277
|
+
for (const rule of rules) {
|
|
278
|
+
const traverse = (c: Condition | ConditionGroup) => {
|
|
279
|
+
if (isConditionGroup(c)) {
|
|
280
|
+
for (const child of c.conditions) {
|
|
281
|
+
traverse(child as Condition | ConditionGroup);
|
|
282
|
+
}
|
|
283
|
+
} else {
|
|
284
|
+
const key = `${c.type}:${c.operator}`;
|
|
285
|
+
const existing = operatorMap.get(key) ?? {
|
|
286
|
+
type: c.type,
|
|
287
|
+
rules: [] as Array<{ id: string; name: string }>,
|
|
288
|
+
};
|
|
289
|
+
if (!existing.rules.some((r) => r.id === rule.id)) {
|
|
290
|
+
existing.rules.push({ id: rule.id, name: rule.name });
|
|
291
|
+
}
|
|
292
|
+
operatorMap.set(key, existing);
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
traverse(rule.conditions);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return [...operatorMap.entries()]
|
|
300
|
+
.map(([key, data]) => {
|
|
301
|
+
const parts = key.split(":");
|
|
302
|
+
return {
|
|
303
|
+
operator: parts[1] ?? "",
|
|
304
|
+
type: data.type,
|
|
305
|
+
count: data.rules.length,
|
|
306
|
+
rules: data.rules,
|
|
307
|
+
};
|
|
308
|
+
})
|
|
309
|
+
.sort((a, b) => b.count - a.count);
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
export const analyzeConsequenceUsage = <
|
|
313
|
+
TContext = unknown,
|
|
314
|
+
TConsequences extends ConsequenceDefinitions = DefaultConsequences,
|
|
315
|
+
>(
|
|
316
|
+
rules: ReadonlyArray<Rule<TContext, TConsequences>>,
|
|
317
|
+
): ReadonlyArray<ConsequenceUsage> => {
|
|
318
|
+
const consequenceMap = new Map<
|
|
319
|
+
string,
|
|
320
|
+
Array<{ id: string; name: string }>
|
|
321
|
+
>();
|
|
322
|
+
|
|
323
|
+
for (const rule of rules) {
|
|
324
|
+
for (const consequence of rule.consequences) {
|
|
325
|
+
const type = consequence.type as string;
|
|
326
|
+
const existing = consequenceMap.get(type) ?? [];
|
|
327
|
+
if (!existing.some((r) => r.id === rule.id)) {
|
|
328
|
+
existing.push({ id: rule.id, name: rule.name });
|
|
329
|
+
}
|
|
330
|
+
consequenceMap.set(type, existing);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return [...consequenceMap.entries()]
|
|
335
|
+
.map(([type, rules]) => ({
|
|
336
|
+
type,
|
|
337
|
+
count: rules.length,
|
|
338
|
+
rules,
|
|
339
|
+
}))
|
|
340
|
+
.sort((a, b) => b.count - a.count);
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
export const findMostComplexRules = <
|
|
344
|
+
TContext = unknown,
|
|
345
|
+
TConsequences extends ConsequenceDefinitions = DefaultConsequences,
|
|
346
|
+
>(
|
|
347
|
+
rules: ReadonlyArray<Rule<TContext, TConsequences>>,
|
|
348
|
+
limit = 10,
|
|
349
|
+
): ReadonlyArray<RuleComplexity> => {
|
|
350
|
+
return rules
|
|
351
|
+
.map(analyzeRuleComplexity)
|
|
352
|
+
.sort((a, b) => b.complexityScore - a.complexityScore)
|
|
353
|
+
.slice(0, limit);
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
export const findLeastUsedFields = <
|
|
357
|
+
TContext = unknown,
|
|
358
|
+
TConsequences extends ConsequenceDefinitions = DefaultConsequences,
|
|
359
|
+
>(
|
|
360
|
+
rules: ReadonlyArray<Rule<TContext, TConsequences>>,
|
|
361
|
+
limit = 10,
|
|
362
|
+
): ReadonlyArray<FieldUsage> => {
|
|
363
|
+
return [...analyzeFieldUsage(rules)]
|
|
364
|
+
.sort((a, b) => a.count - b.count)
|
|
365
|
+
.slice(0, limit);
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
export const formatRuleSetAnalysis = (analysis: RuleSetAnalysis): string => {
|
|
369
|
+
const lines: string[] = [
|
|
370
|
+
"=== Rule Set Analysis ===",
|
|
371
|
+
"",
|
|
372
|
+
`Rules: ${analysis.ruleCount} (${analysis.enabledCount} enabled, ${analysis.disabledCount} disabled)`,
|
|
373
|
+
`Total Conditions: ${analysis.totalConditions}`,
|
|
374
|
+
`Total Consequences: ${analysis.totalConsequences}`,
|
|
375
|
+
"",
|
|
376
|
+
`Unique Fields: ${analysis.uniqueFields.length}`,
|
|
377
|
+
` ${analysis.uniqueFields.join(", ") || "(none)"}`,
|
|
378
|
+
"",
|
|
379
|
+
`Unique Operators: ${analysis.uniqueOperators.length}`,
|
|
380
|
+
` ${analysis.uniqueOperators.join(", ") || "(none)"}`,
|
|
381
|
+
"",
|
|
382
|
+
`Consequence Types: ${analysis.uniqueConsequenceTypes.length}`,
|
|
383
|
+
` ${analysis.uniqueConsequenceTypes.join(", ") || "(none)"}`,
|
|
384
|
+
"",
|
|
385
|
+
`Categories: ${analysis.uniqueCategories.length}`,
|
|
386
|
+
` ${analysis.uniqueCategories.join(", ") || "(none)"}`,
|
|
387
|
+
"",
|
|
388
|
+
`Tags: ${analysis.uniqueTags.length}`,
|
|
389
|
+
` ${analysis.uniqueTags.join(", ") || "(none)"}`,
|
|
390
|
+
"",
|
|
391
|
+
`Priority Range: ${analysis.priorityRange.min} - ${analysis.priorityRange.max}`,
|
|
392
|
+
"",
|
|
393
|
+
`Average Complexity: ${analysis.averageComplexity.toFixed(2)}`,
|
|
394
|
+
`Complexity Distribution:`,
|
|
395
|
+
` Low (< 5): ${analysis.complexityDistribution.low}`,
|
|
396
|
+
` Medium (5-15): ${analysis.complexityDistribution.medium}`,
|
|
397
|
+
` High (> 15): ${analysis.complexityDistribution.high}`,
|
|
398
|
+
];
|
|
399
|
+
|
|
400
|
+
return lines.join("\n");
|
|
401
|
+
};
|