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: [993d359c] Fix Reddit children import wiring and unranked child labels. Listing imports attach posts directly under the subreddit without ensure_path pulling comment-path segments in, and the ranking panel shows imported titles. Update integration tests for JS SSE morphs and children fetch. Co-authored-by: Cursor Side A — unified diff (full patch): diff --git a/server/src/api/ui_html.rs b/server/src/api/ui_html.rs index d1defd28242fd2ca3b886adc91a7070bef75e653..e649a7d192feade465e19ce6187a829f6ec74372 100644 --- a/server/src/api/ui_html.rs +++ b/server/src/api/ui_html.rs @@ -58,7 +58,7 @@ pub async fn post_ui_html( let tree = state.tree.read().await; let empty = crate::reducer::NodeState::default(); let node = tree.get(&parent).unwrap_or(&empty); - let panel = ranking_panel(&parent, node); + let panel = ranking_panel(&parent, node, &tree); JsBuilder::new() .morph_selector("#ranking-panel", panel) .into_response() diff --git a/server/src/fetch/mod.rs b/server/src/fetch/mod.rs index 0177bb161cea1b72a100b52efdfc5e710271c9eb..35f968c21c69ca557bab7951413e3cfbbccfebfd 100644 --- a/server/src/fetch/mod.rs +++ b/server/src/fetch/mod.rs @@ -107,7 +107,7 @@ pub fn fetch_entity_stream( 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)); + b = b.morph_selector("#ranking-panel", ranking_panel(&id, node, &tree)); } yield Ok(js_event(b.build())); } diff --git a/server/src/html/mod.rs b/server/src/html/mod.rs index 27ce9118c73ec5643e04363ab1a36cf5da6101bf..e88cc43ddc9d8100f7994be6f5960ec4d8f22c55 100644 --- a/server/src/html/mod.rs +++ b/server/src/html/mod.rs @@ -15,7 +15,7 @@ use crate::{ ranking::{ connected_components_from_voted_pairs, ranked_items_subset, RankedItem, MAX_ITERS, TOL, }, - reducer::NodeState, + reducer::{GlobalTree, NodeState}, state::AppState, ui_action::UI_RPC_FIELD, }; @@ -182,8 +182,15 @@ fn display_label(id: &ItemId) -> String { .to_string() } +fn child_label(tree: &GlobalTree, id: &ItemId) -> String { + tree.get(id) + .and_then(|n| n.data.as_ref()) + .map(|d| d.title.clone()) + .unwrap_or_else(|| display_label(id)) +} + /// Plain (unscored) list of children that have no votes yet. -fn unranked_list(label: &str, items: &[ItemId]) -> Markup { +fn unranked_list(label: &str, items: &[ItemId], tree: &GlobalTree) -> Markup { html! { @if !items.is_empty() { h3 class="rank-heading muted small" { (label) } @@ -191,7 +198,7 @@ fn unranked_list(label: &str, items: &[ItemId]) -> Markup { @for it in items { li { a href=(item_href(it)) { - strong { (display_label(it)) } + strong { (child_label(tree, it)) } } } } @@ -200,7 +207,7 @@ fn unranked_list(label: &str, items: &[ItemId]) -> Markup { } } -pub fn ranking_panel(item: &ItemId, node: &NodeState) -> Markup { +pub fn ranking_panel(item: &ItemId, node: &NodeState, tree: &GlobalTree) -> Markup { let group = &node.local_ranking; let n = group.idx_to_item.len(); let (comps, _isolates) = @@ -248,7 +255,7 @@ pub fn ranking_panel(item: &ItemId, node: &NodeState) -> Markup { @let label = if multi { format!("Ranking group {}", gi + 1) } else { "Ranking".to_string() }; (rank_list(&label, ranked, 1)) } - (unranked_list("Unranked", &unranked)) + (unranked_list("Unranked", &unranked, tree)) } } } @@ -297,7 +304,7 @@ async fn item_page(state: AppState, uri: Uri, item: ItemId) -> Markup { (input_panel("", None)) (breadcrumb_path(&item)) (entity_section(&item, node, false)) - (ranking_panel(&item, node)) + (ranking_panel(&item, node, &tree)) }; layout("sorter2", body, views) } diff --git a/server/src/reddit.rs b/server/src/reddit.rs index a0eb688709478ee0185b953b41a5d26cc354764d..69d979bc7e4a1cb078f70114dc539bdc1986b574 100644 --- a/server/src/reddit.rs +++ b/server/src/reddit.rs @@ -300,9 +300,16 @@ async fn reddit_worker( } { let mut tree = tree.write().await; - apply_entity_import(&mut tree, &child_id, child_payload); if kind == FetchKind::Children { - tree.link_child(&fetch_id, &child_id); + let view = entity_view_from_payload(&child_id, &child_payload); + tree.apply_entity_under_parent( + &fetch_id, + &child_id, + child_payload, + view, + ); + } else { + apply_entity_import(&mut tree, &child_id, child_payload); } } written += 1; diff --git a/server/src/reducer.rs b/server/src/reducer.rs index a36cd5c9287d61536f9f4a3f6b0df2342388857a..4e42d0369dab50bb2f8ca664aa69b628292f6c07 100644 --- a/server/src/reducer.rs +++ b/server/src/reducer.rs @@ -209,14 +209,24 @@ impl GlobalTree { } } - /// Directly attach `child` under `parent`, bypassing path-based nesting. - /// Used for imported listings (e.g. a subreddit's posts) so they show up - /// as children of the subreddit rather than a deep `…/comments/` path. - pub fn link_child(&mut self, parent: &ItemId, child: &ItemId) { + /// Import entity data for `id` and attach it as a direct child of `parent` + /// without running [`Self::ensure_path`] on `id` (avoids Reddit `/comments/` + /// parent rules pulling intermediate path segments into the subreddit). + pub fn apply_entity_under_parent( + &mut self, + parent: &ItemId, + id: &ItemId, + payload: Value, + view: Option, + ) { self.ensure_path(parent); - self.ensure_path(child); + self.ensure_node(id); + if let Some(node) = self.nodes.get_mut(id) { + node.entity_raw = Some(payload); + node.data = view; + } if let Some(p) = self.nodes.get_mut(parent) { - p.children.insert(child.clone()); + p.children.insert(id.clone()); } } } diff --git a/test/reddit_import.clj b/test/reddit_import.clj index 84cbdcf7965e50290313cbce2243a16b583d2097..45a2a19f20799d77e84d8aa64735ab5e7e45f97c 100644 --- a/test/reddit_import.clj +++ b/test/reddit_import.clj @@ -67,6 +67,36 @@ (do (Thread/sleep 200) (recur)) false))))) +(defn- run-reddit-fetch-assertions [app-base data-dir] + (let [browse-url (str app-base "/~/https://reddit.com/r/rust") + log-path (str data-dir "/events.jsonl") + before (:out (process/shell {:out :string :err :string} + "curl" "-sf" browse-url))] + (is (str/includes? before "Fetch from Reddit")) + (is (not (str/includes? before "The Rust Programming Language"))) + (let [sse (curl-fetch-ui-sse app-base "reddit.com/r/rust" "self")] + (is (zero? (:exit sse)) "POST /ui fetch_entity (self) SSE succeeds") + (is (str/includes? (:out sse) "Idiomorph.morph")) + (is (str/includes? (:out sse) "The Rust Programming Language")) + (is (wait-event-log log-path 2000) "event log written")) + (let [after (:out (process/shell {:out :string :err :string} + "curl" "-sf" browse-url)) + log (slurp (io/file log-path))] + (is (str/includes? after "The Rust Programming Language")) + (is (str/includes? log "\"type\":\"entity_imported\"")) + (is (str/includes? log "\"subscribers\":350000")) + (is (str/includes? log "\"display_name\":\"rust\""))) + (let [children-sse (curl-fetch-ui-sse app-base "reddit.com/r/rust" "children")] + (is (zero? (:exit children-sse)) "POST /ui fetch_entity (children) SSE succeeds") + (is (str/includes? (:out children-sse) "Idiomorph.morph")) + (is (str/includes? (:out children-sse) "Announcing Rust 1.99"))) + (let [after-children (:out (process/shell {:out :string :err :string} + "curl" "-sf" browse-url)) + log2 (slurp (io/file log-path))] + (is (str/includes? after-children "Announcing Rust 1.99")) + (is (str/includes? after-children "Unranked")) + (is (str/includes? log2 "announcing_rust_199"))))) + (deftest reddit-fetch-via-mock-api (testing "Fetch more queues import; event log stores full payload; page shows title" (let [root (repo-root) @@ -102,24 +132,7 @@ bin)] (try (is (wait-health app-base 20000) "app healthz") - (let [browse-url (str app-base "/~/https://reddit.com/r/rust") - before (:out (process/shell {:out :string :err :string} - "curl" "-sf" browse-url))] - (is (str/includes? before "Fetch from Reddit")) - (is (not (str/includes? before "The Rust Programming Language"))) - (let [log-path (str data-dir "/events.jsonl") - sse (curl-fetch-ui-sse app-base "reddit.com/r/rust")] - (is (zero? (:exit sse)) "POST /ui fetch_entity SSE succeeds") - (is (str/includes? (:out sse) "event: complete")) - (is (str/includes? (:out sse) "The Rust Programming Language")) - (is (wait-event-log log-path 2000) "event log written") - (let [after (:out (process/shell {:out :string :err :string} - "curl" "-sf" browse-url)) - log (slurp (io/file log-path))] - (is (str/includes? after "The Rust Programming Language")) - (is (str/includes? log "\"type\":\"entity_imported\"")) - (is (str/includes? log "\"subscribers\":350000")) - (is (str/includes? log "\"display_name\":\"rust\""))))) + (run-reddit-fetch-assertions app-base data-dir) (finally (process/destroy proc)))) (finally diff --git a/test/smoke.clj b/test/smoke.clj index 11887c48282088e140d823a88ba616f6325835b3..ce9f958c84b9a89ae55e215ab519f1df6435e24b 100644 --- a/test/smoke.clj +++ b/test/smoke.clj @@ -49,7 +49,7 @@ (is (wait-health base 15000) "server responds to /healthz") (let [home (:out (process/shell {:out :string :err :string} "curl" "-sf" (str base "/")))] - (is (str/includes? home "vote-panel")) + (is (str/includes? home "entity-section")) (is (str/includes? home "ranking-panel")) (is (str/includes? home "parser-panel")) (is (str/includes? home "__rpc__"))) Side B — contributor: tommy-mor Side B — commit message: [81de487b] Fix zero-ratio guard in reducer to drop before registering items or pair. Previously the early-return for zero-weight votes happened after ensure_item and voted_pairs.insert, leaving ghost items in the index and the pair incorrectly marked as voted. Move the check to before any side effects. Co-Authored-By: Claude Sonnet 4.6 Side B — unified diff (full patch): diff --git a/server/src/reducer.rs b/server/src/reducer.rs index 6841d35cfc9de2389f340a22b8a45acb335e36c3..0e36979abe0f051493038ff7e652efc7f7a0ac80 100644 --- a/server/src/reducer.rs +++ b/server/src/reducer.rs @@ -112,6 +112,10 @@ impl GroupState { if vote.ratio_right < 0 { vote.ratio_right = 0; } + if vote.ratio_left == 0 || vote.ratio_right == 0 { + // Zero on either side produces no valid edge; drop before registering items or pair. + return; + } let a_idx = self.ensure_item(&vote.a); let b_idx = self.ensure_item(&vote.b); @@ -121,10 +125,6 @@ impl GroupState { let w_a = vote.ratio_left as f64; let w_b = vote.ratio_right as f64; - if w_a == 0.0 || w_b == 0.0 { - // Zero on either side produces no valid edge; drop the vote. - return; - } self.add_edge_weight(b_idx, a_idx, w_a); self.add_edge_weight(a_idx, b_idx, w_b); diff --git a/server/tests/basic.rs b/server/tests/basic.rs index cc8c1a0d139f3722ba6ecd13dd001c65be835b67..08159f4a7f0850fd165817a4a1af4f31ced2ad76 100644 --- a/server/tests/basic.rs +++ b/server/tests/basic.rs @@ -546,12 +546,10 @@ fn reducer_negative_ratio_clamped_to_zero() { delegate: Some("00000000-0000-0000-0000-000000000000:test:local/test".to_string()), thread_tag: "t".to_string(), }); - // Items are registered, but the zero-clamped vote produces no edges. - assert_eq!(group.idx_to_item.len(), 2); - let a_idx = group.item_to_idx[&item_id("https://slug.social/~/t/a")]; - let b_idx = group.item_to_idx[&item_id("https://slug.social/~/t/b")]; - assert!(!group.edges.contains_key(&(a_idx, b_idx))); - assert!(!group.edges.contains_key(&(b_idx, a_idx))); + // Nothing registered: zero-clamped vote is dropped before ensure_item. + assert!(group.idx_to_item.is_empty()); + assert!(group.edges.is_empty()); + assert!(group.voted_pairs.is_empty()); }