constitution · epochs · watch · epoch 3

comparison

c_d6d339485601 (tommy-mor) vs c_a896b2dc05d5 (tommy-mor)

download prompt · raw event · cmp_ca549f41db8ec1

council reasoning

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

Side B makes a durable design change by replacing the persisted recent-vote deque with an append-only list, removing post-apply trimming, updating loading logic to enforce the cap at query time, and adding a test verifying only the newest 200 votes are exposed. Side A is a useful CLI enhancement that formats and displays existing connectivity statistics with tests, but it primarily improves presentation rather than changing core project behavior or storage semantics.

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

Side A adds a small, tested, low-risk CLI feature that surfaces existing connectivity data, improving usability without touching persistence layout. Side B refactors the recent-votes storage from a capped Deque to an unbounded List, removing the write-time trim entirely and only capping at read time—this risks unbounded on-disk growth of recent_votes and bumps the schema version, a riskier tradeoff than the modest but safe UI addition in A.

~x-ai/grok-latest · winner B · 2:5 · permalink

B is a lasting core-design change: recent_votes moves from capped Deque/VecDeque with write-time trim (and post-apply trim_recent_votes over vote_parents) to append-only List/Vec with query-time cap, schema bump, and a real load-cap test. A only formats already-available ConnectivityStats in the CLI plus two string-format unit tests—useful UX, but not structural.

openai/gpt-5.3-chat · winner B · 3:1 · permalink

B makes a fundamental storage-model change from a capped deque to an append-only list with query-time capping, removes write-time trimming, updates schema version, and adjusts reconstruction logic—affecting persistence semantics and correctness. A only adds CLI formatting and tests to display existing connectivity stats without changing core behavior.

openai/gpt-5.2-chat · winner B · 4:1 · permalink

Side B makes a substantive architectural change from a capped `Deque` to an append-only `List` with query-time capping, updating reducer logic, storage schema (schema version bump 3→4), write path (`push_front`→`push`), read path slicing, and adding a test to enforce the new cap semantics. Side A only adds CLI formatting and tests to display existing `ConnectivityStats`, which is useful but purely presentational and does not alter core behavior.

sides

A — c_d6d339485601 (tommy-mor)

message

[14749a34] Show graph topology with pair suggestions

Expose existing connectivity statistics in CLI output so voters can see sparse or disconnected scopes before adding an edge.

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

diff preview

diff --git a/cli/src/main.rs b/cli/src/main.rs
index 70435b412188a151c5e89e842a5de57f7480ddf2..abb5a55b49f60fe28fbfd4ec02715cb94ea0b4ec 100644
--- a/cli/src/main.rs
+++ b/cli/src/main.rs
@@ -397,6 +397,29 @@ fn print_item_response(resp: &ItemResponse) {
     }
 }
 
