Skip to main content

forge_reasoning/gaps/
suggestions.rs

1//! Action suggestion generation for knowledge gaps
2//!
3//! Provides context-aware suggestions for resolving knowledge gaps based on
4//! gap type, hypothesis status, and dependency relationships.
5
6use crate::hypothesis::HypothesisId;
7use crate::belief::BeliefGraph;
8
9use super::analyzer::{KnowledgeGap, GapType, GapSuggestion, SuggestedAction};
10
11/// Generate context-aware action suggestion for a single gap
12///
13/// Analyzes gap type and linked hypothesis context to recommend the best action.
14pub fn generate_suggestion(
15    gap: &KnowledgeGap,
16    graph: &BeliefGraph,
17) -> GapSuggestion {
18    let priority = gap.score;
19
20    // Determine action based on gap type and context
21    let action = match &gap.gap_type {
22        GapType::UntestedAssumption => {
23            // Suggest verification check
24            SuggestedAction::CreateVerificationCheck {
25                command: format!("run verification test for {}", gap.description),
26                hypothesis_id: gap.hypothesis_id.unwrap_or_else(|| HypothesisId::new()),
27            }
28        }
29        GapType::MissingInformation => {
30            // Suggest research or investigate based on description
31            if gap.description.to_lowercase().contains("unknown") ||
32               gap.description.to_lowercase().contains("unclear") {
33                SuggestedAction::Research {
34                    topic: gap.description.clone(),
35                }
36            } else {
37                SuggestedAction::Investigate {
38                    area: gap.description.clone(),
39                    details: "Missing information prevents progress".to_string(),
40                }
41            }
42        }
43        GapType::ContradictoryEvidence => {
44            // Suggest investigation into conflict
45            SuggestedAction::Investigate {
46                area: gap.description.clone(),
47                details: "Conflicting evidence needs resolution".to_string(),
48            }
49        }
50        GapType::UnknownDependency => {
51            // Suggest dependency resolution
52            if let Some(hid) = gap.hypothesis_id {
53                // Try to find dependents
54                if let Ok(dependents) = graph.dependents(hid) {
55                    if let Some(&first_dependent) = dependents.first() {
56                        SuggestedAction::ResolveDependency {
57                            dependent_id: first_dependent,
58                            dependee_id: hid,
59                        }
60                    } else {
61                        SuggestedAction::Investigate {
62                            area: gap.description.clone(),
63                            details: "Unknown dependency relationship".to_string(),
64                        }
65                    }
66                } else {
67                    SuggestedAction::Investigate {
68                        area: gap.description.clone(),
69                        details: "Unknown dependency relationship".to_string(),
70                    }
71                }
72            } else {
73                SuggestedAction::Investigate {
74                    area: gap.description.clone(),
75                    details: "Unknown dependency relationship".to_string(),
76                }
77            }
78        }
79        GapType::Other(desc) => {
80            SuggestedAction::Other {
81                description: desc.clone(),
82            }
83        }
84    };
85
86    // Refine action based on linked hypothesis context
87    let refined_action = if let Some(_hid) = gap.hypothesis_id {
88        // This is a tokio::block_in_place situation since we're in a sync function
89        // For now, use a simplified check without async
90        // In practice, the caller should have already loaded hypothesis data
91        action
92    } else {
93        action
94    };
95
96    let rationale = generate_rationale(&refined_action, gap);
97
98    GapSuggestion {
99        gap_id: gap.id,
100        action: refined_action,
101        rationale,
102        priority,
103    }
104}
105
106/// Generate human-readable rationale for a suggested action
107fn generate_rationale(action: &SuggestedAction, gap: &KnowledgeGap) -> String {
108    match action {
109        SuggestedAction::RunTest { test_name } => {
110            format!("Run test '{}' to verify assumption and gather evidence", test_name)
111        }
112        SuggestedAction::Investigate { area, details } => {
113            format!("Investigate '{}' - {}", area, details)
114        }
115        SuggestedAction::GatherEvidence { .. } => {
116            "Gather more evidence to increase confidence in linked hypothesis".to_string()
117        }
118        SuggestedAction::ResolveDependency { dependent_id, dependee_id } => {
119            format!("Resolve dependency between {} and {} to unblock progress",
120                dependent_id, dependee_id)
121        }
122        SuggestedAction::CreateVerificationCheck { .. } => {
123            format!("Create verification check for: {}", gap.description)
124        }
125        SuggestedAction::Research { topic } => {
126            format!("Research '{}' to fill knowledge gap", topic)
127        }
128        SuggestedAction::Other { description } => {
129            format!("Address gap: {}", description)
130        }
131    }
132}
133
134/// Generate suggestions for all gaps
135///
136/// Returns sorted suggestions (highest priority first) for unfilled gaps.
137pub fn generate_all_suggestions(
138    gaps: &[KnowledgeGap],
139    graph: &BeliefGraph,
140) -> Vec<GapSuggestion> {
141    // Filter unfilled gaps
142    let unfilled: Vec<_> = gaps.iter()
143        .filter(|g| g.filled_at.is_none())
144        .collect();
145
146    // Generate suggestions
147    let mut suggestions: Vec<_> = unfilled.iter()
148        .map(|gap| generate_suggestion(gap, graph))
149        .collect();
150
151    // Sort by priority (highest first)
152    suggestions.sort_by(|a, b| {
153        b.priority.partial_cmp(&a.priority).unwrap_or(std::cmp::Ordering::Equal)
154    });
155
156    suggestions
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162    use crate::gaps::analyzer::{GapCriticality, KnowledgeGap, GapId};
163    use crate::hypothesis::HypothesisBoard;
164    use chrono::Utc;
165
166    fn make_test_gap(
167        description: &str,
168        gap_type: GapType,
169        criticality: GapCriticality,
170    ) -> KnowledgeGap {
171        KnowledgeGap {
172            id: GapId::new(),
173            description: description.to_string(),
174            hypothesis_id: None,
175            criticality,
176            gap_type,
177            created_at: Utc::now(),
178            filled_at: None,
179            resolution_notes: None,
180            score: 0.7,
181            depth: 0,
182            evidence_strength: 0.0,
183        }
184    }
185
186    #[test]
187    fn test_untested_assumption_generates_verification_check() {
188        let graph = BeliefGraph::new();
189
190        let gap = make_test_gap(
191            "Need to verify function behavior",
192            GapType::UntestedAssumption,
193            GapCriticality::Medium,
194        );
195
196        let suggestion = generate_suggestion(&gap, &graph);
197
198        match suggestion.action {
199            SuggestedAction::CreateVerificationCheck { .. } => {
200                // Success
201            }
202            _ => panic!("Expected CreateVerificationCheck, got {:?}", suggestion.action),
203        }
204    }
205
206    #[test]
207    fn test_missing_information_generates_research_or_investigate() {
208        let _board = HypothesisBoard::in_memory();
209        let graph = BeliefGraph::new();
210
211        // Test with "unknown" keyword
212        let gap1 = make_test_gap(
213            "Unknown behavior in edge case",
214            GapType::MissingInformation,
215            GapCriticality::Low,
216        );
217
218        let suggestion1 = generate_suggestion(&gap1, &graph);
219
220        match suggestion1.action {
221            SuggestedAction::Research { .. } => {
222                // Success - should suggest Research
223            }
224            SuggestedAction::Investigate { .. } => {
225                // Also acceptable
226            }
227            _ => panic!("Expected Research or Investigate, got {:?}", suggestion1.action),
228        }
229
230        // Test without "unknown" keyword
231        let gap2 = make_test_gap(
232            "Missing data on API response",
233            GapType::MissingInformation,
234            GapCriticality::Low,
235        );
236
237        let suggestion2 = generate_suggestion(&gap2, &graph);
238
239        match suggestion2.action {
240            SuggestedAction::Investigate { .. } => {
241                // Success
242            }
243            _ => panic!("Expected Investigate, got {:?}", suggestion2.action),
244        }
245    }
246
247    #[test]
248    fn test_contradictory_evidence_generates_investigate() {
249        let _board = HypothesisBoard::in_memory();
250        let graph = BeliefGraph::new();
251
252        let gap = make_test_gap(
253            "Conflicting test results",
254            GapType::ContradictoryEvidence,
255            GapCriticality::High,
256        );
257
258        let suggestion = generate_suggestion(&gap, &graph);
259
260        match suggestion.action {
261            SuggestedAction::Investigate { .. } => {
262                // Success
263            }
264            _ => panic!("Expected Investigate, got {:?}", suggestion.action),
265        }
266
267        assert!(suggestion.rationale.contains("Conflicting evidence") ||
268                suggestion.rationale.contains("conflict"));
269    }
270
271    #[test]
272    fn test_suggestions_sort_by_priority() {
273        let _board = HypothesisBoard::in_memory();
274        let graph = BeliefGraph::new();
275
276        let mut gap1 = make_test_gap("Low priority", GapType::MissingInformation, GapCriticality::Low);
277        gap1.score = 0.3;
278
279        let mut gap2 = make_test_gap("High priority", GapType::MissingInformation, GapCriticality::High);
280        gap2.score = 0.9;
281
282        let suggestions = generate_all_suggestions(&[gap1, gap2], &graph);
283
284        assert_eq!(suggestions.len(), 2);
285        assert_eq!(suggestions[0].priority, 0.9);
286        assert_eq!(suggestions[1].priority, 0.3);
287    }
288
289    #[test]
290    fn test_filled_gaps_filtered_from_suggestions() {
291        let _board = HypothesisBoard::in_memory();
292        let graph = BeliefGraph::new();
293
294        let mut filled_gap = make_test_gap("Filled", GapType::MissingInformation, GapCriticality::Low);
295        filled_gap.filled_at = Some(Utc::now());
296
297        let unfilled_gap = make_test_gap("Unfilled", GapType::MissingInformation, GapCriticality::Low);
298        let unfilled_gap_id = unfilled_gap.id; // Save id before moving
299
300        let suggestions = generate_all_suggestions(&[filled_gap, unfilled_gap], &graph);
301
302        assert_eq!(suggestions.len(), 1);
303        assert_eq!(suggestions[0].gap_id, unfilled_gap_id);
304    }
305
306    #[test]
307    fn test_rationale_is_meaningful() {
308        let _board = HypothesisBoard::in_memory();
309        let graph = BeliefGraph::new();
310
311        let gap = make_test_gap(
312            "Test gap",
313            GapType::MissingInformation,
314            GapCriticality::Medium,
315        );
316
317        let suggestion = generate_suggestion(&gap, &graph);
318
319        assert!(!suggestion.rationale.is_empty());
320        assert!(suggestion.rationale.len() > 10);
321    }
322}