use std::collections::HashMap;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TransformReport {
pub id: String,
pub timestamp: i64,
pub execution: ExecutionMetadata,
pub recipes: Vec<RecipeSummary>,
pub files: FileSummary,
pub safety: SafetySummary,
pub timing: TimingSummary,
pub failures: Vec<FailureEntry>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rollback_session: Option<RollbackInfo>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExecutionMetadata {
pub command: String,
pub version: String,
pub mode: String,
pub project_root: PathBuf,
pub target_path: PathBuf,
pub allow_risky: bool,
pub strict: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RecipeSummary {
pub name: String,
pub description: String,
pub extensions: Vec<String>,
pub files_processed: usize,
pub files_changed: usize,
pub files_skipped: usize,
pub unsupported_patterns: usize,
#[serde(default)]
pub parse_failures: usize,
#[serde(default)]
pub execution_duration_ms: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileSummary {
pub total: usize,
pub parseable: usize,
pub analyses: Vec<FileAnalysis>,
pub skipped: Vec<SkippedFile>,
pub changed: Vec<ChangedFile>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileAnalysis {
pub path: PathBuf,
pub patterns: Vec<String>,
pub confidence: u8,
pub safety: String,
pub is_safe: bool,
#[serde(default)]
pub tags: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SkippedFile {
pub path: PathBuf,
pub reason: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChangedFile {
pub path: PathBuf,
pub lines_added: usize,
pub lines_removed: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub preview: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SafetySummary {
pub safe_count: usize,
pub risky_count: usize,
pub warnings: Vec<String>,
pub recommendations: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TimingSummary {
pub total_ms: u64,
pub per_recipe: HashMap<String, RecipeTiming>,
pub per_file: HashMap<PathBuf, u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RecipeTiming {
pub detect_ms: u64,
pub transform_ms: u64,
pub total_ms: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FailureEntry {
pub recipe: String,
pub stage: String,
pub error: String,
pub affected_files: Vec<PathBuf>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RollbackInfo {
pub session_id: String,
pub created_at: i64,
pub file_count: usize,
}
impl TransformReport {
pub fn new(id: String, execution: ExecutionMetadata) -> Self {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs() as i64;
Self {
id,
timestamp,
execution,
recipes: Vec::new(),
files: FileSummary {
total: 0,
parseable: 0,
analyses: Vec::new(),
skipped: Vec::new(),
changed: Vec::new(),
},
safety: SafetySummary {
safe_count: 0,
risky_count: 0,
warnings: Vec::new(),
recommendations: Vec::new(),
},
timing: TimingSummary {
total_ms: 0,
per_recipe: HashMap::new(),
per_file: HashMap::new(),
},
failures: Vec::new(),
rollback_session: None,
}
}
#[allow(dead_code)]
pub fn total_changes(&self) -> usize {
self.files.changed.len()
}
#[allow(dead_code)]
pub fn total_skipped(&self) -> usize {
self.files.skipped.len()
}
#[allow(dead_code)]
pub fn has_failures(&self) -> bool {
!self.failures.is_empty()
}
}
impl Default for TransformReport {
fn default() -> Self {
Self::new(
generate_report_id(),
ExecutionMetadata {
command: String::new(),
version: env!("CARGO_PKG_VERSION").to_string(),
mode: String::new(),
project_root: PathBuf::new(),
target_path: PathBuf::new(),
allow_risky: false,
strict: false,
},
)
}
}
fn generate_report_id() -> String {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
format!("morph-report-{}", timestamp)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_report_creation() {
let report = TransformReport::default();
assert!(!report.id.is_empty());
}
#[test]
fn test_report_totals() {
let mut report = TransformReport::default();
report.files.changed.push(ChangedFile {
path: PathBuf::from("test.js"),
lines_added: 10,
lines_removed: 5,
preview: None,
});
assert_eq!(report.total_changes(), 1);
assert_eq!(report.total_skipped(), 0);
assert!(!report.has_failures());
}
#[test]
fn test_serialization() {
let report = TransformReport::default();
let json = serde_json::to_string(&report).unwrap();
assert!(json.contains("morph-report-"));
let deserialized: TransformReport = serde_json::from_str(&json).unwrap();
assert_eq!(report.id, deserialized.id);
}
}