first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View 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.