κ Rule Engine — Algorithm Extraction

Language-agnostic pseudocode extracted from Phoenix Python source. Source files: dsl.py (198L), engine.py (353L), state_machine.py (726L), policy.py (5L).


1. Full EBNF Grammar

RuleSet        = { Rule } ;
Rule           = "rule" IDENTIFIER "{" GuardBlock EffectBlock "}" ;
GuardBlock     = "guards" "{" { GuardClause } "}" ;
GuardClause    = ( Expression | "else" ) "->" Action ;
Action         = "admit" | "reject" STRING ;
EffectBlock    = "effects" "{" { EffectCall } "}" ;
EffectCall     = IDENTIFIER "(" ArgList ")" ;
ArgList        = Arg { "," Arg } ;
Arg            = Expression | STRING ;

Expression     = OrExpr ;
OrExpr         = AndExpr { "or" AndExpr } ;
AndExpr        = NotExpr { "and" NotExpr } ;
NotExpr        = [ "not" ] Comparison ;
Comparison     = Additive { CompOp Additive } ;
CompOp         = "==" | "!=" | "<" | ">" | "<=" | ">=" ;
Additive       = Multiplicative { ("+" | "-") Multiplicative } ;
Multiplicative = Unary { ("*" | "/" | "%") Unary } ;
Unary          = [ "-" ] Primary ;
Primary        = INTEGER | "true" | "false" | Variable | FuncCall | "(" Expression ")" ;
Variable       = "$" DotPath ;
DotPath        = IDENTIFIER { "." IDENTIFIER } ;
FuncCall       = IDENTIFIER "(" [ ArgList ] ")" ;

INTEGER        = DIGIT { DIGIT } ;
STRING         = '"' { CHAR } '"' ;
IDENTIFIER     = LETTER { LETTER | DIGIT | "_" } ;

Critical constraints:

  • No floating-point literals — 3.14 is a syntax error
  • All / performs integer division (truncate toward zero)
  • No import, require, or external references inside rule bodies
  • Variables are read-only within a rule — mutations collected separately

2. AST Node Types

Node Type Fields Semantics
RuleNode name: string, guards: GuardClause[], effects: EffectCall[] Top-level rule definition
GuardClause condition: Expression or null (else), action: “admit”|”reject”, reason: string? Guard evaluation; first match wins
EffectCall function: string, args: Expression[] Side-effect invocation
BinaryOp op: string, left: Expression, right: Expression Arithmetic or comparison
UnaryOp op: “-“, operand: Expression Numeric negation
LogicalOp op: “and”|”or”|”not”, operands: Expression[] Boolean logic
IntLiteral value: int64 Integer constant
BoolLiteral value: bool true or false
StringLiteral value: string Quoted string constant
VarRef path: string[] Variable dereference (e.g. $actor.rep)
FuncCall name: string, args: Expression[] Built-in function call

3. 8 Built-in Functions

BPS Constants

BPS_100_PERCENT = 10000   // 100%  = 10000 basis points
BPS_1_PERCENT   = 100     // 1%    = 100 basis points
BPS_50_PERCENT  = 5000    // 50%   = 5000 basis points

Function Definitions

Function Signature Implementation Notes
decay(v, r) (int64, int64) → int64 (v * (10000 - r)) / 10000 Apply rate-bps decay. r=500 → 5% loss
diminishing(v, k) (int64, int64) → int64 (v * k) / (k + v) k defaults to 1000. Returns diminishing returns value
bps_mul(v, b) (int64, int64) → int64 (v * b) / 10000 Multiply by basis-point fraction
bps_div(v, b) (int64, int64) → int64 (v * 10000) / b Divide by basis-point fraction
isqrt(n) int64 → int64 Newton’s method (see below) Integer square root, no float
ilog2(n) int64 → int64 bit_length(n) - 1 Integer base-2 logarithm
rep(node) node → int64 lookup reputation score Returns node’s total reputation
min(a, b) (int64, int64) → int64 a < b ? a : b Minimum of two values
max(a, b) (int64, int64) → int64 a > b ? a : b Maximum of two values
abs(a) int64 → int64 a < 0 ? -a : a Absolute value
clamp(v, lo, hi) (int64, int64, int64) → int64 max(lo, min(v, hi)) Clamp value to range
cap(v, m) (int64, int64) → int64 min(v, m) Cap value at maximum

