Side B delivers a real user-facing feature (account/alias management page, alias switching, re-linking OAuth) with corresponding CSS, template, and test updates, representing lasting functional value. Side A is a solid but narrow internal refactor (Rc<RefCell> -> plain HashMap + OnceLock singleton) that improves code quality and enables Send+Sync but has much smaller scope and impact than B's feature work.
constitution · epochs · watch · epoch 3
c_257b57c092b1 (tommy-mor) vs c_164969eeea14 (tommy-mor)
download prompt · raw event · cmp_5d5fc673c2ad18
council reasoning
B delivers a lasting product surface: /login becomes a real account page (trust weight, alias list/switch, claim forms, OAuth re-link) with shared claim UI, CSS, and test updates. A is a solid parser design fix (drop Rc<RefCell>, Send+Sync handlers, OnceLock-cached graph) but stays an internal efficiency/thread-safety refactor with less user-facing value.
Side A refactors the parser graph to be immutable after construction, removes unnecessary Rc<RefCell> indirection, makes handlers Send + Sync, and caches the graph in a OnceLock so it is built once and safely reused. Side B adds a substantial account-management UI (alias switching, claiming, OAuth relinking, styling, and page restructuring), but it is primarily a feature/UI expansion rather than a foundational runtime improvement with broad, lasting performance and thread-safety benefits.
sides
A — 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)]
B — c_164969eeea14 (tommy-mor)
message
[d6b4a41e] Turn /login into an account page with alias switching. Signed-in users can see trust weight, switch aliases, claim new ones, and re-link OAuth from one place. Co-authored-by: Cursor <cursoragent@cursor.com>
diff preview
diff --git a/server/src/auth/mod.rs b/server/src/auth/mod.rs
index d4a85ef52c15dc35148e4c743f0d646cbbdb056d..5906f93b13853421e96a3c37bc9d8202a47842bf 100644
--- a/server/src/auth/mod.rs
+++ b/server/src/auth/mod.rs
@@ -94,63 +94,169 @@ fn alias_list(db: &durable::Db, uuid: &str) -> Vec<String> {
.unwrap_or_default()
}
-fn login_body(
- session: Option<&session::SessionActor>,
+fn alias_claim_forms(return_to: &str, submit_label: &str) -> Result<Markup, StatusCode> {
+ let check_rpc = template_json_compact(&serde_json::json!({
+ "action": "check_pseudonym",
+ "pseudonym": {"$form": "pseudonym"},
+ }))
+ .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
+ let claim_rpc = template_json_compact(&serde_json::json!({
+ "action": "claim_pseudonym",
+ "pseudonym": {"$form": "pseudonym"},
+ "return_to": return_to,
+ }))
+ .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
+
+ Ok(html! {
+ div class="alias-claim" {
+ form id="alias-check-form" method="POST" action="/ui" {
+ input type="hidden" name=(UI_RPC_FIELD) value=(check_rpc);
+ label for="alias-input" { "alias" }
+ input type="text" id="alias-input" name="pseudonym" autocomplete="off"
+ data-testid="alias-input" maxlength="64" placeholder="letters, numbers, _ -";
+ p id="alias-status" class="muted" data-testid="alias-status" { "type to check availability" }
+ }
+ form id="alias-claim-form" method="POST" action="/ui" {
+ input type="hidden" name=(UI_RPC_FIELD) value=(claim_rpc);
+ input type="hidden" name="pseudonym" id="alias-claim-field" value="";
+ button type="submit" class="btn-primary" data-testid="alias-claim" { (submit_label) }
+ }
+ }
+ })
+}
+
+fn signed_out_body(providers: &[(&str, String)]) -> Markup {
+ html! {
+ main class="panel login-page" {
+ section class="login-section" {
+ h1 { "sign in" }
+ p class="muted" { "link an account to vote under a lasting alias" }
+ @if providers.is_empty() {
+ p class="muted" {
+ "OAuth is not configured. Set GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET."
+ }
+ } @else {
+ ul class="oauth-provider-list" {
+ @for (name, href) in providers {
+ li {
+ a href=(href) class="btn-primary oauth-provider"
+ data-testid=(format!("oauth-{}", name.to_lowercase())) {
+ (format!("Continue with {name}"))
+ }
+ }
+ }
+ }
+ }
+ }
+ p class="login-back" { a href="/" { "← back" } }
+ }
+ }
+}
+
+fn account_body(
+ actor: &session::SessionActor,
aliases: &[String],
providers: &[(&str, String)],
+ claim_forms: Markup,
) -> Markup {
+ let current = actor.pseudonym.trim();
html! {
- main class="panel login-page" {
- div class="login-grid" {
- section class="login-oauth" {
- h1 { "sign in" }
- @if providers.is_empty() {
- p class="muted" {
- "OAuth is not configured. Set GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET."
- }
- } @else {
- ul class="oauth-provider-list" {
- @for (name, href) in providers {
- li {
- a href=(href) class="button oauth-provider" data-testid=(format!("oauth-{}", name.to_lowercase())) {
- (format!("Continue with {name}"))
+ main class="panel login-page account-page" {
+ section class="login-section" {
+ h1 { "account" }
+ @if current.is_empty() {
+ p class="muted" { "finish setup by choosing an alias below" }
+ } @else {
+ p class="account-current" {
+ "voting as "
+ strong data-testid="account-current" { (current) }
+ }
+ }
+ p class="muted small" data-testid="account-weight" {
+ "trust weight " (format!("{:.1}", actor.trust_weight))
+ " · rises when you link more OAuth providers"
+ }
+ }
+
+ section class="login-section" {
+ h2 { "aliases" }
+ @if aliases.is_empty() {
+ p class="muted" data-testid="alias-list-empty" { "none yet — claim one below" }
+ } @else {
+ ul id="alias-list" class="alias-list" data-testid="alias-list" {
+ @for alias in aliases {
+ @let is_current = alias == current;
+ li class=(if is_current { "alias-item alias-current" } else { "alias-item" }) {
+ span class="alias-name" { (alias) }
+ @if is_current {
+ span class="alias-badge" data-testid="alias-current-badge" { "current" }
+ } @else {
+ form class="alias-switch" method="post" action="/auth/switch"
+ data-navigate="full" {
+ input type="hidden" name="pseudonym" value=(alias);
+ button type="submit" class="btn-secondary"
+ data-testid=(format!("alias-switch-{alias}")) {
+ "use"
+ }
}
}
}
}
}
- @if let Some(actor) = session {
- p class="muted small" {
- "session active · weight " (format!("{:.1}", actor.trust_weight))
- }
- form method="post" action="/auth/logout" data-navigate="full" {
- button type="submit" { "log out" }
- }
- }
}
- section class="login-aliases" {
- h2 { "your aliases" }
- ul id="alias-list" class="alias-list" {
- @if aliases.is_empty() {
- li class="muted" data-testid="alias-list-empty" { "none yet" }
- } @else {
- @for alias in aliases {
- li { (alias) }
+ }
+
+ section class="login-section" {
+ h2 { "add alias" }
+ p class="muted small" { "each alias is unique across sorter2" }
+ (claim_forms)
+ }
+
+ @if !providers.is_empty() {
+ section class="login-section" {
+ h2 { "linked sign-in" }
+ p class="muted small" { "sign in again with the same provider to return to this account" }
+ ul class="oauth-provider-list" {
+ @for (name, href) in providers {
+ li {
+ a href=(href) class="btn-secondary oauth-provider"
+ data-testid=(format!("oauth-relink-{}", name.to_lowercase())) {
+ (format!("Re-link {name}"))
+ }
}
}
}
}
}
- p { a href="/" { "← back" } }
+
+ section class="login-section login-actions" {
+ form method="post" action="/auth/logout" data-navigate="full" {
+ button type="submit" class="btn-secondary" data-testid="account-logout" { "log out" }
+ }
+ }
+
+ p class="login-back" { a href="/" { "← back" } }
}
}
}
+fn login_body(
+ session: Option<&session::SessionActor>,
+ aliases: &[String],
+ providers: &[(&str, String)],
+ claim_forms: Option<Markup>,
+) -> Markup {
+ match (session, claim_forms) {
+ (Some(actor), Some(forms)) => account_body(actor, aliases, providers, forms),
+ _ => signed_out_body(providers),
+ }
+}
+
pub async fn login_page(
State(state): State<AppState>,
jar: CookieJar,
Query(query): Query<LoginQuery>,
-) -> Response {
+) -> Result<Response, StatusCode> {
let return_to = return_from_query_or_jar(&jar, query.return_to.as_deref());
let jar = jar.add(session::auth_return_cookie_value(&return_to));
@@ -164,16 +270,26 @@ pub async fn login_page(
.unwrap_or_default();
let providers = oauth_providers(&base_url_from_env(state.cfg.port), &return_to);
+ let claim_forms = if session.is_some() {
+ Some(alias_claim_forms("/login", "claim alias")?)
+ } else {
+ None
+ };
+
let markup = layout(
- "login · sorter2",
- login_body(session.as_ref(), &aliases, &providers),
+ if session.is_some() {
+ "account · sorter2"
+ } else {
+ "login · sorter2"
+ },
+ login_body(session.as_ref(), &aliases, &providers, claim_forms),
state.views.get_views("/login"),
session
.as_ref()
.filter(|s| !s.pseudonym.trim().is_empty())
.map(|s| s.pseudonym.as_str()),
);
- (jar, Html(markup.into_string())).into_response()
+ Ok((jar, Html(markup.into_string())).into_response())
}
pub async fn alias_page(
@@ -186,38 +302,17 @@ pub async fn alias_page(
let db = state.projection_store.db();
let session = session::load_valid_session(db, &session_id).ok_or(StatusCode::UNAUTHORIZED)?;
if session::session_has_pseudonym(&session) {
- return Ok(Redirect::to(&return_to).into_response());
+ // Already onboarded — manage aliases on the account page.
+ return Ok(Redirect::to("/login").into_response());
}
- let check_rpc = template_json_compact(&serde_json::json!({
- "action": "check_pseudonym",
- "pseudonym": {"$form": "pseudonym"},
- }))
- .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
- let claim_rpc = template_json_compact(&serde_json::json!({
- "action": "claim_pseudonym",
- "pseudonym": {"$form": "pseudonym"},
- "return_to": return_to,
- }))
- .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
-
+ let claim_forms = alias_claim_forms(&return_to, "continue")?;
let body = html! {
main class="panel alias-page" {
h1 { "choose alias" }
p class="muted" { "pick a unique display name for your votes" }
- form id="alias-check-form" method="POST" action="/ui" {
- input type="hidden" name=(UI_RPC_FIELD) value=(check_rpc);
- label { "alias" }
- input type="text" id="alias-input" name="pseudonym" autocomplete="off"
- data-testid="alias-input" maxlength="64";
- p id="alias-status" class="muted" data-testid="alias-status" { "type to check availability" }
- }
- form id="alias-claim-form" method="POST" action="/ui" {
- input type="hidden" name=(UI_RPC_FIELD) value=(claim_rpc);
- input type="hidden" nam
… preview truncated; 4,534 characters omittedHardlinks — judgments / attempts / prompt
judgments
attempts
Prompt text is loaded only by the download route.