Skip to main content

morph_cli/core/
fingerprint.rs

1use std::collections::BTreeMap;
2use std::path::Path;
3use serde::{Serialize, Deserialize};
4use anyhow::{Context, Result};
5
6use crate::core::detection::scanner::Scanner;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct FileStatistics {
10    pub total_files: usize,
11    pub scanned_files: usize,
12    pub skipped_files: usize,
13}
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct ProjectFingerprint {
17    pub timestamp: u64,
18    pub detected_frameworks: Vec<String>,
19    pub dependencies: BTreeMap<String, String>,
20    pub workspaces: Vec<String>,
21    pub file_statistics: FileStatistics,
22}
23
24const FINGERPRINT_PATH: &str = ".morph-cli/project.json";
25
26impl ProjectFingerprint {
27    pub fn generate(project_root: &Path) -> Result<Self> {
28        let mut scanner = Scanner::new(project_root.to_path_buf());
29        let result = scanner.scan();
30
31        let detected_frameworks = result.detection.frameworks.iter()
32            .map(|f| {
33                if let Some(v) = &f.version {
34                    format!("{} ({})", f.name, v)
35                } else {
36                    f.name.clone()
37                }
38            })
39            .collect();
40
41        // Dependencies
42        let mut dependencies = BTreeMap::new();
43        let pkg_path = project_root.join("package.json");
44        if let Some(pkg) = crate::core::detection::package_json::PackageJson::load(&pkg_path) {
45            for (k, v) in pkg.dependencies {
46                dependencies.insert(k, v);
47            }
48            for (k, v) in pkg.dev_dependencies {
49                dependencies.insert(k, v);
50            }
51        }
52
53        // Workspaces
54        let workspaces = result.workspace.packages.iter()
55            .map(|p| p.name.clone())
56            .collect();
57
58        let file_statistics = FileStatistics {
59            total_files: result.total_files,
60            scanned_files: result.scanned_files.len(),
61            skipped_files: result.skipped_files.len(),
62        };
63
64        let timestamp = std::time::SystemTime::now()
65            .duration_since(std::time::UNIX_EPOCH)
66            .unwrap_or_default()
67            .as_secs();
68
69        Ok(Self {
70            timestamp,
71            detected_frameworks,
72            dependencies,
73            workspaces,
74            file_statistics,
75        })
76    }
77
78    pub fn save(&self, project_root: &Path) -> Result<()> {
79        let path = project_root.join(FINGERPRINT_PATH);
80        if let Some(parent) = path.parent() {
81            std::fs::create_dir_all(parent)?;
82        }
83        let json = serde_json::to_string_pretty(self)
84            .context("Failed to serialize project fingerprint")?;
85        std::fs::write(&path, json)
86            .with_context(|| format!("Failed to write project fingerprint: {}", path.display()))?;
87        Ok(())
88    }
89
90    pub fn load(project_root: &Path) -> Result<Option<Self>> {
91        let path = project_root.join(FINGERPRINT_PATH);
92        if !path.exists() {
93            return Ok(None);
94        }
95        let content = std::fs::read_to_string(&path)
96            .with_context(|| format!("Failed to read project fingerprint: {}", path.display()))?;
97        let fingerprint = serde_json::from_str(&content)
98            .with_context(|| format!("Failed to parse project fingerprint: {}", path.display()))?;
99        Ok(Some(fingerprint))
100    }
101
102    pub fn compatibility_check(&self, current: &Self) -> Result<(), String> {
103        // 1. Check frameworks
104        let self_fws: std::collections::BTreeSet<_> = self.detected_frameworks.iter().collect();
105        let current_fws: std::collections::BTreeSet<_> = current.detected_frameworks.iter().collect();
106        if self_fws != current_fws {
107            return Err(format!(
108                "Detected frameworks changed: current={:?}, recorded={:?}",
109                current.detected_frameworks, self.detected_frameworks
110            ));
111        }
112
113        // 2. Check workspaces
114        let self_ws: std::collections::BTreeSet<_> = self.workspaces.iter().collect();
115        let current_ws: std::collections::BTreeSet<_> = current.workspaces.iter().collect();
116        if self_ws != current_ws {
117            return Err(format!(
118                "Workspace packages structure changed: current={:?}, recorded={:?}",
119                current.workspaces, self.workspaces
120            ));
121        }
122
123        // 3. Check file count change (more than 50% change or absolute diff > 100)
124        let diff = (self.file_statistics.total_files as isize - current.file_statistics.total_files as isize).abs();
125        if self.file_statistics.total_files > 0 {
126            let percentage = (diff as f64 / self.file_statistics.total_files as f64) * 100.0;
127            if percentage > 50.0 && diff > 20 {
128                return Err(format!(
129                    "File counts changed significantly: current={}, recorded={} ({:.1}% change)",
130                    current.file_statistics.total_files, self.file_statistics.total_files, percentage
131                ));
132            }
133        }
134
135        Ok(())
136    }
137}