Skip to content

Rules API Reference

Complete API reference for rule classes in KBS.

Table of Contents


KBS::Rule

A production rule that fires when all conditions match.

Structure: A rule consists of: 1. Name - Unique identifier 2. Priority - Execution order (higher = more urgent) 3. Conditions - Array of patterns to match 4. Action - Lambda executed when all conditions match


Constructor

initialize(name, conditions: [], action: nil, priority: 0, &block)

Creates a new rule.

Parameters: - name (Symbol or String) - Unique rule identifier - conditions (Array, optional) - Conditions to match (default: []) - action (Proc, optional) - Action lambda to execute (default: nil) - priority (Integer, optional) - Rule priority (default: 0) - &block (Block, optional) - Configuration block yielding self

Returns: KBS::Rule instance

Example - Low-level API (Direct Construction):

# Minimal rule
rule = KBS::Rule.new(:high_temperature)

# Rule with all parameters
rule = KBS::Rule.new(
  :high_temperature,
  conditions: [
    KBS::Condition.new(:temperature, value: ->(v) { v > 80 })
  ],
  action: ->(facts) { puts "High temperature detected!" },
  priority: 10
)

Example - Low-level API (Block Configuration):

rule = KBS::Rule.new(:high_temperature) do |r|
  r.conditions << KBS::Condition.new(:temperature, value: ->(v) { v > 80 })
  r.action = ->(facts) { puts "High temperature: #{facts[0][:value]}" }
end

Using DSL (Recommended):

kb = KBS.knowledge_base do
  rule "high_temperature", priority: 10 do
    on :temperature, value: greater_than(80)
    perform do |facts, bindings|
      puts "High temperature: #{bindings[:value?]}"
    end
  end
end

# Add to engine
kb.rules.each { |r| engine.add_rule(r) }


Public Attributes

name

Type: Symbol or String

Read-only: Yes (via attr_reader)

Description: Unique rule identifier

Example - Low-level API:

rule = KBS::Rule.new(:high_temperature, priority: 10)
puts rule.name  # => :high_temperature

Using DSL (Recommended):

kb = KBS.knowledge_base do
  rule "high_temperature", priority: 10 do
    on :temperature, value: greater_than(80)
    perform { puts "Alert!" }
  end
end

puts kb.rules.first.name  # => "high_temperature"

Best Practice: Use descriptive names that indicate the rule's purpose:

# Good
"high_temperature_alert"
"low_inventory_reorder"
"fraud_detection_high_risk"

# Less clear
"rule1"
"temp_rule"
"check"


priority

Type: Integer

Read-only: Yes (via attr_reader)

Description: Rule priority (higher = executes first in KBS::Blackboard::Engine)

Default: 0

Range: Any integer (commonly 0-100)

Example - Low-level API:

rule = KBS::Rule.new(:critical_alert, priority: 100)
puts rule.priority  # => 100

Using DSL (Recommended):

kb = KBS.knowledge_base do
  rule "critical_alert", priority: 100 do
    on :alert, level: "critical"
    perform { puts "CRITICAL ALERT!" }
  end
end

Priority Semantics: - KBS::Engine: Priority is stored but NOT used for execution order (rules fire in arbitrary order) - KBS::Blackboard::Engine: Higher priority rules fire first within production nodes

Common Priority Ranges:

# Critical safety rules
priority: 100

# Important business rules
priority: 50

# Standard rules
priority: 10

# Cleanup/logging rules
priority: 0

# Background tasks
priority: -10

Example - Priority Ordering:

kb = KBS.knowledge_base do
  rule "log_temperature", priority: 0 do
    on :temperature, value: :temp?
    perform { |facts, b| puts "Logged: #{b[:temp?]}" }
  end

  rule "critical_alert", priority: 100 do
    on :temperature, value: greater_than(100)
    perform { puts "CRITICAL TEMPERATURE!" }
  end

  rule "high_alert", priority: 50 do
    on :temperature, value: greater_than(80)
    perform { puts "High temperature warning" }
  end
end

