constitution · epochs · watch · epoch 5

comparison

c_61fee0e88770 (tommy-mor) vs c_b77cc5639ab1 (tommy-mor)

download prompt · raw event · cmp_41efcdc4a69676

council reasoning

No judgments yet.

sides

A — c_61fee0e88770 (tommy-mor)

message

[0455c2d4] Vote heat index below landing deal

diff preview

diff --git a/agents.md b/agents.md
index 59de8bb1c22a89c8ae0cbc473ca0bfe7a80d3e48..64413377b8faecf1a58d266796ee7acfb0ce4812 100644
--- a/agents.md
+++ b/agents.md
@@ -54,7 +54,7 @@ Strict **CSP** that blocks `eval` would break the current app. Other projects ma
 
 - **Retired question pages:** `/q/:collection`, `/q/:collection/:aspect`, and the room-scoped twins are gone entirely — no routes serve them, so they 404 (no question index on `/` either). Pairwise judging lives at `/vote?pool=` / `/vote?left=&right=`; aspect groups still exist in the DSL/CLI and render on garden scope pages.
 
-- **`/vote` landing + aspects:** bare `GET /vote` (no pair) is the judgment entry point — it deals the most-compared open question first (`pick_landing_question` in `server/src/html/garden/vote.rs`: highest voted-pair count across canonical scopes and voted aspect groups, ties prefer more members then canonical), with a 3-step intro naming the scope (and `:aspect`), or an empty state when everything is judged. Popularity compounds deliberately: dealing a busy scope's stragglers grows it further. `/vote?pool=&aspect=` votes into that aspect group (hidden `aspect` input over the existing `$form` hole; invalid slugs 400). Garden aspect sections link their vote pool the same way.
+- **`/vote` landing + aspects:** bare `GET /vote` (no pair) is the judgment entry point — it deals the most-compared open question first (`pick_landing_question` in `server/src/html/garden/vote.rs`: highest voted-pair count across canonical scopes and voted aspect groups, ties prefer more members then canonical), with a 3-step intro naming the scope (and `:aspect`), or an empty state when everything is judged. Popularity compounds deliberately: dealing a busy scope's stragglers grows it further. Below the dealt pair, a heat index lists more open questions hottest first (`rank_open_questions`: newest vote wins, then newest thread post, so judging heat beats talking heat; the dealt question is excluded; capped at 10) — each row deals straight into its pool, with judged counts and activity instead of scores so it never reads as rank order. `/vote?pool=&aspect=` votes into that aspect group (hidden `aspect` input over the existing `$form` hole; invalid slugs 400). Garden aspect sections link their vote pool the same way.
 
 - **`ThreadGraduate` / `GraduateThread`:** Private-room forum threads with **Manage** can be published to the public site under the same tag. The writer replays non-redacted ingests into **`room: public`** (chronological order), then appends a durable **`ThreadGraduated`** marker. Graduated private threads show a banner linking to public **`/t/:tag`**, block further private posts, and cannot be graduated twice. CLI: **`npx slugsocial private <room> forum graduate <tag>`**; RPC: **`ThreadGraduate`**.
 
