npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@arbitre/core

v0.3.1

Published

Rete-inspired production rule engine for declarative state governance

Readme

@arbitre/core

Rete-inspired production rule engine for declarative state governance.

What's New in 0.2.0

  • Beta network / multi-fact joins — Rules can match across multiple fact types with join constraints
  • Temporal operators — Time-based conditions (since, within, after) with clock abstraction
  • Clock abstraction — Real and virtual clocks for deterministic testing
  • Windowed accumulation — Aggregate facts over sliding time windows
  • Cross-type accumulation — Accumulate across multiple fact types in a single rule
  • Custom accumulate functions — Register domain-specific aggregation logic
  • Pattern validation — Compile-time validation of fact patterns and join constraints

Overview

@arbitre/core implements a forward-chaining production rule engine based on the RETE algorithm. It evaluates rules against a working memory of state and facts, resolving conflicts via salience and activation groups, and applies changes through a pipeline-style action system.

Key features:

  • Match-Resolve-Act cycle with configurable limits and conflict resolution
  • Alpha network for fast single-condition filtering
  • Beta network for multi-fact joins with bind variables
  • Truth Maintenance System (TMS) for automatic retraction of derived state
  • Namespaced state with dot-path access
  • Temporal operators for time-based reasoning
  • Accumulate nodes for aggregation patterns

Quick Start

import { createSession } from "@arbitre/core";

const session = createSession({
  rules: [
    {
      name: "high-temp-alert",
      when: { $gt: [{ $path: "sensors.temperature" }, 100] },
      then: [{ $set: { "alerts.overheating": true } }],
      salience: 10,
    },
  ],
  initialState: {
    sensors: { temperature: 105 },
    alerts: { overheating: false },
  },
});

const result = session.fire();
// result.rulesFired === 1
// session.getPath("alerts.overheating") === true

Multi-Fact Joins

const session = createSession({
  factTypes: [
    { type: "order", fields: { customerId: "string", total: "number" } },
    { type: "customer", fields: { id: "string", tier: "string" } },
  ],
  rules: [
    {
      name: "vip-large-order",
      patterns: [
        { type: "order", bind: "o", constraints: [{ field: "total", op: "gt", value: 1000 }] },
        { type: "customer", bind: "c", constraints: [{ field: "tier", op: "eq", value: "vip" }] },
      ],
      when: { $eq: [{ $path: "o.customerId" }, { $path: "c.id" }] },
      then: [{ $set: { "notifications.vipLargeOrder": true } }],
    },
  ],
});

session.assertFact("customer", { id: "c1", tier: "vip" });
session.assertFact("order", { customerId: "c1", total: 2500 });
session.fire();

Clock-based rules

import { createSession, createVirtualClock } from "@arbitre/core";

const clock = createVirtualClock(0);
const session = createSession({
  clock,
  rules: [
    {
      name: "deadline",
      when: { "$meta.$now": { $gt: 30000 } },
      then: [{ $set: { "user.status": "idle" } }],
    },
  ],
  initialState: { user: { lastActive: 0, status: "active" } },
});

clock.advance(31000);
session.tick();
// user.status === "idle"

Architecture

Match-Resolve-Act Cycle

Each call to session.fire() runs a forward-chaining loop:

  1. Match — Evaluate all rule conditions against current state/facts
  2. Resolve — Collect activated rules into the agenda, order by salience
  3. Act — Fire the highest-priority rule, apply then stages, repeat

The cycle terminates when no rules activate or limits are reached.

Alpha Network

Single-condition nodes that filter facts by type and field constraints. Each fact assertion propagates through alpha nodes to determine which rules could potentially match.

Beta Network

Join nodes that combine partial matches from multiple alpha nodes. Beta nodes maintain a token memory of partial matches and produce complete matches when join constraints are satisfied.

Agenda

Priority queue of activated rule instances. Conflict resolution strategy:

  • Salience — Higher salience fires first (default: 0)
  • Activation groups — Only one rule per group fires per cycle
  • Recency — Among equal-salience rules, most recently activated wins

Truth Maintenance System (TMS)

