Side B makes a substantive engineering improvement: it removes unnecessary Rc<RefCell<>> indirection in favor of direct HashMap ownership, adds Send+Sync bounds, and caches the graph in a OnceLock instead of rebuilding it on every parse call, improving both performance and correctness. Side A is purely additive CSS styling with no logic changes, useful but lower-impact and more superficial than B's architectural fix to the parser's core data structures.
constitution · epochs · watch · epoch 3
c_b447fc224eaf (tommy-mor) vs c_257b57c092b1 (tommy-mor)
download prompt · raw event · cmp_66a73080b947b3
council reasoning
B refactors the core parser graph from Rc<RefCell<Node>> to owned Nodes with Send+Sync handlers and OnceLock caching, fixing rebuild-on-every-parse overhead and enabling safe sharing; A only adds a new static CSS file of presentational rules with no logic or bugfix.
Side B makes a substantive architectural improvement by replacing the parser graph's Rc<RefCell>-based mutable structure with an immutable HashMap, requiring handlers to be Send + Sync, and caching the graph in a OnceLock so it is built once instead of on every parse. Side A only adds a new CSS stylesheet with presentation rules, which improves appearance but does not change core behavior or fix functionality.
sides
A — c_b447fc224eaf (tommy-mor)
message
[d61c0469] ?
diff preview
diff --git a/server/static/sorter.css b/server/static/sorter.css
new file mode 100644
index 0000000000000000000000000000000000000000..f5fb317a2c2a86d3dd00a639fff017fa3b8cc354
--- /dev/null
+++ b/server/static/sorter.css
@@ -0,0 +1,168 @@
+:root {
+ --bg: #0f1115;
+ --fg: #e8eaed;
+ --muted: #9aa0a6;
+ --accent: #7cacf8;
+ --panel: #1a1d24;
+ --border: #2a2f3a;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+body {
+ margin: 0;
+ font-family: system-ui, -apple-system, sans-serif;
+ background: var(--bg);
+ color: var(--fg);
+ line-height: 1.5;
+}
+
+.muted {
+ color: var(--muted);
+}
+
+.small {
+ font-size: 0.875rem;
+}
+
+.view-meta {
+ position: fixed;
+ top: 0.5rem;
+ right: 0.5rem;
+ font-size: 0.75rem;
+}
+
+.demo-panel {
+ max-width: 40rem;
+ margin: 4rem auto 2rem;
+ padding: 2rem;
+ background: var(--panel);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+}
+
+.demo-panel h1 {
+ margin-top: 0;
+}
+
+.btn-primary {
+ background: var(--accent);
+ color: #0f1115;
+ border: none;
+ padding: 0.5rem 1rem;
+ border-radius: 4px;
+ font-size: 1rem;
+ cursor: pointer;
+}
+
+.btn-primary:hover {
+ filter: brightness(1.1);
+}
+
+code {
+ font-size: 0.85em;
+ background: var(--bg);
+ padding: 0.1em 0.35em;
+ border-radius: 3px;
+}
+
+.vote-fields {
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+ margin-bottom: 1rem;
+}
+
+.vote-fields label {
+ display: flex;
+ flex-direction: column;
+ gap: 0.25rem;
+}
+
+.vote-fields input {
+ padding: 0.4rem 0.5rem;
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ background: var(--bg);
+ color: var(--fg);
+}
+
+.rank-list {
+ margin: 0;
+ padding-left: 0;
+ list-style: none;
+}
+
+.rank-list li {
+ margin-bottom: 0.35rem;
+}
+
+.rank-num {
+ color: var(--muted);
+}
+
+#parser-input {
+ width: 100%;
+ padding: 0.5rem;
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ background: var(--bg);
+ color: var(--fg);
+ font-size: 1rem;
+ font-family: inherit;
+ resize: vertical;
+}
+
+#parser-form .btn-primary {
+ margin-top: 0.5rem;
+}
+
+.breadcrumbs {
+ font-size: 0.875rem;
+ margin-bottom: 1rem;
+ color: var(--muted);
+}
+
+.breadcrumbs a {
+ color: var(--accent);
+ text-decoration: none;
+}
+
+.breadcrumbs a:hover {
+ text-decoration: underline;
+}
+
+.breadcrumbs .separator {
+ color: var(--muted);
+}
+
+.rank-list a {
+ color: var(--accent);
+ text-decoration: none;
+}
+
+.rank-list a:hover {
+ text-decoration: underline;
+}
+
+.entity-card h2 {
+ margin-top: 0;
+}
+
+.scope-name {
+ color: var(--accent);
+ font-weight: 600;
+}
+
+.rank-heading {
+ margin: 0.75rem 0 0.25rem;
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+}
+
+.rank-gap {
+ text-align: center;
+ margin: 0.25rem 0;
+}
B — c_257b57c092b1 (tommy-mor)
message
[d63870bc] fix
diff preview
diff --git a/server/src/parser.rs b/server/src/parser.rs
index 4bf201008857e3082b70a55e3dcbb2502ab3ed22..ea437c4434045b44cba04a2b3416df850db518e7 100644
--- a/server/src/parser.rs
+++ b/server/src/parser.rs
@@ -1,6 +1,6 @@
use std::collections::HashMap;
-use std::rc::Rc;
-use std::cell::RefCell;
+use std::sync::OnceLock;
+
use crate::parser_action::{GuideOption, ParserAction, ScrollingSuggestion, Suggestion};
// --- Core Abstractions ---
@@ -98,8 +98,8 @@ pub struct Edge {
description: Option<&'static str>,
}
-/// Handler function for generating UI actions
-type Handler = Box<dyn Fn(&str, &str, &HashMap<String, String>) -> ParserAction>;
+/// Handler function for generating UI actions (Send + Sync so the graph can live in `OnceLock`).
+type Handler = Box<dyn Fn(&str, &str, &HashMap<String, String>) -> ParserAction + Send + Sync>;
/// Node in the graph
pub struct Node {
@@ -109,16 +109,16 @@ pub struct Node {
handler: Option<Handler>,
}
-/// The composable parser graph
+/// The composable parser graph (immutable after `build`).
pub struct Graph {
- nodes: HashMap<NodeId, Rc<RefCell<Node>>>,
+ nodes: HashMap<NodeId, Node>,
root: NodeId,
}
// --- Graph Builder (Fluent API) ---
pub struct GraphBuilder {
- nodes: HashMap<NodeId, Rc<RefCell<Node>>>,
+ nodes: HashMap<NodeId, Node>,
current_node: Option<NodeId>,
root: NodeId,
}
@@ -126,31 +126,29 @@ pub struct GraphBuilder {
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);
-
+ nodes.insert(
+ "root",
+ Node {
+ id: "root",
+ edges: Vec::new(),
+ handler: None,
+ },
+ );
+
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.nodes.entry(node_id).or_insert_with(|| Node {
+ id: node_id,
+ edges: Vec::new(),
+ handler: None,
+ });
self.current_node = Some(node_id);
self
}
@@ -161,39 +159,39 @@ impl GraphBuilder {
}
/// Add an edge with description
- pub fn edge_with_desc(mut self, pattern: EdgePattern, target: NodeId, desc: Option<&'static str>) -> Self {
+ 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 {
+
+ self.nodes.entry(target).or_insert_with(|| Node {
+ id: target,
+ edges: Vec::new(),
+ handler: None,
+ });
+
+ if let Some(node) = self.nodes.get_mut(current) {
+ node.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>) -> ParserAction + 'static
+ pub fn handler<F>(mut self, handler: F) -> Self
+ where
+ F: Fn(&str, &str, &HashMap<String, String>) -> ParserAction + Send + Sync + '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));
+ if let Some(node) = self.nodes.get_mut(current) {
+ node.handler = Some(Box::new(handler));
}
self
}
@@ -225,24 +223,25 @@ impl Graph {
}
fn parse_recursive(&self, state: &mut ParserState) -> ParserAction {
- let node = self.nodes.get(state.current_node_id)
+ 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 {
+ if let Some(handler) = &node.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);
+ return self.suggest_from_edges(node, state);
}
-
+
let remaining = &state.input[state.cursor..];
-
+
// Try to match each edge
- for edge in &node_ref.edges {
+ for edge in &node.edges {
if let Some((consumed, captured)) = edge.pattern.matches(remaining) {
// Save state for potential backtracking
let saved_cursor = state.cursor;
@@ -297,7 +296,7 @@ impl Graph {
}
// No edges matched - try to provide suggestions
- self.suggest_from_edges(&node_ref, state)
+ self.suggest_from_edges(node, state)
}
fn suggest_from_edges(&self, node: &Node, state: &ParserState) -> ParserAction {
@@ -692,10 +691,15 @@ pub fn build_reddit_graph() -> Graph {
// --- Public API ---
+static REDDIT_GRAPH: OnceLock<Graph> = OnceLock::new();
+
+fn reddit_graph() -> &'static Graph {
+ REDDIT_GRAPH.get_or_init(build_reddit_graph)
+}
+
/// Parse a query string and return a UI action
pub fn parse_reddit_url(query: &str) -> ParserAction {
- let graph = build_reddit_graph();
- graph.parse(query)
+ reddit_graph().parse(query)
}
#[cfg(test)]
Hardlinks — judgments / attempts / prompt
judgments
attempts
Prompt text is loaded only by the download route.