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
@@ -0,0 +1,652 @@
1
+ # Working with Facts
2
+
3
+ Facts are the fundamental units of knowledge in KBS. This guide covers the complete lifecycle of facts: creating, querying, updating, and removing them.
4
+
5
+ ## What is a Fact?
6
+
7
+ A fact represents an observation or piece of knowledge about your domain. Facts have:
8
+
9
+ - **Type** - A symbol categorizing the fact (e.g., `:stock`, `:sensor`, `:alert`)
10
+ - **Attributes** - Key-value pairs describing the fact (e.g., `{ symbol: "AAPL", price: 150 }`)
11
+ - **Identity** - Unique instance in working memory
12
+
13
+ **Example Facts:**
14
+
15
+ ```ruby
16
+ # Sensor reading
17
+ type: :sensor
18
+ attributes: { id: "bedroom", temp: 28, humidity: 65 }
19
+
20
+ # Stock quote
21
+ type: :stock
22
+ attributes: { symbol: "AAPL", price: 150.50, volume: 1000000 }
23
+
24
+ # Alert
25
+ type: :alert
26
+ attributes: { sensor_id: "bedroom", message: "High temperature" }
27
+ ```
28
+
29
+ ## Fact Types
30
+
31
+ KBS provides two fact implementations:
32
+
33
+ ### 1. Transient Facts (`KBS::Fact`)
34
+
35
+ In-memory facts that disappear when your program exits.
36
+
37
+ ```ruby
38
+ engine = KBS::Engine.new
39
+
40
+ # Add transient fact
41
+ fact = engine.add_fact(:stock, { symbol: "AAPL", price: 150 })
42
+
43
+ # Facts lost on restart
44
+ ```
45
+
46
+ **Use for:**
47
+ - Short-lived applications
48
+ - Prototyping
49
+ - Testing
50
+ - Pure computation (no persistence needed)
51
+
52
+ ### 2. Persistent Facts (`KBS::Blackboard::Fact`)
53
+
54
+ Database-backed facts with UUIDs that survive restarts.
55
+
56
+ ```ruby
57
+ engine = KBS::Blackboard::Engine.new(db_path: 'kb.db')
58
+
59
+ # Add persistent fact (saved to database)
60
+ fact = engine.add_fact(:stock, { symbol: "AAPL", price: 150 })
61
+ puts fact.id # => "550e8400-e29b-41d4-a716-446655440000"
62
+
63
+ # Facts reload on next run
64
+ engine.close
65
+
66
+ # Next run
67
+ engine = KBS::Blackboard::Engine.new(db_path: 'kb.db')
68
+ puts engine.facts.size # => 1 (fact persisted)
69
+ ```
70
+
71
+ **Use for:**
72
+ - Long-running systems
73
+ - Systems requiring restart
74
+ - Audit trails
75
+ - Multi-agent collaboration
76
+
77
+ **Both types share the same interface**, so code works identically:
78
+
79
+ ```ruby
80
+ fact.type # => :stock
81
+ fact[:symbol] # => "AAPL"
82
+ fact[:price] # => 150
83
+ fact.attributes # => { symbol: "AAPL", price: 150 }
84
+ ```
85
+
86
+ ## Creating Facts
87
+
88
+ ### Basic Creation
89
+
90
+ ```ruby
91
+ # Method 1: Via engine (recommended)
92
+ fact = engine.add_fact(:sensor, { id: "bedroom", temp: 28 })
93
+
94
+ # Method 2: Direct instantiation
95
+ fact = KBS::Fact.new(:sensor, { id: "bedroom", temp: 28 })
96
+ engine.add_fact(fact)
97
+ ```
98
+
99
+ **`add_fact` automatically:**
100
+ - Stores fact in working memory
101
+ - Triggers pattern matching in RETE network
102
+ - Notifies observers
103
+ - Persists to database (if using Blackboard::Engine)
104
+
105
+ ### With Type Conversion
106
+
107
+ Attributes are stored as-is:
108
+
109
+ ```ruby
110
+ engine.add_fact(:reading, {
111
+ value: 42, # Integer
112
+ timestamp: Time.now, # Time object
113
+ active: true, # Boolean
114
+ metadata: { foo: 1 } # Hash
115
+ })
116
+ ```
117
+
118
+ ### Bulk Creation
119
+
120
+ ```ruby
121
+ facts = [
122
+ [:stock, { symbol: "AAPL", price: 150 }],
123
+ [:stock, { symbol: "GOOGL", price: 2800 }],
124
+ [:stock, { symbol: "MSFT", price: 300 }]
125
+ ]
126
+
127
+ facts.each do |type, attrs|
128
+ engine.add_fact(type, attrs)
129
+ end
130
+ ```
131
+
132
+ ### From External Data
133
+
134
+ ```ruby
135
+ require 'json'
136
+
137
+ # Load from JSON
138
+ json_data = File.read('sensors.json')
139
+ sensor_data = JSON.parse(json_data, symbolize_names: true)
140
+
141
+ sensor_data.each do |reading|
142
+ engine.add_fact(:sensor, {
143
+ id: reading[:sensor_id],
144
+ temp: reading[:temperature],
145
+ humidity: reading[:humidity]
146
+ })
147
+ end
148
+ ```
149
+
150
+ ```ruby
151
+ require 'csv'
152
+
153
+ # Load from CSV
154
+ CSV.foreach('stocks.csv', headers: true) do |row|
155
+ engine.add_fact(:stock, {
156
+ symbol: row['symbol'],
157
+ price: row['price'].to_f,
158
+ volume: row['volume'].to_i
159
+ })
160
+ end
161
+ ```
162
+
163
+ ## Accessing Fact Attributes
164
+
165
+ ### Array-Style Access
166
+
167
+ ```ruby
168
+ fact = engine.add_fact(:sensor, { id: "bedroom", temp: 28 })
169
+
170
+ # Read attributes
171
+ fact[:id] # => "bedroom"
172
+ fact[:temp] # => 28
173
+ fact[:missing] # => nil
174
+ ```
175
+
176
+ ### Attributes Hash
177
+
178
+ ```ruby
179
+ fact.attributes
180
+ # => { id: "bedroom", temp: 28 }
181
+
182
+ # Iterate attributes
183
+ fact.attributes.each do |key, value|
184
+ puts "#{key}: #{value}"
185
+ end
186
+ ```
187
+
188
+ ### Type Access
189
+
190
+ ```ruby
191
+ fact.type # => :sensor
192
+ ```
193
+
194
+ ### Identity (Persistent Facts Only)
195
+
196
+ ```ruby
197
+ # Blackboard facts have UUIDs
198
+ fact.id # => "550e8400-e29b-41d4-a716-446655440000"
199
+
200
+ # Transient facts use object_id
201
+ fact.object_id # => 70123456789000
202
+ ```
203
+
204
+ ## Querying Facts
205
+
206
+ ### Get All Facts
207
+
208
+ ```ruby
209
+ all_facts = engine.facts
210
+ # => [#<Fact type=:sensor>, #<Fact type=:stock>, ...]
211
+ ```
212
+
213
+ ### Filter by Type
214
+
215
+ ```ruby
216
+ # Get all sensor facts
217
+ sensors = engine.facts.select { |f| f.type == :sensor }
218
+
219
+ # Get all stock facts
220
+ stocks = engine.facts.select { |f| f.type == :stock }
221
+ ```
222
+
223
+ ### Filter by Attribute
224
+
225
+ ```ruby
226
+ # Find facts with specific attribute value
227
+ high_temps = engine.facts.select { |f|
228
+ f.type == :sensor && f[:temp] && f[:temp] > 30
229
+ }
230
+
231
+ # Find by multiple criteria
232
+ aapl_stocks = engine.facts.select { |f|
233
+ f.type == :stock && f[:symbol] == "AAPL"
234
+ }
235
+ ```
236
+
237
+ ### Find Single Fact
238
+
239
+ ```ruby
240
+ # Find first matching fact
241
+ fact = engine.facts.find { |f|
242
+ f.type == :sensor && f[:id] == "bedroom"
243
+ }
244
+
245
+ # Or return nil if not found
246
+ fact = engine.facts.find { |f|
247
+ f.type == :alert && f[:severity] == "critical"
248
+ }
249
+ ```
250
+
251
+ ### Complex Queries
252
+
253
+ ```ruby
254
+ # Count facts
255
+ sensor_count = engine.facts.count { |f| f.type == :sensor }
256
+
257
+ # Group by type
258
+ facts_by_type = engine.facts.group_by(&:type)
259
+ # => { sensor: [...], stock: [...], alert: [...] }
260
+
261
+ # Map attributes
262
+ symbols = engine.facts
263
+ .select { |f| f.type == :stock }
264
+ .map { |f| f[:symbol] }
265
+ .uniq
266
+ # => ["AAPL", "GOOGL", "MSFT"]
267
+ ```
268
+
269
+ ### Query Helper Method
270
+
271
+ Create reusable query methods:
272
+
273
+ ```ruby
274
+ class QueryHelper
275
+ def initialize(engine)
276
+ @engine = engine
277
+ end
278
+
279
+ def facts_of_type(type)
280
+ @engine.facts.select { |f| f.type == type }
281
+ end
282
+
283
+ def facts_where(type, &block)
284
+ facts_of_type(type).select(&block)
285
+ end
286
+
287
+ def fact_where(type, &block)
288
+ facts_of_type(type).find(&block)
289
+ end
290
+ end
291
+
292
+ # Usage
293
+ helper = QueryHelper.new(engine)
294
+
295
+ # Get all high-temp sensors
296
+ high_temps = helper.facts_where(:sensor) { |f| f[:temp] > 30 }
297
+
298
+ # Get specific sensor
299
+ bedroom = helper.fact_where(:sensor) { |f| f[:id] == "bedroom" }
300
+ ```
301
+
302
+ ## Updating Facts
303
+
304
+ Facts are immutable in KBS. To "update" a fact, remove the old one and add a new one.
305
+
306
+ ### Update Pattern
307
+
308
+ ```ruby
309
+ # Find existing fact
310
+ old_fact = engine.facts.find { |f|
311
+ f.type == :sensor && f[:id] == "bedroom"
312
+ }
313
+
314
+ if old_fact
315
+ # Remove old fact
316
+ engine.remove_fact(old_fact)
317
+
318
+ # Add updated fact
319
+ engine.add_fact(:sensor, {
320
+ id: "bedroom",
321
+ temp: 30, # Updated temperature
322
+ humidity: 65
323
+ })
324
+
325
+ # Re-run matching
326
+ engine.run
327
+ end
328
+ ```
329
+
330
+ ### Update Helper
331
+
332
+ ```ruby
333
+ def update_fact(engine, type, matcher, new_attrs)
334
+ old_fact = engine.facts.find { |f|
335
+ f.type == type && matcher.call(f)
336
+ }
337
+
338
+ if old_fact
339
+ engine.remove_fact(old_fact)
340
+ engine.add_fact(type, new_attrs)
341
+ end
342
+ end
343
+
344
+ # Usage
345
+ update_fact(engine, :sensor, ->(f) { f[:id] == "bedroom" },
346
+ { id: "bedroom", temp: 30, humidity: 65 }
347
+ )
348
+ ```
349
+
350
+ ### Blackboard Update (Persistent Facts)
351
+
352
+ Blackboard facts support in-place updates:
353
+
354
+ ```ruby
355
+ engine = KBS::Blackboard::Engine.new(db_path: 'kb.db')
356
+
357
+ fact = engine.add_fact(:sensor, { id: "bedroom", temp: 28 })
358
+
359
+ # Update attributes (saves to database)
360
+ fact.update({ temp: 30 })
361
+
362
+ # Or update via engine
363
+ engine.update_fact(fact.id, { temp: 32 })
364
+ ```
365
+
366
+ ## Removing Facts
367
+
368
+ ### Remove Single Fact
369
+
370
+ ```ruby
371
+ # Find and remove
372
+ fact = engine.facts.find { |f| f.type == :alert }
373
+ engine.remove_fact(fact) if fact
374
+
375
+ # Re-run to propagate changes
376
+ engine.run
377
+ ```
378
+
379
+ ### Remove Multiple Facts
380
+
381
+ ```ruby
382
+ # Remove all alerts
383
+ alerts = engine.facts.select { |f| f.type == :alert }
384
+ alerts.each { |fact| engine.remove_fact(fact) }
385
+ engine.run
386
+ ```
387
+
388
+ ### Remove by Criteria
389
+
390
+ ```ruby
391
+ # Remove all stale sensor readings (older than 5 minutes)
392
+ stale = engine.facts.select { |f|
393
+ f.type == :sensor &&
394
+ f[:timestamp] &&
395
+ (Time.now - f[:timestamp]) > 300
396
+ }
397
+
398
+ stale.each { |fact| engine.remove_fact(fact) }
399
+ engine.run
400
+ ```
401
+
402
+ ### Clear All Facts
403
+
404
+ ```ruby
405
+ # Clear working memory
406
+ engine.facts.dup.each { |f| engine.remove_fact(f) }
407
+ engine.run
408
+ ```
409
+
410
+ **Note:** Use `.dup` to avoid modifying array while iterating.
411
+
412
+ ## Fact Lifecycle
413
+
414
+ ### Lifecycle Stages
415
+
416
+ ```
417
+ 1. Creation
418
+ ├─> engine.add_fact(:type, { ... })
419
+ └─> Fact instantiated
420
+
421
+ 2. Storage
422
+ ├─> Added to WorkingMemory
423
+ └─> Persisted (if Blackboard::Engine)
424
+
425
+ 3. Matching
426
+ ├─> Alpha network activation
427
+ ├─> Join network propagation
428
+ └─> Production node tokens created
429
+
430
+ 4. Rule Firing
431
+ ├─> engine.run()
432
+ └─> Actions execute with fact
433
+
434
+ 5. Update (Optional)
435
+ ├─> engine.remove_fact(old_fact)
436
+ ├─> engine.add_fact(:type, new_attrs)
437
+ └─> Matching re-triggered
438
+
439
+ 6. Removal
440
+ ├─> engine.remove_fact(fact)
441
+ ├─> Removed from WorkingMemory
442
+ ├─> Deleted from database (if persistent)
443
+ └─> Tokens invalidated
444
+ ```
445
+
446
+ ### Observing Fact Changes
447
+
448
+ Working memory uses the Observer pattern:
449
+
450
+ ```ruby
451
+ class FactObserver
452
+ def update(operation, fact)
453
+ case operation
454
+ when :add
455
+ puts "Added: #{fact.type} - #{fact.attributes}"
456
+ when :remove
457
+ puts "Removed: #{fact.type} - #{fact.attributes}"
458
+ end
459
+ end
460
+ end
461
+
462
+ observer = FactObserver.new
463
+ engine.working_memory.add_observer(observer)
464
+
465
+ engine.add_fact(:sensor, { id: "bedroom", temp: 28 })
466
+ # Output: Added: sensor - {:id=>"bedroom", :temp=>28}
467
+ ```
468
+
469
+ ## Best Practices
470
+
471
+ ### 1. Use Consistent Fact Types
472
+
473
+ ```ruby
474
+ # Good: Consistent naming
475
+ :sensor_reading
476
+ :stock_quote
477
+ :user_alert
478
+
479
+ # Bad: Inconsistent
480
+ :sensor
481
+ :Stock
482
+ :UserAlert
483
+ ```
484
+
485
+ ### 2. Keep Attributes Flat
486
+
487
+ ```ruby
488
+ # Good: Flat structure
489
+ engine.add_fact(:sensor, {
490
+ sensor_id: "bedroom",
491
+ temp: 28,
492
+ humidity: 65
493
+ })
494
+
495
+ # Bad: Nested (harder to match)
496
+ engine.add_fact(:sensor, {
497
+ id: "bedroom",
498
+ readings: { temp: 28, humidity: 65 }
499
+ })
500
+ ```
501
+
502
+ ### 3. Include Timestamps
503
+
504
+ ```ruby
505
+ # Good: Temporal reasoning enabled
506
+ engine.add_fact(:reading, {
507
+ sensor_id: "bedroom",
508
+ value: 28,
509
+ timestamp: Time.now
510
+ })
511
+ ```
512
+
513
+ ### 4. Validate Before Adding
514
+
515
+ ```ruby
516
+ def add_sensor_reading(engine, id, temp)
517
+ # Validate
518
+ raise ArgumentError, "Invalid temp" unless temp.is_a?(Numeric)
519
+ raise ArgumentError, "Temp out of range" unless temp.between?(-50, 100)
520
+
521
+ # Add fact
522
+ engine.add_fact(:sensor, {
523
+ id: id,
524
+ temp: temp,
525
+ timestamp: Time.now
526
+ })
527
+ end
528
+ ```
529
+
530
+ ### 5. Use Symbols for Type
531
+
532
+ ```ruby
533
+ # Good
534
+ engine.add_fact(:sensor, { ... })
535
+
536
+ # Bad
537
+ engine.add_fact("sensor", { ... }) # Strings not idiomatic
538
+ ```
539
+
540
+ ### 6. Namespace Fact Types
541
+
542
+ ```ruby
543
+ # Good: Clear namespacing for large systems
544
+ :trading_order
545
+ :trading_execution
546
+ :trading_alert
547
+
548
+ :sensor_temp
549
+ :sensor_humidity
550
+ :sensor_pressure
551
+ ```
552
+
553
+ ## Common Patterns
554
+
555
+ ### Fact Factory
556
+
557
+ ```ruby
558
+ class SensorFactFactory
559
+ def self.create_reading(id, temp, humidity)
560
+ {
561
+ type: :sensor,
562
+ attributes: {
563
+ id: id,
564
+ temp: temp,
565
+ humidity: humidity,
566
+ timestamp: Time.now
567
+ }
568
+ }
569
+ end
570
+ end
571
+
572
+ # Usage
573
+ reading = SensorFactFactory.create_reading("bedroom", 28, 65)
574
+ engine.add_fact(reading[:type], reading[:attributes])
575
+ ```
576
+
577
+ ### Fact Builder
578
+
579
+ ```ruby
580
+ class FactBuilder
581
+ def initialize(type)
582
+ @type = type
583
+ @attributes = {}
584
+ end
585
+
586
+ def with(key, value)
587
+ @attributes[key] = value
588
+ self
589
+ end
590
+
591
+ def build
592
+ [@type, @attributes]
593
+ end
594
+ end
595
+
596
+ # Usage
597
+ type, attrs = FactBuilder.new(:stock)
598
+ .with(:symbol, "AAPL")
599
+ .with(:price, 150)
600
+ .with(:volume, 1000000)
601
+ .build
602
+
603
+ engine.add_fact(type, attrs)
604
+ ```
605
+
606
+ ### Fact Repository
607
+
608
+ ```ruby
609
+ class FactRepository
610
+ def initialize(engine)
611
+ @engine = engine
612
+ end
613
+
614
+ def add(type, attributes)
615
+ @engine.add_fact(type, attributes.merge(created_at: Time.now))
616
+ end
617
+
618
+ def find_by_id(type, id)
619
+ @engine.facts.find { |f| f.type == type && f[:id] == id }
620
+ end
621
+
622
+ def where(type, &block)
623
+ @engine.facts.select { |f| f.type == type && block.call(f) }
624
+ end
625
+
626
+ def remove_where(type, &block)
627
+ facts = where(type, &block)
628
+ facts.each { |f| @engine.remove_fact(f) }
629
+ @engine.run
630
+ end
631
+ end
632
+
633
+ # Usage
634
+ repo = FactRepository.new(engine)
635
+ repo.add(:sensor, { id: "bedroom", temp: 28 })
636
+
637
+ bedroom = repo.find_by_id(:sensor, "bedroom")
638
+ high_temps = repo.where(:sensor) { |f| f[:temp] > 30 }
639
+ repo.remove_where(:alert) { |f| f[:stale] }
640
+ ```
641
+
642
+ ## Next Steps
643
+
644
+ - **[Pattern Matching](pattern-matching.md)** - How facts match conditions
645
+ - **[Writing Rules](writing-rules.md)** - Using facts in rule conditions
646
+ - **[Blackboard Memory](blackboard-memory.md)** - Persistent fact storage
647
+ - **[Persistence Guide](persistence.md)** - SQLite, Redis, and hybrid storage
648
+ - **[API Reference](../api/facts.md)** - Complete Fact API documentation
649
+
650
+ ---
651
+
652
+ *Facts are immutable knowledge. When facts change, replace them to trigger re-evaluation.*