constitution · epochs · watch · epoch 3

comparison

c_b8e80699547c (tommy-mor) vs c_c534b41e8607 (tommy-mor)

download prompt · raw event · cmp_a465ff042248f6

council reasoning

~anthropic/claude-sonnet-latest · winner A · 3:1 · permalink

Side A adds a substantial, functional Reddit API client module with rate limiting, typed response structures, and error handling—real, reusable infrastructure. Side B merely deletes a dead/broken test function to silence a warning, a trivial cleanup with negligible lasting value compared to A's new functionality.

~x-ai/grok-latest · winner A · 15:1 · permalink

Side A adds a full RedditClient module (~420 lines) with rate limiting, multiple API endpoints, typed post/comment models, custom deserializers, and helper extractors—substantive lasting feature code. Side B only deletes one unused, non-#[test] helper that triggered a warning, a trivial cleanup with negligible project impact.

openai/gpt-chat-latest · winner A · 50:1 · permalink

Side A adds a complete Reddit API client module with rate limiting, HTTP request handling, structured serde models for Reddit responses, custom deserializers, and helper methods for working with posts and comments, providing significant new functionality. Side B only removes an unused test function that was generating a warning, which is a minor cleanup with little lasting impact on the project's capabilities.

sides

A — c_b8e80699547c (tommy-mor)

message

[432e1450] nice

diff preview

