@f-o-t/rules-engine 2.0.2 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +168 -0
- package/LICENSE.md +16 -4
- package/README.md +106 -23
- package/__tests__/builder.test.ts +363 -0
- package/__tests__/cache.test.ts +130 -0
- package/__tests__/config.test.ts +35 -0
- package/__tests__/engine.test.ts +1213 -0
- package/__tests__/evaluate.test.ts +339 -0
- package/__tests__/exports.test.ts +30 -0
- package/__tests__/filter-sort.test.ts +303 -0
- package/__tests__/integration.test.ts +419 -0
- package/__tests__/money-integration.test.ts +149 -0
- package/__tests__/validation.test.ts +862 -0
- package/biome.json +39 -0
- package/docs/MIGRATION-v3.md +118 -0
- package/fot.config.ts +5 -0
- package/package.json +31 -67
- package/src/analyzer/analysis.ts +401 -0
- package/src/builder/conditions.ts +321 -0
- package/src/builder/rule.ts +192 -0
- package/src/cache/cache.ts +135 -0
- package/src/cache/noop.ts +20 -0
- package/src/core/evaluate.ts +185 -0
- package/src/core/filter.ts +85 -0
- package/src/core/group.ts +103 -0
- package/src/core/sort.ts +90 -0
- package/src/engine/engine.ts +462 -0
- package/src/engine/hooks.ts +235 -0
- package/src/engine/state.ts +322 -0
- package/src/index.ts +303 -0
- package/src/optimizer/index-builder.ts +381 -0
- package/src/serialization/serializer.ts +408 -0
- package/src/simulation/simulator.ts +359 -0
- package/src/types/config.ts +184 -0
- package/src/types/consequence.ts +38 -0
- package/src/types/evaluation.ts +87 -0
- package/src/types/rule.ts +112 -0
- package/src/types/state.ts +116 -0
- package/src/utils/conditions.ts +108 -0
- package/src/utils/hash.ts +30 -0
- package/src/utils/id.ts +6 -0
- package/src/utils/time.ts +42 -0
- package/src/validation/conflicts.ts +440 -0
- package/src/validation/integrity.ts +473 -0
- package/src/validation/schema.ts +386 -0
- package/src/versioning/version-store.ts +337 -0
- package/tsconfig.json +29 -0
- package/dist/index.cjs +0 -3088
- package/dist/index.d.cts +0 -1173
- package/dist/index.d.ts +0 -1173
- package/dist/index.js +0 -3072
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import { createEvaluator } from "@f-o-t/condition-evaluator";
|
|
2
|
+
import { evaluateRule, evaluateRules } from "../core/evaluate";
|
|
3
|
+
import type {
|
|
4
|
+
ConsequenceDefinitions,
|
|
5
|
+
DefaultConsequences,
|
|
6
|
+
} from "../types/consequence";
|
|
7
|
+
import type {
|
|
8
|
+
EvaluationContext,
|
|
9
|
+
RuleEvaluationResult,
|
|
10
|
+
} from "../types/evaluation";
|
|
11
|
+
import type { Rule } from "../types/rule";
|
|
12
|
+
|
|
13
|
+
export type SimulationContext<TContext = unknown> = {
|
|
14
|
+
readonly data: TContext;
|
|
15
|
+
readonly metadata?: Record<string, unknown>;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type SimulationResult<
|
|
19
|
+
TContext = unknown,
|
|
20
|
+
TConsequences extends ConsequenceDefinitions = DefaultConsequences,
|
|
21
|
+
> = {
|
|
22
|
+
readonly context: SimulationContext<TContext>;
|
|
23
|
+
readonly matchedRules: ReadonlyArray<
|
|
24
|
+
RuleEvaluationResult<TContext, TConsequences>
|
|
25
|
+
>;
|
|
26
|
+
readonly unmatchedRules: ReadonlyArray<{
|
|
27
|
+
ruleId: string;
|
|
28
|
+
ruleName: string;
|
|
29
|
+
reason: string;
|
|
30
|
+
}>;
|
|
31
|
+
readonly consequences: ReadonlyArray<{
|
|
32
|
+
type: keyof TConsequences;
|
|
33
|
+
payload: unknown;
|
|
34
|
+
ruleId: string;
|
|
35
|
+
ruleName: string;
|
|
36
|
+
}>;
|
|
37
|
+
readonly executionTimeMs: number;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type WhatIfResult<
|
|
41
|
+
TContext = unknown,
|
|
42
|
+
TConsequences extends ConsequenceDefinitions = DefaultConsequences,
|
|
43
|
+
> = {
|
|
44
|
+
readonly original: SimulationResult<TContext, TConsequences>;
|
|
45
|
+
readonly modified: SimulationResult<TContext, TConsequences>;
|
|
46
|
+
readonly differences: {
|
|
47
|
+
readonly newMatches: ReadonlyArray<string>;
|
|
48
|
+
readonly lostMatches: ReadonlyArray<string>;
|
|
49
|
+
readonly consequenceChanges: ReadonlyArray<{
|
|
50
|
+
type: "added" | "removed" | "modified";
|
|
51
|
+
consequenceType: string;
|
|
52
|
+
ruleId: string;
|
|
53
|
+
}>;
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export type BatchSimulationResult<
|
|
58
|
+
TContext = unknown,
|
|
59
|
+
TConsequences extends ConsequenceDefinitions = DefaultConsequences,
|
|
60
|
+
> = {
|
|
61
|
+
readonly results: ReadonlyArray<SimulationResult<TContext, TConsequences>>;
|
|
62
|
+
readonly summary: {
|
|
63
|
+
readonly totalContexts: number;
|
|
64
|
+
readonly averageMatchedRules: number;
|
|
65
|
+
readonly ruleMatchFrequency: ReadonlyMap<string, number>;
|
|
66
|
+
readonly consequenceFrequency: ReadonlyMap<string, number>;
|
|
67
|
+
readonly averageExecutionTimeMs: number;
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const toEvaluationContext = <TContext>(
|
|
72
|
+
simContext: SimulationContext<TContext>,
|
|
73
|
+
): EvaluationContext<TContext> => ({
|
|
74
|
+
data: simContext.data,
|
|
75
|
+
timestamp: new Date(),
|
|
76
|
+
metadata: simContext.metadata,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
export const simulate = <
|
|
80
|
+
TContext = unknown,
|
|
81
|
+
TConsequences extends ConsequenceDefinitions = DefaultConsequences,
|
|
82
|
+
>(
|
|
83
|
+
rules: ReadonlyArray<Rule<TContext, TConsequences>>,
|
|
84
|
+
context: SimulationContext<TContext>,
|
|
85
|
+
evaluator: ReturnType<typeof createEvaluator> = createEvaluator(),
|
|
86
|
+
): SimulationResult<TContext, TConsequences> => {
|
|
87
|
+
const startTime = performance.now();
|
|
88
|
+
|
|
89
|
+
const enabledRules = rules.filter((r) => r.enabled);
|
|
90
|
+
const evalContext = toEvaluationContext(context);
|
|
91
|
+
const results = evaluateRules(enabledRules, evalContext, evaluator);
|
|
92
|
+
|
|
93
|
+
const matchedRules = results.results.filter((r) => r.matched);
|
|
94
|
+
const unmatchedRules: Array<{
|
|
95
|
+
ruleId: string;
|
|
96
|
+
ruleName: string;
|
|
97
|
+
reason: string;
|
|
98
|
+
}> = [];
|
|
99
|
+
|
|
100
|
+
for (const result of results.results) {
|
|
101
|
+
if (!result.matched) {
|
|
102
|
+
const reason = result.skipped
|
|
103
|
+
? (result.skipReason ?? "Skipped")
|
|
104
|
+
: result.error
|
|
105
|
+
? `Error: ${result.error.message}`
|
|
106
|
+
: "Conditions not met";
|
|
107
|
+
unmatchedRules.push({
|
|
108
|
+
ruleId: result.ruleId,
|
|
109
|
+
ruleName: result.ruleName,
|
|
110
|
+
reason,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const consequences: Array<{
|
|
116
|
+
type: keyof TConsequences;
|
|
117
|
+
payload: unknown;
|
|
118
|
+
ruleId: string;
|
|
119
|
+
ruleName: string;
|
|
120
|
+
}> = [];
|
|
121
|
+
|
|
122
|
+
for (const match of matchedRules) {
|
|
123
|
+
for (const consequence of match.consequences) {
|
|
124
|
+
consequences.push({
|
|
125
|
+
type: consequence.type,
|
|
126
|
+
payload: consequence.payload,
|
|
127
|
+
ruleId: consequence.ruleId,
|
|
128
|
+
ruleName: consequence.ruleName ?? match.ruleName,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const executionTimeMs = performance.now() - startTime;
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
context,
|
|
137
|
+
matchedRules,
|
|
138
|
+
unmatchedRules,
|
|
139
|
+
consequences,
|
|
140
|
+
executionTimeMs,
|
|
141
|
+
};
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
export const simulateSingleRule = <
|
|
145
|
+
TContext = unknown,
|
|
146
|
+
TConsequences extends ConsequenceDefinitions = DefaultConsequences,
|
|
147
|
+
>(
|
|
148
|
+
rule: Rule<TContext, TConsequences>,
|
|
149
|
+
context: SimulationContext<TContext>,
|
|
150
|
+
evaluator: ReturnType<typeof createEvaluator> = createEvaluator(),
|
|
151
|
+
): {
|
|
152
|
+
matched: boolean;
|
|
153
|
+
conditionResult: unknown;
|
|
154
|
+
consequences: ReadonlyArray<{ type: keyof TConsequences; payload: unknown }>;
|
|
155
|
+
} => {
|
|
156
|
+
const evalContext = toEvaluationContext(context);
|
|
157
|
+
const result = evaluateRule(rule, evalContext, evaluator);
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
matched: result.matched,
|
|
161
|
+
conditionResult: result.conditionResult,
|
|
162
|
+
consequences: result.matched
|
|
163
|
+
? rule.consequences.map((c) => ({ type: c.type, payload: c.payload }))
|
|
164
|
+
: [],
|
|
165
|
+
};
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
export const whatIf = <
|
|
169
|
+
TContext = unknown,
|
|
170
|
+
TConsequences extends ConsequenceDefinitions = DefaultConsequences,
|
|
171
|
+
>(
|
|
172
|
+
originalRules: ReadonlyArray<Rule<TContext, TConsequences>>,
|
|
173
|
+
modifiedRules: ReadonlyArray<Rule<TContext, TConsequences>>,
|
|
174
|
+
context: SimulationContext<TContext>,
|
|
175
|
+
evaluator: ReturnType<typeof createEvaluator> = createEvaluator(),
|
|
176
|
+
): WhatIfResult<TContext, TConsequences> => {
|
|
177
|
+
const original = simulate(originalRules, context, evaluator);
|
|
178
|
+
const modified = simulate(modifiedRules, context, evaluator);
|
|
179
|
+
|
|
180
|
+
const originalMatchIds = new Set(original.matchedRules.map((r) => r.ruleId));
|
|
181
|
+
const modifiedMatchIds = new Set(modified.matchedRules.map((r) => r.ruleId));
|
|
182
|
+
|
|
183
|
+
const newMatches = [...modifiedMatchIds].filter(
|
|
184
|
+
(id) => !originalMatchIds.has(id),
|
|
185
|
+
);
|
|
186
|
+
const lostMatches = [...originalMatchIds].filter(
|
|
187
|
+
(id) => !modifiedMatchIds.has(id),
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
const consequenceChanges: Array<{
|
|
191
|
+
type: "added" | "removed" | "modified";
|
|
192
|
+
consequenceType: string;
|
|
193
|
+
ruleId: string;
|
|
194
|
+
}> = [];
|
|
195
|
+
|
|
196
|
+
const originalConsequenceKeys = new Set(
|
|
197
|
+
original.consequences.map((c) => `${c.ruleId}:${c.type as string}`),
|
|
198
|
+
);
|
|
199
|
+
const modifiedConsequenceKeys = new Set(
|
|
200
|
+
modified.consequences.map((c) => `${c.ruleId}:${c.type as string}`),
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
for (const key of modifiedConsequenceKeys) {
|
|
204
|
+
if (!originalConsequenceKeys.has(key)) {
|
|
205
|
+
const [ruleId, consequenceType] = key.split(":");
|
|
206
|
+
consequenceChanges.push({
|
|
207
|
+
type: "added",
|
|
208
|
+
consequenceType: consequenceType ?? "",
|
|
209
|
+
ruleId: ruleId ?? "",
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
for (const key of originalConsequenceKeys) {
|
|
215
|
+
if (!modifiedConsequenceKeys.has(key)) {
|
|
216
|
+
const [ruleId, consequenceType] = key.split(":");
|
|
217
|
+
consequenceChanges.push({
|
|
218
|
+
type: "removed",
|
|
219
|
+
consequenceType: consequenceType ?? "",
|
|
220
|
+
ruleId: ruleId ?? "",
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
original,
|
|
227
|
+
modified,
|
|
228
|
+
differences: {
|
|
229
|
+
newMatches,
|
|
230
|
+
lostMatches,
|
|
231
|
+
consequenceChanges,
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
export const batchSimulate = <
|
|
237
|
+
TContext = unknown,
|
|
238
|
+
TConsequences extends ConsequenceDefinitions = DefaultConsequences,
|
|
239
|
+
>(
|
|
240
|
+
rules: ReadonlyArray<Rule<TContext, TConsequences>>,
|
|
241
|
+
contexts: ReadonlyArray<SimulationContext<TContext>>,
|
|
242
|
+
evaluator: ReturnType<typeof createEvaluator> = createEvaluator(),
|
|
243
|
+
): BatchSimulationResult<TContext, TConsequences> => {
|
|
244
|
+
const results = contexts.map((context) => simulate(rules, context, evaluator));
|
|
245
|
+
|
|
246
|
+
const ruleMatchFrequency = new Map<string, number>();
|
|
247
|
+
const consequenceFrequency = new Map<string, number>();
|
|
248
|
+
let totalMatchedRules = 0;
|
|
249
|
+
let totalExecutionTime = 0;
|
|
250
|
+
|
|
251
|
+
for (const result of results) {
|
|
252
|
+
totalMatchedRules += result.matchedRules.length;
|
|
253
|
+
totalExecutionTime += result.executionTimeMs;
|
|
254
|
+
|
|
255
|
+
for (const match of result.matchedRules) {
|
|
256
|
+
const count = ruleMatchFrequency.get(match.ruleId) ?? 0;
|
|
257
|
+
ruleMatchFrequency.set(match.ruleId, count + 1);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
for (const consequence of result.consequences) {
|
|
261
|
+
const key = consequence.type as string;
|
|
262
|
+
const count = consequenceFrequency.get(key) ?? 0;
|
|
263
|
+
consequenceFrequency.set(key, count + 1);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
results,
|
|
269
|
+
summary: {
|
|
270
|
+
totalContexts: contexts.length,
|
|
271
|
+
averageMatchedRules:
|
|
272
|
+
contexts.length > 0 ? totalMatchedRules / contexts.length : 0,
|
|
273
|
+
ruleMatchFrequency,
|
|
274
|
+
consequenceFrequency,
|
|
275
|
+
averageExecutionTimeMs:
|
|
276
|
+
contexts.length > 0 ? totalExecutionTime / contexts.length : 0,
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
export const findRulesAffectedByContextChange = <
|
|
282
|
+
TContext = unknown,
|
|
283
|
+
TConsequences extends ConsequenceDefinitions = DefaultConsequences,
|
|
284
|
+
>(
|
|
285
|
+
rules: ReadonlyArray<Rule<TContext, TConsequences>>,
|
|
286
|
+
originalContext: SimulationContext<TContext>,
|
|
287
|
+
modifiedContext: SimulationContext<TContext>,
|
|
288
|
+
evaluator: ReturnType<typeof createEvaluator> = createEvaluator(),
|
|
289
|
+
): {
|
|
290
|
+
becameTrue: ReadonlyArray<Rule<TContext, TConsequences>>;
|
|
291
|
+
becameFalse: ReadonlyArray<Rule<TContext, TConsequences>>;
|
|
292
|
+
unchanged: ReadonlyArray<Rule<TContext, TConsequences>>;
|
|
293
|
+
} => {
|
|
294
|
+
const originalResult = simulate(rules, originalContext, evaluator);
|
|
295
|
+
const modifiedResult = simulate(rules, modifiedContext, evaluator);
|
|
296
|
+
|
|
297
|
+
const originalMatchIds = new Set(
|
|
298
|
+
originalResult.matchedRules.map((r) => r.ruleId),
|
|
299
|
+
);
|
|
300
|
+
const modifiedMatchIds = new Set(
|
|
301
|
+
modifiedResult.matchedRules.map((r) => r.ruleId),
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
const becameTrue: Rule<TContext, TConsequences>[] = [];
|
|
305
|
+
const becameFalse: Rule<TContext, TConsequences>[] = [];
|
|
306
|
+
const unchanged: Rule<TContext, TConsequences>[] = [];
|
|
307
|
+
|
|
308
|
+
for (const rule of rules) {
|
|
309
|
+
const wasMatched = originalMatchIds.has(rule.id);
|
|
310
|
+
const isMatched = modifiedMatchIds.has(rule.id);
|
|
311
|
+
|
|
312
|
+
if (!wasMatched && isMatched) {
|
|
313
|
+
becameTrue.push(rule);
|
|
314
|
+
} else if (wasMatched && !isMatched) {
|
|
315
|
+
becameFalse.push(rule);
|
|
316
|
+
} else {
|
|
317
|
+
unchanged.push(rule);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return { becameTrue, becameFalse, unchanged };
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
export const formatSimulationResult = <
|
|
325
|
+
TContext = unknown,
|
|
326
|
+
TConsequences extends ConsequenceDefinitions = DefaultConsequences,
|
|
327
|
+
>(
|
|
328
|
+
result: SimulationResult<TContext, TConsequences>,
|
|
329
|
+
): string => {
|
|
330
|
+
const lines: string[] = [
|
|
331
|
+
"=== Simulation Result ===",
|
|
332
|
+
"",
|
|
333
|
+
`Execution Time: ${result.executionTimeMs.toFixed(2)}ms`,
|
|
334
|
+
"",
|
|
335
|
+
`Matched Rules (${result.matchedRules.length}):`,
|
|
336
|
+
];
|
|
337
|
+
|
|
338
|
+
for (const match of result.matchedRules) {
|
|
339
|
+
lines.push(` - ${match.ruleName} (${match.ruleId})`);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
lines.push("");
|
|
343
|
+
lines.push(`Unmatched Rules (${result.unmatchedRules.length}):`);
|
|
344
|
+
|
|
345
|
+
for (const unmatched of result.unmatchedRules) {
|
|
346
|
+
lines.push(` - ${unmatched.ruleName}: ${unmatched.reason}`);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
lines.push("");
|
|
350
|
+
lines.push(`Consequences (${result.consequences.length}):`);
|
|
351
|
+
|
|
352
|
+
for (const consequence of result.consequences) {
|
|
353
|
+
lines.push(
|
|
354
|
+
` - ${consequence.type as string} from "${consequence.ruleName}"`,
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return lines.join("\n");
|
|
359
|
+
};
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { OperatorMap } from "@f-o-t/condition-evaluator";
|
|
3
|
+
import type {
|
|
4
|
+
AggregatedConsequence,
|
|
5
|
+
ConsequenceDefinitions,
|
|
6
|
+
DefaultConsequences,
|
|
7
|
+
} from "./consequence";
|
|
8
|
+
import type {
|
|
9
|
+
ConflictResolutionStrategy,
|
|
10
|
+
EngineExecutionResult,
|
|
11
|
+
EvaluationContext,
|
|
12
|
+
RuleEvaluationResult,
|
|
13
|
+
} from "./evaluation";
|
|
14
|
+
import type { Rule } from "./rule";
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// Zod Schemas - Define schemas first, then infer types
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
export const LogLevelSchema = z.enum([
|
|
21
|
+
"none",
|
|
22
|
+
"error",
|
|
23
|
+
"warn",
|
|
24
|
+
"info",
|
|
25
|
+
"debug",
|
|
26
|
+
]);
|
|
27
|
+
export type LogLevel = z.infer<typeof LogLevelSchema>;
|
|
28
|
+
|
|
29
|
+
export const CacheConfigSchema = z.object({
|
|
30
|
+
enabled: z.boolean().default(true),
|
|
31
|
+
ttl: z.number().int().positive().default(60000),
|
|
32
|
+
maxSize: z.number().int().positive().default(1000),
|
|
33
|
+
});
|
|
34
|
+
export type CacheConfig = z.infer<typeof CacheConfigSchema>;
|
|
35
|
+
|
|
36
|
+
export const ValidationConfigSchema = z.object({
|
|
37
|
+
enabled: z.boolean().default(true),
|
|
38
|
+
strict: z.boolean().default(false),
|
|
39
|
+
});
|
|
40
|
+
export type ValidationConfig = z.infer<typeof ValidationConfigSchema>;
|
|
41
|
+
|
|
42
|
+
export const VersioningConfigSchema = z.object({
|
|
43
|
+
enabled: z.boolean().default(false),
|
|
44
|
+
maxVersions: z.number().int().positive().default(10),
|
|
45
|
+
});
|
|
46
|
+
export type VersioningConfig = z.infer<typeof VersioningConfigSchema>;
|
|
47
|
+
|
|
48
|
+
// ============================================================================
|
|
49
|
+
// Types that cannot be Zod schemas (contain functions or generics)
|
|
50
|
+
// ============================================================================
|
|
51
|
+
|
|
52
|
+
export type Logger = {
|
|
53
|
+
readonly error: (...args: unknown[]) => void;
|
|
54
|
+
readonly warn: (...args: unknown[]) => void;
|
|
55
|
+
readonly info: (...args: unknown[]) => void;
|
|
56
|
+
readonly debug: (...args: unknown[]) => void;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export type EngineHooks<
|
|
60
|
+
TContext = unknown,
|
|
61
|
+
TConsequences extends ConsequenceDefinitions = DefaultConsequences,
|
|
62
|
+
> = {
|
|
63
|
+
readonly beforeEvaluation?: (
|
|
64
|
+
context: EvaluationContext<TContext>,
|
|
65
|
+
rules: ReadonlyArray<Rule<TContext, TConsequences>>,
|
|
66
|
+
) => void | Promise<void>;
|
|
67
|
+
|
|
68
|
+
readonly afterEvaluation?: (
|
|
69
|
+
result: EngineExecutionResult<TContext, TConsequences>,
|
|
70
|
+
) => void | Promise<void>;
|
|
71
|
+
|
|
72
|
+
readonly beforeRuleEvaluation?: (
|
|
73
|
+
rule: Rule<TContext, TConsequences>,
|
|
74
|
+
context: EvaluationContext<TContext>,
|
|
75
|
+
) => void | Promise<void>;
|
|
76
|
+
|
|
77
|
+
readonly afterRuleEvaluation?: (
|
|
78
|
+
rule: Rule<TContext, TConsequences>,
|
|
79
|
+
result: RuleEvaluationResult<TContext, TConsequences>,
|
|
80
|
+
) => void | Promise<void>;
|
|
81
|
+
|
|
82
|
+
readonly onRuleMatch?: (
|
|
83
|
+
rule: Rule<TContext, TConsequences>,
|
|
84
|
+
context: EvaluationContext<TContext>,
|
|
85
|
+
) => void | Promise<void>;
|
|
86
|
+
|
|
87
|
+
readonly onRuleSkip?: (
|
|
88
|
+
rule: Rule<TContext, TConsequences>,
|
|
89
|
+
reason: string,
|
|
90
|
+
) => void | Promise<void>;
|
|
91
|
+
|
|
92
|
+
readonly onRuleError?: (
|
|
93
|
+
rule: Rule<TContext, TConsequences>,
|
|
94
|
+
error: Error,
|
|
95
|
+
) => void | Promise<void>;
|
|
96
|
+
|
|
97
|
+
readonly onConsequenceCollected?: (
|
|
98
|
+
rule: Rule<TContext, TConsequences>,
|
|
99
|
+
consequence: AggregatedConsequence<TConsequences>,
|
|
100
|
+
) => void | Promise<void>;
|
|
101
|
+
|
|
102
|
+
readonly onCacheHit?: (
|
|
103
|
+
key: string,
|
|
104
|
+
result: EngineExecutionResult<TContext, TConsequences>,
|
|
105
|
+
) => void | Promise<void>;
|
|
106
|
+
|
|
107
|
+
readonly onCacheMiss?: (key: string) => void | Promise<void>;
|
|
108
|
+
|
|
109
|
+
readonly onSlowRule?: (
|
|
110
|
+
rule: Rule<TContext, TConsequences>,
|
|
111
|
+
timeMs: number,
|
|
112
|
+
threshold: number,
|
|
113
|
+
) => void | Promise<void>;
|
|
114
|
+
|
|
115
|
+
readonly onHookError?: (hookName: string, error: Error) => void;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
export type EngineConfig<
|
|
119
|
+
TContext = unknown,
|
|
120
|
+
TConsequences extends ConsequenceDefinitions = DefaultConsequences,
|
|
121
|
+
> = {
|
|
122
|
+
readonly consequences?: TConsequences;
|
|
123
|
+
readonly conflictResolution?: ConflictResolutionStrategy;
|
|
124
|
+
readonly cache?: Partial<CacheConfig>;
|
|
125
|
+
readonly validation?: Partial<ValidationConfig>;
|
|
126
|
+
readonly versioning?: Partial<VersioningConfig>;
|
|
127
|
+
readonly hooks?: EngineHooks<TContext, TConsequences>;
|
|
128
|
+
readonly logLevel?: LogLevel;
|
|
129
|
+
readonly logger?: Logger;
|
|
130
|
+
readonly continueOnError?: boolean;
|
|
131
|
+
readonly slowRuleThresholdMs?: number;
|
|
132
|
+
readonly hookTimeoutMs?: number;
|
|
133
|
+
// NEW: Evaluator configuration (mutually exclusive)
|
|
134
|
+
readonly evaluator?: ReturnType<typeof import("@f-o-t/condition-evaluator").createEvaluator>;
|
|
135
|
+
readonly operators?: OperatorMap;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
export type ResolvedEngineConfig<
|
|
139
|
+
TContext = unknown,
|
|
140
|
+
TConsequences extends ConsequenceDefinitions = DefaultConsequences,
|
|
141
|
+
> = {
|
|
142
|
+
readonly consequences: TConsequences | undefined;
|
|
143
|
+
readonly conflictResolution: ConflictResolutionStrategy;
|
|
144
|
+
readonly cache: CacheConfig;
|
|
145
|
+
readonly validation: ValidationConfig;
|
|
146
|
+
readonly versioning: VersioningConfig;
|
|
147
|
+
readonly hooks: EngineHooks<TContext, TConsequences>;
|
|
148
|
+
readonly logLevel: LogLevel;
|
|
149
|
+
readonly logger: Logger;
|
|
150
|
+
readonly continueOnError: boolean;
|
|
151
|
+
readonly slowRuleThresholdMs: number;
|
|
152
|
+
readonly hookTimeoutMs: number | undefined;
|
|
153
|
+
// NEW: Resolved evaluator instance
|
|
154
|
+
readonly evaluator: ReturnType<typeof import("@f-o-t/condition-evaluator").createEvaluator>;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// ============================================================================
|
|
158
|
+
// Helper functions for parsing/resolving configs with defaults
|
|
159
|
+
// ============================================================================
|
|
160
|
+
|
|
161
|
+
export const parseCacheConfig = (input?: Partial<CacheConfig>): CacheConfig =>
|
|
162
|
+
CacheConfigSchema.parse(input ?? {});
|
|
163
|
+
|
|
164
|
+
export const parseValidationConfig = (
|
|
165
|
+
input?: Partial<ValidationConfig>,
|
|
166
|
+
): ValidationConfig => ValidationConfigSchema.parse(input ?? {});
|
|
167
|
+
|
|
168
|
+
export const parseVersioningConfig = (
|
|
169
|
+
input?: Partial<VersioningConfig>,
|
|
170
|
+
): VersioningConfig => VersioningConfigSchema.parse(input ?? {});
|
|
171
|
+
|
|
172
|
+
export const getDefaultCacheConfig = (): CacheConfig =>
|
|
173
|
+
CacheConfigSchema.parse({});
|
|
174
|
+
|
|
175
|
+
export const getDefaultValidationConfig = (): ValidationConfig =>
|
|
176
|
+
ValidationConfigSchema.parse({});
|
|
177
|
+
|
|
178
|
+
export const getDefaultVersioningConfig = (): VersioningConfig =>
|
|
179
|
+
VersioningConfigSchema.parse({});
|
|
180
|
+
|
|
181
|
+
export const getDefaultLogLevel = (): LogLevel => "warn";
|
|
182
|
+
|
|
183
|
+
export const getDefaultConflictResolution = (): ConflictResolutionStrategy =>
|
|
184
|
+
"priority";
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export type ConsequenceDefinitions = Record<string, z.ZodType>;
|
|
4
|
+
|
|
5
|
+
export type DefaultConsequences = Record<string, z.ZodType>;
|
|
6
|
+
|
|
7
|
+
export type InferConsequenceType<T extends ConsequenceDefinitions> = keyof T;
|
|
8
|
+
|
|
9
|
+
export type InferConsequencePayload<
|
|
10
|
+
T extends ConsequenceDefinitions,
|
|
11
|
+
K extends keyof T,
|
|
12
|
+
> = z.infer<T[K]>;
|
|
13
|
+
|
|
14
|
+
export type Consequence<
|
|
15
|
+
TConsequences extends ConsequenceDefinitions = DefaultConsequences,
|
|
16
|
+
TType extends keyof TConsequences = keyof TConsequences,
|
|
17
|
+
> = {
|
|
18
|
+
readonly type: TType;
|
|
19
|
+
readonly payload: z.infer<TConsequences[TType]>;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type ConsequenceInput<
|
|
23
|
+
TConsequences extends ConsequenceDefinitions = DefaultConsequences,
|
|
24
|
+
TType extends keyof TConsequences = keyof TConsequences,
|
|
25
|
+
> = {
|
|
26
|
+
type: TType;
|
|
27
|
+
payload: z.infer<TConsequences[TType]>;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type AggregatedConsequence<
|
|
31
|
+
TConsequences extends ConsequenceDefinitions = DefaultConsequences,
|
|
32
|
+
> = {
|
|
33
|
+
readonly type: keyof TConsequences;
|
|
34
|
+
readonly payload: unknown;
|
|
35
|
+
readonly ruleId: string;
|
|
36
|
+
readonly ruleName?: string;
|
|
37
|
+
readonly priority: number;
|
|
38
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { GroupEvaluationResult } from "@f-o-t/condition-evaluator";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import type {
|
|
4
|
+
AggregatedConsequence,
|
|
5
|
+
ConsequenceDefinitions,
|
|
6
|
+
DefaultConsequences,
|
|
7
|
+
} from "./consequence";
|
|
8
|
+
import type { Rule } from "./rule";
|
|
9
|
+
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// Zod Schemas - Define schemas first, then infer types
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
14
|
+
export const ConflictResolutionStrategySchema = z.enum([
|
|
15
|
+
"priority",
|
|
16
|
+
"first-match",
|
|
17
|
+
"all",
|
|
18
|
+
"most-specific",
|
|
19
|
+
]);
|
|
20
|
+
export type ConflictResolutionStrategy = z.infer<
|
|
21
|
+
typeof ConflictResolutionStrategySchema
|
|
22
|
+
>;
|
|
23
|
+
|
|
24
|
+
export const EvaluateOptionsSchema = z.object({
|
|
25
|
+
conflictResolution: ConflictResolutionStrategySchema.optional(),
|
|
26
|
+
maxRules: z.number().int().positive().optional(),
|
|
27
|
+
timeout: z.number().int().positive().optional(),
|
|
28
|
+
skipDisabled: z.boolean().optional(),
|
|
29
|
+
tags: z.array(z.string()).optional(),
|
|
30
|
+
category: z.string().optional(),
|
|
31
|
+
ruleSetId: z.string().optional(),
|
|
32
|
+
bypassCache: z.boolean().optional(),
|
|
33
|
+
});
|
|
34
|
+
export type EvaluateOptions = z.infer<typeof EvaluateOptionsSchema>;
|
|
35
|
+
|
|
36
|
+
export const EvaluateConfigSchema = z.object({
|
|
37
|
+
conflictResolution: ConflictResolutionStrategySchema,
|
|
38
|
+
continueOnError: z.boolean(),
|
|
39
|
+
collectAllConsequences: z.boolean(),
|
|
40
|
+
});
|
|
41
|
+
export type EvaluateConfig = z.infer<typeof EvaluateConfigSchema>;
|
|
42
|
+
|
|
43
|
+
// ============================================================================
|
|
44
|
+
// Types that cannot be Zod schemas (contain complex generics)
|
|
45
|
+
// ============================================================================
|
|
46
|
+
|
|
47
|
+
export type EvaluationContext<TContext = unknown> = {
|
|
48
|
+
readonly data: TContext;
|
|
49
|
+
readonly timestamp: Date;
|
|
50
|
+
readonly correlationId?: string;
|
|
51
|
+
readonly metadata?: Readonly<Record<string, unknown>>;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export type RuleEvaluationResult<
|
|
55
|
+
_TContext = unknown,
|
|
56
|
+
TConsequences extends ConsequenceDefinitions = DefaultConsequences,
|
|
57
|
+
> = {
|
|
58
|
+
readonly ruleId: string;
|
|
59
|
+
readonly ruleName: string;
|
|
60
|
+
readonly matched: boolean;
|
|
61
|
+
readonly conditionResult: GroupEvaluationResult;
|
|
62
|
+
readonly consequences: ReadonlyArray<AggregatedConsequence<TConsequences>>;
|
|
63
|
+
readonly evaluationTimeMs: number;
|
|
64
|
+
readonly skipped: boolean;
|
|
65
|
+
readonly skipReason?: string;
|
|
66
|
+
readonly error?: Error;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export type EngineExecutionResult<
|
|
70
|
+
TContext = unknown,
|
|
71
|
+
TConsequences extends ConsequenceDefinitions = DefaultConsequences,
|
|
72
|
+
> = {
|
|
73
|
+
readonly context: EvaluationContext<TContext>;
|
|
74
|
+
readonly results: ReadonlyArray<
|
|
75
|
+
RuleEvaluationResult<TContext, TConsequences>
|
|
76
|
+
>;
|
|
77
|
+
readonly matchedRules: ReadonlyArray<Rule<TContext, TConsequences>>;
|
|
78
|
+
readonly consequences: ReadonlyArray<AggregatedConsequence<TConsequences>>;
|
|
79
|
+
readonly totalRulesEvaluated: number;
|
|
80
|
+
readonly totalRulesMatched: number;
|
|
81
|
+
readonly totalRulesSkipped: number;
|
|
82
|
+
readonly totalRulesErrored: number;
|
|
83
|
+
readonly executionTimeMs: number;
|
|
84
|
+
readonly stoppedEarly: boolean;
|
|
85
|
+
readonly stoppedByRuleId?: string;
|
|
86
|
+
readonly cacheHit: boolean;
|
|
87
|
+
};
|