constitution · epochs · watch · epoch 3

comparison

c_66eb04076a98 (tommy-mor) vs c_3ff71f7eaeda (tommy-mor)

download prompt · raw event · cmp_34e2b58c8fab55

council reasoning

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

B delivers multiple real feature/UX fixes (unpin via HUD form instead of stale link, vote-count badges on garden compare icons, CSS/theme updates, browser test coverage) plus doc corrections, spanning several files with tested behavior changes. A is a smaller, focused but narrower refactor of a single color-gradient function with good unit tests, but it affects only cosmetic rank-row coloring in one file.

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

B delivers concrete product fixes: pairwise vote counts on garden row controls, correcting the pin HUD from a misleading item permalink to a real unpin POST, plus matching CSS, docs, and browser/unit coverage. A is a clean, well-tested design tweak (min–max score gradient instead of list ordinal) but narrows to ranking row color only, so it adds less lasting surface area than B’s multi-path UX and correctness work.

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

Side A changes the ranking color logic from list-position-based gradients to score-based min–max normalization within each group, introducing a dedicated `score_gradient_t` helper, updating rendering to use actual scores, and adding focused tests for normalization, tied scores, and stability. Side B adds several UI enhancements (vote counts, HUD unpin behavior, styling, and tests), but they are more feature-oriented and spread across multiple files, whereas Side A corrects a core visualization behavior with a clearer, reusable design.

sides

A — c_66eb04076a98 (tommy-mor)

message

[0366806e] Color rank rows by vote mass within each group, not list position.

Min–max normalization keeps similar scores visually close while still
using the full gradient as groups grow and absolute mass dilutes.

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

diff preview

diff --git a/server/src/html/mod.rs b/server/src/html/mod.rs
index a58cbbee3490a08a625cb06df06848c59a615d65..4eff2e19ed4d303ff8e80c1eabd8a15b4990e643 100644
--- a/server/src/html/mod.rs
+++ b/server/src/html/mod.rs
@@ -264,12 +264,19 @@ pub fn scope_theme_style(parent: &ItemId) -> String {
     )
 }
 