engine = KBS::Blackboard::Engine.new
kb.rules.each { |r| engine.add_rule(r) }
engine.add_fact(:temperature, value: 110)
engine.run

# Output (in priority order):
# CRITICAL TEMPERATURE!      (priority 100)
# High temperature warning   (priority 50)
# Logged: 110                (priority 0)


conditions

Type: Array<KBS::Condition>

Read/Write: Yes (via attr_accessor)

Description: Array of conditions that must all match for rule to fire

Example - Low-level API:

rule = KBS::Rule.new(:temperature_alert)
rule.conditions << KBS::Condition.new(:temperature, value: ->(v) { v > 80 })
rule.conditions << KBS::Condition.new(:sensor, status: "active")

puts rule.conditions.size  # => 2

Using DSL (Recommended):

kb = KBS.knowledge_base do
  rule "temperature_alert" do
    on :temperature, value: greater_than(80)
    on :sensor, status: "active"
    perform { puts "Alert!" }
  end
end

puts kb.rules.first.conditions.size  # => 2

Condition Order Matters (for performance):

Low-level API:

# Good - Most selective condition first
rule.conditions = [
  KBS::Condition.new(:sensor, id: 42),             # Filters to 1 fact
  KBS::Condition.new(:temperature, value: :temp?)  # Then match temperature
]

# Less optimal - Less selective first
rule.conditions = [
  KBS::Condition.new(:temperature, value: :temp?),  # Matches many facts
  KBS::Condition.new(:sensor, id: 42)               # Could have filtered first
]

Using DSL (Recommended):

# Good - Most selective condition first
rule "sensor_alert" do
  on :sensor, id: 42                    # Filters to 1 fact
  on :temperature, value: :temp?        # Then match temperature
  perform { |facts, b| puts b[:temp?] }
end

# Less optimal - Less selective first
rule "sensor_alert" do
  on :temperature, value: :temp?        # Matches many facts
  on :sensor, id: 42                    # Could have filtered first
  perform { |facts, b| puts b[:temp?] }
end

See Performance Guide for condition ordering strategies.


action

Type: Proc (lambda or proc)

Read/Write: Yes (via attr_accessor)

Description: Lambda executed when all conditions match

Signature: action.call(facts) or action.call(facts, bindings) (both supported)

Parameters: - facts (Array) - Array of matched facts (parallel to conditions array) - bindings (Hash, optional) - Variable bindings extracted from facts

Example - Low-level API (Facts Parameter):

rule.action = ->(facts) do
  temp_fact = facts[0]  # First condition's matched fact
  sensor_fact = facts[1]  # Second condition's matched fact

  puts "Temperature: #{temp_fact[:value]} from sensor #{sensor_fact[:id]}"
end

Example - Low-level API (Bindings Parameter):

# Rule with variable bindings
rule = KBS::Rule.new(:temperature_alert) do |r|
  r.conditions << KBS::Condition.new(:temperature, value: :temp?, location: :loc?)
  r.action = ->(facts, bindings) do
    # bindings: {:temp? => 85, :loc? => "server_room"}
    puts "#{bindings[:loc?]}: #{bindings[:temp?]}°F"
  end
end

Using DSL (Recommended):

rule "temperature_alert" do
  on :temperature, value: :temp?, location: :loc?
  perform do |facts, bindings|
    # Cleaner - DSL automatically provides bindings
    puts "#{bindings[:loc?]}: #{bindings[:temp?]}°F"
  end
end

Action Requirements: - Must be a Proc (lambda or proc) - Should be idempotent if possible (safe to run multiple times) - Should not modify facts directly (use engine.add_fact / engine.remove_fact instead) - May add/remove facts (triggers new rule evaluation)


Public Methods

fire(facts)

Executes the rule's action with matched facts.

Parameters: - facts (Array) - Matched facts (one per condition)

Returns: Result of action lambda, or nil if no action

Side Effects: - Increments internal @fired_count - Executes action lambda - Action may modify external state, add/remove facts, etc.

Example - Low-level API:

