constitution · epochs · watch · epoch 3

comparison

c_ef71be9831cc (tommy-mor) vs c_77729db919ab (tommy-mor)

download prompt · raw event · cmp_70795f3c6df4ed

council reasoning

~anthropic/claude-sonnet-latest · winner B · 6:4 · permalink

Side B introduces a genuinely new, composable URL-canonicalization subsystem (url_rules module with engine + registry, domain-specific rules for reddit/youtube, breadcrumb/parent derivation) that fixes real correctness issues (schemeless IDs, non-canonical hosts, youtube variants) and is exercised by substantial new tests. Side A is a solid internal refactor (GroupState -> ScopeVotes, deriving edges on demand) that improves clarity but is mostly mechanical renaming/restructuring of existing logic without adding new capability or fixing an external-facing bug, making B's contribution somewhat more impactful in scope though both are reasonable quality refactors.

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

A removes incremental edge/index caches from GroupState (ensure_item, rollback_weights, voted_pairs) in favor of ScopeVotes that keep only uuid_votes/recent_votes and rebuild edges at rank time—a lasting single-source-of-truth fix across reducer, ranking, storage, and pair logic. B adds a real url_rules engine and full-URL ItemIds, but much of the diff is mechanical https:// string churn in tests and call sites rather than equally deep design consolidation.

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

Side A makes a substantial architectural change by replacing cached `GroupState` with `ScopeVotes`, deriving ranking edges and connected components on demand from deduplicated votes. It updates ranking, storage, reducers, UI, and tests consistently, simplifying persisted state while preserving behavior. Side B mainly introduces URL canonicalization infrastructure and migrates many tests and IDs to `https://` forms, which is useful but much of the patch is mechanical conversion rather than a core data model improvement.

sides

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

B — c_77729db919ab (tommy-mor)

message

[239c074b] url schema stuff

diff preview

diff --git a/AGENTS.md b/AGENTS.md
index 426a88e7c1da54fe0a28c5c76fa4e1f1bc117fcf..e60b9ba6012593361ef10e8fdd9439cd9932e09b 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -58,3 +58,4 @@ Use **tmux** for `cargo run --package sorter2-server` (dev server). Rebuild afte
 
 - First `cargo test` / `cargo build --release` is slow; Clojure smoke test always does a release build.
 - `legacy/` and `ideas/` are not part of the workspace build.
+- **ItemId** for web URLs is a canonical full URL (`https://reddit.com/r/rust`). Rules live in [`server/src/url_rules/`](server/src/url_rules/) (composable Rust, not a config DSL). After changing canonicalization rules, rebuild the projection: `cargo run --package sorter2-server -- replay-index`.
diff --git a/Cargo.lock b/Cargo.lock
index 0dd4fce5fb6400ae153cca4e3dbf5a5158e6d8b4..49a908ef935c430dbe63c6a28d8a24e38b489486 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1951,6 +1951,7 @@ dependencies = [
  "tower-http 0.5.2",
  "tracing",
  "tracing-subscriber",
+ "url",
  "urlencoding",
 ]
 
diff --git a/REPLAY.sh b/REPLAY.sh
new file mode 100755
index 0000000000000000000000000000000000000000..f2dbd8aea60c02d2feef74805f7ef5c2b7022537
--- /dev/null
+++ b/REPLAY.sh
@@ -0,0 +1,2 @@
+cargo run --package sorter2-server -- replay-index
+
diff --git a/server/Cargo.toml b/server/Cargo.toml
index 27f552c20b97ef28cdde4cb6b1a4980375135111..ad4912791aff59fb1d3293f66ad381ae618cd60b 100644
--- a/server/Cargo.toml
+++ b/server/Cargo.toml
@@ -24,6 +24,7 @@ async-stream = "0.3"
 futures-util = { version = "0.3", default-features = false, features = ["std"] }
 rand = "0.8"
 urlencoding = "2"
+url = "2"
 durable = { path = "../durable" }
 
 [dev-dependencies]
