Commit A fixes a genuine correctness bug (bipartite chain oscillation on star topologies) with a principled algorithmic correction grounded in the cited paper, plus solid regression tests (Rust + Clojure fixtures) proving the fix. Commit B is a substantial refactor (GroupState -> ScopeVotes, deriving edges on demand) that improves architecture and removes cached-state bugs risk, but it is churn-heavy, touches many files for a design preference, and doesn't fix a user-visible bug or add new guarantees beyond what A already established.
constitution · epochs · watch · epoch 3
c_48aeaf9b52c3 (tommy-mor) vs c_ef71be9831cc (tommy-mor)
download prompt · raw event · cmp_4c255dc8dfdf4d
council reasoning
A fixes a real correctness bug in the core Rank Centrality math (wrong d_max caused bipartite oscillation and uniform star scores), aligns the implementation with the paper, and adds targeted regression tests. B is a worthwhile structural simplification (votes-only ScopeVotes, derive edges on demand) but is largely mechanical renames and state-hoisting across many call sites without changing ranking behavior.
Side B performs a substantial architectural refactor by replacing the cached `GroupState` with `ScopeVotes`, deriving edge weights and connected components on demand (`edges_from_scope`, `scope_components`, `item_index`) and updating persistence, ranking, pairing, UI, and tests to use the new model. Side A fixes an important correctness bug in Rank Centrality by switching to degree-based `d_max` and adds strong regression tests, but B's redesign simplifies state management, removes duplicated cached data, and establishes a more maintainable foundation across the project.
sides
A — c_48aeaf9b52c3 (tommy-mor)
message
[595b3850] Fix star-topology ranking by using degree-based d_max (#146). A pure forward star at the default `>` ratio (2:1) produced uniform 1/3 scores, and the alphabetical-fallback sort placed the unambiguous winner last. Root cause: `compute_scores_from_edges` divided by the max sum of pairwise-normalized weights, so every node ended up with P_ii = 0 — a bipartite Markov chain whose power iteration oscillated and, after the configured even iteration count, returned to the uniform initial state. Switch the divisor to the unweighted max neighbor degree, matching the canonical Rank Centrality definition in Negahban–Oh–Shah 2012 §3.1 (arXiv:1209.1688, eq. defP and the d_max definition in §6). This gives every non-saturated node a positive self-loop, makes the chain aperiodic, and converges the star to π_zebra = 1/2, π_alpha = π_beta = 1/4. Add Rust regression test and a Clojure test that drives the sorterc binary against four .sorter fixtures (star, inverse star, chain, cycle). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
diff preview
diff --git a/server/src/ranking.rs b/server/src/ranking.rs
index 3710c9f64437f5bef3b2121905b6f3bcb7611047..38e6d09b4370e5f8cbae09c0e5760b4e7f1ef7db 100644
--- a/server/src/ranking.rs
+++ b/server/src/ranking.rs
@@ -1,4 +1,4 @@
-use std::collections::HashMap;
+use std::collections::{HashMap, HashSet};
use crate::path_types::ItemId;
use crate::reducer::GroupState;
@@ -143,23 +143,35 @@ fn compute_scores_from_edges(n: usize, edges: impl Iterator<Item = ((usize, usiz
}
}
+ // Rank Centrality (Negahban, Oh, Shah 2012, §3.1):
+ // P_ij = (1/d_max) * A_ij for i ≠ j compared
+ // P_ii = 1 - (1/d_max) * Σ_k A_ik
+ // where d_i is the *degree* (number of distinct neighbors compared) and
+ // d_max = max_i d_i. Using the unweighted degree — not the sum of
+ // pairwise-normalized weights — is what guarantees aperiodicity: it
+ // forces P_ii > 0 for every non-maximum-degree node, and for max-degree
+ // nodes whenever any neighbor weight is below 1 (i.e. not a unanimous
+ // loss). Without this, regular comparison graphs (e.g. a pure star at
+ // ratio 2:1) produce a bipartite chain that oscillates instead of
+ // converging — see issue #146.
let mut out_edges: Vec<Vec<(usize, f64)>> = vec![Vec::new(); n];
- let mut out_deg: Vec<f64> = vec![0.0; n];
+ let mut neighbors: Vec<HashSet<usize>> = vec![HashSet::new(); n];
for ((src, dst), w) in &normalized {
out_edges[*src].push((*dst, *w));
- out_deg[*src] += w;
+ neighbors[*src].insert(*dst);
+ neighbors[*dst].insert(*src);
}
- let mut max_out = 0.0f64;
- for &d in &out_deg {
- if d > max_out {
- max_out = d;
- }
- }
- if max_out <= 1e-12 {
+ let weight_sum: Vec<f64> = out_edges
+ .iter()
+ .map(|es| es.iter().map(|(_, w)| *w).sum())
+ .collect();
+ let d_max = neighbors.iter().map(|s| s.len()).max().unwrap_or(0);
+ if d_max == 0 {
return vec![1.0 / n as f64; n];
}
+ let d_max_f = d_max as f64;
let mut scores = vec![1.0 / n as f64; n];
let mut next = vec![0.0f64; n];
@@ -167,14 +179,14 @@ fn compute_scores_from_edges(n: usize, edges: impl Iterator<Item = ((usize, usiz
for _ in 0..max_iters {
next.fill(0.0);
for i in 0..n {
- let stay_prob = (max_out - out_deg[i]) / max_out;
+ let stay_prob = (d_max_f - weight_sum[i]) / d_max_f;
next[i] += scores[i] * stay_prob;
if out_edges[i].is_empty() {
continue;
}
for &(dst, w) in &out_edges[i] {
- next[dst] += scores[i] * (w / max_out);
+ next[dst] += scores[i] * (w / d_max_f);
}
}
@@ -270,6 +282,38 @@ mod tests {
}
}
+ /// Regression for issue #146: pure forward star at default `>` ratio (2:1).
+ /// Under the old (sum-of-weights) divisor every node had P_ii = 0 and the
+ /// chain was bipartite; power iteration oscillated and returned the
+ /// uniform initial distribution after an even number of steps. Using the
+ /// paper's degree-based d_max gives every node a positive self-loop and
+ /// the chain converges to the correct stationary distribution.
+ #[test]
+ fn star_topology_winner_at_top_via_subset() {
+ let mut g = mk_group();
+ g.apply_vote(vote(1, "zebra", "alpha", 2, 1));
+ g.apply_vote(vote(2, "zebra", "beta", 2, 1));
+
+ let mut items: Vec<(usize, String)> = g
+ .idx_to_item
+ .iter()
+ .enumerate()
+ .map(|(i, it)| (i, it.as_str().to_string()))
+ .collect();
+ items.sort_by(|a, b| a.1.cmp(&b.1));
+ let idxs: Vec<usize> = items.iter().map(|(i, _)| *i).collect();
+
+ let ranked = ranked_items_subset(&g, &idxs, 10000, 1e-8);
+ for r in &ranked {
+ eprintln!("{}: {}", r.item.as_str(), r.score);
+ }
+ assert_eq!(
+ ranked[0].item.as_str(),
+ "https://slug.social/zebra",
+ "zebra won both votes and should rank #1"
+ );
+ }
+
#[test]
fn group_ranking_cache_dirty_flow() {
let mut g = mk_group();
diff --git a/test/fixtures/ranking/chain.sorter b/test/fixtures/ranking/chain.sorter
new file mode 100644
index 0000000000000000000000000000000000000000..912a96bdc08f631be27f3c9afc7e05004a2457c0
--- /dev/null
+++ b/test/fixtures/ranking/chain.sorter
@@ -0,0 +1,10 @@
+#t3
+
+~/t3/a { head of chain }
+~/t3/b { middle }
+~/t3/c { tail }
+
+{ a > b }
+~/t3/a > ~/t3/b
+{ b > c }
+~/t3/b > ~/t3/c
diff --git a/test/fixtures/ranking/cycle.sorter b/test/fixtures/ranking/cycle.sorter
new file mode 100644
index 0000000000000000000000000000000000000000..771731ae176e5d77e00ab89767ff2d7465bb7c6b
--- /dev/null
+++ b/test/fixtures/ranking/cycle.sorter
@@ -0,0 +1,12 @@
+#t4
+
+~/t4/a { node a }
+~/t4/b { node b }
+~/t4/c { node c }
+
+{ a > b }
+~/t4/a > ~/t4/b
+{ b > c }
+~/t4/b > ~/t4/c
+{ c > a }
+~/t4/c > ~/t4/a
diff --git a/test/fixtures/ranking/star.sorter b/test/fixtures/ranking/star.sorter
new file mode 100644
index 0000000000000000000000000000000000000000..135c9f9097d57b73c7ab18fac737ed3598d76fbe
--- /dev/null
+++ b/test/fixtures/ranking/star.sorter
@@ -0,0 +1,11 @@
+#repro
+
+~/repro/zebra { winner — beats both others }
+~/repro/alpha { loser — alphabetically first }
+~/repro/beta { loser — alphabetically middle }
+
+{ zebra beats alpha }
+~/repro/zebra > ~/repro/alpha
+
+{ zebra beats beta }
+~/repro/zebra > ~/repro/beta
diff --git a/test/fixtures/ranking/star_inverse.sorter b/test/fixtures/ranking/star_inverse.sorter
new file mode 100644
index 0000000000000000000000000000000000000000..dab842fa924e5124865ee216b9565f6c7471c812
--- /dev/null
+++ b/test/fixtures/ranking/star_inverse.sorter
@@ -0,0 +1,11 @@
+#t2
+
+~/t2/win { source of incoming edges (loses both) }
+~/t2/loss-a { winner }
+~/t2/loss-b { winner }
+
+{ loss-a beats win }
+~/t2/loss-a > ~/t2/win
+
+{ loss-b beats win }
+~/t2/loss-b > ~/t2/win
diff --git a/test/ranking.clj b/test/ranking.clj
new file mode 100644
index 0000000000000000000000000000000000000000..e1358783a84a264ee633bd79151ba29750b717cf
--- /dev/null
+++ b/test/ranking.clj
@@ -0,0 +1,74 @@
+(ns test.ranking
+ "Drives sorterc on .sorter fixtures and asserts ranking properties.
+
+ Regression coverage for issue #146 — pure forward star at default ratio
+ (2:1 for `>`) used to produce tied uniform scores because the random walk
+ on the normalized edge weights was bipartite. Fixed by switching to the
+ degree-based d_max from Negahban–Oh–Shah rank centrality (§3.1)."
+ (:require [clojure.test :refer [deftest is testing]]
+ [babashka.process :as p]
+ [cheshire.core :as json]
+ [clojure.java.io :as io]))
+
+(def sorterc-bin
+ "Path to the locally-built sorterc binary. Builds on demand if missing."
+ (let [dbg "target/debug/sorterc"
+ release "target/release/sorterc"]
+ (cond
+ (.exists (io/file release)) release
+ (.exists (io/file dbg)) dbg
+ :else
+ (do (println "building sorterc…")
+ (let [r (p/shell {:out :string :err :string :continue true}
+ "cargo build -p sorterc")]
+ (when-not (zero? (:exit r))
+ (throw (ex-info "cargo build -p sorterc failed"
+ {:stderr (:err r)}))))
+ dbg))))
+
+(defn compile-sorter [fixture-path]
+ (let [{:keys [out exit]} (p/shell {:out :string :err :string :continue true}
+ sorterc-bin "compile" fixture-path)]
+ (when-not (zero? exit)
+ (throw (ex-info "sorterc exit nonzero" {:fixture fixture-path :out out})))
+ (json/parse-string out true)))
+
+(defn first-component-ranking [result]
+ (-> result :rankings first :components first :ranking))
+
+(defn item-leaf [item]
+ (last (clojure.string/split item #"/")))
+
+(deftest chain-ranks-head-first
+ (let [ranking (first-component-ranking (compile-sorter "test/fixtures/ranking/chain.sorter"))
+ names (mapv (comp item-leaf :item) ranking)]
+ (is (= ["a" "b" "c"] names)
+ "chain a>b>c should rank a, b, c in order")
+ (is (apply > (map :score ranking))
+ "scores should attenuate strictly down the chain")))
+
+(deftest inverse-star-puts-winners-on-top
+ (let [ranking (first-component-ranking (compile-sorter "test/fixtures/ranking/star_inverse.sorter"))
+ names (mapv (comp item-leaf :item) ranking)]
+ (is (= "win" (last names))
+ "the item that lost to both others should be ranked last")))
+
+(deftest cycle-produces-uniform-scores
+ (let [ranking (first-component-ranking (compile-sorter "test/fixtures/ranking/cycle.sorter"))
+ scores (map :score ranking)]
+ (is (every? #(< (Math/abs (- % 1/3)) 1e-3) scores)
+ "a perfectly symmetric 3-cycle should give every node ~1/3")))
+
+(deftest star-topology-winner-at-top
+ ;; Issue #146 regression: source-only star at default `>` ratio (2:1).
+ ;; Pre-fix produced uniform 1/3 scores; alphabetical fallback put the
+ ;; unambiguous winner at the bottom. Post-fix the chain is aperiodic and
+ ;; converges to π_zebra = 1/2, π_alpha = π_beta = 1/4.
+ (let [ranking (first-component-ranking (compile-sorter "test/fixtures/ranking/star.sorter"))
+ names (mapv (comp item-leaf :item) ranking)
+ by-name (into {} (map (juxt (comp item-leaf :item) :score) ranking))]
+ (is (= "zebra" (first names))
+ "zebra won both votes and should rank #1")
+ (is (< (Math/abs (- (by-name "zebra") 0.5)) 1e-3))
+ (is (< (Math/abs (- (by-name "alpha") 0.25)) 1e-3))
+ (is (< (Math/abs (- (by-name "beta") 0.25)) 1e-3))))
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 omittedHardlinks — judgments / attempts / prompt
judgments
attempts
Prompt text is loaded only by the download route.