Facts API Reference¶
Complete API reference for fact and condition classes in KBS.
Table of Contents¶
- KBS::Fact - Transient in-memory fact
- KBS::Blackboard::Fact - Persistent fact with UUID
- KBS::Condition - Pattern matching condition
- Fact Patterns
- Pattern Matching Semantics
KBS::Fact¶
Transient in-memory fact used by the core RETE engine.
Constructor¶
initialize(type, attributes = {})
¶
Creates a new transient fact.
Parameters:
- type
(Symbol) - Fact type (e.g., :temperature
, :order
)
- attributes
(Hash, optional) - Fact attributes (default: {}
)
Returns: KBS::Fact
instance
Example:
# Fact with attributes
fact = KBS::Fact.new(:temperature, location: "server_room", value: 85)
# Fact without attributes (marker/flag)
flag = KBS::Fact.new(:system_ready)
Internal Behavior:
- @id
is set to object_id
(unique Ruby object identifier)
- @type
stores the fact type
- @attributes
stores the attribute hash
Public Attributes¶
id
¶
Type: Integer
Read-only: Yes (via attr_reader
)
Description: Unique identifier (Ruby object ID)
Example:
Note: Not stable across Ruby processes. For persistent IDs, use KBS::Blackboard::Fact
with UUIDs.
type
¶
Type: Symbol
Read-only: Yes (via attr_reader
)
Description: The fact type
Example:
attributes
¶
Type: Hash
Read-only: Yes (via attr_reader
)
Description: The fact's attribute hash
Example:
fact = KBS::Fact.new(:temperature, location: "server_room", value: 85)
puts fact.attributes # => {:location=>"server_room", :value=>85}
Important: Direct modification bypasses change tracking:
# Don't do this (changes not tracked)
fact.attributes[:value] = 90
# Instead use []= accessor
fact[:value] = 90
Public Methods¶
[](key)
¶
Retrieves an attribute value.
Parameters:
- key
(Symbol) - Attribute key
Returns: Attribute value or nil
if not present
Example:
fact = KBS::Fact.new(:temperature, location: "server_room", value: 85)
puts fact[:location] # => "server_room"
puts fact[:value] # => 85
puts fact[:missing] # => nil
[]=(key, value)
¶
Sets an attribute value.
Parameters:
- key
(Symbol) - Attribute key
- value
- Attribute value
Returns: The value
Side Effects: Modifies the fact's attributes hash
Example:
fact = KBS::Fact.new(:temperature, value: 85)
fact[:value] = 90
fact[:timestamp] = Time.now
puts fact.attributes # => {:value=>90, :timestamp=>2025-01-15 10:30:00}
Important for KBS::Fact: Changes are NOT persisted and do NOT trigger re-evaluation. For tracked updates, use KBS::Blackboard::Fact
.
matches?(pattern)
¶
Checks if this fact matches a pattern.
Parameters:
- pattern
(Hash) - Pattern hash with :type
and attribute constraints
Returns: true
if matches, false
otherwise
Pattern Types:
1. Type constraint: pattern[:type]
must equal fact type
2. Literal values: Attribute must equal specified value
3. Predicate lambdas: value.is_a?(Proc)
- attribute passed to lambda, must return truthy
4. Variable bindings: value.is_a?(Symbol) && value.to_s.start_with?('?')
- always matches (variable captures value)
Example - Literal Matching:
fact = KBS::Fact.new(:temperature, location: "server_room", value: 85)
# Type only
fact.matches?(type: :temperature) # => true
fact.matches?(type: :pressure) # => false
# Type + literal attribute
fact.matches?(type: :temperature, location: "server_room") # => true
fact.matches?(type: :temperature, location: "lobby") # => false
# Multiple literals
fact.matches?(type: :temperature, location: "server_room", value: 85) # => true
fact.matches?(type: :temperature, location: "server_room", value: 90) # => false
Example - Predicate Matching:
fact = KBS::Fact.new(:temperature, value: 85)
# Lambda predicate
fact.matches?(type: :temperature, value: ->(v) { v > 80 }) # => true
fact.matches?(type: :temperature, value: ->(v) { v > 100 }) # => false
# Complex predicate
fact.matches?(
type: :temperature,
value: ->(v) { v >= 70 && v <= 90 }
) # => true
Example - Variable Binding:
fact = KBS::Fact.new(:temperature, location: "server_room", value: 85)
# Variables always match (they capture the value)
fact.matches?(type: :temperature, location: :loc?) # => true
fact.matches?(type: :temperature, value: :temp?) # => true
# Variables with other constraints
fact.matches?(
type: :temperature,
location: "server_room", # Literal constraint
value: :temp? # Variable binding
) # => true
Example - Missing Attributes:
fact = KBS::Fact.new(:temperature, value: 85) # No :location attribute
# Missing attributes fail predicate/literal checks
fact.matches?(type: :temperature, location: "server_room") # => false
fact.matches?(type: :temperature, location: ->(l) { l.length > 5 }) # => false (no :location)
# Missing attributes match variables
fact.matches?(type: :temperature, location: :loc?) # => true (variable matches nil)
Algorithm:
1. If pattern[:type]
present and doesn't match fact type → return false
2. For each key in pattern (except :type
):
- If value is Proc: call with fact attribute value, return false
if falsy or attribute missing
- If value is variable (symbol starting with ?
): skip (always matches)
- Otherwise: return false
if fact attribute ≠ pattern value
3. Return true
if all checks passed
to_s
¶
Returns string representation of fact.
Parameters: None
Returns: String
in format "type(attr1: val1, attr2: val2)"
Example:
fact = KBS::Fact.new(:temperature, location: "server_room", value: 85)
puts fact.to_s # => "temperature(location: server_room, value: 85)"
flag = KBS::Fact.new(:system_ready)
puts flag.to_s # => "system_ready()"
KBS::Blackboard::Fact¶
Persistent fact with UUID, used by blackboard memory.
Inherits: None (separate implementation from KBS::Fact
)
Key Differences from KBS::Fact:
- Has UUID instead of object ID
- []=
and update()
trigger persistence and audit logging
- retract()
method to remove from blackboard
- Reference to blackboard memory for update tracking
Constructor¶
initialize(uuid, type, attributes, blackboard = nil)
¶
Creates a persistent fact. Usually created via engine.add_fact()
, not directly.
Parameters:
- uuid
(String) - Unique identifier (UUID format)
- type
(Symbol) - Fact type
- attributes
(Hash) - Fact attributes
- blackboard
(KBS::Blackboard::Memory, optional) - Reference to blackboard (default: nil
)
Returns: KBS::Blackboard::Fact
instance
Example - Direct Construction (rare):
require 'securerandom'
fact = KBS::Blackboard::Fact.new(
SecureRandom.uuid,
:temperature,
{ location: "server_room", value: 85 }
)
puts fact.uuid # => "550e8400-e29b-41d4-a716-446655440000"
Example - Typical Usage:
engine = KBS::Blackboard::Engine.new(db_path: 'kb.db')
fact = engine.add_fact(:temperature, location: "server_room", value: 85)
# Returns KBS::Blackboard::Fact with UUID and blackboard reference
Public Attributes¶
uuid
¶
Type: String
Read-only: Yes (via attr_reader
)
Description: Globally unique identifier (UUID format)
Example:
fact = engine.add_fact(:temperature, value: 85)
puts fact.uuid # => "550e8400-e29b-41d4-a716-446655440000"
Use Cases: - Stable ID across restarts - Foreign keys in external systems - Audit trail references
type
¶
Type: Symbol
Read-only: Yes (via attr_reader
)
Description: The fact type
Example:
attributes
¶
Type: Hash
Read-only: Yes (via attr_reader
)
Description: The fact's attribute hash
Example:
fact = engine.add_fact(:temperature, location: "server_room", value: 85)
puts fact.attributes # => {:location=>"server_room", :value=>85}
Important: Direct modification bypasses persistence:
# Don't do this (not persisted)
fact.attributes[:value] = 90
# Instead use []= or update()
fact[:value] = 90
# or
fact.update(value: 90)
Public Methods¶
[](key)
¶
Retrieves an attribute value.
Parameters:
- key
(Symbol) - Attribute key
Returns: Attribute value or nil
if not present
Example:
fact = engine.add_fact(:temperature, location: "server_room", value: 85)
puts fact[:location] # => "server_room"
puts fact[:value] # => 85
puts fact[:missing] # => nil
[]=(key, value)
¶
Sets an attribute value with persistence.
Parameters:
- key
(Symbol) - Attribute key
- value
- Attribute value (must be JSON-serializable for most stores)
Returns: The value
Side Effects:
- Updates fact's attributes hash
- Calls blackboard.update_fact(self, @attributes)
if blackboard present
- Persists change to store
- Logs to audit trail
- Notifies observers
Example:
fact = engine.add_fact(:temperature, value: 85)
fact[:value] = 90 # Immediately persisted
# After restart
engine2 = KBS::Blackboard::Engine.new(db_path: 'kb.db')
reloaded = engine2.blackboard.get_facts_by_type(:temperature).first
puts reloaded[:value] # => 90
Important: Updates do NOT trigger rule re-evaluation. To trigger rules, retract and re-add:
update(new_attributes)
¶
Bulk update multiple attributes with persistence.
Parameters:
- new_attributes
(Hash) - Hash of attributes to merge
Returns: nil
Side Effects:
- Merges new_attributes
into @attributes
- Persists changes
- Logs to audit trail
- Notifies observers
Example:
fact = engine.add_fact(:temperature, location: "server_room", value: 85)
fact.update(value: 90, timestamp: Time.now)
puts fact.attributes
# => {:location=>"server_room", :value=>90, :timestamp=>2025-01-15 10:30:00}
Difference from []=
: Updates multiple attributes in single persistence operation (more efficient).
retract()
¶
Removes this fact from the blackboard.
Parameters: None
Returns: nil
Side Effects:
- Calls blackboard.remove_fact(self)
if blackboard present
- Marks fact as inactive in store
- Logs retraction to audit trail
- Deactivates in alpha memories
- Notifies observers
Example:
fact = engine.add_fact(:temperature, value: 85)
fact.retract # Fact removed
# Equivalent to:
engine.remove_fact(fact)
Use Case: Fact self-destruction in rule actions:
rule "auto_expire_old_alerts" do
on :alert, timestamp: ->(ts) { Time.now - ts > 3600 }
perform do |facts, bindings|
# Fact can remove itself
alert_fact = bindings[:matched_fact?]
alert_fact.retract
end
end
matches?(pattern)
¶
Checks if this fact matches a pattern. Same semantics as KBS::Fact#matches?
.
Parameters:
- pattern
(Hash) - Pattern hash with :type
and attribute constraints
Returns: true
if matches, false
otherwise
Example:
fact = engine.add_fact(:temperature, location: "server_room", value: 85)
fact.matches?(type: :temperature) # => true
fact.matches?(type: :temperature, value: ->(v) { v > 80 }) # => true
fact.matches?(type: :pressure) # => false
See KBS::Fact#matches?
for detailed semantics.
to_s
¶
Returns string representation with UUID prefix.
Parameters: None
Returns: String
in format "type(uuid_prefix...: attr1=val1, attr2=val2)"
Example:
fact = engine.add_fact(:temperature, location: "server_room", value: 85)
puts fact.to_s
# => "temperature(550e8400...: location=server_room, value=85)"
Note: Only first 8 characters of UUID shown for brevity.
to_h
¶
Returns hash representation of fact.
Parameters: None
Returns: Hash
with keys :uuid
, :type
, :attributes
Example:
fact = engine.add_fact(:temperature, location: "server_room", value: 85)
hash = fact.to_h
puts hash
# => {
# :uuid => "550e8400-e29b-41d4-a716-446655440000",
# :type => :temperature,
# :attributes => {:location=>"server_room", :value=>85}
# }
Use Cases: - Serialization for APIs - Logging - Testing assertions
KBS::Condition¶
Pattern matching condition used in rule definitions.
Constructor¶
initialize(type, pattern = {}, negated: false)
¶
Creates a condition that matches facts.
Parameters:
- type
(Symbol) - Fact type to match
- pattern
(Hash, optional) - Attribute constraints (default: {}
)
- negated
(Boolean, optional) - If true
, condition matches when pattern is absent (default: false
)
Returns: KBS::Condition
instance
Example - Positive Condition:
# Match any temperature fact
condition = KBS::Condition.new(:temperature)
# Match temperature facts with location="server_room"
condition = KBS::Condition.new(:temperature, location: "server_room")
# Match temperature facts with value > 80
condition = KBS::Condition.new(:temperature, value: ->(v) { v > 80 })
Example - Negated Condition:
# Match when there is NO alert fact
condition = KBS::Condition.new(:alert, {}, negated: true)
# Match when there is NO critical alert
condition = KBS::Condition.new(:alert, { level: "critical" }, negated: true)
Example - Variable Binding:
# Capture temperature value in :temp? variable
condition = KBS::Condition.new(:temperature, value: :temp?)
# Capture location and value
condition = KBS::Condition.new(
:temperature,
location: :loc?,
value: :temp?
)
Public Attributes¶
type
¶
Type: Symbol
Read-only: Yes (via attr_reader
)
Description: The fact type this condition matches
Example:
pattern
¶
Type: Hash
Read-only: Yes (via attr_reader
)
Description: The attribute pattern to match
Example:
condition = KBS::Condition.new(:temperature, location: "server_room", value: :temp?)
puts condition.pattern # => {:location=>"server_room", :value=>:temp?}
negated
¶
Type: Boolean
Read-only: Yes (via attr_reader
)
Description: Whether this is a negation condition
Example:
pos_condition = KBS::Condition.new(:temperature, value: :temp?)
puts pos_condition.negated # => false
neg_condition = KBS::Condition.new(:alert, {}, negated: true)
puts neg_condition.negated # => true
variable_bindings
¶
Type: Hash<Symbol, Symbol>
Read-only: Yes (via attr_reader
)
Description: Map of variable names to attribute keys (e.g., {:temp? => :value}
)
Example:
condition = KBS::Condition.new(
:temperature,
location: :loc?,
value: :temp?
)
puts condition.variable_bindings
# => {:loc?=>:location, :temp?=>:value}
Use Case: RETE engine uses this to extract bindings when condition matches:
fact = KBS::Fact.new(:temperature, location: "server_room", value: 85)
bindings = {}
condition.variable_bindings.each do |var, attr|
bindings[var] = fact[attr]
end
puts bindings # => {:loc?=>"server_room", :temp?=>85}
Fact Patterns¶
Patterns are hashes used to match facts. They appear in:
- Condition.new(type, pattern)
- Fact#matches?(pattern)
- Alpha memory keys
Pattern Structure¶
{
type: :fact_type, # Optional: fact type constraint
attribute1: literal_value, # Literal constraint
attribute2: :variable?, # Variable binding
attribute3: ->(v) { ... } # Predicate lambda
}
Pattern Types¶
1. Empty Pattern¶
Matches all facts of a type.
2. Literal Pattern¶
Matches facts with exact attribute values.
condition = KBS::Condition.new(
:temperature,
location: "server_room",
sensor_id: 42
)
# Matches:
KBS::Fact.new(:temperature, location: "server_room", sensor_id: 42, value: 85)
# Doesn't match:
KBS::Fact.new(:temperature, location: "lobby", sensor_id: 42)
KBS::Fact.new(:temperature, location: "server_room", sensor_id: 99)
3. Predicate Pattern¶
Matches facts where attribute satisfies lambda.
condition = KBS::Condition.new(
:temperature,
value: ->(v) { v > 80 && v < 100 },
location: ->(l) { l.start_with?("server") }
)
# Matches:
KBS::Fact.new(:temperature, location: "server_room", value: 85)
KBS::Fact.new(:temperature, location: "server_1", value: 90)
# Doesn't match:
KBS::Fact.new(:temperature, location: "server_room", value: 110) # value > 100
KBS::Fact.new(:temperature, location: "lobby", value: 85) # location doesn't start with "server"
Important: Predicate fails if attribute is missing:
fact = KBS::Fact.new(:temperature, location: "server_room") # No :value
fact.matches?(type: :temperature, value: ->(v) { v > 0 }) # => false (no :value attribute)
4. Variable Binding Pattern¶
Variables (symbols starting with ?
) capture attribute values for use in join tests and action blocks.
condition = KBS::Condition.new(
:temperature,
location: :loc?,
value: :temp?
)
# Matches ANY temperature fact, binding :loc? and :temp?
fact = KBS::Fact.new(:temperature, location: "server_room", value: 85)
# Bindings: {:loc? => "server_room", :temp? => 85}
5. Mixed Pattern¶
Combine literals, predicates, and variables.
condition = KBS::Condition.new(
:temperature,
location: "server_room", # Literal
value: :temp?, # Variable
timestamp: ->(ts) { ts > cutoff_time } # Predicate
)
# Only matches temperature facts from server_room with recent timestamp
# Captures value in :temp? variable
Pattern Matching Semantics¶
Matching Algorithm¶
For fact.matches?(pattern)
:
- Type Check: If
pattern[:type]
present, must equalfact.type
- Attribute Checks: For each
key, value
in pattern (except:type
): - Variable (
value.is_a?(Symbol) && value.to_s.start_with?('?')
): Always matches (capturesfact[key]
) - Predicate (
value.is_a?(Proc)
): Must satisfyvalue.call(fact[key])
. Fails iffact[key]
is nil. - Literal: Must equal
fact[key]
- Result:
true
if all checks pass,false
otherwise
Open World Assumption¶
Facts are not required to have all attributes in the pattern. Patterns only constrain attributes they specify.
fact = KBS::Fact.new(:temperature, location: "server_room", value: 85, timestamp: Time.now)
# Matches - pattern doesn't mention :timestamp
fact.matches?(type: :temperature, location: "server_room") # => true
# Matches - pattern only constrains :value
fact.matches?(type: :temperature, value: ->(v) { v > 80 }) # => true
But: If pattern specifies an attribute the fact lacks, match fails:
fact = KBS::Fact.new(:temperature, value: 85) # No :location
# Fails - fact missing :location attribute
fact.matches?(type: :temperature, location: "server_room") # => false
# Fails - predicate can't evaluate nil
fact.matches?(type: :temperature, location: ->(l) { l.length > 5 }) # => false
# Succeeds - variable matches nil
fact.matches?(type: :temperature, location: :loc?) # => true (binds :loc? => nil)
Variable Binding Extraction¶
Variables are extracted during condition construction:
condition = KBS::Condition.new(
:order,
symbol: :sym?,
quantity: :qty?,
price: :price?
)
puts condition.variable_bindings
# => {:sym?=>:symbol, :qty?=>:quantity, :price?=>:price}
When a fact matches, bindings are populated:
fact = KBS::Fact.new(:order, symbol: "AAPL", quantity: 100, price: 150.25)
bindings = {}
condition.variable_bindings.each do |var, attr|
bindings[var] = fact[attr]
end
puts bindings
# => {:sym?=>"AAPL", :qty?=>100, :price?=>150.25}
Predicate Constraints¶
Predicates are powerful but have caveats:
1. Nil Attributes Fail:
fact = KBS::Fact.new(:temperature, location: "server_room") # No :value
# Predicate fails - can't call lambda on nil
fact.matches?(type: :temperature, value: ->(v) { v > 0 }) # => false
2. Predicates Run on Every Match Attempt:
# This predicate runs every time a fact is checked
expensive_check = ->(v) { complex_calculation(v) }
condition = KBS::Condition.new(:temperature, value: expensive_check)
# For 1000 temperature facts, expensive_check runs 1000 times
3. Predicates Should Be Pure Functions:
# Bad - side effects
counter = 0
condition = KBS::Condition.new(:temperature, value: ->(v) { counter += 1; v > 80 })
# Good - pure predicate
condition = KBS::Condition.new(:temperature, value: ->(v) { v > 80 })
4. Predicates Can't Access Other Attributes:
# This doesn't work - predicate only receives attribute value
condition = KBS::Condition.new(
:temperature,
value: ->(v) { v > @threshold } # @threshold from where?
)
# Use closures to capture context
threshold = 80
condition = KBS::Condition.new(
:temperature,
value: ->(v) { v > threshold } # Closure captures threshold
)
Negation Semantics¶
Negated conditions match when NO fact satisfies the pattern:
# Rule fires when there's NO critical alert
rule "all_clear" do
negated :alert, level: "critical" # negated: true
perform { puts "All systems normal" }
end
Important: Negation matches absence, not presence of opposite:
# Matches when NO alert with level="critical" exists
negated :alert, level: "critical"
# NOT equivalent to: Match when alert with level != "critical" exists
# To match non-critical alerts, use predicate:
on :alert, level: ->(l) { l != "critical" }
See Negation Guide for detailed semantics.
Common Patterns¶
1. Range Checks¶
# Between 70 and 90
condition = KBS::Condition.new(
:temperature,
value: ->(v) { v >= 70 && v <= 90 }
)
# Outside range
condition = KBS::Condition.new(
:temperature,
value: ->(v) { v < 70 || v > 90 }
)
2. String Matching¶
# Starts with
condition = KBS::Condition.new(
:sensor,
name: ->(n) { n.start_with?("temp_") }
)
# Regex match
condition = KBS::Condition.new(
:sensor,
name: ->(n) { n =~ /^sensor_\d+$/ }
)
# Contains substring
condition = KBS::Condition.new(
:log_entry,
message: ->(m) { m.include?("ERROR") }
)
3. Collection Membership¶
# One of several values
valid_statuses = ["pending", "processing", "completed"]
condition = KBS::Condition.new(
:order,
status: ->(s) { valid_statuses.include?(s) }
)
# Not in collection
invalid_statuses = ["cancelled", "failed"]
condition = KBS::Condition.new(
:order,
status: ->(s) { !invalid_statuses.include?(s) }
)
4. Timestamp Checks¶
# Recent facts (last hour)
cutoff = Time.now - 3600
condition = KBS::Condition.new(
:temperature,
timestamp: ->(ts) { ts > cutoff }
)
# Old facts (older than 1 day)
cutoff = Time.now - 86400
condition = KBS::Condition.new(
:temperature,
timestamp: ->(ts) { ts < cutoff }
)
5. Cross-Attribute Constraints (Using Multiple Conditions)¶
You can't directly compare two attributes of the same fact in one condition. Use multiple conditions:
# Want: Match orders where quantity * price > 10000
# Can't do this in one condition:
# condition = KBS::Condition.new(:order, ...) # No way to access both :quantity and :price
# Instead: Capture variables and check in action or use join test
rule "large_order" do
on :order, quantity: :qty?, price: :price?
perform do |facts, bindings|
total = bindings[:qty?] * bindings[:price?]
if total > 10000
puts "Large order: $#{total}"
end
end
end
6. Null/Nil Checks¶
Variables capture nil
, predicates fail on nil
:
# Match facts with ANY value for :location (including nil)
condition = KBS::Condition.new(:temperature, location: :loc?)
# Matches fact.new(:temperature, location: nil) → binds :loc? => nil
# Matches fact.new(:temperature) → binds :loc? => nil
# Match facts with NON-NIL :location
condition = KBS::Condition.new(
:temperature,
location: ->(l) { !l.nil? }
)
# Fails fact.new(:temperature, location: nil)
# Fails fact.new(:temperature) (no :location attribute)
Performance Tips¶
1. Order Predicates by Selectivity¶
# Good - Most selective predicate first
condition = KBS::Condition.new(
:temperature,
sensor_id: 42, # Likely filters to 1 fact
value: ->(v) { v > 80 } # Then check value
)
# Less optimal - Expensive check first
condition = KBS::Condition.new(
:temperature,
value: ->(v) { expensive_calculation(v) }, # Runs on many facts
sensor_id: 42 # Could have filtered first
)
Note: Within a single condition, Ruby evaluates hash in insertion order (Ruby 1.9+), but RETE evaluates all constraints anyway. The real optimization is condition ordering in rules.
2. Avoid Expensive Predicates¶
# Bad - Complex regex on every fact
condition = KBS::Condition.new(
:log_entry,
message: ->(m) { m =~ /very.*complex.*regex.*pattern/ }
)
# Better - Simple check first, complex check in action
rule "complex_log_analysis" do
on :log_entry, level: "ERROR", message: :msg? # Simple literal filter
perform do |facts, bindings|
if bindings[:msg?] =~ /very.*complex.*regex.*pattern/
# Expensive check runs only on ERROR logs
end
end
end
3. Use Literals When Possible¶
Literals are fastest (hash equality check). Predicates are slower (lambda call).
# Fast
condition = KBS::Condition.new(:temperature, location: "server_room")
# Slower (but necessary for ranges/complex checks)
condition = KBS::Condition.new(:temperature, value: ->(v) { v > 80 })
Testing Patterns¶
Testing Fact Matching¶
require 'minitest/autorun'
class TestFactMatching < Minitest::Test
def test_literal_match
fact = KBS::Fact.new(:temperature, location: "server_room", value: 85)
assert fact.matches?(type: :temperature)
assert fact.matches?(type: :temperature, location: "server_room")
refute fact.matches?(type: :temperature, location: "lobby")
end
def test_predicate_match
fact = KBS::Fact.new(:temperature, value: 85)
assert fact.matches?(type: :temperature, value: ->(v) { v > 80 })
refute fact.matches?(type: :temperature, value: ->(v) { v > 100 })
end
def test_variable_binding
fact = KBS::Fact.new(:temperature, location: "server_room", value: 85)
# Variables always match
assert fact.matches?(type: :temperature, location: :loc?, value: :temp?)
end
def test_missing_attribute
fact = KBS::Fact.new(:temperature, value: 85) # No :location
# Literal fails on missing
refute fact.matches?(type: :temperature, location: "server_room")
# Predicate fails on missing
refute fact.matches?(type: :temperature, location: ->(l) { l.length > 0 })
# Variable succeeds on missing (binds to nil)
assert fact.matches?(type: :temperature, location: :loc?)
end
end
Testing Variable Extraction¶
class TestVariableExtraction < Minitest::Test
def test_variable_bindings
condition = KBS::Condition.new(
:temperature,
location: :loc?,
value: :temp?
)
expected = { :loc? => :location, :temp? => :value }
assert_equal expected, condition.variable_bindings
end
def test_no_variables
condition = KBS::Condition.new(:temperature, location: "server_room")
assert_empty condition.variable_bindings
end
end
See Also¶
- Engine API - Adding facts to engines
- Rules API - Using conditions in rules
- Pattern Matching Guide - Detailed pattern semantics
- Variable Binding Guide - Join tests and bindings
- DSL Guide - Declarative condition syntax