diff --git a/orchestrator/src/services/obligations/evaluator.ts b/orchestrator/src/services/obligations/evaluator.ts index 7f59b76..5df97cc 100644 --- a/orchestrator/src/services/obligations/evaluator.ts +++ b/orchestrator/src/services/obligations/evaluator.ts @@ -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, -): 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)[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, ): 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, +): unknown { + return ruleEnginePath(context, path); } diff --git a/orchestrator/src/services/rulesEngine.ts b/orchestrator/src/services/rulesEngine.ts index 37e4ff5..c0e0cd5 100644 --- a/orchestrator/src/services/rulesEngine.ts +++ b/orchestrator/src/services/rulesEngine.ts @@ -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