kbs 0.1.0 → 0.2.1

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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +19 -0
  3. data/README.md +85 -57
  4. data/docs/advanced/performance.md +109 -76
  5. data/docs/advanced/testing.md +399 -263
  6. data/docs/api/blackboard.md +1 -1
  7. data/docs/api/engine.md +77 -8
  8. data/docs/api/facts.md +3 -3
  9. data/docs/api/rules.md +110 -40
  10. data/docs/architecture/blackboard.md +108 -117
  11. data/docs/assets/images/fact-rule-relationship.svg +65 -0
  12. data/docs/assets/images/fact-structure.svg +42 -0
  13. data/docs/assets/images/inference-cycle.svg +47 -0
  14. data/docs/assets/images/kb-components.svg +43 -0
  15. data/docs/assets/images/rule-structure.svg +44 -0
  16. data/docs/assets/images/trading-signal-network.svg +1 -1
  17. data/docs/examples/index.md +219 -5
  18. data/docs/guides/blackboard-memory.md +89 -58
  19. data/docs/guides/dsl.md +24 -24
  20. data/docs/guides/getting-started.md +109 -107
  21. data/docs/guides/writing-rules.md +470 -311
  22. data/docs/index.md +16 -18
  23. data/docs/quick-start.md +92 -99
  24. data/docs/what-is-a-fact.md +694 -0
  25. data/docs/what-is-a-knowledge-base.md +350 -0
  26. data/docs/what-is-a-rule.md +833 -0
  27. data/examples/.gitignore +1 -0
  28. data/examples/advanced_example_dsl.rb +1 -1
  29. data/examples/ai_enhanced_kbs_dsl.rb +1 -1
  30. data/examples/car_diagnostic_dsl.rb +1 -1
  31. data/examples/concurrent_inference_demo.rb +0 -1
  32. data/examples/concurrent_inference_demo_dsl.rb +0 -1
  33. data/examples/csv_trading_system_dsl.rb +1 -1
  34. data/examples/iot_demo_using_dsl.rb +1 -1
  35. data/examples/portfolio_rebalancing_system_dsl.rb +1 -1
  36. data/examples/rule_source_demo.rb +123 -0
  37. data/examples/stock_trading_advanced_dsl.rb +1 -1
  38. data/examples/temp_dsl.txt +6214 -5269
  39. data/examples/timestamped_trading_dsl.rb +1 -1
  40. data/examples/trading_demo_dsl.rb +1 -1
  41. data/examples/working_demo_dsl.rb +1 -1
  42. data/lib/kbs/decompiler.rb +204 -0
  43. data/lib/kbs/dsl/knowledge_base.rb +100 -1
  44. data/lib/kbs/dsl.rb +3 -1
  45. data/lib/kbs/engine.rb +41 -0
  46. data/lib/kbs/version.rb +1 -1
  47. data/lib/kbs.rb +14 -12
  48. data/mkdocs.yml +30 -30
  49. metadata +15 -10
  50. data/docs/DOCUMENTATION_STATUS.md +0 -158
  51. data/docs/examples/expert-systems.md +0 -1031
  52. data/docs/examples/multi-agent.md +0 -1335
  53. data/docs/examples/stock-trading.md +0 -488
  54. data/examples/knowledge_base.db +0 -0
  55. data/examples/temp.txt +0 -7693
