Skip to content

Negation

Negated conditions match when a pattern is absent from working memory. This guide explains negation semantics, use cases, performance implications, and common pitfalls.

Negation Basics

Syntax

KBS::Condition.new(:alert, { sensor_id: :id? }, negated: true)

Semantics: Condition satisfied when NO fact matches the pattern.

Simple Example

KBS::Rule.new("send_first_alert") do |r|
  r.conditions = [
    # Positive: High temperature detected
    KBS::Condition.new(:high_temp, { sensor_id: :id? }),

    # Negative: No alert sent yet
    KBS::Condition.new(:alert_sent, { sensor_id: :id? }, negated: true)
  ]

  r.action = lambda do |facts, bindings|
    send_alert(bindings[:id?])
    engine.add_fact(:alert_sent, { sensor_id: bindings[:id?] })
  end
end

Behavior: - First run: :high_temp exists, :alert_sent doesn't → rule fires - Second run: Both :high_temp and :alert_sent exist → rule doesn't fire

Negation Semantics

Open World Assumption

Negation means "no matching fact exists", not "fact is explicitly false":

# Negated condition
KBS::Condition.new(:error, { id: :id? }, negated: true)

# Matches when:
# - No :error fact exists with that id
# - Working memory is empty
# - :error facts exist but with different ids

# Does NOT match when:
# - Any :error fact with matching id exists

Variable Binding in Negation

Variables in negated conditions still create join constraints:

r.conditions = [
  KBS::Condition.new(:sensor, { id: :id?, temp: :temp? }),
  KBS::Condition.new(:alert, { sensor_id: :id? }, negated: true)
]

# For each sensor fact:
#   Check if NO alert exists with sensor_id == sensor's id
#   If no such alert: rule fires

Negation Node Behavior

1. Token arrives with bindings { :id? => "bedroom" }
2. Check alpha memory for :alert facts
3. Filter for matches where sensor_id == "bedroom"
4. If count == 0: propagate token (condition satisfied)
5. If count > 0: block token (condition not satisfied)

Use Cases

1. Guard Conditions

Prevent duplicate actions:

KBS::Rule.new("process_order") do |r|
  r.conditions = [
    KBS::Condition.new(:order, { id: :id?, status: "pending" }),
    KBS::Condition.new(:processing, { order_id: :id? }, negated: true)
  ]

  r.action = lambda do |facts, bindings|
    # Only process if not already processing
    engine.add_fact(:processing, { order_id: bindings[:id?] })
    process_order(bindings[:id?])
  end
end

2. Missing Information Detection

Alert when expected data is absent:

KBS::Rule.new("missing_threshold") do |r|
  r.conditions = [
    KBS::Condition.new(:sensor, { id: :id? }),
    KBS::Condition.new(:threshold, { sensor_id: :id? }, negated: true)
  ]

  r.action = lambda do |facts, bindings|
    alert("Sensor #{bindings[:id?]} has no threshold configured!")
  end
end

3. State Transitions

Ensure prerequisites before transitioning:

KBS::Rule.new("activate_account") do |r|
  r.conditions = [
    KBS::Condition.new(:user, { id: :id?, email_verified: true }),
    KBS::Condition.new(:account_active, { user_id: :id? }, negated: true)
  ]

  r.action = lambda do |facts, bindings|
    engine.add_fact(:account_active, { user_id: bindings[:id?] })
  end
end

4. Timeout Detection

Fire when response hasn't arrived:

KBS::Rule.new("timeout_alert") do |r|
  r.conditions = [
    KBS::Condition.new(:request, {
      id: :req_id?,
      created_at: :created?
    }, predicate: lambda { |f|
      (Time.now - f[:created_at]) > 300  # 5 minutes
    }),

    KBS::Condition.new(:response, { request_id: :req_id? }, negated: true)
  ]

  r.action = lambda do |facts, bindings|
    alert("Request #{bindings[:req_id?]} timed out!")
  end
end

5. Mutual Exclusion

Ensure only one option selected:

KBS::Rule.new("select_default") do |r|
  r.conditions = [
    KBS::Condition.new(:user, { id: :id? }),
    KBS::Condition.new(:preference, { user_id: :id?, theme: :theme? }, negated: true)
  ]

  r.action = lambda do |facts, bindings|
    # No preference set → use default
    engine.add_fact(:preference, { user_id: bindings[:id?], theme: "light" })
  end