Tracks which rules produced which state changes. When a rule's conditions become false, TMS can automatically retract its contributions:

  • autoRetract: "all" — Retract all derived state when justification is lost
  • autoRetract: "ui-contributions" — Retract only UI-namespace contributions

Namespaces

State is organized as a flat dot-path map. Paths like "sensors.temperature" and "alerts.overheating" provide logical grouping without nested object complexity.

API Reference

Exports

// Main entry point
import { createSession } from "@arbitre/core";

// Testing utilities
import { createTestSession, fireWith, assertRuleFired, assertRuleNotFired, assertState } from "@arbitre/core/testing";

// Debug utilities
import { explainResult, formatChanges, dumpState } from "@arbitre/core/debug";

createSession(config: SessionConfig): RuleSession

Factory function that compiles rules and returns a session instance.

SessionConfig:

| Field | Type | Description | |-------|------|-------------| | rules | ProductionRule[] | Rules to compile into the network | | initialState | Record<string, unknown> | Starting state | | expressions | ArbitreExpressionConfig | Pure namespaced Kuery profile extensions and stricter bounds | | limits | SessionLimits | Cycle/firing limits | | tms | TmsConfig | Truth maintenance configuration | | errorHandling | "strict" \| "lenient" | Error behavior | | factTypes | FactTypeDefinition[] | Fact type schemas | | accumulates | AccumulateConfig[] | Accumulation definitions | | accumulateFunctions | Record<string, CustomAccumulateFunction> | Custom aggregation functions | | clock | ArbiterClock | Clock for temporal operators | | autoFireOnFactChange | boolean | Auto-fire on fact assert/retract |

RuleSession

| Method | Description | |--------|-------------| | registerRule(rule) | Add a rule at runtime | | removeRule(name) | Remove a rule by name | | assert(path, value) | Assert a state value | | retract(path) | Retract a state value | | fire() | Run the match-resolve-act cycle | | subscribe(path, cb) | Watch a path for changes | | update(path, value) | Assert + fire in one call | | getState() | Get full state snapshot | | getPath(path) | Get value at a dot-path | | setFocus(group) | Set the active activation group | | dispose() | Clean up resources | | assertFact(type, data) | Assert a fact, returns fact ID | | retractFact(id) | Retract a fact by ID | | getFacts(type) | Get all facts of a type | | tick(now?) | Advance temporal evaluation | | scheduleRule(name, opts) | Schedule a rule for future firing | | cancelSchedule(name) | Cancel a scheduled rule |

ProductionRule

interface ProductionRule<TState> {
  name: string;
  when: TypedQuery<TState>;          // kuery expression
  then: ThenStage<TState>[];         // pipeline stages
  else?: ThenStage<TState>[];        // stages when condition is false
  salience?: number;                  // priority (default: 0)
  activationGroup?: string;           // mutex group
  onConflict?: "override" | "warn" | "error";
  enabled?: boolean;
  description?: string;
  expires?: number;                   // TTL in ms
  patterns?: FactPattern[];           // multi-fact patterns
  accumulate?: AccumulateConfig[];    // aggregation configs
}

Action Types (ThenStage operators)

| Operator | Description | |----------|-------------| | $set | Set values at paths | | $unset | Remove values at paths | | $inc | Increment numeric values | | $push | Append to arrays | | $pull | Remove from arrays | | $merge | Shallow merge objects |

RHS value expressions

when continues to use Kuery query syntax. RHS values use the immutable arbitre-v1 strict expression profile. Mongo-inspired shorthand is lowered once at registration:

import { expression, literal } from "@arbitre/core";

const rule = {
  name: "total",
  when: { enabled: true },
  then: [{ $set: {
    total: { $sum: ["$price", "$tax"] },
    decision: { $switch: { branches: [
      { case: "$approved", then: "yes" }
    ], default: "no" } },
	deadline: { $rtime: "+1d" },
	isRecent: { $within: ["$createdAt", 30000] },
    data: literal({ $sum: [1, 2] }),
    canonical: expression({ kind: "op", op: "add", args: [
      { kind: "literal", value: 1 }, { kind: "literal", value: 2 }
    ] })
  }}]
};

