Skip to main content

crackle_runtime/
kiln.rs

1use crate::task::{CrackleTask, TaskMetadata, TaskOutput, Timestamp};
2use crate::patterns::{
3    ClusteringPattern, ConservationPattern, CorrelationPattern, CracklePattern, PhaseTransitionPattern,
4};
5use crate::profile::ThermalProfile;
6
7/// A completed task entry stored in the kiln.
8#[derive(Debug, Clone)]
9pub struct TaskEntry {
10    /// The task label.
11    pub label: String,
12    /// Metrics produced during firing.
13    pub metrics: Vec<(String, f64)>,
14    /// Metrics produced during cooling (may differ from firing).
15    pub cooled_metrics: Vec<(String, f64)>,
16    /// Task metadata.
17    pub metadata: TaskMetadata,
18}
19
20impl TaskEntry {
21    /// All metrics: cooled metrics override firing metrics of the same name.
22    pub fn all_metrics(&self) -> Vec<(String, f64)> {
23        let mut result = self.cooled_metrics.clone();
24        for (name, val) in &self.metrics {
25            if !result.iter().any(|(n, _)| n == name) {
26                result.push((name.clone(), *val));
27            }
28        }
29        result
30    }
31}
32
33/// The kiln — the runtime that fires tasks and cools them to detect patterns.
34///
35/// Like a pottery kiln, this runtime has two distinct phases:
36///
37/// 1. **Firing**: Tasks execute (`fire()`), producing outputs and metrics.
38///    This is the hot phase — the work gets done.
39///
40/// 2. **Cooling**: After all tasks have fired, the runtime examines the completed
41///    tasks for emergent patterns. The crackle glaze forms in the cooling, not the firing.
42///
43/// # Example
44///
45/// ```
46/// use crackle_runtime::{CrackleTask, Kiln, ThermalProfile, TaskOutput};
47///
48/// struct MyTask { x: f64 }
49/// impl CrackleTask for MyTask {
50///     type Output = f64;
51///     fn fire(&self) -> TaskOutput<Self::Output> {
52///         TaskOutput::new(self.x, vec![("value".into(), self.x)])
53///     }
54/// }
55///
56/// let mut kiln = Kiln::new(ThermalProfile::default());
57/// kiln.fire_task(MyTask { x: 1.0 });
58/// kiln.fire_task(MyTask { x: 2.0 });
59/// kiln.fire_task(MyTask { x: 3.0 });
60///
61/// let patterns = kiln.cool();
62/// ```
63pub struct Kiln {
64    profile: ThermalProfile,
65    entries: Vec<TaskEntry>,
66    cooled: bool,
67}
68
69impl Kiln {
70    /// Create a new kiln with the given thermal profile.
71    pub fn new(profile: ThermalProfile) -> Self {
72        Kiln {
73            profile,
74            entries: Vec::new(),
75            cooled: false,
76        }
77    }
78
79    /// Create a kiln with default thermal profile.
80    pub fn default_profile() -> Self {
81        Kiln::new(ThermalProfile::default())
82    }
83
84    /// Fire a single task and record its output.
85    ///
86    /// Returns the task's output value.
87    ///
88    /// # Panics
89    ///
90    /// Panics if called after `cool()`.
91    pub fn fire_task<T: CrackleTask>(&self, task: T) -> TaskOutput<T::Output> {
92        assert!(!self.cooled, "cannot fire tasks after cooling");
93
94        task.fire()
95    }
96
97    /// Fire a task and record it in the kiln for later cooling.
98    ///
99    /// This stores the task's metrics internally so patterns can be detected
100    /// during the cooling phase.
101    pub fn fire_and_record<T: CrackleTask>(&mut self, task: T) -> TaskOutput<T::Output> {
102        assert!(!self.cooled, "cannot fire tasks after cooling");
103
104        let label = task.label();
105        let fired_at = Timestamp::now();
106        let start = std::time::Instant::now();
107
108        let output = task.fire();
109
110        let fire_duration = start.elapsed();
111        let metadata = TaskMetadata {
112            fired_at,
113            cooled_at: None,
114            fire_duration,
115            label: label.clone(),
116        };
117
118        let entry = TaskEntry {
119            label,
120            metrics: output.metrics.clone(),
121            cooled_metrics: vec![],
122            metadata,
123        };
124
125        self.entries.push(entry);
126        output
127    }
128
129    /// Fire multiple tasks in sequence and record them all.
130    pub fn fire_all<T: CrackleTask>(&mut self, tasks: Vec<T>) -> Vec<TaskOutput<T::Output>> {
131        tasks
132            .into_iter()
133            .map(|task| self.fire_and_record(task))
134            .collect()
135    }
136
137    /// Add a pre-computed task entry directly (useful for testing).
138    pub fn add_entry(&mut self, label: impl Into<String>, metrics: Vec<(String, f64)>) {
139        let label = label.into();
140        let metadata = TaskMetadata::new(&label);
141        self.entries.push(TaskEntry {
142            label,
143            metrics,
144            cooled_metrics: vec![],
145            metadata,
146        });
147    }
148
149    /// The number of tasks currently in the kiln.
150    pub fn task_count(&self) -> usize {
151        self.entries.len()
152    }
153
154    /// Get all task entries.
155    pub fn entries(&self) -> &[TaskEntry] {
156        &self.entries
157    }
158
159    /// Cool the kiln: run pattern detection across all completed tasks.
160    ///
161    /// This is where the beauty emerges. Just as a pottery kiln's crackle glaze
162    /// forms during cooling, the patterns that crackle-runtime detects are only
163    /// visible after the heat of execution has passed.
164    ///
165    /// Returns all detected patterns.
166    pub fn cool(&mut self) -> Vec<CracklePattern> {
167        self.cooled = true;
168        let mut patterns = Vec::new();
169
170        if self.entries.len() < self.profile.rate.min_tasks_for_detection() {
171            return patterns;
172        }
173
174        let labels: Vec<String> = self.entries.iter().map(|e| e.label.clone()).collect();
175        let metrics: Vec<Vec<(String, f64)>> = self.entries.iter().map(|e| e.all_metrics()).collect();
176
177        // Run each task's cool() phase — set cooled timestamps
178        let cooled_ts = Timestamp::now();
179        for entry in &mut self.entries {
180            entry.metadata.cooled_at = Some(cooled_ts);
181        }
182
183        if self.profile.detect_clustering {
184            let p = ClusteringPattern::detect(
185                &labels,
186                &metrics,
187                self.profile.rate.cluster_threshold(),
188            );
189            patterns.extend(p);
190        }
191
192        if self.profile.detect_phase_transitions {
193            let p = PhaseTransitionPattern::detect(
194                &labels,
195                &metrics,
196                self.profile.rate.phase_transition_sensitivity(),
197            );
198            patterns.extend(p);
199        }
200
201        if self.profile.detect_conservation {
202            let p = ConservationPattern::detect(
203                &labels,
204                &metrics,
205                self.profile.rate.conservation_tolerance(),
206            );
207            patterns.extend(p);
208        }
209
210        if self.profile.detect_correlations {
211            let p = CorrelationPattern::detect(
212                &labels,
213                &metrics,
214                self.profile.rate.correlation_threshold(),
215            );
216            patterns.extend(p);
217        }
218
219        // Sort by confidence descending
220        patterns.sort_by(|a, b| b.confidence().partial_cmp(&a.confidence()).unwrap_or(std::cmp::Ordering::Equal));
221
222        patterns
223    }
224
225    /// Check if the kiln has been cooled.
226    pub fn is_cooled(&self) -> bool {
227        self.cooled
228    }
229
230    /// Get the thermal profile.
231    pub fn profile(&self) -> &ThermalProfile {
232        &self.profile
233    }
234
235    /// Reset the kiln for a new firing cycle.
236    pub fn reset(&mut self) {
237        self.entries.clear();
238        self.cooled = false;
239    }
240}