constitution · epochs · watch · epoch 3

comparison

c_f515f8a12d7a (tommy-mor) vs c_b3cb2762569d (tommy-mor)

download prompt · raw event · cmp_43f6517e6e3647

council reasoning

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

Side A is a small, focused fix that removes duplicate/dead UI code (the toolbar + ExpandNewThreadForm action) and unifies the new-thread compose flow, directly simplifying the codebase with tests updated accordingly. Side B is a large, unfocused commit ('nice') that deletes a legacy parser file while duplicating nearly the same 1800-line reddit-URL parser under new module names (parser.rs, parser_action.rs) and bolts on an unrelated, seemingly experimental 'parser-panel' navigation demo feature, adding bulk and complexity without clear lasting value or coherent purpose.

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

A is a precise design cleanup: SSR the home compose slot like rooms, delete the toolbar/+ dual path, and remove ExpandNewThreadForm plus its handler/tests—less surface and one consistent flow. B mostly relocates ~1.8k lines of legacy parser (UIAction→ParserAction rename), plus .gitignore noise; the real new value is wiring (ParseQuery, parser_render, JS tab/input), which matters but is thinner relative to the churn.

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

Side A makes a focused architectural simplification: it removes the now-redundant `ExpandNewThreadForm` UI action and server handler, and changes the home page to server-render `#new-thread-ui-slot` directly with the same compose/login state used on room pages. This eliminates duplicate interaction flow and dead code, whereas Side B is dominated by importing a very large parser subsystem and deleting a legacy file, adding substantial complexity whose lasting value is less certain despite the new demo UI and tests.

sides

A — c_f515f8a12d7a (tommy-mor)

message

[601d3a05] fix(html): drop home toolbar + and ExpandNewThreadForm (single + flow)

Public home now SSRs #new-thread-ui-slot like room pages: collapsed compose
for signed-in users, login hint when logged out. Removes the extra toolbar
that morphed the same collapsed state and the expand_new_thread_form action.

Made-with: Cursor

diff preview

diff --git a/server/src/api/ui_html.rs b/server/src/api/ui_html.rs
index e979053ff1ba8c0e2bf55add0a32b7de11cf1e56..f3ce5cb2ab2f923440a8479d0f1fb4acbba166ca 100644
--- a/server/src/api/ui_html.rs
+++ b/server/src/api/ui_html.rs
@@ -139,51 +139,6 @@ async fn dispatch_ui_action(
                 }
             }
         }
