constitution · epochs · watch · epoch 3

comparison

c_7ec4b410de02 (tommy-mor) vs c_a896b2dc05d5 (tommy-mor)

download prompt · raw event · cmp_1a5132a04331bf

council reasoning

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

Side A delivers a self-contained, well-tested UI feature (clickable unpin from ranked child list) with corresponding CSS and updated browser test coverage exercising the new flow end-to-end. Side B refactors the recent-votes storage from a capped Deque to an append-only List but removes the write-time trim entirely, meaning storage now grows unbounded per node while only capping at query time—a real scalability regression traded for simplicity, despite the added unit test.

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

B redesigns the core recent-votes path (Deque/VecDeque → append-only List/Vec, schema v3→v4, drop write-time trim in projection_apply, cap only on load) with a dedicated unit test—lasting storage/reducer simplification. A is a solid but narrower UX win: wiring the existing set_garden_pin clear POST into ranked child pin icons plus CSS and browser coverage.

openai/gpt-chat-latest · winner B · 3:2 · permalink

Side B makes a deeper architectural change by replacing the durable recent-vote deque with an append-only list, removing write-time trimming logic, updating the in-memory representation, bumping the storage schema version, and adding a test that verifies query-time capping. Side A improves usability by making pinned child-group icons submit the existing unpin flow and adds browser coverage, but it is a localized UI enhancement rather than a broader storage/design improvement.

sides

A — c_7ec4b410de02 (tommy-mor)

message

[02cd761c] Make ranked child group pin icons clickable to unpin.

Lets users clear the garden pin from the ranked child groups list via the same POST /ui set_garden_pin flow as the HUD and item header.

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

diff preview

