Side A is pure file reorganization (splitting a monolith into modules) with no behavioral change, which has value but is mechanical churn. Side B introduces a real type-safety improvement by threading CanonicalItemUrl through resolve_item, validate, and rank computations instead of raw strings, reducing stringly-typed bugs, plus adds Deref impls that reduce boilerplate call sites—this is a more substantive, lasting design improvement to the codebase's type system.
constitution · epochs · watch · epoch 3
c_ebc883667d61 (tommy-mor) vs c_c42f908efc44 (tommy-mor)
download prompt · raw event · cmp_854a8d87d6adbd
council reasoning
B tightens core path typing end-to-end (resolve_item → CanonicalItemUrl, typed pools/maps in validate/rank/pair RPC, Deref on href newtypes), removing repeated String↔CanonicalItemUrl glue and hardening the API model. A’s lasting piece is real but smaller: deleting leftover html/forum.rs so forum/ is the sole module root; most of the huge diff is relocated split-module code rather than new behavior.
Side B makes a substantive type-safety refactor by changing `resolve_item` to return `CanonicalItemUrl`, propagating canonical path types through validation, ranking, pair selection, and RPC code, and adding `Deref<Target=str>` plus `from_stored` usage to reduce string conversions and parsing errors. Side A primarily removes the duplicate `server/src/html/forum.rs` and establishes `forum/mod.rs` as the sole module root—a useful structural cleanup that resolves module ambiguity, but it largely reorganizes existing code rather than strengthening core behavior.
sides
A — c_ebc883667d61 (tommy-mor)
message
[21b57b50] fix(html): drop duplicate forum.rs; use forum/ as sole module root Removes the leftover monolithic file so mod forum resolves to forum/mod.rs without ambiguity. Keeps feed, views, and other split modules as the source. Made-with: Cursor
diff preview
diff --git a/server/src/html/forum.rs b/server/src/html/forum.rs
deleted file mode 100644
index 5ad8dfc84dd735d589432e2c613ff687a75f2e61..0000000000000000000000000000000000000000
--- a/server/src/html/forum.rs
+++ /dev/null
@@ -1,1405 +0,0 @@
-use axum::{
- extract::{Path, Query, State},
- http::{HeaderMap, StatusCode, Uri},
- response::{Html, IntoResponse},
-};
-use axum_extra::extract::cookie::CookieJar;
-use maud::{html, Markup};
-use serde::Deserialize;
-
-use crate::{
- api::optional_principal,
- canonical_path::{canonicalize_item, canonicalize_tag},
- events::ThreadCapability,
- form_template::template_json_compact,
- identity::parse_username,
- reducer::{scope_from_room_wire, ReducerState, ScopeId},
- state::AppState,
- timeago,
-};
-use serde_json::json;
-
-use super::js_string_literal;
-use super::ui_action::{HtmlUiAction, UI_RPC_FIELD};
-
-use super::{
- bc_segment, bc_threads, cli_panel, layout, now_ms, profile_href, recency_class,
- render_linkified_with_embeds_in_scope, theme_from_jar, theme_next_from_uri, JsBuilder,
-};
-
-#[derive(Clone)]
-struct ThreadRow {
- tag: String,
- subtitle: Option<String>,
- last_ts: i64,
- ingests: usize,
-}
-
-#[derive(Clone)]
-struct RoomMemberRow {
- username: String,
- capabilities: Vec<&'static str>,
-}
-
-/// URL helpers for public `/t/…` and private room threads `/r/{short}/{slug}/t/…`.
-#[derive(Clone)]
-pub struct ThreadNav {
- pub room_wire: String,
- scope: ScopeId,
- room_path: String,
- thread_path_prefix: String,
- garden_path_prefix: String,
-}
-
-impl ThreadNav {
- pub(crate) fn public() -> Self {
- Self {
- room_wire: "public".into(),
- scope: ScopeId::Public,
- room_path: "/t".into(),
- thread_path_prefix: "/t".into(),
- garden_path_prefix: "/~".into(),
- }
- }
-
- /// `room_id` wire form `shortid/slug`.
- pub(crate) fn from_room_id(room_id: &str) -> Option<Self> {
- let (short, slug) = room_id.split_once('/')?;
- if short.is_empty() || slug.is_empty() {
- return None;
- }
- Some(Self {
- room_wire: room_id.to_string(),
- scope: ScopeId::Room(room_id.to_string()),
- room_path: format!("/r/{short}/{slug}"),
- thread_path_prefix: format!("/r/{short}/{slug}/t"),
- garden_path_prefix: format!("/r/{short}/{slug}/~"),
- })
- }
-
- pub(crate) fn scope(&self) -> ScopeId {
- self.scope.clone()
- }
-
- pub(crate) fn room_url(&self) -> &str {
- &self.room_path
- }
-
- pub(crate) fn thread_url(&self, tag: &str) -> String {
- format!("{}/{}", self.thread_path_prefix, tag)
- }
-
- pub(crate) fn garden_root_url(&self) -> &str {
- &self.garden_path_prefix
- }
-
- pub(crate) fn garden_item_url(&self, item: &str) -> String {
- if let Some(tail) = crate::path_types::CanonicalItemUrl::parse(item)
- .and_then(|c| c.tilde_tail().map(str::to_owned))
- {
- format!("{}/{}", self.garden_path_prefix, tail)
- } else {
- format!("{}/{}", self.garden_path_prefix, canonicalize_item(item))
- }
- }
-
- fn thread_page_url(&self, tag: &str, offset: usize) -> String {
- let base = self.thread_url(tag);
- if offset == 0 {
- base
- } else {
- format!("{base}?offset={offset}")
- }
- }
-
- fn post_url(&self, tag: &str, idx: usize) -> String {
- format!("{}/{}/{}", self.thread_path_prefix, tag, idx)
- }
-}
-
-/// `POST /ui` + `__rpc__` from an inline link (`onclick`); same-origin credentials as other morph actions.
-fn thread_ui_fetch_onclick(rpc_compact_json: &str) -> String {
- format!(
- "fetch('/ui',{{method:'POST',headers:{{'Content-Type':'application/x-www-form-urlencoded'}},body:new URLSearchParams({{__rpc__:{}}}).toString(),credentials:'same-origin'}}).then(r=>r.text()).then(eval);return false",
- js_string_literal(rpc_compact_json)
- )
-}
-
-fn thread_nav_for_ingest(ing: &crate::events::Ingest) -> Option<ThreadNav> {
- let room = ing.room_id.trim();
- if room.is_empty() || room == "public" {
- Some(ThreadNav::public())
- } else {
- ThreadNav::from_room_id(room)
- }
-}
-
-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))
-}
-
-fn post_header_meta(
- nav: &ThreadNav,
- tag: &str,
- post_idx: usize,
- principal: &str,
- ts: i64,
- now: i64,
-) -> Markup {
- let post_href = nav.post_url(tag, post_idx);
- let profile = profile_href(principal);
- let hover = timeago::rfc3339_utc(ts);
- let ago = timeago::timeago(now, ts);
- html! {
- div class="ingest-meta muted" title=(hover) {
- a href=(post_href) class="post-num" { "#" (post_idx) }
- " "
- a href=(profile) class="post-author" { "@" (principal) }
- " · "
- (ago)
- }
- }
-}
-
-fn post_header_row(
- nav: &ThreadNav,
- tag: &str,
- post_idx: usize,
- ing: &crate::events::Ingest,
- _viewer: Option<&str>,
- now: i64,
- show_delete: bool,
-) -> Markup {
- let meta = post_header_meta(nav, tag, post_idx, &ing.principal, ing.ts, now);
- html! {
- div class="ingest-header-row" {
- (meta)
- @if show_delete {
- form class="post-delete-form" method="POST" action="/ui" {
- input type="hidden" name=(UI_RPC_FIELD) value=(template_json_compact(&HtmlUiAction::RedactPost { post_id: ing.id.clone() }).unwrap());
- button type="submit" class="post-delete-btn" { "delete" }
- }
- }
- }
- }
-}
-
-fn redacted_header_row(
- nav: &ThreadNav,
- tag: &str,
- post_idx: usize,
- ing: &crate::events::Ingest,
- now: i64,
- expanded: bool,
-) -> Markup {
- let meta = post_header_meta(nav, tag, post_idx, &ing.principal, ing.ts, now);
- let rpc_expand = template_json_compact(&json!({
- "action": "expand_redacted_post",
- "room": nav.room_wire,
- "thread_tag": tag,
- "post_index": post_idx,
- }))
- .unwrap();
- let rpc_collapse = template_json_compact(&json!({
- "action": "collapse_redacted_post",
- "room": nav.room_wire,
- "thread_tag": tag,
- "post_index": post_idx,
- }))
- .unwrap();
- let onclick_expand = thread_ui_fetch_onclick(&rpc_expand);
- let onclick_collapse = thread_ui_fetch_onclick(&rpc_collapse);
- html! {
- div class="ingest-header-row ingest-tombstone-row" {
- (meta)
- span class="post-tombstone-inline muted" {
- "deleted · "
- @if expanded {
- a href="#" class="hide-deleted-link"
- onclick=(onclick_collapse) {
- "[hide deleted content]"
- }
- } @else {
- a href="#" class="show-deleted-link"
- onclick=(onclick_expand) {
- "[show deleted content]"
- }
- }
- }
- }
- }
-}
-
-fn ingest_entry_markup(
- nav: &ThreadNav,
- tag: &str,
- post_idx: usize,
- ing: &crate::events::Ingest,
- viewer: Option<&str>,
- now: i64,
- reduced: &ReducerState,
-) -> Markup {
- let redacted = reduced.redacted_posts.contains(&ing.id);
- let show_delete = viewer == Some(ing.principal.as_str()) && !redacted;
- if redacted {
- html! {
- div class="ingest-entry ingest-redacted" data-ingest-id=(ing.id) {
- (redacted_header_row(nav, tag, post_idx, ing, now, false))
- }
- }
- } else {
- let truncated = ing.raw.len() > 2000;
- let display_body = if truncated { &ing.raw[..2000] } else { &ing.raw[..] };
- html! {
- div class="ingest-entry" data-ingest-id=(ing.id) {
- (post_header_row(nav, tag, post_idx, ing, viewer, now, show_delete))
- (render_linkified_with_embeds_in_scope(display_body, nav.garden_root_url()))
- @if truncated {
- @let rpc_full = template_json_compact(&json!({
- "action": "expand_post_full",
- "room": nav.room_wire,
- "thread_tag": tag,
- "post_index": post_idx,
- })).unwrap();
- @let onclick_full = thread_ui_fetch_onclick(&rpc_full);
- a href="#" class="show-full-link"
- onclick=(onclick_full) {
- "[show full post]"
- }
- }
- }
- }
- }
-}
-
-fn collect_thread_rows_for_scope(reduced: &ReducerState, scope: &ScopeId, now: i64) -> Vec<ThreadRow> {
- let _ = now;
- reduced
- .forum_threads
- .iter()
- .filter(|((s, _), _)| s == scope)
- .map(|((_, tag), thread)| {
- let ingests = reduced
- .ingests_by_scope_thread
- .get(&(scope.clone(), tag.clone()))
- .map(|q| q.len())
- .unwrap_or(0);
- ThreadRow {
- tag: tag.clone(),
- subtitle: None,
- last_ts: thread.last_activity_ts,
- ingests,
- }
- })
- .collect()
-}
-
-fn rooms_for_user(reduced: &ReducerState, username: &str) -> Vec<String> {
- let mut v: Vec<String> = reduced
- .grants
- .iter()
- .filter(|(rid, m)| reduced.rooms.contains(*rid) && m.contains_key(username))
- .map(|(rid, _)| rid.clone())
- .collect();
- v.sort();
- v
-}
-
-pub(crate) fn user_can_view_room(reduced: &ReducerState, room_id: &str, username: Option<&str>) -> bool {
- if !reduced.rooms.contains(room_id) {
- return false;
- }
- let Some(u) = username else {
- return false;
- };
- reduced.user_has_cap(room_id, u, ThreadCapability::View)
-}
-
-pub(crate) fn user_can_post_room(reduced: &ReducerState, room_id: &str, username: &str) -> bool {
- reduced.user_has_cap(room_id, username, ThreadCapability::Post)
-}
-
-fn capability_label(cap: ThreadCapability) -> &'static str {
- match cap {
- ThreadCapability::View => "view",
- ThreadCapability::Post => "post",
- ThreadCapability::Vote => "vote",
- ThreadCapability::AddItem => "add_item",
- ThreadCapability::Manage => "manage",
- }
-}
-
-fn room_members_for_room(reduced: &ReducerState, room_id: &str) -> Vec<RoomMemberRow> {
- let mut rows: Vec<RoomMemberRow> = reduced
- .grants
- .get(room_id)
- .into_iter()
- .flat_map(|members| members.iter())
- .map(|(username, caps)| {
- let mut ordered = Vec::new();
- for cap in [
- ThreadCapability::View,
- ThreadCapability::Post,
- ThreadCapability::Vote,
- ThreadCapability::AddItem,
- ThreadCapability::Manage,
- ] {
- if caps.contains(&cap) {
- ordered.push(capability_label(cap));
- }
- }
- RoomMemberRow {
- username: username.clone(),
- capabilities: ordered,
- }
- })
- .collect();
- rows.sort_by(|a, b| a.username.cmp(&b.username));
- rows
-}
-
-fn room_members_inner(members: &[RoomMemberRow]) -> Markup {
- html! {
- h3 { "members
… preview truncated; 77,260 characters omittedB — c_c42f908efc44 (tommy-mor)
message
[674964ef] refactor: Deref for href newtypes, CanonicalItemUrl through resolve_item - Implement Deref<Target=str> for GardenItemUrl, ForumThreadUrl, TildeOntologyPath - resolve_item returns CanonicalItemUrl; validate uses HashSet<CanonicalItemUrl> - compute_scope_rank_changes keys are CanonicalItemUrl; pair RPC uses Vec pool - pick_random_distinct_canonical; connectivity stats on &[CanonicalItemUrl] - Global rank unranked uses stored ids before GardenItemUrl mapping Made-with: Cursor
diff preview
diff --git a/server/src/api/helpers.rs b/server/src/api/helpers.rs
index 03b3e77911ccd662bec8635345dafe2593cf242e..1b291db83df7364a026f2e147e0a29a70a399371 100644
--- a/server/src/api/helpers.rs
+++ b/server/src/api/helpers.rs
@@ -30,13 +30,13 @@ pub fn now_ms() -> i64 {
t.as_millis() as i64
}
-/// Resolve an item path as a first-class canonical path.
-pub fn resolve_item(item: &str) -> Result<String, String> {
+/// Resolve DSL/user input to a stored canonical item id.
+pub fn resolve_item(item: &str) -> Result<CanonicalItemUrl, String> {
let canonical = canonicalize_item(item);
if canonical.is_empty() {
return Err(format!("empty item path: `{}`", item));
}
- Ok(canonical)
+ Ok(CanonicalItemUrl(canonical))
}
pub fn parse_parent_specs(parent: Option<&String>) -> Vec<String> {
@@ -94,7 +94,7 @@ pub fn paginate_rankings(
(out_components, out_unranked)
}
-pub fn pick_random_distinct(items: &[String]) -> Option<(String, String)> {
+pub fn pick_random_distinct_canonical(items: &[CanonicalItemUrl]) -> Option<(CanonicalItemUrl, CanonicalItemUrl)> {
use rand::seq::SliceRandom;
if items.len() < 2 {
return None;
@@ -123,15 +123,12 @@ pub fn is_pair_voted(group: &crate::reducer::GroupState, a: &str, b: &str) -> bo
group.voted_pairs.contains(&(i, j))
}
-pub fn compute_connectivity_stats(group: &crate::reducer::GroupState, pool: &[String]) -> ConnectivityStats {
+pub fn compute_connectivity_stats(group: &crate::reducer::GroupState, pool: &[CanonicalItemUrl]) -> ConnectivityStats {
let n = pool.len();
let global_idxs: Vec<Option<usize>> = pool
.iter()
- .map(|it| {
- let key = CanonicalItemUrl(it.clone());
- group.item_to_idx.get(&key).copied()
- })
+ .map(|it| group.item_to_idx.get(it).copied())
.collect();
let present: Vec<usize> = global_idxs.iter().filter_map(|x| *x).collect();
diff --git a/server/src/api/mod.rs b/server/src/api/mod.rs
index cf22cb0129366c3aed031bc86f3197a4321cb806..a10ce662105cff8fad949c6b83f7035ce79bed18 100644
--- a/server/src/api/mod.rs
+++ b/server/src/api/mod.rs
@@ -24,7 +24,7 @@ pub use auth::{
pub use helpers::{
api_error, compute_connectivity_stats, is_pair_voted, now_ms, paginate_rankings,
- parse_parent_specs, pick_random_distinct, resolve_item, sha256_hex, vote_touches_path,
+ parse_parent_specs, pick_random_distinct_canonical, resolve_item, sha256_hex, vote_touches_path,
};
pub use rpc::handle_rpc_batch;
diff --git a/server/src/api/rpc.rs b/server/src/api/rpc.rs
index 5f7d50188f1381267402f2e57e671234ef5db2fd..de0955d0887d32740e1fd18365205c5bdb53c247 100644
--- a/server/src/api/rpc.rs
+++ b/server/src/api/rpc.rs
@@ -29,7 +29,7 @@ use crate::{
use super::auth::verify_bearer_principal;
use super::helpers::{
compute_connectivity_stats, is_pair_voted, now_ms, paginate_rankings, parse_parent_specs,
- pick_random_distinct, resolve_item, vote_touches_path,
+ pick_random_distinct_canonical, resolve_item, vote_touches_path,
};
use super::validate::{normalize_room_and_thread, validate_ingest_document};
@@ -146,21 +146,21 @@ fn authorize_room_read(reduced: &ReducerState, headers: &HeaderMap, room: &str)
}
fn compute_scope_rank_changes(
- parent: &str,
+ parent: &CanonicalItemUrl,
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>> {
+ fn build_positions(rankings: &crate::scope_rank::ChildrenRankings) -> HashMap<CanonicalItemUrl, Option<RankPosition>> {
let mut map = HashMap::new();
for comp in &rankings.component_rankings {
let total = comp.ranked.len();
for (i, item) in comp.ranked.iter().enumerate() {
- map.insert(item.item.as_str().to_string(), Some(RankPosition { rank: i + 1, of: total }));
+ map.insert(item.item.clone(), Some(RankPosition { rank: i + 1, of: total }));
}
}
for item in &rankings.unranked_items {
- map.insert(item.as_str().to_string(), None);
+ map.insert(item.clone(), None);
}
map
}
@@ -168,7 +168,7 @@ fn compute_scope_rank_changes(
let before_pos = build_positions(before);
let after_pos = build_positions(after);
- let all_items: std::collections::BTreeSet<String> = before_pos.keys().cloned()
+ let all_items: std::collections::BTreeSet<CanonicalItemUrl> = before_pos.keys().cloned()
.chain(after_pos.keys().cloned())
.collect();
@@ -184,7 +184,7 @@ fn compute_scope_rank_changes(
};
if changed {
changes.push(RankChange {
- item: GardenItemUrl::from_storage_str(&item, room_wire),
+ item: GardenItemUrl::from_stored(&item, room_wire),
before: b,
after: a,
});
@@ -203,11 +203,7 @@ fn compute_scope_rank_changes(
});
Some(ScopeRankChanges {
- parent: if parent.is_empty() {
- "/".to_string()
- } else {
- GardenItemUrl::from_storage_str(parent, room_wire).into_inner()
- },
+ parent: GardenItemUrl::from_stored(parent, room_wire).into_inner(),
changes,
})
}
@@ -473,8 +469,8 @@ async fn rpc_post(
for s in &v.doc.statements {
if let dsl::Stmt::Vote { item1, item2, .. } = s {
if let (Ok(a), Ok(b)) = (resolve_item(item1), resolve_item(item2)) {
- if let Some(p) = CanonicalItemUrl::parse(&a).and_then(|c| c.parent()) { parents.insert(p); }
- if let Some(p) = CanonicalItemUrl::parse(&b).and_then(|c| c.parent()) { parents.insert(p); }
+ if let Some(p) = a.parent() { parents.insert(p); }
+ if let Some(p) = b.parent() { parents.insert(p); }
}
}
}
@@ -525,7 +521,7 @@ async fn rpc_post(
.filter_map(|p| {
let before = pre_rankings.get(p)?;
let after = crate::scope_rank::build_children_rankings(content, p);
- compute_scope_rank_changes(p.as_str(), before, &after, &room_key)
+ compute_scope_rank_changes(p, before, &after, &room_key)
})
.collect();
if v.is_empty() { None } else { Some(v) }
@@ -638,8 +634,8 @@ async fn rpc_check(
for s in &v.doc.statements {
if let dsl::Stmt::Vote { item1, item2, .. } = s {
if let (Ok(a), Ok(b)) = (resolve_item(item1), resolve_item(item2)) {
- if let Some(p) = CanonicalItemUrl::parse(&a).and_then(|c| c.parent()) { parents.insert(p); }
- if let Some(p) = CanonicalItemUrl::parse(&b).and_then(|c| c.parent()) { parents.insert(p); }
+ if let Some(p) = a.parent() { parents.insert(p); }
+ if let Some(p) = b.parent() { parents.insert(p); }
}
}
}
@@ -961,7 +957,7 @@ fn rpc_search(reduced: &ReducerState, q: &str, limit: usize, principal: Option<&
async fn rpc_get_pair(state: &AppState, room: String, parent_path: String) -> Result<RpcResult, RpcErr> {
let scope = scope_from_room_wire(&room);
let reduced_arc = state.reduced.clone();
- let pool: Vec<String> = {
+ let pool: Vec<CanonicalItemUrl> = {
let reduced = reduced_arc.read().await;
let content = content_for_room(&reduced, &room);
let tmp = if parent_path.trim().is_empty() {
@@ -970,12 +966,11 @@ async fn rpc_get_pair(state: &AppState, room: String, parent_path: String) -> Re
Some(parent_path.clone())
};
let specs = parse_parent_specs(tmp.as_ref());
- let raw_pool: Vec<CanonicalItemUrl> = if specs.is_empty() {
+ if specs.is_empty() {
content.ranking_group.idx_to_item.clone()
} else {
crate::scope_rank::resolve_scope(content, &specs)
- };
- raw_pool.into_iter().map(|it| it.0).collect()
+ }
};
if pool.len() < 2 {
return Err((
@@ -983,31 +978,30 @@ async fn rpc_get_pair(state: &AppState, room: String, parent_path: String) -> Re
Some("add items via ingest".into()),
));
}
- let selected: Option<(String, String)> = {
+ let selected: Option<(CanonicalItemUrl, CanonicalItemUrl)> = {
let mut reduced = reduced_arc.write().await;
let content = reduced.content.entry(scope.clone()).or_default();
let group = &mut content.ranking_group;
if group.idx_to_item.is_empty() {
- pick_random_distinct(&pool)
+ pick_random_distinct_canonical(&pool)
} else {
let mut rng = rand::thread_rng();
- let idxs: Vec<usize> = pool.iter()
- .filter_map(|it| {
- let key = CanonicalItemUrl(it.clone());
- group.item_to_idx.get(&key).copied()
- })
+ let idxs: Vec<usize> = pool
+ .iter()
+ .filter_map(|it| group.item_to_idx.get(it).copied())
.collect();
let ranked = ranked_items_subset(group, &idxs, 10000, 1e-8);
- let ranked_set: HashSet<String> = ranked.iter().map(|r| r.item.as_str().to_string()).collect();
- let unsorted: Vec<String> = pool.iter()
+ let ranked_set: HashSet<CanonicalItemUrl> = ranked.iter().map(|r| r.item.clone()).collect();
+ let unsorted: Vec<CanonicalItemUrl> = pool
+ .iter()
.filter(|it| !ranked_set.contains(*it))
.cloned()
.collect();
- let mut pick: Option<(String, String)> = None;
+ let mut pick: Option<(CanonicalItemUrl, CanonicalItemUrl)> = None;
if !unsorted.is_empty() {
if let Some(left) = unsorted.choose(&mut rng).cloned() {
- let mut candidates: Vec<String> = if !ranked.is_empty() {
- ranked.iter().map(|r| r.item.as_str().to_string()).collect()
+ let mut candidates: Vec<CanonicalItemUrl> = if !ranked.is_empty() {
+ ranked.iter().map(|r| r.item.clone()).collect()
} else {
pool.clone()
};
@@ -1021,21 +1015,21 @@ async fn rpc_get_pair(state: &AppState, room: String, parent_path: String) -> Re
let a = ranked[i].item.as_str();
let b = ranked[i + 1].item.as_str();
if a != b && !is_pair_voted(group, a, b) {
- pick = Some((a.to_string(), b.to_string()));
+ pick = Some((ranked[i].item.clone(), ranked[i + 1].item.clone()));
break;
}
}
if pick.is_none() {
for _ in 0..64 {
let (Some(a), Some(b)) = (pool.choose(&mut rng).cloned(), pool.choose(&mut rng).cloned()) else { break; };
- if a != b && !is_pair_voted(group, &a, &b) {
+ if a != b && !is_pair_voted(group, a.as_str(), b.as_str()) {
pick = Some((a, b));
break;
}
}
}
}
- pick.or_else(|| pick_random_distinct(&pool))
+ pick.or_else(|| pick_random_distinct_canonical(&pool))
}
};
let Some((left, right)) = selected else {
@@ -1043,8 +1037,8 @@ async fn rpc_get_pair(state: &AppState, room: String, parent_path: String) -> Re
};
let reduced = reduced_arc.read().await;
let content = content_for_ro
… preview truncated; 8,683 characters omittedHardlinks — judgments / attempts / prompt
judgments
attempts
Prompt text is loaded only by the download route.