constitution · epochs · watch · epoch 3

comparison

c_2f5d9e0370f8 (tommy-mor) vs c_b6adf338f405 (tommy-mor)

download prompt · raw event · cmp_622b85edaca20e

council reasoning

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

Side A implements a substantive DSL redesign (title-first items, explanation-first votes) with real parser logic changes, updated documentation, and extensive test/fixture updates across Rust and Clojure suites, representing meaningful lasting functionality. Side B merely deletes an unused file with no functional change, which is minor housekeeping (though safe removal of dead code has some value, it is far less significant than A's feature work).

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

A redesigns the core sorter DSL (explanation-first votes, stricter item body placement) with real parser changes in dsl.rs plus docs/UI/fixture updates, which reshape how users write and how the system parses forever. B only deletes an unused timeline.rs module—helpful cleanup, but no lasting product or design contribution.

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

Side A makes a substantial functional change by redesigning the DSL syntax for items and votes, updating the parser to support block-prefixed vote explanations, enforcing the new grammar with explicit errors, adapting UI generation, and revising documentation and extensive tests/fixtures. Side B only deletes an apparently unused timeline module without introducing new behavior or demonstrating a corresponding architectural replacement, so its lasting project impact is much smaller.

sides

A — c_2f5d9e0370f8 (tommy-mor)

message

[6bda2635] Use title-first items and explanation-first votes (#135)

* Require block-first sorter DSL statements

Co-authored-by: tommy <thmorriss@gmail.com>

* Use title-first items with explanation-first votes

Co-authored-by: tommy <thmorriss@gmail.com>

* Update garden vote test DSL fixtures

Co-authored-by: tommy <thmorriss@gmail.com>

* Update browser vote DSL payloads

Co-authored-by: tommy <thmorriss@gmail.com>

---------

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

diff preview

diff --git a/cli/DSL.txt b/cli/DSL.txt
index 18f69ef25f01583fddf9fc90e077bb2c3fa72bb6..c9b12bf0edaf73ad252f0a202b7fe61e311df50a 100644
--- a/cli/DSL.txt
+++ b/cli/DSL.txt
@@ -18,9 +18,20 @@ Blank lines are preserved to maintain paragraph structure.
 ~/item/a {itembody}
 ~/python { A high-level scripting language }
 
-~/item/a > ~/python { Item A is better because of X. }
-~/python 3:1 ~/go { Python's ecosystem is much richer than Go's. }
-https://example.com/lang = ~/go { They are equally good in this context. }
+{
+Item A is better because of X.
+}
+~/item/a > ~/python
+
+{
+Python's ecosystem is much richer than Go's.
+}
+~/python 3:1 ~/go
+
+{
+They are equally good in this context.
+}
+https://example.com/lang = ~/go
 ```
 
 SYNTAX RULES
@@ -35,7 +46,7 @@ Starts a thread. Tag allows alphanumeric, `-`, `_`, and `/`. Subtitle max 100 ch
 ~/<local-item-path> { description }
 ```
 Defines an ontology item (garden layer). Paths can be nested (e.g. `~/languages/python`).
-A leading `/` alone is **not** allowed in the DSL — use `~/` only. Descriptions (bodies) are wrapped in `{}`. Can be adjacent (e.g. `~/arrived{ready}`).
+A leading `/` alone is **not** allowed in the DSL — use `~/` only. Descriptions (bodies) are wrapped in `{}` and follow the item path.
 
 ```sorter
 https://example.com/item { description }
@@ -46,7 +57,10 @@ Canonicalization rules for URLs:
 - `~/` and `https://slug.social/~/` map to the same local item path.
 
 ```sorter
-<item1> <comparison> <item2> { required explanation }
+{
+required explanation
+}
+<item1> <comparison> <item2>
 ```
 Compares two items. The explanation is REQUIRED.
 Comparisons:
@@ -65,7 +79,8 @@ When writing bodies or explanations, you can use braces `{}` and code blocks wit
 3. Single braces: { ... }
 
 ```sorter
-~/code { Here is a block: ```def foo(): return {"a": 1}``` }
+{ Here is a block: ```def foo(): return {"a": 1}``` }
+~/code
 ```
 
 STYLE
diff --git a/cli/GUIDE.sorter b/cli/GUIDE.sorter
index 7ea1c2649cb4a323b46f0bf389025079a9c9ab43..86accba72ecea547215d947fb6552e0ead25687c 100644
--- a/cli/GUIDE.sorter
+++ b/cli/GUIDE.sorter
@@ -83,7 +83,8 @@ Item definitions (attaches a description to an item):
   ~/thread/item { description }
 
 Comparisons:
-  ~/thread/item-a 3:1 ~/thread/item-b { reasoning }
+  { reasoning }
+  ~/thread/item-a 3:1 ~/thread/item-b
 
 Ratio formats:
   3:1   left is 3x better than right
@@ -95,8 +96,7 @@ Shorthand:
   <     means 1:2 (right is better)
   =     means 1:1 (equal)
 
-Bodies can attach without whitespace:
-  ~/thread/item{Description here}
+Item bodies follow the item path. Vote explanations come first; the comparison is the verdict line.
 }
 
 You can write any prose in your posts. These won't be part of the garden but only the thread.
@@ -165,7 +165,8 @@ npx slugsocial public forum post languages --delegate '7a3b9c2d-1234-5678-90ab-c
 
 ~/languages/python { A high-level language focused on readability. }
 ~/languages/rust { A systems language focused on safety and performance. }
-~/languages/python 2:1 ~/languages/rust { Python has simpler syntax for beginners - fewer symbols, explicit over implicit.  Rust's borrow checker adds cognitive load even for simple programs.  Both are readable once learned, but Python's learning curve is gentler.  }
+{ Python has simpler syntax for beginners - fewer symbols, explicit over implicit.  Rust's borrow checker adds cognitive load even for simple programs.  Both are readable once learned, but Python's learning curve is gentler.  }
+~/languages/python 2:1 ~/languages/rust
 EOF
 
 # See current ranking
diff --git a/ideas/single-thread.md b/ideas/single-thread.md
index 86230fe6119c31045c21dbfcb12311b323c02fa8..0efb7199524cc3b308b1922c03e3a99193e4586c 100644
--- a/ideas/single-thread.md
+++ b/ideas/single-thread.md
@@ -15,7 +15,8 @@ Previously a `.sorter` document could scatter `#tags` throughout:
 ~/languages/rust {A systems language.}
 #tools
 ~/tools/cargo {Rust's build system.}
-~/languages/rust 2:1 ~/tools/cargo {Rust is more foundational than its tooling.}
+{Rust is more foundational than its tooling.}
+~/languages/rust 2:1 ~/tools/cargo
 ```
 
 The system would fan the ingest into both `#languages` and `#tools` — the same
diff --git a/server/src/api/mod.rs b/server/src/api/mod.rs
index 9f3c1cc21c2ae157c024a447980a97e190c8f066..920e967b47852ea82fa61b84c457ae3582dd9800 100644
--- a/server/src/api/mod.rs
+++ b/server/src/api/mod.rs
@@ -72,16 +72,16 @@ mod tests {
         apply_ingest(
             &mut reduced,
             1,
-            "~/t/a {a}\n~/t/b {b}\n~/t/a 2:1 ~/t/b {because}\n",
+            "~/t/a {a}\n~/t/b {b}\n{because}\n~/t/a 2:1 ~/t/b\n",
         );
-        let text = "~/t/a 1:1 ~/t/b {equal}\n";
+        let text = "{equal}\n~/t/a 1:1 ~/t/b\n";
         validate_ingest_document(&reduced, text, &crate::reducer::ScopeId::Public).unwrap();
     }
 
     #[test]
     fn validate_ingest_document_rejects_vote_on_undefined_item() {
         let reduced = ReducerState::default();
-        let text = "~/t/a {x}\n~/t/b 1:1 ~/t/missing {why}\n";
+        let text = "~/t/a {x}\n{why}\n~/t/b 1:1 ~/t/missing\n";
         let err = validate_ingest_document(&reduced, text, &crate::reducer::ScopeId::Public).unwrap_err();
         assert_eq!(err.0, StatusCode::BAD_REQUEST);
         assert!(err.1.contains("undefined item"));
diff --git a/server/src/api/ui_html.rs b/server/src/api/ui_html.rs
index 696e7b3605e2e68aee0351116c494c2958506add..7cbda3876451687aa7a55547fc06bbe86ac9d260 100644
--- a/server/src/api/ui_html.rs
+++ b/server/src/api/ui_html.rs
@@ -222,13 +222,13 @@ async fn dispatch_ui_action(
             }
 
             let text = format!(
-                "@{}\n{} {}:{} {} {{\n{}\n}}\n",
+                "@{}\n{{\n{}\n}}\n{} {}:{} {}\n",
                 crate::api::auth::WEB_BROWSER_AGENT,
+                exp,
                 left_id.as_str(),
                 rl,
                 rr,
-                right_id.as_str(),
-                exp
+                right_id.as_str()
             );
 
             match rpc_post_with_bearer(state, &session.bearer, room.clone(), thread_tag.clone(), text).await {
diff --git a/server/src/dsl.rs b/server/src/dsl.rs
index 7962342bd023b3b5061a684d84d4ab164134b111..def8b497c71567016a522648b9630d7038163e41 100644
--- a/server/src/dsl.rs
+++ b/server/src/dsl.rs
@@ -11,16 +11,21 @@ pub struct Document {
 /// A single statement in the DSL (or prose when using `parse_full`).
 #[derive(Debug, Clone, PartialEq, Eq)]
 pub enum Stmt {
-    Item { title: String, body: Option<String> },
+    Item {
+        title: String,
+        body: Option<String>,
+    },
     Vote {
         item1: String,
         item2: String,
         ratio_left: i32,
         ratio_right: i32,
-        /// Required non-empty explanation (from trailing `{ ... }`).
+        /// Required non-empty explanation (from leading `{ ... }`).
         explanation: String,
     },
-    Prose { text: String },
+    Prose {
+        text: String,
+    },
 }
 
 #[derive(Debug, thiserror::Error)]
@@ -391,71 +396,46 @@ fn parse_comparison_at(s: &str, i: usize) -> Option<((i32, i32), usize)> {
     Some(((left, right), j))
 }
 
-fn parse_item_statement(stripped: &str, masker: &BlockMasker) -> Result<Stmt, DslError> {
-    // item: ("~/" | "https://..." | "http://...") item_ref body?
-    // vote: same for both operands.
-    //
-    // Important: body token can be adjacent to the item name (no whitespace),
-    // e.g. "~/arrived{...}" -> "~/arrived__BLOCK_x__".
-    let s = stripped;
-    let bytes = s.as_bytes();
-    if bytes.is_empty() {
-        return Err(DslError::Parse("missing item statement".to_string()));
+fn parse_block_prefixed_statement(
+    block_token: &str,
+    tail: &str,
+    masker: &BlockMasker,
+) -> Result<Stmt, DslError> {
+    // vote: block item_ref comparison item_ref
+    let s = tail.trim_start();
+    if s.is_empty() {
+        return Err(DslError::Parse(
+            "missing vote statement after leading explanation block".to_string(),
+        ));
     }
 
     let (item1, j) =
         parse_item_name_at(s, 0).ok_or_else(|| DslError::Parse("invalid item name".to_string()))?;
+    let explanation = masker.extract_body(block_token);
+    let mut i = skip_ws(s, j);
 
-    // Either we have:
-    // - immediate/whitespace block token => Item
-    // - comparison => Vote
-    // - whitespace then block token => Item
-    // - whitespace then comparison => Vote
-    let i = skip_ws(s, j);
-
-    // If next is end or a block token => Item.
     if i >= s.len() {
-        return Ok(Stmt::Item {
-            title: item1,
-            body: None,
-        });
-    }
-    if let Some((tok, end)) = parse_block_token_at(s, i) {
-        let body = masker.extract_body(&tok);
-        let tail = s[end..].trim();
-        if !tail.is_empty() {
-            return Err(DslError::Parse("extra tokens after item".to_string()));
-        }
-        return Ok(Stmt::Item {
-            title: item1,
-            body: Some(body),
-        });
+        return Err(DslError::Parse(
+            "leading `{ ... }` blocks are vote explanations; item bodies belong after item paths"
+                .to_string(),
+        ));
     }
 
-    // Otherwise parse comparison then "/item2" then REQUIRED body.
-    let ((ratio_left, ratio_right), mut k) = parse_comparison_at(s, i)
+    let ((ratio_left, ratio_right), k) = parse_comparison_at(s, i)
         .ok_or_else(|| DslError::Parse(format!("invalid comparison near: {}", &s[i..])))?;
     if ratio_left == 0 && ratio_right == 0 {
         return Err(DslError::Parse(
             "vote ratio 0:0 is invalid; use 1:1 for a tie or omit the vote".to_string(),
         ));
     }
-    k = skip_ws(s, k);
-    let (item2, mut m) = parse_item_name_at(s, k)
+    i = skip_ws(s, k);
+    let (item2, m) = parse_item_name_at(s, i)
         .ok_or_else(|| DslError::Parse("invalid rhs item name".to_string()))?;
-    m = skip_ws(s, m);
-
-    let Some((tok, end)) = parse_block_token_at(s, m) else {
-        return Err(DslError::Parse(
-            "missing vote explanation (add a trailing `{ ... }`)".to_string(),
-        ));
-    };
-    let explanation = masker.extract_body(&tok);
+    i = skip_ws(s, m);
     if explanation.trim().is_empty() {
         return Err(DslError::Parse("empty vote explanation".to_string()));
     }
-    m = end;
-    let tail = s[m..].trim();
+    let tail = s[i..].trim();
     if !tail.is_empty() {
         return Err(DslError::Parse("extra tokens after vote".to_string()));
     }
@@ -469,6 +449,35 @@ fn parse_item_statement(stripped: &str, masker: &BlockMasker) -> Result<Stmt, Ds
     })
 }
 
+fn parse_item_definition_statement(stripped: &str, masker: &BlockMasker) -> Result<Stmt, DslError> {
+    let (item1, j) =
+        parse_item_name_at(stripped, 0).ok_or_else(|| DslError::Parse("invalid item name".to_string()))?;
+    let i = skip_ws(stripped, j);
+
+    if i >= stripped.len() {
+        return Ok(Stmt::Item {
+            title: item1,
+            body: None,
+        });
+    }
+
+    if let Some((tok, end)) = parse_block_token_at(stripped, i) {
+        let body = masker.extract_body(&tok);
+        let tail = stripped[end..].trim();
+        if !tail.is_empty() {
+            return Err(DslError::Parse("extra tokens after item".to_string()));
+        }
+        return Ok(Stmt::Item {
+            title: item1,
+            body: Some(body),
+        });
+    }
+
+    Err(DslError::Parse(
+        "vote explanations must start with a `{ ... }` block before the comparison".to_string(),
+    ))
+}
+
 fn parse_line(masked_line: &str, masker: &BlockMasker) -> Result<Vec<Stmt>, DslError> {
     let stripped = masked_line.trim_start();
     if stripped.is_empty() {
@@ -477,27 +486,28 @@ fn parse_line(masked_line: &str, masker: &BlockMasker) -> Result<Vec<Stmt>, DslE
     let first = stripped.chars().next().unwrap();
     match first {
         '#' => Err(DslError::Parse("not a DS

… preview truncated; 47,409 characters omitted

download full diff A

B — c_b6adf338f405 (tommy-mor)

message

[6c7377b6] deleted vestigal file

diff preview

diff --git a/server/src/timeline.rs b/server/src/timeline.rs
deleted file mode 100644
index 265ca4ae9946ecd29a7d1d4ec791de8465ce658e..0000000000000000000000000000000000000000
--- a/server/src/timeline.rs
+++ /dev/null
@@ -1,145 +0,0 @@
-//! Room admin lines merged into forum thread views.
-
-use crate::{
-    canonical_path::canonicalize_tag,
-    reducer::{ReducerState, RoomTimelineEntry, RoomTimelineKind},
-};
-
-fn cap_label(c: crate::events::ThreadCapability) -> &'static str {
-    use crate::events::ThreadCapability::*;
-    match c {
-        View => "view",
-        Post => "post",
-        Vote => "vote",
-        AddItem => "add_item",
-        Manage => "manage",
-    }
-}
-
-fn caps_list(caps: &[crate::events::ThreadCapability]) -> String {
-    let mut v: Vec<_> = caps.iter().map(|c| cap_label(*c)).collect();
-    v.sort();
-    v.join(", ")
-}
-
-/// Human-readable system line for the thread feed.
-pub fn format_room_timeline_entry(e: &RoomTimelineEntry) -> String {
-    match &e.kind {
-        RoomTimelineKind::RoomCreated { owner, slug } => {
-            format!("@{owner} created room #{slug}")
-        }
-        RoomTimelineKind::GrantAdded {
-            username,
-            granted_by,
-            capabilities,
-        } => {
-            format!(
-                "@{granted_by} granted @{} {}",
-                username,
-                caps_list(capabilities)
-            )
-        }
-        RoomTimelineKind::GrantRevoked {
-            username,
-            revoked_by,
-            capabilities,
-        } => {
-            format!(
-                "@{revoked_by} revoked @{} {}",
-                username,
-                caps_list(capabilities)
-            )
-        }
-    }
-}
-
-#[derive(Clone, Debug)]
-pub enum MergedThreadRow {
-    System { ts: i64, text: String },
-    Post {
-        index: usize,
-        id: String,
-        ts: i64,
-        principal: String,
-        raw: String,
-    },
-}
-
-/// Merge room admin lines with thread ingests for one room + tag. Oldest first.
-/// `actor_prefix` filters posts only (system lines always included).
-pub fn merge_thread_rows(
-    reduced: &ReducerState,
-    room_wire: &str,
-    thread_tag: &str,
-    since: Option<i64>,
-    before: Option<i64>,
-    actor_prefix: &str,
-) -> Vec<MergedThreadRow> {
-    let scope = crate::reducer::scope_from_room_wire(room_wire);
-    let tag = canonicalize_tag(thread_tag);
-    let key = (scope.clone(), tag.clone());
-
-    let mut rows: Vec<MergedThreadRow> = Vec::new();
-
-    if let Some(entries) = reduced.room_timeline.get(room_wire.trim()) {
-        for e in entries {
-            if since.map_or(true, |s| e.ts >= s) && before.map_or(true, |b| e.ts < b) {
-                rows.push(MergedThreadRow::System {
-                    ts: e.ts,
-                    text: format_room_timeline_entry(e),
-                });
-            }
-        }
-    }
-
-    let all_ids: Vec<String> = reduced
-        .ingests_by_scope_thread
-        .get(&key)
-        .map(|q| q.iter().rev().cloned().collect())
-        .unwrap_or_default();
-
-    for (idx, id) in all_ids.into_iter().enumerate() {
-        let Some(ing) = reduced.ingests_by_id.get(&id) else {
-            continue;
-        };
-        if since.map_or(true, |s| ing.ts >= s) && before.map_or(true, |b| ing.ts < b) {
-            if !actor_prefix.is_empty()
-                && !ing
-                    .principal
-                    .to_lowercase()
-                    .starts_with(actor_prefix)
-            {
-                continue;
-            }
-            rows.push(MergedThreadRow::Post {
-                index: idx,
-                id: ing.id.clone(),
-                ts: ing.ts,
-                principal: ing.principal.clone(),
-                raw: ing.raw.clone(),
-            });
-        }
-    }
-
-    rows.sort_by(|a, b| {
-        let ta = match a {
-            MergedThreadRow::System { ts, .. } | MergedThreadRow::Post { ts, .. } => *ts,
-        };
-        let tb = match b {
-            MergedThreadRow::System { ts, .. } | MergedThreadRow::Post { ts, .. } => *ts,
-        };
-        ta.cmp(&tb)
-    });
-    rows
-}
-
-/// Shared-site thread view (`room_wire == "public"` on the wire). Same merge as private rooms; private-room timeline is unused here.
-pub fn merge_public_thread_rows(
-    reduced: &ReducerState,
-    thread_tag: &str,
-    since: Option<i64>,
-    before: Option<i64>,
-    actor_prefix: &str,
-) -> Vec<MergedThreadRow> {
-    merge_thread_rows(reduced, "public", thread_tag, since, before, actor_prefix)
-}

download full diff B

Hardlinks — judgments / attempts / prompt

prompt download

judgments

attempts

Prompt text is loaded only by the download route.