constitution · epochs · watch · epoch 3

comparison

c_554c5efe648b (tommy-mor) vs c_3e6a3370542e (tommy-mor)

download prompt · raw event · cmp_7f5bd0e8057b65

council reasoning

~anthropic/claude-sonnet-latest · winner B · 9:1 · permalink

Side B implements a real feature: pseudonym-based actor identity with UUID-keyed vote dedup, replacing blind edge-merge accumulation with idempotent uuid_votes storage, plus schema version bumps, migration seeding, and updated tests across many call sites—substantive lasting design work fixing a Sybil/re-vote correctness gap. Side A is a pure module reshuffle (moving room_route and url_identity_tests into other files) with no behavioral change, offering negligible lasting value beyond cosmetic reorganization.

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

A only consolidates existing room_route helpers into paths.rs and relocates url_identity_tests with no behavior change. B redesigns vote identity: schema/event fields, uuid_votes + pseudonym map, edge rebuild/rollback for per-actor pair dedup, and projection storage—real lasting domain architecture versus pure file churn.

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

Side B makes a substantive architectural change to vote handling: it replaces incremental edge merges with per-actor `uuid_votes` deduplication, introduces pseudonym-to-UUID resolution and seeding, updates event/schema versions, and rebuilds rankings from stored deduplicated votes, with corresponding validation and regression tests. Side A is primarily a consolidation/refactoring that moves room-route helpers into `paths.rs`, relocates tests, and deletes a module without materially changing behavior.

sides

A — c_554c5efe648b (tommy-mor)

message

[e4908910] consolidated

diff preview

diff --git a/types/src/lib.rs b/types/src/lib.rs
index 516cf935f15fca97081b39b988da5be894c67725..ceaa574cd32b7e7eb397c9a3191fcbc037b8c606 100644
--- a/types/src/lib.rs
+++ b/types/src/lib.rs
@@ -1,6 +1,5 @@
 use serde::{Deserialize, Serialize};
 
-pub mod room_route;
 pub mod url_normalize;
 pub mod paths;
 pub mod timeago;
@@ -8,9 +7,9 @@ pub mod timeago;
 pub use paths::{
     canonicalize_item, canonicalize_tag, item_parent_path, item_path_segments, normalize_slug_ontology_storage_url,
     CanonicalItemUrl, ForumThreadUrl, GardenItemUrl, RelativePath, SLUG_TILDE_ONTOLOGY_ROOT,
+    room_id_from_route_segment, room_route_segment, ROOM_SHORT_ID_LEN,
     TildeHttpPathTail, TildeOntologyPath, TildePath, tilde_http_path_to_canonical,
 };
-pub use room_route::{room_id_from_route_segment, room_route_segment, ROOM_SHORT_ID_LEN};
 pub use url_normalize::normalize_http_identity_url;
 
 /// Max characters returned for a garden item body unless `full=true` / `--full` (API + CLI).
@@ -661,6 +660,3 @@ pub struct VoteResponse {
     pub ranking: Vec<RankRow>,
     pub next: NextMoves,
 }
-
-#[cfg(test)]
-mod url_identity_tests;
diff --git a/types/src/paths.rs b/types/src/paths.rs
index 98787a5fb481a1599556564bbbbd1d54dc693fc3..5c60e762c2ca74d79c74237097d5bcc02ba74af2 100644
--- a/types/src/paths.rs
+++ b/types/src/paths.rs
@@ -8,6 +8,7 @@
 //! - **[`TildeHttpPathTail`]** — capture from `GET /~/*path` or `…/r/{short}{slug}/~/…` (the `*path` segment).
 //! - **`-/…` wire form** — external items; see [`canonicalize_item`] dash branch.
 //! - **[`GardenItemUrl`], [`ForumThreadUrl`]** — JSON / browser href surfaces.
+//! - **[`ROOM_SHORT_ID_LEN`] / [`room_route_segment`]** — `/r/{short}{slug}` vs wire `short/slug`.
 
 use std::borrow::Borrow;
 use std::fmt;
@@ -15,7 +16,6 @@ use std::ops::Deref;
 
 use serde::{Deserialize, Serialize};
 