diff --git a/server/src/entity_store.rs b/server/src/entity_store.rs
index d5d17c3676e4a8ddec998e9f5a9dbafe9c2d9d0e..d29f39aecca6f12cdcf263cf77c3654eb4ee6cfa 100644
--- a/server/src/entity_store.rs
+++ b/server/src/entity_store.rs
@@ -124,7 +124,7 @@ mod tests {
     fn round_trip_payload() {
         let tmp = tempfile::tempdir().unwrap();
         let store = EntityStore::open(tmp.path()).unwrap();
-        let id = ItemId::parse("reddit.com/r/rust").unwrap();
+        let id = ItemId::from_url("https://reddit.com/r/rust").unwrap();
         let payload = json!({"kind": "t5", "data": {"display_name": "rust"}});
 
         store.put(&id, &payload).unwrap();
diff --git a/server/src/event_log.rs b/server/src/event_log.rs
index 36f5b406084065b608735987cdb483c236e03081..2c9290b6fdbf2c2ad1c0f1ffd7374b2d9cc97f36 100644
--- a/server/src/event_log.rs
+++ b/server/src/event_log.rs
@@ -199,7 +199,7 @@ mod tests {
         log.append(&sample_record(
             1,
             Event::NodeEnsured {
-                id: "reddit.com/r/rust".into(),
+                id: "https://reddit.com/r/rust".into(),
             },
         ))
         .await
@@ -237,7 +237,7 @@ mod tests {
         let path = tmp.path().join("events.jsonl");
         let log = EventLog::new(&path);
         let event = Event::NodeEnsured {
-            id: "reddit.com/r/rust".into(),
+            id: "https://reddit.com/r/rust".into(),
         };
         log.append(&sample_record(1, event)).await.unwrap();
 
@@ -255,7 +255,7 @@ mod tests {
         let path = tmp.path().join("events.jsonl");
         std::fs::write(
             &path,
-            r#"{"type":"node_ensured","id":"reddit.com/r/rust"}
+            r#"{"type":"node_ensured","id":"https://reddit.com/r/rust"}
 {"schema":1,"seq":1,"ts":1,"event":{"type":"vote_recorded","ts":1,"a":"a","b":"b","ratio_left":2,"ratio_right":1,"scope":""}}
 "#,
         )
@@ -295,7 +295,7 @@ mod tests {
         log.append(&sample_record(
             1,
             Event::NodeEnsured {
-                id: "reddit.com/r/rust".into(),
+                id: "https://reddit.com/r/rust".into(),
             },
         ))
         .await
@@ -303,7 +303,7 @@ mod tests {
         log.append(&sample_record(
             3,
             Event::NodeEnsured {
-                id: "reddit.com/r/python".into(),
+                id: "https://reddit.com/r/python".into(),
             },
         ))
         .await
diff --git a/server/src/journal.rs b/server/src/journal.rs
index 521a108019de1ea870d14c4fafbfe572c20ce0de..50bc89f976edb82b7b0e49e954a8eccbbe82bf87 100644
--- a/server/src/journal.rs
+++ b/server/src/journal.rs
@@ -141,10 +141,10 @@ mod tests {
         let j2 = journal.clone();
         let (r1, r2) = tokio::join!(
             j1.append(Event::NodeEnsured {
-                id: "reddit.com/r/rust".into(),
+                id: "https://reddit.com/r/rust".into(),
             }),
             j2.append(Event::NodeEnsured {
-                id: "reddit.com/r/python".into(),
+                id: "https://reddit.com/r/python".into(),
             }),
         );
         r1.unwrap();
@@ -153,10 +153,10 @@ mod tests {
         assert_eq!(projection_store.last_applied_event_count().unwrap(), 2);
         let tree = projection_store.load_tree().unwrap();
         assert!(tree
-            .get(&ItemId::parse("reddit.com/r/rust").unwrap())
+            .get(&ItemId::parse("https://reddit.com/r/rust").unwrap())
             .is_some());
         assert!(tree
-            .get(&ItemId::parse("reddit.com/r/python").unwrap())
+            .get(&ItemId::parse("https://reddit.com/r/python").unwrap())
             .is_some());
     }
 
@@ -170,7 +170,7 @@ mod tests {
                 1,
                 1,
                 Event::NodeEnsured {
-                    id: "reddit.com/r/rust".into(),
+                    id: "https://reddit.com/r/rust".into(),
                 },
             ))
             .await
@@ -186,7 +186,7 @@ mod tests {
                 1,
                 1,
                 Event::NodeEnsured {
-                    id: "reddit.com/r/rust".into(),
+                    id: "https://reddit.com/r/rust".into(),
                 },
             )],
         )
@@ -202,7 +202,7 @@ mod tests {
         );
         journal
             .append(Event::NodeEnsured {
-                id: "reddit.com/r/python".into(),
+                id: "https://reddit.com/r/python".into(),
             })
             .await
             .unwrap();
@@ -227,13 +227,13 @@ mod tests {
         journal
             .append_many(vec![
                 Event::NodeEnsured {
-                    id: "reddit.com/r/rust".into(),
+                    id: "https://reddit.com/r/rust".into(),
                 },
                 Event::NodeEnsured {
-                    id: "reddit.com/r/python".into(),
+                    id: "https://reddit.com/r/python".into(),
                 },
                 Event::NodeEnsured {
-                    id: "reddit.com/r/clojure".into(),
+                    id: "https://reddit.com/r/clojure".into(),
                 },
             ])
             .await
@@ -245,7 +245,7 @@ mod tests {
         assert_eq!(projection_store.last_applied_event_count().unwrap(), 3);
         let tree = projection_store.load_tree().unwrap();
         assert!(tree
-            .get(&ItemId::parse("reddit.com/r/clojure").unwrap())
+            .get(&ItemId::parse("https://reddit.com/r/clojure").unwrap())
             .is_some());
     }
 }
diff --git a/server/src/lib.rs b/server/src/lib.rs
index 9bd5f76fd1406b9b1be4c272f4ba8647edde2678..5c02c8e704e4664453bad75d819df8a067668176 100644
--- a/server/src/lib.rs
+++ b/server/src/lib.rs
@@ -9,6 +9,7 @@ pub mod journal;
 pub mod pair;
 pub mod parser;
 pub mod path_types;
+pub mod url_rules;
 pub mod projection_apply;
 pub mod projection_store;
 pub mod ranking;
diff --git a/server/src/pair.rs b/server/src/pair.rs
index 43f780ba6ea6ce1cdc2e1f4cbb252ba8a10684b9..815a97b80e3e9f348e0937a4f147f2862018edb0 100644
--- a/server/src/pair.rs
+++ b/server/src/pair.rs
@@ -381,42 +381,42 @@ mod tests {
 
     #[test]
     fn suggest_prefers_unvoted_pair() {
-        let parent = ItemId::parse("reddit.com/r/rust").unwrap();
+        let parent = ItemId::parse("https://reddit.com/r/rust").unwrap();
         let mut tree = seed_children(
             &parent,
             &[
-                "reddit.com/r/rust/a",
-                "reddit.com/r/rust/b",
-                "reddit.com/r/rust/c",
+                "https://reddit.com/r/rust/a",
+                "https://reddit.com/r/rust/b",
+                "https://reddit.com/r/rust/c",
             ],
         );
         let vote =
-            VoteData::from_recorded(1, "reddit.com/r/rust/a", "reddit.com/r/rust/b", 2, 1).unwrap();
+            VoteData::from_recorded(1, "https://reddit.com/r/rust/a", "https://reddit.com/r/rust/b", 2, 1).unwrap();
         tree.apply_vote(&parent, vote);
         let group = tree.get(&parent).unwrap().local_ranking.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() == "reddit.com/r/rust/a" && r.as_str() == "reddit.com/r/rust/b")
-            || (l.as_str() == "reddit.com/r/rust/b" && r.as_str() == "reddit.com/r/rust/a");
+        let voted_ab = (l.as_str() == "https://reddit.com/r/rust/a" && r.as_str() == "https://reddit.com/r/rust/b")
+            || (l.as_str() == "https://reddit.com/r/rust/b" && r.as_str() == "https://reddit.com/r/rust/a");
         assert!(!voted_ab);
     }
 
     #[test]
     fn suggest_bridges_separate_components() {
-        let parent = ItemId::parse("reddit.com/r/rust").unwrap();
+        let parent = ItemId::parse("https://reddit.com/r/rust").unwrap();
         let mut tree = seed_children(
             &parent,
             &[
-                "reddit.com/r/rust/a",
-                "reddit.com/r/rust/b",
-                "reddit.com/r/rust/c",
-                "reddit.com/r/rust/d",
+                "https://reddit.com/r/rust/a",
+                "https://reddit.com/r/rust/b",
+                "https://reddit.com/r/rust/c",
+                "https://reddit.com/r/rust/d",
             ],
         );
         let ab =
-            VoteData::from_recorded(1, "reddit.com/r/rust/a", "reddit.com/r/rust/b", 2, 1).unwrap();
+            VoteData::from_recorded(1, "https://reddit.com/r/rust/a", "https://reddit.com/r/rust/b", 2, 1).unwrap();
         let cd =
-            VoteData::from_recorded(2, "reddit.com/r/rust/c", "reddit.com/r/rust/d", 2, 1).unwrap();
+            VoteData::from_recorded(2, "https://reddit.com/r/rust/c", "https://reddit.com/r/rust/d", 2, 1).unwrap();
         tree.apply_vote(&parent, ab);
         tree.apply_vote(&parent, cd);
         let group = tree.get(&parent).unwrap().local_ranking.clone();
@@ -424,37 +424,37 @@ mod tests {
         let pair = suggest_next_pair_in_pool(&group, &pool, None).unwrap();
         let chosen = pair_set(&pair);
         let from_ab =
-            chosen.contains("reddit.com/r/rust/a") || chosen.contains("reddit.com/r/rust/b");
+            chosen.contains("https://reddit.com/r/rust/a") || chosen.contains("https://reddit.com/r/rust/b");
         let from_cd =
-            chosen.contains("reddit.com/r/rust/c") || chosen.contains("reddit.com/r/rust/d");
+            chosen.contains("https://reddit.com/r/rust/c") || chosen.contains("https://reddit.com/r/rust/d");
         assert!(from_ab && from_cd, "expected bridge pair, got {:?}", chosen);
     }
 
     #[test]
     fn suggest_prefers_attach_over_isolate_pair_among_many_unranked() {
-        let parent = ItemId::parse("reddit.com/r/rust").unwrap();
+        let parent = ItemId::parse("https://reddit.com/r/rust").unwrap();
         let mut tree = seed_children(
             &parent,
             &[
-                "reddit.com/r/rust/a",
-                "reddit.com/r/rust/b",
-                "reddit.com/r/rust/c",
-                "reddit.com/r/rust/d",
-                "reddit.com/r/rust/e",
+                "https://reddit.com/r/rust/a",
+                "https://reddit.com/r/rust/b",
+                "https://reddit.com/r/rust/c",
+                "https://reddit.com/r/

… preview truncated; 51,799 characters omitted

download full diff B

Hardlinks — judgments / attempts / prompt

prompt download

judgments

attempts

Prompt text is loaded only by the download route.