{"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[f6e0be88] add fly.io deployment with external postgres support\n\nShip a production docker image and fly config so sorter can run on a single fly machine against managed postgres, including ssl url parsing and db connection retries.\n\nCo-authored-by: Cursor \n\nSide A — unified diff (full patch):\ndiff --git a/borter/src/app/database.clj b/borter/src/app/database.clj\nindex 9a7251a94d3b237bc640541cdeb1845ef3669738..7d829634869ca4d66038d7ceeaf248fd488a566c 100644\n--- a/borter/src/app/database.clj\n+++ b/borter/src/app/database.clj\n@@ -16,13 +16,21 @@\n \n (defn parse-jdbc [jdbc-url]\n (try\n- (drop 1 (re-matches\n- #\"postgres://(.*):(.*)@(.*):(\\d*)/(.*)\"\n- jdbc-url))\n+ (let [normalized (-> jdbc-url\n+ (clojure.string/replace #\"^postgresql://\" \"postgres://\")\n+ (clojure.string/replace #\"\\?.*$\" \"\"))]\n+ (drop 1 (re-matches\n+ #\"postgres://(.*):(.*)@(.*):(\\d*)/(.*)\"\n+ normalized)))\n (catch Exception e\n (log/error \"Failed to parse JDBC URL:\" (.getMessage e))\n nil)))\n \n+(defn ssl-config [jdbc-url]\n+ (when (or (clojure.string/includes? (or jdbc-url \"\") \"sslmode=require\")\n+ (= \"require\" (System/getenv \"PGSSLMODE\")))\n+ {:ssl true :sslmode \"require\"}))\n+\n (defn build-datasource []\n ;; Read DATABASE_URL *at runtime*\n (try\n@@ -39,22 +47,23 @@\n (swap! db-state assoc :last-error \"Invalid DATABASE_URL format\")\n nil)\n (let [[username password host port database] parsed\n- cfg {:auto-commit true\n- :read-only false\n- :connection-timeout 30000\n- :validation-timeout 5000\n- :idle-timeout 600000\n- :max-lifetime 1800000\n- :minimum-idle 10\n- :maximum-pool-size 10\n- :pool-name \"db-pool\"\n- :register-mbeans false\n- :adapter \"postgresql\"\n- :username username\n- :password password\n- :server-name host\n- :port-number (Integer/parseInt port)\n- :database-name database}]\n+ cfg (merge {:auto-commit true\n+ :read-only false\n+ :connection-timeout 30000\n+ :validation-timeout 5000\n+ :idle-timeout 600000\n+ :max-lifetime 1800000\n+ :minimum-idle 2\n+ :maximum-pool-size 5\n+ :pool-name \"db-pool\"\n+ :register-mbeans false\n+ :adapter \"postgresql\"\n+ :username username\n+ :password password\n+ :server-name host\n+ :port-number (Integer/parseInt port)\n+ :database-name database}\n+ (ssl-config database-url))]\n (try\n (log/info \"Attempting to create database connection pool\")\n (make-datasource cfg)\ndiff --git a/borter/src/prod.clj b/borter/src/prod.clj\nindex 1b541a0628494a4e3bf6b339ebd543b8aaec25b8..746763bb14170b172d11060965a0abf511305891 100644\n--- a/borter/src/prod.clj\n+++ b/borter/src/prod.clj\n@@ -81,23 +81,24 @@\n (defn -main\n \"Run with `clj -M -m prod` or with optional port override {:port 3000}\"\n [& {:keys [port] :or {port 8080}}]\n- (log/info \"\\n ______ _____ ______ _______ _______ ______\\n |_____] | | |_____/ | |______ |_____/\\n |_____] |_____| | \\\\_ | |______ | \\\\_\\n\\nšŸ“¦ļø\" version \"\\n\")\n- (log/info \"starting borter\" version \"in\" environment \"environment\")\n- (log/debug \"server configuration:\" electric-server-config)\n- (log/info \"starting repl with config\" (dissoc nrepl-config :auth)) ; don't log password\n- (reset! server-instance (nrepl-server/start-server nrepl-config))\n- (try\n- (db/run-migrations)\n- (log/info \"database migrations completed successfully\")\n- (catch Exception e\n- (log/error e \"failed to run database migrations\")))\n- (try\n- (http/start {:port port})\n- (log/info \"http server started successfully on port\" port)\n- (catch Exception e\n- (log/error e \"failed to start http server\")))\n- ;; Start the database health checker\n- (start-db-health-checker))\n+ (let [port (or (some-> (System/getenv \"PORT\") Integer/parseInt)\n+ port)]\n+ (log/info \"\\n ______ _____ ______ _______ _______ ______\\n |_____] | | |_____/ | |______ |_____/\\n |_____] |_____| | \\\\_ | |______ | \\\\_\\n\\nšŸ“¦ļø\" version \"\\n\")\n+ (log/info \"starting borter\" version \"in\" environment \"environment\")\n+ (log/debug \"server configuration:\" electric-server-config)\n+ (log/info \"starting repl with config\" (dissoc nrepl-config :auth)) ; don't log password\n+ (reset! server-instance (nrepl-server/start-server nrepl-config))\n+ (try\n+ (db/run-migrations)\n+ (log/info \"database migrations completed successfully\")\n+ (catch Exception e\n+ (log/error e \"failed to run database migrations\")))\n+ (try\n+ (http/start {:port port})\n+ (log/info \"http server started successfully on port\" port)\n+ (catch Exception e\n+ (log/error e \"failed to start http server\")))\n+ (start-db-health-checker)))\n \n (defn -shutdown-hook []\n (log/info \"Shutdown hook called, cleaning up resources\")\ndiff --git a/docker/fly/Dockerfile b/docker/fly/Dockerfile\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..47b3346f72a3cb4285cdf185003641729518f2d0\n--- /dev/null\n+++ b/docker/fly/Dockerfile\n@@ -0,0 +1,50 @@\n+# Stage 1: build frontend assets into borter/resources/public\n+FROM node:20-bookworm-slim AS forter-build\n+WORKDIR /app\n+COPY forter/package.json forter/package-lock.json ./forter/\n+RUN cd forter && npm ci\n+COPY forter/ ./forter/\n+COPY borter/resources/public ./borter/resources/public/\n+RUN mkdir -p borter/resources/public && cd forter && ENVIRONMENT=staging npm run staging\n+\n+# Stage 2: build borter uberjar\n+FROM zudsniper/clojure:openjdk-17-node AS borter-build\n+WORKDIR /app/borter\n+COPY borter/deps.edn borter/build.sh borter/version ./\n+COPY borter/src ./src/\n+COPY borter/src-build ./src-build/\n+COPY borter/resources ./resources/\n+COPY --from=forter-build /app/borter/resources/public ./resources/public/\n+RUN clojure -P && VERSION=$(cat version) && clojure -X:build uberjar :version \"\\\"$VERSION\\\"\"\n+\n+# Stage 3: build rustsorter\n+FROM rust:1-bookworm AS rustsorter-build\n+WORKDIR /app\n+RUN apt-get update && apt-get install -y libpq-dev pkg-config && rm -rf /var/lib/apt/lists/*\n+COPY rustsorter/Cargo.toml rustsorter/Cargo.lock ./\n+COPY rustsorter/.sqlx ./.sqlx\n+COPY rustsorter/src ./src\n+ENV SQLX_OFFLINE=true\n+RUN cargo build --release\n+\n+# Stage 4: runtime\n+FROM eclipse-temurin:17-jre-jammy\n+RUN apt-get update && apt-get install -y supervisor curl && rm -rf /var/lib/apt/lists/*\n+\n+WORKDIR /app\n+\n+COPY --from=borter-build /app/borter/target/sorter-*-standalone.jar /app/borter/sorter.jar\n+COPY --from=rustsorter-build /app/target/release/rustsorter /app/rustsorter/rustsorter\n+COPY docker/fly/supervisord.conf /etc/supervisor/conf.d/supervisord.conf\n+COPY docker/fly/entrypoint.sh /entrypoint.sh\n+RUN chmod +x /entrypoint.sh && mkdir -p /app/borter/logs\n+\n+ENV JAVA_OPTS=\"-Xmx256m -Xms64m\"\n+ENV ENVIRONMENT=staging\n+ENV PORT=8080\n+EXPOSE 8080\n+\n+HEALTHCHECK --interval=30s --timeout=10s --start-period=120s --retries=3 \\\n+ CMD curl -f http://127.0.0.1:${PORT}/api/health || exit 1\n+\n+ENTRYPOINT [\"/entrypoint.sh\"]\ndiff --git a/docker/fly/entrypoint.sh b/docker/fly/entrypoint.sh\nnew file mode 100755\nindex 0000000000000000000000000000000000000000..cb210a8c2612370c8c8768029cc761ce9a591b1b\n--- /dev/null\n+++ b/docker/fly/entrypoint.sh\n@@ -0,0 +1,22 @@\n+#!/bin/bash\n+set -euo pipefail\n+\n+if [ -z \"${DATABASE_URL:-}\" ]; then\n+ echo \"DATABASE_URL is required\"\n+ exit 1\n+fi\n+\n+# normalize postgresql:// URLs and keep ssl params for downstream consumers\n+export DATABASE_URL=\"${DATABASE_URL/postgresql:\\/\\//postgres:\\/\\/}\"\n+export PGSSLMODE=\"${PGSSLMODE:-require}\"\n+export PORT=\"${PORT:-8080}\"\n+export ENVIRONMENT=\"${ENVIRONMENT:-staging}\"\n+export REPL_PASSWORD=\"${REPL_PASSWORD:-fly-repl-password}\"\n+export HOSTNAME=\"${HOSTNAME:-localhost}\"\n+export BORTER_LOG_PATH=\"${BORTER_LOG_PATH:-/app/borter/logs}\"\n+export BORTER_LOG_LEVEL=\"${BORTER_LOG_LEVEL:-info}\"\n+export AUTO_LOGIN=\"${AUTO_LOGIN:-true}\"\n+export NO_RATE_LIMITS=\"${NO_RATE_LIMITS:-true}\"\n+export RUSTSORTER_URL=\"${RUSTSORTER_URL:-http://127.0.0.1:8081}\"\n+\n+exec supervisord -c /etc/supervisor/conf.d/supervisord.conf\ndiff --git a/docker/fly/supervisord.conf b/docker/fly/supervisord.conf\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..5e2c67a1cffe379b0c53bd3ce2ec6b7184e789e5\n--- /dev/null\n+++ b/docker/fly/supervisord.conf\n@@ -0,0 +1,28 @@\n+[supervisord]\n+nodaemon=true\n+loglevel=info\n+\n+[program:rustsorter]\n+command=/app/rustsorter/rustsorter\n+directory=/app/rustsorter\n+autostart=true\n+autorestart=true\n+stdout_logfile=/dev/stdout\n+stdout_logfile_maxbytes=0\n+stderr_logfile=/dev/stderr\n+stderr_logfile_maxbytes=0\n+priority=10\n+environment=DATABASE_URL=\"%(ENV_DATABASE_URL)s\",ENVIRONMENT=\"%(ENV_ENVIRONMENT)s\"\n+\n+[program:borter]\n+command=java %(ENV_JAVA_OPTS)s -jar /app/borter/sorter.jar\n+directory=/app/borter\n+autostart=true\n+autorestart=true\n+startsecs=15\n+stdout_logfile=/dev/stdout\n+stdout_logfile_maxbytes=0\n+stderr_logfile=/dev/stderr\n+stderr_logfile_maxbytes=0\n+priority=20\n+environment=DATABASE_URL=\"%(ENV_DATABASE_URL)s\",ENVIRONMENT=\"%(ENV_ENVIRONMENT)s\",PORT=\"%(ENV_PORT)s\",REPL_PASSWORD=\"%(ENV_REPL_PASSWORD)s\",HOSTNAME=\"%(ENV_HOSTNAME)s\",BORTER_LOG_PATH=\"%(ENV_BORTER_LOG_PATH)s\",BORTER_LOG_LEVEL=\"%(ENV_BORTER_LOG_LEVEL)s\",AUTO_LOGIN=\"%(ENV_AUTO_LOGIN)s\",NO_RATE_LIMITS=\"%(ENV_NO_RATE_LIMITS)s\",RUSTSORTER_URL=\"%(ENV_RUSTSORTER_URL)s\",PGSSLMODE=\"%(ENV_PGSSLMODE)s\"\ndiff --git a/fly.toml b/fly.toml\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..0ea6b263bdff021981dfda9aa6c48f9a4ba2f3a9\n--- /dev/null\n+++ b/fly.toml\n@@ -0,0 +1,32 @@\n+app = 'sorter-fly-app'\n+primary_region = 'ewr'\n+\n+[build]\n+ dockerfile = 'docker/fly/Dockerfile'\n+\n+[env]\n+ ENVIRONMENT = 'staging'\n+ PORT = '8080'\n+ PGSSLMODE = 'require'\n+ AUTO_LOGIN = 'true'\n+ NO_RATE_LIMITS = 'true'\n+\n+[http_service]\n+ internal_port = 8080\n+ force_https = true\n+ auto_stop_machines = 'stop'\n+ auto_start_machines = true\n+ min_machines_running = 1\n+ processes = ['app']\n+\n+ [[http_service.checks]]\n+ interval = '30s'\n+ timeout = '10s'\n+ grace_period = '180s'\n+ method = 'GET'\n+ path = '/index.html'\n+\n+[[vm]]\n+ size = 'shared-cpu-1x'\n+ memory = '512mb'\n+ cpus = 1\ndiff --git a/rustsorter/src/main.rs b/rustsorter/src/main.rs\nindex 2dc534ca99566b170e948d11396baa95b91b5870..c4af5978509afe04a2ab940be0d3c96bbaef0571 100644\n--- a/rustsorter/src/main.rs\n+++ b/rustsorter/src/main.rs\n@@ -8,7 +8,7 @@ pub struct AppState {\n pub db: sqlx::PgPool,\n }\n \n-use std::{env, sync::Arc};\n+use std::{env, sync::Arc, time::Duration};\n \n use actix_web::{App, HttpServer, middleware::Logger, web, HttpResponse, get};\n use dotenv::dotenv;\n@@ -33,12 +33,21 @@ async fn main() -> Result<(), anyhow::Error> {\n // Retrieve the database URL from the environment\n let database_string = env::var(\"DATABASE_URL\").expect(\"DATABASE_URL must be set\");\n \n- // Create the database pool\n- let pool = sqlx::postgres::PgPoolOptions::new()\n- .max_connections(5)\n- .connect(&database_string)\n- .await\n- .expect(\"Unable to connect to database\");\n+ // Create the database pool with retries for external databases\n+ let pool = loop {\n+ match sqlx::postgres::PgPoolOptions::new()\n+ .max_connections(5)\n+ .acquire_timeout(Duration::from_secs(10))\n+ .connect(&database_string)\n+ .await\n+ {\n+ Ok(pool) => break pool,\n+ Err(e) => {\n+ eprintln!(\"unable to connect to database: {e}, retrying in 5s...\");\n+ tokio::time::sleep(Duration::from_secs(5)).await;\n+ }\n+ }\n+ };\n \n // Shared application state\n let app_state = Arc::new(AppState { db: pool.clone() });\n\n\nSide B — contributor: tommy-mor\nSide B — commit message:\n[f7eb8b76] css\n\nSide B — unified diff (full patch):\ndiff --git a/server/src/html/garden.rs b/server/src/html/garden.rs\nindex 41f9e9c64a80a55a9d3ece2a6a1f592cf5e8d8dd..8fc6be1de8dd975f9547de615809222236be4b70 100644\n--- a/server/src/html/garden.rs\n+++ b/server/src/html/garden.rs\n@@ -362,7 +362,6 @@ fn child_row_pin_or_vote(\n row_item: &ItemId,\n pinned_room_and_item: Option<&(String, ItemId)>,\n scope_content: &ContentState,\n- next_path: &str,\n ) -> maud::Markup {\n let pin_matches_scope = pinned_room_and_item\n .map(|(r, _)| r == nav.room_wire.as_str())\n@@ -371,21 +370,9 @@ fn child_row_pin_or_vote(\n .filter(|_| pin_matches_scope)\n .map(|(_, i)| i);\n \n- let pin_rpc = template_json_compact(\n- &json!({\n- \"action\": \"set_garden_pin\",\n- \"clear\": false,\n- \"room_wire\": nav.room_wire.clone(),\n- \"item_storage\": row_item.as_str(),\n- \"next\": next_path,\n- \"form_action\": \"/ui\",\n- }),\n- )\n- .expect(\"child pin rpc json\");\n-\n html! {\n- span class=\"ont-garden-child-actions\" data-garden-room=(nav.room_wire.as_str()) {\n- @if let Some(pi) = pinned_item {\n+ @if let Some(pi) = pinned_item {\n+ span class=\"ont-garden-child-actions\" data-garden-room=(nav.room_wire.as_str()) {\n @if pi == row_item {\n span class=\"ont-garden-pinned-here\" title=\"Pinned\" aria-label=\"Pinned\" { \"šŸ“Œ\" }\n } @else {\n@@ -400,11 +387,6 @@ fn child_row_pin_or_vote(\n span class=\"ont-garden-vote-count\" { (format!(\"{}\", nv)) }\n }\n }\n- } @else {\n- form method=\"POST\" action=\"/ui\" data-navigate=\"full\" class=\"ont-pin-form ont-garden-pin-form\" {\n- input type=\"hidden\" name=(UI_RPC_FIELD) value=(pin_rpc);\n- button type=\"submit\" class=\"ont-garden-pin-ico\" title=\"Pin\" aria-label=\"Pin\" { \"šŸ“Œ\" }\n- }\n }\n }\n }\n@@ -1190,7 +1172,7 @@ async fn render_scope_view(\n @let item_url = item_href(r.item.as_str(), &nav);\n @let score_str = format!(\"{:.3}\", r.score);\n li data-garden-item=(r.item.as_str()) {\n- (child_row_pin_or_vote(&nav, &r.item, pin_ref.as_ref(), scope_content, &next_for_pin))\n+ (child_row_pin_or_vote(&nav, &r.item, pin_ref.as_ref(), scope_content))\n a class=\"item-link\" href=(item_url) { code { (item_display_path(r.item.as_str())) } }\n span class=\"ont-rank-score\" { (score_str) }\n }\n@@ -1206,7 +1188,7 @@ async fn render_scope_view(\n ul class=\"ont-group-list\" {\n @for name in &model.child_rankings.unranked_items {\n li data-garden-item=(name.as_str()) {\n- (child_row_pin_or_vote(&nav, name, pin_ref.as_ref(), scope_content, &next_for_pin))\n+ (child_row_pin_or_vote(&nav, name, pin_ref.as_ref(), scope_content))\n @let href = item_href(name.as_str(), &nav);\n a class=\"item-link\" href=(href) { code { (item_display_path(name.as_str())) } }\n }\n@@ -1353,10 +1335,6 @@ async fn vote_compare_inner(\n div id=\"vote-edge-history-region\" {\n (edge_history)\n }\n- div class=\"vote-compare-preview-wrap\" {\n- h3 { \"your vote (after post)\" }\n- div id=\"vote-compare-preview\" class=\"vote-compare-preview\" {}\n- }\n @if can_post {\n form id=\"vote-compare-form\" method=\"POST\" action=\"/ui\" {\n input type=\"hidden\" name=(UI_RPC_FIELD) value=(rpc_json);\ndiff --git a/server/static/theme_retro_craft.css b/server/static/theme_retro_craft.css\nindex 0871e3126c656ba534306cdc3f405f802d10b589..6eb9222184a8795d67a5d09d41de08c8ac1b148f 100644\n--- a/server/static/theme_retro_craft.css\n+++ b/server/static/theme_retro_craft.css\n@@ -638,6 +638,7 @@ body.view-ontology .ont-item-meta {\n flex-wrap: wrap;\n align-items: center;\n gap: 0.35rem;\n+ display: flex;\n }\n body.view-ontology .ont-item-title {\n flex: 1 1 auto;\n","role":"user"}],"model":"~anthropic/claude-sonnet-latest"}