# Operators module — `@suaveplan/rules/operators` _(no submodules)_ ## Purpose Exposes the canonical `OperatorRegistry` class plus 33 built-in operator definitions covering equality, numeric comparison, collection membership, existence, type, length, and string-shape checks. The engine can dispatch comparison through this registry (or a caller-supplied alternative) instead of its internal `compareValues` table — both paths satisfy the same `OperatorDefinition.evaluate` contract, so call sites do not change. The four logical operators (`and`, `or`, `not`, `xor`) are surfaced as placeholder `OperatorDefinition`s for callers who enumerate the universe of operators (e.g. building a UI). Their `evaluate` throws — the engine handles logical operators through its `evaluateLogical` path, not the registry. ## Features - `OperatorRegistry` — `register`, `get`, `mustGet` (raises `RULES.OPERATOR_NOT_FOUND`), `has`, `names`, `unregister`. Supports per-operator `aliases`. - 30 working comparison operators wrapping the engine's internal `compareValues` dispatcher so behaviour is identical between the registry path and the engine's built-in fast path. - 4 logical placeholder operators (`and`/`or`/`not`/`xor`) that throw on direct invocation — they exist for enumeration and discoverability. - `BUILTIN_OPERATORS` — readonly tuple of every built-in. - `createDefaultOperatorRegistry()` — fresh registry instance pre-populated with the built-ins. Recommended path for tests and callers that want isolation. - `defaultOperatorRegistry` — process-global pre-populated singleton for one-shot convenience. ## Usage: Basic Example ```ts import { defaultOperatorRegistry } from "@suaveplan/rules/operators"; const op = defaultOperatorRegistry.mustGet("greaterThan"); await op.evaluate(21, 18); // true ``` ## Usage: Advanced Example ```ts import { OperatorRegistry, createDefaultOperatorRegistry, } from "@suaveplan/rules/operators"; import type { OperatorDefinition } from "@suaveplan/rules"; const isOddOperator: OperatorDefinition = { name: "isOdd", description: "actual % 2 !== 0", evaluate: (actual) => Number(actual) % 2 !== 0, }; const registry: OperatorRegistry = createDefaultOperatorRegistry(); registry.register("isOdd", isOddOperator); // register an alias too: registry.register("notDivisibleByTwo", { ...isOddOperator, aliases: ["odd"] }); registry.has("odd"); // true (alias) registry.unregister("isOdd"); // true ``` ## API Reference ### `class OperatorRegistry implements OperatorRegistryLike` | Method | Returns | Notes | |---|---|---| | `register(name, definition)` | `void` | Empty name → `RULES.OPERATOR_INVALID`. Definition aliases are registered as additional names. Re-registration replaces the prior entry. | | `get(name)` | `OperatorDefinition \| undefined` | Lenient lookup. | | `mustGet(name)` | `OperatorDefinition` | Strict lookup; raises `RulesError(RULES.OPERATOR_NOT_FOUND)` when absent. | | `has(name)` | `boolean` | Includes aliases. | | `names()` | `readonly string[]` | Snapshot of all keys (includes aliases). | | `unregister(name)` | `boolean` | `true` when removed, `false` when absent. | ### Built-in definitions Every name in the `ComparisonOperator` union has a matching `Operator` export — for example `equalsOperator`, `greaterThanOperator`, `betweenOperator`, `containsOperator`, `startsWithOperator`, `matchesOperator`, `lengthEqualsOperator`. Each definition delegates to `compareValues(actual, name, expected)` so registry-path and engine-internal-path behaviour are bit-identical. ### `createDefaultOperatorRegistry(): OperatorRegistry` Returns a fresh registry pre-populated with `BUILTIN_OPERATORS`. Prefer this over `defaultOperatorRegistry` when you need isolation between tests, requests, or tenants. ### `defaultOperatorRegistry: OperatorRegistry` Process-global pre-populated registry. Mutable — callers who register custom operators against it will see those changes from every other consumer of the same singleton. Use with care; prefer `createDefaultOperatorRegistry()` when in doubt. ## Implementation Notes - The four logical placeholder operators (`andOperator`, `orOperator`, `notOperator`, `xorOperator`) throw on direct `evaluate` call because logical operators in this package have always been engine-managed (they need access to the child rule list, depth tracker, and abort signal — none of which fit the `OperatorDefinition.evaluate(actual, expected, context?)` shape). They are kept for enumeration completeness and would be removed if a future change unifies the dispatch model. - `BUILTIN_OPERATORS` is `Object.freeze`d to make its shape part of the public contract — consumers can iterate it but cannot mutate. - The registry's alias handling is deliberately simple: aliases register additional keys pointing at the same `OperatorDefinition` reference. `unregister(alias)` removes only the alias, not the primary name. - This module does NOT register itself against the engine at import time. Engine consumers wire it explicitly via `engine.use(plugin)` when they want registry-based dispatch.