Open Source · MIT · Zero Dependencies · Forever
You've written the if/elif ladder. You've pulled the thresholds
into a config table. You've reached for a real rules engine — and inherited its class
hierarchy, its registry, its own ladder hiding one level down.
Then a rule needed to explain itself, and none of them could.
No. 1 — PY · Ct. of Rule Evaluation
ver·dict
A decision reached by weighing the facts before it — what a jury renders after
testimony. Here: what a Rule renders after weighing a context. Pass or
fail, with its reasoning attached.
"The verdict wasn't guilty or innocent — it was 'passed, because three of four conditions held, and here's which one didn't.'"
Act I
It ships fast. A total check, a membership check, a promo-code fallback — three conditions, one function, done. Then a fourth condition arrives that depends on the outcome of the first two. Evaluation order becomes load-bearing, and nobody wrote that down.
You survive it. You add a comment. You swear the next one will be a real design.
Act II
You pull the thresholds into a database row — no more redeploying for a marketing change. Genuinely better. But nothing says whether the rows combine with all-must-pass or any-one-will-do, and an empty table's meaning ("nothing configured") is a coin flip nobody decided on purpose.
It's data now. The ambiguity just moved with it.
Act III
You reach for a proper rules engine — a real one, with a base class to extend, a registry to populate, a DSL to learn. Then you need one rule that only applies to a single screen, and the abstraction gets in the way of the one thing it was supposed to make easy: adding a rule.
One requirement. A framework's worth of ceremony. Permanent overhead.
Enter Verdict.
A Rule is anything with a name, an optional group,
and an async evaluate(context) method — a structural
Protocol, not a base class. No registration, no inheritance, no
subclassing to satisfy. If it walks like a rule, it is one.
AndRule/OrRule short-circuit exactly the way
and/or already do in your head — and it's a
real contract, not an optimization: the rule after the deciding one
genuinely never runs. Every verdict carries its own reasoning in
RuleResult.data, opaque to Verdict itself, fully readable by you.
Ships today
Same Rule/Engine/Result design, every language it ever ships for.
# a 4th condition means editing this async def can_proceed(ctx): if ctx["requests"] >= ctx["limit"]: return False if ctx["status"] != "active": return False return True # which check failed? no idea — just False.
# a 4th condition is a 4th FunctionRule can_proceed = AndRule("can_proceed", [ FunctionRule("under_limit", under_limit), FunctionRule("in_good_standing", in_good_standing), ]) result = await engine.run_named("can_proceed", ctx) # result.data → which sub-rule passed, # which didn't. Every time. For free.
01 / structure
Rule is a Protocol. Any object with the right shape qualifies — no base class, no registry, no import cycle back into this package.
02 / execution
Evaluation is always sequential, never concurrent — the rule after a deciding failure or pass genuinely never runs. Proven by a call-counter test, not assumed.
03 / correctness
An empty AndRule passes; an empty OrRule fails — the same asymmetry all([])/any([]) already have in plain Python, made explicit per composite.
04 / footprint
Nothing to audit, nothing to pin, nothing that breaks because a transitive package changed underneath it. One small, standalone package.
05 / testing
The shipped example runs 500 deterministically-seeded random cases against an independent, deliberately-dumb oracle — a regression net wider than anyone would hand-curate.
06 / tooling
A vendorable skill teaches Claude, Cursor, and six other harnesses when to reach for Verdict and how to extend it — scripts/install.sh wires it into any repo.
Start in one minute
Zero dependencies, in every language it ships for.
Also available as script/cdn —
see the README.
Works unchanged in a Flutter project too — flutter pub add verdict_rules there.
Having doubts?