isqrt Algorithm (Newton’s Method, Integer-Only)

function isqrt(n: int64) -> int64:
    if n < 0: error("negative input")
    if n == 0: return 0
    x = n
    y = (x + 1) / 2
    while y < x:
        x = y
        y = (x + n / x) / 2
    return x

ilog2 Algorithm

function ilog2(n: int64) -> int64:
    if n <= 0: return 0    // defined as 0 for edge case
    return bit_length(n) - 1

// bit_length: number of bits needed to represent n (e.g., 8 → 4 bits → ilog2=3)

4. BPS Integer Math

All arithmetic uses 64-bit signed integers. No floats permitted.

Range: -2^63 to 2^63 - 1  (= -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807)

Examples

decay(1000, 500)     = 1000 * (10000 - 500) / 10000 = 1000 * 9500 / 10000 = 950
bps_mul(5000, 2000)  = 5000 * 2000 / 10000 = 1000        // 20% of 5000
bps_div(1000, 2000)  = 1000 * 10000 / 2000 = 5000        // 1000 is 20% of what?
diminishing(500, 1000) = 500 * 1000 / (1000 + 500) = 333 // diminishing returns
isqrt(100)           = 10
isqrt(101)           = 10   // floor
ilog2(8)             = 3
ilog2(1024)          = 10

Overflow Protection

// Before any multiplication that might overflow:
function safe_mul(a: int64, b: int64) -> int64:
    if b != 0 and abs(a) > MAX_INT64 / abs(b):
        error("multiplication overflow")
    return a * b

// Division by zero protection:
function safe_div(a: int64, b: int64) -> int64:
    if b == 0: error("division by zero")
    return a / b   // truncate toward zero (not floor)

5. Rule Execution Flow

function execute_rule(rule: RuleNode, context: ReadOnlyState) -> RuleResult:
    mutations = []
    node_budget = 0
    MAX_NODES = 10000    // AST node evaluation budget per rule

    // Phase 1: Guard evaluation — first matching guard wins
    admitted = false
    for guard in rule.guards:
        node_budget += count_ast_nodes(guard.condition)
        if node_budget > MAX_NODES:
            error("rule budget exceeded")
        if guard.condition == null:           // else clause
            match = true
        else:
            match = evaluate_expr(guard.condition, context)
        if match:
            if guard.action == "reject":
                return RuleResult { status: REJECTED, reason: guard.reason }
            admitted = true
            break    // first match wins; remaining guards ignored

    if not admitted:
        return RuleResult { status: REJECTED, reason: "NO_MATCH" }

    // Phase 2: Effect collection — no state changes yet
    for effect in rule.effects:
        node_budget += count_ast_nodes(effect)
        if node_budget > MAX_NODES:
            error("rule budget exceeded")
        mutation = collect_effect(effect, context)
        mutations.append(mutation)

    return RuleResult { status: ADMITTED, mutations: mutations }

// Mutations are applied atomically after all rules run (collect-then-apply)

Collect-then-Apply Atomicity

function process_event(event: Event, rules: RuleNode[], state: State) -> TransitionResult:
    snapshot = state.read_only_snapshot()    // immutable view for rule eval
    all_mutations = []

    for rule in rules:
        result = execute_rule(rule, snapshot)
        if result.status == ADMITTED:
            all_mutations.extend(result.mutations)

    // Validate: detect conflicting mutations on same field
    if has_conflicts(all_mutations):
        error("conflicting mutations — cannot apply")

    // Apply atomically
    new_state = apply_mutations(state, all_mutations)
    proof = generate_merkle_proof(state.root, new_state.root, all_mutations)
    return TransitionResult { new_state, proof, mutations: all_mutations }

6. Rule Execution Order

Fixed and deterministic across all implementations:

1. Admission rules       — Can this action enter the system?
2. StateTransition       — What state changes result?
3. Consequence rules     — What reputation / token effects?
4. Promotion rules       — Level-ups, unlocks, or role changes?

Within each category: alphabetical by rule name (ensures determinism)

7. TransitionType Enum (13 Values)

