constitution · epochs · watch · epoch 3

comparison

c_ed5070da05a1 (tommy-mor) vs c_f8f31c680ca4 (tommy-mor)

download prompt · raw event · cmp_385754a703f1fc

council reasoning

~anthropic/claude-sonnet-latest · winner A · 9:1 · permalink

Side A extracts duplicated, panic-prone thread-index logic into well-documented reducer helper methods, fixes a real bug (garden 404 fallback for empty private rooms), and adds substantial regression tests (integration test + browser test) proving correctness after redaction. Side B merely moves a single div element in markup with no functional or test impact, offering negligible lasting value.

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

A extracts shared thread-index helpers, fixes empty private-room gardens falling back to public content (with an integration test), documents tombstone stability in the reducer, and adds a full browser regression for PostRedact vs chronological indices—plus aligning FeedPost indexing to 0-based. B only relocates an errors div in new-thread HTML, a trivial presentation nit with no lasting design or correctness impact.

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

Side A introduces reusable reducer helpers for chronological thread indices, fixes multiple call sites to use them consistently, adds guards so empty private room gardens return 404 instead of falling back to public content, documents important reducer invariants, and adds regression/integration tests covering these behaviors. Side B only moves the error container element within the new-thread form markup, a minor UI/layout adjustment with far less lasting impact.

sides

A — c_ed5070da05a1 (tommy-mor)

message

[39d32611] helpers and cleanups

diff preview

diff --git a/server/src/api/rpc.rs b/server/src/api/rpc.rs
index 5049a26b096bd8435d6eb9e75ccb751b6f489061..0ff2d701dd1e8661f58abd55672cd91280e9491e 100644
--- a/server/src/api/rpc.rs
+++ b/server/src/api/rpc.rs
@@ -1333,11 +1333,8 @@ pub async fn handle_rpc_batch(
                             }).collect()
                         })
                         .unwrap_or_default();
