kbs 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/deploy-github-pages.yml +52 -0
- data/CHANGELOG.md +68 -2
- data/README.md +235 -334
- data/docs/DOCUMENTATION_STATUS.md +158 -0
- data/docs/advanced/custom-persistence.md +775 -0
- data/docs/advanced/debugging.md +726 -0
- data/docs/advanced/index.md +8 -0
- data/docs/advanced/performance.md +832 -0
- data/docs/advanced/testing.md +691 -0
- data/docs/api/blackboard.md +1157 -0
- data/docs/api/engine.md +978 -0
- data/docs/api/facts.md +1212 -0
- data/docs/api/index.md +12 -0
- data/docs/api/rules.md +1034 -0
- data/docs/architecture/blackboard.md +553 -0
- data/docs/architecture/index.md +277 -0
- data/docs/architecture/network-structure.md +343 -0
- data/docs/architecture/rete-algorithm.md +737 -0
- data/docs/assets/css/custom.css +83 -0
- data/docs/assets/images/blackboard-architecture.svg +136 -0
- data/docs/assets/images/compiled-network.svg +101 -0
- data/docs/assets/images/fact-assertion-flow.svg +117 -0
- data/docs/assets/images/kbs.jpg +0 -0
- data/docs/assets/images/pattern-matching-trace.svg +136 -0
- data/docs/assets/images/rete-network-layers.svg +96 -0
- data/docs/assets/images/system-layers.svg +69 -0
- data/docs/assets/images/trading-signal-network.svg +139 -0
- data/docs/assets/js/mathjax.js +17 -0
- data/docs/examples/expert-systems.md +1031 -0
- data/docs/examples/index.md +9 -0
- data/docs/examples/multi-agent.md +1335 -0
- data/docs/examples/stock-trading.md +488 -0
- data/docs/guides/blackboard-memory.md +558 -0
- data/docs/guides/dsl.md +1321 -0
- data/docs/guides/facts.md +652 -0
- data/docs/guides/getting-started.md +383 -0
- data/docs/guides/index.md +23 -0
- data/docs/guides/negation.md +529 -0
- data/docs/guides/pattern-matching.md +561 -0
- data/docs/guides/persistence.md +451 -0
- data/docs/guides/variable-binding.md +491 -0
- data/docs/guides/writing-rules.md +755 -0
- data/docs/index.md +157 -0
- data/docs/installation.md +156 -0
- data/docs/quick-start.md +228 -0
- data/examples/README.md +2 -2
- data/examples/advanced_example.rb +2 -2
- data/examples/advanced_example_dsl.rb +224 -0
- data/examples/ai_enhanced_kbs.rb +1 -1
- data/examples/ai_enhanced_kbs_dsl.rb +538 -0
- data/examples/blackboard_demo_dsl.rb +50 -0
- data/examples/car_diagnostic.rb +1 -1
- data/examples/car_diagnostic_dsl.rb +54 -0
- data/examples/concurrent_inference_demo.rb +5 -5
- data/examples/concurrent_inference_demo_dsl.rb +363 -0
- data/examples/csv_trading_system.rb +1 -1
- data/examples/csv_trading_system_dsl.rb +525 -0
- data/examples/knowledge_base.db +0 -0
- data/examples/portfolio_rebalancing_system.rb +2 -2
- data/examples/portfolio_rebalancing_system_dsl.rb +613 -0
- data/examples/redis_trading_demo_dsl.rb +177 -0
- data/examples/run_all.rb +50 -0
- data/examples/run_all_dsl.rb +49 -0
- data/examples/stock_trading_advanced.rb +1 -1
- data/examples/stock_trading_advanced_dsl.rb +404 -0
- data/examples/temp.txt +7693 -0
- data/examples/temp_dsl.txt +8447 -0
- data/examples/timestamped_trading.rb +1 -1
- data/examples/timestamped_trading_dsl.rb +258 -0
- data/examples/trading_demo.rb +1 -1
- data/examples/trading_demo_dsl.rb +322 -0
- data/examples/working_demo.rb +1 -1
- data/examples/working_demo_dsl.rb +160 -0
- data/lib/kbs/blackboard/engine.rb +3 -3
- data/lib/kbs/blackboard/fact.rb +1 -1
- data/lib/kbs/condition.rb +1 -1
- data/lib/kbs/dsl/knowledge_base.rb +1 -1
- data/lib/kbs/dsl/variable.rb +1 -1
- data/lib/kbs/{rete_engine.rb → engine.rb} +1 -1
- data/lib/kbs/fact.rb +1 -1
- data/lib/kbs/version.rb +1 -1
- data/lib/kbs.rb +2 -2
- data/mkdocs.yml +181 -0
- metadata +66 -6
- data/examples/stock_trading_system.rb.bak +0 -563
data/docs/api/facts.md
ADDED
|
@@ -0,0 +1,1212 @@
|
|
|
1
|
+
# Facts API Reference
|
|
2
|
+
|
|
3
|
+
Complete API reference for fact and condition classes in KBS.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [KBS::Fact](#kbsfact) - Transient in-memory fact
|
|
8
|
+
- [KBS::Blackboard::Fact](#kbsblackboardfact) - Persistent fact with UUID
|
|
9
|
+
- [KBS::Condition](#kbscondition) - Pattern matching condition
|
|
10
|
+
- [Fact Patterns](#fact-patterns)
|
|
11
|
+
- [Pattern Matching Semantics](#pattern-matching-semantics)
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## KBS::Fact
|
|
16
|
+
|
|
17
|
+
Transient in-memory fact used by the core RETE engine.
|
|
18
|
+
|
|
19
|
+
### Constructor
|
|
20
|
+
|
|
21
|
+
#### `initialize(type, attributes = {})`
|
|
22
|
+
|
|
23
|
+
Creates a new transient fact.
|
|
24
|
+
|
|
25
|
+
**Parameters**:
|
|
26
|
+
- `type` (Symbol) - Fact type (e.g., `:temperature`, `:order`)
|
|
27
|
+
- `attributes` (Hash, optional) - Fact attributes (default: `{}`)
|
|
28
|
+
|
|
29
|
+
**Returns**: `KBS::Fact` instance
|
|
30
|
+
|
|
31
|
+
**Example**:
|
|
32
|
+
```ruby
|
|
33
|
+
# Fact with attributes
|
|
34
|
+
fact = KBS::Fact.new(:temperature, location: "server_room", value: 85)
|
|
35
|
+
|
|
36
|
+
# Fact without attributes (marker/flag)
|
|
37
|
+
flag = KBS::Fact.new(:system_ready)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**Internal Behavior**:
|
|
41
|
+
- `@id` is set to `object_id` (unique Ruby object identifier)
|
|
42
|
+
- `@type` stores the fact type
|
|
43
|
+
- `@attributes` stores the attribute hash
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
### Public Attributes
|
|
48
|
+
|
|
49
|
+
#### `id`
|
|
50
|
+
|
|
51
|
+
**Type**: `Integer`
|
|
52
|
+
|
|
53
|
+
**Read-only**: Yes (via `attr_reader`)
|
|
54
|
+
|
|
55
|
+
**Description**: Unique identifier (Ruby object ID)
|
|
56
|
+
|
|
57
|
+
**Example**:
|
|
58
|
+
```ruby
|
|
59
|
+
fact = KBS::Fact.new(:temperature, value: 85)
|
|
60
|
+
puts fact.id # => 70123456789012 (varies)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**Note**: Not stable across Ruby processes. For persistent IDs, use `KBS::Blackboard::Fact` with UUIDs.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
#### `type`
|
|
68
|
+
|
|
69
|
+
**Type**: `Symbol`
|
|
70
|
+
|
|
71
|
+
**Read-only**: Yes (via `attr_reader`)
|
|
72
|
+
|
|
73
|
+
**Description**: The fact type
|
|
74
|
+
|
|
75
|
+
**Example**:
|
|
76
|
+
```ruby
|
|
77
|
+
fact = KBS::Fact.new(:temperature, value: 85)
|
|
78
|
+
puts fact.type # => :temperature
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
#### `attributes`
|
|
84
|
+
|
|
85
|
+
**Type**: `Hash`
|
|
86
|
+
|
|
87
|
+
**Read-only**: Yes (via `attr_reader`)
|
|
88
|
+
|
|
89
|
+
**Description**: The fact's attribute hash
|
|
90
|
+
|
|
91
|
+
**Example**:
|
|
92
|
+
```ruby
|
|
93
|
+
fact = KBS::Fact.new(:temperature, location: "server_room", value: 85)
|
|
94
|
+
puts fact.attributes # => {:location=>"server_room", :value=>85}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**Important**: Direct modification bypasses change tracking:
|
|
98
|
+
```ruby
|
|
99
|
+
# Don't do this (changes not tracked)
|
|
100
|
+
fact.attributes[:value] = 90
|
|
101
|
+
|
|
102
|
+
# Instead use []= accessor
|
|
103
|
+
fact[:value] = 90
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
### Public Methods
|
|
109
|
+
|
|
110
|
+
#### `[](key)`
|
|
111
|
+
|
|
112
|
+
Retrieves an attribute value.
|
|
113
|
+
|
|
114
|
+
**Parameters**:
|
|
115
|
+
- `key` (Symbol) - Attribute key
|
|
116
|
+
|
|
117
|
+
**Returns**: Attribute value or `nil` if not present
|
|
118
|
+
|
|
119
|
+
**Example**:
|
|
120
|
+
```ruby
|
|
121
|
+
fact = KBS::Fact.new(:temperature, location: "server_room", value: 85)
|
|
122
|
+
puts fact[:location] # => "server_room"
|
|
123
|
+
puts fact[:value] # => 85
|
|
124
|
+
puts fact[:missing] # => nil
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
#### `[]=(key, value)`
|
|
130
|
+
|
|
131
|
+
Sets an attribute value.
|
|
132
|
+
|
|
133
|
+
**Parameters**:
|
|
134
|
+
- `key` (Symbol) - Attribute key
|
|
135
|
+
- `value` - Attribute value
|
|
136
|
+
|
|
137
|
+
**Returns**: The value
|
|
138
|
+
|
|
139
|
+
**Side Effects**: Modifies the fact's attributes hash
|
|
140
|
+
|
|
141
|
+
**Example**:
|
|
142
|
+
```ruby
|
|
143
|
+
fact = KBS::Fact.new(:temperature, value: 85)
|
|
144
|
+
fact[:value] = 90
|
|
145
|
+
fact[:timestamp] = Time.now
|
|
146
|
+
|
|
147
|
+
puts fact.attributes # => {:value=>90, :timestamp=>2025-01-15 10:30:00}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**Important for KBS::Fact**: Changes are NOT persisted and do NOT trigger re-evaluation. For tracked updates, use `KBS::Blackboard::Fact`.
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
#### `matches?(pattern)`
|
|
155
|
+
|
|
156
|
+
Checks if this fact matches a pattern.
|
|
157
|
+
|
|
158
|
+
**Parameters**:
|
|
159
|
+
- `pattern` (Hash) - Pattern hash with `:type` and attribute constraints
|
|
160
|
+
|
|
161
|
+
**Returns**: `true` if matches, `false` otherwise
|
|
162
|
+
|
|
163
|
+
**Pattern Types**:
|
|
164
|
+
1. **Type constraint**: `pattern[:type]` must equal fact type
|
|
165
|
+
2. **Literal values**: Attribute must equal specified value
|
|
166
|
+
3. **Predicate lambdas**: `value.is_a?(Proc)` - attribute passed to lambda, must return truthy
|
|
167
|
+
4. **Variable bindings**: `value.is_a?(Symbol) && value.to_s.start_with?('?')` - always matches (variable captures value)
|
|
168
|
+
|
|
169
|
+
**Example - Literal Matching**:
|
|
170
|
+
```ruby
|
|
171
|
+
fact = KBS::Fact.new(:temperature, location: "server_room", value: 85)
|
|
172
|
+
|
|
173
|
+
# Type only
|
|
174
|
+
fact.matches?(type: :temperature) # => true
|
|
175
|
+
fact.matches?(type: :pressure) # => false
|
|
176
|
+
|
|
177
|
+
# Type + literal attribute
|
|
178
|
+
fact.matches?(type: :temperature, location: "server_room") # => true
|
|
179
|
+
fact.matches?(type: :temperature, location: "lobby") # => false
|
|
180
|
+
|
|
181
|
+
# Multiple literals
|
|
182
|
+
fact.matches?(type: :temperature, location: "server_room", value: 85) # => true
|
|
183
|
+
fact.matches?(type: :temperature, location: "server_room", value: 90) # => false
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
**Example - Predicate Matching**:
|
|
187
|
+
```ruby
|
|
188
|
+
fact = KBS::Fact.new(:temperature, value: 85)
|
|
189
|
+
|
|
190
|
+
# Lambda predicate
|
|
191
|
+
fact.matches?(type: :temperature, value: ->(v) { v > 80 }) # => true
|
|
192
|
+
fact.matches?(type: :temperature, value: ->(v) { v > 100 }) # => false
|
|
193
|
+
|
|
194
|
+
# Complex predicate
|
|
195
|
+
fact.matches?(
|
|
196
|
+
type: :temperature,
|
|
197
|
+
value: ->(v) { v >= 70 && v <= 90 }
|
|
198
|
+
) # => true
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
**Example - Variable Binding**:
|
|
202
|
+
```ruby
|
|
203
|
+
fact = KBS::Fact.new(:temperature, location: "server_room", value: 85)
|
|
204
|
+
|
|
205
|
+
# Variables always match (they capture the value)
|
|
206
|
+
fact.matches?(type: :temperature, location: :loc?) # => true
|
|
207
|
+
fact.matches?(type: :temperature, value: :temp?) # => true
|
|
208
|
+
|
|
209
|
+
# Variables with other constraints
|
|
210
|
+
fact.matches?(
|
|
211
|
+
type: :temperature,
|
|
212
|
+
location: "server_room", # Literal constraint
|
|
213
|
+
value: :temp? # Variable binding
|
|
214
|
+
) # => true
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
**Example - Missing Attributes**:
|
|
218
|
+
```ruby
|
|
219
|
+
fact = KBS::Fact.new(:temperature, value: 85) # No :location attribute
|
|
220
|
+
|
|
221
|
+
# Missing attributes fail predicate/literal checks
|
|
222
|
+
fact.matches?(type: :temperature, location: "server_room") # => false
|
|
223
|
+
fact.matches?(type: :temperature, location: ->(l) { l.length > 5 }) # => false (no :location)
|
|
224
|
+
|
|
225
|
+
# Missing attributes match variables
|
|
226
|
+
fact.matches?(type: :temperature, location: :loc?) # => true (variable matches nil)
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
**Algorithm**:
|
|
230
|
+
1. If `pattern[:type]` present and doesn't match fact type → return `false`
|
|
231
|
+
2. For each key in pattern (except `:type`):
|
|
232
|
+
- If value is Proc: call with fact attribute value, return `false` if falsy or attribute missing
|
|
233
|
+
- If value is variable (symbol starting with `?`): skip (always matches)
|
|
234
|
+
- Otherwise: return `false` if fact attribute ≠ pattern value
|
|
235
|
+
3. Return `true` if all checks passed
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
#### `to_s`
|
|
240
|
+
|
|
241
|
+
Returns string representation of fact.
|
|
242
|
+
|
|
243
|
+
**Parameters**: None
|
|
244
|
+
|
|
245
|
+
**Returns**: `String` in format `"type(attr1: val1, attr2: val2)"`
|
|
246
|
+
|
|
247
|
+
**Example**:
|
|
248
|
+
```ruby
|
|
249
|
+
fact = KBS::Fact.new(:temperature, location: "server_room", value: 85)
|
|
250
|
+
puts fact.to_s # => "temperature(location: server_room, value: 85)"
|
|
251
|
+
|
|
252
|
+
flag = KBS::Fact.new(:system_ready)
|
|
253
|
+
puts flag.to_s # => "system_ready()"
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
## KBS::Blackboard::Fact
|
|
259
|
+
|
|
260
|
+
Persistent fact with UUID, used by blackboard memory.
|
|
261
|
+
|
|
262
|
+
**Inherits**: None (separate implementation from `KBS::Fact`)
|
|
263
|
+
|
|
264
|
+
**Key Differences from KBS::Fact**:
|
|
265
|
+
- Has UUID instead of object ID
|
|
266
|
+
- `[]=` and `update()` trigger persistence and audit logging
|
|
267
|
+
- `retract()` method to remove from blackboard
|
|
268
|
+
- Reference to blackboard memory for update tracking
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
### Constructor
|
|
273
|
+
|
|
274
|
+
#### `initialize(uuid, type, attributes, blackboard = nil)`
|
|
275
|
+
|
|
276
|
+
Creates a persistent fact. Usually created via `engine.add_fact()`, not directly.
|
|
277
|
+
|
|
278
|
+
**Parameters**:
|
|
279
|
+
- `uuid` (String) - Unique identifier (UUID format)
|
|
280
|
+
- `type` (Symbol) - Fact type
|
|
281
|
+
- `attributes` (Hash) - Fact attributes
|
|
282
|
+
- `blackboard` (KBS::Blackboard::Memory, optional) - Reference to blackboard (default: `nil`)
|
|
283
|
+
|
|
284
|
+
**Returns**: `KBS::Blackboard::Fact` instance
|
|
285
|
+
|
|
286
|
+
**Example - Direct Construction** (rare):
|
|
287
|
+
```ruby
|
|
288
|
+
require 'securerandom'
|
|
289
|
+
|
|
290
|
+
fact = KBS::Blackboard::Fact.new(
|
|
291
|
+
SecureRandom.uuid,
|
|
292
|
+
:temperature,
|
|
293
|
+
{ location: "server_room", value: 85 }
|
|
294
|
+
)
|
|
295
|
+
puts fact.uuid # => "550e8400-e29b-41d4-a716-446655440000"
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
**Example - Typical Usage**:
|
|
299
|
+
```ruby
|
|
300
|
+
engine = KBS::Blackboard::Engine.new(db_path: 'kb.db')
|
|
301
|
+
fact = engine.add_fact(:temperature, location: "server_room", value: 85)
|
|
302
|
+
# Returns KBS::Blackboard::Fact with UUID and blackboard reference
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
### Public Attributes
|
|
308
|
+
|
|
309
|
+
#### `uuid`
|
|
310
|
+
|
|
311
|
+
**Type**: `String`
|
|
312
|
+
|
|
313
|
+
**Read-only**: Yes (via `attr_reader`)
|
|
314
|
+
|
|
315
|
+
**Description**: Globally unique identifier (UUID format)
|
|
316
|
+
|
|
317
|
+
**Example**:
|
|
318
|
+
```ruby
|
|
319
|
+
fact = engine.add_fact(:temperature, value: 85)
|
|
320
|
+
puts fact.uuid # => "550e8400-e29b-41d4-a716-446655440000"
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
**Use Cases**:
|
|
324
|
+
- Stable ID across restarts
|
|
325
|
+
- Foreign keys in external systems
|
|
326
|
+
- Audit trail references
|
|
327
|
+
|
|
328
|
+
---
|
|
329
|
+
|
|
330
|
+
#### `type`
|
|
331
|
+
|
|
332
|
+
**Type**: `Symbol`
|
|
333
|
+
|
|
334
|
+
**Read-only**: Yes (via `attr_reader`)
|
|
335
|
+
|
|
336
|
+
**Description**: The fact type
|
|
337
|
+
|
|
338
|
+
**Example**:
|
|
339
|
+
```ruby
|
|
340
|
+
fact = engine.add_fact(:temperature, value: 85)
|
|
341
|
+
puts fact.type # => :temperature
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
---
|
|
345
|
+
|
|
346
|
+
#### `attributes`
|
|
347
|
+
|
|
348
|
+
**Type**: `Hash`
|
|
349
|
+
|
|
350
|
+
**Read-only**: Yes (via `attr_reader`)
|
|
351
|
+
|
|
352
|
+
**Description**: The fact's attribute hash
|
|
353
|
+
|
|
354
|
+
**Example**:
|
|
355
|
+
```ruby
|
|
356
|
+
fact = engine.add_fact(:temperature, location: "server_room", value: 85)
|
|
357
|
+
puts fact.attributes # => {:location=>"server_room", :value=>85}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
**Important**: Direct modification bypasses persistence:
|
|
361
|
+
```ruby
|
|
362
|
+
# Don't do this (not persisted)
|
|
363
|
+
fact.attributes[:value] = 90
|
|
364
|
+
|
|
365
|
+
# Instead use []= or update()
|
|
366
|
+
fact[:value] = 90
|
|
367
|
+
# or
|
|
368
|
+
fact.update(value: 90)
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
---
|
|
372
|
+
|
|
373
|
+
### Public Methods
|
|
374
|
+
|
|
375
|
+
#### `[](key)`
|
|
376
|
+
|
|
377
|
+
Retrieves an attribute value.
|
|
378
|
+
|
|
379
|
+
**Parameters**:
|
|
380
|
+
- `key` (Symbol) - Attribute key
|
|
381
|
+
|
|
382
|
+
**Returns**: Attribute value or `nil` if not present
|
|
383
|
+
|
|
384
|
+
**Example**:
|
|
385
|
+
```ruby
|
|
386
|
+
fact = engine.add_fact(:temperature, location: "server_room", value: 85)
|
|
387
|
+
puts fact[:location] # => "server_room"
|
|
388
|
+
puts fact[:value] # => 85
|
|
389
|
+
puts fact[:missing] # => nil
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
---
|
|
393
|
+
|
|
394
|
+
#### `[]=(key, value)`
|
|
395
|
+
|
|
396
|
+
Sets an attribute value with persistence.
|
|
397
|
+
|
|
398
|
+
**Parameters**:
|
|
399
|
+
- `key` (Symbol) - Attribute key
|
|
400
|
+
- `value` - Attribute value (must be JSON-serializable for most stores)
|
|
401
|
+
|
|
402
|
+
**Returns**: The value
|
|
403
|
+
|
|
404
|
+
**Side Effects**:
|
|
405
|
+
- Updates fact's attributes hash
|
|
406
|
+
- Calls `blackboard.update_fact(self, @attributes)` if blackboard present
|
|
407
|
+
- Persists change to store
|
|
408
|
+
- Logs to audit trail
|
|
409
|
+
- Notifies observers
|
|
410
|
+
|
|
411
|
+
**Example**:
|
|
412
|
+
```ruby
|
|
413
|
+
fact = engine.add_fact(:temperature, value: 85)
|
|
414
|
+
fact[:value] = 90 # Immediately persisted
|
|
415
|
+
|
|
416
|
+
# After restart
|
|
417
|
+
engine2 = KBS::Blackboard::Engine.new(db_path: 'kb.db')
|
|
418
|
+
reloaded = engine2.blackboard.get_facts_by_type(:temperature).first
|
|
419
|
+
puts reloaded[:value] # => 90
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
**Important**: Updates do NOT trigger rule re-evaluation. To trigger rules, retract and re-add:
|
|
423
|
+
```ruby
|
|
424
|
+
old_fact = fact
|
|
425
|
+
fact.retract
|
|
426
|
+
new_fact = engine.add_fact(:temperature, value: 90)
|
|
427
|
+
engine.run
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
---
|
|
431
|
+
|
|
432
|
+
#### `update(new_attributes)`
|
|
433
|
+
|
|
434
|
+
Bulk update multiple attributes with persistence.
|
|
435
|
+
|
|
436
|
+
**Parameters**:
|
|
437
|
+
- `new_attributes` (Hash) - Hash of attributes to merge
|
|
438
|
+
|
|
439
|
+
**Returns**: `nil`
|
|
440
|
+
|
|
441
|
+
**Side Effects**:
|
|
442
|
+
- Merges `new_attributes` into `@attributes`
|
|
443
|
+
- Persists changes
|
|
444
|
+
- Logs to audit trail
|
|
445
|
+
- Notifies observers
|
|
446
|
+
|
|
447
|
+
**Example**:
|
|
448
|
+
```ruby
|
|
449
|
+
fact = engine.add_fact(:temperature, location: "server_room", value: 85)
|
|
450
|
+
|
|
451
|
+
fact.update(value: 90, timestamp: Time.now)
|
|
452
|
+
|
|
453
|
+
puts fact.attributes
|
|
454
|
+
# => {:location=>"server_room", :value=>90, :timestamp=>2025-01-15 10:30:00}
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
**Difference from `[]=`**: Updates multiple attributes in single persistence operation (more efficient).
|
|
458
|
+
|
|
459
|
+
---
|
|
460
|
+
|
|
461
|
+
#### `retract()`
|
|
462
|
+
|
|
463
|
+
Removes this fact from the blackboard.
|
|
464
|
+
|
|
465
|
+
**Parameters**: None
|
|
466
|
+
|
|
467
|
+
**Returns**: `nil`
|
|
468
|
+
|
|
469
|
+
**Side Effects**:
|
|
470
|
+
- Calls `blackboard.remove_fact(self)` if blackboard present
|
|
471
|
+
- Marks fact as inactive in store
|
|
472
|
+
- Logs retraction to audit trail
|
|
473
|
+
- Deactivates in alpha memories
|
|
474
|
+
- Notifies observers
|
|
475
|
+
|
|
476
|
+
**Example**:
|
|
477
|
+
```ruby
|
|
478
|
+
fact = engine.add_fact(:temperature, value: 85)
|
|
479
|
+
fact.retract # Fact removed
|
|
480
|
+
|
|
481
|
+
# Equivalent to:
|
|
482
|
+
engine.remove_fact(fact)
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
**Use Case**: Fact self-destruction in rule actions:
|
|
486
|
+
```ruby
|
|
487
|
+
rule "auto_expire_old_alerts" do
|
|
488
|
+
on :alert, timestamp: ->(ts) { Time.now - ts > 3600 }
|
|
489
|
+
perform do |bindings|
|
|
490
|
+
# Fact can remove itself
|
|
491
|
+
alert_fact = bindings[:matched_fact?]
|
|
492
|
+
alert_fact.retract
|
|
493
|
+
end
|
|
494
|
+
end
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
---
|
|
498
|
+
|
|
499
|
+
#### `matches?(pattern)`
|
|
500
|
+
|
|
501
|
+
Checks if this fact matches a pattern. Same semantics as `KBS::Fact#matches?`.
|
|
502
|
+
|
|
503
|
+
**Parameters**:
|
|
504
|
+
- `pattern` (Hash) - Pattern hash with `:type` and attribute constraints
|
|
505
|
+
|
|
506
|
+
**Returns**: `true` if matches, `false` otherwise
|
|
507
|
+
|
|
508
|
+
**Example**:
|
|
509
|
+
```ruby
|
|
510
|
+
fact = engine.add_fact(:temperature, location: "server_room", value: 85)
|
|
511
|
+
|
|
512
|
+
fact.matches?(type: :temperature) # => true
|
|
513
|
+
fact.matches?(type: :temperature, value: ->(v) { v > 80 }) # => true
|
|
514
|
+
fact.matches?(type: :pressure) # => false
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
See [`KBS::Fact#matches?`](#matchespattern) for detailed semantics.
|
|
518
|
+
|
|
519
|
+
---
|
|
520
|
+
|
|
521
|
+
#### `to_s`
|
|
522
|
+
|
|
523
|
+
Returns string representation with UUID prefix.
|
|
524
|
+
|
|
525
|
+
**Parameters**: None
|
|
526
|
+
|
|
527
|
+
**Returns**: `String` in format `"type(uuid_prefix...: attr1=val1, attr2=val2)"`
|
|
528
|
+
|
|
529
|
+
**Example**:
|
|
530
|
+
```ruby
|
|
531
|
+
fact = engine.add_fact(:temperature, location: "server_room", value: 85)
|
|
532
|
+
puts fact.to_s
|
|
533
|
+
# => "temperature(550e8400...: location=server_room, value=85)"
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
**Note**: Only first 8 characters of UUID shown for brevity.
|
|
537
|
+
|
|
538
|
+
---
|
|
539
|
+
|
|
540
|
+
#### `to_h`
|
|
541
|
+
|
|
542
|
+
Returns hash representation of fact.
|
|
543
|
+
|
|
544
|
+
**Parameters**: None
|
|
545
|
+
|
|
546
|
+
**Returns**: `Hash` with keys `:uuid`, `:type`, `:attributes`
|
|
547
|
+
|
|
548
|
+
**Example**:
|
|
549
|
+
```ruby
|
|
550
|
+
fact = engine.add_fact(:temperature, location: "server_room", value: 85)
|
|
551
|
+
hash = fact.to_h
|
|
552
|
+
|
|
553
|
+
puts hash
|
|
554
|
+
# => {
|
|
555
|
+
# :uuid => "550e8400-e29b-41d4-a716-446655440000",
|
|
556
|
+
# :type => :temperature,
|
|
557
|
+
# :attributes => {:location=>"server_room", :value=>85}
|
|
558
|
+
# }
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
**Use Cases**:
|
|
562
|
+
- Serialization for APIs
|
|
563
|
+
- Logging
|
|
564
|
+
- Testing assertions
|
|
565
|
+
|
|
566
|
+
---
|
|
567
|
+
|
|
568
|
+
## KBS::Condition
|
|
569
|
+
|
|
570
|
+
Pattern matching condition used in rule definitions.
|
|
571
|
+
|
|
572
|
+
### Constructor
|
|
573
|
+
|
|
574
|
+
#### `initialize(type, pattern = {}, negated: false)`
|
|
575
|
+
|
|
576
|
+
Creates a condition that matches facts.
|
|
577
|
+
|
|
578
|
+
**Parameters**:
|
|
579
|
+
- `type` (Symbol) - Fact type to match
|
|
580
|
+
- `pattern` (Hash, optional) - Attribute constraints (default: `{}`)
|
|
581
|
+
- `negated` (Boolean, optional) - If `true`, condition matches when pattern is absent (default: `false`)
|
|
582
|
+
|
|
583
|
+
**Returns**: `KBS::Condition` instance
|
|
584
|
+
|
|
585
|
+
**Example - Positive Condition**:
|
|
586
|
+
```ruby
|
|
587
|
+
# Match any temperature fact
|
|
588
|
+
condition = KBS::Condition.new(:temperature)
|
|
589
|
+
|
|
590
|
+
# Match temperature facts with location="server_room"
|
|
591
|
+
condition = KBS::Condition.new(:temperature, location: "server_room")
|
|
592
|
+
|
|
593
|
+
# Match temperature facts with value > 80
|
|
594
|
+
condition = KBS::Condition.new(:temperature, value: ->(v) { v > 80 })
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
**Example - Negated Condition**:
|
|
598
|
+
```ruby
|
|
599
|
+
# Match when there is NO alert fact
|
|
600
|
+
condition = KBS::Condition.new(:alert, {}, negated: true)
|
|
601
|
+
|
|
602
|
+
# Match when there is NO critical alert
|
|
603
|
+
condition = KBS::Condition.new(:alert, { level: "critical" }, negated: true)
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
**Example - Variable Binding**:
|
|
607
|
+
```ruby
|
|
608
|
+
# Capture temperature value in :temp? variable
|
|
609
|
+
condition = KBS::Condition.new(:temperature, value: :temp?)
|
|
610
|
+
|
|
611
|
+
# Capture location and value
|
|
612
|
+
condition = KBS::Condition.new(
|
|
613
|
+
:temperature,
|
|
614
|
+
location: :loc?,
|
|
615
|
+
value: :temp?
|
|
616
|
+
)
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
---
|
|
620
|
+
|
|
621
|
+
### Public Attributes
|
|
622
|
+
|
|
623
|
+
#### `type`
|
|
624
|
+
|
|
625
|
+
**Type**: `Symbol`
|
|
626
|
+
|
|
627
|
+
**Read-only**: Yes (via `attr_reader`)
|
|
628
|
+
|
|
629
|
+
**Description**: The fact type this condition matches
|
|
630
|
+
|
|
631
|
+
**Example**:
|
|
632
|
+
```ruby
|
|
633
|
+
condition = KBS::Condition.new(:temperature, value: :temp?)
|
|
634
|
+
puts condition.type # => :temperature
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
---
|
|
638
|
+
|
|
639
|
+
#### `pattern`
|
|
640
|
+
|
|
641
|
+
**Type**: `Hash`
|
|
642
|
+
|
|
643
|
+
**Read-only**: Yes (via `attr_reader`)
|
|
644
|
+
|
|
645
|
+
**Description**: The attribute pattern to match
|
|
646
|
+
|
|
647
|
+
**Example**:
|
|
648
|
+
```ruby
|
|
649
|
+
condition = KBS::Condition.new(:temperature, location: "server_room", value: :temp?)
|
|
650
|
+
puts condition.pattern # => {:location=>"server_room", :value=>:temp?}
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
---
|
|
654
|
+
|
|
655
|
+
#### `negated`
|
|
656
|
+
|
|
657
|
+
**Type**: `Boolean`
|
|
658
|
+
|
|
659
|
+
**Read-only**: Yes (via `attr_reader`)
|
|
660
|
+
|
|
661
|
+
**Description**: Whether this is a negation condition
|
|
662
|
+
|
|
663
|
+
**Example**:
|
|
664
|
+
```ruby
|
|
665
|
+
pos_condition = KBS::Condition.new(:temperature, value: :temp?)
|
|
666
|
+
puts pos_condition.negated # => false
|
|
667
|
+
|
|
668
|
+
neg_condition = KBS::Condition.new(:alert, {}, negated: true)
|
|
669
|
+
puts neg_condition.negated # => true
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
---
|
|
673
|
+
|
|
674
|
+
#### `variable_bindings`
|
|
675
|
+
|
|
676
|
+
**Type**: `Hash<Symbol, Symbol>`
|
|
677
|
+
|
|
678
|
+
**Read-only**: Yes (via `attr_reader`)
|
|
679
|
+
|
|
680
|
+
**Description**: Map of variable names to attribute keys (e.g., `{:temp? => :value}`)
|
|
681
|
+
|
|
682
|
+
**Example**:
|
|
683
|
+
```ruby
|
|
684
|
+
condition = KBS::Condition.new(
|
|
685
|
+
:temperature,
|
|
686
|
+
location: :loc?,
|
|
687
|
+
value: :temp?
|
|
688
|
+
)
|
|
689
|
+
|
|
690
|
+
puts condition.variable_bindings
|
|
691
|
+
# => {:loc?=>:location, :temp?=>:value}
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
**Use Case**: RETE engine uses this to extract bindings when condition matches:
|
|
695
|
+
```ruby
|
|
696
|
+
fact = KBS::Fact.new(:temperature, location: "server_room", value: 85)
|
|
697
|
+
bindings = {}
|
|
698
|
+
|
|
699
|
+
condition.variable_bindings.each do |var, attr|
|
|
700
|
+
bindings[var] = fact[attr]
|
|
701
|
+
end
|
|
702
|
+
|
|
703
|
+
puts bindings # => {:loc?=>"server_room", :temp?=>85}
|
|
704
|
+
```
|
|
705
|
+
|
|
706
|
+
---
|
|
707
|
+
|
|
708
|
+
## Fact Patterns
|
|
709
|
+
|
|
710
|
+
Patterns are hashes used to match facts. They appear in:
|
|
711
|
+
- `Condition.new(type, pattern)`
|
|
712
|
+
- `Fact#matches?(pattern)`
|
|
713
|
+
- Alpha memory keys
|
|
714
|
+
|
|
715
|
+
### Pattern Structure
|
|
716
|
+
|
|
717
|
+
```ruby
|
|
718
|
+
{
|
|
719
|
+
type: :fact_type, # Optional: fact type constraint
|
|
720
|
+
attribute1: literal_value, # Literal constraint
|
|
721
|
+
attribute2: :variable?, # Variable binding
|
|
722
|
+
attribute3: ->(v) { ... } # Predicate lambda
|
|
723
|
+
}
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
### Pattern Types
|
|
727
|
+
|
|
728
|
+
#### 1. Empty Pattern
|
|
729
|
+
|
|
730
|
+
Matches all facts of a type.
|
|
731
|
+
|
|
732
|
+
```ruby
|
|
733
|
+
condition = KBS::Condition.new(:temperature)
|
|
734
|
+
# Matches ANY temperature fact
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
#### 2. Literal Pattern
|
|
738
|
+
|
|
739
|
+
Matches facts with exact attribute values.
|
|
740
|
+
|
|
741
|
+
```ruby
|
|
742
|
+
condition = KBS::Condition.new(
|
|
743
|
+
:temperature,
|
|
744
|
+
location: "server_room",
|
|
745
|
+
sensor_id: 42
|
|
746
|
+
)
|
|
747
|
+
|
|
748
|
+
# Matches:
|
|
749
|
+
KBS::Fact.new(:temperature, location: "server_room", sensor_id: 42, value: 85)
|
|
750
|
+
|
|
751
|
+
# Doesn't match:
|
|
752
|
+
KBS::Fact.new(:temperature, location: "lobby", sensor_id: 42)
|
|
753
|
+
KBS::Fact.new(:temperature, location: "server_room", sensor_id: 99)
|
|
754
|
+
```
|
|
755
|
+
|
|
756
|
+
#### 3. Predicate Pattern
|
|
757
|
+
|
|
758
|
+
Matches facts where attribute satisfies lambda.
|
|
759
|
+
|
|
760
|
+
```ruby
|
|
761
|
+
condition = KBS::Condition.new(
|
|
762
|
+
:temperature,
|
|
763
|
+
value: ->(v) { v > 80 && v < 100 },
|
|
764
|
+
location: ->(l) { l.start_with?("server") }
|
|
765
|
+
)
|
|
766
|
+
|
|
767
|
+
# Matches:
|
|
768
|
+
KBS::Fact.new(:temperature, location: "server_room", value: 85)
|
|
769
|
+
KBS::Fact.new(:temperature, location: "server_1", value: 90)
|
|
770
|
+
|
|
771
|
+
# Doesn't match:
|
|
772
|
+
KBS::Fact.new(:temperature, location: "server_room", value: 110) # value > 100
|
|
773
|
+
KBS::Fact.new(:temperature, location: "lobby", value: 85) # location doesn't start with "server"
|
|
774
|
+
```
|
|
775
|
+
|
|
776
|
+
**Important**: Predicate fails if attribute is missing:
|
|
777
|
+
```ruby
|
|
778
|
+
fact = KBS::Fact.new(:temperature, location: "server_room") # No :value
|
|
779
|
+
fact.matches?(type: :temperature, value: ->(v) { v > 0 }) # => false (no :value attribute)
|
|
780
|
+
```
|
|
781
|
+
|
|
782
|
+
#### 4. Variable Binding Pattern
|
|
783
|
+
|
|
784
|
+
Variables (symbols starting with `?`) capture attribute values for use in join tests and action blocks.
|
|
785
|
+
|
|
786
|
+
```ruby
|
|
787
|
+
condition = KBS::Condition.new(
|
|
788
|
+
:temperature,
|
|
789
|
+
location: :loc?,
|
|
790
|
+
value: :temp?
|
|
791
|
+
)
|
|
792
|
+
|
|
793
|
+
# Matches ANY temperature fact, binding :loc? and :temp?
|
|
794
|
+
fact = KBS::Fact.new(:temperature, location: "server_room", value: 85)
|
|
795
|
+
# Bindings: {:loc? => "server_room", :temp? => 85}
|
|
796
|
+
```
|
|
797
|
+
|
|
798
|
+
#### 5. Mixed Pattern
|
|
799
|
+
|
|
800
|
+
Combine literals, predicates, and variables.
|
|
801
|
+
|
|
802
|
+
```ruby
|
|
803
|
+
condition = KBS::Condition.new(
|
|
804
|
+
:temperature,
|
|
805
|
+
location: "server_room", # Literal
|
|
806
|
+
value: :temp?, # Variable
|
|
807
|
+
timestamp: ->(ts) { ts > cutoff_time } # Predicate
|
|
808
|
+
)
|
|
809
|
+
|
|
810
|
+
# Only matches temperature facts from server_room with recent timestamp
|
|
811
|
+
# Captures value in :temp? variable
|
|
812
|
+
```
|
|
813
|
+
|
|
814
|
+
---
|
|
815
|
+
|
|
816
|
+
## Pattern Matching Semantics
|
|
817
|
+
|
|
818
|
+
### Matching Algorithm
|
|
819
|
+
|
|
820
|
+
For `fact.matches?(pattern)`:
|
|
821
|
+
|
|
822
|
+
1. **Type Check**: If `pattern[:type]` present, must equal `fact.type`
|
|
823
|
+
2. **Attribute Checks**: For each `key, value` in pattern (except `:type`):
|
|
824
|
+
- **Variable** (`value.is_a?(Symbol) && value.to_s.start_with?('?')`): Always matches (captures `fact[key]`)
|
|
825
|
+
- **Predicate** (`value.is_a?(Proc)`): Must satisfy `value.call(fact[key])`. **Fails if `fact[key]` is nil.**
|
|
826
|
+
- **Literal**: Must equal `fact[key]`
|
|
827
|
+
3. **Result**: `true` if all checks pass, `false` otherwise
|
|
828
|
+
|
|
829
|
+
### Open World Assumption
|
|
830
|
+
|
|
831
|
+
Facts are not required to have all attributes in the pattern. Patterns only constrain attributes they specify.
|
|
832
|
+
|
|
833
|
+
```ruby
|
|
834
|
+
fact = KBS::Fact.new(:temperature, location: "server_room", value: 85, timestamp: Time.now)
|
|
835
|
+
|
|
836
|
+
# Matches - pattern doesn't mention :timestamp
|
|
837
|
+
fact.matches?(type: :temperature, location: "server_room") # => true
|
|
838
|
+
|
|
839
|
+
# Matches - pattern only constrains :value
|
|
840
|
+
fact.matches?(type: :temperature, value: ->(v) { v > 80 }) # => true
|
|
841
|
+
```
|
|
842
|
+
|
|
843
|
+
**But**: If pattern specifies an attribute the fact lacks, match fails:
|
|
844
|
+
|
|
845
|
+
```ruby
|
|
846
|
+
fact = KBS::Fact.new(:temperature, value: 85) # No :location
|
|
847
|
+
|
|
848
|
+
# Fails - fact missing :location attribute
|
|
849
|
+
fact.matches?(type: :temperature, location: "server_room") # => false
|
|
850
|
+
|
|
851
|
+
# Fails - predicate can't evaluate nil
|
|
852
|
+
fact.matches?(type: :temperature, location: ->(l) { l.length > 5 }) # => false
|
|
853
|
+
|
|
854
|
+
# Succeeds - variable matches nil
|
|
855
|
+
fact.matches?(type: :temperature, location: :loc?) # => true (binds :loc? => nil)
|
|
856
|
+
```
|
|
857
|
+
|
|
858
|
+
### Variable Binding Extraction
|
|
859
|
+
|
|
860
|
+
Variables are extracted during condition construction:
|
|
861
|
+
|
|
862
|
+
```ruby
|
|
863
|
+
condition = KBS::Condition.new(
|
|
864
|
+
:order,
|
|
865
|
+
symbol: :sym?,
|
|
866
|
+
quantity: :qty?,
|
|
867
|
+
price: :price?
|
|
868
|
+
)
|
|
869
|
+
|
|
870
|
+
puts condition.variable_bindings
|
|
871
|
+
# => {:sym?=>:symbol, :qty?=>:quantity, :price?=>:price}
|
|
872
|
+
```
|
|
873
|
+
|
|
874
|
+
When a fact matches, bindings are populated:
|
|
875
|
+
|
|
876
|
+
```ruby
|
|
877
|
+
fact = KBS::Fact.new(:order, symbol: "AAPL", quantity: 100, price: 150.25)
|
|
878
|
+
|
|
879
|
+
bindings = {}
|
|
880
|
+
condition.variable_bindings.each do |var, attr|
|
|
881
|
+
bindings[var] = fact[attr]
|
|
882
|
+
end
|
|
883
|
+
|
|
884
|
+
puts bindings
|
|
885
|
+
# => {:sym?=>"AAPL", :qty?=>100, :price?=>150.25}
|
|
886
|
+
```
|
|
887
|
+
|
|
888
|
+
### Predicate Constraints
|
|
889
|
+
|
|
890
|
+
Predicates are powerful but have caveats:
|
|
891
|
+
|
|
892
|
+
**1. Nil Attributes Fail**:
|
|
893
|
+
```ruby
|
|
894
|
+
fact = KBS::Fact.new(:temperature, location: "server_room") # No :value
|
|
895
|
+
|
|
896
|
+
# Predicate fails - can't call lambda on nil
|
|
897
|
+
fact.matches?(type: :temperature, value: ->(v) { v > 0 }) # => false
|
|
898
|
+
```
|
|
899
|
+
|
|
900
|
+
**2. Predicates Run on Every Match Attempt**:
|
|
901
|
+
```ruby
|
|
902
|
+
# This predicate runs every time a fact is checked
|
|
903
|
+
expensive_check = ->(v) { complex_calculation(v) }
|
|
904
|
+
condition = KBS::Condition.new(:temperature, value: expensive_check)
|
|
905
|
+
|
|
906
|
+
# For 1000 temperature facts, expensive_check runs 1000 times
|
|
907
|
+
```
|
|
908
|
+
|
|
909
|
+
**3. Predicates Should Be Pure Functions**:
|
|
910
|
+
```ruby
|
|
911
|
+
# Bad - side effects
|
|
912
|
+
counter = 0
|
|
913
|
+
condition = KBS::Condition.new(:temperature, value: ->(v) { counter += 1; v > 80 })
|
|
914
|
+
|
|
915
|
+
# Good - pure predicate
|
|
916
|
+
condition = KBS::Condition.new(:temperature, value: ->(v) { v > 80 })
|
|
917
|
+
```
|
|
918
|
+
|
|
919
|
+
**4. Predicates Can't Access Other Attributes**:
|
|
920
|
+
```ruby
|
|
921
|
+
# This doesn't work - predicate only receives attribute value
|
|
922
|
+
condition = KBS::Condition.new(
|
|
923
|
+
:temperature,
|
|
924
|
+
value: ->(v) { v > @threshold } # @threshold from where?
|
|
925
|
+
)
|
|
926
|
+
|
|
927
|
+
# Use closures to capture context
|
|
928
|
+
threshold = 80
|
|
929
|
+
condition = KBS::Condition.new(
|
|
930
|
+
:temperature,
|
|
931
|
+
value: ->(v) { v > threshold } # Closure captures threshold
|
|
932
|
+
)
|
|
933
|
+
```
|
|
934
|
+
|
|
935
|
+
### Negation Semantics
|
|
936
|
+
|
|
937
|
+
Negated conditions match when NO fact satisfies the pattern:
|
|
938
|
+
|
|
939
|
+
```ruby
|
|
940
|
+
# Rule fires when there's NO critical alert
|
|
941
|
+
rule "all_clear" do
|
|
942
|
+
negated :alert, level: "critical" # negated: true
|
|
943
|
+
perform { puts "All systems normal" }
|
|
944
|
+
end
|
|
945
|
+
```
|
|
946
|
+
|
|
947
|
+
**Important**: Negation matches absence, not presence of opposite:
|
|
948
|
+
|
|
949
|
+
```ruby
|
|
950
|
+
# Matches when NO alert with level="critical" exists
|
|
951
|
+
negated :alert, level: "critical"
|
|
952
|
+
|
|
953
|
+
# NOT equivalent to: Match when alert with level != "critical" exists
|
|
954
|
+
# To match non-critical alerts, use predicate:
|
|
955
|
+
on :alert, level: ->(l) { l != "critical" }
|
|
956
|
+
```
|
|
957
|
+
|
|
958
|
+
See [Negation Guide](../guides/negation.md) for detailed semantics.
|
|
959
|
+
|
|
960
|
+
---
|
|
961
|
+
|
|
962
|
+
## Common Patterns
|
|
963
|
+
|
|
964
|
+
### 1. Range Checks
|
|
965
|
+
|
|
966
|
+
```ruby
|
|
967
|
+
# Between 70 and 90
|
|
968
|
+
condition = KBS::Condition.new(
|
|
969
|
+
:temperature,
|
|
970
|
+
value: ->(v) { v >= 70 && v <= 90 }
|
|
971
|
+
)
|
|
972
|
+
|
|
973
|
+
# Outside range
|
|
974
|
+
condition = KBS::Condition.new(
|
|
975
|
+
:temperature,
|
|
976
|
+
value: ->(v) { v < 70 || v > 90 }
|
|
977
|
+
)
|
|
978
|
+
```
|
|
979
|
+
|
|
980
|
+
### 2. String Matching
|
|
981
|
+
|
|
982
|
+
```ruby
|
|
983
|
+
# Starts with
|
|
984
|
+
condition = KBS::Condition.new(
|
|
985
|
+
:sensor,
|
|
986
|
+
name: ->(n) { n.start_with?("temp_") }
|
|
987
|
+
)
|
|
988
|
+
|
|
989
|
+
# Regex match
|
|
990
|
+
condition = KBS::Condition.new(
|
|
991
|
+
:sensor,
|
|
992
|
+
name: ->(n) { n =~ /^sensor_\d+$/ }
|
|
993
|
+
)
|
|
994
|
+
|
|
995
|
+
# Contains substring
|
|
996
|
+
condition = KBS::Condition.new(
|
|
997
|
+
:log_entry,
|
|
998
|
+
message: ->(m) { m.include?("ERROR") }
|
|
999
|
+
)
|
|
1000
|
+
```
|
|
1001
|
+
|
|
1002
|
+
### 3. Collection Membership
|
|
1003
|
+
|
|
1004
|
+
```ruby
|
|
1005
|
+
# One of several values
|
|
1006
|
+
valid_statuses = ["pending", "processing", "completed"]
|
|
1007
|
+
condition = KBS::Condition.new(
|
|
1008
|
+
:order,
|
|
1009
|
+
status: ->(s) { valid_statuses.include?(s) }
|
|
1010
|
+
)
|
|
1011
|
+
|
|
1012
|
+
# Not in collection
|
|
1013
|
+
invalid_statuses = ["cancelled", "failed"]
|
|
1014
|
+
condition = KBS::Condition.new(
|
|
1015
|
+
:order,
|
|
1016
|
+
status: ->(s) { !invalid_statuses.include?(s) }
|
|
1017
|
+
)
|
|
1018
|
+
```
|
|
1019
|
+
|
|
1020
|
+
### 4. Timestamp Checks
|
|
1021
|
+
|
|
1022
|
+
```ruby
|
|
1023
|
+
# Recent facts (last hour)
|
|
1024
|
+
cutoff = Time.now - 3600
|
|
1025
|
+
condition = KBS::Condition.new(
|
|
1026
|
+
:temperature,
|
|
1027
|
+
timestamp: ->(ts) { ts > cutoff }
|
|
1028
|
+
)
|
|
1029
|
+
|
|
1030
|
+
# Old facts (older than 1 day)
|
|
1031
|
+
cutoff = Time.now - 86400
|
|
1032
|
+
condition = KBS::Condition.new(
|
|
1033
|
+
:temperature,
|
|
1034
|
+
timestamp: ->(ts) { ts < cutoff }
|
|
1035
|
+
)
|
|
1036
|
+
```
|
|
1037
|
+
|
|
1038
|
+
### 5. Cross-Attribute Constraints (Using Multiple Conditions)
|
|
1039
|
+
|
|
1040
|
+
You can't directly compare two attributes of the same fact in one condition. Use multiple conditions:
|
|
1041
|
+
|
|
1042
|
+
```ruby
|
|
1043
|
+
# Want: Match orders where quantity * price > 10000
|
|
1044
|
+
# Can't do this in one condition:
|
|
1045
|
+
# condition = KBS::Condition.new(:order, ...) # No way to access both :quantity and :price
|
|
1046
|
+
|
|
1047
|
+
# Instead: Capture variables and check in action or use join test
|
|
1048
|
+
rule "large_order" do
|
|
1049
|
+
on :order, quantity: :qty?, price: :price?
|
|
1050
|
+
perform do |bindings|
|
|
1051
|
+
total = bindings[:qty?] * bindings[:price?]
|
|
1052
|
+
if total > 10000
|
|
1053
|
+
puts "Large order: $#{total}"
|
|
1054
|
+
end
|
|
1055
|
+
end
|
|
1056
|
+
end
|
|
1057
|
+
```
|
|
1058
|
+
|
|
1059
|
+
### 6. Null/Nil Checks
|
|
1060
|
+
|
|
1061
|
+
Variables capture `nil`, predicates fail on `nil`:
|
|
1062
|
+
|
|
1063
|
+
```ruby
|
|
1064
|
+
# Match facts with ANY value for :location (including nil)
|
|
1065
|
+
condition = KBS::Condition.new(:temperature, location: :loc?)
|
|
1066
|
+
# Matches fact.new(:temperature, location: nil) → binds :loc? => nil
|
|
1067
|
+
# Matches fact.new(:temperature) → binds :loc? => nil
|
|
1068
|
+
|
|
1069
|
+
# Match facts with NON-NIL :location
|
|
1070
|
+
condition = KBS::Condition.new(
|
|
1071
|
+
:temperature,
|
|
1072
|
+
location: ->(l) { !l.nil? }
|
|
1073
|
+
)
|
|
1074
|
+
# Fails fact.new(:temperature, location: nil)
|
|
1075
|
+
# Fails fact.new(:temperature) (no :location attribute)
|
|
1076
|
+
```
|
|
1077
|
+
|
|
1078
|
+
---
|
|
1079
|
+
|
|
1080
|
+
## Performance Tips
|
|
1081
|
+
|
|
1082
|
+
### 1. Order Predicates by Selectivity
|
|
1083
|
+
|
|
1084
|
+
```ruby
|
|
1085
|
+
# Good - Most selective predicate first
|
|
1086
|
+
condition = KBS::Condition.new(
|
|
1087
|
+
:temperature,
|
|
1088
|
+
sensor_id: 42, # Likely filters to 1 fact
|
|
1089
|
+
value: ->(v) { v > 80 } # Then check value
|
|
1090
|
+
)
|
|
1091
|
+
|
|
1092
|
+
# Less optimal - Expensive check first
|
|
1093
|
+
condition = KBS::Condition.new(
|
|
1094
|
+
:temperature,
|
|
1095
|
+
value: ->(v) { expensive_calculation(v) }, # Runs on many facts
|
|
1096
|
+
sensor_id: 42 # Could have filtered first
|
|
1097
|
+
)
|
|
1098
|
+
```
|
|
1099
|
+
|
|
1100
|
+
**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.
|
|
1101
|
+
|
|
1102
|
+
### 2. Avoid Expensive Predicates
|
|
1103
|
+
|
|
1104
|
+
```ruby
|
|
1105
|
+
# Bad - Complex regex on every fact
|
|
1106
|
+
condition = KBS::Condition.new(
|
|
1107
|
+
:log_entry,
|
|
1108
|
+
message: ->(m) { m =~ /very.*complex.*regex.*pattern/ }
|
|
1109
|
+
)
|
|
1110
|
+
|
|
1111
|
+
# Better - Simple check first, complex check in action
|
|
1112
|
+
rule "complex_log_analysis" do
|
|
1113
|
+
on :log_entry, level: "ERROR", message: :msg? # Simple literal filter
|
|
1114
|
+
perform do |bindings|
|
|
1115
|
+
if bindings[:msg?] =~ /very.*complex.*regex.*pattern/
|
|
1116
|
+
# Expensive check runs only on ERROR logs
|
|
1117
|
+
end
|
|
1118
|
+
end
|
|
1119
|
+
end
|
|
1120
|
+
```
|
|
1121
|
+
|
|
1122
|
+
### 3. Use Literals When Possible
|
|
1123
|
+
|
|
1124
|
+
Literals are fastest (hash equality check). Predicates are slower (lambda call).
|
|
1125
|
+
|
|
1126
|
+
```ruby
|
|
1127
|
+
# Fast
|
|
1128
|
+
condition = KBS::Condition.new(:temperature, location: "server_room")
|
|
1129
|
+
|
|
1130
|
+
# Slower (but necessary for ranges/complex checks)
|
|
1131
|
+
condition = KBS::Condition.new(:temperature, value: ->(v) { v > 80 })
|
|
1132
|
+
```
|
|
1133
|
+
|
|
1134
|
+
---
|
|
1135
|
+
|
|
1136
|
+
## Testing Patterns
|
|
1137
|
+
|
|
1138
|
+
### Testing Fact Matching
|
|
1139
|
+
|
|
1140
|
+
```ruby
|
|
1141
|
+
require 'minitest/autorun'
|
|
1142
|
+
|
|
1143
|
+
class TestFactMatching < Minitest::Test
|
|
1144
|
+
def test_literal_match
|
|
1145
|
+
fact = KBS::Fact.new(:temperature, location: "server_room", value: 85)
|
|
1146
|
+
|
|
1147
|
+
assert fact.matches?(type: :temperature)
|
|
1148
|
+
assert fact.matches?(type: :temperature, location: "server_room")
|
|
1149
|
+
refute fact.matches?(type: :temperature, location: "lobby")
|
|
1150
|
+
end
|
|
1151
|
+
|
|
1152
|
+
def test_predicate_match
|
|
1153
|
+
fact = KBS::Fact.new(:temperature, value: 85)
|
|
1154
|
+
|
|
1155
|
+
assert fact.matches?(type: :temperature, value: ->(v) { v > 80 })
|
|
1156
|
+
refute fact.matches?(type: :temperature, value: ->(v) { v > 100 })
|
|
1157
|
+
end
|
|
1158
|
+
|
|
1159
|
+
def test_variable_binding
|
|
1160
|
+
fact = KBS::Fact.new(:temperature, location: "server_room", value: 85)
|
|
1161
|
+
|
|
1162
|
+
# Variables always match
|
|
1163
|
+
assert fact.matches?(type: :temperature, location: :loc?, value: :temp?)
|
|
1164
|
+
end
|
|
1165
|
+
|
|
1166
|
+
def test_missing_attribute
|
|
1167
|
+
fact = KBS::Fact.new(:temperature, value: 85) # No :location
|
|
1168
|
+
|
|
1169
|
+
# Literal fails on missing
|
|
1170
|
+
refute fact.matches?(type: :temperature, location: "server_room")
|
|
1171
|
+
|
|
1172
|
+
# Predicate fails on missing
|
|
1173
|
+
refute fact.matches?(type: :temperature, location: ->(l) { l.length > 0 })
|
|
1174
|
+
|
|
1175
|
+
# Variable succeeds on missing (binds to nil)
|
|
1176
|
+
assert fact.matches?(type: :temperature, location: :loc?)
|
|
1177
|
+
end
|
|
1178
|
+
end
|
|
1179
|
+
```
|
|
1180
|
+
|
|
1181
|
+
### Testing Variable Extraction
|
|
1182
|
+
|
|
1183
|
+
```ruby
|
|
1184
|
+
class TestVariableExtraction < Minitest::Test
|
|
1185
|
+
def test_variable_bindings
|
|
1186
|
+
condition = KBS::Condition.new(
|
|
1187
|
+
:temperature,
|
|
1188
|
+
location: :loc?,
|
|
1189
|
+
value: :temp?
|
|
1190
|
+
)
|
|
1191
|
+
|
|
1192
|
+
expected = { :loc? => :location, :temp? => :value }
|
|
1193
|
+
assert_equal expected, condition.variable_bindings
|
|
1194
|
+
end
|
|
1195
|
+
|
|
1196
|
+
def test_no_variables
|
|
1197
|
+
condition = KBS::Condition.new(:temperature, location: "server_room")
|
|
1198
|
+
|
|
1199
|
+
assert_empty condition.variable_bindings
|
|
1200
|
+
end
|
|
1201
|
+
end
|
|
1202
|
+
```
|
|
1203
|
+
|
|
1204
|
+
---
|
|
1205
|
+
|
|
1206
|
+
## See Also
|
|
1207
|
+
|
|
1208
|
+
- [Engine API](engine.md) - Adding facts to engines
|
|
1209
|
+
- [Rules API](rules.md) - Using conditions in rules
|
|
1210
|
+
- [Pattern Matching Guide](../guides/pattern-matching.md) - Detailed pattern semantics
|
|
1211
|
+
- [Variable Binding Guide](../guides/variable-binding.md) - Join tests and bindings
|
|
1212
|
+
- [DSL Guide](../guides/dsl.md) - Declarative condition syntax
|