1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
//! Advanced Agenda System (Drools-style)
//!
//! This module implements advanced agenda features similar to Drools:
//! - Activation Groups: Only one rule in a group can fire
//! - Agenda Groups: Sequential execution of rule groups
//! - Ruleflow Groups: Workflow-based execution
//! - Auto-focus: Automatic agenda group switching
//! - Lock-on-active: Prevent re-activation during rule firing
//! - Conflict Resolution Strategies: Multiple ordering strategies
use std::cmp::Ordering;
use std::collections::{BinaryHeap, HashMap, HashSet};
/// Conflict Resolution Strategy
///
/// Determines how conflicting activations are ordered in the agenda.
/// Similar to CLIPS and Drools conflict resolution strategies.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConflictResolutionStrategy {
/// Salience-based ordering (default) - Higher salience fires first
Salience,
/// LEX (Recency) - Most recently inserted facts fire first
/// Sorts by the timestamp of the most recent fact used in the activation
LEX,
/// MEA (Recency + Specificity) - LEX + more specific rules first
/// Combines recency with condition count (more conditions = more specific)
MEA,
/// Depth-first - Fire rule immediately after insertion
/// Re-evaluates agenda after each rule fires
Depth,
/// Breadth-first - Collect all activations before firing (default)
/// Fires all activations in current cycle before re-evaluating
Breadth,
/// Simplicity - Rules with fewer conditions fire first
/// Simpler rules are prioritized
Simplicity,
/// Complexity - Rules with more conditions fire first
/// More complex/specific rules are prioritized
Complexity,
/// Random - Random ordering
/// Useful for testing non-deterministic behavior
Random,
}
/// Activation represents a rule that is ready to fire
#[derive(Debug, Clone)]
pub struct Activation {
/// Rule name
pub rule_name: String,
/// Priority/salience (higher fires first)
pub salience: i32,
/// Activation group (only one rule in group can fire)
pub activation_group: Option<String>,
/// Agenda group (for sequential execution)
pub agenda_group: String,
/// Ruleflow group (for workflow execution)
pub ruleflow_group: Option<String>,
/// No-loop flag
pub no_loop: bool,
/// Lock-on-active flag
pub lock_on_active: bool,
/// Auto-focus flag
pub auto_focus: bool,
/// Creation timestamp (for conflict resolution)
pub created_at: std::time::Instant,
/// Number of conditions in the rule (for complexity/simplicity strategies)
pub condition_count: usize,
/// Matched fact handle (which fact triggered this activation)
pub matched_fact_handle: Option<super::FactHandle>,
/// Internal ID
id: usize,
}
impl Activation {
/// Create a new activation
pub fn new(rule_name: String, salience: i32) -> Self {
Self {
rule_name,
salience,
activation_group: None,
agenda_group: "MAIN".to_string(),
ruleflow_group: None,
no_loop: true,
lock_on_active: false,
auto_focus: false,
created_at: std::time::Instant::now(),
condition_count: 1, // Default to 1
matched_fact_handle: None,
id: 0,
}
}
/// Builder: Set matched fact handle
pub fn with_matched_fact(mut self, handle: super::FactHandle) -> Self {
self.matched_fact_handle = Some(handle);
self
}
/// Builder: Set condition count
pub fn with_condition_count(mut self, count: usize) -> Self {
self.condition_count = count;
self
}
/// Builder: Set activation group
pub fn with_activation_group(mut self, group: String) -> Self {
self.activation_group = Some(group);
self
}
/// Builder: Set agenda group
pub fn with_agenda_group(mut self, group: String) -> Self {
self.agenda_group = group;
self
}
/// Builder: Set ruleflow group
pub fn with_ruleflow_group(mut self, group: String) -> Self {
self.ruleflow_group = Some(group);
self
}
/// Builder: Set no-loop
pub fn with_no_loop(mut self, no_loop: bool) -> Self {
self.no_loop = no_loop;
self
}
/// Builder: Set lock-on-active
pub fn with_lock_on_active(mut self, lock: bool) -> Self {
self.lock_on_active = lock;
self
}
/// Builder: Set auto-focus
pub fn with_auto_focus(mut self, auto_focus: bool) -> Self {
self.auto_focus = auto_focus;
self
}
}
// Implement ordering for priority queue (higher salience first)
impl PartialEq for Activation {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl Eq for Activation {}
impl PartialOrd for Activation {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Activation {
fn cmp(&self, other: &Self) -> Ordering {
// First compare by salience (higher is better)
match self.salience.cmp(&other.salience) {
Ordering::Equal => {
// Then by creation time (earlier is better = reverse)
other.created_at.cmp(&self.created_at)
}
other_order => other_order,
}
}
}
/// Advanced Agenda (Drools-style)
pub struct AdvancedAgenda {
/// All activations by agenda group
activations: HashMap<String, BinaryHeap<Activation>>,
/// Current focus (agenda group)
focus: String,
/// Focus stack
focus_stack: Vec<String>,
/// Fired rules (for no-loop)
fired_rules: HashSet<String>,
/// Fired activation groups
fired_activation_groups: HashSet<String>,
/// Locked groups (lock-on-active)
locked_groups: HashSet<String>,
/// Active ruleflow groups
active_ruleflow_groups: HashSet<String>,
/// Next activation ID
next_id: usize,
/// Conflict resolution strategy
strategy: ConflictResolutionStrategy,
}
impl AdvancedAgenda {
/// Create a new agenda with "MAIN" as default focus
pub fn new() -> Self {
let mut agenda = Self {
activations: HashMap::new(),
focus: "MAIN".to_string(),
focus_stack: Vec::new(),
fired_rules: HashSet::new(),
fired_activation_groups: HashSet::new(),
locked_groups: HashSet::new(),
active_ruleflow_groups: HashSet::new(),
next_id: 0,
strategy: ConflictResolutionStrategy::Salience, // Default strategy
};
agenda
.activations
.insert("MAIN".to_string(), BinaryHeap::new());
agenda
}
/// Set the conflict resolution strategy
pub fn set_strategy(&mut self, strategy: ConflictResolutionStrategy) {
self.strategy = strategy;
// Re-sort all existing activations with new strategy
let current_strategy = self.strategy; // Copy strategy to avoid borrow issues
for heap in self.activations.values_mut() {
let mut activations: Vec<_> = heap.drain().collect();
Self::sort_with_strategy(current_strategy, &mut activations);
*heap = activations.into_iter().collect();
}
}
/// Get current strategy
pub fn strategy(&self) -> ConflictResolutionStrategy {
self.strategy
}
/// Sort activations according to given strategy (static method)
fn sort_with_strategy(strategy: ConflictResolutionStrategy, activations: &mut [Activation]) {
match strategy {
ConflictResolutionStrategy::Salience => {
// Default: sort by salience (higher first), then by recency
activations.sort_by(|a, b| match b.salience.cmp(&a.salience) {
Ordering::Equal => b.created_at.cmp(&a.created_at),
other => other,
});
}
ConflictResolutionStrategy::LEX => {
// Recency: most recent first
activations.sort_by(|a, b| b.created_at.cmp(&a.created_at));
}
ConflictResolutionStrategy::MEA => {
// Recency + Specificity: recent first, then more conditions
activations.sort_by(|a, b| match b.created_at.cmp(&a.created_at) {
Ordering::Equal => b.condition_count.cmp(&a.condition_count),
other => other,
});
}
ConflictResolutionStrategy::Depth => {
// Depth-first: same as salience (handled in fire loop)
activations.sort_by(|a, b| match b.salience.cmp(&a.salience) {
Ordering::Equal => b.created_at.cmp(&a.created_at),
other => other,
});
}
ConflictResolutionStrategy::Breadth => {
// Breadth-first: same as salience (default behavior)
activations.sort_by(|a, b| match b.salience.cmp(&a.salience) {
Ordering::Equal => b.created_at.cmp(&a.created_at),
other => other,
});
}
ConflictResolutionStrategy::Simplicity => {
// Simpler rules (fewer conditions) first
activations.sort_by(|a, b| match a.condition_count.cmp(&b.condition_count) {
Ordering::Equal => b.created_at.cmp(&a.created_at),
other => other,
});
}
ConflictResolutionStrategy::Complexity => {
// More complex rules (more conditions) first
activations.sort_by(|a, b| match b.condition_count.cmp(&a.condition_count) {
Ordering::Equal => b.created_at.cmp(&a.created_at),
other => other,
});
}
ConflictResolutionStrategy::Random => {
// Random ordering using stdlib hash-based randomization
// Use addresses as pseudo-random source for deterministic tests
use std::collections::hash_map::RandomState;
use std::hash::{BuildHasher, Hash, Hasher};
let hasher_builder = RandomState::new();
activations.sort_by_cached_key(|a| {
let mut hasher = hasher_builder.build_hasher();
a.rule_name.hash(&mut hasher);
a.created_at.hash(&mut hasher);
hasher.finish()
});
}
}
}
/// Add an activation to the agenda
pub fn add_activation(&mut self, mut activation: Activation) {
// Auto-focus: switch to this agenda group if requested
if activation.auto_focus && activation.agenda_group != self.focus {
self.set_focus(activation.agenda_group.clone());
}
// Check activation group: if group already fired, skip
if let Some(ref group) = activation.activation_group {
if self.fired_activation_groups.contains(group) {
return; // Skip this activation
}
}
// Check ruleflow group: if not active, skip
if let Some(ref group) = activation.ruleflow_group {
if !self.active_ruleflow_groups.contains(group) {
return; // Skip this activation
}
}
// Assign ID
activation.id = self.next_id;
self.next_id += 1;
// Add to appropriate agenda group
self.activations
.entry(activation.agenda_group.clone())
.or_default()
.push(activation);
}
/// Get the next activation to fire (from current focus)
pub fn get_next_activation(&mut self) -> Option<Activation> {
loop {
// Try to get from current focus
if let Some(heap) = self.activations.get_mut(&self.focus) {
while let Some(activation) = heap.pop() {
// Check no-loop
if activation.no_loop && self.fired_rules.contains(&activation.rule_name) {
continue;
}
// Check lock-on-active
if activation.lock_on_active
&& self.locked_groups.contains(&activation.agenda_group)
{
continue;
}
// Check activation group
if let Some(ref group) = activation.activation_group {
if self.fired_activation_groups.contains(group) {
continue;
}
}
return Some(activation);
}
}
// No more activations in current focus, try to pop focus stack
if let Some(prev_focus) = self.focus_stack.pop() {
self.focus = prev_focus;
} else {
return None; // Agenda is empty
}
}
}
/// Mark a rule as fired
pub fn mark_rule_fired(&mut self, activation: &Activation) {
self.fired_rules.insert(activation.rule_name.clone());
// If has activation group, mark group as fired (no other rules in group can fire)
if let Some(ref group) = activation.activation_group {
self.fired_activation_groups.insert(group.clone());
}
// Lock the agenda group if lock-on-active
if activation.lock_on_active {
self.locked_groups.insert(activation.agenda_group.clone());
}
}
/// Check if a rule has already fired
pub fn has_fired(&self, rule_name: &str) -> bool {
self.fired_rules.contains(rule_name)
}
/// Set focus to a specific agenda group
pub fn set_focus(&mut self, group: String) {
if group != self.focus {
self.focus_stack.push(self.focus.clone());
self.focus = group;
}
}
/// Get current focus
pub fn get_focus(&self) -> &str {
&self.focus
}
/// Clear all agenda groups
pub fn clear(&mut self) {
self.activations.clear();
self.activations
.insert("MAIN".to_string(), BinaryHeap::new());
self.focus = "MAIN".to_string();
self.focus_stack.clear();
self.fired_rules.clear();
self.fired_activation_groups.clear();
self.locked_groups.clear();
}
/// Reset fired flags (for re-evaluation)
pub fn reset_fired_flags(&mut self) {
self.fired_rules.clear();
self.fired_activation_groups.clear();
self.locked_groups.clear();
}
/// Activate a ruleflow group (make rules in this group eligible to fire)
pub fn activate_ruleflow_group(&mut self, group: String) {
self.active_ruleflow_groups.insert(group);
}
/// Deactivate a ruleflow group
pub fn deactivate_ruleflow_group(&mut self, group: &str) {
self.active_ruleflow_groups.remove(group);
}
/// Check if ruleflow group is active
pub fn is_ruleflow_group_active(&self, group: &str) -> bool {
self.active_ruleflow_groups.contains(group)
}
/// Get agenda statistics
pub fn stats(&self) -> AgendaStats {
let total_activations: usize = self.activations.values().map(|heap| heap.len()).sum();
let groups = self.activations.len();
AgendaStats {
total_activations,
groups,
focus: self.focus.clone(),
fired_rules: self.fired_rules.len(),
fired_activation_groups: self.fired_activation_groups.len(),
active_ruleflow_groups: self.active_ruleflow_groups.len(),
}
}
}
impl Default for AdvancedAgenda {
fn default() -> Self {
Self::new()
}
}
/// Agenda statistics
#[derive(Debug, Clone)]
pub struct AgendaStats {
pub total_activations: usize,
pub groups: usize,
pub focus: String,
pub fired_rules: usize,
pub fired_activation_groups: usize,
pub active_ruleflow_groups: usize,
}
impl std::fmt::Display for AgendaStats {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Agenda Stats: {} activations, {} groups, focus='{}', {} fired rules",
self.total_activations, self.groups, self.focus, self.fired_rules
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_activation() {
let mut agenda = AdvancedAgenda::new();
let act1 = Activation::new("Rule1".to_string(), 10);
let act2 = Activation::new("Rule2".to_string(), 20);
agenda.add_activation(act1);
agenda.add_activation(act2);
// Higher salience fires first
let next = agenda.get_next_activation().unwrap();
assert_eq!(next.rule_name, "Rule2");
}
#[test]
fn test_activation_groups() {
let mut agenda = AdvancedAgenda::new();
let act1 =
Activation::new("Rule1".to_string(), 10).with_activation_group("group1".to_string());
let act2 =
Activation::new("Rule2".to_string(), 20).with_activation_group("group1".to_string());
agenda.add_activation(act1);
agenda.add_activation(act2);
// First activation fires
let first = agenda.get_next_activation().unwrap();
agenda.mark_rule_fired(&first);
// Second activation should be skipped (same group)
let second = agenda.get_next_activation();
assert!(second.is_none());
}
#[test]
fn test_agenda_groups() {
let mut agenda = AdvancedAgenda::new();
let act1 =
Activation::new("Rule1".to_string(), 10).with_agenda_group("group_a".to_string());
let act2 =
Activation::new("Rule2".to_string(), 20).with_agenda_group("group_b".to_string());
agenda.add_activation(act1);
agenda.add_activation(act2);
// MAIN is empty, nothing fires
assert!(agenda.get_next_activation().is_none());
// Set focus to group_a
agenda.set_focus("group_a".to_string());
let next = agenda.get_next_activation().unwrap();
assert_eq!(next.rule_name, "Rule1");
}
#[test]
fn test_auto_focus() {
let mut agenda = AdvancedAgenda::new();
let act = Activation::new("Rule1".to_string(), 10)
.with_agenda_group("special".to_string())
.with_auto_focus(true);
agenda.add_activation(act);
// Auto-focus should switch to "special"
assert_eq!(agenda.get_focus(), "special");
}
#[test]
fn test_ruleflow_groups() {
let mut agenda = AdvancedAgenda::new();
let act = Activation::new("Rule1".to_string(), 10).with_ruleflow_group("flow1".to_string());
// Without activating ruleflow group, activation is not added
agenda.add_activation(act.clone());
assert_eq!(agenda.stats().total_activations, 0);
// Activate ruleflow group
agenda.activate_ruleflow_group("flow1".to_string());
agenda.add_activation(act);
assert_eq!(agenda.stats().total_activations, 1);
}
}