-fn rank_row_style(parent: &ItemId, ordinal: usize, total: usize) -> String {
-    let t = if total <= 1 {
-        0.0
-    } else {
-        ordinal as f64 / (total - 1) as f64
-    };
+/// Map vote mass to gradient position using the group's score range, not raw mass or
+/// list position. Vote mass sums to 1 across the component, so absolute values dilute
+/// as N grows; min–max within the visible list preserves similar scores → similar colors.
+fn score_gradient_t(score: f64, min_score: f64, max_score: f64) -> f64 {
+    let spread = max_score - min_score;
+    if spread < 1e-9 {
+        return 0.5;
+    }
+    ((max_score - score) / spread).clamp(0.0, 1.0)
+}
+
+fn rank_row_style(parent: &ItemId, score: f64, min_score: f64, max_score: f64) -> String {
+    let t = score_gradient_t(score, min_score, max_score);
     let base_hue = scope_base_hue(parent);
     let hue = (base_hue + 118.0 * t) % 360.0;
     let lightness = 0.74 - 0.34 * t;
@@ -295,14 +302,15 @@ fn rank_list(
     highlighted: &HashSet<ItemId>,
     tree: &GlobalTree,
 ) -> Markup {
-    let group_len = items.len();
+    let min_score = items.iter().map(|r| r.score).fold(f64::INFINITY, f64::min);
+    let max_score = items.iter().map(|r| r.score).fold(f64::NEG_INFINITY, f64::max);
     html! {
         @if !items.is_empty() {
             h3 class="rank-heading muted small" { (label) }
             ol class="rank-list" {
                 @for (i, r) in items.iter().enumerate() {
                     @let href = item_href(&r.item);
-                    @let style = rank_row_style(parent, i, group_len);
+                    @let style = rank_row_style(parent, r.score, min_score, max_score);
                     @let class = rank_row_class(&r.item, highlighted);
                     li class=(class)
                         data-rank-item=(r.item.as_str())
@@ -517,19 +525,37 @@ pub async fn browse(State(state): State<AppState>, uri: Uri) -> impl IntoRespons
 
 #[cfg(test)]
 mod tests {
-    use super::{rank_row_style, SORTER_UI_JS};
+    use super::{rank_row_style, score_gradient_t, SORTER_UI_JS};
     use crate::path_types::ItemId;
 
     #[test]
-    fn rank_row_style_gradients_per_group_not_globally() {
+    fn score_gradient_t_uses_group_range_not_absolute_mass() {
+        assert!((score_gradient_t(0.12, 0.08, 0.12) - 0.0).abs() < 1e-9);
+        assert!((score_gradient_t(0.08, 0.08, 0.12) - 1.0).abs() < 1e-9);
+        // Raw 12% mass would map near the dark end globally; within this group it's the top.
+        assert!(score_gradient_t(0.12, 0.08, 0.12) < score_gradient_t(0.12, 0.0, 1.0));
+    }
+
+    #[test]
+    fn score_gradient_t_similar_scores_similar_t() {
+        let a = score_gradient_t(0.41, 0.20, 0.60);
+        let b = score_gradient_t(0.40, 0.20, 0.60);
+        assert!((a - b).abs() < 0.05);
+        assert!((a - score_gradient_t(0.60, 0.20, 0.60)).abs() > 0.3);
+    }
+
+    #[test]
+    fn score_gradient_t_tied_scores_neutral() {
+        assert!((score_gradient_t(0.25, 0.25, 0.25) - 0.5).abs() < 1e-9);
+    }
+
+    #[test]
+    fn rank_row_style_same_inputs_same_color() {
         let parent = ItemId::opaque("test-scope");
-        let first_in_four = rank_row_style(&parent, 0, 4);
-        let last_in_four = rank_row_style(&parent, 3, 4);
-        let first_in_two = rank_row_style(&parent, 0, 2);
-        let last_in_two = rank_row_style(&parent, 1, 2);
-        assert_eq!(first_in_four, first_in_two);
-        assert_eq!(last_in_four, last_in_two);
-        assert_ne!(first_in_four, last_in_four);
+        assert_eq!(
+            rank_row_style(&parent, 0.33, 0.20, 0.60),
+            rank_row_style(&parent, 0.33, 0.20, 0.60),
+        );
     }
 
     #[test]

download full diff A

B — c_3ff71f7eaeda (tommy-mor)

message

[30a67104] fixes

diff preview

diff --git a/agents.md b/agents.md
index a6a283716e09fcaba1fd690f4e877e0bbecda2c0..d9a924d2f77c444d9b112bbf37a480b963ace4f0 100644
--- a/agents.md
+++ b/agents.md
@@ -39,7 +39,7 @@ Strict **CSP** that blocks `eval` would break the current app. Other projects ma
 
 - **`VoteComparePost`:** On success returns **`text/javascript`** that **morphs** **`#vote-compare-preview`** (new ingest card) and **`#vote-edge-history-region`** (recomputed edge list). Uses **`RpcResult::PostOk`**’s **`post_id`** / **`post_index`** for the card. **`__rpc__`** carries **`form_action: "/ui"`**; **`thread_tag`** and ratio fields come from the same form as **`$form`** holes.
 
-- **Garden pin / compare voting:** Cookie **`slug_garden_pin`** via **`set_garden_pin`**. Pairwise UI: **`GET /vote/compare?…`** / **`GET /r/:room_key/vote/compare?…`**. HUD: **`#slug-pin-hud`** when **`layout`** passes garden metadata on **`body`**.
+- **Garden pin / compare voting:** Cookie **`slug_garden_pin`** via **`set_garden_pin`**. Pairwise UI: **`GET /vote/compare?…`** / **`GET /r/:room_key/vote/compare?…`**. HUD: **`#slug-pin-hud`** when **`layout`** passes garden metadata on **`body`**; the label is **`POST /ui`** **`set_garden_pin`** **`clear:true`** (**`slug_ui.js`**), not a permalink to the item.
 
 **Rule of thumb:** New **CLI or API** verbs → `RpcCommand`. New **in-page morph or form-driven** behavior that only makes sense in the browser → `HtmlUiAction`. If both need the same operation, implement the real work once (e.g. call shared RPC helpers from `post_ui_html`) and keep the wire shapes separate.
 
diff --git a/server/src/html/garden.rs b/server/src/html/garden.rs
index 8a46b1e92d38f2c390f88d48750d95d11388cd73..121d9498e8cb93d4d001dc1bbce23d74fbb958f5 100644
--- a/server/src/html/garden.rs
+++ b/server/src/html/garden.rs
@@ -16,7 +16,6 @@ use crate::{
     canonical_path::{canonicalize_item, canonicalize_tag},
     form_template::template_json_compact,
     html::{
-        forum::ingest_entry_markup,
         ui_action::UI_RPC_FIELD,
         user_can_post_room,
         JsBuilder,
@@ -111,6 +110,23 @@ fn votes_for_edge(content: &ContentState, a: &ItemId, b: &ItemId) -> Vec<crate::
     out
 }
 
+/// Number of vote ingests recorded for this unordered pair in `content` (same scope as ranking).
+fn edge_vote_count_for_pair(content: &ContentState, a: &ItemId, b: &ItemId) -> usize {
+    let (lo, hi) = canonical_edge_items(a, b);
+    let lo_s = lo.as_str();
+    let hi_s = hi.as_str();
+    content
+        .item_votes
+        .get(&lo)
+        .into_iter()
+        .flat_map(|q| q.iter())
+        .filter(|v| {
+            (v.a.as_str() == lo_s && v.b.as_str() == hi_s)
+                || (v.a.as_str() == hi_s && v.b.as_str() == lo_s)
+        })
+        .count()
+}
+
 fn vote_thread_tags_for_pair(content: &ContentState, a: &ItemId, b: &ItemId) -> Vec<String> {
     let set: HashSet<String> = content
         .item_threads
@@ -288,6 +304,7 @@ fn child_row_pin_or_vote(
     nav: &ThreadNav,
     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
@@ -315,7 +332,16 @@ fn child_row_pin_or_vote(
                 @if pi == row_item {
                     span class="ont-garden-pinned-here" title="Pinned" aria-label="Pinned" { "📌" }
                 } @else {
-                    a class="ont-garden-vote-ico" href=(vote_compare_href(nav, pi, row_item, None)) title="Vote vs pinned" aria-label="Vote" { "⚖" }
+                    @let nv = edge_vote_count_for_pair(scope_content, pi, row_item);
+                    @let tip = format!(
+                        "Compare and vote — {nv} pairwise vote{} in this scope for pinned vs this row",
+                        if nv == 1 { "" } else { "s" },
+                    );
+                    @let aria = format!("Vote; {} pairwise {}", nv, if nv == 1 { "vote" } else { "votes" });
+                    a class="ont-garden-vote-ico" href=(vote_compare_href(nav, pi, row_item, None)) title=(tip) aria-label=(aria) {
+                        span class="ont-garden-vote-glyph" aria-hidden="true" { "⚖" }
+                        span class="ont-garden-vote-count" { (format!("{}", nv)) }
+                    }
                 }
             } @else {
                 form method="POST" action="/ui" data-navigate="full" class="ont-pin-form ont-garden-pin-form" {
@@ -978,10 +1004,9 @@ async fn render_scope_view(
 ) -> axum::response::Response {
     let scope = nav.scope();
     let pin_ref = pinned_item_from_jar(&jar);
-    let model = {
-        let reduced = state.reduced.read().await;
-        build_item_page_view_model(&reduced, &scope, browse.item())
-    };
+    let reduced = state.reduced.read().await;
+    let model = build_item_page_view_model(&reduced, &scope, browse.item());
+    let scope_content = content_for_garden_view(&reduced, &scope);
     let thread_href = |tag: &str| nav.thread_url(tag);
     let external_empty_body = browse.is_external() && model.body.is_none();
     let cli_path_arg = item_display_path(&model.item);
@@ -1108,7 +1133,7 @@ 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(), &next_for_pin))
+                                        (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) }
                                     }
@@ -1124,7 +1149,7 @@ 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(), &next_for_pin))
+                                    (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())) } }
                                 }
@@ -1363,6 +1388,33 @@ mod tests {
         }));
     }
 
+    #[test]
+    fn edge_vote_count_for_pair_matches_votes_for_edge_len() {
+        use super::{
+            content_for_garden_view, edge_vote_count_for_pair, votes_for_edge,
+        };
+        use crate::path_types::ItemId;
+        let mut reduced = ReducerState::default();
+        apply_ingest(
+            &mut reduced,
+            1,
+            "@00000000-0000-0000-0000-000000000000:test:local/test\n\
+             ~/topic {root}\n\
+             ~/topic/a {alpha}\n\
+             ~/topic/b {beta}\n\
+             ~/topic/a 3:2 ~/topic/b {first vote}\n\
+             ~/topic/b 2:3 ~/topic/a {second vote}\n",
+        );
+        let content = content_for_garden_view(&reduced, &ScopeId::Public);
+        let a = ItemId::parse("~/topic/a").unwrap().normalized_storage();
+        let b = ItemId::parse("~/topic/b").unwrap().normalized_storage();
+        assert_eq!(
+            edge_vote_count_for_pair(content, &a, &b),
+            votes_for_edge(content, &a, &b).len()
+        );
+        assert_eq!(votes_for_edge(content, &a, &b).len(), 2);
+    }
+
     #[test]
     fn item_page_model_includes_body_and_unranked_without_votes() {
         let mut reduced = ReducerState::default();
diff --git a/server/static/slug_ui.js b/server/static/slug_ui.js
index c0de1cddbba78227dfb80bfd41e7b855b3a42bc3..86f935f8dd998f3d6df016f33b1e47f9780782ea 100644
--- a/server/static/slug_ui.js
+++ b/server/static/slug_ui.js
@@ -170,15 +170,6 @@
       return { room: raw.slice(0, i), item: raw.slice(i + 1) };
     }
 
-    function gardenItemHref(prefix, storageUrl) {
-      var marker = 'https://slug.social/~/';
-      if (storageUrl.indexOf(marker) === 0) {
-        var tail = storageUrl.slice(marker.length);
-        return prefix.replace(/\/$/, '') + (tail ? '/' + tail : '');
-      }
-      return storageUrl;
-    }
-
     function refreshPinHud() {
       var hud = document.getElementById('slug-pin-hud');
       if (!hud) return;
@@ -187,19 +178,37 @@
       var pin = decodePinCookie();
       hud.innerHTML = '';
       if (!pin || !prefix || pin.room !== bodyRoom) return;
-      var a = document.createElement('a');
-      a.className = 'slug-pin-hud-link';
-      a.href = gardenItemHref(prefix, pin.item);
-      a.title = 'Pinned item';
+      var form = document.createElement('form');
+      form.method = 'POST';
+      form.action = '/ui';
+      form.setAttribute('data-navigate', 'full');
+      form.className = 'slug-pin-hud-form';
+      var rpc = document.createElement('input');
+      rpc.type = 'hidden';
+      rpc.name = '__rpc__';
+      rpc.value = JSON.stringify({
+        action: 'set_garden_pin',
+        clear: true,
+        room_wire: '',
+        next: window.location.pathname + window.location.search,
+        form_action: '/ui',
+      });
+      form.appendChild(rpc);
+      var btn = document.createElement('button');
+      btn.type = 'submit';
+      btn.className = 'slug-pin-hud-link slug-pin-hud-unpin-btn';
+      btn.title = 'Unpin — removes this item from the corner HUD';
+      btn.setAttribute('aria-label', 'Unpin pinned item');
       var span = document.createElement('span');
       span.className = 'slug-pin-hud-glyph';
       span.setAttribute('aria-hidden', 'true');
       span.textContent = '📌';
-      a.appendChild(span);
+      btn.appendChild(span);
       var label = pin.item.replace(/^https:\/\/slug\.social\/~\/?/, '~/');
       if (label.length > 36) label = label.slice(0, 34) + '…';
-      a.appendChild(document.createTextNode(' ' + label));
-      hud.appendChild(a);
+      btn.appendChild(document.createTextNode(' ' + label));
+      form.appendChild(btn);
+      hud.appendChild(form);
     }
     refreshPinHud();
 
diff --git a/server/static/theme_default.css b/server/static/theme_default.css
index ec0fbe7acee0aa2802f978f14a9b0fc86e78c5b8..9178f0629cfb868348740e6dea1626dc6afb8345 100644
--- a/server/static/theme_default.css
+++ b/server/static/theme_default.css
@@ -799,6 +799,12 @@ details > summary::-webkit-details-marker { display: none; }
 }
 
 /* Pinned item HUD — bottom bar, same plane as spread */
+.slug-pin-hud-form {
+  display: inline;
+  margin: 0;
+  padding: 0;
+  border: none;
+}
 #slug-pin-hud.slug-pin-hud {
   margin-left: auto;
   max-width: min(42vw, 280px);
@@ -808,6 +814,13 @@ details > summary::-webkit-details-marker { display: none; }
   overflow: hidden;
   text-overflow: ellipsis;
 }
+.slug-pin-hud-link.slug-pin-hud-unpin-btn {
+  background: transparent;
+  border: none;
+  cursor: pointer;
+  font-size: inherit;
+  font-family: inherit;
+}
 .slug-pin-hud-link {
   color: var(--ui);
   text-decoration: none;
@@ -815,7 +828,10 @@ details > summary::-webkit-details-marker { display: none; }
   align-items: center;
   gap: 4px;
 }
-.slug-pin-hud-link:hover { color: var(--signal); }
+.slug-pin-hud-link:hover,
+.slug-pin-hud-unpin-btn:hover {
+  color: var(--signal);
+}
 .slug-pin-hud-glyph { font-size: 13px; line-height: 1; }
 
 /* Garden pin / vote controls */
@@ -890,10 +906,21 @@ span.ont-garden-pinned-here {
   align-items: center;
   justify-content: center;
 }
+a.ont-garden-vote-ico {
+  gap: 4px;
+}
 a.ont-garden-vote-ico:hover {
   color: var(--signal);
   background: var(--g4);
 }
+.ont-garden-v

… preview truncated; 4,237 characters omitted

download full diff B

Hardlinks — judgments / attempts / prompt

prompt download

judgments

attempts

Prompt text is loaded only by the download route.