Skip to main content

wm_core/
mutable.rs

1//! Mutable Structures — Phase 6
2//!
3//! Makes previously fixed structures learnable:
4//! 1. `GanaRegistry` — Gana taxonomy drift based on co-usage patterns
5//! 2. `DynamicGalaxyRegistry` — Dynamic galaxy creation from memory clustering
6//! 3. `LearnedDreamCycle` — Learned dream cycle phase selection
7//! 4. `LearnedCycleStrategy` — Learned autonomous cycle strategies
8//! 5. Phase effectiveness measurement and feedback
9
10#![allow(clippy::significant_drop_tightening)]
11
12use crate::Gana;
13use serde::{Deserialize, Serialize};
14use std::collections::HashMap;
15
16// ── 6.1: GanaRegistry with Drift ──────────────────────────────────────
17
18/// Tracks co-usage patterns between Ganas and allows taxonomy drift.
19///
20/// When two Ganas are frequently used together (e.g., memory.create + memory.search),
21/// the registry records the co-usage. Over time, if a Gana pair exceeds a threshold,
22/// the registry can suggest merging or reorganizing the taxonomy.
23///
24/// This implements the "mutable structures" principle: the taxonomy itself
25/// becomes a learnable structure rather than a fixed design decision.
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct GanaRegistry {
28    /// Co-usage matrix: "a:b" → count (string key for JSON serialization)
29    co_usage: HashMap<String, u64>,
30    /// Co-usage pair lookup: (a, b) → string key (not serialized, rebuilt on load)
31    #[serde(skip)]
32    co_usage_pairs: HashMap<(u8, u8), String>,
33    /// Per-Gana total usage count
34    usage_counts: HashMap<u8, u64>,
35    /// Per-Gana success rate (rolling average)
36    success_rates: HashMap<u8, f32>,
37    /// Drift threshold — when co-usage exceeds this, suggest reorganization
38    drift_threshold: u64,
39    /// Whether drift is enabled
40    drift_enabled: bool,
41    /// Suggested merges from drift analysis
42    suggested_merges: Vec<GanaMerge>,
43}
44
45/// A suggested Gana merge from drift analysis.
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct GanaMerge {
48    /// First Gana index
49    pub gana_a: u8,
50    /// Second Gana index
51    pub gana_b: u8,
52    /// Co-usage count
53    pub co_usage_count: u64,
54    /// Confidence (0.0-1.0)
55    pub confidence: f32,
56}
57
58impl GanaRegistry {
59    /// Create a new GanaRegistry with default settings.
60    #[must_use]
61    pub fn new() -> Self {
62        Self {
63            co_usage: HashMap::new(),
64            co_usage_pairs: HashMap::new(),
65            usage_counts: HashMap::new(),
66            success_rates: HashMap::new(),
67            drift_threshold: 100,
68            drift_enabled: true,
69            suggested_merges: Vec::new(),
70        }
71    }
72
73    /// Create a registry with a custom drift threshold.
74    #[must_use]
75    pub fn with_threshold(drift_threshold: u64) -> Self {
76        Self {
77            co_usage: HashMap::new(),
78            co_usage_pairs: HashMap::new(),
79            usage_counts: HashMap::new(),
80            success_rates: HashMap::new(),
81            drift_threshold,
82            drift_enabled: true,
83            suggested_merges: Vec::new(),
84        }
85    }
86
87    /// Record a tool dispatch within a Gana.
88    pub fn record_usage(&mut self, gana: Gana, success: bool) {
89        let idx = gana as u8;
90        *self.usage_counts.entry(idx).or_insert(0) += 1;
91
92        // Update success rate (rolling average)
93        let current = self.success_rates.get(&idx).copied().unwrap_or(0.5);
94        let count = self.usage_counts[&idx];
95        let new_rate = if success {
96            current + (1.0 - current) / count as f32
97        } else {
98            current * (1.0 - 1.0 / count as f32)
99        };
100        self.success_rates.insert(idx, new_rate);
101    }
102
103    /// Record co-usage of two Ganas in the same session/context.
104    pub fn record_co_usage(&mut self, gana_a: Gana, gana_b: Gana) {
105        let (a, b) = if gana_a as u8 <= gana_b as u8 {
106            (gana_a as u8, gana_b as u8)
107        } else {
108            (gana_b as u8, gana_a as u8)
109        };
110        let key = format!("{a}:{b}");
111        self.co_usage_pairs.insert((a, b), key.clone());
112        *self.co_usage.entry(key).or_insert(0) += 1;
113
114        // Check for drift
115        if self.drift_enabled {
116            let count = self.co_usage[&format!("{a}:{b}")];
117            if count == self.drift_threshold {
118                self.suggested_merges.push(GanaMerge {
119                    gana_a: a,
120                    gana_b: b,
121                    co_usage_count: count,
122                    confidence: 0.5,
123                });
124            } else if count > self.drift_threshold && count % self.drift_threshold == 0 {
125                // Increase confidence
126                if let Some(merge) = self
127                    .suggested_merges
128                    .iter_mut()
129                    .find(|m| m.gana_a == a && m.gana_b == b)
130                {
131                    merge.co_usage_count = count;
132                    merge.confidence = (merge.confidence + 0.1).min(1.0);
133                }
134            }
135        }
136    }
137
138    /// Get the success rate for a Gana.
139    #[must_use]
140    pub fn success_rate(&self, gana: Gana) -> f32 {
141        self.success_rates
142            .get(&(gana as u8))
143            .copied()
144            .unwrap_or(0.5)
145    }
146
147    /// Get the usage count for a Gana.
148    #[must_use]
149    pub fn usage_count(&self, gana: Gana) -> u64 {
150        self.usage_counts.get(&(gana as u8)).copied().unwrap_or(0)
151    }
152
153    /// Get all usage counts (Gana index → count).
154    #[must_use]
155    pub const fn usage_counts(&self) -> &HashMap<u8, u64> {
156        &self.usage_counts
157    }
158
159    /// Get all co-usage counts (string key "a:b" → count).
160    #[must_use]
161    pub const fn co_usage(&self) -> &HashMap<String, u64> {
162        &self.co_usage
163    }
164
165    /// Get the co-usage count between two Ganas.
166    #[must_use]
167    pub fn co_usage_count(&self, gana_a: Gana, gana_b: Gana) -> u64 {
168        let (a, b) = if gana_a as u8 <= gana_b as u8 {
169            (gana_a as u8, gana_b as u8)
170        } else {
171            (gana_b as u8, gana_a as u8)
172        };
173        self.co_usage_pairs
174            .get(&(a, b))
175            .and_then(|key| self.co_usage.get(key))
176            .copied()
177            .unwrap_or(0)
178    }
179
180    /// Get all suggested merges from drift analysis.
181    #[must_use]
182    pub fn suggested_merges(&self) -> &[GanaMerge] {
183        &self.suggested_merges
184    }
185
186    /// Analyze drift and return the top N suggested reorganizations.
187    #[must_use]
188    pub fn analyze_drift(&self, top_n: usize) -> Vec<GanaMerge> {
189        let mut merges = self.suggested_merges.clone();
190        merges.sort_by_key(|x| std::cmp::Reverse(x.co_usage_count));
191        merges.truncate(top_n);
192        merges
193    }
194
195    /// Rebuild the co_usage_pairs lookup from co_usage data.
196    ///
197    /// Call this after deserializing a GanaRegistry, since `co_usage_pairs`
198    /// is skipped during serialization.
199    pub fn rebuild_pairs(&mut self) {
200        self.co_usage_pairs.clear();
201        for key in self.co_usage.keys() {
202            let parts: Vec<&str> = key.split(':').collect();
203            if parts.len() == 2 {
204                if let (Ok(a), Ok(b)) = (parts[0].parse::<u8>(), parts[1].parse::<u8>()) {
205                    self.co_usage_pairs.insert((a, b), key.clone());
206                }
207            }
208        }
209    }
210
211    /// Clear all co-usage data (e.g., after applying a reorganization).
212    pub fn clear(&mut self) {
213        self.co_usage.clear();
214        self.co_usage_pairs.clear();
215        self.usage_counts.clear();
216        self.success_rates.clear();
217        self.suggested_merges.clear();
218    }
219
220    /// Get a snapshot of the registry as JSON.
221    #[must_use]
222    pub fn snapshot(&self) -> serde_json::Value {
223        serde_json::json!({
224            "total_ganas_tracked": self.usage_counts.len(),
225            "total_co_usage_pairs": self.co_usage.len(),
226            "suggested_merges": self.suggested_merges.len(),
227            "drift_threshold": self.drift_threshold,
228            "drift_enabled": self.drift_enabled,
229        })
230    }
231}
232
233impl Default for GanaRegistry {
234    fn default() -> Self {
235        Self::new()
236    }
237}
238
239// ── 6.2: Dynamic Galaxy Registry ──────────────────────────────────────
240
241/// A dynamically created galaxy from memory clustering.
242#[derive(Debug, Clone, Serialize, Deserialize)]
243pub struct DynamicGalaxy {
244    /// Unique ID for the dynamic galaxy
245    pub id: String,
246    /// Human-readable name
247    pub name: String,
248    /// Description of what this galaxy contains
249    pub description: String,
250    /// Tags that triggered the clustering
251    pub cluster_tags: Vec<String>,
252    /// Number of memories in this cluster
253    pub memory_count: usize,
254    /// When this galaxy was created (Unix timestamp)
255    pub created_at: u64,
256    /// Effectiveness score (how useful this cluster has been)
257    pub effectiveness: f32,
258}
259
260/// Registry for dynamic galaxies created from memory clustering.
261///
262/// Instead of a fixed set of 14 galaxies, the system can create new
263/// virtual galaxies when memory clustering reveals natural groupings
264/// that don't fit the existing taxonomy.
265#[derive(Debug, Clone, Serialize, Deserialize)]
266pub struct DynamicGalaxyRegistry {
267    /// Registered dynamic galaxies
268    galaxies: HashMap<String, DynamicGalaxy>,
269    /// Minimum cluster size to create a dynamic galaxy
270    min_cluster_size: usize,
271    /// Maximum number of dynamic galaxies
272    max_galaxies: usize,
273    /// Effectiveness threshold for pruning
274    prune_threshold: f32,
275}
276
277impl DynamicGalaxyRegistry {
278    /// Create a new dynamic galaxy registry.
279    #[must_use]
280    pub fn new() -> Self {
281        Self {
282            galaxies: HashMap::new(),
283            min_cluster_size: 10,
284            max_galaxies: 20,
285            prune_threshold: 0.1,
286        }
287    }
288
289    /// Create a registry with custom settings.
290    #[must_use]
291    pub fn with_config(min_cluster_size: usize, max_galaxies: usize, prune_threshold: f32) -> Self {
292        Self {
293            galaxies: HashMap::new(),
294            min_cluster_size,
295            max_galaxies,
296            prune_threshold,
297        }
298    }
299
300    /// Try to create a dynamic galaxy from a memory cluster.
301    ///
302    /// Returns the created galaxy, or `None` if the cluster is too small
303    /// or the registry is full.
304    pub fn try_create(
305        &mut self,
306        name: &str,
307        description: &str,
308        cluster_tags: Vec<String>,
309        memory_count: usize,
310    ) -> Option<&DynamicGalaxy> {
311        if memory_count < self.min_cluster_size {
312            return None;
313        }
314
315        if self.galaxies.len() >= self.max_galaxies {
316            // Try to prune ineffective galaxies first
317            self.prune();
318            if self.galaxies.len() >= self.max_galaxies {
319                return None;
320            }
321        }
322
323        let id = format!("dyn_{}", name.to_lowercase().replace(' ', "_"));
324        if self.galaxies.contains_key(&id) {
325            // Update existing
326            if let Some(g) = self.galaxies.get_mut(&id) {
327                g.memory_count = memory_count;
328            }
329            return self.galaxies.get(&id);
330        }
331
332        let timestamp = std::time::SystemTime::now()
333            .duration_since(std::time::UNIX_EPOCH)
334            .map_or(0, |d| d.as_secs());
335
336        let galaxy = DynamicGalaxy {
337            id: id.clone(),
338            name: name.to_string(),
339            description: description.to_string(),
340            cluster_tags,
341            memory_count,
342            created_at: timestamp,
343            effectiveness: 0.5,
344        };
345
346        self.galaxies.insert(id.clone(), galaxy);
347        self.galaxies.get(&id)
348    }
349
350    /// Get a dynamic galaxy by ID.
351    #[must_use]
352    pub fn get(&self, id: &str) -> Option<&DynamicGalaxy> {
353        self.galaxies.get(id)
354    }
355
356    /// Get all dynamic galaxies.
357    #[must_use]
358    pub fn all(&self) -> Vec<&DynamicGalaxy> {
359        self.galaxies.values().collect()
360    }
361
362    /// Update the effectiveness of a dynamic galaxy.
363    pub fn update_effectiveness(&mut self, id: &str, effectiveness: f32) {
364        if let Some(g) = self.galaxies.get_mut(id) {
365            g.effectiveness = effectiveness;
366        }
367    }
368
369    /// Prune galaxies below the effectiveness threshold.
370    ///
371    /// Returns the number of galaxies pruned.
372    pub fn prune(&mut self) -> usize {
373        let before = self.galaxies.len();
374        self.galaxies
375            .retain(|_, g| g.effectiveness >= self.prune_threshold);
376        before - self.galaxies.len()
377    }
378
379    /// Number of dynamic galaxies.
380    #[must_use]
381    pub fn len(&self) -> usize {
382        self.galaxies.len()
383    }
384
385    /// Number of dynamic galaxies (alias for `len()`).
386    #[must_use]
387    pub fn galaxy_count(&self) -> usize {
388        self.galaxies.len()
389    }
390
391    /// Whether the registry is empty.
392    #[must_use]
393    pub fn is_empty(&self) -> bool {
394        self.galaxies.is_empty()
395    }
396}
397
398impl Default for DynamicGalaxyRegistry {
399    fn default() -> Self {
400        Self::new()
401    }
402}
403
404// ── 6.3: Learned Dream Cycle ──────────────────────────────────────────
405
406/// Effectiveness record for a dream phase.
407#[derive(Debug, Clone, Serialize, Deserialize)]
408pub struct PhaseEffectiveness {
409    /// Number of times this phase has run
410    pub runs: u64,
411    /// Number of times this phase produced useful results
412    pub useful_results: u64,
413    /// Average improvement score (0.0-1.0)
414    pub avg_improvement: f32,
415    /// Average duration in ms
416    pub avg_duration_ms: u64,
417}
418
419impl PhaseEffectiveness {
420    /// Create a new effectiveness record.
421    #[must_use]
422    pub const fn new() -> Self {
423        Self {
424            runs: 0,
425            useful_results: 0,
426            avg_improvement: 0.0,
427            avg_duration_ms: 0,
428        }
429    }
430
431    /// Effectiveness score (0.0-1.0).
432    #[must_use]
433    pub fn score(&self) -> f32 {
434        if self.runs == 0 {
435            return 0.5;
436        }
437        let success_rate = self.useful_results as f32 / self.runs as f32;
438        success_rate.midpoint(self.avg_improvement)
439    }
440
441    /// Record a phase execution.
442    pub fn record(&mut self, useful: bool, improvement: f32, duration_ms: u64) {
443        let n = self.runs as f32;
444        self.avg_improvement = self.avg_improvement.mul_add(n, improvement) / (n + 1.0);
445        self.avg_duration_ms =
446            ((self.avg_duration_ms as f32).mul_add(n, duration_ms as f32) / (n + 1.0)) as u64;
447        self.runs += 1;
448        if useful {
449            self.useful_results += 1;
450        }
451    }
452}
453
454impl Default for PhaseEffectiveness {
455    fn default() -> Self {
456        Self::new()
457    }
458}
459
460/// Learned dream cycle — selects dream phases based on historical effectiveness.
461///
462/// Instead of running all 12 phases in fixed order, the learned dream cycle
463/// uses historical effectiveness data to:
464/// 1. Prioritize phases that have been most useful
465/// 2. Skip phases with consistently low effectiveness
466/// 3. Adapt the phase order based on current memory state
467#[derive(Debug, Clone, Serialize, Deserialize)]
468pub struct LearnedDreamCycle {
469    /// Effectiveness records per phase (indexed by phase ordinal)
470    phase_effectiveness: HashMap<u8, PhaseEffectiveness>,
471    /// Minimum effectiveness to keep a phase in the cycle
472    min_effectiveness: f32,
473    /// Minimum runs before making skip decisions
474    min_runs: u64,
475    /// Whether learning is enabled
476    learning_enabled: bool,
477    /// Current phase ordering (learned)
478    phase_order: Vec<u8>,
479}
480
481impl LearnedDreamCycle {
482    /// Create a new learned dream cycle with all phases in default order.
483    #[must_use]
484    pub fn new() -> Self {
485        let default_order: Vec<u8> = (0..12u8).collect();
486        Self {
487            phase_effectiveness: HashMap::new(),
488            min_effectiveness: 0.2,
489            min_runs: 5,
490            learning_enabled: true,
491            phase_order: default_order,
492        }
493    }
494
495    /// Create with custom settings.
496    #[must_use]
497    pub fn with_config(min_effectiveness: f32, min_runs: u64, learning_enabled: bool) -> Self {
498        Self {
499            phase_effectiveness: HashMap::new(),
500            min_effectiveness,
501            min_runs,
502            learning_enabled,
503            phase_order: (0..12u8).collect(),
504        }
505    }
506
507    /// Record a phase execution result.
508    pub fn record_phase(
509        &mut self,
510        phase_idx: u8,
511        useful: bool,
512        improvement: f32,
513        duration_ms: u64,
514    ) {
515        let record = self.phase_effectiveness.entry(phase_idx).or_default();
516        record.record(useful, improvement, duration_ms);
517
518        // Reorder phases if learning is enabled
519        if self.learning_enabled {
520            self.update_phase_order();
521        }
522    }
523
524    /// Get the learned phase order (phases sorted by effectiveness, descending).
525    #[must_use]
526    pub fn phase_order(&self) -> &[u8] {
527        &self.phase_order
528    }
529
530    /// Get the phases to run (filtering out ineffective ones).
531    #[must_use]
532    pub fn phases_to_run(&self) -> Vec<u8> {
533        self.phase_order
534            .iter()
535            .filter(|&&idx| {
536                if let Some(eff) = self.phase_effectiveness.get(&idx) {
537                    if eff.runs >= self.min_runs {
538                        return eff.score() >= self.min_effectiveness;
539                    }
540                }
541                true // Keep phases without enough data
542            })
543            .copied()
544            .collect()
545    }
546
547    /// Get effectiveness data for a phase.
548    #[must_use]
549    pub fn effectiveness(&self, phase_idx: u8) -> Option<&PhaseEffectiveness> {
550        self.phase_effectiveness.get(&phase_idx)
551    }
552
553    /// Update the phase order based on effectiveness scores.
554    fn update_phase_order(&mut self) {
555        let mut scored: Vec<(u8, f32)> = (0..12u8)
556            .map(|idx| {
557                let score = self
558                    .phase_effectiveness
559                    .get(&idx)
560                    .map_or(0.5, PhaseEffectiveness::score);
561                (idx, score)
562            })
563            .collect();
564        scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
565        self.phase_order = scored.into_iter().map(|(idx, _)| idx).collect();
566    }
567
568    /// Get a snapshot of all phase effectiveness data.
569    #[must_use]
570    pub fn snapshot(&self) -> serde_json::Value {
571        let phases: Vec<serde_json::Value> = (0..12u8)
572            .map(|idx| {
573                if let Some(eff) = self.phase_effectiveness.get(&idx) {
574                    serde_json::json!({
575                        "phase": idx,
576                        "runs": eff.runs,
577                        "useful": eff.useful_results,
578                        "score": eff.score(),
579                        "avg_improvement": eff.avg_improvement,
580                        "avg_duration_ms": eff.avg_duration_ms,
581                    })
582                } else {
583                    serde_json::json!({"phase": idx, "runs": 0})
584                }
585            })
586            .collect();
587
588        serde_json::json!({
589            "phases": phases,
590            "phase_order": self.phase_order,
591            "phases_to_run": self.phases_to_run(),
592            "min_effectiveness": self.min_effectiveness,
593            "learning_enabled": self.learning_enabled,
594        })
595    }
596}
597
598impl Default for LearnedDreamCycle {
599    fn default() -> Self {
600        Self::new()
601    }
602}
603
604// ── 6.4: Learned Cycle Strategy ───────────────────────────────────────
605
606/// Strategy for autonomous cycle selection.
607#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
608pub enum CycleStrategy {
609    /// Run all cycles in fixed order (default)
610    FixedOrder,
611    /// Run cycles based on priority learned from effectiveness
612    PriorityBased,
613    /// Run only the most effective cycle type
614    BestOnly,
615    /// Adaptive: use priority-based with exploration
616    Adaptive,
617}
618
619/// Effectiveness record for an autonomous cycle type.
620#[derive(Debug, Clone, Serialize, Deserialize)]
621pub struct CycleEffectiveness {
622    /// Number of times this cycle type has run
623    pub runs: u64,
624    /// Number of useful proposals generated
625    pub proposals_generated: u64,
626    /// Average usefulness score (0.0-1.0)
627    pub avg_usefulness: f32,
628    /// Average duration in ms
629    pub avg_duration_ms: u64,
630}
631
632impl CycleEffectiveness {
633    /// Create a new cycle effectiveness record.
634    #[must_use]
635    pub const fn new() -> Self {
636        Self {
637            runs: 0,
638            proposals_generated: 0,
639            avg_usefulness: 0.0,
640            avg_duration_ms: 0,
641        }
642    }
643
644    /// Effectiveness score (0.0-1.0).
645    #[must_use]
646    pub fn score(&self) -> f32 {
647        if self.runs == 0 {
648            return 0.5;
649        }
650        let proposal_rate = self.proposals_generated as f32 / self.runs as f32;
651        proposal_rate.midpoint(self.avg_usefulness)
652    }
653
654    /// Record a cycle execution.
655    pub fn record(&mut self, proposals: u64, usefulness: f32, duration_ms: u64) {
656        let n = self.runs as f32;
657        self.avg_usefulness = self.avg_usefulness.mul_add(n, usefulness) / (n + 1.0);
658        self.avg_duration_ms =
659            ((self.avg_duration_ms as f32).mul_add(n, duration_ms as f32) / (n + 1.0)) as u64;
660        self.runs += 1;
661        self.proposals_generated += proposals;
662    }
663}
664
665impl Default for CycleEffectiveness {
666    fn default() -> Self {
667        Self::new()
668    }
669}
670
671/// Learned autonomous cycle strategy.
672///
673/// Instead of hardcoded cycle ordering, the bicameral system learns
674/// which cycle types are most effective and adapts the strategy.
675#[derive(Debug, Clone, Serialize, Deserialize)]
676pub struct LearnedCycleStrategy {
677    /// Effectiveness per cycle type (indexed by CycleType ordinal)
678    cycle_effectiveness: HashMap<u8, CycleEffectiveness>,
679    /// Current strategy
680    strategy: CycleStrategy,
681    /// Exploration rate (for Adaptive strategy)
682    exploration_rate: f32,
683    /// Minimum runs before switching from FixedOrder
684    min_runs: u64,
685    /// Learned priority order (cycle type ordinals, highest priority first)
686    priority_order: Vec<u8>,
687}
688
689impl LearnedCycleStrategy {
690    /// Create a new learned cycle strategy with default settings.
691    #[must_use]
692    pub fn new() -> Self {
693        Self {
694            cycle_effectiveness: HashMap::new(),
695            strategy: CycleStrategy::FixedOrder,
696            exploration_rate: 0.1,
697            min_runs: 10,
698            priority_order: (0..8u8).collect(),
699        }
700    }
701
702    /// Create with a specific strategy.
703    #[must_use]
704    pub fn with_strategy(strategy: CycleStrategy) -> Self {
705        Self {
706            cycle_effectiveness: HashMap::new(),
707            strategy,
708            exploration_rate: 0.1,
709            min_runs: 10,
710            priority_order: (0..8u8).collect(),
711        }
712    }
713
714    /// Record a cycle execution.
715    pub fn record_cycle(
716        &mut self,
717        cycle_type_idx: u8,
718        proposals: u64,
719        usefulness: f32,
720        duration_ms: u64,
721    ) {
722        let record = self.cycle_effectiveness.entry(cycle_type_idx).or_default();
723        record.record(proposals, usefulness, duration_ms);
724
725        // Update strategy based on data
726        if self.strategy == CycleStrategy::FixedOrder {
727            let total_runs: u64 = self.cycle_effectiveness.values().map(|e| e.runs).sum();
728            if total_runs >= self.min_runs {
729                self.strategy = CycleStrategy::PriorityBased;
730                self.update_priority_order();
731            }
732        } else if matches!(
733            self.strategy,
734            CycleStrategy::PriorityBased | CycleStrategy::Adaptive
735        ) {
736            self.update_priority_order();
737        }
738    }
739
740    /// Get the current strategy.
741    #[must_use]
742    pub const fn strategy(&self) -> CycleStrategy {
743        self.strategy
744    }
745
746    /// Get the priority order for cycle execution.
747    #[must_use]
748    pub fn priority_order(&self) -> &[u8] {
749        &self.priority_order
750    }
751
752    /// Get the cycle types to run, in order.
753    ///
754    /// For `FixedOrder`, returns all types in default order.
755    /// For `PriorityBased`, returns types sorted by effectiveness.
756    /// For `BestOnly`, returns only the top type.
757    /// For `Adaptive`, returns priority order with occasional exploration.
758    #[must_use]
759    pub fn cycles_to_run(&self) -> Vec<u8> {
760        match self.strategy {
761            CycleStrategy::FixedOrder => (0..8u8).collect(),
762            CycleStrategy::PriorityBased | CycleStrategy::Adaptive => {
763                if self.strategy == CycleStrategy::Adaptive {
764                    // With probability exploration_rate, include a random cycle
765                    // (simplified: just return full priority order)
766                }
767                self.priority_order.clone()
768            }
769            CycleStrategy::BestOnly => self.priority_order.first().copied().into_iter().collect(),
770        }
771    }
772
773    /// Get effectiveness data for a cycle type.
774    #[must_use]
775    pub fn effectiveness(&self, cycle_type_idx: u8) -> Option<&CycleEffectiveness> {
776        self.cycle_effectiveness.get(&cycle_type_idx)
777    }
778
779    /// Set the strategy manually.
780    pub fn set_strategy(&mut self, strategy: CycleStrategy) {
781        self.strategy = strategy;
782        if matches!(
783            strategy,
784            CycleStrategy::PriorityBased | CycleStrategy::Adaptive
785        ) {
786            self.update_priority_order();
787        }
788    }
789
790    /// Update the priority order based on effectiveness scores.
791    fn update_priority_order(&mut self) {
792        let mut scored: Vec<(u8, f32)> = (0..8u8)
793            .map(|idx| {
794                let score = self
795                    .cycle_effectiveness
796                    .get(&idx)
797                    .map_or(0.5, CycleEffectiveness::score);
798                (idx, score)
799            })
800            .collect();
801        scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
802        self.priority_order = scored.into_iter().map(|(idx, _)| idx).collect();
803    }
804
805    /// Get a snapshot of the strategy state.
806    #[must_use]
807    pub fn snapshot(&self) -> serde_json::Value {
808        let cycles: Vec<serde_json::Value> = (0..8u8)
809            .map(|idx| {
810                if let Some(eff) = self.cycle_effectiveness.get(&idx) {
811                    serde_json::json!({
812                        "cycle_type": idx,
813                        "runs": eff.runs,
814                        "proposals": eff.proposals_generated,
815                        "score": eff.score(),
816                        "avg_usefulness": eff.avg_usefulness,
817                    })
818                } else {
819                    serde_json::json!({"cycle_type": idx, "runs": 0})
820                }
821            })
822            .collect();
823
824        serde_json::json!({
825            "strategy": format!("{:?}", self.strategy),
826            "cycles": cycles,
827            "priority_order": self.priority_order,
828            "cycles_to_run": self.cycles_to_run(),
829            "exploration_rate": self.exploration_rate,
830        })
831    }
832}
833
834impl Default for LearnedCycleStrategy {
835    fn default() -> Self {
836        Self::new()
837    }
838}
839
840// ── Tests ─────────────────────────────────────────────────────────────
841
842#[cfg(test)]
843mod tests {
844    use super::*;
845
846    // ── GanaRegistry Tests ──
847
848    #[test]
849    fn gana_registry_record_usage() {
850        let mut registry = GanaRegistry::new();
851        registry.record_usage(Gana::Horn, true);
852        registry.record_usage(Gana::Horn, true);
853        registry.record_usage(Gana::Horn, false);
854
855        assert_eq!(registry.usage_count(Gana::Horn), 3);
856        let rate = registry.success_rate(Gana::Horn);
857        assert!(rate > 0.5); // 2/3 success
858    }
859
860    #[test]
861    fn gana_registry_co_usage() {
862        let mut registry = GanaRegistry::new();
863        registry.record_co_usage(Gana::Horn, Gana::Encampment);
864        registry.record_co_usage(Gana::Horn, Gana::Encampment);
865        registry.record_co_usage(Gana::Horn, Gana::Encampment);
866
867        assert_eq!(registry.co_usage_count(Gana::Horn, Gana::Encampment), 3);
868        // Order shouldn't matter
869        assert_eq!(registry.co_usage_count(Gana::Encampment, Gana::Horn), 3);
870    }
871
872    #[test]
873    fn gana_registry_drift_suggestion() {
874        let mut registry = GanaRegistry::with_threshold(5);
875        for _ in 0..5 {
876            registry.record_co_usage(Gana::Horn, Gana::WinnowingBasket);
877        }
878
879        let merges = registry.suggested_merges();
880        assert_eq!(merges.len(), 1);
881        assert_eq!(merges[0].gana_a, Gana::Horn as u8);
882        assert_eq!(merges[0].gana_b, Gana::WinnowingBasket as u8);
883    }
884
885    #[test]
886    fn gana_registry_drift_confidence_increases() {
887        let mut registry = GanaRegistry::with_threshold(5);
888        for _ in 0..10 {
889            registry.record_co_usage(Gana::Horn, Gana::WinnowingBasket);
890        }
891
892        let merges = registry.suggested_merges();
893        assert_eq!(merges.len(), 1);
894        assert!(merges[0].confidence > 0.5);
895    }
896
897    #[test]
898    fn gana_registry_analyze_drift() {
899        let mut registry = GanaRegistry::with_threshold(3);
900        for _ in 0..5 {
901            registry.record_co_usage(Gana::Horn, Gana::WinnowingBasket);
902        }
903        for _ in 0..3 {
904            registry.record_co_usage(Gana::Ghost, Gana::Star);
905        }
906
907        let top = registry.analyze_drift(2);
908        assert_eq!(top.len(), 2);
909        // Horn-WinnowingBasket has more co-usage (5 > 3)
910        assert!(top[0].co_usage_count >= top[1].co_usage_count);
911    }
912
913    #[test]
914    fn gana_registry_clear() {
915        let mut registry = GanaRegistry::new();
916        registry.record_usage(Gana::Horn, true);
917        registry.record_co_usage(Gana::Horn, Gana::Neck);
918        registry.clear();
919
920        assert_eq!(registry.usage_count(Gana::Horn), 0);
921        assert_eq!(registry.co_usage_count(Gana::Horn, Gana::Neck), 0);
922    }
923
924    #[test]
925    fn gana_registry_snapshot() {
926        let mut registry = GanaRegistry::new();
927        registry.record_usage(Gana::Horn, true);
928        let snap = registry.snapshot();
929        assert!(snap.get("total_ganas_tracked").is_some());
930    }
931
932    // ── DynamicGalaxyRegistry Tests ──
933
934    #[test]
935    fn dynamic_galaxy_create() {
936        let mut registry = DynamicGalaxyRegistry::with_config(5, 10, 0.1);
937        let galaxy = registry.try_create(
938            "Rust Patterns",
939            "Memories about Rust design patterns",
940            vec!["rust".to_string(), "patterns".to_string()],
941            15,
942        );
943        assert!(galaxy.is_some());
944        assert_eq!(galaxy.unwrap().name, "Rust Patterns");
945        assert_eq!(registry.len(), 1);
946    }
947
948    #[test]
949    fn dynamic_galaxy_too_small() {
950        let mut registry = DynamicGalaxyRegistry::with_config(10, 5, 0.1);
951        let galaxy = registry.try_create("Small", "Too small", vec![], 3);
952        assert!(galaxy.is_none());
953        assert!(registry.is_empty());
954    }
955
956    #[test]
957    fn dynamic_galaxy_max_limit() {
958        let mut registry = DynamicGalaxyRegistry::with_config(1, 2, 0.0);
959        registry.try_create("G1", "desc", vec![], 5);
960        registry.try_create("G2", "desc", vec![], 5);
961        registry.try_create("G3", "desc", vec![], 5);
962        assert_eq!(registry.len(), 2); // Max 2
963    }
964
965    #[test]
966    fn dynamic_galaxy_prune() {
967        let mut registry = DynamicGalaxyRegistry::with_config(1, 10, 0.5);
968        registry.try_create("G1", "desc", vec![], 5);
969        registry.try_create("G2", "desc", vec![], 5);
970        registry.update_effectiveness("dyn_g1", 0.1); // Below threshold
971        registry.update_effectiveness("dyn_g2", 0.8); // Above threshold
972
973        let pruned = registry.prune();
974        assert_eq!(pruned, 1);
975        assert_eq!(registry.len(), 1);
976        assert!(registry.get("dyn_g2").is_some());
977    }
978
979    #[test]
980    fn dynamic_galaxy_update_existing() {
981        let mut registry = DynamicGalaxyRegistry::with_config(1, 10, 0.0);
982        registry.try_create("Test", "desc", vec![], 5);
983        registry.try_create("Test", "desc", vec![], 10);
984        let g = registry.get("dyn_test").unwrap();
985        assert_eq!(g.memory_count, 10);
986        assert_eq!(registry.len(), 1);
987    }
988
989    // ── LearnedDreamCycle Tests ──
990
991    #[test]
992    fn learned_dream_default_order() {
993        let cycle = LearnedDreamCycle::new();
994        assert_eq!(cycle.phase_order(), &[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]);
995    }
996
997    #[test]
998    fn learned_dream_record_phase() {
999        let mut cycle = LearnedDreamCycle::new();
1000        cycle.record_phase(0, true, 0.8, 100);
1001        cycle.record_phase(0, true, 0.9, 120);
1002
1003        let eff = cycle.effectiveness(0).unwrap();
1004        assert_eq!(eff.runs, 2);
1005        assert_eq!(eff.useful_results, 2);
1006        assert!((eff.avg_improvement - 0.85).abs() < 0.01);
1007    }
1008
1009    #[test]
1010    fn learned_dream_reorders_by_effectiveness() {
1011        let mut cycle = LearnedDreamCycle::new();
1012
1013        // Phase 5 has high effectiveness
1014        for _ in 0..10 {
1015            cycle.record_phase(5, true, 0.9, 50);
1016        }
1017
1018        // Phase 0 has low effectiveness
1019        for _ in 0..10 {
1020            cycle.record_phase(0, false, 0.1, 200);
1021        }
1022
1023        let order = cycle.phase_order();
1024        // Phase 5 should come before phase 0
1025        let pos5 = order.iter().position(|&x| x == 5).unwrap();
1026        let pos0 = order.iter().position(|&x| x == 0).unwrap();
1027        assert!(pos5 < pos0);
1028    }
1029
1030    #[test]
1031    fn learned_dream_filters_ineffective() {
1032        let mut cycle = LearnedDreamCycle::with_config(0.5, 5, true);
1033
1034        // Phase 3 is consistently bad
1035        for _ in 0..10 {
1036            cycle.record_phase(3, false, 0.1, 200);
1037        }
1038
1039        let to_run = cycle.phases_to_run();
1040        // Phase 3 should be filtered out
1041        assert!(!to_run.contains(&3));
1042    }
1043
1044    #[test]
1045    fn learned_dream_keeps_phases_without_data() {
1046        let cycle = LearnedDreamCycle::new();
1047        let to_run = cycle.phases_to_run();
1048        // All phases should be included when there's no data
1049        assert_eq!(to_run.len(), 12);
1050    }
1051
1052    #[test]
1053    fn learned_dream_snapshot() {
1054        let mut cycle = LearnedDreamCycle::new();
1055        cycle.record_phase(0, true, 0.8, 100);
1056        let snap = cycle.snapshot();
1057        assert!(snap.get("phases").is_some());
1058    }
1059
1060    // ── PhaseEffectiveness Tests ──
1061
1062    #[test]
1063    fn phase_effectiveness_score() {
1064        let mut eff = PhaseEffectiveness::new();
1065        assert!((eff.score() - 0.5).abs() < 0.01); // Default for 0 runs
1066
1067        eff.record(true, 0.8, 100);
1068        eff.record(true, 0.9, 120);
1069        eff.record(false, 0.1, 50);
1070
1071        // score = (success_rate + avg_improvement) / 2
1072        // success_rate = 2/3, avg_improvement = (0.8+0.9+0.1)/3 ≈ 0.6
1073        let score = eff.score();
1074        assert!(score > 0.5);
1075    }
1076
1077    // ── LearnedCycleStrategy Tests ──
1078
1079    #[test]
1080    fn cycle_strategy_default_is_fixed() {
1081        let strategy = LearnedCycleStrategy::new();
1082        assert_eq!(strategy.strategy(), CycleStrategy::FixedOrder);
1083        assert_eq!(strategy.priority_order().len(), 8);
1084    }
1085
1086    #[test]
1087    fn cycle_strategy_transitions_to_priority() {
1088        let mut strategy = LearnedCycleStrategy::new();
1089        // Record enough cycles to trigger transition
1090        for _ in 0..15 {
1091            strategy.record_cycle(0, 2, 0.8, 100);
1092        }
1093        assert_eq!(strategy.strategy(), CycleStrategy::PriorityBased);
1094    }
1095
1096    #[test]
1097    fn cycle_strategy_priority_order() {
1098        let mut strategy = LearnedCycleStrategy::with_strategy(CycleStrategy::PriorityBased);
1099
1100        // Cycle type 3 is most effective
1101        for _ in 0..10 {
1102            strategy.record_cycle(3, 5, 0.9, 100);
1103        }
1104        // Cycle type 0 is least effective
1105        for _ in 0..10 {
1106            strategy.record_cycle(0, 0, 0.1, 200);
1107        }
1108
1109        let order = strategy.priority_order();
1110        assert_eq!(order[0], 3); // Best cycle first
1111    }
1112
1113    #[test]
1114    fn cycle_strategy_best_only() {
1115        let mut strategy = LearnedCycleStrategy::with_strategy(CycleStrategy::BestOnly);
1116
1117        for _ in 0..5 {
1118            strategy.record_cycle(2, 3, 0.8, 100);
1119        }
1120        for _ in 0..5 {
1121            strategy.record_cycle(5, 1, 0.3, 100);
1122        }
1123
1124        // record_cycle updates priority_order when strategy is PriorityBased or Adaptive
1125        // but BestOnly doesn't auto-update. Set to Adaptive first, then back.
1126        strategy.set_strategy(CycleStrategy::Adaptive);
1127        strategy.set_strategy(CycleStrategy::BestOnly);
1128
1129        let to_run = strategy.cycles_to_run();
1130        assert_eq!(to_run.len(), 1);
1131        assert_eq!(to_run[0], 2); // Best cycle
1132    }
1133
1134    #[test]
1135    fn cycle_strategy_fixed_order_returns_all() {
1136        let strategy = LearnedCycleStrategy::with_strategy(CycleStrategy::FixedOrder);
1137        let to_run = strategy.cycles_to_run();
1138        assert_eq!(to_run.len(), 8);
1139    }
1140
1141    #[test]
1142    fn cycle_strategy_set_strategy() {
1143        let mut strategy = LearnedCycleStrategy::new();
1144        strategy.set_strategy(CycleStrategy::Adaptive);
1145        assert_eq!(strategy.strategy(), CycleStrategy::Adaptive);
1146    }
1147
1148    #[test]
1149    fn cycle_strategy_snapshot() {
1150        let mut strategy = LearnedCycleStrategy::new();
1151        strategy.record_cycle(0, 2, 0.8, 100);
1152        let snap = strategy.snapshot();
1153        assert!(snap.get("strategy").is_some());
1154    }
1155
1156    #[test]
1157    fn cycle_effectiveness_score() {
1158        let mut eff = CycleEffectiveness::new();
1159        assert!((eff.score() - 0.5).abs() < 0.01);
1160
1161        eff.record(3, 0.8, 100);
1162        eff.record(0, 0.2, 200);
1163
1164        let score = eff.score();
1165        // proposal_rate = 3/2 = 1.5, avg_usefulness = 0.5
1166        // score = (1.5 + 0.5) / 2 = 1.0 (capped conceptually)
1167        assert!(score > 0.5);
1168    }
1169
1170    // ── Serialization Tests ──
1171
1172    #[test]
1173    fn gana_registry_serialization() {
1174        let mut registry = GanaRegistry::new();
1175        registry.record_usage(Gana::Horn, true);
1176        registry.record_co_usage(Gana::Horn, Gana::Neck);
1177
1178        let json = serde_json::to_string(&registry).unwrap();
1179        let mut back: GanaRegistry = serde_json::from_str(&json).unwrap();
1180        back.rebuild_pairs();
1181        assert_eq!(back.usage_count(Gana::Horn), 1);
1182        assert_eq!(back.co_usage_count(Gana::Horn, Gana::Neck), 1);
1183    }
1184
1185    #[test]
1186    fn dynamic_galaxy_registry_serialization() {
1187        let mut registry = DynamicGalaxyRegistry::new();
1188        registry.try_create("Test", "desc", vec!["tag".to_string()], 15);
1189
1190        let json = serde_json::to_string(&registry).unwrap();
1191        let back: DynamicGalaxyRegistry = serde_json::from_str(&json).unwrap();
1192        assert_eq!(back.len(), 1);
1193    }
1194
1195    #[test]
1196    fn learned_dream_cycle_serialization() {
1197        let mut cycle = LearnedDreamCycle::new();
1198        cycle.record_phase(0, true, 0.8, 100);
1199
1200        let json = serde_json::to_string(&cycle).unwrap();
1201        let back: LearnedDreamCycle = serde_json::from_str(&json).unwrap();
1202        assert_eq!(back.effectiveness(0).unwrap().runs, 1);
1203    }
1204
1205    #[test]
1206    fn learned_cycle_strategy_serialization() {
1207        let mut strategy = LearnedCycleStrategy::new();
1208        strategy.record_cycle(0, 2, 0.8, 100);
1209
1210        let json = serde_json::to_string(&strategy).unwrap();
1211        let back: LearnedCycleStrategy = serde_json::from_str(&json).unwrap();
1212        assert_eq!(back.effectiveness(0).unwrap().runs, 1);
1213    }
1214}