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