1use std::collections::HashMap;
7use std::sync::Arc;
8
9use chrono::{DateTime, Utc};
10use serde::{Deserialize, Serialize};
11
12use crate::hypothesis::HypothesisBoard;
13use crate::hypothesis::types::HypothesisId;
14use crate::belief::BeliefGraph;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
20pub struct GapId(uuid::Uuid);
21
22impl GapId {
23 pub fn new() -> Self {
25 Self(uuid::Uuid::new_v4())
26 }
27
28 pub fn from_bytes(bytes: [u8; 16]) -> Self {
30 Self(uuid::Uuid::from_bytes(bytes))
31 }
32
33 pub fn as_bytes(&self) -> [u8; 16] {
35 self.0.as_bytes().clone()
36 }
37}
38
39impl Default for GapId {
40 fn default() -> Self {
41 Self::new()
42 }
43}
44
45impl std::fmt::Display for GapId {
46 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47 write!(f, "{}", self.0)
48 }
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
55pub enum GapCriticality {
56 Low,
58 Medium,
60 High,
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
68pub enum GapType {
69 MissingInformation,
71 UntestedAssumption,
73 ContradictoryEvidence,
75 UnknownDependency,
77 Other(String),
79}
80
81#[derive(Clone, Debug, Serialize, Deserialize)]
89pub struct KnowledgeGap {
90 pub id: GapId,
92 pub description: String,
94 pub hypothesis_id: Option<HypothesisId>,
96 pub criticality: GapCriticality,
98 pub gap_type: GapType,
100 pub created_at: DateTime<Utc>,
102 pub filled_at: Option<DateTime<Utc>>,
104 pub resolution_notes: Option<String>,
106 pub score: f64,
108 pub depth: usize,
110 pub evidence_strength: f64,
112}
113
114#[derive(Clone, Debug)]
119pub struct ScoringConfig {
120 pub criticality_weight: f64,
122 pub depth_weight: f64,
124 pub evidence_weight: f64,
126 pub age_weight: f64,
128 pub auto_close_threshold: f64,
130}
131
132impl Default for ScoringConfig {
133 fn default() -> Self {
134 Self {
135 criticality_weight: 0.5,
136 depth_weight: 0.3,
137 evidence_weight: 0.15,
138 age_weight: 0.05,
139 auto_close_threshold: 0.9,
140 }
141 }
142}
143
144#[derive(Clone, Debug, Serialize, Deserialize)]
148pub struct GapSuggestion {
149 pub gap_id: GapId,
151 pub action: SuggestedAction,
153 pub rationale: String,
155 pub priority: f64,
157}
158
159#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
163pub enum SuggestedAction {
164 RunTest { test_name: String },
166 Investigate { area: String, details: String },
168 GatherEvidence { hypothesis_id: HypothesisId },
170 ResolveDependency { dependent_id: HypothesisId, dependee_id: HypothesisId },
172 CreateVerificationCheck { command: String, hypothesis_id: HypothesisId },
174 Research { topic: String },
176 Other { description: String },
178}
179
180pub struct KnowledgeGapAnalyzer {
185 board: Arc<HypothesisBoard>,
187 graph: Arc<BeliefGraph>,
189 gaps: HashMap<GapId, KnowledgeGap>,
191 scoring_config: ScoringConfig,
193}
194
195impl KnowledgeGapAnalyzer {
196 pub fn new(board: Arc<HypothesisBoard>, graph: Arc<BeliefGraph>) -> Self {
198 Self {
199 board,
200 graph,
201 gaps: HashMap::new(),
202 scoring_config: ScoringConfig::default(),
203 }
204 }
205
206 pub fn with_scoring_config(mut self, config: ScoringConfig) -> Self {
208 self.scoring_config = config;
209 self
210 }
211
212 pub async fn register_gap(
216 &mut self,
217 description: String,
218 criticality: GapCriticality,
219 gap_type: GapType,
220 hypothesis_id: Option<HypothesisId>,
221 ) -> crate::errors::Result<GapId> {
222 let id = GapId::new();
223 let created_at = Utc::now();
224
225 let depth = if let Some(hid) = hypothesis_id {
227 self.compute_depth(hid).await
228 } else {
229 0
230 };
231
232 let evidence_strength = if let Some(hid) = hypothesis_id {
234 self.compute_evidence_strength(hid).await
235 } else {
236 0.0
237 };
238
239 let gap = KnowledgeGap {
241 id,
242 description,
243 hypothesis_id,
244 criticality,
245 gap_type,
246 created_at,
247 filled_at: None,
248 resolution_notes: None,
249 score: 0.0, depth,
251 evidence_strength,
252 };
253
254 let score = super::scoring::compute_gap_score(&gap, &self.scoring_config);
256
257 let mut gap = gap;
258 gap.score = score;
259
260 self.gaps.insert(id, gap);
261 Ok(id)
262 }
263
264 pub async fn fill_gap(
266 &mut self,
267 gap_id: GapId,
268 resolution_notes: String,
269 ) -> crate::errors::Result<()> {
270 let gap = self.gaps.get_mut(&gap_id)
271 .ok_or_else(|| crate::errors::ReasoningError::NotFound(
272 format!("Gap {} not found", gap_id)
273 ))?;
274
275 gap.filled_at = Some(Utc::now());
276 gap.resolution_notes = Some(resolution_notes);
277 Ok(())
278 }
279
280 pub async fn list_gaps(&self, unfilled_only: bool) -> Vec<KnowledgeGap> {
284 let mut gaps: Vec<_> = self.gaps.values()
285 .filter(|g| !unfilled_only || g.filled_at.is_none())
286 .cloned()
287 .collect();
288
289 gaps.sort_by(|a, b| {
291 b.score.partial_cmp(&a.score).unwrap_or(std::cmp::Ordering::Equal)
292 });
293
294 gaps
295 }
296
297 pub fn get_gap(&self, gap_id: GapId) -> Option<&KnowledgeGap> {
299 self.gaps.get(&gap_id)
300 }
301
302 pub async fn auto_close_gaps(&mut self) -> Vec<GapId> {
306 let mut closed = Vec::new();
307
308 for gap in self.gaps.values_mut() {
309 if gap.filled_at.is_some() {
311 continue;
312 }
313
314 let hypothesis_id = match gap.hypothesis_id {
315 Some(hid) => hid,
316 None => continue,
317 };
318
319 let hypothesis = match self.board.get(hypothesis_id).await {
321 Ok(Some(h)) => h,
322 _ => continue,
323 };
324
325 if hypothesis.current_confidence().get() > self.scoring_config.auto_close_threshold {
327 gap.filled_at = Some(Utc::now());
328 gap.resolution_notes = Some(
329 "Auto-closed: hypothesis reached high confidence".to_string()
330 );
331 closed.push(gap.id);
332 }
333 }
334
335 closed
336 }
337
338 pub async fn get_suggestions(&self, unfilled_only: bool) -> Vec<GapSuggestion> {
342 let gaps = self.list_gaps(unfilled_only).await;
343 super::suggestions::generate_all_suggestions(&gaps, &self.graph)
344 }
345
346 pub fn recompute_scores(&mut self) {
348 super::scoring::recompute_all_scores(&mut self.gaps, &self.scoring_config);
349 }
350
351 async fn compute_depth(&self, hypothesis_id: HypothesisId) -> usize {
355 match self.graph.dependency_chain(hypothesis_id) {
357 Ok(chain) => chain.len(),
358 Err(_) => 0,
359 }
360 }
361
362 async fn compute_evidence_strength(&self, hypothesis_id: HypothesisId) -> f64 {
364 match self.board.list_evidence(hypothesis_id).await {
365 Ok(evidence_list) => {
366 if evidence_list.is_empty() {
367 0.0
368 } else {
369 let total: f64 = evidence_list.iter()
370 .map(|e| e.strength().abs())
371 .sum();
372 total / evidence_list.len() as f64
373 }
374 }
375 Err(_) => 0.0,
376 }
377 }
378}
379
380#[cfg(test)]
381mod tests {
382 use super::*;
383 use crate::hypothesis::confidence::Confidence;
384
385 #[tokio::test]
386 async fn test_gap_id_uniqueness() {
387 let id1 = GapId::new();
388 let id2 = GapId::new();
389 assert_ne!(id1, id2);
390 }
391
392 #[tokio::test]
393 async fn test_scoring_config_default() {
394 let config = ScoringConfig::default();
395 assert_eq!(config.criticality_weight, 0.5);
396 assert_eq!(config.depth_weight, 0.3);
397 assert_eq!(config.evidence_weight, 0.15);
398 assert_eq!(config.age_weight, 0.05);
399 assert_eq!(config.auto_close_threshold, 0.9);
400 }
401
402 #[tokio::test]
403 async fn test_register_gap() {
404 let board = Arc::new(HypothesisBoard::in_memory());
405 let graph = Arc::new(BeliefGraph::new());
406 let mut analyzer = KnowledgeGapAnalyzer::new(board, graph);
407
408 let gap_id = analyzer.register_gap(
409 "Test gap".to_string(),
410 GapCriticality::Medium,
411 GapType::MissingInformation,
412 None,
413 ).await.unwrap();
414
415 let gap = analyzer.get_gap(gap_id);
416 assert!(gap.is_some());
417 assert_eq!(gap.unwrap().description, "Test gap");
418 }
419
420 #[tokio::test]
421 async fn test_fill_gap() {
422 let board = Arc::new(HypothesisBoard::in_memory());
423 let graph = Arc::new(BeliefGraph::new());
424 let mut analyzer = KnowledgeGapAnalyzer::new(board, graph);
425
426 let gap_id = analyzer.register_gap(
427 "Test gap".to_string(),
428 GapCriticality::Low,
429 GapType::UntestedAssumption,
430 None,
431 ).await.unwrap();
432
433 analyzer.fill_gap(gap_id, "Resolved".to_string()).await.unwrap();
434
435 let gap = analyzer.get_gap(gap_id).unwrap();
436 assert!(gap.filled_at.is_some());
437 assert_eq!(gap.resolution_notes, Some("Resolved".to_string()));
438 }
439
440 #[tokio::test]
441 async fn test_list_gaps_sorts_by_priority() {
442 let board = Arc::new(HypothesisBoard::in_memory());
443 let graph = Arc::new(BeliefGraph::new());
444 let mut analyzer = KnowledgeGapAnalyzer::new(board, graph);
445
446 analyzer.register_gap(
448 "Low priority".to_string(),
449 GapCriticality::Low,
450 GapType::MissingInformation,
451 None,
452 ).await.unwrap();
453
454 analyzer.register_gap(
455 "High priority".to_string(),
456 GapCriticality::High,
457 GapType::MissingInformation,
458 None,
459 ).await.unwrap();
460
461 let gaps = analyzer.list_gaps(false).await;
462 assert_eq!(gaps[0].criticality, GapCriticality::High);
464 assert_eq!(gaps[1].criticality, GapCriticality::Low);
465 }
466
467 #[tokio::test]
468 async fn test_register_gap_computes_score_correctly() {
469 let board = Arc::new(HypothesisBoard::in_memory());
470 let graph = Arc::new(BeliefGraph::new());
471 let mut analyzer = KnowledgeGapAnalyzer::new(board, graph);
472
473 let gap_id = analyzer.register_gap(
474 "Test gap".to_string(),
475 GapCriticality::High,
476 GapType::MissingInformation,
477 None,
478 ).await.unwrap();
479
480 let gap = analyzer.get_gap(gap_id).unwrap();
481 assert!(gap.score > 0.0);
483 assert!(gap.score <= 1.0);
484 }
485
486 #[tokio::test]
487 async fn test_auto_close_gaps_closes_high_confidence_hypotheses() {
488 let board = Arc::new(HypothesisBoard::in_memory());
489 let graph = Arc::new(BeliefGraph::new());
490 let mut analyzer = KnowledgeGapAnalyzer::new(board.clone(), graph);
491
492 let prior = Confidence::new(0.95).unwrap();
494 let h_id = board.propose("High confidence hypothesis", prior).await.unwrap();
495
496 let gap_id = analyzer.register_gap(
498 "Test gap".to_string(),
499 GapCriticality::Medium,
500 GapType::UntestedAssumption,
501 Some(h_id),
502 ).await.unwrap();
503
504 let closed = analyzer.auto_close_gaps().await;
506 assert_eq!(closed.len(), 1);
507 assert_eq!(closed[0], gap_id);
508
509 let gap = analyzer.get_gap(gap_id).unwrap();
511 assert!(gap.filled_at.is_some());
512 assert!(gap.resolution_notes.is_some());
513 }
514
515 #[tokio::test]
516 async fn test_get_suggestions_returns_sorted_list() {
517 let board = Arc::new(HypothesisBoard::in_memory());
518 let graph = Arc::new(BeliefGraph::new());
519 let mut analyzer = KnowledgeGapAnalyzer::new(board, graph);
520
521 analyzer.register_gap(
523 "Low priority".to_string(),
524 GapCriticality::Low,
525 GapType::MissingInformation,
526 None,
527 ).await.unwrap();
528
529 analyzer.register_gap(
530 "High priority".to_string(),
531 GapCriticality::High,
532 GapType::UntestedAssumption,
533 None,
534 ).await.unwrap();
535
536 let suggestions = analyzer.get_suggestions(true).await;
538
539 assert_eq!(suggestions.len(), 2);
541
542 assert!(suggestions[0].priority >= suggestions[1].priority);
544 }
545
546 #[tokio::test]
547 async fn test_recompute_scores_updates_all_gaps() {
548 let board = Arc::new(HypothesisBoard::in_memory());
549 let graph = Arc::new(BeliefGraph::new());
550 let mut analyzer = KnowledgeGapAnalyzer::new(board.clone(), graph);
551
552 let gap_id = analyzer.register_gap(
554 "Test gap".to_string(),
555 GapCriticality::Medium,
556 GapType::MissingInformation,
557 None,
558 ).await.unwrap();
559
560 {
562 let gap = analyzer.gaps.get_mut(&gap_id).unwrap();
563 gap.score = 0.0;
564 }
565
566 analyzer.recompute_scores();
568
569 let gap = analyzer.get_gap(gap_id).unwrap();
571 assert!(gap.score > 0.0);
572 }
573
574 #[tokio::test]
575 async fn test_depth_computation_matches_dependency_graph() {
576 let board = Arc::new(HypothesisBoard::in_memory());
577
578 let prior = Confidence::new(0.5).unwrap();
580 let h1 = board.propose("H1", prior).await.unwrap();
581 let h2 = board.propose("H2", prior).await.unwrap();
582 let h3 = board.propose("H3", prior).await.unwrap();
583
584 let mut graph = BeliefGraph::new();
586 graph.add_dependency(h1, h2).unwrap();
587 graph.add_dependency(h2, h3).unwrap();
588
589 let mut analyzer = KnowledgeGapAnalyzer::new(board.clone(), Arc::new(graph));
591
592 let gap_id = analyzer.register_gap(
594 "Test gap".to_string(),
595 GapCriticality::Medium,
596 GapType::UntestedAssumption,
597 Some(h1),
598 ).await.unwrap();
599
600 let gap = analyzer.get_gap(gap_id).unwrap();
602 assert_eq!(gap.depth, 2);
603 }
604}