Side A makes several coordinated, verifiable fixes (correct out-of-band delegate passing, error-instead-of-silent-fallback for small sibling pools, dead button removal, route rename) with updates propagated consistently across server code, tests, and browser test scripts. Side B is a minimal one-line fix defining a missing env var to resolve a NameError, which is valid but much narrower in scope and impact than Side A's multi-part correctness and UX improvements.
constitution · epochs · watch · epoch 3
c_11d6a0ec9839 (tommy-mor) vs c_4ed2e2cca39b (tommy-mor)
download prompt · raw event · cmp_a94a5d2cb0eb5c
council reasoning
A delivers several lasting behavioral fixes: out-of-band delegate attribution, correct early-return when the sibling pool is too small (instead of silently falling back to all items), and removal of a useless swap control, plus the route rename. B only adds a missing GITHUB_API_BASE_URL default—important for avoiding a NameError, but a three-line constant definition with far less ongoing design impact.
Side A fixes several functional issues across the voting flow: it correctly passes the delegate attribution out-of-band instead of embedding it in the DSL text, stops incorrectly falling back to all items when the sibling pool is too small by returning no next pair, removes the unnecessary swap control, and consistently renames the route from `/vote/compare` to `/vote` across handlers and tests. Side B fixes a real but narrowly scoped startup/runtime bug by defining `GITHUB_API_BASE_URL` with a default, preventing a NameError in the GitHub OAuth path.
sides
A — c_11d6a0ec9839 (tommy-mor)
message
[36e92cf2] Fix /vote/compare page: attribution, fallback, swap button, and route rename. - Pass WEB_BROWSER_AGENT as delegate_opt out-of-band (not in DSL body text) - Error instead of falling back to all items when sibling pool < 2 - Remove pointless "swap sides" button - Rename route /vote/compare → /vote Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
diff preview
diff --git a/server/src/api/rpc.rs b/server/src/api/rpc.rs
index 0b7070a2b7289e5c40778c8d062bd4150285a8df..8e3d8e34922ddaaa89ea699edd99402bd40fdb1b 100644
--- a/server/src/api/rpc.rs
+++ b/server/src/api/rpc.rs
@@ -281,6 +281,7 @@ pub async fn rpc_post_with_bearer(
bearer_token: &str,
room: String,
thread_tag: String,
+ delegate_opt: Option<String>,
text: String,
) -> Result<RpcResult, RpcErr> {
use axum::http::{header, HeaderMap, HeaderValue};
@@ -288,7 +289,7 @@ pub async fn rpc_post_with_bearer(
let hv = HeaderValue::from_str(&format!("Bearer {bearer_token}"))
.map_err(|_| ("invalid session token".into(), None))?;
headers.insert(header::AUTHORIZATION, hv);
- rpc_post(state, &headers, room, thread_tag, None, text, false).await
+ rpc_post(state, &headers, room, thread_tag, delegate_opt, text, false).await
}
async fn rpc_check(
diff --git a/server/src/api/ui_html.rs b/server/src/api/ui_html.rs
index a13a94767d8905adc09e31effff838e166781b6c..5faa642451d69555cb391974beb0ad22c9355c8c 100644
--- a/server/src/api/ui_html.rs
+++ b/server/src/api/ui_html.rs
@@ -118,6 +118,7 @@ async fn dispatch_ui_action(
&session.bearer,
room.clone(),
thread_tag.clone(),
+ None,
text,
)
.await
@@ -246,8 +247,7 @@ async fn dispatch_ui_action(
}
let text = format!(
- "@{}\n{{\n{}\n}}\n{} {}:{} {}\n",
- crate::api::auth::WEB_BROWSER_AGENT,
+ "{{\n{}\n}}\n{} {}:{} {}\n",
exp,
left_id.as_str(),
rl,
@@ -260,6 +260,7 @@ async fn dispatch_ui_action(
&session.bearer,
room.clone(),
thread_tag.clone(),
+ Some(crate::api::auth::WEB_BROWSER_AGENT.to_string()),
text,
)
.await
diff --git a/server/src/html/forum/nav.rs b/server/src/html/forum/nav.rs
index d33dc50f0b89a5ed7730c3bca204a67f6309388f..ae67f550a4bdf0bbeff58384c29a715015b64bd5 100644
--- a/server/src/html/forum/nav.rs
+++ b/server/src/html/forum/nav.rs
@@ -85,7 +85,7 @@ impl ThreadNav {
format!("{}/{}/{}", self.thread_path_prefix, tag, idx)
}
- /// Empty for public; `/r/:seg` for room — prefix for routes like `/vote/compare`.
+ /// Empty for public; `/r/:seg` for room — prefix for routes like `/vote`.
pub(crate) fn room_path_prefix_for_vote_compare(&self) -> String {
match &self.scope {
ScopeId::Public => String::new(),
diff --git a/server/src/html/garden/vote.rs b/server/src/html/garden/vote.rs
index bdb4008c29ba49d7dc97b1cd2c32d9cdfce1ca04..1d7fc8aa7436bfa9c1186dfd940a8d751ab7e088 100644
--- a/server/src/html/garden/vote.rs
+++ b/server/src/html/garden/vote.rs
@@ -208,7 +208,7 @@ pub(crate) async fn vote_compare_post_success_js(
state: &AppState,
nav: &ThreadNav,
_room_wire: &str,
- thread_tag: &str,
+ _thread_tag: &str,
left: &ItemId,
right: &ItemId,
_post_id: &str,
@@ -218,8 +218,7 @@ pub(crate) async fn vote_compare_post_success_js(
let content = content_for_garden_view(&reduced, &nav.scope());
let edge_history = vote_edge_history_markup(content, left, right);
let next_pair = suggest_next_vote_pair(content, left, right);
- let nav_markup =
- vote_compare_nav_markup(nav, next_pair.as_ref(), left, right, Some(thread_tag));
+ let nav_markup = vote_compare_nav_markup(nav, next_pair.as_ref());
drop(reduced);
JsBuilder::new()
.morph_inner_selector("#vote-edge-history-region", edge_history)
@@ -236,7 +235,7 @@ pub(super) fn vote_compare_href(
let left_q = urlencoding::encode(left.as_str());
let right_q = urlencoding::encode(right.as_str());
let base = format!(
- "{}/vote/compare?left={}&right={}",
+ "{}/vote?left={}&right={}",
nav.room_path_prefix_for_vote_compare(),
left_q,
right_q
@@ -251,12 +250,8 @@ pub(super) fn vote_compare_href(
fn vote_compare_nav_markup(
nav: &ThreadNav,
next_pair: Option<&(ItemId, ItemId)>,
- left: &ItemId,
- right: &ItemId,
- thread_override: Option<&str>,
) -> maud::Markup {
let next_pair_href = next_pair.map(|(nl, nr)| vote_compare_href(nav, nl, nr, None));
- let swap_pair_href = vote_compare_href(nav, right, left, thread_override);
html! {
div class="vote-compare-nav" {
@if let Some(href) = &next_pair_href {
@@ -264,7 +259,6 @@ fn vote_compare_nav_markup(
} @else {
span class="vote-compare-next is-disabled" { "no next pair" }
}
- a class="vote-compare-next" href=(swap_pair_href) { "swap sides" }
}
}
}
@@ -274,7 +268,7 @@ pub(super) fn suggest_next_vote_pair(
current_left: &ItemId,
current_right: &ItemId,
) -> Option<(ItemId, ItemId)> {
- let mut pool: Vec<ItemId> = if current_left.parent().as_ref().map(|p| p.as_str())
+ let pool: Vec<ItemId> = if current_left.parent().as_ref().map(|p| p.as_str())
== current_right.parent().as_ref().map(|p| p.as_str())
{
current_left
@@ -291,7 +285,7 @@ pub(super) fn suggest_next_vote_pair(
Vec::new()
};
if pool.len() < 2 {
- pool = content.items.iter().cloned().collect();
+ return None;
}
suggest_next_pair_in_pool(
&content.ranking_group,
@@ -334,7 +328,7 @@ pub struct VoteCompareQuery {
pub thread: Option<String>,
}
-/// Public pairwise vote UI — `/vote/compare?left=&right=&thread=`.
+/// Public pairwise vote UI — `/vote?left=&right=&thread=`.
pub async fn vote_compare_page(
State(state): State<AppState>,
Query(q): Query<VoteCompareQuery>,
@@ -426,7 +420,7 @@ async fn vote_compare_inner(
let next_path = uri
.path_and_query()
.map(|pq| pq.as_str().to_string())
- .unwrap_or_else(|| "/vote/compare".into());
+ .unwrap_or_else(|| "/vote".into());
let rpc_json = template_json_compact(&json!({
"action": "vote_compare_post",
@@ -462,7 +456,7 @@ async fn vote_compare_inner(
Some(&item_bodies_for_cards),
))
}
- (vote_compare_nav_markup(&nav, next_pair.as_ref(), &left, &right, q.thread.as_deref()))
+ (vote_compare_nav_markup(&nav, next_pair.as_ref()))
div id="vote-edge-history-region" {
(edge_history)
}
diff --git a/server/src/html/ui_action.rs b/server/src/html/ui_action.rs
index c7b643c8dde37dd1e3001f5ff716fe4f79d24c5e..dd5294202569454acf9f41b458718700bbf96f39 100644
--- a/server/src/html/ui_action.rs
+++ b/server/src/html/ui_action.rs
@@ -38,7 +38,7 @@ pub enum HtmlUiAction {
#[serde(default)]
form_id: Option<String>,
},
- /// Post a pairwise vote from `/vote/compare` (browser compose).
+ /// Post a pairwise vote from `/vote` (browser compose).
VoteComparePost {
room: String,
thread_tag: String,
diff --git a/server/src/lib.rs b/server/src/lib.rs
index ad8e31099c807fb5844acb16cd5086a2f19327a7..e102e73ec167fa35dcc22d4ec8c6f40202982429 100644
--- a/server/src/lib.rs
+++ b/server/src/lib.rs
@@ -82,9 +82,9 @@ pub fn create_app(state: AppState) -> Router {
.route("/u/:username", get(crate::html::user_profile_page))
.route("/try", get(crate::html::editor_page))
.route("/try/check", post(crate::html::editor_check))
- .route("/vote/compare", get(crate::html::vote_compare_page))
+ .route("/vote", get(crate::html::vote_compare_page))
.route(
- "/r/:room_key/vote/compare",
+ "/r/:room_key/vote",
get(crate::html::room_vote_compare_page),
)
.route("/~", get(crate::html::garden_index))
diff --git a/server/tests/integration_html.rs b/server/tests/integration_html.rs
index a25a456c7cafd74a69e65e0c2e8ed2bfb2e27498..b3e31376737b2d5aabb739f408984a1f5ae0c974 100644
--- a/server/tests/integration_html.rs
+++ b/server/tests/integration_html.rs
@@ -52,12 +52,12 @@ async fn test_view_counts_increment_and_display() {
.normalized_storage()
.to_storage_string();
let vote_q_right_first = format!(
- "/vote/compare?right={}&left={}",
+ "/vote?right={}&left={}",
urlencoding::encode(&right),
urlencoding::encode(&left)
);
let vote_q_left_first = format!(
- "/vote/compare?left={}&right={}",
+ "/vote?left={}&right={}",
urlencoding::encode(&left),
urlencoding::encode(&right)
);
@@ -148,7 +148,7 @@ https://github.com/ghvotehi/a/issues/10 {\n\
.normalized_storage()
.to_storage_string();
let q = format!(
- "/vote/compare?left={}&right={}",
+ "/vote?left={}&right={}",
urlencoding::encode(&left),
urlencoding::encode(&right)
);
diff --git a/server/tests/integration_ui.rs b/server/tests/integration_ui.rs
index 091afd922d28f48ab36aa9b9084aa825036d1f74..23db7b5672418d5bb0ab7529e05ef1f89e59062a 100644
--- a/server/tests/integration_ui.rs
+++ b/server/tests/integration_ui.rs
@@ -110,7 +110,7 @@ async fn test_choose_username_carries_redirect_next() {
.expect("pending session must exist");
pending.provider = Some("google".to_string());
pending.provider_id = Some("google-user-redirect".to_string());
- pending.redirect_next = Some("/vote/compare?left=%7E%2Fa&right=%7E%2Fb".to_string());
+ pending.redirect_next = Some("/vote?left=%7E%2Fa&right=%7E%2Fb".to_string());
}
let choose = client
@@ -122,7 +122,7 @@ async fn test_choose_username_carries_redirect_next() {
assert_eq!(choose.status(), reqwest::StatusCode::OK);
let body = choose.text().await.unwrap();
- assert!(body.contains("window.location = \"/vote/compare?left=%7E%2Fa&right=%7E%2Fb\""));
+ assert!(body.contains("window.location = \"/vote?left=%7E%2Fa&right=%7E%2Fb\""));
}
#[tokio::test]
diff --git a/test/browser_vote_compare.clj b/test/browser_vote_compare.clj
index 7cd1e467fdd8a4480df1be2afd78adb45b9cdc6d..a98e144756421e1fa5064db6a9fc0dd8b08471c7 100644
--- a/test/browser_vote_compare.clj
+++ b/test/browser_vote_compare.clj
@@ -66,7 +66,7 @@
:headers {"Authorization" (str "Bearer " alice-token)})
post-json (json/parse-string (:body post-resp) false)
_ (is (true? (get-in post-json ["results" 0 "ok"])) "seed items + edge vote via rpc")
- cmp-url (str base-url "/vote/compare?left=" (enc left-url) "&right=" (enc third-url))]
+ cmp-url (str base-url "/vote?left=" (enc left-url) "&right=" (enc third-url))]
(core/with-playwright [pw]
(core/with-browser [browser (core/launch-chromium pw {:headless true :channel "chrome"})]
(core/with-context [ctx (core/new-context browser)]
B — c_4ed2e2cca39b (tommy-mor)
message
[0d3270d1] Fix GitHub OAuth callback NameError on missing API base URL. Co-authored-by: Cursor <cursoragent@cursor.com>
diff preview
diff --git a/constitution.py b/constitution.py
index f819007252f435680b8356fb4da83469b21e33af..4dd5b9dfbba231d46289c490f91b1dd5b1018bcf 100644
--- a/constitution.py
+++ b/constitution.py
@@ -119,6 +119,9 @@ JSONL_PATH = pathlib.Path(os.environ.get("JSONL_PATH", "/data/ledger.jsonl"))
GITHUB_CLIENT_ID = os.environ.get("GITHUB_CLIENT_ID", "")
GITHUB_CLIENT_SECRET = os.environ.get("GITHUB_CLIENT_SECRET", "")
+GITHUB_API_BASE_URL = os.environ.get(
+ "GITHUB_API_BASE_URL", "https://api.github.com"
+).rstrip("/")
OPENROUTER_API_KEY = os.environ.get("OPENROUTER_API_KEY", "")
OPENROUTER_BASE_URL = os.environ.get("OPENROUTER_BASE_URL", "https://openrouter.ai").rstrip("/")
Hardlinks — judgments / attempts / prompt
judgments
attempts
Prompt text is loaded only by the download route.