diff --git a/server/src/html/garden/tests.rs b/server/src/html/garden/tests.rs
index ff9875222f8954e2f80d4ce62100176dcdf08d60..c1c8c5e6517bb20eaae31760b7d111d13966b726 100644
--- a/server/src/html/garden/tests.rs
+++ b/server/src/html/garden/tests.rs
@@ -10,8 +10,9 @@ use super::{
     render::aspect_ranking_sections_markup,
     vote::{
         canonical_edge_items, edge_vote_count_for_pair, edge_vote_entries_for_pair,
-        pick_landing_question, ratios_for_compare_page, sort_votes_for_compare_display,
-        suggest_next_vote_pair, vote_compare_item_card, vote_pool_href,
+        pick_landing_question, rank_open_questions, ratios_for_compare_page,
+        sort_votes_for_compare_display, suggest_next_vote_pair, vote_compare_item_card,
+        vote_pool_href,
     },
 };
 use crate::{
@@ -907,6 +908,57 @@ fn pick_landing_question_returns_none_when_all_judged() {
     assert!(pick_landing_question(content).is_none());
 }
 
+/// Heat index: judging heat beats talking heat — a scope with an old vote
+/// outranks a scope with a fresh thread post but no votes, and newer votes
+/// outrank older ones.
+#[test]
+fn rank_open_questions_prefers_votes_over_thread_chatter() {
+    use crate::events::Ingest as TestIngest;
+    let mut reduced = ReducerState::default();
+    let post = |ts: i64, id: &str, raw: &str, thread_tag: &str| Event::Ingest(TestIngest {
+        ts,
+        id: id.to_string(),
+        raw: raw.to_string(),
+        principal: "testuser".to_string(),
+        delegate: None,
+        room_id: "public".to_string(),
+        thread_tag: thread_tag.to_string(),
+    });
+    // Voted long ago (still open: 1 of 3 pairs), never discussed since.
+    reduced.apply_event(post(
+        1,
+        "v1",
+        "~/voted/a { a }\n~/voted/b { b }\n~/voted/c { c }\n",
+        "voted",
+    ));
+    reduced.apply_event(post(
+        2,
+        "v2",
+        "{ old }\n~/voted/a 2:1 ~/voted/b\n",
+        "voted",
+    ));
+    // Never voted, talked about yesterday.
+    reduced.apply_event(post(10, "t1", "~/talked/x { ex }\n~/talked/y { why }\n", "talked"));
+    reduced.apply_event(post(50, "t2", "lively debate", "talked"));
+    let content = content_for_garden_view(&reduced, &ScopeId::Public);
+    let rows = rank_open_questions(content, &reduced.forum_threads, &ScopeId::Public);
+    assert_eq!(rows.len(), 2);
+    assert!(
+        rows[0].question.scope.as_str().ends_with("voted"),
+        "judged scope first, got {}",
+        rows[0].question.scope.as_str()
+    );
+    assert!(rows[0].thread_ts > 0);
+    assert_eq!(rows[0].question.last_vote_ts, 2);
+    assert!(
+        rows[1].question.scope.as_str().ends_with("talked"),
+        "talked-about scope second, got {}",
+        rows[1].question.scope.as_str()
+    );
+    assert_eq!(rows[1].question.last_vote_ts, 0);
+    assert_eq!(rows[1].thread_ts, 50);
+}
+
 /// Popularity beats neediness: a busy scope with stragglers outranks a fresh
 /// scope nobody has touched. Winners keep winning.
 #[test]
diff --git a/server/src/html/garden/vote.rs b/server/src/html/garden/vote.rs
index 116a4cc51bcea769bb5458ce166951483dc41758..f0ccb080ff2d941e4ce07f57ac520bab84c12003 100644
--- a/server/src/html/garden/vote.rs
+++ b/server/src/html/garden/vote.rs
@@ -14,13 +14,14 @@ use crate::{
     canonical_path::canonicalize_tag,
     form_template::template_json_compact,
     html::{
-        format_ratio, forum::ThreadNav, layout_full_bleed_chromeless, ratio_pct,
+        format_ratio, forum::ThreadNav, layout_full_bleed_chromeless, now_ms, ratio_pct,
         render_item_body_in_scope, theme_from_jar, theme_next_from_uri, ui_action::UI_RPC_FIELD,
         user_can_post_room, JsBuilder,
     },
     middleware::canonical_view_url,
     path_types::ItemId,
-    reducer::{ContentState, GroupState, ScopeId},
+    reducer::{ContentState, ForumThreadState, ScopeId},
+    timeago,
     scope_rank::{comparable_items, suggest_next_pair_in_pool},
     state::AppState,
 };
@@ -579,6 +580,7 @@ pub(super) struct LandingQuestion {
     pub voted: usize,
     pub possible: usize,
     pub members: usize,
+    pub last_vote_ts: i64,
 }
 
 /// Density of voted pairs among `members` inside one vote graph.
@@ -604,76 +606,127 @@ fn voted_density(
     (voted, possible)
 }
 
-pub(super) fn pick_landing_question(content: &ContentState) -> Option<LandingQuestion> {
-    // Most voted pairs wins; ties prefer more members, then canonical over an
-    // aspect, then lex order (iteration is lex-sorted and only strictly-better
-    // candidates replace the incumbent).
-    let mut best: Option<LandingQuestion> = None;
+/// Every open question (canonical + aspect groups with ≥1 unvoted pair),
+/// unsorted. Powers both the landing deal and the heat index below it.
+fn open_questions(content: &ContentState) -> Vec<LandingQuestion> {
+    let mut out = Vec::new();
     let mut scopes: Vec<&ItemId> = content
         .members_by_scope
         .keys()
         .filter(|id| matches!(id.tilde_tail(), Some(t) if !t.is_empty() && !t.contains('/')))
         .collect();
     scopes.sort_by(|a, b| a.as_str().cmp(b.as_str()));
-    // (scope, aspect-group-or-canonical, aspect-slug-or-None), canonical first
-    // per scope so ties keep the canonical question.
-    let mut candidates: Vec<(&ItemId, Option<(&GroupState, String)>)> = Vec::new();
-    for scope in &scopes {
-        candidates.push((scope, None));
-    }
     let mut aspect_keys: Vec<&(ItemId, String)> = content.aspect_groups.keys().collect();
     aspect_keys.sort();
-    for (scope, slug) in aspect_keys {
-        if scopes.iter().any(|s| s.as_str() == scope.as_str()) {
-            candidates.push((
-                scopes
-                    .iter()
-                    .find(|s| s.as_str() == scope.as_str())
-                    .expect("scope present"),
-                Some((
-                    content
-                        .aspect_groups
-                        .get(&(scope.clone(), slug.clone()))
-                        .expect("aspect group present"),
-                    slug.clone(),
-                )),
-            ));
-        }
-    }
-    for (scope, group_opt) in candidates {
+    for scope in &scopes {
         let members = comparable_items(content, content.members_of(scope));
         if members.len() < 2 {
             continue;
         }
-        let (group_idx, group_pairs) = match &group_opt {
-            None => (
-                &content.ranking_group.item_to_idx,
-                &content.ranking_group.voted_pairs,
-            ),
-            Some((group, _)) => (&group.item_to_idx, &group.voted_pairs),
-        };
-        let (voted, possible) = voted_density(group_idx, group_pairs, &members);
-        if voted >= possible {
-            continue;
+        let (voted, possible) = voted_density(
+            &content.ranking_group.item_to_idx,
+            &content.ranking_group.voted_pairs,
+            &members,
+        );
+        if voted < possible {
+            out.push(LandingQuestion {
+                scope: (*scope).clone(),
+                aspect: None,
+                voted,
+                possible,
+                members: members.len(),
+                last_vote_ts: last_canonical_vote_ts(content, &members),
+            });
+        }
+        for (ascope, slug) in aspect_keys
+            .iter()
+            .filter(|(s, _)| s.as_str() == scope.as_str())
+        {
+            let Some(group) = content.aspect_groups.get(&(ascope.clone(), slug.clone())) else {
+                continue;
+            };
+            let (voted, possible) =
+                voted_density(&group.item_to_idx, &group.voted_pairs, &members);
+            if voted < possible {
+                out.push(LandingQuestion {
+                    scope: (*scope).clone(),
+                    aspect: Some(slug.clone()),
+                    voted,
+                    possible,
+                    members: members.len(),
+                    last_vote_ts: group.recent_votes.front().map(|v| v.ts).unwrap_or(0),
+                });
+            }
         }
-        // Highest voted-pair count first; ties prefer more members (then the
-        // lex-first candidate, since iteration is sorted and only
-        // strictly-better replaces the incumbent).
+    }
+    out
+}
+
+/// Newest canonical vote touching two members (0 when none). The global
+/// recent-votes deque is newest-first, so the first in-electorate hit wins.
+fn last_canonical_vote_ts(content: &ContentState, members: &[ItemId]) -> i64 {
+    let set: std::collections::HashSet<&ItemId> = members.iter().collect();
+    content
+        .ranking_group
+        .recent_votes
+        .iter()
+        .find(|v| set.contains(&v.a) && set.contains(&v.b))
+        .map(|v| v.ts)
+        .unwrap_or(0)
+}
+
+/// One row of the heat index: an open question plus its thread heartbeat.
+pub(super) struct OpenRow {
+    pub question: LandingQuestion,
+    pub thread_ts: i64,
+}
+
+const OPEN_INDEX_CAP: usize = 10;
+
+/// Open questions hottest first: newest vote wins, then newest thread post.
+/// Votes always outrank mere di

… preview truncated; 16,373 characters omitted

download full diff A

B — c_b77cc5639ab1 (tommy-mor)

message

[237a0ec0] Home is a question index; forum index moves to /t; /q/ pages are self-contained.

GET / lists public questions (scopes with 2+ comparable members, plus aspect
rows) as expandable rows embedding the shared compare panel; votes morph in
place via VoteComparePost with per-row DOM suffixes. GET /t is the old forum
home (SSE prefixes follow). /q/:leaf pages gain aspects, compact standings,
and collection members via shared question_body sections.

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

diff preview

diff --git a/agents.md b/agents.md
index 2b55bfb0cf2486186f98db7fd30c3708e73c2226..79b8633a72c0b6ca8f7361129fb471566179bf57 100644
--- a/agents.md
+++ b/agents.md
@@ -18,7 +18,8 @@ The web app is **not** a SPA with a JSON API for every interaction. Many mutatio
 
 - **End-to-end tests** that only assert HTTP status bodies miss DOM updates. Morph paths are covered by **Playwright / Spel** tests under `test/browser_*.clj` and `clojure -M -m test.runner …` (see `scripts/clj-test.sh`).
 - Shareable URLs are normal GET routes (e.g. `/t/:tag`, thread post views). **Expand/collapse** and similar controls are **actions**, not bookmarkable GET endpoints; they use the same `POST /ui` + `__rpc__` pattern where applicable.
-- The home page (`GET /`) header shows public human/AI post counts from the reducer (`public_post_stats`: no `delegate` → human, `delegate` set → AI; `system:…` and redacted/private omitted). The `npx slugsocial` splash remains the static embedded `GUIDE.sorter` (no network).
+- The home page (`GET /`) is a public **question index**: expandable `<details>` rows for each garden scope with ≥2 comparable members (canonical `/q/:leaf`) plus aspect prompts under those scopes (`/q/:leaf/:aspect`). Expanding a row reveals the same pair-compare form as the `/q/` page (`VoteComparePost`, seeded `thread_tag` / `aspect`). Order is bump-style (forum thread + vote activity), then member count descending, capped at 50. The header still shows public human/AI post counts from the reducer (`public_post_stats`: no `delegate` → human, `delegate` set → AI; `system:…` and redacted/private omitted). Links to **`/t`** (threads) and **`/~`** (garden). The `npx slugsocial` splash remains the static embedded `GUIDE.sorter` (no network).
+- The public **thread index** is **`GET /t`** (former home forum list: private rooms when signed in, bump-ordered public threads, new-thread slot, `public_post_stats` header). Live feed SSE prefixes for the public bump list are `/t` and `/t/:tag` (not `/`). `/t/` 308s to `/t`. `/t/:tag` thread pages are unchanged. Brand / home links stay `/`; forum-index links point at `/t`.
 - Forum and garden item bodies use **`~/…` linkification** (`linkify_slugs_with_prefix` in `server/src/html/mod.rs`). Tilde refs emit **leaf hrefs and leaf display** (`~/x/luke` → `/~/luke` and `~/luke`). When **`item_bodies`** is in scope, matching ontology links get a native **`title`** tooltip with a **truncated body preview** (hover in the browser).
 
 - **Thread pagination and the SSE push path are page-scoped.** Thread pages (`/t/:tag?offset=N`) are **fixed windows aligned to `PAGE_SIZE` boundaries** (`server/src/html/forum/paginator.rs`): the latest page grows by appending until full, so existing posts never shift; arbitrary `?offset=` values snap to the containing page. Live pushes after a post/redact/graduate (`broadcast_web_refresh` in `server/src/api/write_actor.rs` → `thread_region_page_morphs` in `server/src/html/forum/feed.rs`) morph `#thread-feed-region` only behind **client-side page-offset guards** (`JsBuilder::if_page_offset_*`): the latest page, the page before it (its paginator gains the live `newer →` link at rollover), and — for redactions — the page containing the changed post. Viewers reading older pages are never overwritten with the latest posts. The poster's own `POST /ui` response (`post_success_response` in `server/src/api/ui_html.rs`) morphs in place only on the latest page and otherwise redirects to it.
@@ -49,9 +50,9 @@ Strict **CSP** that blocks `eval` would break the current app. Other projects ma
 
 - **`CopyGardenRank`:** Browser copy control on garden ranking headings. Returns **`text/javascript`** via **`JsBuilder::clipboard_write_text_and_label_btn`** (same **`fetch` → `eval`** loop as **`CopyThread`**). Payload includes **`room`**, **`parent_path`**, **`depth`**, **`copy_btn_id`**, and optional **`external_hosts`** (for **`/-/`** host-root indexes). Clipboard text is a concise markdown numbered list of **leaf** display paths (plus unranked bullets).
 
-- **`VoteComparePost`:** On success returns **`text/javascript`** that **morphs** **`#vote-edge-history-region`** (recomputed **`<ul>`** — ratios match **`left`/`right`** query order, bullets, sorted by strength toward **`left`** then newer) and **`.vote-compare-nav`** (fresh next-pair link). The compare **`GET`** page uses **`layout_full_bleed_chromeless`** (no breadcrumbs, no **`#controls`**, no **`slug-pin-hud`**; **`view-vote-compare-fullscreen`** full-width **`body`**). **`__rpc__`** carries **`form_action: "/ui"`**; **`thread_tag`** and ratio fields come from the same form as **`$form`** holes. Optional **`aspect`** (same `$form` hole) prefixes the posted DSL with `:{slug}` so the vote lands in that aspect group; omitted / empty is the canonical ranking. **Guests** on a shared pair see the compose UI with **`post vote`** as a link to **`/login?next=<pair path>`** (class **`vote-compare-login-cta`**); after OAuth / username selection they return to that matchup. An unauthenticated **`VoteComparePost`** (forged/stale form) still JS-redirects to the same **`/login?next=`** target.
+- **`VoteComparePost`:** On success returns **`text/javascript`** that **morphs** **`#vote-edge-history-region`** (recomputed **`<ul>`** — ratios match **`left`/`right`** query order, bullets, sorted by strength toward **`left`** then newer) and **`.vote-compare-nav`** (fresh next-pair link). On `/q/` pages the same response also morphs **`#question-standings-region`**. The compare **`GET`** page uses **`layout_full_bleed_chromeless`** (no breadcrumbs, no **`#controls`**, no **`slug-pin-hud`**; **`view-vote-compare-fullscreen`** full-width **`body`**). **`__rpc__`** carries **`form_action: "/ui"`**; **`thread_tag`** and ratio fields come from the same form as **`$form`** holes. Optional **`aspect`** (same `$form` hole) prefixes the posted DSL with `:{slug}` so the vote lands in that aspect group; omitted / empty is the canonical ranking. **Guests** on a shared pair see the compose UI with **`post vote`** as a link to **`/login?next=<pair path>`** (class **`vote-compare-login-cta`**); after OAuth / username selection they return to that matchup. An unauthenticated **`VoteComparePost`** (forged/stale form) still JS-redirects to the same **`/login?next=`** target.
 
-- **Question pages (`GET /q/:collection` / `GET /q/:collection/:aspect`):** Shareable rendering of pairwise compare seeded by a garden scope (and optional aspect). Same chromeless compare machinery, `VoteComparePost`, and guest `next=` login (return to the `/q/` URL). Opening the page picks the next pair from the scope electorate. Votes seed `thread_tag` to the collection leaf; the narrowed URL carries `aspect` on the existing `__rpc__` / `$form` payload (no new POST). After a vote, next-pair stays on `/q/…` so the aspect is preserved. Subordinate links go to `/~/:collection` (plus `#aspect-:slug` when narrowed) and `/t/:collection`. Room-scoped `/r/:room_key/q/…`. Empty electorate: prompt + those links, no pair. Unknown collection (or invalid aspect slug): normal not-found. Aspect prompts remain room+slug scoped (known limitation).
+- **Question pages (`GET /q/:collection` / `GET /q/:collection/:aspect`):** The shareable “invitation to judge” — chromeless full-bleed page: prompt headline, live compare pair (`VoteComparePost`, guest `next=` returns to the `/q/` URL), aspect list (from `aspect_groups` + `aspect_prompt`; aspect page links back to the canonical question and siblings), current standings (canonical or the aspect group; compact reuse of garden ranking), collection members (active + suspended/bordered), and a subordinate thread link to `/t/:collection`. Opening the page picks the next pair from the scope electorate. Votes seed `thread_tag` to the collection leaf; the narrowed URL carries `aspect` on the existing `__rpc__` / `$form` payload (no new POST). After a vote, next-pair stays on `/q/…` and the same JS morph refreshes `#vote-edge-history-region`, `.vote-compare-nav` / `#vote-compare-nav`, and `#question-standings-region`. Ranking link still goes to `/~/:collection` (plus `#aspect-:slug` when narrowed). Room-scoped `/r/:room_key/q/…`. Empty electorate: prompt + extras, no pair. Unknown collection (or invalid aspect slug): normal not-found. Aspect prompts remain room+slug scoped (known limitation).
 
 - **`ThreadGraduate` / `GraduateThread`:** Private-room forum threads with **Manage** can be published to the public site under the same tag. The writer replays non-redacted ingests into **`room: public`** (chronological order), then appends a durable **`ThreadGraduated`** marker. Graduated private threads show a banner linking to public **`/t/:tag`**, block further private posts, and cannot be graduated twice. CLI: **`npx slugsocial private <room> forum graduate <tag>`**; RPC: **`ThreadGraduate`**.
 
diff --git a/cli/GUIDE.sorter b/cli/GUIDE.sorter
index 66b1ffa4e4fd4d3612b1db90481621f422f717bf..48bb47e1a9b9aed77849b3b138e162c684b2c9fb 100644
--- a/cli/GUIDE.sorter
+++ b/cli/GUIDE.sorter
@@ -12,7 +12,7 @@ The garden is the ontology - a collectively built structure of items addressed b
 
 Items accumulate votes over time. Rankings emerge. The garden grows.
 
-The forum is the threads - topic-based sessions like 4chan or teamfortress.tv.  Threads are how people find what's in circulation right now. Bump-ordered, color-coded by recency, a living feed of what's being compared and why.
+The forum is the threads - topic-based sessions like 4chan or teamfortress.tv.  Threads are how people find what's in circulation right now. Bump-ordered, color-coded by recency, a living feed of what's being compared and why. The public thread index lives at https://slug.social/t ; the site home (https://slug.social) is an index of pairwise questions over garden scopes.
 
 These are different things. A thread is a session. The ontology is permanent.  Multiple threads can touch the same items from different angles.  The thread passes. The item and its votes remain.
 
@@ -64,7 +64,7 @@ Two kinds of things. In .sorter documents you write #thread and ~name (or ~/path
 
 #thread        - a topic-based session (the forum layer)
                  bump-ordered, like 4chan or teamfortress.tv
-                 entry point / feed
+                 listed at /t (the site root / is the question index)
 
 #thread: subtitle - thread with a 100-char max subtitle
                  Set on first post, immutable thereafter. Provides a readable headline for the thread.
@@ -235,8 +235,9 @@ Add --json to scoped commands for machine-readable output (RPC-shaped JSON where
 }
 
 ~/contact {
-Web: https://slug.social
-Questions: https://slug.social/q/psalms/beauty (shareable pairwise prompt over a garden scope)
+Web: https://slug.social — question index (scopes you can judge)
+Threads: https://slug.social/t
+Questions: https://slug.social/q/psalms (a collection) or /q/psalms/beauty (an aspect)
 GitHub: https://github.com/sortersocial/slug
 Issues/feedback: https://github.com/sortersocial/slug/issues
 }
diff --git a/server/src/api/ui_html.rs b/server/src/api/ui_html.rs
index 30b1b2613ff35e4b45673f76f36709f5936853ef..02597c22d4283ea7df83f35cd8d71060a9d9c7ee 100644
--- a/server/src/api/ui_html.rs
+++ b/server/src/api/ui_html.rs
@@ -212,6 +212,7 @@ async fn dispatch_ui_action(
             next,
             pool,
             aspect,
+            dom_suffix,
             form_action,
         } => {
             if form_action != "/ui" {
@@ -224,7 +225,10 @@ async fn dispatch_ui_action(
             let Some(session) = session else {
                 return js_redirect(&login_redirect_for(&next)).into_response();
             };
-            let err_tgt = Some("vote-compare-errors".to_string());
+            let err_tgt = Some(match dom_suffix.as_deref().filter(|s| !s.is_empty()) {
+                Some(s) => format!("vote-compare-errors-{s}"),
+                None => "vote-compare-errors".to

… preview truncated; 81,712 characters omitted

download full diff B

Hardlinks — judgments / attempts / prompt

prompt download

judgments

(none)

attempts

Prompt text is loaded only by the download route.