scribe_selection/
simple_router.rs

1//! Simple rule-based router for file selection strategy selection.
2//!
3//! Replaces the complex multi-armed bandit approach with a straightforward
4//! decision tree based on project context and constraints.
5
6use serde::{Deserialize, Serialize};
7
8/// Selection strategy options
9#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
10pub enum SelectionStrategy {
11    /// Importance-based greedy selection
12    ImportanceGreedy,
13    /// Dependency-aware selection
14    DependencyAware,
15    /// Coverage-optimizing selection
16    CoverageOptimized,
17    /// Random selection (baseline)
18    Random,
19    /// Two-pass speculative selection
20    TwoPassSpeculative,
21    /// Quota-managed selection
22    QuotaManaged,
23}
24
25impl SelectionStrategy {
26    pub fn name(&self) -> &'static str {
27        match self {
28            Self::ImportanceGreedy => "importance_greedy",
29            Self::DependencyAware => "dependency_aware",
30            Self::CoverageOptimized => "coverage_optimized",
31            Self::Random => "random",
32            Self::TwoPassSpeculative => "two_pass_speculative",
33            Self::QuotaManaged => "quota_managed",
34        }
35    }
36}
37
38/// Project size categories
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub enum ProjectSize {
41    Small,  // < 50 files
42    Medium, // 50-500 files
43    Large,  // > 500 files
44}
45
46/// Time constraint levels
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub enum TimeConstraint {
49    Tight,   // Need results quickly
50    Normal,  // Standard time expectations
51    Relaxed, // Can afford thorough analysis
52}
53
54/// Context features for routing decisions
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct SelectionContext {
57    /// Number of available files
58    pub file_count: usize,
59    /// Average file importance score
60    pub avg_importance: f64,
61    /// Dependency graph density (edges/nodes)
62    pub dependency_density: f64,
63    /// Budget constraint ratio (available/total)
64    pub budget_ratio: f64,
65    /// Dominant file type (source, test, config, etc.)
66    pub dominant_file_type: String,
67    /// Project size category
68    pub project_size: ProjectSize,
69    /// Time constraint level
70    pub time_constraint: TimeConstraint,
71}
72
73/// Result of a routing decision
74#[derive(Debug, Clone)]
75pub struct RoutingDecision {
76    /// Selected strategy
77    pub strategy: SelectionStrategy,
78    /// Reason for this choice (for debugging/logging)
79    pub reason: String,
80}
81
82/// Simple rule-based router for selection strategy
83pub struct SimpleRouter;
84
85impl SimpleRouter {
86    /// Create a new simple router
87    pub fn new() -> Self {
88        Self
89    }
90
91    /// Route to the appropriate selection strategy based on context
92    ///
93    /// Decision priority:
94    /// 1. Budget constraints (critical)
95    /// 2. Time constraints (important)
96    /// 3. Project size and complexity (characteristics)
97    /// 4. Default to balanced approach
98    pub fn route_selection(&self, context: &SelectionContext) -> RoutingDecision {
99        // Priority 1: Budget constraints are critical
100        if context.budget_ratio < 0.3 {
101            return RoutingDecision {
102                strategy: SelectionStrategy::QuotaManaged,
103                reason: format!(
104                    "Low budget ratio ({:.2}) requires quota management",
105                    context.budget_ratio
106                ),
107            };
108        }
109
110        // Priority 2: Time constraints affect strategy complexity
111        if matches!(context.time_constraint, TimeConstraint::Tight) {
112            // For tight deadlines with complex dependencies, use dependency-aware
113            if context.dependency_density > 0.5 {
114                return RoutingDecision {
115                    strategy: SelectionStrategy::DependencyAware,
116                    reason: format!(
117                        "Tight time constraint with high dependency density ({:.2}) needs dependency-aware selection",
118                        context.dependency_density
119                    ),
120                };
121            }
122            // Otherwise use the fastest simple strategy
123            return RoutingDecision {
124                strategy: SelectionStrategy::ImportanceGreedy,
125                reason: "Tight time constraint requires fast importance-based selection".to_string(),
126            };
127        }
128
129        // Priority 3: Small projects benefit from simple strategies
130        if matches!(context.project_size, ProjectSize::Small) {
131            return RoutingDecision {
132                strategy: SelectionStrategy::ImportanceGreedy,
133                reason: format!(
134                    "Small project ({} files) works well with importance-based selection",
135                    context.file_count
136                ),
137            };
138        }
139
140        // Priority 4: High dependency density needs specialized handling
141        if context.dependency_density > 0.7 {
142            return RoutingDecision {
143                strategy: SelectionStrategy::DependencyAware,
144                reason: format!(
145                    "High dependency density ({:.2}) requires dependency-aware selection",
146                    context.dependency_density
147                ),
148            };
149        }
150
151        // Priority 5: Large projects benefit from two-pass analysis
152        if context.file_count > 200 {
153            return RoutingDecision {
154                strategy: SelectionStrategy::TwoPassSpeculative,
155                reason: format!(
156                    "Large project ({} files) benefits from two-pass speculative selection",
157                    context.file_count
158                ),
159            };
160        }
161
162        // Default: Use coverage-optimized for balanced results
163        RoutingDecision {
164            strategy: SelectionStrategy::CoverageOptimized,
165            reason: "Standard context: using coverage-optimized selection for balanced results".to_string(),
166        }
167    }
168}
169
170impl Default for SimpleRouter {
171    fn default() -> Self {
172        Self::new()
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179
180    fn create_test_context() -> SelectionContext {
181        SelectionContext {
182            file_count: 100,
183            avg_importance: 0.5,
184            dependency_density: 0.3,
185            budget_ratio: 0.8,
186            dominant_file_type: "source".to_string(),
187            project_size: ProjectSize::Medium,
188            time_constraint: TimeConstraint::Normal,
189        }
190    }
191
192    #[test]
193    fn test_low_budget_uses_quota_managed() {
194        let router = SimpleRouter::new();
195        let mut context = create_test_context();
196        context.budget_ratio = 0.2;
197
198        let decision = router.route_selection(&context);
199        assert_eq!(decision.strategy, SelectionStrategy::QuotaManaged);
200        assert!(decision.reason.contains("budget"));
201    }
202
203    #[test]
204    fn test_tight_time_with_dependencies_uses_dependency_aware() {
205        let router = SimpleRouter::new();
206        let mut context = create_test_context();
207        context.time_constraint = TimeConstraint::Tight;
208        context.dependency_density = 0.6;
209
210        let decision = router.route_selection(&context);
211        assert_eq!(decision.strategy, SelectionStrategy::DependencyAware);
212    }
213
214    #[test]
215    fn test_tight_time_without_dependencies_uses_importance_greedy() {
216        let router = SimpleRouter::new();
217        let mut context = create_test_context();
218        context.time_constraint = TimeConstraint::Tight;
219        context.dependency_density = 0.2;
220
221        let decision = router.route_selection(&context);
222        assert_eq!(decision.strategy, SelectionStrategy::ImportanceGreedy);
223    }
224
225    #[test]
226    fn test_small_project_uses_importance_greedy() {
227        let router = SimpleRouter::new();
228        let mut context = create_test_context();
229        context.project_size = ProjectSize::Small;
230        context.file_count = 30;
231
232        let decision = router.route_selection(&context);
233        assert_eq!(decision.strategy, SelectionStrategy::ImportanceGreedy);
234    }
235
236    #[test]
237    fn test_high_dependency_density_uses_dependency_aware() {
238        let router = SimpleRouter::new();
239        let mut context = create_test_context();
240        context.dependency_density = 0.8;
241
242        let decision = router.route_selection(&context);
243        assert_eq!(decision.strategy, SelectionStrategy::DependencyAware);
244    }
245
246    #[test]
247    fn test_large_file_count_uses_two_pass() {
248        let router = SimpleRouter::new();
249        let mut context = create_test_context();
250        context.file_count = 250;
251
252        let decision = router.route_selection(&context);
253        assert_eq!(decision.strategy, SelectionStrategy::TwoPassSpeculative);
254    }
255
256    #[test]
257    fn test_default_uses_coverage_optimized() {
258        let router = SimpleRouter::new();
259        let context = create_test_context();
260
261        let decision = router.route_selection(&context);
262        assert_eq!(decision.strategy, SelectionStrategy::CoverageOptimized);
263    }
264
265    #[test]
266    fn test_priority_order_budget_over_time() {
267        let router = SimpleRouter::new();
268        let mut context = create_test_context();
269        context.budget_ratio = 0.2;
270        context.time_constraint = TimeConstraint::Tight;
271
272        let decision = router.route_selection(&context);
273        // Budget constraint should take priority
274        assert_eq!(decision.strategy, SelectionStrategy::QuotaManaged);
275    }
276
277    #[test]
278    fn test_priority_order_time_over_size() {
279        let router = SimpleRouter::new();
280        let mut context = create_test_context();
281        context.time_constraint = TimeConstraint::Tight;
282        context.project_size = ProjectSize::Small;
283        context.dependency_density = 0.2;
284
285        let decision = router.route_selection(&context);
286        // Time constraint should take priority over project size
287        assert_eq!(decision.strategy, SelectionStrategy::ImportanceGreedy);
288    }
289}