constitution · epochs · watch · epoch 3

comparison

c_2e0ba3ceee22 (tommy-mor) vs c_f7f1b6183703 (tommy-mor)

download prompt · raw event · cmp_06e9b4c91ebb64

council reasoning

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

Side B implements a substantial, coherent feature (self vs children fetch, SSE-as-JS-eval streaming, subreddit listing import, ranking panel refactor into connected components/unranked lists) with corresponding test fixture and integration test updates. Side A is a trivial cosmetic tweak (breadcrumb text, CSS color/spacing, removed a heading) with no functional value beyond styling.

~x-ai/grok-latest · winner B · 1:25 · permalink

Side A only tweaks presentation (breadcrumb label, remove ranking h2, accent color/font-size). Side B adds lasting product capability: separate self vs children Reddit fetch, listing import/link_child, multi-component ranking UI, unified SSE-as-JS morphs, OAuth base split, and test fixtures—real design and features versus cosmetic churn.

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

Side B introduces substantial functionality and architecture changes: it adds separate fetch modes for entities versus children, extends the Reddit import pipeline with FetchKind, child-list parsing and linking, updates SSE handling to stream executable UI morphs, and redesigns ranking to handle connected components and unranked items. Side A only makes presentation tweaks, such as changing the breadcrumb root label to "~", removing a ranking heading, and adjusting CSS typography and colors.

sides

A — c_2e0ba3ceee22 (tommy-mor)

message

[8fce2727] looks better

diff preview

