constitution · epochs · watch · epoch 3

comparison

c_6a02ffb06a41 (tommy-mor) vs c_ca72f0995396 (tommy-mor)

download prompt · raw event · cmp_135cf83f240698

council reasoning

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

Commit A fixes a real correctness bug: the previous code silently mapped a missing post lookup to index 0 (which is also a valid index for the first post), making the UI guard `if index > 0` meaningless and the link route (0-based) inconsistent with the displayed 1-based index; A aligns the indexing with the actual /t/tag/N route, replaces the silent fallback with an explicit invariant check, and adds tests asserting correct indices. Commit B is a reasonable UI refactor (grouping CLI hints, hover-to-copy, JS-safety assert) but is mostly cosmetic/feature polish without fixing an underlying correctness issue.

~x-ai/grok-latest · winner A · 3:1 · permalink

A fixes real contract/indexing bugs: rank-history thread_post_index becomes required 0-based (matching /t/tag/N), drops unwrap_or(0) masking and 1-based off-by-one link logic, and locks that in with type docs plus integration asserts. B is worthwhile CLI UX (grouped panels, row click-to-copy, JS quote asserts, CSS), but it is largely presentation polish versus A’s lasting correctness and API consistency.

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

Side A fixes the semantics of `thread_post_index` by making rank history consistently use the existing 0-based URL index, replacing silent `unwrap_or(0)` fallbacks with `expect(...)` to enforce the invariant, updating documentation, removing incorrect conditional rendering, and adding integration tests that verify the behavior. Side B is primarily a UI enhancement that refactors the CLI panel to support multiple commands with click-to-copy and adds assertions for JS-safe strings, which is useful but less fundamental than correcting and validating core data behavior.

sides

A — c_6a02ffb06a41 (tommy-mor)

message

[3bc88847] removed optional

diff preview