end

Multiple Negations

Conjunction (AND)

All negations must be satisfied:

r.conditions = [
  KBS::Condition.new(:a, {}),
  KBS::Condition.new(:b, {}, negated: true),
  KBS::Condition.new(:c, {}, negated: true)
]

# Fires when: a exists AND b doesn't exist AND c doesn't exist

Complex Negation

KBS::Rule.new("unique_error") do |r|
  r.conditions = [
    KBS::Condition.new(:error, { type: :type? }),
    KBS::Condition.new(:error_handled, { type: :type? }, negated: true),
    KBS::Condition.new(:error_ignored, { type: :type? }, negated: true)
  ]

  r.action = lambda do |facts, bindings|
    # Error exists but neither handled nor ignored
    handle_new_error(bindings[:type?])
  end
end

Performance Implications

Negation is Expensive

Reason: Must check alpha memory on every token arrival.

# Expensive: Large alpha memory to search
KBS::Condition.new(:log_entry, {}, negated: true)
# Must check all log_entry facts for each token

# Better: Specific pattern
KBS::Condition.new(:error_log, { severity: "critical" }, negated: true)
# Smaller alpha memory, fewer checks

Negation Node Overhead

class NegationNode
  def left_activate(token)
    # For EVERY token:
    matching_facts = @alpha_memory.items.select { |fact|
      perform_join_tests(token, fact)
    }

    if matching_facts.empty?
      propagate(token)  # No matches = condition satisfied
    else
      block(token)      # Matches exist = condition not satisfied
      track_inhibitors(token, matching_facts)
    end
  end
end

Cost: O(A × T) where A = alpha memory size, T = join test cost

Optimization Strategies

1. Order negations last:

# Good: Positive conditions first
r.conditions = [
  KBS::Condition.new(:a, {}),
  KBS::Condition.new(:b, {}),
  KBS::Condition.new(:c, {}, negated: true)  # Last
]

# Bad: Negation first
r.conditions = [
  KBS::Condition.new(:c, {}, negated: true),  # First
  KBS::Condition.new(:a, {}),
  KBS::Condition.new(:b, {})
]

2. Minimize negations:

# Bad: Multiple negations
r.conditions = [
  KBS::Condition.new(:foo, {}, negated: true),
  KBS::Condition.new(:bar, {}, negated: true),
  KBS::Condition.new(:baz, {}, negated: true)
]

# Better: Single positive condition
# Add fact when conditions met:
unless foo_exists? || bar_exists? || baz_exists?
  engine.add_fact(:conditions_clear, {})
end

r.conditions = [
  KBS::Condition.new(:conditions_clear, {})
]

3. Use specific patterns:

# Expensive
KBS::Condition.new(:event, {}, negated: true)

# Cheaper
KBS::Condition.new(:event, { type: "error", severity: "critical" }, negated: true)

Common Pitfalls

1. Forgetting Variable Binding

# Bad: Variables don't connect
r.conditions = [
  KBS::Condition.new(:sensor, { id: :id1? }),
  KBS::Condition.new(:alert, { id: :id2? }, negated: true)  # Different variable!
]

# Good: Consistent variables
r.conditions = [
  KBS::Condition.new(:sensor, { id: :id? }),
  KBS::Condition.new(:alert, { sensor_id: :id? }, negated: true)  # Same :id?
]

2. Infinite Loops

# Bad: Rule fires forever
KBS::Rule.new("infinite_loop") do |r|
  r.conditions = [
    KBS::Condition.new(:start, {}),
    KBS::Condition.new(:done, {}, negated: true)
  ]

  r.action = lambda do |facts, bindings|
    # Never adds :done fact!
    do_something()
  end
end

# Good: Add termination fact
KBS::Rule.new("runs_once") do |r|
  r.conditions = [
    KBS::Condition.new(:start, {}),
    KBS::Condition.new(:done, {}, negated: true)
  ]

  r.action = lambda do |facts, bindings|
    do_something()
    engine.add_fact(:done, {})  # Prevents re-firing
  end
end

3. Negation of Missing Attributes

