constitution · epochs · watch · epoch 3

comparison

c_995cbd9de96d (tommy-mor) vs c_3f420a1f5aa1 (tommy-mor)

download prompt · raw event · cmp_da8844f1b7ffb0

council reasoning

openai/gpt-chat-latest · winner B · 25:1 · permalink

Commit B delivers substantial new functionality across the application. It introduces persistent theme selection via cookies and a new POST /theme endpoint, updates the shared layout to support server-rendered themes, preserves theme state through authentication, propagates theme handling across many HTML routes, improves private-room URL generation for API/RPC responses, updates CLI/web hints, and adds helper functions and tests. These are user-visible behavioral improvements affecting multiple subsystems. Commit A is a formatting-only change that reorganizes imports, wraps long lines, and reformats code without changing behavior. While useful for readability and consistency, it contributes far less functionality than the broad feature work in commit B.

openai/gpt-5.3-chat · winner B · 1:25 · permalink

Commit A is purely formatting and reordering with no behavioral changes. Commit B introduces a full theming system (cookies, layout integration, new endpoint), propagates theme handling across many routes, adds room-aware URL helpers, updates RPC responses, and includes new tests. It significantly expands functionality and touches many subsystems.

openai/gpt-5.2-chat · winner B · 9:1 · permalink

Commit B introduces a substantial theming system refactor and extension: adds theme cookie handling, normalization, POST /theme endpoint, layout changes, propagation of theme through many HTML handlers, updates to auth flows to preserve theme, new helper functions for room-aware URL generation, and extensive RPC adjustments plus tests. It spans multiple modules and meaningfully changes behavior. Commit A is purely formatting/reordering without functional changes. Therefore B contributes far more.

sides

A — c_995cbd9de96d (tommy-mor)

message

[2e4be477] format

diff preview

diff --git a/server/src/html/garden.rs b/server/src/html/garden.rs
index 76ba96e45c9e16291a6ccd8096fdd01b274c1560..2d5fcd903fe8f56800ad2c5564b704bd25f9b18d 100644
--- a/server/src/html/garden.rs
+++ b/server/src/html/garden.rs
@@ -9,30 +9,28 @@ use serde::Deserialize;
 use serde_json::json;
 use std::collections::HashSet;
 
-use base64::{engine::general_purpose::URL_SAFE_NO_PAD as B64_ENGINE, Engine as _};
+use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD as B64_ENGINE};
 
 use crate::{
     api::optional_principal,
     canonical_path::{canonicalize_item, canonicalize_tag},
-    form_template::template_json_compact,
-    html::{
-        ui_action::UI_RPC_FIELD,
-        user_can_post_room,
-        JsBuilder,
-    },
     events::ThreadCapability,
+    form_template::template_json_compact,
+    html::{JsBuilder, ui_action::UI_RPC_FIELD, user_can_post_room},
     path_types::ItemId,
-    reducer::{scope_from_room_wire, ContentState, ReducerState, ScopeId},
-    scope_rank::{build_children_rankings, ChildrenRankings},
+    reducer::{ContentState, ReducerState, ScopeId, scope_from_room_wire},
+    scope_rank::{ChildrenRankings, build_children_rankings},
     state::AppState,
     timeago,
 };
 
 use super::{
-    bc_path, bc_path_external, bc_segment, cli_panel, layout, layout_full_bleed_chromeless, now_ms,
-    ratio_pct, render_linkified_with_embeds_in_scope, theme_from_jar, theme_next_from_uri,
+    bc_path, bc_path_external, bc_segment,
     breadcrumb_path::{ExternalOntologyPath, OntologyPath},
-    forum::{ingest_entry_markup, ThreadNav},
+    cli_panel,
+    forum::{ThreadNav, ingest_entry_markup},
+    layout, layout_full_bleed_chromeless, now_ms, ratio_pct, render_linkified_with_embeds_in_scope,
+    theme_from_jar, theme_next_from_uri,
 };
 
 /// `GET /vote/compare` — pairs `left` / `right` query params with optional `thread`.