diff --git a/server/src/html/mod.rs b/server/src/html/mod.rs
index 0acba3ac745f23be473243e0a9b9cc6e57e8d86f..2864407ed6e8ec284a1dc663acf1805534566b62 100644
--- a/server/src/html/mod.rs
+++ b/server/src/html/mod.rs
@@ -138,7 +138,7 @@ fn segment_label(seg: &str) -> &str {
 pub fn breadcrumb_path(item: &ItemId) -> Markup {
     html! {
         nav class="breadcrumbs" aria-label="Breadcrumb" {
-            a href="/" { "Internet" }
+            a href="/" { "~" }
             @for path in item.breadcrumb_paths() {
                 @let seg = path.segments().last().map_or("", |v| *v);
                 span class="separator" { " / " }
@@ -198,12 +198,6 @@ pub fn ranking_panel(item: &ItemId, group: &GroupState) -> Markup {
     let (top, bottom) = top_bottom(group, 8);
     html! {
         section id="ranking-panel" class="demo-panel" {
-            h2 {
-                "Ranking"
-                @if !item.is_root() {
-                    " — " span class="scope-name" { (item.as_str()) }
-                }
-            }
             @if total == 0 {
                 p class="muted" {
                     @if item.is_root() {
diff --git a/server/static/sorter.css b/server/static/sorter.css
index a648e88a57290346b1069868134a212b180def52..36f68d415cea33c8562d5d02c0c9d4c5b225a782 100644
--- a/server/static/sorter.css
+++ b/server/static/sorter.css
@@ -104,7 +104,7 @@ code {
   padding: 0.5rem;
   border: 1px solid var(--border);
   background: var(--bg);
-  color: var(--fg);
+  color: var(--accent);
   font-size: 1rem;
   font-family: inherit;
   resize: vertical;
@@ -120,7 +120,8 @@ code {
 }
 
 .breadcrumbs {
-  font-size: 0.875rem;
+  font-size: 1rem;
+  margin-top: 1rem;
   margin-bottom: 1rem;
   color: var(--muted);
 }

download full diff A

B — c_f7f1b6183703 (tommy-mor)

message

[902a4c5c] refactor

diff preview

diff --git a/server/src/api/ui_html.rs b/server/src/api/ui_html.rs
index 7af6527d03c483f33f3469ce6766c01a554c5fe3..d1defd28242fd2ca3b886adc91a7070bef75e653 100644
--- a/server/src/api/ui_html.rs
+++ b/server/src/api/ui_html.rs
@@ -56,12 +56,9 @@ pub async fn post_ui_html(
                 return ui_js_warn(&e).into_response();
             }
             let tree = state.tree.read().await;
-            let empty = crate::reducer::GroupState::new();
-            let group = tree
-                .get(&parent)
-                .map(|n| &n.local_ranking)
-                .unwrap_or(&empty);
-            let panel = ranking_panel(&parent, group);
+            let empty = crate::reducer::NodeState::default();
+            let node = tree.get(&parent).unwrap_or(&empty);
+            let panel = ranking_panel(&parent, node);
             JsBuilder::new()
                 .morph_selector("#ranking-panel", panel)
                 .into_response()
@@ -88,9 +85,9 @@ pub async fn post_ui_html(
                     .into_response()
             }
         },
-        HtmlUiAction::FetchEntity { item } => {
+        HtmlUiAction::FetchEntity { item, kind } => {
             let id = parse_item_param(&item);
-            fetch::fetch_entity_stream(state, id).into_response()
+            fetch::fetch_entity_stream(state, id, kind).into_response()
         }
     }
 }
diff --git a/server/src/fetch/html.rs b/server/src/fetch/html.rs
index 63634508496e224c38b9ec0308b7a6086462f925..9b6c2d0964640a157bca2a6a64cb0820238d8e4f 100644
--- a/server/src/fetch/html.rs
+++ b/server/src/fetch/html.rs
@@ -5,7 +5,7 @@ use maud::{html, Markup};
 use crate::{
     form_template::template_json_compact,
     path_types::ItemId,
-    reddit::is_fetchable,
+    reddit::{is_children_fetchable, is_fetchable},
     reducer::NodeState,
     ui_action::UI_RPC_FIELD,
 };
@@ -26,25 +26,16 @@ fn entity_panel(node: &NodeState) -> Markup {
     }
 }
 
-/// Reddit/API import — `POST /ui` with `fetch_entity` returns an SSE stream.
-pub fn fetch_entity_panel(item: &ItemId, has_data: bool, fetching: bool) -> Markup {
-    if !is_fetchable(item) {
-        return html! {};
-    }
-    let label = if fetching {
-        "Fetching…"
-    } else if has_data {
-        "Fetch more"
-    } else {
-        "Fetch from Reddit"
-    };
+/// One `fetch_entity` form/button targeting `kind` ("self" or "children").
+fn fetch_button(item: &ItemId, kind: &str, label: &str, fetching: bool) -> Markup {
     let rpc = template_json_compact(&serde_json::json!({
         "action": "fetch_entity",
         "item": item.as_str(),
+        "kind": kind,
     }))
     .expect("fetch_entity rpc template");
     html! {
-        form method="post" action="/ui" id="fetch-entity-form" class="fetch-entity-form" {
+        form method="post" action="/ui" class="fetch-entity-form" {
             input type="hidden" name=(UI_RPC_FIELD) value=(rpc);
             @if fetching {
                 button type="submit" class="btn-secondary" disabled { (label) }
@@ -55,6 +46,33 @@ pub fn fetch_entity_panel(item: &ItemId, has_data: bool, fetching: bool) -> Mark
     }
 }
 
+/// Reddit/API import controls — `POST /ui` with `fetch_entity` returns an SSE
+/// stream whose events are JS snippets to `eval`.
+pub fn fetch_entity_panel(item: &ItemId, has_data: bool, fetching: bool) -> Markup {
+    let self_ok = is_fetchable(item);
+    let children_ok = is_children_fetchable(item);
+    if !self_ok && !children_ok {
+        return html! {};
+    }
+    let self_label = if fetching {
+        "Fetching…"
+    } else if has_data {
+        "Refresh this"
+    } else {
+        "Fetch from Reddit"
+    };
+    html! {
+        div id="fetch-controls" class="fetch-controls" {
+            @if self_ok {
+                (fetch_button(item, "self", self_label, fetching))
+            }
+            @if children_ok {
+                (fetch_button(item, "children", if fetching { "Fetching…" } else { "Fetch posts" }, fetching))
+            }
+        }
+    }
+}
+
 /// Entity card + fetch control (target `#entity-section` for Idiomorph / SSE).
 pub fn entity_section(item: &ItemId, node: &NodeState, fetching: bool) -> Markup {
     let has_data = node.data.is_some();
diff --git a/server/src/fetch/mod.rs b/server/src/fetch/mod.rs
index 2290f9d3a0f1cbf1806c6339f82a4515c11cc3d3..0177bb161cea1b72a100b52efdfc5e710271c9eb 100644
--- a/server/src/fetch/mod.rs
+++ b/server/src/fetch/mod.rs
@@ -1,4 +1,8 @@
 //! Entity import over `POST /ui` as SSE (Reddit worker in [`crate::reddit`]).
+//!
+//! Each SSE event's `data` is a JS snippet that the browser `eval`s — the same
+//! Idiomorph-morph snippets the non-streaming `/ui` responses use. There is no
+//! bespoke JSON envelope; the client just evals whatever each event carries.
 
 pub mod html;
 
@@ -8,14 +12,15 @@ use std::time::Duration;
 use async_stream::stream;
 use axum::response::sse::{Event, KeepAlive, Sse};
 use futures_util::Stream;
-use serde::Serialize;
 use tokio::sync::oneshot;
 
 use crate::{
+    html::{ranking_panel, JsBuilder},
     path_types::ItemId,
-    reddit::FetchJobResult,
+    reddit::{FetchJobResult, FetchKind},
     reducer::NodeState,
     state::AppState,
+    ui_action::FetchTarget,
 };
 
 pub fn now_ms() -> i64 {
@@ -25,55 +30,62 @@ pub fn now_ms() -> i64 {
     t.as_millis() as i64
 }
 
-#[derive(Serialize)]
-struct SseMorphPayload {
-    selector: &'static str,
-    html: String,
+fn js_event(js: String) -> Event {
+    Event::default().data(js)
 }
 
-fn morph_complete_event(html: maud::Markup) -> Event {
-    let payload = SseMorphPayload {
-        selector: "#entity-section",
-        html: html.into_string(),
-    };
-    let data = serde_json::to_string(&payload).unwrap_or_else(|_| "{}".into());
-    Event::default().event("complete").data(data)
+/// JS that surfaces a transient message in the page's `#errors` region.
+fn error_js(message: &str) -> String {
+    JsBuilder::new()
+        .morph_selector(
+            "#errors",
+            maud::html! { div id="errors" { p class="muted" { (message) } } },
+        )
+        .build()
 }
 
-/// Stream `fetching` → `complete` / `error` for [`crate::ui_action::HtmlUiAction::FetchEntity`].
+/// Stream Idiomorph-morph JS snippets for [`crate::ui_action::HtmlUiAction::FetchEntity`].
 pub fn fetch_entity_stream(
     state: AppState,
     id: ItemId,
+    target: FetchTarget,
 ) -> Sse<impl Stream<Item = Result<Event, Infallible>>> {
-    tracing::debug!(item = %id, "fetch entity stream opened");
+    let kind = match target {
+        FetchTarget::SelfEntity => FetchKind::SelfEntity,
+        FetchTarget::Children => FetchKind::Children,
+    };
+    tracing::debug!(item = %id, ?kind, "fetch entity stream opened");
 
     let stream = stream! {
         if id.is_root() {
-            yield Ok(Event::default().event("error").data("{\"message\":\"nothing to fetch for the root\"}"));
+            yield Ok(js_event(error_js("Nothing to fetch for the root.")));
             return;
         }
 
-        if !crate::reddit::is_fetchable(&id) {
-            tracing::debug!(item = %id, "fetch stream: not fetchable");
-            yield Ok(Event::default().event("error").data("{\"message\":\"this page cannot be fetched from Reddit\"}"));
+        let fetchable = match kind {
+            FetchKind::SelfEntity => crate::reddit::is_fetchable(&id),
+            FetchKind::Children => crate::reddit::is_children_fetchable(&id),
+        };
+        if !fetchable {
+            tracing::debug!(item = %id, ?kind, "fetch stream: not fetchable");
+            yield Ok(js_event(error_js("This page cannot be fetched from Reddit.")));
             return;
         }
 
-        let fetching_html = {
+        // Optimistic "Fetching…" morph of the entity section.
+        let fetching_js = {
             let tree = state.tree.read().await;
             let empty = NodeState::default();
             let node = tree.get(&id).unwrap_or(&empty);
-            html::entity_section(&id, node, true).into_string()
+            JsBuilder::new()
+                .morph_selector("#entity-section", html::entity_section(&id, node, true))
+                .build()
         };
-        let fetching_payload = serde_json::json!({
-            "selector": "#entity-section",
-            "html": fetching_html,
-        });
-        yield Ok(Event::default().event("fetching").data(fetching_payload.to_string()));
+        yield Ok(js_event(fetching_js));
 
         let (tx, rx) = oneshot::channel();
-        state.reddit.request_fetch(id.clone(), true, Some(tx));
-        tracing::debug!(item = %id, "fetch stream: queued reddit job");
+        state.queue_entity_fetch(id.clone(), kind, Some(tx));
+        tracing::debug!(item = %id, ?kind, "fetch stream: queued reddit job");
 
         let result = match rx.await {
             Ok(r) => r,
@@ -82,31 +94,42 @@ pub fn fetch_entity_stream(
                 FetchJobResult::Failed("reddit worker stopped".into())
             }
         };
-
         tracing::debug!(item = %id, ?result, "fetch stream: job finished");
 
         match result {
-            FetchJobResult::Imported | FetchJobResult::NotFound => {
+            FetchJobResult::Imported(_)
+            | FetchJobResult::NotFound
+            | FetchJobResult::SkippedCached
+            | FetchJobResult::SkippedDuplicate => {
                 let tree = state.tree.read().await;
                 let empty = NodeState::default();
                 let node = tree.get(&id).unwrap_or(&empty);
-                yield Ok(morph_complete_event(html::entity_section(&id, node, false)));
+                let mut b = JsBuilder::new()
+                    .morph_selector("#entity-section", html::entity_section(&id, node, false));
+                if kind == FetchKind::Children {
+                    b = b.morph_selector("#ranking-panel", ranking_panel(&id, node));
+                }
+                yield Ok(js_event(b.build()));
             }
-            FetchJobResult::SkippedCached | FetchJobResult::SkippedDuplicate => {
+            FetchJobResult::RateLimited { reset_secs } => {
                 let tree = state.tree.read().await;
                 let empty = NodeState::default();
                 let node = tree.get(&id).unwrap_or(&empty);
-                yield Ok(morph_complete_event(html::entity_section(&id, node, false)));
-            }
-            FetchJobResult::RateLimited { reset_secs } => {
-                yield Ok(Event::default().event("error").data(
-                    serde_json::json!({"message": format!("Reddit rate limit — retry in {reset_secs}s")}).to_string(),
-                ));
+                let js = JsBuilder::new()
+                    .morph_selector("#entity-section", html::entity_section(&id, node, false))
+                    .raw(&error_js(&format!("Reddit rate limit — retry in {reset_secs}s.")))
+                    .build();
+                yield Ok(js_event(js));
             }
             FetchJobResult::Failed(msg) => {
-                yield Ok(Event::default().event("error").data(
-                    serde_json::json!({"message": msg}).to_string(),
-                ));
+                let tree = state.tree.read().await;
+                let empty = NodeState::default();
+                let node = tree.get(&id).unwrap_or(&empty);
+                let js = JsBuilder::new()
+                    .morph_selector("#entity-section", html::entity_section(&id, node, false))
+                    .raw(&error_js(&format!("Fetch failed: {msg}")))
+                    .build();
+                yield Ok(js_event(js));
             }
         }
     };
diff --git a/server/src/html/mod.rs b/server/src/html/mod.rs
index 9314a7556306ddab969b896dbf4126b542a46722..27ce9118c73ec5643e04363ab1a36cf5da6101bf 100644
--- a/server/src/html/mod.rs
+++ b/server/src/html/mod.rs
@@ -6,12 +6,16 @@ use axum::{
 };
 use maud::{html, Markup, DOCTYPE};
 
+use std::collections::HashSet;


… preview truncated; 29,086 characters omitted

download full diff B

Hardlinks — judgments / attempts / prompt

prompt download

judgments

attempts

Prompt text is loaded only by the download route.