@@ -0,0 +1,694 @@
1
+ # What is a Fact?
2
+
3
+ A **fact** is the fundamental unit of knowledge in KBS - a piece of information about your domain that the system can reason about. Facts are the "data" on which rules operate.
4
+
5
+ ## Core Concept
6
+
7
+ Think of a fact as a **typed data record** that represents something true at a particular moment:
8
+
9
+ - "The temperature in the server room is 85°F"
10
+ - "Stock AAPL is trading at $150.25 with volume 1.2M"
11
+ - "Sensor #42 is active"
12
+ - "Order #123 is pending"
13
+
14
+ Each fact has:
15
+
16
+ 1. **Type** - What kind of thing this is (`:temperature`, `:stock`, `:sensor`, `:order`)
17
+ 2. **Attributes** - Key-value pairs describing it (`location: "server_room"`, `value: 85`)
18
+
19
+ ## Anatomy of a Fact
20
+
21
+ ### Structure
22
+
23
+ ```ruby
24
+ fact = KBS::Fact.new(:temperature, location: "server_room", value: 85)
25
+
26
+ fact.type # => :temperature
27
+ fact.attributes # => {:location => "server_room", :value => 85}
28
+ fact[:location] # => "server_room"
29
+ fact[:value] # => 85
30
+ ```
31
+
32
+ ### Visual Representation
33
+
34
+ ![Fact Structure](assets/images/fact-structure.svg)
35
+
36
+ *A fact consists of a type symbol and a hash of attribute key-value pairs.*
37
+
38
+ ## How Facts Differ from Other Data Structures
39
+
40
+ | Aspect | Fact | Plain Hash | Database Row | Object |
41
+ |--------|------|-----------|--------------|--------|
42
+ | **Type** | Explicit (`:temperature`) | None | Table name | Class |
43
+ | **Pattern Matching** | Built-in | Manual | SQL WHERE | Manual |
44
+ | **Identity** | By content & type | By reference | By primary key | By reference |
45
+ | **Purpose** | Reasoning & inference | General storage | Persistent storage | Behavior + data |
46
+ | **Lifecycle** | Add/retract from KB | Create/destroy | Insert/delete | New/GC |
47
+
48
+ **Example Comparison:**
49
+
50
+ ```ruby
51
+ # Plain Hash
52
+ data = { location: "server_room", value: 85 }
53
+ # What kind of data is this? No way to tell.
54
+
55
+ # Database Row
56
+ # SELECT * FROM temperatures WHERE location = 'server_room'
57
+ # Requires SQL, separate from logic
58
+
59
+ # Object
60
+ class Temperature
61
+ attr_accessor :location, :value
62
+ end
63
+ temp = Temperature.new
64
+ # Has behavior but no built-in pattern matching
65
+
66
+ # Fact
67
+ fact = KBS::Fact.new(:temperature, location: "server_room", value: 85)
68
+ # Self-describing, pattern-matchable, inference-ready
69
+ ```
70
+
71
+ ## Fact Lifecycle
72
+
73
+ ### 1. Creation
74
+
75
+ Facts are created and added to a knowledge base:
76
+
77
+ ```ruby
78
+ # In-memory knowledge base
79
+ kb = KBS.knowledge_base do
80
+ fact :temperature, location: "server_room", value: 85
81
+ end
82
+
83
+ # Blackboard (persistent)
84
+ engine = KBS::Blackboard::Engine.new(db_path: 'kb.db')
85
+ fact = engine.add_fact(:temperature, location: "server_room", value: 85)
86
+ ```
87
+
88
+ ### 2. Pattern Matching
89
+
90
+ Once added, facts are automatically matched against rule patterns:
91
+
92
+ ```ruby
93
+ rule "high_temperature" do
94
+ # This pattern matches our fact above
95
+ on :temperature, location: "server_room", value: greater_than(80)
96
+ perform { puts "Alert!" }
97
+ end
98
+ ```
99
+
100
+ ### 3. Rule Firing
101
+
102
+ When all conditions of a rule match, the rule fires:
103
+
104
+ ```ruby
105
+ kb.run # → "Alert!" (rule fires because fact matches)
106
+ ```
107
+
108
+ ### 4. Updates (Blackboard Only)
109
+
110
+ Persistent facts can be updated:
111
+
112
+ ```ruby
113
+ fact[:value] = 90 # Update persisted immediately
114
+ fact.update(value: 90, timestamp: Time.now) # Bulk update
115
+ ```
116
+
117
+ **Note**: Updates do NOT trigger rule re-evaluation. To re-trigger rules, retract and re-add.
118
+
119
+ ### 5. Retraction
120
+
121
+ Facts can be removed from working memory:
122
+
123
+ ```ruby
124
+ # DSL
125
+ retract fact
126
+
127
+ # Blackboard - fact can retract itself
128
+ fact.retract
129
+ ```
130
+
131
+ ## Fact Types (Implementations)
132
+
133
+ KBS provides two fact implementations:
134
+
135
+ ### 1. Transient Facts (`KBS::Fact`)
136
+
137
+ - **Used by**: In-memory knowledge bases
138
+ - **Identity**: Ruby object ID
139
+ - **Persistence**: None (lost on process exit)
140
+ - **Performance**: Fast (no I/O)
141
+
142
+ ```ruby
143
+ fact = KBS::Fact.new(:temperature, value: 85)
144
+ puts fact.id # => 70123456789012 (Ruby object ID)
145
+
146
+ # Lightweight, perfect for short-lived reasoning
147
+ kb = KBS.knowledge_base do
148
+ fact :stock, symbol: "AAPL", price: 150
149
+ run
150
+ end
151
+ # Facts disappear when kb goes out of scope
152
+ ```
153
+
154
+ **Best for**:
155
+
156
+ - Event stream processing
157
+ - Short-lived analyses
158
+ - Prototyping and testing
159
+ - When restart durability isn't needed
160
+
161
+ ### 2. Persistent Facts (`KBS::Blackboard::Fact`)
162
+
163
+ - **Used by**: Blackboard knowledge bases
164
+ - **Identity**: UUID (stable across restarts)
165
+ - **Persistence**: SQLite, Redis, or Hybrid storage
166
+ - **Audit Trail**: Complete change history
167
+ - **Performance**: Slower (I/O overhead)
168
+
169
+ ```ruby
170
+ engine = KBS::Blackboard::Engine.new(db_path: 'kb.db')
171
+ fact = engine.add_fact(:temperature, value: 85)
172
+
173
+ puts fact.uuid # => "550e8400-e29b-41d4-a716-446655440000"
174
+
175
+ # Update persists
176
+ fact[:value] = 90
177
+
178
+ # Restart process
179
+ engine2 = KBS::Blackboard::Engine.new(db_path: 'kb.db')
180
+ reloaded = engine2.blackboard.get_facts_by_type(:temperature).first
181
+ puts reloaded[:value] # => 90 (persisted)
182
+ ```
183
+
184
+ **Best for**:
185
+
186
+ - Long-running systems
187
+ - Multi-agent coordination
188
+ - Audit requirements
189
+ - Systems that must survive restarts
190
+ - Distributed reasoning
191
+
192
+ ## Pattern Matching
193
+
194
+ Facts excel at pattern matching—the ability to find facts that satisfy specific criteria.
195
+
196
+ ### Literal Matching
197
+
198
+ Match exact values:
199
+
200
+ ```ruby
201
+ fact = KBS::Fact.new(:stock, symbol: "AAPL", price: 150.25)
202
+
203
+ # Matches
204
+ fact.matches?(type: :stock, symbol: "AAPL") # => true
205
+
206
+ # Doesn't match
207
+ fact.matches?(type: :stock, symbol: "GOOGL") # => false
208
+ ```
209
+
210
+ ### Predicate Matching
211
+
212
+ Match with lambda conditions:
213
+
214
+ ```ruby
215
+ fact = KBS::Fact.new(:temperature, value: 85)
216
+
217
+ # Matches
218
+ fact.matches?(type: :temperature, value: ->(v) { v > 80 }) # => true
219
+ fact.matches?(type: :temperature, value: ->(v) { v < 100 }) # => true
220
+
221
+ # Doesn't match
222
+ fact.matches?(type: :temperature, value: ->(v) { v > 90 }) # => false
223
+ ```
224
+
225
+ ### Variable Binding
226
+
227
+ Capture values for use in rule actions:
228
+
229
+ ```ruby
230
+ rule "report_temperature" do
231
+ on :temperature, location: :loc?, value: :temp?
232
+ # ^^^^^^ ^^^^^^
233
+ # Variables (end with ?)
234
+
235
+ perform do |facts, bindings|
236
+ # bindings contains captured values
237
+ puts "#{bindings[:loc?]}: #{bindings[:temp?]}°F"
238
+ end
239
+ end
240
+
241
+ # Add fact
242
+ fact :temperature, location: "server_room", value: 85
243
+
244
+ run # → "server_room: 85°F"
245
+ ```
246
+
247
+ ### Join Tests
248
+
249
+ Variables create joins across multiple facts:
250
+
251
+ ```ruby
252
+ rule "inventory_check" do
253
+ on :order, product_id: :pid?, quantity: :qty?
254
+ on :inventory, product_id: :pid?, available: :avail?
255
+ # ^^^^^^
256
+ # Same variable = JOIN condition
257
+
258
+ perform do |facts, bindings|
259
+ # Only fires when BOTH facts have same product_id
260
+ if bindings[:avail?] < bindings[:qty?]
261
+ puts "Insufficient inventory for #{bindings[:pid?]}"
262
+ end
263
+ end
264
+ end
265
+ ```
266
+
267
+ ## Common Fact Patterns
268
+
269
+ ### 1. Entity Facts
270
+
271
+ Represent domain objects:
272
+
273
+ ```ruby
274
+ fact :customer, id: 12345, name: "Acme Corp", tier: "gold"
275
+ fact :product, sku: "ABC-123", price: 49.99, in_stock: true
276
+ fact :order, id: 789, customer_id: 12345, total: 499.90
277
+ ```
278
+
279
+ ### 2. Event Facts
280
+
281
+ Represent things that happened:
282
+
283
+ ```ruby
284
+ fact :order_placed, order_id: 789, timestamp: Time.now
285
+ fact :payment_received, order_id: 789, amount: 499.90
286
+ fact :item_shipped, tracking: "1Z999", order_id: 789
287
+ ```
288
+
289
+ ### 3. Sensor Facts
290
+
291
+ Real-time measurements:
292
+
293
+ ```ruby
294
+ fact :temperature, sensor_id: 42, value: 85, timestamp: Time.now
295
+ fact :pressure, sensor_id: 43, value: 14.7, unit: "psi"
296
+ fact :motion_detected, camera_id: 5, location: "entrance"
297
+ ```
298
+
299
+ ### 4. State Facts
300
+
301
+ Current system state:
302
+
303
+ ```ruby
304
+ fact :connection, server: "db-1", status: "active"
305
+ fact :worker, id: 3, status: "busy", task_id: 456
306
+ fact :cache, key: "user:123", valid_until: Time.now + 3600
307
+ ```
308
+
309
+ ### 5. Derived Facts
310
+
311
+ Facts inferred from other facts:
312
+
313
+ ```ruby
314
+ rule "derive_alert" do
315
+ on :temperature, value: greater_than(80), location: :loc?
316
+ without :alert, location: :loc? # No existing alert
317
+
318
+ perform do |facts, bindings|
319
+ # Add derived fact
320
+ fact :alert,
321
+ location: bindings[:loc?],
322
+ level: "high",
323
+ source: "temperature_monitor"
324
+ end
325
+ end
326
+ ```
327
+
328
+ ### 6. Flag Facts
329
+
330
+ Boolean markers (attributes optional):
331
+
332
+ ```ruby
333
+ fact :system_ready
334
+ fact :maintenance_mode
335
+ fact :debug_enabled
336
+ fact :cache_warmed
337
+
338
+ # Used in rules
339
+ rule "process_requests" do
340
+ on :system_ready
341
+ without :maintenance_mode
342
+ on :request, id: :req_id?
343
+ perform { |facts, b| handle_request(b[:req_id?]) }
344
+ end
345
+ ```
346
+
347
+ ## Fact vs. Rule Relationship
348
+
349
+ Facts and rules work together in a symbiotic relationship:
350
+
351
+ ![Fact-Rule Relationship](assets/images/fact-rule-relationship.svg)
352
+
353
+ *Facts (data) and rules (logic) interact through pattern matching: rules match facts, execute actions, and may create new facts, continuing the inference cycle.*
354
+
355
+ **Example:**
356
+
357
+ ```ruby
358
+ # FACTS represent the current state
359
+ fact :stock, symbol: "AAPL", price: 150, volume: 1_000_000
360
+ fact :portfolio, cash: 10_000, max_position: 5_000
361
+
362
+ # RULES define logic
363
+ rule "momentum_buy" do
364
+ # IF these facts exist with these patterns...
365
+ on :stock, symbol: :sym?, price: :price?, volume: greater_than(500_000)
366
+ on :portfolio, cash: :cash?, max_position: :max?
367
+
368
+ # THEN execute this action
369
+ perform do |facts, bindings|
370
+ position_size = [bindings[:max?], bindings[:cash?] * 0.1].min
371
+ shares = (position_size / bindings[:price?]).floor
372
+
373
+ if shares > 0
374
+ # Action may create new facts
375
+ fact :order,
376
+ symbol: bindings[:sym?],
377
+ shares: shares,
378
+ type: "market_buy"
379
+ end
380
+ end
381
+ end
382
+ ```
383
+
384
+ ## Fact Semantics
385
+
386
+ ### Open World Assumption
387
+
388
+ Facts can have any attributes. Patterns only constrain what they mention:
389
+
390
+ ```ruby
391
+ # Fact has 4 attributes
392
+ fact = KBS::Fact.new(:stock,
393
+ symbol: "AAPL",
394
+ price: 150,
395
+ volume: 1_000_000,
396
+ exchange: "NASDAQ"
397
+ )
398
+
399
+ # Pattern only constrains 2 - still matches!
400
+ fact.matches?(type: :stock, symbol: "AAPL") # => true
401
+ ```
402
+
403
+ ### Closed Attribute Assumption
404
+
405
+ If a pattern requires an attribute, the fact must have it:
406
+
407
+ ```ruby
408
+ fact = KBS::Fact.new(:stock, symbol: "AAPL", price: 150)
409
+ # No :volume attribute
410
+
411
+ # Fails - fact missing required :volume
412
+ fact.matches?(type: :stock, volume: greater_than(1000)) # => false
413
+ ```
414
+
415
+ ### Type Safety
416
+
417
+ Type is always checked first:
418
+
419
+ ```ruby
420
+ fact = KBS::Fact.new(:stock, symbol: "AAPL")
421
+
422
+ # Fails immediately - wrong type
423
+ fact.matches?(type: :temperature) # => false
424
+
425
+ # Succeeds - right type
426
+ fact.matches?(type: :stock) # => true
427
+ ```
428
+
429
+ ### Value Immutability (Transient Facts)
430
+
431
+ Transient facts should be treated as immutable. Changing attributes doesn't trigger re-evaluation:
432
+
433
+ ```ruby
434
+ fact = KBS::Fact.new(:temperature, value: 85)
435
+ engine.add_fact(fact)
436
+
437
+ # Don't do this - change not tracked
438
+ fact[:value] = 90 # Rules won't re-fire
439
+
440
+ # Instead, retract and re-add
441
+ engine.remove_fact(fact)
442
+ new_fact = KBS::Fact.new(:temperature, value: 90)
443
+ engine.add_fact(new_fact)
444
+ ```
445
+
446
+ ### Value Mutability (Persistent Facts)
447
+
448
+ Persistent facts track updates but don't re-trigger rules:
449
+
450
+ ```ruby
451
+ fact = engine.add_fact(:temperature, value: 85)
452
+
453
+ # This persists but doesn't re-fire rules
454
+ fact[:value] = 90
455
+
456
+ # To re-trigger rules, retract and re-add
457
+ fact.retract
458
+ new_fact = engine.add_fact(:temperature, value: 90)
459
+ ```
460
+
461
+ ## Performance Considerations
462
+
463
+ ### Fact Count Impact
464
+
465
+ - **RETE strength**: Efficient with many facts and stable rules
466
+ - **Alpha memories**: Facts indexed by type
467
+ - **Beta network**: Partial matches cached as tokens
468
+ - **Unlinking**: Empty nodes deactivated automatically
469
+
470
+ **Scaling characteristics**:
471
+
472
+ - 10-1,000 facts: Excellent performance
473
+ - 1,000-10,000 facts: Very good (alpha memory indexing helps)
474
+ - 10,000-100,000 facts: Good (consider indexing strategies)
475
+ - 100,000+ facts: Consider domain-specific optimizations
476
+
477
+ ### Attribute Count Impact
478
+
479
+ Facts can have any number of attributes:
480
+
481
+ ```ruby
482
+ # Small fact (fast)
483
+ fact :flag, active: true
484
+
485
+ # Medium fact (typical)
486
+ fact :order,
487
+ id: 123,
488
+ customer_id: 456,
489
+ total: 99.99,
490
+ status: "pending"
491
+
492
+ # Large fact (fine, but consider if all attributes needed)
493
+ fact :trade,
494
+ symbol: "AAPL",
495
+ price: 150.25,
496
+ volume: 1000,
497
+ timestamp: Time.now,
498
+ order_id: 789,
499
+ account_id: 456,
500
+ commission: 1.50,
501
+ exchange: "NASDAQ",
502
+ # ... 20 more attributes
503
+ ```
504
+
505
+ **Guideline**: Include attributes you'll pattern match on. Store auxiliary data in external systems if not needed for rules.
506
+
507
+ ### Pattern Complexity Impact
508
+
509
+ ```ruby
510
+ # Fast - literal match (hash equality)
511
+ on :stock, symbol: "AAPL"
512
+
513
+ # Medium - simple predicate
514
+ on :stock, price: ->(p) { p > 100 }
515
+
516
+ # Slow - complex predicate (runs on every match attempt)
517
+ on :stock, price: ->(p) {
518
+ historical_data = fetch_history(p) # External call!
519
+ calculate_volatility(historical_data) > threshold
520
+ }
521
+ ```
522
+
523
+ **Guideline**: Keep predicates simple. Do expensive checks in rule actions, not patterns.
524
+
525
+ ## Common Pitfalls
526
+
527
+ ### 1. Forgetting Fact Type
528
+
529
+ ```ruby
530
+ # Wrong - no type
531
+ fact = { location: "server_room", value: 85 }
532
+
533
+ # Right - always include type
534
+ fact :temperature, location: "server_room", value: 85
535
+ ```
536
+
537
+ ### 2. Expecting Updates to Re-trigger Rules
538
+
539
+ ```ruby
540
+ fact = engine.add_fact(:temperature, value: 85)
541
+
542
+ # This rule fires
543
+ rule "high_temp" do
544
+ on :temperature, value: greater_than(80)
545
+ perform { puts "High!" }
546
+ end
547
+
548
+ # Update doesn't re-fire rule
549
+ fact[:value] = 90 # Rule doesn't fire again
550
+
551
+ # Must retract and re-add to re-trigger
552
+ fact.retract
553
+ engine.add_fact(:temperature, value: 90)
554
+ engine.run # Now rule fires
555
+ ```
556
+
557
+ ### 3. Side Effects in Predicates
558
+
559
+ ```ruby
560
+ # Wrong - side effects
561
+ counter = 0
562
+ on :stock, price: ->(p) {
563
+ counter += 1 # Bad! Runs on every match attempt
564
+ p > 100
565
+ }
566
+
567
+ # Right - pure predicate
568
+ threshold = 100
569
+ on :stock, price: ->(p) { p > threshold }
570
+ ```
571
+
572
+ ### 4. Missing Attributes in Predicates
573
+
574
+ ```ruby
575
+ fact = KBS::Fact.new(:stock, symbol: "AAPL") # No :price
576
+
577
+ # Fails - predicate can't evaluate nil
578
+ fact.matches?(type: :stock, price: ->(p) { p > 100 }) # => false
579
+
580
+ # Use variable to capture nil
581
+ fact.matches?(type: :stock, price: :price?) # => true (binds :price? => nil)
582
+ ```
583
+
584
+ ### 5. Confusing Negation
585
+
586
+ ```ruby
587
+ # Matches when NO critical alert EXISTS
588
+ without :alert, level: "critical"
589
+
590
+ # NOT the same as: Match alerts that aren't critical
591
+ # For that, use:
592
+ on :alert, level: ->(l) { l != "critical" }
593
+ ```
594
+
595
+ ## Best Practices
596
+
597
+ ### 1. Use Descriptive Fact Types
598
+
599
+ ```ruby
600
+ # Good - clear semantic meaning
601
+ fact :temperature_reading, sensor_id: 42, value: 85
602
+ fact :order_placed, order_id: 123, timestamp: Time.now
603
+ fact :inventory_shortage, product_id: "ABC", deficit: 50
604
+
605
+ # Avoid - vague types
606
+ fact :data, type: "temp", id: 42, val: 85
607
+ fact :event, kind: "order", timestamp: Time.now
608
+ ```
609
+
610
+ ### 2. Include Identifying Attributes
611
+
612
+ ```ruby
613
+ # Good - can query and match specifically
614
+ fact :sensor, id: 42, status: "active", location: "room_1"
615
+ fact :order, id: 123, customer_id: 456, total: 99.99
616
+
617
+ # Harder to work with - no unique identifier
618
+ fact :sensor, status: "active"
619
+ ```
620
+
621
+ ### 3. Add Timestamps for Time-Based Reasoning
622
+
623
+ ```ruby
624
+ fact :temperature,
625
+ sensor_id: 42,
626
+ value: 85,
627
+ timestamp: Time.now
628
+
629
+ # Enables rules like:
630
+ rule "stale_data" do
631
+ on :temperature,
632
+ timestamp: ->(ts) { Time.now - ts > 300 }
633
+ perform { puts "Stale data!" }
634
+ end
635
+ ```
636
+
637
+ ### 4. Use Fact Types to Model Domain
638
+
639
+ Organize facts around your domain concepts:
640
+
641
+ **Stock Trading:**
642
+ ```ruby
643
+ fact :stock, symbol: "AAPL", price: 150, volume: 1_000_000
644
+ fact :order, id: 123, type: "buy", shares: 100
645
+ fact :position, symbol: "AAPL", shares: 500, cost_basis: 145
646
+ fact :alert, level: "high", message: "Price spike detected"
647
+ ```
648
+
649
+ **IoT Monitoring:**
650
+ ```ruby
651
+ fact :sensor, id: 42, type: "temperature", location: "server_1"
652
+ fact :reading, sensor_id: 42, value: 85, timestamp: Time.now
653
+ fact :threshold, sensor_id: 42, max: 80, min: 60
654
+ fact :alert, sensor_id: 42, severity: "warning"
655
+ ```
656
+
657
+ ### 5. Keep Facts Focused
658
+
659
+ One fact = one piece of knowledge
660
+
661
+ ```ruby
662
+ # Good - focused facts
663
+ fact :order, id: 123, status: "pending"
664
+ fact :customer, id: 456, name: "Acme"
665
+ fact :payment, order_id: 123, amount: 99.99
666
+
667
+ # Avoid - bloated fact with everything
668
+ fact :transaction,
669
+ order_id: 123,
670
+ customer_id: 456,
671
+ customer_name: "Acme",
672
+ payment_method: "credit",
673
+ # ... 30 more fields
674
+ ```
675
+
676
+ ## Further Reading
677
+
678
+ - **[Facts API Reference](api/facts.md)** - Complete method documentation
679
+ - **[Pattern Matching Guide](guides/pattern-matching.md)** - Detailed matching semantics
680
+ - **[Variable Binding Guide](guides/variable-binding.md)** - Join tests and captures
681
+ - **[Knowledge Base](what-is-a-knowledge-base.md)** - How facts fit into knowledge bases
682
+ - **[RETE Algorithm](architecture/rete-algorithm.md)** - How facts are matched efficiently
683
+
684
+ ## Summary
685
+
686
+ A **fact** is:
687
+
688
+ - The fundamental unit of knowledge in KBS
689
+ - A **typed record** with attributes (`:type` + `{key: value}`)
690
+ - **Pattern-matchable** using literals, predicates, and variables
691
+ - Available in both **transient** (fast, volatile) and **persistent** (durable, auditable) forms
692
+ - The "data" that rules reason about
693
+
694
+ Think of facts as **statements of truth** that the knowledge base can automatically reason about and act upon.