constitution · epochs · watch · epoch 3

comparison

c_11ce057e37af (tommy-mor) vs c_e4fb43f04791 (tommy-mor)

download prompt · raw event · cmp_0ff92915656248

council reasoning

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

Side A makes substantive parser and rendering improvements: it introduces deterministic typed block masking, a prose item-reference tokenizer that correctly handles raw URLs, punctuation, newlines, and code fences, enforces braced DSL item bodies instead of ambiguous fenced bodies, fixes external breadcrumb root detection, and updates linkification plus extensive tests. Side B is primarily a UI/layout cleanup and CSS styling change (removing a wrapper element and improving ranking-number presentation), with little impact on core functionality or long-term correctness.

openai/gpt-5.3-chat · winner A · 9:1 · permalink

A introduces substantive parsing and correctness improvements: deterministic block tokens replacing RNG, typed block tracking, a new prose tokenizer that avoids code fences and trims punctuation, and stricter validation requiring braced item bodies—plus integrating this into linkification. B mainly removes a wrapper div and tweaks CSS/layout, with no comparable impact on core logic or correctness.

openai/gpt-5.2-chat · winner A · 5:1 · permalink

Side A introduces a typed, deterministic BlockMasker with BlockKind tracking, adds a prose tokenizer that avoids linkifying inside code fences, enforces braced DSL item bodies with explicit parse errors, and rewrites HTML linkification to use the tokenizer—backed by extensive new tests. Side B mainly removes a wrapping section in vote_compare and tweaks CSS for ranking lists, which is largely presentational and far less architecturally significant.

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

Side A implements a substantive, tested feature: a typed block-masking system, deterministic tokenization, and a proper prose tokenizer that correctly linkifies item refs while respecting code fences and line boundaries, backed by many new unit tests and bugfixes (e.g., requiring braced item bodies). Side B is a mix of minor CSS tweaks (ranking-list marker styling) and a cosmetic removal of a wrapping HTML section/indentation change with no functional test coverage or clear lasting design improvement.

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

A adds lasting core behavior: deterministic typed block masking, prose item-ref tokenization (tilde/dash/raw URLs, newline/punctuation boundaries, code-fence exclusion), braced body enforcement, linkify/resolver updates, and tests. B only unwraps a vote-compare shell div and tweaks ranking-list number CSS—cosmetic UI polish with little structural impact.

sides

A — c_11ce057e37af (tommy-mor)

message

