Files
skills/docs/foundation/flow/rules/src/optimizer/optimizer.md
2026-05-29 08:20:56 +01:00

4.9 KiB

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

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

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.