{"messages":[{"content":"You are a constitutional council ranking individual git commits for ownership allocation.\n\nCompare these two commits. Decide which contributed more lasting value to the project.\n\nJudge substance, not spectacle:\n- Prefer correct, lasting design and real bugfixes over churn, formatting, renames, or generated noise.\n- Prefer clarity and necessity over sheer line count. A small precise change can beat a large diffuse one.\n- Do not favor a side merely because its patch is longer or noisier.\n- Weight what the change does for the project, not the contributor's name.\n\nReturn ONLY a JSON object: {\"winner\": \"A\" or \"B\", \"ratio\": \"N:M\", \"explanation\": \"...\"}\nThe explanation must cite concrete differences in the patches (1-3 sentences).\n\nSide A — contributor: tommy-mor\nSide A — commit message:\n[9e3d989b] Add test that 25 spanning-tree votes with perfect ratios sort the alphabet.\n\nDemonstrates rank centrality recovers the true order from a random connected\ncomparison graph when each vote encodes item strength as (idx+1).\n\nCo-authored-by: Cursor \n\nSide A — unified diff (full patch):\ndiff --git a/server/src/ranking.rs b/server/src/ranking.rs\nindex c9b4d5d89d2f22cf53a5b43eb979e19a11bc89a1..93cb4c9f5887a1e598cdc9d648751055f618adcc 100644\n--- a/server/src/ranking.rs\n+++ b/server/src/ranking.rs\n@@ -369,6 +369,48 @@ mod tests {\n assert_eq!(comp1, vec![\"c\", \"d\"]);\n }\n \n+ /// A random spanning tree over 26 items needs only n−1 = 25 pairwise votes.\n+ /// When each vote uses the \"perfect\" ratio (strength left : strength right =\n+ /// (idx_left+1) : (idx_right+1)), rank centrality recovers the true order.\n+ /// See `rank-eric.py` (Eric's demo of Negahban–Oh–Shah rank centrality).\n+ #[test]\n+ fn twenty_five_random_votes_perfect_ratios_sort_alphabet() {\n+ use rand::seq::SliceRandom;\n+\n+ const N: usize = 26;\n+ let letters: Vec = (0..N).map(|i| char::from(b'a' + i as u8)).collect();\n+\n+ let mut rng = rand::thread_rng();\n+ let mut perm: Vec = (0..N).collect();\n+ perm.shuffle(&mut rng);\n+\n+ let mut g = mk_group();\n+ for k in 1..N {\n+ let i = *perm[..k].choose(&mut rng).unwrap();\n+ let j = perm[k];\n+ let (a, b) = (letters[i], letters[j]);\n+ g.apply_vote(vote(\n+ k as i64,\n+ &a.to_string(),\n+ &b.to_string(),\n+ (i + 1) as i32,\n+ (j + 1) as i32,\n+ ));\n+ }\n+\n+ let ranked = ranked_items(&g);\n+ assert_eq!(ranked.len(), N);\n+ for (rank, item) in ranked.iter().enumerate() {\n+ let expected = char::from(b'a' + (N - 1 - rank) as u8);\n+ assert_eq!(\n+ item.item.as_str(),\n+ expected.to_string(),\n+ \"rank {rank}: expected '{expected}', got '{}'\",\n+ item.item.as_str()\n+ );\n+ }\n+ }\n+\n #[test]\n fn subset_ranking_ranks_within_component_only() {\n let mut g = mk_group();\n\n\nSide B — contributor: tommy-mor\nSide B — commit message:\n[9ecc4e2e] nice\n\nSide B — unified diff (full patch):\ndiff --git a/server/src/api/rpc.rs b/server/src/api/rpc.rs\nindex 5675dcd2e28062acbe3c037b94b77c33adf32474..a18241f3ed7b4a248748fcefc799f9801ca62461 100644\n--- a/server/src/api/rpc.rs\n+++ b/server/src/api/rpc.rs\n@@ -936,7 +936,15 @@ pub async fn handle_rpc_batch(\n line_ok(RpcResult::ForumThreads(rpc_list_forum_threads(&reduced, &room)))\n }\n RpcCommand::RoomCreate { slug, visibility } => {\n- match verify_bearer_principal(&headers, &*state.reduced.read().await) {\n+ // Scope the first read so its guard drops before any nested `read().await` / `write().await`.\n+ // A guard from `match verify(..., &*state.reduced.read().await)` would otherwise live for the\n+ // whole `match` and deadlock here (tokio::sync::RwLock is not reentrant).\n+ let principal = {\n+ let reduced = state.reduced.read().await;\n+ verify_bearer_principal(&headers, &*reduced)\n+ };\n+ match principal {\n+ Err((_, m)) => line_err(m, None),\n Ok(principal) => {\n let slug = slug.trim().to_lowercase();\n if slug.is_empty() || slug.len() > 64 {\n@@ -994,7 +1002,6 @@ pub async fn handle_rpc_batch(\n }\n }\n }\n- Err((_, m)) => line_err(m, None),\n }\n }\n RpcCommand::RoomGrant {\n@@ -1002,17 +1009,28 @@ pub async fn handle_rpc_batch(\n username,\n capability,\n } => {\n- match verify_bearer_principal(&headers, &*state.reduced.read().await) {\n+ let principal = {\n+ let reduced = state.reduced.read().await;\n+ verify_bearer_principal(&headers, &*reduced)\n+ };\n+ match principal {\n Err((_, m)) => line_err(m, None),\n Ok(principal) => {\n- let reduced = state.reduced.read().await;\n- if !reduced.user_has_cap(&room, &principal, ThreadCapability::Manage) {\n+ let can_manage = {\n+ let reduced = state.reduced.read().await;\n+ reduced.user_has_cap(&room, &principal, ThreadCapability::Manage)\n+ };\n+ if !can_manage {\n line_err(\"requires Manage capability\", None)\n } else {\n match parse_username(&username) {\n Err(msg) => line_err(\"invalid username\", Some(msg)),\n Ok(target) => {\n- if !reduced.users_by_provider.values().any(|u| u == &target) {\n+ let user_exists = {\n+ let reduced = state.reduced.read().await;\n+ reduced.users_by_provider.values().any(|u| u == &target)\n+ };\n+ if !user_exists {\n line_err(format!(\"user @{target} not found\"), None)\n } else {\n match parse_capability(&capability) {\ndiff --git a/server/tests/integration.rs b/server/tests/integration.rs\nindex 9162bd5bee84035e3908bfb9c8e201b7878ca339..b930120da09fe7d307f0411b84fb639fb8bd0b15 100644\n--- a/server/tests/integration.rs\n+++ b/server/tests/integration.rs\n@@ -95,6 +95,23 @@ async fn test_healthz() {\n assert_eq!(response.text().await.unwrap(), \"ok\");\n }\n \n+#[tokio::test]\n+async fn test_room_create_private_rpc() {\n+ let (addr, _tmp, _log, _handle) = create_test_server().await;\n+ let client = reqwest::Client::new();\n+ let batch = serde_json::json!([{\n+ \"RoomCreate\": { \"slug\": \"secret-project\", \"visibility\": \"private\" }\n+ }]);\n+ let body = rpc_batch(&client, addr, Some(&test_bearer()), batch).await;\n+ let line = &body[\"results\"][0];\n+ assert_eq!(line[\"ok\"], true, \"room create: {:?}\", line);\n+ let room_id = line[\"result\"][\"RoomCreated\"][\"room_id\"].as_str().unwrap();\n+ assert!(\n+ room_id.contains(\"/secret-project\"),\n+ \"expected room_id to contain slug, got {room_id}\"\n+ );\n+}\n+\n #[tokio::test]\n async fn test_index_page() {\n // HTML routes are offline during the auth-v3 refactor.\ndiff --git a/test/auth.bb b/test/auth.bb\nindex 611e04f1806ef81d678a91f499597fe55f691dd7..a67cc1de763434176d2f68e419dd37a8cd328395 100644\n--- a/test/auth.bb\n+++ b/test/auth.bb\n@@ -176,7 +176,7 @@\n (assert! (= 200 (:status poll)) \"pending-session poll returns 200\")\n (let [poll-json (json/parse-string (:body poll) true)]\n (assert! (:complete poll-json) \"pending session complete=true\")\n- (assert! (= \"@bbuser\" (:user poll-json)) \"poll returns @bbuser\")\n+ (assert! (= \"bbuser\" (:user poll-json)) \"poll returns stored username bbuser\")\n (assert! (clojure.string/starts-with? (:token poll-json) \"slug_\") \"poll returns bearer token\")\n \n (println \"\\nwhoami…\")\n@@ -184,7 +184,7 @@\n :headers {\"Authorization\" (str \"Bearer \" (:token poll-json))})]\n (assert! (= 200 (:status who)) \"whoami returns 200\")\n (let [who-json (json/parse-string (:body who) true)]\n- (assert! (= \"@bbuser\" (:user who-json)) \"whoami user is @bbuser\"))))))\n+ (assert! (= \"bbuser\" (:user who-json)) \"whoami user is bbuser (stored form)\"))))))\n \n (println \"\\nCLI: identity start → OAuth → identity poll → whoami…\")\n (let [cli-home (str tmp-dir \"/cli-home\")\n@@ -208,7 +208,7 @@\n (str \"identity poll exits 0 (stderr: \" (:err poll-proc) \")\"))\n (let [poll-cli (json/parse-string (:out poll-proc) true)]\n (assert! (= \"complete\" (:phase poll-cli)) \"identity poll --json phase\")\n- (assert! (= \"@cliuser\" (:user poll-cli)) \"CLI poll user\")\n+ (assert! (= \"cliuser\" (:user poll-cli)) \"CLI poll user (stored form)\")\n (assert! (clojure.string/starts-with? (:token poll-cli) \"slug_\") \"CLI poll token\")\n (let [token-path (str cli-home \"/.config/slugsocial/token\")]\n (assert! (fs/exists? token-path) \"token written under isolated HOME\")\n@@ -219,7 +219,7 @@\n (assert! (zero? (:exit who-proc))\n (str \"whoami exits 0 (stderr: \" (:err who-proc) \")\"))\n (let [who-cli (json/parse-string (:out who-proc) true)]\n- (assert! (= \"@cliuser\" (:user who-cli)) \"CLI whoami uses saved token\"))))))))\n+ (assert! (= \"cliuser\" (:user who-cli)) \"CLI whoami uses saved token\"))))))))\n \n (finally\n (when-some [s @!server] (common/kill-server s))\ndiff --git a/test/common.bb b/test/common.bb\nindex ebb16c615a8b40e8830b5d5765d1e935a45538d0..4ed450377cdbb020f5ff164a812dfbbfc23c0f47 100644\n--- a/test/common.bb\n+++ b/test/common.bb\n@@ -78,9 +78,20 @@\n \n (defn start-server\n \"Start the slugsocial-server binary with the given env map.\n- Returns the babashka.process map.\"\n- [server-bin env-map]\n- (p/process [server-bin] {:out :inherit :err :inherit :env env-map}))\n+ Returns the babashka.process map.\n+\n+ When `log-file` (string path) is provided, stdout and stderr are appended there\n+ instead of inheriting the parent descriptors. Inheriting shared pipes while the\n+ parent blocks on HTTP I/O can fill the pipe buffer and deadlock the server on log writes.\"\n+ ([server-bin env-map]\n+ (start-server server-bin env-map nil))\n+ ([server-bin env-map log-file]\n+ (p/process [server-bin]\n+ (if log-file\n+ ;; Two string paths (same file): babashka.process can deref the process cleanly.\n+ ;; :err :out + ProcessBuilder$Redirect breaks stream copying in deref/kill-server.\n+ {:env env-map :out log-file :err log-file}\n+ {:out :inherit :err :inherit :env env-map}))))\n \n (defn kill-server\n \"Forcibly kill a server process (babashka.process map) and wait for it to exit.\"\ndiff --git a/test/grants.bb b/test/grants.bb\nindex 7066c1f2a57f39a362052f8524206dccf37bb7b1..793ba216f2de76a77eb20a76d08403db07ff411b 100644\n--- a/test/grants.bb\n+++ b/test/grants.bb\n@@ -35,13 +35,14 @@\n (defn- http-client []\n (-> (java.net.http.HttpClient/newBuilder)\n (.followRedirects java.net.http.HttpClient$Redirect/ALWAYS)\n+ (.connectTimeout (java.time.Duration/ofSeconds 15))\n (.build)))\n \n (defn- http-get [url & {:keys [headers]}]\n (let [b (java.net.http.HttpRequest/newBuilder (java.net.URI/create url))]\n (doseq [[k v] (or headers {})]\n (.header b k v))\n- (let [req (-> b (.GET) (.build))\n+ (let [req (-> b (.timeout (java.time.Duration/ofSeconds 60)) (.GET) (.build))\n resp (.send (http-client) req (java.net.http.HttpResponse$BodyHandlers/ofString))]\n {:status (.statusCode resp) :body (.body resp)})))\n \n@@ -52,6 +53,7 @@\n (doseq [[k v] (or headers {})]\n (.header b k v))\n (let [req (-> b\n+ (.timeout (java.time.Duration/ofSeconds 60))\n (.POST (java.net.http.HttpRequest$BodyPublishers/ofString body))\n (.build))\n resp (.send (http-client) req (java.net.http.HttpResponse$BodyHandlers/ofString))]\n@@ -67,6 +69,7 @@\n b (java.net.http.HttpRequest/newBuilder (java.net.URI/create url))]\n (.header b \"Content-Type\" \"application/x-www-form-urlencoded\")\n (let [req (-> b\n+ (.timeout (java.time.Duration/ofSeconds 60))\n (.POST (java.net.http.HttpRequest$BodyPublishers/ofString pairs))\n (.build))\n resp (.send (http-client) req (java.net.http.HttpResponse$BodyHandlers/ofString))]\n","role":"user"}],"model":"~anthropic/claude-sonnet-latest"}