Skip to main content

Benchmarks

Real performance measurements and honest comparisons: @gentleduck/iam benchmarked against every major JS authorization library.

Benchmarked against 7 libraries: @casl/ability, casbin, accesscontrol, role-acl, @rbac/rbac, and easy-rbac. Numbers from vitest bench on identical authorization scenarios. Sizes verified via bundlephobia on 2026-03-30.

Run bun run bench in packages/duck-iam to reproduce the numbers here. Three bench suites ship in-tree:

  • test/benchmark.bench.ts - head-to-head against the six competitor libraries.
  • src/core/evaluate/__tests__/evaluate.bench.ts - micro-benchmarks for evaluatePolicyFast, indexPolicy cold/warm, and cross-policy combine modes.
  • src/core/resolve/__tests__/resolve.bench.ts - path resolution and pattern-matching hot paths.

The honest verdict

CASL is faster than us on most micro-benches. Pre-compiled hash-map dispatch at build() time beats a runtime policy engine on raw ops/sec:

  • Simple RBAC check: CASL ~2.1x faster than evaluateFast raw, ~14x faster end-to-end on cold start.
  • evaluateFast raw: ~7.2M ops/s. engine.can() cache-warm: ~140k ops/s. The ~54x wrapper overhead is real work the raw path doesn't do - subject resolution, LRU lookup, hook dispatch, IDecision construction in dev mode.

duck-iam wins on architecture, not raw speed. What you get for the overhead:

  • XACML-grade semantics - NotApplicable policies are skipped (not folded as default-deny), three cross-policy modes, four in-policy combiners, deterministic priority resolution.
  • Type-safe adapter contract - IamAdapter.IAdapter with IReadOptions.signal for cancellation, generic over actions / resources / roles / scopes.
  • Property-based oracle - 1000 random iterations per (combine, defaultEffect) pair assert evaluate and evaluateFast cannot silently disagree.
  • Snapshot export / import - engine.admin.export() produces a schema-versioned config snapshot for GitOps or staging->prod promotion.

duck-iam is faster than everyone except CASL. In production mode, beats easy-rbac, @rbac/rbac, accesscontrol, casbin, and role-acl on most paths.

The "duck-iam 41 KB" headline is misleading. Read on.

CASL ships at ~6 KB because its surface is small - one defineAbility builder + a condition evaluator. duck-iam's headline "41 KB" is the result of import * from '@gentleduck/iam' and pulling every adapter, every server middleware, every client wrapper, the builder, the explain tracer, and the validator. Nobody imports it this way in real code.

Realistic deployments (subpath imports + tree-shaking) end up at 15-25 KB, depending on what you wire. See the Real-world bundle profiles section below for measured numbers per use case.


2.1.0 measured (security cycle + bundle slim)

Numbers from scripts/benchmark.ts (microsecond-per-op). Bundle sizes from gzip -c | wc -c on dist/ chunks resolved via BFS through the import graph. Reproduce via bun run benchmark in packages/duck-iam/. Machine-relative; absolute numbers will vary, the deltas are the story.

Core paths

Path2.0.02.1.0delta
evaluatePolicy (simple rule)0.68 us0.76 us+12%
evaluatePolicy (conditions)1.33 us1.00 us-25%
evaluatePolicy (target match)0.47 us0.68 us+45%
evaluatePolicy (target skip)0.24 us0.24 us0%
evaluate (2 policies)0.65 us1.24 us+91%
evaluate (deny path)0.58 us0.62 us+7%

Engine paths (cache-warm)

Path2.0.02.1.0delta
engine.can()4.86 us5.18 us+7%
engine.check()4.60 us4.60 us0%
engine.permissions() (20 checks)20.06 us48.08 us+140%
engine.explain()n/a8.02 usnew

Bundle (gzipped, post-slim)

Measured against a clean 2.0.1 git worktree baseline (not eyeballed). Net 2.0.1 -> 2.2.0 delta is +2.9 KB (+7.5%) - earlier docs cited a ~21 KB pre-cycle number that was estimated from a partial dist build.