@@ -90,7 +88,11 @@ fn canonical_edge_items(a: &ItemId, b: &ItemId) -> (ItemId, ItemId) {
 }
 
 /// All votes whose endpoints are exactly this unordered pair (unsorted).
-fn edge_vote_entries_for_pair(content: &ContentState, a: &ItemId, b: &ItemId) -> Vec<crate::reducer::VoteData> {
+fn edge_vote_entries_for_pair(
+    content: &ContentState,
+    a: &ItemId,
+    b: &ItemId,
+) -> Vec<crate::reducer::VoteData> {
     let (lo, hi) = canonical_edge_items(a, b);
     let lo_s = lo.as_str();
     let hi_s = hi.as_str();
@@ -107,7 +109,11 @@ fn edge_vote_entries_for_pair(content: &ContentState, a: &ItemId, b: &ItemId) ->
         .collect()
 }
 
-fn ratios_for_compare_page(v: &crate::reducer::VoteData, page_left: &ItemId, page_right: &ItemId) -> (i32, i32) {
+fn ratios_for_compare_page(
+    v: &crate::reducer::VoteData,
+    page_left: &ItemId,
+    page_right: &ItemId,
+) -> (i32, i32) {
     let pl = page_left.as_str();
     let pr = page_right.as_str();
     match (v.a.as_str(), v.b.as_str()) {
@@ -121,11 +127,7 @@ fn left_share_normalized(ratio_left: i32, ratio_right: i32) -> f64 {
     let l = ratio_left.max(0) as f64;
     let r = ratio_right.max(0) as f64;
     let sum = l + r;
-    if sum <= 0.0 {
-        0.5
-    } else {
-        l / sum
-    }
+    if sum <= 0.0 { 0.5 } else { l / sum }
 }
 
 /// Stronger preference for **`page_left` first**; ties **newer first**.
@@ -177,11 +179,7 @@ fn vote_thread_tags_for_pair(content: &ContentState, a: &ItemId, b: &ItemId) ->
     v.into_iter().map(|t| canonicalize_tag(&t)).collect()
 }
 
-fn vote_edge_history_markup(
-    content: &ContentState,
-    left: &ItemId,
-    right: &ItemId,
-) -> maud::Markup {
+fn vote_edge_history_markup(content: &ContentState, left: &ItemId, right: &ItemId) -> maud::Markup {
     let votes = edge_vote_entries_for_pair(content, left, right);
     let votes = sort_votes_for_compare_display(votes, left, right);
     let legend_left = item_display_path(left.as_str());
@@ -276,7 +274,12 @@ fn item_code_label(item: &str) -> String {
     item_display_path(item)
 }
 
-fn vote_compare_href(nav: &ThreadNav, left: &ItemId, right: &ItemId, thread_override: Option<&str>) -> String {
+fn vote_compare_href(
+    nav: &ThreadNav,
+    left: &ItemId,
+    right: &ItemId,
+    thread_override: Option<&str>,
+) -> String {
     let left_q = urlencoding::encode(left.as_str());
     let right_q = urlencoding::encode(right.as_str());
     let base = format!(
@@ -299,7 +302,8 @@ fn ont_pin_vote_controls(
     next_path: &str,
 ) -> maud::Markup {
     let room_wire = nav.room_wire.clone();
-    let current = ItemId::parse(current_storage).unwrap_or_else(|| ItemId::opaque(current_storage.to_string()));
+    let current = ItemId::parse(current_storage)
+        .unwrap_or_else(|| ItemId::opaque(current_storage.to_string()));
     let pin_matches_scope = pinned_room_and_item
         .map(|(r, _)| r == nav.room_wire.as_str())
         .unwrap_or(false);
@@ -307,26 +311,22 @@ fn ont_pin_vote_controls(
         .filter(|_| pin_matches_scope)
         .map(|(_, i)| i);
 
-    let pin_rpc = template_json_compact(
-        &json!({
-            "action": "set_garden_pin",
-            "clear": false,
-            "room_wire": room_wire,
-            "item_storage": current.as_str(),
-            "next": next_path,
-            "form_action": "/ui",
-        }),
-    )
+    let pin_rpc = template_json_compact(&json!({
+        "action": "set_garden_pin",
+        "clear": false,
+        "room_wire": room_wire,
+        "item_storage": current.as_str(),
+        "next": next_path,
+        "form_action": "/ui",
+    }))
     .expect("pin rpc json");
-    let unpin_rpc = template_json_compact(
-        &json!({
-            "action": "set_garden_pin",
-            "clear": true,
-            "room_wire": "",
-            "next": next_path,
-            "form_action": "/ui",
-        }),
-    )
+    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! {
@@ -497,9 +497,9 @@ fn room_scope_has_garden_content(reduced: &ReducerState, nav: &ThreadNav) -> boo
 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",
-        ),
+        ScopeId::Room(_) => reduced
+            .content_for_scope(scope)
+            .expect("room garden only renders after room_scope_has_garden_content returned true"),
     }
 }
 
@@ -723,10 +723,8 @@ pub async fn room_external_garden_index(
     }
     let ext_path = ExternalOntologyPath::from_input("");
     let parent = ItemId::parse("https://.").unwrap();
-    let child_rankings = build_children_rankings(
-        content_for_garden_view(&reduced, &nav.scope()),
-        &parent,
-    );
+    let child_rankings =
+        build_children_rankings(content_for_garden_view(&reduced, &nav.scope()), &parent);
     drop(reduced);
 
     let page = layout(
@@ -953,48 +951,74 @@ fn build_rank_history(
         None => return vec![],
         Some(e) => e,
     };
-    entries.iter().map(|e| {
-        // Resolve caused_by: votes from this ingest that directly touched this item.
-        let caused_by: Vec<crate::reducer::VoteData> = reduced.ingests_by_id
-            .get(&e.post_id)
-            .and_then(|ing| crate::dsl::parse_full(&ing.raw).ok())
-            .map(|doc| {
-                doc.statements.into_iter().filter_map(|s| {
-                    if let crate::dsl::Stmt::Vote { item1, item2, ratio_left, ratio_right, explanation } = s {
-                        let a_str = crate::canonical_path::canonicalize_item(&item1);
-                        let b_str = crate::canonical_path::canonicalize_item(&item2);
-                        if a_str == item || b_str == item {
-                            Some(crate::reducer::VoteData {
-                                ts: e.ts,
-                                a: ItemId::parse(&a_str).unwrap_or_else(|| ItemId::opaque(a_str)),
-                                b: ItemId::parse(&b_str).unwrap_or_else(|| ItemId::opaque(b_str)),
-                                ratio_left, ratio_right,
-                                body: explanation,
-                                principal: reduced.ingests_by_id.get(&e.post_id)
-                                    .map(|ing| ing.principal.clone())
-                                    .unwrap_or_default(),
-                                delegate: reduced.ingests_by_id.get(&e.post_id).and_then(|ing| ing.delegate.clone()),
-                                thread_tag: e.thread.clone(),
-                            })
-                        } else { None }
-                    } else { None }
-                }).collect()
-            })
-            .unwrap_or_default();
-
-        let thread_post_index =
-            reduced.thread_post_index_chronological(scope, &e.thread, &e.post_id);
-
-        RankHistoryEntryView {
-            ts: e.ts,
-            scope_rank: e.scope_rank,
-            scope_total: e.scope_total,
-            scope_rank_delta: e.scope_rank_delta,
-            thread: e.thread.clone(),
-            thread_post_index,
-            caused_by,
-        }
-    }).collect()
+    entries
+        .iter()
+        .map(|e| {
+            // Resolve caused_by: votes from this ingest that directly touched this item.
+            let caused_by: Vec<crate::reducer::VoteData> = reduced
+                .ingests_by_id
+                .get(&e.post_id)
+                .and_then(|ing| crate::dsl::parse_full(&ing.raw).ok())
+                .map(|doc| {
+                    doc.statements
+                        .into_iter()
+                        .filter_map(|s| {
+                            if let crate::dsl::Stmt::Vote {
+                                item1,
+                                item2,
+                                ratio_left,
+                                ratio_right,
+                                explanation,
+                            } = s
+                            {
+                                let a_str = crate::canonical_path::canonicalize_item(&item1);
+                                let b_str = crate::canonical_path::canonicalize_item(&item2);
+                                if a_str == item || b_str == item {
+                                    Some(crate::reducer::VoteData {
+                                        ts: e.ts,
+                                        a: ItemId::parse(&a_str)
+                                            .unwrap_or_else(|| ItemId::opaque(a_str)),
+                                        b: ItemId::parse(&b_str)
+                                            .unwrap_or_else(|| ItemId::opaque(b_str)),
+                                        ratio_left,
+                                        ratio_right,
+                                        body: explanation,
+                                        principal: reduced
+                                            .ingests_by_id
+                                            .get(&e.post_id)
+                                            .map(|ing| ing.principal.clone())
+                                            .unwrap_or_default(),
+                                        delegate: reduced
+                                            .ingests_by_id
+                                            .get(&e.post_id)
+                                            .and_then(|ing| ing.delegate.clone()),
+                                        thread_tag: e.thread.clone(),
+                                    })
+                                } else {
+                                    None
+                                }
+                            } else {
+                       

… preview truncated; 4,089 characters omitted

download full diff A

B — c_3f420a1f5aa1 (tommy-mor)

message

[2fe70b0e] themes

diff preview

diff --git a/server/src/api/auth.rs b/server/src/api/auth.rs
index 559db65de14c6157690ffbf18eca0cf65b0a5202..b3631b06153d52f88348fef927e7a024b7b85ad6 100644
--- a/server/src/api/auth.rs
+++ b/server/src/api/auth.rs
@@ -1,7 +1,7 @@
 use axum::{
     body::Body,
     extract::{Path, Query, State},
-    http::{header, HeaderMap, HeaderValue, StatusCode},
+    http::{header, HeaderMap, HeaderValue, StatusCode, Uri},
     response::{IntoResponse, Redirect, Response},
     Form, Json,
 };
@@ -17,7 +17,7 @@ use crate::{
     events::{Event, GrantAdded, TokenIssued, UserRegistered},
     html::{
         auth_complete_page, auth_signed_in_fragment, choose_username_error_fragment,
-        choose_username_page, JsBuilder,
+        choose_username_page, theme_cookie_header_from_jar, theme_from_jar, theme_next_from_uri, JsBuilder,
     },
     identity::{parse_agent, parse_username},
     reducer::ReducerState,
@@ -48,15 +48,17 @@ fn js_form_error_fragment(session: &str, error: &str) -> Response {
         .into_response()
 }
 
-fn js_signed_in_fragment(bearer: &str) -> Response {
+fn js_signed_in_fragment(bearer: &str, jar: &CookieJar) -> Response {
     let mut response = JsBuilder::new()
         .id("choose-username-form")
         .morph_inner(auth_signed_in_fragment())
         .redirect("/auth/complete")
         .into_response();
-    response
-        .headers_mut()
-        .insert(header::SET_COOKIE, session_cookie_header_value(bearer));
+    let headers = response.headers_mut();
+    headers.append(header::SET_COOKIE, session_cookie_header_value(bearer));
+    if let Some(theme) = theme_cookie_header_from_jar(jar) {
+        headers.append(header::SET_COOKIE, theme);
+    }
     response
 }
 
@@ -69,13 +71,18 @@ pub fn optional_principal(headers: &HeaderMap, jar: &CookieJar, reduced: &Reduce
     verify_token(reduced, c.value()).ok()
 }
 
-fn redirect_with_session_cookie(public_url: &str, path_and_query: &str, bearer: &str) -> Response {
-    Response::builder()
+fn redirect_with_session_cookie(public_url: &str, path_and_query: &str, bearer: &str, jar: &CookieJar) -> Response {
+    let mut res = Response::builder()
         .status(StatusCode::TEMPORARY_REDIRECT)
         .header(header::LOCATION, format!("{public_url}{path_and_query}"))
-        .header(header::SET_COOKIE, session_cookie_header_value(bearer))
         .body(Body::empty())
-        .unwrap()
+        .unwrap();
+    let headers = res.headers_mut();
+    headers.append(header::SET_COOKIE, session_cookie_header_value(bearer));
+    if let Some(theme) = theme_cookie_header_from_jar(jar) {
+        headers.append(header::SET_COOKIE, theme);
+    }
+    res
 }
 
 async fn apply_invite_redemption(state: &AppState, invite_token: &str, grantee_username: &str) -> Result<(), String> {
@@ -286,7 +293,11 @@ pub struct AuthCallbackQuery {
     pub state: String,
 }
 
-pub async fn get_auth_callback(Query(q): Query<AuthCallbackQuery>, State(state): State<AppState>) -> impl IntoResponse {
+pub async fn get_auth_callback(
+    Query(q): Query<AuthCallbackQuery>,
+    State(state): State<AppState>,
+    jar: CookieJar,
+) -> impl IntoResponse {
     let sessions = pending_sessions(&state);
     {
         let sessions_read = sessions.read().await;
@@ -365,7 +376,7 @@ pub async fn get_auth_callback(Query(q): Query<AuthCallbackQuery>, State(state):
             }
             let cookie_bearer = bearer.clone();
             s.complete = Some((username, bearer));
-            return redirect_with_session_cookie(&public_url, "/", &cookie_bearer).into_response();
+            return redirect_with_session_cookie(&public_url, "/", &cookie_bearer, &jar).into_response();
         }
     }
 
@@ -378,14 +389,20 @@ pub struct ChooseUsernameQuery {
     pub error: Option<String>,
 }
 
-pub async fn get_choose_username(Query(q): Query<ChooseUsernameQuery>, State(state): State<AppState>) -> impl IntoResponse {
+pub async fn get_choose_username(
+    Query(q): Query<ChooseUsernameQuery>,
+    State(state): State<AppState>,
+    jar: CookieJar,
+    uri: Uri,
+) -> impl IntoResponse {
     let sessions = pending_sessions(&state);
     let sessions_read = sessions.read().await;
     if !sessions_read.contains_key(&q.session) {
         return api_error(StatusCode::NOT_FOUND, "unknown session", None).into_response();
     }
     drop(sessions_read);
-    choose_username_page(&q.session, q.error.as_deref()).into_response()
+    let next = theme_next_from_uri(&uri);
+    choose_username_page(&q.session, q.error.as_deref(), theme_from_jar(&jar), &next).into_response()
 }
 
 #[derive(Debug, Deserialize)]
@@ -396,6 +413,7 @@ pub struct ChooseUsernameForm {
 
 pub async fn post_choose_username(
     State(state): State<AppState>,
+    jar: CookieJar,
     Form(form): Form<ChooseUsernameForm>,
 ) -> impl IntoResponse {
     let canon_user = match parse_username(&form.username) {
@@ -477,7 +495,7 @@ pub async fn post_choose_username(
         s.complete = Some((canon_user.clone(), bearer.clone()));
     }
 
-    js_signed_in_fragment(&bearer).into_response()
+    js_signed_in_fragment(&bearer, &jar).into_response()
 }
 
 /// Start a browser-only OAuth flow (no CLI polling). Sets session cookie on success.
@@ -569,8 +587,9 @@ pub async fn get_pending_session(
     .into_response()
 }
 
-pub async fn get_auth_complete() -> impl IntoResponse {
-    auth_complete_page()
+pub async fn get_auth_complete(jar: CookieJar, uri: Uri) -> impl IntoResponse {
+    let next = theme_next_from_uri(&uri);
+    auth_complete_page(theme_from_jar(&jar), &next).into_response()
 }
 
 pub async fn get_whoami(State(state): State<AppState>, headers: HeaderMap) -> impl IntoResponse {
diff --git a/server/src/api/helpers.rs b/server/src/api/helpers.rs
index 81e2a55fa3abb8609b4099f91a989480336e11eb..9b71491e9f9efc44a2a4beba09be8f64bd2ff2ee 100644
--- a/server/src/api/helpers.rs
+++ b/server/src/api/helpers.rs
@@ -39,6 +39,55 @@ pub fn item_path_for_api(item: &str) -> String {
     }
 }
 
+/// Same as [`item_path_for_api`], but for private rooms ontology items are prefixed with
+/// `/r/{short}/{slug}` so the URL matches the web app (`/r/…/~/…` routes).
+pub fn item_path_for_api_in_room(item: &str, room_wire: &str) -> String {
+    let room = room_wire.trim();
+    if room.is_empty() || room == "public" {
+        return item_path_for_api(item);
+    }
+    let Some((short, slug)) = room.split_once('/') else {
+        return item_path_for_api(item);
+    };
+    if short.is_empty() || slug.is_empty() {
+        return item_path_for_api(item);
+    }
+    let Some(c) = CanonicalItemUrl::parse(item) else {
+        return item_path_for_api(item);
+    };
+    let root = CanonicalItemUrl::ontology_root();
+    let item_norm = c.as_str().trim_end_matches('/');
+    let root_norm = root.as_str().trim_end_matches('/');
+    if let Some(tail) = c.tilde_tail() {
+        return if tail.is_empty() {
+            format!("https://slug.social/r/{short}/{slug}/~")
+        } else {
+            format!("https://slug.social/r/{short}/{slug}/~/{}", tail)
+        };
+    }
+    if item_norm == root_norm {
+        return format!("https://slug.social/r/{short}/{slug}/~");
+    }
+    item_path_for_api(item)
+}
+
+/// Absolute thread URL for forum JSON (`/t/…` vs `/r/…/t/…`).
+pub fn forum_thread_web_url(room_wire: &str, thread_tag: &str) -> String {
+    let room = room_wire.trim();
+    let tag = thread_tag.trim().trim_start_matches('#');
+    if room.is_empty() || room == "public" {
+        format!("https://slug.social/t/{tag}")
+    } else if let Some((short, slug)) = room.split_once('/') {
+        if short.is_empty() || slug.is_empty() {
+            format!("https://slug.social/t/{tag}")
+        } else {
+            format!("https://slug.social/r/{short}/{slug}/t/{tag}")
+        }
+    } else {
+        format!("https://slug.social/t/{tag}")
+    }
+}
+
 /// Resolve an item path as a first-class canonical path.
 pub fn resolve_item(item: &str) -> Result<String, String> {
     let canonical = canonicalize_item(item);
@@ -188,3 +237,52 @@ pub fn vote_touches_path(a: &str, b: &str, parent_canon: &str) -> bool {
     let under = |item: &str| item == parent_canon || item.starts_with(&format!("{}/", parent_canon));
     under(a) || under(b)
 }
+
+#[cfg(test)]
+mod wire_url_tests {
+    use super::{forum_thread_web_url, item_path_for_api_in_room};
+
+    #[test]
+    fn public_room_unchanged() {
+        let u = "https://slug.social/~/a/b";
+        assert_eq!(item_path_for_api_in_room(u, "public"), u);
+    }
+
+    #[test]
+    fn private_room_prefixes_ontology() {
+        assert_eq!(
+            item_path_for_api_in_room("https://slug.social/~/topic/x", "9ab12cd/my-room"),
+            "https://slug.social/r/9ab12cd/my-room/~/topic/x"
+        );
+    }
+
+    #[test]
+    fn private_room_ontology_root() {
+        assert_eq!(
+            item_path_for_api_in_room("https://slug.social/~", "9ab12cd/my-room"),
+            "https://slug.social/r/9ab12cd/my-room/~"
+        );
+        assert_eq!(
+            item_path_for_api_in_room("https://slug.social/~/", "9ab12cd/my-room"),
+            "https://slug.social/r/9ab12cd/my-room/~"
+        );
+    }
+
+    #[test]
+    fn external_url_untouched_in_private_room() {
+        let u = "https://example.com/z";
+        assert_eq!(item_path_for_api_in_room(u, "9ab12cd/my-room"), u);
+    }
+
+    #[test]
+    fn forum_web_public_vs_room() {
+        assert_eq!(
+            forum_thread_web_url("public", "debate"),
+            "https://slug.social/t/debate"
+        );
+        assert_eq!(
+            forum_thread_web_url("9ab12cd/my-room", "#debate"),
+            "https://slug.social/r/9ab12cd/my-room/t/debate"
+        );
+    }
+}
diff --git a/server/src/api/rpc.rs b/server/src/api/rpc.rs
index 31f5fcfb4eaf4df0a9cbac532dd3dedfe3611810..5b91f5836625eedbb1cd9423168046e3fb576c17 100644
--- a/server/src/api/rpc.rs
+++ b/server/src/api/rpc.rs
@@ -27,8 +27,9 @@ use crate::{
 
 use super::auth::verify_bearer_principal;
 use super::helpers::{
-    compute_connectivity_stats, is_pair_voted, item_path_for_api, now_ms, paginate_rankings,
-    parse_parent_specs, pick_random_distinct, resolve_item, vote_touches_path,
+    compute_connectivity_stats, forum_thread_web_url, is_pair_voted, item_path_for_api,
+    item_path_for_api_in_room, now_ms, paginate_rankings, parse_parent_specs, pick_random_distinct,
+    resolve_item, vote_touches_path,
 };
 use super::validate::{normalize_room_and_thread, validate_ingest_document};
 
@@ -148,6 +149,7 @@ fn compute_scope_rank_changes(
     parent: &str,
     before: &crate::scope_rank::ChildrenRankings,
     after: &crate::scope_rank::ChildrenRankings,
+    room_wire: &str,
 ) -> Option<ScopeRankChanges> {
     fn build_positions(rankings: &crate::scope_rank::ChildrenRankings) -> HashMap<String, Option<RankPosition>> {
         let mut map = HashMap::new();
@@ -182,7 +184,7 @@ fn compute_scope_rank_changes(
         };
         if changed {
             changes.push(RankChange {
-                item: item_path_for_api(&item),
+                item: item_path_for_api_in_room(&item, room_wire),
                 before: b,
                 after: a,
             });
@@ -204,7 +206,7 @@ fn compute_scope_rank_changes(
         parent: if parent.is_empty() {
             "/".to_string()
         } else {
-            item_path_for_api(parent)
+            item_path_for_api_in_room(parent, room_wire)
         },
         changes,
     })
@@ -256,6 +258,7 @@ fn build_rank_response_for_content(
     offset: usize,
     limit: Option<usize>,
     want_percent: bool,
+    room_wire: &str,
 ) -> Result<RankResponse, RpcErr> {
     let parent_owned = parent.map(|s| s.to_string());
     let specs = parse_parent_specs(parent_owned.as_ref());
@@ -299,7 +302,7 @@ fn build_rank_response_for_content(
                     .ranked
                     .into_iter()
                     .map(|r| RankRow {
-                  

… preview truncated; 42,928 characters omitted

download full diff B

Hardlinks — judgments / attempts / prompt

prompt download

judgments

attempts

Prompt text is loaded only by the download route.