constitution · epochs · watch · epoch 3

comparison

c_b3cb2762569d (tommy-mor) vs c_c25451965a7f (tommy-mor)

download prompt · raw event · cmp_ec6bb02f2b8bd1

council reasoning

~anthropic/claude-sonnet-latest · winner B · 65:35 · permalink

Side B is a focused, genuine bugfix: it replaces multiple hardcoded staging/production hostnames with a single reusable `get-base-url` util (reducing duplication) and fixes real broken redirects for custom hosts plus corrects the CI deploy trigger to the staging branch. Side A, while large, is mostly a mechanical move of legacy/dead code (`legacy/parser.rs` → `server/src/parser.rs`) with type renames (UIAction→ParserAction) and duplicated test bodies verbatim, which inflates line count without adding much genuinely new, verified logic beyond wiring speculative UI hooks.

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

Commit A integrates a full Reddit path parser (graph, actions, render, UI morphing, debounced input/Tab completion, and tests) into the live server, turning legacy code into a working navigate panel; B only centralizes base-URL logic via HOSTNAME/origin and switches the Fly workflow to staging. The feature wiring and product surface in A outweigh B’s targeted ops/config fixes.

openai/gpt-chat-latest · winner A · 9:2 · permalink

Side A adds a substantial new capability by integrating a Reddit path parser into the server and UI: it introduces parser modules, rendering, request handling, client-side interactions for live parsing/tab completion, and corresponding integration tests. Although much of the parser code is moved from a legacy location into the server, the commit also wires it into the application and exposes new functionality, whereas Side B mainly centralizes base-URL generation via a shared utility, replaces hardcoded redirect hosts with HOSTNAME/window.location.origin, and adjusts the deployment workflow to deploy from the staging branch.

sides

A — c_b3cb2762569d (tommy-mor)

message

[604a14ad] nice

diff preview

diff --git a/.gitignore b/.gitignore
index 16de5edb7185b04ef5bc64512814d7dfe2c1f50c..73e8f22cf0d39c706e7cdce5e39f1903a0f9181b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,5 @@
 /.lsp/
 *.swp
 .DS_Store