rule = KBS::Rule.new(:log_temperature) do |r|
  r.conditions << KBS::Condition.new(:temperature, value: :temp?)
  r.action = ->(facts, bindings) do
    puts "Temperature: #{bindings[:temp?]}"
  end
end

fact = KBS::Fact.new(:temperature, value: 85)
rule.fire([fact])
# Output: Temperature: 85

Using DSL (Recommended):

kb = KBS.knowledge_base do
  rule "log_temperature" do
    on :temperature, value: :temp?
    perform do |facts, bindings|
      puts "Temperature: #{bindings[:temp?]}"
    end
  end

  fact :temperature, value: 85
  run  # Fires the rule automatically
end
# Output: Temperature: 85

Note: Typically called by the RETE engine, not user code. Users call engine.run which fires all activated rules.


Rule Lifecycle

1. Rule Creation

# Via DSL (recommended)
kb = KBS.knowledge_base do
  rule "my_rule", priority: 10 do
    on :temperature, value: :temp?
    perform { |facts, b| puts b[:temp?] }
  end
end

# Or programmatically
rule = KBS::Rule.new(
  :my_rule,
  conditions: [KBS::Condition.new(:temperature, value: :temp?)],
  action: ->(facts) { puts facts[0][:value] },
  priority: 10
)

2. Rule Registration

engine.add_rule(rule)
# Internally:
# - Adds rule to @rules array
# - Compiles rule into RETE network
# - Creates alpha memories for condition patterns
# - Creates join nodes (or negation nodes)
# - Creates production node for rule
# - Activates existing facts through new network

3. Rule Activation

engine.add_fact(:temperature, value: 85)
# Internally:
# - Fact activates matching alpha memories
# - Propagates through join nodes
# - Creates tokens in beta memories
# - Token reaches production node
# - Rule is "activated" (ready to fire)

4. Rule Firing

engine.run
# Internally (KBS::Engine):
# - Iterates production nodes
# - For each token in production node:
#   - Calls rule.fire(token.facts)
#   - Executes action lambda

# Internally (KBS::Blackboard::Engine):
# - Same as above, but:
#   - Logs rule firing to audit trail
#   - Marks token as fired (prevents duplicate firing)
#   - Records variable bindings

5. Rule Re-firing

Rules can fire multiple times:

rule "log_temperature" do
  on :temperature, value: :temp?
  perform { |facts, b| puts "Temperature: #{b[:temp?]}" }
end

engine.add_fact(:temperature, value: 85)
engine.add_fact(:temperature, value: 90)
engine.add_fact(:temperature, value: 95)
engine.run

# Output:
# Temperature: 85
# Temperature: 90
# Temperature: 95

Each fact creates a separate activation (token) that fires independently.


Rule Patterns

1. Simple Rule (One Condition)

Match single fact type:

rule "log_all_temperatures" do
  on :temperature, value: :temp?
  perform do |facts, bindings|
    puts "Temperature: #{bindings[:temp?]}"
  end
end

2. Join Rule (Multiple Conditions)

Match multiple related facts:

rule "sensor_temperature_alert" do
  on :sensor, id: :sensor_id?, status: "active"
  on :temperature, sensor_id: :sensor_id?, value: greater_than(80)
  perform do |facts, bindings|
    puts "Sensor #{bindings[:sensor_id?]} reports high temperature"
  end
end

# Matches when:
# - sensor fact with id=42, status="active" exists
# - temperature fact with sensor_id=42, value > 80 exists

Variable Binding: :sensor_id? in first condition must equal sensor_id in second condition (join test).


3. Guard Rule (Negation)

Match when fact is absent:

rule "all_clear" do
  on :system, status: "running"
  negated :alert, level: "critical"  # Fire when NO critical alerts exist
  perform do
    puts "All systems normal"
  end
end

4. State Machine Rule

Rules can implement state transitions:

rule "pending_to_processing" do
  on :order, id: :order_id?, status: "pending"
  on :worker, status: "available", id: :worker_id?
  perform do |facts, bindings|
    # Transition order to processing
    order = find_order(bindings[:order_id?])
    order.update(status: "processing", worker_id: bindings[:worker_id?])

    # Update worker
    worker = find_worker(bindings[:worker_id?])
    worker.update(status: "busy")
  end
