constitution · epochs · watch · epoch 3

comparison

c_16438843de8f (tommy-mor) vs c_a337088f57f0 (tommy-mor)

download prompt · raw event · cmp_06b3513005406c

council reasoning

~anthropic/claude-sonnet-latest · winner B · 65:35 · permalink

Side B delivers a coherent, integrated improvement to the dev workflow (cargo-watch reload, persistent fixture-data with reused seed summary, smarter port picking) that plugs cleanly into existing tooling. Side A adds infra files (Dockerfile, fly.toml, deps.edn) plus event_log.rs/views.rs dropped at the repo root outside any src tree, suggesting orphaned/unintegrated scaffolding rather than a wired-in feature.

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

A adds lasting core runtime and ops pieces (EventLog JSONL append/load, ViewStore with async flush, multi-stage Dockerfile, fly.toml mounts/health checks), which the product and deploy path depend on. B improves local DX (cargo-watch fixture, persistent fixture-data/, prefer-8080) plus a small forum HTML trim—useful iteration polish, but not comparable foundational design.

openai/gpt-chat-latest · winner A · 5:2 · permalink

Side A adds substantial project infrastructure and core functionality: a production Dockerfile and Fly.io deployment config, a reusable EventLog module that appends and reloads JSONL events with error handling and corrupt-line tolerance, and a persistent ViewStore that loads and atomically flushes view counts. Side B mainly improves the local development workflow by making the walkthrough fixture persistent with cargo-watch, preferred ports, and fixture reuse, plus a small UI cleanup, which is useful but has a narrower long-term impact on the project's 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_a337088f57f0 (tommy-mor)

message

[a45842df] better dev iteration script

diff preview

diff --git a/.gitignore b/.gitignore
index 9cd2eaece834481bee87639cd7cb6be77f33e302..b70cf5b97cd3556e061233d8b0aefd333568a991 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,7 @@ target/
 repomix-output.xml
 psalms_kjv.txt
 dev-data/
+fixture-data/
 .worktrees/
 .specstory/
 .specstory/
diff --git a/bb.edn b/bb.edn
index 5d1d642d75d03f70879d7673e7ad95360336a8b1..4680e82d4227057d2eea6041dc5647beb759131d 100644
--- a/bb.edn
+++ b/bb.edn
@@ -47,7 +47,7 @@
                                            "RUST_LOG"      "info"})})))}
 
   fixture
-  {:doc "Run local server + mock OAuth + seeded walkthrough data for manual browser demos"
+  {:doc "Run local server via cargo-watch + mock OAuth + seeded walkthrough data (persistent ./fixture-data/, prefers PORT 8080). Requires cargo-watch."
    :requires ([test.walkthrough-fixture :as walkthrough-fixture])
    :task (walkthrough-fixture/run-fixture)}
 
diff --git a/server/src/html/forum.rs b/server/src/html/forum.rs
index e00e7110e2ad778d63b3d00b0cc4ffe20ba2e0d4..815fc6529062b103553644fc15e516334aec2b88 100644
--- a/server/src/html/forum.rs
+++ b/server/src/html/forum.rs
@@ -976,12 +976,6 @@ pub async fn room_page(
         html! {
             (strip)
             nav class="breadcrumb" { (bc_room(&nav, slug_display, None)) }
-            h2 { (slug_display) }
-            p class="muted" { (room_id) }
-            p class="muted room-links" {
-                "room garden · "
-                a href=(nav.garden_root_url()) { "~" }
-            }
             (room_members_section_markup(&reduced, &room_id, false))
             h3 { "threads" }
             (render_thread_feed(Some(&nav), "room-thread-feed", &rows, now))
diff --git a/test/common.clj b/test/common.clj
index 76a3e99fdb67c10bcb5c2f53d8bcd0917398af50..c6deba0d90995b0e3f9c4b8ad66a516ed19b870e 100644
--- a/test/common.clj
+++ b/test/common.clj
@@ -107,6 +107,16 @@
    (.close ss)
    port))
 
+(defn pick-port-prefer
+  "Use `preferred` if it can be bound, otherwise an ephemeral port (same as `pick-port`)."
+  [preferred]
+  (try
+    (let [ss (java.net.ServerSocket. preferred)]
+      (.close ss)
+      preferred)
+    (catch java.io.IOException _
+      (pick-port))))
+
 (defn wait-for-server
   "Poll /healthz until it returns 'ok', up to `timeout-ms`."
   [base-url timeout-ms]
diff --git a/test/walkthrough_fixture.clj b/test/walkthrough_fixture.clj
index 4b814822029c913e38151ca53848d1c64fb58373..a487ff1ef8548580577914fb33f321586859c9af 100644
--- a/test/walkthrough_fixture.clj
+++ b/test/walkthrough_fixture.clj
@@ -1,11 +1,18 @@
 (ns test.walkthrough-fixture
   "Launch a local slug server with mock OAuth and seed browser-friendly demo data."
   (:require [babashka.fs :as fs]
+            [babashka.process :as p]
             [cheshire.core :as json]
             [clojure.string :as str]
             [test.common :as common]
             [test.oauth :as oauth]))
 
+(def ^:private fixture-data-dir "fixture-data")
+;; Prefer the same port as `bb dev` / `bb watch`; fall back if something else is listening.
+(def ^:private preferred-slug-port 8080)
+;; First `cargo watch` compile can exceed a release-binary startup; allow several minutes.
+(def ^:private health-wait-ms 300000)
+
 (defn- assert! [pred msg]
   (when-not pred
     (throw (ex-info msg {}))))
@@ -85,49 +92,87 @@
             :thread_url (str base-url "/r/" room-short "/" room-slug "/t/walkthrough-thread")
             :garden_url (str base-url "/r/" room-short "/" room-slug "/~/secret/item")}}))
 
