constitution · epochs · watch · epoch 3

comparison

c_fc17a4aa31e6 (tommy-mor) vs c_ef71be9831cc (tommy-mor)

download prompt · raw event · cmp_acbf1e0250b872

council reasoning

~anthropic/claude-sonnet-latest · winner B · 85:15 · permalink

Side B is a substantial architectural refactor that removes cached edge/index state (GroupState) in favor of a minimal source-of-truth (ScopeVotes) with edges/components derived on demand, touching ranking, reducer, storage, pair-suggestion logic, and tests consistently—reducing state-sync bugs and simplifying persistence. Side A is a small, purely cosmetic UI tweak (moving the delete button into the meta row with CSS flex adjustments) with no behavioral or architectural impact.

~x-ai/grok-latest · winner B · 1:10 · permalink

A only relocates the delete control in forum HTML/CSS (post_header_meta + flex layout tweaks) with no behavioral or data-model change. B simplifies the core ranking store by replacing GroupState’s cached indices/edges/voted_pairs with ScopeVotes (uuid_votes + recent_votes) and deriving edges/components at rank time across reducer, ranking, pair, storage, and tests—reducing maintained state and inconsistency risk.

openai/gpt-chat-latest · winner B · 9:1 · permalink

Side B performs a substantial architectural refactor by replacing the cached `GroupState` with `ScopeVotes`, deriving graph edges, connected components, and rankings on demand from persisted votes. It updates ranking, storage, reducer, UI, and tests consistently, simplifying persisted state while preserving behavior; Side A is primarily a UI layout change that moves the delete button into the post metadata and adjusts CSS for flexbox positioning.

sides

A — c_fc17a4aa31e6 (tommy-mor)

message

[af6c4464] moved delete button

diff preview