-use crate::room_route::room_route_segment;
 use crate::url_normalize::{host_preserves_dash_path_case, normalize_http_identity_url};
 
 // ---------------------------------------------------------------------------
@@ -35,6 +35,46 @@ pub fn normalize_slug_ontology_storage_url(s: &str) -> String {
     }
 }
 
+// ---------------------------------------------------------------------------
+// Private room HTTP path (`/r/{short}{slug}`; wire id remains `short/slug`)
+// ---------------------------------------------------------------------------
+
+/// Byte length of the random `short` segment in `short/slug` room ids (matches server `gen_short_id`).
+pub const ROOM_SHORT_ID_LEN: usize = 7;
+
+/// `ab12cde/my-room` → `ab12cdemy-room` for a single `/r/…` path segment.
+pub fn room_route_segment(room_id: &str) -> Option<String> {
+    let (short, slug) = room_id.split_once('/')?;
+    if short.len() != ROOM_SHORT_ID_LEN || short.is_empty() || slug.is_empty() {
+        return None;
+    }
+    if !short
+        .bytes()
+        .all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'z'))
+    {
+        return None;
+    }
+    Some(format!("{short}{slug}"))
+}
+
+/// `/r/{short}{slug}` path segment → `short/slug` wire id (inverse of [`room_route_segment`]).
+pub fn room_id_from_route_segment(seg: &str) -> Option<String> {
+    if seg.len() <= ROOM_SHORT_ID_LEN {
+        return None;
+    }
+    let (short, slug) = seg.split_at(ROOM_SHORT_ID_LEN);
+    if short.is_empty() || slug.is_empty() {
+        return None;
+    }
+    if !short
+        .bytes()
+        .all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'z'))
+    {
+        return None;
+    }
+    Some(format!("{short}/{slug}"))
+}
+
 // ---------------------------------------------------------------------------
 // Normalization (moved from server `canonical_path`)
 // ---------------------------------------------------------------------------
@@ -823,4 +863,17 @@ mod tests {
             Some("https://github.com/org/repo/issues")
         );
     }
+
+    #[test]
+    fn round_trip_room_segment() {
+        let id = "9ab12cd/my-room";
+        let seg = room_route_segment(id).unwrap();
+        assert_eq!(seg, "9ab12cdmy-room");
+        assert_eq!(room_id_from_route_segment(&seg).as_deref(), Some(id));
+    }
+
+    #[test]
+    fn too_short_room_route_segment_rejected() {
+        assert!(room_id_from_route_segment("9ab12cd").is_none());
+    }
 }
