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.
89 lines
5.5 KiB
Markdown
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))`.
|