Skip to main content

fabula_discovery/
session.rs

1use crate::corpus::TraceCorpus;
2use crate::score::{PatternScore, ScoredPattern};
3use crate::traits::{CandidateGenerator, PatternEvaluator, PatternFilter};
4
5/// Configuration for a discovery session.
6#[derive(Debug, Clone)]
7pub struct SessionConfig {
8    /// Maximum number of generate-evaluate rounds.
9    pub max_rounds: usize,
10    /// How many candidates to request per round.
11    pub candidates_per_round: usize,
12}
13
14impl Default for SessionConfig {
15    fn default() -> Self {
16        Self {
17            max_rounds: 10,
18            candidates_per_round: 50,
19        }
20    }
21}
22
23/// The result of a completed discovery session.
24#[derive(Debug, Clone)]
25pub struct SessionHistory {
26    /// How many rounds actually ran.
27    pub rounds: usize,
28    /// All candidates that were evaluated, in order.
29    pub all_scored: Vec<ScoredPattern<String, String>>,
30    /// Candidates that passed the filter.
31    pub accepted: Vec<ScoredPattern<String, String>>,
32}
33
34/// Orchestrates the generate-evaluate loop with configurable budgets.
35pub struct DiscoverySession {
36    config: SessionConfig,
37}
38
39impl DiscoverySession {
40    /// Create a session with the given configuration.
41    pub fn new(config: SessionConfig) -> Self {
42        Self { config }
43    }
44
45    /// Run the full discovery loop.
46    ///
47    /// 1. Generator proposes candidates
48    /// 2. Evaluators score each candidate
49    /// 3. Filter decides which to keep
50    /// 4. Scored results feed back to the generator
51    /// 5. Repeat for `max_rounds`
52    pub fn run(
53        &mut self,
54        corpus: &TraceCorpus,
55        mut generator: impl CandidateGenerator,
56        evaluators: Vec<Box<dyn PatternEvaluator>>,
57        filter: impl PatternFilter,
58    ) -> SessionHistory {
59        let mut all_scored = Vec::new();
60        let mut accepted = Vec::new();
61        let mut rounds_run = 0;
62
63        for round in 0..self.config.max_rounds {
64            let candidates = generator.generate(corpus, self.config.candidates_per_round);
65
66            if candidates.is_empty() {
67                break;
68            }
69
70            rounds_run += 1;
71            let mut round_scored = Vec::new();
72            for pattern in candidates {
73                let mut scores = std::collections::HashMap::new();
74                for evaluator in &evaluators {
75                    let value = evaluator.evaluate(&pattern, corpus);
76                    scores.insert(evaluator.name().to_string(), value);
77                }
78
79                let scored = ScoredPattern {
80                    pattern,
81                    score: PatternScore {
82                        scores,
83                        round,
84                        generator: generator.name().to_string(),
85                    },
86                };
87
88                if filter.accept(&scored) {
89                    accepted.push(scored.clone());
90                }
91
92                round_scored.push(scored);
93            }
94
95            generator.feedback(&round_scored);
96            all_scored.extend(round_scored);
97        }
98
99        SessionHistory {
100            rounds: rounds_run,
101            all_scored,
102            accepted,
103        }
104    }
105}