diff --git a/server/src/api/rpc.rs b/server/src/api/rpc.rs
index ba93ca87182d49f57dffc8220f604ffa150f9a6c..5049a26b096bd8435d6eb9e75ccb751b6f489061 100644
--- a/server/src/api/rpc.rs
+++ b/server/src/api/rpc.rs
@@ -1337,8 +1337,7 @@ pub async fn handle_rpc_batch(
                         .ingests_by_scope_thread
                         .get(&(scope.clone(), e.thread.clone()))
                         .and_then(|q| q.iter().rev().position(|id| id == &e.post_id))
-                        .map(|i| i + 1)
-                        .unwrap_or(0);
+                        .expect("rank history post_id must be in ingests_by_scope_thread for (scope, thread)");
                     RankHistoryRow {
                         ts: e.ts,
                         scope_rank: e.scope_rank,
diff --git a/server/src/html/garden.rs b/server/src/html/garden.rs
index 319feb15a6b68d2b5df98b4289fedbc9bdd048d3..23245d6fdfc9019b199ab8417150faf5f3297067 100644
--- a/server/src/html/garden.rs
+++ b/server/src/html/garden.rs
@@ -450,6 +450,7 @@ struct RankHistoryEntryView {
     scope_total: usize,
     scope_rank_delta: i32,
     thread: String,
+    /// 0-based index as [`crate::html::forum::ingest::thread_post_index_in_scope`] / `/t/tag/N`.
     thread_post_index: usize,
     caused_by: Vec<crate::reducer::VoteData>,
 }
@@ -573,11 +574,11 @@ fn build_rank_history(
             })
             .unwrap_or_default();
 
-        let thread_post_index = reduced.ingests_by_scope_thread
+        let thread_post_index = reduced
+            .ingests_by_scope_thread
             .get(&(scope.clone(), e.thread.clone()))
             .and_then(|q| q.iter().rev().position(|id| id == &e.post_id))
-            .map(|i| i + 1)
-            .unwrap_or(0);
+            .expect("rank history post_id must be in ingests_by_scope_thread for (scope, thread)");
 
         RankHistoryEntryView {
             ts: e.ts,
@@ -704,11 +705,9 @@ async fn render_scope_view(
                                 span class="muted" { (ago) (label) }
                                 " · "
                                 a href=(thread_href(&e.thread)) { "#" (e.thread) }
-                                @if e.thread_post_index > 0 {
-                                    " "
-                                    a href=(format!("{}/{}", thread_href(&e.thread), e.thread_post_index)) {
-                                        span class="muted" { "post #" (e.thread_post_index) }
-                                    }
+                                " "
+                                a href=(format!("{}/{}", thread_href(&e.thread), e.thread_post_index)) {
+                                    span class="muted" { "post #" (e.thread_post_index) }
                                 }
                             }
                             @if e.caused_by.is_empty() {
diff --git a/server/tests/integration.rs b/server/tests/integration.rs
index d4c5bfe9c6f1c71dc61878bd8c5e729b1b7c69ef..9766adb43ad315a64f5df17b79f66718ce149509 100644
--- a/server/tests/integration.rs
+++ b/server/tests/integration.rs
@@ -1322,6 +1322,11 @@ async fn test_rank_history() {
     assert_eq!(entry["scope_rank_delta"], 0, "delta is 0 on first appearance");
     let caused_by = entry["caused_by"].as_array().unwrap();
     assert_eq!(caused_by.len(), 2, "both votes in the ingest touched rust");
+    assert_eq!(
+        entry["thread_post_index"],
+        0,
+        "rank history links use same 0-based index as /t/hist-test/0"
+    );
 
     ingest(
         "00000000-0000-0000-0000-000000000002:rig:test/model",
@@ -1349,6 +1354,16 @@ async fn test_rank_history() {
     assert_eq!(caused_by2.len(), 1);
     assert!(caused_by2[0]["a"].as_str().unwrap().ends_with("python") ||
             caused_by2[0]["b"].as_str().unwrap().ends_with("python"));
+    assert_eq!(
+        hist2[0]["thread_post_index"],
+        0,
+        "first hist-test post is chronological index 0"
+    );
+    assert_eq!(
+        hist2[1]["thread_post_index"],
+        1,
+        "second ingest is chronological index 1"
+    );
 
     let hist_rust2 = rpc_batch(
         &client,
diff --git a/types/src/lib.rs b/types/src/lib.rs
index edbb923e4d7d41ad82dfc254c3bd697562383527..49abba8ea9f786ef68e3157d2a5d309e15e09ba0 100644
--- a/types/src/lib.rs
+++ b/types/src/lib.rs
@@ -253,7 +253,7 @@ pub struct FeedPost {
     /// Primary thread tag (without #), if the ingest declared one.
     #[serde(skip_serializing_if = "Option::is_none")]
     pub thread: Option<String>,
-    /// 1-indexed chronological position of this post within the thread.
+    /// 1-based display ordinal for this post within the thread (feed only; URLs use 0-based paths).
     #[serde(skip_serializing_if = "Option::is_none")]
     pub thread_post_index: Option<usize>,
     /// Full raw body of the ingest document.
@@ -628,7 +628,7 @@ pub struct RankHistoryRow {
     pub score: f64,
     /// Thread tag of the ingest that triggered this rank change.
     pub thread: String,
-    /// 1-indexed chronological position of this post within the thread.
+    /// 0-indexed chronological position of this post within the thread (same as `/t/tag/N` routes).
     pub thread_post_index: usize,
     /// Votes from this ingest that directly touched this item. Empty when change was transitive.
     pub caused_by: Vec<VoteRow>,

download full diff A

B — c_ca72f0995396 (tommy-mor)

message

[798c764d] feat(html): grouped cli_panel with hover-to-copy and JS-safe asserts

Single bordered panel for multiple commands; rows copy on click without a
separate copy control. Assert CLI strings contain no chars that would break
single-quoted onclick JS.

Made-with: Cursor

diff preview

diff --git a/server/src/html/forum.rs b/server/src/html/forum.rs
index f6e45b05bb92126966e304619f80ef1d45be7a4b..075860914a6ce18bb0dfa73dc5651ef1a6318b67 100644
--- a/server/src/html/forum.rs
+++ b/server/src/html/forum.rs
@@ -718,7 +718,7 @@ pub async fn home(
             }
             div id="public-new-thread-ui-slot" {}
             (render_thread_feed(Some(&nav), "thread-feed", &public_rows, now))
-            (cli_panel("npx slugsocial public forum list"))
+            (cli_panel(&["npx slugsocial public forum list"]))
         },
         None,
         theme_from_jar(&jar),
@@ -868,7 +868,7 @@ async fn thread_view_inner(
             div id="thread-live-region" {
                 (compose_form(&nav, &tag, show_compose))
             }
-            (cli_panel(&cli))
+            (cli_panel(std::slice::from_ref(&cli)))
         },
         None,
         theme_from_jar(&jar),
@@ -984,9 +984,7 @@ pub async fn room_page(
                     (new_thread_form_for_room(&nav, true, false))
                 }
             }
-            (cli_panel(&forum_cli))
-            (cli_panel(&garden_cli))
-            (cli_panel(&audit_cli))
+            (cli_panel(&[forum_cli, garden_cli, audit_cli]))
         },
         None,
         theme_from_jar(&jar),
@@ -1409,7 +1407,7 @@ pub async fn user_profile_page(
                     }
                 }
             }
-            (cli_panel(&format!("npx slugsocial public forum list")))
+            (cli_panel(&[format!("npx slugsocial public forum list")]))
         },
         None,
         theme_from_jar(&jar),
diff --git a/server/src/html/garden.rs b/server/src/html/garden.rs
index 9b4789a79c3e293cbfc5f033a0eac8650320d94d..c8ce7de450f7d14e04020511e7eb7323bd487deb 100644
--- a/server/src/html/garden.rs
+++ b/server/src/html/garden.rs
@@ -139,7 +139,7 @@ pub async fn garden_index(
                     }
                 }
             }
-            (cli_panel("npx slugsocial garden tree"))
+            (cli_panel(&["npx slugsocial garden tree"]))
         },
         None,
         theme_from_jar(&jar),
@@ -542,7 +542,7 @@ async fn render_scope_view(
                 ScopeId::Public => format!("npx slugsocial public garden body {}", path.as_str().trim_start_matches("https://slug.social/~/")),
                 ScopeId::Room(room_id) => format!("npx slugsocial private {room_id} garden body {}", path.as_str().trim_start_matches("https://slug.social/~/")),
             };
-            (cli_panel(&cli))
+            (cli_panel(std::slice::from_ref(&cli)))
         },
         None,
         theme_from_jar(&jar),