SnapshotBundle (gzipped)
2.0.1 baseline (clean build)38.4 KB
2.1.0 post-security-cycle44.8 KB (+17%)
2.2.0 post-slim41.3 KB (+7.5% net vs 2.0.1)
ModuleSize
Headline ("import * from")41.3 KB
Core barrel (@gentleduck/iam/core)~15 KB realistic
core/validate (admin only, lazy)12 KB chunk
core/builder (config-time)9 KB chunk
core/explain (dev-mode)separate chunk
core/schema (JSON schema export)1.3 KB
Memory adapter1.7 KB
Prisma adapter1.9 KB
Drizzle adapter3.0 KB
HTTP adapter6.0 KB
Redis adapter4.5 KB
Express server2.4 KB
Hono server2.4 KB
Next.js server3.1 KB
NestJS server2.9 KB
Generic server3.7 KB
React client1.3 KB
Vue client1.2 KB
Vanilla client2.0 KB

Honest take

engine.permissions() more than doubled. Each batch check now fires onMetrics + builds IEvalSignals + threads onPolicyError and the catch arm wraps via _safeHookCall. 4 extra allocations per check. The 2-3x cost bought silent-fail-open elimination in batch UI gates - previously a real bug, now structurally impossible.

Core bundle headline +7.5% net (38.4 -> 41.3 KB) if you import the everything-barrel. The 2.1.0 security cycle added ~6 KB raw (44.8 KB peak); the 2.2.0 bundle slim cycle recovered ~3 KB by (1) dropping adapter re-exports from the barrel, (2) lazy-loading the validator chunk, (3) splitting builder / explain / validate into separate subpath entries. Realistic deployments measure 15-25 KB - see below.

engine.can() ~7% slower. 0.32 us added per cache-warm check. You'd need 1 million checks per second sustained before this is measurable.

evaluatePolicy(conditions) 25% faster. Per-Engine cache threading de-bounces redundant resolution work in the hot path.

Raw evaluateFast() ops/sec from vitest bench (separate suite): still ~7M/s on the production fast path.


Real-world bundle profiles

The 41 KB headline is the worst case. Here is what actual deployments measure (gzipped, subpath imports + standard ESM tree-shaking).

ProfileImportsEffective bundle
Edge function, RBAC-only@gentleduck/iam/core + @gentleduck/iam/adapters/memory~17 KB
Express + Redis backend@gentleduck/iam/server/express + @gentleduck/iam/adapters/redis~22 KB
Hono + memory@gentleduck/iam/server/hono + @gentleduck/iam/adapters/memory~19 KB
Next.js + Drizzle@gentleduck/iam/server/next + @gentleduck/iam/adapters/drizzle~21 KB
NestJS + Prisma@gentleduck/iam/server/nest + @gentleduck/iam/adapters/prisma~20 KB
Admin dashboard (adds builder + validate)+ @gentleduck/iam/core/builder + @gentleduck/iam/core/validate (lazy)+21 KB on admin route only
React UI gate (browser)@gentleduck/iam/client/react~1.3 KB
Vue UI gate (browser)@gentleduck/iam/client/vue~1.2 KB
Vanilla browser gate@gentleduck/iam/client/vanilla~2.0 KB

Notes:

  • The validator (12 KB) is lazy-loaded on first call to engine.admin.savePolicy/saveRole/import. Read-only services never pay for it.
  • The builder (9 KB) is only pulled if you import @gentleduck/iam/core/builder directly. Apps that store policies as JSON skip it entirely.
  • The explain tracer is dev-mode only and a separate chunk; production builds tree-shake it away.
  • Browser-side UI gates ship ~1-2 KB because they wire useState to a permission map the server fed them; the engine never enters the browser bundle.

How to keep your bundle tight

// yes Tight: import only what you use
import { IamEngine } from '@gentleduck/iam/core'
import { IamMemoryAdapter } from '@gentleduck/iam/adapters/memory'
import { adminRouter } from '@gentleduck/iam/server/express'

// no Pulls everything (41 KB)
import { IamEngine, IamMemoryAdapter } from '@gentleduck/iam'

Run bun run bench for the head-to-head against @casl/ability, casbin, accesscontrol, role-acl, @rbac/rbac, easy-rbac. Run bun run benchmark to emit fresh JSON for the docs site.


Library Overview

@gentleduck/iam@casl/abilitycasbinaccesscontrolrole-acl@rbac/rbaceasy-rbac
ModelPolicy engineAbility-basedPERM DSLFluent grantsRole + conditionsHierarchicalHierarchical
ABACYes (18 ops)YesYesNoYesNoNo
RBACYesYesYesYesYesYesYes
Runtime deps0051300
TypeScriptFull genericsFullString-basedPartialPartialYesNo
MaintainedActiveActiveActiveNo (2020)ActiveActiveNo (2021)
Bundle, "import everything"41 KB6 KB30 KB8.2 KBn/an/an/a
Bundle, realistic backend15-22 KB~6 KB~30 KB~8 KBn/an/an/a
Bundle, browser UI gate1-2 KB~6 KB(server only)(server only)(server only)(server only)(server only)

