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: [075d4d37] Fix OAuth test mocks so Clojure E2E auth flows work again. HttpServer handlers were crashing on query parsing and token POSTs, which broke Playwright login; also read alias/history via real CSS selectors. Co-authored-by: Cursor Side A — unified diff (full patch): diff --git a/test/auth_login.clj b/test/auth_login.clj index 6f2f00d7aa7340e63d1ac465b0a2234cec7a983f..aa63b66ccb2471b710ba3d168d0461252b39fc10 100644 --- a/test/auth_login.clj +++ b/test/auth_login.clj @@ -14,27 +14,27 @@ (defn- type-alias! [pg text] (page/evaluate pg (.replace - "(() => { const i = document.getElementById('alias-input'); const f = document.getElementById('alias-check-form'); if (!i || !f) return; + "(() => { const i = document.getElementById('alias-input'); const f = document.getElementById('alias-check-form'); if (!i || !f) return Promise.resolve('missing-form'); i.value = __TEXT__; const cf = document.getElementById('alias-claim-field'); if (cf) cf.value = i.value; return fetch(f.action, { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams(new FormData(f)).toString() }) .then(function (r) { return r.text(); }) - .then(function (t) { eval(t); }); })()" + .then(function (t) { eval(t); return document.getElementById('alias-status')?.textContent || ''; }); })()" "__TEXT__" - (pr-str text))) - (Thread/sleep 400)) + (pr-str text)))) -(defn- element-text [pg test-id] +(defn- element-text [pg selector] (let [raw (page/evaluate pg - (str "document.querySelector('[data-testid=\"" test-id "\"]')?.textContent || ''"))] + (str "document.querySelector(" (pr-str selector) ")?.textContent || ''"))] (when (string? raw) (str/trim raw)))) (defn- wait-for-text [pg test-id text timeout-ms] - (let [deadline (+ (System/currentTimeMillis) timeout-ms)] + (let [deadline (+ (System/currentTimeMillis) timeout-ms) + selector (str "[data-testid=\"" test-id "\"]")] (loop [] - (let [got (or (element-text pg test-id) "")] + (let [got (or (element-text pg selector) "")] (cond (= got text) got (< (System/currentTimeMillis) deadline) (do (Thread/sleep 200) (recur)) diff --git a/test/support/mock_oauth.clj b/test/support/mock_oauth.clj index 5ba7e9be3cf6d2226648e9609a09ed45f306930f..333fe08d56cb06bac2a338cad1c73894d54c4495 100644 --- a/test/support/mock_oauth.clj +++ b/test/support/mock_oauth.clj @@ -7,7 +7,7 @@ (defn- query-param [query key] (when query (some (fn [pair] - (let [[k v] (str/split pair "=" 2)] + (let [[k v] (str/split pair #"=" 2)] (when (= k key) (URLDecoder/decode (or v "") "UTF-8")))) (str/split query #"&")))) @@ -31,11 +31,11 @@ (defn- send-redirect [^HttpExchange ex location] (.set (.getResponseHeaders ex) "Location" location) - (.sendResponseHeaders ex 302 -1) + (.sendResponseHeaders ex 302 0) (.close (.getResponseBody ex))) (defn- read-form [^HttpExchange ex] - (let [body (slurp (.getInputStream ex))] + (let [body (slurp (.getRequestBody ex))] {:code (query-param body "code") :grant (query-param body "grant_type")})) @@ -45,7 +45,7 @@ (str/replace #"^[Bb]earer " ""))) (defn- parse-token-user [token] - (when (str/starts-with? token "mock:") + (when (and token (str/starts-with? token "mock:")) (parse-mock-user (subs token 5)))) (defn- authorize-redirect [exchange query] @@ -55,7 +55,7 @@ user (parse-mock-user mock-user) code (str "mock:" (:id user) ":" (:login user)) loc (str redirect-uri "?code=" (java.net.URLEncoder/encode code "UTF-8") - "&state=" (java.net.URLEncoder/encode state "UTF-8"))] + "&state=" (java.net.URLEncoder/encode (or state "") "UTF-8"))] (send-redirect exchange loc))) (defn start-mock-oauth @@ -65,49 +65,56 @@ handler (proxy [HttpHandler] [] (handle [^HttpExchange exchange] - (let [uri (.getRequestURI exchange) - path (.getPath uri) - query (.getQuery uri) - method (.getRequestMethod exchange)] - (cond - ;; GitHub authorize - (str/ends-with? path "/login/oauth/authorize") - (authorize-redirect exchange query) + (try + (let [uri (.getRequestURI exchange) + path (.getPath uri) + query (.getQuery uri) + method (.getRequestMethod exchange)] + (cond + ;; GitHub authorize + (str/ends-with? path "/login/oauth/authorize") + (authorize-redirect exchange query) - ;; Reddit authorize - (str/ends-with? path "/api/v1/authorize") - (authorize-redirect exchange query) + ;; Reddit authorize + (str/ends-with? path "/api/v1/authorize") + (authorize-redirect exchange query) - ;; GitHub token - (and (= method "POST") (str/ends-with? path "/login/oauth/access_token")) - (let [code (or (:code (read-form exchange)) "mock:1002:newbie")] - (send-json exchange 200 (str "{\"access_token\":\"" code "\",\"token_type\":\"bearer\"}"))) + ;; GitHub token + (and (= method "POST") (str/ends-with? path "/login/oauth/access_token")) + (let [code (or (:code (read-form exchange)) "mock:1002:newbie")] + (send-json exchange 200 (str "{\"access_token\":\"" code "\",\"token_type\":\"bearer\"}"))) - ;; Reddit token (client_credentials for import + authorization_code for login) - (and (= method "POST") (str/ends-with? path "/api/v1/access_token")) - (let [form (read-form exchange) - grant (or (:grant form) "") - code (or (:code form) "mock:t2_test:redditor")] - (if (= grant "client_credentials") - (send-json exchange 200 "{\"access_token\":\"app-token\",\"token_type\":\"bearer\",\"expires_in\":3600}") - (send-json exchange 200 (str "{\"access_token\":\"" code "\",\"token_type\":\"bearer\",\"expires_in\":3600}")))) + ;; Reddit token (client_credentials for import + authorization_code for login) + (and (= method "POST") (str/ends-with? path "/api/v1/access_token")) + (let [form (read-form exchange) + grant (or (:grant form) "") + code (or (:code form) "mock:t2_test:redditor")] + (if (= grant "client_credentials") + (send-json exchange 200 "{\"access_token\":\"app-token\",\"token_type\":\"bearer\",\"expires_in\":3600}") + (send-json exchange 200 (str "{\"access_token\":\"" code "\",\"token_type\":\"bearer\",\"expires_in\":3600}")))) - ;; GitHub user - (= path "/user") - (let [token (bearer-token exchange) - user (or (parse-token-user token) {:id "1002" :login "newbie" :numeric? true})] - (send-json exchange 200 - (str "{\"id\":" (:id user) ",\"login\":\"" (:login user) "\"}"))) + ;; GitHub user + (= path "/user") + (let [token (bearer-token exchange) + user (or (parse-token-user token) {:id "1002" :login "newbie" :numeric? true})] + (send-json exchange 200 + (str "{\"id\":" (:id user) ",\"login\":\"" (:login user) "\"}"))) - ;; Reddit /api/v1/me - (str/ends-with? path "/api/v1/me") - (let [token (bearer-token exchange) - user (or (parse-token-user token) {:id "t2_test" :login "redditor"})] - (send-json exchange 200 - (str "{\"id\":\"" (:id user) "\",\"name\":\"" (:login user) "\"}"))) + ;; Reddit /api/v1/me + (str/ends-with? path "/api/v1/me") + (let [token (bearer-token exchange) + user (or (parse-token-user token) {:id "t2_test" :login "redditor"})] + (send-json exchange 200 + (str "{\"id\":\"" (:id user) "\",\"name\":\"" (:login user) "\"}"))) - :else - (send-json exchange 404 "{\"error\":\"not found\"}")))))] + :else + (send-json exchange 404 "{\"error\":\"not found\"}"))) + (catch Throwable t + (binding [*out* *err*] + (println "mock-oauth handler error:" t)) + (try + (send-json exchange 500 "{\"error\":\"mock-oauth internal\"}") + (catch Throwable _))))))] (.createContext server "/" handler) (.setExecutor server nil) (.start server) diff --git a/test/support/mock_reddit.clj b/test/support/mock_reddit.clj index a630cf0938722193e9af88382d60e777ff371be4..faa27945b6394363914c853627a0bad1e819a5f9 100644 --- a/test/support/mock_reddit.clj +++ b/test/support/mock_reddit.clj @@ -12,7 +12,7 @@ (defn- query-param [query key] (when query (some (fn [pair] - (let [[k v] (str/split pair "=" 2)] + (let [[k v] (str/split pair #"=" 2)] (when (= k key) (URLDecoder/decode (or v "") "UTF-8")))) (str/split query #"&")))) @@ -34,11 +34,11 @@ (defn- send-redirect [^HttpExchange ex location] (.set (.getResponseHeaders ex) "Location" location) - (.sendResponseHeaders ex 302 -1) + (.sendResponseHeaders ex 302 0) (.close (.getResponseBody ex))) (defn- read-form [^HttpExchange ex] - (let [body (slurp (.getInputStream ex))] + (let [body (slurp (.getRequestBody ex))] {:code (query-param body "code") :grant (query-param body "grant_type")})) @@ -48,7 +48,7 @@ (str/replace #"^[Bb]earer " ""))) (defn- parse-token-user [token] - (when (str/starts-with? token "mock:") + (when (and token (str/starts-with? token "mock:")) (parse-mock-user (subs token 5)))) (defn start-mock-reddit @@ -72,7 +72,7 @@ user (parse-mock-user (query-param query "mock_user")) code (str "mock:" (:id user) ":" (:login user)) loc (str redirect-uri "?code=" (java.net.URLEncoder/encode code "UTF-8") - "&state=" (java.net.URLEncoder/encode state "UTF-8"))] + "&state=" (java.net.URLEncoder/encode (or state "") "UTF-8"))] (send-redirect exchange loc)) (and (= method "POST") (str/ends-with? path "/api/v1/access_token")) Side B — contributor: tommy-mor Side B — commit message: [14749a34] Show graph topology with pair suggestions Expose existing connectivity statistics in CLI output so voters can see sparse or disconnected scopes before adding an edge. Co-authored-by: Cursor Side B — unified diff (full patch): diff --git a/cli/src/main.rs b/cli/src/main.rs index 70435b412188a151c5e89e842a5de57f7480ddf2..abb5a55b49f60fe28fbfd4ec02715cb94ea0b4ec 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -397,6 +397,29 @@ fn print_item_response(resp: &ItemResponse) { } } +fn format_connectivity_stats(stats: &ConnectivityStats) -> String { + let density = if stats.pairs_possible == 0 { + 0.0 + } else { + stats.pairs_voted as f64 / stats.pairs_possible as f64 * 100.0 + }; + let component_label = if stats.components == 1 { "component" } else { "components" }; + let connection_status = if stats.comparisons_until_connected == 0 { + "connected".to_string() + } else { + let comparison_label = if stats.comparisons_until_connected == 1 { + "comparison" + } else { + "comparisons" + }; + format!("{} {comparison_label} to connect", stats.comparisons_until_connected) + }; + format!( + "graph: {} items, {}/{} pairs ({density:.1}% density), {} {component_label}, {connection_status}", + stats.items, stats.pairs_voted, stats.pairs_possible, stats.components, + ) +} + fn print_pair_response(resp: &PairResponse) { println!("{} vs {}", resp.left, resp.right); if let Some(b) = &resp.left_body { @@ -413,6 +436,10 @@ fn print_pair_response(resp: &PairResponse) { println!(); println!("threads: {}", resp.threads.iter().map(|t| format!("#{t}")).collect::>().join(" ")); } + if let Some(stats) = &resp.connectivity { + println!(); + println!("{}", format_connectivity_stats(stats)); + } println!(); println!("---"); println!("Before writing your comparison, talk to your human. Ask them about their relationship to these two items — which one has meant more to them, whether their perspective has changed over time, and where they land now. Listen to what they say. Then write a comparison that synthesizes their perspective with your own analysis. The best submissions on slug.social carry both voices — your reasoning and their experience."); @@ -816,6 +843,43 @@ fn write_secret_file(name: &str, contents: &str) -> Result<()> { Ok(()) } +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn connectivity_stats_show_sparse_disconnected_graph() { + let stats = ConnectivityStats { + items: 9, + components: 3, + comparisons_until_connected: 2, + pairs_voted: 8, + pairs_possible: 36, + }; + + assert_eq!( + format_connectivity_stats(&stats), + "graph: 9 items, 8/36 pairs (22.2% density), 3 components, 2 comparisons to connect" + ); + } + + #[test] + fn connectivity_stats_show_connected_graph() { + let stats = ConnectivityStats { + items: 4, + components: 1, + comparisons_until_connected: 0, + pairs_voted: 3, + pairs_possible: 6, + }; + + assert_eq!( + format_connectivity_stats(&stats), + "graph: 4 items, 3/6 pairs (50.0% density), 1 component, connected" + ); + } +} + async fn run_scoped(base: &str, room: &str, sub: ScopedCmd) -> Result<()> { let room = room.trim(); let client = http_client()?;