# Doesn't work as expected
KBS::Condition.new(:sensor, { error: nil }, negated: true)

# Better: Check for absence of error fact
KBS::Condition.new(:sensor, { id: :id? }),
KBS::Condition.new(:sensor_error, { sensor_id: :id? }, negated: true)

4. Over-Using Negation

# Bad: Many negations
KBS::Rule.new("many_negations") do |r|
  r.conditions = [
    KBS::Condition.new(:a, {}, negated: true),
    KBS::Condition.new(:b, {}, negated: true),
    KBS::Condition.new(:c, {}, negated: true),
    KBS::Condition.new(:d, {}, negated: true)
  ]
  # Expensive! Checks 4 alpha memories per token
end

# Good: Refactor to positive logic
# Add a single fact representing "all clear" state

Negation Patterns

Default Values

# If no preference, use default
KBS::Rule.new("set_default_theme") do |r|
  r.conditions = [
    KBS::Condition.new(:user, { id: :id? }),
    KBS::Condition.new(:theme_preference, { user_id: :id? }, negated: true)
  ]

  r.action = lambda do |facts, bindings|
    engine.add_fact(:theme_preference, { user_id: bindings[:id?], theme: "dark" })
  end
end

Cleanup Rules

# Remove orphaned records
KBS::Rule.new("cleanup_orphaned_comments") do |r|
  r.conditions = [
    KBS::Condition.new(:comment, { post_id: :pid? }),
    KBS::Condition.new(:post, { id: :pid? }, negated: true)
  ]

  r.action = lambda do |facts, bindings|
    comment = facts[0]
    engine.remove_fact(comment)
  end
end

Prerequisite Checking

# Ensure all prerequisites met
KBS::Rule.new("deploy_application") do |r|
  r.conditions = [
    KBS::Condition.new(:deploy_requested, {}),
    KBS::Condition.new(:tests_passed, {}),
    KBS::Condition.new(:build_succeeded, {}),
    KBS::Condition.new(:deployment_blocked, {}, negated: true)
  ]

  r.action = lambda do |facts, bindings|
    deploy()
  end
end

Debugging Negations

Trace Negation Checks

class DebugNegationNode < KBS::NegationNode
  def left_activate(token)
    matches = @alpha_memory.items.select { |f| perform_join_tests(token, f) }
    puts "Negation check:"
    puts "  Pattern: #{@alpha_memory.pattern}"
    puts "  Token: #{token.inspect}"
    puts "  Matching facts: #{matches.size}"
    puts "  Result: #{matches.empty? ? 'PASS' : 'BLOCK'}"
    super
  end
end

Count Inhibitors

# Check how many facts are blocking tokens
engine.production_nodes.each do |name, node|
  # Find negation nodes in network
  # Count tokens blocked by each
end

Validate Negation Logic

# Test: Rule should fire when condition absent
engine.add_fact(:trigger, {})
engine.run
assert rule_fired, "Should fire when negated condition absent"

# Test: Rule should NOT fire when condition present
engine.add_fact(:blocker, {})
engine.run
refute rule_fired, "Should not fire when negated condition present"

Alternatives to Negation

Sometimes positive logic is clearer and faster:

Pattern: Explicit State

# Instead of:
KBS::Condition.new(:processing, { id: :id? }, negated: true)

# Use explicit state:
KBS::Condition.new(:status, { id: :id?, value: "idle" })

Pattern: Status Flags

# Instead of:
KBS::Condition.new(:error, {}, negated: true)

# Use status flag:
KBS::Condition.new(:system_status, { healthy: true })

Pattern: Computed Facts

# Instead of checking absence in rule:
KBS::Condition.new(:response, { req_id: :id? }, negated: true)

# Add a fact when timeout occurs:
KBS::Rule.new("detect_timeout") do |r|
  r.conditions = [
    KBS::Condition.new(:request, {
      id: :id?,
      created_at: :time?
    }, predicate: lambda { |f| (Time.now - f[:created_at]) > 300 })
  ]

  r.action = lambda do |facts, bindings|
    engine.add_fact(:timeout, { request_id: bindings[:id?] })
  end
end

# Then use positive check:
KBS::Condition.new(:timeout, { request_id: :id? })

Next Steps


Negation is powerful but expensive. Use sparingly and order last for best performance.