diff --git a/server/src/html/forum/ingest.rs b/server/src/html/forum/ingest.rs
index 168106e495cf1a81f62f6a49e35c64beea287924..308358abc6cd6811079d9ec41b114093c0294d02 100644
--- a/server/src/html/forum/ingest.rs
+++ b/server/src/html/forum/ingest.rs
@@ -45,6 +45,7 @@ fn post_header_meta(
     principal: &str,
     ts: i64,
     now: i64,
+    delete_post_id: Option<&str>,
 ) -> Markup {
     let post_href = nav.post_url(tag, post_idx);
     let profile = profile_href(principal);
@@ -52,11 +53,19 @@ fn post_header_meta(
     let ago = timeago::timeago(now, ts);
     html! {
         div class="ingest-meta muted" title=(hover) {
-            a href=(post_href) class="post-num" { "#" (post_idx) }
-            " "
-            a href=(profile) class="post-author" { "@" (principal) }
-            " · "
-            (ago)
+            span class="ingest-meta-primary" {
+                a href=(post_href) class="post-num" { "#" (post_idx) }
+                " "
+                a href=(profile) class="post-author" { "@" (principal) }
+                " · "
+                (ago)
+            }
+            @if let Some(pid) = delete_post_id {
+                form class="post-delete-form" method="POST" action="/ui" {
+                    input type="hidden" name=(UI_RPC_FIELD) value=(template_json_compact(&HtmlUiAction::RedactPost { post_id: pid.to_string() }).unwrap());
+                    button type="submit" class="post-delete-btn" { "delete" }
+                }
+            }
         }
     }
 }
@@ -70,18 +79,12 @@ pub(super) fn post_header_row(
     now: i64,
     show_delete: bool,
 ) -> Markup {
-    let meta = post_header_meta(nav, tag, post_idx, &ing.principal, ing.ts, now);
-    html! {
-        div class="ingest-header-row" {
-            (meta)
-            @if show_delete {
-                form class="post-delete-form" method="POST" action="/ui" {
-                    input type="hidden" name=(UI_RPC_FIELD) value=(template_json_compact(&HtmlUiAction::RedactPost { post_id: ing.id.clone() }).unwrap());
-                    button type="submit" class="post-delete-btn" { "delete" }
-                }
-            }
-        }
-    }
+    let delete_post_id = if show_delete {
+        Some(ing.id.as_str())
+    } else {
+        None
+    };
+    post_header_meta(nav, tag, post_idx, &ing.principal, ing.ts, now, delete_post_id)
 }
 
 pub(super) fn redacted_header_row(
@@ -92,7 +95,7 @@ pub(super) fn redacted_header_row(
     now: i64,
     expanded: bool,
 ) -> Markup {
-    let meta = post_header_meta(nav, tag, post_idx, &ing.principal, ing.ts, now);
+    let meta = post_header_meta(nav, tag, post_idx, &ing.principal, ing.ts, now, None);
     let rpc_expand = template_json_compact(&json!({
         "action": "expand_redacted_post",
         "room": nav.room_wire,
diff --git a/server/static/theme_default.css b/server/static/theme_default.css
index 560881c1eec62f12d1e66875287be3c2f39198fc..83b49d079e6bf3e47c27fbd443a0ae75b1a03051 100644
--- a/server/static/theme_default.css
+++ b/server/static/theme_default.css
@@ -552,12 +552,21 @@ pre a.pre-link {
 }
 
 div.ingest-meta {
+  align-items: center;
   background: var(--g3);
   border-bottom: 2px solid var(--lo);
   color: var(--meta);
+  display: flex;
+  flex-wrap: wrap;
   font-size: 12px;
+  gap: 6px 10px;
+  justify-content: space-between;
   padding: 3px 10px;
 }
+span.ingest-meta-primary {
+  flex: 1 1 auto;
+  min-width: 0;
+}
 a.post-num { color: var(--meta); font-size: 12px; }
 a.post-num:hover { color: var(--signal); }
 a.post-author { color: var(--meta); font-size: 12px; text-decoration: none; }
@@ -579,7 +588,12 @@ div.ingest-header-row div.ingest-meta {
   flex: 1 1 auto;
   padding: 0;
 }
-form.post-delete-form { display: inline; margin: 0; }
+form.post-delete-form {
+  display: block;
+  flex-shrink: 0;
+  margin: 0;
+  margin-left: auto;
+}
 button.post-delete-btn {
   background: var(--g4);
   border: var(--bv) solid;
diff --git a/server/static/theme_retro_craft.css b/server/static/theme_retro_craft.css
index eb022a8c84c1d0cff36410541872d68a11195e31..7181c7f8547940a402ae0211220fd6ee93ee1364 100644
--- a/server/static/theme_retro_craft.css
+++ b/server/static/theme_retro_craft.css
@@ -132,8 +132,22 @@ p.post-truncation-action {
   margin: 0;
 }
 div.ingest-meta {
+  align-items: center;
   border-bottom: 1px solid var(--line);
-  padding: 0.35rem 0.65rem;
+  display: flex;
+  flex-wrap: wrap;
+  justify-content: space-between;
+  padding-left: 0.35rem;
+}
+span.ingest-meta-primary {
+  flex: 1 1 auto;
+  min-width: 0;
+}
+form.post-delete-form {
+  display: block;
+  flex-shrink: 0;
+  margin: 0;
+  margin-left: auto;
 }
 a.post-num {
   color: var(--ink-dim);

download full diff A

B — c_ef71be9831cc (tommy-mor)

message

[af73743d] Replace GroupState with ScopeVotes and derive edges at ranking time.

Store only uuid_votes and recent_votes per scope; rank centrality and pair logic rebuild edge weights on demand instead of maintaining cached state.

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

diff preview

diff --git a/server/src/events.rs b/server/src/events.rs
index 8a166d49b4f26835fbc2b58cb1f4bdbf002763b8..015208311f6c5c23a0e8aab068d002a69c89c4e1 100644
--- a/server/src/events.rs
+++ b/server/src/events.rs
@@ -43,7 +43,7 @@ pub enum ViewEvent {
 #[derive(Debug, Clone, Serialize, Deserialize)]
 #[serde(tag = "type", rename_all = "snake_case")]
 pub enum Event {
-    /// Pairwise comparison vote (replayed into the parent node's [`crate::reducer::GroupState`] on boot).
+    /// Pairwise comparison vote (replayed into the parent node's [`crate::reducer::ScopeVotes`] on boot).
     /// `scope` is the parent [`crate::path_types::ItemId`] string; empty string is the tree root.
     VoteRecorded {
         ts: i64,
diff --git a/server/src/html/mod.rs b/server/src/html/mod.rs
index 4eff2e19ed4d303ff8e80c1eabd8a15b4990e643..1e2e7a06856d8a62378741aaf5ed94a4ffed337e 100644
--- a/server/src/html/mod.rs
+++ b/server/src/html/mod.rs
@@ -13,7 +13,7 @@ use crate::{
     form_template::template_json_compact,
     path_types::ItemId,
     ranking::{
-        connected_components_from_voted_pairs, ranked_items_subset, RankedItem, MAX_ITERS, TOL,
+        ranked_items_subset, scope_components, RankedItem, MAX_ITERS, TOL,
     },
     reducer::{GlobalTree, NodeState},
     state::AppState,
@@ -397,10 +397,9 @@ pub fn ranking_panel_with_highlights(
     tree: &GlobalTree,
     highlighted: &HashSet<ItemId>,
 ) -> Markup {
-    let group = &node.local_ranking;
-    let n = group.idx_to_item.len();
-    let (comps, _isolates) =
-        connected_components_from_voted_pairs(n, group.voted_pairs.iter().copied());
+    let scope = &node.votes;
+    let (comps, _isolates, _) =
+        scope_components(scope);
 
     // Each connected component of voted items is its own ranking; isolated and
     // never-voted children fall into the "unranked" bucket below.
@@ -410,7 +409,7 @@ pub fn ranking_panel_with_highlights(
         if comp.len() < 2 {
             continue;
         }
-        let ranked = ranked_items_subset(group, comp, MAX_ITERS, TOL);
+        let ranked = ranked_items_subset(scope, comp, MAX_ITERS, TOL);
         for r in &ranked {
             ranked_ids.insert(r.item.clone());
         }
diff --git a/server/src/html/vote.rs b/server/src/html/vote.rs
index bf82aef3ad9c5f4e9e877c47dab27beb29a80b8f..3aa00c417c89a9cab3417c650b50ed7c73f08e20 100644
--- a/server/src/html/vote.rs
+++ b/server/src/html/vote.rs
@@ -14,7 +14,7 @@ use crate::{
     html::{ranking_panel_with_highlights, scope_theme_style, JsBuilder},
     pair::{children_of, resolve_pair, suggest_next_pair_in_pool},
     path_types::ItemId,
-    reducer::{GlobalTree, GroupState, NodeState, VoteData},
+    reducer::{GlobalTree, NodeState, ScopeVotes, VoteData},
     state::{parse_item_param, AppState},
     ui_action::UI_RPC_FIELD,
 };
@@ -68,8 +68,8 @@ fn ratios_for_page(v: &VoteData, page_left: &ItemId, page_right: &ItemId) -> (i3
     }
 }
 
-fn edge_votes(group: &GroupState, left: &ItemId, right: &ItemId) -> Vec<VoteData> {
-    group
+fn edge_votes(scope: &ScopeVotes, left: &ItemId, right: &ItemId) -> Vec<VoteData> {
+    scope
         .recent_votes
         .iter()
         .filter(|v| {
@@ -113,11 +113,11 @@ fn slider_value_from_ratios(r_left: i32, r_right: i32) -> i32 {
 
 fn vote_edge_history(
     tree: &GlobalTree,
-    group: &GroupState,
+    scope: &ScopeVotes,
     left: &ItemId,
     right: &ItemId,
 ) -> Markup {
-    let mut votes = edge_votes(group, left, right);
+    let mut votes = edge_votes(scope, left, right);
     votes.sort_by(|a, b| b.ts.cmp(&a.ts));
     let legend_left = child_title(tree, left);
     let legend_right = child_title(tree, right);
@@ -228,9 +228,9 @@ pub(crate) fn vote_recorded_morph(
 ) -> JsBuilder {
     let pool = children_of(tree, parent);
     let empty = NodeState::default();
-    let group = tree.get(parent).unwrap_or(&empty).local_ranking.clone();
-    let edge_history = vote_edge_history(tree, &group, left, right);
-    let next_pair = suggest_next(&group, left, right, &pool);
+    let scope = tree.get(parent).unwrap_or(&empty).votes.clone();
+    let edge_history = vote_edge_history(tree, &scope, left, right);
+    let next_pair = suggest_next(&scope, left, right, &pool);
     let actions = vote_compare_actions(parent, next_pair.as_ref());
     let sidebar = vote_ranking_sidebar(tree, parent, left, right);
     JsBuilder::new()
@@ -252,12 +252,12 @@ fn vote_compare_item_card(tree: &GlobalTree, item: &ItemId, side_class: &str) ->
 }
 
 fn suggest_next(
-    group: &GroupState,
+    scope: &ScopeVotes,
     left: &ItemId,
     right: &ItemId,
     pool: &[ItemId],
 ) -> Option<(ItemId, ItemId)> {
-    suggest_next_pair_in_pool(group, pool, Some((left, right)))
+    suggest_next_pair_in_pool(scope, pool, Some((left, right)))
 }
 
 pub async fn vote_page(
@@ -284,9 +284,9 @@ pub async fn vote_page(
         };
 
     let pool = children_of(&tree, &parent);
-    let group = &parent_node.local_ranking;
-    let next_pair = suggest_next(group, &left, &right, &pool);
-    let edge_history = vote_edge_history(&tree, group, &left, &right);
+    let scope = &parent_node.votes;
+    let next_pair = suggest_next(&scope, &left, &right, &pool);
+    let edge_history = vote_edge_history(&tree, &scope, &left, &right);
 
     let rpc_json = template_json_compact(&serde_json::json!({
         "action": "record_vote",
@@ -388,8 +388,8 @@ mod polarity_tests {
         let mut tree = GlobalTree::new();
         tree.apply_vote(&parent, vote, TEST_ACTOR_UUID);
 
-        let group = &tree.get(&parent).unwrap().local_ranking;
-        let ranked = ranked_items(group);
+        let scope = &tree.get(&parent).unwrap().votes;
+        let ranked = ranked_items(scope);
         assert_eq!(
             ranked[0].item, left,
             "left item should rank first when ratio favours the left"
diff --git a/server/src/pair.rs b/server/src/pair.rs
index 42a1b1eb2adf16730d34d0fe23c13d5a75d7ba27..9873295c51526726089875bbfd2f97d7faa91872 100644
--- a/server/src/pair.rs
+++ b/server/src/pair.rs
@@ -11,8 +11,8 @@ use std::collections::{HashMap, HashSet};
 
 use crate::{
     path_types::ItemId,
-    ranking::{connected_components_from_voted_pairs, ranked_items},
-    reducer::{GlobalTree, GroupState},
+    ranking::{pair_is_voted, ranked_items, scope_components},
+    reducer::{GlobalTree, ScopeVotes},
 };
 
 fn pairs_match(a: &ItemId, b: &ItemId, x: &ItemId, y: &ItemId) -> bool {
@@ -23,26 +23,15 @@ fn pair_excluded(a: &ItemId, b: &ItemId, exclude: Option<(&ItemId, &ItemId)>) ->
     exclude.is_some_and(|(x, y)| pairs_match(a, b, x, y))
 }
 
-fn pair_is_voted(group: &GroupState, a: &ItemId, b: &ItemId) -> bool {
-    let Some(&ai) = group.item_to_idx.get(a) else {
-        return false;
-    };
-    let Some(&bi) = group.item_to_idx.get(b) else {
-        return false;
-    };
-    let (i, j) = if ai < bi { (ai, bi) } else { (bi, ai) };
-    group.voted_pairs.contains(&(i, j))
-}
 
 struct ComponentLayout {
     ids: HashMap<ItemId, usize>,
     established: HashSet<usize>,
 }
 
-fn component_layout(group: &GroupState, pool: &[ItemId]) -> ComponentLayout {
-    let n = group.idx_to_item.len();
-    let (comps, isolates) =
-        connected_components_from_voted_pairs(n, group.voted_pairs.iter().copied());
+fn component_layout(scope: &ScopeVotes, pool: &[ItemId]) -> ComponentLayout {
+    let (comps, isolates, idx_to_item) = scope_components(scope);
+    let n = idx_to_item.len();
 
     let mut established = HashSet::new();
     let mut ids: HashMap<ItemId, usize> = HashMap::new();
@@ -52,14 +41,14 @@ fn component_layout(group: &GroupState, pool: &[ItemId]) -> ComponentLayout {
         }
         for &idx in comp {
             if idx < n {
-                ids.insert(group.idx_to_item[idx].clone(), comp_idx);
+                ids.insert(idx_to_item[idx].clone(), comp_idx);
             }
         }
     }
     let mut next = comps.len();
     for &idx in &isolates {
         if idx < n {
-            ids.insert(group.idx_to_item[idx].clone(), next);
+            ids.insert(idx_to_item[idx].clone(), next);
             next += 1;
         }
     }
@@ -121,7 +110,7 @@ fn established_groups_in_pool<'a>(
     groups
 }
 
-fn ranked_pool_order(group: &GroupState, pool: &[ItemId]) -> Vec<ItemId> {
+fn ranked_pool_order(group: &ScopeVotes, pool: &[ItemId]) -> Vec<ItemId> {
     let pool_set: HashSet<_> = pool.iter().collect();
     ranked_items(group)
         .into_iter()
@@ -132,7 +121,7 @@ fn ranked_pool_order(group: &GroupState, pool: &[ItemId]) -> Vec<ItemId> {
 
 /// Walk 1↔2, 2↔3, …; optional `require_unvoted` skips voted edges.
 fn zip_adjacent_pair(
-    group: &GroupState,
+    group: &ScopeVotes,
     order: &[ItemId],
     exclude: Option<(&ItemId, &ItemId)>,
     require_unvoted: bool,
@@ -153,7 +142,7 @@ fn zip_adjacent_pair(
 
 /// Grow the voted graph toward one component (no rank centrality).
 fn suggest_grow_pair(
-    group: &GroupState,
+    group: &ScopeVotes,
     pool: &[ItemId],
     layout: &ComponentLayout,
     exclude: Option<(&ItemId, &ItemId)>,
@@ -216,7 +205,7 @@ fn suggest_grow_pair(
 
 /// Pick the next pair to vote on within `pool`.
 pub fn suggest_next_pair_in_pool(
-    group: &GroupState,
+    group: &ScopeVotes,
     pool: &[ItemId],
     exclude: Option<(&ItemId, &ItemId)>,
 ) -> Option<(ItemId, ItemId)> {
@@ -315,7 +304,7 @@ pub fn resolve_pair(
         (None, None) => {
             let group = tree
                 .get(parent)
-                .map(|n| &n.local_ranking)
+                .map(|n| &n.votes)
                 .cloned()
                 .unwrap_or_default();
             suggest_next_pair_in_pool(&group, &children, None).ok_or(PairError::NoPair)
@@ -398,7 +387,7 @@ mod tests {
                 "https://reddit.com/r/rust/b",
             ],
         );
-        let group = tree.get(&parent).unwrap().local_ranking.clone();
+        let group = tree.get(&parent).unwrap().votes.clone();
         let pool = children_of(&tree, &parent);
         assert!(!pair_is_voted(&group, &pool[0], &pool[1]));
         assert!(suggest_next_pair_in_pool(&group, &pool, None).is_some());
@@ -417,7 +406,7 @@ mod tests {
         );
         let vote = test_vote(1, "https://reddit.com/r/rust/a", "https://reddit.com/r/rust/b", 2, 1);
         apply(&mut tree, &parent, vote);
-        let group = tree.get(&parent).unwrap().local_ranking.clone();
+        let group = tree.get(&parent).unwrap().votes.clone();
         let pool = children_of(&tree, &parent);
         let (l, r) = suggest_next_pair_in_pool(&group, &pool, None).unwrap();
         let voted_ab = (l.as_str() == "https://reddit.com/r/rust/a" && r.as_str() == "https://reddit.com/r/rust/b")
@@ -441,7 +430,7 @@ mod tests {
         let cd = test_vote(2, "https://reddit.com/r/rust/c", "https://reddit.com/r/rust/d", 2, 1);
         apply(&mut tree, &parent, ab);
         apply(&mut tree, &parent, cd);
-        let group = tree.get(&parent).unwrap().local_ranking.clone();
+        let group = tree.get(&parent).unwrap().votes.clone();
         let pool = children_of(&tree, &parent);
         let pair = suggest_next_pair_in_pool(&group, &pool, None).unwrap();
         let chosen = pair_set(&pair);
@@ -467,7 +456,7 @@ mod tests {
         );
         let ab = test_vote(1, "https://reddit.com/r/rust/a", "https://reddit.com/r/rust/b", 2, 1);
         apply(&mut tree, &parent, ab);
-        let group = tree.get(&parent).unwrap().local_ranking.clone();
+        let group = tree.get(&parent).unwrap().votes.clone();
         let pool = children_of(&tree, &parent);
         let pair = suggest_next_pair_in_pool(&group, &pool, None).unwrap();
         let chosen = pair_set(&pair);
@@ -496,7 +485,7 @@ mod tests {
         );
         let ab = test_vote(1, "https://reddit.com/r/rust/a", "https://reddit.com/r/rust/b", 2, 1);
         apply(&mut tree, &parent, ab);
-        let group = tree.get(&parent).unwrap().local_ranking.clone();
+       

… preview truncated; 38,568 characters omitted

download full diff B

Hardlinks — judgments / attempts / prompt

prompt download

judgments

attempts

Prompt text is loaded only by the download route.