@f-o-t/rules-engine 3.0.0 → 3.0.5

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