Rule Engine (κ)
κ is the deterministic evaluator for every rule the system enforces — admission limits, rate caps, decay schedules, reputation penalties, arbiter bounds. κ rules are source-controlled, version-hashed, and evaluated with pure integer math. Two arbiters with the same κ version and the same input must produce bit-identical output; this is what makes κ acceptable as input to θ consensus.
Phase 1 reality (R87, 2026-05-07): κ ships end-to-end. The 20 Phase 1 sub-tasks — base library (P1.1.x, BPS arithmetic + determinism harness), DSL pipeline (P1.2.x, lexer/parser/validator/registry), runtime (P1.3.x, engine/builtins/state-access/policy-gate), admission (P1.4.x, evaluator/denial-reasons/budgets/tool-lock-adapter), and versioning (P1.5.x, hash/migration/activation/canonical/parity) — landed in R85→R87 (PRs #205–#220). κ is the deterministic core that admission middleware calls, that θ vote payloads version-hash against, and that all rule-shaped state mutations route through. Phase 2+ rule extensions (governance-proposed rule upgrades, fork-aware versioning) remain spec-only.
Authoritative specs: ../../../spec/s11-rule-engine.md and ../../../spec/s12-dsl.md. Admission-specific rules live in ../../../spec/s10-admission.md.
Integer-only arithmetic
κ does no floating-point math. Everything is 64-bit integers in basis points — units of 1 / 10000. So:
10000= 100%2500= 25%1= 0.01%
A reputation decay of “1.5% per epoch” is written as 150 bps, evaluated as value - (value * 150 / 10000), and yields the same result on every arbiter.
Floating-point is forbidden in rule bodies. It produces platform-dependent results and would break consensus.
Basis-point arithmetic — examples
| Expression | Value | Reading |
|---|---|---|
bps_mul(1000, 500) |
50 |
1000 × 5% = 50 |
bps_mul(10000, 10000) |
10000 |
100% × 100% = 100% (identity) |
bps_div(5000, 2500) |
20000 |
5000 ÷ 25% = 20000 |
bps_pct(3750) |
"37.50%" |
display helper — rendering only, not consensus-visible |
decay(1000, 150, 1) |
985 |
1000 after one epoch of 1.5% decay |
decay(1000, 150, 2) |
970 |
compounded: 1000 → 985 → 970 (floor at each step) |
All operators round via floor, not banker’s rounding. Floor is chosen because it is monotone under composition — decay(decay(x, r, 1), r, 1) == decay(x, r, 2) up to at most one unit of drift, and the drift is deterministic-given-inputs.
Forbidden operations
Rule bodies cannot invoke any of the following, because any of them would make evaluation non-deterministic:
- Clock reads. Time enters κ only via explicit inputs passed by the caller.
- Randomness. κ has no RNG. Where randomness is needed (e.g. leader election), the engine consumes a precomputed VRF output as an input. See ADR-002.
- Floating-point arithmetic. See above.
- Side effects. κ functions are pure: inputs in, value out. No writes, no network, no logging. Logging happens at the enclosing α layer.
- External I/O. No file reads, no HTTP.
Built-in functions
κ exposes eight built-ins, all integer-only:
| Function | Purpose |
|---|---|
min(a, b) |
Minimum of two integers |
max(a, b) |
Maximum of two integers |
sqrt(x) |
Integer square root (floor) |
log2(x) |
Integer floor of log base 2 |
abs(x) |
Absolute value |
cap(x, ceiling) |
min(x, ceiling), spelled explicitly |
decay(value, rate_bps, epochs) |
Deterministic exponential decay in basis points |
diminishing(x, curve) |
Diminishing-returns transform over a precomputed curve |
Anything else is a DSL-level syntax error. The list is intentionally small and grows only via ADR.
DSL grammar (EBNF fragment)
The κ DSL is an expression language over integers plus a narrow effect API. Rules are data; no host-language escape hatch.
rule ::= "rule" NAME "{" guard effects "}" ;
guard ::= "guard:" expression ( "and" expression )* ;
effects ::= "effects:" effect+ ;
effect ::= target "." method "(" arglist? ")" ;
target ::= "stake" | "reputation" | "token" | "state" | "obligation" | "finality" ;
expression ::= comparison | builtin_call ;
comparison ::= term op term ;
op ::= "==" | "!=" | "<" | "<=" | ">" | ">=" ;
term ::= INT | identifier | builtin_call ;
builtin_call ::= ("min"|"max"|"sqrt"|"log2"|"abs"|"cap"|"decay"|"diminishing") "(" arglist ")" ;
arglist ::= term ( "," term )* ;
NAME ::= [A-Z][A-Za-z0-9_]* ;
identifier ::= [a-z][a-z0-9_]* ;
INT ::= "-"? [0-9]+ ; (* no float, no hex, no underscore separators *)
Explicit omissions: no string literals, no user-defined functions, no for/while, no new, no import. A ruleset is a sequence of rule declarations evaluated top-down (see rule application algorithm below).
Worked rule — AcceptCommitment
rule AcceptCommitment {
guard:
event.type == "COMMITMENT_REQUEST"
and event.status == "PENDING"
and stake.available(event.actor) >= event.amount
and reputation.score(event.actor, "commissioning") >= 100
effects:
state.transition(event.id, from="PENDING", to="ACCEPTED")
stake.freeze(event.actor, event.amount)
obligation.assign(event.actor, event.id, deadline=event.deadline)
}
Reading the rule: the guard states four preconditions joined by and; if every one evaluates true, the effects fire atomically (all three land in a single logical event application, or none do — transactional at the β layer, see ../../execution/task-pipeline.md).
Rule application algorithm
fn apply(event, ruleset):
matches = []
for rule in ruleset.sorted_by_specificity():
if evaluate_guard(rule.guard, event):
matches.append(rule)
break # first-match-wins; no fallthrough
if matches.is_empty():
return ADMISSION_DENIED("no rule matched")
rule = matches[0]
try:
apply_effects_atomically(rule.effects, event)
return ADMISSION_ACCEPTED(rule.name)
except BudgetExceeded:
return ADMISSION_DENIED("rule budget exceeded")
except EffectInvariantViolated as e:
return ADMISSION_DENIED(e.reason)
Specificity ordering: rules are sorted by (a) guard term count descending, then (b) declaration order. This gives stable, deterministic ordering regardless of storage format. Ties within a ruleset are a load-time error — κ refuses to boot with ambiguous matches.
Rule versioning
Every κ ruleset has a rule_version_hash — SHA-256 over the canonical serialization of the rule bodies plus the engine version. This hash participates in two load-bearing places:
- θ consensus votes — arbiters sign
(round_id, merkle_root, rule_version_hash); a mismatch prevents a vote from being counted. - ι fork ids —
fork_id = SHA-256(parent_fork_id || divergence_event || rule_version_hash || reason).
This means a silent rule change is impossible: either all arbiters upgrade together, or the upgrade creates a fork under RULE_UPGRADE.
Test corpus parity requirement
Before a new rule_version_hash may be activated, π governance requires a parity run against the standing test corpus:
- Evaluate every corpus event under
v_oldandv_new. - Compute effect-set hashes per event:
h = SHA-256(canonical(effects)). - Activation proceeds only if
h_old == h_newfor every corpus event where both versions admit the event, and the divergence set (events admitted under one version but not the other) matches the proposal’s declared scope.
A divergence outside the declared scope automatically fails the π proposal at stage 1. See ../enforcement/governance.md §versioning.
Constitutional axioms
κ does not evaluate in a vacuum. Every rule is subject to seven constitutional axioms, listed in ../constitution.md and encoded as AX-01 through AX-07. Axioms are rules about rules: they bound what κ may be asked to evaluate. An attempt to load a ruleset that violates an axiom is rejected at boot before any rules are registered.
Admission layer
Admission — whether a given tool call is allowed at all — is a κ evaluation that runs at α’s tool-lock stage (stage 1 of the 5-stage chain tool-lock → schema-validate → audit-enter → dispatch → audit-exit). The inputs are: caller identity, tool name, current mode, reputation snapshot, and rule version. The output is admit-or-deny with a typed reason.
See ../../../spec/s10-admission.md for admission-specific rules.
Evaluation budget
A κ evaluation is bounded: it has a maximum instruction count (enforced by the engine) and a maximum call depth. Exceeding either is a deterministic RuleBudgetExceeded failure — the same on every arbiter, consistent with consensus. The bounds themselves are part of the rule version hash.
Default budget constants
| Bound | Phase 0 default | Scope |
|---|---|---|
MAX_INTEGER_OPS |
10_000 |
Total integer ops across guard + effects (combined) |
MAX_CALL_DEPTH |
16 |
Nested builtin_call frames |
MAX_ARG_COUNT |
8 |
Arity of any single builtin_call |
A rule exceeding any of these deterministically returns ADMISSION_DENIED(reason="budget:<which>"). Budget overruns never silently truncate — κ fails loud so admission denials remain interpretable.
What κ is not
- Not a full programming language. The DSL is intentionally restricted; control flow is
ifand pattern-match only; no unbounded recursion. - Not an LLM prompt. Rules are deterministic integer math. LLM-mediated decisions (if any) happen outside κ and feed their outputs in as explicit inputs.
- Not a scheduler. κ says whether something is allowed; β decides when it runs.
Phase 1 posture
- Integer-math and basis-point conventions apply across every rule body.
- The DSL pipeline ships end-to-end: lexer (Chevrotain), parser (custom CST), validator (aggregate-errors), registry (specificity-ordered).
- The κ engine evaluates against frozen state snapshots with parent-pointer binding chains; AST budget caps (MAX_INTEGER_OPS, MAX_CALL_DEPTH) fail loud as
ADMISSION_DENIED(reason="budget:<which>"). - Phase 0 α
tool-lockmiddleware now consumes κ admission decisions via the P1.4.4 adapter; ad-hoc Phase 0 rule-shaped code paths still exist and route through κ’s basis-point conventions. - Phase 2+ rule upgrades (governance-proposed mutations, fork-aware
rule_version_hashdivergence) are spec-only and will graduate at the next κ-shipping round.
See also
../constitution.md— the seven axioms (AX-01–AX-07)consensus.md— θ, which signs the rule version hashstate-fork.md— ι, whose fork ids include the rule version hash../enforcement/governance.md— π, the process for approving rule upgrades../../../spec/s10-admission.md— admission rules../../../spec/s11-rule-engine.md— authoritative rule-engine spec../../../spec/s12-dsl.md— authoritative DSL spec../../../architecture/decisions/ADR-002-vrf-implementation.md— VRF for precomputed randomness../../../architecture/decisions/ADR-006-dsl-grammar.md— DSL parser technology choice (Chevrotain)