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

brms-ts

v1.1.0

Published

Motor de reglas de negocio en TypeScript estilo Drools: reglas declarativas JSON/YAML ejecutadas por un motor de inferencia forward-chaining tipo RETE.

Downloads

677

Readme

brms-ts

npm version npm downloads license types

A business rule management engine for TypeScript, inspired by Drools. Declarative rules are written in JSON or YAML and executed by a forward-chaining inference engine (RETE-style) with an incremental agenda, salience-based conflict resolution, and no-loop control.

  • Declarative rules in JSON or YAML, validated against a schema.
  • Rich conditions: eq, neq, gt, gte, lt, lte, in, contains, between, startsWith, endsWith, matches, isEmpty, and exists, combined with and / or / not, plus custom predicates.
  • Fact type selectors: a rule can be scoped to one fact type or a list of types.
  • Field references: rule values can reference fields of the bound fact ({ $fact: 'path' }).
  • Actions: insert, modify, and retract facts, or invoke registered functions.
  • Forward chaining: actions change facts, which re-trigger rules incrementally.
  • Conflict resolution by salience (highest first), tie-broken by rule order.
  • Safe by design: rule data is interpreted, never eval-ed.
  • Typed, documented, and dual-published (ESM + CommonJS) with generated docs.

Installation

npm install brms-ts

The package ships as ESM and CommonJS with bundled type definitions.

Quick start

import { RuleEngine } from 'brms-ts';

const engine = new RuleEngine();

// Register a custom function referenced by a rule action.
engine.registerFunction('approve', (facts) => {
  const id = facts[0]?.attributes['id'];
  console.log('approved', typeof id === 'string' ? id : '');
});

// Load rules from YAML (JSON is also accepted).
engine.loadRules(`
rules:
  - name: approve-adults
    salience: 10
    noLoop: true
    when:
      kind: comparison
      field: age
      operator: gte
      value: 18
    then:
      - { kind: invoke, function: approve }
`);

engine.insert({ type: 'Applicant', attributes: { id: 'a1', age: 20 } });
engine.fireAllRules();

Rule format

A rules document is an object with a rules array. Each rule has a name, an optional salience (default 0), noLoop flag (default false), and type fact selector (default: any type), a when condition, and a then list of actions.

rules:
  - name: vip-discount
    salience: 10 # higher fires first
    noLoop: true # do not re-activate from its own changes
    type: Order # only facts of this type (string, or array of alternatives)
    when:
      kind: and
      conditions:
        - { kind: comparison, field: amount, operator: gte, value: 100 }
        - { kind: predicate, predicate: isVip } # custom predicate
    then:
      - { kind: modify, target: 0, attributes: { discount: 0.2 } }
      - { kind: invoke, function: audit, args: ['discount-applied'] }

Conditions (when)

| Kind | Shape | Meaning | | ------------ | ----------------------------------- | ---------------------------------------------------------------------- | | comparison | { kind, field, operator, value? } | Compare a fact field against a value (no value for unary operators). | | predicate | { kind, predicate, args? } | Call a registered custom predicate. | | and | { kind, conditions: [...] } | All sub-conditions must hold. | | or | { kind, conditions: [...] } | At least one sub-condition must hold. | | not | { kind, condition } | The sub-condition must not hold. |

Comparison operators:

| Operator | Meaning | | ------------------------------------- | ------------------------------------------------------------------------------------------ | | eq, neq, gt, gte, lt, lte | Equality and ordering. Ordering only compares number-to-number or string-to-string. | | in | The field value is a member of the given array (strict equality). | | contains | Substring match for strings; membership for arrays. | | between | The field value lies within the inclusive [min, max] range (two numbers or two strings). | | startsWith, endsWith | String prefix/suffix match. | | matches | The field string matches the given regular-expression pattern. | | isEmpty | The field is absent, null, '', [], or {}. Takes no value. | | exists | The field is present and not null. Takes no value. |

Fields support dot notation for nested attributes, e.g. address.city.

Field references ($fact)

Any rule value position accepts a reference to a field of the fact bound to the rule, written as { $fact: 'path' }. The path uses the same dot notation as comparison fields:

rules:
  - name: within-limit
    when: { kind: comparison, field: discount, operator: lte, value: { $fact: maxDiscount } }
    then:
      - { kind: invoke, function: notify, args: [{ $fact: id }] }