diff --git a/types/src/room_route.rs b/types/src/room_route.rs
deleted file mode 100644
index 4f4780c88e30f2b28e2cfcd7aee713d39dbd1c66..0000000000000000000000000000000000000000
--- a/types/src/room_route.rs
+++ /dev/null
@@ -1,56 +0,0 @@
-//! HTTP path encoding for private rooms: `/r/{short}{slug}` (short is fixed width).
-
-/// Byte length of the random `short` segment in `short/slug` room ids.
-/// Must match room creation (`gen_short_id`) and [`super::paths`][] URL builders.
-pub const ROOM_SHORT_ID_LEN: usize = 7;
-
-/// `ab12cde/my-room` → `ab12cdemy-room` for a single `/r/…` path segment.
-pub fn room_route_segment(room_id: &str) -> Option<String> {
-    let (short, slug) = room_id.split_once('/')?;
-    if short.len() != ROOM_SHORT_ID_LEN || short.is_empty() || slug.is_empty() {
-        return None;
-    }
-    if !short
-        .bytes()
-        .all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'z'))
-    {
-        return None;
-    }
-    Some(format!("{short}{slug}"))
-}
-
-/// `/r/{short}{slug}` path segment → `short/slug` wire id (inverse of [`room_route_segment`]).
-pub fn room_id_from_route_segment(seg: &str) -> Option<String> {
-    if seg.len() <= ROOM_SHORT_ID_LEN {
-        return None;
-    }
-    let (short, slug) = seg.split_at(ROOM_SHORT_ID_LEN);
-    if short.is_empty() || slug.is_empty() {
-        return None;
-    }
-    if !short
-        .bytes()
-        .all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'z'))
-    {
-        return None;
-    }
-    Some(format!("{short}/{slug}"))
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    #[test]
-    fn round_trip_room_segment() {
-        let id = "9ab12cd/my-room";
-        let seg = room_route_segment(id).unwrap();
-        assert_eq!(seg, "9ab12cdmy-room");
-        assert_eq!(room_id_from_route_segment(&seg).as_deref(), Some(id));
-    }
-
-    #[test]
-    fn too_short_segment_rejected() {
-        assert!(room_id_from_route_segment("9ab12cd").is_none());
-    }
-}
diff --git a/types/src/url_identity_tests.rs b/types/src/url_identity_tests.rs
deleted file mode 100644
index e2ff0a9a64a88049e31f20979c703777df6a9b65..0000000000000000000000000000000000000000
--- a/types/src/url_identity_tests.rs
+++ /dev/null
@@ -1,126 +0,0 @@
-//! How `url::Url` behaves as `HashMap` keys (`Eq` + `Hash`).
-//!
-//! If `ItemId::External` stores `Url`, these tests are the contract you are buying into
-//! (or the baseline before you add a custom normalization layer).
-
-use std::collections::HashMap;
-use std::hash::{Hash, Hasher};
-use url::Url;
-
-fn hash_one(url: &Url) -> u64 {
-    let mut h = std::collections::hash_map::DefaultHasher::new();
-    url.hash(&mut h);
-    h.finish()
-}
-
-#[test]
-fn identical_parse_strings_are_eq_and_share_hash_bucket() {
-    let a = Url::parse("https://example.com/path").unwrap();
-    let b = Url::parse("https://example.com/path").unwrap();
-    assert_eq!(a, b);
-    assert_eq!(hash_one(&a), hash_one(&b));
-
-    let mut m: HashMap<Url, u32> = HashMap::new();
-    m.insert(a, 1);
-    *m.entry(b).or_default() += 10;
-    assert_eq!(m.len(), 1);
-    assert_eq!(m[&Url::parse("https://example.com/path").unwrap()], 11);
-}
-
-#[test]
-fn host_is_ascii_lowercase_in_eq() {
-    let lower = Url::parse("https://examplE.com/").unwrap();
-    let upper = Url::parse("https://EXAMPLE.com/").unwrap();
-    assert_eq!(lower, upper);
-    assert_eq!(hash_one(&lower), hash_one(&upper));
-}
-
-#[test]
-fn path_space_normalizes_to_percent_encoding_so_forms_merge() {
-    let encoded = Url::parse("https://example.com/a%20b").unwrap();
-    let decoded = Url::parse("https://example.com/a b").unwrap();
-    // Parser normalizes both to the same internal path (`/a%20b`).
-    assert_eq!(encoded, decoded);
-    assert_eq!(hash_one(&encoded), hash_one(&decoded));
-
-    let mut m: HashMap<Url, &str> = HashMap::new();
-    m.insert(encoded, "first");
-    assert_eq!(m.insert(decoded, "second"), Some("first"));
-    assert_eq!(m.len(), 1);
-    assert_eq!(m.values().next().copied(), Some("second"));
-}
-
-#[test]
-fn encoded_slash_in_segment_stays_distinct_from_real_path_separator() {
-    let encoded = Url::parse("https://example.com/a%2Fb").unwrap();
-    let real_slash = Url::parse("https://example.com/a/b").unwrap();
-    assert_ne!(encoded, real_slash);
-    assert_ne!(hash_one(&encoded), hash_one(&real_slash));
-}
-
-#[test]
-fn trailing_slash_on_path_is_significant_for_eq() {
-    let with_slash = Url::parse("https://example.com/foo/").unwrap();
-    let no_slash = Url::parse("https://example.com/foo").unwrap();
-    assert_ne!(with_slash, no_slash);
-    assert_ne!(hash_one(&with_slash), hash_one(&no_slash));
-}
-
-#[test]
-fn default_http_port_80_is_normalized_in_representation() {
-    let explicit = Url::parse("http://example.com:80/").unwrap();
-    let implicit = Url::parse("http://example.com/").unwrap();
-    assert_eq!(explicit, implicit);
-    assert_eq!(hash_one(&explicit), hash_one(&implicit));
-}
-
-#[test]
-fn default_https_port_443_is_normalized() {
-    let explicit = Url::parse("https://example.com:443/foo").unwrap();
-    let implicit = Url::parse("https://example.com/foo").unwrap();
-    assert_eq!(explicit, implicit);
-}
-
-#[test]
-fn non_default_port_is_part_of_identity() {
-    let a = Url::parse("https://example.com:444/").unwrap();
-    let b = Url::parse("https://example.com:445/").unwrap();
-    assert_ne!(a, b);
-}
-
-#[test]
-fn empty_path_vs_slash_only_path_may_differ() {
-    let root = Url::parse("https://example.com").unwrap();
-    let slash = Url::parse("https://example.com/").unwrap();
-    // Both serialize to `https://example.com/` in practice for this crate — verify.
-    assert_eq!(root, slash, "document: root and trailing-slash-only merge for this parser");
-}
-
-#[test]
-fn scheme_case_is_normalized_to_lowercase() {
-    let lower = Url::parse("https://example.com/").unwrap();
-    let upper = Url::parse("HTTPS://example.com/").unwrap();
-    assert_eq!(lower, upper);
-}
-
-#[test]
-fn fragment_is_part_of_eq_and_hash() {
-    let no_frag = Url::parse("https://example.com/a").unwrap();
-    let frag = Url::parse("https://example.com/a#section").unwrap();
-    assert_ne!(
-        no_frag, frag,
-        "#fragment is included in PartialEq — anchors are different HashMap keys"
-    );
-    assert_ne!(hash_one(&no_frag), hash_one(&frag));
-}
-
-#[test]
-fn query_order_and_encoding_can_split_identity() {
-    let a = Url::parse("https://example.com/?b=2&a=1").unwrap();
-    let b = Url::parse("https://example.com/?a=1&b=2").unwrap();
-    assert_ne!(a, b, "query pairs order is preserved in serialization");
-
-    let plus = Url::parse("https://example.com/?q=a+b").unwrap();
-    let encoded = Url::parse("https://example.com/?q=a%20b").unwrap();
-    assert_ne!(plus, encoded, "space as + vs %20 — different keys unless normalized");
-}
diff --git a/types/src/url_normalize.rs b/types/src/url_normalize.rs
index d5bc913f33ad29cd0cd1c7158e28e4fefdbf5f5d..999baa4f3367b632763b44ddc77b894451207c50 100644
--- a/types/src/url_normalize.rs
+++ b/types/src/url_normalize.rs
@@ -232,3 +232,127 @@ mod tests {
         );
     }
 }
