{"messages":[{"content":"You are a constitutional council ranking individual git commits for ownership allocation.\n\nCompare these two commits. Decide which contributed more lasting value to the project.\n\nJudge substance, not spectacle:\n- Prefer correct, lasting design and real bugfixes over churn, formatting, renames, or generated noise.\n- Prefer clarity and necessity over sheer line count. A small precise change can beat a large diffuse one.\n- Do not favor a side merely because its patch is longer or noisier.\n- Weight what the change does for the project, not the contributor's name.\n\nReturn ONLY a JSON object: {\"winner\": \"A\" or \"B\", \"ratio\": \"N:M\", \"explanation\": \"...\"}\nThe explanation must cite concrete differences in the patches (1-3 sentences).\n\nSide A — contributor: tommy-mor\nSide A — commit message:\n[2a94401b] Run rustfmt workspace-wide and fix lint tooling.\n\nPin rustfmt and clippy in rust-toolchain.toml after a broken component install, merge a duplicate vote-slider CSS rule, and add VS Code settings so rust-analyzer uses the project toolchain.\n\nCo-authored-by: Cursor \n\nSide A — unified diff (full patch):\ndiff --git a/.vscode/settings.json b/.vscode/settings.json\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..98ebd754a6d5a972550506c69c5a10c10e3210b6\n--- /dev/null\n+++ b/.vscode/settings.json\n@@ -0,0 +1,8 @@\n+{\n+ \"rust-analyzer.rustc.source\": \"discover\",\n+ \"rust-analyzer.check.command\": \"check\",\n+ \"rust-analyzer.procMacro.enable\": true,\n+ \"rust-analyzer.cargo.extraEnv\": {\n+ \"RUSTUP_TOOLCHAIN\": \"1.88.0\"\n+ }\n+}\ndiff --git a/durable/examples/combined_example.rs b/durable/examples/combined_example.rs\nindex 626a6e1cf7e3c9c26f9f2edc58950d9bc31ec67e..6e9cb3210714d74c9a2cc0ed4f87e1b8d84788da 100644\n--- a/durable/examples/combined_example.rs\n+++ b/durable/examples/combined_example.rs\n@@ -1,5 +1,5 @@\n use durable::{Db, DurableMap, DurableVec};\n-use serde::{Serialize, Deserialize};\n+use serde::{Deserialize, Serialize};\n use std::time::{SystemTime, UNIX_EPOCH};\n \n #[derive(Debug, Clone, Serialize, Deserialize)]\n@@ -28,36 +28,48 @@ fn get_timestamp() -> u64 {\n fn main() -> Result<(), Box> {\n // Open or create a database\n let db = Db::open(\"chat_db\")?;\n- \n+\n // Create our collections\n let mut users = DurableMap::::new(&db, \"users\")?;\n let mut messages = DurableVec::::new(&db, \"messages\")?;\n let mut user_message_indices = DurableMap::>::new(&db, \"user_messages\")?;\n- \n+\n // Create some users\n- users.insert(\"alice\".to_string(), User {\n- username: \"alice\".to_string(),\n- display_name: \"Alice Smith\".to_string(),\n- message_count: 0,\n- })?;\n- \n- users.insert(\"bob\".to_string(), User {\n- username: \"bob\".to_string(),\n- display_name: \"Bob Johnson\".to_string(),\n- message_count: 0,\n- })?;\n- \n- users.insert(\"charlie\".to_string(), User {\n- username: \"charlie\".to_string(),\n- display_name: \"Charlie Brown\".to_string(),\n- message_count: 0,\n- })?;\n- \n+ users.insert(\n+ \"alice\".to_string(),\n+ User {\n+ username: \"alice\".to_string(),\n+ display_name: \"Alice Smith\".to_string(),\n+ message_count: 0,\n+ },\n+ )?;\n+\n+ users.insert(\n+ \"bob\".to_string(),\n+ User {\n+ username: \"bob\".to_string(),\n+ display_name: \"Bob Johnson\".to_string(),\n+ message_count: 0,\n+ },\n+ )?;\n+\n+ users.insert(\n+ \"charlie\".to_string(),\n+ User {\n+ username: \"charlie\".to_string(),\n+ display_name: \"Charlie Brown\".to_string(),\n+ message_count: 0,\n+ },\n+ )?;\n+\n // Helper to send a message\n- let send_message = |from: &str, to: &str, content: &str, \n+ let send_message = |from: &str,\n+ to: &str,\n+ content: &str,\n messages: &mut DurableVec,\n users: &mut DurableMap,\n- indices: &mut DurableMap>| -> Result<(), Box> {\n+ indices: &mut DurableMap>|\n+ -> Result<(), Box> {\n // Create message\n let msg_id = messages.len()? as u64;\n let message = Message {\n@@ -67,61 +79,93 @@ fn main() -> Result<(), Box> {\n content: content.to_string(),\n timestamp: get_timestamp(),\n };\n- \n+\n // Store message\n messages.push(message)?;\n let msg_index = messages.len()? - 1;\n- \n+\n // Update sender's message count\n if let Some(mut sender) = users.get(&from.to_string())? {\n sender.message_count += 1;\n users.insert(from.to_string(), sender)?;\n }\n- \n+\n // Track message indices for recipient\n let mut recipient_indices = indices.get(&to.to_string())?.unwrap_or_default();\n recipient_indices.push(msg_index);\n indices.insert(to.to_string(), recipient_indices)?;\n- \n+\n Ok(())\n };\n- \n+\n // Send some messages\n println!(\"💬 Chat Application Demo\\n\");\n println!(\"Sending messages...\");\n- \n- send_message(\"alice\", \"bob\", \"Hey Bob, how's the Durable library coming along?\", \n- &mut messages, &mut users, &mut user_message_indices)?;\n- \n- send_message(\"bob\", \"alice\", \"It's going great! We have DurableVec and DurableMap working!\", \n- &mut messages, &mut users, &mut user_message_indices)?;\n- \n- send_message(\"charlie\", \"alice\", \"That sounds awesome! Can I help with testing?\", \n- &mut messages, &mut users, &mut user_message_indices)?;\n- \n- send_message(\"alice\", \"charlie\", \"Absolutely! The more testing the better!\", \n- &mut messages, &mut users, &mut user_message_indices)?;\n- \n- send_message(\"bob\", \"charlie\", \"Check out the examples directory for usage patterns\", \n- &mut messages, &mut users, &mut user_message_indices)?;\n- \n+\n+ send_message(\n+ \"alice\",\n+ \"bob\",\n+ \"Hey Bob, how's the Durable library coming along?\",\n+ &mut messages,\n+ &mut users,\n+ &mut user_message_indices,\n+ )?;\n+\n+ send_message(\n+ \"bob\",\n+ \"alice\",\n+ \"It's going great! We have DurableVec and DurableMap working!\",\n+ &mut messages,\n+ &mut users,\n+ &mut user_message_indices,\n+ )?;\n+\n+ send_message(\n+ \"charlie\",\n+ \"alice\",\n+ \"That sounds awesome! Can I help with testing?\",\n+ &mut messages,\n+ &mut users,\n+ &mut user_message_indices,\n+ )?;\n+\n+ send_message(\n+ \"alice\",\n+ \"charlie\",\n+ \"Absolutely! The more testing the better!\",\n+ &mut messages,\n+ &mut users,\n+ &mut user_message_indices,\n+ )?;\n+\n+ send_message(\n+ \"bob\",\n+ \"charlie\",\n+ \"Check out the examples directory for usage patterns\",\n+ &mut messages,\n+ &mut users,\n+ &mut user_message_indices,\n+ )?;\n+\n // Display all users and their message counts\n println!(\"\\n👥 Users:\");\n let mut all_users = users.to_vec()?;\n all_users.sort_by_key(|(username, _)| username.clone());\n- \n+\n for (username, user) in all_users {\n- println!(\" {} ({}) - {} messages sent\", \n- user.display_name, username, user.message_count);\n+ println!(\n+ \" {} ({}) - {} messages sent\",\n+ user.display_name, username, user.message_count\n+ );\n }\n- \n+\n // Display all messages\n println!(\"\\n📨 All messages:\");\n for (i, msg) in messages.iter()?.enumerate() {\n let msg = msg?;\n println!(\" [{}] {} → {}: {}\", i, msg.from, msg.to, msg.content);\n }\n- \n+\n // Show inbox for each user\n println!(\"\\n📥 User inboxes:\");\n for item in users.iter() {\n@@ -135,26 +179,26 @@ fn main() -> Result<(), Box> {\n }\n }\n }\n- \n+\n // Statistics\n println!(\"\\n📊 Statistics:\");\n println!(\" Total users: {}\", users.len()?);\n println!(\" Total messages: {}\", messages.len()?);\n- \n+\n // Demonstrate persistence\n println!(\"\\n💾 Data has been persisted to disk!\");\n println!(\" Database location: ./chat_db\");\n- \n+\n // Clean up\n drop(messages);\n drop(users);\n drop(user_message_indices);\n drop(db);\n- \n+\n // Remove the database for this example\n std::fs::remove_dir_all(\"chat_db\").ok();\n- \n+\n println!(\"\\n✅ Example completed!\");\n- \n+\n Ok(())\n-} \n\\ No newline at end of file\n+}\ndiff --git a/durable/examples/map_example.rs b/durable/examples/map_example.rs\nindex 08b8f2c8826caf53c4c20b422a540c92f6624029..1d9c4a14b0f4cfe39ade84ebb1f1a14033e8ff1b 100644\n--- a/durable/examples/map_example.rs\n+++ b/durable/examples/map_example.rs\n@@ -1,5 +1,5 @@\n use durable::{Db, DurableMap};\n-use serde::{Serialize, Deserialize};\n+use serde::{Deserialize, Serialize};\n \n #[derive(Debug, Clone, Serialize, Deserialize)]\n struct UserProfile {\n@@ -11,10 +11,10 @@ struct UserProfile {\n fn main() -> Result<(), Box> {\n // Open or create a database\n let db = Db::open(\"example_db\")?;\n- \n+\n // Create a persistent map of user profiles\n let mut users = DurableMap::::new(&db, \"users\")?;\n- \n+\n // Insert some users\n // Using put() when we don't need the old value - more efficient!\n users.put(\n@@ -25,7 +25,7 @@ fn main() -> Result<(), Box> {\n score: 1500,\n },\n )?;\n- \n+\n users.put(\n \"bob\".to_string(),\n UserProfile {\n@@ -34,7 +34,7 @@ fn main() -> Result<(), Box> {\n score: 1200,\n },\n )?;\n- \n+\n // Using insert() when we might need the old value\n let old_charlie = users.insert(\n \"charlie\".to_string(),\n@@ -44,21 +44,24 @@ fn main() -> Result<(), Box> {\n score: 1800,\n },\n )?;\n- \n+\n if old_charlie.is_some() {\n println!(\"Replaced existing charlie entry\");\n }\n- \n+\n println!(\"Total users: {}\", users.len()?);\n- \n+\n // Look up a specific user\n if let Some(alice) = users.get(&\"alice\".to_string())? {\n println!(\"\\nAlice's profile: {:?}\", alice);\n }\n- \n+\n // Check if a user exists\n- println!(\"\\nDoes 'david' exist? {}\", users.contains_key(&\"david\".to_string())?);\n- \n+ println!(\n+ \"\\nDoes 'david' exist? {}\",\n+ users.contains_key(&\"david\".to_string())?\n+ );\n+\n // Update a user's score\n if let Some(mut bob) = users.get(&\"bob\".to_string())? {\n bob.score += 100;\n@@ -66,34 +69,40 @@ fn main() -> Result<(), Box> {\n users.put(\"bob\".to_string(), bob)?;\n println!(\"Updated Bob's score!\");\n }\n- \n+\n // Iterate over all users\n println!(\"\\nAll users (sorted by username):\");\n let mut all_users = users.to_vec()?;\n all_users.sort_by_key(|(username, _)| username.clone());\n- \n+\n for (username, profile) in all_users {\n- println!(\" {} ({}) - Score: {}\", username, profile.email, profile.score);\n+ println!(\n+ \" {} ({}) - Score: {}\",\n+ username, profile.email, profile.score\n+ );\n }\n- \n+\n // Get just the usernames\n let mut usernames = users.keys_vec()?;\n usernames.sort();\n println!(\"\\nAll usernames: {:?}\", usernames);\n- \n+\n // Find the highest scoring user\n let profiles = users.values_vec()?;\n if let Some(top_user) = profiles.iter().max_by_key(|p| p.score) {\n- println!(\"\\nTop scorer: {} with {} points\", top_user.name, top_user.score);\n+ println!(\n+ \"\\nTop scorer: {} with {} points\",\n+ top_user.name, top_user.score\n+ );\n }\n- \n+\n // Remove a user\n if let Some(removed) = users.remove(&\"charlie\".to_string())? {\n println!(\"\\nRemoved user: {}\", removed.name);\n println!(\"Users remaining: {}\", users.len()?);\n }\n- \n+\n println!(\"\\nData has been persisted to disk.\");\n- \n+\n Ok(())\n-} \n\\ No newline at end of file\n+}\ndiff --git a/durable/examples/nested_example.rs b/durable/examples/nested_example.rs\nindex 3b880f2c8b8ad2633d4b5fcf2016652216c9de8e..f16090cf6fb854ac7a1b00acfd31fbd12c25dbce 100644\n--- a/durable/examples/nested_example.rs\n+++ b/durable/examples/nested_example.rs\n@@ -3,27 +3,31 @@ use durable::{Db, DurableMap, DurableVec};\n fn main() -> Result<(), Box> {\n // Open a database\n let db = Db::open(\"nested_example_db\")?;\n- \n+\n // Create a map where each user has a list of posts\n- let user_posts: DurableMap> = DurableMap::new_nested(&db, \"user_posts\");\n- \n+ let user_posts: DurableMap> =\n+ DurableMap::new_nested(&db, \"user_posts\");\n+\n // Add posts for Alice\n println!(\"Adding posts for Alice...\");\n let mut alice_posts = user_posts.entry(\"alice\".to_string())?.or_default()?;\n alice_posts.push(\"Hello, world!\".to_string())?;\n alice_posts.push(\"Rust is awesome!\".to_string())?;\n alice_posts.push(\"Loving persistent data structures!\".to_string())?;\n- \n+\n // Add posts for Bob\n println!(\"Adding posts for Bob...\");\n let mut bob_posts = user_posts.entry(\"bob\".to_string())?.or_default()?;\n bob_posts.push(\"First post\".to_string())?;\n bob_posts.push(\"Learning Rust\".to_string())?;\n- \n+\n // Add a post for Charlie in a chained call\n println!(\"Adding post for Charlie...\");\n- user_posts.entry(\"charlie\".to_string())?.or_default()?.push(\"One-liner post!\".to_string())?;\n- \n+ user_posts\n+ .entry(\"charlie\".to_string())?\n+ .or_default()?\n+ .push(\"One-liner post!\".to_string())?;\n+\n // Read back Alice's posts\n println!(\"\\nAlice's posts:\");\n let alice_posts_read = user_posts.entry(\"alice\".to_string())?.or_default()?;\n@@ -32,7 +36,7 @@ fn main() -> Result<(), Box> {\n println!(\" {}: {}\", i + 1, post);\n }\n }\n- \n+\n // Read back Bob's posts\n println!(\"\\nBob's posts:\");\n let bob_posts_read = user_posts.entry(\"bob\".to_string())?.or_default()?;\n@@ -41,7 +45,7 @@ fn main() -> Result<(), Box> {\n println!(\" {}: {}\", i + 1, post);\n }\n }\n- \n+\n // Read back Charlie's posts\n println!(\"\\nCharlie's posts:\");\n let charlie_posts_read = user_posts.entry(\"charlie\".to_string())?.or_default()?;\n@@ -50,15 +54,15 @@ fn main() -> Result<(), Box> {\n println!(\" {}: {}\", i + 1, post);\n }\n }\n- \n+\n println!(\"\\nDemonstration of persistence...\");\n println!(\"Data is now persisted to disk. You can stop and restart this program,\");\n println!(\"and all the posts will still be there!\");\n- \n+\n println!(\"\\nTotal users with posts: 3\");\n println!(\"Alice has {} posts\", alice_posts_read.len()?);\n println!(\"Bob has {} posts\", bob_posts_read.len()?);\n println!(\"Charlie has {} posts\", charlie_posts_read.len()?);\n- \n+\n Ok(())\n-}\n\\ No newline at end of file\n+}\ndiff --git a/durable/examples/ranking_history.rs b/durable/examples/ranking_history.rs\nindex 4da8592d240318318ae20c834d616210edb16c8d..7624f1780de18b04b239b8b05789aafdd07310cc 100644\n--- a/durable/examples/ranking_history.rs\n+++ b/durable/examples/ranking_history.rs\n@@ -1,5 +1,5 @@\n use durable::{Db, DurableMap, DurableVec};\n-use serde::{Serialize, Deserialize};\n+use serde::{Deserialize, Serialize};\n \n #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, PartialOrd)]\n struct RankingEntry {\n@@ -24,9 +24,9 @@ impl RankingEntry {\n fn main() -> Result<(), Box> {\n println!(\"🎮 Gaming Ranking History System (Rewritten with Nested Entry API)\");\n println!(\"================================================================\");\n- \n+\n let db = Db::open(\"ranking_history_db\")?;\n- \n+\n // THE CORE CHANGE: Define the truly nested data structure.\n // Instead of a composite key, we nest a Map within a Map.\n // This represents the ideal, ergonomic API.\n@@ -35,16 +35,16 @@ fn main() -> Result<(), Box> {\n type Rankings = DurableMap;\n \n let rankings: Rankings = DurableMap::new_nested(&db, \"game_rankings_v2\");\n- \n+\n // Simulate some game days\n let today = 20241215u32;\n let yesterday = 20241214u32;\n let last_week = 20241208u32;\n- \n+\n // No more `make_key` helper function!\n- \n+\n println!(\"\\n📊 Adding ranking data using chained entry().or_default()...\");\n- \n+\n // Add rankings for CS2 today. This demonstrates the new, clean access pattern.\n println!(\"Adding CS2 rankings for today ({})\", today);\n let mut cs2_today = rankings\n@@ -57,7 +57,7 @@ fn main() -> Result<(), Box> {\n cs2_today.push(RankingEntry::new(\"player2\", 2380))?;\n cs2_today.push(RankingEntry::new(\"player3\", 2320))?;\n cs2_today.push(RankingEntry::new(\"player4\", 2280))?;\n- \n+\n // Add rankings for CS2 yesterday\n println!(\"Adding CS2 rankings for yesterday ({})\", yesterday);\n rankings\n@@ -90,7 +90,7 @@ fn main() -> Result<(), Box> {\n valorant_today.push(RankingEntry::new(\"player6\", 1850))?;\n valorant_today.push(RankingEntry::new(\"player7\", 1820))?;\n valorant_today.push(RankingEntry::new(\"player1\", 1800))?; // Same player, different game\n- \n+\n // Add TF2 rankings (matching the docs example)\n println!(\"Adding TF2 rankings for last week ({})\", last_week);\n rankings\n@@ -105,9 +105,9 @@ fn main() -> Result<(), Box> {\n .entry(last_week)?\n .or_default()?\n .push(RankingEntry::new(\"old_school_gamer\", 3150))?;\n- \n+\n println!(\"\\n🏆 Reading back ranking data with the same natural API...\");\n- \n+\n // Get today's CS2 leaderboard\n println!(\"\\n🎯 CS2 Leaderboard for {} (today):\", today);\n let mut today_rankings = rankings\n@@ -119,41 +119,53 @@ fn main() -> Result<(), Box> {\n \n // Sort by score descending\n today_rankings.sort_by(|a, b| b.score.cmp(&a.score));\n- \n+\n for (rank, entry) in today_rankings.iter().enumerate() {\n- println!(\" {}. {} - {} points\", rank + 1, entry.player_id, entry.score);\n+ println!(\n+ \" {}. {} - {} points\",\n+ rank + 1,\n+ entry.player_id,\n+ entry.score\n+ );\n }\n- \n+\n // Show cross-game analysis is still easy\n println!(\"\\n🎮 Multi-game player analysis for player1 on {}:\", today);\n- let cs2_player1_score = today_rankings.iter()\n+ let cs2_player1_score = today_rankings\n+ .iter()\n .find(|e| e.player_id == \"player1\")\n .map(|e| e.score);\n \n- let valorant_player1_score = valorant_today.to_vec()?.iter()\n+ let valorant_player1_score = valorant_today\n+ .to_vec()?\n+ .iter()\n .find(|e| e.player_id == \"player1\")\n .map(|e| e.score);\n \n- if let Some(score) = cs2_player1_score { println!(\" CS2 Score: {}\", score); }\n- if let Some(score) = valorant_player1_score { println!(\" Valorant Score: {}\", score); }\n- \n+ if let Some(score) = cs2_player1_score {\n+ println!(\" CS2 Score: {}\", score);\n+ }\n+ if let Some(score) = valorant_player1_score {\n+ println!(\" Valorant Score: {}\", score);\n+ }\n+\n // Showcase the power of the nested structure for stats\n // For nested collections, we use the keys API instead of iter()\n println!(\"\\n📊 Dynamic Database Statistics (discovered games):\");\n- \n+\n // Note: For nested collections, we iterate over known keys or use a different approach\n // since the values (nested DurableMaps) cannot be directly deserialized\n let games = vec![\"cs2\", \"valorant\", \"tf2\"]; // In a real app, you might track these separately\n- \n+\n for game in games {\n let game_history = rankings.entry(game.to_string())?.or_default()?;\n let active_days = game_history.len()?;\n- \n+\n if active_days > 0 {\n // For demonstration, let's count entries from known days\n let mut total_entries = 0;\n let days = [today, yesterday, last_week];\n- \n+\n for day in days {\n if let Ok(daily_rankings) = game_history.entry(day) {\n if let Ok(rankings_vec) = daily_rankings.or_default() {\n@@ -161,14 +173,18 @@ fn main() -> Result<(), Box> {\n }\n }\n }\n- \n+\n if total_entries > 0 {\n- println!(\" • {}: {} total entries across {} active day(s)\", \n- game.to_uppercase(), total_entries, active_days);\n+ println!(\n+ \" • {}: {} total entries across {} active day(s)\",\n+ game.to_uppercase(),\n+ total_entries,\n+ active_days\n+ );\n }\n }\n }\n- \n+\n println!(\"\\n✨ Key Benefits of This Rewritten Approach:\");\n println!(\" • No more manual key construction (`format!`) - the core goal is met!\");\n println!(\" • The code's structure now mirrors the mental model: `rankings[game][day]`\");\n@@ -176,4 +192,4 @@ fn main() -> Result<(), Box> {\n println!(\" • Demonstrates the full power of the `DurableCollection` and `entry()` design.\");\n \n Ok(())\n-}\n\\ No newline at end of file\n+}\ndiff --git a/durable/examples/simple_ranking.rs b/durable/examples/simple_ranking.rs\nindex 3cc873c41f3d93c7fa9e6e2dfc80e815f456b1f9..3c35c62674d7199f9b9864192fbbacb6ac0621a1 100644\n--- a/durable/examples/simple_ranking.rs\n+++ b/durable/examples/simple_ranking.rs\n@@ -4,65 +4,68 @@ fn main() -> Result<(), Box> {\n println!(\"🏆 Simple Game Ranking Example\");\n println!(\"Demonstrating the pattern from docs/motivation.md\");\n println!(\"===============================================\");\n- \n+\n let db = Db::open(\"simple_ranking_db\")?;\n- \n+\n // This is the exact pattern from the docs: Game Mode → List of (Player, Score)\n // For simplicity, we're showing one day's data per game mode\n- let rankings: DurableMap> = DurableMap::new_nested(&db, \"rankings\");\n- \n+ let rankings: DurableMap> =\n+ DurableMap::new_nested(&db, \"rankings\");\n+\n println!(\"\\n📊 Adding TF2 rankings (from the docs example)...\");\n- \n+\n // This is the exact code pattern shown in docs/motivation.md\n let mut tf2_rankings = rankings.entry(\"tf2\".to_string())?.or_default()?;\n tf2_rankings.push((\"player1\".to_string(), 1500))?;\n tf2_rankings.push((\"player2\".to_string(), 1400))?;\n tf2_rankings.push((\"player3\".to_string(), 1300))?;\n- \n+\n println!(\"✅ Added TF2 rankings using the docs pattern!\");\n- \n+\n // Add some other games for comparison\n println!(\"\\n📊 Adding CS2 rankings...\");\n let mut cs2_rankings = rankings.entry(\"cs2\".to_string())?.or_default()?;\n cs2_rankings.push((\"pro_player\".to_string(), 2500))?;\n cs2_rankings.push((\"skilled_gamer\".to_string(), 2200))?;\n- \n+\n println!(\"✅ Added CS2 rankings!\");\n- \n+\n // Now read back the data\n println!(\"\\n🏆 Current TF2 Leaderboard:\");\n let tf2_data = rankings.entry(\"tf2\".to_string())?.or_default()?;\n- \n+\n // Convert to vec and sort for display\n let mut tf2_leaderboard = tf2_data.to_vec()?;\n tf2_leaderboard.sort_by(|a, b| b.1.cmp(&a.1)); // Sort by score descending\n- \n+\n for (rank, (player, score)) in tf2_leaderboard.iter().enumerate() {\n println!(\" {}. {} - {} points\", rank + 1, player, score);\n }\n- \n+\n println!(\"\\n🏆 Current CS2 Leaderboard:\");\n let cs2_data = rankings.entry(\"cs2\".to_string())?.or_default()?;\n- \n+\n let mut cs2_leaderboard = cs2_data.to_vec()?;\n cs2_leaderboard.sort_by(|a, b| b.1.cmp(&a.1));\n- \n+\n for (rank, (player, score)) in cs2_leaderboard.iter().enumerate() {\n println!(\" {}. {} - {} points\", rank + 1, player, score);\n }\n- \n+\n println!(\"\\n📈 Database Statistics:\");\n println!(\" TF2 has {} players\", tf2_data.len()?);\n println!(\" CS2 has {} players\", cs2_data.len()?);\n- \n+\n println!(\"\\n✨ This demonstrates the exact pattern from docs/motivation.md:\");\n println!(\" rankings.entry(game_mode)?.or_default()?.push((player, score))?;\");\n println!(\" \");\n println!(\" Compare this to the manual key construction required with raw KV stores:\");\n- println!(\" let key = format!(\\\"leaderboard:{{}}:{{}}:player:{{}}\\\", game_mode, day, player_id);\");\n+ println!(\n+ \" let key = format!(\\\"leaderboard:{{}}:{{}}:player:{{}}\\\", game_mode, day, player_id);\"\n+ );\n println!(\" db.insert(key.as_bytes(), score.to_le_bytes())?;\");\n println!(\" \");\n println!(\" Durable provides the ergonomic, type-safe abstraction over RocksDB!\");\n- \n+\n Ok(())\n-}\n\\ No newline at end of file\n+}\ndiff --git a/durable/examples/streaming_demo.rs b/durable/examples/streaming_demo.rs\nindex 3a74a5318675f94f89aaf01562a5b28f8a1b664a..437a37e3f820a45ad738df68ef1928d20846a028 100644\n--- a/durable/examples/streaming_demo.rs\n+++ b/durable/examples/streaming_demo.rs\n@@ -2,44 +2,44 @@ use durable::{Db, DurableMap, DurableVec};\n \n fn main() -> Result<(), Box> {\n let db = Db::open(\"streaming_demo_db\")?;\n- \n+\n // Create collections with a moderate amount of data\n let mut map = DurableMap::::new(&db, \"large_map\")?;\n let mut vec = DurableVec::::new(&db, \"large_vec\")?;\n- \n+\n println!(\"🚀 Streaming Iterator Demo\\n\");\n- \n+\n // Add 1000 entries to demonstrate streaming\n println!(\"Adding 1000 entries to map and vec...\");\n for i in 0..1000 {\n map.insert(i, format!(\"Value {}\", i))?;\n vec.push(format!(\"Item {}\", i))?;\n }\n- \n+\n println!(\"\\n📊 Collection sizes:\");\n println!(\" Map entries: {}\", map.len()?);\n println!(\" Vec elements: {}\", vec.len()?);\n- \n+\n // Demonstrate streaming iteration - memory efficient\n println!(\"\\n✨ Streaming iteration (memory efficient):\");\n- \n+\n // Count items without loading into memory\n let map_count = map.iter().count();\n- println!(\" Counted {} map entries without loading into memory\", map_count);\n- \n+ println!(\n+ \" Counted {} map entries without loading into memory\",\n+ map_count\n+ );\n+\n // Find specific items efficiently\n let target = 500;\n- let found = map.iter()\n- .find(|item| {\n- item.as_ref()\n- .map(|(k, _)| *k == target)\n- .unwrap_or(false)\n- });\n- \n+ let found = map\n+ .iter()\n+ .find(|item| item.as_ref().map(|(k, _)| *k == target).unwrap_or(false));\n+\n if let Some(Ok((k, v))) = found {\n println!(\" Found key {} with value '{}' via streaming\", k, v);\n }\n- \n+\n // Process only what we need\n println!(\"\\n🎯 Processing first 10 items only:\");\n for (i, item) in vec.iter()?.take(10).enumerate() {\n@@ -48,34 +48,31 @@ fn main() -> Result<(), Box> {\n Err(e) => println!(\" [{}] Error: {:?}\", i, e),\n }\n }\n- \n+\n // Filter and process without loading all data\n println!(\"\\n🔍 Filtering even keys without loading all data:\");\n- let even_count = map.keys()\n- .filter(|item| {\n- item.as_ref()\n- .map(|k| k % 2 == 0)\n- .unwrap_or(false)\n- })\n+ let even_count = map\n+ .keys()\n+ .filter(|item| item.as_ref().map(|k| k % 2 == 0).unwrap_or(false))\n .count();\n println!(\" Found {} even keys\", even_count);\n- \n+\n // Compare with loading everything into memory\n println!(\"\\n⚠️ Loading all data into memory (less efficient for large collections):\");\n let all_values = map.values_vec()?;\n println!(\" Loaded {} values into a Vec\", all_values.len());\n- \n+\n println!(\"\\n✅ Streaming iterators provide:\");\n println!(\" • Constant memory usage regardless of collection size\");\n println!(\" • Ability to process data larger than RAM\");\n println!(\" • Early termination when finding specific items\");\n println!(\" • Efficient filtering and transformation\");\n- \n+\n // Clean up\n drop(map);\n drop(vec);\n drop(db);\n std::fs::remove_dir_all(\"streaming_demo_db\").ok();\n- \n+\n Ok(())\n-} \n\\ No newline at end of file\n+}\ndiff --git a/durable/examples/vec_example.rs b/durable/examples/vec_example.rs\nindex 07d394b5f3964ea1942a645c2f008e9e3c37d6d8..7576bdb0c14fc57a5cc52a9a93fd9bf59ffa03af 100644\n--- a/durable/examples/vec_example.rs\n+++ b/durable/examples/vec_example.rs\n@@ -1,5 +1,5 @@\n use durable::{Db, DurableVec};\n-use serde::{Serialize, Deserialize};\n+use serde::{Deserialize, Serialize};\n \n #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]\n struct Task {\n@@ -11,56 +11,57 @@ struct Task {\n fn main() -> Result<(), Box> {\n // Open or create a database\n let db = Db::open(\"example_db\")?;\n- \n+\n // Create a persistent vector of tasks\n let mut tasks = DurableVec::::new(&db, \"tasks\")?;\n- \n+\n // Add some tasks\n tasks.push(Task {\n id: 1,\n title: \"Build Durable library\".to_string(),\n completed: true,\n })?;\n- \n+\n tasks.push(Task {\n id: 2,\n title: \"Write comprehensive tests\".to_string(),\n completed: true,\n })?;\n- \n+\n tasks.push(Task {\n id: 3,\n title: \"Create documentation\".to_string(),\n completed: false,\n })?;\n- \n+\n println!(\"Total tasks: {}\", tasks.len()?);\n- \n+\n // Iterate through all tasks\n println!(\"\\nAll tasks:\");\n for (i, task) in tasks.iter()?.enumerate() {\n let task = task?;\n- println!(\" [{}] {} - {}\", \n- i, \n- task.title, \n+ println!(\n+ \" [{}] {} - {}\",\n+ i,\n+ task.title,\n if task.completed { \"✓\" } else { \"○\" }\n );\n }\n- \n+\n // Get a specific task\n if let Some(task) = tasks.get(1)? {\n println!(\"\\nTask at index 1: {:?}\", task);\n }\n- \n+\n // Mark the last task as completed\n if let Some(mut last_task) = tasks.pop()? {\n println!(\"\\nCompleting task: {}\", last_task.title);\n last_task.completed = true;\n tasks.push(last_task)?;\n }\n- \n+\n // The data persists even after the program exits!\n println!(\"\\nData has been persisted to disk.\");\n- \n+\n Ok(())\n-} \n\\ No newline at end of file\n+}\ndiff --git a/durable/src/lib.rs b/durable/src/lib.rs\nindex 8c46d49e27750832f19368e8f4e4a6f58ef287bc..7bfdbf20287f8e48322a99e9f935c7f4b63c4639 100644\n--- a/durable/src/lib.rs\n+++ b/durable/src/lib.rs\n@@ -1,30 +1,30 @@\n //! Durable - RocksDB-backed persistent data structures for Rust\n \n+use rocksdb::{Options, WriteBatch, DB as RocksDB};\n use std::path::Path;\n use std::sync::Arc;\n-use rocksdb::{DB as RocksDB, Options, WriteBatch};\n use thiserror::Error;\n \n-pub mod vec;\n pub mod map;\n-pub use vec::DurableVec;\n+pub mod vec;\n pub use map::DurableMap;\n+pub use vec::DurableVec;\n \n /// Error types for Durable operations\n #[derive(Error, Debug)]\n pub enum DurableError {\n #[error(\"RocksDB error: {0}\")]\n RocksDB(#[from] rocksdb::Error),\n- \n+\n #[error(\"Serialization error: {0}\")]\n Serialization(#[from] bincode::Error),\n- \n+\n #[error(\"Key not found\")]\n KeyNotFound,\n- \n+\n #[error(\"Collection not found: {0}\")]\n CollectionNotFound(String),\n- \n+\n #[error(\"Data corruption: {0}\")]\n Corruption(String),\n }\n@@ -35,7 +35,7 @@ pub type Result = std::result::Result;\n pub trait DurableCollection {\n /// Creates a new instance of the collection from a database handle\n /// and a pre-determined, unique key prefix.\n- /// \n+ ///\n /// This is the key method that allows `DurableMap` to instantiate\n /// a nested collection handle.\n fn from_prefix(db: Db, prefix: Vec) -> Self;\n@@ -53,13 +53,13 @@ impl Db {\n let mut opts = Options::default();\n opts.create_if_missing(true);\n opts.create_missing_column_families(true);\n- \n+\n let db = RocksDB::open(&opts, path)?;\n- Ok(Db { \n+ Ok(Db {\n inner: Arc::new(db),\n })\n }\n- \n+\n /// Create a new write batch for atomic operations\n pub fn batch(&self) -> Batch {\n Batch {\n@@ -67,41 +67,44 @@ impl Db {\n inner: WriteBatch::default(),\n }\n }\n- \n+\n /// Get the underlying RocksDB handle (for advanced usage)\n pub(crate) fn rocks(&self) -> &RocksDB {\n &self.inner\n }\n- \n+\n /// Get a new unique collection ID for nested collections\n pub fn new_collection_id(&self) -> Result {\n let key = b\"__global_meta:next_collection_id\";\n- \n+\n // Get current value\n let current_bytes = self.rocks().get(key)?;\n let current_id = match current_bytes {\n Some(bytes) => {\n if bytes.len() != 8 {\n- return Err(DurableError::Corruption(\"Invalid collection ID bytes size\".into()));\n+ return Err(DurableError::Corruption(\n+ \"Invalid collection ID bytes size\".into(),\n+ ));\n }\n- let id_bytes: [u8; 8] = bytes[..8].try_into()\n+ let id_bytes: [u8; 8] = bytes[..8]\n+ .try_into()\n .map_err(|_| DurableError::Corruption(\"Invalid collection ID bytes\".into()))?;\n u64::from_le_bytes(id_bytes)\n }\n None => 0,\n };\n- \n+\n let next_id = current_id + 1;\n- \n+\n // Try to atomically update - use compare-and-swap semantics\n let mut batch = WriteBatch::default();\n batch.put(key, &next_id.to_le_bytes());\n- \n+\n // For now, just write it directly. In a real implementation,\n // we'd want proper compare-and-swap to handle concurrent access\n self.rocks().write(batch)?;\n self.rocks().flush_wal(true)?;\n- \n+\n Ok(current_id)\n }\n }\n@@ -129,5 +132,4 @@ impl Batch {\n self.db.rocks().flush_wal(true)?;\n Ok(())\n }\n- \n }\ndiff --git a/durable/src/map.rs b/durable/src/map.rs\nindex d3cc39bfede02f66532e4055299acdbffb8f6280..f7fdf224f52e791ec1204668ece9413035b4bfce 100644\n--- a/durable/src/map.rs\n+++ b/durable/src/map.rs\n@@ -1,6 +1,6 @@\n-use crate::{Batch, Db, Result, DurableError, DurableCollection};\n-use rocksdb::{IteratorMode, WriteBatch, Direction};\n-use serde::{Serialize, Deserialize};\n+use crate::{Batch, Db, DurableCollection, DurableError, Result};\n+use rocksdb::{Direction, IteratorMode, WriteBatch};\n+use serde::{Deserialize, Serialize};\n use std::marker::PhantomData;\n \n /// A persistent map backed by RocksDB\n@@ -10,78 +10,78 @@ pub struct DurableMap {\n _phantom: PhantomData<(K, V)>,\n }\n \n-impl DurableMap \n-where \n+impl DurableMap\n+where\n K: Serialize + for<'de> Deserialize<'de>,\n V: Serialize + for<'de> Deserialize<'de>,\n {\n /// Create a new DurableMap with the given name\n pub fn new(db: &Db, name: &str) -> Result {\n let prefix = format!(\"map:{}\", name).into_bytes();\n- \n+\n Ok(DurableMap {\n db: db.clone(),\n prefix,\n _phantom: PhantomData,\n })\n }\n- \n+\n /// Insert a key-value pair into the map\n pub fn insert(&mut self, key: K, value: V) -> Result> {\n let key_bytes = bincode::serialize(&key)?;\n let value_bytes = bincode::serialize(&value)?;\n- \n+\n // Get the old value if it exists\n let old_value = self.get(&key)?;\n- \n+\n let mut batch = WriteBatch::default();\n- \n+\n // Write the new value\n let db_key = self.entry_key(&key_bytes);\n batch.put(&db_key, &value_bytes);\n- \n+\n // Update length if this is a new key\n if old_value.is_none() {\n let new_len = self.len()? + 1;\n let len_key = self.meta_key(\"len\");\n batch.put(&len_key, &(new_len as u64).to_le_bytes());\n }\n- \n+\n // Commit atomically\n self.db.rocks().write(batch)?;\n self.db.rocks().flush_wal(true)?;\n- \n+\n Ok(old_value)\n }\n- \n+\n /// Put a key-value pair into the map without returning the old value\n- /// \n+ ///\n /// This is more efficient than `insert` when you don't need the old value,\n /// as it only checks for key existence without deserializing the value.\n pub fn put(&mut self, key: K, value: V) -> Result<()> {\n let key_bytes = bincode::serialize(&key)?;\n let value_bytes = bincode::serialize(&value)?;\n let db_key = self.entry_key(&key_bytes);\n- \n+\n let mut batch = WriteBatch::default();\n- \n+\n // Check if this is a new key (without deserializing the value)\n let is_new = self.db.rocks().get_pinned(&db_key)?.is_none();\n- \n+\n // Write the new value\n batch.put(&db_key, &value_bytes);\n- \n+\n // Update length if this is a new key\n if is_new {\n let new_len = self.len()? + 1;\n let len_key = self.meta_key(\"len\");\n batch.put(&len_key, &(new_len as u64).to_le_bytes());\n }\n- \n+\n // Commit atomically\n self.db.rocks().write(batch)?;\n self.db.rocks().flush_wal(true)?;\n- \n+\n Ok(())\n }\n \n@@ -105,12 +105,12 @@ where\n \n Ok(())\n }\n- \n+\n /// Get a value by key\n pub fn get(&self, key: &K) -> Result> {\n let key_bytes = bincode::serialize(key)?;\n let db_key = self.entry_key(&key_bytes);\n- \n+\n match self.db.rocks().get(&db_key)? {\n Some(bytes) => {\n let value = bincode::deserialize(&bytes)?;\n@@ -119,20 +119,20 @@ where\n None => Ok(None),\n }\n }\n- \n+\n /// Check if a key exists in the map\n pub fn contains_key(&self, key: &K) -> Result {\n let key_bytes = bincode::serialize(key)?;\n let db_key = self.entry_key(&key_bytes);\n- \n+\n Ok(self.db.rocks().get(&db_key)?.is_some())\n }\n- \n+\n /// Remove a key-value pair from the map\n pub fn remove(&mut self, key: &K) -> Result> {\n let key_bytes = bincode::serialize(key)?;\n let db_key = self.entry_key(&key_bytes);\n- \n+\n // Get the old value\n let old_value = match self.db.rocks().get(&db_key)? {\n Some(bytes) => {\n@@ -141,36 +141,37 @@ where\n }\n None => None,\n };\n- \n+\n // Delete the key if it existed and update length\n if old_value.is_some() {\n let mut batch = WriteBatch::default();\n- \n+\n // Delete the entry\n batch.delete(&db_key);\n- \n+\n // Update length\n let new_len = self.len()? - 1;\n let len_key = self.meta_key(\"len\");\n batch.put(&len_key, &(new_len as u64).to_le_bytes());\n- \n+\n // Commit atomically\n self.db.rocks().write(batch)?;\n self.db.rocks().flush_wal(true)?;\n }\n- \n+\n Ok(old_value)\n }\n- \n \n- \n /// Clear all entries from the map\n pub fn clear(&mut self) -> Result<()> {\n let prefix = self.entry_prefix();\n let mut batch = WriteBatch::default();\n- \n+\n // Collect all keys to delete\n- let iter = self.db.rocks().iterator(IteratorMode::From(&prefix, Direction::Forward));\n+ let iter = self\n+ .db\n+ .rocks()\n+ .iterator(IteratorMode::From(&prefix, Direction::Forward));\n for item in iter {\n let (key, _) = item?;\n if !key.starts_with(&prefix) {\n@@ -178,32 +179,35 @@ where\n }\n batch.delete(&key);\n }\n- \n+\n // Reset length to 0\n let len_key = self.meta_key(\"len\");\n batch.delete(&len_key);\n- \n+\n // Commit atomically\n self.db.rocks().write(batch)?;\n self.db.rocks().flush_wal(true)?;\n- \n+\n Ok(())\n }\n- \n+\n /// Iterate over all key-value pairs using a streaming iterator\n pub fn iter(&self) -> MapIterator<'_, K, V> {\n let prefix = self.entry_prefix();\n- let iter = self.db.rocks().iterator(IteratorMode::From(&prefix, Direction::Forward));\n- \n+ let iter = self\n+ .db\n+ .rocks()\n+ .iterator(IteratorMode::From(&prefix, Direction::Forward));\n+\n MapIterator {\n inner: iter,\n prefix,\n _phantom: PhantomData,\n }\n }\n- \n+\n /// Load all key-value pairs into a Vec\n- /// \n+ ///\n /// Note: This loads the entire collection into memory. For large collections,\n /// prefer using `iter()` which streams elements.\n pub fn to_vec(&self) -> Result> {\n@@ -213,21 +217,24 @@ where\n }\n Ok(result)\n }\n- \n+\n /// Iterate over all keys using a streaming iterator\n pub fn keys(&self) -> KeyIterator<'_, K, V> {\n let prefix = self.entry_prefix();\n- let iter = self.db.rocks().iterator(IteratorMode::From(&prefix, Direction::Forward));\n- \n+ let iter = self\n+ .db\n+ .rocks()\n+ .iterator(IteratorMode::From(&prefix, Direction::Forward));\n+\n KeyIterator {\n inner: iter,\n prefix,\n _phantom: PhantomData,\n }\n }\n- \n+\n /// Load all keys into a Vec\n- /// \n+ ///\n /// Note: This loads all keys into memory. For large collections,\n /// prefer using `keys()` which streams elements.\n pub fn keys_vec(&self) -> Result> {\n@@ -237,21 +244,24 @@ where\n }\n Ok(result)\n }\n- \n+\n /// Iterate over all values using a streaming iterator\n pub fn values(&self) -> ValueIterator<'_, K, V> {\n let prefix = self.entry_prefix();\n- let iter = self.db.rocks().iterator(IteratorMode::From(&prefix, Direction::Forward));\n- \n+ let iter = self\n+ .db\n+ .rocks()\n+ .iterator(IteratorMode::From(&prefix, Direction::Forward));\n+\n ValueIterator {\n inner: iter,\n prefix,\n _phantom: PhantomData,\n }\n }\n- \n+\n /// Load all values into a Vec\n- /// \n+ ///\n /// Note: This loads all values into memory. For large collections,\n /// prefer using `values()` which streams elements.\n pub fn values_vec(&self) -> Result> {\n@@ -261,54 +271,52 @@ where\n }\n Ok(result)\n }\n- \n+\n /// Insert multiple key-value pairs in a single batch\n pub fn extend(&mut self, iter: I) -> Result<()>\n where\n- I: IntoIterator\n+ I: IntoIterator,\n {\n let mut batch = WriteBatch::default();\n let current_len = self.len()?;\n let mut new_entries = 0;\n- \n+\n for (key, value) in iter {\n let key_bytes = bincode::serialize(&key)?;\n let value_bytes = bincode::serialize(&value)?;\n let db_key = self.entry_key(&key_bytes);\n- \n+\n // Check if this is a new key\n if !self.contains_key(&key)? {\n new_entries += 1;\n }\n- \n+\n batch.put(&db_key, &value_bytes);\n }\n- \n+\n // Update length if we added new entries\n if new_entries > 0 {\n let new_len = current_len + new_entries;\n let len_key = self.meta_key(\"len\");\n batch.put(&len_key, &(new_len as u64).to_le_bytes());\n }\n- \n+\n // Commit atomically\n self.db.rocks().write(batch)?;\n self.db.rocks().flush_wal(true)?;\n- \n+\n Ok(())\n }\n- \n \n- \n // Helper methods\n- \n+\n fn entry_key(&self, key_bytes: &[u8]) -> Vec {\n let mut db_key = self.prefix.clone();\n db_key.extend_from_slice(b\":entry:\");\n db_key.extend_from_slice(key_bytes);\n db_key\n }\n- \n+\n fn entry_prefix(&self) -> Vec {\n let mut prefix = self.prefix.clone();\n prefix.extend_from_slice(b\":entry:\");\n@@ -321,14 +329,14 @@ impl DurableMap {\n /// Create a new DurableMap for nested collections (no serialization constraints)\n pub fn new_nested(db: &Db, name: &str) -> Self {\n let prefix = format!(\"map:{}\", name).into_bytes();\n- \n+\n DurableMap {\n db: db.clone(),\n prefix,\n _phantom: PhantomData,\n }\n }\n- \n+\n /// Create a new DurableMap from a prefix (used for nested collections)\n pub fn from_prefix(db: Db, prefix: Vec) -> Self {\n Self {\n@@ -337,7 +345,7 @@ impl DurableMap {\n _phantom: PhantomData,\n }\n }\n- \n+\n /// Get the number of entries in the map (unconstrained version for nested collections)\n pub fn len(&self) -> Result {\n let key = self.meta_key(\"len\");\n@@ -346,14 +354,15 @@ impl DurableMap {\n if bytes.len() != 8 {\n return Err(DurableError::Corruption(\"Invalid length bytes size\".into()));\n }\n- let len_bytes: [u8; 8] = bytes[..8].try_into()\n+ let len_bytes: [u8; 8] = bytes[..8]\n+ .try_into()\n .map_err(|_| DurableError::Corruption(\"Invalid length bytes\".into()))?;\n Ok(u64::from_le_bytes(len_bytes) as usize)\n }\n None => Ok(0),\n }\n }\n- \n+\n /// Check if the map is empty (unconstrained version for nested collections)\n pub fn is_empty(&self) -> Result {\n Ok(self.len()? == 0)\n@@ -366,7 +375,7 @@ impl DurableMap {\n db_key.extend_from_slice(key_bytes);\n db_key\n }\n- \n+\n fn meta_key(&self, meta_type: &str) -> Vec {\n let mut key = self.prefix.clone();\n key.extend_from_slice(b\":__meta:\");\n@@ -411,7 +420,7 @@ pub struct OccupiedEntry<'a, K, V> {\n value_marker: Vec, // The bytes read from RocksDB, e.g., [0x02, ...]\n }\n \n-impl<'a, K, V> OccupiedEntry<'a, K, V> \n+impl<'a, K, V> OccupiedEntry<'a, K, V>\n where\n V: DurableCollection,\n {\n@@ -421,8 +430,9 @@ where\n if self.value_marker.len() != 9 || self.value_marker[0] != 0x02 {\n return Err(DurableError::Corruption(\"Invalid collection marker\".into()));\n }\n- \n- let col_id_bytes: [u8; 8] = self.value_marker[1..9].try_into()\n+\n+ let col_id_bytes: [u8; 8] = self.value_marker[1..9]\n+ .try_into()\n .map_err(|_| DurableError::Corruption(\"Invalid collection ID\".into()))?;\n let col_id = u64::from_le_bytes(col_id_bytes);\n \n@@ -433,7 +443,7 @@ where\n // 3. Create the collection handle using the trait method\n Ok(V::from_prefix(self.map.db.clone(), child_prefix))\n }\n- \n+\n /// Gets a handle to the existing nested collection (same as get but consumes self)\n pub fn or_default(self) -> Result {\n self.get()\n@@ -446,7 +456,7 @@ pub struct VacantEntry<'a, K, V> {\n key: K, // The original key from the user\n }\n \n-impl<'a, K, V> VacantEntry<'a, K, V> \n+impl<'a, K, V> VacantEntry<'a, K, V>\n where\n K: Serialize,\n V: DurableCollection,\n@@ -518,7 +528,7 @@ where\n V: for<'de> Deserialize<'de>,\n {\n type Item = Result<(K, V)>;\n- \n+\n fn next(&mut self) -> Option {\n match self.inner.next() {\n Some(Ok((db_key, value_bytes))) => {\n@@ -526,13 +536,16 @@ where\n if !db_key.starts_with(&self.prefix) {\n return None;\n }\n- \n+\n // Extract the key part (skip prefix)\n let key_start = self.prefix.len();\n let key_bytes = &db_key[key_start..];\n- \n+\n // Deserialize key and value\n- match (bincode::deserialize(key_bytes), bincode::deserialize(&value_bytes)) {\n+ match (\n+ bincode::deserialize(key_bytes),\n+ bincode::deserialize(&value_bytes),\n+ ) {\n (Ok(key), Ok(value)) => Some(Ok((key, value))),\n (Err(e), _) | (_, Err(e)) => Some(Err(e.into())),\n }\n@@ -555,7 +568,7 @@ where\n K: for<'de> Deserialize<'de>,\n {\n type Item = Result;\n- \n+\n fn next(&mut self) -> Option {\n match self.inner.next() {\n Some(Ok((db_key, _))) => {\n@@ -563,11 +576,11 @@ where\n if !db_key.starts_with(&self.prefix) {\n return None;\n }\n- \n+\n // Extract the key part (skip prefix)\n let key_start = self.prefix.len();\n let key_bytes = &db_key[key_start..];\n- \n+\n // Deserialize key\n match bincode::deserialize(key_bytes) {\n Ok(key) => Some(Ok(key)),\n@@ -592,7 +605,7 @@ where\n V: for<'de> Deserialize<'de>,\n {\n type Item = Result;\n- \n+\n fn next(&mut self) -> Option {\n match self.inner.next() {\n Some(Ok((db_key, value_bytes))) => {\n@@ -600,7 +613,7 @@ where\n if !db_key.starts_with(&self.prefix) {\n return None;\n }\n- \n+\n // Deserialize value\n match bincode::deserialize(&value_bytes) {\n Ok(value) => Some(Ok(value)),\n@@ -616,214 +629,232 @@ where\n #[cfg(test)]\n mod tests {\n use super::*;\n- use tempfile::TempDir;\n use std::collections::HashMap;\n- \n+ use tempfile::TempDir;\n+\n fn setup_test_db() -> (TempDir, Db) {\n let temp_dir = TempDir::new().unwrap();\n let db = Db::open(temp_dir.path()).unwrap();\n (temp_dir, db)\n }\n- \n+\n #[test]\n fn test_insert_and_get() {\n let (_temp, db) = setup_test_db();\n let mut map = DurableMap::::new(&db, \"test_map\").unwrap();\n- \n+\n // Insert some values\n assert_eq!(map.insert(\"one\".to_string(), 1).unwrap(), None);\n assert_eq!(map.insert(\"two\".to_string(), 2).unwrap(), None);\n assert_eq!(map.insert(\"three\".to_string(), 3).unwrap(), None);\n- \n+\n // Get values\n assert_eq!(map.get(&\"one\".to_string()).unwrap(), Some(1));\n assert_eq!(map.get(&\"two\".to_string()).unwrap(), Some(2));\n assert_eq!(map.get(&\"three\".to_string()).unwrap(), Some(3));\n assert_eq!(map.get(&\"four\".to_string()).unwrap(), None);\n- \n+\n // Update existing value\n assert_eq!(map.insert(\"two\".to_string(), 22).unwrap(), Some(2));\n assert_eq!(map.get(&\"two\".to_string()).unwrap(), Some(22));\n }\n- \n+\n #[test]\n fn test_remove() {\n let (_temp, db) = setup_test_db();\n let mut map = DurableMap::::new(&db, \"remove_map\").unwrap();\n- \n+\n // Insert and remove\n map.insert(\"key\".to_string(), \"value\".to_string()).unwrap();\n- assert_eq!(map.remove(&\"key\".to_string()).unwrap(), Some(\"value\".to_string()));\n+ assert_eq!(\n+ map.remove(&\"key\".to_string()).unwrap(),\n+ Some(\"value\".to_string())\n+ );\n assert_eq!(map.remove(&\"key\".to_string()).unwrap(), None);\n assert_eq!(map.get(&\"key\".to_string()).unwrap(), None);\n }\n- \n+\n #[test]\n fn test_contains_key() {\n let (_temp, db) = setup_test_db();\n let mut map = DurableMap::::new(&db, \"contains_map\").unwrap();\n- \n+\n map.insert(42, \"answer\".to_string()).unwrap();\n- \n+\n assert!(map.contains_key(&42).unwrap());\n assert!(!map.contains_key(&43).unwrap());\n }\n- \n+\n #[test]\n fn test_len_and_clear() {\n let (_temp, db) = setup_test_db();\n let mut map = DurableMap::::new(&db, \"len_map\").unwrap();\n- \n+\n // Empty map\n assert_eq!(map.len().unwrap(), 0);\n assert!(map.is_empty().unwrap());\n- \n+\n // Add items\n for i in 0..10 {\n map.insert(i, i * 2).unwrap();\n }\n assert_eq!(map.len().unwrap(), 10);\n assert!(!map.is_empty().unwrap());\n- \n+\n // Clear\n map.clear().unwrap();\n assert_eq!(map.len().unwrap(), 0);\n assert!(map.is_empty().unwrap());\n }\n- \n+\n #[test]\n fn test_persistence() {\n let (temp_dir, db) = setup_test_db();\n- \n+\n // Create and populate map\n {\n let mut map = DurableMap::>::new(&db, \"persist_map\").unwrap();\n- map.insert(\"binary\".to_string(), vec![1, 2, 3, 4, 5]).unwrap();\n+ map.insert(\"binary\".to_string(), vec![1, 2, 3, 4, 5])\n+ .unwrap();\n map.insert(\"data\".to_string(), vec![10, 20, 30]).unwrap();\n }\n- \n+\n // Drop the database\n drop(db);\n- \n+\n // Reopen and verify data persists\n {\n let db = Db::open(temp_dir.path()).unwrap();\n let map = DurableMap::>::new(&db, \"persist_map\").unwrap();\n- \n- assert_eq!(map.get(&\"binary\".to_string()).unwrap(), Some(vec![1, 2, 3, 4, 5]));\n- assert_eq!(map.get(&\"data\".to_string()).unwrap(), Some(vec![10, 20, 30]));\n+\n+ assert_eq!(\n+ map.get(&\"binary\".to_string()).unwrap(),\n+ Some(vec![1, 2, 3, 4, 5])\n+ );\n+ assert_eq!(\n+ map.get(&\"data\".to_string()).unwrap(),\n+ Some(vec![10, 20, 30])\n+ );\n assert_eq!(map.len().unwrap(), 2);\n }\n }\n- \n+\n #[test]\n fn test_iteration() {\n let (_temp, db) = setup_test_db();\n let mut map = DurableMap::::new(&db, \"iter_map\").unwrap();\n- \n+\n // Insert data\n let data = vec![\n (\"apple\".to_string(), 1),\n (\"banana\".to_string(), 2),\n (\"cherry\".to_string(), 3),\n ];\n- \n+\n for (k, v) in &data {\n map.insert(k.clone(), *v).unwrap();\n }\n- \n+\n // Test iter()\n let mut items = map.to_vec().unwrap();\n items.sort_by_key(|(k, _)| k.clone());\n assert_eq!(items, data);\n- \n+\n // Test keys()\n let mut keys = map.keys_vec().unwrap();\n keys.sort();\n assert_eq!(keys, vec![\"apple\", \"banana\", \"cherry\"]);\n- \n+\n // Test values()\n let mut values = map.values_vec().unwrap();\n values.sort();\n assert_eq!(values, vec![1, 2, 3]);\n }\n- \n+\n #[test]\n fn test_extend() {\n let (_temp, db) = setup_test_db();\n let mut map = DurableMap::::new(&db, \"extend_map\").unwrap();\n- \n+\n // Extend from iterator\n let data: HashMap = vec![\n (1, \"one\".to_string()),\n (2, \"two\".to_string()),\n (3, \"three\".to_string()),\n- ].into_iter().collect();\n- \n+ ]\n+ .into_iter()\n+ .collect();\n+\n map.extend(data.clone()).unwrap();\n- \n+\n // Verify all items were inserted\n for (k, v) in data {\n assert_eq!(map.get(&k).unwrap(), Some(v));\n }\n assert_eq!(map.len().unwrap(), 3);\n }\n- \n+\n #[test]\n fn test_complex_keys() {\n- use serde::{Serialize, Deserialize};\n- \n+ use serde::{Deserialize, Serialize};\n+\n #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]\n struct ComplexKey {\n id: u64,\n name: String,\n }\n- \n+\n let (_temp, db) = setup_test_db();\n let mut map = DurableMap::::new(&db, \"complex_map\").unwrap();\n- \n- let key1 = ComplexKey { id: 1, name: \"first\".to_string() };\n- let key2 = ComplexKey { id: 2, name: \"second\".to_string() };\n- \n+\n+ let key1 = ComplexKey {\n+ id: 1,\n+ name: \"first\".to_string(),\n+ };\n+ let key2 = ComplexKey {\n+ id: 2,\n+ name: \"second\".to_string(),\n+ };\n+\n map.insert(key1.clone(), \"value1\".to_string()).unwrap();\n map.insert(key2.clone(), \"value2\".to_string()).unwrap();\n- \n+\n assert_eq!(map.get(&key1).unwrap(), Some(\"value1\".to_string()));\n assert_eq!(map.get(&key2).unwrap(), Some(\"value2\".to_string()));\n }\n- \n+\n #[test]\n fn test_multiple_maps_same_db() {\n let (_temp, db) = setup_test_db();\n- \n+\n let mut map1 = DurableMap::::new(&db, \"map1\").unwrap();\n let mut map2 = DurableMap::::new(&db, \"map2\").unwrap();\n- \n+\n // Insert different data\n map1.insert(\"shared_key\".to_string(), 100).unwrap();\n map2.insert(\"shared_key\".to_string(), 200).unwrap();\n- \n+\n // Verify isolation\n assert_eq!(map1.get(&\"shared_key\".to_string()).unwrap(), Some(100));\n assert_eq!(map2.get(&\"shared_key\".to_string()).unwrap(), Some(200));\n }\n- \n+\n #[test]\n fn test_streaming_iterators() {\n let (_temp, db) = setup_test_db();\n let mut map = DurableMap::::new(&db, \"stream_map\").unwrap();\n- \n+\n // Insert test data\n let data = vec![\n (\"alice\".to_string(), 100),\n (\"bob\".to_string(), 200),\n (\"charlie\".to_string(), 300),\n ];\n- \n+\n for (k, v) in &data {\n map.insert(k.clone(), *v).unwrap();\n }\n- \n+\n // Test streaming iteration\n let mut collected = Vec::new();\n for item in map.iter() {\n@@ -832,7 +863,7 @@ mod tests {\n }\n collected.sort_by_key(|(k, _)| k.clone());\n assert_eq!(collected, data);\n- \n+\n // Test keys iterator\n let mut keys = Vec::new();\n for key in map.keys() {\n@@ -840,7 +871,7 @@ mod tests {\n }\n keys.sort();\n assert_eq!(keys, vec![\"alice\", \"bob\", \"charlie\"]);\n- \n+\n // Test values iterator\n let mut values = Vec::new();\n for value in map.values() {\n@@ -848,48 +879,51 @@ mod tests {\n }\n values.sort();\n assert_eq!(values, vec![100, 200, 300]);\n- \n+\n // Test that iterators properly handle prefix boundaries\n let mut map2 = DurableMap::::new(&db, \"stream_map2\").unwrap();\n map2.insert(\"dave\".to_string(), 400).unwrap();\n- \n+\n // Each iterator should only see its own data\n let collected1: Vec<_> = map.iter().map(Result::unwrap).collect();\n let collected2: Vec<_> = map2.iter().map(Result::unwrap).collect();\n- \n+\n assert_eq!(collected1.len(), 3);\n assert_eq!(collected2.len(), 1);\n assert_eq!(collected2[0], (\"dave\".to_string(), 400));\n }\n- \n+\n #[test]\n fn test_metadata_length_tracking() {\n let (_temp, db) = setup_test_db();\n let mut map = DurableMap::::new(&db, \"length_map\").unwrap();\n- \n+\n // Empty map\n assert_eq!(map.len().unwrap(), 0);\n assert!(map.is_empty().unwrap());\n- \n+\n // Insert operations should update length\n- map.insert(\"key1\".to_string(), \"value1\".to_string()).unwrap();\n+ map.insert(\"key1\".to_string(), \"value1\".to_string())\n+ .unwrap();\n assert_eq!(map.len().unwrap(), 1);\n- \n- map.insert(\"key2\".to_string(), \"value2\".to_string()).unwrap();\n+\n+ map.insert(\"key2\".to_string(), \"value2\".to_string())\n+ .unwrap();\n assert_eq!(map.len().unwrap(), 2);\n- \n+\n // Updating existing key should not change length\n- map.insert(\"key1\".to_string(), \"new_value1\".to_string()).unwrap();\n+ map.insert(\"key1\".to_string(), \"new_value1\".to_string())\n+ .unwrap();\n assert_eq!(map.len().unwrap(), 2);\n- \n+\n // Remove operations should update length\n map.remove(&\"key1\".to_string()).unwrap();\n assert_eq!(map.len().unwrap(), 1);\n- \n+\n // Removing non-existent key should not change length\n map.remove(&\"non_existent\".to_string()).unwrap();\n assert_eq!(map.len().unwrap(), 1);\n- \n+\n // Extend should update length correctly\n let data = vec![\n (\"key3\".to_string(), \"value3\".to_string()),\n@@ -898,7 +932,7 @@ mod tests {\n ];\n map.extend(data).unwrap();\n assert_eq!(map.len().unwrap(), 4); // key2 + 3 new keys\n- \n+\n // Extend with existing keys should only count new ones\n let mixed_data = vec![\n (\"key2\".to_string(), \"updated_value2\".to_string()), // existing\n@@ -906,39 +940,39 @@ mod tests {\n ];\n map.extend(mixed_data).unwrap();\n assert_eq!(map.len().unwrap(), 5); // only key6 was new\n- \n+\n // Clear should reset length to 0\n map.clear().unwrap();\n assert_eq!(map.len().unwrap(), 0);\n assert!(map.is_empty().unwrap());\n }\n- \n+\n #[test]\n fn test_put_method() {\n let (_temp, db) = setup_test_db();\n let mut map = DurableMap::::new(&db, \"put_map\").unwrap();\n- \n+\n // Put new entries\n map.put(\"a\".to_string(), 1).unwrap();\n map.put(\"b\".to_string(), 2).unwrap();\n map.put(\"c\".to_string(), 3).unwrap();\n- \n+\n // Verify entries exist and length is correct\n assert_eq!(map.get(&\"a\".to_string()).unwrap(), Some(1));\n assert_eq!(map.get(&\"b\".to_string()).unwrap(), Some(2));\n assert_eq!(map.get(&\"c\".to_string()).unwrap(), Some(3));\n assert_eq!(map.len().unwrap(), 3);\n- \n+\n // Update existing entry with put\n map.put(\"b\".to_string(), 20).unwrap();\n assert_eq!(map.get(&\"b\".to_string()).unwrap(), Some(20));\n assert_eq!(map.len().unwrap(), 3); // Length should not change\n- \n+\n // Compare put vs insert performance characteristics\n // put() doesn't return old value but is more efficient\n map.put(\"d\".to_string(), 4).unwrap();\n assert_eq!(map.len().unwrap(), 4);\n- \n+\n // insert() returns old value\n let old = map.insert(\"d\".to_string(), 40).unwrap();\n assert_eq!(old, Some(4));\n@@ -950,145 +984,186 @@ mod tests {\n mod proptests {\n use super::*;\n use proptest::prelude::*;\n- use tempfile::TempDir;\n use std::collections::HashMap;\n- \n+ use tempfile::TempDir;\n+\n fn setup_test_db() -> (TempDir, Db) {\n let temp_dir = TempDir::new().unwrap();\n let db = Db::open(temp_dir.path()).unwrap();\n (temp_dir, db)\n }\n- \n+\n proptest! {\n #[test]\n fn prop_insert_get_consistency(data: HashMap) {\n let (_temp, db) = setup_test_db();\n let mut map = DurableMap::::new(&db, \"prop_map\").unwrap();\n- \n+\n // Insert all pairs\n for (k, v) in &data {\n map.insert(k.clone(), *v).unwrap();\n }\n- \n+\n // Verify all can be retrieved\n for (k, v) in &data {\n prop_assert_eq!(map.get(k).unwrap(), Some(*v));\n }\n- \n+\n // Verify length\n prop_assert_eq!(map.len().unwrap(), data.len());\n }\n- \n+\n #[test]\n fn prop_remove_consistency(data: HashMap) {\n let (_temp, db) = setup_test_db();\n let mut map = DurableMap::::new(&db, \"remove_map\").unwrap();\n- \n+\n // Insert all\n map.extend(data.clone()).unwrap();\n- \n+\n // Remove all and verify\n for (k, v) in data {\n prop_assert_eq!(map.remove(&k).unwrap(), Some(v));\n prop_assert_eq!(map.remove(&k).unwrap(), None);\n prop_assert!(!map.contains_key(&k).unwrap());\n }\n- \n+\n prop_assert!(map.is_empty().unwrap());\n }\n- \n+\n #[test]\n fn prop_clear_makes_empty(data: HashMap) {\n let (_temp, db) = setup_test_db();\n let mut map = DurableMap::::new(&db, \"clear_map\").unwrap();\n- \n+\n map.extend(data).unwrap();\n map.clear().unwrap();\n- \n+\n prop_assert_eq!(map.len().unwrap(), 0);\n prop_assert!(map.is_empty().unwrap());\n prop_assert_eq!(map.to_vec().unwrap(), vec![]);\n }\n }\n- \n+\n #[test]\n fn test_nested_collections() {\n use crate::DurableVec;\n- \n+\n let (_temp, db) = setup_test_db();\n- \n+\n // Create a map where values are DurableVec\n- let users_posts: DurableMap> = DurableMap::new_nested(&db, \"user_posts\");\n- \n+ let users_posts: DurableMap> =\n+ DurableMap::new_nested(&db, \"user_posts\");\n+\n // Test creating nested collections through the entry API\n- let mut alice_posts = users_posts.entry(\"alice\".to_string()).unwrap().or_default().unwrap();\n+ let mut alice_posts = users_posts\n+ .entry(\"alice\".to_string())\n+ .unwrap()\n+ .or_default()\n+ .unwrap();\n alice_posts.push(101).unwrap();\n alice_posts.push(102).unwrap();\n alice_posts.push(103).unwrap();\n- \n+\n // Test accessing the same collection again\n- let alice_posts_again = users_posts.entry(\"alice\".to_string()).unwrap().or_default().unwrap();\n+ let alice_posts_again = users_posts\n+ .entry(\"alice\".to_string())\n+ .unwrap()\n+ .or_default()\n+ .unwrap();\n assert_eq!(alice_posts_again.len().unwrap(), 3);\n assert_eq!(alice_posts_again.get(0).unwrap(), Some(101));\n assert_eq!(alice_posts_again.get(1).unwrap(), Some(102));\n assert_eq!(alice_posts_again.get(2).unwrap(), Some(103));\n- \n+\n // Test creating a different nested collection\n- let mut bob_posts = users_posts.entry(\"bob\".to_string()).unwrap().or_default().unwrap();\n+ let mut bob_posts = users_posts\n+ .entry(\"bob\".to_string())\n+ .unwrap()\n+ .or_default()\n+ .unwrap();\n bob_posts.push(201).unwrap();\n bob_posts.push(202).unwrap();\n- \n+\n // Verify isolation between nested collections\n assert_eq!(alice_posts_again.len().unwrap(), 3);\n assert_eq!(bob_posts.len().unwrap(), 2);\n- \n+\n // Test chained calls\n- users_posts.entry(\"charlie\".to_string()).unwrap().or_default().unwrap().push(301).unwrap();\n- let charlie_posts = users_posts.entry(\"charlie\".to_string()).unwrap().or_default().unwrap();\n+ users_posts\n+ .entry(\"charlie\".to_string())\n+ .unwrap()\n+ .or_default()\n+ .unwrap()\n+ .push(301)\n+ .unwrap();\n+ let charlie_posts = users_posts\n+ .entry(\"charlie\".to_string())\n+ .unwrap()\n+ .or_default()\n+ .unwrap();\n assert_eq!(charlie_posts.len().unwrap(), 1);\n assert_eq!(charlie_posts.get(0).unwrap(), Some(301));\n }\n- \n- // Note: Nested DurableMap-in-DurableMap requires implementing Serialize/Deserialize \n+\n+ // Note: Nested DurableMap-in-DurableMap requires implementing Serialize/Deserialize\n // for DurableMap, which is not straightforward since it contains database handles.\n // For now, let's focus on the more common case of Map-to-Vec nesting.\n- \n+\n // Deep nesting with Map -> Map -> Vec also requires DurableMap serialization\n // Let's skip this for now and focus on the fundamental Map -> Vec case\n- \n+\n #[test]\n fn test_nested_collection_persistence() {\n use crate::DurableVec;\n- \n+\n let (temp_dir, db) = setup_test_db();\n- \n+\n // Create nested structure and populate it\n {\n- let users_data: DurableMap> = DurableMap::new_nested(&db, \"users\");\n- let mut user1_data = users_data.entry(\"user1\".to_string()).unwrap().or_default().unwrap();\n+ let users_data: DurableMap> =\n+ DurableMap::new_nested(&db, \"users\");\n+ let mut user1_data = users_data\n+ .entry(\"user1\".to_string())\n+ .unwrap()\n+ .or_default()\n+ .unwrap();\n user1_data.push(\"data1\".to_string()).unwrap();\n user1_data.push(\"data2\".to_string()).unwrap();\n- \n- let mut user2_data = users_data.entry(\"user2\".to_string()).unwrap().or_default().unwrap();\n+\n+ let mut user2_data = users_data\n+ .entry(\"user2\".to_string())\n+ .unwrap()\n+ .or_default()\n+ .unwrap();\n user2_data.push(\"other_data\".to_string()).unwrap();\n }\n- \n+\n // Drop the database\n drop(db);\n- \n+\n // Reopen and verify persistence\n {\n let db = Db::open(temp_dir.path()).unwrap();\n- let users_data: DurableMap> = DurableMap::new_nested(&db, \"users\");\n- \n- let user1_data = users_data.entry(\"user1\".to_string()).unwrap().or_default().unwrap();\n+ let users_data: DurableMap> =\n+ DurableMap::new_nested(&db, \"users\");\n+\n+ let user1_data = users_data\n+ .entry(\"user1\".to_string())\n+ .unwrap()\n+ .or_default()\n+ .unwrap();\n assert_eq!(user1_data.len().unwrap(), 2);\n assert_eq!(user1_data.get(0).unwrap(), Some(\"data1\".to_string()));\n assert_eq!(user1_data.get(1).unwrap(), Some(\"data2\".to_string()));\n- \n- let user2_data = users_data.entry(\"user2\".to_string()).unwrap().or_default().unwrap();\n+\n+ let user2_data = users_data\n+ .entry(\"user2\".to_string())\n+ .unwrap()\n+ .or_default()\n+ .unwrap();\n assert_eq!(user2_data.len().unwrap(), 1);\n assert_eq!(user2_data.get(0).unwrap(), Some(\"other_data\".to_string()));\n }\n }\n-} \n\\ No newline at end of file\n+}\ndiff --git a/durable/src/vec.rs b/durable/src/vec.rs\nindex 28d08fd0786df241aaf9c01b279708e547e4c034..bb6a690e5f491687b4446dc4dc612f14eb50c031 100644\n--- a/durable/src/vec.rs\n+++ b/durable/src/vec.rs\n@@ -1,6 +1,6 @@\n-use crate::{Db, Result, DurableError, DurableCollection};\n+use crate::{Db, DurableCollection, DurableError, Result};\n use rocksdb::WriteBatch;\n-use serde::{Serialize, Deserialize};\n+use serde::{Deserialize, Serialize};\n use std::marker::PhantomData;\n \n /// A persistent vector backed by RocksDB\n@@ -10,21 +10,21 @@ pub struct DurableVec {\n _phantom: PhantomData,\n }\n \n-impl DurableVec \n-where \n- T: Serialize + for<'de> Deserialize<'de>\n+impl DurableVec\n+where\n+ T: Serialize + for<'de> Deserialize<'de>,\n {\n /// Create a new DurableVec with the given name\n pub fn new(db: &Db, name: &str) -> Result {\n let prefix = format!(\"vec:{}\", name).into_bytes();\n- \n+\n Ok(DurableVec {\n db: db.clone(),\n prefix,\n _phantom: PhantomData,\n })\n }\n- \n+\n /// Create a new DurableVec from a prefix (used for nested collections)\n pub fn from_prefix(db: Db, prefix: Vec) -> Self {\n Self {\n@@ -33,7 +33,7 @@ where\n _phantom: PhantomData,\n }\n }\n- \n+\n /// Get the length of the vector\n pub fn len(&self) -> Result {\n let key = self.meta_key(\"len\");\n@@ -42,180 +42,185 @@ where\n if bytes.len() != 8 {\n return Err(DurableError::Corruption(\"Invalid length bytes size\".into()));\n }\n- let len_bytes: [u8; 8] = bytes[..8].try_into()\n+ let len_bytes: [u8; 8] = bytes[..8]\n+ .try_into()\n .map_err(|_| DurableError::Corruption(\"Invalid length bytes\".into()))?;\n Ok(u64::from_le_bytes(len_bytes) as usize)\n }\n None => Ok(0),\n }\n }\n- \n+\n /// Check if the vector is empty\n pub fn is_empty(&self) -> Result {\n Ok(self.len()? == 0)\n }\n- \n+\n /// Push an element to the end of the vector\n pub fn push(&mut self, value: T) -> Result<()> {\n let len = self.len()?;\n let mut batch = WriteBatch::default();\n- \n+\n // Serialize the value\n let value_bytes = bincode::serialize(&value)?;\n- \n+\n // Write the element\n let elem_key = self.element_key(len);\n batch.put(&elem_key, &value_bytes);\n- \n+\n // Update the length\n let new_len = (len + 1) as u64;\n let len_key = self.meta_key(\"len\");\n batch.put(&len_key, &new_len.to_le_bytes());\n- \n+\n // Commit atomically\n self.db.rocks().write(batch)?;\n self.db.rocks().flush_wal(true)?;\n- \n+\n Ok(())\n }\n- \n+\n /// Get an element at the given index\n pub fn get(&self, index: usize) -> Result> {\n let len = self.len()?;\n if index >= len {\n return Ok(None);\n }\n- \n+\n let key = self.element_key(index);\n match self.db.rocks().get(&key)? {\n Some(bytes) => {\n let value = bincode::deserialize(&bytes)?;\n Ok(Some(value))\n }\n- None => Err(DurableError::Corruption(\n- format!(\"Element at index {} not found but index < len\", index)\n- )),\n+ None => Err(DurableError::Corruption(format!(\n+ \"Element at index {} not found but index < len\",\n+ index\n+ ))),\n }\n }\n- \n+\n /// Clear all elements from the vector\n pub fn clear(&mut self) -> Result<()> {\n let len = self.len()?;\n let mut batch = WriteBatch::default();\n- \n+\n // Delete all elements\n for i in 0..len {\n let key = self.element_key(i);\n batch.delete(&key);\n }\n- \n+\n // Delete the length meta key\n let len_key = self.meta_key(\"len\");\n batch.delete(&len_key);\n- \n+\n // Commit atomically\n self.db.rocks().write(batch)?;\n self.db.rocks().flush_wal(true)?;\n- \n+\n Ok(())\n }\n- \n+\n /// Create a streaming iterator over the vector\n pub fn iter(&self) -> Result> + '_> {\n let prefix = self.element_prefix();\n- let iter = self.db.rocks().iterator(rocksdb::IteratorMode::From(&prefix, rocksdb::Direction::Forward));\n- \n+ let iter = self.db.rocks().iterator(rocksdb::IteratorMode::From(\n+ &prefix,\n+ rocksdb::Direction::Forward,\n+ ));\n+\n Ok(VecIterator {\n inner: iter,\n prefix,\n _phantom: PhantomData,\n })\n }\n- \n+\n /// Convert the entire vector to a Vec in memory\n- /// \n+ ///\n /// Note: This loads the entire collection into memory. For large collections,\n /// prefer using `iter()` which streams elements.\n pub fn to_vec(&self) -> Result> {\n let len = self.len()?;\n let mut result = Vec::with_capacity(len);\n- \n+\n for item in self.iter()? {\n result.push(item?);\n }\n- \n+\n Ok(result)\n }\n- \n+\n /// Push multiple elements in a single batch\n pub fn extend(&mut self, iter: I) -> Result<()>\n where\n- I: IntoIterator\n+ I: IntoIterator,\n {\n let mut batch = WriteBatch::default();\n let mut len = self.len()?;\n- \n+\n for value in iter {\n let value_bytes = bincode::serialize(&value)?;\n let elem_key = self.element_key(len);\n batch.put(&elem_key, &value_bytes);\n len += 1;\n }\n- \n+\n // Update length\n let len_key = self.meta_key(\"len\");\n batch.put(&len_key, &(len as u64).to_le_bytes());\n- \n+\n // Commit atomically\n self.db.rocks().write(batch)?;\n self.db.rocks().flush_wal(true)?;\n- \n+\n Ok(())\n }\n- \n+\n /// Remove and return the last element\n pub fn pop(&mut self) -> Result> {\n let len = self.len()?;\n if len == 0 {\n return Ok(None);\n }\n- \n+\n let last_idx = len - 1;\n let value = self.get(last_idx)?;\n- \n+\n let mut batch = WriteBatch::default();\n- \n+\n // Delete the last element\n let elem_key = self.element_key(last_idx);\n batch.delete(&elem_key);\n- \n+\n // Update length\n let len_key = self.meta_key(\"len\");\n batch.put(&len_key, &(last_idx as u64).to_le_bytes());\n- \n+\n // Commit atomically\n self.db.rocks().write(batch)?;\n self.db.rocks().flush_wal(true)?;\n- \n+\n Ok(value)\n }\n- \n+\n // Helper methods\n- \n+\n fn element_key(&self, index: usize) -> Vec {\n let mut key = self.prefix.clone();\n key.push(b':');\n key.extend_from_slice(&(index as u64).to_be_bytes());\n key\n }\n- \n+\n fn meta_key(&self, meta_type: &str) -> Vec {\n let mut key = self.prefix.clone();\n key.extend_from_slice(b\":__meta:\");\n key.extend_from_slice(meta_type.as_bytes());\n key\n }\n- \n+\n fn element_prefix(&self) -> Vec {\n let mut prefix = self.prefix.clone();\n prefix.push(b':');\n@@ -224,9 +229,9 @@ where\n }\n \n // Implement the DurableCollection trait for DurableVec\n-impl DurableCollection for DurableVec \n+impl DurableCollection for DurableVec\n where\n- T: Serialize + for<'de> Deserialize<'de>\n+ T: Serialize + for<'de> Deserialize<'de>,\n {\n fn from_prefix(db: Db, prefix: Vec) -> Self {\n DurableVec::from_prefix(db, prefix)\n@@ -242,10 +247,10 @@ pub struct VecIterator<'a, T> {\n \n impl<'a, T> Iterator for VecIterator<'a, T>\n where\n- T: for<'de> Deserialize<'de>\n+ T: for<'de> Deserialize<'de>,\n {\n type Item = Result;\n- \n+\n fn next(&mut self) -> Option {\n loop {\n match self.inner.next() {\n@@ -254,14 +259,14 @@ where\n if !key.starts_with(&self.prefix) {\n return None;\n }\n- \n+\n // Check if this is a meta key (skip it)\n // The key pattern is: prefix:element_index or prefix:__meta:type\n // We want to skip any key that contains \"__meta:\"\n if key.windows(7).any(|w| w == b\"__meta:\") {\n continue; // Skip this key and try the next one\n }\n- \n+\n // Deserialize the value\n match bincode::deserialize(&value) {\n Ok(item) => return Some(Ok(item)),\n@@ -279,37 +284,37 @@ where\n mod tests {\n use super::*;\n use tempfile::TempDir;\n- \n+\n fn setup_test_db() -> (TempDir, Db) {\n let temp_dir = TempDir::new().unwrap();\n let db = Db::open(temp_dir.path()).unwrap();\n (temp_dir, db)\n }\n- \n+\n #[test]\n fn test_push_and_get() {\n let (_temp, db) = setup_test_db();\n let mut vec = DurableVec::::new(&db, \"test_vec\").unwrap();\n- \n+\n // Push some values\n vec.push(\"first\".to_string()).unwrap();\n vec.push(\"second\".to_string()).unwrap();\n vec.push(\"third\".to_string()).unwrap();\n- \n+\n // Check length\n assert_eq!(vec.len().unwrap(), 3);\n- \n+\n // Get values\n assert_eq!(vec.get(0).unwrap(), Some(\"first\".to_string()));\n assert_eq!(vec.get(1).unwrap(), Some(\"second\".to_string()));\n assert_eq!(vec.get(2).unwrap(), Some(\"third\".to_string()));\n assert_eq!(vec.get(3).unwrap(), None);\n }\n- \n+\n #[test]\n fn test_persistence() {\n let (temp_dir, db) = setup_test_db();\n- \n+\n // Create and populate vector\n {\n let mut vec = DurableVec::::new(&db, \"persist_vec\").unwrap();\n@@ -317,55 +322,55 @@ mod tests {\n vec.push(100).unwrap();\n vec.push(-7).unwrap();\n }\n- \n+\n // Drop the database\n drop(db);\n- \n+\n // Reopen and verify data persists\n {\n let db = Db::open(temp_dir.path()).unwrap();\n let vec = DurableVec::::new(&db, \"persist_vec\").unwrap();\n- \n+\n assert_eq!(vec.len().unwrap(), 3);\n assert_eq!(vec.get(0).unwrap(), Some(42));\n assert_eq!(vec.get(1).unwrap(), Some(100));\n assert_eq!(vec.get(2).unwrap(), Some(-7));\n }\n }\n- \n+\n #[test]\n fn test_clear() {\n let (_temp, db) = setup_test_db();\n let mut vec = DurableVec::::new(&db, \"clear_vec\").unwrap();\n- \n+\n // Add some elements\n vec.extend(vec![1, 2, 3, 4, 5]).unwrap();\n assert_eq!(vec.len().unwrap(), 5);\n- \n+\n // Clear\n vec.clear().unwrap();\n assert_eq!(vec.len().unwrap(), 0);\n assert!(vec.is_empty().unwrap());\n- \n+\n // Should be able to push again\n vec.push(42).unwrap();\n assert_eq!(vec.len().unwrap(), 1);\n assert_eq!(vec.get(0).unwrap(), Some(42));\n }\n- \n+\n #[test]\n fn test_pop() {\n let (_temp, db) = setup_test_db();\n let mut vec = DurableVec::::new(&db, \"pop_vec\").unwrap();\n- \n+\n // Empty pop\n assert_eq!(vec.pop().unwrap(), None);\n- \n+\n // Push and pop\n vec.push(\"a\".to_string()).unwrap();\n vec.push(\"b\".to_string()).unwrap();\n vec.push(\"c\".to_string()).unwrap();\n- \n+\n assert_eq!(vec.pop().unwrap(), Some(\"c\".to_string()));\n assert_eq!(vec.len().unwrap(), 2);\n assert_eq!(vec.pop().unwrap(), Some(\"b\".to_string()));\n@@ -374,68 +379,70 @@ mod tests {\n assert_eq!(vec.len().unwrap(), 0);\n assert_eq!(vec.pop().unwrap(), None);\n }\n- \n+\n #[test]\n fn test_iteration() {\n let (_temp, db) = setup_test_db();\n let mut vec = DurableVec::::new(&db, \"iter_vec\").unwrap();\n- \n+\n // Add elements\n let values = vec![10, 20, 30, 40, 50];\n vec.extend(values.clone()).unwrap();\n- \n+\n // Iterate and collect\n let collected = vec.to_vec().unwrap();\n- \n+\n assert_eq!(collected, values);\n }\n- \n+\n #[test]\n fn test_extend() {\n let (_temp, db) = setup_test_db();\n let mut vec = DurableVec::::new(&db, \"extend_vec\").unwrap();\n- \n+\n // Extend with iterator\n- vec.extend(vec![\"a\", \"b\", \"c\"].into_iter().map(String::from)).unwrap();\n+ vec.extend(vec![\"a\", \"b\", \"c\"].into_iter().map(String::from))\n+ .unwrap();\n assert_eq!(vec.len().unwrap(), 3);\n- \n+\n // Extend again\n- vec.extend(vec![\"d\", \"e\"].into_iter().map(String::from)).unwrap();\n+ vec.extend(vec![\"d\", \"e\"].into_iter().map(String::from))\n+ .unwrap();\n assert_eq!(vec.len().unwrap(), 5);\n- \n+\n // Verify all elements\n let all = vec.to_vec().unwrap();\n assert_eq!(all, vec![\"a\", \"b\", \"c\", \"d\", \"e\"]);\n }\n- \n+\n #[test]\n fn test_large_dataset() {\n let (_temp, db) = setup_test_db();\n let mut vec = DurableVec::::new(&db, \"large_vec\").unwrap();\n- \n+\n // Push many elements\n let count = 1000;\n for i in 0..count {\n vec.push(i).unwrap();\n }\n- \n+\n assert_eq!(vec.len().unwrap(), count as usize);\n- \n+\n // Verify some random accesses\n assert_eq!(vec.get(0).unwrap(), Some(0));\n assert_eq!(vec.get(500).unwrap(), Some(500));\n assert_eq!(vec.get(999).unwrap(), Some(999));\n assert_eq!(vec.get(1000).unwrap(), None);\n- \n+\n // Verify iteration count\n let all_values = vec.to_vec().unwrap();\n assert_eq!(all_values.len(), count as usize);\n }\n- \n- #[test] \n+\n+ #[test]\n fn test_complex_types() {\n- use serde::{Serialize, Deserialize};\n- \n+ use serde::{Deserialize, Serialize};\n+\n #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]\n struct User {\n id: u64,\n@@ -443,36 +450,36 @@ mod tests {\n email: String,\n active: bool,\n }\n- \n+\n let (_temp, db) = setup_test_db();\n let mut vec = DurableVec::::new(&db, \"users\").unwrap();\n- \n+\n let user1 = User {\n id: 1,\n name: \"Alice\".to_string(),\n email: \"alice@example.com\".to_string(),\n active: true,\n };\n- \n+\n let user2 = User {\n id: 2,\n name: \"Bob\".to_string(),\n email: \"bob@example.com\".to_string(),\n active: false,\n };\n- \n+\n vec.push(user1.clone()).unwrap();\n vec.push(user2.clone()).unwrap();\n- \n+\n assert_eq!(vec.get(0).unwrap(), Some(user1));\n assert_eq!(vec.get(1).unwrap(), Some(user2));\n }\n- \n+\n #[test]\n fn test_empty_vec_operations() {\n let (_temp, db) = setup_test_db();\n let vec = DurableVec::::new(&db, \"empty_vec\").unwrap();\n- \n+\n // Test operations on empty vec\n assert_eq!(vec.len().unwrap(), 0);\n assert!(vec.is_empty().unwrap());\n@@ -480,93 +487,93 @@ mod tests {\n assert_eq!(vec.get(100).unwrap(), None);\n assert_eq!(vec.to_vec().unwrap(), Vec::::new());\n }\n- \n+\n #[test]\n fn test_multiple_vecs_same_db() {\n let (_temp, db) = setup_test_db();\n- \n+\n // Create multiple vectors with different names\n let mut vec1 = DurableVec::::new(&db, \"vec1\").unwrap();\n let mut vec2 = DurableVec::::new(&db, \"vec2\").unwrap();\n- \n+\n // Push different data to each\n vec1.push(\"vec1_data\".to_string()).unwrap();\n vec2.push(\"vec2_data\".to_string()).unwrap();\n- \n+\n // Verify they don't interfere\n assert_eq!(vec1.get(0).unwrap(), Some(\"vec1_data\".to_string()));\n assert_eq!(vec2.get(0).unwrap(), Some(\"vec2_data\".to_string()));\n assert_eq!(vec1.len().unwrap(), 1);\n assert_eq!(vec2.len().unwrap(), 1);\n }\n- \n+\n #[test]\n fn test_batch_atomicity() {\n let (_temp, db) = setup_test_db();\n let mut vec = DurableVec::::new(&db, \"batch_vec\").unwrap();\n- \n+\n // Add initial data\n vec.push(1).unwrap();\n vec.push(2).unwrap();\n vec.push(3).unwrap();\n- \n+\n // Verify initial state\n assert_eq!(vec.len().unwrap(), 3);\n- \n+\n // Clear should be atomic - either all elements deleted or none\n vec.clear().unwrap();\n assert_eq!(vec.len().unwrap(), 0);\n- \n+\n // Extend should be atomic - either all elements added or none\n vec.extend(vec![10, 20, 30, 40, 50]).unwrap();\n assert_eq!(vec.len().unwrap(), 5);\n let all = vec.to_vec().unwrap();\n assert_eq!(all, vec![10, 20, 30, 40, 50]);\n }\n- \n+\n #[test]\n fn test_unicode_strings() {\n let (_temp, db) = setup_test_db();\n let mut vec = DurableVec::::new(&db, \"unicode_vec\").unwrap();\n- \n+\n let test_strings = vec![\n \"Hello, 世界!\".to_string(),\n \"🦀 Rust 🚀\".to_string(),\n \"Ñoño\".to_string(),\n \"🏴‍☠️ Pirates\".to_string(),\n ];\n- \n+\n vec.extend(test_strings.clone()).unwrap();\n- \n+\n let retrieved = vec.to_vec().unwrap();\n assert_eq!(retrieved, test_strings);\n }\n- \n+\n #[test]\n fn test_streaming_iterator() {\n let (_temp, db) = setup_test_db();\n let mut vec = DurableVec::::new(&db, \"stream_vec\").unwrap();\n- \n+\n // Add test data\n let values = vec![1, 2, 3, 4, 5];\n vec.extend(values.clone()).unwrap();\n- \n+\n // Test streaming iteration\n let mut collected = Vec::new();\n for item in vec.iter().unwrap() {\n collected.push(item.unwrap());\n }\n- \n+\n assert_eq!(collected, values);\n- \n+\n // Test that iterator properly handles prefix boundaries\n let mut vec2 = DurableVec::::new(&db, \"stream_vec2\").unwrap();\n vec2.extend(vec![10, 20, 30]).unwrap();\n- \n+\n // Each iterator should only see its own data\n let collected1: Vec<_> = vec.iter().unwrap().collect::>>().unwrap();\n let collected2: Vec<_> = vec2.iter().unwrap().collect::>>().unwrap();\n- \n+\n assert_eq!(collected1, values);\n assert_eq!(collected2, vec![10, 20, 30]);\n }\n@@ -577,82 +584,82 @@ mod proptests {\n use super::*;\n use proptest::prelude::*;\n use tempfile::TempDir;\n- \n+\n fn setup_test_db() -> (TempDir, Db) {\n let temp_dir = TempDir::new().unwrap();\n let db = Db::open(temp_dir.path()).unwrap();\n (temp_dir, db)\n }\n- \n+\n proptest! {\n #[test]\n fn prop_push_get_consistency(values: Vec) {\n let (_temp, db) = setup_test_db();\n let mut vec = DurableVec::::new(&db, \"prop_vec\").unwrap();\n- \n+\n // Push all values\n for value in &values {\n vec.push(*value).unwrap();\n }\n- \n+\n // Verify length\n prop_assert_eq!(vec.len().unwrap(), values.len());\n- \n+\n // Verify all values can be retrieved correctly\n for (i, expected) in values.iter().enumerate() {\n prop_assert_eq!(vec.get(i).unwrap(), Some(*expected));\n }\n }\n- \n+\n #[test]\n fn prop_extend_iter_roundtrip(values: Vec) {\n let (_temp, db) = setup_test_db();\n let mut vec = DurableVec::::new(&db, \"extend_vec\").unwrap();\n- \n+\n // Extend with all values\n vec.extend(values.clone()).unwrap();\n- \n+\n // Get back via iteration\n let retrieved = vec.to_vec().unwrap();\n- \n+\n prop_assert_eq!(retrieved, values);\n }\n- \n+\n #[test]\n fn prop_pop_removes_last(mut values: Vec) {\n let (_temp, db) = setup_test_db();\n let mut vec = DurableVec::::new(&db, \"pop_vec\").unwrap();\n- \n+\n // Add all values\n vec.extend(values.clone()).unwrap();\n- \n+\n // Pop values and verify\n while let Some(expected) = values.pop() {\n let popped = vec.pop().unwrap();\n prop_assert_eq!(popped, Some(expected));\n prop_assert_eq!(vec.len().unwrap(), values.len());\n }\n- \n+\n // Vector should be empty\n prop_assert!(vec.is_empty().unwrap());\n prop_assert_eq!(vec.pop().unwrap(), None);\n }\n- \n+\n #[test]\n fn prop_clear_makes_empty(values: Vec) {\n let (_temp, db) = setup_test_db();\n let mut vec = DurableVec::::new(&db, \"clear_vec\").unwrap();\n- \n+\n // Add values\n vec.extend(values).unwrap();\n- \n+\n // Clear\n vec.clear().unwrap();\n- \n+\n // Should be empty\n prop_assert_eq!(vec.len().unwrap(), 0);\n prop_assert!(vec.is_empty().unwrap());\n prop_assert_eq!(vec.get(0).unwrap(), None);\n }\n }\n-} \n\\ No newline at end of file\n+}\ndiff --git a/rust-toolchain.toml b/rust-toolchain.toml\nindex e88baf106b9e6550446ae1cd13dbba561193e062..7855e6d557c0d29ac368603b23807b7f6e2379bd 100644\n--- a/rust-toolchain.toml\n+++ b/rust-toolchain.toml\n@@ -1,2 +1,3 @@\n [toolchain]\n channel = \"1.88.0\"\n+components = [\"rustfmt\", \"clippy\"]\ndiff --git a/server/src/events.rs b/server/src/events.rs\nindex b21cb4d6ff3fe2a217f6abca3b17dd639bf4e839..1a1525e265b56cc1c4bbab4ae6d496ce5ea42e33 100644\n--- a/server/src/events.rs\n+++ b/server/src/events.rs\n@@ -20,9 +20,5 @@ pub enum Event {\n /// Register a node path in the fractal tree (no external fetch).\n NodeEnsured { id: String },\n /// Full upstream API payload for a node (domain-specific view derived at replay/render time).\n- EntityImported {\n- id: String,\n- ts: i64,\n- payload: Value,\n- },\n+ EntityImported { id: String, ts: i64, payload: Value },\n }\ndiff --git a/server/src/html/vote.rs b/server/src/html/vote.rs\nindex 30dab303b9d1e763a5b9261dffff13a778e3e4e3..8cf2b0d4d8bd58cc51cc27c9046fa7f7728a7017 100644\n--- a/server/src/html/vote.rs\n+++ b/server/src/html/vote.rs\n@@ -31,10 +31,7 @@ pub struct VoteQuery {\n }\n \n pub fn vote_href(parent: &ItemId) -> String {\n- format!(\n- \"/vote?parent={}\",\n- urlencoding::encode(parent.as_str())\n- )\n+ format!(\"/vote?parent={}\", urlencoding::encode(parent.as_str()))\n }\n \n fn vote_compare_href(parent: &ItemId, left: &ItemId, right: &ItemId) -> String {\n@@ -114,7 +111,12 @@ fn slider_value_from_ratios(r_left: i32, r_right: i32) -> i32 {\n ((r / sum) * 100.0).round().clamp(0.0, 100.0) as i32\n }\n \n-fn vote_edge_history(tree: &GlobalTree, group: &GroupState, left: &ItemId, right: &ItemId) -> Markup {\n+fn vote_edge_history(\n+ tree: &GlobalTree,\n+ group: &GroupState,\n+ left: &ItemId,\n+ right: &ItemId,\n+) -> Markup {\n let mut votes = edge_votes(group, left, right);\n votes.sort_by(|a, b| b.ts.cmp(&a.ts));\n let legend_left = child_title(tree, left);\n@@ -154,7 +156,6 @@ fn vote_edge_history(tree: &GlobalTree, group: &GroupState, left: &ItemId, right\n }\n }\n \n-\n fn vote_hud_form(\n parent: &ItemId,\n left: &ItemId,\n@@ -200,7 +201,12 @@ fn vote_compare_actions(parent: &ItemId, next: Option<&(ItemId, ItemId)>) -> Mar\n }\n }\n \n-fn vote_ranking_sidebar(tree: &GlobalTree, parent: &ItemId, left: &ItemId, right: &ItemId) -> Markup {\n+fn vote_ranking_sidebar(\n+ tree: &GlobalTree,\n+ parent: &ItemId,\n+ left: &ItemId,\n+ right: &ItemId,\n+) -> Markup {\n let empty = NodeState::default();\n let node = tree.get(parent).unwrap_or(&empty);\n let highlighted: HashSet = [left.clone(), right.clone()].into_iter().collect();\n@@ -222,11 +228,7 @@ pub(crate) fn vote_recorded_morph(\n ) -> JsBuilder {\n let pool = children_of(tree, parent);\n let empty = NodeState::default();\n- let group = tree\n- .get(parent)\n- .unwrap_or(&empty)\n- .local_ranking\n- .clone();\n+ let group = tree.get(parent).unwrap_or(&empty).local_ranking.clone();\n let edge_history = vote_edge_history(tree, &group, left, right);\n let next_pair = suggest_next(&group, left, right, &pool);\n let actions = vote_compare_actions(parent, next_pair.as_ref());\n@@ -249,8 +251,12 @@ fn vote_compare_item_card(tree: &GlobalTree, item: &ItemId, side_class: &str) ->\n }\n }\n \n-\n-fn suggest_next(group: &GroupState, left: &ItemId, right: &ItemId, pool: &[ItemId]) -> Option<(ItemId, ItemId)> {\n+fn suggest_next(\n+ group: &GroupState,\n+ left: &ItemId,\n+ right: &ItemId,\n+ pool: &[ItemId],\n+) -> Option<(ItemId, ItemId)> {\n suggest_next_pair_in_pool(group, pool, Some((left, right)))\n }\n \n@@ -262,22 +268,20 @@ pub async fn vote_page(\n let left_param = q.left.as_deref().map(parse_item_param);\n let right_param = q.right.as_deref().map(parse_item_param);\n \n- let tree = state.scope_tree(&parent).unwrap_or_else(|_| GlobalTree::new());\n+ let tree = state\n+ .scope_tree(&parent)\n+ .unwrap_or_else(|_| GlobalTree::new());\n let empty = NodeState::default();\n let parent_node = tree.get(&parent).unwrap_or(&empty);\n \n- let (left, right) = match resolve_pair(\n- &tree,\n- &parent,\n- left_param.as_ref(),\n- right_param.as_ref(),\n- ) {\n- Ok(p) => p,\n- Err(e) => {\n- let (msg, status) = e.status_message();\n- return (status, msg).into_response();\n- }\n- };\n+ let (left, right) =\n+ match resolve_pair(&tree, &parent, left_param.as_ref(), right_param.as_ref()) {\n+ Ok(p) => p,\n+ Err(e) => {\n+ let (msg, status) = e.status_message();\n+ return (status, msg).into_response();\n+ }\n+ };\n \n let pool = children_of(&tree, &parent);\n let group = &parent_node.local_ranking;\n@@ -326,15 +330,7 @@ pub async fn vote_page(\n state.views.increment(path.clone());\n let views = state.views.get_views(&path);\n \n- Html(\n- layout(\n- &title,\n- body,\n- views,\n- )\n- .into_string(),\n- )\n- .into_response()\n+ Html(layout(&title, body, views).into_string()).into_response()\n }\n \n #[cfg(test)]\ndiff --git a/server/src/pair.rs b/server/src/pair.rs\nindex 606ffa51038a57ffacf335aa48bdb1185f8483fb..54b5d2417e9dba04ed8df422156e274c2b2f76b2 100644\n--- a/server/src/pair.rs\n+++ b/server/src/pair.rs\n@@ -89,10 +89,7 @@ fn pair_priority(\n }\n \n /// All unordered pairs from `pool`, optionally skipping `exclude`.\n-fn candidate_pairs(\n- pool: &[ItemId],\n- exclude: Option<(&ItemId, &ItemId)>,\n-) -> Vec<(ItemId, ItemId)> {\n+fn candidate_pairs(pool: &[ItemId], exclude: Option<(&ItemId, &ItemId)>) -> Vec<(ItemId, ItemId)> {\n let mut out = Vec::new();\n for i in 0..pool.len() {\n for j in (i + 1)..pool.len() {\n@@ -228,10 +225,7 @@ impl PairError {\n \"provide both left and right, or neither\",\n axum::http::StatusCode::BAD_REQUEST,\n ),\n- Self::NoPair => (\n- \"no pair available\",\n- axum::http::StatusCode::BAD_REQUEST,\n- ),\n+ Self::NoPair => (\"no pair available\", axum::http::StatusCode::BAD_REQUEST),\n }\n }\n }\n@@ -292,16 +286,20 @@ mod tests {\n \"reddit.com/r/rust/d\",\n ],\n );\n- let ab = VoteData::from_recorded(1, \"reddit.com/r/rust/a\", \"reddit.com/r/rust/b\", 2, 1).unwrap();\n- let cd = VoteData::from_recorded(2, \"reddit.com/r/rust/c\", \"reddit.com/r/rust/d\", 2, 1).unwrap();\n+ let ab =\n+ VoteData::from_recorded(1, \"reddit.com/r/rust/a\", \"reddit.com/r/rust/b\", 2, 1).unwrap();\n+ let cd =\n+ VoteData::from_recorded(2, \"reddit.com/r/rust/c\", \"reddit.com/r/rust/d\", 2, 1).unwrap();\n tree.apply_vote(&parent, ab);\n tree.apply_vote(&parent, cd);\n let group = tree.get(&parent).unwrap().local_ranking.clone();\n let pool = children_of(&tree, &parent);\n let pair = suggest_next_pair_in_pool(&group, &pool, None).unwrap();\n let chosen = pair_set(&pair);\n- let from_ab = chosen.contains(\"reddit.com/r/rust/a\") || chosen.contains(\"reddit.com/r/rust/b\");\n- let from_cd = chosen.contains(\"reddit.com/r/rust/c\") || chosen.contains(\"reddit.com/r/rust/d\");\n+ let from_ab =\n+ chosen.contains(\"reddit.com/r/rust/a\") || chosen.contains(\"reddit.com/r/rust/b\");\n+ let from_cd =\n+ chosen.contains(\"reddit.com/r/rust/c\") || chosen.contains(\"reddit.com/r/rust/d\");\n assert!(from_ab && from_cd, \"expected bridge pair, got {:?}\", chosen);\n }\n \n@@ -316,7 +314,8 @@ mod tests {\n \"reddit.com/r/rust/c\",\n ],\n );\n- let ab = VoteData::from_recorded(1, \"reddit.com/r/rust/a\", \"reddit.com/r/rust/b\", 2, 1).unwrap();\n+ let ab =\n+ VoteData::from_recorded(1, \"reddit.com/r/rust/a\", \"reddit.com/r/rust/b\", 2, 1).unwrap();\n tree.apply_vote(&parent, ab);\n let group = tree.get(&parent).unwrap().local_ranking.clone();\n let pool = children_of(&tree, &parent);\ndiff --git a/server/src/parser.rs b/server/src/parser.rs\nindex 51aa0e0f982546aab68bd5e1ca0cb726e88b3f50..9df2dcc9313f7fe250ce3b6aa167f6ec5d57951f 100644\n--- a/server/src/parser.rs\n+++ b/server/src/parser.rs\n@@ -43,10 +43,7 @@ mod tests {\n \"https://old.reddit.com/r/AmItheAsshole/comments/1trnvdl/aita_for_cancelling/\",\n )\n .unwrap();\n- assert_eq!(\n- id.as_str(),\n- \"reddit.com/r/amitheasshole/comments/1trnvdl\"\n- );\n+ assert_eq!(id.as_str(), \"reddit.com/r/amitheasshole/comments/1trnvdl\");\n }\n \n #[test]\ndiff --git a/server/src/path_types.rs b/server/src/path_types.rs\nindex a0d1a028b8bd3c5d1c714dea3ec597d1ac39e1bc..fafd924452f6fd85e7a5b27ed2653a19581e6e56 100644\n--- a/server/src/path_types.rs\n+++ b/server/src/path_types.rs\n@@ -114,11 +114,7 @@ impl ItemId {\n if self.as_str().contains(\"://\") {\n return self.as_str().to_string();\n }\n- if self\n- .segments()\n- .first()\n- .is_some_and(|s| s.contains('.'))\n- {\n+ if self.segments().first().is_some_and(|s| s.contains('.')) {\n format!(\"https://{}\", self.as_str())\n } else {\n self.as_str().to_string()\n@@ -146,8 +142,7 @@ impl ItemId {\n }\n \n pub fn from_browse_uri(path: &str) -> Option {\n- path.strip_prefix(\"/~/\")\n- .map(ItemId::from_browse_tail)\n+ path.strip_prefix(\"/~/\").map(ItemId::from_browse_tail)\n }\n \n fn canonicalize(raw: &str) -> Option {\n@@ -273,10 +268,7 @@ mod tests {\n \"https://old.reddit.com/r/AmItheAsshole/comments/1trnvdl/aita_for_cancelling/\",\n )\n .unwrap();\n- assert_eq!(\n- id.as_str(),\n- \"reddit.com/r/amitheasshole/comments/1trnvdl\"\n- );\n+ assert_eq!(id.as_str(), \"reddit.com/r/amitheasshole/comments/1trnvdl\");\n }\n \n #[test]\n@@ -296,10 +288,7 @@ mod tests {\n #[test]\n fn parent_of_post_is_subreddit() {\n let id = ItemId::parse(\"reddit.com/r/aww/comments/1trnvdl\").unwrap();\n- assert_eq!(\n- id.parent().unwrap().as_str(),\n- \"reddit.com/r/aww\"\n- );\n+ assert_eq!(id.parent().unwrap().as_str(), \"reddit.com/r/aww\");\n }\n \n #[test]\n@@ -342,7 +331,8 @@ mod tests {\n \n #[test]\n fn from_storage_strips_post_title_slug() {\n- let id = ItemId::from_storage(\"reddit.com/r/rust/comments/aaa/announcing_rust_199\").unwrap();\n+ let id =\n+ ItemId::from_storage(\"reddit.com/r/rust/comments/aaa/announcing_rust_199\").unwrap();\n assert_eq!(id.as_str(), \"reddit.com/r/rust/comments/aaa\");\n }\n \ndiff --git a/server/src/ranking.rs b/server/src/ranking.rs\nindex 93cb4c9f5887a1e598cdc9d648751055f618adcc..a83f7be120d9420db042479222ca9efc230b6da9 100644\n--- a/server/src/ranking.rs\n+++ b/server/src/ranking.rs\n@@ -68,12 +68,8 @@ pub fn connected_components_from_voted_pairs(\n /// there is no score cache.\n pub fn ranked_items(group: &GroupState) -> Vec {\n let n = group.idx_to_item.len();\n- let scores = compute_scores_from_edges(\n- n,\n- group.edges.iter().map(|(&k, &w)| (k, w)),\n- MAX_ITERS,\n- TOL,\n- );\n+ let scores =\n+ compute_scores_from_edges(n, group.edges.iter().map(|(&k, &w)| (k, w)), MAX_ITERS, TOL);\n \n let mut items: Vec = group\n .idx_to_item\n@@ -85,7 +81,11 @@ pub fn ranked_items(group: &GroupState) -> Vec {\n })\n .collect();\n \n- items.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap_or(std::cmp::Ordering::Equal));\n+ items.sort_by(|a, b| {\n+ b.score\n+ .partial_cmp(&a.score)\n+ .unwrap_or(std::cmp::Ordering::Equal)\n+ });\n items\n }\n \n@@ -216,7 +216,12 @@ pub fn compute_scores_from_edges(\n /// Rank-centrality within a subset of items (an induced subgraph), using the group's aggregated edges.\n ///\n /// `idxs` are indices into `group.idx_to_item`. The returned items use the original item names.\n-pub fn ranked_items_subset(group: &GroupState, idxs: &[usize], max_iters: usize, tol: f64) -> Vec {\n+pub fn ranked_items_subset(\n+ group: &GroupState,\n+ idxs: &[usize],\n+ max_iters: usize,\n+ tol: f64,\n+) -> Vec {\n if idxs.is_empty() {\n return vec![];\n }\n@@ -241,11 +246,18 @@ pub fn ranked_items_subset(group: &GroupState, idxs: &[usize], max_iters: usize,\n .enumerate()\n .filter_map(|(j, &orig)| {\n let item = group.idx_to_item.get(orig)?.clone();\n- Some(RankedItem { item, score: *scores.get(j).unwrap_or(&0.0) })\n+ Some(RankedItem {\n+ item,\n+ score: *scores.get(j).unwrap_or(&0.0),\n+ })\n })\n .collect();\n \n- items.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap_or(std::cmp::Ordering::Equal));\n+ items.sort_by(|a, b| {\n+ b.score\n+ .partial_cmp(&a.score)\n+ .unwrap_or(std::cmp::Ordering::Equal)\n+ });\n items\n }\n \n@@ -417,8 +429,10 @@ mod tests {\n g.apply_vote(vote(1, \"a\", \"b\", 3, 1)); // a > b\n g.apply_vote(vote(2, \"c\", \"d\", 1, 4)); // d > c\n \n- let (comps, _) =\n- connected_components_from_voted_pairs(g.idx_to_item.len(), g.voted_pairs.iter().copied());\n+ let (comps, _) = connected_components_from_voted_pairs(\n+ g.idx_to_item.len(),\n+ g.voted_pairs.iter().copied(),\n+ );\n assert_eq!(comps.len(), 2);\n \n // Rank each component and ensure winner is first within that component.\ndiff --git a/server/src/ui_action.rs b/server/src/ui_action.rs\nindex 237411a689fb10ad1b7022ea66aee1969c7507ec..227ab3f6e8e9cce1532454450773f65daa24bfe7 100644\n--- a/server/src/ui_action.rs\n+++ b/server/src/ui_action.rs\n@@ -36,9 +36,7 @@ pub enum HtmlUiAction {\n vote_compare: bool,\n },\n /// Parse pasted Reddit URL/path; redirect to subreddit ranking on success.\n- ParseQuery {\n- query: String,\n- },\n+ ParseQuery { query: String },\n /// Import entity data; `POST /ui` responds with `text/event-stream` whose\n /// events carry JS snippets to `eval` (Idiomorph morphs), not JSON.\n FetchEntity {\ndiff --git a/server/src/views.rs b/server/src/views.rs\nindex a3712e779d4b34bca2c021a2451721c4055222c2..35c2089f4c10911318dbac3d3537d04308723f4f 100644\n--- a/server/src/views.rs\n+++ b/server/src/views.rs\n@@ -140,7 +140,10 @@ async fn flush_worker_loop(\n }\n }\n \n-fn flush_dirty(memory: &MemStateLock, inner: &Arc>) -> Result<(), ViewStoreError> {\n+fn flush_dirty(\n+ memory: &MemStateLock,\n+ inner: &Arc>,\n+) -> Result<(), ViewStoreError> {\n let snapshot: Vec<(String, u64)> = {\n let mut mem = memory.lock().map_err(|_| ViewStoreError::Poisoned)?;\n if mem.dirty.is_empty() {\n@@ -160,9 +163,7 @@ fn flush_dirty(memory: &MemStateLock, inner: &Arc>) -> Res\n let inner = inner.lock().map_err(|_| ViewStoreError::Poisoned)?;\n let mut batch = inner.db.batch();\n for (path, count) in &snapshot {\n- inner\n- .counts\n- .put_in_batch(&mut batch, path, count)?;\n+ inner.counts.put_in_batch(&mut batch, path, count)?;\n }\n batch.commit()?;\n Ok(())\ndiff --git a/server/static/sorter.css b/server/static/sorter.css\nindex bdd3d931365703705be08537a07f3ccaf975d3ea..e66a1e6c1acc473c8ff1ddb1e16e75a82d33741c 100644\n--- a/server/static/sorter.css\n+++ b/server/static/sorter.css\n@@ -468,9 +468,6 @@ h1 {\n margin: 0;\n background: transparent;\n cursor: pointer;\n-}\n-\n-.vote-hud-slider input[type=\"range\"] {\n --vote-track-muted: color-mix(in oklch, var(--muted) 55%, var(--bg));\n }\n \n\n\nSide B — contributor: tommy-mor\nSide B — commit message:\n[595b3850] Fix star-topology ranking by using degree-based d_max (#146).\n\nA pure forward star at the default `>` ratio (2:1) produced uniform 1/3\nscores, and the alphabetical-fallback sort placed the unambiguous winner\nlast. Root cause: `compute_scores_from_edges` divided by the max sum of\npairwise-normalized weights, so every node ended up with P_ii = 0 — a\nbipartite Markov chain whose power iteration oscillated and, after the\nconfigured even iteration count, returned to the uniform initial state.\n\nSwitch the divisor to the unweighted max neighbor degree, matching the\ncanonical Rank Centrality definition in Negahban–Oh–Shah 2012 §3.1\n(arXiv:1209.1688, eq. defP and the d_max definition in §6). This gives\nevery non-saturated node a positive self-loop, makes the chain aperiodic,\nand converges the star to π_zebra = 1/2, π_alpha = π_beta = 1/4.\n\nAdd Rust regression test and a Clojure test that drives the sorterc\nbinary against four .sorter fixtures (star, inverse star, chain, cycle).\n\nCo-Authored-By: Claude Opus 4.7 (1M context) \n\nSide B — unified diff (full patch):\ndiff --git a/server/src/ranking.rs b/server/src/ranking.rs\nindex 3710c9f64437f5bef3b2121905b6f3bcb7611047..38e6d09b4370e5f8cbae09c0e5760b4e7f1ef7db 100644\n--- a/server/src/ranking.rs\n+++ b/server/src/ranking.rs\n@@ -1,4 +1,4 @@\n-use std::collections::HashMap;\n+use std::collections::{HashMap, HashSet};\n \n use crate::path_types::ItemId;\n use crate::reducer::GroupState;\n@@ -143,23 +143,35 @@ fn compute_scores_from_edges(n: usize, edges: impl Iterator 0 for every non-maximum-degree node, and for max-degree\n+ // nodes whenever any neighbor weight is below 1 (i.e. not a unanimous\n+ // loss). Without this, regular comparison graphs (e.g. a pure star at\n+ // ratio 2:1) produce a bipartite chain that oscillates instead of\n+ // converging — see issue #146.\n let mut out_edges: Vec> = vec![Vec::new(); n];\n- let mut out_deg: Vec = vec![0.0; n];\n+ let mut neighbors: Vec> = vec![HashSet::new(); n];\n \n for ((src, dst), w) in &normalized {\n out_edges[*src].push((*dst, *w));\n- out_deg[*src] += w;\n+ neighbors[*src].insert(*dst);\n+ neighbors[*dst].insert(*src);\n }\n \n- let mut max_out = 0.0f64;\n- for &d in &out_deg {\n- if d > max_out {\n- max_out = d;\n- }\n- }\n- if max_out <= 1e-12 {\n+ let weight_sum: Vec = out_edges\n+ .iter()\n+ .map(|es| es.iter().map(|(_, w)| *w).sum())\n+ .collect();\n+ let d_max = neighbors.iter().map(|s| s.len()).max().unwrap_or(0);\n+ if d_max == 0 {\n return vec![1.0 / n as f64; n];\n }\n+ let d_max_f = d_max as f64;\n \n let mut scores = vec![1.0 / n as f64; n];\n let mut next = vec![0.0f64; n];\n@@ -167,14 +179,14 @@ fn compute_scores_from_edges(n: usize, edges: impl Iterator` ratio (2:1).\n+ /// Under the old (sum-of-weights) divisor every node had P_ii = 0 and the\n+ /// chain was bipartite; power iteration oscillated and returned the\n+ /// uniform initial distribution after an even number of steps. Using the\n+ /// paper's degree-based d_max gives every node a positive self-loop and\n+ /// the chain converges to the correct stationary distribution.\n+ #[test]\n+ fn star_topology_winner_at_top_via_subset() {\n+ let mut g = mk_group();\n+ g.apply_vote(vote(1, \"zebra\", \"alpha\", 2, 1));\n+ g.apply_vote(vote(2, \"zebra\", \"beta\", 2, 1));\n+\n+ let mut items: Vec<(usize, String)> = g\n+ .idx_to_item\n+ .iter()\n+ .enumerate()\n+ .map(|(i, it)| (i, it.as_str().to_string()))\n+ .collect();\n+ items.sort_by(|a, b| a.1.cmp(&b.1));\n+ let idxs: Vec = items.iter().map(|(i, _)| *i).collect();\n+\n+ let ranked = ranked_items_subset(&g, &idxs, 10000, 1e-8);\n+ for r in &ranked {\n+ eprintln!(\"{}: {}\", r.item.as_str(), r.score);\n+ }\n+ assert_eq!(\n+ ranked[0].item.as_str(),\n+ \"https://slug.social/zebra\",\n+ \"zebra won both votes and should rank #1\"\n+ );\n+ }\n+\n #[test]\n fn group_ranking_cache_dirty_flow() {\n let mut g = mk_group();\ndiff --git a/test/fixtures/ranking/chain.sorter b/test/fixtures/ranking/chain.sorter\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..912a96bdc08f631be27f3c9afc7e05004a2457c0\n--- /dev/null\n+++ b/test/fixtures/ranking/chain.sorter\n@@ -0,0 +1,10 @@\n+#t3\n+\n+~/t3/a { head of chain }\n+~/t3/b { middle }\n+~/t3/c { tail }\n+\n+{ a > b }\n+~/t3/a > ~/t3/b\n+{ b > c }\n+~/t3/b > ~/t3/c\ndiff --git a/test/fixtures/ranking/cycle.sorter b/test/fixtures/ranking/cycle.sorter\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..771731ae176e5d77e00ab89767ff2d7465bb7c6b\n--- /dev/null\n+++ b/test/fixtures/ranking/cycle.sorter\n@@ -0,0 +1,12 @@\n+#t4\n+\n+~/t4/a { node a }\n+~/t4/b { node b }\n+~/t4/c { node c }\n+\n+{ a > b }\n+~/t4/a > ~/t4/b\n+{ b > c }\n+~/t4/b > ~/t4/c\n+{ c > a }\n+~/t4/c > ~/t4/a\ndiff --git a/test/fixtures/ranking/star.sorter b/test/fixtures/ranking/star.sorter\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..135c9f9097d57b73c7ab18fac737ed3598d76fbe\n--- /dev/null\n+++ b/test/fixtures/ranking/star.sorter\n@@ -0,0 +1,11 @@\n+#repro\n+\n+~/repro/zebra { winner — beats both others }\n+~/repro/alpha { loser — alphabetically first }\n+~/repro/beta { loser — alphabetically middle }\n+\n+{ zebra beats alpha }\n+~/repro/zebra > ~/repro/alpha\n+\n+{ zebra beats beta }\n+~/repro/zebra > ~/repro/beta\ndiff --git a/test/fixtures/ranking/star_inverse.sorter b/test/fixtures/ranking/star_inverse.sorter\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..dab842fa924e5124865ee216b9565f6c7471c812\n--- /dev/null\n+++ b/test/fixtures/ranking/star_inverse.sorter\n@@ -0,0 +1,11 @@\n+#t2\n+\n+~/t2/win { source of incoming edges (loses both) }\n+~/t2/loss-a { winner }\n+~/t2/loss-b { winner }\n+\n+{ loss-a beats win }\n+~/t2/loss-a > ~/t2/win\n+\n+{ loss-b beats win }\n+~/t2/loss-b > ~/t2/win\ndiff --git a/test/ranking.clj b/test/ranking.clj\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..e1358783a84a264ee633bd79151ba29750b717cf\n--- /dev/null\n+++ b/test/ranking.clj\n@@ -0,0 +1,74 @@\n+(ns test.ranking\n+ \"Drives sorterc on .sorter fixtures and asserts ranking properties.\n+\n+ Regression coverage for issue #146 — pure forward star at default ratio\n+ (2:1 for `>`) used to produce tied uniform scores because the random walk\n+ on the normalized edge weights was bipartite. Fixed by switching to the\n+ degree-based d_max from Negahban–Oh–Shah rank centrality (§3.1).\"\n+ (:require [clojure.test :refer [deftest is testing]]\n+ [babashka.process :as p]\n+ [cheshire.core :as json]\n+ [clojure.java.io :as io]))\n+\n+(def sorterc-bin\n+ \"Path to the locally-built sorterc binary. Builds on demand if missing.\"\n+ (let [dbg \"target/debug/sorterc\"\n+ release \"target/release/sorterc\"]\n+ (cond\n+ (.exists (io/file release)) release\n+ (.exists (io/file dbg)) dbg\n+ :else\n+ (do (println \"building sorterc…\")\n+ (let [r (p/shell {:out :string :err :string :continue true}\n+ \"cargo build -p sorterc\")]\n+ (when-not (zero? (:exit r))\n+ (throw (ex-info \"cargo build -p sorterc failed\"\n+ {:stderr (:err r)}))))\n+ dbg))))\n+\n+(defn compile-sorter [fixture-path]\n+ (let [{:keys [out exit]} (p/shell {:out :string :err :string :continue true}\n+ sorterc-bin \"compile\" fixture-path)]\n+ (when-not (zero? exit)\n+ (throw (ex-info \"sorterc exit nonzero\" {:fixture fixture-path :out out})))\n+ (json/parse-string out true)))\n+\n+(defn first-component-ranking [result]\n+ (-> result :rankings first :components first :ranking))\n+\n+(defn item-leaf [item]\n+ (last (clojure.string/split item #\"/\")))\n+\n+(deftest chain-ranks-head-first\n+ (let [ranking (first-component-ranking (compile-sorter \"test/fixtures/ranking/chain.sorter\"))\n+ names (mapv (comp item-leaf :item) ranking)]\n+ (is (= [\"a\" \"b\" \"c\"] names)\n+ \"chain a>b>c should rank a, b, c in order\")\n+ (is (apply > (map :score ranking))\n+ \"scores should attenuate strictly down the chain\")))\n+\n+(deftest inverse-star-puts-winners-on-top\n+ (let [ranking (first-component-ranking (compile-sorter \"test/fixtures/ranking/star_inverse.sorter\"))\n+ names (mapv (comp item-leaf :item) ranking)]\n+ (is (= \"win\" (last names))\n+ \"the item that lost to both others should be ranked last\")))\n+\n+(deftest cycle-produces-uniform-scores\n+ (let [ranking (first-component-ranking (compile-sorter \"test/fixtures/ranking/cycle.sorter\"))\n+ scores (map :score ranking)]\n+ (is (every? #(< (Math/abs (- % 1/3)) 1e-3) scores)\n+ \"a perfectly symmetric 3-cycle should give every node ~1/3\")))\n+\n+(deftest star-topology-winner-at-top\n+ ;; Issue #146 regression: source-only star at default `>` ratio (2:1).\n+ ;; Pre-fix produced uniform 1/3 scores; alphabetical fallback put the\n+ ;; unambiguous winner at the bottom. Post-fix the chain is aperiodic and\n+ ;; converges to π_zebra = 1/2, π_alpha = π_beta = 1/4.\n+ (let [ranking (first-component-ranking (compile-sorter \"test/fixtures/ranking/star.sorter\"))\n+ names (mapv (comp item-leaf :item) ranking)\n+ by-name (into {} (map (juxt (comp item-leaf :item) :score) ranking))]\n+ (is (= \"zebra\" (first names))\n+ \"zebra won both votes and should rank #1\")\n+ (is (< (Math/abs (- (by-name \"zebra\") 0.5)) 1e-3))\n+ (is (< (Math/abs (- (by-name \"alpha\") 0.25)) 1e-3))\n+ (is (< (Math/abs (- (by-name \"beta\") 0.25)) 1e-3))))\n","role":"user"}],"model":"~x-ai/grok-latest"}