1mod detect;
2pub mod learn;
3mod model;
4mod persist;
5
6pub use detect::{detect_error_pattern, normalize_error_signature, DetectedError};
7pub use model::{
8 ErrorEntry, FixEntry, Gotcha, GotchaCategory, GotchaSeverity, GotchaSource, GotchaStats,
9 GotchaStore, PendingError, SessionErrorLog,
10};
11pub use persist::{load_universal_gotchas, save_universal_gotchas};
12
13use chrono::{DateTime, Utc};
14use detect::command_base;
15use model::{gotcha_id, DECAY_ARCHIVE_THRESHOLD, MAX_GOTCHAS, MAX_PENDING, MAX_SESSION_LOGS};
16
17impl GotchaStore {
18 pub fn detect_error(
21 &mut self,
22 output: &str,
23 command: &str,
24 exit_code: i32,
25 files_touched: &[String],
26 session_id: &str,
27 ) -> bool {
28 self.pending_errors.retain(|p| !p.is_expired());
29
30 let Some(detected) = detect_error_pattern(output, command, exit_code) else {
31 return false;
32 };
33
34 let signature = normalize_error_signature(&detected.raw_message);
35 let snippet = output.chars().take(500).collect::<String>();
36
37 self.pending_errors.push(PendingError {
38 error_signature: signature.clone(),
39 category: detected.category,
40 severity: detected.severity,
41 command: command.to_string(),
42 exit_code,
43 files_at_error: files_touched.to_vec(),
44 timestamp: Utc::now(),
45 raw_snippet: snippet,
46 session_id: session_id.to_string(),
47 });
48
49 if self.pending_errors.len() > MAX_PENDING {
50 self.pending_errors.remove(0);
51 }
52
53 self.log_error(session_id, &signature, command);
54 self.stats.total_errors_detected += 1;
55 true
56 }
57
58 pub fn try_resolve_pending(
59 &mut self,
60 command: &str,
61 files_touched: &[String],
62 session_id: &str,
63 ) -> Option<Gotcha> {
64 self.pending_errors.retain(|p| !p.is_expired());
65
66 let cmd_base = command_base(command);
67 let idx = self
68 .pending_errors
69 .iter()
70 .position(|p| command_base(&p.command) == cmd_base)?;
71
72 let pending = self.pending_errors.remove(idx);
73
74 let changed_files: Vec<String> = files_touched
75 .iter()
76 .filter(|f| !pending.files_at_error.contains(f))
77 .cloned()
78 .collect();
79
80 let resolution = if changed_files.is_empty() {
81 format!("Fixed after re-running {cmd_base}")
82 } else {
83 format!("Fixed by editing: {}", changed_files.join(", "))
84 };
85
86 let mut gotcha = Gotcha::new(
87 pending.category,
88 pending.severity,
89 &pending.error_signature,
90 &resolution,
91 GotchaSource::AutoDetected {
92 command: command.to_string(),
93 exit_code: pending.exit_code,
94 },
95 session_id,
96 );
97 gotcha.file_patterns.clone_from(&changed_files);
98
99 self.add_or_merge(gotcha.clone());
100 self.log_fix(
101 session_id,
102 &pending.error_signature,
103 &resolution,
104 &changed_files,
105 );
106 self.stats.total_fixes_correlated += 1;
107 self.updated_at = Utc::now();
108
109 Some(gotcha)
110 }
111
112 pub fn report_gotcha(
115 &mut self,
116 trigger: &str,
117 resolution: &str,
118 category: &str,
119 severity: &str,
120 session_id: &str,
121 ) -> Option<&Gotcha> {
122 let cat = GotchaCategory::from_str_loose(category);
123 let sev = match severity.to_lowercase().as_str() {
124 "critical" => GotchaSeverity::Critical,
125 "info" => GotchaSeverity::Info,
126 _ => GotchaSeverity::Warning,
127 };
128 let id = gotcha_id(trigger, &cat);
129 let gotcha = Gotcha::new(
130 cat,
131 sev,
132 trigger,
133 resolution,
134 GotchaSource::AgentReported {
135 session_id: session_id.to_string(),
136 },
137 session_id,
138 );
139 self.add_or_merge(gotcha);
140 self.updated_at = Utc::now();
141 self.gotchas.iter().find(|g| g.id == id)
142 }
143
144 fn add_or_merge(&mut self, new: Gotcha) {
147 if let Some(existing) = self.gotchas.iter_mut().find(|g| g.id == new.id) {
148 existing.merge_with(&new);
149 } else {
150 self.gotchas.push(new);
151 if self.gotchas.len() > MAX_GOTCHAS {
152 self.gotchas.sort_by(|a, b| {
153 b.confidence
154 .partial_cmp(&a.confidence)
155 .unwrap_or(std::cmp::Ordering::Equal)
156 });
157 self.gotchas.truncate(MAX_GOTCHAS);
158 }
159 }
160 }
161
162 fn log_error(&mut self, session_id: &str, signature: &str, command: &str) {
165 let log = self.get_or_create_session_log(session_id);
166 log.errors.push(ErrorEntry {
167 signature: signature.to_string(),
168 command: command.to_string(),
169 timestamp: Utc::now(),
170 });
171 }
172
173 fn log_fix(&mut self, session_id: &str, error_sig: &str, resolution: &str, files: &[String]) {
174 let log = self.get_or_create_session_log(session_id);
175 log.fixes.push(FixEntry {
176 error_signature: error_sig.to_string(),
177 resolution: resolution.to_string(),
178 files_changed: files.to_vec(),
179 timestamp: Utc::now(),
180 });
181 }
182
183 fn get_or_create_session_log(&mut self, session_id: &str) -> &mut SessionErrorLog {
184 if !self.error_log.iter().any(|l| l.session_id == session_id) {
185 self.error_log.push(SessionErrorLog {
186 session_id: session_id.to_string(),
187 timestamp: Utc::now(),
188 errors: Vec::new(),
189 fixes: Vec::new(),
190 });
191 if self.error_log.len() > MAX_SESSION_LOGS {
192 self.error_log.remove(0);
193 }
194 }
195 self.error_log
196 .iter_mut()
197 .find(|l| l.session_id == session_id)
198 .expect("session log must exist after push")
199 }
200
201 pub fn cross_session_boost(&mut self) {
202 let mut sig_sessions: std::collections::HashMap<String, Vec<String>> =
203 std::collections::HashMap::new();
204
205 for log in &self.error_log {
206 for err in &log.errors {
207 sig_sessions
208 .entry(err.signature.clone())
209 .or_default()
210 .push(log.session_id.clone());
211 }
212 }
213
214 for gotcha in &mut self.gotchas {
215 if let Some(sessions) = sig_sessions.get(&gotcha.trigger) {
216 let unique: Vec<String> = sessions
217 .iter()
218 .filter(|s| !gotcha.session_ids.contains(s))
219 .cloned()
220 .collect();
221 if !unique.is_empty() {
222 let boost = 0.15 * unique.len() as f32;
223 gotcha.confidence = (gotcha.confidence + boost).min(0.95);
224 for s in unique {
225 gotcha.session_ids.push(s);
226 }
227 gotcha.source = GotchaSource::CrossSessionCorrelated {
228 sessions: gotcha.session_ids.clone(),
229 };
230 }
231 }
232 }
233 }
234
235 pub fn apply_decay(&mut self) {
238 let now = Utc::now();
239 let mut decayed = 0u64;
240
241 for gotcha in &mut self.gotchas {
242 let days_since = (now - gotcha.last_seen).num_days().max(0) as f32;
243 if days_since < 1.0 {
244 continue;
245 }
246 let base_rate = gotcha.source.decay_rate();
247 let occurrence_factor = 1.0 / (1.0 + gotcha.occurrences as f32 * 0.1);
248 let decay = base_rate * occurrence_factor * (days_since / 7.0);
249 gotcha.confidence = (gotcha.confidence - decay).max(0.0);
250 }
251
252 let before = self.gotchas.len();
253 self.gotchas
254 .retain(|g| g.confidence >= DECAY_ARCHIVE_THRESHOLD);
255 decayed += (before - self.gotchas.len()) as u64;
256
257 self.stats.gotchas_decayed += decayed;
258 }
259
260 pub fn check_promotions(&mut self) -> Vec<(String, String, String, f32)> {
263 let mut promoted = Vec::new();
264 for gotcha in &self.gotchas {
265 if gotcha.is_promotable() {
266 promoted.push((
267 gotcha.category.to_string(),
268 gotcha.trigger.clone(),
269 gotcha.resolution.clone(),
270 gotcha.confidence,
271 ));
272 }
273 }
274 self.stats.gotchas_promoted += promoted.len() as u64;
275 promoted
276 }
277
278 pub fn extract_universal(&self) -> Vec<Gotcha> {
281 self.gotchas
282 .iter()
283 .filter(|g| {
284 g.category == GotchaCategory::Platform
285 && g.occurrences >= 10
286 && g.session_ids.len() >= 5
287 })
288 .cloned()
289 .collect()
290 }
291
292 pub fn top_relevant(&self, files_touched: &[String], limit: usize) -> Vec<&Gotcha> {
295 let mut scored: Vec<(&Gotcha, f32)> = self
296 .gotchas
297 .iter()
298 .map(|g| (g, relevance_score(g, files_touched)))
299 .filter(|(_, s)| *s > 0.5)
300 .collect();
301
302 scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
303 scored.into_iter().take(limit).map(|(g, _)| g).collect()
304 }
305
306 pub fn format_injection_block(&self, files_touched: &[String]) -> String {
307 let relevant = self.top_relevant(files_touched, 7);
308 if relevant.is_empty() {
309 return String::new();
310 }
311
312 let mut lines = Vec::with_capacity(relevant.len() + 2);
313 lines.push("--- PROJECT GOTCHAS (do NOT repeat these mistakes) ---".to_string());
314
315 for g in &relevant {
316 let prefix = g.severity.prefix();
317 let label = g.category.short_label();
318 let sessions = g.session_ids.len();
319 let age = format_age(g.last_seen);
320 let trigger = crate::core::sanitize::neutralize_metadata(&g.trigger);
321 let resolution = crate::core::sanitize::neutralize_metadata(&g.resolution);
322
323 let source_hint = match &g.source {
324 GotchaSource::AgentReported { .. } => ", agent-confirmed".to_string(),
325 GotchaSource::CrossSessionCorrelated { .. } => {
326 format!(", across {sessions} sessions")
327 }
328 GotchaSource::AutoDetected { .. } => ", auto-detected".to_string(),
329 GotchaSource::Promoted { .. } => ", proven".to_string(),
330 };
331
332 let prevented = if g.prevented_count > 0 {
333 format!(", prevented {}x", g.prevented_count)
334 } else {
335 String::new()
336 };
337
338 lines.push(format!("[{prefix}{label}] {trigger}"));
339 lines.push(format!(
340 " FIX: {} (seen {}x{}{}, {})",
341 resolution, g.occurrences, source_hint, prevented, age
342 ));
343 }
344
345 lines.push("---".to_string());
346 crate::core::sanitize::fence_content("project_gotchas", &lines.join("\n"))
347 }
348
349 pub fn mark_prevented(&mut self, gotcha_id: &str) {
352 if let Some(g) = self.gotchas.iter_mut().find(|g| g.id == gotcha_id) {
353 g.prevented_count += 1;
354 g.confidence = (g.confidence + 0.05).min(0.99);
355 self.stats.total_prevented += 1;
356 }
357 }
358
359 pub fn format_list(&self) -> String {
362 if self.gotchas.is_empty() {
363 return "No gotchas recorded for this project.".to_string();
364 }
365
366 let mut out = Vec::new();
367 out.push(format!(" {} active gotchas\n", self.gotchas.len()));
368
369 let mut sorted = self.gotchas.clone();
370 sorted.sort_by(|a, b| {
371 b.confidence
372 .partial_cmp(&a.confidence)
373 .unwrap_or(std::cmp::Ordering::Equal)
374 });
375
376 for g in &sorted {
377 let prefix = g.severity.prefix();
378 let label = g.category.short_label();
379 let conf = (g.confidence * 100.0) as u32;
380 let source = match &g.source {
381 GotchaSource::AutoDetected { .. } => "auto",
382 GotchaSource::AgentReported { .. } => "agent",
383 GotchaSource::CrossSessionCorrelated { .. } => "cross-session",
384 GotchaSource::Promoted { .. } => "promoted",
385 };
386 out.push(format!(
387 " [{prefix}{label:8}] {} ({}x, {} sessions, {source}, confidence: {conf}%)",
388 truncate_str(&g.trigger, 60),
389 g.occurrences,
390 g.session_ids.len(),
391 ));
392 out.push(format!(
393 " FIX: {}",
394 truncate_str(&g.resolution, 70)
395 ));
396 if g.prevented_count > 0 {
397 out.push(format!(" Prevented: {}x", g.prevented_count));
398 }
399 out.push(String::new());
400 }
401
402 out.push(format!(
403 " Stats: {} errors detected | {} fixes correlated | {} prevented",
404 self.stats.total_errors_detected,
405 self.stats.total_fixes_correlated,
406 self.stats.total_prevented,
407 ));
408
409 out.join("\n")
410 }
411}
412
413pub fn relevance_score(gotcha: &Gotcha, files_touched: &[String]) -> f32 {
418 let mut score: f32 = 0.0;
419
420 score += (gotcha.occurrences as f32 * gotcha.confidence).min(10.0);
421
422 let hours_ago = (Utc::now() - gotcha.last_seen).num_hours().max(0) as f32;
423 score += 5.0 * (-hours_ago / 168.0).exp();
424
425 let overlap = gotcha
426 .file_patterns
427 .iter()
428 .filter(|fp| {
429 files_touched
430 .iter()
431 .any(|ft| ft.contains(fp.as_str()) || fp.contains(ft.as_str()))
432 })
433 .count();
434 score += overlap as f32 * 3.0;
435
436 score *= gotcha.severity.multiplier();
437
438 if gotcha.session_ids.len() >= 3 {
439 score *= 1.3;
440 }
441
442 if gotcha.prevented_count > 0 {
443 score *= 1.2;
444 }
445
446 score
447}
448
449fn format_age(dt: DateTime<Utc>) -> String {
454 let diff = Utc::now() - dt;
455 let hours = diff.num_hours();
456 if hours < 1 {
457 format!("{}m ago", diff.num_minutes().max(1))
458 } else if hours < 24 {
459 format!("{hours}h ago")
460 } else {
461 format!("{}d ago", diff.num_days())
462 }
463}
464
465fn truncate_str(s: &str, max: usize) -> String {
466 if s.len() <= max {
467 s.to_string()
468 } else {
469 format!("{}...", &s[..max.saturating_sub(3)])
470 }
471}
472
473#[cfg(test)]
478mod tests {
479 use super::*;
480
481 #[test]
482 fn detect_cargo_error() {
483 let output = r"error[E0507]: cannot move out of `self.field` which is behind a shared reference
484 --> src/server.rs:42:13";
485 let result = detect_error_pattern(output, "cargo build", 1);
486 assert!(result.is_some());
487 let d = result.unwrap();
488 assert_eq!(d.category, GotchaCategory::Build);
489 assert_eq!(d.severity, GotchaSeverity::Critical);
490 assert!(d.raw_message.contains("E0507"));
491 }
492
493 #[test]
494 fn detect_npm_error() {
495 let output = "npm ERR! ERESOLVE unable to resolve dependency tree";
496 let result = detect_error_pattern(output, "npm install", 1);
497 assert!(result.is_some());
498 assert_eq!(result.unwrap().category, GotchaCategory::Dependency);
499 }
500
501 #[test]
502 fn detect_python_traceback() {
503 let output = "Traceback (most recent call last):\n File \"app.py\", line 5\nImportError: No module named 'flask'";
504 let result = detect_error_pattern(output, "python app.py", 1);
505 assert!(result.is_some());
506 assert_eq!(result.unwrap().category, GotchaCategory::Runtime);
507 }
508
509 #[test]
510 fn detect_typescript_error() {
511 let output =
512 "src/index.ts(10,5): error TS2339: Property 'foo' does not exist on type 'Bar'.";
513 let result = detect_error_pattern(output, "npx tsc", 1);
514 assert!(result.is_some());
515 assert_eq!(result.unwrap().category, GotchaCategory::Build);
516 }
517
518 #[test]
519 fn detect_go_error() {
520 let output = "./main.go:15:2: undefined: SomeFunc";
521 let result = detect_error_pattern(output, "go build", 1);
522 assert!(result.is_some());
523 }
524
525 #[test]
526 fn detect_jest_failure() {
527 let output = "FAIL src/app.test.ts\n TypeError: Cannot read properties of undefined";
528 let result = detect_error_pattern(output, "npx jest", 1);
529 assert!(result.is_some());
530 assert_eq!(result.unwrap().category, GotchaCategory::Test);
531 }
532
533 #[test]
534 fn no_false_positive_on_success() {
535 let output = "Compiling lean-ctx v2.17.2\nFinished release target(s) in 30s";
536 let result = detect_error_pattern(output, "cargo build --release", 0);
537 assert!(result.is_none());
538 }
539
540 #[test]
541 fn normalize_signature_strips_paths() {
542 let raw = "error[E0507]: cannot move out of /Users/foo/project/src/main.rs:42:13";
543 let sig = normalize_error_signature(raw);
544 assert!(!sig.contains("/Users/foo"));
545 assert!(sig.contains("E0507"));
546 assert!(sig.contains(":_:_"));
547 }
548
549 #[test]
550 fn gotcha_store_add_and_merge() {
551 let mut store = GotchaStore::new("testhash");
552 let g1 = Gotcha::new(
553 GotchaCategory::Build,
554 GotchaSeverity::Critical,
555 "error E0507",
556 "use clone",
557 GotchaSource::AutoDetected {
558 command: "cargo build".into(),
559 exit_code: 1,
560 },
561 "s1",
562 );
563 store.add_or_merge(g1.clone());
564 assert_eq!(store.gotchas.len(), 1);
565
566 let g2 = Gotcha::new(
567 GotchaCategory::Build,
568 GotchaSeverity::Critical,
569 "error E0507",
570 "use ref pattern",
571 GotchaSource::AutoDetected {
572 command: "cargo build".into(),
573 exit_code: 1,
574 },
575 "s2",
576 );
577 store.add_or_merge(g2);
578 assert_eq!(store.gotchas.len(), 1);
579 assert_eq!(store.gotchas[0].occurrences, 2);
580 assert_eq!(store.gotchas[0].session_ids.len(), 2);
581 }
582
583 #[test]
584 fn gotcha_store_detect_and_resolve() {
585 let mut store = GotchaStore::new("testhash");
586
587 let error_output = "error[E0507]: cannot move out of `self.name`";
588 let detected = store.detect_error(error_output, "cargo build", 1, &[], "s1");
589 assert!(detected);
590 assert_eq!(store.pending_errors.len(), 1);
591
592 let resolved =
593 store.try_resolve_pending("cargo build --release", &["src/main.rs".into()], "s1");
594 assert!(resolved.is_some());
595 assert_eq!(store.gotchas.len(), 1);
596 assert!(store.gotchas[0].resolution.contains("src/main.rs"));
597 }
598
599 #[test]
600 fn agent_report_gotcha() {
601 let mut store = GotchaStore::new("testhash");
602 let g = store
603 .report_gotcha(
604 "Use thiserror not anyhow",
605 "Derive thiserror::Error in library code",
606 "convention",
607 "warning",
608 "s1",
609 )
610 .expect("gotcha should be retained in empty store");
611 assert_eq!(g.confidence, 0.9);
612 assert_eq!(g.category, GotchaCategory::Convention);
613 }
614
615 #[test]
616 fn decay_reduces_confidence() {
617 let mut store = GotchaStore::new("testhash");
618 let mut g = Gotcha::new(
619 GotchaCategory::Build,
620 GotchaSeverity::Warning,
621 "test error",
622 "test fix",
623 GotchaSource::AutoDetected {
624 command: "test".into(),
625 exit_code: 1,
626 },
627 "s1",
628 );
629 g.last_seen = Utc::now() - chrono::Duration::days(30);
630 g.confidence = 0.5;
631 store.gotchas.push(g);
632
633 store.apply_decay();
634 assert!(store.gotchas[0].confidence < 0.5);
635 }
636
637 #[test]
638 fn decay_archives_low_confidence() {
639 let mut store = GotchaStore::new("testhash");
640 let mut g = Gotcha::new(
641 GotchaCategory::Build,
642 GotchaSeverity::Info,
643 "old error",
644 "old fix",
645 GotchaSource::AutoDetected {
646 command: "test".into(),
647 exit_code: 1,
648 },
649 "s1",
650 );
651 g.last_seen = Utc::now() - chrono::Duration::days(90);
652 g.confidence = 0.16;
653 store.gotchas.push(g);
654
655 store.apply_decay();
656 assert!(store.gotchas.is_empty());
657 }
658
659 #[test]
660 fn relevance_score_higher_for_recent() {
661 let recent = Gotcha::new(
662 GotchaCategory::Build,
663 GotchaSeverity::Critical,
664 "error A",
665 "fix A",
666 GotchaSource::AutoDetected {
667 command: "test".into(),
668 exit_code: 1,
669 },
670 "s1",
671 );
672 let mut old = recent.clone();
673 old.last_seen = Utc::now() - chrono::Duration::days(14);
674
675 let score_recent = relevance_score(&recent, &[]);
676 let score_old = relevance_score(&old, &[]);
677 assert!(score_recent > score_old);
678 }
679
680 #[test]
681 fn relevance_score_file_overlap_boost() {
682 let mut g = Gotcha::new(
683 GotchaCategory::Build,
684 GotchaSeverity::Warning,
685 "error B",
686 "fix B",
687 GotchaSource::AutoDetected {
688 command: "test".into(),
689 exit_code: 1,
690 },
691 "s1",
692 );
693 g.file_patterns = vec!["src/server.rs".to_string()];
694
695 let with_overlap = relevance_score(&g, &["src/server.rs".to_string()]);
696 let without_overlap = relevance_score(&g, &["src/other.rs".to_string()]);
697 assert!(with_overlap > without_overlap);
698 }
699
700 #[test]
701 fn cross_session_boost_increases_confidence() {
702 let mut store = GotchaStore::new("testhash");
703 let mut g = Gotcha::new(
704 GotchaCategory::Build,
705 GotchaSeverity::Critical,
706 "recurring error",
707 "recurring fix",
708 GotchaSource::AutoDetected {
709 command: "cargo build".into(),
710 exit_code: 1,
711 },
712 "s1",
713 );
714 g.confidence = 0.6;
715 store.gotchas.push(g);
716
717 store.error_log.push(SessionErrorLog {
718 session_id: "s2".into(),
719 timestamp: Utc::now(),
720 errors: vec![ErrorEntry {
721 signature: "recurring error".into(),
722 command: "cargo build".into(),
723 timestamp: Utc::now(),
724 }],
725 fixes: vec![],
726 });
727 store.error_log.push(SessionErrorLog {
728 session_id: "s3".into(),
729 timestamp: Utc::now(),
730 errors: vec![ErrorEntry {
731 signature: "recurring error".into(),
732 command: "cargo build".into(),
733 timestamp: Utc::now(),
734 }],
735 fixes: vec![],
736 });
737
738 store.cross_session_boost();
739 assert!(store.gotchas[0].confidence > 0.6);
740 assert!(store.gotchas[0].session_ids.len() >= 3);
741 }
742
743 #[test]
744 fn promotion_criteria() {
745 let mut g = Gotcha::new(
746 GotchaCategory::Convention,
747 GotchaSeverity::Warning,
748 "use thiserror",
749 "derive thiserror::Error",
750 GotchaSource::AgentReported {
751 session_id: "s1".into(),
752 },
753 "s1",
754 );
755 g.confidence = 0.95;
756 g.occurrences = 6;
757 g.session_ids = vec!["s1".into(), "s2".into(), "s3".into()];
758 g.prevented_count = 3;
759 assert!(g.is_promotable());
760
761 g.occurrences = 2;
762 assert!(!g.is_promotable());
763 }
764
765 #[test]
766 fn format_injection_block_empty() {
767 let store = GotchaStore::new("testhash");
768 assert!(store.format_injection_block(&[]).is_empty());
769 }
770
771 #[test]
772 fn format_injection_block_with_gotchas() {
773 let mut store = GotchaStore::new("testhash");
774 store.add_or_merge(Gotcha::new(
775 GotchaCategory::Build,
776 GotchaSeverity::Critical,
777 "cargo E0507",
778 "use clone",
779 GotchaSource::AutoDetected {
780 command: "cargo build".into(),
781 exit_code: 1,
782 },
783 "s1",
784 ));
785
786 let block = store.format_injection_block(&[]);
787 assert!(block.contains("PROJECT GOTCHAS"));
788 assert!(block.contains("cargo E0507"));
789 assert!(block.contains("use clone"));
790 }
791}