Each RHS entry resolves against live state immediately before its write. Declared token bindings take precedence over root paths. Use session.introspect.getRuleDependencies(name) to inspect condition reads, RHS reads, action writes, and token-binding reads; only condition reads schedule rules. actionWrites contains statically known built-in paths. Rules containing a custom stage also expose actionWritesUnknown: true, without treating custom entry keys as writes; runtime state changes remain exact.

Strict shorthand includes arithmetic ($sum, $multiply, $subtract, $divide), $min/$max/$avg, rounding, string-only $concat, comparisons, boolean logic, membership, $exists, $ifNull, lazy $cond, $switch, $rtime, $after, $before, $elapsed, and $within. $switch evaluates strict-boolean cases in order and only evaluates its selected result. $rtime accepts signed fixed-unit durations (ms, s, m, h, or a fixed 86,400,000 ms d) and produces epoch milliseconds from an explicitly configured clock. There is no $since alias. Variadic $sum/$multiply lower to standard add/mul. $foo.bar selects a declared foo binding, registered $foo namespace, or root path in that order; $$foo.bar forces a declared namespace. Dot characters delimit safe segments and are not literal key characters. Extensions are canonical-only and must be invoked through expression(...); an extension name does not create shorthand. See ADR 0001 for the complete migration table and architectural trust boundary.

$inc uses zero only for an absent terminal own property and otherwise requires an inspectable finite existing number, finite amount, and finite result. $merge shallow-copies exact plain data objects only: Object.prototype and null prototypes are accepted, while exotic prototypes, accessors, symbols, non-enumerables, and unsafe keys are rejected before copying. Missing merges preserve the RHS prototype; existing merges preserve the target prototype, and all materialized properties remain writable for later stages. Commit-path failures are reported with only the operator, rule, path, and a stable reason.

The supported state boundary is trusted plain-data graphs, such as decoded JSON, including arrays and null-prototype containers. Validation failures on supported data occur before writes. Hostile or stateful proxies, accessors, exotic objects, and callbacks are not sandboxed: their traps or code may cause external mutation that Arbitre cannot roll back. getState() and the scope passed to custom stages are convenience views, not deeply immutable security boundaries. Custom stages are trusted, raw, and scope-aware; they must use their supplied write callback for tracked StateChange and provenance records. Checkpoint and provenance snapshots preserve cycles, shared references, and null prototypes within supported containers. Kuery owns bounded expression compilation/evaluation and lazy expression semantics; Arbitre owns lowering, live resolution, write ordering, scheduling, TMS, effects, and custom stages.

Condition operators

| Operator | Description | |----------|-------------| | $eq | Equality | | $ne | Not equal | | $gt / $gte | Greater than (or equal) | | $lt / $lte | Less than (or equal) | | $and / $or / $not | Logical combinators | | $in / $nin | Set membership | | $exists | Path existence check | | $regex | Regular expression match |

Temporal behavior

Clock expression sugar is available only when clock is explicitly configured; Arbitre never supplies an implicit wall clock. $after(t) means now > t, $before(t) means now < t, $elapsed([t, d]) means now - t > d, and $within([t, d]) means now - t < d; equality is false and negative values use the same direct arithmetic. A missing clock or operand returns false, a denied reference remains an error, and a present wrong type is a Kuery type mismatch. Guarded present operands can resolve twice against state that is stable during one expression evaluation. Temporal RHS reads are static dependency metadata and do not independently schedule/refire a rule; use tick with a time condition when reactivity is required. Schedules, rule expiry, and windowed accumulation remain separate orchestration features.

Error Codes

| Code | Description | |------|-------------| | ARBITER_RULE_COMPILATION_FAILED | Rule could not be compiled | | ARBITER_INVALID_PATH | Invalid dot-path | | ARBITER_INVALID_OPERATOR | Unknown operator in then/when | | ARBITER_CYCLE_LIMIT_EXCEEDED | Max cycles reached | | ARBITER_FIRING_LIMIT_EXCEEDED | Max firings reached | | ARBITER_WRITE_CONFLICT | Multiple rules writing same path | | ARBITER_TMS_RETRACT_FAILED | TMS retraction error | | ARBITER_INVALID_NAMESPACE | Invalid namespace in path | | ARBITER_SESSION_DISPOSED | Operation on disposed session | | ARBITER_PROTOTYPE_POLLUTION | Prototype pollution attempt blocked | | ARBITER_EXPRESSION_COMPILATION_FAILED | RHS expression failed bounded registration-time compilation | | ARBITER_EXPRESSION_EVALUATION_FAILED | RHS expression evaluation error | | ARBITER_RULE_NOT_FOUND | Referenced rule does not exist | | ARBITER_INVALID_CLOCK_OPERATION | Invalid clock operation |