-        HtmlUiAction::ExpandNewThreadForm { room_wire } => {
-            let room_wire = room_wire.trim().to_string();
-            if room_wire.is_empty() {
-                return ui_js_warn("missing room").into_response();
-            }
-            if room_wire == "public" {
-                let reduced = state.reduced.read().await;
-                let user = session.map(|s| s.username.as_str());
-                drop(reduced);
-                let markup = if user.is_some() {
-                    fragment_new_thread_slot(&ThreadNav::public(), true, false)
-                } else {
-                    login_to_post_hint_markup()
-                };
-                return JsBuilder::new()
-                    .morph_inner_selector("#new-thread-ui-slot", markup)
-                    .into_response();
-            }
-            let reduced = state.reduced.read().await;
-            let user = session.map(|s| s.username.as_str());
-            if !reduced.rooms.contains(&room_wire) {
-                drop(reduced);
-                return ui_js_warn("room not found").into_response();
-            }
-            if !user_can_view_room(&reduced, &room_wire, user) {
-                drop(reduced);
-                return ui_js_warn("forbidden").into_response();
-            }
-            let can_post = session
-                .as_ref()
-                .map(|s| user_can_post_room(&reduced, &room_wire, &s.username))
-                .unwrap_or(false);
-            drop(reduced);
-            let Some(nav) = ThreadNav::from_room_id(&room_wire) else {
-                return ui_js_warn("bad room").into_response();
-            };
-            let markup = if can_post {
-                fragment_new_thread_slot(&nav, true, false)
-            } else {
-                login_to_post_hint_markup()
-            };
-            JsBuilder::new()
-                .morph_inner_selector("#new-thread-ui-slot", markup)
-                .into_response()
-        }
         HtmlUiAction::SetRoomMembersExpanded { room_wire, expanded } => {
             let room_wire = room_wire.trim().to_string();
             if room_wire.is_empty() {
diff --git a/server/src/html/forum/feed.rs b/server/src/html/forum/feed.rs
index 1b4ae7baa3ad4757b74172d7b67f3f2b33d1075d..945bdd6c48bc164e2cf91fd0996c4321b75f5abf 100644
--- a/server/src/html/forum/feed.rs
+++ b/server/src/html/forum/feed.rs
@@ -14,6 +14,7 @@ use crate::timeago;
 
 use super::ingest::ingest_entry_markup;
 use super::nav::ThreadNav;
+use super::new_thread::{fragment_new_thread_slot, login_to_post_hint_markup};
 use super::page::auth_strip;
 use super::paginator::{render_thread_paginator, PAGE_SIZE};
 use crate::html::{
@@ -217,9 +218,6 @@ pub async fn home(
     let strip = auth_strip(&headers, &jar, &reduced_read);
     drop(reduced_read);
 
-    use crate::html::ui_action::{HtmlUiAction, UI_RPC_FIELD};
-    use crate::form_template::template_json_compact;
-
     let page = layout(
         "slug.social",
         "view-thread",
@@ -243,15 +241,13 @@ pub async fn home(
                 }
             }
             p class="muted" { "dark = time-ordered · light = vote-ranked" }
-            div class="thread-feed-toolbar" {
-                form method="POST" action="/ui" {
-                    input type="hidden" name=(UI_RPC_FIELD) value=(template_json_compact(&HtmlUiAction::ExpandNewThreadForm {
-                        room_wire: "public".into(),
-                    }).expect("static json"));
-                    button type="submit" class="section-add-btn" { "+" }
+            div id="new-thread-ui-slot" {
+                @if user.is_some() {
+                    (fragment_new_thread_slot(&nav, true, false))
+                } @else {
+                    (login_to_post_hint_markup())
                 }
             }
-            div id="new-thread-ui-slot" {}
             (render_thread_feed(Some(&nav), "thread-feed", &public_rows, now))
             (cli_panel(&["npx slugsocial public forum list"]))
         },
diff --git a/server/src/html/ui_action.rs b/server/src/html/ui_action.rs
index 5031ebfeb928f28e471f23210b8c644654adb7c7..da0c9b3541e8e4988a78768cddef22324755c3f2 100644
--- a/server/src/html/ui_action.rs
+++ b/server/src/html/ui_action.rs
@@ -38,11 +38,6 @@ pub enum HtmlUiAction {
     RedactPost {
         post_id: String,
     },
-    /// Morph `#new-thread-ui-slot` inner to the collapsed compose toggle (or login hint).
-    /// Use `room_wire: "public"` for the public forum home; otherwise a private room id (`short/slug`).
-    ExpandNewThreadForm {
-        room_wire: String,
-    },
     /// Morph `#room-members-section` — members list open or collapsed (server-rendered).
     SetRoomMembersExpanded {
         room_wire: String,
@@ -131,26 +126,6 @@ mod tests {
         );
     }
 
-    #[test]
-    fn expand_new_thread_form_public() {
-        let template = serde_json::json!({
-            "action": "expand_new_thread_form",
-            "room_wire": "public",
-        });
-        let mut form = HashMap::new();
-        form.insert(
-            UI_RPC_FIELD.to_string(),
-            serde_json::to_string(&template).unwrap(),
-        );
-        let a = parse_html_ui_from_form(&form).unwrap();
-        assert_eq!(
-            a,
-            HtmlUiAction::ExpandNewThreadForm {
-                room_wire: "public".into(),
-            }
-        );
-    }
-
     #[test]
     fn expand_post_full_round_trip() {
         let template = serde_json::json!({

download full diff A

B — 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 B

Hardlinks — judgments / attempts / prompt

prompt download

judgments

attempts

Prompt text is loaded only by the download route.