constitution · epochs · watch · epoch 3

comparison

c_f6d0fed9bf9a (tommy-mor) vs c_a337088f57f0 (tommy-mor)

download prompt · raw event · cmp_2f25835de8af61

council reasoning

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

Side B is a small, coherent, working change: it improves the dev fixture script to use cargo-watch with a persistent data dir and preferred port, and removes redundant UI markup, all integrated into the actual codebase. Side A is a disorganized dump of stray untracked files (many pasted with shell prompts like 'tommy@Tommys-Laptop...cat file' baked into the diff), placed at repo root rather than proper module paths, indicating this is not a real integrated commit but leftover scratch content of dubious lasting value.

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

Side A lands core lasting domain machinery (rank-centrality with the #146 star-topology fix and tests, event reducer for scopes/rooms/ingests/votes, HTML form $form templates + ui_action parsing, vote-compare page, and a large tested URL parser graph), while B only polishes the fixture DX (cargo-watch, persistent fixture-data/, preferred port 8080) plus a small room page markup trim.

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

Side A introduces substantial project functionality: core ranking and reducer logic, a parser with extensive tests, UI action/form templating, voting pages, browser plumbing, and supporting test scripts, all of which define lasting behavior. Although it contains some accidental pasted terminal output and planning notes, it establishes major reusable infrastructure, whereas Side B mainly improves the developer workflow by making the fixture environment persistent, using cargo-watch, preferring port 8080, and making small UI/test utility adjustments.

sides

A — c_f6d0fed9bf9a (tommy-mor)

message

[1d9d8ade] init seed

diff preview

diff --git a/TEST.sh b/TEST.sh
new file mode 100755
index 0000000000000000000000000000000000000000..266b71f29259f5c2cad2617476ebe2ebc9596d39
--- /dev/null
+++ b/TEST.sh
@@ -0,0 +1,4 @@
+#!/usr/bin/env bash
+set -euo pipefail
+cargo test --all
+./scripts/clj-test.sh
diff --git a/bundle.js b/bundle.js
new file mode 100644
index 0000000000000000000000000000000000000000..8d316f9a57cc7c379fe4bd8e42e092c7eac5d265
--- /dev/null
+++ b/bundle.js
@@ -0,0 +1,52 @@
+/**
+ * Slug web UI: only plumbing — fetch/eval/SSE. No product UI logic here.
+ */
+(function () {
+  function evalJs(js) {
+    if (js && String(js).trim()) {
+      eval(js);
+    }
+  }
+
+  // Theme cookie sync (runs before paint; full reload if localStorage disagrees with cookie)
+
+  function initSlugUi() {
+    // POST forms → eval response (except theme + full-navigation forms)
+    document.addEventListener('submit', async function (e) {
+      var f = e.target;
+      if (!f || f.tagName !== 'FORM') return;
+      if ((f.method || 'get').toLowerCase() !== 'post') return;
+      if (f.id === 'slug-theme-form') return;
+      if (f.getAttribute('data-navigate') === 'full') return;
+      e.preventDefault();
+      var resp = await fetch(f.action, {
+        method: 'POST',
+        body: new URLSearchParams(new FormData(f)),
+        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
+        credentials: 'same-origin',
+      });
+      evalJs(await resp.text());
+    });
+
+    // SSE: server-pushed JS
+    function connectSSE() {
+      var ssePath = window.location.pathname + window.location.search;
+      var es = new EventSource('/sse?path=' + encodeURIComponent(ssePath));
+      es.onmessage = function (e) {
+        evalJs(e.data);
+      };
+      es.onerror = function () {
+        es.close();
+        setTimeout(connectSSE, 3000);
+      };
+    }
+    connectSSE();
+  }
+
+  if (document.readyState === 'loading') {
+    document.addEventListener('DOMContentLoaded', initSlugUi);
+  } else {
+    initSlugUi();
+  }
+})();
+
diff --git a/clj-test.sh b/clj-test.sh
new file mode 100755
index 0000000000000000000000000000000000000000..b62a49b98ed60a65a46807b7ad80fa3142f14952
--- /dev/null
+++ b/clj-test.sh
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+set -euo pipefail
+cd "$(dirname "$0")/.."
+mkdir -p target
+exec clojure -M:kaocha
diff --git a/forms.rs b/forms.rs
new file mode 100644
index 0000000000000000000000000000000000000000..d509fd889fde3fed2662ce0a39006b7a51ae762a
--- /dev/null
+++ b/forms.rs
@@ -0,0 +1,145 @@
+tommy@Tommys-Laptop:~/programming/slug-star/slug|main ⇒  cat server/src/form_template.rs
+//! Plan2-style JSON templates with `{"$form": "field_name"}` holes, filled from
+//! `application/x-www-form-urlencoded` (or any `String` → `String` map) **before**
+//! deserializing into a typed struct.
+//!
+//! # Wire format
+//!
+//! Templates are **compact JSON** (`serde_json::to_string`): one line, no pretty
+//! printing, strings escaped per JSON rules (`\"`, `\n`, etc.). Embed that string
+//! in HTML attributes or text nodes with normal HTML escaping (e.g. maud), not
+//! bespoke encodings.
+//!
+//! # Power vs flat hidden fields
+//!
+//! A form is always a string→string map. You can fake depth with dotted keys (`a.b.c`),
+//! but one structured blob (`__rpc__` = compact JSON) gives you nested objects,
+//! arrays, and optional fields without inventing a new naming scheme each time.
+//!
+//! # Security
+//!
+//! Substitution runs **before** `serde` into your command type. It does not fix
+//! authorization: if the client can replace the hidden `__rpc__` value, they can
+//! change the command shape unless you validate (signed blob, server-side session
+//! context, or treat the blob as hints only). Same threat model as any hidden field.
+
+use serde::Serialize;
+use serde_json::Value;
+use std::collections::HashMap;
+
+/// Serialize a value to compact JSON for a hidden `__rpc__` (or similar) field.
+pub fn template_json_compact<T: Serialize>(v: &T) -> serde_json::Result<String> {
+    serde_json::to_string(v)
+}
+
+/// Recursively walk the JSON AST and replace `{"$form": "key"}` with the submitted
+/// string for `key` (empty if missing). Other keys are unchanged.
+pub fn substitute_form_vars(val: &mut Value, form_data: &HashMap<String, String>) {
+    match val {
+        Value::Object(map) => {
+            if map.len() == 1 {
+                if let Some(Value::String(field_name)) = map.get("$form") {
+                    let submitted = form_data
+                        .get(field_name.as_str())
+                        .map(|s| s.as_str())
+                        .unwrap_or("");
+                    *val = Value::String(submitted.to_string());
+                    return;
+                }
+            }
+            for v in map.values_mut() {
+                substitute_form_vars(v, form_data);
+            }
+        }
+        Value::Array(arr) => {
+            for v in arr.iter_mut() {
+                substitute_form_vars(v, form_data);
+            }
+        }
+        _ => {}
+    }
+}
+
+/// Parse JSON, apply [`substitute_form_vars`], return the mutated value.
+pub fn fill_template_from_form(
+    template_json: &str,
+    form_data: &HashMap<String, String>,
+) -> Result<Value, serde_json::Error> {
+    let mut v: Value = serde_json::from_str(template_json)?;
+    substitute_form_vars(&mut v, form_data);
+    Ok(v)
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use serde::Deserialize;
+
+    #[derive(Debug, Deserialize, PartialEq, Eq)]
+    struct Demo {
+        room: String,
+        thread_tag: String,
+        nested: Nested,
+    }
+
+    #[derive(Debug, Deserialize, PartialEq, Eq)]
+    struct Nested {
+        text: String,
+    }
+
+    #[test]
+    fn holes_become_strings() {
+        let json = r#"{
+            "room": "public",
+            "thread_tag": {"$form": "tag"},
+            "nested": {"text": {"$form": "body"}}
+        }"#;
+        let mut form = HashMap::new();
+        form.insert("tag".into(), "foo".into());
+        form.insert("body".into(), "hello\nworld".into());
+
+        let v = fill_template_from_form(json, &form).unwrap();
+        let d: Demo = serde_json::from_value(v).unwrap();
+        assert_eq!(
+            d,
+            Demo {
+                room: "public".into(),
+                thread_tag: "foo".into(),
+                nested: Nested {
+                    text: "hello\nworld".into(),
+                },
+            }
+        );
+    }
+
+    #[test]
+    fn missing_form_key_is_empty_string() {
+        let json = r#"{"x": {"$form": "nope"}}"#;
+        let mut form = HashMap::new();
+        form.insert("other".into(), "y".into());
+        let v = fill_template_from_form(json, &form).unwrap();
+        assert_eq!(v["x"], "");
+    }
+
+    #[test]
+    fn array_of_holes() {
+        let json = r#"{"items": [{"$form": "a"}, {"$form": "b"}]}"#;
+        let mut form = HashMap::new();
+        form.insert("a".into(), "1".into());
+        form.insert("b".into(), "2".into());
+        let v = fill_template_from_form(json, &form).unwrap();
+        assert_eq!(v["items"], serde_json::json!(["1", "2"]));
+    }
+
+    #[test]
+    fn template_json_compact_escapes_and_single_line() {
+        let s = template_json_compact(&serde_json::json!({
+            "x": "quote\"and\nnewline"
+        }))
+        .unwrap();
+        assert!(!s.contains('\n'));
+        assert!(s.contains("\\\"") || s.contains("\\n"));
+    }
+}
+tommy@Tommys-Laptop:~/programming/slug-star/slug|main ⇒  
+
diff --git a/gameifying.tdsl b/gameifying.tdsl
new file mode 100644
index 0000000000000000000000000000000000000000..93d2a61d94fcba4370c82d9612e6bb0f0318cc15
--- /dev/null
+++ b/gameifying.tdsl
@@ -0,0 +1,2 @@
+consider gating certain views (top all time?) by a 10 day usage streak.. or something like that. 
+or like a path, on homepage(?), that shows day 1: r/amitheasshole, day2: r/aww, day3: gaming, or something like that.  progressive revelation + usage incentive.
diff --git a/pagerank_streaming.tdsl b/pagerank_streaming.tdsl
new file mode 100644
index 0000000000000000000000000000000000000000..0f2231b94936c17610adf653ca26b0faa6f2ac3a
--- /dev/null
+++ b/pagerank_streaming.tdsl
@@ -0,0 +1,11 @@
+eventually i want to have ranking histories.
+like "visionary" and "fraud" waxing and waning in a uplot graph over time for #elon-musk. 
+there are too many query combinations to precomupte the ranking histories 
+(arbitrary user filters, and tag overlap combinations maybe), 
+so we're just going to have to calculate them all on demand. 
+computers are fast, its okay. for each vote, we need to calculate rank centrality again. 
+i was thinking, for n votes we calculate the rank centrality, 
+and get node weights. we then output that data to the client (over websocket, or testable barrier). 
+then we calculate n+1 votes, _but we keep the node weights in memory_ 
+so the rank centrality process converges faster. 
+this also has the side effect of making the ranking stream in satisfyingly as you load the page.
diff --git a/parser.rs b/parser.rs
new file mode 100644
index 0000000000000000000000000000000000000000..2b87b974f8d1dd93bee35681d87668a94e4ef349
--- /dev/null
+++ b/parser.rs
@@ -0,0 +1,1808 @@
+use std::collections::HashMap;
+use std::rc::Rc;
+use std::cell::RefCell;
+use crate::ui::action::UIAction;
+use crate::ui::types::{Suggestion, GuideOption, ScrollingSuggestion};
+
+// --- Core Abstractions ---
+
+/// Unique identifier for nodes in the graph
+type NodeId = &'static str;
+
+/// Pattern matching for edges
+#[derive(Debug, Clone)]
+pub enum EdgePattern {
+    /// Matches exact literal string
+    Literal(&'static str),
+    
+    /// Matches any prefix of a string and suggests the full string
+    /// e.g., PrefixOf("reddit.com") matches "r", "re", "red", "reddit", "reddit.com"
+    PrefixOf(&'static str),
+    
+    /// Captures a variable segment (e.g., subreddit name, username)
+    Variable(&'static str),
+    
+    /// Matches any string (wildcard)
+    Any,
+}
+
+impl EdgePattern {
+    /// Try to match this pattern against input, return (consumed_chars, captured_value)
+    fn matches(&self, input: &str) -> Option<(usize, Option<String>)> {
+        match self {
+            EdgePattern::Literal(lit) => {
+                if input.starts_with(lit) {
+                    Some((lit.len(), None))
+                } else {
+                    None
+                }
+            }
+            EdgePattern::PrefixOf(target) => {
+                // Check if input is a prefix of target
+                if target.starts_with(input) && !input.is_empty() {
+                    // It's a valid prefix
+                    Some((input.len(), None))
+                } else if input.starts_with(target) {
+                    // Full match
+                    Some((target.len(), None))
+                } else {
+                    None
+                }
+            }
+            EdgePattern::Variable(var_name) => {
+                // Consume until next '/' or end of string
+                let end = input.find('/').unwrap_or(input.len());
+                if end > 0 {
+                    let captured = input[..end].to_string();
+                    // Validate based on variable type
+                    if is_valid_variable(var_name, &captured) {
+                        Some((end, Some(captured)))
+                    } else {
+                        None
+                    }
+                } else {
+                    None
+                }
+            }
+            EdgePattern::Any => {
+                // Match everything until next '/' or end
+                let end = input.find('/').unwrap_or(input.len());
+                if end > 0 {
+                    Some((end, Some(input[..end].to_string())))
+                } else {
+                    None
+                }
+            }
+        }
+    }
+    
+    /// G

… preview truncated; 148,978 characters omitted

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.