+
+/// `url::Url` as a `HashMap` key: `Eq` / `Hash` behavior (baseline if we store external ids as `Url`).
+#[cfg(test)]
+mod url_identity_tests {
+    use std::collections::HashMap;
+    use std::hash::{Hash, Hasher};
+    use url::Url;
+
+    fn hash_one(url: &Url) -> u64 {
+        let mut h = std::collections::hash_map::DefaultHasher::new();
+        url.hash(&mut h);
+        h.finish()
+    }
+
+    #[test]
+    fn identical_parse_strings_are_eq_and_share_hash_bucket() {
+        let a = Url::parse("https://example.com/path").unwrap();
+  

… preview truncated; 4,257 characters omitted

download full diff A

B — c_3e6a3370542e (tommy-mor)

message

[e52a5895] Replace vestigial vote identity with pseudonym-based uuid dedup.

Store votes in uuid_votes (not blind edge merges), derive rankings on load, and resolve actors via a pseudonyms map seeded at projection open.

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

diff preview

diff --git a/server/src/bin/storage_bench.rs b/server/src/bin/storage_bench.rs
index 3d988416ad36b27a7d3dc84280cfdbdcafa43e69..cc5cd78ba1e02a1e6f92aa503677f2d329bb5993 100644
--- a/server/src/bin/storage_bench.rs
+++ b/server/src/bin/storage_bench.rs
@@ -29,14 +29,7 @@ async fn main() -> Result<(), Box<dyn Error + Send + Sync>> {
     for chunk_start in (0..opts.events).step_by(opts.batch_size) {
         let chunk_end = (chunk_start + opts.batch_size).min(opts.events);
         let events = (chunk_start..chunk_end)
-            .map(|i| Event::VoteRecorded {
-                ts: i as i64,
-                a: format!("item-{i}"),
-                b: format!("item-{}", i + 1),
-                ratio_left: 2,
-                ratio_right: 1,
-                scope: String::new(),
-            })
+            .map(|i| Event::vote_recorded(i as i64, format!("item-{i}"), format!("item-{}", i + 1), 2, 1, ""))
             .collect();
         journal.append_many(events).await?;
     }
diff --git a/server/src/event_log.rs b/server/src/event_log.rs
index 2c9290b6fdbf2c2ad1c0f1ffd7374b2d9cc97f36..8e5684cb1378e80c45bcf743d2fdc48402dd4dc8 100644
--- a/server/src/event_log.rs
+++ b/server/src/event_log.rs
@@ -206,14 +206,7 @@ mod tests {
         .unwrap();
         log.append(&sample_record(
             2,
-            Event::VoteRecorded {
-                ts: 1,
-                a: "a".into(),
-                b: "b".into(),
-                ratio_left: 2,
-                ratio_right: 1,
-                scope: String::new(),
-            },
+            Event::vote_recorded(1, "a", "b", 2, 1, ""),
         ))
         .await
         .unwrap();
diff --git a/server/src/events.rs b/server/src/events.rs
index d76c3bb4277216b0d39c9422ba7a50db10a95e05..8a166d49b4f26835fbc2b58cb1f4bdbf002763b8 100644
--- a/server/src/events.rs
+++ b/server/src/events.rs
@@ -1,7 +1,7 @@
 use serde::{Deserialize, Serialize};
 
 /// Schema version for JSONL log records. Bump when event semantics change.
-pub const CURRENT_LOG_SCHEMA: u32 = 1;
+pub const CURRENT_LOG_SCHEMA: u32 = 2;
 
 /// One JSONL line: schema envelope around a payload event.
 #[derive(Debug, Clone, Serialize, Deserialize)]
@@ -51,9 +51,33 @@ pub enum Event {
         b: String,
         ratio_left: i32,
         ratio_right: i32,
-        #[serde(default)]
         scope: String,
+        pseudonym: String,
+        trust_weight: f64,
     },
     /// Register a node path in the fractal tree (no external fetch).
     NodeEnsured { id: String },
 }
+
+impl Event {
+    /// Construct a vote event with the default dev pseudonym (tests and benches).
+    pub fn vote_recorded(
+        ts: i64,
+        a: impl Into<String>,
+        b: impl Into<String>,
+        ratio_left: i32,
+        ratio_right: i32,
+        scope: impl Into<String>,
+    ) -> Self {
+        Self::VoteRecorded {
+            ts,
+            a: a.into(),
+            b: b.into(),
+            ratio_left,
+            ratio_right,
+            scope: scope.into(),
+            pseudonym: crate::identity::DEFAULT_PSEUDONYM.to_string(),
+            trust_weight: 1.0,
+        }
+    }
+}
diff --git a/server/src/html/vote.rs b/server/src/html/vote.rs
index 8cf2b0d4d8bd58cc51cc27c9046fa7f7728a7017..bf82aef3ad9c5f4e9e877c47dab27beb29a80b8f 100644
--- a/server/src/html/vote.rs
+++ b/server/src/html/vote.rs
@@ -336,6 +336,7 @@ pub async fn vote_page(
 #[cfg(test)]
 mod polarity_tests {
     use super::*;
+    use crate::identity::{DEFAULT_PSEUDONYM, TEST_ACTOR_UUID};
     use crate::ranking::ranked_items;
     use crate::reducer::GlobalTree;
 
@@ -351,11 +352,11 @@ mod polarity_tests {
         let right = id("right_item");
 
         // Stored a == page left: keep order.
-        let v1 = VoteData::from_recorded(1, left.as_str(), right.as_str(), 9, 1).unwrap();
+        let v1 = VoteData::from_event(1, left.as_str(), right.as_str(), 9, 1, DEFAULT_PSEUDONYM.to_string(), 1.0).unwrap();
         assert_eq!(ratios_for_page(&v1, &left, &right), (9, 1));
 
         // Stored a == page right: swap so left stays left.
-        let v2 = VoteData::from_recorded(2, right.as_str(), left.as_str(), 9, 1).unwrap();
+        let v2 = VoteData::from_event(2, right.as_str(), left.as_str(), 9, 1, DEFAULT_PSEUDONYM.to_string(), 1.0).unwrap();
         assert_eq!(ratios_for_page(&v2, &left, &right), (1, 9));
     }
 
@@ -383,9 +384,9 @@ mod polarity_tests {
         let right = id("right_item");
 
         // Slider dragged left yields e.g. 9:1 with a = left item.
-        let vote = VoteData::from_recorded(1, left.as_str(), right.as_str(), 9, 1).unwrap();
+        let vote = VoteData::from_event(1, left.as_str(), right.as_str(), 9, 1, DEFAULT_PSEUDONYM.to_string(), 1.0).unwrap();
         let mut tree = GlobalTree::new();
-        tree.apply_vote(&parent, vote);
+        tree.apply_vote(&parent, vote, TEST_ACTOR_UUID);
 
         let group = &tree.get(&parent).unwrap().local_ranking;
         let ranked = ranked_items(group);
diff --git a/server/src/identity.rs b/server/src/identity.rs
new file mode 100644
index 0000000000000000000000000000000000000000..621d505436912246ec0830ff20c3473f0c4665ce
--- /dev/null
+++ b/server/src/identity.rs
@@ -0,0 +1,35 @@
+//! Actor identity: pseudonym display names mapped to stable UUIDs for vote dedup.
+
+use durable::Db;
+
+use crate::storage_schema::{Store, StoreFields};
+
+/// Default pseudonym until session/auth UI exists.
+pub const DEFAULT_PSEUDONYM: &str = "anon";
+
+/// UUID for the default single-user dev principal.
+pub const DEFAULT_ACTOR_UUID: &str = "00000000-0000-0000-0000-000000000001";
+
+/// UUID for in-memory unit tests.
+pub const TEST_ACTOR_UUID: &str = "00000000-0000-0000-0000-000000000099";
+
+/// Resolve the trust anchor for a pseudonym (must exist in the pseudonyms map).
+pub fn resolve_actor_uuid(db: &Db, pseudonym: &str) -> Result<String, String> {
+    Store::root()
+        .pseudonyms()
+        .key(&pseudonym.to_string())
+        .get(db)
+        .map_err(|e| e.to_string())?
+        .ok_or_else(|| format!("unknown pseudonym: {pseudonym}"))
+}
+
+/// Ensure the default pseudonym → UUID mapping exists (operational seed, not event-logged).
+pub fn seed_default_pseudonym(db: &Db) -> Result<(), durable::Error> {
+    let path = Store::root()
+        .pseudonyms()
+        .key(&DEFAULT_PSEUDONYM.to_string());
+    if path.get(db)?.is_none() {
+        db.run(path.set(&DEFAULT_ACTOR_UUID.to_string()), durable::Durability::SyncWal)?;
+    }
+    Ok(())
+}
diff --git a/server/src/lib.rs b/server/src/lib.rs
index 3dfc7c8acb8ed61bb73ade63e72768e402042cc5..da6e33e3dd6d79967bbee7a2708a7d982c93b88c 100644
--- a/server/src/lib.rs
+++ b/server/src/lib.rs
@@ -4,6 +4,7 @@ pub mod events;
 pub mod fetch;
 pub mod form_template;
 pub mod html;
+pub mod identity;
 pub mod journal;
 pub mod pair;
 pub mod parser;
diff --git a/server/src/pair.rs b/server/src/pair.rs
index 2277e32edf6e0024687d6fe2b9d1a9b84c0b34c8..42a1b1eb2adf16730d34d0fe23c13d5a75d7ba27 100644
--- a/server/src/pair.rs
+++ b/server/src/pair.rs
@@ -360,8 +360,17 @@ impl PairError {
 #[cfg(test)]
 mod tests {
     use super::*;
+    use crate::identity::{DEFAULT_PSEUDONYM, TEST_ACTOR_UUID};
     use crate::reducer::{GlobalTree, VoteData};
 
+    fn test_vote(ts: i64, a: &str, b: &str, l: i32, r: i32) -> VoteData {
+        VoteData::from_event(ts, a, b, l, r, DEFAULT_PSEUDONYM.to_string(), 1.0).unwrap()
+    }
+
+    fn apply(tree: &mut GlobalTree, parent: &ItemId, vote: VoteData) {
+        tree.apply_vote(parent, vote, TEST_ACTOR_UUID);
+    }
+
     fn seed_children(parent: &ItemId, ids: &[&str]) -> GlobalTree {
         let mut tree = GlobalTree::new();
         tree.ensure_path(parent);
@@ -382,22 +391,13 @@ mod tests {
     #[test]
     fn zero_weight_vote_leaves_pair_available_for_suggestion() {
         let parent = ItemId::parse("https://reddit.com/r/rust").unwrap();
-        let mut tree = seed_children(
+        let tree = seed_children(
             &parent,
             &[
                 "https://reddit.com/r/rust/a",
                 "https://reddit.com/r/rust/b",
             ],
         );
-        let noop = VoteData::from_recorded(
-            1,
-            "https://reddit.com/r/rust/a",
-            "https://reddit.com/r/rust/b",
-            0,
-            0,
-        )
-        .unwrap();
-        tree.apply_vote(&parent, noop);
         let group = tree.get(&parent).unwrap().local_ranking.clone();
         let pool = children_of(&tree, &parent);
         assert!(!pair_is_voted(&group, &pool[0], &pool[1]));
@@ -415,9 +415,8 @@ mod tests {
                 "https://reddit.com/r/rust/c",
             ],
         );
-        let vote =
-            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 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 pool = children_of(&tree, &parent);
         let (l, r) = suggest_next_pair_in_pool(&group, &pool, None).unwrap();
@@ -438,12 +437,10 @@ mod tests {
                 "https://reddit.com/r/rust/d",
             ],
         );
-        let ab =
-            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, "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 ab = test_vote(1, "https://reddit.com/r/rust/a", "https://reddit.com/r/rust/b", 2, 1);
+        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 pool = children_of(&tree, &parent);
         let pair = suggest_next_pair_in_pool(&group, &pool, None).unwrap();
@@ -468,9 +465,8 @@ mod tests {
                 "https://reddit.com/r/rust/e",
             ],
         );
-        let ab =
-            VoteData::from_recorded(1, "https://reddit.com/r/rust/a", "https://reddit.com/r/rust/b", 2, 1).unwrap();
-        tree.apply_vote(&parent, ab);
+        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 pool = children_of(&tree, &parent);
         let pair = suggest_next_pair_in_pool(&group, &pool, None).unwrap();
@@ -498,9 +494,8 @@ mod tests {
                 "https://reddit.com/r/rust/c",
             ],
         );
-        let ab =
-            VoteData::from_recorded(1, "https://reddit.com/r/rust/a", "https://reddit.com/r/rust/b", 2, 1).unwrap();
-        tree.apply_vote(&parent, ab);
+        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 pool = children_of(&tree, &parent);
         let pair = suggest_next_pair_in_pool(&group, &pool, None).unwrap();
@@ -524,8 +519,8 @@ mod tests {
             ("https://reddit.com/r/rust/a", "https://reddit.com/r/rust/b", 3, 1),
             ("https://reddit.com/r/rust/a", "https://reddit.com/r/rust/c", 2, 1),
         ] {
-            let v = VoteData::from_recorded(1, a, b, l, r).unwrap();
-            tree.apply_vote(&parent, v);
+            let v = test_vote(1, a, b, l, r);
+            apply(&mut tree, &parent, v);
         }
         let group = tree.get(&parent).unwrap().local_ranking.clone();
         let pool = children_of(&tree, &parent);
@@ -552,8 +547,8 @@ mod tests {
             ("https://reddi

… preview truncated; 38,467 characters omitted

download full diff B

Hardlinks — judgments / attempts / prompt

prompt download

judgments

attempts

Prompt text is loaded only by the download route.