first commit
This commit is contained in:
106
docs/foundation/flow/rules/src/optimizer/optimizer.md
Normal file
106
docs/foundation/flow/rules/src/optimizer/optimizer.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# Optimizer — `@suaveplan/rules/optimizer`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## 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.
|
||||
Reference in New Issue
Block a user