PR T: consolidate obligations/evaluator into rulesEngine #24
@@ -1,153 +1,45 @@
|
||||
/**
|
||||
* Minimal, self-contained condition evaluator for the obligation
|
||||
* layer.
|
||||
* Obligation-layer condition evaluator.
|
||||
*
|
||||
* A sibling Rules Engine (PR P) ships a more general DSL that this
|
||||
* evaluator is a subset of; keeping a local copy lets the obligation
|
||||
* layer be merged independently. The two surfaces share the same
|
||||
* Condition shape so clauses authored for one run on the other.
|
||||
* Originally shipped as a self-contained subset of the PR P Rules
|
||||
* Engine so the obligation layer could be merged independently. Now
|
||||
* consolidated: this file re-exports the shared types and
|
||||
* `evaluateCondition` from `services/rulesEngine.ts` and provides a
|
||||
* thin compatibility wrapper for `resolvePath(path, context)` which
|
||||
* historically took its arguments in the opposite order.
|
||||
*
|
||||
* Key guarantees:
|
||||
* - Closed operator set (no eval, no code execution).
|
||||
* - Deterministic: given the same (condition, context), always
|
||||
* returns the same boolean.
|
||||
* - Dotted + indexed path resolution so clauses can assert on
|
||||
* deeply nested context objects (e.g. "plan.steps[0].amount").
|
||||
* Keeping this module as a named surface preserves existing imports
|
||||
* under `services/obligations/evaluator` throughout the codebase and
|
||||
* the test suite.
|
||||
*/
|
||||
|
||||
export type Operator =
|
||||
| "eq"
|
||||
| "neq"
|
||||
| "gt"
|
||||
| "gte"
|
||||
| "lt"
|
||||
| "lte"
|
||||
| "in"
|
||||
| "not_in"
|
||||
| "exists"
|
||||
| "matches"
|
||||
| "length_gte"
|
||||
| "length_lte";
|
||||
export type {
|
||||
Operator,
|
||||
LeafCondition,
|
||||
AndCondition,
|
||||
OrCondition,
|
||||
NotCondition,
|
||||
Condition,
|
||||
} from "../rulesEngine";
|
||||
|
||||
export interface LeafCondition {
|
||||
path: string;
|
||||
op: Operator;
|
||||
value?: unknown;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface AndCondition {
|
||||
all: Condition[];
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface OrCondition {
|
||||
any: Condition[];
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface NotCondition {
|
||||
not: Condition;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export type Condition = LeafCondition | AndCondition | OrCondition | NotCondition;
|
||||
|
||||
export function resolvePath(
|
||||
path: string,
|
||||
context: Record<string, unknown>,
|
||||
): unknown {
|
||||
// Handles both "a.b.c" and "a.b[0].c"
|
||||
const tokens = path
|
||||
.replace(/\[(\d+)\]/g, ".$1")
|
||||
.split(".")
|
||||
.filter(Boolean);
|
||||
let cur: unknown = context;
|
||||
for (const t of tokens) {
|
||||
if (cur === null || cur === undefined) return undefined;
|
||||
if (Array.isArray(cur)) {
|
||||
const idx = Number(t);
|
||||
cur = Number.isNaN(idx) ? undefined : cur[idx];
|
||||
} else if (typeof cur === "object") {
|
||||
cur = (cur as Record<string, unknown>)[t];
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
return cur;
|
||||
}
|
||||
|
||||
function isLeaf(c: Condition): c is LeafCondition {
|
||||
return (c as LeafCondition).path !== undefined;
|
||||
}
|
||||
|
||||
function isAnd(c: Condition): c is AndCondition {
|
||||
return Array.isArray((c as AndCondition).all);
|
||||
}
|
||||
|
||||
function isOr(c: Condition): c is OrCondition {
|
||||
return Array.isArray((c as OrCondition).any);
|
||||
}
|
||||
|
||||
function isNot(c: Condition): c is NotCondition {
|
||||
return (c as NotCondition).not !== undefined;
|
||||
}
|
||||
import { evaluateCondition as ruleEngineEvaluate, resolvePath as ruleEnginePath } from "../rulesEngine";
|
||||
import type { Condition } from "../rulesEngine";
|
||||
|
||||
export function evaluateCondition(
|
||||
condition: Condition,
|
||||
context: Record<string, unknown>,
|
||||
): boolean {
|
||||
if (isAnd(condition)) {
|
||||
return condition.all.every((c) => evaluateCondition(c, context));
|
||||
}
|
||||
if (isOr(condition)) {
|
||||
return condition.any.some((c) => evaluateCondition(c, context));
|
||||
}
|
||||
if (isNot(condition)) {
|
||||
return !evaluateCondition(condition.not, context);
|
||||
}
|
||||
if (!isLeaf(condition)) return false;
|
||||
|
||||
const actual = resolvePath(condition.path, context);
|
||||
const expected = condition.value;
|
||||
switch (condition.op) {
|
||||
case "eq":
|
||||
return actual === expected;
|
||||
case "neq":
|
||||
return actual !== expected;
|
||||
case "gt":
|
||||
return typeof actual === "number" && typeof expected === "number" && actual > expected;
|
||||
case "gte":
|
||||
return typeof actual === "number" && typeof expected === "number" && actual >= expected;
|
||||
case "lt":
|
||||
return typeof actual === "number" && typeof expected === "number" && actual < expected;
|
||||
case "lte":
|
||||
return typeof actual === "number" && typeof expected === "number" && actual <= expected;
|
||||
case "in":
|
||||
return Array.isArray(expected) && expected.includes(actual);
|
||||
case "not_in":
|
||||
return Array.isArray(expected) && !expected.includes(actual);
|
||||
case "exists":
|
||||
return actual !== undefined && actual !== null;
|
||||
case "matches":
|
||||
return (
|
||||
typeof actual === "string" &&
|
||||
typeof expected === "string" &&
|
||||
new RegExp(expected).test(actual)
|
||||
);
|
||||
case "length_gte":
|
||||
return (
|
||||
typeof expected === "number" &&
|
||||
(Array.isArray(actual) || typeof actual === "string") &&
|
||||
(actual as unknown[] | string).length >= expected
|
||||
);
|
||||
case "length_lte":
|
||||
return (
|
||||
typeof expected === "number" &&
|
||||
(Array.isArray(actual) || typeof actual === "string") &&
|
||||
(actual as unknown[] | string).length <= expected
|
||||
);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return ruleEngineEvaluate(condition, context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Historical (path, context) signature retained for backward
|
||||
* compatibility with call sites written before the evaluator was
|
||||
* consolidated into the Rules Engine.
|
||||
*/
|
||||
export function resolvePath(
|
||||
path: string,
|
||||
context: Record<string, unknown>,
|
||||
): unknown {
|
||||
return ruleEnginePath(context, path);
|
||||
}
|
||||
|
||||
@@ -91,6 +91,10 @@ export interface EvaluationResult {
|
||||
/* -----------------------------------------------------------------
|
||||
* Dotted-path resolver. Supports a.b.c and a.b[0].c.
|
||||
* --------------------------------------------------------------- */
|
||||
export function resolvePath(ctx: unknown, path: string): unknown {
|
||||
return getPath(ctx, path);
|
||||
}
|
||||
|
||||
function getPath(ctx: unknown, path: string): unknown {
|
||||
if (!path) return ctx;
|
||||
const parts = path
|
||||
|
||||
Reference in New Issue
Block a user