end

5. Cleanup Rule

Low-priority rules that clean up old facts:

rule "expire_old_temperatures", priority: 0 do
  on :temperature, timestamp: less_than(Time.now - 3600)
  perform do |facts, bindings|
    fact = bindings[:matched_fact?]
    fact.retract  # Remove old temperature reading
  end
end

6. Aggregation Rule

Collect multiple facts and compute aggregate:

rule "daily_temperature_summary", priority: 5 do
  on :trigger, event: "end_of_day"
  perform do
    temps = engine.working_memory.facts
      .select { |f| f.type == :temperature }
      .map { |f| f[:value] }

    avg = temps.sum / temps.size.to_f
    max = temps.max
    min = temps.min

    engine.add_fact(:daily_summary, avg: avg, max: max, min: min, date: Date.today)
  end
end

7. Conflict Resolution Rule

Higher priority rule overrides lower priority:

rule "high_risk_order", priority: 100 do
  on :order, id: :order_id?, total: greater_than(10000)
  perform do |facts, bindings|
    puts "HIGH RISK: Order #{bindings[:order_id?]} requires manual review"
    # This fires first due to priority
  end
end

rule "auto_approve_order", priority: 10 do
  on :order, id: :order_id?, status: "pending"
  perform do |facts, bindings|
    puts "Auto-approving order #{bindings[:order_id?]}"
    # This fires later (if at all)
  end
end

8. Recursive Rule

Rule that adds facts triggering other rules:

rule "calculate_fibonacci" do
  on :fib_request, n: :n?
  negated :fib_result, n: :n?  # Not already calculated
  perform do |facts, bindings|
    n = bindings[:n?]

    if n <= 1
      engine.add_fact(:fib_result, n: n, value: n)
    else
      # Request sub-problems
      engine.add_fact(:fib_request, n: n - 1)
      engine.add_fact(:fib_request, n: n - 2)

      # Wait for sub-results in another rule...
    end
  end
end

rule "combine_fibonacci" do
  on :fib_request, n: :n?
  on :fib_result, n: :n_minus_1?, value: :val1?
  on :fib_result, n: :n_minus_2?, value: :val2?
  # ... (complex join test: ?n_minus_1 == ?n - 1, etc.)
  perform do |facts, bindings|
    result = bindings[:val1?] + bindings[:val2?]
    engine.add_fact(:fib_result, n: bindings[:n?], value: result)
  end
end

Best Practices

1. Descriptive Rule Names

# Good
rule "high_temperature_alert"
rule "low_inventory_reorder"
rule "fraud_detection_suspicious_transaction"

# Bad
rule "rule1"
rule "temp"
rule "check"

2. Order Conditions by Selectivity

Most selective (fewest matching facts) first:

# Good - sensor_id=42 filters to ~1 fact
rule "sensor_alert" do
  on :sensor, id: 42, status: :status?              # Very selective
  on :temperature, sensor_id: 42, value: :temp?     # Also selective
  perform { ... }
end

# Bad - :temperature matches many facts
rule "sensor_alert" do
  on :temperature, value: :temp?                    # Matches 1000s of facts
  on :sensor, id: 42, status: :status?              # Could have filtered first
  perform { ... }
end

Why: RETE builds network from first to last condition. Fewer intermediate tokens = faster.


3. Use Priority for Critical Rules

rule "critical_shutdown", priority: 1000 do
  on :temperature, value: greater_than(120)
  perform { shutdown_system! }
end

rule "log_temperature", priority: 0 do
  on :temperature, value: :temp?
  perform { |facts, b| log(b[:temp?]) }
end

Critical safety rules should have high priority to fire before less important rules.


4. Keep Actions Idempotent

# Good - Idempotent (safe to run multiple times)
rule "alert_high_temp" do
  on :temperature, value: greater_than(80)
  perform do |facts, bindings|
    # Check if alert already sent
    unless alert_sent?(bindings[:temp?])
      send_alert(bindings[:temp?])
      mark_alert_sent(bindings[:temp?])
    end
  end