+(defn- rebase-fixture-summary [saved current-base-url current-google-url current-data-dir]
+  (let [inner (:summary saved)
+        room (:room inner)
+        rs (:short room)
+        lg (:slug room)]
+    (assoc saved
+           :base_url current-base-url
+           :mock_google_url current-google-url
+           :data_dir (str current-data-dir)
+           :summary (assoc inner
+                           :room (assoc room
+                                         :url (str current-base-url "/r/" rs "/" lg)
+                                         :thread_url (str current-base-url "/r/" rs "/" lg "/t/walkthrough-thread")
+                                         :garden_url (str current-base-url "/r/" rs "/" lg "/~/secret/item"))))))
+
+(defn- fixture-log-present? [data-dir]
+  (let [p (fs/path data-dir "events.jsonl")]
+    (and (fs/exists? p) (pos? (fs/size p)))))
+
+(defn- load-or-seed-summary!
+  [base-url google-url data-dir summary-path]
+  (if (and (fixture-log-present? data-dir) (fs/exists? summary-path))
+    (let [saved (json/parse-string (slurp summary-path) true)
+          rebased (rebase-fixture-summary saved base-url google-url data-dir)]
+      (spit summary-path (json/generate-string rebased {:pretty true}))
+      (println "")
+      (println "reusing fixture-data/ (delete the directory for a fresh seed)")
+      rebased)
+    (let [seeded (seed-demo! base-url)
+          s {:base_url base-url
+             :mock_google_url google-url
+             :data_dir (str data-dir)
+             :summary seeded}]
+      (spit summary-path (json/generate-string s {:pretty true}))
+      s)))
+
 (defn run-fixture [& _args]
-  (let [build (common/run-cargo-build-release! ["slugsocial-server"])
-        _ (assert! (zero? (:exit build)) "cargo build --release failed")
-        server-bin "target/release/slugsocial-server"
-        tmp-dir (str (fs/create-temp-dir {:prefix "slug-walkthrough-"}))
-        slug-port (common/pick-port)
+  (let [data-dir (str (fs/absolutize (fs/path (fs/cwd) fixture-data-dir)))
+        slug-port (common/pick-port-prefer preferred-slug-port)
         google-port (common/pick-port)
         base-url (str "http://127.0.0.1:" slug-port)
         google-url (str "http://127.0.0.1:" google-port)
-        stable-dir "/tmp/slug-walkthrough-fixture"
-        summary-path (str stable-dir "/summary.json")
+        summary-path (str (fs/path data-dir "summary.json"))
         !server (atom nil)
         !google (atom nil)
-        server-env (common/slug-server-env tmp-dir base-url google-url slug-port)]
+        server-env (merge (common/slug-server-env data-dir base-url google-url slug-port)
+                          {"RUST_LOG" "info"})
+        watch-cmd [(common/cargo-bin) "watch"
+                   "-x" "run -p slugsocial-server"
+                   "-w" "server/src"
+                   "-w" "server/static"
+                   "-w" "types/src"]]
     (try
-      (fs/create-dirs stable-dir)
+      (fs/create-dirs data-dir)
       (reset! !google
               (oauth/start-mock-google google-port
                                        :google-users ["google-user-alice" "google-user-bob"]))
-      (reset! !server (common/start-server server-bin server-env))
-      (assert! (common/wait-for-server base-url 10000) "server did not become healthy")
-      (let [seeded (seed-demo! base-url)
-            summary {:base_url base-url
-                     :mock_google_url google-url
-                     :data_dir tmp-dir
-                     :summary seeded}]
-        (spit summary-path (json/generate-string summary {:pretty true}))
+      (println "")
+      (println "starting cargo-watch (first compile may take a while)…")
+      (flush)
+      (reset! !server (p/process watch-cmd {:inherit true :env server-env}))
+      (assert! (common/wait-for-server base-url health-wait-ms) "server did not become healthy")
+      (let [summary (load-or-seed-summary! base-url google-url data-dir summary-path)]
         (println "")
         (println "walkthrough fixture ready")
         (println (str "  base url:      " base-url))
         (println (str "  room page:     " (get-in summary [:summary :room :url])))
         (println (str "  thread page:   " (get-in summary [:summary :room :thread_url])))
         (println (str "  garden page:   " (get-in summary [:summary :room :garden_url])))
+        (println (str "  data dir:      " data-dir))
         (println (str "  summary json:  " summary-path))
         (println "")
         (println "seeded users")
         (println "  alice / bob via mock OAuth")
         (println "")
+        (println "editing server/src or server/static reloads the server; data persists in fixture-data/")
+        (println "")
         (println "press Ctrl-C to stop")
         (flush)
         (while true
           (Thread/sleep 1000)))
       (finally
         (when-some [s @!server] (common/kill-server s))
-        (when-some [g @!google] ((:stop-fn g)))
-        (fs/delete-tree tmp-dir)))))
+        (when-some [g @!google] ((:stop-fn g)))))))

download full diff B

Hardlinks — judgments / attempts / prompt

prompt download

judgments

attempts

Prompt text is loaded only by the download route.