Complex conditional logic is hard to read, impossible to explain, and painful to test. rulewrite turns each business condition into a named, typed predicate — composable with logical operators and evaluable as a complete diagnostic tree.
Business logic accumulates. What starts as a simple check becomes an unreadable wall of booleans — untestable, unexplainable, and impossible to reuse.
if (
user.status === 'active' &&
user.age >= 18 &&
user.emailVerified &&
!product.archived &&
product.stockLevel > 0 &&
product.countries.includes(user.country) &&
(
product.price <= user.creditLimit ||
user.roles.includes('admin')
)
) {
// can this user even check out?
// which condition blocked them?
// can we test this independently?
allowCheckout();
}
Not testable in isolation. Not reusable across the codebase. Silent on failure — when it returns false, you don't know why.
const canCheckout = all(
customerIsAdult,
customerIsVerified,
customerIsActive,
productIsInStock,
isAvailableInCountry,
isWithinCreditLimit,
);
const result = canCheckout.evaluate(ctx);
// {
// satisfied: false,
// label: 'ALL',
// children: [
// { satisfied: true, label: 'IsAdult' },
// { satisfied: false, label: 'IsEmailVerified' },
// ...
// }
Each condition is named and independently testable. Evaluation tells you exactly which rule failed. The same rules are reusable across every policy in your codebase.
Define atomic rules over your domain types. Compose them into policies using logical operators. Evaluate to get a boolean or a complete diagnostic tree.
Each business condition becomes a named, typed rule. One condition, one rule, one name that appears everywhere it is used.
import { rule } from '@daltonr/rulewrite';
const isAdult = rule<User>(
u => u.age >= 18,
'IsAdult'
);
const isVerified = rule<User>(
u => u.emailVerified,
'IsEmailVerified'
);
Combine rules with AND, OR, NOT, IMPLIES, and PREVENTS. Every result is a rule — compose without limit.
// Simple composition
const canRegister =
isAdult.and(isVerified);
// Conditional requirement
const agePolicy =
isAgeRestricted.implies(isVerified);
// Flat list with all()
const canCheckout = all(
customerIsAdult,
productIsInStock,
isWithinCreditLimit,
);
isSatisfiedBy() for fast checks. evaluate() for the complete diagnostic tree — every sub-rule, every outcome.
// Fast boolean check
canRegister.isSatisfiedBy(user);
// → false
// Full diagnostic tree
canRegister.evaluate(user);
// {
// satisfied: false,
// label: 'AND',
// children: [
// { satisfied: true, label: 'IsAdult' },
// { satisfied: false, label: 'IsEmailVerified' }
// ]
// }
From a single atomic predicate to a production-grade access control policy — rulewrite scales with you.
Every rule carries a label. Labels appear in evaluation trees, audit logs, and user-facing messages — no more silent failures.
AND, OR, NOT, IMPLIES, and PREVENTS. IMPLIES handles conditional requirements precisely — things AND gets wrong.
Define rules over their natural type. Use .on() to lift them into any wider context without coupling or duplication.
someOf, allOf, noneOf — apply any rule across a collection. Works with implications for powerful group policies.
Every evaluate() call returns a recursive tree. Walk it to generate error messages, build audit logs, or render diagnostics.
Rules carry their subject type. The compiler prevents composing rules over incompatible types before you run a single test.
No runtime dependencies. Nothing to audit, nothing to update, nothing that breaks. Just TypeScript.
Pure functions of their inputs. Test each atomic rule in isolation. No mocking, no setup, no framework required.
From a single rule to a complete access control policy in minutes.
import { rule } from '@daltonr/rulewrite';
interface User {
age: number;
emailVerified: boolean;
status: 'active' | 'suspended';
roles: string[];
}
// Each rule is a named predicate over a single type.
// The label is used in evaluation output and audit logs.
export const isAdult = rule<User>(
u => u.age >= 18,
'IsAdult'
);
export const isEmailVerified = rule<User>(
u => u.emailVerified,
'IsEmailVerified'
);
export const isActive = rule<User>(
u => u.status === 'active',
'IsAccountActive'
);
export const isAdmin = rule<User>(
u => u.roles.includes('admin'),
'IsAdmin'
);
import { all, rule } from '@daltonr/rulewrite';
import { isAdult, isEmailVerified, isActive } from '../users/rules';
import { isInStock, requiresPrescription } from '../products/rules';
type CheckoutContext = { customer: User; product: Product };
// Project user rules into the checkout context
const customerIsAdult = isAdult.on((ctx: CheckoutContext) => ctx.customer);
const customerIsVerified = isEmailVerified.on((ctx: CheckoutContext) => ctx.customer);
const customerIsActive = isActive.on((ctx: CheckoutContext) => ctx.customer);
const productIsInStock = isInStock.on((ctx: CheckoutContext) => ctx.product);
// IMPLIES: prescription only required for prescription products
const prescriptionPolicy = requiresPrescription
.on(ctx => ctx.product)
.implies(hasPrescription.on(ctx => ctx.customer));
// all() produces a flat evaluation tree — easier to read than chaining
export const canCheckout = all(
customerIsAdult,
customerIsVerified,
customerIsActive,
productIsInStock,
prescriptionPolicy,
);
import { EvaluationResult } from '@daltonr/rulewrite';
// Walk the tree to collect every failed leaf node
function failures(result: EvaluationResult): string[] {
if (!result.satisfied && !result.children?.length) return [result.label];
return (result.children ?? []).flatMap(failures);
}
// Message catalogue: labels map to user-facing text
const messages: Record<string, string> = {
IsAdult: 'You must be 18 or older to purchase this item.',
IsEmailVerified: 'Please verify your email address before checking out.',
IsAccountActive: 'Your account has been suspended.',
IsInStock: 'This item is currently out of stock.',
};
async function processCheckout(ctx: CheckoutContext) {
const result = canCheckout.evaluate(ctx);
// Log the full tree for audit — permanently records why the decision was made
await auditLog.record({ action: 'checkout_attempt', policy: result });
if (!result.satisfied) {
throw new ValidationError(
failures(result).map(label => messages[label])
);
}
await completeCheckout(ctx);
}
type AuthContext = { user: User; document: Document };
// Atomic rules over each type
const isActive = rule<User>(u => u.status === 'active', 'IsActive');
const isAdmin = rule<User>(u => u.roles.includes('admin'), 'IsAdmin');
const isArchived = rule<Document>(d => d.archived, 'IsArchived');
// Cross-type rules written directly against the context
const isOwner = rule<AuthContext>(
ctx => ctx.document.ownerId === ctx.user.id, 'IsOwner'
);
// Project rules into the shared context
const userIsActive = isActive.on((ctx: AuthContext) => ctx.user);
const userIsAdmin = isAdmin.on((ctx: AuthContext) => ctx.user);
const docIsArchived = isArchived.on((ctx: AuthContext) => ctx.document);
// Readable, testable, self-documenting policies
export const canRead = userIsActive.and(isOwner.or(userIsAdmin));
export const canEdit = userIsActive.and(isOwner.or(userIsAdmin)).and(docIsArchived.not());
export const canDelete = userIsActive.and(isOwner.or(userIsAdmin));
export const canPublish = userIsActive.and(isOwner.or(userIsAdmin)).and(docIsArchived.not());
rulewrite fits anywhere complex decisions are made and explained.
Define read, edit, delete, and publish policies as named rules. Every denied request tells you exactly which condition was not met.
Combine customer rules, product rules, and cross-type conditions into a single policy. Use IMPLIES for conditional requirements like prescription checks.
Collection combinators and IMPLIES combine naturally. "If any attendee is a minor, an adult must also be present" — one readable expression.
Express complex eligibility criteria as named, auditable rules. The evaluation tree becomes the permanent record of why an application was approved or declined.
Pass any policy to a middleware factory. Failed requests return structured error bodies listing every violated condition by name — useful for API consumers.
No other approach combines composability, type safety, and self-describing evaluation in a zero-dependency TypeScript package.
| rulewrite | Raw booleans | Zod / Yup | Hand-rolled Spec | |
|---|---|---|---|---|
| Named, reusable rules | ✓ | ✗ | Schemas only | If you build it |
| Free composition (AND, OR, NOT) | ✓ | && / || / ! | Limited | If you build it |
| IMPLIES operator | ✓ | ✗ | ✗ | If you build it |
| PREVENTS operator (aka NAND) | ✓ | ✗ | ✗ | If you build it |
| Context projection (.on()) | ✓ | ✗ | ✗ | If you build it |
| Collection combinators | ✓ | ✗ | Array schemas | If you build it |
| Evaluation tree (explains why) | ✓ | ✗ | Error messages | If you build it |
| Full TypeScript type safety | ✓ | ✓ | ✓ | Varies |
| Zero dependencies | ✓ | ✓ | ✗ | ✓ |
| Test rules in isolation | ✓ | ✗ | Schema tests | Varies |
| Audit log ready | ✓ | ✗ | ✗ | If you build it |
From first rule to production rule set — everything is documented.
Install, define your first rule, compose, and evaluate. Up and running in five minutes.
📖Ten chapters covering every concept — operators, projection, collections, the evaluation model, and production patterns.
📐Complete method signatures, parameter tables, truth tables, and tree-shape reference for every operator.
🗂️Runnable examples: e-commerce checkout, document access control, and venue attendance rules.
Replace scattered conditionals with named, composable, auditable business rules. Zero dependencies. Pure TypeScript.