-                    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))
-                        .expect("rank history post_id must be in ingests_by_scope_thread for (scope, thread)");
+                    let thread_post_index =
+                        reduced.thread_post_index_chronological(&scope, &e.thread, &e.post_id);
                     RankHistoryRow {
                         ts: e.ts,
                         scope_rank: e.scope_rank,
@@ -1504,11 +1501,11 @@ pub async fn handle_rpc_batch(
                                         .map(|ing| {
                                             let scope = scope_from_room_wire(&ing.room_id);
                                             let thread_post_index = reduced
-                                                .ingests_by_scope_thread
-                                                .get(&(scope, ing.thread_tag.clone()))
-                                                .and_then(|q| {
-                                                    q.iter().rev().position(|pid| pid == &ing.id).map(|i| i + 1)
-                                                });
+                                                .try_thread_post_index_chronological(
+                                                    &scope,
+                                                    &ing.thread_tag,
+                                                    &ing.id,
+                                                );
                                             FeedPost {
                                                 ts: ing.ts,
                                                 id: ing.id.clone(),
@@ -1565,11 +1562,11 @@ pub async fn handle_rpc_batch(
                                     .map(|ing| {
                                         let scope = scope_from_room_wire(&ing.room_id);
                                         let thread_post_index = reduced
-                                            .ingests_by_scope_thread
-                                            .get(&(scope, ing.thread_tag.clone()))
-                                            .and_then(|q| {
-                                                q.iter().rev().position(|pid| pid == &ing.id).map(|i| i + 1)
-                                            });
+                                            .try_thread_post_index_chronological(
+                                                &scope,
+                                                &ing.thread_tag,
+                                                &ing.id,
+                                            );
                                         FeedPost {
                                             ts: ing.ts,
                                             id: ing.id.clone(),
diff --git a/server/src/html/forum/ingest.rs b/server/src/html/forum/ingest.rs
index 308358abc6cd6811079d9ec41b114093c0294d02..27d1852cf3b2a1718605e4633d4238383adfe33d 100644
--- a/server/src/html/forum/ingest.rs
+++ b/server/src/html/forum/ingest.rs
@@ -1,4 +1,3 @@
-use crate::canonical_path::canonicalize_tag;
 use crate::form_template::template_json_compact;
 use crate::reducer::{scope_from_room_wire, ReducerState};
 use maud::{html, Markup};
@@ -31,11 +30,7 @@ pub(super) fn thread_nav_for_ingest(ing: &crate::events::Ingest) -> Option<Threa
 
 pub(super) fn thread_post_index_in_scope(reduced: &ReducerState, ing: &crate::events::Ingest) -> Option<usize> {
     let scope = scope_from_room_wire(&ing.room_id);
-    let tag = canonicalize_tag(&ing.thread_tag);
-    reduced
-        .ingests_by_scope_thread
-        .get(&(scope, tag))
-        .and_then(|q| q.iter().rev().position(|id| id == &ing.id))
+    reduced.try_thread_post_index_chronological(&scope, &ing.thread_tag, &ing.id)
 }
 
 fn post_header_meta(
diff --git a/server/src/html/garden.rs b/server/src/html/garden.rs
index 23245d6fdfc9019b199ab8417150faf5f3297067..423f23fd8c9ad7b7f454d6ea7a9a7607a4c9c5b9 100644
--- a/server/src/html/garden.rs
+++ b/server/src/html/garden.rs
@@ -11,7 +11,7 @@ use crate::{
     canonical_path::canonicalize_item,
     events::ThreadCapability,
     path_types::CanonicalItemUrl,
-    reducer::{ReducerState, ScopeId},
+    reducer::{ContentState, ReducerState, ScopeId},
     ranking::{connected_components_from_voted_pairs, ranked_items_subset},
     scope_rank::{build_children_rankings, ChildrenRankings},
     state::AppState,
@@ -133,6 +133,23 @@ fn user_can_view_room(reduced: &ReducerState, room_id: &str, username: Option<&s
     reduced.user_has_cap(room_id, u, ThreadCapability::View)
 }
 
+/// Private `~/` / `-/` garden pages require at least one ingest in that scope (a `content` entry).
+fn room_scope_has_garden_content(reduced: &ReducerState, nav: &ThreadNav) -> bool {
+    match nav.scope() {
+        ScopeId::Public => true,
+        ScopeId::Room(_) => reduced.content_for_scope(&nav.scope()).is_some(),
+    }
+}
+
+fn content_for_garden_view<'a>(reduced: &'a ReducerState, scope: &ScopeId) -> &'a ContentState {
+    match scope {
+        ScopeId::Public => reduced.public(),
+        ScopeId::Room(_) => reduced.content_for_scope(scope).expect(
+            "room garden only renders after room_scope_has_garden_content returned true",
+        ),
+    }
+}
+
 /// Ontology index — root-level paths. Private (UUID) roots are excluded.
 pub async fn garden_index(
     State(state): State<AppState>,
@@ -298,16 +315,20 @@ pub async fn room_garden_index(
     uri: Uri,
 ) -> impl IntoResponse {
     let room_id = format!("{room_short}/{room_slug}");
+    let Some(nav) = ThreadNav::from_room_id(&room_id) else {
+        return (StatusCode::NOT_FOUND, "bad room path").into_response();
+    };
     let reduced = state.reduced.read().await;
     let user = optional_principal(&headers, &jar, &reduced);
     if !user_can_view_room(&reduced, &room_id, user.as_deref()) {
         drop(reduced);
         return room_not_found_page(&jar, &uri).into_response();
     }
+    if !room_scope_has_garden_content(&reduced, &nav) {
+        drop(reduced);
+        return room_not_found_page(&jar, &uri).into_response();
+    }
     drop(reduced);
-    let Some(nav) = ThreadNav::from_room_id(&room_id) else {
-        return (StatusCode::NOT_FOUND, "bad room path").into_response();
-    };
     render_scope_view(
         state,
         GardenBrowsePath::Tilde(OntologyPath::root()),
@@ -326,25 +347,26 @@ pub async fn room_external_garden_index(
     uri: Uri,
 ) -> impl IntoResponse {
     let room_id = format!("{room_short}/{room_slug}");
+    let Some(nav) = ThreadNav::from_room_id(&room_id) else {
+        return (StatusCode::NOT_FOUND, "bad room path").into_response();
+    };
     let reduced = state.reduced.read().await;
     let user = optional_principal(&headers, &jar, &reduced);
     if !user_can_view_room(&reduced, &room_id, user.as_deref()) {
         drop(reduced);
         return room_not_found_page(&jar, &uri).into_response();
     }
-    drop(reduced);
-    let Some(nav) = ThreadNav::from_room_id(&room_id) else {
-        return (StatusCode::NOT_FOUND, "bad room path").into_response();
-    };
+    if !room_scope_has_garden_content(&reduced, &nav) {
+        drop(reduced);
+        return room_not_found_page(&jar, &uri).into_response();
+    }
     let ext_path = ExternalOntologyPath::from_input("");
     let parent = CanonicalItemUrl::parse("https://.").unwrap();
-    let child_rankings = {
-        let reduced = state.reduced.read().await;
-        let content = reduced
-            .content_for_scope(&nav.scope())
-            .unwrap_or_else(|| reduced.public());
-        build_children_rankings(content, &parent)
-    };
+    let child_rankings = build_children_rankings(
+        content_for_garden_view(&reduced, &nav.scope()),
+        &parent,
+    );
+    drop(reduced);
 
     let page = layout(
         "-/",
@@ -400,16 +422,20 @@ pub async fn room_external_ontology_path(
     uri: Uri,
 ) -> impl IntoResponse {
     let room_id = format!("{room_short}/{room_slug}");
+    let Some(nav) = ThreadNav::from_room_id(&room_id) else {
+        return (StatusCode::NOT_FOUND, "bad room path").into_response();
+    };
     let reduced = state.reduced.read().await;
     let user = optional_principal(&headers, &jar, &reduced);
     if !user_can_view_room(&reduced, &room_id, user.as_deref()) {
         drop(reduced);
         return room_not_found_page(&jar, &uri).into_response();
     }
+    if !room_scope_has_garden_content(&reduced, &nav) {
+        drop(reduced);
+        return room_not_found_page(&jar, &uri).into_response();
+    }
     drop(reduced);
-    let Some(nav) = ThreadNav::from_room_id(&room_id) else {
-        return (StatusCode::NOT_FOUND, "bad room path").into_response();
-    };
     let path = ExternalOntologyPath::from_input(&path);
     render_scope_view(state, GardenBrowsePath::External(path), nav, jar, uri).await
 }
@@ -422,16 +448,20 @@ pub async fn room_ontology_path(
     uri: Uri,
 ) -> impl IntoResponse {
     let room_id = format!("{room_short}/{room_slug}");
+    let Some(nav) = ThreadNav::from_room_id(&room_id) else {
+        return (StatusCode::NOT_FOUND, "bad room path").into_response();
+    };
     let reduced = state.reduced.read().await;
     let user = optional_principal(&headers, &jar, &reduced);
     if !user_can_view_room(&reduced, &room_id, user.as_deref()) {
         drop(reduced);
         return room_not_found_page(&jar, &uri).into_response();
     }
+    if !room_scope_has_garden_content(&reduced, &nav) {
+        drop(reduced);
+        return room_not_found_page(&jar, &uri).into_response();
+    }
     drop(reduced);
-    let Some(nav) = ThreadNav::from_room_id(&room_id) else {
-        return (StatusCode::NOT_FOUND, "bad room path").into_response();
-    };
     let path = OntologyPath::from_input(&path);
     render_scope_view(state, GardenBrowsePath::Tilde(path), nav, jar, uri).await
 }
@@ -474,9 +504,7 @@ fn build_sibling_rank(
     item: &CanonicalItemUrl,
 ) -> Option<SiblingRank> {
     let item = item.clone().normalized_storage();
-    let content = reduced
-        .content_for_scope(scope)
-        .unwrap_or_else(|| reduced.public());
+    let content = content_for_garden_view(reduced, scope);
     let group = &content.ranking_group;
     let parent = item.parent()?.normalized_storage();
     let siblings: Vec<CanonicalItemUrl> = content
@@ -537,9 +565,7 @@ fn build_rank_history(
     scope: &ScopeId,
     item: &str,
 ) -> Vec<RankHistoryEntryView> {
-    let content = reduced
-        .content_for_scope(scope)
-        .unwrap_or_else(|| reduced.public());
+    let content = content_for_garden_view(reduced, scope);
     let item_key = CanonicalItemUrl(item.to_string());
     let entries = match content.rank_history.get(&item_key) {
         None => return vec![],
@@ -574,11 +600,8 @@ fn build_rank_history(
             })
             .unwrap_or_default();
 
-        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))
-            .expect("rank history post_id must be in ingests_by_scope_thread for (scope, thread)");
+        let thread_post_index =
+            reduced.thread_post_index_chronological(scope, &e.thread, &e.post_id);
 
         RankHistoryEntryView {
             ts: e.ts,
@@ -597,9 +620,7 @@ fn build_item_

… preview truncated; 15,058 characters omitted

download full diff A

B — c_f8f31c680ca4 (tommy-mor)

message

[39f8fb3c] nit

diff preview

diff --git a/server/src/html/forum/new_thread.rs b/server/src/html/forum/new_thread.rs
index 4dd2654f953a7db6ee9b25c348f99906da93a94a..3ef791ea96f83ee9bb333ec225483ac317c2881d 100644
--- a/server/src/html/forum/new_thread.rs
+++ b/server/src/html/forum/new_thread.rs
@@ -15,7 +15,6 @@ const TAG_INPUT_ID: &str = "new-thread-tag";
 fn new_thread_compose_section(room_wire: &str) -> Markup {
     html! {
         section class="compose" id=(COMPOSE_SECTION_ID) {
-            div id=(ERRORS_ID) {}
             form id=(FORM_ID) method="POST" action="/ui" data-check-action="/ui" data-check-rpc=(template_json_compact(&json!({
                 "action": "check_ingest",
                 "room": room_wire,
@@ -36,6 +35,7 @@ fn new_thread_compose_section(room_wire: &str) -> Markup {
                 textarea name="text" rows="4" placeholder="First post body…" required {}
                 p { button type="submit" { "create thread / post" } }
             }
+            div id=(ERRORS_ID) {}
         }
     }
 }

download full diff B

Hardlinks — judgments / attempts / prompt

prompt download

judgments

attempts

Prompt text is loaded only by the download route.