Skip to content

pametan/decide

Repository files navigation

decide

A tiny decision-table and scorecard evaluator with explainable, auditable output. You express the decision as data; it returns the outcome and a full trace of which rules fired and why. The engine, not a platform.

TypeScript-first, zero runtime dependencies, and safe by construction — no eval, no Function, no I/O.

import { evaluate, explain } from '@pametan/decide';
import { affordabilityScorecard } from '@pametan/decide/examples';

const result = evaluate(affordabilityScorecard, { age: 22, incomeMonthly: 1800, existingDebt: 900 });
// result.outcome === 'decline', result.score === 30
explain(result); // ['Existing debt >= 500', 'Thin-file age band']

Why

Approve / decline / refer, risk bands, pricing tiers, KYC flags — these decisions too often live as tangled if/else code that's hard to change, hard for risk and compliance to read, and very hard to explain after the fact. Regulators increasingly require that explanation (FCA Consumer Duty and CONC; ECOA/Reg B adverse-action reason codes). decide keeps the decision as declarative data and hands back a trace you can log or turn into a customer-facing reason.

Install

npm install @pametan/decide

Requires Node 24+. Ships ESM with bundled type declarations.

Two models

Decision table

Rules of conditions → outcomes, with a configurable hit policy:

import { evaluate } from '@pametan/decide';
import type { DecisionTable } from '@pametan/decide';

const table: DecisionTable = {
  kind: 'table',
  name: 'eligibility',
  hitPolicy: 'first', // 'first' | 'collect' | 'priority'
  rules: [
    { id: 'under-18', when: { field: 'age', op: 'lt', value: 18 }, outcome: 'decline', reason: 'Under 18' },
    { id: 'tenure', when: { expr: 'employmentMonths < 6' }, outcome: 'refer', reason: 'Short tenure' },
  ],
  fallback: 'approve',
};

evaluate(table, { age: 30, employmentMonths: 24 }).outcome; // 'approve'
  • first — first matching rule wins (deterministic; evaluation stops there).
  • priority — among matches, highest priority wins.
  • collect — returns every matching outcome as a list, or — with collectMerge: true and object outcomes — one shallow-merged object.

Scorecard

Characteristics contribute points (first matching attribute each); the score falls into a band:

import type { Scorecard } from '@pametan/decide';

const card: Scorecard = {
  kind: 'scorecard',
  name: 'affordability',
  base: 50,
  characteristics: [
    { id: 'dti', attributes: [
      { when: { field: 'existingDebt', op: 'gte', value: 500 }, points: -15, reason: 'Debt >= 500' },
      { when: { field: 'existingDebt', op: 'lt', value: 500 }, points: 5 },
    ] },
  ],
  bands: [
    { min: -100, max: 34, outcome: 'decline' },
    { min: 35, max: 50, outcome: 'refer' },
    { min: 51, max: 100, outcome: 'approve' },
  ],
};

Conditions — three forms, mix freely

// 1. Fixed operators (declarative, serialisable — the default)
{ field: 'score', op: 'between', value: [600, 800] }
// ops: eq ne gt gte lt lte in nin between matches exists

// 2. Named custom predicates (supplied at evaluation time)
evaluate(model, input, { predicates: { thinFile: (i) => /* ... */ true } });
// referenced as: { predicate: 'thinFile' }

// 3. A safe expression string
{ expr: 'age >= 18 && country == "GB"' }

Combine with { all: [...] }, { any: [...] }, { not: ... }.

The expression language is deliberately tiny and safe. It supports only field references, number/string/boolean/null literals, the comparison operators == != > >= < <=, the boolean operators && || !, and parentheses. There is no arithmetic, no function calls, no member/index access, no eval — so even untrusted rule text cannot execute code. Anything outside the grammar is a parse error. You can also restrict which forms are allowed per evaluation via allow: { operators, predicates, expressions }.

The result

interface DecisionResult {
  outcome: Outcome | Outcome[] | null; // list under 'collect'; null if no match & no fallback
  matched: boolean;
  trace: TraceEntry[];                 // every rule/attribute considered, with a readable detail
  score?: number;                      // scorecard only
  band?: string;                       // scorecard only
}

explain(result) returns the fired reasons as string[]. The whole trace is shaped to be logged verbatim (e.g. by @pametan/pii-redact-safe audit logging).

API

Export Description
evaluate(model, input, options?) Evaluate a table or scorecard.
evaluateTable / evaluateScorecard Model-specific entry points.
createEvaluator(model, options?) Bind a model + options once.
validateModel(model, options?) Static checks → list of error strings.
explain(result) Fired reasons as string[].
evaluateExpression, parseExpression The safe expression evaluator.
evaluateCondition Evaluate a single condition tree.

Three ready-made example models ship under @pametan/decide/examples (lendingEligibilityTable, affordabilityScorecard, kycRiskTable). All types are exported.

Validate before you ship a model

import { validateModel } from '@pametan/decide';
validateModel(model); // [] when valid; otherwise unknown operators, bad expressions,
                      // unknown predicates, overlapping/inverted bands, etc.

Notes

  • Deterministic and non-mutating; same model + input → same result + trace.
  • For money, use integer minor units (pence/cents) to avoid floating-point drift.

Development

npm install
npm run typecheck
npm test          # operators, hit policies, scorecard, safe-expression, examples
npm run build     # emit dist/

Disclaimer

Provided as an engineering aid, not legal, financial or compliance advice. The rules you encode — and whether they meet your regulatory obligations — are yours to verify. MIT licensed — see LICENSE.


Need the production version of this?

We're Pametan — a specialist fintech/regtech engineering agency working across UK, US and Canadian rails (FCA · CFPB · FCAC). We build the regulated, audited decisioning systems these primitives sit inside: lending decision engines, scorecards, and the audit trails around them.

Talk to us →

About

A tiny decision-table and scorecard evaluator with explainable, auditable output. Fixed operators, custom predicates and a safe expression language. TypeScript, zero-dependency.

Topics

Resources

License

Contributing

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors