{"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[d61c0469] ?\n\nSide A — unified diff (full patch):\ndiff --git a/server/static/sorter.css b/server/static/sorter.css\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..f5fb317a2c2a86d3dd00a639fff017fa3b8cc354\n--- /dev/null\n+++ b/server/static/sorter.css\n@@ -0,0 +1,168 @@\n+:root {\n+ --bg: #0f1115;\n+ --fg: #e8eaed;\n+ --muted: #9aa0a6;\n+ --accent: #7cacf8;\n+ --panel: #1a1d24;\n+ --border: #2a2f3a;\n+}\n+\n+* {\n+ box-sizing: border-box;\n+}\n+\n+body {\n+ margin: 0;\n+ font-family: system-ui, -apple-system, sans-serif;\n+ background: var(--bg);\n+ color: var(--fg);\n+ line-height: 1.5;\n+}\n+\n+.muted {\n+ color: var(--muted);\n+}\n+\n+.small {\n+ font-size: 0.875rem;\n+}\n+\n+.view-meta {\n+ position: fixed;\n+ top: 0.5rem;\n+ right: 0.5rem;\n+ font-size: 0.75rem;\n+}\n+\n+.demo-panel {\n+ max-width: 40rem;\n+ margin: 4rem auto 2rem;\n+ padding: 2rem;\n+ background: var(--panel);\n+ border: 1px solid var(--border);\n+ border-radius: 8px;\n+}\n+\n+.demo-panel h1 {\n+ margin-top: 0;\n+}\n+\n+.btn-primary {\n+ background: var(--accent);\n+ color: #0f1115;\n+ border: none;\n+ padding: 0.5rem 1rem;\n+ border-radius: 4px;\n+ font-size: 1rem;\n+ cursor: pointer;\n+}\n+\n+.btn-primary:hover {\n+ filter: brightness(1.1);\n+}\n+\n+code {\n+ font-size: 0.85em;\n+ background: var(--bg);\n+ padding: 0.1em 0.35em;\n+ border-radius: 3px;\n+}\n+\n+.vote-fields {\n+ display: flex;\n+ flex-direction: column;\n+ gap: 0.75rem;\n+ margin-bottom: 1rem;\n+}\n+\n+.vote-fields label {\n+ display: flex;\n+ flex-direction: column;\n+ gap: 0.25rem;\n+}\n+\n+.vote-fields input {\n+ padding: 0.4rem 0.5rem;\n+ border: 1px solid var(--border);\n+ border-radius: 4px;\n+ background: var(--bg);\n+ color: var(--fg);\n+}\n+\n+.rank-list {\n+ margin: 0;\n+ padding-left: 0;\n+ list-style: none;\n+}\n+\n+.rank-list li {\n+ margin-bottom: 0.35rem;\n+}\n+\n+.rank-num {\n+ color: var(--muted);\n+}\n+\n+#parser-input {\n+ width: 100%;\n+ padding: 0.5rem;\n+ border: 1px solid var(--border);\n+ border-radius: 4px;\n+ background: var(--bg);\n+ color: var(--fg);\n+ font-size: 1rem;\n+ font-family: inherit;\n+ resize: vertical;\n+}\n+\n+#parser-form .btn-primary {\n+ margin-top: 0.5rem;\n+}\n+\n+.breadcrumbs {\n+ font-size: 0.875rem;\n+ margin-bottom: 1rem;\n+ color: var(--muted);\n+}\n+\n+.breadcrumbs a {\n+ color: var(--accent);\n+ text-decoration: none;\n+}\n+\n+.breadcrumbs a:hover {\n+ text-decoration: underline;\n+}\n+\n+.breadcrumbs .separator {\n+ color: var(--muted);\n+}\n+\n+.rank-list a {\n+ color: var(--accent);\n+ text-decoration: none;\n+}\n+\n+.rank-list a:hover {\n+ text-decoration: underline;\n+}\n+\n+.entity-card h2 {\n+ margin-top: 0;\n+}\n+\n+.scope-name {\n+ color: var(--accent);\n+ font-weight: 600;\n+}\n+\n+.rank-heading {\n+ margin: 0.75rem 0 0.25rem;\n+ text-transform: uppercase;\n+ letter-spacing: 0.04em;\n+}\n+\n+.rank-gap {\n+ text-align: center;\n+ margin: 0.25rem 0;\n+}\n\n\nSide B — contributor: tommy-mor\nSide B — commit message:\n[d63870bc] fix\n\nSide B — unified diff (full patch):\ndiff --git a/server/src/parser.rs b/server/src/parser.rs\nindex 4bf201008857e3082b70a55e3dcbb2502ab3ed22..ea437c4434045b44cba04a2b3416df850db518e7 100644\n--- a/server/src/parser.rs\n+++ b/server/src/parser.rs\n@@ -1,6 +1,6 @@\n use std::collections::HashMap;\n-use std::rc::Rc;\n-use std::cell::RefCell;\n+use std::sync::OnceLock;\n+\n use crate::parser_action::{GuideOption, ParserAction, ScrollingSuggestion, Suggestion};\n \n // --- Core Abstractions ---\n@@ -98,8 +98,8 @@ pub struct Edge {\n description: Option<&'static str>,\n }\n \n-/// Handler function for generating UI actions\n-type Handler = Box) -> ParserAction>;\n+/// Handler function for generating UI actions (Send + Sync so the graph can live in `OnceLock`).\n+type Handler = Box) -> ParserAction + Send + Sync>;\n \n /// Node in the graph\n pub struct Node {\n@@ -109,16 +109,16 @@ pub struct Node {\n handler: Option,\n }\n \n-/// The composable parser graph\n+/// The composable parser graph (immutable after `build`).\n pub struct Graph {\n- nodes: HashMap>>,\n+ nodes: HashMap,\n root: NodeId,\n }\n \n // --- Graph Builder (Fluent API) ---\n \n pub struct GraphBuilder {\n- nodes: HashMap>>,\n+ nodes: HashMap,\n current_node: Option,\n root: NodeId,\n }\n@@ -126,31 +126,29 @@ pub struct GraphBuilder {\n impl GraphBuilder {\n pub fn new() -> Self {\n let mut nodes = HashMap::new();\n- let root_node = Rc::new(RefCell::new(Node {\n- id: \"root\",\n- edges: Vec::new(),\n- handler: None,\n- }));\n- nodes.insert(\"root\", root_node);\n- \n+ nodes.insert(\n+ \"root\",\n+ Node {\n+ id: \"root\",\n+ edges: Vec::new(),\n+ handler: None,\n+ },\n+ );\n+\n GraphBuilder {\n nodes,\n current_node: Some(\"root\"),\n root: \"root\",\n }\n }\n- \n+\n /// Select a node to add edges to\n pub fn at(mut self, node_id: NodeId) -> Self {\n- // Create node if it doesn't exist\n- if !self.nodes.contains_key(node_id) {\n- let node = Rc::new(RefCell::new(Node {\n- id: node_id,\n- edges: Vec::new(),\n- handler: None,\n- }));\n- self.nodes.insert(node_id, node);\n- }\n+ self.nodes.entry(node_id).or_insert_with(|| Node {\n+ id: node_id,\n+ edges: Vec::new(),\n+ handler: None,\n+ });\n self.current_node = Some(node_id);\n self\n }\n@@ -161,39 +159,39 @@ impl GraphBuilder {\n }\n \n /// Add an edge with description\n- pub fn edge_with_desc(mut self, pattern: EdgePattern, target: NodeId, desc: Option<&'static str>) -> Self {\n+ pub fn edge_with_desc(\n+ mut self,\n+ pattern: EdgePattern,\n+ target: NodeId,\n+ desc: Option<&'static str>,\n+ ) -> Self {\n let current = self.current_node.expect(\"No current node selected\");\n- \n- // Create target node if it doesn't exist\n- if !self.nodes.contains_key(target) {\n- let node = Rc::new(RefCell::new(Node {\n- id: target,\n- edges: Vec::new(),\n- handler: None,\n- }));\n- self.nodes.insert(target, node);\n- }\n- \n- // Add edge to current node\n- if let Some(node) = self.nodes.get(current) {\n- node.borrow_mut().edges.push(Edge {\n+\n+ self.nodes.entry(target).or_insert_with(|| Node {\n+ id: target,\n+ edges: Vec::new(),\n+ handler: None,\n+ });\n+\n+ if let Some(node) = self.nodes.get_mut(current) {\n+ node.edges.push(Edge {\n pattern,\n target,\n description: desc,\n });\n }\n- \n+\n self\n }\n- \n+\n /// Set handler for current node\n- pub fn handler(self, handler: F) -> Self \n- where \n- F: Fn(&str, &str, &HashMap) -> ParserAction + 'static\n+ pub fn handler(mut self, handler: F) -> Self\n+ where\n+ F: Fn(&str, &str, &HashMap) -> ParserAction + Send + Sync + 'static,\n {\n let current = self.current_node.expect(\"No current node selected\");\n- if let Some(node) = self.nodes.get(current) {\n- node.borrow_mut().handler = Some(Box::new(handler));\n+ if let Some(node) = self.nodes.get_mut(current) {\n+ node.handler = Some(Box::new(handler));\n }\n self\n }\n@@ -225,24 +223,25 @@ impl Graph {\n }\n \n fn parse_recursive(&self, state: &mut ParserState) -> ParserAction {\n- let node = self.nodes.get(state.current_node_id)\n+ let node = self\n+ .nodes\n+ .get(state.current_node_id)\n .expect(\"Node not found in graph\");\n- let node_ref = node.borrow();\n- \n+\n // If we've consumed all input, check for handler or suggestions\n if state.cursor >= state.input.len() {\n- if let Some(handler) = &node_ref.handler {\n+ if let Some(handler) = &node.handler {\n return handler(&state.original_query, &state.current_prefix, &state.context);\n }\n- \n+\n // No handler, try to suggest based on available edges\n- return self.suggest_from_edges(&node_ref, state);\n+ return self.suggest_from_edges(node, state);\n }\n- \n+\n let remaining = &state.input[state.cursor..];\n- \n+\n // Try to match each edge\n- for edge in &node_ref.edges {\n+ for edge in &node.edges {\n if let Some((consumed, captured)) = edge.pattern.matches(remaining) {\n // Save state for potential backtracking\n let saved_cursor = state.cursor;\n@@ -297,7 +296,7 @@ impl Graph {\n }\n \n // No edges matched - try to provide suggestions\n- self.suggest_from_edges(&node_ref, state)\n+ self.suggest_from_edges(node, state)\n }\n \n fn suggest_from_edges(&self, node: &Node, state: &ParserState) -> ParserAction {\n@@ -692,10 +691,15 @@ pub fn build_reddit_graph() -> Graph {\n \n // --- Public API ---\n \n+static REDDIT_GRAPH: OnceLock = OnceLock::new();\n+\n+fn reddit_graph() -> &'static Graph {\n+ REDDIT_GRAPH.get_or_init(build_reddit_graph)\n+}\n+\n /// Parse a query string and return a UI action\n pub fn parse_reddit_url(query: &str) -> ParserAction {\n- let graph = build_reddit_graph();\n- graph.parse(query)\n+ reddit_graph().parse(query)\n }\n \n #[cfg(test)]\n","role":"user"}],"model":"~anthropic/claude-sonnet-latest"}