+data/
+repomix-output.xml
diff --git a/legacy/parser.rs b/legacy/parser.rs
deleted file mode 100644
index 2b87b974f8d1dd93bee35681d87668a94e4ef349..0000000000000000000000000000000000000000
--- a/legacy/parser.rs
+++ /dev/null
@@ -1,1808 +0,0 @@
-use std::collections::HashMap;
-use std::rc::Rc;
-use std::cell::RefCell;
-use crate::ui::action::UIAction;
-use crate::ui::types::{Suggestion, GuideOption, ScrollingSuggestion};
-
-// --- Core Abstractions ---
-
-/// Unique identifier for nodes in the graph
-type NodeId = &'static str;
-
-/// Pattern matching for edges
-#[derive(Debug, Clone)]
-pub enum EdgePattern {
-    /// Matches exact literal string
-    Literal(&'static str),
-    
-    /// Matches any prefix of a string and suggests the full string
-    /// e.g., PrefixOf("reddit.com") matches "r", "re", "red", "reddit", "reddit.com"
-    PrefixOf(&'static str),
-    
-    /// Captures a variable segment (e.g., subreddit name, username)
-    Variable(&'static str),
-    
-    /// Matches any string (wildcard)
-    Any,
-}
-
-impl EdgePattern {
-    /// Try to match this pattern against input, return (consumed_chars, captured_value)
-    fn matches(&self, input: &str) -> Option<(usize, Option<String>)> {
-        match self {
-            EdgePattern::Literal(lit) => {
-                if input.starts_with(lit) {
-                    Some((lit.len(), None))
-                } else {
-                    None
-                }
-            }
-            EdgePattern::PrefixOf(target) => {
-                // Check if input is a prefix of target
-                if target.starts_with(input) && !input.is_empty() {
-                    // It's a valid prefix
-                    Some((input.len(), None))
-                } else if input.starts_with(target) {
-                    // Full match
-                    Some((target.len(), None))
-                } else {
-                    None
-                }
-            }
-            EdgePattern::Variable(var_name) => {
-                // Consume until next '/' or end of string
-                let end = input.find('/').unwrap_or(input.len());
-                if end > 0 {
-                    let captured = input[..end].to_string();
-                    // Validate based on variable type
-                    if is_valid_variable(var_name, &captured) {
-                        Some((end, Some(captured)))
-                    } else {
-                        None
-                    }
-                } else {
-                    None
-                }
-            }
-            EdgePattern::Any => {
-                // Match everything until next '/' or end
-                let end = input.find('/').unwrap_or(input.len());
-                if end > 0 {
-                    Some((end, Some(input[..end].to_string())))
-                } else {
-                    None
-                }
-            }
-        }
-    }
-    
-    /// Get the completion suggestion for this pattern
-    fn completion(&self, partial: &str) -> Option<String> {
-        match self {
-            EdgePattern::PrefixOf(target) => {
-                if target.starts_with(partial) && partial != *target {
-                    Some(target.to_string())
-                } else {
-                    None
-                }
-            }
-            _ => None,
-        }
-    }
-}
-
-/// Edge in the graph
-pub struct Edge {
-    pattern: EdgePattern,
-    target: NodeId,
-    /// Optional description for autocomplete
-    description: Option<&'static str>,
-}
-
-/// Handler function for generating UI actions
-type Handler = Box<dyn Fn(&str, &str, &HashMap<String, String>) -> UIAction>;
-
-/// Node in the graph
-pub struct Node {
-    #[allow(dead_code)]
-    id: NodeId,
-    edges: Vec<Edge>,
-    handler: Option<Handler>,
-}
-
-/// The composable parser graph
-pub struct Graph {
-    nodes: HashMap<NodeId, Rc<RefCell<Node>>>,
-    root: NodeId,
-}
-
-// --- Graph Builder (Fluent API) ---
-
-pub struct GraphBuilder {
-    nodes: HashMap<NodeId, Rc<RefCell<Node>>>,
-    current_node: Option<NodeId>,
-    root: NodeId,
-}
-
-impl GraphBuilder {
-    pub fn new() -> Self {
-        let mut nodes = HashMap::new();
-        let root_node = Rc::new(RefCell::new(Node {
-            id: "root",
-            edges: Vec::new(),
-            handler: None,
-        }));
-        nodes.insert("root", root_node);
-        
-        GraphBuilder {
-            nodes,
-            current_node: Some("root"),
-            root: "root",
-        }
-    }
-    
-    /// Select a node to add edges to
-    pub fn at(mut self, node_id: NodeId) -> Self {
-        // Create node if it doesn't exist
-        if !self.nodes.contains_key(node_id) {
-            let node = Rc::new(RefCell::new(Node {
-                id: node_id,
-                edges: Vec::new(),
-                handler: None,
-            }));
-            self.nodes.insert(node_id, node);
-        }
-        self.current_node = Some(node_id);
-        self
-    }
-    
-    /// Add an edge from the current node
-    pub fn edge(self, pattern: EdgePattern, target: NodeId) -> Self {
-        self.edge_with_desc(pattern, target, None)
-    }
-    
-    /// Add an edge with description
-    pub fn edge_with_desc(mut self, pattern: EdgePattern, target: NodeId, desc: Option<&'static str>) -> Self {
-        let current = self.current_node.expect("No current node selected");
-        
-        // Create target node if it doesn't exist
-        if !self.nodes.contains_key(target) {
-            let node = Rc::new(RefCell::new(Node {
-                id: target,
-                edges: Vec::new(),
-                handler: None,
-            }));
-            self.nodes.insert(target, node);
-        }
-        
-        // Add edge to current node
-        if let Some(node) = self.nodes.get(current) {
-            node.borrow_mut().edges.push(Edge {
-                pattern,
-                target,
-                description: desc,
-            });
-        }
-        
-        self
-    }
-    
-    /// Set handler for current node
-    pub fn handler<F>(self, handler: F) -> Self 
-    where 
-        F: Fn(&str, &str, &HashMap<String, String>) -> UIAction + 'static
-    {
-        let current = self.current_node.expect("No current node selected");
-        if let Some(node) = self.nodes.get(current) {
-            node.borrow_mut().handler = Some(Box::new(handler));
-        }
-        self
-    }
-    
-    /// Build the final graph
-    pub fn build(self) -> Graph {
-        Graph {
-            nodes: self.nodes,
-            root: self.root,
-        }
-    }
-}
-
-// --- Parser Implementation ---
-
-impl Graph {
-    pub fn parse(&self, input: &str) -> UIAction {
-        let normalized = input.trim().to_lowercase();
-        let mut state = ParserState {
-            input: &normalized,
-            cursor: 0,
-            current_node_id: self.root,
-            context: HashMap::new(),
-            original_query: input.to_string(),
-            current_prefix: String::new(),
-        };
-        
-        self.parse_recursive(&mut state)
-    }
-    
-    fn parse_recursive(&self, state: &mut ParserState) -> UIAction {
-        let node = self.nodes.get(state.current_node_id)
-            .expect("Node not found in graph");
-        let node_ref = node.borrow();
-        
-        // If we've consumed all input, check for handler or suggestions
-        if state.cursor >= state.input.len() {
-            if let Some(handler) = &node_ref.handler {
-                return handler(&state.original_query, &state.current_prefix, &state.context);
-            }
-            
-            // No handler, try to suggest based on available edges
-            return self.suggest_from_edges(&node_ref, state);
-        }
-        
-        let remaining = &state.input[state.cursor..];
-        
-        // Try to match each edge
-        for edge in &node_ref.edges {
-            if let Some((consumed, captured)) = edge.pattern.matches(remaining) {
-                // Save state for potential backtracking
-                let saved_cursor = state.cursor;
-                let saved_node = state.current_node_id;
-                let saved_prefix = state.current_prefix.clone();
-                
-                // Update state
-                state.cursor += consumed;
-                state.current_node_id = edge.target;
-                state.current_prefix.push_str(&remaining[..consumed]);
-                
-                // Store captured variable if any
-                if let Some(value) = captured {
-                    if let EdgePattern::Variable(var_name) = &edge.pattern {
-                        state.context.insert(var_name.to_string(), value);
-                    }
-                }
-                
-                // Check if this is a partial match that needs completion
-                if state.cursor == state.input.len() {
-                    if let Some(completion_suffix) = edge.pattern.completion(remaining) {
-                        // Use the current_prefix plus the completion suffix
-                        let full_completion = format!("{}{}", 
-                            state.current_prefix, 
-                            completion_suffix.strip_prefix(remaining).unwrap_or(&completion_suffix)
-                        );
-                        return UIAction::suggest(
-                            state.original_query.clone(),
-                            Some(Suggestion {
-                                text: full_completion.clone(),
-                                completion: full_completion,
-                                description: edge.description.map(|d| d.to_string()),
-                                score: 1.0,
-                            })
-                        );
-                    }
-                }
-                
-                // Continue parsing from the target node
-                let result = self.parse_recursive(state);
-                
-                // If we got a valid response, return it
-                if !matches!(result, UIAction::ShowError(_)) {
-                    return result;
-                }
-                
-                // Otherwise, restore state and try next edge
-                state.cursor = saved_cursor;
-                state.current_node_id = saved_node;
-                state.current_prefix = saved_prefix;
-            }
-        }
-        
-        // No edges matched - try to provide suggestions
-        self.suggest_from_edges(&node_ref, state)
-    }
-    
-    fn suggest_from_edges(&self, node: &Node, state: &ParserState) -> UIAction {
-        let remaining = &state.input[state.cursor..];
-        
-        // Find edges that could match with more input
-        for edge in &node.edges {
-            match &edge.pattern {
-                EdgePattern::PrefixOf(target) => {
-                    if target.starts_with(remaining) && !remaining.is_empty() {
-                        // Use current_prefix instead of rebuilding from input
-                        let full_completion = format!("{}{}", state.current_prefix, target);
-                        return UIAction::suggest(
-                            state.original_query.clone(),
-                            Some(Suggestion {
-                                text: full_completion.clone(),
-                                completion: full_completion,
-                                description: edge.description.map(|d| d.to_string()),
-                                score: 1.0,
-                            })
-                        );
-                    }
-                }
-                EdgePattern::Literal(lit) => {
-             

… preview truncated; 152,463 characters omitted

download full diff A

B — c_c25451965a7f (tommy-mor)

message

[6120bd96] fix redirect urls for custom hosts and deploy from staging

Use HOSTNAME and window.location.origin instead of hardcoded staging.sorter.social, and trigger fly deploys on pushes to staging.

Co-authored-by: Cursor <cursoragent@cursor.com>

diff preview

diff --git a/.github/workflows/fly-deploy.yml b/.github/workflows/fly-deploy.yml
index 3e693915fe6f99a5c2221b2921bf8ebc305180fc..5e11e5c312f62bed34d8cc68bd4a5538f52476b9 100644
--- a/.github/workflows/fly-deploy.yml
+++ b/.github/workflows/fly-deploy.yml
@@ -3,11 +3,11 @@ name: deploy to fly.io
 on:
   push:
     branches:
-      - main
+      - staging
   workflow_dispatch:
 
 concurrency:
-  group: fly-deploy-main
+  group: fly-deploy-staging
   cancel-in-progress: true
 
 jobs:
diff --git a/borter/src/app/linear.clj b/borter/src/app/linear.clj
index f77d8a974e0cf05f9c33233bb625bdb190d2007b..5ef0e34cf1d543826675c6facb66aec079b40561 100644
--- a/borter/src/app/linear.clj
+++ b/borter/src/app/linear.clj
@@ -6,6 +6,7 @@
             [ring.util.response :as response]
             [app.database :as db]
             [app.permissions :as perms]
+            [app.util :as util]
             [honey.sql.helpers :as h]
             [sluj.core :refer [sluj]]))
 
@@ -13,9 +14,7 @@
 (def client-secret (System/getenv "BORTER_LINEAR_CLIENT_SECRET"))
 
 (defn get-hostname []
-  (case (.getCanonicalHostName (java.net.InetAddress/getLocalHost))
-    "sorter.social" "https://sorter.social/api/linear/callback"
-    "http://localhost:3000/api/linear/callback"))
+  (str (util/get-base-url) "/api/linear/callback"))
 
 (defn code->token [code]
   (def code code)
diff --git a/borter/src/app/login.clj b/borter/src/app/login.clj
index 5d545db9bef7765ba8b3fe7d00261c8ce84e9e1a..86817f8e94bc174e5b108859cc0b342953ab7acb 100644
--- a/borter/src/app/login.clj
+++ b/borter/src/app/login.clj
@@ -1,6 +1,7 @@
 (ns app.login
   (:require [crypto.password.bcrypt :as password]
             [app.database :as db]
+            [app.util :as util]
             [honey.sql.helpers :as h]
             [hato.client :as hc]
             [clojure.data.json :as json]
@@ -12,10 +13,7 @@
 (def environment (or (System/getenv "ENVIRONMENT") "development"))
 
 (defn get-base-url []
-  (case environment
-    "production" "https://sorter.social"
-    "staging" "https://staging.sorter.social"
-    "development" "http://localhost:3000"))  ; fallback for development
+  (util/get-base-url))
 
 (defn create-email-content
   "Creates a standardized email structure with customizable content"
diff --git a/borter/src/app/oauth.clj b/borter/src/app/oauth.clj
index 1166f2ee30c9e813840711c72d1ad8f1c62e1ffe..2cd0c62f1fe267f7f5f725a97bc3ba0d0889517f 100644
--- a/borter/src/app/oauth.clj
+++ b/borter/src/app/oauth.clj
@@ -3,6 +3,7 @@
             [clojure.data.json :as json]
             [hato.client :as hc]
             [app.database :as db]
+            [app.util :as util]
             [honey.sql.helpers :as h]
             [ring.util.codec :as codec])
   (:import [java.util Base64]))
@@ -13,12 +14,7 @@
     encoded-bytes))
 
 (defn get-hostname []
-  (let [env (System/getenv "ENVIRONMENT")]
-    (println "Current environment:" env)
-    (case env
-      "production" "https://sorter.social"
-      "staging" "https://staging.sorter.social"
-      "http://localhost:3000")))
+  (util/get-base-url))
 
 (defn get-origin-hostname [req]
   (let [origin (get-in req [:headers "origin"])
diff --git a/borter/src/app/spotify.clj b/borter/src/app/spotify.clj
index 3e11713119141812fa2707ec956fb7ed612ee69a..b38d734351a1d4f1e142d93d4435ffe7bd20c02d 100644
--- a/borter/src/app/spotify.clj
+++ b/borter/src/app/spotify.clj
@@ -1,5 +1,6 @@
 (ns app.spotify
   (:require [app.oauth :as oauth]
+            [app.util :as util]
             [hato.client :as hc]
             [clojure.data.json :as json]
             [ring.util.codec :as codec]
@@ -47,10 +48,7 @@
 
 
 (defn get-hostname []
-  (case (System/getenv "ENVIRONMENT")
-    "production" "https://sorter.social"
-    "staging" "https://staging.sorter.social"
-    "http://localhost:3000"))
+  (util/get-base-url))
 
 (defn create-spotify-tag
   "Creates a tag for a Spotify entity (artist, album, track) if it doesn't exist"
diff --git a/borter/src/app/twitter.clj b/borter/src/app/twitter.clj
index ef7b18fa1fcb82bb944b3035ffed44499181237f..b9a3fdccc15d13918a4d11d8c0443de94fd172b4 100644
--- a/borter/src/app/twitter.clj
+++ b/borter/src/app/twitter.clj
@@ -1,6 +1,7 @@
 (ns app.twitter
   (:require [app.database :as db]
             [app.permissions :as perms]
+            [app.util :as util]
             [honey.sql.helpers :as h]
             [hato.client :as hc]
             [clojure.data.json :as json]
@@ -37,9 +38,7 @@
   (clojure.string/join "&" (map (fn [[k v]] (str (name k) "=" v)) params)))
 
 (defn get-hostname []
-  (case (.getCanonicalHostName (java.net.InetAddress/getLocalHost))
-    "sorter.isnt.online" "https://sorter.isnt.online/api/twitter/callback"
-    "http://localhost:3000/api/twitter/callback"))
+  (str (util/get-base-url) "/api/twitter/callback"))
 
 (defn encode-b64 [s]
   (.encodeToString (java.util.Base64/getEncoder) (.getBytes s)))
diff --git a/borter/src/app/util.clj b/borter/src/app/util.clj
index 384a64306c84aa8288ec44cd5de57edc248a826d..6a8aa0875accb28dc81bf57e645e3961b0a3ace5 100644
--- a/borter/src/app/util.clj
+++ b/borter/src/app/util.clj
@@ -2,6 +2,18 @@
   (:require [clojure.string :as string])
   (:import [java.net URLEncoder]))
 
+(defn get-base-url
+  "Public site base URL for redirects and oauth callbacks."
+  []
+  (if-let [hostname (not-empty (System/getenv "HOSTNAME"))]
+    (if (string/starts-with? hostname "http")
+      (string/replace hostname #"/$" "")
+      (str "https://" (string/replace hostname #"/$" "")))
+    (case (or (System/getenv "ENVIRONMENT") "development")
+      "production" "https://sorter.social"
+      "staging" "https://staging.sorter.social"
+      "http://localhost:3000")))
+
 (defn urlencode-params [params]
   (clojure.string/join "&" (map (fn [[k v]] (str k "=" (URLEncoder/encode (str v) "UTF-8"))) params)))
 
diff --git a/borter/src/app/youtube.clj b/borter/src/app/youtube.clj
index 647ef7c6d4f2503a63a7c074d46dc815b2a23547..fc9a2f5ed516692f19e06b4c0221f510547cf8c0 100644
--- a/borter/src/app/youtube.clj
+++ b/borter/src/app/youtube.clj
@@ -6,6 +6,7 @@
             [ring.util.response :as response]
             [app.database :as db]
             [app.permissions :as perms]
+            [app.util :as util]
             [honey.sql.helpers :as h]
             [sluj.core :refer [sluj]]))
 
@@ -32,9 +33,7 @@
       :body (json/read-str {:key-fn keyword})))
 
 (defn get-hostname []
-  (case (.getCanonicalHostName (java.net.InetAddress/getLocalHost))
-    "localhost" "http://localhost:3000/api/youtube/callback"
-    "https://sorter.isnt.online/api/youtube/callback"))
+  (str (util/get-base-url) "/api/youtube/callback"))
 
 
 
diff --git a/forter/src/utils/authUtils.js b/forter/src/utils/authUtils.js
index 145e5db7ad6f7a6439d68c63beb4d07d31b2de08..98ed7fa671f9887f44dc1479d85569951eb4773e 100644
--- a/forter/src/utils/authUtils.js
+++ b/forter/src/utils/authUtils.js
@@ -5,8 +5,11 @@ import { current_session, fetchSession } from "../session";
 let lastSyncTime = 0;
 const SYNC_THROTTLE_MS = 5000; // Only sync once every 5 seconds
 
-// Get base URL based on Vite's mode
+// Get base URL based on current origin, with build-mode fallbacks for SSR/build
 const getBaseUrl = () => {
+  if (typeof window !== 'undefined' && window.location?.origin) {
+    return window.location.origin;
+  }
   switch (import.meta.env.MODE) {
     case 'production':
       return 'https://sorter.social';

download full diff B

Hardlinks — judgments / attempts / prompt

prompt download

judgments

attempts

Prompt text is loaded only by the download route.