[08565bea] Tokenize prose refs for garden URL links (#147)

* Tokenize prose refs for garden URL links

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

* Stop prose URLs at line boundaries

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

* Require braced DSL item bodies

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

---------

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

diff preview

diff --git a/server/src/dsl.rs b/server/src/dsl.rs
index 4203c2f59dd8825d7a91513c4023f3ccd37f1efc..b3c785674560a0b42a7f4c3aa13d80cd350f485b 100644
--- a/server/src/dsl.rs
+++ b/server/src/dsl.rs
@@ -1,7 +1,5 @@
 use std::collections::HashMap;
 
-use rand::Rng;
-
 /// Parsed DSL document.
 #[derive(Debug, Clone, PartialEq, Eq)]
 pub struct Document {
@@ -39,31 +37,57 @@ pub enum DslError {
 /// Matches the legacy Python parser behavior:
 /// - Supports toggle markers (open == close), e.g. ```...```
 /// - Supports nested markers (open != close), e.g. { ... { ... } ... }
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum BlockKind {
+    CodeFence,
+    DoubleBrace,
+    Brace,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+struct MaskedBlock {
+    kind: BlockKind,
+}
+
 #[derive(Debug, Default, Clone)]
 pub struct BlockMasker {
     pub replacements: HashMap<String, String>,
+    blocks: HashMap<String, MaskedBlock>,
+    next_id: u32,
 }
 
 impl BlockMasker {
     pub fn new() -> Self {
         Self {
             replacements: HashMap::new(),
+            blocks: HashMap::new(),
+            next_id: 0,
         }
     }
 
-    fn new_token(&mut self) -> String {
-        let mut rng = rand::thread_rng();
-        let n: u32 = rng.gen();
-        let token = format!("__BLOCK_{:08x}__", n);
-        // Extremely unlikely collision; if it happens, regenerate.
-        if self.replacements.contains_key(&token) {
-            return self.new_token();
+    fn new_token(&mut self, haystack: &str) -> String {
+        loop {
+            let token = format!("__BLOCK_{:08x}__", self.next_id);
+            self.next_id = self.next_id.wrapping_add(1);
+            if !self.replacements.contains_key(&token) && !haystack.contains(&token) {
+                return token;
+            }
         }
-        token
     }
 
     /// Replace outermost balanced blocks with tokens.
     pub fn mask(&mut self, text: &str, open_marker: &str, close_marker: &str) -> String {
+        self.mask_kind(text, open_marker, close_marker, BlockKind::Brace)
+    }
+
+    /// Replace outermost balanced blocks with typed deterministic tokens.
+    pub fn mask_kind(
+        &mut self,
+        text: &str,
+        open_marker: &str,
+        close_marker: &str,
+        kind: BlockKind,
+    ) -> String {
         if text.is_empty() {
             return text.to_string();
         }
@@ -97,9 +121,10 @@ impl BlockMasker {
                     // Found end of outermost block
                     let s = start_idx.max(0) as usize;
                     let original_block = &text[s..i];
-                    let token = self.new_token();
+                    let token = self.new_token(text);
                     self.replacements
                         .insert(token.clone(), original_block.to_string());
+                    self.blocks.insert(token.clone(), MaskedBlock { kind });
                     result_parts.push(token);
                     current_idx = i;
                 }
@@ -176,13 +201,22 @@ impl BlockMasker {
         }
         token.to_string()
     }
+
+    pub fn block_kind(&self, token: &str) -> Option<BlockKind> {
+        self.blocks.get(token).map(|b| b.kind)
+    }
 }
 
 fn mask_all(mut masker: BlockMasker, text: &str) -> (BlockMasker, String) {
     // Mask hierarchy: Code -> Double Brace -> Single Brace.
-    let t = masker.mask(text, "```", "```");
-    let t = masker.mask(&t, "{{", "}}");
-    let t = masker.mask(&t, "{", "}");
+    let t = masker.mask_kind(text, "```", "```", BlockKind::CodeFence);
+    let t = masker.mask_kind(&t, "{{", "}}", BlockKind::DoubleBrace);
+    let t = masker.mask_kind(&t, "{", "}", BlockKind::Brace);
+    (masker, t)
+}
+
+fn mask_code_fences(mut masker: BlockMasker, text: &str) -> (BlockMasker, String) {
+    let t = masker.mask_kind(text, "```", "```", BlockKind::CodeFence);
     (masker, t)
 }
 
@@ -253,7 +287,34 @@ fn skip_ws(s: &str, mut i: usize) -> usize {
     i
 }
 
-fn parse_item_name_at(s: &str, i: usize) -> Option<(String, usize)> {
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum ProseToken {
+    Text(String),
+    ItemRef(String),
+}
+
+fn trim_prose_item_ref_end(s: &str, mut end: usize) -> usize {
+    while end > 0 {
+        let Some((idx, c)) = s[..end].char_indices().next_back() else {
+            break;
+        };
+        if matches!(
+            c,
+            '.' | ',' | ';' | ':' | '!' | '?' | ')' | ']' | '}' | '"' | '\''
+        ) {
+            end = idx;
+        } else {
+            break;
+        }
+    }
+    end
+}
+
+fn parse_item_name_at_with_mode(
+    s: &str,
+    i: usize,
+    trim_trailing_punctuation: bool,
+) -> Option<(String, usize)> {
     let bytes = s.as_bytes();
     if i >= bytes.len() {
         return None;
@@ -263,6 +324,9 @@ fn parse_item_name_at(s: &str, i: usize) -> Option<(String, usize)> {
     if s[i..].starts_with("https://") || s[i..].starts_with("http://") {
         let mut j = i;
         while j < bytes.len() {
+            if trim_trailing_punctuation && bytes[j] == b'\n' {
+                break;
+            }
             if bytes[j..].starts_with(b"__BLOCK_") || is_ws_byte(bytes[j]) {
                 break;
             }
@@ -271,6 +335,12 @@ fn parse_item_name_at(s: &str, i: usize) -> Option<(String, usize)> {
         if j <= i {
             return None;
         }
+        if trim_trailing_punctuation {
+            j = trim_prose_item_ref_end(s, j);
+            if j <= i {
+                return None;
+            }
+        }
         return Some((s[i..j].to_string(), j));
     }
 
@@ -296,6 +366,12 @@ fn parse_item_name_at(s: &str, i: usize) -> Option<(String, usize)> {
         if j <= i + 2 {
             return None;
         }
+        if trim_trailing_punctuation {
+            j = trim_prose_item_ref_end(s, j);
+            if j <= i + 2 {
+                return None;
+            }
+        }
         let raw = &s[i..j];
         if !is_item_name(raw) {
             return None;
@@ -336,6 +412,46 @@ fn parse_item_name_at(s: &str, i: usize) -> Option<(String, usize)> {
     Some((format!("~/{}", name), j))
 }
 
+fn parse_item_name_at(s: &str, i: usize) -> Option<(String, usize)> {
+    parse_item_name_at_with_mode(s, i, false)
+}
+
+pub fn parse_prose_item_ref_at(s: &str, i: usize) -> Option<(String, usize)> {
+    parse_item_name_at_with_mode(s, i, true)
+}
+
+pub fn tokenize_prose_item_refs(text: &str) -> Vec<ProseToken> {
+    if text.is_empty() {
+        return Vec::new();
+    }
+    let (masker, masked) = mask_code_fences(BlockMasker::new(), text);
+    let mut tokens = Vec::new();
+    let mut text_start = 0usize;
+    let mut i = 0usize;
+
+    while i < masked.len() {
+        if let Some((raw, end)) = parse_prose_item_ref_at(&masked, i) {
+            if text_start < i {
+                tokens.push(ProseToken::Text(masker.unmask(&masked[text_start..i])));
+            }
+            tokens.push(ProseToken::ItemRef(masker.unmask(&raw)));
+            i = end;
+            text_start = i;
+            continue;
+        }
+
+        let Some((_, c)) = masked[i..].char_indices().next() else {
+            break;
+        };
+        i += c.len_utf8();
+    }
+
+    if text_start < masked.len() {
+        tokens.push(ProseToken::Text(masker.unmask(&masked[text_start..])));
+    }
+    tokens
+}
+
 fn parse_block_token_at(s: &str, i: usize) -> Option<(String, usize)> {
     let bytes = s.as_bytes();
     if i >= bytes.len() {
@@ -401,6 +517,12 @@ fn parse_block_prefixed_statement(
     tail: &str,
     masker: &BlockMasker,
 ) -> Result<Stmt, DslError> {
+    if masker.block_kind(block_token) == Some(BlockKind::CodeFence) {
+        return Err(DslError::Parse(
+            "vote explanations must use `{ ... }`; code fences belong inside body blocks"
+                .to_string(),
+        ));
+    }
     // vote: block item_ref comparison item_ref
     let s = tail.trim_start();
     if s.is_empty() {
@@ -462,6 +584,11 @@ fn parse_item_definition_statement(stripped: &str, masker: &BlockMasker) -> Resu
     }
 
     if let Some((tok, end)) = parse_block_token_at(stripped, i) {
+        if masker.block_kind(&tok) == Some(BlockKind::CodeFence) {
+            return Err(DslError::Parse(
+                "item bodies must use `{ ... }`; code fences belong inside body blocks".to_string(),
+            ));
+        }
         let body = masker.extract_body(&tok);
         let tail = stripped[end..].trim();
         if !tail.is_empty() {
@@ -572,6 +699,10 @@ pub fn parse_full(text: &str) -> Result<Document, DslError> {
 
             if let Some((tok, end)) = parse_block_token_at(stripped, 0) {
                 if stripped[end..].trim().is_empty() {
+                    if masker.block_kind(&tok) == Some(BlockKind::CodeFence) {
+                        prose_buffer.push(line);
+                        continue;
+                    }
                     pending_block = Some(tok);
                     continue;
                 }
@@ -619,6 +750,70 @@ mod tests {
         assert_eq!(roundtrip, input);
     }
 
+    #[test]
+    fn blockmasker_tokens_are_deterministic_and_typed() {
+        let input = "x ```code``` y {body}";
+        let (masker, masked) = mask_all(BlockMasker::new(), input);
+        assert!(masked.contains("__BLOCK_00000000__"));
+        assert!(masked.contains("__BLOCK_00000001__"));
+        assert_eq!(
+            masker.block_kind("__BLOCK_00000000__"),
+            Some(BlockKind::CodeFence)
+        );
+        assert_eq!(
+            masker.block_kind("__BLOCK_00000001__"),
+            Some(BlockKind::Brace)
+        );
+        assert_eq!(masker.unmask(&masked), input);
+    }
+
+    #[test]
+    fn prose_tokenizer_finds_tilde_dash_and_raw_url_refs() {
+        let tokens =
+            tokenize_prose_item_refs("see ~/a/b then -/example.com/x and https://Example.com/A/B.");
+        assert_eq!(
+            tokens,
+            vec![
+                ProseToken::Text("see ".to_string()),
+                ProseToken::ItemRef("~/a/b".to_string()),
+                ProseToken::Text(" then ".to_string()),
+                ProseToken::ItemRef("-/example.com/x".to_string()),
+                ProseToken::Text(" and ".to_string()),
+                ProseToken::ItemRef("https://Example.com/A/B".to_string()),
+                ProseToken::Text(".".to_string()),
+            ]
+        );
+    }
+
+    #[test]
+    fn prose_tokenizer_stops_raw_urls_at_newlines() {
+        let tokens = tokenize_prose_item_refs("https://example.com/a/b.\n-/example.com/a/b");
+        assert_eq!(
+            tokens,
+            vec![
+                ProseToken::ItemRef("https://example.com/a/b".to_string()),
+                ProseToken::Text(".\n".to_string()),
+                ProseToken::ItemRef("-/example.com/a/b".to_string()),
+            ]
+        );
+    }
+
+    #[test]
+    fn prose_tokenizer_does_not_linkify_inside_code_fences() {
+        let tokens = tokenize_prose_item_refs(
+            "before ```json\n{\"url\":\"https://example.com\"}\n``` after ~/x",
+        );
+        assert_eq!(
+            tokens,
+            vec![
+                ProseToken::Text(
+                    "before ```json\n{\"url\":\"https://example.com\"}\n``` after ".to_string()
+                ),
+                ProseToken::ItemRef("~/x".to_string()),
+            ]
+        );
+    }
+
     #[test]
     fn parse_item_with_body_strips_outer_braces() {
         let input = "~/rust { Systems language }";
@@ -633,8 +828,8 @@ mod tests {
     }
 
     #[test]
-    fn parse_item_with_fenced_json_body_preserves_braces() {
-        let input = "~/item/in/url ```json\n{\"test\": true}\n```";
+    fn parse_item_with_braced_fenced_json_body_preserves_braces() {
+        let input = "~/item/in/url {\n```json\n{\"test\": true}\n```\n}";
         let doc = parse_full(input).unwrap();
         assert_eq!(
             doc.statements,
@@ -645,6 +840,51 @@ mod tests {
     

… preview truncated; 18,752 characters omitted

download full diff A

B — c_e4fb43f04791 (tommy-mor)

message

[09842c93] remove shell

diff preview

diff --git a/server/src/html/garden.rs b/server/src/html/garden.rs
index 8fc6be1de8dd975f9547de615809222236be4b70..76ba96e45c9e16291a6ccd8096fdd01b274c1560 100644
--- a/server/src/html/garden.rs
+++ b/server/src/html/garden.rs
@@ -1321,55 +1321,53 @@ async fn vote_compare_inner(
     .expect("vote compare rpc json");
 
     let body = html! {
-        section class="vote-compare-shell" {
-            h2 { "compare" }
-            div class="vote-compare-pair" {
-                a class="vote-compare-item" href=(nav.garden_item_href(&left)) {
-                    code { (item_display_path(left.as_str())) }
-                }
-                span class="vote-compare-vs" { "vs" }
-                a class="vote-compare-item" href=(nav.garden_item_href(&right)) {
-                    code { (item_display_path(right.as_str())) }
-                }
-            }
-            div id="vote-edge-history-region" {
-                (edge_history)
-            }
-            @if can_post {
-                form id="vote-compare-form" method="POST" action="/ui" {
-                    input type="hidden" name=(UI_RPC_FIELD) value=(rpc_json);
-                    div class="vote-thread-picker" {
-                        label class="vote-thread-picker-label" { "thread" }
-                        select id="vote-thread-select" name="thread_tag" aria-label="Thread to post vote into" {
-                            @if thread_tags.is_empty() {
-                                option value="vote" selected { "#vote" }
-                            }
-                            @for t in &thread_tags {
-                                @if *t == auto_thread {
-                                    option value=(t) selected { "#" (t) }
-                                } @else {
-                                    option value=(t) { "#" (t) }
-                                }
-                            }
-                        }
+    h2 { "compare" }
+    div class="vote-compare-pair" {
+        a class="vote-compare-item" href=(nav.garden_item_href(&left)) {
+            code { (item_display_path(left.as_str())) }
+        }
+        span class="vote-compare-vs" { "vs" }
+        a class="vote-compare-item" href=(nav.garden_item_href(&right)) {
+            code { (item_display_path(right.as_str())) }
+        }
+    }
+    div id="vote-edge-history-region" {
+        (edge_history)
+    }
+    @if can_post {
+        form id="vote-compare-form" method="POST" action="/ui" {
+            input type="hidden" name=(UI_RPC_FIELD) value=(rpc_json);
+            div class="vote-thread-picker" {
+                label class="vote-thread-picker-label" { "thread" }
+                select id="vote-thread-select" name="thread_tag" aria-label="Thread to post vote into" {
+                    @if thread_tags.is_empty() {
+                        option value="vote" selected { "#vote" }
                     }
-                    input type="hidden" name="ratio_left" id="vote-ratio-left" value="50";
-                    input type="hidden" name="ratio_right" id="vote-ratio-right" value="50";
-                    label class="vote-compare-slider-label" {
-                        span id="vote-slider-left-label" { (item_display_path(left.as_str())) }
-                        input type="range" id="vote-preference-slider" min="0" max="100" value="50"
-                            aria-valuemin="0" aria-valuemax="100";
-                        span id="vote-slider-right-label" { (item_display_path(right.as_str())) }
+                    @for t in &thread_tags {
+                        @if *t == auto_thread {
+                            option value=(t) selected { "#" (t) }
+                        } @else {
+                            option value=(t) { "#" (t) }
+                        }
                     }
-                    label class="vote-explain-label" { "reason (required)" }
-                    textarea name="explanation" id="vote-explain" rows="5" placeholder="why this split?" required {}
-                    div id="vote-compare-errors" {}
-                    p { button type="submit" { "post vote" } }
                 }
-            } @else {
-                p class="muted" { a href="/login" { "log in" } " to post this vote." }
             }
+            input type="hidden" name="ratio_left" id="vote-ratio-left" value="50";
+            input type="hidden" name="ratio_right" id="vote-ratio-right" value="50";
+            label class="vote-compare-slider-label" {
+                span id="vote-slider-left-label" { (item_display_path(left.as_str())) }
+                input type="range" id="vote-preference-slider" min="0" max="100" value="50"
+                    aria-valuemin="0" aria-valuemax="100";
+                span id="vote-slider-right-label" { (item_display_path(right.as_str())) }
+            }
+            label class="vote-explain-label" { "reason (required)" }
+            textarea name="explanation" id="vote-explain" rows="5" placeholder="why this split?" required {}
+            div id="vote-compare-errors" {}
+            p { button type="submit" { "post vote" } }
         }
+    } @else {
+        p class="muted" { a href="/login" { "log in" } " to post this vote." }
+    }
     };
 
     let page = layout_full_bleed_chromeless(
diff --git a/server/static/theme_default.css b/server/static/theme_default.css
index a441c4f79d8cf88f5a8240f9992f47dbbd1ab46b..fdcde86c718eb53cce8273e45a9844c2d8041f88 100644
--- a/server/static/theme_default.css
+++ b/server/static/theme_default.css
@@ -1270,8 +1270,12 @@ body.view-ontology-dark .ont-ranking-list li {
 body.view-ontology-dark .ont-ranking-list li::before {
   color: var(--meta);
   content: counter(ont-rank) ".";
-  font-size: 11px;
-  min-width: 18px;
+  flex-shrink: 0;
+  font-size: 1.35rem;
+  font-weight: 700;
+  font-variant-numeric: tabular-nums;
+  line-height: 1;
+  min-width: 2.25ch;
   text-align: right;
 }
 body.view-ontology-dark .ont-rank-score {
@@ -1424,8 +1428,12 @@ body.view-ontology-light .ont-ranking-list li {
 body.view-ontology-light .ont-ranking-list li::before {
   color: var(--meta);
   content: counter(ont-rank) ".";
-  font-size: 11px;
-  min-width: 18px;
+  flex-shrink: 0;
+  font-size: 1.35rem;
+  font-weight: 700;
+  font-variant-numeric: tabular-nums;
+  line-height: 1;
+  min-width: 2.25ch;
   text-align: right;
 }
 body.view-ontology-light .ont-rank-score {
diff --git a/server/static/theme_retro.css b/server/static/theme_retro.css
index 373f15bbd75b62604cdd14b9f0fadda2d6176991..61e1448b2f66a075c0e33325d6980448712fc927 100644
--- a/server/static/theme_retro.css
+++ b/server/static/theme_retro.css
@@ -135,6 +135,30 @@ body.view-ontology nav.breadcrumb a:hover {
 body.view-ontology nav.breadcrumb a.bc-current { color: #111; font-weight: 600; }
 body.view-ontology nav.breadcrumb .bc-sep { color: #888; padding: 0 2px; }
 
+body.view-ontology ol.ont-ranking-list {
+  counter-reset: ont-rank;
+  list-style: none;
+  margin: 0.5rem 0;
+  padding: 0;
+}
+body.view-ontology ol.ont-ranking-list li {
+  align-items: baseline;
+  counter-increment: ont-rank;
+  display: flex;
+  gap: 0.35rem;
+}
+body.view-ontology ol.ont-ranking-list li::before {
+  flex-shrink: 0;
+  color: #666;
+  content: counter(ont-rank) ".";
+  font-size: 1.35rem;
+  font-weight: 700;
+  font-variant-numeric: tabular-nums;
+  line-height: 1;
+  min-width: 2.25ch;
+  text-align: right;
+}
+
 nav.breadcrumb.ont-sibling-nav {
   margin-top: 0;
   width: 100%;
diff --git a/server/static/theme_retro_craft.css b/server/static/theme_retro_craft.css
index 6eb9222184a8795d67a5d09d41de08c8ac1b148f..7da102040484c887833158a37c307d078205c701 100644
--- a/server/static/theme_retro_craft.css
+++ b/server/static/theme_retro_craft.css
@@ -742,12 +742,32 @@ body.view-ontology button.ont-garden-pin-ico:focus-visible {
   outline-offset: 2px;
 }
 
+body.view-ontology ol.ont-ranking-list {
+  counter-reset: ont-rank;
+  list-style: none;
+  margin: 0;
+  padding: 0;
+}
 body.view-ontology ol.ont-ranking-list li,
 body.view-ontology ul.ont-group-list li {
   display: flex;
   align-items: baseline;
   gap: 0.35rem;
 }
+body.view-ontology ol.ont-ranking-list li {
+  counter-increment: ont-rank;
+}
+body.view-ontology ol.ont-ranking-list li::before {
+  flex-shrink: 0;
+  color: #5c574e;
+  content: counter(ont-rank) ".";
+  font-size: 1.35rem;
+  font-weight: 700;
+  font-variant-numeric: tabular-nums;
+  line-height: 1;
+  min-width: 2.25ch;
+  text-align: right;
+}
 body.view-ontology ol.ont-ranking-list li .item-link,
 body.view-ontology ul.ont-group-list li .item-link {
   flex: 1;

download full diff B

Hardlinks — judgments / attempts / prompt

prompt download

judgments

attempts

Prompt text is loaded only by the download route.