constitution · epochs · watch · epoch 3

comparison

c_cf0a514b261a (tommy-mor) vs c_6a02ffb06a41 (tommy-mor)

download prompt · raw event · cmp_6492c95bf8a8d6

council reasoning

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

Side B fixes a real off-by-one/indexing bug (0-based vs 1-based thread_post_index mismatch used in URLs), removes a dead conditional in the template, and adds regression tests plus doc clarifications across three files, giving concrete lasting correctness value. Side A merely removes a redundant early-return guard that was already dead code, a smaller and lower-impact cleanup with a single adjusted test.

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

B fixes real indexing inconsistency by switching rank-history thread_post_index from 1-based-with-silent-0-default to shared 0-based paths (/t/tag/N), replacing unwrap_or(0) with invariant enforcement and updating API/UI/tests/docs accordingly. A only deletes a dead zero-ratio guard already enforced upstream and retunes one unit test, which is sound cleanup but lower lasting impact.

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

Side B corrects and standardizes the thread post index semantics across RPC, HTML, types, and integration tests: it switches rank history to the intended 0-based `/t/tag/N` indexing, removes the silent `unwrap_or(0)` fallback in favor of asserting the expected invariant with `expect(...)`, and updates rendering and documentation accordingly. Side A is a worthwhile cleanup that removes a redundant zero-ratio guard and updates tests to reflect that zero-weight edges are skipped elsewhere, but it is primarily dead-code removal relying on existing validation.

sides

A — c_cf0a514b261a (tommy-mor)

message

[047b82bd] Remove redundant zero-ratio guard from reducer.

Zeros are already rejected at the DSL parser and browser handler;
the guard in apply_vote was dead code. The negative clamping stays
since add_edge_weight already skips weight-0 edges correctly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

diff preview

diff --git a/server/src/reducer.rs b/server/src/reducer.rs
index 0e36979abe0f051493038ff7e652efc7f7a0ac80..efbf7f67ff24c7a2989102fe62879b2794a27c5f 100644
--- a/server/src/reducer.rs
+++ b/server/src/reducer.rs
@@ -112,11 +112,6 @@ impl GroupState {
         if vote.ratio_right < 0 {
             vote.ratio_right = 0;
         }
-        if vote.ratio_left == 0 || vote.ratio_right == 0 {
-            // Zero on either side produces no valid edge; drop before registering items or pair.
-            return;
-        }
-
         let a_idx = self.ensure_item(&vote.a);
         let b_idx = self.ensure_item(&vote.b);
 
diff --git a/server/tests/basic.rs b/server/tests/basic.rs
index 08159f4a7f0850fd165817a4a1af4f31ced2ad76..1748769ccf7196b2d81cf556d5368c7e723f6a4e 100644
--- a/server/tests/basic.rs
+++ b/server/tests/basic.rs
@@ -533,7 +533,7 @@ fn dsl_parse_rejects_zero_zero_vote_ratio() {
 #[test]
 fn reducer_negative_ratio_clamped_to_zero() {
     let _state = ReducerState::default();
-    // GroupState::apply_vote clamps negatives to 0; when either side is 0 the vote is dropped.
+    // apply_vote clamps negatives to 0; add_edge_weight skips zero-weight edges.
     let mut group = GroupState::new();
     group.apply_vote(slugsocial_server::reducer::VoteData {
         ts: 1,
@@ -546,10 +546,9 @@ fn reducer_negative_ratio_clamped_to_zero() {
         delegate: Some("00000000-0000-0000-0000-000000000000:test:local/test".to_string()),
         thread_tag: "t".to_string(),
     });
-    // Nothing registered: zero-clamped vote is dropped before ensure_item.
-    assert!(group.idx_to_item.is_empty());
+    // Items and pair are registered; edges are absent because weight 0 is skipped.
+    assert_eq!(group.idx_to_item.len(), 2);
     assert!(group.edges.is_empty());
-    assert!(group.voted_pairs.is_empty());
 }
 
 

download full diff A

B — 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 B

Hardlinks — judgments / attempts / prompt

prompt download

judgments

attempts

Prompt text is loaded only by the download route.