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.
Files changed (86) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/deploy-github-pages.yml +52 -0
  3. data/CHANGELOG.md +68 -2
  4. data/README.md +235 -334
  5. data/docs/DOCUMENTATION_STATUS.md +158 -0
  6. data/docs/advanced/custom-persistence.md +775 -0
  7. data/docs/advanced/debugging.md +726 -0
  8. data/docs/advanced/index.md +8 -0
  9. data/docs/advanced/performance.md +832 -0
  10. data/docs/advanced/testing.md +691 -0
  11. data/docs/api/blackboard.md +1157 -0
  12. data/docs/api/engine.md +978 -0
  13. data/docs/api/facts.md +1212 -0
  14. data/docs/api/index.md +12 -0
  15. data/docs/api/rules.md +1034 -0
  16. data/docs/architecture/blackboard.md +553 -0
  17. data/docs/architecture/index.md +277 -0
  18. data/docs/architecture/network-structure.md +343 -0
  19. data/docs/architecture/rete-algorithm.md +737 -0
  20. data/docs/assets/css/custom.css +83 -0
  21. data/docs/assets/images/blackboard-architecture.svg +136 -0
  22. data/docs/assets/images/compiled-network.svg +101 -0
  23. data/docs/assets/images/fact-assertion-flow.svg +117 -0
  24. data/docs/assets/images/kbs.jpg +0 -0
  25. data/docs/assets/images/pattern-matching-trace.svg +136 -0
  26. data/docs/assets/images/rete-network-layers.svg +96 -0
  27. data/docs/assets/images/system-layers.svg +69 -0
  28. data/docs/assets/images/trading-signal-network.svg +139 -0
  29. data/docs/assets/js/mathjax.js +17 -0
  30. data/docs/examples/expert-systems.md +1031 -0
  31. data/docs/examples/index.md +9 -0
  32. data/docs/examples/multi-agent.md +1335 -0
  33. data/docs/examples/stock-trading.md +488 -0
  34. data/docs/guides/blackboard-memory.md +558 -0
  35. data/docs/guides/dsl.md +1321 -0
  36. data/docs/guides/facts.md +652 -0
  37. data/docs/guides/getting-started.md +383 -0
  38. data/docs/guides/index.md +23 -0
  39. data/docs/guides/negation.md +529 -0
  40. data/docs/guides/pattern-matching.md +561 -0
  41. data/docs/guides/persistence.md +451 -0
  42. data/docs/guides/variable-binding.md +491 -0
  43. data/docs/guides/writing-rules.md +755 -0
  44. data/docs/index.md +157 -0
  45. data/docs/installation.md +156 -0
  46. data/docs/quick-start.md +228 -0
  47. data/examples/README.md +2 -2
  48. data/examples/advanced_example.rb +2 -2
  49. data/examples/advanced_example_dsl.rb +224 -0
  50. data/examples/ai_enhanced_kbs.rb +1 -1
  51. data/examples/ai_enhanced_kbs_dsl.rb +538 -0
  52. data/examples/blackboard_demo_dsl.rb +50 -0
  53. data/examples/car_diagnostic.rb +1 -1
  54. data/examples/car_diagnostic_dsl.rb +54 -0
  55. data/examples/concurrent_inference_demo.rb +5 -5
  56. data/examples/concurrent_inference_demo_dsl.rb +363 -0
  57. data/examples/csv_trading_system.rb +1 -1
  58. data/examples/csv_trading_system_dsl.rb +525 -0
  59. data/examples/knowledge_base.db +0 -0
  60. data/examples/portfolio_rebalancing_system.rb +2 -2
  61. data/examples/portfolio_rebalancing_system_dsl.rb +613 -0
  62. data/examples/redis_trading_demo_dsl.rb +177 -0
  63. data/examples/run_all.rb +50 -0
  64. data/examples/run_all_dsl.rb +49 -0
  65. data/examples/stock_trading_advanced.rb +1 -1
  66. data/examples/stock_trading_advanced_dsl.rb +404 -0
  67. data/examples/temp.txt +7693 -0
  68. data/examples/temp_dsl.txt +8447 -0
  69. data/examples/timestamped_trading.rb +1 -1
  70. data/examples/timestamped_trading_dsl.rb +258 -0
  71. data/examples/trading_demo.rb +1 -1
  72. data/examples/trading_demo_dsl.rb +322 -0
  73. data/examples/working_demo.rb +1 -1
  74. data/examples/working_demo_dsl.rb +160 -0
  75. data/lib/kbs/blackboard/engine.rb +3 -3
  76. data/lib/kbs/blackboard/fact.rb +1 -1
  77. data/lib/kbs/condition.rb +1 -1
  78. data/lib/kbs/dsl/knowledge_base.rb +1 -1
  79. data/lib/kbs/dsl/variable.rb +1 -1
  80. data/lib/kbs/{rete_engine.rb → engine.rb} +1 -1
  81. data/lib/kbs/fact.rb +1 -1
  82. data/lib/kbs/version.rb +1 -1
  83. data/lib/kbs.rb +2 -2
  84. data/mkdocs.yml +181 -0
  85. metadata +66 -6
  86. 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