1use std::sync::Mutex;
2
3use async_trait::async_trait;
4use chrono::{DateTime, Utc};
5use rusqlite::{params, Connection, OptionalExtension};
6use uuid::Uuid;
7
8use reflect_core::error::{ReflectError, Result};
9use reflect_core::storage::Storage;
10use reflect_core::types::{
11 ErrorPattern, EvalError, EvalSignal, Outcome, OutcomeCounts, Reflection, ReflectionStats,
12 ScoredReflection, TagCount, Trend,
13};
14
15pub struct SqliteStorage {
16 conn: Mutex<Connection>,
17}
18
19impl SqliteStorage {
20 pub fn open(path: &str) -> Result<Self> {
21 let conn =
22 Connection::open(path).map_err(|e| ReflectError::Storage(e.to_string()))?;
23 let storage = Self {
24 conn: Mutex::new(conn),
25 };
26 storage.init_schema()?;
27 Ok(storage)
28 }
29
30 pub fn open_in_memory() -> Result<Self> {
31 let conn =
32 Connection::open_in_memory().map_err(|e| ReflectError::Storage(e.to_string()))?;
33 let storage = Self {
34 conn: Mutex::new(conn),
35 };
36 storage.init_schema()?;
37 Ok(storage)
38 }
39
40 fn init_schema(&self) -> Result<()> {
41 let conn = self.conn.lock().map_err(|e| ReflectError::Storage(e.to_string()))?;
42 conn.execute_batch("PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON;")
43 .map_err(|e| ReflectError::Storage(e.to_string()))?;
44 conn.execute_batch(include_str!("schema.sql"))
45 .map_err(|e| ReflectError::Storage(e.to_string()))?;
46 Ok(())
47 }
48
49 fn map_err(e: rusqlite::Error) -> ReflectError {
50 ReflectError::Storage(e.to_string())
51 }
52}
53
54fn parse_datetime(s: &str) -> Result<DateTime<Utc>> {
55 DateTime::parse_from_rfc3339(s)
56 .map(|dt| dt.with_timezone(&Utc))
57 .map_err(|e| ReflectError::Storage(format!("invalid datetime: {e}")))
58}
59
60fn row_to_reflection(
61 row: &rusqlite::Row,
62 eval_signals: Vec<EvalSignal>,
63) -> std::result::Result<Reflection, rusqlite::Error> {
64 let id_str: String = row.get(0)?;
65 let id = Uuid::parse_str(&id_str)
66 .map_err(|e| rusqlite::Error::FromSqlConversionFailure(0, rusqlite::types::Type::Text, Box::new(e)))?;
67 let task_description: String = row.get(1)?;
68 let draft: String = row.get(2)?;
69 let critique: String = row.get(3)?;
70 let lesson: String = row.get(4)?;
71 let outcome_str: String = row.get(5)?;
72 let outcome: Outcome = outcome_str
73 .parse()
74 .map_err(|e: String| rusqlite::Error::FromSqlConversionFailure(5, rusqlite::types::Type::Text, Box::new(std::io::Error::new(std::io::ErrorKind::InvalidData, e))))?;
75 let pattern_id: Option<String> = row.get(6)?;
76 let tags_json: String = row.get(7)?;
77 let tags: Vec<String> = serde_json::from_str(&tags_json).unwrap_or_default();
78 let confidence: f64 = row.get(8)?;
79 let validation_count: i32 = row.get(9)?;
80 let contradiction_count: i32 = row.get(10)?;
81 let created_at_str: String = row.get(11)?;
82 let last_recalled_str: Option<String> = row.get(12)?;
83
84 let created_at = DateTime::parse_from_rfc3339(&created_at_str)
85 .map(|dt| dt.with_timezone(&Utc))
86 .map_err(|e| rusqlite::Error::FromSqlConversionFailure(11, rusqlite::types::Type::Text, Box::new(e)))?;
87 let last_recalled = last_recalled_str
88 .map(|s| {
89 DateTime::parse_from_rfc3339(&s)
90 .map(|dt| dt.with_timezone(&Utc))
91 })
92 .transpose()
93 .map_err(|e| rusqlite::Error::FromSqlConversionFailure(12, rusqlite::types::Type::Text, Box::new(e)))?;
94
95 Ok(Reflection {
96 id,
97 task_description,
98 draft,
99 error_signals: eval_signals,
100 critique,
101 lesson,
102 outcome,
103 pattern_id,
104 tags,
105 confidence: confidence as f32,
106 validation_count: validation_count as u32,
107 contradiction_count: contradiction_count as u32,
108 created_at,
109 last_recalled,
110 })
111}
112
113fn load_eval_signals(
114 conn: &Connection,
115 reflection_id: &str,
116) -> std::result::Result<Vec<EvalSignal>, rusqlite::Error> {
117 let mut stmt = conn.prepare(
118 "SELECT evaluator, passed, summary, errors_json FROM eval_signals WHERE reflection_id = ?1",
119 )?;
120 let signals = stmt
121 .query_map(params![reflection_id], |row| {
122 let evaluator: String = row.get(0)?;
123 let passed: bool = row.get(1)?;
124 let summary: String = row.get(2)?;
125 let errors_json: String = row.get(3)?;
126 let errors: Vec<EvalError> = serde_json::from_str(&errors_json).unwrap_or_default();
127 Ok(EvalSignal {
128 evaluator,
129 passed,
130 summary,
131 errors,
132 })
133 })?
134 .collect::<std::result::Result<Vec<_>, _>>()?;
135 Ok(signals)
136}
137
138#[async_trait]
139impl Storage for SqliteStorage {
140 async fn store_reflection(&self, reflection: &Reflection) -> Result<()> {
141 let conn = self.conn.lock().map_err(|e| ReflectError::Storage(e.to_string()))?;
142 let id_str = reflection.id.to_string();
143 let tags_json =
144 serde_json::to_string(&reflection.tags).map_err(|e| ReflectError::Storage(e.to_string()))?;
145 let created_at_str = reflection.created_at.to_rfc3339();
146 let last_recalled_str = reflection.last_recalled.map(|dt| dt.to_rfc3339());
147
148 conn.execute(
149 "INSERT INTO reflections (id, task_description, draft, critique, lesson, outcome, pattern_id, tags, confidence, validation_count, contradiction_count, created_at, last_recalled)
150 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)",
151 params![
152 id_str,
153 reflection.task_description,
154 reflection.draft,
155 reflection.critique,
156 reflection.lesson,
157 reflection.outcome.as_str(),
158 reflection.pattern_id,
159 tags_json,
160 reflection.confidence as f64,
161 reflection.validation_count as i32,
162 reflection.contradiction_count as i32,
163 created_at_str,
164 last_recalled_str,
165 ],
166 )
167 .map_err(SqliteStorage::map_err)?;
168
169 for signal in &reflection.error_signals {
170 let errors_json = serde_json::to_string(&signal.errors)
171 .map_err(|e| ReflectError::Storage(e.to_string()))?;
172 conn.execute(
173 "INSERT INTO eval_signals (reflection_id, evaluator, passed, summary, errors_json)
174 VALUES (?1, ?2, ?3, ?4, ?5)",
175 params![id_str, signal.evaluator, signal.passed, signal.summary, errors_json],
176 )
177 .map_err(SqliteStorage::map_err)?;
178 }
179
180 Ok(())
181 }
182
183 async fn get_reflection(&self, id: &Uuid) -> Result<Option<Reflection>> {
184 let conn = self.conn.lock().map_err(|e| ReflectError::Storage(e.to_string()))?;
185 let id_str = id.to_string();
186
187 let signals = load_eval_signals(&conn, &id_str).map_err(SqliteStorage::map_err)?;
188
189 let result = conn
190 .query_row(
191 "SELECT id, task_description, draft, critique, lesson, outcome, pattern_id, tags, confidence, validation_count, contradiction_count, created_at, last_recalled
192 FROM reflections WHERE id = ?1",
193 params![id_str],
194 |row| row_to_reflection(row, signals.clone()),
195 )
196 .optional()
197 .map_err(SqliteStorage::map_err)?;
198
199 Ok(result)
200 }
201
202 async fn delete_reflection(&self, id: &Uuid) -> Result<bool> {
203 let conn = self.conn.lock().map_err(|e| ReflectError::Storage(e.to_string()))?;
204 let id_str = id.to_string();
205
206 conn.execute(
208 "DELETE FROM eval_signals WHERE reflection_id = ?1",
209 params![id_str],
210 )
211 .map_err(SqliteStorage::map_err)?;
212
213 let rows = conn
217 .execute("DELETE FROM reflections WHERE id = ?1", params![id_str])
218 .map_err(SqliteStorage::map_err)?;
219
220 Ok(rows > 0)
221 }
222
223 async fn search_reflections(
224 &self,
225 query: &str,
226 tags: &[String],
227 limit: usize,
228 ) -> Result<Vec<ScoredReflection>> {
229 let conn = self.conn.lock().map_err(|e| ReflectError::Storage(e.to_string()))?;
230
231 let mut stmt = conn
232 .prepare(
233 "SELECT r.id, r.task_description, r.draft, r.critique, r.lesson, r.outcome,
234 r.pattern_id, r.tags, r.confidence, r.validation_count,
235 r.contradiction_count, r.created_at, r.last_recalled, -fts.rank as score
236 FROM reflections_fts fts
237 JOIN reflections r ON r.rowid = fts.rowid
238 WHERE reflections_fts MATCH ?1
239 ORDER BY rank
240 LIMIT ?2",
241 )
242 .map_err(SqliteStorage::map_err)?;
243
244 let results = stmt
245 .query_map(params![query, limit as i64], |row| {
246 let score: f64 = row.get(13)?;
247 let reflection = row_to_reflection(row, vec![])?;
248 Ok(ScoredReflection {
249 reflection,
250 relevance_score: score,
251 })
252 })
253 .map_err(SqliteStorage::map_err)?
254 .collect::<std::result::Result<Vec<_>, _>>()
255 .map_err(SqliteStorage::map_err)?;
256
257 let mut filtered = Vec::new();
259 for mut sr in results {
260 let signals =
262 load_eval_signals(&conn, &sr.reflection.id.to_string()).map_err(SqliteStorage::map_err)?;
263 sr.reflection.error_signals = signals;
264
265 if !tags.is_empty() {
267 let has_all_tags = tags.iter().all(|tag| sr.reflection.tags.contains(tag));
268 if !has_all_tags {
269 continue;
270 }
271 }
272 filtered.push(sr);
273 }
274
275 Ok(filtered)
276 }
277
278 async fn upsert_pattern(&self, pattern: &ErrorPattern) -> Result<()> {
279 let conn = self.conn.lock().map_err(|e| ReflectError::Storage(e.to_string()))?;
280
281 conn.execute(
282 "INSERT INTO error_patterns (id, category, description, occurrences, first_seen, last_seen)
283 VALUES (?1, ?2, ?3, ?4, ?5, ?6)
284 ON CONFLICT(id) DO UPDATE SET
285 category = ?2,
286 description = ?3,
287 occurrences = ?4,
288 first_seen = ?5,
289 last_seen = ?6",
290 params![
291 pattern.id,
292 pattern.category,
293 pattern.description,
294 pattern.occurrences as i32,
295 pattern.first_seen.to_rfc3339(),
296 pattern.last_seen.to_rfc3339(),
297 ],
298 )
299 .map_err(SqliteStorage::map_err)?;
300
301 Ok(())
302 }
303
304 async fn get_pattern(&self, id: &str) -> Result<Option<ErrorPattern>> {
305 let conn = self.conn.lock().map_err(|e| ReflectError::Storage(e.to_string()))?;
306
307 let result = conn
308 .query_row(
309 "SELECT id, category, description, occurrences, first_seen, last_seen
310 FROM error_patterns WHERE id = ?1",
311 params![id],
312 |row| {
313 let id: String = row.get(0)?;
314 let category: String = row.get(1)?;
315 let description: String = row.get(2)?;
316 let occurrences: i32 = row.get(3)?;
317 let first_seen_str: String = row.get(4)?;
318 let last_seen_str: String = row.get(5)?;
319 Ok((id, category, description, occurrences, first_seen_str, last_seen_str))
320 },
321 )
322 .optional()
323 .map_err(SqliteStorage::map_err)?;
324
325 match result {
326 None => Ok(None),
327 Some((id, category, description, occurrences, first_seen_str, last_seen_str)) => {
328 let first_seen = parse_datetime(&first_seen_str)?;
329 let last_seen = parse_datetime(&last_seen_str)?;
330 Ok(Some(ErrorPattern {
331 id,
332 category,
333 description,
334 occurrences: occurrences as u32,
335 first_seen,
336 last_seen,
337 reflection_ids: vec![],
338 trend: Trend::Stable,
339 }))
340 }
341 }
342 }
343
344 async fn list_patterns(&self, min_occurrences: u32, limit: usize) -> Result<Vec<ErrorPattern>> {
345 let conn = self.conn.lock().map_err(|e| ReflectError::Storage(e.to_string()))?;
346
347 let mut stmt = conn
348 .prepare(
349 "SELECT id, category, description, occurrences, first_seen, last_seen
350 FROM error_patterns
351 WHERE occurrences >= ?1
352 ORDER BY occurrences DESC
353 LIMIT ?2",
354 )
355 .map_err(SqliteStorage::map_err)?;
356
357 let rows = stmt
358 .query_map(params![min_occurrences as i32, limit as i64], |row| {
359 let id: String = row.get(0)?;
360 let category: String = row.get(1)?;
361 let description: String = row.get(2)?;
362 let occurrences: i32 = row.get(3)?;
363 let first_seen_str: String = row.get(4)?;
364 let last_seen_str: String = row.get(5)?;
365 Ok((id, category, description, occurrences, first_seen_str, last_seen_str))
366 })
367 .map_err(SqliteStorage::map_err)?
368 .collect::<std::result::Result<Vec<_>, _>>()
369 .map_err(SqliteStorage::map_err)?;
370
371 let mut patterns = Vec::new();
372 for (id, category, description, occurrences, first_seen_str, last_seen_str) in rows {
373 let first_seen = parse_datetime(&first_seen_str)?;
374 let last_seen = parse_datetime(&last_seen_str)?;
375 patterns.push(ErrorPattern {
376 id,
377 category,
378 description,
379 occurrences: occurrences as u32,
380 first_seen,
381 last_seen,
382 reflection_ids: vec![],
383 trend: Trend::Stable,
384 });
385 }
386
387 Ok(patterns)
388 }
389
390 async fn get_stats(&self) -> Result<ReflectionStats> {
391 let conn = self.conn.lock().map_err(|e| ReflectError::Storage(e.to_string()))?;
392
393 let total_reflections: u64 = conn
394 .query_row("SELECT COUNT(*) FROM reflections", [], |row| row.get(0))
395 .map_err(SqliteStorage::map_err)?;
396
397 let success: u64 = conn
398 .query_row(
399 "SELECT COUNT(*) FROM reflections WHERE outcome = 'success'",
400 [],
401 |row| row.get(0),
402 )
403 .map_err(SqliteStorage::map_err)?;
404
405 let failure: u64 = conn
406 .query_row(
407 "SELECT COUNT(*) FROM reflections WHERE outcome = 'failure'",
408 [],
409 |row| row.get(0),
410 )
411 .map_err(SqliteStorage::map_err)?;
412
413 let partial: u64 = conn
414 .query_row(
415 "SELECT COUNT(*) FROM reflections WHERE outcome = 'partial'",
416 [],
417 |row| row.get(0),
418 )
419 .map_err(SqliteStorage::map_err)?;
420
421 let avg_confidence: f64 = conn
422 .query_row(
423 "SELECT COALESCE(AVG(confidence), 0.0) FROM reflections",
424 [],
425 |row| row.get(0),
426 )
427 .map_err(SqliteStorage::map_err)?;
428
429 let reflections_this_week: u64 = conn
430 .query_row(
431 "SELECT COUNT(*) FROM reflections WHERE created_at >= datetime('now', '-7 days')",
432 [],
433 |row| row.get(0),
434 )
435 .map_err(SqliteStorage::map_err)?;
436
437 let mut tag_stmt = conn
439 .prepare(
440 "SELECT value, COUNT(*) as cnt
441 FROM reflections, json_each(reflections.tags)
442 GROUP BY value
443 ORDER BY cnt DESC
444 LIMIT 10",
445 )
446 .map_err(SqliteStorage::map_err)?;
447
448 let top_tags = tag_stmt
449 .query_map([], |row| {
450 let tag: String = row.get(0)?;
451 let count: u64 = row.get(1)?;
452 Ok(TagCount { tag, count })
453 })
454 .map_err(SqliteStorage::map_err)?
455 .collect::<std::result::Result<Vec<_>, _>>()
456 .map_err(SqliteStorage::map_err)?;
457
458 let mut pat_stmt = conn
460 .prepare(
461 "SELECT id, category, description, occurrences, first_seen, last_seen
462 FROM error_patterns
463 ORDER BY occurrences DESC
464 LIMIT 5",
465 )
466 .map_err(SqliteStorage::map_err)?;
467
468 let pattern_rows = pat_stmt
469 .query_map([], |row| {
470 let id: String = row.get(0)?;
471 let category: String = row.get(1)?;
472 let description: String = row.get(2)?;
473 let occurrences: i32 = row.get(3)?;
474 let first_seen_str: String = row.get(4)?;
475 let last_seen_str: String = row.get(5)?;
476 Ok((id, category, description, occurrences, first_seen_str, last_seen_str))
477 })
478 .map_err(SqliteStorage::map_err)?
479 .collect::<std::result::Result<Vec<_>, _>>()
480 .map_err(SqliteStorage::map_err)?;
481
482 let mut top_patterns = Vec::new();
483 for (id, category, description, occurrences, first_seen_str, last_seen_str) in pattern_rows
484 {
485 let first_seen = parse_datetime(&first_seen_str)?;
486 let last_seen = parse_datetime(&last_seen_str)?;
487 top_patterns.push(ErrorPattern {
488 id,
489 category,
490 description,
491 occurrences: occurrences as u32,
492 first_seen,
493 last_seen,
494 reflection_ids: vec![],
495 trend: Trend::Stable,
496 });
497 }
498
499 Ok(ReflectionStats {
500 total_reflections,
501 by_outcome: OutcomeCounts {
502 success,
503 failure,
504 partial,
505 },
506 top_patterns,
507 top_tags,
508 avg_confidence,
509 reflections_this_week,
510 })
511 }
512}
513
514#[cfg(test)]
515mod tests {
516 use super::*;
517 use chrono::Utc;
518 use reflect_core::types::{EvalError, Outcome, Severity};
519 use uuid::Uuid;
520
521 fn test_reflection(task: &str, lesson: &str, tags: Vec<String>) -> Reflection {
522 Reflection {
523 id: Uuid::now_v7(),
524 task_description: task.into(),
525 draft: "let x = input.parse().unwrap();".into(),
526 error_signals: vec![],
527 critique: "used unwrap on user input".into(),
528 lesson: lesson.into(),
529 outcome: Outcome::Failure,
530 pattern_id: None,
531 tags,
532 confidence: 0.5,
533 validation_count: 0,
534 contradiction_count: 0,
535 created_at: Utc::now(),
536 last_recalled: None,
537 }
538 }
539
540 fn storage() -> SqliteStorage {
541 SqliteStorage::open_in_memory().expect("failed to open in-memory db")
542 }
543
544 #[tokio::test]
545 async fn store_and_get_reflection() {
546 let store = storage();
547 let r = test_reflection("parse user date", "use Result instead of unwrap", vec!["rust".into()]);
548 let id = r.id;
549
550 store.store_reflection(&r).await.unwrap();
551 let fetched = store.get_reflection(&id).await.unwrap().expect("should exist");
552
553 assert_eq!(fetched.id, id);
554 assert_eq!(fetched.task_description, "parse user date");
555 assert_eq!(fetched.lesson, "use Result instead of unwrap");
556 assert_eq!(fetched.tags, vec!["rust".to_string()]);
557 assert_eq!(fetched.outcome, Outcome::Failure);
558 }
559
560 #[tokio::test]
561 async fn delete_reflection() {
562 let store = storage();
563 let r = test_reflection("delete test", "lesson", vec![]);
564 let id = r.id;
565
566 store.store_reflection(&r).await.unwrap();
567 let deleted = store.delete_reflection(&id).await.unwrap();
568 assert!(deleted);
569
570 let fetched = store.get_reflection(&id).await.unwrap();
571 assert!(fetched.is_none());
572
573 let deleted_again = store.delete_reflection(&id).await.unwrap();
574 assert!(!deleted_again);
575 }
576
577 #[tokio::test]
578 async fn search_fts() {
579 let store = storage();
580 let r1 = test_reflection("parse user date input", "use chrono for dates", vec!["rust".into()]);
581 let r2 = test_reflection("handle network timeout", "add retry logic", vec!["networking".into()]);
582
583 store.store_reflection(&r1).await.unwrap();
584 store.store_reflection(&r2).await.unwrap();
585
586 let results = store.search_reflections("date", &[], 10).await.unwrap();
587 assert_eq!(results.len(), 1);
588 assert_eq!(results[0].reflection.task_description, "parse user date input");
589 assert!(results[0].relevance_score > 0.0);
590 }
591
592 #[tokio::test]
593 async fn search_by_tags() {
594 let store = storage();
595 let r1 = test_reflection("task one", "lesson one", vec!["rust".into(), "error-handling".into()]);
596 let r2 = test_reflection("task two", "lesson two", vec!["python".into()]);
597
598 store.store_reflection(&r1).await.unwrap();
599 store.store_reflection(&r2).await.unwrap();
600
601 let results = store
603 .search_reflections("unwrap", &["rust".into()], 10)
604 .await
605 .unwrap();
606
607 assert!(results.len() >= 1);
608 assert!(results.iter().all(|sr| sr.reflection.tags.contains(&"rust".to_string())));
609 }
610
611 #[tokio::test]
612 async fn upsert_and_list_patterns() {
613 let store = storage();
614 let now = Utc::now();
615
616 let pattern = ErrorPattern {
617 id: "unwrap-on-input".into(),
618 category: "error-handling".into(),
619 description: "Using unwrap on user input".into(),
620 occurrences: 3,
621 first_seen: now,
622 last_seen: now,
623 reflection_ids: vec![],
624 trend: Trend::Stable,
625 };
626
627 store.upsert_pattern(&pattern).await.unwrap();
628
629 let fetched = store.get_pattern("unwrap-on-input").await.unwrap().expect("should exist");
630 assert_eq!(fetched.occurrences, 3);
631 assert_eq!(fetched.category, "error-handling");
632
633 let updated = ErrorPattern {
635 occurrences: 5,
636 ..pattern.clone()
637 };
638 store.upsert_pattern(&updated).await.unwrap();
639 let fetched2 = store.get_pattern("unwrap-on-input").await.unwrap().unwrap();
640 assert_eq!(fetched2.occurrences, 5);
641
642 let all = store.list_patterns(1, 10).await.unwrap();
644 assert_eq!(all.len(), 1);
645
646 let filtered = store.list_patterns(10, 10).await.unwrap();
647 assert!(filtered.is_empty());
648 }
649
650 #[tokio::test]
651 async fn store_with_eval_signals() {
652 let store = storage();
653 let mut r = test_reflection("eval test", "check signals", vec![]);
654 r.error_signals = vec![EvalSignal {
655 evaluator: "cargo_test".into(),
656 passed: false,
657 summary: "1 test failed".into(),
658 errors: vec![EvalError {
659 file: Some("src/main.rs".into()),
660 line: Some(42),
661 column: None,
662 code: None,
663 message: "panicked at unwrap".into(),
664 severity: Severity::Error,
665 }],
666 }];
667 let id = r.id;
668
669 store.store_reflection(&r).await.unwrap();
670 let fetched = store.get_reflection(&id).await.unwrap().unwrap();
671
672 assert_eq!(fetched.error_signals.len(), 1);
673 assert_eq!(fetched.error_signals[0].evaluator, "cargo_test");
674 assert!(!fetched.error_signals[0].passed);
675 assert_eq!(fetched.error_signals[0].errors.len(), 1);
676 assert_eq!(fetched.error_signals[0].errors[0].line, Some(42));
677 assert_eq!(fetched.error_signals[0].errors[0].message, "panicked at unwrap");
678 }
679
680 #[tokio::test]
681 async fn get_stats() {
682 let store = storage();
683
684 let r1 = test_reflection("task 1", "lesson 1", vec!["rust".into(), "error-handling".into()]);
685 let mut r2 = test_reflection("task 2", "lesson 2", vec!["rust".into()]);
686 r2.outcome = Outcome::Success;
687 r2.confidence = 0.8;
688
689 store.store_reflection(&r1).await.unwrap();
690 store.store_reflection(&r2).await.unwrap();
691
692 let stats = store.get_stats().await.unwrap();
693 assert_eq!(stats.total_reflections, 2);
694 assert_eq!(stats.by_outcome.failure, 1);
695 assert_eq!(stats.by_outcome.success, 1);
696 assert_eq!(stats.by_outcome.partial, 0);
697 assert!((stats.avg_confidence - 0.65).abs() < 0.01);
698 assert!(stats.top_tags.iter().any(|t| t.tag == "rust" && t.count == 2));
699 }
700}