Download crates/forge_domain/src/session_metrics.rs from SaylorTwift/forgecode: direct link, hf CLI and curl.
- Browser
- Download file 13.6 kB
-
https://huggingface.co/SaylorTwift/forgecode/resolve/main/crates/forge_domain/src/session_metrics.rs
- Command line
-
hf download hf://SaylorTwift/forgecode/crates/forge_domain/src/session_metrics.rs
-
curl -L -o session_metrics.rs https://huggingface.co/SaylorTwift/forgecode/resolve/main/crates/forge_domain/src/session_metrics.rs
13.6 kB
| use std::collections::{HashMap, HashSet}; | |
| use std::time::Duration; | |
| use chrono::{DateTime, Utc}; | |
| use derive_setters::Setters; | |
| use serde::{Deserialize, Serialize}; | |
| use uuid::Uuid; | |
| pub use crate::file_operation::FileOperation; | |
| use crate::{Todo, TodoItem, TodoStatus}; | |
| pub struct Metrics { | |
| pub started_at: Option<DateTime<Utc>>, | |
| /// Holds the last file operation for each file | |
| pub file_operations: HashMap<String, FileOperation>, | |
| /// Tracks all files that have been read in this session | |
| pub files_accessed: HashSet<String>, | |
| /// Tracks all known todos for the session, including historical completed | |
| /// todos that were removed from active updates. | |
| pub todos: Vec<Todo>, | |
| } | |
| impl Metrics { | |
| /// Records a file operation, replacing any previous operation for the same | |
| /// file. Only Read operations are tracked in files_accessed. | |
| pub fn insert(mut self, path: String, metrics: FileOperation) -> Self { | |
| // Only track Read operations in files_accessed | |
| if metrics.tool == crate::ToolKind::Read { | |
| self.files_accessed.insert(path.clone()); | |
| } | |
| self.file_operations.insert(path, metrics); | |
| self | |
| } | |
| /// Gets the session duration if tracking has started | |
| pub fn duration(&self, now: DateTime<Utc>) -> Option<Duration> { | |
| self.started_at | |
| .map(|start| (now - start).to_std().unwrap_or_default()) | |
| } | |
| /// Returns todos currently in pending or in-progress states. | |
| pub fn get_active_todos(&self) -> Vec<Todo> { | |
| self.todos | |
| .iter() | |
| .filter(|todo| matches!(todo.status, TodoStatus::Pending | TodoStatus::InProgress)) | |
| .cloned() | |
| .collect() | |
| } | |
| /// Returns all known todos, including historical completed todos. | |
| pub fn get_todos(&self) -> &[Todo] { | |
| &self.todos | |
| } | |
| /// Applies a list of todo changes using content as the matching key. | |
| /// | |
| /// For each incoming item: | |
| /// - If `status` is `cancelled`: remove the matching item (if found). | |
| /// - If an item with the same content already exists: update its status. | |
| /// - Otherwise: add a new item with a server-generated ID. | |
| /// | |
| /// Completed items that are not mentioned in the incoming list are | |
| /// preserved in history. Active items (pending / in_progress) that are | |
| /// not mentioned remain unchanged. | |
| /// | |
| /// Returns the list of currently active (pending / in_progress) todos. | |
| /// | |
| /// # Errors | |
| /// | |
| /// Returns an error if any todo content is empty or exceeds 1000 | |
| /// characters. | |
| pub fn apply_todo_changes(&mut self, changes: Vec<TodoItem>) -> anyhow::Result<Vec<Todo>> { | |
| for item in &changes { | |
| if item.content.trim().is_empty() { | |
| anyhow::bail!("Todo content cannot be empty"); | |
| } | |
| if item.content.len() > 1000 { | |
| anyhow::bail!("Todo content exceeds maximum length of 1000 characters"); | |
| } | |
| } | |
| for item in changes { | |
| if item.status == TodoStatus::Cancelled { | |
| // Remove the item by content key | |
| self.todos.retain(|t| t.content != item.content); | |
| } else if let Some(existing) = self.todos.iter_mut().find(|t| t.content == item.content) | |
| { | |
| // Update in-place | |
| existing.status = item.status; | |
| } else { | |
| // Add new item with server-generated ID | |
| self.todos.push(Todo { | |
| id: Uuid::new_v4().to_string(), | |
| content: item.content, | |
| status: item.status, | |
| }); | |
| } | |
| } | |
| Ok(self.get_active_todos()) | |
| } | |
| } | |
| mod tests { | |
| use pretty_assertions::assert_eq; | |
| use super::*; | |
| use crate::ToolKind; | |
| fn test_metrics_new() { | |
| let actual = Metrics::default(); | |
| assert_eq!(actual.file_operations.len(), 0); | |
| } | |
| fn test_metrics_record_file_operation() { | |
| let fixture = Metrics::default() | |
| .insert( | |
| "file1.rs".to_string(), | |
| FileOperation::new(ToolKind::Write) | |
| .lines_added(10u64) | |
| .lines_removed(5u64) | |
| .content_hash(Some("hash1".to_string())), | |
| ) | |
| .insert( | |
| "file2.rs".to_string(), | |
| FileOperation::new(ToolKind::Patch) | |
| .lines_added(3u64) | |
| .lines_removed(2u64) | |
| .content_hash(Some("hash2".to_string())), | |
| ) | |
| .insert( | |
| "file1.rs".to_string(), | |
| FileOperation::new(ToolKind::Patch) | |
| .lines_added(5u64) | |
| .lines_removed(1u64) | |
| .content_hash(Some("hash1_v2".to_string())), | |
| ); | |
| let actual = fixture; | |
| // Check file1 has the last operation recorded (second add overwrites the first) | |
| let file1_metrics = actual.file_operations.get("file1.rs").unwrap(); | |
| assert_eq!(file1_metrics.lines_added, 5); | |
| assert_eq!(file1_metrics.lines_removed, 1); | |
| assert_eq!(file1_metrics.content_hash, Some("hash1_v2".to_string())); | |
| // Check file2 has its operation recorded | |
| let file2_metrics = actual.file_operations.get("file2.rs").unwrap(); | |
| assert_eq!(file2_metrics.lines_added, 3); | |
| assert_eq!(file2_metrics.lines_removed, 2); | |
| } | |
| fn test_metrics_record_file_operation_and_undo() { | |
| let path = "file_to_track.rs".to_string(); | |
| // Do operation | |
| let metrics = Metrics::default().insert( | |
| path.clone(), | |
| FileOperation::new(ToolKind::Write) | |
| .lines_added(2u64) | |
| .lines_removed(1u64) | |
| .content_hash(Some("hash_v1".to_string())), | |
| ); | |
| let operation = metrics.file_operations.get(&path).unwrap(); | |
| assert_eq!(metrics.file_operations.len(), 1); | |
| assert_eq!(operation.lines_added, 2); | |
| assert_eq!(operation.lines_removed, 1); | |
| assert_eq!(operation.content_hash, Some("hash_v1".to_string())); | |
| // Undo operation replaces the previous operation | |
| let metrics = metrics.insert( | |
| path.clone(), | |
| FileOperation::new(ToolKind::Undo).content_hash(Some("hash_v0".to_string())), | |
| ); | |
| let operation = metrics.file_operations.get(&path).unwrap(); | |
| assert_eq!(operation.lines_added, 0); | |
| assert_eq!(operation.lines_removed, 0); | |
| assert_eq!(operation.content_hash, Some("hash_v0".to_string())); | |
| } | |
| fn test_metrics_record_multiple_file_operations() { | |
| let path = "file1.rs".to_string(); | |
| let metrics = Metrics::default() | |
| .insert( | |
| path.clone(), | |
| FileOperation::new(ToolKind::Write) | |
| .lines_added(10u64) | |
| .lines_removed(5u64) | |
| .content_hash(Some("hash1".to_string())), | |
| ) | |
| .insert( | |
| path.clone(), | |
| FileOperation::new(ToolKind::Patch) | |
| .lines_added(5u64) | |
| .lines_removed(1u64) | |
| .content_hash(Some("hash2".to_string())), | |
| ) | |
| .insert( | |
| path.clone(), | |
| FileOperation::new(ToolKind::Undo).content_hash(Some("hash1".to_string())), | |
| ); | |
| // Only the last operation is stored | |
| let operation = metrics.file_operations.get(&path).unwrap(); | |
| // Last operation (undo) overwrites previous operations | |
| assert_eq!(operation.lines_added, 0); | |
| assert_eq!(operation.lines_removed, 0); | |
| assert_eq!(operation.content_hash, Some("hash1".to_string())); | |
| } | |
| fn test_files_accessed_only_tracks_reads() { | |
| let metrics = Metrics::default() | |
| .insert("file1.rs".to_string(), FileOperation::new(ToolKind::Read)) | |
| .insert( | |
| "file2.rs".to_string(), | |
| FileOperation::new(ToolKind::Write).lines_added(10u64), | |
| ) | |
| .insert("file3.rs".to_string(), FileOperation::new(ToolKind::Read)) | |
| .insert( | |
| "file3.rs".to_string(), | |
| FileOperation::new(ToolKind::Patch).lines_added(5u64), | |
| ); | |
| // Only Read operations should be in files_accessed | |
| // file3 was read first, then patched - it stays in files_accessed | |
| assert_eq!(metrics.files_accessed.len(), 2); | |
| assert!(metrics.files_accessed.contains("file1.rs")); | |
| assert!(metrics.files_accessed.contains("file3.rs")); | |
| assert!(!metrics.files_accessed.contains("file2.rs")); // Write only, not in set | |
| // file_operations should have the last operation for each file | |
| assert_eq!(metrics.file_operations.len(), 3); | |
| assert_eq!( | |
| metrics.file_operations.get("file1.rs").unwrap().tool, | |
| ToolKind::Read | |
| ); | |
| assert_eq!( | |
| metrics.file_operations.get("file2.rs").unwrap().tool, | |
| ToolKind::Write | |
| ); | |
| assert_eq!( | |
| metrics.file_operations.get("file3.rs").unwrap().tool, | |
| ToolKind::Patch | |
| ); | |
| } | |
| fn todo_item(content: &str, status: TodoStatus) -> TodoItem { | |
| TodoItem { content: content.to_string(), status } | |
| } | |
| fn test_apply_todo_changes_adds_new_items() { | |
| let mut fixture = Metrics::default(); | |
| let actual = fixture | |
| .apply_todo_changes(vec![ | |
| todo_item("Task A", TodoStatus::Pending), | |
| todo_item("Task B", TodoStatus::InProgress), | |
| ]) | |
| .unwrap(); | |
| let expected = [ | |
| fixture | |
| .todos | |
| .iter() | |
| .find(|t| t.content == "Task A") | |
| .cloned() | |
| .unwrap(), | |
| fixture | |
| .todos | |
| .iter() | |
| .find(|t| t.content == "Task B") | |
| .cloned() | |
| .unwrap(), | |
| ]; | |
| assert_eq!(actual.len(), 2); | |
| assert_eq!(actual[0].content, expected[0].content); | |
| assert_eq!(actual[1].content, expected[1].content); | |
| } | |
| fn test_apply_todo_changes_updates_by_content_key() { | |
| let mut fixture = Metrics::default(); | |
| fixture | |
| .apply_todo_changes(vec![todo_item("Task A", TodoStatus::Pending)]) | |
| .unwrap(); | |
| fixture | |
| .apply_todo_changes(vec![todo_item("Task A", TodoStatus::Completed)]) | |
| .unwrap(); | |
| let actual = fixture.get_todos().to_vec(); | |
| assert_eq!(actual.len(), 1); | |
| assert_eq!(actual[0].content, "Task A"); | |
| assert_eq!(actual[0].status, TodoStatus::Completed); | |
| } | |
| fn test_apply_todo_changes_cancelled_removes_item() { | |
| let mut fixture = Metrics::default(); | |
| fixture | |
| .apply_todo_changes(vec![ | |
| todo_item("Task A", TodoStatus::Pending), | |
| todo_item("Task B", TodoStatus::Pending), | |
| ]) | |
| .unwrap(); | |
| fixture | |
| .apply_todo_changes(vec![todo_item("Task A", TodoStatus::Cancelled)]) | |
| .unwrap(); | |
| let actual = fixture.get_todos().to_vec(); | |
| assert_eq!(actual.len(), 1); | |
| assert_eq!(actual[0].content, "Task B"); | |
| } | |
| fn test_apply_todo_changes_preserves_untouched_items() { | |
| let mut fixture = Metrics::default(); | |
| fixture | |
| .apply_todo_changes(vec![ | |
| todo_item("Task A", TodoStatus::Pending), | |
| todo_item("Task B", TodoStatus::Pending), | |
| ]) | |
| .unwrap(); | |
| // Only update Task A; Task B should remain untouched | |
| fixture | |
| .apply_todo_changes(vec![todo_item("Task A", TodoStatus::InProgress)]) | |
| .unwrap(); | |
| let todos = fixture.get_todos().to_vec(); | |
| assert_eq!(todos.len(), 2); | |
| let task_a = todos.iter().find(|t| t.content == "Task A").unwrap(); | |
| let task_b = todos.iter().find(|t| t.content == "Task B").unwrap(); | |
| assert_eq!(task_a.status, TodoStatus::InProgress); | |
| assert_eq!(task_b.status, TodoStatus::Pending); | |
| } | |
| fn test_apply_todo_changes_completed_stays_in_history() { | |
| let mut fixture = Metrics::default(); | |
| fixture | |
| .apply_todo_changes(vec![ | |
| todo_item("Task A", TodoStatus::InProgress), | |
| todo_item("Task B", TodoStatus::Pending), | |
| ]) | |
| .unwrap(); | |
| // Complete Task A — should remain in todos even if not sent again | |
| fixture | |
| .apply_todo_changes(vec![todo_item("Task A", TodoStatus::Completed)]) | |
| .unwrap(); | |
| // Only active todos are returned | |
| let active = fixture.get_active_todos(); | |
| assert_eq!(active.len(), 1); | |
| assert_eq!(active[0].content, "Task B"); | |
| // But Task A is still in the full list | |
| let all = fixture.get_todos().to_vec(); | |
| assert_eq!(all.len(), 2); | |
| } | |
| } | |