From state_machine.py:

enum TransitionType:
    COMMITMENT_CREATE     // New commitment/obligation created
    COMMITMENT_ACCEPT     // Counterparty accepts commitment
    SETTLEMENT_COMPLETE   // Contract fulfilled
    SETTLEMENT_FAIL       // Contract failed or abandoned
    DISPUTE_OPEN          // Dispute raised against a commitment
    DISPUTE_RESOLVE       // Dispute concluded
    GOVERNANCE_PROPOSE    // New governance proposal submitted
    GOVERNANCE_VOTE       // Vote cast on proposal
    IDENTITY_CREATE       // New identity registered
    IDENTITY_UPDATE       // Identity attributes updated
    FORK_CREATE           // New fork created
    FORK_MERGE            // Fork merged back to parent
    REPUTATION_DECAY      // Epoch-triggered reputation decay

Each transition type maps to a rule set in the registry and a state machine step.


8. Rule Engine process_action() Algorithm

From engine.py:

// Named rule registry (the PhoenixDSL / RuleEngine class)
RULE_REGISTRY = {
    "AcceptCommitment": RuleNode { ... },
    "Yield":            RuleNode { ... },
    "Schism":           RuleNode { ... },
    "InvitePeer":       RuleNode { ... },
    // ... additional named rules
}

function process_action(action_name: string, actor: Node, context: ReadOnlyState) -> ActionResult:
    rule = RULE_REGISTRY.get(action_name)
    if rule == null:
        error("unknown action: " + action_name)

    // Inject actor into context
    eval_context = context.with_binding("actor", actor)

    result = execute_rule(rule, eval_context)
    if result.status == REJECTED:
        return ActionResult { allowed: false, reason: result.reason }

    return ActionResult {
        allowed:   true,
        mutations: result.mutations,
        rep_delta: extract_rep_deltas(result.mutations),
    }

9. Policy Gating: check_policy()

From engine.py + policy.py. Policies are pre-guards that run before named rules.

// P1-P13 policy definitions (from policy.py)
enum PolicyId: P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13

struct Policy:
    id:         PolicyId
    predicate:  Expression    // DSL expression (pure, no side effects)
    rejection:  string        // reason string on failure

function check_policy(policy_id: PolicyId, actor: Node, context: ReadOnlyState) -> PolicyResult:
    policy = POLICIES[policy_id]
    ctx = context.with_binding("actor", actor)
    if not evaluate_expr(policy.predicate, ctx):
        return PolicyResult { admitted: false, reason: policy.rejection }
    return PolicyResult { admitted: true }

function check_all_policies(action_name: string, actor: Node, context: ReadOnlyState) -> PolicyResult:
    applicable = get_applicable_policies(action_name)
    for policy_id in applicable:
        result = check_policy(policy_id, actor, context)
        if not result.admitted:
            return result    // first policy failure short-circuits
    return PolicyResult { admitted: true }

10. ReadOnlyState Interface

interface ReadOnlyState:
    reputation:    Map<NodeId, Map<Domain, int64>>
    tokens:        Map<NodeId, TokenRecord[]>
    stakes:        Map<NodeId, int64>
    epoch:         int64
    event_count:   int64
    fork_id:       bytes32
    rule_version:  string

    function with_binding(name: string, value: any) -> ReadOnlyState
    // Returns new context with name bound; original unchanged

11. Dependencies

Module Interaction
η Proof Store Merkle proofs for each state transition
θ Consensus Validates rule evaluation is deterministic and agreed
λ Reputation Rule engine computes reputation deltas via apply_decay + action_table
ι State Fork Rule version conflicts trigger automatic fork creation
μ Integrity Sentinel can dampen effects (advisory, not blocking)
[[concepts/κ-rule-engine κ Rule Engine]] · [[spec/S11-rule-engine S11 Rule Engine Spec]] · [[reference/dsl DSL Reference]] · [[reference/extractions/theta-consensus-extraction θ Extraction]] · [[reference/extractions/lambda-reputation-extraction λ Extraction]]

Back to top

Colibri — documentation-first MCP runtime. Apache 2.0 + Commons Clause.

This site uses Just the Docs, a documentation theme for Jekyll.