Rule the form. Play the field.
Umpire is a declarative field-availability engine. Define fields, declare rules between them, and Umpire tells you which fields are in play β and which stale values just fell out. It answers a structural question, not a validation question: given the current values and conditions, what should be enabled right now?
Forms are the most common use case, but Umpire works anywhere state fits a plain object with interdependent options β game boards, config panels, pricing calculators, permission matrices. If it has fields and rules, Umpire can call the game.
import { enabledWhen, requires, umpire } from '@umpire/core'
const signupUmp = umpire({
fields: {
email: { required: true, isEmpty: (v) => !v },
password: { required: true, isEmpty: (v) => !v },
confirmPassword: { required: true, isEmpty: (v) => !v },
referralCode: {},
companyName: {},
companySize: {},
},
rules: [
requires('confirmPassword', 'password'),
enabledWhen('companyName', (_values, cond) => cond.plan === 'business', {
reason: 'business plan required',
}),
enabledWhen('companySize', (_values, cond) => cond.plan === 'business', {
reason: 'business plan required',
}),
requires('companySize', 'companyName'),
],
})
const availability = signupUmp.check(
{ email: 'alex@example.com', password: 'hunter2' },
{ plan: 'personal' },
)
availability.companyName
// { enabled: false, required: false, reason: 'business plan required', reasons: ['business plan required'] }
const fouls = signupUmp.play(
{
values: {
email: 'alex@example.com',
password: 'hunter2',
companyName: 'Acme',
companySize: '50',
},
conditions: { plan: 'business' },
},
{
values: {
email: 'alex@example.com',
password: 'hunter2',
companyName: 'Acme',
companySize: '50',
},
conditions: { plan: 'personal' },
},
)
// [
// { field: 'companyName', reason: 'business plan required', suggestedValue: undefined },
// { field: 'companySize', reason: 'business plan required', suggestedValue: undefined },
// ]| Package | Purpose |
|---|---|
@umpire/core |
Pure logic engine with zero runtime dependencies |
@umpire/react |
useUmpire() hook for React |
@umpire/signals |
Signal adapter via SignalProtocol (Jotai, Preact, Alien Signals, TC39) |
@umpire/store |
Generic store adapter β bring your own getState() + subscribe(next, prev) |
@umpire/zustand |
Zustand adapter (satisfies the store contract natively) |
@umpire/redux |
Redux / Redux Toolkit adapter |
@umpire/tanstack-store |
TanStack Store adapter |
@umpire/pinia |
Pinia adapter (Vue 3) |
@umpire/vuex |
Vuex 4 adapter (Vue 3) |
@umpire/zod |
Availability-aware Zod schemas β disabled fields produce no errors |
@umpire/reads |
Derived read tables and read-backed rule bridges |
@umpire/testing |
Invariant probes for rule configurations |
@umpire/devtools |
In-app inspector panel β scorecard, traces, foul log, graph view |
- Pure logic, zero dependencies.
- Declarative rules:
requires,disables,enabledWhen,fairWhen,oneOf. - Recommendations, not mutations:
play()suggests resets, you decide when to apply them. - Adapters for React, Zustand, Redux, TanStack Store, Pinia, Vuex, and signals.
- Debuggable:
challenge()traces why any field was ruled out,@umpire/devtoolssurfaces it visually.
npm install @umpire/corePublished packages do not require Bun to consume.
Local repo work expects:
- Node 24+
- Yarn 4
- Bun 1.2+
yarn test and yarn test:coverage use Bun under the hood, so those commands will fail if Bun is not installed.
Full docs, concepts, and examples live at https://sdougbrown.github.io/umpire/
Each published package ships a tight AGENTS.md file for cross-agent discoverability, with .claude/rules/* included as a Claude-specific compatibility surface. In this repo, AGENTS.md is canonical and CLAUDE.md plus .cursor/rules/ are compatibility symlinks.
Alpha.
MIT