1use crate::hypothesis::HypothesisId;
7use crate::belief::BeliefGraph;
8
9use super::analyzer::{KnowledgeGap, GapType, GapSuggestion, SuggestedAction};
10
11pub fn generate_suggestion(
15 gap: &KnowledgeGap,
16 graph: &BeliefGraph,
17) -> GapSuggestion {
18 let priority = gap.score;
19
20 let action = match &gap.gap_type {
22 GapType::UntestedAssumption => {
23 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 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 SuggestedAction::Investigate {
46 area: gap.description.clone(),
47 details: "Conflicting evidence needs resolution".to_string(),
48 }
49 }
50 GapType::UnknownDependency => {
51 if let Some(hid) = gap.hypothesis_id {
53 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 let refined_action = if let Some(_hid) = gap.hypothesis_id {
88 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
106fn 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
134pub fn generate_all_suggestions(
138 gaps: &[KnowledgeGap],
139 graph: &BeliefGraph,
140) -> Vec<GapSuggestion> {
141 let unfilled: Vec<_> = gaps.iter()
143 .filter(|g| g.filled_at.is_none())
144 .collect();
145
146 let mut suggestions: Vec<_> = unfilled.iter()
148 .map(|gap| generate_suggestion(gap, graph))
149 .collect();
150
151 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 }
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 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 }
224 SuggestedAction::Investigate { .. } => {
225 }
227 _ => panic!("Expected Research or Investigate, got {:?}", suggestion1.action),
228 }
229
230 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 }
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 }
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; 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}