+fn format_connectivity_stats(stats: &ConnectivityStats) -> String {
+    let density = if stats.pairs_possible == 0 {
+        0.0
+    } else {
+        stats.pairs_voted as f64 / stats.pairs_possible as f64 * 100.0
+    };
+    let component_label = if stats.components == 1 { "component" } else { "components" };
+    let connection_status = if stats.comparisons_until_connected == 0 {
+        "connected".to_string()
+    } else {
+        let comparison_label = if stats.comparisons_until_connected == 1 {
+            "comparison"
+        } else {
+            "comparisons"
+        };
+        format!("{} {comparison_label} to connect", stats.comparisons_until_connected)
+    };
+    format!(
+        "graph: {} items, {}/{} pairs ({density:.1}% density), {} {component_label}, {connection_status}",
+        stats.items, stats.pairs_voted, stats.pairs_possible, stats.components,
+    )
+}
+
 fn print_pair_response(resp: &PairResponse) {
     println!("{}  vs  {}", resp.left, resp.right);
     if let Some(b) = &resp.left_body {
@@ -413,6 +436,10 @@ fn print_pair_response(resp: &PairResponse) {
         println!();
         println!("threads: {}", resp.threads.iter().map(|t| format!("#{t}")).collect::<Vec<_>>().join(" "));
     }
+    if let Some(stats) = &resp.connectivity {
+        println!();
+        println!("{}", format_connectivity_stats(stats));
+    }
     println!();
     println!("---");
     println!("Before writing your comparison, talk to your human. Ask them about their relationship to these two items — which one has meant more to them, whether their perspective has changed over time, and where they land now. Listen to what they say. Then write a comparison that synthesizes their perspective with your own analysis. The best submissions on slug.social carry both voices — your reasoning and their experience.");
@@ -816,6 +843,43 @@ fn write_secret_file(name: &str, contents: &str) -> Result<()> {
     Ok(())
 }
 
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn connectivity_stats_show_sparse_disconnected_graph() {
+        let stats = ConnectivityStats {
+            items: 9,
+            components: 3,
+            comparisons_until_connected: 2,
+            pairs_voted: 8,
+            pairs_possible: 36,
+        };
+
+        assert_eq!(
+            format_connectivity_stats(&stats),
+            "graph: 9 items, 8/36 pairs (22.2% density), 3 components, 2 comparisons to connect"
+        );
+    }
+
+    #[test]
+    fn connectivity_stats_show_connected_graph() {
+        let stats = ConnectivityStats {
+            items: 4,
+            components: 1,
+            comparisons_until_connected: 0,
+            pairs_voted: 3,
+            pairs_possible: 6,
+        };
+
+        assert_eq!(
+            format_connectivity_stats(&stats),
+            "graph: 4 items, 3/6 pairs (50.0% density), 1 component, connected"
+        );
+    }
+}
+
 async fn run_scoped(base: &str, room: &str, sub: ScopedCmd) -> Result<()> {
     let room = room.trim();
     let client = http_client()?;

download full diff A

B — c_a896b2dc05d5 (tommy-mor)

message

[1531154d] dequeue -> vec

diff preview

diff --git a/server/src/projection_apply.rs b/server/src/projection_apply.rs
index 9c8990a8af927f35d3344c8d0872a516aba56b86..ad404bacb8bcdd5ae0e682cff97f974fd44528ea 100644
--- a/server/src/projection_apply.rs
+++ b/server/src/projection_apply.rs
@@ -6,8 +6,6 @@
 //! batch as the (non-idempotent) edge merges guarantees exactly-once application
 //! across replay.
 
-use std::collections::BTreeSet;
-
 use crate::{
     event_log::EventLogError,
     events::{Event, EventRecord},
@@ -44,7 +42,6 @@ pub fn apply_records(
 
     let db = projection_store.db();
     let mut batch = db.batch();
-    let mut vote_parents: BTreeSet<ItemId> = BTreeSet::new();
     let mut last_seq = 0u64;
 
     for record in records {
@@ -70,7 +67,6 @@ pub fn apply_records(
                     *ts,
                 )
                 .map_err(|e| EventLogError::Apply(e.to_string()))?;
-                vote_parents.insert(parent);
             }
             Event::NodeEnsured { id } => {
                 let parsed = parse_event_id(id)?;
@@ -85,11 +81,5 @@ pub fn apply_records(
         .commit_with(durable::Durability::DisableWal)
         .map_err(|e| EventLogError::Apply(e.to_string()))?;
 
-    for parent in vote_parents {
-        projection_store
-            .trim_recent_votes(&parent)
-            .map_err(|e| EventLogError::Apply(e.to_string()))?;
-    }
-
     Ok(())
 }
diff --git a/server/src/projection_store.rs b/server/src/projection_store.rs
index 8576d671f351004426207894ac35594ddb0f70cf..9a8953d010029d3639dc3987687554bab8b7663e 100644
--- a/server/src/projection_store.rs
+++ b/server/src/projection_store.rs
@@ -18,7 +18,7 @@ use crate::{
 
 const PROJECTION_CURSOR_KEY: &str = "cursor";
 const PROJECTION_SCHEMA_KEY: &str = "schema_version";
-const PROJECTION_SCHEMA_VERSION: u64 = 3;
+const PROJECTION_SCHEMA_VERSION: u64 = 4;
 
 #[derive(Debug, thiserror::Error)]
 pub enum ProjectionStoreError {
@@ -142,16 +142,6 @@ impl ProjectionStore {
         Ok(tree)
     }
 
-    /// Cap a node's recent-vote window after applying votes (best-effort, blind).
-    pub(crate) fn trim_recent_votes(&self, parent: &ItemId) -> Result<(), ProjectionStoreError> {
-        node(parent).recent_votes().truncate_back(
-            &self.db,
-            crate::storage_schema::RECENT_VOTES_CAP,
-            Durability::DisableWal,
-        )?;
-        Ok(())
-    }
-
     /// Cache Reddit display content outside the event log (must be evicted per policy).
     pub fn put_ephemeral_content(
         &self,
diff --git a/server/src/reducer.rs b/server/src/reducer.rs
index 0c75c85150bb9e5f578bbadf58b3e43f8a80be4b..759918b8c0eb8f8bf1ed0911d8877adaa55c8ea6 100644
--- a/server/src/reducer.rs
+++ b/server/src/reducer.rs
@@ -1,4 +1,4 @@
-use std::collections::{HashMap, HashSet, VecDeque};
+use std::collections::{HashMap, HashSet};
 
 use serde::{Deserialize, Serialize};
 
@@ -52,7 +52,7 @@ pub struct GroupState {
     pub idx_to_item: Vec<ItemId>,
     pub edges: HashMap<(usize, usize), f64>,
     pub voted_pairs: HashSet<(usize, usize)>,
-    pub recent_votes: VecDeque<VoteData>,
+    pub recent_votes: Vec<VoteData>,
 }
 
 impl GroupState {
@@ -62,7 +62,7 @@ impl GroupState {
             idx_to_item: Vec::new(),
             edges: HashMap::new(),
             voted_pairs: HashSet::new(),
-            recent_votes: VecDeque::with_capacity(200),
+            recent_votes: Vec::new(),
         }
     }
 
@@ -111,10 +111,7 @@ impl GroupState {
         self.add_edge_weight(b_idx, a_idx, w_a);
         self.add_edge_weight(a_idx, b_idx, w_b);
 
-        self.recent_votes.push_front(vote);
-        while self.recent_votes.len() > 200 {
-            self.recent_votes.pop_back();
-        }
+        self.recent_votes.push(vote);
     }
 }
 
diff --git a/server/src/storage_dto.rs b/server/src/storage_dto.rs
index 9dfb13c53efe4389277625a6ab3bfc18f566a453..3fd6db5cb909ac4896bd8a3ecace796de5f08781 100644
--- a/server/src/storage_dto.rs
+++ b/server/src/storage_dto.rs
@@ -39,7 +39,7 @@ pub struct StoredEntityDataV1 {
     pub link_url: Option<String>,
 }
 
-/// One vote stored in a node's `recent_votes` deque.
+/// One vote stored in a node's `recent_votes` list.
 #[derive(Debug, Clone, Serialize, Deserialize)]
 pub struct StoredVoteV1 {
     pub version: u32,
diff --git a/server/src/storage_schema.rs b/server/src/storage_schema.rs
index bd26e665e084b95b10fdfff091c31e8dc84d07b8..5d2bb1d56927fb61c7c6d2d8602bd6882327f862 100644
--- a/server/src/storage_schema.rs
+++ b/server/src/storage_schema.rs
@@ -2,13 +2,13 @@
 //! durable collections instead of one blob per node.
 //!
 //! A vote updates a handful of keys: a few edge-weight merges, a voted-pair flag,
-//! a recent-vote deque push, and child-link set entries. The in-memory
+//! a recent-vote list append, and child-link set entries. The in-memory
 //! [`crate::reducer::GroupState`] is reconstructed from these keys on read for
 //! rank-centrality.
 
 use std::collections::{BTreeSet, HashMap, HashSet};
 
-use durable::{Batch, Db, Deque, Durable, Leaf, Map, Sum};
+use durable::{Batch, Db, Durable, Leaf, List, Map, Sum};
 
 use crate::{
     path_types::ItemId,
@@ -38,8 +38,8 @@ pub struct NodeSchema {
     pub edges: Map<EdgeKey, Sum<f64>>,
     /// Voted pairs `(min, max) -> true`.
     pub voted_pairs: Map<PairKey, Leaf<bool>>,
-    /// Recent votes, newest at the front (capped on write).
-    pub recent_votes: Deque<Leaf<StoredVoteV1>>,
+    /// Recent votes, append-only oldest-first (cap applied on read).
+    pub recent_votes: List<Leaf<StoredVoteV1>>,
     /// When ephemeral Reddit display content was last fetched (ms); absent after eviction.
     pub fetched_at: Leaf<i64>,
 }
@@ -55,7 +55,7 @@ pub struct Store {
     pub view_meta: Map<String, Leaf<u64>>,
 }
 
-/// Cap on the per-node recent-vote window (matches the in-memory reducer).
+/// Max recent votes returned when loading a node (query-time cap only).
 pub const RECENT_VOTES_CAP: u64 = 200;
 
 fn id_key(id: &ItemId) -> String {
@@ -148,11 +148,14 @@ fn build_group_state(
         }
     }
 
-    // Deque is front=newest; in-memory VecDeque is also front=newest.
-    let mut recent_votes = std::collections::VecDeque::new();
-    for stored in np.recent_votes().iter(db)? {
-        recent_votes.push_back(decode_vote(stored).map_err(durable::Error::Deserialize)?);
-    }
+    // List is index order (oldest first); keep the newest RECENT_VOTES_CAP entries.
+    let stored = np.recent_votes().iter(db)?;
+    let cap = RECENT_VOTES_CAP as usize;
+    let start = stored.len().saturating_sub(cap);
+    let recent_votes = stored[start..]
+        .iter()
+        .map(|s| decode_vote(s.clone()).map_err(durable::Error::Deserialize))
+        .collect::<Result<Vec<_>, _>>()?;
 
     Ok(GroupState {
         item_to_idx,
@@ -248,7 +251,7 @@ pub fn vote_writes(
     };
     batch.write(pnode.voted_pairs().key(&(lo, hi)).set(&true));
 
-    // Recent votes (newest at front).
+    // Recent votes (append-only; cap on read).
     let stored = encode_vote(&VoteData {
         ts,
         a: a_id,
@@ -260,7 +263,7 @@ pub fn vote_writes(
         delegate: None,
         thread_tag: "default".to_string(),
     });
-    batch.push_front(&pnode.recent_votes(), &stored)?;
+    batch.push(&pnode.recent_votes(), &stored)?;
     Ok(())
 }
 
@@ -314,6 +317,35 @@ mod tests {
         assert!(load_node_state(&db, &parent).unwrap().is_none());
     }
 
+    #[test]
+    fn load_caps_recent_votes_at_query_time() {
+        let dir = tempfile::tempdir().unwrap();
+        let db = Db::open(dir.path()).unwrap();
+        let parent = ItemId::root();
+
+        let mut batch = db.batch();
+        for i in 0..RECENT_VOTES_CAP + 10 {
+            vote_writes(&mut batch, &parent, "alpha", "beta", 1, 0, i as i64).unwrap();
+        }
+        batch.commit().unwrap();
+
+        assert_eq!(
+            node(&parent).recent_votes().len(&db).unwrap(),
+            RECENT_VOTES_CAP + 10
+        );
+
+        let node_state = load_node_state(&db, &parent).unwrap().unwrap();
+        assert_eq!(node_state.local_ranking.recent_votes.len(), RECENT_VOTES_CAP as usize);
+        assert_eq!(
+            node_state.local_ranking.recent_votes.first().map(|v| v.ts),
+            Some(10)
+        );
+        assert_eq!(
+            node_state.local_ranking.recent_votes.last().map(|v| v.ts),
+            Some(RECENT_VOTES_CAP as i64 + 9)
+        );
+    }
+
     #[test]
     fn missing_node_is_none() {
         let dir = tempfile::tempdir().unwrap();

download full diff B

Hardlinks — judgments / attempts / prompt

prompt download

judgments

attempts

Prompt text is loaded only by the download route.