end

# Bad - Not idempotent (sends duplicate alerts)
rule "alert_high_temp" do
  on :temperature, value: greater_than(80)
  perform do |facts, bindings|
    send_alert(bindings[:temp?])  # Sends every time rule fires
  end
end

5. Avoid Side Effects in Conditions

# Bad - Side effect in condition predicate
counter = 0
rule "count_temps" do
  on :temperature, value: ->(v) { counter += 1; v > 80 }  # BAD!
  perform { puts "Count: #{counter}" }
end

# Good - Side effects in action only
counter = 0
rule "count_temps" do
  on :temperature, value: greater_than(80)
  perform { counter += 1; puts "Count: #{counter}" }
end

Why: Predicates run during pattern matching (potentially multiple times). Side effects cause unpredictable behavior.


6. Use Variable Bindings for Joins

# Good - Variable binding creates join test
rule "order_inventory_check" do
  on :order, product_id: :pid?, quantity: :qty?
  on :inventory, product_id: :pid?, available: :available?
  perform do |facts, bindings|
    if bindings[:available?] < bindings[:qty?]
      puts "Insufficient inventory for product #{bindings[:pid?]}"
    end
  end
end

# Bad - No join test (matches all combinations)
rule "order_inventory_check" do
  on :order, product_id: :pid1?, quantity: :qty?
  on :inventory, product_id: :pid2?, available: :available?
  perform do |facts, bindings|
    # No guarantee pid1 == pid2!
    if bindings[:pid1?] == bindings[:pid2?]  # Manual check in action (inefficient)
      ...
    end
  end
end

7. Document Complex Rules

# Good - Documented
rule "portfolio_rebalancing", priority: 50 do
  # Triggers when portfolio drift exceeds threshold
  # Conditions:
  # 1. Portfolio exists and is active
  # 2. Current allocation deviates > 5% from target
  # Action:
  # - Calculates rebalancing trades
  # - Creates pending orders

  on :portfolio, id: :portfolio_id?, status: "active"
  on :drift_calculation, portfolio_id: :portfolio_id?, drift: greater_than(0.05)
  perform do |facts, bindings|
    # Implementation...
  end
end

8. Test Rules in Isolation

require 'minitest/autorun'

class TestHighTemperatureRule < Minitest::Test
  def setup
    @engine = KBS::Blackboard::Engine.new
    @fired = false

    @rule = KBS::Rule.new(:high_temp) do |r|
      r.conditions << KBS::Condition.new(:temperature, value: ->(v) { v > 80 })
      r.action = ->(facts) { @fired = true }
    end

    @engine.add_rule(@rule)
  end

  def test_fires_when_temperature_high
    @engine.add_fact(:temperature, value: 85)
    @engine.run
    assert @fired
  end

  def test_does_not_fire_when_temperature_low
    @engine.add_fact(:temperature, value: 75)
    @engine.run
    refute @fired
  end
end

9. Use Negation for Guards

# Good - Negation ensures system ready
rule "start_processing" do
  on :work_item, status: "pending"
  negated :system_error  # Don't process if system has errors
  perform { process_work_item }
end

# Alternative - Check in action (less efficient)
rule "start_processing" do
  on :work_item, status: "pending"
  perform do
    unless system_has_errors?
      process_work_item
    end
  end
end

Why: Negation in condition prevents token creation. Action-based check still creates token (wastes memory).


10. Limit Fact Growth

# Good - Cleanup rule prevents unbounded growth
rule "expire_old_facts", priority: 0 do
  on :temperature, timestamp: less_than(Time.now - 3600)
  perform do |facts, bindings|
    fact = bindings[:matched_fact?]
    fact.retract
  end
end

# Bad - No cleanup (memory leak)
loop do
  engine.add_fact(:temperature, value: rand(100), timestamp: Time.now)
  engine.run
  sleep 1
  # Facts accumulate forever!
end

Common Patterns Reference

Rule Priority Examples

