# Optimizer — `@suaveplan/rules/optimizer` _(no submodules)_ ## Purpose Pure functional optimiser for the `Rule` tree. Applies four independent passes — **flatten** (collapse nested AND-of-AND / OR-of-OR), **dedupe** (drop structurally-equal siblings via `hashRule`), **simplify** (unwrap single-child AND/OR), **sort** (rearrange AND/OR children by ascending estimated cost for short-circuit benefit) — each toggleable via `OptimizeOptions`. Companion analytics — `analyzeRule`, `estimateCost`, `findFields`, `hasAsyncComponents`, `countRules`, `isOptimisationStable` — expose the shapes the optimiser uses internally so callers can introspect rules without re-walking. The optimiser never changes rule semantics — both passes either reduce work or reorder commutative siblings. NOT is preserved unchanged (single-child by design); group operators are sorted only when explicitly `and` / `or`. ## Features - Four toggleable passes; every pass defaults to `true`. - Structural hashing via `serializer.hashRule` for dedupe equality. - `RuleStats` analytics: `totalRules`, `maxDepth`, `fields`, `hasAsync`, `estimatedCost`. - `OptimizeResult` reports `before` and `after` stats so callers can measure savings. - Per-operator cost estimates favour short-circuiting cheap conditions first. ## Usage: Basic Example ```ts import { and, condition } from "@suaveplan/rules/builder"; import { optimizeRule } from "@suaveplan/rules/optimizer"; const r = and( and(condition("a", "equals", 1), condition("b", "equals", 2)), condition("c", "equals", 3), ); const { rule: optimised, before, after } = optimizeRule(r); // optimised is the flat 3-child AND; before.totalRules > after.totalRules ``` ## Usage: Advanced Example ```ts import { and, condition } from "@suaveplan/rules/builder"; import { analyzeRule, findFields, optimizeRule, } from "@suaveplan/rules/optimizer"; const policy = and( condition("region", "matches", "^US|CA$"), // cost 10 condition("age", "greaterThanOrEqual", 18), // cost 1 condition("verified", "equals", true), // cost 1 ); // Only run the sort pass — keep nested structure intact const { rule } = optimizeRule(policy, { flatten: false, dedupe: false, simplify: false, sort: true, }); const stats = analyzeRule(rule); console.log(stats); // { totalRules: 4, maxDepth: 1, fields: [...], hasAsync: false, estimatedCost: 12 } console.log(findFields(rule)); // ["region", "age", "verified"] ``` ## API Reference ### `optimizeRule(rule, options?)` → `OptimizeResult` Runs the configured passes. Returns `{ rule, before, after }` where stats are from `analyzeRule`. ### `interface OptimizeOptions` | Option | Default | Effect | |---|---|---| | `flatten` | `true` | Collapse same-operator nested logicals (AND-of-AND, OR-of-OR). | | `dedupe` | `true` | Drop structurally-equal siblings (uses `hashRule`). | | `simplify` | `true` | Unwrap single-child AND/OR (NOT is preserved). | | `sort` | `true` | Reorder AND/OR children by ascending estimated cost. | ### Analytics - `analyzeRule(rule)` → `RuleStats`. - `estimateCost(rule)` → number — sum of per-node costs. - `findFields(rule)` → list of distinct field paths touched. - `hasAsyncComponents(rule)` → `true` when any node is a `CustomRule` or `ComputedRule`. - `countRules(rule)` → total node count. - `isOptimisationStable(rule)` → `true` when `optimizeRule(rule).rule` hashes the same as the input. ### Cost model | Node | Cost | |---|---| | `equals` / `notEquals` / `strict*` / `gt` / `gte` / `lt` / `lte` / `isNull` / `isNotNull` / `exists` / `notExists` | 1 | | `matches` / `notMatches` | 10 | | `containsAll` / `containsAny` / `overlaps` | 5 | | every other condition operator | 2 | | `custom` / `computed` | 20 | | `logical` / `group` | 0 (cost lives on children) | Per-node costs are constants; the model assumes no live profiling of operator throughput. Callers that have measured workloads can build their own cost model and reorder explicitly via the builder. ## Implementation Notes - The dedupe pass canonicalises children by `hashRule` so commutative-order-different but structurally-identical children collapse together. The first occurrence wins. - Sort is stable per the JS spec; equal-cost children preserve input order. - The `simplify` pass intentionally does NOT unwrap `not` because a single-child NOT is the only valid form — collapsing it would lose the inversion. Similarly, single-child groups are not unwrapped because their `name` is a contract for callers. - `isOptimisationStable` is intended for build-time CI checks: rules that hash differently before vs after optimisation indicate either dead code in the rule tree or an opportunity to commit the optimised form back into source. - Field discovery (`findFields`) walks both `ConditionRule.field` and `ComputedRule.field`. Group `name` is NOT considered a field.