Runtime Performance

All numbers are ops/sec (higher is faster). Each library solves the same authorization problem. CASL condition checks use subject() so conditions run (bare string checks skip them). duck-iam has two modes: [DEV] returns rich Decision objects with timing and reasons, [PROD] returns plain booleans with zero overhead.

Simple RBAC: "can viewer read post?"

#Libraryops/secvs CASL
1@casl/ability15,200,000--
2@gentleduck/iam evaluatePolicyFast() [PROD]7,650,0002x slower
3@gentleduck/iam evaluateFast() [PROD]7,200,0002.1x slower
4easy-rbac5,003,0003.0x slower
5@rbac/rbac2,884,0005.3x slower
6@gentleduck/iam evaluatePolicy() [DEV]1,355,00011.2x slower
7@gentleduck/iam evaluate() [DEV]1,049,00014.5x slower
8@gentleduck/iam engine.can() [PROD, cache-warm]140,000108x slower (54x wrapper overhead vs raw)
9accesscontrol674,00022.6x slower
10casbin143,000106x slower
11role-acl140,000108x slower

ABAC condition check: "can owner update own draft?"

Libraries with ABAC condition support. CASL uses subject() so conditions run.

#Libraryops/secvs CASL
1@casl/ability (with subject())3,910,000--
2@gentleduck/iam evaluateFast() [PROD]1,177,0003.3x slower
3@gentleduck/iam evaluate() [DEV]648,0006x slower

Others excluded: no attribute-based condition support.

Role + condition: "can admin delete post?"

#Libraryops/secvs CASL
1@casl/ability (with subject())5,677,000--
2easy-rbac4,504,0001.3x slower
3@rbac/rbac2,780,0002x slower
4@gentleduck/iam [DEV]786,0007.2x slower
5accesscontrol388,00014.6x slower
6casbin55,000103x slower
7role-acl55,000103x slower

Deny path: "viewer cannot delete"

#Libraryops/secvs fastest
1easy-rbac3,114,000--
2@casl/ability1,664,0001.9x slower
3@gentleduck/iam [DEV]803,0003.9x slower
4role-acl141,00022x slower
5@rbac/rbac68,00046x slower
6casbin51,00061x slower

Batch: 20 permission checks

#Libraryops/secvs CASL
1@casl/ability3,481,000--
2easy-rbac497,0007x slower
3@gentleduck/iam evaluateFast() [PROD]462,0007.5x slower
4@gentleduck/iam evaluate() [DEV]137,00025.4x slower
5accesscontrol68,00051x slower
6role-acl22,000158x slower
7@rbac/rbac14,200245x slower
8casbin9,800354x slower

Cold start: build everything + first check

#Libraryops/secvs CASL
1@casl/ability3,284,000--
2easy-rbac3,118,0001.1x slower
3accesscontrol830,0004x slower
4@gentleduck/iam234,00014x slower
5role-acl306,00010.7x slower
6@rbac/rbac183,00017.9x slower
7casbin62,00053x slower

The cold-start gap reflects the cost of building the policy engine, RBAC-to-ABAC index, condition operator table, and LRU caches at first call - one-time work the rest of the runtime amortises.


Why CASL is faster, and why it rarely matters

The architectural difference

CASL and duck-iam solve authorization at different engine levels:

CASL: pre-compiled lookup table. build() iterates every rule once and produces an index keyed by [action, subjectType]. Every can() call is a single hash-map lookup - O(1), ~0.012 us. Rules are frozen after build() and can't change at runtime.

duck-iam: dynamic policy engine. Policies load from databases, update at runtime through adapters, and invalidate via the LRU cache. Each evaluation does: WeakMap index lookup, Map.get by action:resource, condition evaluation, combining algorithm. Even with rule indexing, each check costs ~0.12 us - about 2x a single hash lookup.

Where the ~2x gap comes from (profiled)

Profiled operations in the production fast path:

