@bernierllc/nevar-evaluator 0.0.1 → 0.1.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/README.md +65 -28
- package/dist/evaluator.d.ts +41 -0
- package/dist/evaluator.js +131 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +13 -0
- package/package.json +53 -7
package/README.md
CHANGED
|
@@ -1,45 +1,82 @@
|
|
|
1
1
|
# @bernierllc/nevar-evaluator
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Evaluates condition trees against a flat context using an operator registry for the Nevar rules engine.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Installation
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
```bash
|
|
8
|
+
npm install @bernierllc/nevar-evaluator
|
|
9
|
+
```
|
|
8
10
|
|
|
9
|
-
##
|
|
11
|
+
## Usage
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
```typescript
|
|
14
|
+
import { evaluate, resolveField } from '@bernierllc/nevar-evaluator';
|
|
15
|
+
import type { OperatorLookup } from '@bernierllc/nevar-evaluator';
|
|
16
|
+
import type { ConditionGroup } from '@bernierllc/nevar-types';
|
|
15
17
|
|
|
16
|
-
|
|
18
|
+
// Any object implementing OperatorLookup (e.g. OperatorRegistry from nevar-operator-registry)
|
|
19
|
+
const operators: OperatorLookup = {
|
|
20
|
+
has: (name) => name === 'gte',
|
|
21
|
+
evaluate: (name, field, target) => (field as number) >= (target as number),
|
|
22
|
+
};
|
|
17
23
|
|
|
18
|
-
|
|
24
|
+
const tree: ConditionGroup = {
|
|
25
|
+
operator: 'AND',
|
|
26
|
+
conditions: [
|
|
27
|
+
{ field: 'user.age', op: 'gte', value: 18 },
|
|
28
|
+
],
|
|
29
|
+
};
|
|
19
30
|
|
|
20
|
-
|
|
31
|
+
const result = evaluate(tree, { user: { age: 21 } }, operators);
|
|
32
|
+
// result.matched === true
|
|
33
|
+
// result.trace === [{ field: 'user.age', op: 'gte', expected: 18, actual: 21, matched: true }]
|
|
21
34
|
|
|
22
|
-
|
|
35
|
+
// Resolve a dot-path field from a nested object
|
|
36
|
+
const value = resolveField({ user: { name: 'Alice' } }, 'user.name');
|
|
37
|
+
// 'Alice'
|
|
38
|
+
```
|
|
23
39
|
|
|
24
|
-
|
|
25
|
-
2. Configure the trusted publisher (e.g., GitHub Actions)
|
|
26
|
-
3. Specify the repository and workflow that should be allowed to publish
|
|
27
|
-
4. Use the configured workflow to publish your actual package
|
|
40
|
+
## API
|
|
28
41
|
|
|
29
|
-
|
|
42
|
+
### `evaluate(tree, context, operators)`
|
|
30
43
|
|
|
31
|
-
|
|
32
|
-
- Contains no executable code
|
|
33
|
-
- Provides no functionality
|
|
34
|
-
- Should not be installed as a dependency
|
|
35
|
-
- Exists only for administrative purposes
|
|
44
|
+
Recursively evaluates a condition tree (AND / OR / NOT groups) against a context object. Returns a boolean match result and a trace of every condition evaluated.
|
|
36
45
|
|
|
37
|
-
|
|
46
|
+
**Parameters:**
|
|
47
|
+
- `tree: ConditionGroup` - The root condition group to evaluate
|
|
48
|
+
- `context: Record<string, unknown>` - Key-value context to evaluate against
|
|
49
|
+
- `operators: OperatorLookup` - Operator lookup for resolving and evaluating operators
|
|
38
50
|
|
|
39
|
-
|
|
40
|
-
- [npm Trusted Publishing Documentation](https://docs.npmjs.com/generating-provenance-statements)
|
|
41
|
-
- [GitHub Actions OIDC Documentation](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect)
|
|
51
|
+
**Returns:** `EvaluationResult` with `matched: boolean` and `trace: EvaluationTrace[]`
|
|
42
52
|
|
|
43
|
-
|
|
53
|
+
**Throws:** `NevarEvaluationError` if an unknown operator is encountered.
|
|
44
54
|
|
|
45
|
-
|
|
55
|
+
### `resolveField(context, fieldPath)`
|
|
56
|
+
|
|
57
|
+
Resolves a dot-notation field path against a context object. Returns `undefined` if any segment is missing.
|
|
58
|
+
|
|
59
|
+
**Parameters:**
|
|
60
|
+
- `context: Record<string, unknown>` - The context object
|
|
61
|
+
- `fieldPath: string` - Dot-notation path (e.g. `"user.name"`)
|
|
62
|
+
|
|
63
|
+
**Returns:** `unknown` - The resolved value or `undefined`
|
|
64
|
+
|
|
65
|
+
### `OperatorLookup` (type)
|
|
66
|
+
|
|
67
|
+
Interface for looking up and evaluating operators. Decouples the evaluator from a specific operator registry implementation.
|
|
68
|
+
|
|
69
|
+
- `evaluate(name: string, fieldValue: unknown, targetValue: unknown): boolean`
|
|
70
|
+
- `has(name: string): boolean`
|
|
71
|
+
|
|
72
|
+
## Integration Documentation
|
|
73
|
+
|
|
74
|
+
### Logger Integration
|
|
75
|
+
This package does not integrate with `@bernierllc/logger`. As a core package, logger integration is optional and not included by default. Consumers should handle logging at the service layer.
|
|
76
|
+
|
|
77
|
+
### NeverHub Integration
|
|
78
|
+
This package does not integrate with `@bernierllc/neverhub-adapter`. As a core package, NeverHub integration is not applicable. NeverHub registration should be handled by service-layer packages that compose this package.
|
|
79
|
+
|
|
80
|
+
## License
|
|
81
|
+
|
|
82
|
+
Copyright (c) 2025 Bernier LLC. All rights reserved.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { ConditionGroup, EvaluationResult } from '@bernierllc/nevar-types';
|
|
2
|
+
/**
|
|
3
|
+
* Interface for looking up and evaluating operators.
|
|
4
|
+
* Decouples the evaluator from a specific operator registry implementation.
|
|
5
|
+
*/
|
|
6
|
+
export interface OperatorLookup {
|
|
7
|
+
evaluate(name: string, fieldValue: unknown, targetValue: unknown): boolean;
|
|
8
|
+
has(name: string): boolean;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Resolves a dot-path field reference against a flat context object.
|
|
12
|
+
* Returns undefined if any segment is missing.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* resolveField({ user: { name: 'Alice' } }, 'user.name') // 'Alice'
|
|
16
|
+
* resolveField({ a: 1 }, 'b.c') // undefined
|
|
17
|
+
*/
|
|
18
|
+
export declare function resolveField(context: Record<string, unknown>, fieldPath: string): unknown;
|
|
19
|
+
/**
|
|
20
|
+
* Evaluates a condition tree against a flat context using an operator registry.
|
|
21
|
+
* Returns a boolean match result and a trace of every condition evaluated.
|
|
22
|
+
*
|
|
23
|
+
* @param tree - The root condition group to evaluate.
|
|
24
|
+
* @param context - A flat (or nested) key-value context to evaluate against.
|
|
25
|
+
* @param operators - An operator lookup for resolving and evaluating operators.
|
|
26
|
+
* @returns An EvaluationResult with matched status and evaluation trace.
|
|
27
|
+
*
|
|
28
|
+
* @throws NevarEvaluationError if an unknown operator is encountered.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```typescript
|
|
32
|
+
* const result = evaluate(
|
|
33
|
+
* { operator: 'AND', conditions: [{ field: 'age', op: 'gte', value: 18 }] },
|
|
34
|
+
* { age: 21 },
|
|
35
|
+
* operatorRegistry,
|
|
36
|
+
* );
|
|
37
|
+
* // result.matched === true
|
|
38
|
+
* // result.trace === [{ field: 'age', op: 'gte', expected: 18, actual: 21, matched: true }]
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export declare function evaluate(tree: ConditionGroup, context: Record<string, unknown>, operators: OperatorLookup): EvaluationResult;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
Copyright (c) 2025 Bernier LLC
|
|
4
|
+
|
|
5
|
+
This file is licensed to the client under a limited-use license.
|
|
6
|
+
The client may use and modify this code *only within the scope of the project it was delivered for*.
|
|
7
|
+
Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.resolveField = resolveField;
|
|
11
|
+
exports.evaluate = evaluate;
|
|
12
|
+
const nevar_types_1 = require("@bernierllc/nevar-types");
|
|
13
|
+
/**
|
|
14
|
+
* Resolves a dot-path field reference against a flat context object.
|
|
15
|
+
* Returns undefined if any segment is missing.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* resolveField({ user: { name: 'Alice' } }, 'user.name') // 'Alice'
|
|
19
|
+
* resolveField({ a: 1 }, 'b.c') // undefined
|
|
20
|
+
*/
|
|
21
|
+
function resolveField(context, fieldPath) {
|
|
22
|
+
const segments = fieldPath.split('.');
|
|
23
|
+
let current = context;
|
|
24
|
+
for (const segment of segments) {
|
|
25
|
+
if (current === null || current === undefined || typeof current !== 'object') {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
current = current[segment];
|
|
29
|
+
}
|
|
30
|
+
return current;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Evaluates a single leaf condition against the context.
|
|
34
|
+
* Returns the trace entry and whether it matched.
|
|
35
|
+
*/
|
|
36
|
+
function evaluateCondition(condition, context, operators) {
|
|
37
|
+
if (!operators.has(condition.op)) {
|
|
38
|
+
throw new nevar_types_1.NevarEvaluationError(`Unknown operator: "${condition.op}"`, {
|
|
39
|
+
code: 'UNKNOWN_OPERATOR',
|
|
40
|
+
context: { operator: condition.op, field: condition.field },
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
const actual = resolveField(context, condition.field);
|
|
44
|
+
// Missing field in context evaluates to false
|
|
45
|
+
if (actual === undefined) {
|
|
46
|
+
return {
|
|
47
|
+
matched: false,
|
|
48
|
+
trace: {
|
|
49
|
+
field: condition.field,
|
|
50
|
+
op: condition.op,
|
|
51
|
+
expected: condition.value,
|
|
52
|
+
actual: undefined,
|
|
53
|
+
matched: false,
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const matched = operators.evaluate(condition.op, actual, condition.value);
|
|
58
|
+
return {
|
|
59
|
+
matched,
|
|
60
|
+
trace: {
|
|
61
|
+
field: condition.field,
|
|
62
|
+
op: condition.op,
|
|
63
|
+
expected: condition.value,
|
|
64
|
+
actual,
|
|
65
|
+
matched,
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Recursively evaluates a condition group (AND / OR / NOT) against the context.
|
|
71
|
+
* Returns the aggregate match result and all trace entries.
|
|
72
|
+
*/
|
|
73
|
+
function evaluateGroup(group, context, operators) {
|
|
74
|
+
const trace = [];
|
|
75
|
+
// Empty conditions → vacuous truth
|
|
76
|
+
if (group.conditions.length === 0) {
|
|
77
|
+
return { matched: true, trace };
|
|
78
|
+
}
|
|
79
|
+
const childResults = [];
|
|
80
|
+
for (const child of group.conditions) {
|
|
81
|
+
if ((0, nevar_types_1.isConditionGroup)(child)) {
|
|
82
|
+
const result = evaluateGroup(child, context, operators);
|
|
83
|
+
trace.push(...result.trace);
|
|
84
|
+
childResults.push(result.matched);
|
|
85
|
+
}
|
|
86
|
+
else if ((0, nevar_types_1.isCondition)(child)) {
|
|
87
|
+
const result = evaluateCondition(child, context, operators);
|
|
88
|
+
trace.push(result.trace);
|
|
89
|
+
childResults.push(result.matched);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
let matched;
|
|
93
|
+
switch (group.operator) {
|
|
94
|
+
case 'AND':
|
|
95
|
+
matched = childResults.every(Boolean);
|
|
96
|
+
break;
|
|
97
|
+
case 'OR':
|
|
98
|
+
matched = childResults.some(Boolean);
|
|
99
|
+
break;
|
|
100
|
+
case 'NOT':
|
|
101
|
+
// NOT evaluates children as AND, then negates
|
|
102
|
+
matched = !childResults.every(Boolean);
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
return { matched, trace };
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Evaluates a condition tree against a flat context using an operator registry.
|
|
109
|
+
* Returns a boolean match result and a trace of every condition evaluated.
|
|
110
|
+
*
|
|
111
|
+
* @param tree - The root condition group to evaluate.
|
|
112
|
+
* @param context - A flat (or nested) key-value context to evaluate against.
|
|
113
|
+
* @param operators - An operator lookup for resolving and evaluating operators.
|
|
114
|
+
* @returns An EvaluationResult with matched status and evaluation trace.
|
|
115
|
+
*
|
|
116
|
+
* @throws NevarEvaluationError if an unknown operator is encountered.
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```typescript
|
|
120
|
+
* const result = evaluate(
|
|
121
|
+
* { operator: 'AND', conditions: [{ field: 'age', op: 'gte', value: 18 }] },
|
|
122
|
+
* { age: 21 },
|
|
123
|
+
* operatorRegistry,
|
|
124
|
+
* );
|
|
125
|
+
* // result.matched === true
|
|
126
|
+
* // result.trace === [{ field: 'age', op: 'gte', expected: 18, actual: 21, matched: true }]
|
|
127
|
+
* ```
|
|
128
|
+
*/
|
|
129
|
+
function evaluate(tree, context, operators) {
|
|
130
|
+
return evaluateGroup(tree, context, operators);
|
|
131
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
Copyright (c) 2025 Bernier LLC
|
|
4
|
+
|
|
5
|
+
This file is licensed to the client under a limited-use license.
|
|
6
|
+
The client may use and modify this code *only within the scope of the project it was delivered for*.
|
|
7
|
+
Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.resolveField = exports.evaluate = void 0;
|
|
11
|
+
var evaluator_1 = require("./evaluator");
|
|
12
|
+
Object.defineProperty(exports, "evaluate", { enumerable: true, get: function () { return evaluator_1.evaluate; } });
|
|
13
|
+
Object.defineProperty(exports, "resolveField", { enumerable: true, get: function () { return evaluator_1.resolveField; } });
|
package/package.json
CHANGED
|
@@ -1,10 +1,56 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bernierllc/nevar-evaluator",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Evaluates condition trees against a flat context using an operator registry for the Nevar rules engine",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist/**/*",
|
|
9
|
+
"README.md",
|
|
10
|
+
"LICENSE"
|
|
11
|
+
],
|
|
5
12
|
"keywords": [
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
|
|
10
|
-
|
|
13
|
+
"nevar",
|
|
14
|
+
"rules-engine",
|
|
15
|
+
"evaluator",
|
|
16
|
+
"condition-tree",
|
|
17
|
+
"evaluation"
|
|
18
|
+
],
|
|
19
|
+
"author": "Bernier LLC",
|
|
20
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public",
|
|
23
|
+
"registry": "https://registry.npmjs.org/"
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=16.0.0"
|
|
27
|
+
},
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/bernierllc/tools.git",
|
|
31
|
+
"directory": "packages/core/nevar-evaluator"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@bernierllc/nevar-types": "0.1.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/jest": "^29.5.0",
|
|
38
|
+
"@types/node": "^20.0.0",
|
|
39
|
+
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
|
40
|
+
"@typescript-eslint/parser": "^6.0.0",
|
|
41
|
+
"eslint": "^8.0.0",
|
|
42
|
+
"jest": "^29.5.0",
|
|
43
|
+
"rimraf": "^5.0.0",
|
|
44
|
+
"ts-jest": "^29.1.0",
|
|
45
|
+
"typescript": "^5.0.0"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"build": "tsc",
|
|
49
|
+
"prebuild": "npm run clean",
|
|
50
|
+
"clean": "rimraf dist",
|
|
51
|
+
"test": "jest",
|
|
52
|
+
"test:run": "jest",
|
|
53
|
+
"test:coverage": "jest --coverage",
|
|
54
|
+
"lint": "eslint src __tests__ --ext .ts"
|
|
55
|
+
}
|
|
56
|
+
}
|