diff --git a/server/src/html/mod.rs b/server/src/html/mod.rs
index e7f5bfb7a4b2dee2adf96447224c58d641092124..6617781a2e8e86c2e2693788ea7cd0eb0e3659a2 100644
--- a/server/src/html/mod.rs
+++ b/server/src/html/mod.rs
@@ -599,18 +599,38 @@ pub(super) fn render_linkified_with_embeds_in_scope(raw: &str, garden_prefix: &s
     }
 }
 
-/// Small CLI hint panel showing how to look up this page from the terminal.
-pub(super) fn cli_panel(cmd: &str) -> Markup {
+/// CLI strings are embedded in a single-quoted JS literal; they must never need escaping.
+fn assert_cli_panel_cmd_js_single_quote_safe(s: &str) {
+    assert!(
+        !s.contains('\\')
+            && !s.contains('\'')
+            && !s.contains('\n')
+            && !s.contains('\r'),
+        "cli_panel cmd must not contain `\\`, `'`, or newlines (got {s:?})"
+    );
+}
+
+/// Small CLI hint panel: one border and title; each line is hover-highlighted and copies on click.
+pub(super) fn cli_panel<I: AsRef<str>>(cmds: &[I]) -> Markup {
+    if cmds.is_empty() {
+        return html! {};
+    }
+    for cmd in cmds {
+        assert_cli_panel_cmd_js_single_quote_safe(cmd.as_ref());
+    }
     html! {
         div class="cli-panel" {
             span class="cli-panel-label muted" { "cli" }
-            code class="cli-panel-cmd" { (cmd) }
-            button
-                class="cli-panel-copy"
-                title="Copy to clipboard"
-                onclick=(format!(r#"navigator.clipboard.writeText('{}'); this.textContent='✓'; setTimeout(() => this.textContent='copy', 2000);"#, cmd.replace("'", "\\'")))
-            {
-                "copy"
+            div class="cli-panel-cmds" {
+                @for cmd in cmds {
+                    @let s = cmd.as_ref();
+                    button type="button" class="cli-panel-row" title="Copy command" onclick=(format!(
+                        r#"navigator.clipboard.writeText('{}');"#,
+                        s
+                    )) {
+                        code class="cli-panel-cmd" { (s) }
+                    }
+                }
             }
         }
     }
diff --git a/server/src/html/search.rs b/server/src/html/search.rs
index 28ceaa53c3fcac6777311535e95fb771b19438f5..43e6ebf36cf0fe72c96f0f9d850bea51ac094c43 100644
--- a/server/src/html/search.rs
+++ b/server/src/html/search.rs
@@ -418,7 +418,7 @@ pub async fn search_page(
                     value=(query) autocomplete="off" autofocus;
             }
             (render_search_results(&results, &query))
-            (cli_panel("npx slugsocial search <query>"))
+            (cli_panel(&["npx slugsocial search <query>"]))
         },
         None,
         theme_from_jar(&jar),
diff --git a/server/static/theme_default.css b/server/static/theme_default.css
index e024a5c8b74139ae8be37b2a1bd17e4c3abe9324..764b66c8208e91e6138b83c534387cf43c85b8b5 100644
--- a/server/static/theme_default.css
+++ b/server/static/theme_default.css
@@ -610,7 +610,7 @@ code {
    CLI PANEL — how to view this page from the terminal
    ---------------------------------------------------------------- */
 div.cli-panel {
-  align-items: baseline;
+  align-items: flex-start;
   background: var(--g1);
   border: var(--bv) solid;
   border-color: var(--lo) var(--hi) var(--hi) var(--lo); /* inset */
@@ -621,11 +621,34 @@ div.cli-panel {
   width: fit-content;
   max-width: 100%;
 }
+.cli-panel-cmds {
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+  flex: 1;
+  min-width: 0;
+}
+button.cli-panel-row {
+  background: transparent;
+  border: none;
+  color: inherit;
+  cursor: pointer;
+  display: block;
+  font: inherit;
+  margin: 0;
+  padding: 2px 4px;
+  text-align: left;
+  width: 100%;
+}
+button.cli-panel-row:hover {
+  background: var(--g3);
+}
 .cli-panel-label {
   font-size: 11px;
   letter-spacing: 0.08em;
   text-transform: uppercase;
   flex-shrink: 0;
+  padding-top: 2px;
 }
 .cli-panel-cmd {
   background: none;
diff --git a/server/static/theme_retro_craft.css b/server/static/theme_retro_craft.css
index 00714575bfa533d1d9c66653b9089642e2b0a6ca..f89ecbc3a18eb9b2b27d1f7764330f6bd6987552 100644
--- a/server/static/theme_retro_craft.css
+++ b/server/static/theme_retro_craft.css
@@ -306,19 +306,41 @@ a.post-nav-btn:hover {
 }
 
 div.cli-panel {
-  align-items: baseline;
+  align-items: flex-start;
   border: 1px dashed var(--line);
   display: flex;
-  flex-wrap: wrap;
   gap: 0.5rem;
   margin: 0.65rem 0;
   padding: 0.45rem 0.65rem;
 }
+.cli-panel-cmds {
+  display: flex;
+  flex-direction: column;
+  gap: 0.25rem;
+  flex: 1;
+  min-width: 0;
+}
+button.cli-panel-row {
+  background: transparent;
+  border: none;
+  color: inherit;
+  cursor: pointer;
+  display: block;
+  font: inherit;
+  margin: 0;
+  padding: 0.1rem 0.2rem;
+  text-align: left;
+  width: 100%;
+}
+button.cli-panel-row:hover {
+  background: color-mix(in srgb, var(--accent) 14%, transparent);
+}
 .cli-panel-label {
   color: var(--ink-dim);
   font-size: 0.72rem;
   letter-spacing: 0.12em;
   text-transform: uppercase;
+  padding-top: 0.12rem;
 }
 .cli-panel-cmd {
   color: var(--accent);

download full diff B

Hardlinks — judgments / attempts / prompt

prompt download

judgments

attempts

Prompt text is loaded only by the download route.