You are a constitutional council ranking individual git commits for ownership allocation. Compare these two commits. Decide which contributed more lasting value to the project. Judge substance, not spectacle: - Prefer correct, lasting design and real bugfixes over churn, formatting, renames, or generated noise. - Prefer clarity and necessity over sheer line count. A small precise change can beat a large diffuse one. - Do not favor a side merely because its patch is longer or noisier. - Weight what the change does for the project, not the contributor's name. Return ONLY a JSON object: {"winner": "A" or "B", "ratio": "N:M", "explanation": "..."} The explanation must cite concrete differences in the patches (1-3 sentences). Side A — contributor: tommy-mor Side A — commit message: [09842c93] remove shell Side A — unified diff (full patch): diff --git a/server/src/html/garden.rs b/server/src/html/garden.rs index 8fc6be1de8dd975f9547de615809222236be4b70..76ba96e45c9e16291a6ccd8096fdd01b274c1560 100644 --- a/server/src/html/garden.rs +++ b/server/src/html/garden.rs @@ -1321,55 +1321,53 @@ async fn vote_compare_inner( .expect("vote compare rpc json"); let body = html! { - section class="vote-compare-shell" { - h2 { "compare" } - div class="vote-compare-pair" { - a class="vote-compare-item" href=(nav.garden_item_href(&left)) { - code { (item_display_path(left.as_str())) } - } - span class="vote-compare-vs" { "vs" } - a class="vote-compare-item" href=(nav.garden_item_href(&right)) { - code { (item_display_path(right.as_str())) } - } - } - div id="vote-edge-history-region" { - (edge_history) - } - @if can_post { - form id="vote-compare-form" method="POST" action="/ui" { - input type="hidden" name=(UI_RPC_FIELD) value=(rpc_json); - div class="vote-thread-picker" { - label class="vote-thread-picker-label" { "thread" } - select id="vote-thread-select" name="thread_tag" aria-label="Thread to post vote into" { - @if thread_tags.is_empty() { - option value="vote" selected { "#vote" } - } - @for t in &thread_tags { - @if *t == auto_thread { - option value=(t) selected { "#" (t) } - } @else { - option value=(t) { "#" (t) } - } - } - } + h2 { "compare" } + div class="vote-compare-pair" { + a class="vote-compare-item" href=(nav.garden_item_href(&left)) { + code { (item_display_path(left.as_str())) } + } + span class="vote-compare-vs" { "vs" } + a class="vote-compare-item" href=(nav.garden_item_href(&right)) { + code { (item_display_path(right.as_str())) } + } + } + div id="vote-edge-history-region" { + (edge_history) + } + @if can_post { + form id="vote-compare-form" method="POST" action="/ui" { + input type="hidden" name=(UI_RPC_FIELD) value=(rpc_json); + div class="vote-thread-picker" { + label class="vote-thread-picker-label" { "thread" } + select id="vote-thread-select" name="thread_tag" aria-label="Thread to post vote into" { + @if thread_tags.is_empty() { + option value="vote" selected { "#vote" } } - input type="hidden" name="ratio_left" id="vote-ratio-left" value="50"; - input type="hidden" name="ratio_right" id="vote-ratio-right" value="50"; - label class="vote-compare-slider-label" { - span id="vote-slider-left-label" { (item_display_path(left.as_str())) } - input type="range" id="vote-preference-slider" min="0" max="100" value="50" - aria-valuemin="0" aria-valuemax="100"; - span id="vote-slider-right-label" { (item_display_path(right.as_str())) } + @for t in &thread_tags { + @if *t == auto_thread { + option value=(t) selected { "#" (t) } + } @else { + option value=(t) { "#" (t) } + } } - label class="vote-explain-label" { "reason (required)" } - textarea name="explanation" id="vote-explain" rows="5" placeholder="why this split?" required {} - div id="vote-compare-errors" {} - p { button type="submit" { "post vote" } } } - } @else { - p class="muted" { a href="/login" { "log in" } " to post this vote." } } + input type="hidden" name="ratio_left" id="vote-ratio-left" value="50"; + input type="hidden" name="ratio_right" id="vote-ratio-right" value="50"; + label class="vote-compare-slider-label" { + span id="vote-slider-left-label" { (item_display_path(left.as_str())) } + input type="range" id="vote-preference-slider" min="0" max="100" value="50" + aria-valuemin="0" aria-valuemax="100"; + span id="vote-slider-right-label" { (item_display_path(right.as_str())) } + } + label class="vote-explain-label" { "reason (required)" } + textarea name="explanation" id="vote-explain" rows="5" placeholder="why this split?" required {} + div id="vote-compare-errors" {} + p { button type="submit" { "post vote" } } } + } @else { + p class="muted" { a href="/login" { "log in" } " to post this vote." } + } }; let page = layout_full_bleed_chromeless( diff --git a/server/static/theme_default.css b/server/static/theme_default.css index a441c4f79d8cf88f5a8240f9992f47dbbd1ab46b..fdcde86c718eb53cce8273e45a9844c2d8041f88 100644 --- a/server/static/theme_default.css +++ b/server/static/theme_default.css @@ -1270,8 +1270,12 @@ body.view-ontology-dark .ont-ranking-list li { body.view-ontology-dark .ont-ranking-list li::before { color: var(--meta); content: counter(ont-rank) "."; - font-size: 11px; - min-width: 18px; + flex-shrink: 0; + font-size: 1.35rem; + font-weight: 700; + font-variant-numeric: tabular-nums; + line-height: 1; + min-width: 2.25ch; text-align: right; } body.view-ontology-dark .ont-rank-score { @@ -1424,8 +1428,12 @@ body.view-ontology-light .ont-ranking-list li { body.view-ontology-light .ont-ranking-list li::before { color: var(--meta); content: counter(ont-rank) "."; - font-size: 11px; - min-width: 18px; + flex-shrink: 0; + font-size: 1.35rem; + font-weight: 700; + font-variant-numeric: tabular-nums; + line-height: 1; + min-width: 2.25ch; text-align: right; } body.view-ontology-light .ont-rank-score { diff --git a/server/static/theme_retro.css b/server/static/theme_retro.css index 373f15bbd75b62604cdd14b9f0fadda2d6176991..61e1448b2f66a075c0e33325d6980448712fc927 100644 --- a/server/static/theme_retro.css +++ b/server/static/theme_retro.css @@ -135,6 +135,30 @@ body.view-ontology nav.breadcrumb a:hover { body.view-ontology nav.breadcrumb a.bc-current { color: #111; font-weight: 600; } body.view-ontology nav.breadcrumb .bc-sep { color: #888; padding: 0 2px; } +body.view-ontology ol.ont-ranking-list { + counter-reset: ont-rank; + list-style: none; + margin: 0.5rem 0; + padding: 0; +} +body.view-ontology ol.ont-ranking-list li { + align-items: baseline; + counter-increment: ont-rank; + display: flex; + gap: 0.35rem; +} +body.view-ontology ol.ont-ranking-list li::before { + flex-shrink: 0; + color: #666; + content: counter(ont-rank) "."; + font-size: 1.35rem; + font-weight: 700; + font-variant-numeric: tabular-nums; + line-height: 1; + min-width: 2.25ch; + text-align: right; +} + nav.breadcrumb.ont-sibling-nav { margin-top: 0; width: 100%; diff --git a/server/static/theme_retro_craft.css b/server/static/theme_retro_craft.css index 6eb9222184a8795d67a5d09d41de08c8ac1b148f..7da102040484c887833158a37c307d078205c701 100644 --- a/server/static/theme_retro_craft.css +++ b/server/static/theme_retro_craft.css @@ -742,12 +742,32 @@ body.view-ontology button.ont-garden-pin-ico:focus-visible { outline-offset: 2px; } +body.view-ontology ol.ont-ranking-list { + counter-reset: ont-rank; + list-style: none; + margin: 0; + padding: 0; +} body.view-ontology ol.ont-ranking-list li, body.view-ontology ul.ont-group-list li { display: flex; align-items: baseline; gap: 0.35rem; } +body.view-ontology ol.ont-ranking-list li { + counter-increment: ont-rank; +} +body.view-ontology ol.ont-ranking-list li::before { + flex-shrink: 0; + color: #5c574e; + content: counter(ont-rank) "."; + font-size: 1.35rem; + font-weight: 700; + font-variant-numeric: tabular-nums; + line-height: 1; + min-width: 2.25ch; + text-align: right; +} body.view-ontology ol.ont-ranking-list li .item-link, body.view-ontology ul.ont-group-list li .item-link { flex: 1; Side B — contributor: tommy-mor Side B — commit message: [2e4be477] format Side B — unified diff (full patch): 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 { +fn edge_vote_entries_for_pair( + content: &ContentState, + a: &ItemId, + b: &ItemId, +) -> Vec { 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 = 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 = 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() } fn build_item_page_view_model( @@ -1301,23 +1325,28 @@ async fn vote_compare_inner( let edge_history = vote_edge_history_markup(content, &left, &right); drop(reduced); - let title = format!("vote — {} vs {}", item_display_path(left.as_str()), item_display_path(right.as_str())); - let next_path = uri.path_and_query().map(|pq| pq.as_str().to_string()).unwrap_or_else(|| "/vote/compare".into()); - - let rpc_json = template_json_compact( - &json!({ - "action": "vote_compare_post", - "room": nav.room_wire, - "thread_tag": {"$form": "thread_tag"}, - "left_item": left.as_str(), - "right_item": right.as_str(), - "ratio_left": {"$form": "ratio_left"}, - "ratio_right": {"$form": "ratio_right"}, - "explanation": {"$form": "explanation"}, - "next": next_path, - "form_action": "/ui", - }), - ) + let title = format!( + "vote — {} vs {}", + item_display_path(left.as_str()), + item_display_path(right.as_str()) + ); + let next_path = uri + .path_and_query() + .map(|pq| pq.as_str().to_string()) + .unwrap_or_else(|| "/vote/compare".into()); + + let rpc_json = template_json_compact(&json!({ + "action": "vote_compare_post", + "room": nav.room_wire, + "thread_tag": {"$form": "thread_tag"}, + "left_item": left.as_str(), + "right_item": right.as_str(), + "ratio_left": {"$form": "ratio_left"}, + "ratio_right": {"$form": "ratio_right"}, + "explanation": {"$form": "explanation"}, + "next": next_path, + "form_action": "/ui", + })) .expect("vote compare rpc json"); let body = html! { @@ -1532,11 +1561,23 @@ mod tests { .iter() .map(|r| r.item.as_str()) .collect(); - assert_eq!(names, vec!["https://slug.social/~/topic/a", "https://slug.social/~/topic/b"]); + assert_eq!( + names, + vec![ + "https://slug.social/~/topic/a", + "https://slug.social/~/topic/b" + ] + ); use crate::path_types::ItemId; assert!( - model.child_rankings.unranked_items.contains(&ItemId::parse("https://slug.social/~/topic/kid1").unwrap()) - || model.child_rankings.unranked_items.contains(&ItemId::parse("https://slug.social/~/topic/kid2").unwrap()) + model + .child_rankings + .unranked_items + .contains(&ItemId::parse("https://slug.social/~/topic/kid1").unwrap()) + || model + .child_rankings + .unranked_items + .contains(&ItemId::parse("https://slug.social/~/topic/kid2").unwrap()) ); } @@ -1609,11 +1650,8 @@ mod tests { 1, "@00000000-0000-0000-0000-000000000000:test:local/test\n~/x {x}\n", ); - let model = build_item_page_view_model( - &reduced, - &ScopeId::Public, - "https://slug.social/~/", - ); + let model = + build_item_page_view_model(&reduced, &ScopeId::Public, "https://slug.social/~/"); assert_eq!(model.child_rankings.unranked_items.len(), 1); assert_eq!( model.child_rankings.unranked_items[0].as_str(),