/** * G6 — Conflict resolution ordering * * Pararules origin: tests/test2.nim "recursion limit" (rule1/rule2/rule3 cascade) * and SPEC.md §Conflict Resolution which defines salience → specificity → addedAt. * * Pararules uses salience in rule declarations to control firing order. * Our engine implements the same three-key sort via orderActivations(): * 1. Salience descending * 2. Specificity (# conditions) descending * 3. Insertion order ascending * * This golden test verifies the pure ordering function, which is the * conflict-resolution kernel that the future agenda will call. */ import { describe, it, expect } from "vitest"; import { orderActivations } from "../../src/conflict.js"; import { Token } from "../../src/beta.js"; import { Session } from "../../src/session.js"; import { HandlerRegistry } from "../../src/registry.js"; import { defineRule, v } from "../../src/builder.js"; import type { Activation, OrderableRule } from "../../src/conflict.js"; import type { EntityId } from "../../src/schema.js"; import type { TokenFact } from "../../src/beta.js"; const ROOT_FACT: TokenFact = { id: 0 as EntityId, attr: "__root", value: null }; const dummyToken = new Token(null, ROOT_FACT, {}); function mkActivation( name: string, salience: number, conditionCount: number, addedAt: number, ): Activation { const rule: OrderableRule = { name, salience, conditions: Array.from({ length: conditionCount }, () => ({})), handler: "noop", addedAt, }; return { rule, token: dummyToken }; } describe("G6 — conflict resolution ordering", () => { it("higher salience fires first", () => { const activations = [ mkActivation("low", 0, 1, 0), mkActivation("high", 10, 1, 1), mkActivation("mid", 5, 1, 2), ]; const ordered = orderActivations(activations); expect(ordered[0]?.rule.name).toBe("high"); expect(ordered[1]?.rule.name).toBe("mid"); expect(ordered[2]?.rule.name).toBe("low"); }); it("salience ties broken by specificity (more conditions wins)", () => { const activations = [ mkActivation("oneCondition", 10, 1, 0), mkActivation("twoConditions", 10, 2, 1), mkActivation("threeConditions", 10, 3, 2), ]; const ordered = orderActivations(activations); expect(ordered[0]?.rule.name).toBe("threeConditions"); expect(ordered[1]?.rule.name).toBe("twoConditions"); expect(ordered[2]?.rule.name).toBe("oneCondition"); }); it("salience and specificity ties broken by insertion order ascending", () => { const activations = [ mkActivation("added2", 5, 2, 2), mkActivation("added0", 5, 2, 0), mkActivation("added1", 5, 2, 1), ]; const ordered = orderActivations(activations); expect(ordered[0]?.rule.name).toBe("added0"); expect(ordered[1]?.rule.name).toBe("added1"); expect(ordered[2]?.rule.name).toBe("added2"); }); it("mixed case: salience, then specificity, then addedAt", () => { const activations = [ mkActivation("ruleC", 5, 3, 2), // low salience, many conditions mkActivation("ruleA", 10, 1, 0), // high salience, 1 condition mkActivation("ruleB", 10, 2, 1), // high salience, 2 conditions ]; const ordered = orderActivations(activations); // B > A (same salience, B has more conditions) // A > C (higher salience) expect(ordered[0]?.rule.name).toBe("ruleB"); expect(ordered[1]?.rule.name).toBe("ruleA"); expect(ordered[2]?.rule.name).toBe("ruleC"); }); it("empty activations list → empty result", () => { expect(orderActivations([])).toHaveLength(0); }); it("single activation → unchanged", () => { const single = [mkActivation("only", 10, 2, 0)]; const ordered = orderActivations(single); expect(ordered).toHaveLength(1); expect(ordered[0]?.rule.name).toBe("only"); }); it("orderActivations does not mutate the input array", () => { const activations = [ mkActivation("b", 5, 1, 0), mkActivation("a", 10, 1, 1), ]; const original = [...activations]; orderActivations(activations); expect(activations[0]?.rule.name).toBe(original[0]?.rule.name); expect(activations[1]?.rule.name).toBe(original[1]?.rule.name); }); it("Session._getOrderableRules returns rules in addedAt order with correct salience", () => { const registry = new HandlerRegistry(); registry.register("noop", () => {}); const session = new Session({ autoFire: false }); const r1 = defineRule({ name: "rule1", salience: 0, what: [{ id: null, attr: "X", binding: v("x") }], handler: "noop", registry, }); const r2 = defineRule({ name: "rule2", salience: 10, what: [{ id: null, attr: "Y", binding: v("y") }], handler: "noop", registry, }); session.add(r1); session.add(r2); const orderable = session._getOrderableRules(); expect(orderable).toHaveLength(2); expect(orderable[0]?.name).toBe("rule1"); expect(orderable[0]?.salience).toBe(0); expect(orderable[0]?.addedAt).toBe(0); expect(orderable[1]?.name).toBe("rule2"); expect(orderable[1]?.salience).toBe(10); expect(orderable[1]?.addedAt).toBe(1); }); });