Accepted positions: the value of binary comparisons, predicate.args, invoke.args, and the attribute values of insert and modify actions (the type of an inserted fact is always a literal string). $fact is a reserved key: an object carrying it must be exactly a reference, otherwise the document is rejected. References that do not resolve behave like a missing field in comparisons (no match) and are replaced by null in action positions.

matches patterns

Patterns are validated when rules are loaded: they must be a literal string (no $fact references), at most 256 characters, compilable, and free of a quantified group with an inner quantifier (for example (a+)+), which guards against catastrophic backtracking. The check is a conservative heuristic, not a linear-time proof: harmless patterns such as (a+b|c)+ may be rejected, and ambiguity through alternation (for example (a|aa)+) is not detected, so review untrusted patterns like code.

Value semantics

The comparison policy is deliberate and covered by tests:

  • No coercion: 1 and '1' are never equal, and ordering compares numbers with numbers and strings with strings only; anything else does not match.
  • A missing field is not null: eq with value: null only matches an explicit null, while neq matches the absent field.
  • in and contains use strict equality; contains is a substring test for strings and membership for arrays.
  • isEmpty matches an absent field, null, '', [], and {}; 0 and false are not empty. exists matches anything present that is not null.
  • Binary operators with no value declared never match (programmatic rules only; the loader rejects such documents). A stray value on isEmpty or exists is ignored at runtime.

Actions (then)

| Kind | Shape | Effect | | --------- | -------------------------------------- | --------------------------------------- | | insert | { kind, fact: { type, attributes } } | Insert a new fact. | | modify | { kind, target: 0, attributes } | Merge attributes into the matched fact. | | retract | { kind, target: 0 } | Remove the matched fact. | | invoke | { kind, function, args? } | Call a registered function. |

target: 0 refers to the fact that activated the rule.

Programmatic API

const engine = new RuleEngine({ cycleLimit: 10_000 });

engine.registerPredicate('isVip', (fact) => fact.attributes['tier'] === 'gold');
engine.registerFunction('audit', (facts, args) => {
  /* side effect */
});

engine.loadRules(source); // string (YAML/JSON), parsed object, or typed Rule[]
const record = engine.insert({ type: 'Order', attributes: { amount: 150 } });
engine.modify(record.id, { amount: 200 });
engine.retract(record.id);

const fired = engine.fireAllRules(); // number of rules fired
const facts = engine.getFacts(); // current fact snapshot

Rules can also be built in code with typed factory helpers:

import { defineRule, and, compare, predicate } from 'brms-ts';

const rule = defineRule({
  name: 'vip-discount',
  salience: 10,
  noLoop: true,
  when: and(compare('amount', 'gte', 100), predicate('isVip')),
  then: [{ kind: 'modify', target: 0, attributes: { discount: 0.2 } }],
});

Example

A runnable credit-approval example lives in the examples/credit-approval directory of the repository. Run it with:

npm run example

It loads rules from rules.yaml, registers a custom predicate and functions, evaluates several applications, and prints the resulting decisions.

Design notes and limitations

  • Single-fact matching. A rule's when is evaluated against one fact at a time; each match produces one activation bound to that fact. Cross-fact multi-pattern joins are intentionally out of scope for this version.
  • Termination. no-loop prevents a rule from re-activating itself from its own changes, and a configurable cycleLimit aborts runaway inference with a CycleLimitExceededError.
  • Security. Conditions and actions are interpreted from data. The engine never uses eval or new Function, and nested field access refuses prototype-polluting keys.

Development

npm run verify      # lint + typecheck + test + build
npm test            # unit and integration tests with coverage thresholds
npm run lint        # ESLint (strict, type-checked, no `any`)
npm run format      # Prettier
npm run docs        # generate API docs with TypeDoc into ./docs
npm run check:pkg   # publint + are-the-types-wrong packaging checks

Commits follow Conventional Commits and are validated by commitlint via a Husky commit-msg hook.

Releasing

Releases are automated with semantic-release through the .github/workflows/ci.yml GitHub Actions workflow. An authorize gate first checks that the pull-request author has write/admin access, so external fork PRs do not run CI unattended. The quality job then runs on every authorized pull request and push to main (lint, typecheck, test, build, package checks); the release job runs only on pushes to main after quality passes, and publishes to npm (with provenance) and creates a GitHub Release. Set an NPM_TOKEN repository secret (an npm automation token) to enable publishing; GITHUB_TOKEN is provided automatically. Provenance requires the repository to be public.

License

MIT