# Emergency shutdown
priority: 1000

# Critical alerts
priority: 500

# Business logic
priority: 100

# Data validation
priority: 50

# Standard processing
priority: 10

# Logging/auditing
priority: 5

# Cleanup
priority: 0

Action Signatures

# 1. Facts only
action: ->(facts) do
  temp_fact = facts[0]
  puts temp_fact[:value]
end

# 2. Facts and bindings (recommended)
action: ->(facts, bindings) do
  puts bindings[:temp?]
end

# 3. DSL style (cleanest)
perform do |facts, bindings|
  puts bindings[:temp?]
end

Condition Patterns

# Literal matching
on :temperature, location: "server_room"

# Range check
on :temperature, value: between(70, 90)
on :temperature, value: greater_than(80)
on :temperature, value: less_than(100)

# Variable binding
on :temperature, location: :loc?, value: :temp?

# Predicate
on :temperature, value: ->(v) { v > 80 && v < 100 }

# Negation
negated :alert, level: "critical"

# Collection membership
on :order, status: one_of("pending", "processing", "completed")

Performance Considerations

Rule Compilation Cost

Adding a rule to the engine compiles it into the RETE network:

# Cost: O(C) where C = number of conditions
engine.add_rule(rule)

Optimization: Add all rules before adding facts:

# Good
kb.rules.each { |r| engine.add_rule(r) }  # Compile all rules first
facts.each { |f| engine.add_fact(f.type, f.attributes) }  # Then add facts
engine.run

# Less optimal
facts.each do |f|
  engine.add_fact(f.type, f.attributes)
  kb.rules.each { |r| engine.add_rule(r) }  # Recompiling for each fact!
  engine.run
end

Condition Ordering

Order conditions from most to least selective:

# Assume:
# - 10,000 temperature facts
# - 100 sensor facts
# - 10 sensors with id=42

# Good (selective first)
rule "alert" do
  on :sensor, id: 42, status: :status?        # Filters to 10 facts
  on :temperature, sensor_id: 42, value: :v?  # Then filters to ~100 facts
  # Creates ~10 intermediate tokens
end

# Bad (unselective first)
rule "alert" do
  on :temperature, value: :v?                 # Matches 10,000 facts!
  on :sensor, id: 42, status: :status?        # Then filters
  # Creates 10,000 intermediate tokens (slow, memory-intensive)
end

Action Complexity

Keep actions fast:

# Good - Fast action
perform do |facts, bindings|
  puts "Temperature: #{bindings[:temp?]}"
end

# Bad - Slow action blocks engine
perform do |facts, bindings|
  sleep 5  # Blocks engine for 5 seconds!
  send_email_alert(bindings[:temp?])  # Network I/O
end

# Better - Offload slow work
perform do |facts, bindings|
  # Post message for async worker
  engine.post_message("alert_system", "email_queue", bindings)
end

Debugging Rules

Why Didn't My Rule Fire?

def debug_rule(engine, rule_name)
  rule = engine.rules.find { |r| r.name == rule_name }
  return "Rule not found" unless rule

  puts "Rule: #{rule.name}"
  puts "Conditions (#{rule.conditions.size}):"

  rule.conditions.each_with_index do |cond, i|
    matching_facts = engine.working_memory.facts.select { |f| f.matches?(cond.pattern.merge(type: cond.type)) }

    puts "  #{i + 1}. #{cond.type} #{cond.pattern}"
    puts "     Negated: #{cond.negated}"
    puts "     Matching facts: #{matching_facts.size}"

    if matching_facts.empty?
      puts "     ❌ NO MATCHING FACTS (rule can't fire)"
    else
      puts "     ✓ #{matching_facts.size} facts match"
      matching_facts.first(3).each do |f|
        puts "       - #{f}"
      end
    end
  end

  # Check production node
  prod_node = engine.production_nodes[rule.name]
  if prod_node
    puts "Production node activations: #{prod_node.tokens.size}"
  else
    puts "Production node not found (rule not compiled?)"
  end
end

debug_rule(engine, :high_temperature)

See Also