diff --git a/reddit.rs b/reddit.rs
new file mode 100644
index 0000000000000000000000000000000000000000..da27f6f76482dd27d44cad9d7b844972bf042e64
--- /dev/null
+++ b/reddit.rs
@@ -0,0 +1,420 @@
+use governor::{Quota, RateLimiter, Jitter};
+use nonzero_ext::nonzero;
+use reqwest::{Client, StatusCode};
+use serde::{Deserialize, Serialize};
+use std::sync::Arc;
+use std::time::Duration;
+use anyhow::{Result, Context, anyhow};
+
+/// Reddit API client with built-in rate limiting
+pub struct RedditClient {
+    client: Client,
+    limiter: Arc<governor::DefaultDirectRateLimiter>,
+    user_agent: String,
+}
+
+impl RedditClient {
+    /// Create a new Reddit client with rate limiting
+    /// Reddit API allows 60 requests per minute for OAuth2 authenticated apps
+    /// We'll be conservative and use 50 requests per minute
+    pub fn new() -> Self {
+        // Create rate limiter: 50 requests per minute
+        let quota = Quota::per_minute(nonzero!(50u32));
+        let limiter = Arc::new(RateLimiter::direct(quota));
+        
+        // Create HTTP client with timeout
+        let client = Client::builder()
+            .timeout(Duration::from_secs(30))
+            .build()
+            .expect("Failed to create HTTP client");
+        
+        // Reddit requires a unique user agent
+        let user_agent = format!(
+            "rust:sorter:v{} (by /u/hourLong_arnould)",
+            env!("CARGO_PKG_VERSION")
+        );
+        
+        Self {
+            client,
+            limiter,
+            user_agent,
+        }
+    }
+    
+    /// Fetch hot posts from a subreddit
+    pub async fn get_subreddit_hot(&self, subreddit: &str, limit: usize) -> Result<RedditListingResponse> {
+        // Wait for rate limiter
+        self.limiter.until_ready_with_jitter(Jitter::up_to(Duration::from_millis(100))).await;
+        
+        let url = format!("https://www.reddit.com/r/{}/hot.json?limit={}", subreddit, limit);
+        
+        let response = self.client
+            .get(&url)
+            .header("User-Agent", &self.user_agent)
+            .send()
+            .await
+            .context("Failed to send request to Reddit")?;
+        
+        self.handle_response(response).await
+    }
+    
+    /// Fetch top posts from a subreddit
+    pub async fn get_subreddit_top(&self, subreddit: &str, limit: usize, time_period: &str) -> Result<RedditListingResponse> {
+        self.limiter.until_ready_with_jitter(Jitter::up_to(Duration::from_millis(100))).await;
+        
+        let url = format!(
+            "https://www.reddit.com/r/{}/top.json?limit={}&t={}",
+            subreddit, limit, time_period
+        );
+        
+        let response = self.client
+            .get(&url)
+            .header("User-Agent", &self.user_agent)
+            .send()
+            .await
+            .context("Failed to send request to Reddit")?;
+        
+        self.handle_response(response).await
+    }
+    
+    /// Fetch new posts from a subreddit
+    pub async fn get_subreddit_new(&self, subreddit: &str, limit: usize) -> Result<RedditListingResponse> {
+        self.limiter.until_ready_with_jitter(Jitter::up_to(Duration::from_millis(100))).await;
+        
+        let url = format!("https://www.reddit.com/r/{}/new.json?limit={}", subreddit, limit);
+        
+        let response = self.client
+            .get(&url)
+            .header("User-Agent", &self.user_agent)
+            .send()
+            .await
+            .context("Failed to send request to Reddit")?;
+        
+        self.handle_response(response).await
+    }
+    
+    /// Fetch a specific post and its comments
+    pub async fn get_post(&self, subreddit: &str, post_id: &str) -> Result<Vec<RedditListingResponse>> {
+        self.limiter.until_ready_with_jitter(Jitter::up_to(Duration::from_millis(100))).await;
+        
+        let url = format!(
+            "https://www.reddit.com/r/{}/comments/{}.json",
+            subreddit, post_id
+        );
+        
+        let response = self.client
+            .get(&url)
+            .header("User-Agent", &self.user_agent)
+            .send()
+            .await
+            .context("Failed to send request to Reddit")?;
+        
+        match response.status() {
+            StatusCode::OK => {
+                let listings: Vec<RedditListingResponse> = response
+                    .json()
+                    .await
+                    .context("Failed to parse Reddit response")?;
+                Ok(listings)
+            }
+            StatusCode::TOO_MANY_REQUESTS => {
+                Err(anyhow!("Reddit rate limit exceeded. Please try again later."))
+            }
+            StatusCode::NOT_FOUND => {
+                Err(anyhow!("Post not found: r/{}/comments/{}", subreddit, post_id))
+            }
+            status => {
+                Err(anyhow!("Reddit API error: {}", status))
+            }
+        }
+    }
+    
+    /// Fetch user profile (posts and comments)
+    pub async fn get_user_profile(&self, username: &str, content_type: &str, limit: usize) -> Result<RedditListingResponse> {
+        self.limiter.until_ready_with_jitter(Jitter::up_to(Duration::from_millis(100))).await;
+        
+        let url = format!(
+            "https://www.reddit.com/user/{}/{}.json?limit={}",
+            username, content_type, limit
+        );
+        
+        let response = self.client
+            .get(&url)
+            .header("User-Agent", &self.user_agent)
+            .send()
+            .await
+            .context("Failed to send request to Reddit")?;
+        
+        self.handle_response(response).await
+    }
+    
+    /// Handle Reddit API response with proper error checking
+    async fn handle_response(&self, response: reqwest::Response) -> Result<RedditListingResponse> {
+        match response.status() {
+            StatusCode::OK => {
+                let listing: RedditListingResponse = response
+                    .json()
+                    .await
+                    .context("Failed to parse Reddit response")?;
+                Ok(listing)
+            }
+            StatusCode::TOO_MANY_REQUESTS => {
+                Err(anyhow!("Reddit rate limit exceeded. Please try again later."))
+            }
+            StatusCode::NOT_FOUND => {
+                Err(anyhow!("Reddit resource not found"))
+            }
+            StatusCode::FORBIDDEN => {
+                Err(anyhow!("Access forbidden. The subreddit may be private."))
+            }
+            status => {
+                Err(anyhow!("Reddit API error: {}", status))
+            }
+        }
+    }
+}
+
+// Reddit API response types
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct RedditListingResponse {
+    pub kind: String,
+    pub data: RedditListingData,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct RedditListingData {
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub after: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub before: Option<String>,
+    pub children: Vec<RedditThing>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub modhash: Option<String>,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct RedditThing {
+    pub kind: String,
+    pub data: RedditThingData,
+}
+
+/// Reddit's "Thing" data can be either a post, comment, or other types
+/// We use untagged enum to handle different data structures
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(untagged)]
+pub enum RedditThingData {
+    Post(RedditPost),
+    Comment(RedditComment),
+    // For things we don't care about yet (like "more" comments)
+    Other(serde_json::Value),
+}
+
+/// Reddit post data with serde handling all the parsing
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct RedditPost {
+    pub id: String,
+    pub name: String, // Full name (t3_xxx)
+    #[serde(default = "default_untitled")]
+    pub title: String,
+    #[serde(default = "default_deleted_user")]
+    pub author: String,
+    pub subreddit: String,
+    #[serde(default)]
+    pub url: String,
+    #[serde(default)]
+    pub permalink: String,
+    #[serde(default, deserialize_with = "deserialize_selftext")]
+    pub selftext: Option<String>,
+    #[serde(default)]
+    pub score: i64,
+    #[serde(default)]
+    pub num_comments: i64,
+    #[serde(default)]
+    pub created_utc: f64,
+    #[serde(default, deserialize_with = "deserialize_thumbnail")]
+    pub thumbnail: Option<String>,
+    #[serde(default)]
+    pub is_video: bool,
+    #[serde(default)]
+    pub is_self: bool,
+    // Additional useful fields
+    #[serde(default)]
+    pub ups: i64,
+    #[serde(default)]
+    pub downs: i64,
+    #[serde(default)]
+    pub upvote_ratio: f64,
+    #[serde(default)]
+    pub over_18: bool,
+    #[serde(default)]
+    pub spoiler: bool,
+    #[serde(default)]
+    pub stickied: bool,
+    #[serde(default)]
+    pub locked: bool,
+    #[serde(default)]
+    pub distinguished: Option<String>,
+}
+
+/// Reddit comment data
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct RedditComment {
+    pub id: String,
+    pub name: String, // Full name (t1_xxx)
+    #[serde(default = "default_deleted_user")]
+    pub author: String,
+    #[serde(default)]
+    pub body: String,
+    #[serde(default)]
+    pub score: i64,
+    #[serde(default)]
+    pub created_utc: f64,
+    #[serde(default)]
+    pub parent_id: String,
+    #[serde(default)]
+    pub permalink: String,
+    #[serde(default)]
+    pub depth: i32,
+    // Additional useful fields
+    #[serde(default)]
+    pub ups: i64,
+    #[serde(default)]
+    pub downs: i64,
+    #[serde(default)]
+    pub edited: RedditEditStatus,
+    #[serde(default)]
+    pub stickied: bool,
+    #[serde(default)]
+    pub distinguished: Option<String>,
+    #[serde(default)]
+    pub is_submitter: bool,
+    #[serde(default)]
+    pub collapsed: bool,
+    #[serde(default)]
+    pub controversiality: i32,
+}
+
+/// Reddit's edit status - can be false or a timestamp
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(untagged)]
+pub enum RedditEditStatus {
+    NotEdited(bool),
+    EditedAt(f64),
+}
+
+impl Default for RedditEditStatus {
+    fn default() -> Self {
+        RedditEditStatus::NotEdited(false)
+    }
+}
+
+// Helper functions for serde defaults
+fn default_untitled() -> String {
+    "Untitled".to_string()
+}
+
+fn default_deleted_user() -> String {
+    "[deleted]".to_string()
+}
+
+// Custom deserializer for selftext (empty strings should be None)
+fn deserialize_selftext<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
+where
+    D: serde::Deserializer<'de>,
+{
+    let s: Option<String> = Option::deserialize(deserializer)?;
+    Ok(s.filter(|text| !text.is_empty()))
+}
+
+// Custom deserializer for thumbnail (filter out "self", "default", empty)
+fn deserialize_thumbnail<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
+where
+    D: serde::Deserializer<'de>,
+{
+    let s: Option<String> = Option::deserialize(deserializer)?;
+    Ok(s.filter(|thumb| {
+        !thumb.is_empty() && thumb != "self" && thumb != "default" && thumb != "nsfw" && thumb != "spoiler"
+    }))
+}
+
+// Helper methods for extracting typed data from Things
+impl RedditThing {
+    /// Try to get this Thing as a Post
+    pub fn as_post(&self) -> Option<&RedditPost> {
+        if self.kind != "t3" {
+            return None;
+        }
+        match &self.data {
+            RedditThingData::Post(post) => Some(post),
+            _ => None,
+        }
+    }
+    
+    /// Try to get this Thing as a Comment
+    pub fn as_comment(&self) -> Option<&RedditComment> {
+        if self.kind != "t1" {
+            return None;
+        }
+        match &self.data {
+            RedditThingData::Comment(comment) => Some(comment),
+            _ => None,
+        }
+    }
+    
+    /// Check if th

… preview truncated; 1,666 characters omitted

download full diff A

B — c_c534b41e8607 (tommy-mor)

message

[2b970f92] delete random test that was a warning

diff preview

diff --git a/server/src/dsl.rs b/server/src/dsl.rs
index af6d3b52621e65288f75f757d54e5abeaa7501e8..c32ff6ccc13ff38ae7712af441bd8d9e8568cc3b 100644
--- a/server/src/dsl.rs
+++ b/server/src/dsl.rs
@@ -671,17 +671,6 @@ mod tests {
         assert!(err_msg.contains("missing vote explanation"), "error: {}", err_msg);
     }
 
-    fn parse_full_keeps_quoted_thread_title_statement_as_prose() {
-        let input = "\"This is a title\" { This is the body of the post }\n";
-        let doc = parse_full(input).unwrap();
-        assert_eq!(
-            doc.statements,
-            vec![Stmt::Prose {
-                text: "\"This is a title\" { This is the body of the post }\n".trim_end_matches('\n').to_string()
-            }]
-        );
-    }
-
     #[test]
     fn parse_full_keeps_regular_quoted_prose() {
         let input = "She said \"hello\" and left.";

download full diff B

Hardlinks — judgments / attempts / prompt

prompt download

judgments

attempts

Prompt text is loaded only by the download route.