diff --git a/server/src/html/garden/pin.rs b/server/src/html/garden/pin.rs
index 1820d7fe7ee167ecbf5ad5124f23b2ab92ffb01f..b7ea92bbe67d6d499915edb607a3a0b36178d7f6 100644
--- a/server/src/html/garden/pin.rs
+++ b/server/src/html/garden/pin.rs
@@ -101,6 +101,7 @@ pub(super) fn child_row_pin_or_vote(
     row_item: &ItemId,
     pinned_room_and_item: Option<&(String, ItemId)>,
     scope_content: &ContentState,
+    next_path: &str,
 ) -> maud::Markup {
     let pin_matches_scope = pinned_room_and_item
         .map(|(r, _)| r == nav.room_wire.as_str())
@@ -108,12 +109,25 @@ pub(super) fn child_row_pin_or_vote(
     let pinned_item = pinned_room_and_item
         .filter(|_| pin_matches_scope)
         .map(|(_, i)| i);
+    let unpin_rpc = template_json_compact(&json!({
+        "action": "set_garden_pin",
+        "clear": true,
+        "room_wire": "",
+        "next": next_path,
+        "form_action": "/ui",
+    }))
+    .expect("unpin rpc json");
 
     html! {
         @if let Some(pi) = pinned_item {
             span class="ont-garden-child-actions" data-garden-room=(nav.room_wire.as_str()) {
                 @if pi == row_item {
-                    span class="ont-garden-pinned-here" title="Pinned" aria-label="Pinned" { "📌" }
+                    form method="POST" action="/ui" data-navigate="full" class="ont-garden-pin-form" {
+                        input type="hidden" name=(UI_RPC_FIELD) value=(unpin_rpc);
+                        button type="submit" class="ont-garden-pin-ico ont-garden-pin-ico-active" title="Unpin" aria-label="Unpin from HUD" {
+                            span class="ont-garden-pin-glyph" aria-hidden="true" { "📌" }
+                        }
+                    }
                 } @else {
                     @let nv = edge_vote_count_for_pair(scope_content, pi, row_item);
                     @let tip = format!(
diff --git a/server/src/html/garden/render.rs b/server/src/html/garden/render.rs
index bd8cbce059ee789de6e8dc9b6f69f54beee2f994..7bfaadc0786b734db8973c53b82296a188d26e22 100644
--- a/server/src/html/garden/render.rs
+++ b/server/src/html/garden/render.rs
@@ -216,7 +216,7 @@ pub(super) async fn render_scope_view(
                                     @let item_url = item_href(r.item.as_str(), &nav);
                                     @let score_str = format!("{:.3}", r.score);
                                     li data-garden-item=(r.item.as_str()) {
-                                        (child_row_pin_or_vote(&nav, &r.item, pin_ref.as_ref(), scope_content))
+                                        (child_row_pin_or_vote(&nav, &r.item, pin_ref.as_ref(), scope_content, &next_for_pin))
                                         a class="item-link" href=(item_url) { code { (item_display_path(r.item.as_str())) } }
                                         span class="ont-rank-score" { (score_str) }
                                     }
@@ -232,7 +232,7 @@ pub(super) async fn render_scope_view(
                         ul class="ont-group-list" {
                             @for name in &model.child_rankings.unranked_items {
                                 li data-garden-item=(name.as_str()) {
-                                    (child_row_pin_or_vote(&nav, name, pin_ref.as_ref(), scope_content))
+                                    (child_row_pin_or_vote(&nav, name, pin_ref.as_ref(), scope_content, &next_for_pin))
                                     @let href = item_href(name.as_str(), &nav);
                                     a class="item-link" href=(href) { code { (item_display_path(name.as_str())) } }
                                 }
diff --git a/server/static/theme_default.css b/server/static/theme_default.css
index 2dabf3a7094cecd89d0c9674d27efe30cd01c010..b846ab86cb6eb5fb56be87efb054fb8b18082583 100644
--- a/server/static/theme_default.css
+++ b/server/static/theme_default.css
@@ -891,8 +891,7 @@ button.ont-pin-btn-active {
   vertical-align: middle;
 }
 button.ont-garden-pin-ico,
-a.ont-garden-vote-ico,
-span.ont-garden-pinned-here {
+a.ont-garden-vote-ico {
   font-size: 13px;
   line-height: 1;
   padding: 2px 6px;
@@ -910,6 +909,11 @@ span.ont-garden-pinned-here {
   align-items: center;
   justify-content: center;
 }
+form.ont-garden-pin-form {
+  display: inline;
+  margin: 0;
+  padding: 0;
+}
 a.ont-garden-vote-ico {
   gap: 4px;
 }
@@ -925,10 +929,14 @@ a.ont-garden-vote-ico:hover {
 span.ont-garden-vote-glyph {
   line-height: 1;
 }
-span.ont-garden-pinned-here {
+button.ont-garden-pin-ico-active {
   border-color: var(--link);
   color: var(--signal);
-  cursor: default;
+  background: color-mix(in srgb, var(--link) 12%, var(--g3));
+}
+button.ont-garden-pin-ico-active:hover {
+  color: var(--signal);
+  background: color-mix(in srgb, var(--link) 18%, var(--g4));
 }
 button.ont-garden-pin-ico:focus-visible {
   outline: 2px solid var(--link);
diff --git a/server/static/theme_retro_craft.css b/server/static/theme_retro_craft.css
index bdd9031b82154a5019782e6dcb8bf7589fafce09..5d4954812ad43f2ee7e63c71c7f0ae0b3e3fc95b 100644
--- a/server/static/theme_retro_craft.css
+++ b/server/static/theme_retro_craft.css
@@ -711,8 +711,7 @@ body.view-ontology span.ont-garden-vote-glyph {
   line-height: 1;
 }
 body.view-ontology button.ont-garden-pin-ico,
-body.view-ontology a.ont-garden-vote-ico,
-body.view-ontology span.ont-garden-pinned-here {
+body.view-ontology a.ont-garden-vote-ico {
   font-size: 0.95rem;
   line-height: 1;
   padding: 0.1rem 0.35rem;
@@ -732,10 +731,14 @@ body.view-ontology a.ont-garden-vote-ico:hover {
   border-color: #a68e6b;
   background: #ebe6dc;
 }
-body.view-ontology span.ont-garden-pinned-here {
+body.view-ontology button.ont-garden-pin-ico-active {
   border-color: #1a4a8c;
   background: color-mix(in srgb, #1a4a8c 10%, #f7f3eb);
-  cursor: default;
+  color: #0d2d5c;
+}
+body.view-ontology button.ont-garden-pin-ico-active:hover {
+  border-color: #a68e6b;
+  background: color-mix(in srgb, #1a4a8c 14%, #ebe6dc);
 }
 body.view-ontology form.ont-garden-pin-form {
   display: inline;
diff --git a/test/browser_garden_pin.clj b/test/browser_garden_pin.clj
index d4fa9ed60b45e872ec60e29307cfa61bcd9dd6b2..579976f4c2493a149f9eba081862ca07919f4fe9 100644
--- a/test/browser_garden_pin.clj
+++ b/test/browser_garden_pin.clj
@@ -58,10 +58,11 @@
      (let [alice-token (oauth/fetch-bearer-token! base-url :username "alice")
            thread-tag "browser-garden-pin"
            raw (str "# " thread-tag "\n\n"
-                    "~/gp-pin-a {alpha}\n"
-                    "~/gp-pin-b {beta}\n"
+                    "~/gp-parent {parent}\n"
+                    "~/gp-parent/pin-a {alpha}\n"
+                    "~/gp-parent/pin-b {beta}\n"
                     "{pin test vote}\n"
-                    "~/gp-pin-a 2:1 ~/gp-pin-b\n")
+                    "~/gp-parent/pin-a 2:1 ~/gp-parent/pin-b\n")
            post-resp (oauth/http-post-json
                       (str base-url "/api/v0/rpc")
                       [{"Post" {"room" "public"
@@ -77,23 +78,43 @@
              (core/with-page [pg (core/new-page-from-context ctx)]
                (page/navigate pg (str base-url "/login"))
                (is (wait-for-text pg "body" "@alice" 15000) "alice session after login")
-               (page/navigate pg (str base-url "/~/gp-pin-a"))
-               (is (wait-for-text pg ".ont-item-shell" "~/gp-pin-a" 15000) "on item a page")
+               (page/navigate pg (str base-url "/~/gp-parent/pin-a"))
+               (is (wait-for-text pg ".ont-item-shell" "~/gp-parent/pin-a" 15000) "on item a page")
                ;; Native form POST /ui (data-navigate=full) — not fetch/eval
                (locator/click (page/locator pg ".ont-item-pin-zone form.ont-pin-form button[type=submit]"))
-               (is (wait-for-text pg "#slug-pin-hud" "gp-pin-a" 15000)
+               (is (wait-for-text pg "#slug-pin-hud" "gp-parent/pin-a" 15000)
                    "HUD shows pinned item label after redirect")
-               (page/navigate pg (str base-url "/~/gp-pin-b"))
-               (is (wait-for-text pg ".ont-item-shell" "~/gp-pin-b" 15000) "on item b page")
+               (page/navigate pg (str base-url "/~/gp-parent/pin-b"))
+               (is (wait-for-text pg ".ont-item-shell" "~/gp-parent/pin-b" 15000) "on item b page")
                (is (wait-for-text pg "a.ont-vote-compare-btn" "vote" 10000)
                    "vote link visible vs pinned item")
-               ;; HUD clears pin (POST set_garden_pin clear), not navigate to item
+               ;; Unpin from ranked child groups on parent page
+               (page/navigate pg (str base-url "/~/gp-parent"))
+               (is (wait-for-text pg ".ont-tab-panel-children" "ranked child groups" 15000)
+                   "parent page shows ranked child groups")
+               (is (wait-for-text pg ".ont-ranking-list button.ont-garden-pin-ico-active" "📌" 10000)
+                   "pinned row shows active pin in ranked list")
+               (locator/click (page/locator pg ".ont-ranking-list button.ont-garden-pin-ico-active"))
+               (is (wait-for-absence-substr pg "#slug-pin-hud" "gp-parent/pin-a" 15000)
+                   "HUD clears after unpin from ranked child list")
+               (is (wait-for-absence-substr pg ".ont-ranking-list" "ont-garden-vote-ico" 10000)
+                   "vote icons removed from ranked list after unpin")
+               (page/navigate pg (str base-url "/~/gp-parent/pin-b"))
+               (is (wait-for-text pg ".ont-item-shell" "~/gp-parent/pin-b" 15000) "on item b page again")
+               (is (wait-for-absence-substr pg "body" "ont-vote-compare-btn" 15000)
+                   "compare vote CTA removed after ranked-list unpin")
+               ;; HUD unpin still works from item page context
+               (page/navigate pg (str base-url "/~/gp-parent/pin-a"))
+               (locator/click (page/locator pg ".ont-item-pin-zone form.ont-pin-form button[type=submit]"))
+               (is (wait-for-text pg "#slug-pin-hud" "gp-parent/pin-a" 15000)
+                   "re-pin from item page for HUD unpin test")
+               (page/navigate pg (str base-url "/~/gp-parent/pin-b"))
                (locator/click (page/locator pg "#slug-pin-hud button.slug-pin-hud-unpin-btn"))
-               (is (wait-for-absence-substr pg "#slug-pin-hud" "gp-pin-a" 15000)
+               (is (wait-for-absence-substr pg "#slug-pin-hud" "gp-parent/pin-a" 15000)
                    "HUD clears after unpin from HUD button")
                (is (wait-for-absence-substr pg "body" "ont-vote-compare-btn" 15000)
                    "compare vote CTA removed after HUD unpin on same reload")
-               (is (str/includes? (or (page/url pg) "") "/~/gp-pin-b")
+               (is (str/includes? (or (page/url pg) "") "/~/gp-parent/pin-b")
                    "still on item b after HUD unpin"))))))
 
      (finally

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.