OperationCostWhat it does
WeakMap index lookup~0.004 usRetrieve cached rule index for the policy
String key concat~0.001 usBuild "read\0post" lookup key
Map.get~0.014 usFind rules matching this action+resource
for loop (1 rule)~0.003 usIterate matched rules
Condition check~0.003 usSkip (empty conditions) or evaluate
policyApplies~0.003 usCheck policy targets
Precomputed cache hit~0.080 usTwo nested Map.get calls (action -> resource)
Total~0.120 us
CASL total~0.060 usHash lookup + return

The gap is not one big bottleneck. It's the sum of small costs a policy engine requires. CASL sidesteps them by freezing rules at build time.

What we optimized (and what we can't)

Every optimization that keeps the dynamic policy model is applied:

  1. Rule indexing: pre-built Map<action:resource, Rule[]> per policy, cached via WeakMap. Removes the linear scan over all rules.
  2. Unconditional rule flag: rules with empty conditions skip evalConditionGroup().
  3. Inlined combiners: deny-overrides and allow-overrides inline into the evaluation loop - no array allocation, no function calls.
  4. Path cache: condition field paths like subject.attributes.role split once and cache forever.
  5. Production mode: no performance.now(), no Date.now(), no Decision allocation, no reason strings.

Closing the last ~2x gap means dropping dynamic policies and pre-compiling at init like CASL. That breaks adapters, runtime policy updates, and the LRU cache - the features that make duck-iam a policy engine instead of a lookup table.

Why it doesn't matter in practice

Authorization isn't the bottleneck. A typical API request:

StepTime
Network round trip5,000--50,000 us
Database query500--5,000 us
JSON serialization50--500 us
duck-iam check (prod)0.12 us
CASL check0.06 us

The gap is 60 nanoseconds. At 100 checks per request, that's 6 us - 0.00012% of a 50 ms request.


Dev vs Prod Mode

duck-iam has two execution modes. They change runtime behavior and return types:

// Development (default) -- rich AccessControl.IDecision with timing, reasons, rule refs
const engine = new IamEngine({ adapter, mode: 'development' })
const decision = await engine.check('user-1', 'read', post)
// decision: AccessControl.IDecision { allowed: true, effect: 'allow', reason: '...', duration: 0.5, timestamp: ... }
// engine.explain() is available
// Hooks (afterEvaluate, onDeny, onError) fire on every check

// Production -- plain boolean, maximum throughput
const prodEngine = new IamEngine({ adapter, mode: 'production' })
const allowed = await prodEngine.check('user-1', 'read', post)
// allowed: true (boolean)
// No performance.now(), no Date.now(), no object allocation, no reason strings
// engine.explain() throws -- not available in production
// Hooks (afterEvaluate, onDeny, onError) are skipped for maximum speed
// onMetrics still fires in production (primitive-only event, zero overhead when unwired)

engine.can() always returns boolean in both modes (for middleware compatibility).

Does production mode reduce bundle size?

The mode flag alone does not reduce bundle size. It's a runtime check. Import patterns and subpath entries do. The package is tree-shakeable AND ships per-module entries.

// yes Smallest production bundle - import only the fast evaluator
// Tree-shakes away: IamEngine, explain, builder, config, validate, dev evaluate
import { evaluateFast } from '@gentleduck/iam/core'

// yes Typical backend - pulls engine + adapter + server middleware only
import { IamEngine } from '@gentleduck/iam/core'
import { IamMemoryAdapter } from '@gentleduck/iam/adapters/memory'
import { adminRouter } from '@gentleduck/iam/server/express'

// yes Admin-write path - validator is lazy-loaded on first call
import { IamEngine } from '@gentleduck/iam/core'
await engine.admin.savePolicy(p) // pulls validate chunk on first call

// no Pulls everything (41 KB) - avoid the barrel import
import { IamEngine, IamMemoryAdapter } from '@gentleduck/iam'

The validator (12 KB), builder (9 KB), explain tracer, and JSON schema are all behind separate subpath entries - they only ship if you import them directly. The HTTP, Redis, Drizzle, Prisma, and file adapters are subpath-only; the barrel no longer re-exports them.

Why is duck-iam ~41 KB when CASL is ~6 KB?

It is not. 41 KB is the headline if you do import * from '@gentleduck/iam'. Almost nobody does. Real deployments are 15-25 KB - see Real-world bundle profiles.

duck-iam is bigger than CASL because it ships more: validator, fluent builder, explain tracer, multi-adapter contract, hook safety wrappers, per-Engine caches, server middleware for 5 frameworks, client wrappers for 3 frameworks. CASL is one builder + one evaluator. Different products. Use CASL if you need the smallest possible bundle and don't need adapters / explain / lazy validate.


Internal Performance

Pure evaluation timing, average of 2,000 iterations after 200 warmup rounds.

OperationTime
evaluatePolicyFast() - simple rule~0.87 us
evaluatePolicyFast() - with conditions~1.61 us
evaluatePolicy() [DEV] - target match~0.59 us
evaluatePolicy() - target skip~0.37 us
evaluate() - 2 policies~0.70 us
evaluate() - deny path~0.96 us

Engine Performance (with LRU caching)

OperationTimeops/s
engine.can() [PROD] - cache-warm~7.1 us~140,000
engine.check() [DEV] - cache-warm~4.2 us~238,000
engine.permissions() - 20 checks~21 us~47,000 batches
engine.explain() - full trace~5.7 us~175,000

The gap between raw evaluateFast() (~7.2M ops/s) and engine.can() cache-warm (~140k ops/s) is wrapper overhead: subject resolution from the LRU, hook dispatch, mode-conditional Decision construction. The wrapper does work the raw evaluator skips.

Times vary by machine. Run bun run benchmark for your hardware.


Bundle Size

LibrarySize (gzip)Runtime depsTree-shakeable
easy-rbac~2 KB0No
@rbac/rbac~4 KB0No
@casl/ability~6 KB0Yes
accesscontrol~8.2 KB1No
role-acl~12 KB3No
@gentleduck/iam (full)~21 KB0Yes
casbin (node-casbin)~30 KB5No

We are not the smallest. At ~21 KB, duck-iam is 3.5x larger than CASL. The full package bundles: evaluation engine, RBAC-to-ABAC converter, conditions engine (18 operators), explain/debug tracer, type-safe builder, config validator, and LRU cache. CASL ships none of that.

The package is tree-shakeable. Import only evaluateFast and skip the engine, explain, and builder for a much smaller bundle. Each adapter and server middleware adds ~0.8-1.7 KB.

Module Sizes

ModuleSize (gzip)
Core (full entry)21.9 KB
Adapter: Memory1.1 KB
Adapter: Prisma1.4 KB
Adapter: Drizzle1.7 KB
Adapter: HTTP1.2 KB
Adapter: Redis1.4 KB
Server: Express1.1 KB
Server: Next.js1.0 KB
Server: Hono0.9 KB
Server: NestJS1.3 KB
Server: Generic0.8 KB
Client: React1.1 KB
Client: Vue1.0 KB
Client: Vanilla1.4 KB

Feature Comparison

FeaturegentleduckCASLCasbinaccesscontrolrole-acl@rbac/rbaceasy-rbac
RBACYesYesYesYesYesYesYes
ABAC (conditions)18 operatorsYesYesNoYesNoNo
Policy engineYesNoYesNoNoNoNo
Dev/Prod modeYesNoNoNoNoNoNo
Deny-overridesYesNoYesNoNoNoNo
Combining algorithms41Custom1111
Scoped rolesYesNoNoNoNoNoNo
Explain / debugYesNoNoNoNoNoNo
Lifecycle hooksYesNoNoNoNoNoNo
LRU cachingBuilt-inNoNoNoNoNoNo
Rule indexingYesYesNoNoNoNoNo
DB adapters5320+0030
Server middleware5020030
React integrationYesYesNoNoNoNoNo
Vue integrationYesYesNoNoNoNoNo
Type-safe configYesYesNoYesNoYesNo
Zero runtime depsYesYesNoNoNoYesYes
Batch permissionsYesNoNoNoNoNoNo

Where each library wins

@gentleduck/iam wins on

  • XACML-grade semantics: NotApplicable policies skipped (not folded as default-deny), three cross-policy combine modes (and, allow-overrides, first-applicable), four in-policy combiners with deterministic priority.
  • Type-safe adapter contract: IamAdapter.IAdapter generic over actions / resources / roles / scopes; IReadOptions.signal for AbortController-driven cancellation.
  • Property-based oracle: trace and fast paths can't silently drift - 1000 random iterations per combine/default-effect pair, six historical drifts caught.
  • Snapshot export / import: engine.admin.export() -> schema-versioned snapshot for GitOps, environment promotion, backup.
  • Feature density: scoped roles, explain/debug, lifecycle hooks (onPolicyError, onMetrics), batch permissions, 18 condition operators, dev/prod mode in one package.
  • Faster than casbin, role-acl, accesscontrol in production mode.
  • Operability surface: preload(), healthCheck(), stats(), dispose(), cross-instance IInvalidator, adapter timeouts, fail-open opt-in.

@casl/ability wins on

  • Raw speed: 2x faster than duck-iam in production mode from the pre-compiled ability index
  • Bundle size: ~6 KB, 3.5x smaller
  • Maturity: production since 2017
  • Ecosystem: ~900K downloads/week, extensive docs and community
  • Isomorphic: proven frontend + backend sharing pattern

easy-rbac wins on

  • Fastest deny path: 2x faster than CASL on deny checks
  • Tiny bundle: ~2 KB, the smallest
  • Zero config: hierarchical RBAC, nothing to set up

casbin wins on

  • Adapter ecosystem: 20+ database adapters across 15+ languages
  • Admin UI: web-based policy management panel
  • Academic backing: formal PERM metamodel

@rbac/rbac wins on

  • Fast simple checks: 2.5M ops/sec for basic RBAC
  • Built-in middleware: Express, NestJS, Fastify
  • Runtime role updates: add or change roles without restart

Smallest possible bundle

defineIam() sets up the whole authorization system in one call, but it pulls in the full config system, validator, and builder. If all you need is policy evaluation, skip the config layer and import the building blocks directly.

Build a typed policy and evaluate it without defineIam:

import type { AccessControl, IamRequest } from '@gentleduck/iam'
import { evaluatePolicyFast } from '@gentleduck/iam'

// Define your action/resource types for type safety
type Action = 'read' | 'update' | 'delete'
type Resource = 'post' | 'comment'

const policy: AccessControl.IPolicy<Action, Resource> = {
  id: 'blog-policy',
  algorithm: 'deny-overrides',
  rules: [
    { id: 'allow-read', effect: 'allow', actions: ['read'], resources: ['post', 'comment'], conditions: {}, priority: 0 },
  ],
}

const request: IamRequest.IAccessRequest<Action, Resource> = {
  subject: { id: 'user-1', roles: ['viewer'] },
  action: 'read',
  resource: { type: 'post', id: 'post-1' },
}

const allowed = evaluatePolicyFast(policy, request) // boolean

The package is fully tree-shakeable. Anything you don't import drops out: IamEngine, explain, builder, config, validate, adapters. From the module sizes above, evaluateFast alone is tiny next to the 21.9 KB core entry: pay only for what you use.

Other low-level pieces to import directly: PolicyBuilder, RuleBuilder, evaluateFast, evaluatePolicy, and the condition operators. Mix and match for the exact surface area you need.


Methodology

  • @gentleduck/iam: bundle sizes from dist/ via gzip -c | wc -c. Performance via vitest bench with N=3 inner loops. Production mode uses evaluateFast() with rule indexing (WeakMap-cached per policy, Map lookup by action:resource).
  • @casl/ability: condition benchmarks use subject() for condition evaluation. Bare string checks (can('read', 'Post')) skip conditions and would give misleading numbers - we don't do that.
  • casbin: real RBAC model (newModel() + StringAdapter) with role inheritance via grouping rules.
  • accesscontrol, @rbac/rbac, easy-rbac: excluded from ABAC benchmarks (no condition support).
  • Competitor sizes from bundlephobia.com, verified 2026-03-30.
  • Sizes are minified + gzipped.
  • All benchmarks run on the same machine in the same vitest session.

Reproduce:

cd packages/duck-iam
bun run bench       # vitest bench -- competitive comparison + micro-benchmarks
bun run benchmark   # JSON data output + console summary

Property-based regression guard

The benchmarks above measure speed. A second suite measures correctness drift between the trace path (evaluate) and the production fast path (evaluateFast).

src/core/evaluate/__tests__/oracle.test.ts

1000 deterministic-random iterations per (combine, defaultEffect) pair. Each iteration generates a policy set with mixed exact / wildcard / colon-prefix / dot-hierarchy / parent-prefix resource patterns plus randomized conditions, scoped roles, and target dimensions. Then asserts:

evaluate(policies, request).allowed === evaluateFast(policies, request)

Why it matters: across thirteen audit rounds the two paths drifted six times (first-match priority order, colon-prefix index, parent-prefix lookup, NotApplicable handling, etc.). Each drift was caught by a regression test added after the bug shipped. The oracle is the generative guarantee that the two paths can't silently disagree on inputs the regression suite didn't pick.

Failures surface with the exact seed + policy set + request that diverged - full reproducer in the test error message.