houserules/packages/rete/tests/golden/GOLDEN-MAP.md
Joey Yakimowich-Payne 0401295bbc
test(rete): port pararules golden tests; tag Phase 1 parity (P1.14)
10 integration tests in packages/rete/tests/golden/ manually wire
AlphaNode → BetaMemory → JoinNode → ProductionNode chains and drive
them via Session.insert/retract. Each test maps to a pararules Nim
reference test (documented in GOLDEN-MAP.md).

Coverage: packages/rete/src at 96.8% statements / 95.4% branch /
97.8% functions — all well above the 90% Phase 1 gate.

Tests: 227 total (166 pre-existing + 61 new golden), all green.
2026-04-16 14:16:19 -06:00

89 lines
5.5 KiB
Markdown

# Golden Test Map
Maps each golden integration test to its pararules origin in the Nim reference implementation.
Source: https://github.com/paranim/pararules
## Overview
These tests manually wire Rete II network components (AlphaNode → BetaMemory → JoinNode →
ProductionNode) via the Session's internal accessors (`_getAlpha()`, `_getWM()`), then insert/
retract facts via `Session.insert/retract` to drive the alpha dispatch pipeline. This proves
component interoperability at the integration level while the full `Session.add()` auto-wiring
is pending (Phase 2).
---
## Test Map
| File | Test Suite | Core Assertion | Pararules Origin | Nim File / Line |
|------|------------|---------------|-----------------|-----------------|
| `g1-single-condition.test.ts` | G1 — single-condition match | Insert (id, Health, 42) → 1 match with hp=42 | `tests/test2.nim` "queries" `getPerson` rule (single what-condition) | test2.nim ~1-30 |
| `g2-two-condition-join.test.ts` | G2 — two-condition join (same entity) | `(?id, X, ?x) ∧ (?id, Y, ?y)` → 1 match per complete entity | `tests/test2.nim` "joins and advanced queries" `getCharacter` rule | test2.nim ~60-90 |
| `g3-derived-facts.test.ts` | G3 — derived facts (truth maintenance) | Insert X=5 → derived Double=10; Retract → Double disappears | `tests/test2.nim` "derived facts" `thenFinally` block | test2.nim ~95-130 |
| `g4-multiple-entities.test.ts` | G4 — multiple entities wildcard | N entities with Health → N matches | `tests/test1.nim` "number of conditions != number of facts" (Xavier/Thomas/George Height entities) | test1.nim ~1-40 |
| `g5-filter-predicate.test.ts` | G5 — filter predicate (cond) | Health=100 passes `hp > 50`; Health=10 filtered out | `tests/test2.nim` "complex types" `stopPlayer` with `cond: x >= windowWidth` | test2.nim ~10-50 |
| `g6-conflict-resolution.test.ts` | G6 — conflict resolution ordering | `orderActivations` sorts by salience↓ → specificity↓ → addedAt↑ | `tests/test2.nim` "recursion limit" (rule cascade order) + SPEC §Conflict Resolution | test2.nim ~130-160 |
| `g7-serialization.test.ts` | G7 — serialization round-trip | `defineRule → serialize → JSON → deserialize` → equivalent `RuleDefinition` | `tests/test3.nim` `staticRuleset` (compile-time rule serialization analogue) | test3.nim ~1-40 |
| `g8-variable-binding-chain.test.ts` | G8 — variable binding 3-condition chain | `(?eid, Type, ?t) ∧ (?eid, Health, ?hp) ∧ (?eid, Position, ?pos)` → idEquality through chain | `tests/test1.nim` "adding facts out of order" (multi-condition, deep binding) | test1.nim ~42-80 |
| `g9-retraction.test.ts` | G9 — retraction removes match | Insert → match; Retract → no match; Re-insert → match again | `tests/test1.nim` "removing facts" (retract clears queryAll) | test1.nim ~82-110 |
| `g10-cycle-detection.test.ts` | G10 — cycle detection / recursion limit | `fireRules()` at depth ≥ limit throws `RecursionLimitExceededError` | `tests/test2.nim` "recursion limit" (rule1→rule2→rule3 cascade loop) | test2.nim ~130-160 |
---
## Pararules API → TypeScript Engine Mapping
| Pararules construct | TypeScript equivalent |
|---------------------|-----------------------|
| `initSession(Fact)` | `new Session({ autoFire: false })` |
| `session.add(rule)` | Manual network wiring via `_getAlpha().buildNode()` |
| `session.insert(id, attr, val)` | `session.insert(id as EntityId, attr, val)` |
| `session.retract(id, attr)` | `session.retract(id as EntityId, attr)` |
| `session.queryAll(rule).len` | `prod.matches.length` |
| `session.query(rule).field` | `query(prod)["field"]` |
| `what: (id, Attr, var)` | `alphaNode.buildNode({id: null, attr: "Attr"})` + `JoinNode(…, "var", "id")` |
| `cond: expr` | `FilterNode([{predicate: "name", args: [...]}], predicateRegistry)` |
| `thenFinally: session.insert(…)` | `DerivedFactProduction(ruleName, wm, handler)` |
| salience on rules | `defineRule({salience: N, …})` + `orderActivations([…])` |
| recursion limit error | `RecursionLimitExceededError` from `session.fireRules()` |
| JSON rule schema | `serialize(rule)` / `deserialize(json, registry)` |
---
## Network Wiring Pattern
All golden tests follow this manual wiring pattern (since `Session.add()` auto-wiring is Phase 2):
```typescript
// 1. Create Session (WM↔AlphaNetwork already wired internally)
const session = new Session({ autoFire: false });
const alpha = session._getAlpha();
// 2. Root BetaMemory with seed token (drives first join)
const rootMem = new BetaMemory();
rootMem.leftActivate(new Token(null, ROOT_FACT, {}));
// 3. Alpha node for each condition
const alphaNode = alpha.buildNode({ id: null, attr: "Health" });
// 4. JoinNode connecting left memory and alpha memory
const join = new JoinNode(rootMem, alphaNode.memory, [], "hp", "eid");
// 5. Wire alpha → join right side
alphaNode.onActivate((id, attr, value) => join.rightActivate(id, attr, value));
alphaNode.onDeactivate((id, attr, value) => join.rightDeactivate(id, attr, value));
// 6. Wire join → ProductionNode
const prod = new ProductionNode("ruleName");
join.addDownstreamActivate(prod.leftActivate.bind(prod));
join.addDownstreamDeactivate(prod.leftDeactivate.bind(prod));
// 7. Insert facts via Session — flows WM → Alpha → JoinNode → ProductionNode
session.insert(e1, "Health", 100);
// 8. Assert
expect(prod.matches).toHaveLength(1);
```
For multi-condition rules, intermediate `BetaMemory` nodes connect joins in a chain,
with each downstream `BetaMemory` forwarding new tokens to the next `JoinNode` via
`addDownstreamActivate(nextJoin.leftActivate.bind(nextJoin))`.