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: [0366806e] Color rank rows by vote mass within each group, not list position. Min–max normalization keeps similar scores visually close while still using the full gradient as groups grow and absolute mass dilutes. Co-authored-by: Cursor Side A — unified diff (full patch): diff --git a/server/src/html/mod.rs b/server/src/html/mod.rs index a58cbbee3490a08a625cb06df06848c59a615d65..4eff2e19ed4d303ff8e80c1eabd8a15b4990e643 100644 --- a/server/src/html/mod.rs +++ b/server/src/html/mod.rs @@ -264,12 +264,19 @@ pub fn scope_theme_style(parent: &ItemId) -> String { ) } -fn rank_row_style(parent: &ItemId, ordinal: usize, total: usize) -> String { - let t = if total <= 1 { - 0.0 - } else { - ordinal as f64 / (total - 1) as f64 - }; +/// Map vote mass to gradient position using the group's score range, not raw mass or +/// list position. Vote mass sums to 1 across the component, so absolute values dilute +/// as N grows; min–max within the visible list preserves similar scores → similar colors. +fn score_gradient_t(score: f64, min_score: f64, max_score: f64) -> f64 { + let spread = max_score - min_score; + if spread < 1e-9 { + return 0.5; + } + ((max_score - score) / spread).clamp(0.0, 1.0) +} + +fn rank_row_style(parent: &ItemId, score: f64, min_score: f64, max_score: f64) -> String { + let t = score_gradient_t(score, min_score, max_score); let base_hue = scope_base_hue(parent); let hue = (base_hue + 118.0 * t) % 360.0; let lightness = 0.74 - 0.34 * t; @@ -295,14 +302,15 @@ fn rank_list( highlighted: &HashSet, tree: &GlobalTree, ) -> Markup { - let group_len = items.len(); + let min_score = items.iter().map(|r| r.score).fold(f64::INFINITY, f64::min); + let max_score = items.iter().map(|r| r.score).fold(f64::NEG_INFINITY, f64::max); html! { @if !items.is_empty() { h3 class="rank-heading muted small" { (label) } ol class="rank-list" { @for (i, r) in items.iter().enumerate() { @let href = item_href(&r.item); - @let style = rank_row_style(parent, i, group_len); + @let style = rank_row_style(parent, r.score, min_score, max_score); @let class = rank_row_class(&r.item, highlighted); li class=(class) data-rank-item=(r.item.as_str()) @@ -517,19 +525,37 @@ pub async fn browse(State(state): State, uri: Uri) -> impl IntoRespons #[cfg(test)] mod tests { - use super::{rank_row_style, SORTER_UI_JS}; + use super::{rank_row_style, score_gradient_t, SORTER_UI_JS}; use crate::path_types::ItemId; #[test] - fn rank_row_style_gradients_per_group_not_globally() { + fn score_gradient_t_uses_group_range_not_absolute_mass() { + assert!((score_gradient_t(0.12, 0.08, 0.12) - 0.0).abs() < 1e-9); + assert!((score_gradient_t(0.08, 0.08, 0.12) - 1.0).abs() < 1e-9); + // Raw 12% mass would map near the dark end globally; within this group it's the top. + assert!(score_gradient_t(0.12, 0.08, 0.12) < score_gradient_t(0.12, 0.0, 1.0)); + } + + #[test] + fn score_gradient_t_similar_scores_similar_t() { + let a = score_gradient_t(0.41, 0.20, 0.60); + let b = score_gradient_t(0.40, 0.20, 0.60); + assert!((a - b).abs() < 0.05); + assert!((a - score_gradient_t(0.60, 0.20, 0.60)).abs() > 0.3); + } + + #[test] + fn score_gradient_t_tied_scores_neutral() { + assert!((score_gradient_t(0.25, 0.25, 0.25) - 0.5).abs() < 1e-9); + } + + #[test] + fn rank_row_style_same_inputs_same_color() { let parent = ItemId::opaque("test-scope"); - let first_in_four = rank_row_style(&parent, 0, 4); - let last_in_four = rank_row_style(&parent, 3, 4); - let first_in_two = rank_row_style(&parent, 0, 2); - let last_in_two = rank_row_style(&parent, 1, 2); - assert_eq!(first_in_four, first_in_two); - assert_eq!(last_in_four, last_in_two); - assert_ne!(first_in_four, last_in_four); + assert_eq!( + rank_row_style(&parent, 0.33, 0.20, 0.60), + rank_row_style(&parent, 0.33, 0.20, 0.60), + ); } #[test] Side B — contributor: tommy-mor Side B — commit message: [6b6eb0c3] Auth form: poem JS morphs form innerHTML on response; no redirect - post_choose_username returns HTML fragments instead of redirects: success → auth_signed_in_fragment ("you're signed in — return to your agent") error → choose_username_error_fragment (form re-rendered with error inline) - Poem JS now reads response body; if non-empty, morphs form innerHTML with it (existing ingest forms return empty body, so they're unaffected) - auth.rs: keep full layout() with poem JS — revert to single layout - auth-success CSS class added to both themes Co-Authored-By: Claude Sonnet 4.6 (1M context) Side B — unified diff (full patch): diff --git a/server/src/api/auth.rs b/server/src/api/auth.rs index c1194f79fd89fb47fe6b425b494a0ffa667abb2d..cb0faa29b834931e2c2b2f5c174c875e2e2e9346 100644 --- a/server/src/api/auth.rs +++ b/server/src/api/auth.rs @@ -16,7 +16,7 @@ use crate::{ canonicalize_username, validate_agent_format, validate_username, Event, TokenIssued, UserRegistered, }, - html::{auth_complete_page, choose_username_page}, + html::{auth_complete_page, auth_signed_in_fragment, choose_username_error_fragment, choose_username_page}, state::{AppState, PendingSession}, }; @@ -274,8 +274,6 @@ pub async fn post_choose_username( return api_error(StatusCode::BAD_REQUEST, "invalid agent format", Some(msg)).into_response(); } - let public_url = std::env::var("SLUG_PUBLIC_URL").unwrap_or_else(|_| "http://127.0.0.1:8080".to_string()); - let reduced_arc = state.reduced.clone(); let reduced = reduced_arc.read().await; let provider_key = (provider.to_lowercase(), provider_id.clone()); @@ -284,11 +282,7 @@ pub async fn post_choose_username( } if reduced.users_by_provider.values().any(|u| u == &canonicalize_username(&form.username)) { drop(reduced); - return Redirect::to(&format!( - "{public_url}/auth/choose-username?session={}&error={}", - urlencoding::encode(&form.session), - urlencoding::encode("that username is taken — try another"), - )).into_response(); + return choose_username_error_fragment(&form.session, "that username is taken — try another").into_response(); } drop(reduced); @@ -324,7 +318,7 @@ pub async fn post_choose_username( s.complete = Some((canon_user.clone(), bearer.clone())); } - Redirect::to(&format!("{public_url}/auth/complete")).into_response() + auth_signed_in_fragment().into_response() } pub async fn post_pending_session( diff --git a/server/src/html/auth.rs b/server/src/html/auth.rs index 40b1ef30a6c1d4063aa2d8e9c93df8972df4b27c..0a14bdbfb66c65d1bd09993ffd4a7bb6f2fe041e 100644 --- a/server/src/html/auth.rs +++ b/server/src/html/auth.rs @@ -1,20 +1,25 @@ -use maud::{html, Markup, DOCTYPE}; +use maud::{html, Markup}; -/// Minimal layout for auth pages — no JS interceptor, real form navigation works. -fn auth_layout(title: &str, body: Markup) -> Markup { +fn form_inner(session: &str, error: Option<&str>) -> Markup { html! { - (DOCTYPE) - html { - head { - meta charset="utf-8"; - meta name="viewport" content="width=device-width, initial-scale=1"; - title { (title) } - link rel="stylesheet" href="/static/theme_default.css"; - } - body class="view-auth" { - (body) - } + input type="hidden" name="session" value=(session); + label for="username" { "username" } + input + type="text" + id="username" + name="username" + placeholder="e.g. alice" + pattern="[a-z0-9_\\-]{1,32}" + maxlength="32" + autocomplete="off" + autofocus; + p.auth-hint { + "lowercase · alphanumeric · hyphens · underscores · max 32" } + @if let Some(msg) = error { + p.auth-error { (msg) } + } + button type="submit" { "continue" } } } @@ -28,27 +33,23 @@ pub fn choose_username_page(session: &str, error: Option<&str>) -> Markup { h1 { "choose a username" } p { "pick a handle for slug.social." } form.auth-form method="POST" action="/auth/choose-username" { - input type="hidden" name="session" value=(session); - label for="username" { "username" } - input - type="text" - id="username" - name="username" - placeholder="e.g. alice" - pattern="[a-z0-9_\\-]{1,32}" - maxlength="32" - autocomplete="off" - autofocus; - p.auth-hint { - "lowercase · alphanumeric · hyphens · underscores · max 32" - } - @if let Some(msg) = error { - p.auth-error { (msg) } - } - button type="submit" { "continue" } + (form_inner(session, error)) } }; - auth_layout("join — slug.social", body) + super::layout("join — slug.social", "view-auth", body, None) +} + +/// Fragment returned to the poem JS on error — replaces the form's innerHTML. +pub fn choose_username_error_fragment(session: &str, error: &str) -> Markup { + form_inner(session, Some(error)) +} + +/// Fragment returned to the poem JS on success — replaces the form's innerHTML. +pub fn auth_signed_in_fragment() -> Markup { + html! { + p.auth-success { "you're signed in — return to your agent." } + p.auth-hint { "you can close this tab." } + } } pub fn auth_complete_page() -> Markup { @@ -62,5 +63,5 @@ pub fn auth_complete_page() -> Markup { p { "Return to your terminal — your agent is polling and will collect your token automatically." } p.auth-hint { "You can close this tab." } }; - auth_layout("signed in — slug.social", body) + super::layout("signed in — slug.social", "view-auth", body, None) } diff --git a/server/src/html/mod.rs b/server/src/html/mod.rs index 2f16d701962d703db0c859bb586dfc08ec385690..8b48a25cce79f0eefc7e29849e1a667cae4c7986 100644 --- a/server/src/html/mod.rs +++ b/server/src/html/mod.rs @@ -15,7 +15,7 @@ mod search; mod tree; use breadcrumb_path::OntologyPath; -pub use auth::{auth_complete_page, choose_username_page}; +pub use auth::{auth_complete_page, auth_signed_in_fragment, choose_username_error_fragment, choose_username_page}; pub use editor::{editor_check, editor_page}; pub use forum::{index, thread_feed_html, thread_post_expand, thread_post_view, thread_view}; pub use garden::{garden_index, ontology_path}; @@ -114,6 +114,8 @@ script { (maud::PreEscaped(r#" }); // Poem: intercept POST forms, send via fetch, await SSE for DOM update. + // If the response body is non-empty HTML, morph the form's innerHTML with it + // (used for inline feedback without a page reload, e.g. auth forms). document.addEventListener('submit', async (e) => { const f = e.target; if (!f || f.tagName !== 'FORM') return; @@ -121,14 +123,19 @@ script { (maud::PreEscaped(r#" e.preventDefault(); const btn = f.querySelector('button[type="submit"], input[type="submit"]'); if (btn) { btn.disabled = true; btn.textContent = '…'; } - await fetch(f.action, { + const resp = await fetch(f.action, { method: 'POST', body: new URLSearchParams(new FormData(f)), headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, credentials: 'same-origin', }); - if (btn) { btn.disabled = false; btn.textContent = 'submit'; } - f.reset(); + const html = await resp.text(); + if (html && html.trim()) { + Idiomorph.morph(f, html, {morphStyle: 'innerHTML'}); + } else { + if (btn) { btn.disabled = false; btn.textContent = 'submit'; } + f.reset(); + } }); // Search: debounced fetch + idiomorph. diff --git a/server/static/theme_default.css b/server/static/theme_default.css index 9e71574da4bed3a0116c347610636780678bd1af..a1d8d1765a7812191edc579125facf1694554c2c 100644 --- a/server/static/theme_default.css +++ b/server/static/theme_default.css @@ -250,6 +250,11 @@ p.auth-error { font-size: 12px; margin: 4px 0 0; } +p.auth-success { + color: var(--signal); + font-size: 13px; + margin: 4px 0 0; +} /* ---------------------------------------------------------------- BUTTONS — raised, press on :active diff --git a/server/static/theme_retro.css b/server/static/theme_retro.css index dc9fa4654f529eb1843557fb580cf3982da46901..8ed8fd88efab32b36bd66cf200aa182219b0a8b5 100644 --- a/server/static/theme_retro.css +++ b/server/static/theme_retro.css @@ -32,6 +32,7 @@ input[type="text"] { input[type="text"]:focus { border-color: #00ff41; } p.auth-hint { color: #555; font-family: monospace; font-size: 0.75rem; margin: 0; } p.auth-error { color: #ff4444; font-family: monospace; font-size: 0.8rem; margin: 0; } +p.auth-success { color: #00ff41; font-family: monospace; font-size: 0.8rem; margin: 0; } /* Ingest form (poem pattern) */ .ingest-form-wrap { margin-top: 1.5rem; }