Testing Utilities

Import from @arbitre/core/testing:

import { createTestSession, fireWith, assertRuleFired, assertRuleNotFired, assertState } from "@arbitre/core/testing";

// Quick session for tests
const session = createTestSession(rules, { counter: 0 });

// Fire rules and get result in one call
const result = fireWith(rules, { temperature: 150 });

// Assertions
assertRuleFired(result, "high-temp-alert");
assertRuleNotFired(result, "low-temp-alert");
assertState(session, "counter", 1);

Debug Utilities

Import from @arbitre/core/debug:

import { explainResult, formatChanges, dumpState } from "@arbitre/core/debug";

const result = session.fire();

console.log(explainResult(result));
// Fired 2 rules in 1 cycles
// Changes:
//   alerts.overheating: false → true (by high-temp-alert)
// Warnings: none

console.log(formatChanges(result));
console.log(dumpState(session));

Performance

The RETE network provides efficient incremental evaluation:

  • Alpha network — O(1) per fact type lookup, linear in constraints per node
  • Beta network — Incremental join; only new tokens propagate
  • State changes — Only affected rules re-evaluate (no full re-scan)
  • Cycle limits — Configurable guards prevent runaway inference

Typical performance for rule sets under 100 rules with moderate fact counts:

  • Single-fact evaluation: < 1ms
  • Multi-fact joins (10 facts × 10 facts): < 5ms
  • Full cycle with TMS: < 10ms

Rule Builder

For programmatic rule construction, use the defineRule fluent builder:

import { defineRule } from "@arbitre/core";

const rule = defineRule("high-value-order")
  .when({ $gt: [{ $path: "order.total" }, 1000] })
  .then([{ $set: { "flags.highValue": true } }])
  .salience(5)
  .description("Flag orders over $1000")
  .build();

Observability

Lifecycle Hooks

import { createSession } from "@arbitre/core";
import type { SessionHooks } from "@arbitre/core";

const hooks: SessionHooks = {
  onRuleActivated: (e) => console.log(`Activated: ${e.ruleName}`),
  onRuleFired: (e) => console.log(`Fired: ${e.ruleName}`),
  onRuleDeactivated: (e) => console.log(`Deactivated: ${e.ruleName}`),
  onFactAsserted: (e) => console.log(`Fact+: ${e.factType}#${e.factId}`),
  onFactRetracted: (e) => console.log(`Fact-: ${e.factType}#${e.factId}`),
  onCycleStart: (e) => console.log(`Cycle ${e.cycleNumber} start`),
  onCycleEnd: (e) => console.log(`Cycle ${e.cycleNumber} end`),
};

const session = createSession({ rules, hooks });

Custom Logger

import type { ArbiterLogger } from "@arbitre/core";

const logger: ArbiterLogger = {
  debug: (msg, ctx) => myLogger.debug(msg, ctx),
  info: (msg, ctx) => myLogger.info(msg, ctx),
  warn: (msg, ctx) => myLogger.warn(msg, ctx),
  error: (msg, ctx) => myLogger.error(msg, ctx),
};

const session = createSession({ rules, logger });

Introspection

const session = createSession({ rules, initialState });
session.fire();

const i = session.introspect;
console.log(i.getRegisteredRules());  // ["rule-a", "rule-b"]
console.log(i.getActiveRules());      // ["rule-a"]
console.log(i.getAgendaEntries());    // [] (empty after fire)
console.log(i.getFactCounts());       // { order: 3 }
console.log(i.getMetrics());          // { totalRulesFired: 2, ... }

Dependencies

| Dependency | Role | |------------|------| | kuery 2.1+ | Bounded expression runtime for rule conditions and RHS values |

License

MIT