constitution · epochs · watch · epoch 3

comparison

c_16438843de8f (tommy-mor) vs c_b7cc67300477 (tommy-mor)

download prompt · raw event · cmp_4dd0a922cc67ff

council reasoning

~anthropic/claude-sonnet-latest · winner B · 3:2 · permalink

Side B adds a concrete, well-reasoned test verifying that the rank-centrality algorithm recovers the true order from a random spanning tree of votes, directly strengthening confidence in core ranking logic. Side A dumps several new files (Dockerfile, fly.toml, deps.edn, event_log.rs, views.rs) that aren't wired into any existing module (no mod declarations or call sites shown), making it look like speculative scaffolding of uncertain integration value rather than a tested, functional addition.

~x-ai/grok-latest · winner A · 3:1 · permalink

A adds lasting production pieces: EventLog (JSONL append/load with corrupt-line handling), ViewStore (persisted counts with coalesced flushes), plus Dockerfile and fly.toml for deployable runtime. B only adds one ranking property test; valuable as a correctness check, but narrower and non-runtime compared with A’s core modules and ops surface.

openai/gpt-chat-latest · winner A · 4:1 · permalink

Side A adds substantial new project infrastructure and functionality: a persistent JSONL event log with append/load logic and error handling, a view-count store with asynchronous disk flushing, plus Docker and Fly.io deployment configuration. Side B adds a single regression/property-style test that exercises the ranking algorithm on a random spanning tree with perfect ratios, which improves verification but does not change runtime behavior or architecture.

sides

A — c_16438843de8f (tommy-mor)

message

[4cd0d15d] more seed

diff preview

diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..9cb07c60cb0da063f747cfbf1b3b876ecb8ba03e
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,34 @@
+# time 0.3.47+ requires Rust 1.88 (edition 2024)
+FROM rust:1.88-slim as builder
+
+WORKDIR /build
+
+RUN apt-get update && \
+    apt-get install -y pkg-config libssl-dev && \
+    rm -rf /var/lib/apt/lists/*
+
+# Copy source and build. (Keep it simple to avoid remote build cache oddities.)
+COPY . .
+RUN cargo build --release --package slugsocial-server
+
+FROM debian:bookworm-slim
+
+RUN apt-get update && \
+    apt-get install -y ca-certificates && \
+    rm -rf /var/lib/apt/lists/*
+
+WORKDIR /app
+
+COPY --from=builder /build/target/release/slugsocial-server /app/slugsocial-server
+
+# Create data directory for persistent volume
+RUN mkdir -p /data
+
+ENV SLUG_DATA_DIR=/data
+ENV SLUG_EVENT_LOG=/data/events.jsonl
+ENV PORT=8080
+
+EXPOSE 8080
+
+CMD ["/app/slugsocial-server"]
+
diff --git a/deps.edn b/deps.edn
new file mode 100644
index 0000000000000000000000000000000000000000..0bf892d44f491cb2313e01ae8a942c3097c52948
--- /dev/null
+++ b/deps.edn
@@ -0,0 +1,10 @@
+{:paths ["." "test"]
+ :deps {cheshire/cheshire {:mvn/version "5.13.0"}
+        http-kit/http-kit {:mvn/version "2.8.0"}
+        babashka/fs {:mvn/version "0.5.32"}
+        babashka/process {:mvn/version "0.6.25"}
+        com.blockether/spel {:mvn/version "0.7.11"}}
+ :aliases
+ {:kaocha {:extra-deps {lambdaisland/kaocha {:mvn/version "1.91.1392"}
+                        lambdaisland/kaocha-junit-xml {:mvn/version "1.17.101"}}
+          :main-opts ["-m" "kaocha.runner"]}}}
diff --git a/event_log.rs b/event_log.rs
new file mode 100644
index 0000000000000000000000000000000000000000..eaae0d495e43a45d6590603892265a62cc92906e
--- /dev/null
+++ b/event_log.rs
@@ -0,0 +1,83 @@
+use std::path::{Path, PathBuf};
+
+use tokio::{
+    fs::{self, OpenOptions},
+    io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
+};
+
+use crate::events::Event;
+
+#[derive(Debug, thiserror::Error)]
+pub enum EventLogError {
+    #[error("io error: {0}")]
+    Io(#[from] std::io::Error),
+    #[error("json error: {0}")]
+    Json(#[from] serde_json::Error),
+}
+
+#[derive(Debug, Clone)]
+pub struct EventLog {
+    path: PathBuf,
+}
+
+impl EventLog {
+    pub fn new(path: impl Into<PathBuf>) -> Self {
+        Self { path: path.into() }
+    }
+
+    pub fn path(&self) -> &Path {
+        &self.path
+    }
+
+    pub async fn ensure_parent_dir(&self) -> Result<(), EventLogError> {
+        if let Some(parent) = self.path.parent() {
+            fs::create_dir_all(parent).await?;
+        }
+        Ok(())
+    }
+
+    pub async fn append(&self, event: &Event) -> Result<(), EventLogError> {
+        self.ensure_parent_dir().await?;
+        let mut f: tokio::fs::File = OpenOptions::new()
+            .create(true)
+            .append(true)
+            .open(&self.path)
+            .await?;
+
+        let mut line = serde_json::to_string(event)?;
+        line.push('\n');
+        f.write_all(line.as_bytes()).await?;
+        f.flush().await?;
+        Ok(())
+    }
+
+    /// Load events from JSONL. Corrupt lines are skipped and returned as `(line_no, line)`.
+    pub async fn load_all(&self) -> Result<(Vec<Event>, Vec<(usize, String)>), EventLogError> {
+        if !fs::try_exists(&self.path).await? {
+            return Ok((vec![], vec![]));
+        }
+
+        let f = fs::File::open(&self.path).await?;
+        let mut reader = BufReader::new(f).lines();
+
+        let mut events = Vec::new();
+        let mut bad_lines = Vec::new();
+
+        let mut line_no: usize = 0;
+        while let Some(line) = reader.next_line().await? {
+            line_no += 1;
+            let trimmed = line.trim();
+            if trimmed.is_empty() {
+                continue;
+            }
+            match serde_json::from_str::<Event>(trimmed) {
+                Ok(ev) => events.push(ev),
+                Err(_) => bad_lines.push((line_no, line)),
+            }
+        }
+
+        Ok((events, bad_lines))
+    }
+}
+
+
diff --git a/fly.toml b/fly.toml
new file mode 100644
index 0000000000000000000000000000000000000000..bbb9345e527452db1d87a549213645c195eae5fc
--- /dev/null
+++ b/fly.toml
@@ -0,0 +1,42 @@
+app = "slugsocial"
+primary_region = "iad"
+
+[build]
+  dockerfile = "Dockerfile"
+
+[env]
+  SLUG_DATA_DIR = "/data"
+  SLUG_EVENT_LOG = "/data/events.jsonl"
+  PORT = "8080"
+
+[[services]]
+  internal_port = 8080
+  protocol = "tcp"
+
+  [[services.ports]]
+    port = 80
+    handlers = ["http"]
+    force_https = true
+
+  [[services.ports]]
+    port = 443
+    handlers = ["tls", "http"]
+
+  [services.concurrency]
+    type = "connections"
+    hard_limit = 1000
+    soft_limit = 500
+
+  [[services.http_checks]]
+    interval = "10s"
+    timeout = "2s"
+    grace_period = "5s"
+    method = "GET"
+    path = "/healthz"
+    protocol = "http"
+    tls_skip_verify = false
+
+[[mounts]]
+  source = "slugsocial_data"
+  destination = "/data"
+
diff --git a/views.rs b/views.rs
new file mode 100644
index 0000000000000000000000000000000000000000..d4f0ffc49475f014698b4da0de6f476884430813
--- /dev/null
+++ b/views.rs
@@ -0,0 +1,63 @@
+use std::{
+    collections::HashMap,
+    sync::{Arc, Mutex},
+};
+use tokio::sync::mpsc;
+
+type CountMap = Arc<Mutex<HashMap<String, u64>>>;
+
+#[derive(Clone)]
+pub struct ViewStore {
+    counts: CountMap,
+    flush_tx: mpsc::Sender<()>,
+}
+
+impl ViewStore {
+    pub fn new(json_path: &str) -> Self {
+        // Load existing counts from disk on startup (best-effort)
+        let initial: HashMap<String, u64> = std::fs::read_to_string(json_path)
+            .ok()
+            .and_then(|s| serde_json::from_str(&s).ok())
+            .unwrap_or_default();
+
+        let counts: CountMap = Arc::new(Mutex::new(initial));
+        let (flush_tx, mut flush_rx) = mpsc::channel::<()>(64);
+        let path = json_path.to_string();
+
+        let counts_for_writer = counts.clone();
+        tokio::spawn(async move {
+            while flush_rx.recv().await.is_some() {
+                while flush_rx.try_recv().is_ok() {}
+
+                let snapshot: HashMap<String, u64> = {
+                    counts_for_writer.lock().unwrap().clone()
+                };
+
+                let path = path.clone();
+                let _ = tokio::task::spawn_blocking(move || {
+                    if let Ok(json) = serde_json::to_string(&snapshot) {
+                        let tmp = format!("{path}.tmp");
+                        if std::fs::write(&tmp, &json).is_ok() {
+                            let _ = std::fs::rename(&tmp, &path);
+                        }
+                    }
+                })
+                .await;
+            }
+        });
+
+        Self { counts, flush_tx }
+    }
+
+    pub fn increment(&self, path: String) {
+        {
+            let mut map = self.counts.lock().unwrap();
+            *map.entry(path).or_insert(0) += 1;
+        }
+        let _ = self.flush_tx.try_send(());
+    }
+
+    pub fn get_views(&self, path: &str) -> u64 {
+        self.counts.lock().unwrap().get(path).copied().unwrap_or(0)
+    }
+}

download full diff A

B — c_b7cc67300477 (tommy-mor)

message

[9e3d989b] Add test that 25 spanning-tree votes with perfect ratios sort the alphabet.

Demonstrates rank centrality recovers the true order from a random connected
comparison graph when each vote encodes item strength as (idx+1).

Co-authored-by: Cursor <cursoragent@cursor.com>

diff preview

diff --git a/server/src/ranking.rs b/server/src/ranking.rs
index c9b4d5d89d2f22cf53a5b43eb979e19a11bc89a1..93cb4c9f5887a1e598cdc9d648751055f618adcc 100644
--- a/server/src/ranking.rs
+++ b/server/src/ranking.rs
@@ -369,6 +369,48 @@ mod tests {
         assert_eq!(comp1, vec!["c", "d"]);
     }
 
+    /// A random spanning tree over 26 items needs only n−1 = 25 pairwise votes.
+    /// When each vote uses the "perfect" ratio (strength left : strength right =
+    /// (idx_left+1) : (idx_right+1)), rank centrality recovers the true order.
+    /// See `rank-eric.py` (Eric's demo of Negahban–Oh–Shah rank centrality).
+    #[test]
+    fn twenty_five_random_votes_perfect_ratios_sort_alphabet() {
+        use rand::seq::SliceRandom;
+
+        const N: usize = 26;
+        let letters: Vec<char> = (0..N).map(|i| char::from(b'a' + i as u8)).collect();
+
+        let mut rng = rand::thread_rng();
+        let mut perm: Vec<usize> = (0..N).collect();
+        perm.shuffle(&mut rng);
+
+        let mut g = mk_group();
+        for k in 1..N {
+            let i = *perm[..k].choose(&mut rng).unwrap();
+            let j = perm[k];
+            let (a, b) = (letters[i], letters[j]);
+            g.apply_vote(vote(
+                k as i64,
+                &a.to_string(),
+                &b.to_string(),
+                (i + 1) as i32,
+                (j + 1) as i32,
+            ));
+        }
+
+        let ranked = ranked_items(&g);
+        assert_eq!(ranked.len(), N);
+        for (rank, item) in ranked.iter().enumerate() {
+            let expected = char::from(b'a' + (N - 1 - rank) as u8);
+            assert_eq!(
+                item.item.as_str(),
+                expected.to_string(),
+                "rank {rank}: expected '{expected}', got '{}'",
+                item.item.as_str()
+            );
+        }
+    }
+
     #[test]
     fn subset_ranking_ranks_within_component_only() {
         let mut g = mk_group();

download full diff B

Hardlinks — judgments / attempts / prompt

prompt download

judgments

attempts

Prompt text is loaded only by the download route.